@insforge/sdk 1.4.3 → 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");
@@ -1100,11 +1175,7 @@ var Auth = class {
1100
1175
  if (!signInOptions || !signInOptions.redirectTo) {
1101
1176
  return {
1102
1177
  data: {},
1103
- error: new InsForgeError(
1104
- "Redirect URI is required",
1105
- 400,
1106
- import_shared_schemas.ERROR_CODES.INVALID_INPUT
1107
- )
1178
+ error: new InsForgeError("Redirect URI is required", 400, import_shared_schemas.ERROR_CODES.INVALID_INPUT)
1108
1179
  };
1109
1180
  }
1110
1181
  const { provider } = signInOptions;
@@ -1179,10 +1250,7 @@ var Auth = class {
1179
1250
  error: null
1180
1251
  };
1181
1252
  } catch (error) {
1182
- return wrapError(
1183
- error,
1184
- "An unexpected error occurred during OAuth code exchange"
1185
- );
1253
+ return wrapError(error, "An unexpected error occurred during OAuth code exchange");
1186
1254
  }
1187
1255
  }
1188
1256
  /**
@@ -1209,10 +1277,7 @@ var Auth = class {
1209
1277
  error: null
1210
1278
  };
1211
1279
  } catch (error) {
1212
- return wrapError(
1213
- error,
1214
- "An unexpected error occurred during ID token sign in"
1215
- );
1280
+ return wrapError(error, "An unexpected error occurred during ID token sign in");
1216
1281
  }
1217
1282
  }
1218
1283
  // ============================================================================
@@ -1253,14 +1318,11 @@ var Auth = class {
1253
1318
  }
1254
1319
  );
1255
1320
  if (response.accessToken) {
1256
- this.saveSessionFromResponse(response);
1321
+ this.saveSessionFromResponse(response, AuthChangeEvent.TOKEN_REFRESHED);
1257
1322
  }
1258
1323
  return { data: response, error: null };
1259
1324
  } catch (error) {
1260
- return wrapError(
1261
- error,
1262
- "An unexpected error occurred during session refresh"
1263
- );
1325
+ return wrapError(error, "An unexpected error occurred during session refresh");
1264
1326
  }
1265
1327
  }
1266
1328
  /**
@@ -1271,11 +1333,11 @@ var Auth = class {
1271
1333
  try {
1272
1334
  if (this.isServerMode()) {
1273
1335
  const accessToken = this.tokenManager.getAccessToken();
1274
- if (!accessToken) return { data: { user: null }, error: null };
1336
+ if (!accessToken) {
1337
+ return { data: { user: null }, error: null };
1338
+ }
1275
1339
  this.http.setAuthToken(accessToken);
1276
- const response = await this.http.get(
1277
- "/api/auth/sessions/current"
1278
- );
1340
+ const response = await this.http.get("/api/auth/sessions/current");
1279
1341
  const user = response.user ?? null;
1280
1342
  return { data: { user }, error: null };
1281
1343
  }
@@ -1313,25 +1375,17 @@ var Auth = class {
1313
1375
  // ============================================================================
1314
1376
  async getProfile(userId) {
1315
1377
  try {
1316
- const response = await this.http.get(
1317
- `/api/auth/profiles/${userId}`
1318
- );
1378
+ const response = await this.http.get(`/api/auth/profiles/${userId}`);
1319
1379
  return { data: response, error: null };
1320
1380
  } catch (error) {
1321
- return wrapError(
1322
- error,
1323
- "An unexpected error occurred while fetching user profile"
1324
- );
1381
+ return wrapError(error, "An unexpected error occurred while fetching user profile");
1325
1382
  }
1326
1383
  }
1327
1384
  async setProfile(profile) {
1328
1385
  try {
1329
- const response = await this.http.patch(
1330
- "/api/auth/profiles/current",
1331
- {
1332
- profile
1333
- }
1334
- );
1386
+ const response = await this.http.patch("/api/auth/profiles/current", {
1387
+ profile
1388
+ });
1335
1389
  const currentUser = this.tokenManager.getUser();
1336
1390
  if (!this.isServerMode() && currentUser && response.profile !== void 0) {
1337
1391
  this.tokenManager.setUser({
@@ -1341,10 +1395,7 @@ var Auth = class {
1341
1395
  }
1342
1396
  return { data: response, error: null };
1343
1397
  } catch (error) {
1344
- return wrapError(
1345
- error,
1346
- "An unexpected error occurred while updating user profile"
1347
- );
1398
+ return wrapError(error, "An unexpected error occurred while updating user profile");
1348
1399
  }
1349
1400
  }
1350
1401
  // ============================================================================
@@ -1357,10 +1408,7 @@ var Auth = class {
1357
1408
  });
1358
1409
  return { data: response, error: null };
1359
1410
  } catch (error) {
1360
- return wrapError(
1361
- error,
1362
- "An unexpected error occurred while sending verification email"
1363
- );
1411
+ return wrapError(error, "An unexpected error occurred while sending verification email");
1364
1412
  }
1365
1413
  }
1366
1414
  async verifyEmail(request) {
@@ -1376,10 +1424,7 @@ var Auth = class {
1376
1424
  }
1377
1425
  return { data: response, error: null };
1378
1426
  } catch (error) {
1379
- return wrapError(
1380
- error,
1381
- "An unexpected error occurred while verifying email"
1382
- );
1427
+ return wrapError(error, "An unexpected error occurred while verifying email");
1383
1428
  }
1384
1429
  }
1385
1430
  // ============================================================================
@@ -1392,10 +1437,7 @@ var Auth = class {
1392
1437
  });
1393
1438
  return { data: response, error: null };
1394
1439
  } catch (error) {
1395
- return wrapError(
1396
- error,
1397
- "An unexpected error occurred while sending password reset email"
1398
- );
1440
+ return wrapError(error, "An unexpected error occurred while sending password reset email");
1399
1441
  }
1400
1442
  }
1401
1443
  async exchangeResetPasswordToken(request) {
@@ -1407,10 +1449,7 @@ var Auth = class {
1407
1449
  );
1408
1450
  return { data: response, error: null };
1409
1451
  } catch (error) {
1410
- return wrapError(
1411
- error,
1412
- "An unexpected error occurred while verifying reset code"
1413
- );
1452
+ return wrapError(error, "An unexpected error occurred while verifying reset code");
1414
1453
  }
1415
1454
  }
1416
1455
  async resetPassword(request) {
@@ -1422,10 +1461,7 @@ var Auth = class {
1422
1461
  );
1423
1462
  return { data: response, error: null };
1424
1463
  } catch (error) {
1425
- return wrapError(
1426
- error,
1427
- "An unexpected error occurred while resetting password"
1428
- );
1464
+ return wrapError(error, "An unexpected error occurred while resetting password");
1429
1465
  }
1430
1466
  }
1431
1467
  // ============================================================================
@@ -1433,16 +1469,12 @@ var Auth = class {
1433
1469
  // ============================================================================
1434
1470
  async getPublicAuthConfig() {
1435
1471
  try {
1436
- const response = await this.http.get(
1437
- "/api/auth/public-config",
1438
- { skipAuthRefresh: true }
1439
- );
1472
+ const response = await this.http.get("/api/auth/public-config", {
1473
+ skipAuthRefresh: true
1474
+ });
1440
1475
  return { data: response, error: null };
1441
1476
  } catch (error) {
1442
- return wrapError(
1443
- error,
1444
- "An unexpected error occurred while fetching auth configuration"
1445
- );
1477
+ return wrapError(error, "An unexpected error occurred while fetching auth configuration");
1446
1478
  }
1447
1479
  }
1448
1480
  };
@@ -1600,11 +1632,7 @@ var StorageBucket = class {
1600
1632
  } catch (error) {
1601
1633
  return {
1602
1634
  data: null,
1603
- error: error instanceof InsForgeError ? error : new InsForgeError(
1604
- "Upload failed",
1605
- 500,
1606
- "STORAGE_ERROR"
1607
- )
1635
+ error: error instanceof InsForgeError ? error : new InsForgeError("Upload failed", 500, "STORAGE_ERROR")
1608
1636
  };
1609
1637
  }
1610
1638
  }
@@ -1650,11 +1678,7 @@ var StorageBucket = class {
1650
1678
  } catch (error) {
1651
1679
  return {
1652
1680
  data: null,
1653
- error: error instanceof InsForgeError ? error : new InsForgeError(
1654
- "Upload failed",
1655
- 500,
1656
- "STORAGE_ERROR"
1657
- )
1681
+ error: error instanceof InsForgeError ? error : new InsForgeError("Upload failed", 500, "STORAGE_ERROR")
1658
1682
  };
1659
1683
  }
1660
1684
  }
@@ -1682,13 +1706,10 @@ var StorageBucket = class {
1682
1706
  );
1683
1707
  }
1684
1708
  if (strategy.confirmRequired && strategy.confirmUrl) {
1685
- const confirmResponse = await this.http.post(
1686
- strategy.confirmUrl,
1687
- {
1688
- size: file.size,
1689
- contentType: file.type || "application/octet-stream"
1690
- }
1691
- );
1709
+ const confirmResponse = await this.http.post(strategy.confirmUrl, {
1710
+ size: file.size,
1711
+ contentType: file.type || "application/octet-stream"
1712
+ });
1692
1713
  return { data: confirmResponse, error: null };
1693
1714
  }
1694
1715
  return {
@@ -1703,11 +1724,7 @@ var StorageBucket = class {
1703
1724
  error: null
1704
1725
  };
1705
1726
  } catch (error) {
1706
- throw error instanceof InsForgeError ? error : new InsForgeError(
1707
- "Presigned upload failed",
1708
- 500,
1709
- "STORAGE_ERROR"
1710
- );
1727
+ throw error instanceof InsForgeError ? error : new InsForgeError("Presigned upload failed", 500, "STORAGE_ERROR");
1711
1728
  }
1712
1729
  }
1713
1730
  /**
@@ -1761,11 +1778,7 @@ var StorageBucket = class {
1761
1778
  } catch (error) {
1762
1779
  return {
1763
1780
  data: null,
1764
- error: error instanceof InsForgeError ? error : new InsForgeError(
1765
- "Download failed",
1766
- 500,
1767
- "STORAGE_ERROR"
1768
- )
1781
+ error: error instanceof InsForgeError ? error : new InsForgeError("Download failed", 500, "STORAGE_ERROR")
1769
1782
  };
1770
1783
  }
1771
1784
  }
@@ -1800,7 +1813,9 @@ var StorageBucket = class {
1800
1813
  } catch (error) {
1801
1814
  const status = error instanceof InsForgeError ? error.statusCode : void 0;
1802
1815
  const isMissingRoute = (status === 404 || status === 405) && !(error instanceof InsForgeError && error.error === "STORAGE_NOT_FOUND");
1803
- if (!isMissingRoute) throw error;
1816
+ if (!isMissingRoute) {
1817
+ throw error;
1818
+ }
1804
1819
  return await this.http.post(
1805
1820
  `/api/storage/buckets/${this.bucketName}/objects/${encoded}/download-strategy`,
1806
1821
  { expiresIn }
@@ -1876,10 +1891,18 @@ var StorageBucket = class {
1876
1891
  async list(options) {
1877
1892
  try {
1878
1893
  const params = {};
1879
- if (options?.prefix) params.prefix = options.prefix;
1880
- if (options?.search) params.search = options.search;
1881
- if (options?.limit) params.limit = options.limit.toString();
1882
- 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
+ }
1883
1906
  const response = await this.http.get(
1884
1907
  `/api/storage/buckets/${this.bucketName}/objects`,
1885
1908
  { params }
@@ -1888,11 +1911,7 @@ var StorageBucket = class {
1888
1911
  } catch (error) {
1889
1912
  return {
1890
1913
  data: null,
1891
- error: error instanceof InsForgeError ? error : new InsForgeError(
1892
- "List failed",
1893
- 500,
1894
- "STORAGE_ERROR"
1895
- )
1914
+ error: error instanceof InsForgeError ? error : new InsForgeError("List failed", 500, "STORAGE_ERROR")
1896
1915
  };
1897
1916
  }
1898
1917
  }
@@ -1909,11 +1928,7 @@ var StorageBucket = class {
1909
1928
  } catch (error) {
1910
1929
  return {
1911
1930
  data: null,
1912
- error: error instanceof InsForgeError ? error : new InsForgeError(
1913
- "Delete failed",
1914
- 500,
1915
- "STORAGE_ERROR"
1916
- )
1931
+ error: error instanceof InsForgeError ? error : new InsForgeError("Delete failed", 500, "STORAGE_ERROR")
1917
1932
  };
1918
1933
  }
1919
1934
  }
@@ -2035,14 +2050,11 @@ var ChatCompletions = class {
2035
2050
  if (params.stream) {
2036
2051
  const headers = this.http.getHeaders();
2037
2052
  headers["Content-Type"] = "application/json";
2038
- const response2 = await this.http.fetch(
2039
- `${this.http.baseUrl}/api/ai/chat/completion`,
2040
- {
2041
- method: "POST",
2042
- headers,
2043
- body: JSON.stringify(backendParams)
2044
- }
2045
- );
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
+ });
2046
2058
  if (!response2.ok) {
2047
2059
  const error = await response2.json();
2048
2060
  throw new Error(error.error || "Stream request failed");
@@ -2090,7 +2102,9 @@ var ChatCompletions = class {
2090
2102
  try {
2091
2103
  while (true) {
2092
2104
  const { done, value } = await reader.read();
2093
- if (done) break;
2105
+ if (done) {
2106
+ break;
2107
+ }
2094
2108
  buffer += decoder.decode(value, { stream: true });
2095
2109
  const lines = buffer.split("\n");
2096
2110
  buffer = lines.pop() || "";
@@ -2138,7 +2152,7 @@ var ChatCompletions = class {
2138
2152
  reader.releaseLock();
2139
2153
  return;
2140
2154
  }
2141
- } catch (e) {
2155
+ } catch {
2142
2156
  console.warn("Failed to parse SSE data:", dataStr);
2143
2157
  }
2144
2158
  }
@@ -2191,10 +2205,7 @@ var Embeddings = class {
2191
2205
  * ```
2192
2206
  */
