feat: SimpleFin bank sync with encrypted token storage
New services:
services/encryptionService.js — AES-256-GCM with SHA-256 derived key
services/simplefinService.js — protocol layer: claim token, fetch accounts/transactions, normalize to DB shapes
services/bankSyncService.js — orchestration: connect, sync, disconnect with encrypted access URL storage
Modified:
routes/dataSources.js — status, connect, sync, disconnect endpoints (gate on BANK_SYNC_ENABLED=true)
client/api.js — simplefinStatus, connectSimplefin, syncDataSource, deleteDataSource, dataSources
client/pages/SettingsPage.jsx — BankSyncSection with connected account info, sync/disconnect actions, setup token input
.env.example — BANK_SYNC_ENABLED, TOKEN_ENCRYPTION_KEY, SIMPLEFIN_APP_NAME
2026-05-28 21:30:20 -05:00
'use strict' ;
2026-05-16 20:26:09 -05:00
const router = require ( 'express' ) . Router ( ) ;
feat: SimpleFin bank sync with encrypted token storage
New services:
services/encryptionService.js — AES-256-GCM with SHA-256 derived key
services/simplefinService.js — protocol layer: claim token, fetch accounts/transactions, normalize to DB shapes
services/bankSyncService.js — orchestration: connect, sync, disconnect with encrypted access URL storage
Modified:
routes/dataSources.js — status, connect, sync, disconnect endpoints (gate on BANK_SYNC_ENABLED=true)
client/api.js — simplefinStatus, connectSimplefin, syncDataSource, deleteDataSource, dataSources
client/pages/SettingsPage.jsx — BankSyncSection with connected account info, sync/disconnect actions, setup token input
.env.example — BANK_SYNC_ENABLED, TOKEN_ENCRYPTION_KEY, SIMPLEFIN_APP_NAME
2026-05-28 21:30:20 -05:00
const { getDb } = require ( '../db/database' ) ;
const { standardizeError } = require ( '../middleware/errorFormatter' ) ;
2026-05-16 20:26:09 -05:00
const { decorateDataSource , ensureManualDataSource } = require ( '../services/transactionService' ) ;
feat: SimpleFin bank sync with encrypted token storage
New services:
services/encryptionService.js — AES-256-GCM with SHA-256 derived key
services/simplefinService.js — protocol layer: claim token, fetch accounts/transactions, normalize to DB shapes
services/bankSyncService.js — orchestration: connect, sync, disconnect with encrypted access URL storage
Modified:
routes/dataSources.js — status, connect, sync, disconnect endpoints (gate on BANK_SYNC_ENABLED=true)
client/api.js — simplefinStatus, connectSimplefin, syncDataSource, deleteDataSource, dataSources
client/pages/SettingsPage.jsx — BankSyncSection with connected account info, sync/disconnect actions, setup token input
.env.example — BANK_SYNC_ENABLED, TOKEN_ENCRYPTION_KEY, SIMPLEFIN_APP_NAME
2026-05-28 21:30:20 -05:00
const { connectSimplefin , syncDataSource , disconnectDataSource } = require ( '../services/bankSyncService' ) ;
const { sanitizeErrorMessage } = require ( '../services/simplefinService' ) ;
2026-05-28 22:06:15 -05:00
const { getBankSyncConfig } = require ( '../services/bankSyncConfigService' ) ;
2026-05-16 20:26:09 -05:00
feat: SimpleFin bank sync with encrypted token storage
New services:
services/encryptionService.js — AES-256-GCM with SHA-256 derived key
services/simplefinService.js — protocol layer: claim token, fetch accounts/transactions, normalize to DB shapes
services/bankSyncService.js — orchestration: connect, sync, disconnect with encrypted access URL storage
Modified:
routes/dataSources.js — status, connect, sync, disconnect endpoints (gate on BANK_SYNC_ENABLED=true)
client/api.js — simplefinStatus, connectSimplefin, syncDataSource, deleteDataSource, dataSources
client/pages/SettingsPage.jsx — BankSyncSection with connected account info, sync/disconnect actions, setup token input
.env.example — BANK_SYNC_ENABLED, TOKEN_ENCRYPTION_KEY, SIMPLEFIN_APP_NAME
2026-05-28 21:30:20 -05:00
const VALID _TYPES = new Set ( [ 'manual' , 'file_import' , 'provider_sync' ] ) ;
2026-05-16 20:26:09 -05:00
const VALID _STATUSES = new Set ( [ 'active' , 'inactive' , 'error' ] ) ;
function cleanFilter ( value ) {
return typeof value === 'string' ? value . trim ( ) : '' ;
}
feat: SimpleFin bank sync with encrypted token storage
New services:
services/encryptionService.js — AES-256-GCM with SHA-256 derived key
services/simplefinService.js — protocol layer: claim token, fetch accounts/transactions, normalize to DB shapes
services/bankSyncService.js — orchestration: connect, sync, disconnect with encrypted access URL storage
Modified:
routes/dataSources.js — status, connect, sync, disconnect endpoints (gate on BANK_SYNC_ENABLED=true)
client/api.js — simplefinStatus, connectSimplefin, syncDataSource, deleteDataSource, dataSources
client/pages/SettingsPage.jsx — BankSyncSection with connected account info, sync/disconnect actions, setup token input
.env.example — BANK_SYNC_ENABLED, TOKEN_ENCRYPTION_KEY, SIMPLEFIN_APP_NAME
2026-05-28 21:30:20 -05:00
function safeError ( err , fallback ) {
const msg = sanitizeErrorMessage ( err ? . message || fallback ) ;
const status = typeof err ? . status === 'number' ? err . status : 500 ;
return { msg , status } ;
}
// ─── GET /api/data-sources ────────────────────────────────────────────────────
2026-05-16 20:26:09 -05:00
router . get ( '/' , ( req , res ) => {
const db = getDb ( ) ;
ensureManualDataSource ( db , req . user . id ) ;
feat: SimpleFin bank sync with encrypted token storage
New services:
services/encryptionService.js — AES-256-GCM with SHA-256 derived key
services/simplefinService.js — protocol layer: claim token, fetch accounts/transactions, normalize to DB shapes
services/bankSyncService.js — orchestration: connect, sync, disconnect with encrypted access URL storage
Modified:
routes/dataSources.js — status, connect, sync, disconnect endpoints (gate on BANK_SYNC_ENABLED=true)
client/api.js — simplefinStatus, connectSimplefin, syncDataSource, deleteDataSource, dataSources
client/pages/SettingsPage.jsx — BankSyncSection with connected account info, sync/disconnect actions, setup token input
.env.example — BANK_SYNC_ENABLED, TOKEN_ENCRYPTION_KEY, SIMPLEFIN_APP_NAME
2026-05-28 21:30:20 -05:00
const type = cleanFilter ( req . query . type ) ;
2026-05-16 20:26:09 -05:00
const status = cleanFilter ( req . query . status ) ;
feat: SimpleFin bank sync with encrypted token storage
New services:
services/encryptionService.js — AES-256-GCM with SHA-256 derived key
services/simplefinService.js — protocol layer: claim token, fetch accounts/transactions, normalize to DB shapes
services/bankSyncService.js — orchestration: connect, sync, disconnect with encrypted access URL storage
Modified:
routes/dataSources.js — status, connect, sync, disconnect endpoints (gate on BANK_SYNC_ENABLED=true)
client/api.js — simplefinStatus, connectSimplefin, syncDataSource, deleteDataSource, dataSources
client/pages/SettingsPage.jsx — BankSyncSection with connected account info, sync/disconnect actions, setup token input
.env.example — BANK_SYNC_ENABLED, TOKEN_ENCRYPTION_KEY, SIMPLEFIN_APP_NAME
2026-05-28 21:30:20 -05:00
if ( type && ! VALID _TYPES . has ( type ) ) return res . status ( 400 ) . json ( standardizeError ( 'type must be manual, file_import, or provider_sync' , 'VALIDATION_ERROR' , 'type' ) ) ;
if ( status && ! VALID _STATUSES . has ( status ) ) return res . status ( 400 ) . json ( standardizeError ( 'status must be active, inactive, or error' , 'VALIDATION_ERROR' , 'status' ) ) ;
2026-05-16 20:26:09 -05:00
let query = `
SELECT
ds . id , ds . user _id , ds . type , ds . provider , ds . name , ds . status ,
ds . config _json , ds . last _sync _at , ds . last _error , ds . created _at , ds . updated _at ,
COUNT ( DISTINCT fa . id ) AS account _count ,
feat: SimpleFin bank sync with encrypted token storage
New services:
services/encryptionService.js — AES-256-GCM with SHA-256 derived key
services/simplefinService.js — protocol layer: claim token, fetch accounts/transactions, normalize to DB shapes
services/bankSyncService.js — orchestration: connect, sync, disconnect with encrypted access URL storage
Modified:
routes/dataSources.js — status, connect, sync, disconnect endpoints (gate on BANK_SYNC_ENABLED=true)
client/api.js — simplefinStatus, connectSimplefin, syncDataSource, deleteDataSource, dataSources
client/pages/SettingsPage.jsx — BankSyncSection with connected account info, sync/disconnect actions, setup token input
.env.example — BANK_SYNC_ENABLED, TOKEN_ENCRYPTION_KEY, SIMPLEFIN_APP_NAME
2026-05-28 21:30:20 -05:00
COUNT ( DISTINCT t . id ) AS transaction _count
2026-05-16 20:26:09 -05:00
FROM data _sources ds
LEFT JOIN financial _accounts fa ON fa . data _source _id = ds . id AND fa . user _id = ds . user _id
feat: SimpleFin bank sync with encrypted token storage
New services:
services/encryptionService.js — AES-256-GCM with SHA-256 derived key
services/simplefinService.js — protocol layer: claim token, fetch accounts/transactions, normalize to DB shapes
services/bankSyncService.js — orchestration: connect, sync, disconnect with encrypted access URL storage
Modified:
routes/dataSources.js — status, connect, sync, disconnect endpoints (gate on BANK_SYNC_ENABLED=true)
client/api.js — simplefinStatus, connectSimplefin, syncDataSource, deleteDataSource, dataSources
client/pages/SettingsPage.jsx — BankSyncSection with connected account info, sync/disconnect actions, setup token input
.env.example — BANK_SYNC_ENABLED, TOKEN_ENCRYPTION_KEY, SIMPLEFIN_APP_NAME
2026-05-28 21:30:20 -05:00
LEFT JOIN transactions t ON t . data _source _id = ds . id AND t . user _id = ds . user _id
2026-05-16 20:26:09 -05:00
WHERE ds . user _id = ?
` ;
const params = [ req . user . id ] ;
feat: SimpleFin bank sync with encrypted token storage
New services:
services/encryptionService.js — AES-256-GCM with SHA-256 derived key
services/simplefinService.js — protocol layer: claim token, fetch accounts/transactions, normalize to DB shapes
services/bankSyncService.js — orchestration: connect, sync, disconnect with encrypted access URL storage
Modified:
routes/dataSources.js — status, connect, sync, disconnect endpoints (gate on BANK_SYNC_ENABLED=true)
client/api.js — simplefinStatus, connectSimplefin, syncDataSource, deleteDataSource, dataSources
client/pages/SettingsPage.jsx — BankSyncSection with connected account info, sync/disconnect actions, setup token input
.env.example — BANK_SYNC_ENABLED, TOKEN_ENCRYPTION_KEY, SIMPLEFIN_APP_NAME
2026-05-28 21:30:20 -05:00
if ( type ) { query += ' AND ds.type = ?' ; params . push ( type ) ; }
if ( status ) { query += ' AND ds.status = ?' ; params . push ( status ) ; }
2026-05-16 20:26:09 -05:00
query += `
GROUP BY ds . id
ORDER BY
CASE WHEN ds . type = 'manual' THEN 0 ELSE 1 END ,
ds . name COLLATE NOCASE ASC
` ;
res . json ( db . prepare ( query ) . all ( ... params ) . map ( decorateDataSource ) ) ;
} ) ;
feat: SimpleFin bank sync with encrypted token storage
New services:
services/encryptionService.js — AES-256-GCM with SHA-256 derived key
services/simplefinService.js — protocol layer: claim token, fetch accounts/transactions, normalize to DB shapes
services/bankSyncService.js — orchestration: connect, sync, disconnect with encrypted access URL storage
Modified:
routes/dataSources.js — status, connect, sync, disconnect endpoints (gate on BANK_SYNC_ENABLED=true)
client/api.js — simplefinStatus, connectSimplefin, syncDataSource, deleteDataSource, dataSources
client/pages/SettingsPage.jsx — BankSyncSection with connected account info, sync/disconnect actions, setup token input
.env.example — BANK_SYNC_ENABLED, TOKEN_ENCRYPTION_KEY, SIMPLEFIN_APP_NAME
2026-05-28 21:30:20 -05:00
// ─── GET /api/data-sources/simplefin/status ──────────────────────────────────
router . get ( '/simplefin/status' , ( req , res ) => {
2026-05-28 22:06:15 -05:00
const { enabled } = getBankSyncConfig ( ) ;
res . json ( { enabled } ) ;
feat: SimpleFin bank sync with encrypted token storage
New services:
services/encryptionService.js — AES-256-GCM with SHA-256 derived key
services/simplefinService.js — protocol layer: claim token, fetch accounts/transactions, normalize to DB shapes
services/bankSyncService.js — orchestration: connect, sync, disconnect with encrypted access URL storage
Modified:
routes/dataSources.js — status, connect, sync, disconnect endpoints (gate on BANK_SYNC_ENABLED=true)
client/api.js — simplefinStatus, connectSimplefin, syncDataSource, deleteDataSource, dataSources
client/pages/SettingsPage.jsx — BankSyncSection with connected account info, sync/disconnect actions, setup token input
.env.example — BANK_SYNC_ENABLED, TOKEN_ENCRYPTION_KEY, SIMPLEFIN_APP_NAME
2026-05-28 21:30:20 -05:00
} ) ;
// ─── POST /api/data-sources/simplefin/connect ────────────────────────────────
router . post ( '/simplefin/connect' , async ( req , res ) => {
2026-05-28 22:06:15 -05:00
if ( ! getBankSyncConfig ( ) . enabled ) {
feat: SimpleFin bank sync with encrypted token storage
New services:
services/encryptionService.js — AES-256-GCM with SHA-256 derived key
services/simplefinService.js — protocol layer: claim token, fetch accounts/transactions, normalize to DB shapes
services/bankSyncService.js — orchestration: connect, sync, disconnect with encrypted access URL storage
Modified:
routes/dataSources.js — status, connect, sync, disconnect endpoints (gate on BANK_SYNC_ENABLED=true)
client/api.js — simplefinStatus, connectSimplefin, syncDataSource, deleteDataSource, dataSources
client/pages/SettingsPage.jsx — BankSyncSection with connected account info, sync/disconnect actions, setup token input
.env.example — BANK_SYNC_ENABLED, TOKEN_ENCRYPTION_KEY, SIMPLEFIN_APP_NAME
2026-05-28 21:30:20 -05:00
return res . status ( 503 ) . json ( standardizeError ( 'Bank sync is not enabled on this server' , 'BANK_SYNC_DISABLED' ) ) ;
}
const setupToken = typeof req . body ? . setupToken === 'string' ? req . body . setupToken . trim ( ) : '' ;
if ( ! setupToken ) {
return res . status ( 400 ) . json ( standardizeError ( 'setupToken is required' , 'VALIDATION_ERROR' , 'setupToken' ) ) ;
}
try {
const db = getDb ( ) ;
const result = await connectSimplefin ( db , req . user . id , setupToken ) ;
res . status ( 201 ) . json ( result ) ;
} catch ( err ) {
const { msg , status } = safeError ( err , 'Failed to connect SimpleFIN' ) ;
res . status ( status ) . json ( standardizeError ( msg , err ? . code || 'SIMPLEFIN_ERROR' ) ) ;
}
} ) ;
// ─── POST /api/data-sources/:id/sync ─────────────────────────────────────────
router . post ( '/:id/sync' , async ( req , res ) => {
2026-05-28 22:06:15 -05:00
if ( ! getBankSyncConfig ( ) . enabled ) {
feat: SimpleFin bank sync with encrypted token storage
New services:
services/encryptionService.js — AES-256-GCM with SHA-256 derived key
services/simplefinService.js — protocol layer: claim token, fetch accounts/transactions, normalize to DB shapes
services/bankSyncService.js — orchestration: connect, sync, disconnect with encrypted access URL storage
Modified:
routes/dataSources.js — status, connect, sync, disconnect endpoints (gate on BANK_SYNC_ENABLED=true)
client/api.js — simplefinStatus, connectSimplefin, syncDataSource, deleteDataSource, dataSources
client/pages/SettingsPage.jsx — BankSyncSection with connected account info, sync/disconnect actions, setup token input
.env.example — BANK_SYNC_ENABLED, TOKEN_ENCRYPTION_KEY, SIMPLEFIN_APP_NAME
2026-05-28 21:30:20 -05:00
return res . status ( 503 ) . json ( standardizeError ( 'Bank sync is not enabled on this server' , 'BANK_SYNC_DISABLED' ) ) ;
}
const id = parseInt ( req . params . id , 10 ) ;
if ( ! Number . isInteger ( id ) || id < 1 ) {
return res . status ( 400 ) . json ( standardizeError ( 'Invalid data source id' , 'VALIDATION_ERROR' , 'id' ) ) ;
}
try {
const db = getDb ( ) ;
const result = await syncDataSource ( db , req . user . id , id ) ;
res . json ( result ) ;
} catch ( err ) {
const { msg , status } = safeError ( err , 'Sync failed' ) ;
res . status ( status ) . json ( standardizeError ( msg , err ? . code || 'SIMPLEFIN_ERROR' ) ) ;
}
} ) ;
// ─── DELETE /api/data-sources/:id ────────────────────────────────────────────
router . delete ( '/:id' , ( req , res ) => {
2026-05-28 22:06:15 -05:00
if ( ! getBankSyncConfig ( ) . enabled ) {
feat: SimpleFin bank sync with encrypted token storage
New services:
services/encryptionService.js — AES-256-GCM with SHA-256 derived key
services/simplefinService.js — protocol layer: claim token, fetch accounts/transactions, normalize to DB shapes
services/bankSyncService.js — orchestration: connect, sync, disconnect with encrypted access URL storage
Modified:
routes/dataSources.js — status, connect, sync, disconnect endpoints (gate on BANK_SYNC_ENABLED=true)
client/api.js — simplefinStatus, connectSimplefin, syncDataSource, deleteDataSource, dataSources
client/pages/SettingsPage.jsx — BankSyncSection with connected account info, sync/disconnect actions, setup token input
.env.example — BANK_SYNC_ENABLED, TOKEN_ENCRYPTION_KEY, SIMPLEFIN_APP_NAME
2026-05-28 21:30:20 -05:00
return res . status ( 503 ) . json ( standardizeError ( 'Bank sync is not enabled on this server' , 'BANK_SYNC_DISABLED' ) ) ;
}
const id = parseInt ( req . params . id , 10 ) ;
if ( ! Number . isInteger ( id ) || id < 1 ) {
return res . status ( 400 ) . json ( standardizeError ( 'Invalid data source id' , 'VALIDATION_ERROR' , 'id' ) ) ;
}
try {
disconnectDataSource ( getDb ( ) , req . user . id , id ) ;
res . json ( { ok : true } ) ;
} catch ( err ) {
const { msg , status } = safeError ( err , 'Failed to disconnect' ) ;
res . status ( status ) . json ( standardizeError ( msg , err ? . code || 'DISCONNECT_ERROR' ) ) ;
}
} ) ;
2026-05-16 20:26:09 -05:00
module . exports = router ;