@ghostlytools/cli 1.2.4 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +16 -6
  2. package/dist/assets/api-BiaX6l6f.js +3 -0
  3. package/dist/assets/api-CrxVxtY1.js +4071 -0
  4. package/dist/assets/api-CrxVxtY1.js.map +1 -0
  5. package/dist/assets/apiKit-CajiDxdK.js +196 -0
  6. package/dist/assets/apiKit-CajiDxdK.js.map +1 -0
  7. package/dist/assets/apps-CbfiCSIw.js +787 -0
  8. package/dist/assets/apps-CbfiCSIw.js.map +1 -0
  9. package/dist/assets/apps-t23gkc28.js +2 -0
  10. package/dist/assets/{arkade-CdnKGDbg.js → arkade-D4BVJFEQ.js} +2 -2
  11. package/dist/assets/{arkade-CdnKGDbg.js.map → arkade-D4BVJFEQ.js.map} +1 -1
  12. package/dist/assets/backup-BAMi2lAq.js +731 -0
  13. package/dist/assets/backup-BAMi2lAq.js.map +1 -0
  14. package/dist/assets/backup-Cg52WeQi.js +2 -0
  15. package/dist/assets/bark_ffi_wasm_bg-EY2uc0Bx.js.map +1 -1
  16. package/dist/assets/buttons-CWl2yNJu.js +2 -0
  17. package/dist/assets/buttons-CX1pKB0j.js +497 -0
  18. package/dist/assets/buttons-CX1pKB0j.js.map +1 -0
  19. package/dist/assets/{endpoint-pgFkR2AS.js → endpoint-Dn7H8qE1.js} +14 -8
  20. package/dist/assets/endpoint-Dn7H8qE1.js.map +1 -0
  21. package/dist/assets/envelope-BvU0fLEt.js +90 -0
  22. package/dist/assets/envelope-BvU0fLEt.js.map +1 -0
  23. package/dist/assets/errors-my6VJR_0.js +77 -0
  24. package/dist/assets/errors-my6VJR_0.js.map +1 -0
  25. package/dist/assets/fedimint_client_wasm_bg-BtHHlFpp.js.map +1 -1
  26. package/dist/assets/{fileBytes-Dbifu5I5.js → fileBytes-CfSXjTFa.js} +2 -2
  27. package/dist/assets/{fileBytes-Dbifu5I5.js.map → fileBytes-CfSXjTFa.js.map} +1 -1
  28. package/dist/assets/fileBytes-CgbibpbV.js +2 -0
  29. package/dist/assets/{fileBytes-BcYZhNgF.js → fileBytes-Di7VZtmU.js} +6 -19
  30. package/dist/assets/fileBytes-Di7VZtmU.js.map +1 -0
  31. package/dist/assets/fileBytesIdb-CFP-DSkZ.js +2 -0
  32. package/dist/assets/{fileBytesIdb-DzSaqI0A.js → fileBytesIdb-D5u6H8X_.js} +4 -4
  33. package/dist/assets/{fileBytesIdb-DzSaqI0A.js.map → fileBytesIdb-D5u6H8X_.js.map} +1 -1
  34. package/dist/assets/{fileBytesOpfs-Dx8RZiV5.js → fileBytesOpfs-Dd1N4ERH.js} +3 -3
  35. package/dist/assets/{fileBytesOpfs-Dx8RZiV5.js.map → fileBytesOpfs-Dd1N4ERH.js.map} +1 -1
  36. package/dist/assets/groupFiles-BNTSDaoq.js +7685 -0
  37. package/dist/assets/groupFiles-BNTSDaoq.js.map +1 -0
  38. package/dist/assets/groupFiles-DBbjOLuD.js +2088 -0
  39. package/dist/assets/groupFiles-DBbjOLuD.js.map +1 -0
  40. package/dist/assets/host-Cmd205Dc.js +4648 -0
  41. package/dist/assets/host-Cmd205Dc.js.map +1 -0
  42. package/dist/assets/hyperdhtRelay-BT_wRxab.js +21 -0
  43. package/dist/assets/hyperdhtRelay-BT_wRxab.js.map +1 -0
  44. package/dist/assets/{hyperdhtRelay-C1p0nkle.js → hyperdhtRelay-CEmBpT0Q.js} +26 -10
  45. package/dist/assets/hyperdhtRelay-CEmBpT0Q.js.map +1 -0
  46. package/dist/assets/{idb-CTF6LF5B.js → idb-B5L8wuOm.js} +7 -5
  47. package/dist/assets/idb-B5L8wuOm.js.map +1 -0
  48. package/dist/assets/irohWeb--YyEA-hX.js +545 -0
  49. package/dist/assets/irohWeb--YyEA-hX.js.map +1 -0
  50. package/dist/assets/{joinTrace-CO8mlhNO.js → joinTrace-CjRDFy85.js} +2 -7
  51. package/dist/assets/{joinTrace-CO8mlhNO.js.map → joinTrace-CjRDFy85.js.map} +1 -1
  52. package/dist/assets/joinTrace-D-fnt2p3.js +2 -0
  53. package/dist/assets/main-BzNN6bkx.js +4302 -0
  54. package/dist/assets/main-BzNN6bkx.js.map +1 -0
  55. package/dist/assets/{mainline-BH6s8HVi.js → mainline-D5D5C-jT.js} +11 -6
  56. package/dist/assets/mainline-D5D5C-jT.js.map +1 -0
  57. package/dist/assets/mints-BrlY773j.js +74 -0
  58. package/dist/assets/mints-BrlY773j.js.map +1 -0
  59. package/dist/assets/mints-CqTdA4Gk.js +2 -0
  60. package/dist/assets/networkReason-DxZWHw6v.js +154 -0
  61. package/dist/assets/networkReason-DxZWHw6v.js.map +1 -0
  62. package/dist/assets/privateFolder-Bk9C9oFv.js +38 -0
  63. package/dist/assets/privateFolder-Bk9C9oFv.js.map +1 -0
  64. package/dist/assets/profiles-B6F4oa9K.js +139 -0
  65. package/dist/assets/profiles-B6F4oa9K.js.map +1 -0
  66. package/dist/assets/{restoredRows-C_84q0Pd.js → restoredRows-Db3oUTko.js} +2 -2
  67. package/dist/assets/{restoredRows-C_84q0Pd.js.map → restoredRows-Db3oUTko.js.map} +1 -1
  68. package/dist/assets/{server-0zfNpABQ.js → server-CBCEYKMN.js} +1103 -235
  69. package/dist/assets/server-CBCEYKMN.js.map +1 -0
  70. package/dist/assets/{src-CbLkHfz8.js → src-DhfGgAUu.js} +836 -2265
  71. package/dist/assets/src-DhfGgAUu.js.map +1 -0
  72. package/dist/assets/turnRefs-ChEfgbSX.js +323 -0
  73. package/dist/assets/turnRefs-ChEfgbSX.js.map +1 -0
  74. package/dist/assets/{usdt-Qf4UuygS.js → usdt-C1Xao2xH.js} +3 -3
  75. package/dist/assets/{usdt-Qf4UuygS.js.map → usdt-C1Xao2xH.js.map} +1 -1
  76. package/dist/ghostly.mjs +4 -22077
  77. package/dist/ghostly.mjs.map +1 -1
  78. package/package.json +1 -1
  79. package/dist/assets/endpoint-pgFkR2AS.js.map +0 -1
  80. package/dist/assets/fileBytes-BcYZhNgF.js.map +0 -1
  81. package/dist/assets/hyperdhtRelay-C1p0nkle.js.map +0 -1
  82. package/dist/assets/idb-CTF6LF5B.js.map +0 -1
  83. package/dist/assets/mainline-BH6s8HVi.js.map +0 -1
  84. package/dist/assets/rolldown-runtime-8H4AJuhK.js +0 -14
  85. package/dist/assets/server-0zfNpABQ.js.map +0 -1
  86. package/dist/assets/src-CbLkHfz8.js.map +0 -1
@@ -1,23 +1,28 @@
1
- import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.js";
2
- import { $ as parseCommunityEdit, $a as receivedTimestamp, $c as identityFromSeed, $o as HANDOFF_PAKE, $r as vapidKeysMatch, $s as verifyEnrollProof, $t as signDidDhtPacket, A as encodeCommunityLink, Aa as reactionEmoji, Ac as signTurnPacket, Ai as DHT_TEXT_CAPABILITY, Ao as webLocalFetch, Ar as FILE_LIMITS, As as readHandoffTurnFrame, B as GroupWakeLimiter, Bc as normalizeRelayUrl, Bi as identityRevocationValue, Bn as INVITE_TAKEN, Br as mayPin, C as openGroupTyping, Ca as readForwarded, Cc as classifyTurnRead, Co as DHT_POLL_INTERVALS, Cr as pickHubs, Cs as newHandoffId, D as GROUP_READ_NOTE_COMMUNITY, Da as ReactionWindow, Dc as firstDeviceSetSecret, Do as GhostlyHttpError, Dr as readLobby, Ds as readHandoffHave, E as CommunitySession, Ea as REACTION_LIMITS, Ec as TURN_MAX, Eo as presenceSeenAt, Er as readBeaconHead, Es as readHandoffDone, F as knockIdentity, Fa as pairedReplyAuthor, Fc as turnStatedVerifier, Fi as IDENTITY_REVOCATION_LABEL, Fn as findEndpoint, Fo as RTC_CONFIG, Fr as TypingSender, Fs as readSetUpdate, Gc as withRequestOptions, Gi as lastSharedWithMe, Gn as GROUP_FILE_LIMITS, Go as LEGACY_SERVICES, Gr as WAKE_CALL_INTERVAL_MS, Gs as enrollCancelReason, H as groupWakes, Hc as traceLink, Hi as newIdentityBinding, Hn as pairedMessageFrame, Hr as pinIsNewer, I as knockRecords, Ia as readReply, Ic as turnVerifierMark, In as isFederationId, Is as setAckFrame, J as frameBytes, Ja as sameButtonText, Jc as createRelayPayload, Ji as validExternalKey, Jo as HANDOFF_CANCEL, Jr as newWakeToken, Js as enrollGrantFrame, Jt as didDhtDocument, K as GROUP_READ_NOTE, Kc as PacketTooLargeError, Ki as PeerProofs, Ko as serviceIdFromName, Kr as WakeLimiter, Ks as enrollDigits, L as mergeKnocks, La as replySnippet, Lc as DEFAULT_RELAYS, Li as IdentityExchange, Ln as parseCashuRequestPayload, Lr as typingActivity, Ls as setUpdateFrame, M as decodeGroupEntryLink, Ma as readReaction, Mc as turnInstance, Mi as dialDescriptors, Mn as ENDPOINT, Mo as decodeControl, Mr as transferStalled, Ms as sealHandoffPiece, N as encodeGroupEntryLink, Na as validReactionNumber, Nc as turnKeys, Nn as cashuRequestPayload, No as encodeControl, O as communityHasFrame, Oa as nextReactionNumber, Oc as nextTurnPosition, Oo as formatLocalTarget, Or as shouldBeHub, Os as readHandoffHello, P as entryParams, Pa as wireReaction, Pc as turnName, Pn as fedimintRequestPayload, Q as carryMentions, Qa as heardTime, Qc as createIdentity, Qn as readGroupFileMeta, Qo as HANDOFF_OFFER, Qr as checkPushEndpoint, Qs as readEnrollHello, R as readKnocks, Ra as BUTTON_ID, Rc as RelayTransport, Rn as parseFedimintRequestPayload, Rs as signTombstone, S as groupTypingEpoch, Sa as forwardedAgain, Sc as TURN_SETTLE_MS, So as DirectPathWatch, Sr as newerHead, Ss as handoffVersions, St as normalizeAtprotoHandle, T as COMMUNITY_LIMITS, Ta as GROUP_REACTION_FRAME, Tc as TURN_LAST_SEQUENCE, To as RELAY_POLL_INTERVALS, Tr as readBeacon, Ts as readHandoffCancel, U as parseGroupWakeFrame, Ua as STATUS_CARD_LIMITS, Uc as budgetRetryMs, Ui as receivedIdentityStatus, Un as engineError, Uo as emptyLinkRecords, Ur as pinNumberHolds, V as groupWakeFrame, Vi as identityStatement, Vn as LIVENESS_PING_MS, Vo as parseCallSignal, Vr as nextPinNumber, W as GROUP_LIMITS, Wc as isDiscoveryBudgetError, Wi as revokesIdentity, Wn as engineText, Wo as HTTP_SERVICE_PROTO, Wr as readPin, Ws as enrollCancelFrame, Xa as arrivalKey, Xi as automaticTransport, Xn as groupFileFallback, Xo as HANDOFF_HAVE, Xr as relayRequest, Xs as enrollProofFrame, Xt as encodeDidDhtDeactivated, Ya as statusCardText, Yi as TRANSPORTS, Yo as HANDOFF_DONE, Yr as parseWakeFrame, Ys as enrollHelloFrame, Yt as didDhtFromPublicKey, Z as GROUP_EDIT_TEXT_BYTES, Za as claimedTime, Zo as HANDOFF_HELLO, Zr as wakeRequest, Zs as readEnrollGrant, Zt as encodeDidDhtPacket, _a as EDIT_BUFFER_MS, _c as newDeviceSetSecret, _n as sparkInvoiceDetails, _o as MEMBER_KEY, _r as freshHubs, _s as handoffOfferFrame, a as planAppUpdate, ac as decodeInviteCode, ai as STATUS_CARD_CAPABILITY, al as bytesEqual, as as handoffAttemptAllowed, ba as dhtEditId, bn as isPubkyKey, bo as verifyCommitSignature, br as mergeBeacon, bs as handoffRetryAfter, ca as sanitizeFileName, cc as inviteOwnership, ci as HOLD_DATED_BACK_MS, cl as toBase64Url, cn as WALLET_NETWORKS, co as readAppRevocation, cr as readGroupSignal, cs as handoffBusyFrame, d as APP_BUNDLE_LIMITS, dc as HANDOFF_CAPABILITY, di as HoldRefusedError, dl as utf8Encode, dn as formatPaymentAmount, dr as meshHubs, ds as handoffContextProof, ea as DHT_MESSAGE_TTL, ec as createDeviceInvite, ei as chatAppId, el as identityFromSeedB64, en as ETHEREUM_USDT, eo as shownTime, es as HANDOFF_PIECE_BYTES, et as mentionsMember, f as APP_BUNDLE_MAGIC, fc as deviceEchoFrame, fi as isHoldUrl, fn as validatePaymentTarget, fo as canonicalJson, fr as pickMeshHubs, fs as handoffDataFrame, ga as parseVideoMeta, gc as devicePingFrame, gn as sparkAddressKind, gr as doorHubs, gs as handoffManifestFrames, h as appViewOf, hc as deviceLinkPairing, hn as SPARK_PROVIDER, ho as GROUP_MEMBER_CAP, hr as beaconRecords, hs as handoffHelloFrame, i as checkAppBeforeRun, ia as parseImageMeta, ic as createChatInvite, ii as EDIT_CAPABILITY, il as sanitizeAvatar, in as SEPOLIA_TEST_USDT, io as appRef, is as filePartName, ja as reactionIsNewer, jc as signTurnRelease, ji as capsDescriptors, jn as decodeBolt11, jo as LIMITS, jr as transferEnded, js as readHandoffVerified, k as decodeCommunityLink, ka as queueReaction, kc as readTurnPacket, ki as CapsExchange, ko as parseLocalTarget, kr as ChatFiles, ks as readHandoffPake, l as readAppStoreSteps, la as sanitizeMime, lc as DEVICES_CAPABILITY, li as HOLD_LIMITS, ll as toZ32, ln as assertTokenUnits, lr as MESH_HUBS, ls as handoffCancelFrame, m as appDigest, ma as parseVoiceMeta, mc as deviceKeyZ32, mi as readManifest, mn as SPARK_NETWORKS, mo as readCanonicalJson, mr as beaconKeys, ms as handoffEphemeral, n as appStoreDecision, na as DHT_FILE_CAPABILITY, nc as readDeviceInvite, ni as APPS_CAPABILITY, nl as verify, nn as ONCHAIN_PROVIDER, no as APP_STATEMENT_LIMITS, nr as CarriedTransport, ns as HANDOFF_TIMINGS, o as APP_STORE_LIMITS, oa as formatFileSize, oc as encodeInviteCode, oi as UPGRADE_CAPABILITY, ol as fromBase64Url, on as TEST_USDT_FAUCET_AMOUNT, oo as isAppKey, os as handoffAttemptFailed, pa as fileMessageText, pc as deviceEchoNonce, pi as newMailbox, pn as walletNetworkOf, pr as COMMUNITY_TOPOLOGY, ps as handoffDigest, q as GroupSession, qa as checkStatusCard, qi as emptyProofLedger, qo as HANDOFF_ACK, qr as deviceWakeRequest, qs as enrollDoneFrame, r as appUpdateDecision, ra as dhtFileId, rc as OwnInviteError, ri as BUTTONS_CAPABILITY, rn as PaymentPreflightError, ro as appFingerprint, rs as HANDOFF_VERIFIED, sa as safeBlobType, si as HOLD_CAPABILITY, sl as randomBytes, sn as USDT_PUBLIC_RPC, so as isAppRef, sr as groupSignalFrame, ss as handoffAttemptSucceeded, t as appRevocationFor, ta as peekDhtMailbox, tc as deviceInviteJoinerParams, ti as isAppVersion, tl as publicKeyFromZ32, tn as EVM_TEST_CHAINS, ts as HANDOFF_REQUEST, tt as groupName, uc as ENROLL_CAPABILITY, ui as HoldKeys, ul as utf8Decode, un as assertWholeSats, ur as mayBeHub, us as handoffContext, v as isAppUrl, va as EDIT_RATE_WINDOW_MS, vc as seedSigner, vn as isBitcoinAddress, vo as commitHash, vr as lobbyKeys, vs as handoffPakeFrame, w as sealGroupTyping, wa as COMMUNITY_REACTION_FRAME, wc as turnPutSummary, wr as rankHubs, ws as readHandoffAck, x as GroupTypingBoard, xa as parseLinkPreview, xc as TURN_RAISE_READ_TIMEOUT_MS, xo as edgeParams, xr as mergeLobby, xs as handoffStreamKey, xt as isAtprotoDid, y as readAppBundle, ya as RateWindow, yc as webCryptoSigner, yo as rosterHas, yr as lobbyRecords, ys as handoffReleaseFrame, zc as currentRelays, zi as emptyIdentityLedger, zn as GhostLink, zr as PIN_LIMITS, zs as verifySetUpdate } from "./src-CbLkHfz8.js";
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
- import { a as fileBytesOf, c as removeFileBytes, i as fileBytes, l as FILE_BYTES_STEP, n as blobDigest } from "./fileBytes-BcYZhNgF.js";
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";
6
- import { n as traceJoin } from "./joinTrace-CO8mlhNO.js";
1
+ import { n as traceJoin } from "./joinTrace-CjRDFy85.js";
2
+ import { $ as appRef, D as parseVideoMeta, Ft as toBase64Url, G as claimedTime, It as toZ32, J as shownTime, K as heardTime, Lt as utf8Decode, N as STATUS_CARD_LIMITS, Nt as randomBytes, O as BUTTON_ID, Ot as bytesEqual, Q as appFingerprint, Rt as utf8Encode, T as parseVoiceMeta, U as statusCardText, V as sameButtonText, W as arrivalKey, Z as APP_STATEMENT_LIMITS, _ as safeBlobType, _t as LIMITS, at as readAppRevocation, bt as encodeControl, c as readGroupFileMeta, g as formatFileSize, ht as readCanonicalJson, it as isAppRef, jt as fromBase64Url, nt as isAppHash, o as groupFileFallback, p as parseImageMeta, pt as canonicalJson, q as receivedTimestamp, rt as isAppKey, t as GROUP_FILE_LIMITS, v as sanitizeFileName, w as fileMessageText, y as sanitizeMime, yt as decodeControl, z as checkStatusCard } from "./groupFiles-DBbjOLuD.js";
3
+ import { $a as HANDOFF_HELLO, $i as EDIT_RATE_WINDOW_MS, $o as readEnrollGrant, $r as STATUS_CARD_CAPABILITY, $t as didDhtFromPublicKey, A as CommunitySession, Aa as RELAY_POLL_INTERVALS, Ao as readHandoffHello, Ar as typingActivity, As as nextTurnPosition, B as knockRecords, Bi as PeerProofs, Bn as isFederationId, Bo as signTombstone, Br as deviceWakeRequest, Bs as RelayTransport, C as sha256Paced, Ca as rosterHas, Ci as CapsExchange, Co as handoffStreamKey, Cr as FILE_LIMITS, Cs as TURN_RAISE_READ_TIMEOUT_MS, D as openGroupTyping, Do as readHandoffCancel, Ds as TURN_LAST_SEQUENCE, E as groupTypingEpoch, Ea as DirectPathWatch, Ei as dialDescriptors, Eo as readHandoffAck, Es as turnPutSummary, Et as normalizeAtprotoHandle, Fa as webLocalFetch, Fi as identityStatement, Fn as decodeBolt11, Fr as pinIsNewer, Fs as turnKeys, G as groupWakeFrame, Ga as emptyLinkRecords, Gn as LIVENESS_PING_MS, Gr as checkPushEndpoint, Gs as budgetRetryMs, H as readKnocks, Hi as validExternalKey, Hn as parseFedimintRequestPayload, Hr as parseWakeFrame, Hs as normalizeRelayUrl, I as decodeGroupEntryLink, Ii as newIdentityBinding, In as ENDPOINT, Ir as pinNumberHolds, Is as turnName, J as GROUP_LIMITS, Ja as serviceIdFromName, Ji as DHT_MESSAGE_TTL, Jn as engineText, Jo as enrollDigits, Jr as chatAppId, Js as PacketTooLargeError, K as groupWakes, Ka as HTTP_SERVICE_PROTO, Kn as pairedMessageFrame, Ko as enrollCancelFrame, Kr as vapidKeysMatch, Ks as isDiscoveryBudgetError, L as encodeGroupEntryLink, La as RTC_CONFIG, Li as receivedIdentityStatus, Ln as cashuRequestPayload, Lo as readSetUpdate, Lr as readPin, Ls as turnStatedVerifier, M as communityHasFrame, Ma as GhostlyHttpError, Mo as readHandoffTurnFrame, Mr as PIN_LIMITS, Ms as signTurnPacket, N as decodeCommunityLink, Na as formatLocalTarget, Ni as emptyIdentityLedger, No as readHandoffVerified, Nr as mayPin, Ns as signTurnRelease, O as sealGroupTyping, Oa as DHT_POLL_INTERVALS, Oo as readHandoffDone, Or as TypingSender, Os as TURN_MAX, P as encodeCommunityLink, Pa as parseLocalTarget, Pi as identityRevocationValue, Po as sealHandoffPiece, Pr as nextPinNumber, Ps as turnInstance, Q as frameBytes, Qa as HANDOFF_HAVE, Qi as EDIT_BUFFER_MS, Qo as enrollProofFrame, Qr as EDIT_CAPABILITY, Qt as didDhtDocument, R as entryParams, Ri as revokesIdentity, Rn as fedimintRequestPayload, Ro as setAckFrame, Rr as WAKE_CALL_INTERVAL_MS, Rs as turnVerifierMark, S as readAppBundleAsync, Sa as commitHash, Sn as isBitcoinAddress, So as handoffRetryAfter, Sr as ChatFiles, T as GroupTypingBoard, Ta as edgeParams, Ti as capsDescriptors, To as newHandoffId, Tr as transferStalled, Ts as classifyTurnRead, Tt as isAtprotoDid, Ua as parseCallSignal, Ui as TRANSPORTS, Un as GhostLink, Ur as relayRequest, V as mergeKnocks, Vi as emptyProofLedger, Vn as parseCashuRequestPayload, Vo as verifySetUpdate, Vr as newWakeToken, Vs as currentRelays, W as GroupWakeLimiter, Wi as automaticTransport, Wn as INVITE_TAKEN, Wr as wakeRequest, Ws as traceLink, X as GROUP_READ_NOTE, Xa as HANDOFF_CANCEL, Xi as DHT_FILE_CAPABILITY, Xo as enrollGrantFrame, Xr as APPS_CAPABILITY, Xs as createRelayPayload, Ya as HANDOFF_ACK, Yi as peekDhtMailbox, Yn as CarriedTransport, Yo as enrollDoneFrame, Yr as isAppVersion, Z as GroupSession, Za as HANDOFF_DONE, Zi as dhtFileId, Zo as enrollHelloFrame, Zr as BUTTONS_CAPABILITY, _a as readReply, _n as walletNetworkOf, _o as handoffHelloFrame, _r as rankHubs, _s as deviceLinkPairing, a as planAppUpdate, aa as COMMUNITY_REACTION_FRAME, ai as HoldRefusedError, an as EVM_TEST_CHAINS, ao as HANDOFF_VERIFIED, ar as pickMeshHubs, as as OwnInviteError, at as groupName, b as pacer, bn as sparkAddressKind, bo as handoffPakeFrame, br as readLobby, bs as seedSigner, ca as ReactionWindow, ci as readManifest, cn as SEPOLIA_TEST_USDT, co as handoffAttemptFailed, cr as beaconRecords, cs as encodeInviteCode, d as APP_BUNDLE_LIMITS, da as reactionEmoji, dn as USDT_PUBLIC_RPC, do as handoffCancelFrame, dr as lobbyKeys, ds as DEVICES_CAPABILITY, ea as RateWindow, ec as createIdentity, ei as UPGRADE_CAPABILITY, en as encodeDidDhtDeactivated, eo as HANDOFF_OFFER, er as groupSignalFrame, es as readEnrollHello, f as APP_BUNDLE_MAGIC, fa as reactionIsNewer, fn as WALLET_NETWORKS, fo as handoffContext, fr as lobbyRecords, fs as ENROLL_CAPABILITY, g as appViewOf, ga as pairedReplyAuthor, gn as validatePaymentTarget, go as handoffEphemeral, gr as pickHubs, gs as deviceKeyZ32, h as appDigest, ha as wireReaction, hn as formatPaymentAmount, ho as handoffDigest, hr as newerHead, hs as deviceEchoNonce, i as checkAppBeforeRun, ia as readForwarded, ic as verify, ii as HoldKeys, in as ETHEREUM_USDT, io as HANDOFF_TIMINGS, ir as meshHubs, is as readDeviceInvite, it as mentionsMember, j as GROUP_READ_NOTE_COMMUNITY, ja as presenceSeenAt, ji as IdentityExchange, jo as readHandoffPake, js as readTurnPacket, k as COMMUNITY_LIMITS, ki as IDENTITY_REVOCATION_LABEL, ko as readHandoffHave, ks as firstDeviceSetSecret, l as readAppStoreSteps, la as nextReactionNumber, lo as handoffAttemptSucceeded, lr as doorHubs, ma as validReactionNumber, mn as assertWholeSats, mo as handoffDataFrame, mr as mergeLobby, ms as deviceEchoFrame, n as appStoreDecision, na as parseLinkPreview, nc as identityFromSeedB64, ni as HOLD_DATED_BACK_MS, no as HANDOFF_PIECE_BYTES, nr as MESH_HUBS, ns as createDeviceInvite, nt as carryMentions, o as APP_STORE_LIMITS, oa as GROUP_REACTION_FRAME, oc as sanitizeAvatar, oi as isHoldUrl, on as ONCHAIN_PROVIDER, oo as filePartName, or as COMMUNITY_TOPOLOGY, os as createChatInvite, p as APP_ICON_PATH, pa as readReaction, pn as assertTokenUnits, po as handoffContextProof, pr as mergeBeacon, ps as HANDOFF_CAPABILITY, q as parseGroupWakeFrame, qa as LEGACY_SERVICES, qn as engineError, qo as enrollCancelReason, qs as withRequestOptions, r as appUpdateDecision, ra as forwardedAgain, rc as publicKeyFromZ32, ri as HOLD_LIMITS, rn as signDidDhtPacket, ro as HANDOFF_REQUEST, rr as mayBeHub, rs as deviceInviteJoinerParams, rt as parseCommunityEdit, sa as REACTION_LIMITS, si as newMailbox, sn as PaymentPreflightError, so as handoffAttemptAllowed, sr as beaconKeys, ss as decodeInviteCode, t as appRevocationFor, ta as dhtEditId, tc as identityFromSeed, ti as HOLD_CAPABILITY, tn as encodeDidDhtPacket, to as HANDOFF_PAKE, tr as readGroupSignal, ts as verifyEnrollProof, tt as GROUP_EDIT_TEXT_BYTES, ua as queueReaction, un as TEST_USDT_FAUCET_AMOUNT, uo as handoffBusyFrame, ur as freshHubs, us as inviteOwnership, va as replySnippet, vn as SPARK_NETWORKS, vo as handoffManifestFrames, vr as readBeacon, vs as devicePingFrame, wa as verifyCommitSignature, wi as DHT_TEXT_CAPABILITY, wn as isPubkyKey, wo as handoffVersions, wr as transferEnded, ws as TURN_SETTLE_MS, xa as MEMBER_KEY, xn as sparkInvoiceDetails, xo as handoffReleaseFrame, xr as shouldBeHub, xs as webCryptoSigner, y as isAppUrl, ya as GROUP_MEMBER_CAP, yn as SPARK_PROVIDER, yo as handoffOfferFrame, yr as readBeaconHead, ys as newDeviceSetSecret, z as knockIdentity, zi as lastSharedWithMe, zn as findEndpoint, zo as setUpdateFrame, zr as WakeLimiter, zs as DEFAULT_RELAYS } from "./src-DhfGgAUu.js";
4
+ 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-B5L8wuOm.js";
5
+ import { a as sealSeed, i as newDeviceKey, o as unsealSeed, r as intentRepository } from "./networkReason-DxZWHw6v.js";
6
+ import { $ as SourceConfigError, A as identityProviders, B as verifyIdentity, C as FileAppender, D as streamStored, E as storedSize, F as onAdaptersChanged, G as normalizePhrase, H as LightningCards, I as registeredLightningProviders, J as sourceKey, K as lightningPaying, L as registeredOnchainProviders, M as resolveAtprotoDid, N as setOwnDidSource, P as chosenResolver, Q as PROVIDER_PLATFORMS, R as reserveAdapterIds, S as WALLET_TYPES, T as removeStored, U as cardsKey, V as CASHU_CARD, W as isRecoveryPhrase, X as cashuMint, Y as CASHU_MINT_SOURCE, Z as NothingSpentError, _ as crossNetwork, _t as setAwayWallets, a as storedGroupFiles, at as breezDatabaseInUse, b as walletInstances, c as receivedPairedReply, ct as iceServerProblem, d as paymentAlias, dt as ModeGate, et as SourceUnreachableError, f as paymentWireId, ft as WrongNetworkError, g as createTiming, gt as requireTurn, h as createFailure, ht as refuseAway, i as groupOfFile, it as breezDatabase, j as assertPublicServer, l as replyRef, lt as iceServerSetting, m as assertConfirmedReal, mt as awayFrom, n as groupFileId, nt as networkMode, o as groupReply, ot as breezDatabasesFor, p as WALLET_NAMES, pt as networkLabel, q as ProviderSources, r as groupFileText, rt as redact, s as pairedWireReply, st as dropBreezDatabase, t as GroupFileDesk, tt as isNothingSpentError, u as replyTo, ut as ModeChanged, v as paymentNetwork, vt as setSingleWriterGate, w as readStored$1, y as paymentNetworksOf, z as boundedIdentityFetch } from "./groupFiles-BNTSDaoq.js";
7
+ import { i as fedimintDatabase, n as createIrohWebEndpoint, o as loadFedimintSdk, r as irohRelayProblem, t as DEFAULT_IROH_RELAYS } from "./irohWeb--YyEA-hX.js";
8
+ import { c as isTestMint, d as paysItsOwnInvoices, l as isWorthlessMint, n as DEFAULT_MINTS, o as fakesLightning, r as TEST_MINT, t as BITCOIN_INVOICE_ON_TESTNET, u as mintNetwork } from "./mints-BrlY773j.js";
9
+ import { c as fileSpace, f as FILE_BYTES_STEP, o as fileBytes, r as blobDigest, s as fileBytesOf, u as removeFileBytes } from "./fileBytes-Di7VZtmU.js";
10
+ import { n as MAX_BACKUP_BYTES, t as BACKUP_MEDIA_TYPE } from "./envelope-BvU0fLEt.js";
11
+ import { t as hyperdhtRelayProblem } from "./hyperdhtRelay-BT_wRxab.js";
7
12
  import { hkdf } from "@noble/hashes/hkdf.js";
