najm-auth 4.0.6 → 4.0.8

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
@@ -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,27 @@ 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
+ * `undeliveredLinkLive` is the one case a caller cannot infer: the mail did not
1819
+ * leave AND the link minted for it could not be taken out of circulation, so a
1820
+ * usable link exists that no one received. It is false whenever the mail left
1821
+ * — that link is live on purpose — and false when an undelivered one was
1822
+ * successfully discarded.
1823
+ */
1824
+ type AdministrativeDelivery = {
1825
+ userId: string;
1826
+ emailSent: boolean;
1827
+ undeliveredLinkLive: boolean;
1828
+ };
1785
1829
  /** Login answer: either a complete session, or a pending credential setup. */
1786
1830
  type LoginResult = (TokenPair & {
1787
1831
  nextStep: 'authenticated';
@@ -1820,6 +1864,14 @@ declare class AuthService {
1820
1864
  inviteUser(body: ProvisionUserInput): Promise<SanitizedUser & {
1821
1865
  emailSent: boolean;
1822
1866
  }>;
1867
+ /**
1868
+ * Mint an invite token and send the activation mail. Shared by first-time
1869
+ * invitation and re-invitation, so both rest on one token contract and one
1870
+ * template and neither can drift into an ad hoc message.
1871
+ *
1872
+ * Nothing here logs the token, the link, the message body, or the recipient.
1873
+ */
1874
+ private deliverInvitation;
1823
1875
  /**
1824
1876
  * Create a login for a person record. The branch is intentional and is the
1825
1877
  * single rule callers rely on:
@@ -1910,6 +1962,62 @@ declare class AuthService {
1910
1962
  resetPassword(token: string, newPassword: string): Promise<{
1911
1963
  message: string;
1912
1964
  }>;
1965
+ /**
1966
+ * Replace an existing account's stored credential with a system-issued
1967
+ * temporary one and durably require the holder to replace it at their next
1968
+ * login.
1969
+ *
1970
+ * The hash write and the durable requirement commit together, so no failure
1971
+ * can leave the temporary credential accepted with nothing forcing its
1972
+ * replacement, nor the requirement standing over an unchanged password.
1973
+ * Session revocation runs inside that same transaction: a cache or session
1974
+ * failure rolls the credential back rather than reporting a reset that a
1975
+ * still-live browser could sail past. No session is issued.
1976
+ *
1977
+ * Strength validation is deliberately skipped — the value is issued by the
1978
+ * system, not chosen by the user — but bcrypt's 72-byte boundary is not.
1979
+ */
1980
+ resetToTemporaryCredential(userId: string, credential: TemporaryCredentialInput): Promise<TemporaryCredentialReset>;
1981
+ /**
1982
+ * Send one password-reset link to an account selected by id. The recipient is
1983
+ * read from that account at command time, so neither an administrator nor a
1984
+ * stale client can redirect the link by supplying an address.
1985
+ *
1986
+ * Delivery is reported truthfully: unlike `forgotPassword` there is no email
1987
+ * enumeration to protect against, because the caller already knows the
1988
+ * account exists. Account status and email verification are left exactly as
1989
+ * they were, and requesting the link does not end the user's current session
1990
+ * — `resetPassword` revokes it when the new password is actually saved.
1991
+ *
1992
+ * Minting supersedes any earlier link for this user. A failed send discards
1993
+ * the fresh token too, so a failure never leaves a live link nobody received.
1994
+ */
1995
+ sendPasswordReset(userId: string): Promise<AdministrativeDelivery>;
1996
+ /**
1997
+ * Re-send the activation link for an account that is still pending.
1998
+ *
1999
+ * It creates no second user and no second profile — that is the whole reason
2000
+ * it exists beside `inviteUser`, which does create one. Only a `pending`
2001
+ * account qualifies: an active or inactive account is reset or reactivated,
2002
+ * never re-invited. Whether a given pending account is genuinely an invited
2003
+ * one rather than an application awaiting a decision is the caller's to
2004
+ * decide; this package cannot see an application.
2005
+ */
2006
+ resendInvitation(userId: string): Promise<AdministrativeDelivery>;
2007
+ /**
2008
+ * Take a link that was minted but never delivered out of circulation, and
2009
+ * answer whether it is really gone.
2010
+ *
2011
+ * A `false` from the store is not a failure: compare-and-delete only refuses
2012
+ * when the stored jti is no longer this one, which means a newer mint already
2013
+ * superseded this link and it can no longer be consumed either way. A throw
2014
+ * is the failure — the store was unreachable, the jti it holds is still ours,
2015
+ * and a usable link now exists that nobody received. That does not change
2016
+ * what the caller is told about delivery, but it must not be swallowed: it is
2017
+ * logged as an error and reported up, so the result stays truthful about more
2018
+ * than the mail.
2019
+ */
2020
+ private discardUndeliveredToken;
1913
2021
  }
1914
2022
 
1915
2023
  /**
@@ -3073,4 +3181,4 @@ declare const authSeed: (config: AuthSeedConfig) => Record<string, SeedEntry>;
3073
3181
  */
3074
3182
  declare function seedAuthData(config: SeedAuthDataConfig): Promise<SeedAuthDataResult>;
3075
3183
 
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 };
3184
+ 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
  *
@@ -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,135 @@ 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
+ const undeliveredLinkLive = emailSent ? false : !await this.discardUndeliveredToken(user.id, jti);
4028
+ return { userId: user.id, emailSent, undeliveredLinkLive };
4029
+ }
4030
+ /**
4031
+ * Re-send the activation link for an account that is still pending.
4032
+ *
4033
+ * It creates no second user and no second profile — that is the whole reason
4034
+ * it exists beside `inviteUser`, which does create one. Only a `pending`
4035
+ * account qualifies: an active or inactive account is reset or reactivated,
4036
+ * never re-invited. Whether a given pending account is genuinely an invited
4037
+ * one rather than an application awaiting a decision is the caller's to
4038
+ * decide; this package cannot see an application.
4039
+ */
4040
+ async resendInvitation(userId) {
4041
+ const user = await this.userService.getById(userId);
4042
+ if (user.status !== "pending") {
4043
+ Err12("Only a pending account can be re-invited", 409);
4044
+ }
4045
+ const email2 = typeof user.email === "string" ? user.email.trim() : "";
4046
+ if (!email2) {
4047
+ Err12("This account has no email address to send an invitation to", 409);
4048
+ }
4049
+ const { emailSent, jti } = await this.deliverInvitation(user.id, email2, user.name, user.role);
4050
+ const undeliveredLinkLive = emailSent ? false : !await this.discardUndeliveredToken(user.id, jti);
4051
+ return { userId: user.id, emailSent, undeliveredLinkLive };
4052
+ }
4053
+ /**
4054
+ * Take a link that was minted but never delivered out of circulation, and
4055
+ * answer whether it is really gone.
4056
+ *
4057
+ * A `false` from the store is not a failure: compare-and-delete only refuses
4058
+ * when the stored jti is no longer this one, which means a newer mint already
4059
+ * superseded this link and it can no longer be consumed either way. A throw
4060
+ * is the failure — the store was unreachable, the jti it holds is still ours,
4061
+ * and a usable link now exists that nobody received. That does not change
4062
+ * what the caller is told about delivery, but it must not be swallowed: it is
4063
+ * logged as an error and reported up, so the result stays truthful about more
4064
+ * than the mail.
4065
+ */
4066
+ async discardUndeliveredToken(userId, jti) {
4067
+ try {
4068
+ await this.tokenService.discardSetPasswordToken(userId, jti);
4069
+ return true;
4070
+ } catch (error) {
4071
+ this.logger.error("Undelivered set-password token is still live", { userId, error });
4072
+ return false;
4073
+ }
4074
+ }
3907
4075
  };
3908
4076
  __decorate18([
3909
4077
  Inject12(AUTH_CONFIG),
@@ -3923,6 +4091,12 @@ __decorate18([
3923
4091
  __metadata18("design:paramtypes", [Object]),
3924
4092
  __metadata18("design:returntype", typeof (_l = typeof Promise !== "undefined" && Promise) === "function" ? _l : Object)
3925
4093
  ], AuthService.prototype, "provisionWithCredentialSetup", null);
4094
+ __decorate18([
4095
+ Transaction4(),
4096
+ __metadata18("design:type", Function),
4097
+ __metadata18("design:paramtypes", [String, Object]),
4098
+ __metadata18("design:returntype", typeof (_m = typeof Promise !== "undefined" && Promise) === "function" ? _m : Object)
4099
+ ], AuthService.prototype, "resetToTemporaryCredential", null);
3926
4100
  AuthService = __decorate18([
3927
4101
  Injectable12(),
3928
4102
  __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])
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "najm-auth",
3
- "version": "4.0.6",
3
+ "version": "4.0.8",
4
4
  "description": "Authentication and authorization library for najm framework",
5
5
  "type": "module",
6
6
  "files": [