najm-auth 2.0.14 → 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/dist/index.d.ts CHANGED
@@ -3,18 +3,80 @@ import { Container } from 'najm-core';
3
3
  import { ValidationPluginConfig } from 'najm-validation';
4
4
  import { RateLimitPluginConfig } from 'najm-rate';
5
5
  import { EmailPluginConfig, EmailService } from 'najm-email';
6
+ import { R as ResolvedIdentityConfig, I as IdentityConfig, T as TemporaryCredentialInput } from './ma-sNHnUGLO.js';
7
+ export { D as DEFAULT_IDENTITY_PRESET, E as EXACT_TEMPORARY_CREDENTIAL_KIND, a as IDENTITY_PRESETS, b as IdentityNormalizer, c as IdentityPreset, d as IdentityPresetName, M as MOROCCAN_CIN_TEMPORARY_CREDENTIAL_KIND, e as TemporaryCredential, f as TemporaryCredentialKind, g as compactPhone, i as isMoroccanCin, h as isTemporaryCredentialKind, m as moroccanCinTemporaryCredential, j as moroccoIdentityPreset, n as normalizeMoroccanCin, k as normalizeMoroccanPhone, l as normalizeTunisianPhone, r as resolveTemporaryCredentialKind, t as toTemporaryCredential, o as tunisiaIdentityPreset } from './ma-sNHnUGLO.js';
8
+ import { ZodType, z } from 'zod';
6
9
  import { I18nService } from 'najm-i18n';
7
10
  import { TDb, SeedEntry } from 'najm-database';
8
11
  import { User, NewUser, RoleEntity, NewRoleEntity, Permission, NewPermission, RolePermission } from './schema/pg.js';
9
- export { CredentialSetupSession, NewCredentialSetupSession, NewOAuthAccount, NewRolePermission, NewToken, OAuthAccount, Token, authSchema, baseFields, credentialSetupSessionsTable, oauthAccountsTable, permissionsTable, rolePermissionsTable, rolesTable, tokenStatusEnum, tokenTypeEnum, tokensTable, userStatusEnum, usersTable } from './schema/pg.js';
12
+ export { CredentialSetupRequirement, CredentialSetupSession, NewCredentialSetupRequirement, NewCredentialSetupSession, NewOAuthAccount, NewRolePermission, NewToken, OAuthAccount, Token, authSchema, baseFields, credentialSetupRequirementsTable, credentialSetupSessionsTable, oauthAccountsTable, permissionsTable, rolePermissionsTable, rolesTable, tokenStatusEnum, tokenTypeEnum, tokensTable, userStatusEnum, usersTable } from './schema/pg.js';
10
13
  import { CacheService } from 'najm-cache';
11
- import { z } from 'zod';
14
+ import { CookieService } from 'najm-cookies';
12
15
  import { Context } from 'hono';
13
16
  import { GuardResult } from 'najm-guard';
14
- import { CookieService } from 'najm-cookies';
15
17
  import 'drizzle-orm';
16
18
  import 'drizzle-orm/pg-core';
17
19
 
20
+ interface CredentialSetupOptions {
21
+ /** Stable server-owned purpose, for example `password`. */
22
+ purpose: string;
23
+ /** HttpOnly browser-session cookie name (default: `najm.credential-setup`). */
24
+ cookieName?: string;
25
+ /** Session lifetime in milliseconds (default: 10 minutes, maximum: 24 hours). */
26
+ ttlMs?: number;
27
+ /** Cookie path (default: `/`). */
28
+ cookiePath?: string;
29
+ }
30
+ interface CredentialSetupSessionInfo {
31
+ userId: string;
32
+ purpose: string;
33
+ expiresAt: string;
34
+ }
35
+ interface CredentialSetupStarted {
36
+ purpose: string;
37
+ expiresAt: string;
38
+ }
39
+ /** The built-in setup purpose Najm mounts endpoints for. */
40
+ declare const PASSWORD_SETUP_PURPOSE = "password";
41
+ /** Durable "this user still owes a setup purpose" row. */
42
+ interface CredentialSetupRequirementRow {
43
+ userId: string;
44
+ purpose: string;
45
+ temporaryCredentialKind: string | null;
46
+ required: boolean;
47
+ completedAt: string | null;
48
+ }
49
+ /**
50
+ * Login answer when the account may not have a normal session yet. Carries no
51
+ * access or refresh token — only the fact that setup is pending and until when.
52
+ */
53
+ interface CredentialSetupPending {
54
+ nextStep: 'credential_setup';
55
+ setupRequired: true;
56
+ purpose: string;
57
+ expiresAt: string;
58
+ }
59
+ interface CredentialSetupPasswordOptions {
60
+ /**
61
+ * Replacement-password schema. Default: 8–72 bytes with at least one letter
62
+ * and one digit — deliberately case-agnostic, because a first-login
63
+ * replacement is typed by someone who just proved they own the account.
64
+ */
65
+ passwordSchema?: ZodType<string>;
66
+ /** Setup-session lifetime in milliseconds (default: 10 minutes). */
67
+ ttlMs?: number;
68
+ /** Setup cookie name (default: `najm.credential-setup`). */
69
+ cookieName?: string;
70
+ }
71
+ interface CredentialSetupConfig {
72
+ password?: CredentialSetupPasswordOptions;
73
+ }
74
+ interface ResolvedCredentialSetupConfig {
75
+ password: Required<Pick<CredentialSetupPasswordOptions, 'ttlMs' | 'cookieName'>> & {
76
+ passwordSchema: ZodType<string>;
77
+ };
78
+ }
79
+
18
80
  /**
19
81
  * JWT configuration for token generation and verification
20
82
  */
