najm-auth 1.1.44 → 2.0.2

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.
@@ -77,6 +77,11 @@ interface AuthClientConfig {
77
77
  /** Request timeout in milliseconds (default: 30000) */
78
78
  timeout?: number;
79
79
  }
80
+ type OAuthProvider = 'google';
81
+ interface OAuthLoginOptions {
82
+ /** Same-origin frontend path after OAuth completes. */
83
+ returnTo?: string;
84
+ }
80
85
  /**
81
86
  * Auth event types
82
87
  */
@@ -182,6 +187,11 @@ declare class NajmAuthClient {
182
187
  password: string;
183
188
  }): Promise<AuthUser>;
184
189
  register(data: Record<string, unknown>): Promise<AuthUser>;
190
+ getOAuthLoginUrl(provider: OAuthProvider, options?: OAuthLoginOptions): string;
191
+ loginWithOAuth(provider: OAuthProvider, options?: OAuthLoginOptions): void;
192
+ loginWithGoogle(options?: OAuthLoginOptions): void;
193
+ linkOAuthAccount(provider: OAuthProvider, options?: OAuthLoginOptions): Promise<void>;
194
+ completeOAuthLogin(): Promise<AuthUser>;
185
195
  logout(): Promise<void>;
186
196
  refresh(): Promise<void>;
187
197
  fetchUser(): Promise<AuthUser | null>;
@@ -230,10 +240,11 @@ declare class NajmAuthClient {
230
240
  private handleTabMessage;
231
241
  private notify;
232
242
  private emit;
243
+ private validateReturnTo;
233
244
  }
234
245
  /**
235
246
  * Factory function to create an auth client.
236
247
  */
237
248
  declare function createAuthClient(config: AuthClientConfig): NajmAuthClient;
238
249
 
239
- export { AuthError as A, type DecodedToken as D, FetchClient as F, type HydrateSession as H, NajmAuthClient as N, type RetryConfig as R, type SyncPayload as S, type TabSyncMessage as T, type AuthClientConfig as a, type AuthEventMap as b, createAuthClient as c, type AuthState as d, type AuthUser as e, type AuthEvent as f, type AuthEventHandler as g, type ServerResponse as h, type TokenPair as i, type RequestOptions as j };
250
+ export { AuthError as A, type DecodedToken as D, FetchClient as F, type HydrateSession as H, NajmAuthClient as N, type OAuthProvider as O, type RetryConfig as R, type SyncPayload as S, type TabSyncMessage as T, type AuthClientConfig as a, type AuthEventMap as b, createAuthClient as c, type AuthState as d, type AuthUser as e, type AuthEvent as f, type AuthEventHandler as g, type ServerResponse as h, type TokenPair as i, type RequestOptions as j, type OAuthLoginOptions as k };
@@ -1,5 +1,5 @@
1
- import { D as DecodedToken, T as TabSyncMessage, S as SyncPayload } from '../NajmAuthClient-D08--i69.js';
2
- export { a as AuthClientConfig, A as AuthError, f as AuthEvent, g as AuthEventHandler, b as AuthEventMap, d as AuthState, e as AuthUser, F as FetchClient, H as HydrateSession, N as NajmAuthClient, j as RequestOptions, R as RetryConfig, h as ServerResponse, i as TokenPair, c as createAuthClient } from '../NajmAuthClient-D08--i69.js';
1
+ import { D as DecodedToken, T as TabSyncMessage, S as SyncPayload } from '../NajmAuthClient-B9dGk9MH.js';
2
+ export { a as AuthClientConfig, A as AuthError, f as AuthEvent, g as AuthEventHandler, b as AuthEventMap, d as AuthState, e as AuthUser, F as FetchClient, H as HydrateSession, N as NajmAuthClient, k as OAuthLoginOptions, O as OAuthProvider, j as RequestOptions, R as RetryConfig, h as ServerResponse, i as TokenPair, c as createAuthClient } from '../NajmAuthClient-B9dGk9MH.js';
3
3
 
