refactor(server): migrate middleware, workers, and db layer to TypeScript (.cts)

- middleware/ (5): requireAuth, securityHeaders, errorFormatter, csrf,
  rateLimiter — fully typed against the shared http Req/Res/Next types.
- workers/dailyWorker — fully typed.
- db/ (4): database, subscriptionCatalogSeed, migrations/versionedMigrations,
  migrations/legacyReconcileMigrations — large dynamic schema/migration infra,
  converted with @ts-nocheck (typing deferred). All requires updated to .cts.

Behavior-preserving. Verified: typecheck:server 0, check:server 0, suite 226/226.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
null 2026-07-06 11:34:08 -05:00
parent 866ba52890
commit 135bd98c6a
93 changed files with 219 additions and 305 deletions

View File

@ -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 Database = require('better-sqlite3');
const path = require('path'); const path = require('path');
const fs = require('fs'); const fs = require('fs');
@ -74,11 +75,11 @@ function isValidSqlDefinition(def) {
} }
// Subscription catalog seed rows live in their own module (pure data, no logic). // 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). // 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). // 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) { function runSubscriptionCatalogV2Migration(database) {
// Category fixes for existing rows // Category fixes for existing rows

View File

@ -1,3 +1,4 @@
// @ts-nocheck — .cts via Node type-stripping; rich typing deferred (large dynamic DB/migration infra).
'use strict'; 'use strict';
// The legacy-reconcile migration list, extracted from db/database.js. Same // The legacy-reconcile migration list, extracted from db/database.js. Same

View File

@ -1,3 +1,4 @@
// @ts-nocheck — .cts via Node type-stripping; rich typing deferred (large dynamic DB/migration infra).
'use strict'; 'use strict';
// The versioned migration list, extracted from db/database.js to keep that // The versioned migration list, extracted from db/database.js to keep that

View File

@ -1,3 +1,4 @@
// @ts-nocheck — .cts via Node type-stripping; rich typing deferred (large dynamic DB/migration infra).
'use strict'; 'use strict';
// Seed rows for the subscription_catalog table, extracted from db/database.js // Seed rows for the subscription_catalog table, extracted from db/database.js

View File

