@insforge/sdk 1.0.2 → 1.0.3-dev.1

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
@@ -138,8 +138,31 @@ var HttpClient = class {
138
138
  // src/lib/token-manager.ts
139
139
  var TOKEN_KEY = "insforge-auth-token";
140
140
  var USER_KEY = "insforge-auth-user";
141
+ var CSRF_TOKEN_COOKIE = "insforge_csrf_token";
142
+ function getCsrfToken() {
143
+ if (typeof document === "undefined") return null;
144
+ const match = document.cookie.split(";").find((c) => c.trim().startsWith(`${CSRF_TOKEN_COOKIE}=`));
145
+ if (!match) return null;
146
+ return match.split("=")[1] || null;
147
+ }
148
+ function setCsrfToken(token) {
149
+ if (typeof document === "undefined") return;
150
+ const maxAge = 7 * 24 * 60 * 60;
151
+ const secure = typeof window !== "undefined" && window.location.protocol === "https:" ? "; Secure" : "";
152
+ document.cookie = `${CSRF_TOKEN_COOKIE}=${encodeURIComponent(token)}; path=/; max-age=${maxAge}; SameSite=Lax${secure}`;
153
+ }
154
+ function clearCsrfToken() {
155
+ if (typeof document === "undefined") return;
156
+ const secure = typeof window !== "undefined" && window.location.protocol === "https:" ? "; Secure" : "";
157
+ document.cookie = `${CSRF_TOKEN_COOKIE}=; path=/; max-age=0; SameSite=Lax${secure}`;
158
+ }
141
159
  var TokenManager = class {
142
160
  constructor(storage) {
161
+ // In-memory storage
162
+ this.accessToken = null;
163
+ this.user = null;
164
+ // Mode: 'memory' (new backend) or 'storage' (legacy backend, default)
165
+ this._mode = "storage";
143
166
  if (storage) {
144
167
  this.storage = storage;
145
168
  } else if (typeof window !== "undefined" && window.localStorage) {
@@ -157,126 +180,117 @@ var TokenManager = class {
157
180
  };
158
181
  }
159
182
  }
160
- saveSession(session) {
161
- this.storage.setItem(TOKEN_KEY, session.accessToken);
162
- this.storage.setItem(USER_KEY, JSON.stringify(session.user));
183
+ /**
184
+ * Get current mode
185
+ */
186
+ get mode() {
187
+ return this._mode;
163
188
  }
164
- getSession() {
189
+ /**
190
+ * Set mode to memory (new backend with cookies + memory)
191
+ */
192
+ setMemoryMode() {
193
+ if (this._mode === "storage") {
194
+ this.storage.removeItem(TOKEN_KEY);
195
+ this.storage.removeItem(USER_KEY);
196
+ }
197
+ this._mode = "memory";
198
+ }
199
+ /**
200
+ * Set mode to storage (legacy backend with localStorage)
201
+ * Also loads existing session from localStorage
202
+ */
203
+ setStorageMode() {
204
+ this._mode = "storage";
205
+ this.loadFromStorage();
206
+ }
207
+ /**
208
+ * Load session from localStorage
209
+ */
210
+ loadFromStorage() {
165
211
  const token = this.storage.getItem(TOKEN_KEY);
166
212
  const userStr = this.storage.getItem(USER_KEY);
167
- if (!token || !userStr) {
168
- return null;
213
+ if (token && userStr) {
214
+ try {
215
+ this.accessToken = token;
216
+ this.user = JSON.parse(userStr);
217
+ } catch {
218
+ this.clearSession();
219
+ }
169
220
  }
170
- try {
171
- const user = JSON.parse(userStr);
172
- return { accessToken: token, user };
173
- } catch {
174
- this.clearSession();
175
- return null;
221
+ }
222
+ /**
223
+ * Save session (memory always, localStorage only in storage mode)
224
+ */
225
+ saveSession(session) {
226
+ this.accessToken = session.accessToken;
227
+ this.user = session.user;
228
+ if (this._mode === "storage") {
229
+ this.storage.setItem(TOKEN_KEY, session.accessToken);
230
+ this.storage.setItem(USER_KEY, JSON.stringify(session.user));
176
231
  }
177
232
  }
233
+ /**
234
+ * Get current session
235
+ */
236
+ getSession() {
237
+ this.loadFromStorage();
238
+ if (!this.accessToken || !this.user) return null;
239
+ return {
240
+ accessToken: this.accessToken,
241
+ user: this.user
242
+ };
243
+ }
244
+ /**
245
+ * Get access token
246
+ */
178
247
  getAccessToken() {
179
- const token = this.storage.getItem(TOKEN_KEY);
180
- return typeof token === "string" ? token : null;
248
+ this.loadFromStorage();
249
+ return this.accessToken;
181
250
  }
251
+ /**
252
+ * Set access token
253
+ */
254
+ setAccessToken(token) {
255
+ this.accessToken = token;
256
+ if (this._mode === "storage") {
257
+ this.storage.setItem(TOKEN_KEY, token);
258
+ }
259
+ }
260
+ /**
261
+ * Get user
262
+ */
263
+ getUser() {
264
+ return this.user;
265
+ }
266
+ /**
267
+ * Set user
268
+ */
269
+ setUser(user) {
270
+ this.user = user;
271
+ if (this._mode === "storage") {
272
+ this.storage.setItem(USER_KEY, JSON.stringify(user));
273
+ }
274
+ }
275
+ /**
276
+ * Clear session (both memory and localStorage)
277
+ */
182
278
  clearSession() {
279
+ this.accessToken = null;
280
+ this.user = null;
183
281
  this.storage.removeItem(TOKEN_KEY);
184
282
  this.storage.removeItem(USER_KEY);
185
283
  }
186
- };
187
-
188
- // src/modules/database-postgrest.ts
189
- import { PostgrestClient } from "@supabase/postgrest-js";
190
- function createInsForgePostgrestFetch(httpClient, tokenManager) {
191
- return async (input, init) => {
192
- const url = typeof input === "string" ? input : input.toString();
193
- const urlObj = new URL(url);
194
- const tableName = urlObj.pathname.slice(1);
195
- const insforgeUrl = `${httpClient.baseUrl}/api/database/records/${tableName}${urlObj.search}`;
196
- const token = tokenManager.getAccessToken();
197
- const httpHeaders = httpClient.getHeaders();
198
- const authToken = token || httpHeaders["Authorization"]?.replace("Bearer ", "");
199
- const headers = new Headers(init?.headers);
200
- if (authToken && !headers.has("Authorization")) {
201
- headers.set("Authorization", `Bearer ${authToken}`);
202
- }
203
- const response = await fetch(insforgeUrl, {
204
- ...init,
205
- headers
206
- });
207
- return response;
208
- };
209
- }
210
- var Database = class {
211
- constructor(httpClient, tokenManager) {
212
- this.postgrest = new PostgrestClient("http://dummy", {
213
- fetch: createInsForgePostgrestFetch(httpClient, tokenManager),
214
- headers: {}
215
- });
216
- }
217
284
  /**
218
- * Create a query builder for a table
219
- *
220
- * @example
221
- * // Basic query
222
- * const { data, error } = await client.database
223
- * .from('posts')
224
- * .select('*')
225
- * .eq('user_id', userId);
226
- *
227
- * // With count (Supabase style!)
228
- * const { data, error, count } = await client.database
229
- * .from('posts')
230
- * .select('*', { count: 'exact' })
231
- * .range(0, 9);
232
- *
233
- * // Just get count, no data
234
- * const { count } = await client.database
235
- * .from('posts')
236
- * .select('*', { count: 'exact', head: true });
237
- *
238
- * // Complex queries with OR
239
- * const { data } = await client.database
240
- * .from('posts')
241
- * .select('*, users!inner(*)')
242
- * .or('status.eq.active,status.eq.pending');
243
- *
244
- * // All features work:
245
- * - Nested selects
246
- * - Foreign key expansion
247
- * - OR/AND/NOT conditions
248
- * - Count with head
249
- * - Range pagination
250
- * - Upserts
285
+ * Check if there's a session in localStorage (for legacy detection)
251
286
  */
252
- from(table) {
253
- return this.postgrest.from(table);
287
+ hasStoredSession() {
288
+ const token = this.storage.getItem(TOKEN_KEY);
289
+ return !!token;
254
290
  }
255
291
  };
256
292
 
257
293
  // src/modules/auth.ts
258
- function convertDbProfileToCamelCase(dbProfile) {
259
- const result = {
260
- id: dbProfile.id
261
- };
262
- Object.keys(dbProfile).forEach((key) => {
263
- result[key] = dbProfile[key];
264
- if (key.includes("_")) {
265
- const camelKey = key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
266
- result[camelKey] = dbProfile[key];
267
- }
268
- });
269
- return result;
270
- }
271
- function convertCamelCaseToDbProfile(profile) {
272
- const dbProfile = {};
273
- Object.keys(profile).forEach((key) => {
274
- if (profile[key] === void 0) return;
275
- const snakeKey = key.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
276
- dbProfile[snakeKey] = profile[key];
277
- });
278
- return dbProfile;
279
- }
280
294
  function isHostedAuthEnvironment() {
281
295
  if (typeof window === "undefined") {
282
296
  return false;
@@ -294,12 +308,11 @@ var Auth = class {
294
308
  constructor(http, tokenManager) {
295
309
  this.http = http;
296
310
  this.tokenManager = tokenManager;
297
- this.database = new Database(http, tokenManager);
298
311
  this.detectAuthCallback();
299
312
  }
300
313
  /**
301
314
  * Automatically detect and handle OAuth callback parameters in the URL
302
- * This runs on initialization to seamlessly complete the OAuth flow
315
+ * This runs after initialization to seamlessly complete the OAuth flow
303
316
  * Matches the backend's OAuth callback response (backend/src/api/routes/auth.ts:540-544)
304
317
  */
305
318
  detectAuthCallback() {
@@ -310,13 +323,19 @@ var Auth = class {
310
323
  const userId = params.get("user_id");
311
324
  const email = params.get("email");
312
325
  const name = params.get("name");
326
+ const csrfToken = params.get("csrf_token");
313
327
  if (accessToken && userId && email) {
328
+ if (csrfToken) {
329
+ this.tokenManager.setMemoryMode();
330
+ setCsrfToken(csrfToken);
331
+ }
314
332
  const session = {
315
333
  accessToken,
316
334
  user: {
317
335
  id: userId,
318
336
  email,
319
- name: name || "",
337
+ profile: null,
338
+ metadata: null,
320
339
  // These fields are not provided by backend OAuth callback
321
340
  // They'll be populated when calling getCurrentUser()
322
341
  emailVerified: false,
@@ -331,6 +350,7 @@ var Auth = class {
331
350
  url.searchParams.delete("user_id");
332
351
  url.searchParams.delete("email");
333
352
  url.searchParams.delete("name");
353
+ url.searchParams.delete("csrf_token");
334
354
  if (params.has("error")) {
335
355
  url.searchParams.delete("error");
336
356
  }
@@ -346,15 +366,16 @@ var Auth = class {
346
366
  async signUp(request) {
347
367
  try {
348
368
  const response = await this.http.post("/api/auth/users", request);
349
- if (response.accessToken && response.user) {
369
+ if (response.accessToken && response.user && !isHostedAuthEnvironment()) {
350
370
  const session = {
351
371
  accessToken: response.accessToken,
352
372
  user: response.user
353
373
  };
354
- if (!isHostedAuthEnvironment()) {
355
- this.tokenManager.saveSession(session);
356
- }
374
+ this.tokenManager.saveSession(session);
357
375
  this.http.setAuthToken(response.accessToken);
376
+ if (response.csrfToken) {
377
+ setCsrfToken(response.csrfToken);
378
+ }
358
379
  }
359
380
  return {
360
381
  data: response,
@@ -380,21 +401,17 @@ var Auth = class {
380
401
  async signInWithPassword(request) {
381
402
  try {
382
403
  const response = await this.http.post("/api/auth/sessions", request);
383
- const session = {
384
- accessToken: response.accessToken || "",
385
- user: response.user || {
386
- id: "",
387
- email: "",
388
- name: "",
389
- emailVerified: false,
390
- createdAt: "",
391
- updatedAt: ""
392
- }
393
- };
394
404
  if (!isHostedAuthEnvironment()) {
405
+ const session = {
406
+ accessToken: response.accessToken,
407
+ user: response.user
408
+ };
395
409
  this.tokenManager.saveSession(session);
410
+ this.http.setAuthToken(response.accessToken);
411
+ if (response.csrfToken) {
412
+ setCsrfToken(response.csrfToken);
413
+ }
396
414
  }
397
- this.http.setAuthToken(response.accessToken || "");
398
415
  return {
399
416
  data: response,
400
417
  error: null
@@ -452,8 +469,13 @@ var Auth = class {
452
469
  */
453
470
  async signOut() {
454
471
  try {
472
+ try {
473
+ await this.http.post("/api/auth/logout", void 0, { credentials: "include" });
474
+ } catch {
475
+ }
455
476
  this.tokenManager.clearSession();
456
477
  this.http.setAuthToken(null);
478
+ clearCsrfToken();
457
479
  return { error: null };
458
480
  } catch (error) {
459
481
  return {
@@ -514,14 +536,9 @@ var Auth = class {
514
536
  }
515
537
  this.http.setAuthToken(session.accessToken);
516
538
  const authResponse = await this.http.get("/api/auth/sessions/current");
517
- const { data: profile, error: profileError } = await this.database.from("users").select("*").eq("id", authResponse.user.id).single();
518
- if (profileError && profileError.code !== "PGRST116") {
519
- return { data: null, error: profileError };
520
- }
521
539
  return {
522
540
  data: {
523
- user: authResponse.user,
524
- profile: profile ? convertDbProfileToCamelCase(profile) : null
541
+ user: authResponse.user
525
542
  },
526
543
  error: null
527
544
  };
@@ -545,29 +562,80 @@ var Auth = class {
545
562
  }
546
563
  /**
547
564
  * Get any user's profile by ID
548
- * Returns profile information from the users table (dynamic fields)
565
+ * Returns profile information from the users table
549
566
  */
550
567
  async getProfile(userId) {
551
- const { data, error } = await this.database.from("users").select("*").eq("id", userId).single();
552
- if (error && error.code === "PGRST116") {
553
- return { data: null, error: null };
554
- }
555
- if (data) {
556
- return { data: convertDbProfileToCamelCase(data), error: null };
568
+ try {
569
+ const response = await this.http.get(`/api/auth/profiles/${userId}`);
570
+ return {
571
+ data: response,
572
+ error: null
573
+ };
574
+ } catch (error) {
575
+ if (error instanceof InsForgeError) {
576
+ return { data: null, error };
577
+ }
578
+ return {
579
+ data: null,
580
+ error: new InsForgeError(
581
+ "An unexpected error occurred while fetching user profile",
582
+ 500,
583
+ "UNEXPECTED_ERROR"
584
+ )
585
+ };
557
586
  }
558
- return { data: null, error };
559
587
  }
560
588
  /**
561
589
  * Get the current session (only session data, no API call)
562
590
  * Returns the stored JWT token and basic user info from local storage
563
591
  */
564
- getCurrentSession() {
592
+ async getCurrentSession() {
565
593
  try {
566
594
  const session = this.tokenManager.getSession();
567
- if (session?.accessToken) {
595
+ if (session) {
568
596
  this.http.setAuthToken(session.accessToken);
569
597
  return { data: { session }, error: null };
570
598
  }
599
+ if (typeof window !== "undefined") {
600
+ try {
601
+ const csrfToken = getCsrfToken();
602
+ const response = await this.http.post(
603
+ "/api/auth/refresh",
604
+ void 0,
605
+ {
606
+ headers: csrfToken ? { "X-CSRF-Token": csrfToken } : {},
607
+ credentials: "include"
608
+ }
609
+ );
610
+ if (response.accessToken) {
611
+ this.tokenManager.setMemoryMode();
612
+ this.tokenManager.setAccessToken(response.accessToken);
613
+ this.http.setAuthToken(response.accessToken);
614
+ if (response.user) {
615
+ this.tokenManager.setUser(response.user);
616
+ }
617
+ if (response.csrfToken) {
618
+ setCsrfToken(response.csrfToken);
619
+ }
620
+ return {
621
+ data: { session: this.tokenManager.getSession() },
622
+ error: null
623
+ };
624
+ }
625
+ } catch (error) {
626
+ if (error instanceof InsForgeError) {
627
+ if (error.statusCode === 404) {
628
+ this.tokenManager.setStorageMode();
629
+ const session2 = this.tokenManager.getSession();
630
+ if (session2) {
631
+ return { data: { session: session2 }, error: null };
632
+ }
633
+ return { data: { session: null }, error: null };
634
+ }
635
+ return { data: { session: null }, error };
636
+ }
637
+ }
638
+ }
571
639
  return { data: { session: null }, error: null };
572
640
  } catch (error) {
573
641
  if (error instanceof InsForgeError) {
@@ -586,42 +654,31 @@ var Auth = class {
586
654
  /**
587
655
  * Set/Update the current user's profile
588
656
  * Updates profile information in the users table (supports any dynamic fields)
657
+ * Requires authentication
589
658
  */
590
659
  async setProfile(profile) {
591
- const session = this.tokenManager.getSession();
592
- if (!session?.accessToken) {
660
+ try {
661
+ const response = await this.http.patch(
662
+ "/api/auth/profiles/current",
663
+ { profile }
664
+ );
665
+ return {
666
+ data: response,
667
+ error: null
668
+ };
669
+ } catch (error) {
670
+ if (error instanceof InsForgeError) {
671
+ return { data: null, error };
672
+ }
593
673
  return {
594
674
  data: null,
595
675
  error: new InsForgeError(
596
- "No authenticated user found",
597
- 401,
598
- "UNAUTHENTICATED"
676
+ "An unexpected error occurred while updating user profile",
677
+ 500,
678
+ "UNEXPECTED_ERROR"
599
679
  )
600
680
  };
601
681
  }
602
- if (!session.user?.id) {
603
- const { data: data2, error: error2 } = await this.getCurrentUser();
604
- if (error2) {
605
- return { data: null, error: error2 };
606
- }
607
- if (data2?.user) {
608
- session.user = {
609
- id: data2.user.id,
610
- email: data2.user.email,
611
- name: "",
612
- emailVerified: false,
613
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
614
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
615
- };
616
- this.tokenManager.saveSession(session);
617
- }
618
- }
619
- const dbProfile = convertCamelCaseToDbProfile(profile);
620
- const { data, error } = await this.database.from("users").update(dbProfile).eq("id", session.user.id).select().single();
621
- if (data) {
622
- return { data: convertDbProfileToCamelCase(data), error: null };
623
- }
624
- return { data: null, error };
625
682
  }
626
683
  /**
627
684
  * Send email verification (code or link based on config)
@@ -775,13 +832,16 @@ var Auth = class {
775
832
  "/api/auth/email/verify",
776
833
  request
777
834
  );
778
- if (response.accessToken) {
835
+ if (!isHostedAuthEnvironment()) {
779
836
  const session = {
780
837
  accessToken: response.accessToken,
781
- user: response.user || {}
838
+ user: response.user
782
839
  };
783
840
  this.tokenManager.saveSession(session);
784
841
  this.http.setAuthToken(response.accessToken);
842
+ if (response.csrfToken) {
843
+ setCsrfToken(response.csrfToken);
844
+ }
785
845
  }
786
846
  return {
787
847
  data: response,
@@ -803,6 +863,75 @@ var Auth = class {
803
863
  }
804
864
  };
805
865
 
866
+ // src/modules/database-postgrest.ts
867
+ import { PostgrestClient } from "@supabase/postgrest-js";
868
+ function createInsForgePostgrestFetch(httpClient, tokenManager) {
869
+ return async (input, init) => {
870
+ const url = typeof input === "string" ? input : input.toString();
871
+ const urlObj = new URL(url);
872
+ const tableName = urlObj.pathname.slice(1);
873
+ const insforgeUrl = `${httpClient.baseUrl}/api/database/records/${tableName}${urlObj.search}`;
874
+ const token = tokenManager.getAccessToken();
875
+ const httpHeaders = httpClient.getHeaders();
876
+ const authToken = token || httpHeaders["Authorization"]?.replace("Bearer ", "");
877
+ const headers = new Headers(init?.headers);
878
+ if (authToken && !headers.has("Authorization")) {
879
+ headers.set("Authorization", `Bearer ${authToken}`);
880
+ }
881
+ const response = await fetch(insforgeUrl, {
882
+ ...init,
883
+ headers
884
+ });
885
+ return response;
886
+ };
887
+ }
888
+ var Database = class {
889
+ constructor(httpClient, tokenManager) {
890
+ this.postgrest = new PostgrestClient("http://dummy", {
891
+ fetch: createInsForgePostgrestFetch(httpClient, tokenManager),
892
+ headers: {}
893
+ });
894
+ }
895
+ /**
896
+ * Create a query builder for a table
897
+ *
898
+ * @example
899
+ * // Basic query
900
+ * const { data, error } = await client.database
901
+ * .from('posts')
902
+ * .select('*')
903
+ * .eq('user_id', userId);
904
+ *
905
+ * // With count (Supabase style!)
906
+ * const { data, error, count } = await client.database
907
+ * .from('posts')
908
+ * .select('*', { count: 'exact' })
909
+ * .range(0, 9);
910
+ *
911
+ * // Just get count, no data
912
+ * const { count } = await client.database
913
+ * .from('posts')
914
+ * .select('*', { count: 'exact', head: true });
915
+ *
916
+ * // Complex queries with OR
917
+ * const { data } = await client.database
918
+ * .from('posts')
919
+ * .select('*, users!inner(*)')
920
+ * .or('status.eq.active,status.eq.pending');
921
+ *
922
+ * // All features work:
923
+ * - Nested selects
924
+ * - Foreign key expansion
925
+ * - OR/AND/NOT conditions
926
+ * - Count with head
927
+ * - Range pagination
928
+ * - Upserts
929
+ */
930
+ from(table) {
931
+ return this.postgrest.from(table);
932
+ }
933
+ };
934
+
806
935
  // src/modules/storage.ts
807
936
  var StorageBucket = class {
808
937
  constructor(bucketName, http) {
@@ -1564,7 +1693,7 @@ var Emails = class {
1564
1693
  }
1565
1694
  /**
1566
1695
  * Send a custom HTML email
1567
- * @param options Email options ilncluding recipients, subject, and HTML content
1696
+ * @param options Email options including recipients, subject, and HTML content
1568
1697
  */
1569
1698
  async send(options) {
1570
1699
  try {
@@ -1597,10 +1726,7 @@ var InsForgeClient = class {
1597
1726
  if (existingSession?.accessToken) {
1598
1727
  this.http.setAuthToken(existingSession.accessToken);
1599
1728
  }
1600
- this.auth = new Auth(
1601
- this.http,
1602
- this.tokenManager
1603
- );
1729
+ this.auth = new Auth(this.http, this.tokenManager);
1604
1730
  this.database = new Database(this.http, this.tokenManager);
1605
1731
  this.storage = new Storage(this.http);
1606
1732
  this.ai = new AI(this.http);