@insforge/sdk 1.1.0 → 1.1.2-edge.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
@@ -318,21 +318,74 @@ function isHostedAuthEnvironment() {
318
318
  }
319
319
  return false;
320
320
  }
321
+ var PKCE_VERIFIER_KEY = "insforge_pkce_verifier";
322
+ function generateCodeVerifier() {
323
+ const array = new Uint8Array(32);
324
+ crypto.getRandomValues(array);
325
+ return base64UrlEncode(array);
326
+ }
327
+ async function generateCodeChallenge(verifier) {
328
+ const encoder = new TextEncoder();
329
+ const data = encoder.encode(verifier);
330
+ const hash = await crypto.subtle.digest("SHA-256", data);
331
+ return base64UrlEncode(new Uint8Array(hash));
332
+ }
333
+ function base64UrlEncode(buffer) {
334
+ const base64 = btoa(String.fromCharCode(...buffer));
335
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
336
+ }
337
+ function storePkceVerifier(verifier) {
338
+ if (typeof sessionStorage !== "undefined") {
339
+ sessionStorage.setItem(PKCE_VERIFIER_KEY, verifier);
340
+ }
341
+ }
342
+ function retrievePkceVerifier() {
343
+ if (typeof sessionStorage === "undefined") {
344
+ return null;
345
+ }
346
+ const verifier = sessionStorage.getItem(PKCE_VERIFIER_KEY);
347
+ if (verifier) {
348
+ sessionStorage.removeItem(PKCE_VERIFIER_KEY);
349
+ }
350
+ return verifier;
351
+ }
321
352
  var Auth = class {
322
353
  constructor(http, tokenManager) {
323
354
  this.http = http;
324
355
  this.tokenManager = tokenManager;
325
- this.detectAuthCallback();
356
+ this.authCallbackHandled = this.detectAuthCallback();
326
357
  }
327
358
  /**
328
359
  * Automatically detect and handle OAuth callback parameters in the URL
329
360
  * This runs after initialization to seamlessly complete the OAuth flow
330
- * Matches the backend's OAuth callback response (backend/src/api/routes/auth.ts:540-544)
361
+ *
362
+ * Supports two flows:
363
+ * - PKCE flow (new): Backend returns `insforge_code` param, exchanged for tokens
364
+ * - Legacy flow: Backend returns tokens directly in URL (backward compatible)
331
365
  */
332
- detectAuthCallback() {
366
+ async detectAuthCallback() {
333
367
  if (typeof window === "undefined") return;
334
368
  try {
335
369
  const params = new URLSearchParams(window.location.search);
370
+ const error = params.get("error");
371
+ if (error) {
372
+ const url = new URL(window.location.href);
373
+ url.searchParams.delete("error");
374
+ window.history.replaceState({}, document.title, url.toString());
375
+ console.debug("OAuth callback error:", error);
376
+ return;
377
+ }
378
+ const code = params.get("insforge_code");
379
+ if (code) {
380
+ const url = new URL(window.location.href);
381
+ url.searchParams.delete("insforge_code");
382
+ window.history.replaceState({}, document.title, url.toString());
383
+ const { error: exchangeError } = await this.exchangeOAuthCode(code);
384
+ if (exchangeError) {
385
+ console.debug("OAuth code exchange failed:", exchangeError.message);
386
+ }
387
+ return;
388
+ }
336
389
  const accessToken = params.get("access_token");
337
390
  const userId = params.get("user_id");
338
391
  const email = params.get("email");
@@ -350,8 +403,6 @@ var Auth = class {
350
403
  email,
351
404
  profile: { name: name || "" },
352
405
  metadata: null,
353
- // These fields are not provided by backend OAuth callback
354
- // They'll be populated when calling getCurrentUser()
355
406
  emailVerified: false,
356
407
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
357
408
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -365,9 +416,6 @@ var Auth = class {
365
416
  url.searchParams.delete("email");
366
417
  url.searchParams.delete("name");
367
418
  url.searchParams.delete("csrf_token");
368
- if (params.has("error")) {
369
- url.searchParams.delete("error");
370
- }
371
419
  window.history.replaceState({}, document.title, url.toString());
372
420
  }
373
421
  } catch (error) {
@@ -448,11 +496,20 @@ var Auth = class {
448
496
  }
449
497
  /**
450
498
  * Sign in with OAuth provider
499
+ * Uses PKCE (Proof Key for Code Exchange) for enhanced security
451
500
  */
452
501
  async signInWithOAuth(options) {
453
502
  try {
454
503
  const { provider, redirectTo, skipBrowserRedirect } = options;
455
- const params = redirectTo ? { redirect_uri: redirectTo } : void 0;
504
+ const codeVerifier = generateCodeVerifier();
505
+ const codeChallenge = await generateCodeChallenge(codeVerifier);
506
+ storePkceVerifier(codeVerifier);
507
+ const params = {
508
+ code_challenge: codeChallenge
509
+ };
510
+ if (redirectTo) {
511
+ params.redirect_uri = redirectTo;
512
+ }
456
513
  const endpoint = `/api/auth/oauth/${provider}`;
457
514
  const response = await this.http.get(endpoint, { params });
458
515
  if (typeof window !== "undefined" && !skipBrowserRedirect) {
@@ -462,7 +519,9 @@ var Auth = class {
462
519
  return {
463
520
  data: {
464
521
  url: response.authUrl,
465
- provider
522
+ provider,
523
+ codeVerifier
524
+ // Return verifier for manual flow (skipBrowserRedirect)
466
525
  },
467
526
  error: null
468
527
  };
@@ -480,6 +539,85 @@ var Auth = class {
480
539
  };
481
540
  }
482
541
  }
542
+ /**
543
+ * Exchange OAuth authorization code for tokens (PKCE flow)
544
+ *
545
+ * After OAuth callback redirects with an `insforge_code` parameter, call this method
546
+ * to exchange it for access tokens. The code verifier is automatically
547
+ * retrieved from sessionStorage if available.
548
+ *
549
+ * Note: This is called automatically by the SDK on initialization. You typically
550
+ * don't need to call this directly unless using `skipBrowserRedirect: true`.
551
+ *
552
+ * @param code - The authorization code from OAuth callback URL
553
+ * @param codeVerifier - Optional code verifier (auto-retrieved from sessionStorage if not provided)
554
+ * @returns Session data with access token and user info
555
+ *
556
+ * @example
557
+ * ```ts
558
+ * // Automatic verifier retrieval (recommended for browser)
559
+ * const params = new URLSearchParams(window.location.search);
560
+ * const code = params.get('insforge_code');
561
+ * if (code) {
562
+ * const { data, error } = await insforge.auth.exchangeOAuthCode(code);
563
+ * }
564
+ *
565
+ * // Manual verifier (for custom flows)
566
+ * const { data, error } = await insforge.auth.exchangeOAuthCode(code, codeVerifier);
567
+ * ```
568
+ */
569
+ async exchangeOAuthCode(code, codeVerifier) {
570
+ try {
571
+ const verifier = codeVerifier ?? retrievePkceVerifier();
572
+ if (!verifier) {
573
+ return {
574
+ data: null,
575
+ error: new InsForgeError(
576
+ "PKCE code verifier not found. Ensure signInWithOAuth was called in the same browser session.",
577
+ 400,
578
+ "PKCE_VERIFIER_MISSING"
579
+ )
580
+ };
581
+ }
582
+ const request = {
583
+ code,
584
+ code_verifier: verifier
585
+ };
586
+ const response = await this.http.post("/api/auth/oauth/exchange", request, { credentials: "include" });
587
+ if (!isHostedAuthEnvironment()) {
588
+ const session = {
589
+ accessToken: response.accessToken,
590
+ user: response.user
591
+ };
592
+ if (response.csrfToken) {
593
+ this.tokenManager.setMemoryMode();
594
+ setCsrfToken(response.csrfToken);
595
+ }
596
+ this.tokenManager.saveSession(session);
597
+ this.http.setAuthToken(response.accessToken);
598
+ }
599
+ return {
600
+ data: {
601
+ accessToken: response.accessToken,
602
+ user: response.user,
603
+ redirectTo: response.redirectTo
604
+ },
605
+ error: null
606
+ };
607
+ } catch (error) {
608
+ if (error instanceof InsForgeError) {
609
+ return { data: null, error };
610
+ }
611
+ return {
612
+ data: null,
613
+ error: new InsForgeError(
614
+ "An unexpected error occurred during OAuth code exchange",
615
+ 500,
616
+ "UNEXPECTED_ERROR"
617
+ )
618
+ };
619
+ }
620
+ }
483
621
  /**
484
622
  * Sign out the current user
485
623
  */
@@ -568,8 +706,10 @@ var Auth = class {
568
706
  /**
569
707
  * Get the current session (only session data, no API call)
570
708
  * Returns the stored JWT token and basic user info from local storage
709
+ * Automatically waits for any pending OAuth callback to complete first
571
710
  */
572
711
  async getCurrentSession() {
712
+ await this.authCallbackHandled;
573
713
  if (isHostedAuthEnvironment()) {
574
714
  return { data: { session: null }, error: null };
575
715
  }
@@ -577,6 +717,11 @@ var Auth = class {
577
717
  const session = this.tokenManager.getSession();
578
718
  if (session) {
579
719
  this.http.setAuthToken(session.accessToken);
720
+ if (!session.user) {
721
+ const authResponse = await this.http.get("/api/auth/sessions/current", { credentials: "include" });
722
+ session.user = authResponse.user;
723
+ this.tokenManager.setUser(session.user);
724
+ }
580
725
  return { data: { session }, error: null };
581
726
  }
582
727
  if (typeof window !== "undefined") {
@@ -868,8 +1013,10 @@ function createInsForgePostgrestFetch(httpClient, tokenManager) {
868
1013
  return async (input, init) => {
869
1014
  const url = typeof input === "string" ? input : input.toString();
870
1015
  const urlObj = new URL(url);
871
- const tableName = urlObj.pathname.slice(1);
872
- const insforgeUrl = `${httpClient.baseUrl}/api/database/records/${tableName}${urlObj.search}`;
1016
+ const pathname = urlObj.pathname.slice(1);
1017
+ const rpcMatch = pathname.match(/^rpc\/(.+)$/);
1018
+ const endpoint = rpcMatch ? `/api/database/rpc/${rpcMatch[1]}` : `/api/database/records/${pathname}`;
1019
+ const insforgeUrl = `${httpClient.baseUrl}${endpoint}${urlObj.search}`;
873
1020
  const token = tokenManager.getAccessToken();
874
1021
  const httpHeaders = httpClient.getHeaders();
875
1022
  const authToken = token || httpHeaders["Authorization"]?.replace("Bearer ", "");
@@ -929,6 +1076,25 @@ var Database = class {
929
1076
  from(table) {
930
1077
  return this.postgrest.from(table);
931
1078
  }
1079
+ /**
1080
+ * Call a PostgreSQL function (RPC)
1081
+ *
1082
+ * @example
1083
+ * // Call a function with parameters
1084
+ * const { data, error } = await client.database
1085
+ * .rpc('get_user_stats', { user_id: 123 });
1086
+ *
1087
+ * // Call a function with no parameters
1088
+ * const { data, error } = await client.database
1089
+ * .rpc('get_all_active_users');
1090
+ *
1091
+ * // With options (head, count, get)
1092
+ * const { data, count } = await client.database
1093
+ * .rpc('search_posts', { query: 'hello' }, { count: 'exact' });
1094
+ */
1095
+ rpc(fn, args, options) {
1096
+ return this.postgrest.rpc(fn, args, options);
1097
+ }
932
1098
  };
933
1099
 
934
1100
  // src/modules/storage.ts
@@ -1734,8 +1900,7 @@ var InsForgeClient = class {
1734
1900
  this.http.setAuthToken(config.edgeFunctionToken);
1735
1901
  this.tokenManager.saveSession({
1736
1902
  accessToken: config.edgeFunctionToken,
1737
- user: {}
1738
- // Will be populated by getCurrentUser()
1903
+ user: null
1739
1904
  });
1740
1905
  }
1741
1906
  const existingSession = this.tokenManager.getSession();