@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.mjs CHANGED
@@ -23,28 +23,35 @@ function resolveApiOrigin(key, apiUrlProp) {
23
23
  throw new Error(`Unknown publishable key prefix "${parts[0]}".`);
24
24
  }
25
25
  try {
26
- return atob(parts.slice(3).join("_")).replace(/\/$/, "");
26
+ return atob(parts.slice(3).join("_")).replace(/\|.+$/, "").replace(/\/$/, "");
27
27
  } catch {
28
28
  throw new Error("Publishable key has an unreadable API origin payload.");
29
29
  }
30
30
  }
31
- var STORAGE_KEY = "vaultix_jwt";
31
+ var TOKEN_COOKIE = "vaultix-token";
32
+ var COOKIE_DAYS = 30;
32
33
  function storeJwt(jwt) {
34
+ if (typeof document === "undefined") return;
33
35
  try {
34
- sessionStorage.setItem(STORAGE_KEY, jwt);
36
+ const expires = new Date(Date.now() + COOKIE_DAYS * 864e5).toUTCString();
37
+ const secure = location.protocol === "https:" ? "; Secure" : "";
38
+ document.cookie = `${TOKEN_COOKIE}=${encodeURIComponent(jwt)}; path=/; expires=${expires}; SameSite=Lax${secure}`;
35
39
  } catch {
36
40
  }
37
41
  }
38
42
  function loadJwt() {
43
+ if (typeof document === "undefined") return null;
39
44
  try {
40
- return sessionStorage.getItem(STORAGE_KEY);
45
+ const m = document.cookie.match(new RegExp(`(?:^|; )${TOKEN_COOKIE}=([^;]+)`));
46
+ return m?.[1] ? decodeURIComponent(m[1]) : null;
41
47
  } catch {
42
48
  return null;
43
49
  }
44
50
  }
45
51
  function clearJwt() {
52
+ if (typeof document === "undefined") return;
46
53
  try {
47
- sessionStorage.removeItem(STORAGE_KEY);
54
+ document.cookie = `${TOKEN_COOKIE}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax`;
48
55
  } catch {
49
56
  }
50
57
  }
