@insforge/sdk 1.0.2 → 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.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,15 +308,14 @@ 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
- detectAuthCallback() {
318
+ async detectAuthCallback() {
306
319
  if (typeof window === "undefined") return;
307
320
  try {
308
321
  const params = new URLSearchParams(window.location.search);
@@ -310,13 +323,20 @@ 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
+ }
332
+ const { data: profileData } = await this.getProfile(userId);
314
333
  const session = {
315
334
  accessToken,
316
335
  user: {
317
336
  id: userId,
318
337
  email,
319
- name: name || "",
338
+ profile: profileData?.profile || { name: name || "" },
339
+ metadata: null,
320
340
  // These fields are not provided by backend OAuth callback
321
341
  // They'll be populated when calling getCurrentUser()
322
342
  emailVerified: false,
@@ -331,6 +351,7 @@ var Auth = class {
331
351
  url.searchParams.delete("user_id");
332
352
  url.searchParams.delete("email");
333
353
  url.searchParams.delete("name");
354
+ url.searchParams.delete("csrf_token");
334
355
  if (params.has("error")) {
335
356
  url.searchParams.delete("error");
336
357
  }
@@ -346,15 +367,16 @@ var Auth = class {
346
367
  async signUp(request) {
347
368
  try {
348
369
  const response = await this.http.post("/api/auth/users", request);
349
- if (response.accessToken && response.user) {
370
+ if (response.accessToken && response.user && !isHostedAuthEnvironment()) {
350
371
  const session = {
351
372
  accessToken: response.accessToken,
352
373
  user: response.user
353
374
  };
354
- if (!isHostedAuthEnvironment()) {
355
- this.tokenManager.saveSession(session);
356
- }
375
+ this.tokenManager.saveSession(session);
357
376
  this.http.setAuthToken(response.accessToken);
377
+ if (response.csrfToken) {
378
+ setCsrfToken(response.csrfToken);
379
+ }
358
380
  }
359
381
  return {
360
382
  data: response,
@@ -380,21 +402,17 @@ var Auth = class {
380
402
  async signInWithPassword(request) {
381
403
  try {
382
404
  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
405
  if (!isHostedAuthEnvironment()) {
406
+ const session = {
407
+ accessToken: response.accessToken,
408
+ user: response.user
409
+ };
395
410
  this.tokenManager.saveSession(session);
411
+ this.http.setAuthToken(response.accessToken);
412
+ if (response.csrfToken) {
413
+ setCsrfToken(response.csrfToken);
414
+ }
396
415
  }
397
- this.http.setAuthToken(response.accessToken || "");
398
416
  return {
399
417
  data: response,
400
418
  error: null
@@ -452,8 +470,13 @@ var Auth = class {
452
470
  */
453
471
  async signOut() {
454
472
  try {
473
+ try {
474
+ await this.http.post("/api/auth/logout", void 0, { credentials: "include" });
475
+ } catch {
476
+ }
455
477
  this.tokenManager.clearSession();
456
478
  this.http.setAuthToken(null);
479
+ clearCsrfToken();
457
480
  return { error: null };
458
481
  } catch (error) {
459
482
  return {
@@ -514,14 +537,9 @@ var Auth = class {
514
537
  }
515
538
  this.http.setAuthToken(session.accessToken);
516
539
  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
540
  return {
522
541
  data: {
523
- user: authResponse.user,
524
- profile: profile ? convertDbProfileToCamelCase(profile) : null
542
+ user: authResponse.user
525
543
  },
526
544
  error: null
527
545
  };
@@ -545,29 +563,80 @@ var Auth = class {
545
563
  }
546
564
  /**
547
565
  * Get any user's profile by ID
548
- * Returns profile information from the users table (dynamic fields)
566
+ * Returns profile information from the users table
549
567
  */
550
568
  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 };
569
+ try {
570
+ const response = await this.http.get(`/api/auth/profiles/${userId}`);
571
+ return {
572
+ data: response,
573
+ error: null
574
+ };
575
+ } catch (error) {
576
+ if (error instanceof InsForgeError) {
577
+ return { data: null, error };
578
+ }
579
+ return {
580
+ data: null,
581
+ error: new InsForgeError(
582
+ "An unexpected error occurred while fetching user profile",
583
+ 500,
584
+ "UNEXPECTED_ERROR"
585
+ )
586
+ };
557
587
  }
558
- return { data: null, error };
559
588
  }
560
589
  /**
561
590
  * Get the current session (only session data, no API call)
562
591
  * Returns the stored JWT token and basic user info from local storage
563
592
  */
564
- getCurrentSession() {
593
+ async getCurrentSession() {
565
594
  try {
566
595
  const session = this.tokenManager.getSession();
567
- if (session?.accessToken) {
596
+ if (session) {
568
597
  this.http.setAuthToken(session.accessToken);
569
598
  return { data: { session }, error: null };
570
599
  }
600
+ if (typeof window !== "undefined") {
601
+ try {
602
+ const csrfToken = getCsrfToken();
603
+ const response = await this.http.post(
604
+ "/api/auth/refresh",
605
+ void 0,
606
+ {
607
+ headers: csrfToken ? { "X-CSRF-Token": csrfToken } : {},
608
+ credentials: "include"
609
+ }
610
+ );
611
+ if (response.accessToken) {
612
+ this.tokenManager.setMemoryMode();
613
+ this.tokenManager.setAccessToken(response.accessToken);
614
+ this.http.setAuthToken(response.accessToken);
615
+ if (response.user) {
616
+ this.tokenManager.setUser(response.user);
617
+ }
618
+ if (response.csrfToken) {
619
+ setCsrfToken(response.csrfToken);
620
+ }
621
+ return {
622
+ data: { session: this.tokenManager.getSession() },
623
+ error: null
624
+ };
625
+ }
626
+ } catch (error) {
627
+ if (error instanceof InsForgeError) {
628
+ if (error.statusCode === 404) {
629
+ this.tokenManager.setStorageMode();
630
+ const session2 = this.tokenManager.getSession();
631
+ if (session2) {
632
+ return { data: { session: session2 }, error: null };
633
+ }
634
+ return { data: { session: null }, error: null };
635
+ }
636
+ return { data: { session: null }, error };
637
+ }
638
+ }
639
+ }
571
640
  return { data: { session: null }, error: null };
572
641
  } catch (error) {
573
642
  if (error instanceof InsForgeError) {
@@ -586,42 +655,31 @@ var Auth = class {
586
655
  /**
587
656
  * Set/Update the current user's profile
588
657
  * Updates profile information in the users table (supports any dynamic fields)
658
+ * Requires authentication
589
659
  */
590
660
  async setProfile(profile) {
591
- const session = this.tokenManager.getSession();
592
- if (!session?.accessToken) {
661
+ try {
662
+ const response = await this.http.patch(
663
+ "/api/auth/profiles/current",
664
+ { profile }
665
+ );
666
+ return {
667
+ data: response,
668
+ error: null
669
+ };
670
+ } catch (error) {
671
+ if (error instanceof InsForgeError) {
672
+ return { data: null, error };
673
+ }
593
674
  return {
594
675
  data: null,
595
676
  error: new InsForgeError(
596
- "No authenticated user found",
597
- 401,
598
- "UNAUTHENTICATED"
677
+ "An unexpected error occurred while updating user profile",
678
+ 500,
679
+ "UNEXPECTED_ERROR"
599
680
  )
600
681
  };
601
682
  }
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
683
  }
626
684
  /**
627
685
  * Send email verification (code or link based on config)
@@ -775,13 +833,16 @@ var Auth = class {
775
833
  "/api/auth/email/verify",
776
834
  request
777
835
  );
778
- if (response.accessToken) {
836
+ if (!isHostedAuthEnvironment()) {
779
837
  const session = {
780
838
  accessToken: response.accessToken,
781
- user: response.user || {}
839
+ user: response.user
782
840
  };
783
841
  this.tokenManager.saveSession(session);
784
842
  this.http.setAuthToken(response.accessToken);
843
+ if (response.csrfToken) {
844
+ setCsrfToken(response.csrfToken);
845
+ }
785
846
  }
786
847
  return {
787
848
  data: response,
@@ -803,6 +864,75 @@ var Auth = class {
803
864
  }
804
865
  };
805
866
 
867
+ // src/modules/database-postgrest.ts
868
+ import { PostgrestClient } from "@supabase/postgrest-js";
869
+ function createInsForgePostgrestFetch(httpClient, tokenManager) {
870
+ return async (input, init) => {
871
+ const url = typeof input === "string" ? input : input.toString();
872
+ const urlObj = new URL(url);
873
+ const tableName = urlObj.pathname.slice(1);
874
+ const insforgeUrl = `${httpClient.baseUrl}/api/database/records/${tableName}${urlObj.search}`;
875
+ const token = tokenManager.getAccessToken();
876
+ const httpHeaders = httpClient.getHeaders();
877
+ const authToken = token || httpHeaders["Authorization"]?.replace("Bearer ", "");
878
+ const headers = new Headers(init?.headers);
879
+ if (authToken && !headers.has("Authorization")) {
880
+ headers.set("Authorization", `Bearer ${authToken}`);
881
+ }
882
+ const response = await fetch(insforgeUrl, {
883
+ ...init,
884
+ headers
885
+ });
886
+ return response;
887
+ };
888
+ }
889
+ var Database = class {
890
+ constructor(httpClient, tokenManager) {
891
+ this.postgrest = new PostgrestClient("http://dummy", {
892
+ fetch: createInsForgePostgrestFetch(httpClient, tokenManager),
893
+ headers: {}
894
+ });
895
+ }
896
+ /**
897
+ * Create a query builder for a table
898
+ *
899
+ * @example
900
+ * // Basic query
901
+ * const { data, error } = await client.database
902
+ * .from('posts')
903
+ * .select('*')
904
+ * .eq('user_id', userId);
905
+ *
906
+ * // With count (Supabase style!)
907
+ * const { data, error, count } = await client.database
908
+ * .from('posts')
909
+ * .select('*', { count: 'exact' })
910
+ * .range(0, 9);
911
+ *
912
+ * // Just get count, no data
913
+ * const { count } = await client.database
914
+ * .from('posts')
915
+ * .select('*', { count: 'exact', head: true });
916
+ *
917
+ * // Complex queries with OR
918
+ * const { data } = await client.database
919
+ * .from('posts')
920
+ * .select('*, users!inner(*)')
921
+ * .or('status.eq.active,status.eq.pending');
922
+ *
923
+ * // All features work:
924
+ * - Nested selects
925
+ * - Foreign key expansion
926
+ * - OR/AND/NOT conditions
927
+ * - Count with head
928
+ * - Range pagination
929
+ * - Upserts
930
+ */
931
+ from(table) {
932
+ return this.postgrest.from(table);
933
+ }
934
+ };
935
+
806
936
  // src/modules/storage.ts
807
937
  var StorageBucket = class {
808
938
  constructor(bucketName, http) {
@@ -1564,7 +1694,7 @@ var Emails = class {
1564
1694
  }
1565
1695
  /**
1566
1696
  * Send a custom HTML email
1567
- * @param options Email options ilncluding recipients, subject, and HTML content
1697
+ * @param options Email options including recipients, subject, and HTML content
1568
1698
  */
1569
1699
  async send(options) {
1570
1700
  try {
@@ -1597,10 +1727,7 @@ var InsForgeClient = class {
1597
1727
  if (existingSession?.accessToken) {
1598
1728
  this.http.setAuthToken(existingSession.accessToken);
1599
1729
  }
1600
- this.auth = new Auth(
1601
- this.http,
1602
- this.tokenManager
1603
- );
1730
+ this.auth = new Auth(this.http, this.tokenManager);
1604
1731
  this.database = new Database(this.http, this.tokenManager);
1605
1732
  this.storage = new Storage(this.http);
1606
1733
  this.ai = new AI(this.http);