@vaultix.ai/react 0.3.2 → 0.3.4

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.js CHANGED
@@ -33,6 +33,7 @@ __export(index_exports, {
33
33
  getStoredToken: () => getStoredToken,
34
34
  useAuth: () => useAuth,
35
35
  useOrganization: () => useOrganization,
36
+ usePlatformConnect: () => usePlatformConnect,
36
37
  useSession: () => useSession,
37
38
  useUser: () => useUser,
38
39
  useVaultix: () => useVaultix
@@ -57,28 +58,35 @@ function resolveApiOrigin(key, apiUrlProp) {
57
58
  throw new Error(`Unknown publishable key prefix "${parts[0]}".`);
58
59
  }
59
60
  try {
60
- return atob(parts.slice(3).join("_")).replace(/\/$/, "");
61
+ return atob(parts.slice(3).join("_")).replace(/\|.+$/, "").replace(/\/$/, "");
61
62
  } catch {
62
63
  throw new Error("Publishable key has an unreadable API origin payload.");
63
64
  }
64
65
  }
65
- var STORAGE_KEY = "vaultix_jwt";
66
+ var TOKEN_COOKIE = "vaultix-token";
67
+ var COOKIE_DAYS = 30;
66
68
  function storeJwt(jwt) {
69
+ if (typeof document === "undefined") return;
67
70
  try {
68
- sessionStorage.setItem(STORAGE_KEY, jwt);
71
+ const expires = new Date(Date.now() + COOKIE_DAYS * 864e5).toUTCString();
72
+ const secure = location.protocol === "https:" ? "; Secure" : "";
73
+ document.cookie = `${TOKEN_COOKIE}=${encodeURIComponent(jwt)}; path=/; expires=${expires}; SameSite=Lax${secure}`;
69
74
  } catch {
70
75
  }
71
76
  }
72
77
  function loadJwt() {
78
+ if (typeof document === "undefined") return null;
73
79
  try {
74
- return sessionStorage.getItem(STORAGE_KEY);
80
+ const m = document.cookie.match(new RegExp(`(?:^|; )${TOKEN_COOKIE}=([^;]+)`));
81
+ return m?.[1] ? decodeURIComponent(m[1]) : null;
75
82
  } catch {
76
83
  return null;
77
84
  }
78
85
  }
79
86
  function clearJwt() {
87
+ if (typeof document === "undefined") return;
80
88
  try {
81
- sessionStorage.removeItem(STORAGE_KEY);
89
+ document.cookie = `${TOKEN_COOKIE}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax`;
82
90
  } catch {
83
91
  }
84
92
  }
@@ -149,26 +157,27 @@ function VaultixProvider({
149
157
  async function hydrate() {
150
158
  let jwt = null;
151
159
  if (typeof window !== "undefined") {
152
- const url = new URL(window.location.href);
153
- const handshakeToken = url.searchParams.get("__vaultix_handshake");
154
- if (handshakeToken) {
155
- url.searchParams.delete("__vaultix_handshake");
156
- window.history.replaceState({}, "", url.toString());
157
- try {
158
- const res = await fetch(`${apiOrigin}/api/v1/tokens/exchange`, {
159
- method: "POST",
160
- headers: { "Content-Type": "application/json" },
161
- body: JSON.stringify({ handshake_token: handshakeToken })
162
- });
163
- if (res.ok) {
164
- const data = await res.json();
165
- jwt = data.session_jwt;
166
- storeJwt(jwt);
160
+ jwt = loadJwt();
161
+ if (!jwt) {
162
+ const url = new URL(window.location.href);
163
+ const handshakeToken = url.searchParams.get("__vaultix_handshake");
164
+ if (handshakeToken) {
165
+ url.searchParams.delete("__vaultix_handshake");
166
+ window.history.replaceState({}, "", url.toString());
167
+ try {
168
+ const res = await fetch(`${apiOrigin}/api/v1/tokens/exchange`, {
169
+ method: "POST",
170
+ headers: { "Content-Type": "application/json" },
171
+ body: JSON.stringify({ handshake_token: handshakeToken })
172
+ });
173
+ if (res.ok) {
174
+ const data = await res.json();
175
+ jwt = data.session_jwt;
176
+ storeJwt(jwt);
177
+ }
178
+ } catch {
167
179
  }
168
- } catch {
169
180
  }
170
- } else {
171
- jwt = loadJwt();
172
181
  }
173
182
  jwtRef.current = jwt;
174
183
  }
@@ -209,7 +218,7 @@ function VaultixProvider({
209
218
  if (refreshTimerRef.current) clearTimeout(refreshTimerRef.current);
210
219
  };
211
220
  }, [apiOrigin, publishableKey, scheduleRefresh]);
212
- const signOut = (0, import_react.useCallback)(async () => {
221
+ const signOut = (0, import_react.useCallback)(async (redirectUrl) => {
213
222
  const jwt = jwtRef.current;
214
223
  clearAuth();
215
224
  if (refreshTimerRef.current) clearTimeout(refreshTimerRef.current);
@@ -221,8 +230,29 @@ function VaultixProvider({
221
230
  headers
222
231
  }).catch(() => {
223
232
  });
224
- if (afterSignInUrl) window.location.href = afterSignInUrl;
225
- }, [apiOrigin, afterSignInUrl, clearAuth]);
233
+ const dest = redirectUrl ?? afterSignUpUrl ?? "/";
234
+ if (typeof window !== "undefined") window.location.href = dest;
235
+ }, [apiOrigin, afterSignUpUrl, clearAuth]);
236
+ const updateUser = (0, import_react.useCallback)(async (params) => {
237
+ const userId = user?.id;
238
+ if (!userId) throw new Error("No active session");
239
+ const jwt = jwtRef.current;
240
+ const headers = { "Content-Type": "application/json" };
241
+ if (jwt) headers["Authorization"] = `Bearer ${jwt}`;
242
+ const res = await fetch(`${apiOrigin}/api/v1/users/${userId}`, {
243
+ method: "PATCH",
244
+ credentials: jwt ? "omit" : "include",
245
+ headers,
246
+ body: JSON.stringify(params)
247
+ });
248
+ if (!res.ok) {
249
+ const err = await res.json().catch(() => ({ error: "Update failed" }));
250
+ throw new Error(err.error ?? "Update failed");
251
+ }
252
+ const updated = await res.json();
253
+ setUser(updated);
254
+ return updated;
255
+ }, [apiOrigin, user?.id]);
226
256
  const connectPlatform = (0, import_react.useCallback)((provider, options = {}) => {
227
257
  const params = new URLSearchParams({ mode: "connect" });
228
258
  params.set("redirect_url", options.redirectUrl ?? (typeof window !== "undefined" ? window.location.href : "/"));
@@ -240,7 +270,9 @@ function VaultixProvider({
240
270
  organization,
241
271
  isLoaded,
242
272
  isSignedIn: !!session,
273
+ apiOrigin,
243
274
  signOut,
275
+ updateUser,
244
276
  connectPlatform
245
277
  };
246
278
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(VaultixContext.Provider, { value, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
@@ -266,19 +298,58 @@ function useSession() {
266
298
  return { session, isLoaded, isSignedIn };
267
299
  }
268
300
  function useUser() {
269
- const { user, isLoaded, isSignedIn } = useVaultixContext();
270
- return { user, isLoaded, isSignedIn };
301
+ const { user, isLoaded, isSignedIn, updateUser } = useVaultixContext();
302
+ return { user, isLoaded, isSignedIn, update: updateUser };
271
303
  }
272
304
  function useOrganization() {
273
305
  const { organization, isLoaded } = useVaultixContext();
274
306
  return { organization, isLoaded };
275
307
  }
308
+ function usePlatformConnect(provider, defaultOptions) {
309
+ const { connectPlatform } = useVaultixContext();
310
+ const [connectedAccount, setConnectedAccount] = (0, import_react2.useState)(null);
311
+ const connect = (0, import_react2.useCallback)(
312
+ (overrides) => {
313
+ connectPlatform(provider, { ...defaultOptions, ...overrides });
314
+ },
315
+ [connectPlatform, provider, defaultOptions]
316
+ );
317
+ (0, import_react2.useEffect)(() => {
318
+ if (typeof window === "undefined") return;
319
+ const params = new URLSearchParams(window.location.search);
320
+ const connectToken = params.get("__vaultix_connect");
321
+ const returnedProvider = params.get("provider");
322
+ if (!connectToken || returnedProvider !== provider) return;
323
+ try {
324
+ const segment = connectToken.split(".")[1];
325
+ if (!segment) return;
326
+ const b64 = segment.replace(/-/g, "+").replace(/_/g, "/");
327
+ const payload = JSON.parse(atob(b64));
328
+ const result = {
329
+ provider: returnedProvider,
330
+ raw: payload,
331
+ ...typeof payload["platform_user_id"] === "string" && { platformUserId: payload["platform_user_id"] },
332
+ ...typeof payload["username"] === "string" && { username: payload["username"] },
333
+ ...typeof payload["access_token"] === "string" && { accessToken: payload["access_token"] },
334
+ ...typeof payload["scope"] === "string" && { scope: payload["scope"] }
335
+ };
336
+ setConnectedAccount(result);
337
+ const clean = new URL(window.location.href);
338
+ clean.searchParams.delete("__vaultix_connect");
339
+ clean.searchParams.delete("provider");
340
+ window.history.replaceState({}, "", clean.toString());
341
+ } catch {
342
+ }
343
+ }, [provider]);
344
+ return { connect, connectedAccount };
345
+ }
276
346
  function useAuth() {
277
347
  const { user, session, isLoaded, isSignedIn, signOut } = useVaultixContext();
278
348
  const getToken = (0, import_react2.useCallback)(async () => {
279
349
  if (!isSignedIn) return null;
280
350
  try {
281
- return sessionStorage.getItem("vaultix_jwt");
351
+ const m = document.cookie.match(/(?:^|; )vaultix-token=([^;]+)/);
352
+ return m?.[1] ? decodeURIComponent(m[1]) : null;
282
353
  } catch {
283
354
  return null;
284
355
  }
@@ -333,9 +404,15 @@ function SignIn({
333
404
  onSuccess,
334
405
  onError,
335
406
  apiOrigin: apiOriginProp,
336
- className
407
+ className,
408
+ appearance
337
409
  }) {
338
410
  const apiOrigin = resolveApiOrigin2(apiOriginProp);
411
+ const vars = appearance?.variables ?? {};
412
+ const primaryColor = vars.colorPrimary;
413
+ const cardBg = vars.colorBackground ?? "rgba(22,27,45,0.92)";
414
+ const textColor = vars.colorText ?? "rgba(255,255,255,0.9)";
415
+ const mutedColor = vars.colorTextMuted ?? "#475569";
339
416
  const [step, setStep] = (0, import_react3.useState)("email");
340
417
  const [email, setEmail] = (0, import_react3.useState)("");
341
418
  const [password, setPassword] = (0, import_react3.useState)("");
@@ -392,6 +469,23 @@ function SignIn({
392
469
  setLoading(false);
393
470
  }
394
471
  }
472
+ async function handleMagicLinkRequest() {
473
+ setError(null);
474
+ setLoading(true);
475
+ try {
476
+ const target = resolveAfterSignInUrl(redirectUrl);
477
+ await fetch(`${apiOrigin}/api/v1/auth/magic-link/send`, {
478
+ method: "POST",
479
+ headers: { "Content-Type": "application/json" },
480
+ body: JSON.stringify({ email, redirect_url: target })
481
+ });
482
+ setStep("magic-link-sent");
483
+ } catch {
484
+ setErr("Network error. Please try again.");
485
+ } finally {
486
+ setLoading(false);
487
+ }
488
+ }
395
489
  async function handlePasswordSubmit(e) {
396
490
  e.preventDefault();
397
491
  setError(null);
@@ -560,6 +654,7 @@ function SignIn({
560
654
  password: "Enter password",
561
655
  passkey: "Passkey sign-in",
562
656
  totp: "Two-factor code",
657
+ "magic-link-sent": "Check your inbox",
563
658
  forgot: "Forgot password",
564
659
  "forgot-verify": "Enter reset code",
565
660
  "forgot-reset": "New password"
@@ -569,6 +664,7 @@ function SignIn({
569
664
  password: email,
570
665
  passkey: email,
571
666
  totp: "Enter the 6-digit code from your authenticator app",
667
+ "magic-link-sent": `We sent a sign-in link to ${email}`,
572
668
  forgot: "We'll send a reset code to your email",
573
669
  "forgot-verify": `Code sent to ${email}`,
574
670
  "forgot-reset": "Choose a new password"
@@ -581,12 +677,19 @@ function SignIn({
581
677
  "backdrop-blur-[16px]",
582
678
  className
583
679
  ),
584
- style: { background: "rgba(22,27,45,0.92)" },
680
+ style: { background: cardBg },
585
681
  children: [
586
682
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "px-8 pt-8 pb-6 text-center", children: [
587
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "inline-flex items-center justify-center w-10 h-10 rounded-xl bg-gradient-to-br from-purple-600 to-blue-600 shadow-lg shadow-purple-500/30 mb-4", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(LockIcon, {}) }),
588
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h1", { className: "text-xl font-semibold text-white/90", children: title[step] }),
589
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "text-sm text-[#475569] mt-1", children: subtitle[step] })
683
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
684
+ "div",
685
+ {
686
+ className: "inline-flex items-center justify-center w-10 h-10 rounded-xl shadow-lg mb-4",
687
+ style: primaryColor ? { background: primaryColor, boxShadow: `0 10px 15px -3px ${primaryColor}4d` } : { background: "linear-gradient(to bottom right, #7c3aed, #2563eb)", boxShadow: "0 10px 15px -3px rgba(124,58,237,0.3)" },
688
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(LockIcon, {})
689
+ }
690
+ ),
691
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h1", { className: "text-xl font-semibold", style: { color: textColor }, children: title[step] }),
692
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "text-sm mt-1", style: { color: mutedColor }, children: subtitle[step] })
590
693
  ] }),
591
694
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "px-8 pb-8 space-y-4", children: [
592
695
  error && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "rounded-lg bg-red-500/10 border border-red-500/30 px-3 py-2", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "text-xs text-red-400", children: error }) }),
@@ -597,7 +700,7 @@ function SignIn({
597
700
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(MetaButton, { onClick: handleMetaSignIn }),
598
701
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(LinkedInButton, { onClick: handleLinkedInSignIn }),
599
702
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(XButton, { onClick: handleXSignIn }),
600
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Divider, {}),
703
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Divider, { mutedColor }),
601
704
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("form", { onSubmit: handleEmailSubmit, className: "space-y-3", children: [
602
705
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
603
706
  Input,
@@ -607,10 +710,11 @@ function SignIn({
607
710
  value: email,
608
711
  onChange: setEmail,
609
712
  autoFocus: true,
610
- required: true
713
+ required: true,
714
+ primaryColor
611
715
  }
612
716
  ),
613
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(PrimaryButton, { loading, children: "Continue" })
717
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(PrimaryButton, { loading, primaryColor, children: "Continue" })
614
718
  ] })
615
719
  ] }),
616
720
  step === "password" && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("form", { onSubmit: handlePasswordSubmit, className: "space-y-3", children: [
@@ -622,23 +726,50 @@ function SignIn({
622
726
  value: password,
623
727
  onChange: setPassword,
624
728
  autoFocus: true,
625
- required: true
729
+ required: true,
730
+ primaryColor
626
731
  }
627
732
  ),
628
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(PrimaryButton, { loading, children: "Sign in" }),
733
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(PrimaryButton, { loading, primaryColor, children: "Sign in" }),
734
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex items-center gap-2", children: [
735
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "flex-1 h-px bg-white/8" }),
736
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "text-[10px]", style: { color: mutedColor }, children: "or" }),
737
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "flex-1 h-px bg-white/8" })
738
+ ] }),
739
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(GhostButton, { onClick: handleMagicLinkRequest, mutedColor, children: "\u2709 Email me a sign-in link" }),
629
740
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(GhostButton, { onClick: () => {
630
741
  setStep("forgot");
631
742
  setError(null);
632
- }, children: "Forgot password?" }),
633
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(GhostButton, { onClick: () => setStep("email"), children: "\u2190 Back" })
743
+ }, mutedColor, children: "Forgot password?" }),
744
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(GhostButton, { onClick: () => setStep("email"), mutedColor, children: "\u2190 Back" })
745
+ ] }),
746
+ step === "magic-link-sent" && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "space-y-4 text-center", children: [
747
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
748
+ "div",
749
+ {
750
+ className: "w-14 h-14 rounded-2xl flex items-center justify-center mx-auto border",
751
+ style: primaryColor ? { background: `${primaryColor}26`, borderColor: `${primaryColor}4d` } : { background: "rgba(168,85,247,0.15)", borderColor: "rgba(168,85,247,0.3)" },
752
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(EnvelopeIcon, { color: primaryColor ?? "#a78bfa" })
753
+ }
754
+ ),
755
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("p", { className: "text-sm leading-relaxed", style: { color: mutedColor }, children: [
756
+ "The link expires in ",
757
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { color: textColor }, className: "font-medium", children: "15 minutes" }),
758
+ " and can only be used once."
759
+ ] }),
760
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(GhostButton, { onClick: handleMagicLinkRequest, mutedColor, children: "Resend link" }),
761
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(GhostButton, { onClick: () => {
762
+ setStep("password");
763
+ setError(null);
764
+ }, mutedColor, children: "Use password instead" })
634
765
  ] }),