@@ -115,26 +122,27 @@ function VaultixProvider({
115
122
  async function hydrate() {
116
123
  let jwt = null;
117
124
  if (typeof window !== "undefined") {
118
- const url = new URL(window.location.href);
119
- const handshakeToken = url.searchParams.get("__vaultix_handshake");
120
- if (handshakeToken) {
121
- url.searchParams.delete("__vaultix_handshake");
122
- window.history.replaceState({}, "", url.toString());
123
- try {
124
- const res = await fetch(`${apiOrigin}/api/v1/tokens/exchange`, {
125
- method: "POST",
126
- headers: { "Content-Type": "application/json" },
127
- body: JSON.stringify({ handshake_token: handshakeToken })
128
- });
129
- if (res.ok) {
130
- const data = await res.json();
131
- jwt = data.session_jwt;
132
- storeJwt(jwt);
125
+ jwt = loadJwt();
126
+ if (!jwt) {
127
+ const url = new URL(window.location.href);
128
+ const handshakeToken = url.searchParams.get("__vaultix_handshake");
129
+ if (handshakeToken) {
130
+ url.searchParams.delete("__vaultix_handshake");
131
+ window.history.replaceState({}, "", url.toString());
132
+ try {
133
+ const res = await fetch(`${apiOrigin}/api/v1/tokens/exchange`, {
134
+ method: "POST",
135
+ headers: { "Content-Type": "application/json" },
136
+ body: JSON.stringify({ handshake_token: handshakeToken })
137
+ });
138
+ if (res.ok) {
139
+ const data = await res.json();
140
+ jwt = data.session_jwt;
141
+ storeJwt(jwt);
142
+ }
143
+ } catch {
133
144
  }
134
- } catch {
135
145
  }
136
- } else {
137
- jwt = loadJwt();
138
146
  }
139
147
  jwtRef.current = jwt;
140
148
  }
@@ -175,7 +183,7 @@ function VaultixProvider({
175
183
  if (refreshTimerRef.current) clearTimeout(refreshTimerRef.current);
176
184
  };
177
185
  }, [apiOrigin, publishableKey, scheduleRefresh]);
178
- const signOut = useCallback(async () => {
186
+ const signOut = useCallback(async (redirectUrl) => {
179
187
  const jwt = jwtRef.current;
180
188
  clearAuth();
181
189
  if (refreshTimerRef.current) clearTimeout(refreshTimerRef.current);
@@ -187,8 +195,29 @@ function VaultixProvider({
187
195
  headers
188
196
  }).catch(() => {
189
197
  });
190
- if (afterSignInUrl) window.location.href = afterSignInUrl;
191
- }, [apiOrigin, afterSignInUrl, clearAuth]);
198
+ const dest = redirectUrl ?? afterSignUpUrl ?? "/";
199
+ if (typeof window !== "undefined") window.location.href = dest;
200
+ }, [apiOrigin, afterSignUpUrl, clearAuth]);
201
+ const updateUser = useCallback(async (params) => {
202
+ const userId = user?.id;
203
+ if (!userId) throw new Error("No active session");
204
+ const jwt = jwtRef.current;
205
+ const headers = { "Content-Type": "application/json" };
206
+ if (jwt) headers["Authorization"] = `Bearer ${jwt}`;
207
+ const res = await fetch(`${apiOrigin}/api/v1/users/${userId}`, {
208
+ method: "PATCH",
209
+ credentials: jwt ? "omit" : "include",
210
+ headers,
211
+ body: JSON.stringify(params)
212
+ });
213
+ if (!res.ok) {
214
+ const err = await res.json().catch(() => ({ error: "Update failed" }));
215
+ throw new Error(err.error ?? "Update failed");
216
+ }
217
+ const updated = await res.json();
218
+ setUser(updated);
219
+ return updated;
220
+ }, [apiOrigin, user?.id]);
192
221
  const connectPlatform = useCallback((provider, options = {}) => {
193
222
  const params = new URLSearchParams({ mode: "connect" });
194
223
  params.set("redirect_url", options.redirectUrl ?? (typeof window !== "undefined" ? window.location.href : "/"));
@@ -206,7 +235,9 @@ function VaultixProvider({
206
235
  organization,
207
236
  isLoaded,
208
237
  isSignedIn: !!session,
238
+ apiOrigin,
209
239
  signOut,
240
+ updateUser,
210
241
  connectPlatform
211
242
  };
212
243
  return /* @__PURE__ */ jsx(VaultixContext.Provider, { value, children: /* @__PURE__ */ jsx(
@@ -223,7 +254,7 @@ function VaultixProvider({
223
254
  }
224
255
 
225
256
  // src/hooks/index.ts
226
- import { useCallback as useCallback2 } from "react";
257
+ import { useCallback as useCallback2, useEffect as useEffect2, useState as useState2 } from "react";
227
258
  function useVaultix() {
228
259
  return useVaultixContext();
229
260
  }
@@ -232,19 +263,58 @@ function useSession() {
232
263
  return { session, isLoaded, isSignedIn };
233
264
  }
234
265
  function useUser() {
235
- const { user, isLoaded, isSignedIn } = useVaultixContext();
236
- return { user, isLoaded, isSignedIn };
266
+ const { user, isLoaded, isSignedIn, updateUser } = useVaultixContext();
267
+ return { user, isLoaded, isSignedIn, update: updateUser };
237
268
  }
238
269
  function useOrganization() {
239
270
  const { organization, isLoaded } = useVaultixContext();
240
271
  return { organization, isLoaded };
241
272
  }
273
+ function usePlatformConnect(provider, defaultOptions) {
274
+ const { connectPlatform } = useVaultixContext();
275
+ const [connectedAccount, setConnectedAccount] = useState2(null);
276
+ const connect = useCallback2(
277
+ (overrides) => {
278
+ connectPlatform(provider, { ...defaultOptions, ...overrides });
279
+ },
280
+ [connectPlatform, provider, defaultOptions]
281
+ );
282
+ useEffect2(() => {
283
+ if (typeof window === "undefined") return;
284
+ const params = new URLSearchParams(window.location.search);
285
+ const connectToken = params.get("__vaultix_connect");
286
+ const returnedProvider = params.get("provider");
287
+ if (!connectToken || returnedProvider !== provider) return;
288
+ try {
289
+ const segment = connectToken.split(".")[1];
290
+ if (!segment) return;
291
+ const b64 = segment.replace(/-/g, "+").replace(/_/g, "/");
292
+ const payload = JSON.parse(atob(b64));
293
+ const result = {
294
+ provider: returnedProvider,
295
+ raw: payload,
296
+ ...typeof payload["platform_user_id"] === "string" && { platformUserId: payload["platform_user_id"] },
297
+ ...typeof payload["username"] === "string" && { username: payload["username"] },
298
+ ...typeof payload["access_token"] === "string" && { accessToken: payload["access_token"] },
299
+ ...typeof payload["scope"] === "string" && { scope: payload["scope"] }
300
+ };
301
+ setConnectedAccount(result);
302
+ const clean = new URL(window.location.href);
303
+ clean.searchParams.delete("__vaultix_connect");
304
+ clean.searchParams.delete("provider");
305
+ window.history.replaceState({}, "", clean.toString());
306
+ } catch {
307
+ }
308
+ }, [provider]);
309
+ return { connect, connectedAccount };
310
+ }
242
311
  function useAuth() {
243
312
  const { user, session, isLoaded, isSignedIn, signOut } = useVaultixContext();
244
313
  const getToken = useCallback2(async () => {
245
314
  if (!isSignedIn) return null;
246
315
  try {
247
- return sessionStorage.getItem("vaultix_jwt");
316
+ const m = document.cookie.match(/(?:^|; )vaultix-token=([^;]+)/);
317
+ return m?.[1] ? decodeURIComponent(m[1]) : null;
248
318
  } catch {
249
319
  return null;
250
320
  }
@@ -262,7 +332,7 @@ function useAuth() {
262
332
 
263
333
  // src/components/SignIn.tsx
264
334
  import { clsx } from "clsx";
265
- import { useRef as useRef2, useState as useState2 } from "react";
335
+ import { useRef as useRef2, useState as useState3 } from "react";
266
336
  import { jsx as jsx2, jsxs } from "react/jsx-runtime";
267
337
  function toBase64url(buf) {
268
338
  return btoa(String.fromCharCode(...new Uint8Array(buf))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
@@ -299,18 +369,24 @@ function SignIn({
299
369
  onSuccess,
300
370
  onError,
301
371
  apiOrigin: apiOriginProp,
302
- className
372
+ className,
373
+ appearance
303
374
  }) {
304
375
  const apiOrigin = resolveApiOrigin2(apiOriginProp);
305
- const [step, setStep] = useState2("email");
306
- const [email, setEmail] = useState2("");
307
- const [password, setPassword] = useState2("");
308
- const [totp, setTotp] = useState2("");
309
- const [forgotCode, setForgotCode] = useState2("");
310
- const [newPassword, setNewPassword] = useState2("");
311
- const [error, setError] = useState2(null);
312
- const [loading, setLoading] = useState2(false);
313
- const [info, setInfo] = useState2(null);
376
+ const vars = appearance?.variables ?? {};
377
+ const primaryColor = vars.colorPrimary;
378
+ const cardBg = vars.colorBackground ?? "rgba(22,27,45,0.92)";
379
+ const textColor = vars.colorText ?? "rgba(255,255,255,0.9)";
380
+ const mutedColor = vars.colorTextMuted ?? "#475569";
381
+ const [step, setStep] = useState3("email");
382
+ const [email, setEmail] = useState3("");
383
+ const [password, setPassword] = useState3("");
384
+ const [totp, setTotp] = useState3("");
385
+ const [forgotCode, setForgotCode] = useState3("");
386
+ const [newPassword, setNewPassword] = useState3("");
387
+ const [error, setError] = useState3(null);
388
+ const [loading, setLoading] = useState3(false);
389
+ const [info, setInfo] = useState3(null);
314
390
  const challengeIdRef = useRef2(null);
315
391
  function setErr(msg) {
316
392
  setError(msg);
@@ -358,6 +434,23 @@ function SignIn({
358
434
  setLoading(false);
359
435
  }
360
436
  }
437
+ async function handleMagicLinkRequest() {
438
+ setError(null);
439
+ setLoading(true);
440
+ try {
441
+ const target = resolveAfterSignInUrl(redirectUrl);
442
+ await fetch(`${apiOrigin}/api/v1/auth/magic-link/send`, {
443
+ method: "POST",
444
+ headers: { "Content-Type": "application/json" },
445
+ body: JSON.stringify({ email, redirect_url: target })
446
+ });
447
+ setStep("magic-link-sent");
448
+ } catch {
449
+ setErr("Network error. Please try again.");
450
+ } finally {
451
+ setLoading(false);
452
+ }
453
+ }
361
454
  async function handlePasswordSubmit(e) {
362
455
  e.preventDefault();
363
456
  setError(null);
@@ -526,6 +619,7 @@ function SignIn({
526
619
  password: "Enter password",
527
620
  passkey: "Passkey sign-in",
528
621
  totp: "Two-factor code",
622
+ "magic-link-sent": "Check your inbox",
529
623
  forgot: "Forgot password",
530
624
  "forgot-verify": "Enter reset code",
531
625
  "forgot-reset": "New password"
@@ -535,6 +629,7 @@ function SignIn({
535
629
  password: email,
536
630
  passkey: email,
537
631
  totp: "Enter the 6-digit code from your authenticator app",
632
+ "magic-link-sent": `We sent a sign-in link to ${email}`,
538
633
  forgot: "We'll send a reset code to your email",
539
634
  "forgot-verify": `Code sent to ${email}`,
540
635
  "forgot-reset": "Choose a new password"
@@ -547,12 +642,19 @@ function SignIn({
547
642
  "backdrop-blur-[16px]",
548
643
  className
549
644
  ),
550
- style: { background: "rgba(22,27,45,0.92)" },
645
+ style: { background: cardBg },
551
646
  children: [
552
647
  /* @__PURE__ */ jsxs("div", { className: "px-8 pt-8 pb-6 text-center", children: [
553
- /* @__PURE__ */ jsx2("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__ */ jsx2(LockIcon, {}) }),
554
- /* @__PURE__ */ jsx2("h1", { className: "text-xl font-semibold text-white/90", children: title[step] }),
555
- /* @__PURE__ */ jsx2("p", { className: "text-sm text-[#475569] mt-1", children: subtitle[step] })
648
+ /* @__PURE__ */ jsx2(
649
+ "div",
650
+ {
651
+ className: "inline-flex items-center justify-center w-10 h-10 rounded-xl shadow-lg mb-4",
652
+ 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)" },
653
+ children: /* @__PURE__ */ jsx2(LockIcon, {})
654
+ }
655
+ ),
656
+ /* @__PURE__ */ jsx2("h1", { className: "text-xl font-semibold", style: { color: textColor }, children: title[step] }),
657
+ /* @__PURE__ */ jsx2("p", { className: "text-sm mt-1", style: { color: mutedColor }, children: subtitle[step] })
556
658
  ] }),
557
659
  /* @__PURE__ */ jsxs("div", { className: "px-8 pb-8 space-y-4", children: [
558
660
  error && /* @__PURE__ */ jsx2("div", { className: "rounded-lg bg-red-500/10 border border-red-500/30 px-3 py-2", children: /* @__PURE__ */ jsx2("p", { className: "text-xs text-red-400", children: error }) }),
@@ -563,7 +665,7 @@ function SignIn({
563
665
  /* @__PURE__ */ jsx2(MetaButton, { onClick: handleMetaSignIn }),
564
666
  /* @__PURE__ */ jsx2(LinkedInButton, { onClick: handleLinkedInSignIn }),
565
667
  /* @__PURE__ */ jsx2(XButton, { onClick: handleXSignIn }),
566
- /* @__PURE__ */ jsx2(Divider, {}),
668
+ /* @__PURE__ */ jsx2(Divider, { mutedColor }),
567
669
  /* @__PURE__ */ jsxs("form", { onSubmit: handleEmailSubmit, className: "space-y-3", children: [
568
670
  /* @__PURE__ */ jsx2(
569
671
  Input,
@@ -573,10 +675,11 @@ function SignIn({
573
675
  value: email,
574
676
  onChange: setEmail,
575
677
  autoFocus: true,
576
- required: true
678
+ required: true,
679
+ primaryColor
577
680
  }
578
681
  ),
579
- /* @__PURE__ */ jsx2(PrimaryButton, { loading, children: "Continue" })
682
+ /* @__PURE__ */ jsx2(PrimaryButton, { loading, primaryColor, children: "Continue" })
580
683
  ] })
581
684
  ] }),
582
685
  step === "password" && /* @__PURE__ */ jsxs("form", { onSubmit: handlePasswordSubmit, className: "space-y-3", children: [
@@ -588,23 +691,50 @@ function SignIn({
588
691
  value: password,
589
692
  onChange: setPassword,
590
693
  autoFocus: true,
591
- required: true
694
+ required: true,
695
+ primaryColor
592
696
  }
593
697
  ),
594
- /* @__PURE__ */ jsx2(PrimaryButton, { loading, children: "Sign in" }),
698
+ /* @__PURE__ */ jsx2(PrimaryButton, { loading, primaryColor, children: "Sign in" }),
699
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
700
+ /* @__PURE__ */ jsx2("div", { className: "flex-1 h-px bg-white/8" }),
701
+ /* @__PURE__ */ jsx2("span", { className: "text-[10px]", style: { color: mutedColor }, children: "or" }),
702
+ /* @__PURE__ */ jsx2("div", { className: "flex-1 h-px bg-white/8" })
703
+ ] }),
704
+ /* @__PURE__ */ jsx2(GhostButton, { onClick: handleMagicLinkRequest, mutedColor, children: "\u2709 Email me a sign-in link" }),
595
705
  /* @__PURE__ */ jsx2(GhostButton, { onClick: () => {
596
706
  setStep("forgot");
597
707
  setError(null);
598
- }, children: "Forgot password?" }),
599
- /* @__PURE__ */ jsx2(GhostButton, { onClick: () => setStep("email"), children: "\u2190 Back" })
708
+ }, mutedColor, children: "Forgot password?" }),
709
+ /* @__PURE__ */ jsx2(GhostButton, { onClick: () => setStep("email"), mutedColor, children: "\u2190 Back" })
710
+ ] }),
711
+ step === "magic-link-sent" && /* @__PURE__ */ jsxs("div", { className: "space-y-4 text-center", children: [
712
+ /* @__PURE__ */ jsx2(
713
+ "div",
714
+ {
715
+ className: "w-14 h-14 rounded-2xl flex items-center justify-center mx-auto border",
716
+ style: primaryColor ? { background: `${primaryColor}26`, borderColor: `${primaryColor}4d` } : { background: "rgba(168,85,247,0.15)", borderColor: "rgba(168,85,247,0.3)" },
717
+ children: /* @__PURE__ */ jsx2(EnvelopeIcon, { color: primaryColor ?? "#a78bfa" })
718
+ }
719
+ ),
720
+ /* @__PURE__ */ jsxs("p", { className: "text-sm leading-relaxed", style: { color: mutedColor }, children: [
721
+ "The link expires in ",
722
+ /* @__PURE__ */ jsx2("span", { style: { color: textColor }, className: "font-medium", children: "15 minutes" }),
723
+ " and can only be used once."
724
+ ] }),
725
+ /* @__PURE__ */ jsx2(GhostButton, { onClick: handleMagicLinkRequest, mutedColor, children: "Resend link" }),
726
+ /* @__PURE__ */ jsx2(GhostButton, { onClick: () => {
727
+ setStep("password");
728
+ setError(null);
729
+ }, mutedColor, children: "Use password instead" })
600
730
  ] }),
601
731
  step === "passkey" && /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
602
- /* @__PURE__ */ jsxs(PrimaryButton, { loading, onClick: handlePasskeySignIn, children: [
732
+ /* @__PURE__ */ jsxs(PrimaryButton, { loading, onClick: handlePasskeySignIn, primaryColor, children: [
603
733
  /* @__PURE__ */ jsx2(FingerprintIcon, {}),
604
734
  "Authenticate with passkey"
605
735
  ] }),
606
- /* @__PURE__ */ jsx2(GhostButton, { onClick: () => setStep("password"), children: "Use password instead" }),
607
- /* @__PURE__ */ jsx2(GhostButton, { onClick: () => setStep("email"), children: "\u2190 Back" })
736
+ /* @__PURE__ */ jsx2(GhostButton, { onClick: () => setStep("password"), mutedColor, children: "Use password instead" }),
737
+ /* @__PURE__ */ jsx2(GhostButton, { onClick: () => setStep("email"), mutedColor, children: "\u2190 Back" })
608
738
  ] }),
609
739
  step === "totp" && /* @__PURE__ */ jsxs("form", { onSubmit: handleTotpSubmit, className: "space-y-3", children: [
610
740
  /* @__PURE__ */ jsx2(
@@ -619,11 +749,12 @@ function SignIn({
619
749
  onChange: setTotp,
620
750
  autoFocus: true,
621
751
  required: true,
622
- className: "text-center tracking-[0.4em] text-lg"
752
+ className: "text-center tracking-[0.4em] text-lg",
753
+ primaryColor
623
754
  }
624
755
  ),
625
- /* @__PURE__ */ jsx2(PrimaryButton, { loading, children: "Verify" }),
626
- /* @__PURE__ */ jsx2(GhostButton, { onClick: () => setStep("password"), children: "\u2190 Back" })
756
+ /* @__PURE__ */ jsx2(PrimaryButton, { loading, primaryColor, children: "Verify" }),
757
+ /* @__PURE__ */ jsx2(GhostButton, { onClick: () => setStep("password"), mutedColor, children: "\u2190 Back" })
627
758
  ] }),
628
759
  step === "forgot" && /* @__PURE__ */ jsxs("form", { onSubmit: handleForgotSubmit, className: "space-y-3", children: [
629
760
  /* @__PURE__ */ jsx2(
@@ -634,14 +765,15 @@ function SignIn({
634
765
  value: email,
635
766
  onChange: setEmail,
636
767
  autoFocus: true,
637
- required: true
768
+ required: true,
769
+ primaryColor
638
770
  }
639
771
  ),
640
- /* @__PURE__ */ jsx2(PrimaryButton, { loading, children: "Send reset code" }),
772
+ /* @__PURE__ */ jsx2(PrimaryButton, { loading, primaryColor, children: "Send reset code" }),
641
773
  /* @__PURE__ */ jsx2(GhostButton, { onClick: () => {
642
774
  setStep("password");
643
775
  setError(null);
644
- }, children: "\u2190 Back" })
776
+ }, mutedColor, children: "\u2190 Back" })
645
777
  ] }),
646
778
  step === "forgot-verify" && /* @__PURE__ */ jsxs("form", { onSubmit: handleForgotVerifySubmit, className: "space-y-3", children: [
647
779
  /* @__PURE__ */ jsx2(
@@ -656,10 +788,11 @@ function SignIn({
656
788
  onChange: setForgotCode,
657
789
  autoFocus: true,
658
790
  required: true,
659
- className: "text-center tracking-[0.4em] text-lg"
791
+ className: "text-center tracking-[0.4em] text-lg",
792
+ primaryColor
660
793
  }
661
794
  ),
662
- /* @__PURE__ */ jsx2(PrimaryButton, { loading, children: "Continue" })
795
+ /* @__PURE__ */ jsx2(PrimaryButton, { loading, primaryColor, children: "Continue" })
663
796
  ] }),
664
797
  step === "forgot-reset" && /* @__PURE__ */ jsxs("form", { onSubmit: handleResetPasswordSubmit, className: "space-y-3", children: [
665
798
  /* @__PURE__ */ jsx2(
@@ -671,18 +804,19 @@ function SignIn({
671
804
  onChange: setNewPassword,
672
805
  autoFocus: true,
673
806
  required: true,
674
- minLength: 8
807
+ minLength: 8,
808
+ primaryColor
675
809
  }
676
810
  ),
677
811
  /* @__PURE__ */ jsx2(PasswordStrength, { password: newPassword }),
678
- /* @__PURE__ */ jsx2(PrimaryButton, { loading, children: "Reset password" })
812
+ /* @__PURE__ */ jsx2(PrimaryButton, { loading, primaryColor, children: "Reset password" })
679
813
  ] })
680
814
  ] })
681
815
  ]
682
816
  }
683
817
  );
684
818
  }
685
- function Input({ onChange, className, ...props }) {
819
+ function Input({ onChange, className, primaryColor, ...props }) {
686
820
  return /* @__PURE__ */ jsx2(
687
821
  "input",
688
822
  {
@@ -691,39 +825,48 @@ function Input({ onChange, className, ...props }) {
691
825
  className: clsx(
692
826
  "w-full bg-white/5 border border-white/8 rounded-xl px-4 py-2.5",
693
827
  "text-sm text-white/90 placeholder:text-[#475569]",
694
- "focus:outline-none focus:border-purple-500/60 transition-colors",
828
+ "focus:outline-none transition-colors",
829
+ !primaryColor && "focus:border-purple-500/60",
695
830
  className
696
- )
831
+ ),
832
+ style: primaryColor ? { "--vx-focus": primaryColor } : void 0,
833
+ onFocus: (e) => {
834
+ if (primaryColor) e.currentTarget.style.borderColor = `${primaryColor}99`;
835
+ props.onFocus?.(e);
836
+ },
837
+ onBlur: (e) => {
838
+ if (primaryColor) e.currentTarget.style.borderColor = "";
839
+ props.onBlur?.(e);
840
+ }
697
841
  }
698
842
  );
699
843
  }
700
- function PrimaryButton({ children, loading, onClick }) {
844
+ function PrimaryButton({ children, loading, onClick, primaryColor }) {
701
845
  return /* @__PURE__ */ jsx2(
702
846
  "button",
703
847
  {
704
848
  type: onClick ? "button" : "submit",
705
849
  onClick,
706
850
  disabled: loading,
851
+ style: primaryColor ? { background: primaryColor, boxShadow: `0 10px 15px -3px ${primaryColor}33` } : void 0,
707
852
  className: clsx(
708
853
  "w-full flex items-center justify-center gap-2 px-4 py-2.5 rounded-xl",
709
- "text-sm font-semibold text-white",
710
- "bg-gradient-to-r from-purple-600 to-blue-600",
711
- "hover:from-purple-500 hover:to-blue-500",
712
- "shadow-lg shadow-purple-500/20",
713
- "transition-all duration-150",
714
- "disabled:opacity-60 disabled:cursor-not-allowed"
854
+ "text-sm font-semibold text-white transition-all duration-150",
855
+ "disabled:opacity-60 disabled:cursor-not-allowed",
856
+ !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"
715
857
  ),
716
858
  children: loading ? /* @__PURE__ */ jsx2(Spinner, {}) : children
717
859
  }
718
860
  );
719
861
  }
720
- function GhostButton({ children, onClick }) {
862
+ function GhostButton({ children, onClick, mutedColor }) {
721
863
  return /* @__PURE__ */ jsx2(
722
864
  "button",
723
865
  {
724
866
  type: "button",
725
867
  onClick,
726
- className: "w-full text-xs text-[#475569] hover:text-white/70 transition-colors py-1",
868
+ className: "w-full text-xs transition-colors py-1 hover:opacity-80",
869
+ style: { color: mutedColor ?? "#475569" },
727
870
  children
728
871
  }
729
872
  );
@@ -767,10 +910,10 @@ function GitHubButton({ onClick }) {
767
910
  function XButton({ onClick }) {
768
911
  return /* @__PURE__ */ jsx2(OAuthButton, { icon: /* @__PURE__ */ jsx2(XIcon, {}), label: "Continue with X", onClick });
769
912
  }
770
- function Divider() {
913
+ function Divider({ mutedColor }) {
771
914
  return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
772
915
  /* @__PURE__ */ jsx2("div", { className: "flex-1 h-px bg-white/8" }),
773
- /* @__PURE__ */ jsx2("span", { className: "text-[10px] text-[#475569] uppercase tracking-wider", children: "or" }),
916
+ /* @__PURE__ */ jsx2("span", { className: "text-[10px] uppercase tracking-wider", style: { color: mutedColor ?? "#475569" }, children: "or" }),
774
917
  /* @__PURE__ */ jsx2("div", { className: "flex-1 h-px bg-white/8" })
775
918
  ] });
776
919
  }
@@ -840,10 +983,16 @@ function GitHubIcon() {
840
983
  function XIcon() {
841
984
  return /* @__PURE__ */ jsx2("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "white", children: /* @__PURE__ */ jsx2("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" }) });
842
985
  }
986
+ function EnvelopeIcon({ color = "#a78bfa" }) {
987
+ return /* @__PURE__ */ jsxs("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: color, strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", children: [
988
+ /* @__PURE__ */ jsx2("rect", { x: "2", y: "4", width: "20", height: "16", rx: "2" }),
989
+ /* @__PURE__ */ jsx2("path", { d: "m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" })
990
+ ] });
991
+ }
843
992
 
844
993
  // src/components/SignUp.tsx
845
994
  import { clsx as clsx2 } from "clsx";
846
- import { useState as useState3 } from "react";
995
+ import { useState as useState4 } from "react";
847
996
  import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
848
997
  function resolveApiOrigin3(prop) {
849
998
  if (prop) return prop;
@@ -853,6 +1002,13 @@ function resolveApiOrigin3(prop) {
853
1002
  }
854
1003
  return "";
855
1004
  }
1005
+ function resolvePk2() {
1006
+ if (typeof document !== "undefined") {
1007
+ const el = document.querySelector("[data-vaultix-pk]");
1008
+ if (el) return el.getAttribute("data-vaultix-pk") ?? "";
1009
+ }
1010
+ return "";
1011
+ }
856
1012
  function resolveAfterSignUpUrl(redirectUrlProp) {
857
1013
  if (redirectUrlProp) return redirectUrlProp;
858
1014
  if (typeof document !== "undefined") {
@@ -870,16 +1026,22 @@ function SignUp({
870
1026
  onSuccess,
871
1027
  onError,
872
1028
  apiOrigin: apiOriginProp,
873
- className
1029
+ className,
1030
+ appearance
874
1031
  }) {
875
1032
  const apiOrigin = resolveApiOrigin3(apiOriginProp);
876
- const [step, setStep] = useState3("email");
877
- const [email, setEmail] = useState3("");
878
- const [password, setPassword] = useState3("");
879
- const [verificationCode, setVerificationCode] = useState3("");
880
- const [error, setError] = useState3(null);
881
- const [loading, setLoading] = useState3(false);
882
- const [registrationId, setRegistrationId] = useState3(null);
1033
+ const vars = appearance?.variables ?? {};
1034
+ const primaryColor = vars.colorPrimary;
1035
+ const cardBg = vars.colorBackground ?? "rgba(22,27,45,0.92)";
1036
+ const textColor = vars.colorText ?? "rgba(255,255,255,0.9)";
1037
+ const mutedColor = vars.colorTextMuted ?? "#475569";
1038
+ const [step, setStep] = useState4("email");
1039
+ const [email, setEmail] = useState4("");
1040
+ const [password, setPassword] = useState4("");
1041
+ const [verificationCode, setVerificationCode] = useState4("");
1042
+ const [error, setError] = useState4(null);
1043
+ const [loading, setLoading] = useState4(false);
1044
+ const [registrationId, setRegistrationId] = useState4(null);
883
1045
  function setErr(msg) {
884
1046
  setError(msg);
885
1047
  onError?.(msg);
@@ -891,10 +1053,12 @@ function SignUp({
891
1053
  url.searchParams.set("__vaultix_handshake", handshakeToken);
892
1054
  window.location.href = url.toString();
893
1055
  }
894
- function handleGoogleSignUp() {
1056
+ function oauthRedirect(provider) {
895
1057
  const target = resolveAfterSignUpUrl(redirectUrl);
1058
+ const pk = resolvePk2();
896
1059
  const params = new URLSearchParams({ redirect_url: target });
897
- window.location.href = `${apiOrigin}/api/v1/auth/oauth/google?${params}`;
1060
+ if (pk) params.set("pk", pk);
1061
+ window.location.href = `${apiOrigin}/api/v1/auth/oauth/${provider}?${params}`;
898
1062
  }
899
1063
  async function handleEmailPassword(e) {
900
1064
  e.preventDefault();
@@ -965,48 +1129,61 @@ function SignUp({
965
1129
  "w-full max-w-sm mx-auto rounded-2xl border border-white/8 shadow-2xl backdrop-blur-[16px]",
966
1130
  className
967
1131
  ),
968
- style: { background: "rgba(22,27,45,0.92)" },
1132
+ style: { background: cardBg },
969
1133
  children: [
970
1134
  /* @__PURE__ */ jsxs2("div", { className: "px-8 pt-8 pb-6 text-center", children: [
971
- /* @__PURE__ */ jsx3("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__ */ jsx3(SparkleIcon, {}) }),
972
- /* @__PURE__ */ jsx3("h1", { className: "text-xl font-semibold text-white/90", children: step === "verify" ? "Check your email" : "Create an account" }),
973
- /* @__PURE__ */ jsx3("p", { className: "text-sm text-[#475569] mt-1", children: step === "verify" ? `We sent a code to ${email}` : "Start your free trial today" })
1135
+ /* @__PURE__ */ jsx3(
1136
+ "div",
1137
+ {
1138
+ className: "inline-flex items-center justify-center w-10 h-10 rounded-xl shadow-lg mb-4",
1139
+ 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)" },
1140
+ children: /* @__PURE__ */ jsx3(SparkleIcon, {})
1141
+ }
1142
+ ),
1143
+ /* @__PURE__ */ jsx3("h1", { className: "text-xl font-semibold", style: { color: textColor }, children: step === "verify" ? "Check your email" : "Create an account" }),
1144
+ /* @__PURE__ */ jsx3("p", { className: "text-sm mt-1", style: { color: mutedColor }, children: step === "verify" ? `We sent a code to ${email}` : "Start your free trial today" })
974
1145
  ] }),
975
1146
  /* @__PURE__ */ jsxs2("div", { className: "px-8 pb-8 space-y-4", children: [
976
1147
  error && /* @__PURE__ */ jsx3("div", { className: "rounded-lg bg-red-500/10 border border-red-500/30 px-3 py-2", children: /* @__PURE__ */ jsx3("p", { className: "text-xs text-red-400", children: error }) }),
977
1148
  step === "email" && /* @__PURE__ */ jsxs2("div", { className: "space-y-3", children: [
978
- /* @__PURE__ */ jsx3(GoogleButton2, { onClick: handleGoogleSignUp }),
979
- /* @__PURE__ */ jsx3(Divider2, {}),
1149
+ /* @__PURE__ */ jsx3(OAuthButton2, { icon: /* @__PURE__ */ jsx3(GoogleIcon2, {}), label: "Continue with Google", onClick: () => oauthRedirect("google") }),
1150
+ /* @__PURE__ */ jsx3(OAuthButton2, { icon: /* @__PURE__ */ jsx3(GitHubIcon2, {}), label: "Continue with GitHub", onClick: () => oauthRedirect("github") }),
1151
+ /* @__PURE__ */ jsx3(OAuthButton2, { icon: /* @__PURE__ */ jsx3(MetaIcon2, {}), label: "Continue with Facebook", onClick: () => oauthRedirect("meta") }),
1152
+ /* @__PURE__ */ jsx3(OAuthButton2, { icon: /* @__PURE__ */ jsx3(LinkedInIcon2, {}), label: "Continue with LinkedIn", onClick: () => oauthRedirect("linkedin") }),
1153
+ /* @__PURE__ */ jsx3(OAuthButton2, { icon: /* @__PURE__ */ jsx3(XIcon2, {}), label: "Continue with X", onClick: () => oauthRedirect("x") }),
1154
+ /* @__PURE__ */ jsx3(Divider2, { mutedColor }),
980
1155
  /* @__PURE__ */ jsxs2("form", { onSubmit: handleEmailPassword, className: "space-y-3", children: [
981
1156
  /* @__PURE__ */ jsx3(
982
- SignUpInput,
1157
+ Input2,
983
1158
  {
984
1159
  type: "email",
985
1160
  placeholder: "you@company.com",
986
1161
  value: email,
987
1162
  onChange: setEmail,
988
1163
  autoFocus: true,
989
- required: true
1164
+ required: true,
1165
+ primaryColor
990
1166
  }
991
1167
  ),
992
1168
  /* @__PURE__ */ jsx3(
993
- SignUpInput,
1169
+ Input2,
994
1170
  {
995
1171
  type: "password",
996
1172
  placeholder: "Create a password",
997
1173
  value: password,
998
1174
  onChange: setPassword,
999
1175
  required: true,
1000
- minLength: 8
1176
+ minLength: 8,
1177
+ primaryColor
1001
1178
  }
1002
1179
  ),
1003
- /* @__PURE__ */ jsx3(PasswordStrength2, { password }),
1004
- /* @__PURE__ */ jsx3(SignUpPrimaryButton, { loading, children: "Create account" })
1180
+ /* @__PURE__ */ jsx3(PasswordStrength2, { password, primaryColor }),
1181
+ /* @__PURE__ */ jsx3(PrimaryButton2, { loading, primaryColor, children: "Create account" })
1005
1182
  ] })
1006
1183
  ] }),
1007
1184
  step === "verify" && /* @__PURE__ */ jsxs2("form", { onSubmit: handleVerification, className: "space-y-3", children: [
1008
1185
  /* @__PURE__ */ jsx3(
1009
- SignUpInput,
1186
+ Input2,
1010
1187
  {
1011
1188
  type: "text",
1012
1189
  inputMode: "numeric",
@@ -1017,16 +1194,18 @@ function SignUp({
1017
1194
  onChange: setVerificationCode,
1018
1195
  autoFocus: true,
1019
1196
  required: true,
1020
- className: "text-center tracking-[0.4em] text-lg"
1197
+ className: "text-center tracking-[0.4em] text-lg",
1198
+ primaryColor
1021
1199
  }
1022
1200
  ),
1023
- /* @__PURE__ */ jsx3(SignUpPrimaryButton, { loading, children: "Verify email" }),
1201
+ /* @__PURE__ */ jsx3(PrimaryButton2, { loading, primaryColor, children: "Verify email" }),
1024
1202
  /* @__PURE__ */ jsx3(
1025
1203
  "button",
1026
1204
  {
1027
1205
  type: "button",
1028
1206
  onClick: resendCode,
1029
- className: "w-full text-xs text-[#475569] hover:text-white/70 transition-colors py-1",
1207
+ className: "w-full text-xs transition-colors py-1 hover:opacity-80",
1208
+ style: { color: mutedColor },
1030
1209
  children: "Resend code"
1031
1210
  }
1032
1211
  )
@@ -1036,7 +1215,7 @@ function SignUp({
1036
1215
  }
1037
1216
  );
1038
1217
  }
1039
- function SignUpInput({ onChange, className, ...props }) {
1218
+ function Input2({ onChange, className, primaryColor, ...props }) {
1040
1219
  return /* @__PURE__ */ jsx3(
1041
1220
  "input",
1042
1221
  {
@@ -1045,34 +1224,47 @@ function SignUpInput({ onChange, className, ...props }) {
1045
1224
  className: clsx2(
1046
1225
  "w-full bg-white/5 border border-white/8 rounded-xl px-4 py-2.5",
1047
1226
  "text-sm text-white/90 placeholder:text-[#475569]",
1048
- "focus:outline-none focus:border-emerald-500/60 transition-colors",
1227
+ "focus:outline-none transition-colors",
1228
+ !primaryColor && "focus:border-emerald-500/60",
1049
1229
  className
1050
- )
1230
+ ),
1231
+ onFocus: (e) => {
1232
+ if (primaryColor) e.currentTarget.style.borderColor = `${primaryColor}99`;
1233
+ props.onFocus?.(e);
1234
+ },
1235
+ onBlur: (e) => {
1236
+ if (primaryColor) e.currentTarget.style.borderColor = "";
1237
+ props.onBlur?.(e);
1238
+ }
1051
1239
  }
1052
1240
  );
1053
1241
  }
1054
- function SignUpPrimaryButton({
1242
+ function PrimaryButton2({
1055
1243
  children,
1056
- loading
1244
+ loading,
1245
+ primaryColor
1057
1246
  }) {
1058
1247
  return /* @__PURE__ */ jsx3(
1059
1248
  "button",
1060
1249
  {
1061
1250
  type: "submit",
1062
1251
  disabled: loading,
1252
+ style: primaryColor ? { background: primaryColor, boxShadow: `0 10px 15px -3px ${primaryColor}33` } : void 0,
1063
1253
  className: clsx2(
1064
1254
  "w-full flex items-center justify-center gap-2 px-4 py-2.5 rounded-xl",
1065
- "text-sm font-semibold text-white",
1066
- "bg-gradient-to-r from-emerald-600 to-teal-600",
1067
- "hover:from-emerald-500 hover:to-teal-500",
1068
- "shadow-lg shadow-emerald-500/20 transition-all duration-150",
1069
- "disabled:opacity-60 disabled:cursor-not-allowed"
1255
+ "text-sm font-semibold text-white transition-all duration-150",
1256
+ "disabled:opacity-60 disabled:cursor-not-allowed",
1257
+ !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"
1070
1258
  ),
1071
1259
  children: loading ? /* @__PURE__ */ jsx3(Spinner2, {}) : children
1072
1260
  }
1073
1261
  );
1074
1262
  }
1075
- function GoogleButton2({ onClick }) {
1263
+ function OAuthButton2({
1264
+ icon,
1265
+ label,
1266
+ onClick
1267
+ }) {
1076
1268
  return /* @__PURE__ */ jsxs2(
1077
1269
  "button",
1078
1270
  {
@@ -1086,20 +1278,20 @@ function GoogleButton2({ onClick }) {
1086
1278
  "transition-all duration-150"
1087
1279
  ),
1088
1280
  children: [
1089
- /* @__PURE__ */ jsx3(GoogleIcon2, {}),
1090
- "Continue with Google"
1281
+ icon,
1282
+ label
1091
1283
  ]
1092
1284
  }
1093
1285
  );
1094
1286
  }
1095
- function Divider2() {
1287
+ function Divider2({ mutedColor }) {
1096
1288
  return /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-3", children: [
1097
1289
  /* @__PURE__ */ jsx3("div", { className: "flex-1 h-px bg-white/8" }),
1098
- /* @__PURE__ */ jsx3("span", { className: "text-[10px] text-[#475569] uppercase tracking-wider", children: "or" }),
1290
+ /* @__PURE__ */ jsx3("span", { className: "text-[10px] uppercase tracking-wider", style: { color: mutedColor ?? "#475569" }, children: "or" }),
1099
1291
  /* @__PURE__ */ jsx3("div", { className: "flex-1 h-px bg-white/8" })
1100
1292
  ] });
1101
1293
  }
1102
- function PasswordStrength2({ password }) {
1294
+ function PasswordStrength2({ password, primaryColor }) {
1103
1295
  const score = (() => {
1104
1296
  if (password.length === 0) return 0;
1105
1297
  let s = 0;
@@ -1111,15 +1303,17 @@ function PasswordStrength2({ password }) {
1111
1303
  })();
1112
1304
  if (password.length === 0) return null;
1113
1305
  const labels = ["", "Weak", "Fair", "Good", "Strong"];
1114
- const colors = ["", "bg-red-500", "bg-amber-400", "bg-blue-400", "bg-emerald-400"];
1306
+ const tailwindColors = ["", "bg-red-500", "bg-amber-400", "bg-blue-400", "bg-emerald-400"];
1115
1307
  return /* @__PURE__ */ jsxs2("div", { className: "space-y-1", children: [
1116
1308
  /* @__PURE__ */ jsx3("div", { className: "flex gap-1", children: [1, 2, 3, 4].map((i) => /* @__PURE__ */ jsx3(
1117
1309
  "div",
1118
1310
  {
1119
1311
  className: clsx2(
1120
1312
  "flex-1 h-0.5 rounded-full transition-all duration-300",
1121
- i <= score ? colors[score] : "bg-white/10"
1122
- )
1313
+ i <= score && !primaryColor ? tailwindColors[score] : void 0,
1314
+ i > score || !primaryColor && i <= score ? void 0 : void 0
1315
+ ),
1316
+ style: i <= score && primaryColor ? { background: score >= 4 ? primaryColor : void 0 } : { background: i <= score ? void 0 : "rgba(255,255,255,0.1)" }
1123
1317
  },
1124
1318
  i
1125
1319
  )) }),
@@ -1143,6 +1337,18 @@ function GoogleIcon2() {
1143
1337
  /* @__PURE__ */ jsx3("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" })
1144
1338
  ] });
1145
1339
  }
1340
+ function MetaIcon2() {
1341
+ return /* @__PURE__ */ jsx3("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "#1877F2", children: /* @__PURE__ */ jsx3("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" }) });
1342
+ }
1343
+ function LinkedInIcon2() {
1344
+ return /* @__PURE__ */ jsx3("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "#0A66C2", children: /* @__PURE__ */ jsx3("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" }) });
1345
+ }
1346
+ function GitHubIcon2() {
1347
+ return /* @__PURE__ */ jsx3("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "white", children: /* @__PURE__ */ jsx3("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" }) });
1348
+ }
1349
+ function XIcon2() {
1350
+ return /* @__PURE__ */ jsx3("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "white", children: /* @__PURE__ */ jsx3("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" }) });
1351
+ }
1146
1352
 
1147
1353
  // src/components/SignedIn.tsx
1148
1354
  import { Fragment, jsx as jsx4 } from "react/jsx-runtime";
@@ -1158,10 +1364,10 @@ function SignedOut({ children }) {
1158
1364
  }
1159
1365
 
1160
1366
  // src/components/RedirectComponents.tsx
1161
- import { useEffect as useEffect2 } from "react";
1367
+ import { useEffect as useEffect3 } from "react";
1162
1368
  function RedirectToSignIn({ redirectUrl = "/sign-in" }) {
1163
1369
  const { isLoaded, isSignedIn } = useVaultixContext();
1164
- useEffect2(() => {
1370
+ useEffect3(() => {
1165
1371
  if (isLoaded && !isSignedIn) {
1166
1372
  window.location.href = redirectUrl;
1167
1373
  }
@@ -1170,7 +1376,7 @@ function RedirectToSignIn({ redirectUrl = "/sign-in" }) {
1170
1376
  }
1171
1377
  function RedirectToSignUp({ redirectUrl = "/sign-up" }) {
1172
1378
  const { isLoaded, isSignedIn } = useVaultixContext();
1173
- useEffect2(() => {
1379
+ useEffect3(() => {
1174
1380
  if (isLoaded && !isSignedIn) {
1175
1381
  window.location.href = redirectUrl;
1176
1382
  }
@@ -1180,22 +1386,17 @@ function RedirectToSignUp({ redirectUrl = "/sign-up" }) {
1180
1386
 
1181
1387
  // src/components/UserButton.tsx
1182
1388
  import { clsx as clsx3 } from "clsx";
1183
- import { useEffect as useEffect3, useRef as useRef3, useState as useState4 } from "react";
1184
- import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
1185
- function UserButton({
1186
- showName = false,
1187
- afterSignOutUrl,
1188
- className
1189
- }) {
1389
+ import { useCallback as useCallback3, useEffect as useEffect4, useRef as useRef3, useState as useState5 } from "react";
1390
+ import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
1391
+ function UserButton({ showName = false, afterSignOutUrl, className }) {
1190
1392
  const { user, session, isLoaded, isSignedIn, signOut } = useVaultixContext();
1191
- const [open, setOpen] = useState4(false);
1192
- const [signingOut, setSigningOut] = useState4(false);
1393
+ const [open, setOpen] = useState5(false);
1394
+ const [signingOut, setSigningOut] = useState5(false);
1395
+ const [showProfile, setShowProfile] = useState5(false);
1193
1396
  const containerRef = useRef3(null);
1194
- useEffect3(() => {
1397
+ useEffect4(() => {
1195
1398
  function onPointerDown(e) {
1196
- if (containerRef.current && !containerRef.current.contains(e.target)) {
1197
- setOpen(false);
1198
- }
1399
+ if (containerRef.current && !containerRef.current.contains(e.target)) setOpen(false);
1199
1400
  }
1200
1401
  document.addEventListener("pointerdown", onPointerDown);
1201
1402
  return () => document.removeEventListener("pointerdown", onPointerDown);
@@ -1208,98 +1409,456 @@ function UserButton({
1208
1409
  await signOut();
1209
1410
  if (afterSignOutUrl) window.location.href = afterSignOutUrl;
1210
1411
  }
1211
- return /* @__PURE__ */ jsxs3("div", { ref: containerRef, className: clsx3("relative", className), children: [
1212
- /* @__PURE__ */ jsxs3(
1412
+ return /* @__PURE__ */ jsxs3(Fragment2, { children: [
1413
+ /* @__PURE__ */ jsxs3("div", { ref: containerRef, className: clsx3("relative", className), children: [
1414
+ /* @__PURE__ */ jsxs3(
1415
+ "button",
1416
+ {
1417
+ onClick: () => setOpen((o) => !o),
1418
+ className: "flex items-center gap-2 rounded-xl p-1 hover:bg-white/8 transition-colors",
1419
+ children: [
1420
+ /* @__PURE__ */ jsx5(Avatar, { initials, imageUrl: user.imageUrl }),
1421
+ showName && /* @__PURE__ */ jsx5("span", { className: "text-sm font-medium text-white/90 pr-1", children: user.firstName ?? user.email })
1422
+ ]
1423
+ }
1424
+ ),
1425
+ open && /* @__PURE__ */ jsxs3(
1426
+ "div",
1427
+ {
1428
+ className: clsx3("absolute right-0 mt-2 w-64 rounded-2xl border border-white/8", "backdrop-blur-[16px] shadow-2xl shadow-black/40 z-50"),
1429
+ style: { background: "rgba(22,27,45,0.96)" },
1430
+ children: [
1431
+ /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-3 px-4 py-3 border-b border-white/8", children: [
1432
+ /* @__PURE__ */ jsx5(Avatar, { initials, imageUrl: user.imageUrl, size: "lg" }),
1433
+ /* @__PURE__ */ jsxs3("div", { className: "flex-1 min-w-0", children: [
1434
+ (user.firstName || user.lastName) && /* @__PURE__ */ jsx5("p", { className: "text-sm font-semibold text-white/90 truncate", children: [user.firstName, user.lastName].filter(Boolean).join(" ") }),
1435
+ /* @__PURE__ */ jsx5("p", { className: "text-xs text-[#475569] truncate", children: user.email })
1436
+ ] })
1437
+ ] }),
1438
+ session && /* @__PURE__ */ jsx5("div", { className: "px-4 py-2 border-b border-white/8", children: /* @__PURE__ */ jsxs3("div", { className: "flex items-center justify-between", children: [
1439
+ /* @__PURE__ */ jsx5("span", { className: "text-[10px] uppercase tracking-widest text-[#475569]", children: "Risk level" }),
1440
+ /* @__PURE__ */ jsx5(RiskBadge, { level: session.riskLevel })
1441
+ ] }) }),
1442
+ /* @__PURE__ */ jsxs3("div", { className: "p-2", children: [
1443
+ /* @__PURE__ */ jsx5(DropdownItem, { label: "Manage account", icon: /* @__PURE__ */ jsx5(UserIcon, {}), onClick: () => {
1444
+ setOpen(false);
1445
+ setShowProfile(true);
1446
+ } }),
1447
+ /* @__PURE__ */ jsx5("div", { className: "h-px bg-white/8 my-1" }),
1448
+ /* @__PURE__ */ jsx5(DropdownItem, { label: signingOut ? "Signing out\u2026" : "Sign out", icon: /* @__PURE__ */ jsx5(SignOutIcon, {}), onClick: handleSignOut, destructive: true })
1449
+ ] })
1450
+ ]
1451
+ }
1452
+ )
1453
+ ] }),
1454
+ showProfile && /* @__PURE__ */ jsx5(ProfileModal, { onClose: () => setShowProfile(false) })
1455
+ ] });
1456
+ }
1457
+ function ProfileModal({ onClose }) {
1458
+ const { user, updateUser } = useVaultixContext();
1459
+ const [tab, setTab] = useState5("profile");
1460
+ const [firstName, setFirstName] = useState5(user?.firstName ?? "");
1461
+ const [lastName, setLastName] = useState5(user?.lastName ?? "");
1462
+ const [imageUrl, setImageUrl] = useState5(user?.imageUrl ?? "");
1463
+ const [currentPassword, setCurrentPassword] = useState5("");
1464
+ const [newPassword, setNewPassword] = useState5("");
1465
+ const [confirmPassword, setConfirmPassword] = useState5("");
1466
+ const [saving, setSaving] = useState5(false);
1467
+ const [error, setError] = useState5(null);
1468
+ const [success, setSuccess] = useState5(null);
1469
+ useEffect4(() => {
1470
+ function onKey(e) {
1471
+ if (e.key === "Escape") onClose();
1472
+ }
1473
+ document.addEventListener("keydown", onKey);
1474
+ return () => document.removeEventListener("keydown", onKey);
1475
+ }, [onClose]);
1476
+ async function saveProfile(e) {
1477
+ e.preventDefault();
1478
+ setError(null);
1479
+ setSuccess(null);
1480
+ setSaving(true);
1481
+ try {
1482
+ await updateUser({ firstName: firstName.trim() || null, lastName: lastName.trim() || null, imageUrl: imageUrl.trim() || null });
1483
+ setSuccess("Profile updated.");
1484
+ } catch (err) {
1485
+ setError(err instanceof Error ? err.message : "Update failed");
1486
+ } finally {
1487
+ setSaving(false);
1488
+ }
1489
+ }
1490
+ async function savePassword(e) {
1491
+ e.preventDefault();
1492
+ setError(null);
1493
+ setSuccess(null);
1494
+ if (newPassword !== confirmPassword) {
1495
+ setError("Passwords do not match.");
1496
+ return;
1497
+ }
1498
+ if (newPassword.length < 8) {
1499
+ setError("Password must be at least 8 characters.");
1500
+ return;
1501
+ }
1502
+ setSaving(true);
1503
+ try {
1504
+ await updateUser({ currentPassword, newPassword });
1505
+ setSuccess("Password changed.");
1506
+ setCurrentPassword("");
1507
+ setNewPassword("");
1508
+ setConfirmPassword("");
1509
+ } catch (err) {
1510
+ setError(err instanceof Error ? err.message : "Update failed");
1511
+ } finally {
1512
+ setSaving(false);
1513
+ }
1514
+ }
1515
+ const TABS = [
1516
+ { id: "profile", label: "Profile" },
1517
+ { id: "password", label: "Change password" },
1518
+ { id: "security", label: "Security" }
1519
+ ];
1520
+ return /* @__PURE__ */ jsx5(
1521
+ "div",
1522
+ {
1523
+ className: "fixed inset-0 z-[9999] flex items-center justify-center p-4",
1524
+ style: { background: "rgba(0,0,0,0.6)", backdropFilter: "blur(4px)" },
1525
+ onPointerDown: (e) => {
1526
+ if (e.target === e.currentTarget) onClose();
1527
+ },
1528
+ children: /* @__PURE__ */ jsxs3("div", { className: "w-full max-w-md rounded-2xl border border-white/8 shadow-2xl", style: { background: "rgba(22,27,45,0.98)" }, children: [
1529
+ /* @__PURE__ */ jsxs3("div", { className: "flex items-center justify-between px-6 py-4 border-b border-white/8", children: [
1530
+ /* @__PURE__ */ jsx5("h2", { className: "text-sm font-semibold text-white/90", children: "Manage account" }),
1531
+ /* @__PURE__ */ jsx5("button", { onClick: onClose, className: "text-[#475569] hover:text-white transition-colors", children: /* @__PURE__ */ jsx5(CloseIcon, {}) })
1532
+ ] }),
1533
+ /* @__PURE__ */ jsx5("div", { className: "flex gap-1 px-6 pt-4 flex-wrap", children: TABS.map(({ id, label }) => /* @__PURE__ */ jsx5(
1534
+ "button",
1535
+ {
1536
+ onClick: () => {
1537
+ setTab(id);
1538
+ setError(null);
1539
+ setSuccess(null);
1540
+ },
1541
+ className: clsx3(
1542
+ "px-3 py-1.5 rounded-lg text-xs font-medium transition-colors",
1543
+ tab === id ? "bg-purple-500/15 text-purple-300 border border-purple-500/30" : "text-[#475569] hover:text-white/70"
1544
+ ),
1545
+ children: label
1546
+ },
1547
+ id
1548
+ )) }),
1549
+ /* @__PURE__ */ jsxs3("div", { className: "px-6 pt-3", children: [
1550
+ error && /* @__PURE__ */ jsx5("div", { className: "rounded-lg bg-red-500/10 border border-red-500/30 px-3 py-2 text-xs text-red-400", children: error }),
1551
+ success && /* @__PURE__ */ jsx5("div", { className: "rounded-lg bg-emerald-500/10 border border-emerald-500/30 px-3 py-2 text-xs text-emerald-400", children: success })
1552
+ ] }),
1553
+ tab === "profile" && /* @__PURE__ */ jsxs3("form", { onSubmit: saveProfile, className: "px-6 py-4 space-y-4", children: [
1554
+ /* @__PURE__ */ jsxs3("div", { className: "grid grid-cols-2 gap-3", children: [
1555
+ /* @__PURE__ */ jsx5(ModalField, { label: "First name", value: firstName, onChange: setFirstName, placeholder: "Jane" }),
1556
+ /* @__PURE__ */ jsx5(ModalField, { label: "Last name", value: lastName, onChange: setLastName, placeholder: "Smith" })
1557
+ ] }),
1558
+ /* @__PURE__ */ jsx5(ModalField, { label: "Avatar URL", value: imageUrl, onChange: setImageUrl, placeholder: "https://\u2026", type: "url" }),
1559
+ /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-3 pt-1", children: [
1560
+ /* @__PURE__ */ jsx5(SaveButton, { saving }),
1561
+ /* @__PURE__ */ jsx5("button", { type: "button", onClick: onClose, className: "text-sm text-[#475569] hover:text-white/70 transition-colors", children: "Cancel" })
1562
+ ] })
1563
+ ] }),
1564
+ tab === "password" && /* @__PURE__ */ jsxs3("form", { onSubmit: savePassword, className: "px-6 py-4 space-y-4", children: [
1565
+ /* @__PURE__ */ jsx5(ModalField, { label: "Current password", value: currentPassword, onChange: setCurrentPassword, type: "password", placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" }),
1566
+ /* @__PURE__ */ jsx5(ModalField, { label: "New password", value: newPassword, onChange: setNewPassword, type: "password", placeholder: "Min 8 characters" }),
1567
+ /* @__PURE__ */ jsx5(ModalField, { label: "Confirm new password", value: confirmPassword, onChange: setConfirmPassword, type: "password", placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" }),
1568
+ /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-3 pt-1", children: [
1569
+ /* @__PURE__ */ jsx5(SaveButton, { saving, label: "Change password" }),
1570
+ /* @__PURE__ */ jsx5("button", { type: "button", onClick: onClose, className: "text-sm text-[#475569] hover:text-white/70 transition-colors", children: "Cancel" })
1571
+ ] })
1572
+ ] }),
1573
+ tab === "security" && /* @__PURE__ */ jsx5(SecurityTab, {})
1574
+ ] })
1575
+ }
1576
+ );
1577
+ }
1578
+ function SecurityTab() {
1579
+ const { apiOrigin } = useVaultixContext();
1580
+ const [totpEnabled, setTotpEnabled] = useState5(null);
1581
+ const [backupCodesLeft, setBackupCodesLeft] = useState5(null);
1582
+ const [enrollStep, setEnrollStep] = useState5("idle");
1583
+ const [qrDataUrl, setQrDataUrl] = useState5("");
1584
+ const [manualKey, setManualKey] = useState5("");
1585
+ const [enrollCode, setEnrollCode] = useState5("");
1586
+ const [disableCode, setDisableCode] = useState5("");
1587
+ const [backupCodes, setBackupCodes] = useState5([]);
1588
+ const [loading, setLoading] = useState5(false);
1589
+ const [error, setError] = useState5(null);
1590
+ const [showDisable, setShowDisable] = useState5(false);
1591
+ const fetchStatus = useCallback3(async () => {
1592
+ const res = await fetch(`${apiOrigin}/api/v1/auth/totp/status`, { credentials: "include" });
1593
+ if (res.ok) {
1594
+ const d = await res.json();
1595
+ setTotpEnabled(d.enabled);
1596
+ setBackupCodesLeft(d.backup_codes_remaining);
1597
+ }
1598
+ }, [apiOrigin]);
1599
+ useEffect4(() => {
1600
+ void fetchStatus();
1601
+ }, [fetchStatus]);
1602
+ async function startEnroll() {
1603
+ setError(null);
1604
+ setLoading(true);
1605
+ try {
1606
+ const res = await fetch(`${apiOrigin}/api/v1/auth/totp/enroll`, { method: "POST", credentials: "include" });
1607
+ const d = await res.json();
1608
+ if (!res.ok) {
1609
+ setError(d.error ?? "Failed to start enrollment.");
1610
+ return;
1611
+ }
1612
+ setQrDataUrl(d.qr_data_url);
1613
+ setManualKey(d.manual_entry_key);
1614
+ setEnrollStep("qr");
1615
+ } finally {
1616
+ setLoading(false);
1617
+ }
1618
+ }
1619
+ async function verifyEnroll(e) {
1620
+ e.preventDefault();
1621
+ setError(null);
1622
+ setLoading(true);
1623
+ try {
1624
+ const res = await fetch(`${apiOrigin}/api/v1/auth/totp/verify-enroll`, {
1625
+ method: "POST",
1626
+ credentials: "include",
1627
+ headers: { "Content-Type": "application/json" },
1628
+ body: JSON.stringify({ code: enrollCode })
1629
+ });
1630
+ const d = await res.json();
1631
+ if (!res.ok) {
1632
+ setError(d.error ?? "Invalid code. Try again.");
1633
+ return;
1634
+ }
1635
+ setBackupCodes(d.backup_codes);
1636
+ setEnrollStep("backup-codes");
1637
+ setTotpEnabled(true);
1638
+ setBackupCodesLeft(d.backup_codes.length);
1639
+ } finally {
1640
+ setLoading(false);
1641
+ }
1642
+ }
1643
+ async function disableTotp(e) {
1644
+ e.preventDefault();
1645
+ setError(null);
1646
+ setLoading(true);
1647
+ try {
1648
+ const res = await fetch(`${apiOrigin}/api/v1/auth/totp`, {
1649
+ method: "DELETE",
1650
+ credentials: "include",
1651
+ headers: { "Content-Type": "application/json" },
1652
+ body: JSON.stringify({ code: disableCode })
1653
+ });
1654
+ const d = await res.json();
1655
+ if (!res.ok) {
1656
+ setError(d.error ?? "Invalid code.");
1657
+ return;
1658
+ }
1659
+ setTotpEnabled(false);
1660
+ setBackupCodesLeft(null);
1661
+ setShowDisable(false);
1662
+ setDisableCode("");
1663
+ } finally {
1664
+ setLoading(false);
1665
+ }
1666
+ }
1667
+ if (totpEnabled === null) {
1668
+ return /* @__PURE__ */ jsx5("div", { className: "px-6 py-8 text-center text-[#475569] text-sm", children: "Loading\u2026" });
1669
+ }
1670
+ if (enrollStep === "backup-codes") {
1671
+ return /* @__PURE__ */ jsxs3("div", { className: "px-6 py-4 space-y-4", children: [
1672
+ /* @__PURE__ */ jsxs3("div", { className: "rounded-xl bg-emerald-500/10 border border-emerald-500/30 px-4 py-3", children: [
1673
+ /* @__PURE__ */ jsx5("p", { className: "text-xs font-semibold text-emerald-400 mb-1", children: "Authenticator app enabled!" }),
1674
+ /* @__PURE__ */ jsx5("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." })
1675
+ ] }),
1676
+ /* @__PURE__ */ jsx5("div", { className: "grid grid-cols-2 gap-1.5", children: backupCodes.map((c) => /* @__PURE__ */ jsx5("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)) }),
1677
+ /* @__PURE__ */ jsx5(
1678
+ "button",
1679
+ {
1680
+ onClick: () => {
1681
+ const t = backupCodes.join("\n");
1682
+ navigator.clipboard.writeText(t).catch(() => {
1683
+ });
1684
+ },
1685
+ className: "w-full text-xs text-[#475569] hover:text-white/70 transition-colors py-1 border border-white/8 rounded-lg",
1686
+ children: "Copy all codes"
1687
+ }
1688
+ ),
1689
+ /* @__PURE__ */ jsx5(
1690
+ "button",
1691
+ {
1692
+ onClick: () => setEnrollStep("idle"),
1693
+ 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",
1694
+ children: "Done"
1695
+ }
1696
+ )
1697
+ ] });
1698
+ }
1699
+ if (enrollStep === "qr") {
1700
+ return /* @__PURE__ */ jsxs3("div", { className: "px-6 py-4 space-y-4", children: [
1701
+ /* @__PURE__ */ jsx5("p", { className: "text-xs text-[#475569]", children: "Scan this QR code with Google Authenticator, Authy, or any TOTP app." }),
1702
+ qrDataUrl && /* @__PURE__ */ jsx5("div", { className: "flex justify-center", children: /* @__PURE__ */ jsx5("img", { src: qrDataUrl, alt: "TOTP QR code", className: "rounded-xl", width: 180, height: 180 }) }),
1703
+ /* @__PURE__ */ jsxs3("details", { className: "text-[11px] text-[#475569]", children: [
1704
+ /* @__PURE__ */ jsx5("summary", { className: "cursor-pointer hover:text-white/60", children: "Can't scan? Enter manually" }),
1705
+ /* @__PURE__ */ jsx5("p", { className: "mt-1 font-mono break-all bg-white/5 rounded px-2 py-1 text-white/60 select-all", children: manualKey })
1706
+ ] }),
1707
+ error && /* @__PURE__ */ jsx5("div", { className: "rounded-lg bg-red-500/10 border border-red-500/30 px-3 py-2 text-xs text-red-400", children: error }),
1708
+ /* @__PURE__ */ jsxs3("form", { onSubmit: verifyEnroll, className: "space-y-3", children: [
1709
+ /* @__PURE__ */ jsx5(
1710
+ "input",
1711
+ {
1712
+ type: "text",
1713
+ inputMode: "numeric",
1714
+ pattern: "[0-9]{6}",
1715
+ maxLength: 6,
1716
+ placeholder: "000000",
1717
+ value: enrollCode,
1718
+ onChange: (e) => setEnrollCode(e.target.value),
1719
+ autoFocus: true,
1720
+ required: true,
1721
+ 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]"
1722
+ }
1723
+ ),
1724
+ /* @__PURE__ */ jsx5(
1725
+ "button",
1726
+ {
1727
+ type: "submit",
1728
+ disabled: loading || enrollCode.length !== 6,
1729
+ 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",
1730
+ children: loading ? "Verifying\u2026" : "Verify and enable"
1731
+ }
1732
+ ),
1733
+ /* @__PURE__ */ jsx5(
1734
+ "button",
1735
+ {
1736
+ type: "button",
1737
+ onClick: () => {
1738
+ setEnrollStep("idle");
1739
+ setError(null);
1740
+ },
1741
+ className: "w-full text-xs text-[#475569] hover:text-white/70 transition-colors py-1",
1742
+ children: "\u2190 Cancel"
1743
+ }
1744
+ )
1745
+ ] })
1746
+ ] });
1747
+ }
1748
+ return /* @__PURE__ */ jsxs3("div", { className: "px-6 py-4 space-y-4", children: [
1749
+ /* @__PURE__ */ jsx5("div", { className: "rounded-xl border border-white/8 p-4", style: { background: "rgba(255,255,255,0.03)" }, children: /* @__PURE__ */ jsxs3("div", { className: "flex items-center justify-between", children: [
1750
+ /* @__PURE__ */ jsxs3("div", { children: [
1751
+ /* @__PURE__ */ jsx5("p", { className: "text-sm font-medium text-white/90", children: "Authenticator app" }),
1752
+ /* @__PURE__ */ jsx5("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" })
1753
+ ] }),
1754
+ /* @__PURE__ */ jsx5("span", { className: clsx3(
1755
+ "text-[10px] font-semibold uppercase tracking-wide px-2 py-0.5 rounded-full border",
1756
+ totpEnabled ? "bg-emerald-500/15 text-emerald-400 border-emerald-500/30" : "bg-white/5 text-[#475569] border-white/8"
1757
+ ), children: totpEnabled ? "On" : "Off" })
1758
+ ] }) }),
1759
+ error && /* @__PURE__ */ jsx5("div", { className: "rounded-lg bg-red-500/10 border border-red-500/30 px-3 py-2 text-xs text-red-400", children: error }),
1760
+ !totpEnabled && /* @__PURE__ */ jsx5(
1213
1761
  "button",
1214
1762
  {
1215
- onClick: () => setOpen((o) => !o),
1216
- className: "flex items-center gap-2 rounded-xl p-1 hover:bg-white/8 transition-colors",
1217
- children: [
1218
- /* @__PURE__ */ jsx5(Avatar, { initials, imageUrl: user.imageUrl }),
1219
- showName && /* @__PURE__ */ jsx5("span", { className: "text-sm font-medium text-white/90 pr-1", children: user.firstName ?? user.email })
1220
- ]
1763
+ onClick: startEnroll,
1764
+ disabled: loading,
1765
+ 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",
1766
+ children: loading ? "Loading\u2026" : "Enable authenticator app"
1221
1767
  }
1222
1768
  ),
1223
- open && /* @__PURE__ */ jsxs3(
1224
- "div",
1769
+ totpEnabled && !showDisable && /* @__PURE__ */ jsx5(
1770
+ "button",
1225
1771
  {
1226
- className: clsx3(
1227
- "absolute right-0 mt-2 w-64 rounded-2xl border border-white/8",
1228
- "backdrop-blur-[16px] shadow-2xl shadow-black/40 z-50"
1772
+ onClick: () => setShowDisable(true),
1773
+ 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",
1774
+ children: "Disable authenticator app"
1775
+ }
1776
+ ),
1777
+ totpEnabled && showDisable && /* @__PURE__ */ jsxs3("form", { onSubmit: disableTotp, className: "space-y-3", children: [
1778
+ /* @__PURE__ */ jsx5("p", { className: "text-[11px] text-[#475569]", children: "Enter your current 6-digit code (or a backup code) to confirm." }),
1779
+ /* @__PURE__ */ jsx5(
1780
+ "input",
1781
+ {
1782
+ type: "text",
1783
+ placeholder: "000000 or backup code",
1784
+ value: disableCode,
1785
+ onChange: (e) => setDisableCode(e.target.value),
1786
+ autoFocus: true,
1787
+ required: true,
1788
+ 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"
1789
+ }
1790
+ ),
1791
+ /* @__PURE__ */ jsxs3("div", { className: "flex gap-2", children: [
1792
+ /* @__PURE__ */ jsx5(
1793
+ "button",
1794
+ {
1795
+ type: "submit",
1796
+ disabled: loading || !disableCode,
1797
+ 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",
1798
+ children: loading ? "Disabling\u2026" : "Confirm disable"
1799
+ }
1229
1800
  ),
1230
- style: { background: "rgba(22,27,45,0.96)" },
1231
- children: [
1232
- /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-3 px-4 py-3 border-b border-white/8", children: [
1233
- /* @__PURE__ */ jsx5(Avatar, { initials, imageUrl: user.imageUrl, size: "lg" }),
1234
- /* @__PURE__ */ jsxs3("div", { className: "flex-1 min-w-0", children: [
1235
- (user.firstName || user.lastName) && /* @__PURE__ */ jsx5("p", { className: "text-sm font-semibold text-white/90 truncate", children: [user.firstName, user.lastName].filter(Boolean).join(" ") }),
1236
- /* @__PURE__ */ jsx5("p", { className: "text-xs text-[#475569] truncate", children: user.email })
1237
- ] })
1238
- ] }),
1239
- session && /* @__PURE__ */ jsx5("div", { className: "px-4 py-2 border-b border-white/8", children: /* @__PURE__ */ jsxs3("div", { className: "flex items-center justify-between", children: [
1240
- /* @__PURE__ */ jsx5("span", { className: "text-[10px] uppercase tracking-widest text-[#475569]", children: "Risk level" }),
1241
- /* @__PURE__ */ jsx5(RiskBadge, { level: session.riskLevel })
1242
- ] }) }),
1243
- /* @__PURE__ */ jsxs3("div", { className: "p-2", children: [
1244
- /* @__PURE__ */ jsx5(
1245
- DropdownItem,
1246
- {
1247
- label: "Manage account",
1248
- icon: /* @__PURE__ */ jsx5(UserIcon, {}),
1249
- onClick: () => setOpen(false)
1250
- }
1251
- ),
1252
- /* @__PURE__ */ jsx5(
1253
- DropdownItem,
1254
- {
1255
- label: "Security settings",
1256
- icon: /* @__PURE__ */ jsx5(ShieldIcon, {}),
1257
- onClick: () => setOpen(false)
1258
- }
1259
- ),
1260
- /* @__PURE__ */ jsx5("div", { className: "h-px bg-white/8 my-1" }),
1261
- /* @__PURE__ */ jsx5(
1262
- DropdownItem,
1263
- {
1264
- label: signingOut ? "Signing out\u2026" : "Sign out",
1265
- icon: /* @__PURE__ */ jsx5(SignOutIcon, {}),
1266
- onClick: handleSignOut,
1267
- destructive: true
1268
- }
1269
- )
1270
- ] })
1271
- ]
1801
+ /* @__PURE__ */ jsx5(
1802
+ "button",
1803
+ {
1804
+ type: "button",
1805
+ onClick: () => {
1806
+ setShowDisable(false);
1807
+ setError(null);
1808
+ setDisableCode("");
1809
+ },
1810
+ className: "px-4 py-2 rounded-xl text-xs text-[#475569] hover:text-white hover:bg-white/8 border border-white/8 transition-colors",
1811
+ children: "Cancel"
1812
+ }
1813
+ )
1814
+ ] })
1815
+ ] })
1816
+ ] });
1817
+ }
1818
+ function ModalField({ label, value, onChange, placeholder, type = "text" }) {
1819
+ return /* @__PURE__ */ jsxs3("div", { className: "space-y-1", children: [
1820
+ /* @__PURE__ */ jsx5("label", { className: "text-[10px] uppercase tracking-widest text-[#475569]", children: label }),
1821
+ /* @__PURE__ */ jsx5(
1822
+ "input",
1823
+ {
1824
+ type,
1825
+ value,
1826
+ onChange: (e) => onChange(e.target.value),
1827
+ placeholder,
1828
+ className: clsx3(
1829
+ "w-full bg-white/5 border border-white/8 rounded-xl px-3 py-2 text-sm text-white/90",
1830
+ "placeholder:text-[#475569] focus:outline-none focus:border-purple-500/60 transition-colors"
1831
+ )
1272
1832
  }
1273
1833
  )
1274
1834
  ] });
1275
1835
  }
1276
- function Avatar({ initials, imageUrl, size = "sm" }) {
1277
- const dim = size === "lg" ? "w-9 h-9 text-sm" : "w-8 h-8 text-xs";
1278
- if (imageUrl) {
1279
- return (
1280
- // eslint-disable-next-line @next/next/no-img-element
1281
- /* @__PURE__ */ jsx5(
1282
- "img",
1283
- {
1284
- src: imageUrl,
1285
- alt: "",
1286
- className: clsx3(dim, "rounded-full object-cover ring-2 ring-white/10")
1287
- }
1288
- )
1289
- );
1290
- }
1836
+ function SaveButton({ saving, label = "Save changes" }) {
1291
1837
  return /* @__PURE__ */ jsx5(
1292
- "div",
1838
+ "button",
1293
1839
  {
1840
+ type: "submit",
1841
+ disabled: saving,
1294
1842
  className: clsx3(
1295
- dim,
1296
- "rounded-full flex items-center justify-center font-semibold text-white",
1297
- "bg-gradient-to-br from-purple-600 to-blue-600 ring-2 ring-white/10"
1843
+ "px-4 py-2 rounded-xl text-sm font-semibold text-white transition-all",
1844
+ "bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-500 hover:to-blue-500",
1845
+ "shadow-lg shadow-purple-500/20 disabled:opacity-60"
1298
1846
  ),
1299
- children: initials
1847
+ children: saving ? "Saving\u2026" : label
1300
1848
  }
1301
1849
  );
1302
1850
  }
1851
+ function Avatar({ initials, imageUrl, size = "sm" }) {
1852
+ const dim = size === "lg" ? "w-9 h-9 text-sm" : "w-8 h-8 text-xs";
1853
+ if (imageUrl) {
1854
+ return /* @__PURE__ */ jsx5("img", { src: imageUrl, alt: "", className: clsx3(dim, "rounded-full object-cover ring-2 ring-white/10") });
1855
+ }
1856
+ return /* @__PURE__ */ jsx5("div", { className: clsx3(
1857
+ dim,
1858
+ "rounded-full flex items-center justify-center font-semibold text-white",
1859
+ "bg-gradient-to-br from-purple-600 to-blue-600 ring-2 ring-white/10"
1860
+ ), children: initials });
1861
+ }
1303
1862
  function AvatarSkeleton() {
1304
1863
  return /* @__PURE__ */ jsx5("div", { className: "w-8 h-8 rounded-full bg-white/5 animate-pulse" });
1305
1864
  }
@@ -1310,16 +1869,10 @@ function RiskBadge({ level }) {
1310
1869
  high: "bg-orange-500/15 text-orange-400 border-orange-500/30",
1311
1870
  critical: "bg-red-500/15 text-red-400 border-red-500/30"
1312
1871
  };
1313
- return /* @__PURE__ */ jsx5(
1314
- "span",
1315
- {
1316
- className: clsx3(
1317
- "inline-flex items-center text-[10px] font-semibold uppercase tracking-wider px-2 py-0.5 rounded-full border",
1318
- styles[level] ?? styles.low
1319
- ),
1320
- children: level
1321
- }
1322
- );
1872
+ return /* @__PURE__ */ jsx5("span", { className: clsx3(
1873
+ "inline-flex items-center text-[10px] font-semibold uppercase tracking-wider px-2 py-0.5 rounded-full border",
1874
+ styles[level] ?? styles.low
1875
+ ), children: level });
1323
1876
  }
1324
1877
  function DropdownItem({ label, icon, onClick, destructive }) {
1325
1878
  return /* @__PURE__ */ jsxs3(
@@ -1343,9 +1896,6 @@ function UserIcon() {
1343
1896
  /* @__PURE__ */ jsx5("circle", { cx: "12", cy: "7", r: "4" })
1344
1897
  ] });
1345
1898
  }
1346
- function ShieldIcon() {
1347
- return /* @__PURE__ */ jsx5("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx5("path", { d: "M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" }) });
1348
- }
1349
1899
  function SignOutIcon() {
1350
1900
  return /* @__PURE__ */ jsxs3("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
1351
1901
  /* @__PURE__ */ jsx5("path", { d: "M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" }),
@@ -1353,10 +1903,16 @@ function SignOutIcon() {
1353
1903
  /* @__PURE__ */ jsx5("line", { x1: "21", y1: "12", x2: "9", y2: "12" })
1354
1904
  ] });
1355
1905
  }
1906
+ function CloseIcon() {
1907
+ return /* @__PURE__ */ jsxs3("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
1908
+ /* @__PURE__ */ jsx5("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
1909
+ /* @__PURE__ */ jsx5("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
1910
+ ] });
1911
+ }
1356
1912
 
1357
1913
  // src/components/OrganizationSwitcher.tsx
1358
1914
  import { clsx as clsx4 } from "clsx";
1359
- import { useEffect as useEffect4, useRef as useRef4, useState as useState5 } from "react";
1915
+ import { useEffect as useEffect5, useRef as useRef4, useState as useState6 } from "react";
1360
1916
  import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
1361
1917
  function resolveApiOrigin4() {
1362
1918
  if (typeof document !== "undefined") {
@@ -1370,11 +1926,11 @@ function OrganizationSwitcher({
1370
1926
  className
1371
1927
  }) {
1372
1928
  const { organization, isLoaded, isSignedIn } = useVaultixContext();
1373
- const [open, setOpen] = useState5(false);
1374
- const [orgs, setOrgs] = useState5([]);
1375
- const [switching, setSwitching] = useState5(null);
1929
+ const [open, setOpen] = useState6(false);
1930
+ const [orgs, setOrgs] = useState6([]);
1931
+ const [switching, setSwitching] = useState6(null);
1376
1932
  const containerRef = useRef4(null);
1377
- useEffect4(() => {
1933
+ useEffect5(() => {
1378
1934
  function onPointerDown(e) {
1379
1935
  if (containerRef.current && !containerRef.current.contains(e.target)) {
1380
1936
  setOpen(false);
@@ -1383,7 +1939,7 @@ function OrganizationSwitcher({
1383
1939
  document.addEventListener("pointerdown", onPointerDown);
1384
1940
  return () => document.removeEventListener("pointerdown", onPointerDown);
1385
1941
  }, []);
1386
- useEffect4(() => {
1942
+ useEffect5(() => {
1387
1943
  if (!open) return;
1388
1944
  const api = resolveApiOrigin4();
1389
1945
  fetch(`${api}/v1/me/organizations`, { credentials: "include" }).then((r) => r.json()).then(
@@ -1548,17 +2104,19 @@ function MiniSpinner() {
1548
2104
  }
1549
2105
 
1550
2106
  // src/index.ts
1551
- var STORAGE_KEY2 = "vaultix_jwt";
1552
2107
  function getStoredToken() {
2108
+ if (typeof document === "undefined") return null;
1553
2109
  try {
1554
- return (typeof sessionStorage !== "undefined" ? sessionStorage : null)?.getItem(STORAGE_KEY2) ?? null;
2110
+ const m = document.cookie.match(/(?:^|; )vaultix-token=([^;]+)/);
2111
+ return m?.[1] ? decodeURIComponent(m[1]) : null;
1555
2112
  } catch {
1556
2113
  return null;
1557
2114
  }
1558
2115
  }
1559
2116
  function clearStoredToken() {
2117
+ if (typeof document === "undefined") return;
1560
2118
  try {
1561
- if (typeof sessionStorage !== "undefined") sessionStorage.removeItem(STORAGE_KEY2);
2119
+ document.cookie = "vaultix-token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax";
1562
2120
  } catch {
1563
2121
  }
1564
2122
  }
@@ -1576,6 +2134,7 @@ export {
1576
2134
  getStoredToken,
1577
2135
  useAuth,
1578
2136
  useOrganization,
2137
+ usePlatformConnect,
1579
2138
  useSession,
1580
2139
  useUser,
1581
2140
  useVaultix