4
4
  /**
5
5
  * Decode a JWT token payload without verification.
@@ -138,7 +138,7 @@ function decodeToken(token) {
138
138
  }
139
139
  __name(decodeToken, "decodeToken");
140
140
  function isTokenExpired(decoded) {
141
- if (!decoded.exp) return false;
141
+ if (!decoded.exp) return true;
142
142
  return Date.now() / 1e3 >= decoded.exp;
143
143
  }
144
144
  __name(isTokenExpired, "isTokenExpired");
@@ -264,6 +264,43 @@ var NajmAuthClient = class _NajmAuthClient {
264
264
  );
265
265
  return res.data;
266
266
  }
267
+ getOAuthLoginUrl(provider, options = {}) {
268
+ const baseURL = this.config.baseURL.replace(/\/$/, "");
269
+ const authPrefix = this.prefix.startsWith("/") ? this.prefix : `/${this.prefix}`;
270
+ const raw = `${baseURL}${authPrefix}/oauth/${provider}/start`;
271
+ const absolute = /^[a-z][a-z\d+.-]*:\/\//i.test(raw);
272
+ const url = new URL(raw, absolute ? void 0 : "https://najm.invalid");
273
+ if (options.returnTo) {
274
+ url.searchParams.set("returnTo", this.validateReturnTo(options.returnTo));
275
+ }
276
+ return absolute ? url.toString() : `${url.pathname}${url.search}`;
277
+ }
278
+ loginWithOAuth(provider, options) {
279
+ if (typeof window === "undefined") {
280
+ throw new Error("OAuth login requires a browser environment");
281
+ }
282
+ window.location.assign(this.getOAuthLoginUrl(provider, options));
283
+ }
284
+ loginWithGoogle(options) {
285
+ this.loginWithOAuth("google", options);
286
+ }
287
+ async linkOAuthAccount(provider, options = {}) {
288
+ const query = options.returnTo ? `?${new URLSearchParams({ returnTo: this.validateReturnTo(options.returnTo) })}` : "";
289
+ const res = await this.api.post(
290
+ `${this.prefix}/oauth/${provider}/link${query}`
291
+ );
292
+ if (!res.data?.authorizationUrl) throw new Error("OAuth provider did not return an authorization URL");
293
+ if (typeof window === "undefined") throw new Error("OAuth linking requires a browser environment");
294
+ window.location.assign(res.data.authorizationUrl);
295
+ }
296
+ async completeOAuthLogin() {
297
+ await this.refresh();
298
+ const user = await this.fetchUser();
299
+ if (!user) throw new Error("OAuth session could not be completed");
300
+ this.tabSync?.broadcastSync(this.getSyncPayload());
301
+ this.emit("login", user);
302
+ return user;
303
+ }
267
304
  async logout() {
268
305
  this.resetState();
269
306
  this.tabSync?.broadcastLogout();
@@ -555,6 +592,18 @@ var NajmAuthClient = class _NajmAuthClient {
555
592
  this.eventListeners.get("stateChange")?.forEach((h) => h(this.state));
556
593
  }
557
594
  }
595
+ validateReturnTo(value) {
596
+ const candidate = value.trim();
597
+ if (!candidate.startsWith("/") || candidate.startsWith("//") || candidate.includes("\\")) {
598
+ throw new Error("returnTo must be a same-origin path");
599
+ }
600
+ const base = new URL("https://najm.invalid");
601
+ const parsed = new URL(candidate, base);
602
+ if (parsed.origin !== base.origin || parsed.username || parsed.password) {
603
+ throw new Error("returnTo must be a same-origin path");
604
+ }
605
+ return `${parsed.pathname}${parsed.search}${parsed.hash}`;
606
+ }
558
607
  };
559
608
  function createAuthClient(config) {
560
609
  return new NajmAuthClient(config);
@@ -1,7 +1,7 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import * as react from 'react';
3
3
  import { ReactNode, CSSProperties, ReactElement } from 'react';
4
- import { N as NajmAuthClient, H as HydrateSession, d as AuthState, e as AuthUser, A as AuthError, f as AuthEvent, b as AuthEventMap } from '../../NajmAuthClient-D08--i69.js';
4
+ import { N as NajmAuthClient, H as HydrateSession, d as AuthState, e as AuthUser, A as AuthError, k as OAuthLoginOptions, f as AuthEvent, b as AuthEventMap } from '../../NajmAuthClient-B9dGk9MH.js';
5
5
 
6
6
  interface AuthProviderProps {
7
7
  client: NajmAuthClient;
@@ -162,6 +162,27 @@ interface UseChangePasswordReturn {
162
162
  }
163
163
  declare function useChangePassword(opts?: UseChangePasswordOptions): UseChangePasswordReturn;
164
164
 
165
+ interface UseGoogleLoginOptions {
166
+ onError?: (error: AuthError | Error) => void;
167
+ }
168
+ interface UseGoogleLoginReturn {
169
+ loginWithGoogle: (options?: OAuthLoginOptions) => void;
170
+ linkGoogle: (options?: OAuthLoginOptions) => Promise<void>;
171
+ isRedirecting: boolean;
172
+ error: AuthError | Error | null;
173
+ }
174
+ declare function useGoogleLogin(options?: UseGoogleLoginOptions): UseGoogleLoginReturn;
175
+
176
+ interface UseOAuthCallbackOptions {
177
+ onSuccess?: (user: AuthUser) => void;
178
+ onError?: (error: AuthError | Error) => void;
179
+ }
180
+ declare function useOAuthCallback(options?: UseOAuthCallbackOptions): {
181
+ complete: () => Promise<AuthUser>;
182
+ isLoading: boolean;
183
+ error: AuthError | Error;
184
+ };
185
+
165
186
  /**
166
187
  * Subscribe to a specific auth event. Cleans up on unmount.
167
188
  * Uses a stable ref for the handler to avoid re-subscribing on every render.
@@ -537,4 +558,22 @@ interface RedirectToLoginProps {
537
558
  */