@@ -115,8 +177,12 @@ interface AuthConfig {
115
177
  lockout: LockoutConfig;
116
178
  /** Bcrypt work factor (default: 10) */
117
179
  bcryptRounds: number;
180
+ /** Server-scoped login and phone identity policy. */
181
+ identity: ResolvedIdentityConfig;
118
182
  /** Session cookie cache settings */
119
183
  session: SessionCookieConfig;
184
+ /** Resolved credential-setup policy for the built-in `password` flow. */
185
+ credentialSetup: ResolvedCredentialSetupConfig;
120
186
  /** Resolved external identity-provider configuration. */
121
187
  oauth?: ResolvedOAuthConfig;
122
188
  }
@@ -133,6 +199,8 @@ interface AuthSchema {
133
199
  tokens: any;
134
200
  /** Durable one-time browser sessions used by CredentialSetupService. */
135
201
  credentialSetupSessions?: any;
202
+ /** Durable "this user still owes a setup purpose" rows. */
203
+ credentialSetupRequirements?: any;
136
204
  roles: any;
137
205
  permissions: any;
138
206
  rolePermissions: any;
@@ -176,6 +244,18 @@ type AuthPluginConfig = {
176
244
  bcryptRounds?: number;
177
245
  /** Session cookie cache settings (optional — sensible defaults applied) */
178
246
  session?: Partial<SessionCookieConfig>;
247
+ /**
248
+ * Login identifier normalization. Defaults to the Moroccan preset; pass
249
+ * another preset to replace it, or `extend` for project-specific
250
+ * identifiers.
251
+ */
252
+ identity?: IdentityConfig;
253
+ /**
254
+ * Policy overrides for the built-in credential-setup flow. The flow itself
255
+ * is always mounted — there is no activation switch — and does nothing until
256
+ * a user has a durable setup requirement.
257
+ */
258
+ credentialSetup?: CredentialSetupConfig;
179
259
  /** Optional config forwarded to validation() dependency */
180
260
  validation?: ValidationPluginConfig;
181
261
  /** Optional config forwarded to rateLimit() dependency */
@@ -272,7 +352,15 @@ var auth = {
272
352
  oauthProviderAccountLinked: "This Google account is already linked.",
273
353
  oauthSignupDisabled: "Registration with Google is disabled.",
274
354
  oauthHostedDomainDenied: "This Google Workspace domain is not allowed.",
275
- oauthLinkSessionExpired: "Your session changed before Google could be linked. Please try again."
355
+ oauthLinkSessionExpired: "Your session changed before Google could be linked. Please try again.",
356
+ oauthCredentialSetupRequired: "Set a new password before signing in with Google.",
357
+ credentialSetupRequired: "Set a new password before continuing.",
358
+ credentialSetupSessionRequired: "Credential setup session is required.",
359
+ credentialSetupSessionInvalid: "Credential setup session is invalid or expired.",
360
+ credentialSetupAlreadyCompleted: "The password was already replaced. Please sign in again.",
361
+ credentialSetupPasswordRejected: "Choose a different password.",
362
+ credentialSetupTemporaryShape: "Choose a password that is not your temporary credential.",
363
+ credentialSetupSamePassword: "Choose a password different from your current one."
276
364
  },
277
365
  success: {
278
366
  login: "Login successful",
@@ -285,7 +373,10 @@ var auth = {
285
373
  tokenRefreshed: "Token refreshed successfully",
286
374
  sessionRecovered: "Session recovered successfully",
287
375
  oauthLogin: "Google sign-in successful",
288
- oauthLinked: "Google account linked successfully"
376
+ oauthLinked: "Google account linked successfully",
377
+ credentialSetupPending: "Password setup is required",
378
+ credentialSetupPasswordReplaced: "Password set successfully",
379
+ credentialSetupCancelled: "Password setup cancelled"
289
380
  },
290
381
  emails: {
291
382
  passwordReset: {
@@ -301,6 +392,7 @@ var users = {
301
392
  notFound: "User not found",
302
393
  idExists: "User ID already exists",
303
394
  emailRequired: "Email is required",
395
+ phoneExists: "Phone number already registered",
304
396
  passwordRequired: "Password is required",
305
397
  invalidEmail: "Invalid email format",
306
398
  weakPassword: "Password is too weak",
@@ -391,6 +483,14 @@ declare const AUTH_LOCALES: {
391
483
  oauthSignupDisabled: string;
392
484
  oauthHostedDomainDenied: string;
393
485
  oauthLinkSessionExpired: string;
486
+ oauthCredentialSetupRequired: string;
487
+ credentialSetupRequired: string;
488
+ credentialSetupSessionRequired: string;
489
+ credentialSetupSessionInvalid: string;
490
+ credentialSetupAlreadyCompleted: string;
491
+ credentialSetupPasswordRejected: string;
492
+ credentialSetupTemporaryShape: string;
493
+ credentialSetupSamePassword: string;
394
494
  };
395
495
  success: {
396
496
  login: string;
@@ -404,6 +504,9 @@ declare const AUTH_LOCALES: {
404
504
  sessionRecovered: string;
405
505
  oauthLogin: string;
406
506
  oauthLinked: string;
507
+ credentialSetupPending: string;
508
+ credentialSetupPasswordReplaced: string;
509
+ credentialSetupCancelled: string;
407
510
  };
408
511
  emails: {
409
512
  passwordReset: {
@@ -419,6 +522,7 @@ declare const AUTH_LOCALES: {
419
522
  notFound: string;
420
523
  idExists: string;
421
524
  emailRequired: string;
525
+ phoneExists: string;
422
526
  passwordRequired: string;
423
527
  invalidEmail: string;
424
528
  weakPassword: string;
@@ -589,6 +693,10 @@ declare class UserValidator {
589
693
  * Check if email already exists in database
590
694
  */
591
695
  checkEmailUnique(email: string, excludeId?: string): Promise<void>;
696
+ /**
697
+ * Check that a normalized phone number is not already taken.
698
+ */
699
+ checkPhoneUnique(phone: string, excludeId?: string): Promise<void>;
592
700
  /**
593
701
  * Check if user exists by ID
594
702
  */
@@ -597,6 +705,7 @@ declare class UserValidator {
597
705
  * Check if user exists by email
598
706
  */
599
707
  checkUserExistsByEmail(email: string): Promise<{
708
+ password: string;
600
709
  id: string;
601
710
  name: string;
602
711
  createdAt: string;
@@ -605,7 +714,6 @@ declare class UserValidator {
605
714
  emailVerified: boolean;
606
715
  phone: string;
607
716
  phoneVerified: boolean;
608
- password: string;
609
717
  image: string;
610
718
  status: "active" | "pending" | "inactive";
611
719
  roleId: string;
@@ -619,6 +727,7 @@ declare class UserValidator {
619
727
  * Check if email exists in database
620
728
  */
621
729
  checkEmailExists(email: string): Promise<{
730
+ password: string;
622
731
  id: string;
623
732
  name: string;
624
733
  createdAt: string;
@@ -627,7 +736,6 @@ declare class UserValidator {
627
736
  emailVerified: boolean;
628
737
  phone: string;
629
738
  phoneVerified: boolean;
630
- password: string;
631
739
  image: string;
632
740
  status: "active" | "pending" | "inactive";
633
741
  roleId: string;
@@ -663,6 +771,8 @@ declare class UserValidator {
663
771
  * - At least one number
664
772
  */
665
773
  validatePasswordStrength(password: string): void;
774
+ /** Keep every byte passed to bcrypt significant. */
775
+ validatePasswordLength(password: string): void;
666
776
  }
667
777
 
668
778
  declare class RoleRepository {
@@ -825,7 +935,9 @@ declare class UserService {
825
935
  }) | undefined>;
826
936
  findByPhone(phone: string): Promise<UserWithPermissions>;
827
937
  getAuthRecordById(id: string): Promise<User | undefined>;
828
- create(data: Record<string, any>): Promise<SanitizedUser>;
938
+ create(data: Record<string, any>, options?: {
939
+ validatePasswordStrength?: boolean;
940
+ }): Promise<SanitizedUser>;
829
941
  update(id: string, data: Record<string, any>): Promise<SanitizedUser>;
830
942
  delete(id: string): Promise<SanitizedUser>;
831
943
  deleteAll(): Promise<SanitizedUser[]>;
@@ -899,13 +1011,26 @@ declare class TokenRepository {
899
1011
  getUser(userId: string): Promise<any>;
900
1012
  }
901
1013
 
1014
+ declare class CredentialSetupRequirementRepository {
1015
+ private db;
1016
+ private schema;
1017
+ private get requirements();
1018
+ private get columns();
1019
+ markRequired(userId: string, purpose: string, temporaryCredentialKind: string | null): Promise<CredentialSetupRequirementRow>;
1020
+ find(userId: string, purpose: string): Promise<CredentialSetupRequirementRow | undefined>;
1021
+ listRequired(userId: string): Promise<CredentialSetupRequirementRow[]>;
1022
+ /** Only a still-required row completes, so a replayed completion is a no-op. */
1023
+ complete(userId: string, purpose: string): Promise<CredentialSetupRequirementRow | undefined>;
1024
+ }
1025
+
902
1026
  declare class TokenService {
903
1027
  private tokenRepository;
904
1028
  private cookieManager;
905
1029
  private cache;
1030
+ private credentialSetupRequirements?;
906
1031
  private config;
907
1032
  private t;
908
- constructor(tokenRepository: TokenRepository, cookieManager: CookieManager, cache: CacheService);
1033
+ constructor(tokenRepository: TokenRepository, cookieManager: CookieManager, cache: CacheService, credentialSetupRequirements?: CredentialSetupRequirementRepository);
909
1034
  /**
910
1035
  * Get blacklist key prefix
911
1036
  */
@@ -1151,9 +1276,11 @@ declare const userIdParam: z.ZodObject<{
1151
1276
  declare const loginDto: z.ZodUnion<readonly [z.ZodObject<{
1152
1277
  identifier: z.ZodString;
1153
1278
  password: z.ZodString;
1279
+ rememberMe: z.ZodOptional<z.ZodBoolean>;
1154
1280
  }, z.core.$strip>, z.ZodObject<{
1155
1281
  email: z.ZodString;
1156
1282
  password: z.ZodString;
1283
+ rememberMe: z.ZodOptional<z.ZodBoolean>;
1157
1284
  }, z.core.$strip>]>;
1158
1285
  declare const changePasswordDto: z.ZodObject<{
1159
1286
  currentPassword: z.ZodString;
@@ -1198,16 +1325,143 @@ type UserIdInParam = z.infer<typeof userIdInParam>;
1198
1325
  type AssignRoleParams = z.infer<typeof assignRoleParams>;
1199
1326
  type UserListQuery = z.infer<typeof userListQuery>;
1200
1327
 
1328
+ /**
1329
+ * The durable half of credential setup: what a user still owes, independent of
1330
+ * any browser. `CredentialSetupService` owns the other half — the short-lived
1331
+ * cookie session that lets one browser satisfy it.
1332
+ */
1333
+ declare class CredentialSetupRequirementService {
1334
+ private readonly repository;
1335
+ private readonly tokens;
1336
+ constructor(repository: CredentialSetupRequirementRepository, tokens: TokenService);
1337
+ /**
1338
+ * Idempotent. Re-marking clears a previous completion, and revokes the
1339
+ * user's current sessions so an already signed-in browser cannot skip it.
1340
+ */
1341
+ markRequired(userId: string, purpose: string, options?: {
1342
+ temporaryCredentialKind?: string | null;
1343
+ }): Promise<CredentialSetupRequirementRow>;
1344
+ find(userId: string, purpose: string): Promise<CredentialSetupRequirementRow | undefined>;
1345
+ isRequired(userId: string, purpose: string): Promise<boolean>;
1346
+ listRequired(userId: string): Promise<CredentialSetupRequirementRow[]>;
1347
+ /** Returns the completed row, or `undefined` when nothing was still required. */
1348
+ completeRequirement(userId: string, purpose: string): Promise<CredentialSetupRequirementRow | undefined>;
1349
+ }
1350
+
1201
1351
  declare class AuthSessionService {
1202
1352
  private tokenService;
1203
1353
  private userService;
1204
1354
  private cookieManager;
1205
- constructor(tokenService: TokenService, userService: UserService, cookieManager: CookieManager);
1355
+ private credentialSetupRequirements?;
1356
+ constructor(tokenService: TokenService, userService: UserService, cookieManager: CookieManager, credentialSetupRequirements?: CredentialSetupRequirementService);
1206
1357
  establish(user: SanitizedUser): Promise<TokenPair & {
1207
1358
  user: SanitizedUser;
1208
1359
  }>;
1209
1360
  }
1210
1361
 
1362
+ type NewSetupSession = {
1363
+ userId: string;
1364
+ purpose: string;
1365
+ tokenHash: string;
1366
+ expiresAt: string;
1367
+ };
1368
+ declare class CredentialSetupRepository {
1369
+ private db;
1370
+ private schema;
1371
+ private get sessions();
1372
+ replaceActive(data: NewSetupSession): Promise<any>;
1373
+ findActive(tokenHash: string, purpose: string): Promise<any>;
1374
+ consume(tokenHash: string, purpose: string): Promise<any>;
1375
+ revoke(tokenHash: string, purpose: string): Promise<any>;
1376
+ deleteExpired(): Promise<any>;
1377
+ }
1378
+
1379
+ declare const DEFAULT_CREDENTIAL_SETUP_COOKIE_NAME = "najm.credential-setup";
1380
+ declare const DEFAULT_CREDENTIAL_SETUP_TTL_MS: number;
1381
+ declare class CredentialSetupService {
1382
+ private readonly repository;
1383
+ private readonly tokens;
1384
+ private readonly authCookies;
1385
+ private readonly cookies;
1386
+ private t?;
1387
+ constructor(repository: CredentialSetupRepository, tokens: TokenService, authCookies: CookieManager, cookies: CookieService);
1388
+ /**
1389
+ * Revoke normal auth sessions and issue a single-purpose, short-lived,
1390
+ * browser-session cookie. No access or refresh token is minted.
1391
+ */
1392
+ begin(userId: string, options: CredentialSetupOptions): Promise<CredentialSetupStarted>;
1393
+ /** Validate and return the current active setup session without consuming it. */
1394
+ require(options: CredentialSetupOptions): Promise<CredentialSetupSessionInfo>;
1395
+ /**
1396
+ * Atomically consume the setup session and execute the application-owned
1397
+ * credential mutation in the same database transaction. If the callback
1398
+ * fails, consumption rolls back and the browser may safely retry.
1399
+ */
1400
+ consume<T>(options: CredentialSetupOptions, complete: (session: CredentialSetupSessionInfo) => Promise<T> | T): Promise<T>;
1401
+ cancel(options: CredentialSetupOptions): Promise<{
1402
+ cancelled: true;
1403
+ }>;
1404
+ pruneExpired(): Promise<void>;
1405
+ /** Tolerates direct construction outside DI, where `t` is not injected. */
1406
+ private message;
1407
+ private sessionRequired;
1408
+ private sessionInvalid;
1409
+ private resolveOptions;
1410
+ private hashToken;
1411
+ private setCookie;
1412
+ private clearCookie;
1413
+ }
1414
+
1415
+ /**
1416
+ * The built-in `password` setup flow. Always mounted by `auth()`; inert until a
1417
+ * user actually owes the `password` purpose.
1418
+ */
1419
+ declare class PasswordSetupService {
1420
+ private readonly setup;
1421
+ private readonly requirements;
1422
+ private readonly users;
1423
+ private readonly userRecords;
1424
+ private readonly validator;
1425
+ private readonly encryption;
1426
+ private config;
1427
+ private t?;
1428
+ constructor(setup: CredentialSetupService, requirements: CredentialSetupRequirementService, users: UserService, userRecords: UserRepository, validator: UserValidator, encryption: EncryptionService);
1429
+ /** Tolerates direct construction outside DI, where `t` is not injected. */
1430
+ private message;
1431
+ private get options();
1432
+ private pending;
1433
+ /** Revoke normal sessions and hand the browser a short-lived setup cookie. */
1434
+ begin(userId: string): Promise<CredentialSetupPending>;
1435
+ /** Read the active setup session without consuming it. */
1436
+ status(): Promise<CredentialSetupPending>;
1437
+ /**
1438
+ * Everything that can fail on the user's input is checked against the
1439
+ * *unconsumed* session, so a mistyped replacement leaves the browser able to
1440
+ * retry. Only the two writes that must agree — the password and the
1441
+ * requirement — happen inside the one-time consumption.
1442
+ */
1443
+ change(newPassword: string): Promise<{
1444
+ changed: true;
1445
+ signInAgain: true;
1446
+ }>;
1447
+ cancel(): Promise<{
1448
+ cancelled: true;
1449
+ }>;
1450
+ private validateReplacement;
1451
+ /**
1452
+ * Runs inside the setup-session consumption transaction, so the password
1453
+ * update, the requirement completion, and the one-time session all commit or
1454
+ * roll back together.
1455
+ */
1456
+ private persist;
1457
+ private validatePolicy;
1458
+ /**
1459
+ * The stored hash is of the *normalized* temporary credential, so a
1460
+ * differently-cased retype has to be compared in both forms.
1461
+ */
1462
+ private isCurrentPassword;
1463
+ }
1464
+
1211
1465
  /**
1212
1466
  * Identity fields for creating a user behind a person record (parent, student,
1213
1467
  * teacher, staff…). Role can be given by name (`role`) or id (`roleId`).
@@ -1216,11 +1470,35 @@ type ProvisionUserInput = {
1216
1470
  id?: string;
1217
1471
  name?: string;
1218
1472
  email: string;
1473
+ /** Normalized through the configured identity preset before it is stored. */
1474
+ phone?: string;
1219
1475
  role?: string;
1220
1476
  roleId?: string;
1221
1477
  image?: string | null;
1222
1478
  status?: 'active' | 'inactive' | 'pending';
1223
1479
  };
1480
+ /**
1481
+ * Provisioning that hands the user a temporary credential and durably requires
1482
+ * them to replace it at first login.
1483
+ *
1484
+ * Modelled as a union rather than optional fields on purpose: a caller must not
1485
+ * be able to set a permanent password and mark it temporary in the same call.
1486
+ */
1487
+ type ProvisionUserWithSetupInput = ProvisionUserInput & {
1488
+ temporaryCredential: TemporaryCredentialInput;
1489
+ requireCredentialSetup: typeof PASSWORD_SETUP_PURPOSE;
1490
+ password?: never;
1491
+ };
1492
+ type ProvisionUserWithPasswordInput = ProvisionUserInput & {
1493
+ password?: string | null;
1494
+ temporaryCredential?: never;
1495
+ requireCredentialSetup?: never;
1496
+ };
1497
+ /** Login answer: either a complete session, or a pending credential setup. */
1498
+ type LoginResult = (TokenPair & {
1499
+ nextStep: 'authenticated';
1500
+ user: SanitizedUser;
1501
+ }) | CredentialSetupPending;
1224
1502
  declare class AuthService {
1225
1503
  private tokenService;
1226
1504
  private userService;
@@ -1230,11 +1508,13 @@ declare class AuthService {
1230
1508
  private i18nService;
1231
1509
  private emailService;
1232
1510
  private authSessionService?;
1511
+ private credentialSetupRequirements?;
1512
+ private passwordSetup?;
1233
1513
  private config;
1234
1514
  private t;
1235
1515
  private logger;
1236
1516
  private dummyHash?;
1237
- constructor(tokenService: TokenService, userService: UserService, userValidator: UserValidator, encryptionService: EncryptionService, cookieManager: CookieManager, i18nService: I18nService, emailService: EmailService, authSessionService?: AuthSessionService);
1517
+ constructor(tokenService: TokenService, userService: UserService, userValidator: UserValidator, encryptionService: EncryptionService, cookieManager: CookieManager, i18nService: I18nService, emailService: EmailService, authSessionService?: AuthSessionService, credentialSetupRequirements?: CredentialSetupRequirementService, passwordSetup?: PasswordSetupService);
1238
1518
  private isLockoutActive;
1239
1519
  private nextLockoutUntil;
1240
1520
  private getDummyHash;
@@ -1260,12 +1540,14 @@ declare class AuthService {
1260
1540
  *
1261
1541
  * Returns the created (sanitized) user so the caller can link `userId`.
1262
1542
  */
1263
- provisionUser(body: ProvisionUserInput & {
1264
- password?: string | null;
1265
- }): Promise<SanitizedUser>;
1266
- loginUser(body: LoginDto): Promise<TokenPair & {
1267
- user: SanitizedUser;
1268
- }>;
1543
+ provisionUser(body: ProvisionUserWithPasswordInput | ProvisionUserWithSetupInput): Promise<SanitizedUser>;
1544
+ /**
1545
+ * Create the account and mark the durable requirement in one transaction, so
1546
+ * a user never exists holding a temporary credential that nothing forces
1547
+ * them to replace. No session is issued here.
1548
+ */
1549
+ private provisionWithCredentialSetup;
1550
+ loginUser(body: LoginDto): Promise<LoginResult>;
1269
1551
  /**
1270
1552
  * Verify credentials and account policy without minting access/refresh
1271
1553
  * tokens or writing normal auth cookies. Sensitive onboarding flows can use
@@ -1278,7 +1560,19 @@ declare class AuthService {
1278
1560
  * should exchange the result for a short-lived, purpose-bound setup session.
1279
1561
  */
1280
1562
  verifyPendingCredentials(body: LoginDto, expectedRole: string): Promise<SanitizedUser>;
1281
- private verifyCredentialsForPolicy;
1563
+ /**
1564
+ * The one credential path. Resolves identity, the durable setup requirement,
1565
+ * and the credential normalization that requirement implies — in that order —
1566
+ * before a single hash comparison decides the outcome. Nothing about the
1567
+ * requirement is revealed until credentials and account policy have passed.
1568
+ */
1569
+ private authenticate;
1570
+ /**
1571
+ * A user-chosen password is compared byte-for-byte. Only an active
1572
+ * requirement, and only its own stored kind, can transform the submitted
1573
+ * value — so lowercasing a CIN never leaks into normal logins.
1574
+ */
1575
+ private resolveLoginCredential;
1282
1576
  /** Establish a complete normal auth session for an already verified user. */
1283
1577
  establishSession(user: SanitizedUser): Promise<TokenPair & {
1284
1578
  user: SanitizedUser;
@@ -1340,10 +1634,9 @@ declare class AuthController {
1340
1634
  private authService;
1341
1635
  constructor(authService: AuthService);
1342
1636
  registerUser(body: RegisterDto): Promise<SanitizedUser>;
1343
- loginUser(body: LoginDto): Promise<TokenPair & {
1344
- user: SanitizedUser;
1345
- }>;
1637
+ loginUser(body: LoginDto): Promise<LoginResult>;
1346
1638
  inviteUser(body: InviteUserDto): Promise<Omit<{
1639
+ password: string;
1347
1640
  id: string;
1348
1641
  name: string;
1349
1642
  createdAt: string;
@@ -1352,7 +1645,6 @@ declare class AuthController {
1352
1645
  emailVerified: boolean;
1353
1646
  phone: string;
1354
1647
  phoneVerified: boolean;
1355
- password: string;
1356
1648
  image: string;
1357
1649
  status: "active" | "pending" | "inactive";
1358
1650
  roleId: string;
@@ -1377,6 +1669,7 @@ declare class AuthController {
1377
1669
  message: string;
1378
1670
  }>;
1379
1671
  userProfile(authorization?: string): Promise<Omit<{
1672
+ password: string;
1380
1673
  id: string;
1381
1674
  name: string;
1382
1675
  createdAt: string;
@@ -1385,7 +1678,6 @@ declare class AuthController {
1385
1678
  emailVerified: boolean;
1386
1679
  phone: string;
1387
1680
  phoneVerified: boolean;
1388
- password: string;
1389
1681
  image: string;
1390
1682
  status: "active" | "pending" | "inactive";
1391
1683
  roleId: string;
@@ -1450,10 +1742,27 @@ declare class AuthResolver {
1450
1742
  }
1451
1743
 
1452
1744
  /**
1453
- * Normalize an authentication identifier for both login resolution and
1454
- * rate-limit bucketing. Email comparison is case-insensitive. Phone login uses
1455
- * an E.164-compatible value and accepts common visual separators plus a `00`
1456
- * international prefix.
1745
+ * Publishes this server's identity resolver to request-local state before rate
1746
+ * limiting. Isolated Najm servers own separate service/config instances, so one
1747
+ * `auth()` call cannot replace another server's country preset.
1748
+ */
1749
+ declare class AuthIdentityContextService {
1750
+ private config;
1751
+ private container;
1752
+ constructor(config: AuthConfig);
1753
+ configure(): Promise<void>;
1754
+ }
1755
+
1756
+ type IdentityResolver = (value: unknown) => string | null;
1757
+ /**
1758
+ * Build the identifier pipeline used by login lookup, lockout accounting, and
1759
+ * rate-limit bucketing alike. Order is fixed: email, then project extensions,
1760
+ * then the selected country preset, then generic E.164.
1761
+ */
1762
+ declare function createIdentityResolver(config?: IdentityConfig): IdentityResolver;
1763
+ /**
1764
+ * Normalize with Najm's default identity policy. Server-owned login, phone,
1765
+ * lockout, and rate-limit paths use their own `AuthConfig.identity.resolve`.
1457
1766
  */
1458
1767
  declare function normalizeAuthIdentifier(value: unknown): string | null;
1459
1768
  declare function isEmailIdentifier(value: string): boolean;
@@ -1465,7 +1774,7 @@ interface RunAsUser {
1465
1774
  }
1466
1775
  declare function runAsUser<T>(container: Container, user: RunAsUser, fn: () => Promise<T> | T): Promise<T>;
1467
1776
 
1468
- declare const AUTH_MODULE: readonly [typeof AuthService, typeof AuthSessionService, typeof CookieManager, typeof EncryptionService, typeof AuthGuard, typeof AuthController, typeof AuthResolver];
1777
+ declare const AUTH_MODULE: readonly [typeof AuthService, typeof AuthSessionService, typeof CookieManager, typeof EncryptionService, typeof AuthGuard, typeof AuthController, typeof AuthResolver, typeof AuthIdentityContextService];
1469
1778
 
1470
1779
  declare class PermissionRepository {
1471
1780
  db: TDb;
@@ -2215,73 +2524,68 @@ declare class UserController {
2215
2524
  removeRole(params: UserIdInParam): Promise<SanitizedUser>;
2216
2525
  }
2217
2526
 
2218
- type NewSetupSession = {
2219
- userId: string;
2220
- purpose: string;
2221
- tokenHash: string;
2222
- expiresAt: string;
2223
- };
2224
- declare class CredentialSetupRepository {
2225
- private db;
2226
- private schema;
2227
- private get sessions();
2228
- replaceActive(data: NewSetupSession): Promise<any>;
2229
- findActive(tokenHash: string, purpose: string): Promise<any>;
2230
- consume(tokenHash: string, purpose: string): Promise<any>;
2231
- revoke(tokenHash: string, purpose: string): Promise<any>;
2232
- deleteExpired(): Promise<any>;
2233
- }
2234
-
2235
- interface CredentialSetupOptions {
2236
- /** Stable server-owned purpose, for example `password-setup`. */
2237
- purpose: string;
2238
- /** HttpOnly browser-session cookie name (default: `najm.credential-setup`). */
2239
- cookieName?: string;
2240
- /** Session lifetime in milliseconds (default: 10 minutes, maximum: 24 hours). */
2241
- ttlMs?: number;
2242
- /** Cookie path (default: `/`). */
2243
- cookiePath?: string;
2244
- }
2245
- interface CredentialSetupSessionInfo {
2246
- userId: string;
2247
- purpose: string;
2248
- expiresAt: string;
2249
- }
2250
- interface CredentialSetupStarted {
2251
- purpose: string;
2252
- expiresAt: string;
2253
- }
2527
+ /**
2528
+ * Default replacement-password policy for the built-in `password` setup flow.
2529
+ *
2530
+ * Deliberately looser than `passwordField` on one axis: no uppercase
2531
+ * requirement. A first-login replacement is typed by someone who just proved
2532
+ * they hold the temporary credential, often on a phone keyboard, and forcing a
2533
+ * shift key there buys nothing that length and a digit do not.
2534
+ */
2535
+ declare const defaultCredentialSetupPasswordSchema: z.ZodString;
2536
+ /**
2537
+ * Edge shape only — bounds, not policy. The configured policy schema is applied
2538
+ * in the service, because it comes from `auth({ credentialSetup })`.
2539
+ */
2540
+ declare const credentialSetupChangeDto: z.ZodObject<{
2541
+ newPassword: z.ZodString;
2542
+ }, z.core.$strip>;
2543
+ type CredentialSetupChangeDto = z.infer<typeof credentialSetupChangeDto>;
2254
2544
 
2255
- declare class CredentialSetupService {
2256
- private readonly repository;
2257
- private readonly tokens;
2258
- private readonly authCookies;
2259
- private readonly cookies;
2260
- constructor(repository: CredentialSetupRepository, tokens: TokenService, authCookies: CookieManager, cookies: CookieService);
2261
- /**
2262
- * Revoke normal auth sessions and issue a single-purpose, short-lived,
2263
- * browser-session cookie. No access or refresh token is minted.
2264
- */
2265
- begin(userId: string, options: CredentialSetupOptions): Promise<CredentialSetupStarted>;
2266
- /** Validate and return the current active setup session without consuming it. */
2267
- require(options: CredentialSetupOptions): Promise<CredentialSetupSessionInfo>;
2268
- /**
2269
- * Atomically consume the setup session and execute the application-owned
2270
- * credential mutation in the same database transaction. If the callback
2271
- * fails, consumption rolls back and the browser may safely retry.
2272
- */
2273
- consume<T>(options: CredentialSetupOptions, complete: (session: CredentialSetupSessionInfo) => Promise<T> | T): Promise<T>;
2274
- cancel(options: CredentialSetupOptions): Promise<{
2545
+ /**
2546
+ * Standard endpoints for the built-in `password` setup flow. Authorization is
2547
+ * the opaque setup cookie, not a normal session — a user in setup has none.
2548
+ */
2549
+ declare class CredentialSetupController {
2550
+ private passwords;
2551
+ constructor(passwords: PasswordSetupService);
2552
+ status(): Promise<CredentialSetupPending>;
2553
+ change(body: CredentialSetupChangeDto): Promise<{
2554
+ changed: true;
2555
+ signInAgain: true;
2556
+ }>;
2557
+ cancel(): Promise<{
2275
2558
  cancelled: true;
2276
2559
  }>;
2277
- pruneExpired(): Promise<void>;
2278
- private resolveOptions;
2279
- private hashToken;
2280
- private setCookie;
2281
- private clearCookie;
2282
2560
  }
2283
2561
 
2284
- declare const CREDENTIAL_SETUP_MODULE: readonly [typeof CredentialSetupRepository, typeof CredentialSetupService];
2562
+ /**
2563
+ * Stable machine-readable codes for the credential-setup flow. Clients branch
2564
+ * on these; the accompanying message is localized and may change.
2565
+ */
2566
+ declare const CREDENTIAL_SETUP_CODES: {
2567
+ /** Login succeeded but the account must replace its credential first. */
2568
+ readonly REQUIRED: "AUTH_CREDENTIAL_SETUP_REQUIRED";
2569
+ /** No setup cookie was presented. */
2570
+ readonly SESSION_REQUIRED: "AUTH_CREDENTIAL_SETUP_SESSION_REQUIRED";
2571
+ /** The setup cookie is unknown, already used, or expired. */
2572
+ readonly SESSION_INVALID: "AUTH_CREDENTIAL_SETUP_SESSION_INVALID";
2573
+ /** Nothing was still required for this user and purpose. */
2574
+ readonly ALREADY_COMPLETED: "AUTH_CREDENTIAL_SETUP_ALREADY_COMPLETED";
2575
+ /** The replacement failed the configured policy. */
2576
+ readonly PASSWORD_REJECTED: "AUTH_CREDENTIAL_SETUP_PASSWORD_REJECTED";
2577
+ /** The replacement is the credential being replaced. */
2578
+ readonly SAME_PASSWORD: "AUTH_CREDENTIAL_SETUP_SAME_PASSWORD";
2579
+ /** OAuth cannot mint a session while a requirement is outstanding. */
2580
+ readonly OAUTH_BLOCKED: "oauth_credential_setup_required";
2581
+ };
2582
+ type CredentialSetupCode = (typeof CREDENTIAL_SETUP_CODES)[keyof typeof CREDENTIAL_SETUP_CODES];
2583
+ declare const credentialSetupError: (code: CredentialSetupCode, message: string, status?: number) => never;
2584
+
2585
+ /** Purposes are server-owned identifiers, never user input. */
2586
+ declare function normalizeSetupPurpose(purpose: string): string;
2587
+
2588
+ declare const CREDENTIAL_SETUP_MODULE: readonly [typeof CredentialSetupRepository, typeof CredentialSetupRequirementRepository, typeof CredentialSetupService, typeof CredentialSetupRequirementService, typeof PasswordSetupService, typeof CredentialSetupController];
2285
2589
 
2286
2590
  declare const USER_STATUS: readonly ["active", "inactive", "pending"];
2287
2591
  declare const TOKEN_STATUS: readonly ["active", "revoked", "expired"];
@@ -2424,4 +2728,4 @@ declare const authSeed: (config: AuthSeedConfig) => Record<string, SeedEntry>;
2424
2728
  */
2425
2729
  declare function seedAuthData(config: SeedAuthDataConfig): Promise<SeedAuthDataResult>;
2426
2730
 
2427
- 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, CREDENTIAL_SETUP_MODULE, 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 CredentialSetupOptions, CredentialSetupRepository, CredentialSetupService, type CredentialSetupSessionInfo, type CredentialSetupStarted, 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, authIdentityRateLimitKey, authSeed, avatarsPath, calculateAge, calculateYearsOfExperience, changePasswordDto, checkPermissionDto, clean, configureOwnership, confirmResetPasswordDto, createPermissionDto, createRoleDto, createTokenDto, createUserDto, defineRoles, emailParam, formatDate, getAuthLocale, getAvatarFile, inviteUserDto, isAdmin, isAdministrator, isAuth, isEmailIdentifier, isEmpty, isFile, isPath, join, languageParam, loginDto, normalizeAuthIdentifier, own, parseSchema, permissionIdParam, pickProps, refreshTokenDto, registerDto, resetPasswordDto, revokeTokenDto, roleIdParam, runAsUser, seedAuthData, tokenIdParam, updatePermissionDto, updateRoleDto, updateTokenDto, updateUserDto, userIdInParam, userIdParam, userListQuery, verifyTokenDto, where };
2731
+ 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, CREDENTIAL_SETUP_CODES, CREDENTIAL_SETUP_MODULE, 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 CredentialSetupChangeDto, type CredentialSetupCode, type CredentialSetupConfig, CredentialSetupController, type CredentialSetupOptions, type CredentialSetupPasswordOptions, type CredentialSetupPending, CredentialSetupRepository, CredentialSetupRequirementRepository, type CredentialSetupRequirementRow, CredentialSetupRequirementService, CredentialSetupService, type CredentialSetupSessionInfo, type CredentialSetupStarted, DEFAULT_CREDENTIAL_SETUP_COOKIE_NAME, DEFAULT_CREDENTIAL_SETUP_TTL_MS, type DefineRolesOptions, type EmailParam, EncryptionService, type GoogleOAuthConfig, IdentityConfig, type IdentityResolver, type InviteUserDto, type JwtConfig, type JwtPayload, type LanguageParam, type LoginDto, type LoginResult, NewPermission, NewRoleEntity, NewUser, type OAuthConfig, type OAuthProvider, Owned, type OwnedMethods, type OwnershipConfig, type OwnershipProvider, type OwnershipRule, OwnershipToken, type OwnershipTokenOptions, PASSWORD_SETUP_PURPOSE, PasswordSetupService, Permission, PermissionController, PermissionGuard, type PermissionIdParam, PermissionRepository, PermissionService, PermissionValidator, Policy, type ProvisionUserInput, type ProvisionUserWithPasswordInput, type ProvisionUserWithSetupInput, ROLES, ROLE_GROUPS, type RefreshTokenDto, type RegisterDto, type ResetPasswordDto, type ResolvedCredentialSetupConfig, ResolvedIdentityConfig, 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, TemporaryCredentialInput, 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, authIdentityRateLimitKey, authSeed, avatarsPath, calculateAge, calculateYearsOfExperience, changePasswordDto, checkPermissionDto, clean, configureOwnership, confirmResetPasswordDto, createIdentityResolver, createPermissionDto, createRoleDto, createTokenDto, createUserDto, credentialSetupChangeDto, credentialSetupError, defaultCredentialSetupPasswordSchema, defineRoles, emailParam, formatDate, getAuthLocale, getAvatarFile, inviteUserDto, isAdmin, isAdministrator, isAuth, isEmailIdentifier, isEmpty, isFile, isPath, join, languageParam, loginDto, normalizeAuthIdentifier, normalizeSetupPurpose, own, parseSchema, permissionIdParam, pickProps, refreshTokenDto, registerDto, resetPasswordDto, revokeTokenDto, roleIdParam, runAsUser, seedAuthData, tokenIdParam, updatePermissionDto, updateRoleDto, updateTokenDto, updateUserDto, userIdInParam, userIdParam, userListQuery, verifyTokenDto, where };