@ghostlytools/cli 1.1.3 → 1.1.5

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 (46) hide show
  1. package/README.md +7 -2
  2. package/dist/assets/argon2id-088uQfnH.js.map +1 -1
  3. package/dist/assets/bark_ffi_wasm_bg-EY2uc0Bx.js.map +1 -1
  4. package/dist/assets/bitcoindevkit_bg-B-iLWrmX.js.map +1 -1
  5. package/dist/assets/breez_sdk_spark_wasm_bg.wasm +0 -0
  6. package/dist/assets/fedimint_client_wasm_bg-BtHHlFpp.js.map +1 -1
  7. package/dist/assets/{fileBytes-DxiJHmTk.js → fileBytes-3AQ2tJzW.js} +5 -17
  8. package/dist/assets/{fileBytes-DxiJHmTk.js.map → fileBytes-3AQ2tJzW.js.map} +1 -1
  9. package/dist/assets/{fileBytes-BQ84L9Fe.js → fileBytes-CAZDydIe.js} +2 -2
  10. package/dist/assets/{fileBytes-BQ84L9Fe.js.map → fileBytes-CAZDydIe.js.map} +1 -1
  11. package/dist/assets/{fileBytesIdb-DQkQ10PS.js → fileBytesIdb-CmG00qE-.js} +3 -3
  12. package/dist/assets/{fileBytesIdb-DQkQ10PS.js.map → fileBytesIdb-CmG00qE-.js.map} +1 -1
  13. package/dist/assets/{fileBytesOpfs-BFfR_68b.js → fileBytesOpfs-CTgqt-p3.js} +3 -3
  14. package/dist/assets/{fileBytesOpfs-BFfR_68b.js.map → fileBytesOpfs-CTgqt-p3.js.map} +1 -1
  15. package/dist/assets/{hyperdhtRelay-CHMS7_KO.js → hyperdhtRelay-w9cwVqGZ.js} +3 -3
  16. package/dist/assets/{hyperdhtRelay-CHMS7_KO.js.map → hyperdhtRelay-w9cwVqGZ.js.map} +1 -1
  17. package/dist/assets/{idb-Co2lObpU.js → idb-BIdus0WX.js} +32 -8
  18. package/dist/assets/idb-BIdus0WX.js.map +1 -0
  19. package/dist/assets/joinTrace-CO8mlhNO.js +29 -0
  20. package/dist/assets/joinTrace-CO8mlhNO.js.map +1 -0
  21. package/dist/assets/legacy_ciphers-JJOu0JXL.js.map +1 -1
  22. package/dist/assets/{mainline-B0UTU0-4.js → mainline-BabMhm8L.js} +2 -2
  23. package/dist/assets/{mainline-B0UTU0-4.js.map → mainline-BabMhm8L.js.map} +1 -1
  24. package/dist/assets/nacl-fast-CDhNCYHs.js.map +1 -1
  25. package/dist/assets/noble_curves-DDWn5AH0.js.map +1 -1
  26. package/dist/assets/noble_hashes-B7AZyFyS.js.map +1 -1
  27. package/dist/assets/openpgp-BBFr-zHQ.js.map +1 -1
  28. package/dist/assets/{restoredRows-C1O9FfA4.js → restoredRows-lhTKGmCO.js} +2 -2
  29. package/dist/assets/{restoredRows-C1O9FfA4.js.map → restoredRows-lhTKGmCO.js.map} +1 -1
  30. package/dist/assets/rolldown-runtime-8H4AJuhK.js +14 -0
  31. package/dist/assets/{server-9AM0ta6q.js → server-TamKsfmC.js} +1601 -262
  32. package/dist/assets/server-TamKsfmC.js.map +1 -0
  33. package/dist/assets/sha512-BYId-G-T.js.map +1 -1
  34. package/dist/assets/{src-C_E99PGx.js → src-C6qQYrAH.js} +2190 -207
  35. package/dist/assets/src-C6qQYrAH.js.map +1 -0
  36. package/dist/assets/storage-Bya5b2Xt.js.map +1 -1
  37. package/dist/assets/tree-store-N362nuzZ.js.map +1 -1
  38. package/dist/assets/unbzip2-stream-B1C-JX9L.js.map +1 -1
  39. package/dist/assets/{web-OUQl4Q7L.js → web-DwgdhfcY.js} +16 -16
  40. package/dist/assets/{web-OUQl4Q7L.js.map → web-DwgdhfcY.js.map} +1 -1
  41. package/dist/ghostly.mjs +905 -29
  42. package/dist/ghostly.mjs.map +1 -1
  43. package/package.json +7 -7
  44. package/dist/assets/idb-Co2lObpU.js.map +0 -1
  45. package/dist/assets/server-9AM0ta6q.js.map +0 -1
  46. package/dist/assets/src-C_E99PGx.js.map +0 -1
package/dist/ghostly.mjs CHANGED
@@ -1,7 +1,8 @@
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-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-C_E99PGx.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";
2
+ import { t as __exportAll } from "./assets/rolldown-runtime-8H4AJuhK.js";
3
+ import { $ as parseSshSignature, Aa as APP_STATEMENT_LIMITS, Ac as toZ32, Ai as irohDescriptor, An as normalizeDomain, Ar as TYPING_REFRESH_MS, At as didWebFile, Ba as canonicalJsonBytes, Bi as sanitizeFileName, Bn as decodeBolt11, Bt as strictBase64Url, Cn as decodeDnsResponse, Ct as DidError, Dc as fromBase64Url$1, Dn as encodeDnsQuery, Dt as didJwsAlg, En as domainWellKnownFile, Et as didGhostlyServiceEntry, Fn as wellKnownUrl, Ft as parseDid, Gt as resolveDidDht, Hi as PLAYABLE_AUDIO, Hs as seedSigner, Ia as readAppRevocations, In as requestLnurlInvoice, It as parseDidDocument, J as groupName, Jr as checkPushEndpoint, Ki as parseVoiceMeta, La as signAppObject, Ln as resolveLightningDestination, Lt as parseDidSignaturePaste, Ma as appRef, Mc as utf8Encode, Mn as recordsFromWellKnown, Mr as sanitizeTypingStatus, Mt as didWebFileUrl, Na as checkAppRevokeStatement, Nn as recordsName, Nt as didWebHost, Oc as randomBytes$1, On as isPublicAddress, Ot as didSignedMessages, Pn as txtValue, Pt as normalizeDid, Q as parseSshPublicKey, Qr as BUTTONS_CAPABILITY, Ra as signAppRevocation, Ri as readImageMeta, Rn as engineError, Rt as signingKeys, Sn as DOMAIN_FILE_MAX_BYTES, Tc as sanitizeAvatar, Tn as domainTxtRecord, Tt as checkDidSignature, Ui as VOICE_LIMITS, Vi as sanitizeMime, Wi as baseMime, X as SSHSIG_NAMESPACE, Z as SshSigError, Za as GhostlyHttpError, _a as ITEM_STATES, _c as openRelayPayload, _i as boundVerifiedIdentity, _n as pubkyProofCapability, _t as shortAtprotoDid, a as checkAppStoreIndex, ao as RTC_CONFIG, at as decodeAnyBitcoinAddress, ba as buttonLabelClash, bi as identityStatement, bn as homeserverWebEndpoint, bt as DID_DOCUMENT_MAX_BYTES$1, c as APP_BUNDLE_LIMITS, cc as setLinkTraceSink, co as callRtcConfig, ct as ATPROTO_PROOF_COLLECTION, d as assembleAppBundle, do as parseCallSignal, dt as atprotoProofRecord, et as sameSshKey, f as checkAppManifest, fn as PUBKY_PROOF_MAX_BYTES, fo as signalHasVideo, ft as atprotoProofRkey, gn as normalizePubkyKey, gt as parseAtprotoDidDocument, ha as replySnippet, hi as IdentityCheckUnavailable, hn as newPubkyProofFolder, ht as normalizeAtprotoHandle, io as CALL_SIGNAL_MAX_AGE_MS, it as BitcoinAddressError, ja as appFingerprint, jc as utf8Decode, ji as irohRelayUrl, jn as recordsFromTxt, js as inviteLink, jt as didWebFileNames, ka as APP_PREFIXES, kc as toBase64Url$1, kn as nip05RootKey, kr as TYPING_KINDS, kt as didWebDocumentUrl, lo as callSignalHeardAt, lt as ATPROTO_PROOF_SCOPE, m as readAppBundle, mc as RELAY_PAYLOAD_MAX_BYTES, mn as isPubkyProofFolder, mt as isAtprotoDid, nt as verifySshSignature, o as readAppStore, on as walletNetworkOf, oo as answersOffer, ot as ATPROTO_DECLARED_SCOPE, p as isAppUrl, pn as isPubkyKey, pt as checkAtprotoProofRecord, qi as voicePeaksOf, rt as verifyBitcoinMessage, s as signAppStore, so as buildSdpFromSignal, st as ATPROTO_FULL_SCOPE, t as appStoreDecision, tn as WALLET_NETWORKS, to as LIMITS, tt as sshSignCommand, uo as extractParamsFromSdp, ut as atprotoDidMethod, va as RUN_RESULTS, vn as pubkyProofFile, vt as AtprotoProofError, wc as MAX_AVATAR_LENGTH, wn as dnsProofName, wt as checkDidJws, xa as checkStatusCard, xn as pubkyHomeserverOf, xt as DID_FILE_MAX_BYTES, ya as STATUS_CARD_LIMITS, yn as pubkyProofPath, yt as verifyAtprotoRecordProof, za as canonicalJson, zn as engineText, zt as staticDidDocument } from "./assets/src-C6qQYrAH.js";
4
+ import { c as fileStore, d as store$1, f as transact, i as STORES, l as openDb, o as databaseName, p as wrap } from "./assets/idb-BIdus0WX.js";
5
+ import { a as fileBytesOf, i as fileBytes, l as FILE_BYTES_STEP } from "./assets/fileBytes-3AQ2tJzW.js";
5
6
  import { createRequire } from "node:module";
