diff options
Diffstat (limited to 'packages/taler-util/src/http-client')
-rw-r--r-- | packages/taler-util/src/http-client/README.md | 19 | ||||
-rw-r--r-- | packages/taler-util/src/http-client/authentication.ts | 95 | ||||
-rw-r--r-- | packages/taler-util/src/http-client/bank-conversion.ts | 184 | ||||
-rw-r--r-- | packages/taler-util/src/http-client/bank-core.ts | 950 | ||||
-rw-r--r-- | packages/taler-util/src/http-client/bank-integration.ts | 149 | ||||
-rw-r--r-- | packages/taler-util/src/http-client/bank-revenue.ts | 122 | ||||
-rw-r--r-- | packages/taler-util/src/http-client/bank-wire.ts | 203 | ||||
-rw-r--r-- | packages/taler-util/src/http-client/challenger.ts | 291 | ||||
-rw-r--r-- | packages/taler-util/src/http-client/exchange.ts | 221 | ||||
-rw-r--r-- | packages/taler-util/src/http-client/merchant.ts | 2365 | ||||
-rw-r--r-- | packages/taler-util/src/http-client/officer-account.ts | 28 | ||||
-rw-r--r-- | packages/taler-util/src/http-client/types.ts | 2727 | ||||
-rw-r--r-- | packages/taler-util/src/http-client/utils.ts | 102 |
13 files changed, 6408 insertions, 1048 deletions
diff --git a/packages/taler-util/src/http-client/README.md b/packages/taler-util/src/http-client/README.md new file mode 100644 index 000000000..33d1a8645 --- /dev/null +++ b/packages/taler-util/src/http-client/README.md @@ -0,0 +1,19 @@ +## HTTP Cclients + +This folder contain class or function specifically designed to facilitate HTTP client +interactions with a the core systems. + +These API defines: + +1. **API Communication**: Handle communication with the component API, + abstracting away the details of HTTP requests and responses. + This includes making GET, POST, PUT, and DELETE requests to the servers. +2. **Data Formatting**: Responsible for formatting requests to the API in a + way that's expected by the servers (JSON) and parsing the responses back + into formats usable by the client. +3. **Authentication and Security**: Handling authentication with the server API, + which could involve sending API keys, client credentials, or managing tokens. + It might also implement security features to ensure data integrity and confidentiality during transit. +4. **Error Handling**: Providing robust error handling and retry mechanisms + for failed HTTP requests, including logging and potentially user notifications for critical failures. +5. **Data Validation**: Before sending requests, it could validate the data to ensure it meets the API's expected format, types, and value ranges, reducing the likelihood of errors and improving system reliability. diff --git a/packages/taler-util/src/http-client/authentication.ts b/packages/taler-util/src/http-client/authentication.ts index b27a266e9..8897a2fa0 100644 --- a/packages/taler-util/src/http-client/authentication.ts +++ b/packages/taler-util/src/http-client/authentication.ts @@ -14,11 +14,29 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ +/** + * Imports. + */ import { HttpStatusCode } from "../http-status-codes.js"; -import { HttpRequestLibrary, createPlatformHttpLib, makeBasicAuthHeader } from "../http.js"; +import { + HttpRequestLibrary, + createPlatformHttpLib, + makeBasicAuthHeader, + readTalerErrorResponse, +} from "../http.js"; import { LibtoolVersion } from "../libtool-version.js"; -import { opEmptySuccess, opKnownFailure, opSuccess, opUnknownFailure } from "../operation.js"; -import { AccessToken, TalerAuthentication, codecForTokenSuccessResponse } from "./types.js"; +import { + opEmptySuccess, + opKnownHttpFailure, + opSuccessFromHttp, + opUnknownFailure, +} from "../operation.js"; +import { + AccessToken, + TalerAuthentication, + codecForTokenSuccessResponse, + codecForTokenSuccessResponseMerchant, +} from "./types.js"; import { makeBearerTokenAuthHeader } from "./utils.js"; export class TalerAuthenticationHttpClient { @@ -28,23 +46,23 @@ export class TalerAuthenticationHttpClient { constructor( readonly baseUrl: string, - readonly username: string, httpClient?: HttpRequestLibrary, ) { this.httpLib = httpClient ?? createPlatformHttpLib(); } isCompatible(version: string): boolean { - const compare = LibtoolVersion.compare(this.PROTOCOL_VERSION, version) - return compare?.compatible ?? false + const compare = LibtoolVersion.compare(this.PROTOCOL_VERSION, version); + return compare?.compatible ?? false; } /** * https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-token - * - * @returns + * + * @returns */ - async createAccessToken( + async createAccessTokenBasic( + username: string, password: string, body: TalerAuthentication.TokenRequest, ) { @@ -52,16 +70,49 @@ export class TalerAuthenticationHttpClient { const resp = await this.httpLib.fetch(url.href, { method: "POST", headers: { - Authorization: makeBasicAuthHeader(this.username, password), + Authorization: makeBasicAuthHeader(username, password), }, - body + body, }); switch (resp.status) { - case HttpStatusCode.Ok: return opSuccess(resp, codecForTokenSuccessResponse()) + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForTokenSuccessResponse()); //FIXME: missing in docs - case HttpStatusCode.Unauthorized: return opKnownFailure("wrong-credentials", resp) - case HttpStatusCode.NotFound: return opKnownFailure("not-found", resp) - default: return opUnknownFailure(resp, await resp.text()) + case HttpStatusCode.Unauthorized: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + /** + * + * @returns + */ + async createAccessTokenBearer( + token: AccessToken, + body: TalerAuthentication.TokenRequest, + ) { + const url = new URL(`token`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + headers: { + Authorization: makeBearerTokenAuthHeader(token), + }, + body, + }); + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForTokenSuccessResponseMerchant()); + //FIXME: missing in docs + case HttpStatusCode.Unauthorized: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); } } @@ -71,14 +122,16 @@ export class TalerAuthenticationHttpClient { method: "DELETE", headers: { Authorization: makeBearerTokenAuthHeader(token), - } + }, }); switch (resp.status) { - case HttpStatusCode.Ok: return opEmptySuccess() + case HttpStatusCode.Ok: + return opEmptySuccess(resp); //FIXME: missing in docs - case HttpStatusCode.NotFound: return opKnownFailure("not-found", resp) - default: return opUnknownFailure(resp, await resp.text()) + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); } } - -}
\ No newline at end of file +} diff --git a/packages/taler-util/src/http-client/bank-conversion.ts b/packages/taler-util/src/http-client/bank-conversion.ts index 2bc9fdb79..cb14d8b34 100644 --- a/packages/taler-util/src/http-client/bank-conversion.ts +++ b/packages/taler-util/src/http-client/bank-conversion.ts @@ -1,137 +1,223 @@ +/* + This file is part of GNU Taler + (C) 2022-2024 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * Imports. + */ import { AmountJson, Amounts } from "../amounts.js"; -import { HttpRequestLibrary } from "../http-common.js"; +import { HttpRequestLibrary, readTalerErrorResponse } from "../http-common.js"; import { HttpStatusCode } from "../http-status-codes.js"; import { createPlatformHttpLib } from "../http.js"; -import { FailCasesByMethod, ResultByMethod, opEmptySuccess, opKnownFailure, opSuccess, opUnknownFailure } from "../operation.js"; +import { LibtoolVersion } from "../libtool-version.js"; +import { + FailCasesByMethod, + ResultByMethod, + opEmptySuccess, + opKnownHttpFailure, + opSuccessFromHttp, + opUnknownFailure, +} from "../operation.js"; import { TalerErrorCode } from "../taler-error-codes.js"; import { codecForTalerErrorDetail } from "../wallet-types.js"; import { AccessToken, TalerBankConversionApi, - UserAndToken, codecForCashinConversionResponse, codecForCashoutConversionResponse, - codecForConversionBankConfig + codecForConversionBankConfig, } from "./types.js"; -import { makeBearerTokenAuthHeader } from "./utils.js"; +import { + CacheEvictor, + makeBearerTokenAuthHeader, + nullEvictor, +} from "./utils.js"; -export type TalerBankConversionResultByMethod<prop extends keyof TalerBankConversionHttpClient> = ResultByMethod<TalerBankConversionHttpClient, prop> -export type TalerBankConversionErrorsByMethod<prop extends keyof TalerBankConversionHttpClient> = FailCasesByMethod<TalerBankConversionHttpClient, prop> +export type TalerBankConversionResultByMethod< + prop extends keyof TalerBankConversionHttpClient, +> = ResultByMethod<TalerBankConversionHttpClient, prop>; +export type TalerBankConversionErrorsByMethod< + prop extends keyof TalerBankConversionHttpClient, +> = FailCasesByMethod<TalerBankConversionHttpClient, prop>; + +export enum TalerBankConversionCacheEviction { + UPDATE_RATE, +} /** * The API is used by the wallets. */ export class TalerBankConversionHttpClient { + public readonly PROTOCOL_VERSION = "0:0:0"; + httpLib: HttpRequestLibrary; + cacheEvictor: CacheEvictor<TalerBankConversionCacheEviction>; constructor( readonly baseUrl: string, httpClient?: HttpRequestLibrary, + cacheEvictor?: CacheEvictor<TalerBankConversionCacheEviction>, ) { 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-bank-conversion-info.html#get--config - * + * */ async getConfig() { const url = new URL(`config`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { - method: "GET" + method: "GET", }); switch (resp.status) { - case HttpStatusCode.Ok: return opSuccess(resp, codecForConversionBankConfig()) - default: return opUnknownFailure(resp, await resp.text()) + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForConversionBankConfig()); + case HttpStatusCode.NotImplemented: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); } } /** * https://docs.taler.net/core/api-bank-conversion-info.html#get--cashin-rate - * + * */ - async getCashinRate(conversion: { debit?: AmountJson, credit?: AmountJson }) { + async getCashinRate(conversion: { debit?: AmountJson; credit?: AmountJson }) { const url = new URL(`cashin-rate`, this.baseUrl); if (conversion.debit) { - url.searchParams.set("amount_debit", Amounts.stringify(conversion.debit)) + url.searchParams.set("amount_debit", Amounts.stringify(conversion.debit)); } if (conversion.credit) { - url.searchParams.set("amount_credit", Amounts.stringify(conversion.credit)) + url.searchParams.set( + "amount_credit", + Amounts.stringify(conversion.credit), + ); } const resp = await this.httpLib.fetch(url.href, { method: "GET", }); switch (resp.status) { - case HttpStatusCode.Ok: return opSuccess(resp, codecForCashinConversionResponse()) + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForCashinConversionResponse()); case HttpStatusCode.BadRequest: { - const body = await resp.json() - const details = codecForTalerErrorDetail().decode(body) + const body = await resp.json(); + const details = codecForTalerErrorDetail().decode(body); switch (details.code) { - case TalerErrorCode.GENERIC_PARAMETER_MISSING: return opKnownFailure("missing-params", resp); - case TalerErrorCode.GENERIC_PARAMETER_MALFORMED: return opKnownFailure("wrong-calculation", resp); - case TalerErrorCode.GENERIC_CURRENCY_MISMATCH: return opKnownFailure("wrong-currency", resp); - default: return opUnknownFailure(resp, body) + case TalerErrorCode.GENERIC_PARAMETER_MISSING: + return opKnownHttpFailure(resp.status, resp); + case TalerErrorCode.GENERIC_PARAMETER_MALFORMED: + return opKnownHttpFailure(resp.status, resp); + case TalerErrorCode.GENERIC_CURRENCY_MISMATCH: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, body); } } - case HttpStatusCode.Conflict: return opKnownFailure("amount-too-small", resp); - case HttpStatusCode.NotImplemented: return opKnownFailure("conversion-not-supported", resp); - default: return opUnknownFailure(resp, await resp.text()) + case HttpStatusCode.Conflict: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotImplemented: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); } } /** * https://docs.taler.net/core/api-bank-conversion-info.html#get--cashout-rate - * + * */ - async getCashoutRate(conversion: { debit?: AmountJson, credit?: AmountJson }) { + async getCashoutRate(conversion: { + debit?: AmountJson; + credit?: AmountJson; + }) { const url = new URL(`cashout-rate`, this.baseUrl); if (conversion.debit) { - url.searchParams.set("amount_debit", Amounts.stringify(conversion.debit)) + url.searchParams.set("amount_debit", Amounts.stringify(conversion.debit)); } if (conversion.credit) { - url.searchParams.set("amount_credit", Amounts.stringify(conversion.credit)) + url.searchParams.set( + "amount_credit", + Amounts.stringify(conversion.credit), + ); } const resp = await this.httpLib.fetch(url.href, { method: "GET", }); switch (resp.status) { - case HttpStatusCode.Ok: return opSuccess(resp, codecForCashoutConversionResponse()) + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForCashoutConversionResponse()); case HttpStatusCode.BadRequest: { - const body = await resp.json() - const details = codecForTalerErrorDetail().decode(body) + const body = await resp.json(); + const details = codecForTalerErrorDetail().decode(body); switch (details.code) { - case TalerErrorCode.GENERIC_PARAMETER_MISSING: return opKnownFailure("missing-params", resp); - case TalerErrorCode.GENERIC_PARAMETER_MALFORMED: return opKnownFailure("wrong-calculation", resp); - case TalerErrorCode.GENERIC_CURRENCY_MISMATCH: return opKnownFailure("wrong-currency", resp); - default: return opUnknownFailure(resp, body) + case TalerErrorCode.GENERIC_PARAMETER_MISSING: + return opKnownHttpFailure(resp.status, resp); + case TalerErrorCode.GENERIC_PARAMETER_MALFORMED: + return opKnownHttpFailure(resp.status, resp); + case TalerErrorCode.GENERIC_CURRENCY_MISMATCH: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, body); } } - case HttpStatusCode.Conflict: return opKnownFailure("amount-too-small", resp); - case HttpStatusCode.NotImplemented: return opKnownFailure("conversion-not-supported", resp); - default: return opUnknownFailure(resp, await resp.text()) + case HttpStatusCode.Conflict: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotImplemented: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); } } /** * https://docs.taler.net/core/api-bank-conversion-info.html#post--conversion-rate - * + * */ - async updateConversionRate(auth: AccessToken, body: TalerBankConversionApi.ConversionRate) { + async updateConversionRate( + auth: AccessToken, + body: TalerBankConversionApi.ConversionRate, + ) { const url = new URL(`conversion-rate`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "POST", headers: { - Authorization: makeBearerTokenAuthHeader(auth) + Authorization: makeBearerTokenAuthHeader(auth), }, - body + body, }); switch (resp.status) { - case HttpStatusCode.NoContent: return opEmptySuccess() - case HttpStatusCode.Unauthorized: return opKnownFailure("unauthorized", resp); - case HttpStatusCode.NotImplemented: return opKnownFailure("conversion-not-supported", resp); - default: return opUnknownFailure(resp, await resp.text()) + case HttpStatusCode.NoContent: { + this.cacheEvictor.notifySuccess( + TalerBankConversionCacheEviction.UPDATE_RATE, + ); + return opEmptySuccess(resp); + } + case HttpStatusCode.Unauthorized: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotImplemented: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); } } - } - diff --git a/packages/taler-util/src/http-client/bank-core.ts b/packages/taler-util/src/http-client/bank-core.ts index 51d6d7c96..6c8051ada 100644 --- a/packages/taler-util/src/http-client/bank-core.ts +++ b/packages/taler-util/src/http-client/bank-core.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2022 Taler Systems S.A. + (C) 2022-2024 Taler Systems S.A. GNU Taler is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -15,62 +15,126 @@ */ import { + AbsoluteTime, HttpStatusCode, LibtoolVersion, + LongPollParams, + OperationAlternative, + OperationFail, + OperationOk, TalerErrorCode, - codecForTalerErrorDetail + codecForChallenge, + codecForTanTransmission, + opKnownAlternativeFailure, + opKnownHttpFailure, + opKnownTalerFailure, } from "@gnu-taler/taler-util"; import { HttpRequestLibrary, - createPlatformHttpLib + createPlatformHttpLib, + readTalerErrorResponse, } from "@gnu-taler/taler-util/http"; -import { FailCasesByMethod, ResultByMethod, opEmptySuccess, opFixedSuccess, opKnownFailure, opSuccess, opUnknownFailure } from "../operation.js"; -import { TalerAuthenticationHttpClient } from "./authentication.js"; -import { TalerBankConversionHttpClient } from "./bank-conversion.js"; -import { TalerBankIntegrationHttpClient } from "./bank-integration.js"; -import { TalerRevenueHttpClient } from "./bank-revenue.js"; -import { TalerWireGatewayHttpClient } from "./bank-wire.js"; -import { AccessToken, PaginationParams, TalerCorebankApi, UserAndToken, WithdrawalOperationStatus, codecForAccountData, codecForBankAccountCreateWithdrawalResponse, codecForBankAccountTransactionInfo, codecForBankAccountTransactionsResponse, codecForCashoutPending, codecForCashoutStatusResponse, codecForCashouts, codecForCoreBankConfig, codecForCreateTransactionResponse, codecForGlobalCashouts, codecForListBankAccountsResponse, codecForMonitorResponse, codecForPublicAccountsResponse, codecForRegisterAccountResponse, codecForWithdrawalPublicInfo } from "./types.js"; -import { addPaginationParams, makeBearerTokenAuthHeader } from "./utils.js"; - +import { + FailCasesByMethod, + ResultByMethod, + opEmptySuccess, + opFixedSuccess, + opSuccessFromHttp, + opUnknownFailure, +} from "../operation.js"; +import { + AccessToken, + PaginationParams, + TalerCorebankApi, + UserAndToken, + WithdrawalOperationStatus, + codecForAccountData, + codecForBankAccountCreateWithdrawalResponse, + codecForBankAccountTransactionInfo, + codecForBankAccountTransactionsResponse, + codecForCashoutPending, + codecForCashoutStatusResponse, + codecForCashouts, + codecForCoreBankConfig, + codecForCreateTransactionResponse, + codecForGlobalCashouts, + codecForListBankAccountsResponse, + codecForMonitorResponse, + codecForPublicAccountsResponse, + codecForRegisterAccountResponse, + codecForWithdrawalPublicInfo, +} from "./types.js"; +import { + CacheEvictor, + IdempotencyRetry, + addLongPollingParam, + addPaginationParams, + makeBearerTokenAuthHeader, + nullEvictor, +} from "./utils.js"; -export type TalerCoreBankResultByMethod<prop extends keyof TalerCoreBankHttpClient> = ResultByMethod<TalerCoreBankHttpClient, prop> -export type TalerCoreBankErrorsByMethod<prop extends keyof TalerCoreBankHttpClient> = FailCasesByMethod<TalerCoreBankHttpClient, prop> +export type TalerCoreBankResultByMethod< + prop extends keyof TalerCoreBankHttpClient, +> = ResultByMethod<TalerCoreBankHttpClient, prop>; +export type TalerCoreBankErrorsByMethod< + prop extends keyof TalerCoreBankHttpClient, +> = FailCasesByMethod<TalerCoreBankHttpClient, prop>; +export enum TalerCoreBankCacheEviction { + DELETE_ACCOUNT, + CREATE_ACCOUNT, + UPDATE_ACCOUNT, + UPDATE_PASSWORD, + CREATE_TRANSACTION, + CONFIRM_WITHDRAWAL, + ABORT_WITHDRAWAL, + CREATE_WITHDRAWAL, + CREATE_CASHOUT, +} /** - * Protocol version spoken with the bank. + * Protocol version spoken with the core bank. + * + * Endpoint must be ordered in the same way that in the docs + * Response code (http and taler) must have the same order that in the docs + * That way is easier to see changes * * Uses libtool's current:revision:age versioning. */ export class TalerCoreBankHttpClient { - public readonly PROTOCOL_VERSION = "0:0:0"; + public readonly PROTOCOL_VERSION = "4:0:0"; httpLib: HttpRequestLibrary; - + cacheEvictor: CacheEvictor<TalerCoreBankCacheEviction>; constructor( readonly baseUrl: string, httpClient?: HttpRequestLibrary, + cacheEvictor?: CacheEvictor<TalerCoreBankCacheEviction>, ) { this.httpLib = httpClient ?? createPlatformHttpLib(); + this.cacheEvictor = cacheEvictor ?? nullEvictor; } isCompatible(version: string): boolean { - const compare = LibtoolVersion.compare(this.PROTOCOL_VERSION, version) - return compare?.compatible ?? false + const compare = LibtoolVersion.compare(this.PROTOCOL_VERSION, version); + return compare?.compatible ?? false; } /** * https://docs.taler.net/core/api-corebank.html#config - * + * */ async getConfig() { const url = new URL(`config`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { - method: "GET" + method: "GET", }); switch (resp.status) { - case HttpStatusCode.Ok: return opSuccess(resp, codecForCoreBankConfig()) - default: return opUnknownFailure(resp, await resp.text()) + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForCoreBankConfig()); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); } } @@ -80,189 +144,287 @@ export class TalerCoreBankHttpClient { /** * https://docs.taler.net/core/api-corebank.html#post--accounts - * + * */ - async createAccount(auth: AccessToken, body: TalerCorebankApi.RegisterAccountRequest) { + async createAccount( + auth: AccessToken | undefined, + body: TalerCorebankApi.RegisterAccountRequest, + ) { const url = new URL(`accounts`, this.baseUrl); + const headers: Record<string, string> = {}; + if (auth) { + headers.Authorization = makeBearerTokenAuthHeader(auth); + } const resp = await this.httpLib.fetch(url.href, { method: "POST", body, - headers: { - Authorization: makeBearerTokenAuthHeader(auth) - }, + headers: headers, }); switch (resp.status) { - case HttpStatusCode.Ok: return opSuccess(resp, codecForRegisterAccountResponse()) - case HttpStatusCode.BadRequest: return opKnownFailure("invalid-phone-or-email", resp); - case HttpStatusCode.Unauthorized: return opKnownFailure("unauthorized", resp); + case HttpStatusCode.Ok: { + await this.cacheEvictor.notifySuccess( + TalerCoreBankCacheEviction.CREATE_ACCOUNT, + ); + return opSuccessFromHttp(resp, codecForRegisterAccountResponse()); + } + case HttpStatusCode.BadRequest: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Unauthorized: + return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.Conflict: { - const body = await resp.json() - const details = codecForTalerErrorDetail().decode(body) + const details = await readTalerErrorResponse(resp); switch (details.code) { - case TalerErrorCode.BANK_REGISTER_USERNAME_REUSE: return opKnownFailure("username-already-exists", resp); - case TalerErrorCode.BANK_REGISTER_PAYTO_URI_REUSE: return opKnownFailure("payto-already-exists", resp); - case TalerErrorCode.BANK_UNALLOWED_DEBIT: return opKnownFailure("insufficient-funds", resp); - case TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT: return opKnownFailure("username-reserved", resp); - case TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT: return opKnownFailure("user-cant-set-debt", resp); - default: return opUnknownFailure(resp, body) + case TalerErrorCode.BANK_REGISTER_USERNAME_REUSE: + return opKnownTalerFailure(details.code, details); + case TalerErrorCode.BANK_REGISTER_PAYTO_URI_REUSE: + return opKnownTalerFailure(details.code, details); + case TalerErrorCode.BANK_UNALLOWED_DEBIT: + return opKnownTalerFailure(details.code, details); + case TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT: + return opKnownTalerFailure(details.code, details); + case TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT: + return opKnownTalerFailure(details.code, details); + case TalerErrorCode.BANK_NON_ADMIN_SET_MIN_CASHOUT: + return opKnownTalerFailure(details.code, details); + case TalerErrorCode.BANK_NON_ADMIN_SET_TAN_CHANNEL: + return opKnownTalerFailure(details.code, details); + case TalerErrorCode.BANK_TAN_CHANNEL_NOT_SUPPORTED: + return opKnownTalerFailure(details.code, details); + case TalerErrorCode.BANK_MISSING_TAN_INFO: + return opKnownTalerFailure(details.code, details); + default: + return opUnknownFailure(resp, details); } } - default: return opUnknownFailure(resp, await resp.text()) + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); } } /** * https://docs.taler.net/core/api-corebank.html#delete--accounts-$USERNAME - * + * */ - async deleteAccount(auth: UserAndToken) { + async deleteAccount(auth: UserAndToken, cid?: string) { const url = new URL(`accounts/${auth.username}`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "DELETE", headers: { - Authorization: makeBearerTokenAuthHeader(auth.token) + Authorization: makeBearerTokenAuthHeader(auth.token), + "X-Challenge-Id": cid, }, }); switch (resp.status) { - case HttpStatusCode.NoContent: return opEmptySuccess() - case HttpStatusCode.NotFound: return opKnownFailure("not-found", resp); - case HttpStatusCode.Unauthorized: return opKnownFailure("unauthorized", resp); + case HttpStatusCode.Accepted: + return opKnownAlternativeFailure( + resp, + resp.status, + codecForChallenge(), + ); + case HttpStatusCode.NoContent: + return opEmptySuccess(resp); + case HttpStatusCode.Unauthorized: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.Conflict: { - const body = await resp.json() - const details = codecForTalerErrorDetail().decode(body) + const details = await readTalerErrorResponse(resp); switch (details.code) { - case TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT: return opKnownFailure("username-reserved", resp); - case TalerErrorCode.BANK_ACCOUNT_BALANCE_NOT_ZERO: return opKnownFailure("balance-not-zero", resp); - default: return opUnknownFailure(resp, body) + case TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT: + return opKnownTalerFailure(details.code, details); + case TalerErrorCode.BANK_ACCOUNT_BALANCE_NOT_ZERO: + return opKnownTalerFailure(details.code, details); + default: + return opUnknownFailure(resp, details); } } - default: return opUnknownFailure(resp, await resp.text()) + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); } } /** * https://docs.taler.net/core/api-corebank.html#patch--accounts-$USERNAME - * + * */ - async updateAccount(auth: UserAndToken, body: TalerCorebankApi.AccountReconfiguration) { + async updateAccount( + auth: UserAndToken, + body: TalerCorebankApi.AccountReconfiguration, + cid?: string, + ) { const url = new URL(`accounts/${auth.username}`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "PATCH", body, headers: { - Authorization: makeBearerTokenAuthHeader(auth.token) + Authorization: makeBearerTokenAuthHeader(auth.token), + "X-Challenge-Id": cid, }, }); switch (resp.status) { - case HttpStatusCode.NoContent: return opEmptySuccess() - case HttpStatusCode.NotFound: return opKnownFailure("not-found", resp); - case HttpStatusCode.Unauthorized: return opKnownFailure("unauthorized", resp); + case HttpStatusCode.Accepted: + return opKnownAlternativeFailure( + resp, + resp.status, + codecForChallenge(), + ); + case HttpStatusCode.NoContent: + return opEmptySuccess(resp); + case HttpStatusCode.Unauthorized: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.Conflict: { - const body = await resp.json() - const details = codecForTalerErrorDetail().decode(body) + const details = await readTalerErrorResponse(resp); switch (details.code) { - case TalerErrorCode.BANK_NON_ADMIN_PATCH_LEGAL_NAME: return opKnownFailure("user-cant-change-name", resp); - case TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT: return opKnownFailure("user-cant-change-debt", resp); - case TalerErrorCode.BANK_NON_ADMIN_PATCH_CASHOUT: return opKnownFailure("user-cant-change-cashout", resp); - case TalerErrorCode.BANK_NON_ADMIN_PATCH_CONTACT: return opKnownFailure("user-cant-change-contact", resp); - default: return opUnknownFailure(resp, body) + case TalerErrorCode.BANK_NON_ADMIN_PATCH_LEGAL_NAME: + return opKnownTalerFailure(details.code, details); + case TalerErrorCode.BANK_NON_ADMIN_PATCH_CASHOUT: + return opKnownTalerFailure(details.code, details); + case TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT: + return opKnownTalerFailure(details.code, details); + case TalerErrorCode.BANK_NON_ADMIN_SET_MIN_CASHOUT: + return opKnownTalerFailure(details.code, details); + case TalerErrorCode.BANK_TAN_CHANNEL_NOT_SUPPORTED: + return opKnownTalerFailure(details.code, details); + case TalerErrorCode.BANK_MISSING_TAN_INFO: + return opKnownTalerFailure(details.code, details); + default: + return opUnknownFailure(resp, details); } } - default: return opUnknownFailure(resp, await resp.text()) + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); } } /** * https://docs.taler.net/core/api-corebank.html#patch--accounts-$USERNAME-auth - * + * */ - async updatePassword(auth: UserAndToken, body: TalerCorebankApi.AccountPasswordChange) { + async updatePassword( + auth: UserAndToken, + body: TalerCorebankApi.AccountPasswordChange, + cid?: string, + ) { const url = new URL(`accounts/${auth.username}/auth`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "PATCH", body, headers: { - Authorization: makeBearerTokenAuthHeader(auth.token) + Authorization: makeBearerTokenAuthHeader(auth.token), + "X-Challenge-Id": cid, }, }); switch (resp.status) { - case HttpStatusCode.NoContent: return opEmptySuccess() - case HttpStatusCode.NotFound: return opKnownFailure("not-found", resp); - case HttpStatusCode.Unauthorized: return opKnownFailure("unauthorized", resp); + case HttpStatusCode.Accepted: + return opKnownAlternativeFailure( + resp, + resp.status, + codecForChallenge(), + ); + case HttpStatusCode.NoContent: + return opEmptySuccess(resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Unauthorized: + return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.Conflict: { - const body = await resp.json() - const details = codecForTalerErrorDetail().decode(body) + const details = await readTalerErrorResponse(resp); switch (details.code) { - case TalerErrorCode.BANK_NON_ADMIN_PATCH_MISSING_OLD_PASSWORD: return opKnownFailure("user-require-old-password", resp); - case TalerErrorCode.BANK_PATCH_BAD_OLD_PASSWORD: return opKnownFailure("wrong-old-password", resp); - default: return opUnknownFailure(resp, body) + case TalerErrorCode.BANK_NON_ADMIN_PATCH_MISSING_OLD_PASSWORD: + return opKnownTalerFailure(details.code, details); + case TalerErrorCode.BANK_PATCH_BAD_OLD_PASSWORD: + return opKnownTalerFailure(details.code, details); + default: + return opUnknownFailure(resp, details); } } - default: return opUnknownFailure(resp, await resp.text()) + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); } } /** * https://docs.taler.net/core/api-corebank.html#get--public-accounts - * + * */ - async getPublicAccounts(filter: { account?: string } = {}, pagination?: PaginationParams) { + async getPublicAccounts( + filter: { account?: string } = {}, + pagination?: PaginationParams, + ) { const url = new URL(`public-accounts`, this.baseUrl); - addPaginationParams(url, pagination) + addPaginationParams(url, pagination); if (filter.account !== undefined) { - url.searchParams.set("filter_name", filter.account) + url.searchParams.set("filter_name", filter.account); } const resp = await this.httpLib.fetch(url.href, { method: "GET", }); switch (resp.status) { - case HttpStatusCode.Ok: return opSuccess(resp, codecForPublicAccountsResponse()) - case HttpStatusCode.NoContent: return opFixedSuccess({ public_accounts: [] }) - case HttpStatusCode.NotFound: return opFixedSuccess({ public_accounts: [] }) - default: return opUnknownFailure(resp, await resp.text()) + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForPublicAccountsResponse()); + case HttpStatusCode.NoContent: + return opFixedSuccess({ public_accounts: [] }); + case HttpStatusCode.NotFound: + return opFixedSuccess({ public_accounts: [] }); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); } } /** * https://docs.taler.net/core/api-corebank.html#get--accounts - * + * */ - async getAccounts(auth: AccessToken, filter: { account?: string } = {}, pagination?: PaginationParams) { + async getAccounts( + auth: AccessToken, + filter: { account?: string } = {}, + pagination?: PaginationParams, + ) { const url = new URL(`accounts`, this.baseUrl); - addPaginationParams(url, pagination) + addPaginationParams(url, pagination); if (filter.account !== undefined) { - url.searchParams.set("filter_name", filter.account) + url.searchParams.set("filter_name", filter.account); } const resp = await this.httpLib.fetch(url.href, { method: "GET", headers: { - Authorization: makeBearerTokenAuthHeader(auth) + Authorization: makeBearerTokenAuthHeader(auth), }, }); switch (resp.status) { - case HttpStatusCode.Ok: return opSuccess(resp, codecForListBankAccountsResponse()) - case HttpStatusCode.NoContent: return opFixedSuccess({ accounts: [] }) - case HttpStatusCode.Unauthorized: return opKnownFailure("unauthorized", resp); - default: return opUnknownFailure(resp, await resp.text()) + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForListBankAccountsResponse()); + case HttpStatusCode.NoContent: + return opFixedSuccess({ accounts: [] }); + case HttpStatusCode.Unauthorized: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); } } /** * https://docs.taler.net/core/api-corebank.html#get--accounts-$USERNAME - * + * */ async getAccount(auth: UserAndToken) { const url = new URL(`accounts/${auth.username}`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "GET", headers: { - Authorization: makeBearerTokenAuthHeader(auth.token) + Authorization: makeBearerTokenAuthHeader(auth.token), }, }); switch (resp.status) { - case HttpStatusCode.Ok: return opSuccess(resp, codecForAccountData()) - case HttpStatusCode.Unauthorized: return opKnownFailure("unauthorized", resp); - case HttpStatusCode.NotFound: return opKnownFailure("not-found", resp); - default: return opUnknownFailure(resp, await resp.text()) + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForAccountData()); + case HttpStatusCode.Unauthorized: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); } } @@ -272,75 +434,137 @@ export class TalerCoreBankHttpClient { /** * https://docs.taler.net/core/api-corebank.html#get--accounts-$USERNAME-transactions - * + * */ - async getTransactions(auth: UserAndToken, pagination?: PaginationParams) { + async getTransactions( + auth: UserAndToken, + params?: PaginationParams & LongPollParams, + ) { const url = new URL(`accounts/${auth.username}/transactions`, this.baseUrl); - addPaginationParams(url, pagination) + addPaginationParams(url, params); + addLongPollingParam(url, params); const resp = await this.httpLib.fetch(url.href, { method: "GET", headers: { - Authorization: makeBearerTokenAuthHeader(auth.token) + Authorization: makeBearerTokenAuthHeader(auth.token), }, }); switch (resp.status) { - case HttpStatusCode.Ok: return opSuccess(resp, codecForBankAccountTransactionsResponse()) - case HttpStatusCode.NoContent: return opFixedSuccess({ transactions: [] }) - case HttpStatusCode.Unauthorized: return opKnownFailure("unauthorized", resp); - case HttpStatusCode.NotFound: return opKnownFailure("not-found", resp); - default: return opUnknownFailure(resp, await resp.text()) + case HttpStatusCode.Ok: + return opSuccessFromHttp( + resp, + codecForBankAccountTransactionsResponse(), + ); + case HttpStatusCode.NoContent: + return opFixedSuccess({ transactions: [] }); + case HttpStatusCode.Unauthorized: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); } } /** * https://docs.taler.net/core/api-corebank.html#get--accounts-$USERNAME-transactions-$TRANSACTION_ID - * + * */ async getTransactionById(auth: UserAndToken, txid: number) { - const url = new URL(`accounts/${auth.username}/transactions/${String(txid)}`, this.baseUrl); + const url = new URL( + `accounts/${auth.username}/transactions/${String(txid)}`, + this.baseUrl, + ); const resp = await this.httpLib.fetch(url.href, { method: "GET", headers: { - Authorization: makeBearerTokenAuthHeader(auth.token) + Authorization: makeBearerTokenAuthHeader(auth.token), }, }); switch (resp.status) { - case HttpStatusCode.Ok: return opSuccess(resp, codecForBankAccountTransactionInfo()) - case HttpStatusCode.NotFound: return opKnownFailure("not-found", resp); - case HttpStatusCode.Unauthorized: return opKnownFailure("unauthorized", resp); - default: return opUnknownFailure(resp, await resp.text()) + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForBankAccountTransactionInfo()); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Unauthorized: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); } } /** * https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-transactions - * + * */ - async createTransaction(auth: UserAndToken, body: TalerCorebankApi.CreateTransactionRequest) { + async createTransaction( + auth: UserAndToken, + body: TalerCorebankApi.CreateTransactionRequest, + idempotencyCheck: IdempotencyRetry | undefined, + cid?: string, + ): Promise< + //manually definition all return types because of recursion + | OperationOk<TalerCorebankApi.CreateTransactionResponse> + | OperationAlternative<HttpStatusCode.Accepted, TalerCorebankApi.Challenge> + | OperationFail<HttpStatusCode.NotFound> + | OperationFail<HttpStatusCode.BadRequest> + | OperationFail<HttpStatusCode.Unauthorized> + | OperationFail<TalerErrorCode.BANK_UNALLOWED_DEBIT> + | OperationFail<TalerErrorCode.BANK_ADMIN_CREDITOR> + | OperationFail<TalerErrorCode.BANK_SAME_ACCOUNT> + | OperationFail<TalerErrorCode.BANK_UNKNOWN_CREDITOR> + | OperationFail<TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED> + > { const url = new URL(`accounts/${auth.username}/transactions`, this.baseUrl); + if (idempotencyCheck) { + body.request_uid = idempotencyCheck.uid; + } const resp = await this.httpLib.fetch(url.href, { method: "POST", headers: { - Authorization: makeBearerTokenAuthHeader(auth.token) + Authorization: makeBearerTokenAuthHeader(auth.token), + "X-Challenge-Id": cid, }, body, }); switch (resp.status) { - case HttpStatusCode.Ok: return opSuccess(resp, codecForCreateTransactionResponse()) - case HttpStatusCode.BadRequest: return opKnownFailure("invalid-input", resp); - case HttpStatusCode.Unauthorized: return opKnownFailure("unauthorized", resp); - case HttpStatusCode.NotFound: return opKnownFailure("not-found", resp); + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForCreateTransactionResponse()); + case HttpStatusCode.Accepted: + return opKnownAlternativeFailure( + resp, + resp.status, + codecForChallenge(), + ); + case HttpStatusCode.BadRequest: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Unauthorized: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.Conflict: { - const body = await resp.json() - const details = codecForTalerErrorDetail().decode(body) + const details = await readTalerErrorResponse(resp); switch (details.code) { - case TalerErrorCode.BANK_SAME_ACCOUNT: return opKnownFailure("creditor-same", resp); - case TalerErrorCode.BANK_UNKNOWN_CREDITOR: return opKnownFailure("creditor-not-found", resp); - case TalerErrorCode.BANK_UNALLOWED_DEBIT: return opKnownFailure("insufficient-funds", resp); - default: return opUnknownFailure(resp, body) + case TalerErrorCode.BANK_ADMIN_CREDITOR: + return opKnownTalerFailure(details.code, details); + case TalerErrorCode.BANK_SAME_ACCOUNT: + return opKnownTalerFailure(details.code, details); + case TalerErrorCode.BANK_UNKNOWN_CREDITOR: + return opKnownTalerFailure(details.code, details); + case TalerErrorCode.BANK_UNALLOWED_DEBIT: + return opKnownTalerFailure(details.code, details); + case TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED: + if (!idempotencyCheck) { + return opKnownTalerFailure(details.code, details); + } + const nextRetry = idempotencyCheck.next(); + return this.createTransaction(auth, body, nextRetry, cid); + default: + return opUnknownFailure(resp, details); } } - default: return opUnknownFailure(resp, await resp.text()) + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); } } @@ -350,101 +574,147 @@ export class TalerCoreBankHttpClient { /** * https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-withdrawals - * + * */ - async createWithdrawal(auth: UserAndToken, body: TalerCorebankApi.BankAccountCreateWithdrawalRequest) { + async createWithdrawal( + auth: UserAndToken, + body: TalerCorebankApi.BankAccountCreateWithdrawalRequest, + ) { const url = new URL(`accounts/${auth.username}/withdrawals`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "POST", headers: { - Authorization: makeBearerTokenAuthHeader(auth.token) + Authorization: makeBearerTokenAuthHeader(auth.token), }, body, }); switch (resp.status) { - case HttpStatusCode.Ok: return opSuccess(resp, codecForBankAccountCreateWithdrawalResponse()) - case HttpStatusCode.NotFound: return opKnownFailure("account-not-found", resp); - case HttpStatusCode.Conflict: return opKnownFailure("insufficient-funds", resp); - case HttpStatusCode.Unauthorized: return opKnownFailure("unauthorized", resp); - default: return opUnknownFailure(resp, await resp.text()) + case HttpStatusCode.Ok: + return opSuccessFromHttp( + resp, + codecForBankAccountCreateWithdrawalResponse(), + ); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Conflict: + return opKnownHttpFailure(resp.status, resp); + //FIXME: missing in docs + case HttpStatusCode.Unauthorized: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); } } /** - * https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-withdrawals-$WITHDRAWAL_ID-abort - * + * https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-withdrawals-$WITHDRAWAL_ID-confirm + * */ - async abortWithdrawalById(auth: UserAndToken, wid: string) { - const url = new URL(`accounts/${auth.username}/withdrawals/${wid}/abort`, this.baseUrl); + async confirmWithdrawalById(auth: UserAndToken, wid: string, cid?: string) { + const url = new URL( + `accounts/${auth.username}/withdrawals/${wid}/confirm`, + this.baseUrl, + ); const resp = await this.httpLib.fetch(url.href, { method: "POST", headers: { - Authorization: makeBearerTokenAuthHeader(auth.token) + Authorization: makeBearerTokenAuthHeader(auth.token), + "X-Challenge-Id": cid, }, }); switch (resp.status) { - case HttpStatusCode.NoContent: return opEmptySuccess() + case HttpStatusCode.Accepted: + return opKnownAlternativeFailure( + resp, + resp.status, + codecForChallenge(), + ); + case HttpStatusCode.NoContent: + return opEmptySuccess(resp); //FIXME: missing in docs - case HttpStatusCode.BadRequest: return opKnownFailure("invalid-id", resp) - case HttpStatusCode.NotFound: return opKnownFailure("not-found", resp) - case HttpStatusCode.Conflict: return opKnownFailure("previously-confirmed", resp); - default: return opUnknownFailure(resp, await resp.text()) + case HttpStatusCode.BadRequest: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Conflict: { + const details = await readTalerErrorResponse(resp); + switch (details.code) { + case TalerErrorCode.BANK_CONFIRM_ABORT_CONFLICT: + return opKnownTalerFailure(details.code, details); + case TalerErrorCode.BANK_CONFIRM_INCOMPLETE: + return opKnownTalerFailure(details.code, details); + case TalerErrorCode.BANK_UNALLOWED_DEBIT: + return opKnownTalerFailure(details.code, details); + default: + return opUnknownFailure(resp, details); + } + } + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); } } /** - * https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-withdrawals-$WITHDRAWAL_ID-confirm - * + * https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-withdrawals-$WITHDRAWAL_ID-abort + * */ - async confirmWithdrawalById(auth: UserAndToken, wid: string) { - const url = new URL(`accounts/${auth.username}/withdrawals/${wid}/confirm`, this.baseUrl); + async abortWithdrawalById(auth: UserAndToken, wid: string) { + const url = new URL( + `accounts/${auth.username}/withdrawals/${wid}/abort`, + this.baseUrl, + ); const resp = await this.httpLib.fetch(url.href, { method: "POST", headers: { - Authorization: makeBearerTokenAuthHeader(auth.token) + Authorization: makeBearerTokenAuthHeader(auth.token), }, }); switch (resp.status) { - case HttpStatusCode.NoContent: return opEmptySuccess() + case HttpStatusCode.NoContent: + return opEmptySuccess(resp); //FIXME: missing in docs - case HttpStatusCode.BadRequest: return opKnownFailure("invalid-id", resp) - case HttpStatusCode.NotFound: return opKnownFailure("not-found", resp) - case HttpStatusCode.Conflict: { - const body = await resp.json() - const details = codecForTalerErrorDetail().decode(body) - switch (details.code) { - case TalerErrorCode.BANK_CONFIRM_ABORT_CONFLICT: return opKnownFailure("previously-aborted", resp); - case TalerErrorCode.BANK_CONFIRM_INCOMPLETE: return opKnownFailure("no-exchange-or-reserve-selected", resp); - case TalerErrorCode.BANK_UNALLOWED_DEBIT: return opKnownFailure("insufficient-funds", resp); - default: return opUnknownFailure(resp, body) - } - } - default: return opUnknownFailure(resp, await resp.text()) + case HttpStatusCode.BadRequest: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Conflict: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); } } /** * https://docs.taler.net/core/api-corebank.html#get--withdrawals-$WITHDRAWAL_ID - * + * */ - async getWithdrawalById(wid: string, wait?: { - old_state?: WithdrawalOperationStatus, - timeoutMs: number - }) { + async getWithdrawalById( + wid: string, + params?: { + old_state?: WithdrawalOperationStatus; + } & LongPollParams, + ) { const url = new URL(`withdrawals/${wid}`, this.baseUrl); - if (wait) { - url.searchParams.set("long_poll_ms", String(wait.timeoutMs)) - url.searchParams.set("old_state", !wait.old_state ? "pending" : wait.old_state) + addLongPollingParam(url, params); + if (params) { + url.searchParams.set( + "old_state", + !params.old_state ? "pending" : params.old_state, + ); } const resp = await this.httpLib.fetch(url.href, { method: "GET", }); switch (resp.status) { - case HttpStatusCode.Ok: return opSuccess(resp, codecForWithdrawalPublicInfo()) + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForWithdrawalPublicInfo()); //FIXME: missing in docs - case HttpStatusCode.BadRequest: return opKnownFailure("invalid-id", resp) - case HttpStatusCode.NotFound: return opKnownFailure("not-found", resp) - default: return opUnknownFailure(resp, await resp.text()) + case HttpStatusCode.BadRequest: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); } } @@ -454,153 +724,225 @@ export class TalerCoreBankHttpClient { /** * https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-cashouts - * + * */ - async createCashout(auth: UserAndToken, body: TalerCorebankApi.CashoutRequest) { + async createCashout( + auth: UserAndToken, + body: TalerCorebankApi.CashoutRequest, + cid?: string, + ) { const url = new URL(`accounts/${auth.username}/cashouts`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "POST", headers: { - Authorization: makeBearerTokenAuthHeader(auth.token) + Authorization: makeBearerTokenAuthHeader(auth.token), + "X-Challenge-Id": cid, }, body, }); switch (resp.status) { - case HttpStatusCode.Ok: return opSuccess(resp, codecForCashoutPending()) - case HttpStatusCode.NotFound: return opKnownFailure("account-not-found", resp) + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForCashoutPending()); + case HttpStatusCode.Accepted: + return opKnownAlternativeFailure( + resp, + resp.status, + codecForChallenge(), + ); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.Conflict: { - const body = await resp.json() - const details = codecForTalerErrorDetail().decode(body) + const details = await readTalerErrorResponse(resp); switch (details.code) { - case TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED: return opKnownFailure("request-already-used", resp); - case TalerErrorCode.BANK_BAD_CONVERSION: return opKnownFailure("incorrect-exchange-rate", resp); - case TalerErrorCode.BANK_MISSING_TAN_INFO: return opKnownFailure("no-contact-info", resp); - case TalerErrorCode.BANK_UNALLOWED_DEBIT: return opKnownFailure("no-enough-balance", resp); - default: return opUnknownFailure(resp, body) + case TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED: + return opKnownTalerFailure(details.code, details); + case TalerErrorCode.BANK_CONVERSION_AMOUNT_TO_SMALL: + return opKnownTalerFailure(details.code, details); + case TalerErrorCode.BANK_BAD_CONVERSION: + return opKnownTalerFailure(details.code, details); + case TalerErrorCode.BANK_UNALLOWED_DEBIT: + return opKnownTalerFailure(details.code, details); + case TalerErrorCode.BANK_CONFIRM_INCOMPLETE: + return opKnownTalerFailure(details.code, details); + default: + return opUnknownFailure(resp, details); } } - case HttpStatusCode.NotImplemented: return opKnownFailure("cashout-not-supported", resp); - case HttpStatusCode.BadGateway: return opKnownFailure("tan-failed", resp); - default: return opUnknownFailure(resp, await resp.text()) + case HttpStatusCode.BadGateway: { + const details = await readTalerErrorResponse(resp); + switch (details.code) { + case TalerErrorCode.BANK_TAN_CHANNEL_SCRIPT_FAILED: + return opKnownTalerFailure(details.code, details); + default: + return opUnknownFailure(resp, details); + } + } + case HttpStatusCode.NotImplemented: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); } } /** - * https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-cashouts-$CASHOUT_ID-abort - * + * https://docs.taler.net/core/api-corebank.html#get--accounts-$USERNAME-cashouts-$CASHOUT_ID + * */ - async abortCashoutById(auth: UserAndToken, cid: number) { - const url = new URL(`accounts/${auth.username}/cashouts/${cid}/abort`, this.baseUrl); + async getCashoutById(auth: UserAndToken, cid: number) { + const url = new URL( + `accounts/${auth.username}/cashouts/${cid}`, + this.baseUrl, + ); const resp = await this.httpLib.fetch(url.href, { - method: "POST", + method: "GET", headers: { - Authorization: makeBearerTokenAuthHeader(auth.token) + Authorization: makeBearerTokenAuthHeader(auth.token), }, }); switch (resp.status) { - case HttpStatusCode.NoContent: return opEmptySuccess() - case HttpStatusCode.NotFound: return opKnownFailure("not-found", resp); - case HttpStatusCode.Conflict: return opKnownFailure("already-confirmed", resp); - case HttpStatusCode.NotImplemented: return opKnownFailure("cashout-not-supported", resp); - default: return opUnknownFailure(resp, await resp.text()) + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForCashoutStatusResponse()); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotImplemented: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); } } /** - * https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-cashouts-$CASHOUT_ID-confirm - * + * https://docs.taler.net/core/api-corebank.html#get--accounts-$USERNAME-cashouts + * */ - async confirmCashoutById(auth: UserAndToken, cid: number, body: TalerCorebankApi.CashoutConfirmRequest) { - const url = new URL(`accounts/${auth.username}/cashouts/${cid}/confirm`, this.baseUrl); + async getAccountCashouts(auth: UserAndToken, pagination?: PaginationParams) { + const url = new URL(`accounts/${auth.username}/cashouts`, this.baseUrl); + addPaginationParams(url, pagination); const resp = await this.httpLib.fetch(url.href, { - method: "POST", + method: "GET", headers: { - Authorization: makeBearerTokenAuthHeader(auth.token) + Authorization: makeBearerTokenAuthHeader(auth.token), }, - body, }); switch (resp.status) { - case HttpStatusCode.NoContent: return opEmptySuccess() - case HttpStatusCode.NotFound: return opKnownFailure("not-found", resp); - // case HttpStatusCode.Forbidden: return opKnownFailure("wrong-tan-or-credential", resp); - case HttpStatusCode.Conflict: { - const body = await resp.json() - const details = codecForTalerErrorDetail().decode(body) - switch (details.code) { - case TalerErrorCode.BANK_CONFIRM_ABORT_CONFLICT: return opKnownFailure("already-aborted", resp); - case TalerErrorCode.BANK_CONFIRM_INCOMPLETE: return opKnownFailure("no-cashout-payto", resp); - case TalerErrorCode.BANK_UNALLOWED_DEBIT: return opKnownFailure("no-enough-balance", resp); - case TalerErrorCode.BANK_BAD_CONVERSION: return opKnownFailure("incorrect-exchange-rate", resp); - case TalerErrorCode.BANK_TAN_CHALLENGE_FAILED: return opKnownFailure("invalid-code", resp); - default: return opUnknownFailure(resp, body) - } - } - case HttpStatusCode.TooManyRequests: return opKnownFailure("too-many-attempts", resp); - case HttpStatusCode.NotImplemented: return opKnownFailure("cashout-not-supported", resp); - default: return opUnknownFailure(resp, await resp.text()) + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForCashouts()); + case HttpStatusCode.NoContent: + return opFixedSuccess({ cashouts: [] }); + case HttpStatusCode.NotImplemented: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); } } /** - * https://docs.taler.net/core/api-corebank.html#get--accounts-$USERNAME-cashouts-$CASHOUT_ID - * + * https://docs.taler.net/core/api-corebank.html#get--cashouts + * */ - async getCashoutById(auth: UserAndToken, cid: number) { - const url = new URL(`accounts/${auth.username}/cashouts/${cid}`, this.baseUrl); + async getGlobalCashouts(auth: AccessToken, pagination?: PaginationParams) { + const url = new URL(`cashouts`, this.baseUrl); + addPaginationParams(url, pagination); const resp = await this.httpLib.fetch(url.href, { method: "GET", headers: { - Authorization: makeBearerTokenAuthHeader(auth.token) + Authorization: makeBearerTokenAuthHeader(auth), }, }); switch (resp.status) { - case HttpStatusCode.Ok: return opSuccess(resp, codecForCashoutStatusResponse()) - case HttpStatusCode.NotFound: return opKnownFailure("not-found", resp); - case HttpStatusCode.NotImplemented: return opKnownFailure("cashout-not-supported", resp); - default: return opUnknownFailure(resp, await resp.text()) + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForGlobalCashouts()); + case HttpStatusCode.NoContent: + return opFixedSuccess({ cashouts: [] }); + case HttpStatusCode.NotImplemented: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); } } + // + // 2FA + // + /** - * https://docs.taler.net/core/api-corebank.html#get--accounts-$USERNAME-cashouts - * + * https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-challenge-$CHALLENGE_ID + * */ - async getAccountCashouts(auth: UserAndToken, pagination?: PaginationParams) { - const url = new URL(`accounts/${auth.username}/cashouts`, this.baseUrl); - addPaginationParams(url, pagination) + async sendChallenge(auth: UserAndToken, cid: string) { + const url = new URL( + `accounts/${auth.username}/challenge/${cid}`, + this.baseUrl, + ); const resp = await this.httpLib.fetch(url.href, { - method: "GET", + method: "POST", headers: { - Authorization: makeBearerTokenAuthHeader(auth.token) + Authorization: makeBearerTokenAuthHeader(auth.token), }, }); switch (resp.status) { - case HttpStatusCode.Ok: return opSuccess(resp, codecForCashouts()) - case HttpStatusCode.NoContent: return opFixedSuccess({ cashouts: [] }); - case HttpStatusCode.NotFound: return opKnownFailure("account-not-found", resp);; - case HttpStatusCode.NotImplemented: return opKnownFailure("cashout-not-supported", resp); - default: return opUnknownFailure(resp, await resp.text()) + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForTanTransmission()); + case HttpStatusCode.Unauthorized: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.BadGateway: { + const details = await readTalerErrorResponse(resp); + switch (details.code) { + case TalerErrorCode.BANK_TAN_CHANNEL_SCRIPT_FAILED: + return opKnownTalerFailure(details.code, details); + default: + return opUnknownFailure(resp, details); + } + } + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); } } /** - * https://docs.taler.net/core/api-corebank.html#get--cashouts - * + * https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-challenge-$CHALLENGE_ID-confirm + * */ - async getGlobalCashouts(auth: AccessToken, pagination?: PaginationParams) { - const url = new URL(`cashouts`, this.baseUrl); - addPaginationParams(url, pagination) + async confirmChallenge( + auth: UserAndToken, + cid: string, + body: TalerCorebankApi.ChallengeSolve, + ) { + const url = new URL( + `accounts/${auth.username}/challenge/${cid}/confirm`, + this.baseUrl, + ); const resp = await this.httpLib.fetch(url.href, { - method: "GET", + method: "POST", headers: { - Authorization: makeBearerTokenAuthHeader(auth) + Authorization: makeBearerTokenAuthHeader(auth.token), }, + body, }); switch (resp.status) { - case HttpStatusCode.Ok: return opSuccess(resp, codecForGlobalCashouts()) - case HttpStatusCode.NoContent: return opFixedSuccess({ cashouts: [] }); - case HttpStatusCode.NotImplemented: return opKnownFailure("cashout-not-supported", resp); - default: return opUnknownFailure(resp, await resp.text()) + case HttpStatusCode.NoContent: + return opEmptySuccess(resp); + case HttpStatusCode.Unauthorized: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Conflict: { + const details = await readTalerErrorResponse(resp); + switch (details.code) { + case TalerErrorCode.BANK_TAN_CHALLENGE_EXPIRED: + return opKnownTalerFailure(details.code, details); + case TalerErrorCode.BANK_TAN_CHALLENGE_FAILED: + return opKnownTalerFailure(details.code, details); + default: + return opUnknownFailure(resp, details); + } + } + case HttpStatusCode.TooManyRequests: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); } } @@ -610,30 +952,43 @@ export class TalerCoreBankHttpClient { /** * https://docs.taler.net/core/api-corebank.html#get--monitor - * + * */ - async getMonitor(auth: AccessToken, params: { timeframe?: TalerCorebankApi.MonitorTimeframeParam, which?: number } = {}) { + async getMonitor( + auth: AccessToken, + params: { + timeframe?: TalerCorebankApi.MonitorTimeframeParam; + date?: AbsoluteTime; + } = {}, + ) { const url = new URL(`monitor`, this.baseUrl); if (params.timeframe) { - url.searchParams.set("timeframe", TalerCorebankApi.MonitorTimeframeParam[params.timeframe]) + url.searchParams.set( + "timeframe", + TalerCorebankApi.MonitorTimeframeParam[params.timeframe], + ); } - if (params.which) { - url.searchParams.set("which", String(params.which)) + if (params.date) { + const { t_s: seconds } = AbsoluteTime.toProtocolTimestamp(params.date); + if (seconds !== "never") { + url.searchParams.set("date_s", String(seconds)); + } } const resp = await this.httpLib.fetch(url.href, { method: "GET", headers: { - Authorization: makeBearerTokenAuthHeader(auth) + Authorization: makeBearerTokenAuthHeader(auth), }, }); switch (resp.status) { - case HttpStatusCode.Ok: return opSuccess(resp, codecForMonitorResponse()) - case HttpStatusCode.BadRequest: return opKnownFailure("invalid-input", resp); - case HttpStatusCode.Unauthorized: return opKnownFailure("unauthorized", resp); - //FIXME remove when server is updated - //FIXME: should be 404 ? - case HttpStatusCode.ServiceUnavailable: return opKnownFailure("monitor-not-supported", resp); - default: return opUnknownFailure(resp, await resp.text()) + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForMonitorResponse()); + case HttpStatusCode.BadRequest: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Unauthorized: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); } } @@ -643,46 +998,41 @@ export class TalerCoreBankHttpClient { /** * https://docs.taler.net/core/api-corebank.html#taler-bank-integration-api - * + * */ - getIntegrationAPI(): TalerBankIntegrationHttpClient { - const url = new URL(`taler-integration/`, this.baseUrl); - return new TalerBankIntegrationHttpClient(url.href, this.httpLib) + getIntegrationAPI(): URL { + return new URL(`taler-integration/`, this.baseUrl); } /** * https://docs.taler.net/core/api-corebank.html#taler-bank-integration-api - * + * */ - getWireGatewayAPI(username: string): TalerWireGatewayHttpClient { - const url = new URL(`accounts/${username}/taler-wire-gateway/`, this.baseUrl); - return new TalerWireGatewayHttpClient(url.href, username, this.httpLib) + getWireGatewayAPI(username: string): URL { + return new URL(`accounts/${username}/taler-wire-gateway/`, this.baseUrl); } /** - * https://docs.taler.net/core/api-corebank.html#taler-bank-integration-api - * - */ - getRevenueAPI(username: string): TalerRevenueHttpClient { - const url = new URL(`accounts/${username}/taler-revenue/`, this.baseUrl); - return new TalerRevenueHttpClient(url.href, username, this.httpLib,) + * https://docs.taler.net/core/api-corebank.html#taler-bank-integration-api + * + */ + getRevenueAPI(username: string): URL { + return new URL(`accounts/${username}/taler-revenue/`, this.baseUrl); } /** * https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-token - * - */ - getAuthenticationAPI(username: string): TalerAuthenticationHttpClient { - const url = new URL(`accounts/${username}/`, this.baseUrl); - return new TalerAuthenticationHttpClient(url.href, username, this.httpLib,) + * + */ + getAuthenticationAPI(username: string): URL { + return new URL(`accounts/${username}/`, this.baseUrl); } /** * https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-token - * - */ - getConversionInfoAPI(): TalerBankConversionHttpClient { - const url = new URL(`conversion-info/`, this.baseUrl); - return new TalerBankConversionHttpClient(url.href, this.httpLib) + * + */ + getConversionInfoAPI(): URL { + return new URL(`conversion-info/`, this.baseUrl); } } diff --git a/packages/taler-util/src/http-client/bank-integration.ts b/packages/taler-util/src/http-client/bank-integration.ts index 8131b36b6..75e6a627a 100644 --- a/packages/taler-util/src/http-client/bank-integration.ts +++ b/packages/taler-util/src/http-client/bank-integration.ts @@ -1,24 +1,57 @@ -import { HttpRequestLibrary, readSuccessResponseJsonOrThrow } from "../http-common.js"; +/* + This file is part of GNU Taler + (C) 2022-2024 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import { HttpRequestLibrary, readTalerErrorResponse } from "../http-common.js"; import { HttpStatusCode } from "../http-status-codes.js"; import { createPlatformHttpLib } from "../http.js"; -import { FailCasesByMethod, ResultByMethod, opKnownFailure, opSuccess, opUnknownFailure } from "../operation.js"; +import { LibtoolVersion } from "../libtool-version.js"; +import { + FailCasesByMethod, + ResultByMethod, + opEmptySuccess, + opKnownHttpFailure, + opKnownTalerFailure, + opSuccessFromHttp, + opUnknownFailure, +} from "../operation.js"; import { TalerErrorCode } from "../taler-error-codes.js"; import { codecForTalerErrorDetail } from "../wallet-types.js"; import { + LongPollParams, TalerBankIntegrationApi, WithdrawalOperationStatus, codecForBankWithdrawalOperationPostResponse, codecForBankWithdrawalOperationStatus, - codecForIntegrationBankConfig + codecForIntegrationBankConfig, } from "./types.js"; +import { addLongPollingParam } from "./utils.js"; -export type TalerBankIntegrationResultByMethod<prop extends keyof TalerBankIntegrationHttpClient> = ResultByMethod<TalerBankIntegrationHttpClient, prop> -export type TalerBankIntegrationErrorsByMethod<prop extends keyof TalerBankIntegrationHttpClient> = FailCasesByMethod<TalerBankIntegrationHttpClient, prop> +export type TalerBankIntegrationResultByMethod< + prop extends keyof TalerBankIntegrationHttpClient, +> = ResultByMethod<TalerBankIntegrationHttpClient, prop>; +export type TalerBankIntegrationErrorsByMethod< + prop extends keyof TalerBankIntegrationHttpClient, +> = FailCasesByMethod<TalerBankIntegrationHttpClient, prop>; /** * The API is used by the wallets. */ export class TalerBankIntegrationHttpClient { + public readonly PROTOCOL_VERSION = "2:0:2"; + httpLib: HttpRequestLibrary; constructor( @@ -28,71 +61,119 @@ export class TalerBankIntegrationHttpClient { this.httpLib = httpClient ?? createPlatformHttpLib(); } + isCompatible(version: string): boolean { + const compare = LibtoolVersion.compare(this.PROTOCOL_VERSION, version); + return compare?.compatible ?? false; + } + /** * https://docs.taler.net/core/api-bank-integration.html#get--config - * + * */ async getConfig() { const url = new URL(`config`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { - method: "GET" + method: "GET", }); switch (resp.status) { - case HttpStatusCode.Ok: return opSuccess(resp, codecForIntegrationBankConfig()) - default: return opUnknownFailure(resp, await resp.text()) + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForIntegrationBankConfig()); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); } } /** * https://docs.taler.net/core/api-bank-integration.html#get--withdrawal-operation-$WITHDRAWAL_ID - * + * */ - async getWithdrawalOperationById(woid: string, wait?: { - old_state?: WithdrawalOperationStatus, - timeoutMs: number - }) { + async getWithdrawalOperationById( + woid: string, + params?: { + old_state?: WithdrawalOperationStatus; + } & LongPollParams, + ) { const url = new URL(`withdrawal-operation/${woid}`, this.baseUrl); - if (wait) { - url.searchParams.set("long_poll_ms", String(wait.timeoutMs)) - url.searchParams.set("old_state", !wait.old_state ? "pending" : wait.old_state) + addLongPollingParam(url, params); + if (params) { + url.searchParams.set( + "old_state", + !params.old_state ? "pending" : params.old_state, + ); } const resp = await this.httpLib.fetch(url.href, { - method: "GET" + method: "GET", }); switch (resp.status) { - case HttpStatusCode.Ok: return opSuccess(resp, codecForBankWithdrawalOperationStatus()) - case HttpStatusCode.NotFound: return opKnownFailure("not-found", resp) - default: return opUnknownFailure(resp, await resp.text()) + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForBankWithdrawalOperationStatus()); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); } } /** * https://docs.taler.net/core/api-bank-integration.html#post-$BANK_API_BASE_URL-withdrawal-operation-$wopid - * + * */ - async completeWithdrawalOperationById(woid: string, body: TalerBankIntegrationApi.BankWithdrawalOperationPostRequest) { + async completeWithdrawalOperationById( + woid: string, + body: TalerBankIntegrationApi.BankWithdrawalOperationPostRequest, + ) { const url = new URL(`withdrawal-operation/${woid}`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "POST", body, }); switch (resp.status) { - case HttpStatusCode.Ok: return opSuccess(resp, codecForBankWithdrawalOperationPostResponse()) - case HttpStatusCode.NotFound: return opKnownFailure("not-found", resp) + case HttpStatusCode.Ok: + return opSuccessFromHttp( + resp, + codecForBankWithdrawalOperationPostResponse(), + ); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.Conflict: { - const body = await resp.json() - const details = codecForTalerErrorDetail().decode(body) + const body = await readTalerErrorResponse(resp); + const details = codecForTalerErrorDetail().decode(body); switch (details.code) { - case TalerErrorCode.BANK_WITHDRAWAL_OPERATION_RESERVE_SELECTION_CONFLICT: return opKnownFailure("already-selected", resp); - case TalerErrorCode.BANK_DUPLICATE_RESERVE_PUB_SUBJECT: return opKnownFailure("duplicated-reserve-id", resp); - case TalerErrorCode.BANK_UNKNOWN_ACCOUNT: return opKnownFailure("account-not-found", resp); - case TalerErrorCode.BANK_ACCOUNT_IS_NOT_EXCHANGE: return opKnownFailure("account-not-exchange", resp); - default: return opUnknownFailure(resp, body) + case TalerErrorCode.BANK_WITHDRAWAL_OPERATION_RESERVE_SELECTION_CONFLICT: + return opKnownTalerFailure(details.code, details); + case TalerErrorCode.BANK_DUPLICATE_RESERVE_PUB_SUBJECT: + return opKnownTalerFailure(details.code, details); + case TalerErrorCode.BANK_UNKNOWN_ACCOUNT: + return opKnownTalerFailure(details.code, details); + case TalerErrorCode.BANK_ACCOUNT_IS_NOT_EXCHANGE: + return opKnownTalerFailure(details.code, details); + default: + return opUnknownFailure(resp, details); } } - default: return opUnknownFailure(resp, await resp.text()) + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); } } + /** + * https://docs.taler.net/core/api-bank-integration.html#post-$BANK_API_BASE_URL-withdrawal-operation-$wopid + * + */ + async abortWithdrawalOperationById(woid: string) { + const url = new URL(`withdrawal-operation/${woid}/abort`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + }); + switch (resp.status) { + case HttpStatusCode.NoContent: + return opEmptySuccess(resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Conflict: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } } - diff --git a/packages/taler-util/src/http-client/bank-revenue.ts b/packages/taler-util/src/http-client/bank-revenue.ts index 040ad8dd2..34afe7d86 100644 --- a/packages/taler-util/src/http-client/bank-revenue.ts +++ b/packages/taler-util/src/http-client/bank-revenue.ts @@ -1,15 +1,55 @@ -import { HttpRequestLibrary, makeBasicAuthHeader, readSuccessResponseJsonOrThrow } from "../http-common.js"; +/* + This file is part of GNU Taler + (C) 2022-2024 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import { + HttpRequestLibrary, + makeBasicAuthHeader, + readTalerErrorResponse, +} from "../http-common.js"; import { HttpStatusCode } from "../http-status-codes.js"; import { createPlatformHttpLib } from "../http.js"; -import { FailCasesByMethod, ResultByMethod, opKnownFailure, opSuccess, opUnknownFailure } from "../operation.js"; -import { PaginationParams, TalerRevenueApi, codecForMerchantIncomingHistory } from "./types.js"; -import { addPaginationParams } from "./utils.js"; +import { LibtoolVersion } from "../libtool-version.js"; +import { + FailCasesByMethod, + ResultByMethod, + opKnownHttpFailure, + opSuccessFromHttp, + opUnknownFailure, +} from "../operation.js"; +import { + LongPollParams, + PaginationParams, + codecForRevenueConfig, + codecForRevenueIncomingHistory, +} from "./types.js"; +import { addLongPollingParam, addPaginationParams } from "./utils.js"; -export type TalerBankRevenueResultByMethod<prop extends keyof TalerRevenueHttpClient> = ResultByMethod<TalerRevenueHttpClient, prop> -export type TalerBankRevenueErrorsByMethod<prop extends keyof TalerRevenueHttpClient> = FailCasesByMethod<TalerRevenueHttpClient, prop> +export type TalerBankRevenueResultByMethod< + prop extends keyof TalerRevenueHttpClient, +> = ResultByMethod<TalerRevenueHttpClient, prop>; +export type TalerBankRevenueErrorsByMethod< + prop extends keyof TalerRevenueHttpClient, +> = FailCasesByMethod<TalerRevenueHttpClient, prop>; +type UsernameAndPassword = { + username: string; + password: string; +}; /** - * The API is used by the merchant (or other parties) to query + * The API is used by the merchant (or other parties) to query * for incoming transactions to their account. */ export class TalerRevenueHttpClient { @@ -17,32 +57,74 @@ export class TalerRevenueHttpClient { constructor( readonly baseUrl: string, - readonly username: string, httpClient?: HttpRequestLibrary, ) { this.httpLib = httpClient ?? createPlatformHttpLib(); } + public readonly PROTOCOL_VERSION = "0:0:0"; + + isCompatible(version: string): boolean { + const compare = LibtoolVersion.compare(this.PROTOCOL_VERSION, version); + return compare?.compatible ?? false; + } + + /** + * https://docs.taler.net/core/api-bank-revenue.html#get--config + * + */ + async getConfig(auth?: UsernameAndPassword) { + const url = new URL(`config`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + headers: { + Authorization: auth + ? makeBasicAuthHeader(auth.username, auth.password) + : undefined, + }, + }); + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForRevenueConfig()); + case HttpStatusCode.Unauthorized: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } /** * https://docs.taler.net/core/api-bank-revenue.html#get--history - * - * @returns + * + * @returns */ - async getHistory(auth: string, pagination?: PaginationParams) { + async getHistory( + auth?: UsernameAndPassword, + params?: PaginationParams & LongPollParams, + ) { const url = new URL(`history`, this.baseUrl); - addPaginationParams(url, pagination) + addPaginationParams(url, params); + addLongPollingParam(url, params); const resp = await this.httpLib.fetch(url.href, { method: "GET", headers: { - Authorization: makeBasicAuthHeader(this.username, auth), - } + Authorization: auth + ? makeBasicAuthHeader(auth.username, auth.password) + : undefined, + }, }); switch (resp.status) { - case HttpStatusCode.Ok: return opSuccess(resp, codecForMerchantIncomingHistory()) - case HttpStatusCode.BadRequest: return opKnownFailure("invalid-input", resp); - case HttpStatusCode.Unauthorized: return opKnownFailure("unauthorized", resp); - case HttpStatusCode.NotFound: return opKnownFailure("endpoint-wrong-or-username-wrong", resp); - default: return opUnknownFailure(resp, await resp.text()) + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForRevenueIncomingHistory()); + case HttpStatusCode.BadRequest: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Unauthorized: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); } } -}
\ No newline at end of file +} diff --git a/packages/taler-util/src/http-client/bank-wire.ts b/packages/taler-util/src/http-client/bank-wire.ts index 7e3c00637..a8c976a80 100644 --- a/packages/taler-util/src/http-client/bank-wire.ts +++ b/packages/taler-util/src/http-client/bank-wire.ts @@ -1,18 +1,53 @@ -import { HttpRequestLibrary, makeBasicAuthHeader } from "../http-common.js"; +/* + This file is part of GNU Taler + (C) 2022-2024 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import { HttpRequestLibrary, makeBasicAuthHeader, readTalerErrorResponse } from "../http-common.js"; import { HttpStatusCode } from "../http-status-codes.js"; import { createPlatformHttpLib } from "../http.js"; -import { FailCasesByMethod, ResultByMethod, opFixedSuccess, opKnownFailure, opSuccess, opUnknownFailure } from "../operation.js"; -import { PaginationParams, TalerWireGatewayApi, codecForAddIncomingResponse, codecForIncomingHistory, codecForOutgoingHistory, codecForTransferResponse } from "./types.js"; -import { addPaginationParams } from "./utils.js"; +import { + FailCasesByMethod, + ResultByMethod, + opFixedSuccess, + opKnownHttpFailure, + opSuccessFromHttp, + opUnknownFailure, +} from "../operation.js"; +import { + LongPollParams, + PaginationParams, + TalerWireGatewayApi, + codecForAddIncomingResponse, + codecForIncomingHistory, + codecForOutgoingHistory, + codecForTransferResponse, +} from "./types.js"; +import { addLongPollingParam, addPaginationParams } from "./utils.js"; -export type TalerWireGatewayResultByMethod<prop extends keyof TalerWireGatewayHttpClient> = ResultByMethod<TalerWireGatewayHttpClient, prop> -export type TalerWireGatewayErrorsByMethod<prop extends keyof TalerWireGatewayHttpClient> = FailCasesByMethod<TalerWireGatewayHttpClient, prop> +export type TalerWireGatewayResultByMethod< + prop extends keyof TalerWireGatewayHttpClient, +> = ResultByMethod<TalerWireGatewayHttpClient, prop>; +export type TalerWireGatewayErrorsByMethod< + prop extends keyof TalerWireGatewayHttpClient, +> = FailCasesByMethod<TalerWireGatewayHttpClient, prop>; /** - * The API is used by the exchange to trigger transactions and query - * incoming transactions, as well as by the auditor to query incoming + * The API is used by the exchange to trigger transactions and query + * incoming transactions, as well as by the auditor to query incoming * and outgoing transactions. - * + * * https://docs.taler.net/core/api-bank-wire.html */ export class TalerWireGatewayHttpClient { @@ -25,99 +60,167 @@ export class TalerWireGatewayHttpClient { ) { this.httpLib = httpClient ?? createPlatformHttpLib(); } + // public readonly PROTOCOL_VERSION = "4:0:0"; + // isCompatible(version: string): boolean { + // const compare = LibtoolVersion.compare(this.PROTOCOL_VERSION, version) + // return compare?.compatible ?? false + // } + + // /** + // * https://docs.taler.net/core/api-corebank.html#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 opSuccess(resp, codecForCoreBankConfig()) + // default: return opUnknownFailure(resp, await readTalerErrorResponse(resp)) + // } + // } /** * https://docs.taler.net/core/api-bank-wire.html#post--transfer - * + * */ - async transfer(auth: string, body: TalerWireGatewayApi.TransferRequest, - ) { + async transfer(auth: string, body: TalerWireGatewayApi.TransferRequest) { const url = new URL(`transfer`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "POST", headers: { Authorization: makeBasicAuthHeader(this.username, auth), }, - body + body, }); switch (resp.status) { - case HttpStatusCode.Ok: return opSuccess(resp, codecForTransferResponse()) - case HttpStatusCode.BadRequest: return opKnownFailure("invalid-input", resp); - case HttpStatusCode.Unauthorized: return opKnownFailure("unauthorized", resp); - case HttpStatusCode.NotFound: return opKnownFailure("not-found", resp); - case HttpStatusCode.Conflict: return opKnownFailure("request-uid-already-used", resp); - default: return opUnknownFailure(resp, await resp.text()) + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForTransferResponse()); + //FIXME: show more details in docs + case HttpStatusCode.BadRequest: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Unauthorized: + return opKnownHttpFailure(resp.status, resp); + //FIXME: show more details in docs + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Conflict: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); } } /** * https://docs.taler.net/core/api-bank-wire.html#get--history-incoming - * + * */ - async getHistoryIncoming(auth: string, pagination?: PaginationParams) { + async getHistoryIncoming( + auth: string, + params?: PaginationParams & LongPollParams, + ) { const url = new URL(`history/incoming`, this.baseUrl); - addPaginationParams(url, pagination) + addPaginationParams(url, params); + addLongPollingParam(url, params); const resp = await this.httpLib.fetch(url.href, { method: "GET", headers: { Authorization: makeBasicAuthHeader(this.username, auth), - } + }, }); switch (resp.status) { - case HttpStatusCode.Ok: return opSuccess(resp, codecForIncomingHistory()) - case HttpStatusCode.NoContent: return opFixedSuccess({ incoming_transactions: [] }) - case HttpStatusCode.BadRequest: return opKnownFailure("invalid-input", resp); - case HttpStatusCode.Unauthorized: return opKnownFailure("unauthorized", resp); - case HttpStatusCode.NotFound: return opKnownFailure("not-found", resp); - default: return opUnknownFailure(resp, await resp.text()) + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForIncomingHistory()); + //FIXME: account should not be returned or make it optional + case HttpStatusCode.NoContent: + return opFixedSuccess({ + incoming_transactions: [], + credit_account: undefined, + }); + //FIXME: show more details in docs + case HttpStatusCode.BadRequest: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Unauthorized: + return opKnownHttpFailure(resp.status, resp); + //FIXME: show more details in docs + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); } - // return readSuccessResponseJsonOrThrow(resp, codecForIncomingHistory()); } /** * https://docs.taler.net/core/api-bank-wire.html#get--history-outgoing - * + * */ - async getHistoryOutgoing(auth: string, pagination?: PaginationParams) { + async getHistoryOutgoing( + auth: string, + params?: PaginationParams & LongPollParams, + ) { const url = new URL(`history/outgoing`, this.baseUrl); - addPaginationParams(url, pagination) + addPaginationParams(url, params); + addLongPollingParam(url, params); const resp = await this.httpLib.fetch(url.href, { method: "GET", headers: { Authorization: makeBasicAuthHeader(this.username, auth), - } + }, }); switch (resp.status) { - case HttpStatusCode.Ok: return opSuccess(resp, codecForOutgoingHistory()) - case HttpStatusCode.NoContent: return opFixedSuccess({ outgoing_transactions: [] }) - case HttpStatusCode.BadRequest: return opKnownFailure("invalid-input", resp); - case HttpStatusCode.Unauthorized: return opKnownFailure("unauthorized", resp); - case HttpStatusCode.NotFound: return opKnownFailure("not-found", resp); - default: return opUnknownFailure(resp, await resp.text()) + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForOutgoingHistory()); + //FIXME: account should not be returned or make it optional + case HttpStatusCode.NoContent: + return opFixedSuccess({ + outgoing_transactions: [], + debit_account: undefined, + }); + //FIXME: show more details in docs + case HttpStatusCode.BadRequest: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Unauthorized: + return opKnownHttpFailure(resp.status, resp); + //FIXME: show more details in docs + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); } } /** * https://docs.taler.net/core/api-bank-wire.html#post--admin-add-incoming - * + * */ - async addIncoming(auth: string, body: TalerWireGatewayApi.AddIncomingRequest,) { + async addIncoming( + auth: string, + body: TalerWireGatewayApi.AddIncomingRequest, + ) { const url = new URL(`admin/add-incoming`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "POST", headers: { Authorization: makeBasicAuthHeader(this.username, auth), }, - body + body, }); switch (resp.status) { - case HttpStatusCode.Ok: return opSuccess(resp, codecForAddIncomingResponse()) - case HttpStatusCode.BadRequest: return opKnownFailure("invalid-input", resp); - case HttpStatusCode.Unauthorized: return opKnownFailure("unauthorized", resp); - case HttpStatusCode.NotFound: return opKnownFailure("not-found", resp); - case HttpStatusCode.Conflict: return opKnownFailure("reserve-id-already-used", resp); - default: return opUnknownFailure(resp, await resp.text()) + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForAddIncomingResponse()); + //FIXME: show more details in docs + case HttpStatusCode.BadRequest: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Unauthorized: + return opKnownHttpFailure(resp.status, resp); + //FIXME: show more details in docs + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Conflict: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); } } } - 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)); + } + } +} diff --git a/packages/taler-util/src/http-client/exchange.ts b/packages/taler-util/src/http-client/exchange.ts index f55be0043..68d68267f 100644 --- a/packages/taler-util/src/http-client/exchange.ts +++ b/packages/taler-util/src/http-client/exchange.ts @@ -1,82 +1,179 @@ -import { HttpRequestLibrary } from "../http-common.js"; +import { HttpRequestLibrary, readTalerErrorResponse } from "../http-common.js"; import { HttpStatusCode } from "../http-status-codes.js"; import { createPlatformHttpLib } from "../http.js"; import { LibtoolVersion } from "../libtool-version.js"; import { hash } from "../nacl-fast.js"; -import { FailCasesByMethod, ResultByMethod, opEmptySuccess, opFixedSuccess, opKnownFailure, opSuccess, opUnknownFailure } from "../operation.js"; -import { TalerSignaturePurpose, amountToBuffer, bufferForUint32, buildSigPS, decodeCrock, eddsaSign, encodeCrock, stringToBytes, timestampRoundedToBuffer } from "../taler-crypto.js"; -import { OfficerAccount, PaginationParams, SigningKey, TalerExchangeApi, codecForAmlDecisionDetails, codecForAmlRecords, codecForExchangeConfig } from "./types.js"; -import { addPaginationParams } from "./utils.js"; +import { + FailCasesByMethod, + ResultByMethod, + opEmptySuccess, + opFixedSuccess, + opKnownHttpFailure, + opSuccessFromHttp, + opUnknownFailure, +} from "../operation.js"; +import { + TalerSignaturePurpose, + amountToBuffer, + bufferForUint32, + buildSigPS, + decodeCrock, + eddsaSign, + encodeCrock, + stringToBytes, + timestampRoundedToBuffer, +} from "../taler-crypto.js"; +import { + OfficerAccount, + PaginationParams, + SigningKey, + TalerExchangeApi, + codecForAmlDecisionDetails, + codecForAmlRecords, + codecForExchangeConfig, + codecForExchangeKeys, +} from "./types.js"; +import { CacheEvictor, addPaginationParams, nullEvictor } from "./utils.js"; + +export type TalerExchangeResultByMethod< + prop extends keyof TalerExchangeHttpClient, +> = ResultByMethod<TalerExchangeHttpClient, prop>; +export type TalerExchangeErrorsByMethod< + prop extends keyof TalerExchangeHttpClient, +> = FailCasesByMethod<TalerExchangeHttpClient, prop>; + +export enum TalerExchangeCacheEviction { + CREATE_DESCISION, +} -export type TalerExchangeResultByMethod<prop extends keyof TalerExchangeHttpClient> = ResultByMethod<TalerExchangeHttpClient, prop> -export type TalerExchangeErrorsByMethod<prop extends keyof TalerExchangeHttpClient> = FailCasesByMethod<TalerExchangeHttpClient, prop> /** */ export class TalerExchangeHttpClient { httpLib: HttpRequestLibrary; - public readonly PROTOCOL_VERSION = "17:0:0"; + public readonly PROTOCOL_VERSION = "18:0:1"; + cacheEvictor: CacheEvictor<TalerExchangeCacheEviction>; constructor( readonly baseUrl: string, httpClient?: HttpRequestLibrary, + cacheEvictor?: CacheEvictor<TalerExchangeCacheEviction>, ) { this.httpLib = httpClient ?? createPlatformHttpLib(); + this.cacheEvictor = cacheEvictor ?? nullEvictor; } isCompatible(version: string): boolean { - const compare = LibtoolVersion.compare(this.PROTOCOL_VERSION, version) - return compare?.compatible ?? false + const compare = LibtoolVersion.compare(this.PROTOCOL_VERSION, version); + return compare?.compatible ?? false; } /** - * https://docs.taler.net/core/api-merchant.html#get--config - * + * https://docs.taler.net/core/api-exchange.html#get--seed + * + */ + async getSeed() { + const url = new URL(`seed`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + }); + switch (resp.status) { + case HttpStatusCode.Ok: + const buffer = await resp.bytes(); + const uintar = new Uint8Array(buffer); + + return opFixedSuccess(uintar); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + /** + * https://docs.taler.net/core/api-exchange.html#get--config + * */ async getConfig() { const url = new URL(`config`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { - method: "GET" + method: "GET", + }); + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForExchangeConfig()); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + /** + * https://docs.taler.net/core/api-merchant.html#get--config + * + * PARTIALLY IMPLEMENTED!! + */ + async getKeys() { + const url = new URL(`keys`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "GET", }); switch (resp.status) { - case HttpStatusCode.Ok: return opSuccess(resp, codecForExchangeConfig()) - default: return opUnknownFailure(resp, await resp.text()) + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForExchangeKeys()); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); } } + // TERMS + // // AML operations // /** * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-decisions-$STATE - * + * */ - async getDecisionsByState(auth: OfficerAccount, state: TalerExchangeApi.AmlState, pagination?: PaginationParams) { - const url = new URL(`aml/${auth.id}/decisions/${TalerExchangeApi.AmlState[state]}`, this.baseUrl); - addPaginationParams(url, pagination) + async getDecisionsByState( + auth: OfficerAccount, + state: TalerExchangeApi.AmlState, + pagination?: PaginationParams, + ) { + const url = new URL( + `aml/${auth.id}/decisions/${TalerExchangeApi.AmlState[state]}`, + this.baseUrl, + ); + addPaginationParams(url, pagination); const resp = await this.httpLib.fetch(url.href, { method: "GET", headers: { - "Taler-AML-Officer-Signature": buildQuerySignature(auth.signingKey) - } + "Taler-AML-Officer-Signature": buildQuerySignature(auth.signingKey), + }, }); switch (resp.status) { - case HttpStatusCode.Ok: return opSuccess(resp, codecForAmlRecords()) - case HttpStatusCode.NoContent: return opFixedSuccess({ records: [] }) + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForAmlRecords()); + case HttpStatusCode.NoContent: + return opFixedSuccess({ records: [] }); //this should be unauthorized - case HttpStatusCode.Forbidden: return opKnownFailure("unauthorized", resp); - case HttpStatusCode.Unauthorized: return opKnownFailure("unauthorized", resp); - case HttpStatusCode.NotFound: return opKnownFailure("officer-not-found", resp); - case HttpStatusCode.Conflict: return opKnownFailure("officer-disabled", resp); - default: return opUnknownFailure(resp, await resp.text()) + case HttpStatusCode.Forbidden: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Unauthorized: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Conflict: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); } } /** * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-decision-$H_PAYTO - * + * */ async getDecisionDetails(auth: OfficerAccount, account: string) { const url = new URL(`aml/${auth.id}/decision/${account}`, this.baseUrl); @@ -84,48 +181,62 @@ export class TalerExchangeHttpClient { const resp = await this.httpLib.fetch(url.href, { method: "GET", headers: { - "Taler-AML-Officer-Signature": buildQuerySignature(auth.signingKey) - } + "Taler-AML-Officer-Signature": buildQuerySignature(auth.signingKey), + }, }); switch (resp.status) { - case HttpStatusCode.Ok: return opSuccess(resp, codecForAmlDecisionDetails()) - case HttpStatusCode.NoContent: return opFixedSuccess({ aml_history: [], kyc_attributes: [] }) + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForAmlDecisionDetails()); + case HttpStatusCode.NoContent: + return opFixedSuccess({ aml_history: [], kyc_attributes: [] }); //this should be unauthorized - case HttpStatusCode.Forbidden: return opKnownFailure("unauthorized", resp); - case HttpStatusCode.Unauthorized: return opKnownFailure("unauthorized", resp); - case HttpStatusCode.NotFound: return opKnownFailure("officer-not-found", resp); - case HttpStatusCode.Conflict: return opKnownFailure("officer-disabled", resp); - default: return opUnknownFailure(resp, await resp.text()) + case HttpStatusCode.Forbidden: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Unauthorized: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Conflict: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); } } /** * https://docs.taler.net/core/api-exchange.html#post--aml-$OFFICER_PUB-decision - * + * */ - async addDecisionDetails(auth: OfficerAccount, decision: Omit<TalerExchangeApi.AmlDecision, "officer_sig">) { + async addDecisionDetails( + auth: OfficerAccount, + decision: Omit<TalerExchangeApi.AmlDecision, "officer_sig">, + ) { const url = new URL(`aml/${auth.id}/decision`, this.baseUrl); - const body = buildDecisionSignature(auth.signingKey, decision) + const body = buildDecisionSignature(auth.signingKey, decision); const resp = await this.httpLib.fetch(url.href, { method: "POST", body, }); switch (resp.status) { - case HttpStatusCode.NoContent: return opEmptySuccess() + case HttpStatusCode.NoContent: + return opEmptySuccess(resp); //FIXME: this should be unauthorized - case HttpStatusCode.Forbidden: return opKnownFailure("unauthorized", resp); - case HttpStatusCode.Unauthorized: return opKnownFailure("unauthorized", resp); - //FIXME: this two need to be splitted by error code - case HttpStatusCode.NotFound: return opKnownFailure("officer-or-account-not-found", resp); - case HttpStatusCode.Conflict: return opKnownFailure("officer-disabled-or-recent-decision", resp); - default: return opUnknownFailure(resp, await resp.text()) + case HttpStatusCode.Forbidden: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Unauthorized: + return opKnownHttpFailure(resp.status, resp); + //FIXME: this two need to be split by error code + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Conflict: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); } } - - } function buildQuerySignature(key: SigningKey): string { @@ -140,11 +251,11 @@ function buildDecisionSignature( key: SigningKey, decision: Omit<TalerExchangeApi.AmlDecision, "officer_sig">, ): TalerExchangeApi.AmlDecision { - const zero = new Uint8Array(new ArrayBuffer(64)) + const zero = new Uint8Array(new ArrayBuffer(64)); const sigBlob = buildSigPS(TalerSignaturePurpose.TALER_SIGNATURE_AML_DECISION) //TODO: new need the null terminator, also in the exchange - .put(hash(stringToBytes(decision.justification)))//check null + .put(hash(stringToBytes(decision.justification))) //check null .put(timestampRoundedToBuffer(decision.decision_time)) .put(amountToBuffer(decision.new_threshold)) .put(decodeCrock(decision.h_payto)) @@ -155,6 +266,6 @@ function buildDecisionSignature( const officer_sig = encodeCrock(eddsaSign(sigBlob, key)); return { ...decision, - officer_sig - } -}
\ No newline at end of file + officer_sig, + }; +} diff --git a/packages/taler-util/src/http-client/merchant.ts b/packages/taler-util/src/http-client/merchant.ts index a6dc4661f..892971fee 100644 --- a/packages/taler-util/src/http-client/merchant.ts +++ b/packages/taler-util/src/http-client/merchant.ts @@ -1,43 +1,2372 @@ -import { HttpRequestLibrary } from "../http-common.js"; -import { HttpStatusCode } from "../http-status-codes.js"; -import { createPlatformHttpLib } from "../http.js"; -import { LibtoolVersion } from "../libtool-version.js"; -import { FailCasesByMethod, ResultByMethod, opSuccess, opUnknownFailure } from "../operation.js"; -import { codecForMerchantConfig } from "./types.js"; +/* + This file is part of GNU Taler + (C) 2022-2024 Taler Systems S.A. -export type TalerMerchantResultByMethod<prop extends keyof TalerMerchantHttpClient> = ResultByMethod<TalerMerchantHttpClient, prop> -export type TalerMerchantErrorsByMethod<prop extends keyof TalerMerchantHttpClient> = FailCasesByMethod<TalerMerchantHttpClient, prop> + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import { + AccessToken, + FailCasesByMethod, + HttpStatusCode, + LibtoolVersion, + PaginationParams, + ResultByMethod, + TalerMerchantApi, + codecForAbortResponse, + codecForAccountAddResponse, + codecForAccountKycRedirects, + codecForAccountsSummaryResponse, + codecForBankAccountEntry, + codecForClaimResponse, + codecForInstancesResponse, + codecForInventorySummaryResponse, + codecForMerchantConfig, + codecForMerchantOrderPrivateStatusResponse, + codecForMerchantPosProductDetail, + codecForMerchantRefundResponse, + codecForOrderHistory, + codecForOtpDeviceDetails, + codecForOtpDeviceSummaryResponse, + codecForOutOfStockResponse, + codecForPaidRefundStatusResponse, + codecForPaymentResponse, + codecForPostOrderResponse, + codecForProductDetail, + codecForQueryInstancesResponse, + codecForStatusGoto, + codecForStatusPaid, + codecForStatusStatusUnpaid, + codecForTansferList, + codecForTemplateDetails, + codecForTemplateSummaryResponse, + codecForTokenFamiliesList, + codecForTokenFamilyDetails, + codecForWalletRefundResponse, + codecForWalletTemplateDetails, + codecForWebhookDetails, + codecForWebhookSummaryResponse, + opEmptySuccess, + opKnownAlternativeFailure, + opKnownHttpFailure, +} from "@gnu-taler/taler-util"; +import { + HttpRequestLibrary, + HttpResponse, + createPlatformHttpLib, + readTalerErrorResponse, +} from "@gnu-taler/taler-util/http"; +import { opSuccessFromHttp, opUnknownFailure } from "../operation.js"; +import { + CacheEvictor, + addMerchantPaginationParams, + makeBearerTokenAuthHeader, + nullEvictor, +} from "./utils.js"; + +export type TalerMerchantInstanceResultByMethod< + prop extends keyof TalerMerchantInstanceHttpClient, +> = ResultByMethod<TalerMerchantInstanceHttpClient, prop>; +export type TalerMerchantInstanceErrorsByMethod< + prop extends keyof TalerMerchantInstanceHttpClient, +> = FailCasesByMethod<TalerMerchantInstanceHttpClient, prop>; + +export enum TalerMerchantInstanceCacheEviction { + CREATE_ORDER, + UPDATE_ORDER, + DELETE_ORDER, + UPDATE_CURRENT_INSTANCE, + DELETE_CURRENT_INSTANCE, + CREATE_BANK_ACCOUNT, + UPDATE_BANK_ACCOUNT, + DELETE_BANK_ACCOUNT, + CREATE_PRODUCT, + UPDATE_PRODUCT, + DELETE_PRODUCT, + CREATE_TRANSFER, + DELETE_TRANSFER, + CREATE_DEVICE, + UPDATE_DEVICE, + DELETE_DEVICE, + CREATE_TEMPLATE, + UPDATE_TEMPLATE, + DELETE_TEMPLATE, + CREATE_WEBHOOK, + UPDATE_WEBHOOK, + DELETE_WEBHOOK, + CREATE_TOKENFAMILY, + UPDATE_TOKENFAMILY, + DELETE_TOKENFAMILY, + LAST, +} +export enum TalerMerchantManagementCacheEviction { + CREATE_INSTANCE = TalerMerchantInstanceCacheEviction.LAST + 1, + UPDATE_INSTANCE, + DELETE_INSTANCE, +} /** + * Protocol version spoken with the core bank. + * + * Endpoint must be ordered in the same way that in the docs + * Response code (http and taler) must have the same order that in the docs + * That way is easier to see changes + * + * Uses libtool's current:revision:age versioning. */ -export class TalerMerchantHttpClient { - httpLib: HttpRequestLibrary; - public readonly PROTOCOL_VERSION = "5:0:1"; +export class TalerMerchantInstanceHttpClient { + public readonly PROTOCOL_VERSION = "15:0:0"; + + readonly httpLib: HttpRequestLibrary; + readonly cacheEvictor: CacheEvictor<TalerMerchantInstanceCacheEviction>; constructor( readonly baseUrl: string, httpClient?: HttpRequestLibrary, + cacheEvictor?: CacheEvictor<TalerMerchantInstanceCacheEviction>, ) { this.httpLib = httpClient ?? createPlatformHttpLib(); + this.cacheEvictor = cacheEvictor ?? nullEvictor; } isCompatible(version: string): boolean { - const compare = LibtoolVersion.compare(this.PROTOCOL_VERSION, version) - return compare?.compatible ?? false + const compare = LibtoolVersion.compare(this.PROTOCOL_VERSION, version); + return compare?.compatible ?? false; } + /** * https://docs.taler.net/core/api-merchant.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, codecForMerchantConfig()); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + // + // Wallet API + // + + /** + * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-orders-$ORDER_ID-claim + */ + async claimOrder(orderId: string, body: TalerMerchantApi.ClaimRequest) { + const url = new URL(`orders/${orderId}/claim`, this.baseUrl); + + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + body, + }); + + switch (resp.status) { + case HttpStatusCode.Ok: { + this.cacheEvictor.notifySuccess( + TalerMerchantInstanceCacheEviction.UPDATE_ORDER, + ); + return opSuccessFromHttp(resp, codecForClaimResponse()); + } + case HttpStatusCode.Conflict: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-orders-$ORDER_ID-pay + */ + async makePayment(orderId: string, body: TalerMerchantApi.PayRequest) { + const url = new URL(`orders/${orderId}/pay`, this.baseUrl); + + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + body, + }); + + switch (resp.status) { + case HttpStatusCode.Ok: { + this.cacheEvictor.notifySuccess( + TalerMerchantInstanceCacheEviction.UPDATE_ORDER, + ); + return opSuccessFromHttp(resp, codecForPaymentResponse()); + } + case HttpStatusCode.BadRequest: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.PaymentRequired: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Forbidden: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.RequestTimeout: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Conflict: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Gone: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.PreconditionFailed: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.BadGateway: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.GatewayTimeout: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-orders-$ORDER_ID + */ + + async getPaymentStatus( + orderId: string, + params: TalerMerchantApi.PaymentStatusRequestParams = {}, + ) { + const url = new URL(`orders/${orderId}`, this.baseUrl); + + if (params.allowRefundedForRepurchase !== undefined) { + url.searchParams.set( + "allow_refunded_for_repurchase", + params.allowRefundedForRepurchase ? "YES" : "NO", + ); + } + if (params.awaitRefundObtained !== undefined) { + url.searchParams.set( + "await_refund_obtained", + params.allowRefundedForRepurchase ? "YES" : "NO", + ); + } + if (params.claimToken !== undefined) { + url.searchParams.set("token", params.claimToken); + } + if (params.contractTermHash !== undefined) { + url.searchParams.set("h_contract", params.contractTermHash); + } + if (params.refund !== undefined) { + url.searchParams.set("refund", params.refund); + } + if (params.sessionId !== undefined) { + url.searchParams.set("session_id", params.sessionId); + } + if (params.timeout !== undefined) { + url.searchParams.set("timeout_ms", String(params.timeout)); + } + + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + // body, + }); + + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForStatusPaid()); + case HttpStatusCode.Accepted: + return opSuccessFromHttp(resp, codecForStatusGoto()); + // case HttpStatusCode.Found: not possible since content is not HTML + case HttpStatusCode.PaymentRequired: + return opSuccessFromHttp(resp, codecForStatusStatusUnpaid()); + case HttpStatusCode.Forbidden: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotAcceptable: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#demonstrating-payment + */ + async demostratePayment(orderId: string, body: TalerMerchantApi.PaidRequest) { + const url = new URL(`orders/${orderId}/paid`, this.baseUrl); + + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + body, + }); + + switch (resp.status) { + case HttpStatusCode.Ok: { + this.cacheEvictor.notifySuccess( + TalerMerchantInstanceCacheEviction.UPDATE_ORDER, + ); + return opSuccessFromHttp(resp, codecForPaidRefundStatusResponse()); + } + case HttpStatusCode.BadRequest: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Forbidden: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#aborting-incomplete-payments + */ + async abortIncompletePayment( + orderId: string, + body: TalerMerchantApi.AbortRequest, + ) { + const url = new URL(`orders/${orderId}/abort`, this.baseUrl); + + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + body, + }); + + switch (resp.status) { + case HttpStatusCode.Ok: { + this.cacheEvictor.notifySuccess( + TalerMerchantInstanceCacheEviction.UPDATE_ORDER, + ); + return opSuccessFromHttp(resp, codecForAbortResponse()); + } + case HttpStatusCode.BadRequest: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Forbidden: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#obtaining-refunds + */ + async obtainRefund( + orderId: string, + body: TalerMerchantApi.WalletRefundRequest, + ) { + const url = new URL(`orders/${orderId}/refund`, this.baseUrl); + + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + body, + }); + + switch (resp.status) { + case HttpStatusCode.Ok: { + this.cacheEvictor.notifySuccess( + TalerMerchantInstanceCacheEviction.UPDATE_ORDER, + ); + return opSuccessFromHttp(resp, codecForWalletRefundResponse()); + } + case HttpStatusCode.BadRequest: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Forbidden: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + // + // Management + // + + /** + * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-auth + */ + async updateCurrentInstanceAuthentication( + token: AccessToken | undefined, + body: TalerMerchantApi.InstanceAuthConfigurationMessage, + ) { + const url = new URL(`private/auth`, this.baseUrl); + + const headers: Record<string, string> = {}; + if (token) { + headers.Authorization = makeBearerTokenAuthHeader(token); + } + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + body, + headers, + }); + + switch (resp.status) { + case HttpStatusCode.Ok: // FIXME: missing in docs + return opEmptySuccess(resp); + case HttpStatusCode.NoContent: + return opEmptySuccess(resp); + case HttpStatusCode.Unauthorized: // FIXME: missing in docs + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#patch-[-instances-$INSTANCE]-private + */ + async updateCurrentInstance( + token: AccessToken | undefined, + body: TalerMerchantApi.InstanceReconfigurationMessage, + ) { + const url = new URL(`private`, this.baseUrl); + + const headers: Record<string, string> = {}; + if (token) { + headers.Authorization = makeBearerTokenAuthHeader(token); + } + const resp = await this.httpLib.fetch(url.href, { + method: "PATCH", + body, + headers, + }); + switch (resp.status) { + case HttpStatusCode.NoContent: { + this.cacheEvictor.notifySuccess( + TalerMerchantInstanceCacheEviction.UPDATE_CURRENT_INSTANCE, + ); + return opEmptySuccess(resp); + } + case HttpStatusCode.Unauthorized: // FIXME: missing in docs + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private + * + */ + async getCurrentInstanceDetails(token: AccessToken) { + const url = new URL(`private`, this.baseUrl); + + const headers: Record<string, string> = {}; + if (token) { + headers.Authorization = makeBearerTokenAuthHeader(token); + } + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + headers, + }); + + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForQueryInstancesResponse()); + case HttpStatusCode.Unauthorized: // FIXME: missing in docs + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: // FIXME: missing in docs + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCE]-private + */ + async deleteCurrentInstance( + token: AccessToken | undefined, + params: { purge?: boolean } = {}, + ) { + const url = new URL(`private`, this.baseUrl); + + if (params.purge !== undefined) { + url.searchParams.set("purge", params.purge ? "YES" : "NO"); + } + + const headers: Record<string, string> = {}; + if (token) { + headers.Authorization = makeBearerTokenAuthHeader(token); + } + const resp = await this.httpLib.fetch(url.href, { + method: "DELETE", + headers, + }); + + switch (resp.status) { + case HttpStatusCode.NoContent: { + this.cacheEvictor.notifySuccess( + TalerMerchantInstanceCacheEviction.DELETE_CURRENT_INSTANCE, + ); + return opEmptySuccess(resp); + } + case HttpStatusCode.Unauthorized: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Conflict: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#get--instances-$INSTANCE-private-kyc + */ + async getCurrentIntanceKycStatus( + token: AccessToken | undefined, + params: TalerMerchantApi.GetKycStatusRequestParams = {}, + ) { + const url = new URL(`private/kyc`, this.baseUrl); + + if (params.wireHash) { + url.searchParams.set("h_wire", params.wireHash); + } + if (params.exchangeURL) { + url.searchParams.set("exchange_url", params.exchangeURL); + } + if (params.timeout) { + url.searchParams.set("timeout_ms", String(params.timeout)); + } + + const headers: Record<string, string> = {}; + if (token) { + headers.Authorization = makeBearerTokenAuthHeader(token); + } + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + headers, + }); + + switch (resp.status) { + case HttpStatusCode.Accepted: + return opSuccessFromHttp(resp, codecForAccountKycRedirects()); + case HttpStatusCode.NoContent: + return opEmptySuccess(resp); + case HttpStatusCode.Unauthorized: // FIXME: missing in docs + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: // FIXME: missing in docs + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.BadGateway: + return opKnownAlternativeFailure( + resp, + resp.status, + codecForAccountKycRedirects(), + ); + case HttpStatusCode.ServiceUnavailable: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.GatewayTimeout: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + // + // Bank Accounts + // + + /** + * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-accounts + */ + async addBankAccount( + token: AccessToken | undefined, + body: TalerMerchantApi.AccountAddDetails, + ) { + const url = new URL(`private/accounts`, this.baseUrl); + + const headers: Record<string, string> = {}; + if (token) { + headers.Authorization = makeBearerTokenAuthHeader(token); + } + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + body, + headers, + }); + + switch (resp.status) { + case HttpStatusCode.Ok: { + this.cacheEvictor.notifySuccess( + TalerMerchantInstanceCacheEviction.CREATE_BANK_ACCOUNT, + ); + return opSuccessFromHttp(resp, codecForAccountAddResponse()); + } + case HttpStatusCode.Unauthorized: // FIXME: missing in docs + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Conflict: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#patch-[-instances-$INSTANCE]-private-accounts-$H_WIRE + */ + async updateBankAccount( + token: AccessToken | undefined, + wireAccount: string, + body: TalerMerchantApi.AccountPatchDetails, + ) { + const url = new URL(`private/accounts/${wireAccount}`, this.baseUrl); + + const headers: Record<string, string> = {}; + if (token) { + headers.Authorization = makeBearerTokenAuthHeader(token); + } + const resp = await this.httpLib.fetch(url.href, { + method: "PATCH", + body, + headers, + }); + switch (resp.status) { + case HttpStatusCode.NoContent: { + this.cacheEvictor.notifySuccess( + TalerMerchantInstanceCacheEviction.UPDATE_BANK_ACCOUNT, + ); + return opEmptySuccess(resp); + } + case HttpStatusCode.Unauthorized: // FIXME: missing in docs + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-accounts + */ + async listBankAccounts(token: AccessToken, params?: PaginationParams) { + const url = new URL(`private/accounts`, this.baseUrl); + + // addMerchantPaginationParams(url, params); + + const headers: Record<string, string> = {}; + if (token) { + headers.Authorization = makeBearerTokenAuthHeader(token); + } + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + headers, + }); + + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForAccountsSummaryResponse()); + case HttpStatusCode.Unauthorized: // FIXME: missing in docs + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-accounts-$H_WIRE + */ + async getBankAccountDetails( + token: AccessToken | undefined, + wireAccount: string, + ) { + const url = new URL(`private/accounts/${wireAccount}`, this.baseUrl); + + const headers: Record<string, string> = {}; + if (token) { + headers.Authorization = makeBearerTokenAuthHeader(token); + } + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + headers, + }); + + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForBankAccountEntry()); + case HttpStatusCode.Unauthorized: // FIXME: missing in docs + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCE]-private-accounts-$H_WIRE + */ + async deleteBankAccount(token: AccessToken | undefined, wireAccount: string) { + const url = new URL(`private/accounts/${wireAccount}`, this.baseUrl); + + const headers: Record<string, string> = {}; + if (token) { + headers.Authorization = makeBearerTokenAuthHeader(token); + } + const resp = await this.httpLib.fetch(url.href, { + method: "DELETE", + headers, + }); + + switch (resp.status) { + case HttpStatusCode.NoContent: { + this.cacheEvictor.notifySuccess( + TalerMerchantInstanceCacheEviction.DELETE_BANK_ACCOUNT, + ); + return opEmptySuccess(resp); + } + case HttpStatusCode.Unauthorized: // FIXME: missing in docs + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + // + // Inventory Management + // + + /** + * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-products + */ + async addProduct( + token: AccessToken | undefined, + body: TalerMerchantApi.ProductAddDetail, + ) { + const url = new URL(`private/products`, this.baseUrl); + + const headers: Record<string, string> = {}; + if (token) { + headers.Authorization = makeBearerTokenAuthHeader(token); + } + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + body, + headers, + }); + + switch (resp.status) { + case HttpStatusCode.NoContent: { + this.cacheEvictor.notifySuccess( + TalerMerchantInstanceCacheEviction.CREATE_PRODUCT, + ); + return opEmptySuccess(resp); + } + case HttpStatusCode.Unauthorized: // FIXME: missing in docs + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: // FIXME: missing in docs + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Conflict: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#patch-[-instances-$INSTANCE]-private-products-$PRODUCT_ID + */ + async updateProduct( + token: AccessToken | undefined, + productId: string, + body: TalerMerchantApi.ProductPatchDetail, + ) { + const url = new URL(`private/products/${productId}`, this.baseUrl); + + const headers: Record<string, string> = {}; + if (token) { + headers.Authorization = makeBearerTokenAuthHeader(token); + } + const resp = await this.httpLib.fetch(url.href, { + method: "PATCH", + body, + headers, + }); + + switch (resp.status) { + case HttpStatusCode.NoContent: { + this.cacheEvictor.notifySuccess( + TalerMerchantInstanceCacheEviction.UPDATE_PRODUCT, + ); + return opEmptySuccess(resp); + } + case HttpStatusCode.Unauthorized: // FIXME: missing in docs + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Conflict: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-products + */ + async listProducts( + token: AccessToken | undefined, + params?: PaginationParams, + ) { + const url = new URL(`private/products`, this.baseUrl); + + addMerchantPaginationParams(url, params); + + const headers: Record<string, string> = {}; + if (token) { + headers.Authorization = makeBearerTokenAuthHeader(token); + } + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + headers, + }); + + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForInventorySummaryResponse()); + case HttpStatusCode.Unauthorized: // FIXME: not in docs + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-pos + */ + async getPointOfSaleInventory(token: AccessToken | undefined) { + const url = new URL(`private/pos`, this.baseUrl); + + const headers: Record<string, string> = {}; + if (token) { + headers.Authorization = makeBearerTokenAuthHeader(token); + } + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + headers, + }); + + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForMerchantPosProductDetail()); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + + } + + /** + * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-products-$PRODUCT_ID + */ + async getProductDetails(token: AccessToken | undefined, productId: string) { + const url = new URL(`private/products/${productId}`, this.baseUrl); + + const headers: Record<string, string> = {}; + if (token) { + headers.Authorization = makeBearerTokenAuthHeader(token); + } + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + headers, + }); + + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForProductDetail()); + case HttpStatusCode.Unauthorized: // FIXME: missing in docs + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#reserving-inventory + */ + async lockProduct( + token: AccessToken | undefined, + productId: string, + body: TalerMerchantApi.LockRequest, + ) { + const url = new URL(`private/products/${productId}/lock`, this.baseUrl); + + const headers: Record<string, string> = {}; + if (token) { + headers.Authorization = makeBearerTokenAuthHeader(token); + } + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + body, + headers, + }); + + switch (resp.status) { + case HttpStatusCode.NoContent: { + this.cacheEvictor.notifySuccess( + TalerMerchantInstanceCacheEviction.UPDATE_PRODUCT, + ); + return opEmptySuccess(resp); + } + case HttpStatusCode.Unauthorized: // FIXME: missing in docs + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Gone: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#removing-products-from-inventory + */ + async deleteProduct(token: AccessToken | undefined, productId: string) { + const url = new URL(`private/products/${productId}`, this.baseUrl); + + const headers: Record<string, string> = {}; + if (token) { + headers.Authorization = makeBearerTokenAuthHeader(token); + } + const resp = await this.httpLib.fetch(url.href, { + method: "DELETE", + headers, + }); + + switch (resp.status) { + case HttpStatusCode.NoContent: { + this.cacheEvictor.notifySuccess( + TalerMerchantInstanceCacheEviction.DELETE_PRODUCT, + ); + return opEmptySuccess(resp); + } + case HttpStatusCode.Unauthorized: // FIXME: missing in docs + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Conflict: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + // + // Payment processing + // + + /** + * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-orders + */ + async createOrder( + token: AccessToken | undefined, + body: TalerMerchantApi.PostOrderRequest, + ) { + const url = new URL(`private/orders`, this.baseUrl); + + const headers: Record<string, string> = {}; + if (token) { + headers.Authorization = makeBearerTokenAuthHeader(token); + } + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + body, + headers, + }); + return this.procesOrderCreationResponse(resp); + } + + private async procesOrderCreationResponse(resp: HttpResponse) { + switch (resp.status) { + case HttpStatusCode.Ok: { + this.cacheEvictor.notifySuccess( + TalerMerchantInstanceCacheEviction.CREATE_ORDER, + ); + return opSuccessFromHttp(resp, codecForPostOrderResponse()); + } + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Unauthorized: // FIXME: missing in docs + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Conflict: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Gone: + return opKnownAlternativeFailure( + resp, + resp.status, + codecForOutOfStockResponse(), + ); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#inspecting-orders + */ + async listOrders( + token: AccessToken | undefined, + params: TalerMerchantApi.ListOrdersRequestParams = {}, + ) { + const url = new URL(`private/orders`, this.baseUrl); + + if (params.date) { + url.searchParams.set("date_s", String(params.date)); + } + if (params.fulfillmentUrl) { + url.searchParams.set("fulfillment_url", params.fulfillmentUrl); + } + if (params.paid !== undefined) { + url.searchParams.set("paid", params.paid ? "YES" : "NO"); + } + if (params.refunded !== undefined) { + url.searchParams.set("refunded", params.refunded ? "YES" : "NO"); + } + if (params.sessionId) { + url.searchParams.set("session_id", params.sessionId); + } + if (params.timeout) { + url.searchParams.set("timeout", String(params.timeout)); + } + if (params.wired !== undefined) { + url.searchParams.set("wired", params.wired ? "YES" : "NO"); + } + addMerchantPaginationParams(url, params); + + const headers: Record<string, string> = {}; + if (token) { + headers.Authorization = makeBearerTokenAuthHeader(token); + } + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + headers, + }); + + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForOrderHistory()); + case HttpStatusCode.NotFound: // FIXME: missing in docs + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Unauthorized: // FIXME: missing in docs + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-orders-$ORDER_ID + */ + async getOrderDetails( + token: AccessToken | undefined, + orderId: string, + params: TalerMerchantApi.GetOrderRequestParams = {}, + ) { + const url = new URL(`private/orders/${orderId}`, this.baseUrl); + + if (params.allowRefundedForRepurchase !== undefined) { + url.searchParams.set( + "allow_refunded_for_repurchase", + params.allowRefundedForRepurchase ? "YES" : "NO", + ); + } + if (params.sessionId) { + url.searchParams.set("session_id", params.sessionId); + } + if (params.timeout) { + url.searchParams.set("timeout_ms", String(params.timeout)); + } + + const headers: Record<string, string> = {}; + if (token) { + headers.Authorization = makeBearerTokenAuthHeader(token); + } + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + headers, + }); + + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp( + resp, + codecForMerchantOrderPrivateStatusResponse(), + ); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Unauthorized: // FIXME: missing in docs + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.BadGateway: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.GatewayTimeout: + return opKnownAlternativeFailure( + resp, + resp.status, + codecForOutOfStockResponse(), + ); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#private-order-data-cleanup + */ + async forgetOrder( + token: AccessToken | undefined, + orderId: string, + body: TalerMerchantApi.ForgetRequest, + ) { + const url = new URL(`private/orders/${orderId}/forget`, this.baseUrl); + + const headers: Record<string, string> = {}; + if (token) { + headers.Authorization = makeBearerTokenAuthHeader(token); + } + const resp = await this.httpLib.fetch(url.href, { + method: "PATCH", + body, + headers, + }); + + switch (resp.status) { + case HttpStatusCode.Ok: { + this.cacheEvictor.notifySuccess( + TalerMerchantInstanceCacheEviction.UPDATE_ORDER, + ); + return opEmptySuccess(resp); + } + case HttpStatusCode.NoContent: + return opEmptySuccess(resp); + case HttpStatusCode.Unauthorized: // FIXME: missing in docs + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.BadRequest: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Conflict: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCE]-private-orders-$ORDER_ID + */ + async deleteOrder(token: AccessToken | undefined, orderId: string) { + const url = new URL(`private/orders/${orderId}`, this.baseUrl); + + const headers: Record<string, string> = {}; + if (token) { + headers.Authorization = makeBearerTokenAuthHeader(token); + } + const resp = await this.httpLib.fetch(url.href, { + method: "DELETE", + headers, + }); + + switch (resp.status) { + case HttpStatusCode.NoContent: { + this.cacheEvictor.notifySuccess( + TalerMerchantInstanceCacheEviction.DELETE_ORDER, + ); + return opEmptySuccess(resp); + } + case HttpStatusCode.Unauthorized: // FIXME: missing in docs + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Conflict: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + // + // Refunds + // + + /** + * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-orders-$ORDER_ID-refund + */ + async addRefund( + token: AccessToken | undefined, + orderId: string, + body: TalerMerchantApi.RefundRequest, + ) { + const url = new URL(`private/orders/${orderId}/refund`, this.baseUrl); + + const headers: Record<string, string> = {}; + if (token) { + headers.Authorization = makeBearerTokenAuthHeader(token); + } + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + body, + headers, + }); + + switch (resp.status) { + case HttpStatusCode.Ok: { + this.cacheEvictor.notifySuccess( + TalerMerchantInstanceCacheEviction.UPDATE_ORDER, + ); + return opSuccessFromHttp(resp, codecForMerchantRefundResponse()); + } + case HttpStatusCode.Forbidden: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Unauthorized: // FIXME: missing in docs + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Gone: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Conflict: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + // + // Wire Transfer + // + + /** + * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-transfers + */ + async informWireTransfer( + token: AccessToken | undefined, + body: TalerMerchantApi.TransferInformation, + ) { + const url = new URL(`private/transfers`, this.baseUrl); + + const headers: Record<string, string> = {}; + if (token) { + headers.Authorization = makeBearerTokenAuthHeader(token); + } + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + body, + headers, + }); + + switch (resp.status) { + case HttpStatusCode.NoContent: { + this.cacheEvictor.notifySuccess( + TalerMerchantInstanceCacheEviction.CREATE_TRANSFER, + ); + return opEmptySuccess(resp); + } + case HttpStatusCode.Unauthorized: // FIXME: missing in docs + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Conflict: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-transfers + */ + async listWireTransfers( + token: AccessToken | undefined, + params: TalerMerchantApi.ListWireTransferRequestParams = {}, + ) { + const url = new URL(`private/transfers`, this.baseUrl); + + if (params.after) { + url.searchParams.set("after", String(params.after)); + } + if (params.before) { + url.searchParams.set("before", String(params.before)); + } + if (params.paytoURI) { + url.searchParams.set("payto_uri", params.paytoURI); + } + if (params.verified !== undefined) { + url.searchParams.set("verified", params.verified ? "YES" : "NO"); + } + addMerchantPaginationParams(url, params); + + const headers: Record<string, string> = {}; + if (token) { + headers.Authorization = makeBearerTokenAuthHeader(token); + } + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + headers, + }); + + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForTansferList()); + case HttpStatusCode.Unauthorized: // FIXME: missing in docs + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: // FIXME: missing in docs + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCE]-private-transfers-$TID + */ + async deleteWireTransfer(token: AccessToken | undefined, transferId: string) { + const url = new URL(`private/transfers/${transferId}`, this.baseUrl); + + const headers: Record<string, string> = {}; + if (token) { + headers.Authorization = makeBearerTokenAuthHeader(token); + } + const resp = await this.httpLib.fetch(url.href, { + method: "DELETE", + headers, + }); + + switch (resp.status) { + case HttpStatusCode.NoContent: { + this.cacheEvictor.notifySuccess( + TalerMerchantInstanceCacheEviction.DELETE_TRANSFER, + ); + return opEmptySuccess(resp); + } + case HttpStatusCode.Unauthorized: // FIXME: missing in docs + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Conflict: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + // + // OTP Devices + // + + /** + * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-otp-devices + */ + async addOtpDevice( + token: AccessToken | undefined, + body: TalerMerchantApi.OtpDeviceAddDetails, + ) { + const url = new URL(`private/otp-devices`, this.baseUrl); + + const headers: Record<string, string> = {}; + if (token) { + headers.Authorization = makeBearerTokenAuthHeader(token); + } + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + body, + headers, + }); + + switch (resp.status) { + case HttpStatusCode.NoContent: { + this.cacheEvictor.notifySuccess( + TalerMerchantInstanceCacheEviction.CREATE_DEVICE, + ); + return opEmptySuccess(resp); + } + case HttpStatusCode.Unauthorized: // FIXME: missing in docs + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#patch-[-instances-$INSTANCE]-private-otp-devices-$DEVICE_ID + */ + async updateOtpDevice( + token: AccessToken | undefined, + deviceId: string, + body: TalerMerchantApi.OtpDevicePatchDetails, + ) { + const url = new URL(`private/otp-devices/${deviceId}`, this.baseUrl); + + const headers: Record<string, string> = {}; + if (token) { + headers.Authorization = makeBearerTokenAuthHeader(token); + } + const resp = await this.httpLib.fetch(url.href, { + method: "PATCH", + body, + headers, + }); + switch (resp.status) { + case HttpStatusCode.NoContent: { + this.cacheEvictor.notifySuccess( + TalerMerchantInstanceCacheEviction.UPDATE_DEVICE, + ); + return opEmptySuccess(resp); + } + case HttpStatusCode.Unauthorized: // FIXME: missing in docs + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Conflict: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-otp-devices + */ + async listOtpDevices( + token: AccessToken | undefined, + params?: PaginationParams, + ) { + const url = new URL(`private/otp-devices`, this.baseUrl); + + addMerchantPaginationParams(url, params); + + const headers: Record<string, string> = {}; + if (token) { + headers.Authorization = makeBearerTokenAuthHeader(token); + } + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + headers, + }); + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForOtpDeviceSummaryResponse()); + case HttpStatusCode.Unauthorized: // FIXME: missing in docs + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-otp-devices-$DEVICE_ID + */ + async getOtpDeviceDetails( + token: AccessToken | undefined, + deviceId: string, + params: TalerMerchantApi.GetOtpDeviceRequestParams = {}, + ) { + const url = new URL(`private/otp-devices/${deviceId}`, this.baseUrl); + + if (params.faketime) { + url.searchParams.set("faketime", String(params.faketime)); + } + if (params.price) { + url.searchParams.set("price", params.price); + } + const headers: Record<string, string> = {}; + if (token) { + headers.Authorization = makeBearerTokenAuthHeader(token); + } + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + headers, + }); + + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForOtpDeviceDetails()); + case HttpStatusCode.Unauthorized: // FIXME: missing in docs + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCE]-private-otp-devices-$DEVICE_ID + */ + async deleteOtpDevice(token: AccessToken | undefined, deviceId: string) { + const url = new URL(`private/otp-devices/${deviceId}`, this.baseUrl); + + const headers: Record<string, string> = {}; + if (token) { + headers.Authorization = makeBearerTokenAuthHeader(token); + } const resp = await this.httpLib.fetch(url.href, { - method: "GET" + method: "DELETE", + headers, }); switch (resp.status) { - case HttpStatusCode.Ok: return opSuccess(resp, codecForMerchantConfig()) - default: return opUnknownFailure(resp, await resp.text()) + case HttpStatusCode.NoContent: { + this.cacheEvictor.notifySuccess( + TalerMerchantInstanceCacheEviction.DELETE_DEVICE, + ); + return opEmptySuccess(resp); + } + case HttpStatusCode.Unauthorized: // FIXME: missing in docs + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); } } -}
\ No newline at end of file + // + // Templates + // + + /** + * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-templates + */ + async addTemplate( + token: AccessToken | undefined, + body: TalerMerchantApi.TemplateAddDetails, + ) { + const url = new URL(`private/templates`, this.baseUrl); + + const headers: Record<string, string> = {}; + if (token) { + headers.Authorization = makeBearerTokenAuthHeader(token); + } + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + body, + headers, + }); + switch (resp.status) { + case HttpStatusCode.NoContent: { + this.cacheEvictor.notifySuccess( + TalerMerchantInstanceCacheEviction.CREATE_TEMPLATE, + ); + return opEmptySuccess(resp); + } + case HttpStatusCode.Unauthorized: // FIXME: missing in docs + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#patch-[-instances-$INSTANCE]-private-templates-$TEMPLATE_ID + */ + async updateTemplate( + token: AccessToken | undefined, + templateId: string, + body: TalerMerchantApi.TemplatePatchDetails, + ) { + const url = new URL(`private/templates/${templateId}`, this.baseUrl); + + const headers: Record<string, string> = {}; + if (token) { + headers.Authorization = makeBearerTokenAuthHeader(token); + } + const resp = await this.httpLib.fetch(url.href, { + method: "PATCH", + body, + headers, + }); + switch (resp.status) { + case HttpStatusCode.NoContent: { + this.cacheEvictor.notifySuccess( + TalerMerchantInstanceCacheEviction.UPDATE_TEMPLATE, + ); + return opEmptySuccess(resp); + } + case HttpStatusCode.Unauthorized: // FIXME: missing in docs + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Conflict: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#inspecting-template + */ + async listTemplates( + token: AccessToken | undefined, + params?: PaginationParams, + ) { + const url = new URL(`private/templates`, this.baseUrl); + + addMerchantPaginationParams(url, params); + + const headers: Record<string, string> = {}; + if (token) { + headers.Authorization = makeBearerTokenAuthHeader(token); + } + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + headers, + }); + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForTemplateSummaryResponse()); + case HttpStatusCode.Unauthorized: // FIXME: missing in docs + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-templates-$TEMPLATE_ID + */ + async getTemplateDetails(token: AccessToken | undefined, templateId: string) { + const url = new URL(`private/templates/${templateId}`, this.baseUrl); + + const headers: Record<string, string> = {}; + if (token) { + headers.Authorization = makeBearerTokenAuthHeader(token); + } + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + headers, + }); + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForTemplateDetails()); + case HttpStatusCode.Unauthorized: // FIXME: missing in docs + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCE]-private-templates-$TEMPLATE_ID + */ + async deleteTemplate(token: AccessToken | undefined, templateId: string) { + const url = new URL(`private/templates/${templateId}`, this.baseUrl); + + const headers: Record<string, string> = {}; + if (token) { + headers.Authorization = makeBearerTokenAuthHeader(token); + } + const resp = await this.httpLib.fetch(url.href, { + method: "DELETE", + headers, + }); + switch (resp.status) { + case HttpStatusCode.NoContent: { + this.cacheEvictor.notifySuccess( + TalerMerchantInstanceCacheEviction.DELETE_TEMPLATE, + ); + return opEmptySuccess(resp); + } + case HttpStatusCode.Unauthorized: // FIXME: missing in docs + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-templates-$TEMPLATE_ID + */ + async useTemplateGetInfo(templateId: string) { + const url = new URL(`templates/${templateId}`, this.baseUrl); + + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + }); + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForWalletTemplateDetails()); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCES]-templates-$TEMPLATE_ID + */ + async useTemplateCreateOrder( + templateId: string, + body: TalerMerchantApi.UsingTemplateDetails, + ) { + const url = new URL(`templates/${templateId}`, this.baseUrl); + + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + body, + }); + + return this.procesOrderCreationResponse(resp); + } + + // + // Webhooks + // + + /** + * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCES]-private-webhooks + */ + async addWebhook( + token: AccessToken | undefined, + body: TalerMerchantApi.WebhookAddDetails, + ) { + const url = new URL(`private/webhooks`, this.baseUrl); + + const headers: Record<string, string> = {}; + if (token) { + headers.Authorization = makeBearerTokenAuthHeader(token); + } + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + body, + headers, + }); + + switch (resp.status) { + case HttpStatusCode.NoContent: { + this.cacheEvictor.notifySuccess( + TalerMerchantInstanceCacheEviction.CREATE_WEBHOOK, + ); + return opEmptySuccess(resp); + } + case HttpStatusCode.Unauthorized: // FIXME: missing in docs + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#patch-[-instances-$INSTANCES]-private-webhooks-$WEBHOOK_ID + */ + async updateWebhook( + token: AccessToken | undefined, + webhookId: string, + body: TalerMerchantApi.WebhookPatchDetails, + ) { + const url = new URL(`private/webhooks/${webhookId}`, this.baseUrl); + + const headers: Record<string, string> = {}; + if (token) { + headers.Authorization = makeBearerTokenAuthHeader(token); + } + const resp = await this.httpLib.fetch(url.href, { + method: "PATCH", + body, + headers, + }); + + switch (resp.status) { + case HttpStatusCode.NoContent: { + this.cacheEvictor.notifySuccess( + TalerMerchantInstanceCacheEviction.UPDATE_WEBHOOK, + ); + return opEmptySuccess(resp); + } + case HttpStatusCode.Unauthorized: // FIXME: missing in docs + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Conflict: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCES]-private-webhooks + */ + async listWebhooks( + token: AccessToken | undefined, + params?: PaginationParams, + ) { + const url = new URL(`private/webhooks`, this.baseUrl); + + const headers: Record<string, string> = {}; + if (token) { + headers.Authorization = makeBearerTokenAuthHeader(token); + } + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + headers, + }); + + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForWebhookSummaryResponse()); + case HttpStatusCode.Unauthorized: // FIXME: missing in docs + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCES]-private-webhooks-$WEBHOOK_ID + */ + async getWebhookDetails(token: AccessToken | undefined, webhookId: string) { + const url = new URL(`private/webhooks/${webhookId}`, this.baseUrl); + + const headers: Record<string, string> = {}; + if (token) { + headers.Authorization = makeBearerTokenAuthHeader(token); + } + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + headers, + }); + switch (resp.status) { + case HttpStatusCode.NoContent: + return opSuccessFromHttp(resp, codecForWebhookDetails()); + case HttpStatusCode.Unauthorized: // FIXME: missing in docs + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCES]-private-webhooks-$WEBHOOK_ID + */ + async deleteWebhook(token: AccessToken | undefined, webhookId: string) { + const url = new URL(`private/webhooks/${webhookId}`, this.baseUrl); + + const headers: Record<string, string> = {}; + if (token) { + headers.Authorization = makeBearerTokenAuthHeader(token); + } + const resp = await this.httpLib.fetch(url.href, { + method: "DELETE", + headers, + }); + switch (resp.status) { + case HttpStatusCode.NoContent: { + this.cacheEvictor.notifySuccess( + TalerMerchantInstanceCacheEviction.DELETE_WEBHOOK, + ); + return opEmptySuccess(resp); + } + case HttpStatusCode.Unauthorized: // FIXME: missing in docs + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + // + // token families + // + + /** + * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCES]-private-tokenfamilies + */ + async createTokenFamily( + token: AccessToken | undefined, + body: TalerMerchantApi.TokenFamilyCreateRequest, + ) { + const url = new URL(`private/tokenfamilies`, this.baseUrl); + + const headers: Record<string, string> = {}; + if (token) { + headers.Authorization = makeBearerTokenAuthHeader(token); + } + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + body, + headers, + }); + + switch (resp.status) { + case HttpStatusCode.NoContent: { + this.cacheEvictor.notifySuccess( + TalerMerchantInstanceCacheEviction.CREATE_TOKENFAMILY, + ); + return opEmptySuccess(resp); + } + case HttpStatusCode.Unauthorized: // FIXME: missing in docs + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#patch-[-instances-$INSTANCES]-private-tokenfamilies-$TOKEN_FAMILY_SLUG + */ + async updateTokenFamily( + token: AccessToken | undefined, + tokenSlug: string, + body: TalerMerchantApi.TokenFamilyUpdateRequest, + ) { + const url = new URL(`private/tokenfamilies/${tokenSlug}`, this.baseUrl); + + const headers: Record<string, string> = {}; + if (token) { + headers.Authorization = makeBearerTokenAuthHeader(token); + } + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + body, + headers, + }); + switch (resp.status) { + case HttpStatusCode.Ok: { + this.cacheEvictor.notifySuccess( + TalerMerchantInstanceCacheEviction.UPDATE_TOKENFAMILY, + ); + return opSuccessFromHttp(resp, codecForTokenFamilyDetails()); + } + case HttpStatusCode.Unauthorized: // FIXME: missing in docs + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCES]-private-tokenfamilies + */ + async listTokenFamilies( + token: AccessToken | undefined, + params?: PaginationParams, + ) { + const url = new URL(`private/tokenfamilies`, this.baseUrl); + + const headers: Record<string, string> = {}; + if (token) { + headers.Authorization = makeBearerTokenAuthHeader(token); + } + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + headers, + }); + + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForTokenFamiliesList()); + case HttpStatusCode.Unauthorized: // FIXME: missing in docs + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCES]-private-tokenfamilies-$TOKEN_FAMILY_SLUG + */ + async getTokenFamilyDetails( + token: AccessToken | undefined, + tokenSlug: string, + ) { + const url = new URL(`private/tokenfamilies/${tokenSlug}`, this.baseUrl); + + const headers: Record<string, string> = {}; + if (token) { + headers.Authorization = makeBearerTokenAuthHeader(token); + } + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + headers, + }); + + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForTokenFamilyDetails()); + case HttpStatusCode.Unauthorized: // FIXME: missing in docs + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCES]-private-tokenfamilies-$TOKEN_FAMILY_SLUG + */ + async deleteTokenFamily(token: AccessToken | undefined, tokenSlug: string) { + const url = new URL(`private/tokenfamilies/${tokenSlug}`, this.baseUrl); + + const headers: Record<string, string> = {}; + if (token) { + headers.Authorization = makeBearerTokenAuthHeader(token); + } + const resp = await this.httpLib.fetch(url.href, { + method: "DELETE", + headers, + }); + switch (resp.status) { + case HttpStatusCode.NoContent: { + this.cacheEvictor.notifySuccess( + TalerMerchantInstanceCacheEviction.DELETE_TOKENFAMILY, + ); + return opEmptySuccess(resp); + } + case HttpStatusCode.Unauthorized: // FIXME: missing in docs + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + /** + * Get the auth api against the current instance + * + * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-token + * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCE]-private-token + */ + getAuthenticationAPI(): URL { + return new URL(`private/`, this.baseUrl); + } +} + +export type TalerMerchantManagementResultByMethod< + prop extends keyof TalerMerchantManagementHttpClient, +> = ResultByMethod<TalerMerchantManagementHttpClient, prop>; +export type TalerMerchantManagementErrorsByMethod< + prop extends keyof TalerMerchantManagementHttpClient, +> = FailCasesByMethod<TalerMerchantManagementHttpClient, prop>; + +export class TalerMerchantManagementHttpClient extends TalerMerchantInstanceHttpClient { + readonly cacheManagementEvictor: CacheEvictor< + TalerMerchantInstanceCacheEviction | TalerMerchantManagementCacheEviction + >; + constructor( + readonly baseUrl: string, + httpClient?: HttpRequestLibrary, + // cacheManagementEvictor?: CacheEvictor<TalerMerchantManagementCacheEviction>, + cacheEvictor?: CacheEvictor< + TalerMerchantInstanceCacheEviction | TalerMerchantManagementCacheEviction + >, + ) { + super(baseUrl, httpClient, cacheEvictor); + this.cacheManagementEvictor = cacheEvictor ?? nullEvictor; + } + + getSubInstanceAPI(instanceId: string) { + return new URL(`instances/${instanceId}/`, this.baseUrl); + } + + // + // Instance Management + // + + /** + * https://docs.taler.net/core/api-merchant.html#post--management-instances + */ + async createInstance( + token: AccessToken | undefined, + body: TalerMerchantApi.InstanceConfigurationMessage, + ) { + const url = new URL(`management/instances`, this.baseUrl); + + const headers: Record<string, string> = {}; + if (token) { + headers.Authorization = makeBearerTokenAuthHeader(token); + } + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + body, + headers, + }); + + switch (resp.status) { + case HttpStatusCode.NoContent: { + this.cacheManagementEvictor.notifySuccess( + TalerMerchantManagementCacheEviction.CREATE_INSTANCE, + ); + return opEmptySuccess(resp); + } + case HttpStatusCode.Unauthorized: // FIXME: missing in docs + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Conflict: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#post--management-instances-$INSTANCE-auth + */ + async updateInstanceAuthentication( + token: AccessToken | undefined, + instanceId: string, + body: TalerMerchantApi.InstanceAuthConfigurationMessage, + ) { + const url = new URL( + `management/instances/${instanceId}/auth`, + this.baseUrl, + ); + + const headers: Record<string, string> = {}; + if (token) { + headers.Authorization = makeBearerTokenAuthHeader(token); + } + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + body, + headers, + }); + + switch (resp.status) { + case HttpStatusCode.NoContent: + return opEmptySuccess(resp); + case HttpStatusCode.Unauthorized: // FIXME: missing in docs + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#patch--management-instances-$INSTANCE + */ + async updateInstance( + token: AccessToken | undefined, + instanceId: string, + body: TalerMerchantApi.InstanceReconfigurationMessage, + ) { + const url = new URL(`management/instances/${instanceId}`, this.baseUrl); + + const headers: Record<string, string> = {}; + if (token) { + headers.Authorization = makeBearerTokenAuthHeader(token); + } + const resp = await this.httpLib.fetch(url.href, { + method: "PATCH", + body, + headers, + }); + switch (resp.status) { + case HttpStatusCode.NoContent: { + this.cacheManagementEvictor.notifySuccess( + TalerMerchantManagementCacheEviction.UPDATE_INSTANCE, + ); + return opEmptySuccess(resp); + } + case HttpStatusCode.Unauthorized: // FIXME: missing in docs + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#get--management-instances + */ + async listInstances( + token: AccessToken | undefined, + params?: PaginationParams, + ) { + const url = new URL(`management/instances`, this.baseUrl); + + const headers: Record<string, string> = {}; + if (token) { + headers.Authorization = makeBearerTokenAuthHeader(token); + } + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + headers, + }); + + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForInstancesResponse()); + case HttpStatusCode.Unauthorized: // FIXME: missing in docs + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#get--management-instances-$INSTANCE + * + */ + async getInstanceDetails(token: AccessToken | undefined, instanceId: string) { + const url = new URL(`management/instances/${instanceId}`, this.baseUrl); + + const headers: Record<string, string> = {}; + if (token) { + headers.Authorization = makeBearerTokenAuthHeader(token); + } + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + headers, + }); + + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForQueryInstancesResponse()); + case HttpStatusCode.Unauthorized: // FIXME: missing in docs + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: // FIXME: missing in docs + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#delete--management-instances-$INSTANCE + */ + async deleteInstance( + token: AccessToken | undefined, + instanceId: string, + params: { purge?: boolean } = {}, + ) { + const url = new URL(`management/instances/${instanceId}`, this.baseUrl); + + if (params.purge !== undefined) { + url.searchParams.set("purge", params.purge ? "YES" : "NO"); + } + + const headers: Record<string, string> = {}; + if (token) { + headers.Authorization = makeBearerTokenAuthHeader(token); + } + const resp = await this.httpLib.fetch(url.href, { + method: "DELETE", + headers, + }); + switch (resp.status) { + case HttpStatusCode.NoContent: { + this.cacheManagementEvictor.notifySuccess( + TalerMerchantManagementCacheEviction.DELETE_INSTANCE, + ); + return opEmptySuccess(resp); + } + case HttpStatusCode.Unauthorized: // FIXME: missing in docs + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Conflict: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#get--management-instances-$INSTANCE-kyc + */ + async getIntanceKycStatus( + token: AccessToken | undefined, + instanceId: string, + params: TalerMerchantApi.GetKycStatusRequestParams, + ) { + const url = new URL(`management/instances/${instanceId}/kyc`, this.baseUrl); + + if (params.wireHash) { + url.searchParams.set("h_wire", params.wireHash); + } + if (params.exchangeURL) { + url.searchParams.set("exchange_url", params.exchangeURL); + } + if (params.timeout) { + url.searchParams.set("timeout_ms", String(params.timeout)); + } + + const headers: Record<string, string> = {}; + if (token) { + headers.Authorization = makeBearerTokenAuthHeader(token); + } + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + headers, + }); + switch (resp.status) { + case HttpStatusCode.Accepted: + return opSuccessFromHttp(resp, codecForAccountKycRedirects()); + case HttpStatusCode.NoContent: + return opEmptySuccess(resp); + case HttpStatusCode.NotFound: + return opEmptySuccess(resp); + case HttpStatusCode.Unauthorized: // FIXME: missing in docs + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.BadGateway: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.ServiceUnavailable: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Conflict: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } +} diff --git a/packages/taler-util/src/http-client/officer-account.ts b/packages/taler-util/src/http-client/officer-account.ts index 4b2529e20..2c1426be2 100644 --- a/packages/taler-util/src/http-client/officer-account.ts +++ b/packages/taler-util/src/http-client/officer-account.ts @@ -1,4 +1,21 @@ +/* + This file is part of GNU Taler + (C) 2022-2024 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + import { + EncryptionNonce, LockedAccount, OfficerAccount, OfficerId, @@ -10,7 +27,8 @@ import { encodeCrock, encryptWithDerivedKey, getRandomBytesF, - stringToBytes + kdf, + stringToBytes, } from "@gnu-taler/taler-util"; /** @@ -53,13 +71,19 @@ export async function unlockOfficerAccount( */ export async function createNewOfficerAccount( password: string, + extraNonce: EncryptionNonce, ): Promise<OfficerAccount & { safe: LockedAccount }> { const { eddsaPriv, eddsaPub } = createEddsaKeyPair(); const key = stringToBytes(password); + const localRnd = getRandomBytesF(24); + const mergedRnd: EncryptionNonce = extraNonce + ? kdf(24, stringToBytes("aml-officer"), extraNonce, localRnd) + : localRnd; + const protectedPrivKey = await encryptWithDerivedKey( - getRandomBytesF(24), + mergedRnd, key, eddsaPriv, password, diff --git a/packages/taler-util/src/http-client/types.ts b/packages/taler-util/src/http-client/types.ts index b0d4deca1..edddf7d94 100644 --- a/packages/taler-util/src/http-client/types.ts +++ b/packages/taler-util/src/http-client/types.ts @@ -5,6 +5,7 @@ import { buildCodecForUnion, codecForAny, codecForBoolean, + codecForConstNumber, codecForConstString, codecForEither, codecForList, @@ -12,11 +13,25 @@ import { codecForNumber, codecForString, codecOptional, + codecOptionalDefault, } from "../codec.js"; import { PaytoString, codecForPaytoString } from "../payto.js"; -import { AmountString } from "../taler-types.js"; -import { TalerActionString, codecForTalerActionString } from "../taleruri.js"; -import { codecForTimestamp } from "../time.js"; +import { + AmountString, + ExchangeWireAccount, + InternationalizedString, + codecForExchangeWireAccount, + codecForInternationalizedString, + codecForLocation, +} from "../taler-types.js"; +import { TalerUriString, codecForTalerUriString } from "../taleruri.js"; +import { + AbsoluteTime, + TalerProtocolDuration, + TalerProtocolTimestamp, + codecForDuration, + codecForTimestamp, +} from "../time.js"; export type UserAndPassword = { username: string; @@ -53,15 +68,17 @@ export type PaginationParams = { */ limit?: number; /** - * milliseconds the server should wait for at least one result to be shown - */ - timoutMs?: number; - /** * order */ - order: "asc" | "dec"; + order?: "asc" | "dec"; }; +export type LongPollParams = { + /** + * milliseconds the server should wait for at least one result to be shown + */ + timeoutMs?: number; +}; /// /// HASH /// @@ -160,19 +177,9 @@ type ImageDataUrl = string; type WadId = string; -interface Timestamp { - // Seconds since epoch, or the special - // value "never" to represent an event that will - // never happen. - t_s: number | "never"; -} +type Timestamp = TalerProtocolTimestamp; -interface RelativeTime { - // Duration in microseconds or "forever" - // to represent an infinite duration. Numeric - // values are capped at 2^53 - 1 inclusive. - d_us: number | "forever"; -} +type RelativeTime = TalerProtocolDuration; export interface LoginToken { token: AccessToken; @@ -180,10 +187,54 @@ export interface LoginToken { } declare const __ac_token: unique symbol; +/** + * Use `createAccessToken(string)` function to build one. + */ export type AccessToken = string & { [__ac_token]: true; }; +/** + * Create a rfc8959 access token. + * Adds secret-token: prefix if there is none. + * Encode the token with rfc7230 to send in a http header. + * + * @param token + * @returns + */ +export function createRFC8959AccessTokenEncoded(token: string): AccessToken { + return ( + token.startsWith("secret-token:") + ? token + : `secret-token:${encodeURIComponent(token)}` + ) as AccessToken; +} + +/** + * Create a rfc8959 access token. + * Adds secret-token: prefix if there is none. + * + * @param token + * @returns + */ +export function createRFC8959AccessTokenPlain(token: string): AccessToken { + return ( + token.startsWith("secret-token:") ? token : `secret-token:${token}` + ) as AccessToken; +} + +/** + * Convert string to access token. + * + * @param clientSecret + * @returns + */ +export function createClientSecretAccessToken( + clientSecret: string, +): AccessToken { + return clientSecret as AccessToken; +} + declare const __officer_signature: unique symbol; export type OfficerSignature = string & { [__officer_signature]: true; @@ -216,6 +267,16 @@ export namespace TalerAuthentication { // Opque access token. access_token: AccessToken; } + export interface TokenSuccessResponseMerchant { + // Expiration determined by the server. + // Can be based on the token_duration + // from the request, but ultimately the + // server decides the expiration. + expiration: Timestamp; + + // Opque access token. + token: AccessToken; + } } // DD51 https://docs.taler.net/design-documents/051-fractional-digits.html @@ -240,6 +301,7 @@ export interface CurrencySpecification { alt_unit_names: { [log10: string]: string }; } +//FIXME: implement this codec export const codecForAccessToken = codecForString as () => Codec<AccessToken>; export const codecForTokenSuccessResponse = (): Codec<TalerAuthentication.TokenSuccessResponse> => @@ -248,6 +310,13 @@ export const codecForTokenSuccessResponse = .property("expiration", codecForTimestamp) .build("TalerAuthentication.TokenSuccessResponse"); +export const codecForTokenSuccessResponseMerchant = + (): Codec<TalerAuthentication.TokenSuccessResponseMerchant> => + buildCodecForObject<TalerAuthentication.TokenSuccessResponseMerchant>() + .property("token", codecForAccessToken()) + .property("expiration", codecForTimestamp) + .build("TalerAuthentication.TokenSuccessResponseMerchant"); + export const codecForCurrencySpecificiation = (): Codec<CurrencySpecification> => buildCodecForObject<CurrencySpecification>() @@ -271,20 +340,39 @@ export const codecForCoreBankConfig = (): Codec<TalerCorebankApi.Config> => buildCodecForObject<TalerCorebankApi.Config>() .property("name", codecForConstString("libeufin-bank")) .property("version", codecForString()) + .property("bank_name", codecForString()) + .property("base_url", codecOptional(codecForString())) .property("allow_conversion", codecForBoolean()) - .property("allow_deletions", codecForBoolean()) .property("allow_registrations", codecForBoolean()) - .property("allow_edit_cashout_payto_uri", codecForBoolean()) + .property("allow_deletions", codecForBoolean()) .property("allow_edit_name", codecForBoolean()) + .property("allow_edit_cashout_payto_uri", codecForBoolean()) .property("default_debit_threshold", codecForAmountString()) - .property("currency_specification", codecForCurrencySpecificiation()) .property("currency", codecForString()) - .property("supported_tan_channels", codecForList(codecForEither( - codecForConstString(TanChannel.SMS), - codecForConstString(TanChannel.EMAIL), - ))) + .property("currency_specification", codecForCurrencySpecificiation()) + .property( + "supported_tan_channels", + codecForList( + codecForEither( + codecForConstString(TalerCorebankApi.TanChannel.SMS), + codecForConstString(TalerCorebankApi.TanChannel.EMAIL), + ), + ), + ) + .property("wire_type", codecOptionalDefault(codecForString(), "iban")) .build("TalerCorebankApi.Config"); +//FIXME: implement this codec +export const codecForURN = codecForString; + +export const codecForExchangeConfigInfo = + (): Codec<TalerMerchantApi.ExchangeConfigInfo> => + buildCodecForObject<TalerMerchantApi.ExchangeConfigInfo>() + .property("base_url", codecForString()) + .property("currency", codecForString()) + .property("master_pub", codecForString()) + .build("TalerMerchantApi.ExchangeConfigInfo"); + export const codecForMerchantConfig = (): Codec<TalerMerchantApi.VersionResponse> => buildCodecForObject<TalerMerchantApi.VersionResponse>() @@ -292,18 +380,660 @@ export const codecForMerchantConfig = .property("currency", codecForString()) .property("version", codecForString()) .property("currencies", codecForMap(codecForCurrencySpecificiation())) + .property("exchanges", codecForList(codecForExchangeConfigInfo())) .build("TalerMerchantApi.VersionResponse"); +export const codecForClaimResponse = + (): Codec<TalerMerchantApi.ClaimResponse> => + buildCodecForObject<TalerMerchantApi.ClaimResponse>() + .property("contract_terms", codecForContractTerms()) + .property("sig", codecForString()) + .build("TalerMerchantApi.ClaimResponse"); + +export const codecForPaymentResponse = + (): Codec<TalerMerchantApi.PaymentResponse> => + buildCodecForObject<TalerMerchantApi.PaymentResponse>() + .property("pos_confirmation", codecOptional(codecForString())) + .property("sig", codecForString()) + .build("TalerMerchantApi.PaymentResponse"); + +export const codecForStatusPaid = (): Codec<TalerMerchantApi.StatusPaid> => + buildCodecForObject<TalerMerchantApi.StatusPaid>() + .property("refund_amount", codecForAmountString()) + .property("refund_pending", codecForBoolean()) + .property("refund_taken", codecForAmountString()) + .property("refunded", codecForBoolean()) + .property("type", codecForConstString("paid")) + .build("TalerMerchantApi.StatusPaid"); + +export const codecForStatusGoto = + (): Codec<TalerMerchantApi.StatusGotoResponse> => + buildCodecForObject<TalerMerchantApi.StatusGotoResponse>() + .property("public_reorder_url", codecForURL()) + .property("type", codecForConstString("goto")) + .build("TalerMerchantApi.StatusGotoResponse"); + +export const codecForStatusStatusUnpaid = + (): Codec<TalerMerchantApi.StatusUnpaidResponse> => + buildCodecForObject<TalerMerchantApi.StatusUnpaidResponse>() + .property("type", codecForConstString("unpaid")) + .property("already_paid_order_id", codecOptional(codecForString())) + .property("fulfillment_url", codecOptional(codecForString())) + .property("taler_pay_uri", codecForTalerUriString()) + .build("TalerMerchantApi.PaymentResponse"); + +export const codecForPaidRefundStatusResponse = + (): Codec<TalerMerchantApi.PaidRefundStatusResponse> => + buildCodecForObject<TalerMerchantApi.PaidRefundStatusResponse>() + .property("pos_confirmation", codecOptional(codecForString())) + .property("refunded", codecForBoolean()) + .build("TalerMerchantApi.PaidRefundStatusResponse"); + +export const codecForMerchantAbortPayRefundSuccessStatus = + (): Codec<TalerMerchantApi.MerchantAbortPayRefundSuccessStatus> => + buildCodecForObject<TalerMerchantApi.MerchantAbortPayRefundSuccessStatus>() + .property("exchange_pub", codecForString()) + .property("exchange_sig", codecForString()) + .property("exchange_status", codecForConstNumber(200)) + .property("type", codecForConstString("success")) + .build("TalerMerchantApi.MerchantAbortPayRefundSuccessStatus"); + +export const codecForMerchantAbortPayRefundFailureStatus = + (): Codec<TalerMerchantApi.MerchantAbortPayRefundFailureStatus> => + buildCodecForObject<TalerMerchantApi.MerchantAbortPayRefundFailureStatus>() + .property("exchange_code", codecForNumber()) + .property("exchange_reply", codecForAny()) + .property("exchange_status", codecForNumber()) + .property("type", codecForConstString("failure")) + .build("TalerMerchantApi.MerchantAbortPayRefundFailureStatus"); + +export const codecForMerchantAbortPayRefundStatus = + (): Codec<TalerMerchantApi.MerchantAbortPayRefundStatus> => + buildCodecForUnion<TalerMerchantApi.MerchantAbortPayRefundStatus>() + .discriminateOn("type") + .alternative("success", codecForMerchantAbortPayRefundSuccessStatus()) + .alternative("failure", codecForMerchantAbortPayRefundFailureStatus()) + .build("TalerMerchantApi.MerchantAbortPayRefundStatus"); + +export const codecForAbortResponse = + (): Codec<TalerMerchantApi.AbortResponse> => + buildCodecForObject<TalerMerchantApi.AbortResponse>() + .property("refunds", codecForList(codecForMerchantAbortPayRefundStatus())) + .build("TalerMerchantApi.AbortResponse"); + +export const codecForWalletRefundResponse = + (): Codec<TalerMerchantApi.WalletRefundResponse> => + buildCodecForObject<TalerMerchantApi.WalletRefundResponse>() + .property("merchant_pub", codecForString()) + .property("refund_amount", codecForAmountString()) + .property("refunds", codecForList(codecForMerchantCoinRefundStatus())) + .build("TalerMerchantApi.AbortResponse"); + +export const codecForMerchantCoinRefundSuccessStatus = + (): Codec<TalerMerchantApi.MerchantCoinRefundSuccessStatus> => + buildCodecForObject<TalerMerchantApi.MerchantCoinRefundSuccessStatus>() + .property("type", codecForConstString("success")) + .property("coin_pub", codecForString()) + .property("exchange_status", codecForConstNumber(200)) + .property("exchange_sig", codecForString()) + .property("rtransaction_id", codecForNumber()) + .property("refund_amount", codecForAmountString()) + .property("exchange_pub", codecForString()) + .property("execution_time", codecForTimestamp) + .build("TalerMerchantApi.MerchantCoinRefundSuccessStatus"); + +export const codecForMerchantCoinRefundFailureStatus = + (): Codec<TalerMerchantApi.MerchantCoinRefundFailureStatus> => + buildCodecForObject<TalerMerchantApi.MerchantCoinRefundFailureStatus>() + .property("type", codecForConstString("failure")) + .property("coin_pub", codecForString()) + .property("exchange_status", codecForNumber()) + .property("rtransaction_id", codecForNumber()) + .property("refund_amount", codecForAmountString()) + .property("exchange_code", codecOptional(codecForNumber())) + .property("exchange_reply", codecOptional(codecForAny())) + .property("execution_time", codecForTimestamp) + .build("TalerMerchantApi.MerchantCoinRefundFailureStatus"); + +export const codecForMerchantCoinRefundStatus = + (): Codec<TalerMerchantApi.MerchantCoinRefundStatus> => + buildCodecForUnion<TalerMerchantApi.MerchantCoinRefundStatus>() + .discriminateOn("type") + .alternative("success", codecForMerchantCoinRefundSuccessStatus()) + .alternative("failure", codecForMerchantCoinRefundFailureStatus()) + .build("TalerMerchantApi.MerchantCoinRefundStatus"); + +export const codecForQueryInstancesResponse = + (): Codec<TalerMerchantApi.QueryInstancesResponse> => + buildCodecForObject<TalerMerchantApi.QueryInstancesResponse>() + .property("name", codecForString()) + .property("user_type", codecForString()) + .property("email", codecOptional(codecForString())) + .property("website", codecOptional(codecForString())) + .property("logo", codecOptional(codecForString())) + .property("merchant_pub", codecForString()) + .property("address", codecForLocation()) + .property("jurisdiction", codecForLocation()) + .property("use_stefan", codecForBoolean()) + .property("default_wire_transfer_delay", codecForDuration) + .property("default_pay_delay", codecForDuration) + .property( + "auth", + buildCodecForObject<{ + method: "external" | "token"; + }>() + .property( + "method", + codecForEither( + codecForConstString("token"), + codecForConstString("external"), + ), + ) + .build("TalerMerchantApi.QueryInstancesResponse.auth"), + ) + .build("TalerMerchantApi.QueryInstancesResponse"); + +export const codecForAccountKycRedirects = + (): Codec<TalerMerchantApi.AccountKycRedirects> => + buildCodecForObject<TalerMerchantApi.AccountKycRedirects>() + .property( + "pending_kycs", + codecForList(codecForMerchantAccountKycRedirect()), + ) + .property("timeout_kycs", codecForList(codecForExchangeKycTimeout())) + + .build("TalerMerchantApi.AccountKycRedirects"); + +export const codecForMerchantAccountKycRedirect = + (): Codec<TalerMerchantApi.MerchantAccountKycRedirect> => + buildCodecForObject<TalerMerchantApi.MerchantAccountKycRedirect>() + .property("kyc_url", codecForURL()) + .property("aml_status", codecForNumber()) + .property("exchange_url", codecForURL()) + .property("payto_uri", codecForPaytoString()) + .build("TalerMerchantApi.MerchantAccountKycRedirect"); + +export const codecForExchangeKycTimeout = + (): Codec<TalerMerchantApi.ExchangeKycTimeout> => + buildCodecForObject<TalerMerchantApi.ExchangeKycTimeout>() + .property("exchange_url", codecForURL()) + .property("exchange_code", codecForNumber()) + .property("exchange_http_status", codecForNumber()) + .build("TalerMerchantApi.ExchangeKycTimeout"); + +export const codecForAccountAddResponse = + (): Codec<TalerMerchantApi.AccountAddResponse> => + buildCodecForObject<TalerMerchantApi.AccountAddResponse>() + .property("h_wire", codecForString()) + .property("salt", codecForString()) + .build("TalerMerchantApi.AccountAddResponse"); + +export const codecForAccountsSummaryResponse = + (): Codec<TalerMerchantApi.AccountsSummaryResponse> => + buildCodecForObject<TalerMerchantApi.AccountsSummaryResponse>() + .property("accounts", codecForList(codecForBankAccountSummaryEntry())) + .build("TalerMerchantApi.AccountsSummaryResponse"); + +export const codecForBankAccountSummaryEntry = + (): Codec<TalerMerchantApi.BankAccountSummaryEntry> => + buildCodecForObject<TalerMerchantApi.BankAccountSummaryEntry>() + .property("payto_uri", codecForPaytoString()) + .property("h_wire", codecForString()) + .build("TalerMerchantApi.BankAccountSummaryEntry"); + +export const codecForBankAccountEntry = + (): Codec<TalerMerchantApi.BankAccountEntry> => + buildCodecForObject<TalerMerchantApi.BankAccountEntry>() + .property("payto_uri", codecForPaytoString()) + .property("h_wire", codecForString()) + .property("salt", codecForString()) + .property("credit_facade_url", codecOptional(codecForURL())) + .property("active", codecOptional(codecForBoolean())) + .build("TalerMerchantApi.BankAccountEntry"); + +export const codecForInventorySummaryResponse = + (): Codec<TalerMerchantApi.InventorySummaryResponse> => + buildCodecForObject<TalerMerchantApi.InventorySummaryResponse>() + .property("products", codecForList(codecForInventoryEntry())) + .build("TalerMerchantApi.InventorySummaryResponse"); + +export const codecForInventoryEntry = + (): Codec<TalerMerchantApi.InventoryEntry> => + buildCodecForObject<TalerMerchantApi.InventoryEntry>() + .property("product_id", codecForString()) + .property("product_serial", codecForNumber()) + .build("TalerMerchantApi.InventoryEntry"); + +export const codecForMerchantPosProductDetail = + (): Codec<TalerMerchantApi.MerchantPosProductDetail> => + buildCodecForObject<TalerMerchantApi.MerchantPosProductDetail>() + .property("product_serial", codecForNumber()) + .property("product_id", codecOptional(codecForString())) + .property("categories", codecForList(codecForNumber())) + .property("description", codecForString()) + .property("description_i18n", codecForInternationalizedString()) + .property("unit", codecForString()) + .property("price", codecForAmountString()) + .property("image", codecForString()) + .property("taxes", codecOptional(codecForList(codecForTax()))) + .property("total_stock", codecForNumber()) + .property("minimum_age", codecOptional(codecForNumber())) + .build("TalerMerchantApi.MerchantPosProductDetail"); + +export const codecForMerchantCategory = + (): Codec<TalerMerchantApi.MerchantCategory> => + buildCodecForObject<TalerMerchantApi.MerchantCategory>() + .property("id", codecForNumber()) + .property("name", codecForString()) + .property("name_i18n", codecForInternationalizedString()) + .build("TalerMerchantApi.MerchantCategory"); + +export const codecForFullInventoryDetailsResponse = + (): Codec<TalerMerchantApi.FullInventoryDetailsResponse> => + buildCodecForObject<TalerMerchantApi.FullInventoryDetailsResponse>() + .property("categories", codecForList(codecForMerchantCategory())) + .property("products", codecForList(codecForMerchantPosProductDetail())) + .build("TalerMerchantApi.FullInventoryDetailsResponse"); + +export const codecForProductDetail = + (): Codec<TalerMerchantApi.ProductDetail> => + buildCodecForObject<TalerMerchantApi.ProductDetail>() + .property("description", codecForString()) + .property("description_i18n", codecForInternationalizedString()) + .property("unit", codecForString()) + .property("price", codecForAmountString()) + .property("image", codecForString()) + .property("taxes", codecOptional(codecForList(codecForTax()))) + .property("address", codecOptional(codecForLocation())) + .property("next_restock", codecOptional(codecForTimestamp)) + .property("total_stock", codecForNumber()) + .property("total_sold", codecForNumber()) + .property("total_lost", codecForNumber()) + .property("minimum_age", codecOptional(codecForNumber())) + .build("TalerMerchantApi.ProductDetail"); + +export const codecForTax = (): Codec<TalerMerchantApi.Tax> => + buildCodecForObject<TalerMerchantApi.Tax>() + .property("name", codecForString()) + .property("tax", codecForAmountString()) + .build("TalerMerchantApi.Tax"); + +export const codecForPostOrderResponse = + (): Codec<TalerMerchantApi.PostOrderResponse> => + buildCodecForObject<TalerMerchantApi.PostOrderResponse>() + .property("order_id", codecForString()) + .property("token", codecOptional(codecForString())) + .build("TalerMerchantApi.PostOrderResponse"); + +export const codecForOutOfStockResponse = + (): Codec<TalerMerchantApi.OutOfStockResponse> => + buildCodecForObject<TalerMerchantApi.OutOfStockResponse>() + .property("product_id", codecForString()) + .property("available_quantity", codecForNumber()) + .property("requested_quantity", codecForNumber()) + .property("restock_expected", codecForTimestamp) + .build("TalerMerchantApi.OutOfStockResponse"); + +export const codecForOrderHistory = (): Codec<TalerMerchantApi.OrderHistory> => + buildCodecForObject<TalerMerchantApi.OrderHistory>() + .property("orders", codecForList(codecForOrderHistoryEntry())) + .build("TalerMerchantApi.OrderHistory"); + +export const codecForOrderHistoryEntry = + (): Codec<TalerMerchantApi.OrderHistoryEntry> => + buildCodecForObject<TalerMerchantApi.OrderHistoryEntry>() + .property("order_id", codecForString()) + .property("row_id", codecForNumber()) + .property("timestamp", codecForTimestamp) + .property("amount", codecForAmountString()) + .property("summary", codecForString()) + .property("refundable", codecForBoolean()) + .property("paid", codecForBoolean()) + .build("TalerMerchantApi.OrderHistoryEntry"); + +export const codecForMerchant = (): Codec<TalerMerchantApi.Merchant> => + buildCodecForObject<TalerMerchantApi.Merchant>() + .property("name", codecForString()) + .property("email", codecOptional(codecForString())) + .property("logo", codecOptional(codecForString())) + .property("website", codecOptional(codecForString())) + .property("address", codecOptional(codecForLocation())) + .property("jurisdiction", codecOptional(codecForLocation())) + .build("TalerMerchantApi.MerchantInfo"); + +export const codecForExchange = (): Codec<TalerMerchantApi.Exchange> => + buildCodecForObject<TalerMerchantApi.Exchange>() + .property("master_pub", codecForString()) + .property("priority", codecForNumber()) + .property("url", codecForString()) + .build("TalerMerchantApi.Exchange"); + +export const codecForContractTerms = + (): Codec<TalerMerchantApi.ContractTerms> => + buildCodecForObject<TalerMerchantApi.ContractTerms>() + .property("order_id", codecForString()) + .property("fulfillment_url", codecOptional(codecForString())) + .property("fulfillment_message", codecOptional(codecForString())) + .property( + "fulfillment_message_i18n", + codecOptional(codecForInternationalizedString()), + ) + .property("merchant_base_url", codecForString()) + .property("h_wire", codecForString()) + .property("auto_refund", codecOptional(codecForDuration)) + .property("wire_method", codecForString()) + .property("summary", codecForString()) + .property( + "summary_i18n", + codecOptional(codecForInternationalizedString()), + ) + .property("nonce", codecForString()) + .property("amount", codecForAmountString()) + .property("pay_deadline", codecForTimestamp) + .property("refund_deadline", codecForTimestamp) + .property("wire_transfer_deadline", codecForTimestamp) + .property("timestamp", codecForTimestamp) + .property("delivery_location", codecOptional(codecForLocation())) + .property("delivery_date", codecOptional(codecForTimestamp)) + .property("max_fee", codecForAmountString()) + .property("merchant", codecForMerchant()) + .property("merchant_pub", codecForString()) + .property("exchanges", codecForList(codecForExchange())) + .property("products", codecForList(codecForProduct())) + .property("extra", codecForAny()) + .build("TalerMerchantApi.ContractTerms"); + +export const codecForProduct = (): Codec<TalerMerchantApi.Product> => + buildCodecForObject<TalerMerchantApi.Product>() + .property("product_id", codecOptional(codecForString())) + .property("description", codecForString()) + .property( + "description_i18n", + codecOptional(codecForInternationalizedString()), + ) + .property("quantity", codecOptional(codecForNumber())) + .property("unit", codecOptional(codecForString())) + .property("price", codecOptional(codecForAmountString())) + .property("image", codecOptional(codecForString())) + .property("taxes", codecOptional(codecForList(codecForTax()))) + .property("delivery_date", codecOptional(codecForTimestamp)) + .build("TalerMerchantApi.Product"); + +export const codecForCheckPaymentPaidResponse = + (): Codec<TalerMerchantApi.CheckPaymentPaidResponse> => + buildCodecForObject<TalerMerchantApi.CheckPaymentPaidResponse>() + .property("order_status", codecForConstString("paid")) + .property("refunded", codecForBoolean()) + .property("refund_pending", codecForBoolean()) + .property("wired", codecForBoolean()) + .property("deposit_total", codecForAmountString()) + .property("exchange_code", codecForNumber()) + .property("exchange_http_status", codecForNumber()) + .property("refund_amount", codecForAmountString()) + .property("contract_terms", codecForContractTerms()) + .property("wire_reports", codecForList(codecForTransactionWireReport())) + .property("wire_details", codecForList(codecForTransactionWireTransfer())) + .property("refund_details", codecForList(codecForRefundDetails())) + .property("order_status_url", codecForURL()) + .build("TalerMerchantApi.CheckPaymentPaidResponse"); + +export const codecForCheckPaymentUnpaidResponse = + (): Codec<TalerMerchantApi.CheckPaymentUnpaidResponse> => + buildCodecForObject<TalerMerchantApi.CheckPaymentUnpaidResponse>() + .property("order_status", codecForConstString("unpaid")) + .property("taler_pay_uri", codecForTalerUriString()) + .property("creation_time", codecForTimestamp) + .property("summary", codecForString()) + .property("total_amount", codecForAmountString()) + .property("already_paid_order_id", codecOptional(codecForString())) + .property("already_paid_fulfillment_url", codecOptional(codecForString())) + .property("order_status_url", codecForString()) + .build("TalerMerchantApi.CheckPaymentPaidResponse"); + +export const codecForCheckPaymentClaimedResponse = + (): Codec<TalerMerchantApi.CheckPaymentClaimedResponse> => + buildCodecForObject<TalerMerchantApi.CheckPaymentClaimedResponse>() + .property("order_status", codecForConstString("claimed")) + .property("contract_terms", codecForContractTerms()) + .build("TalerMerchantApi.CheckPaymentClaimedResponse"); + +export const codecForMerchantOrderPrivateStatusResponse = + (): Codec<TalerMerchantApi.MerchantOrderStatusResponse> => + buildCodecForUnion<TalerMerchantApi.MerchantOrderStatusResponse>() + .discriminateOn("order_status") + .alternative("paid", codecForCheckPaymentPaidResponse()) + .alternative("unpaid", codecForCheckPaymentUnpaidResponse()) + .alternative("claimed", codecForCheckPaymentClaimedResponse()) + .build("TalerMerchantApi.MerchantOrderStatusResponse"); + +export const codecForRefundDetails = + (): Codec<TalerMerchantApi.RefundDetails> => + buildCodecForObject<TalerMerchantApi.RefundDetails>() + .property("reason", codecForString()) + .property("pending", codecForBoolean()) + .property("timestamp", codecForTimestamp) + .property("amount", codecForAmountString()) + .build("TalerMerchantApi.RefundDetails"); + +export const codecForTransactionWireTransfer = + (): Codec<TalerMerchantApi.TransactionWireTransfer> => + buildCodecForObject<TalerMerchantApi.TransactionWireTransfer>() + .property("exchange_url", codecForURL()) + .property("wtid", codecForString()) + .property("execution_time", codecForTimestamp) + .property("amount", codecForAmountString()) + .property("confirmed", codecForBoolean()) + .build("TalerMerchantApi.TransactionWireTransfer"); + +export const codecForTransactionWireReport = + (): Codec<TalerMerchantApi.TransactionWireReport> => + buildCodecForObject<TalerMerchantApi.TransactionWireReport>() + .property("code", codecForNumber()) + .property("hint", codecForString()) + .property("exchange_code", codecForNumber()) + .property("exchange_http_status", codecForNumber()) + .property("coin_pub", codecForString()) + .build("TalerMerchantApi.TransactionWireReport"); + +export const codecForMerchantRefundResponse = + (): Codec<TalerMerchantApi.MerchantRefundResponse> => + buildCodecForObject<TalerMerchantApi.MerchantRefundResponse>() + .property("taler_refund_uri", codecForTalerUriString()) + .property("h_contract", codecForString()) + .build("TalerMerchantApi.MerchantRefundResponse"); + +export const codecForTansferList = (): Codec<TalerMerchantApi.TransferList> => + buildCodecForObject<TalerMerchantApi.TransferList>() + .property("transfers", codecForList(codecForTransferDetails())) + .build("TalerMerchantApi.TransferList"); + +export const codecForTransferDetails = + (): Codec<TalerMerchantApi.TransferDetails> => + buildCodecForObject<TalerMerchantApi.TransferDetails>() + .property("credit_amount", codecForAmountString()) + .property("wtid", codecForString()) + .property("payto_uri", codecForPaytoString()) + .property("exchange_url", codecForURL()) + .property("transfer_serial_id", codecForNumber()) + .property("execution_time", codecOptional(codecForTimestamp)) + .property("verified", codecOptional(codecForBoolean())) + .property("confirmed", codecOptional(codecForBoolean())) + .build("TalerMerchantApi.TransferDetails"); + +export const codecForOtpDeviceSummaryResponse = + (): Codec<TalerMerchantApi.OtpDeviceSummaryResponse> => + buildCodecForObject<TalerMerchantApi.OtpDeviceSummaryResponse>() + .property("otp_devices", codecForList(codecForOtpDeviceEntry())) + .build("TalerMerchantApi.OtpDeviceSummaryResponse"); + +export const codecForOtpDeviceEntry = + (): Codec<TalerMerchantApi.OtpDeviceEntry> => + buildCodecForObject<TalerMerchantApi.OtpDeviceEntry>() + .property("otp_device_id", codecForString()) + .property("device_description", codecForString()) + .build("TalerMerchantApi.OtpDeviceEntry"); + +export const codecForOtpDeviceDetails = + (): Codec<TalerMerchantApi.OtpDeviceDetails> => + buildCodecForObject<TalerMerchantApi.OtpDeviceDetails>() + .property("device_description", codecForString()) + .property("otp_algorithm", codecForNumber()) + .property("otp_ctr", codecOptional(codecForNumber())) + .property("otp_timestamp", codecForNumber()) + .property("otp_code", codecOptional(codecForString())) + .build("TalerMerchantApi.OtpDeviceDetails"); + +export const codecForTemplateSummaryResponse = + (): Codec<TalerMerchantApi.TemplateSummaryResponse> => + buildCodecForObject<TalerMerchantApi.TemplateSummaryResponse>() + .property("templates", codecForList(codecForTemplateEntry())) + .build("TalerMerchantApi.TemplateSummaryResponse"); + +export const codecForTemplateEntry = + (): Codec<TalerMerchantApi.TemplateEntry> => + buildCodecForObject<TalerMerchantApi.TemplateEntry>() + .property("template_id", codecForString()) + .property("template_description", codecForString()) + .build("TalerMerchantApi.TemplateEntry"); + +export const codecForTemplateDetails = + (): Codec<TalerMerchantApi.TemplateDetails> => + buildCodecForObject<TalerMerchantApi.TemplateDetails>() + .property("template_description", codecForString()) + .property("otp_id", codecOptional(codecForString())) + .property("template_contract", codecForTemplateContractDetails()) + .property("required_currency", codecOptional(codecForString())) + .property( + "editable_defaults", + codecOptional(codecForTemplateContractDetailsDefaults()), + ) + .build("TalerMerchantApi.TemplateDetails"); + +export const codecForTemplateContractDetails = + (): Codec<TalerMerchantApi.TemplateContractDetails> => + buildCodecForObject<TalerMerchantApi.TemplateContractDetails>() + .property("summary", codecOptional(codecForString())) + .property("currency", codecOptional(codecForString())) + .property("amount", codecOptional(codecForAmountString())) + .property("minimum_age", codecForNumber()) + .property("pay_duration", codecForDuration) + .build("TalerMerchantApi.TemplateContractDetails"); + +export const codecForTemplateContractDetailsDefaults = + (): Codec<TalerMerchantApi.TemplateContractDetailsDefaults> => + buildCodecForObject<TalerMerchantApi.TemplateContractDetailsDefaults>() + .property("summary", codecOptional(codecForString())) + .property("currency", codecOptional(codecForString())) + .property("amount", codecOptional(codecForAmountString())) + .build("TalerMerchantApi.TemplateContractDetailsDefaults"); + +export const codecForWalletTemplateDetails = + (): Codec<TalerMerchantApi.WalletTemplateDetails> => + buildCodecForObject<TalerMerchantApi.WalletTemplateDetails>() + .property("template_contract", codecForTemplateContractDetails()) + .property("required_currency", codecOptional(codecForString())) + .property( + "editable_defaults", + codecOptional(codecForTemplateContractDetailsDefaults()), + ) + .build("TalerMerchantApi.WalletTemplateDetails"); + +export const codecForWebhookSummaryResponse = + (): Codec<TalerMerchantApi.WebhookSummaryResponse> => + buildCodecForObject<TalerMerchantApi.WebhookSummaryResponse>() + .property("webhooks", codecForList(codecForWebhookEntry())) + .build("TalerMerchantApi.WebhookSummaryResponse"); + +export const codecForWebhookEntry = (): Codec<TalerMerchantApi.WebhookEntry> => + buildCodecForObject<TalerMerchantApi.WebhookEntry>() + .property("webhook_id", codecForString()) + .property("event_type", codecForString()) + .build("TalerMerchantApi.WebhookEntry"); + +export const codecForWebhookDetails = + (): Codec<TalerMerchantApi.WebhookDetails> => + buildCodecForObject<TalerMerchantApi.WebhookDetails>() + .property("event_type", codecForString()) + .property("url", codecForString()) + .property("http_method", codecForString()) + .property("header_template", codecOptional(codecForString())) + .property("body_template", codecOptional(codecForString())) + .build("TalerMerchantApi.WebhookDetails"); + +export const codecForTokenFamilyKind = + (): Codec<TalerMerchantApi.TokenFamilyKind> => + codecForEither( + codecForConstString("discount"), + codecForConstString("subscription"), + ) as any; //FIXME: create a codecForEnum +export const codecForTokenFamilyDetails = + (): Codec<TalerMerchantApi.TokenFamilyDetails> => + buildCodecForObject<TalerMerchantApi.TokenFamilyDetails>() + .property("slug", codecForString()) + .property("name", codecForString()) + .property("description", codecForString()) + .property("description_i18n", codecForInternationalizedString()) + .property("valid_after", codecForTimestamp) + .property("valid_before", codecForTimestamp) + .property("duration", codecForDuration) + .property("kind", codecForTokenFamilyKind()) + .property("issued", codecForNumber()) + .property("redeemed", codecForNumber()) + .build("TalerMerchantApi.TokenFamilyDetails"); + +export const codecForTokenFamiliesList = + (): Codec<TalerMerchantApi.TokenFamiliesList> => + buildCodecForObject<TalerMerchantApi.TokenFamiliesList>() + .property("token_families", codecForList(codecForTokenFamilySummary())) + .build("TalerMerchantApi.TokenFamiliesList"); + +export const codecForTokenFamilySummary = + (): Codec<TalerMerchantApi.TokenFamilySummary> => + buildCodecForObject<TalerMerchantApi.TokenFamilySummary>() + .property("slug", codecForString()) + .property("name", codecForString()) + .property("valid_after", codecForTimestamp) + .property("valid_before", codecForTimestamp) + .property("kind", codecForTokenFamilyKind()) + .build("TalerMerchantApi.TokenFamilySummary"); + +export const codecForInstancesResponse = + (): Codec<TalerMerchantApi.InstancesResponse> => + buildCodecForObject<TalerMerchantApi.InstancesResponse>() + .property("instances", codecForList(codecForInstance())) + .build("TalerMerchantApi.InstancesResponse"); + +export const codecForInstance = (): Codec<TalerMerchantApi.Instance> => + buildCodecForObject<TalerMerchantApi.Instance>() + .property("name", codecForString()) + .property("user_type", codecForString()) + .property("website", codecOptional(codecForString())) + .property("logo", codecOptional(codecForString())) + .property("id", codecForString()) + .property("merchant_pub", codecForString()) + .property("payment_targets", codecForList(codecForString())) + .property("deleted", codecForBoolean()) + .build("TalerMerchantApi.Instance"); + export const codecForExchangeConfig = (): Codec<TalerExchangeApi.ExchangeVersionResponse> => buildCodecForObject<TalerExchangeApi.ExchangeVersionResponse>() .property("version", codecForString()) .property("name", codecForConstString("taler-exchange")) + .property("implementation", codecOptional(codecForURN())) .property("currency", codecForString()) .property("currency_specification", codecForCurrencySpecificiation()) .property("supported_kyc_requirements", codecForList(codecForString())) .build("TalerExchangeApi.ExchangeVersionResponse"); +export const codecForExchangeKeys = + (): Codec<TalerExchangeApi.ExchangeKeysResponse> => + buildCodecForObject<TalerExchangeApi.ExchangeKeysResponse>() + .property("version", codecForString()) + .property("base_url", codecForString()) + .property("currency", codecForString()) + .build("TalerExchangeApi.ExchangeKeysResponse"); + const codecForBalance = (): Codec<TalerCorebankApi.Balance> => buildCodecForObject<TalerCorebankApi.Balance>() .property("amount", codecForAmountString()) @@ -322,6 +1052,7 @@ const codecForPublicAccount = (): Codec<TalerCorebankApi.PublicAccount> => .property("balance", codecForBalance()) .property("payto_uri", codecForPaytoString()) .property("is_taler_exchange", codecForBoolean()) + .property("row_id", codecOptional(codecForNumber())) .build("TalerCorebankApi.PublicAccount"); export const codecForPublicAccountsResponse = @@ -333,12 +1064,24 @@ export const codecForPublicAccountsResponse = export const codecForAccountMinimalData = (): Codec<TalerCorebankApi.AccountMinimalData> => buildCodecForObject<TalerCorebankApi.AccountMinimalData>() + .property("username", codecForString()) + .property("name", codecForString()) + .property("payto_uri", codecForPaytoString()) .property("balance", codecForBalance()) + .property("row_id", codecForNumber()) .property("debit_threshold", codecForAmountString()) - .property("name", codecForString()) - .property("username", codecForString()) + .property("min_cashout", codecOptional(codecForAmountString())) .property("is_public", codecForBoolean()) .property("is_taler_exchange", codecForBoolean()) + .property( + "status", + codecOptional( + codecForEither( + codecForConstString("active"), + codecForConstString("deleted"), + ), + ), + ) .build("TalerCorebankApi.AccountMinimalData"); export const codecForListBankAccountsResponse = @@ -353,10 +1096,29 @@ export const codecForAccountData = (): Codec<TalerCorebankApi.AccountData> => .property("balance", codecForBalance()) .property("payto_uri", codecForPaytoString()) .property("debit_threshold", codecForAmountString()) + .property("min_cashout", codecOptional(codecForAmountString())) .property("contact_data", codecOptional(codecForChallengeContactData())) .property("cashout_payto_uri", codecOptional(codecForPaytoString())) .property("is_public", codecForBoolean()) .property("is_taler_exchange", codecForBoolean()) + .property( + "tan_channel", + codecOptional( + codecForEither( + codecForConstString(TalerCorebankApi.TanChannel.SMS), + codecForConstString(TalerCorebankApi.TanChannel.EMAIL), + ), + ), + ) + .property( + "status", + codecOptional( + codecForEither( + codecForConstString("active"), + codecForConstString("deleted"), + ), + ), + ) .build("TalerCorebankApi.AccountData"); export const codecForChallengeContactData = @@ -369,13 +1131,6 @@ export const codecForChallengeContactData = export const codecForWithdrawalPublicInfo = (): Codec<TalerCorebankApi.WithdrawalPublicInfo> => buildCodecForObject<TalerCorebankApi.WithdrawalPublicInfo>() - .property("username", codecForString()) - .property("amount", codecForAmountString()) - .property( - "selected_exchange_account", - codecOptional(codecForPaytoString()), - ) - .property("selected_reserve_pub", codecOptional(codecForString())) .property( "status", codecForEither( @@ -385,6 +1140,13 @@ export const codecForWithdrawalPublicInfo = codecForConstString("confirmed"), ), ) + .property("amount", codecForAmountString()) + .property("username", codecForString()) + .property("selected_reserve_pub", codecOptional(codecForString())) + .property( + "selected_exchange_account", + codecOptional(codecForPaytoString()), + ) .build("TalerCorebankApi.WithdrawalPublicInfo"); export const codecForBankAccountTransactionsResponse = @@ -429,13 +1191,13 @@ export const codecForRegisterAccountResponse = export const codecForBankAccountCreateWithdrawalResponse = (): Codec<TalerCorebankApi.BankAccountCreateWithdrawalResponse> => buildCodecForObject<TalerCorebankApi.BankAccountCreateWithdrawalResponse>() - .property("taler_withdraw_uri", codecForTalerActionString()) + .property("taler_withdraw_uri", codecForTalerUriString()) .property("withdrawal_id", codecForString()) .build("TalerCorebankApi.BankAccountCreateWithdrawalResponse"); export const codecForCashoutPending = - (): Codec<TalerCorebankApi.CashoutPending> => - buildCodecForObject<TalerCorebankApi.CashoutPending>() + (): Codec<TalerCorebankApi.CashoutResponse> => + buildCodecForObject<TalerCorebankApi.CashoutResponse>() .property("cashout_id", codecForNumber()) .build("TalerCorebankApi.CashoutPending"); @@ -461,14 +1223,6 @@ export const codecForCashouts = (): Codec<TalerCorebankApi.Cashouts> => export const codecForCashoutInfo = (): Codec<TalerCorebankApi.CashoutInfo> => buildCodecForObject<TalerCorebankApi.CashoutInfo>() .property("cashout_id", codecForNumber()) - .property( - "status", - codecForEither( - codecForConstString("pending"), - codecForConstString("aborted"), - codecForConstString("confirmed"), - ), - ) .build("TalerCorebankApi.CashoutInfo"); export const codecForGlobalCashouts = @@ -482,41 +1236,15 @@ export const codecForGlobalCashoutInfo = buildCodecForObject<TalerCorebankApi.GlobalCashoutInfo>() .property("cashout_id", codecForNumber()) .property("username", codecForString()) - .property( - "status", - codecForEither( - codecForConstString("pending"), - codecForConstString("aborted"), - codecForConstString("confirmed"), - ), - ) .build("TalerCorebankApi.GlobalCashoutInfo"); export const codecForCashoutStatusResponse = (): Codec<TalerCorebankApi.CashoutStatusResponse> => buildCodecForObject<TalerCorebankApi.CashoutStatusResponse>() - .property("amount_credit", codecForAmountString()) .property("amount_debit", codecForAmountString()) - .property("confirmation_time", codecOptional(codecForTimestamp)) - .property("creation_time", codecForTimestamp) - // .property("credit_payto_uri", codecForPaytoString()) - .property( - "status", - codecForEither( - codecForConstString("pending"), - codecForConstString("aborted"), - codecForConstString("confirmed"), - ), - ) - .property( - "tan_channel", - codecForEither( - codecForConstString(TanChannel.SMS), - codecForConstString(TanChannel.EMAIL), - ), - ) + .property("amount_credit", codecForAmountString()) .property("subject", codecForString()) - .property("tan_info", codecForString()) + .property("creation_time", codecForTimestamp) .build("TalerCorebankApi.CashoutStatusResponse"); export const codecForConversionRatesResponse = @@ -598,7 +1326,6 @@ export const codecForBankWithdrawalOperationPostResponse = .property( "status", codecForEither( - codecForConstString("pending"), codecForConstString("selected"), codecForConstString("aborted"), codecForConstString("confirmed"), @@ -607,26 +1334,33 @@ export const codecForBankWithdrawalOperationPostResponse = .property("confirm_transfer_url", codecOptional(codecForURL())) .build("TalerBankIntegrationApi.BankWithdrawalOperationPostResponse"); -export const codecForMerchantIncomingHistory = - (): Codec<TalerRevenueApi.MerchantIncomingHistory> => - buildCodecForObject<TalerRevenueApi.MerchantIncomingHistory>() +export const codecForRevenueConfig = (): Codec<TalerRevenueApi.RevenueConfig> => + buildCodecForObject<TalerRevenueApi.RevenueConfig>() + .property("name", codecForConstString("taler-revenue")) + .property("version", codecForString()) + .property("currency", codecForString()) + .property("implementation", codecOptional(codecForString())) + .build("TalerRevenueApi.RevenueConfig"); + +export const codecForRevenueIncomingHistory = + (): Codec<TalerRevenueApi.RevenueIncomingHistory> => + buildCodecForObject<TalerRevenueApi.RevenueIncomingHistory>() .property("credit_account", codecForPaytoString()) .property( "incoming_transactions", - codecForList(codecForMerchantIncomingBankTransaction()), + codecForList(codecForRevenueIncomingBankTransaction()), ) .build("TalerRevenueApi.MerchantIncomingHistory"); -export const codecForMerchantIncomingBankTransaction = - (): Codec<TalerRevenueApi.MerchantIncomingBankTransaction> => - buildCodecForObject<TalerRevenueApi.MerchantIncomingBankTransaction>() - .property("row_id", codecForNumber()) - .property("date", codecForTimestamp) +export const codecForRevenueIncomingBankTransaction = + (): Codec<TalerRevenueApi.RevenueIncomingBankTransaction> => + buildCodecForObject<TalerRevenueApi.RevenueIncomingBankTransaction>() .property("amount", codecForAmountString()) + .property("date", codecForTimestamp) .property("debit_account", codecForPaytoString()) - .property("exchange_url", codecForURL()) - .property("wtid", codecForString()) - .build("TalerRevenueApi.MerchantIncomingBankTransaction"); + .property("row_id", codecForNumber()) + .property("subject", codecForString()) + .build("TalerRevenueApi.RevenueIncomingBankTransaction"); export const codecForTransferResponse = (): Codec<TalerWireGatewayApi.TransferResponse> => @@ -708,7 +1442,7 @@ export const codecForAddIncomingResponse = export const codecForAmlRecords = (): Codec<TalerExchangeApi.AmlRecords> => buildCodecForObject<TalerExchangeApi.AmlRecords>() .property("records", codecForList(codecForAmlRecord())) - .build("TalerExchangeApi.PublicAccountsResponse"); + .build("TalerExchangeApi.AmlRecords"); export const codecForAmlRecord = (): Codec<TalerExchangeApi.AmlRecord> => buildCodecForObject<TalerExchangeApi.AmlRecord>() @@ -735,6 +1469,24 @@ export const codecForAmlDecisionDetail = .property("decider_pub", codecForString()) .build("TalerExchangeApi.AmlDecisionDetail"); +export const codecForChallenge = (): Codec<TalerCorebankApi.Challenge> => + buildCodecForObject<TalerCorebankApi.Challenge>() + .property("challenge_id", codecForNumber()) + .build("TalerCorebankApi.Challenge"); + +export const codecForTanTransmission = + (): Codec<TalerCorebankApi.TanTransmission> => + buildCodecForObject<TalerCorebankApi.TanTransmission>() + .property( + "tan_channel", + codecForEither( + codecForConstString(TalerCorebankApi.TanChannel.SMS), + codecForConstString(TalerCorebankApi.TanChannel.EMAIL), + ), + ) + .property("tan_info", codecForString()) + .build("TalerCorebankApi.TanTransmission"); + interface KycDetail { provider_section: string; attributes?: Object; @@ -760,51 +1512,6 @@ export const codecForAmlDecision = (): Codec<TalerExchangeApi.AmlDecision> => .property("kyc_requirements", codecOptional(codecForList(codecForString()))) .build("TalerExchangeApi.AmlDecision"); -// version: string; - -// // Name of the API. -// name: "taler-conversion-info"; - -// // Currency used by this bank. -// regional_currency: string; - -// // How the bank SPA should render this currency. -// regional_currency_specification: CurrencySpecification; - -// // External currency used during conversion. -// fiat_currency: string; - -// // How the bank SPA should render this currency. -// fiat_currency_specification: CurrencySpecification; - -// Extra conversion rate information. -// // Only present if server opts in to report the static conversion rate. -// conversion_info?: { - -// // Fee to subtract after applying the cashin ratio. -// cashin_fee: AmountString; - -// // Fee to subtract after applying the cashout ratio. -// cashout_fee: AmountString; - -// // Minimum amount authorised for cashin, in fiat before conversion -// cashin_min_amount: AmountString; - -// // Minimum amount authorised for cashout, in regional before conversion -// cashout_min_amount: AmountString; - -// // Smallest possible regional amount, converted amount is rounded to this amount -// cashin_tiny_amount: AmountString; - -// // Smallest possible fiat amount, converted amount is rounded to this amount -// cashout_tiny_amount: AmountString; - -// // Rounding mode used during cashin conversion -// cashin_rounding_mode: "zero" | "up" | "nearest"; - -// // Rounding mode used during cashout conversion -// cashout_rounding_mode: "zero" | "up" | "nearest"; -// } export const codecForConversionInfo = (): Codec<TalerBankConversionApi.ConversionInfo> => buildCodecForObject<TalerBankConversionApi.ConversionInfo>() @@ -847,14 +1554,83 @@ export const codecForConversionBankConfig = .property("fiat_currency", codecForString()) .property("fiat_currency_specification", codecForCurrencySpecificiation()) - .property("conversion_rate", codecOptional(codecForConversionInfo())) + .property("conversion_rate", codecForConversionInfo()) .build("ConversionBankConfig.IntegrationConfig"); -// export const codecFor = -// (): Codec<TalerWireGatewayApi.PublicAccountsResponse> => -// buildCodecForObject<TalerWireGatewayApi.PublicAccountsResponse>() -// .property("", codecForString()) -// .build("TalerWireGatewayApi.PublicAccountsResponse"); +export const codecForChallengerTermsOfServiceResponse = + (): Codec<ChallengerApi.ChallengerTermsOfServiceResponse> => + buildCodecForObject<ChallengerApi.ChallengerTermsOfServiceResponse>() + .property("name", codecForConstString("challenger")) + .property("version", codecForString()) + .property("implementation", codecOptional(codecForString())) + .build("ChallengerApi.ChallengerTermsOfServiceResponse"); + +export const codecForChallengeSetupResponse = + (): Codec<ChallengerApi.ChallengeSetupResponse> => + buildCodecForObject<ChallengerApi.ChallengeSetupResponse>() + .property("nonce", codecForString()) + .build("ChallengerApi.ChallengeSetupResponse"); + +export const codecForChallengeStatus = + (): Codec<ChallengerApi.ChallengeStatus> => + buildCodecForObject<ChallengerApi.ChallengeStatus>() + .property("restrictions", codecOptional(codecForMap(codecForAny()))) + .property("fix_address", codecForBoolean()) + .property("last_address", codecOptional(codecForMap(codecForAny()))) + .property("changes_left", codecForNumber()) + .build("ChallengerApi.ChallengeStatus"); +export const codecForChallengeCreateResponse = + (): Codec<ChallengerApi.ChallengeCreateResponse> => + buildCodecForObject<ChallengerApi.ChallengeCreateResponse>() + .property("attempts_left", codecForNumber()) + .property("address", codecForAny()) + .property("transmitted", codecForBoolean()) + .property("next_tx_time", codecForString()) + .build("ChallengerApi.ChallengeCreateResponse"); + +export const codecForInvalidPinResponse = + (): Codec<ChallengerApi.InvalidPinResponse> => + buildCodecForObject<ChallengerApi.InvalidPinResponse>() + .property("ec", codecOptional(codecForNumber())) + .property("hint", codecForAny()) + .property("addresses_left", codecForNumber()) + .property("pin_transmissions_left", codecForNumber()) + .property("auth_attempts_left", codecForNumber()) + .property("exhausted", codecForBoolean()) + .property("no_challenge", codecForBoolean()) + .build("ChallengerApi.InvalidPinResponse"); + +export const codecForChallengerAuthResponse = + (): Codec<ChallengerApi.ChallengerAuthResponse> => + buildCodecForObject<ChallengerApi.ChallengerAuthResponse>() + .property("access_token", codecForString()) + .property("token_type", codecForAny()) + .property("expires_in", codecForNumber()) + .build("ChallengerApi.ChallengerAuthResponse"); + +export const codecForChallengerInfoResponse = + (): Codec<ChallengerApi.ChallengerInfoResponse> => + buildCodecForObject<ChallengerApi.ChallengerInfoResponse>() + .property("id", codecForNumber()) + .property("address", codecForAny()) + .property("address_type", codecForString()) + .property("expires", codecForTimestamp) + .build("ChallengerApi.ChallengerInfoResponse"); + +export const codecForTemplateEditableDetails = + (): Codec<TalerMerchantApi.TemplateEditableDetails> => + buildCodecForObject<TalerMerchantApi.TemplateEditableDetails>() + .property("summary", codecOptional(codecForString())) + .property("currency", codecOptional(codecForString())) + .property("amount", codecOptional(codecForAmountString())) + .build("TemplateEditableDetails"); + +export const codecForMerchantReserveCreateConfirmation = + (): Codec<TalerMerchantApi.MerchantReserveCreateConfirmation> => + buildCodecForObject<TalerMerchantApi.MerchantReserveCreateConfirmation>() + .property("accounts", codecForList(codecForExchangeWireAccount())) + .property("reserve_pub", codecForString()) + .build("MerchantReserveCreateConfirmation"); type EmailAddress = string; type PhoneNumber = string; @@ -865,6 +1641,8 @@ type Base32 = string; type DecimalNumber = string; type RsaSignature = string; +type Float = number; +type LibtoolVersion = string; // The type of a coin's blinded envelope depends on the cipher that is used // for signing with a denomination key. type CoinEnvelope = RSACoinEnvelope | CSCoinEnvelope; @@ -889,15 +1667,15 @@ interface CSCoinEnvelope { // a 256-bit nonce, converted to Crockford Base32. type DenominationBlindingKeyP = string; +//FIXME: implement this codec const codecForURL = codecForString; +//FIXME: implement this codec const codecForLibtoolVersion = codecForString; +//FIXME: implement this codec const codecForCurrencyName = codecForString; +//FIXME: implement this codec const codecForDecimalNumber = codecForString; -enum TanChannel { - SMS = "sms", - EMAIL = "email", -} export type WithdrawalOperationStatus = | "pending" | "selected" @@ -1068,18 +1846,34 @@ export namespace TalerWireGatewayApi { } export namespace TalerRevenueApi { - export interface MerchantIncomingHistory { + export interface RevenueConfig { + // Name of the API. + name: "taler-revenue"; + + // libtool-style representation of the Bank protocol version, see + // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning + // The format is "current:revision:age". + version: string; + + // Currency used by this gateway. + currency: string; + + // URN of the implementation (needed to interpret 'revision' in version). + // @since v0, may become mandatory in the future. + implementation?: string; + } + + export interface RevenueIncomingHistory { // Array of incoming transactions. - incoming_transactions: MerchantIncomingBankTransaction[]; + incoming_transactions: RevenueIncomingBankTransaction[]; // Payto URI to identify the receiver of funds. - // This must be one of the merchant's bank accounts. // Credit account is shared by all incoming transactions // as per the nature of the request. - credit_account: PaytoString; + credit_account: string; } - export interface MerchantIncomingBankTransaction { + export interface RevenueIncomingBankTransaction { // Opaque identifier of the returned record. row_id: SafeUint64; @@ -1090,13 +1884,10 @@ export namespace TalerRevenueApi { amount: AmountString; // Payto URI to identify the sender of funds. - debit_account: PaytoString; - - // Base URL of the exchange where the transfer originated form. - exchange_url: string; + debit_account: string; - // The wire transfer identifier. - wtid: WireTransferIdentifierRawP; + // The wire transfer subject. + subject: string; } } @@ -1156,7 +1947,7 @@ export namespace TalerBankConversionApi { // Extra conversion rate information. // Only present if server opts in to report the static conversion rate. - conversion_rate?: ConversionInfo; + conversion_rate: ConversionInfo; } export interface CashinConversionResponse { @@ -1211,6 +2002,7 @@ export namespace TalerBankConversionApi { cashout_rounding_mode: RoundingMode; } } + export namespace TalerBankIntegrationApi { export interface BankVersion { // libtool-style representation of the Bank protocol version, see @@ -1288,6 +2080,7 @@ export namespace TalerBankIntegrationApi { confirm_transfer_url?: string; } } + export namespace TalerCorebankApi { export interface IntegrationConfig { // libtool-style representation of the Bank protocol version, see @@ -1311,6 +2104,16 @@ export namespace TalerCorebankApi { // API version in the form $n:$n:$n version: string; + // Bank display name to be used in user interfaces. + // For consistency use "Taler Bank" if missing. + // @since v4, will become mandatory in the next version. + bank_name: string; + + // Advertised base URL to use when you sharing an URL with another + // program. + // @since v4. + base_url?: string; + // If 'true' the server provides local currency conversion support // If 'false' some parts of the API are not supported and return 501 allow_conversion: boolean; @@ -1342,6 +2145,11 @@ export namespace TalerCorebankApi { // TAN channels supported by the server supported_tan_channels: TanChannel[]; + + // Wire transfer type supported by the bank. + // Default to 'iban' is missing + // @since v4, may become mandatory in the future. + wire_type: string; } export interface BankAccountCreateWithdrawalRequest { @@ -1353,7 +2161,7 @@ export namespace TalerCorebankApi { withdrawal_id: string; // URI that can be passed to the wallet to initiate the withdrawal. - taler_withdraw_uri: TalerActionString; + taler_withdraw_uri: TalerUriString; } export interface WithdrawalPublicInfo { // Current status of the operation @@ -1408,6 +2216,12 @@ export namespace TalerCorebankApi { // query string parameter of the 'payto' field. In case it // is given in both places, the paytoUri's takes the precedence. amount?: AmountString; + + // Nonce to make the request idempotent. Requests with the same + // request_uid that differ in any of the other fields + // are rejected. + // @since v4, will become mandatory in the next version. + request_uid?: ShortHashCode; } export interface CreateTransactionResponse { @@ -1442,10 +2256,6 @@ export namespace TalerCorebankApi { is_taler_exchange?: boolean; // Addresses where to send the TAN for transactions. - // Currently only used for cashouts. - // If missing, cashouts will fail. - // In the future, might be used for other transactions - // as well. contact_data?: ChallengeContactData; // 'payto' address of a fiat bank account. @@ -1459,8 +2269,18 @@ export namespace TalerCorebankApi { payto_uri?: PaytoString; // If present, set the max debit allowed for this user - // Only admin can change this property. + // Only admin can set this property. debit_threshold?: AmountString; + + // If present, set a custom minimum cashout amount for this account. + // Only admin can set this property + // @since v4 + min_cashout?: AmountString; + + // If present, enables 2FA and set the TAN channel used for challenges + // Only admin can set this property, other user can reconfig their account + // after creation. + tan_channel?: TanChannel; } export interface ChallengeContactData { @@ -1497,12 +2317,20 @@ export namespace TalerCorebankApi { // If present, change the max debit allowed for this user // Only admin can change this property. debit_threshold?: AmountString; + + // If present, change the custom minimum cashout amount for this account. + // Only admin can set this property + // @since v4 + min_cashout?: AmountString; + + // If present, enables 2FA and set the TAN channel used for challenges + tan_channel?: TanChannel | null; } export interface AccountPasswordChange { // New password. new_password: string; - // Old password. If present, chec that the old password matches. + // Old password. If present, check that the old password matches. // Optional for admin account. old_password?: string; } @@ -1522,6 +2350,10 @@ export namespace TalerCorebankApi { // Is this a taler exchange account? is_taler_exchange: boolean; + + // Opaque unique ID used for pagination. + // @since v4, will become mandatory in the future. + row_id?: Integer; } export interface ListBankAccountsResponse { @@ -1538,17 +2370,37 @@ export namespace TalerCorebankApi { // Legal name of the account owner. name: string; + // Internal payto URI of this bank account. + payto_uri: PaytoString; + // current balance of the account balance: Balance; // Number indicating the max debit allowed for the requesting user. debit_threshold: AmountString; + // Custom minimum cashout amount for this account. + // If null or absent, the global conversion fee is used. + // @since v4 + min_cashout?: AmountString; + // Is this account visible to anyone? is_public: boolean; // Is this a taler exchange account? is_taler_exchange: boolean; + + // Opaque unique ID used for pagination. + // @since v4, will become mandatory in the future. + row_id?: Integer; + + // Current status of the account + // active: the account can be used + // deleted: the account has been deleted but is retained for compliance + // reasons, only the administrator can access it + // Default to 'active' is missing + // @since v4, will become mandatory in the next version. + status?: "active" | "deleted"; } export interface AccountData { @@ -1564,6 +2416,11 @@ export namespace TalerCorebankApi { // Number indicating the max debit allowed for the requesting user. debit_threshold: AmountString; + // Custom minimum cashout amount for this account. + // If null or absent, the global conversion fee is used. + // @since v4 + min_cashout?: AmountString; + contact_data?: ChallengeContactData; // 'payto' address pointing the bank account @@ -1579,6 +2436,17 @@ export namespace TalerCorebankApi { // Is this a taler exchange account? is_taler_exchange: boolean; + + // Is 2FA enabled and what channel is used for challenges? + tan_channel?: TanChannel; + + // Current status of the account + // active: the account can be used + // deleted: the account has been deleted but is retained for compliance + // reasons, only the administrator can access it + // Default to 'active' is missing + // @since v4, will become mandatory in the next version. + status?: "active" | "deleted"; } export interface CashoutRequest { @@ -1608,20 +2476,16 @@ export namespace TalerCorebankApi { // correctly based on the amount_debit and the cashout rate, // otherwise the request will fail. amount_credit: AmountString; - - // Which channel the TAN should be sent to. If - // this field is missing, it defaults to SMS. - // The default choice prefers to change the communication - // channel respect to the one used to issue this request. - tan_channel?: TanChannel; } - export interface CashoutPending { + export interface CashoutResponse { // ID identifying the operation being created - // and now waiting for the TAN confirmation. cashout_id: number; } + /** + * @deprecated since 4, use 2fa + */ export interface CashoutConfirmRequest { // the TAN that confirms $CASHOUT_ID. tan: string; @@ -1634,7 +2498,10 @@ export namespace TalerCorebankApi { export interface CashoutInfo { cashout_id: number; - status: "pending" | "aborted" | "confirmed"; + /** + * @deprecated since 4, use new 2fa + */ + status?: "pending" | "aborted" | "confirmed"; } export interface GlobalCashouts { // Every string represents a cash-out operation ID. @@ -1643,12 +2510,9 @@ export namespace TalerCorebankApi { export interface GlobalCashoutInfo { cashout_id: number; username: string; - status: "pending" | "aborted" | "confirmed"; } export interface CashoutStatusResponse { - status: "pending" | "aborted" | "confirmed"; - // Amount debited to the internal // regional currency bank account. amount_debit: AmountString; @@ -1659,24 +2523,8 @@ export namespace TalerCorebankApi { // Transaction subject. subject: string; - // Fiat bank account that will receive the cashed out amount. - // Specified as a payto URI. - // credit_payto_uri: PaytoString; - // Time when the cashout was created. creation_time: Timestamp; - - // Time when the cashout was confirmed via its TAN. - // Missing when the operation wasn't confirmed yet. - confirmation_time?: Timestamp; - - // Channel of the last successful transmission of the TAN challenge. - // Missing when all transmissions failed. - tan_channel?: TanChannel; - - // Info of the last successful transmission of the TAN challenge. - // Missing when all transmissions failed. - tan_info?: string; } export interface ConversionRatesResponse { @@ -1767,6 +2615,29 @@ export namespace TalerCorebankApi { // exchange to another bank account. talerOutVolume: AmountString; } + export interface TanTransmission { + // Channel of the last successful transmission of the TAN challenge. + tan_channel: TanChannel; + + // Info of the last successful transmission of the TAN challenge. + tan_info: string; + } + + export interface Challenge { + // Unique identifier of the challenge to solve to run this protected + // operation. + challenge_id: number; + } + + export interface ChallengeSolve { + // The TAN code that solves $CHALLENGE_ID + tan: string; + } + + export enum TanChannel { + SMS = "sms", + EMAIL = "email", + } } export namespace TalerExchangeApi { @@ -1875,7 +2746,12 @@ export namespace TalerExchangeApi { // Name of the protocol. name: "taler-exchange"; - // Currency supported by this exchange. + // URN of the implementation (needed to interpret 'revision' in version). + // @since v18, may become mandatory in the future. + implementation?: string; + + // Currency supported by this exchange, given + // as a currency code ("USD" or "EUR"). currency: string; // How wallets should render this currency. @@ -1942,6 +2818,404 @@ export namespace TalerExchangeApi { // with purpose TALER_SIGNATURE_MASTER_WIRE_DETAILS. master_sig: EddsaSignature; } + + export interface ExchangeKeysResponse { + // libtool-style representation of the Exchange protocol version, see + // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning + // The format is "current:revision:age". + version: string; + + // The exchange's base URL. + base_url: string; + + // The exchange's currency or asset unit. + currency: string; + + /** + * FIXME: PARTIALLY IMPLEMENTED!! + */ + + // How wallets should render this currency. + // currency_specification: CurrencySpecification; + + // // Absolute cost offset for the STEFAN curve used + // // to (over) approximate fees payable by amount. + // stefan_abs: AmountString; + + // // Factor to multiply the logarithm of the amount + // // with to (over) approximate fees payable by amount. + // // Note that the total to be paid is first to be + // // divided by the smallest denomination to obtain + // // the value that the logarithm is to be taken of. + // stefan_log: AmountString; + + // // Linear cost factor for the STEFAN curve used + // // to (over) approximate fees payable by amount. + // // + // // Note that this is a scalar, as it is multiplied + // // with the actual amount. + // stefan_lin: Float; + + // // Type of the asset. "fiat", "crypto", "regional" + // // or "stock". Wallets should adjust their UI/UX + // // based on this value. + // asset_type: string; + + // // Array of wire accounts operated by the exchange for + // // incoming wire transfers. + // accounts: WireAccount[]; + + // // Object mapping names of wire methods (i.e. "iban" or "x-taler-bank") + // // to wire fees. + // wire_fees: { method: AggregateTransferFee[] }; + + // // List of exchanges that this exchange is partnering + // // with to enable wallet-to-wallet transfers. + // wads: ExchangePartner[]; + + // // Set to true if this exchange allows the use + // // of reserves for rewards. + // // @deprecated in protocol v18. + // rewards_allowed: false; + + // // EdDSA master public key of the exchange, used to sign entries + // // in denoms and signkeys. + // master_public_key: EddsaPublicKey; + + // // Relative duration until inactive reserves are closed; + // // not signed (!), can change without notice. + // reserve_closing_delay: RelativeTime; + + // // Threshold amounts beyond which wallet should + // // trigger the KYC process of the issuing + // // exchange. Optional option, if not given there is no limit. + // // Currency must match currency. + // wallet_balance_limit_without_kyc?: AmountString[]; + + // // Denominations offered by this exchange + // denominations: DenomGroup[]; + + // // Compact EdDSA signature (binary-only) over the + // // contatentation of all of the master_sigs (in reverse + // // chronological order by group) in the arrays under + // // "denominations". Signature of TALER_ExchangeKeySetPS + // exchange_sig: EddsaSignature; + + // // Public EdDSA key of the exchange that was used to generate the signature. + // // Should match one of the exchange's signing keys from signkeys. It is given + // // explicitly as the client might otherwise be confused by clock skew as to + // // which signing key was used for the exchange_sig. + // exchange_pub: EddsaPublicKey; + + // // Denominations for which the exchange currently offers/requests recoup. + // recoup: Recoup[]; + + // // Array of globally applicable fees by time range. + // global_fees: GlobalFees[]; + + // // The date when the denomination keys were last updated. + // list_issue_date: Timestamp; + + // // Auditors of the exchange. + // auditors: AuditorKeys[]; + + // // The exchange's signing keys. + // signkeys: SignKey[]; + + // // Optional field with a dictionary of (name, object) pairs defining the + // // supported and enabled extensions, such as age_restriction. + // extensions?: { name: ExtensionManifest }; + + // // Signature by the exchange master key of the SHA-256 hash of the + // // normalized JSON-object of field extensions, if it was set. + // // The signature has purpose TALER_SIGNATURE_MASTER_EXTENSIONS. + // extensions_sig?: EddsaSignature; + } + + interface ExtensionManifest { + // The criticality of the extension MUST be provided. It has the same + // semantics as "critical" has for extensions in X.509: + // - if "true", the client must "understand" the extension before + // proceeding, + // - if "false", clients can safely skip extensions they do not + // understand. + // (see https://datatracker.ietf.org/doc/html/rfc5280#section-4.2) + critical: boolean; + + // The version information MUST be provided in Taler's protocol version + // ranges notation, see + // https://docs.taler.net/core/api-common.html#protocol-version-ranges + version: LibtoolVersion; + + // Optional configuration object, defined by the feature itself + config?: object; + } + + interface SignKey { + // The actual exchange's EdDSA signing public key. + key: EddsaPublicKey; + + // Initial validity date for the signing key. + stamp_start: Timestamp; + + // Date when the exchange will stop using the signing key, allowed to overlap + // slightly with the next signing key's validity to allow for clock skew. + stamp_expire: Timestamp; + + // Date when all signatures made by the signing key expire and should + // henceforth no longer be considered valid in legal disputes. + stamp_end: Timestamp; + + // Signature over key and stamp_expire by the exchange master key. + // Signature of TALER_ExchangeSigningKeyValidityPS. + // Must have purpose TALER_SIGNATURE_MASTER_SIGNING_KEY_VALIDITY. + master_sig: EddsaSignature; + } + + interface AuditorKeys { + // The auditor's EdDSA signing public key. + auditor_pub: EddsaPublicKey; + + // The auditor's URL. + auditor_url: string; + + // The auditor's name (for humans). + auditor_name: string; + + // An array of denomination keys the auditor affirms with its signature. + // Note that the message only includes the hash of the public key, while the + // signature is actually over the expanded information including expiration + // times and fees. The exact format is described below. + denomination_keys: AuditorDenominationKey[]; + } + interface AuditorDenominationKey { + // Hash of the public RSA key used to sign coins of the respective + // denomination. Note that the auditor's signature covers more than just + // the hash, but this other information is already provided in denoms and + // thus not repeated here. + denom_pub_h: HashCode; + + // Signature of TALER_ExchangeKeyValidityPS. + auditor_sig: EddsaSignature; + } + + interface GlobalFees { + // What date (inclusive) does these fees go into effect? + start_date: Timestamp; + + // What date (exclusive) does this fees stop going into effect? + end_date: Timestamp; + + // Account history fee, charged when a user wants to + // obtain a reserve/account history. + history_fee: AmountString; + + // Annual fee charged for having an open account at the + // exchange. Charged to the account. If the account + // balance is insufficient to cover this fee, the account + // is automatically deleted/closed. (Note that the exchange + // will keep the account history around for longer for + // regulatory reasons.) + account_fee: AmountString; + + // Purse fee, charged only if a purse is abandoned + // and was not covered by the account limit. + purse_fee: AmountString; + + // How long will the exchange preserve the account history? + // After an account was deleted/closed, the exchange will + // retain the account history for legal reasons until this time. + history_expiration: RelativeTime; + + // Non-negative number of concurrent purses that any + // account holder is allowed to create without having + // to pay the purse_fee. + purse_account_limit: Integer; + + // How long does an exchange keep a purse around after a purse + // has expired (or been successfully merged)? A 'GET' request + // for a purse will succeed until the purse expiration time + // plus this value. + purse_timeout: RelativeTime; + + // Signature of TALER_GlobalFeesPS. + master_sig: EddsaSignature; + } + + interface Recoup { + // Hash of the public key of the denomination that is being revoked under + // emergency protocol (see /recoup). + h_denom_pub: HashCode; + + // We do not include any signature here, as the primary use-case for + // this emergency involves the exchange having lost its signing keys, + // so such a signature here would be pretty worthless. However, the + // exchange will not honor /recoup requests unless they are for + // denomination keys listed here. + } + + interface AggregateTransferFee { + // Per transfer wire transfer fee. + wire_fee: AmountString; + + // Per transfer closing fee. + closing_fee: AmountString; + + // What date (inclusive) does this fee go into effect? + // The different fees must cover the full time period in which + // any of the denomination keys are valid without overlap. + start_date: Timestamp; + + // What date (exclusive) does this fee stop going into effect? + // The different fees must cover the full time period in which + // any of the denomination keys are valid without overlap. + end_date: Timestamp; + + // Signature of TALER_MasterWireFeePS with + // purpose TALER_SIGNATURE_MASTER_WIRE_FEES. + sig: EddsaSignature; + } + + interface ExchangePartner { + // Base URL of the partner exchange. + partner_base_url: string; + + // Public master key of the partner exchange. + partner_master_pub: EddsaPublicKey; + + // Per exchange-to-exchange transfer (wad) fee. + wad_fee: AmountString; + + // Exchange-to-exchange wad (wire) transfer frequency. + wad_frequency: RelativeTime; + + // When did this partnership begin (under these conditions)? + start_date: Timestamp; + + // How long is this partnership expected to last? + end_date: Timestamp; + + // Signature using the exchange's offline key over + // TALER_WadPartnerSignaturePS + // with purpose TALER_SIGNATURE_MASTER_PARTNER_DETAILS. + master_sig: EddsaSignature; + } + + type DenomGroup = + | DenomGroupRsa + | DenomGroupCs + | DenomGroupRsaAgeRestricted + | DenomGroupCsAgeRestricted; + interface DenomGroupRsa extends DenomGroupCommon { + cipher: "RSA"; + + denoms: ({ + rsa_pub: RsaPublicKey; + } & DenomCommon)[]; + } + interface DenomGroupCs extends DenomGroupCommon { + cipher: "CS"; + + denoms: ({ + cs_pub: Cs25519Point; + } & DenomCommon)[]; + } + + // Binary representation of the age groups. + // The bits set in the mask mark the edges at the beginning of a next age + // group. F.e. for the age groups + // 0-7, 8-9, 10-11, 12-13, 14-15, 16-17, 18-21, 21-* + // the following bits are set: + // + // 31 24 16 8 0 + // | | | | | + // oooooooo oo1oo1o1 o1o1o1o1 ooooooo1 + // + // A value of 0 means that the exchange does not support the extension for + // age-restriction. + type AgeMask = Integer; + + interface DenomGroupRsaAgeRestricted extends DenomGroupCommon { + cipher: "RSA+age_restricted"; + age_mask: AgeMask; + + denoms: ({ + rsa_pub: RsaPublicKey; + } & DenomCommon)[]; + } + interface DenomGroupCsAgeRestricted extends DenomGroupCommon { + cipher: "CS+age_restricted"; + age_mask: AgeMask; + + denoms: ({ + cs_pub: Cs25519Point; + } & DenomCommon)[]; + } + // Common attributes for all denomination groups + interface DenomGroupCommon { + // How much are coins of this denomination worth? + value: AmountString; + + // Fee charged by the exchange for withdrawing a coin of this denomination. + fee_withdraw: AmountString; + + // Fee charged by the exchange for depositing a coin of this denomination. + fee_deposit: AmountString; + + // Fee charged by the exchange for refreshing a coin of this denomination. + fee_refresh: AmountString; + + // Fee charged by the exchange for refunding a coin of this denomination. + fee_refund: AmountString; + } + interface DenomCommon { + // Signature of TALER_DenominationKeyValidityPS. + master_sig: EddsaSignature; + + // When does the denomination key become valid? + stamp_start: Timestamp; + + // When is it no longer possible to withdraw coins + // of this denomination? + stamp_expire_withdraw: Timestamp; + + // When is it no longer possible to deposit coins + // of this denomination? + stamp_expire_deposit: Timestamp; + + // Timestamp indicating by when legal disputes relating to these coins must + // be settled, as the exchange will afterwards destroy its evidence relating to + // transactions involving this coin. + stamp_expire_legal: Timestamp; + + // Set to 'true' if the exchange somehow "lost" + // the private key. The denomination was not + // necessarily revoked, but still cannot be used + // to withdraw coins at this time (theoretically, + // the private key could be recovered in the + // future; coins signed with the private key + // remain valid). + lost?: boolean; + } + type DenominationKey = RsaDenominationKey | CSDenominationKey; + interface RsaDenominationKey { + cipher: "RSA"; + + // 32-bit age mask. + age_mask: Integer; + + // RSA public key + rsa_public_key: RsaPublicKey; + } + interface CSDenominationKey { + cipher: "CS"; + + // 32-bit age mask. + age_mask: Integer; + + // Public key of the denomination. + cs_public_key: Cs25519Point; + } } export namespace TalerMerchantApi { @@ -1954,6 +3228,10 @@ export namespace TalerMerchantApi { // Name of the protocol. name: "taler-merchant"; + // URN of the implementation (needed to interpret 'revision' in version). + // @since **v8**, may become mandatory in the future. + implementation?: string; + // Default (!) currency supported by this backend. // This is the currency that the backend should // suggest by default to the user when entering @@ -1961,15 +3239,39 @@ export namespace TalerMerchantApi { // supported currencies and how to render them. currency: string; - // How wallets should render currencies supported + // How services should render currencies supported // by this backend. Maps // currency codes (e.g. "EUR" or "KUDOS") to // the respective currency specification. // All currencies in this map are supported by - // the backend. + // the backend. Note that the actual currency + // specifications are a *hint* for applications + // that would like *advice* on how to render amounts. + // Applications *may* ignore the currency specification + // if they know how to render currencies that they are + // used with. currencies: { [currency: string]: CurrencySpecification }; + + // Array of exchanges trusted by the merchant. + // Since protocol **v6**. + exchanges: ExchangeConfigInfo[]; } + export interface ExchangeConfigInfo { + // Base URL of the exchange REST API. + base_url: string; + + // Currency for which the merchant is configured + // to trust the exchange. + // May not be the one the exchange actually uses, + // but is the only one we would trust this exchange for. + currency: string; + + // Offline master public key of the exchange. The + // /keys data must be signed with this public + // key for us to trust it. + master_pub: EddsaPublicKey; + } export interface ClaimRequest { // Nonce to identify the wallet that claimed the order. nonce: string; @@ -1998,7 +3300,131 @@ export namespace TalerMerchantApi { pos_confirmation?: string; } - interface PayRequest { + export interface PaymentStatusRequestParams { + // Hash of the order’s contract terms (this is used to + // authenticate the wallet/customer in case + // $ORDER_ID is guessable). + // Required once an order was claimed. + contractTermHash?: string; + // Authorizes the request via the claim token that + // was returned in the PostOrderResponse. Used with + // unclaimed orders only. Whether token authorization is + // required is determined by the merchant when the + // frontend creates the order. + claimToken?: string; + // Session ID that the payment must be bound to. + // If not specified, the payment is not session-bound. + sessionId?: string; + // If specified, the merchant backend will wait up to + // timeout_ms milliseconds for completion of the payment + // before sending the HTTP response. A client must never + // rely on this behavior, as the merchant backend may return + // a response immediately. + timeout?: number; + // If set to “yes”, poll for the order’s pending refunds + // to be picked up. timeout_ms specifies how long we + // will wait for the refund. + awaitRefundObtained?: boolean; + // Indicates that we are polling for a refund above the + // given AMOUNT. timeout_ms will specify how long we + // will wait for the refund. + refund?: AmountString; + // Since protocol v9 refunded orders are only returned + // under “already_paid_order_id” if this flag is set + // explicitly to “YES”. + allowRefundedForRepurchase?: boolean; + } + export interface GetKycStatusRequestParams { + // If specified, the KYC check should return + // the KYC status only for this wire account. + // Otherwise, for all wire accounts. + wireHash?: string; + // If specified, the KYC check should return + // the KYC status only for the given exchange. + // Otherwise, for all exchanges we interacted with. + exchangeURL?: string; + // If specified, the merchant will wait up to + // timeout_ms milliseconds for the exchanges to + // confirm completion of the KYC process(es). + timeout?: number; + } + export interface GetOtpDeviceRequestParams { + // Timestamp in seconds to use when calculating + // the current OTP code of the device. Since protocol v10. + faketime?: number; + // Price to use when calculating the current OTP + // code of the device. Since protocol v10. + price?: AmountString; + } + export interface GetOrderRequestParams { + // Session ID that the payment must be bound to. + // If not specified, the payment is not session-bound. + sessionId?: string; + // Timeout in milliseconds to wait for a payment if + // the answer would otherwise be negative (long polling). + timeout?: number; + // Since protocol v9 refunded orders are only returned + // under “already_paid_order_id” if this flag is set + // explicitly to “YES”. + allowRefundedForRepurchase?: boolean; + } + export interface ListWireTransferRequestParams { + // Filter for transfers to the given bank account + // (subject and amount MUST NOT be given in the payto URI). + paytoURI?: string; + // Filter for transfers executed before the given timestamp. + before?: number; + // Filter for transfers executed after the given timestamp. + after?: number; + // At most return the given number of results. Negative for + // descending in execution time, positive for ascending in + // execution time. Default is -20. + limit?: number; + // Starting transfer_serial_id for an iteration. + offset?: string; + // Filter transfers by verification status. + verified?: boolean; + order?: "asc" | "dec"; + } + export interface ListOrdersRequestParams { + // If set to yes, only return paid orders, if no only + // unpaid orders. Do not give (or use “all”) to see all + // orders regardless of payment status. + paid?: boolean; + // If set to yes, only return refunded orders, if no only + // unrefunded orders. Do not give (or use “all”) to see + // all orders regardless of refund status. + refunded?: boolean; + // If set to yes, only return wired orders, if no only + // orders with missing wire transfers. Do not give (or + // use “all”) to see all orders regardless of wire transfer + // status. + wired?: boolean; + // At most return the given number of results. Negative + // for descending by row ID, positive for ascending by + // row ID. Default is 20. Since protocol v12. + limit?: number; + // Non-negative date in seconds after the UNIX Epoc, see delta + // for its interpretation. If not specified, we default to the + // oldest or most recent entry, depending on delta. + date?: AbsoluteTime; + // Starting product_serial_id for an iteration. + // Since protocol v12. + offset?: string; + // Timeout in milliseconds to wait for additional orders if the + // answer would otherwise be negative (long polling). Only useful + // if delta is positive. Note that the merchant MAY still return + // a response that contains fewer than delta orders. + timeout?: number; + // Since protocol v6. Filters by session ID. + sessionId?: string; + // Since protocol v6. Filters by fulfillment URL. + fulfillmentUrl?: string; + + order?: "asc" | "dec"; + } + + export interface PayRequest { // The coins used to make the payment. coins: CoinPaySig[]; @@ -2029,7 +3455,9 @@ export namespace TalerMerchantApi { exchange_url: string; } - interface StatusPaid { + export interface StatusPaid { + type: "paid"; + // Was the payment refunded (even partially, via refund or abort)? refunded: boolean; @@ -2042,14 +3470,16 @@ export namespace TalerMerchantApi { // Amount that already taken by the wallet. refund_taken: AmountString; } - interface StatusGotoResponse { + export interface StatusGotoResponse { + type: "goto"; // The client should go to the reorder URL, there a fresh // order might be created as this one is taken by another // customer or wallet (or repurchase detection logic may // apply). public_reorder_url: string; } - interface StatusUnpaidResponse { + export interface StatusUnpaidResponse { + type: "unpaid"; // URI that the wallet must process to complete the payment. taler_pay_uri: string; @@ -2062,16 +3492,16 @@ export namespace TalerMerchantApi { already_paid_order_id?: string; } - interface PaidRefundStatusResponse { + export interface PaidRefundStatusResponse { // Text to be shown to the point-of-sale staff as a proof of - // payment (present only if re-usable OTP algorithm is used). + // payment (present only if reusable OTP algorithm is used). pos_confirmation?: string; // True if the order has been subjected to // refunds. False if it was simply paid. refunded: boolean; } - interface PaidRequest { + export interface PaidRequest { // Signature on TALER_PaymentResponsePS with the public // key of the merchant instance. sig: EddsaSignature; @@ -2088,7 +3518,7 @@ export namespace TalerMerchantApi { session_id: string; } - interface AbortRequest { + export interface AbortRequest { // Hash of the order's contract terms (this is used to authenticate the // wallet/customer in case $ORDER_ID is guessable). h_contract: HashCode; @@ -2108,18 +3538,18 @@ export namespace TalerMerchantApi { // URL of the exchange this coin was withdrawn from. exchange_url: string; } - interface AbortResponse { + export interface AbortResponse { // List of refund responses about the coins that the wallet // requested an abort for. In the same order as the coins // from the original request. // The rtransaction_id is implied to be 0. refunds: MerchantAbortPayRefundStatus[]; } - type MerchantAbortPayRefundStatus = + export type MerchantAbortPayRefundStatus = | MerchantAbortPayRefundSuccessStatus | MerchantAbortPayRefundFailureStatus; // Details about why a refund failed. - interface MerchantAbortPayRefundFailureStatus { + export interface MerchantAbortPayRefundFailureStatus { // Used as tag for the sum type RefundStatus sum type. type: "failure"; @@ -2135,7 +3565,7 @@ export namespace TalerMerchantApi { // Additional details needed to verify the refund confirmation signature // (h_contract_terms and merchant_pub) are already known // to the wallet and thus not included. - interface MerchantAbortPayRefundSuccessStatus { + export interface MerchantAbortPayRefundSuccessStatus { // Used as tag for the sum type MerchantCoinRefundStatus sum type. type: "success"; @@ -2154,12 +3584,12 @@ export namespace TalerMerchantApi { exchange_pub: EddsaPublicKey; } - interface WalletRefundRequest { + export interface WalletRefundRequest { // Hash of the order's contract terms (this is used to authenticate the // wallet/customer). h_contract: HashCode; } - interface WalletRefundResponse { + export interface WalletRefundResponse { // Amount that was refunded in total. refund_amount: AmountString; @@ -2169,11 +3599,11 @@ export namespace TalerMerchantApi { // Public key of the merchant. merchant_pub: EddsaPublicKey; } - type MerchantCoinRefundStatus = + export type MerchantCoinRefundStatus = | MerchantCoinRefundSuccessStatus | MerchantCoinRefundFailureStatus; // Details about why a refund failed. - interface MerchantCoinRefundFailureStatus { + export interface MerchantCoinRefundFailureStatus { // Used as tag for the sum type RefundStatus sum type. type: "failure"; @@ -2203,7 +3633,7 @@ export namespace TalerMerchantApi { // Additional details needed to verify the refund confirmation signature // (h_contract_terms and merchant_pub) are already known // to the wallet and thus not included. - interface MerchantCoinRefundSuccessStatus { + export interface MerchantCoinRefundSuccessStatus { // Used as tag for the sum type MerchantCoinRefundStatus sum type. type: "success"; @@ -2274,7 +3704,7 @@ export namespace TalerMerchantApi { blind_sig: BlindedRsaSignature; } - interface InstanceConfigurationMessage { + export interface InstanceConfigurationMessage { // Name of the merchant instance to create (will become $INSTANCE). // Must match the regex ^[A-Za-z0-9][A-Za-z0-9_.@-]+$. id: string; @@ -2322,7 +3752,7 @@ export namespace TalerMerchantApi { default_pay_delay: RelativeTime; } - interface InstanceAuthConfigurationMessage { + export interface InstanceAuthConfigurationMessage { // Type of authentication. // "external": The mechant backend does not do // any authentication checks. Instead an API @@ -2336,40 +3766,10 @@ export namespace TalerMerchantApi { // After the auth token has been set (with method "token"), // the value must be provided in a "Authorization: Bearer $token" // header. - token?: string; - } - - interface LoginTokenRequest { - // Scope of the token (which kinds of operations it will allow) - scope: "readonly" | "write"; - - // Server may impose its own upper bound - // on the token validity duration - duration?: RelativeTime; - - // Can this token be refreshed? - // Defaults to false. - refreshable?: boolean; + token?: AccessToken; } - interface LoginTokenSuccessResponse { - // The login token that can be used to access resources - // that are in scope for some time. Must be prefixed - // with "Bearer " when used in the "Authorization" HTTP header. - // Will already begin with the RFC 8959 prefix. - token: string; - - // Scope of the token (which kinds of operations it will allow) - scope: "readonly" | "write"; - // Server may impose its own upper bound - // on the token validity duration - expiration: Timestamp; - - // Can this token be refreshed? - refreshable: boolean; - } - - interface InstanceReconfigurationMessage { + export interface InstanceReconfigurationMessage { // Merchant name corresponding to this instance. name: string; @@ -2410,12 +3810,12 @@ export namespace TalerMerchantApi { default_pay_delay: RelativeTime; } - interface InstancesResponse { + export interface InstancesResponse { // List of instances that are present in the backend (see Instance). instances: Instance[]; } - interface Instance { + export interface Instance { // Merchant name corresponding to this instance. name: string; @@ -2443,7 +3843,7 @@ export namespace TalerMerchantApi { deleted: boolean; } - interface QueryInstancesResponse { + export interface QueryInstancesResponse { // Merchant name corresponding to this instance. name: string; @@ -2487,11 +3887,11 @@ export namespace TalerMerchantApi { // Authentication configuration. // Does not contain the token when token auth is configured. auth: { - type: "external" | "token"; + method: "external" | "token"; }; } - interface AccountKycRedirects { + export interface AccountKycRedirects { // Array of pending KYCs. pending_kycs: MerchantAccountKycRedirect[]; @@ -2499,7 +3899,7 @@ export namespace TalerMerchantApi { timeout_kycs: ExchangeKycTimeout[]; } - interface MerchantAccountKycRedirect { + export interface MerchantAccountKycRedirect { // URL that the user should open in a browser to // proceed with the KYC process (as returned // by the exchange's /kyc-check/ endpoint). @@ -2517,7 +3917,7 @@ export namespace TalerMerchantApi { payto_uri: PaytoString; } - interface ExchangeKycTimeout { + export interface ExchangeKycTimeout { // Base URL of the exchange this is about. exchange_url: string; @@ -2531,7 +3931,7 @@ export namespace TalerMerchantApi { exchange_http_status: number; } - interface AccountAddDetails { + export interface AccountAddDetails { // payto:// URI of the account. payto_uri: PaytoString; @@ -2547,11 +3947,13 @@ export namespace TalerMerchantApi { credit_facade_credentials?: FacadeCredentials; } - type FacadeCredentials = NoFacadeCredentials | BasicAuthFacadeCredentials; - interface NoFacadeCredentials { + export type FacadeCredentials = + | NoFacadeCredentials + | BasicAuthFacadeCredentials; + export interface NoFacadeCredentials { type: "none"; } - interface BasicAuthFacadeCredentials { + export interface BasicAuthFacadeCredentials { type: "basic"; // Username to use to authenticate @@ -2560,7 +3962,7 @@ export namespace TalerMerchantApi { // Password to use to authenticate password: string; } - interface AccountAddResponse { + export interface AccountAddResponse { // Hash over the wire details (including over the salt). h_wire: HashCode; @@ -2568,7 +3970,7 @@ export namespace TalerMerchantApi { salt: HashCode; } - interface AccountPatchDetails { + export interface AccountPatchDetails { // URL from where the merchant can download information // about incoming wire transfers to this account. credit_facade_url?: string; @@ -2583,11 +3985,20 @@ export namespace TalerMerchantApi { credit_facade_credentials?: FacadeCredentials; } - interface AccountsSummaryResponse { + export interface AccountsSummaryResponse { // List of accounts that are known for the instance. - accounts: BankAccountEntry[]; + accounts: BankAccountSummaryEntry[]; } - interface BankAccountEntry { + + // TODO: missing in docs + export interface BankAccountSummaryEntry { + // payto:// URI of the account. + payto_uri: PaytoString; + + // Hash over the wire details (including over the salt). + h_wire: HashCode; + } + export interface BankAccountEntry { // payto:// URI of the account. payto_uri: PaytoString; @@ -2603,10 +4014,10 @@ export namespace TalerMerchantApi { // true if this account is active, // false if it is historic. - active: boolean; + active?: boolean; } - interface ProductAddDetail { + export interface ProductAddDetail { // Product ID to use. product_id: string; @@ -2648,7 +4059,7 @@ export namespace TalerMerchantApi { minimum_age?: Integer; } - interface ProductPatchDetail { + export interface ProductPatchDetail { // Human-readable product description. description: string; @@ -2690,17 +4101,81 @@ export namespace TalerMerchantApi { minimum_age?: Integer; } - interface InventorySummaryResponse { + export interface InventorySummaryResponse { // List of products that are present in the inventory. products: InventoryEntry[]; } - interface InventoryEntry { + export interface InventoryEntry { // Product identifier, as found in the product. product_id: string; + // product_serial_id of the product in the database. + product_serial: Integer; } - interface ProductDetail { + export interface FullInventoryDetailsResponse { + // List of products that are present in the inventory. + products: MerchantPosProductDetail[]; + + // List of categories in the inventory. + categories: MerchantCategory[]; + } + + export interface MerchantPosProductDetail { + // A unique numeric ID of the product + product_serial: number; + + // A merchant-internal unique identifier for the product + product_id?: string; + + // A list of category IDs this product belongs to. + // Typically, a product only belongs to one category, but more than one is supported. + categories: number[]; + + // Human-readable product description. + description: string; + + // Map from IETF BCP 47 language tags to localized descriptions. + description_i18n: { [lang_tag: string]: string }; + + // Unit in which the product is measured (liters, kilograms, packages, etc.). + unit: string; + + // The price for one unit of the product. Zero is used + // to imply that this product is not sold separately, or + // that the price is not fixed, and must be supplied by the + // front-end. If non-zero, this price MUST include applicable + // taxes. + price: AmountString; + + // An optional base64-encoded product image. + image?: ImageDataUrl; + + // A list of taxes paid by the merchant for one unit of this product. + taxes?: Tax[]; + + // Number of units of the product in stock in sum in total, + // including all existing sales ever. Given in product-specific + // units. + // Optional, if missing treat as "infinite". + total_stock?: Integer; + + // Minimum age buyer must have (in years). + minimum_age?: Integer; + } + + export interface MerchantCategory { + // A unique numeric ID of the category + id: number; + + // The name of the category. This will be shown to users and used in the order summary. + name: string; + + // Map from IETF BCP 47 language tags to localized names + name_i18n?: { [lang_tag: string]: string }; + } + + export interface ProductDetail { // Human-readable product description. description: string; @@ -2721,7 +4196,7 @@ export namespace TalerMerchantApi { image: ImageDataUrl; // A list of taxes paid by the merchant for one unit of this product. - taxes: Tax[]; + taxes?: Tax[]; // Number of units of the product in stock in sum in total, // including all existing sales ever. Given in product-specific @@ -2736,7 +4211,7 @@ export namespace TalerMerchantApi { total_lost: Integer; // Identifies where the product is in stock. - address: Location; + address?: Location; // Identifies when we expect the next restocking to happen. next_restock?: Timestamp; @@ -2744,7 +4219,7 @@ export namespace TalerMerchantApi { // Minimum age buyer must have (in years). minimum_age?: Integer; } - interface LockRequest { + export interface LockRequest { // UUID that identifies the frontend performing the lock // Must be unique for the lifetime of the lock. lock_uuid: string; @@ -2756,7 +4231,7 @@ export namespace TalerMerchantApi { quantity: Integer; } - interface PostOrderRequest { + export interface PostOrderRequest { // The order must at least contain the minimal // order detail, but can override all. order: Order; @@ -2784,7 +4259,7 @@ export namespace TalerMerchantApi { // be used in case different UUIDs were used for different // products (i.e. in case the user started with multiple // shopping sessions that were combined during checkout). - lock_uuids: string[]; + lock_uuids?: string[]; // Should a token for claiming the order be generated? // False can make sense if the ORDER_ID is sufficiently @@ -2797,9 +4272,9 @@ export namespace TalerMerchantApi { otp_id?: string; } - type Order = MinimalOrderDetail | ContractTerms; + export type Order = MinimalOrderDetail & Partial<ContractTerms>; - interface MinimalOrderDetail { + export interface MinimalOrderDetail { // Amount to be paid by the customer. amount: AmountString; @@ -2808,6 +4283,9 @@ export namespace TalerMerchantApi { // See documentation of fulfillment_url in ContractTerms. // Either fulfillment_url or fulfillment_message must be specified. + // When creating an order, the fulfillment URL can + // contain ${ORDER_ID} which will be substituted with the + // order ID of the newly created order. fulfillment_url?: string; // See documentation of fulfillment_message in ContractTerms. @@ -2815,7 +4293,7 @@ export namespace TalerMerchantApi { fulfillment_message?: string; } - interface MinimalInventoryProduct { + export interface MinimalInventoryProduct { // Which product is requested (here mandatory!). product_id: string; @@ -2823,7 +4301,7 @@ export namespace TalerMerchantApi { quantity: Integer; } - interface PostOrderResponse { + export interface PostOrderResponse { // Order ID of the response that was just created. order_id: string; @@ -2832,7 +4310,7 @@ export namespace TalerMerchantApi { // in the request. token?: ClaimToken; } - interface OutOfStockResponse { + export interface OutOfStockResponse { // Product ID of an out-of-stock item. product_id: string; @@ -2847,12 +4325,12 @@ export namespace TalerMerchantApi { restock_expected?: Timestamp; } - interface OrderHistory { + export interface OrderHistory { // Timestamp-sorted array of all orders matching the query. // The order of the sorting depends on the sign of delta. orders: OrderHistoryEntry[]; } - interface OrderHistoryEntry { + export interface OrderHistoryEntry { // Order ID of the transaction related to this entry. order_id: string; @@ -2878,11 +4356,11 @@ export namespace TalerMerchantApi { paid: boolean; } - type MerchantOrderStatusResponse = + export type MerchantOrderStatusResponse = | CheckPaymentPaidResponse | CheckPaymentClaimedResponse | CheckPaymentUnpaidResponse; - interface CheckPaymentPaidResponse { + export interface CheckPaymentPaidResponse { // The customer paid for this contract. order_status: "paid"; @@ -2933,14 +4411,14 @@ export namespace TalerMerchantApi { // to show the order QR code / trigger the wallet. order_status_url: string; } - interface CheckPaymentClaimedResponse { + export interface CheckPaymentClaimedResponse { // A wallet claimed the order, but did not yet pay for the contract. order_status: "claimed"; // Contract terms. contract_terms: ContractTerms; } - interface CheckPaymentUnpaidResponse { + export interface CheckPaymentUnpaidResponse { // The order was neither claimed nor paid. order_status: "unpaid"; @@ -2971,7 +4449,7 @@ export namespace TalerMerchantApi { // We do we NOT return the contract terms here because they may not // exist in case the wallet did not yet claim them. } - interface RefundDetails { + export interface RefundDetails { // Reason given for the refund. reason: string; @@ -2984,7 +4462,7 @@ export namespace TalerMerchantApi { // Total amount that was refunded (minus a refund fee). amount: AmountString; } - interface TransactionWireTransfer { + export interface TransactionWireTransfer { // Responsible exchange. exchange_url: string; @@ -3002,7 +4480,7 @@ export namespace TalerMerchantApi { // POST /transfers API, or is it merely claimed by the exchange? confirmed: boolean; } - interface TransactionWireReport { + export interface TransactionWireReport { // Numerical error code. code: number; @@ -3019,20 +4497,20 @@ export namespace TalerMerchantApi { coin_pub: CoinPublicKey; } - interface ForgetRequest { + export interface ForgetRequest { // Array of valid JSON paths to forgettable fields in the order's // contract terms. fields: string[]; } - interface RefundRequest { + export interface RefundRequest { // Amount to be refunded. refund: AmountString; // Human-readable refund justification. reason: string; } - interface MerchantRefundResponse { + export interface MerchantRefundResponse { // URL (handled by the backend) that the wallet should access to // trigger refund processing. // taler://refund/... @@ -3043,7 +4521,7 @@ export namespace TalerMerchantApi { h_contract: HashCode; } - interface TransferInformation { + export interface TransferInformation { // How much was wired to the merchant (minus fees). credit_amount: AmountString; @@ -3057,11 +4535,11 @@ export namespace TalerMerchantApi { exchange_url: string; } - interface TransferList { + export interface TransferList { // List of all the transfers that fit the filter that we know. transfers: TransferDetails[]; } - interface TransferDetails { + export interface TransferDetails { // How much was wired to the merchant (minus fees). credit_amount: AmountString; @@ -3093,196 +4571,40 @@ export namespace TalerMerchantApi { confirmed?: boolean; } - interface ReserveCreateRequest { - // Amount that the merchant promises to put into the reserve. - initial_balance: AmountString; - - // Exchange the merchant intends to use for rewards. - exchange_url: string; - - // Desired wire method, for example "iban" or "x-taler-bank". - wire_method: string; - } - interface ReserveCreateConfirmation { - // Public key identifying the reserve. - reserve_pub: EddsaPublicKey; - - // Wire accounts of the exchange where to transfer the funds. - accounts: TalerExchangeApi.WireAccount[]; - } - - interface RewardReserveStatus { - // Array of all known reserves (possibly empty!). - reserves: ReserveStatusEntry[]; - } - interface ReserveStatusEntry { - // Public key of the reserve. - reserve_pub: EddsaPublicKey; - - // Timestamp when it was established. - creation_time: Timestamp; - - // Timestamp when it expires. - expiration_time: Timestamp; - - // Initial amount as per reserve creation call. - merchant_initial_amount: AmountString; - - // Initial amount as per exchange, 0 if exchange did - // not confirm reserve creation yet. - exchange_initial_amount: AmountString; - - // Amount picked up so far. - pickup_amount: AmountString; - - // Amount approved for rewards that exceeds the pickup_amount. - committed_amount: AmountString; - - // Is this reserve active (false if it was deleted but not purged)? - active: boolean; - } - - interface ReserveDetail { - // Timestamp when it was established. - creation_time: Timestamp; - - // Timestamp when it expires. - expiration_time: Timestamp; - - // Initial amount as per reserve creation call. - merchant_initial_amount: AmountString; - - // Initial amount as per exchange, 0 if exchange did - // not confirm reserve creation yet. - exchange_initial_amount: AmountString; - - // Amount picked up so far. - pickup_amount: AmountString; - - // Amount approved for rewards that exceeds the pickup_amount. - committed_amount: AmountString; - - // Array of all rewards created by this reserves (possibly empty!). - // Only present if asked for explicitly. - rewards?: RewardStatusEntry[]; - - // Is this reserve active (false if it was deleted but not purged)? - active: boolean; - - // Array of wire accounts of the exchange that could - // be used to fill the reserve, can be NULL - // if the reserve is inactive or was already filled - accounts?: TalerExchangeApi.WireAccount[]; - - // URL of the exchange hosting the reserve, - // NULL if the reserve is inactive - exchange_url: string; - } - interface RewardStatusEntry { - // Unique identifier for the reward. - reward_id: HashCode; - - // Total amount of the reward that can be withdrawn. - total_amount: AmountString; - - // Human-readable reason for why the reward was granted. - reason: string; - } - - interface RewardCreateRequest { - // Amount that the customer should be rewarded. - amount: AmountString; - - // Justification for giving the reward. - justification: string; - - // URL that the user should be directed to after receiving the reward, - // will be included in the reward_token. - next_url: string; - } - interface RewardCreateConfirmation { - // Unique reward identifier for the reward that was created. - reward_id: HashCode; - - // taler://reward URI for the reward. - taler_reward_uri: string; - - // URL that will directly trigger processing - // the reward when the browser is redirected to it. - reward_status_url: string; - - // When does the reward expire? - reward_expiration: Timestamp; - } - - interface RewardDetails { - // Amount that we authorized for this reward. - total_authorized: AmountString; - - // Amount that was picked up by the user already. - total_picked_up: AmountString; - - // Human-readable reason given when authorizing the reward. - reason: string; - - // Timestamp indicating when the reward is set to expire (may be in the past). - expiration: Timestamp; - - // Reserve public key from which the reward is funded. - reserve_pub: EddsaPublicKey; - - // Array showing the pickup operations of the wallet (possibly empty!). - // Only present if asked for explicitly. - pickups?: PickupDetail[]; - } - interface PickupDetail { - // Unique identifier for the pickup operation. - pickup_id: HashCode; - - // Number of planchets involved. - num_planchets: Integer; - - // Total amount requested for this pickup_id. - requested_amount: AmountString; - } - - interface RewardsResponse { - // List of rewards that are present in the backend. - rewards: Reward[]; - } - interface Reward { - // ID of the reward in the backend database. - row_id: number; - - // Unique identifier for the reward. - reward_id: HashCode; - - // (Remaining) amount of the reward (including fees). - reward_amount: AmountString; - } - - interface OtpDeviceAddDetails { + export interface OtpDeviceAddDetails { // Device ID to use. otp_device_id: string; // Human-readable description for the device. otp_device_description: string; - // A base64-encoded key + // A key encoded with RFC 3548 Base32. + // IMPORTANT: This is not using the typical + // Taler base32-crockford encoding. + // Instead it uses the RFC 3548 encoding to + // be compatible with the TOTP standard. otp_key: string; // Algorithm for computing the POS confirmation. - otp_algorithm: Integer; + // "NONE" or 0: No algorithm (no pos confirmation will be generated) + // "TOTP_WITHOUT_PRICE" or 1: Without amounts (typical OTP device) + // "TOTP_WITH_PRICE" or 2: With amounts (special-purpose OTP device) + // The "String" variants are supported @since protocol **v7**. + otp_algorithm: Integer | string; // Counter for counter-based OTP devices. otp_ctr?: Integer; } - interface OtpDevicePatchDetails { + export interface OtpDevicePatchDetails { // Human-readable description for the device. otp_device_description: string; - // A base64-encoded key + // A key encoded with RFC 3548 Base32. + // IMPORTANT: This is not using the typical + // Taler base32-crockford encoding. + // Instead it uses the RFC 3548 encoding to + // be compatible with the TOTP standard. otp_key: string; // Algorithm for computing the POS confirmation. @@ -3292,11 +4614,11 @@ export namespace TalerMerchantApi { otp_ctr?: Integer; } - interface OtpDeviceSummaryResponse { + export interface OtpDeviceSummaryResponse { // Array of devices that are present in our backend. otp_devices: OtpDeviceEntry[]; } - interface OtpDeviceEntry { + export interface OtpDeviceEntry { // Device identifier. otp_device_id: string; @@ -3304,17 +4626,55 @@ export namespace TalerMerchantApi { device_description: string; } - interface OtpDeviceDetails { + export interface OtpDeviceDetails { // Human-readable description for the device. device_description: string; // Algorithm for computing the POS confirmation. + // + // Currently, the following numbers are defined: + // 0: None + // 1: TOTP without price + // 2: TOTP with price otp_algorithm: Integer; // Counter for counter-based OTP devices. otp_ctr?: Integer; + + // Current time for time-based OTP devices. + // Will match the faketime argument of the + // query if one was present, otherwise the current + // time at the backend. + // + // Available since protocol **v10**. + otp_timestamp: Integer; + + // Current OTP confirmation string of the device. + // Matches exactly the string that would be returned + // as part of a payment confirmation for the given + // amount and time (so may contain multiple OTP codes). + // + // If the otp_algorithm is time-based, the code is + // returned for the current time, or for the faketime + // if a TIMESTAMP query argument was provided by the client. + // + // When using OTP with counters, the counter is **NOT** + // increased merely because this endpoint created + // an OTP code (this is a GET request, after all!). + // + // If the otp_algorithm requires an amount, the + // amount argument must be specified in the + // query, otherwise the otp_code is not + // generated. + // + // This field is *optional* in the response, as it is + // only provided if we could compute it based on the + // otp_algorithm and matching client query arguments. + // + // Available since protocol **v10**. + otp_code?: string; } - interface TemplateAddDetails { + export interface TemplateAddDetails { // Template ID to use. template_id: string; @@ -3327,8 +4687,25 @@ export namespace TalerMerchantApi { // Additional information in a separate template. template_contract: TemplateContractDetails; + + // Key-value pairs matching a subset of the + // fields from template_contract that are + // user-editable defaults for this template. + // Since protocol **v13**. + editable_defaults?: TemplateContractDetailsDefaults; + + // Required currency for payments. Useful if no + // amount is specified in the template_contract + // but the user should be required to pay in a + // particular currency anyway. Merchant backends + // may reject requests if the template_contract + // or editable_defaults do + // specify an amount in a different currency. + // This parameter is optional. + // Since protocol **v13**. + required_currency?: string; } - interface TemplateContractDetails { + export interface TemplateContractDetails { // Human-readable summary for the template. summary?: string; @@ -3350,7 +4727,19 @@ export namespace TalerMerchantApi { // It is deleted if the customer did not pay and if the duration is over. pay_duration: RelativeTime; } - interface TemplatePatchDetails { + + export interface TemplateContractDetailsDefaults { + summary?: string; + + currency?: string; + + /** + * Amount *or* a plain currency string. + */ + amount?: string; + } + + export interface TemplatePatchDetails { // Human-readable description for the template. template_description: string; @@ -3360,21 +4749,62 @@ export namespace TalerMerchantApi { // Additional information in a separate template. template_contract: TemplateContractDetails; + + // Key-value pairs matching a subset of the + // fields from template_contract that are + // user-editable defaults for this template. + // Since protocol **v13**. + editable_defaults?: TemplateContractDetailsDefaults; + + // Required currency for payments. Useful if no + // amount is specified in the template_contract + // but the user should be required to pay in a + // particular currency anyway. Merchant backends + // may reject requests if the template_contract + // or editable_defaults do + // specify an amount in a different currency. + // This parameter is optional. + // Since protocol **v13**. + required_currency?: string; } - interface TemplateSummaryResponse { + export interface TemplateSummaryResponse { // List of templates that are present in our backend. - templates_list: TemplateEntry[]; + templates: TemplateEntry[]; } - interface TemplateEntry { + export interface TemplateEntry { // Template identifier, as found in the template. template_id: string; // Human-readable description for the template. template_description: string; } - interface TemplateDetails { + + export interface WalletTemplateDetails { + // Hard-coded information about the contrac terms + // for this template. + template_contract: TemplateContractDetails; + + // Key-value pairs matching a subset of the + // fields from template_contract that are + // user-editable defaults for this template. + // Since protocol **v13**. + editable_defaults?: TemplateContractDetailsDefaults; + + // Required currency for payments. Useful if no + // amount is specified in the template_contract + // but the user should be required to pay in a + // particular currency anyway. Merchant backends + // may reject requests if the template_contract + // or editable_defaults do + // specify an amount in a different currency. + // This parameter is optional. + // Since protocol **v13**. + required_currency?: string; + } + + export interface TemplateDetails { // Human-readable description for the template. template_description: string; @@ -3384,8 +4814,25 @@ export namespace TalerMerchantApi { // Additional information in a separate template. template_contract: TemplateContractDetails; + + // Key-value pairs matching a subset of the + // fields from template_contract that are + // user-editable defaults for this template. + // Since protocol **v13**. + editable_defaults?: TemplateContractDetailsDefaults; + + // Required currency for payments. Useful if no + // amount is specified in the template_contract + // but the user should be required to pay in a + // particular currency anyway. Merchant backends + // may reject requests if the template_contract + // or editable_defaults do + // specify an amount in a different currency. + // This parameter is optional. + // Since protocol **v13**. + required_currency?: string; } - interface UsingTemplateDetails { + export interface UsingTemplateDetails { // Summary of the template summary?: string; @@ -3393,7 +4840,7 @@ export namespace TalerMerchantApi { amount?: AmountString; } - interface WebhookAddDetails { + export interface WebhookAddDetails { // Webhook ID to use. webhook_id: string; @@ -3413,7 +4860,7 @@ export namespace TalerMerchantApi { body_template?: string; } - interface WebhookPatchDetails { + export interface WebhookPatchDetails { // The event of the webhook: why the webhook is used. event_type: string; @@ -3430,12 +4877,12 @@ export namespace TalerMerchantApi { body_template?: string; } - interface WebhookSummaryResponse { + export interface WebhookSummaryResponse { // Return webhooks that are present in our backend. webhooks: WebhookEntry[]; } - interface WebhookEntry { + export interface WebhookEntry { // Webhook identifier, as found in the webhook. webhook_id: string; @@ -3443,7 +4890,7 @@ export namespace TalerMerchantApi { event_type: string; } - interface WebhookDetails { + export interface WebhookDetails { // The event of the webhook: why the webhook is used. event_type: string; @@ -3460,7 +4907,115 @@ export namespace TalerMerchantApi { body_template?: string; } - interface ContractTerms { + export interface TokenFamilyCreateRequest { + // Identifier for the token family consisting of unreserved characters + // according to RFC 3986. + slug: string; + + // Human-readable name for the token family. + name: string; + + // Human-readable description for the token family. + description: string; + + // Optional map from IETF BCP 47 language tags to localized descriptions. + description_i18n?: { [lang_tag: string]: string }; + + // Start time of the token family's validity period. + // If not specified, merchant backend will use the current time. + valid_after?: Timestamp; + + // End time of the token family's validity period. + valid_before: Timestamp; + + // Validity duration of an issued token. + duration: RelativeTime; + + // Kind of the token family. + kind: TokenFamilyKind; + } + + export enum TokenFamilyKind { + Discount = "discount", + Subscription = "subscription", + } + + export interface TokenFamilyUpdateRequest { + // Human-readable name for the token family. + name: string; + + // Human-readable description for the token family. + description: string; + + // Optional map from IETF BCP 47 language tags to localized descriptions. + description_i18n: { [lang_tag: string]: string }; + + // Start time of the token family's validity period. + valid_after: Timestamp; + + // End time of the token family's validity period. + valid_before: Timestamp; + + // Validity duration of an issued token. + duration: RelativeTime; + } + + export interface TokenFamiliesList { + // All configured token families of this instance. + token_families: TokenFamilySummary[]; + } + + export interface TokenFamilySummary { + // Identifier for the token family consisting of unreserved characters + // according to RFC 3986. + slug: string; + + // Human-readable name for the token family. + name: string; + + // Start time of the token family's validity period. + valid_after: Timestamp; + + // End time of the token family's validity period. + valid_before: Timestamp; + + // Kind of the token family. + kind: TokenFamilyKind; + } + + export interface TokenFamilyDetails { + // Identifier for the token family consisting of unreserved characters + // according to RFC 3986. + slug: string; + + // Human-readable name for the token family. + name: string; + + // Human-readable description for the token family. + description: string; + + // Optional map from IETF BCP 47 language tags to localized descriptions. + description_i18n?: { [lang_tag: string]: string }; + + // Start time of the token family's validity period. + valid_after: Timestamp; + + // End time of the token family's validity period. + valid_before: Timestamp; + + // Validity duration of an issued token. + duration: RelativeTime; + + // Kind of the token family. + kind: TokenFamilyKind; + + // How many tokens have been issued for this family. + issued: Integer; + + // How many tokens have been redeemed for this family. + redeemed: Integer; + } + export interface ContractTerms { // Human-readable description of the whole purchase. summary: string; @@ -3581,9 +5136,16 @@ export namespace TalerMerchantApi { // Useful when the merchant needs to store extra information on a // contract without storing it separately in their database. extra?: any; + + // Minimum age the buyer must have (in years). Default is 0. + // This value is at least as large as the maximum over all + // minimum age requirements of the products in this contract. + // It might also be set independent of any product, due to + // legal requirements. + minimum_age?: Integer; } - interface Product { + export interface Product { // Merchant-internal identifier for the product. product_id?: string; @@ -3612,14 +5174,14 @@ export namespace TalerMerchantApi { delivery_date?: Timestamp; } - interface Tax { + export interface Tax { // The name of the tax. name: string; // Amount paid in tax. tax: AmountString; } - interface Merchant { + export interface Merchant { // The merchant's legal name of business. name: string; @@ -3641,7 +5203,7 @@ export namespace TalerMerchantApi { } // Delivery location, loosely modeled as a subset of // ISO20022's PostalAddress25. - interface Location { + export interface Location { // Nation with its own government. country?: string; @@ -3683,7 +5245,7 @@ export namespace TalerMerchantApi { // Base URL of the auditor. url: string; } - interface Exchange { + export interface Exchange { // The exchange's base URL. url: string; @@ -3702,4 +5264,195 @@ export namespace TalerMerchantApi { // Master public key of the exchange. master_pub: EddsaPublicKey; } + + export interface MerchantReserveCreateConfirmation { + // Public key identifying the reserve. + reserve_pub: EddsaPublicKey; + + // Wire accounts of the exchange where to transfer the funds. + accounts: ExchangeWireAccount[]; + } + + export interface TemplateEditableDetails { + // Human-readable summary for the template. + summary?: string; + + // Required currency for payments to the template. + // The user may specify any amount, but it must be + // in this currency. + // This parameter is optional and should not be present + // if "amount" is given. + currency?: string; + + // The price is imposed by the merchant and cannot be changed by the customer. + // This parameter is optional. + amount?: AmountString; + } + + export interface MerchantTemplateContractDetails { + // Human-readable summary for the template. + summary?: string; + + // The price is imposed by the merchant and cannot be changed by the customer. + // This parameter is optional. + amount?: string; + + // Minimum age buyer must have (in years). Default is 0. + minimum_age: number; + + // The time the customer need to pay before his order will be deleted. + // It is deleted if the customer did not pay and if the duration is over. + pay_duration: TalerProtocolDuration; + } + + export interface MerchantTemplateAddDetails { + // Template ID to use. + template_id: string; + + // Human-readable description for the template. + template_description: string; + + // A base64-encoded image selected by the merchant. + // This parameter is optional. + // We are not sure about it. + image?: string; + + editable_defaults?: TemplateEditableDetails; + + // Additional information in a separate template. + template_contract: MerchantTemplateContractDetails; + + // OTP device ID. + // This parameter is optional. + otp_id?: string; + } +} + +export namespace ChallengerApi { + export interface ChallengerTermsOfServiceResponse { + // Name of the service + name: "challenger"; + + // libtool-style representation of the Challenger protocol version, see + // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning + // The format is "current:revision:age". + version: string; + + // URN of the implementation (needed to interpret 'revision' in version). + // @since v0, may become mandatory in the future. + implementation?: string; + } + + export interface ChallengeSetupResponse { + // Nonce to use when constructing /authorize endpoint. + nonce: string; + } + + export interface Restriction { + regex?: string; + hint?: string; + hint_i18n?: InternationalizedString; + } + + export interface ChallengeStatus { + // Object; map of keys (names of the fields of the address + // to be entered by the user) to objects with a "regex" (string) + // containing an extended Posix regular expression for allowed + // address field values, and a "hint"/"hint_i18n" giving a + // human-readable explanation to display if the value entered + // by the user does not match the regex. Keys that are not mapped + // to such an object have no restriction on the value provided by + // the user. See "ADDRESS_RESTRICTIONS" in the challenger configuration. + restrictions: Record<string, Restriction> | undefined; + + // indicates if the given address cannot be changed anymore, the + // form should be read-only if set to true. + fix_address: boolean; + + // form values from the previous submission if available, details depend + // on the ADDRESS_TYPE, should be used to pre-populate the form + last_address: Record<string, string> | undefined; + + // number of times the address can still be changed, may or may not be + // shown to the user + changes_left: Integer; + } + + export interface ChallengeCreateResponse { + // how many more attempts are allowed, might be shown to the user, + // highlighting might be appropriate for low values such as 1 or 2 (the + // form will never be used if the value is zero) + attempts_left: Integer; + + // the address that is being validated, might be shown or not + address: Object; + + // true if we just retransmitted the challenge, false if we sent a + // challenge recently and thus refused to transmit it again this time; + // might make a useful hint to the user + transmitted: boolean; + + // timestamp explaining when we would re-transmit the challenge the next + // time (at the earliest) if requested by the user + next_tx_time: string; + } + + export interface InvalidPinResponse { + // numeric Taler error code, should be shown to indicate the error + // compactly for reporting to developers + ec?: number; + + // human-readable Taler error code, should be shown for the user to + // understand the error + hint: string; + + // how many times is the user still allowed to change the address; + // if 0, the user should not be shown a link to jump to the + // address entry form + addresses_left: Integer; + + // how many times might the PIN still be retransmitted + pin_transmissions_left: Integer; + + // how many times might the user still try entering the PIN code + auth_attempts_left: Integer; + + // if true, the PIN was not even evaluated as the user previously + // exhausted the number of attempts + exhausted: boolean; + + // if true, the PIN was not even evaluated as no challenge was ever + // issued (the user must have skipped the step of providing their + // address first!) + no_challenge: boolean; + } + + export interface ChallengerAuthResponse { + // Token used to authenticate access in /info. + access_token: string; + + // Type of the access token. + token_type: "Bearer"; + + // Amount of time that an access token is valid (in seconds). + expires_in: Integer; + } + + export interface ChallengerInfoResponse { + // Unique ID of the record within Challenger + // (identifies the rowid of the token). + id: Integer; + + // Address that was validated. + // Key-value pairs, details depend on the + // address_type. + address: Object; + + // Type of the address. + address_type: string; + + // How long do we consider the address to be + // valid for this user. + expires: Timestamp; + } } diff --git a/packages/taler-util/src/http-client/utils.ts b/packages/taler-util/src/http-client/utils.ts index ab6f809ef..bf186ce46 100644 --- a/packages/taler-util/src/http-client/utils.ts +++ b/packages/taler-util/src/http-client/utils.ts @@ -1,11 +1,33 @@ +/* + This file is part of GNU Taler + (C) 2022-2024 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * Imports. + */ import { base64FromArrayBuffer } from "../base64.js"; -import { stringToBytes } from "../taler-crypto.js"; -import { AccessToken, PaginationParams } from "./types.js"; +import { encodeCrock, getRandomBytes, stringToBytes } from "../taler-crypto.js"; +import { AccessToken, LongPollParams, PaginationParams } from "./types.js"; /** * Helper function to generate the "Authorization" HTTP header. */ -export function makeBasicAuthHeader(username: string, password: string): string { +export function makeBasicAuthHeader( + username: string, + password: string, +): string { const auth = `${username}:${password}`; const authEncoded: string = base64FromArrayBuffer(stringToBytes(auth)); return `Basic ${authEncoded}`; @@ -13,11 +35,11 @@ export function makeBasicAuthHeader(username: string, password: string): string /** * rfc8959 - * @param token - * @returns + * @param token + * @returns */ export function makeBearerTokenAuthHeader(token: AccessToken): string { - return `Bearer secret-token:${token}`; + return `Bearer ${token}`; } /** @@ -25,14 +47,70 @@ export function makeBearerTokenAuthHeader(token: AccessToken): string { */ export function addPaginationParams(url: URL, pagination?: PaginationParams) { if (!pagination) return; - if (pagination.timoutMs) { - url.searchParams.set("long_poll_ms", String(pagination.timoutMs)) + if (pagination.offset) { + url.searchParams.set("start", pagination.offset); } + const order = !pagination || pagination.order === "asc" ? 1 : -1; + const limit = + !pagination || !pagination.limit || pagination.limit === 0 + ? 5 + : Math.abs(pagination.limit); + //always send delta + url.searchParams.set("delta", String(order * limit)); +} + +export function addMerchantPaginationParams( + url: URL, + pagination?: PaginationParams, +) { + if (!pagination) return; if (pagination.offset) { - url.searchParams.set("start", pagination.offset) + url.searchParams.set("offset", pagination.offset); } - const order = !pagination || pagination.order === "asc" ? 1 : -1 - const limit = !pagination || !pagination.limit || pagination.limit === 0 ? 5 : Math.abs(pagination.limit) + const order = !pagination || pagination.order === "asc" ? 1 : -1; + const limit = + !pagination || !pagination.limit || pagination.limit === 0 + ? 5 + : Math.abs(pagination.limit); //always send delta - url.searchParams.set("delta", String(order * limit)) + url.searchParams.set("limit", String(order * limit)); +} + +export function addLongPollingParam(url: URL, param?: LongPollParams) { + if (!param) return; + if (param.timeoutMs) { + url.searchParams.set("long_poll_ms", String(param.timeoutMs)); + } +} + +export interface CacheEvictor<T> { + notifySuccess: (op: T) => Promise<void>; +} + +export const nullEvictor: CacheEvictor<unknown> = { + notifySuccess: () => Promise.resolve(), +}; + +export class IdempotencyRetry { + public readonly uid: string; + public readonly timesLeft: number; + public readonly maxTries: number; + + private constructor(timesLeft: number, maxTimesLeft: number) { + this.timesLeft = timesLeft; + this.maxTries = maxTimesLeft; + this.uid = encodeCrock(getRandomBytes(32)) + } + + static tryFiveTimes() { + return new IdempotencyRetry(5, 5) + } + + next(): IdempotencyRetry | undefined { + const left = this.timesLeft -1 + if (left <= 0) { + return undefined + } + return new IdempotencyRetry(left, this.maxTries); + } } |