commit 202048215d45dd36ee3d637d003b81f77c7d4586
parent 76391c92a1509a1588e3c811dca2dbf203d2e7a7
Author: Sebastian <sebasjm@gmail.com>
Date: Thu, 20 Mar 2025 16:10:09 -0300
update corebank api
Diffstat:
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",