@poly-x/react 0.1.0-alpha.18 → 0.1.0-alpha.19
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.cjs +169 -0
- package/dist/index.d.cts +63 -1
- package/dist/index.d.mts +63 -1
- package/dist/index.mjs +167 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -407,6 +407,40 @@ var BffSessionStore = class {
|
|
|
407
407
|
this.loading = false;
|
|
408
408
|
}
|
|
409
409
|
}
|
|
410
|
+
/**
|
|
411
|
+
* v2 (FR-LIVE-1/2): re-read server truth and update claims, resolving whether **authorization
|
|
412
|
+
* actually changed**. Drives the live-authz orchestrator (`startLiveAuthz`): a `true` means a
|
|
413
|
+
* consumer that gates on `useUser()` should re-guard. Never flickers to `resolving`; a definite
|
|
414
|
+
* signed-out answer signs the user out (revoked), a network blip keeps the last-known claims.
|
|
415
|
+
* The orchestrator single-flights callers, so this needs no internal in-flight guard.
|
|
416
|
+
*/
|
|
417
|
+
refresh = async () => {
|
|
418
|
+
try {
|
|
419
|
+
const response = await fetch(SESSION_ENDPOINT, {
|
|
420
|
+
credentials: "same-origin",
|
|
421
|
+
headers: { accept: "application/json" }
|
|
422
|
+
});
|
|
423
|
+
const body = response.ok ? await response.json() : null;
|
|
424
|
+
if (!Boolean(body?.isSignedIn && body.claims)) {
|
|
425
|
+
const changed = this.state.status !== "signed-out";
|
|
426
|
+
if (changed) this.set({
|
|
427
|
+
status: "signed-out",
|
|
428
|
+
reason: "revoked"
|
|
429
|
+
});
|
|
430
|
+
return changed;
|
|
431
|
+
}
|
|
432
|
+
const prevClaims = this.state.status === "authenticated" ? JSON.stringify(this.state.session.claims) : null;
|
|
433
|
+
const nextClaims = body.claims;
|
|
434
|
+
const changed = prevClaims !== JSON.stringify(nextClaims);
|
|
435
|
+
if (changed) this.set({
|
|
436
|
+
status: "authenticated",
|
|
437
|
+
session: { claims: nextClaims }
|
|
438
|
+
});
|
|
439
|
+
return changed;
|
|
440
|
+
} catch {
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
};
|
|
410
444
|
set(next) {
|
|
411
445
|
this.state = next;
|
|
412
446
|
for (const listener of this.listeners) listener();
|
|
@@ -1946,6 +1980,138 @@ const UserButton = Object.assign(UserButtonRoot, {
|
|
|
1946
1980
|
Header
|
|
1947
1981
|
});
|
|
1948
1982
|
//#endregion
|
|
1983
|
+
//#region src/live-authz.ts
|
|
1984
|
+
const DEFAULT_POLL_MS = 6e4;
|
|
1985
|
+
const DEFAULT_FOCUS_THROTTLE_MS = 5e3;
|
|
1986
|
+
/** Wire the triggers and return a handle. Safe to call with no window/document (SSR → no-op wiring). */
|
|
1987
|
+
function startLiveAuthz(opts) {
|
|
1988
|
+
const pollMs = opts.pollMs ?? DEFAULT_POLL_MS;
|
|
1989
|
+
const throttleMs = opts.focusThrottleMs ?? DEFAULT_FOCUS_THROTTLE_MS;
|
|
1990
|
+
const win = opts.win ?? (typeof window !== "undefined" ? window : void 0);
|
|
1991
|
+
const doc = opts.doc ?? (typeof document !== "undefined" ? document : void 0);
|
|
1992
|
+
let stopped = false;
|
|
1993
|
+
let inFlight = false;
|
|
1994
|
+
let pending = false;
|
|
1995
|
+
let lastRefreshAt = -Infinity;
|
|
1996
|
+
/**
|
|
1997
|
+
* Single-flight refresh (FR-LIVE-5). `coalesce` = queue exactly one follow-up run if a refresh
|
|
1998
|
+
* is already in flight — used by the explicit push signal so a change mid-refresh isn't missed.
|
|
1999
|
+
* Ambient triggers (focus/online/poll) pass `false`: if a refresh is already running, drop —
|
|
2000
|
+
* they carry no new information the in-flight run won't already pick up.
|
|
2001
|
+
*/
|
|
2002
|
+
async function run(coalesce) {
|
|
2003
|
+
if (stopped) return;
|
|
2004
|
+
if (inFlight) {
|
|
2005
|
+
if (coalesce) pending = true;
|
|
2006
|
+
return;
|
|
2007
|
+
}
|
|
2008
|
+
inFlight = true;
|
|
2009
|
+
try {
|
|
2010
|
+
let changed = false;
|
|
2011
|
+
try {
|
|
2012
|
+
changed = await opts.refresh();
|
|
2013
|
+
} catch {}
|
|
2014
|
+
lastRefreshAt = Date.now();
|
|
2015
|
+
if (changed && !stopped) opts.onChanged?.();
|
|
2016
|
+
} finally {
|
|
2017
|
+
inFlight = false;
|
|
2018
|
+
if (pending && !stopped) {
|
|
2019
|
+
pending = false;
|
|
2020
|
+
run(true);
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
/** Throttled, non-queuing refresh for ambient triggers (focus/visible/online). */
|
|
2025
|
+
function throttledRun() {
|
|
2026
|
+
if (Date.now() - lastRefreshAt >= throttleMs) run(false);
|
|
2027
|
+
}
|
|
2028
|
+
const isVisible = () => !doc || doc.visibilityState !== "hidden";
|
|
2029
|
+
const onFocus = () => throttledRun();
|
|
2030
|
+
const onOnline = () => throttledRun();
|
|
2031
|
+
const onVisibility = () => {
|
|
2032
|
+
if (isVisible()) throttledRun();
|
|
2033
|
+
};
|
|
2034
|
+
if (win) {
|
|
2035
|
+
win.addEventListener("focus", onFocus);
|
|
2036
|
+
win.addEventListener("online", onOnline);
|
|
2037
|
+
}
|
|
2038
|
+
if (doc) doc.addEventListener("visibilitychange", onVisibility);
|
|
2039
|
+
const unsubscribeSignal = opts.subscribeSignal?.(() => void run(true));
|
|
2040
|
+
let pollTimer;
|
|
2041
|
+
if (pollMs > 0) pollTimer = setInterval(() => {
|
|
2042
|
+
if (isVisible()) run(false);
|
|
2043
|
+
}, pollMs);
|
|
2044
|
+
return {
|
|
2045
|
+
refreshNow: () => void run(true),
|
|
2046
|
+
stop: () => {
|
|
2047
|
+
if (stopped) return;
|
|
2048
|
+
stopped = true;
|
|
2049
|
+
if (win) {
|
|
2050
|
+
win.removeEventListener("focus", onFocus);
|
|
2051
|
+
win.removeEventListener("online", onOnline);
|
|
2052
|
+
}
|
|
2053
|
+
if (doc) doc.removeEventListener("visibilitychange", onVisibility);
|
|
2054
|
+
unsubscribeSignal?.();
|
|
2055
|
+
if (pollTimer !== void 0) clearInterval(pollTimer);
|
|
2056
|
+
}
|
|
2057
|
+
};
|
|
2058
|
+
}
|
|
2059
|
+
//#endregion
|
|
2060
|
+
//#region src/live-session.ts
|
|
2061
|
+
/**
|
|
2062
|
+
* @poly-x/react/live-session — the React glue that keeps the BFF session live (SDK v2).
|
|
2063
|
+
*
|
|
2064
|
+
* `useLiveAuthz` starts the framework-agnostic orchestrator (`startLiveAuthz`, F013) bound to the
|
|
2065
|
+
* BFF session store's `refresh()`: the platform `authz:changed` signal (when a `subscribeSignal`
|
|
2066
|
+
* transport is supplied) + window focus/reconnect + a ~60s safety poll → a single-flight
|
|
2067
|
+
* re-introspect that updates reactive `useUser()`. `useAuthzChanged` lets an app with its own guard
|
|
2068
|
+
* layer react to a change (FR-REACT-1). Apps that gate on `useUser()` re-guard automatically
|
|
2069
|
+
* (FR-REACT-2) — they need no callback.
|
|
2070
|
+
*
|
|
2071
|
+
* The `subscribeSignal` transport is optional: without it the passive triggers still converge
|
|
2072
|
+
* (FR-SIG-4). The default poly-alert transport is provided separately (a later slice) once the
|
|
2073
|
+
* subscription-credential custody decision lands.
|
|
2074
|
+
*/
|
|
2075
|
+
const changeListeners = /* @__PURE__ */ new Set();
|
|
2076
|
+
function emitChanged() {
|
|
2077
|
+
for (const listener of changeListeners) listener();
|
|
2078
|
+
}
|
|
2079
|
+
/**
|
|
2080
|
+
* Start the live-authz orchestrator for this app's BFF session. Mount once (e.g. in the app root).
|
|
2081
|
+
* Returns nothing; drives `useUser()` reactivity + fires `useAuthzChanged` callbacks on a change.
|
|
2082
|
+
*/
|
|
2083
|
+
function useLiveAuthz(options = {}) {
|
|
2084
|
+
const { subscribeSignal, pollMs, focusThrottleMs } = options;
|
|
2085
|
+
(0, react.useEffect)(() => {
|
|
2086
|
+
const store = getBffSessionStore();
|
|
2087
|
+
const handle = startLiveAuthz({
|
|
2088
|
+
refresh: () => store.refresh(),
|
|
2089
|
+
onChanged: emitChanged,
|
|
2090
|
+
subscribeSignal,
|
|
2091
|
+
pollMs,
|
|
2092
|
+
focusThrottleMs
|
|
2093
|
+
});
|
|
2094
|
+
return () => handle.stop();
|
|
2095
|
+
}, [
|
|
2096
|
+
subscribeSignal,
|
|
2097
|
+
pollMs,
|
|
2098
|
+
focusThrottleMs
|
|
2099
|
+
]);
|
|
2100
|
+
}
|
|
2101
|
+
/**
|
|
2102
|
+
* Register a callback fired when the user's authorization changes (FR-REACT-1) — for apps that keep
|
|
2103
|
+
* their own ability/route model and need to re-run their guard or refetch view data. Apps that gate
|
|
2104
|
+
* on `useUser()` do not need this. Requires `useLiveAuthz` to be mounted somewhere in the tree.
|
|
2105
|
+
*/
|
|
2106
|
+
function useAuthzChanged(callback) {
|
|
2107
|
+
(0, react.useEffect)(() => {
|
|
2108
|
+
changeListeners.add(callback);
|
|
2109
|
+
return () => {
|
|
2110
|
+
changeListeners.delete(callback);
|
|
2111
|
+
};
|
|
2112
|
+
}, [callback]);
|
|
2113
|
+
}
|
|
2114
|
+
//#endregion
|
|
1949
2115
|
//#region src/index.ts
|
|
1950
2116
|
/**
|
|
1951
2117
|
* @poly-x/react — PolyX SDK for React SPAs. Sign-in UI + orchestration land in
|
|
@@ -1974,8 +2140,11 @@ exports.capturePreciseLocation = capturePreciseLocation;
|
|
|
1974
2140
|
exports.createLoginController = createLoginController;
|
|
1975
2141
|
exports.initialsFrom = initialsFrom;
|
|
1976
2142
|
exports.resolveSide = resolveSide;
|
|
2143
|
+
exports.startLiveAuthz = startLiveAuthz;
|
|
1977
2144
|
exports.useAuth = useAuth;
|
|
1978
2145
|
exports.useAuthCallback = useAuthCallback;
|
|
2146
|
+
exports.useAuthzChanged = useAuthzChanged;
|
|
2147
|
+
exports.useLiveAuthz = useLiveAuthz;
|
|
1979
2148
|
exports.useSession = useSession;
|
|
1980
2149
|
exports.useUser = useUser;
|
|
1981
2150
|
exports.version = version;
|
package/dist/index.d.cts
CHANGED
|
@@ -631,6 +631,68 @@ declare function UserAvatar({
|
|
|
631
631
|
className
|
|
632
632
|
}: UserAvatarProps): ReactNode;
|
|
633
633
|
//#endregion
|
|
634
|
+
//#region src/live-authz.d.ts
|
|
635
|
+
/**
|
|
636
|
+
* @poly-x/react/live-authz — keeps live claims current (SDK v2: FR-LIVE, FR-SIG, FR-TRIG).
|
|
637
|
+
*
|
|
638
|
+
* Orchestrates the refresh triggers behind ADR-0008/0009: the platform `authz:changed` signal
|
|
639
|
+
* (instant), window focus / tab-visible, network reconnect, and a **hidden-pausing safety poll**
|
|
640
|
+
* (~60s) as the staleness ceiling. Every refresh coalesces through a **single-flight** guard;
|
|
641
|
+
* focus/reconnect are throttled so churn never storms the platform; the push signal is never
|
|
642
|
+
* throttled (it's an explicit change). The injected `refresh` reports whether claims actually
|
|
643
|
+
* changed, so `onChanged` (which drives `useAuthzChanged`) fires only on a real change (FR-REACT-5).
|
|
644
|
+
*
|
|
645
|
+
* Pure of React — unit-testable with injected window/document + fake timers.
|
|
646
|
+
*/
|
|
647
|
+
interface LiveAuthzOptions {
|
|
648
|
+
/** Bring claims current; resolves `true` if the claims actually changed. Errors are swallowed
|
|
649
|
+
* (keep last-known — FR-LIVE-1 / EC-LIVE-1). */
|
|
650
|
+
refresh: () => Promise<boolean>;
|
|
651
|
+
/** Subscribe to the platform `authz:changed` signal; return an unsubscribe fn (ADR-0008). */
|
|
652
|
+
subscribeSignal?: (onSignal: () => void) => () => void;
|
|
653
|
+
/** Fired after a triggered refresh that changed claims (drives `useAuthzChanged`). */
|
|
654
|
+
onChanged?: () => void;
|
|
655
|
+
/** Background safety-poll interval in ms. Default 60000; `<= 0` disables the poll. */
|
|
656
|
+
pollMs?: number;
|
|
657
|
+
/** Throttle window (ms) for focus/reconnect/visibility refreshes. Default 5000. */
|
|
658
|
+
focusThrottleMs?: number;
|
|
659
|
+
/** Injectable window (tests). Defaults to the global `window` when present. */
|
|
660
|
+
win?: Pick<Window, "addEventListener" | "removeEventListener">;
|
|
661
|
+
/** Injectable document (tests). Defaults to the global `document` when present. */
|
|
662
|
+
doc?: Pick<Document, "addEventListener" | "removeEventListener"> & {
|
|
663
|
+
readonly visibilityState: DocumentVisibilityState;
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
interface LiveAuthzHandle {
|
|
667
|
+
/** Trigger a coalesced refresh immediately (used by the push signal). */
|
|
668
|
+
refreshNow: () => void;
|
|
669
|
+
/** Detach every listener + timer. Idempotent. */
|
|
670
|
+
stop: () => void;
|
|
671
|
+
}
|
|
672
|
+
/** Wire the triggers and return a handle. Safe to call with no window/document (SSR → no-op wiring). */
|
|
673
|
+
declare function startLiveAuthz(opts: LiveAuthzOptions): LiveAuthzHandle;
|
|
674
|
+
//#endregion
|
|
675
|
+
//#region src/live-session.d.ts
|
|
676
|
+
interface UseLiveAuthzOptions {
|
|
677
|
+
/** Optional push transport (ADR-0008). Omit to run passive-only (focus/reconnect/poll). */
|
|
678
|
+
subscribeSignal?: LiveAuthzOptions["subscribeSignal"];
|
|
679
|
+
/** Safety-poll interval (ms). Default 60000; `<= 0` disables. */
|
|
680
|
+
pollMs?: number;
|
|
681
|
+
/** Throttle window (ms) for focus/reconnect refreshes. Default 5000. */
|
|
682
|
+
focusThrottleMs?: number;
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Start the live-authz orchestrator for this app's BFF session. Mount once (e.g. in the app root).
|
|
686
|
+
* Returns nothing; drives `useUser()` reactivity + fires `useAuthzChanged` callbacks on a change.
|
|
687
|
+
*/
|
|
688
|
+
declare function useLiveAuthz(options?: UseLiveAuthzOptions): void;
|
|
689
|
+
/**
|
|
690
|
+
* Register a callback fired when the user's authorization changes (FR-REACT-1) — for apps that keep
|
|
691
|
+
* their own ability/route model and need to re-run their guard or refetch view data. Apps that gate
|
|
692
|
+
* on `useUser()` do not need this. Requires `useLiveAuthz` to be mounted somewhere in the tree.
|
|
693
|
+
*/
|
|
694
|
+
declare function useAuthzChanged(callback: () => void): void;
|
|
695
|
+
//#endregion
|
|
634
696
|
//#region src/index.d.ts
|
|
635
697
|
/**
|
|
636
698
|
* @poly-x/react — PolyX SDK for React SPAs. Sign-in UI + orchestration land in
|
|
@@ -639,4 +701,4 @@ declare function UserAvatar({
|
|
|
639
701
|
declare const PACKAGE_NAME = "@poly-x/react";
|
|
640
702
|
declare const version = "0.0.0";
|
|
641
703
|
//#endregion
|
|
642
|
-
export { type Align, AuthCallback, type AuthCallbackMessage, type AuthCallbackProps, type AuthChannel, BroadcastAuthChannel, BroadcastChannelSessionChannel, type BrowserBridge, type CallbackParams, type CaptureOptions, type CapturedGeo, ForgotPassword, type ForgotPasswordLabels, type ForgotPasswordProps, type GeolocationLike, type LoginController, type LoginControllerDeps, PACKAGE_NAME, PolyXContext, type PolyXContextValue, PolyXProvider, type PolyXProviderProps, PopupCancelledError, type PopupHandle, Protected, type ProtectedProps, ResetPassword, type ResetPasswordLabels, type ResetPasswordProps, SessionStoragePkceStore, type Side, type SideOption, SignIn, type SignInLabels, type SignInOptions, type SignInProps, type SignInVariant, type UseAuth, UserAvatar, type UserAvatarProps, UserButton, type UserButtonActionProps, type UserButtonLabels, type UserButtonLinkProps, type UserButtonProps, type UserButtonSlotProps, WebLocksLock, WindowBrowserBridge, capturePreciseLocation, createLoginController, initialsFrom, resolveSide, useAuth, useAuthCallback, useSession, useUser, version };
|
|
704
|
+
export { type Align, AuthCallback, type AuthCallbackMessage, type AuthCallbackProps, type AuthChannel, BroadcastAuthChannel, BroadcastChannelSessionChannel, type BrowserBridge, type CallbackParams, type CaptureOptions, type CapturedGeo, ForgotPassword, type ForgotPasswordLabels, type ForgotPasswordProps, type GeolocationLike, type LiveAuthzHandle, type LiveAuthzOptions, type LoginController, type LoginControllerDeps, PACKAGE_NAME, PolyXContext, type PolyXContextValue, PolyXProvider, type PolyXProviderProps, PopupCancelledError, type PopupHandle, Protected, type ProtectedProps, ResetPassword, type ResetPasswordLabels, type ResetPasswordProps, SessionStoragePkceStore, type Side, type SideOption, SignIn, type SignInLabels, type SignInOptions, type SignInProps, type SignInVariant, type UseAuth, type UseLiveAuthzOptions, UserAvatar, type UserAvatarProps, UserButton, type UserButtonActionProps, type UserButtonLabels, type UserButtonLinkProps, type UserButtonProps, type UserButtonSlotProps, WebLocksLock, WindowBrowserBridge, capturePreciseLocation, createLoginController, initialsFrom, resolveSide, startLiveAuthz, useAuth, useAuthCallback, useAuthzChanged, useLiveAuthz, useSession, useUser, version };
|
package/dist/index.d.mts
CHANGED
|
@@ -631,6 +631,68 @@ declare function UserAvatar({
|
|
|
631
631
|
className
|
|
632
632
|
}: UserAvatarProps): ReactNode;
|
|
633
633
|
//#endregion
|
|
634
|
+
//#region src/live-authz.d.ts
|
|
635
|
+
/**
|
|
636
|
+
* @poly-x/react/live-authz — keeps live claims current (SDK v2: FR-LIVE, FR-SIG, FR-TRIG).
|
|
637
|
+
*
|
|
638
|
+
* Orchestrates the refresh triggers behind ADR-0008/0009: the platform `authz:changed` signal
|
|
639
|
+
* (instant), window focus / tab-visible, network reconnect, and a **hidden-pausing safety poll**
|
|
640
|
+
* (~60s) as the staleness ceiling. Every refresh coalesces through a **single-flight** guard;
|
|
641
|
+
* focus/reconnect are throttled so churn never storms the platform; the push signal is never
|
|
642
|
+
* throttled (it's an explicit change). The injected `refresh` reports whether claims actually
|
|
643
|
+
* changed, so `onChanged` (which drives `useAuthzChanged`) fires only on a real change (FR-REACT-5).
|
|
644
|
+
*
|
|
645
|
+
* Pure of React — unit-testable with injected window/document + fake timers.
|
|
646
|
+
*/
|
|
647
|
+
interface LiveAuthzOptions {
|
|
648
|
+
/** Bring claims current; resolves `true` if the claims actually changed. Errors are swallowed
|
|
649
|
+
* (keep last-known — FR-LIVE-1 / EC-LIVE-1). */
|
|
650
|
+
refresh: () => Promise<boolean>;
|
|
651
|
+
/** Subscribe to the platform `authz:changed` signal; return an unsubscribe fn (ADR-0008). */
|
|
652
|
+
subscribeSignal?: (onSignal: () => void) => () => void;
|
|
653
|
+
/** Fired after a triggered refresh that changed claims (drives `useAuthzChanged`). */
|
|
654
|
+
onChanged?: () => void;
|
|
655
|
+
/** Background safety-poll interval in ms. Default 60000; `<= 0` disables the poll. */
|
|
656
|
+
pollMs?: number;
|
|
657
|
+
/** Throttle window (ms) for focus/reconnect/visibility refreshes. Default 5000. */
|
|
658
|
+
focusThrottleMs?: number;
|
|
659
|
+
/** Injectable window (tests). Defaults to the global `window` when present. */
|
|
660
|
+
win?: Pick<Window, "addEventListener" | "removeEventListener">;
|
|
661
|
+
/** Injectable document (tests). Defaults to the global `document` when present. */
|
|
662
|
+
doc?: Pick<Document, "addEventListener" | "removeEventListener"> & {
|
|
663
|
+
readonly visibilityState: DocumentVisibilityState;
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
interface LiveAuthzHandle {
|
|
667
|
+
/** Trigger a coalesced refresh immediately (used by the push signal). */
|
|
668
|
+
refreshNow: () => void;
|
|
669
|
+
/** Detach every listener + timer. Idempotent. */
|
|
670
|
+
stop: () => void;
|
|
671
|
+
}
|
|
672
|
+
/** Wire the triggers and return a handle. Safe to call with no window/document (SSR → no-op wiring). */
|
|
673
|
+
declare function startLiveAuthz(opts: LiveAuthzOptions): LiveAuthzHandle;
|
|
674
|
+
//#endregion
|
|
675
|
+
//#region src/live-session.d.ts
|
|
676
|
+
interface UseLiveAuthzOptions {
|
|
677
|
+
/** Optional push transport (ADR-0008). Omit to run passive-only (focus/reconnect/poll). */
|
|
678
|
+
subscribeSignal?: LiveAuthzOptions["subscribeSignal"];
|
|
679
|
+
/** Safety-poll interval (ms). Default 60000; `<= 0` disables. */
|
|
680
|
+
pollMs?: number;
|
|
681
|
+
/** Throttle window (ms) for focus/reconnect refreshes. Default 5000. */
|
|
682
|
+
focusThrottleMs?: number;
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Start the live-authz orchestrator for this app's BFF session. Mount once (e.g. in the app root).
|
|
686
|
+
* Returns nothing; drives `useUser()` reactivity + fires `useAuthzChanged` callbacks on a change.
|
|
687
|
+
*/
|
|
688
|
+
declare function useLiveAuthz(options?: UseLiveAuthzOptions): void;
|
|
689
|
+
/**
|
|
690
|
+
* Register a callback fired when the user's authorization changes (FR-REACT-1) — for apps that keep
|
|
691
|
+
* their own ability/route model and need to re-run their guard or refetch view data. Apps that gate
|
|
692
|
+
* on `useUser()` do not need this. Requires `useLiveAuthz` to be mounted somewhere in the tree.
|
|
693
|
+
*/
|
|
694
|
+
declare function useAuthzChanged(callback: () => void): void;
|
|
695
|
+
//#endregion
|
|
634
696
|
//#region src/index.d.ts
|
|
635
697
|
/**
|
|
636
698
|
* @poly-x/react — PolyX SDK for React SPAs. Sign-in UI + orchestration land in
|
|
@@ -639,4 +701,4 @@ declare function UserAvatar({
|
|
|
639
701
|
declare const PACKAGE_NAME = "@poly-x/react";
|
|
640
702
|
declare const version = "0.0.0";
|
|
641
703
|
//#endregion
|
|
642
|
-
export { type Align, AuthCallback, type AuthCallbackMessage, type AuthCallbackProps, type AuthChannel, BroadcastAuthChannel, BroadcastChannelSessionChannel, type BrowserBridge, type CallbackParams, type CaptureOptions, type CapturedGeo, ForgotPassword, type ForgotPasswordLabels, type ForgotPasswordProps, type GeolocationLike, type LoginController, type LoginControllerDeps, PACKAGE_NAME, PolyXContext, type PolyXContextValue, PolyXProvider, type PolyXProviderProps, PopupCancelledError, type PopupHandle, Protected, type ProtectedProps, ResetPassword, type ResetPasswordLabels, type ResetPasswordProps, SessionStoragePkceStore, type Side, type SideOption, SignIn, type SignInLabels, type SignInOptions, type SignInProps, type SignInVariant, type UseAuth, UserAvatar, type UserAvatarProps, UserButton, type UserButtonActionProps, type UserButtonLabels, type UserButtonLinkProps, type UserButtonProps, type UserButtonSlotProps, WebLocksLock, WindowBrowserBridge, capturePreciseLocation, createLoginController, initialsFrom, resolveSide, useAuth, useAuthCallback, useSession, useUser, version };
|
|
704
|
+
export { type Align, AuthCallback, type AuthCallbackMessage, type AuthCallbackProps, type AuthChannel, BroadcastAuthChannel, BroadcastChannelSessionChannel, type BrowserBridge, type CallbackParams, type CaptureOptions, type CapturedGeo, ForgotPassword, type ForgotPasswordLabels, type ForgotPasswordProps, type GeolocationLike, type LiveAuthzHandle, type LiveAuthzOptions, type LoginController, type LoginControllerDeps, PACKAGE_NAME, PolyXContext, type PolyXContextValue, PolyXProvider, type PolyXProviderProps, PopupCancelledError, type PopupHandle, Protected, type ProtectedProps, ResetPassword, type ResetPasswordLabels, type ResetPasswordProps, SessionStoragePkceStore, type Side, type SideOption, SignIn, type SignInLabels, type SignInOptions, type SignInProps, type SignInVariant, type UseAuth, type UseLiveAuthzOptions, UserAvatar, type UserAvatarProps, UserButton, type UserButtonActionProps, type UserButtonLabels, type UserButtonLinkProps, type UserButtonProps, type UserButtonSlotProps, WebLocksLock, WindowBrowserBridge, capturePreciseLocation, createLoginController, initialsFrom, resolveSide, startLiveAuthz, useAuth, useAuthCallback, useAuthzChanged, useLiveAuthz, useSession, useUser, version };
|
package/dist/index.mjs
CHANGED
|
@@ -406,6 +406,40 @@ var BffSessionStore = class {
|
|
|
406
406
|
this.loading = false;
|
|
407
407
|
}
|
|
408
408
|
}
|
|
409
|
+
/**
|
|
410
|
+
* v2 (FR-LIVE-1/2): re-read server truth and update claims, resolving whether **authorization
|
|
411
|
+
* actually changed**. Drives the live-authz orchestrator (`startLiveAuthz`): a `true` means a
|
|
412
|
+
* consumer that gates on `useUser()` should re-guard. Never flickers to `resolving`; a definite
|
|
413
|
+
* signed-out answer signs the user out (revoked), a network blip keeps the last-known claims.
|
|
414
|
+
* The orchestrator single-flights callers, so this needs no internal in-flight guard.
|
|
415
|
+
*/
|
|
416
|
+
refresh = async () => {
|
|
417
|
+
try {
|
|
418
|
+
const response = await fetch(SESSION_ENDPOINT, {
|
|
419
|
+
credentials: "same-origin",
|
|
420
|
+
headers: { accept: "application/json" }
|
|
421
|
+
});
|
|
422
|
+
const body = response.ok ? await response.json() : null;
|
|
423
|
+
if (!Boolean(body?.isSignedIn && body.claims)) {
|
|
424
|
+
const changed = this.state.status !== "signed-out";
|
|
425
|
+
if (changed) this.set({
|
|
426
|
+
status: "signed-out",
|
|
427
|
+
reason: "revoked"
|
|
428
|
+
});
|
|
429
|
+
return changed;
|
|
430
|
+
}
|
|
431
|
+
const prevClaims = this.state.status === "authenticated" ? JSON.stringify(this.state.session.claims) : null;
|
|
432
|
+
const nextClaims = body.claims;
|
|
433
|
+
const changed = prevClaims !== JSON.stringify(nextClaims);
|
|
434
|
+
if (changed) this.set({
|
|
435
|
+
status: "authenticated",
|
|
436
|
+
session: { claims: nextClaims }
|
|
437
|
+
});
|
|
438
|
+
return changed;
|
|
439
|
+
} catch {
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
442
|
+
};
|
|
409
443
|
set(next) {
|
|
410
444
|
this.state = next;
|
|
411
445
|
for (const listener of this.listeners) listener();
|
|
@@ -1945,6 +1979,138 @@ const UserButton = Object.assign(UserButtonRoot, {
|
|
|
1945
1979
|
Header
|
|
1946
1980
|
});
|
|
1947
1981
|
//#endregion
|
|
1982
|
+
//#region src/live-authz.ts
|
|
1983
|
+
const DEFAULT_POLL_MS = 6e4;
|
|
1984
|
+
const DEFAULT_FOCUS_THROTTLE_MS = 5e3;
|
|
1985
|
+
/** Wire the triggers and return a handle. Safe to call with no window/document (SSR → no-op wiring). */
|
|
1986
|
+
function startLiveAuthz(opts) {
|
|
1987
|
+
const pollMs = opts.pollMs ?? DEFAULT_POLL_MS;
|
|
1988
|
+
const throttleMs = opts.focusThrottleMs ?? DEFAULT_FOCUS_THROTTLE_MS;
|
|
1989
|
+
const win = opts.win ?? (typeof window !== "undefined" ? window : void 0);
|
|
1990
|
+
const doc = opts.doc ?? (typeof document !== "undefined" ? document : void 0);
|
|
1991
|
+
let stopped = false;
|
|
1992
|
+
let inFlight = false;
|
|
1993
|
+
let pending = false;
|
|
1994
|
+
let lastRefreshAt = -Infinity;
|
|
1995
|
+
/**
|
|
1996
|
+
* Single-flight refresh (FR-LIVE-5). `coalesce` = queue exactly one follow-up run if a refresh
|
|
1997
|
+
* is already in flight — used by the explicit push signal so a change mid-refresh isn't missed.
|
|
1998
|
+
* Ambient triggers (focus/online/poll) pass `false`: if a refresh is already running, drop —
|
|
1999
|
+
* they carry no new information the in-flight run won't already pick up.
|
|
2000
|
+
*/
|
|
2001
|
+
async function run(coalesce) {
|
|
2002
|
+
if (stopped) return;
|
|
2003
|
+
if (inFlight) {
|
|
2004
|
+
if (coalesce) pending = true;
|
|
2005
|
+
return;
|
|
2006
|
+
}
|
|
2007
|
+
inFlight = true;
|
|
2008
|
+
try {
|
|
2009
|
+
let changed = false;
|
|
2010
|
+
try {
|
|
2011
|
+
changed = await opts.refresh();
|
|
2012
|
+
} catch {}
|
|
2013
|
+
lastRefreshAt = Date.now();
|
|
2014
|
+
if (changed && !stopped) opts.onChanged?.();
|
|
2015
|
+
} finally {
|
|
2016
|
+
inFlight = false;
|
|
2017
|
+
if (pending && !stopped) {
|
|
2018
|
+
pending = false;
|
|
2019
|
+
run(true);
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
/** Throttled, non-queuing refresh for ambient triggers (focus/visible/online). */
|
|
2024
|
+
function throttledRun() {
|
|
2025
|
+
if (Date.now() - lastRefreshAt >= throttleMs) run(false);
|
|
2026
|
+
}
|
|
2027
|
+
const isVisible = () => !doc || doc.visibilityState !== "hidden";
|
|
2028
|
+
const onFocus = () => throttledRun();
|
|
2029
|
+
const onOnline = () => throttledRun();
|
|
2030
|
+
const onVisibility = () => {
|
|
2031
|
+
if (isVisible()) throttledRun();
|
|
2032
|
+
};
|
|
2033
|
+
if (win) {
|
|
2034
|
+
win.addEventListener("focus", onFocus);
|
|
2035
|
+
win.addEventListener("online", onOnline);
|
|
2036
|
+
}
|
|
2037
|
+
if (doc) doc.addEventListener("visibilitychange", onVisibility);
|
|
2038
|
+
const unsubscribeSignal = opts.subscribeSignal?.(() => void run(true));
|
|
2039
|
+
let pollTimer;
|
|
2040
|
+
if (pollMs > 0) pollTimer = setInterval(() => {
|
|
2041
|
+
if (isVisible()) run(false);
|
|
2042
|
+
}, pollMs);
|
|
2043
|
+
return {
|
|
2044
|
+
refreshNow: () => void run(true),
|
|
2045
|
+
stop: () => {
|
|
2046
|
+
if (stopped) return;
|
|
2047
|
+
stopped = true;
|
|
2048
|
+
if (win) {
|
|
2049
|
+
win.removeEventListener("focus", onFocus);
|
|
2050
|
+
win.removeEventListener("online", onOnline);
|
|
2051
|
+
}
|
|
2052
|
+
if (doc) doc.removeEventListener("visibilitychange", onVisibility);
|
|
2053
|
+
unsubscribeSignal?.();
|
|
2054
|
+
if (pollTimer !== void 0) clearInterval(pollTimer);
|
|
2055
|
+
}
|
|
2056
|
+
};
|
|
2057
|
+
}
|
|
2058
|
+
//#endregion
|
|
2059
|
+
//#region src/live-session.ts
|
|
2060
|
+
/**
|
|
2061
|
+
* @poly-x/react/live-session — the React glue that keeps the BFF session live (SDK v2).
|
|
2062
|
+
*
|
|
2063
|
+
* `useLiveAuthz` starts the framework-agnostic orchestrator (`startLiveAuthz`, F013) bound to the
|
|
2064
|
+
* BFF session store's `refresh()`: the platform `authz:changed` signal (when a `subscribeSignal`
|
|
2065
|
+
* transport is supplied) + window focus/reconnect + a ~60s safety poll → a single-flight
|
|
2066
|
+
* re-introspect that updates reactive `useUser()`. `useAuthzChanged` lets an app with its own guard
|
|
2067
|
+
* layer react to a change (FR-REACT-1). Apps that gate on `useUser()` re-guard automatically
|
|
2068
|
+
* (FR-REACT-2) — they need no callback.
|
|
2069
|
+
*
|
|
2070
|
+
* The `subscribeSignal` transport is optional: without it the passive triggers still converge
|
|
2071
|
+
* (FR-SIG-4). The default poly-alert transport is provided separately (a later slice) once the
|
|
2072
|
+
* subscription-credential custody decision lands.
|
|
2073
|
+
*/
|
|
2074
|
+
const changeListeners = /* @__PURE__ */ new Set();
|
|
2075
|
+
function emitChanged() {
|
|
2076
|
+
for (const listener of changeListeners) listener();
|
|
2077
|
+
}
|
|
2078
|
+
/**
|
|
2079
|
+
* Start the live-authz orchestrator for this app's BFF session. Mount once (e.g. in the app root).
|
|
2080
|
+
* Returns nothing; drives `useUser()` reactivity + fires `useAuthzChanged` callbacks on a change.
|
|
2081
|
+
*/
|
|
2082
|
+
function useLiveAuthz(options = {}) {
|
|
2083
|
+
const { subscribeSignal, pollMs, focusThrottleMs } = options;
|
|
2084
|
+
useEffect(() => {
|
|
2085
|
+
const store = getBffSessionStore();
|
|
2086
|
+
const handle = startLiveAuthz({
|
|
2087
|
+
refresh: () => store.refresh(),
|
|
2088
|
+
onChanged: emitChanged,
|
|
2089
|
+
subscribeSignal,
|
|
2090
|
+
pollMs,
|
|
2091
|
+
focusThrottleMs
|
|
2092
|
+
});
|
|
2093
|
+
return () => handle.stop();
|
|
2094
|
+
}, [
|
|
2095
|
+
subscribeSignal,
|
|
2096
|
+
pollMs,
|
|
2097
|
+
focusThrottleMs
|
|
2098
|
+
]);
|
|
2099
|
+
}
|
|
2100
|
+
/**
|
|
2101
|
+
* Register a callback fired when the user's authorization changes (FR-REACT-1) — for apps that keep
|
|
2102
|
+
* their own ability/route model and need to re-run their guard or refetch view data. Apps that gate
|
|
2103
|
+
* on `useUser()` do not need this. Requires `useLiveAuthz` to be mounted somewhere in the tree.
|
|
2104
|
+
*/
|
|
2105
|
+
function useAuthzChanged(callback) {
|
|
2106
|
+
useEffect(() => {
|
|
2107
|
+
changeListeners.add(callback);
|
|
2108
|
+
return () => {
|
|
2109
|
+
changeListeners.delete(callback);
|
|
2110
|
+
};
|
|
2111
|
+
}, [callback]);
|
|
2112
|
+
}
|
|
2113
|
+
//#endregion
|
|
1948
2114
|
//#region src/index.ts
|
|
1949
2115
|
/**
|
|
1950
2116
|
* @poly-x/react — PolyX SDK for React SPAs. Sign-in UI + orchestration land in
|
|
@@ -1953,4 +2119,4 @@ const UserButton = Object.assign(UserButtonRoot, {
|
|
|
1953
2119
|
const PACKAGE_NAME = "@poly-x/react";
|
|
1954
2120
|
const version = "0.0.0";
|
|
1955
2121
|
//#endregion
|
|
1956
|
-
export { AuthCallback, BroadcastAuthChannel, BroadcastChannelSessionChannel, ForgotPassword, PACKAGE_NAME, PolyXContext, PolyXProvider, PopupCancelledError, Protected, ResetPassword, SessionStoragePkceStore, SignIn, UserAvatar, UserButton, WebLocksLock, WindowBrowserBridge, capturePreciseLocation, createLoginController, initialsFrom, resolveSide, useAuth, useAuthCallback, useSession, useUser, version };
|
|
2122
|
+
export { AuthCallback, BroadcastAuthChannel, BroadcastChannelSessionChannel, ForgotPassword, PACKAGE_NAME, PolyXContext, PolyXProvider, PopupCancelledError, Protected, ResetPassword, SessionStoragePkceStore, SignIn, UserAvatar, UserButton, WebLocksLock, WindowBrowserBridge, capturePreciseLocation, createLoginController, initialsFrom, resolveSide, startLiveAuthz, useAuth, useAuthCallback, useAuthzChanged, useLiveAuthz, useSession, useUser, version };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poly-x/react",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.19",
|
|
4
4
|
"description": "PolyX SDK for React SPAs - provider, hooks, drop-in auth UI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"module": "./dist/index.mjs",
|
|
28
28
|
"types": "./dist/index.d.cts",
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@poly-x/core": "0.1.0-alpha.
|
|
30
|
+
"@poly-x/core": "0.1.0-alpha.19"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
33
|
"react": ">=18",
|