@restingowlorg/owlauth 1.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.
Files changed (69) hide show
  1. package/LICENSE +373 -0
  2. package/README.md +307 -0
  3. package/dist/config.d.ts +13 -0
  4. package/dist/config.js +16 -0
  5. package/dist/core/auth.manager.d.ts +6 -0
  6. package/dist/core/auth.manager.js +21 -0
  7. package/dist/core/auth.service.init.d.ts +2 -0
  8. package/dist/core/auth.service.init.js +26 -0
  9. package/dist/index.d.ts +5 -0
  10. package/dist/index.js +11 -0
  11. package/dist/infra/databases/mongo/adapter.d.ts +6 -0
  12. package/dist/infra/databases/mongo/adapter.js +24 -0
  13. package/dist/infra/databases/mongo/db.d.ts +5 -0
  14. package/dist/infra/databases/mongo/db.js +43 -0
  15. package/dist/infra/databases/mongo/mongo.d.ts +5 -0
  16. package/dist/infra/databases/mongo/mongo.js +11 -0
  17. package/dist/infra/databases/postgresql/adapter.d.ts +6 -0
  18. package/dist/infra/databases/postgresql/adapter.js +26 -0
  19. package/dist/infra/databases/postgresql/db.d.ts +5 -0
  20. package/dist/infra/databases/postgresql/db.js +50 -0
  21. package/dist/infra/databases/postgresql/helpers.d.ts +8 -0
  22. package/dist/infra/databases/postgresql/helpers.js +55 -0
  23. package/dist/infra/databases/postgresql/postgres.d.ts +5 -0
  24. package/dist/infra/databases/postgresql/postgres.js +11 -0
  25. package/dist/infra/databases/postgresql/schema.d.ts +6 -0
  26. package/dist/infra/databases/postgresql/schema.js +9 -0
  27. package/dist/infra/security/bcrypt.adapter.d.ts +9 -0
  28. package/dist/infra/security/bcrypt.adapter.js +62 -0
  29. package/dist/infra/security/bcrypt.adapter.test.d.ts +1 -0
  30. package/dist/infra/security/bcrypt.adapter.test.js +67 -0
  31. package/dist/infra/security/pwned-passwords.d.ts +5 -0
  32. package/dist/infra/security/pwned-passwords.js +45 -0
  33. package/dist/infra/security/pwned-passwords.test.d.ts +1 -0
  34. package/dist/infra/security/pwned-passwords.test.js +62 -0
  35. package/dist/infra/security/security-audit-logger.d.ts +11 -0
  36. package/dist/infra/security/security-audit-logger.js +90 -0
  37. package/dist/repositories/contracts.d.ts +21 -0
  38. package/dist/repositories/contracts.js +2 -0
  39. package/dist/repositories/mongo/magicLink.repo.d.ts +26 -0
  40. package/dist/repositories/mongo/magicLink.repo.js +106 -0
  41. package/dist/repositories/mongo/user.repo.d.ts +16 -0
  42. package/dist/repositories/mongo/user.repo.js +84 -0
  43. package/dist/repositories/postgresql/magic.link.repo.d.ts +21 -0
  44. package/dist/repositories/postgresql/magic.link.repo.js +97 -0
  45. package/dist/repositories/postgresql/user.repo.d.ts +14 -0
  46. package/dist/repositories/postgresql/user.repo.js +50 -0
  47. package/dist/services/auth.service.d.ts +22 -0
  48. package/dist/services/auth.service.js +362 -0
  49. package/dist/services/auth.service.test.d.ts +1 -0
  50. package/dist/services/auth.service.test.js +297 -0
  51. package/dist/services/magic-link.service.d.ts +22 -0
  52. package/dist/services/magic-link.service.js +196 -0
  53. package/dist/services/magic-link.service.test.d.ts +1 -0
  54. package/dist/services/magic-link.service.test.js +230 -0
  55. package/dist/strategies/CredentialsStrategy.d.ts +4 -0
  56. package/dist/strategies/CredentialsStrategy.js +32 -0
  57. package/dist/strategies/CredentialsStrategy.test.d.ts +1 -0
  58. package/dist/strategies/CredentialsStrategy.test.js +29 -0
  59. package/dist/strategies/MagicLinkStrategy.d.ts +4 -0
  60. package/dist/strategies/MagicLinkStrategy.js +21 -0
  61. package/dist/strategies/MagicLinkStrategy.test.d.ts +1 -0
  62. package/dist/strategies/MagicLinkStrategy.test.js +38 -0
  63. package/dist/types/index.d.ts +224 -0
  64. package/dist/types/index.js +2 -0
  65. package/dist/utils/check-blocked-passwords.d.ts +1 -0
  66. package/dist/utils/check-blocked-passwords.js +10 -0
  67. package/dist/utils/check-blocked-passwords.test.d.ts +1 -0
  68. package/dist/utils/check-blocked-passwords.test.js +27 -0
  69. package/package.json +102 -0
