@blinkdotnew/sdk 0.19.1 → 0.19.4
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.d.mts +16 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +216 -13
- package/dist/index.mjs +216 -13
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -118,6 +118,11 @@ type AuthProvider = 'email' | 'google' | 'github' | 'apple' | 'microsoft' | 'twi
|
|
|
118
118
|
interface AuthOptions {
|
|
119
119
|
redirectUrl?: string;
|
|
120
120
|
metadata?: Record<string, any>;
|
|
121
|
+
/**
|
|
122
|
+
* Force redirect flow instead of popup (useful for Safari or when popups are blocked)
|
|
123
|
+
* When true, always uses redirect flow regardless of browser detection
|
|
124
|
+
*/
|
|
125
|
+
forceRedirect?: boolean;
|
|
121
126
|
}
|
|
122
127
|
interface SignUpData {
|
|
123
128
|
email: string;
|
|
@@ -1008,8 +1013,14 @@ declare class BlinkAuth {
|
|
|
1008
1013
|
* Sign in with Microsoft (headless mode)
|
|
1009
1014
|
*/
|
|
1010
1015
|
signInWithMicrosoft(options?: AuthOptions): Promise<BlinkUser>;
|
|
1016
|
+
/**
|
|
1017
|
+
* Check if current browser is Safari (desktop, iPhone, iPad)
|
|
1018
|
+
* Safari has strict popup blocking, so we must use redirect flow
|
|
1019
|
+
*/
|
|
1020
|
+
private isSafari;
|
|
1011
1021
|
/**
|
|
1012
1022
|
* Generic provider sign-in method (headless mode)
|
|
1023
|
+
* Uses redirect flow for Safari, popup flow for other browsers
|
|
1013
1024
|
*/
|
|
1014
1025
|
signInWithProvider(provider: AuthProvider, options?: AuthOptions): Promise<BlinkUser>;
|
|
1015
1026
|
/**
|
|
@@ -1114,6 +1125,11 @@ declare class BlinkAuth {
|
|
|
1114
1125
|
private setTokens;
|
|
1115
1126
|
private clearTokens;
|
|
1116
1127
|
private getStoredTokens;
|
|
1128
|
+
/**
|
|
1129
|
+
* Extract URL parameters from both search params and hash fragments
|
|
1130
|
+
* Safari OAuth redirects often use hash fragments instead of query params
|
|
1131
|
+
*/
|
|
1132
|
+
private extractUrlParams;
|
|
1117
1133
|
private extractTokensFromUrl;
|
|
1118
1134
|
private clearUrlTokens;
|
|
1119
1135
|
private redirectToAuth;
|
package/dist/index.d.ts
CHANGED
|
@@ -118,6 +118,11 @@ type AuthProvider = 'email' | 'google' | 'github' | 'apple' | 'microsoft' | 'twi
|
|
|
118
118
|
interface AuthOptions {
|
|
119
119
|
redirectUrl?: string;
|
|
120
120
|
metadata?: Record<string, any>;
|
|
121
|
+
/**
|
|
122
|
+
* Force redirect flow instead of popup (useful for Safari or when popups are blocked)
|
|
123
|
+
* When true, always uses redirect flow regardless of browser detection
|
|
124
|
+
*/
|
|
125
|
+
forceRedirect?: boolean;
|
|
121
126
|
}
|
|
122
127
|
interface SignUpData {
|
|
123
128
|
email: string;
|
|
@@ -1008,8 +1013,14 @@ declare class BlinkAuth {
|
|
|
1008
1013
|
* Sign in with Microsoft (headless mode)
|
|
1009
1014
|
*/
|
|
1010
1015
|
signInWithMicrosoft(options?: AuthOptions): Promise<BlinkUser>;
|
|
1016
|
+
/**
|
|
1017
|
+
* Check if current browser is Safari (desktop, iPhone, iPad)
|
|
1018
|
+
* Safari has strict popup blocking, so we must use redirect flow
|
|
1019
|
+
*/
|
|
1020
|
+
private isSafari;
|
|
1011
1021
|
/**
|
|
1012
1022
|
* Generic provider sign-in method (headless mode)
|
|
1023
|
+
* Uses redirect flow for Safari, popup flow for other browsers
|
|
1013
1024
|
*/
|
|
1014
1025
|
signInWithProvider(provider: AuthProvider, options?: AuthOptions): Promise<BlinkUser>;
|
|
1015
1026
|
/**
|
|
@@ -1114,6 +1125,11 @@ declare class BlinkAuth {
|
|
|
1114
1125
|
private setTokens;
|
|
1115
1126
|
private clearTokens;
|
|
1116
1127
|
private getStoredTokens;
|
|
1128
|
+
/**
|
|
1129
|
+
* Extract URL parameters from both search params and hash fragments
|
|
1130
|
+
* Safari OAuth redirects often use hash fragments instead of query params
|
|
1131
|
+
*/
|
|
1132
|
+
private extractUrlParams;
|
|
1117
1133
|
private extractTokensFromUrl;
|
|
1118
1134
|
private clearUrlTokens;
|
|
1119
1135
|
private redirectToAuth;
|
package/dist/index.js
CHANGED
|
@@ -1144,6 +1144,36 @@ var BlinkAuth = class {
|
|
|
1144
1144
|
return;
|
|
1145
1145
|
}
|
|
1146
1146
|
}
|
|
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
|
+
}
|
|
1147
1177
|
const tokensFromUrl = this.extractTokensFromUrl();
|
|
1148
1178
|
if (tokensFromUrl) {
|
|
1149
1179
|
console.log("\u{1F4E5} Found tokens in URL, setting them...");
|
|
@@ -1151,6 +1181,31 @@ var BlinkAuth = class {
|
|
|
1151
1181
|
this.clearUrlTokens();
|
|
1152
1182
|
console.log("\u2705 Auth initialization complete (from URL)");
|
|
1153
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
|
+
}
|
|
1208
|
+
}
|
|
1154
1209
|
}
|
|
1155
1210
|
const storedTokens = await this.getStoredTokens();
|
|
1156
1211
|
if (storedTokens) {
|
|
@@ -1504,37 +1559,145 @@ var BlinkAuth = class {
|
|
|
1504
1559
|
}
|
|
1505
1560
|
return this.signInWithProvider("microsoft", options);
|
|
1506
1561
|
}
|
|
1562
|
+
/**
|
|
1563
|
+
* Check if current browser is Safari (desktop, iPhone, iPad)
|
|
1564
|
+
* Safari has strict popup blocking, so we must use redirect flow
|
|
1565
|
+
*/
|
|
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
|
|
1594
|
+
});
|
|
1595
|
+
}
|
|
1596
|
+
return isSafari;
|
|
1597
|
+
}
|
|
1507
1598
|
/**
|
|
1508
1599
|
* Generic provider sign-in method (headless mode)
|
|
1600
|
+
* Uses redirect flow for Safari, popup flow for other browsers
|
|
1509
1601
|
*/
|
|
1510
1602
|
async signInWithProvider(provider, options) {
|
|
1511
1603
|
if (this.authConfig.mode !== "headless") {
|
|
1512
1604
|
throw new BlinkAuthError("INVALID_CREDENTIALS" /* INVALID_CREDENTIALS */, "signInWithProvider is only available in headless mode");
|
|
1513
1605
|
}
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
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}`;
|
|
1521
1613
|
}
|
|
1522
|
-
|
|
1614
|
+
}
|
|
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
|
+
});
|
|
1657
|
+
}
|
|
1658
|
+
return new Promise((resolve, reject) => {
|
|
1523
1659
|
const popupUrl = new URL("/auth", this.authUrl);
|
|
1524
1660
|
popupUrl.searchParams.set("provider", provider);
|
|
1525
1661
|
popupUrl.searchParams.set("project_id", this.config.projectId);
|
|
1526
1662
|
popupUrl.searchParams.set("state", state);
|
|
1527
1663
|
popupUrl.searchParams.set("mode", "popup");
|
|
1528
1664
|
popupUrl.searchParams.set("redirect_url", redirectUrl);
|
|
1665
|
+
if (typeof window !== "undefined") {
|
|
1666
|
+
popupUrl.searchParams.set("opener_origin", window.location.origin);
|
|
1667
|
+
}
|
|
1529
1668
|
const popup = window.open(
|
|
1530
1669
|
popupUrl.toString(),
|
|
1531
1670
|
"blink-auth",
|
|
1532
1671
|
"width=500,height=600,scrollbars=yes,resizable=yes"
|
|
1533
1672
|
);
|
|
1534
1673
|
if (!popup) {
|
|
1535
|
-
|
|
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();
|
|
1536
1683
|
return;
|
|
1537
1684
|
}
|
|
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
|
+
}
|
|
1538
1701
|
let timeoutId;
|
|
1539
1702
|
const messageListener = (event) => {
|
|
1540
1703
|
let allowed = false;
|
|
@@ -1544,7 +1707,12 @@ var BlinkAuth = class {
|
|
|
1544
1707
|
} catch {
|
|
1545
1708
|
}
|
|
1546
1709
|
if (event.origin === "http://localhost:3000" || event.origin === "http://localhost:3001") allowed = true;
|
|
1547
|
-
if (
|
|
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);
|
|
1548
1716
|
if (event.data?.type === "BLINK_AUTH_TOKENS") {
|
|
1549
1717
|
const { access_token, refresh_token, token_type, expires_in, refresh_expires_in, projectId, state: returnedState } = event.data;
|
|
1550
1718
|
try {
|
|
@@ -2304,17 +2472,50 @@ var BlinkAuth = class {
|
|
|
2304
2472
|
return null;
|
|
2305
2473
|
}
|
|
2306
2474
|
}
|
|
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
|
+
}
|
|
2307
2498
|
extractTokensFromUrl() {
|
|
2308
2499
|
if (typeof window === "undefined") return null;
|
|
2309
|
-
const params =
|
|
2500
|
+
const params = this.extractUrlParams();
|
|
2310
2501
|
const accessToken = params.get("access_token");
|
|
2311
2502
|
const refreshToken = params.get("refresh_token");
|
|
2503
|
+
const state = params.get("state");
|
|
2504
|
+
const error = params.get("error");
|
|
2312
2505
|
console.log("\u{1F50D} Extracting tokens from URL:", {
|
|
2313
2506
|
url: window.location.href,
|
|
2507
|
+
search: window.location.search,
|
|
2508
|
+
hash: window.location.hash,
|
|
2314
2509
|
accessToken: accessToken ? `${accessToken.substring(0, 20)}...` : null,
|
|
2315
2510
|
refreshToken: refreshToken ? `${refreshToken.substring(0, 20)}...` : null,
|
|
2511
|
+
state: state ? `${state.substring(0, 10)}...` : null,
|
|
2512
|
+
error: error || null,
|
|
2316
2513
|
allParams: Object.fromEntries(params.entries())
|
|
2317
2514
|
});
|
|
2515
|
+
if (error) {
|
|
2516
|
+
console.log("\u274C Error parameter found in URL, not extracting tokens");
|
|
2517
|
+
return null;
|
|
2518
|
+
}
|
|
2318
2519
|
if (accessToken) {
|
|
2319
2520
|
const tokens = {
|
|
2320
2521
|
access_token: accessToken,
|
|
@@ -2329,7 +2530,8 @@ var BlinkAuth = class {
|
|
|
2329
2530
|
};
|
|
2330
2531
|
console.log("\u2705 Tokens extracted successfully:", {
|
|
2331
2532
|
hasAccessToken: !!tokens.access_token,
|
|
2332
|
-
hasRefreshToken: !!tokens.refresh_token
|
|
2533
|
+
hasRefreshToken: !!tokens.refresh_token,
|
|
2534
|
+
state: state || "no state"
|
|
2333
2535
|
});
|
|
2334
2536
|
return tokens;
|
|
2335
2537
|
}
|
|
@@ -2349,8 +2551,9 @@ var BlinkAuth = class {
|
|
|
2349
2551
|
url.searchParams.delete("code");
|
|
2350
2552
|
url.searchParams.delete("error");
|
|
2351
2553
|
url.searchParams.delete("error_description");
|
|
2554
|
+
url.hash = "";
|
|
2352
2555
|
window.history.replaceState({}, "", url.toString());
|
|
2353
|
-
console.log("\u{1F9F9} URL cleaned up, removed auth parameters");
|
|
2556
|
+
console.log("\u{1F9F9} URL cleaned up, removed auth parameters from both search and hash");
|
|
2354
2557
|
}
|
|
2355
2558
|
redirectToAuth() {
|
|
2356
2559
|
if (typeof window !== "undefined") {
|
package/dist/index.mjs
CHANGED
|
@@ -1142,6 +1142,36 @@ var BlinkAuth = class {
|
|
|
1142
1142
|
return;
|
|
1143
1143
|
}
|
|
1144
1144
|
}
|
|
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
|
+
}
|
|
1145
1175
|
const tokensFromUrl = this.extractTokensFromUrl();
|
|
1146
1176
|
if (tokensFromUrl) {
|
|
1147
1177
|
console.log("\u{1F4E5} Found tokens in URL, setting them...");
|
|
@@ -1149,6 +1179,31 @@ var BlinkAuth = class {
|
|
|
1149
1179
|
this.clearUrlTokens();
|
|
1150
1180
|
console.log("\u2705 Auth initialization complete (from URL)");
|
|
1151
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
|
+
}
|
|
1206
|
+
}
|
|
1152
1207
|
}
|
|
1153
1208
|
const storedTokens = await this.getStoredTokens();
|
|
1154
1209
|
if (storedTokens) {
|
|
@@ -1502,37 +1557,145 @@ var BlinkAuth = class {
|
|
|
1502
1557
|
}
|
|
1503
1558
|
return this.signInWithProvider("microsoft", options);
|
|
1504
1559
|
}
|
|
1560
|
+
/**
|
|
1561
|
+
* Check if current browser is Safari (desktop, iPhone, iPad)
|
|
1562
|
+
* Safari has strict popup blocking, so we must use redirect flow
|
|
1563
|
+
*/
|
|
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
|
|
1592
|
+
});
|
|
1593
|
+
}
|
|
1594
|
+
return isSafari;
|
|
1595
|
+
}
|
|
1505
1596
|
/**
|
|
1506
1597
|
* Generic provider sign-in method (headless mode)
|
|
1598
|
+
* Uses redirect flow for Safari, popup flow for other browsers
|
|
1507
1599
|
*/
|
|
1508
1600
|
async signInWithProvider(provider, options) {
|
|
1509
1601
|
if (this.authConfig.mode !== "headless") {
|
|
1510
1602
|
throw new BlinkAuthError("INVALID_CREDENTIALS" /* INVALID_CREDENTIALS */, "signInWithProvider is only available in headless mode");
|
|
1511
1603
|
}
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
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}`;
|
|
1519
1611
|
}
|
|
1520
|
-
|
|
1612
|
+
}
|
|
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
|
+
});
|
|
1655
|
+
}
|
|
1656
|
+
return new Promise((resolve, reject) => {
|
|
1521
1657
|
const popupUrl = new URL("/auth", this.authUrl);
|
|
1522
1658
|
popupUrl.searchParams.set("provider", provider);
|
|
1523
1659
|
popupUrl.searchParams.set("project_id", this.config.projectId);
|
|
1524
1660
|
popupUrl.searchParams.set("state", state);
|
|
1525
1661
|
popupUrl.searchParams.set("mode", "popup");
|
|
1526
1662
|
popupUrl.searchParams.set("redirect_url", redirectUrl);
|
|
1663
|
+
if (typeof window !== "undefined") {
|
|
1664
|
+
popupUrl.searchParams.set("opener_origin", window.location.origin);
|
|
1665
|
+
}
|
|
1527
1666
|
const popup = window.open(
|
|
1528
1667
|
popupUrl.toString(),
|
|
1529
1668
|
"blink-auth",
|
|
1530
1669
|
"width=500,height=600,scrollbars=yes,resizable=yes"
|
|
1531
1670
|
);
|
|
1532
1671
|
if (!popup) {
|
|
1533
|
-
|
|
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();
|
|
1534
1681
|
return;
|
|
1535
1682
|
}
|
|
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
|
+
}
|
|
1536
1699
|
let timeoutId;
|
|
1537
1700
|
const messageListener = (event) => {
|
|
1538
1701
|
let allowed = false;
|
|
@@ -1542,7 +1705,12 @@ var BlinkAuth = class {
|
|
|
1542
1705
|
} catch {
|
|
1543
1706
|
}
|
|
1544
1707
|
if (event.origin === "http://localhost:3000" || event.origin === "http://localhost:3001") allowed = true;
|
|
1545
|
-
if (
|
|
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);
|
|
1546
1714
|
if (event.data?.type === "BLINK_AUTH_TOKENS") {
|
|
1547
1715
|
const { access_token, refresh_token, token_type, expires_in, refresh_expires_in, projectId, state: returnedState } = event.data;
|
|
1548
1716
|
try {
|
|
@@ -2302,17 +2470,50 @@ var BlinkAuth = class {
|
|
|
2302
2470
|
return null;
|
|
2303
2471
|
}
|
|
2304
2472
|
}
|
|
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
|
+
}
|
|
2305
2496
|
extractTokensFromUrl() {
|
|
2306
2497
|
if (typeof window === "undefined") return null;
|
|
2307
|
-
const params =
|
|
2498
|
+
const params = this.extractUrlParams();
|
|
2308
2499
|
const accessToken = params.get("access_token");
|
|
2309
2500
|
const refreshToken = params.get("refresh_token");
|
|
2501
|
+
const state = params.get("state");
|
|
2502
|
+
const error = params.get("error");
|
|
2310
2503
|
console.log("\u{1F50D} Extracting tokens from URL:", {
|
|
2311
2504
|
url: window.location.href,
|
|
2505
|
+
search: window.location.search,
|
|
2506
|
+
hash: window.location.hash,
|
|
2312
2507
|
accessToken: accessToken ? `${accessToken.substring(0, 20)}...` : null,
|
|
2313
2508
|
refreshToken: refreshToken ? `${refreshToken.substring(0, 20)}...` : null,
|
|
2509
|
+
state: state ? `${state.substring(0, 10)}...` : null,
|
|
2510
|
+
error: error || null,
|
|
2314
2511
|
allParams: Object.fromEntries(params.entries())
|
|
2315
2512
|
});
|
|
2513
|
+
if (error) {
|
|
2514
|
+
console.log("\u274C Error parameter found in URL, not extracting tokens");
|
|
2515
|
+
return null;
|
|
2516
|
+
}
|
|
2316
2517
|
if (accessToken) {
|
|
2317
2518
|
const tokens = {
|
|
2318
2519
|
access_token: accessToken,
|
|
@@ -2327,7 +2528,8 @@ var BlinkAuth = class {
|
|
|
2327
2528
|
};
|
|
2328
2529
|
console.log("\u2705 Tokens extracted successfully:", {
|
|
2329
2530
|
hasAccessToken: !!tokens.access_token,
|
|
2330
|
-
hasRefreshToken: !!tokens.refresh_token
|
|
2531
|
+
hasRefreshToken: !!tokens.refresh_token,
|
|
2532
|
+
state: state || "no state"
|
|
2331
2533
|
});
|
|
2332
2534
|
return tokens;
|
|
2333
2535
|
}
|
|
@@ -2347,8 +2549,9 @@ var BlinkAuth = class {
|
|
|
2347
2549
|
url.searchParams.delete("code");
|
|
2348
2550
|
url.searchParams.delete("error");
|
|
2349
2551
|
url.searchParams.delete("error_description");
|
|
2552
|
+
url.hash = "";
|
|
2350
2553
|
window.history.replaceState({}, "", url.toString());
|
|
2351
|
-
console.log("\u{1F9F9} URL cleaned up, removed auth parameters");
|
|
2554
|
+
console.log("\u{1F9F9} URL cleaned up, removed auth parameters from both search and hash");
|
|
2352
2555
|
}
|
|
2353
2556
|
redirectToAuth() {
|
|
2354
2557
|
if (typeof window !== "undefined") {
|
package/package.json
CHANGED