@ghostlytools/cli 1.3.0 → 1.3.2

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.
Files changed (81) hide show
  1. package/README.md +9 -3
  2. package/dist/assets/{api-BiaX6l6f.js → api-SYPJgFRU.js} +2 -2
  3. package/dist/assets/{api-CrxVxtY1.js → api-aup3Amud.js} +141 -40
  4. package/dist/assets/api-aup3Amud.js.map +1 -0
  5. package/dist/assets/{apiKit-CajiDxdK.js → apiKit-DYkVEs6r.js} +30 -30
  6. package/dist/assets/apiKit-DYkVEs6r.js.map +1 -0
  7. package/dist/assets/{apps-t23gkc28.js → apps-DcZKu29s.js} +1 -1
  8. package/dist/assets/{apps-CbfiCSIw.js → apps-Rbk2HHXb.js} +3 -4
  9. package/dist/assets/{apps-CbfiCSIw.js.map → apps-Rbk2HHXb.js.map} +1 -1
  10. package/dist/assets/{arkade-D4BVJFEQ.js → arkade-CuWlNnCz.js} +4 -4
  11. package/dist/assets/arkade-CuWlNnCz.js.map +1 -0
  12. package/dist/assets/{backup-BAMi2lAq.js → backup-CJWLDHuS.js} +8 -8
  13. package/dist/assets/backup-CJWLDHuS.js.map +1 -0
  14. package/dist/assets/{backup-Cg52WeQi.js → backup-CSMw2GHv.js} +1 -1
  15. package/dist/assets/bark_ffi_wasm_bg-EY2uc0Bx.js.map +1 -1
  16. package/dist/assets/bitcoind-CZchoh43.js +349 -0
  17. package/dist/assets/bitcoind-CZchoh43.js.map +1 -0
  18. package/dist/assets/bitcoindProvider-DGw3xBSa.js +97 -0
  19. package/dist/assets/bitcoindProvider-DGw3xBSa.js.map +1 -0
  20. package/dist/assets/bitcoindevkit_bg-B-iLWrmX.js.map +1 -1
  21. package/dist/assets/buttons-CGRqZ7z0.js +2 -0
  22. package/dist/assets/{buttons-CX1pKB0j.js → buttons-QEnAGWoC.js} +4 -5
  23. package/dist/assets/{buttons-CX1pKB0j.js.map → buttons-QEnAGWoC.js.map} +1 -1
  24. package/dist/assets/{endpoint-Dn7H8qE1.js → endpoint-BOTFMpfS.js} +44 -12
  25. package/dist/assets/endpoint-BOTFMpfS.js.map +1 -0
  26. package/dist/assets/{envelope-BvU0fLEt.js → envelope-K27mzoyu.js} +2 -2
  27. package/dist/assets/{envelope-BvU0fLEt.js.map → envelope-K27mzoyu.js.map} +1 -1
  28. package/dist/assets/{errors-my6VJR_0.js → errors-DO5z6h_l.js} +29 -1
  29. package/dist/assets/errors-DO5z6h_l.js.map +1 -0
  30. package/dist/assets/fedimint_client_wasm_bg-BtHHlFpp.js.map +1 -1
  31. package/dist/assets/groupFiles-DHTrTA4I.js +3229 -0
  32. package/dist/assets/groupFiles-DHTrTA4I.js.map +1 -0
  33. package/dist/assets/{host-Cmd205Dc.js → host-C7hA-Za7.js} +135 -42
  34. package/dist/assets/host-C7hA-Za7.js.map +1 -0
  35. package/dist/assets/{hyperdhtRelay-CEmBpT0Q.js → hyperdhtRelay-BVoWY2ua.js} +2 -3
  36. package/dist/assets/{hyperdhtRelay-CEmBpT0Q.js.map → hyperdhtRelay-BVoWY2ua.js.map} +1 -1
  37. package/dist/assets/{irohWeb--YyEA-hX.js → irohWeb-BJPk9bMn.js} +2 -3
  38. package/dist/assets/{irohWeb--YyEA-hX.js.map → irohWeb-BJPk9bMn.js.map} +1 -1
  39. package/dist/assets/{main-BzNN6bkx.js → main-DC8cUkDx.js} +142 -35
  40. package/dist/assets/main-DC8cUkDx.js.map +1 -0
  41. package/dist/assets/{mainline-D5D5C-jT.js → mainline-CxbxpoU4.js} +2 -2
  42. package/dist/assets/{mainline-D5D5C-jT.js.map → mainline-CxbxpoU4.js.map} +1 -1
  43. package/dist/assets/{networkReason-DxZWHw6v.js → networkReason-D42iNuUB.js} +2 -2
  44. package/dist/assets/{networkReason-DxZWHw6v.js.map → networkReason-D42iNuUB.js.map} +1 -1
  45. package/dist/assets/nwc-Cm4sYgLv.js +612 -0
  46. package/dist/assets/nwc-Cm4sYgLv.js.map +1 -0
  47. package/dist/assets/nwcProvider-0D7vdPDd.js +77 -0
  48. package/dist/assets/nwcProvider-0D7vdPDd.js.map +1 -0
  49. package/dist/assets/{profiles-B6F4oa9K.js → profiles-BHc9pEYg.js} +2 -2
  50. package/dist/assets/{profiles-B6F4oa9K.js.map → profiles-BHc9pEYg.js.map} +1 -1
  51. package/dist/assets/registry-CLolR0kM.js +2 -0
  52. package/dist/assets/{groupFiles-BNTSDaoq.js → registry-CVdSZW5l.js} +19 -3911
  53. package/dist/assets/registry-CVdSZW5l.js.map +1 -0
  54. package/dist/assets/resolve-BnlEajYz.js +591 -0
  55. package/dist/assets/resolve-BnlEajYz.js.map +1 -0
  56. package/dist/assets/{server-CBCEYKMN.js → server-CJNEKxzE.js} +284 -1202
  57. package/dist/assets/server-CJNEKxzE.js.map +1 -0
  58. package/dist/assets/{src-DhfGgAUu.js → src-yMUQvdyL.js} +2264 -71
  59. package/dist/assets/src-yMUQvdyL.js.map +1 -0
  60. package/dist/assets/{turnRefs-ChEfgbSX.js → turnRefs-B2a8to9q.js} +2 -2
  61. package/dist/assets/{turnRefs-ChEfgbSX.js.map → turnRefs-B2a8to9q.js.map} +1 -1
  62. package/dist/assets/types-C4_KafL_.js +122 -0
  63. package/dist/assets/types-C4_KafL_.js.map +1 -0
  64. package/dist/assets/{usdt-C1Xao2xH.js → usdt-DesFeDiG.js} +3 -3
  65. package/dist/assets/{usdt-C1Xao2xH.js.map → usdt-DesFeDiG.js.map} +1 -1
  66. package/dist/ghostly.mjs +1 -1
  67. package/package.json +1 -1
  68. package/dist/assets/api-CrxVxtY1.js.map +0 -1
  69. package/dist/assets/apiKit-CajiDxdK.js.map +0 -1
  70. package/dist/assets/arkade-D4BVJFEQ.js.map +0 -1
  71. package/dist/assets/backup-BAMi2lAq.js.map +0 -1
  72. package/dist/assets/buttons-CWl2yNJu.js +0 -2
  73. package/dist/assets/endpoint-Dn7H8qE1.js.map +0 -1
  74. package/dist/assets/errors-my6VJR_0.js.map +0 -1
  75. package/dist/assets/groupFiles-BNTSDaoq.js.map +0 -1
  76. package/dist/assets/groupFiles-DBbjOLuD.js +0 -2088
  77. package/dist/assets/groupFiles-DBbjOLuD.js.map +0 -1
  78. package/dist/assets/host-Cmd205Dc.js.map +0 -1
  79. package/dist/assets/main-BzNN6bkx.js.map +0 -1
  80. package/dist/assets/server-CBCEYKMN.js.map +0 -1
  81. package/dist/assets/src-DhfGgAUu.js.map +0 -1
@@ -1,2185 +1,10 @@
1
- import { Ft as toBase64Url, Lt as utf8Decode, Nt as randomBytes, Rt as utf8Encode, a as fetchesByItself, d as readGroupWantNo, i as GROUP_WANT_NO_FRAME, jt as fromBase64Url, l as readGroupHave, n as GROUP_HAVE_FRAME, o as groupFileFallback, s as isGroupMessageId, t as GROUP_FILE_LIMITS, u as readGroupWant, w as fileMessageText } from "./groupFiles-DBbjOLuD.js";
2
- import { Ai as IdentityCheckUnavailable, An as pubkyProofPath, At as verifyAtprotoRecordProof, Bt as didWebDocumentUrl, Cn as PUBKY_PROOF_MAX_BYTES, Ct as atprotoProofRkey, Dn as normalizePubkyKey, Dt as parseAtprotoDidDocument, En as newPubkyProofFolder, Et as normalizeAtprotoHandle, Fn as decodeBolt11, Ft as checkDidJws, Gt as normalizeDid, Ht as didWebFileNames, It as checkDidSignature, Jn as engineText, Jt as parseDidSignaturePaste, Kt as parseDid, Lt as didGhostlyServiceEntry, Mi as boundVerifiedIdentity, Mn as pubkyHomeserverOf, Mt as DID_FILE_MAX_BYTES, Nn as requestLnurlInvoice, On as pubkyProofCapability, Ot as shortAtprotoDid, Pn as resolveLightningDestination, Pt as DidError, Qs as openRelayPayload, Rt as didJwsAlg, Si as wellKnownUrl, Sr as ChatFiles, St as atprotoProofRecord, Tn as isPubkyProofFolder, Tt as isAtprotoDid, Ut as didWebFileUrl, Vt as didWebFile, Wt as didWebHost, Xt as staticDidDocument, Ys as RELAY_PAYLOAD_MAX_BYTES, Yt as signingKeys, Zt as strictBase64Url, _i as normalizeDomain, _n as walletNetworkOf, _t as ATPROTO_DECLARED_SCOPE, bi as recordsName, bt as ATPROTO_PROOF_SCOPE, ct as SshSigError, di as dnsProofName, dt as sameSshKey, ea as RateWindow, fi as domainTxtRecord, fn as WALLET_NETWORKS, ft as sshSignCommand, gi as nip05RootKey, gt as decodeAnyBitcoinAddress, hi as isPublicAddress, ht as BitcoinAddressError, jn as homeserverWebEndpoint, jt as DID_DOCUMENT_MAX_BYTES$1, kn as pubkyProofFile, kt as AtprotoProofError, li as DOMAIN_FILE_MAX_BYTES, lt as parseSshPublicKey, mi as encodeDnsQuery, mt as verifyBitcoinMessage, nn as resolveDidDht, pi as domainWellKnownFile, pt as verifySshSignature, qn as engineError, qt as parseDidDocument, st as SSHSIG_NAMESPACE, ui as decodeDnsResponse, ut as parseSshSignature, va as replySnippet, vi as recordsFromTxt, vt as ATPROTO_FULL_SCOPE, wn as isPubkyKey, wr as transferEnded, wt as checkAtprotoProofRecord, xi as txtValue, xt as atprotoDidMethod, yi as recordsFromWellKnown, yt as ATPROTO_PROOF_COLLECTION, zt as didSignedMessages } from "./src-DhfGgAUu.js";
3
- import { c as fileStore, f as store$1, i as STORES, m as wrap, o as databaseName, p as transact } from "./idb-B5L8wuOm.js";
4
- import { a as sealSeed, i as newDeviceKey, o as unsealSeed, t as networkReason } from "./networkReason-DxZWHw6v.js";
5
- import { t as BITCOIN_INVOICE_ON_TESTNET, u as mintNetwork } from "./mints-BrlY773j.js";
6
- import { f as FILE_BYTES_STEP, o as fileBytes, s as fileBytesOf } from "./fileBytes-Di7VZtmU.js";
1
+ import { An as pubkyProofFile, At as verifyAtprotoRecordProof, Bt as didWebDocumentUrl, Cl as utf8Decode, Ct as atprotoProofRkey, Di as domainWellKnownFile, Dn as newPubkyProofFolder, Ei as domainTxtRecord, En as isPubkyProofFolder, Et as normalizeAtprotoHandle, Ft as checkDidJws, Gt as normalizeDid, Ht as didWebFileNames, Ii as wellKnownUrl, It as checkDidSignature, Jt as parseDidSignaturePaste, Kt as parseDid, Lt as didGhostlyServiceEntry, Mn as homeserverWebEndpoint, Mt as DID_FILE_MAX_BYTES, Nn as pubkyHomeserverOf, On as normalizePubkyKey, Ot as shortAtprotoDid, Pt as DidError, Rt as didJwsAlg, St as atprotoProofRecord, Ti as dnsProofName, Tn as isPubkyKey, Tt as isAtprotoDid, Ut as didWebFileUrl, Vt as didWebFile, Wi as IdentityCheckUnavailable, Wt as didWebHost, Xt as staticDidDocument, Yt as signingKeys, Zt as strictBase64Url, _t as ATPROTO_DECLARED_SCOPE, bl as randomBytes, bt as ATPROTO_PROOF_SCOPE, ct as SshSigError, dt as sameSshKey, ft as sshSignCommand, gt as decodeAnyBitcoinAddress, ht as BitcoinAddressError, ji as normalizeDomain, jn as pubkyProofPath, jt as DID_DOCUMENT_MAX_BYTES, kn as pubkyProofCapability, kt as AtprotoProofError, ll as openRelayPayload, lt as parseSshPublicKey, mt as verifyBitcoinMessage, nn as resolveDidDht, ol as RELAY_PAYLOAD_MAX_BYTES, pt as verifySshSignature, qt as parseDidDocument, st as SSHSIG_NAMESPACE, ut as parseSshSignature, vt as ATPROTO_FULL_SCOPE, wl as utf8Encode, wn as PUBKY_PROOF_MAX_BYTES, wt as checkAtprotoProofRecord, xl as toBase64Url, yl as fromBase64Url, yt as ATPROTO_PROOF_COLLECTION, zt as didSignedMessages } from "./src-yMUQvdyL.js";
2
+ import { _ as reserveAdapterIds, a as DomainCheckError, c as assertPublicHost, d as ownDid, i as verifiedAtprotoHandle, l as chosenResolver, m as registeredIdentityProviders, n as resolveAtprotoDid, o as assertDomainRecord, r as resolveAtprotoHandle, s as assertNip05, t as assertPublicServer, u as resolverById, v as boundedIdentityFetch } from "./resolve-BnlEajYz.js";
7
3
  import { ed25519 } from "@noble/curves/ed25519.js";
8
4
  import { sha256 } from "@noble/hashes/sha2.js";
9
5
  import { schnorr } from "@noble/curves/secp256k1.js";
10
- import { validateMnemonic } from "@scure/bip39";
11
- import { wordlist } from "@scure/bip39/wordlists/english.js";
12
6
  import { generateSecretKey } from "nostr-tools/pure";
13
7
  import { decode, npubEncode } from "nostr-tools/nip19";