635
766
  step === "passkey" && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "space-y-3", children: [
636
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(PrimaryButton, { loading, onClick: handlePasskeySignIn, children: [
767
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(PrimaryButton, { loading, onClick: handlePasskeySignIn, primaryColor, children: [
637
768
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(FingerprintIcon, {}),
638
769
  "Authenticate with passkey"
639
770
  ] }),
640
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(GhostButton, { onClick: () => setStep("password"), children: "Use password instead" }),
641
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(GhostButton, { onClick: () => setStep("email"), children: "\u2190 Back" })
771
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(GhostButton, { onClick: () => setStep("password"), mutedColor, children: "Use password instead" }),
772
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(GhostButton, { onClick: () => setStep("email"), mutedColor, children: "\u2190 Back" })
642
773
  ] }),
643
774
  step === "totp" && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("form", { onSubmit: handleTotpSubmit, className: "space-y-3", children: [
644
775
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
@@ -653,11 +784,12 @@ function SignIn({
653
784
  onChange: setTotp,
654
785
  autoFocus: true,
655
786
  required: true,
656
- className: "text-center tracking-[0.4em] text-lg"
787
+ className: "text-center tracking-[0.4em] text-lg",
788
+ primaryColor
657
789
  }
658
790
  ),
659
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(PrimaryButton, { loading, children: "Verify" }),
660
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(GhostButton, { onClick: () => setStep("password"), children: "\u2190 Back" })
791
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(PrimaryButton, { loading, primaryColor, children: "Verify" }),
792
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(GhostButton, { onClick: () => setStep("password"), mutedColor, children: "\u2190 Back" })
661
793
  ] }),
662
794
  step === "forgot" && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("form", { onSubmit: handleForgotSubmit, className: "space-y-3", children: [
663
795
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
@@ -668,14 +800,15 @@ function SignIn({
668
800
  value: email,
669
801
  onChange: setEmail,
670
802
  autoFocus: true,
671
- required: true
803
+ required: true,
804
+ primaryColor
672
805
  }
673
806
  ),
674
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(PrimaryButton, { loading, children: "Send reset code" }),
807
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(PrimaryButton, { loading, primaryColor, children: "Send reset code" }),
675
808
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(GhostButton, { onClick: () => {
676
809
  setStep("password");
677
810
  setError(null);
678
- }, children: "\u2190 Back" })
811
+ }, mutedColor, children: "\u2190 Back" })
679
812
  ] }),
