@ghostlytools/cli 1.2.3 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +18 -7
  2. package/dist/assets/api-BiaX6l6f.js +3 -0
  3. package/dist/assets/api-CrxVxtY1.js +4071 -0
  4. package/dist/assets/api-CrxVxtY1.js.map +1 -0
  5. package/dist/assets/apiKit-CajiDxdK.js +196 -0
  6. package/dist/assets/apiKit-CajiDxdK.js.map +1 -0
  7. package/dist/assets/apps-CbfiCSIw.js +787 -0
  8. package/dist/assets/apps-CbfiCSIw.js.map +1 -0
  9. package/dist/assets/apps-t23gkc28.js +2 -0
  10. package/dist/assets/{arkade-B-ALutmU.js → arkade-D4BVJFEQ.js} +2 -2
  11. package/dist/assets/{arkade-B-ALutmU.js.map → arkade-D4BVJFEQ.js.map} +1 -1
  12. package/dist/assets/backup-BAMi2lAq.js +731 -0
  13. package/dist/assets/backup-BAMi2lAq.js.map +1 -0
  14. package/dist/assets/backup-Cg52WeQi.js +2 -0
  15. package/dist/assets/bark_ffi_wasm_bg-EY2uc0Bx.js.map +1 -1
  16. package/dist/assets/bitcoindevkit_bg-B-iLWrmX.js.map +1 -1
  17. package/dist/assets/buttons-CWl2yNJu.js +2 -0
  18. package/dist/assets/buttons-CX1pKB0j.js +497 -0
  19. package/dist/assets/buttons-CX1pKB0j.js.map +1 -0
  20. package/dist/assets/{endpoint-pgFkR2AS.js → endpoint-Dn7H8qE1.js} +14 -8
  21. package/dist/assets/endpoint-Dn7H8qE1.js.map +1 -0
  22. package/dist/assets/envelope-BvU0fLEt.js +90 -0
  23. package/dist/assets/envelope-BvU0fLEt.js.map +1 -0
  24. package/dist/assets/errors-my6VJR_0.js +77 -0
  25. package/dist/assets/errors-my6VJR_0.js.map +1 -0
  26. package/dist/assets/{fileBytes-Dbifu5I5.js → fileBytes-CfSXjTFa.js} +2 -2
  27. package/dist/assets/{fileBytes-Dbifu5I5.js.map → fileBytes-CfSXjTFa.js.map} +1 -1
  28. package/dist/assets/fileBytes-CgbibpbV.js +2 -0
  29. package/dist/assets/{fileBytes-BcYZhNgF.js → fileBytes-Di7VZtmU.js} +6 -19
  30. package/dist/assets/fileBytes-Di7VZtmU.js.map +1 -0
  31. package/dist/assets/fileBytesIdb-CFP-DSkZ.js +2 -0
  32. package/dist/assets/{fileBytesIdb-DzSaqI0A.js → fileBytesIdb-D5u6H8X_.js} +4 -4
  33. package/dist/assets/{fileBytesIdb-DzSaqI0A.js.map → fileBytesIdb-D5u6H8X_.js.map} +1 -1
  34. package/dist/assets/{fileBytesOpfs-Dx8RZiV5.js → fileBytesOpfs-Dd1N4ERH.js} +3 -3
  35. package/dist/assets/{fileBytesOpfs-Dx8RZiV5.js.map → fileBytesOpfs-Dd1N4ERH.js.map} +1 -1
  36. package/dist/assets/groupFiles-BNTSDaoq.js +7685 -0
  37. package/dist/assets/groupFiles-BNTSDaoq.js.map +1 -0
  38. package/dist/assets/groupFiles-DBbjOLuD.js +2088 -0
  39. package/dist/assets/groupFiles-DBbjOLuD.js.map +1 -0
  40. package/dist/assets/host-Cmd205Dc.js +4648 -0
  41. package/dist/assets/host-Cmd205Dc.js.map +1 -0
  42. package/dist/assets/hyperdhtRelay-BT_wRxab.js +21 -0
  43. package/dist/assets/hyperdhtRelay-BT_wRxab.js.map +1 -0
  44. package/dist/assets/{hyperdhtRelay-hKC9DpIH.js → hyperdhtRelay-CEmBpT0Q.js} +26 -10
  45. package/dist/assets/hyperdhtRelay-CEmBpT0Q.js.map +1 -0
  46. package/dist/assets/{idb-CTF6LF5B.js → idb-B5L8wuOm.js} +7 -5
  47. package/dist/assets/idb-B5L8wuOm.js.map +1 -0
  48. package/dist/assets/irohWeb--YyEA-hX.js +545 -0
  49. package/dist/assets/irohWeb--YyEA-hX.js.map +1 -0
  50. package/dist/assets/{joinTrace-CO8mlhNO.js → joinTrace-CjRDFy85.js} +2 -7
  51. package/dist/assets/{joinTrace-CO8mlhNO.js.map → joinTrace-CjRDFy85.js.map} +1 -1
  52. package/dist/assets/joinTrace-D-fnt2p3.js +2 -0
  53. package/dist/assets/main-BzNN6bkx.js +4302 -0
  54. package/dist/assets/main-BzNN6bkx.js.map +1 -0
  55. package/dist/assets/{mainline-DriRu_hb.js → mainline-D5D5C-jT.js} +14 -6
  56. package/dist/assets/mainline-D5D5C-jT.js.map +1 -0
  57. package/dist/assets/mints-BrlY773j.js +74 -0
  58. package/dist/assets/mints-BrlY773j.js.map +1 -0
  59. package/dist/assets/mints-CqTdA4Gk.js +2 -0
  60. package/dist/assets/networkReason-DxZWHw6v.js +154 -0
  61. package/dist/assets/networkReason-DxZWHw6v.js.map +1 -0
  62. package/dist/assets/privateFolder-Bk9C9oFv.js +38 -0
  63. package/dist/assets/privateFolder-Bk9C9oFv.js.map +1 -0
  64. package/dist/assets/profiles-B6F4oa9K.js +139 -0
  65. package/dist/assets/profiles-B6F4oa9K.js.map +1 -0
  66. package/dist/assets/{restoredRows-C_84q0Pd.js → restoredRows-Db3oUTko.js} +2 -2
  67. package/dist/assets/{restoredRows-C_84q0Pd.js.map → restoredRows-Db3oUTko.js.map} +1 -1
  68. package/dist/assets/{server-DHMXMwNn.js → server-CBCEYKMN.js} +1290 -277
  69. package/dist/assets/server-CBCEYKMN.js.map +1 -0
  70. package/dist/assets/{src-_FzJkeAJ.js → src-DhfGgAUu.js} +1068 -2266
  71. package/dist/assets/src-DhfGgAUu.js.map +1 -0
  72. package/dist/assets/turnRefs-ChEfgbSX.js +323 -0
  73. package/dist/assets/turnRefs-ChEfgbSX.js.map +1 -0
  74. package/dist/assets/{usdt-D0lO2pRR.js → usdt-C1Xao2xH.js} +3 -3
  75. package/dist/assets/{usdt-D0lO2pRR.js.map → usdt-C1Xao2xH.js.map} +1 -1
  76. package/dist/ghostly.mjs +4 -21992
  77. package/dist/ghostly.mjs.map +1 -1
  78. package/package.json +1 -1
  79. package/dist/assets/endpoint-pgFkR2AS.js.map +0 -1
  80. package/dist/assets/fileBytes-BcYZhNgF.js.map +0 -1
  81. package/dist/assets/hyperdhtRelay-hKC9DpIH.js.map +0 -1
  82. package/dist/assets/idb-CTF6LF5B.js.map +0 -1
  83. package/dist/assets/mainline-DriRu_hb.js.map +0 -1
  84. package/dist/assets/rolldown-runtime-8H4AJuhK.js +0 -14
  85. package/dist/assets/server-DHMXMwNn.js.map +0 -1
  86. package/dist/assets/src-_FzJkeAJ.js.map +0 -1
@@ -1,4 +1,4 @@
1
- import { Rc as RelayTransport, Wc as isDiscoveryBudgetError, Yc as newerPacket, Zc as parseRelayPayload, tl as publicKeyFromZ32 } from "./src-_FzJkeAJ.js";
1
+ import { $s as parseRelayPayload, Bs as RelayTransport, Ks as isDiscoveryBudgetError, Zs as newerPacket, rc as publicKeyFromZ32 } from "./src-DhfGgAUu.js";
2
2
  import { ed25519 } from "@noble/curves/ed25519.js";
3
3
  import { createHash } from "node:crypto";
4
4
  import DHT from "bittorrent-dht";
@@ -115,17 +115,22 @@ var RelaysAndDht = class {
115
115
  this.relays = new RelayTransport(relays);
116
116
  }
117
117
  async publish(identity, records, options = {}) {
118
- await this.relays.publishDated(identity, records, (key, payload, _timestamp, conflict) => this.putBoth(key, payload, options, conflict), options);
118
+ await this.relays.publishDated(identity, records, (key, payload, timestamp, conflict) => this.putBoth(key, payload, options, conflict, timestamp), options, false, this.dht !== null);
119
119
  }
120
120
  publishPayload(pubKeyZ32, payload, options = {}) {
121
121
  return this.putBoth(pubKeyZ32, payload, options);
122
122
  }
