najm-auth 4.0.0 → 4.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -138,8 +138,9 @@ auth({
138
138
  publicRegistration?: boolean // Default: true; mounts POST /auth/register
139
139
  bcryptRounds?: number // Default: 10 (valid: 4-31)
140
140
 
141
- // Frontend
142
- frontendUrl?: string // Password reset link base URL
141
+ // Frontend
142
+ frontendUrl?: string // Password reset link base URL
143
+ appName?: string // Security email brand (default: 'Your app')
143
144
 
144
145
  // Login identifier normalization (see "Identity presets")
145
146
  identity?: {
@@ -1119,15 +1120,28 @@ errors cannot change the authentication result.
1119
1120
  ### Password Reset Tokens
1120
1121
 
1121
1122
  Reset and invite links are signed JWTs whose `jti` is stored in the configured
1122
- cache with the same expiry. `verifyResetToken()` atomically compares and
1123
- deletes that value, so exactly one concurrent caller can consume a link and a
1124
- stale link cannot delete the value for a newer one.
1123
+ cache with the same expiry. `consumeSetPasswordToken()` atomically compares and
1124
+ deletes that value while preserving whether the token was a reset or invite;
1125
+ the backward-compatible `verifyResetToken()` returns only the user id. Exactly
1126
+ one concurrent caller can consume a link, and a stale link cannot delete the
1127
+ value for a newer one.
1125
1128
 
1126
1129
  `AuthService.resetPassword()` validates the replacement password before
1127
1130
  consumption. Once consumed, a token stays consumed even if the later user
1128
1131
  mutation fails; restoring it would make the link replayable, so the user must
1129
1132
  request a new one.
1130
1133
 
1134
+ Accepting an account invitation also marks the destination email verified and
1135
+ activates the account when its status is `pending`. An ordinary password reset
1136
+ changes neither verification nor lifecycle status, and an explicitly inactive
1137
+ invited account remains inactive.
1138
+
1139
+ Set `appName` on `auth()` to brand the invitation subject and email card. The
1140
+ provisioned role is presented as the account type, so a sponsor invitation can
1141
+ say “Activate your sponsor account” without application-owned HTML. The shared
1142
+ template uses inline critical styles for Gmail and keeps the raw token URL out
1143
+ of visible fallback copy.
1144
+
1131
1145
  The built-in memory and Redis drivers implement the required atomic primitive.
1132
1146
  A custom cache driver may omit `compareAndDelete()` for compatibility with
1133
1147
  unrelated cache usage, but reset and invite consumption then fails closed. Do
package/dist/index.d.ts CHANGED
@@ -198,6 +198,8 @@ interface AuthConfig {
198
198
  defaultRole: string | null;
199
199
  /** Frontend URL for password reset links (default: 'http://localhost:3000') */
200
200
  frontendUrl: string;
201
+ /** Product name used in security email subjects and templates. */
202
+ appName: string;
201
203
  /** Registration mode: 'active' auto-activates, 'pending' requires admin approval (default: 'active') */
202
204
  registrationMode: 'active' | 'pending';
203
205
  /** Whether the unauthenticated POST /auth/register route is mounted. */
@@ -265,6 +267,8 @@ type AuthPluginConfig = {
265
267
  defaultRole?: string | null;
266
268
  /** Frontend URL for password reset links. Falls back to FRONTEND_URL env var, then 'http://localhost:3000' */
267
269
  frontendUrl?: string;
270
+ /** Product name used in account invitation emails (default: 'Your app'). */
271
+ appName?: string;
268
272
  /** Registration mode: 'active' auto-activates new users, 'pending' requires admin approval (default: 'active') */
269
273
  registrationMode?: 'active' | 'pending';
270
274
  /**
@@ -432,7 +436,7 @@ var auth = {
432
436
  subject: "Reset your password"
433
437
  },
434
438
  accountInvite: {
435
- subject: "You've been invited — set up your account"
439
+ subject: "{{appName}}: activate your {{accountLabel}}"
436
440
  }
437
441
  }
438
442
  };
@@ -1233,6 +1237,11 @@ declare class CredentialSetupRequirementRepository {
1233
1237
  complete(userId: string, purpose: string): Promise<CredentialSetupRequirementRow | undefined>;
1234
1238
  }
1235
1239
 
1240
+ type SetPasswordTokenType = 'reset' | 'invite';
1241
+ interface ConsumedSetPasswordToken {
1242
+ userId: string;
1243
+ type: SetPasswordTokenType;
1244
+ }
1236
1245
  declare class TokenService {
1237
1246
  private tokenRepository;
1238
1247
  private cookieManager;
@@ -1480,6 +1489,12 @@ declare class TokenService {
1480
1489
  * request that then failed validation would cost the user their link for
1481
1490
  * nothing.
1482
1491
  */
1492
+ consumeSetPasswordToken(token: string): Promise<ConsumedSetPasswordToken>;
1493
+ /**
1494
+ * Backward-compatible user-id-only reset/invite token consumption.
1495
+ * Prefer `consumeSetPasswordToken()` when the caller must distinguish an
1496
+ * account invitation from an ordinary password reset.
1497
+ */
1483
1498
  verifyResetToken(token: string): Promise<string>;
1484
1499
  private getUserSessionVersion;
1485
1500
  }
@@ -3037,4 +3052,4 @@ declare const authSeed: (config: AuthSeedConfig) => Record<string, SeedEntry>;
3037
3052
  */
3038
3053
  declare function seedAuthData(config: SeedAuthDataConfig): Promise<SeedAuthDataResult>;
3039
3054
 
3040
- 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, 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, 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 };
3055
+ 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 };
package/dist/index.js CHANGED
@@ -2708,7 +2708,7 @@ var TokenService = class TokenService2 {
2708
2708
  * request that then failed validation would cost the user their link for
2709
2709
  * nothing.
2710
2710
  */
2711
- async verifyResetToken(token) {
2711
+ async consumeSetPasswordToken(token) {
2712
2712
  let decoded;
2713
2713
  try {
2714
2714
  decoded = jwt.verify(token, this.config.jwt.refreshSecret);
@@ -2726,7 +2726,18 @@ var TokenService = class TokenService2 {
2726
2726
  if (!await consume.call(this.cache, key, decoded.jti)) {
2727
2727
  Err7(this.t("errors.invalidResetToken"));
2728
2728
  }
2729
- return decoded.userId;
2729
+ return {
2730
+ userId: decoded.userId,
2731
+ type: decoded.type
2732
+ };
2733
+ }
2734
+ /**
2735
+ * Backward-compatible user-id-only reset/invite token consumption.
2736
+ * Prefer `consumeSetPasswordToken()` when the caller must distinguish an
2737
+ * account invitation from an ordinary password reset.
2738
+ */
2739
+ async verifyResetToken(token) {
2740
+ return (await this.consumeSetPasswordToken(token)).userId;
2730
2741
  }
2731
2742
  async getUserSessionVersion(userId) {
2732
2743
  return this.invalidation.getSessionVersion(userId);
@@ -3477,9 +3488,16 @@ var AuthService = class AuthService2 {
3477
3488
  });
3478
3489
  const { token } = await this.tokenService.generateInviteToken(user.id);
3479
3490
  const inviteLink = `${this.config.frontendUrl}/reset-password?token=${token}`;
3491
+ const accountType = body.role?.trim().toLowerCase();
3492
+ const accountLabel = accountType ? `${accountType} account` : "account";
3480
3493
  let emailSent = false;
3481
3494
  try {
3482
- await this.emailService.sendHtml(body.email, this.t("emails.accountInvite.subject"), accountInviteTemplate({
3495
+ await this.emailService.sendHtml(body.email, this.t("emails.accountInvite.subject", {
3496
+ accountLabel,
3497
+ appName: this.config.appName
3498
+ }), accountInviteTemplate({
3499
+ accountType,
3500
+ appName: this.config.appName,
3483
3501
  inviteLink,
3484
3502
  userName: user.name || body.email
3485
3503
  }));
@@ -3818,10 +3836,18 @@ var AuthService = class AuthService2 {
3818
3836
  }
3819
3837
  async resetPassword(token, newPassword) {
3820
3838
  this.userValidator.validatePasswordStrength(newPassword);
3821
- const userId = await this.tokenService.verifyResetToken(token);
3822
- await this.userService.update(userId, { password: newPassword });
3823
- await this.tokenService.invalidateUserAccessTokens(userId);
3824
- await this.tokenService.revokeAllForUser(userId);
3839
+ const consumed = await this.tokenService.consumeSetPasswordToken(token);
3840
+ const user = await this.userService.getById(consumed.userId);
3841
+ const acceptsInvitation = consumed.type === "invite";
3842
+ await this.userService.update(consumed.userId, {
3843
+ password: newPassword,
3844
+ ...acceptsInvitation ? {
3845
+ emailVerified: true,
3846
+ ...user.status === "pending" ? { status: "active" } : {}
3847
+ } : {}
3848
+ });
3849
+ await this.tokenService.invalidateUserAccessTokens(consumed.userId);
3850
+ await this.tokenService.revokeAllForUser(consumed.userId);
3825
3851
  this.cookieManager.clearRefreshToken();
3826
3852
  this.cookieManager.clearSessionCookie();
3827
3853
  return { message: this.t("success.passwordReset") };
@@ -6327,7 +6353,7 @@ var en_default = {
6327
6353
  subject: "Reset your password"
6328
6354
  },
6329
6355
  accountInvite: {
6330
- subject: "You've been invited \u2014 set up your account"
6356
+ subject: "{{appName}}: activate your {{accountLabel}}"
6331
6357
  }
6332
6358
  }
6333
6359
  },
@@ -7459,6 +7485,13 @@ var validateCallbackUrl = /* @__PURE__ */ __name((value, name) => {
7459
7485
  }
7460
7486
  return callback.toString();
7461
7487
  }, "validateCallbackUrl");
7488
+ var resolveAppName = /* @__PURE__ */ __name((value) => {
7489
+ const appName = value?.trim() || "Your app";
7490
+ if (appName.length > 80 || /[\u0000-\u001f\u007f]/.test(appName)) {
7491
+ throw new Error("auth.appName must be at most 80 characters without control characters");
7492
+ }
7493
+ return appName;
7494
+ }, "resolveAppName");
7462
7495
  var resolveGoogleConfig = /* @__PURE__ */ __name((config) => {
7463
7496
  const configuredGoogle = config?.oauth?.google;
7464
7497
  if (!configuredGoogle)
@@ -7535,6 +7568,7 @@ var resolveAuthConfig = /* @__PURE__ */ __name((config) => {
7535
7568
  blacklistPrefix: config?.blacklistPrefix ?? "auth:blacklist:",
7536
7569
  defaultRole: config?.defaultRole ?? null,
7537
7570
  frontendUrl: config?.frontendUrl ?? process.env.FRONTEND_URL ?? "http://localhost:3000",
7571
+ appName: resolveAppName(config?.appName),
7538
7572
  registrationMode: config?.registrationMode ?? "active",
7539
7573
  publicRegistration: config?.publicRegistration ?? true,
7540
7574
  requireVerifiedEmail: config?.requireVerifiedEmail ?? false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "najm-auth",
3
- "version": "4.0.0",
3
+ "version": "4.0.2",
4
4
  "description": "Authentication and authorization library for najm framework",
5
5
  "type": "module",
6
6
  "files": [
@@ -97,7 +97,7 @@
97
97
  "najm-guard": "^2.0.2",
98
98
  "najm-i18n": "^2.1.2",
99
99
  "najm-cache": "^2.2.0",
100
- "najm-email": "^2.0.3",
100
+ "najm-email": "^2.0.4",
101
101
  "najm-rate": "^2.1.1",
102
102
  "najm-validation": "^2.0.2",
103
103
  "jsonwebtoken": "^9.0.3",