@ragable/sdk 0.8.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +93 -13
- package/dist/index.d.ts +93 -13
- package/dist/index.js +256 -81
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +256 -81
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -760,7 +760,11 @@ function generateIdempotencyKey() {
|
|
|
760
760
|
return `idk-${Date.now()}-${_uuidCounter}-${Math.random().toString(36).slice(2, 10)}`;
|
|
761
761
|
}
|
|
762
762
|
function requestCacheKey(req) {
|
|
763
|
-
|
|
763
|
+
const auth = req.headers.get("authorization") ?? "";
|
|
764
|
+
const dbInstance = req.headers.get("x-database-instance-id") ?? "";
|
|
765
|
+
return `${req.method}:${req.url}
|
|
766
|
+
${auth}
|
|
767
|
+
${dbInstance}`;
|
|
764
768
|
}
|
|
765
769
|
var Transport = class {
|
|
766
770
|
constructor(options = {}) {
|
|
@@ -787,13 +791,16 @@ var Transport = class {
|
|
|
787
791
|
async execute(req) {
|
|
788
792
|
if (req.method === "GET") {
|
|
789
793
|
const key = requestCacheKey(req);
|
|
790
|
-
|
|
791
|
-
if (
|
|
792
|
-
|
|
793
|
-
this.inflightGets.
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
794
|
+
let base = this.inflightGets.get(key);
|
|
795
|
+
if (!base) {
|
|
796
|
+
base = this._executeWithRetry(req);
|
|
797
|
+
this.inflightGets.set(key, base);
|
|
798
|
+
const clear = () => {
|
|
799
|
+
if (this.inflightGets.get(key) === base) this.inflightGets.delete(key);
|
|
800
|
+
};
|
|
801
|
+
base.then(clear, clear);
|
|
802
|
+
}
|
|
803
|
+
return base.then((r) => r.clone());
|
|
797
804
|
}
|
|
798
805
|
return this._executeWithRetry(req);
|
|
799
806
|
}
|
|
@@ -813,6 +820,8 @@ var Transport = class {
|
|
|
813
820
|
let lastError;
|
|
814
821
|
const maxAttempts = 1 + retryOpts.maxRetries;
|
|
815
822
|
let did401Refresh = false;
|
|
823
|
+
const isIdempotent = req.method === "GET" || req.method === "HEAD";
|
|
824
|
+
const retryEnabled = retryOpts.maxRetries > 0 && (isIdempotent || req.retry !== void 0);
|
|
816
825
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
817
826
|
try {
|
|
818
827
|
const response = await this._singleFetch(finalReq, timeoutMs);
|
|
@@ -825,7 +834,7 @@ var Transport = class {
|
|
|
825
834
|
continue;
|
|
826
835
|
}
|
|
827
836
|
}
|
|
828
|
-
if (!response.ok && retryOpts.retryOn.includes(response.status) && attempt < maxAttempts - 1) {
|
|
837
|
+
if (retryEnabled && !response.ok && retryOpts.retryOn.includes(response.status) && attempt < maxAttempts - 1) {
|
|
829
838
|
let delayMs = jitteredDelay(retryOpts.baseDelayMs, attempt, retryOpts.maxDelayMs);
|
|
830
839
|
if (retryOpts.respectRetryAfter) {
|
|
831
840
|
const ra = parseRetryAfter(response.headers.get("retry-after"));
|
|
@@ -841,7 +850,7 @@ var Transport = class {
|
|
|
841
850
|
throw e;
|
|
842
851
|
}
|
|
843
852
|
lastError = e;
|
|
844
|
-
if (attempt < maxAttempts - 1) {
|
|
853
|
+
if (retryEnabled && attempt < maxAttempts - 1) {
|
|
845
854
|
const delayMs = jitteredDelay(retryOpts.baseDelayMs, attempt, retryOpts.maxDelayMs);
|
|
846
855
|
this.onRetry?.(finalReq, attempt + 1, delayMs, e.message);
|
|
847
856
|
await sleep(delayMs);
|
|
@@ -973,13 +982,29 @@ function extractPostgRESTErrorMessage(payload, status, statusText) {
|
|
|
973
982
|
return msg;
|
|
974
983
|
}
|
|
975
984
|
async function parsePostgRESTResponse(response) {
|
|
976
|
-
if (response.status === 204) return null;
|
|
985
|
+
if (response.status === 204 && response.ok) return null;
|
|
977
986
|
const text = await response.text();
|
|
978
|
-
if (!text)
|
|
987
|
+
if (!text) {
|
|
988
|
+
if (!response.ok) {
|
|
989
|
+
throw new RagableError(
|
|
990
|
+
response.statusText || `HTTP ${response.status}`,
|
|
991
|
+
response.status,
|
|
992
|
+
null
|
|
993
|
+
);
|
|
994
|
+
}
|
|
995
|
+
return null;
|
|
996
|
+
}
|
|
979
997
|
let payload;
|
|
980
998
|
try {
|
|
981
999
|
payload = JSON.parse(text);
|
|
982
1000
|
} catch {
|
|
1001
|
+
if (!response.ok) {
|
|
1002
|
+
throw new RagableError(
|
|
1003
|
+
extractPostgRESTErrorMessage(text, response.status, response.statusText),
|
|
1004
|
+
response.status,
|
|
1005
|
+
null
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
983
1008
|
throw new RagableError(
|
|
984
1009
|
`PostgREST response parse error: ${text.slice(0, 200)}`,
|
|
985
1010
|
response.status,
|
|
@@ -1948,6 +1973,7 @@ var AuthBroadcastChannel = class {
|
|
|
1948
1973
|
};
|
|
1949
1974
|
|
|
1950
1975
|
// src/auth.ts
|
|
1976
|
+
var MAX_TIMEOUT_MS = 2 ** 31 - 1;
|
|
1951
1977
|
function parseExpiresInSeconds(raw) {
|
|
1952
1978
|
if (typeof raw === "number") return raw;
|
|
1953
1979
|
const s = raw.trim().toLowerCase();
|
|
@@ -1994,7 +2020,14 @@ var RagableAuth = class {
|
|
|
1994
2020
|
__publicField(this, "listeners", /* @__PURE__ */ new Map());
|
|
1995
2021
|
__publicField(this, "broadcast", null);
|
|
1996
2022
|
__publicField(this, "visibilityHandler", null);
|
|
1997
|
-
|
|
2023
|
+
/** Memoizes the one-shot restore so concurrent callers (constructor eager init,
|
|
2024
|
+
* every `onAuthStateChange` subscriber, `getSession`) share a single result.
|
|
2025
|
+
* Non-null also means "restore has started", replacing the old boolean flag. */
|
|
2026
|
+
__publicField(this, "initializePromise", null);
|
|
2027
|
+
/** Bumped on every explicit session change (sign-in/out, refresh). The async
|
|
2028
|
+
* restore captures this and refuses to overwrite a newer session op that
|
|
2029
|
+
* landed while it was reading storage (e.g. a sign-out during page load). */
|
|
2030
|
+
__publicField(this, "sessionEpoch", 0);
|
|
1998
2031
|
this.baseUrl = DEFAULT_RAGABLE_API_BASE.replace(/\/+$/, "");
|
|
1999
2032
|
this.authGroupId = config.authGroupId;
|
|
2000
2033
|
this.fetchImpl = bindFetch(config.fetch);
|
|
@@ -2028,33 +2061,43 @@ var RagableAuth = class {
|
|
|
2028
2061
|
if (this.debug) console.debug("[RagableAuth]", ...args);
|
|
2029
2062
|
}
|
|
2030
2063
|
// ── Lifecycle ──────────────────────────────────────────────────────────────
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
this.
|
|
2064
|
+
/**
|
|
2065
|
+
* Restore a persisted session (once). Memoized: every caller awaits the same
|
|
2066
|
+
* promise, so the eager constructor init, `getSession`, and each
|
|
2067
|
+
* `onAuthStateChange` subscriber's INITIAL_SESSION replay never race or
|
|
2068
|
+
* double-restore. Does NOT emit `INITIAL_SESSION` globally — that event is
|
|
2069
|
+
* delivered per-subscriber by `onAuthStateChange` (Supabase-parity), so a
|
|
2070
|
+
* listener attached after restore still sees the existing session.
|
|
2071
|
+
*/
|
|
2072
|
+
initialize() {
|
|
2073
|
+
if (this.initializePromise) return this.initializePromise;
|
|
2074
|
+
this.initializePromise = this._initialize();
|
|
2075
|
+
return this.initializePromise;
|
|
2076
|
+
}
|
|
2077
|
+
async _initialize() {
|
|
2078
|
+
if (!this.persistSession) return this.currentSession;
|
|
2079
|
+
const epoch = this.sessionEpoch;
|
|
2080
|
+
try {
|
|
2081
|
+
const raw = await this.storage.getItem(this.storageKey);
|
|
2082
|
+
if (!raw) return this.currentSession;
|
|
2083
|
+
if (this.sessionEpoch !== epoch) return this.currentSession;
|
|
2084
|
+
const session = JSON.parse(raw);
|
|
2085
|
+
const stillValid = !!session.expires_at && session.expires_at > nowSeconds();
|
|
2086
|
+
if (stillValid) {
|
|
2087
|
+
this.currentSession = session;
|
|
2088
|
+
this.scheduleRefresh(session);
|
|
2089
|
+
this.log("Restored session from storage");
|
|
2090
|
+
} else if (session.refresh_token) {
|
|
2091
|
+
this.currentSession = session;
|
|
2092
|
+
this.log("Stored session expired, attempting refresh");
|
|
2093
|
+
const refreshed = await this.singleFlightRefresh(session.refresh_token);
|
|
2094
|
+
if (refreshed) this.currentSession = refreshed;
|
|
2095
|
+
} else {
|
|
2096
|
+
await this.storage.removeItem(this.storageKey);
|
|
2055
2097
|
}
|
|
2098
|
+
} catch (e) {
|
|
2099
|
+
this.log("Failed to restore session", e);
|
|
2056
2100
|
}
|
|
2057
|
-
this.emit("INITIAL_SESSION", this.currentSession);
|
|
2058
2101
|
return this.currentSession;
|
|
2059
2102
|
}
|
|
2060
2103
|
// ── Auth methods ───────────────────────────────────────────────────────────
|
|
@@ -2084,6 +2127,7 @@ var RagableAuth = class {
|
|
|
2084
2127
|
});
|
|
2085
2128
|
}
|
|
2086
2129
|
async signOut(_options) {
|
|
2130
|
+
this.sessionEpoch++;
|
|
2087
2131
|
this.currentSession = null;
|
|
2088
2132
|
this.clearRefreshTimer();
|
|
2089
2133
|
if (this.persistSession) {
|
|
@@ -2103,12 +2147,12 @@ var RagableAuth = class {
|
|
|
2103
2147
|
});
|
|
2104
2148
|
}
|
|
2105
2149
|
async getSession() {
|
|
2106
|
-
|
|
2150
|
+
await this.initialize();
|
|
2107
2151
|
return { data: { session: this.currentSession }, error: null };
|
|
2108
2152
|
}
|
|
2109
2153
|
async getUser() {
|
|
2110
2154
|
return asPostgrestResponse(async () => {
|
|
2111
|
-
const token = this.
|
|
2155
|
+
const token = await this.getValidAccessToken();
|
|
2112
2156
|
if (!token) throw new RagableError("Not authenticated", 401, null);
|
|
2113
2157
|
return this.fetchAuthWithBearer("/me", "GET", token);
|
|
2114
2158
|
});
|
|
@@ -2153,6 +2197,15 @@ var RagableAuth = class {
|
|
|
2153
2197
|
_subCounter++;
|
|
2154
2198
|
const id = `sub-${_subCounter}`;
|
|
2155
2199
|
this.listeners.set(id, callback);
|
|
2200
|
+
void this.initialize().then((session) => {
|
|
2201
|
+
const cb = this.listeners.get(id);
|
|
2202
|
+
if (!cb) return;
|
|
2203
|
+
try {
|
|
2204
|
+
cb("INITIAL_SESSION", session);
|
|
2205
|
+
} catch (e) {
|
|
2206
|
+
this.log("Listener threw", e);
|
|
2207
|
+
}
|
|
2208
|
+
});
|
|
2156
2209
|
const unsubscribe = () => {
|
|
2157
2210
|
this.listeners.delete(id);
|
|
2158
2211
|
};
|
|
@@ -2162,12 +2215,19 @@ var RagableAuth = class {
|
|
|
2162
2215
|
getAccessToken() {
|
|
2163
2216
|
return this.currentSession?.access_token ?? null;
|
|
2164
2217
|
}
|
|
2165
|
-
|
|
2166
|
-
|
|
2218
|
+
/**
|
|
2219
|
+
* Returns an access token guaranteed fresh for at least `refreshSkewSeconds`,
|
|
2220
|
+
* refreshing (single-flight) if needed. Pass `force: true` to bypass the skew
|
|
2221
|
+
* check and refresh now — used by the transport's 401 handler so a token the
|
|
2222
|
+
* server rejected (key rotation, clock skew, early revocation) self-heals on
|
|
2223
|
+
* retry instead of failing the call.
|
|
2224
|
+
*/
|
|
2225
|
+
async getValidAccessToken(force = false) {
|
|
2226
|
+
await this.initialize();
|
|
2167
2227
|
const session = this.currentSession;
|
|
2168
2228
|
if (!session) return null;
|
|
2169
2229
|
const secondsUntilExpiry = session.expires_at - nowSeconds();
|
|
2170
|
-
if (secondsUntilExpiry <= this.refreshSkewSeconds) {
|
|
2230
|
+
if (force || secondsUntilExpiry <= this.refreshSkewSeconds) {
|
|
2171
2231
|
const refreshed = await this.singleFlightRefresh(session.refresh_token);
|
|
2172
2232
|
return refreshed?.access_token ?? null;
|
|
2173
2233
|
}
|
|
@@ -2260,6 +2320,7 @@ var RagableAuth = class {
|
|
|
2260
2320
|
};
|
|
2261
2321
|
}
|
|
2262
2322
|
async setSessionInternal(session, event) {
|
|
2323
|
+
this.sessionEpoch++;
|
|
2263
2324
|
this.currentSession = session;
|
|
2264
2325
|
await this.persistCurrentSession();
|
|
2265
2326
|
this.broadcast?.postSessionUpdated(JSON.stringify(session));
|
|
@@ -2290,12 +2351,13 @@ var RagableAuth = class {
|
|
|
2290
2351
|
if (!this.autoRefreshToken) return;
|
|
2291
2352
|
const secondsUntilExpiry = session.expires_at - nowSeconds();
|
|
2292
2353
|
const refreshIn = Math.max(0, secondsUntilExpiry - this.refreshSkewSeconds);
|
|
2293
|
-
|
|
2354
|
+
const delayMs = Math.min(refreshIn * 1e3, MAX_TIMEOUT_MS);
|
|
2355
|
+
this.log(`Scheduling refresh in ${Math.round(delayMs / 1e3)}s`);
|
|
2294
2356
|
this.refreshTimer = setTimeout(() => {
|
|
2295
2357
|
this.singleFlightRefresh(session.refresh_token).catch((e) => {
|
|
2296
2358
|
this.log("Scheduled refresh failed", e);
|
|
2297
2359
|
});
|
|
2298
|
-
},
|
|
2360
|
+
}, delayMs);
|
|
2299
2361
|
}
|
|
2300
2362
|
clearRefreshTimer() {
|
|
2301
2363
|
if (this.refreshTimer !== null) {
|
|
@@ -2303,16 +2365,57 @@ var RagableAuth = class {
|
|
|
2303
2365
|
this.refreshTimer = null;
|
|
2304
2366
|
}
|
|
2305
2367
|
}
|
|
2368
|
+
/**
|
|
2369
|
+
* Refresh the session, deduplicating concurrent callers onto one in-flight
|
|
2370
|
+
* request. Side effects (persisting the new session, or clearing it and
|
|
2371
|
+
* emitting SIGNED_OUT / TOKEN_REFRESH_FAILED) run exactly once inside the
|
|
2372
|
+
* shared promise, so two callers can't double-emit. Resolves to the new
|
|
2373
|
+
* session, or `null` when the refresh failed.
|
|
2374
|
+
*/
|
|
2306
2375
|
async singleFlightRefresh(refreshToken) {
|
|
2307
2376
|
if (this.refreshPromise) return this.refreshPromise;
|
|
2308
|
-
this.refreshPromise =
|
|
2377
|
+
this.refreshPromise = (async () => {
|
|
2378
|
+
const outcome = await this._doRefresh(refreshToken);
|
|
2379
|
+
if (outcome.ok) return outcome.session;
|
|
2380
|
+
if (outcome.terminal) {
|
|
2381
|
+
await this.handleTerminalRefreshFailure();
|
|
2382
|
+
} else {
|
|
2383
|
+
this.emit("TOKEN_REFRESH_FAILED", this.currentSession);
|
|
2384
|
+
}
|
|
2385
|
+
return null;
|
|
2386
|
+
})().finally(() => {
|
|
2309
2387
|
this.refreshPromise = null;
|
|
2310
2388
|
});
|
|
2311
2389
|
return this.refreshPromise;
|
|
2312
2390
|
}
|
|
2391
|
+
/** Clear the session locally and emit SIGNED_OUT after a definitively-rejected
|
|
2392
|
+
* refresh, so onAuthStateChange-driven UI redirects to login. */
|
|
2393
|
+
async handleTerminalRefreshFailure() {
|
|
2394
|
+
this.sessionEpoch++;
|
|
2395
|
+
this.currentSession = null;
|
|
2396
|
+
this.clearRefreshTimer();
|
|
2397
|
+
if (this.persistSession) {
|
|
2398
|
+
try {
|
|
2399
|
+
await this.storage.removeItem(this.storageKey);
|
|
2400
|
+
} catch (e) {
|
|
2401
|
+
this.log("Failed to clear session after terminal refresh failure", e);
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
this.broadcast?.postSessionRemoved();
|
|
2405
|
+
this.emit("TOKEN_REFRESH_FAILED", null);
|
|
2406
|
+
this.emit("SIGNED_OUT", null);
|
|
2407
|
+
}
|
|
2313
2408
|
async _doRefresh(refreshToken) {
|
|
2409
|
+
let raw;
|
|
2410
|
+
try {
|
|
2411
|
+
raw = await this.fetchAuth("/refresh", "POST", { refreshToken });
|
|
2412
|
+
} catch (e) {
|
|
2413
|
+
const status = e instanceof RagableError ? e.status : 0;
|
|
2414
|
+
const terminal = status === 400 || status === 401 || status === 403;
|
|
2415
|
+
this.log("Refresh request failed", { status, terminal });
|
|
2416
|
+
return { ok: false, terminal, error: e };
|
|
2417
|
+
}
|
|
2314
2418
|
try {
|
|
2315
|
-
const raw = await this.fetchAuth("/refresh", "POST", { refreshToken });
|
|
2316
2419
|
const me = await this.fetchAuthWithBearer("/me", "GET", raw.accessToken);
|
|
2317
2420
|
const expiresIn = parseExpiresInSeconds(raw.expiresIn);
|
|
2318
2421
|
const session = {
|
|
@@ -2324,10 +2427,10 @@ var RagableAuth = class {
|
|
|
2324
2427
|
user: me.user
|
|
2325
2428
|
};
|
|
2326
2429
|
await this.setSessionInternal(session, "TOKEN_REFRESHED");
|
|
2327
|
-
return session;
|
|
2430
|
+
return { ok: true, session };
|
|
2328
2431
|
} catch (e) {
|
|
2329
|
-
this.log("
|
|
2330
|
-
return
|
|
2432
|
+
this.log("Post-refresh /me failed", e);
|
|
2433
|
+
return { ok: false, terminal: false, error: e };
|
|
2331
2434
|
}
|
|
2332
2435
|
}
|
|
2333
2436
|
// ─── Visibility listener ───────────────────────────────────────────────────
|
|
@@ -3104,6 +3207,8 @@ function normalizeBrowserApiBase() {
|
|
|
3104
3207
|
function effectiveDataAuth(options) {
|
|
3105
3208
|
if (options.dataAuth) return options.dataAuth;
|
|
3106
3209
|
const hasStatic = Boolean(options.dataStaticKey?.trim()) || typeof options.getDataStaticKey === "function";
|
|
3210
|
+
const canUserAuth = Boolean(options.authGroupId?.trim()) || typeof options.getAccessToken === "function";
|
|
3211
|
+
if (hasStatic && canUserAuth) return "auto";
|
|
3107
3212
|
if (hasStatic) return "publicAnon";
|
|
3108
3213
|
return "user";
|
|
3109
3214
|
}
|
|
@@ -3118,16 +3223,26 @@ function requireAuthGroupId(options) {
|
|
|
3118
3223
|
}
|
|
3119
3224
|
return id;
|
|
3120
3225
|
}
|
|
3121
|
-
async function
|
|
3226
|
+
async function tryGetUserAccessToken(options, ragableAuth) {
|
|
3122
3227
|
if (ragableAuth) {
|
|
3123
|
-
const token = await ragableAuth.getValidAccessToken();
|
|
3124
|
-
if (token) return token;
|
|
3228
|
+
const token = await ragableAuth.getValidAccessToken().catch(() => null);
|
|
3229
|
+
if (token?.trim()) return token.trim();
|
|
3125
3230
|
}
|
|
3126
3231
|
const getter = options.getAccessToken;
|
|
3127
3232
|
if (getter) {
|
|
3128
3233
|
const token = await getter();
|
|
3129
3234
|
if (token?.trim()) return token.trim();
|
|
3130
3235
|
}
|
|
3236
|
+
return null;
|
|
3237
|
+
}
|
|
3238
|
+
async function resolveStaticDataKey(options) {
|
|
3239
|
+
const fromGetter = options.getDataStaticKey ? await options.getDataStaticKey() : null;
|
|
3240
|
+
const key = (fromGetter?.trim() || options.dataStaticKey?.trim()) ?? "";
|
|
3241
|
+
return key || null;
|
|
3242
|
+
}
|
|
3243
|
+
async function requireAccessToken(options, ragableAuth) {
|
|
3244
|
+
const token = await tryGetUserAccessToken(options, ragableAuth);
|
|
3245
|
+
if (token) return token;
|
|
3131
3246
|
throw new RagableError(
|
|
3132
3247
|
"No access token available. Sign in first with auth.signInWithPassword() or provide getAccessToken callback.",
|
|
3133
3248
|
401,
|
|
@@ -3139,8 +3254,18 @@ async function resolveDatabaseAuthBearer(options, ragableAuth) {
|
|
|
3139
3254
|
if (mode === "user") {
|
|
3140
3255
|
return requireAccessToken(options, ragableAuth);
|
|
3141
3256
|
}
|
|
3142
|
-
|
|
3143
|
-
|
|
3257
|
+
if (mode === "auto") {
|
|
3258
|
+
const userTok = await tryGetUserAccessToken(options, ragableAuth);
|
|
3259
|
+
if (userTok) return userTok;
|
|
3260
|
+
const key2 = await resolveStaticDataKey(options);
|
|
3261
|
+
if (key2) return key2;
|
|
3262
|
+
throw new RagableError(
|
|
3263
|
+
"No access token or data key available. Sign in with auth.signInWithPassword() or configure dataStaticKey.",
|
|
3264
|
+
401,
|
|
3265
|
+
{ code: "SDK_NO_ACCESS_TOKEN" }
|
|
3266
|
+
);
|
|
3267
|
+
}
|
|
3268
|
+
const key = await resolveStaticDataKey(options);
|
|
3144
3269
|
if (!key) {
|
|
3145
3270
|
throw new RagableError(
|
|
3146
3271
|
mode === "publicAnon" ? "dataAuth publicAnon requires getDataStaticKey or dataStaticKey" : "dataAuth admin requires getDataStaticKey or dataStaticKey",
|
|
@@ -3244,6 +3369,14 @@ var RagableBrowserAuthClient = class {
|
|
|
3244
3369
|
getSession() {
|
|
3245
3370
|
return this.auth.getSession();
|
|
3246
3371
|
}
|
|
3372
|
+
/**
|
|
3373
|
+
* Returns a valid (auto-refreshed) access token for the current session, or
|
|
3374
|
+
* `null` if signed out. The sanctioned way to obtain a token for a hand-rolled
|
|
3375
|
+
* `fetch` to a custom endpoint — never read tokens out of storage yourself.
|
|
3376
|
+
*/
|
|
3377
|
+
getValidAccessToken() {
|
|
3378
|
+
return this.ragableAuth ? this.ragableAuth.getValidAccessToken() : Promise.resolve(null);
|
|
3379
|
+
}
|
|
3247
3380
|
};
|
|
3248
3381
|
function collectionRecordToRowWithMeta(record) {
|
|
3249
3382
|
const { data, id, createdAt, updatedAt } = record;
|
|
@@ -3282,13 +3415,11 @@ var BrowserCollectionApi = class {
|
|
|
3282
3415
|
const { returnMode, body } = this.normalizeFindArgs(whereOrParams);
|
|
3283
3416
|
const res = await this.requestFind(body);
|
|
3284
3417
|
if (res.error) return res;
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
}
|
|
3291
|
-
return res;
|
|
3418
|
+
const data = returnMode === "flat" ? collectionRecordsToRowWithMeta(res.data) : res.data;
|
|
3419
|
+
return {
|
|
3420
|
+
data,
|
|
3421
|
+
error: null
|
|
3422
|
+
};
|
|
3292
3423
|
});
|
|
3293
3424
|
/**
|
|
3294
3425
|
* @deprecated Use {@link BrowserCollectionApi.findMany} — same behavior.
|
|
@@ -3477,7 +3608,16 @@ var RagableBrowserDatabaseClient = class {
|
|
|
3477
3608
|
const headers = this.baseHeaders();
|
|
3478
3609
|
headers.set("Authorization", `Bearer ${token}`);
|
|
3479
3610
|
headers.set("Content-Type", "application/json");
|
|
3480
|
-
const
|
|
3611
|
+
const dataMode = effectiveDataAuth(this.options);
|
|
3612
|
+
let readOnly;
|
|
3613
|
+
if (dataMode === "publicAnon") {
|
|
3614
|
+
readOnly = true;
|
|
3615
|
+
} else if (dataMode === "auto") {
|
|
3616
|
+
const userTok = await tryGetUserAccessToken(this.options, this.ragableAuth);
|
|
3617
|
+
readOnly = userTok ? params.readOnly !== false : true;
|
|
3618
|
+
} else {
|
|
3619
|
+
readOnly = params.readOnly !== false;
|
|
3620
|
+
}
|
|
3481
3621
|
const response = await this.fetchImpl(
|
|
3482
3622
|
this.toUrl(`/auth-groups/${gid}/data/query`),
|
|
3483
3623
|
{
|
|
@@ -3770,10 +3910,11 @@ async function subscribeBrowserRealtime(options, ragableAuth, fetchImpl, params)
|
|
|
3770
3910
|
return subscription;
|
|
3771
3911
|
}
|
|
3772
3912
|
var BrowserStorageBucketClient = class {
|
|
3773
|
-
constructor(options, fetchImpl, bucketId) {
|
|
3913
|
+
constructor(options, fetchImpl, bucketId, ragableAuth = null) {
|
|
3774
3914
|
this.options = options;
|
|
3775
3915
|
this.fetchImpl = fetchImpl;
|
|
3776
3916
|
this.bucketId = bucketId;
|
|
3917
|
+
this.ragableAuth = ragableAuth;
|
|
3777
3918
|
}
|
|
3778
3919
|
get authGroupId() {
|
|
3779
3920
|
const id = this.options.authGroupId?.trim();
|
|
@@ -3783,14 +3924,28 @@ var BrowserStorageBucketClient = class {
|
|
|
3783
3924
|
base() {
|
|
3784
3925
|
return `${normalizeBrowserApiBase()}/auth-groups/${this.authGroupId}/storage/buckets/${encodeURIComponent(this.bucketId)}`;
|
|
3785
3926
|
}
|
|
3927
|
+
/**
|
|
3928
|
+
* Same credential resolution as the database client (see resolveDatabaseAuthBearer):
|
|
3929
|
+
* in the generated-site default (`auto`), a signed-in user's auto-refreshed JWT
|
|
3930
|
+
* is used so storage calls carry the user's identity; logged-out visitors fall
|
|
3931
|
+
* back to the anon key. Previously storage ignored the managed session entirely.
|
|
3932
|
+
*/
|
|
3786
3933
|
async bearerToken() {
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3934
|
+
return resolveDatabaseAuthBearer(this.options, this.ragableAuth);
|
|
3935
|
+
}
|
|
3936
|
+
/**
|
|
3937
|
+
* The storage backend has historically returned HTTP 200 with an `{ error }`
|
|
3938
|
+
* body on some failures; without this guard the SDK would resolve those as
|
|
3939
|
+
* successful uploads/deletes. Treat any 2xx whose body carries a non-empty
|
|
3940
|
+
* `error` as a failure.
|
|
3941
|
+
*/
|
|
3942
|
+
assertNoEmbeddedError(payload, status) {
|
|
3943
|
+
if (payload && typeof payload === "object" && !Array.isArray(payload)) {
|
|
3944
|
+
const err = payload.error;
|
|
3945
|
+
if (typeof err === "string" && err.trim()) {
|
|
3946
|
+
throw new RagableError(err, status, payload);
|
|
3947
|
+
}
|
|
3792
3948
|
}
|
|
3793
|
-
throw new RagableError("No auth token for storage. Provide dataStaticKey or getAccessToken.", 401, { code: "SDK_NO_ACCESS_TOKEN" });
|
|
3794
3949
|
}
|
|
3795
3950
|
async req(method, path, body) {
|
|
3796
3951
|
const token = await this.bearerToken();
|
|
@@ -3803,7 +3958,8 @@ var BrowserStorageBucketClient = class {
|
|
|
3803
3958
|
body: body instanceof FormData ? body : body !== void 0 ? JSON.stringify(body) : void 0
|
|
3804
3959
|
});
|
|
3805
3960
|
const payload = await res.json().catch(() => ({}));
|
|
3806
|
-
if (!res.ok) throw new RagableError(payload
|
|
3961
|
+
if (!res.ok) throw new RagableError(extractErrorMessage(payload, res.statusText), res.status, payload);
|
|
3962
|
+
this.assertNoEmbeddedError(payload, res.status);
|
|
3807
3963
|
return payload;
|
|
3808
3964
|
}
|
|
3809
3965
|
list(params = {}) {
|
|
@@ -3827,7 +3983,8 @@ var BrowserStorageBucketClient = class {
|
|
|
3827
3983
|
if (params.cacheControl) form.set("cacheControl", params.cacheControl);
|
|
3828
3984
|
const res = await this.fetchImpl(`${this.base()}/upload`, { method: "POST", headers, body: form });
|
|
3829
3985
|
const payload = await res.json().catch(() => ({}));
|
|
3830
|
-
if (!res.ok) throw new RagableError(payload
|
|
3986
|
+
if (!res.ok) throw new RagableError(extractErrorMessage(payload, res.statusText), res.status, payload);
|
|
3987
|
+
this.assertNoEmbeddedError(payload, res.status);
|
|
3831
3988
|
return payload;
|
|
3832
3989
|
}
|
|
3833
3990
|
download(params) {
|
|
@@ -3868,12 +4025,18 @@ var BrowserStorageBucketClient = class {
|
|
|
3868
4025
|
}
|
|
3869
4026
|
};
|
|
3870
4027
|
var RagableBrowserStorageClient = class {
|
|
3871
|
-
constructor(options, fetchImpl) {
|
|
4028
|
+
constructor(options, fetchImpl, ragableAuth = null) {
|
|
3872
4029
|
this.options = options;
|
|
3873
4030
|
this.fetchImpl = fetchImpl;
|
|
4031
|
+
this.ragableAuth = ragableAuth;
|
|
3874
4032
|
}
|
|
3875
4033
|
from(bucketId) {
|
|
3876
|
-
return new BrowserStorageBucketClient(
|
|
4034
|
+
return new BrowserStorageBucketClient(
|
|
4035
|
+
this.options,
|
|
4036
|
+
this.fetchImpl,
|
|
4037
|
+
bucketId,
|
|
4038
|
+
this.ragableAuth
|
|
4039
|
+
);
|
|
3877
4040
|
}
|
|
3878
4041
|
};
|
|
3879
4042
|
var RagableBrowserMailClient = class {
|
|
@@ -4434,13 +4597,12 @@ var RagableBrowser = class {
|
|
|
4434
4597
|
auth: options.auth
|
|
4435
4598
|
});
|
|
4436
4599
|
this.transport.setRefreshHandler(async () => {
|
|
4437
|
-
|
|
4438
|
-
|
|
4600
|
+
const mode = effectiveDataAuth(options);
|
|
4601
|
+
if (mode !== "user" && mode !== "auto") return null;
|
|
4602
|
+
return this._ragableAuth.getValidAccessToken(true).catch(() => null);
|
|
4603
|
+
});
|
|
4604
|
+
this._ragableAuth.initialize().catch(() => {
|
|
4439
4605
|
});
|
|
4440
|
-
if (!options.getAccessToken && effectiveDataAuth(options) === "user") {
|
|
4441
|
-
this._ragableAuth.initialize().catch(() => {
|
|
4442
|
-
});
|
|
4443
|
-
}
|
|
4444
4606
|
} else {
|
|
4445
4607
|
this._ragableAuth = null;
|
|
4446
4608
|
}
|
|
@@ -4459,13 +4621,26 @@ var RagableBrowser = class {
|
|
|
4459
4621
|
);
|
|
4460
4622
|
this.database._setTransport(this.transport);
|
|
4461
4623
|
this.db = this.database;
|
|
4462
|
-
this.storage = new RagableBrowserStorageClient(
|
|
4624
|
+
this.storage = new RagableBrowserStorageClient(
|
|
4625
|
+
options,
|
|
4626
|
+
bindFetch(options.fetch),
|
|
4627
|
+
this._ragableAuth
|
|
4628
|
+
);
|
|
4463
4629
|
this.mail = new RagableBrowserMailClient(options, this._ragableAuth);
|
|
4464
4630
|
this.functions = new RagableBrowserFunctionsClient(
|
|
4465
4631
|
options,
|
|
4466
4632
|
this._ragableAuth
|
|
4467
4633
|
).asInvoker();
|
|
4468
4634
|
}
|
|
4635
|
+
/**
|
|
4636
|
+
* Resolves once the persisted session has been restored (and refreshed if it
|
|
4637
|
+
* was expired). Await this before reading auth state at startup to avoid a
|
|
4638
|
+
* logged-out flash, e.g. `const session = await client.ready()`. Resolves
|
|
4639
|
+
* `null` when no auth group is configured or no session is stored.
|
|
4640
|
+
*/
|
|
4641
|
+
ready() {
|
|
4642
|
+
return this._ragableAuth ? this._ragableAuth.initialize() : Promise.resolve(null);
|
|
4643
|
+
}
|
|
4469
4644
|
destroy() {
|
|
4470
4645
|
this._ragableAuth?.destroy();
|
|
4471
4646
|
}
|