commit 2c393173814afe4dc5a2ac7bec8ac8ca21495fef
parent 04e7e052dc4b9c89602a59dcc3532ea9a43f920c
Author: Sebastian <sebasjm@gmail.com>
Date: Wed, 10 Sep 2025 10:12:58 -0300
fix #10272
Diffstat:
7 files changed, 478 insertions(+), 153 deletions(-)
diff --git a/packages/taler-harness/src/integrationtests/test-merchant-self-provision-activation.ts b/packages/taler-harness/src/integrationtests/test-merchant-self-provision-activation.ts
@@ -18,10 +18,13 @@
* Imports.
*/
import {
+ alternativeOrThrow,
HttpStatusCode,
+ LoginTokenScope,
MerchantAuthMethod,
setPrintHttpRequestAsCurl,
succeedOrThrow,
+ TalerMerchantInstanceHttpClient,
TalerMerchantManagementHttpClient,
} from "@gnu-taler/taler-util";
import { createSimpleTestkudosEnvironmentV3 } from "harness/environments.js";
@@ -30,6 +33,7 @@ import { GlobalTestState } from "../harness/harness.js";
import { startTanHelper } from "harness/tan-helper.js";
import { chmodSync, fstat, writeFileSync } from "node:fs";
import { randomBytes } from "node:crypto";
+import { solveMFA } from "./test-merchant-self-provision-inactive-account-permissions.js";
/**
* Do basic checks on instance management and authentication.
@@ -62,21 +66,13 @@ taler-harness run-helper --socket ${socketFile} -- $@
additionalMerchantConfig(m) {
m.modifyConfig(async (cfg) => {
cfg.setString("merchant", "ENABLE_SELF_PROVISIONING", "yes");
- cfg.setString(
- "merchant",
- "HELPER_SMS",
- helperScript
- );
- cfg.setString(
- "merchant",
- "HELPER_EMAIL",
- helperScript,
- );
+ cfg.setString("merchant", "HELPER_SMS", helperScript);
+ cfg.setString("merchant", "HELPER_EMAIL", helperScript);
cfg.setString("merchant", "MANDATORY_TAN_CHANNELS", "email sms");
});
},
});
- const helper = await startTanHelper({socketFile});
+ const helper = await startTanHelper({ socketFile });
const merchantClient = new TalerMerchantManagementHttpClient(
merchant.makeInstanceBaseUrl(),
@@ -105,16 +101,14 @@ taler-harness run-helper --socket ${socketFile} -- $@
use_stefan: false,
};
const signupStart =
- await merchantClient.createInstanceSelfProvision(instanceInfo);
+ alternativeOrThrow(await merchantClient.createInstanceSelfProvision(instanceInfo), HttpStatusCode.Accepted);
// creation requires 2fa
- t.assertDeepEqual(signupStart.type, "fail");
- t.assertDeepEqual(signupStart.case, HttpStatusCode.Accepted);
- t.assertDeepEqual(signupStart.body.challenges.length, 2);
- t.assertDeepEqual(signupStart.body.combi_and, true);
+ t.assertDeepEqual(signupStart.challenges.length, 2);
+ t.assertDeepEqual(signupStart.combi_and, true);
- const firstChallenge = signupStart.body.challenges[0];
- const secondChallenge = signupStart.body.challenges[1];
+ const firstChallenge = signupStart.challenges[0];
+ const secondChallenge = signupStart.challenges[1];
//FIXME: check the order
// always first emails since is cheaper
@@ -135,7 +129,7 @@ taler-harness run-helper --socket ${socketFile} -- $@
);
const message = helper.getLastCodeForAddress(instanceInfo.phone_number);
- const [tanCode,] = message.split('\n')
+ const [tanCode] = message.split("\n");
succeedOrThrow(
await merchantClient.confirmChallenge(firstChallenge.challenge_id, {
tan: tanCode,
@@ -149,7 +143,7 @@ taler-harness run-helper --socket ${socketFile} -- $@
);
const message = helper.getLastCodeForAddress(instanceInfo.email);
- const [tanCode,] = message.split('\n')
+ const [tanCode] = message.split("\n");
succeedOrThrow(
await merchantClient.confirmChallenge(secondChallenge.challenge_id, {
tan: tanCode,
@@ -166,6 +160,11 @@ taler-harness run-helper --socket ${socketFile} -- $@
t.assertDeepEqual(completeSignup.type, "ok");
+ const instanceApi = new TalerMerchantInstanceHttpClient(
+ merchantClient.getSubInstanceAPI(instanceInfo.id),
+ merchantClient.httpLib,
+ );
+
{
// new instance is completed, now it should be visible
const r = succeedOrThrow(
@@ -174,6 +173,45 @@ taler-harness run-helper --socket ${socketFile} -- $@
t.assertDeepEqual(r.instances.length, 3);
}
+ const loginChallenge = alternativeOrThrow(
+ await instanceApi.createAccessToken(
+ instanceInfo.id,
+ instanceInfo.auth.password,
+ {
+ scope: LoginTokenScope.All,
+ },
+ ),
+ HttpStatusCode.Accepted,
+ );
+
+ await solveMFA(merchantClient, helper, loginChallenge, {
+ [TanChannel.EMAIL]: instanceInfo.email,
+ [TanChannel.SMS]: instanceInfo.phone_number,
+ });
+
+ const { access_token: token } = succeedOrThrow(
+ await instanceApi.createAccessToken(
+ instanceInfo.id,
+ instanceInfo.auth.password,
+ {
+ scope: LoginTokenScope.All,
+ },
+ {
+ challengeIds: loginChallenge.challenges.map((c) => c.challenge_id),
+ },
+ ),
+ );
+
+ const det = succeedOrThrow(
+ await instanceApi.getCurrentInstanceDetails(token),
+ );
+
+ // check that the instance has the new email
+ t.assertDeepEqual(det.email, instanceInfo.email);
+ t.assertDeepEqual(det.email_validated, true);
+ t.assertDeepEqual(det.phone_number, instanceInfo.phone_number);
+ t.assertDeepEqual(det.phone_validated, true);
+
helper.stop();
}
diff --git a/packages/taler-harness/src/integrationtests/test-merchant-self-provision-forgot-password.ts b/packages/taler-harness/src/integrationtests/test-merchant-self-provision-forgot-password.ts
@@ -19,11 +19,15 @@
*/
import {
AccessToken,
+ alternativeOrThrow,
+ failOrThrow,
HttpStatusCode,
LoginTokenScope,
MerchantAuthMethod,
succeedOrThrow,
+ TalerMerchantInstanceHttpClient,
TalerMerchantManagementHttpClient,
+ TanChannel,
URL,
} from "@gnu-taler/taler-util";
import {
@@ -36,13 +40,31 @@ import {
} from "../harness/harness.js";
import { createTopsEnvironment } from "harness/tops.js";
import { createSimpleTestkudosEnvironmentV3 } from "harness/environments.js";
+import { randomBytes } from "node:crypto";
+import { chmodSync, writeFileSync } from "node:fs";
+import { startTanHelper } from "harness/tan-helper.js";
+import { solveMFA } from "./test-merchant-self-provision-inactive-account-permissions.js";
/**
* The merchant should get the TAN code on request to be used to activate the account.
*/
-export async function runMerchantSelfProvisionForgotPasswordTest(t: GlobalTestState) {
+export async function runMerchantSelfProvisionForgotPasswordTest(
+ t: GlobalTestState,
+) {
// Set up test environment
+ // FIXME: maybe merchant can use commands?
+ const RND = randomBytes(10).toString("hex");
+ const socketFile = `${t.testDir}/tan-helper-${RND}.socket`;
+ const helperScript = `${t.testDir}/harness-helper-${RND}.sh`;
+ writeFileSync(
+ helperScript,
+ `#!/bin/bash
+taler-harness run-helper --socket ${socketFile} -- $@
+`,
+ );
+ chmodSync(helperScript, "777");
+
const {
walletClient,
bankClient,
@@ -50,19 +72,111 @@ export async function runMerchantSelfProvisionForgotPasswordTest(t: GlobalTestSt
merchant,
merchantAdminAccessToken,
bank,
-
- } = await createSimpleTestkudosEnvironmentV3(t);
+ } = await createSimpleTestkudosEnvironmentV3(t, undefined, {
+ additionalMerchantConfig(m) {
+ m.modifyConfig(async (cfg) => {
+ cfg.setString("merchant", "ENABLE_SELF_PROVISIONING", "yes");
+ cfg.setString("merchant", "HELPER_EMAIL", helperScript);
+ cfg.setString("merchant", "MANDATORY_TAN_CHANNELS", "email");
+ });
+ },
+ });
+ const helper = await startTanHelper({ socketFile });
const merchantClient = new TalerMerchantManagementHttpClient(
merchant.makeInstanceBaseUrl(),
);
{
- const r = succeedOrThrow(await merchantClient.listInstances(merchantAdminAccessToken));
+ const r = succeedOrThrow(
+ await merchantClient.listInstances(merchantAdminAccessToken),
+ );
t.assertDeepEqual(r.instances.length, 2);
}
-
+ const instanceInfo = {
+ id: "self-instance",
+ name: "My instance",
+ auth: {
+ method: MerchantAuthMethod.TOKEN,
+ password: "123",
+ },
+ default_pay_delay: { d_us: "forever" as const },
+ default_wire_transfer_delay: { d_us: "forever" as const },
+ jurisdiction: {},
+ address: {},
+ email: "some@taler.net",
+ use_stefan: false,
+ };
+
+ const tryCreation = alternativeOrThrow(
+ await merchantClient.createInstanceSelfProvision(instanceInfo),
+ HttpStatusCode.Accepted,
+ );
+
+ await solveMFA(merchantClient, helper, tryCreation, {
+ [TanChannel.EMAIL]: instanceInfo.email,
+ });
+
+ succeedOrThrow(
+ await merchantClient.createInstanceSelfProvision(instanceInfo, {
+ challengeIds: tryCreation.challenges.map((c) => c.challenge_id),
+ }),
+ );
+
+ const instanceApi = new TalerMerchantInstanceHttpClient(
+ merchantClient.getSubInstanceAPI(instanceInfo.id),
+ merchantClient.httpLib,
+ );
+
+ const newPassword = {
+ method: MerchantAuthMethod.TOKEN,
+ password: "xxx",
+ };
+
+ const mfa = alternativeOrThrow(
+ await instanceApi.forgotPasswordSelfProvision(newPassword),
+ HttpStatusCode.Accepted,
+ );
+
+ await solveMFA(instanceApi, helper, mfa, {
+ [TanChannel.EMAIL]: instanceInfo.email,
+ });
+
+ succeedOrThrow(
+ await instanceApi.forgotPasswordSelfProvision(newPassword, {
+ challengeIds: mfa.challenges.map((c) => c.challenge_id),
+ }),
+ );
+
+ const mfa2 = alternativeOrThrow(
+ await instanceApi.createAccessToken(instanceInfo.id, newPassword.password, {
+ scope: LoginTokenScope.All,
+ }),
+ HttpStatusCode.Accepted,
+ );
+
+ await solveMFA(instanceApi, helper, mfa2, {
+ [TanChannel.EMAIL]: instanceInfo.email,
+ });
+
+ const tk = succeedOrThrow(
+ await instanceApi.createAccessToken(
+ instanceInfo.id,
+ newPassword.password,
+ {
+ scope: LoginTokenScope.All,
+ },
+ {
+ challengeIds: mfa2.challenges.map((c) => c.challenge_id),
+ },
+ ),
+ );
+
+ helper.stop();
}
-runMerchantSelfProvisionForgotPasswordTest.suites = ["merchant","self-provision"];
+runMerchantSelfProvisionForgotPasswordTest.suites = [
+ "merchant",
+ "self-provision",
+];
diff --git a/packages/taler-harness/src/integrationtests/test-merchant-self-provision-inactive-account-permissions.ts b/packages/taler-harness/src/integrationtests/test-merchant-self-provision-inactive-account-permissions.ts
@@ -18,21 +18,44 @@
* Imports.
*/
import {
+ alternativeOrThrow,
+ ChallengeResponse,
+ failOrThrow,
+ HttpStatusCode,
+ LoginTokenScope,
+ MerchantAuthMethod,
succeedOrThrow,
- TalerMerchantManagementHttpClient
+ TalerMerchantInstanceHttpClient,
+ TalerMerchantManagementHttpClient,
+ TanChannel,
} from "@gnu-taler/taler-util";
import { createSimpleTestkudosEnvironmentV3 } from "harness/environments.js";
-import {
- GlobalTestState
-} from "../harness/harness.js";
+import { GlobalTestState } from "../harness/harness.js";
+import { chmodSync, writeFileSync } from "node:fs";
+import { randomBytes } from "node:crypto";
+import { startTanHelper, TestTanHelperService } from "harness/tan-helper.js";
/**
* Test that the merchant can change name and emails adress but can't start kyc process
* before activating the account
*/
-export async function runMerchantSelfProvisionInactiveAccountPermissionsTest(t: GlobalTestState) {
+export async function runMerchantSelfProvisionInactiveAccountPermissionsTest(
+ t: GlobalTestState,
+) {
// Set up test environment
+ // FIXME: maybe merchant can use commands?
+ const RND = randomBytes(10).toString("hex");
+ const socketFile = `${t.testDir}/tan-helper-${RND}.socket`;
+ const helperScript = `${t.testDir}/harness-helper-${RND}.sh`;
+ writeFileSync(
+ helperScript,
+ `#!/bin/bash
+ taler-harness run-helper --socket ${socketFile} -- $@
+ `,
+ );
+ chmodSync(helperScript, "777");
+
const {
walletClient,
bankClient,
@@ -40,18 +63,180 @@ export async function runMerchantSelfProvisionInactiveAccountPermissionsTest(t:
merchant,
bank,
merchantAdminAccessToken,
- } = await createSimpleTestkudosEnvironmentV3(t);
+ } = await createSimpleTestkudosEnvironmentV3(t, undefined, {
+ additionalMerchantConfig(m) {
+ m.modifyConfig(async (cfg) => {
+ cfg.setString("merchant", "ENABLE_SELF_PROVISIONING", "yes");
+ cfg.setString("merchant", "HELPER_SMS", helperScript);
+ cfg.setString("merchant", "HELPER_EMAIL", helperScript);
+ cfg.setString("merchant", "MANDATORY_TAN_CHANNELS", "email");
+ });
+ },
+ });
+ const helper = await startTanHelper({ socketFile });
const merchantClient = new TalerMerchantManagementHttpClient(
merchant.makeInstanceBaseUrl(),
);
{
- const r = succeedOrThrow(await merchantClient.listInstances(merchantAdminAccessToken));
+ const r = succeedOrThrow(
+ await merchantClient.listInstances(merchantAdminAccessToken),
+ );
+ t.assertDeepEqual(r.instances.length, 2);
+ }
+ const instanceInfo = {
+ id: "self-instance",
+ name: "My instance",
+ auth: {
+ method: MerchantAuthMethod.TOKEN,
+ password: "123",
+ },
+ default_pay_delay: { d_us: "forever" as const },
+ default_wire_transfer_delay: { d_us: "forever" as const },
+ jurisdiction: {},
+ address: {},
+ email: "some@taler.net",
+ use_stefan: false,
+ };
+
+ const signupStart = alternativeOrThrow(
+ await merchantClient.createInstanceSelfProvision(instanceInfo),
+ HttpStatusCode.Accepted,
+ );
+
+ // creation requires 2fa
+ t.assertDeepEqual(signupStart.challenges.length, 1);
+ t.assertDeepEqual(signupStart.combi_and, true);
+
+ const { challenge_id: firstChallenge } = signupStart.challenges[0];
+
+ {
+ // new instance is pending, then is not listed
+ const r = succeedOrThrow(
+ await merchantClient.listInstances(merchantAdminAccessToken),
+ );
t.assertDeepEqual(r.instances.length, 2);
}
-
+ const instanceApi = new TalerMerchantInstanceHttpClient(
+ merchantClient.getSubInstanceAPI(instanceInfo.id),
+ merchantClient.httpLib,
+ );
+
+ succeedOrThrow(await merchantClient.sendChallenge(firstChallenge));
+
+ const [tan] = helper.getLastCodeForAddress(instanceInfo.email).split("\n");
+
+ t.assertTrue(tan.length > 0, "the mechant didnt send the code to the email");
+
+ // in this case the user used the wrong email and
+ // needs to change it before confirm
+
+ instanceInfo.email = "another@email.com";
+
+ const emailChange = alternativeOrThrow(
+ await merchantClient.createInstanceSelfProvision(instanceInfo),
+ HttpStatusCode.Accepted,
+ );
+
+ // creation requires 2fa
+ t.assertDeepEqual(emailChange.challenges.length, 1);
+ t.assertDeepEqual(emailChange.combi_and, true);
+
+ await solveMFA(merchantClient, helper, emailChange, {
+ [TanChannel.EMAIL]: instanceInfo.email,
+ });
+
+ const wrongInfo = {
+ ...instanceInfo,
+ email: "just_wrong_email",
+ };
+
+ // const bad = failOrThrow(
+ await merchantClient.createInstanceSelfProvision(wrongInfo, {
+ challengeIds: emailChange.challenges.map(c => c.challenge_id),
+ });
+ // ,
+ // HttpStatusCode.Conflict,
+ // );
+
+ // console.log("ERRORORORORORO", bad)
+
+ const completeSignup = succeedOrThrow(
+ await merchantClient.createInstanceSelfProvision(instanceInfo, {
+ challengeIds: emailChange.challenges.map(c => c.challenge_id),
+ }),
+ );
+
+ await merchantClient.createInstanceSelfProvision(wrongInfo, {
+ challengeIds: emailChange.challenges.map(c => c.challenge_id),
+ });
+
+ const loginChallenge = alternativeOrThrow(
+ await instanceApi.createAccessToken(
+ instanceInfo.id,
+ instanceInfo.auth.password,
+ {
+ scope: LoginTokenScope.All,
+ },
+ ),
+ HttpStatusCode.Accepted,
+ );
+
+ await solveMFA(merchantClient, helper, loginChallenge, {
+ [TanChannel.EMAIL]: instanceInfo.email,
+ });
+
+ const { access_token: token } = succeedOrThrow(
+ await instanceApi.createAccessToken(
+ instanceInfo.id,
+ instanceInfo.auth.password,
+ {
+ scope: LoginTokenScope.All,
+ },
+ {
+ challengeIds: loginChallenge.challenges.map((c) => c.challenge_id),
+ },
+ ),
+ );
+
+ const det = succeedOrThrow(
+ await instanceApi.getCurrentInstanceDetails(token),
+ );
+
+
+ // check that the instance has the new email
+ t.assertDeepEqual(det.email, instanceInfo.email)
+ t.assertDeepEqual(det.email_validated, true)
+ helper.stop();
}
-runMerchantSelfProvisionInactiveAccountPermissionsTest.suites = ["merchant","self-provision"];
+runMerchantSelfProvisionInactiveAccountPermissionsTest.suites = [
+ "merchant",
+ "self-provision",
+];
+
+export async function solveMFA(
+ api: TalerMerchantInstanceHttpClient,
+ helper: TestTanHelperService,
+ resp: ChallengeResponse,
+ addrs: Partial<Record<TanChannel, string>>,
+) {
+ for (const {
+ challenge_id: challenge,
+ challenge_type: type,
+ } of resp.challenges) {
+ const addrUsed = addrs[type];
+ if (!addrUsed) {
+ throw Error(
+ `The MFA is asking unexpected channel: ${type} id ${challenge}`,
+ );
+ }
+ succeedOrThrow(await api.sendChallenge(challenge));
+
+ const [tan] = helper.getLastCodeForAddress(addrUsed).split("\n");
+
+ succeedOrThrow(await api.confirmChallenge(challenge, { tan }));
+ }
+}
diff --git a/packages/taler-harness/src/integrationtests/test-peer-push-large.ts b/packages/taler-harness/src/integrationtests/test-peer-push-large.ts
@@ -71,7 +71,7 @@ export async function runPeerPushLargeTest(t: GlobalTestState) {
] = await Promise.all([
createSimpleTestkudosEnvironmentV3(t, coinConfigList, {
additionalBankConfig(b) {
-
+
},
}),
createWalletDaemonWithClient(t, {
diff --git a/packages/taler-util/src/http-client/merchant.ts b/packages/taler-util/src/http-client/merchant.ts
@@ -2524,121 +2524,6 @@ export class TalerMerchantInstanceHttpClient {
return opUnknownHttpFailure(resp);
}
}
-
- /**
- * https://docs.taler.net/core/api-merchant.html#post--instances-$INSTANCE-forgot-password
- */
- async forgotPasswordSelfProvision(
- body: TalerMerchantApi.InstanceAuthConfigurationMessage,
- params: {
- challengeIds?: string[];
- } = {},
- ) {
- const url = new URL(`forgot-password`, this.baseUrl);
-
- const headers: Record<string, string> = {};
- if (params.challengeIds && params.challengeIds.length > 0) {
- headers["Taler-Challenge-Ids"] = params.challengeIds.join(", ");
- }
- const resp = await this.httpLib.fetch(url.href, {
- method: "POST",
- body,
- headers,
- });
-
- switch (resp.status) {
- case HttpStatusCode.NoContent: {
- return opEmptySuccess();
- }
- case HttpStatusCode.Accepted: {
- return opKnownAlternativeHttpFailure(
- resp,
- resp.status,
- codecForChallengeResponse(),
- );
- }
- case HttpStatusCode.Unauthorized:
- return opKnownHttpFailure(resp.status, resp);
- default:
- return opUnknownHttpFailure(resp);
- }
- }
-
- /**
- * Get the auth api against the current instance
- *
- * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-token
- * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCE]-private-token
- */
- getAuthenticationAPI(): URL {
- return new URL(`private/`, this.baseUrl);
- }
-}
-
-export type TalerMerchantManagementResultByMethod<
- prop extends keyof TalerMerchantManagementHttpClient,
-> = ResultByMethod<TalerMerchantManagementHttpClient, prop>;
-export type TalerMerchantManagementErrorsByMethod<
- prop extends keyof TalerMerchantManagementHttpClient,
-> = FailCasesByMethod<TalerMerchantManagementHttpClient, prop>;
-
-export class TalerMerchantManagementHttpClient extends TalerMerchantInstanceHttpClient {
- readonly cacheManagementEvictor: CacheEvictor<
- TalerMerchantInstanceCacheEviction | TalerMerchantManagementCacheEviction
- >;
- constructor(
- readonly baseUrl: string,
- httpClient?: HttpRequestLibrary,
- // cacheManagementEvictor?: CacheEvictor<TalerMerchantManagementCacheEviction>,
- cacheEvictor?: CacheEvictor<
- TalerMerchantInstanceCacheEviction | TalerMerchantManagementCacheEviction
- >,
- ) {
- super(baseUrl, httpClient, cacheEvictor);
- this.cacheManagementEvictor = cacheEvictor ?? nullEvictor;
- }
-
- getSubInstanceAPI(instanceId: string): string {
- return new URL(`instances/${instanceId}/`, this.baseUrl).href;
- }
-
- //
- // 2FA Authentication
- //
-
- /**
- * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-auth-channels-$CHANNEL-validate
- *
- * @param channel
- * @returns
- */
- async requestAccountChannelValidation(channel: MerchantTanChannel) {
- const url = new URL(`auth-channels/${channel}/validate`, this.baseUrl);
-
- const resp = await this.httpLib.fetch(url.href, {
- method: "POST",
- });
-
- switch (resp.status) {
- case HttpStatusCode.NoContent: {
- return opEmptySuccess();
- }
- case HttpStatusCode.Accepted: {
- return opKnownAlternativeHttpFailure(
- resp,
- resp.status,
- codecForChallengeResponse(),
- );
- }
- case HttpStatusCode.Unauthorized:
- return opKnownHttpFailure(resp.status, resp);
- case HttpStatusCode.Conflict:
- return opKnownHttpFailure(resp.status, resp);
- default:
- return opUnknownHttpFailure(resp);
- }
- }
-
/**
* https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-challenge-$CHALLENGE_ID
*
@@ -2744,6 +2629,82 @@ export class TalerMerchantManagementHttpClient extends TalerMerchantInstanceHttp
return opUnknownHttpFailure(resp);
}
}
+ /**
+ * https://docs.taler.net/core/api-merchant.html#post--instances-$INSTANCE-forgot-password
+ */
+ async forgotPasswordSelfProvision(
+ body: TalerMerchantApi.InstanceAuthConfigurationMessage,
+ params: {
+ challengeIds?: string[];
+ } = {},
+ ) {
+ const url = new URL(`forgot-password`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (params.challengeIds && params.challengeIds.length > 0) {
+ headers["Taler-Challenge-Ids"] = params.challengeIds.join(", ");
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body,
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ return opEmptySuccess();
+ }
+ case HttpStatusCode.Accepted: {
+ return opKnownAlternativeHttpFailure(
+ resp,
+ resp.status,
+ codecForChallengeResponse(),
+ );
+ }
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownHttpFailure(resp);
+ }
+ }
+
+ /**
+ * Get the auth api against the current instance
+ *
+ * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-token
+ * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCE]-private-token
+ */
+ getAuthenticationAPI(): URL {
+ return new URL(`private/`, this.baseUrl);
+ }
+}
+
+export type TalerMerchantManagementResultByMethod<
+ prop extends keyof TalerMerchantManagementHttpClient,
+> = ResultByMethod<TalerMerchantManagementHttpClient, prop>;
+export type TalerMerchantManagementErrorsByMethod<
+ prop extends keyof TalerMerchantManagementHttpClient,
+> = FailCasesByMethod<TalerMerchantManagementHttpClient, prop>;
+
+export class TalerMerchantManagementHttpClient extends TalerMerchantInstanceHttpClient {
+ readonly cacheManagementEvictor: CacheEvictor<
+ TalerMerchantInstanceCacheEviction | TalerMerchantManagementCacheEviction
+ >;
+ constructor(
+ readonly baseUrl: string,
+ httpClient?: HttpRequestLibrary,
+ // cacheManagementEvictor?: CacheEvictor<TalerMerchantManagementCacheEviction>,
+ cacheEvictor?: CacheEvictor<
+ TalerMerchantInstanceCacheEviction | TalerMerchantManagementCacheEviction
+ >,
+ ) {
+ super(baseUrl, httpClient, cacheEvictor);
+ this.cacheManagementEvictor = cacheEvictor ?? nullEvictor;
+ }
+
+ getSubInstanceAPI(instanceId: string): string {
+ return new URL(`instances/${instanceId}/`, this.baseUrl).href;
+ }
//
// Instance Management
diff --git a/packages/taler-util/src/operation.ts b/packages/taler-util/src/operation.ts
@@ -204,7 +204,13 @@ export function opKnownTalerFailure<T extends TalerErrorCode>(
export function opUnknownFailure(error: unknown): never {
throw TalerError.fromException(error);
}
-
+/**
+ * The operation result should be ok
+ * Return the body of the result
+ *
+ * @param resp
+ * @returns
+ */
export function succeedOrThrow<R>(resp: OperationResult<R, unknown>): R {
if (isOperationOk(resp)) {
return resp.body;
@@ -216,13 +222,22 @@ export function succeedOrThrow<R>(resp: OperationResult<R, unknown>): R {
throw TalerError.fromException(resp);
}
+/**
+ * The operation is expected to fail with a body.
+ * Return the body of the result.
+ * Throw if the operation didn't fail with expected code.
+ *
+ * @param resp
+ * @param s
+ * @returns
+ */
export function alternativeOrThrow<Error, Body, Alt>(
resp:
| OperationOk<Body>
| OperationAlternative<Error, Alt>
| OperationFail<Error>,
s: Error,
-): Promise<Alt> {
+): Alt {
if (isOperationOk(resp)) {
throw TalerError.fromException(
new Error(`request succeed but failure "${s}" was expected`),
@@ -240,6 +255,15 @@ export function alternativeOrThrow<Error, Body, Alt>(
return (resp as any).body;
}
+/**
+ * The operation is expected to fail.
+ * Return the error details.
+ * Throw if the operation didn't fail with expected code.
+ *
+ * @param resp
+ * @param s
+ * @returns
+ */
export function failOrThrow<E>(
resp: OperationResult<unknown, E>,
s: E,
diff --git a/packages/taler-util/src/types-taler-merchant.ts b/packages/taler-util/src/types-taler-merchant.ts
@@ -3745,7 +3745,10 @@ export const codecForQueryInstancesResponse =
buildCodecForObject<QueryInstancesResponse>()
.property("name", codecForString())
.property("email", codecOptional(codecForString()))
+ .property("phone_number", codecOptional(codecForString()))
.property("website", codecOptional(codecForString()))
+ .property("email_validated", codecOptional(codecForBoolean()))
+ .property("phone_validated", codecOptional(codecForBoolean()))
.property("logo", codecOptional(codecForString()))
.property("merchant_pub", codecForEddsaPublicKey())
.property("address", codecForLocation())