@ -1,62 +1,39 @@
import type { Req, Res, Next } from '../types/http';
const crypto = require('crypto'); const crypto = require('crypto');
const { logAudit } = require('../services/auditService.cts'); const { logAudit } = require('../services/auditService.cts');
// ───────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────
// CSRF Middleware // CSRF Middleware — double-submit token protection for state-changing routes.
// Protects state-changing routes (POST, PUT, DELETE) from cross-site request
// forgery by validating tokens stored in session cookie.
// ───────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────
const CSRF_HEADER_NAME = 'x-csrf-token'; 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 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'; 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 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'; const CSRF_COOKIE_NAME = process.env.CSRF_COOKIE_NAME || 'bt_csrf_token';
// Generate a cryptographically secure CSRF token function generateCsrfToken(): string {
function generateCsrfToken() {
return crypto.randomBytes(32).toString('hex'); return crypto.randomBytes(32).toString('hex');
} }
/** /**
* Detect HTTPS the same way services/authService.cookieOpts does: * 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.
*/ */
function requestLooksHttps(req) { function requestLooksHttps(req?: Req): boolean {
if (!req) return false; if (!req) return false;
if (req.secure) return true; if (req.secure) return true;
const proto = req.get?.('x-forwarded-proto') || req.headers?.['x-forwarded-proto']; 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. * Get or create CSRF token for the current session (readable double-submit cookie).
* 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.
*/ */
function getCsrfToken(req, res) { function getCsrfToken(req: Req, res: Res): string {
let token = req.cookies?.[CSRF_COOKIE_NAME]; let token = req.cookies?.[CSRF_COOKIE_NAME];
if (!token) { if (!token) {
token = generateCsrfToken(); token = generateCsrfToken();
res.cookie(CSRF_COOKIE_NAME, token, { res.cookie(CSRF_COOKIE_NAME, token, {
@ -66,19 +43,14 @@ function getCsrfToken(req, res) {
path: '/', path: '/',
}); });
} }
return token; return token;
} }
/** /**
* Validate CSRF token from request. * Validate CSRF token from request (x-csrf-token header or csrf_token body field).
* 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.
*/ */
function validateCsrfToken(req) { function validateCsrfToken(req: Req): boolean {
const cookieToken = req.cookies?.[CSRF_COOKIE_NAME]; const cookieToken = req.cookies?.[CSRF_COOKIE_NAME];
if (!cookieToken) return false; if (!cookieToken) return false;
@ -92,36 +64,27 @@ function validateCsrfToken(req) {
} }
/** /**
* CSRF middleware - validates tokens for state-changing methods. * 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)
*/ */
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. // 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]; const fullPath = (req.originalUrl || '').split('?')[0];
if (fullPath === '/api/auth/login') { if (fullPath === '/api/auth/login') {
return next(); return next();
} }
// Only validate state-changing methods
if (!['POST', 'PUT', 'DELETE', 'PATCH'].includes(req.method)) { if (!['POST', 'PUT', 'DELETE', 'PATCH'].includes(req.method)) {
return next(); return next();
} }
// Skip validation for OPTIONS (preflight)
if (req.method === 'OPTIONS') { if (req.method === 'OPTIONS') {
return next(); 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) { if (req.csrfSkip) {
return next(); return next();
} }
// Validate the CSRF token
if (!validateCsrfToken(req)) { if (!validateCsrfToken(req)) {
logAudit({ user_id: req.user?.id || null, action: 'csrf.failure', ip_address: req.ip, user_agent: req.get('user-agent') }); 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({ return res.status(403).json({
@ -136,9 +99,8 @@ function csrfMiddleware(req, res, next) {
/** /**
* Attach CSRF token to response locals for template rendering. * 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); res.locals.csrfToken = getCsrfToken(req, res);
next(); next();
} }

View File

@ -1,66 +1,52 @@
/** /**
* Centralized Error Formatter Middleware * Centralized Error Formatter Middleware
* *
* Standard error response format: * Standard error response format:
* { * { "error", "message", "field", "code" }
* "error": "ValidationError",
* "message": "Human-readable description",
* "field": "optional-field-name",
* "code": "machine-readable-code"
* }
*/ */
import type { Req, Res, Next } from '../types/http';
const { ValidationError, formatError } = require('../utils/apiError.cts'); const { ValidationError, formatError } = require('../utils/apiError.cts');
/** /** Extract field name from various validation error patterns */
* Extract field name from various validation error patterns function extractFieldFromError(message: any): string | null {
*/
function extractFieldFromError(message) {
if (!message) return 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); const fieldMatch = message.match(/^(\w+)\s+(?:must be|is|are)\s+/i);
if (fieldMatch) return fieldMatch[1]; 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); 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]; 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); const requiredMatch = message.match(/^(username|password|name|bill_id|category_id|due_day)\s+(?:is|are)\s+required/i);
if (requiredMatch) return requiredMatch[1]; if (requiredMatch) return requiredMatch[1];
return null; return null;
} }
/** /** Convert a plain error message/string into a standardized error object */
* Convert a plain error message/string into a standardized error object function standardizeError(message: any, code = 'VALIDATION_ERROR', fieldOverride: any = null) {
*/
function standardizeError(message, code = 'VALIDATION_ERROR', fieldOverride = null) {
const field = fieldOverride || extractFieldFromError(message); const field = fieldOverride || extractFieldFromError(message);
return { return {
error: code, error: code,
message: typeof message === 'string' ? message : String(message), message: typeof message === 'string' ? message : String(message),
field: field || null, field: field || null,
code: code code: code,
}; };
} }
/** /** Express middleware that ensures all error responses follow standard format */
* Express middleware that ensures all error responses follow standard format function errorFormatter(req: Req, res: Res, next: Next): void {
*/
function errorFormatter(req, res, next) {
const originalJson = res.json; const originalJson = res.json;
res.json = function(data) { res.json = function(this: any, data: any) {
// If data is an error object (has error property), standardize it
if (data && typeof data === 'object' && !Array.isArray(data) && data.error && !data.success) { if (data && typeof data === 'object' && !Array.isArray(data) && data.error && !data.success) {
// Already standardized (machine-readable code + human message) — pass through // Already standardized (machine-readable code + human message) — pass through
if (typeof data.code === 'string' && data.code && typeof data.message === 'string') { if (typeof data.code === 'string' && data.code && typeof data.message === 'string') {
return originalJson.call(this, data); 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 message = (typeof data.message === 'string' && data.message) ? data.message : data.error;
const code = (typeof data.code === 'string' && data.code) ? data.code : 'ERROR'; const code = (typeof data.code === 'string' && data.code) ? data.code : 'ERROR';
const standardized = standardizeError(message, code, data.field); const standardized = standardizeError(message, code, data.field);
@ -68,35 +54,23 @@ function errorFormatter(req, res, next) {
} }
return originalJson.call(this, data); return originalJson.call(this, data);
}; };
next(); next();
} }
/** function formatValidationError(message: any, field?: any) {
* Helper to format validation errors with proper field extraction
*/
function formatValidationError(message, field) {
return standardizeError(message, 'VALIDATION_ERROR', field); return standardizeError(message, 'VALIDATION_ERROR', field);
} }
/** function formatNotFoundError(message: any, field?: any) {
* Helper to format not found errors
*/
function formatNotFoundError(message, field) {
return standardizeError(message, 'NOT_FOUND', field); return standardizeError(message, 'NOT_FOUND', field);
} }
/** function formatAuthError(message: any) {
* Helper to format authentication errors
*/
function formatAuthError(message) {
return standardizeError(message, 'AUTH_ERROR'); return standardizeError(message, 'AUTH_ERROR');
} }
/** function formatForbiddenError(message: any) {
* Helper to format forbidden/access errors
*/
function formatForbiddenError(message) {
return standardizeError(message, 'FORBIDDEN'); return standardizeError(message, 'FORBIDDEN');
} }

View File

@ -1,15 +1,17 @@
'use strict'; 'use strict';
import type { Req, Res } from '../types/http';
const { rateLimit, ipKeyGenerator } = require('express-rate-limit'); const { rateLimit, ipKeyGenerator } = require('express-rate-limit');
function makeLimiter(max, windowMs, message) { function makeLimiter(max: number, windowMs: number, message: string): any {
return rateLimit({ return rateLimit({
windowMs, windowMs,
max, max,
standardHeaders: 'draft-7', standardHeaders: 'draft-7',
legacyHeaders: false, legacyHeaders: false,
// Override default handler so the response is always JSON, not HTML // 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 }); res.status(429).json({ error: message });
}, },
}); });
@ -21,21 +23,19 @@ const loginLimiter = makeLimiter(
'Too many login attempts. Please try again in 15 minutes.', 'Too many login attempts. Please try again in 15 minutes.',
); );
// 5 FAILED login attempts per 15 minutes per username — layered on top of the // 5 FAILED login attempts per 15 minutes per username — layered on the per-IP
// per-IP limiter so a distributed attacker (or many clients behind one NAT/proxy // limiter. Successful logins don't count, so legit users are unaffected.
// sharing an IP bucket) cannot brute-force a single account. Successful logins
// don't count toward the limit, so legitimate users are unaffected.
const loginUsernameLimiter = rateLimit({ const loginUsernameLimiter = rateLimit({
windowMs: 15 * 60 * 1000, windowMs: 15 * 60 * 1000,
max: 5, max: 5,
standardHeaders: 'draft-7', standardHeaders: 'draft-7',
legacyHeaders: false, legacyHeaders: false,
skipSuccessfulRequests: true, skipSuccessfulRequests: true,
keyGenerator: (req) => { keyGenerator: (req: Req) => {
const username = String(req.body?.username || '').trim().toLowerCase(); const username = String(req.body?.username || '').trim().toLowerCase();
return username ? `user:${username}` : ipKeyGenerator(req); 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.' }); 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, max: 10,
standardHeaders: 'draft-7', standardHeaders: 'draft-7',
legacyHeaders: false, legacyHeaders: false,
keyGenerator: (req) => req.user?.id?.toString() || ipKeyGenerator(req), keyGenerator: (req: Req) => req.user?.id?.toString() || ipKeyGenerator(req),
handler(req, res) { handler(req: Req, res: Res) {
res.status(429).json({ error: 'Too many sync requests. Please try again in 15 minutes.' }); res.status(429).json({ error: 'Too many sync requests. Please try again in 15 minutes.' });
}, },
}); });
@ -108,7 +108,7 @@ const allLimiters = [
syncLimiter, syncLimiter,
]; ];
function resetStores() { function resetStores(): void {
for (const limiter of allLimiters) { for (const limiter of allLimiters) {
if (limiter.store.reset) { if (limiter.store.reset) {
limiter.store.reset(); limiter.store.reset();

View File

@ -1,15 +1,16 @@
import type { Req, Res, Next } from '../types/http';
const crypto = require('crypto'); const crypto = require('crypto');
const { getSessionUser, COOKIE_NAME, SINGLE_COOKIE_NAME, cookieOpts, publicUser, recordLogin } = require('../services/authService.cts'); const { getSessionUser, COOKIE_NAME, SINGLE_COOKIE_NAME, cookieOpts, publicUser, recordLogin } = require('../services/authService.cts');
const { getDb, getSetting } = require('../db/database'); const { getDb, getSetting } = require('../db/database.cts');
const { standardizeError } = require('./errorFormatter'); const { standardizeError } = require('./errorFormatter.cts');
function getSingleModeUser() { function getSingleModeUser(): any {
if (getSetting('auth_mode') !== 'single') return null; if (getSetting('auth_mode') !== 'single') return null;
const userId = getSetting('default_user_id'); const userId = getSetting('default_user_id');
if (!userId) return null; if (!userId) return null;
// Single-user mode: validate only that the configured user exists, // Single-user mode: validate only that the configured user exists, is active,
// is active, and has role 'user'. Sessions are not relevant here — // and has role 'user'. Sessions are not relevant here.
// single-user mode bypasses session auth entirely.
const row = getDb().prepare(` const row = getDb().prepare(`
SELECT id, username, display_name, role, must_change_password, first_login, SELECT id, username, display_name, role, must_change_password, first_login,
active, is_default_admin, last_seen_version active, is_default_admin, last_seen_version
@ -19,7 +20,7 @@ function getSingleModeUser() {
return row ? publicUser(row) : null; 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 // Single-user mode: bypass session entirely, auto-attach the default user
const singleUser = getSingleModeUser(); const singleUser = getSingleModeUser();
if (singleUser) { if (singleUser) {
@ -27,7 +28,6 @@ function requireAuth(req, res, next) {
req.singleUserMode = true; req.singleUserMode = true;
// Track logins via a presence cookie so login history works without a real session. // 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]; const existing = req.cookies?.[SINGLE_COOKIE_NAME];
if (existing) { if (existing) {
req.singleSessionId = existing; req.singleSessionId = existing;
@ -56,7 +56,7 @@ function requireAuth(req, res, next) {
next(); next();
} }
function requireUser(req, res, next) { function requireUser(req: Req, res: Res, next: Next): any {
if (req.user?.is_default_admin) { if (req.user?.is_default_admin) {
return res.status(403).json(standardizeError('Default admin account does not have tracker access', 'FORBIDDEN')); 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(); 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, // In single-user mode the auto-attached user is never admin,
// so admin routes naturally stay protected by session. // so admin routes naturally stay protected by session.
if (req.user?.role !== 'admin') { if (req.user?.role !== 'admin') {

View File

@ -1,12 +1,11 @@
'use strict'; 'use strict';
import type { Req, Res, Next } from '../types/http';
const crypto = require('crypto'); const crypto = require('crypto');
/** /** Generates a secure nonce for CSP policy. Call once per request. */
* Generates a secure nonce for CSP policy. function getCspNonce(req: Req): string {
* Call once per request to get a unique nonce.
*/
function getCspNonce(req) {
if (!req.cspNonce) { if (!req.cspNonce) {
req.cspNonce = crypto.randomBytes(16).toString('base64'); req.cspNonce = crypto.randomBytes(16).toString('base64');
} }
@ -15,15 +14,9 @@ function getCspNonce(req) {
/** /**
* Applies baseline security response headers on every request. * Applies baseline security response headers on every request.
* * (See original notes on why CSP uses no nonce index.html is static.)
* 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'.
*/ */
function securityHeaders(req, res, next) { function securityHeaders(req: Req, res: Res, next: Next): void {
const cspPolicy = const cspPolicy =
`default-src 'self'; ` + `default-src 'self'; ` +
`script-src 'self'; ` + `script-src 'self'; ` +
@ -35,26 +28,15 @@ function securityHeaders(req, res, next) {
`form-action 'self'; ` + `form-action 'self'; ` +
`base-uri 'self'; ` + `base-uri 'self'; ` +
`object-src 'none';`; `object-src 'none';`;
res.setHeader('Content-Security-Policy', cspPolicy); res.setHeader('Content-Security-Policy', cspPolicy);
// Prevent MIME-type sniffing (browsers must respect Content-Type)
res.setHeader('X-Content-Type-Options', 'nosniff'); res.setHeader('X-Content-Type-Options', 'nosniff');
// Disallow embedding in iframes from other origins — prevents clickjacking
res.setHeader('X-Frame-Options', 'SAMEORIGIN'); 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'); 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'); res.setHeader('X-Permitted-Cross-Domain-Policies', 'none');
// Don't advertise the server tech stack
res.removeHeader('X-Powered-By'); res.removeHeader('X-Powered-By');
// HSTS — only set when the app is explicitly configured to run over HTTPS. // HSTS — only when explicitly configured to run over HTTPS.
// Setting this on HTTP breaks the site. Enable with HTTPS=true env var.
if (process.env.HTTPS === 'true') { if (process.env.HTTPS === 'true') {
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains'); res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
} }

View File

@ -3,7 +3,7 @@ import type { Req, Res, Next } from '../types/http';
const express = require('express'); const express = require('express');
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const { requireAuth, requireAdmin } = require('../middleware/requireAuth'); const { requireAuth, requireAdmin } = require('../middleware/requireAuth.cts');
const router = express.Router(); 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. // 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? // GET /api/about-admin/update-check-setting — is the external version check enabled?
router.get('/update-check-setting', requireAuth, requireAdmin, (req: Req, res: Res) => { router.get('/update-check-setting', requireAuth, requireAdmin, (req: Req, res: Res) => {

View File

@ -2,7 +2,7 @@
import type { Req, Res, Next } from '../types/http'; import type { Req, Res, Next } from '../types/http';
const express = require('express'); const express = require('express');
const router = express.Router(); 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 { getBankSyncConfig, setBankSyncEnabled, setSyncIntervalHours, setSyncDays, setDebugLogging } = require('../services/bankSyncConfigService.cts');
const { getStatus: getBankSyncWorkerStatus } = require('../services/bankSyncWorker.cts'); const { getStatus: getBankSyncWorkerStatus } = require('../services/bankSyncWorker.cts');
const { isEnvKeyActive, keyFingerprint } = require('../services/encryptionService.cts'); const { isEnvKeyActive, keyFingerprint } = require('../services/encryptionService.cts');
@ -26,7 +26,7 @@ const {
runAllCleanup, runAllCleanup,
validateAndApplySettings: applyCleanupSettings, validateAndApplySettings: applyCleanupSettings,
} = require('../services/cleanupService.cts'); } = 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) // All routes mounted at /api/admin (requireAuth + requireAdmin applied at server level)

View File

@ -2,7 +2,7 @@
import type { Req, Res, Next } from '../types/http'; import type { Req, Res, Next } from '../types/http';
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const { standardizeError } = require('../middleware/errorFormatter'); const { standardizeError } = require('../middleware/errorFormatter.cts');
const { getAnalyticsSummary } = require('../services/analyticsService.cts'); const { getAnalyticsSummary } = require('../services/analyticsService.cts');
router.get('/summary', (req: Req, res: Res) => { router.get('/summary', (req: Req, res: Res) => {

View File

@ -11,15 +11,15 @@ function getAppVersion() {
return _appVersion; 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 { login, logout, hashPassword, cookieOpts, COOKIE_NAME, SINGLE_COOKIE_NAME, rotateSessionId, invalidateOtherSessions, recordLogin, recordFailedLogin } = require('../services/authService.cts');
const { decryptSecret } = require('../services/encryptionService.cts'); const { decryptSecret } = require('../services/encryptionService.cts');
const { getCsrfToken } = require('../middleware/csrf'); const { getCsrfToken } = require('../middleware/csrf.cts');
const { requireAuth, requireAdmin } = require('../middleware/requireAuth'); const { requireAuth, requireAdmin } = require('../middleware/requireAuth.cts');
const { getPublicOidcInfo } = require('../services/oidcService.cts'); const { getPublicOidcInfo } = require('../services/oidcService.cts');
const { ValidationError, formatError } = require('../utils/apiError.cts'); const { ValidationError, formatError } = require('../utils/apiError.cts');
const { standardizeError } = require('../middleware/errorFormatter'); const { standardizeError } = require('../middleware/errorFormatter.cts');
const { passwordLimiter } = require('../middleware/rateLimiter'); const { passwordLimiter } = require('../middleware/rateLimiter.cts');
const { logAudit } = require('../services/auditService.cts'); const { logAudit } = require('../services/auditService.cts');
// ───────────────────────────────────────── // ─────────────────────────────────────────

View File

@ -2,7 +2,7 @@
import type { Req, Res, Next } from '../types/http'; import type { Req, Res, Next } from '../types/http';
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const { getDb, ensureUserDefaultCategories } = require('../db/database'); const { getDb, ensureUserDefaultCategories } = require('../db/database.cts');
const { const {
auditBillsForUser, auditBillsForUser,
categoryBelongsToUser, categoryBelongsToUser,
@ -15,7 +15,7 @@ const {
applyBalanceDelta, applyBalanceDelta,
} = require('../services/billsService.cts'); } = require('../services/billsService.cts');
const { amortizationSchedule, debtAprSnapshot } = require('../services/aprService.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 { validatePaymentInput, serializePayment } = require('../services/paymentValidation.cts');
const { addMerchantRule, syncBillPaymentsFromSimplefin, merchantMatches } = require('../services/billMerchantRuleService.cts'); const { addMerchantRule, syncBillPaymentsFromSimplefin, merchantMatches } = require('../services/billMerchantRuleService.cts');
const { normalizeMerchant } = require('../services/subscriptionService.cts'); const { normalizeMerchant } = require('../services/subscriptionService.cts');

View File

@ -1,9 +1,9 @@
// @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services). // @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'; import type { Req, Res, Next } from '../types/http';
const express = require('express'); const express = require('express');
const { standardizeError } = require('../middleware/errorFormatter'); const { standardizeError } = require('../middleware/errorFormatter.cts');
const router = express.Router(); const router = express.Router();
const { getDb } = require('../db/database'); const { getDb } = require('../db/database.cts');
const { buildTrackerRow, getCycleRange } = require('../services/statusService.cts'); const { buildTrackerRow, getCycleRange } = require('../services/statusService.cts');
const { getUserSettings } = require('../services/userSettings.cts'); const { getUserSettings } = require('../services/userSettings.cts');
const { accountingActiveSql } = require('../services/paymentAccountingService.cts'); const { accountingActiveSql } = require('../services/paymentAccountingService.cts');

View File

@ -1,9 +1,9 @@
// @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services). // @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'; import type { Req, Res, Next } from '../types/http';
const express = require('express'); const express = require('express');
const { standardizeError } = require('../middleware/errorFormatter'); const { standardizeError } = require('../middleware/errorFormatter.cts');
const router = express.Router(); const router = express.Router();
const { getDb, ensureUserDefaultCategories } = require('../db/database'); const { getDb, ensureUserDefaultCategories } = require('../db/database.cts');
const { accountingActiveSql } = require('../services/paymentAccountingService.cts'); const { accountingActiveSql } = require('../services/paymentAccountingService.cts');
// GET /api/categories // GET /api/categories

View File

@ -3,13 +3,13 @@ import type { Req, Res, Next } from '../types/http';
'use strict'; 'use strict';
const router = require('express').Router(); const router = require('express').Router();
const { getDb } = require('../db/database'); const { getDb } = require('../db/database.cts');
const { standardizeError } = require('../middleware/errorFormatter'); const { standardizeError } = require('../middleware/errorFormatter.cts');
const { decorateDataSource, ensureManualDataSource } = require('../services/transactionService.cts'); const { decorateDataSource, ensureManualDataSource } = require('../services/transactionService.cts');
const { connectSimplefin, syncDataSource, backfillDataSource, disconnectDataSource } = require('../services/bankSyncService.cts'); const { connectSimplefin, syncDataSource, backfillDataSource, disconnectDataSource } = require('../services/bankSyncService.cts');
const { sanitizeErrorMessage } = require('../services/simplefinService.cts'); const { sanitizeErrorMessage } = require('../services/simplefinService.cts');
const { getBankSyncConfig } = require('../services/bankSyncConfigService.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_TYPES = new Set(['manual', 'file_import', 'provider_sync']);
const VALID_STATUSES = new Set(['active', 'inactive', 'error']); const VALID_STATUSES = new Set(['active', 'inactive', 'error']);

View File

@ -1,14 +1,14 @@
// @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services). // @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'; import type { Req, Res, Next } from '../types/http';
const express = require('express'); const express = require('express');
const { standardizeError } = require('../middleware/errorFormatter'); const { standardizeError } = require('../middleware/errorFormatter.cts');
const router = express.Router(); const router = express.Router();
const os = require('os'); const os = require('os');
const path = require('path'); const path = require('path');
const fs = require('fs'); const fs = require('fs');
const Database = require('better-sqlite3'); const Database = require('better-sqlite3');
const xlsx = require('xlsx'); const xlsx = require('xlsx');
const { getDb } = require('../db/database'); const { getDb } = require('../db/database.cts');
const { fromCents } = require('../utils/money.mts'); 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 // GET /api/export?year=2026&format=csv — or a date range: ?from=YYYY-MM-DD&to=YYYY-MM-DD

View File

@ -3,7 +3,7 @@ import type { Req, Res, Next } from '../types/http';
'use strict'; 'use strict';
const express = require('express'); const express = require('express');
const { standardizeError } = require('../middleware/errorFormatter'); const { standardizeError } = require('../middleware/errorFormatter.cts');
const router = express.Router(); const router = express.Router();
const { const {
previewSpreadsheet, previewSpreadsheet,

View File

@ -1,8 +1,8 @@
// @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services). // @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'; import type { Req, Res, Next } from '../types/http';
const router = require('express').Router(); const router = require('express').Router();
const { standardizeError } = require('../middleware/errorFormatter'); const { standardizeError } = require('../middleware/errorFormatter.cts');
const { getDb } = require('../db/database'); const { getDb } = require('../db/database.cts');
const { applyBankPaymentAsSourceOfTruth, reactivatePaymentsOverriddenBy } = require('../services/paymentAccountingService.cts'); const { applyBankPaymentAsSourceOfTruth, reactivatePaymentsOverriddenBy } = require('../services/paymentAccountingService.cts');
const { const {
listMatchSuggestions, listMatchSuggestions,

View File

@ -2,7 +2,7 @@
import type { Req, Res, Next } from '../types/http'; import type { Req, Res, Next } from '../types/http';
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const { getDb } = require('../db/database'); const { getDb } = require('../db/database.cts');
const { getCycleRange } = require('../services/statusService.cts'); const { getCycleRange } = require('../services/statusService.cts');
const { accountingActiveSql } = require('../services/paymentAccountingService.cts'); const { accountingActiveSql } = require('../services/paymentAccountingService.cts');
const { toCents, fromCents } = require('../utils/money.mts'); const { toCents, fromCents } = require('../utils/money.mts');

View File

@ -2,8 +2,8 @@
import type { Req, Res, Next } from '../types/http'; import type { Req, Res, Next } from '../types/http';
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const { getDb, getSetting, setSetting } = require('../db/database'); const { getDb, getSetting, setSetting } = require('../db/database.cts');
const { requireAuth, requireUser, requireAdmin } = require('../middleware/requireAuth'); const { requireAuth, requireUser, requireAdmin } = require('../middleware/requireAuth.cts');
const { sendTestEmail } = require('../services/notificationService.cts'); const { sendTestEmail } = require('../services/notificationService.cts');
const { sendTestPush } = require('../services/notificationService.cts')._push || {}; const { sendTestPush } = require('../services/notificationService.cts')._push || {};
const { decryptSecret } = require('../services/encryptionService.cts'); 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; const hour = Number.isInteger(h) && h >= 0 && h <= 23 ? h : 6;
setSetting('reminder_hour', String(hour)); setSetting('reminder_hour', String(hour));
try { try {
require('../workers/dailyWorker').rescheduleDailyWorker(); require('../workers/dailyWorker.cts').rescheduleDailyWorker();
} catch (err) { } catch (err) {
console.error('[notifications] Failed to reschedule daily worker:', err.message); console.error('[notifications] Failed to reschedule daily worker:', err.message);
} }

View File

@ -1,9 +1,9 @@
// @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services). // @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'; import type { Req, Res, Next } from '../types/http';
const express = require('express'); const express = require('express');
const { standardizeError } = require('../middleware/errorFormatter'); const { standardizeError } = require('../middleware/errorFormatter.cts');
const router = require('express').Router(); const router = require('express').Router();
const { getDb } = require('../db/database'); const { getDb } = require('../db/database.cts');
const { computeBalanceDelta, applyBalanceDelta } = require('../services/billsService.cts'); const { computeBalanceDelta, applyBalanceDelta } = require('../services/billsService.cts');
const { validatePaymentInput, serializePayment } = require('../services/paymentValidation.cts'); const { validatePaymentInput, serializePayment } = require('../services/paymentValidation.cts');
const { getCycleRange, resolveDueDate } = require('../services/statusService.cts'); const { getCycleRange, resolveDueDate } = require('../services/statusService.cts');

View File

@ -5,13 +5,13 @@ import type { Req, Res, Next } from '../types/http';
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const bcrypt = require('bcryptjs'); 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 { hashPassword, invalidateOtherSessions, rotateSessionId, COOKIE_NAME, cookieOpts } = require('../services/authService.cts');
const { getImportHistory } = require('../services/spreadsheetImportService.cts'); const { getImportHistory } = require('../services/spreadsheetImportService.cts');
const { logAudit } = require('../services/auditService.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'); const { encryptSecret, decryptSecret } = require('../services/encryptionService.cts');
// All profile routes require authentication — enforced in server.js. // All profile routes require authentication — enforced in server.js.

View File

@ -2,8 +2,8 @@
import type { Req, Res, Next } from '../types/http'; import type { Req, Res, Next } from '../types/http';
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const { getDb } = require('../db/database'); const { getDb } = require('../db/database.cts');
const { standardizeError } = require('../middleware/errorFormatter'); const { standardizeError } = require('../middleware/errorFormatter.cts');
const { calculateSnowball, calculateAvalanche } = require('../services/snowballService.cts'); const { calculateSnowball, calculateAvalanche } = require('../services/snowballService.cts');
const { calculateMinimumOnly, debtAprSnapshot } = require('../services/aprService.cts'); const { calculateMinimumOnly, debtAprSnapshot } = require('../services/aprService.cts');
const { serializeBill } = require('../services/billsService.cts'); const { serializeBill } = require('../services/billsService.cts');

View File

@ -4,7 +4,7 @@ import type { Req, Res, Next } from '../types/http';
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const { getDb } = require('../db/database'); const { getDb } = require('../db/database.cts');
const { const {
getSpendingSummary, getSpendingTransactions, categorizeTransaction, getSpendingSummary, getSpendingTransactions, categorizeTransaction,
getSpendingBudgets, setSpendingBudget, getSpendingBudgets, setSpendingBudget,

View File

@ -4,7 +4,7 @@ const express = require('express');
const router = express.Router(); const router = express.Router();
const fs = require('fs'); const fs = require('fs');
const os = require('os'); 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 { getStatusRuntime, recordError } = require('../services/statusRuntime.cts');
const { listBackups } = require('../services/backupService.cts'); const { listBackups } = require('../services/backupService.cts');
const { getScheduleStatus } = require('../services/backupScheduler.cts'); const { getScheduleStatus } = require('../services/backupScheduler.cts');

View File

@ -2,8 +2,8 @@
import type { Req, Res, Next } from '../types/http'; import type { Req, Res, Next } from '../types/http';
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const { getDb, ensureUserDefaultCategories } = require('../db/database'); const { getDb, ensureUserDefaultCategories } = require('../db/database.cts');
const { standardizeError } = require('../middleware/errorFormatter'); const { standardizeError } = require('../middleware/errorFormatter.cts');
const { fromCents } = require('../utils/money.mts'); const { fromCents } = require('../utils/money.mts');
const { const {
createSubscriptionFromRecommendation, createSubscriptionFromRecommendation,

View File

@ -2,7 +2,7 @@
import type { Req, Res, Next } from '../types/http'; import type { Req, Res, Next } from '../types/http';
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const { getDb } = require('../db/database'); const { getDb } = require('../db/database.cts');
const { getCycleRange, resolveDueDate } = require('../services/statusService.cts'); const { getCycleRange, resolveDueDate } = require('../services/statusService.cts');
const { getUserSettings } = require('../services/userSettings.cts'); const { getUserSettings } = require('../services/userSettings.cts');
const { accountingActiveSql } = require('../services/paymentAccountingService.cts'); const { accountingActiveSql } = require('../services/paymentAccountingService.cts');

View File

@ -3,7 +3,7 @@ import type { Req, Res, Next } from '../types/http';
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const { getTracker, getUpcomingBills, getOverdueCount } = require('../services/trackerService.cts'); 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 // GET /api/tracker/overdue-count — lightweight count for sidebar badge
router.get('/overdue-count', (req: Req, res: Res) => { router.get('/overdue-count', (req: Req, res: Res) => {

View File

@ -1,8 +1,8 @@
// @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services). // @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'; import type { Req, Res, Next } from '../types/http';
const router = require('express').Router(); const router = require('express').Router();
const { getDb } = require('../db/database'); const { getDb } = require('../db/database.cts');
const { standardizeError } = require('../middleware/errorFormatter'); const { standardizeError } = require('../middleware/errorFormatter.cts');
const { const {
decorateTransaction, decorateTransaction,
ensureManualDataSource, ensureManualDataSource,

View File

@ -4,11 +4,11 @@ import type { Req, Res, Next } from '../types/http';
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const { getDb } = require('../db/database'); const { getDb } = require('../db/database.cts');
const { seedDemoData } = require('../scripts/seedDemoData'); const { seedDemoData } = require('../scripts/seedDemoData');
const { demoDataLimiter } = require('../middleware/rateLimiter'); const { demoDataLimiter } = require('../middleware/rateLimiter.cts');
const { eraseUserData } = require('../services/userDataService.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 // GET /api/user/seeded-status — returns whether the current user has any seeded data
router.get('/seeded-status', (req: Req, res: Res) => { router.get('/seeded-status', (req: Req, res: Res) => {

View File

@ -2,15 +2,15 @@ const express = require('express');
const cookieParser = require('cookie-parser'); const cookieParser = require('cookie-parser');
const path = require('path'); const path = require('path');
const { getDb } = require('./db/database'); const { getDb } = require('./db/database.cts');
const { requireAuth, requireUser, requireAdmin } = require('./middleware/requireAuth'); const { requireAuth, requireUser, requireAdmin } = require('./middleware/requireAuth.cts');
const { recordError } = require('./services/statusRuntime.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 { 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 } = const { importLimiter, exportLimiter, adminActionLimiter, oidcLimiter, loginLimiter, loginUsernameLimiter, passwordLimiter, backupOperationLimiter } =
require('./middleware/rateLimiter'); require('./middleware/rateLimiter.cts');
const { csrfMiddleware, csrfTokenProvider } = require('./middleware/csrf'); const { csrfMiddleware, csrfTokenProvider } = require('./middleware/csrf.cts');
const app = express(); const app = express();
const PORT = process.env.PORT || 3000; 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.get('/login.html', (req, res) => res.redirect(302, '/login'));
app.use(express.static(DIST)); app.use(express.static(DIST));
// Ensure CSRF cookie is set for SPA by calling getCsrfToken before sending index.html // 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) => { app.get('/{*splat}', (req, res) => {
// Set CSRF cookie if not present (needed for SPA to read token) // Set CSRF cookie if not present (needed for SPA to read token)
getCsrfToken(req, res); getCsrfToken(req, res);
@ -221,7 +221,7 @@ async function main() {
} }
// Run session cleanup on startup // Run session cleanup on startup
const { cleanupExpiredSessions } = require('./db/database'); const { cleanupExpiredSessions } = require('./db/database.cts');
try { try {
console.log('[cleanup] Running session cleanup on startup'); console.log('[cleanup] Running session cleanup on startup');
cleanupExpiredSessions(); cleanupExpiredSessions();
@ -273,7 +273,7 @@ async function main() {
if (userCount > 0) console.log(`Users found: ${userCount}`); if (userCount > 0) console.log(`Users found: ${userCount}`);
// Set up periodic session cleanup // Set up periodic session cleanup
const { cleanupExpiredSessions } = require('./db/database'); const { cleanupExpiredSessions } = require('./db/database.cts');
const rawInterval = process.env.SESSION_CLEANUP_INTERVAL_MS; const rawInterval = process.env.SESSION_CLEANUP_INTERVAL_MS;
let CLEANUP_INTERVAL_MS = 86400000; // 24 hours default let CLEANUP_INTERVAL_MS = 86400000; // 24 hours default
if (rawInterval !== undefined) { if (rawInterval !== undefined) {
@ -316,7 +316,7 @@ async function main() {
// Start daily worker (autopay marking, notifications, session pruning, cleanup) // Start daily worker (autopay marking, notifications, session pruning, cleanup)
try { try {
require('./workers/dailyWorker').start(); require('./workers/dailyWorker.cts').start();
} catch (err) { } catch (err) {
console.error('[dailyWorker] Failed to start daily worker:', err.message); console.error('[dailyWorker] Failed to start daily worker:', err.message);
} }

View File

@ -2,7 +2,7 @@
import type { Db } from '../types/db'; 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 // Lazy-loaded in-memory cache — loaded once on first use
let _patterns: any[] | null = null; let _patterns: any[] | null = null;

View File

@ -2,7 +2,7 @@
import type { Db } from '../types/db'; import type { Db } from '../types/db';
const { getDb } = require('../db/database'); const { getDb } = require('../db/database.cts');
const { accountingActiveSql } = require('./paymentAccountingService.cts'); const { accountingActiveSql } = require('./paymentAccountingService.cts');
// Aggregation output re-typed by the client; branding adds friction (Dollars|null // Aggregation output re-typed by the client; branding adds friction (Dollars|null
// through many .filter/.sort/arith) without much safety here → plain require (any). // through many .filter/.sort/arith) without much safety here → plain require (any).

View File

@ -1,6 +1,6 @@
import type { Db } from '../types/db'; import type { Db } from '../types/db';
const { getDb } = require('../db/database'); const { getDb } = require('../db/database.cts');
interface AuditParams { interface AuditParams {
user_id?: number | null; user_id?: number | null;

View File

@ -3,7 +3,7 @@ import type { Req } from '../types/http';
const crypto = require('crypto'); const crypto = require('crypto');
const bcrypt = require('bcryptjs'); const bcrypt = require('bcryptjs');
const { getDb } = require('../db/database'); const { getDb } = require('../db/database.cts');
const { buildDeviceFingerprint } = require('./loginFingerprint.cts'); const { buildDeviceFingerprint } = require('./loginFingerprint.cts');
const { encryptSecret, decryptSecret } = require('./encryptionService.cts'); const { encryptSecret, decryptSecret } = require('./encryptionService.cts');

View File

@ -2,7 +2,7 @@
import type { Db } from '../types/db'; import type { Db } from '../types/db';
const cron = require('node-cron'); const cron = require('node-cron');
const { getSetting, setSetting } = require('../db/database'); const { getSetting, setSetting } = require('../db/database.cts');
const { applyScheduledRetention, createBackup } = require('./backupService.cts'); const { applyScheduledRetention, createBackup } = require('./backupService.cts');
interface ScheduleSettings { interface ScheduleSettings {

View File

@ -5,7 +5,7 @@ const crypto = require('crypto');
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const Database = require('better-sqlite3'); 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( const BACKUP_DIR = path.resolve(
process.env.BACKUP_PATH || path.join(path.dirname(getDbPath()), '..', 'backups') process.env.BACKUP_PATH || path.join(path.dirname(getDbPath()), '..', 'backups')

View File

@ -3,7 +3,7 @@
// import required so Node type-strips this .cts (it has no other import). // import required so Node type-strips this .cts (it has no other import).
import type { Db } from '../types/db'; 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_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 const SYNC_DAYS_EFFECTIVE = 44; // 1-day buffer so request latency never tips over the hard limit

View File

@ -2,7 +2,7 @@
import type { Db } from '../types/db'; import type { Db } from '../types/db';
const { getDb } = require('../db/database'); const { getDb } = require('../db/database.cts');
const { getBankSyncConfig } = require('./bankSyncConfigService.cts'); const { getBankSyncConfig } = require('./bankSyncConfigService.cts');
const { syncDataSource } = require('./bankSyncService.cts'); const { syncDataSource } = require('./bankSyncService.cts');

View File

@ -4,7 +4,7 @@ import type { Db } from '../types/db';
import type { Req } from '../types/http'; import type { Req } from '../types/http';
const crypto = require('crypto'); const crypto = require('crypto');
const { getDb } = require('../db/database'); const { getDb } = require('../db/database.cts');
const { normalizeCycleType, resolveDueDate } = require('./statusService.cts'); const { normalizeCycleType, resolveDueDate } = require('./statusService.cts');
const { fromCents } = require('../utils/money.mts') as typeof import('../utils/money.mts'); const { fromCents } = require('../utils/money.mts') as typeof import('../utils/money.mts');

View File

@ -6,7 +6,7 @@ const os = require('os');
const fs = require('fs'); const fs = require('fs');
const path = require('path'); 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'); const { BACKUP_DIR } = require('./backupService.cts');
// ─── Helpers ───────────────────────────────────────────────────────────────── // ─── Helpers ─────────────────────────────────────────────────────────────────

View File

@ -3,7 +3,7 @@
import type { Db } from '../types/db'; import type { Db } from '../types/db';
const crypto = require('crypto'); const crypto = require('crypto');
const { getDb } = require('../db/database'); const { getDb } = require('../db/database.cts');
const { decorateTransaction, ensureManualDataSource } = require('./transactionService.cts'); const { decorateTransaction, ensureManualDataSource } = require('./transactionService.cts');
type Row = Record<string, any>; type Row = Record<string, any>;

View File

@ -2,7 +2,7 @@
import type { Dollars } from '../utils/money.mts'; import type { Dollars } from '../utils/money.mts';
const { getDb } = require('../db/database'); const { getDb } = require('../db/database.cts');
const { getCycleRange } = require('./statusService.cts'); const { getCycleRange } = require('./statusService.cts');
const { accountingActiveSql } = require('./paymentAccountingService.cts'); const { accountingActiveSql } = require('./paymentAccountingService.cts');
const { getUserSettings } = require('./userSettings.cts'); const { getUserSettings } = require('./userSettings.cts');

View File

@ -25,7 +25,7 @@ function getEnvIkm(): Buffer | null {
// Returns the auto-generated DB IKM, creating it on first call. // Returns the auto-generated DB IKM, creating it on first call.
function getDbIkm(): Buffer { function getDbIkm(): Buffer {
const { getSetting, setSetting } = require('../db/database'); const { getSetting, setSetting } = require('../db/database.cts');
let stored = getSetting('_auto_encryption_key'); let stored = getSetting('_auto_encryption_key');
if (!stored) { if (!stored) {
stored = crypto.randomBytes(48).toString('hex'); stored = crypto.randomBytes(48).toString('hex');
@ -57,7 +57,7 @@ function isEnvKeyActive(): boolean {
function keyFingerprint(): string | null { function keyFingerprint(): string | null {
let ikm = getEnvIkm(); let ikm = getEnvIkm();
if (!ikm) { if (!ikm) {
const { getSetting } = require('../db/database'); const { getSetting } = require('../db/database.cts');
const stored = getSetting('_auto_encryption_key'); const stored = getSetting('_auto_encryption_key');
if (!stored) return null; if (!stored) return null;
ikm = Buffer.from(stored, 'utf8'); ikm = Buffer.from(stored, 'utf8');

View File

@ -2,7 +2,7 @@
import type { Db } from '../types/db'; import type { Db } from '../types/db';
const { getDb } = require('../db/database'); const { getDb } = require('../db/database.cts');
const { getCycleRange, resolveDueDate } = require('./statusService.cts'); const { getCycleRange, resolveDueDate } = require('./statusService.cts');
const { decorateTransaction } = require('./transactionService.cts'); const { decorateTransaction } = require('./transactionService.cts');
const { fromCents } = require('../utils/money.mts') as typeof import('../utils/money.mts'); const { fromCents } = require('../utils/money.mts') as typeof import('../utils/money.mts');

View File

@ -1,7 +1,7 @@
import type { Db } from '../types/db'; import type { Db } from '../types/db';
const nodemailer = require('nodemailer'); const nodemailer = require('nodemailer');
const { getDb, getSetting } = require('../db/database'); const { getDb, getSetting } = require('../db/database.cts');
const { decryptSecret, encryptSecret } = require('./encryptionService.cts'); const { decryptSecret, encryptSecret } = require('./encryptionService.cts');
const { accountingActiveSql } = require('./paymentAccountingService.cts'); const { accountingActiveSql } = require('./paymentAccountingService.cts');
const { const {

View File

@ -11,7 +11,7 @@
import type { Db } from '../types/db'; import type { Db } from '../types/db';
const { getDb } = require('../db/database'); const { getDb } = require('../db/database.cts');
const { decorateTransaction, ensureManualDataSource } = require('./transactionService.cts'); const { decorateTransaction, ensureManualDataSource } = require('./transactionService.cts');
const { const {
saveImportSession, saveImportSession,

View File

@ -52,7 +52,7 @@
const crypto = require('crypto'); const crypto = require('crypto');
const { Issuer } = require('openid-client'); 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'); const { encryptSecret, decryptSecret } = require('./encryptionService.cts');
// Decrypt the stored OIDC client secret, falling back to plaintext for // Decrypt the stored OIDC client secret, falling back to plaintext for

View File

@ -14,7 +14,7 @@
const xlsx = require('xlsx'); const xlsx = require('xlsx');
const crypto = require('crypto'); const crypto = require('crypto');
const { getDb, ensureUserDefaultCategories } = require('../db/database'); const { getDb, ensureUserDefaultCategories } = require('../db/database.cts');
const { computeBalanceDelta, applyBalanceDelta } = require('./billsService.cts'); const { computeBalanceDelta, applyBalanceDelta } = require('./billsService.cts');
const { toCents, fromCents } = require('../utils/money.mts'); const { toCents, fromCents } = require('../utils/money.mts');

View File

@ -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. // Wrapped in try/catch — DB may not be ready at require-time in tests.
function seedFromDb(): void { function seedFromDb(): void {
try { try {
const { getSetting } = require('../db/database'); const { getSetting } = require('../db/database.cts');
state.worker.last_run_at = getSetting(DB_KEY.workerLastRun) || null; state.worker.last_run_at = getSetting(DB_KEY.workerLastRun) || null;
state.worker.next_run_at = getSetting(DB_KEY.workerNextRun) || null; state.worker.next_run_at = getSetting(DB_KEY.workerNextRun) || null;
state.worker.last_error = getSetting(DB_KEY.workerError) || null; state.worker.last_error = getSetting(DB_KEY.workerError) || null;
@ -56,7 +56,7 @@ seedFromDb();
function dbSet(key: string, value: unknown): void { function dbSet(key: string, value: unknown): void {
try { try {
const { setSetting } = require('../db/database'); const { setSetting } = require('../db/database.cts');
setSetting(key, value == null ? '' : String(value)); setSetting(key, value == null ? '' : String(value));
} catch { /* non-fatal */ } } catch { /* non-fatal */ }
} }

View File

@ -2,7 +2,7 @@
import type { Db } from '../types/db'; 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. // Dynamic better-sqlite3 bill/payment rows — columns read individually.
type Row = Record<string, any>; type Row = Record<string, any>;

View File

@ -2,7 +2,7 @@
import type { Db } from '../types/db'; 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 { buildTrackerRow, getCycleRange, resolveDueDate, isPaidStatus } = require('./statusService.cts');
const { getUserSettings } = require('./userSettings.cts'); const { getUserSettings } = require('./userSettings.cts');
const { computeBalanceDelta, applyBalanceDelta } = require('./billsService.cts'); const { computeBalanceDelta, applyBalanceDelta } = require('./billsService.cts');

View File

@ -2,7 +2,7 @@
import type { Db } from '../types/db'; import type { Db } from '../types/db';
const { getDb } = require('../db/database'); const { getDb } = require('../db/database.cts');
const { computeBalanceDelta, applyBalanceDelta } = require('./billsService.cts'); const { computeBalanceDelta, applyBalanceDelta } = require('./billsService.cts');
const { const {
applyBankPaymentAsSourceOfTruth, applyBankPaymentAsSourceOfTruth,

View File

@ -7,7 +7,7 @@
// import required so Node type-strips this .cts (it has no other import). // import required so Node type-strips this .cts (it has no other import).
import type { Db } from '../types/db'; 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 const REPO_API_BASE = process.env.REPO_API_URL
|| 'https://dream.scheller.ltd/api/v1/repos/null/BillTracker'; || 'https://dream.scheller.ltd/api/v1/repos/null/BillTracker';

View File

@ -2,7 +2,7 @@
import type { Db } from '../types/db'; 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. // Independent user-owned tables (each has a user_id) wiped wholesale.
const USER_TABLES = [ const USER_TABLES = [

View File

@ -6,7 +6,7 @@ const fs = require('fs');
const os = require('os'); const os = require('os');
const path = require('path'); const path = require('path');
const Database = require('better-sqlite3'); const Database = require('better-sqlite3');
const { getDb } = require('../db/database'); const { getDb } = require('../db/database.cts');
const { billingCycleForCycleType, cycleTypeFromBillingCycle } = require('./billsService.cts'); const { billingCycleForCycleType, cycleTypeFromBillingCycle } = require('./billsService.cts');
const { toCents } = require('../utils/money.mts'); const { toCents } = require('../utils/money.mts');

View File

@ -2,7 +2,7 @@
import type { Db } from '../types/db'; import type { Db } from '../types/db';
const { getDb, getSetting } = require('../db/database'); const { getDb, getSetting } = require('../db/database.cts');
const USER_SETTING_KEYS = [ const USER_SETTING_KEYS = [
'currency', 'currency',

View File

@ -13,7 +13,7 @@ const fs = require('node:fs');
const dbPath = path.join(os.tmpdir(), `bill-tracker-amountsug-${process.pid}.sqlite`); const dbPath = path.join(os.tmpdir(), `bill-tracker-amountsug-${process.pid}.sqlite`);
process.env.DB_PATH = dbPath; 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'); const { computeAmountSuggestion, computeAmountSuggestionsBatch } = require('../services/amountSuggestionService.cts');
let db, userId, bills; let db, userId, bills;

View File

@ -16,7 +16,7 @@ const crypto = require('node:crypto');
const dbPath = path.join(os.tmpdir(), `bill-tracker-authsvc-${process.pid}.sqlite`); const dbPath = path.join(os.tmpdir(), `bill-tracker-authsvc-${process.pid}.sqlite`);
process.env.DB_PATH = dbPath; 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 auth = require('../services/authService.cts');
const sha256 = (s) => crypto.createHash('sha256').update(s).digest('hex'); const sha256 = (s) => crypto.createHash('sha256').update(s).digest('hex');

View File

@ -15,7 +15,7 @@ const backupPath = path.join(os.tmpdir(), `${testId}-backups`);
process.env.DB_PATH = dbPath; process.env.DB_PATH = dbPath;
process.env.BACKUP_PATH = backupPath; process.env.BACKUP_PATH = backupPath;
const { closeDb, setSetting, getDb } = require('../db/database'); const { closeDb, setSetting, getDb } = require('../db/database.cts');
const { const {
BACKUP_DIR, BACKUP_DIR,
createBackup, createBackup,

View File

@ -7,7 +7,7 @@ const path = require('node:path');
const dbPath = path.join(os.tmpdir(), `bill-tracker-bank-sync-test-${process.pid}.sqlite`); const dbPath = path.join(os.tmpdir(), `bill-tracker-bank-sync-test-${process.pid}.sqlite`);
process.env.DB_PATH = dbPath; 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 { encryptSecret } = require('../services/encryptionService.cts');
const { syncDataSource } = require('../services/bankSyncService.cts'); const { syncDataSource } = require('../services/bankSyncService.cts');

View File

@ -15,7 +15,7 @@ const fs = require('node:fs');
const dbPath = path.join(os.tmpdir(), `bill-tracker-merchantrule-${process.pid}.sqlite`); const dbPath = path.join(os.tmpdir(), `bill-tracker-merchantrule-${process.pid}.sqlite`);
process.env.DB_PATH = dbPath; 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'); const { addMerchantRule, syncBillPaymentsFromSimplefin } = require('../services/billMerchantRuleService.cts');
let db, userId, billId; let db, userId, billId;

View File

@ -7,7 +7,7 @@ const path = require('node:path');
const dbPath = path.join(os.tmpdir(), `bill-tracker-reorder-test-${process.pid}.sqlite`); const dbPath = path.join(os.tmpdir(), `bill-tracker-reorder-test-${process.pid}.sqlite`);
process.env.DB_PATH = dbPath; 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 { getTracker } = require('../services/trackerService.cts');
function createUser(db, suffix) { function createUser(db, suffix) {

View File

@ -13,7 +13,7 @@ const fs = require('node:fs');
const dbPath = path.join(os.tmpdir(), `bill-tracker-bills-deleted-route-${process.pid}.sqlite`); const dbPath = path.join(os.tmpdir(), `bill-tracker-bills-deleted-route-${process.pid}.sqlite`);
process.env.DB_PATH = dbPath; process.env.DB_PATH = dbPath;
const { getDb, closeDb } = require('../db/database'); const { getDb, closeDb } = require('../db/database.cts');
function createUser(db, suffix) { function createUser(db, suffix) {
return db.prepare( return db.prepare(

View File

@ -16,7 +16,7 @@ const {
foldLine, foldLine,
rruleForCycle, rruleForCycle,
} = require('../services/calendarFeedService.cts'); } = require('../services/calendarFeedService.cts');
const { getDb, closeDb } = require('../db/database'); const { getDb, closeDb } = require('../db/database.cts');
function createUser(db, username = 'calendar-user') { function createUser(db, username = 'calendar-user') {
return db.prepare(` return db.prepare(`

View File

@ -7,7 +7,7 @@ const path = require('node:path');
const dbPath = path.join(os.tmpdir(), `bill-tracker-category-groups-test-${process.pid}.sqlite`); const dbPath = path.join(os.tmpdir(), `bill-tracker-category-groups-test-${process.pid}.sqlite`);
process.env.DB_PATH = dbPath; process.env.DB_PATH = dbPath;
const { getDb, closeDb } = require('../db/database'); const { getDb, closeDb } = require('../db/database.cts');
function createUser(db, suffix) { function createUser(db, suffix) {
return db.prepare(` return db.prepare(`

View File

@ -7,7 +7,7 @@ const path = require('node:path');
const dbPath = path.join(os.tmpdir(), `bill-tracker-category-reorder-test-${process.pid}.sqlite`); const dbPath = path.join(os.tmpdir(), `bill-tracker-category-reorder-test-${process.pid}.sqlite`);
process.env.DB_PATH = dbPath; process.env.DB_PATH = dbPath;
const { getDb, closeDb } = require('../db/database'); const { getDb, closeDb } = require('../db/database.cts');
function createUser(db, suffix) { function createUser(db, suffix) {
return db.prepare(` return db.prepare(`

View File

@ -7,7 +7,7 @@ const path = require('node:path');
const dbPath = path.join(os.tmpdir(), `bill-tracker-csv-import-test-${process.pid}.sqlite`); const dbPath = path.join(os.tmpdir(), `bill-tracker-csv-import-test-${process.pid}.sqlite`);
process.env.DB_PATH = dbPath; process.env.DB_PATH = dbPath;
const { getDb, closeDb } = require('../db/database'); const { getDb, closeDb } = require('../db/database.cts');
const { const {
commitCsvTransactions, commitCsvTransactions,
previewCsvTransactions, previewCsvTransactions,

View File

@ -18,7 +18,7 @@ const dbPath = path.join(os.tmpdir(), `bill-tracker-encsvc-${process.pid}.sqlite
process.env.DB_PATH = dbPath; process.env.DB_PATH = dbPath;
delete process.env.TOKEN_ENCRYPTION_KEY; // start in DB-key mode 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'); const enc = require('../services/encryptionService.cts');
test.before(() => { getDb(); }); // init schema + migrations (also creates the initial admin) test.before(() => { getDb(); }); // init schema + migrations (also creates the initial admin)

View File

@ -11,7 +11,7 @@ const fs = require('node:fs');
const dbPath = path.join(os.tmpdir(), `bill-tracker-erase-${process.pid}.sqlite`); const dbPath = path.join(os.tmpdir(), `bill-tracker-erase-${process.pid}.sqlite`);
process.env.DB_PATH = dbPath; process.env.DB_PATH = dbPath;
const { getDb, closeDb } = require('../db/database'); const { getDb, closeDb } = require('../db/database.cts');
const { eraseUserData } = require('../services/userDataService.cts'); const { eraseUserData } = require('../services/userDataService.cts');
function makeUser(db, name) { function makeUser(db, name) {

View File

@ -13,7 +13,7 @@ const fs = require('node:fs');
const dbPath = path.join(os.tmpdir(), `bill-tracker-roundtrip-test-${process.pid}.sqlite`); const dbPath = path.join(os.tmpdir(), `bill-tracker-roundtrip-test-${process.pid}.sqlite`);
process.env.DB_PATH = dbPath; 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 { buildUserDbExportFile } = require('../routes/export.cts');
const { previewUserDbImport, applyUserDbImport } = require('../services/userDbImportService.cts'); const { previewUserDbImport, applyUserDbImport } = require('../services/userDbImportService.cts');

View File

@ -11,7 +11,7 @@ const fs = require('node:fs');
const dbPath = path.join(os.tmpdir(), `bill-tracker-export-richer-${process.pid}.sqlite`); const dbPath = path.join(os.tmpdir(), `bill-tracker-export-richer-${process.pid}.sqlite`);
process.env.DB_PATH = dbPath; 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 exportRouter = require('../routes/export.cts');
const { getUserExportData } = exportRouter; const { getUserExportData } = exportRouter;

View File

@ -11,8 +11,8 @@
const test = require('node:test'); const test = require('node:test');
const assert = require('node:assert/strict'); const assert = require('node:assert/strict');
const buildVersioned = require('../db/migrations/versionedMigrations'); const buildVersioned = require('../db/migrations/versionedMigrations.cts');
const buildReconcile = require('../db/migrations/legacyReconcileMigrations'); const buildReconcile = require('../db/migrations/legacyReconcileMigrations.cts');
test('both migration modules build their arrays with no deps needed at construction', () => { test('both migration modules build their arrays with no deps needed at construction', () => {
const versioned = buildVersioned({}); const versioned = buildVersioned({});

View File

@ -11,7 +11,7 @@ const fs = require('node:fs');
const dbPath = path.join(os.tmpdir(), `bill-tracker-ofx-${process.pid}.sqlite`); const dbPath = path.join(os.tmpdir(), `bill-tracker-ofx-${process.pid}.sqlite`);
process.env.DB_PATH = dbPath; 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'); const { parseOfx, previewOfxTransactions, commitOfxTransactions } = require('../services/ofxImportService.cts');
// OFX 1.x SGML — container tags closed, leaf tags unclosed. // OFX 1.x SGML — container tags closed, leaf tags unclosed.

View File

@ -15,7 +15,7 @@ const fs = require('node:fs');
const dbPath = path.join(os.tmpdir(), `bill-tracker-payacct-${process.pid}.sqlite`); const dbPath = path.join(os.tmpdir(), `bill-tracker-payacct-${process.pid}.sqlite`);
process.env.DB_PATH = dbPath; process.env.DB_PATH = dbPath;
const { getDb, closeDb } = require('../db/database'); const { getDb, closeDb } = require('../db/database.cts');
const { const {
isBankBackedPayment, isBankBackedPayment,
accountingActiveSql, accountingActiveSql,

View File

@ -13,7 +13,7 @@ const fs = require('node:fs');
const dbPath = path.join(os.tmpdir(), `bill-tracker-quickpay-${process.pid}.sqlite`); const dbPath = path.join(os.tmpdir(), `bill-tracker-quickpay-${process.pid}.sqlite`);
process.env.DB_PATH = dbPath; process.env.DB_PATH = dbPath;
const { getDb, closeDb } = require('../db/database'); const { getDb, closeDb } = require('../db/database.cts');
const router = require('../routes/payments.cts'); const router = require('../routes/payments.cts');
function handler(method, routePath) { function handler(method, routePath) {

View File

@ -14,7 +14,7 @@ const fs = require('node:fs');
const dbPath = path.join(os.tmpdir(), `bill-tracker-payments-${process.pid}.sqlite`); const dbPath = path.join(os.tmpdir(), `bill-tracker-payments-${process.pid}.sqlite`);
process.env.DB_PATH = dbPath; process.env.DB_PATH = dbPath;
const { getDb, closeDb } = require('../db/database'); const { getDb, closeDb } = require('../db/database.cts');
const router = require('../routes/payments.cts'); const router = require('../routes/payments.cts');
function handler(method, routePath) { function handler(method, routePath) {

View File

@ -7,7 +7,7 @@ const path = require('node:path');
const dbPath = path.join(os.tmpdir(), `bill-tracker-profile-route-test-${process.pid}.sqlite`); const dbPath = path.join(os.tmpdir(), `bill-tracker-profile-route-test-${process.pid}.sqlite`);
process.env.DB_PATH = dbPath; process.env.DB_PATH = dbPath;
const { getDb, closeDb } = require('../db/database'); const { getDb, closeDb } = require('../db/database.cts');
const { publicUser } = require('../services/authService.cts'); const { publicUser } = require('../services/authService.cts');
function createUser(db, suffix) { function createUser(db, suffix) {

View File

@ -17,7 +17,7 @@ const fs = require('node:fs');
const dbPath = path.join(os.tmpdir(), `bill-tracker-recon-${process.pid}.sqlite`); const dbPath = path.join(os.tmpdir(), `bill-tracker-recon-${process.pid}.sqlite`);
process.env.DB_PATH = dbPath; 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 { getTracker } = require('../services/trackerService.cts');
const { getAnalyticsSummary } = require('../services/analyticsService.cts'); const { getAnalyticsSummary } = require('../services/analyticsService.cts');
const { resolveDueDate } = require('../services/statusService.cts'); const { resolveDueDate } = require('../services/statusService.cts');

View File

@ -12,7 +12,7 @@ const fs = require('node:fs');
const dbPath = path.join(os.tmpdir(), `bill-tracker-snowball-plan-${process.pid}.sqlite`); const dbPath = path.join(os.tmpdir(), `bill-tracker-snowball-plan-${process.pid}.sqlite`);
process.env.DB_PATH = dbPath; process.env.DB_PATH = dbPath;
const { getDb, closeDb } = require('../db/database'); const { getDb, closeDb } = require('../db/database.cts');
const router = require('../routes/snowball.cts'); const router = require('../routes/snowball.cts');
function handler(method, routePath) { function handler(method, routePath) {

View File

@ -13,7 +13,7 @@ const fs = require('node:fs');
const dbPath = path.join(os.tmpdir(), `bill-tracker-spendsvc-${process.pid}.sqlite`); const dbPath = path.join(os.tmpdir(), `bill-tracker-spendsvc-${process.pid}.sqlite`);
process.env.DB_PATH = dbPath; 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'); const { setSpendingBudget, getSpendingBudgets } = require('../services/spendingService.cts');
let db, userId, catId; let db, userId, catId;

View File

@ -7,7 +7,7 @@ const path = require('node:path');
const dbPath = path.join(os.tmpdir(), `bill-tracker-spending-summary-test-${process.pid}.sqlite`); const dbPath = path.join(os.tmpdir(), `bill-tracker-spending-summary-test-${process.pid}.sqlite`);
process.env.DB_PATH = dbPath; 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'); const { getSpendingSummary, setSpendingBudget } = require('../services/spendingService.cts');
function createUser(db, suffix) { function createUser(db, suffix) {

View File

@ -7,7 +7,7 @@ const path = require('node:path');
const dbPath = path.join(os.tmpdir(), `bill-tracker-subscription-service-test-${process.pid}.sqlite`); const dbPath = path.join(os.tmpdir(), `bill-tracker-subscription-service-test-${process.pid}.sqlite`);
process.env.DB_PATH = dbPath; process.env.DB_PATH = dbPath;
const { getDb, closeDb } = require('../db/database'); const { getDb, closeDb } = require('../db/database.cts');
const { const {
getSubscriptionRecommendations, getSubscriptionRecommendations,
recordSubscriptionFeedback, recordSubscriptionFeedback,

View File

@ -12,7 +12,7 @@ const fs = require('node:fs');
const dbPath = path.join(os.tmpdir(), `bill-tracker-summary-bt-test-${process.pid}.sqlite`); const dbPath = path.join(os.tmpdir(), `bill-tracker-summary-bt-test-${process.pid}.sqlite`);
process.env.DB_PATH = dbPath; process.env.DB_PATH = dbPath;
const { getDb, closeDb } = require('../db/database'); const { getDb, closeDb } = require('../db/database.cts');
function callSummary(userId, year, month) { function callSummary(userId, year, month) {
const router = require('../routes/summary.cts'); const router = require('../routes/summary.cts');

View File

@ -12,7 +12,7 @@ const fs = require('node:fs');
const dbPath = path.join(os.tmpdir(), `bill-tracker-summary-skip-test-${process.pid}.sqlite`); const dbPath = path.join(os.tmpdir(), `bill-tracker-summary-skip-test-${process.pid}.sqlite`);
process.env.DB_PATH = dbPath; process.env.DB_PATH = dbPath;
const { getDb, closeDb } = require('../db/database'); const { getDb, closeDb } = require('../db/database.cts');
function callSummary(userId, year, month) { function callSummary(userId, year, month) {
const router = require('../routes/summary.cts'); const router = require('../routes/summary.cts');

View File

@ -14,7 +14,7 @@ const { generateSync } = require('otplib');
const dbPath = path.join(os.tmpdir(), `bill-tracker-totp-${process.pid}.sqlite`); const dbPath = path.join(os.tmpdir(), `bill-tracker-totp-${process.pid}.sqlite`);
process.env.DB_PATH = dbPath; 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 { encryptSecret } = require('../services/encryptionService.cts');
const totp = require('../services/totpService.cts'); const totp = require('../services/totpService.cts');

View File

@ -14,7 +14,7 @@ const fs = require('node:fs');
const dbPath = path.join(os.tmpdir(), `bill-tracker-trackersvc-${process.pid}.sqlite`); const dbPath = path.join(os.tmpdir(), `bill-tracker-trackersvc-${process.pid}.sqlite`);
process.env.DB_PATH = dbPath; 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'); const { getTracker, getOverdueCount } = require('../services/trackerService.cts');
// Fixed "now" so occurrence gating + statuses are deterministic (mid-June 2026). // Fixed "now" so occurrence gating + statuses are deterministic (mid-June 2026).

View File

@ -7,7 +7,7 @@ const path = require('node:path');
const dbPath = path.join(os.tmpdir(), `bill-tracker-transaction-match-test-${process.pid}.sqlite`); const dbPath = path.join(os.tmpdir(), `bill-tracker-transaction-match-test-${process.pid}.sqlite`);
process.env.DB_PATH = dbPath; 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 { ensureManualDataSource } = require('../services/transactionService.cts');
const { getTracker } = require('../services/trackerService.cts'); const { getTracker } = require('../services/trackerService.cts');
const { const {

View File

@ -12,7 +12,7 @@ const fs = require('node:fs');
const dbPath = path.join(os.tmpdir(), `bill-tracker-match-state-${process.pid}.sqlite`); const dbPath = path.join(os.tmpdir(), `bill-tracker-match-state-${process.pid}.sqlite`);
process.env.DB_PATH = dbPath; 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'); const { markMatched, markUnmatched, markIgnored } = require('../services/transactionMatchState.cts');
let db, userId, otherId, billId; let db, userId, otherId, billId;

View File

@ -12,7 +12,7 @@ const fs = require('node:fs');
const dbPath = path.join(os.tmpdir(), `bill-tracker-updatecheck-test-${process.pid}.sqlite`); const dbPath = path.join(os.tmpdir(), `bill-tracker-updatecheck-test-${process.pid}.sqlite`);
process.env.DB_PATH = dbPath; 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'); const svc = require('../services/updateCheckService.cts');
test('disabled: makes no external request and returns disabled', async () => { test('disabled: makes no external request and returns disabled', async () => {

View File

@ -1,7 +1,9 @@
'use strict'; 'use strict';
import type { Db } from '../types/db';
const cron = require('node-cron'); 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 { buildTrackerRow, getCycleRange } = require('../services/statusService.cts');
const { pruneExpiredSessions } = require('../services/authService.cts'); const { pruneExpiredSessions } = require('../services/authService.cts');
const { pruneExpiredChallenges: pruneWebAuthnChallenges } = require('../services/webauthnService.cts'); const { pruneExpiredChallenges: pruneWebAuthnChallenges } = require('../services/webauthnService.cts');
@ -14,42 +16,35 @@ const {
} = require('../services/statusRuntime.cts'); } = require('../services/statusRuntime.cts');
const { localDateString, localDateStringDaysAgo } = require('../utils/dates.mts'); const { localDateString, localDateStringDaysAgo } = require('../utils/dates.mts');
// The hour (023) the single daily job runs — configurable via the global // The hour (023) the single daily job runs — configurable via 'reminder_hour'
// 'reminder_hour' setting, defaulting to 6 AM. One source of truth for both the // (default 6 AM). One source of truth for both the cron expression and next-run.
// cron expression and the next-run display. function resolveReminderHour(): number {
function resolveReminderHour() {
const parsed = parseInt(getSetting('reminder_hour') ?? '6', 10); const parsed = parseInt(getSetting('reminder_hour') ?? '6', 10);
return Number.isInteger(parsed) && parsed >= 0 && parsed <= 23 ? parsed : 6; 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); const next = new Date(from);
next.setHours(resolveReminderHour(), 0, 0, 0); next.setHours(resolveReminderHour(), 0, 0, 0);
if (next <= from) next.setDate(next.getDate() + 1); if (next <= from) next.setDate(next.getDate() + 1);
return next.toISOString(); return next.toISOString();
} }
async function runDailyTasks() { async function runDailyTasks(): Promise<void> {
const db = getDb(); const db: Db = getDb();
const now = new Date(); const now = new Date();
const year = now.getFullYear(); const year = now.getFullYear();
const month = now.getMonth() + 1; 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 todayStr = localDateString(now);
const bills = db.prepare('SELECT * FROM bills WHERE active = 1 AND deleted_at IS NULL').all(); const bills = db.prepare('SELECT * FROM bills WHERE active = 1 AND deleted_at IS NULL').all();
if (bills.length > 0) { if (bills.length > 0) {
// Batch-fetch payments for all active bills from the last 90 days to avoid const billIds = bills.map((b: any) => b.id);
// 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 placeholders = billIds.map(() => '?').join(','); const placeholders = billIds.map(() => '?').join(',');
const windowStart = localDateStringDaysAgo(90, now); const windowStart = localDateStringDaysAgo(90, now);
let allPayments = []; let allPayments: any[] = [];
try { try {
allPayments = db.prepare(` allPayments = db.prepare(`
SELECT * FROM payments SELECT * FROM payments
@ -57,18 +52,17 @@ async function runDailyTasks() {
AND paid_date >= ? AND paid_date >= ?
AND deleted_at IS NULL AND deleted_at IS NULL
`).all(...billIds, windowStart); `).all(...billIds, windowStart);
} catch (err) { } catch (err: any) {
console.error('[worker] Failed to batch-fetch payments:', err.message); console.error('[worker] Failed to batch-fetch payments:', err.message);
} }
// Group payments by bill_id in memory // Group payments by bill_id in memory
const paymentsByBill = new Map(); const paymentsByBill = new Map<any, any>();
for (const p of allPayments) { for (const p of allPayments) {
if (!paymentsByBill.has(p.bill_id)) paymentsByBill.set(p.bill_id, []); if (!paymentsByBill.has(p.bill_id)) paymentsByBill.set(p.bill_id, []);
paymentsByBill.get(p.bill_id).push(p); paymentsByBill.get(p.bill_id).push(p);
} }
// Prepare autopay update statement once outside the loop
const markAutopay = db.prepare( const markAutopay = db.prepare(
"UPDATE bills SET autodraft_status = 'assumed_paid', updated_at = datetime('now') WHERE id = ?" "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); const range = getCycleRange(year, month, bill);
if (!range) continue; // bill does not apply this cycle 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( 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); const row = buildTrackerRow(bill, payments, year, month, todayStr);
@ -93,7 +86,7 @@ async function runDailyTasks() {
) { ) {
try { try {
markAutopay.run(bill.id); markAutopay.run(bill.id);
} catch (err) { } catch (err: any) {
console.error(`[worker] Failed to mark autopay for bill ${bill.id}:`, err.message); console.error(`[worker] Failed to mark autopay for bill ${bill.id}:`, err.message);
} }
} }
@ -103,15 +96,15 @@ async function runDailyTasks() {
pruneExpiredSessions(); pruneExpiredSessions();
pruneWebAuthnChallenges(db); pruneWebAuthnChallenges(db);
await runNotifications().catch(err => { await runNotifications().catch((err: any) => {
console.error('[worker] Notification error (non-fatal):', err.message); 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); 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); console.error('[worker] Cleanup error (non-fatal):', err.message);
}); });
@ -119,40 +112,39 @@ async function runDailyTasks() {
console.log(`[worker] Daily tasks ran at ${todayStr}`); 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. // (Re)create the daily cron at the configured hour. Stops any prior task first.
function scheduleDaily() { function scheduleDaily(): void {
if (scheduledTask) { if (scheduledTask) {
scheduledTask.stop(); scheduledTask.stop();
scheduledTask = null; scheduledTask = null;
} }
const hour = resolveReminderHour(); const hour = resolveReminderHour();
scheduledTask = cron.schedule(`0 ${hour} * * *`, () => { scheduledTask = cron.schedule(`0 ${hour} * * *`, () => {
runDailyTasks().catch(err => { runDailyTasks().catch((err: any) => {
markWorkerError(err, nextDailyRunIso()); markWorkerError(err, nextDailyRunIso());
console.error('[worker] Daily task error:', err); console.error('[worker] Daily task error:', err);
}); });
}); });
} }
// Called when the 'reminder_hour' setting changes so it applies without a // Called when 'reminder_hour' changes so it applies without a restart.
// restart. Defensive: a failure here must never take the worker down. function rescheduleDailyWorker(): void {
function rescheduleDailyWorker() {
try { try {
scheduleDaily(); scheduleDaily();
markWorkerStarted(nextDailyRunIso()); markWorkerStarted(nextDailyRunIso());
console.log(`[worker] Rescheduled daily tasks to ${resolveReminderHour()}:00`); console.log(`[worker] Rescheduled daily tasks to ${resolveReminderHour()}:00`);
} catch (err) { } catch (err: any) {
console.error('[worker] Failed to reschedule daily worker:', err.message); console.error('[worker] Failed to reschedule daily worker:', err.message);
} }
} }
function start() { function start(): void {
markWorkerStarted(nextDailyRunIso()); markWorkerStarted(nextDailyRunIso());
// Run once at startup // Run once at startup
runDailyTasks().catch(err => { runDailyTasks().catch((err: any) => {
markWorkerError(err, nextDailyRunIso()); markWorkerError(err, nextDailyRunIso());
console.error('[worker] Startup task error:', err); console.error('[worker] Startup task error:', err);
}); });