@vylth/nexus-react 1.0.0

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.
@@ -0,0 +1,86 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode } from 'react';
3
+
4
+ interface NexusUser {
5
+ id: string;
6
+ email: string;
7
+ username: string;
8
+ firstName: string;
9
+ lastName: string;
10
+ avatar: string;
11
+ globalRole: string;
12
+ emailVerified: boolean;
13
+ affiliateCode: string;
14
+ twoFactorEnabled: boolean;
15
+ appRoles?: Record<string, {
16
+ role: string;
17
+ permissions: string[];
18
+ }>;
19
+ }
20
+ interface NexusConfig {
21
+ clientId: string;
22
+ redirectUri: string;
23
+ nexusUrl?: string;
24
+ apiUrl?: string;
25
+ }
26
+ interface NexusAuthResult {
27
+ access_token: string;
28
+ refresh_token: string;
29
+ expires_in: number;
30
+ investor: {
31
+ id: string;
32
+ email: string;
33
+ first_name: string;
34
+ last_name: string;
35
+ username: string;
36
+ avatar_url: string;
37
+ email_verified: boolean;
38
+ two_factor_enabled: boolean;
39
+ global_role: string;
40
+ };
41
+ session_id: string;
42
+ }
43
+ interface NexusContextValue {
44
+ user: NexusUser | null;
45
+ isAuthenticated: boolean;
46
+ isLoading: boolean;
47
+ accessToken: string | null;
48
+ login: () => void;
49
+ logout: () => void;
50
+ refreshUser: () => Promise<void>;
51
+ }
52
+
53
+ interface NexusProviderProps extends NexusConfig {
54
+ children: ReactNode;
55
+ onLogin?: (user: NexusUser) => void;
56
+ onLogout?: () => void;
57
+ }
58
+ declare function NexusProvider({ children, clientId, redirectUri, nexusUrl, apiUrl, onLogin, onLogout, }: NexusProviderProps): react_jsx_runtime.JSX.Element;
59
+
60
+ interface NexusCallbackProps {
61
+ onSuccess?: () => void;
62
+ onError?: (error: string) => void;
63
+ }
64
+ declare function NexusCallback({ onSuccess, onError }: NexusCallbackProps): react_jsx_runtime.JSX.Element;
65
+
66
+ interface NexusLoginButtonProps {
67
+ label?: string;
68
+ size?: 'sm' | 'md' | 'lg';
69
+ variant?: 'dark' | 'light' | 'outline';
70
+ className?: string;
71
+ style?: React.CSSProperties;
72
+ }
73
+ declare function NexusLoginButton({ label, size, variant, className, style: customStyle, }: NexusLoginButtonProps): react_jsx_runtime.JSX.Element;
74
+
75
+ declare function useNexus(): NexusContextValue;
76
+
77
+ declare function setTokens(access: string, refresh: string): void;
78
+ declare function getAccessToken(): string | null;
79
+ declare function getRefreshToken(): string | null;
80
+ declare function clearTokens(): void;
81
+ declare function isTokenValid(): boolean;
82
+ declare function decodeUser(token: string): NexusUser | null;
83
+ declare function refreshTokens(apiUrl: string): Promise<boolean>;
84
+ declare function nexusFetch<T>(apiUrl: string, path: string, options?: RequestInit, onAuthFailure?: () => void): Promise<T>;
85
+
86
+ export { type NexusAuthResult, NexusCallback, type NexusConfig, type NexusContextValue, NexusLoginButton, NexusProvider, type NexusUser, clearTokens, decodeUser, getAccessToken, getRefreshToken, isTokenValid, nexusFetch, refreshTokens, setTokens, useNexus };
@@ -0,0 +1,86 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode } from 'react';
3
+
4
+ interface NexusUser {
5
+ id: string;
6
+ email: string;
7
+ username: string;
8
+ firstName: string;
9
+ lastName: string;
10
+ avatar: string;
11
+ globalRole: string;
12
+ emailVerified: boolean;
13
+ affiliateCode: string;
14
+ twoFactorEnabled: boolean;
15
+ appRoles?: Record<string, {
16
+ role: string;
17
+ permissions: string[];
18
+ }>;
19
+ }
20
+ interface NexusConfig {
21
+ clientId: string;
22
+ redirectUri: string;
23
+ nexusUrl?: string;
24
+ apiUrl?: string;
25
+ }
26
+ interface NexusAuthResult {
27
+ access_token: string;
28
+ refresh_token: string;
29
+ expires_in: number;
30
+ investor: {
31
+ id: string;
32
+ email: string;
33
+ first_name: string;
34
+ last_name: string;
35
+ username: string;
36
+ avatar_url: string;
37
+ email_verified: boolean;
38
+ two_factor_enabled: boolean;
39
+ global_role: string;
40
+ };
41
+ session_id: string;
42
+ }
43
+ interface NexusContextValue {
44
+ user: NexusUser | null;
45
+ isAuthenticated: boolean;
46
+ isLoading: boolean;
47
+ accessToken: string | null;
48
+ login: () => void;
49
+ logout: () => void;
50
+ refreshUser: () => Promise<void>;
51
+ }
52
+
53
+ interface NexusProviderProps extends NexusConfig {
54
+ children: ReactNode;
55
+ onLogin?: (user: NexusUser) => void;
56
+ onLogout?: () => void;
57
+ }
58
+ declare function NexusProvider({ children, clientId, redirectUri, nexusUrl, apiUrl, onLogin, onLogout, }: NexusProviderProps): react_jsx_runtime.JSX.Element;
59
+
60
+ interface NexusCallbackProps {
61
+ onSuccess?: () => void;
62
+ onError?: (error: string) => void;
63
+ }
64
+ declare function NexusCallback({ onSuccess, onError }: NexusCallbackProps): react_jsx_runtime.JSX.Element;
65
+
66
+ interface NexusLoginButtonProps {
67
+ label?: string;
68
+ size?: 'sm' | 'md' | 'lg';
69
+ variant?: 'dark' | 'light' | 'outline';
70
+ className?: string;
71
+ style?: React.CSSProperties;
72
+ }
73
+ declare function NexusLoginButton({ label, size, variant, className, style: customStyle, }: NexusLoginButtonProps): react_jsx_runtime.JSX.Element;
74
+
75
+ declare function useNexus(): NexusContextValue;
76
+
77
+ declare function setTokens(access: string, refresh: string): void;
78
+ declare function getAccessToken(): string | null;
79
+ declare function getRefreshToken(): string | null;
80
+ declare function clearTokens(): void;
81
+ declare function isTokenValid(): boolean;
82
+ declare function decodeUser(token: string): NexusUser | null;
83
+ declare function refreshTokens(apiUrl: string): Promise<boolean>;
84
+ declare function nexusFetch<T>(apiUrl: string, path: string, options?: RequestInit, onAuthFailure?: () => void): Promise<T>;
85
+
86
+ export { type NexusAuthResult, NexusCallback, type NexusConfig, type NexusContextValue, NexusLoginButton, NexusProvider, type NexusUser, clearTokens, decodeUser, getAccessToken, getRefreshToken, isTokenValid, nexusFetch, refreshTokens, setTokens, useNexus };
package/dist/index.js ADDED
@@ -0,0 +1,472 @@
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/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ NexusCallback: () => NexusCallback,
24
+ NexusLoginButton: () => NexusLoginButton,
25
+ NexusProvider: () => NexusProvider,
26
+ clearTokens: () => clearTokens,
27
+ decodeUser: () => decodeUser,
28
+ getAccessToken: () => getAccessToken,
29
+ getRefreshToken: () => getRefreshToken,
30
+ isTokenValid: () => isTokenValid,
31
+ nexusFetch: () => nexusFetch,
32
+ refreshTokens: () => refreshTokens,
33
+ setTokens: () => setTokens,
34
+ useNexus: () => useNexus
35
+ });
36
+ module.exports = __toCommonJS(index_exports);
37
+
38
+ // src/NexusProvider.tsx
39
+ var import_react = require("react");
40
+
41
+ // src/client.ts
42
+ var TOKEN_KEY = "nexus_access_token";
43
+ var REFRESH_KEY = "nexus_refresh_token";
44
+ var STATE_KEY = "nexus_oauth_state";
45
+ var memoryAccessToken = null;
46
+ var memoryRefreshToken = null;
47
+ var refreshPromise = null;
48
+ function setTokens(access, refresh) {
49
+ memoryAccessToken = access;
50
+ memoryRefreshToken = refresh;
51
+ localStorage.setItem(TOKEN_KEY, access);
52
+ localStorage.setItem(REFRESH_KEY, refresh);
53
+ }
54
+ function getAccessToken() {
55
+ if (!memoryAccessToken) {
56
+ memoryAccessToken = localStorage.getItem(TOKEN_KEY);
57
+ }
58
+ return memoryAccessToken;
59
+ }
60
+ function getRefreshToken() {
61
+ if (!memoryRefreshToken) {
62
+ memoryRefreshToken = localStorage.getItem(REFRESH_KEY);
63
+ }
64
+ return memoryRefreshToken;
65
+ }
66
+ function clearTokens() {
67
+ memoryAccessToken = null;
68
+ memoryRefreshToken = null;
69
+ localStorage.removeItem(TOKEN_KEY);
70
+ localStorage.removeItem(REFRESH_KEY);
71
+ }
72
+ function isTokenValid() {
73
+ const token = getAccessToken();
74
+ if (!token) return false;
75
+ try {
76
+ const payload = JSON.parse(atob(token.split(".")[1].replace(/-/g, "+").replace(/_/g, "/")));
77
+ return payload.exp * 1e3 > Date.now() + 3e4;
78
+ } catch {
79
+ return false;
80
+ }
81
+ }
82
+ function decodeUser(token) {
83
+ try {
84
+ const payload = JSON.parse(atob(token.split(".")[1].replace(/-/g, "+").replace(/_/g, "/")));
85
+ return {
86
+ id: payload.sub,
87
+ email: payload.email,
88
+ username: payload.username || "",
89
+ firstName: "",
90
+ lastName: "",
91
+ avatar: payload.avatar || "",
92
+ globalRole: payload.global_role || "citizen",
93
+ emailVerified: payload.email_verified || false,
94
+ affiliateCode: payload.affiliate_code || "",
95
+ twoFactorEnabled: payload.two_factor_verified || false,
96
+ appRoles: payload.app_roles
97
+ };
98
+ } catch {
99
+ return null;
100
+ }
101
+ }
102
+ function generateState() {
103
+ const state = Math.random().toString(36).substring(2) + Math.random().toString(36).substring(2);
104
+ sessionStorage.setItem(STATE_KEY, state);
105
+ return state;
106
+ }
107
+ function validateState(state) {
108
+ const saved = sessionStorage.getItem(STATE_KEY);
109
+ sessionStorage.removeItem(STATE_KEY);
110
+ return saved === state;
111
+ }
112
+ function getLoginUrl(config) {
113
+ const nexusUrl = config.nexusUrl || "https://accounts.vylth.com";
114
+ const state = generateState();
115
+ const params = new URLSearchParams({
116
+ client_id: config.clientId,
117
+ redirect_uri: config.redirectUri,
118
+ state
119
+ });
120
+ return `${nexusUrl}/oauth/authorize?${params.toString()}`;
121
+ }
122
+ async function exchangeCode(code, apiUrl) {
123
+ const res = await fetch(`${apiUrl}/token/exchange`, {
124
+ method: "POST",
125
+ headers: { "Content-Type": "application/json" },
126
+ body: JSON.stringify({ code })
127
+ });
128
+ if (!res.ok) {
129
+ const err = await res.json().catch(() => ({ error: "Token exchange failed" }));
130
+ throw new Error(err.error || "Token exchange failed");
131
+ }
132
+ const data = await res.json();
133
+ setTokens(data.access_token, data.refresh_token);
134
+ return data;
135
+ }
136
+ async function doRefresh(apiUrl) {
137
+ const rt = getRefreshToken();
138
+ if (!rt) return false;
139
+ try {
140
+ const res = await fetch(`${apiUrl}/token/refresh`, {
141
+ method: "POST",
142
+ headers: { "Content-Type": "application/json" },
143
+ body: JSON.stringify({ refresh_token: rt })
144
+ });
145
+ if (res.ok) {
146
+ const data = await res.json();
147
+ setTokens(data.access_token, data.refresh_token);
148
+ return true;
149
+ }
150
+ } catch {
151
+ }
152
+ return false;
153
+ }
154
+ function refreshTokens(apiUrl) {
155
+ if (!refreshPromise) {
156
+ refreshPromise = doRefresh(apiUrl).finally(() => {
157
+ refreshPromise = null;
158
+ });
159
+ }
160
+ return refreshPromise;
161
+ }
162
+ async function nexusFetch(apiUrl, path, options = {}, onAuthFailure) {
163
+ const token = getAccessToken();
164
+ const headers = {
165
+ "Content-Type": "application/json",
166
+ ...options.headers
167
+ };
168
+ if (token) {
169
+ headers["Authorization"] = `Bearer ${token}`;
170
+ }
171
+ const res = await fetch(`${apiUrl}${path}`, { ...options, headers });
172
+ if (res.status === 401 && token) {
173
+ const refreshed = await refreshTokens(apiUrl);
174
+ if (refreshed) {
175
+ headers["Authorization"] = `Bearer ${getAccessToken()}`;
176
+ const retry = await fetch(`${apiUrl}${path}`, { ...options, headers });
177
+ if (!retry.ok) {
178
+ const err = await retry.json().catch(() => ({ error: "Request failed" }));
179
+ throw new Error(err.error || "Request failed");
180
+ }
181
+ return retry.json();
182
+ }
183
+ clearTokens();
184
+ onAuthFailure?.();
185
+ throw new Error("Session expired");
186
+ }
187
+ if (!res.ok) {
188
+ const err = await res.json().catch(() => ({ error: `HTTP ${res.status}` }));
189
+ throw new Error(err.error || `HTTP ${res.status}`);
190
+ }
191
+ return res.json();
192
+ }
193
+
194
+ // src/NexusProvider.tsx
195
+ var import_jsx_runtime = require("react/jsx-runtime");
196
+ var NexusContext = (0, import_react.createContext)(null);
197
+ function NexusProvider({
198
+ children,
199
+ clientId,
200
+ redirectUri,
201
+ nexusUrl,
202
+ apiUrl,
203
+ onLogin,
204
+ onLogout
205
+ }) {
206
+ const resolvedApiUrl = apiUrl || "https://auth.vylth.com/api/nexus";
207
+ const config = { clientId, redirectUri, nexusUrl, apiUrl: resolvedApiUrl };
208
+ const [user, setUser] = (0, import_react.useState)(null);
209
+ const [isLoading, setIsLoading] = (0, import_react.useState)(true);
210
+ const initAuth = (0, import_react.useCallback)(async () => {
211
+ const token = getAccessToken();
212
+ if (!token) {
213
+ setIsLoading(false);
214
+ return;
215
+ }
216
+ if (isTokenValid()) {
217
+ const decoded = decodeUser(token);
218
+ if (decoded) {
219
+ setUser(decoded);
220
+ setIsLoading(false);
221
+ return;
222
+ }
223
+ }
224
+ const refreshed = await refreshTokens(resolvedApiUrl);
225
+ if (refreshed) {
226
+ const newToken = getAccessToken();
227
+ if (newToken) {
228
+ const decoded = decodeUser(newToken);
229
+ setUser(decoded);
230
+ }
231
+ } else {
232
+ clearTokens();
233
+ }
234
+ setIsLoading(false);
235
+ }, [resolvedApiUrl]);
236
+ (0, import_react.useEffect)(() => {
237
+ initAuth();
238
+ }, [initAuth]);
239
+ const login = (0, import_react.useCallback)(() => {
240
+ window.location.href = getLoginUrl(config);
241
+ }, [config]);
242
+ const logout = (0, import_react.useCallback)(() => {
243
+ clearTokens();
244
+ setUser(null);
245
+ onLogout?.();
246
+ }, [onLogout]);
247
+ const refreshUser = (0, import_react.useCallback)(async () => {
248
+ const refreshed = await refreshTokens(resolvedApiUrl);
249
+ if (refreshed) {
250
+ const token = getAccessToken();
251
+ if (token) {
252
+ const decoded = decodeUser(token);
253
+ setUser(decoded);
254
+ }
255
+ }
256
+ }, [resolvedApiUrl]);
257
+ const setUserWithCallback = (0, import_react.useCallback)(
258
+ (u) => {
259
+ setUser(u);
260
+ onLogin?.(u);
261
+ },
262
+ [onLogin]
263
+ );
264
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
265
+ NexusContext.Provider,
266
+ {
267
+ value: {
268
+ user,
269
+ isAuthenticated: !!user,
270
+ isLoading,
271
+ accessToken: getAccessToken(),
272
+ login,
273
+ logout,
274
+ refreshUser
275
+ },
276
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(NexusConfigContext.Provider, { value: { ...config, setUser: setUserWithCallback }, children })
277
+ }
278
+ );
279
+ }
280
+ var NexusConfigContext = (0, import_react.createContext)(null);
281
+
282
+ // src/NexusCallback.tsx
283
+ var import_react2 = require("react");
284
+ var import_jsx_runtime2 = require("react/jsx-runtime");
285
+ function NexusCallback({ onSuccess, onError }) {
286
+ const config = (0, import_react2.useContext)(NexusConfigContext);
287
+ const [error, setError] = (0, import_react2.useState)("");
288
+ (0, import_react2.useEffect)(() => {
289
+ if (!config) return;
290
+ const params = new URLSearchParams(window.location.search);
291
+ const code = params.get("code");
292
+ const state = params.get("state");
293
+ const oauthError = params.get("error");
294
+ if (oauthError) {
295
+ const msg = `Authentication denied: ${oauthError}`;
296
+ setError(msg);
297
+ onError?.(msg);
298
+ return;
299
+ }
300
+ if (!code) {
301
+ const msg = "No authorization code received";
302
+ setError(msg);
303
+ onError?.(msg);
304
+ return;
305
+ }
306
+ if (state && !validateState(state)) {
307
+ const msg = "Invalid state parameter";
308
+ setError(msg);
309
+ onError?.(msg);
310
+ return;
311
+ }
312
+ const apiUrl = config.apiUrl || "https://auth.vylth.com/api/nexus";
313
+ exchangeCode(code, apiUrl).then((result) => {
314
+ const user = decodeUser(result.access_token);
315
+ if (user) {
316
+ user.firstName = result.investor.first_name;
317
+ user.lastName = result.investor.last_name;
318
+ user.avatar = result.investor.avatar_url || user.avatar;
319
+ user.emailVerified = result.investor.email_verified;
320
+ config.setUser(user);
321
+ }
322
+ onSuccess?.();
323
+ }).catch((err) => {
324
+ const msg = err.message || "Authentication failed";
325
+ setError(msg);
326
+ onError?.(msg);
327
+ });
328
+ }, [config]);
329
+ if (error) {
330
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", minHeight: "100vh", fontFamily: "system-ui, sans-serif", background: "#0a0a0a", color: "#fff" }, children: [
331
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: { color: "#ef4444", marginBottom: "16px" }, children: error }),
332
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
333
+ "button",
334
+ {
335
+ onClick: () => window.location.href = "/",
336
+ style: { padding: "8px 24px", borderRadius: "8px", border: "1px solid rgba(255,255,255,0.1)", background: "rgba(255,255,255,0.05)", color: "#fff", cursor: "pointer" },
337
+ children: "Try Again"
338
+ }
339
+ )
340
+ ] });
341
+ }
342
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { display: "flex", alignItems: "center", justifyContent: "center", minHeight: "100vh", background: "#0a0a0a", color: "#fff" }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { textAlign: "center" }, children: [
343
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { width: "32px", height: "32px", border: "3px solid rgba(255,255,255,0.1)", borderTopColor: "#d4a574", borderRadius: "50%", animation: "nexus-spin 0.8s linear infinite", margin: "0 auto 16px" } }),
344
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: { fontFamily: "monospace", fontSize: "14px", opacity: 0.6 }, children: "Authenticating with Nexus..." }),
345
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("style", { children: `@keyframes nexus-spin { to { transform: rotate(360deg) } }` })
346
+ ] }) });
347
+ }
348
+
349
+ // src/NexusLoginButton.tsx
350
+ var import_react3 = require("react");
351
+ var import_jsx_runtime3 = require("react/jsx-runtime");
352
+ var NEXUS_LOGO = `data:image/svg+xml,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>`)}`;
353
+ var sizes = {
354
+ sm: { padding: "8px 16px", fontSize: "13px", iconSize: 16, gap: 6 },
355
+ md: { padding: "10px 24px", fontSize: "14px", iconSize: 18, gap: 8 },
356
+ lg: { padding: "12px 32px", fontSize: "15px", iconSize: 20, gap: 10 }
357
+ };
358
+ var variants = {
359
+ dark: {
360
+ background: "#1a1a1a",
361
+ color: "#ffffff",
362
+ border: "1px solid rgba(255,255,255,0.1)",
363
+ hoverBg: "#252525"
364
+ },
365
+ light: {
366
+ background: "#ffffff",
367
+ color: "#0a0a0a",
368
+ border: "1px solid rgba(0,0,0,0.1)",
369
+ hoverBg: "#f5f5f5"
370
+ },
371
+ outline: {
372
+ background: "transparent",
373
+ color: "#d4a574",
374
+ border: "1px solid #d4a574",
375
+ hoverBg: "rgba(212,165,116,0.1)"
376
+ }
377
+ };
378
+ function NexusLoginButton({
379
+ label = "Sign in with Nexus",
380
+ size = "md",
381
+ variant = "dark",
382
+ className,
383
+ style: customStyle
384
+ }) {
385
+ const context = (0, import_react3.useContext)(NexusContext);
386
+ const s = sizes[size];
387
+ const v = variants[variant];
388
+ const handleClick = () => {
389
+ context?.login();
390
+ };
391
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
392
+ "button",
393
+ {
394
+ onClick: handleClick,
395
+ className,
396
+ style: {
397
+ display: "inline-flex",
398
+ alignItems: "center",
399
+ justifyContent: "center",
400
+ gap: `${s.gap}px`,
401
+ padding: s.padding,
402
+ fontSize: s.fontSize,
403
+ fontFamily: "'JetBrains Mono', 'SF Mono', 'Fira Code', monospace",
404
+ fontWeight: 500,
405
+ letterSpacing: "0.02em",
406
+ background: v.background,
407
+ color: v.color,
408
+ border: v.border,
409
+ borderRadius: "12px",
410
+ cursor: "pointer",
411
+ transition: "all 0.2s ease",
412
+ textDecoration: "none",
413
+ whiteSpace: "nowrap",
414
+ ...customStyle
415
+ },
416
+ onMouseEnter: (e) => {
417
+ e.target.style.background = v.hoverBg;
418
+ e.target.style.transform = "translateY(-1px)";
419
+ },
420
+ onMouseLeave: (e) => {
421
+ e.target.style.background = v.background;
422
+ e.target.style.transform = "translateY(0)";
423
+ },
424
+ children: [
425
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
426
+ "svg",
427
+ {
428
+ width: s.iconSize,
429
+ height: s.iconSize,
430
+ viewBox: "0 0 24 24",
431
+ fill: "none",
432
+ stroke: "#d4a574",
433
+ strokeWidth: "2",
434
+ strokeLinecap: "round",
435
+ strokeLinejoin: "round",
436
+ children: [
437
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M12 2L2 7l10 5 10-5-10-5z" }),
438
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M2 17l10 5 10-5" }),
439
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M2 12l10 5 10-5" })
440
+ ]
441
+ }
442
+ ),
443
+ label
444
+ ]
445
+ }
446
+ );
447
+ }
448
+
449
+ // src/useNexus.ts
450
+ var import_react4 = require("react");
451
+ function useNexus() {
452
+ const context = (0, import_react4.useContext)(NexusContext);
453
+ if (!context) {
454
+ throw new Error("useNexus must be used within a <NexusProvider>");
455
+ }
456
+ return context;
457
+ }
458
+ // Annotate the CommonJS export names for ESM import in node:
459
+ 0 && (module.exports = {
460
+ NexusCallback,
461
+ NexusLoginButton,
462
+ NexusProvider,
463
+ clearTokens,
464
+ decodeUser,
465
+ getAccessToken,
466
+ getRefreshToken,
467
+ isTokenValid,
468
+ nexusFetch,
469
+ refreshTokens,
470
+ setTokens,
471
+ useNexus
472
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,434 @@
1
+ // src/NexusProvider.tsx
2
+ import { createContext, useCallback, useEffect, useState } from "react";
3
+
4
+ // src/client.ts
5
+ var TOKEN_KEY = "nexus_access_token";
6
+ var REFRESH_KEY = "nexus_refresh_token";
7
+ var STATE_KEY = "nexus_oauth_state";
8
+ var memoryAccessToken = null;
9
+ var memoryRefreshToken = null;
10
+ var refreshPromise = null;
11
+ function setTokens(access, refresh) {
12
+ memoryAccessToken = access;
13
+ memoryRefreshToken = refresh;
14
+ localStorage.setItem(TOKEN_KEY, access);
15
+ localStorage.setItem(REFRESH_KEY, refresh);
16
+ }
17
+ function getAccessToken() {
18
+ if (!memoryAccessToken) {
19
+ memoryAccessToken = localStorage.getItem(TOKEN_KEY);
20
+ }
21
+ return memoryAccessToken;
22
+ }
23
+ function getRefreshToken() {
24
+ if (!memoryRefreshToken) {
25
+ memoryRefreshToken = localStorage.getItem(REFRESH_KEY);
26
+ }
27
+ return memoryRefreshToken;
28
+ }
29
+ function clearTokens() {
30
+ memoryAccessToken = null;
31
+ memoryRefreshToken = null;
32
+ localStorage.removeItem(TOKEN_KEY);
33
+ localStorage.removeItem(REFRESH_KEY);
34
+ }
35
+ function isTokenValid() {
36
+ const token = getAccessToken();
37
+ if (!token) return false;
38
+ try {
39
+ const payload = JSON.parse(atob(token.split(".")[1].replace(/-/g, "+").replace(/_/g, "/")));
40
+ return payload.exp * 1e3 > Date.now() + 3e4;
41
+ } catch {
42
+ return false;
43
+ }
44
+ }
45
+ function decodeUser(token) {
46
+ try {
47
+ const payload = JSON.parse(atob(token.split(".")[1].replace(/-/g, "+").replace(/_/g, "/")));
48
+ return {
49
+ id: payload.sub,
50
+ email: payload.email,
51
+ username: payload.username || "",
52
+ firstName: "",
53
+ lastName: "",
54
+ avatar: payload.avatar || "",
55
+ globalRole: payload.global_role || "citizen",
56
+ emailVerified: payload.email_verified || false,
57
+ affiliateCode: payload.affiliate_code || "",
58
+ twoFactorEnabled: payload.two_factor_verified || false,
59
+ appRoles: payload.app_roles
60
+ };
61
+ } catch {
62
+ return null;
63
+ }
64
+ }
65
+ function generateState() {
66
+ const state = Math.random().toString(36).substring(2) + Math.random().toString(36).substring(2);
67
+ sessionStorage.setItem(STATE_KEY, state);
68
+ return state;
69
+ }
70
+ function validateState(state) {
71
+ const saved = sessionStorage.getItem(STATE_KEY);
72
+ sessionStorage.removeItem(STATE_KEY);
73
+ return saved === state;
74
+ }
75
+ function getLoginUrl(config) {
76
+ const nexusUrl = config.nexusUrl || "https://accounts.vylth.com";
77
+ const state = generateState();
78
+ const params = new URLSearchParams({
79
+ client_id: config.clientId,
80
+ redirect_uri: config.redirectUri,
81
+ state
82
+ });
83
+ return `${nexusUrl}/oauth/authorize?${params.toString()}`;
84
+ }
85
+ async function exchangeCode(code, apiUrl) {
86
+ const res = await fetch(`${apiUrl}/token/exchange`, {
87
+ method: "POST",
88
+ headers: { "Content-Type": "application/json" },
89
+ body: JSON.stringify({ code })
90
+ });
91
+ if (!res.ok) {
92
+ const err = await res.json().catch(() => ({ error: "Token exchange failed" }));
93
+ throw new Error(err.error || "Token exchange failed");
94
+ }
95
+ const data = await res.json();
96
+ setTokens(data.access_token, data.refresh_token);
97
+ return data;
98
+ }
99
+ async function doRefresh(apiUrl) {
100
+ const rt = getRefreshToken();
101
+ if (!rt) return false;
102
+ try {
103
+ const res = await fetch(`${apiUrl}/token/refresh`, {
104
+ method: "POST",
105
+ headers: { "Content-Type": "application/json" },
106
+ body: JSON.stringify({ refresh_token: rt })
107
+ });
108
+ if (res.ok) {
109
+ const data = await res.json();
110
+ setTokens(data.access_token, data.refresh_token);
111
+ return true;
112
+ }
113
+ } catch {
114
+ }
115
+ return false;
116
+ }
117
+ function refreshTokens(apiUrl) {
118
+ if (!refreshPromise) {
119
+ refreshPromise = doRefresh(apiUrl).finally(() => {
120
+ refreshPromise = null;
121
+ });
122
+ }
123
+ return refreshPromise;
124
+ }
125
+ async function nexusFetch(apiUrl, path, options = {}, onAuthFailure) {
126
+ const token = getAccessToken();
127
+ const headers = {
128
+ "Content-Type": "application/json",
129
+ ...options.headers
130
+ };
131
+ if (token) {
132
+ headers["Authorization"] = `Bearer ${token}`;
133
+ }
134
+ const res = await fetch(`${apiUrl}${path}`, { ...options, headers });
135
+ if (res.status === 401 && token) {
136
+ const refreshed = await refreshTokens(apiUrl);
137
+ if (refreshed) {
138
+ headers["Authorization"] = `Bearer ${getAccessToken()}`;
139
+ const retry = await fetch(`${apiUrl}${path}`, { ...options, headers });
140
+ if (!retry.ok) {
141
+ const err = await retry.json().catch(() => ({ error: "Request failed" }));
142
+ throw new Error(err.error || "Request failed");
143
+ }
144
+ return retry.json();
145
+ }
146
+ clearTokens();
147
+ onAuthFailure?.();
148
+ throw new Error("Session expired");
149
+ }
150
+ if (!res.ok) {
151
+ const err = await res.json().catch(() => ({ error: `HTTP ${res.status}` }));
152
+ throw new Error(err.error || `HTTP ${res.status}`);
153
+ }
154
+ return res.json();
155
+ }
156
+
157
+ // src/NexusProvider.tsx
158
+ import { jsx } from "react/jsx-runtime";
159
+ var NexusContext = createContext(null);
160
+ function NexusProvider({
161
+ children,
162
+ clientId,
163
+ redirectUri,
164
+ nexusUrl,
165
+ apiUrl,
166
+ onLogin,
167
+ onLogout
168
+ }) {
169
+ const resolvedApiUrl = apiUrl || "https://auth.vylth.com/api/nexus";
170
+ const config = { clientId, redirectUri, nexusUrl, apiUrl: resolvedApiUrl };
171
+ const [user, setUser] = useState(null);
172
+ const [isLoading, setIsLoading] = useState(true);
173
+ const initAuth = useCallback(async () => {
174
+ const token = getAccessToken();
175
+ if (!token) {
176
+ setIsLoading(false);
177
+ return;
178
+ }
179
+ if (isTokenValid()) {
180
+ const decoded = decodeUser(token);
181
+ if (decoded) {
182
+ setUser(decoded);
183
+ setIsLoading(false);
184
+ return;
185
+ }
186
+ }
187
+ const refreshed = await refreshTokens(resolvedApiUrl);
188
+ if (refreshed) {
189
+ const newToken = getAccessToken();
190
+ if (newToken) {
191
+ const decoded = decodeUser(newToken);
192
+ setUser(decoded);
193
+ }
194
+ } else {
195
+ clearTokens();
196
+ }
197
+ setIsLoading(false);
198
+ }, [resolvedApiUrl]);
199
+ useEffect(() => {
200
+ initAuth();
201
+ }, [initAuth]);
202
+ const login = useCallback(() => {
203
+ window.location.href = getLoginUrl(config);
204
+ }, [config]);
205
+ const logout = useCallback(() => {
206
+ clearTokens();
207
+ setUser(null);
208
+ onLogout?.();
209
+ }, [onLogout]);
210
+ const refreshUser = useCallback(async () => {
211
+ const refreshed = await refreshTokens(resolvedApiUrl);
212
+ if (refreshed) {
213
+ const token = getAccessToken();
214
+ if (token) {
215
+ const decoded = decodeUser(token);
216
+ setUser(decoded);
217
+ }
218
+ }
219
+ }, [resolvedApiUrl]);
220
+ const setUserWithCallback = useCallback(
221
+ (u) => {
222
+ setUser(u);
223
+ onLogin?.(u);
224
+ },
225
+ [onLogin]
226
+ );
227
+ return /* @__PURE__ */ jsx(
228
+ NexusContext.Provider,
229
+ {
230
+ value: {
231
+ user,
232
+ isAuthenticated: !!user,
233
+ isLoading,
234
+ accessToken: getAccessToken(),
235
+ login,
236
+ logout,
237
+ refreshUser
238
+ },
239
+ children: /* @__PURE__ */ jsx(NexusConfigContext.Provider, { value: { ...config, setUser: setUserWithCallback }, children })
240
+ }
241
+ );
242
+ }
243
+ var NexusConfigContext = createContext(null);
244
+
245
+ // src/NexusCallback.tsx
246
+ import { useContext, useEffect as useEffect2, useState as useState2 } from "react";
247
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
248
+ function NexusCallback({ onSuccess, onError }) {
249
+ const config = useContext(NexusConfigContext);
250
+ const [error, setError] = useState2("");
251
+ useEffect2(() => {
252
+ if (!config) return;
253
+ const params = new URLSearchParams(window.location.search);
254
+ const code = params.get("code");
255
+ const state = params.get("state");
256
+ const oauthError = params.get("error");
257
+ if (oauthError) {
258
+ const msg = `Authentication denied: ${oauthError}`;
259
+ setError(msg);
260
+ onError?.(msg);
261
+ return;
262
+ }
263
+ if (!code) {
264
+ const msg = "No authorization code received";
265
+ setError(msg);
266
+ onError?.(msg);
267
+ return;
268
+ }
269
+ if (state && !validateState(state)) {
270
+ const msg = "Invalid state parameter";
271
+ setError(msg);
272
+ onError?.(msg);
273
+ return;
274
+ }
275
+ const apiUrl = config.apiUrl || "https://auth.vylth.com/api/nexus";
276
+ exchangeCode(code, apiUrl).then((result) => {
277
+ const user = decodeUser(result.access_token);
278
+ if (user) {
279
+ user.firstName = result.investor.first_name;
280
+ user.lastName = result.investor.last_name;
281
+ user.avatar = result.investor.avatar_url || user.avatar;
282
+ user.emailVerified = result.investor.email_verified;
283
+ config.setUser(user);
284
+ }
285
+ onSuccess?.();
286
+ }).catch((err) => {
287
+ const msg = err.message || "Authentication failed";
288
+ setError(msg);
289
+ onError?.(msg);
290
+ });
291
+ }, [config]);
292
+ if (error) {
293
+ return /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", minHeight: "100vh", fontFamily: "system-ui, sans-serif", background: "#0a0a0a", color: "#fff" }, children: [
294
+ /* @__PURE__ */ jsx2("p", { style: { color: "#ef4444", marginBottom: "16px" }, children: error }),
295
+ /* @__PURE__ */ jsx2(
296
+ "button",
297
+ {
298
+ onClick: () => window.location.href = "/",
299
+ style: { padding: "8px 24px", borderRadius: "8px", border: "1px solid rgba(255,255,255,0.1)", background: "rgba(255,255,255,0.05)", color: "#fff", cursor: "pointer" },
300
+ children: "Try Again"
301
+ }
302
+ )
303
+ ] });
304
+ }
305
+ return /* @__PURE__ */ jsx2("div", { style: { display: "flex", alignItems: "center", justifyContent: "center", minHeight: "100vh", background: "#0a0a0a", color: "#fff" }, children: /* @__PURE__ */ jsxs("div", { style: { textAlign: "center" }, children: [
306
+ /* @__PURE__ */ jsx2("div", { style: { width: "32px", height: "32px", border: "3px solid rgba(255,255,255,0.1)", borderTopColor: "#d4a574", borderRadius: "50%", animation: "nexus-spin 0.8s linear infinite", margin: "0 auto 16px" } }),
307
+ /* @__PURE__ */ jsx2("p", { style: { fontFamily: "monospace", fontSize: "14px", opacity: 0.6 }, children: "Authenticating with Nexus..." }),
308
+ /* @__PURE__ */ jsx2("style", { children: `@keyframes nexus-spin { to { transform: rotate(360deg) } }` })
309
+ ] }) });
310
+ }
311
+
312
+ // src/NexusLoginButton.tsx
313
+ import { useContext as useContext2 } from "react";
314
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
315
+ var NEXUS_LOGO = `data:image/svg+xml,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>`)}`;
316
+ var sizes = {
317
+ sm: { padding: "8px 16px", fontSize: "13px", iconSize: 16, gap: 6 },
318
+ md: { padding: "10px 24px", fontSize: "14px", iconSize: 18, gap: 8 },
319
+ lg: { padding: "12px 32px", fontSize: "15px", iconSize: 20, gap: 10 }
320
+ };
321
+ var variants = {
322
+ dark: {
323
+ background: "#1a1a1a",
324
+ color: "#ffffff",
325
+ border: "1px solid rgba(255,255,255,0.1)",
326
+ hoverBg: "#252525"
327
+ },
328
+ light: {
329
+ background: "#ffffff",
330
+ color: "#0a0a0a",
331
+ border: "1px solid rgba(0,0,0,0.1)",
332
+ hoverBg: "#f5f5f5"
333
+ },
334
+ outline: {
335
+ background: "transparent",
336
+ color: "#d4a574",
337
+ border: "1px solid #d4a574",
338
+ hoverBg: "rgba(212,165,116,0.1)"
339
+ }
340
+ };
341
+ function NexusLoginButton({
342
+ label = "Sign in with Nexus",
343
+ size = "md",
344
+ variant = "dark",
345
+ className,
346
+ style: customStyle
347
+ }) {
348
+ const context = useContext2(NexusContext);
349
+ const s = sizes[size];
350
+ const v = variants[variant];
351
+ const handleClick = () => {
352
+ context?.login();
353
+ };
354
+ return /* @__PURE__ */ jsxs2(
355
+ "button",
356
+ {
357
+ onClick: handleClick,
358
+ className,
359
+ style: {
360
+ display: "inline-flex",
361
+ alignItems: "center",
362
+ justifyContent: "center",
363
+ gap: `${s.gap}px`,
364
+ padding: s.padding,
365
+ fontSize: s.fontSize,
366
+ fontFamily: "'JetBrains Mono', 'SF Mono', 'Fira Code', monospace",
367
+ fontWeight: 500,
368
+ letterSpacing: "0.02em",
369
+ background: v.background,
370
+ color: v.color,
371
+ border: v.border,
372
+ borderRadius: "12px",
373
+ cursor: "pointer",
374
+ transition: "all 0.2s ease",
375
+ textDecoration: "none",
376
+ whiteSpace: "nowrap",
377
+ ...customStyle
378
+ },
379
+ onMouseEnter: (e) => {
380
+ e.target.style.background = v.hoverBg;
381
+ e.target.style.transform = "translateY(-1px)";
382
+ },
383
+ onMouseLeave: (e) => {
384
+ e.target.style.background = v.background;
385
+ e.target.style.transform = "translateY(0)";
386
+ },
387
+ children: [
388
+ /* @__PURE__ */ jsxs2(
389
+ "svg",
390
+ {
391
+ width: s.iconSize,
392
+ height: s.iconSize,
393
+ viewBox: "0 0 24 24",
394
+ fill: "none",
395
+ stroke: "#d4a574",
396
+ strokeWidth: "2",
397
+ strokeLinecap: "round",
398
+ strokeLinejoin: "round",
399
+ children: [
400
+ /* @__PURE__ */ jsx3("path", { d: "M12 2L2 7l10 5 10-5-10-5z" }),
401
+ /* @__PURE__ */ jsx3("path", { d: "M2 17l10 5 10-5" }),
402
+ /* @__PURE__ */ jsx3("path", { d: "M2 12l10 5 10-5" })
403
+ ]
404
+ }
405
+ ),
406
+ label
407
+ ]
408
+ }
409
+ );
410
+ }
411
+
412
+ // src/useNexus.ts
413
+ import { useContext as useContext3 } from "react";
414
+ function useNexus() {
415
+ const context = useContext3(NexusContext);
416
+ if (!context) {
417
+ throw new Error("useNexus must be used within a <NexusProvider>");
418
+ }
419
+ return context;
420
+ }
421
+ export {
422
+ NexusCallback,
423
+ NexusLoginButton,
424
+ NexusProvider,
425
+ clearTokens,
426
+ decodeUser,
427
+ getAccessToken,
428
+ getRefreshToken,
429
+ isTokenValid,
430
+ nexusFetch,
431
+ refreshTokens,
432
+ setTokens,
433
+ useNexus
434
+ };
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@vylth/nexus-react",
3
+ "version": "1.0.0",
4
+ "description": "Nexus SSO SDK for React — Sign in with Nexus",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "files": ["dist"],
9
+ "scripts": {
10
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean",
11
+ "dev": "tsup src/index.ts --format cjs,esm --dts --watch"
12
+ },
13
+ "peerDependencies": {
14
+ "react": ">=18.0.0",
15
+ "react-dom": ">=18.0.0"
16
+ },
17
+ "devDependencies": {
18
+ "react": "^18.3.1",
19
+ "react-dom": "^18.3.1",
20
+ "@types/react": "^18.3.3",
21
+ "typescript": "^5.5.0",
22
+ "tsup": "^8.0.0"
23
+ },
24
+ "license": "MIT"
25
+ }