680
813
  step === "forgot-verify" && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("form", { onSubmit: handleForgotVerifySubmit, className: "space-y-3", children: [
681
814
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
@@ -690,10 +823,11 @@ function SignIn({
690
823
  onChange: setForgotCode,
691
824
  autoFocus: true,
692
825
  required: true,
693
- className: "text-center tracking-[0.4em] text-lg"
826
+ className: "text-center tracking-[0.4em] text-lg",
827
+ primaryColor
694
828
  }
695
829
  ),
696
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(PrimaryButton, { loading, children: "Continue" })
830
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(PrimaryButton, { loading, primaryColor, children: "Continue" })
697
831
  ] }),
698
832
  step === "forgot-reset" && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("form", { onSubmit: handleResetPasswordSubmit, className: "space-y-3", children: [
699
833
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
@@ -705,18 +839,19 @@ function SignIn({
705
839
  onChange: setNewPassword,
706
840
  autoFocus: true,
707
841
  required: true,
708
- minLength: 8
842
+ minLength: 8,
843
+ primaryColor
709
844
  }
710
845
  ),
711
846
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(PasswordStrength, { password: newPassword }),
712
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(PrimaryButton, { loading, children: "Reset password" })
847
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(PrimaryButton, { loading, primaryColor, children: "Reset password" })
713
848
  ] })
714
849
  ] })
715
850
  ]
716
851
  }
717
852
  );
718
853
  }
719
- function Input({ onChange, className, ...props }) {
854
+ function Input({ onChange, className, primaryColor, ...props }) {
720
855
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
721
856
  "input",
722
857
  {
@@ -725,39 +860,48 @@ function Input({ onChange, className, ...props }) {
725
860
  className: (0, import_clsx.clsx)(
726
861
  "w-full bg-white/5 border border-white/8 rounded-xl px-4 py-2.5",
727
862
  "text-sm text-white/90 placeholder:text-[#475569]",
728
- "focus:outline-none focus:border-purple-500/60 transition-colors",
863
+ "focus:outline-none transition-colors",
864
+ !primaryColor && "focus:border-purple-500/60",
729
865
  className
730
- )
866
+ ),
867
+ style: primaryColor ? { "--vx-focus": primaryColor } : void 0,
868
+ onFocus: (e) => {
869
+ if (primaryColor) e.currentTarget.style.borderColor = `${primaryColor}99`;
870
+ props.onFocus?.(e);
871
+ },
872
+ onBlur: (e) => {
873
+ if (primaryColor) e.currentTarget.style.borderColor = "";
874
+ props.onBlur?.(e);
875
+ }
731
876
  }
732
877
  );
733
878
  }
734
- function PrimaryButton({ children, loading, onClick }) {
879
+ function PrimaryButton({ children, loading, onClick, primaryColor }) {
735
880
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
736
881
  "button",
737
882
  {
738
883
  type: onClick ? "button" : "submit",
739
884
  onClick,
740
885
  disabled: loading,
886
+ style: primaryColor ? { background: primaryColor, boxShadow: `0 10px 15px -3px ${primaryColor}33` } : void 0,
741
887
  className: (0, import_clsx.clsx)(
742
888
  "w-full flex items-center justify-center gap-2 px-4 py-2.5 rounded-xl",
743
- "text-sm font-semibold text-white",
744
- "bg-gradient-to-r from-purple-600 to-blue-600",
745
- "hover:from-purple-500 hover:to-blue-500",
746
- "shadow-lg shadow-purple-500/20",
747
- "transition-all duration-150",
748
- "disabled:opacity-60 disabled:cursor-not-allowed"
889
+ "text-sm font-semibold text-white transition-all duration-150",
890
+ "disabled:opacity-60 disabled:cursor-not-allowed",
891
+ !primaryColor && "bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-500 hover:to-blue-500 shadow-lg shadow-purple-500/20"
749
892
  ),
750
893
  children: loading ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Spinner, {}) : children
751
894
  }
752
895
  );
753
896
  }
754
- function GhostButton({ children, onClick }) {
897
+ function GhostButton({ children, onClick, mutedColor }) {
755
898
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
756
899
  "button",
757
900
  {
758
901
  type: "button",
759
902
  onClick,
760
- className: "w-full text-xs text-[#475569] hover:text-white/70 transition-colors py-1",
903
+ className: "w-full text-xs transition-colors py-1 hover:opacity-80",
904
+ style: { color: mutedColor ?? "#475569" },
761
905
  children
762
906
  }
763
907
  );
@@ -801,10 +945,10 @@ function GitHubButton({ onClick }) {
801
945
  function XButton({ onClick }) {
802
946
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(OAuthButton, { icon: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(XIcon, {}), label: "Continue with X", onClick });
803
947
  }
804
- function Divider() {
948
+ function Divider({ mutedColor }) {
805
949
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex items-center gap-3", children: [
806
950
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "flex-1 h-px bg-white/8" }),
807
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "text-[10px] text-[#475569] uppercase tracking-wider", children: "or" }),
951
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "text-[10px] uppercase tracking-wider", style: { color: mutedColor ?? "#475569" }, children: "or" }),
808
952
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "flex-1 h-px bg-white/8" })
809
953
  ] });
810
954
  }
@@ -874,6 +1018,12 @@ function GitHubIcon() {
874
1018
  function XIcon() {
875
1019
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "white", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.744l7.737-8.835L1.254 2.25H8.08l4.253 5.622 5.911-5.622zm-1.161 17.52h1.833L7.084 4.126H5.117z" }) });
876
1020
  }
1021
+ function EnvelopeIcon({ color = "#a78bfa" }) {
1022
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: color, strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", children: [
1023
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("rect", { x: "2", y: "4", width: "20", height: "16", rx: "2" }),
1024
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" })
1025
+ ] });
1026
+ }
877
1027
 
878
1028
  // src/components/SignUp.tsx
879
1029
  var import_clsx2 = require("clsx");
@@ -887,6 +1037,13 @@ function resolveApiOrigin3(prop) {
887
1037
  }
888
1038
  return "";
889
1039
  }
