@elvix.is/sdk 0.5.8 → 0.5.9

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/react.d.ts CHANGED
@@ -432,6 +432,27 @@ type AuthFormProps = {
432
432
  redirect?: string;
433
433
  token?: string;
434
434
  }) => void;
435
+ /**
436
+ * Where to send the user after EVERY terminal success path. One prop, one
437
+ * destination — no per-method customisation. Applies uniformly to:
438
+ *
439
+ * - OTP verify success (with or without an onboarding step).
440
+ * - Google sign-in (both the GIS credential and the redirect-OAuth
441
+ * return path that consumes `#elvix_token=...` on mount).
442
+ * - Passkey sign-in.
443
+ * - The onboarding "Add a passkey" success.
444
+ * - The onboarding "Skip for now" button on the passkey step.
445
+ * - The onboarding username step success.
446
+ *
447
+ * Resolution order at the moment of navigation:
448
+ * `redirectAfterSignIn ?? <backend-provided redirect> ?? "/"`
449
+ *
450
+ * If a host wants different destinations per method, they should switch
451
+ * on the result inside `onResult`/`onAuthenticated` and call
452
+ * `router.push(...)` themselves; this prop is the single declarative
453
+ * fallback that ALL success paths honour.
454
+ */
455
+ redirectAfterSignIn?: string;
435
456
  /**
436
457
  * Fires on every terminal outcome: success AND every error path
437
458
  * (invalid OTP, expired challenge, rate-limited, network blip,
@@ -538,14 +559,24 @@ declare const useUserRoles: (opts: Opts) => UseUserListResult;
538
559
  declare const useUserScopes: (opts: Opts) => UseUserListResult;
539
560
  declare const useUserMemberships: (opts: Opts) => UseUserListResult;
540
561
 
541
- declare function ElvixLifecycleWatcher({ baseUrl, pollMs, onSignedOut, }: {
562
+ type ElvixLifecycleWatcherProps = {
542
563
  /** elvix origin. Defaults to "" (same-origin). */
543
564
  baseUrl?: string;
544
- /** Poll interval in ms. Default 7000. */
565
+ /** Poll interval in ms when SSE isn't available. Default 7000. */
545
566
  pollMs?: number;
567
+ /**
568
+ * Application id to subscribe to (SSE mode). When set together with
569
+ * `userId` AND we're same-origin with `baseUrl`, the watcher opens an
570
+ * EventSource on `/api/presence/stream` and skips polling entirely.
571
+ * Omit to force the polling path (the cross-origin SDK case).
572
+ */
573
+ applicationId?: string;
574
+ /** User id to watch — SSE filters by this. Required with `applicationId`. */
575
+ userId?: string;
546
576
  /** Called once with the reason when the session ends. Defaults to a reload. */
547
577
  onSignedOut?: (reason: string) => void;
548
- }): null;
578
+ };
579
+ declare function ElvixLifecycleWatcher({ baseUrl, pollMs, applicationId, userId, onSignedOut, }: ElvixLifecycleWatcherProps): null;
549
580
 
