@geekmidas/auth 0.0.1
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/__tests__/cacheTokenStorage.spec.cjs +146 -0
- package/dist/__tests__/cacheTokenStorage.spec.mjs +145 -0
- package/dist/__tests__/client.spec.cjs +17281 -0
- package/dist/__tests__/client.spec.mjs +17308 -0
- package/dist/__tests__/tokenManager.spec.cjs +197 -0
- package/dist/__tests__/tokenManager.spec.mjs +196 -0
- package/dist/cacheTokenStorage-Ca05vf3T.mjs +31 -0
- package/dist/cacheTokenStorage-CvptztkW.cjs +37 -0
- package/dist/cacheTokenStorage.cjs +3 -0
- package/dist/cacheTokenStorage.mjs +3 -0
- package/dist/chunk-DWy1uDak.cjs +39 -0
- package/dist/client-DaiUm0UZ.cjs +155 -0
- package/dist/client-DjNZS6vZ.mjs +137 -0
- package/dist/client.cjs +7 -0
- package/dist/client.mjs +4 -0
- package/dist/index.cjs +9 -0
- package/dist/index.mjs +5 -0
- package/dist/playground.cjs +0 -0
- package/dist/playground.mjs +0 -0
- package/dist/server.cjs +5 -0
- package/dist/server.mjs +4 -0
- package/dist/tokenManager-B60gFnEF.cjs +69 -0
- package/dist/tokenManager-BVXxfHRa.mjs +63 -0
- package/dist/tokenManager.cjs +3 -0
- package/dist/tokenManager.mjs +3 -0
- package/package.json +38 -0
- package/src/__tests__/cacheTokenStorage.spec.ts +213 -0
- package/src/__tests__/client.spec.ts +465 -0
- package/src/__tests__/tokenManager.spec.ts +259 -0
- package/src/cacheTokenStorage.ts +43 -0
- package/src/client.ts +211 -0
- package/src/index.ts +3 -0
- package/src/playground.ts +0 -0
- package/src/server.ts +11 -0
- package/src/tokenManager.ts +104 -0
- package/tsdown.config.ts +3 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
const require_chunk = require('../chunk-DWy1uDak.cjs');
|
|
2
|
+
const require_tokenManager = require('../tokenManager-B60gFnEF.cjs');
|
|
3
|
+
const vitest = require_chunk.__toESM(require("vitest"));
|
|
4
|
+
|
|
5
|
+
//#region src/__tests__/tokenManager.spec.ts
|
|
6
|
+
(0, vitest.describe)("TokenManager", () => {
|
|
7
|
+
let tokenManager;
|
|
8
|
+
let userPayload;
|
|
9
|
+
(0, vitest.beforeEach)(() => {
|
|
10
|
+
tokenManager = new require_tokenManager.TokenManager({
|
|
11
|
+
accessTokenSecret: "test-access-secret",
|
|
12
|
+
refreshTokenSecret: "test-refresh-secret",
|
|
13
|
+
accessTokenExpiresIn: "15m",
|
|
14
|
+
refreshTokenExpiresIn: "7d"
|
|
15
|
+
});
|
|
16
|
+
userPayload = {
|
|
17
|
+
userId: "user123",
|
|
18
|
+
email: "test@example.com",
|
|
19
|
+
role: "admin"
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
(0, vitest.describe)("token generation and verification workflow", () => {
|
|
23
|
+
(0, vitest.it)("should generate and verify complete token pair", () => {
|
|
24
|
+
const { accessToken, refreshToken } = tokenManager.generateTokenPair(userPayload);
|
|
25
|
+
const accessDecoded = tokenManager.verifyAccessToken(accessToken);
|
|
26
|
+
const refreshDecoded = tokenManager.verifyRefreshToken(refreshToken);
|
|
27
|
+
(0, vitest.expect)(accessDecoded.userId).toBe(userPayload.userId);
|
|
28
|
+
(0, vitest.expect)(accessDecoded.email).toBe(userPayload.email);
|
|
29
|
+
(0, vitest.expect)(accessDecoded.role).toBe(userPayload.role);
|
|
30
|
+
(0, vitest.expect)(refreshDecoded.userId).toBe(userPayload.userId);
|
|
31
|
+
(0, vitest.expect)(refreshDecoded.email).toBe(userPayload.email);
|
|
32
|
+
(0, vitest.expect)(refreshDecoded.role).toBe(userPayload.role);
|
|
33
|
+
});
|
|
34
|
+
(0, vitest.it)("should handle complete token refresh workflow", () => {
|
|
35
|
+
const { refreshToken } = tokenManager.generateTokenPair(userPayload);
|
|
36
|
+
const newAccessToken = tokenManager.refreshAccessToken(refreshToken);
|
|
37
|
+
const newDecoded = tokenManager.verifyAccessToken(newAccessToken);
|
|
38
|
+
(0, vitest.expect)(newDecoded.userId).toBe(userPayload.userId);
|
|
39
|
+
(0, vitest.expect)(newDecoded.email).toBe(userPayload.email);
|
|
40
|
+
(0, vitest.expect)(newDecoded.role).toBe(userPayload.role);
|
|
41
|
+
(0, vitest.expect)(newDecoded.iat).toBeDefined();
|
|
42
|
+
(0, vitest.expect)(newDecoded.exp).toBeDefined();
|
|
43
|
+
});
|
|
44
|
+
(0, vitest.it)("should maintain payload integrity through refresh cycle", () => {
|
|
45
|
+
const originalPayload = {
|
|
46
|
+
userId: "user456",
|
|
47
|
+
email: "user@example.com",
|
|
48
|
+
permissions: ["read", "write"],
|
|
49
|
+
metadata: { department: "engineering" }
|
|
50
|
+
};
|
|
51
|
+
const { refreshToken } = tokenManager.generateTokenPair(originalPayload);
|
|
52
|
+
const newAccessToken = tokenManager.refreshAccessToken(refreshToken);
|
|
53
|
+
const decoded = tokenManager.verifyAccessToken(newAccessToken);
|
|
54
|
+
(0, vitest.expect)(decoded.userId).toBe(originalPayload.userId);
|
|
55
|
+
(0, vitest.expect)(decoded.email).toBe(originalPayload.email);
|
|
56
|
+
(0, vitest.expect)(decoded.permissions).toEqual(originalPayload.permissions);
|
|
57
|
+
(0, vitest.expect)(decoded.metadata).toEqual(originalPayload.metadata);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
(0, vitest.describe)("token expiration handling", () => {
|
|
61
|
+
(0, vitest.it)("should correctly identify token expiration status", () => {
|
|
62
|
+
const { accessToken } = tokenManager.generateTokenPair(userPayload);
|
|
63
|
+
(0, vitest.expect)(tokenManager.isTokenExpired(accessToken)).toBe(false);
|
|
64
|
+
const expiration = tokenManager.getTokenExpiration(accessToken);
|
|
65
|
+
(0, vitest.expect)(expiration).toBeInstanceOf(Date);
|
|
66
|
+
(0, vitest.expect)(expiration.getTime()).toBeGreaterThan(Date.now());
|
|
67
|
+
});
|
|
68
|
+
(0, vitest.it)("should handle expired tokens correctly", async () => {
|
|
69
|
+
vitest.vi.useFakeTimers();
|
|
70
|
+
const shortLivedManager = new require_tokenManager.TokenManager({
|
|
71
|
+
accessTokenSecret: "test-access-secret",
|
|
72
|
+
refreshTokenSecret: "test-refresh-secret",
|
|
73
|
+
accessTokenExpiresIn: "10m"
|
|
74
|
+
});
|
|
75
|
+
const { accessToken } = shortLivedManager.generateTokenPair(userPayload);
|
|
76
|
+
vitest.vi.advanceTimersByTime(1e3 * 60 * 16);
|
|
77
|
+
(0, vitest.expect)(tokenManager.isTokenExpired(accessToken)).toBe(true);
|
|
78
|
+
vitest.vi.useRealTimers();
|
|
79
|
+
});
|
|
80
|
+
(0, vitest.it)("should decode tokens without verification", () => {
|
|
81
|
+
const { accessToken } = tokenManager.generateTokenPair(userPayload);
|
|
82
|
+
const decoded = tokenManager.decodeToken(accessToken);
|
|
83
|
+
(0, vitest.expect)(decoded).not.toBeNull();
|
|
84
|
+
(0, vitest.expect)(decoded.userId).toBe(userPayload.userId);
|
|
85
|
+
(0, vitest.expect)(decoded.email).toBe(userPayload.email);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
(0, vitest.describe)("security validation", () => {
|
|
89
|
+
(0, vitest.it)("should reject tokens with invalid signatures", () => {
|
|
90
|
+
const wrongSecretManager = new require_tokenManager.TokenManager({
|
|
91
|
+
accessTokenSecret: "wrong-secret",
|
|
92
|
+
refreshTokenSecret: "test-refresh-secret"
|
|
93
|
+
});
|
|
94
|
+
const { accessToken } = wrongSecretManager.generateTokenPair(userPayload);
|
|
95
|
+
(0, vitest.expect)(() => {
|
|
96
|
+
tokenManager.verifyAccessToken(accessToken);
|
|
97
|
+
}).toThrow("Invalid access token");
|
|
98
|
+
});
|
|
99
|
+
(0, vitest.it)("should reject malformed tokens", () => {
|
|
100
|
+
(0, vitest.expect)(() => {
|
|
101
|
+
tokenManager.verifyAccessToken("invalid-token");
|
|
102
|
+
}).toThrow("Invalid access token");
|
|
103
|
+
(0, vitest.expect)(() => {
|
|
104
|
+
tokenManager.verifyRefreshToken("invalid-token");
|
|
105
|
+
}).toThrow("Invalid refresh token");
|
|
106
|
+
(0, vitest.expect)(() => {
|
|
107
|
+
tokenManager.refreshAccessToken("invalid-token");
|
|
108
|
+
}).toThrow("Invalid refresh token");
|
|
109
|
+
});
|
|
110
|
+
(0, vitest.it)("should handle cross-secret validation correctly", () => {
|
|
111
|
+
const { accessToken, refreshToken } = tokenManager.generateTokenPair(userPayload);
|
|
112
|
+
(0, vitest.expect)(() => {
|
|
113
|
+
tokenManager.verifyRefreshToken(accessToken);
|
|
114
|
+
}).toThrow("Invalid refresh token");
|
|
115
|
+
(0, vitest.expect)(() => {
|
|
116
|
+
tokenManager.verifyAccessToken(refreshToken);
|
|
117
|
+
}).toThrow("Invalid access token");
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
(0, vitest.describe)("configuration flexibility", () => {
|
|
121
|
+
(0, vitest.it)("should support custom expiration times", () => {
|
|
122
|
+
const customManager = new require_tokenManager.TokenManager({
|
|
123
|
+
accessTokenSecret: "test-access-secret",
|
|
124
|
+
refreshTokenSecret: "test-refresh-secret",
|
|
125
|
+
accessTokenExpiresIn: "1h",
|
|
126
|
+
refreshTokenExpiresIn: "30d"
|
|
127
|
+
});
|
|
128
|
+
const { accessToken, refreshToken } = customManager.generateTokenPair(userPayload);
|
|
129
|
+
const accessDecoded = customManager.verifyAccessToken(accessToken);
|
|
130
|
+
const refreshDecoded = customManager.verifyRefreshToken(refreshToken);
|
|
131
|
+
const accessExpTime = (accessDecoded.exp - accessDecoded.iat) * 1e3;
|
|
132
|
+
const refreshExpTime = (refreshDecoded.exp - refreshDecoded.iat) * 1e3;
|
|
133
|
+
(0, vitest.expect)(accessExpTime).toBeGreaterThan(55 * 60 * 1e3);
|
|
134
|
+
(0, vitest.expect)(accessExpTime).toBeLessThan(65 * 60 * 1e3);
|
|
135
|
+
(0, vitest.expect)(refreshExpTime).toBeGreaterThan(29 * 24 * 60 * 60 * 1e3);
|
|
136
|
+
(0, vitest.expect)(refreshExpTime).toBeLessThan(31 * 24 * 60 * 60 * 1e3);
|
|
137
|
+
});
|
|
138
|
+
(0, vitest.it)("should use default expiration times", () => {
|
|
139
|
+
const defaultManager = new require_tokenManager.TokenManager({
|
|
140
|
+
accessTokenSecret: "test-access-secret",
|
|
141
|
+
refreshTokenSecret: "test-refresh-secret"
|
|
142
|
+
});
|
|
143
|
+
const { accessToken, refreshToken } = defaultManager.generateTokenPair(userPayload);
|
|
144
|
+
const accessDecoded = defaultManager.verifyAccessToken(accessToken);
|
|
145
|
+
const refreshDecoded = defaultManager.verifyRefreshToken(refreshToken);
|
|
146
|
+
const accessExpTime = (accessDecoded.exp - accessDecoded.iat) * 1e3;
|
|
147
|
+
const refreshExpTime = (refreshDecoded.exp - refreshDecoded.iat) * 1e3;
|
|
148
|
+
(0, vitest.expect)(accessExpTime).toBeGreaterThan(14 * 60 * 1e3);
|
|
149
|
+
(0, vitest.expect)(accessExpTime).toBeLessThan(16 * 60 * 1e3);
|
|
150
|
+
(0, vitest.expect)(refreshExpTime).toBeGreaterThan(6 * 24 * 60 * 60 * 1e3);
|
|
151
|
+
(0, vitest.expect)(refreshExpTime).toBeLessThan(8 * 24 * 60 * 60 * 1e3);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
(0, vitest.describe)("edge cases and error handling", () => {
|
|
155
|
+
(0, vitest.it)("should handle empty payload gracefully", () => {
|
|
156
|
+
const emptyPayload = { userId: "" };
|
|
157
|
+
const { accessToken, refreshToken } = tokenManager.generateTokenPair(emptyPayload);
|
|
158
|
+
const accessDecoded = tokenManager.verifyAccessToken(accessToken);
|
|
159
|
+
const refreshDecoded = tokenManager.verifyRefreshToken(refreshToken);
|
|
160
|
+
(0, vitest.expect)(accessDecoded.userId).toBe("");
|
|
161
|
+
(0, vitest.expect)(refreshDecoded.userId).toBe("");
|
|
162
|
+
});
|
|
163
|
+
(0, vitest.it)("should handle large payloads correctly", () => {
|
|
164
|
+
const largePayload = {
|
|
165
|
+
userId: "user123",
|
|
166
|
+
permissions: Array.from({ length: 100 }, (_, i) => `permission_${i}`),
|
|
167
|
+
metadata: {
|
|
168
|
+
department: "engineering",
|
|
169
|
+
team: "platform",
|
|
170
|
+
projects: Array.from({ length: 50 }, (_, i) => `project_${i}`)
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
const { accessToken, refreshToken } = tokenManager.generateTokenPair(largePayload);
|
|
174
|
+
const accessDecoded = tokenManager.verifyAccessToken(accessToken);
|
|
175
|
+
const refreshDecoded = tokenManager.verifyRefreshToken(refreshToken);
|
|
176
|
+
(0, vitest.expect)(accessDecoded.userId).toBe(largePayload.userId);
|
|
177
|
+
(0, vitest.expect)(accessDecoded.permissions).toEqual(largePayload.permissions);
|
|
178
|
+
(0, vitest.expect)(accessDecoded.metadata).toEqual(largePayload.metadata);
|
|
179
|
+
(0, vitest.expect)(refreshDecoded.userId).toBe(largePayload.userId);
|
|
180
|
+
(0, vitest.expect)(refreshDecoded.permissions).toEqual(largePayload.permissions);
|
|
181
|
+
(0, vitest.expect)(refreshDecoded.metadata).toEqual(largePayload.metadata);
|
|
182
|
+
});
|
|
183
|
+
(0, vitest.it)("should return null for completely invalid tokens", () => {
|
|
184
|
+
(0, vitest.expect)(tokenManager.decodeToken("")).toBeNull();
|
|
185
|
+
(0, vitest.expect)(tokenManager.decodeToken("not.a.token")).toBeNull();
|
|
186
|
+
(0, vitest.expect)(tokenManager.decodeToken("invalid-token")).toBeNull();
|
|
187
|
+
(0, vitest.expect)(tokenManager.getTokenExpiration("")).toBeNull();
|
|
188
|
+
(0, vitest.expect)(tokenManager.getTokenExpiration("not.a.token")).toBeNull();
|
|
189
|
+
(0, vitest.expect)(tokenManager.getTokenExpiration("invalid-token")).toBeNull();
|
|
190
|
+
(0, vitest.expect)(tokenManager.isTokenExpired("")).toBe(true);
|
|
191
|
+
(0, vitest.expect)(tokenManager.isTokenExpired("not.a.token")).toBe(true);
|
|
192
|
+
(0, vitest.expect)(tokenManager.isTokenExpired("invalid-token")).toBe(true);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
//#endregion
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { TokenManager } from "../tokenManager-BVXxfHRa.mjs";
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
//#region src/__tests__/tokenManager.spec.ts
|
|
5
|
+
describe("TokenManager", () => {
|
|
6
|
+
let tokenManager;
|
|
7
|
+
let userPayload;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
tokenManager = new TokenManager({
|
|
10
|
+
accessTokenSecret: "test-access-secret",
|
|
11
|
+
refreshTokenSecret: "test-refresh-secret",
|
|
12
|
+
accessTokenExpiresIn: "15m",
|
|
13
|
+
refreshTokenExpiresIn: "7d"
|
|
14
|
+
});
|
|
15
|
+
userPayload = {
|
|
16
|
+
userId: "user123",
|
|
17
|
+
email: "test@example.com",
|
|
18
|
+
role: "admin"
|
|
19
|
+
};
|
|
20
|
+
});
|
|
21
|
+
describe("token generation and verification workflow", () => {
|
|
22
|
+
it("should generate and verify complete token pair", () => {
|
|
23
|
+
const { accessToken, refreshToken } = tokenManager.generateTokenPair(userPayload);
|
|
24
|
+
const accessDecoded = tokenManager.verifyAccessToken(accessToken);
|
|
25
|
+
const refreshDecoded = tokenManager.verifyRefreshToken(refreshToken);
|
|
26
|
+
expect(accessDecoded.userId).toBe(userPayload.userId);
|
|
27
|
+
expect(accessDecoded.email).toBe(userPayload.email);
|
|
28
|
+
expect(accessDecoded.role).toBe(userPayload.role);
|
|
29
|
+
expect(refreshDecoded.userId).toBe(userPayload.userId);
|
|
30
|
+
expect(refreshDecoded.email).toBe(userPayload.email);
|
|
31
|
+
expect(refreshDecoded.role).toBe(userPayload.role);
|
|
32
|
+
});
|
|
33
|
+
it("should handle complete token refresh workflow", () => {
|
|
34
|
+
const { refreshToken } = tokenManager.generateTokenPair(userPayload);
|
|
35
|
+
const newAccessToken = tokenManager.refreshAccessToken(refreshToken);
|
|
36
|
+
const newDecoded = tokenManager.verifyAccessToken(newAccessToken);
|
|
37
|
+
expect(newDecoded.userId).toBe(userPayload.userId);
|
|
38
|
+
expect(newDecoded.email).toBe(userPayload.email);
|
|
39
|
+
expect(newDecoded.role).toBe(userPayload.role);
|
|
40
|
+
expect(newDecoded.iat).toBeDefined();
|
|
41
|
+
expect(newDecoded.exp).toBeDefined();
|
|
42
|
+
});
|
|
43
|
+
it("should maintain payload integrity through refresh cycle", () => {
|
|
44
|
+
const originalPayload = {
|
|
45
|
+
userId: "user456",
|
|
46
|
+
email: "user@example.com",
|
|
47
|
+
permissions: ["read", "write"],
|
|
48
|
+
metadata: { department: "engineering" }
|
|
49
|
+
};
|
|
50
|
+
const { refreshToken } = tokenManager.generateTokenPair(originalPayload);
|
|
51
|
+
const newAccessToken = tokenManager.refreshAccessToken(refreshToken);
|
|
52
|
+
const decoded = tokenManager.verifyAccessToken(newAccessToken);
|
|
53
|
+
expect(decoded.userId).toBe(originalPayload.userId);
|
|
54
|
+
expect(decoded.email).toBe(originalPayload.email);
|
|
55
|
+
expect(decoded.permissions).toEqual(originalPayload.permissions);
|
|
56
|
+
expect(decoded.metadata).toEqual(originalPayload.metadata);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
describe("token expiration handling", () => {
|
|
60
|
+
it("should correctly identify token expiration status", () => {
|
|
61
|
+
const { accessToken } = tokenManager.generateTokenPair(userPayload);
|
|
62
|
+
expect(tokenManager.isTokenExpired(accessToken)).toBe(false);
|
|
63
|
+
const expiration = tokenManager.getTokenExpiration(accessToken);
|
|
64
|
+
expect(expiration).toBeInstanceOf(Date);
|
|
65
|
+
expect(expiration.getTime()).toBeGreaterThan(Date.now());
|
|
66
|
+
});
|
|
67
|
+
it("should handle expired tokens correctly", async () => {
|
|
68
|
+
vi.useFakeTimers();
|
|
69
|
+
const shortLivedManager = new TokenManager({
|
|
70
|
+
accessTokenSecret: "test-access-secret",
|
|
71
|
+
refreshTokenSecret: "test-refresh-secret",
|
|
72
|
+
accessTokenExpiresIn: "10m"
|
|
73
|
+
});
|
|
74
|
+
const { accessToken } = shortLivedManager.generateTokenPair(userPayload);
|
|
75
|
+
vi.advanceTimersByTime(1e3 * 60 * 16);
|
|
76
|
+
expect(tokenManager.isTokenExpired(accessToken)).toBe(true);
|
|
77
|
+
vi.useRealTimers();
|
|
78
|
+
});
|
|
79
|
+
it("should decode tokens without verification", () => {
|
|
80
|
+
const { accessToken } = tokenManager.generateTokenPair(userPayload);
|
|
81
|
+
const decoded = tokenManager.decodeToken(accessToken);
|
|
82
|
+
expect(decoded).not.toBeNull();
|
|
83
|
+
expect(decoded.userId).toBe(userPayload.userId);
|
|
84
|
+
expect(decoded.email).toBe(userPayload.email);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
describe("security validation", () => {
|
|
88
|
+
it("should reject tokens with invalid signatures", () => {
|
|
89
|
+
const wrongSecretManager = new TokenManager({
|
|
90
|
+
accessTokenSecret: "wrong-secret",
|
|
91
|
+
refreshTokenSecret: "test-refresh-secret"
|
|
92
|
+
});
|
|
93
|
+
const { accessToken } = wrongSecretManager.generateTokenPair(userPayload);
|
|
94
|
+
expect(() => {
|
|
95
|
+
tokenManager.verifyAccessToken(accessToken);
|
|
96
|
+
}).toThrow("Invalid access token");
|
|
97
|
+
});
|
|
98
|
+
it("should reject malformed tokens", () => {
|
|
99
|
+
expect(() => {
|
|
100
|
+
tokenManager.verifyAccessToken("invalid-token");
|
|
101
|
+
}).toThrow("Invalid access token");
|
|
102
|
+
expect(() => {
|
|
103
|
+
tokenManager.verifyRefreshToken("invalid-token");
|
|
104
|
+
}).toThrow("Invalid refresh token");
|
|
105
|
+
expect(() => {
|
|
106
|
+
tokenManager.refreshAccessToken("invalid-token");
|
|
107
|
+
}).toThrow("Invalid refresh token");
|
|
108
|
+
});
|
|
109
|
+
it("should handle cross-secret validation correctly", () => {
|
|
110
|
+
const { accessToken, refreshToken } = tokenManager.generateTokenPair(userPayload);
|
|
111
|
+
expect(() => {
|
|
112
|
+
tokenManager.verifyRefreshToken(accessToken);
|
|
113
|
+
}).toThrow("Invalid refresh token");
|
|
114
|
+
expect(() => {
|
|
115
|
+
tokenManager.verifyAccessToken(refreshToken);
|
|
116
|
+
}).toThrow("Invalid access token");
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
describe("configuration flexibility", () => {
|
|
120
|
+
it("should support custom expiration times", () => {
|
|
121
|
+
const customManager = new TokenManager({
|
|
122
|
+
accessTokenSecret: "test-access-secret",
|
|
123
|
+
refreshTokenSecret: "test-refresh-secret",
|
|
124
|
+
accessTokenExpiresIn: "1h",
|
|
125
|
+
refreshTokenExpiresIn: "30d"
|
|
126
|
+
});
|
|
127
|
+
const { accessToken, refreshToken } = customManager.generateTokenPair(userPayload);
|
|
128
|
+
const accessDecoded = customManager.verifyAccessToken(accessToken);
|
|
129
|
+
const refreshDecoded = customManager.verifyRefreshToken(refreshToken);
|
|
130
|
+
const accessExpTime = (accessDecoded.exp - accessDecoded.iat) * 1e3;
|
|
131
|
+
const refreshExpTime = (refreshDecoded.exp - refreshDecoded.iat) * 1e3;
|
|
132
|
+
expect(accessExpTime).toBeGreaterThan(55 * 60 * 1e3);
|
|
133
|
+
expect(accessExpTime).toBeLessThan(65 * 60 * 1e3);
|
|
134
|
+
expect(refreshExpTime).toBeGreaterThan(29 * 24 * 60 * 60 * 1e3);
|
|
135
|
+
expect(refreshExpTime).toBeLessThan(31 * 24 * 60 * 60 * 1e3);
|
|
136
|
+
});
|
|
137
|
+
it("should use default expiration times", () => {
|
|
138
|
+
const defaultManager = new TokenManager({
|
|
139
|
+
accessTokenSecret: "test-access-secret",
|
|
140
|
+
refreshTokenSecret: "test-refresh-secret"
|
|
141
|
+
});
|
|
142
|
+
const { accessToken, refreshToken } = defaultManager.generateTokenPair(userPayload);
|
|
143
|
+
const accessDecoded = defaultManager.verifyAccessToken(accessToken);
|
|
144
|
+
const refreshDecoded = defaultManager.verifyRefreshToken(refreshToken);
|
|
145
|
+
const accessExpTime = (accessDecoded.exp - accessDecoded.iat) * 1e3;
|
|
146
|
+
const refreshExpTime = (refreshDecoded.exp - refreshDecoded.iat) * 1e3;
|
|
147
|
+
expect(accessExpTime).toBeGreaterThan(14 * 60 * 1e3);
|
|
148
|
+
expect(accessExpTime).toBeLessThan(16 * 60 * 1e3);
|
|
149
|
+
expect(refreshExpTime).toBeGreaterThan(6 * 24 * 60 * 60 * 1e3);
|
|
150
|
+
expect(refreshExpTime).toBeLessThan(8 * 24 * 60 * 60 * 1e3);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
describe("edge cases and error handling", () => {
|
|
154
|
+
it("should handle empty payload gracefully", () => {
|
|
155
|
+
const emptyPayload = { userId: "" };
|
|
156
|
+
const { accessToken, refreshToken } = tokenManager.generateTokenPair(emptyPayload);
|
|
157
|
+
const accessDecoded = tokenManager.verifyAccessToken(accessToken);
|
|
158
|
+
const refreshDecoded = tokenManager.verifyRefreshToken(refreshToken);
|
|
159
|
+
expect(accessDecoded.userId).toBe("");
|
|
160
|
+
expect(refreshDecoded.userId).toBe("");
|
|
161
|
+
});
|
|
162
|
+
it("should handle large payloads correctly", () => {
|
|
163
|
+
const largePayload = {
|
|
164
|
+
userId: "user123",
|
|
165
|
+
permissions: Array.from({ length: 100 }, (_, i) => `permission_${i}`),
|
|
166
|
+
metadata: {
|
|
167
|
+
department: "engineering",
|
|
168
|
+
team: "platform",
|
|
169
|
+
projects: Array.from({ length: 50 }, (_, i) => `project_${i}`)
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
const { accessToken, refreshToken } = tokenManager.generateTokenPair(largePayload);
|
|
173
|
+
const accessDecoded = tokenManager.verifyAccessToken(accessToken);
|
|
174
|
+
const refreshDecoded = tokenManager.verifyRefreshToken(refreshToken);
|
|
175
|
+
expect(accessDecoded.userId).toBe(largePayload.userId);
|
|
176
|
+
expect(accessDecoded.permissions).toEqual(largePayload.permissions);
|
|
177
|
+
expect(accessDecoded.metadata).toEqual(largePayload.metadata);
|
|
178
|
+
expect(refreshDecoded.userId).toBe(largePayload.userId);
|
|
179
|
+
expect(refreshDecoded.permissions).toEqual(largePayload.permissions);
|
|
180
|
+
expect(refreshDecoded.metadata).toEqual(largePayload.metadata);
|
|
181
|
+
});
|
|
182
|
+
it("should return null for completely invalid tokens", () => {
|
|
183
|
+
expect(tokenManager.decodeToken("")).toBeNull();
|
|
184
|
+
expect(tokenManager.decodeToken("not.a.token")).toBeNull();
|
|
185
|
+
expect(tokenManager.decodeToken("invalid-token")).toBeNull();
|
|
186
|
+
expect(tokenManager.getTokenExpiration("")).toBeNull();
|
|
187
|
+
expect(tokenManager.getTokenExpiration("not.a.token")).toBeNull();
|
|
188
|
+
expect(tokenManager.getTokenExpiration("invalid-token")).toBeNull();
|
|
189
|
+
expect(tokenManager.isTokenExpired("")).toBe(true);
|
|
190
|
+
expect(tokenManager.isTokenExpired("not.a.token")).toBe(true);
|
|
191
|
+
expect(tokenManager.isTokenExpired("invalid-token")).toBe(true);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
//#endregion
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
//#region src/cacheTokenStorage.ts
|
|
2
|
+
var CacheTokenStorage = class {
|
|
3
|
+
cache;
|
|
4
|
+
accessTokenKey;
|
|
5
|
+
refreshTokenKey;
|
|
6
|
+
constructor(cache, accessTokenKey = "access_token", refreshTokenKey = "refresh_token") {
|
|
7
|
+
this.cache = cache;
|
|
8
|
+
this.accessTokenKey = accessTokenKey;
|
|
9
|
+
this.refreshTokenKey = refreshTokenKey;
|
|
10
|
+
}
|
|
11
|
+
async getAccessToken() {
|
|
12
|
+
const token = await this.cache.get(this.accessTokenKey);
|
|
13
|
+
return token || null;
|
|
14
|
+
}
|
|
15
|
+
async setAccessToken(token, ttl) {
|
|
16
|
+
await this.cache.set(this.accessTokenKey, token, ttl);
|
|
17
|
+
}
|
|
18
|
+
async getRefreshToken() {
|
|
19
|
+
const token = await this.cache.get(this.refreshTokenKey);
|
|
20
|
+
return token || null;
|
|
21
|
+
}
|
|
22
|
+
async setRefreshToken(token, ttl) {
|
|
23
|
+
await this.cache.set(this.refreshTokenKey, token, ttl);
|
|
24
|
+
}
|
|
25
|
+
async clearTokens() {
|
|
26
|
+
await Promise.all([this.cache.delete(this.accessTokenKey), this.cache.delete(this.refreshTokenKey)]);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
//#endregion
|
|
31
|
+
export { CacheTokenStorage };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
|
|
2
|
+
//#region src/cacheTokenStorage.ts
|
|
3
|
+
var CacheTokenStorage = class {
|
|
4
|
+
cache;
|
|
5
|
+
accessTokenKey;
|
|
6
|
+
refreshTokenKey;
|
|
7
|
+
constructor(cache, accessTokenKey = "access_token", refreshTokenKey = "refresh_token") {
|
|
8
|
+
this.cache = cache;
|
|
9
|
+
this.accessTokenKey = accessTokenKey;
|
|
10
|
+
this.refreshTokenKey = refreshTokenKey;
|
|
11
|
+
}
|
|
12
|
+
async getAccessToken() {
|
|
13
|
+
const token = await this.cache.get(this.accessTokenKey);
|
|
14
|
+
return token || null;
|
|
15
|
+
}
|
|
16
|
+
async setAccessToken(token, ttl) {
|
|
17
|
+
await this.cache.set(this.accessTokenKey, token, ttl);
|
|
18
|
+
}
|
|
19
|
+
async getRefreshToken() {
|
|
20
|
+
const token = await this.cache.get(this.refreshTokenKey);
|
|
21
|
+
return token || null;
|
|
22
|
+
}
|
|
23
|
+
async setRefreshToken(token, ttl) {
|
|
24
|
+
await this.cache.set(this.refreshTokenKey, token, ttl);
|
|
25
|
+
}
|
|
26
|
+
async clearTokens() {
|
|
27
|
+
await Promise.all([this.cache.delete(this.accessTokenKey), this.cache.delete(this.refreshTokenKey)]);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
//#endregion
|
|
32
|
+
Object.defineProperty(exports, 'CacheTokenStorage', {
|
|
33
|
+
enumerable: true,
|
|
34
|
+
get: function () {
|
|
35
|
+
return CacheTokenStorage;
|
|
36
|
+
}
|
|
37
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
//#region rolldown:runtime
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __commonJS = (cb, mod) => function() {
|
|
9
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
13
|
+
key = keys[i];
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
15
|
+
get: ((k) => from[k]).bind(null, key),
|
|
16
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
return to;
|
|
20
|
+
};
|
|
21
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
22
|
+
value: mod,
|
|
23
|
+
enumerable: true
|
|
24
|
+
}) : target, mod));
|
|
25
|
+
|
|
26
|
+
//#endregion
|
|
27
|
+
|
|
28
|
+
Object.defineProperty(exports, '__commonJS', {
|
|
29
|
+
enumerable: true,
|
|
30
|
+
get: function () {
|
|
31
|
+
return __commonJS;
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
Object.defineProperty(exports, '__toESM', {
|
|
35
|
+
enumerable: true,
|
|
36
|
+
get: function () {
|
|
37
|
+
return __toESM;
|
|
38
|
+
}
|
|
39
|
+
});
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
|
|
2
|
+
//#region src/client.ts
|
|
3
|
+
var LocalStorageTokenStorage = class {
|
|
4
|
+
accessTokenKey;
|
|
5
|
+
refreshTokenKey;
|
|
6
|
+
constructor(accessTokenKey = "access_token", refreshTokenKey = "refresh_token") {
|
|
7
|
+
this.accessTokenKey = accessTokenKey;
|
|
8
|
+
this.refreshTokenKey = refreshTokenKey;
|
|
9
|
+
}
|
|
10
|
+
getAccessToken() {
|
|
11
|
+
if (typeof window === "undefined") return null;
|
|
12
|
+
return localStorage.getItem(this.accessTokenKey);
|
|
13
|
+
}
|
|
14
|
+
setAccessToken(token, ttl) {
|
|
15
|
+
if (typeof window === "undefined") return;
|
|
16
|
+
localStorage.setItem(this.accessTokenKey, token);
|
|
17
|
+
}
|
|
18
|
+
getRefreshToken() {
|
|
19
|
+
if (typeof window === "undefined") return null;
|
|
20
|
+
return localStorage.getItem(this.refreshTokenKey);
|
|
21
|
+
}
|
|
22
|
+
setRefreshToken(token, ttl) {
|
|
23
|
+
if (typeof window === "undefined") return;
|
|
24
|
+
localStorage.setItem(this.refreshTokenKey, token);
|
|
25
|
+
}
|
|
26
|
+
clearTokens() {
|
|
27
|
+
if (typeof window === "undefined") return;
|
|
28
|
+
localStorage.removeItem(this.accessTokenKey);
|
|
29
|
+
localStorage.removeItem(this.refreshTokenKey);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
var MemoryTokenStorage = class {
|
|
33
|
+
accessToken = null;
|
|
34
|
+
refreshToken = null;
|
|
35
|
+
getAccessToken() {
|
|
36
|
+
return this.accessToken;
|
|
37
|
+
}
|
|
38
|
+
setAccessToken(token, ttl) {
|
|
39
|
+
this.accessToken = token;
|
|
40
|
+
}
|
|
41
|
+
getRefreshToken() {
|
|
42
|
+
return this.refreshToken;
|
|
43
|
+
}
|
|
44
|
+
setRefreshToken(token, ttl) {
|
|
45
|
+
this.refreshToken = token;
|
|
46
|
+
}
|
|
47
|
+
clearTokens() {
|
|
48
|
+
this.accessToken = null;
|
|
49
|
+
this.refreshToken = null;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
var TokenClient = class {
|
|
53
|
+
storage;
|
|
54
|
+
refreshEndpoint;
|
|
55
|
+
onTokenRefresh;
|
|
56
|
+
onTokenExpired;
|
|
57
|
+
constructor(options = {}) {
|
|
58
|
+
this.storage = options.storage || new LocalStorageTokenStorage();
|
|
59
|
+
this.refreshEndpoint = options.refreshEndpoint;
|
|
60
|
+
this.onTokenRefresh = options.onTokenRefresh;
|
|
61
|
+
this.onTokenExpired = options.onTokenExpired;
|
|
62
|
+
}
|
|
63
|
+
async getAccessToken() {
|
|
64
|
+
return await this.storage.getAccessToken();
|
|
65
|
+
}
|
|
66
|
+
async getRefreshToken() {
|
|
67
|
+
return await this.storage.getRefreshToken();
|
|
68
|
+
}
|
|
69
|
+
async setTokens(accessToken, refreshToken, accessTtl, refreshTtl) {
|
|
70
|
+
await this.storage.setAccessToken(accessToken, accessTtl);
|
|
71
|
+
if (refreshToken) await this.storage.setRefreshToken(refreshToken, refreshTtl);
|
|
72
|
+
}
|
|
73
|
+
async clearTokens() {
|
|
74
|
+
await this.storage.clearTokens();
|
|
75
|
+
}
|
|
76
|
+
isTokenExpired(token) {
|
|
77
|
+
try {
|
|
78
|
+
const payload = JSON.parse(atob(token.split(".")[1]));
|
|
79
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
80
|
+
return payload.exp < now;
|
|
81
|
+
} catch {
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
getTokenExpiration(token) {
|
|
86
|
+
try {
|
|
87
|
+
const payload = JSON.parse(atob(token.split(".")[1]));
|
|
88
|
+
return new Date(payload.exp * 1e3);
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async refreshTokens() {
|
|
94
|
+
const refreshToken = await this.getRefreshToken();
|
|
95
|
+
if (!refreshToken || !this.refreshEndpoint) return false;
|
|
96
|
+
try {
|
|
97
|
+
const response = await fetch(this.refreshEndpoint, {
|
|
98
|
+
method: "POST",
|
|
99
|
+
headers: { "Content-Type": "application/json" },
|
|
100
|
+
body: JSON.stringify({ refreshToken })
|
|
101
|
+
});
|
|
102
|
+
if (!response.ok) throw new Error("Failed to refresh token");
|
|
103
|
+
const data = await response.json();
|
|
104
|
+
const { accessToken, refreshToken: newRefreshToken } = data;
|
|
105
|
+
await this.setTokens(accessToken, newRefreshToken);
|
|
106
|
+
if (this.onTokenRefresh) this.onTokenRefresh({
|
|
107
|
+
accessToken,
|
|
108
|
+
refreshToken: newRefreshToken
|
|
109
|
+
});
|
|
110
|
+
return true;
|
|
111
|
+
} catch (error) {
|
|
112
|
+
await this.clearTokens();
|
|
113
|
+
if (this.onTokenExpired) this.onTokenExpired();
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
async getValidAccessToken() {
|
|
118
|
+
const accessToken = await this.getAccessToken();
|
|
119
|
+
if (!accessToken) return null;
|
|
120
|
+
if (this.isTokenExpired(accessToken)) {
|
|
121
|
+
const refreshed = await this.refreshTokens();
|
|
122
|
+
if (!refreshed) return null;
|
|
123
|
+
return await this.getAccessToken();
|
|
124
|
+
}
|
|
125
|
+
return accessToken;
|
|
126
|
+
}
|
|
127
|
+
async createAuthHeaders() {
|
|
128
|
+
const token = await this.getAccessToken();
|
|
129
|
+
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
130
|
+
}
|
|
131
|
+
async createValidAuthHeaders() {
|
|
132
|
+
const token = await this.getValidAccessToken();
|
|
133
|
+
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
//#endregion
|
|
138
|
+
Object.defineProperty(exports, 'LocalStorageTokenStorage', {
|
|
139
|
+
enumerable: true,
|
|
140
|
+
get: function () {
|
|
141
|
+
return LocalStorageTokenStorage;
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
Object.defineProperty(exports, 'MemoryTokenStorage', {
|
|
145
|
+
enumerable: true,
|
|
146
|
+
get: function () {
|
|
147
|
+
return MemoryTokenStorage;
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
Object.defineProperty(exports, 'TokenClient', {
|
|
151
|
+
enumerable: true,
|
|
152
|
+
get: function () {
|
|
153
|
+
return TokenClient;
|
|
154
|
+
}
|
|
155
|
+
});
|