@forgesworn/moneyer 0.3.3 → 0.4.0
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/CHANGELOG.md +16 -0
- package/README.md +23 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.js +27 -0
- package/dist/server.js +9 -1
- package/llms.txt +5 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.0] - 2026-08-23
|
|
4
|
+
|
|
5
|
+
- **Reaching the mint over Tor.** `MONEYER_ONION_URL` names this mint's
|
|
6
|
+
hidden service, and a request arriving on that host gets every URL built
|
|
7
|
+
from it: the callback, the `withdrawLink`, the `payLink`, the lightning
|
|
8
|
+
address in the metadata. Without it a Tor visitor was answered with the
|
|
9
|
+
clearnet origin, so their wallet was told to leave Tor to finish the job
|
|
10
|
+
- the callback fetch going out over clearnet, from their address, naming
|
|
11
|
+
the mint they bank with. That is worse than not working, because it looks
|
|
12
|
+
like it worked.
|
|
13
|
+
- The Host header **chooses** between origins here and never builds one.
|
|
14
|
+
`MONEYER_PUBLIC_ORIGIN` exists because that header is attacker
|
|
15
|
+
controlled, and the property is unchanged: the worst a forged Host can do
|
|
16
|
+
is get back the onion URL instead of the clearnet one, and the operator
|
|
17
|
+
configured both.
|
|
18
|
+
|
|
3
19
|
## [0.3.3] - 2026-08-22
|
|
4
20
|
|
|
5
21
|
- **Refusals are logged, so an operator can see why a wallet is failing.**
|
package/README.md
CHANGED
|
@@ -76,6 +76,7 @@ default, and a variable set to an empty string counts as unset.
|
|
|
76
76
|
| `MONEYER_HOST` | `127.0.0.1` | listen address |
|
|
77
77
|
| `MONEYER_PORT` | `3737` | listen port |
|
|
78
78
|
| `MONEYER_PUBLIC_ORIGIN` | derived from `Host` | the origin wallets are told to call back on. Required behind a reverse proxy, and required for zap-to-note |
|
|
79
|
+
| `MONEYER_ONION_URL` | | this mint as a Tor hidden service, e.g. `http://<v3>.onion`. A request arriving on that host gets its URLs built from it (see below) |
|
|
79
80
|
| `MONEYER_USERNAME` | `mint` | the local part of the mint's own lightning address |
|
|
80
81
|
| `MONEYER_DESCRIPTION` | `an LNURLcash note` | what a note is called on the wire (`defaultDescription`, and `description` on discovery) |
|
|
81
82
|
| `MONEYER_DB` | `moneyer.sqlite` | SQLite path; `:memory:` allowed |
|
|
@@ -489,6 +490,28 @@ A registered name resolves on both rails at once:
|
|
|
489
490
|
The discovery endpoint advertises `namePriceMsat` while registration is
|
|
490
491
|
open, so a wallet can offer the flow without asking.
|
|
491
492
|
|
|
493
|
+
## Reaching the mint over Tor
|
|
494
|
+
|
|
495
|
+
Set `MONEYER_ONION_URL` to this mint's hidden service address and a request
|
|
496
|
+
that arrives on that host gets every URL built from it: the callback, the
|
|
497
|
+
`withdrawLink`, the `payLink`, the lightning address in the metadata.
|
|
498
|
+
|
|
499
|
+
Without it a Tor visitor is answered with the clearnet origin, and their
|
|
500
|
+
wallet is then told to leave Tor to finish the job - the callback fetch
|
|
501
|
+
goes out over clearnet, from their address, naming the mint they bank
|
|
502
|
+
with. That is worse than not working, because it looks like it worked.
|
|
503
|
+
|
|
504
|
+
The Host header **chooses** between origins here; it never builds one.
|
|
505
|
+
`MONEYER_PUBLIC_ORIGIN` exists precisely because the header is attacker
|
|
506
|
+
controlled, and that property is unchanged: the worst a forged Host can do
|
|
507
|
+
is get back the onion URL instead of the clearnet one, and the operator
|
|
508
|
+
configured both. Anything else falls through to `MONEYER_PUBLIC_ORIGIN`.
|
|
509
|
+
|
|
510
|
+
Running the hidden service itself is not this mint's job. Point a Tor
|
|
511
|
+
`HiddenServiceDir` (or a proxy that speaks onion) at whatever host and port
|
|
512
|
+
it already listens on, the same way you would front it with Caddy for
|
|
513
|
+
clearnet.
|
|
514
|
+
|
|
492
515
|
## Endpoints
|
|
493
516
|
|
|
494
517
|
| | |
|
package/dist/config.d.ts
CHANGED
package/dist/config.js
CHANGED
|
@@ -139,6 +139,32 @@ export const configFromEnv = (env = process.env) => {
|
|
|
139
139
|
throw new Error(`MONEYER_PUBLIC_ORIGIN must be http or https, got ${JSON.stringify(publicOrigin)}.`);
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
|
+
// The same mint reached over Tor. A hidden service is a different
|
|
143
|
+
// origin, and a mint that answers a Tor visitor with its clearnet URL
|
|
144
|
+
// has told that visitor's wallet to leave Tor to finish the job - which
|
|
145
|
+
// is both broken and the exact thing they came here to avoid.
|
|
146
|
+
//
|
|
147
|
+
// Setting this does not weaken the reason publicOrigin exists. The Host
|
|
148
|
+
// header is spoofable, which is why it is not trusted to BUILD an
|
|
149
|
+
// origin; it is only used to CHOOSE between origins the operator
|
|
150
|
+
// configured. The worst a forged Host can do is get the onion URL back
|
|
151
|
+
// instead of the clearnet one, and the operator set both.
|
|
152
|
+
const onionUrl = env.MONEYER_ONION_URL;
|
|
153
|
+
if (onionUrl) {
|
|
154
|
+
let parsed;
|
|
155
|
+
try {
|
|
156
|
+
parsed = new URL(onionUrl);
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
throw new Error(`MONEYER_ONION_URL is not a URL: ${JSON.stringify(onionUrl)}.`);
|
|
160
|
+
}
|
|
161
|
+
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
|
|
162
|
+
throw new Error(`MONEYER_ONION_URL must be http or https, got ${JSON.stringify(onionUrl)}.`);
|
|
163
|
+
}
|
|
164
|
+
if (!parsed.hostname.endsWith('.onion')) {
|
|
165
|
+
throw new Error(`MONEYER_ONION_URL must be a .onion address, got ${JSON.stringify(parsed.hostname)}.`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
142
168
|
const motd = text(env.MONEYER_MOTD);
|
|
143
169
|
if (motd !== undefined && motd.length > MOTD_MAX) {
|
|
144
170
|
throw new Error(`MONEYER_MOTD must be at most ${MOTD_MAX} characters - it is a banner, not a page.`);
|
|
@@ -174,6 +200,7 @@ export const configFromEnv = (env = process.env) => {
|
|
|
174
200
|
host: env.MONEYER_HOST ?? DEFAULTS.host,
|
|
175
201
|
port: int(env.MONEYER_PORT, DEFAULTS.port),
|
|
176
202
|
...(publicOrigin ? { publicOrigin } : {}),
|
|
203
|
+
...(onionUrl ? { onionUrl } : {}),
|
|
177
204
|
username: env.MONEYER_USERNAME ?? DEFAULTS.username,
|
|
178
205
|
description: env.MONEYER_DESCRIPTION ?? DEFAULTS.description,
|
|
179
206
|
...(name ? { name } : {}),
|
package/dist/server.js
CHANGED
|
@@ -302,7 +302,15 @@ export const createMoneyer = async (config, deps = {}) => {
|
|
|
302
302
|
const handle = async (req, res) => {
|
|
303
303
|
const requestUrl = new URL(req.url ?? '/', `http://${req.headers.host ?? '127.0.0.1'}`);
|
|
304
304
|
const q = requestUrl.searchParams;
|
|
305
|
-
|
|
305
|
+
// Which of this mint's own origins the caller reached it on. The Host
|
|
306
|
+
// header chooses between them and never builds one: it is attacker
|
|
307
|
+
// controlled, and the worst a forged value can do here is get back an
|
|
308
|
+
// origin the operator configured anyway.
|
|
309
|
+
const onionHost = config.onionUrl ? new URL(config.onionUrl).host.toLowerCase() : null;
|
|
310
|
+
const askedFor = (req.headers.host ?? '').toLowerCase();
|
|
311
|
+
const origin = onionHost && askedFor === onionHost
|
|
312
|
+
? config.onionUrl
|
|
313
|
+
: (config.publicOrigin ?? `http://${req.headers.host ?? '127.0.0.1'}`);
|
|
306
314
|
const host = new URL(origin).host;
|
|
307
315
|
const send = (body, status = 200) => {
|
|
308
316
|
res.writeHead(status, {
|
package/llms.txt
CHANGED
|
@@ -65,6 +65,11 @@ verifyAnnouncement(content, mintPubkey) -> {valid, document}
|
|
|
65
65
|
discovery document + sig by the note signing key. MONEYER_ANNOUNCE=true,
|
|
66
66
|
off by default.
|
|
67
67
|
configFromEnv(env?) -> MoneyerConfig MONEYER_* environment variables
|
|
68
|
+
MONEYER_ONION_URL: this mint as a Tor hidden service. A request arriving on
|
|
69
|
+
that Host gets every URL (callback, withdrawLink, payLink, the address in
|
|
70
|
+
the metadata) built from it; anything else falls through to
|
|
71
|
+
MONEYER_PUBLIC_ORIGIN. The Host header only CHOOSES between origins the
|
|
72
|
+
operator configured - it never builds one.
|
|
68
73
|
createFakeBackend() -> LightningBackend with .control test hooks
|
|
69
74
|
createClnBackend({url, rune}) / createLndBackend({url, macaroon})
|
|
70
75
|
createNoteSigner(privHex) -> {pubkey, sign(noteId, amountMsat)}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forgesworn/moneyer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "An LNURLcash (LUD-25) mint - strikes Lightning bearer notes. Independent implementation, cln/lnd funding sources, SQLite, zero HTTP framework.",
|
|
5
5
|
"author": "TheCryptoDonkey",
|
|
6
6
|
"license": "MIT",
|