najm-auth 1.1.39 → 1.1.42

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
@@ -9,7 +9,7 @@ Production-ready authentication and authorization library for the Najm framework
9
9
  - ✅ Permission-based access control (PBAC) with wildcards
10
10
  - ✅ Row-level ownership scoping for multi-tenant apps
11
11
  - ✅ Built-in password reset flow with email support
12
- - ✅ Multi-dialect support (PostgreSQL, SQLite, MySQL)
12
+ - ✅ Multi-dialect support (PostgreSQL, SQLite)
13
13
  - ✅ Type-safe decorators with TypeScript
14
14
  - ✅ Rate limiting on auth endpoints
15
15
  - ✅ Internationalization (i18n) for all messages
@@ -103,7 +103,7 @@ FRONTEND_URL=https://app.example.com
103
103
  ```typescript
104
104
  auth({
105
105
  // Database
106
- dialect?: 'pg' | 'sqlite' | 'mysql' // Default: 'pg'
106
+ dialect?: 'pg' | 'sqlite' // Default: 'pg' (RETURNING-capable engines only)
107
107
  schema?: AuthSchema // Override dialect schema
108
108
 
109
109
  // JWT
@@ -121,9 +121,9 @@ auth({
121
121
  database?: string // Default: 'default'
122
122
  blacklistPrefix?: string // Default: 'auth:blacklist:'
123
123
 
124
- // Registration
125
- defaultRole?: string | null // Auto-assign role to new users
126
- bcryptRounds?: number // Default: 10 (valid: 4-31)
124
+ // Registration
125
+ defaultRole?: string | null // Auto-assign role to new users
126
+ bcryptRounds?: number // Default: 10 (valid: 4-31)
127
127
 
128
128
  // Frontend
129
129
  frontendUrl?: string // Password reset link base URL
@@ -156,7 +156,7 @@ All routes are prefixed with `/auth` and auto-registered by the plugin.
156
156
 
157
157
  | Method | Path | Description |
158
158
  |--------|------|-------------|
159
- | `GET` | `/users?limit=50&offset=0` | List users (limit 1-100) |
159
+ | `GET` | `/users?limit=50&offset=0` | List users (limit 1-100) |
160
160
  | `GET` | `/users/:id` | Get user by ID |
161
161
  | `POST` | `/users` | Create new user |
162
162
  | `PUT` | `/users/:id` | Update user |
@@ -591,12 +591,12 @@ async resetPassword(token: string, newPassword: string) {
591
591
  }
592
592
  ```
593
593
 
594
- ### Session Management
595
-
596
- - Sessions are single-device: the token table stores one refresh row per user, so a new login replaces the previous device's refresh session
597
- - A stale refresh token presented after the 120-second rotation grace window revokes the active refresh session as reuse protection
598
- - The signed session cookie is accepted for up to its configured TTL (5 minutes by default) without a database or revocation-cache read
599
- - Use `@RateLimit` on logout for DDoS protection
594
+ ### Session Management
595
+
596
+ - Sessions are multi-device: the token table stores one refresh row per login session (keyed by a unique `tokenFamily`), so a user can stay logged in on several devices at once. Logout and rotation are scoped to the current session; password change/reset revoke every session
597
+ - A stale refresh token presented after the 120-second rotation grace window revokes only that session's family as reuse protection
598
+ - The signed session cookie is accepted for up to its configured TTL (5 minutes by default) without a database or revocation-cache read
599
+ - Use `@RateLimit` on logout for DDoS protection
600
600
 
601
601
  ### Token Blacklist
602
602
 
