najm-auth 2.0.1 → 2.0.3

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.
@@ -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);
package/dist/index.d.ts CHANGED
@@ -2,11 +2,11 @@ import * as najm_core from 'najm-core';
2
2
  import { Container } from 'najm-core';
3
3
  import { ValidationPluginConfig } from 'najm-validation';
4
4
  import { RateLimitPluginConfig } from 'najm-rate';
5
+ import { EmailPluginConfig, EmailService } from 'najm-email';
5
6
  import { I18nService } from 'najm-i18n';
6
- import { EmailService } from 'najm-email';
7
7
  import { TDb, SeedEntry } from 'najm-database';
8
8
  import { User, NewUser, RoleEntity, NewRoleEntity, Permission, NewPermission, RolePermission } from './schema/pg.js';
9
- export { NewRolePermission, NewToken, Token, authSchema, baseFields, permissionsTable, rolePermissionsTable, rolesTable, tokenStatusEnum, tokenTypeEnum, tokensTable, userStatusEnum, usersTable } from './schema/pg.js';
9
+ export { NewOAuthAccount, NewRolePermission, NewToken, OAuthAccount, Token, authSchema, baseFields, oauthAccountsTable, permissionsTable, rolePermissionsTable, rolesTable, tokenStatusEnum, tokenTypeEnum, tokensTable, userStatusEnum, usersTable } from './schema/pg.js';
10
10
  import { CacheService } from 'najm-cache';
11
11
  import { z } from 'zod';
12
12
  import { GuardResult } from 'najm-guard';
@@ -45,6 +45,48 @@ interface SessionCookieConfig {
45
45
  /** Secret used to HMAC-sign the cookie. Defaults to jwt.accessSecret. */
46
46
  secret?: string;
47
47
  }
48
+ type OAuthProvider = 'google';
49
+ interface GoogleOAuthConfig {
50
+ /** Google OAuth web client ID. Falls back to GOOGLE_CLIENT_ID. */
51
+ clientId?: string;
52
+ /** Google OAuth web client secret. Falls back to GOOGLE_CLIENT_SECRET. */
53
+ clientSecret?: string;
54
+ /**
55
+ * Absolute backend callback URL registered in Google Cloud. Falls back to
56
+ * GOOGLE_CALLBACK_URL, then `${frontendUrl}/api/auth/oauth/google/callback`.
57
+ */
58
+ callbackUrl?: string;
59
+ /** Frontend route that completes the Najm client session. */
60
+ frontendCallbackPath?: string;
61
+ /** Frontend route that receives stable OAuth errors. */
62
+ errorRedirectPath?: string;
63
+ /** Create a Najm user for a new Google identity (default: true). */
64
+ allowSignup?: boolean;
65
+ /** Link an existing user by verified email (default: false). */
66
+ autoLinkVerifiedEmail?: boolean;
67
+ /** Optional Google Workspace hosted-domain allowlist. */
68
+ allowedHostedDomains?: string[];
69
+ }
70
+ interface OAuthConfig {
71
+ /**
72
+ * Enable Google with environment defaults (`google: true`), or override
73
+ * individual settings for split-origin deployments and policy changes.
74
+ */
75
+ google?: true | GoogleOAuthConfig;
76
+ }
77
+ interface ResolvedGoogleOAuthConfig {
78
+ clientId: string;
79
+ clientSecret: string;
80
+ callbackUrl: string;
81
+ frontendCallbackPath: string;
82
+ errorRedirectPath: string;
83
+ allowSignup: boolean;
84
+ autoLinkVerifiedEmail: boolean;
85
+ allowedHostedDomains: string[];
86
+ }
87
+ interface ResolvedOAuthConfig {
88
+ google?: ResolvedGoogleOAuthConfig;
89
+ }
48
90
  /**
49
91
  * Complete auth plugin configuration (internal)
50
92
  */
@@ -73,6 +115,8 @@ interface AuthConfig {
73
115
  bcryptRounds: number;
74
116
  /** Session cookie cache settings */
75
117
  session: SessionCookieConfig;
118
+ /** Resolved external identity-provider configuration. */
119
+ oauth?: ResolvedOAuthConfig;
76
120
  }
77
121
  /**
78
122
  * Auth plugin configuration options
@@ -88,6 +132,8 @@ interface AuthSchema {
88
132
  roles: any;
89
133
  permissions: any;
90
134
  rolePermissions: any;
135
+ /** Required when an OAuth provider is enabled. */
136
+ oauthAccounts?: any;
91
137
  }