550
581
  type ElvixUsernameResult = {
551
582
  ok: true;
package/dist/react.js CHANGED
@@ -1383,7 +1383,7 @@ function ElvixSignInForm(props) {
1383
1383
  googleClientId: props.googleClientId ?? app?.googleClientId ?? void 0,
1384
1384
  websiteUrl: props.websiteUrl ?? app?.websiteUrl ?? null
1385
1385
  };
1386
- const { framed = true, presentation = "card", theme = "light" } = resolved;
1386
+ const { framed = resolved.mode === "preview", presentation = "card", theme = "light" } = resolved;
1387
1387
  const card = /* @__PURE__ */ jsx7(AuthCard, { ...resolved });
1388
1388
  let content = card;
1389
1389
  if (presentation === "drawer") {
@@ -1487,6 +1487,7 @@ function AuthBody({
1487
1487
  belowMethods,
1488
1488
  googleConfig,
1489
1489
  googleClientId,
1490
+ redirectAfterSignIn,
1490
1491
  onAuthenticated,
1491
1492
  onResult
1492
1493
  }) {
@@ -1514,7 +1515,16 @@ function AuthBody({
1514
1515
  const [usernameSuggestions, setUsernameSuggestions] = useState5([]);
1515
1516
  const [usernameCheck, setUsernameCheck] = useState5({ kind: "idle" });
1516
1517
  const [onboardingBusy, setOnboardingBusy] = useState5(null);
1517
- const [finalRedirect, setFinalRedirect] = useState5("/");
1518
+ const [backendFinalRedirect, setBackendFinalRedirect] = useState5("/");
1519
+ const finalRedirect = useCallback3(
1520
+ (backendRedirect) => {
1521
+ if (redirectAfterSignIn) return redirectAfterSignIn;
1522
+ if (backendRedirect) return backendRedirect;
1523
+ if (backendFinalRedirect) return backendFinalRedirect;
1524
+ return "/";
1525
+ },
1526
+ [redirectAfterSignIn, backendFinalRedirect]
1527
+ );
1518
1528
  useEffect4(() => {
1519
1529
  if (resendIn <= 0) return;
1520
1530
  const t = setInterval(() => setResendIn((s) => Math.max(0, s - 1)), 1e3);
@@ -1537,22 +1547,22 @@ function AuthBody({
1537
1547
  if (body.next_step === "username") {
1538
1548
  setUsernameSuggestions(body.suggestions ?? []);
1539
1549
  setUsernameValue(body.suggestions?.[0] ?? "");
1540
- setFinalRedirect(body.final ?? "/");
1550
+ setBackendFinalRedirect(body.final ?? "/");
1541
1551
  setStep("username");
1542
1552
  return;
1543
1553
  }
1544
1554
  if (body.next_step === "passkey") {
1545
- setFinalRedirect(body.final ?? "/");
1555
+ setBackendFinalRedirect(body.final ?? "/");
1546
1556
  setStep("passkey");
1547
1557
  return;
1548
1558
  }
1549
1559
  if (body.next_step === "recover" && body.recover) {
1550
1560
  setRecoverState(body.recover);
1551
- setFinalRedirect(body.final ?? "/");
1561
+ setBackendFinalRedirect(body.final ?? "/");
1552
1562
  setStep("recover");
1553
1563
  return;
1554
1564
  }
1555
- const redirect = body.redirect ?? defaultRedirect(intent);
1565
+ const redirect = finalRedirect(body.redirect ?? defaultRedirect(intent));
1556
1566
  onResult?.({ ok: true, redirect });
1557
1567
  if (onAuthenticated) {
1558
1568
  onAuthenticated({ ok: true, redirect, token: body.token });
@@ -1560,7 +1570,7 @@ function AuthBody({
1560
1570
  }
1561
1571
  window.location.href = redirect;
1562
1572
  },
1563
- [intent, onAuthenticated, onResult]
1573
+ [intent, onAuthenticated, onResult, finalRedirect]
1564
1574
  );
1565
1575
  useEffect4(() => {
1566
1576
  if (isPreview) return;
@@ -1793,12 +1803,13 @@ function AuthBody({
1793
1803
  );
1794
1804
  return;
1795
1805
  }
1796
- onResult?.({ ok: true, redirect: finalRedirect });
1806
+ const redirect = finalRedirect();
1807
+ onResult?.({ ok: true, redirect });
1797
1808
  if (onAuthenticated) {
1798
- onAuthenticated({ ok: true, redirect: finalRedirect });
1809
+ onAuthenticated({ ok: true, redirect });
1799
1810
  return;
1800
1811
  }
1801
- window.location.href = finalRedirect;
1812
+ window.location.href = redirect;
1802
1813
  } finally {
1803
1814
  setOnboardingBusy(null);
1804
1815
  }
@@ -1806,12 +1817,13 @@ function AuthBody({
1806
1817
  const onSkipPasskey = useCallback3(() => {
1807
1818
  if (onboardingBusy) return;
1808
1819
  setOnboardingBusy("skip");
1809
- onResult?.({ ok: true, redirect: finalRedirect });
1820
+ const redirect = finalRedirect();
1821
+ onResult?.({ ok: true, redirect });
1810
1822
  if (onAuthenticated) {
1811
- onAuthenticated({ ok: true, redirect: finalRedirect });
1823
+ onAuthenticated({ ok: true, redirect });
1812
1824
  return;
1813
1825
  }
1814
- window.location.href = finalRedirect;
1826
+ window.location.href = redirect;
1815
1827
  }, [onboardingBusy, finalRedirect, onAuthenticated, onResult]);
1816
1828
  return /* @__PURE__ */ jsxs6(Fragment3, { children: [
1817
1829
  showHeader && /* @__PURE__ */ jsxs6(
@@ -2062,10 +2074,12 @@ function AuthBody({
2062
2074
  state: recoverState.state,
2063
2075
  sinceAt: recoverState.sinceAt,
2064
2076
  onRestore: ({ redirect }) => {
2077
+ const dest = finalRedirect(redirect);
2078
+ onResult?.({ ok: true, redirect: dest });
2065
2079
  if (onAuthenticated) {
2066
- onAuthenticated({ ok: true, redirect });
2080
+ onAuthenticated({ ok: true, redirect: dest });
2067
2081
  } else {
2068
- window.location.href = redirect;
2082
+ window.location.href = dest;
2069
2083
  }
2070
2084
  },
2071
2085
  onCancel: ({ redirect }) => {
@@ -2628,26 +2642,81 @@ var useUserMemberships = (opts) => useUserList("memberships", opts);
2628
2642
 
2629
2643
  // src/react/lifecycle-watcher.tsx
2630
2644
  import { useEffect as useEffect6 } from "react";
2645
+ var StatusValue = {
2646
+ ACTIVE: "active",
2647
+ PAUSED: "paused",
2648
+ BANNED: "banned",
2649
+ DELETED: "deleted",
2650
+ INACTIVE: "inactive"
2651
+ };
2652
+ function isSameOrigin2(baseUrl) {
2653
+ if (typeof window === "undefined") return false;
2654
+ if (!baseUrl) return true;
2655
+ try {
2656
+ return new URL(baseUrl, window.location.origin).origin === window.location.origin;
2657
+ } catch {
2658
+ return false;
2659
+ }
2660
+ }
2631
2661
  function ElvixLifecycleWatcher({
2632
2662
  baseUrl = "",
2633
2663
  pollMs = 7e3,
2664
+ applicationId,
2665
+ userId,
2634
2666
  onSignedOut
2635
2667
  }) {
2636
2668
  useEffect6(() => {
2637
2669
  let cancelled = false;
2638
2670
  let fired = false;
2671
+ function fire(reason) {
2672
+ if (cancelled || fired) return;
2673
+ fired = true;
2674
+ setElvixToken(null);
2675
+ if (onSignedOut) onSignedOut(reason);
2676
+ else if (typeof window !== "undefined") window.location.reload();
2677
+ }
2678
+ const canSse = applicationId !== void 0 && userId !== void 0 && typeof window !== "undefined" && typeof EventSource !== "undefined" && isSameOrigin2(baseUrl);
2679
+ if (canSse) {
2680
+ let onRecord2 = function(rec) {
2681
+ if (rec.userId !== userId) return;
2682
+ if (rec.status === StatusValue.ACTIVE) return;
2683
+ fire(rec.status);
2684
+ }, handle2 = function(e) {
2685
+ try {
2686
+ onRecord2(JSON.parse(e.data));
2687
+ } catch {
2688
+ }
2689
+ }, handleSnapshot2 = function(e) {
2690
+ try {
2691
+ for (const r of JSON.parse(e.data)) onRecord2(r);
2692
+ } catch {
2693
+ }
2694
+ };
2695
+ var onRecord = onRecord2, handle = handle2, handleSnapshot = handleSnapshot2;
2696
+ const url2 = new URL(`${baseUrl}/api/presence/stream`, window.location.origin);
2697
+ url2.searchParams.set("applicationId", applicationId);
2698
+ url2.searchParams.set("userId", userId);
2699
+ const ev = new EventSource(url2.toString());
2700
+ ev.addEventListener("user.lifecycle.changed", handle2);
2701
+ ev.addEventListener("lifecycle.snapshot", handleSnapshot2);
2702
+ return () => {
2703
+ cancelled = true;
2704
+ ev.removeEventListener("user.lifecycle.changed", handle2);
2705
+ ev.removeEventListener("lifecycle.snapshot", handleSnapshot2);
2706
+ ev.close();
2707
+ };
2708
+ }
2639
2709
  const poll = async () => {
2640
2710
  try {
2641
- const res = await fetch(`${baseUrl}/api/v1/session`, { method: "POST", ...authInit() });
2711
+ const init = authInit();
2712
+ const res = await fetch(`${baseUrl}/api/v1/session`, {
2713
+ method: "POST",
2714
+ headers: init.headers,
2715
+ credentials: init.credentials
2716
+ });
2642
2717
  const body = await res.json().catch(() => ({}));
2643
2718
  if (cancelled || fired) return;
2644
- if (!body.ok) {
2645
- fired = true;
2646
- setElvixToken(null);
2647
- const reason = body.error ?? "signed_out";
2648
- if (onSignedOut) onSignedOut(reason);
2649
- else if (typeof window !== "undefined") window.location.reload();
2650
- }
2719
+ if (!body.ok) fire(body.error ?? "signed_out");
2651
2720
  } catch {
2652
2721
  }
2653
2722
  };
@@ -2657,7 +2726,7 @@ function ElvixLifecycleWatcher({
2657
2726
  cancelled = true;
2658
2727
  clearInterval(id);
2659
2728
  };
2660
- }, [baseUrl, pollMs, onSignedOut]);
2729
+ }, [baseUrl, pollMs, applicationId, userId, onSignedOut]);
2661
2730
  return null;
2662
2731
  }
2663
2732
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elvix.is/sdk",
3
- "version": "0.5.8",
3
+ "version": "0.5.9",
4
4
  "description": "Official elvix SDK. Drop-in React components, server helpers, and an MCP server so AI coding agents integrate elvix on the first try.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://elvix.is",