@ragable/sdk 0.7.10 → 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.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 ───────────────────────────────────────────────────
@@ -2335,95 +2438,6 @@ function stripTrailingCommas(text) {
2335
2438
  return text.replace(/,(\s*[}\]])/g, "$1").replace(/,\s*$/, "");
2336
2439
  }
2337
2440
 
2338
- // src/content.ts
2339
- var MAX_IMAGE_BYTES = 5 * 1024 * 1024;
2340
- function toWireUserContent(content) {
2341
- if (typeof content === "string") return content;
2342
- const out = [];
2343
- for (const part of content) {
2344
- if (!part) continue;
2345
- if (part.type === "text") {
2346
- out.push(toWireTextPart(part));
2347
- } else if (part.type === "image") {
2348
- out.push(toWireImagePart(part));
2349
- }
2350
- }
2351
- return out;
2352
- }
2353
- function toWireTextPart(part) {
2354
- return { type: "text", text: part.text };
2355
- }
2356
- function toWireImagePart(part) {
2357
- const url = imagePartToUrl(part);
2358
- const detail = part.detail;
2359
- return {
2360
- type: "image_url",
2361
- image_url: detail ? { url, detail } : { url }
2362
- };
2363
- }
2364
- function imagePartToUrl(part) {
2365
- const img = part.image;
2366
- if (img instanceof URL) return img.href;
2367
- if (typeof img === "string") {
2368
- if (img.startsWith("http://") || img.startsWith("https://")) return img;
2369
- if (img.startsWith("data:")) return img;
2370
- const mediaType = requireMediaType(part, "raw base64 string");
2371
- assertBase64SizeOk(img);
2372
- return `data:${mediaType};base64,${img}`;
2373
- }
2374
- if (img instanceof Uint8Array || img instanceof ArrayBuffer) {
2375
- const bytes = img instanceof Uint8Array ? img : new Uint8Array(img);
2376
- assertBinarySizeOk(bytes);
2377
- const mediaType = requireMediaType(part, "binary image data");
2378
- const b64 = bytesToBase64(bytes);
2379
- return `data:${mediaType};base64,${b64}`;
2380
- }
2381
- throw new RagableError(
2382
- "ImagePart.image must be a string, URL, Uint8Array, or ArrayBuffer",
2383
- 400,
2384
- { code: "SDK_INVALID_IMAGE_PART" }
2385
- );
2386
- }
2387
- function requireMediaType(part, what) {
2388
- const m = part.mediaType?.trim();
2389
- if (!m) {
2390
- throw new RagableError(
2391
- `ImagePart.mediaType is required for ${what} (e.g. "image/png")`,
2392
- 400,
2393
- { code: "SDK_IMAGE_MEDIA_TYPE_REQUIRED" }
2394
- );
2395
- }
2396
- return m;
2397
- }
2398
- function assertBinarySizeOk(bytes) {
2399
- if (bytes.byteLength > MAX_IMAGE_BYTES) {
2400
- throw new RagableError(
2401
- `Image exceeds 5MB limit (${bytes.byteLength} bytes)`,
2402
- 400,
2403
- { code: "SDK_IMAGE_TOO_LARGE" }
2404
- );
2405
- }
2406
- }
2407
- function assertBase64SizeOk(b64) {
2408
- const approxBytes = Math.floor(b64.length * 3 / 4);
2409
- if (approxBytes > MAX_IMAGE_BYTES) {
2410
- throw new RagableError(
2411
- `Image exceeds 5MB limit (~${approxBytes} bytes decoded)`,
2412
- 400,
2413
- { code: "SDK_IMAGE_TOO_LARGE" }
2414
- );
2415
- }
2416
- }
2417
- function bytesToBase64(bytes) {
2418
- const CHUNK = 32768;
2419
- let binary = "";
2420
- for (let i = 0; i < bytes.length; i += CHUNK) {
2421
- const slice = bytes.subarray(i, i + CHUNK);
2422
- binary += String.fromCharCode(...slice);
2423
- }
2424
- return btoa(binary);
2425
- }
2426
-
2427
2441
  // src/stream-parts.ts
