@blinkdotnew/sdk 0.19.4 → 0.19.6

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
@@ -1018,6 +1018,142 @@ var HttpClient = class {
1018
1018
  }
1019
1019
  };
1020
1020
 
1021
+ // src/utils/browser-env.ts
1022
+ function hasWindow() {
1023
+ return typeof window !== "undefined";
1024
+ }
1025
+ function hasWindowLocation() {
1026
+ return typeof window !== "undefined" && typeof window.location !== "undefined";
1027
+ }
1028
+ function hasDocument() {
1029
+ return typeof document !== "undefined";
1030
+ }
1031
+ function isReactNative2() {
1032
+ return typeof navigator !== "undefined" && navigator.product === "ReactNative";
1033
+ }
1034
+ function getWindowLocation() {
1035
+ if (!hasWindow()) return null;
1036
+ try {
1037
+ return window.location;
1038
+ } catch {
1039
+ return null;
1040
+ }
1041
+ }
1042
+ function getLocationHref() {
1043
+ const loc = getWindowLocation();
1044
+ if (!loc) return null;
1045
+ try {
1046
+ return loc.href;
1047
+ } catch {
1048
+ return null;
1049
+ }
1050
+ }
1051
+ function getLocationOrigin() {
1052
+ const loc = getWindowLocation();
1053
+ if (!loc) return null;
1054
+ try {
1055
+ return loc.origin;
1056
+ } catch {
1057
+ return null;
1058
+ }
1059
+ }
1060
+ function getLocationHostname() {
1061
+ const loc = getWindowLocation();
1062
+ if (!loc) return null;
1063
+ try {
1064
+ return loc.hostname;
1065
+ } catch {
1066
+ return null;
1067
+ }
1068
+ }
1069
+ function getLocationPathname() {
1070
+ const loc = getWindowLocation();
1071
+ if (!loc) return null;
1072
+ try {
1073
+ return loc.pathname;
1074
+ } catch {
1075
+ return null;
1076
+ }
1077
+ }
1078
+ function getLocationSearch() {
1079
+ const loc = getWindowLocation();
1080
+ if (!loc) return null;
1081
+ try {
1082
+ return loc.search;
1083
+ } catch {
1084
+ return null;
1085
+ }
1086
+ }
1087
+ function getLocationHash() {
1088
+ const loc = getWindowLocation();
1089
+ if (!loc) return null;
1090
+ try {
1091
+ return loc.hash;
1092
+ } catch {
1093
+ return null;
1094
+ }
1095
+ }
1096
+ function getLocationProtocol() {
1097
+ const loc = getWindowLocation();
1098
+ if (!loc) return null;
1099
+ try {
1100
+ return loc.protocol;
1101
+ } catch {
1102
+ return null;
1103
+ }
1104
+ }
1105
+ function getLocationHost() {
1106
+ const loc = getWindowLocation();
1107
+ if (!loc) return null;
1108
+ try {
1109
+ return loc.host;
1110
+ } catch {
1111
+ return null;
1112
+ }
1113
+ }
1114
+ function constructFullUrl() {
1115
+ if (!hasWindow()) return null;
1116
+ const protocol = getLocationProtocol();
1117
+ const host = getLocationHost();
1118
+ const pathname = getLocationPathname();
1119
+ const search = getLocationSearch();
1120
+ const hash = getLocationHash();
1121
+ if (!protocol || !host) return null;
1122
+ return `${protocol}//${host}${pathname || ""}${search || ""}${hash || ""}`;
1123
+ }
1124
+ function getDocumentReferrer() {
1125
+ if (!hasDocument()) return null;
1126
+ try {
1127
+ return document.referrer || null;
1128
+ } catch {
1129
+ return null;
1130
+ }
1131
+ }
1132
+ function getWindowInnerWidth() {
1133
+ if (!hasWindow()) return null;
1134
+ try {
1135
+ return window.innerWidth;
1136
+ } catch {
1137
+ return null;
1138
+ }
1139
+ }
1140
+ function isIframe() {
1141
+ if (!hasWindow()) return false;
1142
+ try {
1143
+ return window.self !== window.top;
1144
+ } catch {
1145
+ return true;
1146
+ }
1147
+ }
1148
+ function getSessionStorage() {
1149
+ if (!hasWindow()) return null;
1150
+ try {
1151
+ return window.sessionStorage;
1152
+ } catch {
1153
+ return null;
1154
+ }
1155
+ }
1156
+
1021
1157
  // src/auth.ts