538
559
  declare function RedirectToLogin({ to, preserveFrom }: RedirectToLoginProps): any;
539
560
 
540
- export { AuthBoundary, type AuthEventEntry, AuthGate, AuthLoading, AuthProvider, Can, IfAuth, LoginButton, PermissionList, Protected, RedirectToLogin, Role, type SessionStatus, SignOutButton, SignedIn, SignedOut, UserAvatar, UserEmail, UserName, UserRole, useAuth, useAuthClient, useAuthEvent, useAuthEvents, useChangePassword, useForgotPassword, useLogin, useLogout, usePermissions, useRegister, useResetPassword, useSession, useUser };
561
+ interface GoogleLoginButtonProps extends OAuthLoginOptions {
562
+ children: ReactNode;
563
+ onError?: (error: Error) => void;
564
+ }
565
+ declare function GoogleLoginButton({ children, returnTo, onError }: GoogleLoginButtonProps): ReactElement<{
566
+ onClick?: (event: unknown) => void;
567
+ disabled?: boolean;
568
+ }, string | react.JSXElementConstructor<any>>;
569
+
570
+ interface OAuthCallbackProps {
571
+ fallback?: ReactNode;
572
+ errorFallback?: ReactNode | ((props: {
573
+ error: Error;
574
+ }) => ReactNode);
575
+ defaultRedirect?: string;
576
+ }
577
+ declare function OAuthCallback({ fallback, errorFallback, defaultRedirect, }: OAuthCallbackProps): react_jsx_runtime.JSX.Element;
578
+
579
+ export { AuthBoundary, type AuthEventEntry, AuthGate, AuthLoading, AuthProvider, Can, GoogleLoginButton, IfAuth, LoginButton, OAuthCallback, PermissionList, Protected, RedirectToLogin, Role, type SessionStatus, SignOutButton, SignedIn, SignedOut, UserAvatar, UserEmail, UserName, UserRole, useAuth, useAuthClient, useAuthEvent, useAuthEvents, useChangePassword, useForgotPassword, useGoogleLogin, useLogin, useLogout, useOAuthCallback, usePermissions, useRegister, useResetPassword, useSession, useUser };
@@ -310,11 +310,74 @@ function useChangePassword(opts) {
310
310
  }
