najm-auth 1.1.39 → 1.1.41

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) */
@@ -706,6 +720,12 @@ declare class TokenRepository {
706
720
  /** Shared query helper */
707
721
  private queryHelper?;
708
722
  private get q();
723
+ /**
724
+ * Upsert the refresh-token row for a session, keyed on `tokenFamily` (the
725
+ * per-login session identifier, unique). A brand-new login inserts a fresh
726
+ * family row; a refresh rotation updates only that family's row, leaving the
727
+ * user's other sessions untouched.
728
+ */
709
729
  storeRefreshToken(tokenData: {
710
730
  userId: string;
711
731
  token: string;
@@ -716,17 +736,26 @@ declare class TokenRepository {
716
736
  previousUsedAt?: string | null;
717
737
  }): Promise<any>;
718
738
  /**
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.
739
+ * Claim the previous-token grace slot for a single family. Conditional on
740
+ * BOTH the stored previousHash still matching the presented token AND
741
+ * previousUsedAt being NULL. Gating on the hash (not just the flag) closes
742
+ * the rotation race: the winner's rotation rewrites previousHash via
743
+ * storeRefreshToken (and resets previousUsedAt to NULL), so a loser whose
744
+ * UPDATE lands after that rotation no longer matches and gets zero rows —
745
+ * exactly one caller ever claims the slot.
746
+ */
747
+ markPreviousUsed(tokenFamily: string, previousHash: string): Promise<any>;
748
+ /** Look up a single session's token row by its family identifier. */
749
+ getByFamily(tokenFamily: string): Promise<any>;
750
+ /** Revoke a single session (one family). */
751
+ revokeFamily(tokenFamily: string): Promise<any>;
752
+ /** Revoke every session for a user (password change/reset, logout-all). */
753
+ revokeAllForUser(userId: string): Promise<any>;
754
+ /**
755
+ * Opportunistic cleanup: with one row per family (no unique userId), expired
756
+ * and abandoned sessions accumulate. Delete every expired row.
725
757
  */
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>;
758
+ deleteExpired(): Promise<any>;
730
759
  isUserExists(userId: string): Promise<boolean>;
731
760
  getRoleNameById(userId: string): Promise<string>;
732
761
  getUserPermissions(userId: string): Promise<string[]>;
@@ -761,7 +790,10 @@ declare class TokenService {
761
790
  * Throws error if token is invalid, expired, or blacklisted
762
791
  */
763
792
  verifyAccessToken(token: string): Promise<JwtPayload>;
764
- verifyRefreshToken(token: string): string;
793
+ verifyRefreshToken(token: string): {
794
+ userId: string;
795
+ tokenFamily: string;
796
+ };
765
797
  private static readonly PREVIOUS_GRACE_SECONDS;
766
798
  /**
767
799
  * Read the refresh cookie and return the userId it belongs to.
@@ -789,16 +821,20 @@ declare class TokenService {
789
821
  userId: string;
790
822
  roles?: string[];
791
823
  permissions?: string[];
824
+ tokenFamily?: string;
792
825
  }): Promise<string>;
793
826
  /**
794
- * Generate refresh token with unique jti
827
+ * Generate refresh token with unique jti. The token carries its session's
828
+ * family so rotation/revocation can target a single session.
795
829
  */
796
830
  private signRefreshToken;
797
831
  generateRefreshToken(data: {
798
832
  userId: string;
833
+ tokenFamily?: string;
799
834
  }): string;
800
835
  generateTokens(userId: string, tokenFamily?: string): Promise<{
801
836
  userId: string;
837
+ tokenFamily: string;
802
838
  roles: string[];
803
839
  permissions: string[];
804
840
  accessToken: string;
@@ -831,6 +867,7 @@ declare class TokenService {
831
867
  */
832
868
  refreshTokens(): Promise<{
833
869
  userId: string;
870
+ tokenFamily: string;
834
871
  roles: string[];
835
872
  permissions: string[];
836
873
  accessToken: string;
@@ -838,14 +875,51 @@ declare class TokenService {
838
875
  accessTokenExpiresAt: number;
839
876
  refreshTokenExpiresAt: number;
840
877
  }>;
841
- revokeToken(userId: string): Promise<any>;
878
+ /** Revoke every refresh session for a user (password change/reset, logout-all). */
879
+ revokeAllForUser(userId: string): Promise<any>;
880
+ /** Revoke a single refresh session (one family). */
881
+ revokeFamily(tokenFamily: string): Promise<any>;
882
+ /**
883
+ * Opportunistic cleanup of expired/abandoned sessions. With one row per
884
+ * family (no unique userId), abandoned logins would otherwise accumulate.
885
+ * Best-effort — never let cleanup failure break the calling flow.
886
+ */
887
+ deleteExpiredSessions(): Promise<void>;
842
888
  invalidateUserAccessTokens(userId: string): Promise<number>;
843
889
  getUserFromCookie(): Promise<any>;
890
+ private get revokedFamilyPrefix();
891
+ private revokedFamilyKey;
892
+ /**
893
+ * Mark a family as revoked in cache for the access-token TTL, so every
894
+ * access token minted for that family (not just the presented one) is
895
+ * rejected by verifyAccessToken until it would have expired anyway.
896
+ */
897
+ private markFamilyRevoked;
898
+ /**
899
+ * Revoke only the suspect family — NOT the whole user. Bumping the global
900
+ * per-user session version here would kill every device's access tokens on a
901
+ * single family's reuse detection. Instead drop the family's refresh row and
902
+ * mark the family revoked so its access tokens stop verifying.
903
+ */
844
904
  private revokeSuspectRefreshFamily;
845
905
  /**
846
- * Logout user - blacklist access token and revoke refresh token
906
+ * Logout the CURRENT session only blacklist the presented access token,
907
+ * mark its family revoked, and delete that family's refresh row. Other
908
+ * devices/sessions for the same user keep working. Use a password change or
909
+ * reset (revoke-all) to terminate every session.
910
+ *
911
+ * The family is resolved from, in order: a verified Bearer access token's
912
+ * `tokenFamily` claim, then a verified refresh cookie whose hash still
913
+ * matches the current family row. If neither is available, fall back to
914
+ * revoke-all.
847
915
  */
848
916
  logout(userId: string, authorization?: string): Promise<void>;
917
+ /**
918
+ * Resolve a logout target from the refresh cookie only if the cookie maps to
919
+ * the user's current/valid family row. This mirrors resolveUserFromCookie()
920
+ * without throwing, because logout can still fall back to revoke-all.
921
+ */
922
+ private resolveRefreshCookieFamily;
849
923
  /**
850
924
  * Generate secure password reset token
851
925
  * Returns both the plain token (to send via email) and userId for identification
@@ -962,6 +1036,13 @@ declare class AuthService {
962
1036
  data: any;
963
1037
  message: string;
964
1038
  }>;
1039
+ /**
1040
+ * Prune expired refresh sessions for every user. Login already prunes
1041
+ * opportunistically; expose this so consumers can also run it from a
1042
+ * scheduled job (cron / queue) to reclaim rows from users who never return.
1043
+ * Best-effort — safe to call repeatedly.
1044
+ */
1045
+ pruneExpiredSessions(): Promise<void>;
965
1046
  getUserProfile(userData: AuthUser): Promise<AuthUser & {
966
1047
  language: string;
967
1048
  }>;