@ghostlytools/cli 1.2.1 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.js";
2
- import { $a as appRef, $c as randomBytes, $i as parseImageMeta, $n as engineError, $o as filePartName, $r as pinNumberHolds, $s as createChatInvite, $t as PaymentPreflightError, A as decodeGroupEntryLink, Aa as pairedReplyAuthor, Ac as RelayTransport, Ai as IDENTITY_REVOCATION_LABEL, Ao as RTC_CONFIG, Ar as mergeBeacon, As as readSetUpdate, B as groupWakes, Bc as createRelayPayload, Bi as lastSharedWithMe, Bo as LEGACY_SERVICES, Br as FILE_LIMITS, Bs as enrollCancelReason, C as COMMUNITY_LIMITS, Ca as nextReactionNumber, Cc as nextTurnPosition, Ci as readManifest, Co as formatLocalTarget, Cr as COMMUNITY_TOPOLOGY, Cs as readHandoffHello, D as decodeCommunityLink, Da as readReaction, Dc as turnKeys, Di as dialDescriptors, Do as decodeControl, Dr as freshHubs, Ds as sealHandoffPiece, E as communityHasFrame, Ea as reactionIsNewer, Ec as signTurnRelease, Ei as capsDescriptors, Eo as LIMITS, Er as doorHubs, Es as readHandoffVerified, F as mergeKnocks, Fc as budgetRetryMs, Fi as identityRevocationValue, Fr as readBeacon, Ga as arrivalKey, Gc as identityFromSeed, Gi as automaticTransport, Gn as findEndpoint, Go as HANDOFF_HAVE, Gr as TypingSender, Gs as enrollProofFrame, Gt as didDhtFromPublicKey, Ha as checkStatusCard, Hi as emptyProofLedger, Hn as ENDPOINT, Ho as HANDOFF_ACK, Hr as transferStalled, Hs as enrollDoneFrame, I as readKnocks, Ic as isDiscoveryBudgetError, Ii as identityStatement, Io as parseCallSignal, Ir as readBeaconHead, J as carryMentions, Ja as receivedTimestamp, Jc as verify, Jn as parseFedimintRequestPayload, Jo as HANDOFF_PAKE, Js as verifyEnrollProof, Ka as claimedTime, Kc as identityFromSeedB64, Kn as isFederationId, Ko as HANDOFF_HELLO, Ks as readEnrollGrant, Kt as encodeDidDhtDeactivated, Lc as withRequestOptions, Li as newIdentityBinding, Lr as readLobby, M as entryParams, Ma as replySnippet, Mc as normalizeRelayUrl, Mi as IdentityExchange, Mr as newerHead, Ms as setUpdateFrame, N as knockIdentity, Na as BUTTON_ID, Nr as pickHubs, Ns as signTombstone, O as encodeCommunityLink, Oa as validReactionNumber, Oc as turnName, Oo as encodeControl, Or as lobbyKeys, P as knockRecords, Pc as traceLink, Pi as emptyIdentityLedger, Pr as rankHubs, Ps as verifySetUpdate, Qa as appFingerprint, Qc as fromBase64Url, Qi as dhtFileId, Qn as pairedMessageFrame, Qo as HANDOFF_VERIFIED, Qr as pinIsNewer, Qs as OwnInviteError, Qt as ONCHAIN_PROVIDER, R as GroupWakeLimiter, Ra as STATUS_CARD_LIMITS, Rc as PacketTooLargeError, Ri as receivedIdentityStatus, Ro as emptyLinkRecords, Rr as shouldBeHub, S as sealGroupTyping, Sa as ReactionWindow, Sc as firstDeviceSetSecret, Si as newMailbox, So as GhostlyHttpError, Sr as pickMeshHubs, Ss as readHandoffHave, T as GROUP_READ_NOTE_COMMUNITY, Ta as reactionEmoji, Tc as signTurnPacket, Ti as DHT_TEXT_CAPABILITY, To as webLocalFetch, Tr as beaconRecords, Ts as readHandoffTurnFrame, U as GROUP_READ_NOTE, Ua as sameButtonText, Ui as validExternalKey, Un as cashuRequestPayload, Uo as HANDOFF_CANCEL, Us as enrollGrantFrame, V as parseGroupWakeFrame, Vi as PeerProofs, Vn as decodeBolt11, Vo as serviceIdFromName, Vr as transferEnded, Vs as enrollDigits, W as GroupSession, Wa as statusCardText, Wc as createIdentity, Wi as TRANSPORTS, Wn as fedimintRequestPayload, Wo as HANDOFF_DONE, Ws as enrollHelloFrame, Wt as didDhtDocument, X as mentionsMember, Xc as sanitizeAvatar, Xi as peekDhtMailbox, Xn as INVITE_TAKEN, Xo as HANDOFF_REQUEST, Xr as mayPin, Xs as deviceInviteJoinerParams, Xt as ETHEREUM_USDT, Y as parseCommunityEdit, Ya as shownTime, Yi as DHT_MESSAGE_TTL, Yn as GhostLink, Yo as HANDOFF_PIECE_BYTES, Yr as PIN_LIMITS, Ys as createDeviceInvite, Yt as signDidDhtPacket, Z as groupName, Za as APP_STATEMENT_LIMITS, Zc as bytesEqual, Zi as DHT_FILE_CAPABILITY, Zn as LIVENESS_PING_MS, Zo as HANDOFF_TIMINGS, Zr as nextPinNumber, Zs as readDeviceInvite, Zt as EVM_TEST_CHAINS, _ as readAppBundle, _a as forwardedAgain, _c as TURN_SETTLE_MS, _i as HOLD_CAPABILITY, _o as DirectPathWatch, _r as groupSignalFrame, _s as handoffVersions, _t as isAtprotoDid, a as planAppUpdate, ac as ENROLL_CAPABILITY, ai as parseWakeFrame, an as assertTokenUnits, as as handoffContext, b as groupTypingEpoch, ba as GROUP_REACTION_FRAME, bc as TURN_LAST_SEQUENCE, bi as HoldKeys, bo as RELAY_POLL_INTERVALS, br as mayBeHub, bs as readHandoffCancel, ca as fileMessageText, cc as deviceEchoNonce, ci as checkPushEndpoint, cn as validatePaymentTarget, cr as readGroupFileMeta, cs as handoffDigest, da as parseVideoMeta, dc as devicePingFrame, di as isAppVersion, dn as SPARK_PROVIDER, ds as handoffManifestFrames, ec as decodeInviteCode, ei as readPin, el as toBase64Url, en as SEPOLIA_TEST_USDT, er as engineText, es as handoffAttemptAllowed, f as appDigest, fa as EDIT_BUFFER_MS, fc as newDeviceSetSecret, fi as APPS_CAPABILITY, fn as sparkAddressKind, fo as MEMBER_KEY, fr as CarriedTransport, fs as handoffOfferFrame, g as isAppUrl, ga as parseLinkPreview, gc as TURN_RAISE_READ_TIMEOUT_MS, gi as UPGRADE_CAPABILITY, gn as isPubkyKey, go as edgeParams, gs as handoffStreamKey, ha as dhtEditId, hi as STATUS_CARD_CAPABILITY, ho as verifyCommitSignature, hs as handoffRetryAfter, i as checkAppBeforeRun, ia as sanitizeMime, ic as DEVICES_CAPABILITY, ii as newWakeToken, in as WALLET_NETWORKS, is as handoffCancelFrame, j as encodeGroupEntryLink, ja as readReply, jc as currentRelays, jr as mergeLobby, js as setAckFrame, ka as wireReaction, kc as DEFAULT_RELAYS, kr as lobbyRecords, l as APP_BUNDLE_LIMITS, la as parseVoiceMeta, lc as deviceKeyZ32, li as vapidKeysMatch, ln as walletNetworkOf, lo as readCanonicalJson, ls as handoffEphemeral, ma as RateWindow, mc as webCryptoSigner, mi as EDIT_CAPABILITY, mn as isBitcoinAddress, mo as rosterHas, ms as handoffReleaseFrame, n as appStoreDecision, na as safeBlobType, ni as WakeLimiter, nl as utf8Decode, nn as TEST_USDT_FAUCET_AMOUNT, no as isAppRef, ns as handoffAttemptSucceeded, oc as HANDOFF_CAPABILITY, oi as relayRequest, on as assertWholeSats, or as groupFileFallback, os as handoffContextProof, p as appViewOf, pa as EDIT_RATE_WINDOW_MS, pc as seedSigner, pi as BUTTONS_CAPABILITY, pn as sparkInvoiceDetails, po as commitHash, ps as handoffPakeFrame, q as GROUP_EDIT_TEXT_BYTES, qa as heardTime, qc as publicKeyFromZ32, qn as parseCashuRequestPayload, qo as HANDOFF_OFFER, qr as typingActivity, qs as readEnrollHello, qt as encodeDidDhtPacket, r as appUpdateDecision, ra as sanitizeFileName, rc as inviteOwnership, ri as deviceWakeRequest, rl as utf8Encode, rn as USDT_PUBLIC_RPC, ro as readAppRevocation, rs as handoffBusyFrame, s as readAppStore, sc as deviceEchoFrame, si as wakeRequest, sn as formatPaymentAmount, so as canonicalJson, ss as handoffDataFrame, t as appRevocationFor, ta as formatFileSize, tc as encodeInviteCode, ti as WAKE_CALL_INTERVAL_MS, tl as toZ32, to as isAppKey, tr as GROUP_FILE_LIMITS, ts as handoffAttemptFailed, u as APP_BUNDLE_MAGIC, uc as deviceLinkPairing, ui as chatAppId, un as SPARK_NETWORKS, uo as GROUP_MEMBER_CAP, us as handoffHelloFrame, va as readForwarded, vc as classifyTurnRead, vi as HOLD_DATED_BACK_MS, vo as DHT_POLL_INTERVALS, vr as readGroupSignal, vs as newHandoffId, vt as normalizeAtprotoHandle, w as CommunitySession, wa as queueReaction, wc as readTurnPacket, wi as CapsExchange, wo as parseLocalTarget, wr as beaconKeys, ws as readHandoffPake, x as openGroupTyping, xa as REACTION_LIMITS, xc as TURN_MAX, xi as HoldRefusedError, xo as presenceSeenAt, xr as meshHubs, xs as readHandoffDone, y as GroupTypingBoard, ya as COMMUNITY_REACTION_FRAME, yc as turnPutSummary, yi as HOLD_LIMITS, yr as MESH_HUBS, ys as readHandoffAck, z as groupWakeFrame, zi as revokesIdentity, zo as HTTP_SERVICE_PROTO, zr as ChatFiles, zs as enrollCancelFrame } from "./src-aKGjb_yz.js";
2
+ import { $ as groupName, $c as sanitizeAvatar, $i as peekDhtMailbox, $o as HANDOFF_REQUEST, $r as isAppVersion, $s as deviceInviteJoinerParams, $t as EVM_TEST_CHAINS, A as encodeCommunityLink, Aa as readReaction, Ac as turnKeys, Ai as dialDescriptors, An as ENDPOINT, Ao as decodeControl, Ar as transferStalled, As as sealHandoffPiece, B as GroupWakeLimiter, Bc as withRequestOptions, Bi as newIdentityBinding, Bn as pairedMessageFrame, Br as pinIsNewer, C as openGroupTyping, Ca as GROUP_REACTION_FRAME, Cc as TURN_LAST_SEQUENCE, Co as RELAY_POLL_INTERVALS, Cr as readBeacon, Cs as readHandoffCancel, D as GROUP_READ_NOTE_COMMUNITY, Da as queueReaction, Dc as readTurnPacket, Di as CapsExchange, Do as parseLocalTarget, Dr as ChatFiles, Ds as readHandoffPake, E as CommunitySession, Ea as nextReactionNumber, Ec as nextTurnPosition, Eo as formatLocalTarget, Er as shouldBeHub, Es as readHandoffHello, F as knockIdentity, Fa as replySnippet, Fc as normalizeRelayUrl, Fi as IdentityExchange, Fn as parseCashuRequestPayload, Fr as typingActivity, Fs as setUpdateFrame, G as GROUP_READ_NOTE, Ga as checkStatusCard, Gi as emptyProofLedger, Go as HANDOFF_ACK, Gr as deviceWakeRequest, Gs as enrollDoneFrame, H as groupWakes, Hi as revokesIdentity, Hn as engineText, Ho as HTTP_SERVICE_PROTO, Hr as readPin, Hs as enrollCancelFrame, I as knockRecords, Ia as BUTTON_ID, In as parseFedimintRequestPayload, Is as signTombstone, Ja as arrivalKey, Jc as identityFromSeed, Ji as automaticTransport, Jn as groupFileFallback, Jo as HANDOFF_HAVE, Jr as relayRequest, Js as enrollProofFrame, Jt as encodeDidDhtDeactivated, K as GroupSession, Ka as sameButtonText, Ki as validExternalKey, Ko as HANDOFF_CANCEL, Kr as newWakeToken, Ks as enrollGrantFrame, Kt as didDhtDocument, L as mergeKnocks, Lc as traceLink, Li as emptyIdentityLedger, Ln as GhostLink, Lr as PIN_LIMITS, Ls as verifySetUpdate, M as decodeGroupEntryLink, Ma as wireReaction, Mc as DEFAULT_RELAYS, Mn as fedimintRequestPayload, N as encodeGroupEntryLink, Na as pairedReplyAuthor, Nc as RelayTransport, Ni as IDENTITY_REVOCATION_LABEL, Nn as findEndpoint, No as RTC_CONFIG, Nr as TypingSender, Ns as readSetUpdate, O as communityHasFrame, Oa as reactionEmoji, Oc as signTurnPacket, Oi as DHT_TEXT_CAPABILITY, Oo as webLocalFetch, Or as FILE_LIMITS, Os as readHandoffTurnFrame, P as entryParams, Pa as readReply, Pc as currentRelays, Pn as isFederationId, Ps as setAckFrame, Q as mentionsMember, Qa as shownTime, Qi as DHT_MESSAGE_TTL, Qo as HANDOFF_PIECE_BYTES, Qr as chatAppId, Qs as createDeviceInvite, Qt as ETHEREUM_USDT, R as readKnocks, Rc as budgetRetryMs, Ri as identityRevocationValue, Rn as INVITE_TAKEN, Rr as mayPin, S as groupTypingEpoch, Sa as COMMUNITY_REACTION_FRAME, Sc as turnPutSummary, Sr as rankHubs, Ss as readHandoffAck, T as COMMUNITY_LIMITS, Ta as ReactionWindow, Tc as firstDeviceSetSecret, To as GhostlyHttpError, Tr as readLobby, Ts as readHandoffHave, U as parseGroupWakeFrame, Uc as createRelayPayload, Ui as lastSharedWithMe, Un as GROUP_FILE_LIMITS, Uo as LEGACY_SERVICES, Ur as WAKE_CALL_INTERVAL_MS, Us as enrollCancelReason, V as groupWakeFrame, Va as STATUS_CARD_LIMITS, Vc as PacketTooLargeError, Vi as receivedIdentityStatus, Vn as engineError, Vo as emptyLinkRecords, Vr as pinNumberHolds, Wi as PeerProofs, Wo as serviceIdFromName, Wr as WakeLimiter, Ws as enrollDigits, X as carryMentions, Xa as heardTime, Xc as publicKeyFromZ32, Xn as readGroupFileMeta, Xo as HANDOFF_OFFER, Xr as checkPushEndpoint, Xs as readEnrollHello, Y as GROUP_EDIT_TEXT_BYTES, Ya as claimedTime, Yc as identityFromSeedB64, Yo as HANDOFF_HELLO, Yr as wakeRequest, Ys as readEnrollGrant, Yt as encodeDidDhtPacket, Z as parseCommunityEdit, Za as receivedTimestamp, Zc as verify, Zo as HANDOFF_PAKE, Zr as vapidKeysMatch, Zs as verifyEnrollProof, Zt as signDidDhtPacket, _a as RateWindow, _c as webCryptoSigner, _o as rosterHas, _r as lobbyRecords, _s as handoffReleaseFrame, a as planAppUpdate, aa as safeBlobType, ai as HOLD_CAPABILITY, al as utf8Decode, an as USDT_PUBLIC_RPC, ao as isAppRef, ar as groupSignalFrame, as as handoffAttemptSucceeded, ba as forwardedAgain, bc as TURN_SETTLE_MS, bo as DirectPathWatch, br as newerHead, bs as handoffVersions, bt as normalizeAtprotoHandle, cc as ENROLL_CAPABILITY, ci as HoldKeys, cn as assertWholeSats, cr as mayBeHub, cs as handoffContext, d as APP_BUNDLE_LIMITS, da as fileMessageText, dc as deviceEchoNonce, di as newMailbox, dn as walletNetworkOf, dr as COMMUNITY_TOPOLOGY, ds as handoffDigest, ea as DHT_FILE_CAPABILITY, ec as readDeviceInvite, ei as APPS_CAPABILITY, el as bytesEqual, en as ONCHAIN_PROVIDER, eo as APP_STATEMENT_LIMITS, er as CarriedTransport, es as HANDOFF_TIMINGS, f as APP_BUNDLE_MAGIC, fa as parseVoiceMeta, fc as deviceKeyZ32, fi as readManifest, fn as SPARK_NETWORKS, fo as readCanonicalJson, fr as beaconKeys, fs as handoffEphemeral, ga as EDIT_RATE_WINDOW_MS, gc as seedSigner, gn as isBitcoinAddress, go as commitHash, gr as lobbyKeys, gs as handoffPakeFrame, h as appViewOf, ha as EDIT_BUFFER_MS, hc as newDeviceSetSecret, hn as sparkInvoiceDetails, ho as MEMBER_KEY, hr as freshHubs, hs as handoffOfferFrame, i as checkAppBeforeRun, ia as formatFileSize, ic as encodeInviteCode, ii as UPGRADE_CAPABILITY, il as toZ32, in as TEST_USDT_FAUCET_AMOUNT, io as isAppKey, is as handoffAttemptFailed, ja as validReactionNumber, jc as turnName, jn as cashuRequestPayload, jo as encodeControl, k as decodeCommunityLink, ka as reactionIsNewer, kc as signTurnRelease, ki as capsDescriptors, kn as decodeBolt11, ko as LIMITS, kr as transferEnded, ks as readHandoffVerified, l as readAppStoreSteps, lc as HANDOFF_CAPABILITY, li as HoldRefusedError, ln as formatPaymentAmount, lr as meshHubs, ls as handoffContextProof, m as appDigest, ma as parseVideoMeta, mc as devicePingFrame, mn as sparkAddressKind, mr as doorHubs, ms as handoffManifestFrames, n as appStoreDecision, na as parseImageMeta, nc as createChatInvite, ni as EDIT_CAPABILITY, nl as randomBytes, nn as SEPOLIA_TEST_USDT, no as appRef, ns as filePartName, o as APP_STORE_LIMITS, oa as sanitizeFileName, oc as inviteOwnership, oi as HOLD_DATED_BACK_MS, ol as utf8Encode, on as WALLET_NETWORKS, oo as readAppRevocation, or as readGroupSignal, os as handoffBusyFrame, pc as deviceLinkPairing, pn as SPARK_PROVIDER, po as GROUP_MEMBER_CAP, pr as beaconRecords, ps as handoffHelloFrame, qa as statusCardText, qc as createIdentity, qi as TRANSPORTS, qo as HANDOFF_DONE, qr as parseWakeFrame, qs as enrollHelloFrame, qt as didDhtFromPublicKey, r as appUpdateDecision, rc as decodeInviteCode, ri as STATUS_CARD_CAPABILITY, rl as toBase64Url, rs as handoffAttemptAllowed, sa as sanitizeMime, sc as DEVICES_CAPABILITY, si as HOLD_LIMITS, sn as assertTokenUnits, sr as MESH_HUBS, ss as handoffCancelFrame, t as appRevocationFor, ta as dhtFileId, tc as OwnInviteError, ti as BUTTONS_CAPABILITY, tl as fromBase64Url, tn as PaymentPreflightError, to as appFingerprint, ts as HANDOFF_VERIFIED, uc as deviceEchoFrame, ui as isHoldUrl, un as validatePaymentTarget, uo as canonicalJson, ur as pickMeshHubs, us as handoffDataFrame, v as isAppUrl, va as dhtEditId, vn as isPubkyKey, vo as verifyCommitSignature, vr as mergeBeacon, vs as handoffRetryAfter, w as sealGroupTyping, wa as REACTION_LIMITS, wc as TURN_MAX, wo as presenceSeenAt, wr as readBeaconHead, ws as readHandoffDone, x as GroupTypingBoard, xa as readForwarded, xc as classifyTurnRead, xo as DHT_POLL_INTERVALS, xr as pickHubs, xs as newHandoffId, y as readAppBundle, ya as parseLinkPreview, yc as TURN_RAISE_READ_TIMEOUT_MS, yo as edgeParams, yr as mergeLobby, ys as handoffStreamKey, yt as isAtprotoDid, zc as isDiscoveryBudgetError, zi as identityStatement, zn as LIVENESS_PING_MS, zo as parseCallSignal, zr as nextPinNumber } from "./src-XzcHzneo.js";
3
3
  import { a as clearProfileStores, c as fileStore, d as profileOpenFailure, f as store, i as STORES$1, l as newestWalletTx, m as wrap, n as APP_STORAGE_SCOPE_INDEX, o as databaseName, p as transact, r as CARD_INDEX, s as deleteDatabase, t as APP_STORAGE_CHAT_INDEX, u as openDb } from "./idb-CTF6LF5B.js";
