summaryrefslogtreecommitdiff
path: root/packages/taler-util
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2023-02-15 23:32:42 +0100
committerFlorian Dold <florian@dold.me>2023-02-16 02:50:29 +0100
commit825d2c4352022e7397854b2bd9ba7d3589873c07 (patch)
treed23530bf8408367439e6b3820ea0c4269bfeb39a /packages/taler-util
parentcb2f4c21d85707abb0221cbf2a859a98836b2d44 (diff)
downloadwallet-core-825d2c4352022e7397854b2bd9ba7d3589873c07.tar.gz
wallet-core-825d2c4352022e7397854b2bd9ba7d3589873c07.tar.bz2
wallet-core-825d2c4352022e7397854b2bd9ba7d3589873c07.zip
make wallet-cli runnable under qtart
Diffstat (limited to 'packages/taler-util')
-rw-r--r--packages/taler-util/package.json30
-rw-r--r--packages/taler-util/src/clk.ts53
-rw-r--r--packages/taler-util/src/compat.d.ts22
-rw-r--r--packages/taler-util/src/compat.node.ts59
-rw-r--r--packages/taler-util/src/compat.qtart.ts53
-rw-r--r--packages/taler-util/src/errors.ts248
-rw-r--r--packages/taler-util/src/http-common.ts39
-rw-r--r--packages/taler-util/src/http-impl.node.d.ts17
-rw-r--r--packages/taler-util/src/http-impl.node.ts175
-rw-r--r--packages/taler-util/src/http-impl.qtart.ts127
-rw-r--r--packages/taler-util/src/http.ts360
-rw-r--r--packages/taler-util/src/index.browser.ts4
-rw-r--r--packages/taler-util/src/index.node.ts1
-rw-r--r--packages/taler-util/src/index.ts1
-rw-r--r--packages/taler-util/src/qtart.ts36
-rw-r--r--packages/taler-util/src/twrpc-impl.missing.ts9
16 files changed, 1201 insertions, 33 deletions
diff --git a/packages/taler-util/package.json b/packages/taler-util/package.json
index 23ff5dcfa..9cf47d256 100644
--- a/packages/taler-util/package.json
+++ b/packages/taler-util/package.json
@@ -15,12 +15,38 @@
},
"./twrpc": {
"default": "./lib/twrpc.js"
+ },
+ "./compat": {
+ "types": "./lib/compat.node.js",
+ "node": "./lib/compat.node.js",
+ "qtart": "./lib/compat.qtart.js",
+ "default": "./lib/not-implemented.js"
+ },
+ "./clk": {
+ "default": "./lib/clk.js"
+ },
+ "./http": {
+ "default": "./lib/http.js"
+ },
+ "./qtart": {
+ "types": "./lib/qtart.js",
+ "qtart": "./lib/qtart.js",
+ "default": "./lib/not-implemented.js"
}
},
"imports": {
"#twrpc-impl": {
- "node": "./lib/twrpc-impl.node.js",
- "default": "./lib/twrpc-impl.missing.js"
+ "node": "./lib/twrpc-impl.node.js"
+ },
+ "#compat-impl": {
+ "node": "./lib/compat.node.js",
+ "qtart": "./lib/compat.qtart.js",
+ "type": "./lib/compat.d.ts"
+ },
+ "#http-impl": {
+ "type": "./lib/http-impl.node.js",
+ "node": "./lib/http-impl.node.js",
+ "qtart": "./lib/http-impl.qtart.js"
}
},
"scripts": {
diff --git a/packages/taler-util/src/clk.ts b/packages/taler-util/src/clk.ts
index e99ebf733..7bcd19b04 100644
--- a/packages/taler-util/src/clk.ts
+++ b/packages/taler-util/src/clk.ts
@@ -17,10 +17,12 @@
/**
* Imports.
*/
-import process from "process";
-import path from "path";
-import readline from "readline";
-import { devNull } from "os";
+import {
+ processExit,
+ processArgv,
+ readlinePrompt,
+ pathBasename,
+} from "#compat-impl";
export namespace clk {
class Converter<T> {}
@@ -359,13 +361,13 @@ export namespace clk {
console.error(
`error: unknown option '--${r.key}' for ${currentName}`,
);
- process.exit(-1);
+ processExit(-1);
throw Error("not reached");
}
if (d.isFlag) {
if (r.value !== undefined) {
console.error(`error: flag '--${r.key}' does not take a value`);
- process.exit(-1);
+ processExit(-1);
throw Error("not reached");
}
storeFlag(d, true);
@@ -373,7 +375,7 @@ export namespace clk {
if (r.value === undefined) {
if (i === unparsedArgs.length - 1) {
console.error(`error: option '--${r.key}' needs an argument`);
- process.exit(-1);
+ processExit(-1);
throw Error("not reached");
}
storeOption(d, unparsedArgs[i + 1]);
@@ -391,7 +393,7 @@ export namespace clk {
const opt = this.shortOptions[chr];
if (!opt) {
console.error(`error: option '-${chr}' not known`);
- process.exit(-1);
+ processExit(-1);
}
if (opt.isFlag) {
storeFlag(opt, true);
@@ -399,7 +401,7 @@ export namespace clk {
if (si == optShort.length - 1) {
if (i === unparsedArgs.length - 1) {
console.error(`error: option '-${chr}' needs an argument`);
- process.exit(-1);
+ processExit(-1);
throw Error("not reached");
} else {
storeOption(opt, unparsedArgs[i + 1]);
@@ -418,7 +420,7 @@ export namespace clk {
const subcmd = this.subcommandMap[argVal];
if (!subcmd) {
console.error(`error: unknown command '${argVal}'`);
- process.exit(-1);
+ processExit(-1);
throw Error("not reached");
}
foundSubcommand = subcmd.commandGroup;
@@ -427,7 +429,7 @@ export namespace clk {
const d = this.arguments[posArgIndex];
if (!d) {
console.error(`error: too many arguments for ${currentName}`);
- process.exit(-1);
+ processExit(-1);
throw Error("not reached");
}
myArgs[d.name] = unparsedArgs[i];
@@ -437,7 +439,7 @@ export namespace clk {
if (parsedArgs[this.argKey].help) {
this.printHelp(progname, parents);
- process.exit(0);
+ processExit(0);
throw Error("not reached");
}
@@ -450,7 +452,7 @@ export namespace clk {
console.error(
`error: missing positional argument '${d.name}' for ${currentName}`,
);
- process.exit(-1);
+ processExit(-1);
throw Error("not reached");
}
}
@@ -464,7 +466,7 @@ export namespace clk {
} else {
const name = option.flagspec.join(",");
console.error(`error: missing option '${name}'`);
- process.exit(-1);
+ processExit(-1);
throw Error("not reached");
}
}
@@ -492,16 +494,16 @@ export namespace clk {
} catch (e) {
console.error(`An error occurred while running ${currentName}`);
console.error(e);
- process.exit(1);
+ processExit(1);
}
Promise.resolve(r).catch((e) => {
console.error(`An error occurred while running ${currentName}`);
console.error(e);
- process.exit(1);
+ processExit(1);
});
} else {
this.printHelp(progname, parents);
- process.exit(-1);
+ processExit(-1);
throw Error("not reached");
}
}
@@ -524,15 +526,15 @@ export namespace clk {
if (cmdlineArgs) {
args = cmdlineArgs;
} else {
- args = process.argv.slice(1);
+ args = processArgv().slice(1);
}
if (args.length < 1) {
console.error(
"Error while parsing command line arguments: not enough arguments",
);
- process.exit(-1);
+ processExit(-1);
}
- const progname = path.basename(args[0]);
+ const progname = pathBasename(args[0]);
const rest = args.slice(1);
this.mainCommand.run(progname, [], rest, {});
@@ -622,15 +624,6 @@ export namespace clk {
}
export function prompt(question: string): Promise<string> {
- const stdinReadline = readline.createInterface({
- input: process.stdin,
- output: process.stdout,
- });
- return new Promise<string>((resolve, reject) => {
- stdinReadline.question(question, (res) => {
- resolve(res);
- stdinReadline.close();
- });
- });
+ return readlinePrompt(question);
}
}
diff --git a/packages/taler-util/src/compat.d.ts b/packages/taler-util/src/compat.d.ts
new file mode 100644
index 000000000..12ba31124
--- /dev/null
+++ b/packages/taler-util/src/compat.d.ts
@@ -0,0 +1,22 @@
+/*
+ This file is part of GNU Taler
+ (C) 2023 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/>
+ */
+
+export function processExit(status: number): never;
+export function processArgv(): string[];
+export function readlinePrompt(prompt: string): Promise<string>;
+export function pathBasename(s: string): string;
+export function setUnhandledRejectionHandler(h: (e: any) => void): void;
+export function getenv(name: string): string | undefined; \ No newline at end of file
diff --git a/packages/taler-util/src/compat.node.ts b/packages/taler-util/src/compat.node.ts
new file mode 100644
index 000000000..ed27a7acd
--- /dev/null
+++ b/packages/taler-util/src/compat.node.ts
@@ -0,0 +1,59 @@
+/*
+ This file is part of GNU Taler
+ (C) 2023 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 process from "node:process";
+import readline from "node:readline";
+import path from "node:path";
+import os from "node:os";
+
+export function processExit(status: number): never {
+ process.exit(1);
+}
+
+export function processArgv(): string[] {
+ return [...process.argv];
+}
+
+export function readlinePrompt(prompt: string): Promise<string> {
+ const stdinReadline = readline.createInterface({
+ input: process.stdin,
+ output: process.stdout,
+ });
+ return new Promise<string>((resolve, reject) => {
+ stdinReadline.question(prompt, (res) => {
+ resolve(res);
+ stdinReadline.close();
+ });
+ });
+}
+
+export function pathBasename(p: string): string {
+ return path.basename(p);
+}
+
+export function pathHomedir(): string {
+ return os.homedir();
+}
+
+export function setUnhandledRejectionHandler(h: (e: any) => void): void {
+ process.on("unhandledRejection", (e) => {
+ h(e);
+ });
+}
+
+export function getenv(name: string): string | undefined {
+ return process.env[name];
+}
diff --git a/packages/taler-util/src/compat.qtart.ts b/packages/taler-util/src/compat.qtart.ts
new file mode 100644
index 000000000..f8b336b11
--- /dev/null
+++ b/packages/taler-util/src/compat.qtart.ts
@@ -0,0 +1,53 @@
+/*
+ This file is part of GNU Taler
+ (C) 2023 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/>
+ */
+
+// qtart "std" library
+// @ts-ignore
+import * as std from "std";
+
+export function processExit(status: number): never {
+ std.exit(status);
+ throw Error("not reached");
+}
+
+export function processArgv(): string[] {
+ // @ts-ignore
+ return ["qtart", ...globalThis.scriptArgs];
+}
+
+export function readlinePrompt(prompt: string): Promise<string> {
+ throw new Error("not supported");
+}
+
+export function pathBasename(p: string): string {
+ const slashIndex = p.lastIndexOf("/");
+ if (slashIndex < 0) {
+ return p;
+ }
+ return p.substring(0, slashIndex);
+}
+
+export function pathHomedir(): string {
+ return std.getenv("HOME");
+}
+
+export function setUnhandledRejectionHandler(h: (e: any) => void): void {
+ // not supported
+}
+
+export function getenv(name: string): string | undefined {
+ return std.getenv(name);
+}
diff --git a/packages/taler-util/src/errors.ts b/packages/taler-util/src/errors.ts
new file mode 100644
index 000000000..038bdbc7c
--- /dev/null
+++ b/packages/taler-util/src/errors.ts
@@ -0,0 +1,248 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019-2020 Taler Systems SA
+
+ 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/>
+ */
+
+/**
+ * Classes and helpers for error handling specific to wallet operations.
+ *
+ * @author Florian Dold <dold@taler.net>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ AbsoluteTime,
+ PayMerchantInsufficientBalanceDetails,
+ PayPeerInsufficientBalanceDetails,
+ TalerErrorCode,
+ TalerErrorDetail,
+ TransactionType,
+} from "@gnu-taler/taler-util";
+
+type empty = Record<string, never>;
+
+export interface DetailsMap {
+ [TalerErrorCode.WALLET_PENDING_OPERATION_FAILED]: {
+ innerError: TalerErrorDetail;
+ transactionId?: string;
+ };
+ [TalerErrorCode.WALLET_EXCHANGE_DENOMINATIONS_INSUFFICIENT]: {
+ exchangeBaseUrl: string;
+ };
+ [TalerErrorCode.WALLET_EXCHANGE_PROTOCOL_VERSION_INCOMPATIBLE]: {
+ exchangeProtocolVersion: string;
+ walletProtocolVersion: string;
+ };
+ [TalerErrorCode.WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK]: empty;
+ [TalerErrorCode.WALLET_TIPPING_COIN_SIGNATURE_INVALID]: empty;
+ [TalerErrorCode.WALLET_ORDER_ALREADY_CLAIMED]: {
+ orderId: string;
+ claimUrl: string;
+ };
+ [TalerErrorCode.WALLET_CONTRACT_TERMS_MALFORMED]: empty;
+ [TalerErrorCode.WALLET_CONTRACT_TERMS_SIGNATURE_INVALID]: {
+ merchantPub: string;
+ orderId: string;
+ };
+ [TalerErrorCode.WALLET_CONTRACT_TERMS_BASE_URL_MISMATCH]: {
+ baseUrlForDownload: string;
+ baseUrlFromContractTerms: string;
+ };
+ [TalerErrorCode.WALLET_INVALID_TALER_PAY_URI]: {
+ talerPayUri: string;
+ };
+ [TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR]: {
+ requestUrl: string;
+ requestMethod: string;
+ httpStatusCode: number;
+ errorResponse?: any;
+ };
+ [TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION]: {
+ stack?: string;
+ };
+ [TalerErrorCode.WALLET_BANK_INTEGRATION_PROTOCOL_VERSION_INCOMPATIBLE]: {
+ exchangeProtocolVersion: string;
+ walletProtocolVersion: string;
+ };
+ [TalerErrorCode.WALLET_CORE_API_OPERATION_UNKNOWN]: {
+ operation: string;
+ };
+ [TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED]: {
+ requestUrl: string;
+ requestMethod: string;
+ throttleStats: Record<string, unknown>;
+ };
+ [TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT]: empty;
+ [TalerErrorCode.WALLET_NETWORK_ERROR]: {
+ requestUrl: string;
+ requestMethod: string;
+ };
+ [TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE]: {
+ requestUrl: string;
+ requestMethod: string;
+ httpStatusCode: number;
+ validationError?: string;
+ };
+ [TalerErrorCode.WALLET_EXCHANGE_COIN_SIGNATURE_INVALID]: empty;
+ [TalerErrorCode.WALLET_WITHDRAWAL_GROUP_INCOMPLETE]: {
+ errorsPerCoin: Record<number, TalerErrorDetail>;
+ };
+ [TalerErrorCode.WALLET_CORE_NOT_AVAILABLE]: empty;
+ [TalerErrorCode.GENERIC_UNEXPECTED_REQUEST_ERROR]: {
+ httpStatusCode: number;
+ };
+ [TalerErrorCode.WALLET_PAY_MERCHANT_SERVER_ERROR]: {
+ requestError: TalerErrorDetail;
+ };
+ [TalerErrorCode.WALLET_CRYPTO_WORKER_ERROR]: {
+ innerError: TalerErrorDetail;
+ };
+ [TalerErrorCode.WALLET_CRYPTO_WORKER_BAD_REQUEST]: {
+ detail: string;
+ };
+ [TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED]: {
+ kycUrl: string;
+ };
+ [TalerErrorCode.WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE]: {
+ insufficientBalanceDetails: PayMerchantInsufficientBalanceDetails;
+ };
+ [TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE]: {
+ insufficientBalanceDetails: PayPeerInsufficientBalanceDetails;
+ };
+}
+
+type ErrBody<Y> = Y extends keyof DetailsMap ? DetailsMap[Y] : empty;
+
+export function makeErrorDetail<C extends TalerErrorCode>(
+ code: C,
+ detail: ErrBody<C>,
+ hint?: string,
+): TalerErrorDetail {
+ if (!hint && !(detail as any).hint) {
+ hint = getDefaultHint(code);
+ }
+ const when = AbsoluteTime.now();
+ return { code, when, hint, ...detail };
+}
+
+export function makePendingOperationFailedError(
+ innerError: TalerErrorDetail,
+ tag: TransactionType,
+ uid: string,
+): TalerError {
+ return TalerError.fromDetail(TalerErrorCode.WALLET_PENDING_OPERATION_FAILED, {
+ innerError,
+ transactionId: `${tag}:${uid}`,
+ });
+}
+
+export function summarizeTalerErrorDetail(ed: TalerErrorDetail): string {
+ const errName = TalerErrorCode[ed.code] ?? "<unknown>";
+ return `Error (${ed.code}/${errName})`;
+}
+
+function getDefaultHint(code: number): string {
+ const errName = TalerErrorCode[code];
+ if (errName) {
+ return `Error (${errName})`;
+ } else {
+ return `Error (<unknown>)`;
+ }
+}
+
+export class TalerProtocolViolationError extends Error {
+ constructor(hint?: string) {
+ let msg: string;
+ if (hint) {
+ msg = `Taler protocol violation error (${hint})`;
+ } else {
+ msg = `Taler protocol violation error`;
+ }
+ super(msg);
+ Object.setPrototypeOf(this, TalerProtocolViolationError.prototype);
+ }
+}
+
+export class TalerError<T = any> extends Error {
+ errorDetail: TalerErrorDetail & T;
+ private constructor(d: TalerErrorDetail & T) {
+ super(d.hint ?? `Error (code ${d.code})`);
+ this.errorDetail = d;
+ Object.setPrototypeOf(this, TalerError.prototype);
+ }
+
+ static fromDetail<C extends TalerErrorCode>(
+ code: C,
+ detail: ErrBody<C>,
+ hint?: string,
+ ): TalerError {
+ if (!hint) {
+ hint = getDefaultHint(code);
+ }
+ const when = AbsoluteTime.now();
+ return new TalerError<unknown>({ code, when, hint, ...detail });
+ }
+
+ static fromUncheckedDetail(d: TalerErrorDetail): TalerError {
+ return new TalerError<unknown>({ ...d });
+ }
+
+ static fromException(e: any): TalerError {
+ const errDetail = getErrorDetailFromException(e);
+ return new TalerError(errDetail);
+ }
+
+ hasErrorCode<C extends keyof DetailsMap>(
+ code: C,
+ ): this is TalerError<DetailsMap[C]> {
+ return this.errorDetail.code === code;
+ }
+}
+
+/**
+ * Convert an exception (or anything that was thrown) into
+ * a TalerErrorDetail object.
+ */
+export function getErrorDetailFromException(e: any): TalerErrorDetail {
+ if (e instanceof TalerError) {
+ return e.errorDetail;
+ }
+ if (e instanceof Error) {
+ const err = makeErrorDetail(
+ TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
+ {
+ stack: e.stack,
+ },
+ `unexpected exception (message: ${e.message})`,
+ );
+ return err;
+ }
+ // Something was thrown that is not even an exception!
+ // Try to stringify it.
+ let excString: string;
+ try {
+ excString = e.toString();
+ } catch (e) {
+ // Something went horribly wrong.
+ excString = "can't stringify exception";
+ }
+ const err = makeErrorDetail(
+ TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
+ {},
+ `unexpected exception (not an exception, ${excString})`,
+ );
+ return err;
+}
diff --git a/packages/taler-util/src/http-common.ts b/packages/taler-util/src/http-common.ts
new file mode 100644
index 000000000..eeb335ba7
--- /dev/null
+++ b/packages/taler-util/src/http-common.ts
@@ -0,0 +1,39 @@
+/*
+ This file is part of GNU Taler
+ (C) 2023 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/>
+
+ SPDX-License-Identifier: AGPL3.0-or-later
+*/
+
+const textEncoder = new TextEncoder();
+
+export interface HttpLibArgs {
+ enableThrottling?: boolean,
+}
+
+export function encodeBody(body: any): ArrayBuffer {
+ if (body == null) {
+ return new ArrayBuffer(0);
+ }
+ if (typeof body === "string") {
+ return textEncoder.encode(body).buffer;
+ } else if (ArrayBuffer.isView(body)) {
+ return body.buffer;
+ } else if (body instanceof ArrayBuffer) {
+ return body;
+ } else if (typeof body === "object") {
+ return textEncoder.encode(JSON.stringify(body)).buffer;
+ }
+ throw new TypeError("unsupported request body type");
+}
diff --git a/packages/taler-util/src/http-impl.node.d.ts b/packages/taler-util/src/http-impl.node.d.ts
new file mode 100644
index 000000000..b0fba9b30
--- /dev/null
+++ b/packages/taler-util/src/http-impl.node.d.ts
@@ -0,0 +1,17 @@
+import { HttpLibArgs } from "./http-common.js";
+import { HttpRequestLibrary, HttpRequestOptions, HttpResponse } from "./http.js";
+/**
+ * Implementation of the HTTP request library interface for node.
+ */
+export declare class HttpLibImpl implements HttpRequestLibrary {
+ private throttle;
+ private throttlingEnabled;
+ constructor(args?: HttpLibArgs);
+ /**
+ * Set whether requests should be throttled.
+ */
+ setThrottling(enabled: boolean): void;
+ fetch(url: string, opt?: HttpRequestOptions): Promise<HttpResponse>;
+ get(url: string, opt?: HttpRequestOptions): Promise<HttpResponse>;
+ postJson(url: string, body: any, opt?: HttpRequestOptions): Promise<HttpResponse>;
+}
diff --git a/packages/taler-util/src/http-impl.node.ts b/packages/taler-util/src/http-impl.node.ts
new file mode 100644
index 000000000..5f2b3ac8a
--- /dev/null
+++ b/packages/taler-util/src/http-impl.node.ts
@@ -0,0 +1,175 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 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/>
+
+ SPDX-License-Identifier: AGPL3.0-or-later
+*/
+
+/**
+ * Imports.
+ */
+import * as http from "node:http";
+import { RequestOptions } from "node:http";
+import { TalerError } from "./errors.js";
+import { encodeBody, HttpLibArgs } from "./http-common.js";
+import {
+ DEFAULT_REQUEST_TIMEOUT_MS,
+ Headers,
+ HttpRequestLibrary,
+ HttpRequestOptions,
+ HttpResponse,
+} from "./http.js";
+import {
+ Logger,
+ RequestThrottler,
+ TalerErrorCode,
+ typedArrayConcat,
+ URL,
+} from "./index.js";
+
+const logger = new Logger("http-impl.node.ts");
+
+const textDecoder = new TextDecoder();
+
+/**
+ * Implementation of the HTTP request library interface for node.
+ */
+export class HttpLibImpl implements HttpRequestLibrary {
+ private throttle = new RequestThrottler();
+ private throttlingEnabled = true;
+
+ constructor(args?: HttpLibArgs) {
+ this.throttlingEnabled = args?.enableThrottling ?? false;
+ }
+
+ /**
+ * Set whether requests should be throttled.
+ */
+ setThrottling(enabled: boolean): void {
+ this.throttlingEnabled = enabled;
+ }
+
+ async fetch(url: string, opt?: HttpRequestOptions): Promise<HttpResponse> {
+ const method = opt?.method?.toUpperCase() ?? "GET";
+ let body = opt?.body;
+
+ logger.trace(`Requesting ${method} ${url}`);
+
+ const parsedUrl = new URL(url);
+ if (this.throttlingEnabled && this.throttle.applyThrottle(url)) {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED,
+ {
+ requestMethod: method,
+ requestUrl: url,
+ throttleStats: this.throttle.getThrottleStats(url),
+ },
+ `request to origin ${parsedUrl.origin} was throttled`,
+ );
+ }
+ let timeoutMs: number | undefined;
+ if (typeof opt?.timeout?.d_ms === "number") {
+ timeoutMs = opt.timeout.d_ms;
+ } else {
+ timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS;
+ }
+
+ const headers = { ...opt?.headers };
+ headers["Content-Type"] = "application/json";
+
+ let reqBody: ArrayBuffer | undefined;
+
+ if (opt?.method == "POST") {
+ reqBody = encodeBody(opt.body);
+ }
+
+ const options: RequestOptions = {
+ protocol: parsedUrl.protocol,
+ port: parsedUrl.port,
+ host: parsedUrl.host,
+ method: method,
+ path: parsedUrl.pathname,
+ headers: opt?.headers,
+ };
+
+ const chunks: Uint8Array[] = [];
+
+ return new Promise((resolve, reject) => {
+ const req = http.request(options, (res) => {
+ res.on("data", (d) => {
+ chunks.push(d);
+ });
+ res.on("end", () => {
+ const headers: Headers = new Headers();
+ for (const [k, v] of Object.entries(res.headers)) {
+ if (!v) {
+ continue;
+ }
+ if (typeof v === "string") {
+ headers.set(k, v);
+ } else {
+ headers.set(k, v.join(", "));
+ }
+ }
+ const data = typedArrayConcat(chunks);
+ const resp: HttpResponse = {
+ requestMethod: method,
+ requestUrl: parsedUrl.href,
+ status: res.statusCode || 0,
+ headers,
+ async bytes() {
+ return data;
+ },
+ json() {
+ const text = textDecoder.decode(data);
+ return JSON.parse(text);
+ },
+ async text() {
+ const text = textDecoder.decode(data);
+ return text;
+ },
+ };
+ resolve(resp);
+ });
+ res.on("error", (e) => {
+ reject(e);
+ });
+ });
+
+ if (reqBody) {
+ req.write(reqBody);
+ }
+ req.end();
+ });
+ }
+
+ async get(url: string, opt?: HttpRequestOptions): Promise<HttpResponse> {
+ return this.fetch(url, {
+ method: "GET",
+ ...opt,
+ });
+ }
+
+ async postJson(
+ url: string,
+ body: any,
+ opt?: HttpRequestOptions,
+ ): Promise<HttpResponse> {
+ return this.fetch(url, {
+ method: "POST",
+ body,
+ ...opt,
+ });
+ }
+}
diff --git a/packages/taler-util/src/http-impl.qtart.ts b/packages/taler-util/src/http-impl.qtart.ts
new file mode 100644
index 000000000..954b41802
--- /dev/null
+++ b/packages/taler-util/src/http-impl.qtart.ts
@@ -0,0 +1,127 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 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/>
+
+ SPDX-License-Identifier: AGPL3.0-or-later
+*/
+
+/**
+ * Imports.
+ */
+import { Logger } from "@gnu-taler/taler-util";
+import { TalerError } from "./errors.js";
+import { encodeBody, HttpLibArgs } from "./http-common.js";
+import {
+ Headers,
+ HttpRequestLibrary,
+ HttpRequestOptions,
+ HttpResponse,
+} from "./http.js";
+import { RequestThrottler, TalerErrorCode, URL } from "./index.js";
+import { qjsOs } from "./qtart.js";
+
+const logger = new Logger("http-impl.qtart.ts");
+
+const textDecoder = new TextDecoder();
+
+/**
+ * Implementation of the HTTP request library interface for node.
+ */
+export class HttpLibImpl implements HttpRequestLibrary {
+ private throttle = new RequestThrottler();
+ private throttlingEnabled = true;
+
+ constructor(args?: HttpLibArgs) {
+ this.throttlingEnabled = args?.enableThrottling ?? false;
+ }
+
+ /**
+ * Set whether requests should be throttled.
+ */
+ setThrottling(enabled: boolean): void {
+ this.throttlingEnabled = enabled;
+ }
+
+ async fetch(url: string, opt?: HttpRequestOptions): Promise<HttpResponse> {
+ const method = opt?.method ?? "GET";
+
+ logger.trace(`Requesting ${method} ${url}`);
+
+ const parsedUrl = new URL(url);
+ if (this.throttlingEnabled && this.throttle.applyThrottle(url)) {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED,
+ {
+ requestMethod: method,
+ requestUrl: url,
+ throttleStats: this.throttle.getThrottleStats(url),
+ },
+ `request to origin ${parsedUrl.origin} was throttled`,
+ );
+ }
+
+ let data: ArrayBuffer | undefined = undefined;
+ let headers: string[] = [];
+ if (opt?.headers) {
+ for (let headerName of Object.keys(opt.headers)) {
+ headers.push(`${headerName}: ${opt.headers[headerName]}`);
+ }
+ }
+ if (method.toUpperCase() === "POST") {
+ data = encodeBody(opt?.body);
+ }
+ const res = await qjsOs.fetchHttp(url, {
+ method,
+ data,
+ headers,
+ });
+ return {
+ requestMethod: method,
+ // FIXME: We don't return headers!
+ headers: new Headers(),
+ async bytes() {
+ return res.data;
+ },
+ json() {
+ const text = textDecoder.decode(res.data);
+ return JSON.parse(text);
+ },
+ async text() {
+ const text = textDecoder.decode(res.data);
+ return text;
+ },
+ requestUrl: url,
+ status: res.status,
+ };
+ }
+
+ async get(url: string, opt?: HttpRequestOptions): Promise<HttpResponse> {
+ return this.fetch(url, {
+ method: "GET",
+ ...opt,
+ });
+ }
+
+ async postJson(
+ url: string,
+ body: any,
+ opt?: HttpRequestOptions,
+ ): Promise<HttpResponse> {
+ return this.fetch(url, {
+ method: "POST",
+ body,
+ ...opt,
+ });
+ }
+}
diff --git a/packages/taler-util/src/http.ts b/packages/taler-util/src/http.ts
new file mode 100644
index 000000000..fd594b655
--- /dev/null
+++ b/packages/taler-util/src/http.ts
@@ -0,0 +1,360 @@
+/*
+ This file is part of TALER
+ (C) 2016 GNUnet e.V.
+
+ 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.
+
+ 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
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Helpers for doing XMLHttpRequest-s that are based on ES6 promises.
+ * Allows for easy mocking for test cases.
+ *
+ * The API is inspired by the HTML5 fetch API.
+ */
+
+/**
+ * Imports
+ */
+import {
+ Logger,
+ Duration,
+ AbsoluteTime,
+ TalerErrorDetail,
+ Codec,
+ j2s,
+ CancellationToken,
+} from "@gnu-taler/taler-util";
+import { TalerErrorCode } from "@gnu-taler/taler-util";
+import { makeErrorDetail, TalerError } from "./errors.js";
+import * as impl from "#http-impl";
+import { HttpLibArgs } from "./http-common.js";
+
+const logger = new Logger("http.ts");
+
+/**
+ * An HTTP response that is returned by all request methods of this library.
+ */
+export interface HttpResponse {
+ requestUrl: string;
+ requestMethod: string;
+ status: number;
+ headers: Headers;
+ json(): Promise<any>;
+ text(): Promise<string>;
+ bytes(): Promise<ArrayBuffer>;
+}
+
+export const DEFAULT_REQUEST_TIMEOUT_MS = 60000;
+
+export interface HttpRequestOptions {
+ method?: "POST" | "PUT" | "GET";
+ headers?: { [name: string]: string };
+
+ /**
+ * Timeout after which the request should be aborted.
+ */
+ timeout?: Duration;
+
+ /**
+ * Cancellation token that should abort the request when
+ * cancelled.
+ */
+ cancellationToken?: CancellationToken;
+
+ body?: string | ArrayBuffer | Record<string, unknown>;
+}
+
+/**
+ * Headers, roughly modeled after the fetch API's headers object.
+ */
+export class Headers {
+ private headerMap = new Map<string, string>();
+
+ get(name: string): string | null {
+ const r = this.headerMap.get(name.toLowerCase());
+ if (r) {
+ return r;
+ }
+ return null;
+ }
+
+ set(name: string, value: string): void {
+ const normalizedName = name.toLowerCase();
+ const existing = this.headerMap.get(normalizedName);
+ if (existing !== undefined) {
+ this.headerMap.set(normalizedName, existing + "," + value);
+ } else {
+ this.headerMap.set(normalizedName, value);
+ }
+ }
+
+ toJSON(): any {
+ const m: Record<string, string> = {};
+ this.headerMap.forEach((v, k) => (m[k] = v));
+ return m;
+ }
+}
+
+/**
+ * Interface for the HTTP request library used by the wallet.
+ *
+ * The request library is bundled into an interface to make mocking and
+ * request tunneling easy.
+ */
+export interface HttpRequestLibrary {
+ /**
+ * Make an HTTP GET request.
+ *
+ * FIXME: Get rid of this, we want the API surface to be minimal.
+ */
+ get(url: string, opt?: HttpRequestOptions): Promise<HttpResponse>;
+
+ /**
+ * Make an HTTP POST request with a JSON body.
+ *
+ * FIXME: Get rid of this, we want the API surface to be minimal.
+ */
+ postJson(
+ url: string,
+ body: any,
+ opt?: HttpRequestOptions,
+ ): Promise<HttpResponse>;
+
+ /**
+ * Make an HTTP POST request with a JSON body.
+ */
+ fetch(url: string, opt?: HttpRequestOptions): Promise<HttpResponse>;
+}
+
+type TalerErrorResponse = {
+ code: number;
+} & unknown;
+
+type ResponseOrError<T> =
+ | { isError: false; response: T }
+ | { isError: true; talerErrorResponse: TalerErrorResponse };
+
+export async function readTalerErrorResponse(
+ httpResponse: HttpResponse,
+): Promise<TalerErrorDetail> {
+ const errJson = await httpResponse.json();
+ const talerErrorCode = errJson.code;
+ if (typeof talerErrorCode !== "number") {
+ logger.warn(
+ `malformed error response (status ${httpResponse.status}): ${j2s(
+ errJson,
+ )}`,
+ );
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ {
+ requestUrl: httpResponse.requestUrl,
+ requestMethod: httpResponse.requestMethod,
+ httpStatusCode: httpResponse.status,
+ },
+ "Error response did not contain error code",
+ );
+ }
+ return errJson;
+}
+
+export async function readUnexpectedResponseDetails(
+ httpResponse: HttpResponse,
+): Promise<TalerErrorDetail> {
+ const errJson = await httpResponse.json();
+ const talerErrorCode = errJson.code;
+ if (typeof talerErrorCode !== "number") {
+ return makeErrorDetail(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ {
+ requestUrl: httpResponse.requestUrl,
+ requestMethod: httpResponse.requestMethod,
+ httpStatusCode: httpResponse.status,
+ },
+ "Error response did not contain error code",
+ );
+ }
+ return makeErrorDetail(
+ TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
+ {
+ requestUrl: httpResponse.requestUrl,
+ requestMethod: httpResponse.requestMethod,
+ httpStatusCode: httpResponse.status,
+ errorResponse: errJson,
+ },
+ `Unexpected HTTP status (${httpResponse.status}) in response`,
+ );
+}
+
+export async function readSuccessResponseJsonOrErrorCode<T>(
+ httpResponse: HttpResponse,
+ codec: Codec<T>,
+): Promise<ResponseOrError<T>> {
+ if (!(httpResponse.status >= 200 && httpResponse.status < 300)) {
+ return {
+ isError: true,
+ talerErrorResponse: await readTalerErrorResponse(httpResponse),
+ };
+ }
+ const respJson = await httpResponse.json();
+ let parsedResponse: T;
+ try {
+ parsedResponse = codec.decode(respJson);
+ } catch (e: any) {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ {
+ requestUrl: httpResponse.requestUrl,
+ requestMethod: httpResponse.requestMethod,
+ httpStatusCode: httpResponse.status,
+ validationError: e.toString(),
+ },
+ "Response invalid",
+ );
+ }
+ return {
+ isError: false,
+ response: parsedResponse,
+ };
+}
+
+type HttpErrorDetails = {
+ requestUrl: string;
+ requestMethod: string;
+ httpStatusCode: number;
+};
+
+export function getHttpResponseErrorDetails(
+ httpResponse: HttpResponse,
+): HttpErrorDetails {
+ return {
+ requestUrl: httpResponse.requestUrl,
+ requestMethod: httpResponse.requestMethod,
+ httpStatusCode: httpResponse.status,
+ };
+}
+
+export function throwUnexpectedRequestError(
+ httpResponse: HttpResponse,
+ talerErrorResponse: TalerErrorResponse,
+): never {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
+ {
+ requestUrl: httpResponse.requestUrl,
+ requestMethod: httpResponse.requestMethod,
+ httpStatusCode: httpResponse.status,
+ errorResponse: talerErrorResponse,
+ },
+ `Unexpected HTTP status ${httpResponse.status} in response`,
+ );
+}
+
+export async function readSuccessResponseJsonOrThrow<T>(
+ httpResponse: HttpResponse,
+ codec: Codec<T>,
+): Promise<T> {
+ const r = await readSuccessResponseJsonOrErrorCode(httpResponse, codec);
+ if (!r.isError) {
+ return r.response;
+ }
+ throwUnexpectedRequestError(httpResponse, r.talerErrorResponse);
+}
+
+export async function readSuccessResponseTextOrErrorCode<T>(
+ httpResponse: HttpResponse,
+): Promise<ResponseOrError<string>> {
+ if (!(httpResponse.status >= 200 && httpResponse.status < 300)) {
+ const errJson = await httpResponse.json();
+ const talerErrorCode = errJson.code;
+ if (typeof talerErrorCode !== "number") {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ {
+ httpStatusCode: httpResponse.status,
+ requestUrl: httpResponse.requestUrl,
+ requestMethod: httpResponse.requestMethod,
+ },
+ "Error response did not contain error code",
+ );
+ }
+ return {
+ isError: true,
+ talerErrorResponse: errJson,
+ };
+ }
+ const respJson = await httpResponse.text();
+ return {
+ isError: false,
+ response: respJson,
+ };
+}
+
+export async function checkSuccessResponseOrThrow(
+ httpResponse: HttpResponse,
+): Promise<void> {
+ if (!(httpResponse.status >= 200 && httpResponse.status < 300)) {
+ const errJson = await httpResponse.json();
+ const talerErrorCode = errJson.code;
+ if (typeof talerErrorCode !== "number") {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ {
+ httpStatusCode: httpResponse.status,
+ requestUrl: httpResponse.requestUrl,
+ requestMethod: httpResponse.requestMethod,
+ },
+ "Error response did not contain error code",
+ );
+ }
+ throwUnexpectedRequestError(httpResponse, errJson);
+ }
+}
+
+export async function readSuccessResponseTextOrThrow<T>(
+ httpResponse: HttpResponse,
+): Promise<string> {
+ const r = await readSuccessResponseTextOrErrorCode(httpResponse);
+ if (!r.isError) {
+ return r.response;
+ }
+ throwUnexpectedRequestError(httpResponse, r.talerErrorResponse);
+}
+
+/**
+ * Get the timestamp at which the response's content is considered expired.
+ */
+export function getExpiry(
+ httpResponse: HttpResponse,
+ opt: { minDuration?: Duration },
+): AbsoluteTime {
+ const expiryDateMs = new Date(
+ httpResponse.headers.get("expiry") ?? "",
+ ).getTime();
+ let t: AbsoluteTime;
+ if (Number.isNaN(expiryDateMs)) {
+ t = AbsoluteTime.now();
+ } else {
+ t = {
+ t_ms: expiryDateMs,
+ };
+ }
+ if (opt.minDuration) {
+ const t2 = AbsoluteTime.addDuration(AbsoluteTime.now(), opt.minDuration);
+ return AbsoluteTime.max(t, t2);
+ }
+ return t;
+}
+
+export function createPlatformHttpLib(args?: HttpLibArgs): HttpRequestLibrary {
+ return new impl.HttpLibImpl(args);
+}
diff --git a/packages/taler-util/src/index.browser.ts b/packages/taler-util/src/index.browser.ts
index 3b8e194b3..2a600644d 100644
--- a/packages/taler-util/src/index.browser.ts
+++ b/packages/taler-util/src/index.browser.ts
@@ -19,3 +19,7 @@
import { loadBrowserPrng } from "./prng-browser.js";
loadBrowserPrng();
export * from "./index.js";
+
+// The web stuff doesn't support package.json export declarations yet,
+// so we export more stuff here than we should.
+export * from "./http.js";
diff --git a/packages/taler-util/src/index.node.ts b/packages/taler-util/src/index.node.ts
index bd59f320a..018b4767f 100644
--- a/packages/taler-util/src/index.node.ts
+++ b/packages/taler-util/src/index.node.ts
@@ -21,4 +21,3 @@ initNodePrng();
export * from "./index.js";
export * from "./talerconfig.js";
export * from "./globbing/minimatch.js";
-export { clk } from "./clk.js";
diff --git a/packages/taler-util/src/index.ts b/packages/taler-util/src/index.ts
index 661b0332f..cf4f545a4 100644
--- a/packages/taler-util/src/index.ts
+++ b/packages/taler-util/src/index.ts
@@ -36,3 +36,4 @@ export * from "./CancellationToken.js";
export * from "./contract-terms.js";
export * from "./base64.js";
export * from "./merchant-api-types.js";
+export * from "./errors.js";
diff --git a/packages/taler-util/src/qtart.ts b/packages/taler-util/src/qtart.ts
new file mode 100644
index 000000000..f8edf234e
--- /dev/null
+++ b/packages/taler-util/src/qtart.ts
@@ -0,0 +1,36 @@
+
+// @ts-ignore
+import * as _qjsOsImp from "os";
+// @ts-ignore
+import * as _qjsStdImp from "std";
+
+
+export interface QjsHttpResp {
+ status: number;
+ data: ArrayBuffer;
+}
+
+export interface QjsHttpOptions {
+ method: string;
+ debug?: boolean;
+ data?: ArrayBuffer;
+ headers?: string[];
+}
+
+
+export interface QjsOsLib {
+ fetchHttp(url: string, options?: QjsHttpOptions): Promise<QjsHttpResp>;
+ postMessageToHost(s: string): void;
+ setMessageFromHostHandler(h: (s: string) => void): void;
+ rename(oldPath: string, newPath: string): number;
+}
+
+export interface QjsStdLib {
+ writeFile(filename: string, contents: string): void;
+ loadFile(filename: string): string;
+}
+
+// This is not the nodejs "os" module, but the qjs "os" module.
+export const qjsOs: QjsOsLib = _qjsOsImp as any;
+
+export const qjsStd: QjsStdLib = _qjsStdImp as any; \ No newline at end of file
diff --git a/packages/taler-util/src/twrpc-impl.missing.ts b/packages/taler-util/src/twrpc-impl.missing.ts
index d9ed37815..7d7fa84ae 100644
--- a/packages/taler-util/src/twrpc-impl.missing.ts
+++ b/packages/taler-util/src/twrpc-impl.missing.ts
@@ -14,4 +14,13 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
+import type { RpcConnectArgs, RpcServerArgs } from "./twrpc.js";
+
// Not implemented.
+export async function connectRpc<T>(args: RpcConnectArgs<T>): Promise<T> {
+ throw Error("not implemented");
+}
+
+export async function runRpcServer(args: RpcServerArgs): Promise<void> {
+ throw Error("not implemented");
+}