package/dist/index.d.ts CHANGED
@@ -76,7 +76,7 @@ interface AuthConfig {
76
76
  */
77
77
  /**
78
78
  * Auth schema shape (dialect-agnostic)
79
- * Import from 'najm-auth/pg', 'najm-auth/sqlite', or 'najm-auth/mysql'
79
+ * Import from 'najm-auth/pg' or 'najm-auth/sqlite'.
80
80
  */
81
81
  interface AuthSchema {
82
82
  users: any;
@@ -86,9 +86,17 @@ interface AuthSchema {
86
86
  rolePermissions: any;
87
87
  }
88
88
  type AuthPluginConfig = {
89
- /** Database dialect (default: 'pg'). Auto-selects the correct schema. */
90
- dialect?: 'pg' | 'sqlite' | 'mysql';
91
- /** Database schema tables (optional, overrides dialect). Use authSchema from 'najm-auth/sqlite' or 'najm-auth/mysql' */
89
+ /**
90
+ * Database dialect (default: 'pg'). Auto-selects the correct schema.
91
+ * Only RETURNING-capable engines are supported the auth data layer
92
+ * relies on `.returning()` for every write. MySQL is not supported.
93
+ */
94
+ dialect?: 'pg' | 'sqlite';
95
+ /**
96
+ * Database schema tables (optional, overrides dialect). Use authSchema
97
+ * from 'najm-auth/pg' or 'najm-auth/sqlite'. A custom schema must still be
98
+ * backed by a RETURNING-capable engine (Postgres or SQLite).
99
+ */
92
100
  schema?: AuthSchema;
93
101
  /** JWT configuration (secrets can be set via env vars) */
94
102
  jwt?: Partial<JwtConfig>;
@@ -124,6 +132,12 @@ interface JwtPayload {
124
132
  userId: string;
125
133
  /** Unique token ID for blacklist-based revocation */
126
134
  jti: string;
135
+ /**
136
+ * Refresh-token session/family identifier. Present on both refresh tokens
137
+ * (required) and access tokens (so a single family's revocation can reject
138
+ * every access token minted for that session, not just the presented one).
139
+ */
140
+ tokenFamily?: string;
127
141
  /** Per-user access token generation version for mass invalidation */
128
142
  sessionVersion?: number;
129
143
  /** User roles (included for client-side RBAC) */
@@ -194,7 +208,16 @@ var auth = {
194
208
  passwordChanged: "Password changed successfully",
195
209
  passwordResetSent: "If that email exists, a reset link has been sent",
196
210
  passwordReset: "Password has been reset successfully",
211
+ accountInviteSent: "Invitation sent successfully",
197
212
  tokenRefreshed: "Token refreshed successfully"
213
+ },
214
+ emails: {
215
+ passwordReset: {
216
+ subject: "Reset your password"
217
+ },
218
+ accountInvite: {
219
+ subject: "You've been invited — set up your account"
220
+ }
198
221
  }
199
222
  };
200
223
  var users = {
@@ -287,8 +310,17 @@ declare const AUTH_LOCALES: {
287
310
  passwordChanged: string;
288
311
  passwordResetSent: string;
289
312
  passwordReset: string;
313
+ accountInviteSent: string;
290
314
  tokenRefreshed: string;
291
315
  };
316
+ emails: {
317
+ passwordReset: {
318
+ subject: string;
319
+ };
320
+ accountInvite: {
321
+ subject: string;
322
+ };
323
+ };
292
324
  };
293
325
  users: {
294
326
  errors: {
@@ -706,6 +738,12 @@ declare class TokenRepository {
706
738
  /** Shared query helper */
707
739
  private queryHelper?;
708
740
  private get q();
741
+ /**
742
+ * Upsert the refresh-token row for a session, keyed on `tokenFamily` (the
743
+ * per-login session identifier, unique). A brand-new login inserts a fresh
744
+ * family row; a refresh rotation updates only that family's row, leaving the
745
+ * user's other sessions untouched.
746
+ */
709
747
  storeRefreshToken(tokenData: {
710
748
  userId: string;
711
749
  token: string;
@@ -716,17 +754,26 @@ declare class TokenRepository {
716
754
  previousUsedAt?: string | null;
717
755
  }): Promise<any>;
718
756
  /**
719
- * Claim the previous-token grace slot. Conditional on BOTH the stored
720
- * previousHash still matching the presented token AND previousUsedAt being
721
- * NULL. Gating on the hash (not just the flag) closes the rotation race: the
722
- * winner's rotation rewrites previousHash via storeRefreshToken, so a loser
723
- * whose UPDATE lands after that rotation no longer matches and gets zero
724
- * rows exactly one caller ever claims the slot.
757
+ * Claim the previous-token grace slot for a single family. Conditional on
758
+ * BOTH the stored previousHash still matching the presented token AND
759
+ * previousUsedAt being NULL. Gating on the hash (not just the flag) closes
760
+ * the rotation race: the winner's rotation rewrites previousHash via
761
+ * storeRefreshToken (and resets previousUsedAt to NULL), so a loser whose
762
+ * UPDATE lands after that rotation no longer matches and gets zero rows —
763
+ * exactly one caller ever claims the slot.
764
+ */
765
+ markPreviousUsed(tokenFamily: string, previousHash: string): Promise<any>;
766
+ /** Look up a single session's token row by its family identifier. */
767
+ getByFamily(tokenFamily: string): Promise<any>;
768
+ /** Revoke a single session (one family). */
769
+ revokeFamily(tokenFamily: string): Promise<any>;
770
+ /** Revoke every session for a user (password change/reset, logout-all). */
771
+ revokeAllForUser(userId: string): Promise<any>;
772
+ /**
773
+ * Opportunistic cleanup: with one row per family (no unique userId), expired
774
+ * and abandoned sessions accumulate. Delete every expired row.
725
775
  */
726
- markPreviousUsed(userId: string, previousHash: string): Promise<any>;
727
- getRefreshTokenWithFamily(userId: string): Promise<any>;
728
- revokeToken(userId: string): Promise<any>;
729
- revokeByFamily(tokenFamily: string): Promise<any>;
776
+ deleteExpired(): Promise<any>;
730
777
  isUserExists(userId: string): Promise<boolean>;
731
778
  getRoleNameById(userId: string): Promise<string>;
732
779
  getUserPermissions(userId: string): Promise<string[]>;
@@ -761,7 +808,10 @@ declare class TokenService {
761
808
  * Throws error if token is invalid, expired, or blacklisted
762
809
  */
763
810
  verifyAccessToken(token: string): Promise<JwtPayload>;
764
- verifyRefreshToken(token: string): string;
811
+ verifyRefreshToken(token: string): {
812
+ userId: string;
813
+ tokenFamily: string;
814
+ };
765
815
  private static readonly PREVIOUS_GRACE_SECONDS;
766
816
  /**
767
817
  * Read the refresh cookie and return the userId it belongs to.
@@ -789,16 +839,20 @@ declare class TokenService {
789
839
  userId: string;
790
840
  roles?: string[];
791
841
  permissions?: string[];
842
+ tokenFamily?: string;
792
843
  }): Promise<string>;
793
844
  /**
794
- * Generate refresh token with unique jti
845
+ * Generate refresh token with unique jti. The token carries its session's
846
+ * family so rotation/revocation can target a single session.
795
847
  */
796
848
  private signRefreshToken;
797
849
  generateRefreshToken(data: {
798
850
  userId: string;
851
+ tokenFamily?: string;
799
852
  }): string;
800
853
  generateTokens(userId: string, tokenFamily?: string): Promise<{
801
854
  userId: string;
855
+ tokenFamily: string;
802
856
  roles: string[];
803
857
  permissions: string[];
804
858
  accessToken: string;
@@ -831,6 +885,7 @@ declare class TokenService {
831
885
  */
832
886
  refreshTokens(): Promise<{
833
887
  userId: string;
888
+ tokenFamily: string;
834
889
  roles: string[];
835
890
  permissions: string[];
836
891
  accessToken: string;
@@ -838,14 +893,57 @@ declare class TokenService {
838
893
  accessTokenExpiresAt: number;
839
894
  refreshTokenExpiresAt: number;
840
895
  }>;
841
- revokeToken(userId: string): Promise<any>;
896
+ /** Revoke every refresh session for a user (password change/reset, logout-all). */
897
+ revokeAllForUser(userId: string): Promise<any>;
898
+ /** Revoke a single refresh session (one family). */
899
+ revokeFamily(tokenFamily: string): Promise<any>;
900
+ /**
901
+ * Opportunistic cleanup of expired/abandoned sessions. With one row per
902
+ * family (no unique userId), abandoned logins would otherwise accumulate.
903
+ * Best-effort — never let cleanup failure break the calling flow.
904
+ */
905
+ deleteExpiredSessions(): Promise<void>;
842
906
  invalidateUserAccessTokens(userId: string): Promise<number>;
843
907
  getUserFromCookie(): Promise<any>;
908
+ private get revokedFamilyPrefix();
909
+ private revokedFamilyKey;
910
+ /**
911
+ * Mark a family as revoked in cache for the access-token TTL, so every
912
+ * access token minted for that family (not just the presented one) is
913
+ * rejected by verifyAccessToken until it would have expired anyway.
914
+ */
915
+ private markFamilyRevoked;
916
+ /**
917
+ * Revoke only the suspect family — NOT the whole user. Bumping the global
918
+ * per-user session version here would kill every device's access tokens on a
919
+ * single family's reuse detection. Instead drop the family's refresh row and
920
+ * mark the family revoked so its access tokens stop verifying.
921
+ */
844
922
  private revokeSuspectRefreshFamily;
845
923
  /**
846
- * Logout user - blacklist access token and revoke refresh token
924
+ * Logout the CURRENT session only blacklist the presented access token,
925
+ * mark its family revoked, and delete that family's refresh row. Other
926
+ * devices/sessions for the same user keep working. Use a password change or
927
+ * reset (revoke-all) to terminate every session.
928
+ *
929
+ * The family is resolved from, in order: a verified Bearer access token's
930
+ * `tokenFamily` claim, then a verified refresh cookie whose hash still
931
+ * matches the current family row. If neither is available, fall back to
932
+ * revoke-all.
847
933
  */
848
934
  logout(userId: string, authorization?: string): Promise<void>;
935
+ /**
936
+ * Resolve a logout target from the refresh cookie only if the cookie maps to
937
+ * the user's current/valid family row. This mirrors resolveUserFromCookie()
938
+ * without throwing, because logout can still fall back to revoke-all.
939
+ */
940
+ private resolveRefreshCookieFamily;
941
+ /**
942
+ * Generate a one-time set-password token (used by both password reset and
943
+ * account invites). Stores the jti in cache so the token can only be used
944
+ * once, with a TTL matching the JWT expiry.
945
+ */
946
+ private generateSetPasswordToken;
849
947
  /**
850
948
  * Generate secure password reset token
851
949
  * Returns both the plain token (to send via email) and userId for identification
@@ -854,6 +952,15 @@ declare class TokenService {
854
952
  token: string;
855
953
  userId: string;
856
954
  }>;
955
+ /**
956
+ * Generate secure account-invite token.
957
+ * Longer expiry (3d) than reset because an invited user may not check
958
+ * their email immediately. Consumed via the same reset-password endpoint.
959
+ */
960
+ generateInviteToken(userId: string): Promise<{
961
+ token: string;
962
+ userId: string;
963
+ }>;
857
964
  /**
858
965
  * Verify password reset token
859
966
  * Returns userId if valid, throws error if expired/invalid
@@ -888,6 +995,12 @@ declare const updateUserDto: z.ZodObject<{
888
995
  inactive: "inactive";
889
996
  }>>>;
890
997
  }, z.core.$strip>;
998
+ declare const inviteUserDto: z.ZodObject<{
999
+ name: z.ZodOptional<z.ZodString>;
1000
+ email: z.ZodString;
1001
+ roleId: z.ZodOptional<z.ZodString>;
1002
+ image: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1003
+ }, z.core.$strip>;
891
1004
  declare const userIdParam: z.ZodObject<{
892
1005
  id: z.ZodString;
893
1006
  }, z.core.$strip>;
@@ -925,6 +1038,7 @@ declare const userListQuery: z.ZodObject<{
925
1038
  }, z.core.$strip>;
926
1039
  type CreateUserDto = z.infer<typeof createUserDto>;
927
1040
  type UpdateUserDto = z.infer<typeof updateUserDto>;
1041
+ type InviteUserDto = z.infer<typeof inviteUserDto>;
928
1042
  type UserIdParam = z.infer<typeof userIdParam>;
929
1043
  type LoginDto = z.infer<typeof loginDto>;
930
1044
  type ChangePasswordDto = z.infer<typeof changePasswordDto>;
@@ -954,6 +1068,16 @@ declare class AuthService {
954
1068
  private getDummyHash;
955
1069
  warmupPasswordHash(): Promise<void>;
956
1070
  registerUser(body: CreateUserDto): Promise<SanitizedUser>;
1071
+ /**
1072
+ * Admin-initiated account creation. The user is created with a random,
1073
+ * unusable password (the schema requires one) and then emailed a one-time
1074
+ * link to set their own. They can't log in until they do, since they never
1075
+ * learn the random password.
1076
+ *
1077
+ * Email is best-effort: a send failure logs a warning but never rolls back
1078
+ * account creation (and with the console provider, nothing is actually sent).
1079
+ */
1080
+ inviteUser(body: InviteUserDto): Promise<SanitizedUser>;
957
1081
  loginUser(body: LoginDto): Promise<TokenPair & {
958
1082
  user: SanitizedUser;
959
1083
  }>;
@@ -962,6 +1086,13 @@ declare class AuthService {
962
1086
  data: any;
963
1087
  message: string;
964
1088
  }>;
1089
+ /**
1090
+ * Prune expired refresh sessions for every user. Login already prunes
1091
+ * opportunistically; expose this so consumers can also run it from a
1092
+ * scheduled job (cron / queue) to reclaim rows from users who never return.
1093
+ * Best-effort — safe to call repeatedly.
1094
+ */
1095
+ pruneExpiredSessions(): Promise<void>;
965
1096
  getUserProfile(userData: AuthUser): Promise<AuthUser & {
966
1097
  language: string;
967
1098
  }>;
@@ -996,6 +1127,7 @@ declare class AuthController {
996
1127
  loginUser(body: LoginDto): Promise<TokenPair & {
997
1128
  user: SanitizedUser;
998
1129
  }>;
1130
+ inviteUser(body: InviteUserDto): Promise<SanitizedUser>;
999
1131
  refreshTokens(): Promise<TokenPair>;
1000
1132
  logoutUser(userId: string, authorization?: string): Promise<{
1001
1133
  data: any;
@@ -1968,4 +2100,4 @@ declare const authSeed: (config: AuthSeedConfig) => Record<string, SeedEntry>;
1968
2100
  */
1969
2101
  declare function seedAuthData(config: SeedAuthDataConfig): Promise<SeedAuthDataResult>;
1970
2102
 
1971
- 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, type AuthUser, 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 DefineRolesOptions, type EmailParam, EncryptionService, type JwtConfig, type JwtPayload, type LanguageParam, type LoginDto, NewPermission, NewRoleEntity, NewUser, Owned, type OwnedMethods, type OwnershipConfig, type OwnershipProvider, type OwnershipRule, OwnershipToken, type OwnershipTokenOptions, Permission, PermissionController, PermissionGuard, type PermissionIdParam, PermissionRepository, PermissionService, PermissionValidator, Policy, ROLES, ROLE_GROUPS, type RefreshTokenDto, 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, authSeed, avatarsPath, calculateAge, calculateYearsOfExperience, changePasswordDto, checkPermissionDto, clean, configureOwnership, confirmResetPasswordDto, createPermissionDto, createRoleDto, createTokenDto, createUserDto, defineRoles, emailParam, formatDate, getAuthLocale, getAvatarFile, isAdmin, isAdministrator, isAuth, isEmpty, isFile, isPath, join, languageParam, loginDto, own, parseSchema, permissionIdParam, pickProps, refreshTokenDto, resetPasswordDto, revokeTokenDto, roleIdParam, runAsUser, seedAuthData, tokenIdParam, updatePermissionDto, updateRoleDto, updateTokenDto, updateUserDto, userIdInParam, userIdParam, userListQuery, verifyTokenDto, where };
2103
+ 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, type AuthUser, 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 DefineRolesOptions, type EmailParam, EncryptionService, type InviteUserDto, type JwtConfig, type JwtPayload, type LanguageParam, type LoginDto, NewPermission, NewRoleEntity, NewUser, Owned, type OwnedMethods, type OwnershipConfig, type OwnershipProvider, type OwnershipRule, OwnershipToken, type OwnershipTokenOptions, Permission, PermissionController, PermissionGuard, type PermissionIdParam, PermissionRepository, PermissionService, PermissionValidator, Policy, ROLES, ROLE_GROUPS, type RefreshTokenDto, 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, authSeed, avatarsPath, calculateAge, calculateYearsOfExperience, changePasswordDto, checkPermissionDto, clean, configureOwnership, confirmResetPasswordDto, createPermissionDto, createRoleDto, createTokenDto, createUserDto, defineRoles, emailParam, formatDate, getAuthLocale, getAvatarFile, inviteUserDto, isAdmin, isAdministrator, isAuth, isEmpty, isFile, isPath, join, languageParam, loginDto, own, parseSchema, permissionIdParam, pickProps, refreshTokenDto, resetPasswordDto, revokeTokenDto, roleIdParam, runAsUser, seedAuthData, tokenIdParam, updatePermissionDto, updateRoleDto, updateTokenDto, updateUserDto, userIdInParam, userIdParam, userListQuery, verifyTokenDto, where };