4
4
  import { a as fileBytesOf, c as removeFileBytes, i as fileBytes, l as FILE_BYTES_STEP, n as blobDigest } from "./fileBytes-BcYZhNgF.js";
5
5
  import { $ as BACKUP_MEDIA_TYPE, A as requireTurn, At as paymentWireId, B as networkLabel, C as breezDatabasesFor, Ct as storedGroupFiles, D as sourceKey, Dt as replyRef, E as ProviderSources, Et as receivedPairedReply, F as sealSeed, Ft as streamStored, G as BITCOIN_INVOICE_ON_TESTNET, H as cashuMint, I as unsealSeed, J as fakesLightning, K as DEFAULT_MINTS, L as ModeChanged, M as setSingleWriterGate, Mt as readStored$1, N as intentRepository, Nt as removeStored, O as awayFrom, Ot as replyTo, P as newDeviceKey, Pt as storedSize, Q as paysItsOwnInvoices, R as ModeGate, S as breezDatabaseInUse, St as groupOfFile, T as lightningPaying, Tt as pairedWireReply, V as CASHU_MINT_SOURCE, W as WALLET_TYPES, X as isWorthlessMint, Y as isTestMint, Z as mintNetwork, _ as LightningCards, _t as verifyIdentity, a as irohRelayProblem, at as reserveAdapterIds, b as normalizePhrase, bt as groupFileId, c as createFailure, ct as SourceConfigError, d as paymentNetwork, dt as networkMode, et as MAX_BACKUP_BYTES, f as paymentNetworksOf, ft as redact, g as CASHU_CARD, gt as boundedIdentityFetch, ht as setOwnDidSource, i as createIrohWebEndpoint, it as registeredOnchainProviders, j as setAwayWallets, jt as FileAppender, k as refuseAway, kt as paymentAlias, l as createTiming, lt as SourceUnreachableError, mt as resolveAtprotoDid, n as loadFedimintSdk, nt as onAdaptersChanged, o as WALLET_NAMES, ot as NothingSpentError, p as walletInstances, pt as assertPublicServer, q as TEST_MINT, r as DEFAULT_IROH_RELAYS, rt as registeredLightningProviders, s as assertConfirmedReal, st as PROVIDER_PLATFORMS, t as fedimintDatabase, tt as identityProviders, u as crossNetwork, ut as isNothingSpentError, v as cardsKey, vt as chosenResolver, w as dropBreezDatabase, wt as groupReply, x as breezDatabase, xt as groupFileText, y as isRecoveryPhrase, yt as GroupFileDesk, z as WrongNetworkError } from "../ghostly.mjs";
