@lovable.dev/cloud-auth-js 0.0.3 → 0.0.4-dev.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/dist/index.cjs CHANGED
@@ -27,7 +27,10 @@ module.exports = __toCommonJS(src_exports);
27
27
  // src/auth.ts
28
28
  var EXPECTED_MESSAGE_TYPE = "authorization_response";
29
29
  var DEFAULT_OAUTH_BROKER_URL = "/~oauth/initiate";
30
- var DEFAULT_SUPPORTED_OAUTH_ORIGINS = ["https://oauth.lovable.app"];
30
+ var DEFAULT_SUPPORTED_OAUTH_ORIGINS = ["https://oauth.lovable.app", "https://lovable.dev"];
31
+ var DEFAULT_MOBILE_DEEP_LINK_REDIRECT_URI = "lovable://oauth-callback";
32
+ var POPUP_CHECK_INTERVAL_MS = 500;
33
+ var IFRAME_FALLBACK_TIMEOUT_MS = 12e4;
31
34
  function startWebMessageListener(supportedOrigins) {
32
35
  let resolvePromise;
33
36
  const promise = new Promise((resolve) => {
@@ -64,6 +67,26 @@ function getPopupDimensions(isInIframe) {
64
67
  const top = hasBrowserPosition ? window.screenY + (window.outerHeight - height) / 2 : (window.screen.height - height) / 2;
65
68
  return { width, height, left, top };
66
69
  }
70
+ function processOAuthResponse(data, expectedState) {
71
+ if (data.state !== expectedState) {
72
+ return { error: new Error("State is invalid") };
73
+ }
74
+ if (data.error) {
75
+ if (data.error === "legacy_flow") {
76
+ return {
77
+ error: new Error("This flow is not supported in Preview mode. Please open the app in a new tab to sign in.")
78
+ };
79
+ }
80
+ return { error: new Error(data.error_description ?? "Sign in failed") };
81
+ }
82
+ if (!data.access_token || !data.refresh_token) {
83
+ return { error: new Error("No tokens received") };
84
+ }
85
+ return {
86
+ tokens: { access_token: data.access_token, refresh_token: data.refresh_token },
87
+ error: null
88
+ };
89
+ }
67
90
  function generateState() {
68
91
  if (typeof crypto !== "undefined" && crypto.getRandomValues) {
69
92
  return [...crypto.getRandomValues(new Uint8Array(16))].map((b) => b.toString(16).padStart(2, "0")).join("");
@@ -80,22 +103,26 @@ function createAuth(config = {}) {
80
103
  } catch {
81
104
  isInIframe = true;
82
105
  }
106
+ const ua = navigator.userAgent;
107
+ const isMobile = /iPhone|iPad|iPod|Android/i.test(ua);
108
+ const isMobileApp = /LovableApp\//.test(ua);
83
109
  const state = generateState();
84
- const redirectUri = opts.redirect_uri ?? window.location.origin;
110
+ const redirectUri = isMobileApp && isInIframe ? DEFAULT_MOBILE_DEEP_LINK_REDIRECT_URI : opts.redirect_uri ?? window.location.origin;
85
111
  const params = new URLSearchParams({
112
+ ...opts.extraParams,
86
113
  provider,
87
114
  redirect_uri: redirectUri,
88
- state,
89
- ...opts.extraParams
115
+ state
90
116
  });
91
117
  if (!isInIframe) {
92
118
  window.location.href = `${oauthBrokerUrl}?${params.toString()}`;
93
119
  return { error: null, redirected: true };
94
120
  }
95
- params.set("response_mode", "web_message");
121
+ if (!isMobileApp) {
122
+ params.set("response_mode", "web_message");
123
+ }
96
124
  const url = `${oauthBrokerUrl}?${params.toString()}`;
97
125
  const { messagePromise, cleanup } = startWebMessageListener(supportedOAuthOrigins);
98
- const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
99
126
  let popup;
100
127
  if (isMobile) {
101
128
  popup = window.open(url, "_blank");
@@ -103,6 +130,24 @@ function createAuth(config = {}) {
103
130
  const { width, height, left, top } = getPopupDimensions(isInIframe);
104
131
  popup = window.open(url, "oauth", `width=${width},height=${height},left=${left},top=${top}`);
105
132
  }
133
+ if (!popup && isMobileApp) {
134
+ let webViewTimeoutId;
135
+ const webViewTimeoutPromise = new Promise((_, reject) => {
136
+ webViewTimeoutId = setTimeout(() => {
137
+ reject(new Error("OAuth timed out waiting for response"));
138
+ }, IFRAME_FALLBACK_TIMEOUT_MS);
139
+ });
140
+ try {
141
+ const data = await Promise.race([messagePromise, webViewTimeoutPromise]);
142
+ return processOAuthResponse(data, state);
143
+ } catch (error) {
144
+ return { error: error instanceof Error ? error : new Error(String(error)) };
145
+ } finally {
146
+ if (webViewTimeoutId)
147
+ clearTimeout(webViewTimeoutId);
148
+ cleanup();
149
+ }
150
+ }
106
151
  if (!popup) {
107
152
  cleanup();
108
153
  return { error: new Error("Popup was blocked") };
@@ -114,35 +159,11 @@ function createAuth(config = {}) {
114
159
  clearInterval(popupCheckInterval);
115
160
  reject(new Error("Sign in was cancelled"));
116
161
  }
117
- }, 500);
162
+ }, POPUP_CHECK_INTERVAL_MS);
118
163
  });
119
164
  try {
120
165
  const data = await Promise.race([messagePromise, popupClosedPromise]);
121
- if (data.error) {
122
- if (data.error === "legacy_flow") {
123
- return {
124
- error: new Error(
125
- "This flow is not supported in Preview mode. Please open the app in a new tab to sign in."
126
- )
127
- };
128
- }
129
- return {
130
- error: new Error(data.error_description ?? "Sign in failed")
131
- };
132
- }
133
- if (data.state !== state) {
134
- return { error: new Error("State is invalid") };
135
- }
136
- if (!data.access_token || !data.refresh_token) {
137
- return { error: new Error("No tokens received") };
138
- }
139
- return {
140
- tokens: {
141
- access_token: data.access_token,
142
- refresh_token: data.refresh_token
143
- },
144
- error: null
145
- };
166
+ return processOAuthResponse(data, state);
146
167
  } catch (error) {
147
168
  return {
148
169
  error: error instanceof Error ? error : new Error(String(error))
package/dist/index.d.cts CHANGED
@@ -30,4 +30,4 @@ interface LovableAuth {
30
30
 
31
31
  declare function createLovableAuth(config?: LovableAuthConfig): LovableAuth;
32
32
 
33
- export { type LovableAuth, type LovableAuthConfig, type OAuthProvider, type OAuthTokens, type SignInWithOAuthOptions, type SignInWithOAuthResult, createLovableAuth };
33
+ export { LovableAuth, LovableAuthConfig, OAuthProvider, OAuthTokens, SignInWithOAuthOptions, SignInWithOAuthResult, createLovableAuth };
package/dist/index.d.ts CHANGED
@@ -30,4 +30,4 @@ interface LovableAuth {
30
30
 
31
31
  declare function createLovableAuth(config?: LovableAuthConfig): LovableAuth;
32
32
 
33
- export { type LovableAuth, type LovableAuthConfig, type OAuthProvider, type OAuthTokens, type SignInWithOAuthOptions, type SignInWithOAuthResult, createLovableAuth };
33
+ export { LovableAuth, LovableAuthConfig, OAuthProvider, OAuthTokens, SignInWithOAuthOptions, SignInWithOAuthResult, createLovableAuth };
package/dist/index.js CHANGED
@@ -1,7 +1,10 @@
1
1
  // src/auth.ts
2
2
  var EXPECTED_MESSAGE_TYPE = "authorization_response";
3
3
  var DEFAULT_OAUTH_BROKER_URL = "/~oauth/initiate";
4
- var DEFAULT_SUPPORTED_OAUTH_ORIGINS = ["https://oauth.lovable.app"];
4
+ var DEFAULT_SUPPORTED_OAUTH_ORIGINS = ["https://oauth.lovable.app", "https://lovable.dev"];
5
+ var DEFAULT_MOBILE_DEEP_LINK_REDIRECT_URI = "lovable://oauth-callback";
6
+ var POPUP_CHECK_INTERVAL_MS = 500;
7
+ var IFRAME_FALLBACK_TIMEOUT_MS = 12e4;
5
8
  function startWebMessageListener(supportedOrigins) {
6
9
  let resolvePromise;
7
10
  const promise = new Promise((resolve) => {
@@ -38,6 +41,26 @@ function getPopupDimensions(isInIframe) {
38
41
  const top = hasBrowserPosition ? window.screenY + (window.outerHeight - height) / 2 : (window.screen.height - height) / 2;
39
42
  return { width, height, left, top };
40
43
  }
44
+ function processOAuthResponse(data, expectedState) {
45
+ if (data.state !== expectedState) {
46
+ return { error: new Error("State is invalid") };
47
+ }
48
+ if (data.error) {
49
+ if (data.error === "legacy_flow") {
50
+ return {
51
+ error: new Error("This flow is not supported in Preview mode. Please open the app in a new tab to sign in.")
52
+ };
53
+ }
54
+ return { error: new Error(data.error_description ?? "Sign in failed") };
55
+ }
56
+ if (!data.access_token || !data.refresh_token) {
57
+ return { error: new Error("No tokens received") };
58
+ }
59
+ return {
60
+ tokens: { access_token: data.access_token, refresh_token: data.refresh_token },
61
+ error: null
62
+ };
63
+ }
41
64
  function generateState() {
42
65
  if (typeof crypto !== "undefined" && crypto.getRandomValues) {
43
66
  return [...crypto.getRandomValues(new Uint8Array(16))].map((b) => b.toString(16).padStart(2, "0")).join("");
@@ -54,22 +77,26 @@ function createAuth(config = {}) {
54
77
  } catch {
55
78
  isInIframe = true;
56
79
  }
80
+ const ua = navigator.userAgent;
81
+ const isMobile = /iPhone|iPad|iPod|Android/i.test(ua);
82
+ const isMobileApp = /LovableApp\//.test(ua);
57
83
  const state = generateState();
58
- const redirectUri = opts.redirect_uri ?? window.location.origin;
84
+ const redirectUri = isMobileApp && isInIframe ? DEFAULT_MOBILE_DEEP_LINK_REDIRECT_URI : opts.redirect_uri ?? window.location.origin;
59
85
  const params = new URLSearchParams({
86
+ ...opts.extraParams,
60
87
  provider,
61
88
  redirect_uri: redirectUri,
62
- state,
63
- ...opts.extraParams
89
+ state
64
90
  });
65
91
  if (!isInIframe) {
66
92
  window.location.href = `${oauthBrokerUrl}?${params.toString()}`;
67
93
  return { error: null, redirected: true };
68
94
  }
69
- params.set("response_mode", "web_message");
95
+ if (!isMobileApp) {
96
+ params.set("response_mode", "web_message");
97
+ }
70
98
  const url = `${oauthBrokerUrl}?${params.toString()}`;
71
99
  const { messagePromise, cleanup } = startWebMessageListener(supportedOAuthOrigins);
72
- const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
73
100
  let popup;
74
101
  if (isMobile) {
75
102
  popup = window.open(url, "_blank");
@@ -77,6 +104,24 @@ function createAuth(config = {}) {
77
104
  const { width, height, left, top } = getPopupDimensions(isInIframe);
78
105
  popup = window.open(url, "oauth", `width=${width},height=${height},left=${left},top=${top}`);
79
106
  }
107
+ if (!popup && isMobileApp) {
108
+ let webViewTimeoutId;
109
+ const webViewTimeoutPromise = new Promise((_, reject) => {
110
+ webViewTimeoutId = setTimeout(() => {
111
+ reject(new Error("OAuth timed out waiting for response"));
112
+ }, IFRAME_FALLBACK_TIMEOUT_MS);
113
+ });
114
+ try {
115
+ const data = await Promise.race([messagePromise, webViewTimeoutPromise]);
116
+ return processOAuthResponse(data, state);
117
+ } catch (error) {
118
+ return { error: error instanceof Error ? error : new Error(String(error)) };
119
+ } finally {
120
+ if (webViewTimeoutId)
121
+ clearTimeout(webViewTimeoutId);
122
+ cleanup();
123
+ }
124
+ }
80
125
  if (!popup) {
81
126
  cleanup();
82
127
  return { error: new Error("Popup was blocked") };
@@ -88,35 +133,11 @@ function createAuth(config = {}) {
88
133
  clearInterval(popupCheckInterval);
89
134
  reject(new Error("Sign in was cancelled"));
90
135
  }
91
- }, 500);
136
+ }, POPUP_CHECK_INTERVAL_MS);
92
137
  });
93
138
  try {
94
139
  const data = await Promise.race([messagePromise, popupClosedPromise]);
95
- if (data.error) {
96
- if (data.error === "legacy_flow") {
97
- return {
98
- error: new Error(
99
- "This flow is not supported in Preview mode. Please open the app in a new tab to sign in."
100
- )
101
- };
102
- }
103
- return {
104
- error: new Error(data.error_description ?? "Sign in failed")
105
- };
106
- }
107
- if (data.state !== state) {
108
- return { error: new Error("State is invalid") };
109
- }
110
- if (!data.access_token || !data.refresh_token) {
111
- return { error: new Error("No tokens received") };
112
- }
113
- return {
114
- tokens: {
115
- access_token: data.access_token,
116
- refresh_token: data.refresh_token
117
- },
118
- error: null
119
- };
140
+ return processOAuthResponse(data, state);
120
141
  } catch (error) {
121
142
  return {
122
143
  error: error instanceof Error ? error : new Error(String(error))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lovable.dev/cloud-auth-js",
3
- "version": "0.0.3",
3
+ "version": "0.0.4-dev.1",
4
4
  "description": "Lovable Cloud Auth JS",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",
@@ -13,7 +13,7 @@
13
13
  "build": "tsup src/index.ts --format cjs,esm --dts --outDir dist",
14
14
  "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
15
15
  "test": "vitest run",
16
- "typecheck": "tsc --noEmit",
16
+ "typecheck": "tsgo --noEmit",
17
17
  "prepublishOnly": "npm run clean && npm run build",
18
18
  "clean": "rimraf dist"
19
19
  },
@@ -25,6 +25,7 @@
25
25
  "license": "MIT",
26
26
  "devDependencies": {
27
27
  "@types/node": "^22",
28
+ "jsdom": "^26",
28
29
  "rimraf": "^5.0.0",
29
30
  "tsup": "^7.2.0",
30
31
  "typescript": "^5",