1040
+ function resolvePk2() {
1041
+ if (typeof document !== "undefined") {
1042
+ const el = document.querySelector("[data-vaultix-pk]");
1043
+ if (el) return el.getAttribute("data-vaultix-pk") ?? "";
1044
+ }
1045
+ return "";
1046
+ }
890
1047
  function resolveAfterSignUpUrl(redirectUrlProp) {
891
1048
  if (redirectUrlProp) return redirectUrlProp;
892
1049
  if (typeof document !== "undefined") {
@@ -904,9 +1061,15 @@ function SignUp({
904
1061
  onSuccess,
905
1062
  onError,
906
1063
  apiOrigin: apiOriginProp,
907
- className
1064
+ className,
1065
+ appearance
908
1066
  }) {
909
1067
  const apiOrigin = resolveApiOrigin3(apiOriginProp);
1068
+ const vars = appearance?.variables ?? {};
1069
+ const primaryColor = vars.colorPrimary;
1070
+ const cardBg = vars.colorBackground ?? "rgba(22,27,45,0.92)";
1071
+ const textColor = vars.colorText ?? "rgba(255,255,255,0.9)";
1072
+ const mutedColor = vars.colorTextMuted ?? "#475569";
910
1073
  const [step, setStep] = (0, import_react4.useState)("email");
911
1074
  const [email, setEmail] = (0, import_react4.useState)("");
912
1075
  const [password, setPassword] = (0, import_react4.useState)("");
@@ -925,10 +1088,12 @@ function SignUp({
925
1088
  url.searchParams.set("__vaultix_handshake", handshakeToken);
926
1089
  window.location.href = url.toString();
927
1090
  }
928
- function handleGoogleSignUp() {
1091
+ function oauthRedirect(provider) {
929
1092
  const target = resolveAfterSignUpUrl(redirectUrl);
1093
+ const pk = resolvePk2();
930
1094
  const params = new URLSearchParams({ redirect_url: target });
931
- window.location.href = `${apiOrigin}/api/v1/auth/oauth/google?${params}`;
1095
+ if (pk) params.set("pk", pk);
1096
+ window.location.href = `${apiOrigin}/api/v1/auth/oauth/${provider}?${params}`;
932
1097
  }
933
1098
  async function handleEmailPassword(e) {
934
1099
  e.preventDefault();
@@ -999,48 +1164,61 @@ function SignUp({
999
1164
  "w-full max-w-sm mx-auto rounded-2xl border border-white/8 shadow-2xl backdrop-blur-[16px]",
1000
1165
  className
1001
1166
  ),
1002
- style: { background: "rgba(22,27,45,0.92)" },
1167
+ style: { background: cardBg },
1003
1168
  children: [
1004
1169
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "px-8 pt-8 pb-6 text-center", children: [
1005
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "inline-flex items-center justify-center w-10 h-10 rounded-xl bg-gradient-to-br from-emerald-600 to-teal-600 shadow-lg shadow-emerald-500/30 mb-4", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SparkleIcon, {}) }),
1006
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h1", { className: "text-xl font-semibold text-white/90", children: step === "verify" ? "Check your email" : "Create an account" }),
1007
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "text-sm text-[#475569] mt-1", children: step === "verify" ? `We sent a code to ${email}` : "Start your free trial today" })
1170
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1171
+ "div",
1172
+ {
1173
+ className: "inline-flex items-center justify-center w-10 h-10 rounded-xl shadow-lg mb-4",
1174
+ style: primaryColor ? { background: primaryColor, boxShadow: `0 10px 15px -3px ${primaryColor}4d` } : { background: "linear-gradient(to bottom right, #059669, #0d9488)", boxShadow: "0 10px 15px -3px rgba(5,150,105,0.3)" },
1175
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SparkleIcon, {})
1176
+ }
1177
+ ),
1178
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h1", { className: "text-xl font-semibold", style: { color: textColor }, children: step === "verify" ? "Check your email" : "Create an account" }),
1179
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "text-sm mt-1", style: { color: mutedColor }, children: step === "verify" ? `We sent a code to ${email}` : "Start your free trial today" })
1008
1180
  ] }),
1009
1181
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "px-8 pb-8 space-y-4", children: [
1010
1182
  error && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "rounded-lg bg-red-500/10 border border-red-500/30 px-3 py-2", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "text-xs text-red-400", children: error }) }),
1011
1183
  step === "email" && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "space-y-3", children: [
1012
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(GoogleButton2, { onClick: handleGoogleSignUp }),
1013
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Divider2, {}),
1184
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(OAuthButton2, { icon: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(GoogleIcon2, {}), label: "Continue with Google", onClick: () => oauthRedirect("google") }),
1185
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(OAuthButton2, { icon: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(GitHubIcon2, {}), label: "Continue with GitHub", onClick: () => oauthRedirect("github") }),
1186
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(OAuthButton2, { icon: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MetaIcon2, {}), label: "Continue with Facebook", onClick: () => oauthRedirect("meta") }),
1187
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(OAuthButton2, { icon: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(LinkedInIcon2, {}), label: "Continue with LinkedIn", onClick: () => oauthRedirect("linkedin") }),
1188
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(OAuthButton2, { icon: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(XIcon2, {}), label: "Continue with X", onClick: () => oauthRedirect("x") }),
1189
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Divider2, { mutedColor }),
1014
1190
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("form", { onSubmit: handleEmailPassword, className: "space-y-3", children: [
1015
1191
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1016
- SignUpInput,
1192
+ Input2,
1017
1193
  {
1018
1194
  type: "email",
1019
1195
  placeholder: "you@company.com",
1020
1196
  value: email,
1021
1197
  onChange: setEmail,
1022
1198
  autoFocus: true,
1023
- required: true
1199
+ required: true,
1200
+ primaryColor
1024
1201
  }
1025
1202
  ),
1026
1203
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1027
- SignUpInput,
1204
+ Input2,
1028
1205
  {
1029
1206
  type: "password",
1030
1207
  placeholder: "Create a password",
1031
1208
  value: password,
1032
1209
  onChange: setPassword,
1033
1210
  required: true,
1034
- minLength: 8
1211
+ minLength: 8,
1212
+ primaryColor
1035
1213
  }
1036
1214
  ),
1037
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PasswordStrength2, { password }),
1038
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SignUpPrimaryButton, { loading, children: "Create account" })
1215
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PasswordStrength2, { password, primaryColor }),
1216
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PrimaryButton2, { loading, primaryColor, children: "Create account" })
1039
1217
  ] })
1040
1218
  ] }),
1041
1219
  step === "verify" && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("form", { onSubmit: handleVerification, className: "space-y-3", children: [
1042
1220
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1043
- SignUpInput,
1221
+ Input2,
1044
1222
  {
1045
1223
  type: "text",
1046
1224
  inputMode: "numeric",
@@ -1051,16 +1229,18 @@ function SignUp({
1051
1229
  onChange: setVerificationCode,
1052
1230
  autoFocus: true,
1053
1231
  required: true,
1054
- className: "text-center tracking-[0.4em] text-lg"
1232
+ className: "text-center tracking-[0.4em] text-lg",
1233
+ primaryColor
1055
1234
  }
1056
1235
  ),
1057
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SignUpPrimaryButton, { loading, children: "Verify email" }),
1236
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PrimaryButton2, { loading, primaryColor, children: "Verify email" }),
1058
1237
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1059
1238
  "button",
1060
1239
  {
1061
1240
  type: "button",
1062
1241
  onClick: resendCode,
1063
- className: "w-full text-xs text-[#475569] hover:text-white/70 transition-colors py-1",
1242
+ className: "w-full text-xs transition-colors py-1 hover:opacity-80",
1243
+ style: { color: mutedColor },
1064
1244
  children: "Resend code"
1065
1245
  }
1066
1246
  )
@@ -1070,7 +1250,7 @@ function SignUp({
1070
1250
  }
1071
1251
  );
1072
1252
  }
1073
- function SignUpInput({ onChange, className, ...props }) {
1253
+ function Input2({ onChange, className, primaryColor, ...props }) {
1074
1254
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1075
1255
  "input",
1076
1256
  {
@@ -1079,34 +1259,47 @@ function SignUpInput({ onChange, className, ...props }) {
1079
1259
  className: (0, import_clsx2.clsx)(
1080
1260
  "w-full bg-white/5 border border-white/8 rounded-xl px-4 py-2.5",
1081
1261
  "text-sm text-white/90 placeholder:text-[#475569]",
1082
- "focus:outline-none focus:border-emerald-500/60 transition-colors",
1262
+ "focus:outline-none transition-colors",
1263
+ !primaryColor && "focus:border-emerald-500/60",
1083
1264
  className
1084
- )
1265
+ ),
1266
+ onFocus: (e) => {
1267
+ if (primaryColor) e.currentTarget.style.borderColor = `${primaryColor}99`;
1268
+ props.onFocus?.(e);
1269
+ },
1270
+ onBlur: (e) => {
1271
+ if (primaryColor) e.currentTarget.style.borderColor = "";
1272
+ props.onBlur?.(e);
1273
+ }
1085
1274
  }
1086
1275
  );
1087
1276
  }
1088
- function SignUpPrimaryButton({
1277
+ function PrimaryButton2({
1089
1278
  children,
1090
- loading
1279
+ loading,
1280
+ primaryColor
1091
1281
  }) {
1092
1282
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1093
1283
  "button",
1094
1284
  {
1095
1285
  type: "submit",
1096
1286
  disabled: loading,
1287
+ style: primaryColor ? { background: primaryColor, boxShadow: `0 10px 15px -3px ${primaryColor}33` } : void 0,
1097
1288
  className: (0, import_clsx2.clsx)(
1098
1289
  "w-full flex items-center justify-center gap-2 px-4 py-2.5 rounded-xl",
1099
- "text-sm font-semibold text-white",
1100
- "bg-gradient-to-r from-emerald-600 to-teal-600",
1101
- "hover:from-emerald-500 hover:to-teal-500",
1102
- "shadow-lg shadow-emerald-500/20 transition-all duration-150",
1103
- "disabled:opacity-60 disabled:cursor-not-allowed"
1290
+ "text-sm font-semibold text-white transition-all duration-150",
1291
+ "disabled:opacity-60 disabled:cursor-not-allowed",
1292
+ !primaryColor && "bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-500 hover:to-teal-500 shadow-lg shadow-emerald-500/20"
1104
1293
  ),
1105
1294
  children: loading ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Spinner2, {}) : children
1106
1295
  }
1107
1296
  );
1108
1297
  }
