@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/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 crypto from "crypto";
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
- for (const [providerName, providerConfig] of Object.entries(config.providers)) {
338
- const name = providerName.toLowerCase();
339
- const typedConfig = providerConfig;
340
- this.validateProviderConfig(providerName, typedConfig);
341
- if (typedConfig.provider) {
342
- this.validateProviderImplementation(providerName, typedConfig.provider);
343
- this.log("INFO", "Init", `Registered inline provider: ${providerName}`);
344
- } else {
345
- if (!_Lixa.DEFAULT_PROVIDERS.has(name) && !_Lixa.CONFIGURED_PROVIDERS.has(name)) {
346
- this.log("ERROR", "Init", `Provider '${providerName}' not available`);
347
- throw new ProviderNotConfiguredError(
348
- providerName,
349
- { hint: `Ensure the provider is registered via '@vunexa/lixa-providers' or provided inline.` }
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 = crypto.createHash("sha256").update(codeVerifier).digest("base64");
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