taler-typescript-core

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

commit 034b675906062a83a22d830164e24a6acd31276d
parent 03b8a0058c7030bd23336112320d879e080bc753
Author: Florian Dold <florian@dold.me>
Date:   Wed, 30 Oct 2024 13:51:33 +0100

harness: aml program support, reproducer for #9154

Diffstat:
Mpackages/taler-harness/src/index.ts | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Mpackages/taler-harness/src/integrationtests/test-kyc-skip-expiration.ts | 10+++-------
Mpackages/taler-util/src/codec.ts | 33++++++++++++++++++++++++++++-----
Apackages/taler-util/src/types-taler-aml.ts | 162+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-util/src/types-taler-exchange.ts | 31++++++++++++++++++++-----------
5 files changed, 275 insertions(+), 27 deletions(-)

diff --git a/packages/taler-harness/src/index.ts b/packages/taler-harness/src/index.ts @@ -33,6 +33,7 @@ import { TalerCoreBankHttpClient, TalerMerchantInstanceHttpClient, TalerMerchantManagementHttpClient, + TalerProtocolTimestamp, TransactionsResponse, createRFC8959AccessTokenEncoded, createRFC8959AccessTokenPlain, @@ -63,21 +64,25 @@ import { deepStrictEqual } from "assert"; import fs from "fs"; import os from "os"; import path from "path"; +import { + AmlOutcome, + codecForAmlProgramInput, +} from "../../taler-util/src/types-taler-aml.js"; import { runBench1 } from "./bench1.js"; import { runBench2 } from "./bench2.js"; import { runBench3 } from "./bench3.js"; import { runEnvFull } from "./env-full.js"; import { runEnv1 } from "./env1.js"; import { + createSimpleTestkudosEnvironmentV2, + createWalletDaemonWithClient, +} from "./harness/environments.js"; +import { GlobalTestState, WalletClient, delayMs, runTestWithState, } from "./harness/harness.js"; -import { - createSimpleTestkudosEnvironmentV2, - createWalletDaemonWithClient, -} from "./harness/environments.js"; import { getTestInfo, runTests } from "./integrationtests/testrunner.js"; import { lintExchangeDeployment } from "./lint.js"; @@ -1421,6 +1426,59 @@ testingCli.subcommand("tvgcheck", "tvgcheck").action(async (args) => { console.log("check passed!"); }); +export const amlProgramCli = testingCli.subcommand( + "amlProgram", + "aml-program", + { + help: "Command line interface for the GNU Taler test/deployment harness.", + }, +); + +amlProgramCli + .subcommand("noRules", "no-rules") + .flag("requires", ["-r"]) + .flag("attributes", ["-a"]) + .maybeOption("config", ["-c", "--config"], clk.STRING) + .action(async (args) => { + logger.info("Hello, this is the no-rules AML program."); + if (args.noRules.requires) { + logger.info("Reporting requirements"); + // No requirements. + return; + } + + if (args.noRules.attributes) { + logger.info("reporting attributes"); + // No attributes + return; + } + + const buffers = []; + + // node.js readable streams implement the async iterator protocol + for await (const data of process.stdin) { + buffers.push(data); + } + + const finalBuffer = Buffer.concat(buffers); + const inputStr = finalBuffer.toString("utf-8"); + const inputJson = JSON.parse(inputStr); + const progInput = codecForAmlProgramInput().decode(inputJson); + + logger.info(`got input: ${j2s(progInput)}`); + + const outcome: AmlOutcome = { + new_rules: { + expiration_time: TalerProtocolTimestamp.never(), + rules: [], + custom_measures: {}, + }, + events: [], + }; + + console.log(j2s(outcome)); + }); + export function main() { testingCli.run(); } diff --git a/packages/taler-harness/src/integrationtests/test-kyc-skip-expiration.ts b/packages/taler-harness/src/integrationtests/test-kyc-skip-expiration.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2020 Taler Systems S.A. + (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 General Public License as published by the Free Software @@ -66,14 +66,10 @@ function adjustExchangeConfig(config: Configuration) { config.setString( "AML-PROGRAM-P1", "command", - "taler-exchange-helper-measure-test-form", + "taler-harness aml-program no-rules", ); config.setString("AML-PROGRAM-P1", "enabled", "true"); - config.setString( - "AML-PROGRAM-P1", - "description", - "test for full_name and birthdate", - ); + config.setString("AML-PROGRAM-P1", "description", "remove all rules"); config.setString("AML-PROGRAM-P1", "description_i18n", "{}"); config.setString("AML-PROGRAM-P1", "fallback", "M1"); diff --git a/packages/taler-util/src/codec.ts b/packages/taler-util/src/codec.ts @@ -83,6 +83,7 @@ interface Alternative { class ObjectCodecBuilder<OutputType, PartialOutputType> { private propList: Prop[] = []; + private _allowExtra: boolean = false; /** * Define a property for the object. @@ -98,6 +99,11 @@ class ObjectCodecBuilder<OutputType, PartialOutputType> { return this as any; } + allowExtra(): ObjectCodecBuilder<OutputType, PartialOutputType> { + this._allowExtra = true; + return this; + } + /** * Return the built codec. * @@ -106,6 +112,7 @@ class ObjectCodecBuilder<OutputType, PartialOutputType> { */ build(objectDisplayName: string): Codec<PartialOutputType> { const propList = this.propList; + const allowExtra = this._allowExtra; return { decode(x: any, c?: Context): PartialOutputType { if (!c) { @@ -129,6 +136,20 @@ class ObjectCodecBuilder<OutputType, PartialOutputType> { ); obj[prop.name] = propVal; } + for (const prop in x) { + if (prop in obj) { + continue; + } + if (allowExtra) { + obj[prop] = x[prop]; + } else { + logger.warn( + `Extra property ${prop} for ${objectDisplayName} at ${renderContext( + c, + )}`, + ); + } + } return obj as PartialOutputType; }, }; @@ -146,7 +167,7 @@ class UnionCodecBuilder< constructor( private discriminator: TagPropertyLabel, private baseCodec?: Codec<CommonBaseType>, - ) { } + ) {} /** * Define a property for the object. @@ -491,7 +512,10 @@ export function codecOptional<V>(innerCodec: Codec<V>): Codec<V | undefined> { }; } -export function codecOptionalDefault<V>(innerCodec: Codec<V>, def: V): Codec<V> { +export function codecOptionalDefault<V>( + innerCodec: Codec<V>, + def: V, +): Codec<V> { return { decode(x: any, c?: Context): V { if (x === undefined || x === null) { @@ -503,18 +527,17 @@ export function codecOptionalDefault<V>(innerCodec: Codec<V>, def: V): Codec<V> } export function codecForLazy<V>(innerCodec: () => Codec<V>): Codec<V> { - let instance: Codec<V> | undefined = undefined + let instance: Codec<V> | undefined = undefined; return { decode(x: any, c?: Context): V { if (instance === undefined) { - instance = innerCodec() + instance = innerCodec(); } return instance.decode(x, c); }, }; } - export type CodecType<T> = T extends Codec<infer X> ? X : any; export function codecForEither<T extends Array<Codec<unknown>>>( diff --git a/packages/taler-util/src/types-taler-aml.ts b/packages/taler-util/src/types-taler-aml.ts @@ -0,0 +1,162 @@ +/* + 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 + */ + +/** + * @fileoverview Type and schema definitions and helpers for the Taler AML helpers. + */ + +/** + * Imports. + */ +import { + buildCodecForObject, + Codec, + codecForAny, + codecForList, +} from "./index.js"; +import { TalerProtocolTimestamp } from "./time.js"; +import { + AccountProperties, + codecForAccountProperties, + LegitimizationRuleSet, +} from "./types-taler-exchange.js"; + +export interface AmlProgramInput { + // JSON object that was provided as + // part of the *measure*. This JSON object is + // provided under "context" in the main JSON object + // input to the AML program. This "context" should + // satify both the REQUIRES clause of the respective + // check and the output of "-r" from the + // AML program's command-line option. + context?: Record<string, unknown>; + + // JSON object that captures the + // output of a ``[kyc-provider-]`` or (HTML) FORM. + // In the case of KYC data provided by providers, + // the keys in the JSON object will be the attribute + // names and the values must be strings representing + // the data. In the case of file uploads, the data + // MUST be base64-encoded. + // In the case of KYC data provided by HTML FORMs, the + // keys will match the HTML FORM field names and + // the values will use the `KycStructuredFormData` + // encoding. + attributes: Record<string, unknown>; + + // JSON array with the results of historic + // AML desisions about the account. + aml_history: AmlHistoryEntry[]; + + // JSON array with the results of historic + // KYC data about the account. + kyc_history: KycHistoryEntry[]; +} + +export interface AmlHistoryEntry { + // When was the AML decision taken. + decision_time: TalerProtocolTimestamp; + + // What was the justification given for the decision. + justification: string; + + // Public key of the AML officer taking the decision. + decider_pub: string; + + // Properties associated with the account by the decision. + properties: Object; + + // New set of legitimization rules that was put in place. + new_rules: LegitimizationRuleSet; + + // True if the account was flagged for (further) + // investigation. + to_investigate: boolean; + + // True if this is the currently active decision. + is_active: boolean; +} + +export interface KycHistoryEntry { + // Name of the provider + // which was used to collect the attributes. NULL if they were + // just uploaded via a form by the account owner. + provider_name?: string; + + // True if the KYC process completed. + finished: boolean; + + // Numeric `error code <error-codes>`, if the + // KYC process did not succeed; 0 on success. + code: number; + + // Human-readable description of ``code``. Optional. + hint?: string; + + // Optional detail given when the KYC process failed. + error_message?: string; + + // Identifier of the user at the KYC provider. Optional. + provider_user_id?: string; + + // Identifier of the KYC process at the KYC provider. Optional. + provider_legitimization_id?: string; + + // The collected KYC data. + // NULL if the attribute data could not + // be decrypted or was not yet collected. + attributes?: Record<string, unknown>; + + // Time when the KYC data was collected + collection_time: TalerProtocolTimestamp; + + // Time when the KYC data will expire. + expiration_time: TalerProtocolTimestamp; +} + +export interface AmlOutcome { + // Should the client's account be investigated + // by AML staff? + // Defaults to false. + to_investigate?: boolean; + + // Free-form properties about the account. + // Can be used to store properties such as PEP, + // risk category, type of business, hits on + // sanctions lists, etc. + properties?: AccountProperties; + + // Types of events to add to the KYC events table. + // (for statistics). + events?: string[]; + + // KYC rules to apply. Note that this + // overrides *all* of the default rules + // until the ``expiration_time`` and specifies + // the successor measure to apply after the + // expiration time. + new_rules: LegitimizationRuleSet; +} + +export const codecForAmlProgramInput = (): Codec<AmlProgramInput> => + buildCodecForObject<AmlProgramInput>() + .property("aml_history", codecForList(codecForAny())) + .property("kyc_history", codecForList(codecForAny())) + .property("attributes", codecForAccountProperties()) + .property("context", codecForAny()) + .build("AmlProgramInput"); diff --git a/packages/taler-util/src/types-taler-exchange.ts b/packages/taler-util/src/types-taler-exchange.ts @@ -892,8 +892,14 @@ export const codecForExchangeKeysJson = (): Codec<ExchangeKeysJson> => .property("global_fees", codecForList(codecForGlobalFees())) .property("accounts", codecForList(codecForExchangeWireAccount())) .property("wire_fees", codecForMap(codecForList(codecForWireFeesJson()))) - .property("zero_limits", codecOptional(codecForList(codecForZeroLimitedOperation()))) - .property("hard_limits", codecOptional(codecForList(codecForAccountLimit()))) + .property( + "zero_limits", + codecOptional(codecForList(codecForZeroLimitedOperation())), + ) + .property( + "hard_limits", + codecOptional(codecForList(codecForAccountLimit())), + ) .property("denominations", codecForList(codecForNgDenominations)) .property( "wallet_balance_limit_without_kyc", @@ -1705,7 +1711,15 @@ export interface AccountKycStatus { limits?: AccountLimit[]; } -export type LimitOperationType = "WITHDRAW" | "DEPOSIT" | "MERGE" | "AGGREGATE" | "BALANCE" | "REFUND" | "CLOSE" | "TRANSACTION"; +export type LimitOperationType = + | "WITHDRAW" + | "DEPOSIT" + | "MERGE" + | "AGGREGATE" + | "BALANCE" + | "REFUND" + | "CLOSE" + | "TRANSACTION"; export interface AccountLimit { // Operation that is limited. @@ -2453,6 +2467,7 @@ export const codecForAccountProperties = (): Codec<AccountProperties> => .property("business_domain", codecOptional(codecForString())) .property("is_frozen", codecOptional(codecForBoolean())) .property("was_reported", codecOptional(codecForBoolean())) + .allowExtra() .build("TalerExchangeApi.AccountProperties"); export const codecForLegitimizationRuleSet = (): Codec<LegitimizationRuleSet> => @@ -2526,10 +2541,7 @@ export const codecForOperationType = codecForEither( export const codecForAccountLimit = (): Codec<AccountLimit> => buildCodecForObject<AccountLimit>() - .property( - "operation_type", - codecForOperationType, - ) + .property("operation_type", codecForOperationType) .property("timeframe", codecForDuration) .property("threshold", codecForAmountString()) .property("soft_limit", codecOptional(codecForBoolean())) @@ -2537,10 +2549,7 @@ export const codecForAccountLimit = (): Codec<AccountLimit> => export const codecForZeroLimitedOperation = (): Codec<ZeroLimitedOperation> => buildCodecForObject<ZeroLimitedOperation>() - .property( - "operation_type", - codecForOperationType - ) + .property("operation_type", codecForOperationType) .build("TalerExchangeApi.ZeroLimitedOperation"); export const codecForKycCheckPublicInformation =