311
311
  __name(useChangePassword, "useChangePassword");
312
312
 
313
+ // src/client/react/useGoogleLogin.ts
314
+ import { useCallback as useCallback7, useState as useState8 } from "react";
315
+ function useGoogleLogin(options) {
316
+ const client = useAuthClient();
317
+ const [isRedirecting, setIsRedirecting] = useState8(false);
318
+ const [error, setError] = useState8(null);
319
+ const fail = useCallback7((value) => {
320
+ const next = value instanceof Error ? value : new Error(String(value));
321
+ setIsRedirecting(false);
322
+ setError(next);
323
+ options?.onError?.(next);
324
+ }, [options?.onError]);
325
+ const loginWithGoogle = useCallback7((loginOptions) => {
326
+ setError(null);
327
+ setIsRedirecting(true);
328
+ try {
329
+ client.loginWithGoogle(loginOptions);
330
+ } catch (value) {
331
+ fail(value);
332
+ }
333
+ }, [client, fail]);
334
+ const linkGoogle = useCallback7(async (loginOptions) => {
335
+ setError(null);
336
+ setIsRedirecting(true);
337
+ try {
338
+ await client.linkOAuthAccount("google", loginOptions);
339
+ } catch (value) {
340
+ fail(value);
341
+ }
342
+ }, [client, fail]);
343
+ return { loginWithGoogle, linkGoogle, isRedirecting, error };
344
+ }
345
+ __name(useGoogleLogin, "useGoogleLogin");
346
+
347
+ // src/client/react/useOAuthCallback.ts
348
+ import { useCallback as useCallback8, useRef as useRef3, useState as useState9 } from "react";
349
+ function useOAuthCallback(options) {
350
+ const client = useAuthClient();
351
+ const promise = useRef3(null);
352
+ const [isLoading, setIsLoading] = useState9(false);
353
+ const [error, setError] = useState9(null);
354
+ const complete = useCallback8(async () => {
355
+ if (promise.current) return promise.current;
356
+ setIsLoading(true);
357
+ setError(null);
358
+ promise.current = client.completeOAuthLogin();
359
+ try {
360
+ const user = await promise.current;
361
+ options?.onSuccess?.(user);
362
+ return user;
363
+ } catch (value) {
364
+ const next = value instanceof Error ? value : new Error(String(value));
365
+ setError(next);
366
+ options?.onError?.(next);
367
+ throw next;
368
+ } finally {
369
+ setIsLoading(false);
370
+ }
371
+ }, [client, options?.onSuccess, options?.onError]);
372
+ return { complete, isLoading, error };
373
+ }
374
+ __name(useOAuthCallback, "useOAuthCallback");
375
+
313
376
  // src/client/react/useAuthEvent.ts
