@insforge/sdk 1.0.2-dev.0 → 1.0.3-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.js CHANGED
@@ -177,8 +177,31 @@ var HttpClient = class {
177
177
  // src/lib/token-manager.ts
178
178
  var TOKEN_KEY = "insforge-auth-token";
179
179
  var USER_KEY = "insforge-auth-user";
180
+ var CSRF_TOKEN_COOKIE = "insforge_csrf_token";
181
+ function getCsrfToken() {
182
+ if (typeof document === "undefined") return null;
183
+ const match = document.cookie.split(";").find((c) => c.trim().startsWith(`${CSRF_TOKEN_COOKIE}=`));
184
+ if (!match) return null;
185
+ return match.split("=")[1] || null;
186
+ }
187
+ function setCsrfToken(token) {
188
+ if (typeof document === "undefined") return;
189
+ const maxAge = 7 * 24 * 60 * 60;
190
+ const secure = typeof window !== "undefined" && window.location.protocol === "https:" ? "; Secure" : "";
191
+ document.cookie = `${CSRF_TOKEN_COOKIE}=${encodeURIComponent(token)}; path=/; max-age=${maxAge}; SameSite=Lax${secure}`;
192
+ }
193
+ function clearCsrfToken() {
194
+ if (typeof document === "undefined") return;
195
+ const secure = typeof window !== "undefined" && window.location.protocol === "https:" ? "; Secure" : "";
196
+ document.cookie = `${CSRF_TOKEN_COOKIE}=; path=/; max-age=0; SameSite=Lax${secure}`;
197
+ }
180
198
  var TokenManager = class {
181
199
  constructor(storage) {
200
+ // In-memory storage
201
+ this.accessToken = null;
202
+ this.user = null;
203
+ // Mode: 'memory' (new backend) or 'storage' (legacy backend, default)
204
+ this._mode = "storage";
182
205
  if (storage) {
183
206
  this.storage = storage;
184
207
  } else if (typeof window !== "undefined" && window.localStorage) {
@@ -196,126 +219,117 @@ var TokenManager = class {
196
219
  };
197
220
  }
198
221
  }
199
- saveSession(session) {
200
- this.storage.setItem(TOKEN_KEY, session.accessToken);
201
- this.storage.setItem(USER_KEY, JSON.stringify(session.user));
222
+ /**
223
+ * Get current mode
224
+ */
225
+ get mode() {
226
+ return this._mode;
202
227
  }
203
- getSession() {
228
+ /**
229
+ * Set mode to memory (new backend with cookies + memory)
230
+ */
231
+ setMemoryMode() {
232
+ if (this._mode === "storage") {
233
+ this.storage.removeItem(TOKEN_KEY);
234
+ this.storage.removeItem(USER_KEY);
235
+ }
236
+ this._mode = "memory";
237
+ }
238
+ /**
239
+ * Set mode to storage (legacy backend with localStorage)
240
+ * Also loads existing session from localStorage
241
+ */
242
+ setStorageMode() {
243
+ this._mode = "storage";
244
+ this.loadFromStorage();
245
+ }
246
+ /**
247
+ * Load session from localStorage
248
+ */
249
+ loadFromStorage() {
204
250
  const token = this.storage.getItem(TOKEN_KEY);
205
251
  const userStr = this.storage.getItem(USER_KEY);
206
- if (!token || !userStr) {
207
- return null;
252
+ if (token && userStr) {
253
+ try {
254
+ this.accessToken = token;
255
+ this.user = JSON.parse(userStr);
256
+ } catch {
257
+ this.clearSession();
258
+ }
208
259
  }
209
- try {
210
- const user = JSON.parse(userStr);
211
- return { accessToken: token, user };
212
- } catch {
213
- this.clearSession();
214
- return null;
260
+ }
261
+ /**
262
+ * Save session (memory always, localStorage only in storage mode)
263
+ */
264
+ saveSession(session) {
265
+ this.accessToken = session.accessToken;
266
+ this.user = session.user;
267
+ if (this._mode === "storage") {
268
+ this.storage.setItem(TOKEN_KEY, session.accessToken);
269
+ this.storage.setItem(USER_KEY, JSON.stringify(session.user));
215
270
  }
216
271
  }
272
+ /**
273
+ * Get current session
274
+ */
275
+ getSession() {
276
+ this.loadFromStorage();
277
+ if (!this.accessToken || !this.user) return null;
278
+ return {
279
+ accessToken: this.accessToken,
280
+ user: this.user
281
+ };
282
+ }
283
+ /**
284
+ * Get access token
285
+ */
217
286
  getAccessToken() {
218
- const token = this.storage.getItem(TOKEN_KEY);
219
- return typeof token === "string" ? token : null;
287
+ this.loadFromStorage();
288
+ return this.accessToken;
220
289
  }
290
+ /**
291
+ * Set access token
292
+ */
293
+ setAccessToken(token) {
294
+ this.accessToken = token;
295
+ if (this._mode === "storage") {
296
+ this.storage.setItem(TOKEN_KEY, token);
297
+ }
298
+ }
299
+ /**
300
+ * Get user
301
+ */
302
+ getUser() {
303
+ return this.user;
304
+ }
305
+ /**
306
+ * Set user
307
+ */
308
+ setUser(user) {
309
+ this.user = user;
310
+ if (this._mode === "storage") {
311
+ this.storage.setItem(USER_KEY, JSON.stringify(user));
312
+ }
313
+ }
314
+ /**
315
+ * Clear session (both memory and localStorage)
316
+ */
221
317
  clearSession() {
318
+ this.accessToken = null;
319
+ this.user = null;
222
320
  this.storage.removeItem(TOKEN_KEY);
223
321
  this.storage.removeItem(USER_KEY);
224
322
  }
225
- };
226
-
227
- // src/modules/database-postgrest.ts
228
- var import_postgrest_js = require("@supabase/postgrest-js");
229
- function createInsForgePostgrestFetch(httpClient, tokenManager) {
230
- return async (input, init) => {
231
- const url = typeof input === "string" ? input : input.toString();
232
- const urlObj = new URL(url);
233
- const tableName = urlObj.pathname.slice(1);
234
- const insforgeUrl = `${httpClient.baseUrl}/api/database/records/${tableName}${urlObj.search}`;
235
- const token = tokenManager.getAccessToken();
236
- const httpHeaders = httpClient.getHeaders();
237
- const authToken = token || httpHeaders["Authorization"]?.replace("Bearer ", "");
238
- const headers = new Headers(init?.headers);
239
- if (authToken && !headers.has("Authorization")) {
240
- headers.set("Authorization", `Bearer ${authToken}`);
241
- }
242
- const response = await fetch(insforgeUrl, {
243
- ...init,
244
- headers
245
- });
246
- return response;
247
- };
248
- }
249
- var Database = class {
250
- constructor(httpClient, tokenManager) {
251
- this.postgrest = new import_postgrest_js.PostgrestClient("http://dummy", {
252
- fetch: createInsForgePostgrestFetch(httpClient, tokenManager),
253
- headers: {}
254
- });
255
- }
256
323
  /**
257
- * Create a query builder for a table
258
- *
259
- * @example
260
- * // Basic query
261
- * const { data, error } = await client.database
262
- * .from('posts')
263
- * .select('*')
264
- * .eq('user_id', userId);
265
- *
266
- * // With count (Supabase style!)
267
- * const { data, error, count } = await client.database
268
- * .from('posts')
269
- * .select('*', { count: 'exact' })
270
- * .range(0, 9);
271
- *
272
- * // Just get count, no data
273
- * const { count } = await client.database
274
- * .from('posts')
275
- * .select('*', { count: 'exact', head: true });
276
- *
277
- * // Complex queries with OR
278
- * const { data } = await client.database
279
- * .from('posts')
280
- * .select('*, users!inner(*)')
281
- * .or('status.eq.active,status.eq.pending');
282
- *
283
- * // All features work:
284
- * - Nested selects
285
- * - Foreign key expansion
286
- * - OR/AND/NOT conditions
287
- * - Count with head
288
- * - Range pagination
289
- * - Upserts
324
+ * Check if there's a session in localStorage (for legacy detection)
290
325
  */
291
- from(table) {
292
- return this.postgrest.from(table);
326
+ hasStoredSession() {
327
+ const token = this.storage.getItem(TOKEN_KEY);
328
+ return !!token;
293
329
  }
294
330
  };
295
331
 
296
332
  // src/modules/auth.ts
297
- function convertDbProfileToCamelCase(dbProfile) {
298
- const result = {
299
- id: dbProfile.id
300
- };
301
- Object.keys(dbProfile).forEach((key) => {
302
- result[key] = dbProfile[key];
303
- if (key.includes("_")) {
304
- const camelKey = key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
305
- result[camelKey] = dbProfile[key];
306
- }
307
- });
308
- return result;
309
- }
310
- function convertCamelCaseToDbProfile(profile) {
311
- const dbProfile = {};
312
- Object.keys(profile).forEach((key) => {
313
- if (profile[key] === void 0) return;
314
- const snakeKey = key.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
315
- dbProfile[snakeKey] = profile[key];
316
- });
317
- return dbProfile;
318
- }
319
333
  function isHostedAuthEnvironment() {
320
334
  if (typeof window === "undefined") {
321
335
  return false;
@@ -333,15 +347,14 @@ var Auth = class {
333
347
  constructor(http, tokenManager) {
334
348
  this.http = http;
335
349
  this.tokenManager = tokenManager;
336
- this.database = new Database(http, tokenManager);
337
350
  this.detectAuthCallback();
338
351
  }
339
352
  /**
340
353
  * Automatically detect and handle OAuth callback parameters in the URL
341
- * This runs on initialization to seamlessly complete the OAuth flow
354
+ * This runs after initialization to seamlessly complete the OAuth flow
342
355
  * Matches the backend's OAuth callback response (backend/src/api/routes/auth.ts:540-544)
343
356
  */
344
- detectAuthCallback() {
357
+ async detectAuthCallback() {
345
358
  if (typeof window === "undefined") return;
346
359
  try {
347
360
  const params = new URLSearchParams(window.location.search);
@@ -349,13 +362,20 @@ var Auth = class {
349
362
  const userId = params.get("user_id");
350
363
  const email = params.get("email");
351
364
  const name = params.get("name");
365
+ const csrfToken = params.get("csrf_token");
352
366
  if (accessToken && userId && email) {
367
+ if (csrfToken) {
368
+ this.tokenManager.setMemoryMode();
369
+ setCsrfToken(csrfToken);
370
+ }
371
+ const { data: profileData } = await this.getProfile(userId);
353
372
  const session = {
354
373
  accessToken,
355
374
  user: {
356
375
  id: userId,
357
376
  email,
358
- name: name || "",
377
+ profile: profileData?.profile || { name: name || "" },
378
+ metadata: null,
359
379
  // These fields are not provided by backend OAuth callback
360
380
  // They'll be populated when calling getCurrentUser()
361
381
  emailVerified: false,
@@ -370,6 +390,7 @@ var Auth = class {
370
390
  url.searchParams.delete("user_id");
371
391
  url.searchParams.delete("email");
372
392
  url.searchParams.delete("name");
393
+ url.searchParams.delete("csrf_token");
373
394
  if (params.has("error")) {
374
395
  url.searchParams.delete("error");
375
396
  }
@@ -385,15 +406,16 @@ var Auth = class {
385
406
  async signUp(request) {
386
407
  try {
387
408
  const response = await this.http.post("/api/auth/users", request);
388
- if (response.accessToken && response.user) {
409
+ if (response.accessToken && response.user && !isHostedAuthEnvironment()) {
389
410
  const session = {
390
411
  accessToken: response.accessToken,
391
412
  user: response.user
392
413
  };
393
- if (!isHostedAuthEnvironment()) {
394
- this.tokenManager.saveSession(session);
395
- }
414
+ this.tokenManager.saveSession(session);
396
415
  this.http.setAuthToken(response.accessToken);
416
+ if (response.csrfToken) {
417
+ setCsrfToken(response.csrfToken);
418
+ }
397
419
  }
398
420
  return {
399
421
  data: response,
@@ -419,21 +441,17 @@ var Auth = class {
419
441
  async signInWithPassword(request) {
420
442
  try {
421
443
  const response = await this.http.post("/api/auth/sessions", request);
422
- const session = {
423
- accessToken: response.accessToken || "",
424
- user: response.user || {
425
- id: "",
426
- email: "",
427
- name: "",
428
- emailVerified: false,
429
- createdAt: "",
430
- updatedAt: ""
431
- }
432
- };
433
444
  if (!isHostedAuthEnvironment()) {
445
+ const session = {
446
+ accessToken: response.accessToken,
447
+ user: response.user
448
+ };
434
449
  this.tokenManager.saveSession(session);
450
+ this.http.setAuthToken(response.accessToken);
451
+ if (response.csrfToken) {
452
+ setCsrfToken(response.csrfToken);
453
+ }
435
454
  }
436
- this.http.setAuthToken(response.accessToken || "");
437
455
  return {
438
456
  data: response,
439
457
  error: null
@@ -491,8 +509,13 @@ var Auth = class {
491
509
  */
492
510
  async signOut() {
493
511
  try {
512
+ try {
513
+ await this.http.post("/api/auth/logout", void 0, { credentials: "include" });
514
+ } catch {
515
+ }
494
516
  this.tokenManager.clearSession();
495
517
  this.http.setAuthToken(null);
518
+ clearCsrfToken();
496
519
  return { error: null };
497
520
  } catch (error) {
498
521
  return {
@@ -553,14 +576,9 @@ var Auth = class {
553
576
  }
554
577
  this.http.setAuthToken(session.accessToken);
555
578
  const authResponse = await this.http.get("/api/auth/sessions/current");
556
- const { data: profile, error: profileError } = await this.database.from("users").select("*").eq("id", authResponse.user.id).single();
557
- if (profileError && profileError.code !== "PGRST116") {
558
- return { data: null, error: profileError };
559
- }
560
579
  return {
561
580
  data: {
562
- user: authResponse.user,
563
- profile: profile ? convertDbProfileToCamelCase(profile) : null
581
+ user: authResponse.user
564
582
  },
565
583
  error: null
566
584
  };
@@ -584,29 +602,80 @@ var Auth = class {
584
602
  }
585
603
  /**
586
604
  * Get any user's profile by ID
587
- * Returns profile information from the users table (dynamic fields)
605
+ * Returns profile information from the users table
588
606
  */
589
607
  async getProfile(userId) {
590
- const { data, error } = await this.database.from("users").select("*").eq("id", userId).single();
591
- if (error && error.code === "PGRST116") {
592
- return { data: null, error: null };
593
- }
594
- if (data) {
595
- return { data: convertDbProfileToCamelCase(data), error: null };
608
+ try {
609
+ const response = await this.http.get(`/api/auth/profiles/${userId}`);
610
+ return {
611
+ data: response,
612
+ error: null
613
+ };
614
+ } catch (error) {
615
+ if (error instanceof InsForgeError) {
616
+ return { data: null, error };
617
+ }
618
+ return {
619
+ data: null,
620
+ error: new InsForgeError(
621
+ "An unexpected error occurred while fetching user profile",
622
+ 500,
623
+ "UNEXPECTED_ERROR"
624
+ )
625
+ };
596
626
  }
597
- return { data: null, error };
598
627
  }
599
628
  /**
600
629
  * Get the current session (only session data, no API call)
601
630
  * Returns the stored JWT token and basic user info from local storage
602
631
  */
603
- getCurrentSession() {
632
+ async getCurrentSession() {
604
633
  try {
605
634
  const session = this.tokenManager.getSession();
606
- if (session?.accessToken) {
635
+ if (session) {
607
636
  this.http.setAuthToken(session.accessToken);
608
637
  return { data: { session }, error: null };
609
638
  }
639
+ if (typeof window !== "undefined") {
640
+ try {
641
+ const csrfToken = getCsrfToken();
642
+ const response = await this.http.post(
643
+ "/api/auth/refresh",
644
+ void 0,
645
+ {
646
+ headers: csrfToken ? { "X-CSRF-Token": csrfToken } : {},
647
+ credentials: "include"
648
+ }
649
+ );
650
+ if (response.accessToken) {
651
+ this.tokenManager.setMemoryMode();
652
+ this.tokenManager.setAccessToken(response.accessToken);
653
+ this.http.setAuthToken(response.accessToken);
654
+ if (response.user) {
655
+ this.tokenManager.setUser(response.user);
656
+ }
657
+ if (response.csrfToken) {
658
+ setCsrfToken(response.csrfToken);
659
+ }
660
+ return {
661
+ data: { session: this.tokenManager.getSession() },
662
+ error: null
663
+ };
664
+ }
665
+ } catch (error) {
666
+ if (error instanceof InsForgeError) {
667
+ if (error.statusCode === 404) {
668
+ this.tokenManager.setStorageMode();
669
+ const session2 = this.tokenManager.getSession();
670
+ if (session2) {
671
+ return { data: { session: session2 }, error: null };
672
+ }
673
+ return { data: { session: null }, error: null };
674
+ }
675
+ return { data: { session: null }, error };
676
+ }
677
+ }
678
+ }
610
679
  return { data: { session: null }, error: null };
611
680
  } catch (error) {
612
681
  if (error instanceof InsForgeError) {
@@ -625,42 +694,31 @@ var Auth = class {
625
694
  /**
626
695
  * Set/Update the current user's profile
627
696
  * Updates profile information in the users table (supports any dynamic fields)
697
+ * Requires authentication
628
698
  */
629
699
  async setProfile(profile) {
630
- const session = this.tokenManager.getSession();
631
- if (!session?.accessToken) {
700
+ try {
701
+ const response = await this.http.patch(
702
+ "/api/auth/profiles/current",
703
+ { profile }
704
+ );
705
+ return {
706
+ data: response,
707
+ error: null
708
+ };
709
+ } catch (error) {
710
+ if (error instanceof InsForgeError) {
711
+ return { data: null, error };
712
+ }
632
713
  return {
633
714
  data: null,
634
715
  error: new InsForgeError(
635
- "No authenticated user found",
636
- 401,
637
- "UNAUTHENTICATED"
716
+ "An unexpected error occurred while updating user profile",
717
+ 500,
718
+ "UNEXPECTED_ERROR"
638
719
  )
639
720
  };
640
721
  }
641
- if (!session.user?.id) {
642
- const { data: data2, error: error2 } = await this.getCurrentUser();
643
- if (error2) {
644
- return { data: null, error: error2 };
645
- }
646
- if (data2?.user) {
647
- session.user = {
648
- id: data2.user.id,
649
- email: data2.user.email,
650
- name: "",
651
- emailVerified: false,
652
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
653
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
654
- };
655
- this.tokenManager.saveSession(session);
656
- }
657
- }
658
- const dbProfile = convertCamelCaseToDbProfile(profile);
659
- const { data, error } = await this.database.from("users").update(dbProfile).eq("id", session.user.id).select().single();
660
- if (data) {
661
- return { data: convertDbProfileToCamelCase(data), error: null };
662
- }
663
- return { data: null, error };
664
722
  }
665
723
  /**
666
724
  * Send email verification (code or link based on config)
@@ -814,13 +872,16 @@ var Auth = class {
814
872
  "/api/auth/email/verify",
815
873
  request
816
874
  );
817
- if (response.accessToken) {
875
+ if (!isHostedAuthEnvironment()) {
818
876
  const session = {
819
877
  accessToken: response.accessToken,
820
- user: response.user || {}
878
+ user: response.user
821
879
  };
822
880
  this.tokenManager.saveSession(session);
823
881
  this.http.setAuthToken(response.accessToken);
882
+ if (response.csrfToken) {
883
+ setCsrfToken(response.csrfToken);
884
+ }
824
885
  }
825
886
  return {
826
887
  data: response,
@@ -842,6 +903,75 @@ var Auth = class {
842
903
  }
843
904
  };
844
905
 
906
+ // src/modules/database-postgrest.ts
907
+ var import_postgrest_js = require("@supabase/postgrest-js");
908
+ function createInsForgePostgrestFetch(httpClient, tokenManager) {
909
+ return async (input, init) => {
910
+ const url = typeof input === "string" ? input : input.toString();
911
+ const urlObj = new URL(url);
912
+ const tableName = urlObj.pathname.slice(1);
913
+ const insforgeUrl = `${httpClient.baseUrl}/api/database/records/${tableName}${urlObj.search}`;
914
+ const token = tokenManager.getAccessToken();
915
+ const httpHeaders = httpClient.getHeaders();
916
+ const authToken = token || httpHeaders["Authorization"]?.replace("Bearer ", "");
917
+ const headers = new Headers(init?.headers);
918
+ if (authToken && !headers.has("Authorization")) {
919
+ headers.set("Authorization", `Bearer ${authToken}`);
920
+ }
921
+ const response = await fetch(insforgeUrl, {
922
+ ...init,
923
+ headers
924
+ });
925
+ return response;
926
+ };
927
+ }
928
+ var Database = class {
929
+ constructor(httpClient, tokenManager) {
930
+ this.postgrest = new import_postgrest_js.PostgrestClient("http://dummy", {
931
+ fetch: createInsForgePostgrestFetch(httpClient, tokenManager),
932
+ headers: {}
933
+ });
934
+ }
935
+ /**
936
+ * Create a query builder for a table
937
+ *
938
+ * @example
939
+ * // Basic query
940
+ * const { data, error } = await client.database
941
+ * .from('posts')
942
+ * .select('*')
943
+ * .eq('user_id', userId);
944
+ *
945
+ * // With count (Supabase style!)
946
+ * const { data, error, count } = await client.database
947
+ * .from('posts')
948
+ * .select('*', { count: 'exact' })
949
+ * .range(0, 9);
950
+ *
951
+ * // Just get count, no data
952
+ * const { count } = await client.database
953
+ * .from('posts')
954
+ * .select('*', { count: 'exact', head: true });
955
+ *
956
+ * // Complex queries with OR
957
+ * const { data } = await client.database
958
+ * .from('posts')
959
+ * .select('*, users!inner(*)')
960
+ * .or('status.eq.active,status.eq.pending');
961
+ *
962
+ * // All features work:
963
+ * - Nested selects
964
+ * - Foreign key expansion
965
+ * - OR/AND/NOT conditions
966
+ * - Count with head
967
+ * - Range pagination
968
+ * - Upserts
969
+ */
970
+ from(table) {
971
+ return this.postgrest.from(table);
972
+ }
973
+ };
974
+
845
975
  // src/modules/storage.ts
846
976
  var StorageBucket = class {
847
977
  constructor(bucketName, http) {
@@ -1636,10 +1766,7 @@ var InsForgeClient = class {
1636
1766
  if (existingSession?.accessToken) {
1637
1767
  this.http.setAuthToken(existingSession.accessToken);
1638
1768
  }
1639
- this.auth = new Auth(
1640
- this.http,
1641
- this.tokenManager
1642
- );
1769
+ this.auth = new Auth(this.http, this.tokenManager);
1643
1770
  this.database = new Database(this.http, this.tokenManager);
1644
1771
  this.storage = new Storage(this.http);
1645
1772
  this.ai = new AI(this.http);