thunder-bridge 0.8.4 → 0.8.8
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 +105 -7
- package/dist/index.cjs +122 -17
- package/dist/index.d.cts +34 -8
- package/dist/index.d.ts +34 -8
- package/dist/index.js +120 -17
- package/dist/{rail-QZtUN1D-.d.cts → rail-J8QoYbSr.d.cts} +20 -1
- package/dist/{rail-QZtUN1D-.d.ts → rail-J8QoYbSr.d.ts} +20 -1
- package/dist/server.cjs +117 -8
- package/dist/server.d.cts +42 -3
- package/dist/server.d.ts +42 -3
- package/dist/server.js +114 -7
- 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.
|
|
@@ -167,7 +169,8 @@ const tipJar = lnurlToSvg("https://agora.gripe/tip");
|
|
|
167
169
|
|
|
168
170
|
**Webhooks** - [`src/webhook.ts`](src/webhook.ts): `parseWebhookRequest`,
|
|
169
171
|
`parseWebhook`, `parseWatchedWebhookRequest`, `parseWatchedWebhook`,
|
|
170
|
-
`verifyWebhookSignature
|
|
172
|
+
`verifyWebhookSignature`, `answerWebhookChallengeRequest`,
|
|
173
|
+
`answerWebhookChallenge`. See [Webhooks](#webhooks).
|
|
171
174
|
|
|
172
175
|
**Errors** - [`src/errors.ts`](src/errors.ts): `ProblemError`,
|
|
173
176
|
`NoWalletAvailableError`, `GatewayCheatError`, `UnverifiedRecipientError`,
|
|
@@ -247,6 +250,53 @@ already in the account:
|
|
|
247
250
|
Neither has been seen with Fio, which forwards the message untouched. Check it
|
|
248
251
|
against the banks your payers actually use before you promise them a rail.
|
|
249
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
|
+
|
|
250
300
|
## What is still trusted
|
|
251
301
|
|
|
252
302
|
- **The gateway chooses which of your addresses gets paid.** Nothing here can
|
|
@@ -339,17 +389,34 @@ try {
|
|
|
339
389
|
|
|
340
390
|
Pass `webhookUrl` and optionally `webhookSecret` when you create a payment, or on
|
|
341
391
|
any rail. Once it reaches `paid` the gateway POSTs the same JSON the API returns,
|
|
342
|
-
so the body is a `Payment`. Every delivery carries `x-timestamp
|
|
343
|
-
`x-signature
|
|
344
|
-
|
|
345
|
-
|
|
392
|
+
so the body is a `Payment`. Every delivery carries `x-timestamp` and an
|
|
393
|
+
`x-signature` over `<timestamp>.<body>` rather than the body alone, so a captured
|
|
394
|
+
delivery cannot be replayed at you later. With a secret set it is
|
|
395
|
+
`sha256=<hmac>` keyed with that secret, and without one it is `ed25519=<signature>`
|
|
396
|
+
from the gateway's own key, which is the better default and is below. Retries widen
|
|
397
|
+
until the payment itself runs out, never sooner than an hour. An invoice that expires
|
|
398
|
+
fires nothing.
|
|
346
399
|
|
|
347
400
|
Delivery is at-least-once, so deduplicate on `id`.
|
|
348
401
|
|
|
402
|
+
Your handler answers one challenge before any of that. The gateway POSTs
|
|
403
|
+
`{"type":"webhook-challenge","nonce":"..."}` to the URL while the create is still open
|
|
404
|
+
and refuses the payment with a 424 unless the nonce comes back, so the endpoint has to
|
|
405
|
+
be deployed before you register it. `answerWebhookChallengeRequest` verifies that
|
|
406
|
+
challenge and hands you the response to return, or `null` when the delivery was a real
|
|
407
|
+
settlement, and it leaves the body unread either way.
|
|
408
|
+
|
|
349
409
|
```ts
|
|
350
|
-
import {
|
|
410
|
+
import {
|
|
411
|
+
answerWebhookChallengeRequest,
|
|
412
|
+
parseWebhookRequest,
|
|
413
|
+
proveSettlement,
|
|
414
|
+
} from "thunder-bridge";
|
|
351
415
|
|
|
352
416
|
app.post("/hooks/paid", async (context) => {
|
|
417
|
+
const challenge = await answerWebhookChallengeRequest(context.req.raw, secret);
|
|
418
|
+
if (challenge) return challenge;
|
|
419
|
+
|
|
353
420
|
const payment = await parseWebhookRequest(context.req.raw, secret);
|
|
354
421
|
if (payment === null) return context.text("bad signature", 401);
|
|
355
422
|
|
|
@@ -382,7 +449,38 @@ app.post("/hooks/bank", async (context) => {
|
|
|
382
449
|
```
|
|
383
450
|
|
|
384
451
|
Give each rail its own path, as above, and neither endpoint has to guess which body
|
|
385
|
-
it was handed.
|
|
452
|
+
it was handed. Both events also carry `kind`, `"minted"` or `"watched"`, so a single
|
|
453
|
+
path serving a trigger that both rails settle on can branch on the field instead of
|
|
454
|
+
on which fields are missing.
|
|
455
|
+
|
|
456
|
+
### Or hand the gateway no secret at all
|
|
457
|
+
|
|
458
|
+
A secret you give the gateway is kept in its ledger and replicated to its peers,
|
|
459
|
+
because any instance may be the one that delivers. Leave `webhookSecret` out and the
|
|
460
|
+
delivery is signed with the gateway's own key instead, `x-signature:
|
|
461
|
+
ed25519=<signature>` over the same `<timestamp>.<body>`. Fetch the public half once
|
|
462
|
+
and pass it as `{ publicKey }` wherever a secret would go.
|
|
463
|
+
|
|
464
|
+
```ts
|
|
465
|
+
const publicKey = await gateway.webhookKey();
|
|
466
|
+
|
|
467
|
+
app.post("/hooks/paid", async (context) => {
|
|
468
|
+
const challenge = await answerWebhookChallengeRequest(context.req.raw, { publicKey });
|
|
469
|
+
if (challenge) return challenge;
|
|
470
|
+
|
|
471
|
+
const payment = await parseWebhookRequest(context.req.raw, { publicKey });
|
|
472
|
+
if (payment === null) return context.text("bad signature", 401);
|
|
473
|
+
...
|
|
474
|
+
});
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
Answering echoes the nonce and nothing else here, because there is no secret to sign it
|
|
478
|
+
with. Holding the URL the gateway challenged is the whole proof in that case.
|
|
479
|
+
|
|
480
|
+
The key is derived from the gateway's `CLUSTER_KEY`, so every instance in one cluster
|
|
481
|
+
signs alike and an operator rotating that key changes this one too. Neither
|
|
482
|
+
credential is ever accepted for the other's scheme, so a secret cannot check an
|
|
483
|
+
`ed25519=` delivery and a public key cannot check a `sha256=` one.
|
|
386
484
|
|
|
387
485
|
`parseWebhookRequest` refuses anything more than five minutes out of date,
|
|
388
486
|
adjustable with `toleranceSecs`. The signature proves the body came from someone
|
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
|
+
answerWebhookChallenge: () => answerWebhookChallenge,
|
|
34
|
+
answerWebhookChallengeRequest: () => answerWebhookChallengeRequest,
|
|
33
35
|
bankRail: () => bankRail,
|
|
34
36
|
bankTransfer: () => bankTransfer,
|
|
35
37
|
bankVerifyEndpoint: () => bankVerifyEndpoint,
|
|
@@ -632,6 +634,7 @@ function triggerEventFromWire(body) {
|
|
|
632
634
|
if (sealed !== null && typeof sealed !== "string") return null;
|
|
633
635
|
return {
|
|
634
636
|
id,
|
|
637
|
+
kind: kindOf(wire),
|
|
635
638
|
paymentHash,
|
|
636
639
|
verifyUrl,
|
|
637
640
|
status,
|
|
@@ -643,6 +646,11 @@ function triggerEventFromWire(body) {
|
|
|
643
646
|
amountMsat: wire["incoming_amount"] === void 0 ? null : msatFrom(wire["incoming_amount"], 1)
|
|
644
647
|
};
|
|
645
648
|
}
|
|
649
|
+
function kindOf(wire) {
|
|
650
|
+
const said = wire["kind"];
|
|
651
|
+
if (said === "minted" || said === "watched") return said;
|
|
652
|
+
return text(wire["ln_address"]) === null ? "watched" : "minted";
|
|
653
|
+
}
|
|
646
654
|
function toAmount(msat) {
|
|
647
655
|
return { value: String(msat), asset_code: ASSET_CODE, asset_scale: ASSET_SCALE };
|
|
648
656
|
}
|
|
@@ -780,6 +788,23 @@ var ThunderBridge = class {
|
|
|
780
788
|
return quote;
|
|
781
789
|
}
|
|
782
790
|
/** Read a payment back, null when the gateway has never heard of it */
|
|
791
|
+
/**
|
|
792
|
+
* The key this gateway signs webhooks with when you registered none of your own,
|
|
793
|
+
* ready to hand to `parseWebhookRequest` as `{ publicKey }`. Fetch it once and
|
|
794
|
+
* keep it: it is the same for every instance in the cluster
|
|
795
|
+
*/
|
|
796
|
+
async webhookKey() {
|
|
797
|
+
const response = await fetch(`${this.baseUrl}/webhook-key`);
|
|
798
|
+
if (!response.ok) throw await problemFrom(response);
|
|
799
|
+
const body = await response.json().catch(() => null);
|
|
800
|
+
if (body?.algorithm !== "ed25519" || typeof body.public_key !== "string") {
|
|
801
|
+
throw new ProblemError({
|
|
802
|
+
status: response.status,
|
|
803
|
+
title: "The gateway published no ed25519 webhook key"
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
return body.public_key;
|
|
807
|
+
}
|
|
783
808
|
async getPayment(id) {
|
|
784
809
|
const response = await fetch(`${this.baseUrl}/incoming-payments/${encodeURIComponent(id)}`, {
|
|
785
810
|
headers: this.reading()
|
|
@@ -1265,6 +1290,7 @@ function minorScaleOf(currency) {
|
|
|
1265
1290
|
// src/bank.ts
|
|
1266
1291
|
var DEFAULT_CURRENCY = "CZK";
|
|
1267
1292
|
var DEFAULT_LOOK_BACK_SECS = 7 * 24 * 60 * 60;
|
|
1293
|
+
var DEFAULT_POLL_EVERY_SECS = 30;
|
|
1268
1294
|
var IBAN = /^[A-Z]{2}[0-9]{2}[0-9A-Z]{8,30}$/;
|
|
1269
1295
|
var FORBIDDEN_IN_SPD = "*";
|
|
1270
1296
|
async function bankTransfer(params) {
|
|
@@ -1291,6 +1317,9 @@ async function bankTransfer(params) {
|
|
|
1291
1317
|
return { id: watched.id, paymentHash: watched.paymentHash, verifyUrl, spd };
|
|
1292
1318
|
}
|
|
1293
1319
|
function bankVerifyEndpoint(config) {
|
|
1320
|
+
const paced = {
|
|
1321
|
+
"cache-control": `max-age=${config.pollEverySecs ?? DEFAULT_POLL_EVERY_SECS}`
|
|
1322
|
+
};
|
|
1294
1323
|
return async (request) => {
|
|
1295
1324
|
const asked = readQuery(new URL(request.url));
|
|
1296
1325
|
if (asked === null) return Response.json({ settled: false }, { status: 400 });
|
|
@@ -1301,11 +1330,11 @@ function bankVerifyEndpoint(config) {
|
|
|
1301
1330
|
}
|
|
1302
1331
|
const since = unixNow() - (config.lookBackSecs ?? DEFAULT_LOOK_BACK_SECS);
|
|
1303
1332
|
const landed = (await config.statement(since)).some((credit) => pays(credit, asked));
|
|
1304
|
-
if (!landed) return Response.json({ settled: false });
|
|
1305
|
-
return Response.json(
|
|
1306
|
-
settled: true,
|
|
1307
|
-
|
|
1308
|
-
|
|
1333
|
+
if (!landed) return Response.json({ settled: false }, { headers: paced });
|
|
1334
|
+
return Response.json(
|
|
1335
|
+
{ settled: true, preimage: await hmacHex(config.secret, `preimage|${subject}`) },
|
|
1336
|
+
{ headers: paced }
|
|
1337
|
+
);
|
|
1309
1338
|
};
|
|
1310
1339
|
}
|
|
1311
1340
|
function readQuery(url) {
|
|
@@ -1763,18 +1792,59 @@ function refusals2(answers) {
|
|
|
1763
1792
|
).join("; ");
|
|
1764
1793
|
}
|
|
1765
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
|
+
|
|
1766
1825
|
// src/webhook.ts
|
|
1767
1826
|
var SIGNATURE_HEADER = "x-signature";
|
|
1768
1827
|
var TIMESTAMP_HEADER = "x-timestamp";
|
|
1769
|
-
var
|
|
1828
|
+
var SHARED_SECRET_PREFIX = "sha256=";
|
|
1829
|
+
var GATEWAY_KEY_PREFIX = "ed25519=";
|
|
1770
1830
|
var DEFAULT_TOLERANCE_SECS = 300;
|
|
1771
|
-
|
|
1831
|
+
var CHALLENGE = "webhook-challenge";
|
|
1832
|
+
async function verifyWebhookSignature(body, signature, credential, timestamp, options = {}) {
|
|
1772
1833
|
if (!recent(timestamp, options.toleranceSecs ?? DEFAULT_TOLERANCE_SECS)) return false;
|
|
1773
|
-
|
|
1774
|
-
|
|
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());
|
|
1775
1845
|
}
|
|
1776
|
-
async function parseWebhook(body, signature,
|
|
1777
|
-
if (!await verifyWebhookSignature(body, signature,
|
|
1846
|
+
async function parseWebhook(body, signature, credential, timestamp, options = {}) {
|
|
1847
|
+
if (!await verifyWebhookSignature(body, signature, credential, timestamp, options)) return null;
|
|
1778
1848
|
const text2 = typeof body === "string" ? body : new TextDecoder().decode(body);
|
|
1779
1849
|
try {
|
|
1780
1850
|
return paymentFromWire(JSON.parse(text2));
|
|
@@ -1782,14 +1852,14 @@ async function parseWebhook(body, signature, secret, timestamp, options = {}) {
|
|
|
1782
1852
|
return null;
|
|
1783
1853
|
}
|
|
1784
1854
|
}
|
|
1785
|
-
async function parseWebhookRequest(request,
|
|
1855
|
+
async function parseWebhookRequest(request, credential, options = {}) {
|
|
1786
1856
|
const signature = request.headers.get(SIGNATURE_HEADER);
|
|
1787
1857
|
const timestamp = request.headers.get(TIMESTAMP_HEADER);
|
|
1788
1858
|
if (signature === null || timestamp === null) return null;
|
|
1789
|
-
return parseWebhook(await request.text(), signature,
|
|
1859
|
+
return parseWebhook(await request.text(), signature, credential, timestamp, options);
|
|
1790
1860
|
}
|
|
1791
|
-
async function parseWatchedWebhook(body, signature,
|
|
1792
|
-
if (!await verifyWebhookSignature(body, signature,
|
|
1861
|
+
async function parseWatchedWebhook(body, signature, credential, timestamp, options = {}) {
|
|
1862
|
+
if (!await verifyWebhookSignature(body, signature, credential, timestamp, options)) return null;
|
|
1793
1863
|
const text2 = typeof body === "string" ? body : new TextDecoder().decode(body);
|
|
1794
1864
|
try {
|
|
1795
1865
|
return triggerEventFromWire(JSON.parse(text2));
|
|
@@ -1797,11 +1867,44 @@ async function parseWatchedWebhook(body, signature, secret, timestamp, options =
|
|
|
1797
1867
|
return null;
|
|
1798
1868
|
}
|
|
1799
1869
|
}
|
|
1800
|
-
async function parseWatchedWebhookRequest(request,
|
|
1870
|
+
async function parseWatchedWebhookRequest(request, credential, options = {}) {
|
|
1801
1871
|
const signature = request.headers.get(SIGNATURE_HEADER);
|
|
1802
1872
|
const timestamp = request.headers.get(TIMESTAMP_HEADER);
|
|
1803
1873
|
if (signature === null || timestamp === null) return null;
|
|
1804
|
-
return parseWatchedWebhook(await request.text(), signature,
|
|
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
|
+
}
|
|
1805
1908
|
}
|
|
1806
1909
|
function recent(timestamp, toleranceSecs) {
|
|
1807
1910
|
const sent = Number(timestamp);
|
|
@@ -1832,6 +1935,8 @@ function signed(timestamp, body) {
|
|
|
1832
1935
|
REQUEST_IN_FLIGHT,
|
|
1833
1936
|
ThunderBridge,
|
|
1834
1937
|
UnverifiedRecipientError,
|
|
1938
|
+
answerWebhookChallenge,
|
|
1939
|
+
answerWebhookChallengeRequest,
|
|
1835
1940
|
bankRail,
|
|
1836
1941
|
bankTransfer,
|
|
1837
1942
|
bankVerifyEndpoint,
|
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 PaymentStatus, Q as Quote, R as 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
|
|
@@ -338,23 +345,42 @@ declare function toLnurl(endpoint: string): string;
|
|
|
338
345
|
type WebhookOptions = {
|
|
339
346
|
toleranceSecs?: number;
|
|
340
347
|
};
|
|
348
|
+
/**
|
|
349
|
+
* What checks a delivery. A string is the `webhook.secret` you registered. Pass
|
|
350
|
+
* `{ publicKey }` instead, the hex from the gateway's `/webhook-key`, when you
|
|
351
|
+
* registered no secret and would rather it held nothing of yours
|
|
352
|
+
*/
|
|
353
|
+
type WebhookCredential = string | {
|
|
354
|
+
publicKey: string;
|
|
355
|
+
};
|
|
341
356
|
/** Verify the `X-Signature` header against the raw body and the `X-Timestamp` that came with it */
|
|
342
|
-
declare function verifyWebhookSignature(body: string | Uint8Array, signature: string,
|
|
357
|
+
declare function verifyWebhookSignature(body: string | Uint8Array, signature: string, credential: WebhookCredential, timestamp: string, options?: WebhookOptions): Promise<boolean>;
|
|
343
358
|
/** Verify and parse in one step, returns null on a bad signature or a body that is not a payment */
|
|
344
|
-
declare function parseWebhook(body: string | Uint8Array, signature: string,
|
|
359
|
+
declare function parseWebhook(body: string | Uint8Array, signature: string, credential: WebhookCredential, timestamp: string, options?: WebhookOptions): Promise<Payment | null>;
|
|
345
360
|
/**
|
|
346
361
|
* Verify and parse from a Fetch API `Request` as used by Hono, Next, SvelteKit,
|
|
347
362
|
* Cloudflare Workers and Deno, WebCrypto only so it runs anywhere fetch does
|
|
348
363
|
*/
|
|
349
|
-
declare function parseWebhookRequest(request: Request,
|
|
364
|
+
declare function parseWebhookRequest(request: Request, credential: WebhookCredential, options?: WebhookOptions): Promise<Payment | null>;
|
|
350
365
|
/**
|
|
351
366
|
* The same, for a payment the gateway only watched. A bank transfer and a blind
|
|
352
367
|
* Lightning leg carry no address, amount or invoice, so they arrive in the shape
|
|
353
368
|
* `followTrigger` and `getWatched` hand back rather than the minted one
|
|
354
369
|
*/
|
|
355
|
-
declare function parseWatchedWebhook(body: string | Uint8Array, signature: string,
|
|
370
|
+
declare function parseWatchedWebhook(body: string | Uint8Array, signature: string, credential: WebhookCredential, timestamp: string, options?: WebhookOptions): Promise<TriggerEvent | null>;
|
|
356
371
|
/** `parseWatchedWebhook` from a Fetch API `Request`, the way `parseWebhookRequest` is */
|
|
357
|
-
declare function parseWatchedWebhookRequest(request: Request,
|
|
372
|
+
declare function parseWatchedWebhookRequest(request: Request, credential: WebhookCredential, options?: WebhookOptions): Promise<TriggerEvent | null>;
|
|
373
|
+
/**
|
|
374
|
+
* Answer the one challenge the gateway sends before it will watch a payment your
|
|
375
|
+
* webhook is registered on. Returns the body to send back with a 200, or null when
|
|
376
|
+
* this delivery is not a challenge, so a handler tries this first and then parses
|
|
377
|
+
*/
|
|
378
|
+
declare function answerWebhookChallenge(body: string | Uint8Array, signature: string, credential: WebhookCredential, timestamp: string, options?: WebhookOptions): Promise<string | null>;
|
|
379
|
+
/**
|
|
380
|
+
* `answerWebhookChallenge` from a Fetch API `Request`, leaving the body unread so
|
|
381
|
+
* the same handler can go on to `parseWebhookRequest` when this was no challenge
|
|
382
|
+
*/
|
|
383
|
+
declare function answerWebhookChallengeRequest(request: Request, credential: WebhookCredential, options?: WebhookOptions): Promise<Response | null>;
|
|
358
384
|
|
|
359
385
|
/**
|
|
360
386
|
* The way a gateway was caught out, every code is a check that held against the
|
|
@@ -432,4 +458,4 @@ declare class NoWalletAvailableError extends ProblemError {
|
|
|
432
458
|
}, wallets: WalletFailure[]);
|
|
433
459
|
}
|
|
434
460
|
|
|
435
|
-
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 WebhookOptions, 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 };
|
|
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 };
|
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 PaymentStatus, Q as Quote, R as 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
|
|
@@ -338,23 +345,42 @@ declare function toLnurl(endpoint: string): string;
|
|
|
338
345
|
type WebhookOptions = {
|
|
339
346
|
toleranceSecs?: number;
|
|
340
347
|
};
|
|
348
|
+
/**
|
|
349
|
+
* What checks a delivery. A string is the `webhook.secret` you registered. Pass
|
|
350
|
+
* `{ publicKey }` instead, the hex from the gateway's `/webhook-key`, when you
|
|
351
|
+
* registered no secret and would rather it held nothing of yours
|
|
352
|
+
*/
|
|
353
|
+
type WebhookCredential = string | {
|
|
354
|
+
publicKey: string;
|
|
355
|
+
};
|
|
341
356
|
/** Verify the `X-Signature` header against the raw body and the `X-Timestamp` that came with it */
|
|
342
|
-
declare function verifyWebhookSignature(body: string | Uint8Array, signature: string,
|
|
357
|
+
declare function verifyWebhookSignature(body: string | Uint8Array, signature: string, credential: WebhookCredential, timestamp: string, options?: WebhookOptions): Promise<boolean>;
|
|
343
358
|
/** Verify and parse in one step, returns null on a bad signature or a body that is not a payment */
|
|
344
|
-
declare function parseWebhook(body: string | Uint8Array, signature: string,
|
|
359
|
+
declare function parseWebhook(body: string | Uint8Array, signature: string, credential: WebhookCredential, timestamp: string, options?: WebhookOptions): Promise<Payment | null>;
|
|
345
360
|
/**
|
|
346
361
|
* Verify and parse from a Fetch API `Request` as used by Hono, Next, SvelteKit,
|
|
347
362
|
* Cloudflare Workers and Deno, WebCrypto only so it runs anywhere fetch does
|
|
348
363
|
*/
|
|
349
|
-
declare function parseWebhookRequest(request: Request,
|
|
364
|
+
declare function parseWebhookRequest(request: Request, credential: WebhookCredential, options?: WebhookOptions): Promise<Payment | null>;
|
|
350
365
|
/**
|
|
351
366
|
* The same, for a payment the gateway only watched. A bank transfer and a blind
|
|
352
367
|
* Lightning leg carry no address, amount or invoice, so they arrive in the shape
|
|
353
368
|
* `followTrigger` and `getWatched` hand back rather than the minted one
|
|
354
369
|
*/
|
|
355
|
-
declare function parseWatchedWebhook(body: string | Uint8Array, signature: string,
|
|
370
|
+
declare function parseWatchedWebhook(body: string | Uint8Array, signature: string, credential: WebhookCredential, timestamp: string, options?: WebhookOptions): Promise<TriggerEvent | null>;
|
|
356
371
|
/** `parseWatchedWebhook` from a Fetch API `Request`, the way `parseWebhookRequest` is */
|
|
357
|
-
declare function parseWatchedWebhookRequest(request: Request,
|
|
372
|
+
declare function parseWatchedWebhookRequest(request: Request, credential: WebhookCredential, options?: WebhookOptions): Promise<TriggerEvent | null>;
|
|
373
|
+
/**
|
|
374
|
+
* Answer the one challenge the gateway sends before it will watch a payment your
|
|
375
|
+
* webhook is registered on. Returns the body to send back with a 200, or null when
|
|
376
|
+
* this delivery is not a challenge, so a handler tries this first and then parses
|
|
377
|
+
*/
|
|
378
|
+
declare function answerWebhookChallenge(body: string | Uint8Array, signature: string, credential: WebhookCredential, timestamp: string, options?: WebhookOptions): Promise<string | null>;
|
|
379
|
+
/**
|
|
380
|
+
* `answerWebhookChallenge` from a Fetch API `Request`, leaving the body unread so
|
|
381
|
+
* the same handler can go on to `parseWebhookRequest` when this was no challenge
|
|
382
|
+
*/
|
|
383
|
+
declare function answerWebhookChallengeRequest(request: Request, credential: WebhookCredential, options?: WebhookOptions): Promise<Response | null>;
|
|
358
384
|
|
|
359
385
|
/**
|
|
360
386
|
* The way a gateway was caught out, every code is a check that held against the
|
|
@@ -432,4 +458,4 @@ declare class NoWalletAvailableError extends ProblemError {
|
|
|
432
458
|
}, wallets: WalletFailure[]);
|
|
433
459
|
}
|
|
434
460
|
|
|
435
|
-
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 WebhookOptions, 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 };
|
|
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 };
|