From 6746d42380c390523a02435aa78e27154cdc0886 Mon Sep 17 00:00:00 2001 From: null Date: Mon, 6 Jul 2026 11:06:05 -0500 Subject: [PATCH] refactor(server): migrate 5 more services to TypeScript (.cts) webauthnService, cleanupService, backupService, bankSyncService, authService. Behavior-preserving. Verified: typecheck:server 0, check:server 0, suite 226/226. Co-Authored-By: Claude Opus 4.8 --- middleware/requireAuth.js | 2 +- routes/admin.js | 6 +- routes/auth.js | 8 +- routes/authOidc.js | 2 +- routes/dataSources.js | 2 +- routes/profile.js | 2 +- routes/status.js | 2 +- services/{authService.js => authService.cts} | 93 ++++++++----------- services/backupScheduler.cts | 2 +- .../{backupService.js => backupService.cts} | 90 +++++++++--------- ...bankSyncService.js => bankSyncService.cts} | 47 ++++------ services/bankSyncWorker.cts | 2 +- .../{cleanupService.js => cleanupService.cts} | 63 +++++-------- ...webauthnService.js => webauthnService.cts} | 32 ++++--- tests/authService.test.js | 2 +- tests/backupAndCleanup.test.js | 4 +- tests/bankSyncService.test.js | 2 +- tests/profileRoute.test.js | 2 +- workers/dailyWorker.js | 6 +- 19 files changed, 163 insertions(+), 206 deletions(-) rename services/{authService.js => authService.cts} (86%) rename services/{backupService.js => backupService.cts} (79%) rename services/{bankSyncService.js => bankSyncService.cts} (86%) rename services/{cleanupService.js => cleanupService.cts} (81%) rename services/{webauthnService.js => webauthnService.cts} (89%) diff --git a/middleware/requireAuth.js b/middleware/requireAuth.js index c212493..5eb3383 100644 --- a/middleware/requireAuth.js +++ b/middleware/requireAuth.js @@ -1,5 +1,5 @@ const crypto = require('crypto'); -const { getSessionUser, COOKIE_NAME, SINGLE_COOKIE_NAME, cookieOpts, publicUser, recordLogin } = require('../services/authService'); +const { getSessionUser, COOKIE_NAME, SINGLE_COOKIE_NAME, cookieOpts, publicUser, recordLogin } = require('../services/authService.cts'); const { getDb, getSetting } = require('../db/database'); const { standardizeError } = require('./errorFormatter'); diff --git a/routes/admin.js b/routes/admin.js index 126b3da..2e22a2e 100644 --- a/routes/admin.js +++ b/routes/admin.js @@ -4,7 +4,7 @@ const { getDb, rollbackMigration } = require('../db/database'); const { getBankSyncConfig, setBankSyncEnabled, setSyncIntervalHours, setSyncDays, setDebugLogging } = require('../services/bankSyncConfigService.cts'); const { getStatus: getBankSyncWorkerStatus } = require('../services/bankSyncWorker.cts'); const { isEnvKeyActive, keyFingerprint } = require('../services/encryptionService.cts'); -const { hashPassword } = require('../services/authService'); +const { hashPassword } = require('../services/authService.cts'); const { logAudit } = require('../services/auditService.cts'); const { createBackup, @@ -13,7 +13,7 @@ const { importBackupBuffer, listBackups, restoreBackup, -} = require('../services/backupService'); +} = require('../services/backupService.cts'); const { getScheduleStatus, runScheduledBackupNow, @@ -23,7 +23,7 @@ const { getCleanupStatus, runAllCleanup, validateAndApplySettings: applyCleanupSettings, -} = require('../services/cleanupService'); +} = require('../services/cleanupService.cts'); const { backupOperationLimiter } = require('../middleware/rateLimiter'); // All routes mounted at /api/admin (requireAuth + requireAdmin applied at server level) diff --git a/routes/auth.js b/routes/auth.js index b5b8894..9f4c092 100644 --- a/routes/auth.js +++ b/routes/auth.js @@ -10,7 +10,7 @@ function getAppVersion() { } const { getDb, getSetting, setSetting } = require('../db/database'); -const { login, logout, hashPassword, cookieOpts, COOKIE_NAME, SINGLE_COOKIE_NAME, rotateSessionId, invalidateOtherSessions, recordLogin, recordFailedLogin } = require('../services/authService'); +const { login, logout, hashPassword, cookieOpts, COOKIE_NAME, SINGLE_COOKIE_NAME, rotateSessionId, invalidateOtherSessions, recordLogin, recordFailedLogin } = require('../services/authService.cts'); const { decryptSecret } = require('../services/encryptionService.cts'); const { getCsrfToken } = require('../middleware/csrf'); const { requireAuth, requireAdmin } = require('../middleware/requireAuth'); @@ -205,7 +205,7 @@ router.post('/totp/challenge', async (req, res) => { } try { - const { createSession } = require('../services/authService'); + const { createSession } = require('../services/authService.cts'); const session = await createSession(userId); if (!session) return res.status(500).json(standardizeError('Failed to create session', 'SERVER_ERROR')); @@ -437,7 +437,7 @@ const { createRegistrationChallenge, verifyRegistration, createAuthenticationChallenge, verifyAuthentication, consumeLoginChallenge, getCredentials, deleteCredential, -} = require('../services/webauthnService'); +} = require('../services/webauthnService.cts'); // GET /api/auth/webauthn/status router.get('/webauthn/status', requireAuth, (req, res) => { @@ -570,7 +570,7 @@ router.post('/webauthn/challenge', async (req, res) => { return res.status(401).json(standardizeError('Security key verification failed.', 'AUTH_ERROR')); } - const { createSession } = require('../services/authService'); + const { createSession } = require('../services/authService.cts'); const s = await createSession(session.userId); if (!s) return res.status(500).json(standardizeError('Failed to create session', 'SERVER_ERROR')); diff --git a/routes/authOidc.js b/routes/authOidc.js index c298a0a..15c2f30 100644 --- a/routes/authOidc.js +++ b/routes/authOidc.js @@ -13,7 +13,7 @@ const express = require('express'); const router = express.Router(); -const { createSession, cookieOpts, COOKIE_NAME, recordLogin } = require('../services/authService'); +const { createSession, cookieOpts, COOKIE_NAME, recordLogin } = require('../services/authService.cts'); const { getOidcConfig, isOidcLoginActive, diff --git a/routes/dataSources.js b/routes/dataSources.js index c636b6a..47b5634 100644 --- a/routes/dataSources.js +++ b/routes/dataSources.js @@ -4,7 +4,7 @@ const router = require('express').Router(); const { getDb } = require('../db/database'); const { standardizeError } = require('../middleware/errorFormatter'); const { decorateDataSource, ensureManualDataSource } = require('../services/transactionService.cts'); -const { connectSimplefin, syncDataSource, backfillDataSource, disconnectDataSource } = require('../services/bankSyncService'); +const { connectSimplefin, syncDataSource, backfillDataSource, disconnectDataSource } = require('../services/bankSyncService.cts'); const { sanitizeErrorMessage } = require('../services/simplefinService.cts'); const { getBankSyncConfig } = require('../services/bankSyncConfigService.cts'); const { syncLimiter } = require('../middleware/rateLimiter'); diff --git a/routes/profile.js b/routes/profile.js index 462c128..d449933 100644 --- a/routes/profile.js +++ b/routes/profile.js @@ -6,7 +6,7 @@ const bcrypt = require('bcryptjs'); const { passwordLimiter } = require('../middleware/rateLimiter'); const { getDb, getSetting } = require('../db/database'); -const { hashPassword, invalidateOtherSessions, rotateSessionId, COOKIE_NAME, cookieOpts } = require('../services/authService'); +const { hashPassword, invalidateOtherSessions, rotateSessionId, COOKIE_NAME, cookieOpts } = require('../services/authService.cts'); const { getImportHistory } = require('../services/spreadsheetImportService'); const { logAudit } = require('../services/auditService.cts'); const { standardizeError } = require('../middleware/errorFormatter'); diff --git a/routes/status.js b/routes/status.js index 1857260..cc40cd0 100644 --- a/routes/status.js +++ b/routes/status.js @@ -4,7 +4,7 @@ const fs = require('fs'); const os = require('os'); const { getDb, getSetting } = require('../db/database'); const { getStatusRuntime, recordError } = require('../services/statusRuntime.cts'); -const { listBackups } = require('../services/backupService'); +const { listBackups } = require('../services/backupService.cts'); const { getScheduleStatus } = require('../services/backupScheduler.cts'); const { checkForUpdates } = require('../services/updateCheckService.cts'); const { getStatus: getBankSyncWorkerStatus } = require('../services/bankSyncWorker.cts'); diff --git a/services/authService.js b/services/authService.cts similarity index 86% rename from services/authService.js rename to services/authService.cts index bf7653b..172dce1 100644 --- a/services/authService.js +++ b/services/authService.cts @@ -1,3 +1,6 @@ +import type { Db } from '../types/db'; +import type { Req } from '../types/http'; + const crypto = require('crypto'); const bcrypt = require('bcryptjs'); const { getDb } = require('../db/database'); @@ -5,7 +8,7 @@ const { buildDeviceFingerprint } = require('./loginFingerprint.cts'); const { encryptSecret, decryptSecret } = require('./encryptionService.cts'); // Store SHA-256(token) in the DB; the raw token stays only in the cookie. -function hashSession(token) { +function hashSession(token: string): string { return crypto.createHash('sha256').update(token).digest('hex'); } @@ -14,18 +17,16 @@ const SINGLE_COOKIE_NAME = 'bt_single_session'; const SESSION_DAYS = 7; // Pre-computed hash used to equalize login timing when the account is unknown, -// inactive, or OIDC-only. Without it those paths skip bcrypt.compare and -// respond measurably faster, letting an attacker enumerate valid usernames. -// Cost factor 12 matches real password hashes. Computed once at module load. +// inactive, or OIDC-only. Cost factor 12 matches real password hashes. const TIMING_EQUALIZATION_HASH = bcrypt.hashSync(crypto.randomBytes(32).toString('hex'), 12); -function envFlag(name) { +function envFlag(name: string): boolean | null { const value = process.env[name]; if (value === undefined) return null; return ['1', 'true', 'yes', 'on'].includes(String(value).toLowerCase()); } -function requestLooksHttps(req) { +function requestLooksHttps(req?: Req): boolean { if (!req) return false; if (req.secure) return true; const proto = req.get?.('x-forwarded-proto') || req.headers?.['x-forwarded-proto']; @@ -34,11 +35,10 @@ function requestLooksHttps(req) { /** * Build session-cookie options. - * * COOKIE_SECURE=true/false is explicit. HTTPS=true keeps the old deployment knob. * Otherwise, mark cookies Secure only when the current request appears to be HTTPS. */ -function cookieOpts(req) { +function cookieOpts(req?: Req) { const cookieSecure = envFlag('COOKIE_SECURE'); const httpsSecure = envFlag('HTTPS'); const secure = cookieSecure !== null @@ -56,13 +56,12 @@ function cookieOpts(req) { }; } -async function login(username, password) { - const db = getDb(); +async function login(username: any, password: any) { + const db: Db = getDb(); const user = db.prepare('SELECT * FROM users WHERE username = ?').get(username); // Unknown, inactive, or OIDC-only account: still burn a bcrypt comparison so - // the response time is indistinguishable from a wrong password (no timing - // oracle for username enumeration). + // the response time is indistinguishable from a wrong password. if (!user || user.active === 0 || (user.auth_provider && user.auth_provider !== 'local')) { await bcrypt.compare(password, TIMING_EQUALIZATION_HASH); return null; @@ -70,10 +69,9 @@ async function login(username, password) { const valid = await bcrypt.compare(password, user.password_hash); // Return userId so the route can log the failed attempt against the known account. - // The external response is identical either way — no user enumeration. if (!valid) return { error: 'bad_password', userId: user.id }; - // TOTP is enabled — don't create a session yet; issue a short-lived challenge instead + // TOTP is enabled — issue a short-lived challenge instead of a session if (user.totp_enabled) { const { createChallenge } = require('./totpService.cts'); const challengeToken = createChallenge(getDb(), user.id); @@ -82,7 +80,7 @@ async function login(username, password) { // WebAuthn is enabled — issue a WebAuthn authentication challenge instead if (user.webauthn_enabled) { - const { createAuthenticationChallenge, createLoginChallenge } = require('./webauthnService'); + const { createAuthenticationChallenge, createLoginChallenge } = require('./webauthnService.cts'); const { options, challengeId } = await createAuthenticationChallenge(getDb(), user.id); const loginToken = createLoginChallenge(getDb(), user.id, challengeId); return { requires_webauthn: true, challenge_token: loginToken, webauthn_options: options }; @@ -91,7 +89,7 @@ async function login(username, password) { // Clean up expired sessions for this user before creating new session try { db.prepare("DELETE FROM sessions WHERE user_id = ? AND expires_at < datetime('now')").run(user.id); - } catch (err) { + } catch (err: any) { console.error('[cleanup-error] Failed to cleanup user expired sessions:', err.message); } @@ -110,8 +108,8 @@ async function login(username, password) { return { sessionId, user: publicUser(user) }; } -async function createSession(userId) { - const db = getDb(); +async function createSession(userId: any) { + const db: Db = getDb(); const user = db.prepare('SELECT * FROM users WHERE id = ?').get(userId); if (!user) return null; if (user.active === 0) return null; @@ -119,7 +117,7 @@ async function createSession(userId) { // Clean up expired sessions for this user before creating new session try { db.prepare("DELETE FROM sessions WHERE user_id = ? AND expires_at < datetime('now')").run(userId); - } catch (err) { + } catch (err: any) { console.error('[cleanup-error] Failed to cleanup user expired sessions:', err.message); } @@ -133,20 +131,19 @@ async function createSession(userId) { return { sessionId, user: publicUser(user) }; } -function logout(sessionId) { +function logout(sessionId: any): void { if (!sessionId) return; getDb().prepare('DELETE FROM sessions WHERE id = ?').run(hashSession(sessionId)); } /** * Regenerate session ID for security (e.g., on privilege escalation). - * This invalidates the old session and creates a new one with the same user. */ -function rotateSessionId(oldSessionId, userId) { +function rotateSessionId(oldSessionId: any, userId: any): string | null { if (!oldSessionId || !userId) return null; - - const db = getDb(); - + + const db: Db = getDb(); + // Verify the old session belongs to the user and is valid const existingSession = db.prepare('SELECT user_id FROM sessions WHERE id = ? AND expires_at > datetime(\'now\')').get(hashSession(oldSessionId)); if (!existingSession || existingSession.user_id !== userId) { @@ -172,7 +169,7 @@ function rotateSessionId(oldSessionId, userId) { return newSessionId; } -function getSessionUser(sessionId) { +function getSessionUser(sessionId: any): any { if (!sessionId) return null; const row = getDb().prepare(` SELECT u.id, u.username, u.display_name, u.role, u.must_change_password, u.first_login, @@ -184,11 +181,11 @@ function getSessionUser(sessionId) { return row || null; } -async function hashPassword(password) { +async function hashPassword(password: string): Promise { return bcrypt.hash(password, 12); } -function publicUser(u) { +function publicUser(u: any) { const displayName = u.display_name || null; return { id: u.id, @@ -209,11 +206,9 @@ function publicUser(u) { /** * Records a successful login with encrypted IP/UA and prunes older entries * so each user keeps at most 10 login history rows. - * Session fingerprint is stored for current-session detection. - * New device alerts and geolocation are resolved asynchronously. */ -function recordLogin(userId, ipAddress, userAgent, sessionId) { - const db = getDb(); +function recordLogin(userId: any, ipAddress: any, userAgent: any, sessionId: any): void { + const db: Db = getDb(); const device = buildDeviceFingerprint({ userAgent, ipAddress }); const encIp = ipAddress ? encryptSecret(ipAddress) : null; @@ -223,18 +218,18 @@ function recordLogin(userId, ipAddress, userAgent, sessionId) { : null; // Collect prior fingerprints before insert to detect new devices - let priorFingerprints; + let priorFingerprints: any[]; try { priorFingerprints = db.prepare(` SELECT device_fingerprint FROM user_login_history WHERE user_id = ? AND success = 1 ORDER BY logged_in_at DESC, id DESC LIMIT 10 - `).all(userId).map(r => r.device_fingerprint); + `).all(userId).map((r: any) => r.device_fingerprint); } catch { priorFingerprints = []; } - let insertedId; + let insertedId: any; db.transaction(() => { const result = db.prepare(` INSERT INTO user_login_history ( @@ -275,8 +270,8 @@ function recordLogin(userId, ipAddress, userAgent, sessionId) { const isPrivate = /^(10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|127\.|::1|localhost)/i.test(ipAddress); if (!isPrivate) { fetch(`http://ip-api.com/json/${ipAddress}?fields=status,city,country,regionName,isp`, { signal: AbortSignal.timeout(5000) }) - .then(r => r.json()) - .then(data => { + .then((r: Response) => r.json()) + .then((data: any) => { if (data.status !== 'success') return; const updDb = getDb(); updDb.prepare(` @@ -321,14 +316,11 @@ function recordLogin(userId, ipAddress, userAgent, sessionId) { /** * Records a failed login attempt (wrong password for a known account). - * Stored with success=0 so it shows in login history but doesn't count as - * a real session. Only called when the username maps to a real user — - * unknown usernames are not tracked to prevent user-enumeration side-channels. */ -function recordFailedLogin(userId, ipAddress, userAgent) { +function recordFailedLogin(userId: any, ipAddress: any, userAgent: any): void { if (!userId) return; try { - const db = getDb(); + const db: Db = getDb(); const device = buildDeviceFingerprint({ userAgent, ipAddress }); const encIp = ipAddress ? encryptSecret(ipAddress) : null; const encUa = userAgent ? encryptSecret(userAgent.slice(0, 500)) : null; @@ -367,17 +359,14 @@ function pruneExpiredSessions() { } /** - * Invalidate all sessions for a user except for a specific session ID - * @param {number} userId - User ID - * @param {string} keepSessionId - Session ID to keep (typically the current session) - * @returns {Object} Result object with changes count + * Invalidate all sessions for a user except for a specific session ID. */ -function invalidateOtherSessions(userId, keepSessionId) { +function invalidateOtherSessions(userId: any, keepSessionId: any) { if (!userId) return { changes: 0 }; - - const db = getDb(); - let result; - + + const db: Db = getDb(); + let result: any; + if (keepSessionId) { result = db.prepare( "DELETE FROM sessions WHERE user_id = ? AND id != ?" @@ -387,7 +376,7 @@ function invalidateOtherSessions(userId, keepSessionId) { "DELETE FROM sessions WHERE user_id = ?" ).run(userId); } - + return result; } diff --git a/services/backupScheduler.cts b/services/backupScheduler.cts index 62825fb..be896ce 100644 --- a/services/backupScheduler.cts +++ b/services/backupScheduler.cts @@ -3,7 +3,7 @@ import type { Db } from '../types/db'; const cron = require('node-cron'); const { getSetting, setSetting } = require('../db/database'); -const { applyScheduledRetention, createBackup } = require('./backupService'); +const { applyScheduledRetention, createBackup } = require('./backupService.cts'); interface ScheduleSettings { enabled: boolean; diff --git a/services/backupService.js b/services/backupService.cts similarity index 79% rename from services/backupService.js rename to services/backupService.cts index 819e5b1..e3fd666 100644 --- a/services/backupService.js +++ b/services/backupService.cts @@ -1,3 +1,6 @@ +// import required so Node type-strips this .cts (it has no other import). +import type { Db } from '../types/db'; + const crypto = require('crypto'); const fs = require('fs'); const path = require('path'); @@ -11,22 +14,22 @@ const BACKUP_ID_RE = /^(?:bill-tracker-backup|pre-restore|imported-backup|schedu // ─── Helper Functions ───────────────────────────────────────────────────────── -function ensureBackupDir() { +function ensureBackupDir(): void { fs.mkdirSync(BACKUP_DIR, { recursive: true, mode: 0o700 }); } -function timestampPart(date = new Date()) { +function timestampPart(date: Date = new Date()): string { return date.toISOString() .replace(/[-:]/g, '') .replace('T', '-') .replace(/\.\d{3}Z$/, `-${String(date.getMilliseconds()).padStart(3, '0')}Z`); } -function makeBackupId(prefix = 'bill-tracker-backup') { +function makeBackupId(prefix = 'bill-tracker-backup'): string { return `${prefix}-${timestampPart()}-${crypto.randomBytes(4).toString('hex')}.sqlite`; } -function assertValidBackupId(id) { +function assertValidBackupId(id: unknown): string { if ( typeof id !== 'string' || id.includes('/') || @@ -34,53 +37,44 @@ function assertValidBackupId(id) { path.isAbsolute(id) || !BACKUP_ID_RE.test(id) ) { - const err = new Error('Invalid backup id'); + const err = new Error('Invalid backup id') as any; err.status = 400; throw err; } return id; } -function backupPathForId(id) { +function backupPathForId(id: string): string { assertValidBackupId(id); ensureBackupDir(); const resolved = path.resolve(BACKUP_DIR, id); const relative = path.relative(BACKUP_DIR, resolved); if (relative.startsWith('..') || path.isAbsolute(relative)) { - const err = new Error('Invalid backup id'); + const err = new Error('Invalid backup id') as any; err.status = 400; throw err; } return resolved; } -/** - * Generates SHA-256 checksum for a file. - * @param {string} filePath - Path to the file - * @returns {string} - Hex-encoded SHA-256 hash - */ -function checksumFile(filePath) { +/** Generates SHA-256 checksum for a file. */ +function checksumFile(filePath: string): string { const hash = crypto.createHash('sha256'); hash.update(fs.readFileSync(filePath)); return hash.digest('hex'); } -/** - * Validates a backup file's SHA-256 checksum. - * @param {string} filePath - Path to the backup file - * @param {string} expectedChecksum - Expected SHA-256 hex digest - * @returns {boolean} - True if checksum matches - */ -function validateChecksum(filePath, expectedChecksum) { +/** Validates a backup file's SHA-256 checksum. */ +function validateChecksum(filePath: string, expectedChecksum: unknown): boolean { if (typeof expectedChecksum !== 'string' || !/^[a-f0-9]{64}$/i.test(expectedChecksum)) { return false; } - + const actualChecksum = checksumFile(filePath); return actualChecksum.toLowerCase() === expectedChecksum.toLowerCase(); } -function cleanupSqliteSidecars(filePath) { +function cleanupSqliteSidecars(filePath: string): void { for (const suffix of ['-wal', '-shm']) { try { const sidecar = `${filePath}${suffix}`; @@ -89,7 +83,7 @@ function cleanupSqliteSidecars(filePath) { } } -function metadataForFile(filePath) { +function metadataForFile(filePath: string): any { const id = path.basename(filePath); assertValidBackupId(id); const stat = fs.statSync(filePath); @@ -111,33 +105,33 @@ function metadataForFile(filePath) { }; } -function listBackups() { +function listBackups(): any[] { ensureBackupDir(); return fs.readdirSync(BACKUP_DIR) - .filter(name => BACKUP_ID_RE.test(name)) - .map(name => { + .filter((name: string) => BACKUP_ID_RE.test(name)) + .map((name: string) => { const filePath = backupPathForId(name); if (!fs.statSync(filePath).isFile()) return null; return metadataForFile(filePath); }) .filter(Boolean) - .sort((a, b) => b.modified_at.localeCompare(a.modified_at)); + .sort((a: any, b: any) => b.modified_at.localeCompare(a.modified_at)); } -function validateSqliteDatabase(filePath) { - let db; +function validateSqliteDatabase(filePath: string): void { + let db: any; try { db = new Database(filePath, { readonly: true, fileMustExist: true }); const result = db.pragma('integrity_check', { simple: true }); if (result !== 'ok') { - const err = new Error('Backup failed SQLite integrity check'); + const err = new Error('Backup failed SQLite integrity check') as any; err.status = 400; throw err; } db.prepare('SELECT name FROM sqlite_master LIMIT 1').get(); - } catch (err) { + } catch (err: any) { if (err.status) throw err; - const safeErr = new Error('Backup is not a valid SQLite database'); + const safeErr = new Error('Backup is not a valid SQLite database') as any; safeErr.status = 400; throw safeErr; } finally { @@ -146,7 +140,7 @@ function validateSqliteDatabase(filePath) { } } -async function createBackup(prefix = 'bill-tracker-backup') { +async function createBackup(prefix = 'bill-tracker-backup'): Promise { ensureBackupDir(); const db = getDb(); db.pragma('wal_checkpoint(TRUNCATE)'); @@ -156,7 +150,7 @@ async function createBackup(prefix = 'bill-tracker-backup') { const tempPath = `${finalPath}.partial`; if (fs.existsSync(finalPath) || fs.existsSync(tempPath)) { - const err = new Error('Backup file already exists'); + const err = new Error('Backup file already exists') as any; err.status = 409; throw err; } @@ -174,11 +168,11 @@ async function createBackup(prefix = 'bill-tracker-backup') { } } -async function importBackupBuffer(buffer, options = {}) { +async function importBackupBuffer(buffer: any, options: any = {}): Promise { ensureBackupDir(); if (!Buffer.isBuffer(buffer) || buffer.length === 0) { - const err = new Error('Backup upload is required'); + const err = new Error('Backup upload is required') as any; err.status = 400; throw err; } @@ -188,26 +182,26 @@ async function importBackupBuffer(buffer, options = {}) { const tempPath = `${finalPath}.upload`; if (fs.existsSync(finalPath) || fs.existsSync(tempPath)) { - const err = new Error('Backup file already exists'); + const err = new Error('Backup file already exists') as any; err.status = 409; throw err; } try { fs.writeFileSync(tempPath, buffer, { flag: 'wx', mode: 0o600 }); - + // SHA-256 checksum validation const providedChecksum = options.expectedChecksum; if (providedChecksum) { if (!validateChecksum(tempPath, providedChecksum)) { fs.unlinkSync(tempPath); cleanupSqliteSidecars(tempPath); - const err = new Error('Backup integrity verification failed: checksum mismatch'); + const err = new Error('Backup integrity verification failed: checksum mismatch') as any; err.status = 400; throw err; } } - + validateSqliteDatabase(tempPath); fs.renameSync(tempPath, finalPath); fs.chmodSync(finalPath, 0o600); @@ -219,10 +213,10 @@ async function importBackupBuffer(buffer, options = {}) { } } -function getBackupFile(id) { +function getBackupFile(id: string): { path: string; metadata: any } { const filePath = backupPathForId(id); if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) { - const err = new Error('Backup not found'); + const err = new Error('Backup not found') as any; err.status = 404; throw err; } @@ -232,25 +226,25 @@ function getBackupFile(id) { }; } -function deleteBackup(id) { +function deleteBackup(id: string) { const backup = getBackupFile(id); fs.unlinkSync(backup.path); cleanupSqliteSidecars(backup.path); return { deleted: true, id: backup.metadata.id, deleted_at: new Date().toISOString() }; } -function applyRetention(retentionCount, options = {}) { +function applyRetention(retentionCount: any, options: any = {}) { const keep = parseInt(retentionCount, 10); if (!Number.isInteger(keep) || keep < 1) return { deleted: [] }; const type = options.type || null; const backups = type - ? listBackups().filter(backup => backup.type === type) + ? listBackups().filter((backup: any) => backup.type === type) : listBackups(); // listBackups() is already sorted newest-first; delete everything beyond `keep`. const toDelete = backups.slice(keep); - const deleted = []; + const deleted: any[] = []; for (const backup of toDelete) { try { @@ -263,11 +257,11 @@ function applyRetention(retentionCount, options = {}) { return { deleted }; } -function applyScheduledRetention(retentionCount) { +function applyScheduledRetention(retentionCount: any) { return applyRetention(retentionCount, { type: 'scheduled' }); } -async function restoreBackup(id) { +async function restoreBackup(id: string): Promise { const source = getBackupFile(id); validateSqliteDatabase(source.path); diff --git a/services/bankSyncService.js b/services/bankSyncService.cts similarity index 86% rename from services/bankSyncService.js rename to services/bankSyncService.cts index c6c00b6..f28ac79 100644 --- a/services/bankSyncService.js +++ b/services/bankSyncService.cts @@ -1,5 +1,7 @@ 'use strict'; +import type { Db } from '../types/db'; + const { assertEncryptionReady, encryptSecret, decryptSecret } = require('./encryptionService.cts'); const { claimSetupToken, @@ -16,16 +18,18 @@ const { applyMerchantStoreMatches } = require('./merchantStoreMatchService.cts') const { autoMatchForUser } = require('./matchSuggestionService.cts'); const { getUserSettings } = require('./userSettings.cts'); -function sinceEpochDays(days) { +type Row = Record; + +function sinceEpochDays(days: number): number { return Math.floor((Date.now() - days * 86400 * 1000) / 1000); } -function safeErrorMessage(err) { +function safeErrorMessage(err: any): string { return sanitizeErrorMessage(err?.message || String(err || 'Sync failed')); } // Upsert a single financial account, return the local row. -function upsertAccount(db, accountRow) { +function upsertAccount(db: Db, accountRow: Row): Row { const existing = db.prepare(` SELECT id, monitored FROM financial_accounts WHERE data_source_id = ? AND provider_account_id = ? AND user_id = ? @@ -58,16 +62,9 @@ function upsertAccount(db, accountRow) { return { id: result.lastInsertRowid, monitored: 1 }; } -// Insert a transaction, or update one we previously stored as PENDING. A pending charge -// can change amount before it settles, and eventually flips pending → posted (gaining a -// real posted_date). A transaction we already recorded as posted is final and left alone; -// a row the user has matched or ignored is never touched. +// Insert a transaction, or update one we previously stored as PENDING. // Returns: 'inserted' | 'posted' (pending→settled) | 'updated' (pending refreshed) | 'skipped'. -function upsertTransaction(db, txRow) { - // Key on (user_id, provider_transaction_id) to match the UNIQUE dedup index — - // provider_transaction_id is deliberately stable across disconnect/reconnect, so - // this also lets the pending→posted / amount-refresh paths survive a reconnect - // (a data_source_id-keyed lookup would miss the prior row and skip the update). +function upsertTransaction(db: Db, txRow: Row): string { const existing = db.prepare(` SELECT id, pending, match_status FROM transactions WHERE user_id = ? AND provider_transaction_id = ? @@ -87,7 +84,7 @@ function upsertTransaction(db, txRow) { txRow.description, txRow.payee, txRow.memo, txRow.match_status, txRow.ignored, txRow.pending, txRow.raw_data, ); return 'inserted'; - } catch (err) { + } catch (err: any) { if (err.code === 'SQLITE_CONSTRAINT_UNIQUE' || (err.message || '').includes('UNIQUE')) { return 'skipped'; } @@ -114,12 +111,9 @@ function upsertTransaction(db, txRow) { return 'skipped'; } -async function runSync(db, userId, dataSource, { days, debug = false } = {}) { +async function runSync(db: Db, userId: number, dataSource: Row, { days, debug = false }: { days?: number; debug?: boolean } = {}): Promise { const accessUrl = decryptSecret(dataSource.encrypted_secret); const isFirstSync = !dataSource.last_sync_at; - // Explicit `days` param (e.g. backfill) takes precedence. - // Initial seed always uses the full SYNC_DAYS_EFFECTIVE window regardless of admin config. - // Routine syncs use the admin-configured sync_days (default 30); falls back to SYNC_DAYS_DEFAULT. const config = getBankSyncConfig(); const syncDays = days ?? (isFirstSync ? SYNC_DAYS_EFFECTIVE : (config.sync_days || SYNC_DAYS_DEFAULT)); const since = sinceEpochDays(syncDays); @@ -141,15 +135,12 @@ async function runSync(db, userId, dataSource, { days, debug = false } = {}) { let transactionsPosted = 0; // pending → settled this cycle let pendingCleared = 0; // stale pending rows pruned (re-posted under new id / dropped) - // Remove pending rows we hold for an account that are no longer in the feed — a pending - // charge that re-posted under a new id, or was dropped by the bank before settling. const pruneOrphanPending = db.prepare(` DELETE FROM transactions WHERE account_id = ? AND user_id = ? AND pending = 1 AND match_status = 'unmatched' AND provider_transaction_id NOT IN (SELECT value FROM json_each(?)) `); - // Store any errlist warnings alongside a successful sync so users can see them const partialError = raw._errlistSummary ? sanitizeErrorMessage(`Partial sync — some connections failed: ${raw._errlistSummary}`) : null; @@ -160,10 +151,6 @@ async function runSync(db, userId, dataSource, { days, debug = false } = {}) { WHERE id = ? AND user_id = ? `); - // Batch every account + transaction write and the source-status update into ONE - // commit (better-sqlite3 db.transaction): far fewer fsyncs on large syncs, and - // atomic — a mid-sync failure can't leave a half-written account. The whole - // block is synchronous (the async fetch already happened above). const ingest = db.transaction(() => { for (const rawAccount of accounts) { const accountRow = normalizeAccount(rawAccount, dataSource.id, userId); @@ -175,7 +162,7 @@ async function runSync(db, userId, dataSource, { days, debug = false } = {}) { if (localAccount.monitored === 0) continue; - const seenTxIds = []; + const seenTxIds: any[] = []; for (const rawTx of txList) { const txRow = normalizeTransaction( rawTx, localAccount.id, dataSource.id, userId, rawAccount.id, rawAccount.currency, @@ -226,7 +213,7 @@ async function runSync(db, userId, dataSource, { days, debug = false } = {}) { // ─── Public API ─────────────────────────────────────────────────────────────── -async function connectSimplefin(db, userId, setupToken) { +async function connectSimplefin(db: Db, userId: number, setupToken: any): Promise { assertEncryptionReady(); const accessUrl = await claimSetupToken(setupToken); @@ -240,7 +227,7 @@ async function connectSimplefin(db, userId, setupToken) { const dataSourceId = result.lastInsertRowid; const dataSource = db.prepare('SELECT * FROM data_sources WHERE id = ? AND user_id = ?').get(dataSourceId, userId); - let syncResult = { accountsUpserted: 0, transactionsNew: 0, transactionsSkip: 0 }; + let syncResult: any = { accountsUpserted: 0, transactionsNew: 0, transactionsSkip: 0 }; try { syncResult = await runSync(db, userId, dataSource); } catch (err) { @@ -255,7 +242,7 @@ async function connectSimplefin(db, userId, setupToken) { return { dataSource: decorateDataSource(fresh), ...syncResult }; } -async function syncDataSource(db, userId, dataSourceId, { debug } = {}) { +async function syncDataSource(db: Db, userId: number, dataSourceId: any, { debug }: { debug?: boolean } = {}): Promise { assertEncryptionReady(); const dataSource = db.prepare(` @@ -285,7 +272,7 @@ async function syncDataSource(db, userId, dataSourceId, { debug } = {}) { return { dataSource: decorateDataSource(fresh), ...syncResult }; } -async function backfillDataSource(db, userId, dataSourceId) { +async function backfillDataSource(db: Db, userId: number, dataSourceId: any): Promise { assertEncryptionReady(); const dataSource = db.prepare(` @@ -312,7 +299,7 @@ async function backfillDataSource(db, userId, dataSourceId) { return { dataSource: decorateDataSource(fresh), ...syncResult }; } -function disconnectDataSource(db, userId, dataSourceId) { +function disconnectDataSource(db: Db, userId: number, dataSourceId: any): void { const row = db.prepare(` SELECT id FROM data_sources WHERE id = ? AND user_id = ? AND provider = 'simplefin' `).get(dataSourceId, userId); diff --git a/services/bankSyncWorker.cts b/services/bankSyncWorker.cts index 3891df0..cd210d2 100644 --- a/services/bankSyncWorker.cts +++ b/services/bankSyncWorker.cts @@ -4,7 +4,7 @@ import type { Db } from '../types/db'; const { getDb } = require('../db/database'); const { getBankSyncConfig } = require('./bankSyncConfigService.cts'); -const { syncDataSource } = require('./bankSyncService'); +const { syncDataSource } = require('./bankSyncService.cts'); // Skip a source if it was synced less than this long ago (catches recent manual syncs) const MIN_SYNC_AGE_MS = 60 * 60 * 1000; // 1 hour diff --git a/services/cleanupService.js b/services/cleanupService.cts similarity index 81% rename from services/cleanupService.js rename to services/cleanupService.cts index d1f2b6b..d57409a 100644 --- a/services/cleanupService.js +++ b/services/cleanupService.cts @@ -1,21 +1,23 @@ 'use strict'; +import type { Db } from '../types/db'; + const os = require('os'); const fs = require('fs'); const path = require('path'); const { getDb, getSetting, setSetting } = require('../db/database'); -const { BACKUP_DIR } = require('./backupService'); +const { BACKUP_DIR } = require('./backupService.cts'); // ─── Helpers ───────────────────────────────────────────────────────────────── -function parseBool(key, defaultTrue = true) { +function parseBool(key: string, defaultTrue = true): boolean { const v = getSetting(key); if (v === null) return defaultTrue; return v !== 'false'; } -function parseIntSetting(key, fallback) { +function parseIntSetting(key: string, fallback: number): number { const v = parseInt(getSetting(key) || '', 10); return isNaN(v) ? fallback : v; } @@ -26,7 +28,7 @@ function parseIntSetting(key, fallback) { * Delete rows from import_sessions where expires_at has passed. * These are created by the spreadsheet import preview endpoint (24h TTL). */ -function pruneExpiredImportSessions() { +function pruneExpiredImportSessions(): number { const result = getDb() .prepare("DELETE FROM import_sessions WHERE expires_at <= datetime('now')") .run(); @@ -35,16 +37,8 @@ function pruneExpiredImportSessions() { /** * Remove stale export temp files from the OS temp directory. - * - * SQLite exports (user-db): bill-tracker-user-{id}-{ts}.sqlite - * Written by the /export/user-db route, deleted in the download callback. - * Server crash can leave orphans → swept with maxAgeHours. - * - * Excel exports (user-excel): bill-tracker-user-{id}-{ts}.xlsx - * Currently streamed in-memory (no disk file), but guard exists so that - * if the code path ever changes, xlsx files are always removed within 24 h. */ -function pruneStaleExportFiles(maxAgeHours) { +function pruneStaleExportFiles(maxAgeHours: number): { removed: number; errors: number } { const tmpDir = os.tmpdir(); const sqliteCutoff = maxAgeHours * 60 * 60 * 1000; const xlsxCutoff = 24 * 60 * 60 * 1000; // xlsx files must not outlive 24 h @@ -52,10 +46,10 @@ function pruneStaleExportFiles(maxAgeHours) { let removed = 0; let errors = 0; - let entries; + let entries: string[]; try { entries = fs.readdirSync(tmpDir); - } catch (err) { + } catch (err: any) { console.warn('[cleanup] Cannot read tmpdir:', err.message); return { removed, errors: 1 }; } @@ -84,10 +78,8 @@ function pruneStaleExportFiles(maxAgeHours) { /** * Remove orphaned .partial and .upload backup files from the backup directory. - * These are left behind only if the server crashes mid-backup or mid-import. - * Uses a 2-hour cutoff so an in-progress operation is never interrupted. */ -function pruneOrphanedBackupPartials(backupDir) { +function pruneOrphanedBackupPartials(backupDir: string): { removed: number; errors: number } { const cutoff = 2 * 60 * 60 * 1000; const now = Date.now(); let removed = 0; @@ -95,10 +87,10 @@ function pruneOrphanedBackupPartials(backupDir) { if (!backupDir || !fs.existsSync(backupDir)) return { removed, errors }; - let entries; + let entries: string[]; try { entries = fs.readdirSync(backupDir); - } catch (err) { + } catch (err: any) { console.warn('[cleanup] Cannot read backup dir:', err.message); return { removed, errors: 1 }; } @@ -121,10 +113,9 @@ function pruneOrphanedBackupPartials(backupDir) { } /** - * Trim import_history rows older than maxAgeDays. - * Rows are per-user audit records. This task is disabled by default. + * Trim import_history rows older than maxAgeDays. Disabled by default. */ -function pruneImportHistory(maxAgeDays) { +function pruneImportHistory(maxAgeDays: number): number { if (!maxAgeDays || maxAgeDays <= 0) return 0; const result = getDb() .prepare("DELETE FROM import_history WHERE imported_at < datetime('now', ?)") @@ -134,16 +125,10 @@ function pruneImportHistory(maxAgeDays) { /** * Permanently purge soft-deleted bills and categories after a 30-day recovery - * window. Bill deletion cascades to bill-owned records via foreign keys. - * - * transactions.matched_bill_id is ON DELETE SET NULL, so purging a bill nulls the - * pointer on any matched transaction but would leave match_status='matched' — a - * limbo row excluded from spending (match_status != 'matched') yet attributed to no - * bill. Release those matches back to 'unmatched' in the same transaction (and - * self-heal any pre-existing orphans) so purged bills don't silently drop spend. + * window. Also releases limbo matches back to 'unmatched' (see QA-B5-04). */ -function pruneSoftDeletedFinancialRecords(maxAgeDays = 30) { - const db = getDb(); +function pruneSoftDeletedFinancialRecords(maxAgeDays = 30): { bills: number; categories: number; releasedMatches: number } { + const db: Db = getDb(); const cutoff = `-${maxAgeDays} days`; const purge = db.transaction(() => { const releasedMatches = db.prepare(` @@ -175,17 +160,17 @@ function readSettings() { }; } -function validateAndApplySettings(input = {}) { - const toSave = {}; +function validateAndApplySettings(input: any = {}) { + const toSave: Record = {}; - const bool = (key, settingKey) => { + const bool = (key: string, settingKey: string) => { if (input[key] !== undefined) toSave[settingKey] = input[key] ? 'true' : 'false'; }; - const rangedInt = (key, settingKey, min, max, label) => { + const rangedInt = (key: string, settingKey: string, min: number, max: number, label: string) => { if (input[key] === undefined) return; const v = parseInt(input[key], 10); if (isNaN(v) || v < min || v > max) { - const err = new Error(`${label} must be between ${min} and ${max}`); + const err = new Error(`${label} must be between ${min} and ${max}`) as any; err.status = 400; throw err; } @@ -208,7 +193,7 @@ function validateAndApplySettings(input = {}) { async function runAllCleanup() { const settings = readSettings(); - const tasks = {}; + const tasks: Record = {}; if (settings.import_sessions_enabled) { const pruned = pruneExpiredImportSessions(); @@ -250,7 +235,7 @@ async function runAllCleanup() { function getCleanupStatus() { const raw = getSetting('cleanup_last_result'); - let last_result = null; + let last_result: any = null; try { if (raw) last_result = JSON.parse(raw); } catch {} return { diff --git a/services/webauthnService.js b/services/webauthnService.cts similarity index 89% rename from services/webauthnService.js rename to services/webauthnService.cts index 4d7f1d1..1f77e04 100644 --- a/services/webauthnService.js +++ b/services/webauthnService.cts @@ -1,5 +1,7 @@ 'use strict'; +import type { Db } from '../types/db'; + const crypto = require('crypto'); const { generateRegistrationOptions, @@ -11,11 +13,11 @@ const { const APP_NAME = 'Bill Tracker'; const CHALLENGE_TTL_MS = 15 * 60 * 1000; -function getRpId() { +function getRpId(): string { return process.env.WEBAUTHN_RP_ID || 'localhost'; } -function getOrigin() { +function getOrigin(): string { const o = process.env.WEBAUTHN_ORIGIN; if (o) return o; const port = process.env.PORT || 3000; @@ -24,7 +26,7 @@ function getOrigin() { // ── Registration ────────────────────────────────────────────────────────────── -async function createRegistrationChallenge(db, userId, username) { +async function createRegistrationChallenge(db: Db, userId: number, username: string) { let { webauthn_user_id } = db.prepare('SELECT webauthn_user_id FROM users WHERE id = ?').get(userId) || {}; if (!webauthn_user_id) { @@ -43,7 +45,7 @@ async function createRegistrationChallenge(db, userId, username) { userName: username, userDisplayName: username, attestationType: 'none', - excludeCredentials: existing.map(c => ({ id: c.credential_id })), + excludeCredentials: existing.map((c: any) => ({ id: c.credential_id })), authenticatorSelection: { residentKey: 'preferred', userVerification: 'preferred', @@ -61,7 +63,7 @@ async function createRegistrationChallenge(db, userId, username) { return { options, challengeId }; } -async function verifyRegistration(db, userId, challengeId, response, credentialName) { +async function verifyRegistration(db: Db, userId: number, challengeId: string, response: any, credentialName: any) { const row = db.prepare( "SELECT challenge FROM webauthn_challenges WHERE id = ? AND user_id = ? AND challenge_type = 'registration' AND expires_at > datetime('now')" ).get(challengeId, userId); @@ -98,7 +100,7 @@ async function verifyRegistration(db, userId, challengeId, response, credentialN db.prepare('DELETE FROM webauthn_challenges WHERE id = ?').run(challengeId); return { verified: true, credentialId: credential.id }; - } catch (err) { + } catch (err: any) { console.error('[webauthn] registration error:', err.message); return { verified: false, error: err.message }; } @@ -106,13 +108,13 @@ async function verifyRegistration(db, userId, challengeId, response, credentialN // ── Authentication ──────────────────────────────────────────────────────────── -async function createAuthenticationChallenge(db, userId) { +async function createAuthenticationChallenge(db: Db, userId: number) { const credentials = db.prepare('SELECT credential_id, transports FROM webauthn_credentials WHERE user_id = ?').all(userId); if (!credentials.length) throw new Error('No registered WebAuthn credentials'); const options = await generateAuthenticationOptions({ rpID: getRpId(), - allowCredentials: credentials.map(c => ({ + allowCredentials: credentials.map((c: any) => ({ id: c.credential_id, transports: c.transports ? JSON.parse(c.transports) : undefined, })), @@ -129,7 +131,7 @@ async function createAuthenticationChallenge(db, userId) { return { options, challengeId }; } -async function verifyAuthentication(db, userId, challengeId, response) { +async function verifyAuthentication(db: Db, userId: number, challengeId: string, response: any) { const row = db.prepare( "SELECT challenge FROM webauthn_challenges WHERE id = ? AND user_id = ? AND challenge_type = 'authentication' AND expires_at > datetime('now')" ).get(challengeId, userId); @@ -161,7 +163,7 @@ async function verifyAuthentication(db, userId, challengeId, response) { db.prepare('DELETE FROM webauthn_challenges WHERE id = ?').run(challengeId); return { verified: true }; - } catch (err) { + } catch (err: any) { console.error('[webauthn] authentication error:', err.message); return { verified: false, error: err.message }; } @@ -170,7 +172,7 @@ async function verifyAuthentication(db, userId, challengeId, response) { // ── Login challenge (mirrors totpService.createChallenge / consumeChallenge) ── // Issued after password passes; consumed when the WebAuthn assertion is verified. -function createLoginChallenge(db, userId, webauthnChallengeId) { +function createLoginChallenge(db: Db, userId: number, webauthnChallengeId: any): string { const id = crypto.randomUUID(); const expiresAt = new Date(Date.now() + CHALLENGE_TTL_MS).toISOString().slice(0, 19).replace('T', ' '); db.prepare("DELETE FROM webauthn_challenges WHERE user_id = ? AND challenge_type = 'login'").run(userId); @@ -180,7 +182,7 @@ function createLoginChallenge(db, userId, webauthnChallengeId) { return id; } -function consumeLoginChallenge(db, loginChallengeId) { +function consumeLoginChallenge(db: Db, loginChallengeId: any): { userId: number; authChallengeId: string } | null { const row = db.prepare( "SELECT user_id, challenge FROM webauthn_challenges WHERE id = ? AND challenge_type = 'login' AND expires_at > datetime('now')" ).get(loginChallengeId); @@ -191,19 +193,19 @@ function consumeLoginChallenge(db, loginChallengeId) { // ── Credential management ───────────────────────────────────────────────────── -function getCredentials(db, userId) { +function getCredentials(db: Db, userId: number): any[] { return db.prepare( 'SELECT id, credential_id, credential_name, aaguid, backup_eligible, backup_state, created_at FROM webauthn_credentials WHERE user_id = ? ORDER BY created_at DESC' ).all(userId); } -function deleteCredential(db, credentialId, userId) { +function deleteCredential(db: Db, credentialId: any, userId: number) { return db.prepare('DELETE FROM webauthn_credentials WHERE credential_id = ? AND user_id = ?').run(credentialId, userId); } // ── Cleanup ─────────────────────────────────────────────────────────────────── -function pruneExpiredChallenges(db) { +function pruneExpiredChallenges(db: Db): void { db.prepare("DELETE FROM webauthn_challenges WHERE expires_at <= datetime('now')").run(); } diff --git a/tests/authService.test.js b/tests/authService.test.js index 5afa7ea..282e1dc 100644 --- a/tests/authService.test.js +++ b/tests/authService.test.js @@ -17,7 +17,7 @@ const dbPath = path.join(os.tmpdir(), `bill-tracker-authsvc-${process.pid}.sqlit process.env.DB_PATH = dbPath; const { getDb, closeDb } = require('../db/database'); -const auth = require('../services/authService'); +const auth = require('../services/authService.cts'); const sha256 = (s) => crypto.createHash('sha256').update(s).digest('hex'); let db, userId, otherUserId; diff --git a/tests/backupAndCleanup.test.js b/tests/backupAndCleanup.test.js index 8fead82..0d00192 100644 --- a/tests/backupAndCleanup.test.js +++ b/tests/backupAndCleanup.test.js @@ -25,7 +25,7 @@ const { applyScheduledRetention, importBackupBuffer, checksumFile, -} = require('../services/backupService'); +} = require('../services/backupService.cts'); const { validateScheduleSettings, computeNextRun, @@ -35,7 +35,7 @@ const { pruneOrphanedBackupPartials, pruneStaleExportFiles, pruneSoftDeletedFinancialRecords, -} = require('../services/cleanupService'); +} = require('../services/cleanupService.cts'); // ── Teardown ───────────────────────────────────────────────────────────────── test.after(() => { diff --git a/tests/bankSyncService.test.js b/tests/bankSyncService.test.js index 16b768f..035bdf9 100644 --- a/tests/bankSyncService.test.js +++ b/tests/bankSyncService.test.js @@ -9,7 +9,7 @@ process.env.DB_PATH = dbPath; const { getDb, closeDb } = require('../db/database'); const { encryptSecret } = require('../services/encryptionService.cts'); -const { syncDataSource } = require('../services/bankSyncService'); +const { syncDataSource } = require('../services/bankSyncService.cts'); function createUser(db, suffix) { return db.prepare(` diff --git a/tests/profileRoute.test.js b/tests/profileRoute.test.js index 224eb4e..2bef2e5 100644 --- a/tests/profileRoute.test.js +++ b/tests/profileRoute.test.js @@ -8,7 +8,7 @@ const dbPath = path.join(os.tmpdir(), `bill-tracker-profile-route-test-${process process.env.DB_PATH = dbPath; const { getDb, closeDb } = require('../db/database'); -const { publicUser } = require('../services/authService'); +const { publicUser } = require('../services/authService.cts'); function createUser(db, suffix) { return db.prepare(` diff --git a/workers/dailyWorker.js b/workers/dailyWorker.js index c52c179..af70b23 100644 --- a/workers/dailyWorker.js +++ b/workers/dailyWorker.js @@ -3,10 +3,10 @@ const cron = require('node-cron'); const { getDb, getSetting } = require('../db/database'); const { buildTrackerRow, getCycleRange } = require('../services/statusService.cts'); -const { pruneExpiredSessions } = require('../services/authService'); -const { pruneExpiredChallenges: pruneWebAuthnChallenges } = require('../services/webauthnService'); +const { pruneExpiredSessions } = require('../services/authService.cts'); +const { pruneExpiredChallenges: pruneWebAuthnChallenges } = require('../services/webauthnService.cts'); const { runNotifications, runDriftNotifications } = require('../services/notificationService'); -const { runAllCleanup } = require('../services/cleanupService'); +const { runAllCleanup } = require('../services/cleanupService.cts'); const { markWorkerError, markWorkerStarted,