commit 3f19a44c869b97622876c6a65583729c29cf3f4e
parent 3b35655e6ae8d78e7fc016ba58967412c2dde770
Author: Martin Schanzenbach <schanzen@gnunet.org>
Date: Thu, 16 Jul 2026 11:14:59 +0200
add missing taldir files
Diffstat:
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");
+ }
+}
+
+
+