8
13
  import { sha256 } from "@noble/hashes/sha2.js";
9
14
  import { base64, base64url, base64urlnopad, bech32, hex } from "@scure/base";
10
15
  import { bytesToHex, concatBytes, hexToBytes, utf8ToBytes } from "@noble/hashes/utils.js";
11
16
  import { chacha20poly1305 } from "@noble/ciphers/chacha.js";
12
17
  import { secp256k1 } from "@noble/curves/secp256k1.js";
13
- import { wordlist } from "@scure/bip39/wordlists/english.js";
14
- import { decode, noteEncode, npubEncode } from "nostr-tools/nip19";
15
- import { finalizeEvent, getPublicKey, verifyEvent } from "nostr-tools/pure";
16
18
  import { generateMnemonic, mnemonicToSeedSync, validateMnemonic } from "@scure/bip39";
19
+ import { wordlist } from "@scure/bip39/wordlists/english.js";
17
20
  import { Amount, MeltChangeError, OutputData, Wallet, decodePaymentRequest, deserializeSwapPreview, getEncodedToken, getTokenMetadata, isMintOperationError, serializeSwapPreview } from "@cashu/cashu-ts";
18
21
  import { Address, OutScript, Transaction } from "@scure/btc-signer";
19
22
  import * as nip04 from "nostr-tools/nip04";
20
23
  import * as nip44 from "nostr-tools/nip44";
24
+ import { finalizeEvent, getPublicKey, verifyEvent } from "nostr-tools/pure";
25
+ import { decode, noteEncode, npubEncode } from "nostr-tools/nip19";
21
26
  /** Evidence older than this says nothing about the clock now. */
22
27
  var CLOCK_EVIDENCE_MS = 18e5;
23
28
  /** Sources kept of each kind: a profile has a few relays, and may have hundreds of contacts. */
