@elvix.is/sdk 0.5.8 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/react.d.ts CHANGED
@@ -276,6 +276,7 @@ declare function ElvixSignIn({ onResult, redirectAfterSignIn, copy: copyProp, cl
276
276
  type ElvixSignInResult = {
277
277
  ok: true;
278
278
  redirect?: string;
279
+ token?: string;
279
280
  } | {
280
281
  ok: false;
281
282
  error: string;
@@ -432,6 +433,27 @@ type AuthFormProps = {
432
433
  redirect?: string;
433
434
  token?: string;
434
435
  }) => void;
436
+ /**
437
+ * Where to send the user after EVERY terminal success path. One prop, one
438
+ * destination — no per-method customisation. Applies uniformly to:
439
+ *
440
+ * - OTP verify success (with or without an onboarding step).
441
+ * - Google sign-in (both the GIS credential and the redirect-OAuth
442
+ * return path that consumes `#elvix_token=...` on mount).
443
+ * - Passkey sign-in.
444
+ * - The onboarding "Add a passkey" success.
445
+ * - The onboarding "Skip for now" button on the passkey step.
446
+ * - The onboarding username step success.
447
+ *
448
+ * Resolution order at the moment of navigation:
449
+ * `redirectAfterSignIn ?? <backend-provided redirect> ?? "/"`
450
+ *
451
+ * If a host wants different destinations per method, they should switch
452
+ * on the result inside `onResult`/`onAuthenticated` and call
453
+ * `router.push(...)` themselves; this prop is the single declarative
454
+ * fallback that ALL success paths honour.
455
+ */
456
+ redirectAfterSignIn?: string;
435
457
  /**
436
458
  * Fires on every terminal outcome: success AND every error path
437
459
  * (invalid OTP, expired challenge, rate-limited, network blip,
@@ -538,14 +560,24 @@ declare const useUserRoles: (opts: Opts) => UseUserListResult;
538
560
  declare const useUserScopes: (opts: Opts) => UseUserListResult;
539
561
  declare const useUserMemberships: (opts: Opts) => UseUserListResult;
540
562
 
541
- declare function ElvixLifecycleWatcher({ baseUrl, pollMs, onSignedOut, }: {
563
+ type ElvixLifecycleWatcherProps = {
542
564
  /** elvix origin. Defaults to "" (same-origin). */
543
565
  baseUrl?: string;
544
- /** Poll interval in ms. Default 7000. */
566
+ /** Poll interval in ms when SSE isn't available. Default 7000. */
545
567
  pollMs?: number;
568
+ /**
569
+ * Application id to subscribe to (SSE mode). When set together with
570
+ * `userId` AND we're same-origin with `baseUrl`, the watcher opens an
571
+ * EventSource on `/api/presence/stream` and skips polling entirely.
572
+ * Omit to force the polling path (the cross-origin SDK case).
573
+ */
574
+ applicationId?: string;
575
+ /** User id to watch — SSE filters by this. Required with `applicationId`. */
576
+ userId?: string;
546
577
  /** Called once with the reason when the session ends. Defaults to a reload. */
547
578
  onSignedOut?: (reason: string) => void;
548
- }): null;
579
+ };
580
+ declare function ElvixLifecycleWatcher({ baseUrl, pollMs, applicationId, userId, onSignedOut, }: ElvixLifecycleWatcherProps): null;
549
581
 
550
582
  type ElvixUsernameResult = {
551
583
  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,30 +1547,42 @@ 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
+ if (!isSameOrigin(baseUrl)) {
1556
+ const redirect2 = finalRedirect(body.final ?? body.redirect);
1557
+ const token2 = body.token ?? getElvixToken() ?? void 0;
1558
+ onResult?.({ ok: true, redirect: redirect2, token: token2 });
1559
+ if (onAuthenticated) {
1560
+ onAuthenticated({ ok: true, redirect: redirect2, token: token2 });
1561
+ return;
1562
+ }
1563
+ window.location.href = redirect2;
1564
+ return;
1565
+ }
1566
+ setBackendFinalRedirect(body.final ?? "/");
1546
1567
  setStep("passkey");
1547
1568
  return;
1548
1569
  }