@@ -180,7 +180,7 @@ async function migrateWalletNetworks(now = Date.now()) {
180
180
  //#endregion
181
181
  //#region ../browser/src/engine/paymentAdapters/usdtWallet.ts
182
182
  /** The Ethereum libraries, a large part of the app: loaded once a USDT wallet is made, opened or restored, not with the engine. */
183
- var loadUsdt = () => import("./usdt-CUfucwZf.js");
183
+ var loadUsdt = () => import("./usdt-Deo3g0Nq.js");
184
184
  /** Every new profile starts with this wallet: an address to receive on, nothing to set up. */
185
185
  var DEFAULT_USDT = {
186
186
  network: "ethereum",
@@ -642,7 +642,9 @@ async function restoreArkDatabase(walletId, snapshot) {
642
642
  //#endregion
643
643
  //#region ../browser/src/engine/paymentAdapters/arkWallet.ts
644
644
  /** The Ark SDK, a large part of the app: loaded once an Ark wallet is made, opened or restored, not with the engine. */
645
- var loadArkade = () => import("./arkade-CtCpFaF7.js");
645
+ var loadArkade = () => import("./arkade-_gS9wShF.js");
646
+ /** How long a Testnet wallet about to make way for another waits for its server to say what it holds. */
647
+ var RETIRE_READ_MS = 1e4;
646
648
  /** Every new profile starts with this wallet: nothing to set up before receiving. */
647
649
  var DEFAULT_ARK = {
648
650
  network: "bitcoin",
@@ -1048,10 +1050,23 @@ var ArkWallet = class {
1048
1050
  async retirable(refusal) {
1049
1051
  const saved = this.saved;
1050
1052
  if ((await wrap((await store(STORES$1.intents, "readonly")).getAll())).some((i) => this.ours(i))) throw new Error(refusal);
1051
- if (this.adapter && await this.gate.within(this.adapter.balance()) > 0) throw new Error(refusal);
1053
+ if (this.adapter && await this.gate.within(this.held(this.adapter)) > 0) throw new Error(refusal);
1052
1054
  await this.lock();
1053
1055
  return saved;
1054
1056
  }
1057
+ /**
1058
+ * What the wallet holds, asked of its server. A test network's server that stopped answering must not hold the
1059
+ * wallet on it (one that never answered does not either): past RETIRE_READ_MS the last balance read stands.
1060
+ * Mainnet waits for its server's own word.
1061
+ */
1062
+ held(adapter) {
1063
+ const asked = adapter.balance();
1064
+ if (this.network !== "testnet") return asked;
1065
+ return new Promise((resolve, reject) => {
1066
+ const timer = setTimeout(() => resolve(this.view.balance), RETIRE_READ_MS);
1067
+ asked.then(resolve, reject).finally(() => clearTimeout(timer));
1068
+ });
1069
+ }
1055
1070
  /** A payment of this wallet's network (the other network's wallet has its own). */
1056
1071
  ours(intent) {
1057
1072
  return intent.review.method === "arkade" && arkMode(intent.review.network) === this.network;
@@ -1876,13 +1891,24 @@ var BarkWallet = class {
1876
1891
  async retirable(refusal) {
1877
1892
  const saved = this.saved;
1878
1893
  if ((await intentRepository.list()).some((i) => this.ours(i))) throw new Error(refusal);
1879
- if (this.adapter) {
1880
- const b = await this.gate.within(this.adapter.balance());
1881
- if (b.spendableSats + b.pendingInRoundSats + b.pendingBoardSats + b.pendingExitSats + b.pendingLightningSendSats + b.claimableLightningReceiveSats > 0) throw new Error(refusal);
1882
- }
1894
+ if (this.adapter && await this.gate.within(this.held(this.adapter)) > 0) throw new Error(refusal);
1883
1895
  await this.lock();
1884
1896
  return saved;
1885
1897
  }
1898
+ /**
1899
+ * What the wallet holds, asked of its server: spendable, on its way in and on its way out. A test network's server
1900
+ * that stopped answering must not hold the wallet on it (one that never answered does not either): past
1901
+ * RETIRE_READ_MS the last reading stands, whose three sums are those same six parts. Mainnet waits for its
1902
+ * server's own word.
1903
+ */
1904
+ held(adapter) {
1905
+ const asked = adapter.balance().then((b) => b.spendableSats + b.pendingInRoundSats + b.pendingBoardSats + b.pendingExitSats + b.pendingLightningSendSats + b.claimableLightningReceiveSats);
1906
+ if (this.network !== "testnet") return asked;
1907
+ return new Promise((resolve, reject) => {
1908
+ const timer = setTimeout(() => resolve(this.view.balance + (this.view.pending ?? 0) + (this.view.exiting ?? 0)), RETIRE_READ_MS);
1909
+ asked.then(resolve, reject).finally(() => clearTimeout(timer));
1910
+ });
1911
+ }
1886
1912
  /** A payment of this wallet's network (the other network's wallet has its own). */
1887
1913
  ours(intent) {
1888
1914
  return intent.review.method === "bark" && barkMode(intent.review.network) === this.network;
@@ -11145,6 +11171,11 @@ var APP_CHECK_TIMINGS = {
11145
11171
  };
11146
11172
  /** Revocations are checked this long at a time before the page's thread is let go. */
11147
11173
  var REVOCATIONS_SLICE_MS = 10;
11174
+ /**
11175
+ * Revocations of store indexes whose signature was checked, kept by `appRevocationId` so an index that changes around
11176
+ * them does not have each checked again (the one read longest ago dropped first): four indexes' worth, about 1 MiB.
11177
+ */
11178
+ var STORE_REVOCATIONS_KEPT = 4 * APP_STORE_LIMITS.revoked;
11148
11179
  /** `ghostly-revoke.json` files whose result is kept, so an unchanged one is not checked again (the oldest dropped first). */
11149
11180
  var REVOCATION_FILES_KEPT = 64;
11150
11181
  /** A fetched bundle kept for its install screen: at most this many, for this long. */
@@ -11231,7 +11262,7 @@ var Apps = class {
11231
11262
  /** Reads of a bundle under way, by digest: icons drawn at once share one read. */
11232
11263
  verifying = /* @__PURE__ */ new Map();
11233
11264
  /**
11234
- * The `icon.png` of each checked bundle, by digest (at most 256 KiB, or null for none): every icon shown on the Apps
11265
+ * The `icon.png` of each checked bundle, by digest (at most 256 KiB and 1024 × 1024 pixels, or null for none): every icon shown on the Apps
11235
11266
  * page, in a chat's cards and in the composer's Apps is read without the whole bundle again.
11236
11267
  */
11237
11268
  icons = /* @__PURE__ */ new Map();
@@ -11244,6 +11275,13 @@ var Apps = class {
11244
11275
  checking = null;
11245
11276
  /** Runs the person started with Run anyway (`<ref> <digest>`), for this engine's life: a removal does not cut them off. */
11246
11277
  anyway = /* @__PURE__ */ new Set();
11278
+ /** The apps started here (`entry`), for this engine's life: one of them uninstalled since may still have a frame open. */
11279
+ ran = /* @__PURE__ */ new Set();
11280
+ /**
11281
+ * The versions started here (`entry`), by reference: digest to `sequence`, for this engine's life. A frame goes on
11282
+ * running the version it started with whatever is installed since, so that version is the one judged when it asks.
11283
+ */
11284
+ started = /* @__PURE__ */ new Map();
11247
11285
  views = null;
11248
11286
  held = /* @__PURE__ */ new Map();
11249
11287
  heldStores = null;
@@ -11254,6 +11292,19 @@ var Apps = class {
11254
11292
  listingMisses = /* @__PURE__ */ new Map();
11255
11293
  /** `ghostly-revoke.json` files checked, by the SHA-256 of their bytes: their revocations, or null when refused. */
11256
11294
  revocationFiles = /* @__PURE__ */ new Map();
11295
+ /** Revocations read in a store's index and verified, by `appRevocationId`: at most `STORE_REVOCATIONS_KEPT`. */
11296
+ storeRevocations = /* @__PURE__ */ new Set();
11297
+ checkedRevocations = {
11298
+ has: (id) => {
11299
+ if (!this.storeRevocations.delete(id)) return false;
11300
+ this.storeRevocations.add(id);
11301
+ return true;
11302
+ },
11303
+ add: (id) => {
11304
+ this.storeRevocations.add(id);
11305
+ if (this.storeRevocations.size > STORE_REVOCATIONS_KEPT) this.storeRevocations.delete(this.storeRevocations.values().next().value);
11306
+ }
11307
+ };
11257
11308
  stopped = false;
11258
11309
  constructor(host) {
11259
11310
  this.host = host;
@@ -11337,22 +11388,48 @@ var Apps = class {
11337
11388
  * a store of the person's removed (unless this run was started with Run anyway), is cut off with `stopped`, whether
11338
11389
  * or not the page stops its frame.
11339
11390
  */
11340
- async stillRunnable(app) {
11341
- const run = this.runStatus(app, await this.heldViews(), app.revocations);
11391
+ async stillRunnable(app, digest) {
11392
+ const version = this.versionRunning(app, digest);
11393
+ const run = this.runStatus(version, await this.heldViews(), app.revocations);
11342
11394
  if (run.status === "revoked") fail("stopped", "Its publisher revoked this version");
11343
- if (run.status === "removed" && !this.anyway.has(`${app.ref} ${app.digest}`)) fail("stopped", `Removed by ${run.by[0].name}: ${run.by[0].reason}`);
11395
+ if (run.status === "removed" && !this.anyway.has(`${version.ref} ${version.digest}`)) fail("stopped", `Removed by ${run.by[0].name}: ${run.by[0].reason}`);
11344
11396
  return app;
11345
11397
  }
11346
- async runningOrFail(ref) {
11347
- return this.stillRunnable(await this.installedOrFail(ref));
11398
+ /**
11399
+ * The version a frame runs: the one it names by `digest` (what `entry` gave it), which an update installed since does
11400
+ * not change. None named: the installed one. One never started here does not run.
11401
+ */
11402
+ versionRunning(app, digest) {
11403
+ if (digest === void 0 || digest === app.digest) return app;
11404
+ const sequence = typeof digest === "string" ? this.started.get(app.ref)?.get(digest) : void 0;
11405
+ if (sequence === void 0) fail("stopped", "This version is not running");
11406
+ return {
11407
+ ref: app.ref,
11408
+ sequence,
11409
+ digest
11410
+ };
11411
+ }
11412
+ /** The versions started here that are not the installed one any more. */
11413
+ startedBefore(app) {
11414
+ return [...this.started.get(app.ref) ?? []].filter(([digest]) => digest !== app.digest).map(([digest, sequence]) => ({
11415
+ ref: app.ref,
11416
+ sequence,
11417
+ digest
11418
+ }));
11419
+ }
11420
+ async runningOrFail(ref, digest) {
11421
+ return this.stillRunnable(await this.installedOrFail(ref), digest);
11348
11422
  }
11349
11423
  /**
11350
11424
  * Before an app's frames go to a contact (`appOpen`, `appSend`): refused with `stopped` as its storage is. A reference
11351
- * not installed here (a bot that speaks an app's messages) is not this store's to refuse.
11425
+ * not installed here (a bot that speaks an app's messages) is not this store's to refuse, unless it ran here and was
11426
+ * uninstalled since: the person removed it, so a frame of it still open says nothing more (`not-installed`). `digest`:
11427
+ * the version the frame runs (`versionRunning`).
11352
11428
  */
11353
- async chatRunnable({ ref }) {
11429
+ async chatRunnable({ ref, digest }) {
11354
11430
  const app = isAppRef(ref) ? await this.app(ref) : void 0;
11355
- if (app) await this.stillRunnable(app);
11431
+ if (!app && this.ran.has(ref)) fail("not-installed", "This app is not installed");
11432
+ if (app) await this.stillRunnable(app, digest);
11356
11433
  }
11357
11434
  async putApp(app) {
11358
11435
  try {
@@ -11543,7 +11620,10 @@ var Apps = class {
11543
11620
  }
11544
11621
  /**
11545
11622
  * Reads the index at `url`. With the index held for this store, bytes equal to it (by digest) are the held index: its
11546
- * signature and every revocation in it were checked when it was read, so only its expiry is taken again.
11623
+ * signature and every revocation in it were checked when it was read, so only its expiry is taken again. Any other
11624
+ * index is read with a pause every few ms (the engine shares the page's thread, and an index holds up to 4096
11625
+ * listings and 4096 revocations, each of these a signature check), and a revocation checked in an earlier read, of
11626
+ * this store or another, a preview included, is not checked again.
11547
11627
  */
11548
11628
  async readStoreAt(url, heldKey, held) {
11549
11629
  const [indexBytes, sigBytes] = await Promise.all([this.host.fetch(url, { maxBytes: APP_FETCH_LIMITS.storeIndexBytes }), this.host.fetch(besideUrl(url, "ghostly-store.sig"), { maxBytes: APP_FETCH_LIMITS.sigBytes })]);
@@ -11555,7 +11635,16 @@ var Apps = class {
11555
11635
  expired: held.index.expires < this.nowS()
11556
11636
  }
11557
11637
  };
11558
- return readAppStore(indexBytes, sigBytes, this.nowS(), heldKey);
11638
+ const steps = readAppStoreSteps(indexBytes, sigBytes, this.nowS(), heldKey, this.checkedRevocations);
11639
+ let since = performance.now();
11640
+ for (;;) {
11641
+ const step = steps.next();
11642
+ if (step.done) return step.value;
11643
+ if (performance.now() - since >= REVOCATIONS_SLICE_MS) {
11644
+ await new Promise((resolve) => setTimeout(resolve, 0));
11645
+ since = performance.now();
11646
+ }
11647
+ }
11559
11648
  }
11560
11649
  /**
11561
11650
  * The revocations its publisher signed, read beside each URL (`ghostly-revoke.json`); none there is not an error.
@@ -12142,7 +12231,11 @@ var Apps = class {
12142
12231
  }
12143
12232
  const after = await this.readRevocations(app.ref, [app.from, ...app.manifest.sources ?? []], revokeRead);
12144
12233
  const revocations = before || after ? [...before ?? [], ...after ?? []] : null;
12145
- const versions = [app, ...app.pending ? [versionOf(app.pending.manifest, app.pending.digest)] : []];
12234
+ const versions = [
12235
+ app,
12236
+ ...app.pending ? [versionOf(app.pending.manifest, app.pending.digest)] : [],
12237
+ ...this.startedBefore(app)
12238
+ ];
12146
12239
  const stays = (app.revocations ?? []).filter((r) => !revocations || versions.some((v) => appRevocationFor(v, [r])));
12147
12240
  const merged = new Map([...stays, ...revocations ?? []].map((r) => [canonicalJson(r), r]));
12148
12241
  if ((await this.app(app.ref))?.store === void 0 && app.store !== void 0) {
@@ -12233,6 +12326,9 @@ var Apps = class {
12233
12326
  if (run.status === "removed") this.anyway.add(`${app.ref} ${app.digest}`);
12234
12327
  const bundle = await this.verifiedBundle(app);
12235
12328
  const entry = bundle.files.get(bundle.manifest.entry) ?? fail("damaged", "The entry is missing");
12329
+ this.ran.add(app.ref);
12330
+ const versions = this.started.get(app.ref) ?? /* @__PURE__ */ new Map();
12331
+ this.started.set(app.ref, versions.set(app.digest, app.sequence));
12236
12332
  return {
12237
12333
  ref: app.ref,
12238
12334
  digest: app.digest,
@@ -12243,9 +12339,10 @@ var Apps = class {
12243
12339
  entry: utf8Decode(entry)
12244
12340
  };
12245
12341
  }
12246
- /** A file of the installed bundle, for the broker's `file` (`ghostly.file(path)`). */
12247
- async file({ ref, path }) {
12248
- const app = path === "icon.png" ? await this.installedOrFail(ref) : await this.runningOrFail(ref);
12342
+ /** A file of the installed bundle, for the broker's `file` (`ghostly.file(path)`). `digest`: the version the frame runs. */
12343
+ async file({ ref, path, digest }) {
12344
+ const app = path === "icon.png" ? await this.installedOrFail(ref) : await this.runningOrFail(ref, digest);
12345
+ if (path !== "icon.png" && digest !== void 0 && digest !== app.digest) fail("no-file", "No such file in this app");
12249
12346
  const icon = path === "icon.png" ? this.icons.get(app.digest) : void 0;
12250
12347
  if (icon) return icon.slice();
12251
12348
  if (icon === null) fail("no-file", "No such file in this app");
@@ -12256,6 +12353,10 @@ var Apps = class {
12256
12353
  async view(app, stores) {
12257
12354
  const { manifest } = app;
12258
12355
  const listedBy = this.listedBy(app.ref, stores);
12356
+ const stopped = this.startedBefore(app).map((v) => ({
12357
+ digest: v.digest,
12358
+ run: this.runStatus(v, this.storeViews(stores), app.revocations)
12359
+ })).filter((v) => v.run.status !== "ok");
12259
12360
  return {
12260
12361
  ref: app.ref,
12261
12362
  name: app.name,
@@ -12285,7 +12386,8 @@ var Apps = class {
12285
12386
  sequence: app.equivocation.sequence,
12286
12387
  at: app.equivocation.at
12287
12388
  } },
12288
- ...app.checkedAt !== void 0 && { checkedAt: app.checkedAt }
12389
+ ...app.checkedAt !== void 0 && { checkedAt: app.checkedAt },
12390
+ ...stopped.length && { stopped }
12289
12391
  };
12290
12392
  }
12291
12393
  async list() {
@@ -12307,6 +12409,7 @@ var Apps = class {
12307
12409
  this.held.delete(app.ref);
12308
12410
  }
12309
12411
  for (const key of [...this.usage.keys()]) if (key.startsWith(`${app.ref}\0`)) this.usage.delete(key);
12412
+ this.started.delete(app.ref);
12310
12413
  await this.removeBundle(app.bytes, app.digest);
12311
12414
  if (app.pending) await this.removeBundle(app.pending.bytes, app.pending.digest);
12312
12415
  }
@@ -12337,8 +12440,8 @@ var Apps = class {
12337
12440
  }
12338
12441
  return used;
12339
12442
  }
12340
- async storageGet({ ref, scope, key }) {
12341
- const app = await this.runningOrFail(ref);
12443
+ async storageGet({ ref, scope, key, digest }) {
12444
+ const app = await this.runningOrFail(ref, digest);
12342
12445
  const row = await wrap((await store(STORES$1.appStorage, "readonly")).get([
12343
12446
  app.ref,
12344
12447
  this.scopeOf(scope),
@@ -12347,8 +12450,8 @@ var Apps = class {
12347
12450
  return row ? { value: JSON.parse(row.value) } : null;
12348
12451
  }
12349
12452
  /** Keeps a JSON value under a key: at most 64 KiB, and 5 MiB for every key of this app in this scope. */
12350
- async storageSet({ ref, scope, key, value }) {
12351
- const app = await this.runningOrFail(ref);
12453
+ async storageSet({ ref, scope, key, value, digest }) {
12454
+ const app = await this.runningOrFail(ref, digest);
12352
12455
  const where = this.scopeOf(scope), name = this.keyOf(key);
12353
12456
  let text;
12354
12457
  try {
@@ -12384,8 +12487,8 @@ var Apps = class {
12384
12487
  this.usage.set(`${app.ref}\0${where}`, next);
12385
12488
  });
12386
12489
  }
12387
- async storageDelete({ ref, scope, key }) {
12388
- const app = await this.runningOrFail(ref);
12490
+ async storageDelete({ ref, scope, key, digest }) {
12491
+ const app = await this.runningOrFail(ref, digest);
12389
12492
  const where = this.scopeOf(scope), name = this.keyOf(key);
12390
12493
  await this.serial(`${app.ref}\0${where}`, async () => {
12391
12494
  const used = await this.usageOf(app.ref, where);
@@ -12403,8 +12506,8 @@ var Apps = class {
12403
12506
  this.usage.set(`${app.ref}\0${where}`, used - old.size);
12404
12507
  });
12405
12508
  }
12406
- async storageKeys({ ref, scope }) {
12407
- const app = await this.runningOrFail(ref);
12509
+ async storageKeys({ ref, scope, digest }) {
12510
+ const app = await this.runningOrFail(ref, digest);
12408
12511
  return (await wrap((await store(STORES$1.appStorage, "readonly")).getAllKeys(rowsOf(app.ref, this.scopeOf(scope))))).map((k) => k[2]);
12409
12512
  }
12410
12513
  /** An app's storage in every scope, as export files (WISP 1200 § Where installed apps live), offered before an uninstall. */
@@ -21699,13 +21802,16 @@ function pinOfFormerMember(pin, inRoster) {
21699
21802
  function mayPinIn(group) {
21700
21803
  return !!group?.myKey && group.status === "active" && mayPin(group.profile, group.myKey, group.members.find((m) => m.role === "admin")?.key);
21701
21804
  }
21702
- /** The row a pin names, when it is here: the row of that id (a group's), or of `peer_`/`me_` and that id, or any other. */
21805
+ /** The row a pin names, when it is here: the row of that id (a group's), or of `peer_`/`me_` and that id, or any other of a chat's. */
21703
21806
  async function pinnedRow(host, chat, id) {
21704
21807
  if (!id) return void 0;
21705
21808
  const group = isGroup(chat);
21706
- if (host.message) for (const rowId of group ? [id] : [`peer_${id}`, `me_${id}`]) {
21707
- const row = await host.message(chat, rowId);
21708
- if (row && replyRef(row, group) === id) return row;
21809
+ if (host.message) {
21810
+ for (const rowId of group ? [id] : [`peer_${id}`, `me_${id}`]) {
21811
+ const row = await host.message(chat, rowId);
21812
+ if (row && replyRef(row, group) === id) return row;
21813
+ }
21814
+ if (group) return void 0;
21709
21815
  }
21710
21816
  return reactionTarget(await host.messages(chat), id, group);
21711
21817
  }
@@ -21930,6 +22036,17 @@ var FAREWELL_RETRY_MS = 6e4;
21930
22036
  var FAREWELL_TOLD_MS = 6e5;
21931
22037
  var FAREWELL_SAY_MS = 1e4;
21932
22038
  /**
22039
+ * A key asking in my lobby that no commit I hold names may be a member let in by a commit I have not heard yet: a hub
22040
+ * keeps an edge to it for a moment, on which it says where it is and takes commits (`wantAhead`). At most `AHEAD_EDGES`
22041
+ * at a time, each awaited `AHEAD_WAIT_MS` and closed `AHEAD_LINGER_MS` after it came up unless a commit named the key
22042
+ * by then; the same key is not waited for again for a while (longer once its edge was up).
22043
+ */
22044
+ var AHEAD_EDGES = 4;
22045
+ var AHEAD_WAIT_MS = 12e4;
22046
+ var AHEAD_LINGER_MS = 2e4;
22047
+ var AHEAD_RETRY_MS = 6e4;
22048
+ var AHEAD_HEARD_MS = 6e5;
22049
+ /**
21933
22050
  * A key asking from before the commit that took it out was there and was sent that commit: not waited for. The two
21934
22051
  * times are two devices' clocks (the asker's, the commit's signer's), so "before" allows this much between them:
21935
22052
  * a returning member whose clock runs behind the admin's is told all the same.
@@ -22401,6 +22518,7 @@ var Communities = class {
22401
22518
  await this.publishBeacon(group.id, live, now, false).catch(() => {});
22402
22519
  }
22403
22520
  live.farewells.clear();
22521
+ live.ahead.clear();
22404
22522
  await this.reconcile(group.id, live, now);
22405
22523
  continue;
22406
22524
  }
@@ -22494,6 +22612,7 @@ var Communities = class {
22494
22612
  if (this.leftItself(live, h.key) ? h.ts - FAREWELL_SKEW_MS > out : h.ts + FAREWELL_SKEW_MS > out) this.wantFarewell(groupId, live, h.key, now);
22495
22613
  }
22496
22614
  this.endFarewells(live, now);
22615
+ this.endAhead(groupId, live, now);
22497
22616
  if (live.hub) {
22498
22617
  const busy = this.admitting(groupId, live, now);
22499
22618
  if (s.state.pendingLeaves.some((r) => r.s !== me && this.recentHub(live, r.s))) live.lobbyBusyUntil = Math.max(live.lobbyBusyUntil, now + this.timings.lobbyBusyMs);
@@ -22572,8 +22691,9 @@ var Communities = class {
22572
22691
  }
22573
22692
  await this.keepLooking(groupId, live, now);
22574
22693
  for (const [key, since] of live.pendingEntries) {
22575
- const linkId = this.host.entries(groupId).get(key);
22576
- if (!(!!linkId && this.host.linkReady(linkId, 2)) && now - since > ENTRY_UNANSWERED_MS) live.pendingEntries.delete(key);
22694
+ const linkId = this.host.entries(groupId).get(key), up = !!linkId && this.host.linkReady(linkId, 2);
22695
+ const elsewhere = rosterHas(s.roster, key) && since < this.addedAt(s, key) + this.timings.knockFallbackMs;
22696
+ if (!up && (elsewhere || now - since > ENTRY_UNANSWERED_MS)) live.pendingEntries.delete(key);
22577
22697
  else if (now - since > ENTRY_TIMEOUT_MS$1) {
22578
22698
  live.pendingEntries.delete(key);
22579
22699
  this.refused.set(key, now + REFUSED_FOR_MS$1);
@@ -22632,7 +22752,7 @@ var Communities = class {
22632
22752
  const edges = this.host.edges(groupId);
22633
22753
  if (now - live.beaconAt <= 2 * this.timings.beaconReadMs) {
22634
22754
  const hubs = new Set(freshHubs(this.hubs(live), now).map((h) => h.key));
22635
- for (const [key, id] of edges) if (!hubs.has(key) && !live.members.has(key) && this.host.linkReady(id, 2) && !this.gone(live, key)) live.members.set(key, now);
22755
+ for (const [key, id] of edges) if (!hubs.has(key) && !live.members.has(key) && this.host.linkReady(id, 2) && rosterHas(live.session.roster, key) && !this.gone(live, key)) live.members.set(key, now);
22636
22756
  for (const key of [...live.members.keys()]) if (hubs.has(key)) live.members.delete(key);
22637
22757
  }
22638
22758
  for (const [key, since] of live.members) {
@@ -22770,16 +22890,21 @@ var Communities = class {
22770
22890
  for (let i = chain.length - 1; i >= 0; i--) if ((chain[i].k === "leave" || chain[i].k === "remove") && chain[i].s === key) return chain[i].k === "leave";
22771
22891
  return false;
22772
22892
  }
22773
- /** A frame goes on an edge to a member; to someone the chain took out, only the commit that took it out. */
22774
- mayHear(live, key, frame) {
22775
- if (!live?.session.wasRemoved(key)) return true;
22893
+ /**
22894
+ * A frame goes on an edge to a member; to someone the chain took out, only the commit that took it out. What the
22895
+ * group says (`said`: a frame of mine for everyone, or one a hub relays) goes to the members of the roster only:
22896
+ * a key no commit names hears none of it, and one let in on a branch that lost hears the commits.
22897
+ */
22898
+ mayHear(live, key, frame, said = false) {
22776
22899
  const f = frame;
22777
- return f.t === "group-commit" && (f.commit?.k === "remove" || f.commit?.k === "leave") && f.commit.s === key;
22900
+ if (live?.session.wasRemoved(key)) return f.t === "group-commit" && (f.commit?.k === "remove" || f.commit?.k === "leave") && f.commit.s === key;
22901
+ if (!live || !said || rosterHas(live.session.roster, key)) return true;
22902
+ return f.t === "group-commit" && live.session.isRecentMember(key);
22778
22903
  }
22779
- /** The edges of the group that may hear `frame`. */
22904
+ /** The edges of the group that may hear `frame`, said to everyone. */
22780
22905
  hearers(groupId, frame) {
22781
22906
  const live = this.live.get(groupId);
22782
- return [...this.host.edges(groupId)].filter(([key]) => this.mayHear(live, key, frame));
22907
+ return [...this.host.edges(groupId)].filter(([key]) => this.mayHear(live, key, frame, true));
22783
22908
  }
22784
22909
  /** As a hub: someone the chain took out is asking (in my lobby, or listing itself as a hub). An edge to tell it, within bounds. */
22785
22910
  wantFarewell(groupId, live, key, now) {
@@ -22800,6 +22925,34 @@ var Communities = class {
22800
22925
  for (const [key, at] of live.farewellSaid) if (now - at >= FAREWELL_SAY_MS) live.farewellSaid.delete(key);
22801
22926
  }
22802
22927
  /**
22928
+ * As a hub: a key no commit I hold names is asking in my lobby. It may be a member ahead of me on the roster, who needs
22929
+ * the edge for me to find out (its answer to my `group-sync` carries the commit); or nobody, since whoever was once
22930
+ * given the group's rendezvous secret can write a key there. An edge, within bounds, that is no member's: it takes
22931
+ * no place among my members, and nothing the group says goes on it (`mayHear`).
22932
+ */
22933
+ wantAhead(groupId, live, key, now) {
22934
+ if (live.ahead.has(key) || now < (live.aheadQuiet.get(key) ?? 0)) return;
22935
+ if (live.ahead.size >= AHEAD_EDGES || this.capacity(groupId, live, now) <= 0) return;
22936
+ live.ahead.set(key, { since: now });
22937
+ if (!this.host.edges(groupId).has(key)) live.expect.add(key);
22938
+ }
22939
+ /** Those edges end: a commit named the key (a member of mine now, or out), it never came, or its moment is over; and a hub no more keeps none. */
22940
+ endAhead(groupId, live, now) {
22941
+ const edges = this.host.edges(groupId);
22942
+ for (const [key, a] of live.ahead) {
22943
+ const member = rosterHas(live.session.roster, key), named = member || live.session.wasRemoved(key);
22944
+ if (!named && live.hub) {
22945
+ const id = edges.get(key);
22946
+ if (!a.up && id && this.host.linkReady(id, 2)) a.up = now;
22947
+ if (a.up ? now - a.up < AHEAD_LINGER_MS : now - a.since < AHEAD_WAIT_MS) continue;
22948
+ }
22949
+ live.ahead.delete(key);
22950
+ if (!named) live.aheadQuiet.set(key, now + (a.up ? AHEAD_HEARD_MS : AHEAD_RETRY_MS));
22951
+ else if (member && live.hub && live.members.size < this.capacity(groupId, live, now)) live.members.set(key, now);
22952
+ }
22953
+ for (const [key, until] of live.aheadQuiet) if (until <= now) live.aheadQuiet.delete(key);
22954
+ }
22955
+ /**
22803
22956
  * Someone the chain took out says where it is (`group-sync`): it is handed the commits up to the one that took it
22804
22957
  * out, and nothing else (`CommunitySession.farewell`). Nothing else it says is heard.
22805
22958
  */
@@ -22863,6 +23016,10 @@ var Communities = class {
22863
23016
  if (ts + FAREWELL_SKEW_MS > (live.session.outAt(key) ?? Infinity)) this.wantFarewell(groupId, live, key, now);
22864
23017
  continue;
22865
23018
  }
23019
+ if (!rosterHas(live.session.roster, key)) {
23020
+ this.wantAhead(groupId, live, key, now);
23021
+ continue;
23022
+ }
22866
23023
  live.emptySince = now;
22867
23024
  if (!live.members.has(key) && live.members.size >= this.capacity(groupId, live, now)) continue;
22868
23025
  if (!live.members.has(key)) {
@@ -22883,6 +23040,7 @@ var Communities = class {
22883
23040
  for (const h of freshHubs(this.hubs(live), now)) if (h.key !== s.myKey && !this.hubDead(groupId, live, h, now)) wanted.add(h.key);
22884
23041
  for (const key of live.members.keys()) wanted.add(key);
22885
23042
  for (const key of live.farewells.keys()) wanted.add(key);
23043
+ for (const key of live.ahead.keys()) wanted.add(key);
22886
23044
  } else for (const key of live.myHubs) wanted.add(key);
22887
23045
  for (const [key, id] of existing) if (this.host.linkReady(id, 2) && (live.members.has(key) || this.recentHub(live, key)) && !s.wasRemoved(key)) wanted.add(key);
22888
23046
  }
@@ -23003,8 +23161,7 @@ var Communities = class {
23003
23161
  if (now - ts > 18e4 || this.refused.has(key) || live.pendingEntries.has(key) || key === link.host || key === s.myKey) continue;
23004
23162
  const stillKnocking = now - ts < 2 * this.timings.slowKnockMs;
23005
23163
  if (rosterHas(s.roster, key)) {
23006
- const added = [...s.state.chain].reverse().find((c) => c.k === "add" && c.s === key)?.ts ?? 0;
23007
- if (!stillKnocking || ts < added + this.timings.knockFallbackMs) continue;
23164
+ if (!stillKnocking || ts < this.addedAt(s, key) + this.timings.knockFallbackMs) continue;
23008
23165
  }
23009
23166
  const seen = live.knocksSeen.get(key);
23010
23167
  const changed = !!seen && seen.doors !== doorSig;
@@ -23034,6 +23191,10 @@ var Communities = class {
23034
23191
  }
23035
23192
  for (const [key, { first }] of live.knocksSeen) if (now - first > 36e4) live.knocksSeen.delete(key);
23036
23193
  }
23194
+ /** When the chain last added this member (0: never). */
23195
+ addedAt(s, key) {
23196
+ return [...s.state.chain].reverse().find((c) => c.k === "add" && c.s === key)?.ts ?? 0;
23197
+ }
23037
23198
  /** Leave requests: the door commits them (see `answerKnocks`); any hub after a while. */
23038
23199
  async commitLeaves(live, otherHubs, now) {
23039
23200
  const s = live.session;
@@ -23280,6 +23441,7 @@ var Communities = class {
23280
23441
  if (f.t === "group-sync" && live.session.isMember) this.sayFarewell(groupId, live, peerKey, f);
23281
23442
  return;
23282
23443
  }
23444
+ if (!live.session.isRecentMember(peerKey) && f.t !== "group-commit" && f.t !== "group-msg" && f.t !== "group-secrets" && !(f.t === "group-secret" && f.to === live.session.myKey)) return;
23283
23445
  if ((f.t === "group-secret" || f.t === "group-entry") && typeof f.to === "string" && f.to !== live.session.myKey) {
23284
23446
  if (!live.hub) return;
23285
23447
  const id = `${String(f.t)}:${f.to}:${String(f.h ?? f.x)}`;
@@ -23334,7 +23496,7 @@ var Communities = class {
23334
23496
  edgeReady(groupId, peerKey, linkId) {
23335
23497
  const live = this.live.get(groupId);
23336
23498
  if (!live || live.session.wasRemoved(peerKey)) return;
23337
- if (live.hub && !this.peerIsHub(live, peerKey)) live.members.set(peerKey, this.now());
23499
+ if (live.hub && !this.peerIsHub(live, peerKey) && rosterHas(live.session.roster, peerKey)) live.members.set(peerKey, this.now());
23338
23500
  this.sendTo(linkId, live.session.syncFrame());
23339
23501
  this.host.emit();
23340
23502
  }
@@ -23449,6 +23611,8 @@ var Communities = class {
23449
23611
  farewells: /* @__PURE__ */ new Map(),
23450
23612
  farewellQuiet: /* @__PURE__ */ new Map(),
23451
23613
  farewellSaid: /* @__PURE__ */ new Map(),
23614
+ ahead: /* @__PURE__ */ new Map(),
23615
+ aheadQuiet: /* @__PURE__ */ new Map(),
23452
23616
  knocksScanned: false,
23453
23617
  knockCursor: 0,
23454
23618
  lastShardPoll: 0,
@@ -24653,6 +24817,8 @@ var Groups = class {
24653
24817
  removedAt = /* @__PURE__ */ new Map();
24654
24818
  /** Who is typing in each private group, and this side's word there (WISP 902 · Group Mesh § Typing). */
24655
24819
  typings;
24820
+ /** A member's pins taken per window in a private group (group and member key). */
24821
+ pinPaces = /* @__PURE__ */ new Map();
24656
24822
  /** Edge signaling through members (WISP 902 · Group Mesh § Signaling through members). */
24657
24823
  signals;
24658
24824
  /** Files and voice messages in every group, private or community (WISP 503 · Group Files). */
@@ -24718,6 +24884,13 @@ var Groups = class {
24718
24884
  if (!asItIs) await this.files?.sweep(all.map((g) => g.id));
24719
24885
  for (const group of this.stored.values()) for (const [, linkId] of this.host.entries(group.id)) if (!group.invitation?.entry || group.invitation.linkId !== linkId) await this.host.closeEdge(linkId);
24720
24886
  }
24887
+ /** The pins taken per window from one member's edge in a private group, as a paired chat's session limits them. */
24888
+ pinPace(groupId, member) {
24889
+ const key = `${groupId}\n${member}`;
24890
+ let pace = this.pinPaces.get(key);
24891
+ if (!pace) this.pinPaces.set(key, pace = new ReactionWindow(PIN_LIMITS.receive, PIN_LIMITS.windowMs));
24892
+ return pace;
24893
+ }
24721
24894
  /** A group's pin now (WISP 400 § Pinned message), the latest one taken, unpinned or not. */
24722
24895
  pinOf(groupId) {
24723
24896
  return this.record(groupId)?.pin;
@@ -25030,6 +25203,15 @@ var Groups = class {
25030
25203
  if (!groupOfFile(fileId, [...this.stored.keys(), ...this.communities.ids()]) || !this.files) throw new Error("No such file");
25031
25204
  return this.files.download(fileId);
25032
25205
  }
25206
+ /** The person stops a group's file that is coming or would be fetched (Cancel, Decline), or pauses it (`keep`). */
25207
+ haltFile(fileId, keep = false) {
25208
+ if (!groupOfFile(fileId, [...this.stored.keys(), ...this.communities.ids()]) || !this.files) throw new Error("No such file");
25209
+ return this.files.halt(fileId, keep);
25210
+ }
25211
+ /** Automatic downloads were turned off: the groups' files chosen by themselves and not here yet stop coming. */
25212
+ autoDownloadsOff() {
25213
+ return this.files?.autoDownloadsOff() ?? Promise.resolve();
25214
+ }
25033
25215
  /** An edge's files/3 session opened or closed (WISP 503: the bytes of a group's files go over it). */
25034
25216
  fileSession(groupId, peerKey, linkId, open) {
25035
25217
  this.files?.session(groupId, peerKey, linkId, open);
@@ -25328,6 +25510,7 @@ var Groups = class {
25328
25510
  this.gapsAsked.delete(groupId);
25329
25511
  this.gapsLooked.delete(groupId);
25330
25512
  this.typings.forget(groupId);
25513
+ for (const key of this.pinPaces.keys()) if (key.startsWith(`${groupId}\n`)) this.pinPaces.delete(key);
25331
25514
  this.downSince.delete(groupId);
25332
25515
  this.hereHeard.delete(groupId);
25333
25516
  this.hereActed.delete(groupId);
@@ -25956,6 +26139,8 @@ var Groups = class {
25956
26139
  return;
25957
26140
  }
25958
26141
  if (t === "group-pin") {
26142
+ const said = this.pinPace(groupId, peerKey).take() ? readPin(frame) : null;
26143
+ if (!said || !pinIsNewer(this.record(groupId)?.pin, said)) return;
25959
26144
  const signed = session.signedPin(frame);
25960
26145
  if (signed && rosterHas(session.roster, peerKey)) await this.host.groupPin?.(groupId, peerKey, signed);
25961
26146
  return;
@@ -28068,7 +28253,7 @@ var HoldEngine = class {
28068
28253
  this.host.changed();
28069
28254
  return;
28070
28255
  }
28071
- const pointer = packet ? keys.readPointer(packet) : null;
28256
+ const pointer = packet ? keys.readPointer(packet, this.host.loopbackStorage?.()) : null;
28072
28257
  if (!pointer) return;
28073
28258
  let hold = this.state(linkId);
28074
28259
  if (pointer.rev < hold.peerPointerRev) return;
@@ -28107,11 +28292,12 @@ var HoldEngine = class {
28107
28292
  try {
28108
28293
  const manifest = keys.open(await this.fetchBytes(pointer.manifestUrl, HOLD_LIMITS.maxManifestBytes), {
28109
28294
  maxBytes: HOLD_LIMITS.maxManifestBytes,
28110
- now
28295
+ now,
28296
+ loopback: this.host.loopbackStorage?.()
28111
28297
  });
28112
28298
  if (manifest.header.kind !== "manifest") throw new HoldRefusedError("format", "Not a manifest");
28113
28299
  mailbox = manifest.header.mailbox;
28114
- entries = readManifest(manifest.header.meta).filter(([seq]) => seq > this.state(linkId).inSeq).sort((a, b) => a[0] - b[0]);
28300
+ entries = readManifest(manifest.header.meta, this.host.loopbackStorage?.()).filter(([seq]) => seq > this.state(linkId).inSeq).sort((a, b) => a[0] - b[0]);
28115
28301
  } catch (error) {
28116
28302
  if (error instanceof HoldRefusedError) {
28117
28303
  await this.save(linkId, {
@@ -28253,9 +28439,11 @@ var HoldEngine = class {
28253
28439
  }
28254
28440
  }
28255
28441
  async readBytes(url, max, signal) {
28442
+ if (!isHoldUrl(url, this.host.loopbackStorage?.())) throw new Error("The storage address is not one this app reads");
28256
28443
  const response = await (this.host.fetch ?? ((...args) => fetch(...args)))(url, {
28257
28444
  cache: "no-store",
28258
28445
  credentials: "omit",
28446
+ redirect: "error",
28259
28447
  signal
28260
28448
  });
28261
28449
  if (!response.ok) throw new Error(`The storage answered ${response.status}`);
@@ -34460,12 +34648,12 @@ var GhostlyNode = class GhostlyNode {
34460
34648
  await this.messagesChanged(linkId, [messageId]);
34461
34649
  this.emitState();
34462
34650
  },
34463
- text: async (linkId, messageId) => (await db.getMessages(linkId)).find((m) => m.id === messageId)?.text ?? null,
34651
+ text: async (linkId, messageId) => (await db.getMessage(linkId, messageId))?.text ?? null,
34464
34652
  reply: async (linkId, messageId) => {
34465
- const message = (await db.getMessages(linkId)).find((m) => m.id === messageId);
34653
+ const message = await db.getMessage(linkId, messageId);
34466
34654
  return message && GhostlyNode.wireReply(message);
34467
34655
  },
34468
- forwarded: async (linkId, messageId) => (await db.getMessages(linkId)).find((m) => m.id === messageId)?.forwarded,
34656
+ forwarded: async (linkId, messageId) => (await db.getMessage(linkId, messageId))?.forwarded,
34469
34657
  file: async (fileId) => {
34470
34658
  const stored = await fileStore.get(fileId);
34471
34659
  if (!stored?.metadata) return null;
@@ -34569,6 +34757,7 @@ var GhostlyNode = class GhostlyNode {
34569
34757
  },
34570
34758
  receivePaymentRequest: (linkId, request) => this.desk.onPaymentRequest(linkId, request, true),
34571
34759
  changed: () => this.emitState(),
34760
+ loopbackStorage: () => this.options.holdLoopback === true,
34572
34761
  ttlMs: () => {
34573
34762
  try {
34574
34763
  const raw = typeof localStorage !== "undefined" ? localStorage.getItem("ghostly-test-hold-ttl") : null;
@@ -35805,8 +35994,11 @@ var GhostlyNode = class GhostlyNode {
35805
35994
  appCheckUpdates() {
35806
35995
  return this.appStore.checkUpdates();
35807
35996
  }
35808
- appUninstall(params) {
35809
- return this.appStore.uninstall(params);
35997
+ /** Uninstalls an app. One running then is closed in every chat (the contact hears `close`), and the pages are told, to stop its frames. */
35998
+ async appUninstall(params) {
35999
+ await this.appStore.uninstall(params);
36000
+ this.appsCloseOf(params.ref);
36001
+ this.events.onAppsChecked?.();
35810
36002
  }
35811
36003
  appRunCheck(params) {
35812
36004
  return this.appStore.runCheck(params);
@@ -37206,7 +37398,7 @@ var GhostlyNode = class GhostlyNode {
37206
37398
  async retryMessage({ linkId, messageId }) {
37207
37399
  const live = this.links.get(linkId);
37208
37400
  if (!live?.stored.profile) throw new Error("Retry is only available for paired chat messages");
37209
- const message = (await db.getMessages(linkId)).find((m) => m.id === messageId);
37401
+ const message = await db.getMessage(linkId, messageId);
37210
37402
  if (message?.via === "hold") {
37211
37403
  if (await this.hold.retry(linkId, messageId)) return;
37212
37404
  if (!this.holdingFor(live)) throw new Error("Held messages need S3 storage (Profile → Backups) and a contact that allows them.");
@@ -37326,7 +37518,7 @@ var GhostlyNode = class GhostlyNode {
37326
37518
  */
37327
37519
  async messageDetails({ linkId, messageId }) {
37328
37520
  if (typeof linkId !== "string" || typeof messageId !== "string") throw new Error("Chat not found");
37329
- const message = (await db.getMessages(linkId)).find((m) => m.id === messageId);
37521
+ const message = await db.getMessage(linkId, messageId);
37330
37522
  if (!message) return null;
37331
37523
  const live = this.links.get(linkId), stored = live?.stored;
37332
37524
  const group = linkId.startsWith("group:") ? (await db.getGroups()).find((g) => g.id === linkId.slice(6)) : void 0;
@@ -37533,12 +37725,22 @@ var GhostlyNode = class GhostlyNode {
37533
37725
  }
37534
37726
  }
37535
37727
  /**
37728
+ * The message of mine that carries a payment or a request, read by its id: `me_<the time the desk keeps for it>`.
37729
+ * The chat's history is read only when that row does not carry it, so it costs the same in a long chat as in a short one.
37730
+ */
37731
+ async paymentMessage(linkId, paymentId) {
37732
+ const at = this.desk.payment(paymentId)?.createdAt;
37733
+ const message = at === void 0 ? void 0 : await db.getMessage(linkId, `me_${at}`);
37734
+ if (message?.sender === "me" && message.paymentId === paymentId) return message;
37735
+ return (await db.getMessages(linkId)).find((m) => m.sender === "me" && m.paymentId === paymentId);
37736
+ }
37737
+ /**
37536
37738
  * The contact's app answered for a payment of mine (`pay-res`): its message gets two ticks, as a text gets from its
37537
37739
  * receipt. A payment goes with no delivery state of its own, so its mark read one tick for good. 1:1 chats only: a
37538
37740
  * group's payments live in the group's history, under other ids.
37539
37741
  */
37540
37742
  async notePaymentReceipt(linkId, paymentId) {
37541
- const message = (await db.getMessages(linkId)).find((m) => m.sender === "me" && m.paymentId === paymentId);
37743
+ const message = await this.paymentMessage(linkId, paymentId);
37542
37744
  if (!message || message.delivery === "delivered") return;
37543
37745
  if (await db.patchMessage(linkId, message.id, (m) => m.delivery === "delivered" ? null : {
37544
37746
  delivery: "delivered",
@@ -37756,9 +37958,10 @@ var GhostlyNode = class GhostlyNode {
37756
37958
  }
37757
37959
  fileAction({ linkId, fileId, action }) {
37758
37960
  if (typeof linkId === "string" && linkId.startsWith("group:")) {
37759
- if (action !== "accept" && action !== "request") throw new Error("A group's file is downloaded (accept) or asked for again (request)");
37760
37961
  if (!fileId.startsWith(`group-${linkId.slice(6)}-`)) throw new Error("No such file in this group");
37761
- return this.groups.downloadFile(fileId);
37962
+ if (action === "accept" || action === "request" || action === "resume") return this.groups.downloadFile(fileId);
37963
+ if (action === "cancel" || action === "decline" || action === "pause") return this.groups.haltFile(fileId, action === "pause");
37964
+ throw new Error("A group's file is not sent again from here");
37762
37965
  }
37763
37966
  const live = this.links.get(linkId);
37764
37967
  if (!live) throw new Error("No such chat");
@@ -37847,7 +38050,7 @@ var GhostlyNode = class GhostlyNode {
37847
38050
  const error = await link.announceFile(offer, message.timestamp, GhostlyNode.wireReply(message), message.forwarded);
37848
38051
  const said = !error && link.dhtPendingId === dhtFileId(offer.wireId);
37849
38052
  if (!said && error !== "This file's name is too long to say over the DHT. It is sent when you are live.") return;
37850
- await db.patchMessage(linkId, message.id, (current) => current.delivery !== "waiting" || current.fileOnFloor ? null : {
38053
+ const noted = await db.patchMessage(linkId, message.id, (current) => current.delivery !== "waiting" || current.fileOnFloor ? null : {
37851
38054
  fileOnFloor: said ? "sent" : "unfit",
37852
38055
  ...said && { details: withSend(current.details, {
37853
38056
  at,
@@ -37855,17 +38058,32 @@ var GhostlyNode = class GhostlyNode {
37855
38058
  result: "sent"
37856
38059
  }) }
37857
38060
  });
38061
+ if (said && noted) await live.floorFiles?.then((known) => known.set(dhtFileId(offer.wireId), message.id), () => {});
37858
38062
  await this.messagesChanged(linkId, [message.id]);
37859
38063
  }
37860
38064
  /** A receipt on the DHT floor that names a waiting file's offer (`dhtFileId`): its bubble is in place on the contact's side. */
37861
38065
  async fileSeenOnFloor(linkId, id) {
37862
- const prefix = `${linkId}-out-`;
37863
- const message = (await db.getMessages(linkId)).find((m) => m.sender === "me" && m.file?.id.startsWith(prefix) && m.fileOnFloor === "sent" && dhtFileId(m.file.id.slice(prefix.length)) === id);
37864
- if (!message) return;
37865
- if (await db.patchMessage(linkId, message.id, (current) => current.fileOnFloor !== "sent" ? null : {
38066
+ const live = this.links.get(linkId);
38067
+ if (!live) return;
38068
+ const said = await this.floorFilesOf(live), messageId = said.get(id);
38069
+ if (!messageId) return;
38070
+ said.delete(id);
38071
+ if (await db.patchMessage(linkId, messageId, (current) => current.fileOnFloor !== "sent" ? null : {
37866
38072
  fileOnFloor: "seen",
37867
38073
  ...current.delivery === "waiting" && { deliveryError: "Your contact sees it. The file goes when you are live." }
37868
- })) await this.messagesChanged(linkId, [message.id]);
38074
+ })) await this.messagesChanged(linkId, [messageId]);
38075
+ }
38076
+ /**
38077
+ * The files of mine said on the floor and not seen there yet, their message by their floor id (`dhtFileId`). Read from
38078
+ * the chat's history once in a run, then kept as files are said (`sayFileOnFloor`): a receipt that names none of them
38079
+ * costs the same in a long chat as in a short one.
38080
+ */
38081
+ floorFilesOf(live) {
38082
+ const linkId = live.stored.id, prefix = `${linkId}-out-`;
38083
+ return live.floorFiles ??= db.getMessages(linkId).then((history) => new Map(history.filter((m) => m.sender === "me" && m.file?.id.startsWith(prefix) && m.fileOnFloor === "sent").map((m) => [dhtFileId(m.file.id.slice(prefix.length)), m.id])), (error) => {
38084
+ live.floorFiles = void 0;
38085
+ throw error;
38086
+ });
37869
38087
  }
37870
38088
  /**
37871
38089
  * A file the contact said on the DHT floor (WISP 403 § Files): its bubble takes its place now, waiting for its bytes,
@@ -38865,10 +39083,13 @@ var GhostlyNode = class GhostlyNode {
38865
39083
  * This side opened an app in a paired chat, or updated it: `open` goes to the contact now when the session is live,
38866
39084
  * and again on every session that comes back while it stays open.
38867
39085
  */
38868
- async appOpen({ linkId, ref, version }) {
39086
+ async appOpen({ linkId, ref, version, digest }) {
38869
39087
  const { live, app } = this.appChat(linkId, ref);
38870
39088
  if (!isAppVersion(version)) throw new Error("Not an app version");
38871
- await this.appStore.chatRunnable({ ref });
39089
+ await this.appStore.chatRunnable({
39090
+ ref,
39091
+ digest
39092
+ });
38872
39093
  const open = live.appsOpen ??= /* @__PURE__ */ new Map();
38873
39094
  if (live.link) live.link.openApp(app, version);
38874
39095
  else open.set(app, version);
@@ -38881,10 +39102,13 @@ var GhostlyNode = class GhostlyNode {
38881
39102
  else live.appsOpen?.delete(app);
38882
39103
  }
38883
39104
  /** One data frame of an app open on both sides, on the live session only; nothing is kept to send later. */
38884
- async appSend({ linkId, ref, data }) {
39105
+ async appSend({ linkId, ref, data, digest }) {
38885
39106
  const { live, app } = this.appChat(linkId, ref);
38886
39107
  if (!live.appsOpen?.has(app)) return { error: "not-open" };
38887
- await this.appStore.chatRunnable({ ref });
39108
+ await this.appStore.chatRunnable({
39109
+ ref,
39110
+ digest
39111
+ });
38888
39112
  return { error: live.link ? live.link.sendAppData(app, data) : "offline" };
38889
39113
  }
38890
39114
  /** Every page went away: no app runs here any more, so each open one is closed (`close` goes where it can). */
@@ -38892,6 +39116,16 @@ var GhostlyNode = class GhostlyNode {
38892
39116
  for (const live of this.links.values()) for (const app of [...live.appsOpen?.keys() ?? []]) if (live.link) live.link.closeApp(app);
38893
39117
  else live.appsOpen?.delete(app);
38894
39118
  }
39119
+ /** An app that went (uninstalled) is closed in every chat it was open in, as `appClose` does. */
39120
+ appsCloseOf(ref) {
39121
+ for (const [linkId, live] of this.links) {
39122
+ if (!live.appsOpen?.size) continue;
39123
+ const { app } = this.appChat(linkId, ref);
39124
+ if (!live.appsOpen.has(app)) continue;
39125
+ if (live.link) live.link.closeApp(app);
39126
+ else live.appsOpen.delete(app);
39127
+ }
39128
+ }
38895
39129
  /** Keeps a pin of a 1:1 chat when it is newer than the chat's; `out`: mine, to say until the contact confirms it. */
38896
39130
  async keepLinkPin(linkId, pin, out) {
38897
39131
  const live = this.links.get(linkId);
@@ -38954,6 +39188,7 @@ var GhostlyNode = class GhostlyNode {
38954
39188
  const view = this.groups.views().find((g) => g.id === groupId);
38955
39189
  if (!pin || !view?.myKey || view.status !== "active" || !view.members.some((m) => m.key === member)) return;
38956
39190
  if (!mayPin(view.profile, member, view.members.find((m) => m.role === "admin")?.key)) return;
39191
+ if (!pinIsNewer(this.groups.pinOf(groupId), pin)) return;
38957
39192
  const kept = await this.pinCame(`group:${groupId}`, member === view.myKey ? "me" : member, pin);
38958
39193
  if (!await this.groups.setPin(groupId, {
38959
39194
  ...kept,
@@ -39241,7 +39476,7 @@ var GhostlyNode = class GhostlyNode {
39241
39476
  async closeRequest(paymentId, reason) {
39242
39477
  const request = this.desk.payment(paymentId);
39243
39478
  if (!request || !await this.desk.close(paymentId, reason, "your contact removed the wallet it was paid to")) return;
39244
- const message = (await db.getMessages(request.linkId)).find((m) => m.paymentId === paymentId && m.sender === "me");
39479
+ const message = await this.paymentMessage(request.linkId, paymentId);
39245
39480
  if (message) await this.hold.forget(request.linkId, message.id).catch(() => {});
39246
39481
  }
39247
39482
  /**
@@ -40080,6 +40315,7 @@ var GhostlyNode = class GhostlyNode {
40080
40315
  if (settings[key] !== false) delete this.settings[key];
40081
40316
  else this.settings[key] = false;
40082
40317
  await db.putSettings(this.settings);
40318
+ if (key === "autoDownloads" && settings[key] === false) await this.groups.autoDownloadsOff();
40083
40319
  }
40084
40320
  if (settings.publicProfiles !== void 0) {
40085
40321
  if (settings.publicProfiles !== false) delete this.settings.publicProfiles;
@@ -41627,10 +41863,11 @@ var GhostlyNode = class GhostlyNode {
41627
41863
  this.notePaymentReceipt(linkId, result.id).catch(() => {});
41628
41864
  },
41629
41865
  onFileStored: async (wire) => {
41630
- const stored = (await fileStore.listForLink(linkId)).find((file) => file.direction === "in" && file.wireId === wire.id);
41631
- if (!stored?.digest || !stored.metadata) return void 0;
41632
- if (stored.metadata.name !== wire.name || stored.metadata.size !== wire.size || stored.metadata.mime !== wire.mime || stored.metadata.timestamp !== wire.timestamp) return void 0;
41633
- return stored.digest;
41866
+ if (!stored.group && !this.links.get(linkId)?.files.wireIds.has(wire.id)) return void 0;
41867
+ const held = (await fileStore.listForLink(linkId)).find((file) => file.direction === "in" && file.wireId === wire.id);
41868
+ if (!held?.digest || !held.metadata) return void 0;
41869
+ if (held.metadata.name !== wire.name || held.metadata.size !== wire.size || held.metadata.mime !== wire.mime || held.metadata.timestamp !== wire.timestamp) return void 0;
41870
+ return held.digest;
41634
41871
  },
41635
41872
  onFileIncoming: (file) => this.receiveFile(linkId, file),
41636
41873
  onFileProgress: (id, transferred, direction) => this.fileProgress(this.localFileId(linkId, id, direction), transferred),
@@ -41769,7 +42006,7 @@ var GhostlyNode = class GhostlyNode {
41769
42006
  get nativeFactories() {
41770
42007
  const relay = this.relaysHyperdht ? this.hyperdhtRelay : "";
41771
42008
  return {
41772
- ...relay ? { "hyperdht/1": async (seedB64) => (await import("./hyperdhtRelay-C0XRWySP.js")).createRelayedHyperEndpoint(seedB64, relay) } : {},
42009
+ ...relay ? { "hyperdht/1": async (seedB64) => (await import("./hyperdhtRelay-CgYLaQz3.js")).createRelayedHyperEndpoint(seedB64, relay) } : {},
41773
42010
  ...this.options.nativeTransports,
41774
42011
  ...this.ownIrohRelays ? { "iroh/1": (seedB64) => this.options.nativeTransports["iroh/1"](seedB64, this.settings.irohRelays?.length ? { relays: [...this.settings.irohRelays] } : {}) } : {},
41775
42012
  ...this.options.irohWeb ? { "iroh/1": (seedB64) => createIrohWebEndpoint(seedB64, { relays: this.irohRelays }) } : {}
@@ -42574,4 +42811,4 @@ function signalKind(signal) {
42574
42811
  //#endregion
42575
42812
  export { EngineServer, hyperdhtRelayProblem as n, server_exports as t };
42576
42813
 
42577
- //# sourceMappingURL=server-D5FBZYW8.js.map
42814
+ //# sourceMappingURL=server-Cy4ox7oD.js.map