commit d8b695258e97411d1921f03b83703419aa0bfd88
parent d6a78b960c60d748d1155c51ea86058ec2e6097c
Author: Florian Dold <florian@dold.me>
Date: Tue, 12 May 2026 11:41:43 +0200
fix typing issue in HTTP clients
For an empty response body, we should not return OperationOk<void>, as
void will be absorbed by the type union operator, leading to missed
checks for the "no result" case.
Diffstat:
8 files changed, 165 insertions(+), 90 deletions(-)
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-tokens-discount.ts b/packages/taler-harness/src/integrationtests/test-wallet-tokens-discount.ts
@@ -86,9 +86,7 @@ export async function runWalletTokensDiscountTest(t: GlobalTestState) {
Duration.fromSpec({ years: 1 }),
),
),
- duration: Duration.toTalerProtocolDuration(
- Duration.fromSpec({ days: 90 }),
- ),
+ duration: Duration.toTalerProtocolDuration(Duration.fromSpec({ days: 90 })),
validity_granularity: Duration.toTalerProtocolDuration(
Duration.fromSpec({ days: 1 }),
),
@@ -149,9 +147,9 @@ export async function runWalletTokensDiscountTest(t: GlobalTestState) {
merchantAdminAccessToken,
});
- const {discounts} = await walletClient.call(
+ const { discounts } = await walletClient.call(
WalletApiOperation.ListDiscounts,
- {}
+ {},
);
t.assertTrue(discounts.length === 1);
@@ -168,9 +166,9 @@ export async function runWalletTokensDiscountTest(t: GlobalTestState) {
merchantAdminAccessToken,
});
- let {discounts} = await walletClient.call(
+ let { discounts } = await walletClient.call(
WalletApiOperation.ListDiscounts,
- {}
+ {},
);
t.assertTrue(discounts.length === 1);
@@ -181,7 +179,7 @@ export async function runWalletTokensDiscountTest(t: GlobalTestState) {
// change name of token family
tokenFamilyJson.name = "Test discount, but different name";
- succeedOrThrow<TokenFamilyDetails | void>(
+ succeedOrThrow<TokenFamilyDetails | undefined>(
await merchantApi.updateTokenFamily(
merchantAdminAccessToken,
tokenFamilyJson.slug,
@@ -204,16 +202,18 @@ export async function runWalletTokensDiscountTest(t: GlobalTestState) {
merchantAdminAccessToken,
});
- const {discounts} = await walletClient.call(
+ const { discounts } = await walletClient.call(
WalletApiOperation.ListDiscounts,
- {}
+ {},
);
- const d1 = discounts.find(d => d.name === "Test discount");
+ const d1 = discounts.find((d) => d.name === "Test discount");
t.assertTrue(d1 !== undefined);
t.assertTrue(d1.tokensAvailable === 2);
- d2 = discounts.find(d => d.name === "Test discount, but different name");
+ d2 = discounts.find(
+ (d) => d.name === "Test discount, but different name",
+ );
t.assertTrue(d2 !== undefined);
t.assertTrue(d2.tokensAvailable === 1);
}
@@ -222,14 +222,13 @@ export async function runWalletTokensDiscountTest(t: GlobalTestState) {
logger.info(`Deleting token family with hash ${d2.tokenFamilyHash}`);
// delete token family with different name
- await walletClient.call(
- WalletApiOperation.DeleteDiscount,
- { tokenFamilyHash: d2.tokenFamilyHash },
- );
+ await walletClient.call(WalletApiOperation.DeleteDiscount, {
+ tokenFamilyHash: d2.tokenFamilyHash,
+ });
- const {discounts} = await walletClient.call(
+ const { discounts } = await walletClient.call(
WalletApiOperation.ListDiscounts,
- {}
+ {},
);
t.assertTrue(discounts.length === 1);
@@ -237,12 +236,12 @@ export async function runWalletTokensDiscountTest(t: GlobalTestState) {
}
async function createAndPayOrder(req: {
- orderJson: Order,
- choiceIndex: number,
- t: GlobalTestState,
- walletClient: WalletClient,
- merchantApi: TalerMerchantInstanceHttpClient,
- merchantAdminAccessToken: AccessToken,
+ orderJson: Order;
+ choiceIndex: number;
+ t: GlobalTestState;
+ walletClient: WalletClient;
+ merchantApi: TalerMerchantInstanceHttpClient;
+ merchantAdminAccessToken: AccessToken;
}) {
const orderResp = succeedOrThrow(
await req.merchantApi.createOrder(req.merchantAdminAccessToken, {
@@ -277,7 +276,10 @@ async function createAndPayOrder(req: {
choiceIndex: req.choiceIndex,
});
- await req.walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+ await req.walletClient.call(
+ WalletApiOperation.TestingWaitTransactionsFinal,
+ {},
+ );
}
runWalletTokensDiscountTest.suites = ["merchant", "wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-web-merchant-login.ts b/packages/taler-harness/src/integrationtests/test-web-merchant-login.ts
@@ -39,7 +39,7 @@ export async function runWebMerchantLoginTest(t: GlobalTestState) {
const title = await browser.getTitle();
- t.assertDeepEqual("GNU Taler Merchant Backoffice", title);
+ t.assertDeepEqual("Taler Merchant Portal", title);
await browser.manage().setTimeouts({ implicit: 2000 });
diff --git a/packages/taler-util/src/http-client/bank-core.ts b/packages/taler-util/src/http-client/bank-core.ts
@@ -818,7 +818,7 @@ export class TalerCoreBankHttpClient {
): Promise<
| OperationFail<HttpStatusCode.NotFound>
| OperationAlternative<HttpStatusCode.Accepted, ChallengeResponse>
- | OperationOk<void>
+ | OperationOk<undefined>
| OperationFail<HttpStatusCode.BadRequest>
| OperationFail<TalerErrorCode.BANK_UNALLOWED_DEBIT>
| OperationFail<TalerErrorCode.BANK_CONFIRM_ABORT_CONFLICT>
diff --git a/packages/taler-util/src/http-client/exchange-client.ts b/packages/taler-util/src/http-client/exchange-client.ts
@@ -80,6 +80,7 @@ import {
LegitimizationNeededResponse,
PurseConflict,
PurseConflictPartial,
+ PurseCreateSuccessResponse,
WalletKycCheckResponse,
WalletKycRequest,
codecForAccountKycStatus,
@@ -104,6 +105,7 @@ import {
codecForLegitimizationNeededResponse,
codecForPurseConflict,
codecForPurseConflictPartial,
+ codecForPurseCreateSuccessResponse,
} from "../types-taler-exchange.js";
import {
CacheEvictor,
@@ -322,7 +324,7 @@ export class TalerExchangeHttpClient {
pursePub: string,
body: any, // FIXME
): Promise<
- | OperationOk<void>
+ | OperationOk<PurseCreateSuccessResponse>
| OperationFail<HttpStatusCode.Forbidden>
| OperationFail<HttpStatusCode.NotFound>
| OperationAlternative<HttpStatusCode.Conflict, PurseConflict>
@@ -334,8 +336,7 @@ export class TalerExchangeHttpClient {
});
switch (resp.status) {
case HttpStatusCode.Ok:
- // FIXME: parse PurseCreateSuccessResponse
- return opSuccessFromHttp(resp, codecForAny());
+ return opSuccessFromHttp(resp, codecForPurseCreateSuccessResponse());
case HttpStatusCode.Conflict:
return opKnownAlternativeHttpFailure(
resp,
@@ -359,7 +360,7 @@ export class TalerExchangeHttpClient {
pursePub: string,
purseSig: string,
): Promise<
- | OperationOk<void>
+ | OperationOk<undefined>
| OperationFail<HttpStatusCode.NotFound>
| OperationFail<HttpStatusCode.Conflict>
| OperationFail<HttpStatusCode.Forbidden>
@@ -442,7 +443,7 @@ export class TalerExchangeHttpClient {
pursePub: string,
body: ExchangeReservePurseRequest,
): Promise<
- | OperationOk<void>
+ | OperationOk<PurseCreateSuccessResponse>
| OperationFail<HttpStatusCode.PaymentRequired>
| OperationFail<HttpStatusCode.Forbidden>
| OperationFail<HttpStatusCode.NotFound>
@@ -459,8 +460,7 @@ export class TalerExchangeHttpClient {
});
switch (resp.status) {
case HttpStatusCode.Ok:
- // FIXME: parse PurseCreateSuccessResponse
- return opSuccessFromHttp(resp, codecForAny());
+ return opSuccessFromHttp(resp, codecForPurseCreateSuccessResponse());
case HttpStatusCode.PaymentRequired:
case HttpStatusCode.Forbidden:
case HttpStatusCode.NotFound:
@@ -571,7 +571,7 @@ export class TalerExchangeHttpClient {
body: WalletKycRequest,
): Promise<
| OperationOk<WalletKycCheckResponse>
- | OperationOk<void>
+ | OperationOk<undefined>
| OperationFail<HttpStatusCode.Forbidden>
| OperationAlternative<
HttpStatusCode.UnavailableForLegalReasons,
@@ -828,7 +828,7 @@ export class TalerExchangeHttpClient {
requirement: KycRequirementInformationId,
body: T,
): Promise<
- | OperationOk<void>
+ | OperationOk<undefined>
| OperationFail<HttpStatusCode.Conflict>
| OperationFail<HttpStatusCode.InternalServerError>
| OperationFail<HttpStatusCode.NotFound>
diff --git a/packages/taler-util/src/http-client/mailbox.ts b/packages/taler-util/src/http-client/mailbox.ts
@@ -16,33 +16,33 @@
import {
CancellationToken,
+ EddsaSignatureString,
FailCasesByMethod,
HttpStatusCode,
LibtoolVersion,
+ MailboxConfiguration,
+ MailboxMetadata,
+ MailboxRegisterRequest,
+ MailboxRegisterResult,
+ OperationAlternative,
OperationFail,
OperationOk,
ResultByMethod,
TalerMailboxApi,
carefullyParseConfig,
+ codecForEmptyObject,
codecForTalerMailboxConfigResponse,
+ codecForTalerMailboxMetadata,
codecForTalerMailboxRateLimitedResponse,
+ decodeCrock,
+ eddsaGetPublic,
+ encodeCrock,
opEmptySuccess,
opFixedSuccess,
opKnownAlternativeHttpFailure,
opKnownHttpFailure,
- opUnknownHttpFailure,
- MailboxMetadata,
- codecForTalerMailboxMetadata,
opSuccessFromHttp,
- MailboxRegisterRequest,
- encodeCrock,
- decodeCrock,
- codecForEmptyObject,
- MailboxConfiguration,
- eddsaGetPublic,
- EddsaSignatureString,
- MailboxRegisterResult,
- OperationAlternative,
+ opUnknownHttpFailure,
} from "@gnu-taler/taler-util";
import {
HttpRequestLibrary,
@@ -96,11 +96,10 @@ export class TalerMailboxInstanceHttpClient {
/**
* https://docs.taler.net/core/api-mailbox.html#get--config
*/
- async getConfig() :
- Promise<
- | OperationOk<TalerMailboxApi.TalerMailboxConfigResponse>
+ async getConfig(): Promise<
+ | OperationOk<TalerMailboxApi.TalerMailboxConfigResponse>
| OperationFail<HttpStatusCode.NotFound>
- >{
+ > {
const url = new URL(`/config`, this.baseUrl);
const resp = await this.httpLib.fetch(url.href, {
method: "GET",
@@ -127,7 +126,7 @@ export class TalerMailboxInstanceHttpClient {
h_address: string;
body: Uint8Array;
}): Promise<
- | OperationOk<void>
+ | OperationOk<undefined>
| OperationFail<TalerMailboxApi.MailboxRateLimitedResponse>
| OperationFail<HttpStatusCode.PaymentRequired>
| OperationFail<HttpStatusCode.TooManyRequests>
@@ -153,7 +152,11 @@ export class TalerMailboxInstanceHttpClient {
return opKnownHttpFailure(resp.status, resp);
}
case HttpStatusCode.TooManyRequests: {
- return opKnownAlternativeHttpFailure(resp, resp.status, codecForTalerMailboxRateLimitedResponse());
+ return opKnownAlternativeHttpFailure(
+ resp,
+ resp.status,
+ codecForTalerMailboxRateLimitedResponse(),
+ );
}
default:
return opUnknownHttpFailure(resp);
@@ -167,9 +170,8 @@ export class TalerMailboxInstanceHttpClient {
hMailbox: string;
}): Promise<
| OperationOk<MailboxMessagesResponseRaw>
- | OperationOk<void>
| OperationFail<HttpStatusCode.TooManyRequests>
- >{
+ > {
const { hMailbox: hMailbox } = args;
const url = new URL(`${hMailbox.toUpperCase()}`, this.baseUrl);
@@ -180,16 +182,22 @@ export class TalerMailboxInstanceHttpClient {
switch (resp.status) {
case HttpStatusCode.Ok: {
- const uintar = await resp.bytes() as Uint8Array;
- const etag = resp.headers.get("etag")
- const index = etag? etag : "0";
- return opFixedSuccess({messages: uintar, etag: index});
+ const uintar = (await resp.bytes()) as Uint8Array;
+ const etag = resp.headers.get("etag");
+ const index = etag ? etag : "0";
+ return opFixedSuccess({ messages: uintar, etag: index });
}
case HttpStatusCode.NoContent: {
- return opEmptySuccess();
+ const etag = resp.headers.get("etag");
+ const index = etag ? etag : "0";
+ return opFixedSuccess({ messages: new Uint8Array(), etag: index });
}
case HttpStatusCode.TooManyRequests: {
- return opKnownAlternativeHttpFailure(resp, resp.status, codecForTalerMailboxRateLimitedResponse());
+ return opKnownAlternativeHttpFailure(
+ resp,
+ resp.status,
+ codecForTalerMailboxRateLimitedResponse(),
+ );
}
default:
return opUnknownHttpFailure(resp);
@@ -205,20 +213,30 @@ export class TalerMailboxInstanceHttpClient {
count: number;
signature: EddsaSignatureString;
}): Promise<
- | OperationOk<void>
+ | OperationOk<undefined>
| OperationFail<HttpStatusCode.Forbidden>
| OperationFail<HttpStatusCode.NotFound>
- >{
- const { mailboxConf, matchIf: etag, count: count, signature: signature } = args;
- const mailboxPubkeyString = encodeCrock(eddsaGetPublic(decodeCrock(mailboxConf.privateKey)));
- const url = new URL(`${mailboxPubkeyString.toUpperCase()}?count=${count}`, this.baseUrl);
-
+ > {
+ const {
+ mailboxConf,
+ matchIf: etag,
+ count: count,
+ signature: signature,
+ } = args;
+ const mailboxPubkeyString = encodeCrock(
+ eddsaGetPublic(decodeCrock(mailboxConf.privateKey)),
+ );
+ const url = new URL(
+ `${mailboxPubkeyString.toUpperCase()}?count=${count}`,
+ this.baseUrl,
+ );
const resp = await this.httpLib.fetch(url.href, {
method: "DELETE",
headers: {
- "If-Match" : etag,
- "Taler-Mailbox-Delete-Signature" : signature},
+ "If-Match": etag,
+ "Taler-Mailbox-Delete-Signature": signature,
+ },
cancellationToken: this.cancellationToken,
});
@@ -229,7 +247,7 @@ export class TalerMailboxInstanceHttpClient {
case HttpStatusCode.Forbidden:
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
- default:
+ default:
return opUnknownHttpFailure(resp);
}
}
@@ -237,11 +255,13 @@ export class TalerMailboxInstanceHttpClient {
/**
* https://docs.taler.net/core/api-mailbox.html#get--info-$H_MAILBOX
*/
- async getMailboxInfo(hMailbox: string) : Promise<
+ async getMailboxInfo(
+ hMailbox: string,
+ ): Promise<
| OperationOk<MailboxMetadata>
- | OperationFail<HttpStatusCode.NotFound>
- | OperationFail<HttpStatusCode.TooManyRequests>
- >{
+ | OperationFail<HttpStatusCode.NotFound>
+ | OperationFail<HttpStatusCode.TooManyRequests>
+ > {
const url = new URL(`info/${hMailbox.toUpperCase()}`, this.baseUrl);
const resp = await this.httpLib.fetch(url.href, {
@@ -251,16 +271,23 @@ export class TalerMailboxInstanceHttpClient {
switch (resp.status) {
case HttpStatusCode.Ok: {
- return opSuccessFromHttp(resp,
- codecForTalerMailboxMetadata());
+ return opSuccessFromHttp(resp, codecForTalerMailboxMetadata());
}
case HttpStatusCode.NotFound: {
- return opKnownAlternativeHttpFailure(resp, resp.status, codecForEmptyObject());
+ return opKnownAlternativeHttpFailure(
+ resp,
+ resp.status,
+ codecForEmptyObject(),
+ );
}
case HttpStatusCode.TooManyRequests: {
- return opKnownAlternativeHttpFailure(resp, resp.status, codecForTalerMailboxRateLimitedResponse());
+ return opKnownAlternativeHttpFailure(
+ resp,
+ resp.status,
+ codecForTalerMailboxRateLimitedResponse(),
+ );
}
- default:
+ default:
return opUnknownHttpFailure(resp);
}
}
@@ -268,11 +295,16 @@ export class TalerMailboxInstanceHttpClient {
/**
* https://docs.taler.net/core/api-mailbox.html#post--register
*/
- async registerMailbox(req: MailboxRegisterRequest) : Promise<
+ async registerMailbox(
+ req: MailboxRegisterRequest,
+ ): Promise<
| OperationOk<MailboxRegisterResult>
| OperationFail<HttpStatusCode.Forbidden>
- | OperationAlternative<HttpStatusCode.PaymentRequired, MailboxRegisterResult>
- >{
+ | OperationAlternative<
+ HttpStatusCode.PaymentRequired,
+ MailboxRegisterResult
+ >
+ > {
const url = new URL(`register`, this.baseUrl);
const resp = await this.httpLib.fetch(url.href, {
@@ -283,13 +315,20 @@ export class TalerMailboxInstanceHttpClient {
switch (resp.status) {
case HttpStatusCode.NoContent: {
- return opFixedSuccess({status: "ok"} as MailboxRegisterResult);
+ return opFixedSuccess({ status: "ok" } as MailboxRegisterResult);
}
case HttpStatusCode.Forbidden: {
return opKnownHttpFailure(resp.status, resp);
}
case HttpStatusCode.PaymentRequired: {
- return { type: "fail", case: resp.status, body: { status: "payment-required", talerUri: resp.headers.get("Taler")} as MailboxRegisterResult};
+ return {
+ type: "fail",
+ case: resp.status,
+ body: {
+ status: "payment-required",
+ talerUri: resp.headers.get("Taler"),
+ } as MailboxRegisterResult,
+ };
}
default:
return opUnknownHttpFailure(resp);
diff --git a/packages/taler-util/src/http-client/merchant.ts b/packages/taler-util/src/http-client/merchant.ts
@@ -32,8 +32,8 @@ import {
PaginationParams,
ResultByMethod,
TalerErrorCode,
- TalerErrorDetail,
TalerMerchantApi,
+ TokenFamilyDetails,
assertUnreachable,
carefullyParseConfig,
codecForAbortResponse,
@@ -2718,7 +2718,12 @@ export class TalerMerchantInstanceHttpClient {
token: AccessToken,
tokenSlug: string,
body: TalerMerchantApi.TokenFamilyUpdateRequest,
- ) {
+ ): Promise<
+ | OperationOk<undefined>
+ | OperationOk<TokenFamilyDetails>
+ | OperationFail<HttpStatusCode.NotFound>
+ | OperationFail<HttpStatusCode.Unauthorized>
+ > {
const url = new URL(`private/tokenfamilies/${tokenSlug}`, this.baseUrl);
const headers: Record<string, string> = {};
@@ -2995,7 +3000,9 @@ export class TalerMerchantInstanceHttpClient {
async postDonau(args: {
body: MerchantPostDonauBody;
token?: AccessToken;
- }): Promise<OperationOk<void> | OperationFail<HttpStatusCode.BadGateway>> {
+ }): Promise<
+ OperationOk<undefined> | OperationFail<HttpStatusCode.BadGateway>
+ > {
const headers: Record<string, string> = {};
if (args.token) {
headers.Authorization = makeBearerTokenAuthHeader(args.token);
diff --git a/packages/taler-util/src/operation.ts b/packages/taler-util/src/operation.ts
@@ -107,8 +107,8 @@ export function opFixedSuccess<T>(body: T): OperationOk<T> {
return { type: "ok" as const, case: "ok", body };
}
-export function opEmptySuccess(): OperationOk<void> {
- return { type: "ok" as const, case: "ok", body: void 0 };
+export function opEmptySuccess(): OperationOk<undefined> {
+ return { type: "ok" as const, case: "ok", body: undefined };
}
export function opKnownFailure<const T>(case_: T): OperationFail<T> {
diff --git a/packages/taler-util/src/types-taler-exchange.ts b/packages/taler-util/src/types-taler-exchange.ts
@@ -1239,6 +1239,33 @@ export const codecForExchangeMergeSuccessResponse =
.property("exchange_pub", codecForEddsaPublicKey())
.build("ExchangeMergeSuccessResponse");
+export interface PurseCreateSuccessResponse {
+ // Total amount deposited into the purse so far (without fees).
+ total_deposited: AmountString;
+
+ // Time at the exchange.
+ exchange_timestamp: Timestamp;
+
+ // EdDSA signature of the exchange affirming the payment,
+ // of purpose TALER_SIGNATURE_PURSE_DEPOSIT_CONFIRMED
+ // over a TALER_PurseDepositConfirmedSignaturePS.
+ // Signs over the above and the purse public key and
+ // the hash of the contract terms.
+ exchange_sig: EddsaSignature;
+
+ // public key used to create the signature.
+ exchange_pub: EddsaPublicKey;
+}
+
+export const codecForPurseCreateSuccessResponse =
+ (): Codec<PurseCreateSuccessResponse> =>
+ buildCodecForObject<PurseCreateSuccessResponse>()
+ .property("total_deposited", codecForAmountString())
+ .property("exchange_timestamp", codecForTimestamp)
+ .property("exchange_sig", codecForEddsaSignature())
+ .property("exchange_pub", codecForEddsaPublicKey())
+ .build("PurseCreateSuccessResponse");
+
/**
* Doc name: api-exchange/MergeConflict
*/