@ghostlytools/cli 1.0.3 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +1 -1
  2. package/dist/assets/bark_ffi_wasm_bg-EY2uc0Bx.js.map +1 -1
  3. package/dist/assets/bitcoindevkit_bg-B-iLWrmX.js.map +1 -1
  4. package/dist/assets/{fileBytes-CpP9Vj7s.js → fileBytes-BQ84L9Fe.js} +2 -2
  5. package/dist/assets/{fileBytes-CpP9Vj7s.js.map → fileBytes-BQ84L9Fe.js.map} +1 -1
  6. package/dist/assets/{fileBytes-Br_MpeNT.js → fileBytes-DxiJHmTk.js} +3 -3
  7. package/dist/assets/fileBytes-DxiJHmTk.js.map +1 -0
  8. package/dist/assets/{fileBytesIdb-vubSXPoO.js → fileBytesIdb-DQkQ10PS.js} +3 -3
  9. package/dist/assets/{fileBytesIdb-vubSXPoO.js.map → fileBytesIdb-DQkQ10PS.js.map} +1 -1
  10. package/dist/assets/{fileBytesOpfs-6iSa6u2K.js → fileBytesOpfs-BFfR_68b.js} +3 -3
  11. package/dist/assets/{fileBytesOpfs-6iSa6u2K.js.map → fileBytesOpfs-BFfR_68b.js.map} +1 -1
  12. package/dist/assets/{ghostly_iroh_web-CPa_YEfb.js → ghostly_iroh_web-Dvzh3Y6E.js} +32 -32
  13. package/dist/assets/{ghostly_iroh_web-CPa_YEfb.js.map → ghostly_iroh_web-Dvzh3Y6E.js.map} +1 -1
  14. package/dist/assets/ghostly_iroh_web_bg.wasm +0 -0
  15. package/dist/assets/{hyperdhtRelay-VM9STKLd.js → hyperdhtRelay-CVLCuqVr.js} +3 -3
  16. package/dist/assets/{hyperdhtRelay-VM9STKLd.js.map → hyperdhtRelay-CVLCuqVr.js.map} +1 -1
  17. package/dist/assets/{idb-DIN_Miwb.js → idb-Co2lObpU.js} +4 -4
  18. package/dist/assets/idb-Co2lObpU.js.map +1 -0
  19. package/dist/assets/{mainline-CcO2f8jy.js → mainline-CKFpwXcB.js} +2 -2
  20. package/dist/assets/{mainline-CcO2f8jy.js.map → mainline-CKFpwXcB.js.map} +1 -1
  21. package/dist/assets/{restoredRows-tVxgyG23.js → restoredRows-C1O9FfA4.js} +38 -3
  22. package/dist/assets/restoredRows-C1O9FfA4.js.map +1 -0
  23. package/dist/assets/{server-BnNXYzR9.js → server-vGoe9MET.js} +9460 -784
  24. package/dist/assets/server-vGoe9MET.js.map +1 -0
  25. package/dist/assets/{src-7sdQpCoi.js → src-CJdPTl7Q.js} +4216 -1305
  26. package/dist/assets/src-CJdPTl7Q.js.map +1 -0
  27. package/dist/ghostly.mjs +374 -45
  28. package/dist/ghostly.mjs.map +1 -1
  29. package/package.json +2 -1
  30. package/dist/assets/fileBytes-Br_MpeNT.js.map +0 -1
  31. package/dist/assets/idb-DIN_Miwb.js.map +0 -1
  32. package/dist/assets/restoredRows-tVxgyG23.js.map +0 -1
  33. package/dist/assets/server-BnNXYzR9.js.map +0 -1
  34. package/dist/assets/src-7sdQpCoi.js.map +0 -1
