@absolutejs/auth 0.27.0-beta.6 → 0.27.0-beta.8

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.
@@ -2,14 +2,18 @@ import type { AuditEvent, AuditSink } from './types';
2
2
  export type AuditIntegrity = {
3
3
  hash: string;
4
4
  previousHash: string;
5
+ writerId?: string;
5
6
  };
6
7
  export type AuditChainResult = {
7
8
  brokenAt?: number;
8
9
  ok: boolean;
9
10
  };
10
- export declare const createTamperEvidentSink: ({ secret, sink }: {
11
+ export declare const createTamperEvidentSink: ({ loadWriterHead, secret, seedScanLimit, sink, writerId }: {
12
+ loadWriterHead?: (writerId: string) => Promise<string | undefined> | string | undefined;
11
13
  secret?: string;
14
+ seedScanLimit?: number;
12
15
  sink: AuditSink;
16
+ writerId?: string;
13
17
  }) => AuditSink;
14
18
  export declare const hashAuditEvent: (event: AuditEvent, previousHash: string, secret?: string) => Promise<string>;
15
19
  export declare const verifyAuditChain: (events: AuditEvent[], secret?: string) => Promise<AuditChainResult>;
@@ -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>;
@@ -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";
@@ -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";
package/dist/index.js CHANGED
@@ -3391,8 +3391,60 @@ var credentialsEmailVerification = ({
3391
3391
 
3392
3392
  // src/credentials/login.ts
3393
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
3394
3445
  var credentialsLogin = ({
3395
3446
  authSessionStore,
3447
+ checkBreachesOnLogin,
3396
3448
  credentialStore,
3397
3449
  getUserByEmail,
3398
3450
  isMfaRequired,
@@ -3451,6 +3503,7 @@ var credentialsLogin = ({
3451
3503
  await persistWhen(authSessionStore !== undefined, compatibilityLayer.persist);
3452
3504
  return status("OK", { status: "mfa_required" });
3453
3505
  }
3506
+ const passwordCompromised = checkBreachesOnLogin ? await isPasswordCompromised(password) : false;
3454
3507
  const userSessionId = await promoteToSession({
3455
3508
  authSessionStore,
3456
3509
  cookie: user_session_id,
@@ -3459,7 +3512,7 @@ var credentialsLogin = ({
3459
3512
  user
3460
3513
  });
3461
3514
  await onCredentialsLoginSuccess?.({ user, userSessionId });
3462
- return status("OK", { status: "authenticated" });
3515
+ return status("OK", { passwordCompromised, status: "authenticated" });
3463
3516
  }, {
3464
3517
  body: t6.Object({ email: t6.String(), password: t6.String() }),
3465
3518
  cookie: t6.Cookie({ user_session_id: userSessionIdTypebox })
@@ -3467,57 +3520,6 @@ var credentialsLogin = ({
3467
3520
 
3468
3521
  // src/credentials/passwordReset.ts
3469
3522
  import { Elysia as Elysia7, t as t7 } from "elysia";
3470
-
3471
- // src/credentials/passwordPolicy.ts
3472
- var DEFAULT_MIN_LENGTH = 12;
3473
- var HEX_RADIX = 16;
3474
- var HIBP_PREFIX_LENGTH = 5;
3475
- var HIBP_RANGE_URL = "https://api.pwnedpasswords.com/range/";
3476
- var sha1Hex = async (input) => {
3477
- const digest = await crypto.subtle.digest("SHA-1", new TextEncoder().encode(input));
3478
- return [...new Uint8Array(digest)].map((byte) => byte.toString(HEX_RADIX).padStart(2, "0")).join("").toUpperCase();
3479
- };
3480
- var isPasswordBreached = async (password) => {
3481
- try {
3482
- const hash = await sha1Hex(password);
3483
- const prefix = hash.slice(0, HIBP_PREFIX_LENGTH);
3484
- const suffix = hash.slice(HIBP_PREFIX_LENGTH);
3485
- const response = await fetch(`${HIBP_RANGE_URL}${prefix}`);
3486
- if (!response.ok)
3487
- return false;
3488
- const body = await response.text();
3489
- return body.split(`
3490
- `).some((line) => line.split(":")[0]?.trim() === suffix);
3491
- } catch {
3492
- return false;
3493
- }
3494
- };
3495
- var evaluatePassword = async (password, policy = {}) => {
3496
- const minLength = policy.minLength ?? DEFAULT_MIN_LENGTH;
3497
- const violations = [];
3498
- if (password.length < minLength) {
3499
- violations.push("too_short");
3500
- }
3501
- if (policy.requireUppercase && !/[A-Z]/u.test(password)) {
3502
- violations.push("missing_uppercase");
3503
- }
3504
- if (policy.requireLowercase && !/[a-z]/u.test(password)) {
3505
- violations.push("missing_lowercase");
3506
- }
3507
- if (policy.requireDigit && !/\d/u.test(password)) {
3508
- violations.push("missing_digit");
3509
- }
3510
- if (policy.requireSymbol && !/[^A-Za-z0-9]/u.test(password)) {
3511
- violations.push("missing_symbol");
3512
- }
3513
- if (policy.checkBreaches && await isPasswordBreached(password)) {
3514
- violations.push("breached");
3515
- }
3516
- return { ok: violations.length === 0, violations };
3517
- };
3518
- var isPasswordCompromised = (password) => isPasswordBreached(password);
3519
-
3520
- // src/credentials/passwordReset.ts
3521
3523
  var credentialsPasswordReset = ({
3522
3524
  credentialStore,
3523
3525
  onPasswordReset,
@@ -18708,6 +18710,7 @@ var INTEGRITY_KEY = "__integrity";
18708
18710
  var GENESIS = "";
18709
18711
  var HEX_RADIX2 = 16;
18710
18712
  var HEX_PAD = 2;
18713
+ var DEFAULT_SEED_SCAN_LIMIT = 1000;
18711
18714
  var encoder = new TextEncoder;
18712
18715
  var toHex = (buffer) => [...new Uint8Array(buffer)].map((byte) => byte.toString(HEX_RADIX2).padStart(HEX_PAD, "0")).join("");
18713
18716
  var sha256Hex = async (message) => toHex(await crypto.subtle.digest("SHA-256", encoder.encode(message)));
@@ -18738,15 +18741,31 @@ var brokenAt = (index) => {
18738
18741
  return result;
18739
18742
  };
18740
18743
  var createTamperEvidentSink = ({
18744
+ loadWriterHead,
18741
18745
  secret,
18742
- sink
18746
+ seedScanLimit = DEFAULT_SEED_SCAN_LIMIT,
18747
+ sink,
18748
+ writerId
18743
18749
  }) => {
18750
+ const chainWriterId = writerId ?? crypto.randomUUID();
18751
+ const isResuming = writerId !== undefined;
18744
18752
  let lastHash;
18753
+ let seeded = false;
18745
18754
  const seed = async () => {
18746
- if (lastHash !== undefined)
18755
+ if (seeded)
18756
+ return;
18757
+ seeded = true;
18758
+ if (!isResuming) {
18759
+ lastHash = GENESIS;
18760
+ return;
18761
+ }
18762
+ if (loadWriterHead) {
18763
+ lastHash = await loadWriterHead(chainWriterId) ?? GENESIS;
18747
18764
  return;
18748
- const [recent] = await sink.list?.({ limit: 1 }) ?? [];
18749
- lastHash = recent ? readIntegrity(recent)?.hash ?? GENESIS : GENESIS;
18765
+ }
18766
+ const recent = await sink.list?.({ limit: seedScanLimit }) ?? [];
18767
+ const head = recent.find((event) => readIntegrity(event)?.writerId === chainWriterId);
18768
+ lastHash = head ? readIntegrity(head)?.hash ?? GENESIS : GENESIS;
18750
18769
  };
18751
18770
  return {
18752
18771
  list: sink.list,
@@ -18755,7 +18774,11 @@ var createTamperEvidentSink = ({
18755
18774
  const previousHash = lastHash ?? GENESIS;
18756
18775
  const hash = await hashAuditEvent(event, previousHash, secret);
18757
18776
  lastHash = hash;
18758
- const integrity = { hash, previousHash };
18777
+ const integrity = {
18778
+ hash,
18779
+ previousHash,
18780
+ writerId: chainWriterId
18781
+ };
18759
18782
  await sink.append({
18760
18783
  ...event,
18761
18784
  metadata: { ...event.metadata, [INTEGRITY_KEY]: integrity }
@@ -18771,17 +18794,19 @@ var hashAuditEvent = async (event, previousHash, secret) => {
18771
18794
  return secret === undefined ? sha256Hex(message) : hmacSha256Hex(secret, message);
18772
18795
  };
18773
18796
  var verifyAuditChain = async (events, secret) => {
18774
- let previousHash = GENESIS;
18797
+ const heads = new Map;
18775
18798
  for (let index = 0;index < events.length; index += 1) {
18776
18799
  const event = events[index];
18777
18800
  if (event === undefined)
18778
18801
  return brokenAt(index);
18779
18802
  const integrity = readIntegrity(event);
18803
+ const chain = integrity?.writerId ?? GENESIS;
18804
+ const previousHash = heads.get(chain) ?? GENESIS;
18780
18805
  const expected = await hashAuditEvent(event, previousHash, secret);
18781
18806
  if (integrity === undefined || integrity.previousHash !== previousHash || integrity.hash !== expected) {
18782
18807
  return brokenAt(index);
18783
18808
  }
18784
- previousHash = integrity.hash;
18809
+ heads.set(chain, integrity.hash);
18785
18810
  }
18786
18811
  const valid = { ok: true };
18787
18812
  return valid;
@@ -20974,5 +20999,5 @@ export {
20974
20999
  AuthIdentityConflictError
20975
21000
  };
20976
21001
 
20977
- //# debugId=D6A51261327D341C64756E2164756E21
21002
+ //# debugId=B234E4311897F15C64756E2164756E21
20978
21003
  //# sourceMappingURL=index.js.map