@@ -180,7 +185,7 @@ async function migrateWalletNetworks(now = Date.now()) {
180
185
  //#endregion
181
186
  //#region ../browser/src/engine/paymentAdapters/usdtWallet.ts
182
187
  /** 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-Qf4UuygS.js");
188
+ var loadUsdt = () => import("./usdt-C1Xao2xH.js");
184
189
  /** Every new profile starts with this wallet: an address to receive on, nothing to set up. */
185
190
  var DEFAULT_USDT = {
186
191
  network: "ethereum",
@@ -529,21 +534,6 @@ var UsdtWallet = class {
529
534
  }
530
535
  };
531
536
  //#endregion
532
- //#region ../browser/src/shared/ice.ts
533
- /**
534
- * What is wrong with a TURN/STUN server, or null. Browsers refuse to create any WebRTC connection when
535
- * one server in the list is malformed, so a bad entry here would silently cut off calls and files.
536
- */
537
- function iceServerProblem(server) {
538
- const urls = server.urls.split(/[\s,]+/).filter(Boolean);
539
- if (!urls.length) return "Enter the TURN server address";
540
- for (const url of urls) {
541
- if (!/^(stun|turns?):[^\s/?#]+(\?transport=(udp|tcp))?$/i.test(url)) return `“${url}” is not a TURN or STUN address (turn:host:port)`;
542
- if (/^turns?:/i.test(url) && (!server.username?.trim() || !server.credential?.trim())) return "A TURN server needs its username and credential";
543
- }
544
- return null;
545
- }
546
- //#endregion
547
537
  //#region ../browser/src/engine/paymentAdapters/backup.ts
548
538
  /**
549
539
  * Wallet database versions a backup may carry. 3 is what SDK 0.4.74-0.4.76 made (Ghostly 1.1.3 and earlier);
@@ -642,7 +632,7 @@ async function restoreArkDatabase(walletId, snapshot) {
642
632
  //#endregion
643
633
  //#region ../browser/src/engine/paymentAdapters/arkWallet.ts
644
634
  /** 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-CdnKGDbg.js");
635
+ var loadArkade = () => import("./arkade-D4BVJFEQ.js");
646
636
  /** How long a Testnet wallet about to make way for another waits for its server to say what it holds. */
647
637
  var RETIRE_READ_MS = 1e4;
648
638
  /** Every new profile starts with this wallet: nothing to set up before receiving. */
@@ -11154,18 +11144,39 @@ var DEFAULT_APP_STORES = [{
11154
11144
  url: "https://raw.githubusercontent.com/MiguelMedeiros/ghostly-store/HEAD/ghostly-store.json",
11155
11145
  key: "y379ia3t1urwudj8o4w1qwmuwp1mcxyf956b5r7pqup7stce6diy"
11156
11146
  }];
11147
+ /**
11148
+ * The apps that come with Ghostly. A short list kept here on purpose, never "everything in the official store": an
11149
+ * entry is added with the pin of its bundle in apps/ui/src/assets/apps/built-in.json, which the build reads from the
11150
+ * app's own repository (apps/ui/scripts/built-in-apps.mjs); no bundle is kept in this repository.
11151
+ *
11152
+ * Chess: Ghostly's own publisher key for it (fingerprint odcg w6wj w8dy nqop), version 2.3.0 (`sequence` 4), at the
11153
+ * commit of its repository that the official store lists.
11154
+ */
11155
+ var BUILT_IN_APPS = [{
11156
+ publisher: "odcgw6wjw8dynqop84r47jbjcdqossbgrfiejd367e14hgxmjcho",
11157
+ name: "chess",
11158
+ digest: "EEnFmP0grTkTe_9qgapQsEEZzWB9FcpoimWkHnfecVs",
11159
+ url: "https://cdn.jsdelivr.net/gh/MiguelMedeiros/ghostly-chess@c665ab194478145fd9b9a4777e6c0d1893969649/app.ghostlyapp",
11160
+ title: "Chess",
11161
+ tagline: "Play chess with a contact, live in your chat"
11162
+ }];
11157
11163
  var APP_STORAGE_LIMITS = {
11158
11164
  /** A key, in UTF-8 bytes. */
11159
11165
  keyBytes: 256,
11160
11166
  /** A value, as compact JSON in UTF-8 bytes. */
11161
11167
  valueBytes: 65536,
11162
11168
  /**
11163
- * Counted for each key beside its own bytes and its value's: what the row costs on the device (the app's reference,
11164
- * the scope and the key again, in the row's own key and in its two indexes). Without it a scope of one-byte values
11165
- * held 20 times its 5 MiB.
11169
+ * Counted for each key beside its own bytes and its value's: what the row costs on the device (the app's reference and
11170
+ * the scope, in the row's own key and in its two indexes). Without it a scope of one-byte values held 20 times its 5 MiB.
11166
11171
  */
11167
11172
  rowBytes: 128,
11168
- /** Every key and value of one app in one scope, with `rowBytes` for each key. */
11173
+ /**
11174
+ * How many times a key's bytes count. The device keeps a key four times (the row's own key, its value and both
11175
+ * indexes), three of them as UTF-16, about 10 bytes for each byte of the key: counted once, a scope of 256-byte keys
11176
+ * took 40 to 55 MB for its 5 MiB.
11177
+ */
11178
+ keyTimes: 8,
11179
+ /** Every key and value of one app in one scope, with `rowBytes` for each key and its bytes `keyTimes` over. */
11169
11180
  scopeBytes: 5242880
11170
11181
  };
11171
11182
  var APP_CHECK_TIMINGS = {
@@ -11178,22 +11189,74 @@ var APP_CHECK_TIMINGS = {
11178
11189
  /** Revocations are checked this long at a time before the page's thread is let go. */
11179
11190
  var REVOCATIONS_SLICE_MS = 10;
11180
11191
  /**
11192
+ * `ghostly-revoke.json` files whose result is kept, so an unchanged one is not checked again (the oldest dropped first):
11193
+ * at most this many, and of the ones whose list is held at most one file's limit of bytes in all. A publisher's file is
11194
+ * small; nine large ones beside one app's URLs, or one that changes at every check, would stay for the engine's life.
11195
+ */
11196
+ var REVOCATION_FILES_KEPT = {
11197
+ max: 64,
11198
+ bytes: APP_FETCH_LIMITS.revocationsBytes
11199
+ };
11200
+ /**
11181
11201
  * Revocations of store indexes whose signature was checked, kept by `appRevocationId` so an index that changes around
11182
11202
  * them does not have each checked again (the one read longest ago dropped first): four indexes' worth, about 1 MiB.
11183
11203
  */
11184
11204
  var STORE_REVOCATIONS_KEPT = 4 * APP_STORE_LIMITS.revoked;
11185
- /** `ghostly-revoke.json` files whose result is kept, so an unchanged one is not checked again (the oldest dropped first). */
11186
- var REVOCATION_FILES_KEPT = 64;
11187
11205
  /** A fetched bundle kept for its install screen: at most this many, for this long. */
11188
11206
  var STAGED = {
11189
11207
  max: 4,
11190
11208
  ms: 6e5
11191
11209
  };
11210
+ /**
11211
+ * Checked bundles kept in memory: the most recently used `min`, and beyond them each one a running app used (`entry`,
11212
+ * `file`, `fileAt`) within `ms`. The engine is not told when an app closes; one not used for that long goes, and is read
11213
+ * and checked again if it still runs.
11214
+ */
11215
+ var BUNDLES_KEPT = {
11216
+ min: 2,
11217
+ ms: 6e4
11218
+ };
11192
11219
  /** The first bytes of a bundle: magic, the manifest's length and the largest manifest. */
11193
11220
  var PEEK_BYTES = APP_BUNDLE_MAGIC.length + 4 + APP_BUNDLE_LIMITS.manifestBytes;
11194
11221
  var bundleId = (digest) => `app-${digest}`;
11195
- /** What a row takes of its scope's 5 MiB: its key and value, and `rowBytes`. Not kept in the row, so rows from before count alike. */
11196
- var costOf = (row) => APP_STORAGE_LIMITS.rowBytes + row.size;
11222
+ /**
11223
+ * The key of a scope's counter row (`AppScopeCount` as JSON in `value`, `size` 0): an app's key is never empty
11224
+ * (`keyOf`), and the app's own rows of a scope are `keysOf`, which leaves it out.
11225
+ */
11226
+ var COUNT_KEY = "";
11227
+ var NO_ROWS = {
11228
+ rows: 0,
11229
+ keyBytes: 0,
11230
+ size: 0
11231
+ };
11232
+ /**
11233
+ * What rows take of their scope's 5 MiB: `rowBytes` each, their keys `keyTimes` over and their values. Not kept, so a
11234
+ * count from before counts alike.
11235
+ */
11236
+ var costOf = ({ rows, keyBytes, size }) => rows * APP_STORAGE_LIMITS.rowBytes + size + (APP_STORAGE_LIMITS.keyTimes - 1) * keyBytes;
11237
+ /** One row as a count, `sign` -1 to take it away. */
11238
+ var countOf = ({ key, size }, sign = 1) => ({
11239
+ rows: sign,
11240
+ keyBytes: sign * byteLength(key),
11241
+ size: sign * size
11242
+ });
11243
+ var plus = (a, b) => ({
11244
+ rows: a.rows + b.rows,
11245
+ keyBytes: a.keyBytes + b.keyBytes,
11246
+ size: a.size + b.size
11247
+ });
11248
+ function readCount(value) {
11249
+ try {
11250
+ const c = JSON.parse(value);
11251
+ return [
11252
+ c.rows,
11253
+ c.keyBytes,
11254
+ c.size
11255
+ ].every((n) => Number.isSafeInteger(n) && n >= 0) ? c : null;
11256
+ } catch {
11257
+ return null;
11258
+ }
11259
+ }
11197
11260
  /**
11198
11261
  * A listing's URLs this client reads, in order, a jsDelivr copy first: one this client reads names a full commit, so it
11199
11262
  * holds exactly the version the store reviewed, where a raw `HEAD` may have moved on to one it has not (WISP 1200 §
@@ -11209,6 +11272,46 @@ function fail(code, words) {
11209
11272
  throw new Error(`${code}: ${words}`);
11210
11273
  }
11211
11274
  /**
11275
+ * The key, in `STORES.settings`, of what the person did about the apps that come with Ghostly (one of
11276
+ * `PROFILE_SETTINGS_KEYS`: "Clear all data" drops it, and the profile is offered them as a new one is).
11277
+ */
11278
+ var BUILT_IN_APPS_KEY = "appBuiltIns";
11279
+ /** The marks as kept, read with nothing taken on trust: a record that is not one is no mark. */
11280
+ function builtInMarks(kept) {
11281
+ const removed = {};
11282
+ const held = kept?.removed;
11283
+ if (held && typeof held === "object") {
11284
+ for (const [ref, at] of Object.entries(held)) if (isAppRef(ref) && typeof at === "number") removed[ref] = at;
11285
+ }
11286
+ return { removed };
11287
+ }
11288
+ /** Queues, on the settings store of an open transaction, a change of the built-in apps' marks. */
11289
+ function markBuiltIns(settings, change) {
11290
+ const read = settings.get(BUILT_IN_APPS_KEY);
11291
+ read.onsuccess = () => {
11292
+ const marks = builtInMarks(read.result);
11293
+ change(marks);
11294
+ settings.put(marks, BUILT_IN_APPS_KEY);
11295
+ };
11296
+ }
11297
+ var builtInRef = (app) => appRef(app.publisher, app.name);
11298
+ /**
11299
+ * The entries of the built-in list this profile is offered (WISP 1200 § Built-in apps): each reference once, not
11300
+ * installed (`installed`, by reference) and not removed by the person. An entry that is not a whole one (its key, its
11301
+ * name, its digest, a URL off the list of `appFetch.ts`, no title or tagline) is skipped, as a build with no bundle
11302
+ * for it would be.
11303
+ */
11304
+ function builtInsDue(list, installed, marks) {
11305
+ const due = /* @__PURE__ */ new Map();
11306
+ for (const app of list) {
11307
+ if (!isAppKey(app.publisher) || !isAppHash(app.digest) || !isAppFetchUrl(app.url) || !app.title || !app.tagline) continue;
11308
+ const ref = builtInRef(app);
11309
+ if (!isAppRef(ref) || due.has(ref) || installed.has(ref) || marks.removed[ref] !== void 0) continue;
11310
+ due.set(ref, app);
11311
+ }
11312
+ return [...due.values()];
11313
+ }
11314
+ /**
11212
11315
  * Why a pasted link is not read: a github.com page that is neither a repository nor a file of one (`github-link`), so
11213
11316
  * the person is not told GitHub is off the list; any other host (`host`).
11214
11317
  */
@@ -11231,8 +11334,26 @@ function withRevocations(app, read) {
11231
11334
  revocations: [...merged.values()]
11232
11335
  } : app;
11233
11336
  }
11234
- /** Rows of one app (`[ref]`), or of one app in one scope (`[ref, scope]`), in `STORES.appStorage`. */
11337
+ /** Rows of one app (`[ref]`), or of one app in one scope (`[ref, scope]`), in `STORES.appStorage`, counter rows included. */
11235
11338
  var rowsOf = (...prefix) => IDBKeyRange.bound(prefix, [...prefix, []]);
11339
+ /** The app's own rows of one scope: its counter row left out. */
11340
+ var keysOf = (ref, scope) => IDBKeyRange.bound([
11341
+ ref,
11342
+ scope,
11343
+ COUNT_KEY
11344
+ ], [
11345
+ ref,
11346
+ scope,
11347
+ []
11348
+ ], true);
11349
+ /** A scope's counter row (`COUNT_KEY`). */
11350
+ var counterRow = (ref, scope, count) => ({
11351
+ ref,
11352
+ scope,
11353
+ key: COUNT_KEY,
11354
+ value: JSON.stringify(count),
11355
+ size: 0
11356
+ });
11236
11357
  function done(tx) {
11237
11358
  return new Promise((resolve, reject) => {
11238
11359
  tx.oncomplete = () => resolve();
@@ -11265,8 +11386,10 @@ function peekAppManifest(bytes) {
11265
11386
  var Apps = class {
11266
11387
  host;
11267
11388
  staged = /* @__PURE__ */ new Map();
11268
- /** Bundles read back and checked, by digest, least recently used first: the ones running are read once, not on every `file`. */
11389
+ /** Bundles read back and checked, by digest, least recently used first (`BUNDLES_KEPT`): the ones running are read once, not on every `file`. */
11269
11390
  verified = /* @__PURE__ */ new Map();
11391
+ /** When a running app last used its bundle, by digest (`BUNDLES_KEPT`): an icon drawn is not a use. */
11392
+ runUsed = /* @__PURE__ */ new Map();
11270
11393
  /** Reads of a bundle under way, by digest: icons drawn at once share one read. */
11271
11394
  verifying = /* @__PURE__ */ new Map();
11272
11395
  /**
@@ -11274,7 +11397,7 @@ var Apps = class {
11274
11397
  * page, in a chat's cards and in the composer's Apps is read without the whole bundle again.
11275
11398
  */
11276
11399
  icons = /* @__PURE__ */ new Map();
11277
- /** Bytes used per `ref\0scope`, counted once from the rows and kept in step by the writes here. */
11400
+ /** What each `ref\0scope` holds, read once from its counter row and kept in step by the writes here. */
11278
11401
  usage = /* @__PURE__ */ new Map();
11279
11402
  queues = /* @__PURE__ */ new Map();
11280
11403
  timer = null;
@@ -11298,7 +11421,7 @@ var Apps = class {
11298
11421
  * index listing the same is not peeked again at every check.
11299
11422
  */
11300
11423
  listingMisses = /* @__PURE__ */ new Map();
11301
- /** `ghostly-revoke.json` files checked, by the SHA-256 of their bytes: their revocations, or null when refused. */
11424
+ /** `ghostly-revoke.json` files checked, by the SHA-256 of their bytes: their revocations, or null when refused, and the bytes the held list counts for. */
11302
11425
  revocationFiles = /* @__PURE__ */ new Map();
11303
11426
  /** Revocations read in a store's index and verified, by `appRevocationId`: at most `STORE_REVOCATIONS_KEPT`. */
11304
11427
  storeRevocations = /* @__PURE__ */ new Set();
@@ -11391,6 +11514,59 @@ var Apps = class {
11391
11514
  if (!isAppRef(ref)) fail("bad-ref", "Not an app reference");
11392
11515
  return await this.app(ref) ?? fail("not-installed", "This app is not installed");
11393
11516
  }
11517
+ /** The built-in list's entry of this reference, if it has one. */
11518
+ builtIn(ref) {
11519
+ return (this.host.builtIns ?? BUILT_IN_APPS).find((app) => builtInRef(app) === ref);
11520
+ }
11521
+ async builtInMarks() {
11522
+ return builtInMarks(await wrap((await store(STORES$1.settings, "readonly")).get(BUILT_IN_APPS_KEY)));
11523
+ }
11524
+ /**
11525
+ * The built-in apps this profile is offered (WISP 1200 § Built-in apps): shown with the installed ones, with nothing
11526
+ * stored and nothing granted. Opening one is an install like any other, from the bundle the build carries.
11527
+ */
11528
+ async builtIns() {
11529
+ const list = this.host.builtIns ?? BUILT_IN_APPS;
11530
+ if (!list.length || !this.host.readBuiltIn) return [];
11531
+ const [installed, marks] = await Promise.all([this.installed(), this.builtInMarks()]);
11532
+ return builtInsDue(list, new Set(installed.map((a) => a.ref)), marks).map((app) => ({
11533
+ ref: builtInRef(app),
11534
+ publisher: app.publisher,
11535
+ name: app.name,
11536
+ fingerprint: appFingerprint(app.publisher),
11537
+ digest: app.digest,
11538
+ url: app.url,
11539
+ title: app.title,
11540
+ tagline: app.tagline
11541
+ }));
11542
+ }
11543
+ /**
11544
+ * The bundle the build carries for a built-in app, checked as a fetched one is, and against its entry: the digest the
11545
+ * list pins and the entry's reference. Anything else in that file is not this app, and nothing of it is kept.
11546
+ */
11547
+ async bundled(app) {
11548
+ if (!this.host.readBuiltIn) fail("not-carried", "This build does not carry this app");
11549
+ let bytes;
11550
+ try {
11551
+ bytes = await this.host.readBuiltIn(app);
11552
+ } catch {
11553
+ fail("not-carried", "This build does not carry this app");
11554
+ }
11555
+ if (!(bytes instanceof Uint8Array) || bytes.length > APP_BUNDLE_LIMITS.bundleBytes) fail("not-carried", "This build does not carry this app");
11556
+ const read = await readAppBundleAsync(bytes);
11557
+ if (!read.ok) fail(read.reason, `This is not an app Ghostly can install${read.detail ? ` (${read.detail})` : ""}`);
11558
+ if (read.bundle.digest !== app.digest || versionOf(read.bundle.manifest, read.bundle.digest).ref !== builtInRef(app)) fail("unlisted", "The app this build carries is not the one it names");
11559
+ return {
11560
+ bundle: read.bundle,
11561
+ bytes
11562
+ };
11563
+ }
11564
+ /** The built-in entry of this reference while the profile is offered it (`builtIns`), else undefined. */
11565
+ async offered(ref) {
11566
+ const app = this.builtIn(ref);
11567
+ if (!app || !this.host.readBuiltIn) return void 0;
11568
+ return builtInsDue([app], await this.app(ref) ? /* @__PURE__ */ new Set([ref]) : /* @__PURE__ */ new Set(), await this.builtInMarks())[0];
11569
+ }
11394
11570
  /**
11395
11571
  * An installed app that may still run (WISP 1200 § Takedowns): a running app whose version its publisher revoked, or
11396
11572
  * a store of the person's removed (unless this run was started with Run anyway), is cut off with `stopped`, whether
@@ -11543,7 +11719,7 @@ var Apps = class {
11543
11719
  */
11544
11720
  async writeBundle(digest, bytes) {
11545
11721
  const id = bundleId(digest);
11546
- const expected = toBase64Url(sha256(bytes));
11722
+ const expected = toBase64Url(await sha256Paced(bytes));
11547
11723
  let files;
11548
11724
  let stored;
11549
11725
  try {
@@ -11588,6 +11764,7 @@ var Apps = class {
11588
11764
  }
11589
11765
  async removeBundle(kind, digest) {
11590
11766
  this.verified.delete(digest);
11767
+ this.runUsed.delete(digest);
11591
11768
  this.icons.delete(digest);
11592
11769
  await (await fileBytesOf(kind))?.remove(bundleId(digest)).catch(() => {});
11593
11770
  }
@@ -11597,11 +11774,13 @@ var Apps = class {
11597
11774
  if (app?.digest !== digest && app?.pending?.digest !== digest) await this.removeBundle(kind, digest);
11598
11775
  }
11599
11776
  /** The installed bundle read back and checked again before it runs (WISP 1200 § Signatures: "again before it runs one"). */
11600
- verifiedBundle(app) {
11777
+ verifiedBundle(app, running) {
11778
+ if (running) this.runUsed.set(app.digest, this.now());
11601
11779
  const kept = this.verified.get(app.digest);
11602
11780
  if (kept) {
11603
11781
  this.verified.delete(app.digest);
11604
11782
  this.verified.set(app.digest, kept);
11783
+ this.dropUnusedBundles();
11605
11784
  return Promise.resolve(kept);
11606
11785
  }
11607
11786
  const under = this.verifying.get(app.digest);
@@ -11609,11 +11788,10 @@ var Apps = class {
11609
11788
  const reading = (async () => {
11610
11789
  const bytes = await this.readBundleBytes(app.bytes, app.digest);
11611
11790
  if (!bytes) fail("needs-files", "This app's files are not on this device yet");
11612
- const read = readAppBundle(bytes);
11791
+ const read = await readAppBundleAsync(bytes);
11613
11792
  if (!read.ok || read.bundle.digest !== app.digest) fail("damaged", "This app's stored files do not match what was installed");
11614
11793
  this.verified.set(app.digest, read.bundle);
11615
- const room = Math.max(2, this.started.size + (this.started.has(app.ref) ? 0 : 1));
11616
- while (this.verified.size > room) this.verified.delete(this.verified.keys().next().value);
11794
+ this.dropUnusedBundles();
11617
11795
  this.icons.set(app.digest, read.bundle.files.get("icon.png")?.slice() ?? null);
11618
11796
  return read.bundle;
11619
11797
  })().finally(() => {
@@ -11622,9 +11800,21 @@ var Apps = class {
11622
11800
  this.verifying.set(app.digest, reading);
11623
11801
  return reading;
11624
11802
  }
11803
+ /**
11804
+ * Apps running at once do not push each other's bundle out, which every `file` read would then read and check again
11805
+ * whole; one closed (or only drawn as an icon) goes once two newer ones are kept, at the next bundle read or hit.
11806
+ */
11807
+ dropUnusedBundles() {
11808
+ for (const digest of this.verified.keys()) {
11809
+ if (this.verified.size <= BUNDLES_KEPT.min) break;
11810
+ if (this.now() - (this.runUsed.get(digest) ?? -Infinity) < BUNDLES_KEPT.ms) continue;
11811
+ this.verified.delete(digest);
11812
+ this.runUsed.delete(digest);
11813
+ }
11814
+ }
11625
11815
  async fetchBundle(url) {
11626
11816
  const bytes = await this.host.fetch(url, { maxBytes: APP_BUNDLE_LIMITS.bundleBytes });
11627
- const read = readAppBundle(bytes);
11817
+ const read = await readAppBundleAsync(bytes);
11628
11818
  if (!read.ok) fail(read.reason, `This is not an app Ghostly can install${read.detail ? ` (${read.detail})` : ""}`);
11629
11819
  return {
11630
11820
  bundle: read.bundle,
@@ -11640,7 +11830,7 @@ var Apps = class {
11640
11830
  */
11641
11831
  async readStoreAt(url, heldKey, held) {
11642
11832
  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 })]);
11643
- if (held?.index && held.digest !== void 0 && toBase64Url(sha256(indexBytes)) === held.digest) return {
11833
+ if (held?.index && held.digest !== void 0 && toBase64Url(await sha256Paced(indexBytes)) === held.digest) return {
11644
11834
  ok: true,
11645
11835
  store: {
11646
11836
  index: held.index,
@@ -11649,14 +11839,11 @@ var Apps = class {
11649
11839
  }
11650
11840
  };
11651
11841
  const steps = readAppStoreSteps(indexBytes, sigBytes, this.nowS(), heldKey, this.checkedRevocations);
11652
- let since = performance.now();
11842
+ const pause = pacer(REVOCATIONS_SLICE_MS);
11653
11843
  for (;;) {
11654
11844
  const step = steps.next();
11655
11845
  if (step.done) return step.value;
11656
- if (performance.now() - since >= REVOCATIONS_SLICE_MS) {
11657
- await new Promise((resolve) => setTimeout(resolve, 0));
11658
- since = performance.now();
11659
- }
11846
+ await pause();
11660
11847
  }
11661
11848
  }
11662
11849
  /**
@@ -11680,19 +11867,16 @@ var Apps = class {
11680
11867
  * engine shares the page's thread, and each entry is a signature check. The same bytes are not checked again.
11681
11868
  */
11682
11869
  async readRevocationsFile(bytes) {
11683
- const id = toBase64Url(sha256(bytes));
11870
+ const id = toBase64Url(await sha256Paced(bytes));
11684
11871
  const known = this.revocationFiles.get(id);
11685
- if (known !== void 0) return known;
11872
+ if (known !== void 0) return known.revocations;
11686
11873
  const read = readCanonicalJson(bytes);
11687
11874
  let out = null;
11688
11875
  if (read.ok && Array.isArray(read.value) && read.value.length <= APP_STATEMENT_LIMITS.revocations) {
11689
11876
  out = [];
11690
- let since = performance.now();
11877
+ const pause = pacer(REVOCATIONS_SLICE_MS);
11691
11878
  for (const entry of read.value) {
11692
- if (performance.now() - since >= REVOCATIONS_SLICE_MS) {
11693
- await new Promise((resolve) => setTimeout(resolve, 0));
11694
- since = performance.now();
11695
- }
11879
+ await pause();
11696
11880
  const one = readAppRevocation(entry);
11697
11881
  if (!one.ok) {
11698
11882
  out = null;
@@ -11702,8 +11886,17 @@ var Apps = class {
11702
11886
  }
11703
11887
  }
11704
11888
  this.revocationFiles.delete(id);
11705
- this.revocationFiles.set(id, out);
11706
- for (const old of this.revocationFiles.keys()) if (this.revocationFiles.size > REVOCATION_FILES_KEPT) this.revocationFiles.delete(old);
11889
+ this.revocationFiles.set(id, {
11890
+ revocations: out,
11891
+ bytes: out ? bytes.length : 0
11892
+ });
11893
+ let held = 0;
11894
+ for (const file of this.revocationFiles.values()) held += file.bytes;
11895
+ for (const [old, file] of this.revocationFiles) {
11896
+ if (this.revocationFiles.size <= REVOCATION_FILES_KEPT.max && held <= REVOCATION_FILES_KEPT.bytes) break;
11897
+ this.revocationFiles.delete(old);
11898
+ held -= file.bytes;
11899
+ }
11707
11900
  return out;
11708
11901
  }
11709
11902
  summary(s) {
@@ -11881,6 +12074,12 @@ var Apps = class {
11881
12074
  let urls;
11882
12075
  let expect = null;
11883
12076
  let fromStore;
12077
+ if ("builtIn" in source) {
12078
+ if (!isAppRef(source.builtIn)) fail("bad-ref", "Not an app reference");
12079
+ const app = await this.offered(source.builtIn) ?? fail("not-offered", "This app does not come with Ghostly here");
12080
+ const { bundle, bytes } = await this.bundled(app);
12081
+ return this.stage(bundle, bytes, app.url, void 0, true);
12082
+ }
11884
12083
  if ("url" in source) urls = [typeof source.url === "string" && appPasteUrl(source.url, "app.ghostlyapp") || notReadable(source.url, "Apps are read only from raw.githubusercontent.com, or from cdn.jsdelivr.net at a commit, over HTTPS")];
11885
12084
  else if ("store" in source) {
11886
12085
  const s = await this.storeRecord(source.store);
@@ -11898,6 +12097,11 @@ var Apps = class {
11898
12097
  const { ref, url, sequence, digest } = source.card;
11899
12098
  if (!isAppRef(ref)) fail("bad-ref", "Not an app reference");
11900
12099
  if (!isAppFetchUrl(url)) fail("host", "A card's app is read only from raw.githubusercontent.com, or from cdn.jsdelivr.net at a commit");
12100
+ const carried = typeof digest === "string" ? await this.offered(ref) : void 0;
12101
+ if (carried && carried.digest === digest) {
12102
+ const held = await this.bundled(carried).catch(() => null);
12103
+ if (held) return this.stage(held.bundle, held.bytes, carried.url, void 0, true);
12104
+ }
11901
12105
  urls = [url];
11902
12106
  expect = {
11903
12107
  ref,
@@ -11929,7 +12133,7 @@ var Apps = class {
11929
12133
  const oldest = this.staged.values().next().value;
11930
12134
  if (oldest) this.sweep = setTimeout(() => this.sweepStaged(), STAGED.ms - (this.now() - oldest.at));
11931
12135
  }
11932
- async stage(bundle, bytes, from, fromStore) {
12136
+ async stage(bundle, bytes, from, fromStore, carried = false) {
11933
12137
  for (const [digest, s] of this.staged) if (this.now() - s.at > STAGED.ms) this.staged.delete(digest);
11934
12138
  this.staged.delete(bundle.digest);
11935
12139
  while (this.staged.size >= STAGED.max) this.staged.delete(this.staged.keys().next().value);
@@ -11960,7 +12164,8 @@ var Apps = class {
11960
12164
  asks,
11961
12165
  run: this.runStatus(version, this.storeViews(stores), installed?.revocations),
11962
12166
  listedBy,
11963
- unknownPublisher: !listedBy.some((s) => s.kind === "curated")
12167
+ unknownPublisher: !listedBy.some((s) => s.kind === "curated"),
12168
+ ...carried && { carried: true }
11964
12169
  };
11965
12170
  }
11966
12171
  /**
@@ -12014,6 +12219,7 @@ var Apps = class {
12014
12219
  const kind = await this.writeBundle(digest, bytes);
12015
12220
  const next = this.versionRecord(installed, bundle, kind, from, fromStore);
12016
12221
  await this.putApp(next);
12222
+ if (!installed && this.builtIn(version.ref)) await this.keepBuiltIn(version.ref);
12017
12223
  if (installed && installed.digest !== digest) await this.removeBundle(installed.bytes, installed.digest);
12018
12224
  if (installed?.pending && next.pending?.digest !== installed.pending.digest && installed.pending.digest !== digest) await this.removeBundle(installed.pending.bytes, installed.pending.digest);
12019
12225
  this.staged.delete(digest);
@@ -12052,7 +12258,7 @@ var Apps = class {
12052
12258
  const installed = await this.installedOrFail(ref);
12053
12259
  const pending = installed.pending ?? fail("no-update", "No update waits for this app");
12054
12260
  const bytes = await this.readBundleBytes(pending.bytes, pending.digest);
12055
- const read = bytes ? readAppBundle(bytes) : null;
12261
+ const read = bytes ? await readAppBundleAsync(bytes) : null;
12056
12262
  if (!read?.ok || read.bundle.digest !== pending.digest) {
12057
12263
  const { pending: _p, ...rest } = installed;
12058
12264
  await this.putApp(rest);
@@ -12287,6 +12493,26 @@ var Apps = class {
12287
12493
  const app = await this.installedOrFail(ref);
12288
12494
  const stores = await this.stores();
12289
12495
  if (await this.hasBundle(app.bytes, app.digest)) return this.runCheckOf(app, stores);
12496
+ const carried = this.builtIn(app.ref);
12497
+ if (carried && carried.digest === app.digest) {
12498
+ const held = await this.bundled(carried).catch(() => null);
12499
+ if (held) {
12500
+ const kind = await this.writeBundle(app.digest, held.bytes);
12501
+ const now = await this.app(app.ref);
12502
+ if (!now || now.digest !== app.digest) {
12503
+ await this.removeBundle(kind, app.digest);
12504
+ fail("not-installed", "This app is not installed");
12505
+ }
12506
+ if (now.bytes !== kind) await this.putApp({
12507
+ ...now,
12508
+ bytes: kind
12509
+ });
12510
+ return this.runCheckOf({
12511
+ ...now,
12512
+ bytes: kind
12513
+ }, stores);
12514
+ }
12515
+ }
12290
12516
  const urls = [app.from];
12291
12517
  for (const s of stores) {
12292
12518
  if (app.store !== void 0 && s.key !== app.store) continue;
@@ -12294,6 +12520,14 @@ var Apps = class {
12294
12520
  }
12295
12521
  if (app.store === void 0) urls.push(...app.manifest.sources ?? []);
12296
12522
  for (const url of new Set(urls.filter(isAppFetchUrl))) {
12523
+ try {
12524
+ if (peekAppManifest(await this.host.fetch(url, {
12525
+ maxBytes: PEEK_BYTES,
12526
+ peek: true
12527
+ }))?.digest !== app.digest) continue;
12528
+ } catch {
12529
+ continue;
12530
+ }
12297
12531
  let fetched;
12298
12532
  try {
12299
12533
  fetched = await this.fetchBundle(url);
@@ -12341,7 +12575,7 @@ var Apps = class {
12341
12575
  if (run.status === "removed" && runAnyway !== true) fail("removed", `Removed by ${run.by[0].name}: ${run.by[0].reason}`);
12342
12576
  if (run.status === "needs-files") fail("needs-files", "This app's files are not on this device yet");
12343
12577
  if (run.status === "removed") this.anyway.add(`${app.ref} ${app.digest}`);
12344
- const bundle = await this.verifiedBundle(app);
12578
+ const bundle = await this.verifiedBundle(app, true);
12345
12579
  const entry = bundle.files.get(bundle.manifest.entry) ?? fail("damaged", "The entry is missing");
12346
12580
  this.ran.add(app.ref);
12347
12581
  const versions = this.started.get(app.ref) ?? /* @__PURE__ */ new Map();
@@ -12357,6 +12591,29 @@ var Apps = class {
12357
12591
  };
12358
12592
  }
12359
12593
  /**
12594
+ * Where a file of the installed bundle lies in this device's file storage, for a client that hands the bytes to the
12595
+ * app itself (Desktop: Rust reads them, checks their SHA-256 and gives them to the app's window, so they never cross
12596
+ * the Ghostly window). The same checks as `file`; null where the bundle is not in such storage, and for the icon,
12597
+ * which `file` answers from memory.
12598
+ */
12599
+ async fileAt({ ref, path, digest }) {
12600
+ if (path === "icon.png") return null;
12601
+ const app = await this.runningOrFail(ref, digest);
12602
+ if (digest !== void 0 && digest !== app.digest) fail("no-file", "No such file in this app");
12603
+ if (app.bytes !== "native") return null;
12604
+ const bundle = await this.verifiedBundle(app, true);
12605
+ const file = typeof path === "string" ? bundle.files.get(path) : void 0;
12606
+ const listed = bundle.manifest.files.find((f) => f.path === path);
12607
+ if (!file || !listed) fail("no-file", "No such file in this app");
12608
+ return {
12609
+ space: fileSpace(),
12610
+ id: bundleId(app.digest),
12611
+ offset: file.byteOffset,
12612
+ length: file.length,
12613
+ sha256: listed.sha256
12614
+ };
12615
+ }
12616
+ /**
12360
12617
  * A file of the installed bundle, for the broker's `file` (`ghostly.file(path)`): a copy, which the broker transfers to
12361
12618
  * the app as it is. `digest`: the version the frame runs.
12362
12619
  */
@@ -12366,7 +12623,7 @@ var Apps = class {
12366
12623
  const icon = path === "icon.png" ? this.icons.get(app.digest) : void 0;
12367
12624
  if (icon) return icon.slice();
12368
12625
  if (icon === null) fail("no-file", "No such file in this app");
12369
- const bundle = await this.verifiedBundle(app);
12626
+ const bundle = await this.verifiedBundle(app, path !== APP_ICON_PATH);
12370
12627
  if (typeof path !== "string" || !bundle.files.has(path)) fail("no-file", "No such file in this app");
12371
12628
  return bundle.files.get(path).slice();
12372
12629
  }
@@ -12397,6 +12654,7 @@ var Apps = class {
12397
12654
  run: await this.runCheckOf(app, stores),
12398
12655
  listedBy,
12399
12656
  unknownPublisher: !listedBy.some((s) => s.kind === "curated"),
12657
+ ...this.builtIn(app.ref)?.digest === app.digest && { builtIn: true },
12400
12658
  ...app.pending && { pending: {
12401
12659
  sequence: app.pending.sequence,
12402
12660
  version: app.pending.manifest.version,
@@ -12416,23 +12674,44 @@ var Apps = class {
12416
12674
  }
12417
12675
  /**
12418
12676
  * Uninstalls an app: its bundle (and a waiting update's), its record and its storage in every scope. The person chose
12419
- * it, after the offer of an export (`exportData`).
12677
+ * it, after the offer of an export (`exportData`). A built-in app is marked removed in the same transaction, and one
12678
+ * the person never opened is only marked: no start and no update offers it again (WISP 1200 § Built-in apps).
12420
12679
  */
12421
12680
  async uninstall({ ref }) {
12422
- const app = await this.installedOrFail(ref);
12423
- const tx = (await openDb()).transaction([STORES$1.apps, STORES$1.appStorage], "readwrite");
12424
- tx.objectStore(STORES$1.apps).delete(app.ref);
12425
- tx.objectStore(STORES$1.appStorage).delete(rowsOf(app.ref));
12681
+ if (!isAppRef(ref)) fail("bad-ref", "Not an app reference");
12682
+ const app = await this.app(ref);
12683
+ const builtIn = this.builtIn(ref) !== void 0;
12684
+ if (!app && !builtIn) fail("not-installed", "This app is not installed");
12685
+ const tx = (await openDb()).transaction([
12686
+ STORES$1.apps,
12687
+ STORES$1.appStorage,
12688
+ ...builtIn ? [STORES$1.settings] : []
12689
+ ], "readwrite");
12690
+ tx.objectStore(STORES$1.apps).delete(ref);
12691
+ tx.objectStore(STORES$1.appStorage).delete(rowsOf(ref));
12692
+ if (builtIn) markBuiltIns(tx.objectStore(STORES$1.settings), (marks) => {
12693
+ marks.removed[ref] ??= this.now();
12694
+ });
12426
12695
  try {
12427
12696
  await done(tx);
12428
12697
  } finally {
12429
- this.held.delete(app.ref);
12698
+ this.held.delete(ref);
12430
12699
  }
12431
- for (const key of [...this.usage.keys()]) if (key.startsWith(`${app.ref}\0`)) this.usage.delete(key);
12432
- this.started.delete(app.ref);
12700
+ for (const key of [...this.usage.keys()]) if (key.startsWith(`${ref}\0`)) this.usage.delete(key);
12701
+ this.started.delete(ref);
12702
+ if (!app) return;
12433
12703
  await this.removeBundle(app.bytes, app.digest);
12434
12704
  if (app.pending) await this.removeBundle(app.pending.bytes, app.pending.digest);
12435
12705
  }
12706
+ /** Takes the "removed by the person" mark off a built-in app, if it has one. */
12707
+ async keepBuiltIn(ref) {
12708
+ if ((await this.builtInMarks()).removed[ref] === void 0) return;
12709
+ const tx = (await openDb()).transaction(STORES$1.settings, "readwrite");
12710
+ markBuiltIns(tx.objectStore(STORES$1.settings), (marks) => {
12711
+ delete marks.removed[ref];
12712
+ });
12713
+ await done(tx);
12714
+ }
12436
12715
  scopeOf(scope) {
12437
12716
  if (scope === "alone" || typeof scope === "string" && this.host.isChat(scope)) return scope;
12438
12717
  fail("bad-scope", "Not a chat of this profile");
@@ -12451,13 +12730,24 @@ var Apps = class {
12451
12730
  });
12452
12731
  return run;
12453
12732
  }
12733
+ /**
12734
+ * What a scope holds: its counter row, when the number of rows agrees with it (one key count, no value read). A
12735
+ * scope without one (written before it was kept) or where it disagrees (rows written by an older client) is
12736
+ * counted from its rows once.
12737
+ */
12454
12738
  async usageOf(ref, scope) {
12455
12739
  const id = `${ref}\0${scope}`;
12456
- let used = this.usage.get(id);
12457
- if (used === void 0) {
12458
- used = (await wrap((await store(STORES$1.appStorage, "readonly")).index(APP_STORAGE_SCOPE_INDEX).getAll([ref, scope]))).reduce((sum, r) => sum + costOf(r), 0);
12459
- this.usage.set(id, used);
12460
- }
12740
+ const cached = this.usage.get(id);
12741
+ if (cached) return cached;
12742
+ const rows = await store(STORES$1.appStorage, "readonly");
12743
+ const [counter, n] = await Promise.all([wrap(rows.get([
12744
+ ref,
12745
+ scope,
12746
+ COUNT_KEY
12747
+ ])), wrap(rows.count(keysOf(ref, scope)))]);
12748
+ let used = counter ? readCount(counter.value) : n ? null : NO_ROWS;
12749
+ if (!used || used.rows !== n) used = (await wrap((await store(STORES$1.appStorage, "readonly")).getAll(keysOf(ref, scope)))).reduce((sum, r) => plus(sum, countOf(r)), NO_ROWS);
12750
+ this.usage.set(id, used);
12461
12751
  return used;
12462
12752
  }
12463
12753
  async storageGet({ ref, scope, key, digest }) {
@@ -12471,8 +12761,8 @@ var Apps = class {
12471
12761
  }
12472
12762
  /**
12473
12763
  * Keeps a JSON value under a key: at most 64 KiB, and 5 MiB for every key of this app in this scope, each counted
12474
- * with `rowBytes`. A scope already past that (filled before a key counted `rowBytes`) still takes a write that does
12475
- * not grow it, so the app can make room.
12764
+ * as `costOf` says. A scope already past that (filled under an older count) still takes a write that does not grow
12765
+ * it, so the app can make room.
12476
12766
  */
12477
12767
  async storageSet({ ref, scope, key, value, digest }) {
12478
12768
  const app = await this.runningOrFail(ref, digest);
@@ -12495,20 +12785,24 @@ var Apps = class {
12495
12785
  where,
12496
12786
  name
12497
12787
  ]));
12498
- const next = used - (old ? costOf(old) : 0) + APP_STORAGE_LIMITS.rowBytes + size;
12499
- if (next > APP_STORAGE_LIMITS.scopeBytes && next > used) fail("full", "This app's storage in this chat is full (5 MiB)");
12788
+ const next = plus(old ? plus(used, countOf(old, -1)) : used, countOf({
12789
+ key: name,
12790
+ size
12791
+ }));
12792
+ if (costOf(next) > APP_STORAGE_LIMITS.scopeBytes && costOf(next) > costOf(used)) fail("full", "This app's storage in this chat is full (5 MiB)");
12500
12793
  const writing = await store(STORES$1.appStorage, "readwrite");
12501
12794
  if (where !== "alone" && !this.host.isChat(where)) {
12502
12795
  this.usage.delete(`${app.ref}\0${where}`);
12503
12796
  fail("bad-scope", "Not a chat of this profile");
12504
12797
  }
12505
- await wrap(writing.put({
12798
+ writing.put({
12506
12799
  ref: app.ref,
12507
12800
  scope: where,
12508
12801
  key: name,
12509
12802
  value: text,
12510
12803
  size
12511
- }));
12804
+ });
12805
+ await wrap(writing.put(counterRow(app.ref, where, next)));
12512
12806
  this.usage.set(`${app.ref}\0${where}`, next);
12513
12807
  });
12514
12808
  }
@@ -12523,39 +12817,73 @@ var Apps = class {
12523
12817
  name
12524
12818
  ]));
12525
12819
  if (!old) return;
12526
- await wrap((await store(STORES$1.appStorage, "readwrite")).delete([
12820
+ const next = plus(used, countOf(old, -1));
12821
+ const writing = await store(STORES$1.appStorage, "readwrite");
12822
+ writing.delete([
12527
12823
  app.ref,
12528
12824
  where,
12529
12825
  name
12530
- ]));
12531
- this.usage.set(`${app.ref}\0${where}`, used - costOf(old));
12826
+ ]);
12827
+ await (next.rows ? wrap(writing.put(counterRow(app.ref, where, next))) : wrap(writing.delete([
12828
+ app.ref,
12829
+ where,
12830
+ COUNT_KEY
12831
+ ])));
12832
+ this.usage.set(`${app.ref}\0${where}`, next);
12532
12833
  });
12533
12834
  }
12534
12835
  async storageKeys({ ref, scope, digest }) {
12535
12836
  const app = await this.runningOrFail(ref, digest);
12536
- return (await wrap((await store(STORES$1.appStorage, "readonly")).getAllKeys(rowsOf(app.ref, this.scopeOf(scope))))).map((k) => k[2]);
12837
+ return (await wrap((await store(STORES$1.appStorage, "readonly")).getAllKeys(keysOf(app.ref, this.scopeOf(scope))))).map((k) => k[2]);
12537
12838
  }
12538
12839
  /** An app's storage in every scope, as export files (WISP 1200 § Where installed apps live), offered before an uninstall. */
12539
12840
  async exportData({ ref }) {
12540
- const app = await this.installedOrFail(ref);
12541
- const rows = await wrap((await store(STORES$1.appStorage, "readonly")).getAll(rowsOf(app.ref)));
12542
- const scopes = /* @__PURE__ */ new Map();
12543
- for (const row of rows) {
12544
- const out = scopes.get(row.scope) ?? {
12841
+ return (await this.exportDataText({ ref })).map(({ app, scope, entries }) => {
12842
+ const out = {
12545
12843
  ghostlyAppData: 1,
12546
- app: app.ref,
12547
- scope: row.scope,
12844
+ app,
12845
+ scope,
12548
12846
  entries: {}
12549
12847
  };
12550
- Object.defineProperty(out.entries, row.key, {
12551
- value: JSON.parse(row.value),
12848
+ for (const [key, json] of entries) Object.defineProperty(out.entries, key, {
12849
+ value: JSON.parse(json),
12552
12850
  enumerable: true,
12553
12851
  writable: true,
12554
12852
  configurable: true
12555
12853
  });
12556
- scopes.set(row.scope, out);
12854
+ return out;
12855
+ });
12856
+ }
12857
+ /**
12858
+ * `exportData` with each value as the JSON text it is kept as, for a file written without parsing and writing every
12859
+ * value again. Read a scope at a time, the thread let go between them: ten chats of 5 MiB read at once held it for
12860
+ * the whole read.
12861
+ */
12862
+ async exportDataText({ ref }) {
12863
+ const app = await this.installedOrFail(ref);
12864
+ const scopes = [];
12865
+ const index = (await store(STORES$1.appStorage, "readonly")).index(APP_STORAGE_SCOPE_INDEX);
12866
+ await new Promise((resolve, reject) => {
12867
+ const request = index.openKeyCursor(rowsOf(app.ref), "nextunique");
12868
+ request.onerror = () => reject(request.error);
12869
+ request.onsuccess = () => {
12870
+ const cursor = request.result;
12871
+ if (!cursor) return resolve();
12872
+ scopes.push(cursor.key[1]);
12873
+ cursor.continue();
12874
+ };
12875
+ });
12876
+ const out = [];
12877
+ for (const [i, scope] of scopes.entries()) {
12878
+ if (i) await new Promise((resolve) => setTimeout(resolve, 0));
12879
+ const rows = await wrap((await store(STORES$1.appStorage, "readonly")).getAll(keysOf(app.ref, scope)));
12880
+ if (rows.length) out.push({
12881
+ app: app.ref,
12882
+ scope,
12883
+ entries: rows.map((row) => [row.key, row.value])
12884
+ });
12557
12885
  }
12558
- return [...scopes.values()];
12886
+ return out;
12559
12887
  }
12560
12888
  };
12561
12889
  //#endregion
@@ -16680,7 +17008,7 @@ function handoffNotices(was, handoff, session) {
16680
17008
  * device link must offer every transport both devices run: a Linux Desktop has no WebRTC, and an Iroh relay that
16681
17009
  * one side cannot reach leaves HyperDHT as the only way through.
16682
17010
  */
16683
- var NATIVE_RETRY_MS = 1e4;
17011
+ var NATIVE_RETRY_MS$1 = 1e4;
16684
17012
  var NATIVE_RETRY_MAX_MS = 12e4;
16685
17013
  /** How often a standby reads the turn while its screen shows (WISP 06 § When a device checks), and the jitter on it. */
16686
17014
  var STANDBY_TURN_EVERY_MS = 6e5;
@@ -16989,7 +17317,7 @@ var DeviceLinks = class {
16989
17317
  }
16990
17318
  retryNative(running) {
16991
17319
  if (running.nativeRetry || this.stopped || this.running.get(running.id) !== running) return;
16992
- const wait = Math.min(NATIVE_RETRY_MS * 2 ** running.nativeMisses, NATIVE_RETRY_MAX_MS);
17320
+ const wait = Math.min(NATIVE_RETRY_MS$1 * 2 ** running.nativeMisses, NATIVE_RETRY_MAX_MS);
16993
17321
  running.nativeMisses++;
16994
17322
  running.nativeRetry = setTimeout(() => {
16995
17323
  running.nativeRetry = void 0;
@@ -19363,6 +19691,7 @@ var FileDesk = class {
19363
19691
  openSource: (record) => this.openSource(linkId, record),
19364
19692
  changed: (record, transferred, progress) => this.changed(linkId, record, transferred, progress),
19365
19693
  room: async () => (await fileBytes()).room(),
19694
+ ...this.deps.sendData && { sendData: (id, offset, data) => this.deps.sendData(linkId, id, offset, data) },
19366
19695
  writable: () => this.deps.writable?.(linkId)
19367
19696
  });
19368
19697
  this.chats.set(linkId, made);
@@ -19382,6 +19711,7 @@ var FileDesk = class {
19382
19711
  records.push(file.wire3);
19383
19712
  if (file.wire3.direction === "in") chat.local.set(file.wire3.id, file.id);
19384
19713
  if (!transferEnded(file.wire3)) this.restored.add(file.id);
19714
+ if (file.wire3.state === "done") continue;
19385
19715
  this.deps.transfers.set(file.id, this.view(chat, file.wire3, file.wire3.confirmed, transferStalled(file.wire3, Date.now(), this.restored.has(file.id), Date.now())));
19386
19716
  }
19387
19717
  chat.files.restore(records);
@@ -19822,7 +20152,7 @@ var FileDesk = class {
19822
20152
  view.room = this.deps.transfers.get(id)?.room;
19823
20153
  this.room(id);
19824
20154
  }
19825
- this.deps.transfers.set(id, view);
20155
+ if (record.state !== "done" || this.underway.has(id) || this.deps.transfers.has(id)) this.deps.transfers.set(id, view);
19826
20156
  if (!transferEnded(record)) {
19827
20157
  this.underway.add(id);
19828
20158
  this.ended.delete(id);
@@ -20350,6 +20680,10 @@ var Outbox = class {
20350
20680
  get sameLink() {
20351
20681
  return !this.stopped && this.closed === this.began;
20352
20682
  }
20683
+ /** One message as stored now: by its id when the store can, else picked out of the chat's history. */
20684
+ async one(id) {
20685
+ return this.store.get ? this.store.get(id) : (await this.store.read()).find((m) => m.id === id);
20686
+ }
20353
20687
  /** A new message a turn could not send (the link closed under it, or the ones before it still wait): it waits too. */
20354
20688
  async waitsItsTurn(message) {
20355
20689
  await this.store.update(message.id, "waiting", "Waits for the messages before it to go.", {
@@ -20454,7 +20788,7 @@ var Outbox = class {
20454
20788
  this.busy.add(id);
20455
20789
  let again;
20456
20790
  try {
20457
- let message = read ?? (await this.store.read()).find((m) => m.id === id);
20791
+ let message = read ?? await this.one(id);
20458
20792
  if (!message?.wireId || !message.delivery || message.delivery === "delivered") return;
20459
20793
  const wait = this.waiting.get(id);
20460
20794
  if (wait?.timer) {
@@ -20495,7 +20829,7 @@ var Outbox = class {
20495
20829
  again = this.reopenedWhileBusy.delete(id);
20496
20830
  }
20497
20831
  if (again) {
20498
- const message = (await this.store.read()).find((m) => m.id === id);
20832
+ const message = await this.one(id);
20499
20833
  if (message) await this.again(message);
20500
20834
  }
20501
20835
  }
@@ -20505,7 +20839,7 @@ var Outbox = class {
20505
20839
  * nothing about it is unconfirmed; it counts no attempt until it goes.
20506
20840
  */
20507
20841
  async wait(id, reason) {
20508
- const message = (await this.store.read()).find((m) => m.id === id);
20842
+ const message = await this.one(id);
20509
20843
  if (!message || this.stopped) return;
20510
20844
  await this.store.update(id, "waiting", reason, { resendUntil: message.resendUntil ?? this.now() + this.policy.windowMs });
20511
20845
  this.schedule(id);
@@ -20595,7 +20929,7 @@ var Outbox = class {
20595
20929
  */
20596
20930
  async attempt(id, passed) {
20597
20931
  if (this.stopped || this.busy.has(id)) return true;
20598
- const message = (await this.store.read()).find((m) => m.id === id);
20932
+ const message = await this.one(id);
20599
20933
  if (!message || message.delivery !== "queued" && message.delivery !== "waiting" || message.via === "hold") {
20600
20934
  this.forget(id);
20601
20935
  return true;
@@ -20622,7 +20956,7 @@ var Outbox = class {
20622
20956
  }
20623
20957
  /** After a failed attempt: queued again, unless its window or its attempts are spent. */
20624
20958
  async requeue(id, reason) {
20625
- const message = (await this.store.read()).find((m) => m.id === id);
20959
+ const message = await this.one(id);
20626
20960
  if (message && message.delivery !== "delivered") await this.queue(message, reason);
20627
20961
  }
20628
20962
  async queue(message, reason) {
@@ -20670,7 +21004,7 @@ var Outbox = class {
20670
21004
  this.turn(async () => {
20671
21005
  if (this.receipts.get(id) !== timer) return;
20672
21006
  this.receipts.delete(id);
20673
- const message = (await this.store.read()).find((m) => m.id === id);
21007
+ const message = await this.one(id);
20674
21008
  if (message?.via === "pkarr") await this.expired(message);
20675
21009
  else await this.requeue(id, "No receipt received yet.");
20676
21010
  }).catch(() => {});
@@ -21357,8 +21691,9 @@ var db = {
21357
21691
  tx.onerror = tx.onabort = () => reject(tx.error ?? /* @__PURE__ */ new Error("Link storage failed"));
21358
21692
  });
21359
21693
  },
21360
- async patchLink(linkId, patch) {
21361
- const tx = (await openDb()).transaction(STORES$1.links, "readwrite");
21694
+ /** `relaxed`: for what a power cut may cost (a round trip, a note made again later), no disk sync of its own. */
21695
+ async patchLink(linkId, patch, relaxed = false) {
21696
+ const tx = (await openDb()).transaction(STORES$1.links, "readwrite", relaxed ? RELAXED : void 0);
21362
21697
  const links = tx.objectStore(STORES$1.links);
21363
21698
  const request = links.get(linkId);
21364
21699
  request.onsuccess = () => {
@@ -21619,6 +21954,32 @@ var db = {
21619
21954
  });
21620
21955
  return result;
21621
21956
  },
21957
+ /**
21958
+ * `patchMessage` on these rows of one history, all in one transaction: one disk sync for all of them. Resolves to
21959
+ * the rows changed, in the order of `ids`.
21960
+ */
21961
+ async patchMessages(linkId, ids, change) {
21962
+ const tx = (await openDb()).transaction(STORES$1.messages, "readwrite");
21963
+ const messages = tx.objectStore(STORES$1.messages);
21964
+ const results = [];
21965
+ ids.forEach((id, i) => {
21966
+ const request = messages.get([linkId, id]);
21967
+ request.onsuccess = () => {
21968
+ const message = request.result;
21969
+ const patch = message && change(message);
21970
+ if (!patch) return;
21971
+ messages.put(results[i] = {
21972
+ ...message,
21973
+ ...patch
21974
+ });
21975
+ };
21976
+ });
21977
+ await new Promise((resolve, reject) => {
21978
+ tx.oncomplete = () => resolve();
21979
+ tx.onerror = tx.onabort = () => reject(tx.error ?? /* @__PURE__ */ new Error("Message storage failed"));
21980
+ });
21981
+ return results.filter((row) => !!row);
21982
+ },
21622
21983
  async deleteMessage(linkId, messageId) {
21623
21984
  await wrap((await store(STORES$1.messages, "readwrite")).delete([linkId, messageId]));
21624
21985
  },
@@ -22021,6 +22382,202 @@ async function myPin(host, chat, messageId, remove, current, now = Date.now()) {
22021
22382
  };
22022
22383
  }
22023
22384
  //#endregion
22385
+ //#region ../browser/src/engine/groupAuthorOrder.ts
22386
+ /**
22387
+ * How long a member's message may still be placed again below that member's older ones (WISP 400, requirement 10):
22388
+ * about a catch-up, whose answers end within the three minutes the app waits for the first.
22389
+ */
22390
+ var AUTHOR_ORDER_WINDOW_MS = 18e4;
22391
+ AUTHOR_ORDER_WINDOW_MS / 2;
22392
+ /** Whether `a` was written after `b` by the same member: a later epoch, or a higher number in the same one. */
22393
+ function after(a, b) {
22394
+ return a.epoch > b.epoch || a.epoch === b.epoch && a.seq > b.seq;
22395
+ }
22396
+ /**
22397
+ * A member's messages in a private group in the order that member wrote them (WISP 400, requirement 10, and WISP 902
22398
+ * § Catch-up). A message is placed where it arrives. When one comes of a member whose later messages were placed here
22399
+ * moments ago (two catch-up answers raced: the author's own newest 32 and another member's hand-on of the older ones),
22400
+ * those later ones are placed again below it, in the order they were written, and the lines that came after them before
22401
+ * the older one (another member's, mine) go along below them, in their order: a reply to the newest stays below it. A
22402
+ * row only ever moves down, so nobody else's lines change order. While messages of that member are still missing
22403
+ * (`missing`), the rows wait for them, so each row moves about once in a whole catch-up.
22404
+ */
22405
+ var GroupAuthorOrder = class {
22406
+ host;
22407
+ linkOf;
22408
+ missing;
22409
+ /** Per group: the members' rows followed, in the order they are placed in the history. */
22410
+ rows = /* @__PURE__ */ new Map();
22411
+ /** Per group: the members with a row placed above an older one of theirs, since when, and when their last row came. */
22412
+ due = /* @__PURE__ */ new Map();
22413
+ running = /* @__PURE__ */ new Set();
22414
+ constructor(host, linkOf, missing = () => false) {
22415
+ this.host = host;
22416
+ this.linkOf = linkOf;
22417
+ this.missing = missing;
22418
+ }
22419
+ /** A member's message was stored in the group's history, new: `at` is when, on the engine's clock. */
22420
+ placed(groupId, row) {
22421
+ if (!this.host.placeAgain) return;
22422
+ const rows = (this.rows.get(groupId) ?? []).filter((r) => r.at > row.at - AUTHOR_ORDER_WINDOW_MS).slice(-575);
22423
+ rows.push(row);
22424
+ this.rows.set(groupId, rows);
22425
+ let due = this.due.get(groupId);
22426
+ if (!due?.has(row.sender) && !rows.some((r) => r.sender === row.sender && after(r, row))) return;
22427
+ if (!due) this.due.set(groupId, due = /* @__PURE__ */ new Map());
22428
+ due.set(row.sender, {
22429
+ since: due.get(row.sender)?.since ?? row.at,
22430
+ last: row.at
22431
+ });
22432
+ if (!this.missing(groupId, row.sender)) this.run(groupId, row.at);
22433
+ }
22434
+ /** Time passed: rows nothing more is on its way for, or that waited long enough, are placed again. */
22435
+ tick(now) {
22436
+ for (const groupId of this.due.keys()) this.run(groupId, now);
22437
+ }
22438
+ /** The group's history is gone (left or forgotten). */
22439
+ forget(groupId) {
22440
+ this.rows.delete(groupId);
22441
+ this.due.delete(groupId);
22442
+ }
22443
+ /**
22444
+ * One group's rows put in order, a member at a time; what comes meanwhile is put in order by the next turn, so a
22445
+ * catch-up's frames that come together move each row once, not once per frame.
22446
+ */
22447
+ async run(groupId, now) {
22448
+ if (this.running.has(groupId)) return;
22449
+ this.running.add(groupId);
22450
+ try {
22451
+ for (;;) {
22452
+ const due = this.due.get(groupId);
22453
+ const ready = [...due ?? []].filter(([sender, { since, last }]) => now - last >= 15e3 || now - since >= 9e4 || !this.missing(groupId, sender)).map(([sender]) => sender);
22454
+ if (!ready.length) break;
22455
+ for (const sender of ready) due.delete(sender);
22456
+ if (!due.size) this.due.delete(groupId);
22457
+ for (const sender of ready) {
22458
+ const rows = this.rows.get(groupId) ?? [];
22459
+ const mine = rows.filter((r) => r.sender === sender);
22460
+ let from, below;
22461
+ for (const r of [...mine].reverse()) {
22462
+ if (below && after(r, below) && (!from || after(from, r))) from = r;
22463
+ if (!below || after(below, r)) below = r;
22464
+ }
22465
+ if (!from) continue;
22466
+ const newer = mine.filter((r) => !after(from, r)).sort((a, b) => after(a, b) ? 1 : after(b, a) ? -1 : 0);
22467
+ const ids = new Set(newer.map((r) => r.id));
22468
+ const first = rows.findIndex((r) => ids.has(r.id));
22469
+ let end = rows.length;
22470
+ for (let i = first + 1, top = rows[first]; i < rows.length; i++) {
22471
+ if (rows[i].sender !== sender) continue;
22472
+ if (after(top, rows[i])) {
22473
+ end = i;
22474
+ break;
22475
+ }
22476
+ top = rows[i];
22477
+ }
22478
+ const followers = rows.slice(first, end).filter((r) => r.sender !== sender);
22479
+ for (const r of followers) ids.add(r.id);
22480
+ const moved = [...newer, ...followers];
22481
+ this.rows.set(groupId, [...rows.filter((r) => !ids.has(r.id)), ...moved.map((r) => ({
22482
+ ...r,
22483
+ at: Math.max(r.at, now)
22484
+ }))]);
22485
+ await this.host.placeAgain(this.linkOf(groupId), moved.map((r) => r.id)).catch(() => {});
22486
+ }
22487
+ }
22488
+ } finally {
22489
+ this.running.delete(groupId);
22490
+ }
22491
+ }
22492
+ };
22493
+ //#endregion
22494
+ //#region ../browser/src/engine/groupLineOrder.ts
22495
+ /**
22496
+ * The lines of a group's history ("… joined", "… is no longer a member") among the messages of their moment, for a
22497
+ * member back from away (WISP 902 § Catch-up). A message sealed under an epoch was written before its author knew of
22498
+ * the next commit, so the line of that commit goes below it; a message under the commit's epoch or a later one was
22499
+ * written knowing it, and stays below the line. A catch-up brings the commits first (a commit opens the messages of
22500
+ * its epoch) and, in a private group, the answers of several members race, so the messages of the epoch before can come
22501
+ * after the line. Then the line, and the rows after it written knowing it, are placed again below them, in their order;
22502
+ * no clock is read. A row only ever moves down, and only rows placed here in the last few minutes, so a live
22503
+ * conversation read long ago never moves. Moves made meanwhile go in one turn: a row moves about once a catch-up page.
22504
+ */
22505
+ var GroupLineOrder = class {
22506
+ host;
22507
+ /** Per history: the rows followed, in the order they are placed in it. */
22508
+ rows = /* @__PURE__ */ new Map();
22509
+ due = /* @__PURE__ */ new Set();
22510
+ running = /* @__PURE__ */ new Set();
22511
+ constructor(host) {
22512
+ this.host = host;
22513
+ }
22514
+ /**
22515
+ * A row is being stored, a line of the history or a message sealed under this epoch, and takes its place now: `at` is
22516
+ * when, on the engine's clock. Call what this returns with whether it was new: a message below a line of a later
22517
+ * epoch takes the line (and the rows after it written knowing it) below it.
22518
+ */
22519
+ placing(linkId, row, at) {
22520
+ if (!this.host.placeAgain) return () => {};
22521
+ const followed = {
22522
+ ...row,
22523
+ at
22524
+ };
22525
+ const rows = (this.rows.get(linkId) ?? []).filter((r) => r.at > at - AUTHOR_ORDER_WINDOW_MS).slice(-575);
22526
+ rows.push(followed);
22527
+ this.rows.set(linkId, rows);
22528
+ return (stored) => {
22529
+ const now = this.rows.get(linkId);
22530
+ if (!stored) {
22531
+ if (now) this.rows.set(linkId, now.filter((r) => r !== followed));
22532
+ return;
22533
+ }
22534
+ if (!followed.line && now?.some((r) => r.line && r.epoch > followed.epoch && now.indexOf(r) < now.indexOf(followed))) this.run(linkId);
22535
+ };
22536
+ }
22537
+ /** These rows were placed again below everything (`GroupAuthorOrder`), in this order. */
22538
+ moved(linkId, ids) {
22539
+ const rows = this.rows.get(linkId);
22540
+ if (!rows) return;
22541
+ const moved = ids.map((id) => rows.find((r) => r.id === id)).filter((r) => !!r);
22542
+ if (!moved.length) return;
22543
+ this.rows.set(linkId, [...rows.filter((r) => !moved.includes(r)), ...moved]);
22544
+ if (moved.some((r) => !r.line)) this.run(linkId);
22545
+ }
22546
+ /** The group's history is gone (left or forgotten). */
22547
+ forget(linkId) {
22548
+ this.rows.delete(linkId);
22549
+ this.due.delete(linkId);
22550
+ }
22551
+ /** The history's rows put in order; what comes while a move is written goes in the next turn. */
22552
+ run(linkId) {
22553
+ this.due.add(linkId);
22554
+ if (this.running.has(linkId)) return;
22555
+ this.running.add(linkId);
22556
+ (async () => {
22557
+ try {
22558
+ while (this.due.delete(linkId)) {
22559
+ const rows = this.rows.get(linkId) ?? [];
22560
+ const order = [];
22561
+ for (const row of rows) {
22562
+ const above = row.line ? -1 : order.findIndex((r) => r.line && r.epoch > row.epoch);
22563
+ if (above < 0) order.push(row);
22564
+ else order.splice(above, 0, row);
22565
+ }
22566
+ let from = 0;
22567
+ while (from < rows.length && rows[from] === order[from]) from++;
22568
+ if (from === rows.length) continue;
22569
+ const last = rows[rows.length - 1].at;
22570
+ for (const r of order.slice(from)) r.at = Math.max(r.at, last);
22571
+ this.rows.set(linkId, order);
22572
+ await this.host.placeAgain(linkId, order.slice(from).map((r) => r.id)).catch(() => {});
22573
+ }
22574
+ } finally {
22575
+ this.running.delete(linkId);
22576
+ }
22577
+ })();
22578
+ }
22579
+ };
22580
+ //#endregion
22024
22581
  //#region ../browser/src/engine/hubClocks.ts
22025
22582
  /**
22026
22583
  * A hub dates its beacon entry by its own clock, and a reader judges it by its own. A hub's time closer than this to
@@ -22274,11 +22831,14 @@ var Communities = class {
22274
22831
  /** My frames an edge took, and which of them carry an edit (`frame id → editKey`). */
22275
22832
  frames = new FramesTaken();
22276
22833
  carriers = /* @__PURE__ */ new Map();
22834
+ /** The lines of each history among the messages of their moment (`GroupLineOrder`). */
22835
+ lineOrder;
22277
22836
  constructor(host, store, timings = COMMUNITY_TIMINGS, random = Math.random) {
22278
22837
  this.host = host;
22279
22838
  this.store = store;
22280
22839
  this.timings = timings;
22281
22840
  this.random = random;
22841
+ this.lineOrder = new GroupLineOrder(host);
22282
22842
  }
22283
22843
  /** Application frames and pair payloads handed to the engine, one after the other per group. */
22284
22844
  deliveries = /* @__PURE__ */ new Map();
@@ -22377,6 +22937,7 @@ var Communities = class {
22377
22937
  accepted: true,
22378
22938
  viaLink: true,
22379
22939
  stage: this.joinStage(group),
22940
+ since: group.joining.since,
22380
22941
  ...this.knockingWith.get(group.id) ? { waiting: this.knockingWith.get(group.id) } : {}
22381
22942
  } : void 0;
22382
22943
  if (invitation && !this.keptWhileKnocking(live)) return [{
@@ -22607,6 +23168,21 @@ var Communities = class {
22607
23168
  await s.leave();
22608
23169
  await this.forget(groupId);
22609
23170
  }
23171
+ /**
23172
+ * Delete from this device: left first, as Leave does. With nothing connected to carry the request, a member's leave
23173
+ * goes unsaid and the group goes anyway, as a private group's failed goodbye does: the others still list the device,
23174
+ * but members who never open the app again kept it here for good. An admin with members is still refused: gone with
23175
+ * the role, the group would have no admin.
23176
+ */
23177
+ async deleteHere(groupId) {
23178
+ try {
23179
+ await this.leave(groupId);
23180
+ } catch (error) {
23181
+ const s = this.live.get(groupId)?.session;
23182
+ if (!s || s.isAdmin && s.others.length) throw error;
23183
+ await this.forget(groupId);
23184
+ }
23185
+ }
22610
23186
  successor(groupId) {
22611
23187
  const s = this.live.get(groupId)?.session;
22612
23188
  if (!s) return void 0;
@@ -22677,6 +23253,7 @@ var Communities = class {
22677
23253
  this.lastMentionAt.delete(groupId);
22678
23254
  this.knockingWith.delete(groupId);
22679
23255
  this.knockRead.delete(groupId);
23256
+ this.lineOrder.forget(MESSAGE_LINK$1(groupId));
22680
23257
  for (const linkId of [...this.host.edges(groupId).values(), ...this.host.entries(groupId).values()]) await this.host.closeEdge(linkId);
22681
23258
  await this.store.deleteGroup(groupId);
22682
23259
  await this.files?.drop(groupId);
@@ -23763,6 +24340,11 @@ var Communities = class {
23763
24340
  sender: m.sender,
23764
24341
  file: m.file
23765
24342
  }, session.myKey) : void 0;
24343
+ const placing = this.lineOrder.placing(MESSAGE_LINK$1(id), {
24344
+ id: m.id,
24345
+ epoch: m.epoch,
24346
+ line: false
24347
+ }, this.now());
23766
24348
  const stored = await this.host.storeMessage({
23767
24349
  linkId: MESSAGE_LINK$1(id),
23768
24350
  id: m.id,
@@ -23777,6 +24359,7 @@ var Communities = class {
23777
24359
  ...m.card && { card: m.card },
23778
24360
  ...file && { file }
23779
24361
  });
24362
+ placing(stored !== false);
23780
24363
  const came = m.sender === session.myKey ? timestamp : cameAt(timestamp, stored, arrivalNow(this.now()));
23781
24364
  this.lastMessageAt.set(id, Math.max(this.lastMessageAt.get(id) ?? 0, came));
23782
24365
  if (mentioned) this.lastMentionAt.set(id, Math.max(this.lastMentionAt.get(id) ?? 0, came));
@@ -23873,7 +24456,16 @@ var Communities = class {
23873
24456
  else if (s.status === "active" && before !== after) {
23874
24457
  const at = receivedTimestamp(top.ts, when);
23875
24458
  for (const [key] of after) if (!rosterHas(before, key) && key !== s.myKey) await this.event(groupId, "joined", `${name(key)} joined`, at, top.e, key);
23876
- for (const [key] of before) if (!rosterHas(after, key)) await this.event(groupId, "gone", `${name(key)} is no longer a member`, at, top.e, key);
24459
+ const left = (key) => {
24460
+ const chain = s.state.chain;
24461
+ for (let i = chain.length - 1; i >= 0; i--) if ((chain[i].k === "remove" || chain[i].k === "leave") && chain[i].s === key) return chain[i].k === "leave";
24462
+ return false;
24463
+ };
24464
+ for (const [key] of before) {
24465
+ if (rosterHas(after, key)) continue;
24466
+ if (left(key)) await this.event(groupId, "left", `${name(key)} left`, at, top.e, key);
24467
+ else await this.event(groupId, "gone", `${name(key)} is no longer a member`, at, top.e, key);
24468
+ }
23877
24469
  if (top.k === "role" && before.find(([k]) => k === top.s)?.[1] !== "admin") await this.event(groupId, "admin", `${name(top.s)} ${top.s === s.myKey ? "are" : "is"} now the admin`, at, top.e, top.s);
23878
24470
  if (top.k === "rotate") await this.event(groupId, "rotated", "Keys rotated: a fresh epoch", at, top.e);
23879
24471
  }
@@ -23882,16 +24474,21 @@ var Communities = class {
23882
24474
  /** A line in the group's history. Its id carries its time: two lines at the same moment get distinct ones (`eventTime`). */
23883
24475
  async event(groupId, event, text, timestamp, epoch, member) {
23884
24476
  timestamp = eventTime(this.eventTimes, groupId, timestamp);
23885
- await this.host.storeMessage({
24477
+ const id = `event:${epoch}:${event}:${member ?? ""}:${timestamp}`;
24478
+ this.lineOrder.placing(MESSAGE_LINK$1(groupId), {
24479
+ id,
24480
+ epoch,
24481
+ line: true
24482
+ }, this.now())(await this.host.storeMessage({
23886
24483
  linkId: MESSAGE_LINK$1(groupId),
23887
- id: `event:${epoch}:${event}:${member ?? ""}:${timestamp}`,
24484
+ id,
23888
24485
  text,
23889
24486
  sender: "peer",
23890
24487
  event,
23891
24488
  member,
23892
24489
  timestamp,
23893
24490
  via: "datalink"
23894
- });
24491
+ }) !== false);
23895
24492
  }
23896
24493
  eventTimes = /* @__PURE__ */ new Map();
23897
24494
  };
@@ -24899,6 +25496,11 @@ var INVITED_KEPT_MS = LEFT_KEPT_MS;
24899
25496
  */
24900
25497
  var ACCEPT_AGAIN_MS = 6e4;
24901
25498
  var ACCEPT_AGAINS = 5;
25499
+ /**
25500
+ * Admin side: a member let in over its chat, its edge not up, is sent its welcome again at most this often. The welcome
25501
+ * is the whole chain and the sealed secrets (megabytes, tens of ms to build); its app says its accept once a minute.
25502
+ */
25503
+ var REWELCOME_MS = ACCEPT_AGAIN_MS / 2;
24902
25504
  /** Invitee side: the invitations one contact chat may have waiting here at a time. More are answered as declined. */
24903
25505
  var MAX_INVITATIONS_PER_CHAT = 4;
24904
25506
  /** What an invitation says of its group, kept as a claim within what a group can be: an epoch from 0, a roster of 1 to the most a group holds. */
@@ -24915,9 +25517,13 @@ var FAREWELL_ANSWERS = 3;
24915
25517
  var FAREWELL_COMMITS = 64;
24916
25518
  /**
24917
25519
  * A member whose messages were just asked of another (`Groups.askOf`) is told where I am only once that one's answer
24918
- * has come: no message from it for this long, or `MESH_SYNC_HOLD_MAX_MS` after it was asked. Told at once, its own
25520
+ * has come: no message of it for this long, or `MESH_SYNC_HOLD_MAX_MS` after it was asked. Told at once, its own
24919
25521
  * answer raced the other's, which holds the same messages among everyone's in the order they were written, and a
24920
25522
  * member back in a group of five read each member's lines apart, the newest above older ones (WISP 902 § Catch-up).
25523
+ * A message of the answer is one written before the asking: what the member writes meanwhile is no reason to wait, so
25524
+ * the longest wait only has to cover a whole answer (the 256 messages a member keeps for the others and its own last
25525
+ * 32) at the pace of a phone with a long history, 600 ms a frame. At ten seconds, an answer of 200 messages at four a
25526
+ * second was cut at the 40th, and the others' newest lines landed above what was still coming.
24921
25527
  */
24922
25528
  var MESH_SYNC_HOLD_MS = 1500;
24923
25529
  /** Where a member's gossip turns start among the members it is connected to: a number of its own, from its key. */
@@ -25031,6 +25637,8 @@ var Groups = class {
25031
25637
  syncHeld = /* @__PURE__ */ new Map();
25032
25638
  /** Per mesh group and member, when a message last came over its edge (its own, or one it handed on). */
25033
25639
  lastHanded = /* @__PURE__ */ new Map();
25640
+ /** Admin side, per group and member let in: when its welcome was last sent again, over its chat or the link (`REWELCOME_MS`). */
25641
+ rewelcomed = /* @__PURE__ */ new Map();
25034
25642
  lastGossip = /* @__PURE__ */ new Map();
25035
25643
  /**
25036
25644
  * Per mesh group, each reachable member some of whose messages never reached me (`GroupSession.lacking`): what was
@@ -25062,6 +25670,10 @@ var Groups = class {
25062
25670
  communities;
25063
25671
  /** My messages and edits an edge of a private group took. */
25064
25672
  frames = new FramesTaken();
25673
+ /** Each member's messages in the order that member wrote them, though catch-up answers race (WISP 400, requirement 10). */
25674
+ authorOrder;
25675
+ /** The lines of each history among the messages of their moment (`GroupLineOrder`). */
25676
+ lineOrder;
25065
25677
  /** Hubs of the private groups past 16 members (WISP 902 · Group Mesh § Hubs). */
25066
25678
  hubs;
25067
25679
  /** Groups whose edges are being reconciled because their hubs changed: one at a time. */
@@ -25088,6 +25700,16 @@ var Groups = class {
25088
25700
  }
25089
25701
  }, hubTimings);
25090
25702
  this.typings = new GroupTypings(host);
25703
+ this.lineOrder = new GroupLineOrder(host);
25704
+ const placeAgain = host.placeAgain && (async (linkId, ids) => {
25705
+ const done = host.placeAgain(linkId, ids);
25706
+ this.lineOrder.moved(linkId, ids);
25707
+ await done;
25708
+ });
25709
+ this.authorOrder = new GroupAuthorOrder({ placeAgain }, MESSAGE_LINK, (groupId, sender) => {
25710
+ const session = this.sessions.get(groupId);
25711
+ return !!session && session.missing(sender, this.gapsGone.get(groupId)?.get(sender)?.[session.epoch]) > 0;
25712
+ });
25091
25713
  this.signals = new MeshSignals(host, {
25092
25714
  session: (id) => this.sessions.get(id),
25093
25715
  stored: (id) => this.stored.get(id),
@@ -25125,13 +25747,14 @@ var Groups = class {
25125
25747
  await this.communities.load(all.filter((g) => g.community || g.joining));
25126
25748
  for (const group of all.filter((g) => !g.community && !g.joining)) {
25127
25749
  this.stored.set(group.id, group);
25128
- if (group.state) this.attach(group.state);
25129
- if (group.invited) this.invited.set(group.id, new Set(Object.keys(group.invited)));
25130
25750
  const history = await this.store.getMessages(MESSAGE_LINK(group.id)), said = history.filter((m) => !m.event && !m.groupPay);
25131
- this.framesKept.set(group.id, history.filter((m) => m.frame).map((m) => ({
25751
+ const framed = history.filter((m) => m.frame);
25752
+ this.framesKept.set(group.id, framed.map((m) => ({
25132
25753
  id: m.id,
25133
25754
  bytes: frameBytes(m.frame)
25134
25755
  })));
25756
+ if (group.state) this.attach(group.state, framed.map((m) => m.frame));
25757
+ if (group.invited) this.invited.set(group.id, new Set(Object.keys(group.invited)));
25135
25758
  const last = said[said.length - 1], lastPeer = [...said].reverse().find((m) => m.sender !== "me");
25136
25759
  if (last) this.lastMessageAt.set(group.id, last.timestamp);
25137
25760
  const peerAt = cameOrWritten(lastPeer?.timestamp, lastPeer ? group.came?.peer : void 0);
@@ -25208,6 +25831,7 @@ var Groups = class {
25208
25831
  members: Math.min(Math.max(invitation.n, 0), GROUP_MEMBER_CAP.max),
25209
25832
  accepted: !!invitation.seedB64,
25210
25833
  ...this.acceptSaid.get(group.id)?.unanswered ? { unanswered: true } : {},
25834
+ ...invitation.takenBack ? { takenBack: true } : {},
25211
25835
  ...invitation.entry ? {
25212
25836
  viaLink: true,
25213
25837
  stage: this.joinStage(group)
@@ -25389,6 +26013,7 @@ var Groups = class {
25389
26013
  async accept(groupId) {
25390
26014
  const group = this.stored.get(groupId);
25391
26015
  if (!group?.invitation) throw new Error("No invitation to accept");
26016
+ if (group.invitation.takenBack) throw new Error("This invitation was taken back");
25392
26017
  if (!this.host.linkReady(group.invitation.linkId)) throw new Error("The contact who invited you is not connected. Try again when they are.");
25393
26018
  const seedB64 = group.invitation.seedB64 ?? createIdentity().seedB64;
25394
26019
  group.invitation = {
@@ -25412,6 +26037,7 @@ var Groups = class {
25412
26037
  if (this.isCommunity(groupId)) return this.communities.decline(groupId);
25413
26038
  const group = this.stored.get(groupId);
25414
26039
  if (!group?.invitation) throw new Error("No invitation to decline");
26040
+ if (group.invitation.takenBack) return this.forget(groupId, false);
25415
26041
  if (!group.previous) return this.forget(groupId);
25416
26042
  await this.dropInvitation(group, this.sayDecline(groupId, group.invitation.linkId));
25417
26043
  }
@@ -25427,6 +26053,25 @@ var Groups = class {
25427
26053
  await this.store.putGroup(previous);
25428
26054
  this.host.emit();
25429
26055
  }
26056
+ /**
26057
+ * Invitee side: who sent the invitation took it back. A group I kept under its id comes back as it was; otherwise the
26058
+ * invitation stays a week, marked, so the list says it was taken back where it was (an accept said for it stops).
26059
+ */
26060
+ async takeBackFrom(group) {
26061
+ if (group.previous) return this.dropInvitation(group);
26062
+ if (group.invitation.takenBack) return;
26063
+ const { seedB64: _unused, ...invitation } = group.invitation;
26064
+ group.invitation = {
26065
+ ...invitation,
26066
+ pieces: [],
26067
+ takenBack: this.now()
26068
+ };
26069
+ this.acceptSaid.delete(group.id);
26070
+ await this.store.putGroup(group);
26071
+ const kept = [...this.stored.values()].filter((x) => x.invitation?.takenBack && !x.state && x.invitation.linkId === invitation.linkId).reverse().sort((a, b) => b.invitation.takenBack - a.invitation.takenBack);
26072
+ for (const old of kept.slice(MAX_INVITATIONS_PER_CHAT)) await this.forget(old.id, false);
26073
+ this.host.emit();
26074
+ }
25430
26075
  /** Invitee side: says no to an invitation. With the inviter's chat down (its app is closed, say), returns the answer owed, said when that chat is next up. */
25431
26076
  sayDecline(groupId, linkId) {
25432
26077
  try {
@@ -25619,6 +26264,8 @@ var Groups = class {
25619
26264
  this.lastMessageAt.delete(groupId);
25620
26265
  this.lastPeerMessageAt.delete(groupId);
25621
26266
  this.lastMentionAt.delete(groupId);
26267
+ this.authorOrder.forget(groupId);
26268
+ this.lineOrder.forget(MESSAGE_LINK(groupId));
25622
26269
  await this.store.deleteGroup(groupId);
25623
26270
  await this.store.putGroup(group);
25624
26271
  await this.files?.drop(groupId);
@@ -25812,19 +26459,20 @@ var Groups = class {
25812
26459
  }
25813
26460
  /** `answer`: false when an invitation goes because its inviter took it back, so no decline is said for it. */
25814
26461
  async forget(groupId, answer = true) {
25815
- if (this.isCommunity(groupId)) return this.communities.leave(groupId);
26462
+ if (this.isCommunity(groupId)) return this.communities.deleteHere(groupId);
25816
26463
  const session = this.sessions.get(groupId);
25817
26464
  if (session?.status === "active") try {
25818
26465
  await this.leave(groupId);
25819
26466
  } catch (e) {
25820
26467
  if (session.isAdmin && session.others.length) throw e;
25821
26468
  }
25822
- const gone = this.stored.get(groupId), invitation = answer && gone?.invitation && !gone.invitation.entry ? gone.invitation : void 0;
26469
+ const gone = this.stored.get(groupId), invitation = answer && gone?.invitation && !gone.invitation.entry && !gone.invitation.takenBack ? gone.invitation : void 0;
25823
26470
  const owed = invitation && this.sayDecline(groupId, invitation.linkId);
25824
26471
  this.sessions.delete(groupId);
25825
26472
  this.stored.delete(groupId);
25826
26473
  this.takeBack(groupId);
25827
26474
  this.acceptSaid.delete(groupId);
26475
+ this.entryHosts.delete(groupId);
25828
26476
  this.lastRoster.delete(groupId);
25829
26477
  this.lastTold.delete(groupId);
25830
26478
  this.pendingEntries.delete(groupId);
@@ -25838,6 +26486,7 @@ var Groups = class {
25838
26486
  this.relayAsked.delete(groupId);
25839
26487
  this.syncHeld.delete(groupId);
25840
26488
  this.lastHanded.delete(groupId);
26489
+ this.rewelcomed.delete(groupId);
25841
26490
  this.hubs.forget(groupId);
25842
26491
  this.removedAt.delete(groupId);
25843
26492
  this.lastGossip.delete(groupId);
@@ -25850,6 +26499,8 @@ var Groups = class {
25850
26499
  this.hereHeard.delete(groupId);
25851
26500
  this.hereActed.delete(groupId);
25852
26501
  this.signals.forget(groupId);
26502
+ this.authorOrder.forget(groupId);
26503
+ this.lineOrder.forget(MESSAGE_LINK(groupId));
25853
26504
  for (const linkId of [...this.host.edges(groupId).values(), ...this.host.entries(groupId).values()]) await this.host.closeEdge(linkId);
25854
26505
  await this.store.deleteGroup(groupId);
25855
26506
  if (owed && !this.stored.has(groupId)) {
@@ -25948,6 +26599,7 @@ var Groups = class {
25948
26599
  if (this.ticking) return;
25949
26600
  this.ticking = true;
25950
26601
  try {
26602
+ this.authorOrder.tick(now);
25951
26603
  for (const group of [...this.stored.values()]) {
25952
26604
  if (group.left) {
25953
26605
  if (now - group.left.at > LEFT_KEPT_MS) await this.forget(group.id);
@@ -25957,6 +26609,10 @@ var Groups = class {
25957
26609
  await this.declineOwed(group, now);
25958
26610
  if (!group.state) continue;
25959
26611
  }
26612
+ if (group.invitation?.takenBack && !group.state) {
26613
+ if (now - group.invitation.takenBack > LEFT_KEPT_MS) await this.forget(group.id, false);
26614
+ continue;
26615
+ }
25960
26616
  if (group.invitation?.entry && !group.state) {
25961
26617
  const every = now - group.createdAt > this.timings.patienceMs ? this.timings.slowKnockMs : this.timings.knockMs;
25962
26618
  if (!(this.host.linkReady(group.invitation.linkId) || !!this.host.linkSeen?.(group.invitation.linkId)) && (now - (this.lastKnock.get(group.id) ?? 0) >= every || now >= (this.knockCheckAt.get(group.id) ?? Infinity))) await this.knock(group, now).catch(() => {});
@@ -26075,7 +26731,7 @@ var Groups = class {
26075
26731
  }
26076
26732
  const session = this.sessions.get(groupId), group = this.stored.get(groupId);
26077
26733
  if (!session?.isAdmin || !group || !this.pendingEntries.get(groupId)?.has(peer)) return;
26078
- if (this.growthBlocked(group, session, session.roster.length + 1, linkId)) {
26734
+ if (!rosterHas(session.roster, peer) && this.growthBlocked(group, session, session.roster.length + 1, linkId)) {
26079
26735
  this.pendingEntries.get(groupId)?.delete(peer);
26080
26736
  this.refused.set(peer, Date.now() + REFUSED_FOR_MS);
26081
26737
  this.host.closeEdge(linkId);
@@ -26122,14 +26778,22 @@ var Groups = class {
26122
26778
  return this.knocked.has(group.id) ? "knocked" : "knocking";
26123
26779
  }
26124
26780
  entryOf(group) {
26125
- return group.entry ? {
26781
+ if (!group.entry) return void 0;
26782
+ const { seedB64 } = group.entry;
26783
+ let host = this.entryHosts.get(group.id);
26784
+ if (host?.seedB64 !== seedB64) this.entryHosts.set(group.id, host = {
26785
+ seedB64,
26786
+ key: identityFromSeedB64(seedB64).pubKeyZ32
26787
+ });
26788
+ return {
26126
26789
  link: {
26127
26790
  g: group.id,
26128
- host: identityFromSeedB64(group.entry.seedB64).pubKeyZ32
26791
+ host: host.key
26129
26792
  },
26130
- seedB64: group.entry.seedB64
26131
- } : void 0;
26793
+ seedB64
26794
+ };
26132
26795
  }
26796
+ entryHosts = /* @__PURE__ */ new Map();
26133
26797
  /** Joiner side: the entry session kept after the welcome goes (the edge to the admin is up, or it was kept long enough). */
26134
26798
  async endLinger(groupId) {
26135
26799
  const kept = this.lingering.get(groupId);
@@ -26194,11 +26858,13 @@ var Groups = class {
26194
26858
  });
26195
26859
  let pending = this.pendingEntries.get(group.id);
26196
26860
  for (const { key, ts } of knocks) {
26197
- if (now - ts > 18e4 || rosterHas(session.roster, key) || this.refused.has(key) || pending?.has(key) || key === entry.link.host) continue;
26861
+ const again = this.unwelcomed(group, session, key);
26862
+ if (now - ts > 18e4 || rosterHas(session.roster, key) && !again || this.refused.has(key) || pending?.has(key) || key === entry.link.host) continue;
26863
+ if (again && this.rewelcomedLately(group.id, key)) continue;
26198
26864
  const entryIds = new Set(this.host.entries(group.id).values());
26199
26865
  const contactsInvited = [...this.invited.get(group.id) ?? []].filter((id) => !entryIds.has(id)).length;
26200
26866
  const size = session.roster.length + contactsInvited + (pending?.size ?? 0);
26201
- if ((pending?.size ?? 0) >= MAX_PENDING_ENTRIES || size >= GROUP_MEMBER_CAP.max || this.growthBlocked(group, session, size + 1)) break;
26867
+ if ((pending?.size ?? 0) >= MAX_PENDING_ENTRIES || !again && (size >= GROUP_MEMBER_CAP.max || this.growthBlocked(group, session, size + 1))) break;
26202
26868
  if (!pending) this.pendingEntries.set(group.id, pending = /* @__PURE__ */ new Map());
26203
26869
  pending.set(key, now);
26204
26870
  this.warmUntil.set(group.id, now + this.timings.warmMs);
@@ -26258,9 +26924,9 @@ var Groups = class {
26258
26924
  }
26259
26925
  if (existing?.invitation?.seedB64 && (existing.invitation.linkId !== linkId || existing.invitation.admin !== frame.admin)) return;
26260
26926
  if (existing?.left && (existing.left.admin !== frame.admin || existing.contacts?.[existing.left.admin] !== linkId)) return;
26261
- if (existing?.invitation && !existing.state && existing.invitation.linkId !== linkId && !existing.invitation.genesis) return;
26262
- if ([...this.stored.values()].filter((x) => x.invitation).length >= 32) return;
26263
- if (!existing?.invitation && [...this.stored.values()].filter((x) => x.invitation?.linkId === linkId).length >= MAX_INVITATIONS_PER_CHAT) {
26927
+ if (existing?.invitation && !existing.state && existing.invitation.linkId !== linkId && !existing.invitation.genesis && !existing.invitation.takenBack) return;
26928
+ if ([...this.stored.values()].filter((x) => x.invitation && !x.invitation.takenBack).length >= 32) return;
26929
+ if (!existing?.invitation && [...this.stored.values()].filter((x) => x.invitation?.linkId === linkId && !x.invitation.takenBack).length >= MAX_INVITATIONS_PER_CHAT) {
26264
26930
  try {
26265
26931
  this.host.sendOnLink(linkId, {
26266
26932
  t: "group-decline",
@@ -26312,6 +26978,8 @@ var Groups = class {
26312
26978
  } catch {}
26313
26979
  const edge = this.host.edges(g).get(frame.key);
26314
26980
  if (letIn && session.isAdmin && !(edge && this.host.linkReady(edge))) {
26981
+ if (this.rewelcomedLately(g, frame.key)) return;
26982
+ this.noteRewelcome(g, frame.key);
26315
26983
  const welcome = await session.rewelcome(frame.key).catch(() => []);
26316
26984
  try {
26317
26985
  for (const piece of welcome) this.host.sendOnLink(linkId, piece);
@@ -26320,18 +26988,35 @@ var Groups = class {
26320
26988
  return;
26321
26989
  }
26322
26990
  const group = this.stored.get(g);
26323
- if (frame.key === session.myKey || rosterHas(session.roster, frame.key)) return;
26991
+ if (frame.key === session.myKey) return;
26324
26992
  const entryPeer = this.hostEntry(g, linkId);
26325
26993
  if ([...this.host.entries(g).values()].includes(linkId) && entryPeer !== frame.key) return;
26326
- if (session.roster.length >= GROUP_MEMBER_CAP.max || this.growthBlocked(group, session, session.roster.length + 1, linkId)) return;
26994
+ const again = !!entryPeer && this.unwelcomed(group, session, frame.key);
26995
+ if (rosterHas(session.roster, frame.key) && !again) return;
26996
+ if (again && this.rewelcomedLately(g, frame.key)) {
26997
+ this.pendingEntries.get(g)?.delete(frame.key);
26998
+ await this.host.closeEdge(linkId);
26999
+ return;
27000
+ }
27001
+ if (again) this.noteRewelcome(g, frame.key);
27002
+ if (!again && (session.roster.length >= GROUP_MEMBER_CAP.max || this.growthBlocked(group, session, session.roster.length + 1, linkId))) return;
26327
27003
  if (!entryPeer) await session.setNick(frame.key, this.host.contactName(linkId));
26328
- this.meet(g, frame.key);
27004
+ if (!again) this.meet(g, frame.key);
26329
27005
  if (this.host.linkReady(linkId, 3)) this.markLarge(group, frame.key);
26330
- const welcome = await session.admit(frame.key);
27006
+ let welcome;
27007
+ try {
27008
+ welcome = again ? await session.rewelcome(frame.key) : await session.admit(frame.key);
27009
+ } catch (error) {
27010
+ if (!again) throw error;
27011
+ this.pendingEntries.get(g)?.delete(frame.key);
27012
+ await this.host.closeEdge(linkId);
27013
+ return;
27014
+ }
26331
27015
  if (!entryPeer) group.contacts = {
26332
27016
  ...group.contacts,
26333
27017
  [frame.key]: linkId
26334
27018
  };
27019
+ if (!again) group.unseen = [...(group.unseen ?? []).filter((key) => rosterHas(session.roster, key)), frame.key];
26335
27020
  this.answered(group, linkId);
26336
27021
  await this.store.putGroup(group);
26337
27022
  for (const piece of welcome) this.host.sendOnLink(linkId, piece);
@@ -26354,7 +27039,7 @@ var Groups = class {
26354
27039
  case "group-decline": {
26355
27040
  const mine = this.stored.get(g);
26356
27041
  if (mine?.invitation && !mine.invitation.entry && !mine.state && mine.invitation.linkId === linkId) {
26357
- await this.dropInvitation(mine);
27042
+ await this.takeBackFrom(mine);
26358
27043
  return;
26359
27044
  }
26360
27045
  const entryPeer = this.hostEntry(g, linkId);
@@ -26363,6 +27048,7 @@ var Groups = class {
26363
27048
  await this.host.closeEdge(linkId);
26364
27049
  }
26365
27050
  const group = this.stored.get(g);
27051
+ if (group && await this.declinedUnseen(g, group, linkId)) return;
26366
27052
  if (!group || !this.answered(group, linkId)) return;
26367
27053
  await this.store.putGroup(group);
26368
27054
  this.host.emit();
@@ -26526,7 +27212,8 @@ var Groups = class {
26526
27212
  return;
26527
27213
  }
26528
27214
  const taken = await session.handle(peerKey, frame);
26529
- if (taken.some((f) => f.t === "group-msg")) {
27215
+ const askedAt = this.askedAt(groupId, peerKey);
27216
+ if (taken.some((f) => f.t === "group-msg" && f.ts < askedAt)) {
26530
27217
  let handed = this.lastHanded.get(groupId);
26531
27218
  if (!handed) this.lastHanded.set(groupId, handed = /* @__PURE__ */ new Map());
26532
27219
  handed.set(peerKey, this.now());
@@ -26599,9 +27286,14 @@ var Groups = class {
26599
27286
  return;
26600
27287
  }
26601
27288
  if (!session || !group || session.status !== "active" || !rosterHas(session.roster, peerKey)) return;
27289
+ const seen = !!group.unseen?.includes(peerKey);
27290
+ if (seen) {
27291
+ const unseen = group.unseen.filter((key) => key !== peerKey);
27292
+ group.unseen = unseen.length ? unseen : void 0;
27293
+ }
26602
27294
  const large = this.host.linkReady(linkId, 3) && this.markLarge(group, peerKey);
26603
27295
  const legacy = this.hubs.enabled && this.hubs.edgeReady(groupId, session, group, peerKey, this.host.linkReady(linkId, 4), this.now());
26604
- if (large || legacy) this.store.putGroup(group).catch(() => {});
27296
+ if (large || legacy || seen) this.store.putGroup(group).catch(() => {});
26605
27297
  if (!this.holdSync(groupId, peerKey)) try {
26606
27298
  this.host.sendOnLink(linkId, session.syncFrame(this.askOf(groupId, session, peerKey)));
26607
27299
  } catch {
@@ -26624,6 +27316,41 @@ var Groups = class {
26624
27316
  this.host.edgeUp?.(groupId, peerKey);
26625
27317
  this.host.emit();
26626
27318
  }
27319
+ /**
27320
+ * Admin side: a `group-decline` over the contact chat that let in a member never seen on an edge since. Its welcome
27321
+ * was lost and its app declined the invitation it still showed: it never had the group's keys, so it goes from the
27322
+ * roster (and may be invited again). True when one went.
27323
+ */
27324
+ async declinedUnseen(groupId, group, linkId) {
27325
+ const session = this.sessions.get(groupId);
27326
+ if (!session?.isAdmin || session.status !== "active" || !group.unseen) return false;
27327
+ const key = group.unseen.find((k) => group.contacts?.[k] === linkId && rosterHas(session.roster, k));
27328
+ if (!key) return false;
27329
+ const edge = this.host.edges(groupId).get(key);
27330
+ if (edge && this.host.linkReady(edge)) return false;
27331
+ const unseen = group.unseen.filter((k) => k !== key);
27332
+ group.unseen = unseen.length ? unseen : void 0;
27333
+ await this.remove(groupId, key).catch(() => {});
27334
+ await this.store.putGroup(group);
27335
+ this.host.emit();
27336
+ return true;
27337
+ }
27338
+ /** Admin side: a member in the roster never seen on an edge since it was let in, whose edge is not up now. */
27339
+ /** This member was sent its welcome again less than `REWELCOME_MS` ago, over its chat or through the link. */
27340
+ rewelcomedLately(groupId, key) {
27341
+ const last = this.rewelcomed.get(groupId)?.get(key);
27342
+ return last !== void 0 && this.now() - last < REWELCOME_MS;
27343
+ }
27344
+ noteRewelcome(groupId, key) {
27345
+ let sent = this.rewelcomed.get(groupId);
27346
+ if (!sent) this.rewelcomed.set(groupId, sent = /* @__PURE__ */ new Map());
27347
+ sent.set(key, this.now());
27348
+ }
27349
+ unwelcomed(group, session, key) {
27350
+ if (!session.isAdmin || !group.unseen?.includes(key) || !rosterHas(session.roster, key)) return false;
27351
+ const edge = this.host.edges(group.id).get(key);
27352
+ return !(edge && this.host.linkReady(edge));
27353
+ }
26627
27354
  /** Records that a member's app takes rosters past eight; true when that is news (the caller saves). */
26628
27355
  markLarge(group, key) {
26629
27356
  if (group.large?.includes(key)) return false;
@@ -26731,13 +27458,19 @@ var Groups = class {
26731
27458
  if (out.length) this.lastGossip.set(groupId, now);
26732
27459
  return out;
26733
27460
  }
27461
+ /** When `via` was last asked for another member's messages (`askOf`), or 0: what it sends written before that is its answer. */
27462
+ askedAt(groupId, via) {
27463
+ let at = 0;
27464
+ for (const asked of this.relayAsked.get(groupId)?.values() ?? []) if (asked.via === via && asked.at > at) at = asked.at;
27465
+ return at;
27466
+ }
26734
27467
  /**
26735
27468
  * Whether the answer of the member asked for someone's messages may still be coming: its edge up, asked under
26736
- * `MESH_SYNC_HOLD_MAX_MS` ago, and a message from it (or the asking) under `MESH_SYNC_HOLD_MS` ago.
27469
+ * `MESH_SYNC_HOLD_MAX_MS` ago, and a message of its answer (or the asking) under `MESH_SYNC_HOLD_MS` ago.
26737
27470
  */
26738
27471
  answering(groupId, { via, at }, now) {
26739
27472
  const edge = this.host.edges(groupId).get(via);
26740
- if (!edge || !this.host.linkReady(edge) || now - at >= 1e4) return false;
27473
+ if (!edge || !this.host.linkReady(edge) || now - at >= 18e4) return false;
26741
27474
  return now - Math.max(at, this.lastHanded.get(groupId)?.get(via) ?? 0) < MESH_SYNC_HOLD_MS;
26742
27475
  }
26743
27476
  /**
@@ -26757,21 +27490,41 @@ var Groups = class {
26757
27490
  held.set(peerKey, { ...asked });
26758
27491
  return true;
26759
27492
  }
26760
- /** The syncs that waited for another member's answer (`holdSync`), once it has come: for what it did not hand on. */
27493
+ /**
27494
+ * The syncs that waited for another member's answer (`holdSync`), once it has come: for what it did not hand on.
27495
+ * One member at a time: the first is told and asked for the others too, which wait for its answer in turn. Told
27496
+ * together, each answered with its own messages alone, and when the member asked first had been away too (it held
27497
+ * little of what was missed) a conversation came as one member's lines, then the next's.
27498
+ */
26761
27499
  heldSyncs(groupId, session, now) {
26762
27500
  const held = this.syncHeld.get(groupId);
26763
27501
  if (!held?.size) return;
26764
- const edges = this.host.edges(groupId);
26765
- for (const [key, asked] of held) {
26766
- if (this.answering(groupId, asked, now)) continue;
27502
+ const edges = this.host.edges(groupId), asked = this.relayAsked.get(groupId);
27503
+ const due = [];
27504
+ for (const [key, of] of held) {
27505
+ if (this.answering(groupId, of, now)) continue;
26767
27506
  held.delete(key);
26768
- if (this.relayAsked.get(groupId)?.get(key)?.via === asked.via) this.relayAsked.get(groupId).delete(key);
27507
+ if (asked?.get(key)?.via === of.via) asked.delete(key);
26769
27508
  const edge = edges.get(key);
26770
- if (!edge || !this.host.linkReady(edge) || !rosterHas(session.roster, key)) continue;
26771
- try {
26772
- this.host.sendOnLink(edge, session.syncFrame(this.askOf(groupId, session, key)));
26773
- } catch {}
27509
+ if (edge && this.host.linkReady(edge) && rosterHas(session.roster, key)) due.push(key);
27510
+ }
27511
+ const [next, ...rest] = due;
27512
+ if (!next) return;
27513
+ const ask = new Set(this.askOf(groupId, session, next));
27514
+ for (const key of rest) {
27515
+ ask.add(key);
27516
+ asked?.set(key, {
27517
+ via: next,
27518
+ at: now
27519
+ });
27520
+ held.set(key, {
27521
+ via: next,
27522
+ at: now
27523
+ });
26774
27524
  }
27525
+ try {
27526
+ this.host.sendOnLink(edges.get(next), session.syncFrame([...ask]));
27527
+ } catch {}
26775
27528
  }
26776
27529
  /**
26777
27530
  * Reachable members whose messages below the highest seen never reached me, for `via` to hand on: their own answer
@@ -27024,7 +27777,8 @@ var Groups = class {
27024
27777
  if (!session) throw new Error("Group not found");
27025
27778
  return session;
27026
27779
  }
27027
- attach(state) {
27780
+ /** `frames`: what the group's rows hold to hand on, oldest first (none yet for a group just made, joined or given back). */
27781
+ attach(state, frames = []) {
27028
27782
  const session = new GroupSession(state, {
27029
27783
  clock: () => this.now(),
27030
27784
  seqFloor: () => this.host.seqFloor?.() ?? 0,
@@ -27078,8 +27832,21 @@ var Groups = class {
27078
27832
  ...m.frame && { frame: m.frame }
27079
27833
  };
27080
27834
  const completed = !!m.completes && !!this.host.completeMessage;
27835
+ const placing = completed ? void 0 : this.lineOrder.placing(MESSAGE_LINK(state.id), {
27836
+ id: m.id,
27837
+ epoch: m.epoch,
27838
+ line: false
27839
+ }, this.now());
27081
27840
  const stored = completed ? (await this.host.completeMessage(message), false) : await this.host.storeMessage(message);
27841
+ placing?.(stored !== false);
27082
27842
  if (m.frame && (completed || stored !== false)) await this.frameKept(state.id, m.id, m.frame);
27843
+ if (!completed && stored !== false) this.authorOrder.placed(state.id, {
27844
+ sender: m.sender,
27845
+ epoch: m.epoch,
27846
+ seq: m.seq,
27847
+ id: m.id,
27848
+ at: this.now()
27849
+ });
27083
27850
  const came = m.sender === session.myKey ? timestamp : cameAt(timestamp, stored, arrivalNow(this.now()));
27084
27851
  this.lastMessageAt.set(state.id, Math.max(this.lastMessageAt.get(state.id) ?? 0, came));
27085
27852
  if (mentioned) this.lastMentionAt.set(state.id, Math.max(this.lastMentionAt.get(state.id) ?? 0, came));
@@ -27087,6 +27854,10 @@ var Groups = class {
27087
27854
  noteCame(this.stored.get(state.id), came, m.sender !== session.myKey, mentioned);
27088
27855
  this.typings.messageFrom(state.id, m.sender);
27089
27856
  },
27857
+ kept: async (id, frame) => {
27858
+ await this.host.keepFrame?.(MESSAGE_LINK(state.id), id, frame).catch(() => {});
27859
+ await this.frameKept(state.id, id, frame);
27860
+ },
27090
27861
  edit: (e) => this.host.groupEdit?.(state.id, e),
27091
27862
  changed: () => {
27092
27863
  if (session.status === "removed" && this.hubs.isHub(state.id) && !this.removedAt.has(state.id)) this.removedAt.set(state.id, this.now());
@@ -27099,7 +27870,7 @@ var Groups = class {
27099
27870
  const edge = this.host.edges(state.id).get(to);
27100
27871
  return edge && this.host.linkHandled ? this.host.linkHandled(edge) : Promise.resolve(false);
27101
27872
  } }
27102
- });
27873
+ }, frames);
27103
27874
  this.sessions.set(state.id, session);
27104
27875
  this.lastRoster.set(state.id, session.roster);
27105
27876
  this.lastTold.set(state.id, {
@@ -27128,7 +27899,13 @@ var Groups = class {
27128
27899
  for (const [key] of after) if (!rosterHas(before, key) && key !== session.myKey) await this.event(groupId, "joined", `${name(key)} joined`, when, top.e, key);
27129
27900
  const gone = before.filter(([key]) => !rosterHas(after, key)).map(([key]) => key);
27130
27901
  this.hubs.removed(groupId, gone, this.now());
27131
- for (const key of gone) await this.event(groupId, "gone", `${name(key)} is no longer a member`, when, top.e, key);
27902
+ const left = (key) => {
27903
+ const chain = session.state.chain;
27904
+ for (let e = chain.length - 1; e > 0; e--) if (chain[e].k === "remove" && chain[e].s === key) return !!session.state.leftMarks?.[e];
27905
+ return false;
27906
+ };
27907
+ for (const key of gone) if (left(key)) await this.event(groupId, "left", `${name(key)} left`, when, top.e, key);
27908
+ else await this.event(groupId, "gone", `${name(key)} is no longer a member`, when, top.e, key);
27132
27909
  if (top.k === "role") await this.event(groupId, "admin", `${name(top.s)} ${top.s === session.myKey ? "are" : "is"} now the admin`, when, top.e, top.s);
27133
27910
  if (top.k === "rotate") await this.event(groupId, "rotated", "Keys rotated: a fresh epoch", when, top.e);
27134
27911
  }
@@ -27183,16 +27960,21 @@ var Groups = class {
27183
27960
  /** A line in the group's history; lines at the same time (two members gone in one change) get distinct ones, see `eventTime`. */
27184
27961
  async event(groupId, event, text, timestamp, epoch, member) {
27185
27962
  timestamp = eventTime(this.eventTimes, groupId, timestamp);
27186
- await this.host.storeMessage({
27963
+ const id = `event:${epoch}:${event}:${timestamp}`;
27964
+ this.lineOrder.placing(MESSAGE_LINK(groupId), {
27965
+ id,
27966
+ epoch,
27967
+ line: true
27968
+ }, this.now())(await this.host.storeMessage({
27187
27969
  linkId: MESSAGE_LINK(groupId),
27188
- id: `event:${epoch}:${event}:${timestamp}`,
27970
+ id,
27189
27971
  text,
27190
27972
  sender: "peer",
27191
27973
  event,
27192
27974
  member,
27193
27975
  timestamp,
27194
27976
  via: "datalink"
27195
- });
27977
+ }) !== false);
27196
27978
  }
27197
27979
  eventTimes = /* @__PURE__ */ new Map();
27198
27980
  };
@@ -29146,8 +29928,6 @@ function resumeHere(log, runs, peer) {
29146
29928
  const shared = runs.find((transport) => peer?.includes(transport));
29147
29929
  return shared ? { resume: shared } : {};
29148
29930
  }
29149
- /** A round trip that moved less than this is not news in the history. */
29150
- var RTT_STEP_MS = 10;
29151
29931
  var sequence = 0;
29152
29932
  var lineId = (at) => `${at.toString(36)}-${(++sequence).toString(36)}`;
29153
29933
  /** A row that says the chat is live on its transport from then on. */
@@ -29551,7 +30331,7 @@ var TransportLog = class {
29551
30331
  changed = true;
29552
30332
  }
29553
30333
  const stretch = [...this.history].reverse().find((e) => e.kind === "live" || e.kind === "switched" || e.kind === "down");
29554
- if (stretch && stretch.kind !== "down" && stretch.transport === on && (stretch.rttMs === void 0 || Math.abs(stretch.rttMs - ms) >= RTT_STEP_MS)) {
30334
+ if (stretch && stretch.kind !== "down" && stretch.transport === on && (stretch.rttMs === void 0 || Math.abs(stretch.rttMs - ms) >= 10)) {
29555
30335
  stretch.rttMs = ms;
29556
30336
  changed = true;
29557
30337
  }
@@ -34483,24 +35263,6 @@ function messageAttention(type, message, { startedAt, newest, now }) {
34483
35263
  };
34484
35264
  }
34485
35265
  //#endregion
34486
- //#region ../browser/src/shared/hyperdhtRelay.ts
34487
- /** Only `wss://`, or `ws://` to this machine (a relay for tests or development). */
34488
- function hyperdhtRelayProblem(url) {
34489
- let parsed;
34490
- try {
34491
- parsed = new URL(url);
34492
- } catch {
34493
- return "Enter a relay address (wss://…)";
34494
- }
34495
- if (parsed.protocol === "wss:") return null;
34496
- if (parsed.protocol === "ws:" && [
34497
- "127.0.0.1",
34498
- "localhost",
34499
- "[::1]"
34500
- ].includes(parsed.hostname)) return null;
34501
- return "Use a wss:// relay address";
34502
- }
34503
- //#endregion
34504
35266
  //#region ../browser/src/shared/pushRelay.ts
34505
35267
  /**
34506
35268
  * The push relay a browser hands wake-ups to when it may not post to a push service itself (WISP 401 § Wake-up
@@ -34651,10 +35413,10 @@ var NATIVE_SLOTS = 8;
34651
35413
  var IROH_WEB_SLOTS = 16;
34652
35414
  /** Of a transport's listeners, the ones a group's links never hold: 1:1 chats keep that room. */
34653
35415
  var CHAT_NATIVE_SLOTS = 4;
34654
- /** How often a group link that found no free native slot tries again. */
34655
- var GROUP_NATIVE_RETRY_MS = 15e3;
34656
- /** The longest a group link whose native listener failed to start waits before trying again (its wait doubles up to it). */
34657
- var GROUP_NATIVE_FAILED_MAX_MS = 3e5;
35416
+ /** How often a group link that found no free native slot tries again, and the first wait of a listener that failed to start. */
35417
+ var NATIVE_RETRY_MS = 15e3;
35418
+ /** The longest a chat or a group link whose native listener failed to start waits before trying again (its wait doubles up to it). */
35419
+ var NATIVE_FAILED_MAX_MS = 3e5;
34658
35420
  /** "Clear all data": how long taking things back from the network may hold up the clear, in all. */
34659
35421
  var CLEAR_WITHDRAW_MS = 6e3;
34660
35422
  /**
@@ -34683,11 +35445,19 @@ var DEFAULT_SETTINGS = {
34683
35445
  */
34684
35446
  var CARD_RESTORE_MOST = 50;
34685
35447
  var CARD_RESTORE_MS = 6048e5;
35448
+ /**
35449
+ * A round trip that moved is shown at once, and written to disk by itself (relaxed, no disk sync of its own) at most
35450
+ * this often: a 15 s sample on an internet path moves by 10 ms most of the time. A power cut loses at most this much
35451
+ * of the round trip's drift, never a row; any other change to the story writes the latest round trip with it.
35452
+ */
35453
+ var RTT_SAVE_MS = 3e5;
34686
35454
  /** A reaction said on the live session and not confirmed is said again after this long. */
34687
35455
  var REACTION_RESEND_MS = 3e4;
34688
35456
  /** A file or a payment request of mine that waits for the chat to be live (a text that waits is its outbox's). */
34689
35457
  var waitsForLive = (m) => m.sender === "me" && m.delivery === "waiting" && (!!m.file || !!m.paymentId);
34690
35458
  /** This side's participation key in a chat: derived from its seed once, not at every state built. */
35459
+ /** A read of the contact's packet that found it as the last one did: online the same, the same packet, name and services. */
35460
+ var samePresence = (a, b) => a.online === b.online && a.lastPacketAt === b.lastPacketAt && a.seenAt === b.seenAt && a.nick === b.nick && JSON.stringify(a.services) === JSON.stringify(b.services);
34691
35461
  function participationKeyOf(live) {
34692
35462
  const seed = live.stored.participationSeed;
34693
35463
  if (!seed) return void 0;
@@ -34865,7 +35635,7 @@ var GhostlyNode = class GhostlyNode {
34865
35635
  * Everything that publishes, dials, polls or settles asks this, not the stored setting.
34866
35636
  */
34867
35637
  get networkOn() {
34868
- return !!this.settings.online && !this.limitedMode;
35638
+ return !!this.settings.online && !this.limitedMode && !this.options.offline;
34869
35639
  }
34870
35640
  transport;
34871
35641
  /** The same, for groups' requests: a relay budget keeps a chat's share from them (`CHAT_RESERVE`). */
@@ -34910,6 +35680,8 @@ var GhostlyNode = class GhostlyNode {
34910
35680
  });
34911
35681
  /** `start` has put the kept transfers back (EngineState.transfersRestored). */
34912
35682
  transfersRestored = false;
35683
+ /** `start` has loaded the groups (EngineState.groupsLoaded). */
35684
+ groupsLoaded = false;
34913
35685
  feedbackStartedAt = Date.now();
34914
35686
  feedbackIds = /* @__PURE__ */ new Set();
34915
35687
  walletFeedbackReady = false;
@@ -34993,9 +35765,15 @@ var GhostlyNode = class GhostlyNode {
34993
35765
  groupWakeReceived = /* @__PURE__ */ new Map();
34994
35766
  requestCounts = /* @__PURE__ */ new Map();
34995
35767
  transfers = /* @__PURE__ */ new Map();
35768
+ /**
35769
+ * Views that go in the next state sent, and then away (`GroupFilesHost.brief`): dropped once a state carried them,
35770
+ * unless another view took their place meanwhile.
35771
+ */
35772
+ briefViews = /* @__PURE__ */ new Map();
34996
35773
  /** files/3 in every chat: offers, resumable transfers, checked by digest (WISP 501 rev 0.3). */
34997
35774
  fileDesk = new FileDesk({
34998
35775
  send: (linkId, frame) => this.links.get(linkId)?.link?.sendFilesFrame(frame) ?? false,
35776
+ sendData: (linkId, id, offset, data) => this.links.get(linkId)?.link?.sendFileData(id, offset, data) ?? false,
34999
35777
  writable: (linkId) => this.links.get(linkId)?.link?.filesWritable(),
35000
35778
  receivedBytes: (linkId) => this.links.get(linkId)?.files.receivedBytes ?? 0,
35001
35779
  wireIds: (linkId) => this.links.get(linkId)?.files.wireIds,
@@ -35771,6 +36549,10 @@ var GhostlyNode = class GhostlyNode {
35771
36549
  forgetFrame: async (linkId, id) => {
35772
36550
  await db.patchMessage(linkId, id, (stored) => stored.frame ? { frame: void 0 } : null, true);
35773
36551
  },
36552
+ keepFrame: async (linkId, id, frame) => {
36553
+ await db.patchMessage(linkId, id, () => ({ frame }), true);
36554
+ },
36555
+ placeAgain: (linkId, ids) => this.placeAgain(linkId, ids),
35774
36556
  membersChanged: (groupId) => this.groupMembersChanged(groupId),
35775
36557
  historyGone: (groupId) => this.groupHistoryGone(groupId),
35776
36558
  emit: () => this.emitState(),
@@ -35791,11 +36573,16 @@ var GhostlyNode = class GhostlyNode {
35791
36573
  groupFiles: {
35792
36574
  store: storedGroupFiles,
35793
36575
  transfers: this.transfers,
36576
+ brief: (id) => {
36577
+ const view = this.transfers.get(id);
36578
+ if (view) this.briefViews.set(id, view);
36579
+ },
35794
36580
  settings: () => ({
35795
36581
  autoDownloads: this.settings.autoDownloads !== false,
35796
36582
  serveFiles: this.settings.serveFiles !== false
35797
36583
  }),
35798
36584
  sendFiles: (linkId, frame) => this.links.get(linkId)?.link?.sendFilesFrame(frame) ?? false,
36585
+ sendData: (linkId, id, offset, data) => this.links.get(linkId)?.link?.sendFileData(id, offset, data) ?? false,
35799
36586
  writable: (linkId) => this.links.get(linkId)?.link?.filesWritable()
35800
36587
  }
35801
36588
  });
@@ -36188,7 +36975,7 @@ var GhostlyNode = class GhostlyNode {
36188
36975
  const messages = stored.group ? [] : await db.getMessages(stored.id);
36189
36976
  const storedFiles = stored.group ? [] : await fileStore.listForLink(stored.id);
36190
36977
  const waiting = new Set(messages.flatMap((m) => m.sender === "me" && m.delivery === "waiting" && m.file ? [m.file.id] : []));
36191
- for (const file of storedFiles) if (file.transfer && !file.wire3 && !waiting.has(file.id)) this.transfers.set(file.id, file.transfer.state === "transferring" ? {
36978
+ for (const file of storedFiles) if (file.transfer && file.transfer.state !== "done" && !file.wire3 && !waiting.has(file.id)) this.transfers.set(file.id, file.transfer.state === "transferring" ? {
36192
36979
  ...file.transfer,
36193
36980
  state: "failed",
36194
36981
  error: "Transfer interrupted. Retry when connected."
@@ -36206,6 +36993,7 @@ var GhostlyNode = class GhostlyNode {
36206
36993
  this.transfersRestored = true;
36207
36994
  for (const group of await db.getGroups()) await this.settleHistory(`group:${group.id}`);
36208
36995
  await this.groups.load(this.limitedMode);
36996
+ this.groupsLoaded = true;
36209
36997
  for (const group of this.groups.views()) {
36210
36998
  const note = latestReaction(await db.getMessages(`group:${group.id}`));
36211
36999
  if (note) this.reactionNotes.set(`group:${group.id}`, note);
@@ -36224,7 +37012,7 @@ var GhostlyNode = class GhostlyNode {
36224
37012
  this.prepareSpare(STARTUP_QUIET_MS);
36225
37013
  }
36226
37014
  if (!this.limitedMode) this.openStartedWallets(fresh);
36227
- if (this.appsOn) this.appStore.start().catch(() => {});
37015
+ if (this.appsOn && !this.appsWireOnly) this.appStore.start().catch(() => {});
36228
37016
  if (!this.options.singleDevice && this.networkOn && knownDeviceGate()?.state === "active") this.startDeviceSet().catch(() => {});
36229
37017
  if (!this.options.singleDevice && this.limitedMode && knownDeviceGate()?.state === "active") this.readTurnWhileLimited();
36230
37018
  if (this.restoreCheck === "checking" && this.limitedMode) this.checkRestoredTurn(0);
@@ -36689,7 +37477,7 @@ var GhostlyNode = class GhostlyNode {
36689
37477
  appShelf;
36690
37478
  /** The installed apps and stores, refused while the apps feature is off. */
36691
37479
  get appStore() {
36692
- if (!this.appsOn) throw this.appsOff();
37480
+ if (!this.appsOn || this.appsWireOnly) throw this.appsOff();
36693
37481
  return this.appShelf ??= new Apps({
36694
37482
  fetch: boundedAppFetch({
36695
37483
  fetcher: this.options.appFetch,
@@ -36700,12 +37488,16 @@ var GhostlyNode = class GhostlyNode {
36700
37488
  return !!stored && !stored.group && !!stored.profile && !!stored.pairedPeerKey;
36701
37489
  },
36702
37490
  online: () => this.networkOn && !this.shuttingDown,
36703
- checked: () => this.events.onAppsChecked?.()
37491
+ checked: () => this.events.onAppsChecked?.(),
37492
+ ...this.options.builtInApp && { readBuiltIn: this.options.builtInApp }
36704
37493
  });
36705
37494
  }
36706
37495
  appList() {
36707
37496
  return this.appStore.list();
36708
37497
  }
37498
+ appBuiltIns() {
37499
+ return this.appStore.builtIns();
37500
+ }
36709
37501
  appStoreList() {
36710
37502
  return this.appStore.listStores();
36711
37503
  }
@@ -36751,6 +37543,9 @@ var GhostlyNode = class GhostlyNode {
36751
37543
  appFile(params) {
36752
37544
  return this.appStore.file(params);
36753
37545
  }
37546
+ appFileAt(params) {
37547
+ return this.appStore.fileAt(params);
37548
+ }
36754
37549
  appStorageGet(params) {
36755
37550
  return this.appStore.storageGet(params);
36756
37551
  }
@@ -36766,6 +37561,9 @@ var GhostlyNode = class GhostlyNode {
36766
37561
  appDataExport(params) {
36767
37562
  return this.appStore.exportData(params);
36768
37563
  }
37564
+ appDataExportText(params) {
37565
+ return this.appStore.exportDataText(params);
37566
+ }
36769
37567
  getState() {
36770
37568
  const groups = this.groups.views();
36771
37569
  return {
@@ -36793,6 +37591,7 @@ var GhostlyNode = class GhostlyNode {
36793
37591
  })).sort((a, b) => a.createdAt - b.createdAt),
36794
37592
  transfers: Object.fromEntries(this.transfers),
36795
37593
  transfersRestored: this.transfersRestored,
37594
+ ...this.groupsLoaded && { groupsLoaded: true },
36796
37595
  wallet: this.walletView,
36797
37596
  payments: this.desk.views(),
36798
37597
  identityProofs: this.identities.views(),
@@ -37645,7 +38444,7 @@ var GhostlyNode = class GhostlyNode {
37645
38444
  this.transport.networkChanged?.();
37646
38445
  this.directPath.reset();
37647
38446
  }
37648
- if (params.network) for (const failed of this.groupNativeFailed.values()) {
38447
+ if (params.network) for (const failed of this.nativeFailed.values()) {
37649
38448
  failed.at = 0;
37650
38449
  failed.wait = 0;
37651
38450
  }
@@ -37859,14 +38658,23 @@ var GhostlyNode = class GhostlyNode {
37859
38658
  /**
37860
38659
  * The message of a chat a reply answers, by its id here or the id both sides know it by (`replyRef`): what the
37861
38660
  * reply keeps of it, or why there is nothing to reply to. A group's reply needs the author's member key too.
38661
+ * Read alone by its id, or a chat's `peer_`/`me_` row for a wire id, as `resolveReply` does: a group's message goes
38662
+ * by its row's id, so a miss there is not here; a chat's other rows (a file's, a payment's) are looked for in the
38663
+ * whole chat.
37862
38664
  */
37863
38665
  async replyFor(linkId, messageId) {
37864
38666
  if (typeof messageId !== "string" || !messageId) return "No message to reply to";
37865
- const messages = await db.getMessages(linkId);
37866
- const original = messages.find((m) => m.id === messageId) ?? messages.find((m) => replyRef(m) === messageId);
38667
+ const group = linkId.startsWith("group:");
38668
+ let original = await db.getMessage(linkId, messageId);
38669
+ for (const rowId of group ? [] : [`peer_${messageId}`, `me_${messageId}`]) {
38670
+ if (original) break;
38671
+ const row = await db.getMessage(linkId, rowId);
38672
+ if (row && replyRef(row) === messageId) original = row;
38673
+ }
38674
+ if (!original && !group) original = (await db.getMessages(linkId)).find((m) => replyRef(m) === messageId);
37867
38675
  const ref = original && replyRef(original);
37868
38676
  if (!original || !ref) return "That message is not in this chat, or cannot be replied to";
37869
- if (linkId.startsWith("group:") && !original.member) return "That message cannot be replied to";
38677
+ if (group && !original.member) return "That message cannot be replied to";
37870
38678
  return replyTo(original, ref);
37871
38679
  }
37872
38680
  /**
@@ -38204,6 +39012,7 @@ var GhostlyNode = class GhostlyNode {
38204
39012
  const chat = this.links.get(linkId)?.stored;
38205
39013
  outbox = new Outbox({
38206
39014
  read: () => db.getMessages(linkId),
39015
+ get: (id) => db.getMessage(linkId, id),
38207
39016
  update: async (id, delivery, error, extra) => {
38208
39017
  if (delivery === "delivered") await db.patchMessage(linkId, id, (message) => message.delivery === "delivered" ? null : {
38209
39018
  ...message.delivery && {
@@ -39285,19 +40094,40 @@ var GhostlyNode = class GhostlyNode {
39285
40094
  const cue = transportCue(mark, log.entries, log.history);
39286
40095
  if (cue) this.cueFeedback(cue, linkId);
39287
40096
  }
39288
- saveTransportLog(live, log) {
40097
+ /** `toDisk: false` shows the story and leaves the disk as it is; `relaxed`: written with no disk sync of its own. */
40098
+ saveTransportLog(live, log, { toDisk = true, relaxed = false } = {}) {
39289
40099
  const transportLog = log.entries.map((e) => ({ ...e })), transportHistory = log.history.map((e) => ({ ...e }));
39290
40100
  live.stored = {
39291
40101
  ...live.stored,
39292
40102
  transportLog,
39293
40103
  transportHistory
39294
40104
  };
39295
- db.patchLink(live.stored.id, {
39296
- transportLog,
39297
- transportHistory
39298
- }).catch(() => {});
40105
+ if (toDisk) {
40106
+ live.rttUnsaved = false;
40107
+ db.patchLink(live.stored.id, {
40108
+ transportLog,
40109
+ transportHistory
40110
+ }, relaxed).catch(() => {});
40111
+ }
39299
40112
  this.emitState();
39300
40113
  }
40114
+ /** A round trip measured on the chat's transport: in the view at once, on disk at most every `RTT_SAVE_MS`. */
40115
+ noteRtt(live, ms) {
40116
+ const moved = live.rttPosted === void 0 || Math.abs(live.rttPosted - ms) >= 10;
40117
+ const log = this.transportLogOf(live);
40118
+ if (!log) {
40119
+ if (moved) this.emitState();
40120
+ return;
40121
+ }
40122
+ const changed = log.rtt(ms);
40123
+ if (changed) live.rttUnsaved = true;
40124
+ const now = Date.now();
40125
+ if (live.rttUnsaved && (live.rttSavedAt === void 0 || now - live.rttSavedAt >= RTT_SAVE_MS)) {
40126
+ live.rttSavedAt = now;
40127
+ this.saveTransportLog(live, log, { relaxed: true });
40128
+ } else if (changed) this.saveTransportLog(live, log, { toDisk: false });
40129
+ else if (moved) this.emitState();
40130
+ }
39301
40131
  /** Which ways of paying one chat allows. */
39302
40132
  /** `networks`: for a way of paying, the networks this chat takes it on (the Accept side's cards); the others are kept. */
39303
40133
  async setChatPaymentMethods({ linkId, methods, networks }) {
@@ -39329,7 +40159,9 @@ var GhostlyNode = class GhostlyNode {
39329
40159
  async connect({ linkId }) {
39330
40160
  const link = this.links.get(linkId)?.link;
39331
40161
  if (!link) throw new Error("You are offline. Go online before reconnecting.");
39332
- await this.ensureNativeEndpoints(linkId);
40162
+ const live = this.links.get(linkId), native = this.ensureNativeEndpoints(linkId);
40163
+ const { preferredTransport: preferred, transportFallback: fallback } = live.stored;
40164
+ if (this.reachedOnlyNatively(live) || fallback === false && preferred && preferred !== "webrtc/1") await native;
39333
40165
  await link.connect();
39334
40166
  }
39335
40167
  disconnect({ linkId }) {
@@ -39839,11 +40671,22 @@ var GhostlyNode = class GhostlyNode {
39839
40671
  }
39840
40672
  /** Mini-apps are on in this build (`APPS_ENABLED`) or for this engine (`apps`). */
39841
40673
  get appsOn() {
39842
- return this.options.apps ?? true;
40674
+ return !!(this.options.apps ?? true);
40675
+ }
40676
+ /** `apps/1` only (`apps: "wire"`): this host runs no mini-app and keeps no store, so nothing is installed here to check. */
40677
+ get appsWireOnly() {
40678
+ return this.options.apps === "wire";
39843
40679
  }
39844
- /** Why an app call is refused: this client runs no mini-app (`apps: false`), or the build has them off. */
40680
+ /** Why an app call is refused: this client runs no mini-app (`apps: false`, or `"wire"` for the store's), or the build has them off. */
39845
40681
  appsOff() {
39846
- return /* @__PURE__ */ new Error(this.options.apps === false ? "Apps do not run on this client" : "Apps are unavailable in this release");
40682
+ return /* @__PURE__ */ new Error(this.options.apps === false || this.appsWireOnly ? "Apps do not run on this client" : "Apps are unavailable in this release");
40683
+ }
40684
+ /** Before an app's frames go to a contact: a version removed or revoked here says nothing more (WISP 1200 § Takedowns). */
40685
+ async appChatRunnable(ref, digest) {
40686
+ if (!this.appsWireOnly) await this.appStore.chatRunnable({
40687
+ ref,
40688
+ digest
40689
+ });
39847
40690
  }
39848
40691
  /** A paired 1:1 chat and the chat app id of `ref` in it, from the two pinned participation keys (WISP 1200 § In a chat). */
39849
40692
  appChat(linkId, ref) {
@@ -39869,10 +40712,7 @@ var GhostlyNode = class GhostlyNode {
39869
40712
  async appOpen({ linkId, ref, version, digest }) {
39870
40713
  const { live, app } = this.appChat(linkId, ref);
39871
40714
  if (!isAppVersion(version)) throw new Error("Not an app version");
39872
- await this.appStore.chatRunnable({
39873
- ref,
39874
- digest
39875
- });
40715
+ await this.appChatRunnable(ref, digest);
39876
40716
  const open = live.appsOpen ??= /* @__PURE__ */ new Map();
39877
40717
  if (live.link) live.link.openApp(app, version);
39878
40718
  else open.set(app, version);
@@ -39888,10 +40728,7 @@ var GhostlyNode = class GhostlyNode {
39888
40728
  async appSend({ linkId, ref, data, digest }) {
39889
40729
  const { live, app } = this.appChat(linkId, ref);
39890
40730
  if (!live.appsOpen?.has(app)) return { error: "not-open" };
39891
- await this.appStore.chatRunnable({
39892
- ref,
39893
- digest
39894
- });
40731
+ await this.appChatRunnable(ref, digest);
39895
40732
  return { error: live.link ? live.link.sendAppData(app, data) : "offline" };
39896
40733
  }
39897
40734
  /** Every page went away: no app runs here any more, so each open one is closed (`close` goes where it can). */
@@ -41018,6 +41855,7 @@ var GhostlyNode = class GhostlyNode {
41018
41855
  const settings = rest;
41019
41856
  delete settings.backupReminders;
41020
41857
  const wasOnline = this.networkOn;
41858
+ if (Array.isArray(settings.iceServers)) settings.iceServers = settings.iceServers.map(iceServerSetting);
41021
41859
  for (const server of settings.iceServers ?? []) {
41022
41860
  const problem = iceServerProblem(server);
41023
41861
  if (problem) throw new Error(problem);
@@ -41986,7 +42824,7 @@ var GhostlyNode = class GhostlyNode {
41986
42824
  this.groupWakeReceived.delete(linkId);
41987
42825
  this.quietEdges.delete(linkId);
41988
42826
  this.groupNativeWaiting.delete(linkId);
41989
- this.groupNativeFailed.delete(linkId);
42827
+ this.nativeFailed.delete(linkId);
41990
42828
  this.groupNativeRetryAt = 0;
41991
42829
  if (live.stored.wakeToken && this.meshEdge(live.stored)) {
41992
42830
  const group = live.stored.group, peer = live.stored.groupPeer;
@@ -42102,7 +42940,7 @@ var GhostlyNode = class GhostlyNode {
42102
42940
  if (this.groupEntryTimer || this.shuttingDown || this.options.deferGroups) return;
42103
42941
  this.groupEntryTimer = setInterval(() => {
42104
42942
  this.groups.tick().catch(() => {});
42105
- this.retryGroupNative();
42943
+ this.retryNative();
42106
42944
  }, 1e3);
42107
42945
  }
42108
42946
  stopGroupEntries() {
@@ -42145,6 +42983,7 @@ var GhostlyNode = class GhostlyNode {
42145
42983
  usdtPaymentsSupport: !entry,
42146
42984
  barkPaymentsSupport: !entry,
42147
42985
  largeFilesSupport: !entry,
42986
+ binaryFilesSupport: !entry,
42148
42987
  params: stored,
42149
42988
  rtcAvailable: typeof RTCPeerConnection !== "undefined" && !this.edgeRtcOff.has(linkId),
42150
42989
  resume: !entry && stored.edgeLive ? resumeOn : void 0,
@@ -42250,10 +43089,11 @@ var GhostlyNode = class GhostlyNode {
42250
43089
  role,
42251
43090
  packetAt: presence.lastPacketAt
42252
43091
  });
43092
+ const same = samePresence(live.presence, presence);
42253
43093
  live.presence = presence;
42254
43094
  if (!entry && presence.nick && !live.link?.isDataLinkOpen) this.groups.edgeNick(group, peer, presence.nick);
42255
43095
  if (!entry) this.edgeRtcPresence(linkId, presence);
42256
- this.emitState();
43096
+ if (!same) this.emitState();
42257
43097
  },
42258
43098
  onPairingState: (state) => {
42259
43099
  live.pairing = state;
@@ -42340,7 +43180,7 @@ var GhostlyNode = class GhostlyNode {
42340
43180
  this.edgeRtcOff.delete(linkId);
42341
43181
  if (live) traceLink(live.myPubKeyZ32, "edge-rtc-back", {});
42342
43182
  this.groupNativeWaiting.delete(linkId);
42343
- this.groupNativeFailed.delete(linkId);
43183
+ this.nativeFailed.delete(linkId);
42344
43184
  this.groupNativeRetryAt = 0;
42345
43185
  this.restartEdge(linkId);
42346
43186
  }
@@ -42381,6 +43221,7 @@ var GhostlyNode = class GhostlyNode {
42381
43221
  callsSupport: this.options.callsSupport ?? typeof RTCPeerConnection !== "undefined",
42382
43222
  callsMissing: this.options.callsUnavailable,
42383
43223
  largeFilesSupport: true,
43224
+ binaryFilesSupport: true,
42384
43225
  typingSupport: true,
42385
43226
  reactionsSupport: this.options.reactions !== false,
42386
43227
  editSupport: !GhostlyNode.testNoEdit(),
@@ -42532,11 +43373,7 @@ var GhostlyNode = class GhostlyNode {
42532
43373
  this.emitState();
42533
43374
  },
42534
43375
  onLiveAttempt: () => this.emitState(),
42535
- onRtt: (ms) => {
42536
- const log = this.transportLogOf(live);
42537
- if (log?.rtt(ms)) this.saveTransportLog(live, log);
42538
- else this.emitState();
42539
- },
43376
+ onRtt: (ms) => this.noteRtt(live, ms),
42540
43377
  onDiscoveryError: (error) => {
42541
43378
  if ((error ?? void 0) !== live.discoveryError) {
42542
43379
  live.discoveryError = error ?? void 0;
@@ -42658,6 +43495,7 @@ var GhostlyNode = class GhostlyNode {
42658
43495
  this.emitState();
42659
43496
  },
42660
43497
  onPresence: (presence) => {
43498
+ let same = samePresence(live.presence, presence);
42661
43499
  live.presence = presence;
42662
43500
  if (!stored.profile && presence.nick && presence.nick !== live.stored.peerNick) {
42663
43501
  live.stored = {
@@ -42665,8 +43503,9 @@ var GhostlyNode = class GhostlyNode {
42665
43503
  peerNick: presence.nick
42666
43504
  };
42667
43505
  db.patchLink(linkId, { peerNick: presence.nick });
43506
+ same = false;
42668
43507
  }
42669
- this.emitState();
43508
+ if (!same) this.emitState();
42670
43509
  },
42671
43510
  onMessage: (message) => {
42672
43511
  if (live.stored.inviteCode) {
@@ -42861,7 +43700,7 @@ var GhostlyNode = class GhostlyNode {
42861
43700
  get nativeFactories() {
42862
43701
  const relay = this.relaysHyperdht ? this.hyperdhtRelay : "";
42863
43702
  return {
42864
- ...relay ? { "hyperdht/1": async (seedB64) => (await import("./hyperdhtRelay-C1p0nkle.js")).createRelayedHyperEndpoint(seedB64, relay) } : {},
43703
+ ...relay ? { "hyperdht/1": async (seedB64) => (await import("./hyperdhtRelay-CEmBpT0Q.js")).createRelayedHyperEndpoint(seedB64, relay) } : {},
42865
43704
  ...this.options.nativeTransports,
42866
43705
  ...this.ownIrohRelays ? { "iroh/1": (seedB64) => this.options.nativeTransports["iroh/1"](seedB64, this.settings.irohRelays?.length ? { relays: [...this.settings.irohRelays] } : {}) } : {},
42867
43706
  ...this.options.irohWeb ? { "iroh/1": (seedB64) => createIrohWebEndpoint(seedB64, { relays: this.irohRelays }) } : {}
@@ -42962,17 +43801,18 @@ var GhostlyNode = class GhostlyNode {
42962
43801
  return Promise.all(starts).then((full) => {
42963
43802
  const live = this.links.get(linkId), link = live?.link;
42964
43803
  if (this.shuttingDown || !live || !link || link !== expected) return;
42965
- if (live.stored.group && this.keepsGroupNative(live.stored)) {
43804
+ const group = !!live.stored.group;
43805
+ if (group && this.keepsGroupNative(live.stored)) {
42966
43806
  if (full.includes("no-slot") && !link.availableTransports.some((t) => t !== "webrtc/1")) this.groupNativeWaiting.add(linkId);
42967
43807
  else this.groupNativeWaiting.delete(linkId);
42968
- if (full.includes("failed")) {
42969
- const wait = Math.min(Math.max(GROUP_NATIVE_RETRY_MS, (this.groupNativeFailed.get(linkId)?.wait ?? 0) * 2), GROUP_NATIVE_FAILED_MAX_MS);
42970
- this.groupNativeFailed.set(linkId, {
42971
- at: Date.now() + wait,
42972
- wait
42973
- });
42974
- } else this.groupNativeFailed.delete(linkId);
42975
- } else this.groupNativeFailed.delete(linkId);
43808
+ }
43809
+ if ((group ? this.keepsGroupNative(live.stored) : this.keepsNativeEndpoints(linkId, live.stored)) && full.includes("failed")) {
43810
+ const wait = Math.min(Math.max(NATIVE_RETRY_MS, (this.nativeFailed.get(linkId)?.wait ?? 0) * 2), NATIVE_FAILED_MAX_MS);
43811
+ this.nativeFailed.set(linkId, {
43812
+ at: Date.now() + wait,
43813
+ wait
43814
+ });
43815
+ } else this.nativeFailed.delete(linkId);
42976
43816
  this.emitState();
42977
43817
  });
42978
43818
  }
@@ -43130,25 +43970,25 @@ var GhostlyNode = class GhostlyNode {
43130
43970
  /** Group links waiting for a free native slot (`ensureNativeEndpoints`), by link id. */
43131
43971
  groupNativeWaiting = /* @__PURE__ */ new Set();
43132
43972
  groupNativeRetryAt = 0;
43133
- /** Group links whose native listener failed to start, by link id: when each tries again, and how long it waited last. */
43134
- groupNativeFailed = /* @__PURE__ */ new Map();
43973
+ /** Chats and group links whose native listener failed to start, by link id: when each tries again, and how long it waited last. */
43974
+ nativeFailed = /* @__PURE__ */ new Map();
43135
43975
  /**
43136
43976
  * Now and then, the group links that found no free native slot try again: a chat or another group may have let one go.
43137
- * Those whose listener failed to start try again once their wait is over.
43977
+ * Chats and group links whose listener failed to start try again once their wait is over.
43138
43978
  */
43139
- retryGroupNative() {
43979
+ retryNative() {
43140
43980
  const now = Date.now();
43141
- for (const [linkId, failed] of this.groupNativeFailed) {
43981
+ for (const [linkId, failed] of this.nativeFailed) {
43142
43982
  if (now < failed.at) continue;
43143
43983
  if (!this.links.get(linkId)?.link) {
43144
- this.groupNativeFailed.delete(linkId);
43984
+ this.nativeFailed.delete(linkId);
43145
43985
  continue;
43146
43986
  }
43147
43987
  failed.at = now + failed.wait;
43148
43988
  this.ensureNativeEndpoints(linkId);
43149
43989
  }
43150
43990
  if (!this.groupNativeWaiting.size || Date.now() < this.groupNativeRetryAt) return;
43151
- this.groupNativeRetryAt = Date.now() + GROUP_NATIVE_RETRY_MS;
43991
+ this.groupNativeRetryAt = Date.now() + NATIVE_RETRY_MS;
43152
43992
  for (const linkId of [...this.groupNativeWaiting]) {
43153
43993
  if (!this.links.get(linkId)?.link) {
43154
43994
  this.groupNativeWaiting.delete(linkId);
@@ -43253,6 +44093,24 @@ var GhostlyNode = class GhostlyNode {
43253
44093
  } else this.placedAt.set(message.linkId, Math.max(last, Math.min(message.timestamp, now)));
43254
44094
  return message;
43255
44095
  }
44096
+ /**
44097
+ * These rows, in this order, placed again below everything in their history, as if they came now: a group member's
44098
+ * messages that came above an older one of theirs (WISP 400, requirement 10), and the lines that came after them. The
44099
+ * places are taken before anything is awaited, so a row that comes meanwhile goes below them. Each still shows the
44100
+ * time it showed. All of them in one write: one disk sync, not one a row.
44101
+ */
44102
+ async placeAgain(linkId, ids) {
44103
+ const places = new Map(ids.map((id) => {
44104
+ const timestamp = arrivalKey(Date.now(), this.placedAt.get(linkId));
44105
+ this.placedAt.set(linkId, timestamp);
44106
+ return [id, timestamp];
44107
+ }));
44108
+ const moved = await db.patchMessages(linkId, [...places.keys()], (m) => ({
44109
+ timestamp: places.get(m.id),
44110
+ sentAt: shownTime(m)
44111
+ }));
44112
+ if (moved.length) await this.messagesChanged(linkId, moved.map((m) => m.id));
44113
+ }
43256
44114
  async storeMessage(message) {
43257
44115
  await this.storeNewMessage(message);
43258
44116
  }
@@ -43372,7 +44230,7 @@ var GhostlyNode = class GhostlyNode {
43372
44230
  transportWait: live.link?.transportWait,
43373
44231
  liveAttempt: live.link?.liveAttempt,
43374
44232
  liveDialer: live.stored.profile && !live.stored.group ? live.link?.dialer : void 0,
43375
- transportRttMs: live.link?.rttMs,
44233
+ transportRttMs: live.rttPosted = live.link?.rttMs,
43376
44234
  transportRelayed: live.link?.relayedPath,
43377
44235
  transportLive: live.transportLog?.liveNow(),
43378
44236
  transportLog: stored.id === this.activeLinkId && stored.profile && !stored.group ? stored.transportLog ?? [] : void 0,
@@ -43430,6 +44288,7 @@ var GhostlyNode = class GhostlyNode {
43430
44288
  this.stateTimer = null;
43431
44289
  if (this.gatedOut) return;
43432
44290
  this.events.onState(this.getState());
44291
+ this.dropBriefViews();
43433
44292
  if (this.deviceLinks) this.syncDeviceTokens();
43434
44293
  }, delayMs);
43435
44294
  }
@@ -43441,11 +44300,16 @@ var GhostlyNode = class GhostlyNode {
43441
44300
  }
43442
44301
  if (this.gatedOut) return;
43443
44302
  this.events.onState(this.getState());
44303
+ this.dropBriefViews();
44304
+ }
44305
+ /** A state went with the brief views: they go, so the next states do not carry them (`briefViews`). */
44306
+ dropBriefViews() {
44307
+ for (const [id, view] of this.briefViews) if (this.transfers.get(id) === view) this.transfers.delete(id);
44308
+ this.briefViews.clear();
43444
44309
  }
43445
44310
  };
43446
44311
  //#endregion
43447
44312
  //#region ../browser/src/engine/server.ts
43448
- var server_exports = /* @__PURE__ */ __exportAll({ EngineServer: () => EngineServer });
43449
44313
  var EngineServer = class {
43450
44314
  /** The whole engine: never made on a device that is not the active one (`devices/peer.ts` decides, WISP 06 § The gate). */
43451
44315
  gated = false;
@@ -43517,6 +44381,10 @@ var EngineServer = class {
43517
44381
  get historyMark() {
43518
44382
  return this.changes;
43519
44383
  }
44384
+ /** Whether a chat's or group's history changed after `mark` (`historyMark`); with none, since this engine was made. */
44385
+ historyChanged(id, mark = 0) {
44386
+ return (this.changedAt.get(id) ?? 0) > mark;
44387
+ }
43520
44388
  /**
43521
44389
  * `read`: the chats and groups whose whole history the client read itself, after `mark` (`historyMark`). One that has
43522
44390
  * not changed since is not read and sent again: the client hears only what changes in it from now on.
@@ -43679,6 +44547,6 @@ function signalKind(signal) {
43679
44547
  }
43680
44548
  }
43681
44549
  //#endregion
43682
- export { EngineServer, hyperdhtRelayProblem as n, server_exports as t };
44550
+ export { EngineServer };
43683
44551
 
43684
- //# sourceMappingURL=server-0zfNpABQ.js.map
44552
+ //# sourceMappingURL=server-CBCEYKMN.js.map