package/dist/ghostly.mjs CHANGED
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
- import { a as fileBytesOf, d as __exportAll, i as fileBytes, l as FILE_BYTES_STEP } from "./assets/fileBytes-Br_MpeNT.js";
3
- import { $ as atprotoProofRecord, B as parseSshPublicKey, Ca as callRtcConfig, Cn as wellKnownUrl, Ct as parseDid, Da as signalHasVideo, Dn as engineText, Dr as checkPushEndpoint, Dt as staticDidDocument, Ea as parseCallSignal, En as engineError, Et as signingKeys, G as verifyBitcoinMessage, Gi as RUN_RESULTS, Gr as IdentityCheckUnavailable, H as sameSshKey, Ha as setLinkTraceSink, Hi as replySnippet, I as groupName, Ia as inviteLink, J as ATPROTO_DECLARED_SCOPE, Ja as RELAY_PAYLOAD_MAX_BYTES, Ji as checkStatusCard, Jt as walletNetworkOf, K as BitcoinAddressError, Ki as STATUS_CARD_LIMITS, Nt as resolveDidDht, On as decodeBolt11, Ot as strictBase64Url, Q as atprotoDidMethod, R as SSHSIG_NAMESPACE, Sa as buildSdpFromSignal, Sn as txtValue, St as normalizeDid, Ta as extractParamsFromSdp, Tn as resolveLightningDestination, Tt as parseDidSignaturePaste, U as sshSignCommand, Ut as WALLET_NETWORKS, V as parseSshSignature, W as verifySshSignature, Wi as ITEM_STATES, X as ATPROTO_PROOF_COLLECTION, Xr as identityStatement, Y as ATPROTO_FULL_SCOPE, Z as ATPROTO_PROOF_SCOPE, Za as openRelayPayload, _i as parseVoiceMeta, _n as nip05RootKey, _t as didWebDocumentUrl, an as pubkyProofCapability, at as shortAtprotoDid, ba as RTC_CONFIG, bn as recordsFromWellKnown, bt as didWebFileUrl, cn as homeserverWebEndpoint, co as toBase64Url$1, ct as DID_DOCUMENT_MAX_BYTES$1, di as sanitizeFileName, dn as decodeDnsResponse, do as utf8Encode, dr as TYPING_REFRESH_MS, dt as DidError, en as PUBKY_PROOF_MAX_BYTES, et as atprotoProofRkey, fa as GhostlyHttpError, fi as sanitizeMime, fn as dnsProofName, ft as checkDidJws, ga as LIMITS, gn as isPublicAddress, gt as didSignedMessages, hi as baseMime, hn as encodeDnsQuery, ht as didJwsAlg, in as normalizePubkyKey, io as sanitizeAvatar, it as parseAtprotoDidDocument, kr as BUTTONS_CAPABILITY, li as readImageMeta, ln as pubkyHomeserverOf, lt as DID_FILE_MAX_BYTES, mi as VOICE_LIMITS, mn as domainWellKnownFile, mt as didGhostlyServiceEntry, nn as isPubkyProofFolder, nt as isAtprotoDid, on as pubkyProofFile, oo as fromBase64Url$1, ot as AtprotoProofError, pi as PLAYABLE_AUDIO, pn as domainTxtRecord, pr as sanitizeTypingStatus, pt as checkDidSignature, q as decodeAnyBitcoinAddress, qi as buttonLabelClash, qr as boundVerifiedIdentity, rn as newPubkyProofFolder, ro as MAX_AVATAR_LENGTH, rt as normalizeAtprotoHandle, sn as pubkyProofPath, so as randomBytes$1, st as verifyAtprotoRecordProof, tn as isPubkyKey, tt as checkAtprotoProofRecord, un as DOMAIN_FILE_MAX_BYTES, uo as utf8Decode, ur as TYPING_KINDS, vi as voicePeaksOf, vn as normalizeDomain, vt as didWebFile, wa as callSignalHeardAt, wn as requestLnurlInvoice, wt as parseDidDocument, xa as answersOffer, xn as recordsName, xt as didWebHost, ya as CALL_SIGNAL_MAX_AGE_MS, yn as recordsFromTxt, yt as didWebFileNames, z as SshSigError } from "./assets/src-7sdQpCoi.js";
4
- import { d as wrap, l as store$1, n as STORES, o as fileStore, s as openDb, u as transact } from "./assets/idb-DIN_Miwb.js";
2
+ import { a as fileBytesOf, d as __exportAll, i as fileBytes, l as FILE_BYTES_STEP } from "./assets/fileBytes-DxiJHmTk.js";
3
+ import { $ as checkAtprotoProofRecord, $s as randomBytes$1, $t as isPubkyKey, Aa as RTC_CONFIG, Ai as voicePeaksOf, B as sameSshKey, Ci as sanitizeFileName, Cn as resolveLightningDestination, Ct as parseDidSignaturePaste, Di as baseMime, Ea as LIMITS, Ei as VOICE_LIMITS, En as decodeBolt11, Et as strictBase64Url, Fa as extractParamsFromSdp, G as decodeAnyBitcoinAddress, H as verifySshSignature, Hs as openRelayPayload, I as SSHSIG_NAMESPACE, Ia as parseCallSignal, Ir as BUTTONS_CAPABILITY, J as ATPROTO_PROOF_COLLECTION, K as ATPROTO_DECLARED_SCOPE, Kt as walletNetworkOf, L as SshSigError, La as signalHasVideo, Ma as buildSdpFromSignal, Na as callRtcConfig, Ns as setLinkTraceSink, P as groupName, Pa as callSignalHeardAt, Pr as checkPushEndpoint, Q as atprotoProofRkey, Qi as replySnippet, Qr as IdentityCheckUnavailable, Qs as fromBase64Url$1, Qt as PUBKY_PROOF_MAX_BYTES, R as parseSshPublicKey, Sa as GhostlyHttpError, Sn as requestLnurlInvoice, St as parseDidDocument, Ti as PLAYABLE_AUDIO, Tn as engineText, Tt as staticDidDocument, U as verifyBitcoinMessage, V as sshSignCommand, Vt as WALLET_NETWORKS, W as BitcoinAddressError, X as atprotoDidMethod, Xs as sanitizeAvatar, Y as ATPROTO_PROOF_SCOPE, Ys as MAX_AVATAR_LENGTH, Z as atprotoProofRecord, _n as recordsFromTxt, _t as didWebFileNames, an as pubkyProofPath, at as verifyAtprotoRecordProof, bn as txtValue, bt as normalizeDid, cn as DOMAIN_FILE_MAX_BYTES, dn as domainTxtRecord, dt as checkDidSignature, ea as ITEM_STATES, ec as toBase64Url$1, ei as boundVerifiedIdentity, en as isPubkyProofFolder, et as isAtprotoDid, fn as domainWellKnownFile, ft as didGhostlyServiceEntry, gn as normalizeDomain, gr as TYPING_REFRESH_MS, gt as didWebFile, hn as nip05RootKey, hr as TYPING_KINDS, ht as didWebDocumentUrl, ia as checkStatusCard, in as pubkyProofFile, it as AtprotoProofError, ja as answersOffer, jt as resolveDidDht, ka as CALL_SIGNAL_MAX_AGE_MS, ki as parseVoiceMeta, ln as decodeDnsResponse, lt as DidError, mn as isPublicAddress, mt as didSignedMessages, na as STATUS_CARD_LIMITS, nc as utf8Decode, nn as normalizePubkyKey, ns as inviteLink, nt as parseAtprotoDidDocument, on as homeserverWebEndpoint, ot as DID_DOCUMENT_MAX_BYTES$1, pn as encodeDnsQuery, pt as didJwsAlg, q as ATPROTO_FULL_SCOPE, ra as buttonLabelClash, rc as utf8Encode, ri as identityStatement, rn as pubkyProofCapability, rt as shortAtprotoDid, sn as pubkyHomeserverOf, st as DID_FILE_MAX_BYTES, ta as RUN_RESULTS, tn as newPubkyProofFolder, tt as normalizeAtprotoHandle, un as dnsProofName, ut as checkDidJws, vn as recordsFromWellKnown, vr as sanitizeTypingStatus, vt as didWebFileUrl, wi as sanitizeMime, wn as engineError, wt as signingKeys, xi as readImageMeta, xn as wellKnownUrl, xt as parseDid, yn as recordsName, yt as didWebHost, z as parseSshSignature, zs as RELAY_PAYLOAD_MAX_BYTES } from "./assets/src-CJdPTl7Q.js";
4
+ import { d as wrap, i as databaseName, l as store$1, n as STORES, o as fileStore, s as openDb, u as transact } from "./assets/idb-Co2lObpU.js";
5
5
  import { createRequire } from "node:module";
6
6
  import { spawn } from "node:child_process";
7
7
  import { connect, createServer, isIP } from "node:net";
8
8
  import { appendFileSync, chmodSync, closeSync, existsSync, fsyncSync, lstatSync, mkdirSync, openAsBlob, openSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, writeSync } from "node:fs";
9
9
  import { ed25519 } from "@noble/curves/ed25519.js";
10
- import { createBase58check } from "@scure/base";
11
10
  import { sha256 } from "@noble/hashes/sha2.js";
11
+ import { createBase58check } from "@scure/base";
12
12
  import { schnorr } from "@noble/curves/secp256k1.js";
13
13
  import { wordlist } from "@scure/bip39/wordlists/english.js";
