@ragable/sdk 0.8.0 → 0.8.2

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.mjs CHANGED
@@ -666,7 +666,11 @@ function generateIdempotencyKey() {
666
666
  return `idk-${Date.now()}-${_uuidCounter}-${Math.random().toString(36).slice(2, 10)}`;
667
667
  }
668
668
  function requestCacheKey(req) {
669
- return `${req.method}:${req.url}`;
669
+ const auth = req.headers.get("authorization") ?? "";
670
+ const dbInstance = req.headers.get("x-database-instance-id") ?? "";
671
+ return `${req.method}:${req.url}
672
+ ${auth}
673
+ ${dbInstance}`;
670
674
  }
671
675
  var Transport = class {
672
676
  constructor(options = {}) {
@@ -693,13 +697,16 @@ var Transport = class {
693
697
  async execute(req) {
694
698
  if (req.method === "GET") {
695
699
  const key = requestCacheKey(req);
696
- const existing = this.inflightGets.get(key);
697
- if (existing) return existing;
698
- const promise = this._executeWithRetry(req).finally(() => {
699
- this.inflightGets.delete(key);
700
- });
701
- this.inflightGets.set(key, promise);
702
- return promise;
700
+ let base = this.inflightGets.get(key);
701
+ if (!base) {
702
+ base = this._executeWithRetry(req);
703
+ this.inflightGets.set(key, base);
704
+ const clear = () => {
705
+ if (this.inflightGets.get(key) === base) this.inflightGets.delete(key);
706
+ };
707
+ base.then(clear, clear);
708
+ }
709
+ return base.then((r) => r.clone());
703
710
  }
704
711
  return this._executeWithRetry(req);
705
712
  }
@@ -719,6 +726,8 @@ var Transport = class {
719
726
  let lastError;
720
727
  const maxAttempts = 1 + retryOpts.maxRetries;
721
728
  let did401Refresh = false;
729
+ const isIdempotent = req.method === "GET" || req.method === "HEAD";
730
+ const retryEnabled = retryOpts.maxRetries > 0 && (isIdempotent || req.retry !== void 0);
722
731
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
723
732
  try {
724
733
  const response = await this._singleFetch(finalReq, timeoutMs);
@@ -731,7 +740,7 @@ var Transport = class {
731
740
  continue;
732
741
  }
733
742
  }
734
- if (!response.ok && retryOpts.retryOn.includes(response.status) && attempt < maxAttempts - 1) {
743
+ if (retryEnabled && !response.ok && retryOpts.retryOn.includes(response.status) && attempt < maxAttempts - 1) {
735
744
  let delayMs = jitteredDelay(retryOpts.baseDelayMs, attempt, retryOpts.maxDelayMs);
736
745
  if (retryOpts.respectRetryAfter) {
737
746
  const ra = parseRetryAfter(response.headers.get("retry-after"));
@@ -747,7 +756,7 @@ var Transport = class {
747
756
  throw e;
748
757
  }
749
758
  lastError = e;
750
- if (attempt < maxAttempts - 1) {
759
+ if (retryEnabled && attempt < maxAttempts - 1) {
751
760
  const delayMs = jitteredDelay(retryOpts.baseDelayMs, attempt, retryOpts.maxDelayMs);
752
761
  this.onRetry?.(finalReq, attempt + 1, delayMs, e.message);
753
762
  await sleep(delayMs);
@@ -879,13 +888,29 @@ function extractPostgRESTErrorMessage(payload, status, statusText) {
879
888
  return msg;
880
889
  }
881
890
  async function parsePostgRESTResponse(response) {
882
- if (response.status === 204) return null;
891
+ if (response.status === 204 && response.ok) return null;
883
892
  const text = await response.text();
884
- if (!text) return null;
893
+ if (!text) {
894
+ if (!response.ok) {
895
+ throw new RagableError(
896
+ response.statusText || `HTTP ${response.status}`,
897
+ response.status,
898
+ null
899
+ );
900
+ }
901
+ return null;
902
+ }
885
903
  let payload;
886
904
  try {
887
905
  payload = JSON.parse(text);
888
906
  } catch {
907
+ if (!response.ok) {
908
+ throw new RagableError(
909
+ extractPostgRESTErrorMessage(text, response.status, response.statusText),
910
+ response.status,
911
+ null
912
+ );
913
+ }
889
914
  throw new RagableError(
890
915
  `PostgREST response parse error: ${text.slice(0, 200)}`,
891
916
  response.status,
@@ -1854,6 +1879,7 @@ var AuthBroadcastChannel = class {
1854
1879
  };
1855
1880
 
1856
1881
  // src/auth.ts
1882
+ var MAX_TIMEOUT_MS = 2 ** 31 - 1;
1857
1883
  function parseExpiresInSeconds(raw) {
1858
1884
  if (typeof raw === "number") return raw;
1859
1885
  const s = raw.trim().toLowerCase();
@@ -1900,7 +1926,14 @@ var RagableAuth = class {
1900
1926
  __publicField(this, "listeners", /* @__PURE__ */ new Map());
1901
1927
  __publicField(this, "broadcast", null);
1902
1928
  __publicField(this, "visibilityHandler", null);
1903
- __publicField(this, "initialized", false);
1929
+ /** Memoizes the one-shot restore so concurrent callers (constructor eager init,
1930
+ * every `onAuthStateChange` subscriber, `getSession`) share a single result.
1931
+ * Non-null also means "restore has started", replacing the old boolean flag. */
1932
+ __publicField(this, "initializePromise", null);
1933
+ /** Bumped on every explicit session change (sign-in/out, refresh). The async
1934
+ * restore captures this and refuses to overwrite a newer session op that
1935
+ * landed while it was reading storage (e.g. a sign-out during page load). */
1936
+ __publicField(this, "sessionEpoch", 0);
1904
1937
  this.baseUrl = DEFAULT_RAGABLE_API_BASE.replace(/\/+$/, "");
1905
1938
  this.authGroupId = config.authGroupId;
1906
1939
  this.fetchImpl = bindFetch(config.fetch);
@@ -1934,33 +1967,43 @@ var RagableAuth = class {
1934
1967
  if (this.debug) console.debug("[RagableAuth]", ...args);
1935
1968
  }
1936
1969
  // ── Lifecycle ──────────────────────────────────────────────────────────────
1937
- async initialize() {
1938
- if (this.initialized) return this.currentSession;
1939
- this.initialized = true;
1940
- if (this.persistSession) {
1941
- try {
1942
- const raw = await this.storage.getItem(this.storageKey);
1943
- if (raw) {
1944
- const session = JSON.parse(raw);
1945
- if (session.expires_at && session.expires_at > nowSeconds()) {
1946
- this.currentSession = session;
1947
- this.scheduleRefresh(session);
1948
- this.log("Restored session from storage");
1949
- } else if (session.refresh_token) {
1950
- this.log("Stored session expired, attempting refresh");
1951
- const refreshed = await this._doRefresh(session.refresh_token);
1952
- if (refreshed) {
1953
- this.currentSession = refreshed;
1954
- } else {
1955
- await this.storage.removeItem(this.storageKey);
1956
- }
1957
- }
1958
- }
1959
- } catch (e) {
1960
- this.log("Failed to restore session", e);
1970
+ /**
1971
+ * Restore a persisted session (once). Memoized: every caller awaits the same
1972
+ * promise, so the eager constructor init, `getSession`, and each
1973
+ * `onAuthStateChange` subscriber's INITIAL_SESSION replay never race or
1974
+ * double-restore. Does NOT emit `INITIAL_SESSION` globally — that event is
1975
+ * delivered per-subscriber by `onAuthStateChange` (Supabase-parity), so a
1976
+ * listener attached after restore still sees the existing session.
1977
+ */
1978
+ initialize() {
1979
+ if (this.initializePromise) return this.initializePromise;
1980
+ this.initializePromise = this._initialize();
1981
+ return this.initializePromise;
1982
+ }
1983
+ async _initialize() {
1984
+ if (!this.persistSession) return this.currentSession;
1985
+ const epoch = this.sessionEpoch;
1986
+ try {
1987
+ const raw = await this.storage.getItem(this.storageKey);
1988
+ if (!raw) return this.currentSession;
1989
+ if (this.sessionEpoch !== epoch) return this.currentSession;
1990
+ const session = JSON.parse(raw);
1991
+ const stillValid = !!session.expires_at && session.expires_at > nowSeconds();
1992
+ if (stillValid) {
1993
+ this.currentSession = session;
1994
+ this.scheduleRefresh(session);
1995
+ this.log("Restored session from storage");
1996
+ } else if (session.refresh_token) {
1997
+ this.currentSession = session;
1998
+ this.log("Stored session expired, attempting refresh");
1999
+ const refreshed = await this.singleFlightRefresh(session.refresh_token);
2000
+ if (refreshed) this.currentSession = refreshed;
2001
+ } else {
2002
+ await this.storage.removeItem(this.storageKey);
1961
2003
  }
2004
+ } catch (e) {
2005
+ this.log("Failed to restore session", e);
1962
2006
  }
1963
- this.emit("INITIAL_SESSION", this.currentSession);
1964
2007
  return this.currentSession;
1965
2008
  }
1966
2009
  // ── Auth methods ───────────────────────────────────────────────────────────
@@ -1990,6 +2033,7 @@ var RagableAuth = class {
1990
2033
  });
1991
2034
  }
1992
2035
  async signOut(_options) {
2036
+ this.sessionEpoch++;
1993
2037
  this.currentSession = null;
1994
2038
  this.clearRefreshTimer();
1995
2039
  if (this.persistSession) {
@@ -2009,12 +2053,12 @@ var RagableAuth = class {
2009
2053
  });
2010
2054
  }
2011
2055
  async getSession() {
2012
- if (!this.initialized) await this.initialize();
2056
+ await this.initialize();
2013
2057
  return { data: { session: this.currentSession }, error: null };
2014
2058
  }
2015
2059
  async getUser() {
2016
2060
  return asPostgrestResponse(async () => {
2017
- const token = this.currentSession?.access_token;
2061
+ const token = await this.getValidAccessToken();
2018
2062
  if (!token) throw new RagableError("Not authenticated", 401, null);
2019
2063
  return this.fetchAuthWithBearer("/me", "GET", token);
2020
2064
  });
@@ -2059,6 +2103,15 @@ var RagableAuth = class {
2059
2103
  _subCounter++;
2060
2104
  const id = `sub-${_subCounter}`;
2061
2105
  this.listeners.set(id, callback);
2106
+ void this.initialize().then((session) => {
2107
+ const cb = this.listeners.get(id);
2108
+ if (!cb) return;
2109
+ try {
2110
+ cb("INITIAL_SESSION", session);
2111
+ } catch (e) {
2112
+ this.log("Listener threw", e);
2113
+ }
2114
+ });
2062
2115
  const unsubscribe = () => {
2063
2116
  this.listeners.delete(id);
2064
2117
  };
@@ -2068,12 +2121,19 @@ var RagableAuth = class {
2068
2121
  getAccessToken() {
2069
2122
  return this.currentSession?.access_token ?? null;
2070
2123
  }
2071
- async getValidAccessToken() {
2072
- if (!this.initialized) await this.initialize();
2124
+ /**
2125
+ * Returns an access token guaranteed fresh for at least `refreshSkewSeconds`,
2126
+ * refreshing (single-flight) if needed. Pass `force: true` to bypass the skew
2127
+ * check and refresh now — used by the transport's 401 handler so a token the
2128
+ * server rejected (key rotation, clock skew, early revocation) self-heals on
2129
+ * retry instead of failing the call.
2130
+ */
2131
+ async getValidAccessToken(force = false) {
2132
+ await this.initialize();
2073
2133
  const session = this.currentSession;
2074
2134
  if (!session) return null;
2075
2135
  const secondsUntilExpiry = session.expires_at - nowSeconds();
2076
- if (secondsUntilExpiry <= this.refreshSkewSeconds) {
2136
+ if (force || secondsUntilExpiry <= this.refreshSkewSeconds) {
2077
2137
  const refreshed = await this.singleFlightRefresh(session.refresh_token);
2078
2138
  return refreshed?.access_token ?? null;
2079
2139
  }
@@ -2166,6 +2226,7 @@ var RagableAuth = class {
2166
2226
  };
2167
2227
  }
2168
2228
  async setSessionInternal(session, event) {
2229
+ this.sessionEpoch++;
2169
2230
  this.currentSession = session;
2170
2231
  await this.persistCurrentSession();
2171
2232
  this.broadcast?.postSessionUpdated(JSON.stringify(session));
@@ -2196,12 +2257,13 @@ var RagableAuth = class {
2196
2257
  if (!this.autoRefreshToken) return;
2197
2258
  const secondsUntilExpiry = session.expires_at - nowSeconds();
2198
2259
  const refreshIn = Math.max(0, secondsUntilExpiry - this.refreshSkewSeconds);
2199
- this.log(`Scheduling refresh in ${refreshIn}s`);
2260
+ const delayMs = Math.min(refreshIn * 1e3, MAX_TIMEOUT_MS);
2261
+ this.log(`Scheduling refresh in ${Math.round(delayMs / 1e3)}s`);
2200
2262
  this.refreshTimer = setTimeout(() => {
2201
2263
  this.singleFlightRefresh(session.refresh_token).catch((e) => {
2202
2264
  this.log("Scheduled refresh failed", e);
2203
2265
  });
2204
- }, refreshIn * 1e3);
2266
+ }, delayMs);
2205
2267
  }
2206
2268
  clearRefreshTimer() {
2207
2269
  if (this.refreshTimer !== null) {
@@ -2209,16 +2271,57 @@ var RagableAuth = class {
2209
2271
  this.refreshTimer = null;
2210
2272
  }
2211
2273
  }
2274
+ /**
2275
+ * Refresh the session, deduplicating concurrent callers onto one in-flight
2276
+ * request. Side effects (persisting the new session, or clearing it and
2277
+ * emitting SIGNED_OUT / TOKEN_REFRESH_FAILED) run exactly once inside the
2278
+ * shared promise, so two callers can't double-emit. Resolves to the new
2279
+ * session, or `null` when the refresh failed.
2280
+ */
2212
2281
  async singleFlightRefresh(refreshToken) {
2213
2282
  if (this.refreshPromise) return this.refreshPromise;
2214
- this.refreshPromise = this._doRefresh(refreshToken).finally(() => {
2283
+ this.refreshPromise = (async () => {
2284
+ const outcome = await this._doRefresh(refreshToken);
2285
+ if (outcome.ok) return outcome.session;
2286
+ if (outcome.terminal) {
2287
+ await this.handleTerminalRefreshFailure();
2288
+ } else {
2289
+ this.emit("TOKEN_REFRESH_FAILED", this.currentSession);
2290
+ }
2291
+ return null;
2292
+ })().finally(() => {
2215
2293
  this.refreshPromise = null;
2216
2294
  });
2217
2295
  return this.refreshPromise;
2218
2296
  }
2297
+ /** Clear the session locally and emit SIGNED_OUT after a definitively-rejected
2298
+ * refresh, so onAuthStateChange-driven UI redirects to login. */
2299
+ async handleTerminalRefreshFailure() {
2300
+ this.sessionEpoch++;
2301
+ this.currentSession = null;
2302
+ this.clearRefreshTimer();
2303
+ if (this.persistSession) {
2304
+ try {
2305
+ await this.storage.removeItem(this.storageKey);
2306
+ } catch (e) {
2307
+ this.log("Failed to clear session after terminal refresh failure", e);
2308
+ }
2309
+ }
2310
+ this.broadcast?.postSessionRemoved();
2311
+ this.emit("TOKEN_REFRESH_FAILED", null);
2312
+ this.emit("SIGNED_OUT", null);
2313
+ }
2219
2314
  async _doRefresh(refreshToken) {
2315
+ let raw;
2316
+ try {
2317
+ raw = await this.fetchAuth("/refresh", "POST", { refreshToken });
2318
+ } catch (e) {
2319
+ const status = e instanceof RagableError ? e.status : 0;
2320
+ const terminal = status === 400 || status === 401 || status === 403;
2321
+ this.log("Refresh request failed", { status, terminal });
2322
+ return { ok: false, terminal, error: e };
2323
+ }
2220
2324
  try {
2221
- const raw = await this.fetchAuth("/refresh", "POST", { refreshToken });
2222
2325
  const me = await this.fetchAuthWithBearer("/me", "GET", raw.accessToken);
2223
2326
  const expiresIn = parseExpiresInSeconds(raw.expiresIn);
2224
2327
  const session = {
@@ -2230,10 +2333,10 @@ var RagableAuth = class {
2230
2333
  user: me.user
2231
2334
  };
2232
2335
  await this.setSessionInternal(session, "TOKEN_REFRESHED");
2233
- return session;
2336
+ return { ok: true, session };
2234
2337
  } catch (e) {
2235
- this.log("Refresh failed", e);
2236
- return null;
2338
+ this.log("Post-refresh /me failed", e);
2339
+ return { ok: false, terminal: false, error: e };
2237
2340
  }
2238
2341
  }
2239
2342
  // ─── Visibility listener ───────────────────────────────────────────────────
@@ -3010,6 +3113,8 @@ function normalizeBrowserApiBase() {
3010
3113
  function effectiveDataAuth(options) {
3011
3114
  if (options.dataAuth) return options.dataAuth;
3012
3115
  const hasStatic = Boolean(options.dataStaticKey?.trim()) || typeof options.getDataStaticKey === "function";
3116
+ const canUserAuth = Boolean(options.authGroupId?.trim()) || typeof options.getAccessToken === "function";
3117
+ if (hasStatic && canUserAuth) return "auto";
3013
3118
  if (hasStatic) return "publicAnon";
3014
3119
  return "user";
3015
3120
  }
@@ -3024,16 +3129,26 @@ function requireAuthGroupId(options) {
3024
3129
  }
3025
3130
  return id;
3026
3131
  }
3027
- async function requireAccessToken(options, ragableAuth) {
3132
+ async function tryGetUserAccessToken(options, ragableAuth) {
3028
3133
  if (ragableAuth) {
3029
- const token = await ragableAuth.getValidAccessToken();
3030
- if (token) return token;
3134
+ const token = await ragableAuth.getValidAccessToken().catch(() => null);
3135
+ if (token?.trim()) return token.trim();
3031
3136
  }
3032
3137
  const getter = options.getAccessToken;
3033
3138
  if (getter) {
3034
3139
  const token = await getter();
3035
3140
  if (token?.trim()) return token.trim();
3036
3141
  }
3142
+ return null;
3143
+ }
3144
+ async function resolveStaticDataKey(options) {
3145
+ const fromGetter = options.getDataStaticKey ? await options.getDataStaticKey() : null;
3146
+ const key = (fromGetter?.trim() || options.dataStaticKey?.trim()) ?? "";
3147
+ return key || null;
3148
+ }
3149
+ async function requireAccessToken(options, ragableAuth) {
3150
+ const token = await tryGetUserAccessToken(options, ragableAuth);
3151
+ if (token) return token;
3037
3152
  throw new RagableError(
3038
3153
  "No access token available. Sign in first with auth.signInWithPassword() or provide getAccessToken callback.",
3039
3154
  401,
@@ -3045,8 +3160,18 @@ async function resolveDatabaseAuthBearer(options, ragableAuth) {
3045
3160
  if (mode === "user") {
3046
3161
  return requireAccessToken(options, ragableAuth);
3047
3162
  }
3048
- const fromGetter = options.getDataStaticKey ? await options.getDataStaticKey() : null;
3049
- const key = (fromGetter?.trim() || options.dataStaticKey?.trim()) ?? "";
3163
+ if (mode === "auto") {
3164
+ const userTok = await tryGetUserAccessToken(options, ragableAuth);
3165
+ if (userTok) return userTok;
3166
+ const key2 = await resolveStaticDataKey(options);
3167
+ if (key2) return key2;
3168
+ throw new RagableError(
3169
+ "No access token or data key available. Sign in with auth.signInWithPassword() or configure dataStaticKey.",
3170
+ 401,
3171
+ { code: "SDK_NO_ACCESS_TOKEN" }
3172
+ );
3173
+ }
3174
+ const key = await resolveStaticDataKey(options);
3050
3175
  if (!key) {
3051
3176
  throw new RagableError(
3052
3177
  mode === "publicAnon" ? "dataAuth publicAnon requires getDataStaticKey or dataStaticKey" : "dataAuth admin requires getDataStaticKey or dataStaticKey",
@@ -3150,6 +3275,14 @@ var RagableBrowserAuthClient = class {
3150
3275
  getSession() {
3151
3276
  return this.auth.getSession();
3152
3277
  }
3278
+ /**
3279
+ * Returns a valid (auto-refreshed) access token for the current session, or
3280
+ * `null` if signed out. The sanctioned way to obtain a token for a hand-rolled
3281
+ * `fetch` to a custom endpoint — never read tokens out of storage yourself.
3282
+ */
3283
+ getValidAccessToken() {
3284
+ return this.ragableAuth ? this.ragableAuth.getValidAccessToken() : Promise.resolve(null);
3285
+ }
3153
3286
  };
3154
3287
  function collectionRecordToRowWithMeta(record) {
3155
3288
  const { data, id, createdAt, updatedAt } = record;
@@ -3188,13 +3321,11 @@ var BrowserCollectionApi = class {
3188
3321
  const { returnMode, body } = this.normalizeFindArgs(whereOrParams);
3189
3322
  const res = await this.requestFind(body);
3190
3323
  if (res.error) return res;
3191
- if (returnMode === "flat") {
3192
- return {
3193
- data: collectionRecordsToRowWithMeta(res.data),
3194
- error: null
3195
- };
3196
- }
3197
- return res;
3324
+ const data = returnMode === "flat" ? collectionRecordsToRowWithMeta(res.data) : res.data;
3325
+ return {
3326
+ data,
3327
+ error: null
3328
+ };
3198
3329
  });
3199
3330
  /**
3200
3331
  * @deprecated Use {@link BrowserCollectionApi.findMany} — same behavior.
@@ -3383,7 +3514,16 @@ var RagableBrowserDatabaseClient = class {
3383
3514
  const headers = this.baseHeaders();
3384
3515
  headers.set("Authorization", `Bearer ${token}`);
3385
3516
  headers.set("Content-Type", "application/json");
3386
- const readOnly = effectiveDataAuth(this.options) === "publicAnon" ? true : params.readOnly !== false;
3517
+ const dataMode = effectiveDataAuth(this.options);
3518
+ let readOnly;
3519
+ if (dataMode === "publicAnon") {
3520
+ readOnly = true;
3521
+ } else if (dataMode === "auto") {
3522
+ const userTok = await tryGetUserAccessToken(this.options, this.ragableAuth);
3523
+ readOnly = userTok ? params.readOnly !== false : true;
3524
+ } else {
3525
+ readOnly = params.readOnly !== false;
3526
+ }
3387
3527
  const response = await this.fetchImpl(
3388
3528
  this.toUrl(`/auth-groups/${gid}/data/query`),
3389
3529
  {
@@ -3676,10 +3816,11 @@ async function subscribeBrowserRealtime(options, ragableAuth, fetchImpl, params)
3676
3816
  return subscription;
3677
3817
  }
3678
3818
  var BrowserStorageBucketClient = class {
3679
- constructor(options, fetchImpl, bucketId) {
3819
+ constructor(options, fetchImpl, bucketId, ragableAuth = null) {
3680
3820
  this.options = options;
3681
3821
  this.fetchImpl = fetchImpl;
3682
3822
  this.bucketId = bucketId;
3823
+ this.ragableAuth = ragableAuth;
3683
3824
  }
3684
3825
  get authGroupId() {
3685
3826
  const id = this.options.authGroupId?.trim();
@@ -3689,14 +3830,28 @@ var BrowserStorageBucketClient = class {
3689
3830
  base() {
3690
3831
  return `${normalizeBrowserApiBase()}/auth-groups/${this.authGroupId}/storage/buckets/${encodeURIComponent(this.bucketId)}`;
3691
3832
  }
3833
+ /**
3834
+ * Same credential resolution as the database client (see resolveDatabaseAuthBearer):
3835
+ * in the generated-site default (`auto`), a signed-in user's auto-refreshed JWT
3836
+ * is used so storage calls carry the user's identity; logged-out visitors fall
3837
+ * back to the anon key. Previously storage ignored the managed session entirely.
3838
+ */
3692
3839
  async bearerToken() {
3693
- const staticKey = this.options.dataStaticKey?.trim() || (this.options.getDataStaticKey ? await this.options.getDataStaticKey() : null)?.trim() || null;
3694
- if (staticKey) return staticKey;
3695
- if (this.options.getAccessToken) {
3696
- const tok = await this.options.getAccessToken();
3697
- if (tok?.trim()) return tok.trim();
3840
+ return resolveDatabaseAuthBearer(this.options, this.ragableAuth);
3841
+ }
3842
+ /**
3843
+ * The storage backend has historically returned HTTP 200 with an `{ error }`
3844
+ * body on some failures; without this guard the SDK would resolve those as
3845
+ * successful uploads/deletes. Treat any 2xx whose body carries a non-empty
3846
+ * `error` as a failure.
3847
+ */
3848
+ assertNoEmbeddedError(payload, status) {
3849
+ if (payload && typeof payload === "object" && !Array.isArray(payload)) {
3850
+ const err = payload.error;
3851
+ if (typeof err === "string" && err.trim()) {
3852
+ throw new RagableError(err, status, payload);
3853
+ }
3698
3854
  }
3699
- throw new RagableError("No auth token for storage. Provide dataStaticKey or getAccessToken.", 401, { code: "SDK_NO_ACCESS_TOKEN" });
3700
3855
  }
3701
3856
  async req(method, path, body) {
3702
3857
  const token = await this.bearerToken();
@@ -3709,7 +3864,8 @@ var BrowserStorageBucketClient = class {
3709
3864
  body: body instanceof FormData ? body : body !== void 0 ? JSON.stringify(body) : void 0
3710
3865
  });
3711
3866
  const payload = await res.json().catch(() => ({}));
3712
- if (!res.ok) throw new RagableError(payload?.error ?? res.statusText, res.status, payload);
3867
+ if (!res.ok) throw new RagableError(extractErrorMessage(payload, res.statusText), res.status, payload);
3868
+ this.assertNoEmbeddedError(payload, res.status);
3713
3869
  return payload;
3714
3870
  }
3715
3871
  list(params = {}) {
@@ -3733,7 +3889,8 @@ var BrowserStorageBucketClient = class {
3733
3889
  if (params.cacheControl) form.set("cacheControl", params.cacheControl);
3734
3890
  const res = await this.fetchImpl(`${this.base()}/upload`, { method: "POST", headers, body: form });
3735
3891
  const payload = await res.json().catch(() => ({}));
3736
- if (!res.ok) throw new RagableError(payload?.error ?? res.statusText, res.status, payload);
3892
+ if (!res.ok) throw new RagableError(extractErrorMessage(payload, res.statusText), res.status, payload);
3893
+ this.assertNoEmbeddedError(payload, res.status);
3737
3894
  return payload;
3738
3895
  }
3739
3896
  download(params) {
@@ -3774,12 +3931,18 @@ var BrowserStorageBucketClient = class {
3774
3931
  }
3775
3932
  };
3776
3933
  var RagableBrowserStorageClient = class {
3777
- constructor(options, fetchImpl) {
3934
+ constructor(options, fetchImpl, ragableAuth = null) {
3778
3935
  this.options = options;
3779
3936
  this.fetchImpl = fetchImpl;
3937
+ this.ragableAuth = ragableAuth;
3780
3938
  }
3781
3939
  from(bucketId) {
3782
- return new BrowserStorageBucketClient(this.options, this.fetchImpl, bucketId);
3940
+ return new BrowserStorageBucketClient(
3941
+ this.options,
3942
+ this.fetchImpl,
3943
+ bucketId,
3944
+ this.ragableAuth
3945
+ );
3783
3946
  }
3784
3947
  };
3785
3948
  var RagableBrowserMailClient = class {
@@ -4340,13 +4503,12 @@ var RagableBrowser = class {
4340
4503
  auth: options.auth
4341
4504
  });
4342
4505
  this.transport.setRefreshHandler(async () => {
4343
- if (effectiveDataAuth(options) !== "user") return null;
4344
- return this._ragableAuth.getValidAccessToken();
4506
+ const mode = effectiveDataAuth(options);
4507
+ if (mode !== "user" && mode !== "auto") return null;
4508
+ return this._ragableAuth.getValidAccessToken(true).catch(() => null);
4509
+ });
4510
+ this._ragableAuth.initialize().catch(() => {
4345
4511
  });
4346
- if (!options.getAccessToken && effectiveDataAuth(options) === "user") {
4347
- this._ragableAuth.initialize().catch(() => {
4348
- });
4349
- }
4350
4512
  } else {
4351
4513
  this._ragableAuth = null;
4352
4514
  }
@@ -4365,13 +4527,26 @@ var RagableBrowser = class {
4365
4527
  );
4366
4528
  this.database._setTransport(this.transport);
4367
4529
  this.db = this.database;
4368
- this.storage = new RagableBrowserStorageClient(options, bindFetch(options.fetch));
4530
+ this.storage = new RagableBrowserStorageClient(
4531
+ options,
4532
+ bindFetch(options.fetch),
4533
+ this._ragableAuth
4534
+ );
4369
4535
  this.mail = new RagableBrowserMailClient(options, this._ragableAuth);
4370
4536
  this.functions = new RagableBrowserFunctionsClient(
4371
4537
  options,
4372
4538
  this._ragableAuth
4373
4539
  ).asInvoker();
4374
4540
  }
4541
+ /**
4542
+ * Resolves once the persisted session has been restored (and refreshed if it
4543
+ * was expired). Await this before reading auth state at startup to avoid a
4544
+ * logged-out flash, e.g. `const session = await client.ready()`. Resolves
4545
+ * `null` when no auth group is configured or no session is stored.
4546
+ */
4547
+ ready() {
4548
+ return this._ragableAuth ? this._ragableAuth.initialize() : Promise.resolve(null);
4549
+ }
4375
4550
  destroy() {
4376
4551
  this._ragableAuth?.destroy();
4377
4552
  }
@@ -4381,6 +4556,91 @@ function createBrowserClient(options) {
4381
4556
  }
4382
4557
  var createRagableBrowserClient = createBrowserClient;
4383
4558
 
4559
+ // src/server.ts
4560
+ function optionsForPrivilege(config, privilege) {
4561
+ const base = {
4562
+ organizationId: config.organizationId,
4563
+ ...config.websiteId !== void 0 ? { websiteId: config.websiteId } : {},
4564
+ ...config.authGroupId !== void 0 ? { authGroupId: config.authGroupId } : {},
4565
+ ...config.databaseInstanceId !== void 0 ? { databaseInstanceId: config.databaseInstanceId } : {},
4566
+ ...config.fetch !== void 0 ? { fetch: config.fetch } : {},
4567
+ ...config.headers !== void 0 ? { headers: config.headers } : {}
4568
+ };
4569
+ if (privilege === "admin") {
4570
+ const key = config.adminKey?.trim();
4571
+ if (!key) {
4572
+ throw new RagableError(
4573
+ "Admin privilege requires `adminKey` (the auth group's data-admin key). It is not available \u2014 admin functions may be disabled for this project.",
4574
+ 403,
4575
+ { code: "SDK_ADMIN_KEY_REQUIRED" }
4576
+ );
4577
+ }
4578
+ return { ...base, dataAuth: "admin", dataStaticKey: key };
4579
+ }
4580
+ return {
4581
+ ...base,
4582
+ dataAuth: "auto",
4583
+ getAccessToken: () => config.callerToken ?? null,
4584
+ ...config.publicAnonKey !== void 0 ? { dataStaticKey: config.publicAnonKey } : {}
4585
+ };
4586
+ }
4587
+ var RagableServerClient = class _RagableServerClient {
4588
+ constructor(config, privilege) {
4589
+ this.config = config;
4590
+ __publicField(this, "database");
4591
+ __publicField(this, "db");
4592
+ __publicField(this, "storage");
4593
+ __publicField(this, "mail");
4594
+ __publicField(this, "functions");
4595
+ __publicField(this, "ai");
4596
+ __publicField(this, "agents");
4597
+ /** Which credential this client is using. */
4598
+ __publicField(this, "privilege");
4599
+ this.privilege = privilege;
4600
+ const options = optionsForPrivilege(config, privilege);
4601
+ const transport = new Transport({
4602
+ ...options.fetch !== void 0 ? { fetch: options.fetch } : {},
4603
+ ...options.headers !== void 0 ? { headers: options.headers } : {},
4604
+ ...options.transport
4605
+ });
4606
+ this.database = new RagableBrowserDatabaseClient(options, null);
4607
+ this.database._setTransport(transport);
4608
+ this.db = this.database;
4609
+ this.storage = new RagableBrowserStorageClient(options, bindFetch(options.fetch), null);
4610
+ this.mail = new RagableBrowserMailClient(options, null);
4611
+ this.functions = new RagableBrowserFunctionsClient(options, null).asInvoker();
4612
+ this.ai = new RagableBrowserAiClient({
4613
+ organizationId: options.organizationId,
4614
+ ...options.websiteId !== void 0 ? { websiteId: options.websiteId } : {},
4615
+ ...options.fetch !== void 0 ? { fetch: options.fetch } : {},
4616
+ ...options.headers !== void 0 ? { headers: options.headers } : {},
4617
+ apiBase: normalizeBrowserApiBase()
4618
+ });
4619
+ this.agents = new RagableBrowserAgentsClient(options);
4620
+ }
4621
+ /**
4622
+ * A client authenticated with the **data-admin key** — bypasses collection
4623
+ * security (owner/group/claim grants do not apply) and can write protected
4624
+ * collections. Throws `SDK_ADMIN_KEY_REQUIRED` when no admin key is configured.
4625
+ */
4626
+ asAdmin() {
4627
+ return new _RagableServerClient(this.config, "admin");
4628
+ }
4629
+ /**
4630
+ * A client scoped to the invoking end user's identity (respects collection
4631
+ * security). This is already the default; use it to drop back from `asAdmin()`.
4632
+ */
4633
+ asCaller() {
4634
+ return new _RagableServerClient(this.config, "caller");
4635
+ }
4636
+ };
4637
+ function createServerClient(config) {
4638
+ return new RagableServerClient(
4639
+ config,
4640
+ config.defaultPrivilege ?? "caller"
4641
+ );
4642
+ }
4643
+
4384
4644
  // src/index.ts
4385
4645
  function createClient(options) {
4386
4646
  return createBrowserClient(options);
@@ -4417,6 +4677,7 @@ export {
4417
4677
  RagableError,
4418
4678
  RagableNetworkError,
4419
4679
  RagableSdkError,
4680
+ RagableServerClient,
4420
4681
  RagableTimeoutError,
4421
4682
  SessionStorageAdapter,
4422
4683
  Transport,
@@ -4431,6 +4692,7 @@ export {
4431
4692
  createBrowserClient,
4432
4693
  createClient,
4433
4694
  createRagableBrowserClient,
4695
+ createServerClient,
4434
4696
  createStreamResultFromParts,
4435
4697
  detectStorage,
4436
4698
  effectiveDataAuth,