1109
- function GoogleButton2({ onClick }) {
1298
+ function OAuthButton2({
1299
+ icon,
1300
+ label,
1301
+ onClick
1302
+ }) {
1110
1303
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1111
1304
  "button",
1112
1305
  {
@@ -1120,20 +1313,20 @@ function GoogleButton2({ onClick }) {
1120
1313
  "transition-all duration-150"
1121
1314
  ),
1122
1315
  children: [
1123
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(GoogleIcon2, {}),
1124
- "Continue with Google"
1316
+ icon,
1317
+ label
1125
1318
  ]
1126
1319
  }
1127
1320
  );
1128
1321
  }
1129
- function Divider2() {
1322
+ function Divider2({ mutedColor }) {
1130
1323
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex items-center gap-3", children: [
1131
1324
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "flex-1 h-px bg-white/8" }),
1132
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "text-[10px] text-[#475569] uppercase tracking-wider", children: "or" }),
1325
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "text-[10px] uppercase tracking-wider", style: { color: mutedColor ?? "#475569" }, children: "or" }),
1133
1326
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "flex-1 h-px bg-white/8" })
1134
1327
  ] });
1135
1328
  }
1136
- function PasswordStrength2({ password }) {
1329
+ function PasswordStrength2({ password, primaryColor }) {
1137
1330
  const score = (() => {
1138
1331
  if (password.length === 0) return 0;
1139
1332
  let s = 0;
@@ -1145,15 +1338,17 @@ function PasswordStrength2({ password }) {
1145
1338
  })();
1146
1339
  if (password.length === 0) return null;
1147
1340
  const labels = ["", "Weak", "Fair", "Good", "Strong"];
1148
- const colors = ["", "bg-red-500", "bg-amber-400", "bg-blue-400", "bg-emerald-400"];
1341
+ const tailwindColors = ["", "bg-red-500", "bg-amber-400", "bg-blue-400", "bg-emerald-400"];
1149
1342
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "space-y-1", children: [
1150
1343
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "flex gap-1", children: [1, 2, 3, 4].map((i) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1151
1344
  "div",
1152
1345
  {
1153
1346
  className: (0, import_clsx2.clsx)(
1154
1347
  "flex-1 h-0.5 rounded-full transition-all duration-300",
1155
- i <= score ? colors[score] : "bg-white/10"
1156
- )
1348
+ i <= score && !primaryColor ? tailwindColors[score] : void 0,
1349
+ i > score || !primaryColor && i <= score ? void 0 : void 0
1350
+ ),
1351
+ style: i <= score && primaryColor ? { background: score >= 4 ? primaryColor : void 0 } : { background: i <= score ? void 0 : "rgba(255,255,255,0.1)" }
1157
1352
  },
1158
1353
  i
1159
1354
  )) }),
@@ -1177,6 +1372,18 @@ function GoogleIcon2() {
1177
1372
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z", fill: "#EA4335" })
1178
1373
  ] });
1179
1374
  }
1375
+ function MetaIcon2() {
1376
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "#1877F2", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z" }) });
1377
+ }
1378
+ function LinkedInIcon2() {
1379
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "#0A66C2", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.063 2.063 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" }) });
1380
+ }
1381
+ function GitHubIcon2() {
1382
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "white", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0 1 12 6.844a9.59 9.59 0 0 1 2.504.337c1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.02 10.02 0 0 0 22 12.017C22 6.484 17.522 2 12 2z" }) });
1383
+ }
1384
+ function XIcon2() {
1385
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "white", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.744l7.737-8.835L1.254 2.25H8.08l4.253 5.622 5.911-5.622zm-1.161 17.52h1.833L7.084 4.126H5.117z" }) });
1386
+ }
1180
1387
 
1181
1388
  // src/components/SignedIn.tsx
1182
1389
  var import_jsx_runtime4 = require("react/jsx-runtime");
