najm-auth 2.0.15 → 3.0.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 +172 -50
- package/dist/{NajmAuthClient-Cn9bObLB.d.ts → NajmAuthClient-D2fSvQ_H.d.ts} +30 -5
- package/dist/client/index.d.ts +2 -2
- package/dist/client/index.js +15 -8
- package/dist/client/react/index.d.ts +10 -6
- package/dist/client/react/index.js +10 -3
- package/dist/client/server/index.d.ts +11 -4
- package/dist/client/server/index.js +31 -11
- package/dist/identity/ma.d.ts +1 -0
- package/dist/identity/ma.js +64 -0
- package/dist/index.d.ts +395 -91
- package/dist/index.js +1733 -862
- package/dist/ma-sNHnUGLO.d.ts +88 -0
- package/dist/schema/pg.d.ts +260 -1
- package/dist/schema/pg.js +13 -0
- package/dist/schema/sqlite.d.ts +284 -1
- package/dist/schema/sqlite.js +14 -1
- package/package.json +6 -1
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
|
|
509
|
-
`
|
|
510
|
-
|
|
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
|
|
|
@@ -727,52 +849,52 @@ async resetPassword(token: string, newPassword: string) {
|
|
|
727
849
|
}
|
|
728
850
|
```
|
|
729
851
|
|
|
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
|
|
852
|
+
### Purpose-Bound Credential Setup
|
|
853
|
+
|
|
854
|
+
Use `CredentialSetupService` when valid credentials should open only a
|
|
855
|
+
short-lived setup flow, not a complete application session. The default auth
|
|
856
|
+
schema includes the durable `credential_setup_sessions` table for PostgreSQL
|
|
857
|
+
and SQLite; generate and apply a consumer migration after upgrading.
|
|
858
|
+
|
|
859
|
+
```typescript
|
|
860
|
+
import { AuthService, CredentialSetupService } from 'najm-auth';
|
|
861
|
+
|
|
862
|
+
const options = {
|
|
863
|
+
purpose: 'password-setup',
|
|
864
|
+
cookieName: 'my-app.password-setup',
|
|
865
|
+
ttlMs: 10 * 60 * 1000,
|
|
866
|
+
};
|
|
867
|
+
|
|
868
|
+
// Verify the password without minting access/refresh tokens.
|
|
869
|
+
const user = await authService.verifyCredentials({ identifier, password });
|
|
870
|
+
|
|
871
|
+
// Or narrowly accept only an unverified pending account with one exact role.
|
|
872
|
+
const pendingSponsor = await authService.verifyPendingCredentials(
|
|
873
|
+
{ identifier, password },
|
|
874
|
+
'sponsor',
|
|
875
|
+
);
|
|
876
|
+
|
|
877
|
+
if (await appRequiresPasswordSetup(user.id)) {
|
|
878
|
+
// Revokes normal sessions and writes only an HttpOnly, SameSite=Strict,
|
|
879
|
+
// browser-session cookie. The database stores only its SHA-256 hash.
|
|
880
|
+
return credentialSetup.begin(user.id, options);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
return authService.establishSession(user);
|
|
884
|
+
|
|
885
|
+
// Complete an app-owned mutation in the same transaction as one-time
|
|
886
|
+
// consumption. If the callback fails, token consumption rolls back.
|
|
887
|
+
await credentialSetup.consume(options, async ({ userId }) => {
|
|
888
|
+
await replaceApplicationCredential(userId, newCredential);
|
|
889
|
+
});
|
|
890
|
+
```
|
|
891
|
+
|
|
892
|
+
Setup tokens are bound to a server-owned purpose, expire automatically, are
|
|
893
|
+
replaced when the same user starts that purpose again, and can be cancelled or
|
|
894
|
+
consumed exactly once. `require()` validates the current setup cookie without
|
|
895
|
+
consuming it; `cancel()` revokes it and clears the cookie.
|
|
896
|
+
|
|
897
|
+
### Session Management
|
|
776
898
|
|
|
777
899
|
- 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
900
|
- A stale refresh token presented after the 120-second rotation grace window revokes only that session's family as reuse protection
|
|
@@ -50,6 +50,34 @@ interface TokenPair {
|
|
|
50
50
|
accessTokenExpiresAt?: number;
|
|
51
51
|
refreshTokenExpiresAt?: number;
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Login credentials. `identifier` accepts an email address or a phone number;
|
|
55
|
+
* `email` stays supported for existing callers.
|
|
56
|
+
*/
|
|
57
|
+
interface LoginCredentials {
|
|
58
|
+
identifier?: string;
|
|
59
|
+
email?: string;
|
|
60
|
+
password: string;
|
|
61
|
+
/** Persist the auth cookies past the browser closing. */
|
|
62
|
+
rememberMe?: boolean;
|
|
63
|
+
[key: string]: unknown;
|
|
64
|
+
}
|
|
65
|
+
/** The account must replace its credential before it gets a session. */
|
|
66
|
+
interface CredentialSetupPending {
|
|
67
|
+
nextStep: 'credential_setup';
|
|
68
|
+
setupRequired: true;
|
|
69
|
+
purpose: string;
|
|
70
|
+
expiresAt: string;
|
|
71
|
+
}
|
|
72
|
+
interface AuthenticatedLogin {
|
|
73
|
+
nextStep: 'authenticated';
|
|
74
|
+
user: AuthUser;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Login answer. Branch on `nextStep`: `credential_setup` carries no tokens and
|
|
78
|
+
* leaves the client unauthenticated.
|
|
79
|
+
*/
|
|
80
|
+
type LoginResult = AuthenticatedLogin | CredentialSetupPending;
|
|
53
81
|
/**
|
|
54
82
|
* Retry configuration
|
|
55
83
|
*/
|
|
@@ -182,10 +210,7 @@ declare class NajmAuthClient {
|
|
|
182
210
|
private readonly prefix;
|
|
183
211
|
private readonly threshold;
|
|
184
212
|
constructor(config: AuthClientConfig);
|
|
185
|
-
login(credentials:
|
|
186
|
-
email: string;
|
|
187
|
-
password: string;
|
|
188
|
-
}): Promise<AuthUser>;
|
|
213
|
+
login(credentials: LoginCredentials): Promise<LoginResult>;
|
|
189
214
|
register(data: Record<string, unknown>): Promise<AuthUser>;
|
|
190
215
|
getOAuthLoginUrl(provider: OAuthProvider, options?: OAuthLoginOptions): string;
|
|
191
216
|
loginWithOAuth(provider: OAuthProvider, options?: OAuthLoginOptions): void;
|
|
@@ -255,4 +280,4 @@ declare class NajmAuthClient {
|
|
|
255
280
|
*/
|
|
256
281
|
declare function createAuthClient(config: AuthClientConfig): NajmAuthClient;
|
|
257
282
|
|
|
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
|
|
283
|
+
export { type AuthClientConfig as A, type CredentialSetupPending as C, type DecodedToken as D, FetchClient as F, type HydrateSession as H, type LoginCredentials as L, 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 AuthenticatedLogin as g, type LoginResult as h, type OAuthProvider as i, type RetryConfig as j, type ServerResponse as k, type TokenPair as l, createAuthClient as m };
|
package/dist/client/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { D as DecodedToken, T as TabSyncMessage, S as SyncPayload } from '../NajmAuthClient-
|
|
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,
|
|
1
|
+
import { D as DecodedToken, T as TabSyncMessage, S as SyncPayload } from '../NajmAuthClient-D2fSvQ_H.js';
|
|
2
|
+
export { A as AuthClientConfig, a as AuthError, b as AuthEvent, c as AuthEventHandler, d as AuthEventMap, e as AuthState, f as AuthUser, g as AuthenticatedLogin, C as CredentialSetupPending, F as FetchClient, H as HydrateSession, L as LoginCredentials, h as LoginResult, N as NajmAuthClient, O as OAuthLoginOptions, i as OAuthProvider, R as RequestOptions, j as RetryConfig, k as ServerResponse, l as TokenPair, m as createAuthClient } from '../NajmAuthClient-D2fSvQ_H.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Decode a JWT token payload without verification.
|
package/dist/client/index.js
CHANGED
|
@@ -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
|
-
|
|
247
|
-
|
|
248
|
-
)
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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,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, e as AuthState, f as AuthUser, a as AuthError, O as OAuthLoginOptions, b as AuthEvent, d as AuthEventMap } from '../../NajmAuthClient-
|
|
4
|
+
import { N as NajmAuthClient, H as HydrateSession, e as AuthState, f as AuthUser, h as LoginResult, a as AuthError, L as LoginCredentials, O as OAuthLoginOptions, b as AuthEvent, d as AuthEventMap } from '../../NajmAuthClient-D2fSvQ_H.js';
|
|
5
5
|
|
|
6
6
|
interface AuthProviderProps {
|
|
7
7
|
client: NajmAuthClient;
|
|
@@ -79,14 +79,18 @@ interface UsePermissionsReturn {
|
|
|
79
79
|
declare function usePermissions(): UsePermissionsReturn;
|
|
80
80
|
|
|
81
81
|
interface UseLoginOptions {
|
|
82
|
-
|
|
82
|
+
/** Fires for both branches — check `result.nextStep` before routing. */
|
|
83
|
+
onSuccess?: (result: LoginResult) => void;
|
|
84
|
+
/** Fires only for a completed session. */
|
|
85
|
+
onAuthenticated?: (user: AuthUser) => void;
|
|
86
|
+
/** Fires when the account must replace its credential first. */
|
|
87
|
+
onCredentialSetup?: (setup: Extract<LoginResult, {
|
|
88
|
+
nextStep: 'credential_setup';
|
|
89
|
+
}>) => void;
|
|
83
90
|
onError?: (error: AuthError | Error) => void;
|
|
84
91
|
}
|
|
85
92
|
interface UseLoginReturn {
|
|
86
|
-
login: (credentials:
|
|
87
|
-
email: string;
|
|
88
|
-
password: string;
|
|
89
|
-
}) => Promise<void>;
|
|
93
|
+
login: (credentials: LoginCredentials) => Promise<LoginResult | undefined>;
|
|
90
94
|
isLoading: boolean;
|
|
91
95
|
error: AuthError | Error | null;
|
|
92
96
|
}
|
|
@@ -157,16 +157,23 @@ function useLogin(opts) {
|
|
|
157
157
|
setIsLoading(true);
|
|
158
158
|
setError(null);
|
|
159
159
|
try {
|
|
160
|
-
const
|
|
161
|
-
opts?.onSuccess?.(
|
|
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");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { f as AuthUser, F as FetchClient, N as NajmAuthClient,
|
|
1
|
+
import { f as AuthUser, F as FetchClient, N as NajmAuthClient, j as RetryConfig } from '../../NajmAuthClient-D2fSvQ_H.js';
|
|
2
2
|
import { SessionRecoveryFailure } from '../edge.js';
|
|
3
3
|
export { SessionRecoveryErrorDetails, SessionRecoveryFailureReason, withAuthMiddleware } from '../edge.js';
|
|
4
4
|
import 'next/server';
|
|
@@ -296,13 +296,20 @@ interface AuthCookiePersistenceOptions {
|
|
|
296
296
|
logoutPaths?: string[];
|
|
297
297
|
/** Paths that reissue cookies and must reapply the stored choice. */
|
|
298
298
|
refreshPaths?: string[];
|
|
299
|
+
/**
|
|
300
|
+
* Paths that finish credential setup. The stored choice is cleared there:
|
|
301
|
+
* the login it was recorded for never produced a session.
|
|
302
|
+
*/
|
|
303
|
+
setupCompletionPaths?: string[];
|
|
299
304
|
/**
|
|
300
305
|
* Recognizes a response that has *not* issued a usable session because the
|
|
301
306
|
* user must still set up credentials.
|
|
302
307
|
*
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
308
|
+
* Najm's own setup response is recognized without this — supply it only to
|
|
309
|
+
* cover an application-specific shape. Such a response may carry auth
|
|
310
|
+
* cookies anyway, and persisting them would leave a half-authenticated
|
|
311
|
+
* browser that skips the setup step on reload. Returning `true` strips them
|
|
312
|
+
* and clears the stored choice.
|
|
306
313
|
*/
|
|
307
314
|
isSetupResponse?: (payload: unknown) => boolean;
|
|
308
315
|
}
|
|
@@ -888,6 +888,10 @@ var TabSync = class {
|
|
|
888
888
|
};
|
|
889
889
|
|
|
890
890
|
// src/client/NajmAuthClient.ts
|
|
891
|
+
function isCredentialSetupPending(payload) {
|
|
892
|
+
return typeof payload === "object" && payload !== null && payload.nextStep === "credential_setup";
|
|
893
|
+
}
|
|
894
|
+
__name(isCredentialSetupPending, "isCredentialSetupPending");
|
|
891
895
|
var INITIAL_STATE = {
|
|
892
896
|
user: null,
|
|
893
897
|
accessToken: null,
|
|
@@ -939,20 +943,23 @@ var NajmAuthClient = class _NajmAuthClient {
|
|
|
939
943
|
// Auth Operations
|
|
940
944
|
// =========================================================================
|
|
941
945
|
async login(credentials) {
|
|
942
|
-
const res = await this.api.post(
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
)
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
946
|
+
const res = await this.api.post(`${this.prefix}/login`, { body: credentials, skipAuth: true });
|
|
947
|
+
this.resetRefreshFailures();
|
|
948
|
+
const setup = isCredentialSetupPending(res) ? res : isCredentialSetupPending(res.data) ? res.data : null;
|
|
949
|
+
if (setup) {
|
|
950
|
+
return { ...setup };
|
|
951
|
+
}
|
|
952
|
+
const authenticated = res.data;
|
|
953
|
+
this.applyTokens(authenticated);
|
|
954
|
+
if (authenticated.user) {
|
|
955
|
+
this.state = { ...this.state, user: authenticated.user };
|
|
949
956
|
this.notify();
|
|
950
957
|
} else {
|
|
951
958
|
await this.fetchUser();
|
|
952
959
|
}
|
|
953
960
|
this.tabSync?.broadcastSync(this.getSyncPayload());
|
|
954
961
|
this.emit("login", this.state.user);
|
|
955
|
-
return this.state.user;
|
|
962
|
+
return { nextStep: "authenticated", user: this.state.user };
|
|
956
963
|
}
|
|
957
964
|
async register(data) {
|
|
958
965
|
const res = await this.api.post(
|
|
@@ -1487,8 +1494,17 @@ var DEFAULTS = {
|
|
|
1487
1494
|
maxAgeSeconds: 7 * 24 * 60 * 60,
|
|
1488
1495
|
loginPaths: ["/api/auth/login"],
|
|
1489
1496
|
logoutPaths: ["/api/auth/logout"],
|
|
1490
|
-
refreshPaths: ["/api/auth/refresh", "/api/auth/session/recover"]
|
|
1497
|
+
refreshPaths: ["/api/auth/refresh", "/api/auth/session/recover"],
|
|
1498
|
+
setupCompletionPaths: ["/api/auth/credential-setup/change"]
|
|
1491
1499
|
};
|
|
1500
|
+
function isNajmSetupResponse(payload) {
|
|
1501
|
+
if (typeof payload !== "object" || payload === null) return false;
|
|
1502
|
+
const body = payload;
|
|
1503
|
+
if (body.nextStep === "credential_setup") return true;
|
|
1504
|
+
const data = body.data;
|
|
1505
|
+
return typeof data === "object" && data !== null && data.nextStep === "credential_setup";
|
|
1506
|
+
}
|
|
1507
|
+
__name(isNajmSetupResponse, "isNajmSetupResponse");
|
|
1492
1508
|
function cookieValue(header, name) {
|
|
1493
1509
|
for (const part of header.split(";")) {
|
|
1494
1510
|
const separator = part.indexOf("=");
|
|
@@ -1547,6 +1563,7 @@ function withAuthCookiePersistence(handler, options = {}) {
|
|
|
1547
1563
|
loginPaths = DEFAULTS.loginPaths,
|
|
1548
1564
|
logoutPaths = DEFAULTS.logoutPaths,
|
|
1549
1565
|
refreshPaths = DEFAULTS.refreshPaths,
|
|
1566
|
+
setupCompletionPaths = DEFAULTS.setupCompletionPaths,
|
|
1550
1567
|
isSetupResponse
|
|
1551
1568
|
} = options;
|
|
1552
1569
|
const resolveAction = /* @__PURE__ */ __name(async (request) => {
|
|
@@ -1559,6 +1576,7 @@ function withAuthCookiePersistence(handler, options = {}) {
|
|
|
1559
1576
|
};
|
|
1560
1577
|
}
|
|
1561
1578
|
if (logoutPaths.includes(pathname)) return { type: "clear" };
|
|
1579
|
+
if (setupCompletionPaths.includes(pathname)) return { type: "clear" };
|
|
1562
1580
|
if (refreshPaths.includes(pathname)) {
|
|
1563
1581
|
const remembered = cookieValue(
|
|
1564
1582
|
request.headers.get("cookie") ?? "",
|
|
@@ -1596,9 +1614,11 @@ function withAuthCookiePersistence(handler, options = {}) {
|
|
|
1596
1614
|
let action = await resolveAction(request);
|
|
1597
1615
|
const response = await handler(request);
|
|
1598
1616
|
if (!response.ok) return response;
|
|
1599
|
-
if (action?.type === "apply" &&
|
|
1617
|
+
if (action?.type === "apply" && loginPaths.includes(new URL(request.url).pathname)) {
|
|
1600
1618
|
const payload = await response.clone().json().catch(() => null);
|
|
1601
|
-
if (isSetupResponse(payload))
|
|
1619
|
+
if (isNajmSetupResponse(payload) || isSetupResponse?.(payload)) {
|
|
1620
|
+
action = { type: "setup" };
|
|
1621
|
+
}
|
|
1602
1622
|
}
|
|
1603
1623
|
if (!action) return response;
|
|
1604
1624
|
return applyAction(
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { M as MOROCCAN_CIN_TEMPORARY_CREDENTIAL_KIND, e as TemporaryCredential, T as TemporaryCredentialInput, i as isMoroccanCin, m as moroccanCinTemporaryCredential, j as moroccoIdentityPreset, n as normalizeMoroccanCin, k as normalizeMoroccanPhone } from '../ma-sNHnUGLO.js';
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
|
+
|
|
4
|
+
// src/identity/presets.ts
|
|
5
|
+
var compactPhone = /* @__PURE__ */ __name((value) => value.trim().replace(/[\s().-]+/g, ""), "compactPhone");
|
|
6
|
+
var MOROCCO_LOCAL = /^0\d{9}$/;
|
|
7
|
+
var MOROCCO_NATIONAL = /^212\d{9}$/;
|
|
8
|
+
var MOROCCO_E164 = /^\+212\d{9}$/;
|
|
9
|
+
var normalizeMoroccanPhone = /* @__PURE__ */ __name((value) => {
|
|
10
|
+
const compact = compactPhone(value);
|
|
11
|
+
if (MOROCCO_LOCAL.test(compact))
|
|
12
|
+
return `+212${compact.slice(1)}`;
|
|
13
|
+
if (MOROCCO_NATIONAL.test(compact))
|
|
14
|
+
return `+${compact}`;
|
|
15
|
+
if (MOROCCO_E164.test(compact))
|
|
16
|
+
return compact;
|
|
17
|
+
return null;
|
|
18
|
+
}, "normalizeMoroccanPhone");
|
|
19
|
+
var moroccoIdentityPreset = {
|
|
20
|
+
name: "ma",
|
|
21
|
+
normalize: normalizeMoroccanPhone
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// src/identity/temporaryCredential.ts
|
|
25
|
+
var EXACT_TEMPORARY_CREDENTIAL_KIND = "exact";
|
|
26
|
+
var exactKind = {
|
|
27
|
+
name: EXACT_TEMPORARY_CREDENTIAL_KIND,
|
|
28
|
+
normalize: /* @__PURE__ */ __name((value) => value, "normalize")
|
|
29
|
+
};
|
|
30
|
+
var MOROCCAN_CIN_TEMPORARY_CREDENTIAL_KIND = "ma-cin";
|
|
31
|
+
var MOROCCAN_CIN = /^[a-z]{1,3}\d{5,17}$/i;
|
|
32
|
+
function isMoroccanCin(value) {
|
|
33
|
+
const trimmed = value.trim();
|
|
34
|
+
return trimmed.length >= 8 && trimmed.length <= 20 && MOROCCAN_CIN.test(trimmed);
|
|
35
|
+
}
|
|
36
|
+
__name(isMoroccanCin, "isMoroccanCin");
|
|
37
|
+
function normalizeMoroccanCin(value) {
|
|
38
|
+
return isMoroccanCin(value) ? value.trim().toLowerCase() : value;
|
|
39
|
+
}
|
|
40
|
+
__name(normalizeMoroccanCin, "normalizeMoroccanCin");
|
|
41
|
+
var moroccanCinKind = {
|
|
42
|
+
name: MOROCCAN_CIN_TEMPORARY_CREDENTIAL_KIND,
|
|
43
|
+
normalize: normalizeMoroccanCin,
|
|
44
|
+
isTemporaryShape: isMoroccanCin
|
|
45
|
+
};
|
|
46
|
+
var KINDS = /* @__PURE__ */ new Map([
|
|
47
|
+
[exactKind.name, exactKind],
|
|
48
|
+
[moroccanCinKind.name, moroccanCinKind]
|
|
49
|
+
]);
|
|
50
|
+
function moroccanCinTemporaryCredential(value) {
|
|
51
|
+
if (!isMoroccanCin(value)) {
|
|
52
|
+
throw new Error("moroccanCinTemporaryCredential requires a valid Moroccan CIN");
|
|
53
|
+
}
|
|
54
|
+
return { kind: MOROCCAN_CIN_TEMPORARY_CREDENTIAL_KIND, value };
|
|
55
|
+
}
|
|
56
|
+
__name(moroccanCinTemporaryCredential, "moroccanCinTemporaryCredential");
|
|
57
|
+
export {
|
|
58
|
+
MOROCCAN_CIN_TEMPORARY_CREDENTIAL_KIND,
|
|
59
|
+
isMoroccanCin,
|
|
60
|
+
moroccanCinTemporaryCredential,
|
|
61
|
+
moroccoIdentityPreset,
|
|
62
|
+
normalizeMoroccanCin,
|
|
63
|
+
normalizeMoroccanPhone
|
|
64
|
+
};
|