123
- async putBoth(pubKeyZ32, payload, options, conflict) {
124
- if (!this.dht) return this.relays.publishPayload(pubKeyZ32, payload, options, conflict);
123
+ /**
124
+ * `timestamp`: the payload was just signed here (`publish`), dated so; its signature is not checked again on the way
125
+ * to the relays. Without it (`publishPayload`, signed elsewhere) the relays' side checks it to read its date.
126
+ */
127
+ async putBoth(pubKeyZ32, payload, options, conflict, timestamp) {
128
+ const toRelays = () => timestamp === void 0 ? this.relays.publishPayload(pubKeyZ32, payload, options, conflict) : this.relays.putSigned(pubKeyZ32, payload, timestamp, options, conflict);
129
+ if (!this.dht) return toRelays();
125
130
  const onDht = this.dht.put(pubKeyZ32, payload);
126
131
  onDht.catch(() => {});
127
132
  try {
128
- await this.relays.publishPayload(pubKeyZ32, payload, options, conflict);
133
+ await toRelays();
129
134
  } catch (error) {
130
135
  if (isDiscoveryBudgetError(error)) throw error;
131
136
  await onDht.catch(() => {
@@ -177,6 +182,9 @@ var RelaysAndDht = class {
177
182
  readAnsweredAt(pubKeyZ32) {
178
183
  return this.relays.readAnsweredAt(pubKeyZ32);
179
184
  }
185
+ readHeldBackFor(pubKeyZ32) {
186
+ return this.relays.readHeldBackFor(pubKeyZ32);
187
+ }
180
188
  onServerTime(listener) {
181
189
  return this.relays.onServerTime(listener);
182
190
  }
@@ -187,4 +195,4 @@ var RelaysAndDht = class {
187
195
  //#endregion
188
196
  export { Mainline, RelaysAndDht };
189
197
 
190
- //# sourceMappingURL=mainline-DriRu_hb.js.map
198
+ //# sourceMappingURL=mainline-D5D5C-jT.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mainline-D5D5C-jT.js","names":[],"sources":["../../src/runtime/mainline.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport DHT from \"bittorrent-dht\";\nimport { ed25519 } from \"@noble/curves/ed25519.js\";\nimport {\n RelayTransport, isDiscoveryBudgetError, newerPacket, parseRelayPayload, publicKeyFromZ32,\n type DiscoveryStatus, type GhostRecord, type Identity, type PkarrRequestOptions, type PkarrTransport, type RelayTransportOptions, type SignedPacket,\n} from \"@ghostly/core\";\n\n/**\n * Pkarr over the Mainline DHT (BEP44), from Node, as the Desktop reaches it from Rust: the relays are HTTP bridges to\n * this same DHT, so a headless Ghostly that speaks it directly keeps finding its contacts while the relays fail (they\n * answered HTTP 500 for more than ten minutes on 2026-09-27, and a group's edges stayed down all that while).\n *\n * A Pkarr packet is a BEP44 mutable item: the key is the Ed25519 public key, the value the DNS packet, the sequence\n * number its timestamp in microseconds, and the signature the one the relay payload already carries (over\n * `3:seqi<seq>e1:v<len>:<v>`). So one signed payload goes to the relays and to the DHT unchanged.\n */\n\nexport interface MainlineOptions {\n /** `host:port` nodes to start from; the public routers when absent; none with `false` (a testnet's first node). */\n bootstrap?: string[] | false;\n /** Address to bind (a loopback testnet binds 127.0.0.1). */\n host?: string;\n /** How long a lookup may take before it is given up, in ms. */\n timeoutMs?: number;\n}\n\nconst SIGNATURE = 64, TIMESTAMP = 8;\nconst verify = (sig: Uint8Array, message: Uint8Array, key: Uint8Array) => { try { return ed25519.verify(sig, message, key); } catch { return false; } };\n\n/** A DHT node of this process: puts and gets Pkarr packets. */\nexport class Mainline {\n private readonly dht: DHT;\n /** Its UDP socket is bound. */\n readonly listening: Promise<void>;\n private readonly timeoutMs: number;\n\n constructor(options: MainlineOptions = {}) {\n this.timeoutMs = options.timeoutMs ?? 20_000;\n const bootstrap = options.bootstrap === false ? false : options.bootstrap?.length ? options.bootstrap : undefined;\n this.dht = new DHT({ verify, ...(bootstrap !== undefined ? { bootstrap } : {}), ...(options.host ? { host: options.host } : {}) });\n this.listening = new Promise((resolve) => this.dht.listen(0, options.host, () => resolve()));\n // A node of the table that errors must not end the process.\n this.dht.on(\"error\", () => {});\n }\n\n /** Adds a node (a testnet's peers). */\n addNode(host: string, port: number): void { this.dht.addNode({ host, port }); }\n address(): { port: number } { return this.dht.address() as { port: number }; }\n\n /** Puts a relay payload (`<signature><timestamp><DNS packet>`) as the mutable item of `pubKeyZ32`; resolves with the nodes that took it. */\n put(pubKeyZ32: string, payload: Uint8Array): Promise<number> {\n const k = Buffer.from(publicKeyFromZ32(pubKeyZ32));\n const sig = Buffer.from(payload.subarray(0, SIGNATURE));\n const seq = Number(new DataView(payload.buffer, payload.byteOffset + SIGNATURE, TIMESTAMP).getBigUint64(0));\n const v = Buffer.from(payload.subarray(SIGNATURE + TIMESTAMP));\n return this.bounded(new Promise((resolve, reject) => {\n this.dht.put({ k, v, seq, sig }, (error, _hash, nodes) => {\n if (error) reject(error); else if (!nodes) reject(new Error(\"No DHT node took the packet\")); else resolve(nodes);\n });\n }));\n }\n\n /** The newest packet of `pubKeyZ32` the DHT holds, checked; null when nobody has one. */\n get(pubKeyZ32: string): Promise<SignedPacket | null> {\n const k = Buffer.from(publicKeyFromZ32(pubKeyZ32));\n // A mutable item without salt is found under the SHA-1 of its public key (BEP44).\n const target = createHash(\"sha1\").update(k).digest();\n return this.bounded(new Promise((resolve, reject) => {\n this.dht.get(target, { cache: false }, (error, found) => {\n if (error) return reject(error);\n if (!found?.v || !found.sig) return resolve(null);\n const timestamp = new Uint8Array(TIMESTAMP);\n new DataView(timestamp.buffer).setBigUint64(0, BigInt(found.seq));\n const payload = new Uint8Array(SIGNATURE + TIMESTAMP + found.v.length);\n payload.set(found.sig, 0); payload.set(timestamp, SIGNATURE); payload.set(found.v, SIGNATURE + TIMESTAMP);\n try { resolve(parseRelayPayload(pubKeyZ32, payload)); } catch { resolve(null); }\n });\n }));\n }\n\n private bounded<T>(work: Promise<T>): Promise<T> {\n return new Promise((resolve, reject) => {\n const timer = setTimeout(() => reject(new Error(\"DHT lookup timed out\")), this.timeoutMs);\n work.then((value) => { clearTimeout(timer); resolve(value); }, (error) => { clearTimeout(timer); reject(error); });\n });\n }\n\n destroy(): Promise<void> { return new Promise((resolve) => this.dht.destroy(() => resolve())); }\n}\n\n/**\n * The relays first, as the web app reaches Pkarr (its contacts read only the relays), and the DHT beside them: every\n * packet also goes to the DHT, and a read goes there when every relay is failing or the read failed for anything but\n * this app's own request budget (that is a wait, not an outage).\n *\n * `pinned`: the relays given are the only ones (`GHOSTLY_PKARR_RELAYS`, a private network): the profile's `relays`\n * setting does not replace them. With no DHT (`dht` null), the relays alone carry every packet.\n */\nexport class RelaysAndDht implements PkarrTransport {\n readonly relays: RelayTransport;\n private lastVia: \"relay\" | \"dht\" = \"relay\";\n\n constructor(private readonly dht: Mainline | null, relays: RelayTransportOptions = {}, private readonly pinned = false) {\n this.relays = new RelayTransport(relays);\n }\n\n async publish(identity: Identity, records: GhostRecord[], options: PkarrRequestOptions = {}): Promise<void> {\n // BEP44 sequence numbers must strictly increase, on the relays and on the DHT alike: one payload for both, dated\n // past every packet put or read under the key (`RelayTransport.publishDated`), whichever clock dated those.\n // With the DHT, the packet goes there whatever the relays' budgets say: signed every time. Without it (a private\n // network's relays only), a publish none of them takes now is not signed at all.\n await this.relays.publishDated(identity, records, (key, payload, timestamp, conflict) => this.putBoth(key, payload, options, conflict, timestamp), options, false, this.dht !== null);\n }\n\n publishPayload(pubKeyZ32: string, payload: Uint8Array, options: PkarrRequestOptions = {}): Promise<void> {\n return this.putBoth(pubKeyZ32, payload, options);\n }\n\n /**\n * `timestamp`: the payload was just signed here (`publish`), dated so; its signature is not checked again on the way\n * to the relays. Without it (`publishPayload`, signed elsewhere) the relays' side checks it to read its date.\n */\n private async putBoth(pubKeyZ32: string, payload: Uint8Array, options: PkarrRequestOptions, conflict?: (relay: string) => void, timestamp?: bigint): Promise<void> {\n const toRelays = () => timestamp === undefined\n ? this.relays.publishPayload(pubKeyZ32, payload, options, conflict)\n : this.relays.putSigned(pubKeyZ32, payload, timestamp, options, conflict);\n if (!this.dht) return toRelays();\n const onDht = this.dht.put(pubKeyZ32, payload);\n onDht.catch(() => {});\n try {\n await toRelays();\n } catch (error) {\n // Held back by this app's budget: the relays take it when it frees (browser contacts read only them).\n if (isDiscoveryBudgetError(error)) throw error;\n // The relays failed: out on the DHT is out (a Pkarr relay reads the DHT for a key it has not got).\n await onDht.catch(() => { throw error; });\n }\n }\n\n async resolve(pubKeyZ32: string, options: PkarrRequestOptions = {}): Promise<SignedPacket | null> {\n if (!this.dht) return this.relays.resolve(pubKeyZ32, options);\n let fromRelays: SignedPacket | null = null, failed: unknown = null;\n // Every relay left alone for failing: the relays answer from what they last saw, if anything; the DHT is asked.\n const down = () => this.relays.discovery().relays.every((relay) => relay.state === \"failing\");\n if (!down()) {\n try { fromRelays = await this.relays.resolve(pubKeyZ32, options); this.lastVia = \"relay\"; } catch (error) {\n if (isDiscoveryBudgetError(error)) throw error;\n failed = error;\n }\n if (!failed && !down()) return fromRelays;\n }\n try {\n const fromDht = await this.dht.get(pubKeyZ32);\n this.lastVia = \"dht\";\n if (fromDht) this.relays.sawTimestamp(pubKeyZ32, fromDht.timestampMicros);\n return newerPacket(fromRelays, fromDht);\n } catch (error) {\n if (fromRelays) return fromRelays;\n throw failed ?? error;\n }\n }\n\n describe(): { protocol: string; relays: string[] } {\n return { protocol: this.dht ? \"Pkarr relays (HTTP), and the Mainline DHT (BEP44) directly\" : \"Pkarr relays (HTTP)\", relays: this.relays.describe().relays };\n }\n discovery(): DiscoveryStatus {\n const status = this.relays.discovery();\n return this.lastVia === \"dht\" ? { ...status, path: { via: \"dht\" } } : status;\n }\n subscribe(listener: Parameters<RelayTransport[\"subscribe\"]>[0]): () => void { return this.relays.subscribe(listener); }\n /** A relay's answer only: a packet the DHT hands back when the relays are down may be older than one already read. */\n readAnsweredAt(pubKeyZ32: string): number | undefined { return this.relays.readAnsweredAt(pubKeyZ32); }\n readHeldBackFor(pubKeyZ32: string): number | undefined { return this.relays.readHeldBackFor(pubKeyZ32); }\n onServerTime(listener: Parameters<RelayTransport[\"onServerTime\"]>[0]): () => void { return this.relays.onServerTime(listener); }\n configure({ relays }: { relays: string[]; readRelays: boolean }): void { if (!this.pinned) this.relays.setRelays(relays); }\n}\n"],"mappings":"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"}
@@ -0,0 +1,74 @@
1
+ //#region ../browser/src/shared/mints.ts
2
+ /**
3
+ * Mints a new wallet starts with, so nobody has to know what a mint is before
4
+ * receiving sats. The first one that answers is where Lightning invoices are
5
+ * created; ecash from any of them is accepted from contacts.
6
+ *
7
+ * A mint is a custodian: it holds the sats. This list is a product decision,
8
+ * to be reviewed regularly. It was picked on 2026-09-18 from the community
9
+ * auditor (https://audit.8333.space): mints with thousands of audited swaps,
10
+ * the highest success rates and an OK state at the time.
11
+ */
12
+ var DEFAULT_MINTS = [
13
+ "https://mint.minibits.cash/Bitcoin",
14
+ "https://21mint.me",
15
+ "https://mint.mountainlake.io"
16
+ ];
17
+ /** Public test mint: worthless sats, invoices settle by themselves. */
18
+ var TEST_MINT = "https://testnut.cashu.space";
19
+ /**
20
+ * Mints whose sats are worth nothing: never counted as money, never mixed with real sats.
21
+ * Mutinynet's mint (cashu.mutinynet.com) would belong here, but on 2026-09-22 it answered with a
22
+ * duplicated Access-Control-Allow-Origin header that every browser and WebView rejects.
23
+ */
24
+ var TEST_MINTS = [TEST_MINT];
25
+ var isTestMint = (url) => TEST_MINTS.includes(url.replace(/\/+$/, ""));
26
+ /** A mint on this very machine (a local test server): whatever it issues is not money anyone else holds. */
27
+ function isLocalMint(url) {
28
+ try {
29
+ return [
30
+ "localhost",
31
+ "127.0.0.1",
32
+ "[::1]"
33
+ ].includes(new URL(url).hostname);
34
+ } catch {
35
+ return false;
36
+ }
37
+ }
38
+ /**
39
+ * Mints whose sats are worth nothing: the public test mints and mints on this machine. They belong to
40
+ * the Testnet mode, and their ecash never settles a request for real sats. Only the public test mints
41
+ * are ever added by themselves (a contact must not make this app contact a port on this machine).
42
+ */
43
+ var isWorthlessMint = (url) => isTestMint(url) || isLocalMint(url);
44
+ /**
45
+ * A mint whose word "this invoice is paid" proves nobody paid it. The public test mint runs a fake Lightning backend
46
+ * that marks every invoice paid by itself, and a mint on this machine is a test server that may do the same. Its
47
+ * invoices are still shown to be paid, but the wallet mints one only once a payer says it paid it (see `held` on
48
+ * StoredQuote); asking it for test coins on purpose is `CashuWallet.testCoins`.
49
+ */
50
+ var paysItsOwnInvoices = isWorthlessMint;
51
+ /**
52
+ * Mints on this machine the operator declared test servers with a fake Lightning backend (the CLI's
53
+ * `GHOSTLY_TEST_MINTS`, for regtest and e2e stacks). Nothing else declares one: the apps never do.
54
+ */
55
+ var declaredTestMints = /* @__PURE__ */ new Set();
56
+ var bare = (url) => url.trim().replace(/\/+$/, "");
57
+ function declareTestMints(urls) {
58
+ for (const url of urls) if (isLocalMint(bare(url))) declaredTestMints.add(bare(url));
59
+ }
60
+ /**
61
+ * Whether a Testnet wallet may pay a Bitcoin (`lnbc`) invoice through this mint. Test mints issue `lnbc` invoices,
62
+ * but so does a real node: only a mint known to fake its Lightning moves nothing real when it pays one. That is the
63
+ * public test mint, or a mint on this machine the operator declared a test server; never any other mint on this
64
+ * machine, which may be a self-hosted mint with real sats behind it.
65
+ */
66
+ var fakesLightning = (url) => isTestMint(url) || declaredTestMints.has(bare(url));
67
+ /** Refused before any mint is asked: see `fakesLightning`. */
68
+ var BITCOIN_INVOICE_ON_TESTNET = "This is a Bitcoin invoice (real money): test sats pay one only through the public test mint. Nothing was sent.";
69
+ /** The network a Cashu mint's ecash belongs to: test mints and mints on this machine are Testnet. */
70
+ var mintNetwork = (url) => isWorthlessMint(url) ? "testnet" : "mainnet";
71
+ //#endregion
72
+ export { declareTestMints as a, isTestMint as c, paysItsOwnInvoices as d, TEST_MINTS as i, isWorthlessMint as l, DEFAULT_MINTS as n, fakesLightning as o, TEST_MINT as r, isLocalMint as s, BITCOIN_INVOICE_ON_TESTNET as t, mintNetwork as u };
73
+
74
+ //# sourceMappingURL=mints-BrlY773j.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mints-BrlY773j.js","names":[],"sources":["../../../browser/src/shared/mints.ts"],"sourcesContent":["import type { WalletNetwork } from \"@ghostly/core\";\n/**\n * Mints a new wallet starts with, so nobody has to know what a mint is before\n * receiving sats. The first one that answers is where Lightning invoices are\n * created; ecash from any of them is accepted from contacts.\n *\n * A mint is a custodian: it holds the sats. This list is a product decision,\n * to be reviewed regularly. It was picked on 2026-09-18 from the community\n * auditor (https://audit.8333.space): mints with thousands of audited swaps,\n * the highest success rates and an OK state at the time.\n */\nexport const DEFAULT_MINTS = [\n \"https://mint.minibits.cash/Bitcoin\",\n \"https://21mint.me\",\n \"https://mint.mountainlake.io\",\n];\n\n/** Public test mint: worthless sats, invoices settle by themselves. */\nexport const TEST_MINT = \"https://testnut.cashu.space\";\n\n/**\n * Mints whose sats are worth nothing: never counted as money, never mixed with real sats.\n * Mutinynet's mint (cashu.mutinynet.com) would belong here, but on 2026-09-22 it answered with a\n * duplicated Access-Control-Allow-Origin header that every browser and WebView rejects.\n */\nexport const TEST_MINTS: readonly string[] = [TEST_MINT];\nexport const isTestMint = (url: string) => TEST_MINTS.includes(url.replace(/\\/+$/, \"\"));\n\n/** A mint on this very machine (a local test server): whatever it issues is not money anyone else holds. */\nexport function isLocalMint(url: string): boolean {\n try { return [\"localhost\", \"127.0.0.1\", \"[::1]\"].includes(new URL(url).hostname); } catch { return false; }\n}\n\n/**\n * Mints whose sats are worth nothing: the public test mints and mints on this machine. They belong to\n * the Testnet mode, and their ecash never settles a request for real sats. Only the public test mints\n * are ever added by themselves (a contact must not make this app contact a port on this machine).\n */\nexport const isWorthlessMint = (url: string) => isTestMint(url) || isLocalMint(url);\n\n/**\n * A mint whose word \"this invoice is paid\" proves nobody paid it. The public test mint runs a fake Lightning backend\n * that marks every invoice paid by itself, and a mint on this machine is a test server that may do the same. Its\n * invoices are still shown to be paid, but the wallet mints one only once a payer says it paid it (see `held` on\n * StoredQuote); asking it for test coins on purpose is `CashuWallet.testCoins`.\n */\nexport const paysItsOwnInvoices = isWorthlessMint;\n\n/**\n * Mints on this machine the operator declared test servers with a fake Lightning backend (the CLI's\n * `GHOSTLY_TEST_MINTS`, for regtest and e2e stacks). Nothing else declares one: the apps never do.\n */\nconst declaredTestMints = new Set<string>();\nconst bare = (url: string) => url.trim().replace(/\\/+$/, \"\");\nexport function declareTestMints(urls: readonly string[]): void {\n for (const url of urls) if (isLocalMint(bare(url))) declaredTestMints.add(bare(url));\n}\n\n/**\n * Whether a Testnet wallet may pay a Bitcoin (`lnbc`) invoice through this mint. Test mints issue `lnbc` invoices,\n * but so does a real node: only a mint known to fake its Lightning moves nothing real when it pays one. That is the\n * public test mint, or a mint on this machine the operator declared a test server; never any other mint on this\n * machine, which may be a self-hosted mint with real sats behind it.\n */\nexport const fakesLightning = (url: string) => isTestMint(url) || declaredTestMints.has(bare(url));\n\n/** Refused before any mint is asked: see `fakesLightning`. */\nexport const BITCOIN_INVOICE_ON_TESTNET = \"This is a Bitcoin invoice (real money): test sats pay one only through the public test mint. Nothing was sent.\";\n\n/** Real money, or test networks: each wallet has its own (see WalletNetwork in @ghostly/core). */\nexport type WalletMode = WalletNetwork;\n/** The network a Cashu mint's ecash belongs to: test mints and mints on this machine are Testnet. */\nexport const mintNetwork = (url: string): WalletNetwork => isWorthlessMint(url) ? \"testnet\" : \"mainnet\";\n"],"mappings":";;;;;;;;;;;AAWA,IAAa,gBAAgB;CAC3B;CACA;CACA;AACF;;AAGA,IAAa,YAAY;;;;;;AAOzB,IAAa,aAAgC,CAAC,SAAS;AACvD,IAAa,cAAc,QAAgB,WAAW,SAAS,IAAI,QAAQ,QAAQ,EAAE,CAAC;;AAGtF,SAAgB,YAAY,KAAsB;CAChD,IAAI;EAAE,OAAO;GAAC;GAAa;GAAa;EAAO,CAAC,CAAC,SAAS,IAAI,IAAI,GAAG,CAAC,CAAC,QAAQ;CAAG,QAAQ;EAAE,OAAO;CAAO;AAC5G;;;;;;AAOA,IAAa,mBAAmB,QAAgB,WAAW,GAAG,KAAK,YAAY,GAAG;;;;;;;AAQlF,IAAa,qBAAqB;;;;;AAMlC,IAAM,oCAAoB,IAAI,IAAY;AAC1C,IAAM,QAAQ,QAAgB,IAAI,KAAK,CAAC,CAAC,QAAQ,QAAQ,EAAE;AAC3D,SAAgB,iBAAiB,MAA+B;CAC9D,KAAK,MAAM,OAAO,MAAM,IAAI,YAAY,KAAK,GAAG,CAAC,GAAG,kBAAkB,IAAI,KAAK,GAAG,CAAC;AACrF;;;;;;;AAQA,IAAa,kBAAkB,QAAgB,WAAW,GAAG,KAAK,kBAAkB,IAAI,KAAK,GAAG,CAAC;;AAGjG,IAAa,6BAA6B;;AAK1C,IAAa,eAAe,QAA+B,gBAAgB,GAAG,IAAI,YAAY"}
@@ -0,0 +1,2 @@
1
+ import { a as declareTestMints } from "./mints-BrlY773j.js";
2
+ export { declareTestMints };
@@ -0,0 +1,154 @@
1
+ import { Jn as engineText, qn as engineError } from "./src-DhfGgAUu.js";
2
+ import { f as store, i as STORES, m as wrap, p as transact, u as openDb } from "./idb-B5L8wuOm.js";
3
+ //#region ../browser/src/engine/paymentAdapters/persistence.ts
4
+ /**
5
+ * A USDT payment is one transaction at one account nonce. Another payment from the same account and chain that is
6
+ * submitted or of unknown outcome holds its nonce until it confirms. A review made after it was sent took the next
7
+ * nonce and may go at once. One made before (or read from an RPC that had not seen it yet) has the same or a lower
8
+ * nonce: sent, it would replace the earlier transaction or be dropped, so it is refused. A row with no nonce refuses.
9
+ */
10
+ function nonceTaken(review, intents) {
11
+ if (review.method !== "usdt") return false;
12
+ const from = review.evm?.from?.toLowerCase(), nonce = review.evm?.nonce;
13
+ return intents.some(({ review: other }) => other.id !== review.id && other.method === "usdt" && other.chainId === review.chainId && other.evm?.from?.toLowerCase() === from && ["submitted", "unknown"].includes(other.state) && !(typeof nonce === "number" && typeof other.evm?.nonce === "number" && nonce > other.evm.nonce));
14
+ }
15
+ var intentRepository = {
16
+ async get(id) {
17
+ return wrap((await store(STORES.intents, "readonly")).get(id));
18
+ },
19
+ async list() {
20
+ return wrap((await store(STORES.intents, "readonly")).getAll());
21
+ },
22
+ async put(intent) {
23
+ await transact([STORES.intents], (s) => {
24
+ s[STORES.intents].put(intent);
25
+ });
26
+ },
27
+ async claim(id) {
28
+ const tx = (await openDb()).transaction(STORES.intents, "readwrite");
29
+ return new Promise((resolve, reject) => {
30
+ let saved;
31
+ let reason = engineText("alreadySubmitted");
32
+ const request = tx.objectStore(STORES.intents).getAll();
33
+ request.onsuccess = () => {
34
+ const intents = request.result;
35
+ saved = intents.find((item) => item.review.id === id);
36
+ if (!saved || saved.review.state !== "pending") {
37
+ tx.abort();
38
+ return;
39
+ }
40
+ if (saved.review.requestId && intents.some(({ review }) => review.id !== id && review.requestId === saved.review.requestId && review.linkId === saved.review.linkId && [
41
+ "submitted",
42
+ "settled",
43
+ "unknown"
44
+ ].includes(review.state))) {
45
+ tx.abort();
46
+ return;
47
+ }
48
+ if (nonceTaken(saved.review, intents)) {
49
+ reason = engineText("sentAfterReview");
50
+ tx.abort();
51
+ return;
52
+ }
53
+ saved.review = {
54
+ ...saved.review,
55
+ state: "submitted",
56
+ error: void 0,
57
+ submittedAt: Date.now()
58
+ };
59
+ tx.objectStore(STORES.intents).put(saved);
60
+ };
61
+ tx.oncomplete = () => resolve(saved);
62
+ tx.onabort = tx.onerror = () => reject(new Error(reason));
63
+ });
64
+ },
65
+ async cancel(id) {
66
+ const tx = (await openDb()).transaction(STORES.intents, "readwrite");
67
+ return new Promise((resolve, reject) => {
68
+ let saved;
69
+ const request = tx.objectStore(STORES.intents).get(id);
70
+ request.onsuccess = () => {
71
+ saved = request.result;
72
+ if (!saved || saved.review.state !== "pending") {
73
+ tx.abort();
74
+ return;
75
+ }
76
+ saved.review = {
77
+ ...saved.review,
78
+ state: "cancelled"
79
+ };
80
+ tx.objectStore(STORES.intents).put(saved);
81
+ };
82
+ tx.oncomplete = () => resolve(saved);
83
+ tx.onabort = tx.onerror = () => reject(engineError("cannotCancelSubmitted"));
84
+ });
85
+ }
86
+ };
87
+ /** Key for a wallet that needs no password: its seed is protected like the Cashu proofs in the same database. */
88
+ function newDeviceKey() {
89
+ return btoa(String.fromCharCode(...crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(32)))).replace(/=+$/, "");
90
+ }
91
+ async function passwordKey(password, salt) {
92
+ const material = await crypto.subtle.importKey("raw", new TextEncoder().encode(password), "PBKDF2", false, ["deriveKey"]);
93
+ return crypto.subtle.deriveKey({
94
+ name: "PBKDF2",
95
+ hash: "SHA-256",
96
+ salt,
97
+ iterations: 6e5
98
+ }, material, {
99
+ name: "AES-GCM",
100
+ length: 256
101
+ }, false, ["encrypt", "decrypt"]);
102
+ }
103
+ async function sealSeed(seed, password) {
104
+ if (password.length < 12) throw new Error("Use at least 12 characters for the wallet password");
105
+ const salt = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16)), iv = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(12));
106
+ const ciphertext = await crypto.subtle.encrypt({
107
+ name: "AES-GCM",
108
+ iv
109
+ }, await passwordKey(password, salt), new TextEncoder().encode(seed));
110
+ return {
111
+ version: 1,
112
+ salt: [...salt],
113
+ iv: [...iv],
114
+ ciphertext: [...new Uint8Array(ciphertext)]
115
+ };
116
+ }
117
+ async function unsealSeed(seed, password) {
118
+ if (seed.version !== 1) throw new Error("Unsupported wallet backup version");
119
+ try {
120
+ return new TextDecoder().decode(await crypto.subtle.decrypt({
121
+ name: "AES-GCM",
122
+ iv: new Uint8Array(seed.iv)
123
+ }, await passwordKey(password, new Uint8Array(seed.salt)), new Uint8Array(seed.ciphertext)));
124
+ } catch {
125
+ throw new Error("Could not unlock the wallet. Check the password and backup.");
126
+ }
127
+ }
128
+ //#endregion
129
+ //#region ../browser/src/engine/paymentAdapters/networkReason.ts
130
+ /**
131
+ * A request's failure as a person says it, when the browser's own words say nothing: a request that timed out or was
132
+ * given up ("Fetch is aborted" in Safari, "signal timed out" in Chromium) did not answer in time; one that never left
133
+ * ("Failed to fetch", "Load failed") could not reach it. `host` names what was asked, when known. Anything else is
134
+ * `null`: its own words are kept.
135
+ */
136
+ function networkReason(error, host) {
137
+ const name = error instanceof Error ? error.name : "";
138
+ const text = error instanceof Error ? error.message : String(error);
139
+ if (name === "AbortError" || name === "TimeoutError" || /^(fetch is aborted|signal timed out|signal is aborted without reason|the (user|operation) (aborted a request|was aborted)\.?|this operation was aborted|the operation timed out\.?)$/i.test(text.trim())) return host ? engineText("hostTimedOut", { host }) : engineText("networkTimedOut");
140
+ if (/^(failed to fetch|load failed|fetch failed|networkerror when attempting to fetch resource\.?|network request failed)$/i.test(text.trim())) return host ? engineText("hostUnreachable", { host }) : engineText("networkUnreachable");
141
+ return null;
142
+ }
143
+ /** A request URL's host, for a reason that names it; the URL itself when it does not parse. */
144
+ function rpcHost(url) {
145
+ try {
146
+ return new URL(url).host;
147
+ } catch {
148
+ return url;
149
+ }
150
+ }
151
+ //#endregion
152
+ export { sealSeed as a, newDeviceKey as i, rpcHost as n, unsealSeed as o, intentRepository as r, networkReason as t };
153
+
154
+ //# sourceMappingURL=networkReason-DxZWHw6v.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"networkReason-DxZWHw6v.js","names":[],"sources":["../../../browser/src/engine/paymentAdapters/persistence.ts","../../../browser/src/engine/paymentAdapters/networkReason.ts"],"sourcesContent":["import { engineError, engineText } from \"@ghostly/core\";\nimport { STORES, openDb, store, transact, wrap } from \"../../shared/idb\";\nimport type { IntentRepository, SavedIntent } from \"./coordinator\";\n/**\n * A USDT payment is one transaction at one account nonce. Another payment from the same account and chain that is\n * submitted or of unknown outcome holds its nonce until it confirms. A review made after it was sent took the next\n * nonce and may go at once. One made before (or read from an RPC that had not seen it yet) has the same or a lower\n * nonce: sent, it would replace the earlier transaction or be dropped, so it is refused. A row with no nonce refuses.\n */\nfunction nonceTaken(review:SavedIntent[\"review\"],intents:SavedIntent[]):boolean {\n if(review.method!==\"usdt\")return false;\n const from=review.evm?.from?.toLowerCase(), nonce=review.evm?.nonce;\n return intents.some(({review:other})=>other.id!==review.id && other.method===\"usdt\" && other.chainId===review.chainId && other.evm?.from?.toLowerCase()===from && [\"submitted\",\"unknown\"].includes(other.state)\n && !(typeof nonce===\"number\" && typeof other.evm?.nonce===\"number\" && nonce>other.evm.nonce));\n}\nexport const intentRepository: IntentRepository = {\n async get(id) { return wrap<SavedIntent | undefined>((await store(STORES.intents,\"readonly\")).get(id)); },\n async list() { return wrap<SavedIntent[]>((await store(STORES.intents,\"readonly\")).getAll()); },\n async put(intent) { await transact([STORES.intents],s=>{s[STORES.intents].put(intent);}); },\n async claim(id) {\n const tx=(await openDb()).transaction(STORES.intents,\"readwrite\");\n return new Promise<SavedIntent>((resolve,reject)=>{\n let saved:SavedIntent;\n let reason=engineText(\"alreadySubmitted\");\n const request=tx.objectStore(STORES.intents).getAll();\n request.onsuccess=()=>{\n const intents:SavedIntent[]=request.result;\n saved=intents.find(item=>item.review.id===id)!;\n if(!saved || saved.review.state!==\"pending\") {tx.abort();return;}\n const duplicate=saved.review.requestId && intents.some(({review})=>review.id!==id && review.requestId===saved.review.requestId && review.linkId===saved.review.linkId && [\"submitted\",\"settled\",\"unknown\"].includes(review.state));\n if(duplicate){tx.abort();return;}\n if(nonceTaken(saved.review,intents)){reason=engineText(\"sentAfterReview\");tx.abort();return;}\n saved.review={...saved.review,state:\"submitted\",error:undefined,submittedAt:Date.now()};\n tx.objectStore(STORES.intents).put(saved);\n };\n tx.oncomplete=()=>resolve(saved);\n tx.onabort=tx.onerror=()=>reject(new Error(reason));\n });\n },\n async cancel(id) {\n const tx=(await openDb()).transaction(STORES.intents,\"readwrite\");\n return new Promise<SavedIntent>((resolve,reject)=>{\n let saved:SavedIntent;\n const request=tx.objectStore(STORES.intents).get(id);\n request.onsuccess=()=>{\n saved=request.result;\n if(!saved || saved.review.state!==\"pending\"){tx.abort();return;}\n saved.review={...saved.review,state:\"cancelled\"};\n tx.objectStore(STORES.intents).put(saved);\n };\n tx.oncomplete=()=>resolve(saved);\n tx.onabort=tx.onerror=()=>reject(engineError(\"cannotCancelSubmitted\"));\n });\n },\n};\n/** Key for a wallet that needs no password: its seed is protected like the Cashu proofs in the same database. */\nexport function newDeviceKey():string {return btoa(String.fromCharCode(...crypto.getRandomValues(new Uint8Array(32)))).replace(/=+$/,\"\");}\nexport interface EncryptedSeed {version:1;salt:number[];iv:number[];ciphertext:number[]}\nasync function passwordKey(password:string,salt:Uint8Array<ArrayBuffer>) {\n const material=await crypto.subtle.importKey(\"raw\",new TextEncoder().encode(password),\"PBKDF2\",false,[\"deriveKey\"]);\n return crypto.subtle.deriveKey({name:\"PBKDF2\",hash:\"SHA-256\",salt,iterations:600000},material,{name:\"AES-GCM\",length:256},false,[\"encrypt\",\"decrypt\"]);\n}\nexport async function sealSeed(seed:string,password:string):Promise<EncryptedSeed> {\n if(password.length<12)throw new Error(\"Use at least 12 characters for the wallet password\");\n const salt=crypto.getRandomValues(new Uint8Array(16)),iv=crypto.getRandomValues(new Uint8Array(12));\n const ciphertext=await crypto.subtle.encrypt({name:\"AES-GCM\",iv},await passwordKey(password,salt),new TextEncoder().encode(seed));\n return {version:1,salt:[...salt],iv:[...iv],ciphertext:[...new Uint8Array(ciphertext)]};\n}\nexport async function unsealSeed(seed:EncryptedSeed,password:string):Promise<string> {\n if(seed.version!==1)throw new Error(\"Unsupported wallet backup version\");\n try {return new TextDecoder().decode(await crypto.subtle.decrypt({name:\"AES-GCM\",iv:new Uint8Array(seed.iv)},await passwordKey(password,new Uint8Array(seed.salt)),new Uint8Array(seed.ciphertext)));}\n catch {throw new Error(\"Could not unlock the wallet. Check the password and backup.\");}\n}\n","import { engineText } from \"@ghostly/core\";\n\n/**\n * A request's failure as a person says it, when the browser's own words say nothing: a request that timed out or was\n * given up (\"Fetch is aborted\" in Safari, \"signal timed out\" in Chromium) did not answer in time; one that never left\n * (\"Failed to fetch\", \"Load failed\") could not reach it. `host` names what was asked, when known. Anything else is\n * `null`: its own words are kept.\n */\nexport function networkReason(error: unknown, host?: string): string | null {\n const name = error instanceof Error ? error.name : \"\";\n const text = error instanceof Error ? error.message : String(error);\n if (name === \"AbortError\" || name === \"TimeoutError\" || /^(fetch is aborted|signal timed out|signal is aborted without reason|the (user|operation) (aborted a request|was aborted)\\.?|this operation was aborted|the operation timed out\\.?)$/i.test(text.trim()))\n return host ? engineText(\"hostTimedOut\", { host }) : engineText(\"networkTimedOut\");\n if (/^(failed to fetch|load failed|fetch failed|networkerror when attempting to fetch resource\\.?|network request failed)$/i.test(text.trim()))\n return host ? engineText(\"hostUnreachable\", { host }) : engineText(\"networkUnreachable\");\n return null;\n}\n\n/** A request URL's host, for a reason that names it; the URL itself when it does not parse. */\nexport function rpcHost(url: string): string {\n try { return new URL(url).host; } catch { return url; }\n}\n"],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ import { chmodSync, lstatSync, mkdirSync } from "node:fs";
2
+ //#region src/privateFolder.ts
3
+ /**
4
+ * A folder for a socket outside the profile (when the profile's path is too long for one): made owner-only, and
5
+ * refused when it is a link, someone else made it, or anyone else may enter it.
6
+ */
7
+ function privateFolder(path, what) {
8
+ mkdirSync(path, {
9
+ recursive: true,
10
+ mode: 448
11
+ });
12
+ const info = lstatSync(path);
13
+ if (!info.isDirectory() || process.getuid && info.uid !== process.getuid() || (info.mode & 63) !== 0) throw new Error(`${path} is not a folder of this user's alone: ${what}`);
14
+ }
15
+ /**
16
+ * The profile's own folder, made owner-only again before the daemon's socket goes in it: a folder made by an older
17
+ * release, or opened up by hand, is closed rather than trusted. Refused when it is a link or someone else's.
18
+ */
19
+ function ownFolder(path, what) {
20
+ const info = lstatSync(path);
21
+ if (!info.isDirectory() || process.getuid && info.uid !== process.getuid()) throw new Error(`${path} is not a folder of this user's: ${what}`);
22
+ if ((info.mode & 63) !== 0) chmodSync(path, 448);
23
+ }
24
+ /** False when `path` is there but is not a socket of this user's: nothing to connect to. */
25
+ function ownSocket(path) {
26
+ if (process.platform === "win32") return true;
27
+ let info;
28
+ try {
29
+ info = lstatSync(path);
30
+ } catch {
31
+ return true;
32
+ }
33
+ return info.isSocket() && (!process.getuid || info.uid === process.getuid());
34
+ }
35
+ //#endregion
36
+ export { ownSocket as n, privateFolder as r, ownFolder as t };
37
+
38
+ //# sourceMappingURL=privateFolder-Bk9C9oFv.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"privateFolder-Bk9C9oFv.js","names":[],"sources":["../../src/privateFolder.ts"],"sourcesContent":["import { chmodSync, lstatSync, mkdirSync } from \"node:fs\";\n\n/**\n * A folder for a socket outside the profile (when the profile's path is too long for one): made owner-only, and\n * refused when it is a link, someone else made it, or anyone else may enter it.\n */\nexport function privateFolder(path: string, what: string): void {\n mkdirSync(path, { recursive: true, mode: 0o700 });\n const info = lstatSync(path);\n if (!info.isDirectory() || (process.getuid && info.uid !== process.getuid()) || (info.mode & 0o077) !== 0) {\n throw new Error(`${path} is not a folder of this user's alone: ${what}`);\n }\n}\n\n/**\n * The profile's own folder, made owner-only again before the daemon's socket goes in it: a folder made by an older\n * release, or opened up by hand, is closed rather than trusted. Refused when it is a link or someone else's.\n */\nexport function ownFolder(path: string, what: string): void {\n const info = lstatSync(path);\n if (!info.isDirectory() || (process.getuid && info.uid !== process.getuid())) throw new Error(`${path} is not a folder of this user's: ${what}`);\n if ((info.mode & 0o077) !== 0) chmodSync(path, 0o700);\n}\n\n/** False when `path` is there but is not a socket of this user's: nothing to connect to. */\nexport function ownSocket(path: string): boolean {\n if (process.platform === \"win32\") return true;\n let info;\n try { info = lstatSync(path); } catch { return true; }\n return info.isSocket() && (!process.getuid || info.uid === process.getuid());\n}\n"],"mappings":";;;;;;AAMA,SAAgB,cAAc,MAAc,MAAoB;CAC9D,UAAU,MAAM;EAAE,WAAW;EAAM,MAAM;CAAM,CAAC;CAChD,MAAM,OAAO,UAAU,IAAI;CAC3B,IAAI,CAAC,KAAK,YAAY,KAAM,QAAQ,UAAU,KAAK,QAAQ,QAAQ,OAAO,MAAO,KAAK,OAAO,QAAW,GACtG,MAAM,IAAI,MAAM,GAAG,KAAK,yCAAyC,MAAM;AAE3E;;;;;AAMA,SAAgB,UAAU,MAAc,MAAoB;CAC1D,MAAM,OAAO,UAAU,IAAI;CAC3B,IAAI,CAAC,KAAK,YAAY,KAAM,QAAQ,UAAU,KAAK,QAAQ,QAAQ,OAAO,GAAI,MAAM,IAAI,MAAM,GAAG,KAAK,mCAAmC,MAAM;CAC/I,KAAK,KAAK,OAAO,QAAW,GAAG,UAAU,MAAM,GAAK;AACtD;;AAGA,SAAgB,UAAU,MAAuB;CAC/C,IAAI,QAAQ,aAAa,SAAS,OAAO;CACzC,IAAI;CACJ,IAAI;EAAE,OAAO,UAAU,IAAI;CAAG,QAAQ;EAAE,OAAO;CAAM;CACrD,OAAO,KAAK,SAAS,MAAM,CAAC,QAAQ,UAAU,KAAK,QAAQ,QAAQ,OAAO;AAC5E"}
@@ -0,0 +1,139 @@
1
+ import { t as CliError } from "./errors-my6VJR_0.js";
2
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync, writeSync } from "node:fs";
3
+ import { join, resolve } from "node:path";
4
+ import { createHash } from "node:crypto";
5
+ import { homedir } from "node:os";
6
+ //#region src/profiles.ts
7
+ /**
8
+ * Profiles on disk (WISP 1100 § Profiles and files): `<home>/profiles/<name>/`, 0700, one engine store each.
9
+ * `<home>/current` names the profile commands use when none is given.
10
+ */
11
+ var DEFAULT_PROFILE = "default";
12
+ var NAME = /^[a-z0-9][a-z0-9._-]{0,63}$/i;
13
+ /**
14
+ * An empty `--home` or `--profile` (an unset variable in a script) is a usage error: taken as not given, it ran the
15
+ * command in the current folder, or as the current profile. An empty variable of the environment counts as unset.
16
+ */
17
+ function ghostlyHome(option) {
18
+ if (option === "") throw new CliError("usage", "--home needs a folder");
19
+ return resolve(option ?? (process.env.GHOSTLY_HOME || join(homedir(), ".ghostly")));
20
+ }
21
+ function checkProfileName(name) {
22
+ if (!NAME.test(name) || name.includes("..")) throw new CliError("usage", `Not a profile name: ${JSON.stringify(name)} (letters, digits, ".", "_" and "-", up to 64)`);
23
+ return name;
24
+ }
25
+ function currentProfile(home, option) {
26
+ if (option === "") throw new CliError("usage", "--profile needs a profile name");
27
+ if (option !== void 0) return checkProfileName(option);
28
+ if (process.env.GHOSTLY_PROFILE) return checkProfileName(process.env.GHOSTLY_PROFILE);
29
+ const file = join(home, "current");
30
+ if (existsSync(file)) {
31
+ const name = readFileSync(file, "utf8").trim();
32
+ if (name) return checkProfileName(name);
33
+ }
34
+ return DEFAULT_PROFILE;
35
+ }
36
+ function profileDir(home, name) {
37
+ return join(home, "profiles", checkProfileName(name));
38
+ }
39
+ function profilePaths(home, name) {
40
+ const dir = profileDir(home, name);
41
+ return {
42
+ name,
43
+ dir,
44
+ db: join(dir, "db"),
45
+ socket: socketPath(dir),
46
+ lock: join(dir, "daemon.lock"),
47
+ events: join(dir, "events.jsonl"),
48
+ files: join(dir, "files"),
49
+ log: join(dir, "daemon.log")
50
+ };
51
+ }
52
+ /**
53
+ * The daemon's socket: in the profile's folder, unless that path is longer than a Unix socket may be (104 bytes on
54
+ * macOS); then in a folder of /tmp named after the profile's, which the daemon makes owner-only and checks before
55
+ * use (`privateFolder`), and whose socket a client connects to only when it is this user's (`ownSocket`).
56
+ */
57
+ function socketPath(dir) {
58
+ const inside = join(dir, "daemon.sock");
59
+ if (process.platform === "win32") return `\\\\.\\pipe\\ghostly-${createHash("sha256").update(dir).digest("hex").slice(0, 24)}`;
60
+ if (Buffer.byteLength(inside) <= 100) return inside;
61
+ return join("/tmp", `ghostly-${createHash("sha256").update(dir).digest("hex").slice(0, 24)}`, "daemon.sock");
62
+ }
63
+ function ensureFolder(path) {
64
+ mkdirSync(path, {
65
+ recursive: true,
66
+ mode: 448
67
+ });
68
+ }
69
+ /** Makes the profile's folder; refused when it exists and `existing` is not allowed. */
70
+ function createProfile(home, name, { existing = false } = {}) {
71
+ const paths = profilePaths(home, name);
72
+ if (existsSync(paths.dir) && !existing) throw new CliError("refused", `Profile ${name} exists`);
73
+ ensureFolder(join(home, "profiles"));
74
+ ensureFolder(paths.dir);
75
+ return paths;
76
+ }
77
+ /**
78
+ * A file in the folder of a profile a restore made, until its engine starts for the first time: the store came back
79
+ * whole, as the backup held it, and the money in it is then treated as every restore treats it (WISP 05 § Restoring).
80
+ */
81
+ var RESTORED_MARK = "restored";
82
+ function profileExists(home, name) {
83
+ return existsSync(profileDir(home, name));
84
+ }
85
+ function listProfiles(home) {
86
+ const root = join(home, "profiles");
87
+ if (!existsSync(root)) return [];
88
+ return readdirSync(root).filter((name) => NAME.test(name) && statSync(join(root, name)).isDirectory()).sort();
89
+ }
90
+ function selectProfile(home, name) {
91
+ if (!profileExists(home, name)) throw new CliError("not_found", `No profile ${name}`);
92
+ ensureFolder(home);
93
+ writeFileSync(join(home, "current"), name + "\n", { mode: 384 });
94
+ }
95
+ /**
96
+ * One process per profile: the daemon or a one-shot command. The lock holds the owner's pid; a lock whose
97
+ * process is gone is taken over.
98
+ */
99
+ function acquireLock(paths) {
100
+ for (let attempt = 0; attempt < 2; attempt++) try {
101
+ const fd = openSync(paths.lock, "wx", 384);
102
+ writeSync(fd, String(process.pid));
103
+ closeSync(fd);
104
+ let released = false;
105
+ return () => {
106
+ if (released) return;
107
+ released = true;
108
+ try {
109
+ if (readFileSync(paths.lock, "utf8").trim() === String(process.pid)) rmSync(paths.lock, { force: true });
110
+ } catch {}
111
+ };
112
+ } catch (error) {
113
+ if (error.code !== "EEXIST") throw error;
114
+ const owner = lockOwner(paths);
115
+ if (owner !== null) throw new CliError("busy", `Profile ${paths.name} is in use by process ${owner}`, { pid: owner });
116
+ rmSync(paths.lock, { force: true });
117
+ }
118
+ throw new CliError("busy", `Profile ${paths.name} is in use`);
119
+ }
120
+ /** The pid holding the profile, or null when nobody (or a process that is gone) does. */
121
+ function lockOwner(paths) {
122
+ let pid;
123
+ try {
124
+ pid = Number(readFileSync(paths.lock, "utf8").trim());
125
+ } catch {
126
+ return null;
127
+ }
128
+ if (!Number.isInteger(pid) || pid <= 0) return null;
129
+ try {
130
+ process.kill(pid, 0);
131
+ return pid;
132
+ } catch (error) {
133
+ return error.code === "EPERM" ? pid : null;
134
+ }
135
+ }
136
+ //#endregion
137
+ export { createProfile as a, listProfiles as c, profilePaths as d, selectProfile as f, checkProfileName as i, lockOwner as l, RESTORED_MARK as n, currentProfile as o, acquireLock as r, ghostlyHome as s, DEFAULT_PROFILE as t, profileExists as u };
138
+
139
+ //# sourceMappingURL=profiles-B6F4oa9K.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profiles-B6F4oa9K.js","names":[],"sources":["../../src/profiles.ts"],"sourcesContent":["import { closeSync, existsSync, mkdirSync, openSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync, writeSync } from \"node:fs\";\nimport { createHash } from \"node:crypto\";\nimport { homedir } from \"node:os\";\nimport { join, resolve } from \"node:path\";\nimport { CliError } from \"./errors\";\n\n/**\n * Profiles on disk (WISP 1100 § Profiles and files): `<home>/profiles/<name>/`, 0700, one engine store each.\n * `<home>/current` names the profile commands use when none is given.\n */\nexport const DEFAULT_PROFILE = \"default\";\nconst NAME = /^[a-z0-9][a-z0-9._-]{0,63}$/i;\n\n/**\n * An empty `--home` or `--profile` (an unset variable in a script) is a usage error: taken as not given, it ran the\n * command in the current folder, or as the current profile. An empty variable of the environment counts as unset.\n */\nexport function ghostlyHome(option?: string): string {\n if (option === \"\") throw new CliError(\"usage\", \"--home needs a folder\");\n return resolve(option ?? (process.env.GHOSTLY_HOME || join(homedir(), \".ghostly\")));\n}\n\nexport function checkProfileName(name: string): string {\n if (!NAME.test(name) || name.includes(\"..\")) throw new CliError(\"usage\", `Not a profile name: ${JSON.stringify(name)} (letters, digits, \".\", \"_\" and \"-\", up to 64)`);\n return name;\n}\n\nexport function currentProfile(home: string, option?: string): string {\n if (option === \"\") throw new CliError(\"usage\", \"--profile needs a profile name\");\n if (option !== undefined) return checkProfileName(option);\n if (process.env.GHOSTLY_PROFILE) return checkProfileName(process.env.GHOSTLY_PROFILE);\n const file = join(home, \"current\");\n if (existsSync(file)) {\n const name = readFileSync(file, \"utf8\").trim();\n if (name) return checkProfileName(name);\n }\n return DEFAULT_PROFILE;\n}\n\nexport function profileDir(home: string, name: string): string {\n return join(home, \"profiles\", checkProfileName(name));\n}\n\nexport interface ProfilePaths { name: string; dir: string; db: string; socket: string; lock: string; events: string; files: string; log: string }\nexport function profilePaths(home: string, name: string): ProfilePaths {\n const dir = profileDir(home, name);\n return {\n name, dir, db: join(dir, \"db\"), socket: socketPath(dir), lock: join(dir, \"daemon.lock\"),\n events: join(dir, \"events.jsonl\"), files: join(dir, \"files\"), log: join(dir, \"daemon.log\"),\n };\n}\n\n/**\n * The daemon's socket: in the profile's folder, unless that path is longer than a Unix socket may be (104 bytes on\n * macOS); then in a folder of /tmp named after the profile's, which the daemon makes owner-only and checks before\n * use (`privateFolder`), and whose socket a client connects to only when it is this user's (`ownSocket`).\n */\nfunction socketPath(dir: string): string {\n const inside = join(dir, \"daemon.sock\");\n if (process.platform === \"win32\") return `\\\\\\\\.\\\\pipe\\\\ghostly-${createHash(\"sha256\").update(dir).digest(\"hex\").slice(0, 24)}`;\n if (Buffer.byteLength(inside) <= 100) return inside;\n return join(\"/tmp\", `ghostly-${createHash(\"sha256\").update(dir).digest(\"hex\").slice(0, 24)}`, \"daemon.sock\");\n}\n\nfunction ensureFolder(path: string): void {\n mkdirSync(path, { recursive: true, mode: 0o700 });\n}\n\n/** Makes the profile's folder; refused when it exists and `existing` is not allowed. */\nexport function createProfile(home: string, name: string, { existing = false } = {}): ProfilePaths {\n const paths = profilePaths(home, name);\n if (existsSync(paths.dir) && !existing) throw new CliError(\"refused\", `Profile ${name} exists`);\n ensureFolder(join(home, \"profiles\"));\n ensureFolder(paths.dir);\n return paths;\n}\n\n/**\n * A file in the folder of a profile a restore made, until its engine starts for the first time: the store came back\n * whole, as the backup held it, and the money in it is then treated as every restore treats it (WISP 05 § Restoring).\n */\nexport const RESTORED_MARK = \"restored\";\n\nexport function profileExists(home: string, name: string): boolean {\n return existsSync(profileDir(home, name));\n}\n\nexport function listProfiles(home: string): string[] {\n const root = join(home, \"profiles\");\n if (!existsSync(root)) return [];\n return readdirSync(root).filter((name) => NAME.test(name) && statSync(join(root, name)).isDirectory()).sort();\n}\n\nexport function selectProfile(home: string, name: string): void {\n if (!profileExists(home, name)) throw new CliError(\"not_found\", `No profile ${name}`);\n ensureFolder(home);\n writeFileSync(join(home, \"current\"), name + \"\\n\", { mode: 0o600 });\n}\n\n/**\n * One process per profile: the daemon or a one-shot command. The lock holds the owner's pid; a lock whose\n * process is gone is taken over.\n */\nexport function acquireLock(paths: ProfilePaths): () => void {\n for (let attempt = 0; attempt < 2; attempt++) {\n try {\n const fd = openSync(paths.lock, \"wx\", 0o600);\n writeSync(fd, String(process.pid));\n closeSync(fd);\n let released = false;\n return () => {\n if (released) return;\n released = true;\n try { if (readFileSync(paths.lock, \"utf8\").trim() === String(process.pid)) rmSync(paths.lock, { force: true }); } catch { /* gone already */ }\n };\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== \"EEXIST\") throw error;\n const owner = lockOwner(paths);\n if (owner !== null) throw new CliError(\"busy\", `Profile ${paths.name} is in use by process ${owner}`, { pid: owner });\n rmSync(paths.lock, { force: true });\n }\n }\n throw new CliError(\"busy\", `Profile ${paths.name} is in use`);\n}\n\n/** The pid holding the profile, or null when nobody (or a process that is gone) does. */\nexport function lockOwner(paths: ProfilePaths): number | null {\n let pid: number;\n try { pid = Number(readFileSync(paths.lock, \"utf8\").trim()); } catch { return null; }\n if (!Number.isInteger(pid) || pid <= 0) return null;\n try { process.kill(pid, 0); return pid; } catch (error) { return (error as NodeJS.ErrnoException).code === \"EPERM\" ? pid : null; }\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { i as STORES, u as openDb } from "./idb-CTF6LF5B.js";
1
+ import { i as STORES, u as openDb } from "./idb-B5L8wuOm.js";
2
2
  //#region ../browser/src/shared/restoredRows.ts
3
3
  /**
4
4
  * What a restore changes in the money a backup carries (WISP 05 § Restoring). A bundle holds the wallet as it stood
@@ -111,4 +111,4 @@ async function markLeftOutFiles(ids) {
111
111
  //#endregion
112
112
  export { markLeftOutFiles, markRestoredWallet };
113
113
 
114
- //# sourceMappingURL=restoredRows-C_84q0Pd.js.map
114
+ //# sourceMappingURL=restoredRows-Db3oUTko.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"restoredRows-C_84q0Pd.js","names":[],"sources":["../../../browser/src/shared/restoredRows.ts"],"sourcesContent":["import { STORES, openDb } from \"./idb\";\n\n/**\n * What a restore changes in the money a backup carries (WISP 05 § Restoring). A bundle holds the wallet as it stood\n * when the backup was made, and the profile it came from may have gone on since: none of it is taken at its word.\n */\nexport const RESTORED_WALLET_STORES: readonly string[] = [STORES.proofs, STORES.swaps, STORES.intents];\n\n/** One row of `store` as a restore writes it. A row of any other store, or one that is not what it should be, is kept as it is. */\nexport function restoredWalletRow(store: string, value: unknown): unknown {\n if (!value || typeof value !== \"object\") return value;\n if (store === STORES.proofs) {\n // Ecash as it was when the backup was made: what was spent since is found by asking the mint, once the wallet runs.\n const proof = value as { secret?: unknown; reserved?: boolean };\n return typeof proof.secret === \"string\" && !proof.reserved ? { ...proof, unchecked: true } : value;\n }\n // A swap as it stood when the backup was made: what it brings is checked like the copy's ecash.\n if (store === STORES.swaps) return { ...value, restored: true };\n if (store === STORES.intents) {\n // An older copy cannot prove an unfinished attempt was never sent; it may not authorize a new one.\n const intent = value as { review?: { state?: string; method?: string } };\n // An on-chain review never approved stays unapproved: only Approve on this profile could send it, and a copy has none.\n if (intent.review?.method === \"bitcoin\" && intent.review.state === \"pending\") return { ...intent, review: { ...intent.review, state: \"cancelled\" } };\n return intent.review && [\"pending\", \"submitted\", \"unknown\"].includes(intent.review.state ?? \"\") ? { ...intent, review: { ...intent.review, state: \"unknown\" } } : value;\n }\n return value;\n}\n\n/**\n * The same, for a restore that brought the profile's database back whole (the headless CLI's): every wallet row of\n * this profile's database is rewritten as `restoredWalletRow` says, in one transaction, before the engine reads any.\n * Doing it twice changes nothing more.\n */\nexport async function markRestoredWallet(): Promise<void> {\n const db = await openDb();\n const names = RESTORED_WALLET_STORES.filter((name) => db.objectStoreNames.contains(name));\n if (!names.length) return;\n const tx = db.transaction(names, \"readwrite\");\n const done = new Promise<void>((resolve, reject) => {\n tx.oncomplete = () => resolve();\n tx.onabort = () => reject(tx.error ?? new Error(\"The restored wallet could not be marked\"));\n tx.onerror = () => reject(tx.error ?? new Error(\"The restored wallet could not be marked\"));\n });\n for (const name of names) {\n const walk = tx.objectStore(name).openCursor();\n walk.onsuccess = () => {\n const cursor = walk.result;\n if (!cursor) return;\n const next = restoredWalletRow(name, cursor.value);\n if (next !== cursor.value) cursor.update(next);\n cursor.continue();\n };\n }\n return done;\n}\n\n/**\n * Files a light backup left out (WISP 05 § Light backups), for a restore that brought the database back whole (the\n * headless CLI's): each record is marked `leftOut`, and nothing of it says any longer where its bytes were. A file\n * not in the database is passed over. Doing it twice changes nothing more.\n */\nexport async function markLeftOutFiles(ids: readonly string[]): Promise<void> {\n if (!ids.length) return;\n const db = await openDb();\n const tx = db.transaction([STORES.files, STORES.fileState], \"readwrite\");\n const done = new Promise<void>((resolve, reject) => {\n tx.oncomplete = () => resolve();\n tx.onabort = () => reject(tx.error ?? new Error(\"The files left out could not be marked\"));\n tx.onerror = () => reject(tx.error ?? new Error(\"The files left out could not be marked\"));\n });\n const files = tx.objectStore(STORES.files), states = tx.objectStore(STORES.fileState);\n for (const id of ids) {\n const read = files.get(id);\n read.onsuccess = () => {\n const record = read.result as { blob?: unknown; bytes?: unknown } | undefined;\n if (!record) return;\n const { blob: _blob, bytes: _bytes, ...rest } = record;\n files.put({ ...rest, leftOut: true });\n };\n const state = states.get(id);\n state.onsuccess = () => {\n const value = state.result as { bytes?: unknown } | undefined;\n if (!value || !(\"bytes\" in value)) return;\n const { bytes: _bytes, ...rest } = value;\n states.put(rest);\n };\n }\n return done;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"restoredRows-Db3oUTko.js","names":[],"sources":["../../../browser/src/shared/restoredRows.ts"],"sourcesContent":["import { STORES, openDb } from \"./idb\";\n\n/**\n * What a restore changes in the money a backup carries (WISP 05 § Restoring). A bundle holds the wallet as it stood\n * when the backup was made, and the profile it came from may have gone on since: none of it is taken at its word.\n */\nexport const RESTORED_WALLET_STORES: readonly string[] = [STORES.proofs, STORES.swaps, STORES.intents];\n\n/** One row of `store` as a restore writes it. A row of any other store, or one that is not what it should be, is kept as it is. */\nexport function restoredWalletRow(store: string, value: unknown): unknown {\n if (!value || typeof value !== \"object\") return value;\n if (store === STORES.proofs) {\n // Ecash as it was when the backup was made: what was spent since is found by asking the mint, once the wallet runs.\n const proof = value as { secret?: unknown; reserved?: boolean };\n return typeof proof.secret === \"string\" && !proof.reserved ? { ...proof, unchecked: true } : value;\n }\n // A swap as it stood when the backup was made: what it brings is checked like the copy's ecash.\n if (store === STORES.swaps) return { ...value, restored: true };\n if (store === STORES.intents) {\n // An older copy cannot prove an unfinished attempt was never sent; it may not authorize a new one.\n const intent = value as { review?: { state?: string; method?: string } };\n // An on-chain review never approved stays unapproved: only Approve on this profile could send it, and a copy has none.\n if (intent.review?.method === \"bitcoin\" && intent.review.state === \"pending\") return { ...intent, review: { ...intent.review, state: \"cancelled\" } };\n return intent.review && [\"pending\", \"submitted\", \"unknown\"].includes(intent.review.state ?? \"\") ? { ...intent, review: { ...intent.review, state: \"unknown\" } } : value;\n }\n return value;\n}\n\n/**\n * The same, for a restore that brought the profile's database back whole (the headless CLI's): every wallet row of\n * this profile's database is rewritten as `restoredWalletRow` says, in one transaction, before the engine reads any.\n * Doing it twice changes nothing more.\n */\nexport async function markRestoredWallet(): Promise<void> {\n const db = await openDb();\n const names = RESTORED_WALLET_STORES.filter((name) => db.objectStoreNames.contains(name));\n if (!names.length) return;\n const tx = db.transaction(names, \"readwrite\");\n const done = new Promise<void>((resolve, reject) => {\n tx.oncomplete = () => resolve();\n tx.onabort = () => reject(tx.error ?? new Error(\"The restored wallet could not be marked\"));\n tx.onerror = () => reject(tx.error ?? new Error(\"The restored wallet could not be marked\"));\n });\n for (const name of names) {\n const walk = tx.objectStore(name).openCursor();\n walk.onsuccess = () => {\n const cursor = walk.result;\n if (!cursor) return;\n const next = restoredWalletRow(name, cursor.value);\n if (next !== cursor.value) cursor.update(next);\n cursor.continue();\n };\n }\n return done;\n}\n\n/**\n * Files a light backup left out (WISP 05 § Light backups), for a restore that brought the database back whole (the\n * headless CLI's): each record is marked `leftOut`, and nothing of it says any longer where its bytes were. A file\n * not in the database is passed over. Doing it twice changes nothing more.\n */\nexport async function markLeftOutFiles(ids: readonly string[]): Promise<void> {\n if (!ids.length) return;\n const db = await openDb();\n const tx = db.transaction([STORES.files, STORES.fileState], \"readwrite\");\n const done = new Promise<void>((resolve, reject) => {\n tx.oncomplete = () => resolve();\n tx.onabort = () => reject(tx.error ?? new Error(\"The files left out could not be marked\"));\n tx.onerror = () => reject(tx.error ?? new Error(\"The files left out could not be marked\"));\n });\n const files = tx.objectStore(STORES.files), states = tx.objectStore(STORES.fileState);\n for (const id of ids) {\n const read = files.get(id);\n read.onsuccess = () => {\n const record = read.result as { blob?: unknown; bytes?: unknown } | undefined;\n if (!record) return;\n const { blob: _blob, bytes: _bytes, ...rest } = record;\n files.put({ ...rest, leftOut: true });\n };\n const state = states.get(id);\n state.onsuccess = () => {\n const value = state.result as { bytes?: unknown } | undefined;\n if (!value || !(\"bytes\" in value)) return;\n const { bytes: _bytes, ...rest } = value;\n states.put(rest);\n };\n }\n return done;\n}\n"],"mappings":"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"}