92
138
  type AuthPluginConfig = {
93
139
  /**
@@ -130,8 +176,12 @@ type AuthPluginConfig = {
130
176
  validation?: ValidationPluginConfig;
131
177
  /** Optional config forwarded to rateLimit() dependency */
132
178
  rateLimit?: RateLimitPluginConfig;
179
+ /** Email transport used by password reset and verification flows. */
180
+ email?: EmailPluginConfig;
133
181
  /** AES-256-GCM key for reversible encryption (e.g. API keys). Falls back to NAJM_ENCRYPTION_KEY env var. */
134
182
  encryptionKey?: string;
183
+ /** External identity providers. */
184
+ oauth?: OAuthConfig;
135
185
  };
136
186
  /**
137
187
  * JWT payload structure
@@ -208,7 +258,17 @@ var auth = {
208
258
  sessionExpired: "Session has expired",
209
259
  accountLocked: "Account is temporarily locked. Please try again later.",
210
260
  accountInactive: "Account is inactive. Please contact support.",
211
- emailNotVerified: "Please verify your email address before signing in."
261
+ emailNotVerified: "Please verify your email address before signing in.",
262
+ oauthProviderDisabled: "Google sign-in is not configured.",
263
+ oauthStateInvalid: "The Google sign-in attempt is invalid or expired.",
264
+ oauthAccessDenied: "Google sign-in was cancelled.",
265
+ oauthProviderError: "Google sign-in could not be completed.",
266
+ oauthVerifiedEmailRequired: "Google must provide a verified email address.",
267
+ oauthAccountLinkRequired: "Sign in with your password and link Google from your account.",
268
+ oauthProviderAccountLinked: "This Google account is already linked.",
269
+ oauthSignupDisabled: "Registration with Google is disabled.",
270
+ oauthHostedDomainDenied: "This Google Workspace domain is not allowed.",
271
+ oauthLinkSessionExpired: "Your session changed before Google could be linked. Please try again."
212
272
  },
213
273
  success: {
214
274
  login: "Login successful",
@@ -218,7 +278,9 @@ var auth = {
218
278
  passwordResetSent: "If that email exists, a reset link has been sent",
219
279
  passwordReset: "Password has been reset successfully",
220
280
  accountInviteSent: "Invitation sent successfully",
221
- tokenRefreshed: "Token refreshed successfully"
281
+ tokenRefreshed: "Token refreshed successfully",
282
+ oauthLogin: "Google sign-in successful",
283
+ oauthLinked: "Google account linked successfully"
222
284
  },
223
285
  emails: {
224
286
  passwordReset: {
@@ -314,6 +376,16 @@ declare const AUTH_LOCALES: {
314
376
  accountLocked: string;
315
377
  accountInactive: string;
316
378
  emailNotVerified: string;
379
+ oauthProviderDisabled: string;
380
+ oauthStateInvalid: string;
381
+ oauthAccessDenied: string;
382
+ oauthProviderError: string;
383
+ oauthVerifiedEmailRequired: string;
384
+ oauthAccountLinkRequired: string;
385
+ oauthProviderAccountLinked: string;
386
+ oauthSignupDisabled: string;
387
+ oauthHostedDomainDenied: string;
388
+ oauthLinkSessionExpired: string;
317
389
  };
318
390
  success: {
319
391
  login: string;
@@ -324,6 +396,8 @@ declare const AUTH_LOCALES: {
324
396
  passwordReset: string;
325
397
  accountInviteSent: string;
326
398
  tokenRefreshed: string;
399
+ oauthLogin: string;
400
+ oauthLinked: string;
327
401
  };
328
402
  emails: {
329
403
  passwordReset: {
@@ -478,6 +552,9 @@ declare class UserRepository {
478
552
  getByEmail(email: string): Promise<(User & {
479
553
  role?: string | null;
480
554
  }) | undefined>;
555
+ getByEmailInsensitive(email: string): Promise<(User & {
556
+ role?: string | null;
557
+ }) | undefined>;
481
558
  create(data: NewUser): Promise<User>;
482
559
  update(id: string, data: Partial<NewUser>): Promise<User | undefined>;
483
560
  updateLastLogin(id: string): Promise<User>;
@@ -737,6 +814,9 @@ declare class UserService {
737
814
  findByEmail(email: string): Promise<(User & {
738
815
  role?: string | null;
739
816
  }) | undefined>;
817
+ findByEmailInsensitive(email: string): Promise<(User & {
818
+ role?: string | null;
819
+ }) | undefined>;
740
820
  getAuthRecordById(id: string): Promise<User | undefined>;
741
821
  create(data: Record<string, any>): Promise<SanitizedUser>;
742
822
  update(id: string, data: Record<string, any>): Promise<SanitizedUser>;
@@ -1093,6 +1173,16 @@ type UserIdInParam = z.infer<typeof userIdInParam>;
1093
1173
  type AssignRoleParams = z.infer<typeof assignRoleParams>;
1094
1174
  type UserListQuery = z.infer<typeof userListQuery>;
1095
1175
 
1176
+ declare class AuthSessionService {
1177
+ private tokenService;
1178
+ private userService;
1179
+ private cookieManager;
1180
+ constructor(tokenService: TokenService, userService: UserService, cookieManager: CookieManager);
1181
+ establish(user: SanitizedUser): Promise<TokenPair & {
1182
+ user: SanitizedUser;
1183
+ }>;
1184
+ }
1185
+
1096
1186
  /**
1097
1187
  * Identity fields for creating a user behind a person record (parent, student,
1098
1188
  * teacher, staff…). Role can be given by name (`role`) or id (`roleId`).
@@ -1114,11 +1204,12 @@ declare class AuthService {
1114
1204
  private cookieManager;
1115
1205
  private i18nService;
1116
1206
  private emailService;
1207
+ private authSessionService?;
1117
1208
  private config;
1118
1209
  private t;
1119
1210
  private logger;
1120
1211
  private dummyHash?;
1121
- constructor(tokenService: TokenService, userService: UserService, userValidator: UserValidator, encryptionService: EncryptionService, cookieManager: CookieManager, i18nService: I18nService, emailService: EmailService);
1212
+ constructor(tokenService: TokenService, userService: UserService, userValidator: UserValidator, encryptionService: EncryptionService, cookieManager: CookieManager, i18nService: I18nService, emailService: EmailService, authSessionService?: AuthSessionService);
1122
1213
  private isLockoutActive;
1123
1214
  private nextLockoutUntil;
1124
1215
  private getDummyHash;
@@ -1306,7 +1397,7 @@ interface RunAsUser {
1306
1397
  }
1307
1398
  declare function runAsUser<T>(container: Container, user: RunAsUser, fn: () => Promise<T> | T): Promise<T>;
1308
1399
 
1309
- declare const AUTH_MODULE: readonly [typeof AuthService, typeof CookieManager, typeof EncryptionService, typeof AuthGuard, typeof AuthController, typeof AuthResolver];
1400
+ declare const AUTH_MODULE: readonly [typeof AuthService, typeof AuthSessionService, typeof CookieManager, typeof EncryptionService, typeof AuthGuard, typeof AuthController, typeof AuthResolver];
1310
1401
 
1311
1402
  declare class PermissionRepository {
1312
1403
  db: TDb;
@@ -2197,4 +2288,4 @@ declare const authSeed: (config: AuthSeedConfig) => Record<string, SeedEntry>;
2197
2288
  */
