@insforge/sdk 1.2.7 → 1.2.8-dev.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -284,6 +284,7 @@ var TokenManager = class {
284
284
  // src/lib/http-client.ts
285
285
  var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([500, 502, 503, 504]);
286
286
  var IDEMPOTENT_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
287
+ var REFRESHABLE_AUTH_ERROR_CODE = "AUTH_UNAUTHORIZED";
287
288
  function serializeBody(method, body, headers) {
288
289
  if (body === void 0) return void 0;
289
290
  if (method === "GET" || method === "HEAD") return void 0;
@@ -338,12 +339,11 @@ var HttpClient = class {
338
339
  */
339
340
  constructor(config, tokenManager, logger) {
340
341
  this.userToken = null;
341
- this.autoRefreshToken = true;
342
342
  this.isRefreshing = false;
343
343
  this.refreshPromise = null;
344
344
  this.refreshToken = null;
345
+ this.config = config;
345
346
  this.baseUrl = config.baseUrl || "http://localhost:7130";
346
- this.autoRefreshToken = config.autoRefreshToken ?? true;
347
347
  this.fetch = config.fetch || (globalThis.fetch ? globalThis.fetch.bind(globalThis) : void 0);
348
348
  this.anonKey = config.anonKey;
349
349
  this.defaultHeaders = {
@@ -393,48 +393,19 @@ var HttpClient = class {
393
393
  const jitter = base * (0.85 + Math.random() * 0.3);
394
394
  return Math.round(jitter);
395
395
  }
396
- /**
397
- * Performs an HTTP request with automatic retry and timeout handling.
398
- * Retries on network errors and 5xx server errors with exponential backoff.
399
- * Client errors (4xx) and timeouts are thrown immediately without retry.
400
- * @param method - HTTP method (GET, POST, PUT, PATCH, DELETE).
401
- * @param path - API path relative to the base URL.
402
- * @param options - Optional request configuration including headers, body, and query params.
403
- * @returns Parsed response data.
404
- * @throws {InsForgeError} On timeout, network failure, or HTTP error responses.
405
- */
406
- async handleRequest(method, path, options = {}) {
396
+ shouldRefreshAccessToken(statusCode, errorCode, options = {}) {
397
+ return statusCode === 401 && errorCode === REFRESHABLE_AUTH_ERROR_CODE && !(this.config.isServerMode ?? !!this.config.edgeFunctionToken) && !options.skipAuthRefresh && this.userToken !== null;
398
+ }
399
+ async fetchWithRetry(args) {
407
400
  const {
408
- params,
409
- headers = {},
401
+ method,
402
+ url,
403
+ headers,
410
404
  body,
411
- signal: callerSignal,
412
- ...fetchOptions
413
- } = options;
414
- const url = this.buildUrl(path, params);
415
- const startTime = Date.now();
416
- const canRetry = IDEMPOTENT_METHODS.has(method.toUpperCase()) || options.idempotent === true;
417
- const maxAttempts = canRetry ? this.retryCount : 0;
418
- const requestHeaders = {
419
- ...this.defaultHeaders
420
- };
421
- const authToken = this.userToken || this.anonKey;
422
- if (authToken) {
423
- requestHeaders["Authorization"] = `Bearer ${authToken}`;
424
- }
425
- const processedBody = serializeBody(method, body, requestHeaders);
426
- if (headers instanceof Headers) {
427
- headers.forEach((value, key) => {
428
- requestHeaders[key] = value;
429
- });
430
- } else if (Array.isArray(headers)) {
431
- headers.forEach(([key, value]) => {
432
- requestHeaders[key] = value;
433
- });
434
- } else {
435
- Object.assign(requestHeaders, headers);
436
- }
437
- this.logger.logRequest(method, url, requestHeaders, processedBody);
405
+ fetchOptions,
406
+ callerSignal,
407
+ maxAttempts
408
+ } = args;
438
409
  let lastError;
439
410
  for (let attempt = 0; attempt <= maxAttempts; attempt++) {
440
411
  if (attempt > 0) {
@@ -486,8 +457,8 @@ var HttpClient = class {
486
457
  try {
487
458
  const response = await this.fetch(url, {
488
459
  method,
489
- headers: requestHeaders,
490
- body: processedBody,
460
+ headers,
461
+ body,
491
462
  ...fetchOptions,
492
463
  ...controller ? { signal: controller.signal } : {}
493
464
  });
@@ -501,31 +472,8 @@ var HttpClient = class {
501
472
  );
502
473
  continue;
503
474
  }
504
- let data;
505
- try {
506
- data = await parseResponse(response);
507
- } catch (err) {
508
- if (timer !== void 0) clearTimeout(timer);
509
- if (err instanceof InsForgeError) {
510
- this.logger.logResponse(
511
- method,
512
- url,
513
- err.statusCode || response.status,
514
- Date.now() - startTime,
515
- err
516
- );
517
- }
518
- throw err;
519
- }
520
475
  if (timer !== void 0) clearTimeout(timer);
521
- this.logger.logResponse(
522
- method,
523
- url,
524
- response.status,
525
- Date.now() - startTime,
526
- data
527
- );
528
- return data;
476
+ return response;
529
477
  } catch (err) {
530
478
  if (timer !== void 0) clearTimeout(timer);
531
479
  if (err?.name === "AbortError") {
@@ -538,9 +486,6 @@ var HttpClient = class {
538
486
  }
539
487
  throw err;
540
488
  }
541
- if (err instanceof InsForgeError) {
542
- throw err;
543
- }
544
489
  if (attempt < maxAttempts) {
545
490
  lastError = err;
546
491
  continue;
@@ -558,33 +503,176 @@ var HttpClient = class {
558
503
  "NETWORK_ERROR"
559
504
  );
560
505
  }
506
+ /**
507
+ * Performs an HTTP request with automatic retry and timeout handling.
508
+ * Retries on network errors and 5xx server errors with exponential backoff.
509
+ * Client errors (4xx) and timeouts are thrown immediately without retry.
510
+ * @param method - HTTP method (GET, POST, PUT, PATCH, DELETE).
511
+ * @param path - API path relative to the base URL.
512
+ * @param options - Optional request configuration including headers, body, and query params.
513
+ * @returns Parsed response data.
514
+ * @throws {InsForgeError} On timeout, network failure, or HTTP error responses.
515
+ */
516
+ async handleRequest(method, path, options = {}) {
517
+ const {
518
+ params,
519
+ headers = {},
520
+ body,
521
+ skipAuthRefresh: _skipAuthRefresh,
522
+ signal: callerSignal,
523
+ ...fetchOptions
524
+ } = options;
525
+ const url = this.buildUrl(path, params);
526
+ const startTime = Date.now();
527
+ const canRetry = IDEMPOTENT_METHODS.has(method.toUpperCase()) || options.idempotent === true;
528
+ const maxAttempts = canRetry ? this.retryCount : 0;
529
+ const requestHeaders = {
530
+ ...this.defaultHeaders
531
+ };
532
+ const authToken = this.userToken || this.anonKey;
533
+ if (authToken) {
534
+ requestHeaders["Authorization"] = `Bearer ${authToken}`;
535
+ }
536
+ const processedBody = serializeBody(method, body, requestHeaders);
537
+ if (headers instanceof Headers) {
538
+ headers.forEach((value, key) => {
539
+ requestHeaders[key] = value;
540
+ });
541
+ } else if (Array.isArray(headers)) {
542
+ headers.forEach(([key, value]) => {
543
+ requestHeaders[key] = value;
544
+ });
545
+ } else {
546
+ Object.assign(requestHeaders, headers);
547
+ }
548
+ this.logger.logRequest(method, url, requestHeaders, processedBody);
549
+ const response = await this.fetchWithRetry({
550
+ method,
551
+ url,
552
+ headers: requestHeaders,
553
+ body: processedBody,
554
+ fetchOptions,
555
+ callerSignal,
556
+ maxAttempts
557
+ });
558
+ let data;
559
+ try {
560
+ data = await parseResponse(response);
561
+ } catch (err) {
562
+ if (err instanceof InsForgeError) {
563
+ this.logger.logResponse(
564
+ method,
565
+ url,
566
+ err.statusCode || response.status,
567
+ Date.now() - startTime,
568
+ err
569
+ );
570
+ }
571
+ throw err;
572
+ }
573
+ this.logger.logResponse(
574
+ method,
575
+ url,
576
+ response.status,
577
+ Date.now() - startTime,
578
+ data
579
+ );
580
+ return data;
581
+ }
561
582
  async request(method, path, options = {}) {
562
583
  try {
563
584
  return await this.handleRequest(method, path, { ...options });
564
585
  } catch (error) {
565
- if (error instanceof InsForgeError && error.statusCode === 401 && error.error === "INVALID_TOKEN" && this.autoRefreshToken) {
586
+ if (error instanceof InsForgeError && this.shouldRefreshAccessToken(error.statusCode, error.error, options)) {
566
587
  try {
567
- const newTokenData = await this.handleTokenRefresh();
568
- this.setAuthToken(newTokenData.accessToken);
569
- this.tokenManager.saveSession(newTokenData);
570
- if (newTokenData.csrfToken) {
571
- setCsrfToken(newTokenData.csrfToken);
572
- }
573
- if (newTokenData.refreshToken) {
574
- this.setRefreshToken(newTokenData.refreshToken);
575
- }
576
- return await this.handleRequest(method, path, { ...options });
588
+ await this.refreshAndSaveSession();
577
589
  } catch (error2) {
578
- this.tokenManager.clearSession();
579
- this.userToken = null;
580
- this.refreshToken = null;
581
- clearCsrfToken();
590
+ this.clearAuthSession();
582
591
  throw error2;
583
592
  }
593
+ return await this.handleRequest(method, path, { ...options });
584
594
  }
585
595
  throw error;
586
596
  }
587
597
  }
598
+ /**
599
+ * Performs an SDK-configured fetch and returns the raw Response.
600
+ * This is used by clients such as postgrest-js that need to own response
601
+ * parsing while still sharing SDK auth and refresh behavior.
602
+ */
603
+ async rawFetch(input, init, options = {}) {
604
+ const request = typeof Request !== "undefined" && input instanceof Request ? input : void 0;
605
+ const {
606
+ method: initMethod,
607
+ headers: initHeaders,
608
+ body: initBody,
609
+ signal: initSignal,
610
+ ...fetchOptions
611
+ } = init ?? {};
612
+ const method = initMethod ?? request?.method ?? "GET";
613
+ const url = request?.url ?? input.toString();
614
+ const startTime = Date.now();
615
+ const headers = new Headers(this.getHeaders());
616
+ request?.headers.forEach((value, key) => {
617
+ headers.set(key, value);
618
+ });
619
+ new Headers(initHeaders).forEach((value, key) => {
620
+ headers.set(key, value);
621
+ });
622
+ const requestHeaders = {};
623
+ headers.forEach((value, key) => {
624
+ requestHeaders[key] = value;
625
+ });
626
+ const body = initBody ?? request?.body ?? void 0;
627
+ const callerSignal = initSignal ?? request?.signal;
628
+ const maxAttempts = IDEMPOTENT_METHODS.has(method.toUpperCase()) ? this.retryCount : 0;
629
+ this.logger.logRequest(method, url, requestHeaders, body);
630
+ const response = await this.fetchWithRetry({
631
+ method,
632
+ url,
633
+ headers: requestHeaders,
634
+ body,
635
+ fetchOptions,
636
+ callerSignal,
637
+ maxAttempts
638
+ });
639
+ this.logger.logResponse(
640
+ method,
641
+ url,
642
+ response.status,
643
+ Date.now() - startTime
644
+ );
645
+ let errorCode = null;
646
+ if (response.status === 401) {
647
+ try {
648
+ const data = await response.clone().json();
649
+ if (data && typeof data === "object") {
650
+ const candidate = data.error ?? data.code;
651
+ if (typeof candidate === "string") {
652
+ errorCode = candidate;
653
+ }
654
+ }
655
+ } catch {
656
+ }
657
+ }
658
+ if (!this.shouldRefreshAccessToken(response.status, errorCode, options)) {
659
+ return response;
660
+ }
661
+ let newTokenData;
662
+ try {
663
+ newTokenData = await this.refreshAndSaveSession();
664
+ } catch (error) {
665
+ this.clearAuthSession();
666
+ throw error;
667
+ }
668
+ const retryHeaders = new Headers(initHeaders);
669
+ retryHeaders.set("Authorization", `Bearer ${newTokenData.accessToken}`);
670
+ return await this.rawFetch(
671
+ input,
672
+ { ...init, headers: retryHeaders },
673
+ { skipAuthRefresh: true }
674
+ );
675
+ }
588
676
  /** Performs a GET request. */
589
677
  get(path, options) {
590
678
  return this.request("GET", path, options);
@@ -621,7 +709,7 @@ var HttpClient = class {
621
709
  }
622
710
  return headers;
623
711
  }
624
- async handleTokenRefresh() {
712
+ async refreshAccessToken() {
625
713
  if (this.isRefreshing) {
626
714
  return this.refreshPromise;
627
715
  }
@@ -632,7 +720,7 @@ var HttpClient = class {
632
720
  const body = this.refreshToken ? { refreshToken: this.refreshToken } : void 0;
633
721
  const response = await this.handleRequest(
634
722
  "POST",
635
- "/api/auth/sessions/current",
723
+ this.refreshToken ? "/api/auth/refresh?client_type=mobile" : "/api/auth/refresh",
636
724
  {
637
725
  body,
638
726
  headers: csrfToken ? { "X-CSRF-Token": csrfToken } : {},
@@ -647,6 +735,24 @@ var HttpClient = class {
647
735
  })();
648
736
  return this.refreshPromise;
649
737
  }
738
+ async refreshAndSaveSession() {
739
+ const newTokenData = await this.refreshAccessToken();
740
+ this.setAuthToken(newTokenData.accessToken);
741
+ this.tokenManager.saveSession(newTokenData);
742
+ if (newTokenData.csrfToken) {
743
+ setCsrfToken(newTokenData.csrfToken);
744
+ }
745
+ if (newTokenData.refreshToken) {
746
+ this.setRefreshToken(newTokenData.refreshToken);
747
+ }
748
+ return newTokenData;
749
+ }
750
+ clearAuthSession() {
751
+ this.tokenManager.clearSession();
752
+ this.userToken = null;
753
+ this.refreshToken = null;
754
+ clearCsrfToken();
755
+ }
650
756
  };
651
757
 
652
758
  // src/modules/auth/helpers.ts
@@ -775,7 +881,7 @@ var Auth = class {
775
881
  const response = await this.http.post(
776
882
  this.isServerMode() ? "/api/auth/users?client_type=mobile" : "/api/auth/users",
777
883
  request,
778
- { credentials: "include" }
884
+ { credentials: "include", skipAuthRefresh: true }
779
885
  );
780
886
  if (response.accessToken && response.user) {
781
887
  this.saveSessionFromResponse(response);
@@ -793,7 +899,7 @@ var Auth = class {
793
899
  const response = await this.http.post(
794
900
  this.isServerMode() ? "/api/auth/sessions?client_type=mobile" : "/api/auth/sessions",
795
901
  request,
796
- { credentials: "include" }
902
+ { credentials: "include", skipAuthRefresh: true }
797
903
  );
798
904
  this.saveSessionFromResponse(response);
799
905
  if (response.refreshToken) {
@@ -810,7 +916,7 @@ var Auth = class {
810
916
  await this.http.post(
811
917
  this.isServerMode() ? "/api/auth/logout?client_type=mobile" : "/api/auth/logout",
812
918
  void 0,
813
- { credentials: "include" }
919
+ { credentials: "include", skipAuthRefresh: true }
814
920
  );
815
921
  } catch {
816
922
  }
@@ -847,7 +953,8 @@ var Auth = class {
847
953
  );
848
954
  const oauthPath = isBuiltInProvider ? `/api/auth/oauth/${providerKey}` : `/api/auth/oauth/custom/${providerKey}`;
849
955
  const response = await this.http.get(oauthPath, {
850
- params
956
+ params,
957
+ skipAuthRefresh: true
851
958
  });
852
959
  if (!this.isServerMode() && typeof window !== "undefined" && !skipBrowserRedirect) {
853
960
  window.location.href = response.authUrl;
@@ -895,7 +1002,7 @@ var Auth = class {
895
1002
  const response = await this.http.post(
896
1003
  this.isServerMode() ? "/api/auth/oauth/exchange?client_type=mobile" : "/api/auth/oauth/exchange",
897
1004
  request,
898
- { credentials: "include" }
1005
+ { credentials: "include", skipAuthRefresh: true }
899
1006
  );
900
1007
  this.saveSessionFromResponse(response);
901
1008
  return {
@@ -922,7 +1029,7 @@ var Auth = class {
922
1029
  const response = await this.http.post(
923
1030
  "/api/auth/id-token?client_type=mobile",
924
1031
  { provider, token },
925
- { credentials: "include" }
1032
+ { credentials: "include", skipAuthRefresh: true }
926
1033
  );
927
1034
  this.saveSessionFromResponse(response);
928
1035
  if (response.refreshToken) {
@@ -969,7 +1076,8 @@ var Auth = class {
969
1076
  this.isServerMode() ? { refresh_token: options?.refreshToken } : void 0,
970
1077
  {
971
1078
  headers: csrfToken ? { "X-CSRF-Token": csrfToken } : {},
972
- credentials: "include"
1079
+ credentials: "include",
1080
+ skipAuthRefresh: true
973
1081
  }
974
1082
  );
975
1083
  if (response.accessToken) {
@@ -1072,7 +1180,9 @@ var Auth = class {
1072
1180
  // ============================================================================
1073
1181
  async resendVerificationEmail(request) {
1074
1182
  try {
1075
- const response = await this.http.post("/api/auth/email/send-verification", request);
1183
+ const response = await this.http.post("/api/auth/email/send-verification", request, {
1184
+ skipAuthRefresh: true
1185
+ });
1076
1186
  return { data: response, error: null };
1077
1187
  } catch (error) {
1078
1188
  return wrapError(
@@ -1086,7 +1196,7 @@ var Auth = class {
1086
1196
  const response = await this.http.post(
1087
1197
  this.isServerMode() ? "/api/auth/email/verify?client_type=mobile" : "/api/auth/email/verify",
1088
1198
  request,
1089
- { credentials: "include" }
1199
+ { credentials: "include", skipAuthRefresh: true }
1090
1200
  );
1091
1201
  this.saveSessionFromResponse(response);
1092
1202
  if (response.refreshToken) {
@@ -1105,7 +1215,9 @@ var Auth = class {
1105
1215
  // ============================================================================
1106
1216
  async sendResetPasswordEmail(request) {
1107
1217
  try {
1108
- const response = await this.http.post("/api/auth/email/send-reset-password", request);
1218
+ const response = await this.http.post("/api/auth/email/send-reset-password", request, {
1219
+ skipAuthRefresh: true
1220
+ });
1109
1221
  return { data: response, error: null };
1110
1222
  } catch (error) {
1111
1223
  return wrapError(
@@ -1118,7 +1230,8 @@ var Auth = class {
1118
1230
  try {
1119
1231
  const response = await this.http.post(
1120
1232
  "/api/auth/email/exchange-reset-password-token",
1121
- request
1233
+ request,
1234
+ { skipAuthRefresh: true }
1122
1235
  );
1123
1236
  return { data: response, error: null };
1124
1237
  } catch (error) {
@@ -1132,7 +1245,8 @@ var Auth = class {
1132
1245
  try {
1133
1246
  const response = await this.http.post(
1134
1247
  "/api/auth/email/reset-password",
1135
- request
1248
+ request,
1249
+ { skipAuthRefresh: true }
1136
1250
  );
1137
1251
  return { data: response, error: null };
1138
1252
  } catch (error) {
@@ -1148,7 +1262,8 @@ var Auth = class {
1148
1262
  async getPublicAuthConfig() {
1149
1263
  try {
1150
1264
  const response = await this.http.get(
1151
- "/api/auth/public-config"
1265
+ "/api/auth/public-config",
1266
+ { skipAuthRefresh: true }
1152
1267
  );
1153
1268
  return { data: response, error: null };
1154
1269
  } catch (error) {
@@ -1162,7 +1277,7 @@ var Auth = class {
1162
1277
 
1163
1278
  // src/modules/database-postgrest.ts
1164
1279
  import { PostgrestClient } from "@supabase/postgrest-js";
1165
- function createInsForgePostgrestFetch(httpClient, tokenManager) {
1280
+ function createInsForgePostgrestFetch(httpClient) {
1166
1281
  return async (input, init) => {
1167
1282
  const url = typeof input === "string" ? input : input.toString();
1168
1283
  const urlObj = new URL(url);
@@ -1170,14 +1285,11 @@ function createInsForgePostgrestFetch(httpClient, tokenManager) {
1170
1285
  const rpcMatch = pathname.match(/^rpc\/(.+)$/);
1171
1286
  const endpoint = rpcMatch ? `/api/database/rpc/${rpcMatch[1]}` : `/api/database/records/${pathname}`;
1172
1287
  const insforgeUrl = `${httpClient.baseUrl}${endpoint}${urlObj.search}`;
1173
- const token = tokenManager.getAccessToken();
1174
- const httpHeaders = httpClient.getHeaders();
1175
- const authToken = token || httpHeaders["Authorization"]?.replace("Bearer ", "");
1176
- const headers = new Headers(init?.headers);
1177
- if (authToken && !headers.has("Authorization")) {
1178
- headers.set("Authorization", `Bearer ${authToken}`);
1179
- }
1180
- const response = await fetch(insforgeUrl, {
1288
+ const headers = new Headers(httpClient.getHeaders());
1289
+ new Headers(init?.headers).forEach((value, key) => {
1290
+ headers.set(key, value);
1291
+ });
1292
+ const response = await httpClient.rawFetch(insforgeUrl, {
1181
1293
  ...init,
1182
1294
  headers
1183
1295
  });
@@ -1185,42 +1297,42 @@ function createInsForgePostgrestFetch(httpClient, tokenManager) {
1185
1297
  };
1186
1298
  }
1187
1299
  var Database = class {
1188
- constructor(httpClient, tokenManager) {
1300
+ constructor(httpClient) {
1189
1301
  this.postgrest = new PostgrestClient("http://dummy", {
1190
- fetch: createInsForgePostgrestFetch(httpClient, tokenManager),
1302
+ fetch: createInsForgePostgrestFetch(httpClient),
1191
1303
  headers: {}
1192
1304
  });
1193
1305
  }
1194
1306
  /**
1195
1307
  * Create a query builder for a table
1196
- *
1308
+ *
1197
1309
  * @example
1198
1310
  * // Basic query
1199
1311
  * const { data, error } = await client.database
1200
1312
  * .from('posts')
1201
1313
  * .select('*')
1202
1314
  * .eq('user_id', userId);
1203
- *
1315
+ *
1204
1316
  * // With count (Supabase style!)
1205
1317
  * const { data, error, count } = await client.database
1206
1318
  * .from('posts')
1207
1319
  * .select('*', { count: 'exact' })
1208
1320
  * .range(0, 9);
1209
- *
1321
+ *
1210
1322
  * // Just get count, no data
1211
1323
  * const { count } = await client.database
1212
1324
  * .from('posts')
1213
1325
  * .select('*', { count: 'exact', head: true });
1214
- *
1326
+ *
1215
1327
  * // Complex queries with OR
1216
1328
  * const { data } = await client.database
1217
1329
  * .from('posts')
1218
1330
  * .select('*, users!inner(*)')
1219
1331
  * .or('status.eq.active,status.eq.pending');
1220
- *
1332
+ *
1221
1333
  * // All features work:
1222
1334
  * - Nested selects
1223
- * - Foreign key expansion
1335
+ * - Foreign key expansion
1224
1336
  * - OR/AND/NOT conditions
1225
1337
  * - Count with head
1226
1338
  * - Range pagination
@@ -2320,7 +2432,7 @@ var InsForgeClient = class {
2320
2432
  this.auth = new Auth(this.http, this.tokenManager, {
2321
2433
  isServerMode: config.isServerMode ?? !!config.edgeFunctionToken
2322
2434
  });
2323
- this.database = new Database(this.http, this.tokenManager);
2435
+ this.database = new Database(this.http);
2324
2436
  this.storage = new Storage(this.http);
2325
2437
  this.ai = new AI(this.http);
2326
2438
  this.functions = new Functions(this.http, config.functionsUrl);