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:
@@ -19,17 +19,38 @@ class PocketBaseAdminClient {
|
|||||||
this.pb = new PocketBase(process.env.POCKETBASE_URL);
|
this.pb = new PocketBase(process.env.POCKETBASE_URL);
|
||||||
|
|
||||||
this.pb.beforeSend = async (url, options) => {
|
this.pb.beforeSend = async (url, options) => {
|
||||||
|
// 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.authPromise;
|
await this.authPromise;
|
||||||
|
} catch {
|
||||||
|
// Swallow: fall through to self-heal so a rejected authPromise can't strand every request.
|
||||||
|
}
|
||||||
|
|
||||||
if (this.pb.authStore.isValid && this.isTokenExpiringSoon()) {
|
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 {
|
try {
|
||||||
await this.refreshAuth();
|
await this.refreshAuth();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to refresh admin token, re-authenticating:', error);
|
console.error('Failed to refresh admin token, re-authenticating:', error);
|
||||||
await this.authenticate();
|
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";
|
options.cache = "no-store";
|
||||||
options.headers = {
|
options.headers = {
|
||||||
...options.headers,
|
...options.headers,
|
||||||
@@ -54,8 +75,17 @@ class PocketBaseAdminClient {
|
|||||||
Object.assign(this, createPushService(this.pb));
|
Object.assign(this, createPushService(this.pb));
|
||||||
|
|
||||||
this.authPromise = this.authenticate();
|
this.authPromise = this.authenticate();
|
||||||
this.authPromise.then(() => {
|
// Fail soft at boot: an unreachable PocketBase must not crash the process;
|
||||||
|
// beforeSend self-heals on the next request.
|
||||||
|
this.authPromise
|
||||||
|
.then(() => {
|
||||||
this.startTokenRefresh();
|
this.startTokenRefresh();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
console.error(
|
||||||
|
"PocketBase admin authentication failed - is PocketBase running and are " +
|
||||||
|
"POCKETBASE_URL / POCKETBASE_ADMIN_EMAIL / POCKETBASE_ADMIN_PASSWORD set?"
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,6 +128,8 @@ class PocketBaseAdminClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private startTokenRefresh() {
|
private startTokenRefresh() {
|
||||||
|
if (this.refreshInterval) return;
|
||||||
|
|
||||||
this.refreshInterval = setInterval(async () => {
|
this.refreshInterval = setInterval(async () => {
|
||||||
try {
|
try {
|
||||||
await this.refreshAuth();
|
await this.refreshAuth();
|
||||||
|
|||||||
Reference in New Issue
Block a user