diff options
Diffstat (limited to 'packages/taler-util')
-rw-r--r-- | packages/taler-util/package.json | 2 | ||||
-rw-r--r-- | packages/taler-util/src/http-client/bank-integration.ts | 4 | ||||
-rw-r--r-- | packages/taler-util/src/http-client/challenger.ts | 16 | ||||
-rw-r--r-- | packages/taler-util/src/http-client/types.ts | 61 | ||||
-rw-r--r-- | packages/taler-util/src/http-impl.node.ts | 2 | ||||
-rw-r--r-- | packages/taler-util/src/http-impl.qtart.ts | 15 | ||||
-rw-r--r-- | packages/taler-util/src/notifications.ts | 16 | ||||
-rw-r--r-- | packages/taler-util/src/operation.ts | 51 | ||||
-rw-r--r-- | packages/taler-util/src/qr.ts | 7 | ||||
-rw-r--r-- | packages/taler-util/src/taler-types.ts | 3 | ||||
-rw-r--r-- | packages/taler-util/src/taleruri.test.ts | 12 | ||||
-rw-r--r-- | packages/taler-util/src/taleruri.ts | 10 | ||||
-rw-r--r-- | packages/taler-util/src/transactions-types.ts | 5 | ||||
-rw-r--r-- | packages/taler-util/src/wallet-types.ts | 24 |
14 files changed, 191 insertions, 37 deletions
diff --git a/packages/taler-util/package.json b/packages/taler-util/package.json index 11213cdfa..c165489b3 100644 --- a/packages/taler-util/package.json +++ b/packages/taler-util/package.json @@ -1,6 +1,6 @@ { "name": "@gnu-taler/taler-util", - "version": "0.12.1", + "version": "0.12.2", "description": "Generic helper functionality for GNU Taler", "type": "module", "types": "./lib/index.node.d.ts", diff --git a/packages/taler-util/src/http-client/bank-integration.ts b/packages/taler-util/src/http-client/bank-integration.ts index 23740328b..1e0f7e79c 100644 --- a/packages/taler-util/src/http-client/bank-integration.ts +++ b/packages/taler-util/src/http-client/bank-integration.ts @@ -18,6 +18,7 @@ import { HttpRequestLibrary, readTalerErrorResponse } from "../http-common.js"; import { HttpStatusCode } from "../http-status-codes.js"; import { createPlatformHttpLib } from "../http.js"; import { LibtoolVersion } from "../libtool-version.js"; +import { Logger } from "../logging.js"; import { FailCasesByMethod, ResultByMethod, @@ -46,6 +47,8 @@ export type TalerBankIntegrationErrorsByMethod< prop extends keyof TalerBankIntegrationHttpClient, > = FailCasesByMethod<TalerBankIntegrationHttpClient, prop>; +const logger = new Logger("bank-integration.ts"); + /** * The API is used by the wallets. */ @@ -81,6 +84,7 @@ export class TalerBankIntegrationHttpClient { case HttpStatusCode.Ok: return opSuccessFromHttp(resp, codecForIntegrationBankConfig()); default: + logger.warn(`config request failed, status ${resp.status}`) return opUnknownFailure(resp, await readTalerErrorResponse(resp)); } } diff --git a/packages/taler-util/src/http-client/challenger.ts b/packages/taler-util/src/http-client/challenger.ts index e7b128b02..d2f0dd201 100644 --- a/packages/taler-util/src/http-client/challenger.ts +++ b/packages/taler-util/src/http-client/challenger.ts @@ -8,7 +8,7 @@ import { opKnownAlternativeFailure, opKnownHttpFailure, opSuccessFromHttp, - opUnknownFailure + opUnknownFailure, } from "../operation.js"; import { AccessToken, @@ -19,7 +19,7 @@ import { codecForChallengeStatus, codecForChallengerAuthResponse, codecForChallengerInfoResponse, - codecForChallengerTermsOfServiceResponse + codecForChallengerTermsOfServiceResponse, } from "./types.js"; import { CacheEvictor, @@ -131,6 +131,8 @@ export class ChallengerHttpClient { return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.NotAcceptable: return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.TooManyRequests: + return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.InternalServerError: return opKnownHttpFailure(resp.status, resp); default: @@ -144,7 +146,7 @@ export class ChallengerHttpClient { * https://docs.taler.net/core/api-challenger.html#post--challenge-$NONCE * */ - async challenge(nonce: string, body: Record<"email", string>) { + async challenge(nonce: string, body: Record<string, string>) { const url = new URL(`challenge/${nonce}`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { @@ -164,8 +166,6 @@ export class ChallengerHttpClient { } case HttpStatusCode.BadRequest: return opKnownHttpFailure(resp.status, resp); - case HttpStatusCode.Forbidden: - return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.NotFound: return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.NotAcceptable: @@ -205,7 +205,11 @@ export class ChallengerHttpClient { case HttpStatusCode.BadRequest: return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.Forbidden: - return opKnownHttpFailure(resp.status, resp); + return opKnownAlternativeFailure( + resp, + HttpStatusCode.Forbidden, + codecForChallengeInvalidPinResponse(), + ); case HttpStatusCode.NotFound: return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.NotAcceptable: diff --git a/packages/taler-util/src/http-client/types.ts b/packages/taler-util/src/http-client/types.ts index 6e758773c..3816b1598 100644 --- a/packages/taler-util/src/http-client/types.ts +++ b/packages/taler-util/src/http-client/types.ts @@ -1567,6 +1567,14 @@ export const codecForChallengerTermsOfServiceResponse = .property("name", codecForConstString("challenger")) .property("version", codecForString()) .property("implementation", codecOptional(codecForString())) + .property("restrictions", codecOptional(codecForMap(codecForAny()))) + .property( + "address_type", + codecForEither( + codecForConstString("phone"), + codecForConstString("email"), + ), + ) .build("ChallengerApi.ChallengerTermsOfServiceResponse"); export const codecForChallengeSetupResponse = @@ -1578,10 +1586,13 @@ export const codecForChallengeSetupResponse = export const codecForChallengeStatus = (): Codec<ChallengerApi.ChallengeStatus> => buildCodecForObject<ChallengerApi.ChallengeStatus>() - .property("restrictions", codecOptional(codecForMap(codecForAny()))) .property("fix_address", codecForBoolean()) + .property("solved", codecForBoolean()) .property("last_address", codecOptional(codecForMap(codecForAny()))) .property("changes_left", codecForNumber()) + .property("retransmission_time", codecForTimestamp) + .property("pin_transmissions_left", codecForNumber()) + .property("auth_attempts_left", codecForNumber()) .build("ChallengerApi.ChallengeStatus"); export const codecForChallengeResponse = @@ -1596,10 +1607,10 @@ export const codecForChallengeCreateResponse = (): Codec<ChallengerApi.ChallengeCreateResponse> => buildCodecForObject<ChallengerApi.ChallengeCreateResponse>() .property("attempts_left", codecForNumber()) - .property("address", codecForAny()) .property("type", codecForConstString("created")) + .property("address", codecForAny()) .property("transmitted", codecForBoolean()) - .property("next_tx_time", codecForString()) + .property("retransmission_time", codecForTimestamp) .build("ChallengerApi.ChallengeCreateResponse"); export const codecForChallengeRedirect = @@ -5385,6 +5396,19 @@ export namespace ChallengerApi { // URN of the implementation (needed to interpret 'revision' in version). // @since v0, may become mandatory in the future. implementation?: string; + + // Object; map of keys (names of the fields of the address + // to be entered by the user) to objects with a "regex" (string) + // containing an extended Posix regular expression for allowed + // address field values, and a "hint"/"hint_i18n" giving a + // human-readable explanation to display if the value entered + // by the user does not match the regex. Keys that are not mapped + // to such an object have no restriction on the value provided by + // the user. See "ADDRESS_RESTRICTIONS" in the challenger configuration. + restrictions: Record<string, Restriction> | undefined; + + // @since v2. + address_type: "email" | "phone"; } export interface ChallengeSetupResponse { @@ -5399,16 +5423,6 @@ export namespace ChallengerApi { } export interface ChallengeStatus { - // Object; map of keys (names of the fields of the address - // to be entered by the user) to objects with a "regex" (string) - // containing an extended Posix regular expression for allowed - // address field values, and a "hint"/"hint_i18n" giving a - // human-readable explanation to display if the value entered - // by the user does not match the regex. Keys that are not mapped - // to such an object have no restriction on the value provided by - // the user. See "ADDRESS_RESTRICTIONS" in the challenger configuration. - restrictions: Record<string, Restriction> | undefined; - // indicates if the given address cannot be changed anymore, the // form should be read-only if set to true. fix_address: boolean; @@ -5420,6 +5434,25 @@ export namespace ChallengerApi { // number of times the address can still be changed, may or may not be // shown to the user changes_left: Integer; + + // is the challenge already solved? + solved: boolean; + + // when we would re-transmit the challenge the next + // time (at the earliest) if requested by the user + // only present if challenge already created + // @since v2 + retransmission_time: Timestamp; + + // how many times might the PIN still be retransmitted + // only present if challenge already created + // @since v2 + pin_transmissions_left: Integer; + + // how many times might the user still try entering the PIN code + // only present if challenge already created + // @since v2 + auth_attempts_left: Integer; } export type ChallengeResponse = ChallengeRedirect | ChallengeCreateResponse; @@ -5447,7 +5480,7 @@ export namespace ChallengerApi { // timestamp explaining when we would re-transmit the challenge the next // time (at the earliest) if requested by the user - next_tx_time: string; + retransmission_time: TalerProtocolTimestamp; } export type ChallengeSolveResponse = ChallengeRedirect | InvalidPinResponse; diff --git a/packages/taler-util/src/http-impl.node.ts b/packages/taler-util/src/http-impl.node.ts index d27fd878d..fc5fe5e98 100644 --- a/packages/taler-util/src/http-impl.node.ts +++ b/packages/taler-util/src/http-impl.node.ts @@ -237,7 +237,7 @@ export class HttpLibImpl implements HttpRequestLibrary { }; doCleanup(); if (SHOW_CURL_HTTP_REQUEST) { - console.log(`TALER_API_DEBUG: ${textDecoder.decode(data)}`) + console.log(`TALER_API_DEBUG: ${res.statusCode} ${textDecoder.decode(data)}`) } resolve(resp); }); diff --git a/packages/taler-util/src/http-impl.qtart.ts b/packages/taler-util/src/http-impl.qtart.ts index f60c82fc3..6ccd35b83 100644 --- a/packages/taler-util/src/http-impl.qtart.ts +++ b/packages/taler-util/src/http-impl.qtart.ts @@ -102,8 +102,8 @@ export class HttpLibImpl implements HttpRequestLibrary { if (opt?.headers) { Object.entries(opt?.headers).forEach(([key, value]) => { if (value === undefined) return; - requestHeadersMap[key] = value - }) + requestHeadersMap[key] = value; + }); } let headersList: string[] = []; for (let headerName of Object.keys(requestHeadersMap)) { @@ -115,13 +115,12 @@ export class HttpLibImpl implements HttpRequestLibrary { const cancelPromCap = openPromise<QjsHttpResp>(); + logger.trace(`calling qtart fetchHttp`); + // Just like WHATWG fetch(), the qjs http client doesn't // really support cancellation, so cancellation here just // means that the result is ignored! - const { - promise: fetchProm, - cancelFn - } = qjsOs.fetchHttp(url, { + const { promise: fetchProm, cancelFn } = qjsOs.fetchHttp(url, { method, data, headers: headersList, @@ -138,6 +137,7 @@ export class HttpLibImpl implements HttpRequestLibrary { if (opt?.cancellationToken) { cancelCancelledHandler = opt.cancellationToken.onCancelled(() => { + logger.trace(`cancelling quickjs request`); cancelFn(); cancelPromCap.reject(new RequestCancelledError()); }); @@ -147,6 +147,7 @@ export class HttpLibImpl implements HttpRequestLibrary { try { res = await Promise.race([fetchProm, cancelPromCap.promise]); } catch (e) { + logger.trace(`got exception while waiting for qtart http response`); if (e instanceof RequestCancelledError) { throw TalerError.fromDetail( TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR, @@ -172,6 +173,8 @@ export class HttpLibImpl implements HttpRequestLibrary { throw e; } + logger.trace(`got qtart http response, status ${res.status}`); + if (timeoutHandle != null) { clearTimeout(timeoutHandle); } diff --git a/packages/taler-util/src/notifications.ts b/packages/taler-util/src/notifications.ts index a8a8c3299..49952295a 100644 --- a/packages/taler-util/src/notifications.ts +++ b/packages/taler-util/src/notifications.ts @@ -24,7 +24,11 @@ */ import { AbsoluteTime } from "./time.js"; import { TransactionState } from "./transactions-types.js"; -import { ExchangeEntryState, TalerErrorDetail } from "./wallet-types.js"; +import { + ExchangeEntryState, + TalerErrorDetail, + TransactionIdStr, +} from "./wallet-types.js"; export enum NotificationType { BalanceChange = "balance-change", @@ -134,6 +138,12 @@ export enum ObservabilityEventType { CryptoFinishSuccess = "crypto-finish-success", CryptoFinishError = "crypto-finish-error", Message = "message", + /** + * Declare that an observability event is relevant to a particular transaction. + * If emitted from a request/task, all past/future events for that request/task + * should be shown for the transaction as well. + */ + DeclareConcernsTransaction = "declare-concerns-transaction", } export type ObservabilityEvent = @@ -217,6 +227,10 @@ export type ObservabilityEvent = | { type: ObservabilityEventType.Message; contents: string; + } + | { + type: ObservabilityEventType.DeclareConcernsTransaction; + transactionId: TransactionIdStr; }; export interface BackupOperationErrorNotification { diff --git a/packages/taler-util/src/operation.ts b/packages/taler-util/src/operation.ts index e2ab9d4e4..2d17238dc 100644 --- a/packages/taler-util/src/operation.ts +++ b/packages/taler-util/src/operation.ts @@ -146,7 +146,10 @@ export function opKnownTalerFailure<T extends TalerErrorCode>( return { type: "fail", case: s, detail }; } -export function opUnknownFailure(resp: HttpResponse, error: TalerErrorDetail): never { +export function opUnknownFailure( + resp: HttpResponse, + error: TalerErrorDetail, +): never { throw TalerError.fromDetail( TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR, { @@ -179,15 +182,51 @@ export function narrowOpSuccessOrThrow<Body, ErrorEnum>( } } +export async function succeedOrThrow<R, E>( + promise: Promise<OperationResult<R, E>>, +): Promise<R> { + const resp = await promise; + if (isOperationOk(resp)) { + return resp.body; + } + + if (isOperationFail(resp)) { + throw TalerError.fromUncheckedDetail({ ...resp, case: resp.case } as any); + } + throw TalerError.fromException(resp); +} + +export async function failOrThrow<E>( + s: E, + promise: Promise<OperationResult<unknown, E>>, +): Promise<TalerErrorDetail | undefined> { + const resp = await promise; + if (isOperationOk(resp)) { + throw TalerError.fromException( + new Error(`request succeed but failure "${s}" was expected`), + ); + } + if (isOperationFail(resp) && resp.case === s) { + return resp.detail; + } + throw TalerError.fromException( + new Error( + `request failed with "${JSON.stringify( + resp, + )}" but case "${s}" was expected`, + ), + ); +} + export type ResultByMethod< TT extends object, p extends keyof TT, > = TT[p] extends (...args: any[]) => infer Ret ? Ret extends Promise<infer Result> - ? Result extends OperationResult<any, any> - ? Result - : never - : never //api always use Promises + ? Result extends OperationResult<any, any> + ? Result + : never + : never //api always use Promises : never; //error cases just for functions export type FailCasesByMethod<TT extends object, p extends keyof TT> = Exclude< @@ -195,4 +234,4 @@ export type FailCasesByMethod<TT extends object, p extends keyof TT> = Exclude< OperationOk<any> >; -export type RedirectResult = { redirectURL: URL } +export type RedirectResult = { redirectURL: URL }; diff --git a/packages/taler-util/src/qr.ts b/packages/taler-util/src/qr.ts index 372291250..4d90ccf14 100644 --- a/packages/taler-util/src/qr.ts +++ b/packages/taler-util/src/qr.ts @@ -34,6 +34,9 @@ function encodePaytoAsSwissQrBill(paytoUri: string): EncodeResult { return { type: "skip" }; } const amountStr = parsedPayto.params["amount"]; + if (amountStr === undefined) { + return { type: "skip" }; + } const iban = parsedPayto.targetPath; const countryCode = iban.slice(0, 2); const lines = [ @@ -105,7 +108,9 @@ function encodePaytoAsEpcQr(paytoUri: string): EncodeResult { "", // optional BIC parsedPayto.params["receiver-name"], // Beneficiary name parsedPayto.targetPath, // Beneficiary IBAN - `${Amounts.currencyOf(amountStr)}${Amounts.stringifyValue(amountStr, 2)}`, // Amount + amountStr !== undefined + ? `${Amounts.currencyOf(amountStr)}${Amounts.stringifyValue(amountStr, 2)}` + : "", // Amount (optional) "", // AT-44 Purpose parsedPayto.params["message"], // AT-05 Unstructured remittance information ]; diff --git a/packages/taler-util/src/taler-types.ts b/packages/taler-util/src/taler-types.ts index 66f98ea9a..ac42ca278 100644 --- a/packages/taler-util/src/taler-types.ts +++ b/packages/taler-util/src/taler-types.ts @@ -723,6 +723,8 @@ export class ExchangeKeysJson { currency: string; + currency_specification?: CurrencySpecification; + /** * The exchange's master public key. */ @@ -1504,6 +1506,7 @@ export const codecForExchangeKeysJson = (): Codec<ExchangeKeysJson> => buildCodecForObject<ExchangeKeysJson>() .property("base_url", codecForString()) .property("currency", codecForString()) + .property("currency_specification", codecOptional(codecForCurrencySpecificiation())) .property("master_public_key", codecForString()) .property("auditors", codecForList(codecForAuditor())) .property("list_issue_date", codecForTimestamp) diff --git a/packages/taler-util/src/taleruri.test.ts b/packages/taler-util/src/taleruri.test.ts index b92366fb3..d80470dab 100644 --- a/packages/taler-util/src/taleruri.test.ts +++ b/packages/taler-util/src/taleruri.test.ts @@ -54,6 +54,18 @@ test("taler withdraw uri parsing", (t) => { t.is(r1.bankIntegrationApiBaseUrl, "https://bank.example.com/"); }); +test("taler withdraw uri parsing with external confirmation", (t) => { + const url1 = "taler://withdraw/bank.example.com/12345?external-confirmation=1"; + const r1 = parseWithdrawUri(url1); + if (!r1) { + t.fail(); + return; + } + t.is(r1.externalConfirmation, true); + t.is(r1.withdrawalOperationId, "12345"); + t.is(r1.bankIntegrationApiBaseUrl, "https://bank.example.com/"); +}); + test("taler withdraw uri parsing (http)", (t) => { const url1 = "taler+http://withdraw/bank.example.com/12345"; const r1 = parseWithdrawUri(url1); diff --git a/packages/taler-util/src/taleruri.ts b/packages/taler-util/src/taleruri.ts index 54b7525e3..d3186d2f5 100644 --- a/packages/taler-util/src/taleruri.ts +++ b/packages/taler-util/src/taleruri.ts @@ -29,6 +29,7 @@ import { opFixedSuccess, opKnownTalerFailure } from "./operation.js"; import { TalerErrorCode } from "./taler-error-codes.js"; import { AmountString } from "./taler-types.js"; import { URL, URLSearchParams } from "./url.js"; + /** * A parsed taler URI. */ @@ -89,6 +90,7 @@ export interface WithdrawUriResult { type: TalerUriAction.Withdraw; bankIntegrationApiBaseUrl: string; withdrawalOperationId: string; + externalConfirmation?: boolean; } export interface RefundUriResult { @@ -140,7 +142,12 @@ export function parseWithdrawUriWithError(s: string) { if (pi.type === "fail") { return pi; } - const parts = pi.body.rest.split("/"); + + const c = pi.body.rest.split("?", 2); + const path = c[0]; + const q = new URLSearchParams(c[1] ?? ""); + + const parts = path.split("/"); if (parts.length < 2) { return opKnownTalerFailure(TalerErrorCode.WALLET_TALER_URI_MALFORMED, { @@ -166,6 +173,7 @@ export function parseWithdrawUriWithError(s: string) { `${pi.body.innerProto}://${p}/`, ), withdrawalOperationId: withdrawId, + externalConfirmation: q.get("external-confirmation") == "1", }; return opFixedSuccess(result); } diff --git a/packages/taler-util/src/transactions-types.ts b/packages/taler-util/src/transactions-types.ts index a6ac5aec6..b4e2738ee 100644 --- a/packages/taler-util/src/transactions-types.ts +++ b/packages/taler-util/src/transactions-types.ts @@ -299,6 +299,11 @@ interface WithdrawalDetailsForTalerBankIntegrationApi { */ reserveIsReady: boolean; + /** + * Is the bank transfer for the withdrawal externally confirmed? + */ + externalConfirmation?: boolean; + exchangeCreditAccountDetails?: WithdrawalExchangeAccountDetails[]; } diff --git a/packages/taler-util/src/wallet-types.ts b/packages/taler-util/src/wallet-types.ts index 2c92d9295..ec401f3f6 100644 --- a/packages/taler-util/src/wallet-types.ts +++ b/packages/taler-util/src/wallet-types.ts @@ -558,11 +558,13 @@ export enum ScopeType { } export type ScopeInfoGlobal = { type: ScopeType.Global; currency: string }; + export type ScopeInfoExchange = { type: ScopeType.Exchange; currency: string; url: string; }; + export type ScopeInfoAuditor = { type: ScopeType.Auditor; currency: string; @@ -571,6 +573,22 @@ export type ScopeInfoAuditor = { export type ScopeInfo = ScopeInfoGlobal | ScopeInfoExchange | ScopeInfoAuditor; +/** + * Encode scope info as a string. + * + * Format must be stable as it's used in the database. + */ +export function stringifyScopeInfo(si: ScopeInfo): string { + switch (si.type) { + case ScopeType.Global: + return `taler-si:global/${si.currency}}`; + case ScopeType.Auditor: + return `taler-si:auditor/${si.currency}/${encodeURIComponent(si.url)}`; + case ScopeType.Exchange: + return `taler-si:exchange/${si.currency}/${encodeURIComponent(si.url)}`; + } +} + export interface BalancesResponse { balances: WalletBalance[]; } @@ -3439,3 +3457,9 @@ export const codecForGetQrCodesForPaytoRequest = () => export interface GetQrCodesForPaytoResponse { codes: QrCodeSpec[]; } + +export type EmptyObject = Record<string, never>; + +export const codecForEmptyObject = (): Codec<EmptyObject> => + buildCodecForObject<EmptyObject>() + .build("EmptyObject");
\ No newline at end of file |