@@ -1216,20 +1423,15 @@ function RedirectToSignUp({ redirectUrl = "/sign-up" }) {
1216
1423
  var import_clsx3 = require("clsx");
1217
1424
  var import_react6 = require("react");
1218
1425
  var import_jsx_runtime5 = require("react/jsx-runtime");
1219
- function UserButton({
1220
- showName = false,
1221
- afterSignOutUrl,
1222
- className
1223
- }) {
1426
+ function UserButton({ showName = false, afterSignOutUrl, className }) {
1224
1427
  const { user, session, isLoaded, isSignedIn, signOut } = useVaultixContext();
1225
1428
  const [open, setOpen] = (0, import_react6.useState)(false);
1226
1429
  const [signingOut, setSigningOut] = (0, import_react6.useState)(false);
1430
+ const [showProfile, setShowProfile] = (0, import_react6.useState)(false);
1227
1431
  const containerRef = (0, import_react6.useRef)(null);
1228
1432
  (0, import_react6.useEffect)(() => {
1229
1433
  function onPointerDown(e) {
1230
- if (containerRef.current && !containerRef.current.contains(e.target)) {
1231
- setOpen(false);
1232
- }
1434
+ if (containerRef.current && !containerRef.current.contains(e.target)) setOpen(false);
1233
1435
  }
1234
1436
  document.addEventListener("pointerdown", onPointerDown);
1235
1437
  return () => document.removeEventListener("pointerdown", onPointerDown);
@@ -1242,98 +1444,456 @@ function UserButton({
1242
1444
  await signOut();
1243
1445
  if (afterSignOutUrl) window.location.href = afterSignOutUrl;
1244
1446
  }
1245
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { ref: containerRef, className: (0, import_clsx3.clsx)("relative", className), children: [
1246
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
1447
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
1448
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { ref: containerRef, className: (0, import_clsx3.clsx)("relative", className), children: [
1449
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
1450
+ "button",
1451
+ {
1452
+ onClick: () => setOpen((o) => !o),
1453
+ className: "flex items-center gap-2 rounded-xl p-1 hover:bg-white/8 transition-colors",
1454
+ children: [
1455
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Avatar, { initials, imageUrl: user.imageUrl }),
1456
+ showName && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "text-sm font-medium text-white/90 pr-1", children: user.firstName ?? user.email })
1457
+ ]
1458
+ }
1459
+ ),
1460
+ open && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
1461
+ "div",
1462
+ {
1463
+ className: (0, import_clsx3.clsx)("absolute right-0 mt-2 w-64 rounded-2xl border border-white/8", "backdrop-blur-[16px] shadow-2xl shadow-black/40 z-50"),
1464
+ style: { background: "rgba(22,27,45,0.96)" },
1465
+ children: [
1466
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex items-center gap-3 px-4 py-3 border-b border-white/8", children: [
1467
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Avatar, { initials, imageUrl: user.imageUrl, size: "lg" }),
1468
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex-1 min-w-0", children: [
1469
+ (user.firstName || user.lastName) && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "text-sm font-semibold text-white/90 truncate", children: [user.firstName, user.lastName].filter(Boolean).join(" ") }),
1470
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "text-xs text-[#475569] truncate", children: user.email })
1471
+ ] })
1472
+ ] }),
1473
+ session && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "px-4 py-2 border-b border-white/8", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex items-center justify-between", children: [
1474
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "text-[10px] uppercase tracking-widest text-[#475569]", children: "Risk level" }),
1475
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(RiskBadge, { level: session.riskLevel })
1476
+ ] }) }),
1477
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "p-2", children: [
1478
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(DropdownItem, { label: "Manage account", icon: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(UserIcon, {}), onClick: () => {
1479
+ setOpen(false);
1480
+ setShowProfile(true);
1481
+ } }),
1482
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "h-px bg-white/8 my-1" }),
1483
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(DropdownItem, { label: signingOut ? "Signing out\u2026" : "Sign out", icon: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(SignOutIcon, {}), onClick: handleSignOut, destructive: true })
1484
+ ] })
1485
+ ]
1486
+ }
1487
+ )
1488
+ ] }),
1489
+ showProfile && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ProfileModal, { onClose: () => setShowProfile(false) })
1490
+ ] });
1491
+ }
1492
+ function ProfileModal({ onClose }) {
1493
+ const { user, updateUser } = useVaultixContext();
1494
+ const [tab, setTab] = (0, import_react6.useState)("profile");
1495
+ const [firstName, setFirstName] = (0, import_react6.useState)(user?.firstName ?? "");
1496
+ const [lastName, setLastName] = (0, import_react6.useState)(user?.lastName ?? "");
1497
+ const [imageUrl, setImageUrl] = (0, import_react6.useState)(user?.imageUrl ?? "");
1498
+ const [currentPassword, setCurrentPassword] = (0, import_react6.useState)("");
1499
+ const [newPassword, setNewPassword] = (0, import_react6.useState)("");
1500
+ const [confirmPassword, setConfirmPassword] = (0, import_react6.useState)("");
1501
+ const [saving, setSaving] = (0, import_react6.useState)(false);
1502
+ const [error, setError] = (0, import_react6.useState)(null);
1503
+ const [success, setSuccess] = (0, import_react6.useState)(null);
1504
+ (0, import_react6.useEffect)(() => {
1505
+ function onKey(e) {
1506
+ if (e.key === "Escape") onClose();
1507
+ }
1508
+ document.addEventListener("keydown", onKey);
1509
+ return () => document.removeEventListener("keydown", onKey);
1510
+ }, [onClose]);
1511
+ async function saveProfile(e) {
1512
+ e.preventDefault();
1513
+ setError(null);
1514
+ setSuccess(null);
1515
+ setSaving(true);
1516
+ try {
1517
+ await updateUser({ firstName: firstName.trim() || null, lastName: lastName.trim() || null, imageUrl: imageUrl.trim() || null });
1518
+ setSuccess("Profile updated.");
1519
+ } catch (err) {
1520
+ setError(err instanceof Error ? err.message : "Update failed");
1521
+ } finally {
1522
+ setSaving(false);
1523
+ }
1524
+ }
1525
+ async function savePassword(e) {
1526
+ e.preventDefault();
1527
+ setError(null);
1528
+ setSuccess(null);
1529
+ if (newPassword !== confirmPassword) {
1530
+ setError("Passwords do not match.");
1531
+ return;
1532
+ }
1533
+ if (newPassword.length < 8) {
1534
+ setError("Password must be at least 8 characters.");
1535
+ return;
1536
+ }
1537
+ setSaving(true);
1538
+ try {
1539
+ await updateUser({ currentPassword, newPassword });
1540
+ setSuccess("Password changed.");
1541
+ setCurrentPassword("");
1542
+ setNewPassword("");
1543
+ setConfirmPassword("");
1544
+ } catch (err) {
1545
+ setError(err instanceof Error ? err.message : "Update failed");
1546
+ } finally {
1547
+ setSaving(false);
1548
+ }
1549
+ }
1550
+ const TABS = [
1551
+ { id: "profile", label: "Profile" },
1552
+ { id: "password", label: "Change password" },
1553
+ { id: "security", label: "Security" }
1554
+ ];
1555
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1556
+ "div",
1557
+ {
1558
+ className: "fixed inset-0 z-[9999] flex items-center justify-center p-4",
1559
+ style: { background: "rgba(0,0,0,0.6)", backdropFilter: "blur(4px)" },
1560
+ onPointerDown: (e) => {
1561
+ if (e.target === e.currentTarget) onClose();
1562
+ },
1563
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "w-full max-w-md rounded-2xl border border-white/8 shadow-2xl", style: { background: "rgba(22,27,45,0.98)" }, children: [
1564
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex items-center justify-between px-6 py-4 border-b border-white/8", children: [
1565
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("h2", { className: "text-sm font-semibold text-white/90", children: "Manage account" }),
1566
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { onClick: onClose, className: "text-[#475569] hover:text-white transition-colors", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(CloseIcon, {}) })
1567
+ ] }),
1568
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "flex gap-1 px-6 pt-4 flex-wrap", children: TABS.map(({ id, label }) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1569
+ "button",
1570
+ {
1571
+ onClick: () => {
1572
+ setTab(id);
1573
+ setError(null);
1574
+ setSuccess(null);
1575
+ },
1576
+ className: (0, import_clsx3.clsx)(
1577
+ "px-3 py-1.5 rounded-lg text-xs font-medium transition-colors",
1578
+ tab === id ? "bg-purple-500/15 text-purple-300 border border-purple-500/30" : "text-[#475569] hover:text-white/70"
1579
+ ),
1580
+ children: label
1581
+ },
1582
+ id
1583
+ )) }),
1584
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "px-6 pt-3", children: [
1585
+ error && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "rounded-lg bg-red-500/10 border border-red-500/30 px-3 py-2 text-xs text-red-400", children: error }),
1586
+ success && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "rounded-lg bg-emerald-500/10 border border-emerald-500/30 px-3 py-2 text-xs text-emerald-400", children: success })
1587
+ ] }),
1588
+ tab === "profile" && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("form", { onSubmit: saveProfile, className: "px-6 py-4 space-y-4", children: [
1589
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "grid grid-cols-2 gap-3", children: [
1590
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ModalField, { label: "First name", value: firstName, onChange: setFirstName, placeholder: "Jane" }),
1591
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ModalField, { label: "Last name", value: lastName, onChange: setLastName, placeholder: "Smith" })
1592
+ ] }),
1593
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ModalField, { label: "Avatar URL", value: imageUrl, onChange: setImageUrl, placeholder: "https://\u2026", type: "url" }),
1594
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex items-center gap-3 pt-1", children: [
1595
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(SaveButton, { saving }),
1596
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", onClick: onClose, className: "text-sm text-[#475569] hover:text-white/70 transition-colors", children: "Cancel" })
1597
+ ] })
1598
+ ] }),
1599
+ tab === "password" && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("form", { onSubmit: savePassword, className: "px-6 py-4 space-y-4", children: [
1600
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ModalField, { label: "Current password", value: currentPassword, onChange: setCurrentPassword, type: "password", placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" }),
1601
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ModalField, { label: "New password", value: newPassword, onChange: setNewPassword, type: "password", placeholder: "Min 8 characters" }),
1602
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ModalField, { label: "Confirm new password", value: confirmPassword, onChange: setConfirmPassword, type: "password", placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" }),
1603
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex items-center gap-3 pt-1", children: [
1604
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(SaveButton, { saving, label: "Change password" }),
1605
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", onClick: onClose, className: "text-sm text-[#475569] hover:text-white/70 transition-colors", children: "Cancel" })
1606
+ ] })
1607
+ ] }),
1608
+ tab === "security" && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(SecurityTab, {})
1609
+ ] })
1610
+ }
1611
+ );
1612
+ }
1613
+ function SecurityTab() {
1614
+ const { apiOrigin } = useVaultixContext();
1615
+ const [totpEnabled, setTotpEnabled] = (0, import_react6.useState)(null);
1616
+ const [backupCodesLeft, setBackupCodesLeft] = (0, import_react6.useState)(null);
1617
+ const [enrollStep, setEnrollStep] = (0, import_react6.useState)("idle");
1618
+ const [qrDataUrl, setQrDataUrl] = (0, import_react6.useState)("");
1619
+ const [manualKey, setManualKey] = (0, import_react6.useState)("");
1620
+ const [enrollCode, setEnrollCode] = (0, import_react6.useState)("");
1621
+ const [disableCode, setDisableCode] = (0, import_react6.useState)("");
1622
+ const [backupCodes, setBackupCodes] = (0, import_react6.useState)([]);
1623
+ const [loading, setLoading] = (0, import_react6.useState)(false);
1624
+ const [error, setError] = (0, import_react6.useState)(null);
1625
+ const [showDisable, setShowDisable] = (0, import_react6.useState)(false);
1626
+ const fetchStatus = (0, import_react6.useCallback)(async () => {
1627
+ const res = await fetch(`${apiOrigin}/api/v1/auth/totp/status`, { credentials: "include" });
1628
+ if (res.ok) {
1629
+ const d = await res.json();
1630
+ setTotpEnabled(d.enabled);
1631
+ setBackupCodesLeft(d.backup_codes_remaining);
1632
+ }
1633
+ }, [apiOrigin]);
1634
+ (0, import_react6.useEffect)(() => {
1635
+ void fetchStatus();
1636
+ }, [fetchStatus]);
1637
+ async function startEnroll() {
1638
+ setError(null);
1639
+ setLoading(true);
1640
+ try {
1641
+ const res = await fetch(`${apiOrigin}/api/v1/auth/totp/enroll`, { method: "POST", credentials: "include" });
1642
+ const d = await res.json();
1643
+ if (!res.ok) {
1644
+ setError(d.error ?? "Failed to start enrollment.");
1645
+ return;
1646
+ }
1647
+ setQrDataUrl(d.qr_data_url);
1648
+ setManualKey(d.manual_entry_key);
1649
+ setEnrollStep("qr");
1650
+ } finally {
1651
+ setLoading(false);
1652
+ }
1653
+ }
1654
+ async function verifyEnroll(e) {
1655
+ e.preventDefault();
1656
+ setError(null);
1657
+ setLoading(true);
1658
+ try {
1659
+ const res = await fetch(`${apiOrigin}/api/v1/auth/totp/verify-enroll`, {
1660
+ method: "POST",
1661
+ credentials: "include",
1662
+ headers: { "Content-Type": "application/json" },
1663
+ body: JSON.stringify({ code: enrollCode })
1664
+ });
1665
+ const d = await res.json();
1666
+ if (!res.ok) {
1667
+ setError(d.error ?? "Invalid code. Try again.");
1668
+ return;
1669
+ }
1670
+ setBackupCodes(d.backup_codes);
1671
+ setEnrollStep("backup-codes");
1672
+ setTotpEnabled(true);
1673
+ setBackupCodesLeft(d.backup_codes.length);
1674
+ } finally {
1675
+ setLoading(false);
1676
+ }
1677
+ }
1678
+ async function disableTotp(e) {
1679
+ e.preventDefault();
1680
+ setError(null);
1681
+ setLoading(true);
1682
+ try {
1683
+ const res = await fetch(`${apiOrigin}/api/v1/auth/totp`, {
1684
+ method: "DELETE",
1685
+ credentials: "include",
1686
+ headers: { "Content-Type": "application/json" },
1687
+ body: JSON.stringify({ code: disableCode })
1688
+ });
1689
+ const d = await res.json();
1690
+ if (!res.ok) {
1691
+ setError(d.error ?? "Invalid code.");
1692
+ return;
1693
+ }
1694
+ setTotpEnabled(false);
1695
+ setBackupCodesLeft(null);
1696
+ setShowDisable(false);
1697
+ setDisableCode("");
1698
+ } finally {
1699
+ setLoading(false);
1700
+ }
1701
+ }
1702
+ if (totpEnabled === null) {
1703
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "px-6 py-8 text-center text-[#475569] text-sm", children: "Loading\u2026" });
1704
+ }
1705
+ if (enrollStep === "backup-codes") {
1706
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "px-6 py-4 space-y-4", children: [
1707
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "rounded-xl bg-emerald-500/10 border border-emerald-500/30 px-4 py-3", children: [
1708
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "text-xs font-semibold text-emerald-400 mb-1", children: "Authenticator app enabled!" }),
1709
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "text-[11px] text-emerald-300/70", children: "Save these backup codes somewhere safe. Each can be used once if you lose your phone." })
1710
+ ] }),
1711
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "grid grid-cols-2 gap-1.5", children: backupCodes.map((c) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "font-mono text-xs bg-white/5 border border-white/8 rounded-lg px-3 py-1.5 text-white/80 text-center", children: c }, c)) }),
1712
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1713
+ "button",
1714
+ {
1715
+ onClick: () => {
1716
+ const t = backupCodes.join("\n");
1717
+ navigator.clipboard.writeText(t).catch(() => {
1718
+ });
1719
+ },
1720
+ className: "w-full text-xs text-[#475569] hover:text-white/70 transition-colors py-1 border border-white/8 rounded-lg",
1721
+ children: "Copy all codes"
1722
+ }
1723
+ ),
1724
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1725
+ "button",
1726
+ {
1727
+ onClick: () => setEnrollStep("idle"),
1728
+ className: "w-full py-2 rounded-xl text-xs font-semibold text-white bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-500 hover:to-blue-500 transition-all",
1729
+ children: "Done"
1730
+ }
1731
+ )
1732
+ ] });
1733
+ }
1734
+ if (enrollStep === "qr") {
1735
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "px-6 py-4 space-y-4", children: [
1736
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "text-xs text-[#475569]", children: "Scan this QR code with Google Authenticator, Authy, or any TOTP app." }),
1737
+ qrDataUrl && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "flex justify-center", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("img", { src: qrDataUrl, alt: "TOTP QR code", className: "rounded-xl", width: 180, height: 180 }) }),
1738
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("details", { className: "text-[11px] text-[#475569]", children: [
1739
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("summary", { className: "cursor-pointer hover:text-white/60", children: "Can't scan? Enter manually" }),
1740
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "mt-1 font-mono break-all bg-white/5 rounded px-2 py-1 text-white/60 select-all", children: manualKey })
1741
+ ] }),
1742
+ error && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "rounded-lg bg-red-500/10 border border-red-500/30 px-3 py-2 text-xs text-red-400", children: error }),
1743
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("form", { onSubmit: verifyEnroll, className: "space-y-3", children: [
1744
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1745
+ "input",
1746
+ {
1747
+ type: "text",
1748
+ inputMode: "numeric",
1749
+ pattern: "[0-9]{6}",
1750
+ maxLength: 6,
1751
+ placeholder: "000000",
1752
+ value: enrollCode,
1753
+ onChange: (e) => setEnrollCode(e.target.value),
1754
+ autoFocus: true,
1755
+ required: true,
1756
+ className: "w-full bg-white/5 border border-white/8 rounded-xl px-4 py-2.5 text-sm text-white/90 placeholder:text-[#475569] focus:outline-none focus:border-purple-500/60 transition-colors text-center tracking-[0.4em]"
1757
+ }
1758
+ ),
1759
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1760
+ "button",
1761
+ {
1762
+ type: "submit",
1763
+ disabled: loading || enrollCode.length !== 6,
1764
+ className: "w-full py-2.5 rounded-xl text-sm font-semibold text-white bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-500 hover:to-blue-500 transition-all disabled:opacity-60",
1765
+ children: loading ? "Verifying\u2026" : "Verify and enable"
1766
+ }
1767
+ ),
1768
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1769
+ "button",
1770
+ {
1771
+ type: "button",
1772
+ onClick: () => {
1773
+ setEnrollStep("idle");
1774
+ setError(null);
1775
+ },
1776
+ className: "w-full text-xs text-[#475569] hover:text-white/70 transition-colors py-1",
1777
+ children: "\u2190 Cancel"
1778
+ }
1779
+ )
1780
+ ] })
1781
+ ] });
1782
+ }
1783
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "px-6 py-4 space-y-4", children: [
1784
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "rounded-xl border border-white/8 p-4", style: { background: "rgba(255,255,255,0.03)" }, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex items-center justify-between", children: [
1785
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { children: [
1786
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "text-sm font-medium text-white/90", children: "Authenticator app" }),
1787
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "text-[11px] text-[#475569] mt-0.5", children: totpEnabled ? `Enabled \xB7 ${backupCodesLeft ?? 0} backup code${backupCodesLeft === 1 ? "" : "s"} remaining` : "Add a second layer of security to your account" })
1788
+ ] }),
1789
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: (0, import_clsx3.clsx)(
1790
+ "text-[10px] font-semibold uppercase tracking-wide px-2 py-0.5 rounded-full border",
1791
+ totpEnabled ? "bg-emerald-500/15 text-emerald-400 border-emerald-500/30" : "bg-white/5 text-[#475569] border-white/8"
1792
+ ), children: totpEnabled ? "On" : "Off" })
1793
+ ] }) }),
1794
+ error && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "rounded-lg bg-red-500/10 border border-red-500/30 px-3 py-2 text-xs text-red-400", children: error }),
1795
+ !totpEnabled && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1247
1796
  "button",
