@forgesworn/moneyer 0.4.0 → 0.5.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 +20 -0
- package/README.md +31 -0
- package/dist/server.js +16 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.0] - 2026-08-23
|
|
4
|
+
|
|
5
|
+
- **Both doors, named.** With `MONEYER_ONION_URL` and
|
|
6
|
+
`MONEYER_PUBLIC_ORIGIN` both set, `/w` and the mint address now carry
|
|
7
|
+
`mirrors`: this mint's other origin. Both serve the same notes - a note
|
|
8
|
+
is keyed by `sha256(k1)` and not by host - but a note travels as a URL,
|
|
9
|
+
and a URL names one host, so an onion note handed to somebody without
|
|
10
|
+
Tor is unspendable to them. Naming the other door is what lets a wallet
|
|
11
|
+
offer the holder a way across.
|
|
12
|
+
- Absent rather than empty when there is only one door, because a wallet
|
|
13
|
+
reading `[]` would think it had been told something. The hourly Nostr
|
|
14
|
+
announcement carries it too.
|
|
15
|
+
- Documented, because it is a real limit rather than a missing feature: a
|
|
16
|
+
wallet must **not** fail over to a mirror on its own. Every request that
|
|
17
|
+
identifies a note carries the k1, and nothing in LUD-25 lets a host
|
|
18
|
+
prove it is the same mint before receiving that secret - a hostile
|
|
19
|
+
discovery document can name any host and claim any `mintPubkey`, since
|
|
20
|
+
claiming is not proving. Closing it needs a signed challenge the spec
|
|
21
|
+
does not have.
|
|
22
|
+
|
|
3
23
|
## [0.4.0] - 2026-08-23
|
|
4
24
|
|
|
5
25
|
- **Reaching the mint over Tor.** `MONEYER_ONION_URL` names this mint's
|
package/README.md
CHANGED
|
@@ -507,6 +507,37 @@ controlled, and that property is unchanged: the worst a forged Host can do
|
|
|
507
507
|
is get back the onion URL instead of the clearnet one, and the operator
|
|
508
508
|
configured both. Anything else falls through to `MONEYER_PUBLIC_ORIGIN`.
|
|
509
509
|
|
|
510
|
+
### Serving both audiences at once
|
|
511
|
+
|
|
512
|
+
Both doors serve the **same notes**. A note is keyed by `sha256(k1)` and
|
|
513
|
+
not by host, so a k1 struck on the onion is valid at the clearnet host and
|
|
514
|
+
the other way round. What is bound to a host is only the URL a note
|
|
515
|
+
travels as - and an onion note handed to somebody without Tor is
|
|
516
|
+
unspendable to them, while a clearnet note redeemed over Tor is private
|
|
517
|
+
only if their wallet proxies.
|
|
518
|
+
|
|
519
|
+
So when both are configured, `/w` and the mint address name the other one
|
|
520
|
+
in `mirrors`:
|
|
521
|
+
|
|
522
|
+
```json
|
|
523
|
+
{"tag": "withdrawRequest", "callback": "http://<v3>.onion/w/cb",
|
|
524
|
+
"mirrors": ["https://mint.example"], "mintPubkey": "..."}
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
Absent when there is only one door, rather than an empty array - a wallet
|
|
528
|
+
reading `[]` would think it had been told something. The hourly Nostr
|
|
529
|
+
announcement carries it too, so a wallet that discovers this mint on a
|
|
530
|
+
relay learns both doors before it holds anything.
|
|
531
|
+
|
|
532
|
+
**A wallet must not fail over to a mirror on its own.** Every request that
|
|
533
|
+
identifies a note carries the k1, so switching hosts discloses a bearer
|
|
534
|
+
secret to the new one, and nothing in LUD-25 lets a host prove it is the
|
|
535
|
+
same mint before receiving that secret - a hostile discovery document can
|
|
536
|
+
name any host and claim any `mintPubkey`, because claiming is not proving.
|
|
537
|
+
Show the holder the other door and let them choose it. Closing that
|
|
538
|
+
properly needs a challenge the mint signs, which the spec does not have
|
|
539
|
+
yet.
|
|
540
|
+
|
|
510
541
|
Running the hidden service itself is not this mint's job. Point a Tor
|
|
511
542
|
`HiddenServiceDir` (or a proxy that speaks onion) at whatever host and port
|
|
512
543
|
it already listens on, the same way you would front it with Caddy for
|
package/dist/server.js
CHANGED
|
@@ -208,7 +208,7 @@ export const createMoneyer = async (config, deps = {}) => {
|
|
|
208
208
|
// LUD-25 discovery, built here rather than inline in its route because
|
|
209
209
|
// the mint also announces itself with it, and there must be one
|
|
210
210
|
// description of a mint rather than two that can drift apart.
|
|
211
|
-
const mintAddressDocument = (origin, user) => ({
|
|
211
|
+
const mintAddressDocument = (origin, user, mirrors = []) => ({
|
|
212
212
|
tag: 'withdrawRequest',
|
|
213
213
|
callback: `${origin}/w`,
|
|
214
214
|
minWithdrawable: config.minMintMsat,
|
|
@@ -217,6 +217,7 @@ export const createMoneyer = async (config, deps = {}) => {
|
|
|
217
217
|
: config.maxSendableMsat,
|
|
218
218
|
defaultDescription: config.description,
|
|
219
219
|
payLink: `${origin}/.well-known/lnurlp/${user}`,
|
|
220
|
+
...(mirrors.length ? { mirrors } : {}),
|
|
220
221
|
...(signer ? { mintPubkey: signer.pubkey } : {}),
|
|
221
222
|
// The human layer: who runs this, how to reach them, the terms,
|
|
222
223
|
// and today's message. Absent unless the operator set it.
|
|
@@ -294,7 +295,7 @@ export const createMoneyer = async (config, deps = {}) => {
|
|
|
294
295
|
kind: ANNOUNCE_KIND,
|
|
295
296
|
created_at: Math.floor(Date.now() / 1000),
|
|
296
297
|
tags: [['d', ANNOUNCE_D_TAG]],
|
|
297
|
-
content: announcementContent(mintAddressDocument(config.publicOrigin, config.username), config.signingKey)
|
|
298
|
+
content: announcementContent(mintAddressDocument(config.publicOrigin, config.username, config.onionUrl ? [config.onionUrl] : []), config.signingKey)
|
|
298
299
|
}, hexToBytes(config.zap.nostrKey));
|
|
299
300
|
const { ok, failed } = await nostr.publish(config.zap.relays, event);
|
|
300
301
|
log(`mint announced to ${ok.length} relay${ok.length === 1 ? '' : 's'}${failed.length ? `, ${failed.length} refused` : ''}`);
|
|
@@ -311,6 +312,14 @@ export const createMoneyer = async (config, deps = {}) => {
|
|
|
311
312
|
const origin = onionHost && askedFor === onionHost
|
|
312
313
|
? config.onionUrl
|
|
313
314
|
: (config.publicOrigin ?? `http://${req.headers.host ?? '127.0.0.1'}`);
|
|
315
|
+
// This mint's OTHER doors. A note is keyed by sha256(k1) and not by
|
|
316
|
+
// host, so every one of these serves the same notes - but a note
|
|
317
|
+
// travels as a URL, and a URL names one host. An onion note handed to
|
|
318
|
+
// somebody without Tor is unspendable to them, and a clearnet note
|
|
319
|
+
// redeemed over Tor is only private if their wallet proxies. Saying
|
|
320
|
+
// which other host answers is what lets a wallet move a note between
|
|
321
|
+
// them instead of the holder finding out it cannot.
|
|
322
|
+
const mirrors = [config.publicOrigin, config.onionUrl].filter((candidate) => Boolean(candidate) && candidate !== origin);
|
|
314
323
|
const host = new URL(origin).host;
|
|
315
324
|
const send = (body, status = 200) => {
|
|
316
325
|
res.writeHead(status, {
|
|
@@ -555,7 +564,7 @@ export const createMoneyer = async (config, deps = {}) => {
|
|
|
555
564
|
if (lnurlwMatch) {
|
|
556
565
|
if (!knownUser(lnurlwMatch[1]))
|
|
557
566
|
return fail('Unknown user.', 404);
|
|
558
|
-
return send(mintAddressDocument(origin, lnurlwMatch[1]));
|
|
567
|
+
return send(mintAddressDocument(origin, lnurlwMatch[1], mirrors));
|
|
559
568
|
}
|
|
560
569
|
// ---- LUD-06 pay callback: issue a mint invoice ----
|
|
561
570
|
if (requestUrl.pathname === '/p/cb') {
|
|
@@ -725,6 +734,10 @@ export const createMoneyer = async (config, deps = {}) => {
|
|
|
725
734
|
// signing keys. Without it a wallet that only ever received notes
|
|
726
735
|
// cannot tell an announced key rotation from a substituted key.
|
|
727
736
|
payLink: `${origin}/.well-known/lnurlp/${config.username}`,
|
|
737
|
+
// Where else this same note can be redeemed. A holder with nothing
|
|
738
|
+
// but a note learns from the note itself that the other door
|
|
739
|
+
// exists.
|
|
740
|
+
...(mirrors.length ? { mirrors } : {}),
|
|
728
741
|
...(signer ? { mintPubkey: signer.pubkey } : {})
|
|
729
742
|
});
|
|
730
743
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forgesworn/moneyer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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",
|