thunder-bridge 1.3.0 → 1.4.1
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 +25 -2
- package/dist/index.cjs +31 -3
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +31 -3
- package/dist/{rail-DZzlN-bi.d.cts → rail-CqUfuYXJ.d.cts} +27 -1
- package/dist/{rail-DZzlN-bi.d.ts → rail-CqUfuYXJ.d.ts} +27 -1
- package/dist/server.cjs +32 -2
- package/dist/server.d.cts +39 -3
- package/dist/server.d.ts +39 -3
- package/dist/server.js +29 -1
- package/openapi.yaml +84 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -34,10 +34,16 @@ A page can run the whole flow with no backend of its own. The gateway answers
|
|
|
34
34
|
every origin, and coinos, Alby and Stacker News serve their LNURL endpoints with
|
|
35
35
|
CORS open, so the proof fetches work from a browser too.
|
|
36
36
|
|
|
37
|
+
The url below is a shared demo that answers anyone and forgets everything on
|
|
38
|
+
restart. It is there so this snippet runs as written. It is a base url rather than a
|
|
39
|
+
page, so opening it in a browser gives a `404` and
|
|
40
|
+
[`/health`](https://public.thunder-bridge.agora.gripe/health) is what tells you it is
|
|
41
|
+
up. Point production at a gateway of your own, which takes one command.
|
|
42
|
+
|
|
37
43
|
```ts
|
|
38
44
|
import { ThunderBridge, invoiceToSvg, type CreatePaymentParams } from "thunder-bridge";
|
|
39
45
|
|
|
40
|
-
const gateway = new ThunderBridge("https://thunder-bridge
|
|
46
|
+
const gateway = new ThunderBridge("https://public.thunder-bridge.agora.gripe");
|
|
41
47
|
|
|
42
48
|
const request: CreatePaymentParams = {
|
|
43
49
|
lnAddresses: ["alice@coinos.io", "alice@getalby.com"],
|
|
@@ -92,6 +98,7 @@ them and this table does not repeat them.
|
|
|
92
98
|
| `gateway.firstToSettle(ids, options?)` | wait on several legs, keep the first really paid, drop the losers |
|
|
93
99
|
| `gateway.watchPayment(params)` | hand over an invoice you obtained yourself, without the address or the amount |
|
|
94
100
|
| `gateway.followTrigger(secret, options)` | stream every payment carrying one trigger, reconnecting on its own. `replay` asks for how many past settlements on connect |
|
|
101
|
+
| `gateway.createSocketTicket(params)` | a one minute pass onto one trigger's stream, to hand something that must not hold the secret |
|
|
95
102
|
| `gateway.isPrivate` | whether a token was given |
|
|
96
103
|
|
|
97
104
|
**Proving it** - [`src/verify.ts`](src/verify.ts)
|
|
@@ -109,6 +116,8 @@ them and this table does not repeat them.
|
|
|
109
116
|
| Export | What it does |
|
|
110
117
|
|---|---|
|
|
111
118
|
| `lnurlPayEndpoint(config)` | a whole LNURL-pay endpoint as one Fetch handler, so a static QR points at your domain. `replay` keeps its last settlements on the gateway for a page that opens later. From `thunder-bridge/server` |
|
|
119
|
+
| `watchTicketEndpoint(config)` | mints socket tickets for callers who already know the watch secret, 403 for the rest. From `thunder-bridge/server` |
|
|
120
|
+
| `publicWatchTicketEndpoint(config)` | the same for a board strangers are meant to read, minting for anyone. From `thunder-bridge/server` |
|
|
112
121
|
| `seal(secret, plaintext)`, `unseal` | the blob the gateway stores and cannot read |
|
|
113
122
|
| `toLnurl(url)` | bech32-encode an endpoint url |
|
|
114
123
|
| `bankTransfer(params)` | register a Czech QR platba as a watched payment. Refuses a gateway that serves strangers |
|
|
@@ -117,6 +126,16 @@ them and this table does not repeat them.
|
|
|
117
126
|
| `lightningVerifyEndpoint(config)` | the same shape for Lightning, asking the wallet on the gateway's behalf. From `thunder-bridge/server` |
|
|
118
127
|
| `relayedVerifyUrl(mount, wallet, secret)` | the URL to hand the gateway instead of the wallet's, with the wallet's sealed inside |
|
|
119
128
|
|
|
129
|
+
There are two ticket endpoints rather than one taking a flag, so the call site
|
|
130
|
+
says which board this is. Mount one on its own path and POST to it from the page
|
|
131
|
+
before every connect, because a ticket lives a minute. Whichever you mount, the
|
|
132
|
+
page never holds the watch secret, which is the reason to mount either.
|
|
133
|
+
|
|
134
|
+
A public board is public in full: every viewer of that socket gets each
|
|
135
|
+
settlement's preimage, verify url and payment hash. Fine for a tip jar, wrong the
|
|
136
|
+
moment anything is gated behind those preimages, because then a viewer holds the
|
|
137
|
+
unlock.
|
|
138
|
+
|
|
120
139
|
What your service answers once those handlers are mounted is written out in
|
|
121
140
|
[`openapi.yaml`](openapi.yaml), shipped with this package.
|
|
122
141
|
|
|
@@ -167,9 +186,13 @@ BOLT12 offer is not handled, because this gateway never returns one.
|
|
|
167
186
|
import { invoiceToSvg, lnurlToSvg } from "thunder-bridge";
|
|
168
187
|
|
|
169
188
|
const toPay = invoiceToSvg(payment.bolt11, { size: 320, color: "#1a1a2e" });
|
|
170
|
-
const tipJar = lnurlToSvg("https://agora.gripe/
|
|
189
|
+
const tipJar = lnurlToSvg("https://thunder-bridge.agora.gripe/.well-known/lnurlp/21sats");
|
|
171
190
|
```
|
|
172
191
|
|
|
192
|
+
That second url is live and built with this library, a fixed 21 sat tip served by
|
|
193
|
+
`lnurlPayEndpoint`, so the QR it returns is one you can scan to see the whole flow
|
|
194
|
+
end to end.
|
|
195
|
+
|
|
173
196
|
**Minting your own invoice** - [`src/rail.ts`](src/rail.ts): `invoiceFrom`, from
|
|
174
197
|
`thunder-bridge/server`. A gateway that does not mint is one that never sees an
|
|
175
198
|
address or an amount, so this is how a client gets a provable invoice itself and hands
|
package/dist/index.cjs
CHANGED
|
@@ -897,6 +897,18 @@ function triggerEventFromWire(body) {
|
|
|
897
897
|
amountMsat: wire["incoming_amount"] === void 0 ? null : msatFrom(wire["incoming_amount"], 1)
|
|
898
898
|
};
|
|
899
899
|
}
|
|
900
|
+
function socketTicketFromWire(body) {
|
|
901
|
+
const wire = asObject(body);
|
|
902
|
+
if (wire === null) {
|
|
903
|
+
return null;
|
|
904
|
+
}
|
|
905
|
+
const ticket = text(wire["ticket"]);
|
|
906
|
+
const expiresAt = secondsFrom(wire["expires_at"]);
|
|
907
|
+
if (ticket === null || expiresAt === null) {
|
|
908
|
+
return null;
|
|
909
|
+
}
|
|
910
|
+
return { ticket, expiresAt };
|
|
911
|
+
}
|
|
900
912
|
function kindOf(wire) {
|
|
901
913
|
const said = wire["kind"];
|
|
902
914
|
if (said === "minted" || said === "watched") {
|
|
@@ -1920,10 +1932,26 @@ var ThunderBridge = class {
|
|
|
1920
1932
|
socket?.close();
|
|
1921
1933
|
};
|
|
1922
1934
|
}
|
|
1935
|
+
/**
|
|
1936
|
+
* A one minute pass onto one trigger's stream, for something that must hold
|
|
1937
|
+
* neither the token nor the trigger secret. Mint it in a handler and answer
|
|
1938
|
+
* with the ticket alone, because that is all a browser needs to connect and
|
|
1939
|
+
* all it can do anything with. `watchTicketEndpoint` is this method already
|
|
1940
|
+
* wrapped in a route
|
|
1941
|
+
*/
|
|
1942
|
+
async createSocketTicket(params) {
|
|
1943
|
+
return await this.mintedTicket({
|
|
1944
|
+
trigger_secret: unguessable(params.trigger),
|
|
1945
|
+
replay: params.replay
|
|
1946
|
+
});
|
|
1947
|
+
}
|
|
1923
1948
|
needsTicket(asked) {
|
|
1924
1949
|
return asked === true || this.token !== null;
|
|
1925
1950
|
}
|
|
1926
1951
|
async wsTicket(body) {
|
|
1952
|
+
return encodeURIComponent((await this.mintedTicket(body)).ticket);
|
|
1953
|
+
}
|
|
1954
|
+
async mintedTicket(body) {
|
|
1927
1955
|
const sent = JSON.stringify(body);
|
|
1928
1956
|
const response = await fetch(`${this.baseUrl}/ws-tickets`, {
|
|
1929
1957
|
method: "POST",
|
|
@@ -1933,14 +1961,14 @@ var ThunderBridge = class {
|
|
|
1933
1961
|
if (!response.ok) {
|
|
1934
1962
|
throw await problemFrom(response);
|
|
1935
1963
|
}
|
|
1936
|
-
const
|
|
1937
|
-
if (
|
|
1964
|
+
const ticket = socketTicketFromWire(await response.json().catch(() => null));
|
|
1965
|
+
if (ticket === null) {
|
|
1938
1966
|
throw new ProblemError({
|
|
1939
1967
|
status: response.status,
|
|
1940
1968
|
title: "The gateway answered with something that is not a ticket"
|
|
1941
1969
|
});
|
|
1942
1970
|
}
|
|
1943
|
-
return
|
|
1971
|
+
return ticket;
|
|
1944
1972
|
}
|
|
1945
1973
|
async sending(path, body) {
|
|
1946
1974
|
return { "content-type": "application/json", ...await this.reading("POST", path, body) };
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { T as ThunderBridge, W as WalletFailure, a as ThunderBridgeOptions, b as WatchPaymentParams, c as TriggerEvent, d as WaitOptions, P as Payment, C as CreatePaymentParams, S as Settlement } from './rail-
|
|
2
|
-
export { B as BankRailConfig, e as CreateOptions, f as CreateQuoteParams, F as FollowOptions, L as Leg, g as LightningRailConfig, O as Order, h as PaymentKind, i as PaymentStatus, Q as Quote, R as Rail, j as
|
|
1
|
+
import { T as ThunderBridge, W as WalletFailure, a as ThunderBridgeOptions, b as WatchPaymentParams, c as TriggerEvent, d as WaitOptions, P as Payment, C as CreatePaymentParams, S as Settlement } from './rail-CqUfuYXJ.cjs';
|
|
2
|
+
export { B as BankRailConfig, e as CreateOptions, f as CreateQuoteParams, F as FollowOptions, L as Leg, g as LightningRailConfig, O as Order, h as PaymentKind, i as PaymentStatus, Q as Quote, R as Rail, j as SocketTicket, k as SocketTicketParams, l as WalletReason, m as bankRail, n as lightningRail, t as toLnurl } from './rail-CqUfuYXJ.cjs';
|
|
3
3
|
|
|
4
4
|
/** What a BOLT11 invoice says about itself, every field null when it does not carry one */
|
|
5
5
|
interface Invoice {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { T as ThunderBridge, W as WalletFailure, a as ThunderBridgeOptions, b as WatchPaymentParams, c as TriggerEvent, d as WaitOptions, P as Payment, C as CreatePaymentParams, S as Settlement } from './rail-
|
|
2
|
-
export { B as BankRailConfig, e as CreateOptions, f as CreateQuoteParams, F as FollowOptions, L as Leg, g as LightningRailConfig, O as Order, h as PaymentKind, i as PaymentStatus, Q as Quote, R as Rail, j as
|
|
1
|
+
import { T as ThunderBridge, W as WalletFailure, a as ThunderBridgeOptions, b as WatchPaymentParams, c as TriggerEvent, d as WaitOptions, P as Payment, C as CreatePaymentParams, S as Settlement } from './rail-CqUfuYXJ.js';
|
|
2
|
+
export { B as BankRailConfig, e as CreateOptions, f as CreateQuoteParams, F as FollowOptions, L as Leg, g as LightningRailConfig, O as Order, h as PaymentKind, i as PaymentStatus, Q as Quote, R as Rail, j as SocketTicket, k as SocketTicketParams, l as WalletReason, m as bankRail, n as lightningRail, t as toLnurl } from './rail-CqUfuYXJ.js';
|
|
3
3
|
|
|
4
4
|
/** What a BOLT11 invoice says about itself, every field null when it does not carry one */
|
|
5
5
|
interface Invoice {
|
package/dist/index.js
CHANGED
|
@@ -816,6 +816,18 @@ function triggerEventFromWire(body) {
|
|
|
816
816
|
amountMsat: wire["incoming_amount"] === void 0 ? null : msatFrom(wire["incoming_amount"], 1)
|
|
817
817
|
};
|
|
818
818
|
}
|
|
819
|
+
function socketTicketFromWire(body) {
|
|
820
|
+
const wire = asObject(body);
|
|
821
|
+
if (wire === null) {
|
|
822
|
+
return null;
|
|
823
|
+
}
|
|
824
|
+
const ticket = text(wire["ticket"]);
|
|
825
|
+
const expiresAt = secondsFrom(wire["expires_at"]);
|
|
826
|
+
if (ticket === null || expiresAt === null) {
|
|
827
|
+
return null;
|
|
828
|
+
}
|
|
829
|
+
return { ticket, expiresAt };
|
|
830
|
+
}
|
|
819
831
|
function kindOf(wire) {
|
|
820
832
|
const said = wire["kind"];
|
|
821
833
|
if (said === "minted" || said === "watched") {
|
|
@@ -1839,10 +1851,26 @@ var ThunderBridge = class {
|
|
|
1839
1851
|
socket?.close();
|
|
1840
1852
|
};
|
|
1841
1853
|
}
|
|
1854
|
+
/**
|
|
1855
|
+
* A one minute pass onto one trigger's stream, for something that must hold
|
|
1856
|
+
* neither the token nor the trigger secret. Mint it in a handler and answer
|
|
1857
|
+
* with the ticket alone, because that is all a browser needs to connect and
|
|
1858
|
+
* all it can do anything with. `watchTicketEndpoint` is this method already
|
|
1859
|
+
* wrapped in a route
|
|
1860
|
+
*/
|
|
1861
|
+
async createSocketTicket(params) {
|
|
1862
|
+
return await this.mintedTicket({
|
|
1863
|
+
trigger_secret: unguessable(params.trigger),
|
|
1864
|
+
replay: params.replay
|
|
1865
|
+
});
|
|
1866
|
+
}
|
|
1842
1867
|
needsTicket(asked) {
|
|
1843
1868
|
return asked === true || this.token !== null;
|
|
1844
1869
|
}
|
|
1845
1870
|
async wsTicket(body) {
|
|
1871
|
+
return encodeURIComponent((await this.mintedTicket(body)).ticket);
|
|
1872
|
+
}
|
|
1873
|
+
async mintedTicket(body) {
|
|
1846
1874
|
const sent = JSON.stringify(body);
|
|
1847
1875
|
const response = await fetch(`${this.baseUrl}/ws-tickets`, {
|
|
1848
1876
|
method: "POST",
|
|
@@ -1852,14 +1880,14 @@ var ThunderBridge = class {
|
|
|
1852
1880
|
if (!response.ok) {
|
|
1853
1881
|
throw await problemFrom(response);
|
|
1854
1882
|
}
|
|
1855
|
-
const
|
|
1856
|
-
if (
|
|
1883
|
+
const ticket = socketTicketFromWire(await response.json().catch(() => null));
|
|
1884
|
+
if (ticket === null) {
|
|
1857
1885
|
throw new ProblemError({
|
|
1858
1886
|
status: response.status,
|
|
1859
1887
|
title: "The gateway answered with something that is not a ticket"
|
|
1860
1888
|
});
|
|
1861
1889
|
}
|
|
1862
|
-
return
|
|
1890
|
+
return ticket;
|
|
1863
1891
|
}
|
|
1864
1892
|
async sending(path, body) {
|
|
1865
1893
|
return { "content-type": "application/json", ...await this.reading("POST", path, body) };
|
|
@@ -98,6 +98,23 @@ interface WatchPaymentParams {
|
|
|
98
98
|
sealed?: string;
|
|
99
99
|
webhookUrl?: string;
|
|
100
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* A one minute pass onto one trigger's stream. It opens that trigger and nothing
|
|
103
|
+
* else, which is what makes it the thing to hand a browser when the trigger
|
|
104
|
+
* secret is not. `expiresAt` is unix seconds, like every other time here
|
|
105
|
+
*/
|
|
106
|
+
interface SocketTicket {
|
|
107
|
+
ticket: string;
|
|
108
|
+
expiresAt: number;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Which trigger the ticket opens, and how many of its settlements the socket
|
|
112
|
+
* replays on connect, up to the ceiling the gateway's operator set
|
|
113
|
+
*/
|
|
114
|
+
interface SocketTicketParams {
|
|
115
|
+
trigger: string;
|
|
116
|
+
replay?: number;
|
|
117
|
+
}
|
|
101
118
|
/** Why one wallet in the list could not be used */
|
|
102
119
|
type WalletReason = "address-unusable" | "unreachable" | "amount-not-accepted" | "cannot-prove-delivery" | "invoice-refused";
|
|
103
120
|
interface WalletFailure {
|
|
@@ -326,8 +343,17 @@ declare class ThunderBridge {
|
|
|
326
343
|
* is called. A trigger has no terminal state, so this never resolves
|
|
327
344
|
*/
|
|
328
345
|
followTrigger(secret: string, options: FollowOptions): () => void;
|
|
346
|
+
/**
|
|
347
|
+
* A one minute pass onto one trigger's stream, for something that must hold
|
|
348
|
+
* neither the token nor the trigger secret. Mint it in a handler and answer
|
|
349
|
+
* with the ticket alone, because that is all a browser needs to connect and
|
|
350
|
+
* all it can do anything with. `watchTicketEndpoint` is this method already
|
|
351
|
+
* wrapped in a route
|
|
352
|
+
*/
|
|
353
|
+
createSocketTicket(params: SocketTicketParams): Promise<SocketTicket>;
|
|
329
354
|
private needsTicket;
|
|
330
355
|
private wsTicket;
|
|
356
|
+
private mintedTicket;
|
|
331
357
|
private sending;
|
|
332
358
|
private reading;
|
|
333
359
|
private speaking;
|
|
@@ -586,4 +612,4 @@ declare function nwcRail(config: NwcRailConfig): Rail;
|
|
|
586
612
|
*/
|
|
587
613
|
declare function invoiceFrom(lnAddresses: string[], amountMsat: number): Promise<Resolved>;
|
|
588
614
|
|
|
589
|
-
export {
|
|
615
|
+
export { nwcPay as A, type BankRailConfig as B, type CreatePaymentParams as C, nwcRail as D, nwcSettlement as E, type FollowOptions as F, nwcVerifyEndpoint as G, nwcVerifyUrl as H, type Leg as L, type NwcConnection as N, type Order as O, type Payment as P, type Quote as Q, type Rail as R, type Settlement as S, ThunderBridge as T, type WalletFailure as W, type ThunderBridgeOptions as a, type WatchPaymentParams as b, type TriggerEvent as c, type WaitOptions as d, type CreateOptions as e, type CreateQuoteParams as f, type LightningRailConfig as g, type PaymentKind as h, type PaymentStatus as i, type SocketTicket as j, type SocketTicketParams as k, type WalletReason as l, bankRail as m, lightningRail as n, type BlindLightningRailConfig as o, type NwcInvoice as p, type NwcRailConfig as q, type NwcVerifyConfig as r, type Resolved as s, toLnurl as t, askWallet as u, blindLightningRail as v, invoiceFrom as w, nwcConnection as x, nwcHoldInvoice as y, nwcInvoice as z };
|
|
@@ -98,6 +98,23 @@ interface WatchPaymentParams {
|
|
|
98
98
|
sealed?: string;
|
|
99
99
|
webhookUrl?: string;
|
|
100
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* A one minute pass onto one trigger's stream. It opens that trigger and nothing
|
|
103
|
+
* else, which is what makes it the thing to hand a browser when the trigger
|
|
104
|
+
* secret is not. `expiresAt` is unix seconds, like every other time here
|
|
105
|
+
*/
|
|
106
|
+
interface SocketTicket {
|
|
107
|
+
ticket: string;
|
|
108
|
+
expiresAt: number;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Which trigger the ticket opens, and how many of its settlements the socket
|
|
112
|
+
* replays on connect, up to the ceiling the gateway's operator set
|
|
113
|
+
*/
|
|
114
|
+
interface SocketTicketParams {
|
|
115
|
+
trigger: string;
|
|
116
|
+
replay?: number;
|
|
117
|
+
}
|
|
101
118
|
/** Why one wallet in the list could not be used */
|
|
102
119
|
type WalletReason = "address-unusable" | "unreachable" | "amount-not-accepted" | "cannot-prove-delivery" | "invoice-refused";
|
|
103
120
|
interface WalletFailure {
|
|
@@ -326,8 +343,17 @@ declare class ThunderBridge {
|
|
|
326
343
|
* is called. A trigger has no terminal state, so this never resolves
|
|
327
344
|
*/
|
|
328
345
|
followTrigger(secret: string, options: FollowOptions): () => void;
|
|
346
|
+
/**
|
|
347
|
+
* A one minute pass onto one trigger's stream, for something that must hold
|
|
348
|
+
* neither the token nor the trigger secret. Mint it in a handler and answer
|
|
349
|
+
* with the ticket alone, because that is all a browser needs to connect and
|
|
350
|
+
* all it can do anything with. `watchTicketEndpoint` is this method already
|
|
351
|
+
* wrapped in a route
|
|
352
|
+
*/
|
|
353
|
+
createSocketTicket(params: SocketTicketParams): Promise<SocketTicket>;
|
|
329
354
|
private needsTicket;
|
|
330
355
|
private wsTicket;
|
|
356
|
+
private mintedTicket;
|
|
331
357
|
private sending;
|
|
332
358
|
private reading;
|
|
333
359
|
private speaking;
|
|
@@ -586,4 +612,4 @@ declare function nwcRail(config: NwcRailConfig): Rail;
|
|
|
586
612
|
*/
|
|
587
613
|
declare function invoiceFrom(lnAddresses: string[], amountMsat: number): Promise<Resolved>;
|
|
588
614
|
|
|
589
|
-
export {
|
|
615
|
+
export { nwcPay as A, type BankRailConfig as B, type CreatePaymentParams as C, nwcRail as D, nwcSettlement as E, type FollowOptions as F, nwcVerifyEndpoint as G, nwcVerifyUrl as H, type Leg as L, type NwcConnection as N, type Order as O, type Payment as P, type Quote as Q, type Rail as R, type Settlement as S, ThunderBridge as T, type WalletFailure as W, type ThunderBridgeOptions as a, type WatchPaymentParams as b, type TriggerEvent as c, type WaitOptions as d, type CreateOptions as e, type CreateQuoteParams as f, type LightningRailConfig as g, type PaymentKind as h, type PaymentStatus as i, type SocketTicket as j, type SocketTicketParams as k, type WalletReason as l, bankRail as m, lightningRail as n, type BlindLightningRailConfig as o, type NwcInvoice as p, type NwcRailConfig as q, type NwcVerifyConfig as r, type Resolved as s, toLnurl as t, askWallet as u, blindLightningRail as v, invoiceFrom as w, nwcConnection as x, nwcHoldInvoice as y, nwcInvoice as z };
|
package/dist/server.cjs
CHANGED
|
@@ -43,7 +43,9 @@ __export(server_exports, {
|
|
|
43
43
|
nwcSettlement: () => nwcSettlement,
|
|
44
44
|
nwcVerifyEndpoint: () => nwcVerifyEndpoint,
|
|
45
45
|
nwcVerifyUrl: () => nwcVerifyUrl,
|
|
46
|
-
|
|
46
|
+
publicWatchTicketEndpoint: () => publicWatchTicketEndpoint,
|
|
47
|
+
relayedVerifyUrl: () => relayedVerifyUrl,
|
|
48
|
+
watchTicketEndpoint: () => watchTicketEndpoint
|
|
47
49
|
});
|
|
48
50
|
module.exports = __toCommonJS(server_exports);
|
|
49
51
|
|
|
@@ -1447,6 +1449,18 @@ function lnurlPayEndpoint(config) {
|
|
|
1447
1449
|
}
|
|
1448
1450
|
};
|
|
1449
1451
|
}
|
|
1452
|
+
function watchTicketEndpoint(config) {
|
|
1453
|
+
return async (request) => {
|
|
1454
|
+
const offered = await offeredSecret(request);
|
|
1455
|
+
if (!equalInConstantTime(sha256Hex(offered), sha256Hex(config.watchSecret))) {
|
|
1456
|
+
return Response.json({ reason: "not the watch secret" }, { status: 403 });
|
|
1457
|
+
}
|
|
1458
|
+
return await issue(config);
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
function publicWatchTicketEndpoint(config) {
|
|
1462
|
+
return () => issue(config);
|
|
1463
|
+
}
|
|
1450
1464
|
async function offer(config, url) {
|
|
1451
1465
|
const amountMsat = await config.amountMsat();
|
|
1452
1466
|
const quote = await config.gateway.createQuote({
|
|
@@ -1531,6 +1545,20 @@ function randomNonce() {
|
|
|
1531
1545
|
function refuse(reason) {
|
|
1532
1546
|
return Response.json({ status: "ERROR", reason });
|
|
1533
1547
|
}
|
|
1548
|
+
async function issue(config) {
|
|
1549
|
+
const issued = await config.gateway.createSocketTicket({
|
|
1550
|
+
trigger: config.watchSecret,
|
|
1551
|
+
replay: config.replay
|
|
1552
|
+
});
|
|
1553
|
+
return Response.json({
|
|
1554
|
+
ticket: issued.ticket,
|
|
1555
|
+
expires_at: new Date(issued.expiresAt * 1e3).toISOString()
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1558
|
+
async function offeredSecret(request) {
|
|
1559
|
+
const body = await request.json().catch(() => null);
|
|
1560
|
+
return typeof body?.secret === "string" ? body.secret : "";
|
|
1561
|
+
}
|
|
1534
1562
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1535
1563
|
0 && (module.exports = {
|
|
1536
1564
|
askWallet,
|
|
@@ -1546,5 +1574,7 @@ function refuse(reason) {
|
|
|
1546
1574
|
nwcSettlement,
|
|
1547
1575
|
nwcVerifyEndpoint,
|
|
1548
1576
|
nwcVerifyUrl,
|
|
1549
|
-
|
|
1577
|
+
publicWatchTicketEndpoint,
|
|
1578
|
+
relayedVerifyUrl,
|
|
1579
|
+
watchTicketEndpoint
|
|
1550
1580
|
});
|
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-CqUfuYXJ.cjs';
|
|
2
|
+
export { o as BlindLightningRailConfig, N as NwcConnection, p as NwcInvoice, q as NwcRailConfig, r as NwcVerifyConfig, s as Resolved, u as askWallet, v as blindLightningRail, w as invoiceFrom, x as nwcConnection, y as nwcHoldInvoice, z as nwcInvoice, A as nwcPay, D as nwcRail, E as nwcSettlement, G as nwcVerifyEndpoint, H as nwcVerifyUrl } from './rail-CqUfuYXJ.cjs';
|
|
3
3
|
|
|
4
4
|
/** The wallet's own LUD-21 URL and the hash its preimage has to match */
|
|
5
5
|
interface Relayed {
|
|
@@ -92,6 +92,23 @@ interface Minted {
|
|
|
92
92
|
verifyUrl: string;
|
|
93
93
|
expiresAt: number;
|
|
94
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* A trigger's live stream is opened with a ticket rather than with the watch
|
|
97
|
+
* secret, so something has to hold the secret and trade it for tickets. That is
|
|
98
|
+
* what these two endpoints are, and they are the only place the gateway's token
|
|
99
|
+
* has to be
|
|
100
|
+
*/
|
|
101
|
+
interface WatchTicketConfig {
|
|
102
|
+
/** The gateway that mints the ticket, holding the token this keeps off the wire */
|
|
103
|
+
gateway: ThunderBridge;
|
|
104
|
+
/** The trigger to open, the same secret `lnurlPayEndpoint` groups its payments under */
|
|
105
|
+
watchSecret: string;
|
|
106
|
+
/**
|
|
107
|
+
* How many of this trigger's settlements the socket replays on connect, so a
|
|
108
|
+
* page opened late still shows what it missed, up to the gateway's ceiling
|
|
109
|
+
*/
|
|
110
|
+
replay?: number;
|
|
111
|
+
}
|
|
95
112
|
/**
|
|
96
113
|
* An LNURL-pay endpoint standing in front of a priority list of addresses, as a
|
|
97
114
|
* Fetch handler so it runs on Deno Deploy, Workers, Hono, Next and Node alike.
|
|
@@ -105,5 +122,24 @@ interface Minted {
|
|
|
105
122
|
* Nothing is stored between the two, so this holds no state of its own.
|
|
106
123
|
*/
|
|
107
124
|
declare function lnurlPayEndpoint(config: TriggerConfig): (request: Request) => Promise<Response>;
|
|
125
|
+
/**
|
|
126
|
+
* Trades the watch secret for a socket ticket, for a board that is not public.
|
|
127
|
+
* The caller has to know the secret already, so all this adds is that the secret
|
|
128
|
+
* stops travelling in socket URLs, where the gateway, every proxy in front of it
|
|
129
|
+
* and the browser's own history all keep a copy. Anyone without it gets a 403.
|
|
130
|
+
*
|
|
131
|
+
* POST to it before every connect, because a ticket lives one minute.
|
|
132
|
+
*/
|
|
133
|
+
declare function watchTicketEndpoint(config: WatchTicketConfig): (request: Request) => Promise<Response>;
|
|
134
|
+
/**
|
|
135
|
+
* Mints a socket ticket for anybody who asks, for a board meant to be read by
|
|
136
|
+
* strangers. It reads no body and refuses nobody, which makes the trigger's
|
|
137
|
+
* whole stream public: every viewer gets each settlement's preimage, verify url
|
|
138
|
+
* and payment hash.
|
|
139
|
+
*
|
|
140
|
+
* Only for a trigger where that is the point. Gate anything on those preimages
|
|
141
|
+
* and a viewer of the board is holding the unlock.
|
|
142
|
+
*/
|
|
143
|
+
declare function publicWatchTicketEndpoint(config: WatchTicketConfig): (request: Request) => Promise<Response>;
|
|
108
144
|
|
|
109
|
-
export { type LightningVerifyConfig, type Minted, type Relayed, type TriggerConfig, lightningVerifyEndpoint, lnurlPayEndpoint, relayedVerifyUrl };
|
|
145
|
+
export { type LightningVerifyConfig, type Minted, type Relayed, type TriggerConfig, type WatchTicketConfig, lightningVerifyEndpoint, lnurlPayEndpoint, publicWatchTicketEndpoint, relayedVerifyUrl, watchTicketEndpoint };
|
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-CqUfuYXJ.js';
|
|
2
|
+
export { o as BlindLightningRailConfig, N as NwcConnection, p as NwcInvoice, q as NwcRailConfig, r as NwcVerifyConfig, s as Resolved, u as askWallet, v as blindLightningRail, w as invoiceFrom, x as nwcConnection, y as nwcHoldInvoice, z as nwcInvoice, A as nwcPay, D as nwcRail, E as nwcSettlement, G as nwcVerifyEndpoint, H as nwcVerifyUrl } from './rail-CqUfuYXJ.js';
|
|
3
3
|
|
|
4
4
|
/** The wallet's own LUD-21 URL and the hash its preimage has to match */
|
|
5
5
|
interface Relayed {
|
|
@@ -92,6 +92,23 @@ interface Minted {
|
|
|
92
92
|
verifyUrl: string;
|
|
93
93
|
expiresAt: number;
|
|
94
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* A trigger's live stream is opened with a ticket rather than with the watch
|
|
97
|
+
* secret, so something has to hold the secret and trade it for tickets. That is
|
|
98
|
+
* what these two endpoints are, and they are the only place the gateway's token
|
|
99
|
+
* has to be
|
|
100
|
+
*/
|
|
101
|
+
interface WatchTicketConfig {
|
|
102
|
+
/** The gateway that mints the ticket, holding the token this keeps off the wire */
|
|
103
|
+
gateway: ThunderBridge;
|
|
104
|
+
/** The trigger to open, the same secret `lnurlPayEndpoint` groups its payments under */
|
|
105
|
+
watchSecret: string;
|
|
106
|
+
/**
|
|
107
|
+
* How many of this trigger's settlements the socket replays on connect, so a
|
|
108
|
+
* page opened late still shows what it missed, up to the gateway's ceiling
|
|
109
|
+
*/
|
|
110
|
+
replay?: number;
|
|
111
|
+
}
|
|
95
112
|
/**
|
|
96
113
|
* An LNURL-pay endpoint standing in front of a priority list of addresses, as a
|
|
97
114
|
* Fetch handler so it runs on Deno Deploy, Workers, Hono, Next and Node alike.
|
|
@@ -105,5 +122,24 @@ interface Minted {
|
|
|
105
122
|
* Nothing is stored between the two, so this holds no state of its own.
|
|
106
123
|
*/
|
|
107
124
|
declare function lnurlPayEndpoint(config: TriggerConfig): (request: Request) => Promise<Response>;
|
|
125
|
+
/**
|
|
126
|
+
* Trades the watch secret for a socket ticket, for a board that is not public.
|
|
127
|
+
* The caller has to know the secret already, so all this adds is that the secret
|
|
128
|
+
* stops travelling in socket URLs, where the gateway, every proxy in front of it
|
|
129
|
+
* and the browser's own history all keep a copy. Anyone without it gets a 403.
|
|
130
|
+
*
|
|
131
|
+
* POST to it before every connect, because a ticket lives one minute.
|
|
132
|
+
*/
|
|
133
|
+
declare function watchTicketEndpoint(config: WatchTicketConfig): (request: Request) => Promise<Response>;
|
|
134
|
+
/**
|
|
135
|
+
* Mints a socket ticket for anybody who asks, for a board meant to be read by
|
|
136
|
+
* strangers. It reads no body and refuses nobody, which makes the trigger's
|
|
137
|
+
* whole stream public: every viewer gets each settlement's preimage, verify url
|
|
138
|
+
* and payment hash.
|
|
139
|
+
*
|
|
140
|
+
* Only for a trigger where that is the point. Gate anything on those preimages
|
|
141
|
+
* and a viewer of the board is holding the unlock.
|
|
142
|
+
*/
|
|
143
|
+
declare function publicWatchTicketEndpoint(config: WatchTicketConfig): (request: Request) => Promise<Response>;
|
|
108
144
|
|
|
109
|
-
export { type LightningVerifyConfig, type Minted, type Relayed, type TriggerConfig, lightningVerifyEndpoint, lnurlPayEndpoint, relayedVerifyUrl };
|
|
145
|
+
export { type LightningVerifyConfig, type Minted, type Relayed, type TriggerConfig, type WatchTicketConfig, lightningVerifyEndpoint, lnurlPayEndpoint, publicWatchTicketEndpoint, relayedVerifyUrl, watchTicketEndpoint };
|
package/dist/server.js
CHANGED
|
@@ -1398,6 +1398,18 @@ function lnurlPayEndpoint(config) {
|
|
|
1398
1398
|
}
|
|
1399
1399
|
};
|
|
1400
1400
|
}
|
|
1401
|
+
function watchTicketEndpoint(config) {
|
|
1402
|
+
return async (request) => {
|
|
1403
|
+
const offered = await offeredSecret(request);
|
|
1404
|
+
if (!equalInConstantTime(sha256Hex(offered), sha256Hex(config.watchSecret))) {
|
|
1405
|
+
return Response.json({ reason: "not the watch secret" }, { status: 403 });
|
|
1406
|
+
}
|
|
1407
|
+
return await issue(config);
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1410
|
+
function publicWatchTicketEndpoint(config) {
|
|
1411
|
+
return () => issue(config);
|
|
1412
|
+
}
|
|
1401
1413
|
async function offer(config, url) {
|
|
1402
1414
|
const amountMsat = await config.amountMsat();
|
|
1403
1415
|
const quote = await config.gateway.createQuote({
|
|
@@ -1482,6 +1494,20 @@ function randomNonce() {
|
|
|
1482
1494
|
function refuse(reason) {
|
|
1483
1495
|
return Response.json({ status: "ERROR", reason });
|
|
1484
1496
|
}
|
|
1497
|
+
async function issue(config) {
|
|
1498
|
+
const issued = await config.gateway.createSocketTicket({
|
|
1499
|
+
trigger: config.watchSecret,
|
|
1500
|
+
replay: config.replay
|
|
1501
|
+
});
|
|
1502
|
+
return Response.json({
|
|
1503
|
+
ticket: issued.ticket,
|
|
1504
|
+
expires_at: new Date(issued.expiresAt * 1e3).toISOString()
|
|
1505
|
+
});
|
|
1506
|
+
}
|
|
1507
|
+
async function offeredSecret(request) {
|
|
1508
|
+
const body = await request.json().catch(() => null);
|
|
1509
|
+
return typeof body?.secret === "string" ? body.secret : "";
|
|
1510
|
+
}
|
|
1485
1511
|
export {
|
|
1486
1512
|
askWallet,
|
|
1487
1513
|
blindLightningRail,
|
|
@@ -1496,5 +1522,7 @@ export {
|
|
|
1496
1522
|
nwcSettlement,
|
|
1497
1523
|
nwcVerifyEndpoint,
|
|
1498
1524
|
nwcVerifyUrl,
|
|
1499
|
-
|
|
1525
|
+
publicWatchTicketEndpoint,
|
|
1526
|
+
relayedVerifyUrl,
|
|
1527
|
+
watchTicketEndpoint
|
|
1500
1528
|
};
|
package/openapi.yaml
CHANGED
|
@@ -2,7 +2,7 @@ openapi: 3.1.0
|
|
|
2
2
|
|
|
3
3
|
info:
|
|
4
4
|
title: thunder-bridge, the endpoint your own service serves
|
|
5
|
-
version: 1.
|
|
5
|
+
version: 1.4.1
|
|
6
6
|
license:
|
|
7
7
|
name: MIT
|
|
8
8
|
identifier: MIT
|
|
@@ -26,8 +26,12 @@ info:
|
|
|
26
26
|
|
|
27
27
|
`bankVerifyEndpoint` is the other handler, and it puts a second rail behind the same
|
|
28
28
|
contract: a bank transfer, proved to the gateway by the LUD-21 shape a Lightning wallet
|
|
29
|
-
would have answered with.
|
|
30
|
-
|
|
29
|
+
would have answered with.
|
|
30
|
+
|
|
31
|
+
`watchTicketEndpoint` and `publicWatchTicketEndpoint` are the third, and they exist so a
|
|
32
|
+
browser can read a trigger's live stream without holding the credentials that open it.
|
|
33
|
+
Every path below is therefore yours to mount and yours to serve, and none of them is the
|
|
34
|
+
gateway.
|
|
31
35
|
|
|
32
36
|
The gateway's own API is a separate document, `openapi.yaml` in the repository root, and
|
|
33
37
|
the webhook your server receives is described there under `webhooks`.
|
|
@@ -43,6 +47,8 @@ tags:
|
|
|
43
47
|
description: The two halves of LUD-06, on the one path a lightning address resolves to.
|
|
44
48
|
- name: bank-transfer
|
|
45
49
|
description: The settlement proof for money that arrived over a bank rail instead of Lightning.
|
|
50
|
+
- name: live-board
|
|
51
|
+
description: The exchange that lets a page watch a trigger without being given its secret.
|
|
46
52
|
|
|
47
53
|
paths:
|
|
48
54
|
/.well-known/lnurlp/{name}:
|
|
@@ -137,6 +143,57 @@ paths:
|
|
|
137
143
|
- $ref: "#/components/schemas/invoice"
|
|
138
144
|
- $ref: "#/components/schemas/refusal"
|
|
139
145
|
|
|
146
|
+
/watch/ticket:
|
|
147
|
+
post:
|
|
148
|
+
tags: [live-board]
|
|
149
|
+
operationId: mintWatchTicket
|
|
150
|
+
summary: Trade the watch secret for a socket ticket a browser may hold
|
|
151
|
+
description: |
|
|
152
|
+
Mounted wherever you like by `watchTicketEndpoint`, or by
|
|
153
|
+
`publicWatchTicketEndpoint` when the board is meant to be read by strangers. Which
|
|
154
|
+
one you mounted is the whole difference: the first compares the offered secret in
|
|
155
|
+
constant time and answers 403 without it, the second reads no body and refuses
|
|
156
|
+
nobody.
|
|
157
|
+
|
|
158
|
+
It exists because the socket is opened at the gateway's `/ws/tickets/{ticket}`, and
|
|
159
|
+
minting a ticket needs the bearer token and the watch secret. Neither may reach a
|
|
160
|
+
browser, so this endpoint holds both and hands back only the ticket, which opens one
|
|
161
|
+
trigger and nothing else.
|
|
162
|
+
|
|
163
|
+
The answer is the gateway's own, passed through unchanged. A ticket lives one minute,
|
|
164
|
+
so a page POSTs here immediately before every connect rather than caching one.
|
|
165
|
+
|
|
166
|
+
A public board is public in full. Every viewer of that socket gets each settlement's
|
|
167
|
+
preimage, verify url and payment hash, which is fine for a tip jar and wrong the
|
|
168
|
+
moment anything is gated behind those preimages.
|
|
169
|
+
requestBody:
|
|
170
|
+
required: false
|
|
171
|
+
content:
|
|
172
|
+
application/json:
|
|
173
|
+
schema:
|
|
174
|
+
type: object
|
|
175
|
+
properties:
|
|
176
|
+
secret:
|
|
177
|
+
type: string
|
|
178
|
+
description: |
|
|
179
|
+
The same watch secret the trigger groups its payments under. Required by
|
|
180
|
+
`watchTicketEndpoint` and ignored by `publicWatchTicketEndpoint`.
|
|
181
|
+
responses:
|
|
182
|
+
"200":
|
|
183
|
+
description: A ticket, good for one socket on one trigger for one minute.
|
|
184
|
+
content:
|
|
185
|
+
application/json:
|
|
186
|
+
schema:
|
|
187
|
+
$ref: "#/components/schemas/socket_ticket"
|
|
188
|
+
"403":
|
|
189
|
+
description: |
|
|
190
|
+
The body did not carry the watch secret. Never answered by
|
|
191
|
+
`publicWatchTicketEndpoint`, which does not read one.
|
|
192
|
+
content:
|
|
193
|
+
application/json:
|
|
194
|
+
schema:
|
|
195
|
+
$ref: "#/components/schemas/not_the_secret"
|
|
196
|
+
|
|
140
197
|
/verify/bank:
|
|
141
198
|
get:
|
|
142
199
|
tags: [bank-transfer]
|
|
@@ -274,6 +331,30 @@ components:
|
|
|
274
331
|
const: false
|
|
275
332
|
required: [settled]
|
|
276
333
|
|
|
334
|
+
socket_ticket:
|
|
335
|
+
type: object
|
|
336
|
+
description: The gateway's ticket, passed through so a page reading `.ticket` needs no change.
|
|
337
|
+
properties:
|
|
338
|
+
ticket:
|
|
339
|
+
type: string
|
|
340
|
+
description: |
|
|
341
|
+
Goes in the gateway's `/ws/tickets/{ticket}` path. Signed, not stored, and bound
|
|
342
|
+
to the one trigger it was minted for.
|
|
343
|
+
expires_at:
|
|
344
|
+
type: string
|
|
345
|
+
format: date-time
|
|
346
|
+
description: A minute from minting. A ticket presented after it is refused at the handshake.
|
|
347
|
+
required: [ticket, expires_at]
|
|
348
|
+
|
|
349
|
+
not_the_secret:
|
|
350
|
+
type: object
|
|
351
|
+
description: A refusal that says only that the secret was wrong, never what was expected.
|
|
352
|
+
properties:
|
|
353
|
+
reason:
|
|
354
|
+
type: string
|
|
355
|
+
const: not the watch secret
|
|
356
|
+
required: [reason]
|
|
357
|
+
|
|
277
358
|
ln_address:
|
|
278
359
|
type: string
|
|
279
360
|
format: email
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thunder-bridge",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.1",
|
|
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",
|