@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.cjs
CHANGED
|
@@ -32,35 +32,625 @@ var src_exports = {};
|
|
|
32
32
|
__export(src_exports, {
|
|
33
33
|
AccountLinkingStrategy: () => AccountLinkingStrategy,
|
|
34
34
|
AccountUnlinkError: () => AccountUnlinkError,
|
|
35
|
+
CredentialsManager: () => CredentialsManager,
|
|
36
|
+
CredentialsNotConfiguredError: () => CredentialsNotConfiguredError,
|
|
35
37
|
DEFAULT_SESSION_COOKIE_NAME: () => DEFAULT_SESSION_COOKIE_NAME,
|
|
36
38
|
DEFAULT_SESSION_MAX_AGE_SECONDS: () => DEFAULT_SESSION_MAX_AGE_SECONDS,
|
|
37
39
|
DEFAULT_STATE_COOKIE_NAME: () => DEFAULT_STATE_COOKIE_NAME,
|
|
38
40
|
DEFAULT_STATE_MAX_AGE_SECONDS: () => DEFAULT_STATE_MAX_AGE_SECONDS,
|
|
39
41
|
EmailNotVerifiedError: () => EmailNotVerifiedError,
|
|
42
|
+
InvalidCredentialsError: () => InvalidCredentialsError,
|
|
40
43
|
InvalidOAuthCallbackError: () => InvalidOAuthCallbackError,
|
|
41
44
|
InvalidProviderConfigError: () => InvalidProviderConfigError,
|
|
42
45
|
InvalidStateError: () => InvalidStateError,
|
|
43
46
|
Lixa: () => Lixa,
|
|
44
47
|
LixaError: () => LixaError,
|
|
48
|
+
LocalCredentialsStorage: () => LocalCredentialsStorage,
|
|
49
|
+
Pbkdf2PasswordHasher: () => Pbkdf2PasswordHasher,
|
|
45
50
|
ProviderNotConfiguredError: () => ProviderNotConfiguredError,
|
|
46
51
|
RefreshTokenError: () => RefreshTokenError,
|
|
52
|
+
ScryptPasswordHasher: () => ScryptPasswordHasher,
|
|
47
53
|
SessionNotFoundError: () => SessionNotFoundError,
|
|
48
54
|
TokenExchangeError: () => TokenExchangeError,
|
|
55
|
+
UserAlreadyExistsError: () => UserAlreadyExistsError,
|
|
56
|
+
UserNotFoundError: () => UserNotFoundError,
|
|
57
|
+
WeakPasswordError: () => WeakPasswordError,
|
|
49
58
|
clearSessionCookie: () => clearSessionCookie,
|
|
50
59
|
clearStateCookie: () => clearStateCookie,
|
|
51
60
|
createSessionCookie: () => createSessionCookie,
|
|
52
61
|
createStateCookie: () => createStateCookie,
|
|
62
|
+
credentials: () => credentials_exports,
|
|
53
63
|
decodeIdToken: () => decodeIdToken,
|
|
54
64
|
determineProviderFromIssuer: () => determineProviderFromIssuer,
|
|
55
65
|
extractUserInfo: () => extractUserInfo,
|
|
56
66
|
fetchUserInfo: () => fetchUserInfo,
|
|
57
67
|
isProductionEnvironment: () => isProductionEnvironment,
|
|
58
|
-
serializeCookie: () => serializeCookie
|
|
68
|
+
serializeCookie: () => serializeCookie,
|
|
69
|
+
validatePasswordPolicy: () => validatePasswordPolicy
|
|
59
70
|
});
|
|
60
71
|
module.exports = __toCommonJS(src_exports);
|
|
61
72
|
|
|
62
73
|
// src/lixa.ts
|
|
63
|
-
var
|
|
74
|
+
var import_crypto4 = require("crypto");
|
|
75
|
+
|
|
76
|
+
// src/credentials/hasher.ts
|
|
77
|
+
var import_crypto = __toESM(require("crypto"), 1);
|
|
78
|
+
var ScryptPasswordHasher = class {
|
|
79
|
+
cost;
|
|
80
|
+
blockSize;
|
|
81
|
+
parallelization;
|
|
82
|
+
saltLength;
|
|
83
|
+
keyLength;
|
|
84
|
+
maxmem;
|
|
85
|
+
constructor(options = {}) {
|
|
86
|
+
this.cost = options.cost ?? 16384;
|
|
87
|
+
this.blockSize = options.blockSize ?? 8;
|
|
88
|
+
this.parallelization = options.parallelization ?? 1;
|
|
89
|
+
this.saltLength = options.saltLength ?? 16;
|
|
90
|
+
this.keyLength = options.keyLength ?? 64;
|
|
91
|
+
this.maxmem = options.maxmem ?? 32 * 1024 * 1024;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Hashes a plaintext password using Scrypt.
|
|
95
|
+
*
|
|
96
|
+
* @param password - Plaintext password
|
|
97
|
+
* @returns Formatted Scrypt hash string
|
|
98
|
+
*/
|
|
99
|
+
async hash(password) {
|
|
100
|
+
const salt = import_crypto.default.randomBytes(this.saltLength).toString("hex");
|
|
101
|
+
const derivedKey = await this.deriveKey(
|
|
102
|
+
password,
|
|
103
|
+
salt,
|
|
104
|
+
this.keyLength,
|
|
105
|
+
this.cost,
|
|
106
|
+
this.blockSize,
|
|
107
|
+
this.parallelization
|
|
108
|
+
);
|
|
109
|
+
return `$scrypt$N=${this.cost},r=${this.blockSize},p=${this.parallelization}$${salt}$${derivedKey.toString("hex")}`;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Verifies a password against a stored Scrypt hash.
|
|
113
|
+
*
|
|
114
|
+
* @param password - Plaintext password
|
|
115
|
+
* @param hash - Formatted Scrypt hash string
|
|
116
|
+
* @returns True if password matches hash
|
|
117
|
+
*/
|
|
118
|
+
async verify(password, hash) {
|
|
119
|
+
if (!hash || typeof hash !== "string" || !hash.startsWith("$scrypt$")) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
const parts = hash.split("$");
|
|
123
|
+
if (parts.length !== 5) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
const paramsStr = parts[2];
|
|
127
|
+
const salt = parts[3];
|
|
128
|
+
const storedKeyHex = parts[4];
|
|
129
|
+
if (!paramsStr || !salt || !storedKeyHex) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
const params = /* @__PURE__ */ new Map();
|
|
133
|
+
for (const param of paramsStr.split(",")) {
|
|
134
|
+
const [k, v] = param.split("=");
|
|
135
|
+
if (k && v) {
|
|
136
|
+
params.set(k.trim(), parseInt(v.trim(), 10));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const cost = params.get("N") ?? this.cost;
|
|
140
|
+
const blockSize = params.get("r") ?? this.blockSize;
|
|
141
|
+
const parallelization = params.get("p") ?? this.parallelization;
|
|
142
|
+
const storedKeyBuffer = Buffer.from(storedKeyHex, "hex");
|
|
143
|
+
const derivedKeyBuffer = await this.deriveKey(
|
|
144
|
+
password,
|
|
145
|
+
salt,
|
|
146
|
+
storedKeyBuffer.length,
|
|
147
|
+
cost,
|
|
148
|
+
blockSize,
|
|
149
|
+
parallelization
|
|
150
|
+
);
|
|
151
|
+
if (storedKeyBuffer.length !== derivedKeyBuffer.length) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
return import_crypto.default.timingSafeEqual(storedKeyBuffer, derivedKeyBuffer);
|
|
155
|
+
}
|
|
156
|
+
deriveKey(password, salt, keyLength, cost, blockSize, parallelization) {
|
|
157
|
+
return new Promise((resolve, reject) => {
|
|
158
|
+
import_crypto.default.scrypt(
|
|
159
|
+
password,
|
|
160
|
+
salt,
|
|
161
|
+
keyLength,
|
|
162
|
+
{
|
|
163
|
+
N: cost,
|
|
164
|
+
r: blockSize,
|
|
165
|
+
p: parallelization,
|
|
166
|
+
maxmem: this.maxmem
|
|
167
|
+
},
|
|
168
|
+
(err, derivedKey) => {
|
|
169
|
+
if (err) {
|
|
170
|
+
reject(err);
|
|
171
|
+
} else {
|
|
172
|
+
resolve(derivedKey);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
var Pbkdf2PasswordHasher = class {
|
|
180
|
+
iterations;
|
|
181
|
+
digest;
|
|
182
|
+
saltLength;
|
|
183
|
+
keyLength;
|
|
184
|
+
constructor(options = {}) {
|
|
185
|
+
this.iterations = options.iterations ?? 1e5;
|
|
186
|
+
this.digest = options.digest ?? "sha512";
|
|
187
|
+
this.saltLength = options.saltLength ?? 16;
|
|
188
|
+
this.keyLength = options.keyLength ?? 64;
|
|
189
|
+
}
|
|
190
|
+
async hash(password) {
|
|
191
|
+
const salt = import_crypto.default.randomBytes(this.saltLength).toString("hex");
|
|
192
|
+
const derivedKey = await this.deriveKey(
|
|
193
|
+
password,
|
|
194
|
+
salt,
|
|
195
|
+
this.iterations,
|
|
196
|
+
this.keyLength,
|
|
197
|
+
this.digest
|
|
198
|
+
);
|
|
199
|
+
return `$pbkdf2$i=${this.iterations},d=${this.digest}$${salt}$${derivedKey.toString("hex")}`;
|
|
200
|
+
}
|
|
201
|
+
async verify(password, hash) {
|
|
202
|
+
if (!hash || typeof hash !== "string" || !hash.startsWith("$pbkdf2$")) {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
const parts = hash.split("$");
|
|
206
|
+
if (parts.length !== 5) {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
const paramsStr = parts[2];
|
|
210
|
+
const salt = parts[3];
|
|
211
|
+
const storedKeyHex = parts[4];
|
|
212
|
+
if (!paramsStr || !salt || !storedKeyHex) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
let iterations = this.iterations;
|
|
216
|
+
let digest = this.digest;
|
|
217
|
+
for (const param of paramsStr.split(",")) {
|
|
218
|
+
const [k, v] = param.split("=");
|
|
219
|
+
if (k === "i" && v) {
|
|
220
|
+
iterations = parseInt(v, 10);
|
|
221
|
+
} else if (k === "d" && v) {
|
|
222
|
+
digest = v;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const storedKeyBuffer = Buffer.from(storedKeyHex, "hex");
|
|
226
|
+
const derivedKeyBuffer = await this.deriveKey(
|
|
227
|
+
password,
|
|
228
|
+
salt,
|
|
229
|
+
iterations,
|
|
230
|
+
storedKeyBuffer.length,
|
|
231
|
+
digest
|
|
232
|
+
);
|
|
233
|
+
if (storedKeyBuffer.length !== derivedKeyBuffer.length) {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
return import_crypto.default.timingSafeEqual(storedKeyBuffer, derivedKeyBuffer);
|
|
237
|
+
}
|
|
238
|
+
deriveKey(password, salt, iterations, keyLength, digest) {
|
|
239
|
+
return new Promise((resolve, reject) => {
|
|
240
|
+
import_crypto.default.pbkdf2(password, salt, iterations, keyLength, digest, (err, derivedKey) => {
|
|
241
|
+
if (err) {
|
|
242
|
+
reject(err);
|
|
243
|
+
} else {
|
|
244
|
+
resolve(derivedKey);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
// src/credentials/policy.ts
|
|
252
|
+
async function validatePasswordPolicy(password, policy = {}) {
|
|
253
|
+
const errors = [];
|
|
254
|
+
if (typeof password !== "string") {
|
|
255
|
+
return {
|
|
256
|
+
valid: false,
|
|
257
|
+
errors: ["Password must be a string"]
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
const minLength = policy.minLength ?? 8;
|
|
261
|
+
const maxLength = policy.maxLength ?? 128;
|
|
262
|
+
if (password.length < minLength) {
|
|
263
|
+
errors.push(`Password must be at least ${minLength} characters long`);
|
|
264
|
+
}
|
|
265
|
+
if (password.length > maxLength) {
|
|
266
|
+
errors.push(`Password must not exceed ${maxLength} characters`);
|
|
267
|
+
}
|
|
268
|
+
if (policy.requireUppercase && !/[A-Z]/.test(password)) {
|
|
269
|
+
errors.push("Password must contain at least one uppercase letter (A-Z)");
|
|
270
|
+
}
|
|
271
|
+
if (policy.requireLowercase && !/[a-z]/.test(password)) {
|
|
272
|
+
errors.push("Password must contain at least one lowercase letter (a-z)");
|
|
273
|
+
}
|
|
274
|
+
if (policy.requireNumbers && !/[0-9]/.test(password)) {
|
|
275
|
+
errors.push("Password must contain at least one number (0-9)");
|
|
276
|
+
}
|
|
277
|
+
if (policy.requireSpecialChars && !/[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?~`]/.test(password)) {
|
|
278
|
+
errors.push("Password must contain at least one special character");
|
|
279
|
+
}
|
|
280
|
+
if (policy.customValidator) {
|
|
281
|
+
try {
|
|
282
|
+
const customResult = await policy.customValidator(password);
|
|
283
|
+
if (customResult === false) {
|
|
284
|
+
errors.push("Password failed custom validation rule");
|
|
285
|
+
} else if (typeof customResult === "string" && customResult.trim().length > 0) {
|
|
286
|
+
errors.push(customResult);
|
|
287
|
+
}
|
|
288
|
+
} catch (err) {
|
|
289
|
+
errors.push(`Custom password validator failed: ${err?.message || "Unknown error"}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return {
|
|
293
|
+
valid: errors.length === 0,
|
|
294
|
+
errors
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// src/credentials/local-storage.ts
|
|
299
|
+
var LocalCredentialsStorage = class {
|
|
300
|
+
usersById = /* @__PURE__ */ new Map();
|
|
301
|
+
identifierToId = /* @__PURE__ */ new Map();
|
|
302
|
+
async saveUser(user) {
|
|
303
|
+
const normalizedIdentifier = user.identifier.trim().toLowerCase();
|
|
304
|
+
this.usersById.set(user.id, { ...user, identifier: normalizedIdentifier });
|
|
305
|
+
this.identifierToId.set(normalizedIdentifier, user.id);
|
|
306
|
+
if (user.email) {
|
|
307
|
+
this.identifierToId.set(user.email.trim().toLowerCase(), user.id);
|
|
308
|
+
}
|
|
309
|
+
if (user.username) {
|
|
310
|
+
this.identifierToId.set(user.username.trim().toLowerCase(), user.id);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
async findUserByIdentifier(identifier) {
|
|
314
|
+
const normalized = identifier.trim().toLowerCase();
|
|
315
|
+
const id = this.identifierToId.get(normalized);
|
|
316
|
+
if (!id) {
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
const user = this.usersById.get(id);
|
|
320
|
+
return user ? { ...user } : null;
|
|
321
|
+
}
|
|
322
|
+
async findUserById(id) {
|
|
323
|
+
const user = this.usersById.get(id);
|
|
324
|
+
return user ? { ...user } : null;
|
|
325
|
+
}
|
|
326
|
+
async updatePassword(id, newPasswordHash) {
|
|
327
|
+
const user = this.usersById.get(id);
|
|
328
|
+
if (user) {
|
|
329
|
+
user.passwordHash = newPasswordHash;
|
|
330
|
+
user.updatedAt = Date.now();
|
|
331
|
+
this.usersById.set(id, user);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
async deleteUser(id) {
|
|
335
|
+
const user = this.usersById.get(id);
|
|
336
|
+
if (user) {
|
|
337
|
+
this.identifierToId.delete(user.identifier.toLowerCase());
|
|
338
|
+
if (user.email) this.identifierToId.delete(user.email.toLowerCase());
|
|
339
|
+
if (user.username) this.identifierToId.delete(user.username.toLowerCase());
|
|
340
|
+
this.usersById.delete(id);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
clear() {
|
|
344
|
+
this.usersById.clear();
|
|
345
|
+
this.identifierToId.clear();
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
// src/credentials/credentials-manager.ts
|
|
350
|
+
var import_crypto2 = __toESM(require("crypto"), 1);
|
|
351
|
+
|
|
352
|
+
// src/errors.ts
|
|
353
|
+
var LixaError = class extends Error {
|
|
354
|
+
/**
|
|
355
|
+
* Standard error code string.
|
|
356
|
+
*/
|
|
357
|
+
code;
|
|
358
|
+
/**
|
|
359
|
+
* Additional error context data.
|
|
360
|
+
*/
|
|
361
|
+
details;
|
|
362
|
+
constructor(message, code = "LIXA_ERROR", details) {
|
|
363
|
+
super(message);
|
|
364
|
+
this.name = this.constructor.name;
|
|
365
|
+
this.code = code;
|
|
366
|
+
this.details = details;
|
|
367
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
var InvalidStateError = class extends LixaError {
|
|
371
|
+
constructor(message = "Invalid or expired state", details) {
|
|
372
|
+
super(message, "INVALID_STATE", details);
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
var ProviderNotConfiguredError = class extends LixaError {
|
|
376
|
+
constructor(provider, details) {
|
|
377
|
+
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: '...', ... }`;
|
|
378
|
+
super(message, "PROVIDER_NOT_CONFIGURED", {
|
|
379
|
+
provider,
|
|
380
|
+
...details
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
var InvalidProviderConfigError = class extends LixaError {
|
|
385
|
+
constructor(message, details) {
|
|
386
|
+
super(message, "INVALID_PROVIDER_CONFIG", details);
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
var InvalidOAuthCallbackError = class extends LixaError {
|
|
390
|
+
constructor(message, details) {
|
|
391
|
+
super(message, "INVALID_OAUTH_CALLBACK", details);
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
var TokenExchangeError = class extends LixaError {
|
|
395
|
+
status;
|
|
396
|
+
constructor(message, status, details) {
|
|
397
|
+
super(message, "TOKEN_EXCHANGE_FAILED", { status, ...details });
|
|
398
|
+
this.status = status;
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
var SessionNotFoundError = class extends LixaError {
|
|
402
|
+
constructor(message = "Active session not found or has expired", details) {
|
|
403
|
+
super(message, "SESSION_NOT_FOUND", details);
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
var EmailNotVerifiedError = class extends LixaError {
|
|
407
|
+
constructor(email, details) {
|
|
408
|
+
super(
|
|
409
|
+
`Cannot link account: email '${email || "unknown"}' is not verified by the identity provider`,
|
|
410
|
+
"EMAIL_NOT_VERIFIED",
|
|
411
|
+
{ email, ...details }
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
var AccountUnlinkError = class extends LixaError {
|
|
416
|
+
constructor(message, details) {
|
|
417
|
+
super(message, "ACCOUNT_UNLINK_ERROR", details);
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
var RefreshTokenError = class extends LixaError {
|
|
421
|
+
constructor(message, details) {
|
|
422
|
+
super(message, "REFRESH_TOKEN_ERROR", details);
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
var InvalidCredentialsError = class extends LixaError {
|
|
426
|
+
constructor(message = "Invalid identifier or password", details) {
|
|
427
|
+
super(message, "INVALID_CREDENTIALS", details);
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
var UserAlreadyExistsError = class extends LixaError {
|
|
431
|
+
constructor(identifier, details) {
|
|
432
|
+
super(`User with identifier '${identifier}' already exists`, "USER_ALREADY_EXISTS", {
|
|
433
|
+
identifier,
|
|
434
|
+
...details
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
var UserNotFoundError = class extends LixaError {
|
|
439
|
+
constructor(identifierOrId, details) {
|
|
440
|
+
super(
|
|
441
|
+
`User not found${identifierOrId ? `: '${identifierOrId}'` : ""}`,
|
|
442
|
+
"USER_NOT_FOUND",
|
|
443
|
+
{ identifierOrId, ...details }
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
var WeakPasswordError = class extends LixaError {
|
|
448
|
+
validationErrors;
|
|
449
|
+
constructor(validationErrors = [], details) {
|
|
450
|
+
const message = validationErrors.length > 0 ? `Password does not meet security requirements: ${validationErrors.join("; ")}` : "Password does not meet security requirements";
|
|
451
|
+
super(message, "WEAK_PASSWORD", { validationErrors, ...details });
|
|
452
|
+
this.validationErrors = validationErrors;
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
var CredentialsNotConfiguredError = class extends LixaError {
|
|
456
|
+
constructor(message = "Credentials authentication is not configured in this Lixa instance. Pass credentials: {} in LixaConfig to enable it.", details) {
|
|
457
|
+
super(message, "CREDENTIALS_NOT_CONFIGURED", details);
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
// src/credentials/credentials-manager.ts
|
|
462
|
+
var CredentialsManager = class {
|
|
463
|
+
storage;
|
|
464
|
+
hasher;
|
|
465
|
+
policy;
|
|
466
|
+
identifierType;
|
|
467
|
+
requireUsername;
|
|
468
|
+
requireEmail;
|
|
469
|
+
timingAttackProtection;
|
|
470
|
+
dummyHash = "";
|
|
471
|
+
constructor(config = {}) {
|
|
472
|
+
this.storage = config.storage || new LocalCredentialsStorage();
|
|
473
|
+
this.hasher = config.hasher || new ScryptPasswordHasher();
|
|
474
|
+
this.policy = {
|
|
475
|
+
minLength: 8,
|
|
476
|
+
maxLength: 128,
|
|
477
|
+
...config.policy
|
|
478
|
+
};
|
|
479
|
+
this.identifierType = config.identifierType || "both";
|
|
480
|
+
this.requireUsername = config.requireUsername ?? false;
|
|
481
|
+
this.requireEmail = config.requireEmail ?? false;
|
|
482
|
+
this.timingAttackProtection = config.timingAttackProtection ?? true;
|
|
483
|
+
this.initDummyHash().catch(() => {
|
|
484
|
+
this.dummyHash = "$scrypt$N=16384,r=8,p=1$0123456789abcdef0123456789abcdef$0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
async initDummyHash() {
|
|
488
|
+
this.dummyHash = await this.hasher.hash("dummy_constant_password_12345");
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Normalizes an identifier string.
|
|
492
|
+
*/
|
|
493
|
+
normalizeIdentifier(identifier) {
|
|
494
|
+
return (identifier || "").trim().toLowerCase();
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Validates identifier format based on configured identifierType.
|
|
498
|
+
*/
|
|
499
|
+
validateIdentifierFormat(identifier) {
|
|
500
|
+
const trimmed = (identifier || "").trim();
|
|
501
|
+
if (!trimmed) {
|
|
502
|
+
throw new InvalidCredentialsError("Identifier cannot be empty");
|
|
503
|
+
}
|
|
504
|
+
if (this.identifierType === "email") {
|
|
505
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
506
|
+
if (!emailRegex.test(trimmed)) {
|
|
507
|
+
throw new InvalidCredentialsError("Identifier must be a valid email address");
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Registers a new user with password hashing and policy enforcement.
|
|
513
|
+
*
|
|
514
|
+
* @param params - Registration parameters
|
|
515
|
+
* @returns Created user credentials without password hash
|
|
516
|
+
*/
|
|
517
|
+
async signUp(params) {
|
|
518
|
+
const rawIdentifier = params.username || params.identifier || params.email || "";
|
|
519
|
+
this.validateIdentifierFormat(rawIdentifier);
|
|
520
|
+
const normalized = this.normalizeIdentifier(rawIdentifier);
|
|
521
|
+
const isEmail = normalized.includes("@");
|
|
522
|
+
const email = params.email ? params.email.trim().toLowerCase() : isEmail ? normalized : void 0;
|
|
523
|
+
const username = params.username ? params.username.trim() : !isEmail ? normalized : void 0;
|
|
524
|
+
if (this.requireUsername && (!username || username.trim() === "")) {
|
|
525
|
+
throw new InvalidCredentialsError("Username is required");
|
|
526
|
+
}
|
|
527
|
+
if (this.requireEmail && (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email))) {
|
|
528
|
+
throw new InvalidCredentialsError("A valid email address is required");
|
|
529
|
+
}
|
|
530
|
+
const existing = await this.storage.findUserByIdentifier(normalized);
|
|
531
|
+
if (existing) {
|
|
532
|
+
throw new UserAlreadyExistsError(normalized);
|
|
533
|
+
}
|
|
534
|
+
const policyResult = await validatePasswordPolicy(params.password, this.policy);
|
|
535
|
+
if (!policyResult.valid) {
|
|
536
|
+
throw new WeakPasswordError(policyResult.errors);
|
|
537
|
+
}
|
|
538
|
+
const passwordHash = await this.hasher.hash(params.password);
|
|
539
|
+
const now = Date.now();
|
|
540
|
+
const id = typeof import_crypto2.default.randomUUID === "function" ? import_crypto2.default.randomUUID() : import_crypto2.default.randomBytes(16).toString("hex");
|
|
541
|
+
const user = {
|
|
542
|
+
id,
|
|
543
|
+
identifier: normalized,
|
|
544
|
+
email,
|
|
545
|
+
username,
|
|
546
|
+
passwordHash,
|
|
547
|
+
createdAt: now,
|
|
548
|
+
updatedAt: now,
|
|
549
|
+
metadata: params.metadata
|
|
550
|
+
};
|
|
551
|
+
await this.storage.saveUser(user);
|
|
552
|
+
const { passwordHash: _, ...safeUser } = user;
|
|
553
|
+
return safeUser;
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Verifies credentials against storage with timing attack protection.
|
|
557
|
+
*
|
|
558
|
+
* @param params - Verification parameters
|
|
559
|
+
* @returns User credentials without password hash, or null if verification fails
|
|
560
|
+
*/
|
|
561
|
+
async verifyCredentials(params) {
|
|
562
|
+
const rawIdentifier = params.username || params.identifier;
|
|
563
|
+
if (!rawIdentifier || typeof params.password !== "string") {
|
|
564
|
+
return null;
|
|
565
|
+
}
|
|
566
|
+
const normalized = this.normalizeIdentifier(rawIdentifier);
|
|
567
|
+
const user = await this.storage.findUserByIdentifier(normalized);
|
|
568
|
+
if (!user) {
|
|
569
|
+
if (this.timingAttackProtection) {
|
|
570
|
+
try {
|
|
571
|
+
if (!this.dummyHash) {
|
|
572
|
+
await this.initDummyHash();
|
|
573
|
+
}
|
|
574
|
+
await this.hasher.verify(params.password, this.dummyHash);
|
|
575
|
+
} catch {
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return null;
|
|
579
|
+
}
|
|
580
|
+
const isMatch = await this.hasher.verify(params.password, user.passwordHash);
|
|
581
|
+
if (!isMatch) {
|
|
582
|
+
return null;
|
|
583
|
+
}
|
|
584
|
+
const { passwordHash: _, ...safeUser } = user;
|
|
585
|
+
return safeUser;
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Changes a user's password with old password verification and new password policy enforcement.
|
|
589
|
+
*
|
|
590
|
+
* @param params - Change password parameters
|
|
591
|
+
* @returns True if password was successfully updated
|
|
592
|
+
*/
|
|
593
|
+
async changePassword(params) {
|
|
594
|
+
let user = null;
|
|
595
|
+
const lookupKey = params.username || params.identifier;
|
|
596
|
+
if (params.userId) {
|
|
597
|
+
user = await this.storage.findUserById(params.userId);
|
|
598
|
+
} else if (lookupKey) {
|
|
599
|
+
user = await this.storage.findUserByIdentifier(this.normalizeIdentifier(lookupKey));
|
|
600
|
+
}
|
|
601
|
+
if (!user) {
|
|
602
|
+
if (this.timingAttackProtection) {
|
|
603
|
+
try {
|
|
604
|
+
if (!this.dummyHash) await this.initDummyHash();
|
|
605
|
+
await this.hasher.verify(params.oldPassword, this.dummyHash);
|
|
606
|
+
} catch {
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
throw new UserNotFoundError(params.userId || lookupKey || "unknown");
|
|
610
|
+
}
|
|
611
|
+
const isOldValid = await this.hasher.verify(params.oldPassword, user.passwordHash);
|
|
612
|
+
if (!isOldValid) {
|
|
613
|
+
throw new InvalidCredentialsError("Current password is incorrect");
|
|
614
|
+
}
|
|
615
|
+
const policyResult = await validatePasswordPolicy(params.newPassword, this.policy);
|
|
616
|
+
if (!policyResult.valid) {
|
|
617
|
+
throw new WeakPasswordError(policyResult.errors);
|
|
618
|
+
}
|
|
619
|
+
const newHash = await this.hasher.hash(params.newPassword);
|
|
620
|
+
await this.storage.updatePassword(user.id, newHash);
|
|
621
|
+
return true;
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Finds a user by ID and returns safe user data.
|
|
625
|
+
*/
|
|
626
|
+
async getUserById(id) {
|
|
627
|
+
const user = await this.storage.findUserById(id);
|
|
628
|
+
if (!user) return null;
|
|
629
|
+
const { passwordHash: _, ...safeUser } = user;
|
|
630
|
+
return safeUser;
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* Finds a user by identifier and returns safe user data.
|
|
634
|
+
*/
|
|
635
|
+
async getUserByIdentifier(identifier) {
|
|
636
|
+
const user = await this.storage.findUserByIdentifier(this.normalizeIdentifier(identifier));
|
|
637
|
+
if (!user) return null;
|
|
638
|
+
const { passwordHash: _, ...safeUser } = user;
|
|
639
|
+
return safeUser;
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Gets the underlying storage instance.
|
|
643
|
+
*/
|
|
644
|
+
getStorage() {
|
|
645
|
+
return this.storage;
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Gets the underlying hasher instance.
|
|
649
|
+
*/
|
|
650
|
+
getHasher() {
|
|
651
|
+
return this.hasher;
|
|
652
|
+
}
|
|
653
|
+
};
|
|
64
654
|
|
|
65
655
|
// src/types.ts
|
|
66
656
|
var AccountLinkingStrategy = /* @__PURE__ */ ((AccountLinkingStrategy2) => {
|
|
@@ -71,7 +661,7 @@ var AccountLinkingStrategy = /* @__PURE__ */ ((AccountLinkingStrategy2) => {
|
|
|
71
661
|
|
|
72
662
|
// src/dao/state-cache.ts
|
|
73
663
|
var import_node_cache = __toESM(require("node-cache"), 1);
|
|
74
|
-
var
|
|
664
|
+
var import_crypto3 = require("crypto");
|
|
75
665
|
var LocalStateHandler = class {
|
|
76
666
|
cache;
|
|
77
667
|
stateStorage;
|
|
@@ -91,8 +681,8 @@ var LocalStateHandler = class {
|
|
|
91
681
|
}
|
|
92
682
|
// Default generateState implementation
|
|
93
683
|
async generateState(provider) {
|
|
94
|
-
const state = (0,
|
|
95
|
-
const codeVerifier = (0,
|
|
684
|
+
const state = (0, import_crypto3.randomBytes)(16).toString("hex");
|
|
685
|
+
const codeVerifier = (0, import_crypto3.randomBytes)(32).toString("hex");
|
|
96
686
|
return {
|
|
97
687
|
state,
|
|
98
688
|
data: {
|
|
@@ -109,7 +699,7 @@ var LocalStateHandler = class {
|
|
|
109
699
|
};
|
|
110
700
|
|
|
111
701
|
// src/lixa.ts
|
|
112
|
-
var
|
|
702
|
+
var import_crypto5 = __toESM(require("crypto"), 1);
|
|
113
703
|
|
|
114
704
|
// src/dao/session-cache.ts
|
|
115
705
|
var import_node_cache2 = __toESM(require("node-cache"), 1);
|
|
@@ -248,80 +838,6 @@ async function extractUserInfo(tokenData, providerMetadata) {
|
|
|
248
838
|
return { userInfo };
|
|
249
839
|
}
|
|
250
840
|
|
|
251
|
-
// src/errors.ts
|
|
252
|
-
var LixaError = class extends Error {
|
|
253
|
-
/**
|
|
254
|
-
* Standard error code string.
|
|
255
|
-
*/
|
|
256
|
-
code;
|
|
257
|
-
/**
|
|
258
|
-
* Additional error context data.
|
|
259
|
-
*/
|
|
260
|
-
details;
|
|
261
|
-
constructor(message, code = "LIXA_ERROR", details) {
|
|
262
|
-
super(message);
|
|
263
|
-
this.name = this.constructor.name;
|
|
264
|
-
this.code = code;
|
|
265
|
-
this.details = details;
|
|
266
|
-
Object.setPrototypeOf(this, new.target.prototype);
|
|
267
|
-
}
|
|
268
|
-
};
|
|
269
|
-
var InvalidStateError = class extends LixaError {
|
|
270
|
-
constructor(message = "Invalid or expired state", details) {
|
|
271
|
-
super(message, "INVALID_STATE", details);
|
|
272
|
-
}
|
|
273
|
-
};
|
|
274
|
-
var ProviderNotConfiguredError = class extends LixaError {
|
|
275
|
-
constructor(provider, details) {
|
|
276
|
-
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: '...', ... }`;
|
|
277
|
-
super(message, "PROVIDER_NOT_CONFIGURED", {
|
|
278
|
-
provider,
|
|
279
|
-
...details
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
|
-
};
|
|
283
|
-
var InvalidProviderConfigError = class extends LixaError {
|
|
284
|
-
constructor(message, details) {
|
|
285
|
-
super(message, "INVALID_PROVIDER_CONFIG", details);
|
|
286
|
-
}
|
|
287
|
-
};
|
|
288
|
-
var InvalidOAuthCallbackError = class extends LixaError {
|
|
289
|
-
constructor(message, details) {
|
|
290
|
-
super(message, "INVALID_OAUTH_CALLBACK", details);
|
|
291
|
-
}
|
|
292
|
-
};
|
|
293
|
-
var TokenExchangeError = class extends LixaError {
|
|
294
|
-
status;
|
|
295
|
-
constructor(message, status, details) {
|
|
296
|
-
super(message, "TOKEN_EXCHANGE_FAILED", { status, ...details });
|
|
297
|
-
this.status = status;
|
|
298
|
-
}
|
|
299
|
-
};
|
|
300
|
-
var SessionNotFoundError = class extends LixaError {
|
|
301
|
-
constructor(message = "Active session not found or has expired", details) {
|
|
302
|
-
super(message, "SESSION_NOT_FOUND", details);
|
|
303
|
-
}
|
|
304
|
-
};
|
|
305
|
-
var EmailNotVerifiedError = class extends LixaError {
|
|
306
|
-
constructor(email, details) {
|
|
307
|
-
super(
|
|
308
|
-
`Cannot link account: email '${email || "unknown"}' is not verified by the identity provider`,
|
|
309
|
-
"EMAIL_NOT_VERIFIED",
|
|
310
|
-
{ email, ...details }
|
|
311
|
-
);
|
|
312
|
-
}
|
|
313
|
-
};
|
|
314
|
-
var AccountUnlinkError = class extends LixaError {
|
|
315
|
-
constructor(message, details) {
|
|
316
|
-
super(message, "ACCOUNT_UNLINK_ERROR", details);
|
|
317
|
-
}
|
|
318
|
-
};
|
|
319
|
-
var RefreshTokenError = class extends LixaError {
|
|
320
|
-
constructor(message, details) {
|
|
321
|
-
super(message, "REFRESH_TOKEN_ERROR", details);
|
|
322
|
-
}
|
|
323
|
-
};
|
|
324
|
-
|
|
325
841
|
// src/lixa.ts
|
|
326
842
|
var Lixa = class _Lixa {
|
|
327
843
|
static DEFAULT_PROVIDERS = /* @__PURE__ */ new Map();
|
|
@@ -333,6 +849,7 @@ var Lixa = class _Lixa {
|
|
|
333
849
|
localResourceHandler;
|
|
334
850
|
userResourceStore = /* @__PURE__ */ new Map();
|
|
335
851
|
refreshMutexes = /* @__PURE__ */ new Map();
|
|
852
|
+
credentialsManager;
|
|
336
853
|
config;
|
|
337
854
|
stateHandler;
|
|
338
855
|
sessionHandler;
|
|
@@ -391,26 +908,32 @@ var Lixa = class _Lixa {
|
|
|
391
908
|
this.stateHandler = config.stateHandler || this.localStateHandler;
|
|
392
909
|
this.sessionHandler = config.sessionHandler || this.localSessionHandler;
|
|
393
910
|
this.resourceHandler = config.resourceHandler || this.localResourceHandler;
|
|
911
|
+
if (config.credentials !== void 0 && config.credentials.enabled !== false) {
|
|
912
|
+
this.credentialsManager = new CredentialsManager(config.credentials);
|
|
913
|
+
}
|
|
394
914
|
this.log("INFO", "Init", "Initializing Lixa instance", {
|
|
395
|
-
providers: Object.keys(config.providers),
|
|
915
|
+
providers: config.providers ? Object.keys(config.providers) : [],
|
|
916
|
+
credentialsEnabled: this.credentialsManager !== void 0,
|
|
396
917
|
debug: this.debug
|
|
397
918
|
});
|
|
398
|
-
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
919
|
+
if (config.providers) {
|
|
920
|
+
for (const [providerName, providerConfig] of Object.entries(config.providers)) {
|
|
921
|
+
const name = providerName.toLowerCase();
|
|
922
|
+
const typedConfig = providerConfig;
|
|
923
|
+
this.validateProviderConfig(providerName, typedConfig);
|
|
924
|
+
if (typedConfig.provider) {
|
|
925
|
+
this.validateProviderImplementation(providerName, typedConfig.provider);
|
|
926
|
+
this.log("INFO", "Init", `Registered inline provider: ${providerName}`);
|
|
927
|
+
} else {
|
|
928
|
+
if (!_Lixa.DEFAULT_PROVIDERS.has(name) && !_Lixa.CONFIGURED_PROVIDERS.has(name)) {
|
|
929
|
+
this.log("ERROR", "Init", `Provider '${providerName}' not available`);
|
|
930
|
+
throw new ProviderNotConfiguredError(
|
|
931
|
+
providerName,
|
|
932
|
+
{ hint: `Ensure the provider is registered via '@vunexa/lixa-providers' or provided inline.` }
|
|
933
|
+
);
|
|
934
|
+
}
|
|
935
|
+
this.log("INFO", "Init", `Using registered provider: ${providerName}`);
|
|
412
936
|
}
|
|
413
|
-
this.log("INFO", "Init", `Using registered provider: ${providerName}`);
|
|
414
937
|
}
|
|
415
938
|
}
|
|
416
939
|
this.log("INFO", "Init", "Lixa instance initialized successfully");
|
|
@@ -471,7 +994,7 @@ var Lixa = class _Lixa {
|
|
|
471
994
|
* Structured logging with standardized format and custom logger support.
|
|
472
995
|
*
|
|
473
996
|
* @param level - Log level (INFO, WARN, ERROR, DEBUG)
|
|
474
|
-
* @param context - Context of the log (Init, Auth, Token, Session, State, AccountLinking, Resource)
|
|
997
|
+
* @param context - Context of the log (Init, Auth, Token, Session, State, AccountLinking, Resource, Credentials)
|
|
475
998
|
* @param message - Log message
|
|
476
999
|
* @param data - Optional data to log
|
|
477
1000
|
*/
|
|
@@ -508,7 +1031,7 @@ var Lixa = class _Lixa {
|
|
|
508
1031
|
*/
|
|
509
1032
|
isProviderConfigured(provider) {
|
|
510
1033
|
const providerType = provider.toLowerCase();
|
|
511
|
-
return this.config.providers.hasOwnProperty(providerType);
|
|
1034
|
+
return Boolean(this.config.providers && this.config.providers.hasOwnProperty(providerType));
|
|
512
1035
|
}
|
|
513
1036
|
/**
|
|
514
1037
|
* Gets a provider implementation by name.
|
|
@@ -616,7 +1139,7 @@ var Lixa = class _Lixa {
|
|
|
616
1139
|
* The state parameter is used to prevent CSRF attacks in OAuth flows.
|
|
617
1140
|
*/
|
|
618
1141
|
static generateRandomState() {
|
|
619
|
-
return (0,
|
|
1142
|
+
return (0, import_crypto4.randomBytes)(16).toString("hex");
|
|
620
1143
|
}
|
|
621
1144
|
/**
|
|
622
1145
|
* Generates a cryptographically secure code verifier for PKCE flows.
|
|
@@ -653,7 +1176,7 @@ var Lixa = class _Lixa {
|
|
|
653
1176
|
* @internal
|
|
654
1177
|
*/
|
|
655
1178
|
static generateCodeVerifier() {
|
|
656
|
-
return (0,
|
|
1179
|
+
return (0, import_crypto4.randomBytes)(32).toString("hex");
|
|
657
1180
|
}
|
|
658
1181
|
/**
|
|
659
1182
|
* Generates a code challenge from a code verifier for PKCE flows.
|
|
@@ -699,7 +1222,7 @@ var Lixa = class _Lixa {
|
|
|
699
1222
|
* @internal
|
|
700
1223
|
*/
|
|
701
1224
|
static buildCodeChallenge(codeVerifier) {
|
|
702
|
-
const hash =
|
|
1225
|
+
const hash = import_crypto5.default.createHash("sha256").update(codeVerifier).digest("base64");
|
|
703
1226
|
return hash.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
704
1227
|
}
|
|
705
1228
|
/**
|
|
@@ -741,8 +1264,8 @@ var Lixa = class _Lixa {
|
|
|
741
1264
|
await storage.saveState(stateValue, generated.data, 300);
|
|
742
1265
|
} else {
|
|
743
1266
|
this.log("INFO", "State", "Using default state generation");
|
|
744
|
-
stateValue = state || (0,
|
|
745
|
-
codeVerifier = (0,
|
|
1267
|
+
stateValue = state || (0, import_crypto4.randomBytes)(16).toString("hex");
|
|
1268
|
+
codeVerifier = (0, import_crypto4.randomBytes)(32).toString("hex");
|
|
746
1269
|
const storage = this.stateHandler.stateStorage || this.localStateHandler.stateStorage;
|
|
747
1270
|
await storage.saveState(
|
|
748
1271
|
stateValue,
|
|
@@ -950,7 +1473,7 @@ var Lixa = class _Lixa {
|
|
|
950
1473
|
raw: tokens,
|
|
951
1474
|
linkedAt: Date.now()
|
|
952
1475
|
};
|
|
953
|
-
const sessionId = (0,
|
|
1476
|
+
const sessionId = (0, import_crypto4.randomBytes)(32).toString("hex");
|
|
954
1477
|
session.id = sessionId;
|
|
955
1478
|
this.log("INFO", "Session", "Storing session", { sessionId });
|
|
956
1479
|
await sessionStorage.saveSession(sessionId, session, 86400);
|
|
@@ -1067,8 +1590,8 @@ var Lixa = class _Lixa {
|
|
|
1067
1590
|
throw new ProviderNotConfiguredError(String(provider));
|
|
1068
1591
|
}
|
|
1069
1592
|
const providerImpl = this.getProvider(providerType, providerConfig);
|
|
1070
|
-
const stateValue = state || (0,
|
|
1071
|
-
const codeVerifier = (0,
|
|
1593
|
+
const stateValue = state || (0, import_crypto4.randomBytes)(16).toString("hex");
|
|
1594
|
+
const codeVerifier = (0, import_crypto4.randomBytes)(32).toString("hex");
|
|
1072
1595
|
const storage = this.stateHandler.stateStorage || this.localStateHandler.stateStorage;
|
|
1073
1596
|
await storage.saveState(
|
|
1074
1597
|
stateValue,
|
|
@@ -1113,7 +1636,7 @@ var Lixa = class _Lixa {
|
|
|
1113
1636
|
throw new ProviderNotConfiguredError(String(provider));
|
|
1114
1637
|
}
|
|
1115
1638
|
const providerImpl = this.getProvider(providerType, providerConfig);
|
|
1116
|
-
let codeVerifier = (0,
|
|
1639
|
+
let codeVerifier = (0, import_crypto4.randomBytes)(32).toString("hex");
|
|
1117
1640
|
if (state) {
|
|
1118
1641
|
const stateStorage = this.stateHandler.stateStorage || this.localStateHandler.stateStorage;
|
|
1119
1642
|
const cachedState = await stateStorage.getState(state);
|
|
@@ -1372,8 +1895,178 @@ var Lixa = class _Lixa {
|
|
|
1372
1895
|
this.log("INFO", "Token", "Token exchange response received successfully");
|
|
1373
1896
|
return response.json();
|
|
1374
1897
|
}
|
|
1898
|
+
/**
|
|
1899
|
+
* Checks if credentials (username and password) authentication is configured and enabled.
|
|
1900
|
+
*
|
|
1901
|
+
* @returns True if credentials authentication is available
|
|
1902
|
+
*/
|
|
1903
|
+
isCredentialsEnabled() {
|
|
1904
|
+
return this.credentialsManager !== void 0;
|
|
1905
|
+
}
|
|
1906
|
+
/**
|
|
1907
|
+
* Returns the underlying CredentialsManager instance if configured.
|
|
1908
|
+
*/
|
|
1909
|
+
getCredentialsManager() {
|
|
1910
|
+
return this.credentialsManager;
|
|
1911
|
+
}
|
|
1912
|
+
/**
|
|
1913
|
+
* Registers a new user with username/email and password.
|
|
1914
|
+
* Automatically enforces password policy, hashes password with Scrypt/configured hasher,
|
|
1915
|
+
* stores credentials, and creates a session (unless autoCreateSessionOnSignUp is false).
|
|
1916
|
+
*
|
|
1917
|
+
* @param params - Registration parameters (identifier, password, email, username, metadata)
|
|
1918
|
+
* @returns Created user (without password hash) and optional active session
|
|
1919
|
+
* @throws WeakPasswordError if password does not meet policy requirements
|
|
1920
|
+
* @throws UserAlreadyExistsError if identifier is already registered
|
|
1921
|
+
* @throws CredentialsNotConfiguredError if credentials auth is not configured
|
|
1922
|
+
*/
|
|
1923
|
+
async signUp(params) {
|
|
1924
|
+
if (!this.credentialsManager) {
|
|
1925
|
+
throw new CredentialsNotConfiguredError();
|
|
1926
|
+
}
|
|
1927
|
+
this.log("INFO", "Credentials", "Processing signUp request", { identifier: params.identifier });
|
|
1928
|
+
const user = await this.credentialsManager.signUp(params);
|
|
1929
|
+
const autoSession = this.config.credentials?.autoCreateSessionOnSignUp ?? true;
|
|
1930
|
+
if (!autoSession) {
|
|
1931
|
+
return { user };
|
|
1932
|
+
}
|
|
1933
|
+
const sessionId = (0, import_crypto4.randomBytes)(16).toString("hex");
|
|
1934
|
+
const token = (0, import_crypto4.randomBytes)(32).toString("hex");
|
|
1935
|
+
const ttl = this.config.credentials?.sessionTtlSeconds ?? 86400;
|
|
1936
|
+
const credentialsAccount = {
|
|
1937
|
+
provider: "credentials",
|
|
1938
|
+
providerUserId: user.id,
|
|
1939
|
+
email: user.email,
|
|
1940
|
+
accessToken: token,
|
|
1941
|
+
raw: {
|
|
1942
|
+
access_token: token,
|
|
1943
|
+
token_type: "Bearer",
|
|
1944
|
+
expires_in: ttl
|
|
1945
|
+
},
|
|
1946
|
+
linkedAt: Date.now()
|
|
1947
|
+
};
|
|
1948
|
+
const session = {
|
|
1949
|
+
id: sessionId,
|
|
1950
|
+
userId: user.id,
|
|
1951
|
+
email: user.email,
|
|
1952
|
+
token,
|
|
1953
|
+
provider: "credentials",
|
|
1954
|
+
accounts: {
|
|
1955
|
+
credentials: credentialsAccount
|
|
1956
|
+
}
|
|
1957
|
+
};
|
|
1958
|
+
const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
|
|
1959
|
+
await sessionStorage.saveSession(sessionId, session, ttl);
|
|
1960
|
+
this.log("INFO", "Credentials", "Created session on signUp", { userId: user.id, sessionId });
|
|
1961
|
+
return { user, sessionId, session };
|
|
1962
|
+
}
|
|
1963
|
+
/**
|
|
1964
|
+
* Authenticates a user with username/email and password.
|
|
1965
|
+
* Performs constant-time verification with timing attack mitigation, and creates an active session.
|
|
1966
|
+
* If account linking is configured with AUTO_LINK_BY_VERIFIED_EMAIL, merges with existing session.
|
|
1967
|
+
*
|
|
1968
|
+
* @param params - Sign-in parameters (identifier, password)
|
|
1969
|
+
* @returns Authenticated user, session ID, and session object
|
|
1970
|
+
* @throws InvalidCredentialsError if authentication fails
|
|
1971
|
+
* @throws CredentialsNotConfiguredError if credentials auth is not configured
|
|
1972
|
+
*/
|
|
1973
|
+
async signIn(params) {
|
|
1974
|
+
if (!this.credentialsManager) {
|
|
1975
|
+
throw new CredentialsNotConfiguredError();
|
|
1976
|
+
}
|
|
1977
|
+
const lookupKey = params.username || params.identifier;
|
|
1978
|
+
this.log("INFO", "Credentials", "Processing signIn request", { identifier: lookupKey });
|
|
1979
|
+
const user = await this.credentialsManager.verifyCredentials(params);
|
|
1980
|
+
if (!user) {
|
|
1981
|
+
this.log("WARN", "Credentials", "Invalid credentials provided", { identifier: lookupKey });
|
|
1982
|
+
throw new InvalidCredentialsError();
|
|
1983
|
+
}
|
|
1984
|
+
const ttl = this.config.credentials?.sessionTtlSeconds ?? 86400;
|
|
1985
|
+
const token = (0, import_crypto4.randomBytes)(32).toString("hex");
|
|
1986
|
+
const credentialsAccount = {
|
|
1987
|
+
provider: "credentials",
|
|
1988
|
+
providerUserId: user.id,
|
|
1989
|
+
email: user.email,
|
|
1990
|
+
accessToken: token,
|
|
1991
|
+
raw: {
|
|
1992
|
+
access_token: token,
|
|
1993
|
+
token_type: "Bearer",
|
|
1994
|
+
expires_in: ttl
|
|
1995
|
+
},
|
|
1996
|
+
linkedAt: Date.now()
|
|
1997
|
+
};
|
|
1998
|
+
const sessionStorage = this.sessionHandler.sessionStorage || this.localSessionHandler.sessionStorage;
|
|
1999
|
+
const linkingMode = this.config.accountLinking?.mode;
|
|
2000
|
+
const isAutoLink = linkingMode === "AUTO_LINK_BY_VERIFIED_EMAIL" /* AUTO_LINK_BY_VERIFIED_EMAIL */ || linkingMode === "AUTO_LINK_BY_VERIFIED_EMAIL" || linkingMode === "linkByEmail";
|
|
2001
|
+
if (isAutoLink && user.email && sessionStorage.getSessionByEmail) {
|
|
2002
|
+
const existing = await sessionStorage.getSessionByEmail(user.email);
|
|
2003
|
+
if (existing) {
|
|
2004
|
+
const mergedSession = { ...existing.session };
|
|
2005
|
+
mergedSession.accounts = { ...mergedSession.accounts || {}, credentials: credentialsAccount };
|
|
2006
|
+
if (!mergedSession.userId) mergedSession.userId = user.id;
|
|
2007
|
+
if (!mergedSession.email) mergedSession.email = user.email;
|
|
2008
|
+
await sessionStorage.saveSession(existing.sessionId, mergedSession, ttl);
|
|
2009
|
+
this.log("INFO", "AccountLinking", "Linked credentials to existing session by email", {
|
|
2010
|
+
sessionId: existing.sessionId,
|
|
2011
|
+
email: user.email
|
|
2012
|
+
});
|
|
2013
|
+
return {
|
|
2014
|
+
user,
|
|
2015
|
+
sessionId: existing.sessionId,
|
|
2016
|
+
session: mergedSession
|
|
2017
|
+
};
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
const sessionId = (0, import_crypto4.randomBytes)(16).toString("hex");
|
|
2021
|
+
const session = {
|
|
2022
|
+
id: sessionId,
|
|
2023
|
+
userId: user.id,
|
|
2024
|
+
email: user.email,
|
|
2025
|
+
token,
|
|
2026
|
+
provider: "credentials",
|
|
2027
|
+
accounts: {
|
|
2028
|
+
credentials: credentialsAccount
|
|
2029
|
+
}
|
|
2030
|
+
};
|
|
2031
|
+
await sessionStorage.saveSession(sessionId, session, ttl);
|
|
2032
|
+
this.log("INFO", "Credentials", "User signed in successfully", { userId: user.id, sessionId });
|
|
2033
|
+
return { user, sessionId, session };
|
|
2034
|
+
}
|
|
2035
|
+
/**
|
|
2036
|
+
* Verifies username/email and password credentials without generating a session.
|
|
2037
|
+
*
|
|
2038
|
+
* @param params - Verification parameters (identifier, password)
|
|
2039
|
+
* @returns User credentials (without password hash) or null if invalid
|
|
2040
|
+
* @throws CredentialsNotConfiguredError if credentials auth is not configured
|
|
2041
|
+
*/
|
|
2042
|
+
async verifyCredentials(params) {
|
|
2043
|
+
if (!this.credentialsManager) {
|
|
2044
|
+
throw new CredentialsNotConfiguredError();
|
|
2045
|
+
}
|
|
2046
|
+
return this.credentialsManager.verifyCredentials(params);
|
|
2047
|
+
}
|
|
2048
|
+
/**
|
|
2049
|
+
* Updates a user's password after verifying the current password and enforcing policy on the new password.
|
|
2050
|
+
*
|
|
2051
|
+
* @param params - Change password parameters (userId/identifier, oldPassword, newPassword)
|
|
2052
|
+
* @returns True if password was updated successfully
|
|
2053
|
+
* @throws UserNotFoundError if user is not found
|
|
2054
|
+
* @throws InvalidCredentialsError if current password is incorrect
|
|
2055
|
+
* @throws WeakPasswordError if new password does not meet policy requirements
|
|
2056
|
+
* @throws CredentialsNotConfiguredError if credentials auth is not configured
|
|
2057
|
+
*/
|
|
2058
|
+
async changePassword(params) {
|
|
2059
|
+
if (!this.credentialsManager) {
|
|
2060
|
+
throw new CredentialsNotConfiguredError();
|
|
2061
|
+
}
|
|
2062
|
+
this.log("INFO", "Credentials", "Processing changePassword request", {
|
|
2063
|
+
userId: params.userId,
|
|
2064
|
+
identifier: params.identifier
|
|
2065
|
+
});
|
|
2066
|
+
return this.credentialsManager.changePassword(params);
|
|
2067
|
+
}
|
|
1375
2068
|
findProviderByType(providerType) {
|
|
1376
|
-
for (const [key, value] of Object.entries(this.config.providers)) {
|
|
2069
|
+
for (const [key, value] of Object.entries(this.config.providers || {})) {
|
|
1377
2070
|
if (key.toLowerCase() === providerType.toLowerCase()) {
|
|
1378
2071
|
return value;
|
|
1379
2072
|
}
|
|
@@ -1500,33 +2193,54 @@ function clearStateCookie(options) {
|
|
|
1500
2193
|
header
|
|
1501
2194
|
};
|
|
1502
2195
|
}
|
|
2196
|
+
|
|
2197
|
+
// src/credentials/index.ts
|
|
2198
|
+
var credentials_exports = {};
|
|
2199
|
+
__export(credentials_exports, {
|
|
2200
|
+
CredentialsManager: () => CredentialsManager,
|
|
2201
|
+
LocalCredentialsStorage: () => LocalCredentialsStorage,
|
|
2202
|
+
Pbkdf2PasswordHasher: () => Pbkdf2PasswordHasher,
|
|
2203
|
+
ScryptPasswordHasher: () => ScryptPasswordHasher,
|
|
2204
|
+
validatePasswordPolicy: () => validatePasswordPolicy
|
|
2205
|
+
});
|
|
1503
2206
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1504
2207
|
0 && (module.exports = {
|
|
1505
2208
|
AccountLinkingStrategy,
|
|
1506
2209
|
AccountUnlinkError,
|
|
2210
|
+
CredentialsManager,
|
|
2211
|
+
CredentialsNotConfiguredError,
|
|
1507
2212
|
DEFAULT_SESSION_COOKIE_NAME,
|
|
1508
2213
|
DEFAULT_SESSION_MAX_AGE_SECONDS,
|
|
1509
2214
|
DEFAULT_STATE_COOKIE_NAME,
|
|
1510
2215
|
DEFAULT_STATE_MAX_AGE_SECONDS,
|
|
1511
2216
|
EmailNotVerifiedError,
|
|
2217
|
+
InvalidCredentialsError,
|
|
1512
2218
|
InvalidOAuthCallbackError,
|
|
1513
2219
|
InvalidProviderConfigError,
|
|
1514
2220
|
InvalidStateError,
|
|
1515
2221
|
Lixa,
|
|
1516
2222
|
LixaError,
|
|
2223
|
+
LocalCredentialsStorage,
|
|
2224
|
+
Pbkdf2PasswordHasher,
|
|
1517
2225
|
ProviderNotConfiguredError,
|
|
1518
2226
|
RefreshTokenError,
|
|
2227
|
+
ScryptPasswordHasher,
|
|
1519
2228
|
SessionNotFoundError,
|
|
1520
2229
|
TokenExchangeError,
|
|
2230
|
+
UserAlreadyExistsError,
|
|
2231
|
+
UserNotFoundError,
|
|
2232
|
+
WeakPasswordError,
|
|
1521
2233
|
clearSessionCookie,
|
|
1522
2234
|
clearStateCookie,
|
|
1523
2235
|
createSessionCookie,
|
|
1524
2236
|
createStateCookie,
|
|
2237
|
+
credentials,
|
|
1525
2238
|
decodeIdToken,
|
|
1526
2239
|
determineProviderFromIssuer,
|
|
1527
2240
|
extractUserInfo,
|
|
1528
2241
|
fetchUserInfo,
|
|
1529
2242
|
isProductionEnvironment,
|
|
1530
|
-
serializeCookie
|
|
2243
|
+
serializeCookie,
|
|
2244
|
+
validatePasswordPolicy
|
|
1531
2245
|
});
|
|
1532
2246
|
//# sourceMappingURL=index.cjs.map
|