thunder-bridge 0.8.6 → 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/README.md +49 -0
- package/dist/index.cjs +160 -137
- package/dist/index.d.cts +22 -3
- package/dist/index.d.ts +22 -3
- package/dist/index.js +158 -137
- package/dist/{rail-CdKeyVGn.d.cts → rail-J8QoYbSr.d.cts} +10 -0
- package/dist/{rail-CdKeyVGn.d.ts → rail-J8QoYbSr.d.ts} +10 -0
- package/dist/server.cjs +153 -9
- package/dist/server.d.cts +42 -3
- package/dist/server.d.ts +42 -3
- package/dist/server.js +150 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -112,6 +112,8 @@ them and this table does not repeat them.
|
|
|
112
112
|
| `bankTransfer(params)` | register a Czech QR platba as a watched payment. Refuses a gateway that serves strangers |
|
|
113
113
|
| `bankVerifyEndpoint(config)` | the other half, the LUD-21 shape backed by your own statement |
|
|
114
114
|
| `fioStatement(config)` | a `Statement` reading a Fio account, several tokens used strictly in turn |
|
|
115
|
+
| `lightningVerifyEndpoint(config)` | the same shape for Lightning, asking the wallet on the gateway's behalf. From `thunder-bridge/server` |
|
|
116
|
+
| `relayedVerifyUrl(mount, wallet, secret)` | the URL to hand the gateway instead of the wallet's, with the wallet's sealed inside |
|
|
115
117
|
|
|
116
118
|
What your service answers once those handlers are mounted is written out in
|
|
117
119
|
[`openapi.yaml`](openapi.yaml), shipped with this package.
|
|
@@ -248,6 +250,53 @@ already in the account:
|
|
|
248
250
|
Neither has been seen with Fio, which forwards the message untouched. Check it
|
|
249
251
|
against the banks your payers actually use before you promise them a rail.
|
|
250
252
|
|
|
253
|
+
## Making the gateway poll nobody but you
|
|
254
|
+
|
|
255
|
+
By default a Lightning watch hands the gateway the wallet's own verify URL, so the
|
|
256
|
+
gateway polls `blink.sv` or `coinos.io` directly and its logs, its ledger and its
|
|
257
|
+
peers all carry that domain. If you would rather it never touched a third party and
|
|
258
|
+
never learned which provider your recipient uses, put your own endpoint in between.
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
import { blindLightningRail, lightningVerifyEndpoint } from "thunder-bridge/server";
|
|
262
|
+
|
|
263
|
+
app.get("/verify/lightning", (context) =>
|
|
264
|
+
lightningVerifyEndpoint({ secret: RELAY_SECRET, pollEverySecs: 5 })(context.req.raw),
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
const rail = blindLightningRail({
|
|
268
|
+
gateway,
|
|
269
|
+
lnAddresses: ["you@blink.sv"],
|
|
270
|
+
amountMsat: (order) => order.amountMinor * 40,
|
|
271
|
+
relayVerifyThrough: { endpoint: "https://shop.example/verify/lightning", secret: RELAY_SECRET },
|
|
272
|
+
});
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
The wallet's URL is sealed into the query with your secret, so what the gateway
|
|
276
|
+
stores and replicates is a blob it cannot read. It polls you, you ask the wallet,
|
|
277
|
+
and the preimage still comes from the recipient's own server and still has to hash
|
|
278
|
+
to the payment hash, so standing in the middle buys privacy and pacing without
|
|
279
|
+
making you something anyone has to trust. A wallet you cannot reach answers `502`
|
|
280
|
+
rather than "not settled", because those are different claims.
|
|
281
|
+
|
|
282
|
+
Both rails then run through endpoints of yours, on a pace you set, and the gateway
|
|
283
|
+
is only ever talking to servers that asked to be talked to. It costs you a service
|
|
284
|
+
that has to stay up: a browser-only integration cannot do this, and should keep
|
|
285
|
+
letting the gateway poll the wallet.
|
|
286
|
+
|
|
287
|
+
### How often the gateway asks
|
|
288
|
+
|
|
289
|
+
Your endpoint decides, not the gateway. `bankVerifyEndpoint` answers with
|
|
290
|
+
`Cache-Control: max-age=30`, and the gateway uses that as the interval for every
|
|
291
|
+
payment on your host. Set `pollEverySecs` to whatever your bank's own refresh makes
|
|
292
|
+
sensible: reading a statement that moves once an hour every five seconds only burns
|
|
293
|
+
your rate limit.
|
|
294
|
+
|
|
295
|
+
The gateway also asks the URL once, before it accepts the watch, and refuses with
|
|
296
|
+
`424` if it does not answer this shape. So deploy the endpoint first and register
|
|
297
|
+
second. That is what stops anyone pointing a gateway at a server that never asked to
|
|
298
|
+
be polled for three days.
|
|
299
|
+
|
|
251
300
|
## What is still trusted
|
|
252
301
|
|
|
253
302
|
- **The gateway chooses which of your addresses gets paid.** Nothing here can
|
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,9 +1289,155 @@ 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;
|
|
1440
|
+
var DEFAULT_POLL_EVERY_SECS = 30;
|
|
1293
1441
|
var IBAN = /^[A-Z]{2}[0-9]{2}[0-9A-Z]{8,30}$/;
|
|
1294
1442
|
var FORBIDDEN_IN_SPD = "*";
|
|
1295
1443
|
async function bankTransfer(params) {
|
|
@@ -1316,7 +1464,12 @@ async function bankTransfer(params) {
|
|
|
1316
1464
|
return { id: watched.id, paymentHash: watched.paymentHash, verifyUrl, spd };
|
|
1317
1465
|
}
|
|
1318
1466
|
function bankVerifyEndpoint(config) {
|
|
1467
|
+
const paced = {
|
|
1468
|
+
"cache-control": `max-age=${config.pollEverySecs ?? DEFAULT_POLL_EVERY_SECS}`
|
|
1469
|
+
};
|
|
1319
1470
|
return async (request) => {
|
|
1471
|
+
const consented = await answerVerifyChallengeRequest(request);
|
|
1472
|
+
if (consented !== null) return consented;
|
|
1320
1473
|
const asked = readQuery(new URL(request.url));
|
|
1321
1474
|
if (asked === null) return Response.json({ settled: false }, { status: 400 });
|
|
1322
1475
|
const subject = subjectOf(asked.reference, asked.amountMinor, asked.currency);
|
|
@@ -1326,11 +1479,11 @@ function bankVerifyEndpoint(config) {
|
|
|
1326
1479
|
}
|
|
1327
1480
|
const since = unixNow() - (config.lookBackSecs ?? DEFAULT_LOOK_BACK_SECS);
|
|
1328
1481
|
const landed = (await config.statement(since)).some((credit) => pays(credit, asked));
|
|
1329
|
-
if (!landed) return Response.json({ settled: false });
|
|
1330
|
-
return Response.json(
|
|
1331
|
-
settled: true,
|
|
1332
|
-
|
|
1333
|
-
|
|
1482
|
+
if (!landed) return Response.json({ settled: false }, { headers: paced });
|
|
1483
|
+
return Response.json(
|
|
1484
|
+
{ settled: true, preimage: await hmacHex(config.secret, `preimage|${subject}`) },
|
|
1485
|
+
{ headers: paced }
|
|
1486
|
+
);
|
|
1334
1487
|
};
|
|
1335
1488
|
}
|
|
1336
1489
|
function readQuery(url) {
|
|
@@ -1787,138 +1940,6 @@ function refusals2(answers) {
|
|
|
1787
1940
|
(answer) => answer.reason instanceof Error ? answer.reason.message : String(answer.reason)
|
|
1788
1941
|
).join("; ");
|
|
1789
1942
|
}
|
|
1790
|
-
|
|
1791
|
-
// ../core/ed25519.ts
|
|
1792
|
-
var ALGORITHM = { name: "Ed25519" };
|
|
1793
|
-
var SIGNATURE_BYTES = 64;
|
|
1794
|
-
var PUBLIC_KEY_BYTES = 32;
|
|
1795
|
-
var PKCS8_HEADER = hexToBytes("302e020100300506032b657004220420");
|
|
1796
|
-
async function verifyHex(publicKeyHex, signatureHex, payload) {
|
|
1797
|
-
if (!isHex(publicKeyHex) || publicKeyHex.length !== PUBLIC_KEY_BYTES * 2) return false;
|
|
1798
|
-
if (!isHex(signatureHex) || signatureHex.length !== SIGNATURE_BYTES * 2) return false;
|
|
1799
|
-
try {
|
|
1800
|
-
const key = await crypto.subtle.importKey(
|
|
1801
|
-
"raw",
|
|
1802
|
-
asBuffer2(hexToBytes(publicKeyHex)),
|
|
1803
|
-
ALGORITHM,
|
|
1804
|
-
false,
|
|
1805
|
-
["verify"]
|
|
1806
|
-
);
|
|
1807
|
-
return await crypto.subtle.verify(
|
|
1808
|
-
ALGORITHM,
|
|
1809
|
-
key,
|
|
1810
|
-
asBuffer2(hexToBytes(signatureHex)),
|
|
1811
|
-
asBuffer2(payload)
|
|
1812
|
-
);
|
|
1813
|
-
} catch {
|
|
1814
|
-
return false;
|
|
1815
|
-
}
|
|
1816
|
-
}
|
|
1817
|
-
function asBuffer2(bytes) {
|
|
1818
|
-
return bytes.buffer instanceof ArrayBuffer ? bytes : new Uint8Array(bytes);
|
|
1819
|
-
}
|
|
1820
|
-
|
|
1821
|
-
// src/webhook.ts
|
|
1822
|
-
var SIGNATURE_HEADER = "x-signature";
|
|
1823
|
-
var TIMESTAMP_HEADER = "x-timestamp";
|
|
1824
|
-
var SHARED_SECRET_PREFIX = "sha256=";
|
|
1825
|
-
var GATEWAY_KEY_PREFIX = "ed25519=";
|
|
1826
|
-
var DEFAULT_TOLERANCE_SECS = 300;
|
|
1827
|
-
var CHALLENGE = "webhook-challenge";
|
|
1828
|
-
async function verifyWebhookSignature(body, signature, credential, timestamp, options = {}) {
|
|
1829
|
-
if (!recent(timestamp, options.toleranceSecs ?? DEFAULT_TOLERANCE_SECS)) return false;
|
|
1830
|
-
if (typeof credential !== "string") {
|
|
1831
|
-
if (!signature.startsWith(GATEWAY_KEY_PREFIX)) return false;
|
|
1832
|
-
return verifyHex(
|
|
1833
|
-
credential.publicKey.toLowerCase(),
|
|
1834
|
-
signature.slice(GATEWAY_KEY_PREFIX.length).toLowerCase(),
|
|
1835
|
-
signed(timestamp, body)
|
|
1836
|
-
);
|
|
1837
|
-
}
|
|
1838
|
-
if (signature.startsWith(GATEWAY_KEY_PREFIX)) return false;
|
|
1839
|
-
const received = signature.startsWith(SHARED_SECRET_PREFIX) ? signature.slice(SHARED_SECRET_PREFIX.length) : signature;
|
|
1840
|
-
return equalInConstantTime(await sign(credential, timestamp, body), received.toLowerCase());
|
|
1841
|
-
}
|
|
1842
|
-
async function parseWebhook(body, signature, credential, timestamp, options = {}) {
|
|
1843
|
-
if (!await verifyWebhookSignature(body, signature, credential, timestamp, options)) return null;
|
|
1844
|
-
const text2 = typeof body === "string" ? body : new TextDecoder().decode(body);
|
|
1845
|
-
try {
|
|
1846
|
-
return paymentFromWire(JSON.parse(text2));
|
|
1847
|
-
} catch {
|
|
1848
|
-
return null;
|
|
1849
|
-
}
|
|
1850
|
-
}
|
|
1851
|
-
async function parseWebhookRequest(request, credential, options = {}) {
|
|
1852
|
-
const signature = request.headers.get(SIGNATURE_HEADER);
|
|
1853
|
-
const timestamp = request.headers.get(TIMESTAMP_HEADER);
|
|
1854
|
-
if (signature === null || timestamp === null) return null;
|
|
1855
|
-
return parseWebhook(await request.text(), signature, credential, timestamp, options);
|
|
1856
|
-
}
|
|
1857
|
-
async function parseWatchedWebhook(body, signature, credential, timestamp, options = {}) {
|
|
1858
|
-
if (!await verifyWebhookSignature(body, signature, credential, timestamp, options)) return null;
|
|
1859
|
-
const text2 = typeof body === "string" ? body : new TextDecoder().decode(body);
|
|
1860
|
-
try {
|
|
1861
|
-
return triggerEventFromWire(JSON.parse(text2));
|
|
1862
|
-
} catch {
|
|
1863
|
-
return null;
|
|
1864
|
-
}
|
|
1865
|
-
}
|
|
1866
|
-
async function parseWatchedWebhookRequest(request, credential, options = {}) {
|
|
1867
|
-
const signature = request.headers.get(SIGNATURE_HEADER);
|
|
1868
|
-
const timestamp = request.headers.get(TIMESTAMP_HEADER);
|
|
1869
|
-
if (signature === null || timestamp === null) return null;
|
|
1870
|
-
return parseWatchedWebhook(await request.text(), signature, credential, timestamp, options);
|
|
1871
|
-
}
|
|
1872
|
-
async function answerWebhookChallenge(body, signature, credential, timestamp, options = {}) {
|
|
1873
|
-
if (!await verifyWebhookSignature(body, signature, credential, timestamp, options)) return null;
|
|
1874
|
-
const text2 = typeof body === "string" ? body : new TextDecoder().decode(body);
|
|
1875
|
-
const nonce = challenged(text2);
|
|
1876
|
-
if (nonce === null) return null;
|
|
1877
|
-
if (typeof credential !== "string") return JSON.stringify({ nonce });
|
|
1878
|
-
return JSON.stringify({
|
|
1879
|
-
nonce,
|
|
1880
|
-
signature: `${SHARED_SECRET_PREFIX}${await hmacHex(credential, nonce)}`
|
|
1881
|
-
});
|
|
1882
|
-
}
|
|
1883
|
-
async function answerWebhookChallengeRequest(request, credential, options = {}) {
|
|
1884
|
-
const signature = request.headers.get(SIGNATURE_HEADER);
|
|
1885
|
-
const timestamp = request.headers.get(TIMESTAMP_HEADER);
|
|
1886
|
-
if (signature === null || timestamp === null) return null;
|
|
1887
|
-
const answer = await answerWebhookChallenge(
|
|
1888
|
-
await request.clone().text(),
|
|
1889
|
-
signature,
|
|
1890
|
-
credential,
|
|
1891
|
-
timestamp,
|
|
1892
|
-
options
|
|
1893
|
-
);
|
|
1894
|
-
return answer === null ? null : new Response(answer, { headers: { "content-type": "application/json" } });
|
|
1895
|
-
}
|
|
1896
|
-
function challenged(text2) {
|
|
1897
|
-
try {
|
|
1898
|
-
const said = JSON.parse(text2);
|
|
1899
|
-
if (said["type"] !== CHALLENGE || typeof said["nonce"] !== "string") return null;
|
|
1900
|
-
return said["nonce"];
|
|
1901
|
-
} catch {
|
|
1902
|
-
return null;
|
|
1903
|
-
}
|
|
1904
|
-
}
|
|
1905
|
-
function recent(timestamp, toleranceSecs) {
|
|
1906
|
-
const sent = Number(timestamp);
|
|
1907
|
-
if (!Number.isFinite(sent)) return false;
|
|
1908
|
-
return Math.abs(Math.floor(Date.now() / 1e3) - sent) <= toleranceSecs;
|
|
1909
|
-
}
|
|
1910
|
-
async function sign(secret, timestamp, body) {
|
|
1911
|
-
return hmacHex(secret, signed(timestamp, body));
|
|
1912
|
-
}
|
|
1913
|
-
function signed(timestamp, body) {
|
|
1914
|
-
const encoder = new TextEncoder();
|
|
1915
|
-
const stamp = encoder.encode(`${timestamp}.`);
|
|
1916
|
-
const rest = typeof body === "string" ? encoder.encode(body) : new Uint8Array(body);
|
|
1917
|
-
const payload = new Uint8Array(stamp.length + rest.length);
|
|
1918
|
-
payload.set(stamp);
|
|
1919
|
-
payload.set(rest, stamp.length);
|
|
1920
|
-
return payload;
|
|
1921
|
-
}
|
|
1922
1943
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1923
1944
|
0 && (module.exports = {
|
|
1924
1945
|
GatewayCheatError,
|
|
@@ -1931,6 +1952,8 @@ function signed(timestamp, body) {
|
|
|
1931
1952
|
REQUEST_IN_FLIGHT,
|
|
1932
1953
|
ThunderBridge,
|
|
1933
1954
|
UnverifiedRecipientError,
|
|
1955
|
+
answerVerifyChallenge,
|
|
1956
|
+
answerVerifyChallengeRequest,
|
|
1934
1957
|
answerWebhookChallenge,
|
|
1935
1958
|
answerWebhookChallengeRequest,
|
|
1936
1959
|
bankRail,
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { T as ThunderBridge, P as Payment, C as CreatePaymentParams, a as TriggerEvent, W as WalletFailure } from './rail-
|
|
2
|
-
export { B as BankRailConfig, b as CreateOptions, c as CreateQuoteParams, F as FollowOptions, L as Leg, d as LightningRailConfig, O as Order, e as PaymentKind, f as PaymentStatus, Q as Quote, R as Rail, g as ThunderBridgeOptions, h as WaitOptions, i as WalletReason, j as WatchPaymentParams, k as bankRail, l as lightningRail } from './rail-
|
|
1
|
+
import { T as ThunderBridge, P as Payment, C as CreatePaymentParams, a as TriggerEvent, W as WalletFailure } from './rail-J8QoYbSr.cjs';
|
|
2
|
+
export { B as BankRailConfig, b as CreateOptions, c as CreateQuoteParams, F as FollowOptions, L as Leg, d as LightningRailConfig, O as Order, e as PaymentKind, f as PaymentStatus, Q as Quote, R as Rail, g as ThunderBridgeOptions, h as WaitOptions, i as WalletReason, j as WatchPaymentParams, k as bankRail, l as lightningRail } from './rail-J8QoYbSr.cjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Encrypt what the watcher needs and the gateway must not have. The gateway
|
|
@@ -98,6 +98,13 @@ interface BankVerifyConfig {
|
|
|
98
98
|
statement: Statement;
|
|
99
99
|
/** How far back a credit still counts, seven days by default */
|
|
100
100
|
lookBackSecs?: number;
|
|
101
|
+
/**
|
|
102
|
+
* How often you want the gateway to ask, in seconds. It goes out as
|
|
103
|
+
* `Cache-Control: max-age`, so the pace is yours to set rather than the
|
|
104
|
+
* gateway's, and a bank that updates once a minute should say so instead of
|
|
105
|
+
* being polled every few seconds. Thirty by default, clamped to an hour
|
|
106
|
+
*/
|
|
107
|
+
pollEverySecs?: number;
|
|
101
108
|
}
|
|
102
109
|
/**
|
|
103
110
|
* Ask for a bank transfer and put it under the gateway's watch, so it settles
|
|
@@ -374,6 +381,18 @@ declare function answerWebhookChallenge(body: string | Uint8Array, signature: st
|
|
|
374
381
|
* the same handler can go on to `parseWebhookRequest` when this was no challenge
|
|
375
382
|
*/
|
|
376
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>;
|
|
377
396
|
|
|
378
397
|
/**
|
|
379
398
|
* The way a gateway was caught out, every code is a check that held against the
|
|
@@ -451,4 +470,4 @@ declare class NoWalletAvailableError extends ProblemError {
|
|
|
451
470
|
}, wallets: WalletFailure[]);
|
|
452
471
|
}
|
|
453
472
|
|
|
454
|
-
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
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { T as ThunderBridge, P as Payment, C as CreatePaymentParams, a as TriggerEvent, W as WalletFailure } from './rail-
|
|
2
|
-
export { B as BankRailConfig, b as CreateOptions, c as CreateQuoteParams, F as FollowOptions, L as Leg, d as LightningRailConfig, O as Order, e as PaymentKind, f as PaymentStatus, Q as Quote, R as Rail, g as ThunderBridgeOptions, h as WaitOptions, i as WalletReason, j as WatchPaymentParams, k as bankRail, l as lightningRail } from './rail-
|
|
1
|
+
import { T as ThunderBridge, P as Payment, C as CreatePaymentParams, a as TriggerEvent, W as WalletFailure } from './rail-J8QoYbSr.js';
|
|
2
|
+
export { B as BankRailConfig, b as CreateOptions, c as CreateQuoteParams, F as FollowOptions, L as Leg, d as LightningRailConfig, O as Order, e as PaymentKind, f as PaymentStatus, Q as Quote, R as Rail, g as ThunderBridgeOptions, h as WaitOptions, i as WalletReason, j as WatchPaymentParams, k as bankRail, l as lightningRail } from './rail-J8QoYbSr.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Encrypt what the watcher needs and the gateway must not have. The gateway
|
|
@@ -98,6 +98,13 @@ interface BankVerifyConfig {
|
|
|
98
98
|
statement: Statement;
|
|
99
99
|
/** How far back a credit still counts, seven days by default */
|
|
100
100
|
lookBackSecs?: number;
|
|
101
|
+
/**
|
|
102
|
+
* How often you want the gateway to ask, in seconds. It goes out as
|
|
103
|
+
* `Cache-Control: max-age`, so the pace is yours to set rather than the
|
|
104
|
+
* gateway's, and a bank that updates once a minute should say so instead of
|
|
105
|
+
* being polled every few seconds. Thirty by default, clamped to an hour
|
|
106
|
+
*/
|
|
107
|
+
pollEverySecs?: number;
|
|
101
108
|
}
|
|
102
109
|
/**
|
|
103
110
|
* Ask for a bank transfer and put it under the gateway's watch, so it settles
|
|
@@ -374,6 +381,18 @@ declare function answerWebhookChallenge(body: string | Uint8Array, signature: st
|
|
|
374
381
|
* the same handler can go on to `parseWebhookRequest` when this was no challenge
|
|
375
382
|
*/
|
|
376
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>;
|
|
377
396
|
|
|
378
397
|
/**
|
|
379
398
|
* The way a gateway was caught out, every code is a check that held against the
|
|
@@ -451,4 +470,4 @@ declare class NoWalletAvailableError extends ProblemError {
|
|
|
451
470
|
}, wallets: WalletFailure[]);
|
|
452
471
|
}
|
|
453
472
|
|
|
454
|
-
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 };
|