@absolutejs/auth 0.27.0-beta.5 → 0.27.0-beta.7

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.
@@ -19,6 +19,7 @@ export type CredentialEmailMessage = {
19
19
  type: CredentialEmailType;
20
20
  };
21
21
  export type CredentialsConfig<UserType> = {
22
+ checkBreachesOnLogin?: boolean;
22
23
  credentialStore: CredentialStore;
23
24
  getUserByEmail: (email: string) => Promise<UserType | null | undefined> | UserType | null | undefined;
24
25
  isMfaRequired?: (user: UserType) => boolean | Promise<boolean>;
@@ -0,0 +1,9 @@
1
+ export type EmailValidationResult = {
2
+ ok: boolean;
3
+ reason?: 'disposable' | 'invalid_format' | 'no_mx';
4
+ };
5
+ export declare const isDisposableEmail: (email: string, extraDomains?: Iterable<string>) => boolean;
6
+ export declare const validateEmailDeliverability: (email: string, options?: {
7
+ checkMx?: boolean;
8
+ disposableDomains?: Iterable<string>;
9
+ }) => Promise<EmailValidationResult>;
@@ -1,6 +1,6 @@
1
1
  import { Elysia } from 'elysia';
2
2
  import { type CredentialRouteProps } from './config';
3
- export declare const credentialsLogin: <UserType>({ authSessionStore, credentialStore, getUserByEmail, isMfaRequired, lockoutGuard, loginRoute, onCredentialsLoginError, onCredentialsLoginSuccess, requireEmailVerification, sessionDurationMs }: CredentialRouteProps<UserType>) => Elysia<"", {
3
+ export declare const credentialsLogin: <UserType>({ authSessionStore, checkBreachesOnLogin, credentialStore, getUserByEmail, isMfaRequired, lockoutGuard, loginRoute, onCredentialsLoginError, onCredentialsLoginSuccess, requireEmailVerification, sessionDurationMs }: CredentialRouteProps<UserType>) => Elysia<"", {
4
4
  decorator: {};
5
5
  store: {
6
6
  session: import("..").SessionRecord<UserType>;
@@ -32,6 +32,7 @@ export declare const credentialsLogin: <UserType>({ authSessionStore, credential
32
32
  200: {
33
33
  readonly status: "mfa_required";
34
34
  } | {
35
+ readonly passwordCompromised: boolean;
35
36
  readonly status: "authenticated";
36
37
  };
37
38
  401: "Invalid email or password";
@@ -12,3 +12,4 @@ export type PasswordPolicyResult = {
12
12
  violations: PasswordPolicyViolation[];
13
13
  };
14
14
  export declare const evaluatePassword: (password: string, policy?: PasswordPolicy) => Promise<PasswordPolicyResult>;
15
+ export declare const isPasswordCompromised: (password: string) => Promise<boolean>;
@@ -100,6 +100,7 @@ export declare const credentialRoutes: <UserType>(config: CredentialRouteProps<U
100
100
  200: {
101
101
  readonly status: "mfa_required";
102
102
  } | {
103
+ readonly passwordCompromised: boolean;
103
104
  readonly status: "authenticated";
104
105
  };
105
106
  401: "Invalid email or password";
package/dist/index.d.ts CHANGED
@@ -12725,6 +12725,7 @@ export declare const auth: <UserType>({ providersConfiguration, authorizeRoute,
12725
12725
  200: {
12726
12726
  readonly status: "mfa_required";
12727
12727
  } | {
12728
+ readonly passwordCompromised: boolean;
12728
12729
  readonly status: "authenticated";
12729
12730
  };
12730
12731
  401: "Invalid email or password";
@@ -14652,6 +14653,8 @@ export { sessionRoutes } from './routes/sessions';
14652
14653
  export { stepUpPlugin } from './routes/stepUp';
14653
14654
  export * from './session/sessionsConfig';
14654
14655
  export { endImpersonation, isImpersonating, startImpersonation } from './session/impersonation';
14656
+ export { createAnonymousSession, isAnonymousSession } from './session/anonymous';
14657
+ export { addToSessionRing, listRingSessions, readSessionRing, removeFromSessionRing, switchActiveSession } from './session/multiSession';
14655
14658
  export { listUserSessions, revokeUserSessions } from './session/userSessions';
14656
14659
  export type { UserSession } from './session/userSessions';
14657
14660
  export { sessionCleanup } from './session/cleanup';
@@ -14666,6 +14669,7 @@ export * from './crypto';
14666
14669
  export * from './tenancy';
14667
14670
  export * from './credentials/config';
14668
14671
  export * from './credentials/passwordPolicy';
14672
+ export * from './credentials/emailValidation';
14669
14673
  export * from './credentials/types';
14670
14674
  export { credentialRoutes } from './credentials/routes';
14671
14675
  export { credentialsEmailVerification } from './credentials/emailVerification';
package/dist/index.js CHANGED
@@ -3213,6 +3213,7 @@ var persistWhen = async (shouldPersist, persist) => {
3213
3213
  await persist();
3214
3214
  };
3215
3215
  var promoteToSession = async ({
3216
+ anonymous,
3216
3217
  authSessionStore,
3217
3218
  cookie,
3218
3219
  impersonator,
@@ -3236,6 +3237,8 @@ var promoteToSession = async ({
3236
3237
  data.samlLogout = samlLogout;
3237
3238
  if (impersonator !== undefined)
3238
3239
  data.impersonator = impersonator;
3240
+ if (anonymous === true)
3241
+ data.anonymous = true;
3239
3242
  targetSession[userSessionId] = data;
3240
3243
  cookie.set({
3241
3244
  httpOnly: true,
@@ -3388,8 +3391,60 @@ var credentialsEmailVerification = ({
3388
3391
 
3389
3392
  // src/credentials/login.ts
3390
3393
  import { Elysia as Elysia6, t as t6 } from "elysia";
3394
+
3395
+ // src/credentials/passwordPolicy.ts
3396
+ var DEFAULT_MIN_LENGTH = 12;
3397
+ var HEX_RADIX = 16;
3398
+ var HIBP_PREFIX_LENGTH = 5;
3399
+ var HIBP_RANGE_URL = "https://api.pwnedpasswords.com/range/";
3400
+ var sha1Hex = async (input) => {
3401
+ const digest = await crypto.subtle.digest("SHA-1", new TextEncoder().encode(input));
3402
+ return [...new Uint8Array(digest)].map((byte) => byte.toString(HEX_RADIX).padStart(2, "0")).join("").toUpperCase();
3403
+ };
3404
+ var isPasswordBreached = async (password) => {
3405
+ try {
3406
+ const hash = await sha1Hex(password);
3407
+ const prefix = hash.slice(0, HIBP_PREFIX_LENGTH);
3408
+ const suffix = hash.slice(HIBP_PREFIX_LENGTH);
3409
+ const response = await fetch(`${HIBP_RANGE_URL}${prefix}`);
3410
+ if (!response.ok)
3411
+ return false;
3412
+ const body = await response.text();
3413
+ return body.split(`
3414
+ `).some((line) => line.split(":")[0]?.trim() === suffix);
3415
+ } catch {
3416
+ return false;
3417
+ }
3418
+ };
3419
+ var evaluatePassword = async (password, policy = {}) => {
3420
+ const minLength = policy.minLength ?? DEFAULT_MIN_LENGTH;
3421
+ const violations = [];
3422
+ if (password.length < minLength) {
3423
+ violations.push("too_short");
3424
+ }
3425
+ if (policy.requireUppercase && !/[A-Z]/u.test(password)) {
3426
+ violations.push("missing_uppercase");
3427
+ }
3428
+ if (policy.requireLowercase && !/[a-z]/u.test(password)) {
3429
+ violations.push("missing_lowercase");
3430
+ }
3431
+ if (policy.requireDigit && !/\d/u.test(password)) {
3432
+ violations.push("missing_digit");
3433
+ }
3434
+ if (policy.requireSymbol && !/[^A-Za-z0-9]/u.test(password)) {
3435
+ violations.push("missing_symbol");
3436
+ }
3437
+ if (policy.checkBreaches && await isPasswordBreached(password)) {
3438
+ violations.push("breached");
3439
+ }
3440
+ return { ok: violations.length === 0, violations };
3441
+ };
3442
+ var isPasswordCompromised = (password) => isPasswordBreached(password);
3443
+
3444
+ // src/credentials/login.ts
3391
3445
  var credentialsLogin = ({
3392
3446
  authSessionStore,
3447
+ checkBreachesOnLogin,
3393
3448
  credentialStore,
3394
3449
  getUserByEmail,
3395
3450
  isMfaRequired,
@@ -3448,6 +3503,7 @@ var credentialsLogin = ({
3448
3503
  await persistWhen(authSessionStore !== undefined, compatibilityLayer.persist);
3449
3504
  return status("OK", { status: "mfa_required" });
3450
3505
  }
3506
+ const passwordCompromised = checkBreachesOnLogin ? await isPasswordCompromised(password) : false;
3451
3507
  const userSessionId = await promoteToSession({
3452
3508
  authSessionStore,
3453
3509
  cookie: user_session_id,
@@ -3456,7 +3512,7 @@ var credentialsLogin = ({
3456
3512
  user
3457
3513
  });
3458
3514
  await onCredentialsLoginSuccess?.({ user, userSessionId });
3459
- return status("OK", { status: "authenticated" });
3515
+ return status("OK", { passwordCompromised, status: "authenticated" });
3460
3516
  }, {
3461
3517
  body: t6.Object({ email: t6.String(), password: t6.String() }),
3462
3518
  cookie: t6.Cookie({ user_session_id: userSessionIdTypebox })
@@ -3464,56 +3520,6 @@ var credentialsLogin = ({
3464
3520
 
3465
3521
  // src/credentials/passwordReset.ts
3466
3522
  import { Elysia as Elysia7, t as t7 } from "elysia";
3467
-
3468
- // src/credentials/passwordPolicy.ts
3469
- var DEFAULT_MIN_LENGTH = 12;
3470
- var HEX_RADIX = 16;
3471
- var HIBP_PREFIX_LENGTH = 5;
3472
- var HIBP_RANGE_URL = "https://api.pwnedpasswords.com/range/";
3473
- var sha1Hex = async (input) => {
3474
- const digest = await crypto.subtle.digest("SHA-1", new TextEncoder().encode(input));
3475
- return [...new Uint8Array(digest)].map((byte) => byte.toString(HEX_RADIX).padStart(2, "0")).join("").toUpperCase();
3476
- };
3477
- var isPasswordBreached = async (password) => {
3478
- try {
3479
- const hash = await sha1Hex(password);
3480
- const prefix = hash.slice(0, HIBP_PREFIX_LENGTH);
3481
- const suffix = hash.slice(HIBP_PREFIX_LENGTH);
3482
- const response = await fetch(`${HIBP_RANGE_URL}${prefix}`);
3483
- if (!response.ok)
3484
- return false;
3485
- const body = await response.text();
3486
- return body.split(`
3487
- `).some((line) => line.split(":")[0]?.trim() === suffix);
3488
- } catch {
3489
- return false;
3490
- }
3491
- };
3492
- var evaluatePassword = async (password, policy = {}) => {
3493
- const minLength = policy.minLength ?? DEFAULT_MIN_LENGTH;
3494
- const violations = [];
3495
- if (password.length < minLength) {
3496
- violations.push("too_short");
3497
- }
3498
- if (policy.requireUppercase && !/[A-Z]/u.test(password)) {
3499
- violations.push("missing_uppercase");
3500
- }
3501
- if (policy.requireLowercase && !/[a-z]/u.test(password)) {
3502
- violations.push("missing_lowercase");
3503
- }
3504
- if (policy.requireDigit && !/\d/u.test(password)) {
3505
- violations.push("missing_digit");
3506
- }
3507
- if (policy.requireSymbol && !/[^A-Za-z0-9]/u.test(password)) {
3508
- violations.push("missing_symbol");
3509
- }
3510
- if (policy.checkBreaches && await isPasswordBreached(password)) {
3511
- violations.push("breached");
3512
- }
3513
- return { ok: violations.length === 0, violations };
3514
- };
3515
-
3516
- // src/credentials/passwordReset.ts
3517
3523
  var credentialsPasswordReset = ({
3518
3524
  credentialStore,
3519
3525
  onPasswordReset,
@@ -18141,6 +18147,90 @@ var startImpersonation = async ({
18141
18147
  });
18142
18148
  return sessionId;
18143
18149
  };
18150
+ // src/session/anonymous.ts
18151
+ var DEFAULT_GUEST_TTL_MS = MILLISECONDS_IN_A_DAY;
18152
+ var createAnonymousSession = async ({
18153
+ authSessionStore,
18154
+ cookie,
18155
+ guestUser,
18156
+ inMemorySession,
18157
+ sessionDurationMs = DEFAULT_GUEST_TTL_MS
18158
+ }) => promoteToSession({
18159
+ anonymous: true,
18160
+ authSessionStore,
18161
+ cookie,
18162
+ inMemorySession,
18163
+ sessionDurationMs,
18164
+ user: guestUser
18165
+ });
18166
+ var isAnonymousSession = (session) => session?.anonymous === true;
18167
+ // src/session/multiSession.ts
18168
+ var SEPARATOR = " ";
18169
+ var writeRing = (ring, ids) => ring.set({
18170
+ httpOnly: true,
18171
+ sameSite: "lax",
18172
+ secure: true,
18173
+ value: ids.join(SEPARATOR)
18174
+ });
18175
+ var readRing = (ring) => (ring.value ?? "").split(SEPARATOR).filter((entry) => isUserSessionId(entry));
18176
+ var addToSessionRing = (ring, sessionId) => writeRing(ring, [...new Set([...readRing(ring), sessionId])]);
18177
+ var listRingSessions = async ({
18178
+ authSessionStore,
18179
+ inMemorySession,
18180
+ ring
18181
+ }) => {
18182
+ const resolved = await Promise.all(readRing(ring).map(async (sessionId) => {
18183
+ const session = await loadSessionFromSource({
18184
+ authSessionStore,
18185
+ session: inMemorySession,
18186
+ userSessionId: sessionId
18187
+ });
18188
+ return session === undefined ? undefined : { sessionId, user: session.user };
18189
+ }));
18190
+ return resolved.filter((entry) => entry !== undefined);
18191
+ };
18192
+ var readSessionRing = (ring) => readRing(ring);
18193
+ var removeFromSessionRing = async ({
18194
+ activeCookie,
18195
+ authSessionStore,
18196
+ inMemorySession,
18197
+ ring,
18198
+ sessionId
18199
+ }) => {
18200
+ const remaining = readRing(ring).filter((id) => id !== sessionId);
18201
+ writeRing(ring, remaining);
18202
+ if (authSessionStore)
18203
+ await authSessionStore.removeSession(sessionId);
18204
+ else if (inMemorySession)
18205
+ delete inMemorySession[sessionId];
18206
+ if (activeCookie?.value !== sessionId)
18207
+ return;
18208
+ const [fallback] = remaining;
18209
+ if (fallback === undefined)
18210
+ activeCookie.remove();
18211
+ else
18212
+ activeCookie.set({
18213
+ httpOnly: true,
18214
+ sameSite: "lax",
18215
+ secure: true,
18216
+ value: fallback
18217
+ });
18218
+ };
18219
+ var switchActiveSession = ({
18220
+ activeCookie,
18221
+ ring,
18222
+ sessionId
18223
+ }) => {
18224
+ if (!readRing(ring).includes(sessionId))
18225
+ return false;
18226
+ activeCookie.set({
18227
+ httpOnly: true,
18228
+ sameSite: "lax",
18229
+ secure: true,
18230
+ value: sessionId
18231
+ });
18232
+ return true;
18233
+ };
18144
18234
  // src/utils.ts
18145
18235
  var defineAuthConfig = (configuration) => configuration;
18146
18236
  var defineAuthHtmxConfig = (htmxConfig) => htmxConfig;
@@ -18316,6 +18406,48 @@ var getUserSessionId = ({
18316
18406
  };
18317
18407
  // src/tenancy.ts
18318
18408
  var hasOrganizationScope = (value) => typeof value.organizationId === "string" && value.organizationId.length > 0;
18409
+ // src/credentials/emailValidation.ts
18410
+ import { resolveMx } from "dns/promises";
18411
+ var EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/u;
18412
+ var DISPOSABLE_DOMAINS = new Set([
18413
+ "10minutemail.com",
18414
+ "fakeinbox.com",
18415
+ "getnada.com",
18416
+ "guerrillamail.com",
18417
+ "mailinator.com",
18418
+ "maildrop.cc",
18419
+ "sharklasers.com",
18420
+ "temp-mail.org",
18421
+ "tempmail.com",
18422
+ "throwaway.email",
18423
+ "trashmail.com",
18424
+ "yopmail.com"
18425
+ ]);
18426
+ var domainOf = (email) => email.slice(email.lastIndexOf("@") + 1).toLowerCase();
18427
+ var hasMxRecord = async (domain) => {
18428
+ try {
18429
+ return (await resolveMx(domain)).length > 0;
18430
+ } catch {
18431
+ return false;
18432
+ }
18433
+ };
18434
+ var isDisposableEmail = (email, extraDomains) => {
18435
+ const domain = domainOf(email);
18436
+ return DISPOSABLE_DOMAINS.has(domain) || extraDomains !== undefined && new Set(extraDomains).has(domain);
18437
+ };
18438
+ var validateEmailDeliverability = async (email, options) => {
18439
+ const normalized = email.trim().toLowerCase();
18440
+ if (!EMAIL_PATTERN.test(normalized)) {
18441
+ return { ok: false, reason: "invalid_format" };
18442
+ }
18443
+ if (isDisposableEmail(normalized, options?.disposableDomains)) {
18444
+ return { ok: false, reason: "disposable" };
18445
+ }
18446
+ if (options?.checkMx === true && !await hasMxRecord(domainOf(normalized))) {
18447
+ return { ok: false, reason: "no_mx" };
18448
+ }
18449
+ return { ok: true };
18450
+ };
18319
18451
  // src/credentials/inMemoryCredentialStore.ts
18320
18452
  var cloneCredential = (value) => ({
18321
18453
  ...value
@@ -20580,9 +20712,11 @@ export {
20580
20712
  verifyApiKey,
20581
20713
  verifyAccessToken,
20582
20714
  validateSession,
20715
+ validateEmailDeliverability,
20583
20716
  userSessionIdTypebox,
20584
20717
  trustDevice,
20585
20718
  toPublicJwk,
20719
+ switchActiveSession,
20586
20720
  stepUpPlugin,
20587
20721
  startImpersonation,
20588
20722
  ssoDiscoveryRoute,
@@ -20612,8 +20746,10 @@ export {
20612
20746
  resolveClientProviderEntry,
20613
20747
  resolveAuthHtmxRenderers,
20614
20748
  resolveApiPrincipal,
20749
+ removeFromSessionRing,
20615
20750
  refreshableProviderOptions,
20616
20751
  recordLoginAttempt,
20752
+ readSessionRing,
20617
20753
  providers,
20618
20754
  providerOptions,
20619
20755
  protectRoutePlugin,
@@ -20642,6 +20778,7 @@ export {
20642
20778
  listUserSessions,
20643
20779
  listUserOrganizations,
20644
20780
  listSubjects,
20781
+ listRingSessions,
20645
20782
  knownDevicesTable,
20646
20783
  jwkThumbprint,
20647
20784
  issueTokenSet,
@@ -20652,11 +20789,14 @@ export {
20652
20789
  isRevocableOAuth2Client,
20653
20790
  isRefreshableProviderOption,
20654
20791
  isRefreshableOAuth2Client,
20792
+ isPasswordCompromised,
20655
20793
  isPKCEProviderOption,
20656
20794
  isOIDCProviderOption,
20657
20795
  isMfaEnrolled,
20658
20796
  isImpersonating,
20797
+ isDisposableEmail,
20659
20798
  isAuthIntent,
20799
+ isAnonymousSession,
20660
20800
  inviteToOrganization,
20661
20801
  instantiateUserSession,
20662
20802
  hashToken,
@@ -20784,6 +20924,7 @@ export {
20784
20924
  createAuditEmitter,
20785
20925
  createApiKey,
20786
20926
  createApiClient,
20927
+ createAnonymousSession,
20787
20928
  createAbuseGuard,
20788
20929
  consumeBackupCode,
20789
20930
  constantTimeEqual,
@@ -20802,6 +20943,7 @@ export {
20802
20943
  apiKeysTable,
20803
20944
  apiKeysRoutes,
20804
20945
  apiClientsTable,
20946
+ addToSessionRing,
20805
20947
  accessTokensTable,
20806
20948
  acceptInvitation,
20807
20949
  WEBAUTHN_CHALLENGE_COOKIE,
@@ -20834,5 +20976,5 @@ export {
20834
20976
  AuthIdentityConflictError
20835
20977
  };
20836
20978
 
20837
- //# debugId=514E4AE78CF406D164756E2164756E21
20979
+ //# debugId=580D3E8ACE9EF4E864756E2164756E21
20838
20980
  //# sourceMappingURL=index.js.map