@luminaryworks/auth-react 0.3.1

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.
@@ -0,0 +1,356 @@
1
+ import { UserManager } from 'oidc-client-ts';
2
+ import * as react from 'react';
3
+ import { ReactNode, CSSProperties } from 'react';
4
+
5
+ interface LuminaryIdpConfig {
6
+ /**
7
+ * OIDC issuer. Prefer Auth Gateway (`http://localhost:3010/oidc`) so products
8
+ * stay IdP-agnostic; direct Logto issuer is OK for local MVP.
9
+ */
10
+ issuer: string;
11
+ clientId: string;
12
+ redirectUri: string;
13
+ /**
14
+ * Popup OIDC callback. Defaults to `redirectUri` (same `/auth/callback`).
15
+ * Must be registered on the IdP application.
16
+ */
17
+ popupRedirectUri?: string;
18
+ postLogoutRedirectUri?: string;
19
+ scopes?: string;
20
+ /** API resource indicator (access token `aud`) */
21
+ audience?: string;
22
+ /** localStorage key for access token */
23
+ tokenStorageKey?: string;
24
+ /**
25
+ * Logto Experience API base (no trailing slash), e.g. Auth Gateway
26
+ * `http://localhost:3010` or direct Logto `http://localhost:3001`.
27
+ * When set, Headless password sign-in is attempted before OIDC popup fallback.
28
+ */
29
+ experienceApiBase?: string;
30
+ }
31
+ interface LuminaryAuthSession {
32
+ accessToken: string;
33
+ idToken?: string;
34
+ expiresAt?: number;
35
+ profile?: Record<string, unknown>;
36
+ }
37
+ declare function isIdpConfigured(config: Partial<LuminaryIdpConfig>): config is LuminaryIdpConfig;
38
+ /** Resolve issuer from Auth Gateway base URL or direct IDP_ISSUER. */
39
+ declare function resolveIssuer(env: Record<string, string | undefined>): string | undefined;
40
+ /** Derive Experience API host from Gateway URL or by stripping `/oidc` from issuer. */
41
+ declare function resolveExperienceApiBase(env: Record<string, string | undefined>): string | undefined;
42
+ declare function readIdpConfigFromEnv(env: Record<string, string | undefined>): Partial<LuminaryIdpConfig>;
43
+
44
+ declare function createUserManager(config: LuminaryIdpConfig): UserManager;
45
+ declare function resetUserManager(): void;
46
+ interface SignInOptions {
47
+ returnUrl?: string;
48
+ /**
49
+ * Logto direct sign-in, e.g. `social:google` / `social:github` / `sso:<connectorId>`.
50
+ * Skips the hosted password page and opens the provider immediately.
51
+ */
52
+ directSignIn?: string;
53
+ }
54
+ declare function signInRedirect(config: LuminaryIdpConfig, returnUrlOrOptions?: string | SignInOptions): Promise<void>;
55
+ /**
56
+ * Persist PKCE state (same store as {@link createUserManager}) and return the
57
+ * authorize URL — used by Experience headless bootstrap before navigating to
58
+ * `redirectTo` from `/api/experience/submit`.
59
+ */
60
+ declare function prepareSignInRequestUrl(config: LuminaryIdpConfig, returnUrlOrOptions?: string | SignInOptions): Promise<string>;
61
+ /**
62
+ * OIDC login in a popup window (not an iframe). Opener keeps SPA state;
63
+ * callback page in the popup must call {@link handleSignInPopupCallback}.
64
+ */
65
+ declare function signInPopup(config: LuminaryIdpConfig, returnUrlOrOptions?: string | SignInOptions): Promise<{
66
+ session: LuminaryAuthSession;
67
+ returnUrl?: string;
68
+ }>;
69
+ declare function handleSignInCallback(config: LuminaryIdpConfig): Promise<{
70
+ session: LuminaryAuthSession;
71
+ returnUrl?: string;
72
+ }>;
73
+ /**
74
+ * Complete popup OIDC in the popup window. Notifies the opener and closes.
75
+ * Do not run product SSO exchange here — the opener handles that after signInPopup resolves.
76
+ */
77
+ declare function handleSignInPopupCallback(config: LuminaryIdpConfig): Promise<void>;
78
+ /** True when this window is likely an OIDC popup callback. */
79
+ declare function isOidcPopupWindow(): boolean;
80
+ declare function signOutRedirect(config: LuminaryIdpConfig): Promise<void>;
81
+ declare function getStoredAccessToken(config: LuminaryIdpConfig): string | null;
82
+ declare function clearStoredSession(config: LuminaryIdpConfig): void;
83
+ declare function getCurrentUser(config: LuminaryIdpConfig): Promise<LuminaryAuthSession | null>;
84
+
85
+ /**
86
+ * Logto Experience API (Headless) client.
87
+ * Prefer same-origin Experience base (SPA proxies /api/experience) so cookies work on HTTP localhost.
88
+ * @see https://docs.logto.io/docs/recipes/customize-token-claims (Experience API recipes)
89
+ */
90
+ type ExperienceIdentifierType = "email" | "username" | "phone";
91
+ interface ExperiencePasswordSignInInput {
92
+ /** Experience API origin (no trailing slash). Prefer SPA origin when proxied. */
93
+ apiBase: string;
94
+ identifier: string;
95
+ password: string;
96
+ identifierType?: ExperienceIdentifierType;
97
+ /** Required to bootstrap the OIDC interaction cookie before Experience calls. */
98
+ issuer?: string;
99
+ clientId?: string;
100
+ redirectUri?: string;
101
+ audience?: string;
102
+ scopes?: string;
103
+ /** Carried in OIDC state for post-login return (UserManager PKCE). */
104
+ returnUrl?: string;
105
+ }
106
+ interface ExperiencePasswordSignInResult {
107
+ /** Continue URL from Experience submit (complete OIDC interaction). */
108
+ redirectTo?: string;
109
+ raw?: unknown;
110
+ }
111
+ /**
112
+ * Force authorize onto the Experience/SPA origin so Set-Cookie lands on the same
113
+ * host as subsequent `/api/experience` calls (dev proxy strips Domain).
114
+ */
115
+ declare function sameOriginAuthorizeUrl(authorizeUrl: string, apiBase: string): string;
116
+ /**
117
+ * Headless password sign-in via Logto Experience API.
118
+ * Requires an OIDC interaction cookie (see {@link bootstrapOidcInteraction}) and
119
+ * same-site Experience calls (SPA proxy or Auth Gateway on the SPA origin).
120
+ *
121
+ * On success, follow {@link ExperiencePasswordSignInResult.redirectTo} in the same
122
+ * window — that URL completes the PKCE round-trip started by createSigninRequest.
123
+ * Do not start a second authorize (that shows Logto `/sign-in` again).
124
+ *
125
+ * Tries the guessed identifier type first, then the alternate email/username so
126
+ * users can sign in with either when both methods are enabled on the IdP.
127
+ */
128
+ declare function experiencePasswordSignIn(input: ExperiencePasswordSignInInput): Promise<ExperiencePasswordSignInResult>;
129
+ interface ExperienceSocialConnector {
130
+ id: string;
131
+ target: string;
132
+ name: string;
133
+ logo?: string;
134
+ }
135
+ /**
136
+ * Public Logto sign-in experience (no auth). Returns enabled social connectors
137
+ * in SIE order — use for Headless social buttons (google / github / x / …).
138
+ */
139
+ declare function fetchSocialConnectors(input: {
140
+ /** Same origin as Experience / IdP (e.g. SPA proxy or http://localhost:3001). */
141
+ apiBase: string;
142
+ appId?: string;
143
+ }): Promise<ExperienceSocialConnector[]>;
144
+
145
+ /**
146
+ * AuthGate — single-flight 401 recovery with optional token refresh + reauth UI.
147
+ * Products wire tryRefresh / onReauthRequired; API clients call handleUnauthorized + waitIfBlocked.
148
+ */
149
+ type AuthGatePhase = "idle" | "refreshing" | "reauth";
150
+ type UnauthorizedDisposition = "retry" | "fail";
151
+ interface AuthGateSnapshot {
152
+ phase: AuthGatePhase;
153
+ /** True while refresh or interactive reauth is in flight. */
154
+ blocked: boolean;
155
+ lastError?: string;
156
+ }
157
+ interface AuthGateConfig {
158
+ /**
159
+ * Attempt silent product-token (or OIDC) refresh before showing reauth UI.
160
+ * Return true if a new session was established.
161
+ */
162
+ tryRefresh?: () => Promise<boolean>;
163
+ /** Called once when interactive reauth is required (open modal / popup). */
164
+ onReauthRequired?: () => void;
165
+ /** Called when reauth succeeds or is cancelled (phase back to idle). */
166
+ onSettled?: (ok: boolean) => void;
167
+ }
168
+ declare class AuthGateCoordinator {
169
+ private phase;
170
+ private lastError?;
171
+ /** Cached for useSyncExternalStore — must keep referential equality when unchanged. */
172
+ private snapshot;
173
+ private config;
174
+ private flight;
175
+ private reauthWaiters;
176
+ private listeners;
177
+ configure(config: AuthGateConfig): void;
178
+ getSnapshot: () => AuthGateSnapshot;
179
+ subscribe: (listener: () => void) => (() => void);
180
+ private emit;
181
+ private setPhase;
182
+ /** Await while a recovery is in flight (pause outbound API traffic). */
183
+ waitIfBlocked(): Promise<void>;
184
+ /**
185
+ * Single-flight 401 handler.
186
+ * Concurrent callers share one recovery; all receive the same disposition.
187
+ */
188
+ handleUnauthorized(errorMessage?: string): Promise<UnauthorizedDisposition>;
189
+ /** Open reauth UI without a preceding 401 (cold start / deep link). */
190
+ requestReauth(errorMessage?: string): Promise<UnauthorizedDisposition>;
191
+ /** Product calls after popup / Headless login restored the session. */
192
+ completeReauth(): void;
193
+ /** User dismissed reauth; fail queued recovery waiters. */
194
+ cancelReauth(errorMessage?: string): void;
195
+ private runRecovery;
196
+ }
197
+ /** Process-wide gate (one per SPA bundle). */
198
+ declare const authGate: AuthGateCoordinator;
199
+ /**
200
+ * Run an async request; on 401, recover via AuthGate and retry once.
201
+ * `isUnauthorized` defaults to checking `status === 401` or `statusCode === 401`.
202
+ */
203
+ declare function withAuthGateRetry<T>(execute: () => Promise<T>, options?: {
204
+ isUnauthorized?: (error: unknown) => boolean;
205
+ /** Skip gate (e.g. refresh / sso / login endpoints). */
206
+ bypass?: boolean;
207
+ }): Promise<T>;
208
+
209
+ interface LuminaryAuthContextValue {
210
+ configured: boolean;
211
+ session: LuminaryAuthSession | null;
212
+ accessToken: string | null;
213
+ loading: boolean;
214
+ login: (returnUrl?: string) => Promise<void>;
215
+ /** Popup OIDC — keeps the opener SPA mounted. */
216
+ loginPopup: (returnUrl?: string) => Promise<LuminaryAuthSession>;
217
+ logout: () => Promise<void>;
218
+ completeCallback: () => Promise<{
219
+ returnUrl?: string;
220
+ }>;
221
+ refreshSession: () => Promise<void>;
222
+ }
223
+ interface LuminaryAuthProviderProps {
224
+ config?: Partial<LuminaryIdpConfig>;
225
+ children: React.ReactNode;
226
+ }
227
+ declare function LuminaryAuthProvider({ config: configProp, children }: LuminaryAuthProviderProps): react.JSX.Element;
228
+ declare function useLuminaryAuth(): LuminaryAuthContextValue;
229
+
230
+ /** @deprecated Prefer dynamic connectors from IdP; kept for prop typing. */
231
+ type SocialProviderTarget = string;
232
+ interface HeadlessLoginLabels {
233
+ title?: string;
234
+ subtitle?: string;
235
+ identifierPlaceholder?: string;
236
+ passwordPlaceholder?: string;
237
+ submitPassword?: string;
238
+ /** @deprecated Use social buttons; kept for backward-compatible label overrides. */
239
+ submitSso?: string;
240
+ submitGoogle?: string;
241
+ submitGithub?: string;
242
+ socialDivider?: string;
243
+ hint?: string;
244
+ cancel?: string;
245
+ experienceUnavailable?: string;
246
+ showPassword?: string;
247
+ hidePassword?: string;
248
+ }
249
+ interface HeadlessLoginPanelProps {
250
+ config: Partial<LuminaryIdpConfig>;
251
+ /** Product display name shown as brand signal */
252
+ productName: string;
253
+ logoSrc?: string;
254
+ labels?: HeadlessLoginLabels;
255
+ returnUrl?: string;
256
+ /** Prefer popup (default) for reauth; use redirect for full-page login. */
257
+ mode?: "popup" | "redirect";
258
+ /**
259
+ * Show Experience social connectors (Google / GitHub / …).
260
+ * Default `true`. Set `false` for admin / internal consoles that only allow
261
+ * password (or enterprise SSO via IdP) — hides divider + social buttons and
262
+ * skips fetching connectors.
263
+ * Equivalent to `socialProviders={[]}` when false.
264
+ */
265
+ showSocialConnectors?: boolean;
266
+ /**
267
+ * Social providers (when `showSocialConnectors` is not `false`):
268
+ * - omit / `"auto"` — load enabled connectors from IdP (google, github, x, …)
269
+ * - `string[]` — only these targets (still prefers IdP logos/names when available)
270
+ * - `[]` — hide social buttons
271
+ */
272
+ socialProviders?: "auto" | SocialProviderTarget[];
273
+ showCancel?: boolean;
274
+ onCancel?: () => void;
275
+ /**
276
+ * Called after OIDC session is obtained (popup) or before redirect navigation.
277
+ * Products typically exchange OIDC access token → product JWT here.
278
+ */
279
+ onOidcSession?: (session: LuminaryAuthSession, returnUrl?: string) => Promise<void> | void;
280
+ /** When Experience returns redirectTo, open it (default: same-tab assign). */
281
+ onExperienceRedirect?: (redirectTo: string) => void;
282
+ footer?: ReactNode;
283
+ className?: string;
284
+ style?: CSSProperties;
285
+ /**
286
+ * Brand accent for primary CTA / focus / product label.
287
+ * Defaults to `#3a84ff` (DataLuminary / BlockyEdu).
288
+ */
289
+ themeColor?: string;
290
+ }
291
+ /** Shared default accent for product login panels. */
292
+ declare const DEFAULT_LOGIN_THEME_COLOR = "#3a84ff";
293
+ declare function HeadlessLoginPanel({ config, productName, logoSrc, labels: labelsProp, returnUrl, mode, showSocialConnectors, socialProviders, showCancel, onCancel, onOidcSession, onExperienceRedirect, footer, className, style, themeColor, }: HeadlessLoginPanelProps): react.JSX.Element;
294
+
295
+ interface ReauthOverlayProps {
296
+ config: Partial<LuminaryIdpConfig>;
297
+ productName: string;
298
+ logoSrc?: string;
299
+ labels?: HeadlessLoginLabels;
300
+ returnUrl?: string;
301
+ /** Brand accent forwarded to HeadlessLoginPanel (default `#3a84ff`). */
302
+ themeColor?: string;
303
+ /** Exchange OIDC token / persist product session, then AuthGate completes. */
304
+ onOidcSession: (session: LuminaryAuthSession, returnUrl?: string) => Promise<void> | void;
305
+ /** Optional custom body instead of HeadlessLoginPanel */
306
+ children?: ReactNode;
307
+ /** Allow dismiss → cancelReauth (default true for mid-session; false for hard gate). */
308
+ dismissible?: boolean;
309
+ onDismiss?: () => void;
310
+ }
311
+ /**
312
+ * Full-viewport soft gate shown while AuthGate phase is `reauth` (or `refreshing` spinner).
313
+ * Does not iframe the IdP — uses Headless panel + OIDC popup.
314
+ */
315
+ declare function ReauthOverlay({ config, productName, logoSrc, labels, returnUrl, themeColor, onOidcSession, children, dismissible, onDismiss, }: ReauthOverlayProps): react.JSX.Element | null;
316
+
317
+ interface AuthGateContextValue {
318
+ snapshot: AuthGateSnapshot;
319
+ requestReauth: (message?: string) => Promise<"retry" | "fail">;
320
+ completeReauth: () => void;
321
+ cancelReauth: (message?: string) => void;
322
+ }
323
+ interface AuthGateProviderProps extends AuthGateConfig {
324
+ children: ReactNode;
325
+ /** When set, mounts {@link ReauthOverlay} automatically. */
326
+ overlay?: Omit<ReauthOverlayProps, "onOidcSession"> & {
327
+ onOidcSession: (session: LuminaryAuthSession, returnUrl?: string) => Promise<void> | void;
328
+ config: Partial<LuminaryIdpConfig>;
329
+ };
330
+ }
331
+ declare function AuthGateProvider({ children, tryRefresh, onReauthRequired, onSettled, overlay, }: AuthGateProviderProps): react.JSX.Element;
332
+ declare function useAuthGate(): AuthGateContextValue;
333
+ /** Optional hook — returns null outside provider (for shared API helpers). */
334
+ declare function useAuthGateOptional(): AuthGateContextValue | null;
335
+
336
+ interface PostLoginPathOptions {
337
+ /** sessionStorage key (product-scoped), e.g. `dv:postLoginPath` */
338
+ storageKey: string;
339
+ /** Default destination when nothing remembered */
340
+ defaultPath?: string;
341
+ /** Extra paths that must not be used as return targets */
342
+ unsafePrefixes?: string[];
343
+ }
344
+ /**
345
+ * sessionStorage helpers for OIDC redirect return paths.
346
+ * Product-specific ACL checks (e.g. 403 fallback) stay in the product.
347
+ */
348
+ declare function createPostLoginPathHelpers(options: PostLoginPathOptions): {
349
+ rememberPostLoginPath: (path: string) => void;
350
+ peekPostLoginPath: () => string | undefined;
351
+ consumePostLoginPath: (fallback?: string) => string;
352
+ normalizeAppPath: (path: string) => string;
353
+ isUnsafeReturnPath: (path: string) => boolean;
354
+ };
355
+
356
+ export { type AuthGateConfig, type AuthGatePhase, AuthGateProvider, type AuthGateProviderProps, type AuthGateSnapshot, DEFAULT_LOGIN_THEME_COLOR, type ExperienceIdentifierType, type ExperiencePasswordSignInInput, type ExperiencePasswordSignInResult, type ExperienceSocialConnector, type HeadlessLoginLabels, HeadlessLoginPanel, type HeadlessLoginPanelProps, LuminaryAuthProvider, type LuminaryAuthProviderProps, type LuminaryAuthSession, type LuminaryIdpConfig, type PostLoginPathOptions, ReauthOverlay, type ReauthOverlayProps, type SignInOptions, type SocialProviderTarget, type UnauthorizedDisposition, authGate, clearStoredSession, createPostLoginPathHelpers, createUserManager, experiencePasswordSignIn, fetchSocialConnectors, getCurrentUser, getStoredAccessToken, handleSignInCallback, handleSignInPopupCallback, isIdpConfigured, isOidcPopupWindow, prepareSignInRequestUrl, readIdpConfigFromEnv, resetUserManager, resolveExperienceApiBase, resolveIssuer, sameOriginAuthorizeUrl, signInPopup, signInRedirect, signOutRedirect, useAuthGate, useAuthGateOptional, useLuminaryAuth, withAuthGateRetry };