'use strict'; const fs = require('fs'); const path = require('path'); // better-sqlite3's better_sqlite3.node is architecture-specific (it links // against libnode.so, which is the real Android binary already loaded into // this process by nodejs-mobile-cordova). The copy under node_modules/ is // whichever architecture it happened to be built for last; replace it with // the prebuild matching this device's actual arch before anything requires // better-sqlite3. See scripts/build-better-sqlite3-arm.sh. (function installBetterSqlite3Prebuild() { const archDirs = { arm64: 'arm64-v8a', arm: 'armeabi-v7a', x64: 'x86_64', ia32: 'x86', }; const archDir = archDirs[process.arch]; if (!archDir) return; // Unknown arch — fall back to whatever's bundled. const prebuilt = path.join(__dirname, 'prebuilds', archDir, 'better_sqlite3.node'); const target = path.join(__dirname, 'node_modules', 'better-sqlite3', 'build', 'Release', 'better_sqlite3.node'); if (!fs.existsSync(prebuilt)) return; try { fs.mkdirSync(path.dirname(target), { recursive: true }); fs.copyFileSync(prebuilt, target); } catch (err) { console.error('[main.js] Failed to install better-sqlite3 prebuild for', process.arch, err); } })(); // Node 12.19 predates crypto.randomUUID (added in Node 14.17), used by // services/authService.js for session token IDs. const nodeCrypto = require('crypto'); if (typeof nodeCrypto.randomUUID !== 'function') { nodeCrypto.randomUUID = function () { const b = nodeCrypto.randomBytes(16); b[6] = (b[6] & 0x0f) | 0x40; b[8] = (b[8] & 0x3f) | 0x80; const hex = b.toString('hex'); return [ hex.slice(0, 8), hex.slice(8, 12), hex.slice(12, 16), hex.slice(16, 20), hex.slice(20), ].join('-'); }; } // Node 12.19 predates crypto.hkdfSync (added in Node 15.0), used by // services/encryptionService.js's deriveKey to derive the token-encryption // key from TOKEN_ENCRYPTION_KEY. Implements HKDF-Extract/Expand (RFC 5869) // via createHmac. if (typeof nodeCrypto.hkdfSync !== 'function') { nodeCrypto.hkdfSync = function (digest, ikm, salt, info, keylen) { const hashLen = nodeCrypto.createHash(digest).digest().length; const saltBuf = salt && salt.length ? Buffer.from(salt) : Buffer.alloc(hashLen, 0); const ikmBuf = Buffer.isBuffer(ikm) ? ikm : Buffer.from(ikm); const infoBuf = Buffer.isBuffer(info) ? info : Buffer.from(info || ''); const prk = nodeCrypto.createHmac(digest, saltBuf).update(ikmBuf).digest(); let t = Buffer.alloc(0); let okm = Buffer.alloc(0); let counter = 1; while (okm.length < keylen) { t = nodeCrypto .createHmac(digest, prk) .update(t) .update(infoBuf) .update(Buffer.from([counter])) .digest(); okm = Buffer.concat([okm, t]); counter++; } return okm.slice(0, keylen); }; } // nodejs-mobile-cordova 0.4.3 embeds Node 12.19 built without ICU, so // `Intl` is entirely undefined. Polyfill just the bits the server uses // (utils/money.js, routes/dataSources.js, routes/status.js) — en-US number // formatting with thousands separators, and a no-op DateTimeFormat whose // resolvedOptions().timeZone the callers already treat as optional. if (typeof global.Intl === 'undefined') { global.Intl = { NumberFormat: function (_locale, options) { const opts = options || {}; const minFrac = opts.minimumFractionDigits || 0; const maxFrac = Math.max(opts.maximumFractionDigits || 0, minFrac); this.format = function (value) { const fixed = Number(value).toFixed(maxFrac); let [intPart, fracPart] = fixed.split('.'); const negative = intPart.startsWith('-'); if (negative) intPart = intPart.slice(1); intPart = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ','); let out = (negative ? '-' : '') + intPart; if (fracPart) out += '.' + fracPart; return out; }; }, DateTimeFormat: function () { this.resolvedOptions = function () { return { timeZone: undefined }; }; }, }; } // Server-side paths, relative to this directory (the app's writable storage // on-device — nodejs-mobile-cordova copies nodejs-project here at runtime). process.env.DB_PATH = path.join(__dirname, 'data', 'bills.db'); process.env.BACKUP_PATH = path.join(__dirname, 'backups'); process.env.PORT = process.env.PORT || '3000'; process.env.BIND_HOST = '127.0.0.1'; // The Capacitor WebView's origin (androidScheme: 'https') is cross-origin // from this server's http://localhost:3000, so the LoadingScreen's // /api/health poll and the app's API calls need CORS allowed for it. process.env.CORS_ORIGIN = 'https://localhost'; const cordova = require('cordova-bridge'); // Wait for the WebView to hand over the device-bound encryption key (stored // in Android Keystore / iOS Keychain — see src/crypto.ts) before starting the // server, so encryptionService.js picks up TOKEN_ENCRYPTION_KEY on first use. cordova.channel.on('message', function (msg) { if (msg && msg.type === 'encryptionKey' && typeof msg.key === 'string') { process.env.TOKEN_ENCRYPTION_KEY = msg.key; require('./server/server.js'); } }); cordova.channel.post('message', { type: 'ready' });