@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 +34 -3
- package/dist/react.js +93 -24
- package/package.json +1 -1
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
|
-
|
|
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
|
-
}
|
|
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 =
|
|
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 [
|
|
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
|
-
|
|
1550
|
+
setBackendFinalRedirect(body.final ?? "/");
|
|
1541
1551
|
setStep("username");
|
|
1542
1552
|
return;
|
|
1543
1553
|
}
|
|
1544
1554
|
if (body.next_step === "passkey") {
|
|
1545
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1806
|
+
const redirect = finalRedirect();
|
|
1807
|
+
onResult?.({ ok: true, redirect });
|
|
1797
1808
|
if (onAuthenticated) {
|
|
1798
|
-
onAuthenticated({ ok: true, redirect
|
|
1809
|
+
onAuthenticated({ ok: true, redirect });
|
|
1799
1810
|
return;
|
|
1800
1811
|
}
|
|
1801
|
-
window.location.href =
|
|
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
|
-
|
|
1820
|
+
const redirect = finalRedirect();
|
|
1821
|
+
onResult?.({ ok: true, redirect });
|
|
1810
1822
|
if (onAuthenticated) {
|
|
1811
|
-
onAuthenticated({ ok: true, redirect
|
|
1823
|
+
onAuthenticated({ ok: true, redirect });
|
|
1812
1824
|
return;
|
|
1813
1825
|
}
|
|
1814
|
-
window.location.href =
|
|
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 =
|
|
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
|
|
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.
|
|
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",
|