code_challenge.ts (2339B)
1 import { Code } from "#core/domain/code.ts"; 2 import { isSafeEqual, nonceCode } from "#core/domain/crypto.ts"; 3 import { Ephemeral } from "#core/domain/ephemeral.ts"; 4 import { DomainError } from "#core/domain/error.ts"; 5 import { Limiter } from "#core/domain/limiter.ts"; 6 7 export class CodeChallengeError extends DomainError { 8 } 9 10 export class AlreadyVerifiedCodeChallenge extends CodeChallengeError { 11 constructor(options?: ErrorOptions) { 12 super("Code challenge already verified", options); 13 } 14 } 15 16 export class InvalidCodeChallenge extends CodeChallengeError { 17 constructor(readonly delay: number, options?: ErrorOptions) { 18 super("Invalid code challenge", options); 19 } 20 } 21 22 export class CodeChallenge { 23 constructor( 24 readonly TTL: number, 25 readonly REQUEST_LIMIT: number, 26 readonly ATTEMPT_LIMIT: number, 27 private _verified: boolean = false, 28 private _code: Ephemeral<Code> = Ephemeral.empty(), 29 private _request: Limiter = new Limiter(REQUEST_LIMIT, TTL), 30 private _attempt: Limiter = new Limiter(ATTEMPT_LIMIT, TTL), 31 ) {} 32 33 get verified() { 34 return this._verified; 35 } 36 37 get code() { 38 return this._code.valueOf(); 39 } 40 41 get request() { 42 return this._request.count; 43 } 44 45 get requestDelay() { 46 return this._request.delay; 47 } 48 49 get attempt() { 50 return this._attempt.count; 51 } 52 53 get attemptDelay() { 54 return this._attempt.delay; 55 } 56 57 get codeExpire() { 58 return this._code.expire; 59 } 60 61 get requestExpire() { 62 return this._request.expire; 63 } 64 65 get attemptExpire() { 66 return this._attempt.expire; 67 } 68 69 requestChallenge(): Code { 70 if (this.verified) { 71 throw new AlreadyVerifiedCodeChallenge(); 72 } 73 74 this._request.increment(); 75 const code = nonceCode(); 76 this._code = Ephemeral.of(code, this.TTL); 77 this._attempt = new Limiter(this.ATTEMPT_LIMIT, this.TTL); 78 return code; 79 } 80 81 attemptChallenge(candidate: Code): void { 82 if (this.verified) { 83 throw new AlreadyVerifiedCodeChallenge(); 84 } 85 this._attempt.increment(); 86 this._verified = isSafeEqual(this.code, candidate); 87 if (!this.verified) { 88 throw new InvalidCodeChallenge(this.attemptDelay); 89 } 90 this._code = Ephemeral.empty(); 91 this._request = new Limiter(this.REQUEST_LIMIT, this.TTL); 92 this._attempt = new Limiter(this.ATTEMPT_LIMIT, this.TTL); 93 } 94 }