@lanonasis/oauth-client 1.2.8 → 2.0.3
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 +183 -15
- package/dist/{api-key-storage-web-J3W8nQi2.d.cts → api-key-storage-web-DUyiN9mC.d.cts} +11 -5
- package/dist/{api-key-storage-web-J3W8nQi2.d.ts → api-key-storage-web-DUyiN9mC.d.ts} +11 -5
- package/dist/browser.d.cts +2 -2
- package/dist/browser.d.ts +2 -2
- package/dist/constants-BZPTHasL.d.cts +110 -0
- package/dist/constants-BZPTHasL.d.ts +110 -0
- package/dist/index.cjs +79 -40
- package/dist/index.d.cts +15 -3
- package/dist/index.d.ts +15 -3
- package/dist/index.mjs +79 -40
- package/dist/react/index.cjs +261 -0
- package/dist/react/index.d.cts +95 -0
- package/dist/react/index.d.ts +95 -0
- package/dist/react/index.mjs +238 -0
- package/dist/server/index.cjs +169 -0
- package/dist/server/index.d.cts +184 -0
- package/dist/server/index.d.ts +184 -0
- package/dist/server/index.mjs +146 -0
- package/package.json +57 -15
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/react/index.ts
|
|
21
|
+
var react_exports = {};
|
|
22
|
+
__export(react_exports, {
|
|
23
|
+
COOKIE_NAMES: () => COOKIE_NAMES,
|
|
24
|
+
DEFAULT_AUTH_GATEWAY: () => DEFAULT_AUTH_GATEWAY,
|
|
25
|
+
DEFAULT_COOKIE_DOMAIN: () => DEFAULT_COOKIE_DOMAIN,
|
|
26
|
+
DEFAULT_POLL_INTERVAL: () => DEFAULT_POLL_INTERVAL,
|
|
27
|
+
DEFAULT_PROJECT_SCOPE: () => DEFAULT_PROJECT_SCOPE,
|
|
28
|
+
clearUserCookie: () => clearUserCookie,
|
|
29
|
+
hasAuthCookies: () => hasAuthCookies,
|
|
30
|
+
hasSessionCookie: () => hasSessionCookie,
|
|
31
|
+
isBrowser: () => isBrowser,
|
|
32
|
+
parseUserCookie: () => parseUserCookie,
|
|
33
|
+
useSSO: () => useSSO,
|
|
34
|
+
useSSOSync: () => useSSOSync
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(react_exports);
|
|
37
|
+
|
|
38
|
+
// src/react/useSSO.ts
|
|
39
|
+
var import_react = require("react");
|
|
40
|
+
|
|
41
|
+
// src/cookies/constants.ts
|
|
42
|
+
var COOKIE_NAMES = {
|
|
43
|
+
/** HttpOnly JWT session token */
|
|
44
|
+
SESSION: "lanonasis_session",
|
|
45
|
+
/** Readable user metadata (JSON) */
|
|
46
|
+
USER: "lanonasis_user"
|
|
47
|
+
};
|
|
48
|
+
var DEFAULT_COOKIE_DOMAIN = ".lanonasis.com";
|
|
49
|
+
var DEFAULT_AUTH_GATEWAY = "https://auth.lanonasis.com";
|
|
50
|
+
var DEFAULT_POLL_INTERVAL = 3e4;
|
|
51
|
+
var DEFAULT_PROJECT_SCOPE = "lanonasis-maas";
|
|
52
|
+
|
|
53
|
+
// src/react/cookie-utils-browser.ts
|
|
54
|
+
function isBrowser() {
|
|
55
|
+
return typeof window !== "undefined" && typeof document !== "undefined";
|
|
56
|
+
}
|
|
57
|
+
function parseUserCookie() {
|
|
58
|
+
if (!isBrowser()) return null;
|
|
59
|
+
try {
|
|
60
|
+
const cookies = document.cookie.split(";");
|
|
61
|
+
const userCookie = cookies.find(
|
|
62
|
+
(c) => c.trim().startsWith(`${COOKIE_NAMES.USER}=`)
|
|
63
|
+
);
|
|
64
|
+
if (!userCookie) return null;
|
|
65
|
+
const value = userCookie.split("=").slice(1).join("=").trim();
|
|
66
|
+
const decoded = decodeURIComponent(value);
|
|
67
|
+
const parsed = JSON.parse(decoded);
|
|
68
|
+
if (!parsed.id || !parsed.email || !parsed.role) {
|
|
69
|
+
console.warn("[oauth-client] Invalid user cookie: missing required fields");
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
return parsed;
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.warn("[oauth-client] Failed to parse user cookie:", error);
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function hasSessionCookie() {
|
|
79
|
+
if (!isBrowser()) return false;
|
|
80
|
+
return document.cookie.includes(`${COOKIE_NAMES.SESSION}=`);
|
|
81
|
+
}
|
|
82
|
+
function hasAuthCookies() {
|
|
83
|
+
return hasSessionCookie() && parseUserCookie() !== null;
|
|
84
|
+
}
|
|
85
|
+
function clearUserCookie(domain = ".lanonasis.com") {
|
|
86
|
+
if (!isBrowser()) return;
|
|
87
|
+
document.cookie = `${COOKIE_NAMES.USER}=; domain=${domain}; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/react/useSSO.ts
|
|
91
|
+
function useSSO(config = {}) {
|
|
92
|
+
const {
|
|
93
|
+
authGatewayUrl = DEFAULT_AUTH_GATEWAY,
|
|
94
|
+
onAuthChange,
|
|
95
|
+
pollInterval = DEFAULT_POLL_INTERVAL
|
|
96
|
+
} = config;
|
|
97
|
+
const [state, setState] = (0, import_react.useState)({
|
|
98
|
+
isAuthenticated: false,
|
|
99
|
+
isLoading: true,
|
|
100
|
+
user: null,
|
|
101
|
+
error: null
|
|
102
|
+
});
|
|
103
|
+
const prevStateRef = (0, import_react.useRef)(null);
|
|
104
|
+
const checkAuthState = (0, import_react.useCallback)(() => {
|
|
105
|
+
if (!isBrowser()) {
|
|
106
|
+
setState({
|
|
107
|
+
isAuthenticated: false,
|
|
108
|
+
isLoading: false,
|
|
109
|
+
user: null,
|
|
110
|
+
error: null
|
|
111
|
+
});
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
const hasSession = hasSessionCookie();
|
|
116
|
+
const user = parseUserCookie();
|
|
117
|
+
const newState = {
|
|
118
|
+
isAuthenticated: hasSession && user !== null,
|
|
119
|
+
isLoading: false,
|
|
120
|
+
user: hasSession ? user : null,
|
|
121
|
+
error: null
|
|
122
|
+
};
|
|
123
|
+
setState(newState);
|
|
124
|
+
if (onAuthChange && prevStateRef.current && prevStateRef.current.isAuthenticated !== newState.isAuthenticated) {
|
|
125
|
+
onAuthChange(newState);
|
|
126
|
+
}
|
|
127
|
+
prevStateRef.current = newState;
|
|
128
|
+
} catch (error) {
|
|
129
|
+
const errorState = {
|
|
130
|
+
isAuthenticated: false,
|
|
131
|
+
isLoading: false,
|
|
132
|
+
user: null,
|
|
133
|
+
error: error instanceof Error ? error.message : "Failed to check auth state"
|
|
134
|
+
};
|
|
135
|
+
setState(errorState);
|
|
136
|
+
prevStateRef.current = errorState;
|
|
137
|
+
}
|
|
138
|
+
}, [onAuthChange]);
|
|
139
|
+
const refresh = (0, import_react.useCallback)(() => {
|
|
140
|
+
setState((prev) => ({ ...prev, isLoading: true }));
|
|
141
|
+
checkAuthState();
|
|
142
|
+
}, [checkAuthState]);
|
|
143
|
+
const logout = (0, import_react.useCallback)((returnTo) => {
|
|
144
|
+
if (!isBrowser()) return;
|
|
145
|
+
const logoutUrl = new URL("/web/logout", authGatewayUrl);
|
|
146
|
+
if (returnTo) {
|
|
147
|
+
logoutUrl.searchParams.set("return_to", returnTo);
|
|
148
|
+
}
|
|
149
|
+
window.location.href = logoutUrl.toString();
|
|
150
|
+
}, [authGatewayUrl]);
|
|
151
|
+
const getLoginUrl = (0, import_react.useCallback)((returnTo) => {
|
|
152
|
+
const loginUrl = new URL("/web/login", authGatewayUrl);
|
|
153
|
+
if (returnTo) {
|
|
154
|
+
loginUrl.searchParams.set("return_to", returnTo);
|
|
155
|
+
}
|
|
156
|
+
return loginUrl.toString();
|
|
157
|
+
}, [authGatewayUrl]);
|
|
158
|
+
(0, import_react.useEffect)(() => {
|
|
159
|
+
const timer = setTimeout(checkAuthState, 0);
|
|
160
|
+
return () => clearTimeout(timer);
|
|
161
|
+
}, [checkAuthState]);
|
|
162
|
+
(0, import_react.useEffect)(() => {
|
|
163
|
+
if (!isBrowser() || pollInterval <= 0) return;
|
|
164
|
+
const interval = setInterval(checkAuthState, pollInterval);
|
|
165
|
+
return () => clearInterval(interval);
|
|
166
|
+
}, [checkAuthState, pollInterval]);
|
|
167
|
+
(0, import_react.useEffect)(() => {
|
|
168
|
+
if (!isBrowser()) return;
|
|
169
|
+
const handleStorageChange = (event) => {
|
|
170
|
+
if (event.key === null || event.key?.includes("auth")) {
|
|
171
|
+
checkAuthState();
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
window.addEventListener("storage", handleStorageChange);
|
|
175
|
+
return () => window.removeEventListener("storage", handleStorageChange);
|
|
176
|
+
}, [checkAuthState]);
|
|
177
|
+
(0, import_react.useEffect)(() => {
|
|
178
|
+
if (!isBrowser()) return;
|
|
179
|
+
const handleFocus = () => {
|
|
180
|
+
checkAuthState();
|
|
181
|
+
};
|
|
182
|
+
window.addEventListener("focus", handleFocus);
|
|
183
|
+
return () => window.removeEventListener("focus", handleFocus);
|
|
184
|
+
}, [checkAuthState]);
|
|
185
|
+
return {
|
|
186
|
+
...state,
|
|
187
|
+
refresh,
|
|
188
|
+
logout,
|
|
189
|
+
getLoginUrl
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// src/react/useSSOSync.ts
|
|
194
|
+
var import_react2 = require("react");
|
|
195
|
+
function useSSOSync(supabaseSession, config = {}) {
|
|
196
|
+
const {
|
|
197
|
+
authGatewayUrl = DEFAULT_AUTH_GATEWAY,
|
|
198
|
+
projectScope = DEFAULT_PROJECT_SCOPE,
|
|
199
|
+
onSyncComplete
|
|
200
|
+
} = config;
|
|
201
|
+
const lastSyncedTokenRef = (0, import_react2.useRef)(null);
|
|
202
|
+
const syncWithGateway = (0, import_react2.useCallback)(
|
|
203
|
+
async (session) => {
|
|
204
|
+
if (!isBrowser()) return false;
|
|
205
|
+
try {
|
|
206
|
+
const response = await fetch(
|
|
207
|
+
`${authGatewayUrl}/v1/auth/token/exchange`,
|
|
208
|
+
{
|
|
209
|
+
method: "POST",
|
|
210
|
+
headers: {
|
|
211
|
+
Authorization: `Bearer ${session.access_token}`,
|
|
212
|
+
"Content-Type": "application/json",
|
|
213
|
+
"X-Project-Scope": projectScope
|
|
214
|
+
},
|
|
215
|
+
credentials: "include",
|
|
216
|
+
// Important: include cookies
|
|
217
|
+
body: JSON.stringify({
|
|
218
|
+
project_scope: projectScope,
|
|
219
|
+
platform: "web"
|
|
220
|
+
})
|
|
221
|
+
}
|
|
222
|
+
);
|
|
223
|
+
if (!response.ok) {
|
|
224
|
+
console.warn("[oauth-client] SSO sync failed:", response.status);
|
|
225
|
+
onSyncComplete?.(false);
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
const data = await response.json();
|
|
229
|
+
if (data.cookies_set) {
|
|
230
|
+
console.log("[oauth-client] SSO cookies set successfully");
|
|
231
|
+
}
|
|
232
|
+
onSyncComplete?.(true);
|
|
233
|
+
return true;
|
|
234
|
+
} catch (error) {
|
|
235
|
+
console.error("[oauth-client] SSO sync error:", error);
|
|
236
|
+
onSyncComplete?.(false);
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
[authGatewayUrl, projectScope, onSyncComplete]
|
|
241
|
+
);
|
|
242
|
+
const sync = (0, import_react2.useCallback)(async () => {
|
|
243
|
+
if (!supabaseSession?.access_token) {
|
|
244
|
+
console.warn("[oauth-client] Cannot sync: no Supabase session");
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
return syncWithGateway(supabaseSession);
|
|
248
|
+
}, [supabaseSession, syncWithGateway]);
|
|
249
|
+
(0, import_react2.useEffect)(() => {
|
|
250
|
+
const token = supabaseSession?.access_token;
|
|
251
|
+
if (!token || token === lastSyncedTokenRef.current) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
lastSyncedTokenRef.current = token;
|
|
255
|
+
syncWithGateway(supabaseSession);
|
|
256
|
+
}, [supabaseSession?.access_token, syncWithGateway, supabaseSession]);
|
|
257
|
+
return {
|
|
258
|
+
sync,
|
|
259
|
+
syncWithGateway
|
|
260
|
+
};
|
|
261
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { S as SSOConfig, U as UseSSOReturn, a as SupabaseSession, b as SSOSyncConfig, c as UseSSOSyncReturn, d as SSOUser } from '../constants-BZPTHasL.cjs';
|
|
2
|
+
export { C as COOKIE_NAMES, D as DEFAULT_AUTH_GATEWAY, e as DEFAULT_COOKIE_DOMAIN, f as DEFAULT_POLL_INTERVAL, g as DEFAULT_PROJECT_SCOPE, h as SSOState } from '../constants-BZPTHasL.cjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* React hook for SSO authentication state
|
|
6
|
+
* @module @lanonasis/oauth-client/react
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* React hook for SSO authentication state
|
|
11
|
+
*
|
|
12
|
+
* Reads the lanonasis_user cookie to determine auth state.
|
|
13
|
+
* Works across all *.lanonasis.com subdomains.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```tsx
|
|
17
|
+
* function Navbar() {
|
|
18
|
+
* const { isAuthenticated, user, logout } = useSSO();
|
|
19
|
+
*
|
|
20
|
+
* if (isAuthenticated && user) {
|
|
21
|
+
* return <span>Welcome, {user.email}</span>;
|
|
22
|
+
* }
|
|
23
|
+
* return <a href="https://auth.lanonasis.com/web/login">Sign In</a>;
|
|
24
|
+
* }
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
declare function useSSO(config?: SSOConfig): UseSSOReturn;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* React hook to sync Supabase auth state with lanonasis cookies
|
|
31
|
+
* @module @lanonasis/oauth-client/react
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* React hook to sync Supabase auth state with lanonasis cookies
|
|
36
|
+
*
|
|
37
|
+
* Call this hook in components that use Supabase auth to ensure
|
|
38
|
+
* cross-subdomain SSO cookies are set when user logs in.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```tsx
|
|
42
|
+
* function AuthProvider({ children }) {
|
|
43
|
+
* const { data: { session } } = useSupabaseSession();
|
|
44
|
+
*
|
|
45
|
+
* // Sync Supabase session with SSO cookies
|
|
46
|
+
* useSSOSync(session, {
|
|
47
|
+
* projectScope: 'my-app',
|
|
48
|
+
* onSyncComplete: (success) => {
|
|
49
|
+
* console.log('SSO sync:', success ? 'success' : 'failed');
|
|
50
|
+
* }
|
|
51
|
+
* });
|
|
52
|
+
*
|
|
53
|
+
* return <>{children}</>;
|
|
54
|
+
* }
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
declare function useSSOSync(supabaseSession: SupabaseSession | null, config?: SSOSyncConfig): UseSSOSyncReturn;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Browser-side cookie utilities for SSO authentication
|
|
61
|
+
* @module @lanonasis/oauth-client/react
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Check if we're in a browser environment
|
|
66
|
+
*/
|
|
67
|
+
declare function isBrowser(): boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Parse the lanonasis_user cookie (non-HttpOnly, readable by JS)
|
|
70
|
+
* This cookie contains: { id, email, role, name?, avatar_url? }
|
|
71
|
+
*
|
|
72
|
+
* @returns User data or null if cookie doesn't exist or is invalid
|
|
73
|
+
*/
|
|
74
|
+
declare function parseUserCookie(): SSOUser | null;
|
|
75
|
+
/**
|
|
76
|
+
* Check if the session cookie exists (cannot read value due to HttpOnly)
|
|
77
|
+
*
|
|
78
|
+
* @returns true if lanonasis_session cookie exists
|
|
79
|
+
*/
|
|
80
|
+
declare function hasSessionCookie(): boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Check if user appears to be authenticated based on cookies
|
|
83
|
+
*
|
|
84
|
+
* @returns true if both session and user cookies exist
|
|
85
|
+
*/
|
|
86
|
+
declare function hasAuthCookies(): boolean;
|
|
87
|
+
/**
|
|
88
|
+
* Clear auth cookies (client-side only clears non-HttpOnly cookies)
|
|
89
|
+
* Full logout should redirect to auth gateway
|
|
90
|
+
*
|
|
91
|
+
* @param domain - Cookie domain (default: .lanonasis.com)
|
|
92
|
+
*/
|
|
93
|
+
declare function clearUserCookie(domain?: string): void;
|
|
94
|
+
|
|
95
|
+
export { SSOConfig, SSOSyncConfig, SSOUser, SupabaseSession, UseSSOReturn, UseSSOSyncReturn, clearUserCookie, hasAuthCookies, hasSessionCookie, isBrowser, parseUserCookie, useSSO, useSSOSync };
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { S as SSOConfig, U as UseSSOReturn, a as SupabaseSession, b as SSOSyncConfig, c as UseSSOSyncReturn, d as SSOUser } from '../constants-BZPTHasL.js';
|
|
2
|
+
export { C as COOKIE_NAMES, D as DEFAULT_AUTH_GATEWAY, e as DEFAULT_COOKIE_DOMAIN, f as DEFAULT_POLL_INTERVAL, g as DEFAULT_PROJECT_SCOPE, h as SSOState } from '../constants-BZPTHasL.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* React hook for SSO authentication state
|
|
6
|
+
* @module @lanonasis/oauth-client/react
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* React hook for SSO authentication state
|
|
11
|
+
*
|
|
12
|
+
* Reads the lanonasis_user cookie to determine auth state.
|
|
13
|
+
* Works across all *.lanonasis.com subdomains.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```tsx
|
|
17
|
+
* function Navbar() {
|
|
18
|
+
* const { isAuthenticated, user, logout } = useSSO();
|
|
19
|
+
*
|
|
20
|
+
* if (isAuthenticated && user) {
|
|
21
|
+
* return <span>Welcome, {user.email}</span>;
|
|
22
|
+
* }
|
|
23
|
+
* return <a href="https://auth.lanonasis.com/web/login">Sign In</a>;
|
|
24
|
+
* }
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
declare function useSSO(config?: SSOConfig): UseSSOReturn;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* React hook to sync Supabase auth state with lanonasis cookies
|
|
31
|
+
* @module @lanonasis/oauth-client/react
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* React hook to sync Supabase auth state with lanonasis cookies
|
|
36
|
+
*
|
|
37
|
+
* Call this hook in components that use Supabase auth to ensure
|
|
38
|
+
* cross-subdomain SSO cookies are set when user logs in.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```tsx
|
|
42
|
+
* function AuthProvider({ children }) {
|
|
43
|
+
* const { data: { session } } = useSupabaseSession();
|
|
44
|
+
*
|
|
45
|
+
* // Sync Supabase session with SSO cookies
|
|
46
|
+
* useSSOSync(session, {
|
|
47
|
+
* projectScope: 'my-app',
|
|
48
|
+
* onSyncComplete: (success) => {
|
|
49
|
+
* console.log('SSO sync:', success ? 'success' : 'failed');
|
|
50
|
+
* }
|
|
51
|
+
* });
|
|
52
|
+
*
|
|
53
|
+
* return <>{children}</>;
|
|
54
|
+
* }
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
declare function useSSOSync(supabaseSession: SupabaseSession | null, config?: SSOSyncConfig): UseSSOSyncReturn;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Browser-side cookie utilities for SSO authentication
|
|
61
|
+
* @module @lanonasis/oauth-client/react
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Check if we're in a browser environment
|
|
66
|
+
*/
|
|
67
|
+
declare function isBrowser(): boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Parse the lanonasis_user cookie (non-HttpOnly, readable by JS)
|
|
70
|
+
* This cookie contains: { id, email, role, name?, avatar_url? }
|
|
71
|
+
*
|
|
72
|
+
* @returns User data or null if cookie doesn't exist or is invalid
|
|
73
|
+
*/
|
|
74
|
+
declare function parseUserCookie(): SSOUser | null;
|
|
75
|
+
/**
|
|
76
|
+
* Check if the session cookie exists (cannot read value due to HttpOnly)
|
|
77
|
+
*
|
|
78
|
+
* @returns true if lanonasis_session cookie exists
|
|
79
|
+
*/
|
|
80
|
+
declare function hasSessionCookie(): boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Check if user appears to be authenticated based on cookies
|
|
83
|
+
*
|
|
84
|
+
* @returns true if both session and user cookies exist
|
|
85
|
+
*/
|
|
86
|
+
declare function hasAuthCookies(): boolean;
|
|
87
|
+
/**
|
|
88
|
+
* Clear auth cookies (client-side only clears non-HttpOnly cookies)
|
|
89
|
+
* Full logout should redirect to auth gateway
|
|
90
|
+
*
|
|
91
|
+
* @param domain - Cookie domain (default: .lanonasis.com)
|
|
92
|
+
*/
|
|
93
|
+
declare function clearUserCookie(domain?: string): void;
|
|
94
|
+
|
|
95
|
+
export { SSOConfig, SSOSyncConfig, SSOUser, SupabaseSession, UseSSOReturn, UseSSOSyncReturn, clearUserCookie, hasAuthCookies, hasSessionCookie, isBrowser, parseUserCookie, useSSO, useSSOSync };
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
// src/react/useSSO.ts
|
|
2
|
+
import { useState, useEffect, useCallback, useRef } from "react";
|
|
3
|
+
|
|
4
|
+
// src/cookies/constants.ts
|
|
5
|
+
var COOKIE_NAMES = {
|
|
6
|
+
/** HttpOnly JWT session token */
|
|
7
|
+
SESSION: "lanonasis_session",
|
|
8
|
+
/** Readable user metadata (JSON) */
|
|
9
|
+
USER: "lanonasis_user"
|
|
10
|
+
};
|
|
11
|
+
var DEFAULT_COOKIE_DOMAIN = ".lanonasis.com";
|
|
12
|
+
var DEFAULT_AUTH_GATEWAY = "https://auth.lanonasis.com";
|
|
13
|
+
var DEFAULT_POLL_INTERVAL = 3e4;
|
|
14
|
+
var DEFAULT_PROJECT_SCOPE = "lanonasis-maas";
|
|
15
|
+
|
|
16
|
+
// src/react/cookie-utils-browser.ts
|
|
17
|
+
function isBrowser() {
|
|
18
|
+
return typeof window !== "undefined" && typeof document !== "undefined";
|
|
19
|
+
}
|
|
20
|
+
function parseUserCookie() {
|
|
21
|
+
if (!isBrowser()) return null;
|
|
22
|
+
try {
|
|
23
|
+
const cookies = document.cookie.split(";");
|
|
24
|
+
const userCookie = cookies.find(
|
|
25
|
+
(c) => c.trim().startsWith(`${COOKIE_NAMES.USER}=`)
|
|
26
|
+
);
|
|
27
|
+
if (!userCookie) return null;
|
|
28
|
+
const value = userCookie.split("=").slice(1).join("=").trim();
|
|
29
|
+
const decoded = decodeURIComponent(value);
|
|
30
|
+
const parsed = JSON.parse(decoded);
|
|
31
|
+
if (!parsed.id || !parsed.email || !parsed.role) {
|
|
32
|
+
console.warn("[oauth-client] Invalid user cookie: missing required fields");
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
return parsed;
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.warn("[oauth-client] Failed to parse user cookie:", error);
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function hasSessionCookie() {
|
|
42
|
+
if (!isBrowser()) return false;
|
|
43
|
+
return document.cookie.includes(`${COOKIE_NAMES.SESSION}=`);
|
|
44
|
+
}
|
|
45
|
+
function hasAuthCookies() {
|
|
46
|
+
return hasSessionCookie() && parseUserCookie() !== null;
|
|
47
|
+
}
|
|
48
|
+
function clearUserCookie(domain = ".lanonasis.com") {
|
|
49
|
+
if (!isBrowser()) return;
|
|
50
|
+
document.cookie = `${COOKIE_NAMES.USER}=; domain=${domain}; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// src/react/useSSO.ts
|
|
54
|
+
function useSSO(config = {}) {
|
|
55
|
+
const {
|
|
56
|
+
authGatewayUrl = DEFAULT_AUTH_GATEWAY,
|
|
57
|
+
onAuthChange,
|
|
58
|
+
pollInterval = DEFAULT_POLL_INTERVAL
|
|
59
|
+
} = config;
|
|
60
|
+
const [state, setState] = useState({
|
|
61
|
+
isAuthenticated: false,
|
|
62
|
+
isLoading: true,
|
|
63
|
+
user: null,
|
|
64
|
+
error: null
|
|
65
|
+
});
|
|
66
|
+
const prevStateRef = useRef(null);
|
|
67
|
+
const checkAuthState = useCallback(() => {
|
|
68
|
+
if (!isBrowser()) {
|
|
69
|
+
setState({
|
|
70
|
+
isAuthenticated: false,
|
|
71
|
+
isLoading: false,
|
|
72
|
+
user: null,
|
|
73
|
+
error: null
|
|
74
|
+
});
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
const hasSession = hasSessionCookie();
|
|
79
|
+
const user = parseUserCookie();
|
|
80
|
+
const newState = {
|
|
81
|
+
isAuthenticated: hasSession && user !== null,
|
|
82
|
+
isLoading: false,
|
|
83
|
+
user: hasSession ? user : null,
|
|
84
|
+
error: null
|
|
85
|
+
};
|
|
86
|
+
setState(newState);
|
|
87
|
+
if (onAuthChange && prevStateRef.current && prevStateRef.current.isAuthenticated !== newState.isAuthenticated) {
|
|
88
|
+
onAuthChange(newState);
|
|
89
|
+
}
|
|
90
|
+
prevStateRef.current = newState;
|
|
91
|
+
} catch (error) {
|
|
92
|
+
const errorState = {
|
|
93
|
+
isAuthenticated: false,
|
|
94
|
+
isLoading: false,
|
|
95
|
+
user: null,
|
|
96
|
+
error: error instanceof Error ? error.message : "Failed to check auth state"
|
|
97
|
+
};
|
|
98
|
+
setState(errorState);
|
|
99
|
+
prevStateRef.current = errorState;
|
|
100
|
+
}
|
|
101
|
+
}, [onAuthChange]);
|
|
102
|
+
const refresh = useCallback(() => {
|
|
103
|
+
setState((prev) => ({ ...prev, isLoading: true }));
|
|
104
|
+
checkAuthState();
|
|
105
|
+
}, [checkAuthState]);
|
|
106
|
+
const logout = useCallback((returnTo) => {
|
|
107
|
+
if (!isBrowser()) return;
|
|
108
|
+
const logoutUrl = new URL("/web/logout", authGatewayUrl);
|
|
109
|
+
if (returnTo) {
|
|
110
|
+
logoutUrl.searchParams.set("return_to", returnTo);
|
|
111
|
+
}
|
|
112
|
+
window.location.href = logoutUrl.toString();
|
|
113
|
+
}, [authGatewayUrl]);
|
|
114
|
+
const getLoginUrl = useCallback((returnTo) => {
|
|
115
|
+
const loginUrl = new URL("/web/login", authGatewayUrl);
|
|
116
|
+
if (returnTo) {
|
|
117
|
+
loginUrl.searchParams.set("return_to", returnTo);
|
|
118
|
+
}
|
|
119
|
+
return loginUrl.toString();
|
|
120
|
+
}, [authGatewayUrl]);
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
const timer = setTimeout(checkAuthState, 0);
|
|
123
|
+
return () => clearTimeout(timer);
|
|
124
|
+
}, [checkAuthState]);
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
if (!isBrowser() || pollInterval <= 0) return;
|
|
127
|
+
const interval = setInterval(checkAuthState, pollInterval);
|
|
128
|
+
return () => clearInterval(interval);
|
|
129
|
+
}, [checkAuthState, pollInterval]);
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
if (!isBrowser()) return;
|
|
132
|
+
const handleStorageChange = (event) => {
|
|
133
|
+
if (event.key === null || event.key?.includes("auth")) {
|
|
134
|
+
checkAuthState();
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
window.addEventListener("storage", handleStorageChange);
|
|
138
|
+
return () => window.removeEventListener("storage", handleStorageChange);
|
|
139
|
+
}, [checkAuthState]);
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
if (!isBrowser()) return;
|
|
142
|
+
const handleFocus = () => {
|
|
143
|
+
checkAuthState();
|
|
144
|
+
};
|
|
145
|
+
window.addEventListener("focus", handleFocus);
|
|
146
|
+
return () => window.removeEventListener("focus", handleFocus);
|
|
147
|
+
}, [checkAuthState]);
|
|
148
|
+
return {
|
|
149
|
+
...state,
|
|
150
|
+
refresh,
|
|
151
|
+
logout,
|
|
152
|
+
getLoginUrl
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// src/react/useSSOSync.ts
|
|
157
|
+
import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef2 } from "react";
|
|
158
|
+
function useSSOSync(supabaseSession, config = {}) {
|
|
159
|
+
const {
|
|
160
|
+
authGatewayUrl = DEFAULT_AUTH_GATEWAY,
|
|
161
|
+
projectScope = DEFAULT_PROJECT_SCOPE,
|
|
162
|
+
onSyncComplete
|
|
163
|
+
} = config;
|
|
164
|
+
const lastSyncedTokenRef = useRef2(null);
|
|
165
|
+
const syncWithGateway = useCallback2(
|
|
166
|
+
async (session) => {
|
|
167
|
+
if (!isBrowser()) return false;
|
|
168
|
+
try {
|
|
169
|
+
const response = await fetch(
|
|
170
|
+
`${authGatewayUrl}/v1/auth/token/exchange`,
|
|
171
|
+
{
|
|
172
|
+
method: "POST",
|
|
173
|
+
headers: {
|
|
174
|
+
Authorization: `Bearer ${session.access_token}`,
|
|
175
|
+
"Content-Type": "application/json",
|
|
176
|
+
"X-Project-Scope": projectScope
|
|
177
|
+
},
|
|
178
|
+
credentials: "include",
|
|
179
|
+
// Important: include cookies
|
|
180
|
+
body: JSON.stringify({
|
|
181
|
+
project_scope: projectScope,
|
|
182
|
+
platform: "web"
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
);
|
|
186
|
+
if (!response.ok) {
|
|
187
|
+
console.warn("[oauth-client] SSO sync failed:", response.status);
|
|
188
|
+
onSyncComplete?.(false);
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
const data = await response.json();
|
|
192
|
+
if (data.cookies_set) {
|
|
193
|
+
console.log("[oauth-client] SSO cookies set successfully");
|
|
194
|
+
}
|
|
195
|
+
onSyncComplete?.(true);
|
|
196
|
+
return true;
|
|
197
|
+
} catch (error) {
|
|
198
|
+
console.error("[oauth-client] SSO sync error:", error);
|
|
199
|
+
onSyncComplete?.(false);
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
[authGatewayUrl, projectScope, onSyncComplete]
|
|
204
|
+
);
|
|
205
|
+
const sync = useCallback2(async () => {
|
|
206
|
+
if (!supabaseSession?.access_token) {
|
|
207
|
+
console.warn("[oauth-client] Cannot sync: no Supabase session");
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
return syncWithGateway(supabaseSession);
|
|
211
|
+
}, [supabaseSession, syncWithGateway]);
|
|
212
|
+
useEffect2(() => {
|
|
213
|
+
const token = supabaseSession?.access_token;
|
|
214
|
+
if (!token || token === lastSyncedTokenRef.current) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
lastSyncedTokenRef.current = token;
|
|
218
|
+
syncWithGateway(supabaseSession);
|
|
219
|
+
}, [supabaseSession?.access_token, syncWithGateway, supabaseSession]);
|
|
220
|
+
return {
|
|
221
|
+
sync,
|
|
222
|
+
syncWithGateway
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
export {
|
|
226
|
+
COOKIE_NAMES,
|
|
227
|
+
DEFAULT_AUTH_GATEWAY,
|
|
228
|
+
DEFAULT_COOKIE_DOMAIN,
|
|
229
|
+
DEFAULT_POLL_INTERVAL,
|
|
230
|
+
DEFAULT_PROJECT_SCOPE,
|
|
231
|
+
clearUserCookie,
|
|
232
|
+
hasAuthCookies,
|
|
233
|
+
hasSessionCookie,
|
|
234
|
+
isBrowser,
|
|
235
|
+
parseUserCookie,
|
|
236
|
+
useSSO,
|
|
237
|
+
useSSOSync
|
|
238
|
+
};
|