taler-typescript-core

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

commit 3f19a44c869b97622876c6a65583729c29cf3f4e
parent 3b35655e6ae8d78e7fc016ba58967412c2dde770
Author: Martin Schanzenbach <schanzen@gnunet.org>
Date:   Thu, 16 Jul 2026 11:14:59 +0200

add missing taldir files

Diffstat:
Apackages/taler-util/src/http-client/taldir.ts | 256+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/taler-util/src/types-taler-directory.ts | 114+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-util/src/types-taler-wallet.ts | 17+++++++++++++++++
Apackages/taler-wallet-core/src/taldir.ts | 165+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 552 insertions(+), 0 deletions(-)

diff --git a/packages/taler-util/src/http-client/taldir.ts b/packages/taler-util/src/http-client/taldir.ts @@ -0,0 +1,256 @@ +/* + This file is part of GNU Taler + (C) 2026 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 { + CancellationToken, + EmptyObject, + FailCasesByMethod, + HttpStatusCode, + LibtoolVersion, + OperationFail, + OperationOk, + PaymentResponse, + RelativeTime, + ResultByMethod, + TaldirAlreadyPaidResponse, + TaldirMailboxDetailResponse, + TaldirRateLimitedResponse, + TalerDirectoryApi, + TalerErrorDetail, + carefullyParseConfig, + codecForAny, + codecForTaldirAlreadyPaidResponse, + codecForTaldirMailboxDetailResponse, + codecForTalerDirectoryConfigResponse, + opFixedSuccess, + opKnownHttpFailure, + opSuccessFromHttp, + opUnknownHttpFailure, +} from "@gnu-taler/taler-util"; +import { + HttpRequestLibrary, + createPlatformHttpLib, +} from "@gnu-taler/taler-util/http"; + +export type TalerDirectoryInstanceResultByMethod< + prop extends keyof TalerDirectoryInstanceHttpClient, +> = ResultByMethod<TalerDirectoryInstanceHttpClient, prop>; +export type TalerDirectoryInstanceErrorsByMethod< + prop extends keyof TalerDirectoryInstanceHttpClient, +> = FailCasesByMethod<TalerDirectoryInstanceHttpClient, prop>; + +/** + * Protocol version spoken with the service. + * + * 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 TalerDirectoryInstanceHttpClient { + public static readonly PROTOCOL_VERSION = "1:0:0"; + + readonly httpLib: HttpRequestLibrary; + readonly cancellationToken: CancellationToken | undefined; + + constructor( + readonly baseUrl: string, + httpClient?: HttpRequestLibrary, + cancellationToken?: CancellationToken, + ) { + this.httpLib = httpClient ?? createPlatformHttpLib(); + this.cancellationToken = cancellationToken; + } + + static isCompatible(version: string): boolean { + const compare = LibtoolVersion.compare( + TalerDirectoryInstanceHttpClient.PROTOCOL_VERSION, + version, + ); + return compare?.compatible ?? false; + } + + /** + * https://docs.taler.net/core/api-taldir.html#get--config + */ + async getConfig(): Promise< + | OperationOk<TalerDirectoryApi.TalerDirectoryConfigResponse> + | OperationFail<HttpStatusCode.NotFound> + > { + const url = new URL(`/config`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + }); + switch (resp.status) { + case HttpStatusCode.Ok: + return carefullyParseConfig( + "taler-directory", + TalerDirectoryInstanceHttpClient.PROTOCOL_VERSION, + resp, + codecForTalerDirectoryConfigResponse(), + ); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownHttpFailure(resp); + } + } + + /** + * https://docs.taler.net/core/api-taldir.html#get--$H_ALIAS + */ + async lookupAlias(args: { + hAlias: string; + }): Promise< + | OperationOk<TaldirMailboxDetailResponse> + | OperationFail<HttpStatusCode.NotFound> + > { + const { hAlias: hAlias } = args; + const url = new URL(`${hAlias.toUpperCase()}`, this.baseUrl); + + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + cancellationToken: this.cancellationToken, + }); + + switch (resp.status) { + case HttpStatusCode.Ok: { + return opSuccessFromHttp(resp, codecForAny()); + } + case HttpStatusCode.NotFound: { + return opKnownHttpFailure( + resp.status, + resp, + ); + } + default: + return opUnknownHttpFailure(resp); + } + } + + /** + * https://docs.taler.net/core/api-taldir.html#post--$H_ALIAS + */ + async postRegistration(args: { + hAlias: string; + solution: string; + }): Promise< + | OperationOk<HttpStatusCode.NoContent> + | OperationFail<HttpStatusCode.Forbidden> + | OperationFail<HttpStatusCode.NotFound> + | OperationFail<HttpStatusCode.TooManyRequests> + > { + const { hAlias: hAlias, solution: solution } = args; + const url = new URL(`${hAlias.toUpperCase()}`, this.baseUrl); + + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + body: { + solution: solution, + }, + cancellationToken: this.cancellationToken, + }); + + switch (resp.status) { + case HttpStatusCode.NoContent: { + return opFixedSuccess(resp, resp.status); + } + case HttpStatusCode.NotFound: { + return opKnownHttpFailure( + resp.status, + resp, + ); + } + case HttpStatusCode.Forbidden: { + return opKnownHttpFailure( + resp.status, + resp, + ); + } + case HttpStatusCode.TooManyRequests: { + return opKnownHttpFailure( + resp.status, + resp, + ); + } + default: + return opUnknownHttpFailure(resp); + } + } + + /** + * https://docs.taler.net/core/api-taldir.html#post--register-$ALIASTYPE + */ + async postRegistrationRequest(args: { + alias: string; + aliasType: string; + targetUri: string; + duration: RelativeTime; + }): Promise< + | OperationOk<TaldirAlreadyPaidResponse> + | OperationOk<EmptyObject> + | OperationFail<HttpStatusCode.PaymentRequired> + | OperationFail<HttpStatusCode.TooManyRequests> + | OperationFail<HttpStatusCode.NotFound> + > { + const { + alias: alias, + aliasType: aliasType, + targetUri: targetUri, + duration: duration, + } = args; + const url = new URL(`/register/${aliasType.toUpperCase()}`, this.baseUrl); + + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + body: { + alias: alias, + target_uri: targetUri, + duration: duration, + }, + cancellationToken: this.cancellationToken, + }); + + switch (resp.status) { + case HttpStatusCode.Accepted: { + return opFixedSuccess(resp, {}); + } + case HttpStatusCode.Ok: { + return opSuccessFromHttp(resp, codecForTaldirAlreadyPaidResponse()); + } + case HttpStatusCode.PaymentRequired: { + return opKnownHttpFailure(resp.status, resp); + } + case HttpStatusCode.NotFound: { + return opKnownHttpFailure( + resp.status, + resp, + ); + } + case HttpStatusCode.TooManyRequests: { + return opKnownHttpFailure( + resp.status, + resp, + ); + } + default: + return opUnknownHttpFailure(resp); + } + } + + +} diff --git a/packages/taler-util/src/types-taler-directory.ts b/packages/taler-util/src/types-taler-directory.ts @@ -0,0 +1,114 @@ +/* + This file is part of GNU Taler + (C) 2024 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero 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 Affero General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + + SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { codecForAmountString } from "./amounts.js"; +import { + Codec, + buildCodecForObject, + codecForList, + codecForNumber, +} from "./codec.js"; +import { codecForString, codecForConstString } from "./index.js"; +import { codecForDuration } from "./time.js"; +import { + AmountString, + RelativeTime, +} from "./types-taler-common.js"; + +export const codecForTaldirAliasType = + (): Codec<TaldirAliasType> => + buildCodecForObject<TaldirAliasType>() + .property("name", codecForString()) + .property("challenge_fee", codecForAmountString()) + .build("TalerDirectoryApi.TaldirAliasType"); + +export const codecForTalerDirectoryConfigResponse = + (): Codec<TalerDirectoryConfigResponse> => + buildCodecForObject<TalerDirectoryConfigResponse>() + .property("version", codecForString()) + .property("name", codecForConstString("taler-directory")) + .property("monthly_fee", codecForAmountString()) + .property("alias_types", codecForList(codecForTaldirAliasType())) + .build("TalerDirectoryApi.VersionResponse"); + +interface TaldirAliasType { + // Name of the alias type, e.g. "email" or "sms". + name: string; + + // per challenge fee + challenge_fee: AmountString; + +} + +export interface TalerDirectoryConfigResponse { + // libtool-style representation of the Merchant protocol version, see + // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning + // The format is "current:revision:age". + version: string; + + // Name of the protocol. + name: "taler-directory"; + + // Supported alias types + alias_types: TaldirAliasType[]; + + // fee for one month of registration + monthly_fee: AmountString; +} + +export interface TaldirMailboxDetailResponse { + + // Target URI to associate with this alias. + target_uri: string; + +} + +export const codecForTaldirMailboxDetailResponse = (): Codec<TaldirMailboxDetailResponse> => + buildCodecForObject<TaldirMailboxDetailResponse>() + .property("target_uri", codecForString()) + .build("TalerDirectoryApi.TaldirMailboxDetailResponse"); + +export const codecForTalerDirectoryRateLimitedResponse = + (): Codec<TaldirRateLimitedResponse> => + buildCodecForObject<TaldirRateLimitedResponse>() + .property("code", codecForNumber()) + .property("retry_delay", codecForDuration) + .property("hint", codecForString()) + .build("TalerDirectoryApi.TaldirRateLimitedResponse"); + +export interface TaldirAlreadyPaidResponse { + // The remaining duration for which this registration is still paid for + valid_for: RelativeTime; +} + +export const codecForTaldirAlreadyPaidResponse = + (): Codec<TaldirAlreadyPaidResponse> => + buildCodecForObject<TaldirAlreadyPaidResponse>() + .property("valid_for", codecForDuration) + .build("TalerDirectoryApi.TaldirAlreadyPaidResponse"); + +export interface TaldirRateLimitedResponse { + // Taler error code, TALER_EC_TALDIR_DELIVERY_RATE_LIMITED. + code: number; + + // When the client should retry. + retry_delay: RelativeTime; + + // The human readable error message. + hint: string; +} diff --git a/packages/taler-util/src/types-taler-wallet.ts b/packages/taler-util/src/types-taler-wallet.ts @@ -1422,6 +1422,23 @@ export interface ContactListResponse { export type TaldirRegistrationResponse = TaldirAlreadyPaidResponse | EmptyObject; +export interface TaldirRegistrationCompletionRequest { + alias: string; + aliasType: string; + challenge: string; + taldirBaseUrl: string; + targetUri: string; +} + +export const codecForTaldirRegistrationCompletionRequest = (): Codec<TaldirRegistrationCompletionRequest> => + buildCodecForObject<TaldirRegistrationCompletionRequest>() + .property("targetUri", codecForString()) + .property("taldirBaseUrl", codecForString()) + .property("alias", codecForString()) + .property("aliasType", codecForString()) + .property("challenge", codecForString()) + .build("TaldirRegistrationCompletionRequest"); + export interface TaldirRegistrationRequest { alias: string; aliasType: string; diff --git a/packages/taler-wallet-core/src/taldir.ts b/packages/taler-wallet-core/src/taldir.ts @@ -0,0 +1,165 @@ +/* + This file is part of GNU Taler + (C) 2026 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/> + */ + +/** + * @fileoverview + * Implementation of taldir in wallet-core. + */ + +import { + Logger, + TaldirLookupRequest, + TalerDirectoryInstanceHttpClient, + encodeCrock, + TaldirLookupResponse, + sha512, + HttpStatusCode, + TaldirRegistrationRequest, + TaldirRegistrationResponse, + TaldirRegistrationCompletionRequest, + EmptyObject, +} from "@gnu-taler/taler-util"; +import { WalletExecutionContext } from "./wallet.js"; + +const logger = new Logger("taldir.ts"); + +function createHAliasBuffer(aliasType: string, alias: string): Uint8Array { + const encoder = new TextEncoder(); + const typeBytes = encoder.encode(aliasType); + const aliasBytes = encoder.encode(alias); + const payloadLength = typeBytes.length + aliasBytes.length; + const totalBuffer = new Uint8Array(4 + payloadLength); + const view = new DataView(totalBuffer.buffer); + view.setUint32(0, payloadLength, false); // 'false' specifies Big-Endian / NBO + totalBuffer.set(typeBytes, 4); + totalBuffer.set(aliasBytes, 4 + typeBytes.length); + return totalBuffer; +} + +/** + * Lookup alias + */ +export async function lookupAlias( + wex: WalletExecutionContext, + lookupRequest: TaldirLookupRequest, +): Promise<TaldirLookupResponse> { + const taldirClient = new TalerDirectoryInstanceHttpClient( + lookupRequest.taldirBaseUrl, + wex.http, + ); + // Refresh message size + const resConf = await taldirClient.getConfig(); + switch (resConf.case) { + case "ok": + let supported_types = resConf.body.alias_types; + let typeFound = false; + for (var t of supported_types) { + if (t.name === lookupRequest.aliasType) { + logger.warn(`queried type not supported by directory`); + typeFound = true; + } + } + if (!typeFound) { + return { } + } + break; + default: + throw Error("unable to get directory service config"); + } + // SHA-512(len($ALIASTYPE)+len($ALIAS)||$ALIASTYPE||$ALIAS) + const hAliasBuffer = createHAliasBuffer(lookupRequest.aliasType, lookupRequest.alias); + const hAlias = encodeCrock(sha512(hAliasBuffer)); + const res = await taldirClient.lookupAlias({ hAlias: hAlias }); + switch (res.case) { + case "ok": + return { + targetUri: res.body.target_uri, + }; + case HttpStatusCode.NotFound: + return {}; + default: + throw Error("unexpected taldir messages response empty"); + } +} + +/** + * Initiate registration + */ +export async function registerAlias( + wex: WalletExecutionContext, + registerRequest: TaldirRegistrationRequest, +): Promise<TaldirRegistrationResponse> { + const taldirClient = new TalerDirectoryInstanceHttpClient( + registerRequest.taldirBaseUrl, + wex.http, + ); + const res = await taldirClient.postRegistrationRequest({ + alias: registerRequest.alias, + aliasType: registerRequest.aliasType, + targetUri: registerRequest.taldirBaseUrl, + duration: registerRequest.duration, + }); + switch (res.case) { + case "ok": + return res.body; + case HttpStatusCode.NotFound: + throw Error("taldir reported not found, this means that the alias type was not supported"); + default: + throw Error("unexpected taldir messages response empty"); + } +} + +/** + * Complete + */ +export async function completeAliasRegistration( + wex: WalletExecutionContext, + completionRequest: TaldirRegistrationCompletionRequest, +): Promise<EmptyObject> { + const taldirClient = new TalerDirectoryInstanceHttpClient( + completionRequest.taldirBaseUrl, + wex.http, + ); + const encoder = new TextEncoder(); + const bytes1 = encoder.encode(completionRequest.challenge); + const bytes2 = encoder.encode(completionRequest.targetUri); + const combined = new Uint8Array(bytes1.length + bytes2.length); + combined.set(bytes1, 0); + combined.set(bytes2, bytes1.length); + const solution = encodeCrock(sha512(combined)); + // SHA-512(len($ALIASTYPE)+len($ALIAS)||$ALIASTYPE||$ALIAS) + const hAliasBuffer = createHAliasBuffer(completionRequest.aliasType, completionRequest.alias); + const hAlias = encodeCrock(sha512(hAliasBuffer)); + const res = await taldirClient.postRegistration({ + hAlias: hAlias, + solution: solution, + }); + switch (res.case) { + case "ok": + return {}; + case HttpStatusCode.NotFound: + throw Error("taldir reported that this registration was not found"); + case HttpStatusCode.Forbidden: + throw Error("taldir reported that the solution was incorrect"); + case HttpStatusCode.TooManyRequests: + throw Error("taldir reported that too many tries have been made to solve this registration challenge"); + default: + throw Error("unexpected taldir messages response empty"); + } +} + + +