summaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
Diffstat (limited to 'packages')
-rw-r--r--packages/taler-util/src/http-client/bank-core.ts3
-rw-r--r--packages/taler-util/src/http-client/challenger.ts260
-rw-r--r--packages/taler-util/src/http-client/types.ts268
-rw-r--r--packages/taler-util/src/http-impl.node.ts8
-rw-r--r--packages/taler-util/src/index.ts1
5 files changed, 479 insertions, 61 deletions
diff --git a/packages/taler-util/src/http-client/bank-core.ts b/packages/taler-util/src/http-client/bank-core.ts
index be37560cd..97c1727ff 100644
--- a/packages/taler-util/src/http-client/bank-core.ts
+++ b/packages/taler-util/src/http-client/bank-core.ts
@@ -24,11 +24,10 @@ import {
OperationOk,
TalerErrorCode,
codecForChallenge,
- codecForTalerErrorDetail,
codecForTanTransmission,
opKnownAlternativeFailure,
opKnownHttpFailure,
- opKnownTalerFailure,
+ opKnownTalerFailure
} from "@gnu-taler/taler-util";
import {
HttpRequestLibrary,
diff --git a/packages/taler-util/src/http-client/challenger.ts b/packages/taler-util/src/http-client/challenger.ts
new file mode 100644
index 000000000..fa4214aa6
--- /dev/null
+++ b/packages/taler-util/src/http-client/challenger.ts
@@ -0,0 +1,260 @@
+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 {
+ FailCasesByMethod,
+ ResultByMethod,
+ opKnownHttpFailure,
+ opSuccessFromHttp,
+ opUnknownFailure
+} from "../operation.js";
+import {
+ AccessToken,
+ codecForChallengeCreateResponse,
+ codecForChallengeSetupResponse,
+ codecForChallengeStatus,
+ codecForChallengerAuthResponse,
+ codecForChallengerInfoResponse,
+ codecForChallengerTermsOfServiceResponse
+} from "./types.js";
+import { makeBearerTokenAuthHeader } from "./utils.js";
+
+export type ChallengerResultByMethod<prop extends keyof ChallengerHttpClient> =
+ ResultByMethod<ChallengerHttpClient, prop>;
+export type ChallengerErrorsByMethod<prop extends keyof ChallengerHttpClient> =
+ FailCasesByMethod<ChallengerHttpClient, prop>;
+
+/**
+ */
+export class ChallengerHttpClient {
+ httpLib: HttpRequestLibrary;
+ public readonly PROTOCOL_VERSION = "1:0:0";
+
+ constructor(
+ readonly baseUrl: string,
+ httpClient?: HttpRequestLibrary,
+ ) {
+ this.httpLib = httpClient ?? createPlatformHttpLib();
+ }
+
+ isCompatible(version: string): boolean {
+ const compare = LibtoolVersion.compare(this.PROTOCOL_VERSION, version);
+ return compare?.compatible ?? false;
+ }
+ /**
+ * https://docs.taler.net/core/api-challenger.html#get--config
+ *
+ */
+ async getConfig() {
+ const url = new URL(`config`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(
+ resp,
+ codecForChallengerTermsOfServiceResponse(),
+ );
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+ /**
+ * https://docs.taler.net/core/api-challenger.html#post--setup-$CLIENT_ID
+ *
+ */
+ async setup(clientId: string, token: AccessToken) {
+ const url = new URL(`setup/${clientId}`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ headers: {
+ Authorization: makeBearerTokenAuthHeader(token),
+ },
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForChallengeSetupResponse());
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ // LOGIN
+
+ /**
+ * https://docs.taler.net/core/api-challenger.html#post--authorize-$NONCE
+ *
+ */
+ async login(nonce: string, clientId: string, redirectUri: string, state: string | undefined) {
+ const url = new URL(`authorize/${nonce}`, this.baseUrl);
+ url.searchParams.set("response_type", "code");
+ url.searchParams.set("client_id", clientId);
+ url.searchParams.set("redirect_uri", redirectUri);
+ if (state) {
+ url.searchParams.set("state", state);
+ }
+ // url.searchParams.set("scope", "code");
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForChallengeStatus());
+ case HttpStatusCode.BadRequest:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotAcceptable:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.InternalServerError:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ // CHALLENGE
+
+ /**
+ * https://docs.taler.net/core/api-challenger.html#post--challenge-$NONCE
+ *
+ */
+ async challenge(nonce: string, body: Record<"email", string>) {
+ const url = new URL(`challenge/${nonce}`, this.baseUrl);
+
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body: new URLSearchParams(Object.entries(body)).toString(),
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded"
+ }
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForChallengeCreateResponse());
+ case HttpStatusCode.BadRequest:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ 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:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ // SOLVE
+
+ /**
+ * https://docs.taler.net/core/api-challenger.html#post--solve-$NONCE
+ *
+ */
+ async solve(nonce: string, body: Record<string, string>) {
+ const url = new URL(`solve/${nonce}`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body: new URLSearchParams(Object.entries(body)).toString(),
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded"
+ },
+ redirect: "manual",
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Found:
+ const redirect = resp.headers.get("Location")!
+ const uri = new URL(redirect)
+ const code = uri.searchParams.get("code")!
+ return {
+ type: "ok" as const,
+ body: { code }
+ }
+ // return opSuccessFromHttp(resp, codecForChallengeCreateResponse());
+ case HttpStatusCode.BadRequest:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ 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:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ // AUTH
+
+ /**
+ * https://docs.taler.net/core/api-challenger.html#post--token
+ *
+ */
+ async token(
+ client_id: string,
+ redirect_uri: string,
+ client_secret: AccessToken,
+ code: string,
+ ) {
+ const url = new URL(`token`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded"
+ },
+ body: new URLSearchParams(Object.entries({
+ client_id,
+ redirect_uri,
+ client_secret,
+ code,
+ grant_type: "authorization_code",
+ })).toString(),
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForChallengerAuthResponse());
+ case HttpStatusCode.Forbidden:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ // INFO
+
+ /**
+ * https://docs.taler.net/core/api-challenger.html#get--info
+ *
+ */
+ async info(token: AccessToken) {
+ const url = new URL(`info`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers: {
+ Authorization: makeBearerTokenAuthHeader(token),
+ },
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForChallengerInfoResponse());
+ case HttpStatusCode.Forbidden:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+}
diff --git a/packages/taler-util/src/http-client/types.ts b/packages/taler-util/src/http-client/types.ts
index 94eafb329..e12c2ed6b 100644
--- a/packages/taler-util/src/http-client/types.ts
+++ b/packages/taler-util/src/http-client/types.ts
@@ -195,12 +195,36 @@ export type AccessToken = string & {
/**
* Create a rfc8959 access token.
* Adds secret-token: prefix if there is none.
- *
- * @param token
- * @returns
+ *
+ * @deprecated use createRFC8959AccessToken
+ * @param token
+ * @returns
*/
export function createAccessToken(token: string): AccessToken {
- return (token.startsWith("secret-token:") ? token : `secret-token:${token}`) as AccessToken
+ return (
+ token.startsWith("secret-token:") ? token : `secret-token:${token}`
+ ) as AccessToken;
+}
+/**
+ * Create a rfc8959 access token.
+ * Adds secret-token: prefix if there is none.
+ *
+ * @param token
+ * @returns
+ */
+export function createRFC8959AccessToken(token: string): AccessToken {
+ return (
+ token.startsWith("secret-token:") ? token : `secret-token:${token}`
+ ) as AccessToken;
+}
+/**
+ * Conver string to access token.
+ *
+ * @param clientSecret
+ * @returns
+ */
+export function createClientSecretAccessToken(clientSecret: string): AccessToken {
+ return clientSecret as AccessToken;
}
declare const __officer_signature: unique symbol;
@@ -1430,51 +1454,6 @@ export const codecForAmlDecision = (): Codec<TalerExchangeApi.AmlDecision> =>
.property("kyc_requirements", codecOptional(codecForList(codecForString())))
.build("TalerExchangeApi.AmlDecision");
-// version: string;
-
-// // Name of the API.
-// name: "taler-conversion-info";
-
-// // Currency used by this bank.
-// regional_currency: string;
-
-// // How the bank SPA should render this currency.
-// regional_currency_specification: CurrencySpecification;
-
-// // External currency used during conversion.
-// fiat_currency: string;
-
-// // How the bank SPA should render this currency.
-// fiat_currency_specification: CurrencySpecification;
-
-// Extra conversion rate information.
-// // Only present if server opts in to report the static conversion rate.
-// conversion_info?: {
-
-// // Fee to subtract after applying the cashin ratio.
-// cashin_fee: AmountString;
-
-// // Fee to subtract after applying the cashout ratio.
-// cashout_fee: AmountString;
-
-// // Minimum amount authorised for cashin, in fiat before conversion
-// cashin_min_amount: AmountString;
-
-// // Minimum amount authorised for cashout, in regional before conversion
-// cashout_min_amount: AmountString;
-
-// // Smallest possible regional amount, converted amount is rounded to this amount
-// cashin_tiny_amount: AmountString;
-
-// // Smallest possible fiat amount, converted amount is rounded to this amount
-// cashout_tiny_amount: AmountString;
-
-// // Rounding mode used during cashin conversion
-// cashin_rounding_mode: "zero" | "up" | "nearest";
-
-// // Rounding mode used during cashout conversion
-// cashout_rounding_mode: "zero" | "up" | "nearest";
-// }
export const codecForConversionInfo =
(): Codec<TalerBankConversionApi.ConversionInfo> =>
buildCodecForObject<TalerBankConversionApi.ConversionInfo>()
@@ -1520,11 +1499,65 @@ export const codecForConversionBankConfig =
.property("conversion_rate", codecForConversionInfo())
.build("ConversionBankConfig.IntegrationConfig");
-// export const codecFor =
-// (): Codec<TalerWireGatewayApi.PublicAccountsResponse> =>
-// buildCodecForObject<TalerWireGatewayApi.PublicAccountsResponse>()
-// .property("", codecForString())
-// .build("TalerWireGatewayApi.PublicAccountsResponse");
+export const codecForChallengerTermsOfServiceResponse =
+ (): Codec<ChallengerApi.ChallengerTermsOfServiceResponse> =>
+ buildCodecForObject<ChallengerApi.ChallengerTermsOfServiceResponse>()
+ .property("name", codecForConstString("challenger"))
+ .property("version", codecForString())
+ .property("implementation", codecOptional(codecForString()))
+ .build("ChallengerApi.ChallengerTermsOfServiceResponse");
+
+export const codecForChallengeSetupResponse =
+ (): Codec<ChallengerApi.ChallengeSetupResponse> =>
+ buildCodecForObject<ChallengerApi.ChallengeSetupResponse>()
+ .property("nonce", codecForString())
+ .build("ChallengerApi.ChallengeSetupResponse");
+
+export const codecForChallengeStatus =
+ (): Codec<ChallengerApi.ChallengeStatus> =>
+ buildCodecForObject<ChallengerApi.ChallengeStatus>()
+ .property("restrictions", codecForAny())
+ .property("fix_address", codecForBoolean())
+ .property("last_address", codecForAny())
+ .property("changes_left", codecForNumber())
+ .build("ChallengerApi.ChallengeStatus");
+export const codecForChallengeCreateResponse =
+ (): Codec<ChallengerApi.ChallengeCreateResponse> =>
+ buildCodecForObject<ChallengerApi.ChallengeCreateResponse>()
+ .property("attempts_left", codecForNumber())
+ .property("address", codecForAny())
+ .property("transmitted", codecForBoolean())
+ .property("next_tx_time", codecForString())
+ .build("ChallengerApi.ChallengeCreateResponse");
+
+export const codecForInvalidPinResponse =
+ (): Codec<ChallengerApi.InvalidPinResponse> =>
+ buildCodecForObject<ChallengerApi.InvalidPinResponse>()
+ .property("ec", codecForNumber())
+ .property("hint", codecForAny())
+ .property("addresses_left", codecForNumber())
+ .property("pin_transmissions_left", codecForNumber())
+ .property("auth_attempts_left", codecForNumber())
+ .property("exhausted", codecForBoolean())
+ .property("no_challenge", codecForBoolean())
+ .build("ChallengerApi.InvalidPinResponse");
+
+export const codecForChallengerAuthResponse =
+ (): Codec<ChallengerApi.ChallengerAuthResponse> =>
+ buildCodecForObject<ChallengerApi.ChallengerAuthResponse>()
+ .property("access_token", codecForString())
+ .property("token_type", codecForAny())
+ .property("expires_in", codecForNumber())
+ .build("ChallengerApi.ChallengerAuthResponse");
+
+export const codecForChallengerInfoResponse =
+ (): Codec<ChallengerApi.ChallengerInfoResponse> =>
+ buildCodecForObject<ChallengerApi.ChallengerInfoResponse>()
+ .property("id", codecForNumber())
+ .property("address", codecForAny())
+ .property("address_type", codecForString())
+ .property("expires", codecForTimestamp)
+ .build("ChallengerApi.ChallengerInfoResponse");
type EmailAddress = string;
type PhoneNumber = string;
@@ -1896,6 +1929,7 @@ export namespace TalerBankConversionApi {
cashout_rounding_mode: RoundingMode;
}
}
+
export namespace TalerBankIntegrationApi {
export interface BankVersion {
// libtool-style representation of the Bank protocol version, see
@@ -1973,6 +2007,7 @@ export namespace TalerBankIntegrationApi {
confirm_transfer_url?: string;
}
}
+
export namespace TalerCorebankApi {
export interface IntegrationConfig {
// libtool-style representation of the Bank protocol version, see
@@ -2103,7 +2138,7 @@ export namespace TalerCorebankApi {
// query string parameter of the 'payto' field. In case it
// is given in both places, the paytoUri's takes the precedence.
amount?: AmountString;
-
+
// Nonce to make the request idempotent. Requests with the same
// request_uid that differ in any of the other fields
// are rejected.
@@ -5223,3 +5258,126 @@ export namespace TalerMerchantApi {
master_pub: EddsaPublicKey;
}
}
+
+export namespace ChallengerApi {
+ export interface ChallengerTermsOfServiceResponse {
+ // Name of the service
+ name: "challenger";
+
+ // libtool-style representation of the Challenger protocol version, see
+ // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning
+ // The format is "current:revision:age".
+ version: string;
+
+ // URN of the implementation (needed to interpret 'revision' in version).
+ // @since v0, may become mandatory in the future.
+ implementation?: string;
+ }
+
+ export interface ChallengeSetupResponse {
+ // Nonce to use when constructing /authorize endpoint.
+ nonce: string;
+ }
+
+ 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: Object;
+
+ // indicates if the given address cannot be changed anymore, the
+ // form should be read-only if set to true.
+ fix_address: boolean;
+
+ // form values from the previous submission if available, details depend
+ // on the ADDRESS_TYPE, should be used to pre-populate the form
+ last_address: Object;
+
+ // number of times the address can still be changed, may or may not be
+ // shown to the user
+ changes_left: Integer;
+ }
+
+ export interface ChallengeCreateResponse {
+ // how many more attempts are allowed, might be shown to the user,
+ // highlighting might be appropriate for low values such as 1 or 2 (the
+ // form will never be used if the value is zero)
+ attempts_left: Integer;
+
+ // the address that is being validated, might be shown or not
+ address: Object;
+
+ // true if we just retransmitted the challenge, false if we sent a
+ // challenge recently and thus refused to transmit it again this time;
+ // might make a useful hint to the user
+ transmitted: boolean;
+
+ // timestamp explaining when we would re-transmit the challenge the next
+ // time (at the earliest) if requested by the user
+ next_tx_time: String;
+ }
+
+ export interface InvalidPinResponse {
+ // numeric Taler error code, should be shown to indicate the error
+ // compactly for reporting to developers
+ ec: Integer;
+
+ // human-readable Taler error code, should be shown for the user to
+ // understand the error
+ hint: String;
+
+ // how many times is the user still allowed to change the address;
+ // if 0, the user should not be shown a link to jump to the
+ // address entry form
+ addresses_left: Integer;
+
+ // how many times might the PIN still be retransmitted
+ pin_transmissions_left: Integer;
+
+ // how many times might the user still try entering the PIN code
+ auth_attempts_left: Integer;
+
+ // if true, the PIN was not even evaluated as the user previously
+ // exhausted the number of attempts
+ exhausted: boolean;
+
+ // if true, the PIN was not even evaluated as no challenge was ever
+ // issued (the user must have skipped the step of providing their
+ // address first!)
+ no_challenge: boolean;
+ }
+
+ export interface ChallengerAuthResponse {
+ // Token used to authenticate access in /info.
+ access_token: string;
+
+ // Type of the access token.
+ token_type: "Bearer";
+
+ // Amount of time that an access token is valid (in seconds).
+ expires_in: Integer;
+ }
+
+ export interface ChallengerInfoResponse {
+ // Unique ID of the record within Challenger
+ // (identifies the rowid of the token).
+ id: Integer;
+
+ // Address that was validated.
+ // Key-value pairs, details depend on the
+ // address_type.
+ address: Object;
+
+ // Type of the address.
+ address_type: string;
+
+ // How long do we consider the address to be
+ // valid for this user.
+ expires: Timestamp;
+ }
+}
diff --git a/packages/taler-util/src/http-impl.node.ts b/packages/taler-util/src/http-impl.node.ts
index 8606bc451..45a12c258 100644
--- a/packages/taler-util/src/http-impl.node.ts
+++ b/packages/taler-util/src/http-impl.node.ts
@@ -123,8 +123,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;
+ });
}
logger.trace(`request timeout ${timeoutMs} ms`);
@@ -181,10 +181,10 @@ export class HttpLibImpl implements HttpRequestLibrary {
return arg + " '" + String(v) + "'";
}
console.log(
- `curl -X ${options.method} ${parsedUrl.href} ${ifUndefined(
+ `curl -X ${options.method} "${parsedUrl.href}" ${headers} ${ifUndefined(
"-d",
payload,
- )} ${headers}`,
+ )}`,
);
}
diff --git a/packages/taler-util/src/index.ts b/packages/taler-util/src/index.ts
index 9bd4834d2..24d6e9950 100644
--- a/packages/taler-util/src/index.ts
+++ b/packages/taler-util/src/index.ts
@@ -22,6 +22,7 @@ export * from "./http-client/bank-conversion.js";
export * from "./http-client/authentication.js";
export * from "./http-client/bank-core.js";
export * from "./http-client/merchant.js";
+export * from "./http-client/challenger.js";
export * from "./http-client/bank-integration.js";
export * from "./http-client/bank-revenue.js";
export * from "./http-client/bank-wire.js";