najm-auth 2.0.15 → 3.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/README.md CHANGED
@@ -9,6 +9,8 @@ Production-ready authentication and authorization library for the Najm framework
9
9
  - ✅ Permission-based access control (PBAC) with wildcards
10
10
  - ✅ Row-level ownership scoping for multi-tenant apps
11
11
  - ✅ Built-in password reset flow with email support
12
+ - ✅ Forced first-login credential setup for provisioned accounts
13
+ - ✅ Country identity presets so `06…` and `+2126…` resolve to one account
12
14
  - ✅ Multi-dialect support (PostgreSQL, SQLite)
13
15
  - ✅ Type-safe decorators with TypeScript
14
16
  - ✅ Rate limiting on auth endpoints
@@ -45,7 +47,7 @@ export const products = sqliteTable('products', {
45
47
 
46
48
  // Combined schema (always include authSchema)
47
49
  export const schema = {
48
- ...authSchema, // includes users, tokens, and credentialSetupSessions
50
+ ...authSchema, // includes users, tokens, and credentialSetupSessions
49
51
  products,
50
52
  };
51
53
 
@@ -134,6 +136,21 @@ auth({
134
136
  // Frontend
135
137
  frontendUrl?: string // Password reset link base URL
136
138
 
139
+ // Login identifier normalization (see "Identity presets")
140
+ identity?: {
141
+ preset?: 'ma' | 'tn' | IdentityPreset | null // Default: 'ma'
142
+ extend?: IdentityNormalizer[] // Runs before the preset
143
+ }
144
+
145
+ // Credential setup policy overrides (the flow itself is always on)
146
+ credentialSetup?: {
147
+ password?: {
148
+ passwordSchema?: ZodType<string> // Default: 8-72 bytes, a letter and a digit
149
+ ttlMs?: number // Default: 600000 (10 minutes)
150
+ cookieName?: string // Default: 'najm.credential-setup'
151
+ }
152
+ }
153
+
137
154
  // Optional Google OpenID Connect
138
155
  oauth?: {
139
156
  google?: true | {
@@ -175,6 +192,89 @@ All routes are prefixed with `/auth` and auto-registered by the plugin.
175
192
  | `GET` | `/auth/oauth/google/start` | Start Google sign-in | None |
176
193
  | `GET` | `/auth/oauth/google/callback` | Verify Google callback and create Najm session | None |
177
194
  | `POST` | `/auth/oauth/google/link` | Link Google to the current user | ✅ Required |
195
+ | `GET` | `/auth/credential-setup/setup` | Read the pending setup session | Setup cookie |
196
+ | `POST` | `/auth/credential-setup/change` | Replace the temporary credential | Setup cookie |
197
+ | `POST` | `/auth/credential-setup/cancel` | Abandon the setup session | Setup cookie |
198
+
199
+ ### Identity presets
200
+
201
+ Login lookup, lockout accounting, and rate-limit bucketing all normalize the
202
+ submitted identifier the same way. The pipeline is: email (lowercased) →
203
+ project extensions → the country preset → generic E.164.
204
+
205
+ The resolved pipeline belongs to the specific `auth()` plugin/server instance.
206
+ Multiple isolated Najm servers can therefore use different country presets in
207
+ one process without replacing each other's login or rate-limit behavior.
208
+
209
+ Morocco is the default, so `0612345678`, `212612345678`, and `+212612345678`
210
+ all resolve to `+212612345678` with no configuration.
211
+
212
+ ```typescript
213
+ auth(); // preset: 'ma'
214
+ auth({ identity: { extend: [employeeNumberNormalizer] } });
215
+ auth({ identity: { preset: 'tn' } }); // replaces Morocco
216
+ auth({ identity: { preset: null, extend: [custom] } }); // generic only
217
+ ```
218
+
219
+ Local numbers are country-ambiguous, so presets **replace** each other rather
220
+ than stacking — two presets claiming `06…` would resolve one raw input to two
221
+ different accounts.
222
+
223
+ ### First-login credential setup
224
+
225
+ Provision an account with a temporary credential and Najm refuses it a normal
226
+ session until the holder replaces it:
227
+
228
+ ```typescript
229
+ import { moroccanCinTemporaryCredential } from 'najm-auth/identity/ma';
230
+
231
+ await authService.provisionUser({
232
+ email: guardian.email,
233
+ phone: guardian.phone,
234
+ role: 'family',
235
+ temporaryCredential: moroccanCinTemporaryCredential(guardian.cin),
236
+ requireCredentialSetup: 'password',
237
+ });
238
+ ```
239
+
240
+ `temporaryCredential` also accepts a plain string, compared exactly and
241
+ case-sensitively — enough for a student or registration number:
242
+
243
+ ```typescript
244
+ await authService.provisionUser({
245
+ email: student.schoolEmail,
246
+ role: 'student',
247
+ temporaryCredential: student.registrationNumber,
248
+ requireCredentialSetup: 'password',
249
+ });
250
+ ```
251
+
252
+ Supplying both `password` and `temporaryCredential` is rejected, so an account
253
+ can never hold a permanent password that something also treats as temporary.
254
+ Typed helpers such as `moroccanCinTemporaryCredential()` validate their value,
255
+ and every temporary credential remains limited to bcrypt's 72-byte boundary.
256
+
257
+ Login then answers a discriminated result instead of a token pair:
258
+
259
+ ```typescript
260
+ const result = await auth.client.login({ identifier, password, rememberMe });
261
+
262
+ if (result.nextStep === 'credential_setup') {
263
+ router.push('/change-password'); // no tokens were issued
264
+ } else {
265
+ router.push('/dashboard');
266
+ }
267
+ ```
268
+
269
+ The requirement is enforced at every session-establishment path — password
270
+ login, `AuthSessionService.establish()`, Google OAuth (which redirects with
271
+ `oauthError=oauth_credential_setup_required`), refresh, and signed-session
272
+ recovery — so verified-email OAuth linking cannot skip it. Marking a new
273
+ requirement also revokes the user's current sessions.
274
+
275
+ `withAuthCookiePersistence` recognizes the setup response on its own: it drops
276
+ any session cookies the response carried, clears the remembered preference, and
277
+ leaves the opaque setup cookie alone.
178
278
 
179
279
  ### Google Sign-In
180
280
 
@@ -502,12 +602,34 @@ oauth_accounts
502
602
  ├── providerAccountId (Google `sub`)
503
603
  ├── unique(provider, providerAccountId)
504
604
  └── unique(userId, provider)
605
+
606
+ credential_setup_sessions
607
+ ├── id (string, primary key)
608
+ ├── userId (string, FK → users.id, cascade delete)
609
+ ├── purpose (string)
610
+ ├── tokenHash (string, unique — SHA-256 of the browser cookie)
611
+ ├── expiresAt (timestamp)
612
+ ├── consumedAt (timestamp, nullable)
613
+ └── revokedAt (timestamp, nullable)
614
+
615
+ credential_setup_requirements
616
+ ├── userId (string, FK → users.id, cascade delete)
617
+ ├── purpose (string; `password` for the built-in flow)
618
+ ├── temporaryCredentialKind (string, nullable; `exact` or `ma-cin`)
619
+ ├── required (boolean, default: true)
620
+ ├── completedAt (timestamp, nullable)
621
+ └── primary key (userId, purpose)
505
622
  ```
506
623
 
624
+ `credential_setup_requirements` is keyed on `(userId, purpose)` rather than
625
+ `userId` alone, so one user can owe more than one future setup purpose.
626
+
507
627
  Existing databases must generate and run a migration after upgrading so the
508
- new `oauth_accounts` table exists. Custom `AuthSchema` objects may omit
509
- `oauthAccounts` while OAuth is disabled, but Google configuration fails fast
510
- unless the custom schema supplies it.
628
+ new `oauth_accounts`, `credential_setup_sessions`, and
629
+ `credential_setup_requirements` tables exist. Custom `AuthSchema` objects may
630
+ omit `oauthAccounts` while OAuth is disabled, but Google configuration fails
631
+ fast unless the custom schema supplies it. Both credential-setup tables are
632
+ required of a custom schema, because the setup flow is always mounted.
511
633
 
512
634
  ### ID Strategy
513
635
 
@@ -621,6 +743,95 @@ auth({
621
743
 
622
744
  ---
623
745
 
746
+ ## Next.js App Router Structure
747
+
748
+ Every App Router application keeps the same three files. Copying more than this
749
+ between apps means logic that belongs in the package has leaked into them.
750
+
751
+ ```text
752
+ src/lib/auth.ts defineAuth() configuration — browser, server, and proxy safe
753
+ src/lib/session.ts one createReactServerAuth() instance for Server Components
754
+ src/proxy.ts imports auth.ts only, and exports auth.middleware
755
+ ```
756
+
757
+ ```typescript
758
+ // src/lib/auth.ts
759
+ import { defineAuth } from 'najm-auth/client/server';
760
+
761
+ export const auth = defineAuth({
762
+ apiBaseURL: '/api',
763
+ loginRoute: '/login',
764
+ forbiddenRoute: '/forbidden',
765
+ publicRoutes: ['/', '/login'],
766
+ protectedRoutes: ['/dashboard/:path*', '/admin/:path*'],
767
+ roleRoutes: { '/admin/:path*': ['admin'] },
768
+ });
769
+ ```
770
+
771
+ ```typescript
772
+ // src/lib/session.ts
773
+ import 'server-only';
774
+
775
+ import { createReactServerAuth } from 'najm-auth/client/server/react';
776
+
777
+ import { auth } from './auth';
778
+
779
+ export const serverAuth = createReactServerAuth(auth);
780
+ ```
781
+
782
+ ```typescript
783
+ // src/proxy.ts
784
+ import { auth } from './lib/auth';
785
+
786
+ export default auth.middleware;
787
+ export const config = auth.config;
788
+ ```
789
+
790
+ ### Why `session.ts` exists
791
+
792
+ A Next.js page is not one function. The root layout, each nested layout, and the
793
+ page render separately, and each one that asks for the session pays for its own
794
+ cookie verification and possibly its own recovery round trip. React's `cache()`
795
+ collapses those into one — but only for callers that go through the *same*
796
+ memoized function, which means the application has to own one module that
797
+ creates it. `session.ts` is that module and nothing else; strictness, redirect
798
+ targets, role fallback, and error classification all stay in the package.
799
+
800
+ ```tsx
801
+ // Root layout, nested layout, and page: one resolution between them.
802
+ const session = await serverAuth.getSession(); // null when anonymous
803
+ const session = await serverAuth.requireSession(); // redirects to loginRoute
804
+ const session = await serverAuth.requireRole(['admin', 'operator']);
805
+ ```
806
+
807
+ - `requireSession()` redirects to `loginRoute` when the visitor is missing,
808
+ invalid, or revoked. An unreachable recovery endpoint or an unset session
809
+ secret is an operational fault, not an anonymous visitor: those stay visible
810
+ errors instead of becoming a login redirect that hides the outage.
811
+ - `requireRole()` redirects to `forbiddenRoute`, never to login — the visitor is
812
+ already authenticated, so signing in again cannot change the answer.
813
+ - `session.roles` is authoritative when present, with `user.role` as the
814
+ single-role fallback.
815
+
816
+ ### Scope and limits
817
+
818
+ - **React Server Components only.** Route handlers, server actions, proxy/Edge
819
+ code, and scripts keep using `auth.getSession()`, `auth.requireSession()`, and
820
+ `auth.requireRole()`. Outside a render there is no request cache for `cache()`
821
+ to write to, so the adapter would resolve the session again on every call.
822
+ - **Call the factory once, at module scope.** Calling it inside a layout, page,
823
+ or component builds a fresh memoized resolver per call and shares nothing.
824
+ - **The snapshot is stable for one render.** Code that mutates authentication
825
+ must redirect or refresh into a new render to observe the result.
826
+ - **Requests never share.** The cache is React's per-request cache — no module
827
+ map, no global, no Redis, no `unstable_cache`, no `"use cache"`.
828
+ - **Requires React 18.3 or newer** (the first version exporting `cache()`); the
829
+ factory throws a named error on older versions. The subpath is opt-in, so
830
+ non-React consumers of `najm-auth` are unaffected. Importing it from a Client
831
+ Component or the Edge runtime fails at build time.
832
+
833
+ ---
834
+
624
835
  ## TypeScript Types
625
836
 
626
837
  ```typescript
@@ -727,52 +938,52 @@ async resetPassword(token: string, newPassword: string) {
727
938
  }
728
939
  ```
729
940
 
730
- ### Purpose-Bound Credential Setup
731
-
732
- Use `CredentialSetupService` when valid credentials should open only a
733
- short-lived setup flow, not a complete application session. The default auth
734
- schema includes the durable `credential_setup_sessions` table for PostgreSQL
735
- and SQLite; generate and apply a consumer migration after upgrading.
736
-
737
- ```typescript
738
- import { AuthService, CredentialSetupService } from 'najm-auth';
739
-
740
- const options = {
741
- purpose: 'password-setup',
742
- cookieName: 'my-app.password-setup',
743
- ttlMs: 10 * 60 * 1000,
744
- };
745
-
746
- // Verify the password without minting access/refresh tokens.
747
- const user = await authService.verifyCredentials({ identifier, password });
748
-
749
- // Or narrowly accept only an unverified pending account with one exact role.
750
- const pendingSponsor = await authService.verifyPendingCredentials(
751
- { identifier, password },
752
- 'sponsor',
753
- );
754
-
755
- if (await appRequiresPasswordSetup(user.id)) {
756
- // Revokes normal sessions and writes only an HttpOnly, SameSite=Strict,
757
- // browser-session cookie. The database stores only its SHA-256 hash.
758
- return credentialSetup.begin(user.id, options);
759
- }
760
-
761
- return authService.establishSession(user);
762
-
763
- // Complete an app-owned mutation in the same transaction as one-time
764
- // consumption. If the callback fails, token consumption rolls back.
765
- await credentialSetup.consume(options, async ({ userId }) => {
766
- await replaceApplicationCredential(userId, newCredential);
767
- });
768
- ```
769
-
770
- Setup tokens are bound to a server-owned purpose, expire automatically, are
771
- replaced when the same user starts that purpose again, and can be cancelled or
772
- consumed exactly once. `require()` validates the current setup cookie without
773
- consuming it; `cancel()` revokes it and clears the cookie.
774
-
775
- ### Session Management
941
+ ### Purpose-Bound Credential Setup
942
+
943
+ Use `CredentialSetupService` when valid credentials should open only a
944
+ short-lived setup flow, not a complete application session. The default auth
945
+ schema includes the durable `credential_setup_sessions` table for PostgreSQL
946
+ and SQLite; generate and apply a consumer migration after upgrading.
947
+
948
+ ```typescript
949
+ import { AuthService, CredentialSetupService } from 'najm-auth';
950
+
951
+ const options = {
952
+ purpose: 'password-setup',
953
+ cookieName: 'my-app.password-setup',
954
+ ttlMs: 10 * 60 * 1000,
955
+ };
956
+
957
+ // Verify the password without minting access/refresh tokens.
958
+ const user = await authService.verifyCredentials({ identifier, password });
959
+
960
+ // Or narrowly accept only an unverified pending account with one exact role.
961
+ const pendingSponsor = await authService.verifyPendingCredentials(
962
+ { identifier, password },
963
+ 'sponsor',
964
+ );
965
+
966
+ if (await appRequiresPasswordSetup(user.id)) {
967
+ // Revokes normal sessions and writes only an HttpOnly, SameSite=Strict,
968
+ // browser-session cookie. The database stores only its SHA-256 hash.
969
+ return credentialSetup.begin(user.id, options);
970
+ }
971
+
972
+ return authService.establishSession(user);
973
+
974
+ // Complete an app-owned mutation in the same transaction as one-time
975
+ // consumption. If the callback fails, token consumption rolls back.
976
+ await credentialSetup.consume(options, async ({ userId }) => {
977
+ await replaceApplicationCredential(userId, newCredential);
978
+ });
979
+ ```
980
+
981
+ Setup tokens are bound to a server-owned purpose, expire automatically, are
982
+ replaced when the same user starts that purpose again, and can be cancelled or
983
+ consumed exactly once. `require()` validates the current setup cookie without
984
+ consuming it; `cancel()` revokes it and clears the cookie.
985
+
986
+ ### Session Management
776
987
 
777
988
  - Sessions are multi-device: the token table stores one refresh row per login session (keyed by a unique `tokenFamily`), so a user can stay logged in on several devices at once. Logout and rotation are scoped to the current session; password change/reset revoke every session
778
989
  - A stale refresh token presented after the 120-second rotation grace window revokes only that session's family as reuse protection
@@ -1,139 +1,4 @@
1
- /**
2
- * User data from the auth server
3
- */
4
- interface AuthUser {
5
- id: string;
6
- email: string;
7
- name?: string;
8
- role?: string | null;
9
- permissions?: string[];
10
- [key: string]: unknown;
11
- }
12
- /**
13
- * Full auth state snapshot
14
- */
15
- interface AuthState {
16
- user: AuthUser | null;
17
- accessToken: string | null;
18
- isAuthenticated: boolean;
19
- isLoading: boolean;
20
- roles: string[];
21
- permissions: string[];
22
- }
23
- /**
24
- * Decoded JWT payload (client-side, no verification)
25
- */
26
- interface DecodedToken {
27
- userId: string;
28
- jti?: string;
29
- sessionVersion?: number;
30
- roles?: string[];
31
- permissions?: string[];
32
- exp?: number;
33
- iat?: number;
34
- [key: string]: unknown;
35
- }
36
- /**
37
- * Server response envelope
38
- */
39
- interface ServerResponse<T = unknown> {
40
- data: T;
41
- message?: string;
42
- status?: string;
43
- }
44
- /**
45
- * Token pair from login/refresh (internal — refresh token is httpOnly cookie)
46
- */
47
- interface TokenPair {
48
- accessToken: string;
49
- refreshToken?: string;
50
- accessTokenExpiresAt?: number;
51
- refreshTokenExpiresAt?: number;
52
- }
53
- /**
54
- * Retry configuration
55
- */
56
- interface RetryConfig {
57
- maxRetries?: number;
58
- backoff?: 'exponential' | 'linear';
59
- baseDelay?: number;
60
- }
61
- /**
62
- * Auth client configuration
63
- */
64
- interface AuthClientConfig {
65
- /** API base URL (e.g., '/api' or 'https://api.example.com') */
66
- baseURL: string;
67
- /** Auth endpoints prefix (default: '/auth') */
68
- authPrefix?: string;
69
- /** Proactive refresh at this fraction of token lifetime (default: 0.8) */
70
- refreshThreshold?: number;
71
- /** Enable multi-tab sync via BroadcastChannel (default: true) */
72
- tabSync?: boolean;
73
- /** BroadcastChannel name (default: 'najm-auth') */
74
- channelName?: string;
75
- /** Network retry configuration */
76
- retry?: RetryConfig;
77
- /** Request timeout in milliseconds (default: 30000) */
78
- timeout?: number;
79
- }
80
- type OAuthProvider = 'google';
81
- interface OAuthLoginOptions {
82
- /** Same-origin frontend path after OAuth completes. */
83
- returnTo?: string;
84
- }
85
- /**
86
- * Auth event types
87
- */
88
- interface AuthEventMap {
89
- login: AuthUser;
90
- logout: null;
91
- /** Emitted when server-side logout invalidation fails (state was already cleared) */
92
- logoutError: unknown;
93
- tokenRefresh: null;
94
- sessionExpired: null;
95
- stateChange: AuthState;
96
- userUpdated: AuthUser;
97
- }
98
- type AuthEvent = keyof AuthEventMap;
99
- type AuthEventHandler<K extends AuthEvent = AuthEvent> = (data: AuthEventMap[K]) => void;
100
- /**
101
- * Tab sync message types
102
- */
103
- type TabSyncMessage = {
104
- type: 'logout';
105
- } | {
106
- type: 'sync';
107
- state: SyncPayload;
108
- };
109
- interface SyncPayload {
110
- accessToken: string | null;
111
- user: AuthUser | null;
112
- roles: string[];
113
- permissions: string[];
114
- isAuthenticated: boolean;
115
- }
116
- /**
117
- * FetchClient request options
118
- */
119
- interface RequestOptions {
120
- body?: unknown;
121
- headers?: Record<string, string>;
122
- signal?: AbortSignal;
123
- timeout?: number;
124
- /** Skip auth header attachment (for public endpoints like /login, /register) */
125
- skipAuth?: boolean;
126
- /** @internal Prevents 401-refresh loop after a single retry */
127
- _retried?: boolean;
128
- }
129
- /**
130
- * Auth error thrown by the client
131
- */
132
- declare class AuthError extends Error {
133
- status: number;
134
- body?: unknown;
135
- constructor(status: number, message: string, body?: unknown);
136
- }
1
+ import { R as RetryConfig, a as RequestOptions, A as AuthUser, b as AuthClientConfig, L as LoginCredentials, c as LoginResult, O as OAuthProvider, d as OAuthLoginOptions, e as AuthState, f as AuthEvent, g as AuthEventHandler } from './types-BaSfgxqE.js';
137
2
 
138
3
  interface FetchClientConfig {
139
4
  baseURL: string;
@@ -182,10 +47,7 @@ declare class NajmAuthClient {
182
47
  private readonly prefix;
183
48
  private readonly threshold;
184
49
  constructor(config: AuthClientConfig);
185
- login(credentials: {
186
- email: string;
187
- password: string;
188
- }): Promise<AuthUser>;
50
+ login(credentials: LoginCredentials): Promise<LoginResult>;
189
51
  register(data: Record<string, unknown>): Promise<AuthUser>;
190
52
  getOAuthLoginUrl(provider: OAuthProvider, options?: OAuthLoginOptions): string;
191
53
  loginWithOAuth(provider: OAuthProvider, options?: OAuthLoginOptions): void;
@@ -255,4 +117,4 @@ declare class NajmAuthClient {
255
117
  */
256
118
  declare function createAuthClient(config: AuthClientConfig): NajmAuthClient;
257
119
 
258
- export { type AuthClientConfig as A, type DecodedToken as D, FetchClient as F, type HydrateSession as H, NajmAuthClient as N, type OAuthLoginOptions as O, type RequestOptions as R, type SyncPayload as S, type TabSyncMessage as T, AuthError as a, type AuthEvent as b, type AuthEventHandler as c, type AuthEventMap as d, type AuthState as e, type AuthUser as f, type OAuthProvider as g, type RetryConfig as h, type ServerResponse as i, type TokenPair as j, createAuthClient as k };
120
+ export { FetchClient as F, type HydrateSession as H, NajmAuthClient as N, createAuthClient as c };
@@ -1,21 +1,6 @@
1
1
  import * as next_server from 'next/server';
2
-
3
- type SessionRecoveryFailureReason = 'invalid-cookie-name' | 'invalid-refresh-cookie' | 'invalid-endpoint' | 'fetch-error' | 'http-status' | 'missing-set-cookie' | 'session-cookie-parse' | 'session-cookie-hmac' | 'session-cookie-payload';
4
- interface SessionRecoveryErrorDetails {
5
- name: string;
6
- message: string;
7
- code?: string;
8
- cause?: {
9
- name: string;
10
- message: string;
11
- code?: string;
12
- };
13
- }
14
- interface SessionRecoveryFailure {
15
- reason: SessionRecoveryFailureReason;
16
- httpStatus?: number;
17
- error?: SessionRecoveryErrorDetails;
18
- }
2
+ import { S as SessionRecoveryFailure } from '../sessionRecovery-D5Fa0yZ1.js';
3
+ export { a as SessionRecoveryErrorDetails, b as SessionRecoveryFailureReason } from '../sessionRecovery-D5Fa0yZ1.js';
19
4
 
20
5
  interface AuthMiddlewareConfig {
21
6
  /** Routes that require authentication (glob patterns) */
@@ -82,4 +67,4 @@ interface AuthMiddlewareConfig {
82
67
  */
83
68
  declare function withAuthMiddleware(config: AuthMiddlewareConfig): (request: Request) => Promise<next_server.NextResponse<unknown>>;
84
69
 
85
- export { type AuthMiddlewareConfig, type SessionRecoveryErrorDetails, type SessionRecoveryFailure, type SessionRecoveryFailureReason, withAuthMiddleware };
70
+ export { type AuthMiddlewareConfig, SessionRecoveryFailure, withAuthMiddleware };
@@ -1,5 +1,6 @@
1
- import { D as DecodedToken, T as TabSyncMessage, S as SyncPayload } from '../NajmAuthClient-Cn9bObLB.js';
2
- export { A as AuthClientConfig, a as AuthError, b as AuthEvent, c as AuthEventHandler, d as AuthEventMap, e as AuthState, f as AuthUser, F as FetchClient, H as HydrateSession, N as NajmAuthClient, O as OAuthLoginOptions, g as OAuthProvider, R as RequestOptions, h as RetryConfig, i as ServerResponse, j as TokenPair, k as createAuthClient } from '../NajmAuthClient-Cn9bObLB.js';
1
+ export { F as FetchClient, H as HydrateSession, N as NajmAuthClient, c as createAuthClient } from '../NajmAuthClient-DqGucYXi.js';
2
+ import { D as DecodedToken, T as TabSyncMessage, S as SyncPayload } from '../types-BaSfgxqE.js';
3
+ export { b as AuthClientConfig, h as AuthError, f as AuthEvent, g as AuthEventHandler, i as AuthEventMap, e as AuthState, A as AuthUser, j as AuthenticatedLogin, C as CredentialSetupPending, L as LoginCredentials, c as LoginResult, d as OAuthLoginOptions, O as OAuthProvider, a as RequestOptions, R as RetryConfig, k as ServerResponse, l as TokenPair } from '../types-BaSfgxqE.js';
3
4
 
4
5
  /**
5
6
  * Decode a JWT token payload without verification.
@@ -191,6 +191,10 @@ var TabSync = class {
191
191
  };
192
192
 
193
193
  // src/client/NajmAuthClient.ts
194
+ function isCredentialSetupPending(payload) {
195
+ return typeof payload === "object" && payload !== null && payload.nextStep === "credential_setup";
196
+ }
197
+ __name(isCredentialSetupPending, "isCredentialSetupPending");
194
198
  var INITIAL_STATE = {
195
199
  user: null,
196
200
  accessToken: null,
@@ -242,20 +246,23 @@ var NajmAuthClient = class _NajmAuthClient {
242
246
  // Auth Operations
243
247
  // =========================================================================
244
248
  async login(credentials) {
245
- const res = await this.api.post(
246
- `${this.prefix}/login`,
247
- { body: credentials, skipAuth: true }
248
- );
249
- this.applyTokens(res.data);
250
- if (res.data.user) {
251
- this.state = { ...this.state, user: res.data.user };
249
+ const res = await this.api.post(`${this.prefix}/login`, { body: credentials, skipAuth: true });
250
+ this.resetRefreshFailures();
251
+ const setup = isCredentialSetupPending(res) ? res : isCredentialSetupPending(res.data) ? res.data : null;
252
+ if (setup) {
253
+ return { ...setup };
254
+ }
255
+ const authenticated = res.data;
256
+ this.applyTokens(authenticated);
257
+ if (authenticated.user) {
258
+ this.state = { ...this.state, user: authenticated.user };
252
259
  this.notify();
253
260
  } else {
254
261
  await this.fetchUser();
255
262
  }
256
263
  this.tabSync?.broadcastSync(this.getSyncPayload());
257
264
  this.emit("login", this.state.user);
258
- return this.state.user;
265
+ return { nextStep: "authenticated", user: this.state.user };
259
266
  }
260
267
  async register(data) {
261
268
  const res = await this.api.post(
@@ -1,7 +1,8 @@
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, e as AuthState, f as AuthUser, a as AuthError, O as OAuthLoginOptions, b as AuthEvent, d as AuthEventMap } from '../../NajmAuthClient-Cn9bObLB.js';
4
+ import { N as NajmAuthClient, H as HydrateSession } from '../../NajmAuthClient-DqGucYXi.js';
5
+ import { e as AuthState, A as AuthUser, c as LoginResult, h as AuthError, L as LoginCredentials, d as OAuthLoginOptions, f as AuthEvent, i as AuthEventMap } from '../../types-BaSfgxqE.js';
5
6
 
6
7
  interface AuthProviderProps {
7
8
  client: NajmAuthClient;
@@ -79,14 +80,18 @@ interface UsePermissionsReturn {
79
80
  declare function usePermissions(): UsePermissionsReturn;
80
81
 
81
82
  interface UseLoginOptions {
82
- onSuccess?: (user: AuthUser) => void;
83
+ /** Fires for both branches — check `result.nextStep` before routing. */
84
+ onSuccess?: (result: LoginResult) => void;
85
+ /** Fires only for a completed session. */
86
+ onAuthenticated?: (user: AuthUser) => void;
87
+ /** Fires when the account must replace its credential first. */
88
+ onCredentialSetup?: (setup: Extract<LoginResult, {
89
+ nextStep: 'credential_setup';
90
+ }>) => void;
83
91
  onError?: (error: AuthError | Error) => void;
84
92
  }
85
93
  interface UseLoginReturn {
86
- login: (credentials: {
87
- email: string;
88
- password: string;
89
- }) => Promise<void>;
94
+ login: (credentials: LoginCredentials) => Promise<LoginResult | undefined>;
90
95
  isLoading: boolean;
91
96
  error: AuthError | Error | null;
92
97
  }
@@ -157,16 +157,23 @@ function useLogin(opts) {
157
157
  setIsLoading(true);
158
158
  setError(null);
159
159
  try {
160
- const user = await client.login(credentials);
161
- opts?.onSuccess?.(user);
160
+ const result = await client.login(credentials);
161
+ opts?.onSuccess?.(result);
162
+ if (result.nextStep === "credential_setup") {
163
+ opts?.onCredentialSetup?.(result);
164
+ } else {
165
+ opts?.onAuthenticated?.(result.user);
166
+ }
167
+ return result;
162
168
  } catch (err) {
163
169
  const e = err instanceof Error ? err : new Error(String(err));
164
170
  setError(e);
165
171
  opts?.onError?.(e);
172
+ return void 0;
166
173
  } finally {
167
174
  setIsLoading(false);
168
175
  }
169
- }, [client, opts?.onSuccess, opts?.onError]);
176
+ }, [client, opts?.onSuccess, opts?.onAuthenticated, opts?.onCredentialSetup, opts?.onError]);
170
177
  return { login, isLoading, error };
171
178
  }
172
179
  __name(useLogin, "useLogin");