@insforge/sdk 1.4.2 → 1.4.4

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
@@ -32,6 +32,7 @@ var src_exports = {};
32
32
  __export(src_exports, {
33
33
  AI: () => AI,
34
34
  Auth: () => Auth,
35
+ AuthChangeEvent: () => AuthChangeEvent,
35
36
  Database: () => Database,
36
37
  Emails: () => Emails,
37
38
  Functions: () => Functions,
@@ -43,7 +44,6 @@ __export(src_exports, {
43
44
  Realtime: () => Realtime,
44
45
  Storage: () => Storage,
45
46
  StorageBucket: () => StorageBucket,
46
- TokenManager: () => TokenManager,
47
47
  createAdminClient: () => createAdminClient,
48
48
  createClient: () => createClient,
49
49
  default: () => src_default
@@ -97,7 +97,9 @@ function redactHeaders(headers) {
97
97
  return redacted;
98
98
  }
99
99
  function sanitizeBody(body) {
100
- if (body === null || body === void 0) return body;
100
+ if (body === null || body === void 0) {
101
+ return body;
102
+ }
101
103
  if (typeof body === "string") {
102
104
  try {
103
105
  const parsed = JSON.parse(body);
@@ -106,7 +108,9 @@ function sanitizeBody(body) {
106
108
  return body;
107
109
  }
108
110
  }
109
- if (Array.isArray(body)) return body.map(sanitizeBody);
111
+ if (Array.isArray(body)) {
112
+ return body.map(sanitizeBody);
113
+ }
110
114
  if (typeof body === "object") {
111
115
  const sanitized = {};
112
116
  for (const [key, value] of Object.entries(body)) {
@@ -121,7 +125,9 @@ function sanitizeBody(body) {
121
125
  return body;
122
126
  }
123
127
  function formatBody(body) {
124
- if (body === void 0 || body === null) return "";
128
+ if (body === void 0 || body === null) {
129
+ return "";
130
+ }
125
131
  if (typeof body === "string") {
126
132
  try {
127
133
  return JSON.stringify(JSON.parse(body), null, 2);
@@ -158,7 +164,9 @@ var Logger = class {
158
164
  * @param args - Additional arguments to pass to the log function
159
165
  */
160
166
  log(message, ...args) {
161
- if (!this.enabled) return;
167
+ if (!this.enabled) {
168
+ return;
169
+ }
162
170
  const formatted = `[InsForge Debug] ${message}`;
163
171
  if (this.customLog) {
164
172
  this.customLog(formatted, ...args);
@@ -172,7 +180,9 @@ var Logger = class {
172
180
  * @param args - Additional arguments to pass to the log function
173
181
  */
174
182
  warn(message, ...args) {
175
- if (!this.enabled) return;
183
+ if (!this.enabled) {
184
+ return;
185
+ }
176
186
  const formatted = `[InsForge Debug] ${message}`;
177
187
  if (this.customLog) {
178
188
  this.customLog(formatted, ...args);
@@ -186,7 +196,9 @@ var Logger = class {
186
196
  * @param args - Additional arguments to pass to the log function
187
197
  */
188
198
  error(message, ...args) {
189
- if (!this.enabled) return;
199
+ if (!this.enabled) {
200
+ return;
201
+ }
190
202
  const formatted = `[InsForge Debug] ${message}`;
191
203
  if (this.customLog) {
192
204
  this.customLog(formatted, ...args);
@@ -203,10 +215,10 @@ var Logger = class {
203
215
  * @param body - Request body (sensitive fields will be masked)
204
216
  */
205
217
  logRequest(method, url, headers, body) {
206
- if (!this.enabled) return;
207
- const parts = [
208
- `\u2192 ${method} ${url}`
209
- ];
218
+ if (!this.enabled) {
219
+ return;
220
+ }
221
+ const parts = [`\u2192 ${method} ${url}`];
210
222
  if (headers && Object.keys(headers).length > 0) {
211
223
  parts.push(` Headers: ${JSON.stringify(redactHeaders(headers))}`);
212
224
  }
@@ -227,10 +239,10 @@ var Logger = class {
227
239
  * @param body - Response body (sensitive fields will be masked, large bodies truncated)
228
240
  */
229
241
  logResponse(method, url, status, durationMs, body) {
230
- if (!this.enabled) return;
231
- const parts = [
232
- `\u2190 ${method} ${url} ${status} (${durationMs}ms)`
233
- ];
242
+ if (!this.enabled) {
243
+ return;
244
+ }
245
+ const parts = [`\u2190 ${method} ${url} ${status} (${durationMs}ms)`];
234
246
  const formattedBody = formatBody(sanitizeBody(body));
235
247
  if (formattedBody) {
236
248
  const truncated = formattedBody.length > 1e3 ? formattedBody.slice(0, 1e3) + "... [truncated]" : formattedBody;
@@ -245,21 +257,34 @@ var Logger = class {
245
257
  };
246
258
 
247
259
  // src/lib/token-manager.ts
260
+ var AuthChangeEvent = {
261
+ SIGNED_IN: "signedIn",
262
+ SIGNED_OUT: "signedOut",
263
+ TOKEN_REFRESHED: "tokenRefreshed"
264
+ };
248
265
  var CSRF_TOKEN_COOKIE = "insforge_csrf_token";
249
266
  function getCsrfToken() {
250
- if (typeof document === "undefined") return null;
267
+ if (typeof document === "undefined") {
268
+ return null;
269
+ }
251
270
  const match = document.cookie.split(";").find((c) => c.trim().startsWith(`${CSRF_TOKEN_COOKIE}=`));
252
- if (!match) return null;
271
+ if (!match) {
272
+ return null;
273
+ }
253
274
  return match.split("=")[1] || null;
254
275
  }
255
276
  function setCsrfToken(token) {
256
- if (typeof document === "undefined") return;
277
+ if (typeof document === "undefined") {
278
+ return;
279
+ }
257
280
  const maxAge = 7 * 24 * 60 * 60;
258
281
  const secure = typeof window !== "undefined" && window.location.protocol === "https:" ? "; Secure" : "";
259
282
  document.cookie = `${CSRF_TOKEN_COOKIE}=${encodeURIComponent(token)}; path=/; max-age=${maxAge}; SameSite=Lax${secure}`;
260
283
  }
261
284
  function clearCsrfToken() {
262
- if (typeof document === "undefined") return;
285
+ if (typeof document === "undefined") {
286
+ return;
287
+ }
263
288
  const secure = typeof window !== "undefined" && window.location.protocol === "https:" ? "; Secure" : "";
264
289
  document.cookie = `${CSRF_TOKEN_COOKIE}=; path=/; max-age=0; SameSite=Lax${secure}`;
265
290
  }
@@ -268,25 +293,26 @@ var TokenManager = class {
268
293
  // In-memory storage
269
294
  this.accessToken = null;
270
295
  this.user = null;
271
- // Callback for token changes (used by realtime to reconnect with new token)
272
- this.onTokenChange = null;
296
+ this.authStateChangeCallbacks = /* @__PURE__ */ new Map();
273
297
  }
274
298
  /**
275
299
  * Save session in memory
276
300
  */
277
- saveSession(session) {
301
+ saveSession(session, event = AuthChangeEvent.SIGNED_IN) {
278
302
  const tokenChanged = session.accessToken !== this.accessToken;
279
303
  this.accessToken = session.accessToken;
280
304
  this.user = session.user;
281
- if (tokenChanged && this.onTokenChange) {
282
- this.onTokenChange();
305
+ if (tokenChanged) {
306
+ this.notifyAuthStateChange(event);
283
307
  }
284
308
  }
285
309
  /**
286
310
  * Get current session
287
311
  */
288
312
  getSession() {
289
- if (!this.accessToken || !this.user) return null;
313
+ if (!this.accessToken || !this.user) {
314
+ return null;
315
+ }
290
316
  return {
291
317
  accessToken: this.accessToken,
292
318
  user: this.user
@@ -301,11 +327,11 @@ var TokenManager = class {
301
327
  /**
302
328
  * Set access token
303
329
  */
304
- setAccessToken(token) {
330
+ setAccessToken(token, event = AuthChangeEvent.SIGNED_IN) {
305
331
  const tokenChanged = token !== this.accessToken;
306
332
  this.accessToken = token;
307
- if (tokenChanged && this.onTokenChange) {
308
- this.onTokenChange();
333
+ if (tokenChanged) {
334
+ this.notifyAuthStateChange(event);
309
335
  }
310
336
  }
311
337
  /**
@@ -327,22 +353,74 @@ var TokenManager = class {
327
353
  const hadToken = this.accessToken !== null;
328
354
  this.accessToken = null;
329
355
  this.user = null;
330
- if (hadToken && this.onTokenChange) {
331
- this.onTokenChange();
356
+ if (hadToken) {
357
+ this.notifyAuthStateChange(AuthChangeEvent.SIGNED_OUT);
358
+ }
359
+ }
360
+ onAuthStateChange(callback) {
361
+ const id = Symbol("auth-state-change");
362
+ this.authStateChangeCallbacks.set(id, callback);
363
+ return () => this.authStateChangeCallbacks.delete(id);
364
+ }
365
+ notifyAuthStateChange(event) {
366
+ for (const callback of this.authStateChangeCallbacks.values()) {
367
+ try {
368
+ callback(event);
369
+ } catch (error) {
370
+ console.error("Error in auth state change callback:", error);
371
+ }
332
372
  }
333
373
  }
334
374
  };
335
375
 
376
+ // src/lib/jwt.ts
377
+ function decodeBase64Url(input) {
378
+ const normalized = input.replace(/-/g, "+").replace(/_/g, "/");
379
+ const padded = normalized.padEnd(normalized.length + (4 - normalized.length % 4) % 4, "=");
380
+ const binary = atob(padded);
381
+ const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
382
+ return new TextDecoder().decode(bytes);
383
+ }
384
+ function getJwtExpiration(token) {
385
+ if (!token) {
386
+ return null;
387
+ }
388
+ const [, payload] = token.split(".");
389
+ if (!payload) {
390
+ return null;
391
+ }
392
+ try {
393
+ const parsed = JSON.parse(decodeBase64Url(payload));
394
+ if (typeof parsed.exp !== "number" || !Number.isFinite(parsed.exp)) {
395
+ return null;
396
+ }
397
+ return new Date(parsed.exp * 1e3);
398
+ } catch {
399
+ return null;
400
+ }
401
+ }
402
+ function isJwtExpiredOrExpiring(token, leewaySeconds = 60) {
403
+ if (!token) {
404
+ return false;
405
+ }
406
+ const expires = getJwtExpiration(token);
407
+ if (!expires) {
408
+ return true;
409
+ }
410
+ return expires.getTime() <= Date.now() + leewaySeconds * 1e3;
411
+ }
412
+
336
413
  // src/lib/http-client.ts
337
414
  var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([500, 502, 503, 504]);
338
415
  var IDEMPOTENT_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
339
- var REFRESHABLE_AUTH_ERROR_CODES = /* @__PURE__ */ new Set([
340
- "AUTH_UNAUTHORIZED",
341
- "PGRST301"
342
- ]);
416
+ var REFRESHABLE_AUTH_ERROR_CODES = /* @__PURE__ */ new Set(["AUTH_UNAUTHORIZED", "PGRST301"]);
343
417
  function serializeBody(method, body, headers) {
344
- if (body === void 0) return void 0;
345
- if (method === "GET" || method === "HEAD") return void 0;
418
+ if (body === void 0) {
419
+ return void 0;
420
+ }
421
+ if (method === "GET" || method === "HEAD") {
422
+ return void 0;
423
+ }
346
424
  if (typeof FormData !== "undefined" && body instanceof FormData) {
347
425
  return body;
348
426
  }
@@ -350,7 +428,9 @@ function serializeBody(method, body, headers) {
350
428
  return JSON.stringify(body);
351
429
  }
352
430
  async function parseResponse(response) {
353
- if (response.status === 204) return void 0;
431
+ if (response.status === 204) {
432
+ return void 0;
433
+ }
354
434
  let data;
355
435
  const contentType = response.headers.get("content-type");
356
436
  try {
@@ -452,31 +532,24 @@ var HttpClient = class {
452
532
  return statusCode === 401 && REFRESHABLE_AUTH_ERROR_CODES.has(errorCode ?? "") && !this.config.isServerMode && !this.config.accessToken && !this.config.edgeFunctionToken && !options.skipAuthRefresh && authToken !== null;
453
533
  }
454
534
  async fetchWithRetry(args) {
455
- const {
456
- method,
457
- url,
458
- headers,
459
- body,
460
- fetchOptions,
461
- callerSignal,
462
- maxAttempts
463
- } = args;
535
+ const { method, url, headers, body, fetchOptions, callerSignal, maxAttempts } = args;
464
536
  let lastError;
465
537
  for (let attempt = 0; attempt <= maxAttempts; attempt++) {
466
538
  if (attempt > 0) {
467
539
  const delay = this.computeRetryDelay(attempt);
468
- this.logger.warn(
469
- `Retry ${attempt}/${maxAttempts} for ${method} ${url} in ${delay}ms`
470
- );
471
- if (callerSignal?.aborted) throw callerSignal.reason;
540
+ this.logger.warn(`Retry ${attempt}/${maxAttempts} for ${method} ${url} in ${delay}ms`);
541
+ if (callerSignal?.aborted) {
542
+ throw callerSignal.reason;
543
+ }
472
544
  await new Promise((resolve, reject) => {
473
545
  const onAbort = () => {
474
546
  clearTimeout(timer2);
475
547
  reject(callerSignal.reason);
476
548
  };
477
549
  const timer2 = setTimeout(() => {
478
- if (callerSignal)
550
+ if (callerSignal) {
479
551
  callerSignal.removeEventListener("abort", onAbort);
552
+ }
480
553
  resolve();
481
554
  }, delay);
482
555
  if (callerSignal) {
@@ -518,7 +591,9 @@ var HttpClient = class {
518
591
  ...controller ? { signal: controller.signal } : {}
519
592
  });
520
593
  if (this.isRetryableStatus(response.status) && attempt < maxAttempts) {
521
- if (timer !== void 0) clearTimeout(timer);
594
+ if (timer !== void 0) {
595
+ clearTimeout(timer);
596
+ }
522
597
  await response.body?.cancel();
523
598
  lastError = new InsForgeError(
524
599
  `Server error: ${response.status} ${response.statusText}`,
@@ -527,10 +602,14 @@ var HttpClient = class {
527
602
  );
528
603
  continue;
529
604
  }
530
- if (timer !== void 0) clearTimeout(timer);
605
+ if (timer !== void 0) {
606
+ clearTimeout(timer);
607
+ }
531
608
  return response;
532
609
  } catch (err) {
533
- if (timer !== void 0) clearTimeout(timer);
610
+ if (timer !== void 0) {
611
+ clearTimeout(timer);
612
+ }
534
613
  if (err?.name === "AbortError") {
535
614
  if (controller && controller.signal.aborted && this.timeout > 0 && !callerSignal?.aborted) {
536
615
  throw new InsForgeError(
@@ -552,11 +631,7 @@ var HttpClient = class {
552
631
  );
553
632
  }
554
633
  }
555
- throw lastError || new InsForgeError(
556
- "Request failed after all retry attempts",
557
- 0,
558
- "NETWORK_ERROR"
559
- );
634
+ throw lastError || new InsForgeError("Request failed after all retry attempts", 0, "NETWORK_ERROR");
560
635
  }
561
636
  /**
562
637
  * Performs an HTTP request with automatic retry and timeout handling.
@@ -636,31 +711,15 @@ var HttpClient = class {
636
711
  }
637
712
  throw err;
638
713
  }
639
- this.logger.logResponse(
640
- method,
641
- url,
642
- response.status,
643
- Date.now() - startTime,
644
- data
645
- );
714
+ this.logger.logResponse(method, url, response.status, Date.now() - startTime, data);
646
715
  return data;
647
716
  }
648
717
  async request(method, path, options = {}) {
649
718
  const tokenUsed = this.userToken;
650
719
  try {
651
- return await this.handleRequest(
652
- method,
653
- path,
654
- { ...options },
655
- tokenUsed
656
- );
720
+ return await this.handleRequest(method, path, { ...options }, tokenUsed);
657
721
  } catch (error) {
658
- if (!(error instanceof InsForgeError) || !this.shouldRefreshAccessToken(
659
- error.statusCode,
660
- error.error,
661
- tokenUsed,
662
- options
663
- )) {
722
+ if (!(error instanceof InsForgeError) || !this.shouldRefreshAccessToken(error.statusCode, error.error, tokenUsed, options)) {
664
723
  throw error;
665
724
  }
666
725
  if (tokenUsed !== this.userToken) {
@@ -745,12 +804,7 @@ var HttpClient = class {
745
804
  callerSignal,
746
805
  maxAttempts
747
806
  });
748
- this.logger.logResponse(
749
- method,
750
- url,
751
- response.status,
752
- Date.now() - startTime
753
- );
807
+ this.logger.logResponse(method, url, response.status, Date.now() - startTime);
754
808
  let errorCode = null;
755
809
  if (response.status === 401) {
756
810
  try {
@@ -764,12 +818,7 @@ var HttpClient = class {
764
818
  } catch {
765
819
  }
766
820
  }
767
- if (!this.shouldRefreshAccessToken(
768
- response.status,
769
- errorCode,
770
- tokenUsed,
771
- options
772
- )) {
821
+ if (!this.shouldRefreshAccessToken(response.status, errorCode, tokenUsed, options)) {
773
822
  return response;
774
823
  }
775
824
  if (tokenUsed !== this.userToken) {
@@ -863,10 +912,30 @@ var HttpClient = class {
863
912
  })();
864
913
  return this.refreshPromise;
865
914
  }
915
+ /** Returns a token safe to use for a new connection handshake. */
916
+ async getValidAccessToken(leewaySeconds = 60) {
917
+ const accessToken = this.tokenManager.getAccessToken() ?? this.userToken;
918
+ if (!accessToken || !isJwtExpiredOrExpiring(accessToken, leewaySeconds)) {
919
+ return accessToken;
920
+ }
921
+ const canRefresh = !this.config.isServerMode && !this.config.accessToken && !this.config.edgeFunctionToken && this.userToken !== null;
922
+ if (!canRefresh) {
923
+ return accessToken;
924
+ }
925
+ try {
926
+ const refreshed = await this.refreshAndSaveSession();
927
+ return refreshed.accessToken;
928
+ } catch (error) {
929
+ if (error instanceof InsForgeError && (error.statusCode === 401 || error.statusCode === 403) && this.userToken === accessToken) {
930
+ this.clearAuthSession();
931
+ }
932
+ throw error;
933
+ }
934
+ }
866
935
  async refreshAndSaveSession() {
867
936
  const newTokenData = await this.refreshAccessToken();
868
937
  this.setAuthToken(newTokenData.accessToken);
869
- this.tokenManager.saveSession(newTokenData);
938
+ this.tokenManager.saveSession(newTokenData, AuthChangeEvent.TOKEN_REFRESHED);
870
939
  if (newTokenData.csrfToken) {
871
940
  setCsrfToken(newTokenData.csrfToken);
872
941
  }
@@ -962,11 +1031,15 @@ var Auth = class {
962
1031
  isServerMode() {
963
1032
  return !!this.options.isServerMode;
964
1033
  }
1034
+ /** Subscribe to SDK authentication state changes. */
1035
+ onAuthStateChange(callback) {
1036
+ return this.tokenManager.onAuthStateChange(callback);
1037
+ }
965
1038
  /**
966
1039
  * Save session from API response
967
1040
  * Handles token storage, CSRF token, and HTTP auth header
968
1041
  */
969
- saveSessionFromResponse(response) {
1042
+ saveSessionFromResponse(response, event = AuthChangeEvent.SIGNED_IN) {
970
1043
  if (!response.accessToken || !response.user) {
971
1044
  return false;
972
1045
  }
@@ -978,7 +1051,7 @@ var Auth = class {
978
1051
  setCsrfToken(response.csrfToken);
979
1052
  }
980
1053
  if (!this.isServerMode()) {
981
- this.tokenManager.saveSession(session);
1054
+ this.tokenManager.saveSession(session, event);
982
1055
  }
983
1056
  this.http.setAuthToken(response.accessToken);
984
1057
  this.http.setRefreshToken(response.refreshToken ?? null);
@@ -992,7 +1065,9 @@ var Auth = class {
992
1065
  * Supports PKCE flow (insforge_code)
993
1066
  */
994
1067
  async detectAuthCallback() {
995
- if (this.isServerMode() || typeof window === "undefined") return;
1068
+ if (this.isServerMode() || typeof window === "undefined") {
1069
+ return;
1070
+ }
996
1071
  try {
997
1072
  const params = new URLSearchParams(window.location.search);
998
1073
  const error = params.get("error");
@@ -1054,10 +1129,16 @@ var Auth = class {
1054
1129
  async signOut() {
1055
1130
  try {
1056
1131
  try {
1132
+ const serverMode = this.isServerMode();
1133
+ const csrfToken = !serverMode ? getCsrfToken() : null;
1057
1134
  await this.http.post(
1058
- this.isServerMode() ? "/api/auth/logout?client_type=mobile" : "/api/auth/logout",
1135
+ serverMode ? "/api/auth/logout?client_type=mobile" : "/api/auth/logout",
1059
1136
  void 0,
1060
- { credentials: "include", skipAuthRefresh: true }
1137
+ {
1138
+ credentials: "include",
1139
+ skipAuthRefresh: true,
1140
+ ...csrfToken ? { headers: { "X-CSRF-Token": csrfToken } } : {}
1141
+ }
1061
1142
  );
1062
1143
  } catch {
1063
1144
  }
@@ -1094,11 +1175,7 @@ var Auth = class {
1094
1175
  if (!signInOptions || !signInOptions.redirectTo) {
1095
1176
  return {
1096
1177
  data: {},
1097
- error: new InsForgeError(
1098
- "Redirect URI is required",
1099
- 400,
1100
- import_shared_schemas.ERROR_CODES.INVALID_INPUT
1101
- )
1178
+ error: new InsForgeError("Redirect URI is required", 400, import_shared_schemas.ERROR_CODES.INVALID_INPUT)
1102
1179
  };
1103
1180
  }
1104
1181
  const { provider } = signInOptions;
@@ -1173,10 +1250,7 @@ var Auth = class {
1173
1250
  error: null
1174
1251
  };
1175
1252
  } catch (error) {
1176
- return wrapError(
1177
- error,
1178
- "An unexpected error occurred during OAuth code exchange"
1179
- );
1253
+ return wrapError(error, "An unexpected error occurred during OAuth code exchange");
1180
1254
  }
1181
1255
  }
1182
1256
  /**
@@ -1203,10 +1277,7 @@ var Auth = class {
1203
1277
  error: null
1204
1278
  };
1205
1279
  } catch (error) {
1206
- return wrapError(
1207
- error,
1208
- "An unexpected error occurred during ID token sign in"
1209
- );
1280
+ return wrapError(error, "An unexpected error occurred during ID token sign in");
1210
1281
  }
1211
1282
  }
1212
1283
  // ============================================================================
@@ -1247,14 +1318,11 @@ var Auth = class {
1247
1318
  }
1248
1319
  );
1249
1320
  if (response.accessToken) {
1250
- this.saveSessionFromResponse(response);
1321
+ this.saveSessionFromResponse(response, AuthChangeEvent.TOKEN_REFRESHED);
1251
1322
  }
1252
1323
  return { data: response, error: null };
1253
1324
  } catch (error) {
1254
- return wrapError(
1255
- error,
1256
- "An unexpected error occurred during session refresh"
1257
- );
1325
+ return wrapError(error, "An unexpected error occurred during session refresh");
1258
1326
  }
1259
1327
  }
1260
1328
  /**
@@ -1265,11 +1333,11 @@ var Auth = class {
1265
1333
  try {
1266
1334
  if (this.isServerMode()) {
1267
1335
  const accessToken = this.tokenManager.getAccessToken();
1268
- if (!accessToken) return { data: { user: null }, error: null };
1336
+ if (!accessToken) {
1337
+ return { data: { user: null }, error: null };
1338
+ }
1269
1339
  this.http.setAuthToken(accessToken);
1270
- const response = await this.http.get(
1271
- "/api/auth/sessions/current"
1272
- );
1340
+ const response = await this.http.get("/api/auth/sessions/current");
1273
1341
  const user = response.user ?? null;
1274
1342
  return { data: { user }, error: null };
1275
1343
  }
@@ -1307,25 +1375,17 @@ var Auth = class {
1307
1375
  // ============================================================================
1308
1376
  async getProfile(userId) {
1309
1377
  try {
1310
- const response = await this.http.get(
1311
- `/api/auth/profiles/${userId}`
1312
- );
1378
+ const response = await this.http.get(`/api/auth/profiles/${userId}`);
1313
1379
  return { data: response, error: null };
1314
1380
  } catch (error) {
1315
- return wrapError(
1316
- error,
1317
- "An unexpected error occurred while fetching user profile"
1318
- );
1381
+ return wrapError(error, "An unexpected error occurred while fetching user profile");
1319
1382
  }
1320
1383
  }
1321
1384
  async setProfile(profile) {
1322
1385
  try {
1323
- const response = await this.http.patch(
1324
- "/api/auth/profiles/current",
1325
- {
1326
- profile
1327
- }
1328
- );
1386
+ const response = await this.http.patch("/api/auth/profiles/current", {
1387
+ profile
1388
+ });
1329
1389
  const currentUser = this.tokenManager.getUser();
1330
1390
  if (!this.isServerMode() && currentUser && response.profile !== void 0) {
1331
1391
  this.tokenManager.setUser({
@@ -1335,10 +1395,7 @@ var Auth = class {
1335
1395
  }
1336
1396
  return { data: response, error: null };
1337
1397
  } catch (error) {
1338
- return wrapError(
1339
- error,
1340
- "An unexpected error occurred while updating user profile"
1341
- );
1398
+ return wrapError(error, "An unexpected error occurred while updating user profile");
1342
1399
  }
1343
1400
  }
1344
1401
  // ============================================================================
@@ -1351,10 +1408,7 @@ var Auth = class {
1351
1408
  });
1352
1409
  return { data: response, error: null };
1353
1410
  } catch (error) {
1354
- return wrapError(
1355
- error,
1356
- "An unexpected error occurred while sending verification email"
1357
- );
1411
+ return wrapError(error, "An unexpected error occurred while sending verification email");
1358
1412
  }
1359
1413
  }
1360
1414
  async verifyEmail(request) {
@@ -1370,10 +1424,7 @@ var Auth = class {
1370
1424
  }
1371
1425
  return { data: response, error: null };
1372
1426
  } catch (error) {
1373
- return wrapError(
1374
- error,
1375
- "An unexpected error occurred while verifying email"
1376
- );
1427
+ return wrapError(error, "An unexpected error occurred while verifying email");
1377
1428
  }
1378
1429
  }
1379
1430
  // ============================================================================
@@ -1386,10 +1437,7 @@ var Auth = class {
1386
1437
  });
1387
1438
  return { data: response, error: null };
1388
1439
  } catch (error) {
1389
- return wrapError(
1390
- error,
1391
- "An unexpected error occurred while sending password reset email"
1392
- );
1440
+ return wrapError(error, "An unexpected error occurred while sending password reset email");
1393
1441
  }
1394
1442
  }
1395
1443
  async exchangeResetPasswordToken(request) {
@@ -1401,10 +1449,7 @@ var Auth = class {
1401
1449
  );
1402
1450
  return { data: response, error: null };
1403
1451
  } catch (error) {
1404
- return wrapError(
1405
- error,
1406
- "An unexpected error occurred while verifying reset code"
1407
- );
1452
+ return wrapError(error, "An unexpected error occurred while verifying reset code");
1408
1453
  }
1409
1454
  }
1410
1455
  async resetPassword(request) {
@@ -1416,10 +1461,7 @@ var Auth = class {
1416
1461
  );
1417
1462
  return { data: response, error: null };
1418
1463
  } catch (error) {
1419
- return wrapError(
1420
- error,
1421
- "An unexpected error occurred while resetting password"
1422
- );
1464
+ return wrapError(error, "An unexpected error occurred while resetting password");
1423
1465
  }
1424
1466
  }
1425
1467
  // ============================================================================
@@ -1427,16 +1469,12 @@ var Auth = class {
1427
1469
  // ============================================================================
1428
1470
  async getPublicAuthConfig() {
1429
1471
  try {
1430
- const response = await this.http.get(
1431
- "/api/auth/public-config",
1432
- { skipAuthRefresh: true }
1433
- );
1472
+ const response = await this.http.get("/api/auth/public-config", {
1473
+ skipAuthRefresh: true
1474
+ });
1434
1475
  return { data: response, error: null };
1435
1476
  } catch (error) {
1436
- return wrapError(
1437
- error,
1438
- "An unexpected error occurred while fetching auth configuration"
1439
- );
1477
+ return wrapError(error, "An unexpected error occurred while fetching auth configuration");
1440
1478
  }
1441
1479
  }
1442
1480
  };
@@ -1463,12 +1501,30 @@ function createInsForgePostgrestFetch(httpClient) {
1463
1501
  };
1464
1502
  }
1465
1503
  var Database = class {
1466
- constructor(httpClient) {
1504
+ constructor(httpClient, defaultSchema) {
1467
1505
  this.postgrest = new import_postgrest_js.PostgrestClient("http://dummy", {
1468
1506
  fetch: createInsForgePostgrestFetch(httpClient),
1469
- headers: {}
1507
+ headers: {},
1508
+ ...defaultSchema ? { schema: defaultSchema } : {}
1470
1509
  });
1471
1510
  }
1511
+ /**
1512
+ * Select a non-default Postgres schema for the chained query. Maps to
1513
+ * PostgREST's `Accept-Profile` (reads) / `Content-Profile` (writes) header.
1514
+ * The schema must be exposed by the backend.
1515
+ *
1516
+ * @example
1517
+ * const { data } = await client.database
1518
+ * .schema('analytics')
1519
+ * .from('events')
1520
+ * .select('*');
1521
+ *
1522
+ * @example
1523
+ * await client.database.schema('analytics').rpc('rollup', { day: '2026-01-01' });
1524
+ */
1525
+ schema(schemaName) {
1526
+ return this.postgrest.schema(schemaName);
1527
+ }
1472
1528
  /**
1473
1529
  * Create a query builder for a table
1474
1530
  *
@@ -1576,11 +1632,7 @@ var StorageBucket = class {
1576
1632
  } catch (error) {
1577
1633
  return {
1578
1634
  data: null,
1579
- error: error instanceof InsForgeError ? error : new InsForgeError(
1580
- "Upload failed",
1581
- 500,
1582
- "STORAGE_ERROR"
1583
- )
1635
+ error: error instanceof InsForgeError ? error : new InsForgeError("Upload failed", 500, "STORAGE_ERROR")
1584
1636
  };
1585
1637
  }
1586
1638
  }
@@ -1626,11 +1678,7 @@ var StorageBucket = class {
1626
1678
  } catch (error) {
1627
1679
  return {
1628
1680
  data: null,
1629
- error: error instanceof InsForgeError ? error : new InsForgeError(
1630
- "Upload failed",
1631
- 500,
1632
- "STORAGE_ERROR"
1633
- )
1681
+ error: error instanceof InsForgeError ? error : new InsForgeError("Upload failed", 500, "STORAGE_ERROR")
1634
1682
  };
1635
1683
  }
1636
1684
  }
@@ -1658,13 +1706,10 @@ var StorageBucket = class {
1658
1706
  );
1659
1707
  }
1660
1708
  if (strategy.confirmRequired && strategy.confirmUrl) {
1661
- const confirmResponse = await this.http.post(
1662
- strategy.confirmUrl,
1663
- {
1664
- size: file.size,
1665
- contentType: file.type || "application/octet-stream"
1666
- }
1667
- );
1709
+ const confirmResponse = await this.http.post(strategy.confirmUrl, {
1710
+ size: file.size,
1711
+ contentType: file.type || "application/octet-stream"
1712
+ });
1668
1713
  return { data: confirmResponse, error: null };
1669
1714
  }
1670
1715
  return {
@@ -1679,11 +1724,7 @@ var StorageBucket = class {
1679
1724
  error: null
1680
1725
  };
1681
1726
  } catch (error) {
1682
- throw error instanceof InsForgeError ? error : new InsForgeError(
1683
- "Presigned upload failed",
1684
- 500,
1685
- "STORAGE_ERROR"
1686
- );
1727
+ throw error instanceof InsForgeError ? error : new InsForgeError("Presigned upload failed", 500, "STORAGE_ERROR");
1687
1728
  }
1688
1729
  }
1689
1730
  /**
@@ -1737,11 +1778,7 @@ var StorageBucket = class {
1737
1778
  } catch (error) {
1738
1779
  return {
1739
1780
  data: null,
1740
- error: error instanceof InsForgeError ? error : new InsForgeError(
1741
- "Download failed",
1742
- 500,
1743
- "STORAGE_ERROR"
1744
- )
1781
+ error: error instanceof InsForgeError ? error : new InsForgeError("Download failed", 500, "STORAGE_ERROR")
1745
1782
  };
1746
1783
  }
1747
1784
  }
@@ -1776,7 +1813,9 @@ var StorageBucket = class {
1776
1813
  } catch (error) {
1777
1814
  const status = error instanceof InsForgeError ? error.statusCode : void 0;
1778
1815
  const isMissingRoute = (status === 404 || status === 405) && !(error instanceof InsForgeError && error.error === "STORAGE_NOT_FOUND");
1779
- if (!isMissingRoute) throw error;
1816
+ if (!isMissingRoute) {
1817
+ throw error;
1818
+ }
1780
1819
  return await this.http.post(
1781
1820
  `/api/storage/buckets/${this.bucketName}/objects/${encoded}/download-strategy`,
1782
1821
  { expiresIn }
@@ -1852,10 +1891,18 @@ var StorageBucket = class {
1852
1891
  async list(options) {
1853
1892
  try {
1854
1893
  const params = {};
1855
- if (options?.prefix) params.prefix = options.prefix;
1856
- if (options?.search) params.search = options.search;
1857
- if (options?.limit) params.limit = options.limit.toString();
1858
- if (options?.offset) params.offset = options.offset.toString();
1894
+ if (options?.prefix) {
1895
+ params.prefix = options.prefix;
1896
+ }
1897
+ if (options?.search) {
1898
+ params.search = options.search;
1899
+ }
1900
+ if (options?.limit) {
1901
+ params.limit = options.limit.toString();
1902
+ }
1903
+ if (options?.offset) {
1904
+ params.offset = options.offset.toString();
1905
+ }
1859
1906
  const response = await this.http.get(
1860
1907
  `/api/storage/buckets/${this.bucketName}/objects`,
1861
1908
  { params }
@@ -1864,11 +1911,7 @@ var StorageBucket = class {
1864
1911
  } catch (error) {
1865
1912
  return {
1866
1913
  data: null,
1867
- error: error instanceof InsForgeError ? error : new InsForgeError(
1868
- "List failed",
1869
- 500,
1870
- "STORAGE_ERROR"
1871
- )
1914
+ error: error instanceof InsForgeError ? error : new InsForgeError("List failed", 500, "STORAGE_ERROR")
1872
1915
  };
1873
1916
  }
1874
1917
  }
@@ -1885,11 +1928,7 @@ var StorageBucket = class {
1885
1928
  } catch (error) {
1886
1929
  return {
1887
1930
  data: null,
1888
- error: error instanceof InsForgeError ? error : new InsForgeError(
1889
- "Delete failed",
1890
- 500,
1891
- "STORAGE_ERROR"
1892
- )
1931
+ error: error instanceof InsForgeError ? error : new InsForgeError("Delete failed", 500, "STORAGE_ERROR")
1893
1932
  };
1894
1933
  }
1895
1934
  }
@@ -2011,14 +2050,11 @@ var ChatCompletions = class {
2011
2050
  if (params.stream) {
2012
2051
  const headers = this.http.getHeaders();
2013
2052
  headers["Content-Type"] = "application/json";
2014
- const response2 = await this.http.fetch(
2015
- `${this.http.baseUrl}/api/ai/chat/completion`,
2016
- {
2017
- method: "POST",
2018
- headers,
2019
- body: JSON.stringify(backendParams)
2020
- }
2021
- );
2053
+ const response2 = await this.http.fetch(`${this.http.baseUrl}/api/ai/chat/completion`, {
2054
+ method: "POST",
2055
+ headers,
2056
+ body: JSON.stringify(backendParams)
2057
+ });
2022
2058
  if (!response2.ok) {
2023
2059
  const error = await response2.json();
2024
2060
  throw new Error(error.error || "Stream request failed");
@@ -2066,7 +2102,9 @@ var ChatCompletions = class {
2066
2102
  try {
2067
2103
  while (true) {
2068
2104
  const { done, value } = await reader.read();
2069
- if (done) break;
2105
+ if (done) {
2106
+ break;
2107
+ }
2070
2108
  buffer += decoder.decode(value, { stream: true });
2071
2109
  const lines = buffer.split("\n");
2072
2110
  buffer = lines.pop() || "";
@@ -2114,7 +2152,7 @@ var ChatCompletions = class {
2114
2152
  reader.releaseLock();
2115
2153
  return;
2116
2154
  }
2117
- } catch (e) {
2155
+ } catch {
2118
2156
  console.warn("Failed to parse SSE data:", dataStr);
2119
2157
  }
2120
2158
  }
@@ -2167,10 +2205,7 @@ var Embeddings = class {
2167
2205
  * ```
2168
2206
  */
2169
2207
  async create(params) {
2170
- const response = await this.http.post(
2171
- "/api/ai/embeddings",
2172
- params
2173
- );
2208
+ const response = await this.http.post("/api/ai/embeddings", params);
2174
2209
  return {
2175
2210
  object: response.object,
2176
2211
  data: response.data,
@@ -2256,7 +2291,9 @@ var Functions = class _Functions {
2256
2291
  static deriveSubhostingUrl(baseUrl) {
2257
2292
  try {
2258
2293
  const { hostname } = new URL(baseUrl);
2259
- if (!hostname.endsWith(".insforge.app")) return void 0;
2294
+ if (!hostname.endsWith(".insforge.app")) {
2295
+ return void 0;
2296
+ }
2260
2297
  const appKey = hostname.split(".")[0];
2261
2298
  return `https://${appKey}.functions.insforge.app`;
2262
2299
  } catch {
@@ -2302,7 +2339,9 @@ var Functions = class _Functions {
2302
2339
  const data = await parseResponse(res);
2303
2340
  return { data, error: null };
2304
2341
  } catch (error) {
2305
- if (error instanceof Error && error.name === "AbortError") throw error;
2342
+ if (error instanceof Error && error.name === "AbortError") {
2343
+ throw error;
2344
+ }
2306
2345
  return {
2307
2346
  data: null,
2308
2347
  error: error instanceof InsForgeError ? error : new InsForgeError(
@@ -2321,7 +2360,9 @@ var Functions = class _Functions {
2321
2360
  });
2322
2361
  return { data, error: null };
2323
2362
  } catch (error) {
2324
- if (error instanceof Error && error.name === "AbortError") throw error;
2363
+ if (error instanceof Error && error.name === "AbortError") {
2364
+ throw error;
2365
+ }
2325
2366
  if (error instanceof InsForgeError && error.statusCode === 404) {
2326
2367
  } else {
2327
2368
  return {
@@ -2340,7 +2381,9 @@ var Functions = class _Functions {
2340
2381
  const data = await this.http.request(method, path, { body, headers });
2341
2382
  return { data, error: null };
2342
2383
  } catch (error) {
2343
- if (error instanceof Error && error.name === "AbortError") throw error;
2384
+ if (error instanceof Error && error.name === "AbortError") {
2385
+ throw error;
2386
+ }
2344
2387
  return {
2345
2388
  data: null,
2346
2389
  error: error instanceof InsForgeError ? error : new InsForgeError(
@@ -2355,32 +2398,37 @@ var Functions = class _Functions {
2355
2398
 
2356
2399
  // src/modules/realtime.ts
2357
2400
  var CONNECT_TIMEOUT = 1e4;
2401
+ var SUBSCRIBE_TIMEOUT = 1e4;
2358
2402
  var Realtime = class {
2359
- constructor(baseUrl, tokenManager, anonKey) {
2360
- this.socket = null;
2361
- this.connectPromise = null;
2362
- this.subscribedChannels = /* @__PURE__ */ new Set();
2363
- this.eventListeners = /* @__PURE__ */ new Map();
2403
+ constructor(baseUrl, tokenManager, anonKey, getValidAccessToken = async () => tokenManager.getAccessToken()) {
2364
2404
  this.baseUrl = baseUrl;
2365
2405
  this.tokenManager = tokenManager;
2366
2406
  this.anonKey = anonKey;
2367
- this.tokenManager.onTokenChange = () => this.onTokenChange();
2407
+ this.getValidAccessToken = getValidAccessToken;
2408
+ this.socket = null;
2409
+ this.connectPromise = null;
2410
+ this.connectionAttempt = null;
2411
+ this.nextConnectionAttemptId = 0;
2412
+ this.subscriptions = /* @__PURE__ */ new Map();
2413
+ this.eventListeners = /* @__PURE__ */ new Map();
2414
+ this.tokenManager.onAuthStateChange((event) => {
2415
+ if (event !== AuthChangeEvent.TOKEN_REFRESHED) {
2416
+ this.reconnectForAuthChange();
2417
+ }
2418
+ });
2368
2419
  }
2369
2420
  notifyListeners(event, payload) {
2370
- const listeners = this.eventListeners.get(event);
2371
- if (!listeners) return;
2372
- for (const cb of listeners) {
2421
+ for (const callback of this.eventListeners.get(event) ?? []) {
2373
2422
  try {
2374
- cb(payload);
2375
- } catch (err) {
2376
- console.error(`Error in ${event} callback:`, err);
2423
+ callback(payload);
2424
+ } catch (error) {
2425
+ console.error(`Error in ${event} callback:`, error);
2377
2426
  }
2378
2427
  }
2379
2428
  }
2380
- /**
2381
- * Connect to the realtime server
2382
- * @returns Promise that resolves when connected
2383
- */
2429
+ async getHandshakeToken() {
2430
+ return await this.getValidAccessToken() ?? this.anonKey ?? null;
2431
+ }
2384
2432
  connect() {
2385
2433
  if (this.socket?.connected) {
2386
2434
  return Promise.resolve();
@@ -2388,210 +2436,324 @@ var Realtime = class {
2388
2436
  if (this.connectPromise) {
2389
2437
  return this.connectPromise;
2390
2438
  }
2391
- this.connectPromise = (async () => {
2392
- try {
2393
- const { io } = await import("socket.io-client");
2394
- await new Promise((resolve, reject) => {
2395
- const token = this.tokenManager.getAccessToken() ?? this.anonKey;
2396
- this.socket = io(this.baseUrl, {
2397
- transports: ["websocket"],
2398
- auth: token ? { token } : void 0
2399
- });
2400
- let initialConnection = true;
2401
- let timeoutId = null;
2402
- const cleanup = () => {
2403
- if (timeoutId) {
2404
- clearTimeout(timeoutId);
2405
- timeoutId = null;
2406
- }
2407
- };
2408
- timeoutId = setTimeout(() => {
2409
- if (initialConnection) {
2410
- initialConnection = false;
2411
- this.connectPromise = null;
2412
- this.socket?.disconnect();
2413
- this.socket = null;
2414
- reject(new Error(`Connection timeout after ${CONNECT_TIMEOUT}ms`));
2415
- }
2416
- }, CONNECT_TIMEOUT);
2417
- this.socket.on("connect", () => {
2418
- cleanup();
2419
- for (const channel of this.subscribedChannels) {
2420
- this.socket.emit("realtime:subscribe", { channel });
2421
- }
2422
- this.notifyListeners("connect");
2423
- if (initialConnection) {
2424
- initialConnection = false;
2425
- this.connectPromise = null;
2426
- resolve();
2427
- }
2428
- });
2429
- this.socket.on("connect_error", (error) => {
2430
- cleanup();
2431
- this.notifyListeners("connect_error", error);
2432
- if (initialConnection) {
2433
- initialConnection = false;
2434
- this.connectPromise = null;
2435
- reject(error);
2436
- }
2437
- });
2438
- this.socket.on("disconnect", (reason) => {
2439
- this.notifyListeners("disconnect", reason);
2440
- });
2441
- this.socket.on("realtime:error", (error) => {
2442
- this.notifyListeners("error", error);
2443
- });
2444
- this.socket.onAny((event, message) => {
2445
- if (event === "realtime:error") return;
2446
- this.notifyListeners(event, message);
2447
- });
2439
+ const attemptId = ++this.nextConnectionAttemptId;
2440
+ const connection = (async () => {
2441
+ const { io } = await import("socket.io-client");
2442
+ if (attemptId !== this.nextConnectionAttemptId) {
2443
+ throw new Error("Connection cancelled");
2444
+ }
2445
+ await new Promise((resolve, reject) => {
2446
+ const socket = io(this.baseUrl, {
2447
+ transports: ["websocket"],
2448
+ auth: (callback) => {
2449
+ void this.getHandshakeToken().then(
2450
+ (token) => callback(token ? { token } : {}),
2451
+ () => callback({})
2452
+ );
2453
+ }
2448
2454
  });
2449
- } catch (error) {
2455
+ this.socket = socket;
2456
+ let initialConnection = true;
2457
+ let timeoutId = null;
2458
+ const clearConnectTimeout = () => {
2459
+ if (timeoutId) {
2460
+ clearTimeout(timeoutId);
2461
+ timeoutId = null;
2462
+ }
2463
+ };
2464
+ const dispose = () => {
2465
+ clearConnectTimeout();
2466
+ socket.off("connect", onConnect);
2467
+ socket.off("connect_error", onConnectError);
2468
+ socket.off("disconnect", onDisconnect);
2469
+ socket.off("realtime:error", onRealtimeError);
2470
+ socket.offAny(onAny);
2471
+ socket.disconnect();
2472
+ if (this.socket === socket) {
2473
+ this.socket = null;
2474
+ }
2475
+ if (this.connectionAttempt?.id === attemptId) {
2476
+ this.connectionAttempt = null;
2477
+ }
2478
+ };
2479
+ const fail = (error) => {
2480
+ if (!initialConnection) {
2481
+ return;
2482
+ }
2483
+ initialConnection = false;
2484
+ dispose();
2485
+ reject(error);
2486
+ };
2487
+ const onConnect = () => {
2488
+ if (this.socket !== socket) {
2489
+ return;
2490
+ }
2491
+ clearConnectTimeout();
2492
+ this.resubscribeChannels();
2493
+ this.notifyListeners("connect");
2494
+ if (initialConnection) {
2495
+ initialConnection = false;
2496
+ if (this.connectionAttempt?.id === attemptId) {
2497
+ this.connectionAttempt = null;
2498
+ }
2499
+ resolve();
2500
+ }
2501
+ };
2502
+ const onConnectError = (error) => {
2503
+ clearConnectTimeout();
2504
+ this.notifyListeners("connect_error", error);
2505
+ if (initialConnection) {
2506
+ fail(error);
2507
+ }
2508
+ };
2509
+ const onDisconnect = (reason) => {
2510
+ this.handleDisconnect(reason);
2511
+ };
2512
+ const onRealtimeError = (error) => {
2513
+ this.notifyListeners("error", error);
2514
+ };
2515
+ const onAny = (event, message) => {
2516
+ if (event === "realtime:error") {
2517
+ return;
2518
+ }
2519
+ this.applyPresenceEvent(event, message);
2520
+ this.notifyListeners(event, message);
2521
+ };
2522
+ this.connectionAttempt = { id: attemptId, socket, cancel: fail };
2523
+ socket.on("connect", onConnect);
2524
+ socket.on("connect_error", onConnectError);
2525
+ socket.on("disconnect", onDisconnect);
2526
+ socket.on("realtime:error", onRealtimeError);
2527
+ socket.onAny(onAny);
2528
+ timeoutId = setTimeout(
2529
+ () => fail(new Error(`Connection timeout after ${CONNECT_TIMEOUT}ms`)),
2530
+ CONNECT_TIMEOUT
2531
+ );
2532
+ });
2533
+ })();
2534
+ const trackedConnection = connection.finally(() => {
2535
+ if (this.connectPromise === trackedConnection) {
2450
2536
  this.connectPromise = null;
2451
- throw error;
2452
2537
  }
2453
- })();
2454
- return this.connectPromise;
2538
+ });
2539
+ this.connectPromise = trackedConnection;
2540
+ return trackedConnection;
2455
2541
  }
2456
- /**
2457
- * Disconnect from the realtime server
2458
- */
2459
2542
  disconnect() {
2460
- if (this.socket) {
2461
- this.socket.disconnect();
2462
- this.socket = null;
2543
+ this.nextConnectionAttemptId++;
2544
+ this.connectionAttempt?.cancel(new Error("Disconnected"));
2545
+ this.socket?.disconnect();
2546
+ this.socket = null;
2547
+ this.connectPromise = null;
2548
+ for (const subscription of this.subscriptions.values()) {
2549
+ this.settleSubscription(
2550
+ subscription,
2551
+ {
2552
+ ok: false,
2553
+ channel: subscription.channel,
2554
+ error: { code: "DISCONNECTED", message: "Disconnected" }
2555
+ },
2556
+ false
2557
+ );
2463
2558
  }
2464
- this.subscribedChannels.clear();
2559
+ this.subscriptions.clear();
2465
2560
  }
2466
- /**
2467
- * Handle token changes (e.g., after auth refresh)
2468
- * Updates socket auth so reconnects use the new token
2469
- * If connected, triggers reconnect to apply new token immediately
2470
- */
2471
- onTokenChange() {
2472
- const token = this.tokenManager.getAccessToken() ?? this.anonKey;
2473
- if (this.socket) {
2474
- this.socket.auth = token ? { token } : {};
2561
+ reconnectForAuthChange() {
2562
+ for (const subscription of this.subscriptions.values()) {
2563
+ if (subscription.status === "rejected") {
2564
+ subscription.status = "pending";
2565
+ }
2475
2566
  }
2476
- if (this.socket && (this.socket.connected || this.connectPromise)) {
2477
- this.socket.disconnect();
2478
- this.socket.connect();
2567
+ if (!this.socket) {
2568
+ return;
2569
+ }
2570
+ this.socket.disconnect();
2571
+ this.socket.connect();
2572
+ }
2573
+ handleDisconnect(reason) {
2574
+ for (const subscription of this.subscriptions.values()) {
2575
+ if (subscription.status === "rejected") {
2576
+ continue;
2577
+ }
2578
+ subscription.status = "pending";
2579
+ this.settleSubscription(
2580
+ subscription,
2581
+ {
2582
+ ok: false,
2583
+ channel: subscription.channel,
2584
+ error: { code: "DISCONNECTED", message: "Connection lost before subscription completed" }
2585
+ },
2586
+ true
2587
+ );
2588
+ }
2589
+ this.notifyListeners("disconnect", reason);
2590
+ }
2591
+ resubscribeChannels() {
2592
+ for (const [channel, subscription] of this.subscriptions) {
2593
+ if (subscription.status === "pending") {
2594
+ this.requestSubscription(channel, subscription);
2595
+ }
2596
+ }
2597
+ }
2598
+ requestSubscription(channel, subscription) {
2599
+ if (subscription.pending) {
2600
+ return subscription.pending;
2601
+ }
2602
+ const socket = this.socket;
2603
+ if (!socket?.connected) {
2604
+ return Promise.resolve({
2605
+ ok: false,
2606
+ channel,
2607
+ error: { code: "CONNECTION_FAILED", message: "Not connected to realtime server" }
2608
+ });
2609
+ }
2610
+ subscription.status = "pending";
2611
+ const epoch = ++subscription.epoch;
2612
+ let timeoutId;
2613
+ subscription.pending = new Promise((resolve) => {
2614
+ subscription.settlePending = (response) => {
2615
+ if (timeoutId) {
2616
+ clearTimeout(timeoutId);
2617
+ }
2618
+ subscription.pending = void 0;
2619
+ subscription.settlePending = void 0;
2620
+ resolve(response);
2621
+ };
2622
+ timeoutId = setTimeout(() => {
2623
+ if (this.subscriptions.get(channel) === subscription && subscription.epoch === epoch) {
2624
+ this.settleSubscription(
2625
+ subscription,
2626
+ {
2627
+ ok: false,
2628
+ channel,
2629
+ error: {
2630
+ code: "SUBSCRIBE_TIMEOUT",
2631
+ message: "Subscription acknowledgement timed out"
2632
+ }
2633
+ },
2634
+ true
2635
+ );
2636
+ }
2637
+ }, SUBSCRIBE_TIMEOUT);
2638
+ socket.emit("realtime:subscribe", { channel }, (response) => {
2639
+ if (this.subscriptions.get(channel) !== subscription || subscription.epoch !== epoch) {
2640
+ return;
2641
+ }
2642
+ if (response.ok) {
2643
+ subscription.status = "subscribed";
2644
+ subscription.members = new Map(
2645
+ response.presence.members.map((member) => [member.presenceId, member])
2646
+ );
2647
+ } else {
2648
+ subscription.status = "rejected";
2649
+ subscription.members.clear();
2650
+ }
2651
+ this.settleSubscription(subscription, response, false);
2652
+ });
2653
+ });
2654
+ return subscription.pending;
2655
+ }
2656
+ settleSubscription(subscription, response, incrementEpoch) {
2657
+ if (incrementEpoch) {
2658
+ subscription.epoch++;
2659
+ }
2660
+ subscription.settlePending?.(response);
2661
+ }
2662
+ applyPresenceEvent(event, message) {
2663
+ if (event !== "presence:join" && event !== "presence:leave") {
2664
+ return;
2665
+ }
2666
+ const presenceEvent = message;
2667
+ const channel = presenceEvent.meta?.channel;
2668
+ const member = presenceEvent.member;
2669
+ if (!channel || !member) {
2670
+ return;
2671
+ }
2672
+ const subscription = this.subscriptions.get(channel);
2673
+ if (!subscription) {
2674
+ return;
2675
+ }
2676
+ if (event === "presence:join") {
2677
+ subscription.members.set(member.presenceId, member);
2678
+ } else {
2679
+ subscription.members.delete(member.presenceId);
2479
2680
  }
2480
2681
  }
2481
- /**
2482
- * Check if connected to the realtime server
2483
- */
2484
2682
  get isConnected() {
2485
2683
  return this.socket?.connected ?? false;
2486
2684
  }
2487
- /**
2488
- * Get the current connection state
2489
- */
2490
2685
  get connectionState() {
2491
- if (!this.socket) return "disconnected";
2492
- if (this.socket.connected) return "connected";
2493
- return "connecting";
2686
+ if (!this.socket) {
2687
+ return "disconnected";
2688
+ }
2689
+ return this.socket.connected ? "connected" : "connecting";
2494
2690
  }
2495
- /**
2496
- * Get the socket ID (if connected)
2497
- */
2498
2691
  get socketId() {
2499
2692
  return this.socket?.id;
2500
2693
  }
2501
- /**
2502
- * Subscribe to a channel
2503
- *
2504
- * Automatically connects if not already connected.
2505
- *
2506
- * @param channel - Channel name (e.g., 'orders:123', 'broadcast')
2507
- * @returns Promise with the subscription response
2508
- */
2509
2694
  async subscribe(channel) {
2510
- if (this.subscribedChannels.has(channel)) {
2511
- return { ok: true, channel, presence: { members: [] } };
2695
+ let subscription = this.subscriptions.get(channel);
2696
+ if (subscription) {
2697
+ if (subscription.pending) {
2698
+ return subscription.pending;
2699
+ }
2700
+ if (subscription.status === "subscribed") {
2701
+ return { ok: true, channel, presence: { members: [...subscription.members.values()] } };
2702
+ }
2703
+ } else {
2704
+ subscription = { channel, epoch: 0, status: "pending", members: /* @__PURE__ */ new Map() };
2705
+ this.subscriptions.set(channel, subscription);
2512
2706
  }
2513
2707
  if (!this.socket?.connected) {
2514
2708
  try {
2515
2709
  await this.connect();
2516
2710
  } catch (error) {
2711
+ if (this.subscriptions.get(channel) === subscription) {
2712
+ this.subscriptions.delete(channel);
2713
+ }
2517
2714
  const message = error instanceof Error ? error.message : "Connection failed";
2518
2715
  return { ok: false, channel, error: { code: "CONNECTION_FAILED", message } };
2519
2716
  }
2520
2717
  }
2521
- return new Promise((resolve) => {
2522
- this.socket.emit("realtime:subscribe", { channel }, (response) => {
2523
- if (response.ok) {
2524
- this.subscribedChannels.add(channel);
2525
- }
2526
- resolve(response);
2527
- });
2528
- });
2718
+ return subscription.pending ?? this.requestSubscription(channel, subscription);
2529
2719
  }
2530
- /**
2531
- * Unsubscribe from a channel (fire-and-forget)
2532
- *
2533
- * @param channel - Channel name to unsubscribe from
2534
- */
2535
2720
  unsubscribe(channel) {
2536
- this.subscribedChannels.delete(channel);
2721
+ const subscription = this.subscriptions.get(channel);
2722
+ if (!subscription) {
2723
+ return;
2724
+ }
2725
+ this.subscriptions.delete(channel);
2726
+ this.settleSubscription(
2727
+ subscription,
2728
+ {
2729
+ ok: false,
2730
+ channel,
2731
+ error: { code: "SUBSCRIPTION_CANCELLED", message: "Subscription cancelled" }
2732
+ },
2733
+ true
2734
+ );
2537
2735
  if (this.socket?.connected) {
2538
2736
  this.socket.emit("realtime:unsubscribe", { channel });
2539
2737
  }
2540
2738
  }
2541
- /**
2542
- * Publish a message to a channel
2543
- *
2544
- * @param channel - Channel name
2545
- * @param event - Event name
2546
- * @param payload - Message payload
2547
- */
2548
2739
  async publish(channel, event, payload) {
2549
2740
  if (!this.socket?.connected) {
2550
2741
  throw new Error("Not connected to realtime server. Call connect() first.");
2551
2742
  }
2552
2743
  this.socket.emit("realtime:publish", { channel, event, payload });
2553
2744
  }
2554
- /**
2555
- * Listen for events
2556
- *
2557
- * Reserved event names:
2558
- * - 'connect' - Fired when connected to the server
2559
- * - 'connect_error' - Fired when connection fails (payload: Error)
2560
- * - 'disconnect' - Fired when disconnected (payload: reason string)
2561
- * - 'error' - Fired when a realtime error occurs (payload: RealtimeErrorPayload)
2562
- *
2563
- * All other events receive a `SocketMessage` payload with metadata.
2564
- *
2565
- * @param event - Event name to listen for
2566
- * @param callback - Callback function when event is received
2567
- */
2568
2745
  on(event, callback) {
2569
- if (!this.eventListeners.has(event)) {
2570
- this.eventListeners.set(event, /* @__PURE__ */ new Set());
2571
- }
2572
- this.eventListeners.get(event).add(callback);
2746
+ const listeners = this.eventListeners.get(event) ?? /* @__PURE__ */ new Set();
2747
+ listeners.add(callback);
2748
+ this.eventListeners.set(event, listeners);
2573
2749
  }
2574
- /**
2575
- * Remove a listener for a specific event
2576
- *
2577
- * @param event - Event name
2578
- * @param callback - The callback function to remove
2579
- */
2580
2750
  off(event, callback) {
2581
2751
  const listeners = this.eventListeners.get(event);
2582
- if (listeners) {
2583
- listeners.delete(callback);
2584
- if (listeners.size === 0) {
2585
- this.eventListeners.delete(event);
2586
- }
2752
+ listeners?.delete(callback);
2753
+ if (listeners?.size === 0) {
2754
+ this.eventListeners.delete(event);
2587
2755
  }
2588
2756
  }
2589
- /**
2590
- * Listen for an event only once, then automatically remove the listener
2591
- *
2592
- * @param event - Event name to listen for
2593
- * @param callback - Callback function when event is received
2594
- */
2595
2757
  once(event, callback) {
2596
2758
  const wrapper = (payload) => {
2597
2759
  this.off(event, wrapper);
@@ -2599,13 +2761,11 @@ var Realtime = class {
2599
2761
  };
2600
2762
  this.on(event, wrapper);
2601
2763
  }
2602
- /**
2603
- * Get all currently subscribed channels
2604
- *
2605
- * @returns Array of channel names
2606
- */
2607
2764
  getSubscribedChannels() {
2608
- return Array.from(this.subscribedChannels);
2765
+ return [...this.subscriptions.values()].filter((subscription) => subscription.status === "subscribed").map((subscription) => subscription.channel);
2766
+ }
2767
+ getPresenceState(channel) {
2768
+ return [...this.subscriptions.get(channel)?.members.values() ?? []];
2609
2769
  }
2610
2770
  };
2611
2771
 
@@ -2620,13 +2780,12 @@ var Emails = class {
2620
2780
  */
2621
2781
  async send(options) {
2622
2782
  try {
2623
- const data = await this.http.post(
2624
- "/api/email/send-raw",
2625
- options
2626
- );
2783
+ const data = await this.http.post("/api/email/send-raw", options);
2627
2784
  return { data, error: null };
2628
2785
  } catch (error) {
2629
- if (error instanceof Error && error.name === "AbortError") throw error;
2786
+ if (error instanceof Error && error.name === "AbortError") {
2787
+ throw error;
2788
+ }
2630
2789
  return {
2631
2790
  data: null,
2632
2791
  error: error instanceof InsForgeError ? error : new InsForgeError(
@@ -2709,10 +2868,7 @@ var RazorpayPayments = class {
2709
2868
  );
2710
2869
  return { data, error: null };
2711
2870
  } catch (error) {
2712
- return wrapError(
2713
- error,
2714
- "Razorpay order creation failed"
2715
- );
2871
+ return wrapError(error, "Razorpay order creation failed");
2716
2872
  }
2717
2873
  }
2718
2874
  async verifyOrder(environment, request) {
@@ -2723,10 +2879,7 @@ var RazorpayPayments = class {
2723
2879
  );
2724
2880
  return { data, error: null };
2725
2881
  } catch (error) {
2726
- return wrapError(
2727
- error,
2728
- "Razorpay order verification failed"
2729
- );
2882
+ return wrapError(error, "Razorpay order verification failed");
2730
2883
  }
2731
2884
  }
2732
2885
  async createSubscription(environment, request) {
@@ -2828,14 +2981,15 @@ var InsForgeClient = class {
2828
2981
  isServerMode: config.isServerMode ?? !!accessToken,
2829
2982
  detectOAuthCallback: config.auth?.detectOAuthCallback
2830
2983
  });
2831
- this.database = new Database(this.http);
2984
+ this.database = new Database(this.http, config.db?.schema);
2832
2985
  this.storage = new Storage(this.http);
2833
2986
  this.ai = new AI(this.http);
2834
2987
  this.functions = new Functions(this.http, config.functionsUrl);
2835
2988
  this.realtime = new Realtime(
2836
2989
  this.http.baseUrl,
2837
2990
  this.tokenManager,
2838
- config.anonKey
2991
+ config.anonKey,
2992
+ () => this.http.getValidAccessToken()
2839
2993
  );
2840
2994
  this.emails = new Emails(this.http);
2841
2995
  this.payments = new Payments(this.http);
@@ -2855,32 +3009,35 @@ var InsForgeClient = class {
2855
3009
  /**
2856
3010
  * Set the access token used by every SDK surface. Updates both the HTTP
2857
3011
  * client (database / storage / functions / AI / emails) and the realtime
2858
- * token manager (which fires `onTokenChange` to reconnect the WebSocket
2859
- * with the new bearer). Pass `null` to clear.
3012
+ * token manager. Pass `null` to sign out. By default a token replacement is
3013
+ * treated as a sign-in boundary and reconnects realtime. Pass
3014
+ * `AuthChangeEvent.TOKEN_REFRESHED` for a same-identity refresh to preserve a live socket; the
3015
+ * refreshed token is then used at the next handshake.
2860
3016
  *
2861
3017
  * Use this when an external auth provider (Better Auth, Clerk, Auth0,
2862
3018
  * WorkOS, Kinde, Stytch, …) issues the JWT and you need to keep the
2863
3019
  * long-lived InsForge client in sync. Without this, you'd have to call
2864
3020
  * `client.getHttpClient().setAuthToken(token)` AND reach into the private
2865
- * `client.realtime.tokenManager.setAccessToken(token)` separately
2866
- * forgetting the second one silently breaks realtime auth.
3021
+ * realtime token manager separately.
2867
3022
  *
2868
3023
  * @example
2869
3024
  * ```typescript
3025
+ * import { AuthChangeEvent } from '@insforge/sdk';
3026
+ *
2870
3027
  * // Refresh a third-party-issued JWT periodically
2871
3028
  * const { token } = await fetch('/api/insforge-token').then((r) => r.json());
2872
- * client.setAccessToken(token);
3029
+ * client.setAccessToken(token, AuthChangeEvent.TOKEN_REFRESHED);
2873
3030
  *
2874
3031
  * // Sign-out
2875
3032
  * client.setAccessToken(null);
2876
3033
  * ```
2877
3034
  */
2878
- setAccessToken(token) {
3035
+ setAccessToken(token, event = AuthChangeEvent.SIGNED_IN) {
2879
3036
  this.http.setAuthToken(token);
2880
3037
  if (token === null) {
2881
3038
  this.tokenManager.clearSession();
2882
3039
  } else {
2883
- this.tokenManager.setAccessToken(token);
3040
+ this.tokenManager.setAccessToken(token, event);
2884
3041
  }
2885
3042
  }
2886
3043
  /**
@@ -2914,6 +3071,7 @@ var src_default = InsForgeClient;
2914
3071
  0 && (module.exports = {
2915
3072
  AI,
2916
3073
  Auth,
3074
+ AuthChangeEvent,
2917
3075
  Database,
2918
3076
  Emails,
2919
3077
  Functions,
@@ -2925,7 +3083,6 @@ var src_default = InsForgeClient;
2925
3083
  Realtime,
2926
3084
  Storage,
2927
3085
  StorageBucket,
2928
- TokenManager,
2929
3086
  createAdminClient,
2930
3087
  createClient
2931
3088
  });