1549
1570
  if (body.next_step === "recover" && body.recover) {
1550
1571
  setRecoverState(body.recover);
1551
- setFinalRedirect(body.final ?? "/");
1572
+ setBackendFinalRedirect(body.final ?? "/");
1552
1573
  setStep("recover");
1553
1574
  return;
1554
1575
  }
1555
- const redirect = body.redirect ?? defaultRedirect(intent);
1556
- onResult?.({ ok: true, redirect });
1576
+ const redirect = finalRedirect(body.redirect ?? defaultRedirect(intent));
1577
+ const token = body.token ?? getElvixToken() ?? void 0;
1578
+ onResult?.({ ok: true, redirect, token });
1557
1579
  if (onAuthenticated) {
1558
- onAuthenticated({ ok: true, redirect, token: body.token });
1580
+ onAuthenticated({ ok: true, redirect, token });
1559
1581
  return;
1560
1582
  }
1561
1583
  window.location.href = redirect;
1562
1584
  },
1563
- [intent, onAuthenticated, onResult]
1585
+ [intent, onAuthenticated, onResult, finalRedirect, baseUrl]
1564
1586
  );
1565
1587
  useEffect4(() => {
1566
1588
  if (isPreview) return;
@@ -1793,12 +1815,14 @@ function AuthBody({
1793
1815
  );
1794
1816
  return;
1795
1817
  }
1796
- onResult?.({ ok: true, redirect: finalRedirect });
1818
+ const redirect = finalRedirect();
1819
+ const token = getElvixToken() ?? void 0;
1820
+ onResult?.({ ok: true, redirect, token });
1797
1821
  if (onAuthenticated) {
1798
- onAuthenticated({ ok: true, redirect: finalRedirect });
1822
+ onAuthenticated({ ok: true, redirect, token });
1799
1823
  return;
1800
1824
  }
1801
- window.location.href = finalRedirect;
1825
+ window.location.href = redirect;
1802
1826
  } finally {
1803
1827
  setOnboardingBusy(null);
1804
1828
  }
@@ -1806,12 +1830,14 @@ function AuthBody({
1806
1830
  const onSkipPasskey = useCallback3(() => {
1807
1831
  if (onboardingBusy) return;
1808
1832
  setOnboardingBusy("skip");
1809
- onResult?.({ ok: true, redirect: finalRedirect });
1833
+ const redirect = finalRedirect();
1834
+ const token = getElvixToken() ?? void 0;
1835
+ onResult?.({ ok: true, redirect, token });
1810
1836
  if (onAuthenticated) {
1811
- onAuthenticated({ ok: true, redirect: finalRedirect });
1837
+ onAuthenticated({ ok: true, redirect, token });
1812
1838
  return;
1813
1839
  }
1814
- window.location.href = finalRedirect;
1840
+ window.location.href = redirect;
1815
1841
  }, [onboardingBusy, finalRedirect, onAuthenticated, onResult]);
1816
1842
  return /* @__PURE__ */ jsxs6(Fragment3, { children: [
1817
1843
  showHeader && /* @__PURE__ */ jsxs6(
@@ -2062,10 +2088,12 @@ function AuthBody({
2062
2088
  state: recoverState.state,
2063
2089
  sinceAt: recoverState.sinceAt,
2064
2090
  onRestore: ({ redirect }) => {
2091
+ const dest = finalRedirect(redirect);
2092
+ onResult?.({ ok: true, redirect: dest });
2065
2093
  if (onAuthenticated) {
2066
- onAuthenticated({ ok: true, redirect });
2094
+ onAuthenticated({ ok: true, redirect: dest });
2067
2095
  } else {
2068
- window.location.href = redirect;
2096
+ window.location.href = dest;
2069
2097
  }
2070
2098
  },
2071
2099
  onCancel: ({ redirect }) => {
@@ -2628,26 +2656,81 @@ var useUserMemberships = (opts) => useUserList("memberships", opts);
2628
2656
 
2629
2657
  // src/react/lifecycle-watcher.tsx
2630
2658
  import { useEffect as useEffect6 } from "react";
2659
+ var StatusValue = {
2660
+ ACTIVE: "active",
2661
+ PAUSED: "paused",
2662
+ BANNED: "banned",
2663
+ DELETED: "deleted",
2664
+ INACTIVE: "inactive"
2665
+ };
2666
+ function isSameOrigin2(baseUrl) {
2667
+ if (typeof window === "undefined") return false;
2668
+ if (!baseUrl) return true;
2669
+ try {
2670
+ return new URL(baseUrl, window.location.origin).origin === window.location.origin;
2671
+ } catch {
2672
+ return false;
2673
+ }
2674
+ }
2631
2675
  function ElvixLifecycleWatcher({
2632
2676
  baseUrl = "",
2633
2677
  pollMs = 7e3,
2678
+ applicationId,
2679
+ userId,
2634
2680
  onSignedOut
2635
2681
  }) {
2636
2682
  useEffect6(() => {
2637
2683
  let cancelled = false;
2638
2684
  let fired = false;
2685
+ function fire(reason) {
2686
+ if (cancelled || fired) return;
2687
+ fired = true;
2688
+ setElvixToken(null);
2689
+ if (onSignedOut) onSignedOut(reason);
2690
+ else if (typeof window !== "undefined") window.location.reload();
2691
+ }
2692
+ const canSse = applicationId !== void 0 && userId !== void 0 && typeof window !== "undefined" && typeof EventSource !== "undefined" && isSameOrigin2(baseUrl);
2693
+ if (canSse) {
2694
+ let onRecord2 = function(rec) {
2695
+ if (rec.userId !== userId) return;
2696
+ if (rec.status === StatusValue.ACTIVE) return;
2697
+ fire(rec.status);
2698
+ }, handle2 = function(e) {
2699
+ try {
2700
+ onRecord2(JSON.parse(e.data));
2701
+ } catch {
2702
+ }
2703
+ }, handleSnapshot2 = function(e) {
2704
+ try {
2705
+ for (const r of JSON.parse(e.data)) onRecord2(r);
2706
+ } catch {
2707
+ }
2708
+ };
2709
+ var onRecord = onRecord2, handle = handle2, handleSnapshot = handleSnapshot2;
2710
+ const url2 = new URL(`${baseUrl}/api/presence/stream`, window.location.origin);
2711
+ url2.searchParams.set("applicationId", applicationId);
2712
+ url2.searchParams.set("userId", userId);
2713
+ const ev = new EventSource(url2.toString());
2714
+ ev.addEventListener("user.lifecycle.changed", handle2);
2715
+ ev.addEventListener("lifecycle.snapshot", handleSnapshot2);
2716
+ return () => {
2717
+ cancelled = true;
2718
+ ev.removeEventListener("user.lifecycle.changed", handle2);
2719
+ ev.removeEventListener("lifecycle.snapshot", handleSnapshot2);
2720
+ ev.close();
2721
+ };
2722
+ }
2639
2723
  const poll = async () => {
2640
2724
  try {
2641
- const res = await fetch(`${baseUrl}/api/v1/session`, { method: "POST", ...authInit() });
2725
+ const init = authInit();
2726
+ const res = await fetch(`${baseUrl}/api/v1/session`, {
2727
+ method: "POST",
2728
+ headers: init.headers,
2729
+ credentials: init.credentials
2730
+ });
2642
2731
  const body = await res.json().catch(() => ({}));
2643
2732
  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
- }
2733
+ if (!body.ok) fire(body.error ?? "signed_out");
2651
2734
  } catch {
2652
2735
  }
2653
2736
  };
@@ -2657,7 +2740,7 @@ function ElvixLifecycleWatcher({
2657
2740
  cancelled = true;
2658
2741
  clearInterval(id);
2659
2742
  };
2660
- }, [baseUrl, pollMs, onSignedOut]);
2743
+ }, [baseUrl, pollMs, applicationId, userId, onSignedOut]);
2661
2744
  return null;
2662
2745
  }
2663
2746
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elvix.is/sdk",
3
- "version": "0.5.8",
3
+ "version": "0.6.0",
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",