@poly-x/react 0.1.0-alpha.8 → 0.1.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/index.d.cts CHANGED
@@ -18,6 +18,21 @@ declare function PolyXProvider({
18
18
  children
19
19
  }: PolyXProviderProps): ReactNode;
20
20
  //#endregion
21
+ //#region src/bff-session.d.ts
22
+ interface BffSignOutOptions {
23
+ /** `everywhere` ends every session for the user (FR-SSO-5). Default `device`. */
24
+ scope?: "device" | "everywhere";
25
+ /** Where to land afterwards. Resolved here, client-side — the server takes no redirect target. */
26
+ redirectTo?: string;
27
+ /**
28
+ * Run consumer cleanup after the session is revoked but BEFORE the post-sign-out
29
+ * redirect (the page is still alive at this point) — e.g. clear app cookies, reset
30
+ * local state, flush a store. Awaited; a thrown/rejected hook is swallowed so a
31
+ * failing callback can never trap the user in a signed-out-but-not-redirected state.
32
+ */
33
+ onSignOut?: () => void | Promise<void>;
34
+ }
35
+ //#endregion
21
36
  //#region src/auth-channel.d.ts
22
37
  /**
23
38
  * Same-origin popup→opener handoff of the authorization `code` (F006). The
@@ -115,8 +130,11 @@ interface UseAuth {
115
130
  isLoaded: boolean;
116
131
  isSignedIn: boolean;
117
132
  signIn: (options?: SignInOptions) => Promise<void>;
118
- signOut: () => Promise<void>;
119
- /** Current access token, refreshing if needed; null when signed out. */
133
+ signOut: (options?: BffSignOutOptions) => Promise<void>;
134
+ /**
135
+ * Current access token, refreshing if needed; null when signed out — and always null under
136
+ * BFF custody, where the token is httpOnly server-side by design (FR-SES-3).
137
+ */
120
138
  getToken: () => Promise<string | null>;
121
139
  }
122
140
  declare function useAuth(): UseAuth;
