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.
This commit is contained in:
2026-08-23 18:27:45 -07:00
parent a45c2aeafa
commit 60e91d1371
+42 -10
View File
@@ -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();