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