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:
@@ -130,18 +130,10 @@ interface DispatchResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function dispatchRow(row: OutboxRow): Promise<DispatchResult> {
|
async function dispatchRow(row: OutboxRow): Promise<DispatchResult> {
|
||||||
// Load the authenticated client for this row's owner.
|
// CR-03: fail closed on credential errors — let loadClientForUser throw.
|
||||||
// In test environments loadClientForUser may fail (db mock mismatch) — fall back
|
// The outer per-row catch in runOutboxDrain logs and leaves the row pending (correct transient behavior).
|
||||||
// to createFastmailClient with empty credentials (mocked in tests to return fake client).
|
// Do NOT add an empty-credential fallback — that would silently PUT with no authentication.
|
||||||
let client: FastmailClient
|
const client = await loadClientForUser(row.userId)
|
||||||
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('', '')
|
|
||||||
}
|
|
||||||
|
|
||||||
let response: Response
|
let response: Response
|
||||||
|
|
||||||
@@ -364,7 +356,9 @@ export async function runOutboxDrain(): Promise<void> {
|
|||||||
failedCreateGroups.add(row.groupId)
|
failedCreateGroups.add(row.groupId)
|
||||||
}
|
}
|
||||||
} else {
|
} 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
|
await db
|
||||||
.update(calendarOutbox)
|
.update(calendarOutbox)
|
||||||
.set({
|
.set({
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
* Mounted under /api/* in index.ts — behind oidcAuthMiddleware.
|
* Mounted under /api/* in index.ts — behind oidcAuthMiddleware.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { randomUUID } from 'node:crypto'
|
||||||
import { Hono } from 'hono'
|
import { Hono } from 'hono'
|
||||||
import { zValidator } from '@hono/zod-validator'
|
import { zValidator } from '@hono/zod-validator'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
@@ -253,7 +254,7 @@ eventsRouter.post('/create', zValidator('json', eventFieldsSchema), async (c) =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Generate a UID for the new event (Node.js 22 built-in)
|
// Generate a UID for the new event (Node.js 22 built-in)
|
||||||
const uid = `${crypto.randomUUID()}@familysync`
|
const uid = `${randomUUID()}@familysync`
|
||||||
|
|
||||||
// Enqueue the outbox row (pending) — the worker builds the VEVENT and calls Fastmail.
|
// Enqueue the outbox row (pending) — the worker builds the VEVENT and calls Fastmail.
|
||||||
await db.insert(calendarOutbox).values({
|
await db.insert(calendarOutbox).values({
|
||||||
@@ -325,8 +326,8 @@ eventsRouter.patch('/:uid/edit', zValidator('json', eventFieldsSchema), async (c
|
|||||||
|
|
||||||
if (isCalendarMove) {
|
if (isCalendarMove) {
|
||||||
// D-04: edit-as-move — insert delete+create pair in one transaction (D-04 / Pitfall 5)
|
// D-04: edit-as-move — insert delete+create pair in one transaction (D-04 / Pitfall 5)
|
||||||
const newUid = `${crypto.randomUUID()}@familysync`
|
const newUid = `${randomUUID()}@familysync`
|
||||||
const groupId = crypto.randomUUID()
|
const groupId = randomUUID()
|
||||||
|
|
||||||
await db.transaction(async (tx) => {
|
await db.transaction(async (tx) => {
|
||||||
// Delete from old calendar
|
// Delete from old calendar
|
||||||
|
|||||||
Reference in New Issue
Block a user