new taler:// URI syntax
This commit is contained in:
parent
694d913d1f
commit
ae111663f4
@ -66,9 +66,11 @@ export async function getTipStatus(
|
||||
|
||||
const amount = Amounts.parseOrThrow(tipPickupStatus.amount);
|
||||
|
||||
const merchantOrigin = new URL(res.merchantBaseUrl).origin;
|
||||
|
||||
let tipRecord = await ws.db.get(Stores.tips, [
|
||||
res.merchantTipId,
|
||||
res.merchantOrigin,
|
||||
merchantOrigin,
|
||||
]);
|
||||
|
||||
if (!tipRecord) {
|
||||
@ -117,7 +119,7 @@ export async function getTipStatus(
|
||||
amountLeft: Amounts.parseOrThrow(tipPickupStatus.amount_left),
|
||||
exchangeUrl: tipPickupStatus.exchange_url,
|
||||
nextUrl: tipPickupStatus.extra.next_url,
|
||||
merchantOrigin: res.merchantOrigin,
|
||||
merchantOrigin: merchantOrigin,
|
||||
merchantTipId: res.merchantTipId,
|
||||
expirationTimestamp: tipPickupStatus.stamp_expire,
|
||||
timestamp: tipPickupStatus.stamp_created,
|
||||
|
@ -141,7 +141,11 @@ export async function getBankWithdrawalInfo(
|
||||
if (!uriResult) {
|
||||
throw Error(`can't parse URL ${talerWithdrawUri}`);
|
||||
}
|
||||
const resp = await ws.http.get(uriResult.statusUrl);
|
||||
const reqUrl = new URL(
|
||||
`api/withdraw-operations/${uriResult.withdrawalOperationId}`,
|
||||
uriResult.bankIntegrationApiBaseUrl,
|
||||
);
|
||||
const resp = await ws.http.get(reqUrl.href);
|
||||
const status = await readSuccessResponseJsonOrThrow(
|
||||
resp,
|
||||
codecForWithdrawOperationStatusResponse(),
|
||||
@ -150,7 +154,7 @@ export async function getBankWithdrawalInfo(
|
||||
return {
|
||||
amount: Amounts.parseOrThrow(status.amount),
|
||||
confirmTransferUrl: status.confirm_transfer_url,
|
||||
extractedStatusUrl: uriResult.statusUrl,
|
||||
extractedStatusUrl: uriResult.bankIntegrationApiBaseUrl,
|
||||
selectionDone: status.selection_done,
|
||||
senderWire: status.sender_wire,
|
||||
suggestedExchange: status.suggested_exchange,
|
||||
|
@ -33,136 +33,93 @@ test("taler pay url parsing: wrong scheme", (t) => {
|
||||
});
|
||||
|
||||
test("taler pay url parsing: defaults", (t) => {
|
||||
const url1 = "taler://pay/example.com/-/-/myorder";
|
||||
const url1 = "taler://pay/example.com/myorder/";
|
||||
const r1 = parsePayUri(url1);
|
||||
if (!r1) {
|
||||
t.fail();
|
||||
return;
|
||||
}
|
||||
t.is(r1.merchantBaseUrl, "https://example.com/public/");
|
||||
t.is(r1.sessionId, undefined);
|
||||
t.is(r1.merchantBaseUrl, "https://example.com/");
|
||||
t.is(r1.sessionId, "");
|
||||
|
||||
const url2 = "taler://pay/example.com/-/-/myorder/mysession";
|
||||
const url2 = "taler://pay/example.com/myorder/mysession";
|
||||
const r2 = parsePayUri(url2);
|
||||
if (!r2) {
|
||||
t.fail();
|
||||
return;
|
||||
}
|
||||
t.is(r2.merchantBaseUrl, "https://example.com/public/");
|
||||
t.is(r2.merchantBaseUrl, "https://example.com/");
|
||||
t.is(r2.sessionId, "mysession");
|
||||
});
|
||||
|
||||
test("taler pay url parsing: trailing parts", (t) => {
|
||||
const url1 = "taler://pay/example.com/-/-/myorder/mysession/spam/eggs";
|
||||
const r1 = parsePayUri(url1);
|
||||
if (!r1) {
|
||||
t.fail();
|
||||
return;
|
||||
}
|
||||
t.is(r1.merchantBaseUrl, "https://example.com/public/");
|
||||
t.is(r1.sessionId, "mysession");
|
||||
});
|
||||
|
||||
test("taler pay url parsing: instance", (t) => {
|
||||
const url1 = "taler://pay/example.com/-/myinst/myorder";
|
||||
const url1 = "taler://pay/example.com/instances/myinst/myorder/";
|
||||
const r1 = parsePayUri(url1);
|
||||
if (!r1) {
|
||||
t.fail();
|
||||
return;
|
||||
}
|
||||
t.is(r1.merchantBaseUrl, "https://example.com/public/instances/myinst/");
|
||||
t.is(r1.merchantBaseUrl, "https://example.com/instances/myinst/");
|
||||
t.is(r1.orderId, "myorder");
|
||||
});
|
||||
|
||||
test("taler pay url parsing: path prefix and instance", (t) => {
|
||||
const url1 = "taler://pay/example.com/mypfx/myinst/myorder";
|
||||
const r1 = parsePayUri(url1);
|
||||
if (!r1) {
|
||||
t.fail();
|
||||
return;
|
||||
}
|
||||
t.is(r1.merchantBaseUrl, "https://example.com/mypfx/instances/myinst/");
|
||||
});
|
||||
|
||||
test("taler pay url parsing: complex path prefix", (t) => {
|
||||
const url1 = "taler://pay/example.com/mypfx%2Fpublic/-/myorder";
|
||||
const r1 = parsePayUri(url1);
|
||||
if (!r1) {
|
||||
t.fail();
|
||||
return;
|
||||
}
|
||||
t.is(r1.merchantBaseUrl, "https://example.com/mypfx/public/");
|
||||
t.is(r1.orderId, "myorder");
|
||||
t.is(r1.sessionId, undefined);
|
||||
});
|
||||
|
||||
test("taler pay uri parsing: complex path prefix and instance", (t) => {
|
||||
const url1 = "taler://pay/example.com/mypfx%2Fpublic/foo/myorder";
|
||||
const r1 = parsePayUri(url1);
|
||||
if (!r1) {
|
||||
t.fail();
|
||||
return;
|
||||
}
|
||||
t.is(r1.merchantBaseUrl, "https://example.com/mypfx/public/instances/foo/");
|
||||
t.is(r1.orderId, "myorder");
|
||||
});
|
||||
|
||||
test("taler refund uri parsing: non-https #1", (t) => {
|
||||
const url1 = "taler://refund/example.com/-/-/myorder?insecure=1";
|
||||
const url1 = "taler+http://refund/example.com/myorder";
|
||||
const r1 = parseRefundUri(url1);
|
||||
if (!r1) {
|
||||
t.fail();
|
||||
return;
|
||||
}
|
||||
t.is(r1.merchantBaseUrl, "http://example.com/public/");
|
||||
t.is(r1.merchantBaseUrl, "http://example.com/");
|
||||
t.is(r1.orderId, "myorder");
|
||||
});
|
||||
|
||||
test("taler pay uri parsing: non-https #1", (t) => {
|
||||
const url1 = "taler://pay/example.com/-/-/myorder?insecure=1";
|
||||
test("taler pay uri parsing: non-https", (t) => {
|
||||
const url1 = "taler+http://pay/example.com/myorder/";
|
||||
const r1 = parsePayUri(url1);
|
||||
if (!r1) {
|
||||
t.fail();
|
||||
return;
|
||||
}
|
||||
t.is(r1.merchantBaseUrl, "http://example.com/public/");
|
||||
t.is(r1.merchantBaseUrl, "http://example.com/");
|
||||
t.is(r1.orderId, "myorder");
|
||||
});
|
||||
|
||||
test("taler pay url parsing: non-https #2", (t) => {
|
||||
const url1 = "taler://pay/example.com/-/-/myorder?insecure=2";
|
||||
test("taler pay uri parsing: missing session component", (t) => {
|
||||
const url1 = "taler+http://pay/example.com/myorder";
|
||||
const r1 = parsePayUri(url1);
|
||||
if (!r1) {
|
||||
if (r1) {
|
||||
t.fail();
|
||||
return;
|
||||
}
|
||||
t.is(r1.merchantBaseUrl, "https://example.com/public/");
|
||||
t.is(r1.orderId, "myorder");
|
||||
t.pass();
|
||||
});
|
||||
|
||||
test("taler withdraw uri parsing", (t) => {
|
||||
const url1 = "taler://withdraw/bank.example.com/-/12345";
|
||||
const url1 = "taler://withdraw/bank.example.com/12345";
|
||||
const r1 = parseWithdrawUri(url1);
|
||||
if (!r1) {
|
||||
t.fail();
|
||||
return;
|
||||
}
|
||||
t.is(r1.statusUrl, "https://bank.example.com/api/withdraw-operation/12345");
|
||||
t.is(r1.withdrawalOperationId, "12345");
|
||||
t.is(r1.bankIntegrationApiBaseUrl, "https://bank.example.com/");
|
||||
});
|
||||
|
||||
test("taler refund uri parsing", (t) => {
|
||||
const url1 = "taler://refund/merchant.example.com/-/-/1234";
|
||||
const url1 = "taler://refund/merchant.example.com/1234";
|
||||
const r1 = parseRefundUri(url1);
|
||||
if (!r1) {
|
||||
t.fail();
|
||||
return;
|
||||
}
|
||||
t.is(r1.merchantBaseUrl, "https://merchant.example.com/public/");
|
||||
t.is(r1.merchantBaseUrl, "https://merchant.example.com/");
|
||||
t.is(r1.orderId, "1234");
|
||||
});
|
||||
|
||||
test("taler refund uri parsing with instance", (t) => {
|
||||
const url1 = "taler://refund/merchant.example.com/-/myinst/1234";
|
||||
const url1 = "taler://refund/merchant.example.com/instances/myinst/1234";
|
||||
const r1 = parseRefundUri(url1);
|
||||
if (!r1) {
|
||||
t.fail();
|
||||
@ -171,22 +128,22 @@ test("taler refund uri parsing with instance", (t) => {
|
||||
t.is(r1.orderId, "1234");
|
||||
t.is(
|
||||
r1.merchantBaseUrl,
|
||||
"https://merchant.example.com/public/instances/myinst/",
|
||||
"https://merchant.example.com/instances/myinst/",
|
||||
);
|
||||
});
|
||||
|
||||
test("taler tip pickup uri", (t) => {
|
||||
const url1 = "taler://tip/merchant.example.com/-/-/tipid";
|
||||
const url1 = "taler://tip/merchant.example.com/tipid";
|
||||
const r1 = parseTipUri(url1);
|
||||
if (!r1) {
|
||||
t.fail();
|
||||
return;
|
||||
}
|
||||
t.is(r1.merchantBaseUrl, "https://merchant.example.com/public/");
|
||||
t.is(r1.merchantBaseUrl, "https://merchant.example.com/");
|
||||
});
|
||||
|
||||
test("taler tip pickup uri with instance", (t) => {
|
||||
const url1 = "taler://tip/merchant.example.com/-/tipm/tipid";
|
||||
const url1 = "taler://tip/merchant.example.com/instances/tipm/tipid";
|
||||
const r1 = parseTipUri(url1);
|
||||
if (!r1) {
|
||||
t.fail();
|
||||
@ -194,13 +151,13 @@ test("taler tip pickup uri with instance", (t) => {
|
||||
}
|
||||
t.is(
|
||||
r1.merchantBaseUrl,
|
||||
"https://merchant.example.com/public/instances/tipm/",
|
||||
"https://merchant.example.com/instances/tipm/",
|
||||
);
|
||||
t.is(r1.merchantTipId, "tipid");
|
||||
});
|
||||
|
||||
test("taler tip pickup uri with instance and prefix", (t) => {
|
||||
const url1 = "taler://tip/merchant.example.com/my%2fpfx/tipm/tipid";
|
||||
const url1 = "taler://tip/merchant.example.com/my/pfx/tipm/tipid";
|
||||
const r1 = parseTipUri(url1);
|
||||
if (!r1) {
|
||||
t.fail();
|
||||
@ -208,7 +165,7 @@ test("taler tip pickup uri with instance and prefix", (t) => {
|
||||
}
|
||||
t.is(
|
||||
r1.merchantBaseUrl,
|
||||
"https://merchant.example.com/my/pfx/instances/tipm/",
|
||||
"https://merchant.example.com/my/pfx/tipm/",
|
||||
);
|
||||
t.is(r1.merchantTipId, "tipid");
|
||||
});
|
||||
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
This file is part of GNU Taler
|
||||
(C) 2019 GNUnet e.V.
|
||||
(C) 2019-2020 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
|
||||
@ -17,11 +17,12 @@
|
||||
export interface PayUriResult {
|
||||
merchantBaseUrl: string;
|
||||
orderId: string;
|
||||
sessionId?: string;
|
||||
sessionId: string;
|
||||
}
|
||||
|
||||
export interface WithdrawUriResult {
|
||||
statusUrl: string;
|
||||
bankIntegrationApiBaseUrl: string;
|
||||
withdrawalOperationId: string;
|
||||
}
|
||||
|
||||
export interface RefundUriResult {
|
||||
@ -31,10 +32,13 @@ export interface RefundUriResult {
|
||||
|
||||
export interface TipUriResult {
|
||||
merchantTipId: string;
|
||||
merchantOrigin: string;
|
||||
merchantBaseUrl: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a taler[+http]://withdraw URI.
|
||||
* Return undefined if not passed a valid URI.
|
||||
*/
|
||||
export function parseWithdrawUri(s: string): WithdrawUriResult | undefined {
|
||||
const pfx = "taler://withdraw/";
|
||||
if (!s.toLowerCase().startsWith(pfx)) {
|
||||
@ -42,29 +46,20 @@ export function parseWithdrawUri(s: string): WithdrawUriResult | undefined {
|
||||
}
|
||||
|
||||
const rest = s.substring(pfx.length);
|
||||
const parts = rest.split("/");
|
||||
|
||||
let [host, path, withdrawId] = rest.split("/");
|
||||
|
||||
if (!host) {
|
||||
if (parts.length < 2) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
host = host.toLowerCase();
|
||||
|
||||
if (!path) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!withdrawId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (path === "-") {
|
||||
path = "api/withdraw-operation";
|
||||
}
|
||||
const host = parts[0].toLowerCase();
|
||||
const pathSegments = parts.slice(1, parts.length - 1);
|
||||
const withdrawId = parts[parts.length - 1];
|
||||
const p = [host, ...pathSegments].join("/");
|
||||
|
||||
return {
|
||||
statusUrl: `https://${host}/${path}/${withdrawId}`,
|
||||
bankIntegrationApiBaseUrl: `https://${p}/`,
|
||||
withdrawalOperationId: withdrawId,
|
||||
};
|
||||
}
|
||||
|
||||
@ -77,17 +72,29 @@ export const enum TalerUriType {
|
||||
Unknown = "unknown",
|
||||
}
|
||||
|
||||
/**
|
||||
* Classify a taler:// URI.
|
||||
*/
|
||||
export function classifyTalerUri(s: string): TalerUriType {
|
||||
const sl = s.toLowerCase();
|
||||
if (sl.startsWith("taler://pay/")) {
|
||||
return TalerUriType.TalerPay;
|
||||
}
|
||||
if (sl.startsWith("taler+http://pay/")) {
|
||||
return TalerUriType.TalerPay;
|
||||
}
|
||||
if (sl.startsWith("taler://tip/")) {
|
||||
return TalerUriType.TalerTip;
|
||||
}
|
||||
if (sl.startsWith("taler+http://tip/")) {
|
||||
return TalerUriType.TalerTip;
|
||||
}
|
||||
if (sl.startsWith("taler://refund/")) {
|
||||
return TalerUriType.TalerRefund;
|
||||
}
|
||||
if (sl.startsWith("taler+http://refund/")) {
|
||||
return TalerUriType.TalerRefund;
|
||||
}
|
||||
if (sl.startsWith("taler://withdraw/")) {
|
||||
return TalerUriType.TalerWithdraw;
|
||||
}
|
||||
@ -97,146 +104,103 @@ export function classifyTalerUri(s: string): TalerUriType {
|
||||
return TalerUriType.Unknown;
|
||||
}
|
||||
|
||||
interface TalerUriProtoInfo {
|
||||
innerProto: "http" | "https";
|
||||
rest: string;
|
||||
}
|
||||
|
||||
|
||||
function parseProtoInfo(s: string, action: string): TalerUriProtoInfo | undefined {
|
||||
const pfxPlain = `taler://${action}/`;
|
||||
const pfxHttp = `taler+http://${action}/`;
|
||||
if (s.toLowerCase().startsWith(pfxPlain)) {
|
||||
return {
|
||||
innerProto: "https",
|
||||
rest: s.substring(pfxPlain.length),
|
||||
}
|
||||
} else if (s.toLowerCase().startsWith(pfxHttp)) {
|
||||
return {
|
||||
innerProto: "http",
|
||||
rest: s.substring(pfxHttp.length),
|
||||
}
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a taler[+http]://pay URI.
|
||||
* Return undefined if not passed a valid URI.
|
||||
*/
|
||||
export function parsePayUri(s: string): PayUriResult | undefined {
|
||||
const pfx = "taler://pay/";
|
||||
if (!s.toLowerCase().startsWith(pfx)) {
|
||||
const pi = parseProtoInfo(s, "pay");
|
||||
if (!pi) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const [path, search] = s.slice(pfx.length).split("?");
|
||||
|
||||
let [host, maybePath, maybeInstance, orderId, maybeSessionid] = path.split(
|
||||
"/",
|
||||
);
|
||||
|
||||
if (!host) {
|
||||
const c = pi?.rest.split("?");
|
||||
const parts = c[0].split("/");
|
||||
if (parts.length < 3) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
host = host.toLowerCase();
|
||||
|
||||
if (!maybePath) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!orderId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (maybePath === "-") {
|
||||
maybePath = "";
|
||||
} else {
|
||||
maybePath = decodeURIComponent(maybePath) + "/";
|
||||
}
|
||||
let maybeInstancePath = "";
|
||||
if (maybeInstance !== "-") {
|
||||
maybeInstancePath = `instances/${maybeInstance}/`;
|
||||
}
|
||||
|
||||
let protocol = "https";
|
||||
const searchParams = new URLSearchParams(search);
|
||||
if (searchParams.get("insecure") === "1") {
|
||||
protocol = "http";
|
||||
}
|
||||
|
||||
const merchantBaseUrl =
|
||||
`${protocol}://${host}/` +
|
||||
decodeURIComponent(maybePath) +
|
||||
maybeInstancePath;
|
||||
const host = parts[0].toLowerCase();
|
||||
const sessionId = parts[parts.length - 1];
|
||||
const orderId = parts[parts.length - 2];
|
||||
const pathSegments = parts.slice(1, parts.length - 2);
|
||||
const p = [host, ...pathSegments].join("/");
|
||||
const merchantBaseUrl = `${pi.innerProto}://${p}/`;
|
||||
|
||||
return {
|
||||
merchantBaseUrl,
|
||||
orderId,
|
||||
sessionId: maybeSessionid,
|
||||
sessionId: sessionId,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a taler[+http]://tip URI.
|
||||
* Return undefined if not passed a valid URI.
|
||||
*/
|
||||
export function parseTipUri(s: string): TipUriResult | undefined {
|
||||
const pfx = "taler://tip/";
|
||||
if (!s.toLowerCase().startsWith(pfx)) {
|
||||
const pi = parseProtoInfo(s, "tip");
|
||||
if (!pi) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const path = s.slice(pfx.length);
|
||||
|
||||
let [host, maybePath, maybeInstance, tipId] = path.split("/");
|
||||
|
||||
if (!host) {
|
||||
const c = pi?.rest.split("?");
|
||||
const parts = c[0].split("/");
|
||||
if (parts.length < 2) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
host = host.toLowerCase();
|
||||
|
||||
if (!maybePath) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!tipId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (maybePath === "-") {
|
||||
maybePath = "public/";
|
||||
} else {
|
||||
maybePath = decodeURIComponent(maybePath) + "/";
|
||||
}
|
||||
let maybeInstancePath = "";
|
||||
if (maybeInstance !== "-") {
|
||||
maybeInstancePath = `instances/${maybeInstance}/`;
|
||||
}
|
||||
|
||||
const merchantBaseUrl = `https://${host}/${maybePath}${maybeInstancePath}`;
|
||||
const host = parts[0].toLowerCase();
|
||||
const tipId = parts[parts.length - 1];
|
||||
const pathSegments = parts.slice(1, parts.length - 1);
|
||||
const p = [host, ...pathSegments].join("/");
|
||||
const merchantBaseUrl = `${pi.innerProto}://${p}/`;
|
||||
|
||||
return {
|
||||
merchantTipId: tipId,
|
||||
merchantOrigin: new URL(merchantBaseUrl).origin,
|
||||
merchantBaseUrl,
|
||||
merchantTipId: tipId,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a taler[+http]://refund URI.
|
||||
* Return undefined if not passed a valid URI.
|
||||
*/
|
||||
export function parseRefundUri(s: string): RefundUriResult | undefined {
|
||||
const pfx = "taler://refund/";
|
||||
|
||||
if (!s.toLowerCase().startsWith(pfx)) {
|
||||
const pi = parseProtoInfo(s, "refund");
|
||||
if (!pi) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const [path, search] = s.slice(pfx.length).split("?");
|
||||
|
||||
let [host, maybePath, maybeInstance, orderId] = path.split("/");
|
||||
|
||||
if (!host) {
|
||||
const c = pi?.rest.split("?");
|
||||
const parts = c[0].split("/");
|
||||
if (parts.length < 2) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
host = host.toLowerCase();
|
||||
|
||||
if (!maybePath) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!orderId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (maybePath === "-") {
|
||||
maybePath = "";
|
||||
} else {
|
||||
maybePath = decodeURIComponent(maybePath) + "/";
|
||||
}
|
||||
let maybeInstancePath = "";
|
||||
if (maybeInstance !== "-") {
|
||||
maybeInstancePath = `instances/${maybeInstance}/`;
|
||||
}
|
||||
|
||||
let protocol = "https";
|
||||
const searchParams = new URLSearchParams(search);
|
||||
if (searchParams.get("insecure") === "1") {
|
||||
protocol = "http";
|
||||
}
|
||||
|
||||
const merchantBaseUrl =
|
||||
`${protocol}://${host}/` + maybePath + maybeInstancePath;
|
||||
const host = parts[0].toLowerCase();
|
||||
const orderId = parts[parts.length - 1];
|
||||
const pathSegments = parts.slice(1, parts.length - 1);
|
||||
const p = [host, ...pathSegments].join("/");
|
||||
const merchantBaseUrl = `${pi.innerProto}://${p}/`;
|
||||
|
||||
return {
|
||||
merchantBaseUrl,
|
||||
|
Loading…
Reference in New Issue
Block a user