@fluxbase/sdk 0.0.1-rc.43 → 0.0.1-rc.44

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.cjs CHANGED
@@ -3,6 +3,10 @@
3
3
  // src/fetch.ts
4
4
  var FluxbaseFetch = class {
5
5
  constructor(baseUrl, options = {}) {
6
+ this.refreshTokenCallback = null;
7
+ this.isRefreshing = false;
8
+ this.refreshPromise = null;
9
+ this.anonKey = null;
6
10
  this.baseUrl = baseUrl.replace(/\/$/, "");
7
11
  this.defaultHeaders = {
8
12
  "Content-Type": "application/json",
@@ -11,12 +15,30 @@ var FluxbaseFetch = class {
11
15
  this.timeout = options.timeout ?? 3e4;
12
16
  this.debug = options.debug ?? false;
13
17
  }
18
+ /**
19
+ * Register a callback to refresh the token when a 401 error occurs
20
+ * The callback should return true if refresh was successful, false otherwise
21
+ */
22
+ setRefreshTokenCallback(callback) {
23
+ this.refreshTokenCallback = callback;
24
+ }
25
+ /**
26
+ * Set the anon key for fallback authentication
27
+ * When setAuthToken(null) is called, the Authorization header will be
28
+ * restored to use this anon key instead of being deleted
29
+ */
30
+ setAnonKey(key) {
31
+ this.anonKey = key;
32
+ }
14
33
  /**
15
34
  * Update the authorization header
35
+ * When token is null, restores to anon key if available
16
36
  */
17
37
  setAuthToken(token) {
18
38
  if (token) {
19
39
  this.defaultHeaders["Authorization"] = `Bearer ${token}`;
40
+ } else if (this.anonKey) {
41
+ this.defaultHeaders["Authorization"] = `Bearer ${this.anonKey}`;
20
42
  } else {
21
43
  delete this.defaultHeaders["Authorization"];
22
44
  }
@@ -25,6 +47,12 @@ var FluxbaseFetch = class {
25
47
  * Make an HTTP request
26
48
  */
27
49
  async request(path, options) {
50
+ return this.requestInternal(path, options, false);
51
+ }
52
+ /**
53
+ * Internal request implementation with retry capability
54
+ */
55
+ async requestInternal(path, options, isRetry) {
28
56
  const url = `${this.baseUrl}${path}`;
29
57
  const headers = { ...this.defaultHeaders, ...options.headers };
30
58
  const controller = new AbortController();
@@ -33,10 +61,14 @@ var FluxbaseFetch = class {
33
61
  console.log(`[Fluxbase SDK] ${options.method} ${url}`, options.body);
34
62
  }
35
63
  try {
64
+ const isFormData = options.body && (options.body.constructor?.name === "FormData" || options.body instanceof FormData);
65
+ const requestHeaders = isFormData ? Object.fromEntries(
66
+ Object.entries(headers).filter(([key]) => key.toLowerCase() !== "content-type")
67
+ ) : headers;
36
68
  const response = await fetch(url, {
37
69
  method: options.method,
38
- headers,
39
- body: options.body ? JSON.stringify(options.body) : void 0,
70
+ headers: requestHeaders,
71
+ body: isFormData ? options.body : options.body ? JSON.stringify(options.body) : void 0,
40
72
  signal: controller.signal
41
73
  });
42
74
  clearTimeout(timeoutId);
@@ -50,6 +82,12 @@ var FluxbaseFetch = class {
50
82
  if (this.debug) {
51
83
  console.log(`[Fluxbase SDK] Response:`, response.status, data);
52
84
  }
85
+ if (response.status === 401 && !isRetry && !options.skipAutoRefresh && this.refreshTokenCallback) {
86
+ const refreshSuccess = await this.handleTokenRefresh();
87
+ if (refreshSuccess) {
88
+ return this.requestInternal(path, options, true);
89
+ }
90
+ }
53
91
  if (!response.ok) {
54
92
  const error = new Error(
55
93
  typeof data === "object" && data && "error" in data ? String(data.error) : response.statusText
@@ -72,12 +110,122 @@ var FluxbaseFetch = class {
72
110
  throw new Error("Unknown error occurred");
73
111
  }
74
112
  }
113
+ /**
114
+ * Handle token refresh with deduplication
115
+ * Multiple concurrent requests that fail with 401 will share the same refresh operation
116
+ */
117
+ async handleTokenRefresh() {
118
+ if (this.isRefreshing && this.refreshPromise) {
119
+ return this.refreshPromise;
120
+ }
121
+ this.isRefreshing = true;
122
+ this.refreshPromise = this.executeRefresh();
123
+ try {
124
+ return await this.refreshPromise;
125
+ } finally {
126
+ this.isRefreshing = false;
127
+ this.refreshPromise = null;
128
+ }
129
+ }
130
+ /**
131
+ * Execute the actual token refresh
132
+ */
133
+ async executeRefresh() {
134
+ if (!this.refreshTokenCallback) {
135
+ return false;
136
+ }
137
+ try {
138
+ return await this.refreshTokenCallback();
139
+ } catch (error) {
140
+ if (this.debug) {
141
+ console.error("[Fluxbase SDK] Token refresh failed:", error);
142
+ }
143
+ return false;
144
+ }
145
+ }
75
146
  /**
76
147
  * GET request
77
148
  */
78
149
  async get(path, options = {}) {
79
150
  return this.request(path, { ...options, method: "GET" });
80
151
  }
152
+ /**
153
+ * GET request that returns response with headers (for count queries)
154
+ */
155
+ async getWithHeaders(path, options = {}) {
156
+ return this.requestWithHeaders(path, { ...options, method: "GET" });
157
+ }
158
+ /**
159
+ * Make an HTTP request and return response with headers
160
+ */
161
+ async requestWithHeaders(path, options) {
162
+ return this.requestWithHeadersInternal(path, options, false);
163
+ }
164
+ /**
165
+ * Internal request implementation that returns response with headers
166
+ */
167
+ async requestWithHeadersInternal(path, options, isRetry) {
168
+ const url = `${this.baseUrl}${path}`;
169
+ const headers = { ...this.defaultHeaders, ...options.headers };
170
+ const controller = new AbortController();
171
+ const timeoutId = setTimeout(() => controller.abort(), options.timeout ?? this.timeout);
172
+ if (this.debug) {
173
+ console.log(`[Fluxbase SDK] ${options.method} ${url}`, options.body);
174
+ }
175
+ try {
176
+ const isFormData = options.body && (options.body.constructor?.name === "FormData" || options.body instanceof FormData);
177
+ const requestHeaders = isFormData ? Object.fromEntries(
178
+ Object.entries(headers).filter(([key]) => key.toLowerCase() !== "content-type")
179
+ ) : headers;
180
+ const response = await fetch(url, {
181
+ method: options.method,
182
+ headers: requestHeaders,
183
+ body: isFormData ? options.body : options.body ? JSON.stringify(options.body) : void 0,
184
+ signal: controller.signal
185
+ });
186
+ clearTimeout(timeoutId);
187
+ const contentType = response.headers.get("content-type");
188
+ let data;
189
+ if (contentType?.includes("application/json")) {
190
+ data = await response.json();
191
+ } else {
192
+ data = await response.text();
193
+ }
194
+ if (this.debug) {
195
+ console.log(`[Fluxbase SDK] Response:`, response.status, data);
196
+ }
197
+ if (response.status === 401 && !isRetry && !options.skipAutoRefresh && this.refreshTokenCallback) {
198
+ const refreshSuccess = await this.handleTokenRefresh();
199
+ if (refreshSuccess) {
200
+ return this.requestWithHeadersInternal(path, options, true);
201
+ }
202
+ }
203
+ if (!response.ok) {
204
+ const error = new Error(
205
+ typeof data === "object" && data && "error" in data ? String(data.error) : response.statusText
206
+ );
207
+ error.status = response.status;
208
+ error.details = data;
209
+ throw error;
210
+ }
211
+ return {
212
+ data,
213
+ headers: response.headers,
214
+ status: response.status
215
+ };
216
+ } catch (err) {
217
+ clearTimeout(timeoutId);
218
+ if (err instanceof Error) {
219
+ if (err.name === "AbortError") {
220
+ const timeoutError = new Error("Request timeout");
221
+ timeoutError.status = 408;
222
+ throw timeoutError;
223
+ }
224
+ throw err;
225
+ }
226
+ throw new Error("Unknown error occurred");
227
+ }
228
+ }
81
229
  /**
82
230
  * POST request
83
231
  */
@@ -141,6 +289,9 @@ async function wrapAsyncVoid(operation) {
141
289
 
142
290
  // src/auth.ts
143
291
  var AUTH_STORAGE_KEY = "fluxbase.auth.session";
292
+ var AUTO_REFRESH_TICK_THRESHOLD = 10;
293
+ var AUTO_REFRESH_TICK_MINIMUM = 1e3;
294
+ var MAX_REFRESH_RETRIES = 3;
144
295
  var MemoryStorage = class {
145
296
  constructor() {
146
297
  this.store = /* @__PURE__ */ new Map();
@@ -186,6 +337,10 @@ var FluxbaseAuth = class {
186
337
  this.fetch = fetch2;
187
338
  this.persist = persist;
188
339
  this.autoRefresh = autoRefresh;
340
+ this.fetch.setRefreshTokenCallback(async () => {
341
+ const result = await this.refreshSession();
342
+ return !result.error;
343
+ });
189
344
  if (this.persist) {
190
345
  if (isLocalStorageAvailable()) {
191
346
  this.storage = localStorage;
@@ -253,6 +408,24 @@ var FluxbaseAuth = class {
253
408
  };
254
409
  return { data: { subscription } };
255
410
  }
411
+ /**
412
+ * Start the automatic token refresh timer
413
+ * This is called automatically when autoRefresh is enabled and a session exists
414
+ * Only works in browser environments
415
+ */
416
+ startAutoRefresh() {
417
+ this.scheduleTokenRefresh();
418
+ }
419
+ /**
420
+ * Stop the automatic token refresh timer
421
+ * Call this when you want to disable auto-refresh without signing out
422
+ */
423
+ stopAutoRefresh() {
424
+ if (this.refreshTimer) {
425
+ clearTimeout(this.refreshTimer);
426
+ this.refreshTimer = null;
427
+ }
428
+ }
256
429
  /**
257
430
  * Sign in with email and password (Supabase-compatible)
258
431
  * Returns { user, session } if successful, or SignInWith2FAResponse if 2FA is required
@@ -334,10 +507,13 @@ var FluxbaseAuth = class {
334
507
  "/api/v1/auth/refresh",
335
508
  {
336
509
  refresh_token: this.session.refresh_token
337
- }
510
+ },
511
+ { skipAutoRefresh: true }
512
+ // Prevent infinite loop on 401
338
513
  );
339
514
  const session = {
340
515
  ...response,
516
+ user: response.user ?? this.session.user,
341
517
  expires_at: Date.now() + response.expires_in * 1e3
342
518
  };
343
519
  this.setSessionInternal(session, "TOKEN_REFRESHED");
@@ -386,7 +562,10 @@ var FluxbaseAuth = class {
386
562
  if (attributes.nonce) {
387
563
  requestBody.nonce = attributes.nonce;
388
564
  }
389
- const user = await this.fetch.patch("/api/v1/auth/user", requestBody);
565
+ const user = await this.fetch.patch(
566
+ "/api/v1/auth/user",
567
+ requestBody
568
+ );
390
569
  if (this.session) {
391
570
  this.session.user = user;
392
571
  this.saveSession();
@@ -859,24 +1038,57 @@ var FluxbaseAuth = class {
859
1038
  }
860
1039
  /**
861
1040
  * Internal: Schedule automatic token refresh
1041
+ * Only runs in browser environments when autoRefresh is enabled
862
1042
  */
863
1043
  scheduleTokenRefresh() {
864
- if (!this.autoRefresh || !this.session?.expires_at) {
1044
+ if (!this.autoRefresh || typeof window === "undefined") {
1045
+ return;
1046
+ }
1047
+ if (!this.session?.expires_at) {
865
1048
  return;
866
1049
  }
867
1050
  if (this.refreshTimer) {
868
1051
  clearTimeout(this.refreshTimer);
1052
+ this.refreshTimer = null;
869
1053
  }
870
- const refreshAt = this.session.expires_at - 60 * 1e3;
871
- const delay = refreshAt - Date.now();
872
- if (delay > 0) {
873
- this.refreshTimer = setTimeout(async () => {
874
- const result = await this.refreshSession();
875
- if (result.error) {
876
- console.error("Failed to refresh token:", result.error);
877
- this.clearSession();
878
- }
879
- }, delay);
1054
+ const expiresAt = this.session.expires_at;
1055
+ const now = Date.now();
1056
+ const timeUntilExpiry = expiresAt - now;
1057
+ const refreshIn = Math.max(
1058
+ timeUntilExpiry - AUTO_REFRESH_TICK_THRESHOLD * 1e3,
1059
+ AUTO_REFRESH_TICK_MINIMUM
1060
+ );
1061
+ this.refreshTimer = setTimeout(() => {
1062
+ this.attemptRefresh();
1063
+ }, refreshIn);
1064
+ }
1065
+ /**
1066
+ * Internal: Attempt to refresh the token with retry logic
1067
+ * Uses exponential backoff: 1s, 2s, 4s delays between retries
1068
+ */
1069
+ async attemptRefresh(retries = MAX_REFRESH_RETRIES) {
1070
+ try {
1071
+ const result = await this.refreshSession();
1072
+ if (result.error) {
1073
+ throw result.error;
1074
+ }
1075
+ } catch (error) {
1076
+ if (retries > 0) {
1077
+ const delay = Math.pow(2, MAX_REFRESH_RETRIES - retries) * 1e3;
1078
+ console.warn(
1079
+ `Token refresh failed, retrying in ${delay / 1e3}s (${retries} attempts remaining)`,
1080
+ error
1081
+ );
1082
+ this.refreshTimer = setTimeout(() => {
1083
+ this.attemptRefresh(retries - 1);
1084
+ }, delay);
1085
+ } else {
1086
+ console.error(
1087
+ "Token refresh failed after all retries, signing out",
1088
+ error
1089
+ );
1090
+ this.clearSession();
1091
+ }
880
1092
  }
881
1093
  }
882
1094
  /**
@@ -907,14 +1119,24 @@ var RealtimeChannel = class {
907
1119
  this.reconnectAttempts = 0;
908
1120
  this.maxReconnectAttempts = 10;
909
1121
  this.reconnectDelay = 1e3;
1122
+ this.shouldReconnect = true;
910
1123
  this.heartbeatInterval = null;
911
1124
  this.pendingAcks = /* @__PURE__ */ new Map();
912
1125
  this.messageIdCounter = 0;
1126
+ this.onTokenRefreshNeeded = null;
1127
+ this.isRefreshingToken = false;
913
1128
  this.url = url;
914
1129
  this.channelName = channelName;
915
1130
  this.token = token;
916
1131
  this.config = config;
917
1132
  }
1133
+ /**
1134
+ * Set callback to request a token refresh when connection fails due to auth
1135
+ * @internal
1136
+ */
1137
+ setTokenRefreshCallback(callback) {
1138
+ this.onTokenRefreshNeeded = callback;
1139
+ }
918
1140
  // Implementation
919
1141
  on(event, configOrCallback, callback) {
920
1142
  if (event === "postgres_changes" && typeof configOrCallback !== "function") {
@@ -966,6 +1188,7 @@ var RealtimeChannel = class {
966
1188
  * @param _timeout - Optional timeout in milliseconds (currently unused)
967
1189
  */
968
1190
  subscribe(callback, _timeout) {
1191
+ this.shouldReconnect = true;
969
1192
  this.connect();
970
1193
  if (callback) {
971
1194
  const checkConnection = () => {
@@ -987,6 +1210,7 @@ var RealtimeChannel = class {
987
1210
  * @returns Promise resolving to status string (Supabase-compatible)
988
1211
  */
989
1212
  async unsubscribe(timeout) {
1213
+ this.shouldReconnect = false;
990
1214
  return new Promise((resolve) => {
991
1215
  if (this.ws) {
992
1216
  this.sendMessage({
@@ -1162,6 +1386,22 @@ var RealtimeChannel = class {
1162
1386
  presenceState() {
1163
1387
  return { ...this._presenceState };
1164
1388
  }
1389
+ /**
1390
+ * Check if the current token is expired or about to expire
1391
+ */
1392
+ isTokenExpired() {
1393
+ if (!this.token) return false;
1394
+ try {
1395
+ const parts = this.token.split(".");
1396
+ if (parts.length !== 3 || !parts[1]) return false;
1397
+ const payload = JSON.parse(atob(parts[1].replace(/-/g, "+").replace(/_/g, "/")));
1398
+ if (!payload.exp) return false;
1399
+ const now = Math.floor(Date.now() / 1e3);
1400
+ return payload.exp <= now + 10;
1401
+ } catch {
1402
+ return true;
1403
+ }
1404
+ }
1165
1405
  /**
1166
1406
  * Internal: Connect to WebSocket
1167
1407
  */
@@ -1169,6 +1409,29 @@ var RealtimeChannel = class {
1169
1409
  if (this.ws && this.ws.readyState === WebSocket.OPEN) {
1170
1410
  return;
1171
1411
  }
1412
+ if (this.isTokenExpired() && this.onTokenRefreshNeeded && !this.isRefreshingToken) {
1413
+ this.isRefreshingToken = true;
1414
+ console.log("[Fluxbase Realtime] Token expired, requesting refresh before connecting");
1415
+ this.onTokenRefreshNeeded().then((newToken) => {
1416
+ this.isRefreshingToken = false;
1417
+ if (newToken) {
1418
+ this.token = newToken;
1419
+ console.log("[Fluxbase Realtime] Token refreshed, connecting with new token");
1420
+ }
1421
+ this.connectWithToken();
1422
+ }).catch((err) => {
1423
+ this.isRefreshingToken = false;
1424
+ console.error("[Fluxbase Realtime] Token refresh failed:", err);
1425
+ this.connectWithToken();
1426
+ });
1427
+ return;
1428
+ }
1429
+ this.connectWithToken();
1430
+ }
1431
+ /**
1432
+ * Internal: Actually establish the WebSocket connection
1433
+ */
1434
+ connectWithToken() {
1172
1435
  const wsUrl = new URL(this.url);
1173
1436
  wsUrl.protocol = wsUrl.protocol === "https:" ? "wss:" : "ws:";
1174
1437
  wsUrl.pathname = "/realtime";
@@ -1190,11 +1453,17 @@ var RealtimeChannel = class {
1190
1453
  this.startHeartbeat();
1191
1454
  };
1192
1455
  this.ws.onmessage = (event) => {
1456
+ let message;
1193
1457
  try {
1194
- const message = JSON.parse(event.data);
1195
- this.handleMessage(message);
1458
+ message = typeof event.data === "string" ? JSON.parse(event.data) : event.data;
1196
1459
  } catch (err) {
1197
1460
  console.error("[Fluxbase Realtime] Failed to parse message:", err);
1461
+ return;
1462
+ }
1463
+ try {
1464
+ this.handleMessage(message);
1465
+ } catch (err) {
1466
+ console.error("[Fluxbase Realtime] Error handling message:", err, message);
1198
1467
  }
1199
1468
  };
1200
1469
  this.ws.onerror = (error) => {
@@ -1230,7 +1499,6 @@ var RealtimeChannel = class {
1230
1499
  handleMessage(message) {
1231
1500
  switch (message.type) {
1232
1501
  case "heartbeat":
1233
- this.ws?.send(JSON.stringify({ type: "heartbeat" }));
1234
1502
  break;
1235
1503
  case "broadcast":
1236
1504
  if (message.broadcast) {
@@ -1250,6 +1518,23 @@ var RealtimeChannel = class {
1250
1518
  if (ackHandler) {
1251
1519
  ackHandler.resolve(message.status || "ok");
1252
1520
  }
1521
+ } else if (message.payload && typeof message.payload === "object" && "type" in message.payload) {
1522
+ const payload = message.payload;
1523
+ if (payload.type === "access_token" && this.pendingAcks.has("access_token")) {
1524
+ const ackHandler = this.pendingAcks.get("access_token");
1525
+ if (ackHandler) {
1526
+ ackHandler.resolve("ok");
1527
+ this.pendingAcks.delete("access_token");
1528
+ }
1529
+ console.log("[Fluxbase Realtime] Token updated successfully");
1530
+ } else {
1531
+ if (payload.subscription_id) {
1532
+ this.subscriptionId = payload.subscription_id;
1533
+ console.log("[Fluxbase Realtime] Subscription ID received:", this.subscriptionId);
1534
+ } else {
1535
+ console.log("[Fluxbase Realtime] Acknowledged:", message);
1536
+ }
1537
+ }
1253
1538
  } else {
1254
1539
  if (message.payload && typeof message.payload === "object" && "subscription_id" in message.payload) {
1255
1540
  this.subscriptionId = message.payload.subscription_id;
@@ -1261,6 +1546,18 @@ var RealtimeChannel = class {
1261
1546
  break;
1262
1547
  case "error":
1263
1548
  console.error("[Fluxbase Realtime] Error:", message.error);
1549
+ if (this.pendingAcks.has("access_token")) {
1550
+ const ackHandler = this.pendingAcks.get("access_token");
1551
+ if (ackHandler) {
1552
+ ackHandler.reject(new Error(message.error || "Token update failed"));
1553
+ this.pendingAcks.delete("access_token");
1554
+ }
1555
+ }
1556
+ break;
1557
+ case "postgres_changes":
1558
+ if (message.payload) {
1559
+ this.handlePostgresChanges(message.payload);
1560
+ }
1264
1561
  break;
1265
1562
  }
1266
1563
  }
@@ -1314,7 +1611,7 @@ var RealtimeChannel = class {
1314
1611
  schema: payload.schema,
1315
1612
  table: payload.table,
1316
1613
  commit_timestamp: payload.timestamp || payload.commit_timestamp || (/* @__PURE__ */ new Date()).toISOString(),
1317
- new: payload.new_record || payload.new || {},
1614
+ new: payload.new_record || payload.new || payload.record || {},
1318
1615
  old: payload.old_record || payload.old || {},
1319
1616
  errors: payload.errors || null
1320
1617
  };
@@ -1331,6 +1628,7 @@ var RealtimeChannel = class {
1331
1628
  * Internal: Start heartbeat interval
1332
1629
  */
1333
1630
  startHeartbeat() {
1631
+ this.stopHeartbeat();
1334
1632
  this.heartbeatInterval = setInterval(() => {
1335
1633
  this.sendMessage({ type: "heartbeat" });
1336
1634
  }, 3e4);
@@ -1344,10 +1642,61 @@ var RealtimeChannel = class {
1344
1642
  this.heartbeatInterval = null;
1345
1643
  }
1346
1644
  }
1645
+ /**
1646
+ * Update the authentication token on an existing connection
1647
+ * Sends an access_token message to the server to update auth context
1648
+ * On failure, silently triggers a reconnect
1649
+ *
1650
+ * @param token - The new JWT access token
1651
+ * @internal
1652
+ */
1653
+ updateToken(token) {
1654
+ this.token = token;
1655
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
1656
+ return;
1657
+ }
1658
+ if (!token) {
1659
+ this.disconnect();
1660
+ this.connect();
1661
+ return;
1662
+ }
1663
+ const message = {
1664
+ type: "access_token",
1665
+ token
1666
+ };
1667
+ try {
1668
+ this.ws.send(JSON.stringify(message));
1669
+ const timeout = setTimeout(() => {
1670
+ console.warn(
1671
+ "[Fluxbase Realtime] Token update acknowledgment timeout, reconnecting"
1672
+ );
1673
+ this.disconnect();
1674
+ this.connect();
1675
+ }, 5e3);
1676
+ this.pendingAcks.set("access_token", {
1677
+ resolve: () => {
1678
+ clearTimeout(timeout);
1679
+ },
1680
+ reject: () => {
1681
+ clearTimeout(timeout);
1682
+ this.disconnect();
1683
+ this.connect();
1684
+ },
1685
+ timeout
1686
+ });
1687
+ } catch (error) {
1688
+ console.error("[Fluxbase Realtime] Failed to send token update:", error);
1689
+ this.disconnect();
1690
+ this.connect();
1691
+ }
1692
+ }
1347
1693
  /**
1348
1694
  * Internal: Attempt to reconnect
1349
1695
  */
1350
1696
  attemptReconnect() {
1697
+ if (!this.shouldReconnect) {
1698
+ return;
1699
+ }
1351
1700
  if (this.reconnectAttempts >= this.maxReconnectAttempts) {
1352
1701
  console.error("[Fluxbase Realtime] Max reconnect attempts reached");
1353
1702
  return;
@@ -1365,9 +1714,21 @@ var RealtimeChannel = class {
1365
1714
  var FluxbaseRealtime = class {
1366
1715
  constructor(url, token = null) {
1367
1716
  this.channels = /* @__PURE__ */ new Map();
1717
+ this.tokenRefreshCallback = null;
1368
1718
  this.url = url;
1369
1719
  this.token = token;
1370
1720
  }
1721
+ /**
1722
+ * Set callback to request a token refresh when connections fail due to auth
1723
+ * This callback should refresh the auth token and return the new access token
1724
+ * @internal
1725
+ */
1726
+ setTokenRefreshCallback(callback) {
1727
+ this.tokenRefreshCallback = callback;
1728
+ this.channels.forEach((channel) => {
1729
+ channel.setTokenRefreshCallback(callback);
1730
+ });
1731
+ }
1371
1732
  /**
1372
1733
  * Create or get a channel with optional configuration
1373
1734
  *
@@ -1395,6 +1756,9 @@ var FluxbaseRealtime = class {
1395
1756
  this.token,
1396
1757
  config
1397
1758
  );
1759
+ if (this.tokenRefreshCallback) {
1760
+ channel.setTokenRefreshCallback(this.tokenRefreshCallback);
1761
+ }
1398
1762
  this.channels.set(key, channel);
1399
1763
  return channel;
1400
1764
  }
@@ -1429,10 +1793,16 @@ var FluxbaseRealtime = class {
1429
1793
  }
1430
1794
  /**
1431
1795
  * Update auth token for all channels
1796
+ * Updates both the stored token for new channels and propagates
1797
+ * the token to all existing connected channels.
1798
+ *
1432
1799
  * @param token - The new auth token
1433
1800
  */
1434
1801
  setAuth(token) {
1435
1802
  this.token = token;
1803
+ this.channels.forEach((channel) => {
1804
+ channel.updateToken(token);
1805
+ });
1436
1806
  }
1437
1807
  };
1438
1808
 
@@ -1542,23 +1912,228 @@ var StorageBucket = class {
1542
1912
  xhr.send(formData);
1543
1913
  });
1544
1914
  }
1915
+ async download(path, options) {
1916
+ try {
1917
+ const controller = new AbortController();
1918
+ let timeoutId;
1919
+ if (options?.signal) {
1920
+ if (options.signal.aborted) {
1921
+ return { data: null, error: new Error("Download aborted") };
1922
+ }
1923
+ options.signal.addEventListener("abort", () => controller.abort(), {
1924
+ once: true
1925
+ });
1926
+ }
1927
+ const timeout = options?.timeout ?? (options?.stream ? 0 : 3e4);
1928
+ if (timeout > 0) {
1929
+ timeoutId = setTimeout(() => controller.abort(), timeout);
1930
+ }
1931
+ try {
1932
+ const response = await fetch(
1933
+ `${this.fetch["baseUrl"]}/api/v1/storage/${this.bucketName}/${path}`,
1934
+ {
1935
+ headers: this.fetch["defaultHeaders"],
1936
+ signal: controller.signal
1937
+ }
1938
+ );
1939
+ if (timeoutId) clearTimeout(timeoutId);
1940
+ if (!response.ok) {
1941
+ throw new Error(`Failed to download file: ${response.statusText}`);
1942
+ }
1943
+ if (options?.stream) {
1944
+ if (!response.body) {
1945
+ throw new Error("Response body is not available for streaming");
1946
+ }
1947
+ const contentLength = response.headers.get("content-length");
1948
+ const size = contentLength ? parseInt(contentLength, 10) : null;
1949
+ return {
1950
+ data: { stream: response.body, size },
1951
+ error: null
1952
+ };
1953
+ }
1954
+ const blob = await response.blob();
1955
+ return { data: blob, error: null };
1956
+ } catch (err) {
1957
+ if (timeoutId) clearTimeout(timeoutId);
1958
+ if (err instanceof Error && err.name === "AbortError") {
1959
+ if (options?.signal?.aborted) {
1960
+ return { data: null, error: new Error("Download aborted") };
1961
+ }
1962
+ return { data: null, error: new Error("Download timeout") };
1963
+ }
1964
+ throw err;
1965
+ }
1966
+ } catch (error) {
1967
+ return { data: null, error };
1968
+ }
1969
+ }
1545
1970
  /**
1546
- * Download a file from the bucket
1547
- * @param path - The path/key of the file
1971
+ * Download a file with resumable chunked downloads for large files.
1972
+ * Returns a ReadableStream that abstracts the chunking internally.
1973
+ *
1974
+ * Features:
1975
+ * - Downloads file in chunks using HTTP Range headers
1976
+ * - Automatically retries failed chunks with exponential backoff
1977
+ * - Reports progress via callback
1978
+ * - Falls back to regular streaming if Range not supported
1979
+ *
1980
+ * @param path - The file path within the bucket
1981
+ * @param options - Download options including chunk size, retries, and progress callback
1982
+ * @returns A ReadableStream and file size (consumer doesn't need to know about chunking)
1983
+ *
1984
+ * @example
1985
+ * ```typescript
1986
+ * const { data, error } = await storage.from('bucket').downloadResumable('large.json', {
1987
+ * chunkSize: 5 * 1024 * 1024, // 5MB chunks
1988
+ * maxRetries: 3,
1989
+ * onProgress: (progress) => console.log(`${progress.percentage}% complete`)
1990
+ * });
1991
+ * if (data) {
1992
+ * console.log(`File size: ${data.size} bytes`);
1993
+ * // Process data.stream...
1994
+ * }
1995
+ * ```
1548
1996
  */
1549
- async download(path) {
1997
+ async downloadResumable(path, options) {
1550
1998
  try {
1551
- const response = await fetch(
1552
- `${this.fetch["baseUrl"]}/api/v1/storage/${this.bucketName}/${path}`,
1553
- {
1554
- headers: this.fetch["defaultHeaders"]
1555
- }
1556
- );
1557
- if (!response.ok) {
1558
- throw new Error(`Failed to download file: ${response.statusText}`);
1999
+ const chunkSize = options?.chunkSize ?? 5 * 1024 * 1024;
2000
+ const maxRetries = options?.maxRetries ?? 3;
2001
+ const retryDelayMs = options?.retryDelayMs ?? 1e3;
2002
+ const chunkTimeout = options?.chunkTimeout ?? 3e4;
2003
+ const url = `${this.fetch["baseUrl"]}/api/v1/storage/${this.bucketName}/${path}`;
2004
+ const headers = this.fetch["defaultHeaders"];
2005
+ if (options?.signal?.aborted) {
2006
+ return { data: null, error: new Error("Download aborted") };
2007
+ }
2008
+ const headResponse = await fetch(url, {
2009
+ method: "HEAD",
2010
+ headers,
2011
+ signal: options?.signal
2012
+ });
2013
+ if (!headResponse.ok) {
2014
+ throw new Error(`Failed to get file info: ${headResponse.statusText}`);
2015
+ }
2016
+ const contentLength = headResponse.headers.get("content-length");
2017
+ const acceptRanges = headResponse.headers.get("accept-ranges");
2018
+ const totalSize = contentLength ? parseInt(contentLength, 10) : null;
2019
+ if (acceptRanges !== "bytes") {
2020
+ const { data, error } = await this.download(path, {
2021
+ stream: true,
2022
+ timeout: 0,
2023
+ signal: options?.signal
2024
+ });
2025
+ if (error) return { data: null, error };
2026
+ return {
2027
+ data,
2028
+ error: null
2029
+ };
1559
2030
  }
1560
- const blob = await response.blob();
1561
- return { data: blob, error: null };
2031
+ let downloadedBytes = 0;
2032
+ let currentChunk = 0;
2033
+ const totalChunks = totalSize ? Math.ceil(totalSize / chunkSize) : null;
2034
+ let lastProgressTime = Date.now();
2035
+ let lastProgressBytes = 0;
2036
+ const stream = new ReadableStream({
2037
+ async pull(controller) {
2038
+ if (options?.signal?.aborted) {
2039
+ controller.error(new Error("Download aborted"));
2040
+ return;
2041
+ }
2042
+ if (totalSize !== null && downloadedBytes >= totalSize) {
2043
+ controller.close();
2044
+ return;
2045
+ }
2046
+ const rangeStart = downloadedBytes;
2047
+ const rangeEnd = totalSize !== null ? Math.min(downloadedBytes + chunkSize - 1, totalSize - 1) : downloadedBytes + chunkSize - 1;
2048
+ let retryCount = 0;
2049
+ let chunk = null;
2050
+ while (retryCount <= maxRetries && chunk === null) {
2051
+ try {
2052
+ if (options?.signal?.aborted) {
2053
+ controller.error(new Error("Download aborted"));
2054
+ return;
2055
+ }
2056
+ const chunkController = new AbortController();
2057
+ const timeoutId = setTimeout(
2058
+ () => chunkController.abort(),
2059
+ chunkTimeout
2060
+ );
2061
+ if (options?.signal) {
2062
+ options.signal.addEventListener(
2063
+ "abort",
2064
+ () => chunkController.abort(),
2065
+ { once: true }
2066
+ );
2067
+ }
2068
+ const chunkResponse = await fetch(url, {
2069
+ headers: {
2070
+ ...headers,
2071
+ Range: `bytes=${rangeStart}-${rangeEnd}`
2072
+ },
2073
+ signal: chunkController.signal
2074
+ });
2075
+ clearTimeout(timeoutId);
2076
+ if (!chunkResponse.ok && chunkResponse.status !== 206) {
2077
+ throw new Error(
2078
+ `Chunk download failed: ${chunkResponse.statusText}`
2079
+ );
2080
+ }
2081
+ const arrayBuffer = await chunkResponse.arrayBuffer();
2082
+ chunk = new Uint8Array(arrayBuffer);
2083
+ if (totalSize === null && chunk.byteLength < chunkSize) {
2084
+ downloadedBytes += chunk.byteLength;
2085
+ currentChunk++;
2086
+ controller.enqueue(chunk);
2087
+ controller.close();
2088
+ return;
2089
+ }
2090
+ } catch (err) {
2091
+ if (options?.signal?.aborted) {
2092
+ controller.error(new Error("Download aborted"));
2093
+ return;
2094
+ }
2095
+ retryCount++;
2096
+ if (retryCount > maxRetries) {
2097
+ controller.error(
2098
+ new Error(
2099
+ `Failed to download chunk after ${maxRetries} retries`
2100
+ )
2101
+ );
2102
+ return;
2103
+ }
2104
+ const delay = retryDelayMs * Math.pow(2, retryCount - 1);
2105
+ await new Promise((resolve) => setTimeout(resolve, delay));
2106
+ }
2107
+ }
2108
+ if (chunk) {
2109
+ downloadedBytes += chunk.byteLength;
2110
+ currentChunk++;
2111
+ if (options?.onProgress) {
2112
+ const now = Date.now();
2113
+ const elapsed = (now - lastProgressTime) / 1e3;
2114
+ const bytesPerSecond = elapsed > 0 ? (downloadedBytes - lastProgressBytes) / elapsed : 0;
2115
+ lastProgressTime = now;
2116
+ lastProgressBytes = downloadedBytes;
2117
+ options.onProgress({
2118
+ loaded: downloadedBytes,
2119
+ total: totalSize,
2120
+ percentage: totalSize ? Math.round(downloadedBytes / totalSize * 100) : null,
2121
+ currentChunk,
2122
+ totalChunks,
2123
+ bytesPerSecond
2124
+ });
2125
+ }
2126
+ controller.enqueue(chunk);
2127
+ if (totalSize !== null && downloadedBytes >= totalSize) {
2128
+ controller.close();
2129
+ }
2130
+ }
2131
+ }
2132
+ });
2133
+ return {
2134
+ data: { stream, size: totalSize },
2135
+ error: null
2136
+ };
1562
2137
  } catch (error) {
1563
2138
  return { data: null, error };
1564
2139
  }
@@ -4846,7 +5421,9 @@ var FluxbaseAdminJobs = class _FluxbaseAdminJobs {
4846
5421
  */
4847
5422
  async getJob(jobId) {
4848
5423
  try {
4849
- const data = await this.fetch.get(`/api/v1/admin/jobs/queue/${jobId}`);
5424
+ const data = await this.fetch.get(
5425
+ `/api/v1/admin/jobs/queue/${jobId}`
5426
+ );
4850
5427
  return { data, error: null };
4851
5428
  } catch (error) {
4852
5429
  return { data: null, error };
@@ -5061,8 +5638,14 @@ var FluxbaseAdminJobs = class _FluxbaseAdminJobs {
5061
5638
  return fn;
5062
5639
  }
5063
5640
  const bundled = await _FluxbaseAdminJobs.bundleCode({
5641
+ // Apply global bundle options first
5642
+ ...bundleOptions,
5643
+ // Then override with per-function values (these take priority)
5064
5644
  code: fn.code,
5065
- ...bundleOptions
5645
+ // Use function's sourceDir for resolving relative imports
5646
+ baseDir: fn.sourceDir || bundleOptions?.baseDir,
5647
+ // Use function's nodePaths for additional module resolution
5648
+ nodePaths: fn.nodePaths || bundleOptions?.nodePaths
5066
5649
  });
5067
5650
  return {
5068
5651
  ...fn,
@@ -5122,23 +5705,74 @@ var FluxbaseAdminJobs = class _FluxbaseAdminJobs {
5122
5705
  "esbuild is required for bundling. Install it with: npm install esbuild"
5123
5706
  );
5124
5707
  }
5125
- const result = await esbuild.build({
5708
+ const externals = [...options.external ?? []];
5709
+ const alias = {};
5710
+ if (options.importMap) {
5711
+ for (const [key, value] of Object.entries(options.importMap)) {
5712
+ if (value.startsWith("npm:")) {
5713
+ externals.push(key);
5714
+ } else if (value.startsWith("https://") || value.startsWith("http://")) {
5715
+ externals.push(key);
5716
+ } else if (value.startsWith("/") || value.startsWith("./") || value.startsWith("../")) {
5717
+ alias[key] = value;
5718
+ } else {
5719
+ externals.push(key);
5720
+ }
5721
+ }
5722
+ }
5723
+ const denoExternalPlugin = {
5724
+ name: "deno-external",
5725
+ setup(build) {
5726
+ build.onResolve({ filter: /^npm:/ }, (args) => ({
5727
+ path: args.path,
5728
+ external: true
5729
+ }));
5730
+ build.onResolve({ filter: /^https?:\/\// }, (args) => ({
5731
+ path: args.path,
5732
+ external: true
5733
+ }));
5734
+ build.onResolve({ filter: /^jsr:/ }, (args) => ({
5735
+ path: args.path,
5736
+ external: true
5737
+ }));
5738
+ }
5739
+ };
5740
+ const resolveDir = options.baseDir || process.cwd?.() || "/";
5741
+ const buildOptions = {
5126
5742
  stdin: {
5127
5743
  contents: options.code,
5128
5744
  loader: "ts",
5129
- resolveDir: process.cwd?.() || "/"
5745
+ resolveDir
5130
5746
  },
5747
+ // Set absWorkingDir for consistent path resolution
5748
+ absWorkingDir: resolveDir,
5131
5749
  bundle: true,
5132
5750
  write: false,
5133
5751
  format: "esm",
5134
- platform: "neutral",
5752
+ // Use 'node' platform for better node_modules resolution (Deno supports Node APIs)
5753
+ platform: "node",
5135
5754
  target: "esnext",
5136
5755
  minify: options.minify ?? false,
5137
5756
  sourcemap: options.sourcemap ? "inline" : false,
5138
- external: options.external ?? [],
5757
+ external: externals,
5758
+ plugins: [denoExternalPlugin],
5139
5759
  // Preserve handler export
5140
- treeShaking: true
5141
- });
5760
+ treeShaking: true,
5761
+ // Resolve .ts, .js, .mjs extensions
5762
+ resolveExtensions: [".ts", ".tsx", ".js", ".mjs", ".json"],
5763
+ // ESM conditions for better module resolution
5764
+ conditions: ["import", "module"]
5765
+ };
5766
+ if (Object.keys(alias).length > 0) {
5767
+ buildOptions.alias = alias;
5768
+ }
5769
+ if (options.nodePaths && options.nodePaths.length > 0) {
5770
+ buildOptions.nodePaths = options.nodePaths;
5771
+ }
5772
+ if (options.define) {
5773
+ buildOptions.define = options.define;
5774
+ }
5775
+ const result = await esbuild.build(buildOptions);
5142
5776
  const output = result.outputFiles?.[0];
5143
5777
  if (!output) {
5144
5778
  throw new Error("Bundling failed: no output generated");
@@ -5495,7 +6129,7 @@ var FluxbaseAdmin = class {
5495
6129
 
5496
6130
  // src/query-builder.ts
5497
6131
  var QueryBuilder = class {
5498
- constructor(fetch2, table) {
6132
+ constructor(fetch2, table, schema) {
5499
6133
  this.selectQuery = "*";
5500
6134
  this.filters = [];
5501
6135
  this.orFilters = [];
@@ -5504,17 +6138,33 @@ var QueryBuilder = class {
5504
6138
  this.singleRow = false;
5505
6139
  this.maybeSingleRow = false;
5506
6140
  this.operationType = "select";
6141
+ this.headOnly = false;
5507
6142
  this.fetch = fetch2;
5508
6143
  this.table = table;
6144
+ this.schema = schema;
6145
+ }
6146
+ /**
6147
+ * Build the API path for this table, including schema if specified
6148
+ */
6149
+ buildTablePath() {
6150
+ return this.schema ? `/api/v1/tables/${this.schema}/${this.table}` : `/api/v1/tables/${this.table}`;
5509
6151
  }
5510
6152
  /**
5511
6153
  * Select columns to return
5512
6154
  * @example select('*')
5513
6155
  * @example select('id, name, email')
5514
6156
  * @example select('id, name, posts(title, content)')
6157
+ * @example select('*', { count: 'exact' }) // Get exact count
6158
+ * @example select('*', { count: 'exact', head: true }) // Get count only (no data)
5515
6159
  */
5516
- select(columns = "*") {
6160
+ select(columns = "*", options) {
5517
6161
  this.selectQuery = columns;
6162
+ if (options?.count) {
6163
+ this.countType = options.count;
6164
+ }
6165
+ if (options?.head) {
6166
+ this.headOnly = true;
6167
+ }
5518
6168
  return this;
5519
6169
  }
5520
6170
  /**
@@ -5544,7 +6194,7 @@ var QueryBuilder = class {
5544
6194
  const headers = {
5545
6195
  Prefer: preferValues.join(",")
5546
6196
  };
5547
- let path = `/api/v1/tables/${this.table}`;
6197
+ let path = this.buildTablePath();
5548
6198
  if (options?.onConflict) {
5549
6199
  path += `?on_conflict=${encodeURIComponent(options.onConflict)}`;
5550
6200
  }
@@ -6109,10 +6759,7 @@ var QueryBuilder = class {
6109
6759
  throw new Error("Insert data is required for insert operation");
6110
6760
  }
6111
6761
  const body = Array.isArray(this.insertData) ? this.insertData : this.insertData;
6112
- const response = await this.fetch.post(
6113
- `/api/v1/tables/${this.table}`,
6114
- body
6115
- );
6762
+ const response = await this.fetch.post(this.buildTablePath(), body);
6116
6763
  return {
6117
6764
  data: response,
6118
6765
  error: null,
@@ -6126,7 +6773,7 @@ var QueryBuilder = class {
6126
6773
  throw new Error("Update data is required for update operation");
6127
6774
  }
6128
6775
  const queryString2 = this.buildQueryString();
6129
- const path2 = `/api/v1/tables/${this.table}${queryString2}`;
6776
+ const path2 = `${this.buildTablePath()}${queryString2}`;
6130
6777
  const response = await this.fetch.patch(path2, this.updateData);
6131
6778
  return {
6132
6779
  data: response,
@@ -6138,7 +6785,7 @@ var QueryBuilder = class {
6138
6785
  }
6139
6786
  if (this.operationType === "delete") {
6140
6787
  const queryString2 = this.buildQueryString();
6141
- const path2 = `/api/v1/tables/${this.table}${queryString2}`;
6788
+ const path2 = `${this.buildTablePath()}${queryString2}`;
6142
6789
  await this.fetch.delete(path2);
6143
6790
  return {
6144
6791
  data: null,
@@ -6149,7 +6796,66 @@ var QueryBuilder = class {
6149
6796
  };
6150
6797
  }
6151
6798
  const queryString = this.buildQueryString();
6152
- const path = `/api/v1/tables/${this.table}${queryString}`;
6799
+ const path = `${this.buildTablePath()}${queryString}`;
6800
+ if (this.countType) {
6801
+ const response = await this.fetch.getWithHeaders(path);
6802
+ const serverCount = this.parseContentRangeCount(response.headers);
6803
+ const data2 = response.data;
6804
+ if (this.headOnly) {
6805
+ return {
6806
+ data: null,
6807
+ error: null,
6808
+ count: serverCount,
6809
+ status: response.status,
6810
+ statusText: "OK"
6811
+ };
6812
+ }
6813
+ if (this.singleRow) {
6814
+ if (Array.isArray(data2) && data2.length === 0) {
6815
+ return {
6816
+ data: null,
6817
+ error: { message: "No rows found", code: "PGRST116" },
6818
+ count: serverCount ?? 0,
6819
+ status: 404,
6820
+ statusText: "Not Found"
6821
+ };
6822
+ }
6823
+ const singleData = Array.isArray(data2) ? data2[0] : data2;
6824
+ return {
6825
+ data: singleData,
6826
+ error: null,
6827
+ count: serverCount ?? 1,
6828
+ status: 200,
6829
+ statusText: "OK"
6830
+ };
6831
+ }
6832
+ if (this.maybeSingleRow) {
6833
+ if (Array.isArray(data2) && data2.length === 0) {
6834
+ return {
6835
+ data: null,
6836
+ error: null,
6837
+ count: serverCount ?? 0,
6838
+ status: 200,
6839
+ statusText: "OK"
6840
+ };
6841
+ }
6842
+ const singleData = Array.isArray(data2) ? data2[0] : data2;
6843
+ return {
6844
+ data: singleData,
6845
+ error: null,
6846
+ count: serverCount ?? 1,
6847
+ status: 200,
6848
+ statusText: "OK"
6849
+ };
6850
+ }
6851
+ return {
6852
+ data: data2,
6853
+ error: null,
6854
+ count: serverCount ?? (Array.isArray(data2) ? data2.length : null),
6855
+ status: 200,
6856
+ statusText: "OK"
6857
+ };
6858
+ }
6153
6859
  const data = await this.fetch.get(path);
6154
6860
  if (this.singleRow) {
6155
6861
  if (Array.isArray(data) && data.length === 0) {
@@ -6292,6 +6998,9 @@ var QueryBuilder = class {
6292
6998
  if (this.offsetValue !== void 0) {
6293
6999
  params.append("offset", String(this.offsetValue));
6294
7000
  }
7001
+ if (this.countType) {
7002
+ params.append("count", this.countType);
7003
+ }
6295
7004
  const queryString = params.toString();
6296
7005
  return queryString ? `?${queryString}` : "";
6297
7006
  }
@@ -6313,6 +7022,38 @@ var QueryBuilder = class {
6313
7022
  }
6314
7023
  return String(value);
6315
7024
  }
7025
+ /**
7026
+ * Parse the Content-Range header to extract the total count
7027
+ * Header format: "0-999/50000" or "* /50000" (when no rows returned)
7028
+ */
7029
+ parseContentRangeCount(headers) {
7030
+ const contentRange = headers.get("Content-Range");
7031
+ if (!contentRange) {
7032
+ return null;
7033
+ }
7034
+ const match = contentRange.match(/\/(\d+)$/);
7035
+ if (match && match[1]) {
7036
+ return parseInt(match[1], 10);
7037
+ }
7038
+ return null;
7039
+ }
7040
+ };
7041
+
7042
+ // src/schema-query-builder.ts
7043
+ var SchemaQueryBuilder = class {
7044
+ constructor(fetch2, schemaName) {
7045
+ this.fetch = fetch2;
7046
+ this.schemaName = schemaName;
7047
+ }
7048
+ /**
7049
+ * Create a query builder for a table in this schema
7050
+ *
7051
+ * @param table - The table name (without schema prefix)
7052
+ * @returns A query builder instance for constructing and executing queries
7053
+ */
7054
+ from(table) {
7055
+ return new QueryBuilder(this.fetch, table, this.schemaName);
7056
+ }
6316
7057
  };
6317
7058
 
6318
7059
  // src/client.ts
@@ -6346,6 +7087,7 @@ var FluxbaseClient = class {
6346
7087
  timeout: options?.timeout,
6347
7088
  debug: options?.debug
6348
7089
  });
7090
+ this.fetch.setAnonKey(fluxbaseKey);
6349
7091
  this.auth = new FluxbaseAuth(
6350
7092
  this.fetch,
6351
7093
  options?.auth?.autoRefresh ?? true,
@@ -6393,6 +7135,37 @@ var FluxbaseClient = class {
6393
7135
  from(table) {
6394
7136
  return new QueryBuilder(this.fetch, table);
6395
7137
  }
7138
+ /**
7139
+ * Access a specific database schema
7140
+ *
7141
+ * Use this to query tables in non-public schemas.
7142
+ *
7143
+ * @param schemaName - The schema name (e.g., 'jobs', 'analytics')
7144
+ * @returns A schema query builder for constructing queries on that schema
7145
+ *
7146
+ * @example
7147
+ * ```typescript
7148
+ * // Query the jobs.execution_logs table
7149
+ * const { data } = await client
7150
+ * .schema('jobs')
7151
+ * .from('execution_logs')
7152
+ * .select('*')
7153
+ * .eq('job_id', jobId)
7154
+ * .execute()
7155
+ *
7156
+ * // Insert into a custom schema table
7157
+ * await client
7158
+ * .schema('analytics')
7159
+ * .from('events')
7160
+ * .insert({ event_type: 'click', data: {} })
7161
+ * .execute()
7162
+ * ```
7163
+ *
7164
+ * @category Database
7165
+ */
7166
+ schema(schemaName) {
7167
+ return new SchemaQueryBuilder(this.fetch, schemaName);
7168
+ }
6396
7169
  /**
6397
7170
  * Call a PostgreSQL function (Remote Procedure Call)
6398
7171
  *
@@ -6435,6 +7208,14 @@ var FluxbaseClient = class {
6435
7208
  originalSetAuthToken(token);
6436
7209
  this.realtime.setAuth(token);
6437
7210
  };
7211
+ this.realtime.setTokenRefreshCallback(async () => {
7212
+ const result = await this.auth.refreshSession();
7213
+ if (result.error || !result.data?.session) {
7214
+ console.error("[Fluxbase] Failed to refresh token for realtime:", result.error);
7215
+ return null;
7216
+ }
7217
+ return result.data.session.access_token;
7218
+ });
6438
7219
  }
6439
7220
  /**
6440
7221
  * Get the current authentication token
@@ -6519,12 +7300,29 @@ var FluxbaseClient = class {
6519
7300
  return this.fetch;
6520
7301
  }
6521
7302
  };
7303
+ function getEnvVar(name) {
7304
+ if (typeof process !== "undefined" && process.env) {
7305
+ return process.env[name];
7306
+ }
7307
+ if (typeof Deno !== "undefined" && Deno?.env) {
7308
+ return Deno.env.get(name);
7309
+ }
7310
+ return void 0;
7311
+ }
6522
7312
  function createClient(fluxbaseUrl, fluxbaseKey, options) {
6523
- return new FluxbaseClient(
6524
- fluxbaseUrl,
6525
- fluxbaseKey,
6526
- options
6527
- );
7313
+ const url = fluxbaseUrl || getEnvVar("FLUXBASE_URL") || getEnvVar("NEXT_PUBLIC_FLUXBASE_URL") || getEnvVar("VITE_FLUXBASE_URL");
7314
+ const key = fluxbaseKey || getEnvVar("FLUXBASE_ANON_KEY") || getEnvVar("FLUXBASE_SERVICE_TOKEN") || getEnvVar("FLUXBASE_JOB_TOKEN") || getEnvVar("NEXT_PUBLIC_FLUXBASE_ANON_KEY") || getEnvVar("VITE_FLUXBASE_ANON_KEY");
7315
+ if (!url) {
7316
+ throw new Error(
7317
+ "Fluxbase URL is required. Pass it as the first argument or set FLUXBASE_URL environment variable."
7318
+ );
7319
+ }
7320
+ if (!key) {
7321
+ throw new Error(
7322
+ "Fluxbase key is required. Pass it as the second argument or set FLUXBASE_ANON_KEY environment variable."
7323
+ );
7324
+ }
7325
+ return new FluxbaseClient(url, key, options);
6528
7326
  }
6529
7327
 
6530
7328
  exports.APIKeysManager = APIKeysManager;
@@ -6551,6 +7349,7 @@ exports.InvitationsManager = InvitationsManager;
6551
7349
  exports.OAuthProviderManager = OAuthProviderManager;
6552
7350
  exports.QueryBuilder = QueryBuilder;
6553
7351
  exports.RealtimeChannel = RealtimeChannel;
7352
+ exports.SchemaQueryBuilder = SchemaQueryBuilder;
6554
7353
  exports.SettingsClient = SettingsClient;
6555
7354
  exports.StorageBucket = StorageBucket;
6556
7355
  exports.SystemSettingsManager = SystemSettingsManager;