taler-typescript-core

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

commit 344cdaef8fa1f75c859f40a0a847d8a39cdf3d1a
parent cf65329a025237308e6a28cd321bac7f05769a22
Author: Martin Schanzenbach <schanzen@gnunet.org>
Date:   Thu, 13 Nov 2025 13:50:28 +0100

wallet-core: support paid mailbox registrations (API)

Diffstat:
Mpackages/taler-util/src/http-client/mailbox.ts | 14++++++++------
Mpackages/taler-util/src/types-taler-mailbox.ts | 41++++++++++++++++++++++++++++++++++++++++-
Mpackages/taler-util/src/types-taler-wallet.ts | 3++-
Mpackages/taler-wallet-core/src/mailbox.ts | 50++++++++++++++++++++++++++++++++++++++------------
Mpackages/taler-wallet-core/src/wallet.ts | 4++--
Mpackages/taler-wallet-webextension/src/wallet/Mailbox.tsx | 8+++++---
6 files changed, 95 insertions(+), 25 deletions(-)

diff --git a/packages/taler-util/src/http-client/mailbox.ts b/packages/taler-util/src/http-client/mailbox.ts @@ -31,7 +31,7 @@ import { opKnownAlternativeHttpFailure, opKnownHttpFailure, opUnknownHttpFailure, - MailboxMetadata as MailboxMetadata, + MailboxMetadata, codecForTalerMailboxMetadata, opSuccessFromHttp, MailboxRegisterRequest, @@ -41,6 +41,8 @@ import { MailboxConfiguration, eddsaGetPublic, EddsaSignatureString, + MailboxRegisterResult, + OperationAlternative, } from "@gnu-taler/taler-util"; import { HttpRequestLibrary, @@ -266,10 +268,10 @@ export class TalerMailboxInstanceHttpClient { /** * https://docs.taler.net/core/api-mailbox.html#post--register */ - async updateRegistration(req: MailboxRegisterRequest) : Promise< - | OperationOk<void> + async registerMailbox(req: MailboxRegisterRequest) : Promise< + | OperationOk<MailboxRegisterResult> | OperationFail<HttpStatusCode.Forbidden> - | OperationFail<HttpStatusCode.PaymentRequired> + | OperationAlternative<HttpStatusCode.PaymentRequired, MailboxRegisterResult> >{ const url = new URL(`register`, this.baseUrl); @@ -281,13 +283,13 @@ export class TalerMailboxInstanceHttpClient { switch (resp.status) { case HttpStatusCode.NoContent: { - return opEmptySuccess(); + return opFixedSuccess({status: "ok"} as MailboxRegisterResult); } case HttpStatusCode.Forbidden: { return opKnownHttpFailure(resp.status, resp); } case HttpStatusCode.PaymentRequired: { - return opKnownHttpFailure(resp.status, resp); + 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/types-taler-mailbox.ts b/packages/taler-util/src/types-taler-mailbox.ts @@ -20,7 +20,9 @@ import { codecForAmountString } from "./amounts.js"; import { Codec, buildCodecForObject, + buildCodecForUnion, codecForNumber, + codecOptional, } from "./codec.js"; import { codecForString, @@ -85,7 +87,44 @@ export interface TalerMailboxConfigResponse { monthly_fee: AmountString; } -export const codecForTalerMailboxMessageKeysUpdateRequest = +export type MailboxRegisterResult = + | MailboxRegisterOk + | MailboxRegisterPaymentRequired; + +export interface MailboxRegisterOk { + status: "ok"; +} + +export interface MailboxRegisterPaymentRequired { + status: "payment-required"; + talerUri?: string; +} + +export const codecForMailboxRegisterOk = (): Codec<MailboxRegisterOk> => + buildCodecForObject<MailboxRegisterOk>() + .property("status", codecForConstString("ok")) + .build("MailboxRegisterOk"); + +export const codecForMailboxRegisterPaymentRequired = + (): Codec<MailboxRegisterPaymentRequired> => + buildCodecForObject<MailboxRegisterPaymentRequired>() + .property("status", codecForConstString("payment-required")) + .property("talerUri", codecOptional(codecForString())) + .build("MailboxRegisterPaymentRequired"); + +export const codecForMailboxRegisterResult = + (): Codec<MailboxRegisterResult> => + buildCodecForUnion<MailboxRegisterResult>() + .discriminateOn("status") + .alternative("ok", codecForMailboxRegisterOk()) + .alternative( + "payment-required", + codecForMailboxRegisterPaymentRequired(), + ) + .build("MailboxRegisterResult"); + + +export const codecForTalerMailboxRegisterRequest = (): Codec<MailboxRegisterRequest> => buildCodecForObject<MailboxRegisterRequest>() .property("mailbox_metadata", codecForTalerMailboxMetadata()) diff --git a/packages/taler-util/src/types-taler-wallet.ts b/packages/taler-util/src/types-taler-wallet.ts @@ -52,7 +52,7 @@ import { PaytoString, codecForPaytoString } from "./payto.js"; import { QrCodeSpec } from "./qr.js"; import { AgeCommitmentProof, HpkeSecretKey } from "./taler-crypto.js"; import { TalerErrorCode } from "./taler-error-codes.js"; -import { TemplateParams } from "./taleruri.js"; +import { TalerUri, TalerUriString, TemplateParams } from "./taleruri.js"; import { AbsoluteTime, DurationUnitSpec, @@ -1376,6 +1376,7 @@ export interface MailboxConfiguration { privateKey: EddsaPrivateKeyString; privateEncryptionKey: string; expiration: Timestamp; + payUri?: TalerUri; } export const codecForMailboxConfiguration = (): Codec<MailboxConfiguration> => diff --git a/packages/taler-wallet-core/src/mailbox.ts b/packages/taler-wallet-core/src/mailbox.ts @@ -45,10 +45,23 @@ import { hpkeSecretKeyGetPublic, MailboxMetadata, MailboxRegisterRequest, + MailboxRegisterResult, eddsaSign, Duration, AbsoluteTime, TalerSignaturePurpose, + HttpStatusCode, + Codec, + buildCodecForUnion, + buildCodecForObject, + codecForConstString, + codecOptional, + codecForString, + opKnownFailure, + TalerUris, + parseTalerUri, + Paytos, + succeedOrValue, } from "@gnu-taler/taler-util"; import { WalletExecutionContext, @@ -125,16 +138,14 @@ export async function listMailboxMessages( return { messages: messages }; } + /** - * Update mailbox keys - * Does not actually update key material in - * mailbox configuration, only updates service - * with new key info + * Register or update mailbox */ -export async function updateMailboxKeys( +export async function registerMailbox( wex: WalletExecutionContext, mailboxConf: MailboxConfiguration, -): Promise<void> { +): Promise<MailboxRegisterResult> { const privateSigningKey = decodeCrock(mailboxConf.privateKey); const signingKey = eddsaGetPublic(privateSigningKey); const encryptionKey = hpkeSecretKeyGetPublic(decodeCrock(mailboxConf.privateEncryptionKey)); @@ -169,8 +180,15 @@ export async function updateMailboxKeys( }; const mailboxClient = new TalerMailboxInstanceHttpClient(mailboxConf.mailboxBaseUrl, wex.http); - succeedOrThrow(await mailboxClient.updateRegistration(req)); - return; + const resp = await mailboxClient.registerMailbox(req); + switch (resp.case) { + case "ok": + return resp.body; + case HttpStatusCode.Forbidden: + throw Error("Access to Mailbox API unauthorized"); + case HttpStatusCode.PaymentRequired: + return resp.body; + } } /** @@ -194,9 +212,10 @@ export async function getMailbox( /** - * Initialize mailbox. + * Create new mailbox configuration locally and + * try to register it. */ -export async function initMailbox( +export async function createNewMailbox( wex: WalletExecutionContext, mailboxBaseUrl: string, ): Promise<MailboxConfiguration> { @@ -205,13 +224,20 @@ export async function initMailbox( const privKey = encodeCrock(keys.eddsaPriv); const nowInAYear = AbsoluteTime.addDuration(AbsoluteTime.now(), Duration.fromSpec({ years: 1})); - const mailboxConf: MailboxConfiguration = { + const mailboxConf: MailboxConfiguration = { mailboxBaseUrl: mailboxBaseUrl, privateKey: privKey, privateEncryptionKey: hpkeKey, expiration: AbsoluteTime.toProtocolTimestamp(nowInAYear) }; - await updateMailboxKeys(wex, mailboxConf); + + const resp = await registerMailbox(wex, mailboxConf); + if (resp.status == "payment-required") { + if (!resp.talerUri) { + throw Error("payment required to register mailbox but no Taler URI given"); + } + mailboxConf.payUri = succeedOrValue(TalerUris.fromString(resp.talerUri), undefined); + } await wex.db.runReadWriteTx( { storeNames: ["mailboxConfigurations"], diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts @@ -329,7 +329,7 @@ import { listMailboxMessages, addMailboxMessage, deleteMailboxMessage, - initMailbox, + createNewMailbox, sendTalerUriMessage, getMailbox, refreshMailbox @@ -1963,7 +1963,7 @@ const handlers: { [T in WalletApiOperation]: HandlerWithValidator<T> } = { }, [WalletApiOperation.InitializeMailbox]: { codec: codecForString(), - handler: initMailbox, + handler: createNewMailbox, }, [WalletApiOperation.GetMailbox]: { codec: codecForString(), diff --git a/packages/taler-wallet-webextension/src/wallet/Mailbox.tsx b/packages/taler-wallet-webextension/src/wallet/Mailbox.tsx @@ -26,10 +26,8 @@ import { sha512, MailboxMessageRecord, TranslatedString, - succeedOrThrow, succeedOrValue, TalerProtocolTimestamp, - AbsoluteTime, TalerUris, TalerUriAction, TalerUri @@ -120,10 +118,14 @@ export function MailboxPage({ /> ); } - const onInitializeMailbox = async () => { try { await api.wallet.call(WalletApiOperation.InitializeMailbox, mailboxBaseUrl); + // FIXME the returned mailbox may have a payto URI set + // In that case we need to display the option to pay AND + // Properly check on reload if we can clear the property (and store the + // updated mailbox struct) + // https://bugs.gnunet.org/view.php?id=10605 state?.retry(); } catch (err) { setError(i18n.str`Unexpected error when trying to initialize mailbox: ${err}`);