feat(03-10): fail closed on bad credentials + fix backoff index + explicit randomUUID (CR-03, WR-01, WR-08)

- outboxWorker: remove empty-credential fallback; let loadClientForUser throw on error (CR-03)
- outboxWorker: fix backoff index from nextAttemptCount to row.attemptCount so first retry waits 15s not 60s (WR-01)
- events.ts: replace bare crypto.randomUUID() with import { randomUUID } from 'node:crypto' on all three handlers (WR-08)
This commit is contained in:
Lucas Berger
2026-06-05 20:51:48 -04:00
parent c178dcee0c
commit c21b040b36
2 changed files with 11 additions and 16 deletions
+7 -13
View File
@@ -130,18 +130,10 @@ interface DispatchResult {
}
async function dispatchRow(row: OutboxRow): Promise<DispatchResult> {
// Load the authenticated client for this row's owner.
// In test environments loadClientForUser may fail (db mock mismatch) — fall back
// to createFastmailClient with empty credentials (mocked in tests to return fake client).
let client: FastmailClient
try {
client = await loadClientForUser(row.userId)
} catch {
// Unit-test path: db mock returns outbox rows for any select → decryptPassword throws.
// createFastmailClient is mocked and ignores credentials, so this still works.
// Production path: this branch is never taken (real Drizzle query succeeds).
client = await createFastmailClient('', '')
}
// CR-03: fail closed on credential errors — let loadClientForUser throw.
// The outer per-row catch in runOutboxDrain logs and leaves the row pending (correct transient behavior).
// Do NOT add an empty-credential fallback — that would silently PUT with no authentication.
const client = await loadClientForUser(row.userId)
let response: Response
@@ -364,7 +356,9 @@ export async function runOutboxDrain(): Promise<void> {
failedCreateGroups.add(row.groupId)
}
} else {
const backoffMs = (BACKOFF_SECONDS[nextAttemptCount] ?? 1800) * 1000
// WR-01: use row.attemptCount (the attempt that just failed, 0-based) as the backoff index.
// This makes the first retry wait BACKOFF_SECONDS[0]=15s, not BACKOFF_SECONDS[1]=60s.
const backoffMs = (BACKOFF_SECONDS[row.attemptCount] ?? 1800) * 1000
await db
.update(calendarOutbox)
.set({