1022
1158
  var BlinkAuth = class {
1023
1159
  config;
@@ -1041,11 +1177,14 @@ var BlinkAuth = class {
1041
1177
  // Default mode
1042
1178
  authUrl: "https://blink.new",
1043
1179
  coreUrl: "https://core.blink.new",
1180
+ detectSessionInUrl: true,
1181
+ // Default to true for web compatibility
1044
1182
  ...config.auth
1045
1183
  };
1046
1184
  this.authUrl = this.authConfig.authUrl || "https://blink.new";
1047
1185
  this.coreUrl = this.authConfig.coreUrl || "https://core.blink.new";
1048
- if (typeof window !== "undefined" && this.authUrl === "https://blink.new" && (window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1")) {
1186
+ const hostname = getLocationHostname();
1187
+ if (hostname && this.authUrl === "https://blink.new" && (hostname === "localhost" || hostname === "127.0.0.1")) {
1049
1188
  console.warn("\u26A0\uFE0F Using default authUrl in development. Set auth.authUrl to your app origin for headless auth endpoints to work.");
1050
1189
  }
1051
1190
  if (config.authRequired !== void 0 && !config.auth?.mode) {
@@ -1059,7 +1198,7 @@ var BlinkAuth = class {
1059
1198
  };
1060
1199
  this.storage = config.auth?.storage || config.storage || getDefaultStorageAdapter();
1061
1200
  if (isWeb) {
1062
- this.isIframe = window.self !== window.top;
1201
+ this.isIframe = isIframe();
1063
1202
  this.setupParentWindowListener();
1064
1203
  this.setupCrossTabSync();
1065
1204
  this.initializationPromise = this.initialize();
@@ -1095,9 +1234,12 @@ var BlinkAuth = class {
1095
1234
  * Setup listener for tokens from parent window
1096
1235
  */
1097
1236
  setupParentWindowListener() {
1098
- if (!isWeb || !this.isIframe) return;
1237
+ if (!isWeb || !this.isIframe || !hasWindow()) return;
1099
1238
  window.addEventListener("message", (event) => {
1100
- if (event.origin !== "https://blink.new" && event.origin !== "http://localhost:3000" && event.origin !== "http://localhost:3001") {
1239
+ const origin = event.origin;
1240
+ const isTrustedOrigin = origin === "https://blink.new" || origin === "http://localhost:3000" || origin === "http://localhost:3001" || origin.endsWith(".sites.blink.new") || // Trust all preview URLs
1241
+ origin.endsWith(".preview-blink.com");
1242
+ if (!isTrustedOrigin) {
1101
1243
  return;
1102
1244
  }
1103
1245
  if (event.data?.type === "BLINK_AUTH_TOKENS") {
@@ -1117,7 +1259,7 @@ var BlinkAuth = class {
1117
1259
  this.clearTokens();
1118
1260
  }
1119
1261
  });
1120
- if (window.parent !== window) {
1262
+ if (hasWindow() && window.parent !== window) {
1121
1263
  console.log("\u{1F504} Requesting auth tokens from parent window");
1122
1264
  window.parent.postMessage({
1123
1265
  type: "BLINK_REQUEST_AUTH_TOKENS",
@@ -1142,67 +1284,14 @@ var BlinkAuth = class {
1142
1284
  return;
1143
1285
  }
1144
1286
  }
1145
- try {
1146
- if (typeof window !== "undefined") {
1147
- console.log("\u{1F50D} Checking URL for errors/tokens:", {
1148
- href: window.location.href,
1149
- search: window.location.search,
1150
- hash: window.location.hash
1151
- });
1152
- const urlParams = this.extractUrlParams();
1153
- const errorParam = urlParams.get("error");
1154
- if (errorParam) {
1155
- const errorCode = this.mapErrorCodeFromResponse(errorParam);
1156
- const errorMessage = urlParams.get("error_description") || "Authentication failed";
1157
- const error = new BlinkAuthError(errorCode, errorMessage);
1158
- console.error("\u274C Auth error in URL:", {
1159
- error: errorParam,
1160
- errorMessage,
1161
- allParams: Object.fromEntries(urlParams.entries())
1162
- });
1163
- if (typeof window !== "undefined") {
1164
- window.dispatchEvent(new CustomEvent("blink:auth:error", {
1165
- detail: { error, errorMessage }
1166
- }));
1167
- }
1168
- this.clearUrlTokens();
1169
- return;
1170
- }
1171
- }
1172
- } catch (error) {
1173
- console.error("Error handling failed redirect:", error);
1174
- }
1175
- const tokensFromUrl = this.extractTokensFromUrl();
1176
- if (tokensFromUrl) {
1177
- console.log("\u{1F4E5} Found tokens in URL, setting them...");
1178
- await this.setTokens(tokensFromUrl, true);
1179
- this.clearUrlTokens();
1180
- console.log("\u2705 Auth initialization complete (from URL)");
1181
- return;
1182
- } else {
1183
- console.log("\u26A0\uFE0F No tokens found in URL after redirect - checking if this was a redirect callback");
1184
- if (typeof window !== "undefined") {
1185
- const urlParams = this.extractUrlParams();
1186
- const state = urlParams.get("state");
1187
- if (state) {
1188
- console.log("\u26A0\uFE0F State found in URL but no tokens - redirect may have failed silently");
1189
- try {
1190
- const expectedState = sessionStorage.getItem("blink_oauth_state");
1191
- if (expectedState === state) {
1192
- console.error("\u274C Redirect callback received but no tokens - authentication may have failed");
1193
- const errorMessage = "Authentication failed. Please try again.";
1194
- if (typeof window !== "undefined") {
1195
- window.dispatchEvent(new CustomEvent("blink:auth:error", {
1196
- detail: { error: new BlinkAuthError("NETWORK_ERROR" /* NETWORK_ERROR */, errorMessage), errorMessage }
1197
- }));
1198
- }
1199
- this.clearUrlTokens();
1200
- return;
1201
- }
1202
- } catch (e) {
1203
- console.error("Error checking sessionStorage:", e);
1204
- }
1205
- }
1287
+ if (this.authConfig.detectSessionInUrl !== false) {
1288
+ const tokensFromUrl = this.extractTokensFromUrl();
1289
+ if (tokensFromUrl) {
1290
+ console.log("\u{1F4E5} Found tokens in URL, setting them...");
1291
+ await this.setTokens(tokensFromUrl, true);
1292
+ this.clearUrlTokens();
1293
+ console.log("\u2705 Auth initialization complete (from URL)");
1294
+ return;
1206
1295
  }
1207
1296
  }
1208
1297
  const storedTokens = await this.getStoredTokens();
@@ -1227,11 +1316,11 @@ var BlinkAuth = class {
1227
1316
  }
1228
1317
  }
1229
1318
  console.log("\u274C No tokens found");
1230
- if (this.config.authRequired) {
1319
+ if (this.config.authRequired && hasWindowLocation()) {
1231
1320
  console.log("\u{1F504} Auth required, redirecting to auth page...");
1232
1321
  this.redirectToAuth();
1233
1322
  } else {
1234
- console.log("\u26A0\uFE0F Auth not required, continuing without authentication");
1323
+ console.log("\u26A0\uFE0F Auth not required or no window.location, continuing without authentication");
1235
1324
  }
1236
1325
  } finally {
1237
1326
  this.setLoading(false);
@@ -1242,15 +1331,20 @@ var BlinkAuth = class {
1242
1331
  * Redirect to Blink auth page
1243
1332
  */
1244
1333
  login(nextUrl) {
1334
+ if (!hasWindowLocation()) {
1335
+ console.warn("login() called in non-browser environment (no window.location available)");
1336
+ return;
1337
+ }
1245
1338
  let redirectUrl = nextUrl || this.authConfig.redirectUrl;
1246
- if (!redirectUrl && typeof window !== "undefined") {
1247
- if (window.location.href.startsWith("http")) {
1248
- redirectUrl = window.location.href;
1339
+ if (!redirectUrl) {
1340
+ const href = getLocationHref();
1341
+ if (href && href.startsWith("http")) {
1342
+ redirectUrl = href;
1249
1343
  } else {
1250
- redirectUrl = `${window.location.protocol}//${window.location.host}${window.location.pathname}${window.location.search}${window.location.hash}`;
1344
+ redirectUrl = constructFullUrl() || void 0;
1251
1345
  }
1252
1346
  }
1253
- if (redirectUrl && typeof window !== "undefined") {
1347
+ if (redirectUrl) {
1254
1348
  try {
1255
1349
  const url = new URL(redirectUrl);
1256
1350
  url.searchParams.delete("redirect_url");
@@ -1265,16 +1359,14 @@ var BlinkAuth = class {
1265
1359
  if (this.config.projectId) {
1266
1360
  authUrl.searchParams.set("project_id", this.config.projectId);
1267
1361
  }
1268
- if (typeof window !== "undefined") {
1269
- window.location.href = authUrl.toString();
1270
- }
1362
+ window.location.href = authUrl.toString();
1271
1363
  }
1272
1364
  /**
1273
1365
  * Logout and clear stored tokens
1274
1366
  */
1275
1367
  logout(redirectUrl) {
1276
1368
  this.clearTokens();
1277
- if (redirectUrl && typeof window !== "undefined") {
1369
+ if (redirectUrl && hasWindowLocation()) {
1278
1370
  window.location.href = redirectUrl;
1279
1371
  }
1280
1372
  }
@@ -1558,144 +1650,251 @@ var BlinkAuth = class {
1558
1650
  return this.signInWithProvider("microsoft", options);
1559
1651
  }
1560
1652
  /**
1561
- * Check if current browser is Safari (desktop, iPhone, iPad)
1562
- * Safari has strict popup blocking, so we must use redirect flow
1653
+ * Initiate OAuth for mobile without deep linking (expo-web-browser pattern)
1654
+ *
1655
+ * This method:
1656
+ * 1. Generates a unique session ID
1657
+ * 2. Returns OAuth URL with session parameter
1658
+ * 3. App opens URL in expo-web-browser
1659
+ * 4. App polls checkMobileOAuthSession() until complete
1660
+ *
1661
+ * @param provider - OAuth provider (google, github, apple, etc.)
1662
+ * @param options - Optional metadata
1663
+ * @returns Session ID and OAuth URL
1664
+ *
1665
+ * @example
1666
+ * // React Native with expo-web-browser
1667
+ * import * as WebBrowser from 'expo-web-browser';
1668
+ *
1669
+ * const { sessionId, authUrl } = await blink.auth.initiateMobileOAuth('google');
1670
+ *
1671
+ * // Open browser
1672
+ * await WebBrowser.openAuthSessionAsync(authUrl);
1673
+ *
1674
+ * // Poll for completion
1675
+ * const user = await blink.auth.pollMobileOAuthSession(sessionId);
1676
+ * console.log('Authenticated:', user.email);
1563
1677
  */
1564
- isSafari() {
1565
- if (typeof window === "undefined") return false;
1566
- const ua = navigator.userAgent.toLowerCase();
1567
- const hasSafariUA = /safari/i.test(ua) && !/chrome|chromium|android|edg|firefox|opera/i.test(ua);
1568
- const isSafariVendor = typeof navigator !== "undefined" && !!navigator.vendor && /apple/i.test(navigator.vendor);
1569
- const isIOSSafari = /safari/i.test(ua) && /iphone|ipad|ipod/i.test(ua);
1570
- const hasSafariProperties = typeof window !== "undefined" && ("safari" in window || window.safari !== void 0) && !("chrome" in window);
1571
- const isMacSafari = /macintosh/i.test(ua) && isSafariVendor && !/chrome|chromium|firefox|edg/i.test(ua);
1572
- const isSafari = !!(hasSafariUA || isSafariVendor || isIOSSafari || hasSafariProperties || isMacSafari);
1573
- if (isSafari) {
1574
- console.log("\u{1F34E} Safari browser detected - will use redirect flow", {
1575
- hasSafariUA,
1576
- isSafariVendor,
1577
- isIOSSafari,
1578
- hasSafariProperties,
1579
- isMacSafari,
1580
- userAgent: ua.substring(0, 100),
1581
- vendor: navigator.vendor
1582
- });
1583
- } else {
1584
- console.log("\u{1F50D} Not detected as Safari:", {
1585
- hasSafariUA,
1586
- isSafariVendor,
1587
- isIOSSafari,
1588
- hasSafariProperties,
1589
- isMacSafari,
1590
- userAgent: ua.substring(0, 100),
1591
- vendor: navigator.vendor
1678
+ async initiateMobileOAuth(provider, options) {
1679
+ if (this.authConfig.mode !== "headless") {
1680
+ throw new BlinkAuthError(
1681
+ "INVALID_CREDENTIALS" /* INVALID_CREDENTIALS */,
1682
+ "initiateMobileOAuth is only available in headless mode"
1683
+ );
1684
+ }
1685
+ const sessionId = this.generateSessionId();
1686
+ const authUrl = new URL("/auth", this.authUrl);
1687
+ authUrl.searchParams.set("provider", provider);
1688
+ authUrl.searchParams.set("project_id", this.config.projectId);
1689
+ authUrl.searchParams.set("mode", "mobile-session");
1690
+ authUrl.searchParams.set("session_id", sessionId);
1691
+ if (options?.metadata) {
1692
+ authUrl.searchParams.set("metadata", JSON.stringify(options.metadata));
1693
+ }
1694
+ return {
1695
+ sessionId,
1696
+ authUrl: authUrl.toString()
1697
+ };
1698
+ }
1699
+ /**
1700
+ * Check mobile OAuth session status (single check)
1701
+ *
1702
+ * @param sessionId - Session ID from initiateMobileOAuth
1703
+ * @returns Tokens if session is complete, null if still pending
1704
+ */
1705
+ async checkMobileOAuthSession(sessionId) {
1706
+ try {
1707
+ const response = await fetch(`${this.authUrl}/api/auth/mobile-session/${sessionId}`, {
1708
+ method: "GET",
1709
+ headers: {
1710
+ "Content-Type": "application/json"
1711
+ }
1592
1712
  });
1713
+ if (response.status === 404 || response.status === 202) {
1714
+ return null;
1715
+ }
1716
+ if (!response.ok) {
1717
+ const errorData = await response.json();
1718
+ const errorCode = this.mapErrorCodeFromResponse(errorData.code);
1719
+ throw new BlinkAuthError(
1720
+ errorCode,
1721
+ errorData.error || "Failed to check OAuth session"
1722
+ );
1723
+ }
1724
+ const data = await response.json();
1725
+ return {
1726
+ access_token: data.access_token,
1727
+ refresh_token: data.refresh_token,
1728
+ token_type: data.token_type || "Bearer",
1729
+ expires_in: data.expires_in || 3600,
1730
+ refresh_expires_in: data.refresh_expires_in
1731
+ };
1732
+ } catch (error) {
1733
+ if (error instanceof BlinkAuthError) {
1734
+ throw error;
1735
+ }
1736
+ throw new BlinkAuthError(
1737
+ "NETWORK_ERROR" /* NETWORK_ERROR */,
1738
+ `Network error: ${error instanceof Error ? error.message : "Unknown error"}`
1739
+ );
1593
1740
  }
1594
- return isSafari;
1741
+ }
1742
+ /**
1743
+ * Poll mobile OAuth session until complete (convenience method)
1744
+ *
1745
+ * @param sessionId - Session ID from initiateMobileOAuth
1746
+ * @param options - Polling options
1747
+ * @returns Authenticated user
1748
+ *
1749
+ * @example
1750
+ * const { sessionId, authUrl } = await blink.auth.initiateMobileOAuth('google');
1751
+ * await WebBrowser.openAuthSessionAsync(authUrl);
1752
+ * const user = await blink.auth.pollMobileOAuthSession(sessionId, {
1753
+ * maxAttempts: 60,
1754
+ * intervalMs: 1000
1755
+ * });
1756
+ */
1757
+ async pollMobileOAuthSession(sessionId, options) {
1758
+ const maxAttempts = options?.maxAttempts || 60;
1759
+ const intervalMs = options?.intervalMs || 1e3;
1760
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
1761
+ const tokens = await this.checkMobileOAuthSession(sessionId);
1762
+ if (tokens) {
1763
+ await this.setTokens(tokens, true);
1764
+ return this.authState.user;
1765
+ }
1766
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
1767
+ }
1768
+ throw new BlinkAuthError(
1769
+ "AUTH_TIMEOUT" /* AUTH_TIMEOUT */,
1770
+ "Mobile OAuth session timed out"
1771
+ );
1772
+ }
1773
+ /**
1774
+ * Sign in with OAuth provider using expo-web-browser (React Native)
1775
+ *
1776
+ * This is a convenience method that handles the entire flow:
1777
+ * 1. Initiates mobile OAuth session
1778
+ * 2. Returns auth URL to open in WebBrowser
1779
+ * 3. Provides polling function to call after browser opens
1780
+ *
1781
+ * @param provider - OAuth provider
1782
+ * @returns Object with authUrl and authenticate function
1783
+ *
1784
+ * @example
1785
+ * import * as WebBrowser from 'expo-web-browser';
1786
+ *
1787
+ * const { authUrl, authenticate } = await blink.auth.signInWithProviderMobile('google');
1788
+ *
1789
+ * // Open browser
1790
+ * await WebBrowser.openAuthSessionAsync(authUrl);
1791
+ *
1792
+ * // Wait for authentication
1793
+ * const user = await authenticate();
1794
+ */
1795
+ async signInWithProviderMobile(provider, options) {
1796
+ const { sessionId, authUrl } = await this.initiateMobileOAuth(provider, options);
1797
+ return {
1798
+ authUrl,
1799
+ authenticate: () => this.pollMobileOAuthSession(sessionId, {
1800
+ maxAttempts: 60,
1801
+ intervalMs: 1e3
1802
+ })
1803
+ };
1804
+ }
1805
+ /**
1806
+ * Sign in with Google using expo-web-browser (React Native convenience)
1807
+ */
1808
+ async signInWithGoogleMobile(options) {
1809
+ return this.signInWithProviderMobile("google", options);
1810
+ }
1811
+ /**
1812
+ * Sign in with GitHub using expo-web-browser (React Native convenience)
1813
+ */
1814
+ async signInWithGitHubMobile(options) {
1815
+ return this.signInWithProviderMobile("github", options);
1816
+ }
1817
+ /**
1818
+ * Sign in with Apple using expo-web-browser (React Native convenience)
1819
+ */
1820
+ async signInWithAppleMobile(options) {
1821
+ return this.signInWithProviderMobile("apple", options);
1822
+ }
1823
+ /**
1824
+ * React Native OAuth flow using expo-web-browser (internal)
1825
+ * Automatically handles opening browser and extracting tokens from redirect
1826
+ */
1827
+ async signInWithProviderReactNative(provider, options) {
1828
+ throw new BlinkAuthError(
1829
+ "NETWORK_ERROR" /* NETWORK_ERROR */,
1830
+ 'React Native OAuth detected!\n\nPlease use the mobile-specific methods:\n\nimport * as WebBrowser from "expo-web-browser";\n\nconst { authUrl, authenticate } = await blink.auth.signInWithGoogleMobile();\nawait WebBrowser.openAuthSessionAsync(authUrl);\nconst user = await authenticate();\n\nOr update your useAuth hook to handle this automatically.'
1831
+ );
1595
1832
  }
1596
1833
  /**
1597
1834
  * Generic provider sign-in method (headless mode)
1598
- * Uses redirect flow for Safari, popup flow for other browsers
1835
+ *
1836
+ * Supports both web (popup) and React Native (deep link) OAuth flows.
1837
+ *
1838
+ * **React Native Setup:**
1839
+ * 1. Configure deep linking in your app (app.json):
1840
+ * ```json
1841
+ * { "expo": { "scheme": "com.yourapp" } }
1842
+ * ```
1843
+ *
1844
+ * 2. Add redirect URL in Blink Dashboard:
1845
+ * `com.yourapp://**`
1846
+ *
1847
+ * 3. Handle deep link callbacks:
1848
+ * ```typescript
1849
+ * import * as Linking from 'expo-linking'
1850
+ *
1851
+ * Linking.addEventListener('url', async ({ url }) => {
1852
+ * const { queryParams } = Linking.parse(url)
1853
+ * if (queryParams.access_token) {
1854
+ * await blink.auth.setSession(queryParams)
1855
+ * }
1856
+ * })
1857
+ * ```
1858
+ *
1859
+ * @param provider - OAuth provider (google, github, apple, etc.)
1860
+ * @param options - Optional redirect URL and metadata
1861
+ * @returns Promise that resolves with authenticated user
1599
1862
  */
1600
1863
  async signInWithProvider(provider, options) {
1601
1864
  if (this.authConfig.mode !== "headless") {
1602
1865
  throw new BlinkAuthError("INVALID_CREDENTIALS" /* INVALID_CREDENTIALS */, "signInWithProvider is only available in headless mode");
1603
1866
  }
1604
- const state = this.generateState();
1605
- let redirectUrl = options?.redirectUrl || "";
1606
- if (!redirectUrl && typeof window !== "undefined") {
1607
- if (window.location.href.startsWith("http")) {
1608
- redirectUrl = window.location.href;
1609
- } else {
1610
- redirectUrl = `${window.location.protocol}//${window.location.host}${window.location.pathname}${window.location.search}${window.location.hash}`;
1611
- }
1867
+ if (!hasWindow()) {
1868
+ throw new BlinkAuthError("NETWORK_ERROR" /* NETWORK_ERROR */, "signInWithProvider requires a browser environment");
1612
1869
  }
1613
- try {
1614
- const redirectUrlObj = new URL(redirectUrl);
1615
- redirectUrlObj.searchParams.delete("access_token");
1616
- redirectUrlObj.searchParams.delete("refresh_token");
1617
- redirectUrlObj.searchParams.delete("state");
1618
- redirectUrlObj.searchParams.delete("error");
1619
- redirectUrlObj.searchParams.delete("error_description");
1620
- redirectUrlObj.hash = "";
1621
- redirectUrl = redirectUrlObj.toString();
1622
- } catch (e) {
1623
- console.warn("Failed to clean redirect URL:", e);
1624
- }
1625
- try {
1626
- if (typeof window !== "undefined") {
1627
- sessionStorage.setItem("blink_oauth_state", state);
1628
- sessionStorage.setItem("blink_oauth_redirect_url", redirectUrl);
1629
- console.log("\u{1F4BE} Stored OAuth state:", { state, redirectUrl });
1630
- }
1631
- } catch (e) {
1632
- console.warn("Failed to store OAuth state in sessionStorage:", e);
1633
- }
1634
- const isSafari = this.isSafari();
1635
- const forceRedirect = options?.forceRedirect === true;
1636
- if ((isSafari || forceRedirect) && typeof window !== "undefined") {
1637
- console.log("\u{1F310} Using redirect flow (no popup)", {
1638
- provider,
1639
- projectId: this.config.projectId,
1640
- state,
1641
- redirectUrl,
1642
- authUrl: this.authUrl,
1643
- reason: isSafari ? "Safari detected" : "forceRedirect option set"
1644
- });
1645
- const authRedirectUrl = new URL("/auth", this.authUrl);
1646
- authRedirectUrl.searchParams.set("provider", provider);
1647
- authRedirectUrl.searchParams.set("project_id", this.config.projectId);
1648
- authRedirectUrl.searchParams.set("state", state);
1649
- authRedirectUrl.searchParams.set("mode", "redirect");
1650
- authRedirectUrl.searchParams.set("redirect_url", redirectUrl);
1651
- console.log("\u{1F504} Redirecting to:", authRedirectUrl.toString());
1652
- window.location.href = authRedirectUrl.toString();
1653
- return new Promise(() => {
1654
- });
1870
+ if (isReactNative2()) {
1871
+ return this.signInWithProviderReactNative(provider, options);
1655
1872
  }
1656
1873
  return new Promise((resolve, reject) => {
1874
+ const state = this.generateState();
1875
+ try {
1876
+ const sessionStorage = getSessionStorage();
1877
+ if (sessionStorage) {
1878
+ sessionStorage.setItem("blink_oauth_state", state);
1879
+ }
1880
+ } catch {
1881
+ }
1882
+ const redirectUrl = options?.redirectUrl || getLocationOrigin() || "";
1657
1883
  const popupUrl = new URL("/auth", this.authUrl);
1658
1884
  popupUrl.searchParams.set("provider", provider);
1659
1885
  popupUrl.searchParams.set("project_id", this.config.projectId);
1660
1886
  popupUrl.searchParams.set("state", state);
1661
1887
  popupUrl.searchParams.set("mode", "popup");
1662
1888
  popupUrl.searchParams.set("redirect_url", redirectUrl);
1663
- if (typeof window !== "undefined") {
1664
- popupUrl.searchParams.set("opener_origin", window.location.origin);
1665
- }
1666
1889
  const popup = window.open(
1667
1890
  popupUrl.toString(),
1668
1891
  "blink-auth",
1669
1892
  "width=500,height=600,scrollbars=yes,resizable=yes"
1670
1893
  );
1671
1894
  if (!popup) {
1672
- console.warn("\u26A0\uFE0F Popup was blocked, falling back to redirect flow");
1673
- const authRedirectUrl = new URL("/auth", this.authUrl);
1674
- authRedirectUrl.searchParams.set("provider", provider);
1675
- authRedirectUrl.searchParams.set("project_id", this.config.projectId);
1676
- authRedirectUrl.searchParams.set("state", state);
1677
- authRedirectUrl.searchParams.set("mode", "redirect");
1678
- authRedirectUrl.searchParams.set("redirect_url", redirectUrl);
1679
- console.log("\u{1F504} Falling back to redirect:", authRedirectUrl.toString());
1680
- window.location.href = authRedirectUrl.toString();
1895
+ reject(new BlinkAuthError("POPUP_CANCELED" /* POPUP_CANCELED */, "Popup was blocked"));
1681
1896
  return;
1682
1897
  }
1683
- try {
1684
- if (popup.closed || !popup.window) {
1685
- console.warn("\u26A0\uFE0F Popup appears to be blocked (closed immediately), falling back to redirect flow");
1686
- const authRedirectUrl = new URL("/auth", this.authUrl);
1687
- authRedirectUrl.searchParams.set("provider", provider);
1688
- authRedirectUrl.searchParams.set("project_id", this.config.projectId);
1689
- authRedirectUrl.searchParams.set("state", state);
1690
- authRedirectUrl.searchParams.set("mode", "redirect");
1691
- authRedirectUrl.searchParams.set("redirect_url", redirectUrl);
1692
- console.log("\u{1F504} Falling back to redirect:", authRedirectUrl.toString());
1693
- window.location.href = authRedirectUrl.toString();
1694
- return;
1695
- }
1696
- } catch (e) {
1697
- console.log("\u26A0\uFE0F Could not verify popup state, assuming it opened successfully");
1698
- }
1699
1898
  let timeoutId;
1700
1899
  const messageListener = (event) => {
1701
1900
  let allowed = false;
@@ -1705,16 +1904,13 @@ var BlinkAuth = class {
1705
1904
  } catch {
1706
1905
  }
1707
1906
  if (event.origin === "http://localhost:3000" || event.origin === "http://localhost:3001") allowed = true;
1708
- if (typeof window !== "undefined" && event.origin === window.location.origin) allowed = true;
1709
- if (!allowed) {
1710
- console.log("\u{1F6AB} Blocked postMessage from untrusted origin:", event.origin);
1711
- return;
1712
- }
1713
- console.log("\u2705 Accepted postMessage from origin:", event.origin);
1907
+ if (event.origin.endsWith(".sites.blink.new") || event.origin.endsWith(".preview-blink.com")) allowed = true;
1908
+ if (!allowed) return;
1714
1909
  if (event.data?.type === "BLINK_AUTH_TOKENS") {
1715
1910
  const { access_token, refresh_token, token_type, expires_in, refresh_expires_in, projectId, state: returnedState } = event.data;
1716
1911
  try {
1717
- const expected = sessionStorage.getItem("blink_oauth_state");
1912
+ const sessionStorage = getSessionStorage();
1913
+ const expected = sessionStorage?.getItem("blink_oauth_state");
1718
1914
  if (returnedState && expected && returnedState !== expected) {
1719
1915
  reject(new BlinkAuthError("VERIFICATION_FAILED" /* VERIFICATION_FAILED */, "State mismatch"));
1720
1916
  clearTimeout(timeoutId);
@@ -2238,6 +2434,48 @@ var BlinkAuth = class {
2238
2434
  };
2239
2435
  await this.setTokens(tokens, persist);
2240
2436
  }
2437
+ /**
2438
+ * Manually set auth session from tokens (React Native deep link OAuth)
2439
+ *
2440
+ * Use this method to set the user session after receiving tokens from a deep link callback.
2441
+ * This is the React Native equivalent of automatic URL token detection on web.
2442
+ *
2443
+ * @param tokens - Auth tokens received from deep link or OAuth callback
2444
+ * @param persist - Whether to persist tokens to storage (default: true)
2445
+ *
2446
+ * @example
2447
+ * // React Native: Handle deep link OAuth callback
2448
+ * import * as Linking from 'expo-linking'
2449
+ *
2450
+ * Linking.addEventListener('url', async ({ url }) => {
2451
+ * const { queryParams } = Linking.parse(url)
2452
+ *
2453
+ * if (queryParams.access_token) {
2454
+ * await blink.auth.setSession({
2455
+ * access_token: queryParams.access_token,
2456
+ * refresh_token: queryParams.refresh_token,
2457
+ * expires_in: parseInt(queryParams.expires_in) || 3600,
2458
+ * refresh_expires_in: parseInt(queryParams.refresh_expires_in)
2459
+ * })
2460
+ *
2461
+ * console.log('User authenticated:', blink.auth.currentUser())
2462
+ * }
2463
+ * })
2464
+ */
2465
+ async setSession(tokens, persist = true) {
2466
+ const authTokens = {
2467
+ access_token: tokens.access_token,
2468
+ refresh_token: tokens.refresh_token,
2469
+ token_type: "Bearer",
2470
+ expires_in: tokens.expires_in || 3600,
2471
+ // Default 1 hour
2472
+ refresh_expires_in: tokens.refresh_expires_in,
2473
+ issued_at: Math.floor(Date.now() / 1e3)
2474
+ };
2475
+ await this.setTokens(authTokens, persist);
2476
+ const user = await this.me();
2477
+ return user;
2478
+ }
2241
2479
  /**
2242
2480
  * Refresh access token using refresh token
2243
2481
  */
@@ -2470,50 +2708,18 @@ var BlinkAuth = class {
2470
2708
  return null;
2471
2709
  }
2472
2710
  }
2473
- /**
2474
- * Extract URL parameters from both search params and hash fragments
2475
- * Safari OAuth redirects often use hash fragments instead of query params
2476
- */
2477
- extractUrlParams() {
2478
- const params = new URLSearchParams();
2479
- if (typeof window !== "undefined" && window.location.search) {
2480
- const searchParams = new URLSearchParams(window.location.search);
2481
- for (const [key, value] of searchParams.entries()) {
2482
- params.set(key, value);
2483
- }
2484
- }
2485
- if (typeof window !== "undefined" && window.location.hash) {
2486
- const hash = window.location.hash.substring(1);
2487
- const hashParams = new URLSearchParams(hash);
2488
- for (const [key, value] of hashParams.entries()) {
2489
- if (!params.has(key)) {
2490
- params.set(key, value);
2491
- }
2492
- }
2493
- }
2494
- return params;
2495
- }
2496
2711
  extractTokensFromUrl() {
2497
- if (typeof window === "undefined") return null;
2498
- const params = this.extractUrlParams();
2712
+ const search = getLocationSearch();
2713
+ if (!search) return null;
2714
+ const params = new URLSearchParams(search);
2499
2715
  const accessToken = params.get("access_token");
2500
2716
  const refreshToken = params.get("refresh_token");
2501
- const state = params.get("state");
2502
- const error = params.get("error");
2503
2717
  console.log("\u{1F50D} Extracting tokens from URL:", {
2504
- url: window.location.href,
2505
- search: window.location.search,
2506
- hash: window.location.hash,
2718
+ url: getLocationHref(),
2507
2719
  accessToken: accessToken ? `${accessToken.substring(0, 20)}...` : null,
2508
2720
  refreshToken: refreshToken ? `${refreshToken.substring(0, 20)}...` : null,
2509
- state: state ? `${state.substring(0, 10)}...` : null,
2510
- error: error || null,
2511
2721
  allParams: Object.fromEntries(params.entries())
2512
2722
  });
2513
- if (error) {
2514
- console.log("\u274C Error parameter found in URL, not extracting tokens");
2515
- return null;
2516
- }
2517
2723
  if (accessToken) {
2518
2724
  const tokens = {
2519
2725
  access_token: accessToken,
@@ -2528,8 +2734,7 @@ var BlinkAuth = class {
2528
2734
  };
2529
2735
  console.log("\u2705 Tokens extracted successfully:", {
2530
2736
  hasAccessToken: !!tokens.access_token,
2531
- hasRefreshToken: !!tokens.refresh_token,
2532
- state: state || "no state"
2737
+ hasRefreshToken: !!tokens.refresh_token
2533
2738
  });
2534
2739
  return tokens;
2535
2740
  }
@@ -2537,8 +2742,9 @@ var BlinkAuth = class {
2537
2742
  return null;
2538
2743
  }
2539
2744
  clearUrlTokens() {
2540
- if (typeof window === "undefined") return;
2541
- const url = new URL(window.location.href);
2745
+ const href = getLocationHref();
2746
+ if (!href || !hasWindowLocation()) return;
2747
+ const url = new URL(href);
2542
2748
  url.searchParams.delete("access_token");
2543
2749
  url.searchParams.delete("refresh_token");
2544
2750
  url.searchParams.delete("token_type");
@@ -2549,12 +2755,11 @@ var BlinkAuth = class {
2549
2755
  url.searchParams.delete("code");
2550
2756
  url.searchParams.delete("error");
2551
2757
  url.searchParams.delete("error_description");
2552
- url.hash = "";
2553
2758
  window.history.replaceState({}, "", url.toString());
2554
- console.log("\u{1F9F9} URL cleaned up, removed auth parameters from both search and hash");
2759
+ console.log("\u{1F9F9} URL cleaned up, removed auth parameters");
2555
2760
  }
2556
2761
  redirectToAuth() {
2557
- if (typeof window !== "undefined") {
2762
+ if (hasWindowLocation()) {
2558
2763
  this.login();
2559
2764
  }
2560
2765
  }
@@ -2586,12 +2791,25 @@ var BlinkAuth = class {
2586
2791
  return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
2587
2792
  }
2588
2793
  }
2794
+ /**
2795
+ * Generate unique session ID for mobile OAuth
2796
+ */
2797
+ generateSessionId() {
2798
+ if (typeof crypto !== "undefined" && crypto.getRandomValues) {
2799
+ const array = new Uint8Array(32);
2800
+ crypto.getRandomValues(array);
2801
+ return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join("");
2802
+ } else {
2803
+ return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
2804
+ }
2805
+ }
2589
2806
  /**
2590
2807
  * Extract magic link token from URL
2591
2808
  */
2592
2809
  extractMagicTokenFromUrl() {
2593
- if (typeof window === "undefined") return null;
2594
- const params = new URLSearchParams(window.location.search);
2810
+ const search = getLocationSearch();
2811
+ if (!search) return null;
2812
+ const params = new URLSearchParams(search);
2595
2813
  return params.get("magic_token") || params.get("token");
2596
2814
  }
2597
2815
  /**
@@ -2647,7 +2865,7 @@ var BlinkAuth = class {
2647
2865
  * Setup cross-tab authentication synchronization
2648
2866
  */
2649
2867
  setupCrossTabSync() {
2650
- if (!isWeb) return;
2868
+ if (!isWeb || !hasWindow()) return;
2651
2869
  window.addEventListener("storage", (e) => {
2652
2870
  if (e.key === this.getStorageKey("tokens")) {
2653
2871
  const newTokens = e.newValue ? JSON.parse(e.newValue) : null;
@@ -4951,9 +5169,9 @@ var BlinkAnalyticsImpl = class {
4951
5169
  user_id: this.userId,
4952
5170
  user_email: this.userEmail,
4953
5171
  session_id: sessionId,
4954
- pathname: window.location.pathname,
4955
- referrer: document.referrer || null,
4956
- screen_width: window.innerWidth,
5172
+ pathname: getLocationPathname(),
5173
+ referrer: getDocumentReferrer(),
5174
+ screen_width: getWindowInnerWidth(),
4957
5175
  channel,
4958
5176
  utm_source: this.utmParams.utm_source || this.persistedAttribution.utm_source || null,
4959
5177
  utm_medium: this.utmParams.utm_medium || this.persistedAttribution.utm_medium || null,
@@ -5106,7 +5324,7 @@ var BlinkAnalyticsImpl = class {
5106
5324
  window.__blinkAnalyticsInstances?.add(this);
5107
5325
  }
5108
5326
  setupUnloadListener() {
5109
- if (!isWeb) return;
5327
+ if (!isWeb || !hasWindow()) return;
5110
5328
  window.addEventListener("pagehide", () => {
5111
5329
  this.flush();
5112
5330
  });
@@ -5116,7 +5334,12 @@ var BlinkAnalyticsImpl = class {
5116
5334
  }
5117
5335
  captureUTMParams() {
5118
5336
  if (!isWeb) return;
5119
- const urlParams = new URLSearchParams(window.location.search);
5337
+ const search = getLocationSearch();
5338
+ if (!search) {
5339
+ this.utmParams = {};
5340
+ return;
5341
+ }
5342
+ const urlParams = new URLSearchParams(search);
5120
5343
  this.utmParams = {
5121
5344
  utm_source: urlParams.get("utm_source"),
5122
5345
  utm_medium: urlParams.get("utm_medium"),
@@ -5153,7 +5376,7 @@ var BlinkAnalyticsImpl = class {
5153
5376
  }
5154
5377
  }
5155
5378
  detectChannel() {
5156
- const referrer = document.referrer;
5379
+ const referrer = getDocumentReferrer();
5157
5380
  const utmMedium = this.utmParams.utm_medium;
5158
5381
  this.utmParams.utm_source;
5159
5382
  if (utmMedium) {