All remaining backend files renamed:
src/middleware/auth.ts, logging.ts (2 files)
src/utils/*.ts (20 files: ai, auditQueue, config, crypto,
embeddings, errors, fileType, logger, models,
notify, passwords, platform, promptSafe,
prompts, redact, sessions, transcribe*,
ttsGoogle)
src/db/database.ts, migrate.ts (2 files)
Spot-fixes to satisfy tsc (all within the spirit of 'no behavior
change' — added `: any` annotations where the original JS relied on
duck typing that tsc's default inference narrows too aggressively):
utils/ai.ts — body, converseParams, request literals + fallback
result object + err.code/model/message casts. AI client has lots
of provider-specific ad-hoc object shapes; Day 5 will replace the
`any`s with proper provider-response interfaces.
utils/embeddings.ts — payload + request as `any`; generateEmbedding
call sites pass `undefined as any` for the now-required second
arg (model) until we refactor the signature.
utils/prompts.ts — PROMPTS typed as Record<string, any> so
.loadFromDb / .updatePrompt / .getAllPrompts attachments after
the const literal compile.
utils/transcribeLocal.ts — buildArgs() has two `var args = [...]`
in the same function scope (var-hoisted); both now typed as
any[] so they don't type-clash across conditionals.
Backend is now 54 of 54 TypeScript files, permissive mode.
`npm run typecheck` EXIT 0. Prod container still running the old
JS image — no Dockerfile change yet.
Next: Day 5 flips strict: true, fixes every error tsc surfaces, adds
Vitest + Zod + Knip tooling.
113 lines
3.7 KiB
TypeScript
113 lines
3.7 KiB
TypeScript
// ============================================================
|
|
// Audit log batcher — collects audit/api/access entries in memory
|
|
// and flushes them to Postgres as batched multi-row INSERTs on a
|
|
// 1-second interval or when a buffer reaches 50 entries. Reduces
|
|
// per-request DB load from "one INSERT per audit call" to "one
|
|
// INSERT per ~50 audit calls under load". Transparent to callers.
|
|
//
|
|
// Trade-off: if the process crashes between flushes, up to ~1s of
|
|
// audit entries are lost. The PostgreSQL row is the primary
|
|
// destination; Loki is separately pushed fire-and-forget per call
|
|
// and is not batched (Loki has its own server-side ingestion that
|
|
// handles batching more cheaply).
|
|
// ============================================================
|
|
|
|
var FLUSH_INTERVAL_MS = 1000;
|
|
var FLUSH_MAX_BATCH = 50;
|
|
|
|
function createQueue(tableName, columns) {
|
|
var buffer = [];
|
|
var flushing = false;
|
|
var timer = null;
|
|
|
|
// Build "($1,$2,$3,...),($N,$N+1,...),..." with running placeholder index
|
|
function buildBatchSql(rows) {
|
|
var cols = columns.join(', ');
|
|
var placeholders = [];
|
|
var params = [];
|
|
var k = 1;
|
|
for (var i = 0; i < rows.length; i++) {
|
|
var row = rows[i];
|
|
var tuple = [];
|
|
for (var j = 0; j < columns.length; j++) {
|
|
tuple.push('$' + k++);
|
|
params.push(row[columns[j]] != null ? row[columns[j]] : null);
|
|
}
|
|
placeholders.push('(' + tuple.join(',') + ')');
|
|
}
|
|
return {
|
|
sql: 'INSERT INTO ' + tableName + ' (' + cols + ') VALUES ' + placeholders.join(','),
|
|
params: params
|
|
};
|
|
}
|
|
|
|
async function flush() {
|
|
if (flushing || buffer.length === 0) return;
|
|
flushing = true;
|
|
var toFlush = buffer.splice(0, FLUSH_MAX_BATCH);
|
|
try {
|
|
var pool = require('../db/database').pool;
|
|
var q = buildBatchSql(toFlush);
|
|
await pool.query(q.sql, q.params);
|
|
} catch (err) {
|
|
console.error('[auditQueue:' + tableName + '] flush failed:', err && err.message);
|
|
// Don't re-buffer — these are audit entries, not user data.
|
|
// Losing a burst on DB hiccup is acceptable; the alternative is
|
|
// unbounded memory growth during DB outages.
|
|
} finally {
|
|
flushing = false;
|
|
// If more accumulated during the await, schedule another round
|
|
if (buffer.length >= FLUSH_MAX_BATCH) setImmediate(flush);
|
|
}
|
|
}
|
|
|
|
function push(row) {
|
|
buffer.push(row);
|
|
if (buffer.length >= FLUSH_MAX_BATCH) setImmediate(flush);
|
|
if (!timer) {
|
|
timer = setInterval(flush, FLUSH_INTERVAL_MS);
|
|
if (typeof timer.unref === 'function') timer.unref();
|
|
}
|
|
}
|
|
|
|
// Called from the SIGTERM shutdown path — drains any remaining
|
|
// entries before the process exits.
|
|
async function drain() {
|
|
if (timer) { clearInterval(timer); timer = null; }
|
|
while (buffer.length > 0) {
|
|
await flush();
|
|
if (flushing) { await new Promise(function(r) { setTimeout(r, 20); }); }
|
|
}
|
|
}
|
|
|
|
return { push: push, drain: drain, size: function() { return buffer.length; } };
|
|
}
|
|
|
|
var auditQueue = createQueue('audit_log', [
|
|
'user_id', 'action', 'category', 'details',
|
|
'ip_address', 'user_agent', 'model_used', 'tokens_used', 'duration_ms', 'status'
|
|
]);
|
|
|
|
var apiQueue = createQueue('api_log', [
|
|
'user_id', 'endpoint', 'method', 'status_code',
|
|
'request_size', 'response_size', 'model_used',
|
|
'tokens_input', 'tokens_output', 'cost_estimate',
|
|
'duration_ms', 'ip_address', 'error'
|
|
]);
|
|
|
|
var accessQueue = createQueue('access_log', [
|
|
'user_id', 'action', 'ip_address', 'user_agent', 'success'
|
|
]);
|
|
|
|
async function drainAll() {
|
|
try { await auditQueue.drain(); } catch(e) {}
|
|
try { await apiQueue.drain(); } catch(e) {}
|
|
try { await accessQueue.drain(); } catch(e) {}
|
|
}
|
|
|
|
module.exports = {
|
|
audit: auditQueue,
|
|
api: apiQueue,
|
|
access: accessQueue,
|
|
drainAll: drainAll
|
|
};
|