@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 +35 -3
- package/dist/react.js +109 -26
- package/package.json +1 -1
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
|
-
|
|
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
|
-
}
|
|
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 =
|
|
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,30 +1547,42 @@ 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
|
+
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
|
-
|
|
1572
|
+
setBackendFinalRedirect(body.final ?? "/");
|
|
1552
1573
|
setStep("recover");
|
|
1553
1574
|
return;
|
|
1554
1575
|
}
|
|
1555
|
-
const redirect = body.redirect ?? defaultRedirect(intent);
|
|
1556
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
1822
|
+
onAuthenticated({ ok: true, redirect, token });
|
|
1799
1823
|
return;
|
|
1800
1824
|
}
|
|
1801
|
-
window.location.href =
|
|
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
|
-
|
|
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
|
|
1837
|
+
onAuthenticated({ ok: true, redirect, token });
|
|
1812
1838
|
return;
|
|
1813
1839
|
}
|
|
1814
|
-
window.location.href =
|
|
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 =
|
|
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
|
|
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.
|
|
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",
|