From 163fa68ce29ab6836faf0e5e9bf7bc861a860a59 Mon Sep 17 00:00:00 2001 From: ael-dev3 Date: Tue, 4 Aug 2026 02:27:17 +0200 Subject: [PATCH] Recover legacy notification transport backoff --- .../auth-bridge/src/admissionNotifications.ts | 60 ++++++++++++++++ .../test/admissionNotifications.test.ts | 71 +++++++++++++++++++ 2 files changed, 131 insertions(+) diff --git a/services/auth-bridge/src/admissionNotifications.ts b/services/auth-bridge/src/admissionNotifications.ts index 75ff3b6f..dfbcd754 100644 --- a/services/auth-bridge/src/admissionNotifications.ts +++ b/services/auth-bridge/src/admissionNotifications.ts @@ -52,6 +52,7 @@ const RETRY_DELAYS_MILLISECONDS = Object.freeze([ 4 * 60 * 60_000, 12 * 60 * 60_000, ]) +const LEGACY_TRANSPORT_RETRY_MINIMUM_AGE_MILLISECONDS = 30_000 type DeliveryAttemptStatus = 'pending' | 'retrying' | 'sent' | 'exhausted' @@ -1010,6 +1011,61 @@ function deliveryGeneration(delivery: AdmissionDelivery): AdmissionNotificationG }) } +/** + * Older Cloudflare deployments classified their runtime-level redirect failure + * as the broad `transport` reason. An authenticated operator replay may bring + * the exact legacy fifth-attempt generation forward after Farcaster's minimum + * retry interval. The legacy record did not store a last-attempt timestamp, so + * derive it from the persisted four-hour backoff invariant. The attempt counter + * and six-attempt ceiling never reset. Current deployments emit richer records, + * so this compatibility path cannot become a general-purpose backoff bypass. + */ +function recoverLegacyTransportBackoff( + state: PersistedNotificationState, + diagnostics: PersistedNotificationDiagnostics | null, + generation: AdmissionNotificationGeneration, + now: number, +): PersistedNotificationState { + if ( + !state.delivery + || state.delivery.kind !== 'admitted' + || generation.kind !== 'admitted' + || !generationEquals(deliveryGeneration(state.delivery), generation) + || !diagnostics + || !generationEquals(diagnostics.generation, generation) + || diagnostics.retryReasons.length !== 1 + || diagnostics.retryReasons[0] !== 'transport' + || diagnostics.lastFailureReason !== undefined + || diagnostics.lastAttemptAt !== undefined + || state.delivery.attempts.length === 0 + || state.delivery.attempts.some((attempt) => ( + attempt.status !== 'retrying' + || attempt.attempts !== MAX_DELIVERY_ATTEMPTS - 1 + || attempt.nextAttemptAt === undefined + || attempt.nextAttemptAt <= now + || attempt.nextAttemptAt - RETRY_DELAYS_MILLISECONDS[attempt.attempts - 1] + > now - LEGACY_TRANSPORT_RETRY_MINIMUM_AGE_MILLISECONDS + )) + ) return state + + const attempts = state.delivery.attempts.map((attempt) => { + return Object.freeze({ + appFid: attempt.appFid, + tokenId: attempt.tokenId, + status: 'pending' as const, + attempts: attempt.attempts, + verificationFailures: attempt.verificationFailures, + }) + }) + return Object.freeze({ + ...state, + delivery: Object.freeze({ + ...state.delivery, + attempts: Object.freeze(attempts), + }), + }) +} + async function recordDiagnostics( storage: DurableObjectState['storage'], generation: AdmissionNotificationGeneration, @@ -1904,6 +1960,10 @@ export class AdmissionNotification { ) { return new Response(null, { status: 409 }) } + const diagnostics = readPersistedDiagnostics( + await this.state.storage.get(DIAGNOSTICS_RECORD), + ) + next = recoverLegacyTransportBackoff(next, diagnostics, generation, now) if ( !next.delivery || !generationEquals(deliveryGeneration(next.delivery), generation) diff --git a/services/auth-bridge/test/admissionNotifications.test.ts b/services/auth-bridge/test/admissionNotifications.test.ts index a0644a28..81ad8ed4 100644 --- a/services/auth-bridge/test/admissionNotifications.test.ts +++ b/services/auth-bridge/test/admissionNotifications.test.ts @@ -19,6 +19,7 @@ const TOKEN = 'test-notification-token-with-enough-entropy' const INTERNAL_ORIGIN = 'https://admission-notification.internal' const STATE_KEY = 'admission-notification-v1' const PENDING_STATE_RECORD = 'admission-notification-pending-v2' +const DIAGNOSTICS_RECORD = 'admission-notification-diagnostics-v1' class FakeStorage implements DurableObjectStorage { readonly values = new Map() @@ -586,6 +587,76 @@ describe('admission notification consent and delivery lifecycle', () => { }) }) + it('lets an operator replay bring only a legacy transport backoff forward', async () => { + const fetchImpl = vi.fn(async () => successfulDelivery()) + const h = createHarness({ fetchImpl }) + await applyEvent(h.notification, enabledEvent()) + const state = h.storage.values.get(STATE_KEY) as Record + const subscriptions = state.subscriptions as Array> + h.storage.values.set(STATE_KEY, { + ...state, + delivery: { + authEpoch: 7, + queuedAt: NOW - 60_000, + expiresAt: NOW - 60_000 + 24 * 60 * 60 * 1_000, + attempts: [{ + appFid: APP_FID, + tokenId: subscriptions[0].tokenId, + status: 'retrying', + attempts: 5, + verificationFailures: 0, + nextAttemptAt: NOW - 30_000 + 4 * 60 * 60_000, + }], + }, + }) + h.storage.values.set(DIAGNOSTICS_RECORD, { + authEpoch: 7, + retryReasons: ['transport'], + }) + + await expect((await queue(h.notification)).json()).resolves.toEqual({ + status: 'already-sent', + }) + expect(fetchImpl).toHaveBeenCalledOnce() + expect(stored(h.storage)).toContain('"attempts":6') + expect(stored(h.storage)).toContain('"lastSentAuthEpoch":7') + }) + + it('does not accelerate a current transport retry classification', async () => { + const fetchImpl = vi.fn(async () => successfulDelivery()) + const h = createHarness({ fetchImpl }) + await applyEvent(h.notification, enabledEvent()) + const state = h.storage.values.get(STATE_KEY) as Record + const subscriptions = state.subscriptions as Array> + h.storage.values.set(STATE_KEY, { + ...state, + delivery: { + authEpoch: 7, + queuedAt: NOW - 60_000, + expiresAt: NOW - 60_000 + 24 * 60 * 60 * 1_000, + attempts: [{ + appFid: APP_FID, + tokenId: subscriptions[0].tokenId, + status: 'retrying', + attempts: 1, + verificationFailures: 0, + nextAttemptAt: NOW + 30_000, + }], + }, + }) + h.storage.values.set(DIAGNOSTICS_RECORD, { + generation: 'admitted', + authEpoch: 7, + retryReasons: ['transport-fetch-rejected'], + lastAttemptAt: NOW - 30_000, + lastFailureReason: 'transport-fetch-rejected', + }) + + await expect((await queue(h.notification)).json()).resolves.toEqual({ status: 'queued' }) + expect(fetchImpl).not.toHaveBeenCalled() + expect(stored(h.storage)).toContain('"attempts":1') + }) + it('rejects redirects without following or retrying them', async () => { const h = createHarness({ fetchImpl: vi.fn(async (_input, init) => {