@vunexa/lixa 0.1.6-alpha.12 → 0.1.6-alpha.13
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 +4 -4
- package/dist/credentials/credentials-manager.d.ts +65 -0
- package/dist/credentials/credentials-manager.d.ts.map +1 -0
- package/dist/credentials/hasher.d.ts +88 -0
- package/dist/credentials/hasher.d.ts.map +1 -0
- package/dist/credentials/index.d.ts +6 -0
- package/dist/credentials/index.d.ts.map +1 -0
- package/dist/credentials/local-storage.d.ts +20 -0
- package/dist/credentials/local-storage.d.ts.map +1 -0
- package/dist/credentials/policy.d.ts +12 -0
- package/dist/credentials/policy.d.ts.map +1 -0
- package/dist/credentials/types.d.ts +314 -0
- package/dist/credentials/types.d.ts.map +1 -0
- package/dist/errors.d.ts +42 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/export-types/index.d.ts +651 -13
- package/dist/index.cjs +823 -109
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +635 -13
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +805 -96
- package/dist/index.js.map +1 -1
- package/dist/lixa.d.ts +59 -3
- package/dist/lixa.d.ts.map +1 -1
- package/dist/models/session.d.ts +8 -8
- package/dist/models/session.d.ts.map +1 -1
- package/dist/types.d.ts +16 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,591 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __export = (target, all) => {
|
|
3
|
+
for (var name in all)
|
|
4
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
5
|
+
};
|
|
6
|
+
|
|
1
7
|
// src/lixa.ts
|
|
2
8
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
3
9
|
|
|
10
|
+
// src/credentials/hasher.ts
|
|
11
|
+
import crypto from "crypto";
|
|
12
|
+
var ScryptPasswordHasher = class {
|
|
13
|
+
cost;
|
|
14
|
+
blockSize;
|
|
15
|
+
parallelization;
|
|
16
|
+
saltLength;
|
|
17
|
+
keyLength;
|
|
18
|
+
maxmem;
|
|
19
|
+
constructor(options = {}) {
|
|
20
|
+
this.cost = options.cost ?? 16384;
|
|
21
|
+
this.blockSize = options.blockSize ?? 8;
|
|
22
|
+
this.parallelization = options.parallelization ?? 1;
|
|
23
|
+
this.saltLength = options.saltLength ?? 16;
|
|
24
|
+
this.keyLength = options.keyLength ?? 64;
|
|
25
|
+
this.maxmem = options.maxmem ?? 32 * 1024 * 1024;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Hashes a plaintext password using Scrypt.
|
|
29
|
+
*
|
|
30
|
+
* @param password - Plaintext password
|
|
31
|
+
* @returns Formatted Scrypt hash string
|
|
32
|
+
*/
|
|
33
|
+
async hash(password) {
|
|
34
|
+
const salt = crypto.randomBytes(this.saltLength).toString("hex");
|
|
35
|
+
const derivedKey = await this.deriveKey(
|
|
36
|
+
password,
|
|
37
|
+
salt,
|
|
38
|
+
this.keyLength,
|
|
39
|
+
this.cost,
|
|
40
|
+
this.blockSize,
|
|
41
|
+
this.parallelization
|
|
42
|
+
);
|
|
43
|
+
return `$scrypt$N=${this.cost},r=${this.blockSize},p=${this.parallelization}$${salt}$${derivedKey.toString("hex")}`;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Verifies a password against a stored Scrypt hash.
|
|
47
|
+
*
|
|
48
|
+
* @param password - Plaintext password
|
|
49
|
+
* @param hash - Formatted Scrypt hash string
|
|
50
|
+
* @returns True if password matches hash
|
|
51
|
+
*/
|
|
52
|
+
async verify(password, hash) {
|
|
53
|
+
if (!hash || typeof hash !== "string" || !hash.startsWith("$scrypt$")) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
const parts = hash.split("$");
|
|
57
|
+
if (parts.length !== 5) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
const paramsStr = parts[2];
|
|
61
|
+
const salt = parts[3];
|
|
62
|
+
const storedKeyHex = parts[4];
|
|
63
|
+
if (!paramsStr || !salt || !storedKeyHex) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
const params = /* @__PURE__ */ new Map();
|
|
67
|
+
for (const param of paramsStr.split(",")) {
|
|
68
|
+
const [k, v] = param.split("=");
|
|
69
|
+
if (k && v) {
|
|
70
|
+
params.set(k.trim(), parseInt(v.trim(), 10));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const cost = params.get("N") ?? this.cost;
|
|
74
|
+
const blockSize = params.get("r") ?? this.blockSize;
|
|
75
|
+
const parallelization = params.get("p") ?? this.parallelization;
|
|
76
|
+
const storedKeyBuffer = Buffer.from(storedKeyHex, "hex");
|
|
77
|
+
const derivedKeyBuffer = await this.deriveKey(
|
|
78
|
+
password,
|
|
79
|
+
salt,
|
|
80
|
+
storedKeyBuffer.length,
|
|
81
|
+
cost,
|
|
82
|
+
blockSize,
|
|
83
|
+
parallelization
|
|
84
|
+
);
|
|
85
|
+
if (storedKeyBuffer.length !== derivedKeyBuffer.length) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
return crypto.timingSafeEqual(storedKeyBuffer, derivedKeyBuffer);
|
|
89
|
+
}
|
|
90
|
+
deriveKey(password, salt, keyLength, cost, blockSize, parallelization) {
|
|
91
|
+
return new Promise((resolve, reject) => {
|
|
92
|
+
crypto.scrypt(
|
|
93
|
+
password,
|
|
94
|
+
salt,
|
|
95
|
+
keyLength,
|
|
96
|
+
{
|
|
97
|
+
N: cost,
|
|
98
|
+
r: blockSize,
|
|
99
|
+
p: parallelization,
|
|
100
|
+
maxmem: this.maxmem
|
|
101
|
+
},
|
|
102
|
+
(err, derivedKey) => {
|
|
103
|
+
if (err) {
|
|
104
|
+
reject(err);
|
|
105
|
+
} else {
|
|
106
|
+
resolve(derivedKey);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
var Pbkdf2PasswordHasher = class {
|
|
114
|
+
iterations;
|
|
115
|
+
digest;
|
|
116
|
+
saltLength;
|
|
117
|
+
keyLength;
|
|
118
|
+
constructor(options = {}) {
|
|
119
|
+
this.iterations = options.iterations ?? 1e5;
|
|
120
|
+
this.digest = options.digest ?? "sha512";
|
|
121
|
+
this.saltLength = options.saltLength ?? 16;
|
|
122
|
+
this.keyLength = options.keyLength ?? 64;
|
|
123
|
+
}
|
|
124
|
+
async hash(password) {
|
|
125
|
+
const salt = crypto.randomBytes(this.saltLength).toString("hex");
|
|
126
|
+
const derivedKey = await this.deriveKey(
|
|
127
|
+
password,
|
|
128
|
+
salt,
|
|
129
|
+
this.iterations,
|
|
130
|
+
this.keyLength,
|
|
131
|
+
this.digest
|
|
132
|
+
);
|
|
133
|
+
return `$pbkdf2$i=${this.iterations},d=${this.digest}$${salt}$${derivedKey.toString("hex")}`;
|
|
134
|
+
}
|
|
135
|
+
async verify(password, hash) {
|
|
136
|
+
if (!hash || typeof hash !== "string" || !hash.startsWith("$pbkdf2$")) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
const parts = hash.split("$");
|
|
140
|
+
if (parts.length !== 5) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
const paramsStr = parts[2];
|
|
144
|
+
const salt = parts[3];
|
|
145
|
+
const storedKeyHex = parts[4];
|
|
146
|
+
if (!paramsStr || !salt || !storedKeyHex) {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
let iterations = this.iterations;
|
|
150
|
+
let digest = this.digest;
|
|
151
|
+
for (const param of paramsStr.split(",")) {
|
|
152
|
+
const [k, v] = param.split("=");
|
|
153
|
+
if (k === "i" && v) {
|
|
154
|
+
iterations = parseInt(v, 10);
|
|
155
|
+
} else if (k === "d" && v) {
|
|
156
|
+
digest = v;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const storedKeyBuffer = Buffer.from(storedKeyHex, "hex");
|
|
160
|
+
const derivedKeyBuffer = await this.deriveKey(
|
|
161
|
+
password,
|
|
162
|
+
salt,
|
|
163
|
+
iterations,
|
|
164
|
+
storedKeyBuffer.length,
|
|
165
|
+
digest
|
|
166
|
+
);
|
|
167
|
+
if (storedKeyBuffer.length !== derivedKeyBuffer.length) {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
return crypto.timingSafeEqual(storedKeyBuffer, derivedKeyBuffer);
|
|
171
|
+
}
|
|
172
|
+
deriveKey(password, salt, iterations, keyLength, digest) {
|
|
173
|
+
return new Promise((resolve, reject) => {
|
|
174
|
+
crypto.pbkdf2(password, salt, iterations, keyLength, digest, (err, derivedKey) => {
|
|
175
|
+
if (err) {
|
|
176
|
+
reject(err);
|
|
177
|
+
} else {
|
|
178
|
+
resolve(derivedKey);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// src/credentials/policy.ts
|
|
186
|
+
async function validatePasswordPolicy(password, policy = {}) {
|
|
187
|
+
const errors = [];
|
|
188
|
+
if (typeof password !== "string") {
|
|
189
|
+
return {
|
|
190
|
+
valid: false,
|
|
191
|
+
errors: ["Password must be a string"]
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
const minLength = policy.minLength ?? 8;
|
|
195
|
+
const maxLength = policy.maxLength ?? 128;
|
|
196
|
+
if (password.length < minLength) {
|
|
197
|
+
errors.push(`Password must be at least ${minLength} characters long`);
|
|
198
|
+
}
|
|
199
|
+
if (password.length > maxLength) {
|
|
200
|
+
errors.push(`Password must not exceed ${maxLength} characters`);
|
|
201
|
+
}
|
|
202
|
+
if (policy.requireUppercase && !/[A-Z]/.test(password)) {
|
|
203
|
+
errors.push("Password must contain at least one uppercase letter (A-Z)");
|
|
204
|
+
}
|
|
205
|
+
if (policy.requireLowercase && !/[a-z]/.test(password)) {
|
|
206
|
+
errors.push("Password must contain at least one lowercase letter (a-z)");
|
|
207
|
+
}
|
|
208
|
+
if (policy.requireNumbers && !/[0-9]/.test(password)) {
|
|
209
|
+
errors.push("Password must contain at least one number (0-9)");
|
|
210
|
+
}
|
|
211
|
+
if (policy.requireSpecialChars && !/[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?~`]/.test(password)) {
|
|
212
|
+
errors.push("Password must contain at least one special character");
|
|
213
|
+
}
|
|
214
|
+
if (policy.customValidator) {
|
|
215
|
+
try {
|
|
216
|
+
const customResult = await policy.customValidator(password);
|
|
217
|
+
if (customResult === false) {
|
|
218
|
+
errors.push("Password failed custom validation rule");
|
|
219
|
+
} else if (typeof customResult === "string" && customResult.trim().length > 0) {
|
|
220
|
+
errors.push(customResult);
|
|
221
|
+
}
|
|
222
|
+
} catch (err) {
|
|
223
|
+
errors.push(`Custom password validator failed: ${err?.message || "Unknown error"}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
valid: errors.length === 0,
|
|
228
|
+
errors
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// src/credentials/local-storage.ts
|
|
233
|
+
var LocalCredentialsStorage = class {
|
|
234
|
+
usersById = /* @__PURE__ */ new Map();
|
|
235
|
+
identifierToId = /* @__PURE__ */ new Map();
|
|
236
|
+
async saveUser(user) {
|
|
237
|
+
const normalizedIdentifier = user.identifier.trim().toLowerCase();
|
|
238
|
+
this.usersById.set(user.id, { ...user, identifier: normalizedIdentifier });
|
|
239
|
+
this.identifierToId.set(normalizedIdentifier, user.id);
|
|
240
|
+
if (user.email) {
|
|
241
|
+
this.identifierToId.set(user.email.trim().toLowerCase(), user.id);
|
|
242
|
+
}
|
|
243
|
+
if (user.username) {
|
|
244
|
+
this.identifierToId.set(user.username.trim().toLowerCase(), user.id);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
async findUserByIdentifier(identifier) {
|
|
248
|
+
const normalized = identifier.trim().toLowerCase();
|
|
249
|
+
const id = this.identifierToId.get(normalized);
|
|
250
|
+
if (!id) {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
const user = this.usersById.get(id);
|
|
254
|
+
return user ? { ...user } : null;
|
|
255
|
+
}
|
|
256
|
+
async findUserById(id) {
|
|
257
|
+
const user = this.usersById.get(id);
|
|
258
|
+
return user ? { ...user } : null;
|
|
259
|
+
}
|
|
260
|
+
async updatePassword(id, newPasswordHash) {
|
|
261
|
+
const user = this.usersById.get(id);
|
|
262
|
+
if (user) {
|
|
263
|
+
user.passwordHash = newPasswordHash;
|
|
264
|
+
user.updatedAt = Date.now();
|
|
265
|
+
this.usersById.set(id, user);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
async deleteUser(id) {
|
|
269
|
+
const user = this.usersById.get(id);
|
|
270
|
+
if (user) {
|
|
271
|
+
this.identifierToId.delete(user.identifier.toLowerCase());
|
|
272
|
+
if (user.email) this.identifierToId.delete(user.email.toLowerCase());
|
|
273
|
+
if (user.username) this.identifierToId.delete(user.username.toLowerCase());
|
|
274
|
+
this.usersById.delete(id);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
clear() {
|
|
278
|
+
this.usersById.clear();
|
|
279
|
+
this.identifierToId.clear();
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
// src/credentials/credentials-manager.ts
|
|
284
|
+
import crypto2 from "crypto";
|
|
285
|
+
|
|
286
|
+
// src/errors.ts
|
|
287
|
+
var LixaError = class extends Error {
|
|
288
|
+
/**
|
|
289
|
+
* Standard error code string.
|
|
290
|
+
*/
|
|
291
|
+
code;
|
|
292
|
+
/**
|
|
293
|
+
* Additional error context data.
|
|
294
|
+
*/
|
|
295
|
+
details;
|
|
296
|
+
constructor(message, code = "LIXA_ERROR", details) {
|
|
297
|
+
super(message);
|
|
298
|
+
this.name = this.constructor.name;
|
|
299
|
+
this.code = code;
|
|
300
|
+
this.details = details;
|
|
301
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
var InvalidStateError = class extends LixaError {
|
|
305
|
+
constructor(message = "Invalid or expired state", details) {
|
|
306
|
+
super(message, "INVALID_STATE", details);
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
var ProviderNotConfiguredError = class extends LixaError {
|
|
310
|
+
constructor(provider, details) {
|
|
311
|
+
const message = details?.message && typeof details.message === "string" ? details.message : `Provider '${provider}' is not available. Either import it from '@vunexa/lixa-providers' and include it in the configuration, or provide a custom implementation using the 'provider' field: { provider: new CustomProvider(), clientId: '...', ... }`;
|
|
312
|
+
super(message, "PROVIDER_NOT_CONFIGURED", {
|
|
313
|
+
provider,
|
|
314
|
+
...details
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
var InvalidProviderConfigError = class extends LixaError {
|
|
319
|
+
constructor(message, details) {
|
|
320
|
+
super(message, "INVALID_PROVIDER_CONFIG", details);
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
var InvalidOAuthCallbackError = class extends LixaError {
|
|
324
|
+
constructor(message, details) {
|
|
325
|
+
super(message, "INVALID_OAUTH_CALLBACK", details);
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
var TokenExchangeError = class extends LixaError {
|
|
329
|
+
status;
|
|
330
|
+
constructor(message, status, details) {
|
|
331
|
+
super(message, "TOKEN_EXCHANGE_FAILED", { status, ...details });
|
|
332
|
+
this.status = status;
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
var SessionNotFoundError = class extends LixaError {
|
|
336
|
+
constructor(message = "Active session not found or has expired", details) {
|
|
337
|
+
super(message, "SESSION_NOT_FOUND", details);
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
var EmailNotVerifiedError = class extends LixaError {
|
|
341
|
+
constructor(email, details) {
|
|
342
|
+
super(
|
|
343
|
+
`Cannot link account: email '${email || "unknown"}' is not verified by the identity provider`,
|
|
344
|
+
"EMAIL_NOT_VERIFIED",
|
|
345
|
+
{ email, ...details }
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
var AccountUnlinkError = class extends LixaError {
|
|
350
|
+
constructor(message, details) {
|
|
351
|
+
super(message, "ACCOUNT_UNLINK_ERROR", details);
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
var RefreshTokenError = class extends LixaError {
|
|
355
|
+
constructor(message, details) {
|
|
356
|
+
super(message, "REFRESH_TOKEN_ERROR", details);
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
var InvalidCredentialsError = class extends LixaError {
|
|
360
|
+
constructor(message = "Invalid identifier or password", details) {
|
|
361
|
+
super(message, "INVALID_CREDENTIALS", details);
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
var UserAlreadyExistsError = class extends LixaError {
|
|
365
|
+
constructor(identifier, details) {
|
|
366
|
+
super(`User with identifier '${identifier}' already exists`, "USER_ALREADY_EXISTS", {
|
|
367
|
+
identifier,
|
|
368
|
+
...details
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
var UserNotFoundError = class extends LixaError {
|
|
373
|
+
constructor(identifierOrId, details) {
|
|
374
|
+
super(
|
|
375
|
+
`User not found${identifierOrId ? `: '${identifierOrId}'` : ""}`,
|
|
376
|
+
"USER_NOT_FOUND",
|
|
377
|
+
{ identifierOrId, ...details }
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
var WeakPasswordError = class extends LixaError {
|
|
382
|
+
validationErrors;
|
|
383
|
+
constructor(validationErrors = [], details) {
|
|
384
|
+
const message = validationErrors.length > 0 ? `Password does not meet security requirements: ${validationErrors.join("; ")}` : "Password does not meet security requirements";
|
|
385
|
+
super(message, "WEAK_PASSWORD", { validationErrors, ...details });
|
|
386
|
+
this.validationErrors = validationErrors;
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
var CredentialsNotConfiguredError = class extends LixaError {
|
|
390
|
+
constructor(message = "Credentials authentication is not configured in this Lixa instance. Pass credentials: {} in LixaConfig to enable it.", details) {
|
|
391
|
+
super(message, "CREDENTIALS_NOT_CONFIGURED", details);
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
// src/credentials/credentials-manager.ts
|
|
396
|
+
var CredentialsManager = class {
|
|
397
|
+
storage;
|
|
398
|
+
hasher;
|
|
399
|
+
policy;
|
|
400
|
+
identifierType;
|
|
401
|
+
requireUsername;
|
|
402
|
+
requireEmail;
|
|
403
|
+
timingAttackProtection;
|
|
404
|
+
dummyHash = "";
|
|
405
|
+
constructor(config = {}) {
|
|
406
|
+
this.storage = config.storage || new LocalCredentialsStorage();
|
|
407
|
+
this.hasher = config.hasher || new ScryptPasswordHasher();
|
|
408
|
+
this.policy = {
|
|
409
|
+
minLength: 8,
|
|
410
|
+
maxLength: 128,
|
|
411
|
+
...config.policy
|
|
412
|
+
};
|
|
413
|
+
this.identifierType = config.identifierType || "both";
|
|
414
|
+
this.requireUsername = config.requireUsername ?? false;
|
|
415
|
+
this.requireEmail = config.requireEmail ?? false;
|
|
416
|
+
this.timingAttackProtection = config.timingAttackProtection ?? true;
|
|
417
|
+
this.initDummyHash().catch(() => {
|
|
418
|
+
this.dummyHash = "$scrypt$N=16384,r=8,p=1$0123456789abcdef0123456789abcdef$0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
async initDummyHash() {
|
|
422
|
+
this.dummyHash = await this.hasher.hash("dummy_constant_password_12345");
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Normalizes an identifier string.
|
|
426
|
+
*/
|
|
427
|
+
normalizeIdentifier(identifier) {
|
|
428
|
+
return (identifier || "").trim().toLowerCase();
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Validates identifier format based on configured identifierType.
|
|
432
|
+
*/
|
|
433
|
+
validateIdentifierFormat(identifier) {
|
|
434
|
+
const trimmed = (identifier || "").trim();
|
|
435
|
+
if (!trimmed) {
|
|
436
|
+
throw new InvalidCredentialsError("Identifier cannot be empty");
|
|
437
|
+
}
|
|
438
|
+
if (this.identifierType === "email") {
|
|
439
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
440
|
+
if (!emailRegex.test(trimmed)) {
|
|
441
|
+
throw new InvalidCredentialsError("Identifier must be a valid email address");
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Registers a new user with password hashing and policy enforcement.
|
|
447
|
+
*
|
|
448
|
+
* @param params - Registration parameters
|
|
449
|
+
* @returns Created user credentials without password hash
|
|
450
|
+
*/
|
|
451
|
+
async signUp(params) {
|
|
452
|
+
const rawIdentifier = params.username || params.identifier || params.email || "";
|
|
453
|
+
this.validateIdentifierFormat(rawIdentifier);
|
|
454
|
+
const normalized = this.normalizeIdentifier(rawIdentifier);
|
|
455
|
+
const isEmail = normalized.includes("@");
|
|
456
|
+
const email = params.email ? params.email.trim().toLowerCase() : isEmail ? normalized : void 0;
|
|
457
|
+
const username = params.username ? params.username.trim() : !isEmail ? normalized : void 0;
|
|
458
|
+
if (this.requireUsername && (!username || username.trim() === "")) {
|
|
459
|
+
throw new InvalidCredentialsError("Username is required");
|
|
460
|
+
}
|
|
461
|
+
if (this.requireEmail && (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email))) {
|
|
462
|
+
throw new InvalidCredentialsError("A valid email address is required");
|
|
463
|
+
}
|
|
464
|
+
const existing = await this.storage.findUserByIdentifier(normalized);
|
|
465
|
+
if (existing) {
|
|
466
|
+
throw new UserAlreadyExistsError(normalized);
|
|
467
|
+
}
|
|
468
|
+
const policyResult = await validatePasswordPolicy(params.password, this.policy);
|
|
469
|
+
if (!policyResult.valid) {
|
|
470
|
+
throw new WeakPasswordError(policyResult.errors);
|
|
471
|
+
}
|
|
472
|
+
const passwordHash = await this.hasher.hash(params.password);
|
|
473
|
+
const now = Date.now();
|
|
474
|
+
const id = typeof crypto2.randomUUID === "function" ? crypto2.randomUUID() : crypto2.randomBytes(16).toString("hex");
|
|
475
|
+
const user = {
|
|
476
|
+
id,
|
|
477
|
+
identifier: normalized,
|
|
478
|
+
email,
|
|
479
|
+
username,
|
|
480
|
+
passwordHash,
|
|
481
|
+
createdAt: now,
|
|
482
|
+
updatedAt: now,
|
|
483
|
+
metadata: params.metadata
|
|
484
|
+
};
|
|
485
|
+
await this.storage.saveUser(user);
|
|
486
|
+
const { passwordHash: _, ...safeUser } = user;
|
|
487
|
+
return safeUser;
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Verifies credentials against storage with timing attack protection.
|
|
491
|
+
*
|
|
492
|
+
* @param params - Verification parameters
|
|
493
|
+
* @returns User credentials without password hash, or null if verification fails
|
|
494
|
+
*/
|
|
495
|
+
async verifyCredentials(params) {
|
|
496
|
+
const rawIdentifier = params.username || params.identifier;
|
|
497
|
+
if (!rawIdentifier || typeof params.password !== "string") {
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
500
|
+
const normalized = this.normalizeIdentifier(rawIdentifier);
|
|
501
|
+
const user = await this.storage.findUserByIdentifier(normalized);
|
|
502
|
+
if (!user) {
|
|
503
|
+
if (this.timingAttackProtection) {
|
|
504
|
+
try {
|
|
505
|
+
if (!this.dummyHash) {
|
|
506
|
+
await this.initDummyHash();
|
|
507
|
+
}
|
|
508
|
+
await this.hasher.verify(params.password, this.dummyHash);
|
|
509
|
+
} catch {
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
514
|
+
const isMatch = await this.hasher.verify(params.password, user.passwordHash);
|
|
515
|
+
if (!isMatch) {
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
const { passwordHash: _, ...safeUser } = user;
|
|
519
|
+
return safeUser;
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Changes a user's password with old password verification and new password policy enforcement.
|
|
523
|
+
*
|
|
524
|
+
* @param params - Change password parameters
|
|
525
|
+
* @returns True if password was successfully updated
|
|
526
|
+
*/
|
|
527
|
+
async changePassword(params) {
|
|
528
|
+
let user = null;
|
|
529
|
+
const lookupKey = params.username || params.identifier;
|
|
530
|
+
if (params.userId) {
|
|
531
|
+
user = await this.storage.findUserById(params.userId);
|
|
532
|
+
} else if (lookupKey) {
|
|
533
|
+
user = await this.storage.findUserByIdentifier(this.normalizeIdentifier(lookupKey));
|
|
534
|
+
}
|
|
535
|
+
if (!user) {
|
|
536
|
+
if (this.timingAttackProtection) {
|
|
537
|
+
try {
|
|
538
|
+
if (!this.dummyHash) await this.initDummyHash();
|
|
539
|
+
await this.hasher.verify(params.oldPassword, this.dummyHash);
|
|
540
|
+
} catch {
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
throw new UserNotFoundError(params.userId || lookupKey || "unknown");
|
|
544
|
+
}
|
|
545
|
+
const isOldValid = await this.hasher.verify(params.oldPassword, user.passwordHash);
|
|
546
|
+
if (!isOldValid) {
|
|
547
|
+
throw new InvalidCredentialsError("Current password is incorrect");
|
|
548
|
+
}
|
|
549
|
+
const policyResult = await validatePasswordPolicy(params.newPassword, this.policy);
|
|
550
|
+
if (!policyResult.valid) {
|
|
551
|
+
throw new WeakPasswordError(policyResult.errors);
|
|
552
|
+
}
|
|
553
|
+
const newHash = await this.hasher.hash(params.newPassword);
|
|
554
|
+
await this.storage.updatePassword(user.id, newHash);
|
|
555
|
+
return true;
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Finds a user by ID and returns safe user data.
|
|
559
|
+
*/
|
|
560
|
+
async getUserById(id) {
|
|
561
|
+
const user = await this.storage.findUserById(id);
|
|
562
|
+
if (!user) return null;
|
|
563
|
+
const { passwordHash: _, ...safeUser } = user;
|
|
564
|
+
return safeUser;
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Finds a user by identifier and returns safe user data.
|
|
568
|
+
*/
|
|
569
|
+
async getUserByIdentifier(identifier) {
|
|
570
|
+
const user = await this.storage.findUserByIdentifier(this.normalizeIdentifier(identifier));
|
|
571
|
+
if (!user) return null;
|
|
572
|
+
const { passwordHash: _, ...safeUser } = user;
|
|
573
|
+
return safeUser;
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Gets the underlying storage instance.
|
|
577
|
+
*/
|
|
578
|
+
getStorage() {
|
|
579
|
+
return this.storage;
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Gets the underlying hasher instance.
|
|
583
|
+
*/
|
|
584
|
+
getHasher() {
|
|
585
|
+
return this.hasher;
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
|
|
4
589
|
// src/types.ts
|
|
5
590
|
var AccountLinkingStrategy = /* @__PURE__ */ ((AccountLinkingStrategy2) => {
|
|
6
591
|
AccountLinkingStrategy2["AUTO_LINK_BY_VERIFIED_EMAIL"] = "AUTO_LINK_BY_VERIFIED_EMAIL";
|
|
@@ -48,7 +633,7 @@ var LocalStateHandler = class {
|
|
|
48
633
|
};
|
|
49
634
|
|
|
50
635
|
// src/lixa.ts
|
|
51
|
-
import
|
|
636
|
+
import crypto3 from "crypto";
|
|
52
637
|
|
|
53
638
|
// src/dao/session-cache.ts
|
|
54
639
|
import NodeCache2 from "node-cache";
|
|
@@ -187,80 +772,6 @@ async function extractUserInfo(tokenData, providerMetadata) {
|
|
|
187
772
|
return { userInfo };
|
|
188
773
|
}
|
|
189
774
|
|
|
190
|
-
// src/errors.ts
|
|
191
|
-
var LixaError = class extends Error {
|
|
192
|
-
/**
|
|
193
|
-
* Standard error code string.
|
|
194
|
-
*/
|
|
195
|
-
code;
|
|
196
|
-
/**
|
|
197
|
-
* Additional error context data.
|
|
198
|
-
*/
|
|
199
|
-
details;
|
|
200
|
-
constructor(message, code = "LIXA_ERROR", details) {
|
|
201
|
-
super(message);
|
|
202
|
-
this.name = this.constructor.name;
|
|
203
|
-
this.code = code;
|
|
204
|
-
this.details = details;
|
|
205
|
-
Object.setPrototypeOf(this, new.target.prototype);
|
|
206
|
-
}
|
|
207
|
-
};
|
|
208
|
-
var InvalidStateError = class extends LixaError {
|
|
209
|
-
constructor(message = "Invalid or expired state", details) {
|
|
210
|
-
super(message, "INVALID_STATE", details);
|
|
211
|
-
}
|
|
212
|
-
};
|
|
213
|
-
var ProviderNotConfiguredError = class extends LixaError {
|
|
214
|
-
constructor(provider, details) {
|
|
215
|
-
const message = details?.message && typeof details.message === "string" ? details.message : `Provider '${provider}' is not available. Either import it from '@vunexa/lixa-providers' and include it in the configuration, or provide a custom implementation using the 'provider' field: { provider: new CustomProvider(), clientId: '...', ... }`;
|
|
216
|
-
super(message, "PROVIDER_NOT_CONFIGURED", {
|
|
217
|
-
provider,
|
|
218
|
-
...details
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
};
|
|
222
|
-
var InvalidProviderConfigError = class extends LixaError {
|
|
223
|
-
constructor(message, details) {
|
|
224
|
-
super(message, "INVALID_PROVIDER_CONFIG", details);
|
|
225
|
-
}
|
|
226
|
-
};
|
|
227
|
-
var InvalidOAuthCallbackError = class extends LixaError {
|
|
228
|
-
constructor(message, details) {
|
|
229
|
-
super(message, "INVALID_OAUTH_CALLBACK", details);
|
|
230
|
-
}
|
|
231
|
-
};
|
|
232
|
-
var TokenExchangeError = class extends LixaError {
|
|
233
|
-
status;
|
|
234
|
-
constructor(message, status, details) {
|
|
235
|
-
super(message, "TOKEN_EXCHANGE_FAILED", { status, ...details });
|
|
236
|
-
this.status = status;
|
|
237
|
-
}
|
|
238
|
-
};
|
|
239
|
-
var SessionNotFoundError = class extends LixaError {
|
|
240
|
-
constructor(message = "Active session not found or has expired", details) {
|
|
241
|
-
super(message, "SESSION_NOT_FOUND", details);
|
|
242
|
-
}
|
|
243
|
-
};
|
|
244
|
-
var EmailNotVerifiedError = class extends LixaError {
|
|
245
|
-
constructor(email, details) {
|
|
246
|
-
super(
|
|
247
|
-
`Cannot link account: email '${email || "unknown"}' is not verified by the identity provider`,
|
|
248
|
-
"EMAIL_NOT_VERIFIED",
|
|
249
|
-
{ email, ...details }
|
|
250
|
-
);
|
|
251
|
-
}
|
|
252
|
-
};
|
|
253
|
-
var AccountUnlinkError = class extends LixaError {
|
|
254
|
-
constructor(message, details) {
|
|
255
|
-
super(message, "ACCOUNT_UNLINK_ERROR", details);
|
|
256
|
-
}
|
|
257
|
-
};
|
|
258
|
-
var RefreshTokenError = class extends LixaError {
|
|
259
|
-
constructor(message, details) {
|
|
260
|
-
super(message, "REFRESH_TOKEN_ERROR", details);
|
|
261
|
-
}
|
|
262
|
-
};
|
|
263
|
-
|
|
264
775
|
// src/lixa.ts
|
|
265
776
|
var Lixa = class _Lixa {
|
|
266
777
|
static DEFAULT_PROVIDERS = /* @__PURE__ */ new Map();
|
|
@@ -272,6 +783,7 @@ var Lixa = class _Lixa {
|
|
|
272
783
|
localResourceHandler;
|
|
273
784
|
userResourceStore = /* @__PURE__ */ new Map();
|
|
274
785
|
refreshMutexes = /* @__PURE__ */ new Map();
|
|
786
|
+
credentialsManager;
|
|
275
787
|
config;
|
|
276
788
|
stateHandler;
|
|
277
789
|
sessionHandler;
|
|
@@ -330,26 +842,32 @@ var Lixa = class _Lixa {
|
|
|
330
842
|
this.stateHandler = config.stateHandler || this.localStateHandler;
|
|
331
843
|
this.sessionHandler = config.sessionHandler || this.localSessionHandler;
|
|
332
844
|
this.resourceHandler = config.resourceHandler || this.localResourceHandler;
|
|
845
|
+
if (config.credentials !== void 0 && config.credentials.enabled !== false) {
|
|
846
|
+
this.credentialsManager = new CredentialsManager(config.credentials);
|
|
847
|
+
}
|
|
333
848
|
this.log("INFO", "Init", "Initializing Lixa instance", {
|
|
334
|
-
providers: Object.keys(config.providers),
|
|
849
|
+
providers: config.providers ? Object.keys(config.providers) : [],
|
|
850
|
+
credentialsEnabled: this.credentialsManager !== void 0,
|
|
335
851
|
debug: this.debug
|
|
336
852
|
});
|
|
337
|
-
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
853
|
+
if (config.providers) {
|
|
854
|
+
for (const [providerName, providerConfig] of Object.entries(config.providers)) {
|
|
855
|
+
const name = providerName.toLowerCase();
|
|
856
|
+
const typedConfig = providerConfig;
|
|
857
|
+
this.validateProviderConfig(providerName, typedConfig);
|
|
858
|
+
if (typedConfig.provider) {
|
|
859
|
+
this.validateProviderImplementation(providerName, typedConfig.provider);
|
|
860
|
+
this.log("INFO", "Init", `Registered inline provider: ${providerName}`);
|
|
861
|
+
} else {
|
|
862
|
+
if (!_Lixa.DEFAULT_PROVIDERS.has(name) && !_Lixa.CONFIGURED_PROVIDERS.has(name)) {
|
|
863
|
+
this.log("ERROR", "Init", `Provider '${providerName}' not available`);
|
|
864
|
+
throw new ProviderNotConfiguredError(
|
|
865
|
+
providerName,
|
|
866
|
+
{ hint: `Ensure the provider is registered via '@vunexa/lixa-providers' or provided inline.` }
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
this.log("INFO", "Init", `Using registered provider: ${providerName}`);
|
|
351
870
|
}
|
|
352
|
-
this.log("INFO", "Init", `Using registered provider: ${providerName}`);
|
|
353
871
|
}
|
|
354
872
|
}
|
|
355
873
|
this.log("INFO", "Init", "Lixa instance initialized successfully");
|
|
@@ -410,7 +928,7 @@ var Lixa = class _Lixa {
|
|
|
410
928
|
* Structured logging with standardized format and custom logger support.
|
|
411
929
|
*
|
|
412
930
|
* @param level - Log level (INFO, WARN, ERROR, DEBUG)
|
|
413
|
-
* @param context - Context of the log (Init, Auth, Token, Session, State, AccountLinking, Resource)
|
|
931
|
+
* @param context - Context of the log (Init, Auth, Token, Session, State, AccountLinking, Resource, Credentials)
|
|
414
932
|
* @param message - Log message
|
|
415
933
|
* @param data - Optional data to log
|
|
416
934
|
*/
|
|
@@ -447,7 +965,7 @@ var Lixa = class _Lixa {
|
|
|
447
965
|
*/
|
|
448
966
|
isProviderConfigured(provider) {
|
|
449
967
|
const providerType = provider.toLowerCase();
|
|
450
|
-
return this.config.providers.hasOwnProperty(providerType);
|
|
968
|
+
return Boolean(this.config.providers && this.config.providers.hasOwnProperty(providerType));
|
|
451
969
|
}
|
|
452
970
|
/**
|
|
453
971
|
* Gets a provider implementation by name.
|
|
@@ -638,7 +1156,7 @@ var Lixa = class _Lixa {
|
|
|
638
1156
|
* @internal
|
|
639
1157
|
*/
|
|
640
1158
|
static buildCodeChallenge(codeVerifier) {
|
|
641
|
-
const hash =
|
|
1159
|
+
const hash = crypto3.createHash("sha256").update(codeVerifier).digest("base64");
|
|
642
1160
|
return hash.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
643
1161
|
}
|
|
644
1162
|
/**
|
|
@@ -1311,8 +1829,178 @@ var Lixa = class _Lixa {
|
|
|
1311
1829
|
this.log("INFO", "Token", "Token exchange response received successfully");
|
|
1312
1830
|
return response.json();
|
|
1313
1831
|
}
|
|
1832
|
+
/**
|
|
1833
|
+
* Checks if credentials (username and password) authentication is configured and enabled.
|
|
1834
|
+
*
|
|
1835
|
+
* @returns True if credentials authentication is available
|
|
1836
|
+
*/
|
|
1837
|
+
isCredentialsEnabled() {
|
|
1838
|
+
return this.credentialsManager !== void 0;
|
|
1839
|
+
}
|
|
1840
|
+
/**
|
|
1841
|
+
* Returns the underlying CredentialsManager instance if configured.
|
|
1842
|
+
*/
|
|
1843
|
+
getCredentialsManager() {
|
|
1844
|
+
return this.credentialsManager;
|
|
1845
|
+
}
|
|
1846
|
+
/**
|
|
1847
|
+
* Registers a new user with username/email and password.
|
|
1848
|
+
* Automatically enforces password policy, hashes password with Scrypt/configured hasher,
|
|
1849
|
+
* stores credentials, and creates a session (unless autoCreateSessionOnSignUp is false).
|
|
1850
|
+
*
|
|
1851
|
+
* @param params - Registration parameters (identifier, password, email, username, metadata)
|
|
1852
|
+
* @returns Created user (without password hash) and optional active session
|
|
1853
|
+
* @throws WeakPasswordError if password does not meet policy requirements
|
|
1854
|
+
* @throws UserAlreadyExistsError if identifier is already registered
|
|
1855
|
+
* @throws CredentialsNotConfiguredError if credentials auth is not configured
|
|
1856
|
+
*/
|
|
1857
|
+
async signUp(params) {
|
|
1858
|
+
if (!this.credentialsManager) {
|
|
1859
|
+
throw new CredentialsNotConfiguredError();
|
|
1860
|
+
}
|
|
1861
|
+
this.log("INFO", "Credentials", "Processing signUp request", { identifier: params.identifier });
|
|
1862
|
+
const user = await this.credentialsManager.signUp(params);
|
|
1863
|
+
const autoSession = this.config.credentials?.autoCreateSessionOnSignUp ?? true;
|
|
1864
|
+
if (!autoSession) {
|
|
1865
|
+
return { user };
|
|
1866
|
+
}
|
|
1867
|
+
const sessionId = randomBytes2(16).toString("hex");
|
|
1868
|
+
const token = randomBytes2(32).toString("hex");
|
|
1869
|
+
const ttl = this.config.credentials?.sessionTtlSeconds ?? 86400;
|
|
1870
|
+
const credentialsAccount = {
|
|
1871
|
+
provider: "credentials",
|
|
1872
|
+
providerUserId: user.id,
|
|
1873
|
+
email: user.email,
|
|
1874
|
+
accessToken: token,
|
|
1875
|
+
raw: {
|
|
1876
|
+
access_token: token,
|
|
1877
|
+
token_type: "Bearer",
|
|
1878
|
+
expires_in: ttl
|
|
1879
|
+
},
|
|
1880
|
+
linkedAt: Date.now()
|
|
1881
|
+
};
|
|
1882
|
+
const session = {
|
|
1883
|
+
id: sessionId,
|
|
1884
|
+
userId: user.id,
|
|
1885
|
+
email: user.email,
|
|
1886
|
+
token,
|
|
1887
|
+
provider: "credentials",
|
|
1888
|
+
accounts: {
|
|
1889
|
+
credentials: credentialsAccount
|
|
1890
|
+
}
|
|
1891
|
+
};
|
|
1892
|
+
const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
|
|
1893
|
+
await sessionStorage.saveSession(sessionId, session, ttl);
|
|
1894
|
+
this.log("INFO", "Credentials", "Created session on signUp", { userId: user.id, sessionId });
|
|
1895
|
+
return { user, sessionId, session };
|
|
1896
|
+
}
|
|
1897
|
+
/**
|
|
1898
|
+
* Authenticates a user with username/email and password.
|
|
1899
|
+
* Performs constant-time verification with timing attack mitigation, and creates an active session.
|
|
1900
|
+
* If account linking is configured with AUTO_LINK_BY_VERIFIED_EMAIL, merges with existing session.
|
|
1901
|
+
*
|
|
1902
|
+
* @param params - Sign-in parameters (identifier, password)
|
|
1903
|
+
* @returns Authenticated user, session ID, and session object
|
|
1904
|
+
* @throws InvalidCredentialsError if authentication fails
|
|
1905
|
+
* @throws CredentialsNotConfiguredError if credentials auth is not configured
|
|
1906
|
+
*/
|
|
1907
|
+
async signIn(params) {
|
|
1908
|
+
if (!this.credentialsManager) {
|
|
1909
|
+
throw new CredentialsNotConfiguredError();
|
|
1910
|
+
}
|
|
1911
|
+
const lookupKey = params.username || params.identifier;
|
|
1912
|
+
this.log("INFO", "Credentials", "Processing signIn request", { identifier: lookupKey });
|
|
1913
|
+
const user = await this.credentialsManager.verifyCredentials(params);
|
|
1914
|
+
if (!user) {
|
|
1915
|
+
this.log("WARN", "Credentials", "Invalid credentials provided", { identifier: lookupKey });
|
|
1916
|
+
throw new InvalidCredentialsError();
|
|
1917
|
+
}
|
|
1918
|
+
const ttl = this.config.credentials?.sessionTtlSeconds ?? 86400;
|
|
1919
|
+
const token = randomBytes2(32).toString("hex");
|
|
1920
|
+
const credentialsAccount = {
|
|
1921
|
+
provider: "credentials",
|
|
1922
|
+
providerUserId: user.id,
|
|
1923
|
+
email: user.email,
|
|
1924
|
+
accessToken: token,
|
|
1925
|
+
raw: {
|
|
1926
|
+
access_token: token,
|
|
1927
|
+
token_type: "Bearer",
|
|
1928
|
+
expires_in: ttl
|
|
1929
|
+
},
|
|
1930
|
+
linkedAt: Date.now()
|
|
1931
|
+
};
|
|
1932
|
+
const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
|
|
1933
|
+
const linkingMode = this.config.accountLinking?.mode;
|
|
1934
|
+
const isAutoLink = linkingMode === "AUTO_LINK_BY_VERIFIED_EMAIL" /* AUTO_LINK_BY_VERIFIED_EMAIL */ || linkingMode === "AUTO_LINK_BY_VERIFIED_EMAIL" || linkingMode === "linkByEmail";
|
|
1935
|
+
if (isAutoLink && user.email && sessionStorage.getSessionByEmail) {
|
|
1936
|
+
const existing = await sessionStorage.getSessionByEmail(user.email);
|
|
1937
|
+
if (existing) {
|
|
1938
|
+
const mergedSession = { ...existing.session };
|
|
1939
|
+
mergedSession.accounts = { ...mergedSession.accounts || {}, credentials: credentialsAccount };
|
|
1940
|
+
if (!mergedSession.userId) mergedSession.userId = user.id;
|
|
1941
|
+
if (!mergedSession.email) mergedSession.email = user.email;
|
|
1942
|
+
await sessionStorage.saveSession(existing.sessionId, mergedSession, ttl);
|
|
1943
|
+
this.log("INFO", "AccountLinking", "Linked credentials to existing session by email", {
|
|
1944
|
+
sessionId: existing.sessionId,
|
|
1945
|
+
email: user.email
|
|
1946
|
+
});
|
|
1947
|
+
return {
|
|
1948
|
+
user,
|
|
1949
|
+
sessionId: existing.sessionId,
|
|
1950
|
+
session: mergedSession
|
|
1951
|
+
};
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
const sessionId = randomBytes2(16).toString("hex");
|
|
1955
|
+
const session = {
|
|
1956
|
+
id: sessionId,
|
|
1957
|
+
userId: user.id,
|
|
1958
|
+
email: user.email,
|
|
1959
|
+
token,
|
|
1960
|
+
provider: "credentials",
|
|
1961
|
+
accounts: {
|
|
1962
|
+
credentials: credentialsAccount
|
|
1963
|
+
}
|
|
1964
|
+
};
|
|
1965
|
+
await sessionStorage.saveSession(sessionId, session, ttl);
|
|
1966
|
+
this.log("INFO", "Credentials", "User signed in successfully", { userId: user.id, sessionId });
|
|
1967
|
+
return { user, sessionId, session };
|
|
1968
|
+
}
|
|
1969
|
+
/**
|
|
1970
|
+
* Verifies username/email and password credentials without generating a session.
|
|
1971
|
+
*
|
|
1972
|
+
* @param params - Verification parameters (identifier, password)
|
|
1973
|
+
* @returns User credentials (without password hash) or null if invalid
|
|
1974
|
+
* @throws CredentialsNotConfiguredError if credentials auth is not configured
|
|
1975
|
+
*/
|
|
1976
|
+
async verifyCredentials(params) {
|
|
1977
|
+
if (!this.credentialsManager) {
|
|
1978
|
+
throw new CredentialsNotConfiguredError();
|
|
1979
|
+
}
|
|
1980
|
+
return this.credentialsManager.verifyCredentials(params);
|
|
1981
|
+
}
|
|
1982
|
+
/**
|
|
1983
|
+
* Updates a user's password after verifying the current password and enforcing policy on the new password.
|
|
1984
|
+
*
|
|
1985
|
+
* @param params - Change password parameters (userId/identifier, oldPassword, newPassword)
|
|
1986
|
+
* @returns True if password was updated successfully
|
|
1987
|
+
* @throws UserNotFoundError if user is not found
|
|
1988
|
+
* @throws InvalidCredentialsError if current password is incorrect
|
|
1989
|
+
* @throws WeakPasswordError if new password does not meet policy requirements
|
|
1990
|
+
* @throws CredentialsNotConfiguredError if credentials auth is not configured
|
|
1991
|
+
*/
|
|
1992
|
+
async changePassword(params) {
|
|
1993
|
+
if (!this.credentialsManager) {
|
|
1994
|
+
throw new CredentialsNotConfiguredError();
|
|
1995
|
+
}
|
|
1996
|
+
this.log("INFO", "Credentials", "Processing changePassword request", {
|
|
1997
|
+
userId: params.userId,
|
|
1998
|
+
identifier: params.identifier
|
|
1999
|
+
});
|
|
2000
|
+
return this.credentialsManager.changePassword(params);
|
|
2001
|
+
}
|
|
1314
2002
|
findProviderByType(providerType) {
|
|
1315
|
-
for (const [key, value] of Object.entries(this.config.providers)) {
|
|
2003
|
+
for (const [key, value] of Object.entries(this.config.providers || {})) {
|
|
1316
2004
|
if (key.toLowerCase() === providerType.toLowerCase()) {
|
|
1317
2005
|
return value;
|
|
1318
2006
|
}
|
|
@@ -1439,32 +2127,53 @@ function clearStateCookie(options) {
|
|
|
1439
2127
|
header
|
|
1440
2128
|
};
|
|
1441
2129
|
}
|
|
2130
|
+
|
|
2131
|
+
// src/credentials/index.ts
|
|
2132
|
+
var credentials_exports = {};
|
|
2133
|
+
__export(credentials_exports, {
|
|
2134
|
+
CredentialsManager: () => CredentialsManager,
|
|
2135
|
+
LocalCredentialsStorage: () => LocalCredentialsStorage,
|
|
2136
|
+
Pbkdf2PasswordHasher: () => Pbkdf2PasswordHasher,
|
|
2137
|
+
ScryptPasswordHasher: () => ScryptPasswordHasher,
|
|
2138
|
+
validatePasswordPolicy: () => validatePasswordPolicy
|
|
2139
|
+
});
|
|
1442
2140
|
export {
|
|
1443
2141
|
AccountLinkingStrategy,
|
|
1444
2142
|
AccountUnlinkError,
|
|
2143
|
+
CredentialsManager,
|
|
2144
|
+
CredentialsNotConfiguredError,
|
|
1445
2145
|
DEFAULT_SESSION_COOKIE_NAME,
|
|
1446
2146
|
DEFAULT_SESSION_MAX_AGE_SECONDS,
|
|
1447
2147
|
DEFAULT_STATE_COOKIE_NAME,
|
|
1448
2148
|
DEFAULT_STATE_MAX_AGE_SECONDS,
|
|
1449
2149
|
EmailNotVerifiedError,
|
|
2150
|
+
InvalidCredentialsError,
|
|
1450
2151
|
InvalidOAuthCallbackError,
|
|
1451
2152
|
InvalidProviderConfigError,
|
|
1452
2153
|
InvalidStateError,
|
|
1453
2154
|
Lixa,
|
|
1454
2155
|
LixaError,
|
|
2156
|
+
LocalCredentialsStorage,
|
|
2157
|
+
Pbkdf2PasswordHasher,
|
|
1455
2158
|
ProviderNotConfiguredError,
|
|
1456
2159
|
RefreshTokenError,
|
|
2160
|
+
ScryptPasswordHasher,
|
|
1457
2161
|
SessionNotFoundError,
|
|
1458
2162
|
TokenExchangeError,
|
|
2163
|
+
UserAlreadyExistsError,
|
|
2164
|
+
UserNotFoundError,
|
|
2165
|
+
WeakPasswordError,
|
|
1459
2166
|
clearSessionCookie,
|
|
1460
2167
|
clearStateCookie,
|
|
1461
2168
|
createSessionCookie,
|
|
1462
2169
|
createStateCookie,
|
|
2170
|
+
credentials_exports as credentials,
|
|
1463
2171
|
decodeIdToken,
|
|
1464
2172
|
determineProviderFromIssuer,
|
|
1465
2173
|
extractUserInfo,
|
|
1466
2174
|
fetchUserInfo,
|
|
1467
2175
|
isProductionEnvironment,
|
|
1468
|
-
serializeCookie
|
|
2176
|
+
serializeCookie,
|
|
2177
|
+
validatePasswordPolicy
|
|
1469
2178
|
};
|
|
1470
2179
|
//# sourceMappingURL=index.js.map
|