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 = ResultByMethod; export type ChallengerErrorsByMethod = FailCasesByMethod; export enum ChallengerCacheEviction { CREATE_CHALLENGE, } /** */ export class ChallengerHttpClient { httpLib: HttpRequestLibrary; cacheEvictor: CacheEvictor; public readonly PROTOCOL_VERSION = "1:0:0"; constructor( readonly baseUrl: string, httpClient?: HttpRequestLibrary, cacheEvictor?: CacheEvictor, ) { 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({ 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) { 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({ 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)); } } }