6
7
  import { spawn } from "node:child_process";
7
8
  import { connect, createServer, isIP } from "node:net";
@@ -17,7 +18,7 @@ import { decode, npubEncode } from "nostr-tools/nip19";
17
18
  import { BunkerSigner } from "nostr-tools/nip46";
18
19
  import { SimplePool } from "nostr-tools/pool";
19
20
  import { generateSecretKey } from "nostr-tools/pure";
20
- import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
21
+ import { createHash, createPrivateKey, generateKeyPairSync, randomBytes, timingSafeEqual } from "node:crypto";
21
22
  import { createServer as createServer$1 } from "node:http";
22
23
  import { Readable } from "node:stream";
23
24
  import { pipeline } from "node:stream/promises";
@@ -34,7 +35,7 @@ import { IDBFactory, IDBKeyRange as IDBKeyRange$1, IDBTransaction } from "fake-i
34
35
  import { format } from "node:util";
35
36
  var package_default = {
36
37
  name: "@ghostlytools/cli",
37
- version: "1.1.3",
38
+ version: "1.1.5",
38
39
  type: "module",
39
40
  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
41
  license: "MIT",
@@ -60,9 +61,9 @@ var package_default = {
60
61
  "test": "vitest run"
61
62
  },
62
63
  dependencies: {
63
- "@arkade-os/sdk": "0.4.76",
64
+ "@arkade-os/sdk": "0.4.78",
64
65
  "@atproto/jwk-webcrypto": "^0.3.4",
65
- "@atproto/oauth-client": "^0.8.7",
66
+ "@atproto/oauth-client": "^0.8.8",
66
67
  "@bitcoindevkit/bdk-wallet-web": "0.3.0",
67
68
  "@bitcoinerlab/descriptors-scure": "^3.2.0",
68
69
  "@cashu/cashu-ts": "^4.11.0",
@@ -80,7 +81,7 @@ var package_default = {
80
81
  "@secondts/bark": "0.25.0",
81
82
  "@serenity-kit/opaque": "1.1.0",
82
83
  "@synonymdev/pubky": "0.14.0",
83
- "@tetherto/wdk-wallet-evm": "1.0.0-beta.19",
84
+ "@tetherto/wdk-wallet-evm": "1.0.0-beta.20",
84
85
  "b4a": "1.9.0",
85
86
  "bittorrent-dht": "11.0.12",
86
87
  "ethers": "6.17.0",
@@ -98,10 +99,10 @@ var package_default = {
98
99
  "@ghostly/browser": "*",
99
100
  "@ghostly/core": "*",
100
101
  "@ghostly/iroh-web": "*",
101
- "@types/node": "^26.6.3",
102
+ "@types/node": "^26.6.4",
102
103
  "typescript": "^5.9.3",
103
- "vite": "^8.3.1",
104
- "vitest": "^5.0.2"
104
+ "vite": "^8.3.2",
105
+ "vitest": "^5.0.3"
105
106
  }
106
107
  };
107
108
  //#endregion
@@ -339,16 +340,29 @@ function findSecret(text) {
339
340
  * 12 to 24 words in a row from the English BIP39 list whose checksum holds, anywhere in the text; or a message that
340
341
  * is nothing but 12, 15, 18, 21 or 24 of those words (a seed with one word mistyped is still most of a seed). Prose
341
342
  * breaks a run at once: "the", "a", "is", "to", "and", "of" and "I" are not on the list.
343
+ *
344
+ * One word said over and over is not a seed, though one in 16 such runs passes a 12-word checksum ("word" does): a
345
+ * window of a single word never counts, and a window mostly of one word ("abandon … about") counts only when that
346
+ * word does not go on past it on either side.
342
347
  */
343
348
  function hasMnemonic(text) {
344
349
  const tokens = text.toLowerCase().match(/[a-z]+/g);
345
350
  if (!tokens || tokens.length < 12) return false;
346
351
  let budget = CHECKSUM_BUDGET;
347
352
  let run = [];
353
+ const seedLike = (start, length) => {
354
+ const window = run.slice(start, start + length);
355
+ const counts = /* @__PURE__ */ new Map();
356
+ for (const word of window) counts.set(word, (counts.get(word) ?? 0) + 1);
357
+ if (counts.size === 1) return false;
358
+ const [most, times] = [...counts].reduce((a, b) => b[1] > a[1] ? b : a);
359
+ if (times > length / 2 && (run[start - 1] === most || run[start + length] === most)) return false;
360
+ return checksumHolds(window);
361
+ };
348
362
  const check = () => {
349
363
  if (run.length >= 12) {
350
- for (const length of MNEMONIC_LENGTHS) for (let start = 0; start + length <= run.length && budget > 0; start++, budget--) if (checksumHolds(run.slice(start, start + length))) return true;
351
- if (run.length === tokens.length && MNEMONIC_LENGTHS.includes(run.length)) return true;
364
+ for (const length of MNEMONIC_LENGTHS) for (let start = 0; start + length <= run.length && budget > 0; start++, budget--) if (seedLike(start, length)) return true;
365
+ if (run.length === tokens.length && MNEMONIC_LENGTHS.includes(run.length) && new Set(run).size > 1) return true;
352
366
  }
353
367
  run = [];
354
368
  return false;
@@ -582,6 +596,10 @@ var ENGINE_METHODS = [
582
596
  "retryMessage",
583
597
  "react",
584
598
  "pinMessage",
599
+ "appId",
600
+ "appOpen",
601
+ "appClose",
602
+ "appSend",
585
603
  "messageDetails",
586
604
  "deleteMessage",
587
605
  "exportLinks",
@@ -645,6 +663,7 @@ var ENGINE_METHODS = [
645
663
  "setWakeSubscription",
646
664
  "setWakeMuted",
647
665
  "wakeForCall",
666
+ "setCallOn",
648
667
  "wakeConfirm",
649
668
  "setFastPoll",
650
669
  "createGroup",
@@ -675,6 +694,7 @@ var ENGINE_METHODS = [
675
694
  "deviceEnrollCancel",
676
695
  "deviceEnrollView",
677
696
  "deviceEnrollJoin",
697
+ "deviceEnrollReady",
678
698
  "deviceEnrollFinish",
679
699
  "deviceEnrollRemove",
680
700
  "deviceSet",
@@ -698,7 +718,27 @@ var ENGINE_METHODS = [
698
718
  "deviceSetNoticeSeen",
699
719
  "deviceTurnCheck",
700
720
  "devicePushState",
701
- "devicePushSet"
721
+ "devicePushSet",
722
+ "appList",
723
+ "appStoreList",
724
+ "appStorePreview",
725
+ "appStoreAdd",
726
+ "appStoreRemove",
727
+ "appStoreRefresh",
728
+ "appPreview",
729
+ "appInstall",
730
+ "appUpdateAccept",
731
+ "appCheckUpdates",
732
+ "appUninstall",
733
+ "appRunCheck",
734
+ "appFetchFiles",
735
+ "appEntry",
736
+ "appFile",
737
+ "appStorageGet",
738
+ "appStorageSet",
739
+ "appStorageDelete",
740
+ "appStorageKeys",
741
+ "appDataExport"
702
742
  ];
703
743
  /** Reads the hosts use outside `EngineApi`: the whole state, and one chat's stored messages. */
704
744
  var ENGINE_READS = ["getState", "getMessages"];
@@ -6233,7 +6273,7 @@ function openPassportWindow(url) {
6233
6273
  return window.open(url, "pubky-passport", "popup,width=520,height=760");
6234
6274
  }
6235
6275
  /** A 401 or 403 from the homeserver: the session it was given is not one it accepts. */
6236
- var refused = (e) => {
6276
+ var refused$1 = (e) => {
6237
6277
  const status = e?.data?.statusCode;
6238
6278
  return status === 401 || status === 403;
6239
6279
  };
@@ -6464,7 +6504,7 @@ async function approveAndWork(options, work, { grant, cookie, opened, settled, c
6464
6504
  throw e;
6465
6505
  }
6466
6506
  const fail = (what) => (e) => {
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.");
6507
+ if (viaCookie && !cookieKept && refused$1(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.");
6468
6508
  throw failure(what, e);
6469
6509
  };
6470
6510
  const approved = session;
@@ -8474,7 +8514,7 @@ async function restoreProfile(home, name, file, passphrase) {
8474
8514
  if (!reader) {
8475
8515
  const payload = await readWhole(file, passphrase ?? "");
8476
8516
  work = await restoringFolder(home, name);
8477
- await writeWhole(work, payload);
8517
+ await writeWhole$1(work, payload);
8478
8518
  return await finishRestore(home, name, work);
8479
8519
  }
8480
8520
  const next = async () => {
@@ -8567,7 +8607,7 @@ async function readWhole(file, passphrase) {
8567
8607
  if (payload?.format !== FORMAT_1 || typeof payload.store !== "string" || !Array.isArray(payload.files)) throw new CliError("refused", "This backup is not a headless Ghostly profile");
8568
8608
  return payload;
8569
8609
  }
8570
- async function writeWhole(paths, payload) {
8610
+ async function writeWhole$1(paths, payload) {
8571
8611
  await mkdir(paths.db, {
8572
8612
  recursive: true,
8573
8613
  mode: 448
@@ -12191,6 +12231,565 @@ function connectDaemon(path, timeoutMs = 2e3) {
12191
12231
  });
12192
12232
  }
12193
12233
  //#endregion
12234
+ //#region src/apps.ts
12235
+ var APP_SOURCE = "ghostly-app.json";
12236
+ var APP_REVOKE_FILE = "ghostly-revoke.json";
12237
+ var STORE_INDEX_FILE = "ghostly-store.json";
12238
+ var STORE_SIG_FILE = "ghostly-store.sig";
12239
+ /** How long `app verify` waits for a URL, and how many redirects it follows (each checked as the first). */
12240
+ var FETCH_TIMEOUT_MS = 6e4;
12241
+ var MAX_REDIRECTS = 5;
12242
+ var KEY_HEADER = {
12243
+ publisher: "Ghostly publisher key (WISP 1200). Secret: never share or commit it. Back it up: an app signed with it is updated only with it.",
12244
+ store: "Ghostly store key (WISP 1200). Secret: keep it offline, never share or commit it. Back it up: a store is its key."
12245
+ };
12246
+ var PEM = /-----BEGIN PRIVATE KEY-----\r?\n([A-Za-z0-9+/=\r\n]+)-----END PRIVATE KEY-----/;
12247
+ /** Refuses a key file that is not a plain file of this user's, or that anyone else may read or write. */
12248
+ function checkKeyFile(path) {
12249
+ const info = statSync(path);
12250
+ if (!info.isFile()) throw new CliError("bad_request", `${path} is not a file: --key names a key file`);
12251
+ if (process.platform === "win32") return;
12252
+ if (process.getuid && info.uid !== process.getuid()) throw new CliError("refused", `${path} is not this user's: a key file must be yours alone`);
12253
+ if ((info.mode & 63) !== 0) throw new CliError("refused", `${path} may be read by others (mode ${(info.mode & 511).toString(8)}): run chmod 600 ${path}, and think whether the key was seen`);
12254
+ }
12255
+ /** Reads a key file: the PEM block, an Ed25519 key, and the kind its first line names (when it names one). */
12256
+ function readKey(path, kind) {
12257
+ checkKeyFile(path);
12258
+ const text = readFileSync(path, "utf8");
12259
+ const other = kind === "publisher" ? "store" : "publisher";
12260
+ if (text.startsWith(`Ghostly ${other} key`)) throw new CliError("refused", `${path} is a ${other} key: ${kind === "publisher" ? "a store key never signs an app" : "a publisher key never signs a store index"} (WISP 1200 · Stores)`);
12261
+ const block = PEM.exec(text);
12262
+ let seed;
12263
+ if (block) try {
12264
+ const key = createPrivateKey(block[0]);
12265
+ const d = key.asymmetricKeyType === "ed25519" ? key.export({ format: "jwk" }).d : void 0;
12266
+ if (d) seed = fromBase64Url$1(d);
12267
+ } catch {}
12268
+ if (!seed || seed.length !== 32) throw new CliError("bad_request", `${path} holds no Ed25519 private key (a PEM "PRIVATE KEY" block, as openssl genpkey -algorithm ed25519 writes)`);
12269
+ const signer = seedSigner(seed);
12270
+ return {
12271
+ signer,
12272
+ key: toZ32(signer.publicKey),
12273
+ created: false
12274
+ };
12275
+ }
12276
+ /** Makes a key file: owner-only from the start, never over a file that is there. */
12277
+ function makeKey(path, kind) {
12278
+ mkdirSync(dirname(path), {
12279
+ recursive: true,
12280
+ mode: 448
12281
+ });
12282
+ const { privateKey } = generateKeyPairSync("ed25519");
12283
+ const pem = privateKey.export({
12284
+ type: "pkcs8",
12285
+ format: "pem"
12286
+ });
12287
+ const fd = openSync(path, "wx", 384);
12288
+ try {
12289
+ writeSync(fd, `${KEY_HEADER[kind]}\n${pem}`);
12290
+ } finally {
12291
+ closeSync(fd);
12292
+ }
12293
+ return {
12294
+ ...readKey(path, kind),
12295
+ created: true
12296
+ };
12297
+ }
12298
+ /** What the person is told on stderr when a key was made: where, which, and to back it up. Never the key. */
12299
+ function madeKeyNote(path, kind, key) {
12300
+ return kind === "publisher" ? `new publisher key written to ${path}; back it up (${appFingerprint(key)}: an app is updated only with the key that signed it, and phase 1 has no recovery)` : `new store key written to ${path}; back it up and keep it offline (${appFingerprint(key)}: a store is its key)`;
12301
+ }
12302
+ var refused = (what, reading, extra = {}) => new CliError("refused", `${what} (${reading.reason}${reading.detail ? `: ${reading.detail}` : ""})`, {
12303
+ reason: reading.reason,
12304
+ ...reading.detail ? { detail: reading.detail } : {},
12305
+ ...extra
12306
+ });
12307
+ /** A bundle file's bytes: its size is checked before it is read, so a huge file is refused unread. */
12308
+ function readBundleFile(path) {
12309
+ const info = statSync(path);
12310
+ if (info.isDirectory()) throw new CliError("bad_request", `${path} is a folder: name a .ghostlyapp file`);
12311
+ if (info.size > APP_BUNDLE_LIMITS.bundleBytes) throw refused(`${path} is not a valid bundle`, {
12312
+ reason: "too-large",
12313
+ detail: `${info.size} bytes`
12314
+ });
12315
+ return new Uint8Array(readFileSync(path));
12316
+ }
12317
+ /**
12318
+ * A bundle at an https URL, as a client fetches one: `https:` only, no user or password, jsDelivr only at a full
12319
+ * commit (WISP 1200 · Manifest, "Every URL"); every redirect is held to the same rule, and the download stops past
12320
+ * 16 MiB.
12321
+ */
12322
+ async function fetchBundle(url) {
12323
+ let at = url;
12324
+ for (let hop = 0;; hop++) {
12325
+ if (!isAppUrl(at)) throw new CliError(hop ? "refused" : "usage", hop ? `${url} redirects to ${at}, which a client does not read (https only, no user or password, jsDelivr only at a full commit)` : `Not a URL a client reads: https only, at most ${APP_BUNDLE_LIMITS.url} characters, no user or password, and jsDelivr only as https://cdn.jsdelivr.net/gh/<owner>/<repo>@<40-hex commit>/<path>`);
12326
+ let response;
12327
+ try {
12328
+ response = await fetch(at, {
12329
+ redirect: "manual",
12330
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
12331
+ });
12332
+ } catch (error) {
12333
+ if (error instanceof Error && error.name === "TimeoutError") throw new CliError("timeout", `${at} did not answer in ${FETCH_TIMEOUT_MS / 1e3} s`);
12334
+ throw new CliError("unavailable", `Could not read ${at}: ${error instanceof Error ? error.message : String(error)}`);
12335
+ }
12336
+ if (response.status >= 300 && response.status < 400 && response.headers.get("location")) {
12337
+ await response.body?.cancel();
12338
+ if (hop >= MAX_REDIRECTS) throw new CliError("refused", `${url} redirects more than ${MAX_REDIRECTS} times`);
12339
+ at = new URL(response.headers.get("location"), at).href;
12340
+ continue;
12341
+ }
12342
+ if (response.status === 404 || response.status === 410) {
12343
+ await response.body?.cancel();
12344
+ throw new CliError("not_found", `${at} answered ${response.status}`);
12345
+ }
12346
+ if (!response.ok || !response.body) {
12347
+ await response.body?.cancel();
12348
+ throw new CliError("unavailable", `${at} answered ${response.status}`);
12349
+ }
12350
+ const limit = APP_BUNDLE_LIMITS.bundleBytes;
12351
+ const tooLarge = (bytes) => refused(`${url} is not a valid bundle`, {
12352
+ reason: "too-large",
12353
+ detail: `${bytes} bytes`
12354
+ });
12355
+ const length = Number(response.headers.get("content-length"));
12356
+ if (Number.isFinite(length) && length > limit) {
12357
+ await response.body.cancel();
12358
+ throw tooLarge(length);
12359
+ }
12360
+ const chunks = [];
12361
+ let total = 0;
12362
+ const reader = response.body.getReader();
12363
+ try {
12364
+ for (;;) {
12365
+ const { done, value } = await reader.read();
12366
+ if (done) break;
12367
+ total += value.length;
12368
+ if (total > limit) {
12369
+ await reader.cancel();
12370
+ throw tooLarge(`more than ${limit}`);
12371
+ }
12372
+ chunks.push(value);
12373
+ }
12374
+ } catch (error) {
12375
+ if (error instanceof CliError) throw error;
12376
+ if (error instanceof Error && error.name === "TimeoutError") throw new CliError("timeout", `${at} did not finish in ${FETCH_TIMEOUT_MS / 1e3} s`);
12377
+ throw new CliError("unavailable", `Could not read ${at}: ${error instanceof Error ? error.message : String(error)}`);
12378
+ }
12379
+ const out = new Uint8Array(total);
12380
+ let offset = 0;
12381
+ for (const chunk of chunks) {
12382
+ out.set(chunk, offset);
12383
+ offset += chunk.length;
12384
+ }
12385
+ return out;
12386
+ }
12387
+ }
12388
+ /** What a checked bundle is, as `app verify` and `app publish` print it. */
12389
+ function described(bundle, bytes) {
12390
+ const m = bundle.manifest;
12391
+ return {
12392
+ ref: appRef(m.publisher, m.name),
12393
+ publisher: m.publisher,
12394
+ fingerprint: appFingerprint(m.publisher),
12395
+ name: m.name,
12396
+ title: m.title,
12397
+ version: m.version,
12398
+ sequence: m.sequence,
12399
+ digest: bundle.digest,
12400
+ bytes: bytes.length,
12401
+ sha256: toBase64Url$1(createHash("sha256").update(bytes).digest())
12402
+ };
12403
+ }
12404
+ /** `app verify <bundle|url>`: every check a client makes before it stores a bundle (WISP 1200 · Reading a bundle). */
12405
+ async function verifyApp(source) {
12406
+ const bytes = /^[a-z][a-z0-9+.-]*:\/\//i.test(source) ? await fetchBundle(source) : readBundleFile(resolve(source));
12407
+ const read = readAppBundle(bytes);
12408
+ if (!read.ok) throw refused(`${source} is not a valid bundle`, read);
12409
+ return {
12410
+ valid: true,
12411
+ ...described(read.bundle, bytes),
12412
+ manifest: read.bundle.manifest
12413
+ };
12414
+ }
12415
+ /** Every file of the folder, as the bundle names it; dot files and folders are left out and listed. */
12416
+ function collectFiles(dir, leaveOut) {
12417
+ const files = [];
12418
+ const skipped = [];
12419
+ let bytes = 0;
12420
+ const walk = (folder) => {
12421
+ for (const name of readdirSync(folder).sort()) {
12422
+ const full = join(folder, name);
12423
+ const path = relative(dir, full).split(sep).join("/");
12424
+ if (leaveOut.has(full)) continue;
12425
+ if (name.startsWith(".")) {
12426
+ skipped.push(path);
12427
+ continue;
12428
+ }
12429
+ const info = lstatSync(full);
12430
+ if (info.isSymbolicLink()) throw new CliError("bad_request", `${path} is a link: a bundle holds the folder's own files (copy it in, or remove it)`);
12431
+ if (info.isDirectory()) {
12432
+ walk(full);
12433
+ continue;
12434
+ }
12435
+ if (!info.isFile()) throw new CliError("bad_request", `${path} is not a file`);
12436
+ files.push({
12437
+ path,
12438
+ full,
12439
+ size: info.size
12440
+ });
12441
+ bytes += info.size;
12442
+ if (files.length > APP_BUNDLE_LIMITS.files) throw refused(`${dir} holds more files than a bundle may`, {
12443
+ reason: "too-many-files",
12444
+ detail: `more than ${APP_BUNDLE_LIMITS.files}`
12445
+ });
12446
+ if (bytes > APP_BUNDLE_LIMITS.bundleBytes) throw refused(`${dir} holds more than a bundle may`, {
12447
+ reason: "too-large",
12448
+ detail: `more than ${APP_BUNDLE_LIMITS.bundleBytes} bytes of files`
12449
+ });
12450
+ }
12451
+ };
12452
+ walk(dir);
12453
+ return {
12454
+ files,
12455
+ skipped
12456
+ };
12457
+ }
12458
+ /** Fields of `ghostly-app.json` the command writes itself. */
12459
+ var WRITTEN = ["publisher", "files"];
12460
+ function readSource(path) {
12461
+ if (!existsSync(path)) throw new CliError("not_found", `No ${APP_SOURCE} in ${dirname(path)}: it holds the manifest's fields (name, version, title, tagline, entry, permissions, runtime, license, ...) but publisher, sequence and files, which publish writes`);
12462
+ let value;
12463
+ try {
12464
+ value = JSON.parse(readFileSync(path, "utf8"));
12465
+ } catch (error) {
12466
+ throw new CliError("bad_request", `${path} is not JSON: ${error instanceof Error ? error.message : String(error)}`);
12467
+ }
12468
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new CliError("bad_request", `${path} is not a JSON object`);
12469
+ const draft = value;
12470
+ const written = WRITTEN.filter((k) => k in draft);
12471
+ if (written.length) throw new CliError("bad_request", `${path} has ${written.join(" and ")}: publish writes ${written.length > 1 ? "them" : "it"} from the key and the folder's files`);
12472
+ if (draft.ghostlyApp !== void 0 && draft.ghostlyApp !== 1) throw refused(`${path} is not a manifest this release makes`, {
12473
+ reason: "unsupported-format",
12474
+ detail: "ghostlyApp is not 1"
12475
+ });
12476
+ if (draft.sequence !== void 0 && !(Number.isSafeInteger(draft.sequence) && draft.sequence >= 1)) throw new CliError("bad_request", `${path}: sequence is a whole number from 1 (or leave it out: publish raises it by itself)`);
12477
+ return draft;
12478
+ }
12479
+ /** Writes a file whole or not at all: a reader never sees half a bundle (and a dot file left by a crash is never bundled). */
12480
+ function writeWhole(path, bytes) {
12481
+ const tmp = join(dirname(path), `.${basename(path)}.${process.pid}.tmp`);
12482
+ writeFileSync(tmp, bytes);
12483
+ renameSync(tmp, path);
12484
+ }
12485
+ /**
12486
+ * `app publish <dir> --key <file> [--out <file>]`: `<dir>/ghostly-app.json` (the manifest without `publisher`,
12487
+ * `sequence` and `files`) and every other file in `<dir>` become one signed bundle, by default `<dir>/app.ghostlyapp`.
12488
+ * The sequence is `--sequence` (higher than the bundle already at `--out`'s), else one more than that bundle's (or
12489
+ * the source's `sequence`, if higher, else 1). The
12490
+ * key file is made when it is missing and no earlier bundle is there to update.
12491
+ */
12492
+ async function publishApp(dirArg, keyArg, outArg, sequenceArg) {
12493
+ if (sequenceArg !== void 0 && !(Number.isSafeInteger(sequenceArg) && sequenceArg >= 1)) throw new CliError("usage", `--sequence takes a whole number from 1, not ${sequenceArg}`);
12494
+ const dir = resolve(dirArg);
12495
+ if (!statSync(dir).isDirectory()) throw new CliError("bad_request", `${dir} is not a folder`);
12496
+ const keyPath = resolve(keyArg);
12497
+ const out = resolve(outArg ?? join(dir, "app.ghostlyapp"));
12498
+ const sourcePath = join(dir, APP_SOURCE);
12499
+ const draft = readSource(sourcePath);
12500
+ let previous = null;
12501
+ if (existsSync(out)) {
12502
+ const read = readAppBundle(readBundleFile(out));
12503
+ if (!read.ok) throw refused(`${out} is there and is not a valid bundle: move it away, or name another --out`, read);
12504
+ previous = read.bundle.manifest;
12505
+ }
12506
+ let key;
12507
+ if (existsSync(keyPath)) key = readKey(keyPath, "publisher");
12508
+ else if (previous) throw new CliError("not_found", `No key file ${keyPath}: ${out} is signed by ${appFingerprint(previous.publisher)}, and only that key publishes its next version`);
12509
+ else key = makeKey(keyPath, "publisher");
12510
+ if (previous && previous.publisher !== key.key) throw refused(`${out} is ${appRef(previous.publisher, previous.name)}, signed by another key (${appFingerprint(previous.publisher)}): a version of an app is signed by its own key`, { reason: "other-app" }, { publisher: previous.publisher });
12511
+ if (previous && previous.name !== draft.name) throw refused(`${out} is the app ${JSON.stringify(previous.name)}, not ${JSON.stringify(draft.name)}: name another --out for another app`, { reason: "other-app" });
12512
+ if (sequenceArg !== void 0 && previous && sequenceArg <= previous.sequence) throw refused(`--sequence ${sequenceArg} is not higher than the ${previous.sequence} of ${out}: every client refuses a lower one, and the same one with other content is equivocation`, { reason: sequenceArg < previous.sequence ? "rollback" : "equivocation" }, { previous: previous.sequence });
12513
+ const sequence = sequenceArg ?? Math.max(previous ? previous.sequence + 1 : 1, draft.sequence ?? 1);
12514
+ const { files, skipped } = collectFiles(dir, /* @__PURE__ */ new Set([
12515
+ sourcePath,
12516
+ out,
12517
+ join(dir, APP_REVOKE_FILE)
12518
+ ]));
12519
+ const contents = files.map((f) => ({
12520
+ path: f.path,
12521
+ bytes: new Uint8Array(readFileSync(f.full))
12522
+ }));
12523
+ const manifest = {
12524
+ ...draft,
12525
+ ghostlyApp: 1,
12526
+ publisher: key.key,
12527
+ sequence,
12528
+ files: contents.map((f) => ({
12529
+ path: f.path,
12530
+ size: f.bytes.length,
12531
+ sha256: toBase64Url$1(createHash("sha256").update(f.bytes).digest())
12532
+ }))
12533
+ };
12534
+ const checked = checkAppManifest(JSON.parse(JSON.stringify(manifest)));
12535
+ if (!checked.ok) throw refused(`${sourcePath} and the folder's files make no valid manifest`, checked);
12536
+ const { bytes: manifestBytes, signature } = await signAppObject(APP_PREFIXES.app, checked.manifest, key.signer);
12537
+ const bundle = assembleAppBundle(manifestBytes, canonicalJsonBytes(signature), contents.map((f) => f.bytes));
12538
+ const read = readAppBundle(bundle);
12539
+ if (!read.ok) throw refused("The bundle made is not valid", read);
12540
+ mkdirSync(dirname(out), { recursive: true });
12541
+ writeWhole(out, bundle);
12542
+ return {
12543
+ ...described(read.bundle, bundle),
12544
+ previous: previous?.sequence ?? null,
12545
+ out,
12546
+ files: contents.map((f) => f.path),
12547
+ ...skipped.length ? { skipped } : {},
12548
+ keyCreated: key.created,
12549
+ ...key.created ? { warning: madeKeyNote(keyPath, "publisher", key.key) } : {}
12550
+ };
12551
+ }
12552
+ /**
12553
+ * `app revoke <dir> --key <file> (--digest <digest>... | --up-to <sequence>) [--reason <text>] [--bundle <file>]`: a
12554
+ * `ghostly-revoke/1` statement (WISP 1200 · Publisher keys, Revocation) for the app whose bundle is `<dir>/app.ghostlyapp`
12555
+ * (or `--bundle`), signed by that app's publisher key and added to `<dir>/ghostly-revoke.json`, which stays canonical.
12556
+ * The key is never made here: only the key that signed the app may revoke its versions.
12557
+ */
12558
+ async function revokeApp(dirArg, keyArg, what) {
12559
+ const dir = resolve(dirArg);
12560
+ const bundlePath = resolve(what.bundle ?? join(dir, "app.ghostlyapp"));
12561
+ const listPath = join(dir, APP_REVOKE_FILE);
12562
+ const usage = "ghostly app revoke <dir> --key <file> (--digest <digest>... | --up-to <sequence>) [--reason <text>]";
12563
+ const digests = what.digests ?? [];
12564
+ if (digests.length > 0 === (what.upTo !== void 0)) throw new CliError("usage", `Give --digest (again for each) or --up-to, one of the two: ${usage}`);
12565
+ if (what.upTo !== void 0 && !(Number.isSafeInteger(what.upTo) && what.upTo >= 1)) throw new CliError("usage", `--up-to takes a sequence, a whole number from 1, not ${what.upTo}`);
12566
+ if (!existsSync(bundlePath)) throw new CliError("not_found", `No bundle ${bundlePath}: a revocation names the app of the bundle in <dir> (or --bundle)`);
12567
+ const read = readAppBundle(readBundleFile(bundlePath));
12568
+ if (!read.ok) throw refused(`${bundlePath} is not a valid bundle`, read);
12569
+ const manifest = read.bundle.manifest;
12570
+ const ref = appRef(manifest.publisher, manifest.name);
12571
+ const keyPath = resolve(keyArg);
12572
+ if (!existsSync(keyPath)) throw new CliError("not_found", `No key file ${keyPath}: only the key that signed ${ref} (${appFingerprint(manifest.publisher)}) revokes its versions`);
12573
+ const key = readKey(keyPath, "publisher");
12574
+ if (key.key !== manifest.publisher) throw refused(`${keyPath} is not the publisher key of ${ref} (${appFingerprint(manifest.publisher)}): a revocation by any other key is refused by every client`, { reason: "signature-key" });
12575
+ const statement = digests.length ? {
12576
+ ghostlyRevoke: 1,
12577
+ app: ref,
12578
+ digests
12579
+ } : {
12580
+ ghostlyRevoke: 1,
12581
+ app: ref,
12582
+ upTo: what.upTo
12583
+ };
12584
+ if (what.reason !== void 0) statement.reason = what.reason;
12585
+ const problem = checkAppRevokeStatement(statement);
12586
+ if (problem) throw refused("Not a revocation a client reads", {
12587
+ reason: "bad-revocation",
12588
+ detail: problem
12589
+ });
12590
+ let list = [];
12591
+ if (existsSync(listPath)) {
12592
+ const held = readAppRevocations(new Uint8Array(readFileSync(listPath)));
12593
+ if (!held.ok) throw refused(`${listPath} is there and is not a valid ghostly-revoke.json: fix or move it first`, held);
12594
+ list = held.revocations;
12595
+ }
12596
+ const signed = await signAppRevocation(statement, key.signer);
12597
+ const same = list.some((r) => canonicalJson(r.statement) === canonicalJson(signed.statement));
12598
+ if (!same) {
12599
+ if (list.length >= APP_STATEMENT_LIMITS.revocations) throw refused(`${listPath} holds ${list.length} revocations already`, {
12600
+ reason: "bad-revocation",
12601
+ detail: `at most ${APP_STATEMENT_LIMITS.revocations}`
12602
+ });
12603
+ list = [...list, signed];
12604
+ const bytes = canonicalJsonBytes(list);
12605
+ const check = readAppRevocations(bytes);
12606
+ if (!check.ok) throw refused("The revocation list made is not valid", check);
12607
+ writeWhole(listPath, bytes);
12608
+ }
12609
+ return {
12610
+ app: ref,
12611
+ ...digests.length ? {
12612
+ digests,
12613
+ current: digests.includes(read.bundle.digest)
12614
+ } : {
12615
+ upTo: what.upTo,
12616
+ current: manifest.sequence <= what.upTo
12617
+ },
12618
+ ...what.reason !== void 0 ? { reason: what.reason } : {},
12619
+ added: !same,
12620
+ revocations: list.length,
12621
+ file: listPath
12622
+ };
12623
+ }
12624
+ /**
12625
+ * `store sign <index> --key <file> [--out <dir>]`: a store index written by hand (any JSON layout; `key` may be left
12626
+ * out and is then the signing key's) becomes `ghostly-store.json`, canonical, and `ghostly-store.sig`, in `--out` or
12627
+ * beside the index. It is read back as a client reads it (`expires` at most 90 days ahead), and refused when the index
12628
+ * already there, signed by the same key, has a higher `sequence`, or the same one with other content.
12629
+ */
12630
+ async function signStore(indexArg, keyArg, outArg, now = Math.floor(Date.now() / 1e3)) {
12631
+ const indexPath = resolve(indexArg);
12632
+ const keyPath = resolve(keyArg);
12633
+ const outDir = resolve(outArg ?? dirname(indexPath));
12634
+ let value;
12635
+ try {
12636
+ value = JSON.parse(readFileSync(indexPath, "utf8"));
12637
+ } catch (error) {
12638
+ if (error instanceof SyntaxError) throw new CliError("bad_request", `${indexPath} is not JSON: ${error.message}`);
12639
+ throw error;
12640
+ }
12641
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new CliError("bad_request", `${indexPath} is not a JSON object`);
12642
+ const index = value;
12643
+ let key;
12644
+ if (existsSync(keyPath)) key = readKey(keyPath, "store");
12645
+ else if (index.key !== void 0) throw new CliError("not_found", `No key file ${keyPath}: the index names the store key ${typeof index.key === "string" ? appFingerprint(index.key) : JSON.stringify(index.key)}`);
12646
+ else key = makeKey(keyPath, "store");
12647
+ if (index.key !== void 0 && index.key !== key.key) throw refused(`${indexPath} names another store key than ${keyPath}'s (${appFingerprint(key.key)})`, { reason: "store-key" });
12648
+ index.key = key.key;
12649
+ const checked = checkAppStoreIndex(index);
12650
+ if (!checked.ok) throw refused(`${indexPath} is not a valid store index`, checked);
12651
+ const { indexBytes, sigBytes } = await signAppStore(index, key.signer);
12652
+ const read = readAppStore(indexBytes, sigBytes, now, key.key);
12653
+ if (!read.ok) throw refused(`${indexPath} would be refused by a client`, read);
12654
+ const store = read.store;
12655
+ const indexOut = join(outDir, STORE_INDEX_FILE), sigOut = join(outDir, STORE_SIG_FILE);
12656
+ let previous = null;
12657
+ if (existsSync(indexOut) && existsSync(sigOut)) {
12658
+ const held = readAppStore(new Uint8Array(readFileSync(indexOut)), new Uint8Array(readFileSync(sigOut)), now);
12659
+ if (held.ok && held.store.index.key === key.key) {
12660
+ previous = held.store.index.sequence;
12661
+ const decision = appStoreDecision({
12662
+ key: key.key,
12663
+ sequence: held.store.index.sequence,
12664
+ digest: held.store.digest
12665
+ }, {
12666
+ key: key.key,
12667
+ sequence: store.index.sequence,
12668
+ digest: store.digest
12669
+ });
12670
+ if (decision === "rollback" || decision === "equivocation") throw refused(`${indexOut} already holds sequence ${held.store.index.sequence} of this store: ${decision === "rollback" ? "a lower one is refused by every client" : "the same one with other content is refused as equivocation"}; raise sequence`, { reason: decision }, { held: held.store.index.sequence });
12671
+ }
12672
+ }
12673
+ mkdirSync(outDir, { recursive: true });
12674
+ writeWhole(indexOut, indexBytes);
12675
+ writeWhole(sigOut, sigBytes);
12676
+ const notes = [...key.created ? [madeKeyNote(keyPath, "store", key.key)] : [], ...store.expired ? [`expires (${(/* @__PURE__ */ new Date(store.index.expires * 1e3)).toISOString()}) is past: clients still install from it and say the store was not updated since then`] : []];
12677
+ return {
12678
+ key: key.key,
12679
+ fingerprint: appFingerprint(key.key),
12680
+ name: store.index.name,
12681
+ kind: store.index.kind,
12682
+ sequence: store.index.sequence,
12683
+ previous,
12684
+ expires: store.index.expires,
12685
+ expired: store.expired,
12686
+ apps: store.index.apps.length,
12687
+ removed: store.index.removed.length,
12688
+ revoked: store.index.revoked.length,
12689
+ digest: store.digest,
12690
+ index: indexOut,
12691
+ sig: sigOut,
12692
+ keyCreated: key.created,
12693
+ ...notes.length ? { warning: notes.join("; ") } : {}
12694
+ };
12695
+ }
12696
+ //#endregion
12697
+ //#region src/commands/apps.ts
12698
+ /** The key file a command signs with: always named, never a default, never on the command line itself. */
12699
+ function keyFile(value, usage) {
12700
+ if (typeof value !== "string" || !value) throw new CliError("usage", `--key <file> is needed: ghostly ${usage}`);
12701
+ return value;
12702
+ }
12703
+ var key = {
12704
+ type: "string",
12705
+ description: "The key file (made, owner-only, when it is missing)"
12706
+ };
12707
+ /**
12708
+ * The publisher tools of WISP 1200 (Apps · Publishing): they run here, on files, with no profile or daemon. One entry
12709
+ * per command, in alphabetical order (test/commands.test.ts checks).
12710
+ */
12711
+ var commands$8 = {
12712
+ "app publish": {
12713
+ method: "app.publish",
12714
+ usage: "app publish <dir> --key <file> [--out <file>]",
12715
+ summary: "Bundle and sign an app (WISP 1200): <dir>/ghostly-app.json and every other file in <dir> become one .ghostlyapp, its sequence one more than the bundle at --out (default <dir>/app.ghostlyapp)",
12716
+ args: ["dir"],
12717
+ options: {
12718
+ key: {
12719
+ ...key,
12720
+ description: "The publisher key file (made, owner-only, on first use; back it up)"
12721
+ },
12722
+ out: {
12723
+ type: "string",
12724
+ description: "Where the bundle goes (default <dir>/app.ghostlyapp); the bundle there is the version this one follows"
12725
+ },
12726
+ sequence: {
12727
+ type: "number",
12728
+ description: "This version's sequence, higher than the bundle at --out's (default: one more than it)"
12729
+ }
12730
+ },
12731
+ run: ({ options }, a) => publishApp(a.dir, keyFile(options.key, "app publish <dir> --key <file> [--out <file>] [--sequence n]"), options.out, options.sequence)
12732
+ },
12733
+ "app revoke": {
12734
+ method: "app.revoke",
12735
+ usage: "app revoke <dir> --key <file> (--digest <digest>... | --up-to <sequence>) [--reason <text>] [--bundle <file>]",
12736
+ summary: "Revoke versions of your app (WISP 1200 · Revocation): a ghostly-revoke/1 statement signed with its publisher key, added to <dir>/ghostly-revoke.json",
12737
+ args: ["dir"],
12738
+ options: {
12739
+ key: {
12740
+ type: "string",
12741
+ description: "The app's publisher key file (never made here)"
12742
+ },
12743
+ digest: {
12744
+ type: "list",
12745
+ description: "A version's digest, base64url as app verify prints it (again for each, up to 64)"
12746
+ },
12747
+ "up-to": {
12748
+ type: "number",
12749
+ description: "Every version with this sequence or a lower one"
12750
+ },
12751
+ reason: {
12752
+ type: "string",
12753
+ description: "Why, one line of up to 200 characters, shown to people"
12754
+ },
12755
+ bundle: {
12756
+ type: "string",
12757
+ description: "The bundle naming the app (default <dir>/app.ghostlyapp)"
12758
+ }
12759
+ },
12760
+ run: ({ options }, a) => revokeApp(a.dir, keyFile(options.key, "app revoke <dir> --key <file> (--digest <digest>... | --up-to <sequence>) [--reason <text>]"), {
12761
+ digests: options.digest,
12762
+ upTo: options["up-to"],
12763
+ reason: options.reason,
12764
+ bundle: options.bundle
12765
+ })
12766
+ },
12767
+ "app verify": {
12768
+ method: "app.verify",
12769
+ usage: "app verify <bundle|url>",
12770
+ summary: "Check a .ghostlyapp as a client would, from a file or an https URL: its manifest and digest, or refused (exit 1) with the reason",
12771
+ args: ["bundle"],
12772
+ run: (_, a) => verifyApp(a.bundle)
12773
+ },
12774
+ "store sign": {
12775
+ method: "store.sign",
12776
+ usage: "store sign <index> --key <file> [--out <dir>]",
12777
+ summary: "Sign a store index (WISP 1200 · Stores): writes ghostly-store.json, canonical, and ghostly-store.sig beside it or in --out",
12778
+ args: ["index"],
12779
+ options: {
12780
+ key: {
12781
+ ...key,
12782
+ description: "The store key file (made, owner-only, when it is missing and the index names no key; keep it offline)"
12783
+ },
12784
+ out: {
12785
+ type: "string",
12786
+ description: "The folder to write ghostly-store.json and ghostly-store.sig in (default: the index's)"
12787
+ }
12788
+ },
12789
+ run: ({ options }, a) => signStore(a.index, keyFile(options.key, "store sign <index> --key <file> [--out <dir>]"), options.out)
12790
+ }
12791
+ };
12792
+ //#endregion
12194
12793
  //#region src/commands/shared.ts
12195
12794
  var wait = {
12196
12795
  wait: {
@@ -14040,7 +14639,8 @@ var COMMANDS = {
14040
14639
  ...commands$1,
14041
14640
  ...commands$2,
14042
14641
  ...commands,
14043
- ...commands$7
14642
+ ...commands$7,
14643
+ ...commands$8
14044
14644
  };
14045
14645
  /** Commands that take text (argument or stdin), with the secret guard and delivery waits. */
14046
14646
  var TEXT_COMMANDS = {
@@ -14653,10 +15253,11 @@ function loadCallStack() {
14653
15253
  }
14654
15254
  /**
14655
15255
  * The ICE servers of a call, as libdatachannel takes them: the apps' STUN servers (RTC_CONFIG; libjuice uses the
14656
- * first), then the profile's own (Settings `iceServers`: a TURN relay, typically), as the apps' calls use them.
15256
+ * first; none with `GHOSTLY_STUN=0`), then the profile's own (Settings `iceServers`: a TURN relay, typically), as the
15257
+ * apps' calls use them.
14657
15258
  */
14658
- function callIceServers(extra = []) {
14659
- const out = (RTC_CONFIG.iceServers ?? []).flatMap((server) => [server.urls].flat()).slice(0, 2);
15259
+ function callIceServers(extra = [], env = process.env) {
15260
+ const out = env.GHOSTLY_STUN === "0" ? [] : (RTC_CONFIG.iceServers ?? []).flatMap((server) => [server.urls].flat()).slice(0, 2);
14660
15261
  for (const server of callRtcConfig(extra).iceServers?.slice(RTC_CONFIG.iceServers?.length ?? 0) ?? []) for (const url of [server.urls].flat()) {
14661
15262
  const m = /^(stun|turns?):([^:?\s]+)(?::(\d+))?(?:\?transport=(udp|tcp))?$/i.exec(url);
14662
15263
  if (!m) continue;
@@ -16035,6 +16636,203 @@ function groupShape(group) {
16035
16636
  };
16036
16637
  }
16037
16638
  //#endregion
16639
+ //#region ../browser/src/platform/irohWeb.ts
16640
+ /**
16641
+ * Iroh (WISP 102) in a browser: the same QUIC/TLS session and binding as the
16642
+ * Desktop's native Iroh, but relay only. A page cannot send UDP, so every
16643
+ * packet goes through an Iroh relay over a WebSocket; the relay sees who talks
16644
+ * to whom and when, never the frames.
16645
+ *
16646
+ * The wasm (about 1 MB gzip) loads on the first chat that starts an endpoint,
16647
+ * never on the app's first load.
16648
+ */
16649
+ /**
16650
+ * n0's public relays, the same ones the Desktop's Iroh homes on by default. Iroh itself names them with the trailing
16651
+ * dot of a full domain name (`use1-1.relay.n0.iroh.link.`), and compares relay URLs as text; here they have none,
16652
+ * because WebKit opens no host name that ends in a dot. `BROWSER_SPELLING` is what keeps the two one relay.
16653
+ */
16654
+ var DEFAULT_IROH_RELAYS = [
16655
+ "https://use1-1.relay.n0.iroh.link/",
16656
+ "https://euc1-1.relay.n0.iroh.link/",
16657
+ "https://aps1-1.relay.n0.iroh.link/",
16658
+ "https://usw1-1.relay.n0.iroh.link/"
16659
+ ];
16660
+ /**
16661
+ * The spelling of every relay URL handed to the wasm Iroh (`irohRelayUrl`): the relays it homes on and a contact's
16662
+ * relay before a dial. One spelling, so a contact on this endpoint's relay is reached over the connection already
16663
+ * open to it; without the dot, so Safari can open it (a Desktop's record names its relay with the dot).
16664
+ */
16665
+ var BROWSER_SPELLING = "plain";
16666
+ /**
16667
+ * Why a relay URL cannot be used, or null. TLS, except on this machine (a local `iroh-relay --dev`): a page
16668
+ * may not open plain WebSockets elsewhere, and the relay's operator would see traffic metadata in the clear.
16669
+ */
16670
+ function irohRelayProblem(value) {
16671
+ let url;
16672
+ try {
16673
+ url = new URL(value);
16674
+ } catch {
16675
+ return `Not a relay address: ${value}`;
16676
+ }
16677
+ const local = [
16678
+ "localhost",
16679
+ "127.0.0.1",
16680
+ "[::1]"
16681
+ ].includes(url.hostname);
16682
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && local)) return `Use an https:// relay address: ${value}`;
16683
+ if (url.username || url.password || url.search || url.hash || value.length > 256) return `Not a relay address: ${value}`;
16684
+ return null;
16685
+ }
16686
+ var loaded = null;
16687
+ /** Loads the wasm once per page. Tests pass their own `load` (Node reads the file). */
16688
+ function loadIrohWasm() {
16689
+ loaded ??= import("./assets/ghostly_iroh_web-Dvzh3Y6E.js").then(async (module) => {
16690
+ await module.default();
16691
+ return module;
16692
+ }).catch((error) => {
16693
+ loaded = null;
16694
+ throw error;
16695
+ });
16696
+ return loaded;
16697
+ }
16698
+ var SEND_BUDGET = 122880;
16699
+ var MAX_FRAME = 61440;
16700
+ /** One paired channel over one QUIC stream. Sends are queued in order. */
16701
+ var IrohWebChannel = class {
16702
+ conn;
16703
+ forget;
16704
+ reader = null;
16705
+ buffered = [];
16706
+ queue = Promise.resolve();
16707
+ closed = false;
16708
+ bufferedAmount = 0;
16709
+ sendBudget = SEND_BUDGET;
16710
+ onClose = null;
16711
+ constructor(conn, forget) {
16712
+ this.conn = conn;
16713
+ this.forget = forget;
16714
+ this.read();
16715
+ }
16716
+ get onMessage() {
16717
+ return this.reader;
16718
+ }
16719
+ set onMessage(reader) {
16720
+ this.reader = reader;
16721
+ if (reader) for (const text of this.buffered.splice(0)) reader(text);
16722
+ }
16723
+ async read() {
16724
+ try {
16725
+ for (;;) {
16726
+ const text = await this.conn.recv();
16727
+ if (text === void 0 || this.closed) break;
16728
+ if (this.reader) this.reader(text);
16729
+ else if (this.buffered.length < 64) this.buffered.push(text);
16730
+ else break;
16731
+ }
16732
+ } catch {}
16733
+ this.close();
16734
+ }
16735
+ send(data) {
16736
+ if (typeof data !== "string" || this.closed || !data.length || data.length > MAX_FRAME || this.bufferedAmount + data.length > SEND_BUDGET) throw new Error("Iroh channel is closed or its send budget is full");
16737
+ this.bufferedAmount += data.length;
16738
+ this.queue = this.queue.then(() => this.closed ? void 0 : this.conn.send(data)).catch(() => this.close()).finally(() => {
16739
+ this.bufferedAmount -= data.length;
16740
+ });
16741
+ }
16742
+ drained() {
16743
+ return this.queue;
16744
+ }
16745
+ get rttMs() {
16746
+ const rtt = this.conn.rtt();
16747
+ return rtt >= 0 ? rtt : void 0;
16748
+ }
16749
+ close() {
16750
+ if (this.closed) return;
16751
+ this.closed = true;
16752
+ this.buffered = [];
16753
+ try {
16754
+ this.conn.close();
16755
+ } catch {}
16756
+ this.forget();
16757
+ this.onClose?.();
16758
+ }
16759
+ };
16760
+ /** A NativeEndpoint for `iroh/1`, as the Desktop's is, for one chat's transport seed. */
16761
+ async function createIrohWebEndpoint(seedB64, options = {}) {
16762
+ const seed = fromBase64Url$1(seedB64);
16763
+ if (seed.length !== 32) throw new Error("Invalid transport seed");
16764
+ const relays = [...new Set((options.relays?.length ? options.relays : DEFAULT_IROH_RELAYS).map((relay) => irohRelayUrl(relay, BROWSER_SPELLING)))];
16765
+ const node = await (await (options.load ?? loadIrohWasm)()).IrohNode.start(seed, relays, options.onlineMs ?? 1e4);
16766
+ const channels = /* @__PURE__ */ new Set();
16767
+ let stopped = false;
16768
+ let handler = null;
16769
+ const early = [];
16770
+ const bind = (conn) => {
16771
+ const channel = new IrohWebChannel(conn, () => channels.delete(channel));
16772
+ channels.add(channel);
16773
+ return {
16774
+ channel,
16775
+ binding: conn.binding()
16776
+ };
16777
+ };
16778
+ const endpoint = {
16779
+ transport: "iroh/1",
16780
+ descriptor: {
16781
+ ...node.address(),
16782
+ relayed: true
16783
+ },
16784
+ onDescriptor: null,
16785
+ get onConnection() {
16786
+ return handler;
16787
+ },
16788
+ set onConnection(value) {
16789
+ handler = value;
16790
+ if (value) for (const bound of early.splice(0)) value(bound);
16791
+ },
16792
+ async connect(descriptor) {
16793
+ if (stopped) throw new Error("Iroh endpoint is stopped");
16794
+ const conn = await node.connect(irohDescriptor(descriptor, BROWSER_SPELLING), options.connectMs ?? 2e4);
16795
+ if (stopped) {
16796
+ conn.close();
16797
+ throw new Error("Iroh endpoint is stopped");
16798
+ }
16799
+ return bind(conn);
16800
+ },
16801
+ async close() {
16802
+ if (stopped) return;
16803
+ stopped = true;
16804
+ for (const channel of [...channels]) channel.close();
16805
+ for (const bound of early.splice(0)) bound.channel.close();
16806
+ await node.close().catch(() => {});
16807
+ }
16808
+ };
16809
+ (async () => {
16810
+ while (!stopped) {
16811
+ let conn;
16812
+ try {
16813
+ conn = await node.accept();
16814
+ } catch {
16815
+ break;
16816
+ }
16817
+ if (!conn) break;
16818
+ if (stopped) {
16819
+ conn.close();
16820
+ break;
16821
+ }
16822
+ const bound = bind(conn);
16823
+ const accept = endpoint.onConnection;
16824
+ if (accept) accept(bound);
16825
+ else if (early.length < 4) early.push(bound);
16826
+ else bound.channel.close();
16827
+ }
16828
+ if (!stopped) {
16829
+ endpoint.onUnavailable?.();
16830
+ await endpoint.close();
16831
+ }
16832
+ })();
16833
+ return endpoint;
16834
+ }
16835
+ //#endregion
16038
16836
  //#region ../browser/src/engine/paymentAdapters/fedimintSdk.ts
16039
16837
  var fedimintDatabase = (id) => `ghostly-fedimint-${id}.db`;
16040
16838
  var MODULE_WASM = () => import("./assets/fedimint_client_wasm_bg-BtHHlFpp.js");
@@ -16921,6 +17719,39 @@ function pkarrRelays(env = process.env) {
16921
17719
  return relays;
16922
17720
  }
16923
17721
  /**
17722
+ * The Iroh relays of a private network or a test, `GHOSTLY_IROH_RELAYS` ("https://…,…"; http:// on loopback): the only
17723
+ * relays this process's Iroh homes on, whatever the profile's `irohRelays` setting says. Null when unset (the setting,
17724
+ * else n0's public relays).
17725
+ */
17726
+ function irohRelays(env = process.env) {
17727
+ const relays = (env.GHOSTLY_IROH_RELAYS ?? "").split(",").map((url) => url.trim()).filter(Boolean);
17728
+ if (!relays.length) return null;
17729
+ const bad = relays.map((url) => irohRelayProblem(url)).find(Boolean);
17730
+ if (bad) throw new CliError("usage", `GHOSTLY_IROH_RELAYS: ${bad}`);
17731
+ return relays;
17732
+ }
17733
+ /**
17734
+ * The apps' public STUN servers (Google's, `RTC_CONFIG`), which every WebRTC connection asks for its public address:
17735
+ * `GHOSTLY_STUN=0` leaves them out (a private network, or tests on one machine, where host candidates connect). The
17736
+ * profile's own `iceServers` stay.
17737
+ */
17738
+ function publicStun(env = process.env) {
17739
+ return env.GHOSTLY_STUN !== "0";
17740
+ }
17741
+ /** An RTC configuration without the apps' public STUN servers. */
17742
+ function withoutPublicStun(config) {
17743
+ if (!config?.iceServers) return config;
17744
+ const ours = new Set((RTC_CONFIG.iceServers ?? []).flatMap((server) => [server.urls].flat()));
17745
+ const iceServers = config.iceServers.map((server) => ({
17746
+ ...server,
17747
+ urls: [server.urls].flat().filter((url) => !ours.has(url))
17748
+ })).filter((server) => server.urls.length);
17749
+ return {
17750
+ ...config,
17751
+ iceServers
17752
+ };
17753
+ }
17754
+ /**
16924
17755
  * The Mainline DHT, for Pkarr beside the relays (`RelaysAndDht`): `GHOSTLY_DHT=0` leaves it out (relays only, as the web
16925
17756
  * app), `GHOSTLY_DHT_BOOTSTRAP` ("host:port,…") replaces the public routers (a testnet; one all on loopback binds there).
16926
17757
  * `GHOSTLY_PKARR_RELAYS` without a bootstrap leaves it out too, as on the Desktop: a private network stays off the
@@ -16956,12 +17787,43 @@ async function installWebRtc() {
16956
17787
  "RTCPeerConnectionIceEvent",
16957
17788
  "RTCCertificate"
16958
17789
  ]) scope[name] ??= polyfill[name];
17790
+ refuseDataAnswers(scope);
17791
+ if (!publicStun()) leaveOutPublicStun(scope);
16959
17792
  return true;
16960
17793
  } catch (error) {
16961
17794
  process.stderr.write(`ghostly: WebRTC is unavailable (${error instanceof Error ? error.message : String(error)}); chats use HyperDHT, Iroh or the DHT\n`);
16962
17795
  return false;
16963
17796
  }
16964
17797
  }
17798
+ /**
17799
+ * For tests: `GHOSTLY_TEST_REFUSE_DATA_ANSWERS=<n>` refuses one in every n answers this process applies to its offers
17800
+ * (the first, the n+1st, and so on), as libdatachannel 0.24.5 refuses one in its race (`ANSWER_REFUSED_REDIALS` in
17801
+ * packages/core/src/datalink.ts): the connection closes, and setRemoteDescription throws.
17802
+ */
17803
+ function refuseDataAnswers(scope) {
17804
+ const every = Number(process.env.GHOSTLY_TEST_REFUSE_DATA_ANSWERS) || 0;
17805
+ if (every <= 0) return;
17806
+ let answers = 0;
17807
+ const Base = scope.RTCPeerConnection;
17808
+ scope.RTCPeerConnection = class extends Base {
17809
+ async setRemoteDescription(description) {
17810
+ if (description.type === "answer" && answers++ % every === 0) {
17811
+ this.close();
17812
+ throw new Error("libdatachannel error while adding remote description: Got a remote candidate without ICE transport (GHOSTLY_TEST_REFUSE_DATA_ANSWERS)");
17813
+ }
17814
+ return super.setRemoteDescription(description);
17815
+ }
17816
+ };
17817
+ }
17818
+ /** `GHOSTLY_STUN=0`: every connection the engine makes leaves the apps' public STUN servers out of its configuration. */
17819
+ function leaveOutPublicStun(scope) {
17820
+ const Base = scope.RTCPeerConnection;
17821
+ scope.RTCPeerConnection = class extends Base {
17822
+ constructor(config) {
17823
+ super(withoutPublicStun(config));
17824
+ }
17825
+ };
17826
+ }
16965
17827
  /** Server-sent events, which browsers have and Node does not: Arkade follows its server with them. */
16966
17828
  async function installEventSource() {
16967
17829
  const scope = globalThis;
@@ -17002,13 +17864,17 @@ function leftOutIn(mark) {
17002
17864
  }
17003
17865
  async function startRuntime(paths, options = {}) {
17004
17866
  const pinned = pkarrRelays();
17867
+ const pinnedIroh = irohRelays();
17005
17868
  const trace = process.env.GHOSTLY_LINK_TRACE;
17006
- if (trace) setLinkTraceSink((line) => appendFileSync(trace, line + "\n", { mode: 384 }));
17869
+ if (trace) {
17870
+ setLinkTraceSink((line) => appendFileSync(trace, line + "\n", { mode: 384 }));
17871
+ (await import("./assets/joinTrace-CO8mlhNO.js").then((n) => n.t)).setJoinTraceSink((line) => appendFileSync(trace, line + "\n", { mode: 384 }));
17872
+ }
17007
17873
  const store = await openPersistentIndexedDb(paths.db);
17008
17874
  const restored = join(paths.dir, RESTORED_MARK);
17009
17875
  if (existsSync(restored)) {
17010
17876
  try {
17011
- const rows = await import("./assets/restoredRows-C1O9FfA4.js");
17877
+ const rows = await import("./assets/restoredRows-lhTKGmCO.js");
17012
17878
  await rows.markRestoredWallet();
17013
17879
  await rows.markLeftOutFiles(leftOutIn(restored));
17014
17880
  await store.compact();
@@ -17027,19 +17893,20 @@ async function startRuntime(paths, options = {}) {
17027
17893
  await installEventSource();
17028
17894
  await preloadWalletModules();
17029
17895
  (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")]);
17896
+ const [{ registerFileBytes }, { NodeFileBytes }] = await Promise.all([import("./assets/fileBytes-3AQ2tJzW.js").then((n) => n.o), import("./assets/fileBytes-CAZDydIe.js")]);
17031
17897
  registerFileBytes("native", async () => new NodeFileBytes(paths.files), true);
17032
- const [{ EngineServer }, { createHyperEndpoint }] = await Promise.all([import("./assets/server-9AM0ta6q.js").then((n) => n.t), import("./assets/endpoint-CaSbSFDw.js")]);
17898
+ const [{ EngineServer }, { createHyperEndpoint }] = await Promise.all([import("./assets/server-TamKsfmC.js").then((n) => n.t), import("./assets/endpoint-CaSbSFDw.js")]);
17033
17899
  const network = hyperdhtNetwork();
17034
17900
  const dht = mainlineNetwork();
17035
- const mainline = dht.off ? null : new (await (import("./assets/mainline-B0UTU0-4.js"))).Mainline({
17901
+ const mainline = dht.off ? null : new (await (import("./assets/mainline-BabMhm8L.js"))).Mainline({
17036
17902
  bootstrap: dht.bootstrap,
17037
17903
  host: dht.host
17038
17904
  });
17039
- const transport = mainline || pinned ? new (await (import("./assets/mainline-B0UTU0-4.js"))).RelaysAndDht(mainline, pinned ? { relays: pinned } : {}, !!pinned) : null;
17905
+ const transport = mainline || pinned ? new (await (import("./assets/mainline-BabMhm8L.js"))).RelaysAndDht(mainline, pinned ? { relays: pinned } : {}, !!pinned) : null;
17040
17906
  const server = new EngineServer({
17041
17907
  ...transport ? { transport } : {},
17042
17908
  irohWeb: true,
17909
+ ...pinnedIroh ? { irohRelays: pinnedIroh } : {},
17043
17910
  singleDevice: true,
17044
17911
  nativeTransports: { "hyperdht/1": (seedB64) => createHyperEndpoint(fromBase64Url$1(seedB64), network) },
17045
17912
  automaticWallets: false,
@@ -18433,6 +19300,15 @@ async function tableCommand(name, argv) {
18433
19300
  const g = globals(parsed);
18434
19301
  pretty = g.pretty;
18435
19302
  const args = positionals(command, parsed.positionals);
19303
+ if (command.run) {
19304
+ const result = await command.run(parsed, args);
19305
+ if (typeof result.warning === "string") {
19306
+ process.stderr.write(`ghostly: ${result.warning}\n`);
19307
+ delete result.warning;
19308
+ }
19309
+ print(result);
19310
+ return;
19311
+ }
18436
19312
  const params = command.params?.(parsed, args) ?? {};
18437
19313
  for (const key of Object.keys(params)) if (params[key] === void 0) delete params[key];
18438
19314
  if (command.method === "identity.complete") {
@@ -18608,6 +19484,6 @@ async function run(argv) {
18608
19484
  //#region src/bin.ts
18609
19485
  run(process.argv.slice(2));
18610
19486
  //#endregion
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 };
19487
+ export { BACKUP_MEDIA_TYPE as $, requireTurn as A, streamStored as At, networkLabel as B, breezDatabasesFor as C, replyTo as Ct, sourceKey as D, readStored as Dt, ProviderSources as E, FileAppender as Et, sealSeed as F, BITCOIN_INVOICE_ON_TESTNET as G, cashuMint as H, unsealSeed as I, fakesLightning as J, DEFAULT_MINTS as K, ModeChanged as L, setSingleWriterGate as M, intentRepository as N, awayFrom as O, removeStored as Ot, newDeviceKey as P, paysItsOwnInvoices as Q, ModeGate as R, breezDatabaseInUse as S, replyRef as St, lightningPaying as T, paymentWireId as Tt, MESSAGE_DETAILS_MAX_BYTES as U, CASHU_MINT_SOURCE as V, WALLET_TYPES as W, isWorthlessMint as X, isTestMint as Y, mintNetwork as Z, LightningCards as _, verifyIdentity as _t, irohRelayProblem as a, reserveAdapterIds as at, normalizePhrase as b, pairedWireReply as bt, createFailure as c, SourceConfigError as ct, paymentNetwork as d, networkMode as dt, MAX_BACKUP_BYTES as et, paymentNetworksOf as f, redact as ft, CASHU_CARD as g, boundedIdentityFetch as gt, rpcHost as h, setOwnDidSource as ht, createIrohWebEndpoint as i, registeredOnchainProviders as it, setAwayWallets as j, refuseAway as k, storedSize as kt, createTiming as l, SourceUnreachableError as lt, networkReason as m, resolveAtprotoDid as mt, loadFedimintSdk as n, onAdaptersChanged as nt, WALLET_NAMES as o, NothingSpentError as ot, walletInstances as p, assertPublicServer as pt, TEST_MINT as q, DEFAULT_IROH_RELAYS as r, registeredLightningProviders as rt, assertConfirmedReal as s, PROVIDER_PLATFORMS as st, fedimintDatabase as t, identityProviders as tt, crossNetwork as u, isNothingSpentError as ut, cardsKey as v, chosenResolver as vt, dropBreezDatabase as w, paymentAlias as wt, breezDatabase as x, receivedPairedReply as xt, isRecoveryPhrase as y, groupReply as yt, WrongNetworkError as z };
18612
19488
 
18613
19489
  //# sourceMappingURL=ghostly.mjs.map