2428
2442
  function normalizeFinishReason(raw) {
2429
2443
  switch (raw) {
@@ -2636,9 +2650,7 @@ var ZERO_USAGE = {
2636
2650
  function buildInferenceRequestBody(params, responseFormat) {
2637
2651
  const body = {
2638
2652
  model: params.model,
2639
- messages: params.messages.map(
2640
- (m) => m.role === "user" ? { role: "user", content: toWireUserContent(m.content) } : m
2641
- )
2653
+ messages: params.messages
2642
2654
  };
2643
2655
  if (params.system !== void 0) body.system = params.system;
2644
2656
  if (typeof params.temperature === "number")
@@ -3101,6 +3113,8 @@ function normalizeBrowserApiBase() {
3101
3113
  function effectiveDataAuth(options) {
3102
3114
  if (options.dataAuth) return options.dataAuth;
3103
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";
3104
3118
  if (hasStatic) return "publicAnon";
3105
3119
  return "user";
3106
3120
  }
@@ -3115,16 +3129,26 @@ function requireAuthGroupId(options) {
3115
3129
  }
3116
3130
  return id;
3117
3131
  }
3118
- async function requireAccessToken(options, ragableAuth) {
3132
+ async function tryGetUserAccessToken(options, ragableAuth) {
3119
3133
  if (ragableAuth) {
3120
- const token = await ragableAuth.getValidAccessToken();
3121
- if (token) return token;
3134
+ const token = await ragableAuth.getValidAccessToken().catch(() => null);
3135
+ if (token?.trim()) return token.trim();
3122
3136
  }
3123
3137
  const getter = options.getAccessToken;
3124
3138
  if (getter) {
3125
3139
  const token = await getter();
3126
3140
  if (token?.trim()) return token.trim();
3127
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;
3128
3152
  throw new RagableError(
3129
3153
  "No access token available. Sign in first with auth.signInWithPassword() or provide getAccessToken callback.",
3130
3154
  401,
@@ -3136,8 +3160,18 @@ async function resolveDatabaseAuthBearer(options, ragableAuth) {
3136
3160
  if (mode === "user") {
3137
3161
  return requireAccessToken(options, ragableAuth);
3138
3162
  }
3139
- const fromGetter = options.getDataStaticKey ? await options.getDataStaticKey() : null;
3140
- 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);
3141
3175
  if (!key) {
3142
3176
  throw new RagableError(
3143
3177
  mode === "publicAnon" ? "dataAuth publicAnon requires getDataStaticKey or dataStaticKey" : "dataAuth admin requires getDataStaticKey or dataStaticKey",
@@ -3241,6 +3275,14 @@ var RagableBrowserAuthClient = class {
3241
3275
  getSession() {
3242
3276
  return this.auth.getSession();
3243
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
+ }
3244
3286
  };
3245
3287
  function collectionRecordToRowWithMeta(record) {
3246
3288
  const { data, id, createdAt, updatedAt } = record;
@@ -3279,13 +3321,11 @@ var BrowserCollectionApi = class {
3279
3321
  const { returnMode, body } = this.normalizeFindArgs(whereOrParams);
3280
3322
  const res = await this.requestFind(body);
3281
3323
  if (res.error) return res;
3282
- if (returnMode === "flat") {
3283
- return {
3284
- data: collectionRecordsToRowWithMeta(res.data),
3285
- error: null
3286
- };
3287
- }
3288
- return res;
3324
+ const data = returnMode === "flat" ? collectionRecordsToRowWithMeta(res.data) : res.data;
3325
+ return {
3326
+ data,
3327
+ error: null
3328
+ };
3289
3329
  });
3290
3330
  /**
3291
3331
  * @deprecated Use {@link BrowserCollectionApi.findMany} — same behavior.
@@ -3474,7 +3514,16 @@ var RagableBrowserDatabaseClient = class {
3474
3514
  const headers = this.baseHeaders();
3475
3515
  headers.set("Authorization", `Bearer ${token}`);
3476
3516
  headers.set("Content-Type", "application/json");
3477
- 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
+ }
3478
3527
  const response = await this.fetchImpl(
3479
3528
  this.toUrl(`/auth-groups/${gid}/data/query`),
3480
3529
  {
@@ -3767,10 +3816,11 @@ async function subscribeBrowserRealtime(options, ragableAuth, fetchImpl, params)
3767
3816
  return subscription;
3768
3817
  }
3769
3818
  var BrowserStorageBucketClient = class {
3770
- constructor(options, fetchImpl, bucketId) {
3819
+ constructor(options, fetchImpl, bucketId, ragableAuth = null) {
3771
3820
  this.options = options;
3772
3821
  this.fetchImpl = fetchImpl;
3773
3822
  this.bucketId = bucketId;
3823
+ this.ragableAuth = ragableAuth;
3774
3824
  }
3775
3825
  get authGroupId() {
3776
3826
  const id = this.options.authGroupId?.trim();
@@ -3780,14 +3830,28 @@ var BrowserStorageBucketClient = class {
3780
3830
  base() {
3781
3831
  return `${normalizeBrowserApiBase()}/auth-groups/${this.authGroupId}/storage/buckets/${encodeURIComponent(this.bucketId)}`;
3782
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
+ */
3783
3839
  async bearerToken() {
3784
- const staticKey = this.options.dataStaticKey?.trim() || (this.options.getDataStaticKey ? await this.options.getDataStaticKey() : null)?.trim() || null;
3785
- if (staticKey) return staticKey;
3786
- if (this.options.getAccessToken) {
3787
- const tok = await this.options.getAccessToken();
3788
- 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
+ }
3789
3854
  }
3790
- throw new RagableError("No auth token for storage. Provide dataStaticKey or getAccessToken.", 401, { code: "SDK_NO_ACCESS_TOKEN" });
3791
3855
  }
3792
3856
  async req(method, path, body) {
3793
3857
  const token = await this.bearerToken();
@@ -3800,7 +3864,8 @@ var BrowserStorageBucketClient = class {
3800
3864
  body: body instanceof FormData ? body : body !== void 0 ? JSON.stringify(body) : void 0
3801
3865
  });
3802
3866
  const payload = await res.json().catch(() => ({}));
3803
- 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);
3804
3869
  return payload;
3805
3870
  }
3806
3871
  list(params = {}) {
@@ -3824,7 +3889,8 @@ var BrowserStorageBucketClient = class {
3824
3889
  if (params.cacheControl) form.set("cacheControl", params.cacheControl);
3825
3890
  const res = await this.fetchImpl(`${this.base()}/upload`, { method: "POST", headers, body: form });
3826
3891
  const payload = await res.json().catch(() => ({}));
3827
- 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);
3828
3894
  return payload;
3829
3895
  }
3830
3896
  download(params) {
@@ -3865,12 +3931,18 @@ var BrowserStorageBucketClient = class {
3865
3931
  }
3866
3932
  };
3867
3933
  var RagableBrowserStorageClient = class {
3868
- constructor(options, fetchImpl) {
3934
+ constructor(options, fetchImpl, ragableAuth = null) {
3869
3935
  this.options = options;
3870
3936
  this.fetchImpl = fetchImpl;
3937
+ this.ragableAuth = ragableAuth;
3871
3938
  }
3872
3939
  from(bucketId) {
3873
- return new BrowserStorageBucketClient(this.options, this.fetchImpl, bucketId);
3940
+ return new BrowserStorageBucketClient(
3941
+ this.options,
3942
+ this.fetchImpl,
3943
+ bucketId,
3944
+ this.ragableAuth
3945
+ );
3874
3946
  }
3875
3947
  };
3876
3948
  var RagableBrowserMailClient = class {
@@ -3987,6 +4059,99 @@ var RagableBrowserMailClient = class {
3987
4059
  return message;
3988
4060
  }
3989
4061
  };
4062
+ var RagableBrowserFunctionsClient = class {
4063
+ constructor(options, auth) {
4064
+ this.options = options;
4065
+ this.auth = auth;
4066
+ __publicField(this, "fetchImpl");
4067
+ this.fetchImpl = bindFetch(options.fetch);
4068
+ }
4069
+ requireWebsiteId() {
4070
+ const websiteId = this.options.websiteId?.trim();
4071
+ if (!websiteId) {
4072
+ throw new RagableError(
4073
+ "websiteId is required for functions. Use createWebsiteRagableClient()/createAppClient() or pass createBrowserClient({ websiteId, ... }).",
4074
+ 400,
4075
+ { code: "SDK_MISSING_WEBSITE_ID" }
4076
+ );
4077
+ }
4078
+ return websiteId;
4079
+ }
4080
+ toUrl(name) {
4081
+ const orgId = this.options.organizationId;
4082
+ const websiteId = this.requireWebsiteId();
4083
+ return `${normalizeBrowserApiBase()}/public/organizations/${orgId}/websites/${websiteId}/functions/${encodeURIComponent(
4084
+ name
4085
+ )}/invoke`;
4086
+ }
4087
+ /**
4088
+ * Best-effort end-user bearer, forwarded to the function as `context.auth.token`.
4089
+ * Functions are public, so this never throws — anonymous calls send no token.
4090
+ */
4091
+ async getOptionalToken() {
4092
+ if (this.auth) {
4093
+ const token = await this.auth.getValidAccessToken().catch(() => null);
4094
+ if (token) return token;
4095
+ }
4096
+ const caller = await Promise.resolve(this.options.getAccessToken?.()).catch(
4097
+ () => null
4098
+ );
4099
+ if (typeof caller === "string" && caller.trim()) return caller.trim();
4100
+ const staticKey = this.options.dataStaticKey?.trim();
4101
+ if (staticKey) return staticKey;
4102
+ return null;
4103
+ }
4104
+ /**
4105
+ * Invoke a function by name. Prefer the typed `client.functions.<name>(input)`
4106
+ * accessors; use this when the name is dynamic.
4107
+ */
4108
+ async invoke(name, input, options) {
4109
+ const fnName = String(name ?? "").trim();
4110
+ if (!fnName) {
4111
+ throw new RagableError(
4112
+ "functions.invoke requires a function name",
4113
+ 400,
4114
+ { code: "SDK_MISSING_FUNCTION_NAME" }
4115
+ );
4116
+ }
4117
+ const headers = new Headers(options?.headers ?? this.options.headers);
4118
+ headers.set("Content-Type", "application/json");
4119
+ const token = await this.getOptionalToken();
4120
+ if (token) headers.set("Authorization", `Bearer ${token}`);
4121
+ const response = await this.fetchImpl(this.toUrl(fnName), {
4122
+ method: "POST",
4123
+ headers,
4124
+ body: JSON.stringify({ input: input ?? null }),
4125
+ ...options?.signal ? { signal: options.signal } : {}
4126
+ });
4127
+ const payload = await parseMaybeJsonBody(response);
4128
+ if (!response.ok) {
4129
+ const message = extractErrorMessage(payload, response.statusText);
4130
+ throw new RagableError(message, response.status, payload);
4131
+ }
4132
+ if (payload && typeof payload === "object" && !Array.isArray(payload) && "result" in payload) {
4133
+ return payload.result;
4134
+ }
4135
+ return payload;
4136
+ }
4137
+ /** Build the typed Proxy exposed as `client.functions`. */
4138
+ asInvoker() {
4139
+ const invoke = this.invoke.bind(this);
4140
+ return new Proxy(
4141
+ {},
4142
+ {
4143
+ get: (_target, prop) => {
4144
+ if (typeof prop !== "string") return void 0;
4145
+ if (prop === "then") return void 0;
4146
+ if (prop === "invoke") {
4147
+ return (name, input, options) => invoke(name, input, options);
4148
+ }
4149
+ return (input, options) => invoke(prop, input, options);
4150
+ }
4151
+ }
4152
+ );
4153
+ }
4154
+ };
3990
4155
  var RagableBrowserAgentsClient = class {
3991
4156
  constructor(options) {
3992
4157
  this.options = options;
@@ -4314,6 +4479,11 @@ var RagableBrowser = class {
4314
4479
  __publicField(this, "db");
4315
4480
  __publicField(this, "storage");
4316
4481
  __publicField(this, "mail");
4482
+ /**
4483
+ * Backend edge functions — call a `/functions/<name>.ts` handler with
4484
+ * `client.functions.<name>(input)`. Runs server-side. See {@link FunctionInvoker}.
4485
+ */
4486
+ __publicField(this, "functions");
4317
4487
  __publicField(this, "transport");
4318
4488
  __publicField(this, "_ragableAuth");
4319
4489
  /** Delegates to `database.from()`. Kept for back-compat — prefer `database.from()`. */
@@ -4333,13 +4503,12 @@ var RagableBrowser = class {
4333
4503
  auth: options.auth
4334
4504
  });
4335
4505
  this.transport.setRefreshHandler(async () => {
4336
- if (effectiveDataAuth(options) !== "user") return null;
4337
- 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(() => {
4338
4511
  });
4339
- if (!options.getAccessToken && effectiveDataAuth(options) === "user") {
4340
- this._ragableAuth.initialize().catch(() => {
4341
- });
4342
- }
4343
4512
  } else {
4344
4513
  this._ragableAuth = null;
4345
4514
  }
@@ -4358,8 +4527,25 @@ var RagableBrowser = class {
4358
4527
  );
4359
4528
  this.database._setTransport(this.transport);
4360
4529
  this.db = this.database;
4361
- this.storage = new RagableBrowserStorageClient(options, bindFetch(options.fetch));
4530
+ this.storage = new RagableBrowserStorageClient(
4531
+ options,
4532
+ bindFetch(options.fetch),
4533
+ this._ragableAuth
4534
+ );
4362
4535
  this.mail = new RagableBrowserMailClient(options, this._ragableAuth);
4536
+ this.functions = new RagableBrowserFunctionsClient(
4537
+ options,
4538
+ this._ragableAuth
4539
+ ).asInvoker();
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);
4363
4549
  }
4364
4550
  destroy() {
4365
4551
  this._ragableAuth?.destroy();
@@ -4400,6 +4586,7 @@ export {
4400
4586
  RagableBrowserAiClient,
4401
4587
  RagableBrowserAuthClient,
4402
4588
  RagableBrowserDatabaseClient,
4589
+ RagableBrowserFunctionsClient,
4403
4590
  RagableBrowserMailClient,
4404
4591
  RagableBrowserStorageClient,
4405
4592
  RagableError,
@@ -4413,7 +4600,6 @@ export {
4413
4600
  bindFetch,
4414
4601
  buildInferenceRequestBody,
4415
4602
  buildResponseFormat,
4416
- bytesToBase64,
4417
4603
  collectAssistantTextFromUiSegments,
4418
4604
  collectionRecordToRowWithMeta,
4419
4605
  collectionRecordsToRowWithMeta,
@@ -4429,7 +4615,6 @@ export {
4429
4615
  formatPostgrestError,
4430
4616
  formatSdkError,
4431
4617
  generateIdempotencyKey,
4432
- imagePartToUrl,
4433
4618
  isIncompleteAgentStreamError,
4434
4619
  mapAgentEvent,
4435
4620
  mapFireworksChunk,
@@ -4444,7 +4629,6 @@ export {
4444
4629
  runAgentChatStreamLenient,
4445
4630
  streamObjectFromContext,
4446
4631
  toRagableResult,
4447
- toWireUserContent,
4448
4632
  tryParsePartialJson,
4449
4633
  unwrapPostgrest,
4450
4634
  wrapStreamTextAsObject