2198
2289
  declare function seedAuthData(config: SeedAuthDataConfig): Promise<SeedAuthDataResult>;
2199
2290
 
2200
- export { AUTH_CONFIG, en as AUTH_EN, AUTH_LOCALES, AUTH_MODULE, AUTH_PERMISSIONS, AUTH_ROLE, AUTH_SCHEMA, AUTH_SUPPORTED_LANGUAGES, AUTH_USER, type AssignPermissionDto, type AssignRoleDto, type AssignRoleParams, type AuthConfig, AuthController, AuthGuard, type AuthPluginConfig, AuthQueries, AuthResolver, type AuthSchema, type AuthSeedConfig, AuthService, type AuthUser, Can, CanCreate, CanDelete, CanList, CanRead, CanUpdate, type ChainableGuard, type ChangePasswordDto, type CheckPermissionDto, type ConfiguredOwnership, type ConfirmResetPasswordDto, CookieManager, type CreatePermissionDto, type CreateRoleDto, type CreateTokenDto, type CreateUserDto, type DefineRolesOptions, type EmailParam, EncryptionService, type InviteUserDto, type JwtConfig, type JwtPayload, type LanguageParam, type LoginDto, NewPermission, NewRoleEntity, NewUser, Owned, type OwnedMethods, type OwnershipConfig, type OwnershipProvider, type OwnershipRule, OwnershipToken, type OwnershipTokenOptions, Permission, PermissionController, PermissionGuard, type PermissionIdParam, PermissionRepository, PermissionService, PermissionValidator, Policy, type ProvisionUserInput, ROLES, ROLE_GROUPS, type RefreshTokenDto, type RegisterDto, type ResetPasswordDto, type ResourceAccessor, type ResourceGuards, type ResourceGuardsOptions, type RevokeTokenDto, Role, RoleController, RoleEntity, RoleGuard, type RoleIdParam, type RoleInput, RolePermission, RoleRepository, RoleService, type RoleType, RoleValidator, type RunAsUser, type SanitizedUser, ScopeContext, type ScopeResult, type SeedAuthDataConfig, type SeedAuthDataResult, type SeedUserConfig, type SessionCookieData, TOKEN_STATUS, TOKEN_TYPE, type TokenIdParam, type TokenPair, TokenRepository, TokenService, USER_STATUS, type UpdatePermissionDto, type UpdateRoleDto, type UpdateTokenDto, type UpdateUserDto, User, UserController, type UserIdInParam, type UserIdParam, type UserListQuery, UserRepository, UserService, UserValidator, type UserWithPermissions, type VerifyTokenDto, assignPermissionDto, assignRoleDto, assignRoleParams, auth$1 as auth, authSeed, avatarsPath, calculateAge, calculateYearsOfExperience, changePasswordDto, checkPermissionDto, clean, configureOwnership, confirmResetPasswordDto, createPermissionDto, createRoleDto, createTokenDto, createUserDto, defineRoles, emailParam, formatDate, getAuthLocale, getAvatarFile, inviteUserDto, isAdmin, isAdministrator, isAuth, isEmpty, isFile, isPath, join, languageParam, loginDto, own, parseSchema, permissionIdParam, pickProps, refreshTokenDto, registerDto, resetPasswordDto, revokeTokenDto, roleIdParam, runAsUser, seedAuthData, tokenIdParam, updatePermissionDto, updateRoleDto, updateTokenDto, updateUserDto, userIdInParam, userIdParam, userListQuery, verifyTokenDto, where };
2291
+ export { AUTH_CONFIG, en as AUTH_EN, AUTH_LOCALES, AUTH_MODULE, AUTH_PERMISSIONS, AUTH_ROLE, AUTH_SCHEMA, AUTH_SUPPORTED_LANGUAGES, AUTH_USER, type AssignPermissionDto, type AssignRoleDto, type AssignRoleParams, type AuthConfig, AuthController, AuthGuard, type AuthPluginConfig, AuthQueries, AuthResolver, type AuthSchema, type AuthSeedConfig, AuthService, AuthSessionService, type AuthUser, Can, CanCreate, CanDelete, CanList, CanRead, CanUpdate, type ChainableGuard, type ChangePasswordDto, type CheckPermissionDto, type ConfiguredOwnership, type ConfirmResetPasswordDto, CookieManager, type CreatePermissionDto, type CreateRoleDto, type CreateTokenDto, type CreateUserDto, type DefineRolesOptions, type EmailParam, EncryptionService, type GoogleOAuthConfig, type InviteUserDto, type JwtConfig, type JwtPayload, type LanguageParam, type LoginDto, NewPermission, NewRoleEntity, NewUser, type OAuthConfig, type OAuthProvider, Owned, type OwnedMethods, type OwnershipConfig, type OwnershipProvider, type OwnershipRule, OwnershipToken, type OwnershipTokenOptions, Permission, PermissionController, PermissionGuard, type PermissionIdParam, PermissionRepository, PermissionService, PermissionValidator, Policy, type ProvisionUserInput, ROLES, ROLE_GROUPS, type RefreshTokenDto, type RegisterDto, type ResetPasswordDto, type ResourceAccessor, type ResourceGuards, type ResourceGuardsOptions, type RevokeTokenDto, Role, RoleController, RoleEntity, RoleGuard, type RoleIdParam, type RoleInput, RolePermission, RoleRepository, RoleService, type RoleType, RoleValidator, type RunAsUser, type SanitizedUser, ScopeContext, type ScopeResult, type SeedAuthDataConfig, type SeedAuthDataResult, type SeedUserConfig, type SessionCookieData, TOKEN_STATUS, TOKEN_TYPE, type TokenIdParam, type TokenPair, TokenRepository, TokenService, USER_STATUS, type UpdatePermissionDto, type UpdateRoleDto, type UpdateTokenDto, type UpdateUserDto, User, UserController, type UserIdInParam, type UserIdParam, type UserListQuery, UserRepository, UserService, UserValidator, type UserWithPermissions, type VerifyTokenDto, assignPermissionDto, assignRoleDto, assignRoleParams, auth$1 as auth, authSeed, avatarsPath, calculateAge, calculateYearsOfExperience, changePasswordDto, checkPermissionDto, clean, configureOwnership, confirmResetPasswordDto, createPermissionDto, createRoleDto, createTokenDto, createUserDto, defineRoles, emailParam, formatDate, getAuthLocale, getAvatarFile, inviteUserDto, isAdmin, isAdministrator, isAuth, isEmpty, isFile, isPath, join, languageParam, loginDto, own, parseSchema, permissionIdParam, pickProps, refreshTokenDto, registerDto, resetPasswordDto, revokeTokenDto, roleIdParam, runAsUser, seedAuthData, tokenIdParam, updatePermissionDto, updateRoleDto, updateTokenDto, updateUserDto, userIdInParam, userIdParam, userListQuery, verifyTokenDto, where };