thunder-bridge 0.8.8 → 0.8.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +151 -132
- package/dist/index.d.cts +13 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.js +149 -132
- package/dist/server.cjs +36 -1
- package/dist/server.js +36 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -30,6 +30,8 @@ __export(index_exports, {
|
|
|
30
30
|
REQUEST_IN_FLIGHT: () => REQUEST_IN_FLIGHT,
|
|
31
31
|
ThunderBridge: () => ThunderBridge,
|
|
32
32
|
UnverifiedRecipientError: () => UnverifiedRecipientError,
|
|
33
|
+
answerVerifyChallenge: () => answerVerifyChallenge,
|
|
34
|
+
answerVerifyChallengeRequest: () => answerVerifyChallengeRequest,
|
|
33
35
|
answerWebhookChallenge: () => answerWebhookChallenge,
|
|
34
36
|
answerWebhookChallengeRequest: () => answerWebhookChallengeRequest,
|
|
35
37
|
bankRail: () => bankRail,
|
|
@@ -1287,6 +1289,151 @@ function minorScaleOf(currency) {
|
|
|
1287
1289
|
return 10 ** minorUnitsOf(currency);
|
|
1288
1290
|
}
|
|
1289
1291
|
|
|
1292
|
+
// ../core/ed25519.ts
|
|
1293
|
+
var ALGORITHM = { name: "Ed25519" };
|
|
1294
|
+
var SIGNATURE_BYTES = 64;
|
|
1295
|
+
var PUBLIC_KEY_BYTES = 32;
|
|
1296
|
+
var PKCS8_HEADER = hexToBytes("302e020100300506032b657004220420");
|
|
1297
|
+
async function verifyHex(publicKeyHex, signatureHex, payload) {
|
|
1298
|
+
if (!isHex(publicKeyHex) || publicKeyHex.length !== PUBLIC_KEY_BYTES * 2) return false;
|
|
1299
|
+
if (!isHex(signatureHex) || signatureHex.length !== SIGNATURE_BYTES * 2) return false;
|
|
1300
|
+
try {
|
|
1301
|
+
const key = await crypto.subtle.importKey(
|
|
1302
|
+
"raw",
|
|
1303
|
+
asBuffer2(hexToBytes(publicKeyHex)),
|
|
1304
|
+
ALGORITHM,
|
|
1305
|
+
false,
|
|
1306
|
+
["verify"]
|
|
1307
|
+
);
|
|
1308
|
+
return await crypto.subtle.verify(
|
|
1309
|
+
ALGORITHM,
|
|
1310
|
+
key,
|
|
1311
|
+
asBuffer2(hexToBytes(signatureHex)),
|
|
1312
|
+
asBuffer2(payload)
|
|
1313
|
+
);
|
|
1314
|
+
} catch {
|
|
1315
|
+
return false;
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
function asBuffer2(bytes) {
|
|
1319
|
+
return bytes.buffer instanceof ArrayBuffer ? bytes : new Uint8Array(bytes);
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
// src/webhook.ts
|
|
1323
|
+
var SIGNATURE_HEADER = "x-signature";
|
|
1324
|
+
var TIMESTAMP_HEADER = "x-timestamp";
|
|
1325
|
+
var SHARED_SECRET_PREFIX = "sha256=";
|
|
1326
|
+
var GATEWAY_KEY_PREFIX = "ed25519=";
|
|
1327
|
+
var DEFAULT_TOLERANCE_SECS = 300;
|
|
1328
|
+
var CHALLENGE = "webhook-challenge";
|
|
1329
|
+
var VERIFY_CHALLENGE = "verify-challenge";
|
|
1330
|
+
async function verifyWebhookSignature(body, signature, credential, timestamp, options = {}) {
|
|
1331
|
+
if (!recent(timestamp, options.toleranceSecs ?? DEFAULT_TOLERANCE_SECS)) return false;
|
|
1332
|
+
if (typeof credential !== "string") {
|
|
1333
|
+
if (!signature.startsWith(GATEWAY_KEY_PREFIX)) return false;
|
|
1334
|
+
return verifyHex(
|
|
1335
|
+
credential.publicKey.toLowerCase(),
|
|
1336
|
+
signature.slice(GATEWAY_KEY_PREFIX.length).toLowerCase(),
|
|
1337
|
+
signed(timestamp, body)
|
|
1338
|
+
);
|
|
1339
|
+
}
|
|
1340
|
+
if (signature.startsWith(GATEWAY_KEY_PREFIX)) return false;
|
|
1341
|
+
const received = signature.startsWith(SHARED_SECRET_PREFIX) ? signature.slice(SHARED_SECRET_PREFIX.length) : signature;
|
|
1342
|
+
return equalInConstantTime(await sign(credential, timestamp, body), received.toLowerCase());
|
|
1343
|
+
}
|
|
1344
|
+
async function parseWebhook(body, signature, credential, timestamp, options = {}) {
|
|
1345
|
+
if (!await verifyWebhookSignature(body, signature, credential, timestamp, options)) return null;
|
|
1346
|
+
const text2 = typeof body === "string" ? body : new TextDecoder().decode(body);
|
|
1347
|
+
try {
|
|
1348
|
+
return paymentFromWire(JSON.parse(text2));
|
|
1349
|
+
} catch {
|
|
1350
|
+
return null;
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
async function parseWebhookRequest(request, credential, options = {}) {
|
|
1354
|
+
const signature = request.headers.get(SIGNATURE_HEADER);
|
|
1355
|
+
const timestamp = request.headers.get(TIMESTAMP_HEADER);
|
|
1356
|
+
if (signature === null || timestamp === null) return null;
|
|
1357
|
+
return parseWebhook(await request.text(), signature, credential, timestamp, options);
|
|
1358
|
+
}
|
|
1359
|
+
async function parseWatchedWebhook(body, signature, credential, timestamp, options = {}) {
|
|
1360
|
+
if (!await verifyWebhookSignature(body, signature, credential, timestamp, options)) return null;
|
|
1361
|
+
const text2 = typeof body === "string" ? body : new TextDecoder().decode(body);
|
|
1362
|
+
try {
|
|
1363
|
+
return triggerEventFromWire(JSON.parse(text2));
|
|
1364
|
+
} catch {
|
|
1365
|
+
return null;
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
async function parseWatchedWebhookRequest(request, credential, options = {}) {
|
|
1369
|
+
const signature = request.headers.get(SIGNATURE_HEADER);
|
|
1370
|
+
const timestamp = request.headers.get(TIMESTAMP_HEADER);
|
|
1371
|
+
if (signature === null || timestamp === null) return null;
|
|
1372
|
+
return parseWatchedWebhook(await request.text(), signature, credential, timestamp, options);
|
|
1373
|
+
}
|
|
1374
|
+
async function answerWebhookChallenge(body, signature, credential, timestamp, options = {}) {
|
|
1375
|
+
if (!await verifyWebhookSignature(body, signature, credential, timestamp, options)) return null;
|
|
1376
|
+
const text2 = typeof body === "string" ? body : new TextDecoder().decode(body);
|
|
1377
|
+
const nonce = challenged(text2);
|
|
1378
|
+
if (nonce === null) return null;
|
|
1379
|
+
if (typeof credential !== "string") return JSON.stringify({ nonce });
|
|
1380
|
+
return JSON.stringify({
|
|
1381
|
+
nonce,
|
|
1382
|
+
signature: `${SHARED_SECRET_PREFIX}${await hmacHex(credential, nonce)}`
|
|
1383
|
+
});
|
|
1384
|
+
}
|
|
1385
|
+
async function answerWebhookChallengeRequest(request, credential, options = {}) {
|
|
1386
|
+
const signature = request.headers.get(SIGNATURE_HEADER);
|
|
1387
|
+
const timestamp = request.headers.get(TIMESTAMP_HEADER);
|
|
1388
|
+
if (signature === null || timestamp === null) return null;
|
|
1389
|
+
const answer = await answerWebhookChallenge(
|
|
1390
|
+
await request.clone().text(),
|
|
1391
|
+
signature,
|
|
1392
|
+
credential,
|
|
1393
|
+
timestamp,
|
|
1394
|
+
options
|
|
1395
|
+
);
|
|
1396
|
+
return answer === null ? null : new Response(answer, { headers: { "content-type": "application/json" } });
|
|
1397
|
+
}
|
|
1398
|
+
function challenged(text2) {
|
|
1399
|
+
return nonceOf(text2, CHALLENGE);
|
|
1400
|
+
}
|
|
1401
|
+
function nonceOf(text2, type) {
|
|
1402
|
+
try {
|
|
1403
|
+
const said = JSON.parse(text2);
|
|
1404
|
+
if (said["type"] !== type || typeof said["nonce"] !== "string") return null;
|
|
1405
|
+
return said["nonce"];
|
|
1406
|
+
} catch {
|
|
1407
|
+
return null;
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
function answerVerifyChallenge(body) {
|
|
1411
|
+
const nonce = nonceOf(body, VERIFY_CHALLENGE);
|
|
1412
|
+
return nonce === null ? null : JSON.stringify({ nonce });
|
|
1413
|
+
}
|
|
1414
|
+
async function answerVerifyChallengeRequest(request) {
|
|
1415
|
+
if (request.method !== "POST") return null;
|
|
1416
|
+
const answer = answerVerifyChallenge(await request.clone().text());
|
|
1417
|
+
return answer === null ? null : new Response(answer, { headers: { "content-type": "application/json" } });
|
|
1418
|
+
}
|
|
1419
|
+
function recent(timestamp, toleranceSecs) {
|
|
1420
|
+
const sent = Number(timestamp);
|
|
1421
|
+
if (!Number.isFinite(sent)) return false;
|
|
1422
|
+
return Math.abs(Math.floor(Date.now() / 1e3) - sent) <= toleranceSecs;
|
|
1423
|
+
}
|
|
1424
|
+
async function sign(secret, timestamp, body) {
|
|
1425
|
+
return hmacHex(secret, signed(timestamp, body));
|
|
1426
|
+
}
|
|
1427
|
+
function signed(timestamp, body) {
|
|
1428
|
+
const encoder = new TextEncoder();
|
|
1429
|
+
const stamp = encoder.encode(`${timestamp}.`);
|
|
1430
|
+
const rest = typeof body === "string" ? encoder.encode(body) : new Uint8Array(body);
|
|
1431
|
+
const payload = new Uint8Array(stamp.length + rest.length);
|
|
1432
|
+
payload.set(stamp);
|
|
1433
|
+
payload.set(rest, stamp.length);
|
|
1434
|
+
return payload;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1290
1437
|
// src/bank.ts
|
|
1291
1438
|
var DEFAULT_CURRENCY = "CZK";
|
|
1292
1439
|
var DEFAULT_LOOK_BACK_SECS = 7 * 24 * 60 * 60;
|
|
@@ -1321,6 +1468,8 @@ function bankVerifyEndpoint(config) {
|
|
|
1321
1468
|
"cache-control": `max-age=${config.pollEverySecs ?? DEFAULT_POLL_EVERY_SECS}`
|
|
1322
1469
|
};
|
|
1323
1470
|
return async (request) => {
|
|
1471
|
+
const consented = await answerVerifyChallengeRequest(request);
|
|
1472
|
+
if (consented !== null) return consented;
|
|
1324
1473
|
const asked = readQuery(new URL(request.url));
|
|
1325
1474
|
if (asked === null) return Response.json({ settled: false }, { status: 400 });
|
|
1326
1475
|
const subject = subjectOf(asked.reference, asked.amountMinor, asked.currency);
|
|
@@ -1791,138 +1940,6 @@ function refusals2(answers) {
|
|
|
1791
1940
|
(answer) => answer.reason instanceof Error ? answer.reason.message : String(answer.reason)
|
|
1792
1941
|
).join("; ");
|
|
1793
1942
|
}
|
|
1794
|
-
|
|
1795
|
-
// ../core/ed25519.ts
|
|
1796
|
-
var ALGORITHM = { name: "Ed25519" };
|
|
1797
|
-
var SIGNATURE_BYTES = 64;
|
|
1798
|
-
var PUBLIC_KEY_BYTES = 32;
|
|
1799
|
-
var PKCS8_HEADER = hexToBytes("302e020100300506032b657004220420");
|
|
1800
|
-
async function verifyHex(publicKeyHex, signatureHex, payload) {
|
|
1801
|
-
if (!isHex(publicKeyHex) || publicKeyHex.length !== PUBLIC_KEY_BYTES * 2) return false;
|
|
1802
|
-
if (!isHex(signatureHex) || signatureHex.length !== SIGNATURE_BYTES * 2) return false;
|
|
1803
|
-
try {
|
|
1804
|
-
const key = await crypto.subtle.importKey(
|
|
1805
|
-
"raw",
|
|
1806
|
-
asBuffer2(hexToBytes(publicKeyHex)),
|
|
1807
|
-
ALGORITHM,
|
|
1808
|
-
false,
|
|
1809
|
-
["verify"]
|
|
1810
|
-
);
|
|
1811
|
-
return await crypto.subtle.verify(
|
|
1812
|
-
ALGORITHM,
|
|
1813
|
-
key,
|
|
1814
|
-
asBuffer2(hexToBytes(signatureHex)),
|
|
1815
|
-
asBuffer2(payload)
|
|
1816
|
-
);
|
|
1817
|
-
} catch {
|
|
1818
|
-
return false;
|
|
1819
|
-
}
|
|
1820
|
-
}
|
|
1821
|
-
function asBuffer2(bytes) {
|
|
1822
|
-
return bytes.buffer instanceof ArrayBuffer ? bytes : new Uint8Array(bytes);
|
|
1823
|
-
}
|
|
1824
|
-
|
|
1825
|
-
// src/webhook.ts
|
|
1826
|
-
var SIGNATURE_HEADER = "x-signature";
|
|
1827
|
-
var TIMESTAMP_HEADER = "x-timestamp";
|
|
1828
|
-
var SHARED_SECRET_PREFIX = "sha256=";
|
|
1829
|
-
var GATEWAY_KEY_PREFIX = "ed25519=";
|
|
1830
|
-
var DEFAULT_TOLERANCE_SECS = 300;
|
|
1831
|
-
var CHALLENGE = "webhook-challenge";
|
|
1832
|
-
async function verifyWebhookSignature(body, signature, credential, timestamp, options = {}) {
|
|
1833
|
-
if (!recent(timestamp, options.toleranceSecs ?? DEFAULT_TOLERANCE_SECS)) return false;
|
|
1834
|
-
if (typeof credential !== "string") {
|
|
1835
|
-
if (!signature.startsWith(GATEWAY_KEY_PREFIX)) return false;
|
|
1836
|
-
return verifyHex(
|
|
1837
|
-
credential.publicKey.toLowerCase(),
|
|
1838
|
-
signature.slice(GATEWAY_KEY_PREFIX.length).toLowerCase(),
|
|
1839
|
-
signed(timestamp, body)
|
|
1840
|
-
);
|
|
1841
|
-
}
|
|
1842
|
-
if (signature.startsWith(GATEWAY_KEY_PREFIX)) return false;
|
|
1843
|
-
const received = signature.startsWith(SHARED_SECRET_PREFIX) ? signature.slice(SHARED_SECRET_PREFIX.length) : signature;
|
|
1844
|
-
return equalInConstantTime(await sign(credential, timestamp, body), received.toLowerCase());
|
|
1845
|
-
}
|
|
1846
|
-
async function parseWebhook(body, signature, credential, timestamp, options = {}) {
|
|
1847
|
-
if (!await verifyWebhookSignature(body, signature, credential, timestamp, options)) return null;
|
|
1848
|
-
const text2 = typeof body === "string" ? body : new TextDecoder().decode(body);
|
|
1849
|
-
try {
|
|
1850
|
-
return paymentFromWire(JSON.parse(text2));
|
|
1851
|
-
} catch {
|
|
1852
|
-
return null;
|
|
1853
|
-
}
|
|
1854
|
-
}
|
|
1855
|
-
async function parseWebhookRequest(request, credential, options = {}) {
|
|
1856
|
-
const signature = request.headers.get(SIGNATURE_HEADER);
|
|
1857
|
-
const timestamp = request.headers.get(TIMESTAMP_HEADER);
|
|
1858
|
-
if (signature === null || timestamp === null) return null;
|
|
1859
|
-
return parseWebhook(await request.text(), signature, credential, timestamp, options);
|
|
1860
|
-
}
|
|
1861
|
-
async function parseWatchedWebhook(body, signature, credential, timestamp, options = {}) {
|
|
1862
|
-
if (!await verifyWebhookSignature(body, signature, credential, timestamp, options)) return null;
|
|
1863
|
-
const text2 = typeof body === "string" ? body : new TextDecoder().decode(body);
|
|
1864
|
-
try {
|
|
1865
|
-
return triggerEventFromWire(JSON.parse(text2));
|
|
1866
|
-
} catch {
|
|
1867
|
-
return null;
|
|
1868
|
-
}
|
|
1869
|
-
}
|
|
1870
|
-
async function parseWatchedWebhookRequest(request, credential, options = {}) {
|
|
1871
|
-
const signature = request.headers.get(SIGNATURE_HEADER);
|
|
1872
|
-
const timestamp = request.headers.get(TIMESTAMP_HEADER);
|
|
1873
|
-
if (signature === null || timestamp === null) return null;
|
|
1874
|
-
return parseWatchedWebhook(await request.text(), signature, credential, timestamp, options);
|
|
1875
|
-
}
|
|
1876
|
-
async function answerWebhookChallenge(body, signature, credential, timestamp, options = {}) {
|
|
1877
|
-
if (!await verifyWebhookSignature(body, signature, credential, timestamp, options)) return null;
|
|
1878
|
-
const text2 = typeof body === "string" ? body : new TextDecoder().decode(body);
|
|
1879
|
-
const nonce = challenged(text2);
|
|
1880
|
-
if (nonce === null) return null;
|
|
1881
|
-
if (typeof credential !== "string") return JSON.stringify({ nonce });
|
|
1882
|
-
return JSON.stringify({
|
|
1883
|
-
nonce,
|
|
1884
|
-
signature: `${SHARED_SECRET_PREFIX}${await hmacHex(credential, nonce)}`
|
|
1885
|
-
});
|
|
1886
|
-
}
|
|
1887
|
-
async function answerWebhookChallengeRequest(request, credential, options = {}) {
|
|
1888
|
-
const signature = request.headers.get(SIGNATURE_HEADER);
|
|
1889
|
-
const timestamp = request.headers.get(TIMESTAMP_HEADER);
|
|
1890
|
-
if (signature === null || timestamp === null) return null;
|
|
1891
|
-
const answer = await answerWebhookChallenge(
|
|
1892
|
-
await request.clone().text(),
|
|
1893
|
-
signature,
|
|
1894
|
-
credential,
|
|
1895
|
-
timestamp,
|
|
1896
|
-
options
|
|
1897
|
-
);
|
|
1898
|
-
return answer === null ? null : new Response(answer, { headers: { "content-type": "application/json" } });
|
|
1899
|
-
}
|
|
1900
|
-
function challenged(text2) {
|
|
1901
|
-
try {
|
|
1902
|
-
const said = JSON.parse(text2);
|
|
1903
|
-
if (said["type"] !== CHALLENGE || typeof said["nonce"] !== "string") return null;
|
|
1904
|
-
return said["nonce"];
|
|
1905
|
-
} catch {
|
|
1906
|
-
return null;
|
|
1907
|
-
}
|
|
1908
|
-
}
|
|
1909
|
-
function recent(timestamp, toleranceSecs) {
|
|
1910
|
-
const sent = Number(timestamp);
|
|
1911
|
-
if (!Number.isFinite(sent)) return false;
|
|
1912
|
-
return Math.abs(Math.floor(Date.now() / 1e3) - sent) <= toleranceSecs;
|
|
1913
|
-
}
|
|
1914
|
-
async function sign(secret, timestamp, body) {
|
|
1915
|
-
return hmacHex(secret, signed(timestamp, body));
|
|
1916
|
-
}
|
|
1917
|
-
function signed(timestamp, body) {
|
|
1918
|
-
const encoder = new TextEncoder();
|
|
1919
|
-
const stamp = encoder.encode(`${timestamp}.`);
|
|
1920
|
-
const rest = typeof body === "string" ? encoder.encode(body) : new Uint8Array(body);
|
|
1921
|
-
const payload = new Uint8Array(stamp.length + rest.length);
|
|
1922
|
-
payload.set(stamp);
|
|
1923
|
-
payload.set(rest, stamp.length);
|
|
1924
|
-
return payload;
|
|
1925
|
-
}
|
|
1926
1943
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1927
1944
|
0 && (module.exports = {
|
|
1928
1945
|
GatewayCheatError,
|
|
@@ -1935,6 +1952,8 @@ function signed(timestamp, body) {
|
|
|
1935
1952
|
REQUEST_IN_FLIGHT,
|
|
1936
1953
|
ThunderBridge,
|
|
1937
1954
|
UnverifiedRecipientError,
|
|
1955
|
+
answerVerifyChallenge,
|
|
1956
|
+
answerVerifyChallengeRequest,
|
|
1938
1957
|
answerWebhookChallenge,
|
|
1939
1958
|
answerWebhookChallengeRequest,
|
|
1940
1959
|
bankRail,
|
package/dist/index.d.cts
CHANGED
|
@@ -381,6 +381,18 @@ declare function answerWebhookChallenge(body: string | Uint8Array, signature: st
|
|
|
381
381
|
* the same handler can go on to `parseWebhookRequest` when this was no challenge
|
|
382
382
|
*/
|
|
383
383
|
declare function answerWebhookChallengeRequest(request: Request, credential: WebhookCredential, options?: WebhookOptions): Promise<Response | null>;
|
|
384
|
+
/**
|
|
385
|
+
* Answer the challenge the gateway sends a verify URL before it will poll it,
|
|
386
|
+
* which is how a caller shows the endpoint agreed to the traffic rather than
|
|
387
|
+
* merely being named. Returns null for anything that is not a challenge, so a
|
|
388
|
+
* verify endpoint hands the request on to its own reading of a payment.
|
|
389
|
+
*
|
|
390
|
+
* The nonce is echoed to whoever asked, which grants them nothing, so there is
|
|
391
|
+
* no signature to check here and no secret to hold
|
|
392
|
+
*/
|
|
393
|
+
declare function answerVerifyChallenge(body: string): string | null;
|
|
394
|
+
/** {@link answerVerifyChallenge} against a `Request`, leaving its body unread */
|
|
395
|
+
declare function answerVerifyChallengeRequest(request: Request): Promise<Response | null>;
|
|
384
396
|
|
|
385
397
|
/**
|
|
386
398
|
* The way a gateway was caught out, every code is a check that held against the
|
|
@@ -458,4 +470,4 @@ declare class NoWalletAvailableError extends ProblemError {
|
|
|
458
470
|
}, wallets: WalletFailure[]);
|
|
459
471
|
}
|
|
460
472
|
|
|
461
|
-
export { type BankTransfer, type BankTransferParams, type BankVerifyConfig, CreatePaymentParams, type Credit, type FioConfig, type GatewayCheatCode, GatewayCheatError, IDEMPOTENCY_KEY_REUSED, type IdempotencyConflict, IdempotencyConflictError, type Invoice, type MedianOptions, NO_WALLET_AVAILABLE, NoWalletAvailableError, PAYMENT_ALREADY_WATCHED, Payment, ProblemError, type QrOptions, REQUEST_IN_FLIGHT, type Statement, ThunderBridge, type Ticker, TriggerEvent, UnverifiedRecipientError, WalletFailure, type WebhookCredential, type WebhookOptions, answerWebhookChallenge, answerWebhookChallengeRequest, bankTransfer, bankVerifyEndpoint, bitstamp, coinbase, coinmate, decodeInvoice, fioStatement, invoiceToDataUrl, invoiceToSvg, isProblemType, isProvablyPaid, kraken, lnurlToDataUrl, lnurlToSvg, medianOf, minorScaleOf, minorUnitsOf, msatFor, parseWatchedWebhook, parseWatchedWebhookRequest, parseWebhook, parseWebhookRequest, preimageMatchesHash, proveOrigin, proveSettlement, qrToDataUrl, qrToSvg, seal, spdToDataUrl, spdToSvg, toLnurl, unseal, verifyWebhookSignature };
|
|
473
|
+
export { type BankTransfer, type BankTransferParams, type BankVerifyConfig, CreatePaymentParams, type Credit, type FioConfig, type GatewayCheatCode, GatewayCheatError, IDEMPOTENCY_KEY_REUSED, type IdempotencyConflict, IdempotencyConflictError, type Invoice, type MedianOptions, NO_WALLET_AVAILABLE, NoWalletAvailableError, PAYMENT_ALREADY_WATCHED, Payment, ProblemError, type QrOptions, REQUEST_IN_FLIGHT, type Statement, ThunderBridge, type Ticker, TriggerEvent, UnverifiedRecipientError, WalletFailure, type WebhookCredential, type WebhookOptions, answerVerifyChallenge, answerVerifyChallengeRequest, answerWebhookChallenge, answerWebhookChallengeRequest, bankTransfer, bankVerifyEndpoint, bitstamp, coinbase, coinmate, decodeInvoice, fioStatement, invoiceToDataUrl, invoiceToSvg, isProblemType, isProvablyPaid, kraken, lnurlToDataUrl, lnurlToSvg, medianOf, minorScaleOf, minorUnitsOf, msatFor, parseWatchedWebhook, parseWatchedWebhookRequest, parseWebhook, parseWebhookRequest, preimageMatchesHash, proveOrigin, proveSettlement, qrToDataUrl, qrToSvg, seal, spdToDataUrl, spdToSvg, toLnurl, unseal, verifyWebhookSignature };
|
package/dist/index.d.ts
CHANGED
|
@@ -381,6 +381,18 @@ declare function answerWebhookChallenge(body: string | Uint8Array, signature: st
|
|
|
381
381
|
* the same handler can go on to `parseWebhookRequest` when this was no challenge
|
|
382
382
|
*/
|
|
383
383
|
declare function answerWebhookChallengeRequest(request: Request, credential: WebhookCredential, options?: WebhookOptions): Promise<Response | null>;
|
|
384
|
+
/**
|
|
385
|
+
* Answer the challenge the gateway sends a verify URL before it will poll it,
|
|
386
|
+
* which is how a caller shows the endpoint agreed to the traffic rather than
|
|
387
|
+
* merely being named. Returns null for anything that is not a challenge, so a
|
|
388
|
+
* verify endpoint hands the request on to its own reading of a payment.
|
|
389
|
+
*
|
|
390
|
+
* The nonce is echoed to whoever asked, which grants them nothing, so there is
|
|
391
|
+
* no signature to check here and no secret to hold
|
|
392
|
+
*/
|
|
393
|
+
declare function answerVerifyChallenge(body: string): string | null;
|
|
394
|
+
/** {@link answerVerifyChallenge} against a `Request`, leaving its body unread */
|
|
395
|
+
declare function answerVerifyChallengeRequest(request: Request): Promise<Response | null>;
|
|
384
396
|
|
|
385
397
|
/**
|
|
386
398
|
* The way a gateway was caught out, every code is a check that held against the
|
|
@@ -458,4 +470,4 @@ declare class NoWalletAvailableError extends ProblemError {
|
|
|
458
470
|
}, wallets: WalletFailure[]);
|
|
459
471
|
}
|
|
460
472
|
|
|
461
|
-
export { type BankTransfer, type BankTransferParams, type BankVerifyConfig, CreatePaymentParams, type Credit, type FioConfig, type GatewayCheatCode, GatewayCheatError, IDEMPOTENCY_KEY_REUSED, type IdempotencyConflict, IdempotencyConflictError, type Invoice, type MedianOptions, NO_WALLET_AVAILABLE, NoWalletAvailableError, PAYMENT_ALREADY_WATCHED, Payment, ProblemError, type QrOptions, REQUEST_IN_FLIGHT, type Statement, ThunderBridge, type Ticker, TriggerEvent, UnverifiedRecipientError, WalletFailure, type WebhookCredential, type WebhookOptions, answerWebhookChallenge, answerWebhookChallengeRequest, bankTransfer, bankVerifyEndpoint, bitstamp, coinbase, coinmate, decodeInvoice, fioStatement, invoiceToDataUrl, invoiceToSvg, isProblemType, isProvablyPaid, kraken, lnurlToDataUrl, lnurlToSvg, medianOf, minorScaleOf, minorUnitsOf, msatFor, parseWatchedWebhook, parseWatchedWebhookRequest, parseWebhook, parseWebhookRequest, preimageMatchesHash, proveOrigin, proveSettlement, qrToDataUrl, qrToSvg, seal, spdToDataUrl, spdToSvg, toLnurl, unseal, verifyWebhookSignature };
|
|
473
|
+
export { type BankTransfer, type BankTransferParams, type BankVerifyConfig, CreatePaymentParams, type Credit, type FioConfig, type GatewayCheatCode, GatewayCheatError, IDEMPOTENCY_KEY_REUSED, type IdempotencyConflict, IdempotencyConflictError, type Invoice, type MedianOptions, NO_WALLET_AVAILABLE, NoWalletAvailableError, PAYMENT_ALREADY_WATCHED, Payment, ProblemError, type QrOptions, REQUEST_IN_FLIGHT, type Statement, ThunderBridge, type Ticker, TriggerEvent, UnverifiedRecipientError, WalletFailure, type WebhookCredential, type WebhookOptions, answerVerifyChallenge, answerVerifyChallengeRequest, answerWebhookChallenge, answerWebhookChallengeRequest, bankTransfer, bankVerifyEndpoint, bitstamp, coinbase, coinmate, decodeInvoice, fioStatement, invoiceToDataUrl, invoiceToSvg, isProblemType, isProvablyPaid, kraken, lnurlToDataUrl, lnurlToSvg, medianOf, minorScaleOf, minorUnitsOf, msatFor, parseWatchedWebhook, parseWatchedWebhookRequest, parseWebhook, parseWebhookRequest, preimageMatchesHash, proveOrigin, proveSettlement, qrToDataUrl, qrToSvg, seal, spdToDataUrl, spdToSvg, toLnurl, unseal, verifyWebhookSignature };
|
package/dist/index.js
CHANGED
|
@@ -1215,6 +1215,151 @@ function minorScaleOf(currency) {
|
|
|
1215
1215
|
return 10 ** minorUnitsOf(currency);
|
|
1216
1216
|
}
|
|
1217
1217
|
|
|
1218
|
+
// ../core/ed25519.ts
|
|
1219
|
+
var ALGORITHM = { name: "Ed25519" };
|
|
1220
|
+
var SIGNATURE_BYTES = 64;
|
|
1221
|
+
var PUBLIC_KEY_BYTES = 32;
|
|
1222
|
+
var PKCS8_HEADER = hexToBytes("302e020100300506032b657004220420");
|
|
1223
|
+
async function verifyHex(publicKeyHex, signatureHex, payload) {
|
|
1224
|
+
if (!isHex(publicKeyHex) || publicKeyHex.length !== PUBLIC_KEY_BYTES * 2) return false;
|
|
1225
|
+
if (!isHex(signatureHex) || signatureHex.length !== SIGNATURE_BYTES * 2) return false;
|
|
1226
|
+
try {
|
|
1227
|
+
const key = await crypto.subtle.importKey(
|
|
1228
|
+
"raw",
|
|
1229
|
+
asBuffer2(hexToBytes(publicKeyHex)),
|
|
1230
|
+
ALGORITHM,
|
|
1231
|
+
false,
|
|
1232
|
+
["verify"]
|
|
1233
|
+
);
|
|
1234
|
+
return await crypto.subtle.verify(
|
|
1235
|
+
ALGORITHM,
|
|
1236
|
+
key,
|
|
1237
|
+
asBuffer2(hexToBytes(signatureHex)),
|
|
1238
|
+
asBuffer2(payload)
|
|
1239
|
+
);
|
|
1240
|
+
} catch {
|
|
1241
|
+
return false;
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
function asBuffer2(bytes) {
|
|
1245
|
+
return bytes.buffer instanceof ArrayBuffer ? bytes : new Uint8Array(bytes);
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
// src/webhook.ts
|
|
1249
|
+
var SIGNATURE_HEADER = "x-signature";
|
|
1250
|
+
var TIMESTAMP_HEADER = "x-timestamp";
|
|
1251
|
+
var SHARED_SECRET_PREFIX = "sha256=";
|
|
1252
|
+
var GATEWAY_KEY_PREFIX = "ed25519=";
|
|
1253
|
+
var DEFAULT_TOLERANCE_SECS = 300;
|
|
1254
|
+
var CHALLENGE = "webhook-challenge";
|
|
1255
|
+
var VERIFY_CHALLENGE = "verify-challenge";
|
|
1256
|
+
async function verifyWebhookSignature(body, signature, credential, timestamp, options = {}) {
|
|
1257
|
+
if (!recent(timestamp, options.toleranceSecs ?? DEFAULT_TOLERANCE_SECS)) return false;
|
|
1258
|
+
if (typeof credential !== "string") {
|
|
1259
|
+
if (!signature.startsWith(GATEWAY_KEY_PREFIX)) return false;
|
|
1260
|
+
return verifyHex(
|
|
1261
|
+
credential.publicKey.toLowerCase(),
|
|
1262
|
+
signature.slice(GATEWAY_KEY_PREFIX.length).toLowerCase(),
|
|
1263
|
+
signed(timestamp, body)
|
|
1264
|
+
);
|
|
1265
|
+
}
|
|
1266
|
+
if (signature.startsWith(GATEWAY_KEY_PREFIX)) return false;
|
|
1267
|
+
const received = signature.startsWith(SHARED_SECRET_PREFIX) ? signature.slice(SHARED_SECRET_PREFIX.length) : signature;
|
|
1268
|
+
return equalInConstantTime(await sign(credential, timestamp, body), received.toLowerCase());
|
|
1269
|
+
}
|
|
1270
|
+
async function parseWebhook(body, signature, credential, timestamp, options = {}) {
|
|
1271
|
+
if (!await verifyWebhookSignature(body, signature, credential, timestamp, options)) return null;
|
|
1272
|
+
const text2 = typeof body === "string" ? body : new TextDecoder().decode(body);
|
|
1273
|
+
try {
|
|
1274
|
+
return paymentFromWire(JSON.parse(text2));
|
|
1275
|
+
} catch {
|
|
1276
|
+
return null;
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
async function parseWebhookRequest(request, credential, options = {}) {
|
|
1280
|
+
const signature = request.headers.get(SIGNATURE_HEADER);
|
|
1281
|
+
const timestamp = request.headers.get(TIMESTAMP_HEADER);
|
|
1282
|
+
if (signature === null || timestamp === null) return null;
|
|
1283
|
+
return parseWebhook(await request.text(), signature, credential, timestamp, options);
|
|
1284
|
+
}
|
|
1285
|
+
async function parseWatchedWebhook(body, signature, credential, timestamp, options = {}) {
|
|
1286
|
+
if (!await verifyWebhookSignature(body, signature, credential, timestamp, options)) return null;
|
|
1287
|
+
const text2 = typeof body === "string" ? body : new TextDecoder().decode(body);
|
|
1288
|
+
try {
|
|
1289
|
+
return triggerEventFromWire(JSON.parse(text2));
|
|
1290
|
+
} catch {
|
|
1291
|
+
return null;
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
async function parseWatchedWebhookRequest(request, credential, options = {}) {
|
|
1295
|
+
const signature = request.headers.get(SIGNATURE_HEADER);
|
|
1296
|
+
const timestamp = request.headers.get(TIMESTAMP_HEADER);
|
|
1297
|
+
if (signature === null || timestamp === null) return null;
|
|
1298
|
+
return parseWatchedWebhook(await request.text(), signature, credential, timestamp, options);
|
|
1299
|
+
}
|
|
1300
|
+
async function answerWebhookChallenge(body, signature, credential, timestamp, options = {}) {
|
|
1301
|
+
if (!await verifyWebhookSignature(body, signature, credential, timestamp, options)) return null;
|
|
1302
|
+
const text2 = typeof body === "string" ? body : new TextDecoder().decode(body);
|
|
1303
|
+
const nonce = challenged(text2);
|
|
1304
|
+
if (nonce === null) return null;
|
|
1305
|
+
if (typeof credential !== "string") return JSON.stringify({ nonce });
|
|
1306
|
+
return JSON.stringify({
|
|
1307
|
+
nonce,
|
|
1308
|
+
signature: `${SHARED_SECRET_PREFIX}${await hmacHex(credential, nonce)}`
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
async function answerWebhookChallengeRequest(request, credential, options = {}) {
|
|
1312
|
+
const signature = request.headers.get(SIGNATURE_HEADER);
|
|
1313
|
+
const timestamp = request.headers.get(TIMESTAMP_HEADER);
|
|
1314
|
+
if (signature === null || timestamp === null) return null;
|
|
1315
|
+
const answer = await answerWebhookChallenge(
|
|
1316
|
+
await request.clone().text(),
|
|
1317
|
+
signature,
|
|
1318
|
+
credential,
|
|
1319
|
+
timestamp,
|
|
1320
|
+
options
|
|
1321
|
+
);
|
|
1322
|
+
return answer === null ? null : new Response(answer, { headers: { "content-type": "application/json" } });
|
|
1323
|
+
}
|
|
1324
|
+
function challenged(text2) {
|
|
1325
|
+
return nonceOf(text2, CHALLENGE);
|
|
1326
|
+
}
|
|
1327
|
+
function nonceOf(text2, type) {
|
|
1328
|
+
try {
|
|
1329
|
+
const said = JSON.parse(text2);
|
|
1330
|
+
if (said["type"] !== type || typeof said["nonce"] !== "string") return null;
|
|
1331
|
+
return said["nonce"];
|
|
1332
|
+
} catch {
|
|
1333
|
+
return null;
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
function answerVerifyChallenge(body) {
|
|
1337
|
+
const nonce = nonceOf(body, VERIFY_CHALLENGE);
|
|
1338
|
+
return nonce === null ? null : JSON.stringify({ nonce });
|
|
1339
|
+
}
|
|
1340
|
+
async function answerVerifyChallengeRequest(request) {
|
|
1341
|
+
if (request.method !== "POST") return null;
|
|
1342
|
+
const answer = answerVerifyChallenge(await request.clone().text());
|
|
1343
|
+
return answer === null ? null : new Response(answer, { headers: { "content-type": "application/json" } });
|
|
1344
|
+
}
|
|
1345
|
+
function recent(timestamp, toleranceSecs) {
|
|
1346
|
+
const sent = Number(timestamp);
|
|
1347
|
+
if (!Number.isFinite(sent)) return false;
|
|
1348
|
+
return Math.abs(Math.floor(Date.now() / 1e3) - sent) <= toleranceSecs;
|
|
1349
|
+
}
|
|
1350
|
+
async function sign(secret, timestamp, body) {
|
|
1351
|
+
return hmacHex(secret, signed(timestamp, body));
|
|
1352
|
+
}
|
|
1353
|
+
function signed(timestamp, body) {
|
|
1354
|
+
const encoder = new TextEncoder();
|
|
1355
|
+
const stamp = encoder.encode(`${timestamp}.`);
|
|
1356
|
+
const rest = typeof body === "string" ? encoder.encode(body) : new Uint8Array(body);
|
|
1357
|
+
const payload = new Uint8Array(stamp.length + rest.length);
|
|
1358
|
+
payload.set(stamp);
|
|
1359
|
+
payload.set(rest, stamp.length);
|
|
1360
|
+
return payload;
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1218
1363
|
// src/bank.ts
|
|
1219
1364
|
var DEFAULT_CURRENCY = "CZK";
|
|
1220
1365
|
var DEFAULT_LOOK_BACK_SECS = 7 * 24 * 60 * 60;
|
|
@@ -1249,6 +1394,8 @@ function bankVerifyEndpoint(config) {
|
|
|
1249
1394
|
"cache-control": `max-age=${config.pollEverySecs ?? DEFAULT_POLL_EVERY_SECS}`
|
|
1250
1395
|
};
|
|
1251
1396
|
return async (request) => {
|
|
1397
|
+
const consented = await answerVerifyChallengeRequest(request);
|
|
1398
|
+
if (consented !== null) return consented;
|
|
1252
1399
|
const asked = readQuery(new URL(request.url));
|
|
1253
1400
|
if (asked === null) return Response.json({ settled: false }, { status: 400 });
|
|
1254
1401
|
const subject = subjectOf(asked.reference, asked.amountMinor, asked.currency);
|
|
@@ -1719,138 +1866,6 @@ function refusals2(answers) {
|
|
|
1719
1866
|
(answer) => answer.reason instanceof Error ? answer.reason.message : String(answer.reason)
|
|
1720
1867
|
).join("; ");
|
|
1721
1868
|
}
|
|
1722
|
-
|
|
1723
|
-
// ../core/ed25519.ts
|
|
1724
|
-
var ALGORITHM = { name: "Ed25519" };
|
|
1725
|
-
var SIGNATURE_BYTES = 64;
|
|
1726
|
-
var PUBLIC_KEY_BYTES = 32;
|
|
1727
|
-
var PKCS8_HEADER = hexToBytes("302e020100300506032b657004220420");
|
|
1728
|
-
async function verifyHex(publicKeyHex, signatureHex, payload) {
|
|
1729
|
-
if (!isHex(publicKeyHex) || publicKeyHex.length !== PUBLIC_KEY_BYTES * 2) return false;
|
|
1730
|
-
if (!isHex(signatureHex) || signatureHex.length !== SIGNATURE_BYTES * 2) return false;
|
|
1731
|
-
try {
|
|
1732
|
-
const key = await crypto.subtle.importKey(
|
|
1733
|
-
"raw",
|
|
1734
|
-
asBuffer2(hexToBytes(publicKeyHex)),
|
|
1735
|
-
ALGORITHM,
|
|
1736
|
-
false,
|
|
1737
|
-
["verify"]
|
|
1738
|
-
);
|
|
1739
|
-
return await crypto.subtle.verify(
|
|
1740
|
-
ALGORITHM,
|
|
1741
|
-
key,
|
|
1742
|
-
asBuffer2(hexToBytes(signatureHex)),
|
|
1743
|
-
asBuffer2(payload)
|
|
1744
|
-
);
|
|
1745
|
-
} catch {
|
|
1746
|
-
return false;
|
|
1747
|
-
}
|
|
1748
|
-
}
|
|
1749
|
-
function asBuffer2(bytes) {
|
|
1750
|
-
return bytes.buffer instanceof ArrayBuffer ? bytes : new Uint8Array(bytes);
|
|
1751
|
-
}
|
|
1752
|
-
|
|
1753
|
-
// src/webhook.ts
|
|
1754
|
-
var SIGNATURE_HEADER = "x-signature";
|
|
1755
|
-
var TIMESTAMP_HEADER = "x-timestamp";
|
|
1756
|
-
var SHARED_SECRET_PREFIX = "sha256=";
|
|
1757
|
-
var GATEWAY_KEY_PREFIX = "ed25519=";
|
|
1758
|
-
var DEFAULT_TOLERANCE_SECS = 300;
|
|
1759
|
-
var CHALLENGE = "webhook-challenge";
|
|
1760
|
-
async function verifyWebhookSignature(body, signature, credential, timestamp, options = {}) {
|
|
1761
|
-
if (!recent(timestamp, options.toleranceSecs ?? DEFAULT_TOLERANCE_SECS)) return false;
|
|
1762
|
-
if (typeof credential !== "string") {
|
|
1763
|
-
if (!signature.startsWith(GATEWAY_KEY_PREFIX)) return false;
|
|
1764
|
-
return verifyHex(
|
|
1765
|
-
credential.publicKey.toLowerCase(),
|
|
1766
|
-
signature.slice(GATEWAY_KEY_PREFIX.length).toLowerCase(),
|
|
1767
|
-
signed(timestamp, body)
|
|
1768
|
-
);
|
|
1769
|
-
}
|
|
1770
|
-
if (signature.startsWith(GATEWAY_KEY_PREFIX)) return false;
|
|
1771
|
-
const received = signature.startsWith(SHARED_SECRET_PREFIX) ? signature.slice(SHARED_SECRET_PREFIX.length) : signature;
|
|
1772
|
-
return equalInConstantTime(await sign(credential, timestamp, body), received.toLowerCase());
|
|
1773
|
-
}
|
|
1774
|
-
async function parseWebhook(body, signature, credential, timestamp, options = {}) {
|
|
1775
|
-
if (!await verifyWebhookSignature(body, signature, credential, timestamp, options)) return null;
|
|
1776
|
-
const text2 = typeof body === "string" ? body : new TextDecoder().decode(body);
|
|
1777
|
-
try {
|
|
1778
|
-
return paymentFromWire(JSON.parse(text2));
|
|
1779
|
-
} catch {
|
|
1780
|
-
return null;
|
|
1781
|
-
}
|
|
1782
|
-
}
|
|
1783
|
-
async function parseWebhookRequest(request, credential, options = {}) {
|
|
1784
|
-
const signature = request.headers.get(SIGNATURE_HEADER);
|
|
1785
|
-
const timestamp = request.headers.get(TIMESTAMP_HEADER);
|
|
1786
|
-
if (signature === null || timestamp === null) return null;
|
|
1787
|
-
return parseWebhook(await request.text(), signature, credential, timestamp, options);
|
|
1788
|
-
}
|
|
1789
|
-
async function parseWatchedWebhook(body, signature, credential, timestamp, options = {}) {
|
|
1790
|
-
if (!await verifyWebhookSignature(body, signature, credential, timestamp, options)) return null;
|
|
1791
|
-
const text2 = typeof body === "string" ? body : new TextDecoder().decode(body);
|
|
1792
|
-
try {
|
|
1793
|
-
return triggerEventFromWire(JSON.parse(text2));
|
|
1794
|
-
} catch {
|
|
1795
|
-
return null;
|
|
1796
|
-
}
|
|
1797
|
-
}
|
|
1798
|
-
async function parseWatchedWebhookRequest(request, credential, options = {}) {
|
|
1799
|
-
const signature = request.headers.get(SIGNATURE_HEADER);
|
|
1800
|
-
const timestamp = request.headers.get(TIMESTAMP_HEADER);
|
|
1801
|
-
if (signature === null || timestamp === null) return null;
|
|
1802
|
-
return parseWatchedWebhook(await request.text(), signature, credential, timestamp, options);
|
|
1803
|
-
}
|
|
1804
|
-
async function answerWebhookChallenge(body, signature, credential, timestamp, options = {}) {
|
|
1805
|
-
if (!await verifyWebhookSignature(body, signature, credential, timestamp, options)) return null;
|
|
1806
|
-
const text2 = typeof body === "string" ? body : new TextDecoder().decode(body);
|
|
1807
|
-
const nonce = challenged(text2);
|
|
1808
|
-
if (nonce === null) return null;
|
|
1809
|
-
if (typeof credential !== "string") return JSON.stringify({ nonce });
|
|
1810
|
-
return JSON.stringify({
|
|
1811
|
-
nonce,
|
|
1812
|
-
signature: `${SHARED_SECRET_PREFIX}${await hmacHex(credential, nonce)}`
|
|
1813
|
-
});
|
|
1814
|
-
}
|
|
1815
|
-
async function answerWebhookChallengeRequest(request, credential, options = {}) {
|
|
1816
|
-
const signature = request.headers.get(SIGNATURE_HEADER);
|
|
1817
|
-
const timestamp = request.headers.get(TIMESTAMP_HEADER);
|
|
1818
|
-
if (signature === null || timestamp === null) return null;
|
|
1819
|
-
const answer = await answerWebhookChallenge(
|
|
1820
|
-
await request.clone().text(),
|
|
1821
|
-
signature,
|
|
1822
|
-
credential,
|
|
1823
|
-
timestamp,
|
|
1824
|
-
options
|
|
1825
|
-
);
|
|
1826
|
-
return answer === null ? null : new Response(answer, { headers: { "content-type": "application/json" } });
|
|
1827
|
-
}
|
|
1828
|
-
function challenged(text2) {
|
|
1829
|
-
try {
|
|
1830
|
-
const said = JSON.parse(text2);
|
|
1831
|
-
if (said["type"] !== CHALLENGE || typeof said["nonce"] !== "string") return null;
|
|
1832
|
-
return said["nonce"];
|
|
1833
|
-
} catch {
|
|
1834
|
-
return null;
|
|
1835
|
-
}
|
|
1836
|
-
}
|
|
1837
|
-
function recent(timestamp, toleranceSecs) {
|
|
1838
|
-
const sent = Number(timestamp);
|
|
1839
|
-
if (!Number.isFinite(sent)) return false;
|
|
1840
|
-
return Math.abs(Math.floor(Date.now() / 1e3) - sent) <= toleranceSecs;
|
|
1841
|
-
}
|
|
1842
|
-
async function sign(secret, timestamp, body) {
|
|
1843
|
-
return hmacHex(secret, signed(timestamp, body));
|
|
1844
|
-
}
|
|
1845
|
-
function signed(timestamp, body) {
|
|
1846
|
-
const encoder = new TextEncoder();
|
|
1847
|
-
const stamp = encoder.encode(`${timestamp}.`);
|
|
1848
|
-
const rest = typeof body === "string" ? encoder.encode(body) : new Uint8Array(body);
|
|
1849
|
-
const payload = new Uint8Array(stamp.length + rest.length);
|
|
1850
|
-
payload.set(stamp);
|
|
1851
|
-
payload.set(rest, stamp.length);
|
|
1852
|
-
return payload;
|
|
1853
|
-
}
|
|
1854
1869
|
export {
|
|
1855
1870
|
GatewayCheatError,
|
|
1856
1871
|
IDEMPOTENCY_KEY_REUSED,
|
|
@@ -1862,6 +1877,8 @@ export {
|
|
|
1862
1877
|
REQUEST_IN_FLIGHT,
|
|
1863
1878
|
ThunderBridge,
|
|
1864
1879
|
UnverifiedRecipientError,
|
|
1880
|
+
answerVerifyChallenge,
|
|
1881
|
+
answerVerifyChallengeRequest,
|
|
1865
1882
|
answerWebhookChallenge,
|
|
1866
1883
|
answerWebhookChallengeRequest,
|
|
1867
1884
|
bankRail,
|
package/dist/server.cjs
CHANGED
|
@@ -494,6 +494,7 @@ var NoWalletAvailable = class extends Error {
|
|
|
494
494
|
// ../core/lnurl.ts
|
|
495
495
|
var VERIFY_WITHOUT_PREIMAGE = ["zeuspay.com", "zeusnuts.com", "ecash.love"];
|
|
496
496
|
var RESOLVE_TIMEOUT_MS = 3e4;
|
|
497
|
+
var NAMES_ONE_ACCOUNT = /^[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*$/;
|
|
497
498
|
var MIN_PACE_SECS = 1;
|
|
498
499
|
var MAX_PACE_SECS = 3600;
|
|
499
500
|
var MIN_PER_SECOND = 0.01;
|
|
@@ -622,11 +623,19 @@ function withAmount(callback, amountMsat) {
|
|
|
622
623
|
}
|
|
623
624
|
function splitAddress(address) {
|
|
624
625
|
const [user, domain, ...rest] = address.split("@");
|
|
625
|
-
if (!user || !domain || rest.length > 0 || !publicHttps(`https://${domain}/`)) {
|
|
626
|
+
if (!user || !domain || rest.length > 0 || !NAMES_ONE_ACCOUNT.test(user) || !isNothingButAHost(domain) || !publicHttps(`https://${domain}/`)) {
|
|
626
627
|
throw new WalletRefused("address-unusable", `${address} is not a usable lightning address`);
|
|
627
628
|
}
|
|
628
629
|
return [user, domain];
|
|
629
630
|
}
|
|
631
|
+
function isNothingButAHost(domain) {
|
|
632
|
+
try {
|
|
633
|
+
const parsed = new URL(`https://${domain}/`);
|
|
634
|
+
return parsed.host === domain.toLowerCase() && parsed.pathname === "/" && parsed.search === "" && parsed.hash === "";
|
|
635
|
+
} catch {
|
|
636
|
+
return false;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
630
639
|
async function fetchJson(url, deadline) {
|
|
631
640
|
return (await answeredJson(url, deadline)).said;
|
|
632
641
|
}
|
|
@@ -831,6 +840,30 @@ function refuse(reason) {
|
|
|
831
840
|
return Response.json({ status: "ERROR", reason });
|
|
832
841
|
}
|
|
833
842
|
|
|
843
|
+
// ../core/ed25519.ts
|
|
844
|
+
var PKCS8_HEADER = hexToBytes("302e020100300506032b657004220420");
|
|
845
|
+
|
|
846
|
+
// src/webhook.ts
|
|
847
|
+
var VERIFY_CHALLENGE = "verify-challenge";
|
|
848
|
+
function nonceOf(text2, type) {
|
|
849
|
+
try {
|
|
850
|
+
const said = JSON.parse(text2);
|
|
851
|
+
if (said["type"] !== type || typeof said["nonce"] !== "string") return null;
|
|
852
|
+
return said["nonce"];
|
|
853
|
+
} catch {
|
|
854
|
+
return null;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
function answerVerifyChallenge(body) {
|
|
858
|
+
const nonce = nonceOf(body, VERIFY_CHALLENGE);
|
|
859
|
+
return nonce === null ? null : JSON.stringify({ nonce });
|
|
860
|
+
}
|
|
861
|
+
async function answerVerifyChallengeRequest(request) {
|
|
862
|
+
if (request.method !== "POST") return null;
|
|
863
|
+
const answer = answerVerifyChallenge(await request.clone().text());
|
|
864
|
+
return answer === null ? null : new Response(answer, { headers: { "content-type": "application/json" } });
|
|
865
|
+
}
|
|
866
|
+
|
|
834
867
|
// src/qr.ts
|
|
835
868
|
var import_uqr = require("uqr");
|
|
836
869
|
function encodeForQr(destination) {
|
|
@@ -850,6 +883,8 @@ function lightningVerifyEndpoint(config) {
|
|
|
850
883
|
"cache-control": `max-age=${config.pollEverySecs ?? DEFAULT_POLL_EVERY_SECS}`
|
|
851
884
|
};
|
|
852
885
|
return async (request) => {
|
|
886
|
+
const consented = await answerVerifyChallengeRequest(request);
|
|
887
|
+
if (consented !== null) return consented;
|
|
853
888
|
const sealed = new URL(request.url).searchParams.get(WALLET);
|
|
854
889
|
if (sealed === null) return Response.json({ settled: false }, { status: 400 });
|
|
855
890
|
const opened = await unseal(config.secret, sealed);
|
package/dist/server.js
CHANGED
|
@@ -455,6 +455,7 @@ var NoWalletAvailable = class extends Error {
|
|
|
455
455
|
// ../core/lnurl.ts
|
|
456
456
|
var VERIFY_WITHOUT_PREIMAGE = ["zeuspay.com", "zeusnuts.com", "ecash.love"];
|
|
457
457
|
var RESOLVE_TIMEOUT_MS = 3e4;
|
|
458
|
+
var NAMES_ONE_ACCOUNT = /^[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*$/;
|
|
458
459
|
var MIN_PACE_SECS = 1;
|
|
459
460
|
var MAX_PACE_SECS = 3600;
|
|
460
461
|
var MIN_PER_SECOND = 0.01;
|
|
@@ -583,11 +584,19 @@ function withAmount(callback, amountMsat) {
|
|
|
583
584
|
}
|
|
584
585
|
function splitAddress(address) {
|
|
585
586
|
const [user, domain, ...rest] = address.split("@");
|
|
586
|
-
if (!user || !domain || rest.length > 0 || !publicHttps(`https://${domain}/`)) {
|
|
587
|
+
if (!user || !domain || rest.length > 0 || !NAMES_ONE_ACCOUNT.test(user) || !isNothingButAHost(domain) || !publicHttps(`https://${domain}/`)) {
|
|
587
588
|
throw new WalletRefused("address-unusable", `${address} is not a usable lightning address`);
|
|
588
589
|
}
|
|
589
590
|
return [user, domain];
|
|
590
591
|
}
|
|
592
|
+
function isNothingButAHost(domain) {
|
|
593
|
+
try {
|
|
594
|
+
const parsed = new URL(`https://${domain}/`);
|
|
595
|
+
return parsed.host === domain.toLowerCase() && parsed.pathname === "/" && parsed.search === "" && parsed.hash === "";
|
|
596
|
+
} catch {
|
|
597
|
+
return false;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
591
600
|
async function fetchJson(url, deadline) {
|
|
592
601
|
return (await answeredJson(url, deadline)).said;
|
|
593
602
|
}
|
|
@@ -792,6 +801,30 @@ function refuse(reason) {
|
|
|
792
801
|
return Response.json({ status: "ERROR", reason });
|
|
793
802
|
}
|
|
794
803
|
|
|
804
|
+
// ../core/ed25519.ts
|
|
805
|
+
var PKCS8_HEADER = hexToBytes("302e020100300506032b657004220420");
|
|
806
|
+
|
|
807
|
+
// src/webhook.ts
|
|
808
|
+
var VERIFY_CHALLENGE = "verify-challenge";
|
|
809
|
+
function nonceOf(text2, type) {
|
|
810
|
+
try {
|
|
811
|
+
const said = JSON.parse(text2);
|
|
812
|
+
if (said["type"] !== type || typeof said["nonce"] !== "string") return null;
|
|
813
|
+
return said["nonce"];
|
|
814
|
+
} catch {
|
|
815
|
+
return null;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
function answerVerifyChallenge(body) {
|
|
819
|
+
const nonce = nonceOf(body, VERIFY_CHALLENGE);
|
|
820
|
+
return nonce === null ? null : JSON.stringify({ nonce });
|
|
821
|
+
}
|
|
822
|
+
async function answerVerifyChallengeRequest(request) {
|
|
823
|
+
if (request.method !== "POST") return null;
|
|
824
|
+
const answer = answerVerifyChallenge(await request.clone().text());
|
|
825
|
+
return answer === null ? null : new Response(answer, { headers: { "content-type": "application/json" } });
|
|
826
|
+
}
|
|
827
|
+
|
|
795
828
|
// src/qr.ts
|
|
796
829
|
import { encode } from "uqr";
|
|
797
830
|
function encodeForQr(destination) {
|
|
@@ -811,6 +844,8 @@ function lightningVerifyEndpoint(config) {
|
|
|
811
844
|
"cache-control": `max-age=${config.pollEverySecs ?? DEFAULT_POLL_EVERY_SECS}`
|
|
812
845
|
};
|
|
813
846
|
return async (request) => {
|
|
847
|
+
const consented = await answerVerifyChallengeRequest(request);
|
|
848
|
+
if (consented !== null) return consented;
|
|
814
849
|
const sealed = new URL(request.url).searchParams.get(WALLET);
|
|
815
850
|
if (sealed === null) return Response.json({ settled: false }, { status: 400 });
|
|
816
851
|
const opened = await unseal(config.secret, sealed);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thunder-bridge",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.9",
|
|
4
4
|
"description": "Trustless JavaScript client for the Thunder Bridge Lightning payment gateway. Proves the invoice came from your own wallet before the payer sees it.",
|
|
5
5
|
"author": "i-am-fatik",
|
|
6
6
|
"homepage": "https://agora.gripe/en/tools/thunder-bridge",
|