@blinkdotnew/sdk 2.0.0 → 2.0.1
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/README.md +179 -23
- package/dist/index.d.mts +238 -6
- package/dist/index.d.ts +238 -6
- package/dist/index.js +632 -174
- package/dist/index.mjs +632 -174
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -676,7 +676,7 @@ var HttpClient = class {
|
|
|
676
676
|
});
|
|
677
677
|
}
|
|
678
678
|
/**
|
|
679
|
-
* Stream AI text generation
|
|
679
|
+
* Stream AI text generation with Vercel AI SDK data stream format
|
|
680
680
|
*/
|
|
681
681
|
async streamAiText(prompt, options = {}, onChunk) {
|
|
682
682
|
const url = this.buildUrl(`/api/ai/${this.projectId}/text`);
|
|
@@ -706,7 +706,7 @@ var HttpClient = class {
|
|
|
706
706
|
if (!response.body) {
|
|
707
707
|
throw new BlinkNetworkError("No response body for streaming");
|
|
708
708
|
}
|
|
709
|
-
return this.
|
|
709
|
+
return this.parseDataStream(response.body, onChunk);
|
|
710
710
|
} catch (error) {
|
|
711
711
|
if (error instanceof BlinkError) {
|
|
712
712
|
throw error;
|
|
@@ -731,7 +731,7 @@ var HttpClient = class {
|
|
|
731
731
|
});
|
|
732
732
|
}
|
|
733
733
|
/**
|
|
734
|
-
* Stream AI object generation
|
|
734
|
+
* Stream AI object generation with Vercel AI SDK data stream format
|
|
735
735
|
*/
|
|
736
736
|
async streamAiObject(prompt, options = {}, onPartial) {
|
|
737
737
|
const url = this.buildUrl(`/api/ai/${this.projectId}/object`);
|
|
@@ -761,35 +761,7 @@ var HttpClient = class {
|
|
|
761
761
|
if (!response.body) {
|
|
762
762
|
throw new BlinkNetworkError("No response body for streaming");
|
|
763
763
|
}
|
|
764
|
-
|
|
765
|
-
const decoder = new TextDecoder();
|
|
766
|
-
let buffer = "";
|
|
767
|
-
let latestObject = {};
|
|
768
|
-
try {
|
|
769
|
-
while (true) {
|
|
770
|
-
const { done, value } = await reader.read();
|
|
771
|
-
if (done) break;
|
|
772
|
-
const chunk = decoder.decode(value, { stream: true });
|
|
773
|
-
buffer += chunk;
|
|
774
|
-
try {
|
|
775
|
-
const parsed = JSON.parse(buffer);
|
|
776
|
-
latestObject = parsed;
|
|
777
|
-
if (onPartial) {
|
|
778
|
-
onPartial(parsed);
|
|
779
|
-
}
|
|
780
|
-
} catch {
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
if (buffer) {
|
|
784
|
-
try {
|
|
785
|
-
latestObject = JSON.parse(buffer);
|
|
786
|
-
} catch {
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
return { object: latestObject };
|
|
790
|
-
} finally {
|
|
791
|
-
reader.releaseLock();
|
|
792
|
-
}
|
|
764
|
+
return this.parseDataStream(response.body, void 0, onPartial);
|
|
793
765
|
} catch (error) {
|
|
794
766
|
if (error instanceof BlinkError) {
|
|
795
767
|
throw error;
|
|
@@ -952,94 +924,93 @@ var HttpClient = class {
|
|
|
952
924
|
}
|
|
953
925
|
}
|
|
954
926
|
/**
|
|
955
|
-
* Parse Vercel AI SDK
|
|
956
|
-
*
|
|
927
|
+
* Parse Vercel AI SDK data stream format
|
|
928
|
+
* Handles text chunks (0:"text"), partial objects (2:[...]), and metadata (d:, e:)
|
|
957
929
|
*/
|
|
958
|
-
async
|
|
930
|
+
async parseDataStream(body, onChunk, onPartial) {
|
|
959
931
|
const reader = body.getReader();
|
|
960
932
|
const decoder = new TextDecoder();
|
|
961
|
-
const finalResult = {
|
|
962
|
-
text: "",
|
|
963
|
-
toolCalls: [],
|
|
964
|
-
toolResults: [],
|
|
965
|
-
sources: [],
|
|
966
|
-
files: [],
|
|
967
|
-
reasoning: []
|
|
968
|
-
};
|
|
969
933
|
let buffer = "";
|
|
934
|
+
let finalResult = {};
|
|
970
935
|
try {
|
|
971
936
|
while (true) {
|
|
972
937
|
const { done, value } = await reader.read();
|
|
973
938
|
if (done) break;
|
|
974
939
|
buffer += decoder.decode(value, { stream: true });
|
|
975
|
-
const lines = buffer.split(
|
|
940
|
+
const lines = buffer.split(/\r?\n/);
|
|
976
941
|
buffer = lines.pop() || "";
|
|
977
942
|
for (const line of lines) {
|
|
978
943
|
if (!line.trim()) continue;
|
|
979
|
-
if (line === "[DONE]") {
|
|
980
|
-
continue;
|
|
981
|
-
}
|
|
982
|
-
if (!line.startsWith("data: ")) continue;
|
|
983
944
|
try {
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
945
|
+
if (line.startsWith("f:")) {
|
|
946
|
+
const metadata = JSON.parse(line.slice(2));
|
|
947
|
+
finalResult.messageId = metadata.messageId;
|
|
948
|
+
} else if (line.startsWith("0:")) {
|
|
949
|
+
const textChunk = JSON.parse(line.slice(2));
|
|
950
|
+
if (onChunk) {
|
|
951
|
+
onChunk(textChunk);
|
|
952
|
+
}
|
|
953
|
+
finalResult.text = (finalResult.text || "") + textChunk;
|
|
954
|
+
} else if (line.startsWith("2:")) {
|
|
955
|
+
const data = JSON.parse(line.slice(2));
|
|
956
|
+
if (Array.isArray(data) && data.length > 0) {
|
|
957
|
+
const item = data[0];
|
|
958
|
+
if (typeof item === "string") {
|
|
959
|
+
finalResult.status = item;
|
|
960
|
+
} else if (typeof item === "object") {
|
|
961
|
+
if (onPartial) {
|
|
962
|
+
onPartial(item);
|
|
963
|
+
}
|
|
964
|
+
finalResult.object = item;
|
|
993
965
|
}
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
966
|
+
}
|
|
967
|
+
} else if (line.startsWith("d:")) {
|
|
968
|
+
const metadata = JSON.parse(line.slice(2));
|
|
969
|
+
if (metadata.usage) {
|
|
970
|
+
finalResult.usage = metadata.usage;
|
|
971
|
+
}
|
|
972
|
+
if (metadata.finishReason) {
|
|
973
|
+
finalResult.finishReason = metadata.finishReason;
|
|
974
|
+
}
|
|
975
|
+
} else if (line.startsWith("e:")) {
|
|
976
|
+
const errorData = JSON.parse(line.slice(2));
|
|
977
|
+
finalResult.error = errorData;
|
|
978
|
+
}
|
|
979
|
+
} catch (error) {
|
|
980
|
+
console.warn("Failed to parse stream line:", line, error);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
if (buffer.trim()) {
|
|
985
|
+
try {
|
|
986
|
+
if (buffer.startsWith("0:")) {
|
|
987
|
+
const textChunk = JSON.parse(buffer.slice(2));
|
|
988
|
+
if (onChunk) {
|
|
989
|
+
onChunk(textChunk);
|
|
990
|
+
}
|
|
991
|
+
finalResult.text = (finalResult.text || "") + textChunk;
|
|
992
|
+
} else if (buffer.startsWith("2:")) {
|
|
993
|
+
const data = JSON.parse(buffer.slice(2));
|
|
994
|
+
if (Array.isArray(data) && data.length > 0) {
|
|
995
|
+
const item = data[0];
|
|
996
|
+
if (typeof item === "object") {
|
|
997
|
+
if (onPartial) {
|
|
998
|
+
onPartial(item);
|
|
997
999
|
}
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
case "tool-result":
|
|
1009
|
-
finalResult.toolResults.push({
|
|
1010
|
-
toolCallId: part.toolCallId,
|
|
1011
|
-
toolName: part.toolName,
|
|
1012
|
-
result: part.result
|
|
1013
|
-
});
|
|
1014
|
-
break;
|
|
1015
|
-
case "source-url":
|
|
1016
|
-
finalResult.sources.push({
|
|
1017
|
-
id: part.id,
|
|
1018
|
-
url: part.url,
|
|
1019
|
-
title: part.title
|
|
1020
|
-
});
|
|
1021
|
-
break;
|
|
1022
|
-
case "file":
|
|
1023
|
-
finalResult.files.push(part.file);
|
|
1024
|
-
break;
|
|
1025
|
-
case "reasoning":
|
|
1026
|
-
finalResult.reasoning.push(part.content);
|
|
1027
|
-
break;
|
|
1028
|
-
case "finish":
|
|
1029
|
-
finalResult.finishReason = part.finishReason;
|
|
1030
|
-
finalResult.usage = part.usage;
|
|
1031
|
-
if (part.response) finalResult.response = part.response;
|
|
1032
|
-
break;
|
|
1033
|
-
case "error":
|
|
1034
|
-
finalResult.error = part.error;
|
|
1035
|
-
throw new Error(part.error);
|
|
1036
|
-
case "data":
|
|
1037
|
-
if (!finalResult.customData) finalResult.customData = [];
|
|
1038
|
-
finalResult.customData.push(part.value);
|
|
1039
|
-
break;
|
|
1000
|
+
finalResult.object = item;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
} else if (buffer.startsWith("d:")) {
|
|
1004
|
+
const metadata = JSON.parse(buffer.slice(2));
|
|
1005
|
+
if (metadata.usage) {
|
|
1006
|
+
finalResult.usage = metadata.usage;
|
|
1007
|
+
}
|
|
1008
|
+
if (metadata.finishReason) {
|
|
1009
|
+
finalResult.finishReason = metadata.finishReason;
|
|
1040
1010
|
}
|
|
1041
|
-
} catch (e) {
|
|
1042
1011
|
}
|
|
1012
|
+
} catch (error) {
|
|
1013
|
+
console.warn("Failed to parse final buffer:", buffer, error);
|
|
1043
1014
|
}
|
|
1044
1015
|
}
|
|
1045
1016
|
return finalResult;
|
|
@@ -1049,6 +1020,142 @@ var HttpClient = class {
|
|
|
1049
1020
|
}
|
|
1050
1021
|
};
|
|
1051
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
|
+
|
|
1052
1159
|
// src/auth.ts
|
|
1053
1160
|
var BlinkAuth = class {
|
|
1054
1161
|
config;
|
|
@@ -1072,11 +1179,14 @@ var BlinkAuth = class {
|
|
|
1072
1179
|
// Default mode
|
|
1073
1180
|
authUrl: "https://blink.new",
|
|
1074
1181
|
coreUrl: "https://core.blink.new",
|
|
1182
|
+
detectSessionInUrl: true,
|
|
1183
|
+
// Default to true for web compatibility
|
|
1075
1184
|
...config.auth
|
|
1076
1185
|
};
|
|
1077
1186
|
this.authUrl = this.authConfig.authUrl || "https://blink.new";
|
|
1078
1187
|
this.coreUrl = this.authConfig.coreUrl || "https://core.blink.new";
|
|
1079
|
-
|
|
1188
|
+
const hostname = getLocationHostname();
|
|
1189
|
+
if (hostname && this.authUrl === "https://blink.new" && (hostname === "localhost" || hostname === "127.0.0.1")) {
|
|
1080
1190
|
console.warn("\u26A0\uFE0F Using default authUrl in development. Set auth.authUrl to your app origin for headless auth endpoints to work.");
|
|
1081
1191
|
}
|
|
1082
1192
|
if (config.authRequired !== void 0 && !config.auth?.mode) {
|
|
@@ -1090,7 +1200,7 @@ var BlinkAuth = class {
|
|
|
1090
1200
|
};
|
|
1091
1201
|
this.storage = config.auth?.storage || config.storage || getDefaultStorageAdapter();
|
|
1092
1202
|
if (isWeb) {
|
|
1093
|
-
this.isIframe =
|
|
1203
|
+
this.isIframe = isIframe();
|
|
1094
1204
|
this.setupParentWindowListener();
|
|
1095
1205
|
this.setupCrossTabSync();
|
|
1096
1206
|
this.initializationPromise = this.initialize();
|
|
@@ -1126,7 +1236,7 @@ var BlinkAuth = class {
|
|
|
1126
1236
|
* Setup listener for tokens from parent window
|
|
1127
1237
|
*/
|
|
1128
1238
|
setupParentWindowListener() {
|
|
1129
|
-
if (!isWeb || !this.isIframe) return;
|
|
1239
|
+
if (!isWeb || !this.isIframe || !hasWindow()) return;
|
|
1130
1240
|
window.addEventListener("message", (event) => {
|
|
1131
1241
|
if (event.origin !== "https://blink.new" && event.origin !== "http://localhost:3000" && event.origin !== "http://localhost:3001") {
|
|
1132
1242
|
return;
|
|
@@ -1148,7 +1258,7 @@ var BlinkAuth = class {
|
|
|
1148
1258
|
this.clearTokens();
|
|
1149
1259
|
}
|
|
1150
1260
|
});
|
|
1151
|
-
if (window.parent !== window) {
|
|
1261
|
+
if (hasWindow() && window.parent !== window) {
|
|
1152
1262
|
console.log("\u{1F504} Requesting auth tokens from parent window");
|
|
1153
1263
|
window.parent.postMessage({
|
|
1154
1264
|
type: "BLINK_REQUEST_AUTH_TOKENS",
|
|
@@ -1173,13 +1283,15 @@ var BlinkAuth = class {
|
|
|
1173
1283
|
return;
|
|
1174
1284
|
}
|
|
1175
1285
|
}
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1286
|
+
if (this.authConfig.detectSessionInUrl !== false) {
|
|
1287
|
+
const tokensFromUrl = this.extractTokensFromUrl();
|
|
1288
|
+
if (tokensFromUrl) {
|
|
1289
|
+
console.log("\u{1F4E5} Found tokens in URL, setting them...");
|
|
1290
|
+
await this.setTokens(tokensFromUrl, true);
|
|
1291
|
+
this.clearUrlTokens();
|
|
1292
|
+
console.log("\u2705 Auth initialization complete (from URL)");
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1183
1295
|
}
|
|
1184
1296
|
const storedTokens = await this.getStoredTokens();
|
|
1185
1297
|
if (storedTokens) {
|
|
@@ -1203,11 +1315,11 @@ var BlinkAuth = class {
|
|
|
1203
1315
|
}
|
|
1204
1316
|
}
|
|
1205
1317
|
console.log("\u274C No tokens found");
|
|
1206
|
-
if (this.config.authRequired) {
|
|
1318
|
+
if (this.config.authRequired && hasWindowLocation()) {
|
|
1207
1319
|
console.log("\u{1F504} Auth required, redirecting to auth page...");
|
|
1208
1320
|
this.redirectToAuth();
|
|
1209
1321
|
} else {
|
|
1210
|
-
console.log("\u26A0\uFE0F Auth not required, continuing without authentication");
|
|
1322
|
+
console.log("\u26A0\uFE0F Auth not required or no window.location, continuing without authentication");
|
|
1211
1323
|
}
|
|
1212
1324
|
} finally {
|
|
1213
1325
|
this.setLoading(false);
|
|
@@ -1218,15 +1330,20 @@ var BlinkAuth = class {
|
|
|
1218
1330
|
* Redirect to Blink auth page
|
|
1219
1331
|
*/
|
|
1220
1332
|
login(nextUrl) {
|
|
1333
|
+
if (!hasWindowLocation()) {
|
|
1334
|
+
console.warn("login() called in non-browser environment (no window.location available)");
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1221
1337
|
let redirectUrl = nextUrl || this.authConfig.redirectUrl;
|
|
1222
|
-
if (!redirectUrl
|
|
1223
|
-
|
|
1224
|
-
|
|
1338
|
+
if (!redirectUrl) {
|
|
1339
|
+
const href = getLocationHref();
|
|
1340
|
+
if (href && href.startsWith("http")) {
|
|
1341
|
+
redirectUrl = href;
|
|
1225
1342
|
} else {
|
|
1226
|
-
redirectUrl =
|
|
1343
|
+
redirectUrl = constructFullUrl() || void 0;
|
|
1227
1344
|
}
|
|
1228
1345
|
}
|
|
1229
|
-
if (redirectUrl
|
|
1346
|
+
if (redirectUrl) {
|
|
1230
1347
|
try {
|
|
1231
1348
|
const url = new URL(redirectUrl);
|
|
1232
1349
|
url.searchParams.delete("redirect_url");
|
|
@@ -1241,16 +1358,14 @@ var BlinkAuth = class {
|
|
|
1241
1358
|
if (this.config.projectId) {
|
|
1242
1359
|
authUrl.searchParams.set("project_id", this.config.projectId);
|
|
1243
1360
|
}
|
|
1244
|
-
|
|
1245
|
-
window.location.href = authUrl.toString();
|
|
1246
|
-
}
|
|
1361
|
+
window.location.href = authUrl.toString();
|
|
1247
1362
|
}
|
|
1248
1363
|
/**
|
|
1249
1364
|
* Logout and clear stored tokens
|
|
1250
1365
|
*/
|
|
1251
1366
|
logout(redirectUrl) {
|
|
1252
1367
|
this.clearTokens();
|
|
1253
|
-
if (redirectUrl &&
|
|
1368
|
+
if (redirectUrl && hasWindowLocation()) {
|
|
1254
1369
|
window.location.href = redirectUrl;
|
|
1255
1370
|
}
|
|
1256
1371
|
}
|
|
@@ -1499,6 +1614,16 @@ var BlinkAuth = class {
|
|
|
1499
1614
|
}
|
|
1500
1615
|
/**
|
|
1501
1616
|
* Sign in with Google (headless mode)
|
|
1617
|
+
*
|
|
1618
|
+
* **Universal OAuth** - Works on both Web and React Native!
|
|
1619
|
+
*
|
|
1620
|
+
* On React Native, requires `webBrowser` to be configured in client:
|
|
1621
|
+
* ```typescript
|
|
1622
|
+
* const blink = createClient({
|
|
1623
|
+
* auth: { mode: 'headless', webBrowser: WebBrowser }
|
|
1624
|
+
* })
|
|
1625
|
+
* await blink.auth.signInWithGoogle() // Works on both platforms!
|
|
1626
|
+
* ```
|
|
1502
1627
|
*/
|
|
1503
1628
|
async signInWithGoogle(options) {
|
|
1504
1629
|
if (this.authConfig.mode !== "headless") {
|
|
@@ -1508,6 +1633,9 @@ var BlinkAuth = class {
|
|
|
1508
1633
|
}
|
|
1509
1634
|
/**
|
|
1510
1635
|
* Sign in with GitHub (headless mode)
|
|
1636
|
+
*
|
|
1637
|
+
* **Universal OAuth** - Works on both Web and React Native!
|
|
1638
|
+
* See signInWithGoogle() for setup instructions.
|
|
1511
1639
|
*/
|
|
1512
1640
|
async signInWithGitHub(options) {
|
|
1513
1641
|
if (this.authConfig.mode !== "headless") {
|
|
@@ -1517,6 +1645,9 @@ var BlinkAuth = class {
|
|
|
1517
1645
|
}
|
|
1518
1646
|
/**
|
|
1519
1647
|
* Sign in with Apple (headless mode)
|
|
1648
|
+
*
|
|
1649
|
+
* **Universal OAuth** - Works on both Web and React Native!
|
|
1650
|
+
* See signInWithGoogle() for setup instructions.
|
|
1520
1651
|
*/
|
|
1521
1652
|
async signInWithApple(options) {
|
|
1522
1653
|
if (this.authConfig.mode !== "headless") {
|
|
@@ -1526,6 +1657,9 @@ var BlinkAuth = class {
|
|
|
1526
1657
|
}
|
|
1527
1658
|
/**
|
|
1528
1659
|
* Sign in with Microsoft (headless mode)
|
|
1660
|
+
*
|
|
1661
|
+
* **Universal OAuth** - Works on both Web and React Native!
|
|
1662
|
+
* See signInWithGoogle() for setup instructions.
|
|
1529
1663
|
*/
|
|
1530
1664
|
async signInWithMicrosoft(options) {
|
|
1531
1665
|
if (this.authConfig.mode !== "headless") {
|
|
@@ -1533,28 +1667,267 @@ var BlinkAuth = class {
|
|
|
1533
1667
|
}
|
|
1534
1668
|
return this.signInWithProvider("microsoft", options);
|
|
1535
1669
|
}
|
|
1670
|
+
/**
|
|
1671
|
+
* Initiate OAuth for mobile without deep linking (expo-web-browser pattern)
|
|
1672
|
+
*
|
|
1673
|
+
* This method:
|
|
1674
|
+
* 1. Generates a unique session ID
|
|
1675
|
+
* 2. Returns OAuth URL with session parameter
|
|
1676
|
+
* 3. App opens URL in expo-web-browser
|
|
1677
|
+
* 4. App polls checkMobileOAuthSession() until complete
|
|
1678
|
+
*
|
|
1679
|
+
* @param provider - OAuth provider (google, github, apple, etc.)
|
|
1680
|
+
* @param options - Optional metadata
|
|
1681
|
+
* @returns Session ID and OAuth URL
|
|
1682
|
+
*
|
|
1683
|
+
* @example
|
|
1684
|
+
* // React Native with expo-web-browser
|
|
1685
|
+
* import * as WebBrowser from 'expo-web-browser';
|
|
1686
|
+
*
|
|
1687
|
+
* const { sessionId, authUrl } = await blink.auth.initiateMobileOAuth('google');
|
|
1688
|
+
*
|
|
1689
|
+
* // Open browser
|
|
1690
|
+
* await WebBrowser.openAuthSessionAsync(authUrl);
|
|
1691
|
+
*
|
|
1692
|
+
* // Poll for completion
|
|
1693
|
+
* const user = await blink.auth.pollMobileOAuthSession(sessionId);
|
|
1694
|
+
* console.log('Authenticated:', user.email);
|
|
1695
|
+
*/
|
|
1696
|
+
async initiateMobileOAuth(provider, options) {
|
|
1697
|
+
if (this.authConfig.mode !== "headless") {
|
|
1698
|
+
throw new BlinkAuthError(
|
|
1699
|
+
"INVALID_CREDENTIALS" /* INVALID_CREDENTIALS */,
|
|
1700
|
+
"initiateMobileOAuth is only available in headless mode"
|
|
1701
|
+
);
|
|
1702
|
+
}
|
|
1703
|
+
const sessionId = this.generateSessionId();
|
|
1704
|
+
const authUrl = new URL("/auth", this.authUrl);
|
|
1705
|
+
authUrl.searchParams.set("provider", provider);
|
|
1706
|
+
authUrl.searchParams.set("project_id", this.config.projectId);
|
|
1707
|
+
authUrl.searchParams.set("mode", "mobile-session");
|
|
1708
|
+
authUrl.searchParams.set("session_id", sessionId);
|
|
1709
|
+
if (options?.metadata) {
|
|
1710
|
+
authUrl.searchParams.set("metadata", JSON.stringify(options.metadata));
|
|
1711
|
+
}
|
|
1712
|
+
return {
|
|
1713
|
+
sessionId,
|
|
1714
|
+
authUrl: authUrl.toString()
|
|
1715
|
+
};
|
|
1716
|
+
}
|
|
1717
|
+
/**
|
|
1718
|
+
* Check mobile OAuth session status (single check)
|
|
1719
|
+
*
|
|
1720
|
+
* @param sessionId - Session ID from initiateMobileOAuth
|
|
1721
|
+
* @returns Tokens if session is complete, null if still pending
|
|
1722
|
+
*/
|
|
1723
|
+
async checkMobileOAuthSession(sessionId) {
|
|
1724
|
+
try {
|
|
1725
|
+
const response = await fetch(`${this.authUrl}/api/auth/mobile-session/${sessionId}`, {
|
|
1726
|
+
method: "GET",
|
|
1727
|
+
headers: {
|
|
1728
|
+
"Content-Type": "application/json"
|
|
1729
|
+
}
|
|
1730
|
+
});
|
|
1731
|
+
if (response.status === 404 || response.status === 202) {
|
|
1732
|
+
return null;
|
|
1733
|
+
}
|
|
1734
|
+
if (!response.ok) {
|
|
1735
|
+
const errorData = await response.json();
|
|
1736
|
+
const errorCode = this.mapErrorCodeFromResponse(errorData.code);
|
|
1737
|
+
throw new BlinkAuthError(
|
|
1738
|
+
errorCode,
|
|
1739
|
+
errorData.error || "Failed to check OAuth session"
|
|
1740
|
+
);
|
|
1741
|
+
}
|
|
1742
|
+
const data = await response.json();
|
|
1743
|
+
return {
|
|
1744
|
+
access_token: data.access_token,
|
|
1745
|
+
refresh_token: data.refresh_token,
|
|
1746
|
+
token_type: data.token_type || "Bearer",
|
|
1747
|
+
expires_in: data.expires_in || 3600,
|
|
1748
|
+
refresh_expires_in: data.refresh_expires_in
|
|
1749
|
+
};
|
|
1750
|
+
} catch (error) {
|
|
1751
|
+
if (error instanceof BlinkAuthError) {
|
|
1752
|
+
throw error;
|
|
1753
|
+
}
|
|
1754
|
+
throw new BlinkAuthError(
|
|
1755
|
+
"NETWORK_ERROR" /* NETWORK_ERROR */,
|
|
1756
|
+
`Network error: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1757
|
+
);
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
/**
|
|
1761
|
+
* Poll mobile OAuth session until complete (convenience method)
|
|
1762
|
+
*
|
|
1763
|
+
* @param sessionId - Session ID from initiateMobileOAuth
|
|
1764
|
+
* @param options - Polling options
|
|
1765
|
+
* @returns Authenticated user
|
|
1766
|
+
*
|
|
1767
|
+
* @example
|
|
1768
|
+
* const { sessionId, authUrl } = await blink.auth.initiateMobileOAuth('google');
|
|
1769
|
+
* await WebBrowser.openAuthSessionAsync(authUrl);
|
|
1770
|
+
* const user = await blink.auth.pollMobileOAuthSession(sessionId, {
|
|
1771
|
+
* maxAttempts: 60,
|
|
1772
|
+
* intervalMs: 1000
|
|
1773
|
+
* });
|
|
1774
|
+
*/
|
|
1775
|
+
async pollMobileOAuthSession(sessionId, options) {
|
|
1776
|
+
const maxAttempts = options?.maxAttempts || 60;
|
|
1777
|
+
const intervalMs = options?.intervalMs || 1e3;
|
|
1778
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1779
|
+
const tokens = await this.checkMobileOAuthSession(sessionId);
|
|
1780
|
+
if (tokens) {
|
|
1781
|
+
await this.setTokens(tokens, true);
|
|
1782
|
+
return this.authState.user;
|
|
1783
|
+
}
|
|
1784
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
1785
|
+
}
|
|
1786
|
+
throw new BlinkAuthError(
|
|
1787
|
+
"AUTH_TIMEOUT" /* AUTH_TIMEOUT */,
|
|
1788
|
+
"Mobile OAuth session timed out"
|
|
1789
|
+
);
|
|
1790
|
+
}
|
|
1791
|
+
/**
|
|
1792
|
+
* Sign in with OAuth provider using expo-web-browser (React Native)
|
|
1793
|
+
*
|
|
1794
|
+
* This is a convenience method that handles the entire flow:
|
|
1795
|
+
* 1. Initiates mobile OAuth session
|
|
1796
|
+
* 2. Returns auth URL to open in WebBrowser
|
|
1797
|
+
* 3. Provides polling function to call after browser opens
|
|
1798
|
+
*
|
|
1799
|
+
* @param provider - OAuth provider
|
|
1800
|
+
* @returns Object with authUrl and authenticate function
|
|
1801
|
+
*
|
|
1802
|
+
* @example
|
|
1803
|
+
* import * as WebBrowser from 'expo-web-browser';
|
|
1804
|
+
*
|
|
1805
|
+
* const { authUrl, authenticate } = await blink.auth.signInWithProviderMobile('google');
|
|
1806
|
+
*
|
|
1807
|
+
* // Open browser
|
|
1808
|
+
* await WebBrowser.openAuthSessionAsync(authUrl);
|
|
1809
|
+
*
|
|
1810
|
+
* // Wait for authentication
|
|
1811
|
+
* const user = await authenticate();
|
|
1812
|
+
*/
|
|
1813
|
+
async signInWithProviderMobile(provider, options) {
|
|
1814
|
+
const { sessionId, authUrl } = await this.initiateMobileOAuth(provider, options);
|
|
1815
|
+
return {
|
|
1816
|
+
authUrl,
|
|
1817
|
+
authenticate: () => this.pollMobileOAuthSession(sessionId, {
|
|
1818
|
+
maxAttempts: 60,
|
|
1819
|
+
intervalMs: 1e3
|
|
1820
|
+
})
|
|
1821
|
+
};
|
|
1822
|
+
}
|
|
1823
|
+
/**
|
|
1824
|
+
* Universal OAuth flow using session-based authentication (internal)
|
|
1825
|
+
* Works on ALL platforms: Web, iOS, Android
|
|
1826
|
+
* Uses expo-web-browser to open auth URL and polls for completion
|
|
1827
|
+
*/
|
|
1828
|
+
async signInWithProviderUniversal(provider, options) {
|
|
1829
|
+
const webBrowser = this.authConfig.webBrowser;
|
|
1830
|
+
if (!webBrowser) {
|
|
1831
|
+
throw new BlinkAuthError(
|
|
1832
|
+
"NETWORK_ERROR" /* NETWORK_ERROR */,
|
|
1833
|
+
"webBrowser module is required for universal OAuth flow"
|
|
1834
|
+
);
|
|
1835
|
+
}
|
|
1836
|
+
const { sessionId, authUrl } = await this.initiateMobileOAuth(provider, options);
|
|
1837
|
+
console.log("\u{1F510} Opening OAuth browser for", provider);
|
|
1838
|
+
const result = await webBrowser.openAuthSessionAsync(authUrl);
|
|
1839
|
+
console.log("\u{1F510} Browser closed with result:", result.type);
|
|
1840
|
+
try {
|
|
1841
|
+
const user = await this.pollMobileOAuthSession(sessionId, {
|
|
1842
|
+
maxAttempts: 60,
|
|
1843
|
+
// 30 seconds (500ms intervals)
|
|
1844
|
+
intervalMs: 500
|
|
1845
|
+
});
|
|
1846
|
+
console.log("\u2705 OAuth completed successfully");
|
|
1847
|
+
return user;
|
|
1848
|
+
} catch (pollError) {
|
|
1849
|
+
if (result.type === "cancel" || result.type === "dismiss") {
|
|
1850
|
+
throw new BlinkAuthError(
|
|
1851
|
+
"POPUP_CANCELED" /* POPUP_CANCELED */,
|
|
1852
|
+
"Authentication was canceled"
|
|
1853
|
+
);
|
|
1854
|
+
}
|
|
1855
|
+
throw pollError;
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1536
1858
|
/**
|
|
1537
1859
|
* Generic provider sign-in method (headless mode)
|
|
1860
|
+
*
|
|
1861
|
+
* **Universal OAuth** - Works seamlessly on both Web and React Native!
|
|
1862
|
+
*
|
|
1863
|
+
* When `webBrowser` is configured in the client, this method automatically
|
|
1864
|
+
* uses the session-based OAuth flow that works on ALL platforms.
|
|
1865
|
+
*
|
|
1866
|
+
* **Universal Setup (configure once, works everywhere):**
|
|
1867
|
+
* ```typescript
|
|
1868
|
+
* import * as WebBrowser from 'expo-web-browser'
|
|
1869
|
+
* import AsyncStorage from '@react-native-async-storage/async-storage'
|
|
1870
|
+
*
|
|
1871
|
+
* const blink = createClient({
|
|
1872
|
+
* projectId: 'your-project',
|
|
1873
|
+
* auth: {
|
|
1874
|
+
* mode: 'headless',
|
|
1875
|
+
* webBrowser: WebBrowser // Pass the module here
|
|
1876
|
+
* },
|
|
1877
|
+
* storage: new AsyncStorageAdapter(AsyncStorage)
|
|
1878
|
+
* })
|
|
1879
|
+
*
|
|
1880
|
+
* // Now this works on ALL platforms - no platform checks needed!
|
|
1881
|
+
* const user = await blink.auth.signInWithGoogle()
|
|
1882
|
+
* ```
|
|
1883
|
+
*
|
|
1884
|
+
* @param provider - OAuth provider (google, github, apple, etc.)
|
|
1885
|
+
* @param options - Optional redirect URL and metadata
|
|
1886
|
+
* @returns Promise that resolves with authenticated user
|
|
1538
1887
|
*/
|
|
1539
1888
|
async signInWithProvider(provider, options) {
|
|
1540
1889
|
if (this.authConfig.mode !== "headless") {
|
|
1541
1890
|
throw new BlinkAuthError("INVALID_CREDENTIALS" /* INVALID_CREDENTIALS */, "signInWithProvider is only available in headless mode");
|
|
1542
1891
|
}
|
|
1892
|
+
if (this.authConfig.webBrowser) {
|
|
1893
|
+
return this.signInWithProviderUniversal(provider, options);
|
|
1894
|
+
}
|
|
1895
|
+
if (isReactNative2()) {
|
|
1896
|
+
throw new BlinkAuthError(
|
|
1897
|
+
"NETWORK_ERROR" /* NETWORK_ERROR */,
|
|
1898
|
+
'React Native OAuth requires webBrowser in config!\n\nimport * as WebBrowser from "expo-web-browser";\n\nconst blink = createClient({\n projectId: "your-project",\n auth: {\n mode: "headless",\n webBrowser: WebBrowser\n }\n})\n\nawait blink.auth.signInWithGoogle() // Works on all platforms!'
|
|
1899
|
+
);
|
|
1900
|
+
}
|
|
1901
|
+
if (!hasWindow()) {
|
|
1902
|
+
throw new BlinkAuthError("NETWORK_ERROR" /* NETWORK_ERROR */, "signInWithProvider requires a browser environment");
|
|
1903
|
+
}
|
|
1904
|
+
const shouldPreferRedirect = isWeb && this.isIframe || typeof window !== "undefined" && window.crossOriginIsolated === true;
|
|
1905
|
+
const state = this.generateState();
|
|
1906
|
+
try {
|
|
1907
|
+
const sessionStorage = getSessionStorage();
|
|
1908
|
+
if (sessionStorage) {
|
|
1909
|
+
sessionStorage.setItem("blink_oauth_state", state);
|
|
1910
|
+
}
|
|
1911
|
+
} catch {
|
|
1912
|
+
}
|
|
1913
|
+
const redirectUrl = options?.redirectUrl || getLocationOrigin() || "";
|
|
1914
|
+
const buildAuthUrl = (mode) => {
|
|
1915
|
+
const url = new URL("/auth", this.authUrl);
|
|
1916
|
+
url.searchParams.set("provider", provider);
|
|
1917
|
+
url.searchParams.set("project_id", this.config.projectId);
|
|
1918
|
+
url.searchParams.set("state", state);
|
|
1919
|
+
url.searchParams.set("mode", mode);
|
|
1920
|
+
url.searchParams.set("redirect_url", redirectUrl);
|
|
1921
|
+
url.searchParams.set("opener_origin", getLocationOrigin() || "");
|
|
1922
|
+
return url;
|
|
1923
|
+
};
|
|
1924
|
+
if (shouldPreferRedirect) {
|
|
1925
|
+
window.location.href = buildAuthUrl("redirect").toString();
|
|
1926
|
+
return new Promise(() => {
|
|
1927
|
+
});
|
|
1928
|
+
}
|
|
1543
1929
|
return new Promise((resolve, reject) => {
|
|
1544
|
-
const
|
|
1545
|
-
try {
|
|
1546
|
-
if (typeof window !== "undefined") {
|
|
1547
|
-
sessionStorage.setItem("blink_oauth_state", state);
|
|
1548
|
-
}
|
|
1549
|
-
} catch {
|
|
1550
|
-
}
|
|
1551
|
-
const redirectUrl = options?.redirectUrl || window.location.origin;
|
|
1552
|
-
const popupUrl = new URL("/auth", this.authUrl);
|
|
1553
|
-
popupUrl.searchParams.set("provider", provider);
|
|
1554
|
-
popupUrl.searchParams.set("project_id", this.config.projectId);
|
|
1555
|
-
popupUrl.searchParams.set("state", state);
|
|
1556
|
-
popupUrl.searchParams.set("mode", "popup");
|
|
1557
|
-
popupUrl.searchParams.set("redirect_url", redirectUrl);
|
|
1930
|
+
const popupUrl = buildAuthUrl("popup");
|
|
1558
1931
|
const popup = window.open(
|
|
1559
1932
|
popupUrl.toString(),
|
|
1560
1933
|
"blink-auth",
|
|
@@ -1565,6 +1938,15 @@ var BlinkAuth = class {
|
|
|
1565
1938
|
return;
|
|
1566
1939
|
}
|
|
1567
1940
|
let timeoutId;
|
|
1941
|
+
let closedIntervalId;
|
|
1942
|
+
let cleanedUp = false;
|
|
1943
|
+
const cleanup = () => {
|
|
1944
|
+
if (cleanedUp) return;
|
|
1945
|
+
cleanedUp = true;
|
|
1946
|
+
clearTimeout(timeoutId);
|
|
1947
|
+
if (closedIntervalId) clearInterval(closedIntervalId);
|
|
1948
|
+
window.removeEventListener("message", messageListener);
|
|
1949
|
+
};
|
|
1568
1950
|
const messageListener = (event) => {
|
|
1569
1951
|
let allowed = false;
|
|
1570
1952
|
try {
|
|
@@ -1577,7 +1959,8 @@ var BlinkAuth = class {
|
|
|
1577
1959
|
if (event.data?.type === "BLINK_AUTH_TOKENS") {
|
|
1578
1960
|
const { access_token, refresh_token, token_type, expires_in, refresh_expires_in, projectId, state: returnedState } = event.data;
|
|
1579
1961
|
try {
|
|
1580
|
-
const
|
|
1962
|
+
const sessionStorage = getSessionStorage();
|
|
1963
|
+
const expected = sessionStorage?.getItem("blink_oauth_state");
|
|
1581
1964
|
if (returnedState && expected && returnedState !== expected) {
|
|
1582
1965
|
reject(new BlinkAuthError("VERIFICATION_FAILED" /* VERIFICATION_FAILED */, "State mismatch"));
|
|
1583
1966
|
clearTimeout(timeoutId);
|
|
@@ -1600,29 +1983,34 @@ var BlinkAuth = class {
|
|
|
1600
1983
|
}, true).then(() => {
|
|
1601
1984
|
resolve(this.authState.user);
|
|
1602
1985
|
}).catch(reject);
|
|
1603
|
-
|
|
1604
|
-
window.removeEventListener("message", messageListener);
|
|
1986
|
+
cleanup();
|
|
1605
1987
|
popup.close();
|
|
1606
1988
|
} else if (event.data?.type === "BLINK_AUTH_ERROR") {
|
|
1607
1989
|
const errorCode = this.mapErrorCodeFromResponse(event.data.code);
|
|
1608
1990
|
reject(new BlinkAuthError(errorCode, event.data.message || "Authentication failed"));
|
|
1609
|
-
|
|
1610
|
-
window.removeEventListener("message", messageListener);
|
|
1991
|
+
cleanup();
|
|
1611
1992
|
popup.close();
|
|
1612
1993
|
}
|
|
1613
1994
|
};
|
|
1995
|
+
if (popup.opener === null) {
|
|
1996
|
+
try {
|
|
1997
|
+
popup.close();
|
|
1998
|
+
} catch {
|
|
1999
|
+
}
|
|
2000
|
+
cleanup();
|
|
2001
|
+
window.location.href = buildAuthUrl("redirect").toString();
|
|
2002
|
+
return;
|
|
2003
|
+
}
|
|
1614
2004
|
timeoutId = setTimeout(() => {
|
|
1615
|
-
|
|
2005
|
+
cleanup();
|
|
1616
2006
|
if (!popup.closed) {
|
|
1617
2007
|
popup.close();
|
|
1618
2008
|
}
|
|
1619
2009
|
reject(new BlinkAuthError("AUTH_TIMEOUT" /* AUTH_TIMEOUT */, "Authentication timed out"));
|
|
1620
2010
|
}, 3e5);
|
|
1621
|
-
|
|
2011
|
+
closedIntervalId = setInterval(() => {
|
|
1622
2012
|
if (popup.closed) {
|
|
1623
|
-
|
|
1624
|
-
clearTimeout(timeoutId);
|
|
1625
|
-
window.removeEventListener("message", messageListener);
|
|
2013
|
+
cleanup();
|
|
1626
2014
|
reject(new BlinkAuthError("POPUP_CANCELED" /* POPUP_CANCELED */, "Authentication was canceled"));
|
|
1627
2015
|
}
|
|
1628
2016
|
}, 1e3);
|
|
@@ -2101,6 +2489,48 @@ var BlinkAuth = class {
|
|
|
2101
2489
|
};
|
|
2102
2490
|
await this.setTokens(tokens, persist);
|
|
2103
2491
|
}
|
|
2492
|
+
/**
|
|
2493
|
+
* Manually set auth session from tokens (React Native deep link OAuth)
|
|
2494
|
+
*
|
|
2495
|
+
* Use this method to set the user session after receiving tokens from a deep link callback.
|
|
2496
|
+
* This is the React Native equivalent of automatic URL token detection on web.
|
|
2497
|
+
*
|
|
2498
|
+
* @param tokens - Auth tokens received from deep link or OAuth callback
|
|
2499
|
+
* @param persist - Whether to persist tokens to storage (default: true)
|
|
2500
|
+
*
|
|
2501
|
+
* @example
|
|
2502
|
+
* // React Native: Handle deep link OAuth callback
|
|
2503
|
+
* import * as Linking from 'expo-linking'
|
|
2504
|
+
*
|
|
2505
|
+
* Linking.addEventListener('url', async ({ url }) => {
|
|
2506
|
+
* const { queryParams } = Linking.parse(url)
|
|
2507
|
+
*
|
|
2508
|
+
* if (queryParams.access_token) {
|
|
2509
|
+
* await blink.auth.setSession({
|
|
2510
|
+
* access_token: queryParams.access_token,
|
|
2511
|
+
* refresh_token: queryParams.refresh_token,
|
|
2512
|
+
* expires_in: parseInt(queryParams.expires_in) || 3600,
|
|
2513
|
+
* refresh_expires_in: parseInt(queryParams.refresh_expires_in)
|
|
2514
|
+
* })
|
|
2515
|
+
*
|
|
2516
|
+
* console.log('User authenticated:', blink.auth.currentUser())
|
|
2517
|
+
* }
|
|
2518
|
+
* })
|
|
2519
|
+
*/
|
|
2520
|
+
async setSession(tokens, persist = true) {
|
|
2521
|
+
const authTokens = {
|
|
2522
|
+
access_token: tokens.access_token,
|
|
2523
|
+
refresh_token: tokens.refresh_token,
|
|
2524
|
+
token_type: "Bearer",
|
|
2525
|
+
expires_in: tokens.expires_in || 3600,
|
|
2526
|
+
// Default 1 hour
|
|
2527
|
+
refresh_expires_in: tokens.refresh_expires_in,
|
|
2528
|
+
issued_at: Math.floor(Date.now() / 1e3)
|
|
2529
|
+
};
|
|
2530
|
+
await this.setTokens(authTokens, persist);
|
|
2531
|
+
const user = await this.me();
|
|
2532
|
+
return user;
|
|
2533
|
+
}
|
|
2104
2534
|
/**
|
|
2105
2535
|
* Refresh access token using refresh token
|
|
2106
2536
|
*/
|
|
@@ -2334,12 +2764,13 @@ var BlinkAuth = class {
|
|
|
2334
2764
|
}
|
|
2335
2765
|
}
|
|
2336
2766
|
extractTokensFromUrl() {
|
|
2337
|
-
|
|
2338
|
-
|
|
2767
|
+
const search = getLocationSearch();
|
|
2768
|
+
if (!search) return null;
|
|
2769
|
+
const params = new URLSearchParams(search);
|
|
2339
2770
|
const accessToken = params.get("access_token");
|
|
2340
2771
|
const refreshToken = params.get("refresh_token");
|
|
2341
2772
|
console.log("\u{1F50D} Extracting tokens from URL:", {
|
|
2342
|
-
url:
|
|
2773
|
+
url: getLocationHref(),
|
|
2343
2774
|
accessToken: accessToken ? `${accessToken.substring(0, 20)}...` : null,
|
|
2344
2775
|
refreshToken: refreshToken ? `${refreshToken.substring(0, 20)}...` : null,
|
|
2345
2776
|
allParams: Object.fromEntries(params.entries())
|
|
@@ -2366,8 +2797,9 @@ var BlinkAuth = class {
|
|
|
2366
2797
|
return null;
|
|
2367
2798
|
}
|
|
2368
2799
|
clearUrlTokens() {
|
|
2369
|
-
|
|
2370
|
-
|
|
2800
|
+
const href = getLocationHref();
|
|
2801
|
+
if (!href || !hasWindowLocation()) return;
|
|
2802
|
+
const url = new URL(href);
|
|
2371
2803
|
url.searchParams.delete("access_token");
|
|
2372
2804
|
url.searchParams.delete("refresh_token");
|
|
2373
2805
|
url.searchParams.delete("token_type");
|
|
@@ -2382,7 +2814,7 @@ var BlinkAuth = class {
|
|
|
2382
2814
|
console.log("\u{1F9F9} URL cleaned up, removed auth parameters");
|
|
2383
2815
|
}
|
|
2384
2816
|
redirectToAuth() {
|
|
2385
|
-
if (
|
|
2817
|
+
if (hasWindowLocation()) {
|
|
2386
2818
|
this.login();
|
|
2387
2819
|
}
|
|
2388
2820
|
}
|
|
@@ -2414,12 +2846,25 @@ var BlinkAuth = class {
|
|
|
2414
2846
|
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
|
|
2415
2847
|
}
|
|
2416
2848
|
}
|
|
2849
|
+
/**
|
|
2850
|
+
* Generate unique session ID for mobile OAuth
|
|
2851
|
+
*/
|
|
2852
|
+
generateSessionId() {
|
|
2853
|
+
if (typeof crypto !== "undefined" && crypto.getRandomValues) {
|
|
2854
|
+
const array = new Uint8Array(32);
|
|
2855
|
+
crypto.getRandomValues(array);
|
|
2856
|
+
return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
2857
|
+
} else {
|
|
2858
|
+
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2417
2861
|
/**
|
|
2418
2862
|
* Extract magic link token from URL
|
|
2419
2863
|
*/
|
|
2420
2864
|
extractMagicTokenFromUrl() {
|
|
2421
|
-
|
|
2422
|
-
|
|
2865
|
+
const search = getLocationSearch();
|
|
2866
|
+
if (!search) return null;
|
|
2867
|
+
const params = new URLSearchParams(search);
|
|
2423
2868
|
return params.get("magic_token") || params.get("token");
|
|
2424
2869
|
}
|
|
2425
2870
|
/**
|
|
@@ -2475,7 +2920,7 @@ var BlinkAuth = class {
|
|
|
2475
2920
|
* Setup cross-tab authentication synchronization
|
|
2476
2921
|
*/
|
|
2477
2922
|
setupCrossTabSync() {
|
|
2478
|
-
if (!isWeb) return;
|
|
2923
|
+
if (!isWeb || !hasWindow()) return;
|
|
2479
2924
|
window.addEventListener("storage", (e) => {
|
|
2480
2925
|
if (e.key === this.getStorageKey("tokens")) {
|
|
2481
2926
|
const newTokens = e.newValue ? JSON.parse(e.newValue) : null;
|
|
@@ -3282,7 +3727,13 @@ var BlinkAIImpl = class {
|
|
|
3282
3727
|
options.prompt || "",
|
|
3283
3728
|
requestBody
|
|
3284
3729
|
);
|
|
3285
|
-
|
|
3730
|
+
if (response.data?.result) {
|
|
3731
|
+
return response.data.result;
|
|
3732
|
+
} else if (response.data?.text) {
|
|
3733
|
+
return response.data;
|
|
3734
|
+
} else {
|
|
3735
|
+
throw new BlinkAIError("Invalid response format: missing text");
|
|
3736
|
+
}
|
|
3286
3737
|
} catch (error) {
|
|
3287
3738
|
if (error instanceof BlinkAIError) {
|
|
3288
3739
|
throw error;
|
|
@@ -3351,14 +3802,9 @@ var BlinkAIImpl = class {
|
|
|
3351
3802
|
);
|
|
3352
3803
|
return {
|
|
3353
3804
|
text: result.text || "",
|
|
3354
|
-
finishReason:
|
|
3805
|
+
finishReason: "stop",
|
|
3355
3806
|
usage: result.usage,
|
|
3356
|
-
|
|
3357
|
-
toolResults: result.toolResults,
|
|
3358
|
-
sources: result.sources,
|
|
3359
|
-
files: result.files,
|
|
3360
|
-
reasoningDetails: result.reasoning,
|
|
3361
|
-
response: result.response
|
|
3807
|
+
...result
|
|
3362
3808
|
};
|
|
3363
3809
|
} catch (error) {
|
|
3364
3810
|
if (error instanceof BlinkAIError) {
|
|
@@ -3437,7 +3883,13 @@ var BlinkAIImpl = class {
|
|
|
3437
3883
|
signal: options.signal
|
|
3438
3884
|
}
|
|
3439
3885
|
);
|
|
3440
|
-
|
|
3886
|
+
if (response.data?.result) {
|
|
3887
|
+
return response.data.result;
|
|
3888
|
+
} else if (response.data?.object) {
|
|
3889
|
+
return response.data;
|
|
3890
|
+
} else {
|
|
3891
|
+
throw new BlinkAIError("Invalid response format: missing object");
|
|
3892
|
+
}
|
|
3441
3893
|
} catch (error) {
|
|
3442
3894
|
if (error instanceof BlinkAIError) {
|
|
3443
3895
|
throw error;
|
|
@@ -3499,7 +3951,8 @@ var BlinkAIImpl = class {
|
|
|
3499
3951
|
return {
|
|
3500
3952
|
object: result.object || {},
|
|
3501
3953
|
finishReason: "stop",
|
|
3502
|
-
usage: result.usage
|
|
3954
|
+
usage: result.usage,
|
|
3955
|
+
...result
|
|
3503
3956
|
};
|
|
3504
3957
|
} catch (error) {
|
|
3505
3958
|
if (error instanceof BlinkAIError) {
|
|
@@ -4771,9 +5224,9 @@ var BlinkAnalyticsImpl = class {
|
|
|
4771
5224
|
user_id: this.userId,
|
|
4772
5225
|
user_email: this.userEmail,
|
|
4773
5226
|
session_id: sessionId,
|
|
4774
|
-
pathname:
|
|
4775
|
-
referrer:
|
|
4776
|
-
screen_width:
|
|
5227
|
+
pathname: getLocationPathname(),
|
|
5228
|
+
referrer: getDocumentReferrer(),
|
|
5229
|
+
screen_width: getWindowInnerWidth(),
|
|
4777
5230
|
channel,
|
|
4778
5231
|
utm_source: this.utmParams.utm_source || this.persistedAttribution.utm_source || null,
|
|
4779
5232
|
utm_medium: this.utmParams.utm_medium || this.persistedAttribution.utm_medium || null,
|
|
@@ -4926,7 +5379,7 @@ var BlinkAnalyticsImpl = class {
|
|
|
4926
5379
|
window.__blinkAnalyticsInstances?.add(this);
|
|
4927
5380
|
}
|
|
4928
5381
|
setupUnloadListener() {
|
|
4929
|
-
if (!isWeb) return;
|
|
5382
|
+
if (!isWeb || !hasWindow()) return;
|
|
4930
5383
|
window.addEventListener("pagehide", () => {
|
|
4931
5384
|
this.flush();
|
|
4932
5385
|
});
|
|
@@ -4936,7 +5389,12 @@ var BlinkAnalyticsImpl = class {
|
|
|
4936
5389
|
}
|
|
4937
5390
|
captureUTMParams() {
|
|
4938
5391
|
if (!isWeb) return;
|
|
4939
|
-
const
|
|
5392
|
+
const search = getLocationSearch();
|
|
5393
|
+
if (!search) {
|
|
5394
|
+
this.utmParams = {};
|
|
5395
|
+
return;
|
|
5396
|
+
}
|
|
5397
|
+
const urlParams = new URLSearchParams(search);
|
|
4940
5398
|
this.utmParams = {
|
|
4941
5399
|
utm_source: urlParams.get("utm_source"),
|
|
4942
5400
|
utm_medium: urlParams.get("utm_medium"),
|
|
@@ -4973,7 +5431,7 @@ var BlinkAnalyticsImpl = class {
|
|
|
4973
5431
|
}
|
|
4974
5432
|
}
|
|
4975
5433
|
detectChannel() {
|
|
4976
|
-
const referrer =
|
|
5434
|
+
const referrer = getDocumentReferrer();
|
|
4977
5435
|
const utmMedium = this.utmParams.utm_medium;
|
|
4978
5436
|
this.utmParams.utm_source;
|
|
4979
5437
|
if (utmMedium) {
|