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