1248
1797
  {
1249
- onClick: () => setOpen((o) => !o),
1250
- className: "flex items-center gap-2 rounded-xl p-1 hover:bg-white/8 transition-colors",
1251
- children: [
1252
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Avatar, { initials, imageUrl: user.imageUrl }),
1253
- showName && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "text-sm font-medium text-white/90 pr-1", children: user.firstName ?? user.email })
1254
- ]
1798
+ onClick: startEnroll,
1799
+ disabled: loading,
1800
+ className: "w-full py-2.5 rounded-xl text-sm font-semibold text-white bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-500 hover:to-blue-500 transition-all disabled:opacity-60",
1801
+ children: loading ? "Loading\u2026" : "Enable authenticator app"
1255
1802
  }
1256
1803
  ),
1257
- open && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
1258
- "div",
1804
+ totpEnabled && !showDisable && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1805
+ "button",
1259
1806
  {
1260
- className: (0, import_clsx3.clsx)(
1261
- "absolute right-0 mt-2 w-64 rounded-2xl border border-white/8",
1262
- "backdrop-blur-[16px] shadow-2xl shadow-black/40 z-50"
1807
+ onClick: () => setShowDisable(true),
1808
+ className: "w-full py-2 rounded-xl text-xs font-medium text-red-400 border border-red-500/20 hover:bg-red-500/8 transition-colors",
1809
+ children: "Disable authenticator app"
1810
+ }
1811
+ ),
1812
+ totpEnabled && showDisable && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("form", { onSubmit: disableTotp, className: "space-y-3", children: [
1813
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "text-[11px] text-[#475569]", children: "Enter your current 6-digit code (or a backup code) to confirm." }),
1814
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1815
+ "input",
1816
+ {
1817
+ type: "text",
1818
+ placeholder: "000000 or backup code",
1819
+ value: disableCode,
1820
+ onChange: (e) => setDisableCode(e.target.value),
1821
+ autoFocus: true,
1822
+ required: true,
1823
+ className: "w-full bg-white/5 border border-white/8 rounded-xl px-4 py-2.5 text-sm text-white/90 placeholder:text-[#475569] focus:outline-none focus:border-red-500/60 transition-colors"
1824
+ }
1825
+ ),
1826
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex gap-2", children: [
1827
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1828
+ "button",
1829
+ {
1830
+ type: "submit",
1831
+ disabled: loading || !disableCode,
1832
+ className: "flex-1 py-2 rounded-xl text-xs font-semibold text-white bg-red-600 hover:bg-red-500 transition-colors disabled:opacity-60",
1833
+ children: loading ? "Disabling\u2026" : "Confirm disable"
1834
+ }
1263
1835
  ),
