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 +12 -12
- package/dist/index.d.ts +99 -18
- package/dist/index.js +209 -166
- package/dist/schema/pg.d.ts +2 -2
- package/dist/schema/pg.js +3 -2
- package/dist/schema/sqlite.d.ts +2 -2
- package/dist/schema/sqlite.js +3 -2
- package/package.json +10 -15
- package/dist/schema/mysql.d.ts +0 -1535
- package/dist/schema/mysql.js +0 -85
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
|
|
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'
|
|
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
|
|
597
|
-
- A stale refresh token presented after the 120-second rotation grace window revokes
|
|
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'
|
|
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
|
-
/**
|
|
90
|
-
|
|
91
|
-
|
|
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
|
|
720
|
-
* previousHash still matching the presented token AND
|
|
721
|
-
* NULL. Gating on the hash (not just the flag) closes
|
|
722
|
-
* winner's rotation rewrites previousHash via
|
|
723
|
-
*
|
|
724
|
-
*
|
|
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
|
-
|
|
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):
|
|
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
|
-
|
|
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
|
|
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
|
}>;
|