2193
2207
  async create(params) {
2194
- const response = await this.http.post(
2195
- "/api/ai/embeddings",
2196
- params
2197
- );
2208
+ const response = await this.http.post("/api/ai/embeddings", params);
2198
2209
  return {
2199
2210
  object: response.object,
2200
2211
  data: response.data,
@@ -2280,7 +2291,9 @@ var Functions = class _Functions {
2280
2291
  static deriveSubhostingUrl(baseUrl) {
2281
2292
  try {
2282
2293
  const { hostname } = new URL(baseUrl);
2283
- if (!hostname.endsWith(".insforge.app")) return void 0;
2294
+ if (!hostname.endsWith(".insforge.app")) {
2295
+ return void 0;
2296
+ }
2284
2297
  const appKey = hostname.split(".")[0];
2285
2298
  return `https://${appKey}.functions.insforge.app`;
2286
2299
  } catch {
@@ -2326,7 +2339,9 @@ var Functions = class _Functions {
2326
2339
  const data = await parseResponse(res);
2327
2340
  return { data, error: null };
2328
2341
  } catch (error) {
2329
- if (error instanceof Error && error.name === "AbortError") throw error;
2342
+ if (error instanceof Error && error.name === "AbortError") {
2343
+ throw error;
2344
+ }
2330
2345
  return {
2331
2346
  data: null,
2332
2347
  error: error instanceof InsForgeError ? error : new InsForgeError(
@@ -2345,7 +2360,9 @@ var Functions = class _Functions {
2345
2360
  });
2346
2361
  return { data, error: null };
2347
2362
  } catch (error) {
2348
- if (error instanceof Error && error.name === "AbortError") throw error;
2363
+ if (error instanceof Error && error.name === "AbortError") {
2364
+ throw error;
2365
+ }
2349
2366
  if (error instanceof InsForgeError && error.statusCode === 404) {
2350
2367
  } else {
2351
2368
  return {
@@ -2364,7 +2381,9 @@ var Functions = class _Functions {
2364
2381
  const data = await this.http.request(method, path, { body, headers });
2365
2382
  return { data, error: null };
2366
2383
  } catch (error) {
2367
- if (error instanceof Error && error.name === "AbortError") throw error;
2384
+ if (error instanceof Error && error.name === "AbortError") {
2385
+ throw error;
2386
+ }
2368
2387
  return {
2369
2388
  data: null,
2370
2389
  error: error instanceof InsForgeError ? error : new InsForgeError(
@@ -2379,32 +2398,37 @@ var Functions = class _Functions {
2379
2398
 
2380
2399
  // src/modules/realtime.ts
2381
2400
  var CONNECT_TIMEOUT = 1e4;
2401
+ var SUBSCRIBE_TIMEOUT = 1e4;
2382
2402
  var Realtime = class {
2383
- constructor(baseUrl, tokenManager, anonKey) {
2384
- this.socket = null;
2385
- this.connectPromise = null;
2386
- this.subscribedChannels = /* @__PURE__ */ new Set();
2387
- this.eventListeners = /* @__PURE__ */ new Map();
2403
+ constructor(baseUrl, tokenManager, anonKey, getValidAccessToken = async () => tokenManager.getAccessToken()) {
2388
2404
  this.baseUrl = baseUrl;
2389
2405
  this.tokenManager = tokenManager;
2390
2406
  this.anonKey = anonKey;
2391
- 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
+ });
2392
2419
  }
2393
2420
  notifyListeners(event, payload) {
2394
- const listeners = this.eventListeners.get(event);
2395
- if (!listeners) return;
2396
- for (const cb of listeners) {
2421
+ for (const callback of this.eventListeners.get(event) ?? []) {
2397
2422
  try {
2398
- cb(payload);
2399
- } catch (err) {
2400
- console.error(`Error in ${event} callback:`, err);
2423
+ callback(payload);
2424
+ } catch (error) {
2425
+ console.error(`Error in ${event} callback:`, error);
2401
2426
  }
2402
2427
  }
2403
2428
  }
2404
- /**
2405
- * Connect to the realtime server
2406
- * @returns Promise that resolves when connected
2407
- */
2429
+ async getHandshakeToken() {
2430
+ return await this.getValidAccessToken() ?? this.anonKey ?? null;
2431
+ }
2408
2432
  connect() {
2409
2433
  if (this.socket?.connected) {
2410
2434
  return Promise.resolve();
@@ -2412,210 +2436,324 @@ var Realtime = class {
2412
2436
  if (this.connectPromise) {
2413
2437
  return this.connectPromise;
2414
2438
  }
2415
- this.connectPromise = (async () => {
2416
- try {
2417
- const { io } = await import("socket.io-client");
2418
- await new Promise((resolve, reject) => {
2419
- const token = this.tokenManager.getAccessToken() ?? this.anonKey;
2420
- this.socket = io(this.baseUrl, {
2421
- transports: ["websocket"],
2422
- auth: token ? { token } : void 0
2423
- });
2424
- let initialConnection = true;
2425
- let timeoutId = null;
2426
- const cleanup = () => {
2427
- if (timeoutId) {
2428
- clearTimeout(timeoutId);
2429
- timeoutId = null;
2430
- }
2431
- };
2432
- timeoutId = setTimeout(() => {
2433
- if (initialConnection) {
2434
- initialConnection = false;
2435
- this.connectPromise = null;
2436
- this.socket?.disconnect();
2437
- this.socket = null;
2438
- reject(new Error(`Connection timeout after ${CONNECT_TIMEOUT}ms`));
2439
- }
2440
- }, CONNECT_TIMEOUT);
2441
- this.socket.on("connect", () => {
2442
- cleanup();
2443
- for (const channel of this.subscribedChannels) {
2444
- this.socket.emit("realtime:subscribe", { channel });
2445
- }
2446
- this.notifyListeners("connect");
2447
- if (initialConnection) {
2448
- initialConnection = false;
2449
- this.connectPromise = null;
2450
- resolve();
2451
- }
2452
- });
2453
- this.socket.on("connect_error", (error) => {
2454
- cleanup();
2455
- this.notifyListeners("connect_error", error);
2456
- if (initialConnection) {
2457
- initialConnection = false;
2458
- this.connectPromise = null;
2459
- reject(error);
2460
- }
2461
- });
2462
- this.socket.on("disconnect", (reason) => {
2463
- this.notifyListeners("disconnect", reason);
2464
- });
2465
- this.socket.on("realtime:error", (error) => {
2466
- this.notifyListeners("error", error);
2467
- });
2468
- this.socket.onAny((event, message) => {
2469
- if (event === "realtime:error") return;
2470
- this.notifyListeners(event, message);
2471
- });
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
+ }
2472
2454
  });
2473
- } 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) {
2474
2536
  this.connectPromise = null;
2475
- throw error;
2476
2537
  }
2477
- })();
2478
- return this.connectPromise;
2538
+ });
2539
+ this.connectPromise = trackedConnection;
2540
+ return trackedConnection;
2479
2541
  }
2480
- /**
2481
- * Disconnect from the realtime server
2482
- */
2483
2542
  disconnect() {
2484
- if (this.socket) {
2485
- this.socket.disconnect();
2486
- 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
+ );
2487
2558
  }
2488
- this.subscribedChannels.clear();
2559
+ this.subscriptions.clear();
2489
2560
  }
2490
- /**
2491
- * Handle token changes (e.g., after auth refresh)
2492
- * Updates socket auth so reconnects use the new token
2493
- * If connected, triggers reconnect to apply new token immediately
2494
- */
2495
- onTokenChange() {
2496
- const token = this.tokenManager.getAccessToken() ?? this.anonKey;
2497
- if (this.socket) {
2498
- 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
+ }
2499
2566
  }
2500
- if (this.socket && (this.socket.connected || this.connectPromise)) {
2501
- this.socket.disconnect();
2502
- 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);
2503
2680
  }
2504
2681
  }
2505
- /**
2506
- * Check if connected to the realtime server
2507
- */
2508
2682
  get isConnected() {
2509
2683
  return this.socket?.connected ?? false;
2510
2684
  }
2511
- /**
2512
- * Get the current connection state
2513
- */
2514
2685
  get connectionState() {
2515
- if (!this.socket) return "disconnected";
2516
- if (this.socket.connected) return "connected";
2517
- return "connecting";
2686
+ if (!this.socket) {
2687
+ return "disconnected";
2688
+ }
2689
+ return this.socket.connected ? "connected" : "connecting";
2518
2690
  }
2519
- /**
2520
- * Get the socket ID (if connected)
2521
- */
2522
2691
  get socketId() {
2523
2692
  return this.socket?.id;
2524
2693
  }
2525
- /**
2526
- * Subscribe to a channel
2527
- *
2528
- * Automatically connects if not already connected.
2529
- *
2530
- * @param channel - Channel name (e.g., 'orders:123', 'broadcast')
2531
- * @returns Promise with the subscription response
2532
- */
2533
2694
  async subscribe(channel) {
2534
- if (this.subscribedChannels.has(channel)) {
2535
- 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);
2536
2706
  }
2537
2707
  if (!this.socket?.connected) {
2538
2708
  try {
2539
2709
  await this.connect();
2540
2710
  } catch (error) {
2711
+ if (this.subscriptions.get(channel) === subscription) {
2712
+ this.subscriptions.delete(channel);
2713
+ }
2541
2714
  const message = error instanceof Error ? error.message : "Connection failed";
2542
2715
  return { ok: false, channel, error: { code: "CONNECTION_FAILED", message } };
2543
2716
  }
2544
2717
  }
2545
- return new Promise((resolve) => {
2546
- this.socket.emit("realtime:subscribe", { channel }, (response) => {
2547
- if (response.ok) {
2548
- this.subscribedChannels.add(channel);
2549
- }
2550
- resolve(response);
2551
- });
2552
- });
2718
+ return subscription.pending ?? this.requestSubscription(channel, subscription);
2553
2719
  }
2554
- /**
2555
- * Unsubscribe from a channel (fire-and-forget)
2556
- *
2557
- * @param channel - Channel name to unsubscribe from
2558
- */
2559
2720
  unsubscribe(channel) {
2560
- 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
+ );
2561
2735
  if (this.socket?.connected) {
2562
2736
  this.socket.emit("realtime:unsubscribe", { channel });
2563
2737
  }
2564
2738
  }
2565
- /**
2566
- * Publish a message to a channel
2567
- *
2568
- * @param channel - Channel name
2569
- * @param event - Event name
2570
- * @param payload - Message payload
2571
- */
2572
2739
  async publish(channel, event, payload) {
2573
2740
  if (!this.socket?.connected) {
2574
2741
  throw new Error("Not connected to realtime server. Call connect() first.");
2575
2742
  }
2576
2743
  this.socket.emit("realtime:publish", { channel, event, payload });
2577
2744
  }
2578
- /**
2579
- * Listen for events
2580
- *
2581
- * Reserved event names:
2582
- * - 'connect' - Fired when connected to the server
2583
- * - 'connect_error' - Fired when connection fails (payload: Error)
2584
- * - 'disconnect' - Fired when disconnected (payload: reason string)
2585
- * - 'error' - Fired when a realtime error occurs (payload: RealtimeErrorPayload)
2586
- *
2587
- * All other events receive a `SocketMessage` payload with metadata.
2588
- *
2589
- * @param event - Event name to listen for
2590
- * @param callback - Callback function when event is received
2591
- */
2592
2745
  on(event, callback) {
2593
- if (!this.eventListeners.has(event)) {
2594
- this.eventListeners.set(event, /* @__PURE__ */ new Set());
2595
- }
2596
- 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);
2597
2749
  }
2598
- /**
2599
- * Remove a listener for a specific event
2600
- *
2601
- * @param event - Event name
2602
- * @param callback - The callback function to remove
2603
- */
2604
2750
  off(event, callback) {
2605
2751
  const listeners = this.eventListeners.get(event);
2606
- if (listeners) {
2607
- listeners.delete(callback);
2608
- if (listeners.size === 0) {
2609
- this.eventListeners.delete(event);
2610
- }
2752
+ listeners?.delete(callback);
2753
+ if (listeners?.size === 0) {
2754
+ this.eventListeners.delete(event);
2611
2755
  }
2612
2756
  }
2613
- /**
2614
- * Listen for an event only once, then automatically remove the listener
2615
- *
2616
- * @param event - Event name to listen for
2617
- * @param callback - Callback function when event is received
2618
- */
2619
2757
  once(event, callback) {
2620
2758
  const wrapper = (payload) => {
2621
2759
  this.off(event, wrapper);
@@ -2623,13 +2761,11 @@ var Realtime = class {
2623
2761
  };
2624
2762
  this.on(event, wrapper);
2625
2763
  }
2626
- /**
2627
- * Get all currently subscribed channels
2628
- *
2629
- * @returns Array of channel names
2630
- */
2631
2764
  getSubscribedChannels() {
2632
- 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() ?? []];
2633
2769
  }
2634
2770
  };
2635
2771
 
@@ -2644,13 +2780,12 @@ var Emails = class {
2644
2780
  */
2645
2781
  async send(options) {
2646
2782
  try {
2647
- const data = await this.http.post(
2648
- "/api/email/send-raw",
2649
- options
2650
- );
2783
+ const data = await this.http.post("/api/email/send-raw", options);
2651
2784
  return { data, error: null };
2652
2785
  } catch (error) {
2653
- if (error instanceof Error && error.name === "AbortError") throw error;
2786
+ if (error instanceof Error && error.name === "AbortError") {
2787
+ throw error;
2788
+ }
2654
2789
  return {
2655
2790
  data: null,
2656
2791
  error: error instanceof InsForgeError ? error : new InsForgeError(
@@ -2733,10 +2868,7 @@ var RazorpayPayments = class {
2733
2868
  );
2734
2869
  return { data, error: null };
2735
2870
  } catch (error) {
2736
- return wrapError(
2737
- error,
2738
- "Razorpay order creation failed"
2739
- );
2871
+ return wrapError(error, "Razorpay order creation failed");
2740
2872
  }
2741
2873
  }
2742
2874
  async verifyOrder(environment, request) {
@@ -2747,10 +2879,7 @@ var RazorpayPayments = class {
2747
2879
  );
2748
2880
  return { data, error: null };
2749
2881
  } catch (error) {
2750
- return wrapError(
2751
- error,
2752
- "Razorpay order verification failed"
2753
- );
2882
+ return wrapError(error, "Razorpay order verification failed");
2754
2883
  }
2755
2884
  }
2756
2885
  async createSubscription(environment, request) {
@@ -2859,7 +2988,8 @@ var InsForgeClient = class {
2859
2988
  this.realtime = new Realtime(
2860
2989
  this.http.baseUrl,
2861
2990
  this.tokenManager,
2862
- config.anonKey
2991
+ config.anonKey,
2992
+ () => this.http.getValidAccessToken()
2863
2993
  );
2864
2994
  this.emails = new Emails(this.http);
2865
2995
  this.payments = new Payments(this.http);
@@ -2879,32 +3009,35 @@ var InsForgeClient = class {
2879
3009
  /**
2880
3010
  * Set the access token used by every SDK surface. Updates both the HTTP
2881
3011
  * client (database / storage / functions / AI / emails) and the realtime
2882
- * token manager (which fires `onTokenChange` to reconnect the WebSocket
2883
- * 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.
2884
3016
  *
2885
3017
  * Use this when an external auth provider (Better Auth, Clerk, Auth0,
2886
3018
  * WorkOS, Kinde, Stytch, …) issues the JWT and you need to keep the
2887
3019
  * long-lived InsForge client in sync. Without this, you'd have to call
2888
3020
  * `client.getHttpClient().setAuthToken(token)` AND reach into the private
2889
- * `client.realtime.tokenManager.setAccessToken(token)` separately
2890
- * forgetting the second one silently breaks realtime auth.
3021
+ * realtime token manager separately.
2891
3022
  *
2892
3023
  * @example
2893
3024
  * ```typescript
3025
+ * import { AuthChangeEvent } from '@insforge/sdk';
3026
+ *
2894
3027
  * // Refresh a third-party-issued JWT periodically
2895
3028
  * const { token } = await fetch('/api/insforge-token').then((r) => r.json());
2896
- * client.setAccessToken(token);
3029
+ * client.setAccessToken(token, AuthChangeEvent.TOKEN_REFRESHED);
2897
3030
  *
2898
3031
  * // Sign-out
2899
3032
  * client.setAccessToken(null);
2900
3033
  * ```
2901
3034
  */
2902
- setAccessToken(token) {
3035
+ setAccessToken(token, event = AuthChangeEvent.SIGNED_IN) {
2903
3036
  this.http.setAuthToken(token);
2904
3037
  if (token === null) {
2905
3038
  this.tokenManager.clearSession();
2906
3039
  } else {
2907
- this.tokenManager.setAccessToken(token);
3040
+ this.tokenManager.setAccessToken(token, event);
2908
3041
  }
2909
3042
  }
2910
3043
  /**
@@ -2938,6 +3071,7 @@ var src_default = InsForgeClient;
2938
3071
  0 && (module.exports = {
2939
3072
  AI,
2940
3073
  Auth,
3074
+ AuthChangeEvent,
2941
3075
  Database,
2942
3076
  Emails,
2943
3077
  Functions,
@@ -2949,7 +3083,6 @@ var src_default = InsForgeClient;
2949
3083
  Realtime,
2950
3084
  Storage,
2951
3085
  StorageBucket,
2952
- TokenManager,
2953
3086
  createAdminClient,
2954
3087
  createClient
2955
3088
  });