@@ -201,6 +219,9 @@ interface SignInLabels {
201
219
  emailPlaceholder: string;
202
220
  passwordLabel: string;
203
221
  passwordPlaceholder: string;
222
+ /** Reveal-toggle a11y labels (aria-label / tooltip on the show-hide button). */
223
+ showPassword: string;
224
+ hidePassword: string;
204
225
  submit: string;
205
226
  submitting: string;
206
227
  forgotPassword: string;
@@ -241,6 +262,13 @@ interface SignInProps {
241
262
  * "Forgot password?" link appears under the password field. Omit to hide it.
242
263
  */
243
264
  forgotPasswordUrl?: string;
265
+ /**
266
+ * v6 (FR-CNST): capture precise location for security/presence. When enabled, sign-in asks the
267
+ * browser for a position — the browser's own native permission prompt is the consent gate (no
268
+ * in-form checkbox), and it's remembered per-origin after the first choice. A block, denial, or
269
+ * timeout never blocks sign-in (coarse IP fallback). Off by default.
270
+ */
271
+ captureLocation?: boolean;
244
272
  /** Where to send the browser after success. Overrides the server's default redirect. */
245
273
  afterSignInPath?: string;
246
274
  /** Pre-scope the login to a single organization (skips org discovery). */
@@ -278,7 +306,13 @@ interface ForgotPasswordLabels {
278
306
  submitting: string;
279
307
  /** Shown after submit — always, regardless of whether the account exists. */
280
308
  sentHeading: string;
281
- /** Confirmation body. `{email}` is replaced with the entered address, emphasized. */
309
+ /**
310
+ * Confirmation body. `{email}` is replaced with the entered address, emphasized.
311
+ * Kept deliberately uniform (no account-existence / mail-config signal, per the
312
+ * anti-enumeration reset design) and points the user at their administrator for
313
+ * the cases where no mail can arrive (org has reset email disabled, no provider
314
+ * configured, or a send failure) — none of which the response is allowed to reveal.
315
+ */
282
316
  sentSubheading: string;
283
317
  differentEmail: string;
284
318
  backToSignIn: string;
@@ -372,6 +406,293 @@ declare function AuthCallback({
372
406
  children
373
407
  }: AuthCallbackProps): ReactNode;
374
408
  //#endregion
409
+ //#region src/geolocation.d.ts
410
+ /**
411
+ * @poly-x/react/geolocation — browser precise-location capture for sign-in (v6, FR-CNST/FR-LOC).
412
+ *
413
+ * The consent-gated bridge to `navigator.geolocation`. It NEVER rejects: a missing API resolves
414
+ * to `{ consent: "denied" }` with no coordinates so the caller can always proceed — sign-in must
415
+ * never block on location (D52).
416
+ *
417
+ * Consent vs. fix are kept SEPARATE. `consent` reflects the browser PERMISSION decision only:
418
+ * `denied` means the user actually said no (`PERMISSION_DENIED`). A position that is merely
419
+ * unavailable or times out (common on desktops with no GPS) is NOT a denial — the user consented,
420
+ * we just couldn't get coordinates, so consent stays `granted` with no coords and the platform
421
+ * uses the coarse (IP) fallback. Conflating the two is why an allowed login could be stored as
422
+ * `denied` + coarse. On a no-fix (not a denial) we retry once at low accuracy (network-based),
423
+ * which is far more reliable than high-accuracy GPS on desktop browsers.
424
+ */
425
+ /** Minimal shape of the browser Geolocation API we depend on (injectable for testing). */
426
+ interface GeolocationLike {
427
+ getCurrentPosition(success: (position: {
428
+ coords: {
429
+ latitude: number;
430
+ longitude: number;
431
+ accuracy?: number;
432
+ };
433
+ }) => void, error?: (err: {
434
+ code?: number;
435
+ } | unknown) => void, options?: {
436
+ enableHighAccuracy?: boolean;
437
+ timeout?: number;
438
+ maximumAge?: number;
439
+ }): void;
440
+ }
441
+ /** The consent-labelled result of a capture attempt. Coordinates present only on a successful fix. */
442
+ interface CapturedGeo {
443
+ latitude?: string;
444
+ longitude?: string;
445
+ accuracyMeters?: number;
446
+ /** The browser PERMISSION decision only — never downgraded by a failed/timed-out fix. */
447
+ consent: "granted" | "denied";
448
+ }
449
+ interface CaptureOptions {
450
+ /** High-accuracy (GPS) attempt budget before the network-based retry. Default 8000ms. */
451
+ timeoutMs?: number;
452
+ /** Low-accuracy (network) retry budget when the first attempt gets no fix. Default 12000ms. */
453
+ fallbackTimeoutMs?: number;
454
+ /** Override the geolocation source (tests). Defaults to `navigator.geolocation`. */
455
+ geolocation?: GeolocationLike;
456
+ }
457
+ /**
458
+ * Capture the browser's precise location. Resolves to a consent-labelled result and never
459
+ * throws/rejects. High-accuracy first; on a no-fix (not a denial) retries once at low accuracy.
460
+ */
461
+ declare function capturePreciseLocation(consented: boolean, opts?: CaptureOptions): Promise<CapturedGeo>;
462
+ //#endregion
463
+ //#region src/components/placement.d.ts
464
+ /**
465
+ * Collision-aware placement for the SDK's popovers.
466
+ *
467
+ * A menu pinned to one side is wrong somewhere: an avatar in a page header wants to open
468
+ * down-and-left, the same component in a sidebar footer has to open up-and-right. Rather than
469
+ * make every consumer work that out, measure the trigger against the viewport and pick.
470
+ *
471
+ * Deliberately hand-rolled: `@poly-x/react` has no runtime dependency but `@poly-x/core`, and a
472
+ * full positioning engine is a lot of surface to inherit for one menu. The component only needs
473
+ * to know *which side* — CSS does the actual placing — so this stays a pure function over
474
+ * rectangles, and is unit-testable without a layout engine.
475
+ */
476
+ type Side = "top" | "bottom" | "left" | "right";
477
+ type SideOption = Side | "auto";
478
+ type Align = "start" | "center" | "end";
479
+ interface Rect {
480
+ top: number;
481
+ bottom: number;
482
+ left: number;
483
+ right: number;
484
+ }
485
+ interface Size {
486
+ width: number;
487
+ height: number;
488
+ }
489
+ interface Viewport {
490
+ width: number;
491
+ height: number;
492
+ }
493
+ /**
494
+ * Which side the menu should open on.
495
+ *
496
+ * An explicit side is honoured when it fits and flipped to its opposite when it doesn't — a
497
+ * requested side that runs off-screen is a worse answer than the one that doesn't. `auto` takes
498
+ * the first side that fits, and if none do, the roomiest: something has to be chosen, and the
499
+ * least-bad option beats an arbitrary default.
500
+ */
501
+ declare function resolveSide(preferred: SideOption, trigger: Rect, menu: Size, viewport: Viewport, gap?: number): Side;
502
+ //#endregion
503
+ //#region src/components/user-button.d.ts
504
+ /** Every user-facing string, overridable for i18n / white-labelling. */
505
+ interface UserButtonLabels {
506
+ trigger: string;
507
+ manageAccount: string;
508
+ signOut: string;
509
+ signOutEverywhere: string;
510
+ }
511
+ interface UserButtonActionProps {
512
+ /**
513
+ * The menu entry's text — or the name of a built-in (`manageAccount`, `signOut`,
514
+ * `signOutEverywhere`) to place it at this position instead of appending a second copy.
515
+ */
516
+ label: string;
517
+ /** Icon rendered before the label. */
518
+ labelIcon?: ReactNode;
519
+ onClick?: () => void;
520
+ }
521
+ interface UserButtonLinkProps {
522
+ label: string;
523
+ labelIcon?: ReactNode;
524
+ href: string;
525
+ }
526
+ interface UserButtonSlotProps {
527
+ children?: ReactNode;
528
+ }
529
+ interface UserButtonProps {
530
+ /** Show the user's name next to the avatar. Default `false`. */
531
+ showName?: boolean;
532
+ /** Where to land after signing out. Default the app root. */
533
+ afterSignOutUrl?: string;
534
+ /**
535
+ * Consumer cleanup run after the session is revoked but before the redirect —
536
+ * clear app cookies, reset local stores, etc. Awaited; errors are swallowed so a
537
+ * failing hook can't trap the user. Applies to both "Sign out" and "Sign out everywhere".
538
+ */
539
+ onSignOut?: () => void | Promise<void>;
540
+ /**
541
+ * Destination for the "Manage account" entry. Omitted entirely when unset — PolyX has no
542
+ * hosted profile page, so the destination is the consumer's to own.
543
+ */
544
+ manageAccountUrl?: string;
545
+ /** Offer "Sign out everywhere" (ends every session for the user). Default `false`. */
546
+ showSignOutEverywhere?: boolean;
547
+ /** Start with the menu open. Default `false`. */
548
+ defaultOpen?: boolean;
549
+ /** Rendered while the session resolves, so the header doesn't jump. */
550
+ fallback?: ReactNode;
551
+ /** Appended to the root element's class list, for escape-hatch styling. */
552
+ className?: string;
553
+ /**
554
+ * Appended to the trigger button's class list. The SDK owns that button, so without this a
555
+ * consumer could style everything except the one element they place in their own layout.
556
+ */
557
+ triggerClassName?: string;
558
+ /** Notified when the menu opens or closes — hosts often coordinate their own chrome around it. */
559
+ onOpenChange?: (open: boolean) => void;
560
+ labels?: Partial<UserButtonLabels>;
561
+ /**
562
+ * `<UserButton.MenuItems>` with `<UserButton.Action>` / `<UserButton.Link>` / `<UserButton.Custom>`
563
+ * entries, and/or a `<UserButton.Header>` replacing the identity block.
564
+ */
565
+ children?: ReactNode;
566
+ /** Avatar size in pixels. Default 32. */
567
+ avatarSize?: number;
568
+ /**
569
+ * What the trigger shows. Replaces the avatar (and `showName`); the SDK still owns the button
570
+ * and its menu semantics, so a custom trigger costs no accessibility.
571
+ */
572
+ trigger?: ReactNode;
573
+ /**
574
+ * Which side the menu opens on. Default `auto` — measured against the viewport, because the
575
+ * right answer differs by where the trigger sits (a header avatar opens down, a sidebar-footer
576
+ * one has to open up). An explicit side is flipped only if it would run off-screen.
577
+ */
578
+ side?: SideOption;
579
+ /** Alignment along the chosen side. Default `end`. */
580
+ align?: Align;
581
+ }
582
+ /**
583
+ * The signed-in user surface: avatar trigger + a menu with their identity and sign-out
584
+ * (FR-SIGNIN-6). Reads only the hooks, so it behaves identically under either custody model
585
+ * — an httpOnly BFF session or an in-memory SPA one — and needs no provider in a
586
+ * `@poly-x/next` app.
587
+ *
588
+ * Custom entries compose declaratively:
589
+ *
590
+ * ```tsx
591
+ * <UserButton manageAccountUrl="/account">
592
+ * <UserButton.MenuItems>
593
+ * <UserButton.Action label="signOut" /> // reposition a built-in
594
+ * <UserButton.Link label="Docs" href="/docs" />
595
+ * </UserButton.MenuItems>
596
+ * </UserButton>
597
+ * ```
598
+ */
599
+ declare const UserButton: ((props: UserButtonProps) => ReactNode) & {
600
+ MenuItems: (props: UserButtonSlotProps) => ReactNode;
601
+ Action: (props: UserButtonActionProps) => ReactNode;
602
+ Link: (props: UserButtonLinkProps) => ReactNode; /** Arbitrary JSX as a menu row — bring your own controls. */
603
+ Custom: (props: UserButtonSlotProps) => ReactNode; /** Replaces the built-in identity block. */
604
+ Header: (props: UserButtonSlotProps) => ReactNode;
605
+ };
606
+ //#endregion
607
+ //#region src/components/user-avatar.d.ts
608
+ interface UserAvatarProps {
609
+ /** Image URL. Falls back to initials when absent or when the image fails to load. */
610
+ src?: string;
611
+ /** The name initials are derived from; also the image's alt text. */
612
+ name?: string;
613
+ /** Pixel size of the (square) avatar. Default 32. */
614
+ size?: number;
615
+ /** Appended to the root element's class list, for escape-hatch styling. */
616
+ className?: string;
617
+ }
618
+ /**
619
+ * Up to two initials from a display name. Handles the shapes the platform actually
620
+ * produces — "Ali Ikram", a bare username, an email address.
621
+ */
622
+ declare function initialsFrom(name: string | undefined): string;
623
+ /**
624
+ * The user's picture, or their initials. A missing avatar is the ordinary case rather than
625
+ * an error, so initials are a first-class state, not a broken image.
626
+ */
627
+ declare function UserAvatar({
628
+ src,
629
+ name,
630
+ size,
631
+ className
632
+ }: UserAvatarProps): ReactNode;
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
375
696
  //#region src/index.d.ts
376
697
  /**
377
698
  * @poly-x/react — PolyX SDK for React SPAs. Sign-in UI + orchestration land in
@@ -380,4 +701,4 @@ declare function AuthCallback({
380
701
  declare const PACKAGE_NAME = "@poly-x/react";
381
702
  declare const version = "0.0.0";
382
703
  //#endregion
383
- export { AuthCallback, type AuthCallbackMessage, type AuthCallbackProps, type AuthChannel, BroadcastAuthChannel, BroadcastChannelSessionChannel, type BrowserBridge, type CallbackParams, ForgotPassword, type ForgotPasswordLabels, type ForgotPasswordProps, type LoginController, type LoginControllerDeps, PACKAGE_NAME, PolyXContext, type PolyXContextValue, PolyXProvider, type PolyXProviderProps, PopupCancelledError, type PopupHandle, Protected, type ProtectedProps, ResetPassword, type ResetPasswordLabels, type ResetPasswordProps, SessionStoragePkceStore, SignIn, type SignInLabels, type SignInOptions, type SignInProps, type SignInVariant, type UseAuth, WebLocksLock, WindowBrowserBridge, createLoginController, 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 };