14
14
  import { link, mkdir, open, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
@@ -22,7 +22,7 @@ import { createServer as createServer$1 } from "node:http";
22
22
  import { Readable } from "node:stream";
23
23
  import { pipeline } from "node:stream/promises";
24
24
  import { homedir } from "node:os";
25
- import { generateMnemonic } from "@scure/bip39";
25
+ import { generateMnemonic, validateMnemonic } from "@scure/bip39";
26
26
  import { createInterface } from "node:readline";
27
27
  import { lookup } from "node:dns/promises";
28
28
  import { fileURLToPath, pathToFileURL } from "node:url";
@@ -34,7 +34,7 @@ import { IDBFactory, IDBKeyRange as IDBKeyRange$1, IDBTransaction } from "fake-i
34
34
  import { format } from "node:util";
35
35
  var package_default = {
36
36
  name: "@ghostlytools/cli",
37
- version: "1.0.3",
37
+ version: "1.1.1",
38
38
  type: "module",
39
39
  description: "Ghostly for bots and terminals: the app's own engine, headless on Node, behind a daemon and a `ghostly` command with JSON output.",
40
40
  license: "MIT",
@@ -78,6 +78,7 @@ var package_default = {
78
78
  "@scure/bip39": "2.4.0",
79
79
  "@scure/btc-signer": "2.4.1",
80
80
  "@secondts/bark": "0.25.0",
81
+ "@serenity-kit/opaque": "1.1.0",
81
82
  "@synonymdev/pubky": "0.14.0",
82
83
  "@tetherto/wdk-wallet-evm": "1.0.0-beta.19",
83
84
  "b4a": "1.9.0",
@@ -644,6 +645,7 @@ var ENGINE_METHODS = [
644
645
  "setWakeSubscription",
645
646
  "setWakeMuted",
646
647
  "wakeForCall",
648
+ "wakeConfirm",
647
649
  "setFastPoll",
648
650
  "createGroup",
649
651
  "inviteToGroup",
@@ -666,7 +668,37 @@ var ENGINE_METHODS = [
666
668
  "setGroupPicture",
667
669
  "renameGroup",
668
670
  "setGroupTyping",
669
- "forgetGroup"
671
+ "forgetGroup",
672
+ "setGroupManage",
673
+ "deviceEnrollInvite",
674
+ "deviceEnrollConfirm",
675
+ "deviceEnrollCancel",
676
+ "deviceEnrollView",
677
+ "deviceEnrollJoin",
678
+ "deviceEnrollFinish",
679
+ "deviceEnrollRemove",
680
+ "deviceSet",
681
+ "devicePing",
682
+ "deviceHandoffVerifier",
683
+ "deviceHandoffPush",
684
+ "deviceHandoffPull",
685
+ "deviceHandoffAccept",
686
+ "deviceHandoffSettle",
687
+ "deviceHandoffCancel",
688
+ "deviceHandoffView",
689
+ "deviceHandoffAllow",
690
+ "deviceTakeoverInfo",
691
+ "deviceTakeover",
692
+ "deviceTurnPeek",
693
+ "deviceForkDiscard",
694
+ "deviceRestoreStartOwn",
695
+ "deviceRemove",
696
+ "deviceNewSecret",
697
+ "deviceSecretOfferDismiss",
698
+ "deviceSetNoticeSeen",
699
+ "deviceTurnCheck",
700
+ "devicePushState",
701
+ "devicePushSet"
670
702
  ];
671
703
  /** Reads the hosts use outside `EngineApi`: the whole state, and one chat's stored messages. */
672
704
  var ENGINE_READS = ["getState", "getMessages"];
@@ -1628,6 +1660,7 @@ var FILE_METHODS = {
1628
1660
  });
1629
1661
  const stored = await fileStore.get(fileId);
1630
1662
  if (!stored?.metadata) throw new CliError("not_found", `No file ${fileId}`);
1663
+ if (stored.leftOut) throw new CliError("not_found", "Not in this backup: the light backup this profile was restored from left the file out");
1631
1664
  const transfer = state(ctx).transfers[fileId] ?? stored.transfer;
1632
1665
  if (transfer && transfer.state !== "done") throw new CliError("unavailable", `The file is not all here yet (${transfer.transferred} of ${transfer.size} bytes)`);
1633
1666
  const target = str(params, "path") ? resolve(str(params, "path", true)) : join(resolve(str(params, "dir") ?? "."), sanitizeFileName(stored.metadata.name));
@@ -1643,7 +1676,8 @@ var FILE_METHODS = {
1643
1676
  try {
1644
1677
  const backend = stored.bytes ? await fileBytesOf(stored.bytes) : null;
1645
1678
  if (backend) {
1646
- const size = await backend.size(fileId) ?? 0;
1679
+ const size = await backend.size(fileId);
1680
+ if (size === null) throw new CliError("not_found", "The file's bytes are not on this device");
1647
1681
  for (let offset = 0; offset < size; offset += FILE_BYTES_STEP) {
1648
1682
  const chunk = await backend.read(fileId, offset, FILE_BYTES_STEP);
1649
1683
  await out.write(chunk);
@@ -2992,7 +3026,7 @@ function nostrProofTemplate(statement) {
2992
3026
  tags: [["d", "ghostly-identity-proof"], ["expiration", String(expiresAt)]]
2993
3027
  };
2994
3028
  }
2995
- var hex$3 = (b) => Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
3029
+ var hex$4 = (b) => Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
2996
3030
  var unhex$1 = (s) => Uint8Array.from(s.match(/../g), (x) => parseInt(x, 16));
2997
3031
  function normalizeNostrKey(input) {
2998
3032
  const value = input.trim();
@@ -3076,7 +3110,7 @@ var nostr = {
3076
3110
  const expected = nostrProofTemplate(statement);
3077
3111
  if (event.pubkey !== statement.binding.subject) throw new Error("Signed by another Nostr key");
3078
3112
  if (event.kind !== expected.kind || event.created_at !== expected.created_at || event.content !== expected.content || JSON.stringify(event.tags) !== JSON.stringify(expected.tags)) throw new Error("The signer did not sign the exact Ghostly statement");
3079
- const id = hex$3(sha256(utf8Encode(JSON.stringify([
3113
+ const id = hex$4(sha256(utf8Encode(JSON.stringify([
3080
3114
  0,
3081
3115
  event.pubkey,
3082
3116
  event.created_at,
@@ -6430,7 +6464,7 @@ async function approveAndWork(options, work, { grant, cookie, opened, settled, c
6430
6464
  throw e;
6431
6465
  }
6432
6466
  const fail = (what) => (e) => {
6433
- if (viaCookie && !cookieKept && refused(e)) throw new Error("Pubky Ring approved, but this browser blocked the sign-in cookie of your homeserver, so nothing was changed. Approve with Pubky Passport instead, or use Ghostly in Chrome or the desktop app.");
6467
+ if (viaCookie && !cookieKept && refused(e)) throw new Error("Pubky Ring approved, but this browser blocks your homeserver's sign-in cookie (Vanadium and Safari block cookies from other sites), so nothing was written. Approve with Pubky Passport instead, or use Firefox or the Ghostly desktop app.");
6434
6468
  throw failure(what, e);
6435
6469
  };
6436
6470
  const approved = session;
@@ -6568,7 +6602,7 @@ var testIdentitiesEnabled = () => {
6568
6602
  }
6569
6603
  };
6570
6604
  var HEX64 = /^[a-f0-9]{64}$/;
6571
- var hex$2 = (b) => Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
6605
+ var hex$3 = (b) => Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
6572
6606
  var unhex = (s) => Uint8Array.from(s.match(/../g), (x) => parseInt(x, 16));
6573
6607
  var normalizeHexKey = (input) => {
6574
6608
  const v = input.trim().toLowerCase();
@@ -6578,7 +6612,7 @@ var normalizeHexKey = (input) => {
6578
6612
  var exactKeys = (raw, keys) => !!raw && typeof raw === "object" && !Array.isArray(raw) && Object.keys(raw).sort().join(",") === keys;
6579
6613
  /** The key behind the fake in-app signer. Tests may replace it. */
6580
6614
  var fakeKeyring = { seed: ed25519.utils.randomSecretKey() };
6581
- var fakeKeySubject = (seed = fakeKeyring.seed) => hex$2(ed25519.getPublicKey(seed));
6615
+ var fakeKeySubject = (seed = fakeKeyring.seed) => hex$3(ed25519.getPublicKey(seed));
6582
6616
  var fakeKeySign = (statement, seed = fakeKeyring.seed) => ({ sig: toBase64Url$1(ed25519.sign(statement.bytes, seed)) });
6583
6617
  var fakeKey = {
6584
6618
  id: "fake-key",
@@ -7842,18 +7876,18 @@ var BackupWriter = class BackupWriter {
7842
7876
  this.signal = signal;
7843
7877
  }
7844
7878
  /** `passphrase` null: a backup anyone can read (the caller has made sure that is what the person chose). */
7845
- static async start(sink, passphrase, signal) {
7879
+ static async start(sink, passphrase, signal, version = 2) {
7846
7880
  if (passphrase !== null && passphrase.length < 12) throw new Error("Use at least 12 characters for the backup passphrase");
7847
7881
  check(signal);
7848
7882
  const salt = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16)), base = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(7));
7849
7883
  const header = passphrase === null ? {
7850
7884
  format: "ghostly-backup",
7851
- version: 2,
7885
+ version,
7852
7886
  protection: "none",
7853
7887
  check: { name: "SHA-256-chain" }
7854
7888
  } : {
7855
7889
  format: "ghostly-backup",
7856
- version: 2,
7890
+ version,
7857
7891
  protection: "passphrase",
7858
7892
  kdf: {
7859
7893
  name: "PBKDF2-SHA256",
@@ -7933,7 +7967,7 @@ async function readBackupHeader(source) {
7933
7967
  return null;
7934
7968
  }
7935
7969
  if (header?.format !== "ghostly-backup" || typeof header.version !== "number" || header.version < 2) return null;
7936
- if (header.version !== 2) throw new Error("This backup comes from a newer Ghostly; update to restore it");
7970
+ if (header.version !== 2 && header.version !== 3) throw new Error("This backup comes from a newer Ghostly; update to restore it");
7937
7971
  if (header.protection === "none") {
7938
7972
  if (header.check?.name !== "SHA-256-chain") throw new Error("Unsupported backup format");
7939
7973
  } else if (header.protection === "passphrase") {
@@ -8046,6 +8080,21 @@ var BackupReader = class BackupReader {
8046
8080
  }
8047
8081
  };
8048
8082
  //#endregion
8083
+ //#region ../browser/src/backup/light.ts
8084
+ /**
8085
+ * A light backup (WISP 05 § Light backups): everything of a profile but the bytes of its larger files. Their messages
8086
+ * and records stay; after a restore they show as not in this backup.
8087
+ *
8088
+ * The cut: a file of at most 1 MiB keeps its bytes (a short clip, a sticker, a screenshot, a document), and so does a
8089
+ * voice message of at most 4 MiB. Voice messages are part of the conversation, and the longest one the apps record
8090
+ * (15 minutes at 32 kbit/s, `VOICE_LIMITS`) is about 3.6 MB, so every recorded one stays. Photos from a camera
8091
+ * (usually 2 to 8 MB), videos and large files are what is left out.
8092
+ */
8093
+ var LIGHT_FILE_BYTES = 1048576;
8094
+ var LIGHT_VOICE_BYTES = 4194304;
8095
+ /** Whether a file of this size keeps its bytes in a light backup. */
8096
+ var keptInLight = (size, voice) => size <= (voice ? LIGHT_VOICE_BYTES : LIGHT_FILE_BYTES);
8097
+ //#endregion
8049
8098
  //#region src/profiles.ts
8050
8099
  /**
8051
8100
  * Profiles on disk (WISP 11xx § Profiles and files): `<home>/profiles/<name>/`, 0700, one engine store each.
@@ -8213,7 +8262,8 @@ var BACKUP_METHODS = {
8213
8262
  /**
8214
8263
  * Writes the backup to `path` (resolved by the command), sealed with `passphrase`, or not encrypted when
8215
8264
  * `noPassphrase` is true: one of the two, never a guess. The store is folded first, so the copy is whole. It is
8216
- * written beside `path` and only takes its name once it is complete: a backup that fails leaves no file.
8265
+ * written beside `path` and only takes its name once it is complete: a backup that fails leaves no file. `light`:
8266
+ * the files over the cut (`keptInLight`) are left out, their records kept.
8217
8267
  */
8218
8268
  async "profile.backup"(ctx, params) {
8219
8269
  const path = resolve(str(params, "path", true));
@@ -8224,11 +8274,16 @@ async "profile.backup"(ctx, params) {
8224
8274
  await ctx.runtime.store.compact();
8225
8275
  const paths = ctx.runtime.paths;
8226
8276
  const store = join(paths.db, "snapshot.bin");
8227
- const files = await Promise.all((await walk(paths.files)).map(async (name) => ({
8277
+ const light = bool(params, "light");
8278
+ const all = await Promise.all((await walk(paths.files)).map(async (name) => ({
8228
8279
  name,
8229
8280
  size: (await stat(join(paths.files, name))).size
8230
8281
  })));
8282
+ const kept = async (file) => keptInLight(file.size, false) || file.size <= 4194304 && !!(await fileStore.get(basename(file.name)).catch(() => void 0))?.metadata?.voice;
8283
+ const files = [], leftOut = [];
8284
+ for (const file of all) (!light || await kept(file) ? files : leftOut).push(file);
8231
8285
  const storeSize = (await stat(store)).size, bytes = files.reduce((sum, file) => sum + file.size, 0);
8286
+ const leftOutBytes = leftOut.reduce((sum, file) => sum + file.size, 0);
8232
8287
  const partial = `${path}.partial-${process.pid}`;
8233
8288
  let out = null;
8234
8289
  try {
@@ -8248,7 +8303,14 @@ async "profile.backup"(ctx, params) {
8248
8303
  format: FORMAT$1,
8249
8304
  createdAt: Date.now(),
8250
8305
  files: files.length,
8251
- bytes
8306
+ bytes,
8307
+ ...light && { light: {
8308
+ maxFileBytes: 1048576,
8309
+ maxVoiceBytes: 4194304,
8310
+ files: leftOut.length,
8311
+ bytes: leftOutBytes,
8312
+ ids: leftOut.map((file) => basename(file.name))
8313
+ } }
8252
8314
  });
8253
8315
  await put({
8254
8316
  t: "file",
@@ -8283,7 +8345,12 @@ async "profile.backup"(ctx, params) {
8283
8345
  path,
8284
8346
  bytes: written,
8285
8347
  files: files.length,
8286
- protection: unsealed ? "none" : "passphrase"
8348
+ protection: unsealed ? "none" : "passphrase",
8349
+ ...light && {
8350
+ light: true,
8351
+ leftOut: leftOut.length,
8352
+ leftOutBytes
8353
+ }
8287
8354
  };
8288
8355
  } finally {
8289
8356
  await out?.close().catch(() => {});
@@ -8376,10 +8443,13 @@ async function restoringFolder(home, name) {
8376
8443
  files: join(dir, "files")
8377
8444
  };
8378
8445
  }
8379
- /** The folder is whole: marked as a restored copy (`RESTORED_MARK`), then given the profile's name. Never over a profile. */
8380
- async function finishRestore(home, name, work) {
8446
+ /**
8447
+ * The folder is whole: marked as a restored copy (`RESTORED_MARK`, which names the files a light backup left out),
8448
+ * then given the profile's name. Never over a profile.
8449
+ */
8450
+ async function finishRestore(home, name, work, leftOut = []) {
8381
8451
  const paths = profilePaths(home, name);
8382
- await writeFile(join(work.dir, RESTORED_MARK), "", { mode: 384 });
8452
+ await writeFile(join(work.dir, RESTORED_MARK), leftOut.length ? JSON.stringify({ leftOut }) : "", { mode: 384 });
8383
8453
  if (existsSync(paths.dir)) throw new CliError("refused", `Profile ${name} exists: restore into a new name`);
8384
8454
  await rename(work.dir, paths.dir);
8385
8455
  return paths;
@@ -8424,6 +8494,7 @@ async function restoreProfile(home, name, file, passphrase) {
8424
8494
  const first = await next();
8425
8495
  const head = first?.json === void 0 ? null : parse(first.json);
8426
8496
  if (head?.t !== "profile" || head.format !== FORMAT$1) throw new CliError("refused", "This backup is not a headless Ghostly profile");
8497
+ const leftOut = Array.isArray(head.light?.ids) ? head.light.ids.filter((id) => typeof id === "string") : [];
8427
8498
  work = await restoringFolder(home, name);
8428
8499
  const paths = work;
8429
8500
  await mkdir(paths.db, {
@@ -8474,7 +8545,7 @@ async function restoreProfile(home, name, file, passphrase) {
8474
8545
  } finally {
8475
8546
  await out?.handle.close().catch(() => {});
8476
8547
  }
8477
- return await finishRestore(home, name, work);
8548
+ return await finishRestore(home, name, work, leftOut);
8478
8549
  } catch (error) {
8479
8550
  if (work) await rm(work.dir, {
8480
8551
  recursive: true,
@@ -9000,6 +9071,45 @@ async function unsealSeed(seed, password) {
9000
9071
  }
9001
9072
  }
9002
9073
  //#endregion
9074
+ //#region ../browser/src/engine/paymentAdapters/away.ts
9075
+ var away = /* @__PURE__ */ new Map();
9076
+ /** The wallets away from this device, by wallet id, with the name of their home device ("" when it has none here). */
9077
+ function setAwayWallets(next) {
9078
+ away = new Map(next);
9079
+ }
9080
+ /** The home device's name of a wallet away from this device, or undefined when it is here. */
9081
+ function awayFrom(id) {
9082
+ return away.get(id);
9083
+ }
9084
+ /** A wallet that cannot be used here: it is at home on another device. */
9085
+ var WalletAwayError = class extends Error {
9086
+ constructor(wallet, device) {
9087
+ super(device ? engineText("walletAway", {
9088
+ wallet,
9089
+ device
9090
+ }) : engineText("walletAwayUnnamed", { wallet }));
9091
+ this.name = "WalletAwayError";
9092
+ }
9093
+ };
9094
+ /**
9095
+ * Whether a single-writer wallet (Ark, Bark, Spark and the Breez source, Fedimint) may open its SDK now: the engine
9096
+ * says, from a good turn read under 60 seconds old (WISP 06 § Wallets). Every open asks, the retries of each wallet
9097
+ * included, so a device replaced while it was offline does not reopen one on a retry.
9098
+ */
9099
+ var turnGate = async () => true;
9100
+ function setSingleWriterGate(gate) {
9101
+ turnGate = gate ?? (async () => true);
9102
+ }
9103
+ /** Throws (the wallet tries again later, as it does when its server does not answer) without a fresh good turn read. */
9104
+ async function requireTurn() {
9105
+ if (!await turnGate().catch(() => false)) throw new Error("Checking which device is active");
9106
+ }
9107
+ /** Throws when the wallet `id` is at home on another device. */
9108
+ function refuseAway(id, wallet) {
9109
+ const device = awayFrom(id);
9110
+ if (device !== void 0) throw new WalletAwayError(wallet, device);
9111
+ }
9112
+ //#endregion
9003
9113
  //#region ../browser/src/engine/paymentAdapters/providers/sources.ts
9004
9114
  /** Where a source is stored: the mode's one, or one of several (`key`). */
9005
9115
  var sourceKey = (kind, mode, key) => `${kind}Source-${mode}${key ? `-${key}` : ""}`;
@@ -9117,6 +9227,13 @@ var ProviderSources = class {
9117
9227
  await this.load(this.mode);
9118
9228
  this.view = this.idle();
9119
9229
  }
9230
+ /** One saved secret of the source (a handoff asks whether an LND card pins its certificate, and a Breez phrase), or undefined. */
9231
+ async secret(name) {
9232
+ const stored = this.stored;
9233
+ if (!stored?.secrets) return void 0;
9234
+ const value = JSON.parse(await unsealSeed(stored.secrets, stored.deviceKey ?? ""))[name];
9235
+ return typeof value === "string" && value ? value : void 0;
9236
+ }
9120
9237
  /** The list of providers changed (a plugin registered): the picker is told, nothing reconnects. */
9121
9238
  refreshOffered() {
9122
9239
  this.view = {
@@ -9214,6 +9331,7 @@ var ProviderSources = class {
9214
9331
  }
9215
9332
  /** Creates a provider and checks it is on a network of this mode before anything uses it. */
9216
9333
  async open(descriptor, settings, controller = this.controller) {
9334
+ if (descriptor.id === "breez") await requireTurn();
9217
9335
  const signal = controller.signal;
9218
9336
  const provider = await this.gate.within(abortable(descriptor.create(settings, {
9219
9337
  ...this.options.host(),
@@ -9384,7 +9502,7 @@ var ProviderSources = class {
9384
9502
  s[STORES.settings].delete(sourceKey(this.options.kind, this.mode, this.options.key));
9385
9503
  s[STORES.settings].delete(seenKey(this.options.kind, this.mode, this.options.key));
9386
9504
  });
9387
- await this.disconnect();
9505
+ await this.disconnect(true);
9388
9506
  this.stored = void 0;
9389
9507
  this.seen = void 0;
9390
9508
  this.failures = 0;
@@ -9407,7 +9525,7 @@ var ProviderSources = class {
9407
9525
  });
9408
9526
  this.stopped = true;
9409
9527
  this.gate.close();
9410
- await this.disconnect();
9528
+ await this.disconnect(true);
9411
9529
  this.stored = void 0;
9412
9530
  this.seen = void 0;
9413
9531
  this.view = {
@@ -9448,13 +9566,24 @@ var ProviderSources = class {
9448
9566
  async refresh() {
9449
9567
  clearTimeout(this.timer);
9450
9568
  const provider = this.provider;
9451
- if (!provider || !this.options.refresh) return;
9569
+ if (!provider) return;
9570
+ if (!this.options.refresh) {
9571
+ if (!this.view.read) {
9572
+ this.view = {
9573
+ ...this.view,
9574
+ read: true
9575
+ };
9576
+ this.options.changed();
9577
+ }
9578
+ return;
9579
+ }
9452
9580
  try {
9453
9581
  const details = await this.options.refresh(provider);
9454
9582
  if (provider !== this.provider) return;
9455
9583
  this.view = {
9456
9584
  ...this.view,
9457
9585
  ...details,
9586
+ read: true,
9458
9587
  balanceAt: void 0,
9459
9588
  error: void 0
9460
9589
  };
@@ -9486,7 +9615,8 @@ var ProviderSources = class {
9486
9615
  s[STORES.settings].put(seen, key);
9487
9616
  }).catch(() => {});
9488
9617
  }
9489
- async disconnect() {
9618
+ /** `forget`: the source is removed, so what it keeps on this device goes too (a provider's own `forget`). */
9619
+ async disconnect(forget = false) {
9490
9620
  clearTimeout(this.timer);
9491
9621
  clearTimeout(this.retry);
9492
9622
  this.controller.abort();
@@ -9495,7 +9625,7 @@ var ProviderSources = class {
9495
9625
  this.provider = void 0;
9496
9626
  this.descriptor = void 0;
9497
9627
  this.secrets = {};
9498
- await provider?.close().catch(() => {});
9628
+ await (forget && provider?.forget ? provider.forget() : provider?.close())?.catch(() => {});
9499
9629
  }
9500
9630
  /** Shutting down: nothing reconnects afterwards. */
9501
9631
  async stop() {
@@ -9510,6 +9640,9 @@ var PREFIX = "lightningOp-";
9510
9640
  var opKey = (direction, hash) => `${PREFIX}${direction}-${hash}`;
9511
9641
  var RANGE$1 = () => IDBKeyRange.bound(PREFIX, `${PREFIX}\uffff`);
9512
9642
  /** Every Lightning operation journaled, of every network and card. */
9643
+ /** Lightning payments whose call to the source has not returned, on any card of this page (WISP 06 § Payments in flight). */
9644
+ var paying = 0;
9645
+ var lightningPaying = () => paying;
9513
9646
  async function readLightningJournal() {
9514
9647
  return wrap((await store$1(STORES.settings, "readonly")).getAll(RANGE$1()));
9515
9648
  }
@@ -9748,6 +9881,7 @@ var LightningService = class {
9748
9881
  ref: quote.provider.paymentRef?.(quote.invoice)
9749
9882
  };
9750
9883
  await this.put(op);
9884
+ paying += 1;
9751
9885
  try {
9752
9886
  const result = await quote.provider.payInvoice(quote.invoice, quote.maxFee, context.note);
9753
9887
  op = result.state === "paid" ? {
@@ -9775,6 +9909,8 @@ var LightningService = class {
9775
9909
  state: "unknown",
9776
9910
  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."
9777
9911
  };
9912
+ } finally {
9913
+ paying -= 1;
9778
9914
  }
9779
9915
  await this.put(op);
9780
9916
  if (op.state !== "paid") this.schedule(OUT_POLL_MS);
@@ -9939,6 +10075,156 @@ var LightningService = class {
9939
10075
  }
9940
10076
  };
9941
10077
  //#endregion
10078
+ //#region ../browser/src/engine/paymentAdapters/providers/breezDatabases.ts
10079
+ var netOf = (network) => network === "bitcoin" || network === "mainnet" ? "mainnet" : "regtest";
10080
+ var hex$2 = (bytes) => Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
10081
+ var digest = (text) => hex$2(sha256(new TextEncoder().encode(text))).slice(0, 16);
10082
+ /** What a Breez database name looks like, old or new: nothing about the seed or the profile shows in it. */
10083
+ var BREEZ_DATABASE = /^ghostly-breez-(mainnet|regtest)-[0-9a-f]{16}$/;
10084
+ /** The name every profile of the origin used for this phrase before databases were per profile. */
10085
+ var breezLegacyName = (network, mnemonic) => `ghostly-breez-${netOf(network)}-${digest(`ghostly-breez:${netOf(network)}:${mnemonic}`)}`;
10086
+ /** The name of this phrase's Breez database in one profile (`space`: the profile's peer database, WISP 04). */
10087
+ var breezDatabaseName = (network, mnemonic, space = databaseName()) => `ghostly-breez-${netOf(network)}-${digest(`ghostly-breez:${space}:${netOf(network)}:${mnemonic}`)}`;
10088
+ /**
10089
+ * The databases the SDK keeps under one storage name. SDK 0.26 roots them there: `<name>/<network>/<identity>` and
10090
+ * `<name>/<network>/<identity>-tree`, the identity read from the seed inside the SDK. Older builds kept `<name>`
10091
+ * itself. A browser without `indexedDB.databases()` cannot list them: only the bare names are known there.
10092
+ */
10093
+ async function breezDatabaseFiles(name) {
10094
+ const files = [name, `${name}-tree`];
10095
+ if (typeof indexedDB.databases !== "function") return files;
10096
+ const under = (await indexedDB.databases()).map((d) => d.name ?? "").filter((file) => file.startsWith(`${name}/`));
10097
+ return [.../* @__PURE__ */ new Set([...files, ...under])];
10098
+ }
10099
+ var REGISTER = "ghostly-breez";
10100
+ var STORE = "databases";
10101
+ var request$1 = (r) => new Promise((resolve, reject) => {
10102
+ r.onsuccess = () => resolve(r.result);
10103
+ r.onerror = () => reject(r.error);
10104
+ });
10105
+ function openRegister() {
10106
+ return new Promise((resolve, reject) => {
10107
+ const open = indexedDB.open(REGISTER, 1);
10108
+ open.onupgradeneeded = () => {
10109
+ if (!open.result.objectStoreNames.contains(STORE)) open.result.createObjectStore(STORE);
10110
+ };
10111
+ open.onsuccess = () => resolve(open.result);
10112
+ open.onerror = () => reject(open.error);
10113
+ open.onblocked = () => reject(/* @__PURE__ */ new Error("The Breez register is blocked"));
10114
+ });
10115
+ }
10116
+ async function withRegister(mode, run) {
10117
+ const db = await openRegister();
10118
+ try {
10119
+ const tx = db.transaction(STORE, mode);
10120
+ const done = new Promise((resolve, reject) => {
10121
+ tx.oncomplete = () => resolve();
10122
+ tx.onerror = tx.onabort = () => reject(tx.error);
10123
+ });
10124
+ const result = await run(tx.objectStore(STORE));
10125
+ await done;
10126
+ return result;
10127
+ } finally {
10128
+ db.close();
10129
+ }
10130
+ }
10131
+ /**
10132
+ * Whether the SDK keeps a wallet under this storage name on this device (`breezDatabaseFiles`). A browser that cannot
10133
+ * list databases is answered yes: the first profile then takes the old name whether or not it is there, which is
10134
+ * safe, since only one profile ever has it.
10135
+ */
10136
+ async function exists(name) {
10137
+ if (typeof indexedDB.databases !== "function") return true;
10138
+ return (await indexedDB.databases()).some((d) => d.name === name || !!d.name?.startsWith(`${name}/`));
10139
+ }
10140
+ /**
10141
+ * The Breez database this profile opens for this phrase: the old shared one when this profile owns it (or is the
10142
+ * first to claim it), else the profile's own. Noted in the register either way. A device whose IndexedDB cannot be
10143
+ * read gets the profile's own name: never a database another profile may use.
10144
+ */
10145
+ async function breezDatabase(network, mnemonic, space = databaseName()) {
10146
+ const fresh = breezDatabaseName(network, mnemonic, space);
10147
+ if (typeof indexedDB === "undefined") return fresh;
10148
+ const legacy = breezLegacyName(network, mnemonic);
10149
+ try {
10150
+ const legacyHere = await exists(legacy);
10151
+ return await withRegister("readwrite", async (store) => {
10152
+ const [owner, own] = await Promise.all([request$1(store.get(legacy)), request$1(store.get(fresh))]);
10153
+ if (owner?.space === space) return legacy;
10154
+ if (!own && !owner && legacyHere) {
10155
+ store.put({
10156
+ name: legacy,
10157
+ space,
10158
+ network: netOf(network),
10159
+ legacy: true
10160
+ }, legacy);
10161
+ return legacy;
10162
+ }
10163
+ if (!own) store.put({
10164
+ name: fresh,
10165
+ space,
10166
+ network: netOf(network)
10167
+ }, fresh);
10168
+ return fresh;
10169
+ });
10170
+ } catch {
10171
+ return fresh;
10172
+ }
10173
+ }
10174
+ /** The names this profile has noted for this phrase (nothing is claimed): what removing the wallet deletes. */
10175
+ async function breezDatabasesFor(network, mnemonic, space = databaseName()) {
10176
+ const names = [breezLegacyName(network, mnemonic), breezDatabaseName(network, mnemonic, space)];
10177
+ return (await breezDatabasesOf(space)).filter((name) => names.includes(name));
10178
+ }
10179
+ /**
10180
+ * The name this profile uses for this phrase, read without claiming anything: the one it noted (`breezDatabase`), else
10181
+ * the profile's own name. What a handoff deletes and compares (WISP 06 § Wallets): never the old shared database of
10182
+ * another profile that has not opened the phrase since the change.
10183
+ */
10184
+ async function breezDatabaseInUse(network, mnemonic, space = databaseName()) {
10185
+ return (await breezDatabasesFor(network, mnemonic, space))[0] ?? breezDatabaseName(network, mnemonic, space);
10186
+ }
10187
+ /** Every Breez database name noted for a profile of this device. */
10188
+ async function breezDatabasesOf(space) {
10189
+ if (typeof indexedDB === "undefined") return [];
10190
+ try {
10191
+ return (await withRegister("readonly", (store) => request$1(store.getAll()))).filter((e) => e?.space === space && BREEZ_DATABASE.test(e.name)).map((e) => e.name);
10192
+ } catch {
10193
+ return [];
10194
+ }
10195
+ }
10196
+ var deleteDatabase = (name) => new Promise((resolve) => {
10197
+ try {
10198
+ const r = indexedDB.deleteDatabase(name);
10199
+ r.onsuccess = r.onerror = r.onblocked = () => resolve();
10200
+ } catch {
10201
+ resolve();
10202
+ }
10203
+ });
10204
+ var deleteStrictly = (name) => new Promise((resolve, reject) => {
10205
+ const r = indexedDB.deleteDatabase(name);
10206
+ r.onsuccess = () => resolve();
10207
+ r.onerror = () => reject(r.error ?? /* @__PURE__ */ new Error(`${name} was not deleted`));
10208
+ r.onblocked = () => reject(/* @__PURE__ */ new Error(`${name} is still open`));
10209
+ });
10210
+ /**
10211
+ * Deletes one Breez wallet's databases (all the SDK keeps under the name) and its note in the register. Anything else
10212
+ * is refused. `strict` (a handoff, WISP 06 § Wallets): a delete that is blocked or fails rejects and the note stays,
10213
+ * so the caller tries again.
10214
+ */
10215
+ async function dropBreezDatabase(name, options = {}) {
10216
+ if (!BREEZ_DATABASE.test(name) || typeof indexedDB === "undefined") return;
10217
+ for (const file of await breezDatabaseFiles(name).catch(() => [name, `${name}-tree`])) await (options.strict ? deleteStrictly(file) : deleteDatabase(file));
10218
+ await withRegister("readwrite", (store) => {
10219
+ store.delete(name);
10220
+ }).catch(() => {});
10221
+ }
10222
+ //#endregion
10223
+ //#region ../browser/src/engine/paymentAdapters/providers/recoveryPhrase.ts
10224
+ /** A phrase as BIP 39 wants it, however it was typed or pasted. */
10225
+ var normalizePhrase = (text = "") => text.trim().toLowerCase().split(/\s+/).join(" ");
10226
+ var isRecoveryPhrase = (text) => validateMnemonic(normalizePhrase(text), wordlist);
10227
+ //#endregion
9942
10228
  //#region ../browser/src/engine/paymentAdapters/providers/lightningCards.ts
9943
10229
  /** The card of Lightning through the Cashu mints: at most one per network, shown while the network has mints. */
9944
10230
  var CASHU_CARD = "cashu";
@@ -10275,14 +10561,37 @@ var LightningCards = class {
10275
10561
  list() {
10276
10562
  return readLightningJournal();
10277
10563
  }
10564
+ /**
10565
+ * What a handoff needs to know of the cards (WISP 06 § Wallets), from their sealed settings: the LND cards that pin a
10566
+ * certificate, and each Breez card's database on this device, by wallet id.
10567
+ */
10568
+ async handoffFacts() {
10569
+ const pinned = [], breez = /* @__PURE__ */ new Map();
10570
+ for (const [id, service] of this.services) {
10571
+ const wallet = `lightning:${this.network}:${id}`, providerId = service.view.providerId;
10572
+ if (providerId === "lnd" && await service.sources.secret("certificate")) pinned.push(wallet);
10573
+ if (providerId === "breez") {
10574
+ const mnemonic = await service.sources.secret("mnemonic");
10575
+ if (mnemonic) breez.set(wallet, await breezDatabaseInUse(this.network === "mainnet" ? "bitcoin" : "regtest", normalizePhrase(mnemonic)));
10576
+ }
10577
+ }
10578
+ return {
10579
+ pinned,
10580
+ breez
10581
+ };
10582
+ }
10583
+ /** The cards whose source is opened on this device: not one at home on another device (WISP 06 § Wallets that stay home). */
10584
+ here() {
10585
+ return [...this.services].filter(([id]) => awayFrom(`lightning:${this.network}:${id}`) === void 0).map(([, s]) => s);
10586
+ }
10278
10587
  async reconcile() {
10279
- await Promise.all(this.all().map((s) => s.reconcile()));
10588
+ await Promise.all(this.here().map((s) => s.reconcile()));
10280
10589
  }
10281
10590
  async recover() {
10282
- for (const s of this.all()) await s.recover();
10591
+ for (const s of this.here()) await s.recover();
10283
10592
  }
10284
10593
  async ensureReady() {
10285
- await Promise.all(this.all().map((s) => s.ensureReady()));
10594
+ await Promise.all(this.here().map((s) => s.ensureReady()));
10286
10595
  }
10287
10596
  refreshOffered() {
10288
10597
  for (const s of this.all()) s.refreshOffered();
@@ -16682,6 +16991,15 @@ function irohWasmBytes() {
16682
16991
  * settings and the Mainline DHT directly (read when the relays fail, written always).
16683
16992
  * The caller holds the profile's lock.
16684
16993
  */
16994
+ /** The files a light backup left out, as the restore wrote them in the mark (an empty mark: none). */
16995
+ function leftOutIn(mark) {
16996
+ try {
16997
+ const ids = JSON.parse(readFileSync(mark, "utf8") || "{}").leftOut;
16998
+ return Array.isArray(ids) ? ids.filter((id) => typeof id === "string") : [];
16999
+ } catch {
17000
+ return [];
17001
+ }
17002
+ }
16685
17003
  async function startRuntime(paths, options = {}) {
16686
17004
  const pinned = pkarrRelays();
16687
17005
  const trace = process.env.GHOSTLY_LINK_TRACE;
@@ -16690,7 +17008,9 @@ async function startRuntime(paths, options = {}) {
16690
17008
  const restored = join(paths.dir, RESTORED_MARK);
16691
17009
  if (existsSync(restored)) {
16692
17010
  try {
16693
- await (await import("./assets/restoredRows-tVxgyG23.js")).markRestoredWallet();
17011
+ const rows = await import("./assets/restoredRows-C1O9FfA4.js");
17012
+ await rows.markRestoredWallet();
17013
+ await rows.markLeftOutFiles(leftOutIn(restored));
16694
17014
  await store.compact();
16695
17015
  } catch (error) {
16696
17016
  await store.close().catch(() => {});
@@ -16706,20 +17026,21 @@ async function startRuntime(paths, options = {}) {
16706
17026
  if (testMints) (await Promise.resolve().then(() => mints_exports)).declareTestMints(testMints.split(","));
16707
17027
  await installEventSource();
16708
17028
  await preloadWalletModules();
16709
- (await import("./assets/ghostly_iroh_web-CPa_YEfb.js")).initSync({ module: irohWasmBytes() });
16710
- const [{ registerFileBytes }, { NodeFileBytes }] = await Promise.all([import("./assets/fileBytes-Br_MpeNT.js").then((n) => n.o), import("./assets/fileBytes-CpP9Vj7s.js")]);
17029
+ (await import("./assets/ghostly_iroh_web-Dvzh3Y6E.js")).initSync({ module: irohWasmBytes() });
17030
+ const [{ registerFileBytes }, { NodeFileBytes }] = await Promise.all([import("./assets/fileBytes-DxiJHmTk.js").then((n) => n.o), import("./assets/fileBytes-BQ84L9Fe.js")]);
16711
17031
  registerFileBytes("native", async () => new NodeFileBytes(paths.files), true);
16712
- const [{ EngineServer }, { createHyperEndpoint }] = await Promise.all([import("./assets/server-BnNXYzR9.js").then((n) => n.t), import("./assets/endpoint-CaSbSFDw.js")]);
17032
+ const [{ EngineServer }, { createHyperEndpoint }] = await Promise.all([import("./assets/server-vGoe9MET.js").then((n) => n.t), import("./assets/endpoint-CaSbSFDw.js")]);
16713
17033
  const network = hyperdhtNetwork();
16714
17034
  const dht = mainlineNetwork();
16715
- const mainline = dht.off ? null : new (await (import("./assets/mainline-CcO2f8jy.js"))).Mainline({
17035
+ const mainline = dht.off ? null : new (await (import("./assets/mainline-CKFpwXcB.js"))).Mainline({
16716
17036
  bootstrap: dht.bootstrap,
16717
17037
  host: dht.host
16718
17038
  });
16719
- const transport = mainline || pinned ? new (await (import("./assets/mainline-CcO2f8jy.js"))).RelaysAndDht(mainline, pinned ? { relays: pinned } : {}, !!pinned) : null;
17039
+ const transport = mainline || pinned ? new (await (import("./assets/mainline-CKFpwXcB.js"))).RelaysAndDht(mainline, pinned ? { relays: pinned } : {}, !!pinned) : null;
16720
17040
  const server = new EngineServer({
16721
17041
  ...transport ? { transport } : {},
16722
17042
  irohWeb: true,
17043
+ singleDevice: true,
16723
17044
  nativeTransports: { "hyperdht/1": (seedB64) => createHyperEndpoint(fromBase64Url$1(seedB64), network) },
16724
17045
  automaticWallets: false,
16725
17046
  fedimintSdk: nodeFedimintSdk(join(paths.dir, "fedimint")),
@@ -17296,12 +17617,13 @@ var SPECIAL = [
17296
17617
  }
17297
17618
  ],
17298
17619
  [
17299
- "profile backup --out <file> [--passphrase-file f | --no-passphrase]",
17620
+ "profile backup --out <file> [--passphrase-file f | --no-passphrase] [--light]",
17300
17621
  "A backup of the profile, encrypted with a passphrase (from a file or GHOSTLY_BACKUP_PASSPHRASE); --no-passphrase makes one anyone can read",
17301
17622
  {
17302
17623
  out: o("string", "The backup file to write"),
17303
17624
  "passphrase-file": o("string", "The passphrase, from this file (else GHOSTLY_BACKUP_PASSPHRASE)"),
17304
- passphrase: o("boolean", "--no-passphrase: do not encrypt. The file then holds the profile's keys and wallet secrets in the clear")
17625
+ passphrase: o("boolean", "--no-passphrase: do not encrypt. The file then holds the profile's keys and wallet secrets in the clear"),
17626
+ light: o("boolean", "Leave out files over 1 MB (voice messages over 4 MB); their messages stay")
17305
17627
  }
17306
17628
  ],
17307
17629
  [
@@ -17475,6 +17797,10 @@ async function profileCommand(sub, argv) {
17475
17797
  passphrase: {
17476
17798
  type: "boolean",
17477
17799
  description: ""
17800
+ },
17801
+ light: {
17802
+ type: "boolean",
17803
+ description: ""
17478
17804
  }
17479
17805
  },
17480
17806
  restore: {
@@ -17552,16 +17878,19 @@ async function profileCommand(sub, argv) {
17552
17878
  }
17553
17879
  case "backup": {
17554
17880
  const out = parsed.options.out;
17555
- if (typeof out !== "string") throw new CliError("usage", "ghostly profile backup --out <file> [--passphrase-file f | --no-passphrase] (or GHOSTLY_BACKUP_PASSPHRASE)");
17881
+ if (typeof out !== "string") throw new CliError("usage", "ghostly profile backup --out <file> [--passphrase-file f | --no-passphrase] [--light] (or GHOSTLY_BACKUP_PASSPHRASE)");
17556
17882
  const unsealed = parsed.options.passphrase === false;
17557
17883
  if (parsed.options.passphrase === true) throw new CliError("usage", "The passphrase comes from --passphrase-file or GHOSTLY_BACKUP_PASSPHRASE, never the command line");
17558
17884
  if (unsealed && (parsed.options["passphrase-file"] !== void 0 || process.env.GHOSTLY_BACKUP_PASSPHRASE)) throw new CliError("usage", "A passphrase is set (--passphrase-file or GHOSTLY_BACKUP_PASSPHRASE) and --no-passphrase was given: choose one");
17885
+ const light = parsed.options.light === true ? { light: true } : {};
17559
17886
  const made = await withSession(g, (s) => s.call("profile.backup", unsealed ? {
17560
17887
  path: resolve(out),
17561
- noPassphrase: true
17888
+ noPassphrase: true,
17889
+ ...light
17562
17890
  } : {
17563
17891
  path: resolve(out),
17564
- passphrase: backupPassphrase(parsed.options["passphrase-file"])
17892
+ passphrase: backupPassphrase(parsed.options["passphrase-file"]),
17893
+ ...light
17565
17894
  }));
17566
17895
  if (unsealed) process.stderr.write("Not encrypted: this file holds the profile's keys, chats and wallet secrets in the clear. Anyone who gets it gets everything in it, including any money in its wallets.\n");
17567
17896
  print(made);
@@ -18279,6 +18608,6 @@ async function run(argv) {
18279
18608
  //#region src/bin.ts
18280
18609
  run(process.argv.slice(2));
18281
18610
  //#endregion
18282
- export { boundedIdentityFetch as $, TEST_MINT as A, registeredLightningProviders as B, networkLabel as C, WALLET_TYPES as D, MESSAGE_DETAILS_MAX_BYTES as E, paysItsOwnInvoices as F, SourceConfigError as G, reserveAdapterIds as H, BACKUP_MEDIA_TYPE as I, networkMode as J, SourceUnreachableError as K, MAX_BACKUP_BYTES as L, isTestMint as M, isWorthlessMint as N, BITCOIN_INVOICE_ON_TESTNET as O, mintNetwork as P, setOwnDidSource as Q, identityProviders as R, WrongNetworkError as S, cashuMint as T, NothingSpentError as U, registeredOnchainProviders as V, PROVIDER_PLATFORMS as W, assertPublicServer as X, redact as Y, resolveAtprotoDid as Z, newDeviceKey as _, createFailure as a, replyRef as at, ModeChanged as b, paymentNetwork as c, paymentWireId as ct, networkReason as d, removeStored as dt, verifyIdentity as et, rpcHost as f, storedSize as ft, intentRepository as g, ProviderSources as h, assertConfirmedReal as i, receivedPairedReply as it, fakesLightning as j, DEFAULT_MINTS as k, paymentNetworksOf as l, FileAppender as lt, LightningCards as m, loadFedimintSdk as n, groupReply as nt, createTiming as o, replyTo as ot, CASHU_CARD as p, streamStored as pt, isNothingSpentError as q, WALLET_NAMES as r, pairedWireReply as rt, crossNetwork as s, paymentAlias as st, fedimintDatabase as t, chosenResolver as tt, walletInstances as u, readStored as ut, sealSeed as v, CASHU_MINT_SOURCE as w, ModeGate as x, unsealSeed as y, onAdaptersChanged as z };
18611
+ export { onAdaptersChanged as $, intentRepository as A, MESSAGE_DETAILS_MAX_BYTES as B, ProviderSources as C, FileAppender as Ct, requireTurn as D, streamStored as Dt, refuseAway as E, storedSize as Et, ModeGate as F, fakesLightning as G, BITCOIN_INVOICE_ON_TESTNET as H, WrongNetworkError as I, mintNetwork as J, isTestMint as K, networkLabel as L, sealSeed as M, unsealSeed as N, setAwayWallets as O, ModeChanged as P, identityProviders as Q, CASHU_MINT_SOURCE as R, lightningPaying as S, paymentWireId as St, awayFrom as T, removeStored as Tt, DEFAULT_MINTS as U, WALLET_TYPES as V, TEST_MINT as W, BACKUP_MEDIA_TYPE as X, paysItsOwnInvoices as Y, MAX_BACKUP_BYTES as Z, normalizePhrase as _, pairedWireReply as _t, createFailure as a, SourceConfigError as at, breezDatabasesFor as b, replyTo as bt, paymentNetwork as c, networkMode as ct, networkReason as d, resolveAtprotoDid as dt, registeredLightningProviders as et, rpcHost as f, setOwnDidSource as ft, isRecoveryPhrase as g, groupReply as gt, cardsKey as h, chosenResolver as ht, assertConfirmedReal as i, PROVIDER_PLATFORMS as it, newDeviceKey as j, setSingleWriterGate as k, paymentNetworksOf as l, redact as lt, LightningCards as m, verifyIdentity as mt, loadFedimintSdk as n, reserveAdapterIds as nt, createTiming as o, SourceUnreachableError as ot, CASHU_CARD as p, boundedIdentityFetch as pt, isWorthlessMint as q, WALLET_NAMES as r, NothingSpentError as rt, crossNetwork as s, isNothingSpentError as st, fedimintDatabase as t, registeredOnchainProviders as tt, walletInstances as u, assertPublicServer as ut, breezDatabase as v, receivedPairedReply as vt, sourceKey as w, readStored as wt, dropBreezDatabase as x, paymentAlias as xt, breezDatabaseInUse as y, replyRef as yt, cashuMint as z };
18283
18612
 
18284
18613
  //# sourceMappingURL=ghostly.mjs.map