From 33738e9d71458e5922e56142b693c3dc5de0d788 Mon Sep 17 00:00:00 2001 From: yohlo Date: Sun, 23 Aug 2026 18:27:45 -0700 Subject: [PATCH] fix(pocketbase): self-heal admin auth instead of stranding requests beforeSend now skips its gate for the auth endpoints (no self-await deadlock), swallows a rejected authPromise and re-authenticates when the store is invalid, re-stamps Authorization after the gate so requests built mid-auth carry the fresh token, and guards startTokenRefresh against stacking intervals. A failed boot auth logs a diagnostic instead of crashing the process; recovery happens on the next request. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0189ASmkMee4F5aJ3jnKeKQv --- src/lib/pocketbase/client.ts | 52 +++++++++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 10 deletions(-) diff --git a/src/lib/pocketbase/client.ts b/src/lib/pocketbase/client.ts index fe548da..032bb5f 100644 --- a/src/lib/pocketbase/client.ts +++ b/src/lib/pocketbase/client.ts @@ -19,15 +19,36 @@ class PocketBaseAdminClient { this.pb = new PocketBase(process.env.POCKETBASE_URL); this.pb.beforeSend = async (url, options) => { - await this.authPromise; - - if (this.pb.authStore.isValid && this.isTokenExpiringSoon()) { + // The auth requests themselves must skip the gate below: gating them on + // authPromise would make them await their own completion (deadlock). + if (!url.includes("/collections/_superusers/auth-")) { try { - await this.refreshAuth(); - } catch (error) { - console.error('Failed to refresh admin token, re-authenticating:', error); - await this.authenticate(); + await this.authPromise; + } catch { + // Swallow: fall through to self-heal so a rejected authPromise can't strand every request. } + + if (!this.pb.authStore.isValid) { + // Self-heal: re-auth once PocketBase is reachable again, no restart. + this.authPromise = this.authenticate(); + await this.authPromise; + this.startTokenRefresh(); + } else if (this.isTokenExpiringSoon()) { + try { + await this.refreshAuth(); + } catch (error) { + console.error('Failed to refresh admin token, re-authenticating:', error); + this.authPromise = this.authenticate(); + await this.authPromise; + } + } + + // The SDK stamps Authorization before this hook runs, so a request + // built while auth was still in flight carries no (or a stale) token. + options.headers = { + ...options.headers, + Authorization: this.pb.authStore.token, + }; } options.cache = "no-store"; @@ -54,9 +75,18 @@ class PocketBaseAdminClient { Object.assign(this, createPushService(this.pb)); this.authPromise = this.authenticate(); - this.authPromise.then(() => { - this.startTokenRefresh(); - }); + // Fail soft at boot: an unreachable PocketBase must not crash the process; + // beforeSend self-heals on the next request. + this.authPromise + .then(() => { + this.startTokenRefresh(); + }) + .catch(() => { + console.error( + "PocketBase admin authentication failed - is PocketBase running and are " + + "POCKETBASE_URL / POCKETBASE_ADMIN_EMAIL / POCKETBASE_ADMIN_PASSWORD set?" + ); + }); } private async authenticate() { @@ -98,6 +128,8 @@ class PocketBaseAdminClient { } private startTokenRefresh() { + if (this.refreshInterval) return; + this.refreshInterval = setInterval(async () => { try { await this.refreshAuth();