thunder-bridge 0.8.3 → 0.8.6
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 +102 -8
- package/dist/index.cjs +135 -10
- package/dist/index.d.cts +40 -6
- package/dist/index.d.ts +40 -6
- package/dist/index.js +131 -10
- package/dist/{rail-Cyc5zTtA.d.cts → rail-CdKeyVGn.d.cts} +16 -1
- package/dist/{rail-Cyc5zTtA.d.ts → rail-CdKeyVGn.d.ts} +16 -1
- package/dist/server.cjs +3 -1
- package/dist/server.d.cts +2 -2
- package/dist/server.d.ts +2 -2
- package/dist/server.js +3 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -166,7 +166,9 @@ const tipJar = lnurlToSvg("https://agora.gripe/tip");
|
|
|
166
166
|
```
|
|
167
167
|
|
|
168
168
|
**Webhooks** - [`src/webhook.ts`](src/webhook.ts): `parseWebhookRequest`,
|
|
169
|
-
`parseWebhook`, `
|
|
169
|
+
`parseWebhook`, `parseWatchedWebhookRequest`, `parseWatchedWebhook`,
|
|
170
|
+
`verifyWebhookSignature`, `answerWebhookChallengeRequest`,
|
|
171
|
+
`answerWebhookChallenge`. See [Webhooks](#webhooks).
|
|
170
172
|
|
|
171
173
|
**Errors** - [`src/errors.ts`](src/errors.ts): `ProblemError`,
|
|
172
174
|
`NoWalletAvailableError`, `GatewayCheatError`, `UnverifiedRecipientError`,
|
|
@@ -225,6 +227,27 @@ such as `nas`, the trailing-dot `localhost.`, and anything whose last label is
|
|
|
225
227
|
|
|
226
228
|
It vets the first hop only. See below.
|
|
227
229
|
|
|
230
|
+
### Which transfer counts as paying
|
|
231
|
+
|
|
232
|
+
`bankVerifyEndpoint` calls a credit a settlement when the amount and the currency
|
|
233
|
+
match exactly and the reference appears anywhere in what the payer wrote,
|
|
234
|
+
case-insensitively. With `fioStatement` "what the payer wrote" is four Fio columns
|
|
235
|
+
joined: the variable symbol, the user identification, the message for the recipient
|
|
236
|
+
and the payer's own reference. So a bank that prefixes, appends, or moves the text
|
|
237
|
+
between those fields still settles.
|
|
238
|
+
|
|
239
|
+
Two shapes do not settle, and both leave the payment `pending` while the money is
|
|
240
|
+
already in the account:
|
|
241
|
+
|
|
242
|
+
- **A shortened reference.** The match asks whether the reference is inside what the
|
|
243
|
+
bank forwarded, not the other way round, so a bank that truncates it never matches.
|
|
244
|
+
- **A payer whose bank forwards nothing but a numeric variable symbol.** The
|
|
245
|
+
reference is alphanumeric and cannot travel in a numeric field, and the match does
|
|
246
|
+
not read `X-VS` as an alternative.
|
|
247
|
+
|
|
248
|
+
Neither has been seen with Fio, which forwards the message untouched. Check it
|
|
249
|
+
against the banks your payers actually use before you promise them a rail.
|
|
250
|
+
|
|
228
251
|
## What is still trusted
|
|
229
252
|
|
|
230
253
|
- **The gateway chooses which of your addresses gets paid.** Nothing here can
|
|
@@ -315,19 +338,36 @@ try {
|
|
|
315
338
|
|
|
316
339
|
## Webhooks
|
|
317
340
|
|
|
318
|
-
Pass `webhookUrl` and optionally `webhookSecret` when you create a payment
|
|
319
|
-
it reaches `paid` the gateway POSTs the same JSON the API returns,
|
|
320
|
-
a `Payment`. Every delivery carries `x-timestamp
|
|
321
|
-
`x-signature
|
|
322
|
-
|
|
323
|
-
|
|
341
|
+
Pass `webhookUrl` and optionally `webhookSecret` when you create a payment, or on
|
|
342
|
+
any rail. Once it reaches `paid` the gateway POSTs the same JSON the API returns,
|
|
343
|
+
so the body is a `Payment`. Every delivery carries `x-timestamp` and an
|
|
344
|
+
`x-signature` over `<timestamp>.<body>` rather than the body alone, so a captured
|
|
345
|
+
delivery cannot be replayed at you later. With a secret set it is
|
|
346
|
+
`sha256=<hmac>` keyed with that secret, and without one it is `ed25519=<signature>`
|
|
347
|
+
from the gateway's own key, which is the better default and is below. Retries widen
|
|
348
|
+
until the payment itself runs out, never sooner than an hour. An invoice that expires
|
|
349
|
+
fires nothing.
|
|
324
350
|
|
|
325
351
|
Delivery is at-least-once, so deduplicate on `id`.
|
|
326
352
|
|
|
353
|
+
Your handler answers one challenge before any of that. The gateway POSTs
|
|
354
|
+
`{"type":"webhook-challenge","nonce":"..."}` to the URL while the create is still open
|
|
355
|
+
and refuses the payment with a 424 unless the nonce comes back, so the endpoint has to
|
|
356
|
+
be deployed before you register it. `answerWebhookChallengeRequest` verifies that
|
|
357
|
+
challenge and hands you the response to return, or `null` when the delivery was a real
|
|
358
|
+
settlement, and it leaves the body unread either way.
|
|
359
|
+
|
|
327
360
|
```ts
|
|
328
|
-
import {
|
|
361
|
+
import {
|
|
362
|
+
answerWebhookChallengeRequest,
|
|
363
|
+
parseWebhookRequest,
|
|
364
|
+
proveSettlement,
|
|
365
|
+
} from "thunder-bridge";
|
|
329
366
|
|
|
330
367
|
app.post("/hooks/paid", async (context) => {
|
|
368
|
+
const challenge = await answerWebhookChallengeRequest(context.req.raw, secret);
|
|
369
|
+
if (challenge) return challenge;
|
|
370
|
+
|
|
331
371
|
const payment = await parseWebhookRequest(context.req.raw, secret);
|
|
332
372
|
if (payment === null) return context.text("bad signature", 401);
|
|
333
373
|
|
|
@@ -339,6 +379,60 @@ app.post("/hooks/paid", async (context) => {
|
|
|
339
379
|
});
|
|
340
380
|
```
|
|
341
381
|
|
|
382
|
+
### A watched payment sends a different body
|
|
383
|
+
|
|
384
|
+
`bankRail` and `blindLightningRail` register a payment the gateway was told almost
|
|
385
|
+
nothing about, so its webhook carries no address, no amount and no invoice. That is
|
|
386
|
+
not a `Payment`, and `parseWebhook` answers `null` for it, which looks exactly like a
|
|
387
|
+
bad signature. Use `parseWatchedWebhookRequest` there instead and you get a
|
|
388
|
+
`TriggerEvent`, the shape `getWatched` hands back.
|
|
389
|
+
|
|
390
|
+
```ts
|
|
391
|
+
import { parseWatchedWebhookRequest } from "thunder-bridge";
|
|
392
|
+
|
|
393
|
+
app.post("/hooks/bank", async (context) => {
|
|
394
|
+
const settled = await parseWatchedWebhookRequest(context.req.raw, secret);
|
|
395
|
+
if (settled === null) return context.text("bad signature", 401);
|
|
396
|
+
|
|
397
|
+
await fulfil(settled.id, settled.preimage);
|
|
398
|
+
return context.text("ok");
|
|
399
|
+
});
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
Give each rail its own path, as above, and neither endpoint has to guess which body
|
|
403
|
+
it was handed. Both events also carry `kind`, `"minted"` or `"watched"`, so a single
|
|
404
|
+
path serving a trigger that both rails settle on can branch on the field instead of
|
|
405
|
+
on which fields are missing.
|
|
406
|
+
|
|
407
|
+
### Or hand the gateway no secret at all
|
|
408
|
+
|
|
409
|
+
A secret you give the gateway is kept in its ledger and replicated to its peers,
|
|
410
|
+
because any instance may be the one that delivers. Leave `webhookSecret` out and the
|
|
411
|
+
delivery is signed with the gateway's own key instead, `x-signature:
|
|
412
|
+
ed25519=<signature>` over the same `<timestamp>.<body>`. Fetch the public half once
|
|
413
|
+
and pass it as `{ publicKey }` wherever a secret would go.
|
|
414
|
+
|
|
415
|
+
```ts
|
|
416
|
+
const publicKey = await gateway.webhookKey();
|
|
417
|
+
|
|
418
|
+
app.post("/hooks/paid", async (context) => {
|
|
419
|
+
const challenge = await answerWebhookChallengeRequest(context.req.raw, { publicKey });
|
|
420
|
+
if (challenge) return challenge;
|
|
421
|
+
|
|
422
|
+
const payment = await parseWebhookRequest(context.req.raw, { publicKey });
|
|
423
|
+
if (payment === null) return context.text("bad signature", 401);
|
|
424
|
+
...
|
|
425
|
+
});
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
Answering echoes the nonce and nothing else here, because there is no secret to sign it
|
|
429
|
+
with. Holding the URL the gateway challenged is the whole proof in that case.
|
|
430
|
+
|
|
431
|
+
The key is derived from the gateway's `CLUSTER_KEY`, so every instance in one cluster
|
|
432
|
+
signs alike and an operator rotating that key changes this one too. Neither
|
|
433
|
+
credential is ever accepted for the other's scheme, so a secret cannot check an
|
|
434
|
+
`ed25519=` delivery and a public key cannot check a `sha256=` one.
|
|
435
|
+
|
|
342
436
|
`parseWebhookRequest` refuses anything more than five minutes out of date,
|
|
343
437
|
adjustable with `toleranceSecs`. The signature proves the body came from someone
|
|
344
438
|
holding your secret. It does not prove the payment happened, since the gateway
|
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,
|
|
@@ -50,6 +52,8 @@ __export(index_exports, {
|
|
|
50
52
|
minorScaleOf: () => minorScaleOf,
|
|
51
53
|
minorUnitsOf: () => minorUnitsOf,
|
|
52
54
|
msatFor: () => msatFor,
|
|
55
|
+
parseWatchedWebhook: () => parseWatchedWebhook,
|
|
56
|
+
parseWatchedWebhookRequest: () => parseWatchedWebhookRequest,
|
|
53
57
|
parseWebhook: () => parseWebhook,
|
|
54
58
|
parseWebhookRequest: () => parseWebhookRequest,
|
|
55
59
|
preimageMatchesHash: () => preimageMatchesHash,
|
|
@@ -609,7 +613,8 @@ function watchRequestBody(params, trigger) {
|
|
|
609
613
|
verify_url: params.verifyUrl,
|
|
610
614
|
expires_at: expiresAt.toISOString(),
|
|
611
615
|
trigger: trigger ?? void 0,
|
|
612
|
-
sealed: params.sealed
|
|
616
|
+
sealed: params.sealed,
|
|
617
|
+
webhook: params.webhookUrl ? { url: params.webhookUrl, secret: params.webhookSecret } : void 0
|
|
613
618
|
});
|
|
614
619
|
}
|
|
615
620
|
function triggerEventFromWire(body) {
|
|
@@ -629,6 +634,7 @@ function triggerEventFromWire(body) {
|
|
|
629
634
|
if (sealed !== null && typeof sealed !== "string") return null;
|
|
630
635
|
return {
|
|
631
636
|
id,
|
|
637
|
+
kind: kindOf(wire),
|
|
632
638
|
paymentHash,
|
|
633
639
|
verifyUrl,
|
|
634
640
|
status,
|
|
@@ -640,6 +646,11 @@ function triggerEventFromWire(body) {
|
|
|
640
646
|
amountMsat: wire["incoming_amount"] === void 0 ? null : msatFrom(wire["incoming_amount"], 1)
|
|
641
647
|
};
|
|
642
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
|
+
}
|
|
643
654
|
function toAmount(msat) {
|
|
644
655
|
return { value: String(msat), asset_code: ASSET_CODE, asset_scale: ASSET_SCALE };
|
|
645
656
|
}
|
|
@@ -777,6 +788,23 @@ var ThunderBridge = class {
|
|
|
777
788
|
return quote;
|
|
778
789
|
}
|
|
779
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
|
+
}
|
|
780
808
|
async getPayment(id) {
|
|
781
809
|
const response = await fetch(`${this.baseUrl}/incoming-payments/${encodeURIComponent(id)}`, {
|
|
782
810
|
headers: this.reading()
|
|
@@ -1281,7 +1309,9 @@ async function bankTransfer(params) {
|
|
|
1281
1309
|
verifyUrl,
|
|
1282
1310
|
expiresAt: params.expiresAt,
|
|
1283
1311
|
trigger: params.trigger,
|
|
1284
|
-
sealed: params.sealed
|
|
1312
|
+
sealed: params.sealed,
|
|
1313
|
+
webhookUrl: params.webhookUrl,
|
|
1314
|
+
webhookSecret: params.webhookSecret
|
|
1285
1315
|
});
|
|
1286
1316
|
return { id: watched.id, paymentHash: watched.paymentHash, verifyUrl, spd };
|
|
1287
1317
|
}
|
|
@@ -1602,6 +1632,8 @@ function bankRail(config) {
|
|
|
1602
1632
|
trigger: config.trigger,
|
|
1603
1633
|
sealed: await config.sealed?.(order),
|
|
1604
1634
|
variableSymbol: config.variableSymbol?.(order),
|
|
1635
|
+
webhookUrl: config.webhookUrl,
|
|
1636
|
+
webhookSecret: config.webhookSecret,
|
|
1605
1637
|
allowPublicGateway: config.allowPublicGateway
|
|
1606
1638
|
});
|
|
1607
1639
|
return {
|
|
@@ -1756,18 +1788,59 @@ function refusals2(answers) {
|
|
|
1756
1788
|
).join("; ");
|
|
1757
1789
|
}
|
|
1758
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
|
+
|
|
1759
1821
|
// src/webhook.ts
|
|
1760
1822
|
var SIGNATURE_HEADER = "x-signature";
|
|
1761
1823
|
var TIMESTAMP_HEADER = "x-timestamp";
|
|
1762
|
-
var
|
|
1824
|
+
var SHARED_SECRET_PREFIX = "sha256=";
|
|
1825
|
+
var GATEWAY_KEY_PREFIX = "ed25519=";
|
|
1763
1826
|
var DEFAULT_TOLERANCE_SECS = 300;
|
|
1764
|
-
|
|
1827
|
+
var CHALLENGE = "webhook-challenge";
|
|
1828
|
+
async function verifyWebhookSignature(body, signature, credential, timestamp, options = {}) {
|
|
1765
1829
|
if (!recent(timestamp, options.toleranceSecs ?? DEFAULT_TOLERANCE_SECS)) return false;
|
|
1766
|
-
|
|
1767
|
-
|
|
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());
|
|
1768
1841
|
}
|
|
1769
|
-
async function parseWebhook(body, signature,
|
|
1770
|
-
if (!await verifyWebhookSignature(body, signature,
|
|
1842
|
+
async function parseWebhook(body, signature, credential, timestamp, options = {}) {
|
|
1843
|
+
if (!await verifyWebhookSignature(body, signature, credential, timestamp, options)) return null;
|
|
1771
1844
|
const text2 = typeof body === "string" ? body : new TextDecoder().decode(body);
|
|
1772
1845
|
try {
|
|
1773
1846
|
return paymentFromWire(JSON.parse(text2));
|
|
@@ -1775,11 +1848,59 @@ async function parseWebhook(body, signature, secret, timestamp, options = {}) {
|
|
|
1775
1848
|
return null;
|
|
1776
1849
|
}
|
|
1777
1850
|
}
|
|
1778
|
-
async function parseWebhookRequest(request,
|
|
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 = {}) {
|
|
1779
1867
|
const signature = request.headers.get(SIGNATURE_HEADER);
|
|
1780
1868
|
const timestamp = request.headers.get(TIMESTAMP_HEADER);
|
|
1781
1869
|
if (signature === null || timestamp === null) return null;
|
|
1782
|
-
return
|
|
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
|
+
}
|
|
1783
1904
|
}
|
|
1784
1905
|
function recent(timestamp, toleranceSecs) {
|
|
1785
1906
|
const sent = Number(timestamp);
|
|
@@ -1810,6 +1931,8 @@ function signed(timestamp, body) {
|
|
|
1810
1931
|
REQUEST_IN_FLIGHT,
|
|
1811
1932
|
ThunderBridge,
|
|
1812
1933
|
UnverifiedRecipientError,
|
|
1934
|
+
answerWebhookChallenge,
|
|
1935
|
+
answerWebhookChallengeRequest,
|
|
1813
1936
|
bankRail,
|
|
1814
1937
|
bankTransfer,
|
|
1815
1938
|
bankVerifyEndpoint,
|
|
@@ -1830,6 +1953,8 @@ function signed(timestamp, body) {
|
|
|
1830
1953
|
minorScaleOf,
|
|
1831
1954
|
minorUnitsOf,
|
|
1832
1955
|
msatFor,
|
|
1956
|
+
parseWatchedWebhook,
|
|
1957
|
+
parseWatchedWebhookRequest,
|
|
1833
1958
|
parseWebhook,
|
|
1834
1959
|
parseWebhookRequest,
|
|
1835
1960
|
preimageMatchesHash,
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { T as ThunderBridge, P as Payment, C as CreatePaymentParams, W as WalletFailure } from './rail-
|
|
2
|
-
export { B as BankRailConfig,
|
|
1
|
+
import { T as ThunderBridge, P as Payment, C as CreatePaymentParams, a as TriggerEvent, W as WalletFailure } from './rail-CdKeyVGn.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-CdKeyVGn.cjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Encrypt what the watcher needs and the gateway must not have. The gateway
|
|
@@ -66,6 +66,13 @@ interface BankTransferParams {
|
|
|
66
66
|
* without asking anyone. `seal` it and the gateway cannot read it either
|
|
67
67
|
*/
|
|
68
68
|
sealed?: string;
|
|
69
|
+
/**
|
|
70
|
+
* Where the gateway posts once the money lands, a public https URL. Without one
|
|
71
|
+
* a transfer is only ever learned by following the trigger or asking
|
|
72
|
+
*/
|
|
73
|
+
webhookUrl?: string;
|
|
74
|
+
/** Signs that delivery, so `verifyWebhookSignature` can tell it came from the gateway */
|
|
75
|
+
webhookSecret?: string;
|
|
69
76
|
/**
|
|
70
77
|
* Register on a gateway you do not own anyway. The verify URL names the amount
|
|
71
78
|
* and the reference, so its operator ends up reading your order book, and the
|
|
@@ -331,15 +338,42 @@ declare function toLnurl(endpoint: string): string;
|
|
|
331
338
|
type WebhookOptions = {
|
|
332
339
|
toleranceSecs?: number;
|
|
333
340
|
};
|
|
341
|
+
/**
|
|
342
|
+
* What checks a delivery. A string is the `webhook.secret` you registered. Pass
|
|
343
|
+
* `{ publicKey }` instead, the hex from the gateway's `/webhook-key`, when you
|
|
344
|
+
* registered no secret and would rather it held nothing of yours
|
|
345
|
+
*/
|
|
346
|
+
type WebhookCredential = string | {
|
|
347
|
+
publicKey: string;
|
|
348
|
+
};
|
|
334
349
|
/** Verify the `X-Signature` header against the raw body and the `X-Timestamp` that came with it */
|
|
335
|
-
declare function verifyWebhookSignature(body: string | Uint8Array, signature: string,
|
|
350
|
+
declare function verifyWebhookSignature(body: string | Uint8Array, signature: string, credential: WebhookCredential, timestamp: string, options?: WebhookOptions): Promise<boolean>;
|
|
336
351
|
/** Verify and parse in one step, returns null on a bad signature or a body that is not a payment */
|
|
337
|
-
declare function parseWebhook(body: string | Uint8Array, signature: string,
|
|
352
|
+
declare function parseWebhook(body: string | Uint8Array, signature: string, credential: WebhookCredential, timestamp: string, options?: WebhookOptions): Promise<Payment | null>;
|
|
338
353
|
/**
|
|
339
354
|
* Verify and parse from a Fetch API `Request` as used by Hono, Next, SvelteKit,
|
|
340
355
|
* Cloudflare Workers and Deno, WebCrypto only so it runs anywhere fetch does
|
|
341
356
|
*/
|
|
342
|
-
declare function parseWebhookRequest(request: Request,
|
|
357
|
+
declare function parseWebhookRequest(request: Request, credential: WebhookCredential, options?: WebhookOptions): Promise<Payment | null>;
|
|
358
|
+
/**
|
|
359
|
+
* The same, for a payment the gateway only watched. A bank transfer and a blind
|
|
360
|
+
* Lightning leg carry no address, amount or invoice, so they arrive in the shape
|
|
361
|
+
* `followTrigger` and `getWatched` hand back rather than the minted one
|
|
362
|
+
*/
|
|
363
|
+
declare function parseWatchedWebhook(body: string | Uint8Array, signature: string, credential: WebhookCredential, timestamp: string, options?: WebhookOptions): Promise<TriggerEvent | null>;
|
|
364
|
+
/** `parseWatchedWebhook` from a Fetch API `Request`, the way `parseWebhookRequest` is */
|
|
365
|
+
declare function parseWatchedWebhookRequest(request: Request, credential: WebhookCredential, options?: WebhookOptions): Promise<TriggerEvent | null>;
|
|
366
|
+
/**
|
|
367
|
+
* Answer the one challenge the gateway sends before it will watch a payment your
|
|
368
|
+
* webhook is registered on. Returns the body to send back with a 200, or null when
|
|
369
|
+
* this delivery is not a challenge, so a handler tries this first and then parses
|
|
370
|
+
*/
|
|
371
|
+
declare function answerWebhookChallenge(body: string | Uint8Array, signature: string, credential: WebhookCredential, timestamp: string, options?: WebhookOptions): Promise<string | null>;
|
|
372
|
+
/**
|
|
373
|
+
* `answerWebhookChallenge` from a Fetch API `Request`, leaving the body unread so
|
|
374
|
+
* the same handler can go on to `parseWebhookRequest` when this was no challenge
|
|
375
|
+
*/
|
|
376
|
+
declare function answerWebhookChallengeRequest(request: Request, credential: WebhookCredential, options?: WebhookOptions): Promise<Response | null>;
|
|
343
377
|
|
|
344
378
|
/**
|
|
345
379
|
* The way a gateway was caught out, every code is a check that held against the
|
|
@@ -417,4 +451,4 @@ declare class NoWalletAvailableError extends ProblemError {
|
|
|
417
451
|
}, wallets: WalletFailure[]);
|
|
418
452
|
}
|
|
419
453
|
|
|
420
|
-
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, UnverifiedRecipientError, WalletFailure, type WebhookOptions, bankTransfer, bankVerifyEndpoint, bitstamp, coinbase, coinmate, decodeInvoice, fioStatement, invoiceToDataUrl, invoiceToSvg, isProblemType, isProvablyPaid, kraken, lnurlToDataUrl, lnurlToSvg, medianOf, minorScaleOf, minorUnitsOf, msatFor, parseWebhook, parseWebhookRequest, preimageMatchesHash, proveOrigin, proveSettlement, qrToDataUrl, qrToSvg, seal, spdToDataUrl, spdToSvg, toLnurl, unseal, verifyWebhookSignature };
|
|
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 };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { T as ThunderBridge, P as Payment, C as CreatePaymentParams, W as WalletFailure } from './rail-
|
|
2
|
-
export { B as BankRailConfig,
|
|
1
|
+
import { T as ThunderBridge, P as Payment, C as CreatePaymentParams, a as TriggerEvent, W as WalletFailure } from './rail-CdKeyVGn.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-CdKeyVGn.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Encrypt what the watcher needs and the gateway must not have. The gateway
|
|
@@ -66,6 +66,13 @@ interface BankTransferParams {
|
|
|
66
66
|
* without asking anyone. `seal` it and the gateway cannot read it either
|
|
67
67
|
*/
|
|
68
68
|
sealed?: string;
|
|
69
|
+
/**
|
|
70
|
+
* Where the gateway posts once the money lands, a public https URL. Without one
|
|
71
|
+
* a transfer is only ever learned by following the trigger or asking
|
|
72
|
+
*/
|
|
73
|
+
webhookUrl?: string;
|
|
74
|
+
/** Signs that delivery, so `verifyWebhookSignature` can tell it came from the gateway */
|
|
75
|
+
webhookSecret?: string;
|
|
69
76
|
/**
|
|
70
77
|
* Register on a gateway you do not own anyway. The verify URL names the amount
|
|
71
78
|
* and the reference, so its operator ends up reading your order book, and the
|
|
@@ -331,15 +338,42 @@ declare function toLnurl(endpoint: string): string;
|
|
|
331
338
|
type WebhookOptions = {
|
|
332
339
|
toleranceSecs?: number;
|
|
333
340
|
};
|
|
341
|
+
/**
|
|
342
|
+
* What checks a delivery. A string is the `webhook.secret` you registered. Pass
|
|
343
|
+
* `{ publicKey }` instead, the hex from the gateway's `/webhook-key`, when you
|
|
344
|
+
* registered no secret and would rather it held nothing of yours
|
|
345
|
+
*/
|
|
346
|
+
type WebhookCredential = string | {
|
|
347
|
+
publicKey: string;
|
|
348
|
+
};
|
|
334
349
|
/** Verify the `X-Signature` header against the raw body and the `X-Timestamp` that came with it */
|
|
335
|
-
declare function verifyWebhookSignature(body: string | Uint8Array, signature: string,
|
|
350
|
+
declare function verifyWebhookSignature(body: string | Uint8Array, signature: string, credential: WebhookCredential, timestamp: string, options?: WebhookOptions): Promise<boolean>;
|
|
336
351
|
/** Verify and parse in one step, returns null on a bad signature or a body that is not a payment */
|
|
337
|
-
declare function parseWebhook(body: string | Uint8Array, signature: string,
|
|
352
|
+
declare function parseWebhook(body: string | Uint8Array, signature: string, credential: WebhookCredential, timestamp: string, options?: WebhookOptions): Promise<Payment | null>;
|
|
338
353
|
/**
|
|
339
354
|
* Verify and parse from a Fetch API `Request` as used by Hono, Next, SvelteKit,
|
|
340
355
|
* Cloudflare Workers and Deno, WebCrypto only so it runs anywhere fetch does
|
|
341
356
|
*/
|
|
342
|
-
declare function parseWebhookRequest(request: Request,
|
|
357
|
+
declare function parseWebhookRequest(request: Request, credential: WebhookCredential, options?: WebhookOptions): Promise<Payment | null>;
|
|
358
|
+
/**
|
|
359
|
+
* The same, for a payment the gateway only watched. A bank transfer and a blind
|
|
360
|
+
* Lightning leg carry no address, amount or invoice, so they arrive in the shape
|
|
361
|
+
* `followTrigger` and `getWatched` hand back rather than the minted one
|
|
362
|
+
*/
|
|
363
|
+
declare function parseWatchedWebhook(body: string | Uint8Array, signature: string, credential: WebhookCredential, timestamp: string, options?: WebhookOptions): Promise<TriggerEvent | null>;
|
|
364
|
+
/** `parseWatchedWebhook` from a Fetch API `Request`, the way `parseWebhookRequest` is */
|
|
365
|
+
declare function parseWatchedWebhookRequest(request: Request, credential: WebhookCredential, options?: WebhookOptions): Promise<TriggerEvent | null>;
|
|
366
|
+
/**
|
|
367
|
+
* Answer the one challenge the gateway sends before it will watch a payment your
|
|
368
|
+
* webhook is registered on. Returns the body to send back with a 200, or null when
|
|
369
|
+
* this delivery is not a challenge, so a handler tries this first and then parses
|
|
370
|
+
*/
|
|
371
|
+
declare function answerWebhookChallenge(body: string | Uint8Array, signature: string, credential: WebhookCredential, timestamp: string, options?: WebhookOptions): Promise<string | null>;
|
|
372
|
+
/**
|
|
373
|
+
* `answerWebhookChallenge` from a Fetch API `Request`, leaving the body unread so
|
|
374
|
+
* the same handler can go on to `parseWebhookRequest` when this was no challenge
|
|
375
|
+
*/
|
|
376
|
+
declare function answerWebhookChallengeRequest(request: Request, credential: WebhookCredential, options?: WebhookOptions): Promise<Response | null>;
|
|
343
377
|
|
|
344
378
|
/**
|
|
345
379
|
* The way a gateway was caught out, every code is a check that held against the
|
|
@@ -417,4 +451,4 @@ declare class NoWalletAvailableError extends ProblemError {
|
|
|
417
451
|
}, wallets: WalletFailure[]);
|
|
418
452
|
}
|
|
419
453
|
|
|
420
|
-
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, UnverifiedRecipientError, WalletFailure, type WebhookOptions, bankTransfer, bankVerifyEndpoint, bitstamp, coinbase, coinmate, decodeInvoice, fioStatement, invoiceToDataUrl, invoiceToSvg, isProblemType, isProvablyPaid, kraken, lnurlToDataUrl, lnurlToSvg, medianOf, minorScaleOf, minorUnitsOf, msatFor, parseWebhook, parseWebhookRequest, preimageMatchesHash, proveOrigin, proveSettlement, qrToDataUrl, qrToSvg, seal, spdToDataUrl, spdToSvg, toLnurl, unseal, verifyWebhookSignature };
|
|
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 };
|
package/dist/index.js
CHANGED
|
@@ -541,7 +541,8 @@ function watchRequestBody(params, trigger) {
|
|
|
541
541
|
verify_url: params.verifyUrl,
|
|
542
542
|
expires_at: expiresAt.toISOString(),
|
|
543
543
|
trigger: trigger ?? void 0,
|
|
544
|
-
sealed: params.sealed
|
|
544
|
+
sealed: params.sealed,
|
|
545
|
+
webhook: params.webhookUrl ? { url: params.webhookUrl, secret: params.webhookSecret } : void 0
|
|
545
546
|
});
|
|
546
547
|
}
|
|
547
548
|
function triggerEventFromWire(body) {
|
|
@@ -561,6 +562,7 @@ function triggerEventFromWire(body) {
|
|
|
561
562
|
if (sealed !== null && typeof sealed !== "string") return null;
|
|
562
563
|
return {
|
|
563
564
|
id,
|
|
565
|
+
kind: kindOf(wire),
|
|
564
566
|
paymentHash,
|
|
565
567
|
verifyUrl,
|
|
566
568
|
status,
|
|
@@ -572,6 +574,11 @@ function triggerEventFromWire(body) {
|
|
|
572
574
|
amountMsat: wire["incoming_amount"] === void 0 ? null : msatFrom(wire["incoming_amount"], 1)
|
|
573
575
|
};
|
|
574
576
|
}
|
|
577
|
+
function kindOf(wire) {
|
|
578
|
+
const said = wire["kind"];
|
|
579
|
+
if (said === "minted" || said === "watched") return said;
|
|
580
|
+
return text(wire["ln_address"]) === null ? "watched" : "minted";
|
|
581
|
+
}
|
|
575
582
|
function toAmount(msat) {
|
|
576
583
|
return { value: String(msat), asset_code: ASSET_CODE, asset_scale: ASSET_SCALE };
|
|
577
584
|
}
|
|
@@ -709,6 +716,23 @@ var ThunderBridge = class {
|
|
|
709
716
|
return quote;
|
|
710
717
|
}
|
|
711
718
|
/** Read a payment back, null when the gateway has never heard of it */
|
|
719
|
+
/**
|
|
720
|
+
* The key this gateway signs webhooks with when you registered none of your own,
|
|
721
|
+
* ready to hand to `parseWebhookRequest` as `{ publicKey }`. Fetch it once and
|
|
722
|
+
* keep it: it is the same for every instance in the cluster
|
|
723
|
+
*/
|
|
724
|
+
async webhookKey() {
|
|
725
|
+
const response = await fetch(`${this.baseUrl}/webhook-key`);
|
|
726
|
+
if (!response.ok) throw await problemFrom(response);
|
|
727
|
+
const body = await response.json().catch(() => null);
|
|
728
|
+
if (body?.algorithm !== "ed25519" || typeof body.public_key !== "string") {
|
|
729
|
+
throw new ProblemError({
|
|
730
|
+
status: response.status,
|
|
731
|
+
title: "The gateway published no ed25519 webhook key"
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
return body.public_key;
|
|
735
|
+
}
|
|
712
736
|
async getPayment(id) {
|
|
713
737
|
const response = await fetch(`${this.baseUrl}/incoming-payments/${encodeURIComponent(id)}`, {
|
|
714
738
|
headers: this.reading()
|
|
@@ -1213,7 +1237,9 @@ async function bankTransfer(params) {
|
|
|
1213
1237
|
verifyUrl,
|
|
1214
1238
|
expiresAt: params.expiresAt,
|
|
1215
1239
|
trigger: params.trigger,
|
|
1216
|
-
sealed: params.sealed
|
|
1240
|
+
sealed: params.sealed,
|
|
1241
|
+
webhookUrl: params.webhookUrl,
|
|
1242
|
+
webhookSecret: params.webhookSecret
|
|
1217
1243
|
});
|
|
1218
1244
|
return { id: watched.id, paymentHash: watched.paymentHash, verifyUrl, spd };
|
|
1219
1245
|
}
|
|
@@ -1534,6 +1560,8 @@ function bankRail(config) {
|
|
|
1534
1560
|
trigger: config.trigger,
|
|
1535
1561
|
sealed: await config.sealed?.(order),
|
|
1536
1562
|
variableSymbol: config.variableSymbol?.(order),
|
|
1563
|
+
webhookUrl: config.webhookUrl,
|
|
1564
|
+
webhookSecret: config.webhookSecret,
|
|
1537
1565
|
allowPublicGateway: config.allowPublicGateway
|
|
1538
1566
|
});
|
|
1539
1567
|
return {
|
|
@@ -1688,18 +1716,59 @@ function refusals2(answers) {
|
|
|
1688
1716
|
).join("; ");
|
|
1689
1717
|
}
|
|
1690
1718
|
|
|
1719
|
+
// ../core/ed25519.ts
|
|
1720
|
+
var ALGORITHM = { name: "Ed25519" };
|
|
1721
|
+
var SIGNATURE_BYTES = 64;
|
|
1722
|
+
var PUBLIC_KEY_BYTES = 32;
|
|
1723
|
+
var PKCS8_HEADER = hexToBytes("302e020100300506032b657004220420");
|
|
1724
|
+
async function verifyHex(publicKeyHex, signatureHex, payload) {
|
|
1725
|
+
if (!isHex(publicKeyHex) || publicKeyHex.length !== PUBLIC_KEY_BYTES * 2) return false;
|
|
1726
|
+
if (!isHex(signatureHex) || signatureHex.length !== SIGNATURE_BYTES * 2) return false;
|
|
1727
|
+
try {
|
|
1728
|
+
const key = await crypto.subtle.importKey(
|
|
1729
|
+
"raw",
|
|
1730
|
+
asBuffer2(hexToBytes(publicKeyHex)),
|
|
1731
|
+
ALGORITHM,
|
|
1732
|
+
false,
|
|
1733
|
+
["verify"]
|
|
1734
|
+
);
|
|
1735
|
+
return await crypto.subtle.verify(
|
|
1736
|
+
ALGORITHM,
|
|
1737
|
+
key,
|
|
1738
|
+
asBuffer2(hexToBytes(signatureHex)),
|
|
1739
|
+
asBuffer2(payload)
|
|
1740
|
+
);
|
|
1741
|
+
} catch {
|
|
1742
|
+
return false;
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
function asBuffer2(bytes) {
|
|
1746
|
+
return bytes.buffer instanceof ArrayBuffer ? bytes : new Uint8Array(bytes);
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1691
1749
|
// src/webhook.ts
|
|
1692
1750
|
var SIGNATURE_HEADER = "x-signature";
|
|
1693
1751
|
var TIMESTAMP_HEADER = "x-timestamp";
|
|
1694
|
-
var
|
|
1752
|
+
var SHARED_SECRET_PREFIX = "sha256=";
|
|
1753
|
+
var GATEWAY_KEY_PREFIX = "ed25519=";
|
|
1695
1754
|
var DEFAULT_TOLERANCE_SECS = 300;
|
|
1696
|
-
|
|
1755
|
+
var CHALLENGE = "webhook-challenge";
|
|
1756
|
+
async function verifyWebhookSignature(body, signature, credential, timestamp, options = {}) {
|
|
1697
1757
|
if (!recent(timestamp, options.toleranceSecs ?? DEFAULT_TOLERANCE_SECS)) return false;
|
|
1698
|
-
|
|
1699
|
-
|
|
1758
|
+
if (typeof credential !== "string") {
|
|
1759
|
+
if (!signature.startsWith(GATEWAY_KEY_PREFIX)) return false;
|
|
1760
|
+
return verifyHex(
|
|
1761
|
+
credential.publicKey.toLowerCase(),
|
|
1762
|
+
signature.slice(GATEWAY_KEY_PREFIX.length).toLowerCase(),
|
|
1763
|
+
signed(timestamp, body)
|
|
1764
|
+
);
|
|
1765
|
+
}
|
|
1766
|
+
if (signature.startsWith(GATEWAY_KEY_PREFIX)) return false;
|
|
1767
|
+
const received = signature.startsWith(SHARED_SECRET_PREFIX) ? signature.slice(SHARED_SECRET_PREFIX.length) : signature;
|
|
1768
|
+
return equalInConstantTime(await sign(credential, timestamp, body), received.toLowerCase());
|
|
1700
1769
|
}
|
|
1701
|
-
async function parseWebhook(body, signature,
|
|
1702
|
-
if (!await verifyWebhookSignature(body, signature,
|
|
1770
|
+
async function parseWebhook(body, signature, credential, timestamp, options = {}) {
|
|
1771
|
+
if (!await verifyWebhookSignature(body, signature, credential, timestamp, options)) return null;
|
|
1703
1772
|
const text2 = typeof body === "string" ? body : new TextDecoder().decode(body);
|
|
1704
1773
|
try {
|
|
1705
1774
|
return paymentFromWire(JSON.parse(text2));
|
|
@@ -1707,11 +1776,59 @@ async function parseWebhook(body, signature, secret, timestamp, options = {}) {
|
|
|
1707
1776
|
return null;
|
|
1708
1777
|
}
|
|
1709
1778
|
}
|
|
1710
|
-
async function parseWebhookRequest(request,
|
|
1779
|
+
async function parseWebhookRequest(request, credential, options = {}) {
|
|
1780
|
+
const signature = request.headers.get(SIGNATURE_HEADER);
|
|
1781
|
+
const timestamp = request.headers.get(TIMESTAMP_HEADER);
|
|
1782
|
+
if (signature === null || timestamp === null) return null;
|
|
1783
|
+
return parseWebhook(await request.text(), signature, credential, timestamp, options);
|
|
1784
|
+
}
|
|
1785
|
+
async function parseWatchedWebhook(body, signature, credential, timestamp, options = {}) {
|
|
1786
|
+
if (!await verifyWebhookSignature(body, signature, credential, timestamp, options)) return null;
|
|
1787
|
+
const text2 = typeof body === "string" ? body : new TextDecoder().decode(body);
|
|
1788
|
+
try {
|
|
1789
|
+
return triggerEventFromWire(JSON.parse(text2));
|
|
1790
|
+
} catch {
|
|
1791
|
+
return null;
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
async function parseWatchedWebhookRequest(request, credential, options = {}) {
|
|
1711
1795
|
const signature = request.headers.get(SIGNATURE_HEADER);
|
|
1712
1796
|
const timestamp = request.headers.get(TIMESTAMP_HEADER);
|
|
1713
1797
|
if (signature === null || timestamp === null) return null;
|
|
1714
|
-
return
|
|
1798
|
+
return parseWatchedWebhook(await request.text(), signature, credential, timestamp, options);
|
|
1799
|
+
}
|
|
1800
|
+
async function answerWebhookChallenge(body, signature, credential, timestamp, options = {}) {
|
|
1801
|
+
if (!await verifyWebhookSignature(body, signature, credential, timestamp, options)) return null;
|
|
1802
|
+
const text2 = typeof body === "string" ? body : new TextDecoder().decode(body);
|
|
1803
|
+
const nonce = challenged(text2);
|
|
1804
|
+
if (nonce === null) return null;
|
|
1805
|
+
if (typeof credential !== "string") return JSON.stringify({ nonce });
|
|
1806
|
+
return JSON.stringify({
|
|
1807
|
+
nonce,
|
|
1808
|
+
signature: `${SHARED_SECRET_PREFIX}${await hmacHex(credential, nonce)}`
|
|
1809
|
+
});
|
|
1810
|
+
}
|
|
1811
|
+
async function answerWebhookChallengeRequest(request, credential, options = {}) {
|
|
1812
|
+
const signature = request.headers.get(SIGNATURE_HEADER);
|
|
1813
|
+
const timestamp = request.headers.get(TIMESTAMP_HEADER);
|
|
1814
|
+
if (signature === null || timestamp === null) return null;
|
|
1815
|
+
const answer = await answerWebhookChallenge(
|
|
1816
|
+
await request.clone().text(),
|
|
1817
|
+
signature,
|
|
1818
|
+
credential,
|
|
1819
|
+
timestamp,
|
|
1820
|
+
options
|
|
1821
|
+
);
|
|
1822
|
+
return answer === null ? null : new Response(answer, { headers: { "content-type": "application/json" } });
|
|
1823
|
+
}
|
|
1824
|
+
function challenged(text2) {
|
|
1825
|
+
try {
|
|
1826
|
+
const said = JSON.parse(text2);
|
|
1827
|
+
if (said["type"] !== CHALLENGE || typeof said["nonce"] !== "string") return null;
|
|
1828
|
+
return said["nonce"];
|
|
1829
|
+
} catch {
|
|
1830
|
+
return null;
|
|
1831
|
+
}
|
|
1715
1832
|
}
|
|
1716
1833
|
function recent(timestamp, toleranceSecs) {
|
|
1717
1834
|
const sent = Number(timestamp);
|
|
@@ -1741,6 +1858,8 @@ export {
|
|
|
1741
1858
|
REQUEST_IN_FLIGHT,
|
|
1742
1859
|
ThunderBridge,
|
|
1743
1860
|
UnverifiedRecipientError,
|
|
1861
|
+
answerWebhookChallenge,
|
|
1862
|
+
answerWebhookChallengeRequest,
|
|
1744
1863
|
bankRail,
|
|
1745
1864
|
bankTransfer,
|
|
1746
1865
|
bankVerifyEndpoint,
|
|
@@ -1761,6 +1880,8 @@ export {
|
|
|
1761
1880
|
minorScaleOf,
|
|
1762
1881
|
minorUnitsOf,
|
|
1763
1882
|
msatFor,
|
|
1883
|
+
parseWatchedWebhook,
|
|
1884
|
+
parseWatchedWebhookRequest,
|
|
1764
1885
|
parseWebhook,
|
|
1765
1886
|
parseWebhookRequest,
|
|
1766
1887
|
preimageMatchesHash,
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** Where a payment stands, `paid` is the only status that carries a preimage */
|
|
2
2
|
type PaymentStatus = "pending" | "paid" | "expired";
|
|
3
|
+
/** Whether the gateway resolved the address and got the invoice, or was handed one to watch */
|
|
4
|
+
type PaymentKind = "minted" | "watched";
|
|
3
5
|
/** A payment as the gateway reports it, every field is checkable against the recipient */
|
|
4
6
|
interface Payment {
|
|
5
7
|
id: string;
|
|
@@ -53,6 +55,7 @@ interface Quote {
|
|
|
53
55
|
*/
|
|
54
56
|
interface TriggerEvent {
|
|
55
57
|
id: string;
|
|
58
|
+
kind: PaymentKind;
|
|
56
59
|
paymentHash: string;
|
|
57
60
|
verifyUrl: string;
|
|
58
61
|
status: PaymentStatus;
|
|
@@ -74,6 +77,8 @@ interface WatchPaymentParams {
|
|
|
74
77
|
expiresAt: number;
|
|
75
78
|
trigger?: string;
|
|
76
79
|
sealed?: string;
|
|
80
|
+
webhookUrl?: string;
|
|
81
|
+
webhookSecret?: string;
|
|
77
82
|
}
|
|
78
83
|
/** Why one wallet in the list could not be used */
|
|
79
84
|
type WalletReason = "address-unusable" | "unreachable" | "amount-not-accepted" | "cannot-prove-delivery" | "invoice-refused";
|
|
@@ -187,6 +192,12 @@ declare class ThunderBridge {
|
|
|
187
192
|
*/
|
|
188
193
|
createQuote(params: CreateQuoteParams): Promise<Quote>;
|
|
189
194
|
/** Read a payment back, null when the gateway has never heard of it */
|
|
195
|
+
/**
|
|
196
|
+
* The key this gateway signs webhooks with when you registered none of your own,
|
|
197
|
+
* ready to hand to `parseWebhookRequest` as `{ publicKey }`. Fetch it once and
|
|
198
|
+
* keep it: it is the same for every instance in the cluster
|
|
199
|
+
*/
|
|
200
|
+
webhookKey(): Promise<string>;
|
|
190
201
|
getPayment(id: string): Promise<Payment | null>;
|
|
191
202
|
/**
|
|
192
203
|
* Read back a payment the gateway is only watching, null when it has never
|
|
@@ -312,6 +323,8 @@ interface BankRailConfig {
|
|
|
312
323
|
sealed?: (order: Order) => string | Promise<string>;
|
|
313
324
|
/** Up to ten digits, for accounting systems that still want one */
|
|
314
325
|
variableSymbol?: (order: Order) => string | undefined;
|
|
326
|
+
webhookUrl?: string;
|
|
327
|
+
webhookSecret?: string;
|
|
315
328
|
/** Register on a gateway you do not own anyway, on the terms `bankTransfer` sets out */
|
|
316
329
|
allowPublicGateway?: boolean;
|
|
317
330
|
/** What `Leg.rail` reads, for a shop running more than one account */
|
|
@@ -347,6 +360,8 @@ interface BlindLightningRailConfig {
|
|
|
347
360
|
trigger?: string;
|
|
348
361
|
/** Only a watched leg has anywhere to carry this */
|
|
349
362
|
sealed?: (order: Order) => string | Promise<string>;
|
|
363
|
+
webhookUrl?: string;
|
|
364
|
+
webhookSecret?: string;
|
|
350
365
|
/** What `Leg.rail` reads, for a shop running more than one wallet */
|
|
351
366
|
name?: string;
|
|
352
367
|
}
|
|
@@ -372,4 +387,4 @@ declare function lightningRail(config: LightningRailConfig): Rail;
|
|
|
372
387
|
*/
|
|
373
388
|
declare function blindLightningRail(config: BlindLightningRailConfig): Rail;
|
|
374
389
|
|
|
375
|
-
export { type BankRailConfig as B, type CreatePaymentParams as C, type FollowOptions as F, type Leg as L, type Order as O, type Payment as P, type Quote as Q, type Rail as R, ThunderBridge as T, type WalletFailure as W, type
|
|
390
|
+
export { type BankRailConfig as B, type CreatePaymentParams as C, type FollowOptions as F, type Leg as L, type Order as O, type Payment as P, type Quote as Q, type Rail as R, ThunderBridge as T, type WalletFailure as W, type TriggerEvent as a, type CreateOptions as b, type CreateQuoteParams as c, type LightningRailConfig as d, type PaymentKind as e, type PaymentStatus as f, type ThunderBridgeOptions as g, type WaitOptions as h, type WalletReason as i, type WatchPaymentParams as j, bankRail as k, lightningRail as l, type BlindLightningRailConfig as m, blindLightningRail as n };
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** Where a payment stands, `paid` is the only status that carries a preimage */
|
|
2
2
|
type PaymentStatus = "pending" | "paid" | "expired";
|
|
3
|
+
/** Whether the gateway resolved the address and got the invoice, or was handed one to watch */
|
|
4
|
+
type PaymentKind = "minted" | "watched";
|
|
3
5
|
/** A payment as the gateway reports it, every field is checkable against the recipient */
|
|
4
6
|
interface Payment {
|
|
5
7
|
id: string;
|
|
@@ -53,6 +55,7 @@ interface Quote {
|
|
|
53
55
|
*/
|
|
54
56
|
interface TriggerEvent {
|
|
55
57
|
id: string;
|
|
58
|
+
kind: PaymentKind;
|
|
56
59
|
paymentHash: string;
|
|
57
60
|
verifyUrl: string;
|
|
58
61
|
status: PaymentStatus;
|
|
@@ -74,6 +77,8 @@ interface WatchPaymentParams {
|
|
|
74
77
|
expiresAt: number;
|
|
75
78
|
trigger?: string;
|
|
76
79
|
sealed?: string;
|
|
80
|
+
webhookUrl?: string;
|
|
81
|
+
webhookSecret?: string;
|
|
77
82
|
}
|
|
78
83
|
/** Why one wallet in the list could not be used */
|
|
79
84
|
type WalletReason = "address-unusable" | "unreachable" | "amount-not-accepted" | "cannot-prove-delivery" | "invoice-refused";
|
|
@@ -187,6 +192,12 @@ declare class ThunderBridge {
|
|
|
187
192
|
*/
|
|
188
193
|
createQuote(params: CreateQuoteParams): Promise<Quote>;
|
|
189
194
|
/** Read a payment back, null when the gateway has never heard of it */
|
|
195
|
+
/**
|
|
196
|
+
* The key this gateway signs webhooks with when you registered none of your own,
|
|
197
|
+
* ready to hand to `parseWebhookRequest` as `{ publicKey }`. Fetch it once and
|
|
198
|
+
* keep it: it is the same for every instance in the cluster
|
|
199
|
+
*/
|
|
200
|
+
webhookKey(): Promise<string>;
|
|
190
201
|
getPayment(id: string): Promise<Payment | null>;
|
|
191
202
|
/**
|
|
192
203
|
* Read back a payment the gateway is only watching, null when it has never
|
|
@@ -312,6 +323,8 @@ interface BankRailConfig {
|
|
|
312
323
|
sealed?: (order: Order) => string | Promise<string>;
|
|
313
324
|
/** Up to ten digits, for accounting systems that still want one */
|
|
314
325
|
variableSymbol?: (order: Order) => string | undefined;
|
|
326
|
+
webhookUrl?: string;
|
|
327
|
+
webhookSecret?: string;
|
|
315
328
|
/** Register on a gateway you do not own anyway, on the terms `bankTransfer` sets out */
|
|
316
329
|
allowPublicGateway?: boolean;
|
|
317
330
|
/** What `Leg.rail` reads, for a shop running more than one account */
|
|
@@ -347,6 +360,8 @@ interface BlindLightningRailConfig {
|
|
|
347
360
|
trigger?: string;
|
|
348
361
|
/** Only a watched leg has anywhere to carry this */
|
|
349
362
|
sealed?: (order: Order) => string | Promise<string>;
|
|
363
|
+
webhookUrl?: string;
|
|
364
|
+
webhookSecret?: string;
|
|
350
365
|
/** What `Leg.rail` reads, for a shop running more than one wallet */
|
|
351
366
|
name?: string;
|
|
352
367
|
}
|
|
@@ -372,4 +387,4 @@ declare function lightningRail(config: LightningRailConfig): Rail;
|
|
|
372
387
|
*/
|
|
373
388
|
declare function blindLightningRail(config: BlindLightningRailConfig): Rail;
|
|
374
389
|
|
|
375
|
-
export { type BankRailConfig as B, type CreatePaymentParams as C, type FollowOptions as F, type Leg as L, type Order as O, type Payment as P, type Quote as Q, type Rail as R, ThunderBridge as T, type WalletFailure as W, type
|
|
390
|
+
export { type BankRailConfig as B, type CreatePaymentParams as C, type FollowOptions as F, type Leg as L, type Order as O, type Payment as P, type Quote as Q, type Rail as R, ThunderBridge as T, type WalletFailure as W, type TriggerEvent as a, type CreateOptions as b, type CreateQuoteParams as c, type LightningRailConfig as d, type PaymentKind as e, type PaymentStatus as f, type ThunderBridgeOptions as g, type WaitOptions as h, type WalletReason as i, type WatchPaymentParams as j, bankRail as k, lightningRail as l, type BlindLightningRailConfig as m, blindLightningRail as n };
|
package/dist/server.cjs
CHANGED
|
@@ -777,7 +777,9 @@ function blindLightningRail(config) {
|
|
|
777
777
|
verifyUrl: resolved.verifyUrl,
|
|
778
778
|
expiresAt: resolved.expiresAt,
|
|
779
779
|
trigger: config.trigger,
|
|
780
|
-
sealed: await config.sealed?.(order)
|
|
780
|
+
sealed: await config.sealed?.(order),
|
|
781
|
+
webhookUrl: config.webhookUrl,
|
|
782
|
+
webhookSecret: config.webhookSecret
|
|
781
783
|
});
|
|
782
784
|
return {
|
|
783
785
|
id: watched.id,
|
package/dist/server.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { T as ThunderBridge } from './rail-
|
|
2
|
-
export {
|
|
1
|
+
import { T as ThunderBridge } from './rail-CdKeyVGn.cjs';
|
|
2
|
+
export { m as BlindLightningRailConfig, n as blindLightningRail } from './rail-CdKeyVGn.cjs';
|
|
3
3
|
|
|
4
4
|
interface TriggerConfig {
|
|
5
5
|
/** The gateway that quotes the addresses and mints the invoice */
|
package/dist/server.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { T as ThunderBridge } from './rail-
|
|
2
|
-
export {
|
|
1
|
+
import { T as ThunderBridge } from './rail-CdKeyVGn.js';
|
|
2
|
+
export { m as BlindLightningRailConfig, n as blindLightningRail } from './rail-CdKeyVGn.js';
|
|
3
3
|
|
|
4
4
|
interface TriggerConfig {
|
|
5
5
|
/** The gateway that quotes the addresses and mints the invoice */
|
package/dist/server.js
CHANGED
|
@@ -740,7 +740,9 @@ function blindLightningRail(config) {
|
|
|
740
740
|
verifyUrl: resolved.verifyUrl,
|
|
741
741
|
expiresAt: resolved.expiresAt,
|
|
742
742
|
trigger: config.trigger,
|
|
743
|
-
sealed: await config.sealed?.(order)
|
|
743
|
+
sealed: await config.sealed?.(order),
|
|
744
|
+
webhookUrl: config.webhookUrl,
|
|
745
|
+
webhookSecret: config.webhookSecret
|
|
744
746
|
});
|
|
745
747
|
return {
|
|
746
748
|
id: watched.id,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thunder-bridge",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.6",
|
|
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",
|