taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 202048215d45dd36ee3d637d003b81f77c7d4586
parent 76391c92a1509a1588e3c811dca2dbf203d2e7a7
Author: Sebastian <sebasjm@gmail.com>
Date:   Thu, 20 Mar 2025 16:10:09 -0300

update corebank api

Diffstat:
Mpackages/taler-util/src/http-client/bank-core.ts | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mpackages/taler-util/src/http-client/utils.ts | 3---
Mpackages/taler-util/src/types-taler-common.ts | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Mpackages/taler-util/src/types-taler-corebank.ts | 12++++++++++++
4 files changed, 144 insertions(+), 9 deletions(-)

diff --git a/packages/taler-util/src/http-client/bank-core.ts b/packages/taler-util/src/http-client/bank-core.ts @@ -108,7 +108,7 @@ export enum TalerCoreBankCacheEviction { * Uses libtool's current:revision:age versioning. */ export class TalerCoreBankHttpClient { - public readonly PROTOCOL_VERSION = "4:0:0"; + public readonly PROTOCOL_VERSION = "8:0:0"; httpLib: HttpRequestLibrary; cacheEvictor: CacheEvictor<TalerCoreBankCacheEviction>; @@ -126,25 +126,46 @@ export class TalerCoreBankHttpClient { return compare?.compatible ?? false; } + /** + * https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-token + */ async createAccessTokenBasic( username: string, password: string, body: TokenRequest, + cid?: string, ) { const url = new URL(`accounts/${username}/token`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "POST", headers: { Authorization: makeBasicAuthHeader(username, password), + "X-Challenge-Id": cid, }, body, }); switch (resp.status) { case HttpStatusCode.Ok: return opSuccessFromHttp(resp, codecForTokenSuccessResponse()); - //FIXME: missing in docs + case HttpStatusCode.Accepted: + return opKnownAlternativeFailure( + resp, + resp.status, + codecForChallenge(), + ); case HttpStatusCode.Unauthorized: return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Forbidden: { + const details = await readTalerErrorResponse(resp); + switch (details.code) { + case TalerErrorCode.GENERIC_FORBIDDEN: + return opKnownTalerFailure(details.code, details); + case TalerErrorCode.BANK_ACCOUNT_LOCKED: + return opKnownTalerFailure(details.code, details); + default: + return opUnknownFailure(resp, details); + } + } case HttpStatusCode.NotFound: return opKnownHttpFailure(resp.status, resp); default: @@ -152,6 +173,9 @@ export class TalerCoreBankHttpClient { } } + /** + * https://docs.taler.net/core/api-corebank.html#delete--accounts-$USERNAME-token + */ async deleteAccessToken(user: string, token: AccessToken) { const url = new URL(`accounts/${user}/token`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { @@ -175,6 +199,31 @@ export class TalerCoreBankHttpClient { } /** + * https://docs.taler.net/core/api-corebank.html#get--accounts-$USERNAME-tokens + * + */ + async getAccessTokenList( + user: string, + pagination?: PaginationParams, + ) { + const url = new URL(`accounts/${user}/token`, this.baseUrl); + addPaginationParams(url, pagination); + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + }); + switch (resp.status) { + 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#config * */ @@ -1032,8 +1081,12 @@ export class TalerCoreBankHttpClient { return opSuccessFromHttp(resp, codecForTanTransmission()); case HttpStatusCode.Unauthorized: return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Forbidden: + return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.NotFound: return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.TooManyRequests: + return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.BadGateway: { const details = await readTalerErrorResponse(resp); switch (details.code) { diff --git a/packages/taler-util/src/http-client/utils.ts b/packages/taler-util/src/http-client/utils.ts @@ -72,9 +72,6 @@ export function createAuthorizationHeader(auth?: BasicOrTokenAuth): string | und return undefined; } -/** - * https://bugs.gnunet.org/view.php?id=7949 - */ export function addPaginationParams(url: URL, pagination?: PaginationParams) { if (!pagination) return; if (pagination.offset) { diff --git a/packages/taler-util/src/types-taler-common.ts b/packages/taler-util/src/types-taler-common.ts @@ -38,7 +38,13 @@ import { codecForNumber, codecForString, } from "./codec.js"; -import { EddsaPrivP, ReservePub } from "./index.js"; +import { + codecForEither, + codecForList, + codecOptional, + EddsaPrivP, + ReservePub, +} from "./index.js"; import { TalerProtocolDuration, TalerProtocolTimestamp, @@ -347,10 +353,12 @@ export const codecForCoinHistoryResponse = () => .property("history", codecForAny()) .build("CoinHistoryResponse"); +export type TokenScope = "readonly" | "readwrite" | "revenue" | "wiregateway"; + export interface TokenRequest { // Service-defined scope for the token. // Typical scopes would be "readonly" or "readwrite". - scope: string; + scope: TokenScope; // Server may impose its own upper bound // on the token validity duration @@ -383,11 +391,68 @@ export interface TokenSuccessResponseMerchant { // Opque access token. token: AccessToken; - scope: string; + scope: TokenScope; + + refreshable: boolean; +} + +export interface TokenInfos { + tokens: TokenInfo[]; +} + +export interface TokenInfo { + // Time when the token was created. + creation_time: Timestamp; + + // Expiration determined by the server. + // Can be based on the token_duration + // from the request, but ultimately the + // server decides the expiration. + expiration: Timestamp; + + // Scope for the token. + scope: TokenScope; + // Is the token refreshable into a new token during its + // validity? + // Refreshable tokens effectively provide indefinite + // access if they are refreshed in time. refreshable: boolean; + + // Optional token description + description?: string; + + // Time when the token was last used. + last_access: Timestamp; + + // Opaque unique ID used for pagination. + row_id: Integer; } +export const codecForTokenInfo = (): Codec<TokenInfo> => + buildCodecForObject<TokenInfo>() + .property("creation_time", codecForTimestamp) + .property("expiration", codecForTimestamp) + .property( + "scope", + codecForEither( + codecForConstString("readonly"), + codecForConstString("readwrite"), + codecForConstString("revenue"), + codecForConstString("wiregateway"), + ), + ) + .property("refreshable", codecForBoolean()) + .property("description", codecOptional(codecForString())) + .property("last_access", codecForTimestamp) + .property("row_id", codecForNumber()) + .build("TokenInfo"); + +export const codecForTokenInfoList = (): Codec<TokenInfos> => + buildCodecForObject<TokenInfos>() + .property("tokens", codecForList(codecForTokenInfo())) + .build("TokenInfoList"); + //FIXME: implement this codec export const codecForAccessToken = codecForString as () => Codec<AccessToken>; export const codecForTokenSuccessResponse = (): Codec<TokenSuccessResponse> => @@ -401,7 +466,15 @@ export const codecForTokenSuccessResponseMerchant = buildCodecForObject<TokenSuccessResponseMerchant>() .property("token", codecForAccessToken()) .property("expiration", codecForTimestamp) - .property("scope", codecForString()) + .property( + "scope", + codecForEither( + codecForConstString("readonly"), + codecForConstString("readwrite"), + codecForConstString("revenue"), + codecForConstString("wiregateway"), + ), + ) .property("refreshable", codecForBoolean()) .build("TalerAuthentication.TokenSuccessResponseMerchant"); diff --git a/packages/taler-util/src/types-taler-corebank.ts b/packages/taler-util/src/types-taler-corebank.ts @@ -426,6 +426,11 @@ export interface AccountMinimalData { // @since v4, will become mandatory in the future. row_id?: Integer; + // Is the account locked. + // Defaults to false. + // @since **v7** + is_locked?: boolean; + // Current status of the account // active: the account can be used // deleted: the account has been deleted but is retained for compliance @@ -469,6 +474,11 @@ export interface AccountData { // Is this a taler exchange account? is_taler_exchange: boolean; + // Is the account locked. + // Defaults to false. + // @since **v7** + is_locked?: boolean; + // Is 2FA enabled and what channel is used for challenges? tan_channel?: TanChannel; @@ -751,6 +761,7 @@ export const codecForAccountMinimalData = (): Codec<AccountMinimalData> => .property("row_id", codecForNumber()) .property("debit_threshold", codecForAmountString()) .property("min_cashout", codecOptional(codecForAmountString())) + .property("is_locked", codecOptional(codecForBoolean())) .property("is_public", codecForBoolean()) .property("is_taler_exchange", codecForBoolean()) .property( @@ -780,6 +791,7 @@ export const codecForAccountData = (): Codec<AccountData> => .property("contact_data", codecOptional(codecForChallengeContactData())) .property("cashout_payto_uri", codecOptional(codecForPaytoString())) .property("is_public", codecForBoolean()) + .property("is_locked", codecOptional(codecForBoolean())) .property("is_taler_exchange", codecForBoolean()) .property( "tan_channel",