diff options
Diffstat (limited to 'packages/taler-util/src/http-client/challenger.ts')
-rw-r--r-- | packages/taler-util/src/http-client/challenger.ts | 291 |
1 files changed, 291 insertions, 0 deletions
diff --git a/packages/taler-util/src/http-client/challenger.ts b/packages/taler-util/src/http-client/challenger.ts new file mode 100644 index 000000000..aa530570d --- /dev/null +++ b/packages/taler-util/src/http-client/challenger.ts @@ -0,0 +1,291 @@ +import { HttpRequestLibrary, readTalerErrorResponse } from "../http-common.js"; +import { HttpStatusCode } from "../http-status-codes.js"; +import { createPlatformHttpLib } from "../http.js"; +import { TalerCoreBankCacheEviction } from "../index.node.js"; +import { LibtoolVersion } from "../libtool-version.js"; +import { + FailCasesByMethod, + RedirectResult, + ResultByMethod, + opFixedSuccess, + opKnownAlternativeFailure, + opKnownHttpFailure, + opSuccessFromHttp, + opUnknownFailure, +} from "../operation.js"; +import { + AccessToken, + codecForChallengeCreateResponse, + codecForChallengeSetupResponse, + codecForChallengeStatus, + codecForChallengerAuthResponse, + codecForChallengerInfoResponse, + codecForChallengerTermsOfServiceResponse, + codecForInvalidPinResponse, +} from "./types.js"; +import { CacheEvictor, makeBearerTokenAuthHeader, nullEvictor } from "./utils.js"; + +export type ChallengerResultByMethod<prop extends keyof ChallengerHttpClient> = + ResultByMethod<ChallengerHttpClient, prop>; +export type ChallengerErrorsByMethod<prop extends keyof ChallengerHttpClient> = + FailCasesByMethod<ChallengerHttpClient, prop>; + +export enum ChallengerCacheEviction { + CREATE_CHALLENGE, +} + +/** + */ +export class ChallengerHttpClient { + httpLib: HttpRequestLibrary; + cacheEvictor: CacheEvictor<ChallengerCacheEviction>; + public readonly PROTOCOL_VERSION = "1:0:0"; + + constructor( + readonly baseUrl: string, + httpClient?: HttpRequestLibrary, + cacheEvictor?: CacheEvictor<ChallengerCacheEviction>, + ) { + this.httpLib = httpClient ?? createPlatformHttpLib(); + this.cacheEvictor = cacheEvictor ?? nullEvictor; + } + + isCompatible(version: string): boolean { + const compare = LibtoolVersion.compare(this.PROTOCOL_VERSION, version); + return compare?.compatible ?? false; + } + /** + * https://docs.taler.net/core/api-challenger.html#get--config + * + */ + async getConfig() { + const url = new URL(`config`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + }); + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp( + resp, + codecForChallengerTermsOfServiceResponse(), + ); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + /** + * https://docs.taler.net/core/api-challenger.html#post--setup-$CLIENT_ID + * + */ + async setup(clientId: string, token: AccessToken) { + const url = new URL(`setup/${clientId}`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + headers: { + Authorization: makeBearerTokenAuthHeader(token), + }, + }); + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForChallengeSetupResponse()); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + // LOGIN + + /** + * https://docs.taler.net/core/api-challenger.html#post--authorize-$NONCE + * + */ + async login( + nonce: string, + clientId: string, + redirectUri: string, + state: string | undefined, + ) { + const url = new URL(`authorize/${nonce}`, this.baseUrl); + url.searchParams.set("response_type", "code"); + url.searchParams.set("client_id", clientId); + url.searchParams.set("redirect_uri", redirectUri); + if (state) { + url.searchParams.set("state", state); + } + // url.searchParams.set("scope", "code"); + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + }); + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForChallengeStatus()); + case HttpStatusCode.BadRequest: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotAcceptable: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.InternalServerError: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + // CHALLENGE + + /** + * https://docs.taler.net/core/api-challenger.html#post--challenge-$NONCE + * + */ + async challenge(nonce: string, body: Record<"email", string>) { + const url = new URL(`challenge/${nonce}`, this.baseUrl); + + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + body: new URLSearchParams(Object.entries(body)).toString(), + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + redirect: "manual", + }); + switch (resp.status) { + case HttpStatusCode.Ok: { + await this.cacheEvictor.notifySuccess( + ChallengerCacheEviction.CREATE_CHALLENGE, + ); + return opSuccessFromHttp(resp, codecForChallengeCreateResponse()); + } + case HttpStatusCode.Found: + const redirect = resp.headers.get("Location")!; + return opFixedSuccess<RedirectResult>({ + redirectURL: new URL(redirect), + }); + case HttpStatusCode.BadRequest: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotAcceptable: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.TooManyRequests: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.InternalServerError: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + // SOLVE + + /** + * https://docs.taler.net/core/api-challenger.html#post--solve-$NONCE + * + */ + async solve(nonce: string, body: Record<string, string>) { + const url = new URL(`solve/${nonce}`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + body: new URLSearchParams(Object.entries(body)).toString(), + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + redirect: "manual", + }); + switch (resp.status) { + case HttpStatusCode.Found: + const redirect = resp.headers.get("Location")!; + return opFixedSuccess<RedirectResult>({ + redirectURL: new URL(redirect), + }); + case HttpStatusCode.BadRequest: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Forbidden: + return opKnownAlternativeFailure( + resp, + resp.status, + codecForInvalidPinResponse(), + ); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotAcceptable: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.TooManyRequests: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.InternalServerError: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + // AUTH + + /** + * https://docs.taler.net/core/api-challenger.html#post--token + * + */ + async token( + client_id: string, + redirect_uri: string, + client_secret: AccessToken, + code: string, + ) { + const url = new URL(`token`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: new URLSearchParams( + Object.entries({ + client_id, + redirect_uri, + client_secret, + code, + grant_type: "authorization_code", + }), + ).toString(), + }); + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForChallengerAuthResponse()); + case HttpStatusCode.Forbidden: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + // INFO + + /** + * https://docs.taler.net/core/api-challenger.html#get--info + * + */ + async info(token: AccessToken) { + const url = new URL(`info`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + headers: { + Authorization: makeBearerTokenAuthHeader(token), + }, + }); + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForChallengerInfoResponse()); + case HttpStatusCode.Forbidden: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } +} |