najm-auth 4.0.5 → 4.0.7

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.
@@ -1 +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';
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-Cu6xUMez.js';
@@ -31,7 +31,7 @@ var MOROCCAN_CIN_TEMPORARY_CREDENTIAL_KIND = "ma-cin";
31
31
  var MOROCCAN_CIN = /^[a-z]{1,3}\d{5,17}$/i;
32
32
  function isMoroccanCin(value) {
33
33
  const trimmed = value.trim();
34
- return trimmed.length >= 8 && trimmed.length <= 20 && MOROCCAN_CIN.test(trimmed);
34
+ return trimmed.length >= 7 && trimmed.length <= 20 && MOROCCAN_CIN.test(trimmed);
35
35
  }
36
36
  __name(isMoroccanCin, "isMoroccanCin");
37
37
  function normalizeMoroccanCin(value) {
package/dist/index.d.ts CHANGED
@@ -4,8 +4,8 @@ import { ValidationPluginConfig } from 'najm-validation';
4
4
  import { RateLimitPluginConfig, RateLimitKeyContext, TimeWindow } from 'najm-rate';
5
5
  import { CachePluginConfig, CacheService } from 'najm-cache';
6
6
  import { EmailPluginConfig, EmailService } from 'najm-email';
7
- import { R as ResolvedIdentityConfig, I as IdentityConfig, T as TemporaryCredentialInput } from './ma-sNHnUGLO.js';
8
- 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';
7
+ import { R as ResolvedIdentityConfig, I as IdentityConfig, T as TemporaryCredentialInput } from './ma-Cu6xUMez.js';
8
+ 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-Cu6xUMez.js';
9
9
  import { ZodType, z } from 'zod';
10
10
  import { I18nService } from 'najm-i18n';
11
11
  import { TDb, SeedEntry } from 'najm-database';
@@ -1227,7 +1227,15 @@ declare class UserService {
1227
1227
  create(data: Record<string, any>, options?: {
1228
1228
  validatePasswordStrength?: boolean;
1229
1229
  }): Promise<SanitizedUser>;
1230
- update(id: string, data: Record<string, any>): Promise<SanitizedUser>;
1230
+ /**
1231
+ * `options.validatePasswordStrength: false` is for a system-issued temporary
1232
+ * credential the user is durably required to replace — the same opt-out
1233
+ * `create()` already offers provisioning. A user-chosen password never takes
1234
+ * it.
1235
+ */
1236
+ update(id: string, data: Record<string, any>, options?: {
1237
+ validatePasswordStrength?: boolean;
1238
+ }): Promise<SanitizedUser>;
1231
1239
  delete(id: string): Promise<SanitizedUser>;
1232
1240
  deleteAll(): Promise<SanitizedUser[]>;
1233
1241
  getRoleName(id: string): Promise<string | null>;
@@ -1263,6 +1271,16 @@ interface ConsumedSetPasswordToken {
1263
1271
  userId: string;
1264
1272
  type: SetPasswordTokenType;
1265
1273
  }
1274
+ /**
1275
+ * A freshly minted one-time set-password token. `jti` identifies this exact
1276
+ * token so a caller that fails to deliver it can discard it again without
1277
+ * touching a newer one.
1278
+ */
1279
+ interface SetPasswordToken {
1280
+ token: string;
1281
+ userId: string;
1282
+ jti: string;
1283
+ }
1266
1284
  declare class TokenService {
1267
1285
  private tokenRepository;
1268
1286
  private cookieManager;
@@ -1481,19 +1499,24 @@ declare class TokenService {
1481
1499
  * Generate secure password reset token
1482
1500
  * Returns both the plain token (to send via email) and userId for identification
1483
1501
  */
1484
- generateResetToken(userId: string): Promise<{
1485
- token: string;
1486
- userId: string;
1487
- }>;
1502
+ generateResetToken(userId: string): Promise<SetPasswordToken>;
1488
1503
  /**
1489
1504
  * Generate secure account-invite token.
1490
1505
  * Longer expiry (3d) than reset because an invited user may not check
1491
1506
  * their email immediately. Consumed via the same reset-password endpoint.
1492
1507
  */
1493
- generateInviteToken(userId: string): Promise<{
1494
- token: string;
1495
- userId: string;
1496
- }>;
1508
+ generateInviteToken(userId: string): Promise<SetPasswordToken>;
1509
+ /**
1510
+ * Discard a set-password token this process just minted, identified by the
1511
+ * `jti` its generator returned. For the caller whose email send failed:
1512
+ * minting already superseded any earlier link for that user, so leaving the
1513
+ * fresh one live would keep a link alive that nobody received.
1514
+ *
1515
+ * Compare-and-delete, never a blind delete — a newer link minted in the
1516
+ * meantime must survive a late failure from an older send. Returns whether
1517
+ * this exact token was still the live one.
1518
+ */
1519
+ discardSetPasswordToken(userId: string, jti: string): Promise<boolean>;
1497
1520
  /**
1498
1521
  * Verify and CONSUME a password reset or invite token.
1499
1522
  *
@@ -1782,6 +1805,20 @@ type ProvisionUserWithPasswordInput = ProvisionUserInput & {
1782
1805
  temporaryCredential?: never;
1783
1806
  requireCredentialSetup?: never;
1784
1807
  };
1808
+ /** Outcome of an administrative reset to a system-issued temporary credential. */
1809
+ type TemporaryCredentialReset = {
1810
+ userId: string;
1811
+ purpose: typeof PASSWORD_SETUP_PURPOSE;
1812
+ temporaryCredentialKind: string;
1813
+ };
1814
+ /**
1815
+ * Outcome of an administrative mail-out. `emailSent` is what the provider
1816
+ * actually reported — never an assumption that sending succeeded.
1817
+ */
1818
+ type AdministrativeDelivery = {
1819
+ userId: string;
1820
+ emailSent: boolean;
1821
+ };
1785
1822
  /** Login answer: either a complete session, or a pending credential setup. */
1786
1823
  type LoginResult = (TokenPair & {
1787
1824
  nextStep: 'authenticated';
@@ -1820,6 +1857,14 @@ declare class AuthService {
1820
1857
  inviteUser(body: ProvisionUserInput): Promise<SanitizedUser & {
1821
1858
  emailSent: boolean;
1822
1859
  }>;
1860
+ /**
1861
+ * Mint an invite token and send the activation mail. Shared by first-time
1862
+ * invitation and re-invitation, so both rest on one token contract and one
1863
+ * template and neither can drift into an ad hoc message.
1864
+ *
1865
+ * Nothing here logs the token, the link, the message body, or the recipient.
1866
+ */
1867
+ private deliverInvitation;
1823
1868
  /**
1824
1869
  * Create a login for a person record. The branch is intentional and is the
1825
1870
  * single rule callers rely on:
@@ -1910,6 +1955,55 @@ declare class AuthService {
1910
1955
  resetPassword(token: string, newPassword: string): Promise<{
1911
1956
  message: string;
1912
1957
  }>;
1958
+ /**
1959
+ * Replace an existing account's stored credential with a system-issued
1960
+ * temporary one and durably require the holder to replace it at their next
1961
+ * login.
1962
+ *
1963
+ * The hash write and the durable requirement commit together, so no failure
1964
+ * can leave the temporary credential accepted with nothing forcing its
1965
+ * replacement, nor the requirement standing over an unchanged password.
1966
+ * Session revocation runs inside that same transaction: a cache or session
1967
+ * failure rolls the credential back rather than reporting a reset that a
1968
+ * still-live browser could sail past. No session is issued.
1969
+ *
1970
+ * Strength validation is deliberately skipped — the value is issued by the
1971
+ * system, not chosen by the user — but bcrypt's 72-byte boundary is not.
1972
+ */
1973
+ resetToTemporaryCredential(userId: string, credential: TemporaryCredentialInput): Promise<TemporaryCredentialReset>;
1974
+ /**
1975
+ * Send one password-reset link to an account selected by id. The recipient is
1976
+ * read from that account at command time, so neither an administrator nor a
1977
+ * stale client can redirect the link by supplying an address.
1978
+ *
1979
+ * Delivery is reported truthfully: unlike `forgotPassword` there is no email
1980
+ * enumeration to protect against, because the caller already knows the
1981
+ * account exists. Account status and email verification are left exactly as
1982
+ * they were, and requesting the link does not end the user's current session
1983
+ * — `resetPassword` revokes it when the new password is actually saved.
1984
+ *
1985
+ * Minting supersedes any earlier link for this user. A failed send discards
1986
+ * the fresh token too, so a failure never leaves a live link nobody received.
1987
+ */
1988
+ sendPasswordReset(userId: string): Promise<AdministrativeDelivery>;
1989
+ /**
1990
+ * Re-send the activation link for an account that is still pending.
1991
+ *
1992
+ * It creates no second user and no second profile — that is the whole reason
1993
+ * it exists beside `inviteUser`, which does create one. Only a `pending`
1994
+ * account qualifies: an active or inactive account is reset or reactivated,
1995
+ * never re-invited. Whether a given pending account is genuinely an invited
1996
+ * one rather than an application awaiting a decision is the caller's to
1997
+ * decide; this package cannot see an application.
1998
+ */
1999
+ resendInvitation(userId: string): Promise<AdministrativeDelivery>;
2000
+ /**
2001
+ * Discard a link that was minted but never delivered. A cache that cannot
2002
+ * consume atomically is reported rather than pretended away; the caller has
2003
+ * already been told the mail did not leave, so the truthful result stands
2004
+ * either way.
2005
+ */
2006
+ private discardUndeliveredToken;
1913
2007
  }
1914
2008
 
1915
2009
  /**
@@ -3073,4 +3167,4 @@ declare const authSeed: (config: AuthSeedConfig) => Record<string, SeedEntry>;
3073
3167
  */
3074
3168
  declare function seedAuthData(config: SeedAuthDataConfig): Promise<SeedAuthDataResult>;
3075
3169
 
3076
- export { AUTH_CONFIG, AUTH_CORE_MODULE, en as AUTH_EN, AUTH_LOCALES, AUTH_LOGIN_RATE_LIMIT_ENV, AUTH_MODULE, AUTH_PERMISSIONS, AUTH_ROLE, AUTH_SCHEMA, AUTH_SUPPORTED_LANGUAGES, AUTH_USER, type AssignPermissionDto, type AssignRoleDto, type AssignRoleParams, type AuthConfig, AuthController, AuthGuard, type AuthLoginRateLimitConfig, type AuthPluginConfig, AuthQueries, type AuthRateLimitEnvironment, 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, type ConsumedSetPasswordToken, 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_AUTH_LOGIN_RATE_LIMIT, DEFAULT_CREDENTIAL_SETUP_COOKIE_NAME, DEFAULT_CREDENTIAL_SETUP_TTL_MS, type DefineRolesOptions, type EmailParam, EncryptionService, type GitHubOAuthConfig, 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, PUBLIC_REGISTRATION_MODULE, PasswordSetupService, Permission, PermissionController, PermissionGuard, type PermissionIdParam, PermissionRepository, PermissionService, PermissionValidator, Policy, type ProvisionUserInput, type ProvisionUserWithPasswordInput, type ProvisionUserWithSetupInput, ROLES, ROLE_GROUPS, type RefreshTokenDto, type RegisterDto, RegistrationController, 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, SessionInvalidationService, type SetPasswordTokenType, 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, authEmailRateLimitKey, 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, resolveAuthLoginRateLimitConfig, revokeTokenDto, roleIdParam, runAsUser, seedAuthData, tokenIdParam, updatePermissionDto, updateRoleDto, updateTokenDto, updateUserDto, userIdInParam, userIdParam, userListQuery, verifyTokenDto, where };
3170
+ export { AUTH_CONFIG, AUTH_CORE_MODULE, en as AUTH_EN, AUTH_LOCALES, AUTH_LOGIN_RATE_LIMIT_ENV, AUTH_MODULE, AUTH_PERMISSIONS, AUTH_ROLE, AUTH_SCHEMA, AUTH_SUPPORTED_LANGUAGES, AUTH_USER, type AdministrativeDelivery, type AssignPermissionDto, type AssignRoleDto, type AssignRoleParams, type AuthConfig, AuthController, AuthGuard, type AuthLoginRateLimitConfig, type AuthPluginConfig, AuthQueries, type AuthRateLimitEnvironment, 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, type ConsumedSetPasswordToken, 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_AUTH_LOGIN_RATE_LIMIT, DEFAULT_CREDENTIAL_SETUP_COOKIE_NAME, DEFAULT_CREDENTIAL_SETUP_TTL_MS, type DefineRolesOptions, type EmailParam, EncryptionService, type GitHubOAuthConfig, 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, PUBLIC_REGISTRATION_MODULE, PasswordSetupService, Permission, PermissionController, PermissionGuard, type PermissionIdParam, PermissionRepository, PermissionService, PermissionValidator, Policy, type ProvisionUserInput, type ProvisionUserWithPasswordInput, type ProvisionUserWithSetupInput, ROLES, ROLE_GROUPS, type RefreshTokenDto, type RegisterDto, RegistrationController, 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, SessionInvalidationService, type SetPasswordToken, type SetPasswordTokenType, TOKEN_STATUS, TOKEN_TYPE, TemporaryCredentialInput, type TemporaryCredentialReset, 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, authEmailRateLimitKey, 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, resolveAuthLoginRateLimitConfig, revokeTokenDto, roleIdParam, runAsUser, seedAuthData, tokenIdParam, updatePermissionDto, updateRoleDto, updateTokenDto, updateUserDto, userIdInParam, userIdParam, userListQuery, verifyTokenDto, where };
package/dist/index.js CHANGED
@@ -1916,12 +1916,22 @@ var UserService = class UserService2 {
1916
1916
  const newUser = await this.userRepository.create(userDetails);
1917
1917
  return this.sanitizeUser(newUser);
1918
1918
  }
1919
- async update(id, data) {
1919
+ /**
1920
+ * `options.validatePasswordStrength: false` is for a system-issued temporary
1921
+ * credential the user is durably required to replace — the same opt-out
1922
+ * `create()` already offers provisioning. A user-chosen password never takes
1923
+ * it.
1924
+ */
1925
+ async update(id, data, options = {}) {
1920
1926
  const { password, image } = data;
1921
1927
  await this.userValidator.checkEmailUnique(data.email, id);
1922
1928
  let hashedPassword;
1923
1929
  if (password) {
1924
- this.userValidator.validatePasswordStrength(password);
1930
+ if (options.validatePasswordStrength === false) {
1931
+ this.userValidator.validatePasswordLength(password);
1932
+ } else {
1933
+ this.userValidator.validatePasswordStrength(password);
1934
+ }
1925
1935
  hashedPassword = await this.encryptionService.hashPassword(password);
1926
1936
  }
1927
1937
  const updateData = {
@@ -2711,7 +2721,7 @@ var TokenService = class TokenService2 {
2711
2721
  expiresIn
2712
2722
  });
2713
2723
  await this.cache.set(`${this.resetTokenPrefix}${userId}`, jti, timestring3(expiresIn, "ms"));
2714
- return { token, userId };
2724
+ return { token, userId, jti };
2715
2725
  }
2716
2726
  /**
2717
2727
  * Generate secure password reset token
@@ -2728,6 +2738,23 @@ var TokenService = class TokenService2 {
2728
2738
  async generateInviteToken(userId) {
2729
2739
  return this.generateSetPasswordToken(userId, "invite", "3d");
2730
2740
  }
2741
+ /**
2742
+ * Discard a set-password token this process just minted, identified by the
2743
+ * `jti` its generator returned. For the caller whose email send failed:
2744
+ * minting already superseded any earlier link for that user, so leaving the
2745
+ * fresh one live would keep a link alive that nobody received.
2746
+ *
2747
+ * Compare-and-delete, never a blind delete — a newer link minted in the
2748
+ * meantime must survive a late failure from an older send. Returns whether
2749
+ * this exact token was still the live one.
2750
+ */
2751
+ async discardSetPasswordToken(userId, jti) {
2752
+ const consume = this.cache.compareAndDelete;
2753
+ if (typeof consume !== "function") {
2754
+ Err7.invalidOperation("Discarding a set-password token requires a cache with atomic compare-and-delete");
2755
+ }
2756
+ return consume.call(this.cache, `${this.resetTokenPrefix}${userId}`, jti);
2757
+ }
2731
2758
  /**
2732
2759
  * Verify and CONSUME a password reset or invite token.
2733
2760
  *
@@ -2813,7 +2840,7 @@ var MOROCCAN_CIN_TEMPORARY_CREDENTIAL_KIND = "ma-cin";
2813
2840
  var MOROCCAN_CIN = /^[a-z]{1,3}\d{5,17}$/i;
2814
2841
  function isMoroccanCin(value) {
2815
2842
  const trimmed = value.trim();
2816
- return trimmed.length >= 8 && trimmed.length <= 20 && MOROCCAN_CIN.test(trimmed);
2843
+ return trimmed.length >= 7 && trimmed.length <= 20 && MOROCCAN_CIN.test(trimmed);
2817
2844
  }
2818
2845
  __name(isMoroccanCin, "isMoroccanCin");
2819
2846
  function normalizeMoroccanCin(value) {
@@ -3444,6 +3471,7 @@ var _h2;
3444
3471
  var _j2;
3445
3472
  var _k;
3446
3473
  var _l;
3474
+ var _m;
3447
3475
  var AuthService = class AuthService2 {
3448
3476
  static {
3449
3477
  __name(this, "AuthService");
@@ -3522,16 +3550,27 @@ var AuthService = class AuthService2 {
3522
3550
  status: body.status ?? "active",
3523
3551
  emailVerified: false
3524
3552
  });
3525
- const { token } = await this.tokenService.generateInviteToken(user.id);
3553
+ const { emailSent } = await this.deliverInvitation(user.id, body.email, user.name, body.role);
3554
+ return { ...user, emailSent };
3555
+ }
3556
+ /**
3557
+ * Mint an invite token and send the activation mail. Shared by first-time
3558
+ * invitation and re-invitation, so both rest on one token contract and one
3559
+ * template and neither can drift into an ad hoc message.
3560
+ *
3561
+ * Nothing here logs the token, the link, the message body, or the recipient.
3562
+ */
3563
+ async deliverInvitation(userId, email2, userName, role) {
3564
+ const { token, jti } = await this.tokenService.generateInviteToken(userId);
3526
3565
  const inviteLink = `${this.config.frontendUrl}/reset-password?token=${token}`;
3527
- const accountType = body.role?.trim().toLowerCase();
3566
+ const accountType = role?.trim().toLowerCase() || void 0;
3528
3567
  const accountLabel = accountType ? `${accountType} account` : "account";
3529
3568
  let emailSent = false;
3530
3569
  try {
3531
3570
  const logo = this.config.accountInviteLogo;
3532
3571
  const logoCid = logo ? "najm-account-invite-logo" : void 0;
3533
3572
  const result = await this.emailService.send({
3534
- to: body.email,
3573
+ to: email2,
3535
3574
  subject: this.t("emails.accountInvite.subject", {
3536
3575
  accountLabel,
3537
3576
  appName: this.config.appName
@@ -3542,7 +3581,7 @@ var AuthService = class AuthService2 {
3542
3581
  inviteLink,
3543
3582
  logoAlt: logo?.alt,
3544
3583
  logoSrc: logoCid ? `cid:${logoCid}` : void 0,
3545
- userName: user.name || body.email
3584
+ userName: userName || email2
3546
3585
  }),
3547
3586
  attachments: logo ? [{
3548
3587
  filename: logo.filename,
@@ -3555,9 +3594,9 @@ var AuthService = class AuthService2 {
3555
3594
  });
3556
3595
  emailSent = result.success;
3557
3596
  } catch (error) {
3558
- this.logger.warn("Account invite email failed", { email: body.email, error });
3597
+ this.logger.warn("Account invite email failed", { userId, error });
3559
3598
  }
3560
- return { ...user, emailSent };
3599
+ return { emailSent, jti };
3561
3600
  }
3562
3601
  /**
3563
3602
  * Create a login for a person record. The branch is intentional and is the
@@ -3904,6 +3943,130 @@ var AuthService = class AuthService2 {
3904
3943
  this.cookieManager.clearSessionCookie();
3905
3944
  return { message: this.t("success.passwordReset") };
3906
3945
  }
3946
+ // ==========================================================================
3947
+ // Administrative recovery for an account that already exists
3948
+ //
3949
+ // Three operations an application's own admin surface composes. Each is
3950
+ // bound to a user id, never to a submitted email; none creates a user,
3951
+ // issues a session, or returns a credential, token, or link. Who may call
3952
+ // them, which targets are eligible, how often, and what is audited belong to
3953
+ // the application — this package owns only the credential, token, and
3954
+ // session mechanics underneath.
3955
+ // ==========================================================================
3956
+ /**
3957
+ * Replace an existing account's stored credential with a system-issued
3958
+ * temporary one and durably require the holder to replace it at their next
3959
+ * login.
3960
+ *
3961
+ * The hash write and the durable requirement commit together, so no failure
3962
+ * can leave the temporary credential accepted with nothing forcing its
3963
+ * replacement, nor the requirement standing over an unchanged password.
3964
+ * Session revocation runs inside that same transaction: a cache or session
3965
+ * failure rolls the credential back rather than reporting a reset that a
3966
+ * still-live browser could sail past. No session is issued.
3967
+ *
3968
+ * Strength validation is deliberately skipped — the value is issued by the
3969
+ * system, not chosen by the user — but bcrypt's 72-byte boundary is not.
3970
+ */
3971
+ async resetToTemporaryCredential(userId, credential) {
3972
+ if (!this.credentialSetupRequirements) {
3973
+ Err12.invalidOperation("Credential setup is unavailable: CredentialSetupRequirementService is not registered");
3974
+ }
3975
+ const user = await this.userService.getById(userId);
3976
+ const temporary = toTemporaryCredential(credential);
3977
+ const kind = resolveTemporaryCredentialKind(temporary.kind);
3978
+ if (kind.isTemporaryShape && !kind.isTemporaryShape(temporary.value)) {
3979
+ Err12(`Invalid temporary credential for kind '${kind.name}'`, 400);
3980
+ }
3981
+ const password = kind.normalize(temporary.value);
3982
+ if (!password?.trim()) {
3983
+ Err12("resetToTemporaryCredential requires a non-empty temporaryCredential", 400);
3984
+ }
3985
+ await this.userService.update(user.id, { password }, { validatePasswordStrength: false });
3986
+ await this.credentialSetupRequirements.markRequired(user.id, PASSWORD_SETUP_PURPOSE, {
3987
+ temporaryCredentialKind: kind.name
3988
+ });
3989
+ return {
3990
+ userId: user.id,
3991
+ purpose: PASSWORD_SETUP_PURPOSE,
3992
+ temporaryCredentialKind: kind.name
3993
+ };
3994
+ }
3995
+ /**
3996
+ * Send one password-reset link to an account selected by id. The recipient is
3997
+ * read from that account at command time, so neither an administrator nor a
3998
+ * stale client can redirect the link by supplying an address.
3999
+ *
4000
+ * Delivery is reported truthfully: unlike `forgotPassword` there is no email
4001
+ * enumeration to protect against, because the caller already knows the
4002
+ * account exists. Account status and email verification are left exactly as
4003
+ * they were, and requesting the link does not end the user's current session
4004
+ * — `resetPassword` revokes it when the new password is actually saved.
4005
+ *
4006
+ * Minting supersedes any earlier link for this user. A failed send discards
4007
+ * the fresh token too, so a failure never leaves a live link nobody received.
4008
+ */
4009
+ async sendPasswordReset(userId) {
4010
+ const user = await this.userService.getById(userId);
4011
+ const email2 = typeof user.email === "string" ? user.email.trim() : "";
4012
+ if (!email2) {
4013
+ Err12("This account has no email address to send a password reset to", 409);
4014
+ }
4015
+ const { token, jti } = await this.tokenService.generateResetToken(user.id);
4016
+ const resetLink = `${this.config.frontendUrl}/reset-password?token=${token}`;
4017
+ let emailSent = false;
4018
+ try {
4019
+ const result = await this.emailService.sendHtml(email2, this.t("emails.passwordReset.subject"), passwordResetTemplate({
4020
+ resetLink,
4021
+ userName: user.name || email2
4022
+ }));
4023
+ emailSent = result.success;
4024
+ } catch (error) {
4025
+ this.logger.warn("Administrative password reset email failed", { userId: user.id, error });
4026
+ }
4027
+ if (!emailSent) {
4028
+ await this.discardUndeliveredToken(user.id, jti);
4029
+ }
4030
+ return { userId: user.id, emailSent };
4031
+ }
4032
+ /**
4033
+ * Re-send the activation link for an account that is still pending.
4034
+ *
4035
+ * It creates no second user and no second profile — that is the whole reason
4036
+ * it exists beside `inviteUser`, which does create one. Only a `pending`
4037
+ * account qualifies: an active or inactive account is reset or reactivated,
4038
+ * never re-invited. Whether a given pending account is genuinely an invited
4039
+ * one rather than an application awaiting a decision is the caller's to
4040
+ * decide; this package cannot see an application.
4041
+ */
4042
+ async resendInvitation(userId) {
4043
+ const user = await this.userService.getById(userId);
4044
+ if (user.status !== "pending") {
4045
+ Err12("Only a pending account can be re-invited", 409);
4046
+ }
4047
+ const email2 = typeof user.email === "string" ? user.email.trim() : "";
4048
+ if (!email2) {
4049
+ Err12("This account has no email address to send an invitation to", 409);
4050
+ }
4051
+ const { emailSent, jti } = await this.deliverInvitation(user.id, email2, user.name, user.role);
4052
+ if (!emailSent) {
4053
+ await this.discardUndeliveredToken(user.id, jti);
4054
+ }
4055
+ return { userId: user.id, emailSent };
4056
+ }
4057
+ /**
4058
+ * Discard a link that was minted but never delivered. A cache that cannot
4059
+ * consume atomically is reported rather than pretended away; the caller has
4060
+ * already been told the mail did not leave, so the truthful result stands
4061
+ * either way.
4062
+ */
4063
+ async discardUndeliveredToken(userId, jti) {
4064
+ try {
4065
+ await this.tokenService.discardSetPasswordToken(userId, jti);
4066
+ } catch (error) {
4067
+ this.logger.warn("Undelivered set-password token could not be discarded", { userId, error });
4068
+ }
4069
+ }
3907
4070
  };
3908
4071
  __decorate18([
3909
4072
  Inject12(AUTH_CONFIG),
@@ -3923,6 +4086,12 @@ __decorate18([
3923
4086
  __metadata18("design:paramtypes", [Object]),
3924
4087
  __metadata18("design:returntype", typeof (_l = typeof Promise !== "undefined" && Promise) === "function" ? _l : Object)
3925
4088
  ], AuthService.prototype, "provisionWithCredentialSetup", null);
4089
+ __decorate18([
4090
+ Transaction4(),
4091
+ __metadata18("design:type", Function),
4092
+ __metadata18("design:paramtypes", [String, Object]),
4093
+ __metadata18("design:returntype", typeof (_m = typeof Promise !== "undefined" && Promise) === "function" ? _m : Object)
4094
+ ], AuthService.prototype, "resetToTemporaryCredential", null);
3926
4095
  AuthService = __decorate18([
3927
4096
  Injectable12(),
3928
4097
  __metadata18("design:paramtypes", [typeof (_a12 = typeof TokenService !== "undefined" && TokenService) === "function" ? _a12 : Object, typeof (_b10 = typeof UserService !== "undefined" && UserService) === "function" ? _b10 : Object, typeof (_c7 = typeof UserValidator !== "undefined" && UserValidator) === "function" ? _c7 : Object, typeof (_d6 = typeof EncryptionService !== "undefined" && EncryptionService) === "function" ? _d6 : Object, typeof (_e5 = typeof CookieManager !== "undefined" && CookieManager) === "function" ? _e5 : Object, typeof (_f4 = typeof I18nService2 !== "undefined" && I18nService2) === "function" ? _f4 : Object, typeof (_g3 = typeof EmailService !== "undefined" && EmailService) === "function" ? _g3 : Object, typeof (_h2 = typeof AuthSessionService !== "undefined" && AuthSessionService) === "function" ? _h2 : Object, typeof (_j2 = typeof CredentialSetupRequirementService !== "undefined" && CredentialSetupRequirementService) === "function" ? _j2 : Object, typeof (_k = typeof PasswordSetupService !== "undefined" && PasswordSetupService) === "function" ? _k : Object])
@@ -66,7 +66,12 @@ interface TemporaryCredentialKind {
66
66
  /** Default kind: byte-for-byte, case-sensitive, no transformation. */
67
67
  declare const EXACT_TEMPORARY_CREDENTIAL_KIND = "exact";
68
68
  declare const MOROCCAN_CIN_TEMPORARY_CREDENTIAL_KIND = "ma-cin";
69
- /** A Moroccan CIN is 1–3 letters followed by digits, 8–20 characters overall. */
69
+ /**
70
+ * A Moroccan CIN is 1–3 letters followed by digits, 7–20 characters overall.
71
+ * The floor is 7, not 8: the issued formats are one or two letters followed
72
+ * by five or six digits, so `BB46123` and `A123456` are real cards that an
73
+ * 8 floor turned away.
74
+ */
70
75
  declare function isMoroccanCin(value: string): boolean;
71
76
  /**
72
77
  * People type their CIN in either case. Anything that is not a CIN is left
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "najm-auth",
3
- "version": "4.0.5",
3
+ "version": "4.0.7",
4
4
  "description": "Authentication and authorization library for najm framework",
5
5
  "type": "module",
6
6
  "files": [