@ghostlytools/cli 1.1.4 → 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 (30) hide show
  1. package/README.md +7 -2
  2. package/dist/assets/{fileBytes-DxiJHmTk.js → fileBytes-3AQ2tJzW.js} +5 -17
  3. package/dist/assets/{fileBytes-DxiJHmTk.js.map → fileBytes-3AQ2tJzW.js.map} +1 -1
  4. package/dist/assets/{fileBytes-BQ84L9Fe.js → fileBytes-CAZDydIe.js} +2 -2
  5. package/dist/assets/{fileBytes-BQ84L9Fe.js.map → fileBytes-CAZDydIe.js.map} +1 -1
  6. package/dist/assets/{fileBytesIdb-DQkQ10PS.js → fileBytesIdb-CmG00qE-.js} +3 -3
  7. package/dist/assets/{fileBytesIdb-DQkQ10PS.js.map → fileBytesIdb-CmG00qE-.js.map} +1 -1
  8. package/dist/assets/{fileBytesOpfs-BFfR_68b.js → fileBytesOpfs-CTgqt-p3.js} +3 -3
  9. package/dist/assets/{fileBytesOpfs-BFfR_68b.js.map → fileBytesOpfs-CTgqt-p3.js.map} +1 -1
  10. package/dist/assets/{hyperdhtRelay-BoxPHp7v.js → hyperdhtRelay-w9cwVqGZ.js} +3 -3
  11. package/dist/assets/{hyperdhtRelay-BoxPHp7v.js.map → hyperdhtRelay-w9cwVqGZ.js.map} +1 -1
  12. package/dist/assets/{idb-Co2lObpU.js → idb-BIdus0WX.js} +32 -8
  13. package/dist/assets/idb-BIdus0WX.js.map +1 -0
  14. package/dist/assets/joinTrace-CO8mlhNO.js +29 -0
  15. package/dist/assets/joinTrace-CO8mlhNO.js.map +1 -0
  16. package/dist/assets/{mainline-dkA94eh2.js → mainline-BabMhm8L.js} +2 -2
  17. package/dist/assets/{mainline-dkA94eh2.js.map → mainline-BabMhm8L.js.map} +1 -1
  18. package/dist/assets/{restoredRows-C1O9FfA4.js → restoredRows-lhTKGmCO.js} +2 -2
  19. package/dist/assets/{restoredRows-C1O9FfA4.js.map → restoredRows-lhTKGmCO.js.map} +1 -1
  20. package/dist/assets/rolldown-runtime-8H4AJuhK.js +14 -0
  21. package/dist/assets/{server-CXFTDnFI.js → server-TamKsfmC.js} +1400 -235
  22. package/dist/assets/server-TamKsfmC.js.map +1 -0
  23. package/dist/assets/{src-K3NijMq-.js → src-C6qQYrAH.js} +1721 -106
  24. package/dist/assets/src-C6qQYrAH.js.map +1 -0
  25. package/dist/ghostly.mjs +882 -21
  26. package/dist/ghostly.mjs.map +1 -1
  27. package/package.json +1 -1
  28. package/dist/assets/idb-Co2lObpU.js.map +0 -1
  29. package/dist/assets/server-CXFTDnFI.js.map +0 -1
  30. package/dist/assets/src-K3NijMq-.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, $i as replySnippet, $r as IdentityCheckUnavailable, $s as fromBase64Url$1, $t as isPubkyKey, Aa as CALL_SIGNAL_MAX_AGE_MS, Ai as parseVoiceMeta, B as sameSshKey, Bs as RELAY_PAYLOAD_MAX_BYTES, Ca as GhostlyHttpError, Cn as resolveLightningDestination, Ct as parseDidSignaturePaste, Da as LIMITS, Di as VOICE_LIMITS, Ei as PLAYABLE_AUDIO, En as decodeBolt11, Et as strictBase64Url, Fa as callSignalHeardAt, G as decodeAnyBitcoinAddress, H as verifySshSignature, I as SSHSIG_NAMESPACE, Ia as extractParamsFromSdp, Ir as BUTTONS_CAPABILITY, J as ATPROTO_PROOF_COLLECTION, K as ATPROTO_DECLARED_SCOPE, Kt as walletNetworkOf, L as SshSigError, La as parseCallSignal, Ma as answersOffer, Na as buildSdpFromSignal, Oi as baseMime, P as groupName, Pa as callRtcConfig, Pr as checkPushEndpoint, Ps as setLinkTraceSink, Q as atprotoProofRkey, Qt as PUBKY_PROOF_MAX_BYTES, R as parseSshPublicKey, Ra as signalHasVideo, Si as readImageMeta, Sn as requestLnurlInvoice, St as parseDidDocument, Ti as sanitizeMime, Tn as engineText, Tt as staticDidDocument, U as verifyBitcoinMessage, Us as openRelayPayload, V as sshSignCommand, Vt as WALLET_NETWORKS, W as BitcoinAddressError, X as atprotoDidMethod, Xs as MAX_AVATAR_LENGTH, Y as ATPROTO_PROOF_SCOPE, Z as atprotoProofRecord, Zs as sanitizeAvatar, _n as recordsFromTxt, _t as didWebFileNames, aa as checkStatusCard, an as pubkyProofPath, at as verifyAtprotoRecordProof, bn as txtValue, bt as normalizeDid, cn as DOMAIN_FILE_MAX_BYTES, dn as domainTxtRecord, dt as checkDidSignature, ec as randomBytes$1, 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 buttonLabelClash, ic as utf8Encode, ii as identityStatement, in as pubkyProofFile, it as AtprotoProofError, ja as RTC_CONFIG, ji as voicePeaksOf, jt as resolveDidDht, ln as decodeDnsResponse, lt as DidError, mn as isPublicAddress, mt as didSignedMessages, na as RUN_RESULTS, nn as normalizePubkyKey, 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 STATUS_CARD_LIMITS, rc as utf8Decode, rn as pubkyProofCapability, rs as inviteLink, rt as shortAtprotoDid, sn as pubkyHomeserverOf, st as DID_FILE_MAX_BYTES, ta as ITEM_STATES, tc as toBase64Url$1, ti as boundVerifiedIdentity, tn as newPubkyProofFolder, tt as normalizeAtprotoHandle, un as dnsProofName, ut as checkDidJws, vn as recordsFromWellKnown, vr as sanitizeTypingStatus, vt as didWebFileUrl, wi as sanitizeFileName, wn as engineError, wt as signingKeys, xn as wellKnownUrl, xt as parseDid, yn as recordsName, yt as didWebHost, z as parseSshSignature } from "./assets/src-K3NijMq-.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.4",
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",
@@ -595,6 +596,10 @@ var ENGINE_METHODS = [
595
596
  "retryMessage",
596
597
  "react",
597
598
  "pinMessage",
599
+ "appId",
600
+ "appOpen",
601
+ "appClose",
602
+ "appSend",
598
603
  "messageDetails",
599
604
  "deleteMessage",
600
605
  "exportLinks",
@@ -713,7 +718,27 @@ var ENGINE_METHODS = [
713
718
  "deviceSetNoticeSeen",
714
719
  "deviceTurnCheck",
715
720
  "devicePushState",
716
- "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"
717
742
  ];
718
743
  /** Reads the hosts use outside `EngineApi`: the whole state, and one chat's stored messages. */
719
744
  var ENGINE_READS = ["getState", "getMessages"];
@@ -6248,7 +6273,7 @@ function openPassportWindow(url) {
6248
6273
  return window.open(url, "pubky-passport", "popup,width=520,height=760");
6249
6274
  }
6250
6275
  /** A 401 or 403 from the homeserver: the session it was given is not one it accepts. */
6251
- var refused = (e) => {
6276
+ var refused$1 = (e) => {
6252
6277
  const status = e?.data?.statusCode;
6253
6278
  return status === 401 || status === 403;
6254
6279
  };
@@ -6479,7 +6504,7 @@ async function approveAndWork(options, work, { grant, cookie, opened, settled, c
6479
6504
  throw e;
6480
6505
  }
6481
6506
  const fail = (what) => (e) => {
6482
- 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.");
6483
6508
  throw failure(what, e);
6484
6509
  };
6485
6510
  const approved = session;
@@ -8489,7 +8514,7 @@ async function restoreProfile(home, name, file, passphrase) {
8489
8514
  if (!reader) {
8490
8515
  const payload = await readWhole(file, passphrase ?? "");
8491
8516
  work = await restoringFolder(home, name);
8492
- await writeWhole(work, payload);
8517
+ await writeWhole$1(work, payload);
8493
8518
  return await finishRestore(home, name, work);
8494
8519
  }
8495
8520
  const next = async () => {
@@ -8582,7 +8607,7 @@ async function readWhole(file, passphrase) {
8582
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");
8583
8608
  return payload;
8584
8609
  }
8585
- async function writeWhole(paths, payload) {
8610
+ async function writeWhole$1(paths, payload) {
8586
8611
  await mkdir(paths.db, {
8587
8612
  recursive: true,
8588
8613
  mode: 448
@@ -12206,6 +12231,565 @@ function connectDaemon(path, timeoutMs = 2e3) {
12206
12231
  });
12207
12232
  }
12208
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
12209
12793
  //#region src/commands/shared.ts
12210
12794
  var wait = {
12211
12795
  wait: {
@@ -14055,7 +14639,8 @@ var COMMANDS = {
14055
14639
  ...commands$1,
14056
14640
  ...commands$2,
14057
14641
  ...commands,
14058
- ...commands$7
14642
+ ...commands$7,
14643
+ ...commands$8
14059
14644
  };
14060
14645
  /** Commands that take text (argument or stdin), with the secret guard and delivery waits. */
14061
14646
  var TEXT_COMMANDS = {
@@ -14668,10 +15253,11 @@ function loadCallStack() {
14668
15253
  }
14669
15254
  /**
14670
15255
  * The ICE servers of a call, as libdatachannel takes them: the apps' STUN servers (RTC_CONFIG; libjuice uses the
14671
- * 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.
14672
15258
  */
14673
- function callIceServers(extra = []) {
14674
- 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);
14675
15261
  for (const server of callRtcConfig(extra).iceServers?.slice(RTC_CONFIG.iceServers?.length ?? 0) ?? []) for (const url of [server.urls].flat()) {
14676
15262
  const m = /^(stun|turns?):([^:?\s]+)(?::(\d+))?(?:\?transport=(udp|tcp))?$/i.exec(url);
14677
15263
  if (!m) continue;
@@ -16050,6 +16636,203 @@ function groupShape(group) {
16050
16636
  };
16051
16637
  }
16052
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
16053
16836
  //#region ../browser/src/engine/paymentAdapters/fedimintSdk.ts
16054
16837
  var fedimintDatabase = (id) => `ghostly-fedimint-${id}.db`;
16055
16838
  var MODULE_WASM = () => import("./assets/fedimint_client_wasm_bg-BtHHlFpp.js");
@@ -16936,6 +17719,39 @@ function pkarrRelays(env = process.env) {
16936
17719
  return relays;
16937
17720
  }
16938
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
+ /**
16939
17755
  * The Mainline DHT, for Pkarr beside the relays (`RelaysAndDht`): `GHOSTLY_DHT=0` leaves it out (relays only, as the web
16940
17756
  * app), `GHOSTLY_DHT_BOOTSTRAP` ("host:port,…") replaces the public routers (a testnet; one all on loopback binds there).
16941
17757
  * `GHOSTLY_PKARR_RELAYS` without a bootstrap leaves it out too, as on the Desktop: a private network stays off the
@@ -16971,12 +17787,43 @@ async function installWebRtc() {
16971
17787
  "RTCPeerConnectionIceEvent",
16972
17788
  "RTCCertificate"
16973
17789
  ]) scope[name] ??= polyfill[name];
17790
+ refuseDataAnswers(scope);
17791
+ if (!publicStun()) leaveOutPublicStun(scope);
16974
17792
  return true;
16975
17793
  } catch (error) {
16976
17794
  process.stderr.write(`ghostly: WebRTC is unavailable (${error instanceof Error ? error.message : String(error)}); chats use HyperDHT, Iroh or the DHT\n`);
16977
17795
  return false;
16978
17796
  }
16979
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
+ }
16980
17827
  /** Server-sent events, which browsers have and Node does not: Arkade follows its server with them. */
16981
17828
  async function installEventSource() {
16982
17829
  const scope = globalThis;
@@ -17017,13 +17864,17 @@ function leftOutIn(mark) {
17017
17864
  }
17018
17865
  async function startRuntime(paths, options = {}) {
17019
17866
  const pinned = pkarrRelays();
17867
+ const pinnedIroh = irohRelays();
17020
17868
  const trace = process.env.GHOSTLY_LINK_TRACE;
17021
- 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
+ }
17022
17873
  const store = await openPersistentIndexedDb(paths.db);
17023
17874
  const restored = join(paths.dir, RESTORED_MARK);
17024
17875
  if (existsSync(restored)) {
17025
17876
  try {
17026
- const rows = await import("./assets/restoredRows-C1O9FfA4.js");
17877
+ const rows = await import("./assets/restoredRows-lhTKGmCO.js");
17027
17878
  await rows.markRestoredWallet();
17028
17879
  await rows.markLeftOutFiles(leftOutIn(restored));
17029
17880
  await store.compact();
@@ -17042,19 +17893,20 @@ async function startRuntime(paths, options = {}) {
17042
17893
  await installEventSource();
17043
17894
  await preloadWalletModules();
17044
17895
  (await import("./assets/ghostly_iroh_web-Dvzh3Y6E.js")).initSync({ module: irohWasmBytes() });
17045
- 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")]);
17046
17897
  registerFileBytes("native", async () => new NodeFileBytes(paths.files), true);
17047
- const [{ EngineServer }, { createHyperEndpoint }] = await Promise.all([import("./assets/server-CXFTDnFI.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")]);
17048
17899
  const network = hyperdhtNetwork();
17049
17900
  const dht = mainlineNetwork();
17050
- const mainline = dht.off ? null : new (await (import("./assets/mainline-dkA94eh2.js"))).Mainline({
17901
+ const mainline = dht.off ? null : new (await (import("./assets/mainline-BabMhm8L.js"))).Mainline({
17051
17902
  bootstrap: dht.bootstrap,
17052
17903
  host: dht.host
17053
17904
  });
17054
- const transport = mainline || pinned ? new (await (import("./assets/mainline-dkA94eh2.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;
17055
17906
  const server = new EngineServer({
17056
17907
  ...transport ? { transport } : {},
17057
17908
  irohWeb: true,
17909
+ ...pinnedIroh ? { irohRelays: pinnedIroh } : {},
17058
17910
  singleDevice: true,
17059
17911
  nativeTransports: { "hyperdht/1": (seedB64) => createHyperEndpoint(fromBase64Url$1(seedB64), network) },
17060
17912
  automaticWallets: false,
@@ -18448,6 +19300,15 @@ async function tableCommand(name, argv) {
18448
19300
  const g = globals(parsed);
18449
19301
  pretty = g.pretty;
18450
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
+ }
18451
19312
  const params = command.params?.(parsed, args) ?? {};
18452
19313
  for (const key of Object.keys(params)) if (params[key] === void 0) delete params[key];
18453
19314
  if (command.method === "identity.complete") {
@@ -18623,6 +19484,6 @@ async function run(argv) {
18623
19484
  //#region src/bin.ts
18624
19485
  run(process.argv.slice(2));
18625
19486
  //#endregion
18626
- 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 };
18627
19488
 
18628
19489
  //# sourceMappingURL=ghostly.mjs.map