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