1264
- style: { background: "rgba(22,27,45,0.96)" },
1265
- children: [
1266
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex items-center gap-3 px-4 py-3 border-b border-white/8", children: [
1267
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Avatar, { initials, imageUrl: user.imageUrl, size: "lg" }),
1268
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex-1 min-w-0", children: [
1269
- (user.firstName || user.lastName) && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "text-sm font-semibold text-white/90 truncate", children: [user.firstName, user.lastName].filter(Boolean).join(" ") }),
1270
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "text-xs text-[#475569] truncate", children: user.email })
1271
- ] })
1272
- ] }),
1273
- session && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "px-4 py-2 border-b border-white/8", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex items-center justify-between", children: [
1274
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "text-[10px] uppercase tracking-widest text-[#475569]", children: "Risk level" }),
1275
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(RiskBadge, { level: session.riskLevel })
1276
- ] }) }),
1277
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "p-2", children: [
1278
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1279
- DropdownItem,
1280
- {
1281
- label: "Manage account",
1282
- icon: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(UserIcon, {}),
1283
- onClick: () => setOpen(false)
1284
- }
1285
- ),
1286
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1287
- DropdownItem,
1288
- {
1289
- label: "Security settings",
1290
- icon: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ShieldIcon, {}),
1291
- onClick: () => setOpen(false)
1292
- }
1293
- ),
1294
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "h-px bg-white/8 my-1" }),
1295
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1296
- DropdownItem,
1297
- {
1298
- label: signingOut ? "Signing out\u2026" : "Sign out",
1299
- icon: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(SignOutIcon, {}),
1300
- onClick: handleSignOut,
1301
- destructive: true
1302
- }
1303
- )
1304
- ] })
1305
- ]
1836
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1837
+ "button",
1838
+ {
1839
+ type: "button",
1840
+ onClick: () => {
1841
+ setShowDisable(false);
1842
+ setError(null);
1843
+ setDisableCode("");
1844
+ },
1845
+ className: "px-4 py-2 rounded-xl text-xs text-[#475569] hover:text-white hover:bg-white/8 border border-white/8 transition-colors",
1846
+ children: "Cancel"
1847
+ }
1848
+ )
1849
+ ] })
1850
+ ] })
1851
+ ] });
1852
+ }
1853
+ function ModalField({ label, value, onChange, placeholder, type = "text" }) {
1854
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "space-y-1", children: [
1855
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("label", { className: "text-[10px] uppercase tracking-widest text-[#475569]", children: label }),
1856
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1857
+ "input",
1858
+ {
1859
+ type,
1860
+ value,
1861
+ onChange: (e) => onChange(e.target.value),
1862
+ placeholder,
1863
+ className: (0, import_clsx3.clsx)(
1864
+ "w-full bg-white/5 border border-white/8 rounded-xl px-3 py-2 text-sm text-white/90",
1865
+ "placeholder:text-[#475569] focus:outline-none focus:border-purple-500/60 transition-colors"
1866
+ )
1306
1867
  }
1307
1868
  )
1308
1869
  ] });
1309
1870
  }
1310
- function Avatar({ initials, imageUrl, size = "sm" }) {
1311
- const dim = size === "lg" ? "w-9 h-9 text-sm" : "w-8 h-8 text-xs";
1312
- if (imageUrl) {
1313
- return (
1314
- // eslint-disable-next-line @next/next/no-img-element
1315
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1316
- "img",
1317
- {
1318
- src: imageUrl,
1319
- alt: "",
1320
- className: (0, import_clsx3.clsx)(dim, "rounded-full object-cover ring-2 ring-white/10")
1321
- }
1322
- )
1323
- );
1324
- }
1871
+ function SaveButton({ saving, label = "Save changes" }) {
1325
1872
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1326
- "div",
1873
+ "button",
1327
1874
  {
1875
+ type: "submit",
1876
+ disabled: saving,
1328
1877
  className: (0, import_clsx3.clsx)(
1329
- dim,
1330
- "rounded-full flex items-center justify-center font-semibold text-white",
1331
- "bg-gradient-to-br from-purple-600 to-blue-600 ring-2 ring-white/10"
1878
+ "px-4 py-2 rounded-xl text-sm font-semibold text-white transition-all",
1879
+ "bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-500 hover:to-blue-500",
1880
+ "shadow-lg shadow-purple-500/20 disabled:opacity-60"
1332
1881
  ),
1333
- children: initials
1882
+ children: saving ? "Saving\u2026" : label
1334
1883
  }
1335
1884
  );
1336
1885
  }
1886
+ function Avatar({ initials, imageUrl, size = "sm" }) {
1887
+ const dim = size === "lg" ? "w-9 h-9 text-sm" : "w-8 h-8 text-xs";
1888
+ if (imageUrl) {
1889
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("img", { src: imageUrl, alt: "", className: (0, import_clsx3.clsx)(dim, "rounded-full object-cover ring-2 ring-white/10") });
1890
+ }
1891
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: (0, import_clsx3.clsx)(
1892
+ dim,
1893
+ "rounded-full flex items-center justify-center font-semibold text-white",
1894
+ "bg-gradient-to-br from-purple-600 to-blue-600 ring-2 ring-white/10"
1895
+ ), children: initials });
1896
+ }
1337
1897
  function AvatarSkeleton() {
1338
1898
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "w-8 h-8 rounded-full bg-white/5 animate-pulse" });
1339
1899
  }
@@ -1344,16 +1904,10 @@ function RiskBadge({ level }) {
1344
1904
  high: "bg-orange-500/15 text-orange-400 border-orange-500/30",
1345
1905
  critical: "bg-red-500/15 text-red-400 border-red-500/30"
1346
1906
  };
1347
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1348
- "span",
1349
- {
1350
- className: (0, import_clsx3.clsx)(
1351
- "inline-flex items-center text-[10px] font-semibold uppercase tracking-wider px-2 py-0.5 rounded-full border",
1352
- styles[level] ?? styles.low
1353
- ),
1354
- children: level
1355
- }
1356
- );
1907
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: (0, import_clsx3.clsx)(
1908
+ "inline-flex items-center text-[10px] font-semibold uppercase tracking-wider px-2 py-0.5 rounded-full border",
1909
+ styles[level] ?? styles.low
1910
+ ), children: level });
1357
1911
  }
1358
1912
  function DropdownItem({ label, icon, onClick, destructive }) {
1359
1913
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
@@ -1377,9 +1931,6 @@ function UserIcon() {
1377
1931
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("circle", { cx: "12", cy: "7", r: "4" })
1378
1932
  ] });
1379
1933
  }
1380
- function ShieldIcon() {
1381
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" }) });
1382
- }
1383
1934
  function SignOutIcon() {
1384
1935
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
1385
1936
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" }),
@@ -1387,6 +1938,12 @@ function SignOutIcon() {
1387
1938
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("line", { x1: "21", y1: "12", x2: "9", y2: "12" })
1388
1939
  ] });
1389
1940
  }
1941
+ function CloseIcon() {
1942
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
1943
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
1944
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
1945
+ ] });
1946
+ }
1390
1947
 
1391
1948
  // src/components/OrganizationSwitcher.tsx
1392
1949
  var import_clsx4 = require("clsx");
@@ -1582,17 +2139,19 @@ function MiniSpinner() {
1582
2139
  }
1583
2140
 
1584
2141
  // src/index.ts
1585
- var STORAGE_KEY2 = "vaultix_jwt";
1586
2142
  function getStoredToken() {
2143
+ if (typeof document === "undefined") return null;
1587
2144
  try {
1588
- return (typeof sessionStorage !== "undefined" ? sessionStorage : null)?.getItem(STORAGE_KEY2) ?? null;
2145
+ const m = document.cookie.match(/(?:^|; )vaultix-token=([^;]+)/);
2146
+ return m?.[1] ? decodeURIComponent(m[1]) : null;
1589
2147
  } catch {
1590
2148
  return null;
1591
2149
  }
1592
2150
  }
1593
2151
  function clearStoredToken() {
2152
+ if (typeof document === "undefined") return;
1594
2153
  try {
1595
- if (typeof sessionStorage !== "undefined") sessionStorage.removeItem(STORAGE_KEY2);
2154
+ document.cookie = "vaultix-token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax";
1596
2155
  } catch {
1597
2156
  }
1598
2157
  }
@@ -1611,6 +2170,7 @@ function clearStoredToken() {
1611
2170
  getStoredToken,
1612
2171
  useAuth,
1613
2172
  useOrganization,
2173
+ usePlatformConnect,
1614
2174
  useSession,
1615
2175
  useUser,
1616
2176
  useVaultix