314
- import { useEffect as useEffect3, useRef as useRef3 } from "react";
377
+ import { useEffect as useEffect3, useRef as useRef4 } from "react";
315
378
  function useAuthEvent(event, handler) {
316
379
  const client = useAuthClient();
317
- const handlerRef = useRef3(handler);
380
+ const handlerRef = useRef4(handler);
318
381
  handlerRef.current = handler;
319
382
  useEffect3(() => {
320
383
  const listener = /* @__PURE__ */ __name((data) => handlerRef.current(data), "listener");
@@ -325,7 +388,7 @@ function useAuthEvent(event, handler) {
325
388
  __name(useAuthEvent, "useAuthEvent");
326
389
 
327
390
  // src/client/react/useAuthEvents.ts
328
- import { useEffect as useEffect4, useRef as useRef4, useState as useState8 } from "react";
391
+ import { useEffect as useEffect4, useRef as useRef5, useState as useState10 } from "react";
329
392
  var ALL_EVENTS = [
330
393
  "login",
331
394
  "logout",
@@ -338,8 +401,8 @@ var ALL_EVENTS = [
338
401
  function useAuthEvents(opts) {
339
402
  const client = useAuthClient();
340
403
  const max = opts?.maxEntries ?? 100;
341
- const [entries, setEntries] = useState8([]);
342
- const maxRef = useRef4(max);
404
+ const [entries, setEntries] = useState10([]);
405
+ const maxRef = useRef5(max);
343
406
  maxRef.current = max;
344
407
  useEffect4(() => {
345
408
  const cleanups = [];
@@ -619,14 +682,73 @@ function RedirectToLogin({ to = "/login", preserveFrom = true }) {
619
682
  return null;
620
683
  }
621
684
  __name(RedirectToLogin, "RedirectToLogin");
685
+
686
+ // src/client/react/GoogleLoginButton.tsx
687
+ import { Children as Children3, cloneElement as cloneElement3, isValidElement as isValidElement3 } from "react";
688
+ function GoogleLoginButton({ children, returnTo, onError }) {
689
+ const { loginWithGoogle, isRedirecting } = useGoogleLogin({ onError });
690
+ const child = Children3.only(children);
691
+ if (!isValidElement3(child)) {
692
+ throw new Error("<GoogleLoginButton> expects a single React element as its child");
693
+ }
694
+ const element = child;
695
+ return cloneElement3(element, {
696
+ onClick: /* @__PURE__ */ __name((event) => {
697
+ element.props.onClick?.(event);
698
+ if (!event?.defaultPrevented) {
699
+ loginWithGoogle({ returnTo });
700
+ }
701
+ }, "onClick"),
702
+ disabled: Boolean(element.props.disabled || isRedirecting)
703
+ });
704
+ }
705
+ __name(GoogleLoginButton, "GoogleLoginButton");
706
+
707
+ // src/client/react/OAuthCallback.tsx
708
+ import { useEffect as useEffect7 } from "react";
709
+ import { Fragment as Fragment14, jsx as jsx16 } from "react/jsx-runtime";
710
+ var safeReturnTo = /* @__PURE__ */ __name((value, fallback) => {
711
+ const safeFallback = fallback.startsWith("/") && !fallback.startsWith("//") && !fallback.includes("\\") ? fallback : "/";
712
+ if (!value || !value.startsWith("/") || value.startsWith("//") || value.includes("\\")) return safeFallback;
713
+ try {
714
+ const base = new URL("https://najm.invalid");
715
+ const parsed = new URL(value, base);
716
+ if (parsed.origin !== base.origin) return safeFallback;
717
+ return `${parsed.pathname}${parsed.search}${parsed.hash}`;
718
+ } catch {
719
+ return safeFallback;
720
+ }
721
+ }, "safeReturnTo");
722
+ function OAuthCallback({
723
+ fallback = null,
724
+ errorFallback = null,
725
+ defaultRedirect = "/"
726
+ }) {
727
+ const { complete, error } = useOAuthCallback();
728
+ useEffect7(() => {
729
+ complete().then(() => {
730
+ if (typeof window === "undefined") return;
731
+ const params = new URLSearchParams(window.location.search);
732
+ window.location.replace(safeReturnTo(params.get("returnTo"), defaultRedirect));
733
+ }).catch(() => {
734
+ });
735
+ }, [complete, defaultRedirect]);
736
+ if (error) {
737
+ return /* @__PURE__ */ jsx16(Fragment14, { children: typeof errorFallback === "function" ? errorFallback({ error }) : errorFallback });
738
+ }
739
+ return /* @__PURE__ */ jsx16(Fragment14, { children: fallback });
740
+ }
741
+ __name(OAuthCallback, "OAuthCallback");
622
742
  export {
623
743
  AuthBoundary,
624
744
  AuthGate,
625
745
  AuthLoading,
626
746
  AuthProvider,
627
747
  Can,
748
+ GoogleLoginButton,
628
749
  IfAuth,
629
750
  LoginButton,
751
+ OAuthCallback,
630
752
  PermissionList,
631
753
  Protected,
632
754
  RedirectToLogin,
@@ -644,8 +766,10 @@ export {
644
766
  useAuthEvents,
645
767
  useChangePassword,
646
768
  useForgotPassword,
769
+ useGoogleLogin,
647
770
  useLogin,
648
771
  useLogout,
772
+ useOAuthCallback,
649
773
  usePermissions,
650
774
  useRegister,
651
775
  useResetPassword,
@@ -1,4 +1,4 @@
1
- import { e as AuthUser, F as FetchClient, R as RetryConfig, N as NajmAuthClient } from '../../NajmAuthClient-D08--i69.js';
1
+ import { e as AuthUser, F as FetchClient, R as RetryConfig, N as NajmAuthClient } from '../../NajmAuthClient-B9dGk9MH.js';
2
2
  export { withAuthMiddleware } from '../edge.js';
3
3
  import 'next/server';
4
4
 
@@ -588,6 +588,43 @@ var NajmAuthClient = class _NajmAuthClient {
588
588
  );
589
589
  return res.data;
590
590
  }
591
+ getOAuthLoginUrl(provider, options = {}) {
592
+ const baseURL = this.config.baseURL.replace(/\/$/, "");
593
+ const authPrefix = this.prefix.startsWith("/") ? this.prefix : `/${this.prefix}`;
594
+ const raw = `${baseURL}${authPrefix}/oauth/${provider}/start`;
595
+ const absolute = /^[a-z][a-z\d+.-]*:\/\//i.test(raw);
596
+ const url = new URL(raw, absolute ? void 0 : "https://najm.invalid");
597
+ if (options.returnTo) {
598
+ url.searchParams.set("returnTo", this.validateReturnTo(options.returnTo));
599
+ }
600
+ return absolute ? url.toString() : `${url.pathname}${url.search}`;
601
+ }
602
+ loginWithOAuth(provider, options) {
603
+ if (typeof window === "undefined") {
604
+ throw new Error("OAuth login requires a browser environment");
605
+ }
606
+ window.location.assign(this.getOAuthLoginUrl(provider, options));
607
+ }
608
+ loginWithGoogle(options) {
609
+ this.loginWithOAuth("google", options);
610
+ }
611
+ async linkOAuthAccount(provider, options = {}) {
612
+ const query = options.returnTo ? `?${new URLSearchParams({ returnTo: this.validateReturnTo(options.returnTo) })}` : "";
613
+ const res = await this.api.post(
614
+ `${this.prefix}/oauth/${provider}/link${query}`
615
+ );
616
+ if (!res.data?.authorizationUrl) throw new Error("OAuth provider did not return an authorization URL");
617
+ if (typeof window === "undefined") throw new Error("OAuth linking requires a browser environment");
618
+ window.location.assign(res.data.authorizationUrl);
619
+ }
620
+ async completeOAuthLogin() {
621
+ await this.refresh();
622
+ const user = await this.fetchUser();
623
+ if (!user) throw new Error("OAuth session could not be completed");
624
+ this.tabSync?.broadcastSync(this.getSyncPayload());
625
+ this.emit("login", user);
626
+ return user;
627
+ }
591
628
  async logout() {
592
629
  this.resetState();
593
630
  this.tabSync?.broadcastLogout();
@@ -879,6 +916,18 @@ var NajmAuthClient = class _NajmAuthClient {
879
916
  this.eventListeners.get("stateChange")?.forEach((h) => h(this.state));
880
917
  }
881
918
  }
919
+ validateReturnTo(value) {
920
+ const candidate = value.trim();
921
+ if (!candidate.startsWith("/") || candidate.startsWith("//") || candidate.includes("\\")) {
922
+ throw new Error("returnTo must be a same-origin path");
923
+ }
924
+ const base = new URL("https://najm.invalid");
925
+ const parsed = new URL(candidate, base);
926
+ if (parsed.origin !== base.origin || parsed.username || parsed.password) {
927
+ throw new Error("returnTo must be a same-origin path");
928
+ }
929
+ return `${parsed.pathname}${parsed.search}${parsed.hash}`;
930
+ }
882
931
  };
883
932
  function createAuthClient(config) {
884
933
  return new NajmAuthClient(config);