@@ -0,0 +1,297 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const auth_service_1 = require("./auth.service");
4
+ const core_1 = require("@zxcvbn-ts/core");
5
+ const pwned_passwords_1 = require("../infra/security/pwned-passwords");
6
+ const check_blocked_passwords_1 = require("../utils/check-blocked-passwords");
7
+ // Mock dependencies
8
+ jest.mock("@zxcvbn-ts/core");
9
+ jest.mock("../infra/security/pwned-passwords");
10
+ jest.mock("../utils/check-blocked-passwords");
11
+ describe("AuthService", () => {
12
+ let authService;
13
+ let mockUserRepo;
14
+ let mockCrypto;
15
+ let mockLogger;
16
+ beforeEach(() => {
17
+ mockUserRepo = {
18
+ findById: jest.fn(),
19
+ findByEmail: jest.fn(),
20
+ findByUsername: jest.fn(),
21
+ create: jest.fn(),
22
+ updatePassword: jest.fn()
23
+ };
24
+ mockCrypto = {
25
+ hashPassword: jest.fn(),
26
+ verifyPassword: jest.fn(),
27
+ generateToken: jest.fn(),
28
+ hashToken: jest.fn(),
29
+ verifyToken: jest.fn()
30
+ };
31
+ mockLogger = {
32
+ audit: jest.fn(),
33
+ info: jest.fn(),
34
+ warn: jest.fn(),
35
+ error: jest.fn()
36
+ };
37
+ authService = new auth_service_1.AuthService(mockUserRepo, mockCrypto, mockLogger);
38
+ });
39
+ describe("signup", () => {
40
+ const signupData = {
41
+ email: "test@example.com",
42
+ username: "testuser",
43
+ password: "Password123!"
44
+ };
45
+ it("should successfully sign up a user", async () => {
46
+ check_blocked_passwords_1.containsBlockedPasswords.mockReturnValue(false);
47
+ core_1.zxcvbn.mockReturnValue({ score: 4 });
48
+ pwned_passwords_1.isBreachedPassword.mockResolvedValue({ detected: false });
49
+ mockUserRepo.findByUsername.mockResolvedValue(null);
50
+ mockUserRepo.findByEmail.mockResolvedValue(null);
51
+ mockCrypto.hashPassword.mockResolvedValue("hashed_password");
52
+ mockUserRepo.create.mockResolvedValue({
53
+ id: "1",
54
+ email: signupData.email,
55
+ username: signupData.username
56
+ });
57
+ const result = await authService.signup(signupData.email, signupData.username, signupData.password);
58
+ expect(result.success).toBe(true);
59
+ expect(result.httpCode).toBe(201);
60
+ // eslint-disable-next-line @typescript-eslint/unbound-method
61
+ expect(mockLogger.audit).toHaveBeenCalledWith(expect.objectContaining({ type: "SIGNUP" }));
62
+ });
63
+ it("should fail if required fields are missing", async () => {
64
+ const result = await authService.signup("", "", "");
65
+ expect(result.success).toBe(false);
66
+ expect(result.httpCode).toBe(400);
67
+ // eslint-disable-next-line @typescript-eslint/unbound-method
68
+ expect(mockLogger.audit).toHaveBeenCalledWith(expect.objectContaining({ type: "SIGNUP_FAILURE" }));
69
+ });
70
+ it("should fail if username format is invalid", async () => {
71
+ const result = await authService.signup("test@example.com", "us", "Password123!");
72
+ expect(result.success).toBe(false);
73
+ expect(result.httpCode).toBe(400);
74
+ // eslint-disable-next-line @typescript-eslint/unbound-method
75
+ expect(mockLogger.audit).toHaveBeenCalledWith(expect.objectContaining({ type: "SIGNUP_FAILURE" }));
76
+ });
77
+ it("should fail if password contains blocked terms", async () => {
78
+ check_blocked_passwords_1.containsBlockedPasswords.mockReturnValue(true);
79
+ const result = await authService.signup(signupData.email, signupData.username, "blockedpassword");
80
+ expect(result.success).toBe(false);
81
+ expect(result.httpCode).toBe(400);
82
+ });
83
+ it("should fail if password is too weak", async () => {
84
+ check_blocked_passwords_1.containsBlockedPasswords.mockReturnValue(false);
85
+ core_1.zxcvbn.mockReturnValue({ score: 1 });
86
+ const result = await authService.signup(signupData.email, signupData.username, "weak");
87
+ expect(result.success).toBe(false);
88
+ expect(result.httpCode).toBe(400);
89
+ });
90
+ it("should fail if password is found in a data breach", async () => {
91
+ check_blocked_passwords_1.containsBlockedPasswords.mockReturnValue(false);
92
+ core_1.zxcvbn.mockReturnValue({ score: 4 });
93
+ pwned_passwords_1.isBreachedPassword.mockResolvedValue({ detected: true });
94
+ const result = await authService.signup(signupData.email, signupData.username, "breachedpassword");
95
+ expect(result.success).toBe(false);
96
+ expect(result.httpCode).toBe(400);
97
+ });
98
+ it("should fail if username is already taken", async () => {
99
+ check_blocked_passwords_1.containsBlockedPasswords.mockReturnValue(false);
100
+ core_1.zxcvbn.mockReturnValue({ score: 4 });
101
+ pwned_passwords_1.isBreachedPassword.mockResolvedValue({ detected: false });
102
+ mockUserRepo.findByUsername.mockResolvedValue({ id: "1" });
103
+ const result = await authService.signup(signupData.email, signupData.username, signupData.password);
104
+ expect(result.success).toBe(false);
105
+ expect(result.httpCode).toBe(400);
106
+ expect(result.message).toBe("Username already taken.");
107
+ });
108
+ it("should fail if email is already registered", async () => {
109
+ check_blocked_passwords_1.containsBlockedPasswords.mockReturnValue(false);
110
+ core_1.zxcvbn.mockReturnValue({ score: 4 });
111
+ pwned_passwords_1.isBreachedPassword.mockResolvedValue({ detected: false });
112
+ mockUserRepo.findByUsername.mockResolvedValue(null);
113
+ mockUserRepo.findByEmail.mockResolvedValue({ id: "1" });
114
+ const result = await authService.signup(signupData.email, signupData.username, signupData.password);
115
+ expect(result.success).toBe(false);
116
+ expect(result.httpCode).toBe(400);
117
+ expect(result.message).toBe("Email already registered.");
118
+ });
119
+ it("should return 503 SERVICE_UNAVAILABLE if pwned check fails and pwnedPasswordFailClosed is true", async () => {
120
+ check_blocked_passwords_1.containsBlockedPasswords.mockReturnValue(false);
121
+ core_1.zxcvbn.mockReturnValue({ score: 4 });
122
+ pwned_passwords_1.isBreachedPassword.mockResolvedValue({
123
+ detected: false,
124
+ error: new Error("HIBP API Down")
125
+ });
126
+ const result = await authService.signup(signupData.email, signupData.username, signupData.password, { pwnedPasswordFailClosed: true });
127
+ expect(result.success).toBe(false);
128
+ expect(result.httpCode).toBe(503);
129
+ // eslint-disable-next-line @typescript-eslint/unbound-method
130
+ expect(mockLogger.audit).toHaveBeenCalledWith(expect.objectContaining({
131
+ metadata: expect.objectContaining({
132
+ reason: expect.stringContaining("Fail-Closed")
133
+ })
134
+ }));
135
+ });
136
+ it("should propagate correlationId to auditLogger during signup", async () => {
137
+ check_blocked_passwords_1.containsBlockedPasswords.mockReturnValue(false);
138
+ core_1.zxcvbn.mockReturnValue({ score: 4 });
139
+ pwned_passwords_1.isBreachedPassword.mockResolvedValue({ detected: false });
140
+ mockUserRepo.findByUsername.mockResolvedValue(null);
141
+ mockUserRepo.findByEmail.mockResolvedValue(null);
142
+ mockCrypto.hashPassword.mockResolvedValue("hashed");
143
+ mockUserRepo.create.mockResolvedValue({
144
+ id: "1",
145
+ email: signupData.email,
146
+ username: signupData.username
147
+ });
148
+ const correlationId = "test-corr-id";
149
+ await authService.signup(signupData.email, signupData.username, signupData.password, {
150
+ correlationId
151
+ });
152
+ // eslint-disable-next-line @typescript-eslint/unbound-method
153
+ expect(mockLogger.audit).toHaveBeenCalledWith(expect.objectContaining({ correlationId }));
154
+ });
155
+ });
156
+ describe("login", () => {
157
+ const loginData = {
158
+ email: "test@example.com",
159
+ password: "Password123!"
160
+ };
161
+ it("should successfully log in a user", async () => {
162
+ mockUserRepo.findByEmail.mockResolvedValue({
163
+ id: "1",
164
+ email: loginData.email,
165
+ password: "hashed_password"
166
+ });
167
+ mockCrypto.verifyPassword.mockResolvedValue(true);
168
+ const result = await authService.login(loginData.email, loginData.password);
169
+ expect(result.success).toBe(true);
170
+ expect(result.httpCode).toBe(200);
171
+ // eslint-disable-next-line @typescript-eslint/unbound-method
172
+ expect(mockLogger.audit).toHaveBeenCalledWith(expect.objectContaining({ type: "LOGIN_SUCCESS" }));
173
+ });
174
+ it("should fail if credentials are missing", async () => {
175
+ const result = await authService.login("", "");
176
+ expect(result.success).toBe(false);
177
+ expect(result.httpCode).toBe(400);
178
+ });
179
+ it("should fail if user not found", async () => {
180
+ mockUserRepo.findByEmail.mockResolvedValue(null);
181
+ const result = await authService.login(loginData.email, loginData.password);
182
+ expect(result.success).toBe(false);
183
+ expect(result.httpCode).toBe(401);
184
+ // eslint-disable-next-line @typescript-eslint/unbound-method
185
+ expect(mockLogger.audit).toHaveBeenCalledWith(expect.objectContaining({ type: "LOGIN_FAILURE", metadata: { reason: "User not found" } }));
186
+ });
187
+ it("should fail if password does not match", async () => {
188
+ mockUserRepo.findByEmail.mockResolvedValue({
189
+ id: "1",
190
+ email: loginData.email,
191
+ password: "hashed_password"
192
+ });
193
+ mockCrypto.verifyPassword.mockResolvedValue(false);
194
+ const result = await authService.login(loginData.email, loginData.password);
195
+ expect(result.success).toBe(false);
196
+ expect(result.httpCode).toBe(401);
197
+ // eslint-disable-next-line @typescript-eslint/unbound-method
198
+ expect(mockLogger.audit).toHaveBeenCalledWith(expect.objectContaining({ type: "LOGIN_FAILURE", metadata: { reason: "Invalid password" } }));
199
+ });
200
+ it("should propagate correlationId to auditLogger and error logs during login", async () => {
201
+ const email = "test@example.com";
202
+ const correlationId = "login-trace-id";
203
+ // 1. Audit log on failure
204
+ mockUserRepo.findByEmail.mockResolvedValue(null);
205
+ await authService.login(email, "pass", { correlationId });
206
+ // eslint-disable-next-line @typescript-eslint/unbound-method
207
+ expect(mockLogger.audit).toHaveBeenCalledWith(expect.objectContaining({ type: "LOGIN_FAILURE", correlationId }));
208
+ // 2. Error log on exception
209
+ const error = new Error("DB Error");
210
+ mockUserRepo.findByEmail.mockRejectedValue(error);
211
+ await authService.login(email, "pass", { correlationId });
212
+ // eslint-disable-next-line @typescript-eslint/unbound-method
213
+ expect(mockLogger.error).toHaveBeenCalledWith("Login exception", error, { email }, correlationId);
214
+ });
215
+ });
216
+ describe("changePassword", () => {
217
+ const changePwdData = {
218
+ userId: "1",
219
+ currentPassword: "OldPassword123!",
220
+ newPassword: "NewPassword123!"
221
+ };
222
+ it("should successfully change password", async () => {
223
+ mockUserRepo.findById.mockResolvedValue({
224
+ id: "1",
225
+ email: "test@example.com",
226
+ username: "testuser",
227
+ password: "old_hashed_password"
228
+ });
229
+ mockCrypto.verifyPassword.mockResolvedValue(true);
230
+ check_blocked_passwords_1.containsBlockedPasswords.mockReturnValue(false);
231
+ core_1.zxcvbn.mockReturnValue({ score: 4 });
232
+ pwned_passwords_1.isBreachedPassword.mockResolvedValue({ detected: false });
233
+ mockCrypto.hashPassword.mockResolvedValue("new_hashed_password");
234
+ mockUserRepo.updatePassword.mockResolvedValue(true);
235
+ const result = await authService.changePassword(changePwdData.userId, changePwdData.currentPassword, changePwdData.newPassword);
236
+ expect(result.success).toBe(true);
237
+ expect(result.httpCode).toBe(200);
238
+ // eslint-disable-next-line @typescript-eslint/unbound-method
239
+ expect(mockLogger.audit).toHaveBeenCalledWith(expect.objectContaining({ type: "PASSWORD_CHANGE" }));
240
+ });
241
+ it("should fail if user not found", async () => {
242
+ mockUserRepo.findById.mockResolvedValue(null);
243
+ const result = await authService.changePassword("99", "pass", "newpass");
244
+ expect(result.success).toBe(false);
245
+ expect(result.httpCode).toBe(404);
246
+ });
247
+ it("should fail if current password is incorrect", async () => {
248
+ mockUserRepo.findById.mockResolvedValue({
249
+ id: "1",
250
+ password: "hashed_password"
251
+ });
252
+ mockCrypto.verifyPassword.mockResolvedValue(false);
253
+ const result = await authService.changePassword("1", "wrong", "newpass");
254
+ expect(result.success).toBe(false);
255
+ expect(result.httpCode).toBe(401);
256
+ });
257
+ it("should fail if new password is too weak", async () => {
258
+ mockUserRepo.findById.mockResolvedValue({
259
+ id: "1",
260
+ password: "hashed_password"
261
+ });
262
+ mockCrypto.verifyPassword.mockResolvedValue(true);
263
+ check_blocked_passwords_1.containsBlockedPasswords.mockReturnValue(false);
264
+ core_1.zxcvbn.mockReturnValue({ score: 1 });
265
+ const result = await authService.changePassword("1", "old", "weak");
266
+ expect(result.success).toBe(false);
267
+ expect(result.httpCode).toBe(400);
268
+ });
269
+ it("should return 503 SERVICE_UNAVAILABLE during password change if fail-closed is enabled and check fails", async () => {
270
+ mockUserRepo.findById.mockResolvedValue({
271
+ id: "1",
272
+ email: "test@example.com",
273
+ username: "testuser",
274
+ password: "old"
275
+ });
276
+ mockCrypto.verifyPassword.mockResolvedValue(true);
277
+ check_blocked_passwords_1.containsBlockedPasswords.mockReturnValue(false);
278
+ core_1.zxcvbn.mockReturnValue({ score: 4 });
279
+ pwned_passwords_1.isBreachedPassword.mockResolvedValue({
280
+ detected: false,
281
+ error: new Error("Network Error")
282
+ });
283
+ const result = await authService.changePassword("1", "old", "new", {
284
+ pwnedPasswordFailClosed: true
285
+ });
286
+ expect(result.success).toBe(false);
287
+ expect(result.httpCode).toBe(503);
288
+ });
289
+ it("should propagate correlationId to all logs during password change", async () => {
290
+ const correlationId = "change-pwd-trace";
291
+ mockUserRepo.findById.mockRejectedValue(new Error("DB Error")); // Force exception for error log check
292
+ await authService.changePassword("1", "old", "new", { correlationId });
293
+ // eslint-disable-next-line @typescript-eslint/unbound-method
294
+ expect(mockLogger.error).toHaveBeenCalledWith(expect.any(String), expect.any(Error), undefined, correlationId);
295
+ });
296
+ });
297
+ });
@@ -0,0 +1,22 @@
1
+ import { UserRepository, MagicLinkRepository } from "../repositories/contracts";
2
+ import { ICryptoAdapter, IAuditLogger } from "../types";
3
+ import { AuthResult, RequestMagicLinkResult, VerifyMagicLinkResult, ConsumeMagicLinkResult } from "../types/index";
4
+ export declare class MagicLinkService {
5
+ private users;
6
+ private magicLinks;
7
+ private crypto;
8
+ private logger;
9
+ constructor(users: UserRepository, magicLinks: MagicLinkRepository, crypto: ICryptoAdapter, logger: IAuditLogger);
10
+ /** Request a magic link (passwordless login) */
11
+ request(email: string, options?: {
12
+ correlationId?: string;
13
+ }): Promise<AuthResult<RequestMagicLinkResult>>;
14
+ /** Verify a magic link token without consuming it */
15
+ verify(token: string, options?: {
16
+ correlationId?: string;
17
+ }): Promise<AuthResult<VerifyMagicLinkResult>>;
18
+ /** Consume a magic link token */
19
+ consume(token: string, options?: {
20
+ correlationId?: string;
21
+ }): Promise<AuthResult<ConsumeMagicLinkResult>>;
22
+ }
@@ -0,0 +1,196 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MagicLinkService = void 0;
4
+ class MagicLinkService {
5
+ constructor(users, magicLinks, crypto, logger) {
6
+ this.users = users;
7
+ this.magicLinks = magicLinks;
8
+ this.crypto = crypto;
9
+ this.logger = logger;
10
+ }
11
+ /** Request a magic link (passwordless login) */
12
+ async request(email, options) {
13
+ try {
14
+ const user = await this.users.findByEmail(email);
15
+ if (!user) {
16
+ this.logger.audit({
17
+ type: "MAGIC_LINK_FAILURE",
18
+ email,
19
+ metadata: { reason: "User not found" },
20
+ correlationId: options === null || options === void 0 ? void 0 : options.correlationId
21
+ });
22
+ return {
23
+ success: false,
24
+ data: undefined,
25
+ message: "User not found",
26
+ httpCode: 404
27
+ };
28
+ }
29
+ const token = this.crypto.generateToken();
30
+ const tokenHash = await this.crypto.hashToken(token);
31
+ // invalidate existing tokens for this user
32
+ const invalidated = await this.magicLinks.invalidateByUserId(user.id);
33
+ if (!invalidated) {
34
+ this.logger.error("Failed to invalidate magic links", new Error("DB update failed"), undefined, options === null || options === void 0 ? void 0 : options.correlationId);
35
+ return {
36
+ success: false,
37
+ data: undefined,
38
+ message: "Failed to invalidate existing magic links",
39
+ httpCode: 500
40
+ };
41
+ }
42
+ // create new token
43
+ const record = await this.magicLinks.create({
44
+ userId: user.id,
45
+ tokenHash,
46
+ expiresAt: new Date(Date.now() + 15 * 60 * 1000)
47
+ });
48
+ if (!record) {
49
+ this.logger.error("Failed to create magic link", new Error("DB insert failed"), undefined, options === null || options === void 0 ? void 0 : options.correlationId);
50
+ return {
51
+ success: false,
52
+ data: undefined,
53
+ message: "Failed to create magic link",
54
+ httpCode: 500
55
+ };
56
+ }
57
+ this.logger.audit({
58
+ type: "MAGIC_LINK_REQUESTED",
59
+ email: user.email,
60
+ correlationId: options === null || options === void 0 ? void 0 : options.correlationId
61
+ });
62
+ return {
63
+ success: true,
64
+ data: `${record.id}.${token}`,
65
+ message: "Magic link created",
66
+ httpCode: 200
67
+ };
68
+ }
69
+ catch (err) {
70
+ const message = err instanceof Error ? err.message : "Unknown error";
71
+ this.logger.error("Magic link request exception", err, { email }, options === null || options === void 0 ? void 0 : options.correlationId);
72
+ return {
73
+ success: false,
74
+ data: undefined,
75
+ message: "Failed to request magic link: " + message,
76
+ httpCode: 500
77
+ };
78
+ }
79
+ }
80
+ /** Verify a magic link token without consuming it */
81
+ async verify(token, options) {
82
+ try {
83
+ const parts = token.split(".");
84
+ if (parts.length !== 2) {
85
+ this.logger.audit({
86
+ type: "MAGIC_LINK_FAILURE",
87
+ metadata: { reason: "Malformed token" },
88
+ correlationId: options === null || options === void 0 ? void 0 : options.correlationId
89
+ });
90
+ return {
91
+ success: false,
92
+ data: undefined,
93
+ message: "Invalid or malformed magic link token",
94
+ httpCode: 400
95
+ };
96
+ }
97
+ const [tokenId, tokenValue] = parts;
98
+ const record = await this.magicLinks.findById(tokenId);
99
+ if (!record || record.usedAt || record.expiresAt.getTime() < Date.now()) {
100
+ this.logger.audit({
101
+ type: "MAGIC_LINK_FAILURE",
102
+ metadata: { reason: "Invalid or expired token" },
103
+ correlationId: options === null || options === void 0 ? void 0 : options.correlationId
104
+ });
105
+ return {
106
+ success: false,
107
+ data: undefined,
108
+ message: "Invalid or expired magic link",
109
+ httpCode: 401
110
+ };
111
+ }
112
+ const match = await this.crypto.verifyToken(tokenValue, record.tokenHash);
113
+ if (!match) {
114
+ this.logger.audit({
115
+ type: "MAGIC_LINK_FAILURE",
116
+ metadata: { reason: "Token mismatch" },
117
+ correlationId: options === null || options === void 0 ? void 0 : options.correlationId
118
+ });
119
+ return {
120
+ success: false,
121
+ data: undefined,
122
+ message: "Invalid or expired magic link",
123
+ httpCode: 401
124
+ };
125
+ }
126
+ this.logger.audit({ type: "MAGIC_LINK_VERIFIED", correlationId: options === null || options === void 0 ? void 0 : options.correlationId });
127
+ return {
128
+ success: true,
129
+ data: { isValid: true, userId: String(record.userId), tokenId: String(record.id) },
130
+ message: "Magic link is valid",
131
+ httpCode: 200
132
+ };
133
+ }
134
+ catch (err) {
135
+ const message = err instanceof Error ? err.message : "Unknown error";
136
+ this.logger.error("Magic link verify exception", err, undefined, options === null || options === void 0 ? void 0 : options.correlationId);
137
+ return {
138
+ success: false,
139
+ data: undefined,
140
+ message: "Failed to verify magic link: " + message,
141
+ httpCode: 500
142
+ };
143
+ }
144
+ }
145
+ /** Consume a magic link token */
146
+ async consume(token, options) {
147
+ try {
148
+ const verifyResult = await this.verify(token, options);
149
+ if (!verifyResult.success) {
150
+ return {
151
+ success: false,
152
+ data: undefined,
153
+ message: verifyResult.message,
154
+ httpCode: verifyResult.httpCode
155
+ };
156
+ }
157
+ const consumed = await this.magicLinks.consume(verifyResult.data.tokenId);
158
+ if (!consumed) {
159
+ this.logger.error("Failed to consume magic link", new Error("DB update failed"), undefined, options === null || options === void 0 ? void 0 : options.correlationId);
160
+ this.logger.audit({
161
+ type: "MAGIC_LINK_FAILURE",
162
+ metadata: { reason: "Token already used" },
163
+ correlationId: options === null || options === void 0 ? void 0 : options.correlationId
164
+ });
165
+ return {
166
+ success: false,
167
+ message: "Magic link already used",
168
+ httpCode: 401
169
+ };
170
+ }
171
+ this.logger.audit({ type: "MAGIC_LINK_CONSUMED", correlationId: options === null || options === void 0 ? void 0 : options.correlationId });
172
+ this.logger.audit({
173
+ type: "LOGIN_SUCCESS",
174
+ metadata: { method: "magic-link" },
175
+ correlationId: options === null || options === void 0 ? void 0 : options.correlationId
176
+ });
177
+ return {
178
+ success: true,
179
+ data: { userId: verifyResult.data.userId },
180
+ message: "Magic link consumed",
181
+ httpCode: 200
182
+ };
183
+ }
184
+ catch (err) {
185
+ const message = err instanceof Error ? err.message : "Unknown error";
186
+ this.logger.error("Magic link consume exception", err, undefined, options === null || options === void 0 ? void 0 : options.correlationId);
187
+ return {
188
+ success: false,
189
+ data: undefined,
190
+ message: "Failed to consume magic link: " + message,
191
+ httpCode: 500
192
+ };
193
+ }
194
+ }
195
+ }
196
+ exports.MagicLinkService = MagicLinkService;
@@ -0,0 +1 @@
1
+ export {};