diff --git a/db/database.js b/db/database.cts similarity index 99% rename from db/database.js rename to db/database.cts index bb2cc4f..1954daa 100644 --- a/db/database.js +++ b/db/database.cts @@ -1,3 +1,4 @@ +// @ts-nocheck — .cts via Node type-stripping; rich typing deferred (large dynamic DB/migration infra). const Database = require('better-sqlite3'); const path = require('path'); const fs = require('fs'); @@ -74,11 +75,11 @@ function isValidSqlDefinition(def) { } // Subscription catalog seed rows live in their own module (pure data, no logic). -const { SUBSCRIPTION_CATALOG_ROWS, SUBSCRIPTION_CATALOG_V2_ROWS } = require('./subscriptionCatalogSeed'); +const { SUBSCRIPTION_CATALOG_ROWS, SUBSCRIPTION_CATALOG_V2_ROWS } = require('./subscriptionCatalogSeed.cts'); // The versioned migration list lives in its own module (a factory injected with db + helpers). -const buildVersionedMigrations = require('./migrations/versionedMigrations'); +const buildVersionedMigrations = require('./migrations/versionedMigrations.cts'); // The legacy-reconcile migration list (same factory pattern, legacy DBs only). -const buildLegacyReconcileMigrations = require('./migrations/legacyReconcileMigrations'); +const buildLegacyReconcileMigrations = require('./migrations/legacyReconcileMigrations.cts'); function runSubscriptionCatalogV2Migration(database) { // Category fixes for existing rows diff --git a/db/migrations/legacyReconcileMigrations.js b/db/migrations/legacyReconcileMigrations.cts similarity index 99% rename from db/migrations/legacyReconcileMigrations.js rename to db/migrations/legacyReconcileMigrations.cts index b79ad5a..0647346 100644 --- a/db/migrations/legacyReconcileMigrations.js +++ b/db/migrations/legacyReconcileMigrations.cts @@ -1,3 +1,4 @@ +// @ts-nocheck — .cts via Node type-stripping; rich typing deferred (large dynamic DB/migration infra). 'use strict'; // The legacy-reconcile migration list, extracted from db/database.js. Same diff --git a/db/migrations/versionedMigrations.js b/db/migrations/versionedMigrations.cts similarity index 99% rename from db/migrations/versionedMigrations.js rename to db/migrations/versionedMigrations.cts index b64c3cc..7372373 100644 --- a/db/migrations/versionedMigrations.js +++ b/db/migrations/versionedMigrations.cts @@ -1,3 +1,4 @@ +// @ts-nocheck — .cts via Node type-stripping; rich typing deferred (large dynamic DB/migration infra). 'use strict'; // The versioned migration list, extracted from db/database.js to keep that diff --git a/db/subscriptionCatalogSeed.js b/db/subscriptionCatalogSeed.cts similarity index 99% rename from db/subscriptionCatalogSeed.js rename to db/subscriptionCatalogSeed.cts index 0326664..7bae7e2 100644 --- a/db/subscriptionCatalogSeed.js +++ b/db/subscriptionCatalogSeed.cts @@ -1,3 +1,4 @@ +// @ts-nocheck — .cts via Node type-stripping; rich typing deferred (large dynamic DB/migration infra). 'use strict'; // Seed rows for the subscription_catalog table, extracted from db/database.js diff --git a/middleware/csrf.js b/middleware/csrf.cts similarity index 52% rename from middleware/csrf.js rename to middleware/csrf.cts index 3694bb0..703c24e 100644 --- a/middleware/csrf.js +++ b/middleware/csrf.cts @@ -1,62 +1,39 @@ +import type { Req, Res, Next } from '../types/http'; + const crypto = require('crypto'); const { logAudit } = require('../services/auditService.cts'); // ───────────────────────────────────────────────────────────────────────────── -// CSRF Middleware -// Protects state-changing routes (POST, PUT, DELETE) from cross-site request -// forgery by validating tokens stored in session cookie. +// CSRF Middleware — double-submit token protection for state-changing routes. // ───────────────────────────────────────────────────────────────────────────── const CSRF_HEADER_NAME = 'x-csrf-token'; -// CSRF cookie httpOnly setting - configurable via environment variable -// Default: true — the SPA fetches the token from GET /api/auth/csrf-token and stores -// it in memory, so JavaScript does not need direct access to document.cookie. -// httpOnly=true removes the token from the XSS-accessible cookie surface while -// preserving the double-submit validation on the server. const CSRF_HTTP_ONLY = process.env.CSRF_HTTP_ONLY !== 'false'; // defaults to true - -// CSRF cookie sameSite setting - configurable via environment variable -// Options: 'lax', 'strict', 'none' -// Default: 'strict' (most secure) -// Set CSRF_SAME_SITE=lax for SPA cross-site scenarios const CSRF_SAME_SITE = process.env.CSRF_SAME_SITE || 'strict'; - -// CSRF cookie secure setting - configurable via environment variable -// Default: true (only send over HTTPS) -// Set CSRF_SECURE=false for HTTP development (NOT recommended for production) const CSRF_SECURE = process.env.CSRF_SECURE !== 'false'; // defaults to true - -// CSRF cookie name - configurable via environment variable -// Default: 'bt_csrf_token' -// Use CSRF_COOKIE_NAME to customize for multi-app deployments const CSRF_COOKIE_NAME = process.env.CSRF_COOKIE_NAME || 'bt_csrf_token'; -// Generate a cryptographically secure CSRF token -function generateCsrfToken() { +function generateCsrfToken(): string { return crypto.randomBytes(32).toString('hex'); } /** - * Detect HTTPS the same way services/authService.cookieOpts does: - * req.secure (honors trust proxy) with an x-forwarded-proto fallback for - * deployments where TRUST_PROXY is not configured. + * Detect HTTPS the same way services/authService.cookieOpts does. */ -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']; - return String(proto || '').split(',').map(s => s.trim()).includes('https'); + return String(proto || '').split(',').map((s: string) => s.trim()).includes('https'); } /** - * Get or create CSRF token for the current session. - * In the SPA's double-submit flow, tokens are stored in a readable cookie so - * client/api.js can copy the value into the x-csrf-token header. + * Get or create CSRF token for the current session (readable double-submit cookie). */ -function getCsrfToken(req, res) { +function getCsrfToken(req: Req, res: Res): string { let token = req.cookies?.[CSRF_COOKIE_NAME]; - + if (!token) { token = generateCsrfToken(); res.cookie(CSRF_COOKIE_NAME, token, { @@ -66,19 +43,14 @@ function getCsrfToken(req, res) { path: '/', }); } - + return token; } /** - * Validate CSRF token from request. - * Tokens can be provided via: - * - x-csrf-token header (API clients) - * - csrf_token body field (form submissions) - * Query-parameter tokens are deliberately NOT accepted — URLs leak into - * access logs, browser history, and Referer headers. + * Validate CSRF token from request (x-csrf-token header or csrf_token body field). */ -function validateCsrfToken(req) { +function validateCsrfToken(req: Req): boolean { const cookieToken = req.cookies?.[CSRF_COOKIE_NAME]; if (!cookieToken) return false; @@ -92,36 +64,27 @@ function validateCsrfToken(req) { } /** - * CSRF middleware - validates tokens for state-changing methods. - * Skips validation for: GET, HEAD, OPTIONS (safe methods) - * Requires token for: POST, PUT, DELETE, PATCH (state-changing) + * CSRF middleware — validates tokens for state-changing methods. */ -function csrfMiddleware(req, res, next) { +function csrfMiddleware(req: Req, res: Res, next: Next): any { // Exempt the login endpoint only — no session exists yet to hijack. - // Compare against originalUrl (sans query string) so a "/login" subpath on - // some other mounted router is NOT accidentally exempted. const fullPath = (req.originalUrl || '').split('?')[0]; if (fullPath === '/api/auth/login') { return next(); } - // Only validate state-changing methods if (!['POST', 'PUT', 'DELETE', 'PATCH'].includes(req.method)) { return next(); } - // Skip validation for OPTIONS (preflight) if (req.method === 'OPTIONS') { return next(); } - // Allow API routes to opt-in explicitly via a flag - // This allows flexibility for routes that use alternate auth (e.g., API keys) if (req.csrfSkip) { return next(); } - // Validate the CSRF token if (!validateCsrfToken(req)) { logAudit({ user_id: req.user?.id || null, action: 'csrf.failure', ip_address: req.ip, user_agent: req.get('user-agent') }); return res.status(403).json({ @@ -136,9 +99,8 @@ function csrfMiddleware(req, res, next) { /** * Attach CSRF token to response locals for template rendering. - * Frontend can access req.csrfToken() in templates. */ -function csrfTokenProvider(req, res, next) { +function csrfTokenProvider(req: Req, res: Res, next: Next): void { res.locals.csrfToken = getCsrfToken(req, res); next(); } diff --git a/middleware/errorFormatter.js b/middleware/errorFormatter.cts similarity index 60% rename from middleware/errorFormatter.js rename to middleware/errorFormatter.cts index 9d5328d..45c177f 100644 --- a/middleware/errorFormatter.js +++ b/middleware/errorFormatter.cts @@ -1,66 +1,52 @@ /** * Centralized Error Formatter Middleware - * + * * Standard error response format: - * { - * "error": "ValidationError", - * "message": "Human-readable description", - * "field": "optional-field-name", - * "code": "machine-readable-code" - * } + * { "error", "message", "field", "code" } */ +import type { Req, Res, Next } from '../types/http'; + const { ValidationError, formatError } = require('../utils/apiError.cts'); -/** - * Extract field name from various validation error patterns - */ -function extractFieldFromError(message) { +/** Extract field name from various validation error patterns */ +function extractFieldFromError(message: any): string | null { if (!message) return null; - - // Patterns like "field must be ..." or "field is ..." + const fieldMatch = message.match(/^(\w+)\s+(?:must be|is|are)\s+/i); if (fieldMatch) return fieldMatch[1]; - - // Patterns like "year must be..." or "month must be..." + const yearMonthMatch = message.match(/^(year|month|due_day|interest_rate|actual_amount|start_year|start_month|end_year|end_month)\s+must\b/i); if (yearMonthMatch) return yearMonthMatch[1]; - - // Patterns like "name is required" + const requiredMatch = message.match(/^(username|password|name|bill_id|category_id|due_day)\s+(?:is|are)\s+required/i); if (requiredMatch) return requiredMatch[1]; - + return null; } -/** - * Convert a plain error message/string into a standardized error object - */ -function standardizeError(message, code = 'VALIDATION_ERROR', fieldOverride = null) { +/** Convert a plain error message/string into a standardized error object */ +function standardizeError(message: any, code = 'VALIDATION_ERROR', fieldOverride: any = null) { const field = fieldOverride || extractFieldFromError(message); - + return { error: code, message: typeof message === 'string' ? message : String(message), field: field || null, - code: code + code: code, }; } -/** - * Express middleware that ensures all error responses follow standard format - */ -function errorFormatter(req, res, next) { +/** Express middleware that ensures all error responses follow standard format */ +function errorFormatter(req: Req, res: Res, next: Next): void { const originalJson = res.json; - - res.json = function(data) { - // If data is an error object (has error property), standardize it + + res.json = function(this: any, data: any) { if (data && typeof data === 'object' && !Array.isArray(data) && data.error && !data.success) { // Already standardized (machine-readable code + human message) — pass through if (typeof data.code === 'string' && data.code && typeof data.message === 'string') { return originalJson.call(this, data); } - // Use the human text as the message, never as the machine code const message = (typeof data.message === 'string' && data.message) ? data.message : data.error; const code = (typeof data.code === 'string' && data.code) ? data.code : 'ERROR'; const standardized = standardizeError(message, code, data.field); @@ -68,35 +54,23 @@ function errorFormatter(req, res, next) { } return originalJson.call(this, data); }; - + next(); } -/** - * Helper to format validation errors with proper field extraction - */ -function formatValidationError(message, field) { +function formatValidationError(message: any, field?: any) { return standardizeError(message, 'VALIDATION_ERROR', field); } -/** - * Helper to format not found errors - */ -function formatNotFoundError(message, field) { +function formatNotFoundError(message: any, field?: any) { return standardizeError(message, 'NOT_FOUND', field); } -/** - * Helper to format authentication errors - */ -function formatAuthError(message) { +function formatAuthError(message: any) { return standardizeError(message, 'AUTH_ERROR'); } -/** - * Helper to format forbidden/access errors - */ -function formatForbiddenError(message) { +function formatForbiddenError(message: any) { return standardizeError(message, 'FORBIDDEN'); } diff --git a/middleware/rateLimiter.js b/middleware/rateLimiter.cts similarity index 87% rename from middleware/rateLimiter.js rename to middleware/rateLimiter.cts index 5d2bcc6..ba38392 100644 --- a/middleware/rateLimiter.js +++ b/middleware/rateLimiter.cts @@ -1,15 +1,17 @@ 'use strict'; +import type { Req, Res } from '../types/http'; + const { rateLimit, ipKeyGenerator } = require('express-rate-limit'); -function makeLimiter(max, windowMs, message) { +function makeLimiter(max: number, windowMs: number, message: string): any { return rateLimit({ windowMs, max, standardHeaders: 'draft-7', legacyHeaders: false, // Override default handler so the response is always JSON, not HTML - handler(req, res) { + handler(req: Req, res: Res) { res.status(429).json({ error: message }); }, }); @@ -21,21 +23,19 @@ const loginLimiter = makeLimiter( 'Too many login attempts. Please try again in 15 minutes.', ); -// 5 FAILED login attempts per 15 minutes per username — layered on top of the -// per-IP limiter so a distributed attacker (or many clients behind one NAT/proxy -// sharing an IP bucket) cannot brute-force a single account. Successful logins -// don't count toward the limit, so legitimate users are unaffected. +// 5 FAILED login attempts per 15 minutes per username — layered on the per-IP +// limiter. Successful logins don't count, so legit users are unaffected. const loginUsernameLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 5, standardHeaders: 'draft-7', legacyHeaders: false, skipSuccessfulRequests: true, - keyGenerator: (req) => { + keyGenerator: (req: Req) => { const username = String(req.body?.username || '').trim().toLowerCase(); return username ? `user:${username}` : ipKeyGenerator(req); }, - handler(req, res) { + handler(req: Req, res: Res) { res.status(429).json({ error: 'Too many failed login attempts for this account. Please try again in 15 minutes.' }); }, }); @@ -88,8 +88,8 @@ const syncLimiter = rateLimit({ max: 10, standardHeaders: 'draft-7', legacyHeaders: false, - keyGenerator: (req) => req.user?.id?.toString() || ipKeyGenerator(req), - handler(req, res) { + keyGenerator: (req: Req) => req.user?.id?.toString() || ipKeyGenerator(req), + handler(req: Req, res: Res) { res.status(429).json({ error: 'Too many sync requests. Please try again in 15 minutes.' }); }, }); @@ -108,7 +108,7 @@ const allLimiters = [ syncLimiter, ]; -function resetStores() { +function resetStores(): void { for (const limiter of allLimiters) { if (limiter.store.reset) { limiter.store.reset(); diff --git a/middleware/requireAuth.js b/middleware/requireAuth.cts similarity index 83% rename from middleware/requireAuth.js rename to middleware/requireAuth.cts index 5eb3383..05793d6 100644 --- a/middleware/requireAuth.js +++ b/middleware/requireAuth.cts @@ -1,15 +1,16 @@ +import type { Req, Res, Next } from '../types/http'; + const crypto = require('crypto'); const { getSessionUser, COOKIE_NAME, SINGLE_COOKIE_NAME, cookieOpts, publicUser, recordLogin } = require('../services/authService.cts'); -const { getDb, getSetting } = require('../db/database'); -const { standardizeError } = require('./errorFormatter'); +const { getDb, getSetting } = require('../db/database.cts'); +const { standardizeError } = require('./errorFormatter.cts'); -function getSingleModeUser() { +function getSingleModeUser(): any { if (getSetting('auth_mode') !== 'single') return null; const userId = getSetting('default_user_id'); if (!userId) return null; - // Single-user mode: validate only that the configured user exists, - // is active, and has role 'user'. Sessions are not relevant here — - // single-user mode bypasses session auth entirely. + // Single-user mode: validate only that the configured user exists, is active, + // and has role 'user'. Sessions are not relevant here. const row = getDb().prepare(` SELECT id, username, display_name, role, must_change_password, first_login, active, is_default_admin, last_seen_version @@ -19,7 +20,7 @@ function getSingleModeUser() { return row ? publicUser(row) : null; } -function requireAuth(req, res, next) { +function requireAuth(req: Req, res: Res, next: Next): any { // Single-user mode: bypass session entirely, auto-attach the default user const singleUser = getSingleModeUser(); if (singleUser) { @@ -27,7 +28,6 @@ function requireAuth(req, res, next) { req.singleUserMode = true; // Track logins via a presence cookie so login history works without a real session. - // A new cookie = new browser/device visit → record a login entry. const existing = req.cookies?.[SINGLE_COOKIE_NAME]; if (existing) { req.singleSessionId = existing; @@ -56,7 +56,7 @@ function requireAuth(req, res, next) { next(); } -function requireUser(req, res, next) { +function requireUser(req: Req, res: Res, next: Next): any { if (req.user?.is_default_admin) { return res.status(403).json(standardizeError('Default admin account does not have tracker access', 'FORBIDDEN')); } @@ -66,7 +66,7 @@ function requireUser(req, res, next) { next(); } -function requireAdmin(req, res, next) { +function requireAdmin(req: Req, res: Res, next: Next): any { // In single-user mode the auto-attached user is never admin, // so admin routes naturally stay protected by session. if (req.user?.role !== 'admin') { diff --git a/middleware/securityHeaders.js b/middleware/securityHeaders.cts similarity index 50% rename from middleware/securityHeaders.js rename to middleware/securityHeaders.cts index 9955d7e..e64ecc7 100644 --- a/middleware/securityHeaders.js +++ b/middleware/securityHeaders.cts @@ -1,12 +1,11 @@ 'use strict'; +import type { Req, Res, Next } from '../types/http'; + const crypto = require('crypto'); -/** - * Generates a secure nonce for CSP policy. - * Call once per request to get a unique nonce. - */ -function getCspNonce(req) { +/** Generates a secure nonce for CSP policy. Call once per request. */ +function getCspNonce(req: Req): string { if (!req.cspNonce) { req.cspNonce = crypto.randomBytes(16).toString('base64'); } @@ -15,15 +14,9 @@ function getCspNonce(req) { /** * Applies baseline security response headers on every request. - * - * CSP notes: - * - No nonces. index.html is served via sendFile/static, so a per-request nonce - * is never injected into the markup and would accomplish nothing for scripts. - * Worse, per CSP3 the mere presence of a nonce makes 'unsafe-inline' ignored, - * which would silently block the inline styles Tailwind/Radix/framer-motion - * rely on. All scripts are external and covered by 'self'. + * (See original notes on why CSP uses no nonce — index.html is static.) */ -function securityHeaders(req, res, next) { +function securityHeaders(req: Req, res: Res, next: Next): void { const cspPolicy = `default-src 'self'; ` + `script-src 'self'; ` + @@ -35,26 +28,15 @@ function securityHeaders(req, res, next) { `form-action 'self'; ` + `base-uri 'self'; ` + `object-src 'none';`; - + res.setHeader('Content-Security-Policy', cspPolicy); - - // Prevent MIME-type sniffing (browsers must respect Content-Type) res.setHeader('X-Content-Type-Options', 'nosniff'); - - // Disallow embedding in iframes from other origins — prevents clickjacking res.setHeader('X-Frame-Options', 'SAMEORIGIN'); - - // Don't send full URL in Referer header to third-party origins res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin'); - - // Disallow cross-domain policy files (Flash/PDF legacy attack surface) res.setHeader('X-Permitted-Cross-Domain-Policies', 'none'); - - // Don't advertise the server tech stack res.removeHeader('X-Powered-By'); - // HSTS — only set when the app is explicitly configured to run over HTTPS. - // Setting this on HTTP breaks the site. Enable with HTTPS=true env var. + // HSTS — only when explicitly configured to run over HTTPS. if (process.env.HTTPS === 'true') { res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains'); } diff --git a/routes/aboutAdmin.cts b/routes/aboutAdmin.cts index a683f4e..6c7360f 100644 --- a/routes/aboutAdmin.cts +++ b/routes/aboutAdmin.cts @@ -3,7 +3,7 @@ import type { Req, Res, Next } from '../types/http'; const express = require('express'); const fs = require('fs'); const path = require('path'); -const { requireAuth, requireAdmin } = require('../middleware/requireAuth'); +const { requireAuth, requireAdmin } = require('../middleware/requireAuth.cts'); const router = express.Router(); @@ -490,7 +490,7 @@ router.post('/check-updates', requireAuth, requireAdmin, async (req: Req, res: R }); // QA-B16-01: opt-out control for the external version check. -const { getSetting, setSetting } = require('../db/database'); +const { getSetting, setSetting } = require('../db/database.cts'); // GET /api/about-admin/update-check-setting — is the external version check enabled? router.get('/update-check-setting', requireAuth, requireAdmin, (req: Req, res: Res) => { diff --git a/routes/admin.cts b/routes/admin.cts index 522a29f..e013432 100644 --- a/routes/admin.cts +++ b/routes/admin.cts @@ -2,7 +2,7 @@ import type { Req, Res, Next } from '../types/http'; const express = require('express'); const router = express.Router(); -const { getDb, rollbackMigration } = require('../db/database'); +const { getDb, rollbackMigration } = require('../db/database.cts'); const { getBankSyncConfig, setBankSyncEnabled, setSyncIntervalHours, setSyncDays, setDebugLogging } = require('../services/bankSyncConfigService.cts'); const { getStatus: getBankSyncWorkerStatus } = require('../services/bankSyncWorker.cts'); const { isEnvKeyActive, keyFingerprint } = require('../services/encryptionService.cts'); @@ -26,7 +26,7 @@ const { runAllCleanup, validateAndApplySettings: applyCleanupSettings, } = require('../services/cleanupService.cts'); -const { backupOperationLimiter } = require('../middleware/rateLimiter'); +const { backupOperationLimiter } = require('../middleware/rateLimiter.cts'); // All routes mounted at /api/admin (requireAuth + requireAdmin applied at server level) diff --git a/routes/analytics.cts b/routes/analytics.cts index eb5a434..0416cc2 100644 --- a/routes/analytics.cts +++ b/routes/analytics.cts @@ -2,7 +2,7 @@ import type { Req, Res, Next } from '../types/http'; const express = require('express'); const router = express.Router(); -const { standardizeError } = require('../middleware/errorFormatter'); +const { standardizeError } = require('../middleware/errorFormatter.cts'); const { getAnalyticsSummary } = require('../services/analyticsService.cts'); router.get('/summary', (req: Req, res: Res) => { diff --git a/routes/auth.cts b/routes/auth.cts index 43ee8de..1812c23 100644 --- a/routes/auth.cts +++ b/routes/auth.cts @@ -11,15 +11,15 @@ function getAppVersion() { return _appVersion; } -const { getDb, getSetting, setSetting } = require('../db/database'); +const { getDb, getSetting, setSetting } = require('../db/database.cts'); 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'); +const { getCsrfToken } = require('../middleware/csrf.cts'); +const { requireAuth, requireAdmin } = require('../middleware/requireAuth.cts'); const { getPublicOidcInfo } = require('../services/oidcService.cts'); const { ValidationError, formatError } = require('../utils/apiError.cts'); -const { standardizeError } = require('../middleware/errorFormatter'); -const { passwordLimiter } = require('../middleware/rateLimiter'); +const { standardizeError } = require('../middleware/errorFormatter.cts'); +const { passwordLimiter } = require('../middleware/rateLimiter.cts'); const { logAudit } = require('../services/auditService.cts'); // ───────────────────────────────────────── diff --git a/routes/bills.cts b/routes/bills.cts index d91f0f4..f86c65d 100644 --- a/routes/bills.cts +++ b/routes/bills.cts @@ -2,7 +2,7 @@ import type { Req, Res, Next } from '../types/http'; const express = require('express'); const router = express.Router(); -const { getDb, ensureUserDefaultCategories } = require('../db/database'); +const { getDb, ensureUserDefaultCategories } = require('../db/database.cts'); const { auditBillsForUser, categoryBelongsToUser, @@ -15,7 +15,7 @@ const { applyBalanceDelta, } = require('../services/billsService.cts'); const { amortizationSchedule, debtAprSnapshot } = require('../services/aprService.cts'); -const { standardizeError } = require('../middleware/errorFormatter'); +const { standardizeError } = require('../middleware/errorFormatter.cts'); const { validatePaymentInput, serializePayment } = require('../services/paymentValidation.cts'); const { addMerchantRule, syncBillPaymentsFromSimplefin, merchantMatches } = require('../services/billMerchantRuleService.cts'); const { normalizeMerchant } = require('../services/subscriptionService.cts'); diff --git a/routes/calendar.cts b/routes/calendar.cts index f710350..929b384 100644 --- a/routes/calendar.cts +++ b/routes/calendar.cts @@ -1,9 +1,9 @@ // @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services). import type { Req, Res, Next } from '../types/http'; const express = require('express'); -const { standardizeError } = require('../middleware/errorFormatter'); +const { standardizeError } = require('../middleware/errorFormatter.cts'); const router = express.Router(); -const { getDb } = require('../db/database'); +const { getDb } = require('../db/database.cts'); const { buildTrackerRow, getCycleRange } = require('../services/statusService.cts'); const { getUserSettings } = require('../services/userSettings.cts'); const { accountingActiveSql } = require('../services/paymentAccountingService.cts'); diff --git a/routes/categories.cts b/routes/categories.cts index 49814b0..f0e0f86 100644 --- a/routes/categories.cts +++ b/routes/categories.cts @@ -1,9 +1,9 @@ // @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services). import type { Req, Res, Next } from '../types/http'; const express = require('express'); -const { standardizeError } = require('../middleware/errorFormatter'); +const { standardizeError } = require('../middleware/errorFormatter.cts'); const router = express.Router(); -const { getDb, ensureUserDefaultCategories } = require('../db/database'); +const { getDb, ensureUserDefaultCategories } = require('../db/database.cts'); const { accountingActiveSql } = require('../services/paymentAccountingService.cts'); // GET /api/categories diff --git a/routes/dataSources.cts b/routes/dataSources.cts index 524476a..af6d87a 100644 --- a/routes/dataSources.cts +++ b/routes/dataSources.cts @@ -3,13 +3,13 @@ import type { Req, Res, Next } from '../types/http'; 'use strict'; const router = require('express').Router(); -const { getDb } = require('../db/database'); -const { standardizeError } = require('../middleware/errorFormatter'); +const { getDb } = require('../db/database.cts'); +const { standardizeError } = require('../middleware/errorFormatter.cts'); const { decorateDataSource, ensureManualDataSource } = require('../services/transactionService.cts'); 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'); +const { syncLimiter } = require('../middleware/rateLimiter.cts'); const VALID_TYPES = new Set(['manual', 'file_import', 'provider_sync']); const VALID_STATUSES = new Set(['active', 'inactive', 'error']); diff --git a/routes/export.cts b/routes/export.cts index 6d2babe..ad8492b 100644 --- a/routes/export.cts +++ b/routes/export.cts @@ -1,14 +1,14 @@ // @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services). import type { Req, Res, Next } from '../types/http'; const express = require('express'); -const { standardizeError } = require('../middleware/errorFormatter'); +const { standardizeError } = require('../middleware/errorFormatter.cts'); const router = express.Router(); const os = require('os'); const path = require('path'); const fs = require('fs'); const Database = require('better-sqlite3'); const xlsx = require('xlsx'); -const { getDb } = require('../db/database'); +const { getDb } = require('../db/database.cts'); const { fromCents } = require('../utils/money.mts'); // GET /api/export?year=2026&format=csv — or a date range: ?from=YYYY-MM-DD&to=YYYY-MM-DD diff --git a/routes/import.cts b/routes/import.cts index cb6233d..8b33789 100644 --- a/routes/import.cts +++ b/routes/import.cts @@ -3,7 +3,7 @@ import type { Req, Res, Next } from '../types/http'; 'use strict'; const express = require('express'); -const { standardizeError } = require('../middleware/errorFormatter'); +const { standardizeError } = require('../middleware/errorFormatter.cts'); const router = express.Router(); const { previewSpreadsheet, diff --git a/routes/matches.cts b/routes/matches.cts index 879b788..6f5dd6b 100644 --- a/routes/matches.cts +++ b/routes/matches.cts @@ -1,8 +1,8 @@ // @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services). import type { Req, Res, Next } from '../types/http'; const router = require('express').Router(); -const { standardizeError } = require('../middleware/errorFormatter'); -const { getDb } = require('../db/database'); +const { standardizeError } = require('../middleware/errorFormatter.cts'); +const { getDb } = require('../db/database.cts'); const { applyBankPaymentAsSourceOfTruth, reactivatePaymentsOverriddenBy } = require('../services/paymentAccountingService.cts'); const { listMatchSuggestions, diff --git a/routes/monthly-starting-amounts.cts b/routes/monthly-starting-amounts.cts index b084d7a..fd4375e 100644 --- a/routes/monthly-starting-amounts.cts +++ b/routes/monthly-starting-amounts.cts @@ -2,7 +2,7 @@ import type { Req, Res, Next } from '../types/http'; const express = require('express'); const router = express.Router(); -const { getDb } = require('../db/database'); +const { getDb } = require('../db/database.cts'); const { getCycleRange } = require('../services/statusService.cts'); const { accountingActiveSql } = require('../services/paymentAccountingService.cts'); const { toCents, fromCents } = require('../utils/money.mts'); diff --git a/routes/notifications.cts b/routes/notifications.cts index e1c63a4..bd2e87d 100644 --- a/routes/notifications.cts +++ b/routes/notifications.cts @@ -2,8 +2,8 @@ import type { Req, Res, Next } from '../types/http'; const express = require('express'); const router = express.Router(); -const { getDb, getSetting, setSetting } = require('../db/database'); -const { requireAuth, requireUser, requireAdmin } = require('../middleware/requireAuth'); +const { getDb, getSetting, setSetting } = require('../db/database.cts'); +const { requireAuth, requireUser, requireAdmin } = require('../middleware/requireAuth.cts'); const { sendTestEmail } = require('../services/notificationService.cts'); const { sendTestPush } = require('../services/notificationService.cts')._push || {}; const { decryptSecret } = require('../services/encryptionService.cts'); @@ -48,7 +48,7 @@ router.put('/admin', requireAuth, requireAdmin, (req: Req, res: Res) => { const hour = Number.isInteger(h) && h >= 0 && h <= 23 ? h : 6; setSetting('reminder_hour', String(hour)); try { - require('../workers/dailyWorker').rescheduleDailyWorker(); + require('../workers/dailyWorker.cts').rescheduleDailyWorker(); } catch (err) { console.error('[notifications] Failed to reschedule daily worker:', err.message); } diff --git a/routes/payments.cts b/routes/payments.cts index fae1abc..a697a72 100644 --- a/routes/payments.cts +++ b/routes/payments.cts @@ -1,9 +1,9 @@ // @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services). import type { Req, Res, Next } from '../types/http'; const express = require('express'); -const { standardizeError } = require('../middleware/errorFormatter'); +const { standardizeError } = require('../middleware/errorFormatter.cts'); const router = require('express').Router(); -const { getDb } = require('../db/database'); +const { getDb } = require('../db/database.cts'); const { computeBalanceDelta, applyBalanceDelta } = require('../services/billsService.cts'); const { validatePaymentInput, serializePayment } = require('../services/paymentValidation.cts'); const { getCycleRange, resolveDueDate } = require('../services/statusService.cts'); diff --git a/routes/profile.cts b/routes/profile.cts index b7bacae..bb52bbc 100644 --- a/routes/profile.cts +++ b/routes/profile.cts @@ -5,13 +5,13 @@ import type { Req, Res, Next } from '../types/http'; const express = require('express'); const router = express.Router(); const bcrypt = require('bcryptjs'); -const { passwordLimiter } = require('../middleware/rateLimiter'); +const { passwordLimiter } = require('../middleware/rateLimiter.cts'); -const { getDb, getSetting } = require('../db/database'); +const { getDb, getSetting } = require('../db/database.cts'); const { hashPassword, invalidateOtherSessions, rotateSessionId, COOKIE_NAME, cookieOpts } = require('../services/authService.cts'); const { getImportHistory } = require('../services/spreadsheetImportService.cts'); const { logAudit } = require('../services/auditService.cts'); -const { standardizeError } = require('../middleware/errorFormatter'); +const { standardizeError } = require('../middleware/errorFormatter.cts'); const { encryptSecret, decryptSecret } = require('../services/encryptionService.cts'); // All profile routes require authentication — enforced in server.js. diff --git a/routes/snowball.cts b/routes/snowball.cts index 71cf080..a4cdf86 100644 --- a/routes/snowball.cts +++ b/routes/snowball.cts @@ -2,8 +2,8 @@ import type { Req, Res, Next } from '../types/http'; const express = require('express'); const router = express.Router(); -const { getDb } = require('../db/database'); -const { standardizeError } = require('../middleware/errorFormatter'); +const { getDb } = require('../db/database.cts'); +const { standardizeError } = require('../middleware/errorFormatter.cts'); const { calculateSnowball, calculateAvalanche } = require('../services/snowballService.cts'); const { calculateMinimumOnly, debtAprSnapshot } = require('../services/aprService.cts'); const { serializeBill } = require('../services/billsService.cts'); diff --git a/routes/spending.cts b/routes/spending.cts index 281b891..07dd777 100644 --- a/routes/spending.cts +++ b/routes/spending.cts @@ -4,7 +4,7 @@ import type { Req, Res, Next } from '../types/http'; const express = require('express'); const router = express.Router(); -const { getDb } = require('../db/database'); +const { getDb } = require('../db/database.cts'); const { getSpendingSummary, getSpendingTransactions, categorizeTransaction, getSpendingBudgets, setSpendingBudget, diff --git a/routes/status.cts b/routes/status.cts index 65dcc12..41bd76b 100644 --- a/routes/status.cts +++ b/routes/status.cts @@ -4,7 +4,7 @@ const express = require('express'); const router = express.Router(); const fs = require('fs'); const os = require('os'); -const { getDb, getSetting } = require('../db/database'); +const { getDb, getSetting } = require('../db/database.cts'); const { getStatusRuntime, recordError } = require('../services/statusRuntime.cts'); const { listBackups } = require('../services/backupService.cts'); const { getScheduleStatus } = require('../services/backupScheduler.cts'); diff --git a/routes/subscriptions.cts b/routes/subscriptions.cts index 7fc84b5..6fbb8f4 100644 --- a/routes/subscriptions.cts +++ b/routes/subscriptions.cts @@ -2,8 +2,8 @@ import type { Req, Res, Next } from '../types/http'; const express = require('express'); const router = express.Router(); -const { getDb, ensureUserDefaultCategories } = require('../db/database'); -const { standardizeError } = require('../middleware/errorFormatter'); +const { getDb, ensureUserDefaultCategories } = require('../db/database.cts'); +const { standardizeError } = require('../middleware/errorFormatter.cts'); const { fromCents } = require('../utils/money.mts'); const { createSubscriptionFromRecommendation, diff --git a/routes/summary.cts b/routes/summary.cts index 489d6da..1e7d2fb 100644 --- a/routes/summary.cts +++ b/routes/summary.cts @@ -2,7 +2,7 @@ import type { Req, Res, Next } from '../types/http'; const express = require('express'); const router = express.Router(); -const { getDb } = require('../db/database'); +const { getDb } = require('../db/database.cts'); const { getCycleRange, resolveDueDate } = require('../services/statusService.cts'); const { getUserSettings } = require('../services/userSettings.cts'); const { accountingActiveSql } = require('../services/paymentAccountingService.cts'); diff --git a/routes/tracker.cts b/routes/tracker.cts index b94f741..fe4a0f8 100644 --- a/routes/tracker.cts +++ b/routes/tracker.cts @@ -3,7 +3,7 @@ import type { Req, Res, Next } from '../types/http'; const express = require('express'); const router = express.Router(); const { getTracker, getUpcomingBills, getOverdueCount } = require('../services/trackerService.cts'); -const { standardizeError } = require('../middleware/errorFormatter'); +const { standardizeError } = require('../middleware/errorFormatter.cts'); // GET /api/tracker/overdue-count — lightweight count for sidebar badge router.get('/overdue-count', (req: Req, res: Res) => { diff --git a/routes/transactions.cts b/routes/transactions.cts index 65ffbec..3972bf2 100644 --- a/routes/transactions.cts +++ b/routes/transactions.cts @@ -1,8 +1,8 @@ // @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services). import type { Req, Res, Next } from '../types/http'; const router = require('express').Router(); -const { getDb } = require('../db/database'); -const { standardizeError } = require('../middleware/errorFormatter'); +const { getDb } = require('../db/database.cts'); +const { standardizeError } = require('../middleware/errorFormatter.cts'); const { decorateTransaction, ensureManualDataSource, diff --git a/routes/user.cts b/routes/user.cts index 0470ef5..90a1bd2 100644 --- a/routes/user.cts +++ b/routes/user.cts @@ -4,11 +4,11 @@ import type { Req, Res, Next } from '../types/http'; const express = require('express'); const router = express.Router(); -const { getDb } = require('../db/database'); +const { getDb } = require('../db/database.cts'); const { seedDemoData } = require('../scripts/seedDemoData'); -const { demoDataLimiter } = require('../middleware/rateLimiter'); +const { demoDataLimiter } = require('../middleware/rateLimiter.cts'); const { eraseUserData } = require('../services/userDataService.cts'); -const { standardizeError } = require('../middleware/errorFormatter'); +const { standardizeError } = require('../middleware/errorFormatter.cts'); // GET /api/user/seeded-status — returns whether the current user has any seeded data router.get('/seeded-status', (req: Req, res: Res) => { diff --git a/server.js b/server.js index 023d2dd..f218748 100644 --- a/server.js +++ b/server.js @@ -2,15 +2,15 @@ const express = require('express'); const cookieParser = require('cookie-parser'); const path = require('path'); -const { getDb } = require('./db/database'); -const { requireAuth, requireUser, requireAdmin } = require('./middleware/requireAuth'); +const { getDb } = require('./db/database.cts'); +const { requireAuth, requireUser, requireAdmin } = require('./middleware/requireAuth.cts'); const { recordError } = require('./services/statusRuntime.cts'); -const { securityHeaders } = require('./middleware/securityHeaders'); +const { securityHeaders } = require('./middleware/securityHeaders.cts'); const { logAudit } = require('./services/auditService.cts'); -const { errorFormatter } = require('./middleware/errorFormatter'); +const { errorFormatter } = require('./middleware/errorFormatter.cts'); const { importLimiter, exportLimiter, adminActionLimiter, oidcLimiter, loginLimiter, loginUsernameLimiter, passwordLimiter, backupOperationLimiter } = - require('./middleware/rateLimiter'); -const { csrfMiddleware, csrfTokenProvider } = require('./middleware/csrf'); + require('./middleware/rateLimiter.cts'); +const { csrfMiddleware, csrfTokenProvider } = require('./middleware/csrf.cts'); const app = express(); const PORT = process.env.PORT || 3000; @@ -170,7 +170,7 @@ app.all(['/legacy', '/legacy/*splat'], (req, res) => { app.get('/login.html', (req, res) => res.redirect(302, '/login')); app.use(express.static(DIST)); // Ensure CSRF cookie is set for SPA by calling getCsrfToken before sending index.html -const { getCsrfToken } = require('./middleware/csrf'); +const { getCsrfToken } = require('./middleware/csrf.cts'); app.get('/{*splat}', (req, res) => { // Set CSRF cookie if not present (needed for SPA to read token) getCsrfToken(req, res); @@ -221,7 +221,7 @@ async function main() { } // Run session cleanup on startup - const { cleanupExpiredSessions } = require('./db/database'); + const { cleanupExpiredSessions } = require('./db/database.cts'); try { console.log('[cleanup] Running session cleanup on startup'); cleanupExpiredSessions(); @@ -273,7 +273,7 @@ async function main() { if (userCount > 0) console.log(`Users found: ${userCount}`); // Set up periodic session cleanup - const { cleanupExpiredSessions } = require('./db/database'); + const { cleanupExpiredSessions } = require('./db/database.cts'); const rawInterval = process.env.SESSION_CLEANUP_INTERVAL_MS; let CLEANUP_INTERVAL_MS = 86400000; // 24 hours default if (rawInterval !== undefined) { @@ -316,7 +316,7 @@ async function main() { // Start daily worker (autopay marking, notifications, session pruning, cleanup) try { - require('./workers/dailyWorker').start(); + require('./workers/dailyWorker.cts').start(); } catch (err) { console.error('[dailyWorker] Failed to start daily worker:', err.message); } diff --git a/services/advisoryFilterService.cts b/services/advisoryFilterService.cts index 11a224f..af5d35e 100644 --- a/services/advisoryFilterService.cts +++ b/services/advisoryFilterService.cts @@ -2,7 +2,7 @@ import type { Db } from '../types/db'; -const { getDb } = require('../db/database'); +const { getDb } = require('../db/database.cts'); // Lazy-loaded in-memory cache — loaded once on first use let _patterns: any[] | null = null; diff --git a/services/analyticsService.cts b/services/analyticsService.cts index 9ca4176..173adff 100644 --- a/services/analyticsService.cts +++ b/services/analyticsService.cts @@ -2,7 +2,7 @@ import type { Db } from '../types/db'; -const { getDb } = require('../db/database'); +const { getDb } = require('../db/database.cts'); const { accountingActiveSql } = require('./paymentAccountingService.cts'); // Aggregation output re-typed by the client; branding adds friction (Dollars|null // through many .filter/.sort/arith) without much safety here → plain require (any). diff --git a/services/auditService.cts b/services/auditService.cts index 316d16b..a15b2d7 100644 --- a/services/auditService.cts +++ b/services/auditService.cts @@ -1,6 +1,6 @@ import type { Db } from '../types/db'; -const { getDb } = require('../db/database'); +const { getDb } = require('../db/database.cts'); interface AuditParams { user_id?: number | null; diff --git a/services/authService.cts b/services/authService.cts index 3e1d7bd..df5ae50 100644 --- a/services/authService.cts +++ b/services/authService.cts @@ -3,7 +3,7 @@ import type { Req } from '../types/http'; const crypto = require('crypto'); const bcrypt = require('bcryptjs'); -const { getDb } = require('../db/database'); +const { getDb } = require('../db/database.cts'); const { buildDeviceFingerprint } = require('./loginFingerprint.cts'); const { encryptSecret, decryptSecret } = require('./encryptionService.cts'); diff --git a/services/backupScheduler.cts b/services/backupScheduler.cts index be896ce..11b43c5 100644 --- a/services/backupScheduler.cts +++ b/services/backupScheduler.cts @@ -2,7 +2,7 @@ import type { Db } from '../types/db'; const cron = require('node-cron'); -const { getSetting, setSetting } = require('../db/database'); +const { getSetting, setSetting } = require('../db/database.cts'); const { applyScheduledRetention, createBackup } = require('./backupService.cts'); interface ScheduleSettings { diff --git a/services/backupService.cts b/services/backupService.cts index e3fd666..b242d3f 100644 --- a/services/backupService.cts +++ b/services/backupService.cts @@ -5,7 +5,7 @@ const crypto = require('crypto'); const fs = require('fs'); const path = require('path'); const Database = require('better-sqlite3'); -const { closeDb, getDb, getDbPath } = require('../db/database'); +const { closeDb, getDb, getDbPath } = require('../db/database.cts'); const BACKUP_DIR = path.resolve( process.env.BACKUP_PATH || path.join(path.dirname(getDbPath()), '..', 'backups') diff --git a/services/bankSyncConfigService.cts b/services/bankSyncConfigService.cts index e82470b..8e73369 100644 --- a/services/bankSyncConfigService.cts +++ b/services/bankSyncConfigService.cts @@ -3,7 +3,7 @@ // import required so Node type-strips this .cts (it has no other import). import type { Db } from '../types/db'; -const { getSetting, setSetting } = require('../db/database'); +const { getSetting, setSetting } = require('../db/database.cts'); const SYNC_DAYS_MAX = 45; // SimpleFIN Bridge hard limit — requests beyond this return an error const SYNC_DAYS_EFFECTIVE = 44; // 1-day buffer so request latency never tips over the hard limit diff --git a/services/bankSyncWorker.cts b/services/bankSyncWorker.cts index cd210d2..d659867 100644 --- a/services/bankSyncWorker.cts +++ b/services/bankSyncWorker.cts @@ -2,7 +2,7 @@ import type { Db } from '../types/db'; -const { getDb } = require('../db/database'); +const { getDb } = require('../db/database.cts'); const { getBankSyncConfig } = require('./bankSyncConfigService.cts'); const { syncDataSource } = require('./bankSyncService.cts'); diff --git a/services/calendarFeedService.cts b/services/calendarFeedService.cts index 89bb453..7bf33c9 100644 --- a/services/calendarFeedService.cts +++ b/services/calendarFeedService.cts @@ -4,7 +4,7 @@ import type { Db } from '../types/db'; import type { Req } from '../types/http'; const crypto = require('crypto'); -const { getDb } = require('../db/database'); +const { getDb } = require('../db/database.cts'); const { normalizeCycleType, resolveDueDate } = require('./statusService.cts'); const { fromCents } = require('../utils/money.mts') as typeof import('../utils/money.mts'); diff --git a/services/cleanupService.cts b/services/cleanupService.cts index d57409a..a3513a0 100644 --- a/services/cleanupService.cts +++ b/services/cleanupService.cts @@ -6,7 +6,7 @@ const os = require('os'); const fs = require('fs'); const path = require('path'); -const { getDb, getSetting, setSetting } = require('../db/database'); +const { getDb, getSetting, setSetting } = require('../db/database.cts'); const { BACKUP_DIR } = require('./backupService.cts'); // ─── Helpers ───────────────────────────────────────────────────────────────── diff --git a/services/csvTransactionImportService.cts b/services/csvTransactionImportService.cts index b0e55e0..d5a370b 100644 --- a/services/csvTransactionImportService.cts +++ b/services/csvTransactionImportService.cts @@ -3,7 +3,7 @@ import type { Db } from '../types/db'; const crypto = require('crypto'); -const { getDb } = require('../db/database'); +const { getDb } = require('../db/database.cts'); const { decorateTransaction, ensureManualDataSource } = require('./transactionService.cts'); type Row = Record; diff --git a/services/driftService.cts b/services/driftService.cts index 8fe501a..d96bac3 100644 --- a/services/driftService.cts +++ b/services/driftService.cts @@ -2,7 +2,7 @@ import type { Dollars } from '../utils/money.mts'; -const { getDb } = require('../db/database'); +const { getDb } = require('../db/database.cts'); const { getCycleRange } = require('./statusService.cts'); const { accountingActiveSql } = require('./paymentAccountingService.cts'); const { getUserSettings } = require('./userSettings.cts'); diff --git a/services/encryptionService.cts b/services/encryptionService.cts index 7e5ae68..45304dd 100644 --- a/services/encryptionService.cts +++ b/services/encryptionService.cts @@ -25,7 +25,7 @@ function getEnvIkm(): Buffer | null { // Returns the auto-generated DB IKM, creating it on first call. function getDbIkm(): Buffer { - const { getSetting, setSetting } = require('../db/database'); + const { getSetting, setSetting } = require('../db/database.cts'); let stored = getSetting('_auto_encryption_key'); if (!stored) { stored = crypto.randomBytes(48).toString('hex'); @@ -57,7 +57,7 @@ function isEnvKeyActive(): boolean { function keyFingerprint(): string | null { let ikm = getEnvIkm(); if (!ikm) { - const { getSetting } = require('../db/database'); + const { getSetting } = require('../db/database.cts'); const stored = getSetting('_auto_encryption_key'); if (!stored) return null; ikm = Buffer.from(stored, 'utf8'); diff --git a/services/matchSuggestionService.cts b/services/matchSuggestionService.cts index cfdff99..dc41084 100644 --- a/services/matchSuggestionService.cts +++ b/services/matchSuggestionService.cts @@ -2,7 +2,7 @@ import type { Db } from '../types/db'; -const { getDb } = require('../db/database'); +const { getDb } = require('../db/database.cts'); const { getCycleRange, resolveDueDate } = require('./statusService.cts'); const { decorateTransaction } = require('./transactionService.cts'); const { fromCents } = require('../utils/money.mts') as typeof import('../utils/money.mts'); diff --git a/services/notificationService.cts b/services/notificationService.cts index fb9d172..32e4a83 100644 --- a/services/notificationService.cts +++ b/services/notificationService.cts @@ -1,7 +1,7 @@ import type { Db } from '../types/db'; const nodemailer = require('nodemailer'); -const { getDb, getSetting } = require('../db/database'); +const { getDb, getSetting } = require('../db/database.cts'); const { decryptSecret, encryptSecret } = require('./encryptionService.cts'); const { accountingActiveSql } = require('./paymentAccountingService.cts'); const { diff --git a/services/ofxImportService.cts b/services/ofxImportService.cts index 39e0992..3b9f688 100644 --- a/services/ofxImportService.cts +++ b/services/ofxImportService.cts @@ -11,7 +11,7 @@ import type { Db } from '../types/db'; -const { getDb } = require('../db/database'); +const { getDb } = require('../db/database.cts'); const { decorateTransaction, ensureManualDataSource } = require('./transactionService.cts'); const { saveImportSession, diff --git a/services/oidcService.cts b/services/oidcService.cts index a35204a..e1cf3ee 100644 --- a/services/oidcService.cts +++ b/services/oidcService.cts @@ -52,7 +52,7 @@ const crypto = require('crypto'); const { Issuer } = require('openid-client'); -const { getDb, getSetting, setSetting } = require('../db/database'); +const { getDb, getSetting, setSetting } = require('../db/database.cts'); const { encryptSecret, decryptSecret } = require('./encryptionService.cts'); // Decrypt the stored OIDC client secret, falling back to plaintext for diff --git a/services/spreadsheetImportService.cts b/services/spreadsheetImportService.cts index c74e666..019058b 100644 --- a/services/spreadsheetImportService.cts +++ b/services/spreadsheetImportService.cts @@ -14,7 +14,7 @@ const xlsx = require('xlsx'); const crypto = require('crypto'); -const { getDb, ensureUserDefaultCategories } = require('../db/database'); +const { getDb, ensureUserDefaultCategories } = require('../db/database.cts'); const { computeBalanceDelta, applyBalanceDelta } = require('./billsService.cts'); const { toCents, fromCents } = require('../utils/money.mts'); diff --git a/services/statusRuntime.cts b/services/statusRuntime.cts index 2fe521c..3519705 100644 --- a/services/statusRuntime.cts +++ b/services/statusRuntime.cts @@ -44,7 +44,7 @@ const state: { worker: WorkerState; notifications: NotificationsState; recentErr // Wrapped in try/catch — DB may not be ready at require-time in tests. function seedFromDb(): void { try { - const { getSetting } = require('../db/database'); + const { getSetting } = require('../db/database.cts'); state.worker.last_run_at = getSetting(DB_KEY.workerLastRun) || null; state.worker.next_run_at = getSetting(DB_KEY.workerNextRun) || null; state.worker.last_error = getSetting(DB_KEY.workerError) || null; @@ -56,7 +56,7 @@ seedFromDb(); function dbSet(key: string, value: unknown): void { try { - const { setSetting } = require('../db/database'); + const { setSetting } = require('../db/database.cts'); setSetting(key, value == null ? '' : String(value)); } catch { /* non-fatal */ } } diff --git a/services/statusService.cts b/services/statusService.cts index 5f57ed0..74ab44c 100644 --- a/services/statusService.cts +++ b/services/statusService.cts @@ -2,7 +2,7 @@ import type { Db } from '../types/db'; -const { getSetting } = require('../db/database'); +const { getSetting } = require('../db/database.cts'); // Dynamic better-sqlite3 bill/payment rows — columns read individually. type Row = Record; diff --git a/services/trackerService.cts b/services/trackerService.cts index 1ff7213..c0460b6 100644 --- a/services/trackerService.cts +++ b/services/trackerService.cts @@ -2,7 +2,7 @@ import type { Db } from '../types/db'; -const { getDb } = require('../db/database'); +const { getDb } = require('../db/database.cts'); const { buildTrackerRow, getCycleRange, resolveDueDate, isPaidStatus } = require('./statusService.cts'); const { getUserSettings } = require('./userSettings.cts'); const { computeBalanceDelta, applyBalanceDelta } = require('./billsService.cts'); diff --git a/services/transactionMatchService.cts b/services/transactionMatchService.cts index fbc2034..81dc76c 100644 --- a/services/transactionMatchService.cts +++ b/services/transactionMatchService.cts @@ -2,7 +2,7 @@ import type { Db } from '../types/db'; -const { getDb } = require('../db/database'); +const { getDb } = require('../db/database.cts'); const { computeBalanceDelta, applyBalanceDelta } = require('./billsService.cts'); const { applyBankPaymentAsSourceOfTruth, diff --git a/services/updateCheckService.cts b/services/updateCheckService.cts index 92512a4..d7f893d 100644 --- a/services/updateCheckService.cts +++ b/services/updateCheckService.cts @@ -7,7 +7,7 @@ // import required so Node type-strips this .cts (it has no other import). import type { Db } from '../types/db'; -const { getSetting } = require('../db/database'); +const { getSetting } = require('../db/database.cts'); const REPO_API_BASE = process.env.REPO_API_URL || 'https://dream.scheller.ltd/api/v1/repos/null/BillTracker'; diff --git a/services/userDataService.cts b/services/userDataService.cts index 78ebb16..9354fde 100644 --- a/services/userDataService.cts +++ b/services/userDataService.cts @@ -2,7 +2,7 @@ import type { Db } from '../types/db'; -const { ensureUserDefaultCategories } = require('../db/database'); +const { ensureUserDefaultCategories } = require('../db/database.cts'); // Independent user-owned tables (each has a user_id) wiped wholesale. const USER_TABLES = [ diff --git a/services/userDbImportService.cts b/services/userDbImportService.cts index b1fc8cd..0aac601 100644 --- a/services/userDbImportService.cts +++ b/services/userDbImportService.cts @@ -6,7 +6,7 @@ const fs = require('fs'); const os = require('os'); const path = require('path'); const Database = require('better-sqlite3'); -const { getDb } = require('../db/database'); +const { getDb } = require('../db/database.cts'); const { billingCycleForCycleType, cycleTypeFromBillingCycle } = require('./billsService.cts'); const { toCents } = require('../utils/money.mts'); diff --git a/services/userSettings.cts b/services/userSettings.cts index 701f9d1..e3d7f76 100644 --- a/services/userSettings.cts +++ b/services/userSettings.cts @@ -2,7 +2,7 @@ import type { Db } from '../types/db'; -const { getDb, getSetting } = require('../db/database'); +const { getDb, getSetting } = require('../db/database.cts'); const USER_SETTING_KEYS = [ 'currency', diff --git a/tests/amountSuggestionService.test.js b/tests/amountSuggestionService.test.js index a9432d1..b0db52b 100644 --- a/tests/amountSuggestionService.test.js +++ b/tests/amountSuggestionService.test.js @@ -13,7 +13,7 @@ const fs = require('node:fs'); const dbPath = path.join(os.tmpdir(), `bill-tracker-amountsug-${process.pid}.sqlite`); process.env.DB_PATH = dbPath; -const { getDb, closeDb } = require('../db/database'); +const { getDb, closeDb } = require('../db/database.cts'); const { computeAmountSuggestion, computeAmountSuggestionsBatch } = require('../services/amountSuggestionService.cts'); let db, userId, bills; diff --git a/tests/authService.test.js b/tests/authService.test.js index 282e1dc..edd1bd2 100644 --- a/tests/authService.test.js +++ b/tests/authService.test.js @@ -16,7 +16,7 @@ const crypto = require('node:crypto'); const dbPath = path.join(os.tmpdir(), `bill-tracker-authsvc-${process.pid}.sqlite`); process.env.DB_PATH = dbPath; -const { getDb, closeDb } = require('../db/database'); +const { getDb, closeDb } = require('../db/database.cts'); const auth = require('../services/authService.cts'); const sha256 = (s) => crypto.createHash('sha256').update(s).digest('hex'); diff --git a/tests/backupAndCleanup.test.js b/tests/backupAndCleanup.test.js index 0d00192..878c96f 100644 --- a/tests/backupAndCleanup.test.js +++ b/tests/backupAndCleanup.test.js @@ -15,7 +15,7 @@ const backupPath = path.join(os.tmpdir(), `${testId}-backups`); process.env.DB_PATH = dbPath; process.env.BACKUP_PATH = backupPath; -const { closeDb, setSetting, getDb } = require('../db/database'); +const { closeDb, setSetting, getDb } = require('../db/database.cts'); const { BACKUP_DIR, createBackup, diff --git a/tests/bankSyncService.test.js b/tests/bankSyncService.test.js index 035bdf9..767895d 100644 --- a/tests/bankSyncService.test.js +++ b/tests/bankSyncService.test.js @@ -7,7 +7,7 @@ const path = require('node:path'); const dbPath = path.join(os.tmpdir(), `bill-tracker-bank-sync-test-${process.pid}.sqlite`); process.env.DB_PATH = dbPath; -const { getDb, closeDb } = require('../db/database'); +const { getDb, closeDb } = require('../db/database.cts'); const { encryptSecret } = require('../services/encryptionService.cts'); const { syncDataSource } = require('../services/bankSyncService.cts'); diff --git a/tests/billMerchantRuleService.test.js b/tests/billMerchantRuleService.test.js index aa2edaf..bf9ca55 100644 --- a/tests/billMerchantRuleService.test.js +++ b/tests/billMerchantRuleService.test.js @@ -15,7 +15,7 @@ const fs = require('node:fs'); const dbPath = path.join(os.tmpdir(), `bill-tracker-merchantrule-${process.pid}.sqlite`); process.env.DB_PATH = dbPath; -const { getDb, closeDb } = require('../db/database'); +const { getDb, closeDb } = require('../db/database.cts'); const { addMerchantRule, syncBillPaymentsFromSimplefin } = require('../services/billMerchantRuleService.cts'); let db, userId, billId; diff --git a/tests/billReorder.test.js b/tests/billReorder.test.js index 9c2b92f..a4f3193 100644 --- a/tests/billReorder.test.js +++ b/tests/billReorder.test.js @@ -7,7 +7,7 @@ const path = require('node:path'); const dbPath = path.join(os.tmpdir(), `bill-tracker-reorder-test-${process.pid}.sqlite`); process.env.DB_PATH = dbPath; -const { getDb, closeDb } = require('../db/database'); +const { getDb, closeDb } = require('../db/database.cts'); const { getTracker } = require('../services/trackerService.cts'); function createUser(db, suffix) { diff --git a/tests/billsDeletedRoute.test.js b/tests/billsDeletedRoute.test.js index 0d6700a..3a5c1e3 100644 --- a/tests/billsDeletedRoute.test.js +++ b/tests/billsDeletedRoute.test.js @@ -13,7 +13,7 @@ const fs = require('node:fs'); const dbPath = path.join(os.tmpdir(), `bill-tracker-bills-deleted-route-${process.pid}.sqlite`); process.env.DB_PATH = dbPath; -const { getDb, closeDb } = require('../db/database'); +const { getDb, closeDb } = require('../db/database.cts'); function createUser(db, suffix) { return db.prepare( diff --git a/tests/calendarFeedService.test.js b/tests/calendarFeedService.test.js index de643c6..d3f3926 100644 --- a/tests/calendarFeedService.test.js +++ b/tests/calendarFeedService.test.js @@ -16,7 +16,7 @@ const { foldLine, rruleForCycle, } = require('../services/calendarFeedService.cts'); -const { getDb, closeDb } = require('../db/database'); +const { getDb, closeDb } = require('../db/database.cts'); function createUser(db, username = 'calendar-user') { return db.prepare(` diff --git a/tests/categoryGroups.test.js b/tests/categoryGroups.test.js index 27cfb1b..b475571 100644 --- a/tests/categoryGroups.test.js +++ b/tests/categoryGroups.test.js @@ -7,7 +7,7 @@ const path = require('node:path'); const dbPath = path.join(os.tmpdir(), `bill-tracker-category-groups-test-${process.pid}.sqlite`); process.env.DB_PATH = dbPath; -const { getDb, closeDb } = require('../db/database'); +const { getDb, closeDb } = require('../db/database.cts'); function createUser(db, suffix) { return db.prepare(` diff --git a/tests/categoryReorder.test.js b/tests/categoryReorder.test.js index 3b7a6e1..03948cc 100644 --- a/tests/categoryReorder.test.js +++ b/tests/categoryReorder.test.js @@ -7,7 +7,7 @@ const path = require('node:path'); const dbPath = path.join(os.tmpdir(), `bill-tracker-category-reorder-test-${process.pid}.sqlite`); process.env.DB_PATH = dbPath; -const { getDb, closeDb } = require('../db/database'); +const { getDb, closeDb } = require('../db/database.cts'); function createUser(db, suffix) { return db.prepare(` diff --git a/tests/csvTransactionImportService.test.js b/tests/csvTransactionImportService.test.js index a547de9..75303f4 100644 --- a/tests/csvTransactionImportService.test.js +++ b/tests/csvTransactionImportService.test.js @@ -7,7 +7,7 @@ const path = require('node:path'); const dbPath = path.join(os.tmpdir(), `bill-tracker-csv-import-test-${process.pid}.sqlite`); process.env.DB_PATH = dbPath; -const { getDb, closeDb } = require('../db/database'); +const { getDb, closeDb } = require('../db/database.cts'); const { commitCsvTransactions, previewCsvTransactions, diff --git a/tests/encryptionService.test.js b/tests/encryptionService.test.js index a0a966f..31ab906 100644 --- a/tests/encryptionService.test.js +++ b/tests/encryptionService.test.js @@ -18,7 +18,7 @@ const dbPath = path.join(os.tmpdir(), `bill-tracker-encsvc-${process.pid}.sqlite process.env.DB_PATH = dbPath; delete process.env.TOKEN_ENCRYPTION_KEY; // start in DB-key mode -const { getDb, closeDb, getSetting, setSetting } = require('../db/database'); +const { getDb, closeDb, getSetting, setSetting } = require('../db/database.cts'); const enc = require('../services/encryptionService.cts'); test.before(() => { getDb(); }); // init schema + migrations (also creates the initial admin) diff --git a/tests/eraseUserData.test.js b/tests/eraseUserData.test.js index 2bf3091..00f5a44 100644 --- a/tests/eraseUserData.test.js +++ b/tests/eraseUserData.test.js @@ -11,7 +11,7 @@ const fs = require('node:fs'); const dbPath = path.join(os.tmpdir(), `bill-tracker-erase-${process.pid}.sqlite`); process.env.DB_PATH = dbPath; -const { getDb, closeDb } = require('../db/database'); +const { getDb, closeDb } = require('../db/database.cts'); const { eraseUserData } = require('../services/userDataService.cts'); function makeUser(db, name) { diff --git a/tests/exportImportRoundTrip.test.js b/tests/exportImportRoundTrip.test.js index dbffc33..8b20d70 100644 --- a/tests/exportImportRoundTrip.test.js +++ b/tests/exportImportRoundTrip.test.js @@ -13,7 +13,7 @@ const fs = require('node:fs'); const dbPath = path.join(os.tmpdir(), `bill-tracker-roundtrip-test-${process.pid}.sqlite`); process.env.DB_PATH = dbPath; -const { getDb, closeDb } = require('../db/database'); +const { getDb, closeDb } = require('../db/database.cts'); const { buildUserDbExportFile } = require('../routes/export.cts'); const { previewUserDbImport, applyUserDbImport } = require('../services/userDbImportService.cts'); diff --git a/tests/exportRicher.test.js b/tests/exportRicher.test.js index 0cc08f6..443960f 100644 --- a/tests/exportRicher.test.js +++ b/tests/exportRicher.test.js @@ -11,7 +11,7 @@ const fs = require('node:fs'); const dbPath = path.join(os.tmpdir(), `bill-tracker-export-richer-${process.pid}.sqlite`); process.env.DB_PATH = dbPath; -const { getDb, closeDb } = require('../db/database'); +const { getDb, closeDb } = require('../db/database.cts'); const exportRouter = require('../routes/export.cts'); const { getUserExportData } = exportRouter; diff --git a/tests/migrationModules.test.js b/tests/migrationModules.test.js index 918bbef..00d16ad 100644 --- a/tests/migrationModules.test.js +++ b/tests/migrationModules.test.js @@ -11,8 +11,8 @@ const test = require('node:test'); const assert = require('node:assert/strict'); -const buildVersioned = require('../db/migrations/versionedMigrations'); -const buildReconcile = require('../db/migrations/legacyReconcileMigrations'); +const buildVersioned = require('../db/migrations/versionedMigrations.cts'); +const buildReconcile = require('../db/migrations/legacyReconcileMigrations.cts'); test('both migration modules build their arrays with no deps needed at construction', () => { const versioned = buildVersioned({}); diff --git a/tests/ofxImportService.test.js b/tests/ofxImportService.test.js index 92d1361..41284d3 100644 --- a/tests/ofxImportService.test.js +++ b/tests/ofxImportService.test.js @@ -11,7 +11,7 @@ const fs = require('node:fs'); const dbPath = path.join(os.tmpdir(), `bill-tracker-ofx-${process.pid}.sqlite`); process.env.DB_PATH = dbPath; -const { getDb, closeDb } = require('../db/database'); +const { getDb, closeDb } = require('../db/database.cts'); const { parseOfx, previewOfxTransactions, commitOfxTransactions } = require('../services/ofxImportService.cts'); // OFX 1.x SGML — container tags closed, leaf tags unclosed. diff --git a/tests/paymentAccountingService.test.js b/tests/paymentAccountingService.test.js index befe7c3..e16ad56 100644 --- a/tests/paymentAccountingService.test.js +++ b/tests/paymentAccountingService.test.js @@ -15,7 +15,7 @@ const fs = require('node:fs'); const dbPath = path.join(os.tmpdir(), `bill-tracker-payacct-${process.pid}.sqlite`); process.env.DB_PATH = dbPath; -const { getDb, closeDb } = require('../db/database'); +const { getDb, closeDb } = require('../db/database.cts'); const { isBankBackedPayment, accountingActiveSql, diff --git a/tests/paymentsQuickRoute.test.js b/tests/paymentsQuickRoute.test.js index 1d5a984..d377279 100644 --- a/tests/paymentsQuickRoute.test.js +++ b/tests/paymentsQuickRoute.test.js @@ -13,7 +13,7 @@ const fs = require('node:fs'); const dbPath = path.join(os.tmpdir(), `bill-tracker-quickpay-${process.pid}.sqlite`); process.env.DB_PATH = dbPath; -const { getDb, closeDb } = require('../db/database'); +const { getDb, closeDb } = require('../db/database.cts'); const router = require('../routes/payments.cts'); function handler(method, routePath) { diff --git a/tests/paymentsRoute.test.js b/tests/paymentsRoute.test.js index 92d0b20..77d10c6 100644 --- a/tests/paymentsRoute.test.js +++ b/tests/paymentsRoute.test.js @@ -14,7 +14,7 @@ const fs = require('node:fs'); const dbPath = path.join(os.tmpdir(), `bill-tracker-payments-${process.pid}.sqlite`); process.env.DB_PATH = dbPath; -const { getDb, closeDb } = require('../db/database'); +const { getDb, closeDb } = require('../db/database.cts'); const router = require('../routes/payments.cts'); function handler(method, routePath) { diff --git a/tests/profileRoute.test.js b/tests/profileRoute.test.js index b0a6284..d6c9901 100644 --- a/tests/profileRoute.test.js +++ b/tests/profileRoute.test.js @@ -7,7 +7,7 @@ const path = require('node:path'); const dbPath = path.join(os.tmpdir(), `bill-tracker-profile-route-test-${process.pid}.sqlite`); process.env.DB_PATH = dbPath; -const { getDb, closeDb } = require('../db/database'); +const { getDb, closeDb } = require('../db/database.cts'); const { publicUser } = require('../services/authService.cts'); function createUser(db, suffix) { diff --git a/tests/reconciliation.test.js b/tests/reconciliation.test.js index da35dcc..b28f252 100644 --- a/tests/reconciliation.test.js +++ b/tests/reconciliation.test.js @@ -17,7 +17,7 @@ const fs = require('node:fs'); const dbPath = path.join(os.tmpdir(), `bill-tracker-recon-${process.pid}.sqlite`); process.env.DB_PATH = dbPath; -const { getDb, closeDb } = require('../db/database'); +const { getDb, closeDb } = require('../db/database.cts'); const { getTracker } = require('../services/trackerService.cts'); const { getAnalyticsSummary } = require('../services/analyticsService.cts'); const { resolveDueDate } = require('../services/statusService.cts'); diff --git a/tests/snowballPlanRoute.test.js b/tests/snowballPlanRoute.test.js index a3929af..9295587 100644 --- a/tests/snowballPlanRoute.test.js +++ b/tests/snowballPlanRoute.test.js @@ -12,7 +12,7 @@ const fs = require('node:fs'); const dbPath = path.join(os.tmpdir(), `bill-tracker-snowball-plan-${process.pid}.sqlite`); process.env.DB_PATH = dbPath; -const { getDb, closeDb } = require('../db/database'); +const { getDb, closeDb } = require('../db/database.cts'); const router = require('../routes/snowball.cts'); function handler(method, routePath) { diff --git a/tests/spendingService.test.js b/tests/spendingService.test.js index ed21531..4ae4a95 100644 --- a/tests/spendingService.test.js +++ b/tests/spendingService.test.js @@ -13,7 +13,7 @@ const fs = require('node:fs'); const dbPath = path.join(os.tmpdir(), `bill-tracker-spendsvc-${process.pid}.sqlite`); process.env.DB_PATH = dbPath; -const { getDb, closeDb } = require('../db/database'); +const { getDb, closeDb } = require('../db/database.cts'); const { setSpendingBudget, getSpendingBudgets } = require('../services/spendingService.cts'); let db, userId, catId; diff --git a/tests/spendingSummary.test.js b/tests/spendingSummary.test.js index 7ab87d5..ff88d1d 100644 --- a/tests/spendingSummary.test.js +++ b/tests/spendingSummary.test.js @@ -7,7 +7,7 @@ const path = require('node:path'); const dbPath = path.join(os.tmpdir(), `bill-tracker-spending-summary-test-${process.pid}.sqlite`); process.env.DB_PATH = dbPath; -const { getDb, closeDb } = require('../db/database'); +const { getDb, closeDb } = require('../db/database.cts'); const { getSpendingSummary, setSpendingBudget } = require('../services/spendingService.cts'); function createUser(db, suffix) { diff --git a/tests/subscriptionService.test.js b/tests/subscriptionService.test.js index 396efaf..70c76f3 100644 --- a/tests/subscriptionService.test.js +++ b/tests/subscriptionService.test.js @@ -7,7 +7,7 @@ const path = require('node:path'); const dbPath = path.join(os.tmpdir(), `bill-tracker-subscription-service-test-${process.pid}.sqlite`); process.env.DB_PATH = dbPath; -const { getDb, closeDb } = require('../db/database'); +const { getDb, closeDb } = require('../db/database.cts'); const { getSubscriptionRecommendations, recordSubscriptionFeedback, diff --git a/tests/summaryBankTracking.test.js b/tests/summaryBankTracking.test.js index 8088b20..6f7c0a9 100644 --- a/tests/summaryBankTracking.test.js +++ b/tests/summaryBankTracking.test.js @@ -12,7 +12,7 @@ const fs = require('node:fs'); const dbPath = path.join(os.tmpdir(), `bill-tracker-summary-bt-test-${process.pid}.sqlite`); process.env.DB_PATH = dbPath; -const { getDb, closeDb } = require('../db/database'); +const { getDb, closeDb } = require('../db/database.cts'); function callSummary(userId, year, month) { const router = require('../routes/summary.cts'); diff --git a/tests/summarySkipOverride.test.js b/tests/summarySkipOverride.test.js index d60f9b3..9b3002f 100644 --- a/tests/summarySkipOverride.test.js +++ b/tests/summarySkipOverride.test.js @@ -12,7 +12,7 @@ const fs = require('node:fs'); const dbPath = path.join(os.tmpdir(), `bill-tracker-summary-skip-test-${process.pid}.sqlite`); process.env.DB_PATH = dbPath; -const { getDb, closeDb } = require('../db/database'); +const { getDb, closeDb } = require('../db/database.cts'); function callSummary(userId, year, month) { const router = require('../routes/summary.cts'); diff --git a/tests/totpService.test.js b/tests/totpService.test.js index 9b25e31..50604e0 100644 --- a/tests/totpService.test.js +++ b/tests/totpService.test.js @@ -14,7 +14,7 @@ const { generateSync } = require('otplib'); const dbPath = path.join(os.tmpdir(), `bill-tracker-totp-${process.pid}.sqlite`); process.env.DB_PATH = dbPath; -const { getDb, closeDb } = require('../db/database'); +const { getDb, closeDb } = require('../db/database.cts'); const { encryptSecret } = require('../services/encryptionService.cts'); const totp = require('../services/totpService.cts'); diff --git a/tests/trackerService.test.js b/tests/trackerService.test.js index fe68786..b542e96 100644 --- a/tests/trackerService.test.js +++ b/tests/trackerService.test.js @@ -14,7 +14,7 @@ const fs = require('node:fs'); const dbPath = path.join(os.tmpdir(), `bill-tracker-trackersvc-${process.pid}.sqlite`); process.env.DB_PATH = dbPath; -const { getDb, closeDb } = require('../db/database'); +const { getDb, closeDb } = require('../db/database.cts'); const { getTracker, getOverdueCount } = require('../services/trackerService.cts'); // Fixed "now" so occurrence gating + statuses are deterministic (mid-June 2026). diff --git a/tests/transactionMatchService.test.js b/tests/transactionMatchService.test.js index 6b978e3..09abfa3 100644 --- a/tests/transactionMatchService.test.js +++ b/tests/transactionMatchService.test.js @@ -7,7 +7,7 @@ const path = require('node:path'); const dbPath = path.join(os.tmpdir(), `bill-tracker-transaction-match-test-${process.pid}.sqlite`); process.env.DB_PATH = dbPath; -const { getDb, closeDb } = require('../db/database'); +const { getDb, closeDb } = require('../db/database.cts'); const { ensureManualDataSource } = require('../services/transactionService.cts'); const { getTracker } = require('../services/trackerService.cts'); const { diff --git a/tests/transactionMatchState.test.js b/tests/transactionMatchState.test.js index 3a520ab..df39958 100644 --- a/tests/transactionMatchState.test.js +++ b/tests/transactionMatchState.test.js @@ -12,7 +12,7 @@ const fs = require('node:fs'); const dbPath = path.join(os.tmpdir(), `bill-tracker-match-state-${process.pid}.sqlite`); process.env.DB_PATH = dbPath; -const { getDb, closeDb } = require('../db/database'); +const { getDb, closeDb } = require('../db/database.cts'); const { markMatched, markUnmatched, markIgnored } = require('../services/transactionMatchState.cts'); let db, userId, otherId, billId; diff --git a/tests/updateCheckOptOut.test.js b/tests/updateCheckOptOut.test.js index 51d6a72..d11ce09 100644 --- a/tests/updateCheckOptOut.test.js +++ b/tests/updateCheckOptOut.test.js @@ -12,7 +12,7 @@ const fs = require('node:fs'); const dbPath = path.join(os.tmpdir(), `bill-tracker-updatecheck-test-${process.pid}.sqlite`); process.env.DB_PATH = dbPath; -const { getDb, setSetting, closeDb } = require('../db/database'); +const { getDb, setSetting, closeDb } = require('../db/database.cts'); const svc = require('../services/updateCheckService.cts'); test('disabled: makes no external request and returns disabled', async () => { diff --git a/workers/dailyWorker.js b/workers/dailyWorker.cts similarity index 71% rename from workers/dailyWorker.js rename to workers/dailyWorker.cts index 3614c50..659e6cf 100644 --- a/workers/dailyWorker.js +++ b/workers/dailyWorker.cts @@ -1,7 +1,9 @@ 'use strict'; +import type { Db } from '../types/db'; + const cron = require('node-cron'); -const { getDb, getSetting } = require('../db/database'); +const { getDb, getSetting } = require('../db/database.cts'); const { buildTrackerRow, getCycleRange } = require('../services/statusService.cts'); const { pruneExpiredSessions } = require('../services/authService.cts'); const { pruneExpiredChallenges: pruneWebAuthnChallenges } = require('../services/webauthnService.cts'); @@ -14,42 +16,35 @@ const { } = require('../services/statusRuntime.cts'); const { localDateString, localDateStringDaysAgo } = require('../utils/dates.mts'); -// The hour (0–23) the single daily job runs — configurable via the global -// 'reminder_hour' setting, defaulting to 6 AM. One source of truth for both the -// cron expression and the next-run display. -function resolveReminderHour() { +// The hour (0–23) the single daily job runs — configurable via 'reminder_hour' +// (default 6 AM). One source of truth for both the cron expression and next-run. +function resolveReminderHour(): number { const parsed = parseInt(getSetting('reminder_hour') ?? '6', 10); return Number.isInteger(parsed) && parsed >= 0 && parsed <= 23 ? parsed : 6; } -function nextDailyRunIso(from = new Date()) { +function nextDailyRunIso(from: Date = new Date()): string { const next = new Date(from); next.setHours(resolveReminderHour(), 0, 0, 0); if (next <= from) next.setDate(next.getDate() + 1); return next.toISOString(); } -async function runDailyTasks() { - const db = getDb(); +async function runDailyTasks(): Promise { + const db: Db = getDb(); const now = new Date(); const year = now.getFullYear(); const month = now.getMonth() + 1; - // Local date — keep consistent with year/month above and with the client's - // notion of "today". toISOString() would give the UTC date, which can be a - // different calendar day and caused autopay marking a day early/late. const todayStr = localDateString(now); const bills = db.prepare('SELECT * FROM bills WHERE active = 1 AND deleted_at IS NULL').all(); if (bills.length > 0) { - // Batch-fetch payments for all active bills from the last 90 days to avoid - // one query per bill. Different billing cycles (monthly/quarterly/annual) all - // fit inside a 90-day window for the current month's due-date checks. - const billIds = bills.map(b => b.id); + const billIds = bills.map((b: any) => b.id); const placeholders = billIds.map(() => '?').join(','); const windowStart = localDateStringDaysAgo(90, now); - let allPayments = []; + let allPayments: any[] = []; try { allPayments = db.prepare(` SELECT * FROM payments @@ -57,18 +52,17 @@ async function runDailyTasks() { AND paid_date >= ? AND deleted_at IS NULL `).all(...billIds, windowStart); - } catch (err) { + } catch (err: any) { console.error('[worker] Failed to batch-fetch payments:', err.message); } // Group payments by bill_id in memory - const paymentsByBill = new Map(); + const paymentsByBill = new Map(); for (const p of allPayments) { if (!paymentsByBill.has(p.bill_id)) paymentsByBill.set(p.bill_id, []); paymentsByBill.get(p.bill_id).push(p); } - // Prepare autopay update statement once outside the loop const markAutopay = db.prepare( "UPDATE bills SET autodraft_status = 'assumed_paid', updated_at = datetime('now') WHERE id = ?" ); @@ -77,9 +71,8 @@ async function runDailyTasks() { const range = getCycleRange(year, month, bill); if (!range) continue; // bill does not apply this cycle - // Filter pre-fetched payments to the bill's cycle range const payments = (paymentsByBill.get(bill.id) || []).filter( - p => p.paid_date >= range.start && p.paid_date <= range.end + (p: any) => p.paid_date >= range.start && p.paid_date <= range.end ); const row = buildTrackerRow(bill, payments, year, month, todayStr); @@ -93,7 +86,7 @@ async function runDailyTasks() { ) { try { markAutopay.run(bill.id); - } catch (err) { + } catch (err: any) { console.error(`[worker] Failed to mark autopay for bill ${bill.id}:`, err.message); } } @@ -103,15 +96,15 @@ async function runDailyTasks() { pruneExpiredSessions(); pruneWebAuthnChallenges(db); - await runNotifications().catch(err => { + await runNotifications().catch((err: any) => { console.error('[worker] Notification error (non-fatal):', err.message); }); - await runDriftNotifications().catch(err => { + await runDriftNotifications().catch((err: any) => { console.error('[worker] Drift notification error (non-fatal):', err.message); }); - await runAllCleanup().catch(err => { + await runAllCleanup().catch((err: any) => { console.error('[worker] Cleanup error (non-fatal):', err.message); }); @@ -119,40 +112,39 @@ async function runDailyTasks() { console.log(`[worker] Daily tasks ran at ${todayStr}`); } -let scheduledTask = null; +let scheduledTask: any = null; // (Re)create the daily cron at the configured hour. Stops any prior task first. -function scheduleDaily() { +function scheduleDaily(): void { if (scheduledTask) { scheduledTask.stop(); scheduledTask = null; } const hour = resolveReminderHour(); scheduledTask = cron.schedule(`0 ${hour} * * *`, () => { - runDailyTasks().catch(err => { + runDailyTasks().catch((err: any) => { markWorkerError(err, nextDailyRunIso()); console.error('[worker] Daily task error:', err); }); }); } -// Called when the 'reminder_hour' setting changes so it applies without a -// restart. Defensive: a failure here must never take the worker down. -function rescheduleDailyWorker() { +// Called when 'reminder_hour' changes so it applies without a restart. +function rescheduleDailyWorker(): void { try { scheduleDaily(); markWorkerStarted(nextDailyRunIso()); console.log(`[worker] Rescheduled daily tasks to ${resolveReminderHour()}:00`); - } catch (err) { + } catch (err: any) { console.error('[worker] Failed to reschedule daily worker:', err.message); } } -function start() { +function start(): void { markWorkerStarted(nextDailyRunIso()); // Run once at startup - runDailyTasks().catch(err => { + runDailyTasks().catch((err: any) => { markWorkerError(err, nextDailyRunIso()); console.error('[worker] Startup task error:', err); });