perf,feat(events),hardening: second review pass
Fleet overview N+1: - InstanceResourceService gains a 30s TTL cache with single-flight coalescing; the overview no longer fires six live upstream requests per device on every render (plus the re-login storm), while the per-device detail route probes live via force:true Event journal becomes live: - job terminal transitions (manual executions and the interrupted sweep) now append to the journal, so /api/v1/events SSE feeds the frontend's invalidation controller that was built but never received events - journal pruning moves off the append hot path (was an unindexable full-table json_extract scan per insert) onto the retention timer Console auth hardening: - scrypt upgraded from N=16384 to N=2^16 (OWASP interactive guidance); a new password_kdf column records the derivation per row and legacy hashes rehash transparently on the next successful login without invalidating sessions (migration 18) Legacy stack: - instance URL validation blocks IPv4-compatible IPv6 after WHATWG canonicalization (::a9fe:a9fe metadata, ::7f00:1 loopback slipped past) - status polls cool down auto-login for 60s after a failed attempt so a stale saved password cannot hammer the device into an account lockout Build hygiene: - web bundle splits app (410kB) from vendor (212kB) so framework code stays cacheable across releases; stale root package-lock.json removed (pnpm is the only lockfile)
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import type Database from 'better-sqlite3';
|
||||
import { createHash, randomBytes, randomUUID, timingSafeEqual } from 'node:crypto';
|
||||
import type { EventEnvelope } from '../events/event-journal.js';
|
||||
import type {
|
||||
ExecuteOperationRequest,
|
||||
Job,
|
||||
@@ -121,6 +122,8 @@ interface Options {
|
||||
readonly idFactory?: () => string;
|
||||
readonly tokenFactory?: () => string;
|
||||
readonly nonceFactory?: () => string;
|
||||
/** Receives job terminal transitions so the SSE journal has a live producer. */
|
||||
readonly emit?: (envelope: EventEnvelope) => void;
|
||||
}
|
||||
interface PreparationRow {
|
||||
operation_id: string;
|
||||
@@ -442,6 +445,7 @@ export class SecureOperationExecution {
|
||||
}
|
||||
}
|
||||
this.finish(ids, state, code);
|
||||
this.#emitTerminal(ids.job, state, requestId);
|
||||
return this.job(ids.job);
|
||||
}
|
||||
|
||||
@@ -453,7 +457,10 @@ export class SecureOperationExecution {
|
||||
"SELECT id FROM jobs WHERE operation_id IN ('postNetworkRegisterAuto','postServiceRestart','postBasebandRestart','postSystemReboot') AND risk_level IN ('R2','R3') AND status='running'",
|
||||
)
|
||||
.all() as Array<{ id: string }>;
|
||||
for (const row of jobs) this.finishByJob(row.id, now, 'unknown-result', 'INTERRUPTED');
|
||||
for (const row of jobs) {
|
||||
this.finishByJob(row.id, now, 'unknown-result', 'INTERRUPTED');
|
||||
this.#emitTerminal(row.id, 'unknown-result', now);
|
||||
}
|
||||
return jobs.length;
|
||||
})();
|
||||
}
|
||||
@@ -479,6 +486,21 @@ export class SecureOperationExecution {
|
||||
.run(state, now, now, ids.job);
|
||||
})();
|
||||
}
|
||||
#emitTerminal(jobId: string, state: string, requestId: string): void {
|
||||
if (!this.options.emit) return;
|
||||
try {
|
||||
this.options.emit({
|
||||
kind: 'job',
|
||||
id: this.id(),
|
||||
occurredAt: this.clock().toISOString(),
|
||||
requestId,
|
||||
jobId,
|
||||
});
|
||||
} catch {
|
||||
// A journal failure must never fail the operation itself.
|
||||
}
|
||||
}
|
||||
|
||||
private finishByJob(jobId: string, now: string, state: string, code: string): void {
|
||||
this.options.db
|
||||
.prepare(
|
||||
|
||||
Reference in New Issue
Block a user