14
- import { BunkerSigner } from "nostr-tools/nip46";
15
- import { SimplePool } from "nostr-tools/pool";
16
- //#region ../browser/src/engine/paymentAdapters/away.ts
17
- var away = /* @__PURE__ */ new Map();
18
- /** The wallets away from this device, by wallet id, with the name of their home device ("" when it has none here). */
19
- function setAwayWallets(next) {
20
- away = new Map(next);
21
- }
22
- /** The home device's name of a wallet away from this device, or undefined when it is here. */
23
- function awayFrom(id) {
24
- return away.get(id);
25
- }
26
- /** A wallet that cannot be used here: it is at home on another device. */
27
- var WalletAwayError = class extends Error {
28
- constructor(wallet, device) {
29
- super(device ? engineText("walletAway", {
30
- wallet,
31
- device
32
- }) : engineText("walletAwayUnnamed", { wallet }));
33
- this.name = "WalletAwayError";
34
- }
35
- };
36
- /**
37
- * Whether a single-writer wallet (Ark, Bark, Spark and the Breez source, Fedimint) may open its SDK now: the engine
38
- * says, from a good turn read under 60 seconds old (WISP 06 § Wallets). Every open asks, the retries of each wallet
39
- * included, so a device replaced while it was offline does not reopen one on a retry.
40
- */
41
- var turnGate = async () => true;
42
- function setSingleWriterGate(gate) {
43
- turnGate = gate ?? (async () => true);
44
- }
45
- /** Throws (the wallet tries again later, as it does when its server does not answer) without a fresh good turn read. */
46
- async function requireTurn() {
47
- if (!await turnGate().catch(() => false)) throw new Error("Checking which device is active");
48
- }
49
- /** Throws when the wallet `id` is at home on another device. */
50
- function refuseAway(id, wallet) {
51
- const device = awayFrom(id);
52
- if (device !== void 0) throw new WalletAwayError(wallet, device);
53
- }
54
- //#endregion
55
- //#region ../browser/src/engine/paymentAdapters/modeGate.ts
56
- /** A wallet of the mode being left stopped opening: the switch goes ahead without waiting for its network. */
57
- var ModeChanged = class extends Error {
58
- constructor() {
59
- super("The wallets switched mode");
60
- this.name = "ModeChanged";
61
- }
62
- };
63
- var networkLabel = (network) => network === "mainnet" ? "Mainnet" : "Testnet";
64
- /**
65
- * What was asked of one network's wallet belongs to the other network's (a backup, a chain): nothing was changed.
66
- * `network` is where it belongs, so the engine can hand it to that wallet instead.
67
- */
68
- var WrongNetworkError = class extends Error {
69
- network;
70
- constructor(network, message) {
71
- super(message);
72
- this.network = network;
73
- this.name = "WrongNetworkError";
74
- }
75
- };
76
- /**
77
- * Opening a wallet waits on its network (an Ark server, an RPC), which can take minutes or never answer.
78
- * A mode switch must not wait behind that: `within` gives up on the wait as soon as the mode changes, and
79
- * whatever still arrives afterwards is handed to `dispose`. Nothing is saved before that wait ends, so
80
- * giving up leaves the profile as it was. Every wait on the network inside a wallet's queue goes through it,
81
- * reading a balance too, or the switch, queued behind it, waits anyway.
82
- */
83
- var ModeGate = class {
84
- target;
85
- constructor(target = "mainnet") {
86
- this.target = target;
87
- }
88
- controller = new AbortController();
89
- /** The mode its owner works in, once it says so: a wait that starts while a switch away from it is queued ends at once. */
90
- current;
91
- /** A switch to `mode` was asked for: waits for a wallet of another mode end now. */
92
- switching(mode) {
93
- if (this.target === mode) return;
94
- this.target = mode;
95
- this.controller.abort();
96
- this.controller = new AbortController();
97
- }
98
- /** The owner's switch to `mode` is applied (its queue reached it): waits are for this mode again. */
99
- entered(mode) {
100
- this.current = mode;
101
- }
102
- /**
103
- * A creation took too long: the waits under way end now, and so does every wait that starts until `resume`
104
- * (a creation still sealing its seed has not reached its server yet, and must not wait there either).
105
- */
106
- interrupt() {
107
- this.cut = true;
108
- this.controller.abort();
109
- this.controller = new AbortController();
110
- }
111
- resume() {
112
- this.cut = false;
113
- }
114
- cut = false;
115
- /** The wallet stopped: every wait ends now, and any later one at once. */
116
- close() {
117
- this.closed = true;
118
- this.controller.abort();
119
- }
120
- closed = false;
121
- within(work, dispose) {
122
- const signal = this.controller.signal;
123
- return new Promise((resolve, reject) => {
124
- const give = () => {
125
- reject(new ModeChanged());
126
- work.then((value) => dispose?.(value), () => {});
127
- };
128
- if (this.closed || this.cut || this.current && this.current !== this.target) return give();
129
- signal.addEventListener("abort", give, { once: true });
130
- work.then((value) => {
131
- signal.removeEventListener("abort", give);
132
- if (!signal.aborted) resolve(value);
133
- }, (error) => {
134
- signal.removeEventListener("abort", give);
135
- if (!signal.aborted) reject(error);
136
- });
137
- });
138
- }
139
- };
140
- //#endregion
141
- //#region ../browser/src/shared/ice.ts
142
- /**
143
- * What is wrong with a TURN/STUN server, or null. Browsers refuse to create any WebRTC connection when
144
- * one server in the list is malformed, so a bad entry here would silently cut off calls and files.
145
- */
146
- function iceServerProblem(server) {
147
- if (typeof server?.urls !== "string") return "Each ICE server needs urls: one address, or several comma-separated (turn:host:port)";
148
- const urls = server.urls.split(/[\s,]+/).filter(Boolean);
149
- if (!urls.length) return "Enter the TURN server address";
150
- for (const url of urls) {
151
- if (!/^(stun|turns?):[^\s/?#]+(\?transport=(udp|tcp))?$/i.test(url)) return `“${url}” is not a TURN or STUN address (turn:host:port)`;
152
- if (/^turns?:/i.test(url) && (!server.username?.trim() || !server.credential?.trim())) return "A TURN server needs its username and credential";
153
- }
154
- return null;
155
- }
156
- /**
157
- * A server as WebRTC writes it (`urls` a list, as every TURN guide does) in this setting's own form: the addresses
158
- * comma-separated. Anything else is left as it is, for `iceServerProblem` to say what is wrong.
159
- */
160
- function iceServerSetting(server) {
161
- const urls = server?.urls;
162
- if (!Array.isArray(urls) || !urls.every((url) => typeof url === "string")) return server;
163
- return {
164
- ...server,
165
- urls: urls.join(", ")
166
- };
167
- }
168
- //#endregion
169
- //#region ../browser/src/engine/paymentAdapters/providers/breezDatabases.ts
170
- var netOf = (network) => network === "bitcoin" || network === "mainnet" ? "mainnet" : "regtest";
171
- var hex$2 = (bytes) => Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
172
- var digest = (text) => hex$2(sha256(new TextEncoder().encode(text))).slice(0, 16);
173
- /** What a Breez database name looks like, old or new: nothing about the seed or the profile shows in it. */
174
- var BREEZ_DATABASE = /^ghostly-breez-(mainnet|regtest)-[0-9a-f]{16}$/;
175
- /** The name every profile of the origin used for this phrase before databases were per profile. */
176
- var breezLegacyName = (network, mnemonic) => `ghostly-breez-${netOf(network)}-${digest(`ghostly-breez:${netOf(network)}:${mnemonic}`)}`;
177
- /** The name of this phrase's Breez database in one profile (`space`: the profile's peer database, WISP 04). */
178
- var breezDatabaseName = (network, mnemonic, space = databaseName()) => `ghostly-breez-${netOf(network)}-${digest(`ghostly-breez:${space}:${netOf(network)}:${mnemonic}`)}`;
179
- /**
180
- * The databases the SDK keeps under one storage name. SDK 0.26 roots them there: `<name>/<network>/<identity>` and
181
- * `<name>/<network>/<identity>-tree`, the identity read from the seed inside the SDK. Older builds kept `<name>`
182
- * itself. A browser without `indexedDB.databases()` cannot list them: only the bare names are known there.
183
- */
184
- async function breezDatabaseFiles(name) {
185
- const files = [name, `${name}-tree`];
186
- if (typeof indexedDB.databases !== "function") return files;
187
- const under = (await indexedDB.databases()).map((d) => d.name ?? "").filter((file) => file.startsWith(`${name}/`));
188
- return [.../* @__PURE__ */ new Set([...files, ...under])];
189
- }
190
- var REGISTER = "ghostly-breez";
191
- var STORE = "databases";
192
- var request = (r) => new Promise((resolve, reject) => {
193
- r.onsuccess = () => resolve(r.result);
194
- r.onerror = () => reject(r.error);
195
- });
196
- function openRegister() {
197
- return new Promise((resolve, reject) => {
198
- const open = indexedDB.open(REGISTER, 1);
199
- open.onupgradeneeded = () => {
200
- if (!open.result.objectStoreNames.contains(STORE)) open.result.createObjectStore(STORE);
201
- };
202
- open.onsuccess = () => resolve(open.result);
203
- open.onerror = () => reject(open.error);
204
- open.onblocked = () => reject(/* @__PURE__ */ new Error("The Breez register is blocked"));
205
- });
206
- }
207
- async function withRegister(mode, run) {
208
- const db = await openRegister();
209
- try {
210
- const tx = db.transaction(STORE, mode);
211
- const done = new Promise((resolve, reject) => {
212
- tx.oncomplete = () => resolve();
213
- tx.onerror = tx.onabort = () => reject(tx.error);
214
- });
215
- const result = await run(tx.objectStore(STORE));
216
- await done;
217
- return result;
218
- } finally {
219
- db.close();
220
- }
221
- }
222
- /**
223
- * Whether the SDK keeps a wallet under this storage name on this device (`breezDatabaseFiles`). A browser that cannot
224
- * list databases is answered yes: the first profile then takes the old name whether or not it is there, which is
225
- * safe, since only one profile ever has it.
226
- */
227
- async function exists(name) {
228
- if (typeof indexedDB.databases !== "function") return true;
229
- return (await indexedDB.databases()).some((d) => d.name === name || !!d.name?.startsWith(`${name}/`));
230
- }
231
- /**
232
- * The Breez database this profile opens for this phrase: the old shared one when this profile owns it (or is the
233
- * first to claim it), else the profile's own. Noted in the register either way. A device whose IndexedDB cannot be
234
- * read gets the profile's own name: never a database another profile may use.
235
- */
236
- async function breezDatabase(network, mnemonic, space = databaseName()) {
237
- const fresh = breezDatabaseName(network, mnemonic, space);
238
- if (typeof indexedDB === "undefined") return fresh;
239
- const legacy = breezLegacyName(network, mnemonic);
240
- try {
241
- const legacyHere = await exists(legacy);
242
- return await withRegister("readwrite", async (store) => {
243
- const [owner, own] = await Promise.all([request(store.get(legacy)), request(store.get(fresh))]);
244
- if (owner?.space === space) return legacy;
245
- if (!own && !owner && legacyHere) {
246
- store.put({
247
- name: legacy,
248
- space,
249
- network: netOf(network),
250
- legacy: true
251
- }, legacy);
252
- return legacy;
253
- }
254
- if (!own) store.put({
255
- name: fresh,
256
- space,
257
- network: netOf(network)
258
- }, fresh);
259
- return fresh;
260
- });
261
- } catch {
262
- return fresh;
263
- }
264
- }
265
- /** The names this profile has noted for this phrase (nothing is claimed): what removing the wallet deletes. */
266
- async function breezDatabasesFor(network, mnemonic, space = databaseName()) {
267
- const names = [breezLegacyName(network, mnemonic), breezDatabaseName(network, mnemonic, space)];
268
- return (await breezDatabasesOf(space)).filter((name) => names.includes(name));
269
- }
270
- /**
271
- * The name this profile uses for this phrase, read without claiming anything: the one it noted (`breezDatabase`), else
272
- * the profile's own name. What a handoff deletes and compares (WISP 06 § Wallets): never the old shared database of
273
- * another profile that has not opened the phrase since the change.
274
- */
275
- async function breezDatabaseInUse(network, mnemonic, space = databaseName()) {
276
- return (await breezDatabasesFor(network, mnemonic, space))[0] ?? breezDatabaseName(network, mnemonic, space);
277
- }
278
- /** Every Breez database name noted for a profile of this device. */
279
- async function breezDatabasesOf(space) {
280
- if (typeof indexedDB === "undefined") return [];
281
- try {
282
- return (await withRegister("readonly", (store) => request(store.getAll()))).filter((e) => e?.space === space && BREEZ_DATABASE.test(e.name)).map((e) => e.name);
283
- } catch {
284
- return [];
285
- }
286
- }
287
- var deleteDatabase = (name) => new Promise((resolve) => {
288
- try {
289
- const r = indexedDB.deleteDatabase(name);
290
- r.onsuccess = r.onerror = r.onblocked = () => resolve();
291
- } catch {
292
- resolve();
293
- }
294
- });
295
- var deleteStrictly = (name) => new Promise((resolve, reject) => {
296
- const r = indexedDB.deleteDatabase(name);
297
- r.onsuccess = () => resolve();
298
- r.onerror = () => reject(r.error ?? /* @__PURE__ */ new Error(`${name} was not deleted`));
299
- r.onblocked = () => reject(/* @__PURE__ */ new Error(`${name} is still open`));
300
- });
301
- /**
302
- * Deletes one Breez wallet's databases (all the SDK keeps under the name) and its note in the register. Anything else
303
- * is refused. `strict` (a handoff, WISP 06 § Wallets): a delete that is blocked or fails rejects and the note stays,
304
- * so the caller tries again.
305
- */
306
- async function dropBreezDatabase(name, options = {}) {
307
- if (!BREEZ_DATABASE.test(name) || typeof indexedDB === "undefined") return;
308
- for (const file of await breezDatabaseFiles(name).catch(() => [name, `${name}-tree`])) await (options.strict ? deleteStrictly(file) : deleteDatabase(file));
309
- await withRegister("readwrite", (store) => {
310
- store.delete(name);
311
- }).catch(() => {});
312
- }
313
- //#endregion
314
- //#region ../browser/src/engine/paymentAdapters/providers/types.ts
315
- var PROVIDER_PLATFORMS = [
316
- "web",
317
- "extension",
318
- "desktop"
319
- ];
320
- var PROVIDER_NETWORKS = [
321
- "bitcoin",
322
- "signet",
323
- "testnet",
324
- "regtest",
325
- "mutinynet"
326
- ];
327
- var networkMode = (network) => network === "bitcoin" ? "mainnet" : "testnet";
328
- /** A provider can be offered here: it runs on this platform and on a network of this mode. */
329
- var offeredIn = (descriptor, platform, mode) => descriptor.platforms.includes(platform) && descriptor.networks.some((network) => networkMode(network) === mode);
330
- var FIELD_KINDS = [
331
- "text",
332
- "url",
333
- "secret",
334
- "select",
335
- "textarea"
336
- ];
337
- /** What is stored with the profile and sent on the wire in journals: stable, short, lowercase. */
338
- var PROVIDER_ID = /^[a-z][a-z0-9-]{0,31}$/;
339
- /**
340
- * Problems with a provider descriptor, empty when it is well-formed. The registry refuses a descriptor
341
- * with any; the contract suite checks the same list, so an author sees them before the app does.
342
- */
343
- function providerDescriptorProblems(d) {
344
- const problems = [];
345
- if (!PROVIDER_ID.test(d.id ?? "")) problems.push("id must match ^[a-z][a-z0-9-]{0,31}$");
346
- if (!d.label?.trim()) problems.push("label is empty");
347
- if (d.kind !== "lightning" && d.kind !== "onchain") problems.push("kind must be lightning or onchain");
348
- if (!d.description?.trim()) problems.push("description is empty: say what it is and who holds the money");
349
- if (!Array.isArray(d.networks) || !d.networks.length) problems.push("no networks");
350
- else for (const n of d.networks) if (!PROVIDER_NETWORKS.includes(n)) problems.push(`unknown network ${String(n)}`);
351
- if (!Array.isArray(d.platforms) || !d.platforms.length) problems.push("no platforms");
352
- else for (const p of d.platforms) if (!PROVIDER_PLATFORMS.includes(p)) problems.push(`unknown platform ${String(p)}`);
353
- if (!Array.isArray(d.fields)) problems.push("fields must be a list (empty when there is nothing to configure)");
354
- else {
355
- const names = d.fields.map((f) => f.name);
356
- if (new Set(names).size !== names.length) problems.push("field names are not unique");
357
- for (const f of d.fields) {
358
- if (!/^[a-zA-Z][a-zA-Z0-9_-]{0,63}$/.test(f.name ?? "")) problems.push(`field ${String(f.name)} has an invalid name`);
359
- if (!f.label?.trim()) problems.push(`field ${f.name} has no label`);
360
- if (!FIELD_KINDS.includes(f.kind)) problems.push(`field ${f.name} has an unknown kind`);
361
- if (f.kind === "select" && !f.options?.length) problems.push(`field ${f.name} is a select without options`);
362
- if (f.suggestions !== void 0 && (!Array.isArray(f.suggestions) || f.suggestions.some((o) => typeof o?.value !== "string" || typeof o?.label !== "string"))) problems.push(`field ${f.name} has malformed suggestions`);
363
- if (f.changeable && (f.kind === "secret" || f.kind === "select")) problems.push(`field ${f.name} cannot be changeable: only a server address can`);
364
- }
365
- }
366
- if (typeof d.create !== "function") problems.push("create is not a function");
367
- if (d.validate !== void 0 && typeof d.validate !== "function") problems.push("validate is not a function");
368
- return problems;
369
- }
370
- /** Only these fields reach the UI, whatever else a provider module keeps on its descriptor. */
371
- var describeProvider = ({ id, label, kind, description, networks, platforms, fields, custodial, experimental }) => ({
372
- id,
373
- label,
374
- kind,
375
- description,
376
- networks,
377
- platforms,
378
- fields,
379
- custodial,
380
- experimental
381
- });
382
- /**
383
- * The provider proved that nothing left the wallet (refused before sending, no route found before any
384
- * HTLC, a transaction that was never broadcast). Safe to try again, with this or another source.
385
- *
386
- * Any other error from a spend means the outcome is UNKNOWN: the payment may be in flight or done. It is
387
- * recorded as such and only reconciled (asked about), never retried. When in doubt, do not throw this.
388
- */
389
- var NothingSpentError = class extends Error {
390
- constructor(message) {
391
- super(message);
392
- this.name = "NothingSpentError";
393
- }
394
- };
395
- /**
396
- * The source could not be reached: no answer, a time-out, a refused connection, a server error. The
397
- * engine keeps trying by itself (with backoff) and shows the source as connecting meanwhile.
398
- */
399
- var SourceUnreachableError = class extends Error {
400
- constructor(message, cause) {
401
- super(message);
402
- this.name = "SourceUnreachableError";
403
- if (cause !== void 0) Object.assign(this, { cause });
404
- }
405
- };
406
- /**
407
- * A setting of the source is wrong (a server on another network, a phrase that makes no wallet): trying
408
- * again changes nothing until the setting does. The engine shows it as unavailable at once.
409
- */
410
- var SourceConfigError = class extends Error {
411
- constructor(message, cause) {
412
- super(message);
413
- this.name = "SourceConfigError";
414
- if (cause !== void 0) Object.assign(this, { cause });
415
- }
416
- };
417
- var sourceProblem = (error) => error instanceof Error && error.name === "SourceUnreachableError" ? "unreachable" : error instanceof Error && error.name === "SourceConfigError" ? "config" : "other";
418
- /**
419
- * Whether an error says nothing was spent. By name, not by class: a provider built outside the app (an
420
- * SDK plugin bundled with its own copy of `NothingSpentError`) must be understood the same way.
421
- */
422
- var isNothingSpentError = (error) => error instanceof NothingSpentError || error instanceof Error && error.name === "NothingSpentError";
423
- /**
424
- * An error message fit to show and store: every secret the source was given is blanked out, in case a
425
- * library echoed a URI or a key back.
426
- */
427
- function redact$1(error, secrets = {}) {
428
- let message = error instanceof Error ? error.message : String(error);
429
- for (const value of Object.values(secrets)) if (value && value.length >= 4) message = message.split(value).join("•••");
430
- return message.slice(0, 300);
431
- }
432
- //#endregion
433
- //#region ../browser/src/engine/paymentAdapters/providers/cashuMint.ts
434
- var CASHU_MINT_SOURCE = "cashu-mint";
435
- var parseRef = (ref) => {
436
- const parsed = JSON.parse(ref ?? "null");
437
- if (!parsed?.mint || !parsed.quote) throw new Error("Unknown Cashu quote");
438
- return parsed;
439
- };
440
- /**
441
- * Lightning through the Cashu mints: invoices are mint quotes that turn into ecash once paid, and
442
- * invoices are paid by melting ecash. The default source, and what Lightning was before sources.
443
- *
444
- * The Cashu wallet already polls its quotes and pending melts, mints and books them and says so
445
- * (`onQuotePaid`, `onMeltResolved`): the engine forwards those, and never polls this source itself.
446
- */
447
- var CashuMintLightning = class {
448
- wallet;
449
- mode;
450
- capabilities = {
451
- receive: true,
452
- send: true,
453
- balance: true,
454
- lookup: true
455
- };
456
- settlesItself = true;
457
- /** The melt quote shown to the person is the one paid: quoting again could change the fee. */
458
- quotes = /* @__PURE__ */ new Map();
459
- constructor(wallet, mode) {
460
- this.wallet = wallet;
461
- this.mode = mode;
462
- }
463
- async info() {
464
- return {
465
- network: this.mode === "mainnet" ? "bitcoin" : "testnet",
466
- balance: (await this.wallet.view(this.mode)).balance
467
- };
468
- }
469
- async createInvoice(amount) {
470
- const quote = await this.wallet.receiveLightning(amount, void 0, this.mode);
471
- const decoded = decodeBolt11(quote.invoice);
472
- if (!decoded?.paymentHash) throw new Error("The mint returned an invoice that cannot be read");
473
- return {
474
- invoice: quote.invoice,
475
- paymentHash: decoded.paymentHash,
476
- amount,
477
- expiresAt: quote.expiresAt ?? decoded.expiresAt * 1e3,
478
- ref: JSON.stringify({
479
- mint: quote.mint,
480
- quote: quote.quote
481
- })
482
- };
483
- }
484
- async invoiceStatus(invoice) {
485
- const { mint, quote } = parseRef(invoice.ref);
486
- const state = await this.wallet.isHeld(quote) ? "UNPAID" : await this.wallet.mintQuoteState(mint, quote);
487
- if (state === "PAID" || state === "ISSUED") return {
488
- state: "paid",
489
- amount: invoice.amount
490
- };
491
- return { state: invoice.expiresAt < Date.now() ? "expired" : "open" };
492
- }
493
- /** A real melt quote: its fee reserve is what paying may cost at most. It is kept for `payInvoice`. */
494
- async estimateFee(invoice) {
495
- const quote = await this.wallet.quoteInvoice(invoice, this.mode);
496
- this.quotes.set(invoice.trim().toLowerCase(), quote);
497
- return quote.feeReserve;
498
- }
499
- /** The quote behind a fee just shown, when there is one: `payQuote` pays exactly that. */
500
- quoteFor(invoice) {
501
- return this.quotes.get(invoice.trim().toLowerCase());
502
- }
503
- /** The melt quote `payInvoice` pays, when its fee was shown first: what the mint is asked about later. */
504
- paymentRef(invoice) {
505
- const quote = this.quotes.get(invoice.trim().toLowerCase());
506
- return quote ? JSON.stringify({
507
- mint: quote.mint,
508
- quote: quote.quote
509
- }) : void 0;
510
- }
511
- /**
512
- * A payment the app was cut off from (the tab closed while it was being paid). A melt written down is the wallet's
513
- * to settle and report. With none, the mint's word on the quote decides: paid, or never melted (the melt is written
514
- * down before the mint is handed anything, so an unpaid quote with no melt spent nothing).
515
- */
516
- async interruptedPayment(payment) {
517
- if (await this.wallet.meltInFlight(payment.invoice)) return "pending";
518
- if (!payment.ref) return "unknown";
519
- const { mint, quote } = parseRef(payment.ref);
520
- const state = await this.wallet.meltQuoteState(mint, quote);
521
- if (await this.wallet.meltInFlight(payment.invoice)) return "pending";
522
- return state === "PAID" ? "paid" : state === "PENDING" ? "pending" : "failed";
523
- }
524
- async payInvoice(invoice, maxFee, note) {
525
- const key = invoice.trim().toLowerCase();
526
- let quote = this.quotes.get(key);
527
- try {
528
- quote ??= await this.wallet.quoteInvoice(invoice, this.mode);
529
- if (quote.feeReserve > maxFee) throw engineError("lightningFeeTooHigh", { fee: quote.feeReserve });
530
- } catch (error) {
531
- throw new NothingSpentError(error instanceof Error ? error.message : String(error));
532
- }
533
- this.quotes.delete(key);
534
- const ref = JSON.stringify({
535
- mint: quote.mint,
536
- quote: quote.quote
537
- });
538
- let paid;
539
- try {
540
- paid = await this.wallet.payQuote(quote.quote, quote.mint, note);
541
- } catch (error) {
542
- throw new NothingSpentError(error instanceof Error ? error.message : String(error));
543
- }
544
- return paid ? {
545
- state: "paid",
546
- ref
547
- } : {
548
- state: "pending",
549
- ref
550
- };
551
- }
552
- async paymentStatus(payment) {
553
- const { mint, quote } = parseRef(payment.ref);
554
- const state = await this.wallet.meltQuoteState(mint, quote);
555
- return { state: state === "PAID" ? "paid" : state === "PENDING" ? "pending" : "failed" };
556
- }
557
- async close() {
558
- this.quotes.clear();
559
- }
560
- };
561
- var cashuMint = {
562
- id: CASHU_MINT_SOURCE,
563
- label: "Cashu mints",
564
- kind: "lightning",
565
- description: "Invoices are paid into, and paid from, your Cashu balance. The mints hold the sats.",
566
- networks: ["bitcoin", "testnet"],
567
- platforms: PROVIDER_PLATFORMS,
568
- fields: [],
569
- custodial: true,
570
- async create(_settings, host) {
571
- return new CashuMintLightning(host.cashu, host.mode);
572
- }
573
- };
574
- //#endregion
575
- //#region ../browser/src/engine/paymentAdapters/providers/sources.ts
576
- /** Where a source is stored: the mode's one, or one of several (`key`). */
577
- var sourceKey = (kind, mode, key) => `${kind}Source-${mode}${key ? `-${key}` : ""}`;
578
- var seenKey = (kind, mode, key) => `${kind}SourceSeen-${mode}${key ? `-${key}` : ""}`;
579
- /**
580
- * A source that does not answer is tried again by itself: 2 s, 4 s, 8 s… up to 5 minutes, ±20 % so
581
- * that many apps coming back online do not knock at once.
582
- */
583
- var RETRY_MAX_MS = 3e5;
584
- function retryDelay(failures, random = Math.random) {
585
- const base = Math.min(RETRY_MAX_MS, 2e3 * 2 ** Math.max(0, failures - 1));
586
- return Math.round(base * (.8 + .4 * random()));
587
- }
588
- /**
589
- * Gives up on a connection when its signal aborts (a change of settings interrupts an attempt), even if the
590
- * provider does not listen to it; what still arrives afterwards is closed.
591
- */
592
- function abortable(work, signal) {
593
- return new Promise((resolve, reject) => {
594
- const give = () => {
595
- reject(/* @__PURE__ */ new Error("The connection attempt was interrupted"));
596
- work.then((p) => p.close().catch(() => {}), () => {});
597
- };
598
- if (signal.aborted) return give();
599
- signal.addEventListener("abort", give, { once: true });
600
- work.then((value) => {
601
- signal.removeEventListener("abort", give);
602
- if (!signal.aborted) resolve(value);
603
- }, (error) => {
604
- signal.removeEventListener("abort", give);
605
- if (!signal.aborted) reject(error);
606
- });
607
- });
608
- }
609
- /**
610
- * The active source of one kind (Lightning, on-chain) of one network: Mainnet and Testnet each have their
611
- * own, both connected at once. Every change is serialized.
612
- */
613
- var ProviderSources = class {
614
- options;
615
- mode;
616
- stored;
617
- descriptor;
618
- provider;
619
- /** The connected source's secrets, only to blank them out of error messages. */
620
- secrets = {};
621
- controller = new AbortController();
622
- gate = new ModeGate();
623
- queue = Promise.resolve();
624
- connecting;
625
- timer;
626
- retry;
627
- /** Failed attempts in a row, and since when: what makes a failure sustained. */
628
- failures = 0;
629
- failingSince;
630
- seen;
631
- stopped = false;
632
- view = {
633
- mode: "mainnet",
634
- status: "none",
635
- offered: []
636
- };
637
- constructor(options) {
638
- this.options = options;
639
- this.mode = options.network;
640
- this.view = {
641
- mode: options.network,
642
- status: "none",
643
- offered: []
644
- };
645
- }
646
- serial(run) {
647
- const next = this.queue.then(run, run);
648
- this.queue = next.catch(() => {});
649
- return next;
650
- }
651
- find(id) {
652
- return this.options.descriptors().find((d) => d.id === id);
653
- }
654
- offered() {
655
- return this.options.descriptors().filter((d) => offeredIn(d, this.options.host().platform, this.mode)).map((d) => describeProvider(d));
656
- }
657
- async load(mode) {
658
- const settings = await store$1(STORES.settings, "readonly");
659
- const [stored, seen] = await Promise.all([wrap(settings.get(sourceKey(this.options.kind, mode, this.options.key))), wrap(settings.get(seenKey(this.options.kind, mode, this.options.key)))]);
660
- this.stored = stored;
661
- this.seen = seen;
662
- this.failures = 0;
663
- this.failingSince = void 0;
664
- }
665
- /** The source of the mode (stored, else the default), not connected yet: with the last balance it read. */
666
- idle() {
667
- const id = this.stored?.providerId ?? this.options.defaultId;
668
- const descriptor = id ? this.find(id) : void 0;
669
- const seen = id && this.seen?.providerId === id ? this.seen : void 0;
670
- return {
671
- mode: this.mode,
672
- offered: this.offered(),
673
- providerId: id,
674
- label: descriptor?.label ?? id,
675
- custodial: descriptor?.custodial,
676
- isDefault: !!id && id === this.options.defaultId,
677
- config: this.stored?.config,
678
- secrets: descriptor?.fields.filter((f) => f.kind === "secret" && this.stored?.secrets).map((f) => f.name),
679
- status: !id ? "none" : descriptor ? "connecting" : "error",
680
- error: id && !descriptor ? `${id} is not available in this version of Ghostly` : void 0,
681
- ...seen ? {
682
- balance: seen.balance,
683
- unconfirmed: seen.unconfirmed,
684
- balanceAt: seen.at
685
- } : {}
686
- };
687
- }
688
- async start() {
689
- await this.load(this.mode);
690
- this.view = this.idle();
691
- }
692
- /** One saved secret of the source (a handoff asks whether an LND card pins its certificate, and a Breez phrase), or undefined. */
693
- async secret(name) {
694
- const stored = this.stored;
695
- if (!stored?.secrets) return void 0;
696
- const value = JSON.parse(await unsealSeed(stored.secrets, stored.deviceKey ?? ""))[name];
697
- return typeof value === "string" && value ? value : void 0;
698
- }
699
- /** The list of providers changed (a plugin registered): the picker is told, nothing reconnects. */
700
- refreshOffered() {
701
- this.view = {
702
- ...this.view,
703
- offered: this.offered()
704
- };
705
- this.options.changed();
706
- }
707
- /**
708
- * Connects the mode's source in the background, and keeps trying while it cannot be reached: "Connecting…"
709
- * (with the last balance it read) until the failure is sustained, then "Unavailable", still retrying with
710
- * backoff. `eager` (Retry, waking up): shows "Connecting…" again while it tries.
711
- */
712
- ensureReady(eager = false) {
713
- return this.connecting ??= this.serial(() => this.connect(eager)).finally(() => {
714
- this.connecting = void 0;
715
- });
716
- }
717
- /** Retry, now: the wait before the next attempt is skipped. Resolves when the attempt ends. */
718
- retryNow() {
719
- if (this.provider || this.stopped) return Promise.resolve();
720
- clearTimeout(this.retry);
721
- return this.ensureReady(true);
722
- }
723
- /**
724
- * The app is back (in front, online, the computer woke): a source that is not connected is tried now, and
725
- * a connected one reads its balance now.
726
- */
727
- wake() {
728
- if (this.stopped) return;
729
- if (this.provider) {
730
- this.refresh();
731
- return;
732
- }
733
- if (this.failures && !this.connecting) this.retryNow();
734
- }
735
- async connect(eager = false) {
736
- clearTimeout(this.retry);
737
- if (this.stopped || this.provider) return;
738
- const id = this.stored?.providerId ?? this.options.defaultId;
739
- const descriptor = id ? this.find(id) : void 0;
740
- if (!id || !descriptor) {
741
- this.view = this.idle();
742
- this.options.changed();
743
- return;
744
- }
745
- if (eager && this.view.status === "error") {
746
- this.view = {
747
- ...this.view,
748
- status: "connecting",
749
- retryAt: void 0
750
- };
751
- this.options.changed();
752
- }
753
- const signal = this.controller.signal;
754
- let secrets = {};
755
- try {
756
- if (this.stored?.secrets) secrets = JSON.parse(await unsealSeed(this.stored.secrets, this.stored.deviceKey ?? ""));
757
- const { provider, network, alias } = await this.open(descriptor, {
758
- config: this.stored?.config ?? {},
759
- secrets
760
- });
761
- this.descriptor = descriptor;
762
- this.provider = provider;
763
- this.secrets = secrets;
764
- this.failures = 0;
765
- this.failingSince = void 0;
766
- this.view = {
767
- ...this.idle(),
768
- status: "ready",
769
- network,
770
- alias
771
- };
772
- this.options.changed();
773
- this.refresh();
774
- } catch (error) {
775
- if (this.stopped || error instanceof ModeChanged || signal.aborted) return;
776
- const now = Date.now();
777
- this.failures++;
778
- this.failingSince ??= now;
779
- const problem = sourceProblem(error);
780
- const sustained = problem === "config" || this.failures >= 3 && now - this.failingSince >= 3e4;
781
- const delay = (this.options.backoff ?? retryDelay)(this.failures);
782
- this.view = {
783
- ...this.idle(),
784
- status: sustained ? "error" : "connecting",
785
- error: `Could not connect to ${descriptor.label}: ${redact$1(error, secrets)}`,
786
- problem,
787
- failures: this.failures,
788
- retryAt: now + delay
789
- };
790
- this.options.changed();
791
- this.retry = setTimeout(() => void this.ensureReady(), delay);
792
- }
793
- }
794
- /** Creates a provider and checks it is on a network of this mode before anything uses it. */
795
- async open(descriptor, settings, controller = this.controller) {
796
- if (descriptor.id === "breez") await requireTurn();
797
- const signal = controller.signal;
798
- const provider = await this.gate.within(abortable(descriptor.create(settings, {
799
- ...this.options.host(),
800
- mode: this.mode,
801
- signal
802
- }), signal), (p) => p.close());
803
- try {
804
- const { network, alias } = await this.gate.within(provider.info());
805
- if (!descriptor.networks.includes(network)) throw new Error(`it runs on ${network}, which ${descriptor.label} does not support here`);
806
- if (networkMode(network) !== this.mode) throw new Error(this.mode === "mainnet" ? `it runs on ${network}, a test network: set it up as a Testnet wallet instead` : "it runs on Bitcoin, real money: set it up as a Mainnet wallet instead");
807
- return {
808
- provider,
809
- network,
810
- alias
811
- };
812
- } catch (error) {
813
- await provider.close().catch(() => {});
814
- throw error;
815
- }
816
- }
817
- /**
818
- * Makes `providerId` this mode's source, with the values typed in its form. It is connected and its
819
- * network checked first; only then are its settings saved (secrets sealed) and the old source closed.
820
- */
821
- set(providerId, values) {
822
- const interrupted = this.interrupt();
823
- return this.afterChange(interrupted, this.serial(async () => {
824
- const { descriptor, settings } = this.settingsOf(providerId, values);
825
- await this.guard();
826
- await this.apply(descriptor, settings, false);
827
- }));
828
- }
829
- /** The form's values as a source's settings, checked: its secrets apart. Throws what to fix. */
830
- settingsOf(providerId, values) {
831
- const descriptor = this.find(providerId);
832
- if (!descriptor || !offeredIn(descriptor, this.options.host().platform, this.mode)) throw new Error("That source is not available here");
833
- const settings = {
834
- config: {},
835
- secrets: {}
836
- };
837
- for (const field of descriptor.fields) {
838
- const value = (values[field.name] ?? "").trim();
839
- if (!value && !field.optional) throw new Error(`Enter ${field.label.toLowerCase()}`);
840
- if (field.kind === "select" && value && !field.options?.some((o) => o.value === value)) throw new Error(`Choose ${field.label.toLowerCase()}`);
841
- if (value) (field.kind === "secret" ? settings.secrets : settings.config)[field.name] = value;
842
- }
843
- descriptor.validate?.(settings, this.mode);
844
- return {
845
- descriptor,
846
- settings
847
- };
848
- }
849
- /**
850
- * Whether the saved source is this provider with these very settings (the same wallet added twice). Its secrets are
851
- * unsealed here to compare, and never leave.
852
- */
853
- async holds(providerId, settings) {
854
- const stored = this.stored;
855
- if (!stored || stored.providerId !== providerId) return false;
856
- const same = (a, b) => {
857
- const keys = Object.keys(a).sort();
858
- return JSON.stringify(keys) === JSON.stringify(Object.keys(b).sort()) && keys.every((k) => a[k] === b[k]);
859
- };
860
- if (!same(stored.config, settings.config)) return false;
861
- return same(stored.secrets ? JSON.parse(await unsealSeed(stored.secrets, stored.deviceKey ?? "")) : {}, settings.secrets);
862
- }
863
- /**
864
- * Changes the `changeable` fields (a server address) of this mode's saved source, keeping its sealed
865
- * secrets and whose money it is: the wallet stays, only where it reads the chain changes. Connected and
866
- * checked first, like `set`; a refusal leaves the saved source as it was.
867
- */
868
- reconfigure(values) {
869
- const interrupted = this.interrupt();
870
- return this.afterChange(interrupted, this.serial(async () => {
871
- const stored = this.stored;
872
- const descriptor = stored ? this.find(stored.providerId) : void 0;
873
- if (!stored || !descriptor) throw new Error("There is no saved source to change");
874
- const changeable = descriptor.fields.filter((f) => f.changeable);
875
- if (!changeable.length) throw new Error(`${descriptor.label} has no server to change`);
876
- const config = { ...stored.config };
877
- for (const field of changeable) {
878
- if (!(field.name in values)) continue;
879
- const value = (values[field.name] ?? "").trim();
880
- if (!value && !field.optional) throw new Error(`Enter ${field.label.toLowerCase()}`);
881
- if (value) config[field.name] = value;
882
- else delete config[field.name];
883
- }
884
- const settings = {
885
- config,
886
- secrets: stored.secrets ? JSON.parse(await unsealSeed(stored.secrets, stored.deviceKey ?? "")) : {}
887
- };
888
- descriptor.validate?.(settings, this.mode);
889
- await this.apply(descriptor, settings, true);
890
- }));
891
- }
892
- /** A change refused after interrupting an attempt: the saved source goes on connecting as before. */
893
- afterChange(interrupted, change) {
894
- return change.finally(() => {
895
- if (interrupted && !this.provider && !this.stopped) this.ensureReady();
896
- });
897
- }
898
- /**
899
- * A connection attempt under way (or waiting to try again) gives way to a change of settings, instead of
900
- * holding it for its time-out. Says whether there was one.
901
- */
902
- interrupt() {
903
- if (this.provider || !this.connecting && !this.failures) return false;
904
- clearTimeout(this.retry);
905
- this.controller.abort();
906
- this.controller = new AbortController();
907
- return true;
908
- }
909
- /** Connects `settings`, then saves them and replaces the current source. `same`: the same wallet, reconfigured. */
910
- async apply(descriptor, settings, same) {
911
- const providerId = descriptor.id;
912
- let opened;
913
- const controller = new AbortController();
914
- try {
915
- opened = await this.open(descriptor, settings, controller);
916
- } catch (error) {
917
- throw error instanceof ModeChanged ? error : /* @__PURE__ */ new Error(`Could not connect to ${descriptor.label}: ${redact$1(error, settings.secrets)}`);
918
- }
919
- const deviceKey = newDeviceKey();
920
- const stored = {
921
- providerId,
922
- config: settings.config,
923
- savedAt: Date.now(),
924
- ...Object.keys(settings.secrets).length ? {
925
- secrets: await sealSeed(JSON.stringify(settings.secrets), deviceKey),
926
- deviceKey
927
- } : {}
928
- };
929
- const plain = providerId === this.options.defaultId && !Object.keys(settings.config).length && !stored.secrets;
930
- if (!same) this.seen = void 0;
931
- try {
932
- await transact([STORES.settings], (s) => {
933
- if (plain) s[STORES.settings].delete(sourceKey(this.options.kind, this.mode, this.options.key));
934
- else s[STORES.settings].put(stored, sourceKey(this.options.kind, this.mode, this.options.key));
935
- if (!same) s[STORES.settings].delete(seenKey(this.options.kind, this.mode, this.options.key));
936
- });
937
- } catch (error) {
938
- await opened.provider.close().catch(() => {});
939
- throw error;
940
- }
941
- await this.disconnect();
942
- this.controller = controller;
943
- this.failures = 0;
944
- this.failingSince = void 0;
945
- this.stored = plain ? void 0 : stored;
946
- this.descriptor = descriptor;
947
- this.provider = opened.provider;
948
- this.secrets = settings.secrets;
949
- this.view = {
950
- ...this.idle(),
951
- status: "ready",
952
- network: opened.network,
953
- alias: opened.alias
954
- };
955
- this.options.changed();
956
- this.refresh();
957
- }
958
- /** Forgets this mode's source (and its sealed secrets): the default, if there is one, takes over. */
959
- clear() {
960
- this.interrupt();
961
- return this.serial(async () => {
962
- await this.guard();
963
- await transact([STORES.settings], (s) => {
964
- s[STORES.settings].delete(sourceKey(this.options.kind, this.mode, this.options.key));
965
- s[STORES.settings].delete(seenKey(this.options.kind, this.mode, this.options.key));
966
- });
967
- await this.disconnect(true);
968
- this.stored = void 0;
969
- this.seen = void 0;
970
- this.failures = 0;
971
- this.failingSince = void 0;
972
- this.view = this.idle();
973
- this.options.changed();
974
- }).then(() => this.ensureReady());
975
- }
976
- /**
977
- * Forgets this source for good (its card is removed): its settings, sealed secrets and last balance go, it is
978
- * closed, and nothing takes over. Refused while a payment through it has not ended.
979
- */
980
- forget() {
981
- this.interrupt();
982
- return this.serial(async () => {
983
- await this.guard();
984
- await transact([STORES.settings], (s) => {
985
- s[STORES.settings].delete(sourceKey(this.options.kind, this.mode, this.options.key));
986
- s[STORES.settings].delete(seenKey(this.options.kind, this.mode, this.options.key));
987
- });
988
- this.stopped = true;
989
- this.gate.close();
990
- await this.disconnect(true);
991
- this.stored = void 0;
992
- this.seen = void 0;
993
- this.view = {
994
- mode: this.mode,
995
- status: "none",
996
- offered: []
997
- };
998
- });
999
- }
1000
- async guard() {
1001
- const current = this.stored?.providerId ?? this.options.defaultId;
1002
- const refusal = current ? await this.options.refuseReplacing?.(current, this.mode) : void 0;
1003
- if (refusal) throw new Error(refusal);
1004
- }
1005
- /** The connected source, waiting for a connection under way. Throws when there is none. */
1006
- async use() {
1007
- if (!this.provider) await this.ensureReady();
1008
- if (!this.provider || !this.descriptor) throw new Error(this.view.status === "none" ? `No ${this.options.kind === "onchain" ? "Bitcoin" : "Lightning"} source is set up` : this.view.error ?? "The source is still connecting");
1009
- return {
1010
- provider: this.provider,
1011
- descriptor: this.descriptor
1012
- };
1013
- }
1014
- /** A change waiting on its connection gives up now (a creation that took too long); nothing is saved. */
1015
- cutShort() {
1016
- this.gate.interrupt();
1017
- }
1018
- resume() {
1019
- this.gate.resume();
1020
- }
1021
- /** The connected source's id, when one is connected right now. */
1022
- get activeId() {
1023
- return this.provider ? this.descriptor?.id : void 0;
1024
- }
1025
- get active() {
1026
- return this.provider;
1027
- }
1028
- async refresh() {
1029
- clearTimeout(this.timer);
1030
- const provider = this.provider;
1031
- if (!provider) return;
1032
- if (!this.options.refresh) {
1033
- if (!this.view.read) {
1034
- this.view = {
1035
- ...this.view,
1036
- read: true
1037
- };
1038
- this.options.changed();
1039
- }
1040
- return;
1041
- }
1042
- const before = this.view;
1043
- try {
1044
- const details = await this.options.refresh(provider);
1045
- if (provider !== this.provider) return;
1046
- this.view = {
1047
- ...this.view,
1048
- ...details,
1049
- read: true,
1050
- balanceAt: void 0,
1051
- error: void 0
1052
- };
1053
- this.remember(details);
1054
- } catch (error) {
1055
- if (provider !== this.provider) return;
1056
- this.view = {
1057
- ...this.view,
1058
- error: `Could not read the balance: ${redact$1(error, this.secrets)}`
1059
- };
1060
- }
1061
- const after = this.view, was = before;
1062
- if (Object.keys({
1063
- ...was,
1064
- ...after
1065
- }).some((key) => was[key] !== after[key])) this.options.changed();
1066
- if (this.provider) this.timer = setTimeout(() => void this.refresh(), this.options.refreshMs ?? 15e3);
1067
- }
1068
- /** Keeps the balance read, for the next start: written only when it changed. */
1069
- remember({ balance, unconfirmed }) {
1070
- const providerId = this.descriptor?.id;
1071
- if (!providerId || providerId === this.options.defaultId || balance === void 0 && unconfirmed === void 0) return;
1072
- const was = this.seen;
1073
- if (was?.providerId === providerId && was.balance === balance && was.unconfirmed === unconfirmed) return;
1074
- const seen = {
1075
- providerId,
1076
- balance,
1077
- unconfirmed,
1078
- at: Date.now()
1079
- }, key = seenKey(this.options.kind, this.mode, this.options.key);
1080
- this.seen = seen;
1081
- transact([STORES.settings], (s) => {
1082
- s[STORES.settings].put(seen, key);
1083
- }).catch(() => {});
1084
- }
1085
- /** `forget`: the source is removed, so what it keeps on this device goes too (a provider's own `forget`). */
1086
- async disconnect(forget = false) {
1087
- clearTimeout(this.timer);
1088
- clearTimeout(this.retry);
1089
- this.controller.abort();
1090
- this.controller = new AbortController();
1091
- const provider = this.provider;
1092
- this.provider = void 0;
1093
- this.descriptor = void 0;
1094
- this.secrets = {};
1095
- await (forget && provider?.forget ? provider.forget() : provider?.close())?.catch(() => {});
1096
- }
1097
- /** Shutting down: nothing reconnects afterwards. */
1098
- async stop() {
1099
- this.stopped = true;
1100
- this.gate.close();
1101
- await this.serial(() => this.disconnect());
1102
- }
1103
- };
1104
- //#endregion
1105
- //#region ../browser/src/engine/paymentAdapters/providers/lightningService.ts
1106
- var PREFIX = "lightningOp-";
1107
- var opKey = (direction, hash) => `${PREFIX}${direction}-${hash}`;
1108
- var RANGE = () => IDBKeyRange.bound(PREFIX, `${PREFIX}\uffff`);
1109
- /** Every Lightning operation journaled, of every network and card. */
1110
- /** Lightning payments whose call to the source has not returned, on any card of this page (WISP 06 § Payments in flight). */
1111
- var paying = 0;
1112
- var lightningPaying = () => paying;
1113
- async function readLightningJournal() {
1114
- return wrap((await store$1(STORES.settings, "readonly")).getAll(RANGE()));
1115
- }
1116
- var IN_POLL_MS = 4e3;
1117
- var OUT_POLL_MS = 15e3;
1118
- var RECENT = 20;
1119
- /** Resolved Lightning addresses are kept this long for the amount to be chosen; this many at most. */
1120
- var LNURL_TTL_MS = 6e5;
1121
- var LNURL_KEPT = 20;
1122
- /** A source that cannot tell its fee beforehand gets this ceiling, the same one chat requests are paid under. */
1123
- var defaultFeeCap = (amount) => Math.max(10, Math.ceil(amount * .03));
1124
- /**
1125
- * Lightning of one network: its Lightning card, the invoice a chat request on that network carries and paying a
1126
- * contact's invoice on it all come here, and go to this network's active source. The engine has one per network.
1127
- */
1128
- var LightningService = class {
1129
- network;
1130
- events;
1131
- options;
1132
- sources;
1133
- quotes = /* @__PURE__ */ new Map();
1134
- mode;
1135
- timer;
1136
- passes = Promise.resolve();
1137
- stopped = false;
1138
- recent = [];
1139
- /** Operations written in this run (`put`): a reconcile pass that wrote one is news, recent or not. */
1140
- writes = 0;
1141
- /** Lightning addresses and LNURLs resolved, until an amount is chosen for them. */
1142
- lnurls = /* @__PURE__ */ new Map();
1143
- constructor(network, descriptors, host, events, defaultId, options = {}) {
1144
- this.network = network;
1145
- this.events = events;
1146
- this.options = options;
1147
- this.mode = network;
1148
- this.sources = new ProviderSources({
1149
- kind: "lightning",
1150
- network,
1151
- descriptors,
1152
- host,
1153
- defaultId,
1154
- key: options.key,
1155
- changed: () => {
1156
- this.schedule(0);
1157
- events.changed();
1158
- },
1159
- refuseReplacing: (providerId, mode) => this.refusal(providerId, mode),
1160
- refresh: async (provider) => provider.capabilities.balance ? { balance: (await provider.info()).balance } : {}
1161
- });
1162
- }
1163
- /** The card this service is, when the network has several. */
1164
- get card() {
1165
- return this.options.card;
1166
- }
1167
- /** Whether a journaled operation went through this card (and this network). */
1168
- owns(op) {
1169
- if (op.mode !== this.mode) return false;
1170
- return this.options.owns ? this.options.owns(op) : op.card === void 0 || op.card === this.options.card;
1171
- }
1172
- get view() {
1173
- return {
1174
- ...this.sources.view,
1175
- capabilities: this.sources.active?.capabilities,
1176
- recent: this.recent
1177
- };
1178
- }
1179
- async start() {
1180
- await this.sources.start();
1181
- await this.loadRecent();
1182
- }
1183
- ensureReady() {
1184
- return this.sources.ensureReady();
1185
- }
1186
- async stop() {
1187
- this.stopped = true;
1188
- clearTimeout(this.timer);
1189
- await this.sources.stop();
1190
- }
1191
- refreshOffered() {
1192
- this.sources.refreshOffered();
1193
- }
1194
- async createInvoice(amount, context = {}) {
1195
- const { provider, descriptor } = await this.sources.use();
1196
- if (!provider.capabilities.receive) throw new Error(`${descriptor.label} cannot create invoices`);
1197
- const created = await provider.createInvoice(amount, context.memo ?? "Ghostly");
1198
- const decoded = decodeBolt11(created.invoice);
1199
- if (!decoded || decoded.amountSat !== amount || decoded.paymentHash && decoded.paymentHash !== created.paymentHash || created.amount !== amount) throw new Error(`${descriptor.label} returned an invoice that does not match the request`);
1200
- this.checkNetwork(decoded.network);
1201
- await this.put({
1202
- direction: "in",
1203
- providerId: descriptor.id,
1204
- mode: this.mode,
1205
- ...this.cardField(),
1206
- paymentHash: created.paymentHash,
1207
- invoice: created.invoice,
1208
- amount,
1209
- paymentId: context.paymentId,
1210
- ref: created.ref,
1211
- expiresAt: created.expiresAt,
1212
- createdAt: Date.now(),
1213
- selfSettled: !!provider.settlesItself,
1214
- state: "open"
1215
- });
1216
- this.schedule(IN_POLL_MS);
1217
- return {
1218
- ...created,
1219
- source: descriptor.id
1220
- };
1221
- }
1222
- /** The Cashu wallet minted the ecash of one of its invoices (it polls its own quotes). */
1223
- async reportInvoicePaid(invoice, context = {}) {
1224
- const hash = decodeBolt11(invoice)?.paymentHash;
1225
- const op = hash ? await this.get("in", hash) : void 0;
1226
- if (op && op.state !== "paid") await this.put({
1227
- ...op,
1228
- state: "paid",
1229
- settledAt: Date.now()
1230
- });
1231
- this.events.received({
1232
- paymentId: op?.paymentId ?? context.paymentId,
1233
- providerId: op?.providerId ?? "cashu-mint",
1234
- mint: context.mint
1235
- });
1236
- await this.loadRecent();
1237
- this.events.changed();
1238
- }
1239
- /**
1240
- * Fetches what a Lightning address or LNURL asks for, and keeps it until an amount is chosen. The
1241
- * service's domain learns of the request: the caller says so before asking.
1242
- */
1243
- async resolveDestination(text) {
1244
- const params = await resolveLightningDestination(text, { fetch: this.options.fetch });
1245
- const now = Date.now();
1246
- for (const [id, entry] of this.lnurls) if (entry.at + LNURL_TTL_MS < now) this.lnurls.delete(id);
1247
- while (this.lnurls.size >= LNURL_KEPT) this.lnurls.delete(this.lnurls.keys().next().value);
1248
- const id = crypto.randomUUID();
1249
- this.lnurls.set(id, {
1250
- params,
1251
- at: now
1252
- });
1253
- const { destination, callbackDomain, minSat, maxSat, description, commentAllowed } = params;
1254
- return {
1255
- id,
1256
- kind: destination.kind,
1257
- text: destination.text,
1258
- domain: destination.domain,
1259
- callbackDomain,
1260
- minSat,
1261
- maxSat,
1262
- description,
1263
- commentAllowed
1264
- };
1265
- }
1266
- /**
1267
- * The invoice for `amountSat` from a resolved destination: checked (the amount, a commitment to what
1268
- * was shown, not expired, a network of this mode) before it is quoted and paid like any other invoice.
1269
- */
1270
- async destinationInvoice(id, amountSat, comment) {
1271
- const entry = this.lnurls.get(id);
1272
- if (!entry || entry.at + LNURL_TTL_MS < Date.now()) {
1273
- this.lnurls.delete(id);
1274
- throw new Error("That address was resolved too long ago: check it again");
1275
- }
1276
- const { invoice, successAction } = await requestLnurlInvoice(entry.params, amountSat, comment, { fetch: this.options.fetch });
1277
- this.checkNetwork(invoice.network);
1278
- return {
1279
- invoice: invoice.invoice,
1280
- successAction,
1281
- note: entry.params.destination.text
1282
- };
1283
- }
1284
- /** Whether this card resolved that address (the invoice is asked where it was resolved). */
1285
- hasDestination(id) {
1286
- return this.lnurls.has(id);
1287
- }
1288
- /** What paying this invoice costs at most, from the active source. Nothing is spent. */
1289
- async quote(text) {
1290
- const decoded = decodeBolt11(text);
1291
- if (!decoded) throw new Error("That is not a Lightning invoice");
1292
- if (decoded.amountSat === null) throw new Error("Invoices without an amount are not supported");
1293
- if (!decoded.paymentHash) throw new Error("That invoice has no payment hash");
1294
- if (decoded.expiresAt * 1e3 < Date.now()) throw engineError("invoiceExpired");
1295
- this.checkNetwork(decoded.network);
1296
- const { provider, descriptor } = await this.sources.use();
1297
- if (this.mode === "testnet" && decoded.network === "bitcoin" && !(provider instanceof CashuMintLightning)) throw new Error(BITCOIN_INVOICE_ON_TESTNET);
1298
- if (!provider.capabilities.send) throw new Error(`${descriptor.label} cannot pay invoices`);
1299
- const maxFee = provider.estimateFee ? await provider.estimateFee(decoded.invoice, decoded.amountSat) : defaultFeeCap(decoded.amountSat);
1300
- if (!Number.isSafeInteger(maxFee) || maxFee < 0) throw new Error(`${descriptor.label} returned an invalid fee`);
1301
- const id = crypto.randomUUID();
1302
- this.quotes.set(id, {
1303
- providerId: descriptor.id,
1304
- provider,
1305
- invoice: decoded.invoice,
1306
- amount: decoded.amountSat,
1307
- maxFee,
1308
- paymentHash: decoded.paymentHash
1309
- });
1310
- return {
1311
- quote: id,
1312
- mint: (provider instanceof CashuMintLightning ? provider.quoteFor(decoded.invoice)?.mint : void 0) ?? descriptor.id,
1313
- amount: decoded.amountSat,
1314
- feeReserve: maxFee,
1315
- source: descriptor.id
1316
- };
1317
- }
1318
- hasQuote(id) {
1319
- return this.quotes.has(id);
1320
- }
1321
- /**
1322
- * Pays a quote. True once paid; false while in flight or unknown (reconciled in the background, never
1323
- * paid again). Throws only when nothing left the wallet.
1324
- */
1325
- async pay(quoteId, context = {}) {
1326
- const quote = this.quotes.get(quoteId);
1327
- if (!quote) throw new Error("This quote is no longer valid: check the invoice again");
1328
- if (this.sources.active !== quote.provider) {
1329
- this.quotes.delete(quoteId);
1330
- throw new Error("The Lightning source changed: check the invoice again");
1331
- }
1332
- this.quotes.delete(quoteId);
1333
- const existing = await this.get("out", quote.paymentHash);
1334
- if (existing && existing.state !== "failed") throw new Error(existing.state === "paid" ? "This invoice is already paid" : "This invoice is already being paid");
1335
- let op = {
1336
- direction: "out",
1337
- providerId: quote.providerId,
1338
- mode: this.mode,
1339
- ...this.cardField(),
1340
- paymentHash: quote.paymentHash,
1341
- invoice: quote.invoice,
1342
- amount: quote.amount,
1343
- paymentId: context.paymentId,
1344
- note: context.note?.slice(0, 140),
1345
- expiresAt: 0,
1346
- createdAt: Date.now(),
1347
- selfSettled: !!quote.provider.settlesItself,
1348
- state: "sending",
1349
- maxFee: quote.maxFee,
1350
- ref: quote.provider.paymentRef?.(quote.invoice)
1351
- };
1352
- await this.put(op);
1353
- paying += 1;
1354
- try {
1355
- const result = await quote.provider.payInvoice(quote.invoice, quote.maxFee, context.note);
1356
- op = result.state === "paid" ? {
1357
- ...op,
1358
- state: "paid",
1359
- fee: result.fee,
1360
- ref: result.ref ?? op.ref,
1361
- settledAt: Date.now()
1362
- } : {
1363
- ...op,
1364
- state: "pending",
1365
- ref: result.ref ?? op.ref
1366
- };
1367
- } catch (error) {
1368
- if (isNothingSpentError(error)) {
1369
- await this.put({
1370
- ...op,
1371
- state: "failed",
1372
- error: redact$1(error)
1373
- });
1374
- throw error;
1375
- }
1376
- op = {
1377
- ...op,
1378
- state: "unknown",
1379
- error: quote.provider.capabilities.lookup ? "No answer from the source. It is being checked; nothing is paid again." : "No answer from the source, and it cannot be asked: check this payment in the wallet itself. It is never paid again."
1380
- };
1381
- } finally {
1382
- paying -= 1;
1383
- }
1384
- await this.put(op);
1385
- if (op.state !== "paid") this.schedule(OUT_POLL_MS);
1386
- return op.state === "paid";
1387
- }
1388
- /** The Cashu wallet settled a melt it had left pending. */
1389
- async reportPaymentResolved(invoice, paid, context = {}) {
1390
- const hash = decodeBolt11(invoice)?.paymentHash;
1391
- const op = hash ? await this.get("out", hash) : void 0;
1392
- if (op) await this.put({
1393
- ...op,
1394
- state: paid ? "paid" : "failed",
1395
- settledAt: Date.now()
1396
- });
1397
- this.events.resolved({
1398
- paymentId: op?.paymentId ?? context.paymentId,
1399
- providerId: op?.providerId ?? "cashu-mint",
1400
- mint: context.mint
1401
- }, paid);
1402
- await this.loadRecent();
1403
- this.events.changed();
1404
- }
1405
- /** Asks the active source about everything of its that is open, pending or unknown. One pass at a time. */
1406
- reconcile() {
1407
- const pass = this.passes.then(() => this.reconcileOnce());
1408
- this.passes = pass.catch(() => {});
1409
- return pass;
1410
- }
1411
- async reconcileOnce() {
1412
- if (this.stopped) return;
1413
- let again = 0;
1414
- const writes = this.writes;
1415
- try {
1416
- const provider = this.sources.active, providerId = this.sources.activeId;
1417
- for (const op of await this.list()) {
1418
- if (!this.owns(op)) continue;
1419
- if (op.selfSettled) {
1420
- if (op.direction !== "out" || op.state !== "unknown") continue;
1421
- if (!provider) {
1422
- again = again || OUT_POLL_MS;
1423
- continue;
1424
- }
1425
- if (op.providerId !== providerId || !provider.interruptedPayment) continue;
1426
- try {
1427
- const outcome = await provider.interruptedPayment({
1428
- invoice: op.invoice,
1429
- paymentHash: op.paymentHash,
1430
- ref: op.ref
1431
- });
1432
- if (outcome === "pending") again = again || OUT_POLL_MS;
1433
- if (outcome !== "paid" && outcome !== "failed") continue;
1434
- if ((await this.get("out", op.paymentHash))?.state !== "unknown") continue;
1435
- await this.put({
1436
- ...op,
1437
- state: outcome,
1438
- error: outcome === "failed" ? "The payment did not go through" : void 0,
1439
- settledAt: Date.now()
1440
- });
1441
- this.events.resolved(op, outcome === "paid");
1442
- } catch {
1443
- again = again || OUT_POLL_MS;
1444
- }
1445
- continue;
1446
- }
1447
- if (!(op.direction === "in" ? op.state === "open" : ["pending", "unknown"].includes(op.state))) continue;
1448
- if (!provider || op.providerId !== providerId || !provider.capabilities.lookup) continue;
1449
- try {
1450
- const unchanged = async () => (await this.get(op.direction, op.paymentHash))?.state === op.state;
1451
- if (op.direction === "in") {
1452
- const status = await provider.invoiceStatus({
1453
- invoice: op.invoice,
1454
- paymentHash: op.paymentHash,
1455
- amount: op.amount,
1456
- expiresAt: op.expiresAt,
1457
- ref: op.ref
1458
- });
1459
- if (!await unchanged()) continue;
1460
- if (status.state === "paid") {
1461
- await this.put({
1462
- ...op,
1463
- state: "paid",
1464
- settledAt: Date.now()
1465
- });
1466
- this.events.received(op);
1467
- } else if (status.state === "expired" || op.expiresAt + 6e4 < Date.now()) await this.put({
1468
- ...op,
1469
- state: "expired"
1470
- });
1471
- else again = again ? Math.min(again, IN_POLL_MS) : IN_POLL_MS;
1472
- } else {
1473
- const status = await provider.paymentStatus({
1474
- invoice: op.invoice,
1475
- paymentHash: op.paymentHash,
1476
- ref: op.ref
1477
- });
1478
- if (status.state === "pending") {
1479
- again = again || OUT_POLL_MS;
1480
- continue;
1481
- }
1482
- if (!await unchanged()) continue;
1483
- await this.put({
1484
- ...op,
1485
- state: status.state,
1486
- fee: status.fee ?? op.fee,
1487
- error: status.state === "failed" ? "The payment did not go through" : void 0,
1488
- settledAt: Date.now()
1489
- });
1490
- this.events.resolved(op, status.state === "paid");
1491
- }
1492
- } catch {
1493
- again = again ? Math.min(again, op.direction === "in" ? IN_POLL_MS : OUT_POLL_MS) : op.direction === "in" ? IN_POLL_MS : OUT_POLL_MS;
1494
- }
1495
- }
1496
- } finally {
1497
- if (await this.loadRecent() || this.writes !== writes) this.events.changed();
1498
- if (again) this.schedule(again);
1499
- }
1500
- }
1501
- schedule(ms) {
1502
- if (this.stopped) return;
1503
- clearTimeout(this.timer);
1504
- this.timer = setTimeout(() => void this.reconcile(), ms);
1505
- }
1506
- /** Changing the source is refused while a payment through it has not ended: only it can say how. */
1507
- async refusal(providerId, mode) {
1508
- const open = (await this.list()).filter((op) => op.direction === "out" && !op.selfSettled && op.providerId === providerId && op.mode === mode && this.owns(op) && [
1509
- "sending",
1510
- "pending",
1511
- "unknown"
1512
- ].includes(op.state));
1513
- return open.length ? `A Lightning payment through this source has not ended yet (${open.length}). Wait for it before changing the source.` : void 0;
1514
- }
1515
- cardField() {
1516
- return this.options.card === void 0 ? {} : { card: this.options.card };
1517
- }
1518
- /** A Mainnet source refuses an invoice on a test chain. The reverse (test mints use lnbc) is `quote`'s to refuse. */
1519
- checkNetwork(network) {
1520
- if (this.mode === "mainnet" && network !== "bitcoin") throw new Error(`That invoice is for ${network}, a test network: pay it from a Testnet wallet`);
1521
- }
1522
- async get(direction, hash) {
1523
- return wrap((await store$1(STORES.settings, "readonly")).get(opKey(direction, hash)));
1524
- }
1525
- async put(op) {
1526
- this.writes++;
1527
- await transact([STORES.settings], (s) => {
1528
- s[STORES.settings].put(op, opKey(op.direction, op.paymentHash));
1529
- });
1530
- }
1531
- list() {
1532
- return readLightningJournal();
1533
- }
1534
- /** Crash recovery: a spend journaled as `sending` never got its answer written, so its outcome is unknown. */
1535
- async recover() {
1536
- for (const op of await this.list()) if (op.direction === "out" && op.state === "sending" && this.owns(op)) await this.put({
1537
- ...op,
1538
- state: "unknown",
1539
- error: "Interrupted. It is being checked; nothing is paid again."
1540
- });
1541
- this.schedule(0);
1542
- }
1543
- /** True when the recent list reads otherwise than before. */
1544
- async loadRecent() {
1545
- const was = this.recent;
1546
- this.recent = (await this.list()).filter((op) => this.owns(op)).sort((a, b) => b.createdAt - a.createdAt).slice(0, RECENT).map(({ ref: _ref, note: _note, selfSettled: _self, ...view }) => view);
1547
- return JSON.stringify(was) !== JSON.stringify(this.recent);
1548
- }
1549
- };
1550
- //#endregion
1551
- //#region ../browser/src/engine/paymentAdapters/providers/recoveryPhrase.ts
1552
- /** A phrase as BIP 39 wants it, however it was typed or pasted. */
1553
- var normalizePhrase = (text = "") => text.trim().toLowerCase().split(/\s+/).join(" ");
1554
- var isRecoveryPhrase = (text) => validateMnemonic(normalizePhrase(text), wordlist);
1555
- //#endregion
1556
- //#region ../browser/src/engine/paymentAdapters/providers/lightningCards.ts
1557
- /** The card of Lightning through the Cashu mints: at most one per network, shown while the network has mints. */
1558
- var CASHU_CARD = "cashu";
1559
- /** The card made from the one source a network had before it could have several. */
1560
- var LEGACY_CARD = "main";
1561
- var cardsKey = (network) => `lightningCards-${network}`;
1562
- var NAME_MAX = 32;
1563
- /** Short source names for a card's default name ("Alby Hub (NWC)"): the form's label otherwise. */
1564
- var SHORT = {
1565
- nwc: "NWC",
1566
- lnd: "LND",
1567
- "core-lightning": "Core Lightning",
1568
- breez: "Breez",
1569
- [CASHU_MINT_SOURCE]: "Cashu",
1570
- fedimint: "Fedimint",
1571
- webln: "WebLN"
1572
- };
1573
- /**
1574
- * The cards of a network, made the first time this version opens it from what an older one kept: the network's one
1575
- * source (`lightningSource-<network>`) becomes a card reading that same key, so no secret moves and nothing asks
1576
- * again; with no source saved, the network used the Cashu mints, and that is its card. Either becomes the default for
1577
- * receiving. Read and written in one transaction; a second run finds the cards and changes nothing. Never reads a
1578
- * secret: the source's record names its provider next to its sealed secrets.
1579
- */
1580
- async function loadLightningCards(network, label, now = Date.now()) {
1581
- let result;
1582
- await transact([STORES.settings], (stores) => {
1583
- const settings = stores[STORES.settings];
1584
- const saved = settings.get(cardsKey(network));
1585
- saved.onsuccess = () => {
1586
- const found = saved.result;
1587
- if (found?.cards?.length) {
1588
- result = found;
1589
- return;
1590
- }
1591
- const legacy = settings.get(sourceKey("lightning", network));
1592
- legacy.onsuccess = () => {
1593
- const providerId = legacy.result?.providerId;
1594
- const card = typeof providerId === "string" && providerId && providerId !== "cashu-mint" ? {
1595
- id: LEGACY_CARD,
1596
- name: shortName(providerId, label),
1597
- providerId,
1598
- createdAt: now
1599
- } : cashuCard(now);
1600
- result = {
1601
- cards: [card],
1602
- receive: card.id
1603
- };
1604
- settings.put(result, cardsKey(network));
1605
- };
1606
- };
1607
- });
1608
- return result;
1609
- }
1610
- var cashuCard = (now) => ({
1611
- id: CASHU_CARD,
1612
- name: SHORT[CASHU_MINT_SOURCE],
1613
- providerId: CASHU_MINT_SOURCE,
1614
- key: CASHU_CARD,
1615
- createdAt: now
1616
- });
1617
- var shortName = (providerId, label) => SHORT[providerId] ?? label(providerId);
1618
- /**
1619
- * The Lightning cards of one network, each with its own source, balance and journal: "Alby Hub (NWC)", "Home LND",
1620
- * the Cashu mints. A payment goes through the card picked for it; a chat request's invoice and Receive come from the
1621
- * default for receiving unless another is picked. Adding, removing and renaming are serialized.
1622
- */
1623
- var LightningCards = class {
1624
- network;
1625
- options;
1626
- stored = {
1627
- cards: [],
1628
- receive: CASHU_CARD
1629
- };
1630
- services = /* @__PURE__ */ new Map();
1631
- queue = Promise.resolve();
1632
- /** A card being added: a creation that takes too long cuts its connection short. */
1633
- adding;
1634
- stopped = false;
1635
- constructor(network, options) {
1636
- this.network = network;
1637
- this.options = options;
1638
- }
1639
- serial(run) {
1640
- const next = this.queue.then(run, run);
1641
- this.queue = next.catch(() => {});
1642
- return next;
1643
- }
1644
- label = (providerId) => this.options.descriptors().find((d) => d.id === providerId)?.label ?? providerId;
1645
- make(card) {
1646
- return new LightningService(this.network, this.options.descriptors, this.options.host, this.options.events, card.providerId === "cashu-mint" ? CASHU_MINT_SOURCE : void 0, {
1647
- fetch: this.options.fetch,
1648
- card: card.id,
1649
- key: card.key,
1650
- owns: (op) => this.owner(op) === card.id
1651
- });
1652
- }
1653
- starting;
1654
- /** Opens the cards (once: a use before the engine started opens them too). */
1655
- start() {
1656
- return this.starting ??= this.open();
1657
- }
1658
- async open() {
1659
- this.stored = await loadLightningCards(this.network, this.label);
1660
- for (const card of this.stored.cards) {
1661
- const service = this.make(card);
1662
- this.services.set(card.id, service);
1663
- await service.start();
1664
- }
1665
- }
1666
- /**
1667
- * The card an operation went through. One journaled before cards: the Cashu card's when the mints made it, else the
1668
- * card made from the network's one source (none, when that source was replaced before cards: nobody shows it).
1669
- */
1670
- owner(op) {
1671
- if (op.card !== void 0) return op.card;
1672
- return op.providerId === "cashu-mint" ? CASHU_CARD : LEGACY_CARD;
1673
- }
1674
- /** The cards a person sees: the Cashu card only while the network has mints. In the order they were added. */
1675
- shown() {
1676
- return this.stored.cards.filter((c) => c.id !== "cashu" || this.options.hasMints());
1677
- }
1678
- /** The default for receiving, or the first card shown when that one is not (the Cashu card of a network with no mints). */
1679
- get receivingId() {
1680
- const shown = this.shown();
1681
- return shown.find((c) => c.id === this.stored.receive)?.id ?? shown[0]?.id ?? this.stored.receive;
1682
- }
1683
- get receiving() {
1684
- return this.services.get(this.receivingId) ?? [...this.services.values()][0];
1685
- }
1686
- /** A card by its id; without one, the default for receiving. Throws when it is gone. */
1687
- card(id) {
1688
- if (id === void 0) {
1689
- if (!this.services.size) throw new Error("Lightning is not open yet");
1690
- return this.receiving;
1691
- }
1692
- const service = this.services.get(id);
1693
- if (!service) throw new Error("That Lightning card is gone: pick another");
1694
- return service;
1695
- }
1696
- has(id) {
1697
- return this.services.has(id);
1698
- }
1699
- all() {
1700
- return [...this.services.values()];
1701
- }
1702
- views() {
1703
- const receive = this.receivingId;
1704
- return this.shown().filter((card) => this.services.has(card.id)).map((card) => ({
1705
- ...this.services.get(card.id).view,
1706
- card: card.id,
1707
- name: card.name,
1708
- receive: card.id === receive
1709
- }));
1710
- }
1711
- /** The default for receiving's view: what a caller from before cards reads as the network's Lightning. None before start. */
1712
- get view() {
1713
- const card = this.stored.cards.find((c) => c.id === this.receivingId) ?? this.stored.cards[0];
1714
- return card && this.services.has(card.id) ? {
1715
- ...this.services.get(card.id).view,
1716
- card: card.id,
1717
- name: card.name,
1718
- receive: true
1719
- } : void 0;
1720
- }
1721
- /** The card holding this quote (each card quotes through its own source). */
1722
- withQuote(quote) {
1723
- return this.all().find((s) => s.hasQuote(quote));
1724
- }
1725
- /**
1726
- * Adds a card with `providerId` and the values of its form: connected and checked first, like any source; only then
1727
- * saved. Nothing is saved when it fails. The same wallet twice (same source, same settings) is refused. A card of the
1728
- * person's own takes receiving over from the mints' card (what a source chosen before cards did); next to another
1729
- * card of their own, the default stays.
1730
- */
1731
- add(providerId, values) {
1732
- return this.serial(async () => {
1733
- if (providerId === "cashu-mint") {
1734
- if (this.services.has("cashu")) throw new Error("Lightning through your Cashu mints is already a card");
1735
- if (!this.options.hasMints()) throw new Error("Create a Cashu wallet first: this card pays and receives through its mints");
1736
- const card = cashuCard(Date.now());
1737
- const service = this.make(card);
1738
- await service.start();
1739
- await this.save({
1740
- ...this.stored,
1741
- cards: [...this.stored.cards, card]
1742
- }, card.id);
1743
- this.services.set(card.id, service);
1744
- service.ensureReady();
1745
- return card.id;
1746
- }
1747
- const id = `ln-${[...crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(4))].map((b) => b.toString(16).padStart(2, "0")).join("")}`;
1748
- const card = {
1749
- id,
1750
- name: "",
1751
- providerId,
1752
- key: id,
1753
- createdAt: Date.now()
1754
- };
1755
- const service = this.make(card);
1756
- const same = await this.findSame(providerId, values);
1757
- if (same) throw new Error(`That wallet is already your card “${this.stored.cards.find((c) => c.id === same)?.name}”`);
1758
- await service.start();
1759
- this.adding = service;
1760
- try {
1761
- await service.sources.set(providerId, values);
1762
- } catch (error) {
1763
- await service.stop();
1764
- throw error;
1765
- } finally {
1766
- this.adding = void 0;
1767
- }
1768
- card.name = this.freeName(service.view.alias ? `${service.view.alias.slice(0, 20)} (${shortName(providerId, this.label)})` : shortName(providerId, this.label));
1769
- const receive = this.receivingId === "cashu" ? id : this.stored.receive;
1770
- const hidden = this.services.has("cashu") && !this.options.hasMints();
1771
- const cards = [...this.stored.cards.filter((c) => !hidden || c.id !== "cashu"), card];
1772
- try {
1773
- await this.save({
1774
- cards,
1775
- receive
1776
- }, card.id);
1777
- } catch (error) {
1778
- await service.sources.forget().catch(() => {});
1779
- await service.stop();
1780
- throw error;
1781
- }
1782
- this.services.set(id, service);
1783
- if (hidden) {
1784
- const gone = this.services.get(CASHU_CARD);
1785
- this.services.delete(CASHU_CARD);
1786
- await gone.stop();
1787
- }
1788
- return id;
1789
- });
1790
- }
1791
- /** The card that is already this wallet (the same source with the same settings), if one is. Throws what to fix in the form. */
1792
- async findSame(providerId, values) {
1793
- const { settings } = this.receiving.sources.settingsOf(providerId, values);
1794
- for (const [id, service] of this.services) if (await service.sources.holds(providerId, settings)) return id;
1795
- }
1796
- /** A name no other card of the network has: "LND", then "LND 2". */
1797
- freeName(name) {
1798
- const taken = new Set(this.stored.cards.map((c) => c.name.toLowerCase()));
1799
- if (!taken.has(name.toLowerCase())) return name;
1800
- for (let n = 2;; n++) if (!taken.has(`${name} ${n}`.toLowerCase())) return `${name} ${n}`;
1801
- }
1802
- /**
1803
- * Removes a card: its source and sealed secrets go (refused while a payment through it has not ended). The default
1804
- * for receiving moves to the first card of the person's own left, else the mints' card. A network left with no card gets the Cashu card back, as before cards.
1805
- */
1806
- remove(id) {
1807
- return this.serial(async () => {
1808
- const service = this.services.get(id);
1809
- if (!service) throw new Error("That Lightning card is gone");
1810
- await service.sources.forget();
1811
- await service.stop();
1812
- this.services.delete(id);
1813
- let cards = this.stored.cards.filter((c) => c.id !== id);
1814
- let added;
1815
- if (!cards.length) {
1816
- const card = cashuCard(Date.now());
1817
- cards = [card];
1818
- added = this.make(card);
1819
- this.services.set(card.id, added);
1820
- await added.start();
1821
- }
1822
- const shown = cards.filter((c) => c.id !== "cashu" || this.options.hasMints());
1823
- const next = shown.find((c) => c.id !== "cashu") ?? shown[0] ?? cards[0];
1824
- const receive = this.stored.receive === id || !cards.some((c) => c.id === this.stored.receive) ? next.id : this.stored.receive;
1825
- await this.save({
1826
- cards,
1827
- receive
1828
- });
1829
- if (added) added.ensureReady();
1830
- });
1831
- }
1832
- /** Makes a card the network's default for receiving. */
1833
- setReceive(id) {
1834
- return this.serial(async () => {
1835
- if (!this.shown().some((c) => c.id === id)) throw new Error("That Lightning card is gone");
1836
- await this.save({
1837
- ...this.stored,
1838
- receive: id
1839
- });
1840
- });
1841
- }
1842
- /** Renames a card: 1 to 32 characters, unique on its network. */
1843
- rename(id, name) {
1844
- return this.serial(async () => {
1845
- const clean = name.replace(/\s+/g, " ").trim();
1846
- if (!clean) throw new Error("Enter a name");
1847
- if (clean.length > NAME_MAX) throw new Error(`At most ${NAME_MAX} characters`);
1848
- if (!this.services.has(id)) throw new Error("That Lightning card is gone");
1849
- if (this.stored.cards.some((c) => c.id !== id && c.name.toLowerCase() === clean.toLowerCase())) throw new Error("Another card has that name");
1850
- await this.save({
1851
- ...this.stored,
1852
- cards: this.stored.cards.map((c) => c.id === id ? {
1853
- ...c,
1854
- name: clean
1855
- } : c)
1856
- });
1857
- });
1858
- }
1859
- async save(next, receiveIfNone) {
1860
- const shown = next.cards.filter((c) => c.id !== "cashu" || this.options.hasMints());
1861
- const receive = receiveIfNone && !shown.some((c) => c.id === next.receive) ? receiveIfNone : next.receive;
1862
- const stored = {
1863
- cards: next.cards,
1864
- receive
1865
- };
1866
- await transact([STORES.settings], (s) => {
1867
- s[STORES.settings].put(stored, cardsKey(this.network));
1868
- });
1869
- this.stored = stored;
1870
- this.options.events.changed();
1871
- }
1872
- async ownerOf(direction, invoice) {
1873
- const op = (await readLightningJournal()).find((o) => o.direction === direction && o.invoice === invoice && o.mode === this.network);
1874
- const owner = op ? this.owner(op) : CASHU_CARD;
1875
- return owner && this.services.get(owner) || this.services.get("cashu") || this.receiving;
1876
- }
1877
- async reportInvoicePaid(invoice, context = {}) {
1878
- await (await this.ownerOf("in", invoice)).reportInvoicePaid(invoice, context);
1879
- }
1880
- async reportPaymentResolved(invoice, paid, context = {}) {
1881
- await (await this.ownerOf("out", invoice)).reportPaymentResolved(invoice, paid, context);
1882
- }
1883
- resolveDestination(text) {
1884
- return this.receiving.resolveDestination(text);
1885
- }
1886
- destinationInvoice(id, amountSat, comment) {
1887
- return (this.all().find((s) => s.hasDestination(id)) ?? this.receiving).destinationInvoice(id, amountSat, comment);
1888
- }
1889
- list() {
1890
- return readLightningJournal();
1891
- }
1892
- /**
1893
- * What a handoff needs to know of the cards (WISP 06 § Wallets), from their sealed settings: the LND cards that pin a
1894
- * certificate, and each Breez card's database on this device, by wallet id.
1895
- */
1896
- async handoffFacts() {
1897
- const pinned = [], breez = /* @__PURE__ */ new Map();
1898
- for (const [id, service] of this.services) {
1899
- const wallet = `lightning:${this.network}:${id}`, providerId = service.view.providerId;
1900
- if (providerId === "lnd" && await service.sources.secret("certificate")) pinned.push(wallet);
1901
- if (providerId === "breez") {
1902
- const mnemonic = await service.sources.secret("mnemonic");
1903
- if (mnemonic) breez.set(wallet, await breezDatabaseInUse(this.network === "mainnet" ? "bitcoin" : "regtest", normalizePhrase(mnemonic)));
1904
- }
1905
- }
1906
- return {
1907
- pinned,
1908
- breez
1909
- };
1910
- }
1911
- /** The cards whose source is opened on this device: not one at home on another device (WISP 06 § Wallets that stay home). */
1912
- here() {
1913
- return [...this.services].filter(([id]) => awayFrom(`lightning:${this.network}:${id}`) === void 0).map(([, s]) => s);
1914
- }
1915
- async reconcile() {
1916
- await Promise.all(this.here().map((s) => s.reconcile()));
1917
- }
1918
- async recover() {
1919
- for (const s of this.here()) await s.recover();
1920
- }
1921
- async ensureReady() {
1922
- await Promise.all(this.here().map((s) => s.ensureReady()));
1923
- }
1924
- refreshOffered() {
1925
- for (const s of this.all()) s.refreshOffered();
1926
- }
1927
- wake() {
1928
- for (const s of this.all()) s.sources.wake();
1929
- }
1930
- cutShort() {
1931
- this.adding?.sources.cutShort();
1932
- }
1933
- resume() {
1934
- this.adding?.sources.resume();
1935
- }
1936
- async stop() {
1937
- this.stopped = true;
1938
- for (const s of this.all()) await s.stop();
1939
- }
1940
- get isStopped() {
1941
- return this.stopped;
1942
- }
1943
- };
1944
- //#endregion
1945
- //#region ../browser/src/proofs/verify.ts
1946
- var DAY = 86400;
1947
- /**
1948
- * The checks every proof goes through, whatever its provider: known provider, validity within the
1949
- * provider's limit, bounded evidence, strict parsing, the provider's own check, and the category rule
1950
- * (a self-custodied proof proves exactly the subject it names; an attested one says who attests it).
1951
- * The engine verifies a contact's proof, and the person's own proof before saving it, only through this.
1952
- */
1953
- async function verifyIdentity(providers, statement, evidence, ctx) {
1954
- const { binding } = statement;
1955
- const provider = providers.find((p) => p.id === binding.provider);
1956
- if (!provider) throw new Error("This app cannot verify this kind of identity");
1957
- if (binding.expiresAt - binding.issuedAt > Math.min(provider.validity.maxDays * DAY, 3456e4)) throw new Error("The proof lasts longer than this kind of identity allows");
1958
- if (JSON.stringify(evidence ?? null).length > 16384) throw new Error("Evidence too large");
1959
- const parsed = provider.parseEvidence(evidence);
1960
- const verified = boundVerifiedIdentity(await provider.verify(statement, parsed, ctx));
1961
- if (provider.category === "self-custodied" && verified.subject !== binding.subject) throw new Error("The evidence is for another identity");
1962
- if (provider.category === "provider-attested" && !verified.attester) throw new Error("The evidence does not say who attests it");
1963
- return verified;
1964
- }
1965
- /** Problems with a provider's descriptor, empty when it is well-formed. The registry and the contract suite use it. */
1966
- function descriptorProblems(p) {
1967
- const problems = [];
1968
- if (!/^[a-z][a-z0-9-]{0,31}$/.test(p.id)) problems.push("id must match ^[a-z][a-z0-9-]{0,31}$");
1969
- if (!p.label?.trim()) problems.push("label is empty");
1970
- if (!p.description?.trim()) problems.push("description is empty");
1971
- if (!p.privacy?.trim()) problems.push("privacy is empty: say what verify contacts");
1972
- if (p.category !== "self-custodied" && p.category !== "provider-attested") problems.push("category is unknown");
1973
- if (!p.platforms.length) problems.push("no platforms");
1974
- const { defaultDays, maxDays } = p.validity;
1975
- if (!(Number.isInteger(defaultDays) && Number.isInteger(maxDays) && defaultDays >= 1 && defaultDays <= maxDays && maxDays * DAY <= 3456e4)) problems.push("validity must be whole days, 1 ≤ default ≤ max ≤ 400");
1976
- if (p.recheck && !(Number.isInteger(p.recheck.afterSeconds) && p.recheck.afterSeconds >= 60)) problems.push("recheck.afterSeconds must be at least 60");
1977
- if (!p.signers.length) problems.push("no signers");
1978
- const ids = p.signers.map((s) => s.id);
1979
- if (new Set(ids).size !== ids.length) problems.push("signer ids are not unique");
1980
- for (const s of p.signers) {
1981
- if (![
1982
- "in-app",
1983
- "external-tool",
1984
- "redirect",
1985
- "publish"
1986
- ].includes(s.kind)) problems.push(`signer ${s.id} has an unknown kind`);
1987
- if (s.kind === "in-app" && typeof s.run !== "function") problems.push(`signer ${s.id} has no run()`);
1988
- if (s.kind === "external-tool" && (typeof s.instructions !== "function" || typeof s.parse !== "function")) problems.push(`signer ${s.id} needs instructions() and parse()`);
1989
- if (s.kind === "redirect" && typeof s.start !== "function") problems.push(`signer ${s.id} has no start()`);
1990
- if (s.kind === "publish" && (typeof s.instructions !== "function" || typeof s.evidence !== "function")) problems.push(`signer ${s.id} needs instructions() and evidence()`);
1991
- }
1992
- return problems;
1993
- }
1994
- /**
1995
- * The engine's `ctx.fetch`: HTTPS GET only, no credentials or referrer, a time-out, a size cap,
1996
- * redirects refused unless asked for.
1997
- * `online` false refuses everything (the person turned the network off).
1998
- */
1999
- function boundedIdentityFetch(options = { online: () => true }) {
2000
- return async (url, { headers, maxBytes = 65536, signal, redirect = "error" } = {}) => {
2001
- if (!options.online()) throw new Error("Offline: this identity cannot be checked now");
2002
- const target = new URL(url);
2003
- if (target.protocol !== "https:" || target.username || target.password) throw new Error("Identity checks use HTTPS only");
2004
- const timeout = AbortSignal.timeout(options.timeoutMs ?? 1e4);
2005
- const response = await (options.fetcher ?? fetch)(target.href, {
2006
- headers,
2007
- signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
2008
- credentials: "omit",
2009
- referrerPolicy: "no-referrer",
2010
- cache: "no-store",
2011
- redirect
2012
- });
2013
- if (Number(response.headers.get("content-length")) > maxBytes) throw new Error("Identity check response too large");
2014
- const reader = response.body?.getReader();
2015
- const parts = [];
2016
- let length = 0;
2017
- if (reader) try {
2018
- for (;;) {
2019
- const { done, value } = await reader.read();
2020
- if (done) break;
2021
- length += value.length;
2022
- if (length > maxBytes) throw new Error("Identity check response too large");
2023
- parts.push(value);
2024
- }
2025
- } finally {
2026
- await reader.cancel().catch(() => {});
2027
- }
2028
- const bytes = new Uint8Array(length);
2029
- let offset = 0;
2030
- for (const part of parts) {
2031
- bytes.set(part, offset);
2032
- offset += part.length;
2033
- }
2034
- return {
2035
- status: response.status,
2036
- contentType: response.headers.get("content-type") ?? "",
2037
- text: new TextDecoder().decode(bytes),
2038
- bytes
2039
- };
2040
- };
2041
- }
2042
- //#endregion
2043
- //#region ../browser/src/plugins/bundled.ts
2044
- /**
2045
- * The adapter plugins compiled into this build. Empty unless the build lists some: `GHOSTLY_PLUGINS`
2046
- * names their modules (comma-separated paths from the repository root, each default-exporting a
2047
- * `GhostlyAdapterPlugin`), and the Vite plugin in `packages/browser/vite-plugin.ts` swaps this module
2048
- * for one that imports them. Release builds never set it; the e2e build carries the SDK example.
2049
- */
2050
- var BUNDLED_PLUGINS = [];
2051
- var PLUGIN_ID = /^[a-z][a-z0-9-]{0,63}$/;
2052
- var KEY = Symbol.for("ghostly.adapters/1");
2053
- function store() {
2054
- const g = globalThis;
2055
- return g[KEY] ??= {
2056
- plugins: /* @__PURE__ */ new Map(),
2057
- reserved: {
2058
- lightning: /* @__PURE__ */ new Set(),
2059
- onchain: /* @__PURE__ */ new Set(),
2060
- identity: /* @__PURE__ */ new Set()
2061
- },
2062
- listeners: /* @__PURE__ */ new Set(),
2063
- seeded: false
2064
- };
2065
- }
2066
- var ids = (kind, plugin) => {
2067
- const list = kind === "identity" ? plugin.identities : kind === "lightning" ? plugin.lightning : plugin.onchain;
2068
- return Array.isArray(list) ? list.map((a) => a?.id) : [];
2069
- };
2070
- var KINDS = [
2071
- "lightning",
2072
- "onchain",
2073
- "identity"
2074
- ];
2075
- /** Problems with a plugin, empty when it can be registered. Does not look at what is already registered. */
2076
- function pluginProblems(plugin) {
2077
- const problems = [];
2078
- if (!plugin || typeof plugin !== "object") return ["not a plugin object"];
2079
- if (!PLUGIN_ID.test(plugin.id ?? "")) problems.push("plugin id must match ^[a-z][a-z0-9-]{0,63}$");
2080
- if (plugin.sdk !== 1) problems.push(`plugin was written for SDK API ${String(plugin.sdk)}; this app speaks 1`);
2081
- if (plugin.version !== void 0 && (typeof plugin.version !== "string" || plugin.version.length > 64)) problems.push("version must be a short string");
2082
- for (const kind of ["lightning", "onchain"]) {
2083
- const list = plugin[kind];
2084
- if (list === void 0) continue;
2085
- if (!Array.isArray(list)) {
2086
- problems.push(`${kind} must be a list of descriptors`);
2087
- continue;
2088
- }
2089
- for (const d of list) {
2090
- if (!d || typeof d !== "object") {
2091
- problems.push(`${kind} ${String(d)}: not a descriptor`);
2092
- continue;
2093
- }
2094
- const own = providerDescriptorProblems(d);
2095
- if (d?.kind && d.kind !== kind) own.push(`is listed under ${kind} but says kind ${String(d.kind)}`);
2096
- problems.push(...own.map((p) => `${kind} ${String(d?.id)}: ${p}`));
2097
- }
2098
- }
2099
- if (plugin.identities !== void 0) {
2100
- if (!Array.isArray(plugin.identities)) problems.push("identities must be a list of providers");
2101
- else for (const p of plugin.identities) if (!p || typeof p !== "object") problems.push(`identity ${String(p)}: not a provider`);
2102
- else problems.push(...descriptorProblems(p).map((m) => `identity ${String(p.id)}: ${m}`));
2103
- }
2104
- for (const kind of KINDS) {
2105
- const list = ids(kind, plugin);
2106
- if (new Set(list).size !== list.length) problems.push(`${kind} ids are not unique within the plugin`);
2107
- }
2108
- if (!KINDS.some((kind) => ids(kind, plugin).length)) problems.push("the plugin brings no adapter");
2109
- return problems;
2110
- }
2111
- /** The built-in registries say which ids are theirs. A plugin using one is refused. */
2112
- function reserveAdapterIds(kind, list) {
2113
- const s = store();
2114
- for (const id of list) s.reserved[kind].add(id);
2115
- }
2116
- /**
2117
- * Registers a plugin's adapters. Throws, registering nothing, when the plugin is malformed, its id or
2118
- * one of its adapters' ids is taken (by a built-in or by another plugin), or its contract generation
2119
- * is another one. Returns the function that unregisters it.
2120
- */
2121
- function registerAdapters(plugin) {
2122
- const problems = pluginProblems(plugin);
2123
- const s = store();
2124
- if (!problems.length) {
2125
- if (s.plugins.has(plugin.id)) problems.push(`a plugin with id ${plugin.id} is already registered`);
2126
- for (const kind of KINDS) for (const id of ids(kind, plugin)) {
2127
- if (s.reserved[kind].has(id)) problems.push(`${kind} ${id} is a built-in of this app`);
2128
- for (const other of s.plugins.values()) if (ids(kind, other).includes(id)) problems.push(`${kind} ${id} is already registered by plugin ${other.id}`);
2129
- }
2130
- }
2131
- if (problems.length) throw new Error(`Cannot register plugin ${String(plugin?.id)}: ${problems.join("; ")}`);
2132
- s.plugins.set(plugin.id, plugin);
2133
- notify(s);
2134
- return () => {
2135
- if (s.plugins.get(plugin.id) === plugin) {
2136
- s.plugins.delete(plugin.id);
2137
- notify(s);
2138
- }
2139
- };
2140
- }
2141
- function notify(s) {
2142
- for (const listener of s.listeners) try {
2143
- listener();
2144
- } catch {}
2145
- }
2146
- /** Called when a plugin registers or unregisters: the engine re-reads the lists. */
2147
- function onAdaptersChanged(listener) {
2148
- const s = store();
2149
- s.listeners.add(listener);
2150
- return () => {
2151
- s.listeners.delete(listener);
2152
- };
2153
- }
2154
- /** Every registered plugin, in registration order (bundled ones first). */
2155
- function registeredPlugins() {
2156
- seed();
2157
- return [...store().plugins.values()];
2158
- }
2159
- function registeredLightningProviders() {
2160
- return registeredPlugins().flatMap((p) => p.lightning ?? []);
2161
- }
2162
- function registeredOnchainProviders() {
2163
- return registeredPlugins().flatMap((p) => p.onchain ?? []);
2164
- }
2165
- function registeredIdentityProviders() {
2166
- return registeredPlugins().flatMap((p) => p.identities ?? []);
2167
- }
2168
- /**
2169
- * The plugins compiled into this build register once, when they are first asked for. A broken one is
2170
- * reported and skipped rather than taking the app down with it; the e2e that expects it fails instead.
2171
- */
2172
- function seed() {
2173
- const s = store();
2174
- if (s.seeded) return;
2175
- s.seeded = true;
2176
- for (const plugin of BUNDLED_PLUGINS) try {
2177
- registerAdapters(plugin);
2178
- } catch (error) {
2179
- console.error(`[ghostly] a bundled adapter plugin was not loaded: ${error instanceof Error ? error.message : String(error)}`);
2180
- }
2181
- }
2182
- //#endregion
2183
8
  //#region ../browser/src/proofs/bitcoinWallets.ts
2184
9
  /** What a proof proves and what it does not; shown next to every Bitcoin proof, given or received. */
2185
10
  var BITCOIN_PROOF_LIMITS = [
@@ -2507,6 +332,8 @@ async function withNostrSigner(options, work) {
2507
332
  const action = async () => {
2508
333
  if (options.bunker) {
2509
334
  const bp = parseProofBunker(options.bunker);
335
+ const [{ BunkerSigner }, { SimplePool }] = await Promise.all([import("nostr-tools/nip46"), import("nostr-tools/pool")]);
336
+ if (closed) throw new Error("Signer request cancelled");
2510
337
  pool = new SimplePool();
2511
338
  clientKey = generateSecretKey();
2512
339
  signer = BunkerSigner.fromBunker(clientKey, bp, {
@@ -2668,216 +495,6 @@ var nostr = {
2668
495
  }
2669
496
  };
2670
497
  //#endregion
2671
- //#region ../browser/src/proofs/domain.ts
2672
- /** Public resolvers that answer RFC 8484 wire-format GETs with CORS (checked 2026-09-23). */
2673
- var DOH_RESOLVERS = [
2674
- {
2675
- id: "quad9",
2676
- name: "Quad9",
2677
- url: "https://dns.quad9.net/dns-query"
2678
- },
2679
- {
2680
- id: "cloudflare",
2681
- name: "Cloudflare",
2682
- url: "https://cloudflare-dns.com/dns-query"
2683
- },
2684
- {
2685
- id: "google",
2686
- name: "Google",
2687
- url: "https://dns.google/dns-query"
2688
- }
2689
- ];
2690
- var resolverById = (id) => DOH_RESOLVERS.find((r) => r.id === id) ?? DOH_RESOLVERS.find((r) => r.id === "quad9");
2691
- /** The person's resolver choice, per browser profile (UI and engine share the origin). */
2692
- var RESOLVER_SETTING = "ghostly-doh-resolver";
2693
- function chosenResolver() {
2694
- try {
2695
- return resolverById(globalThis.localStorage?.getItem(RESOLVER_SETTING));
2696
- } catch {
2697
- return resolverById(void 0);
2698
- }
2699
- }
2700
- var DNS_MAX_BYTES = 16384;
2701
- /**
2702
- * Lookups are shared between checks for the DNS TTL, capped: never staler than the resolver itself would
2703
- * be, so a re-check sees a removal as soon as DNS does. Files have no TTL and are only shared in flight.
2704
- */
2705
- var CACHE_MAX_MS = 3e4;
2706
- var CACHE_FILE_MS = 0;
2707
- /** A check that did not confirm the record, with a message for people. */
2708
- var DomainCheckError = class extends Error {};
2709
- /** A resolver that gave no DNS answer at all: not reached, or it refused the request. The next one may answer. */
2710
- var ResolverUnavailable = class extends DomainCheckError {};
2711
- /** One RFC 8484 GET to one resolver. The query has ID 0 and no client subnet, so it carries only the name. */
2712
- async function askResolver(resolver, name, type, options) {
2713
- let response;
2714
- try {
2715
- response = await options.fetch(`${resolver.url}?dns=${toBase64Url(encodeDnsQuery(name, type))}`, {
2716
- headers: { accept: "application/dns-message" },
2717
- maxBytes: DNS_MAX_BYTES,
2718
- signal: options.signal,
2719
- redirect: "error"
2720
- });
2721
- } catch (e) {
2722
- throw /too large/i.test(String(e)) ? new DomainCheckError(`${resolver.name} sent an answer that is too large`) : new ResolverUnavailable(`${resolver.name} could not be reached`);
2723
- }
2724
- if (response.status !== 200 || !/^application\/dns-message\b/i.test(response.contentType)) throw new ResolverUnavailable(`${resolver.name} refused the lookup`);
2725
- let answer;
2726
- try {
2727
- answer = decodeDnsResponse(response.bytes, name, type);
2728
- } catch {
2729
- throw new DomainCheckError(`${resolver.name} sent an unusable answer`);
2730
- }
2731
- if (answer.rcode !== 0 && answer.rcode !== 3) throw new DomainCheckError(`${resolver.name} could not resolve ${name}`);
2732
- return answer;
2733
- }
2734
- /**
2735
- * A lookup through the chosen resolver, and through the others in turn (in list order) only when it gives no DNS
2736
- * answer at all. Quad9, for one, cannot be read from WebKit (the Mac desktop app, Safari): its HTTP/3 answers carry
2737
- * no CORS header. An answer, even "no such name" or a failure, is never asked again elsewhere. `resolver`: who answered.
2738
- */
2739
- async function dohQuery(name, type, options) {
2740
- const chosen = resolverById(options.resolver);
2741
- let first;
2742
- for (const resolver of [chosen, ...DOH_RESOLVERS.filter((r) => r !== chosen)]) try {
2743
- return {
2744
- ...await askResolver(resolver, name, type, options),
2745
- resolver: resolver.id
2746
- };
2747
- } catch (e) {
2748
- if (!(e instanceof ResolverUnavailable) || options.signal?.aborted) throw e;
2749
- first ??= e;
2750
- }
2751
- throw first;
2752
- }
2753
- /**
2754
- * Refuses a domain whose addresses are not all public before its web server is
2755
- * contacted, so a proof cannot make a contact's app probe their own network.
2756
- * The browser resolves again when it fetches; a name that changes its answer in
2757
- * between (rebinding) is not fully excluded by this check — see the draft.
2758
- */
2759
- async function assertPublicHost(domain, options) {
2760
- const [v4, v6] = await Promise.all([dohQuery(domain, "A", options), dohQuery(domain, "AAAA", options)]);
2761
- const addresses = [...v4.answers, ...v6.answers].map((a) => a.data);
2762
- if (!addresses.length) throw new DomainCheckError(`${domain} has no address`);
2763
- if (!addresses.every(isPublicAddress)) throw new DomainCheckError(`${domain} points to a private network address, so it was not contacted`);
2764
- }
2765
- /** A well-known file: https only, no redirects, bounded. `undefined` when the server says it is not there. */
2766
- async function fetchWellKnown(domain, method, options) {
2767
- await assertPublicHost(domain, options);
2768
- const url = wellKnownUrl(domain, method);
2769
- let response;
2770
- try {
2771
- response = await options.fetch(url, {
2772
- headers: { accept: "application/json" },
2773
- maxBytes: DOMAIN_FILE_MAX_BYTES,
2774
- signal: options.signal,
2775
- redirect: "error"
2776
- });
2777
- } catch (e) {
2778
- throw new DomainCheckError(/too large/i.test(String(e)) ? `${url} is too large` : `${domain} could not be reached. The file must be served at exactly this address, without a redirect, with the header Access-Control-Allow-Origin: *`);
2779
- }
2780
- if (response.status === 404 || response.status === 410) return;
2781
- if (response.status >= 300 && response.status < 400) throw new DomainCheckError(`${domain} answered with a redirect; the file must be served at exactly ${url}`);
2782
- if (response.status !== 200) throw new DomainCheckError(`${domain} answered ${response.status}`);
2783
- try {
2784
- return new TextDecoder("utf-8", { fatal: true }).decode(response.bytes);
2785
- } catch {
2786
- throw new DomainCheckError(`${url} is not text`);
2787
- }
2788
- }
2789
- async function lookup(domain, method, options) {
2790
- const resolver = resolverById(options.resolver).id;
2791
- if (method === "dns") {
2792
- const answer = await dohQuery(dnsProofName(domain), "TXT", options);
2793
- const values = [];
2794
- for (const record of answer.answers.slice(0, 32)) try {
2795
- values.push(txtValue(record.data));
2796
- } catch {}
2797
- const ttlMs = answer.answers.length ? Math.min(...answer.answers.map((a) => a.ttl)) * 1e3 : 0;
2798
- return {
2799
- domain,
2800
- method,
2801
- resolver: answer.resolver,
2802
- records: recordsFromTxt(values),
2803
- found: answer.answers.length > 0,
2804
- dnssec: answer.authenticated,
2805
- ttlMs
2806
- };
2807
- }
2808
- const text = await fetchWellKnown(domain, method, options);
2809
- const base = {
2810
- domain,
2811
- method,
2812
- resolver,
2813
- dnssec: false,
2814
- records: [],
2815
- ttlMs: CACHE_FILE_MS
2816
- };
2817
- if (text === void 0) return {
2818
- ...base,
2819
- found: false
2820
- };
2821
- try {
2822
- return method === "nip05" ? {
2823
- ...base,
2824
- found: true,
2825
- nostrKey: nip05RootKey(text)
2826
- } : {
2827
- ...base,
2828
- found: true,
2829
- records: recordsFromWellKnown(text)
2830
- };
2831
- } catch {
2832
- throw new DomainCheckError(`${wellKnownUrl(domain, method)} is not a valid ${method === "nip05" ? "NIP-05" : "Ghostly"} file`);
2833
- }
2834
- }
2835
- var cache$1 = /* @__PURE__ */ new Map();
2836
- /** What a domain publishes by one method. Concurrent and back-to-back checks share one lookup. */
2837
- async function lookupDomain(domainInput, method, options) {
2838
- const domain = normalizeDomain(domainInput), now = options.now ?? Date.now, resolver = resolverById(options.resolver).id;
2839
- const id = `${resolver}|${method}|${domain}`;
2840
- const hit = cache$1.get(id);
2841
- if (hit && hit.until > now()) {
2842
- const { ttlMs: _ttl, ...result } = await hit.result;
2843
- return result;
2844
- }
2845
- const started = now();
2846
- const entry = {
2847
- until: started + CACHE_MAX_MS,
2848
- result: lookup(domain, method, {
2849
- ...options,
2850
- resolver
2851
- })
2852
- };
2853
- cache$1.set(id, entry);
2854
- if (cache$1.size > 128) cache$1.delete(cache$1.keys().next().value);
2855
- try {
2856
- const { ttlMs, ...result } = await entry.result;
2857
- entry.until = started + Math.min(CACHE_MAX_MS, ttlMs);
2858
- return result;
2859
- } catch (e) {
2860
- if (cache$1.get(id) === entry) cache$1.delete(id);
2861
- throw e;
2862
- }
2863
- }
2864
- /** Throws unless `domain` publishes `record` by `method` (DNS or HTTPS). Returns what was found. */
2865
- async function assertDomainRecord(domain, method, record, options) {
2866
- const found = await lookupDomain(domain, method, options);
2867
- if (recordsName(found.records, record)) return found;
2868
- const where = method === "dns" ? `The TXT record at ${dnsProofName(domain)}` : wellKnownUrl(domain);
2869
- if (found.records.some((r) => r.key === record.key)) throw new DomainCheckError(`${where} is for another proof of this key`);
2870
- if (found.records.length) throw new DomainCheckError(`${where} names another Ghostly key: this proof was replaced or removed`);
2871
- throw new DomainCheckError(method === "dns" ? `No Ghostly TXT record at ${dnsProofName(domain)}${found.found ? "" : " (DNS changes can take a few minutes)"}` : found.found ? `${wellKnownUrl(domain)} names no Ghostly proof` : `${wellKnownUrl(domain)} was not found`);
2872
- }
2873
- /** Throws unless the domain's NIP-05 root name is `nostrKey`. */
2874
- async function assertNip05(domain, nostrKey, options) {
2875
- const found = await lookupDomain(domain, "nip05", options);
2876
- if (found.nostrKey === nostrKey) return found;
2877
- const url = wellKnownUrl(domain, "nip05");
2878
- throw new DomainCheckError(!found.found ? `${url} was not found` : found.nostrKey ? `${url} names another Nostr key for ${domain}` : `${url} has no root name "_" for ${domain}`);
2879
- }
2880
- //#endregion
2881
498
  //#region ../browser/src/proofs/providers/domain.ts
2882
499
  var recordFor = (s) => ({
2883
500
  key: s.binding.key,
@@ -4609,7 +2226,7 @@ async function resolve(did, method, options) {
4609
2226
  };
4610
2227
  if (method === "web") {
4611
2228
  const documentUrl = didWebDocumentUrl(did);
4612
- const text = await fetchDidWeb(did, documentUrl, DID_DOCUMENT_MAX_BYTES$1, options);
2229
+ const text = await fetchDidWeb(did, documentUrl, DID_DOCUMENT_MAX_BYTES, options);
4613
2230
  if (text === void 0) throw new DidError(`No DID document at ${documentUrl}`);
4614
2231
  let raw;
4615
2232
  try {
@@ -4668,11 +2285,6 @@ var VM = /^did:[a-z0-9]+:[\x21-\x7e]+#[\x21-\x7e]+$/;
4668
2285
  var SIG = /^[A-Za-z0-9_-]{86}$/;
4669
2286
  var exactKeys$1 = (raw, keys) => !!raw && typeof raw === "object" && !Array.isArray(raw) && Object.keys(raw).sort().join(",") === keys;
4670
2287
  var validVm = (vm) => typeof vm === "string" && vm.length <= 640 && VM.test(vm);
4671
- /** The profile's own Ghostly did:dht, refused as an external identity. The UI and the engine each set where to read it. */
4672
- var ownDid = () => void 0;
4673
- function setOwnDidSource(source) {
4674
- ownDid = source;
4675
- }
4676
2288
  /** The add dialog's checks run in the UI, with the same bounded fetch the engine uses. */
4677
2289
  var uiFetch = boundedIdentityFetch({ online: () => globalThis.navigator?.onLine !== false });
4678
2290
  /** `#key-1`, shortened when a multibase fragment is long. */
@@ -4965,145 +2577,18 @@ var did = {
4965
2577
  if (!document.services.some((s) => s.proof === statement.id && s.key === statement.binding.key)) throw new DidError("The did.json has no Ghostly service for this proof: it was removed, or not added yet");
4966
2578
  return {
4967
2579
  subject,
4968
- source: `did:web · service in the did.json of ${host}`
4969
- };
4970
- }
4971
- const text = await fetchDidWebFile(subject, statement.id, options);
4972
- if (text === void 0) throw new DidError(`${didWebFileUrl(subject, statement.id)} was not found: it was deleted, or not uploaded yet`);
4973
- if (!didWebFileNames(text, subject, statement.text)) throw new DidError(`${didWebFileUrl(subject, statement.id)} is not the statement of this proof`);
4974
- return {
4975
- subject,
4976
- source: `did:web · statement file beside the did.json of ${host}`
4977
- };
4978
- }
4979
- };
4980
- //#endregion
4981
- //#region ../browser/src/proofs/atproto/resolve.ts
4982
- /**
4983
- * AT Protocol identity resolution, the network half: a DID to its document (the PLC directory for
4984
- * did:plc, the host's `/.well-known/did.json` for did:web), and a handle to a DID (DNS TXT
4985
- * `_atproto.<handle>` through the person's DNS-over-HTTPS resolver, then
4986
- * `https://<handle>/.well-known/atproto-did`). Every request goes through the contract's bounded
4987
- * fetch: HTTPS only, no redirects, a size cap, a time-out. URLs are built here from the DID or the
4988
- * handle, never taken from a proof.
4989
- */
4990
- var PLC_DIRECTORY = "https://plc.directory";
4991
- /** DID documents are small; a large answer is not one. */
4992
- var DID_DOCUMENT_MAX_BYTES = 32768;
4993
- var HANDLE_DID_MAX_BYTES = 2048;
4994
- /**
4995
- * The PLC directory. A build made for the e2e suite with VITE_ATPROTO_TEST_PLC may point it at a
4996
- * `.test` host (a name reserved for testing); a release build always uses plc.directory.
4997
- */
4998
- function plcDirectory() {
4999
- try {
5000
- return PLC_DIRECTORY;
5001
- } catch {
5002
- return PLC_DIRECTORY;
5003
- }
5004
- }
5005
- /** Where a DID's document lives. */
5006
- function didDocumentUrl(did, plc = plcDirectory()) {
5007
- const method = atprotoDidMethod(did);
5008
- if (method === "plc") return `${plc}/${did}`;
5009
- if (method === "web") return `https://${did.slice(8)}/.well-known/did.json`;
5010
- throw new Error("Not an AT Protocol DID");
5011
- }
5012
- /**
5013
- * Refuses a host the account chose (a did:web host, the PDS its document names) that points to a private
5014
- * network address: a contact's app never knocks on its own local network for someone else's proof.
5015
- */
5016
- async function assertPublicServer(host, options) {
5017
- try {
5018
- await assertPublicHost(host, {
5019
- fetch: options.fetch,
5020
- signal: options.signal,
5021
- resolver: options.resolver
5022
- });
5023
- } catch (e) {
5024
- throw new Error(`The account's server ${host} was not contacted: ${e instanceof Error ? e.message : String(e)}`);
5025
- }
5026
- }
5027
- /** A DID's document, checked (parseAtprotoDidDocument). Throws a message people can read. */
5028
- async function resolveAtprotoDid(did, options) {
5029
- const url = didDocumentUrl(did, options.plc);
5030
- if (atprotoDidMethod(did) === "web") await assertPublicServer(did.slice(8), options);
5031
- let response;
5032
- try {
5033
- response = await options.fetch(url, {
5034
- headers: { accept: "application/did+ld+json, application/json" },
5035
- maxBytes: DID_DOCUMENT_MAX_BYTES,
5036
- signal: options.signal,
5037
- redirect: "error"
5038
- });
5039
- } catch (e) {
5040
- throw new Error(/too large/i.test(String(e)) ? "The account's DID document is too large" : "The account's DID could not be resolved");
5041
- }
5042
- if (response.status === 404 || response.status === 410) throw new Error("The account's DID is not registered, or was deactivated");
5043
- if (response.status !== 200) throw new Error("The account's DID could not be resolved");
5044
- let doc;
5045
- try {
5046
- doc = JSON.parse(response.text);
5047
- } catch {
5048
- throw new Error("The account's DID document is malformed");
5049
- }
5050
- return parseAtprotoDidDocument(doc, did);
5051
- }
5052
- /**
5053
- * The DID a handle points to, or null when it points nowhere (or to two different DIDs). DNS first,
5054
- * then HTTPS, as the handle spec says. The HTTPS lookup first checks the handle's addresses are public,
5055
- * so a handle cannot make a contact's app knock on its local network.
5056
- */
5057
- async function resolveAtprotoHandle(handle, options) {
5058
- const lookup = {
5059
- fetch: options.fetch,
5060
- signal: options.signal,
5061
- resolver: options.resolver
5062
- };
5063
- try {
5064
- const answer = await dohQuery(`_atproto.${handle}`, "TXT", lookup);
5065
- const dids = /* @__PURE__ */ new Set();
5066
- for (const a of answer.answers) {
5067
- if (a.type !== 16) continue;
5068
- let text;
5069
- try {
5070
- text = txtValue(a.data);
5071
- } catch {
5072
- continue;
5073
- }
5074
- if (text.startsWith("did=")) dids.add(text.slice(4));
2580
+ source: `did:web · service in the did.json of ${host}`
2581
+ };
5075
2582
  }
5076
- if (dids.size > 1) return null;
5077
- const [did] = dids;
5078
- if (did) return isAtprotoDid(did) ? did : null;
5079
- } catch {}
5080
- try {
5081
- await assertPublicHost(handle, lookup);
5082
- const response = await options.fetch(`https://${handle}/.well-known/atproto-did`, {
5083
- headers: { accept: "text/plain" },
5084
- maxBytes: HANDLE_DID_MAX_BYTES,
5085
- signal: options.signal,
5086
- redirect: "error"
5087
- });
5088
- if (response.status !== 200) return null;
5089
- const did = response.text.trim();
5090
- return isAtprotoDid(did) ? did : null;
5091
- } catch {
5092
- return null;
5093
- }
5094
- }
5095
- /**
5096
- * The handle to show for a DID: the one its document claims, and only if that handle resolves back to
5097
- * the same DID. Otherwise nothing: the card shows the DID. Never throws.
5098
- */
5099
- async function verifiedAtprotoHandle(doc, options) {
5100
- if (!doc.handle) return void 0;
5101
- try {
5102
- return await resolveAtprotoHandle(doc.handle, options) === doc.did ? doc.handle : void 0;
5103
- } catch {
5104
- return;
2583
+ const text = await fetchDidWebFile(subject, statement.id, options);
2584
+ if (text === void 0) throw new DidError(`${didWebFileUrl(subject, statement.id)} was not found: it was deleted, or not uploaded yet`);
2585
+ if (!didWebFileNames(text, subject, statement.text)) throw new DidError(`${didWebFileUrl(subject, statement.id)} is not the statement of this proof`);
2586
+ return {
2587
+ subject,
2588
+ source: `did:web · statement file beside the did.json of ${host}`
2589
+ };
5105
2590
  }
5106
- }
2591
+ };
5107
2592
  //#endregion
5108
2593
  //#region ../browser/src/proofs/atproto/oauth.ts
5109
2594
  /**
@@ -6299,1387 +3784,10 @@ function identityProviders() {
6299
3784
  ...testIdentitiesEnabled() ? FAKE_IDENTITY_PROVIDERS : []
6300
3785
  ];
6301
3786
  }
6302
- //#endregion
6303
- //#region ../browser/src/shared/storedFiles.ts
6304
- /** The bytes a file holds: its metadata says, and a file stored before that says it with its Blob. */
6305
- function storedSize(file) {
6306
- return file.metadata?.size ?? file.blob?.size ?? 0;
6307
- }
6308
- /**
6309
- * A group's file (WISP 503) that is not here, read from its record: when the engine shows no transfer for it, it waits
6310
- * for its person's Download. The engine keeps no view of such a file in the states it builds (one for every file a
6311
- * group ever had would go into each of them), so the app's bubble and the CLI read it here. Null for any other file.
6312
- */
6313
- function waitingGroupFile(file) {
6314
- if (!file?.group || file.direction !== "in" || file.transfer?.state !== "transferring") return null;
6315
- return {
6316
- state: "transferring",
6317
- stage: "asking",
6318
- transferred: 0,
6319
- size: file.metadata?.size ?? file.transfer.size,
6320
- direction: "in"
6321
- };
6322
- }
6323
- async function backend(file) {
6324
- return file.bytes ? fileBytesOf(file.bytes) : null;
6325
- }
6326
- /** At most `length` bytes from `offset`. */
6327
- async function readStored(file, offset, length) {
6328
- if (file.blob) return new Uint8Array(await file.blob.slice(offset, offset + length).arrayBuffer());
6329
- const bytes = await backend(file);
6330
- if (!bytes) throw new Error("The file is gone");
6331
- return bytes.read(file.id, offset, length);
6332
- }
6333
- /** The file, a step at a time, from `offset`. */
6334
- async function* streamStored(file, offset = 0) {
6335
- const size = storedSize(file);
6336
- while (offset < size) {
6337
- const part = await readStored(file, offset, Math.min(FILE_BYTES_STEP, size - offset));
6338
- if (!part.length) throw new Error("The file is shorter than it says");
6339
- offset += part.length;
6340
- yield part;
6341
- }
6342
- }
6343
- /** A file's record and its bytes, wherever they are. */
6344
- async function removeStored(id) {
6345
- const file = await fileStore.get(id);
6346
- await fileStore.delete(id);
6347
- if (file?.bytes) await (await backend(file))?.remove(id);
6348
- }
6349
- /** Writes are gathered to this size before they go to storage: a chunk from the wire is 16 KiB. */
6350
- var GATHER = 262144;
6351
- /**
6352
- * Writes one file in order, gathering small writes into larger ones. `written` counts what was handed over,
6353
- * gathered or not; `flush` makes all of it durable.
6354
- */
6355
- var FileAppender = class {
6356
- bytes;
6357
- id;
6358
- buffer = new Uint8Array(GATHER);
6359
- buffered = 0;
6360
- stored;
6361
- constructor(bytes, id, offset = 0) {
6362
- this.bytes = bytes;
6363
- this.id = id;
6364
- this.stored = offset;
6365
- }
6366
- get written() {
6367
- return this.stored + this.buffered;
6368
- }
6369
- async append(chunk) {
6370
- if (this.buffered + chunk.length > GATHER) await this.drain();
6371
- if (chunk.length >= GATHER) {
6372
- await this.bytes.append(this.id, this.stored, chunk);
6373
- this.stored += chunk.length;
6374
- return;
6375
- }
6376
- this.buffer.set(chunk, this.buffered);
6377
- this.buffered += chunk.length;
6378
- }
6379
- async drain() {
6380
- if (!this.buffered) return;
6381
- const part = this.buffer.slice(0, this.buffered);
6382
- await this.bytes.append(this.id, this.stored, part);
6383
- this.stored += part.length;
6384
- this.buffered = 0;
6385
- }
6386
- async flush() {
6387
- await this.drain();
6388
- await this.bytes.flush(this.id);
6389
- }
6390
- async close() {
6391
- await this.drain();
6392
- await this.bytes.close(this.id);
6393
- }
6394
- };
6395
- //#endregion
6396
- //#region ../browser/src/shared/types.ts
6397
- var WALLET_TYPES = [
6398
- "cashu",
6399
- "lightning",
6400
- "arkade",
6401
- "bark",
6402
- "spark",
6403
- "bitcoin",
6404
- "fedimint",
6405
- "usdt"
6406
- ];
6407
- /** A details record larger than this (as JSON) is trimmed: its oldest sends go first. */
6408
- var MESSAGE_DETAILS_MAX_BYTES = 2048;
6409
- //#endregion
6410
- //#region ../browser/src/engine/paymentAdapters/walletInstances.ts
6411
- var WALLET_NAMES = {
6412
- cashu: "Cashu",
6413
- lightning: "Lightning",
6414
- arkade: "Ark",
6415
- bark: "Bark",
6416
- spark: "Spark",
6417
- bitcoin: "Bitcoin",
6418
- fedimint: "Fedimint",
6419
- usdt: "USDT"
6420
- };
6421
- /** How long a one-click creation waits on its server before it gives up, saving nothing (tests shorten it). */
6422
- var createTiming = { timeoutMs: 6e4 };
6423
- /**
6424
- * The wallets a profile has, read from both networks' views: one per type and network, in the deck's order, and one
6425
- * per Lightning card. A wallet exists once it holds something to open (a seed, a source, a mint); Lightning through
6426
- * the Cashu mints comes with that network's Cashu wallet.
6427
- */
6428
- function walletInstances(networks) {
6429
- const list = [];
6430
- for (const type of WALLET_TYPES) for (const network of WALLET_NETWORKS) {
6431
- if (type === "lightning") {
6432
- list.push(...lightningInstances(network, networks[network]));
6433
- continue;
6434
- }
6435
- const config = instanceConfig(type, networks[network]);
6436
- if (config) list.push({
6437
- id: `${type}:${network}`,
6438
- type,
6439
- network,
6440
- config
6441
- });
6442
- }
6443
- return list;
6444
- }
6445
- /** A network's Lightning cards (a view from before cards: its one source, as one card). */
6446
- function lightningInstances(network, view) {
6447
- return (view.lightnings ?? (view.lightning ? [{
6448
- ...view.lightning,
6449
- card: view.lightning.providerId === "cashu-mint" || !view.lightning.providerId ? "cashu" : "main",
6450
- name: "",
6451
- receive: true
6452
- }] : [])).flatMap((ln) => {
6453
- if (!ln.providerId || ln.providerId === "cashu-mint" && !view.mints.length) return [];
6454
- return [{
6455
- id: `lightning:${network}:${ln.card}`,
6456
- type: "lightning",
6457
- network,
6458
- config: {
6459
- providerId: ln.providerId,
6460
- label: ln.label ?? ""
6461
- },
6462
- card: ln.card,
6463
- name: ln.name || ln.label || "Lightning",
6464
- receive: ln.receive
6465
- }];
6466
- });
6467
- }
6468
- function instanceConfig(type, view) {
6469
- const text = (value) => typeof value === "string" ? value : "";
6470
- switch (type) {
6471
- case "cashu": return view.mints.length ? {
6472
- mint: view.mints[0].url,
6473
- mints: String(view.mints.length)
6474
- } : void 0;
6475
- case "lightning": return;
6476
- case "arkade": return view.ark?.configured ? {
6477
- chain: text(view.ark.network),
6478
- provider: text(view.ark.provider)
6479
- } : void 0;
6480
- case "bark": return view.bark?.configured ? {
6481
- chain: text(view.bark.network),
6482
- provider: text(view.bark.provider)
6483
- } : void 0;
6484
- case "spark": return view.spark?.configured ? { chain: text(view.spark.network) } : void 0;
6485
- case "bitcoin": return view.bitcoin?.providerId ? {
6486
- providerId: view.bitcoin.providerId,
6487
- label: text(view.bitcoin.label)
6488
- } : void 0;
6489
- case "fedimint": return view.fedimint?.federations.length ? { federations: String(view.fedimint.federations.length) } : void 0;
6490
- case "usdt": return view.usdt?.configured ? {
6491
- chain: text(view.usdt.network),
6492
- provider: text(view.usdt.provider)
6493
- } : void 0;
6494
- }
6495
- }
6496
- /** For each way of paying, the networks this profile has a wallet on: what every chat tells its contact. */
6497
- function paymentNetworksOf(wallets) {
6498
- const networks = {};
6499
- for (const wallet of wallets) {
6500
- const method = wallet.type;
6501
- networks[method] = [.../* @__PURE__ */ new Set([...networks[method] ?? [], wallet.network])];
6502
- }
6503
- return networks;
6504
- }
6505
- /**
6506
- * The network of a payment or a request: written on it since wallets have their own, else read from what it
6507
- * carries (a target's chain, a mint, an invoice's chain). A test mint's invoice looks like a Bitcoin one; its
6508
- * mints say Testnet.
6509
- */
6510
- function paymentNetwork(payment) {
6511
- if (payment.network) return payment.network;
6512
- if (payment.target) return payment.target.method === "cashu" ? mintNetwork(payment.target.provider) : walletNetworkOf(payment.target.network);
6513
- const mint = payment.mints?.[0] ?? payment.mint;
6514
- if (mint) return mintNetwork(mint);
6515
- const decoded = payment.invoice ? decodeBolt11(payment.invoice) : null;
6516
- return decoded && decoded.network !== "bitcoin" ? "testnet" : "mainnet";
6517
- }
6518
- /** The refusal when a card of one network would pay for the other: test coins and real money never meet. */
6519
- var crossNetwork = (card, asked) => card === asked ? `This is a ${networkLabel(asked)} payment (${asked === "mainnet" ? "real money" : "test coins"}): a ${networkLabel(card)} wallet never pays it. Use a ${networkLabel(asked)} wallet.` : engineText(asked === "mainnet" ? "mainnetPaymentOnTestnet" : "testnetPaymentOnMainnet");
6520
- /**
6521
- * Real money goes out only once the person confirmed it as real money ("Send real money"), whatever screen asked:
6522
- * a spend RPC on Mainnet without `confirmedReal` is refused before anything is spent. Test coins need no second step.
6523
- */
6524
- function assertConfirmedReal(network, confirmedReal) {
6525
- if (network === "mainnet" && confirmedReal !== true) throw new Error(REAL_MONEY_UNCONFIRMED);
6526
- }
6527
- var REAL_MONEY_UNCONFIRMED = engineText("realMoneyUnconfirmed");
6528
- /** One clear message for a creation that failed: nothing was saved, and trying again is safe. */
6529
- function createFailure(label, error) {
6530
- return `Could not create the ${label} wallet: ${(networkReason(error) ?? (error instanceof Error ? error.message : String(error))).replace(/\.$/, "")}. Nothing was saved; try again.`;
6531
- }
6532
- //#endregion
6533
- //#region ../browser/src/shared/paymentIds.ts
6534
- /**
6535
- * Payment ids are chosen by whoever sends them, and every member of a group sees the requests the others make to the
6536
- * group. So a contact's request is kept under the id it came with, unless a record of another chat already holds that
6537
- * id: then under `<id>.<chat>`. A `.` is never in a payment id on the wire (8 to 64 of `A-Z a-z 0-9 _ -`), so no
6538
- * contact can name another's record, and two contacts' requests never share one.
6539
- */
6540
- var paymentAlias = (wireId, linkId) => `${wireId}.${linkId}`;
6541
- /** The id a payment record goes by on the wire, which is also the one its chat and its group's notes know it by. */
6542
- var paymentWireId = (key) => key.split(".", 1)[0];
6543
- //#endregion
6544
- //#region ../browser/src/shared/replies.ts
6545
- /** A paired chat's text (`me_`/`peer_` + its 22-character wire id), or a held file (`peer_` + its 16-character one). */
6546
- var PAIRED_ROW = /^(?:me|peer)_([A-Za-z0-9_-]{22}|[A-Za-z0-9_-]{16})$/;
6547
- var WIRE_ID = /^[A-Za-z0-9_-]{8,64}$/;
6548
- var GROUP_LINK = "group:";
6549
- /**
6550
- * The id a message of a chat goes by on both sides, which a reply names; none for what only this side has (a notice,
6551
- * a group payment line, a refused payment). A paired chat: the text's wire id, a payment's id, a file's wire id. A
6552
- * group: the message id, which is the same for every member.
6553
- */
6554
- function replyRef(message, group = message.linkId?.startsWith(GROUP_LINK) ?? false) {
6555
- if (message.ref) return message.ref;
6556
- if (group) return message.paymentId || message.event || message.groupPay || !message.id.includes(":") ? void 0 : message.id;
6557
- if (message.paymentId) return paymentWireId(message.paymentId);
6558
- if (message.wireId && WIRE_ID.test(message.wireId)) return message.wireId;
6559
- const out = message.file?.id.match(/-out-([A-Za-z0-9_-]{8,64})$/);
6560
- if (out) return out[1];
6561
- const row = message.id.match(PAIRED_ROW);
6562
- return row && !/^\d+$/.test(row[1]) ? row[1] : void 0;
6563
- }
6564
- /** A reply to `original` as this side keeps it: always found, since it is here. The engine's row, or the UI's copy. */
6565
- function replyTo(original, ref) {
6566
- return {
6567
- id: ref,
6568
- snippet: replySnippet(original.text),
6569
- from: original.sender === "me" ? "me" : "peer",
6570
- ...original.member && { member: original.member },
6571
- messageId: original.id
6572
- };
6573
- }
6574
- /**
6575
- * A reply as it goes on a paired chat's wire, from the side that sends it (`f`: who wrote the original, from the
6576
- * sender's side). For a group, `f` is the author's member key, which the reply keeps.
6577
- */
6578
- function pairedWireReply(reply, sender = "me") {
6579
- return {
6580
- i: reply.id,
6581
- s: reply.snippet,
6582
- f: reply.from === sender ? "sender" : "recipient",
6583
- ...reply.button && { b: reply.button }
6584
- };
6585
- }
6586
- /** A paired chat's reply as it arrived (`f` from the sender's side), as this side keeps it. */
6587
- function receivedPairedReply(reply) {
6588
- if (!("f" in reply)) return {
6589
- id: reply.i,
6590
- snippet: ""
6591
- };
6592
- return {
6593
- id: reply.i,
6594
- snippet: reply.s,
6595
- from: reply.f === "sender" ? "peer" : "me",
6596
- ...reply.b && { button: reply.b }
6597
- };
6598
- }
6599
- /** A group's reply as it arrived or was sent (`f` the author's member key), as this member keeps it. */
6600
- function groupReply(reply, myKey) {
6601
- return {
6602
- id: reply.i,
6603
- snippet: reply.s,
6604
- from: reply.f === myKey ? "me" : "peer",
6605
- member: reply.f,
6606
- ...reply.b && { button: reply.b }
6607
- };
6608
- }
6609
- //#endregion
6610
- //#region ../browser/src/engine/groupFiles.ts
6611
- /** A group file's local id: its group's and a random part, never anything a member sends. */
6612
- var groupFileId = (groupId, direction, random = toBase64Url(randomBytes(12))) => `group-${groupId}-${direction}-${random}`;
6613
- /** The group a local file id names, among `groupIds`. */
6614
- var groupOfFile = (fileId, groupIds) => [...groupIds].find((g) => fileId.startsWith(`group-${g}-in-`) || fileId.startsWith(`group-${g}-out-`));
6615
- /** What a file's message says: its caption, or (when the author wrote none) the file itself, not the line older apps show. */
6616
- var groupFileText = (text, meta) => text === groupFileFallback(meta) ? fileMessageText(meta) : text;
6617
- /** The profile's files store and file storage, as a 1:1 chat's files use them. */
6618
- var storedGroupFiles = {
6619
- list: (groupId) => fileStore.listForLink(`group:${groupId}`),
6620
- groups: () => fileStore.groupIds(),
6621
- get: (id) => fileStore.get(id),
6622
- put: (file) => fileStore.put(file),
6623
- patch: (id, fields) => fileStore.patch(id, fields),
6624
- remove: (id) => removeStored(id),
6625
- async write(id, resume = false) {
6626
- const stored = await fileStore.get(id);
6627
- const bytes = stored?.bytes && await fileBytesOf(stored.bytes) || await fileBytes();
6628
- const size = await bytes.size(id);
6629
- const offset = resume && size !== null ? size : 0;
6630
- if (size !== null && size !== offset) await bytes.truncate(id, offset);
6631
- if (stored && stored.bytes !== bytes.kind) await fileStore.patch(id, { bytes: bytes.kind });
6632
- const appender = new FileAppender(bytes, id, offset);
6633
- return {
6634
- offset,
6635
- append: (chunk) => appender.append(chunk),
6636
- flush: () => appender.flush(),
6637
- digest: async () => {
6638
- await appender.close();
6639
- return bytes.digest(id);
6640
- },
6641
- close: () => appender.close(),
6642
- discard: async () => {
6643
- await appender.close().catch(() => {});
6644
- await bytes.remove(id);
6645
- }
6646
- };
6647
- },
6648
- read: (file, offset, length) => readStored(file, offset, length),
6649
- room: async () => (await fileBytes()).room()
6650
- };
6651
- /**
6652
- * Ended transfers an edge keeps the record of. A late frame for one of them is still answered as files/3 says (an abort
6653
- * to an asker going on with a transfer cancelled here); older ones are forgotten, so an edge's records stay few
6654
- * however many transfers it has seen.
6655
- */
6656
- var ENDED_KEPT = 8;
6657
- /** Why a want waits, as the person reads it (the app shows its own words for each `wait`). */
6658
- var WAITING_NOTES = {
6659
- nobody: "Nobody you are connected to has this file yet",
6660
- busy: "Its holders are busy; asking again soon",
6661
- damaged: "It arrived damaged; ask again"
6662
- };
6663
- /** Why a want stopped: every holder served bytes that are not the author's file. */
6664
- var DAMAGED = "Every copy of this file arrived damaged and was deleted. Ask for it again later.";
6665
- var GroupFileDesk = class {
6666
- deps;
6667
- kept = /* @__PURE__ */ new Map();
6668
- /** `<group>\n<message id>` → local id. */
6669
- byMessage = /* @__PURE__ */ new Map();
6670
- wants = /* @__PURE__ */ new Map();
6671
- /**
6672
- * The want last asked on each edge (by link id), and its file: whether a file is asked of an edge's member is read here,
6673
- * not by going through every want each time (`askedOf`). An entry the want has moved on from is dropped when read.
6674
- */
6675
- askedOn = /* @__PURE__ */ new Map();
6676
- edgesById = /* @__PURE__ */ new Map();
6677
- /** My file about to be announced, by `<group>\n<digest>`: the message that announces it takes this local id. */
6678
- own = /* @__PURE__ */ new Map();
6679
- /** Announcements sent per group, and taken per member of a group (WISP 503 § Rules for both, Pace). */
6680
- sentPace = /* @__PURE__ */ new Map();
6681
- sentTaken = /* @__PURE__ */ new Map();
6682
- takenPace = /* @__PURE__ */ new Map();
6683
- /** `<group>\n<message id>` a `group-have` named before its message came → its holders, newest first. */
6684
- ahead = /* @__PURE__ */ new Map();
6685
- /** A community's ids for its next `group-have`, and when one last went. */
6686
- communityHave = /* @__PURE__ */ new Map();
6687
- /** Bytes served today, and the day (UTC days since the epoch). */
6688
- served = {
6689
- day: 0,
6690
- bytes: 0
6691
- };
6692
- constructor(deps) {
6693
- this.deps = deps;
6694
- }
6695
- /**
6696
- * At start: each group's files as they were kept. One fetched by itself, or asked for, and not here yet is asked for
6697
- * again; with automatic downloads off, one chosen by itself before waits for a Download instead.
6698
- */
6699
- async load(groupIds) {
6700
- for (const groupId of groupIds) for (const file of await this.deps.store.list(groupId).catch(() => [])) {
6701
- if (!file.group || !file.metadata || !file.digest || this.kept.has(file.id)) continue;
6702
- const { name, size, mime, voice, video, image } = file.metadata;
6703
- const kept = {
6704
- id: file.id,
6705
- groupId,
6706
- message: file.group.message,
6707
- author: file.group.author,
6708
- meta: {
6709
- name,
6710
- size,
6711
- mime,
6712
- d: file.digest,
6713
- ...voice && { voice },
6714
- ...video && { video },
6715
- ...image && { image }
6716
- },
6717
- mine: file.direction === "out",
6718
- auto: !!file.group.auto,
6719
- asked: !!file.group.asked,
6720
- held: file.transfer?.state === "done",
6721
- holders: [],
6722
- damaged: /* @__PURE__ */ new Set()
6723
- };
6724
- this.add(kept);
6725
- if (kept.held) continue;
6726
- if (file.transfer?.state === "failed") this.show(kept, {
6727
- state: "failed",
6728
- transferred: 0,
6729
- size,
6730
- direction: "in",
6731
- retry: true,
6732
- error: file.transfer.error ?? DAMAGED
6733
- });
6734
- else if (file.group.paused) {
6735
- kept.paused = true;
6736
- kept.stored = await this.keptBytes(kept.id);
6737
- this.show(kept, {
6738
- state: "transferring",
6739
- stage: "paused",
6740
- pausedBy: "me",
6741
- transferred: kept.stored,
6742
- size,
6743
- direction: "in"
6744
- });
6745
- } else if (kept.asked || kept.auto && this.deps.settings().autoDownloads) this.wants.set(kept.id, this.newWant());
6746
- else if (kept.auto) await this.stop(kept, false);
6747
- }
6748
- for (const [id] of this.wants) this.ask(this.kept.get(id));
6749
- this.deps.changed();
6750
- }
6751
- /**
6752
- * At start, once every group was read: the files of a group this device no longer has (`groupIds` are the groups it
6753
- * has, a tombstone or a join under way among them) go, with their bytes. A group left before Leave took its files
6754
- * left them behind, where nothing lists them.
6755
- */
6756
- async sweep(groupIds) {
6757
- const here = new Set(groupIds);
6758
- for (const groupId of await this.deps.store.groups().catch(() => [])) {
6759
- if (here.has(groupId)) continue;
6760
- for (const file of await this.deps.store.list(groupId).catch(() => [])) await this.deps.store.remove(file.id).catch(() => {});
6761
- }
6762
- }
6763
- /**
6764
- * The person stops a file that is coming, or that this device would fetch (Cancel, Decline): nobody is asked for it
6765
- * any more, the transfer under way ends, what came of it is deleted, and it waits for a Download. Paused (`keep`),
6766
- * what came stays, and Resume (`download`) goes on from there.
6767
- */
6768
- async halt(fileId, keep = false) {
6769
- const kept = this.kept.get(fileId);
6770
- if (!kept) throw new Error("No such file");
6771
- if (keep && !this.wants.has(fileId)) return;
6772
- await this.stop(kept, keep);
6773
- }
6774
- /** Automatic downloads were turned off: files chosen by themselves and not here yet stop coming, and wait for a Download. */
6775
- async autoDownloadsOff() {
6776
- for (const kept of [...this.kept.values()]) if (kept.auto && !kept.asked && this.wants.has(kept.id)) await this.stop(kept, false);
6777
- }
6778
- async stop(kept, keep) {
6779
- if (kept.held || kept.mine) return;
6780
- const want = this.wants.get(kept.id);
6781
- this.wants.delete(kept.id);
6782
- const edge = want?.wire && want.linkId ? this.edgesById.get(want.linkId) : void 0;
6783
- const record = edge?.files.get("in", want.wire);
6784
- const { size } = kept.meta;
6785
- const failed = !keep && this.deps.transfers.get(kept.id)?.state === "failed";
6786
- if (!keep) {
6787
- kept.stored = 0;
6788
- kept.fresh = true;
6789
- }
6790
- if (edge && record && !transferEnded(record)) {
6791
- if (!keep) kept.dropped = record.id;
6792
- edge.files.cancel("in", record.id);
6793
- edge.taking.delete(record.id);
6794
- } else if (!keep) kept.idle = (kept.idle ?? Promise.resolve()).then(() => this.deps.store.write(kept.id)).then((writer) => writer.discard()).catch(() => {});
6795
- if (keep) this.show(kept, {
6796
- state: "transferring",
6797
- stage: "paused",
6798
- pausedBy: "me",
6799
- transferred: kept.stored ?? 0,
6800
- size,
6801
- direction: "in"
6802
- });
6803
- else this.waiting(kept);
6804
- const wasPaused = !!kept.paused;
6805
- kept.paused = keep;
6806
- if (kept.auto || kept.asked || wasPaused !== keep || failed) {
6807
- kept.auto = kept.asked = false;
6808
- await this.deps.store.patch(kept.id, {
6809
- group: this.groupField(kept),
6810
- ...failed && { transfer: {
6811
- state: "transferring",
6812
- transferred: 0,
6813
- size
6814
- } }
6815
- }).catch(() => {});
6816
- }
6817
- }
6818
- /**
6819
- * An error when this group took as many announcements of mine as it takes in a minute, or as its members take from
6820
- * one member in `announceWindowMs` (past that they would not fetch the file by themselves); null when one may go now.
6821
- */
6822
- mayAnnounce(groupId) {
6823
- const wait = Math.max(...this.sentPaces(groupId).map((pace) => pace.wait()));
6824
- if (!wait) return null;
6825
- const minutes = Math.ceil(wait / 6e4);
6826
- return `You sent many files to this group just now. Wait ${minutes > 1 ? `${minutes} minutes` : "a minute"}.`;
6827
- }
6828
- /** My file, stored under `fileId` with the author's digest, is about to be announced in the group: its message takes it. */
6829
- outgoing(groupId, fileId, meta) {
6830
- this.own.set(`${groupId}\n${meta.d}`, fileId);
6831
- }
6832
- /** The announcement did not go: the file is not this group's after all. */
6833
- notSent(groupId, meta) {
6834
- this.own.delete(`${groupId}\n${meta.d}`);
6835
- }
6836
- /**
6837
- * A message of the group announces a file (mine, or a member's): the file its row shows. A member's is kept here
6838
- * unfetched, and fetched now when it is one this device takes by itself and the member announced no more than the
6839
- * group fetches that way. The same message again (a catch-up, a whole copy) is the same file.
6840
- */
6841
- async announced(groupId, m, me) {
6842
- const known = this.byMessage.get(`${groupId}\n${m.id}`);
6843
- if (known) return messageFile(this.kept.get(known));
6844
- const ownId = m.sender === me ? this.own.get(`${groupId}\n${m.file.d}`) : void 0;
6845
- if (ownId) {
6846
- this.own.delete(`${groupId}\n${m.file.d}`);
6847
- for (const pace of this.sentPaces(groupId)) pace.take();
6848
- const kept = {
6849
- id: ownId,
6850
- groupId,
6851
- message: m.id,
6852
- author: me,
6853
- meta: m.file,
6854
- mine: true,
6855
- auto: false,
6856
- asked: false,
6857
- held: true,
6858
- holders: [],
6859
- damaged: /* @__PURE__ */ new Set()
6860
- };
6861
- this.add(kept);
6862
- const transfer = {
6863
- state: "done",
6864
- transferred: m.file.size,
6865
- size: m.file.size
6866
- };
6867
- await this.deps.store.patch(ownId, {
6868
- group: {
6869
- message: m.id,
6870
- author: me
6871
- },
6872
- transfer
6873
- }).catch(() => {});
6874
- this.finished(kept, "out");
6875
- this.queueHave(kept);
6876
- return messageFile(kept);
6877
- }
6878
- const paced = this.pace(this.takenPace, `${groupId}\n${m.sender}`, GROUP_FILE_LIMITS.announceTaken, GROUP_FILE_LIMITS.announceWindowMs).take();
6879
- const id = groupFileId(groupId, "in");
6880
- const auto = paced && this.deps.settings().autoDownloads && fetchesByItself(m.file, this.keptAutomatically(groupId));
6881
- const holders = this.ahead.get(`${groupId}\n${m.id}`) ?? [];
6882
- this.ahead.delete(`${groupId}\n${m.id}`);
6883
- const kept = {
6884
- id,
6885
- groupId,
6886
- message: m.id,
6887
- author: m.sender,
6888
- meta: m.file,
6889
- mine: false,
6890
- auto,
6891
- asked: false,
6892
- held: false,
6893
- holders,
6894
- damaged: /* @__PURE__ */ new Set()
6895
- };
6896
- const { name, size, mime, voice, video, image } = m.file;
6897
- this.add(kept);
6898
- await this.deps.store.put({
6899
- id,
6900
- linkId: `group:${groupId}`,
6901
- direction: "in",
6902
- digest: m.file.d,
6903
- createdAt: this.deps.now(),
6904
- metadata: {
6905
- name,
6906
- size,
6907
- mime,
6908
- timestamp: this.deps.now(),
6909
- voice,
6910
- video,
6911
- image
6912
- },
6913
- transfer: {
6914
- state: "transferring",
6915
- transferred: 0,
6916
- size
6917
- },
6918
- group: {
6919
- message: m.id,
6920
- author: m.sender,
6921
- ...auto && { auto: true }
6922
- }
6923
- });
6924
- if (auto) {
6925
- this.wants.set(id, this.newWant());
6926
- this.ask(kept);
6927
- } else this.waiting(kept);
6928
- return messageFile(kept);
6929
- }
6930
- /** The person asks for a file this device did not fetch by itself (Download), or for one that stopped coming. */
6931
- async download(fileId) {
6932
- const kept = this.kept.get(fileId);
6933
- if (!kept) throw new Error("No such file");
6934
- if (kept.held) return;
6935
- const room = await this.deps.store.room().catch(() => null);
6936
- if (room !== null && kept.meta.size > room) throw new Error("Not enough space on this device for this file");
6937
- if (!kept.asked || kept.paused) {
6938
- kept.asked = true;
6939
- kept.paused = false;
6940
- await this.deps.store.patch(kept.id, { group: this.groupField(kept) }).catch(() => {});
6941
- }
6942
- kept.damaged.clear();
6943
- if (this.deps.transfers.get(kept.id)?.state === "failed") await this.deps.store.patch(kept.id, { transfer: {
6944
- state: "transferring",
6945
- transferred: 0,
6946
- size: kept.meta.size
6947
- } }).catch(() => {});
6948
- const want = this.wants.get(kept.id);
6949
- if (!want) this.wants.set(kept.id, this.newWant());
6950
- else if (!want.wire) {
6951
- want.tried.clear();
6952
- want.why = want.busy = void 0;
6953
- }
6954
- this.ask(kept);
6955
- }
6956
- /** The group is gone from this device: its files go too. */
6957
- async drop(groupId) {
6958
- for (const kept of [...this.kept.values()].filter((k) => k.groupId === groupId)) {
6959
- this.kept.delete(kept.id);
6960
- this.byMessage.delete(`${groupId}\n${kept.message}`);
6961
- this.wants.delete(kept.id);
6962
- this.deps.transfers.delete(kept.id);
6963
- await this.deps.store.remove(kept.id).catch(() => {});
6964
- }
6965
- for (const [linkId, edge] of this.edgesById) if (edge.groupId === groupId) {
6966
- edge.files.detach();
6967
- this.edgesById.delete(linkId);
6968
- }
6969
- for (const key of [...this.ahead.keys()]) if (key.startsWith(`${groupId}\n`)) this.ahead.delete(key);
6970
- this.communityHave.delete(groupId);
6971
- this.deps.changed();
6972
- }
6973
- /** An edge's files/3 session opened (both sides agreed it) or closed. */
6974
- session(groupId, peer, linkId, open) {
6975
- const edge = this.edge(groupId, peer, linkId);
6976
- if (open) edge.files.attach();
6977
- else edge.files.detach();
6978
- edge.gone.clear();
6979
- if (open) this.retry(groupId);
6980
- }
6981
- /** An edge came up and both sides said where they are: what this device holds goes to it, and wants waiting for a holder go. */
6982
- edgeReady(groupId, peer, linkId) {
6983
- if (!this.deps.ready(linkId)) return;
6984
- const edge = this.edge(groupId, peer, linkId);
6985
- if (this.deps.settings().serveFiles) {
6986
- const held = [...this.kept.values()].filter((k) => k.groupId === groupId && k.held).map((k) => k.message).slice(-GROUP_FILE_LIMITS.haveIds);
6987
- if (held.length) this.sendHave(edge, held);
6988
- }
6989
- this.retry(groupId);
6990
- }
6991
- /** A `group-want`, `group-want-no` or `group-have` on an edge, from the member it is pinned to. */
6992
- async frame(groupId, peer, linkId, frame) {
6993
- if (frame.t === "group-want") {
6994
- const want = readGroupWant(frame, groupId);
6995
- if (want) await this.serve(this.edge(groupId, peer, linkId), want.id);
6996
- } else if (frame.t === "group-want-no") {
6997
- const no = readGroupWantNo(frame, groupId);
6998
- if (no) this.refused(groupId, peer, linkId, no.id, no.why);
6999
- } else if (frame.t === "group-have") {
7000
- const ids = readGroupHave(frame, groupId);
7001
- if (ids) this.heard(groupId, peer, ids);
7002
- }
7003
- }
7004
- /** A community's `group-have`, an application frame its signer sent the group. */
7005
- communityApp(groupId, sender, frame) {
7006
- const ids = readGroupHave(frame);
7007
- if (ids) this.heard(groupId, sender, ids);
7008
- }
7009
- /**
7010
- * A files/3 frame on an edge. An offer is taken only for a file this device asked that member for (`gm` naming the
7011
- * message): nobody can push a file into another's storage. What it brings belongs to the group's message, not to the
7012
- * edge as a chat.
7013
- */
7014
- filesFrame(groupId, peer, linkId, frame) {
7015
- const edge = this.edge(groupId, peer, linkId);
7016
- if (frame.t === "pf-offer" && typeof frame.id === "string" && /^[A-Za-z0-9_-]{8,64}$/.test(frame.id) && !edge.taking.has(frame.id)) {
7017
- const kept = isGroupMessageId(frame.gm) ? this.kept.get(this.byMessage.get(`${groupId}\n${frame.gm}`) ?? "") : void 0;
7018
- const want = kept && this.wants.get(kept.id);
7019
- if (!kept || kept.held || !want || want.linkId !== linkId || want.wire) {
7020
- this.deps.sendFiles(linkId, {
7021
- t: "pf-refuse",
7022
- id: frame.id,
7023
- why: "invalid"
7024
- });
7025
- return Promise.resolve();
7026
- }
7027
- edge.taking.set(frame.id, kept.id);
7028
- kept.holders = newestHolder(peer, kept.holders);
7029
- want.wire = frame.id;
7030
- want.why = void 0;
7031
- want.movedAt = this.deps.now();
7032
- }
7033
- return edge.files.handle(frame);
7034
- }
7035
- /** Wants that waited too long for an offer, or for bytes, go to the next holder; `group-have`s gathered go. */
7036
- tick() {
7037
- const now = this.deps.now();
7038
- for (const [id, want] of this.wants) {
7039
- const kept = this.kept.get(id);
7040
- if (!kept) {
7041
- this.wants.delete(id);
7042
- continue;
7043
- }
7044
- if (now - (want.wire ? want.movedAt : want.at) < GROUP_FILE_LIMITS.wantWaitMs) continue;
7045
- if (want.wire) {
7046
- const edge = want.linkId ? this.edgesById.get(want.linkId) : void 0;
7047
- if (edge?.files.get("in", want.wire) && !transferEnded(edge.files.get("in", want.wire))) {
7048
- edge.files.cancel("in", want.wire);
7049
- continue;
7050
- }
7051
- want.wire = void 0;
7052
- }
7053
- if (want.holder) want.tried.add(want.holder);
7054
- else {
7055
- want.tried.clear();
7056
- want.why = want.busy = void 0;
7057
- }
7058
- this.ask(kept);
7059
- }
7060
- this.letGo();
7061
- for (const edge of this.edgesById.values()) if (edge.have.size && this.deps.ready(edge.linkId)) this.sendHave(edge, [...edge.have]);
7062
- for (const [groupId, have] of this.communityHave) {
7063
- if (!have.ids.size || now - have.at < GROUP_FILE_LIMITS.communityHaveEveryMs) continue;
7064
- const ids = [...have.ids].slice(-GROUP_FILE_LIMITS.haveIds);
7065
- have.ids.clear();
7066
- have.at = now;
7067
- this.deps.sendApp(groupId, {
7068
- t: GROUP_HAVE_FRAME,
7069
- ids
7070
- }).catch(() => {});
7071
- }
7072
- }
7073
- newWant() {
7074
- return {
7075
- at: this.deps.now(),
7076
- tried: /* @__PURE__ */ new Set(),
7077
- moved: 0,
7078
- movedAt: 0
7079
- };
7080
- }
7081
- /**
7082
- * Wants of this group that wait for a holder: asked now (an edge came up or freed, or a member said it holds
7083
- * something). One queued behind an edge's file is asked only while an edge of the group is free to take it, and free
7084
- * for it (`freeFor`): once every edge has a file asked of it, or the free ones are of members it already asked, it would
7085
- * only queue again. So a file that ends asks the few wants it frees an edge for, not every want waiting (a member back
7086
- * from away with hundreds of files to fetch asked each of them once per file, more so with a member in the group that
7087
- * holds none of them, or serves none).
7088
- */
7089
- retry(groupId) {
7090
- let free;
7091
- for (const [id, want] of this.wants) {
7092
- const kept = this.kept.get(id);
7093
- if (kept?.groupId !== groupId || want.holder) continue;
7094
- if (this.deps.transfers.get(id)?.stage === "queued" && !this.freeFor(kept, want, free ??= this.freeEdges(groupId))) continue;
7095
- this.ask(kept);
7096
- free = void 0;
7097
- }
7098
- }
7099
- /** The group's edges that are up, with files/3 live and no file asked of their member: a want could be asked on them. */
7100
- freeEdges(groupId) {
7101
- return [...this.deps.edges(groupId)].filter(([, linkId]) => this.deps.ready(linkId) && this.edgesById.get(linkId)?.files.live && !this.askedOf(linkId));
7102
- }
7103
- /** One of these free edges (member key, link id) is one `ask` would ask this want on: asked now, it would not only queue again. */
7104
- freeFor(kept, want, free) {
7105
- const membership = this.deps.membership(kept.groupId);
7106
- if (!membership) return false;
7107
- const known = this.knownHolders(kept, membership);
7108
- const others = !this.waitForKnown(kept, want, known, this.deps.edges(kept.groupId));
7109
- return free.some(([key, linkId]) => this.mayAsk(kept, want, key, linkId) && (known.includes(key) || others && key !== membership.me && membership.couldRead(key, kept.message)));
7110
- }
7111
- /** The members known to hold the file (its author first), in the group's roster, but this device. */
7112
- knownHolders(kept, membership) {
7113
- return [kept.author, ...kept.holders.filter((key) => key !== kept.author)].filter((key) => membership && key !== membership.me && membership.inRoster(key));
7114
- }
7115
- /**
7116
- * A known holder that answered busy, or the author whose edge another file of this device's is asked of, its edge up,
7117
- * serves when its place frees: the others are asked only when no known holder is worth waiting for (none in reach, or
7118
- * every one gone), as after a restart here. Only the author's edge holds them back while it is asked of: any member
7119
- * can say it holds files, and one that serves the file asked of it slowly would keep every other file it named here.
7120
- */
7121
- waitForKnown(kept, want, known, edges) {
7122
- return known.some((key) => {
7123
- const linkId = edges.get(key) ?? "";
7124
- return this.deps.ready(linkId) && (want.busy?.has(key) || key === kept.author && this.askedOf(linkId, want));
7125
- });
7126
- }
7127
- /** The want may ask this member in this round: not asked yet, it did not serve it damaged, and did not say it is gone. */
7128
- mayAsk(kept, want, key, linkId) {
7129
- return !want.tried.has(key) && !kept.damaged.has(key) && !this.edgesById.get(linkId ?? "")?.gone.has(kept.message);
7130
- }
7131
- /**
7132
- * Asks the next holder for a file: the author first while its edge is up, then the holders whose edge is up, then every
7133
- * other member whose edge is up and that could read its message. Who holds what is heard, not kept: a holder names few
7134
- * of its files to an edge that opens, and none it was told of is known after a restart here, so a member in reach is
7135
- * asked whether or not it said so (one that does not hold it answers `gone`, and is not asked again). Only over an
7136
- * edge this device has. (A private group past 16 members, or a community, reaches a holder it has no edge with
7137
- * over a file session of the two members, WISP 503 § The file session; that session is not built yet, so such a
7138
- * holder is not asked, and the file waits for one that is reachable.)
7139
- */
7140
- ask(kept) {
7141
- const want = this.wants.get(kept.id);
7142
- if (!want || kept.held || want.wire) return;
7143
- const membership = this.deps.membership(kept.groupId);
7144
- const edges = this.deps.edges(kept.groupId);
7145
- const known = this.knownHolders(kept, membership);
7146
- if (known.length && known.every((key) => kept.damaged.has(key))) {
7147
- this.stopDamaged(kept);
7148
- return;
7149
- }
7150
- const others = this.waitForKnown(kept, want, known, edges) ? [] : [...edges.keys()].filter((key) => !known.includes(key) && membership && key !== membership.me && membership.couldRead(key, kept.message));
7151
- const order = [...known, ...others].filter((key) => this.mayAsk(kept, want, key, edges.get(key)));
7152
- let behind = false;
7153
- for (const key of order) {
7154
- const linkId = edges.get(key);
7155
- if (!linkId || !this.deps.ready(linkId) || !this.edgesById.get(linkId)?.files.live) continue;
7156
- if (this.askedOf(linkId, want)) {
7157
- behind = true;
7158
- continue;
7159
- }
7160
- Object.assign(want, {
7161
- holder: key,
7162
- linkId,
7163
- at: this.deps.now()
7164
- });
7165
- this.askedOn.set(linkId, {
7166
- id: kept.id,
7167
- want
7168
- });
7169
- if (!this.deps.send(linkId, {
7170
- t: "group-want",
7171
- g: kept.groupId,
7172
- id: kept.message
7173
- })) {
7174
- want.tried.add(key);
7175
- continue;
7176
- }
7177
- if (want.holder === key) this.show(kept, {
7178
- state: "transferring",
7179
- stage: "queued",
7180
- transferred: kept.stored ?? 0,
7181
- size: kept.meta.size,
7182
- direction: "in"
7183
- });
7184
- return;
7185
- }
7186
- Object.assign(want, {
7187
- holder: void 0,
7188
- linkId: void 0,
7189
- at: this.deps.now()
7190
- });
7191
- if (behind) {
7192
- this.show(kept, {
7193
- state: "transferring",
7194
- stage: "queued",
7195
- transferred: kept.stored ?? 0,
7196
- size: kept.meta.size,
7197
- direction: "in"
7198
- });
7199
- return;
7200
- }
7201
- const wait = want.why ?? "nobody";
7202
- this.show(kept, {
7203
- state: "transferring",
7204
- stage: "waiting",
7205
- transferred: kept.stored ?? 0,
7206
- size: kept.meta.size,
7207
- direction: "in",
7208
- wait,
7209
- note: WAITING_NOTES[wait],
7210
- ...wait === "damaged" && { stalled: true }
7211
- });
7212
- }
7213
- /**
7214
- * Another file is asked of, or coming from, the member at the other end of this edge. A want gets its edge only in
7215
- * `ask`, which notes it in `askedOn`, and one edge has one want at a time: the want noted there is it, while it still
7216
- * waits and is still on that edge.
7217
- */
7218
- askedOf(linkId, except) {
7219
- const on = this.askedOn.get(linkId);
7220
- if (!on || on.want === except) return false;
7221
- if (on.want.linkId === linkId && this.wants.get(on.id) === on.want) return true;
7222
- this.askedOn.delete(linkId);
7223
- return false;
7224
- }
7225
- /** No holder serves the author's bytes: the want stops, and stays stopped after a restart, until Download. */
7226
- stopDamaged(kept) {
7227
- this.wants.delete(kept.id);
7228
- const { size } = kept.meta;
7229
- this.deps.store.patch(kept.id, { transfer: {
7230
- state: "failed",
7231
- transferred: 0,
7232
- size,
7233
- error: DAMAGED
7234
- } }).catch(() => {});
7235
- this.show(kept, {
7236
- state: "failed",
7237
- transferred: 0,
7238
- size,
7239
- direction: "in",
7240
- retry: true,
7241
- error: DAMAGED
7242
- });
7243
- }
7244
- /** A holder's no: the next one is asked. One that deleted the file is no holder any more. */
7245
- refused(groupId, peer, linkId, messageId, why) {
7246
- const kept = this.kept.get(this.byMessage.get(`${groupId}\n${messageId}`) ?? "");
7247
- const want = kept && this.wants.get(kept.id);
7248
- if (!kept || !want || want.linkId !== linkId || want.wire) return;
7249
- if (why === "gone") {
7250
- kept.holders = kept.holders.filter((key) => key !== peer);
7251
- this.edgesById.get(linkId)?.gone.add(messageId);
7252
- }
7253
- if (why === "busy") {
7254
- want.why = "busy";
7255
- (want.busy ??= /* @__PURE__ */ new Set()).add(peer);
7256
- }
7257
- want.tried.add(peer);
7258
- want.holder = want.linkId = void 0;
7259
- this.ask(kept);
7260
- this.retry(groupId);
7261
- }
7262
- /** A member says it holds these files: remembered as a holder of each (newest first), and asked when a want waits. */
7263
- heard(groupId, from, ids) {
7264
- const membership = this.deps.membership(groupId);
7265
- if (!membership || from === membership.me || !membership.inRoster(from)) return;
7266
- const newest = (holders) => newestHolder(from, holders);
7267
- const edge = this.edgesById.get(this.deps.edges(groupId).get(from) ?? "");
7268
- let free;
7269
- for (const message of ids) {
7270
- const key = `${groupId}\n${message}`, kept = this.kept.get(this.byMessage.get(key) ?? "");
7271
- if (!kept) {
7272
- this.ahead.set(key, newest(this.ahead.get(key) ?? []));
7273
- if (this.ahead.size > GROUP_FILE_LIMITS.heardAhead) this.ahead.delete(this.ahead.keys().next().value);
7274
- continue;
7275
- }
7276
- if (kept.mine) continue;
7277
- kept.holders = newest(kept.holders);
7278
- edge?.gone.delete(message);
7279
- const want = this.wants.get(kept.id);
7280
- if (!want) continue;
7281
- want.tried.delete(from);
7282
- want.busy?.delete(from);
7283
- if (want.holder) continue;
7284
- if (this.deps.transfers.get(kept.id)?.stage === "queued" && !this.freeFor(kept, want, free ??= this.freeEdges(groupId).filter(([key]) => key === from))) continue;
7285
- this.ask(kept);
7286
- free = void 0;
7287
- }
7288
- }
7289
- /** A member asks for a file: offered over files/3 on its edge, or a `group-want-no` saying why not. */
7290
- async serve(edge, messageId) {
7291
- const kept = this.kept.get(this.byMessage.get(`${edge.groupId}\n${messageId}`) ?? "");
7292
- const here = !!kept?.held && !!await this.deps.store.get(kept.id).catch(() => void 0);
7293
- const why = this.mayServe(edge, messageId, here ? kept : void 0);
7294
- if (why || !kept) {
7295
- this.deps.send(edge.linkId, {
7296
- t: GROUP_WANT_NO_FRAME,
7297
- g: edge.groupId,
7298
- id: messageId,
7299
- why: why ?? "gone"
7300
- });
7301
- return;
7302
- }
7303
- const wire = toBase64Url(randomBytes(12));
7304
- edge.serving.set(wire, kept.id);
7305
- edge.servedMoved.set(wire, {
7306
- confirmed: 0,
7307
- at: this.deps.now()
7308
- });
7309
- const { name, mime, size, voice, video, image } = kept.meta;
7310
- const file = {
7311
- id: wire,
7312
- name,
7313
- mime,
7314
- size,
7315
- timestamp: this.deps.now(),
7316
- ...voice && { voice },
7317
- ...video && { video },
7318
- ...image && { image }
7319
- };
7320
- edge.files.offer(file, kept.meta.d);
7321
- }
7322
- /**
7323
- * Why this device does not serve a file now, or null when it does (WISP 503 § Asking, Serving limits): `refused` to
7324
- * someone who could not read the announcement, `gone` when it is not here (`kept` undefined), `busy` when serving is
7325
- * off or full.
7326
- */
7327
- mayServe(edge, messageId, kept) {
7328
- const membership = this.deps.membership(edge.groupId);
7329
- if (!membership || !membership.couldRead(edge.peer, messageId)) return "refused";
7330
- if (!kept) return "gone";
7331
- if (!this.deps.settings().serveFiles || !edge.files.live) return "busy";
7332
- this.letGo();
7333
- const active = this.servingNow();
7334
- if (active.length >= GROUP_FILE_LIMITS.serveAtOnce) {
7335
- edge.busy.add(messageId);
7336
- return "busy";
7337
- }
7338
- if (active.some((a) => a.peer === edge.peer && a.sending)) return "busy";
7339
- const today = Math.floor(this.deps.now() / 864e5);
7340
- if (this.served.day !== today) this.served = {
7341
- day: today,
7342
- bytes: 0
7343
- };
7344
- const reserved = active.reduce((sum, a) => sum + a.size, 0);
7345
- return this.served.bytes + reserved + kept.meta.size > GROUP_FILE_LIMITS.serveBytesPerDay ? "busy" : null;
7346
- }
7347
- /**
7348
- * Transfers served now, all groups together: to whom, what each takes of the day (its size, or what it sent when that
7349
- * is more), and whether bytes are still to go.
7350
- */
7351
- servingNow() {
7352
- const active = [];
7353
- for (const edge of this.edgesById.values()) for (const wire of edge.serving.keys()) {
7354
- const record = edge.files.get("out", wire);
7355
- if (record && !transferEnded(record)) active.push({
7356
- peer: edge.peer,
7357
- size: Math.max(record.file.size, edge.files.sent(wire)),
7358
- sending: record.confirmed < record.file.size
7359
- });
7360
- }
7361
- return active;
7362
- }
7363
- /**
7364
- * An asker has a served transfer send parts again (WISP 503 § Serving limits): every byte sent counts toward the day,
7365
- * so it goes on only while the day has room for it.
7366
- */
7367
- mayResend() {
7368
- const today = Math.floor(this.deps.now() / 864e5);
7369
- if (this.served.day !== today) this.served = {
7370
- day: today,
7371
- bytes: 0
7372
- };
7373
- return this.served.bytes + this.servingNow().reduce((sum, a) => sum + a.size, 0) < GROUP_FILE_LIMITS.serveBytesPerDay;
7374
- }
7375
- /**
7376
- * Served transfers that no longer count end, their places free for others (WISP 503 § Serving limits): the asker's
7377
- * session closed, it is not in the roster any more, or its bytes have not moved for `wantWaitMs` (the asker has moved
7378
- * on by then).
7379
- */
7380
- letGo() {
7381
- const now = this.deps.now();
7382
- for (const edge of this.edgesById.values()) for (const wire of [...edge.serving.keys()]) {
7383
- const record = edge.files.get("out", wire);
7384
- if (!record || transferEnded(record)) continue;
7385
- let moved = edge.servedMoved.get(wire);
7386
- if (!moved || moved.confirmed !== record.confirmed) edge.servedMoved.set(wire, moved = {
7387
- confirmed: record.confirmed,
7388
- at: now
7389
- });
7390
- if (!edge.files.live || !this.deps.membership(edge.groupId)?.inRoster(edge.peer) || now - moved.at >= GROUP_FILE_LIMITS.wantWaitMs) edge.files.cancel("out", wire);
7391
- }
7392
- }
7393
- /**
7394
- * Says on an edge which files this device holds, as many frames as the edge takes now (`haveFrames` in
7395
- * `haveWindowMs`); the ids past that, and those of a frame that could not go, wait for the next tick with room.
7396
- */
7397
- sendHave(edge, ids) {
7398
- for (const id of ids) edge.have.add(id);
7399
- for (let i = 0; i < ids.length; i += GROUP_FILE_LIMITS.haveIds) {
7400
- const frame = ids.slice(i, i + GROUP_FILE_LIMITS.haveIds);
7401
- if (edge.haveSent.wait() || !this.deps.send(edge.linkId, {
7402
- t: "group-have",
7403
- g: edge.groupId,
7404
- ids: frame
7405
- })) return;
7406
- edge.haveSent.take();
7407
- for (const id of frame) edge.have.delete(id);
7408
- }
7409
- }
7410
- /**
7411
- * This device holds a new file: the group hears it with the next `group-have`, which gathers what there is to say
7412
- * until the next tick.
7413
- */
7414
- queueHave(kept) {
7415
- if (!this.deps.settings().serveFiles) return;
7416
- if (this.deps.membership(kept.groupId)?.community) {
7417
- let have = this.communityHave.get(kept.groupId);
7418
- if (!have) this.communityHave.set(kept.groupId, have = {
7419
- ids: /* @__PURE__ */ new Set(),
7420
- at: 0
7421
- });
7422
- have.ids.add(kept.message);
7423
- return;
7424
- }
7425
- for (const [key, linkId] of this.deps.edges(kept.groupId)) this.edge(kept.groupId, key, linkId).have.add(kept.message);
7426
- }
7427
- edge(groupId, peer, linkId) {
7428
- const known = this.edgesById.get(linkId);
7429
- if (known) return known;
7430
- const edge = {
7431
- linkId,
7432
- groupId,
7433
- peer,
7434
- files: void 0,
7435
- serving: /* @__PURE__ */ new Map(),
7436
- servedMoved: /* @__PURE__ */ new Map(),
7437
- taking: /* @__PURE__ */ new Map(),
7438
- ended: [],
7439
- have: /* @__PURE__ */ new Set(),
7440
- haveSent: new RateWindow(GROUP_FILE_LIMITS.haveFrames, GROUP_FILE_LIMITS.haveWindowMs, () => this.deps.now()),
7441
- busy: /* @__PURE__ */ new Set(),
7442
- gone: /* @__PURE__ */ new Set()
7443
- };
7444
- edge.files = new ChatFiles({
7445
- send: (frame) => {
7446
- const kept = frame.t === "pf-offer" ? this.kept.get(edge.serving.get(frame.id) ?? "") : void 0;
7447
- return this.deps.sendFiles(linkId, kept ? {
7448
- ...frame,
7449
- gm: kept.message
7450
- } : frame);
7451
- },
7452
- decide: (file) => this.decide(edge, file),
7453
- openTarget: (record) => this.openTarget(edge, record),
7454
- openSource: (record) => this.openSource(edge, record),
7455
- changed: (record, transferred) => this.transferChanged(edge, record, transferred),
7456
- room: () => this.deps.store.room(),
7457
- ...this.deps.sendData && { sendData: (id, offset, data) => this.deps.sendData(linkId, id, offset, data) },
7458
- writable: () => this.deps.writable?.(linkId),
7459
- resend: (record) => !edge.serving.has(record.id) || this.mayResend()
7460
- });
7461
- this.edgesById.set(linkId, edge);
7462
- return edge;
7463
- }
7464
- /** An offer that answers my want (`filesFrame` checked that): taken, unless it is not the file announced or there is no room. */
7465
- async decide(edge, file) {
7466
- const kept = this.kept.get(edge.taking.get(file.id) ?? ""), want = kept && this.wants.get(kept.id);
7467
- if (!kept || kept.held || want?.linkId !== edge.linkId || want.wire !== file.id || file.size !== kept.meta.size) return { refuse: "invalid" };
7468
- const room = await this.deps.store.room().catch(() => null);
7469
- return room !== null && file.size > room ? {
7470
- refuse: "no-room",
7471
- room
7472
- } : "accept";
7473
- }
7474
- async openTarget(edge, record) {
7475
- const kept = this.kept.get(edge.taking.get(record.id) ?? "");
7476
- if (!kept) throw new Error("No such file");
7477
- await kept.idle;
7478
- const writer = await this.deps.store.write(kept.id, !kept.fresh);
7479
- kept.fresh = false;
7480
- const resumed = writer.offset > 0;
7481
- let damaged = false;
7482
- return {
7483
- offset: writer.offset,
7484
- append: (bytes) => writer.append(bytes),
7485
- flush: () => writer.flush(),
7486
- verify: async () => {
7487
- damaged = await writer.digest() !== kept.meta.d;
7488
- const want = this.wants.get(kept.id);
7489
- if (damaged && want) want.why = "damaged";
7490
- return !damaged;
7491
- },
7492
- discard: () => {
7493
- if (damaged) {
7494
- kept.stored = 0;
7495
- if (resumed) kept.fresh = kept.unclear = true;
7496
- else kept.damaged.add(edge.peer);
7497
- }
7498
- return kept.idle = (damaged || kept.dropped === record.id ? writer.discard() : writer.close()).catch(() => {});
7499
- }
7500
- };
7501
- }
7502
- async openSource(edge, record) {
7503
- const kept = this.kept.get(edge.serving.get(record.id) ?? "");
7504
- const stored = kept && await this.deps.store.get(kept.id);
7505
- if (!kept || !stored) throw new Error("The file is gone");
7506
- return {
7507
- read: (offset, length) => this.deps.store.read(stored, offset, length),
7508
- digest: async () => kept.meta.d
7509
- };
7510
- }
7511
- /** A transfer on the edge ended: its record stays among the last `ENDED_KEPT`, and the oldest past that is forgotten. */
7512
- transferOver(edge, record) {
7513
- if (edge.ended.some(([direction, id]) => direction === record.direction && id === record.id)) return;
7514
- edge.ended.push([record.direction, record.id]);
7515
- while (edge.ended.length > ENDED_KEPT) edge.files.forget(...edge.ended.shift());
7516
- }
7517
- transferChanged(edge, record, transferred) {
7518
- if (transferEnded(record)) this.transferOver(edge, record);
7519
- if (record.direction === "out") {
7520
- if (transferEnded(record) && edge.serving.has(record.id)) {
7521
- const today = Math.floor(this.deps.now() / 864e5);
7522
- if (this.served.day !== today) this.served = {
7523
- day: today,
7524
- bytes: 0
7525
- };
7526
- this.served.bytes += edge.files.sent(record.id);
7527
- edge.serving.delete(record.id);
7528
- edge.servedMoved.delete(record.id);
7529
- const serving = this.deps.settings().serveFiles;
7530
- for (const other of this.edgesById.values()) {
7531
- if (serving) for (const id of other.busy) other.have.add(id);
7532
- other.busy.clear();
7533
- }
7534
- }
7535
- return;
7536
- }
7537
- const kept = this.kept.get(edge.taking.get(record.id) ?? ""), want = kept && this.wants.get(kept.id);
7538
- if (!kept || !want || want.wire !== record.id || want.linkId !== edge.linkId) return;
7539
- if (transferred !== want.moved) {
7540
- want.moved = transferred;
7541
- want.movedAt = this.deps.now();
7542
- }
7543
- if (record.state !== "failed" && record.state !== "declined" && record.state !== "cancelled") kept.stored = transferred;
7544
- const { size } = kept.meta;
7545
- switch (record.state) {
7546
- case "done":
7547
- kept.held = true;
7548
- this.wants.delete(kept.id);
7549
- edge.taking.delete(record.id);
7550
- this.deps.store.patch(kept.id, { transfer: {
7551
- state: "done",
7552
- transferred: size,
7553
- size
7554
- } }).catch(() => {});
7555
- this.finished(kept, "in");
7556
- this.queueHave(kept);
7557
- this.retry(kept.groupId);
7558
- return;
7559
- case "failed":
7560
- case "declined":
7561
- case "cancelled":
7562
- edge.taking.delete(record.id);
7563
- if (want.holder && !kept.unclear) want.tried.add(want.holder);
7564
- kept.unclear = false;
7565
- Object.assign(want, {
7566
- wire: void 0,
7567
- holder: void 0,
7568
- linkId: void 0,
7569
- moved: 0
7570
- });
7571
- this.ask(kept);
7572
- this.retry(kept.groupId);
7573
- return;
7574
- case "verifying":
7575
- this.show(kept, {
7576
- state: "transferring",
7577
- stage: "verifying",
7578
- transferred,
7579
- size,
7580
- direction: "in"
7581
- });
7582
- return;
7583
- default: this.show(kept, {
7584
- state: "transferring",
7585
- transferred,
7586
- size,
7587
- direction: "in"
7588
- });
7589
- }
7590
- }
7591
- add(kept) {
7592
- this.kept.set(kept.id, kept);
7593
- this.byMessage.set(`${kept.groupId}\n${kept.message}`, kept.id);
7594
- }
7595
- /** Bytes this device fetched by itself from the group and keeps (or is fetching): what `autoBytesPerGroup` bounds. */
7596
- keptAutomatically(groupId) {
7597
- let sum = 0;
7598
- for (const kept of this.kept.values()) if (kept.groupId === groupId && kept.auto && !kept.mine) sum += kept.meta.size;
7599
- return sum;
7600
- }
7601
- groupField(kept) {
7602
- return {
7603
- message: kept.message,
7604
- author: kept.author,
7605
- ...kept.auto && { auto: true },
7606
- ...kept.asked && { asked: true },
7607
- ...kept.paused && { paused: true }
7608
- };
7609
- }
7610
- /** The bytes of a file kept here from transfers that did not finish. */
7611
- async keptBytes(id) {
7612
- const writer = await this.deps.store.write(id, true).catch(() => void 0);
7613
- if (!writer) return 0;
7614
- await writer.close().catch(() => {});
7615
- return writer.offset;
7616
- }
7617
- pace(windows, key, limit, ms) {
7618
- let window = windows.get(key);
7619
- if (!window) windows.set(key, window = new RateWindow(limit, ms, () => this.deps.now()));
7620
- return window;
7621
- }
7622
- /** My announcements in a group, in a minute and in the window the members count them over. */
7623
- sentPaces(groupId) {
7624
- return [this.pace(this.sentPace, groupId, GROUP_FILE_LIMITS.announcePerMinute, 6e4), this.pace(this.sentTaken, groupId, GROUP_FILE_LIMITS.announceTaken, GROUP_FILE_LIMITS.announceWindowMs)];
7625
- }
7626
- /**
7627
- * The file waits for its person's Download: said in one state (the CLI's `file.offered`), then left to its record,
7628
- * which says it is not here (`waitingGroupFile`). A view kept for each such file would go into every state the engine
7629
- * builds, so each would cost more with every file the groups ever had.
7630
- */
7631
- waiting(kept) {
7632
- this.show(kept, {
7633
- state: "transferring",
7634
- stage: "asking",
7635
- transferred: 0,
7636
- size: kept.meta.size,
7637
- direction: "in"
7638
- });
7639
- this.deps.brief(kept.id);
7640
- }
7641
- /**
7642
- * All here: said in one state (the CLI's `file.done`), then left to its record, which says it is done. A view kept for
7643
- * each file fetched or sent would go into every state the engine builds until the next start.
7644
- */
7645
- finished(kept, direction) {
7646
- this.show(kept, {
7647
- state: "done",
7648
- transferred: kept.meta.size,
7649
- size: kept.meta.size,
7650
- direction
7651
- });
7652
- this.deps.brief(kept.id);
7653
- }
7654
- /** A file's row shows `view`; the engine hears of it only when that is not what the row shows already. */
7655
- show(kept, view) {
7656
- if (sameView(this.deps.transfers.get(kept.id), view)) return;
7657
- this.deps.transfers.set(kept.id, view);
7658
- this.deps.changed();
7659
- }
7660
- };
7661
- /** Two views of a transfer that say the same: their fields are all plain values. */
7662
- function sameView(a, b) {
7663
- if (!a) return false;
7664
- const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
7665
- for (const key of keys) if (a[key] !== b[key]) return false;
7666
- return true;
7667
- }
7668
- /** A file's holders with `key` the newest of them, the `holders` newest kept. */
7669
- var newestHolder = (key, holders) => [key, ...holders.filter((other) => other !== key)].slice(0, GROUP_FILE_LIMITS.holders);
7670
- function messageFile(kept) {
7671
- const { name, size, mime, voice, video, image } = kept.meta;
7672
- return {
7673
- id: kept.id,
7674
- name,
7675
- size,
7676
- mime,
7677
- ...voice && { voice },
7678
- ...video && { video },
7679
- ...image && { image }
7680
- };
3787
+ function identityProvider(id, providers = identityProviders()) {
3788
+ return providers.find((p) => p.id === id);
7681
3789
  }
7682
3790
  //#endregion
7683
- export { SourceConfigError as $, identityProviders as A, verifyIdentity as B, FileAppender as C, streamStored as D, storedSize as E, onAdaptersChanged as F, normalizePhrase as G, LightningCards as H, registeredLightningProviders as I, sourceKey as J, lightningPaying as K, registeredOnchainProviders as L, resolveAtprotoDid as M, setOwnDidSource as N, waitingGroupFile as O, chosenResolver as P, PROVIDER_PLATFORMS as Q, reserveAdapterIds as R, WALLET_TYPES as S, removeStored as T, cardsKey as U, CASHU_CARD as V, isRecoveryPhrase as W, cashuMint as X, CASHU_MINT_SOURCE as Y, NothingSpentError as Z, crossNetwork as _, setAwayWallets as _t, storedGroupFiles as a, breezDatabaseInUse as at, walletInstances as b, receivedPairedReply as c, iceServerProblem as ct, paymentAlias as d, ModeGate as dt, SourceUnreachableError as et, paymentWireId as f, WrongNetworkError as ft, createTiming as g, requireTurn as gt, createFailure as h, refuseAway as ht, groupOfFile as i, breezDatabase as it, assertPublicServer as j, IDENTITY_PROVIDERS as k, replyRef as l, iceServerSetting as lt, assertConfirmedReal as m, awayFrom as mt, groupFileId as n, networkMode as nt, groupReply as o, breezDatabasesFor as ot, WALLET_NAMES as p, networkLabel as pt, ProviderSources as q, groupFileText as r, redact$1 as rt, pairedWireReply as s, dropBreezDatabase as st, GroupFileDesk as t, isNothingSpentError as tt, replyTo as u, ModeChanged as ut, paymentNetwork as v, setSingleWriterGate as vt, readStored as w, MESSAGE_DETAILS_MAX_BYTES as x, paymentNetworksOf as y, boundedIdentityFetch as z };
3791
+ export { identityProvider as n, identityProviders as r, IDENTITY_PROVIDERS as t };
7684
3792
 
7685
- //# sourceMappingURL=groupFiles-BNTSDaoq.js.map
3793
+ //# sourceMappingURL=registry-CVdSZW5l.js.map