@ghostlytools/cli 1.3.10 → 1.4.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.
- package/README.md +13 -4
- package/SKILL.md +7 -4
- package/dist/assets/{api-Cp9GxjUO.js → api-BqLfti32.js} +2 -2
- package/dist/assets/{api-BCrwJ57L.js → api-JMkEO0RR.js} +130 -50
- package/dist/assets/api-JMkEO0RR.js.map +1 -0
- package/dist/assets/{apiKit-B4qhnLQT.js → apiKit-BpIUCwa-.js} +3 -3
- package/dist/assets/{apiKit-B4qhnLQT.js.map → apiKit-BpIUCwa-.js.map} +1 -1
- package/dist/assets/{apps-CpxENMgl.js → apps-BdfyGVZ1.js} +1 -1
- package/dist/assets/{apps-DOH1P_S0.js → apps-Bettj7WQ.js} +6 -6
- package/dist/assets/apps-Bettj7WQ.js.map +1 -0
- package/dist/assets/{arkade-Cyf5MGxG.js → arkade-CP4HvKaa.js} +2 -2
- package/dist/assets/{arkade-Cyf5MGxG.js.map → arkade-CP4HvKaa.js.map} +1 -1
- package/dist/assets/{backup-CqM-0-2v.js → backup-BbO2zLjx.js} +1 -1
- package/dist/assets/{backup-CTpFFQ-4.js → backup-BmBtta66.js} +7 -7
- package/dist/assets/{backup-CTpFFQ-4.js.map → backup-BmBtta66.js.map} +1 -1
- package/dist/assets/bark_ffi_wasm_bg-EY2uc0Bx.js.map +1 -1
- package/dist/assets/bitcoindevkit_bg-B-iLWrmX.js.map +1 -1
- package/dist/assets/blobFiles-B0rNnHlO.js +35 -0
- package/dist/assets/blobFiles-B0rNnHlO.js.map +1 -0
- package/dist/assets/{buttons-BtxwyM2l.js → buttons-YgijrprJ.js} +4 -4
- package/dist/assets/{buttons-BtxwyM2l.js.map → buttons-YgijrprJ.js.map} +1 -1
- package/dist/assets/buttons-_GBlxAti.js +2 -0
- package/dist/assets/{endpoint-BOTFMpfS.js → endpoint-Cl3cgAYl.js} +3 -2
- package/dist/assets/endpoint-Cl3cgAYl.js.map +1 -0
- package/dist/assets/{envelope-DW8TWKPD.js → envelope-C7vnmsJN.js} +2 -2
- package/dist/assets/{envelope-DW8TWKPD.js.map → envelope-C7vnmsJN.js.map} +1 -1
- package/dist/assets/{errors-B-zMxbcd.js → errors-BLSPp0tK.js} +3 -1
- package/dist/assets/errors-BLSPp0tK.js.map +1 -0
- package/dist/assets/{fileBytes-Di7VZtmU.js → fileBytes-B-fqGdEd.js} +3 -3
- package/dist/assets/{fileBytes-Di7VZtmU.js.map → fileBytes-B-fqGdEd.js.map} +1 -1
- package/dist/assets/{fileBytes-CgbibpbV.js → fileBytes-C2jycN_7.js} +1 -1
- package/dist/assets/{fileBytes-CfSXjTFa.js → fileBytes-D6fqFQEX.js} +6 -3
- package/dist/assets/fileBytes-D6fqFQEX.js.map +1 -0
- package/dist/assets/fileBytesIdb-DOxm_BKt.js +2 -0
- package/dist/assets/{fileBytesIdb-D5u6H8X_.js → fileBytesIdb-_HnxdacI.js} +3 -3
- package/dist/assets/{fileBytesIdb-D5u6H8X_.js.map → fileBytesIdb-_HnxdacI.js.map} +1 -1
- package/dist/assets/{fileBytesOpfs-Dd1N4ERH.js → fileBytesOpfs-p_CANncD.js} +3 -3
- package/dist/assets/{fileBytesOpfs-Dd1N4ERH.js.map → fileBytesOpfs-p_CANncD.js.map} +1 -1
- package/dist/assets/{groupFiles-uxa0U9_n.js → groupFiles-sIdGq4AV.js} +5 -5
- package/dist/assets/groupFiles-sIdGq4AV.js.map +1 -0
- package/dist/assets/{host-BbDWamRc.js → host-Drd-c4VO.js} +43 -20
- package/dist/assets/host-Drd-c4VO.js.map +1 -0
- package/dist/assets/{hyperdhtRelay-De5FT7Am.js → hyperdhtRelay-13aQ4KOf.js} +2 -2
- package/dist/assets/{hyperdhtRelay-De5FT7Am.js.map → hyperdhtRelay-13aQ4KOf.js.map} +1 -1
- package/dist/assets/{idb-B5L8wuOm.js → idb-CMVgtn2k.js} +59 -6
- package/dist/assets/idb-CMVgtn2k.js.map +1 -0
- package/dist/assets/{irohWeb-z5QDDWa-.js → irohWeb-DHCxSvHF.js} +2 -2
- package/dist/assets/{irohWeb-z5QDDWa-.js.map → irohWeb-DHCxSvHF.js.map} +1 -1
- package/dist/assets/{main-_q4Fgypp.js → main-BWAwKY4f.js} +476 -141
- package/dist/assets/main-BWAwKY4f.js.map +1 -0
- package/dist/assets/{mainline-Cld-bKeC.js → mainline-CsG-DdJ5.js} +2 -2
- package/dist/assets/{mainline-Cld-bKeC.js.map → mainline-CsG-DdJ5.js.map} +1 -1
- package/dist/assets/{networkReason-vUHo6A_C.js → networkReason-B91upMCb.js} +3 -3
- package/dist/assets/{networkReason-vUHo6A_C.js.map → networkReason-B91upMCb.js.map} +1 -1
- package/dist/assets/{nwc-zXpmd2FN.js → nwc-BFv67iSz.js} +3 -3
- package/dist/assets/{nwc-zXpmd2FN.js.map → nwc-BFv67iSz.js.map} +1 -1
- package/dist/assets/{nwcProvider-D482SQ1a.js → nwcProvider-BKNFdH9v.js} +2 -2
- package/dist/assets/{nwcProvider-D482SQ1a.js.map → nwcProvider-BKNFdH9v.js.map} +1 -1
- package/dist/assets/{profiles-gPCjLOQq.js → profiles-DSxPYdlU.js} +13 -3
- package/dist/assets/profiles-DSxPYdlU.js.map +1 -0
- package/dist/assets/{registry-CxsDR3c6.js → registry-CHPd_hf7.js} +1 -1
- package/dist/assets/{registry-OVx2kY3d.js → registry-qbnWmwCE.js} +3 -3
- package/dist/assets/registry-qbnWmwCE.js.map +1 -0
- package/dist/assets/{resolve-CPTAnu-9.js → resolve-B63_uSu7.js} +2 -2
- package/dist/assets/{resolve-CPTAnu-9.js.map → resolve-B63_uSu7.js.map} +1 -1
- package/dist/assets/{restoredRows-Db3oUTko.js → restoredRows-DeBImGRd.js} +2 -2
- package/dist/assets/{restoredRows-Db3oUTko.js.map → restoredRows-DeBImGRd.js.map} +1 -1
- package/dist/assets/{server-C-EkLCCt.js → server-Cd1dLOB8.js} +1456 -228
- package/dist/assets/server-Cd1dLOB8.js.map +1 -0
- package/dist/assets/{src-Dg1bvMNv.js → src-DzXL-GYu.js} +7277 -5975
- package/dist/assets/src-DzXL-GYu.js.map +1 -0
- package/dist/assets/turnRefs-BCtM10d3.js +357 -0
- package/dist/assets/turnRefs-BCtM10d3.js.map +1 -0
- package/dist/assets/{usdt-BY5OIIvG.js → usdt-B7JWDhQV.js} +3 -3
- package/dist/assets/{usdt-BY5OIIvG.js.map → usdt-B7JWDhQV.js.map} +1 -1
- package/dist/ghostly.mjs +1 -1
- package/package.json +7 -6
- package/skills/ghostly/README.md +41 -0
- package/skills/ghostly/SKILL.md +79 -0
- package/skills/ghostly/scripts/listen +67 -0
- package/skills/ghostly/scripts/react +25 -0
- package/skills/ghostly/scripts/say +44 -0
- package/skills/ghostly/scripts/status +27 -0
- package/dist/assets/api-BCrwJ57L.js.map +0 -1
- package/dist/assets/apps-DOH1P_S0.js.map +0 -1
- package/dist/assets/buttons-CJqNkG-Q.js +0 -2
- package/dist/assets/endpoint-BOTFMpfS.js.map +0 -1
- package/dist/assets/errors-B-zMxbcd.js.map +0 -1
- package/dist/assets/fileBytes-CfSXjTFa.js.map +0 -1
- package/dist/assets/fileBytesIdb-CFP-DSkZ.js +0 -2
- package/dist/assets/groupFiles-uxa0U9_n.js.map +0 -1
- package/dist/assets/host-BbDWamRc.js.map +0 -1
- package/dist/assets/idb-B5L8wuOm.js.map +0 -1
- package/dist/assets/main-_q4Fgypp.js.map +0 -1
- package/dist/assets/profiles-gPCjLOQq.js.map +0 -1
- package/dist/assets/registry-OVx2kY3d.js.map +0 -1
- package/dist/assets/server-C-EkLCCt.js.map +0 -1
- package/dist/assets/src-Dg1bvMNv.js.map +0 -1
- package/dist/assets/turnRefs-WnggXm2T.js +0 -323
- package/dist/assets/turnRefs-WnggXm2T.js.map +0 -1
|
@@ -1,27 +1,26 @@
|
|
|
1
1
|
import { n as traceJoin } from "./joinTrace-CjRDFy85.js";
|
|
2
|
-
import { $c as
|
|
2
|
+
import { $a as GROUP_MEMBER_CAP, $c as DEVICES_CAPABILITY, $i as DHT_MESSAGE_TTL, $l as randomBytes, $o as handoffContextProof, $r as chatAppId, $s as knockSlotIdentity, A as knockRecords, Aa as wireReaction, Ac as isAppRef, Ai as capsDescriptors, Al as normalizeRelayUrl, An as findEndpoint, Ar as transferStalled, B as GROUP_READ_NOTE, Bc as createContactLink, Bi as identityStatement, Bn as engineError, Bo as HANDOFF_OFFER, Br as pinIsNewer, Bs as readEnrollHello, C as decodeCommunityLink, Ca as ReactionWindow, Cl as turnKeys, Co as RTC_CONFIG, Cr as readBeacon, Cs as readSetUpdate, D as encodeGroupEntryLink, Da as reactionIsNewer, Dl as DEFAULT_RELAYS, Dn as ENDPOINT, Dr as ChatFiles, Ds as verifySetUpdate, E as decodeGroupEntryLink, Ea as reactionEmoji, Ec as appRef, El as turnVerifierMark, En as decodeBolt11, Er as shouldBeHub, Es as signTombstone, F as groupWakeFrame, Fc as parseImageMeta, Fl as withRequestOptions, Fn as INVITE_TAKEN, Fo as HANDOFF_ACK, Fr as typingActivity, Fs as enrollDoneFrame, G as GROUP_EDIT_TEXT_BYTES, Ga as sameButtonText, Gc as readDeviceInvite, Gi as PeerProofs, Gl as publicKeyFromZ32, Go as HANDOFF_VERIFIED, Gr as deviceWakeRequest, Gs as newInbox, Gt as encodeDidDhtPacket, H as frameBytes, Hc as readContactLink, Hi as receivedIdentityStatus, Hl as createIdentity, Hn as parseEngineError, Ho as HANDOFF_PIECE_BYTES, Hr as readPin, Hs as clearMissed, Ht as didDhtDocument, I as groupWakes, Ii as IdentityExchange, Il as PacketTooLargeError, In as LIVENESS_PING_MS, Io as HANDOFF_CANCEL, Is as enrollGrantFrame, J as mentionsMember, Ja as claimedTime, Jc as decodeInviteCode, Ji as TRANSPORTS, Jl as sanitizeAvatar, Jn as groupFileFallback, Jo as handoffAttemptFailed, Jr as relayRequest, Js as takeRequest, Jt as ETHEREUM_USDT, K as carryMentions, Ka as statusCardText, Kc as OwnInviteError, Ki as emptyProofLedger, Kl as verify, Ko as filePartName, Kr as newWakeToken, Ks as planKnocks, L as parseGroupWakeFrame, Lc as canonicalJson, Ln as PAIRED_FRAME_TOO_LARGE, Lo as HANDOFF_DONE, Lr as PIN_LIMITS, Ls as enrollHelloFrame, M as readKnocks, Ma as readReply, Ml as traceLink, Mn as parseCashuRequestPayload, Mo as HTTP_SERVICE_PROTO, Ms as enrollCancelFrame, Na as replySnippet, Nl as budgetRetryMs, Nn as parseFedimintRequestPayload, No as LEGACY_SERVICES, Nr as TypingSender, Ns as enrollCancelReason, O as entryParams, Oa as readReaction, Oc as isAppHash, Oi as CapsExchange, Ol as RelayTransport, On as cashuRequestPayload, Or as FILE_LIMITS, P as GroupWakeLimiter, Pa as BUTTON_ID, Pi as IDENTITY_REVOCATION_LABEL, Pl as isDiscoveryBudgetError, Pn as GhostLink, Po as serviceIdFromName, Ps as enrollDigits, Qa as GROUP_ADMIN_OFF_ERROR, Qo as handoffContext, Qs as contactDay, Qt as SEPOLIA_TEST_USDT, R as GROUP_LIMITS, Ri as emptyIdentityLedger, Rl as createRelayPayload, Rn as pairedMessageFits, Ro as HANDOFF_HAVE, Rr as mayPin, Rs as enrollProofFrame, S as communityHasFrame, Sa as REACTION_LIMITS, Sc as sha256Paced, Sl as turnInstance, Sr as rankHubs, Ta as queueReaction, Tc as appFingerprint, Tl as turnStatedVerifier, Tr as readLobby, Ts as setUpdateFrame, Uc as createDeviceInvite, Ui as revokesIdentity, Ul as identityFromSeed, Un as GROUP_FILE_LIMITS, Uo as HANDOFF_REQUEST, Ur as WAKE_CALL_INTERVAL_MS, Us as endedRequests, Ut as didDhtFromPublicKey, V as GroupSession, Vc as encodeContactLink, Vi as newIdentityBinding, Vn as engineText, Vo as HANDOFF_PAKE, Vr as pinNumberHolds, Vs as verifyEnrollProof, Wa as checkStatusCard, Wc as deviceInviteJoinerParams, Wi as lastSharedWithMe, Wl as identityFromSeedB64, Wo as HANDOFF_TIMINGS, Wr as WakeLimiter, Ws as inboxRecord, Wt as encodeDidDhtDeactivated, Xa as receivedTimestamp, Xc as inviteLink, Xl as bytesEqual, Xn as readGroupFileMeta, Xo as handoffBusyFrame, Xr as checkPushEndpoint, Xs as ContactRequestError, Xt as ONCHAIN_PROVIDER, Y as groupName, Ya as heardTime, Yc as encodeInviteCode, Yi as automaticTransport, Yl as sanitizeDisplayText, Yo as handoffAttemptSucceeded, Yr as wakeRequest, Ys as turnOff, Yt as EVM_TEST_CHAINS, Za as shownTime, Zc as inviteOwnership, Zl as fromBase64Url, Zo as handoffCancelFrame, Zr as vapidKeysMatch, Zs as REQUEST_LIFETIME_MS, Zt as PaymentPreflightError, _ as openGroupTyping, _a as parseLinkPreview, _l as firstDeviceSetSecret, _o as parseLocalTarget, _r as lobbyRecords, _s as readHandoffPake, a as appStoreDecision, aa as sanitizeMime, ac as openRequest, ai as UPGRADE_CAPABILITY, al as deviceLinkPairing, an as assertWholeSats, ao as rosterHas, ar as groupSignalFrame, as as handoffOfferFrame, b as CommunitySession, ba as COMMUNITY_REACTION_FRAME, bl as signTurnPacket, bo as decodeControl, br as newerHead, bs as sealHandoffPiece, c as planAppUpdate, cc as requestRecords, ci as HOLD_LIMITS, cl as seedSigner, cn as walletNetworkOf, co as DirectPathWatch, cr as mayBeHub, cs as handoffRetryAfter, dc as APP_BUNDLE_MAGIC, di as isHoldUrl, dl as TURN_RAISE_READ_TIMEOUT_MS, dn as sparkAddressKind, dr as COMMUNITY_TOPOLOGY, ds as newHandoffId, ea as peekDhtMailbox, ec as knockSlotOf, ei as isAppVersion, el as ENROLL_CAPABILITY, en as TEST_USDT_FAUCET_AMOUNT, er as CarriedTransport, es as handoffDataFrame, eu as toBase64Url, f as readAppStoreSteps, fa as parseVideoMeta, fc as APP_ICON_PATH, fi as newMailbox, fl as TURN_SETTLE_MS, fn as sparkInvoiceDetails, fo as RELAY_POLL_INTERVALS, fr as beaconKeys, fs as readHandoffAck, g as groupTypingEpoch, ga as dhtEditId, gl as TURN_MAX, go as formatLocalTarget, gr as lobbyKeys, gs as readHandoffHello, gt as normalizeAtprotoHandle, h as GroupTypingBoard, ha as RateWindow, hc as appViewOf, hl as TURN_LAST_SEQUENCE, hn as isPubkyKey, ho as GhostlyHttpError, hr as freshHubs, hs as readHandoffHave, ht as isAtprotoDid, i as appRevocationFor, ia as sanitizeFileName, ic as mergeContactKnocks, ii as STATUS_CARD_CAPABILITY, il as deviceKeyZ32, in as assertTokenUnits, io as rosterAdmin, is as handoffManifestFrames, j as mergeKnocks, ja as pairedReplyAuthor, jc as readAppRevocation, ji as dialDescriptors, jn as isFederationId, jo as emptyLinkRecords, k as knockIdentity, ka as validReactionNumber, kc as isAppKey, ki as DHT_TEXT_CAPABILITY, kl as currentRelays, kn as fedimintRequestPayload, ko as parseCallSignal, kr as transferEnded, l as APP_STORE_LIMITS, la as fileMessageText, lc as seenEntry, li as HoldKeys, ll as webCryptoSigner, ln as SPARK_NETWORKS, lr as meshHubs, ls as handoffStreamKey, ma as EDIT_RATE_WINDOW_MS, mc as appDigest, ml as turnPutSummary, mo as presenceSeenAt, mr as doorHubs, ms as readHandoffDone, n as readX401SdJwtVc, na as dhtFileId, nc as linkRecords, ni as BUTTONS_CAPABILITY, nl as deviceEchoFrame, nn as WALLET_NETWORKS, no as commitHash, ns as handoffEphemeral, nu as utf8Decode, o as appUpdateDecision, oc as readKnockSlot, oi as HOLD_CAPABILITY, ol as devicePingFrame, on as formatPaymentAmount, oo as verifyCommitSignature, or as readGroupSignal, os as handoffPakeFrame, pa as EDIT_BUFFER_MS, pi as readManifest, pl as classifyTurnRead, pn as isBitcoinAddress, po as WATCH_PEER_MS, pr as beaconRecords, ps as readHandoffCancel, q as parseCommunityEdit, qa as arrivalKey, qc as createChatInvite, qi as validExternalKey, qo as handoffAttemptAllowed, qr as parseWakeFrame, qs as settleRequest, qt as signDidDhtPacket, r as sameX401HolderJwk, ra as formatFileSize, rc as makeStampPaced, ri as EDIT_CAPABILITY, rl as deviceEchoNonce, rn as arkAddressPrefix, ro as parseGroupCommit, rs as handoffHelloFrame, ru as utf8Encode, s as checkAppBeforeRun, sc as readLinkRecord, si as HOLD_DATED_BACK_MS, sl as newDeviceSetSecret, sn as validatePaymentTarget, so as edgeParams, sr as MESH_HUBS, ss as handoffReleaseFrame, t as newX401HolderKey, ta as DHT_FILE_CAPABILITY, tc as knockSlotRecords, ti as APPS_CAPABILITY, tl as HANDOFF_CAPABILITY, tn as USDT_PUBLIC_RPC, to as MEMBER_KEY, ts as handoffDigest, tu as toZ32, ua as parseVoiceMeta, uc as APP_BUNDLE_LIMITS, ui as HoldRefusedError, un as SPARK_PROVIDER, uo as DHT_POLL_INTERVALS, ur as pickMeshHubs, us as handoffVersions, v as sealGroupTyping, va as forwardedAgain, vc as isAppUrl, vl as nextTurnPosition, vo as webLocalFetch, vr as mergeBeacon, vs as readHandoffTurnFrame, w as encodeCommunityLink, wa as nextReactionNumber, wc as APP_STATEMENT_LIMITS, wl as turnName, wr as readBeaconHead, ws as setAckFrame, x as GROUP_READ_NOTE_COMMUNITY, xa as GROUP_REACTION_FRAME, xc as readAppBundleAsync, xl as signTurnRelease, xo as encodeControl, xr as pickHubs, y as COMMUNITY_LIMITS, ya as readForwarded, yc as pacer, yl as readTurnPacket, yo as LIMITS, yr as mergeLobby, ys as readHandoffVerified, za as STATUS_CARD_LIMITS, zc as readCanonicalJson, zi as identityRevocationValue, zn as pairedMessageFrame, zo as HANDOFF_HELLO, zr as nextPinNumber, zs as readEnrollGrant } from "./src-DzXL-GYu.js";
|
|
3
3
|
import { i as SourceUnreachableError, n as PROVIDER_PLATFORMS, o as isNothingSpentError, r as SourceConfigError, t as NothingSpentError, u as redact } from "./types-C4_KafL_.js";
|
|
4
4
|
import { t as bitcoindRpc } from "./bitcoindProvider-DGw3xBSa.js";
|
|
5
|
-
import { t as nwc } from "./nwcProvider-
|
|
6
|
-
import { a as clearProfileStores, c as
|
|
7
|
-
import { a as sealSeed, i as newDeviceKey, o as unsealSeed, r as intentRepository } from "./networkReason-
|
|
8
|
-
import { $ as setSingleWriterGate, A as LightningCards, B as breezDatabaseInUse, C as FileAppender, D as streamStored, E as storedSize, F as ProviderSources, G as ModeChanged, H as dropBreezDatabase, I as sourceKey, J as networkLabel, K as ModeGate, L as CASHU_MINT_SOURCE, M as isRecoveryPhrase, N as normalizePhrase, P as lightningPaying, Q as setAwayWallets, R as cashuMint, S as WALLET_TYPES, T as removeStored, U as iceServerProblem, V as breezDatabasesFor, W as iceServerSetting, X as refuseAway, Y as awayFrom, Z as requireTurn, _ as crossNetwork, a as storedGroupFiles, b as walletInstances, c as receivedPairedReply, d as paymentAlias, f as paymentWireId, g as createTiming, h as createFailure, i as groupOfFile, j as cardsKey, k as CASHU_CARD, l as replyRef, m as assertConfirmedReal, n as groupFileId, o as groupReply, p as WALLET_NAMES, q as WrongNetworkError, r as groupFileText, s as pairedWireReply, t as GroupFileDesk, u as replyTo, v as paymentNetwork, w as readStored$1, y as paymentNetworksOf, z as breezDatabase } from "./groupFiles-
|
|
9
|
-
import { i as fedimintDatabase, n as createIrohWebEndpoint, o as loadFedimintSdk, r as irohRelayProblem, t as DEFAULT_IROH_RELAYS } from "./irohWeb-
|
|
5
|
+
import { t as nwc } from "./nwcProvider-BKNFdH9v.js";
|
|
6
|
+
import { a as clearProfileStores, c as errorLine, d as openDb, f as profileOpenFailure, h as wrap, i as STORES$1, l as fileStore, m as transact, n as APP_STORAGE_SCOPE_INDEX, o as databaseName, p as store, r as CARD_INDEX, s as deleteDatabase, t as APP_STORAGE_CHAT_INDEX, u as newestWalletTx } from "./idb-CMVgtn2k.js";
|
|
7
|
+
import { a as sealSeed, i as newDeviceKey, o as unsealSeed, r as intentRepository } from "./networkReason-B91upMCb.js";
|
|
8
|
+
import { $ as setSingleWriterGate, A as LightningCards, B as breezDatabaseInUse, C as FileAppender, D as streamStored, E as storedSize, F as ProviderSources, G as ModeChanged, H as dropBreezDatabase, I as sourceKey, J as networkLabel, K as ModeGate, L as CASHU_MINT_SOURCE, M as isRecoveryPhrase, N as normalizePhrase, P as lightningPaying, Q as setAwayWallets, R as cashuMint, S as WALLET_TYPES, T as removeStored, U as iceServerProblem, V as breezDatabasesFor, W as iceServerSetting, X as refuseAway, Y as awayFrom, Z as requireTurn, _ as crossNetwork, a as storedGroupFiles, b as walletInstances, c as receivedPairedReply, d as paymentAlias, f as paymentWireId, g as createTiming, h as createFailure, i as groupOfFile, j as cardsKey, k as CASHU_CARD, l as replyRef, m as assertConfirmedReal, n as groupFileId, o as groupReply, p as WALLET_NAMES, q as WrongNetworkError, r as groupFileText, s as pairedWireReply, t as GroupFileDesk, u as replyTo, v as paymentNetwork, w as readStored$1, y as paymentNetworksOf, z as breezDatabase } from "./groupFiles-sIdGq4AV.js";
|
|
9
|
+
import { i as fedimintDatabase, n as createIrohWebEndpoint, o as loadFedimintSdk, r as irohRelayProblem, t as DEFAULT_IROH_RELAYS } from "./irohWeb-DHCxSvHF.js";
|
|
10
10
|
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";
|
|
11
|
-
import { _ as reserveAdapterIds, b as testBuild, f as setOwnDidSource, g as registeredOnchainProviders, h as registeredLightningProviders, l as chosenResolver, n as resolveAtprotoDid, p as onAdaptersChanged, t as assertPublicServer, v as boundedIdentityFetch, y as verifyIdentity } from "./resolve-
|
|
12
|
-
import { c as fileSpace, f as FILE_BYTES_STEP, o as fileBytes, r as blobDigest, s as fileBytesOf, u as removeFileBytes } from "./fileBytes-
|
|
13
|
-
import { n as MAX_BACKUP_BYTES, t as BACKUP_MEDIA_TYPE } from "./envelope-
|
|
11
|
+
import { _ as reserveAdapterIds, b as testBuild, f as setOwnDidSource, g as registeredOnchainProviders, h as registeredLightningProviders, l as chosenResolver, n as resolveAtprotoDid, p as onAdaptersChanged, t as assertPublicServer, v as boundedIdentityFetch, y as verifyIdentity } from "./resolve-B63_uSu7.js";
|
|
12
|
+
import { c as fileSpace, f as FILE_BYTES_STEP, o as fileBytes, r as blobDigest, s as fileBytesOf, u as removeFileBytes } from "./fileBytes-B-fqGdEd.js";
|
|
13
|
+
import { n as MAX_BACKUP_BYTES, t as BACKUP_MEDIA_TYPE } from "./envelope-C7vnmsJN.js";
|
|
14
14
|
import { t as hyperdhtRelayProblem } from "./hyperdhtRelay-BT_wRxab.js";
|
|
15
15
|
import { sha256 } from "@noble/hashes/sha2.js";
|
|
16
16
|
import { hkdf } from "@noble/hashes/hkdf.js";
|
|
17
17
|
import { base64, base64url, base64urlnopad, bech32, hex } from "@scure/base";
|
|
18
|
-
import { bytesToHex, concatBytes, hexToBytes, utf8ToBytes } from "@noble/hashes/utils.js";
|
|
19
18
|
import { chacha20poly1305 } from "@noble/ciphers/chacha.js";
|
|
19
|
+
import { bytesToHex, concatBytes, hexToBytes, utf8ToBytes } from "@noble/hashes/utils.js";
|
|
20
20
|
import { secp256k1 } from "@noble/curves/secp256k1.js";
|
|
21
21
|
import { verifyEvent } from "nostr-tools/pure";
|
|
22
22
|
import { generateMnemonic, mnemonicToSeedSync, validateMnemonic } from "@scure/bip39";
|
|
23
23
|
import { wordlist } from "@scure/bip39/wordlists/english.js";
|
|
24
|
-
import { Amount, MeltChangeError, OutputData, Wallet, decodePaymentRequest, deserializeSwapPreview, getEncodedToken, getTokenMetadata, isMintOperationError, serializeSwapPreview } from "@cashu/cashu-ts";
|
|
25
24
|
import { decode, noteEncode, npubEncode } from "nostr-tools/nip19";
|
|
26
25
|
/** Evidence older than this says nothing about the clock now. */
|
|
27
26
|
var CLOCK_EVIDENCE_MS = 18e5;
|
|
@@ -199,7 +198,7 @@ function viewNews(before, after) {
|
|
|
199
198
|
//#endregion
|
|
200
199
|
//#region ../browser/src/engine/paymentAdapters/usdtWallet.ts
|
|
201
200
|
/** The Ethereum libraries, a large part of the app: loaded once a USDT wallet is made, opened or restored, not with the engine. */
|
|
202
|
-
var loadUsdt = () => import("./usdt-
|
|
201
|
+
var loadUsdt = () => import("./usdt-B7JWDhQV.js");
|
|
203
202
|
/** Every new profile starts with this wallet: an address to receive on, nothing to set up. */
|
|
204
203
|
var DEFAULT_USDT = {
|
|
205
204
|
network: "ethereum",
|
|
@@ -648,7 +647,7 @@ async function restoreArkDatabase(walletId, snapshot) {
|
|
|
648
647
|
//#endregion
|
|
649
648
|
//#region ../browser/src/engine/paymentAdapters/arkWallet.ts
|
|
650
649
|
/** The Ark SDK, a large part of the app: loaded once an Ark wallet is made, opened or restored, not with the engine. */
|
|
651
|
-
var loadArkade = () => import("./arkade-
|
|
650
|
+
var loadArkade = () => import("./arkade-CP4HvKaa.js");
|
|
652
651
|
/** How long a Testnet wallet about to make way for another waits for its server to say what it holds. */
|
|
653
652
|
var RETIRE_READ_MS = 1e4;
|
|
654
653
|
/** Every new profile starts with this wallet: nothing to set up before receiving. */
|
|
@@ -3556,7 +3555,7 @@ var CashuAdapter = class {
|
|
|
3556
3555
|
token = await this.wallet.executeReviewedCashu(review, prepared);
|
|
3557
3556
|
} catch (error) {
|
|
3558
3557
|
prepared.attemptEndedAt = Date.now();
|
|
3559
|
-
if (persist && isMintOperationError(error) && await this.wallet.reviewedCashuSpentElsewhere(prepared).catch(() => false)) return this.spentElsewhere(prepared, persist);
|
|
3558
|
+
if (persist && (await import("@cashu/cashu-ts")).isMintOperationError(error) && await this.wallet.reviewedCashuSpentElsewhere(prepared).catch(() => false)) return this.spentElsewhere(prepared, persist);
|
|
3560
3559
|
throw error;
|
|
3561
3560
|
}
|
|
3562
3561
|
await persist?.();
|
|
@@ -6703,7 +6702,7 @@ var registry;
|
|
|
6703
6702
|
var loading$1;
|
|
6704
6703
|
var listeners = /* @__PURE__ */ new Set();
|
|
6705
6704
|
function loadIdentityProviders() {
|
|
6706
|
-
return loading$1 ??= import("./registry-
|
|
6705
|
+
return loading$1 ??= import("./registry-CHPd_hf7.js").then((m) => {
|
|
6707
6706
|
registry = m;
|
|
6708
6707
|
for (const listener of listeners) listener();
|
|
6709
6708
|
return m;
|
|
@@ -7132,16 +7131,176 @@ var IdentityProofs = class {
|
|
|
7132
7131
|
}
|
|
7133
7132
|
};
|
|
7134
7133
|
//#endregion
|
|
7134
|
+
//#region ../browser/src/engine/x401.ts
|
|
7135
|
+
var X401_KEY = "x401Credentials";
|
|
7136
|
+
var MAX_CREDENTIALS = 32;
|
|
7137
|
+
/** Keys waiting for an issuer: the oldest goes when one more is made. */
|
|
7138
|
+
var MAX_WAITING_KEYS = 8;
|
|
7139
|
+
var ASK_FOR_A_NEW_KEY = "ask the issuer to bind it to a key from x401 key new";
|
|
7140
|
+
var LABEL_CHARS = 80;
|
|
7141
|
+
var newId$1 = () => toBase64Url(crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(12)));
|
|
7142
|
+
/** The paths of an object's members, nested objects' own members included. */
|
|
7143
|
+
function paths(value, at = []) {
|
|
7144
|
+
return Object.entries(value).flatMap(([key, item]) => {
|
|
7145
|
+
const path = [...at, key];
|
|
7146
|
+
return typeof item === "object" && item !== null && !Array.isArray(item) ? [path, ...paths(item, path)] : [path];
|
|
7147
|
+
});
|
|
7148
|
+
}
|
|
7149
|
+
/** The record's retired keys with `more` after them: the latest `MAX_RETIRED_KEYS`. */
|
|
7150
|
+
var retire = (stored, more) => [...stored.retired ?? [], ...more].slice(-32);
|
|
7151
|
+
/** Why a credential's key is not one waiting for it: what became of it, when this profile made it. */
|
|
7152
|
+
function notWaiting(stored, holder) {
|
|
7153
|
+
const held = stored.credentials.find((c) => sameX401HolderJwk(c.holderKey.jwk, holder));
|
|
7154
|
+
if (held) return `This credential's key already holds credential ${held.id} (${held.label}): a key takes one credential`;
|
|
7155
|
+
const retired = [...stored.retired ?? []].reverse().find((r) => sameX401HolderJwk(r.jwk, holder));
|
|
7156
|
+
if (retired?.credential) return `This credential's key went with credential ${retired.credential} when it was removed`;
|
|
7157
|
+
if (retired) return `This credential's key was dropped: a profile keeps the ${MAX_WAITING_KEYS} newest keys waiting for a credential`;
|
|
7158
|
+
return "This credential is bound to a key this profile did not make";
|
|
7159
|
+
}
|
|
7160
|
+
/**
|
|
7161
|
+
* x401 credentials (WISP 3xx), engine side: the profile's holder keys and the credentials issuers bound to them, in
|
|
7162
|
+
* the settings store beside the identity proofs. Read from the store on each call: nothing of it stays in memory.
|
|
7163
|
+
* Nothing here is shared with a contact, and no presentation is made here yet.
|
|
7164
|
+
*/
|
|
7165
|
+
var X401Credentials = class {
|
|
7166
|
+
/** Changes one after another: each reads the record and writes it back, and two at once would lose one. */
|
|
7167
|
+
queue = Promise.resolve();
|
|
7168
|
+
serial(change) {
|
|
7169
|
+
const next = this.queue.then(change, change);
|
|
7170
|
+
this.queue = next.catch(() => {});
|
|
7171
|
+
return next;
|
|
7172
|
+
}
|
|
7173
|
+
async load() {
|
|
7174
|
+
return await wrap((await store(STORES$1.settings, "readonly")).get("x401Credentials")) ?? {
|
|
7175
|
+
keys: [],
|
|
7176
|
+
credentials: []
|
|
7177
|
+
};
|
|
7178
|
+
}
|
|
7179
|
+
async save(value) {
|
|
7180
|
+
await transact([STORES$1.settings], (stores) => {
|
|
7181
|
+
stores[STORES$1.settings].put(value, X401_KEY);
|
|
7182
|
+
});
|
|
7183
|
+
}
|
|
7184
|
+
/**
|
|
7185
|
+
* A fresh P-256 holder key, kept until a credential bound to it is added. Its public JWK goes to the issuer. A profile
|
|
7186
|
+
* keeps the 8 newest keys waiting: past them, the oldest is dropped, and `dropped` names it.
|
|
7187
|
+
*/
|
|
7188
|
+
async newKey() {
|
|
7189
|
+
const { secret, jwk } = newX401HolderKey();
|
|
7190
|
+
const deviceKey = newDeviceKey();
|
|
7191
|
+
const key = {
|
|
7192
|
+
id: newId$1(),
|
|
7193
|
+
jwk,
|
|
7194
|
+
secret: {
|
|
7195
|
+
sealed: await sealSeed(toBase64Url(secret), deviceKey),
|
|
7196
|
+
deviceKey
|
|
7197
|
+
},
|
|
7198
|
+
createdAt: Date.now()
|
|
7199
|
+
};
|
|
7200
|
+
secret.fill(0);
|
|
7201
|
+
return this.serial(async () => {
|
|
7202
|
+
const stored = await this.load();
|
|
7203
|
+
const keys = [...stored.keys, key];
|
|
7204
|
+
const dropped = keys.slice(0, -8);
|
|
7205
|
+
await this.save({
|
|
7206
|
+
...stored,
|
|
7207
|
+
keys: keys.slice(-8),
|
|
7208
|
+
retired: retire(stored, dropped.map((k) => ({ jwk: k.jwk })))
|
|
7209
|
+
});
|
|
7210
|
+
return {
|
|
7211
|
+
id: key.id,
|
|
7212
|
+
jwk,
|
|
7213
|
+
...dropped.length ? { dropped: dropped[0].id } : {}
|
|
7214
|
+
};
|
|
7215
|
+
});
|
|
7216
|
+
}
|
|
7217
|
+
/**
|
|
7218
|
+
* Adds an issued credential bound to one of the keys `newKey` made. Refused: a credential that does not read, one
|
|
7219
|
+
* that has expired, one bound to another key, one already kept, a label past `LABEL_CHARS` (it was cut without a
|
|
7220
|
+
* word). A label is one line: its line breaks and tabs read as one space, as a group's name.
|
|
7221
|
+
*/
|
|
7222
|
+
add({ credential, label: given }) {
|
|
7223
|
+
const label = given?.replace(/\s+/g, " ").trim();
|
|
7224
|
+
if (label && ([...label].length > LABEL_CHARS || /\p{Cc}/u.test(label))) return Promise.reject(/* @__PURE__ */ new Error(`A credential's label is up to ${LABEL_CHARS} characters on one line`));
|
|
7225
|
+
const read = readX401SdJwtVc(credential);
|
|
7226
|
+
if (!read.ok) return Promise.reject(/* @__PURE__ */ new Error(`Not an SD-JWT VC Ghostly can keep: ${read.reason}`));
|
|
7227
|
+
const vc = read.value;
|
|
7228
|
+
if (vc.expiresAt !== void 0 && vc.expiresAt * 1e3 <= Date.now()) return Promise.reject(/* @__PURE__ */ new Error(`This credential expired on ${(/* @__PURE__ */ new Date(vc.expiresAt * 1e3)).toISOString()}`));
|
|
7229
|
+
return this.serial(() => this.keep(credential.trim(), vc, label));
|
|
7230
|
+
}
|
|
7231
|
+
async keep(credential, vc, label) {
|
|
7232
|
+
const stored = await this.load();
|
|
7233
|
+
if (stored.credentials.some((c) => c.credential === credential)) throw new Error("This credential is kept already");
|
|
7234
|
+
const holderKey = stored.keys.find((k) => sameX401HolderJwk(k.jwk, vc.holder));
|
|
7235
|
+
if (!holderKey) throw new Error(`${notWaiting(stored, vc.holder)}: ${ASK_FOR_A_NEW_KEY}`);
|
|
7236
|
+
if (stored.credentials.length >= MAX_CREDENTIALS) throw new Error(`A profile keeps at most ${MAX_CREDENTIALS} credentials: remove one first`);
|
|
7237
|
+
const kept = {
|
|
7238
|
+
id: newId$1(),
|
|
7239
|
+
label: label || vc.type.slice(0, LABEL_CHARS),
|
|
7240
|
+
format: vc.format,
|
|
7241
|
+
credential,
|
|
7242
|
+
type: vc.type,
|
|
7243
|
+
issuer: vc.issuer,
|
|
7244
|
+
holderKey,
|
|
7245
|
+
addedAt: Date.now(),
|
|
7246
|
+
...vc.expiresAt !== void 0 ? { expiresAt: vc.expiresAt * 1e3 } : {}
|
|
7247
|
+
};
|
|
7248
|
+
await this.save({
|
|
7249
|
+
...stored,
|
|
7250
|
+
keys: stored.keys.filter((k) => k !== holderKey),
|
|
7251
|
+
credentials: [...stored.credentials, kept]
|
|
7252
|
+
});
|
|
7253
|
+
return this.viewOf(kept, false);
|
|
7254
|
+
}
|
|
7255
|
+
async list({ values = false } = {}) {
|
|
7256
|
+
return (await this.load()).credentials.map((c) => this.viewOf(c, values));
|
|
7257
|
+
}
|
|
7258
|
+
/** Removes a credential and its holder key. False when the profile has no credential with that id. */
|
|
7259
|
+
remove({ id }) {
|
|
7260
|
+
return this.serial(async () => {
|
|
7261
|
+
const stored = await this.load();
|
|
7262
|
+
const gone = stored.credentials.find((c) => c.id === id);
|
|
7263
|
+
if (!gone) return false;
|
|
7264
|
+
await this.save({
|
|
7265
|
+
...stored,
|
|
7266
|
+
credentials: stored.credentials.filter((c) => c !== gone),
|
|
7267
|
+
retired: retire(stored, [{
|
|
7268
|
+
jwk: gone.holderKey.jwk,
|
|
7269
|
+
credential: id
|
|
7270
|
+
}])
|
|
7271
|
+
});
|
|
7272
|
+
return true;
|
|
7273
|
+
});
|
|
7274
|
+
}
|
|
7275
|
+
viewOf(c, values) {
|
|
7276
|
+
const read = readX401SdJwtVc(c.credential);
|
|
7277
|
+
const claims = read.ok ? read.value.claims : {};
|
|
7278
|
+
return {
|
|
7279
|
+
id: c.id,
|
|
7280
|
+
label: c.label,
|
|
7281
|
+
format: c.format,
|
|
7282
|
+
type: c.type,
|
|
7283
|
+
issuer: c.issuer,
|
|
7284
|
+
addedAt: c.addedAt,
|
|
7285
|
+
expiresAt: c.expiresAt ?? null,
|
|
7286
|
+
notBefore: read.ok && read.value.notBefore !== void 0 ? read.value.notBefore * 1e3 : null,
|
|
7287
|
+
claims: paths(claims),
|
|
7288
|
+
disclosable: read.ok ? read.value.disclosable : [],
|
|
7289
|
+
...values ? { values: claims } : {}
|
|
7290
|
+
};
|
|
7291
|
+
}
|
|
7292
|
+
};
|
|
7293
|
+
//#endregion
|
|
7135
7294
|
//#region ../browser/src/engine/did.ts
|
|
7136
7295
|
var KEY$1 = "profileDid";
|
|
7137
7296
|
/** Mainline keeps a mutable item about two hours: the same packet goes out again every hour. */
|
|
7138
7297
|
var DID_REPUBLISH_MS = 36e5;
|
|
7139
7298
|
/** After start, once the chats had their first go at the relays. */
|
|
7140
|
-
var FIRST_PUBLISH_MS = 15e3;
|
|
7299
|
+
var FIRST_PUBLISH_MS$1 = 15e3;
|
|
7141
7300
|
/** A few switches flipped in a row make one publish. */
|
|
7142
7301
|
var CHANGE_DELAY_MS = 2e3;
|
|
7143
7302
|
/** A failed publish is tried again this much later (not before the hourly one otherwise). */
|
|
7144
|
-
var RETRY_MS$
|
|
7303
|
+
var RETRY_MS$2 = 3e5;
|
|
7145
7304
|
/** Relay payload header: signature and sequence number, before the DNS packet. */
|
|
7146
7305
|
var HEADER = 72;
|
|
7147
7306
|
var ProfileDid = class {
|
|
@@ -7179,7 +7338,7 @@ var ProfileDid = class {
|
|
|
7179
7338
|
}
|
|
7180
7339
|
start() {
|
|
7181
7340
|
this.running = true;
|
|
7182
|
-
this.schedule(FIRST_PUBLISH_MS);
|
|
7341
|
+
this.schedule(FIRST_PUBLISH_MS$1);
|
|
7183
7342
|
}
|
|
7184
7343
|
stop() {
|
|
7185
7344
|
this.running = false;
|
|
@@ -7265,7 +7424,7 @@ var ProfileDid = class {
|
|
|
7265
7424
|
this.timer = setTimeout(() => {
|
|
7266
7425
|
this.timer = null;
|
|
7267
7426
|
this.publishNow().finally(() => {
|
|
7268
|
-
if (this.running && !this.timer) this.schedule(this.error ? RETRY_MS$
|
|
7427
|
+
if (this.running && !this.timer) this.schedule(this.error ? RETRY_MS$2 : DID_REPUBLISH_MS);
|
|
7269
7428
|
});
|
|
7270
7429
|
}, ms);
|
|
7271
7430
|
}
|
|
@@ -7346,6 +7505,558 @@ var ProfileDid = class {
|
|
|
7346
7505
|
this.stored = next;
|
|
7347
7506
|
}
|
|
7348
7507
|
};
|
|
7508
|
+
//#endregion
|
|
7509
|
+
//#region ../browser/src/engine/contactLinks.ts
|
|
7510
|
+
var CONTACT_LINKS_KEY = "contactLinks";
|
|
7511
|
+
/** Mainline keeps a mutable item about two hours: the link record goes out again every hour. */
|
|
7512
|
+
var LINK_REPUBLISH_MS = 36e5;
|
|
7513
|
+
var OFF_FOR_MS = 2592e6;
|
|
7514
|
+
var FIRST_PUBLISH_MS = 15e3;
|
|
7515
|
+
var RETRY_MS$1 = 3e5;
|
|
7516
|
+
/** How often the knock slots are read: every five minutes, and every 15 s for ten minutes after the link or Requests was shown. */
|
|
7517
|
+
var POLL_MS = 3e5;
|
|
7518
|
+
var POLL_SHOWN_MS = 15e3;
|
|
7519
|
+
var SHOWN_FOR_MS = 6e5;
|
|
7520
|
+
var ContactLinks = class {
|
|
7521
|
+
host;
|
|
7522
|
+
stored = { retired: [] };
|
|
7523
|
+
error;
|
|
7524
|
+
timer = null;
|
|
7525
|
+
running = false;
|
|
7526
|
+
inFlight = null;
|
|
7527
|
+
again = false;
|
|
7528
|
+
writes = Promise.resolve();
|
|
7529
|
+
pollTimer = null;
|
|
7530
|
+
shownAt = -Infinity;
|
|
7531
|
+
/** Making, turning off, taking and settling requests run one at a time: each reads and writes the whole record. */
|
|
7532
|
+
queue = Promise.resolve();
|
|
7533
|
+
constructor(host) {
|
|
7534
|
+
this.host = host;
|
|
7535
|
+
}
|
|
7536
|
+
async load() {
|
|
7537
|
+
const found = await wrap((await store(STORES$1.settings, "readonly")).get(CONTACT_LINKS_KEY));
|
|
7538
|
+
this.stored = found ?? { retired: [] };
|
|
7539
|
+
}
|
|
7540
|
+
start() {
|
|
7541
|
+
this.running = true;
|
|
7542
|
+
this.schedule(FIRST_PUBLISH_MS);
|
|
7543
|
+
this.schedulePoll(FIRST_PUBLISH_MS);
|
|
7544
|
+
}
|
|
7545
|
+
stop() {
|
|
7546
|
+
this.running = false;
|
|
7547
|
+
if (this.timer) clearTimeout(this.timer);
|
|
7548
|
+
this.timer = null;
|
|
7549
|
+
if (this.pollTimer) clearTimeout(this.pollTimer);
|
|
7550
|
+
this.pollTimer = null;
|
|
7551
|
+
}
|
|
7552
|
+
/**
|
|
7553
|
+
* The link was shown or copied, or Requests opened: people open a link in the minutes after it is shared, and the
|
|
7554
|
+
* owner looking at Requests wants what came. Look now, and more often for ten minutes.
|
|
7555
|
+
*/
|
|
7556
|
+
shown() {
|
|
7557
|
+
this.shownAt = Date.now();
|
|
7558
|
+
this.schedulePoll(0);
|
|
7559
|
+
}
|
|
7560
|
+
/** Whether a link key is one of this profile's, current or turned off. */
|
|
7561
|
+
owns(linkKeyZ32) {
|
|
7562
|
+
return this.stored.current?.linkKeyZ32 === linkKeyZ32 || this.stored.retired.some((link) => link.linkKeyZ32 === linkKeyZ32);
|
|
7563
|
+
}
|
|
7564
|
+
/** The requests waiting, newest first. */
|
|
7565
|
+
requests() {
|
|
7566
|
+
return [...this.stored.current?.requests ?? []].reverse().map((r) => ({
|
|
7567
|
+
id: r.key,
|
|
7568
|
+
name: r.name,
|
|
7569
|
+
text: r.text,
|
|
7570
|
+
receivedAt: r.receivedAt,
|
|
7571
|
+
expires: r.expires
|
|
7572
|
+
}));
|
|
7573
|
+
}
|
|
7574
|
+
/**
|
|
7575
|
+
* Accept: the owner joins the invite the request carries, and the chat starts as any chat does. The request leaves
|
|
7576
|
+
* Requests and its asker key is never read again on this link.
|
|
7577
|
+
*/
|
|
7578
|
+
accept(id) {
|
|
7579
|
+
return this.serial(async () => {
|
|
7580
|
+
const request = this.stored.current?.requests?.find((r) => r.key === id);
|
|
7581
|
+
if (!request) throw new Error("This request is no longer waiting");
|
|
7582
|
+
if (request.expires <= Date.now()) throw new Error("This request expired");
|
|
7583
|
+
const linkId = await this.host.accept(request);
|
|
7584
|
+
await this.settle(id);
|
|
7585
|
+
return { linkId };
|
|
7586
|
+
});
|
|
7587
|
+
}
|
|
7588
|
+
/** Decline: nothing is said back. The request's name and text are deleted, and its asker key is never read again. */
|
|
7589
|
+
decline(id) {
|
|
7590
|
+
return this.serial(() => this.settle(id));
|
|
7591
|
+
}
|
|
7592
|
+
/** Reads the knock slots once and takes what is new into Requests. */
|
|
7593
|
+
poll() {
|
|
7594
|
+
return this.serial(() => this.take());
|
|
7595
|
+
}
|
|
7596
|
+
/** Undefined while the profile has no link. */
|
|
7597
|
+
view() {
|
|
7598
|
+
const current = this.stored.current;
|
|
7599
|
+
if (!current) return void 0;
|
|
7600
|
+
const code = encodeContactLink(current.linkKeyZ32);
|
|
7601
|
+
return {
|
|
7602
|
+
code,
|
|
7603
|
+
link: inviteLink(code),
|
|
7604
|
+
createdAt: current.createdAt,
|
|
7605
|
+
state: current.inbox.state === "busy" ? "busy" : "open",
|
|
7606
|
+
waiting: current.inbox.waiting.length,
|
|
7607
|
+
missed: current.inbox.missed.length,
|
|
7608
|
+
...current.published ? { publishedAt: current.published } : {},
|
|
7609
|
+
...this.error ? { error: this.error } : {}
|
|
7610
|
+
};
|
|
7611
|
+
}
|
|
7612
|
+
/** Makes the profile's link, turning the one before off: one link per profile at a time. Requests keeps what waits. */
|
|
7613
|
+
create() {
|
|
7614
|
+
return this.serial(() => this.make());
|
|
7615
|
+
}
|
|
7616
|
+
async make() {
|
|
7617
|
+
const made = createContactLink();
|
|
7618
|
+
const deviceKey = newDeviceKey();
|
|
7619
|
+
const now = Date.now();
|
|
7620
|
+
const retired = this.retire(now);
|
|
7621
|
+
const waiting = this.stored.current?.inbox.waiting.filter((w) => w.expires > now) ?? [];
|
|
7622
|
+
const requests = this.stored.current?.requests?.filter((r) => waiting.some((w) => w.key === r.key)) ?? [];
|
|
7623
|
+
const inbox = {
|
|
7624
|
+
...newInbox(now),
|
|
7625
|
+
waiting,
|
|
7626
|
+
state: waiting.length >= 32 ? "busy" : "open"
|
|
7627
|
+
};
|
|
7628
|
+
await this.save({
|
|
7629
|
+
current: {
|
|
7630
|
+
seed: {
|
|
7631
|
+
sealed: await sealSeed(made.seedB64, deviceKey),
|
|
7632
|
+
deviceKey
|
|
7633
|
+
},
|
|
7634
|
+
linkKeyZ32: made.linkKeyZ32,
|
|
7635
|
+
createdAt: now,
|
|
7636
|
+
inbox,
|
|
7637
|
+
...requests.length ? { requests } : {}
|
|
7638
|
+
},
|
|
7639
|
+
retired
|
|
7640
|
+
});
|
|
7641
|
+
this.host.emit();
|
|
7642
|
+
await this.publishNow();
|
|
7643
|
+
return this.view();
|
|
7644
|
+
}
|
|
7645
|
+
/** Turns the link off, for good: requesters who open it are told so. */
|
|
7646
|
+
turnOff() {
|
|
7647
|
+
return this.serial(() => this.off());
|
|
7648
|
+
}
|
|
7649
|
+
async off() {
|
|
7650
|
+
if (!this.stored.current) return;
|
|
7651
|
+
await this.save({ retired: this.retire(Date.now()) });
|
|
7652
|
+
this.host.emit();
|
|
7653
|
+
await this.publishNow();
|
|
7654
|
+
}
|
|
7655
|
+
/** "Clear all data": the link says `off` once, now, and nothing is kept. */
|
|
7656
|
+
async withdraw() {
|
|
7657
|
+
this.stop();
|
|
7658
|
+
const links = [...this.stored.current ? [this.stored.current] : [], ...this.stored.retired];
|
|
7659
|
+
await Promise.allSettled(links.map(async (link) => this.host.publish(await this.identity(link), linkRecords(inboxRecord(turnOff(newInbox(Date.now())), Date.now())))));
|
|
7660
|
+
}
|
|
7661
|
+
/** Publishes what is due now: the current link's record, and `off` for each retired link not said today. */
|
|
7662
|
+
async publishNow() {
|
|
7663
|
+
if (this.inFlight) {
|
|
7664
|
+
this.again = true;
|
|
7665
|
+
return this.inFlight;
|
|
7666
|
+
}
|
|
7667
|
+
this.inFlight = this.publish().finally(() => {
|
|
7668
|
+
this.inFlight = null;
|
|
7669
|
+
});
|
|
7670
|
+
await this.inFlight;
|
|
7671
|
+
if (this.again) {
|
|
7672
|
+
this.again = false;
|
|
7673
|
+
await this.publishNow();
|
|
7674
|
+
}
|
|
7675
|
+
}
|
|
7676
|
+
serial(work) {
|
|
7677
|
+
const run = this.queue.then(work, work);
|
|
7678
|
+
this.queue = run.catch(() => {});
|
|
7679
|
+
return run;
|
|
7680
|
+
}
|
|
7681
|
+
schedulePoll(ms) {
|
|
7682
|
+
if (!this.running) return;
|
|
7683
|
+
if (this.pollTimer) clearTimeout(this.pollTimer);
|
|
7684
|
+
this.pollTimer = setTimeout(() => {
|
|
7685
|
+
this.pollTimer = null;
|
|
7686
|
+
this.poll().catch(() => {}).finally(() => {
|
|
7687
|
+
if (!this.running || this.pollTimer) return;
|
|
7688
|
+
const shown = Date.now() - this.shownAt < SHOWN_FOR_MS;
|
|
7689
|
+
this.schedulePoll(shown ? POLL_SHOWN_MS : POLL_MS);
|
|
7690
|
+
});
|
|
7691
|
+
}, ms);
|
|
7692
|
+
}
|
|
7693
|
+
async take() {
|
|
7694
|
+
const current = this.stored.current;
|
|
7695
|
+
if (!current || !this.host.online()) return;
|
|
7696
|
+
const now = Date.now();
|
|
7697
|
+
const linkKey = current.linkKeyZ32;
|
|
7698
|
+
const slots = await Promise.all(Array.from({ length: 4 }, (_, i) => this.host.resolve(knockSlotIdentity(linkKey, i).pubKeyZ32).catch(() => null)));
|
|
7699
|
+
let inbox = current.inbox;
|
|
7700
|
+
for (const key of endedRequests(inbox, now)) inbox = settleRequest(inbox, key, "ended");
|
|
7701
|
+
const plan = planKnocks(linkKey, inbox, slots.flatMap((records) => records ? readKnockSlot(linkKey, records) : []), now);
|
|
7702
|
+
inbox = plan.inbox;
|
|
7703
|
+
const taken = [];
|
|
7704
|
+
if (plan.read.length) {
|
|
7705
|
+
const { seedB64 } = await this.identity(current);
|
|
7706
|
+
for (const key of plan.read) {
|
|
7707
|
+
const records = await this.host.resolve(key).catch(() => null);
|
|
7708
|
+
const request = records ? openRequest(seedB64, key, records, now) : null;
|
|
7709
|
+
if (!request || this.host.ownInvite(request.invite)) continue;
|
|
7710
|
+
inbox = takeRequest(linkKey, inbox, key, request.expires, now);
|
|
7711
|
+
taken.push({
|
|
7712
|
+
...request,
|
|
7713
|
+
key,
|
|
7714
|
+
receivedAt: now
|
|
7715
|
+
});
|
|
7716
|
+
}
|
|
7717
|
+
}
|
|
7718
|
+
const latest = this.stored.current;
|
|
7719
|
+
if (!latest || latest.linkKeyZ32 !== linkKey) return;
|
|
7720
|
+
const requests = [...current.requests ?? [], ...taken].filter((r) => inbox.waiting.some((w) => w.key === r.key));
|
|
7721
|
+
const changed = plan.publish || taken.length > 0 || inbox.state !== current.inbox.state || inbox.bits !== current.inbox.bits;
|
|
7722
|
+
await this.save({
|
|
7723
|
+
...this.stored,
|
|
7724
|
+
current: {
|
|
7725
|
+
...latest,
|
|
7726
|
+
inbox,
|
|
7727
|
+
requests
|
|
7728
|
+
}
|
|
7729
|
+
});
|
|
7730
|
+
this.host.emit();
|
|
7731
|
+
if (changed) await this.publishNow();
|
|
7732
|
+
}
|
|
7733
|
+
async settle(key) {
|
|
7734
|
+
const current = this.stored.current;
|
|
7735
|
+
if (!current?.requests?.some((r) => r.key === key)) return;
|
|
7736
|
+
const inbox = clearMissed(settleRequest(current.inbox, key));
|
|
7737
|
+
await this.save({
|
|
7738
|
+
...this.stored,
|
|
7739
|
+
current: {
|
|
7740
|
+
...current,
|
|
7741
|
+
inbox,
|
|
7742
|
+
requests: current.requests.filter((r) => r.key !== key)
|
|
7743
|
+
}
|
|
7744
|
+
});
|
|
7745
|
+
this.host.emit();
|
|
7746
|
+
if (inbox.state !== current.inbox.state) await this.publishNow();
|
|
7747
|
+
}
|
|
7748
|
+
retire(now) {
|
|
7749
|
+
const kept = this.stored.retired.filter((link) => now - link.offAt < OFF_FOR_MS);
|
|
7750
|
+
const current = this.stored.current;
|
|
7751
|
+
return current ? [...kept, {
|
|
7752
|
+
seed: current.seed,
|
|
7753
|
+
linkKeyZ32: current.linkKeyZ32,
|
|
7754
|
+
createdAt: current.createdAt,
|
|
7755
|
+
offAt: now
|
|
7756
|
+
}] : kept;
|
|
7757
|
+
}
|
|
7758
|
+
schedule(ms) {
|
|
7759
|
+
if (!this.running) return;
|
|
7760
|
+
if (this.timer) clearTimeout(this.timer);
|
|
7761
|
+
this.timer = setTimeout(() => {
|
|
7762
|
+
this.timer = null;
|
|
7763
|
+
this.publishNow().finally(() => {
|
|
7764
|
+
if (this.running && !this.timer) this.schedule(this.error ? RETRY_MS$1 : LINK_REPUBLISH_MS);
|
|
7765
|
+
});
|
|
7766
|
+
}, ms);
|
|
7767
|
+
}
|
|
7768
|
+
async publish() {
|
|
7769
|
+
if (!this.host.online()) return;
|
|
7770
|
+
const now = Date.now();
|
|
7771
|
+
const { current, retired } = this.stored;
|
|
7772
|
+
let published = false;
|
|
7773
|
+
const said = /* @__PURE__ */ new Set();
|
|
7774
|
+
this.error = void 0;
|
|
7775
|
+
if (current) try {
|
|
7776
|
+
await this.host.publish(await this.identity(current), linkRecords(inboxRecord(current.inbox, now)));
|
|
7777
|
+
published = true;
|
|
7778
|
+
} catch (error) {
|
|
7779
|
+
this.error = error instanceof Error ? error.message : String(error);
|
|
7780
|
+
}
|
|
7781
|
+
for (const link of retired) {
|
|
7782
|
+
if (now - link.offAt >= 2592e6 || link.said !== void 0 && now - link.said < 864e5) continue;
|
|
7783
|
+
try {
|
|
7784
|
+
await this.host.publish(await this.identity(link), linkRecords(inboxRecord(turnOff(newInbox(now)), now)));
|
|
7785
|
+
said.add(link.linkKeyZ32);
|
|
7786
|
+
} catch {}
|
|
7787
|
+
}
|
|
7788
|
+
const latest = this.stored;
|
|
7789
|
+
await this.save({
|
|
7790
|
+
...latest.current ? { current: published && latest.current.linkKeyZ32 === current?.linkKeyZ32 ? {
|
|
7791
|
+
...latest.current,
|
|
7792
|
+
published: now
|
|
7793
|
+
} : latest.current } : {},
|
|
7794
|
+
retired: latest.retired.filter((link) => now - link.offAt < OFF_FOR_MS).map((link) => said.has(link.linkKeyZ32) ? {
|
|
7795
|
+
...link,
|
|
7796
|
+
said: now
|
|
7797
|
+
} : link)
|
|
7798
|
+
});
|
|
7799
|
+
this.host.emit();
|
|
7800
|
+
}
|
|
7801
|
+
async identity(link) {
|
|
7802
|
+
return identityFromSeedB64(await unsealSeed(link.seed.sealed, link.seed.deviceKey));
|
|
7803
|
+
}
|
|
7804
|
+
/**
|
|
7805
|
+
* The record is the one in memory from now on, and the writes go to the database in the order they were made: a
|
|
7806
|
+
* publish that ends while a request is being taken can never put back the record from before it.
|
|
7807
|
+
*/
|
|
7808
|
+
save(next) {
|
|
7809
|
+
this.stored = next;
|
|
7810
|
+
const write = this.writes.then(async () => {
|
|
7811
|
+
await wrap((await store(STORES$1.settings, "readwrite")).put(next, CONTACT_LINKS_KEY));
|
|
7812
|
+
});
|
|
7813
|
+
this.writes = write.catch(() => {});
|
|
7814
|
+
return write;
|
|
7815
|
+
}
|
|
7816
|
+
};
|
|
7817
|
+
//#endregion
|
|
7818
|
+
//#region ../browser/src/engine/contactAsks.ts
|
|
7819
|
+
var CONTACT_ASKS_KEY = "contactAsks";
|
|
7820
|
+
/** How often a request not yet seen is looked after: every 30 s for ten minutes, then every ten minutes. */
|
|
7821
|
+
var ASK_FAST_MS = 3e4;
|
|
7822
|
+
var ASK_SLOW_MS = 6e5;
|
|
7823
|
+
/** After a knock, its slot is read again this much later, twice at most: another requester may have written at once. */
|
|
7824
|
+
var KNOCK_CHECK_MS = 1e3;
|
|
7825
|
+
var SLACK_MS = 5e3;
|
|
7826
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
7827
|
+
var ContactAsks = class {
|
|
7828
|
+
host;
|
|
7829
|
+
asks = [];
|
|
7830
|
+
timer = null;
|
|
7831
|
+
running = false;
|
|
7832
|
+
queue = Promise.resolve();
|
|
7833
|
+
constructor(host) {
|
|
7834
|
+
this.host = host;
|
|
7835
|
+
}
|
|
7836
|
+
async load() {
|
|
7837
|
+
this.asks = await wrap((await store(STORES$1.settings, "readonly")).get("contactAsks")) ?? [];
|
|
7838
|
+
}
|
|
7839
|
+
start() {
|
|
7840
|
+
this.running = true;
|
|
7841
|
+
this.schedule(ASK_FAST_MS);
|
|
7842
|
+
}
|
|
7843
|
+
stop() {
|
|
7844
|
+
this.running = false;
|
|
7845
|
+
if (this.timer) clearTimeout(this.timer);
|
|
7846
|
+
this.timer = null;
|
|
7847
|
+
}
|
|
7848
|
+
views() {
|
|
7849
|
+
return this.asks.filter((a) => a.chatId && a.state).map((a) => ({
|
|
7850
|
+
chatId: a.chatId,
|
|
7851
|
+
state: a.state,
|
|
7852
|
+
expires: a.expires
|
|
7853
|
+
}));
|
|
7854
|
+
}
|
|
7855
|
+
/**
|
|
7856
|
+
* Sends a request through a contact link. Refused, with the words the person reads, for a code that is not a contact
|
|
7857
|
+
* link, the profile's own link, a link turned off or busy, a request already waiting there, or a name or text that
|
|
7858
|
+
* breaks a bound. Nothing is made before every check passed.
|
|
7859
|
+
*/
|
|
7860
|
+
ask(params) {
|
|
7861
|
+
return this.serial(() => this.send(params));
|
|
7862
|
+
}
|
|
7863
|
+
/**
|
|
7864
|
+
* The chat a request made was deleted: the request is taken back. Nothing more goes out for it, its record under the
|
|
7865
|
+
* asker key is replaced by an empty one so an owner who has not read it yet finds nothing, and asking again through
|
|
7866
|
+
* the link is open. The asker key stays, one per profile and link. An owner who already took it keeps it until it ends.
|
|
7867
|
+
*/
|
|
7868
|
+
forgetChat(chatId) {
|
|
7869
|
+
return this.serial(async () => {
|
|
7870
|
+
const ask = this.asks.find((a) => a.chatId === chatId);
|
|
7871
|
+
if (!ask) return;
|
|
7872
|
+
const out = ask.state === "sending" || ask.state === "knocking" || ask.state === "seen";
|
|
7873
|
+
await this.put({
|
|
7874
|
+
link: ask.link,
|
|
7875
|
+
seed: ask.seed,
|
|
7876
|
+
key: ask.key
|
|
7877
|
+
});
|
|
7878
|
+
this.host.emit();
|
|
7879
|
+
if (!out || !this.host.online()) return;
|
|
7880
|
+
try {
|
|
7881
|
+
await this.host.publish(await this.identity(ask), []);
|
|
7882
|
+
} catch {}
|
|
7883
|
+
});
|
|
7884
|
+
}
|
|
7885
|
+
/** Looks after every request not yet seen once (the timer does this at its pace). */
|
|
7886
|
+
tick() {
|
|
7887
|
+
return this.serial(() => this.look());
|
|
7888
|
+
}
|
|
7889
|
+
async send({ code, name, text, label }) {
|
|
7890
|
+
const reading = readContactLink(code);
|
|
7891
|
+
if (!reading.ok) throw new Error(reading.reason === "chat" ? "This is a chat invite: open it in Join." : "This is not a Ghostly contact link.");
|
|
7892
|
+
const link = reading.linkKeyZ32;
|
|
7893
|
+
if (this.host.ownLink(link)) throw new Error("This is your contact link.");
|
|
7894
|
+
const now = Date.now();
|
|
7895
|
+
const known = this.asks.find((a) => a.link === link);
|
|
7896
|
+
if (known?.state && known.state !== "ended" && known.state !== "off" && (known.expires ?? 0) > now) throw new Error("You already sent a request through this link.");
|
|
7897
|
+
const record = readLinkRecord(await this.host.resolve(link) ?? []);
|
|
7898
|
+
if (record?.state === "off") throw new Error("This contact link was turned off.");
|
|
7899
|
+
if (record?.state === "busy") throw new Error("They are not taking requests right now. Try again later.");
|
|
7900
|
+
const asker = known ? await this.identity(known) : createIdentity();
|
|
7901
|
+
const request = (invite) => ({
|
|
7902
|
+
invite,
|
|
7903
|
+
name: name.trim(),
|
|
7904
|
+
text: text.trim(),
|
|
7905
|
+
issued: now,
|
|
7906
|
+
expires: now + REQUEST_LIFETIME_MS
|
|
7907
|
+
});
|
|
7908
|
+
try {
|
|
7909
|
+
requestRecords(asker.seedB64, link, request(createChatInvite().inviteCode));
|
|
7910
|
+
} catch (error) {
|
|
7911
|
+
if (!(error instanceof ContactRequestError)) throw error;
|
|
7912
|
+
throw new Error(error.problem === "name" ? "Write a name of up to 48 characters." : error.problem === "too-long" ? "Shorten the message." : "Write a message of up to 200 bytes.");
|
|
7913
|
+
}
|
|
7914
|
+
const chat = await this.host.createChat({
|
|
7915
|
+
...label?.trim() ? { label: label.trim() } : {},
|
|
7916
|
+
text: text.trim()
|
|
7917
|
+
});
|
|
7918
|
+
const sent = request(chat.inviteCode);
|
|
7919
|
+
const ask = {
|
|
7920
|
+
link,
|
|
7921
|
+
key: asker.pubKeyZ32,
|
|
7922
|
+
seed: known?.seed ?? await this.seal(asker),
|
|
7923
|
+
chatId: chat.linkId,
|
|
7924
|
+
invite: chat.inviteCode,
|
|
7925
|
+
name: sent.name,
|
|
7926
|
+
text: sent.text,
|
|
7927
|
+
issued: now,
|
|
7928
|
+
expires: sent.expires,
|
|
7929
|
+
state: "sending"
|
|
7930
|
+
};
|
|
7931
|
+
await this.put(ask);
|
|
7932
|
+
this.tick().catch(() => {});
|
|
7933
|
+
this.schedule(ASK_FAST_MS);
|
|
7934
|
+
return { chatId: chat.linkId };
|
|
7935
|
+
}
|
|
7936
|
+
/** Publishes the request when due and knocks when its knock is gone. Never throws: the next look tries again. */
|
|
7937
|
+
async goOut(ask, asker, publish) {
|
|
7938
|
+
const now = Date.now();
|
|
7939
|
+
try {
|
|
7940
|
+
if (publish) {
|
|
7941
|
+
await this.host.publish(asker, requestRecords(asker.seedB64, ask.link, {
|
|
7942
|
+
invite: ask.invite,
|
|
7943
|
+
name: ask.name,
|
|
7944
|
+
text: ask.text,
|
|
7945
|
+
issued: ask.issued,
|
|
7946
|
+
expires: ask.expires
|
|
7947
|
+
}));
|
|
7948
|
+
ask = {
|
|
7949
|
+
...ask,
|
|
7950
|
+
publishedAt: now
|
|
7951
|
+
};
|
|
7952
|
+
}
|
|
7953
|
+
const slot = knockSlotIdentity(ask.link, knockSlotOf(ask.link, ask.key));
|
|
7954
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
7955
|
+
const knocks = readKnockSlot(ask.link, await this.host.resolve(slot.pubKeyZ32) ?? []);
|
|
7956
|
+
const stamp = ask.stamp;
|
|
7957
|
+
if (knocks.some((k) => k.key === ask.key && k.day === stamp.day && k.stamp === stamp.stamp)) break;
|
|
7958
|
+
if (attempt === 2) break;
|
|
7959
|
+
await this.host.publish(slot, knockSlotRecords(ask.link, mergeContactKnocks(knocks, {
|
|
7960
|
+
key: ask.key,
|
|
7961
|
+
day: stamp.day,
|
|
7962
|
+
stamp: stamp.stamp
|
|
7963
|
+
}, now)));
|
|
7964
|
+
await sleep(KNOCK_CHECK_MS);
|
|
7965
|
+
}
|
|
7966
|
+
ask = {
|
|
7967
|
+
...ask,
|
|
7968
|
+
state: ask.state === "sending" ? "knocking" : ask.state,
|
|
7969
|
+
lookedAt: now
|
|
7970
|
+
};
|
|
7971
|
+
} catch {
|
|
7972
|
+
ask = {
|
|
7973
|
+
...ask,
|
|
7974
|
+
lookedAt: now
|
|
7975
|
+
};
|
|
7976
|
+
}
|
|
7977
|
+
await this.put(ask);
|
|
7978
|
+
this.host.emit();
|
|
7979
|
+
}
|
|
7980
|
+
async look() {
|
|
7981
|
+
if (!this.host.online()) return;
|
|
7982
|
+
const now = Date.now();
|
|
7983
|
+
for (const ask of this.asks) {
|
|
7984
|
+
if (ask.state !== "sending" && ask.state !== "knocking" && ask.state !== "seen") continue;
|
|
7985
|
+
if (ask.expires <= now) {
|
|
7986
|
+
await this.put({
|
|
7987
|
+
...ask,
|
|
7988
|
+
state: "ended"
|
|
7989
|
+
});
|
|
7990
|
+
this.host.emit();
|
|
7991
|
+
continue;
|
|
7992
|
+
}
|
|
7993
|
+
if (ask.state === "seen") continue;
|
|
7994
|
+
const pace = now - ask.issued < 6e5 ? ASK_FAST_MS : ASK_SLOW_MS;
|
|
7995
|
+
if (ask.lookedAt !== void 0 && now - ask.lookedAt < pace - SLACK_MS) continue;
|
|
7996
|
+
const record = readLinkRecord(await this.host.resolve(ask.link).catch(() => null) ?? []);
|
|
7997
|
+
if (record?.state === "off") {
|
|
7998
|
+
await this.put({
|
|
7999
|
+
...ask,
|
|
8000
|
+
state: "off"
|
|
8001
|
+
});
|
|
8002
|
+
this.host.emit();
|
|
8003
|
+
continue;
|
|
8004
|
+
}
|
|
8005
|
+
if (record?.seen.includes(seenEntry(ask.link, ask.key))) {
|
|
8006
|
+
await this.put({
|
|
8007
|
+
...ask,
|
|
8008
|
+
state: "seen"
|
|
8009
|
+
});
|
|
8010
|
+
this.host.emit();
|
|
8011
|
+
continue;
|
|
8012
|
+
}
|
|
8013
|
+
let next = ask;
|
|
8014
|
+
const day = contactDay(now);
|
|
8015
|
+
const bits = Math.max(record?.bits ?? 18, 18);
|
|
8016
|
+
if (!next.stamp || next.stamp.day < day - 1 || next.stamp.bits < bits) next = {
|
|
8017
|
+
...next,
|
|
8018
|
+
stamp: {
|
|
8019
|
+
day,
|
|
8020
|
+
bits,
|
|
8021
|
+
stamp: await makeStampPaced(ask.link, ask.key, day, bits)
|
|
8022
|
+
}
|
|
8023
|
+
};
|
|
8024
|
+
await this.goOut(next, await this.identity(next), next.publishedAt === void 0 || now - next.publishedAt >= 36e5);
|
|
8025
|
+
}
|
|
8026
|
+
}
|
|
8027
|
+
schedule(ms) {
|
|
8028
|
+
if (!this.running) return;
|
|
8029
|
+
if (this.timer) clearTimeout(this.timer);
|
|
8030
|
+
this.timer = setTimeout(() => {
|
|
8031
|
+
this.timer = null;
|
|
8032
|
+
this.tick().catch(() => {}).finally(() => {
|
|
8033
|
+
if (!this.running || this.timer) return;
|
|
8034
|
+
if (this.asks.some((a) => a.state === "sending" || a.state === "knocking")) this.schedule(ASK_FAST_MS);
|
|
8035
|
+
else if (this.asks.some((a) => a.state === "seen")) this.schedule(ASK_SLOW_MS);
|
|
8036
|
+
});
|
|
8037
|
+
}, ms);
|
|
8038
|
+
}
|
|
8039
|
+
serial(work) {
|
|
8040
|
+
const run = this.queue.then(work, work);
|
|
8041
|
+
this.queue = run.catch(() => {});
|
|
8042
|
+
return run;
|
|
8043
|
+
}
|
|
8044
|
+
async seal(identity) {
|
|
8045
|
+
const deviceKey = newDeviceKey();
|
|
8046
|
+
return {
|
|
8047
|
+
sealed: await sealSeed(identity.seedB64, deviceKey),
|
|
8048
|
+
deviceKey
|
|
8049
|
+
};
|
|
8050
|
+
}
|
|
8051
|
+
async identity(ask) {
|
|
8052
|
+
return identityFromSeedB64(await unsealSeed(ask.seed.sealed, ask.seed.deviceKey));
|
|
8053
|
+
}
|
|
8054
|
+
async put(ask) {
|
|
8055
|
+
const next = [...this.asks.filter((a) => a.link !== ask.link), ask];
|
|
8056
|
+
await wrap((await store(STORES$1.settings, "readwrite")).put(next, CONTACT_ASKS_KEY));
|
|
8057
|
+
this.asks = next;
|
|
8058
|
+
}
|
|
8059
|
+
};
|
|
7349
8060
|
function profileName(value) {
|
|
7350
8061
|
if (typeof value !== "string" || value.length > 256) return;
|
|
7351
8062
|
return value.replace(/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/g, "").trim().slice(0, 64) || void 0;
|
|
@@ -9883,6 +10594,17 @@ function forwardKind(message) {
|
|
|
9883
10594
|
if (message.file) return "file";
|
|
9884
10595
|
return message.text.trim() ? "text" : { refused: "This message cannot be forwarded" };
|
|
9885
10596
|
}
|
|
10597
|
+
/**
|
|
10598
|
+
* Why a chat did not get a forward, as its result says it: never "" (a result with an error is a failure, whatever it
|
|
10599
|
+
* says). A copy that found no room says so in the words the app knows however the storage put it: IndexedDB's
|
|
10600
|
+
* QuotaExceededError (an empty message in Chromium), the OPFS worker's (named so), Desktop's io error (the system's English).
|
|
10601
|
+
*/
|
|
10602
|
+
function forwardError(error) {
|
|
10603
|
+
const message = (error instanceof Error ? error.message : String(error)).trim();
|
|
10604
|
+
if (/^No space left on device\b/.test(message)) return `Not enough space on this device (${message})`;
|
|
10605
|
+
const name = error?.name;
|
|
10606
|
+
return errorLine(error).trim() || (typeof name === "string" && name ? name : "Error");
|
|
10607
|
+
}
|
|
9886
10608
|
/** How many bytes of a stored file are actually here: all of them once it arrived. */
|
|
9887
10609
|
async function bytesHere(file) {
|
|
9888
10610
|
if (file.blob) return file.blob.size;
|
|
@@ -10164,14 +10886,14 @@ var DEFAULT_APP_STORES = [{
|
|
|
10164
10886
|
* entry is added with the pin of its bundle in apps/ui/src/assets/apps/built-in.json, which the build reads from the
|
|
10165
10887
|
* app's own repository (apps/ui/scripts/built-in-apps.mjs); no bundle is kept in this repository.
|
|
10166
10888
|
*
|
|
10167
|
-
* Chess: Ghostly's own publisher key for it (fingerprint odcg w6wj w8dy nqop), version 2.3.
|
|
10889
|
+
* Chess: Ghostly's own publisher key for it (fingerprint odcg w6wj w8dy nqop), version 2.3.1 (`sequence` 5), at the
|
|
10168
10890
|
* commit of its repository that the official store lists.
|
|
10169
10891
|
*/
|
|
10170
10892
|
var BUILT_IN_APPS = [{
|
|
10171
10893
|
publisher: "odcgw6wjw8dynqop84r47jbjcdqossbgrfiejd367e14hgxmjcho",
|
|
10172
10894
|
name: "chess",
|
|
10173
|
-
digest: "
|
|
10174
|
-
url: "https://cdn.jsdelivr.net/gh/MiguelMedeiros/ghostly-chess@
|
|
10895
|
+
digest: "PnrjBRayGNbhfCkjPXmxLpjKYe2_m9Q65_hUPP8BHOc",
|
|
10896
|
+
url: "https://cdn.jsdelivr.net/gh/MiguelMedeiros/ghostly-chess@0cbf1acc8fd7a03848f4e2a68cbec8854767ceaa/app.ghostlyapp",
|
|
10175
10897
|
title: "Chess",
|
|
10176
10898
|
tagline: "Play chess with a contact, live in your chat"
|
|
10177
10899
|
}];
|
|
@@ -19822,6 +20544,11 @@ var Outbox = class {
|
|
|
19822
20544
|
get sameLink() {
|
|
19823
20545
|
return !this.stopped && this.closed === this.began;
|
|
19824
20546
|
}
|
|
20547
|
+
/** A check is scheduled for something queued or waiting: one that went out keeps its `waiting` entry (its backoff step), not its timer. */
|
|
20548
|
+
get scheduled() {
|
|
20549
|
+
for (const wait of this.waiting.values()) if (wait.timer) return true;
|
|
20550
|
+
return false;
|
|
20551
|
+
}
|
|
19825
20552
|
/** One message as stored now: by its id when the store can, else picked out of the chat's history. */
|
|
19826
20553
|
async one(id) {
|
|
19827
20554
|
return this.store.get ? this.store.get(id) : (await this.store.read()).find((m) => m.id === id);
|
|
@@ -19872,12 +20599,6 @@ var Outbox = class {
|
|
|
19872
20599
|
const unconfirmed = (m) => m.sender === "me" && m.via !== "hold" && m.delivery === "sent" && this.receipts.has(m.id);
|
|
19873
20600
|
return messages.filter((m) => !this.busy.has(m.id) && (toGo(m) || this.file(m) || unconfirmed(m) && (opened !== void 0 && (this.sentUnder.get(m.id) ?? 0) < opened || !!this.resender.via && this.resender.via(m) !== m.via && this.resender.ready(m)))).sort(oldestFirst);
|
|
19874
20601
|
}
|
|
19875
|
-
/**
|
|
19876
|
-
* Looks at each message, oldest first: it goes if the chat can carry it. The ones the chat could not carry are
|
|
19877
|
-
* handed back, and asked once more at the moment a later one is found able to go (`catchUp`): a link says it is open
|
|
19878
|
-
* a moment before it carries text, and a look that began in that moment sent the later messages and left the first
|
|
19879
|
-
* ones behind.
|
|
19880
|
-
*/
|
|
19881
20602
|
/** Whether this chat's waiting files go through this outbox (`Resender.sendFile`). */
|
|
19882
20603
|
get sendsFiles() {
|
|
19883
20604
|
return !!this.resender.sendFile;
|
|
@@ -19892,11 +20613,20 @@ var Outbox = class {
|
|
|
19892
20613
|
return false;
|
|
19893
20614
|
}
|
|
19894
20615
|
if (passed) await this.catchUp(passed);
|
|
19895
|
-
if (!this.sameLink)
|
|
20616
|
+
if (!this.sameLink) {
|
|
20617
|
+
if (!this.stopped && !this.waiting.get(message.id)?.timer) this.schedule(message.id);
|
|
20618
|
+
return true;
|
|
20619
|
+
}
|
|
19896
20620
|
await this.resender.sendFile(message).catch(() => {});
|
|
19897
20621
|
this.forget(message.id);
|
|
19898
20622
|
return true;
|
|
19899
20623
|
}
|
|
20624
|
+
/**
|
|
20625
|
+
* Looks at each message, oldest first: it goes if the chat can carry it. The ones the chat could not carry are
|
|
20626
|
+
* handed back, and asked once more at the moment a later one is found able to go (`catchUp`): a link says it is open
|
|
20627
|
+
* a moment before it carries text, and a look that began in that moment sent the later messages and left the first
|
|
20628
|
+
* ones behind.
|
|
20629
|
+
*/
|
|
19900
20630
|
async inOrder(messages) {
|
|
19901
20631
|
const passed = [];
|
|
19902
20632
|
for (const message of messages) {
|
|
@@ -19991,15 +20721,16 @@ var Outbox = class {
|
|
|
19991
20721
|
await this.store.update(id, "delivered");
|
|
19992
20722
|
this.clear(id);
|
|
19993
20723
|
this.forget(id);
|
|
19994
|
-
if (this.
|
|
20724
|
+
if (this.scheduled) this.flush().catch(() => {});
|
|
19995
20725
|
}
|
|
19996
20726
|
/**
|
|
19997
20727
|
* The chat can carry text again (its link came back, or the DHT path freed up): every queued message goes now,
|
|
19998
20728
|
* oldest first. `reopened`: a new link, so what still awaits a receipt from the old one, or from the DHT, goes on it too
|
|
19999
|
-
* (a receipt on the link also ends the DHT's own retries).
|
|
20729
|
+
* (a receipt on the link also ends the DHT's own retries). Bare, it looks only when a check is scheduled: a receipt,
|
|
20730
|
+
* and every read of the contact's DHT mailbox, call it.
|
|
20000
20731
|
*/
|
|
20001
20732
|
flush({ reopened = false } = {}) {
|
|
20002
|
-
if (this.stopped || !reopened && !this.
|
|
20733
|
+
if (this.stopped || !reopened && !this.scheduled) return Promise.resolve();
|
|
20003
20734
|
const opened = reopened ? ++this.opened : void 0;
|
|
20004
20735
|
return this.turn(async () => {
|
|
20005
20736
|
if (this.stopped) return;
|
|
@@ -20007,15 +20738,47 @@ var Outbox = class {
|
|
|
20007
20738
|
await this.inOrder(this.due(await this.store.read(), opened));
|
|
20008
20739
|
});
|
|
20009
20740
|
}
|
|
20010
|
-
/**
|
|
20741
|
+
/**
|
|
20742
|
+
* A queued message's check came due: the ones written before it that have not gone are looked at first. The checks
|
|
20743
|
+
* of n texts written to an away contact come due a few milliseconds apart, and each read the chat and looked at
|
|
20744
|
+
* every older text again: n²/2 reads at each backoff step. The first look now takes up every check due by the next
|
|
20745
|
+
* few seconds too (`backoffMs[0]`), and what was written before them, and gives each its next one: they come due
|
|
20746
|
+
* together from then on, and the others find theirs taken.
|
|
20747
|
+
*/
|
|
20011
20748
|
async check(id) {
|
|
20012
20749
|
if (this.stopped) return;
|
|
20750
|
+
if (this.waiting.get(id)?.timer || this.receipts.has(id)) return;
|
|
20013
20751
|
const messages = await this.store.read(), message = messages.find((m) => m.id === id);
|
|
20014
20752
|
if (!message || !(toGo(message) || this.file(message))) {
|
|
20015
20753
|
await this.attempt(id);
|
|
20016
20754
|
return;
|
|
20017
20755
|
}
|
|
20018
|
-
|
|
20756
|
+
const soon = this.now() + this.policy.backoffMs[0], taken = [];
|
|
20757
|
+
let until = message.timestamp;
|
|
20758
|
+
for (const m of messages) {
|
|
20759
|
+
const wait = this.waiting.get(m.id);
|
|
20760
|
+
if (m.id === id || !wait || this.busy.has(m.id) || !(toGo(m) || this.file(m)) || wait.timer && wait.at > soon) continue;
|
|
20761
|
+
if (wait.timer) {
|
|
20762
|
+
clearTimeout(wait.timer);
|
|
20763
|
+
wait.timer = void 0;
|
|
20764
|
+
wait.step++;
|
|
20765
|
+
}
|
|
20766
|
+
taken.push(m.id);
|
|
20767
|
+
if (m.timestamp > until) until = m.timestamp;
|
|
20768
|
+
}
|
|
20769
|
+
await this.inOrder(this.due(messages.filter((m) => m.timestamp <= until)));
|
|
20770
|
+
for (const other of taken) {
|
|
20771
|
+
const wait = this.waiting.get(other);
|
|
20772
|
+
if (!this.stopped && wait && !wait.timer && !this.busy.has(other) && !this.receipts.has(other)) this.schedule(other);
|
|
20773
|
+
}
|
|
20774
|
+
}
|
|
20775
|
+
/**
|
|
20776
|
+
* As read by the look in progress, it still waits: the chat cannot carry it, and it is not due to fail or to be
|
|
20777
|
+
* handed to store-and-forward. Nothing acts on it, so it is not read again.
|
|
20778
|
+
*/
|
|
20779
|
+
waitsOn(message) {
|
|
20780
|
+
const wait = this.waiting.get(message.id);
|
|
20781
|
+
return !!wait && (message.delivery === "queued" || message.delivery === "waiting") && message.via !== "hold" && (message.resendUntil ?? Infinity) > this.now() && !this.resender.ready(message) && !(this.resender.divert && this.now() - wait.since >= this.policy.holdAfterMs);
|
|
20019
20782
|
}
|
|
20020
20783
|
/**
|
|
20021
20784
|
* A queued or waiting message is looked at; one sent and awaiting its receipt goes again now, if the chat can carry
|
|
@@ -20023,7 +20786,7 @@ var Outbox = class {
|
|
|
20023
20786
|
*/
|
|
20024
20787
|
async again(message, passed) {
|
|
20025
20788
|
if (message.sender !== "me" || message.via === "hold") return true;
|
|
20026
|
-
if (message.delivery === "queued" || message.delivery === "waiting") return this.attempt(message.id, passed);
|
|
20789
|
+
if (message.delivery === "queued" || message.delivery === "waiting") return this.attempt(message.id, passed, message);
|
|
20027
20790
|
if (message.delivery !== "sent" || !this.receipts.has(message.id)) return true;
|
|
20028
20791
|
this.clear(message.id);
|
|
20029
20792
|
if (this.resender.ready(message)) {
|
|
@@ -20067,10 +20830,15 @@ var Outbox = class {
|
|
|
20067
20830
|
/**
|
|
20068
20831
|
* Looks at one queued message: sends it if the chat can carry it, hands it to store-and-forward, or waits more.
|
|
20069
20832
|
* False when the chat could not carry it and it still waits. `passed`: older messages the chat could not carry a
|
|
20070
|
-
* moment ago, which go first if it can now.
|
|
20833
|
+
* moment ago, which go first if it can now. `read`: its row as the look in progress read it; while it still waits
|
|
20834
|
+
* (`waitsOn`) it is not read again, else a look at n waiting texts read each of them once more.
|
|
20071
20835
|
*/
|
|
20072
|
-
async attempt(id, passed) {
|
|
20836
|
+
async attempt(id, passed, read) {
|
|
20073
20837
|
if (this.stopped || this.busy.has(id)) return true;
|
|
20838
|
+
if (read && this.waitsOn(read)) {
|
|
20839
|
+
if (!this.waiting.get(id)?.timer) this.schedule(id);
|
|
20840
|
+
return false;
|
|
20841
|
+
}
|
|
20074
20842
|
const message = await this.one(id);
|
|
20075
20843
|
if (!message || message.delivery !== "queued" && message.delivery !== "waiting" || message.via === "hold") {
|
|
20076
20844
|
this.forget(id);
|
|
@@ -20122,10 +20890,12 @@ var Outbox = class {
|
|
|
20122
20890
|
schedule(id) {
|
|
20123
20891
|
const wait = this.waiting.get(id) ?? {
|
|
20124
20892
|
step: 0,
|
|
20125
|
-
since: this.now()
|
|
20893
|
+
since: this.now(),
|
|
20894
|
+
at: 0
|
|
20126
20895
|
};
|
|
20127
20896
|
if (wait.timer) clearTimeout(wait.timer);
|
|
20128
20897
|
const delay = this.policy.backoffMs[Math.min(wait.step, this.policy.backoffMs.length - 1)];
|
|
20898
|
+
wait.at = this.now() + delay;
|
|
20129
20899
|
wait.timer = setTimeout(() => {
|
|
20130
20900
|
wait.timer = void 0;
|
|
20131
20901
|
wait.step++;
|
|
@@ -20194,6 +20964,9 @@ var EditQueue = class {
|
|
|
20194
20964
|
restoredAt = -Infinity;
|
|
20195
20965
|
/** The rows known to have an edit on its way, each with when it was last told (`edited`), by `tick`. */
|
|
20196
20966
|
known = /* @__PURE__ */ new Map();
|
|
20967
|
+
/** Of the known rows read: each one's wire id, and the DHT floor id of the edit it last sent (`dhtEditId`). A receipt finds its row by them. */
|
|
20968
|
+
wires = /* @__PURE__ */ new Map();
|
|
20969
|
+
dhtIds = /* @__PURE__ */ new Map();
|
|
20197
20970
|
tick = 0;
|
|
20198
20971
|
/** The chat's history was read for the rows with an edit on its way: `known` has them all, until the next session. */
|
|
20199
20972
|
learnt = false;
|
|
@@ -20247,40 +21020,57 @@ var EditQueue = class {
|
|
|
20247
21020
|
}
|
|
20248
21021
|
/**
|
|
20249
21022
|
* My rows with an edit on its way, by time. `all`: the chat's whole history, when it was read for them (no `row` to
|
|
20250
|
-
* read
|
|
20251
|
-
* no more is forgotten, unless it was told again while it was read.
|
|
21023
|
+
* read them by, or nothing learnt yet in this session). Otherwise the known rows (`only`: those of them) are read
|
|
21024
|
+
* alone, in one read, and one that waits no more is forgotten, unless it was told again while it was read.
|
|
20252
21025
|
*/
|
|
20253
|
-
async pending() {
|
|
21026
|
+
async pending(only) {
|
|
20254
21027
|
const waits = (m) => m.sender === "me" && !!m.edit?.pending;
|
|
20255
|
-
const
|
|
20256
|
-
if (!
|
|
21028
|
+
const read = this.deps.rows;
|
|
21029
|
+
if (!read || !this.learnt) {
|
|
20257
21030
|
const all = await this.deps.read(), rows = all.filter(waits);
|
|
20258
|
-
for (const m of rows)
|
|
21031
|
+
for (const m of rows) {
|
|
21032
|
+
if (!this.known.has(m.id)) this.known.set(m.id, this.tick);
|
|
21033
|
+
if (m.wireId) this.wires.set(m.id, m.wireId);
|
|
21034
|
+
}
|
|
20259
21035
|
this.learnt = true;
|
|
20260
21036
|
return {
|
|
20261
21037
|
rows,
|
|
20262
21038
|
all
|
|
20263
21039
|
};
|
|
20264
21040
|
}
|
|
20265
|
-
const asOf = this.tick, ids = [...this.known.keys()];
|
|
20266
|
-
const read = await Promise.all(ids.map((id) => row(id)));
|
|
21041
|
+
const asOf = this.tick, ids = only ?? [...this.known.keys()];
|
|
20267
21042
|
const rows = [];
|
|
20268
|
-
read.forEach((m, i) => {
|
|
20269
|
-
if (m && waits(m))
|
|
20270
|
-
|
|
21043
|
+
(ids.length ? await read(ids) : []).forEach((m, i) => {
|
|
21044
|
+
if (m && waits(m)) {
|
|
21045
|
+
rows.push(m);
|
|
21046
|
+
if (m.wireId) this.wires.set(m.id, m.wireId);
|
|
21047
|
+
} else if ((this.known.get(ids[i]) ?? 0) <= asOf) {
|
|
21048
|
+
this.known.delete(ids[i]);
|
|
21049
|
+
this.wires.delete(ids[i]);
|
|
21050
|
+
this.dhtIds.delete(ids[i]);
|
|
21051
|
+
}
|
|
20271
21052
|
});
|
|
20272
21053
|
return { rows: rows.sort((a, b) => a.timestamp - b.timestamp) };
|
|
20273
21054
|
}
|
|
21055
|
+
/**
|
|
21056
|
+
* The known rows a receipt may be for: the one `matches` finds among those read before, else those not read yet (none:
|
|
21057
|
+
* it names no edit on its way). Undefined: no rows to read them by, or nothing learnt yet, so all of them.
|
|
21058
|
+
*/
|
|
21059
|
+
candidates(ids, matches) {
|
|
21060
|
+
if (!this.deps.rows || !this.learnt) return void 0;
|
|
21061
|
+
for (const [rowId, value] of ids) if (matches(value) && this.known.has(rowId)) return [rowId];
|
|
21062
|
+
return [...this.known.keys()].filter((rowId) => !ids.has(rowId));
|
|
21063
|
+
}
|
|
20274
21064
|
/** The contact confirmed edit `seq` of my message with this wire id. */
|
|
20275
21065
|
async received(wireId, seq) {
|
|
20276
|
-
const row = (await this.pending()).rows.find((m) => m.wireId === wireId);
|
|
21066
|
+
const row = (await this.pending(this.candidates(this.wires, (id) => id === wireId))).rows.find((m) => m.wireId === wireId);
|
|
20277
21067
|
if (!row?.edit?.pending || row.edit.seq > seq) return;
|
|
20278
21068
|
this.sent.delete(row.id);
|
|
20279
21069
|
await this.deps.settle(row.id, row.edit.seq);
|
|
20280
21070
|
}
|
|
20281
21071
|
/** The contact confirmed, on the DHT floor, the edit that went there under this id (`dhtEditId`). */
|
|
20282
21072
|
async receivedOnDht(id) {
|
|
20283
|
-
const row = (await this.pending()).rows.find((m) => m.wireId && dhtEditId(m.wireId, m.edit.seq) === id);
|
|
21073
|
+
const row = (await this.pending(this.candidates(this.dhtIds, (sent) => sent === id))).rows.find((m) => m.wireId && dhtEditId(m.wireId, m.edit.seq) === id);
|
|
20284
21074
|
if (!row) return;
|
|
20285
21075
|
this.sent.delete(row.id);
|
|
20286
21076
|
await this.deps.settle(row.id, row.edit.seq);
|
|
@@ -20291,7 +21081,7 @@ var EditQueue = class {
|
|
|
20291
21081
|
this.timer = void 0;
|
|
20292
21082
|
}
|
|
20293
21083
|
async run() {
|
|
20294
|
-
if (this.stopped) return;
|
|
21084
|
+
if (this.stopped || !this.deps.ready()) return;
|
|
20295
21085
|
const now = this.now();
|
|
20296
21086
|
const said = (m) => this.sent.get(m.id)?.seq === m.edit.seq ? 1 : 0;
|
|
20297
21087
|
const { rows, all } = await this.pending();
|
|
@@ -20347,6 +21137,7 @@ var EditQueue = class {
|
|
|
20347
21137
|
...message.card && { sc: message.card }
|
|
20348
21138
|
}, message)) continue;
|
|
20349
21139
|
if (edit.restore) this.restoredAt = now;
|
|
21140
|
+
this.dhtIds.set(message.id, dhtEditId(message.wireId, edit.seq));
|
|
20350
21141
|
this.sent.set(message.id, {
|
|
20351
21142
|
seq: edit.seq,
|
|
20352
21143
|
at: now,
|
|
@@ -20997,6 +21788,15 @@ var db = {
|
|
|
20997
21788
|
return wrap((await store(STORES$1.messages, "readonly")).get([linkId, id]));
|
|
20998
21789
|
},
|
|
20999
21790
|
/**
|
|
21791
|
+
* These rows of a chat, in the order asked (undefined where none is), in one transaction: in the CLI's store
|
|
21792
|
+
* (fake-indexeddb) each transaction waiting at once is checked against all the others, so one per row costs the square.
|
|
21793
|
+
*/
|
|
21794
|
+
async getMessagesById(linkId, ids) {
|
|
21795
|
+
if (!ids.length) return [];
|
|
21796
|
+
const messages = await store(STORES$1.messages, "readonly");
|
|
21797
|
+
return Promise.all(ids.map((id) => wrap(messages.get([linkId, id]))));
|
|
21798
|
+
},
|
|
21799
|
+
/**
|
|
21000
21800
|
* The newest `limit` messages of a chat before `before`, oldest first, in `getMessages`' order (time, then id), and
|
|
21001
21801
|
* whether older ones remain. Reads those rows only: a long chat's first page costs what a short one's does.
|
|
21002
21802
|
* `before`: a message's place (the page ends just before it), or a time (`id` left out: the page ends before it).
|
|
@@ -21530,6 +22330,12 @@ async function myPin(host, chat, messageId, remove, current, now = Date.now()) {
|
|
|
21530
22330
|
* about a catch-up, whose answers end within the three minutes the app waits for the first.
|
|
21531
22331
|
*/
|
|
21532
22332
|
var AUTHOR_ORDER_WINDOW_MS = 18e4;
|
|
22333
|
+
/**
|
|
22334
|
+
* While some of a member's messages are still on their way, how long that member's rows wait for the next one before
|
|
22335
|
+
* they are placed again anyway: longer than a catch-up takes to ask for its next 32 (`MESH_GAP_AGAIN_MS`, 10 s). Placed
|
|
22336
|
+
* at each page, the newest rows moved down again with every older page, up to 84 writes a row for 200 caught up.
|
|
22337
|
+
*/
|
|
22338
|
+
var AUTHOR_ORDER_SETTLE_MS = 15e3;
|
|
21533
22339
|
AUTHOR_ORDER_WINDOW_MS / 2;
|
|
21534
22340
|
/** Whether `a` was written after `b` by the same member: a later epoch, or a higher number in the same one. */
|
|
21535
22341
|
function after(a, b) {
|
|
@@ -21539,10 +22345,12 @@ function after(a, b) {
|
|
|
21539
22345
|
* A member's messages in a private group in the order that member wrote them (WISP 400, requirement 10, and WISP 902
|
|
21540
22346
|
* § Catch-up). A message is placed where it arrives. When one comes of a member whose later messages were placed here
|
|
21541
22347
|
* moments ago (two catch-up answers raced: the author's own newest 32 and another member's hand-on of the older ones),
|
|
21542
|
-
*
|
|
21543
|
-
* the
|
|
21544
|
-
*
|
|
21545
|
-
*
|
|
22348
|
+
* it is stored just above the first of them where the history has room there (`above`): the author's newest stays
|
|
22349
|
+
* the last while the older pages come. Where it has none, those later ones are placed again below it, in the order
|
|
22350
|
+
* they were written, and the lines that came after them before the older one (another member's, mine) go along below
|
|
22351
|
+
* them, in their order: a reply to the newest stays below it. A row only ever moves down, so nobody else's lines change
|
|
22352
|
+
* order. While messages of that member are still missing (`missing`), the rows wait for them, so each row moves about
|
|
22353
|
+
* once in a whole catch-up.
|
|
21546
22354
|
*/
|
|
21547
22355
|
var GroupAuthorOrder = class {
|
|
21548
22356
|
host;
|
|
@@ -21550,6 +22358,7 @@ var GroupAuthorOrder = class {
|
|
|
21550
22358
|
missing;
|
|
21551
22359
|
/** Per group: the members' rows followed, in the order they are placed in the history. */
|
|
21552
22360
|
rows = /* @__PURE__ */ new Map();
|
|
22361
|
+
came = 0;
|
|
21553
22362
|
/** Per group: the members with a row placed above an older one of theirs, since when, and when their last row came. */
|
|
21554
22363
|
due = /* @__PURE__ */ new Map();
|
|
21555
22364
|
running = /* @__PURE__ */ new Set();
|
|
@@ -21558,14 +22367,37 @@ var GroupAuthorOrder = class {
|
|
|
21558
22367
|
this.linkOf = linkOf;
|
|
21559
22368
|
this.missing = missing;
|
|
21560
22369
|
}
|
|
21561
|
-
/**
|
|
21562
|
-
|
|
22370
|
+
/**
|
|
22371
|
+
* The row a member's message about to be stored goes just above: the first placed of those that member wrote after
|
|
22372
|
+
* it, when nobody else's came after that one (a hand-on of everyone's older messages brings them in the order they
|
|
22373
|
+
* were written: the message goes after those). None when nothing they wrote later was placed here, or another's
|
|
22374
|
+
* came since: it goes where it comes.
|
|
22375
|
+
*/
|
|
22376
|
+
above(groupId, row) {
|
|
22377
|
+
const rows = this.rows.get(groupId) ?? [];
|
|
22378
|
+
if (rows.some((r) => r.id === row.id)) return void 0;
|
|
22379
|
+
const at = rows.findIndex((r) => r.sender === row.sender && after(r, row));
|
|
22380
|
+
return at >= 0 && rows.slice(at).every((r) => r.sender === row.sender) ? rows[at].id : void 0;
|
|
22381
|
+
}
|
|
22382
|
+
/**
|
|
22383
|
+
* A member's message was stored in the group's history, new: `at` is when, on the engine's clock. `above`: the row
|
|
22384
|
+
* it was stored just above, as `above` named it; else it went to the end.
|
|
22385
|
+
*/
|
|
22386
|
+
placed(groupId, row, above) {
|
|
21563
22387
|
if (!this.host.placeAgain) return;
|
|
21564
22388
|
const rows = (this.rows.get(groupId) ?? []).filter((r) => r.at > row.at - AUTHOR_ORDER_WINDOW_MS).slice(-575);
|
|
21565
|
-
rows.
|
|
22389
|
+
const at = above === void 0 ? -1 : rows.findIndex((r) => r.id === above);
|
|
22390
|
+
const followed = {
|
|
22391
|
+
...row,
|
|
22392
|
+
n: ++this.came
|
|
22393
|
+
};
|
|
22394
|
+
if (at >= 0) rows.splice(at, 0, followed);
|
|
22395
|
+
else rows.push(followed);
|
|
21566
22396
|
this.rows.set(groupId, rows);
|
|
21567
22397
|
let due = this.due.get(groupId);
|
|
21568
|
-
|
|
22398
|
+
const first = rows.slice(0, at >= 0 ? at : -1).findIndex((r) => r.sender === row.sender && after(r, row));
|
|
22399
|
+
if (!due?.has(row.sender) && first < 0) return;
|
|
22400
|
+
if (first >= 0) for (const r of rows.slice(first)) r.at = Math.max(r.at, row.at);
|
|
21569
22401
|
if (!due) this.due.set(groupId, due = /* @__PURE__ */ new Map());
|
|
21570
22402
|
due.set(row.sender, {
|
|
21571
22403
|
since: due.get(row.sender)?.since ?? row.at,
|
|
@@ -21608,21 +22440,22 @@ var GroupAuthorOrder = class {
|
|
|
21608
22440
|
const newer = mine.filter((r) => !after(from, r)).sort((a, b) => after(a, b) ? 1 : after(b, a) ? -1 : 0);
|
|
21609
22441
|
const ids = new Set(newer.map((r) => r.id));
|
|
21610
22442
|
const first = rows.findIndex((r) => ids.has(r.id));
|
|
21611
|
-
let end = rows
|
|
21612
|
-
for (
|
|
21613
|
-
if (
|
|
21614
|
-
if (after(top,
|
|
21615
|
-
end =
|
|
22443
|
+
let end = Infinity, top = rows[first];
|
|
22444
|
+
for (const r of rows.filter((r) => r.n > rows[first].n).sort((a, b) => a.n - b.n)) {
|
|
22445
|
+
if (r.sender !== sender) continue;
|
|
22446
|
+
if (after(top, r)) {
|
|
22447
|
+
end = r.n;
|
|
21616
22448
|
break;
|
|
21617
22449
|
}
|
|
21618
|
-
top =
|
|
22450
|
+
top = r;
|
|
21619
22451
|
}
|
|
21620
|
-
const followers = rows.slice(first
|
|
22452
|
+
const followers = rows.slice(first).filter((r) => r.sender !== sender && r.n < end);
|
|
21621
22453
|
for (const r of followers) ids.add(r.id);
|
|
21622
22454
|
const moved = [...newer, ...followers];
|
|
21623
22455
|
this.rows.set(groupId, [...rows.filter((r) => !ids.has(r.id)), ...moved.map((r) => ({
|
|
21624
22456
|
...r,
|
|
21625
|
-
at: Math.max(r.at, now)
|
|
22457
|
+
at: Math.max(r.at, now),
|
|
22458
|
+
n: ++this.came
|
|
21626
22459
|
}))]);
|
|
21627
22460
|
await this.host.placeAgain(this.linkOf(groupId), moved.map((r) => r.id)).catch(() => {});
|
|
21628
22461
|
}
|
|
@@ -21632,8 +22465,8 @@ var GroupAuthorOrder = class {
|
|
|
21632
22465
|
}
|
|
21633
22466
|
}
|
|
21634
22467
|
};
|
|
21635
|
-
|
|
21636
|
-
|
|
22468
|
+
/** How long the lines wait at most while older messages keep coming: one move every so often in a long catch-up. */
|
|
22469
|
+
var LINE_ORDER_WAIT_MAX_MS = AUTHOR_ORDER_SETTLE_MS;
|
|
21637
22470
|
/**
|
|
21638
22471
|
* The lines of a group's history ("… joined", "… is no longer a member") among the messages of their moment, for a
|
|
21639
22472
|
* member back from away (WISP 902 § Catch-up). A message sealed under an epoch was written before its author knew of
|
|
@@ -21642,30 +22475,39 @@ var GroupAuthorOrder = class {
|
|
|
21642
22475
|
* its epoch) and, in a private group, the answers of several members race, so the messages of the epoch before can come
|
|
21643
22476
|
* after the line. Then the line, and the rows after it written knowing it, are placed again below them, in their order;
|
|
21644
22477
|
* no clock is read. A row only ever moves down, and only rows placed here in the last few minutes, so a live
|
|
21645
|
-
* conversation read long ago never moves.
|
|
22478
|
+
* conversation read long ago never moves. The lines wait for the rest of the older messages (`tick`), and while
|
|
22479
|
+
* messages of an epoch before are still on their way (`onTheirWay`) the rows wait for them, as `GroupAuthorOrder`'s do:
|
|
22480
|
+
* a row moves about once a catch-up.
|
|
21646
22481
|
*/
|
|
21647
22482
|
var GroupLineOrder = class {
|
|
21648
22483
|
host;
|
|
22484
|
+
onTheirWay;
|
|
21649
22485
|
/** Per history: the rows followed, in the order they are placed in it. */
|
|
21650
22486
|
rows = /* @__PURE__ */ new Map();
|
|
22487
|
+
/** Per history with rows out of order: since when, and when the last one came. */
|
|
22488
|
+
waiting = /* @__PURE__ */ new Map();
|
|
21651
22489
|
due = /* @__PURE__ */ new Set();
|
|
21652
22490
|
running = /* @__PURE__ */ new Set();
|
|
21653
|
-
constructor(host) {
|
|
22491
|
+
constructor(host, onTheirWay = () => false) {
|
|
21654
22492
|
this.host = host;
|
|
22493
|
+
this.onTheirWay = onTheirWay;
|
|
21655
22494
|
}
|
|
21656
22495
|
/**
|
|
21657
22496
|
* A row is being stored, a line of the history or a message sealed under this epoch, and takes its place now: `at` is
|
|
21658
22497
|
* when, on the engine's clock. Call what this returns with whether it was new: a message below a line of a later
|
|
21659
|
-
* epoch takes the line (and the rows after it written knowing it) below it.
|
|
22498
|
+
* epoch takes the line (and the rows after it written knowing it) below it. `above`: the row it is stored just above
|
|
22499
|
+
* (`GroupAuthorOrder.above`); else it goes to the end.
|
|
21660
22500
|
*/
|
|
21661
|
-
placing(linkId, row, at) {
|
|
22501
|
+
placing(linkId, row, at, above) {
|
|
21662
22502
|
if (!this.host.placeAgain) return () => {};
|
|
21663
22503
|
const followed = {
|
|
21664
22504
|
...row,
|
|
21665
22505
|
at
|
|
21666
22506
|
};
|
|
21667
22507
|
const rows = (this.rows.get(linkId) ?? []).filter((r) => r.at > at - AUTHOR_ORDER_WINDOW_MS).slice(-575);
|
|
21668
|
-
rows.
|
|
22508
|
+
const index = above === void 0 ? -1 : rows.findIndex((r) => r.id === above);
|
|
22509
|
+
if (index >= 0) rows.splice(index, 0, followed);
|
|
22510
|
+
else rows.push(followed);
|
|
21669
22511
|
this.rows.set(linkId, rows);
|
|
21670
22512
|
return (stored) => {
|
|
21671
22513
|
const now = this.rows.get(linkId);
|
|
@@ -21673,23 +22515,46 @@ var GroupLineOrder = class {
|
|
|
21673
22515
|
if (now) this.rows.set(linkId, now.filter((r) => r !== followed));
|
|
21674
22516
|
return;
|
|
21675
22517
|
}
|
|
21676
|
-
if (
|
|
22518
|
+
if (followed.line || !now) return;
|
|
22519
|
+
const line = now.findIndex((r) => r.line && r.epoch > followed.epoch);
|
|
22520
|
+
if (line < 0 || line > now.indexOf(followed)) return;
|
|
22521
|
+
for (const r of now.slice(line)) r.at = Math.max(r.at, at);
|
|
22522
|
+
this.wait(linkId, at);
|
|
21677
22523
|
};
|
|
21678
22524
|
}
|
|
21679
|
-
/** These rows were placed again below everything (`GroupAuthorOrder`), in this order. */
|
|
21680
|
-
moved(linkId, ids) {
|
|
22525
|
+
/** These rows were placed again below everything (`GroupAuthorOrder`), in this order, at `at` on the engine's clock. */
|
|
22526
|
+
moved(linkId, ids, at) {
|
|
21681
22527
|
const rows = this.rows.get(linkId);
|
|
21682
22528
|
if (!rows) return;
|
|
21683
22529
|
const moved = ids.map((id) => rows.find((r) => r.id === id)).filter((r) => !!r);
|
|
21684
22530
|
if (!moved.length) return;
|
|
21685
22531
|
this.rows.set(linkId, [...rows.filter((r) => !moved.includes(r)), ...moved]);
|
|
21686
|
-
if (moved.some((r) => !r.line)) this.
|
|
22532
|
+
if (moved.some((r) => !r.line)) this.wait(linkId, at);
|
|
22533
|
+
}
|
|
22534
|
+
/**
|
|
22535
|
+
* Time passed: histories no older message came to for a moment, and nothing more is on its way for, or that waited
|
|
22536
|
+
* long enough, are put in order.
|
|
22537
|
+
*/
|
|
22538
|
+
tick(now) {
|
|
22539
|
+
for (const [linkId, { since, last }] of this.waiting) {
|
|
22540
|
+
if (now - last < 3e3 && now - since < LINE_ORDER_WAIT_MAX_MS) continue;
|
|
22541
|
+
if (now - last < 15e3 && now - since < 9e4 && this.onTheirWay(linkId)) continue;
|
|
22542
|
+
this.waiting.delete(linkId);
|
|
22543
|
+
this.run(linkId);
|
|
22544
|
+
}
|
|
21687
22545
|
}
|
|
21688
22546
|
/** The group's history is gone (left or forgotten). */
|
|
21689
22547
|
forget(linkId) {
|
|
21690
22548
|
this.rows.delete(linkId);
|
|
22549
|
+
this.waiting.delete(linkId);
|
|
21691
22550
|
this.due.delete(linkId);
|
|
21692
22551
|
}
|
|
22552
|
+
wait(linkId, at) {
|
|
22553
|
+
this.waiting.set(linkId, {
|
|
22554
|
+
since: this.waiting.get(linkId)?.since ?? at,
|
|
22555
|
+
last: at
|
|
22556
|
+
});
|
|
22557
|
+
}
|
|
21693
22558
|
/** The history's rows put in order; what comes while a move is written goes in the next turn. */
|
|
21694
22559
|
run(linkId) {
|
|
21695
22560
|
this.due.add(linkId);
|
|
@@ -21705,8 +22570,9 @@ var GroupLineOrder = class {
|
|
|
21705
22570
|
if (above < 0) order.push(row);
|
|
21706
22571
|
else order.splice(above, 0, row);
|
|
21707
22572
|
}
|
|
22573
|
+
const place = new Map(rows.map((r, i) => [r, i]));
|
|
21708
22574
|
let from = 0;
|
|
21709
|
-
while (from <
|
|
22575
|
+
while (from < order.length && (from === 0 || place.get(order[from]) > place.get(order[from - 1]))) from++;
|
|
21710
22576
|
if (from === rows.length) continue;
|
|
21711
22577
|
const last = rows[rows.length - 1].at;
|
|
21712
22578
|
for (const r of order.slice(from)) r.at = Math.max(r.at, last);
|
|
@@ -21892,6 +22758,14 @@ var CUT_OFF_STEP_UP_MS = 15e3;
|
|
|
21892
22758
|
/** A lobby request this recent is from a member still looking fast for my side of the edge: mine opens expecting it. */
|
|
21893
22759
|
var LOBBY_EXPECT_MS = 3e4;
|
|
21894
22760
|
/**
|
|
22761
|
+
* A member whose edge was down `memberGoneMs` is dropped by its hub, and its edge closed: its app closed, and is often
|
|
22762
|
+
* opened again within minutes, when it dials an edge my side no longer has and asks in my lobby. So my lobby is read at
|
|
22763
|
+
* the busy pace this long after, as a 1:1 chat watches a contact that went away (`WATCH_PEER_MS`). Read every half minute,
|
|
22764
|
+
* a web member back two minutes after it closed got the lines written meanwhile 30-45 s after it opened, while a 1:1
|
|
22765
|
+
* text from the same bot came in seconds (bug hunt 1013-2254, 2026-10-14).
|
|
22766
|
+
*/
|
|
22767
|
+
var MEMBER_BACK_MS = WATCH_PEER_MS;
|
|
22768
|
+
/**
|
|
21895
22769
|
* A lobby is one record every member asking a hub writes, each reading it and writing it back with its own request: two
|
|
21896
22770
|
* members writing at once (both cut off by the same leave, in the same second) each write what they read, and the later
|
|
21897
22771
|
* write drops the earlier request. So a member reads the lobby back this long after its write (plus up to `LOBBY_CHECK_JITTER_MS`, so
|
|
@@ -22352,10 +23226,10 @@ var Communities = class {
|
|
|
22352
23226
|
await this.require(groupId).rotate();
|
|
22353
23227
|
}
|
|
22354
23228
|
async setPicture(groupId, picture) {
|
|
22355
|
-
await this.require(groupId).setPicture(picture);
|
|
23229
|
+
await this.require(groupId).setPicture(picture, this.now());
|
|
22356
23230
|
}
|
|
22357
23231
|
async rename(groupId, name) {
|
|
22358
|
-
await this.require(groupId).rename(name);
|
|
23232
|
+
await this.require(groupId).rename(name, this.now());
|
|
22359
23233
|
}
|
|
22360
23234
|
/** A new link (the old one reaches nobody), or none. */
|
|
22361
23235
|
async replaceLink(groupId, off = false) {
|
|
@@ -22418,6 +23292,7 @@ var Communities = class {
|
|
|
22418
23292
|
/** Everything timed: beacon, hub election, lobbies, knocks, leaves, edges. The engine calls it every second or so. */
|
|
22419
23293
|
async tick(now = Date.now()) {
|
|
22420
23294
|
this.time = now;
|
|
23295
|
+
this.lineOrder.tick(now);
|
|
22421
23296
|
const active = [...this.live.values()].filter((l) => l.session.status === "active");
|
|
22422
23297
|
this.sharing = {
|
|
22423
23298
|
groups: Math.max(1, active.length),
|
|
@@ -22551,7 +23426,7 @@ var Communities = class {
|
|
|
22551
23426
|
const busy = this.admitting(groupId, live, now);
|
|
22552
23427
|
if (s.state.pendingLeaves.some((r) => r.s !== me && this.recentHub(live, r.s))) live.lobbyBusyUntil = Math.max(live.lobbyBusyUntil, now + this.timings.lobbyBusyMs);
|
|
22553
23428
|
if (this.hubDropped(groupId, live, others, now)) live.lobbyBusyUntil = Math.max(live.lobbyBusyUntil, now + this.timings.lobbyBusyMs);
|
|
22554
|
-
const lobbyEvery = now < live.lobbyBusyUntil && !busy ? this.timings.lobbyPollMs : this.timings.lobbyIdlePollMs;
|
|
23429
|
+
const lobbyEvery = (now < live.lobbyBusyUntil || now < (live.memberBackUntil ?? 0)) && !busy ? this.timings.lobbyPollMs : this.timings.lobbyIdlePollMs;
|
|
22555
23430
|
if (now - live.lastLobbyPoll >= lobbyEvery) {
|
|
22556
23431
|
live.lastLobbyPoll = now;
|
|
22557
23432
|
await this.pollLobby(groupId, live, now);
|
|
@@ -22595,6 +23470,9 @@ var Communities = class {
|
|
|
22595
23470
|
}
|
|
22596
23471
|
let kept = live.myHubs.filter((key) => (fresh.has(key) || this.recentHub(live, key)) && !live.hubsAvoided.has(key));
|
|
22597
23472
|
kept = kept.slice(0, COMMUNITY_TOPOLOGY.hubsPerMember);
|
|
23473
|
+
if (!kept.length) {
|
|
23474
|
+
for (const key of live.restored.keys()) if (kept.length < COMMUNITY_TOPOLOGY.hubsPerMember && !live.hubsAvoided.has(key) && others.some((h) => h.key === key && h.load < COMMUNITY_TOPOLOGY.hubCapacity)) kept.push(key);
|
|
23475
|
+
}
|
|
22598
23476
|
const picked = pickHubs(me, others, now, new Set(live.hubsAvoided.keys()));
|
|
22599
23477
|
const bare = ![...edges.values()].some((id) => this.host.linkReady(id, 2));
|
|
22600
23478
|
for (const key of picked) if (kept.length < Math.max(1, picked.length) && !kept.includes(key)) {
|
|
@@ -22693,7 +23571,11 @@ var Communities = class {
|
|
|
22693
23571
|
const id = edges.get(key);
|
|
22694
23572
|
if (this.gone(live, key)) live.members.delete(key);
|
|
22695
23573
|
else if (id && this.host.linkReady(id, 2)) live.members.set(key, now);
|
|
22696
|
-
else if (
|
|
23574
|
+
else if (live.session.wasRemoved(key)) live.members.delete(key);
|
|
23575
|
+
else if (now - since > this.timings.memberGoneMs) {
|
|
23576
|
+
live.members.delete(key);
|
|
23577
|
+
live.memberBackUntil = now + MEMBER_BACK_MS;
|
|
23578
|
+
}
|
|
22697
23579
|
}
|
|
22698
23580
|
this.shed(groupId, live, now);
|
|
22699
23581
|
return live.members.size;
|
|
@@ -22746,7 +23628,8 @@ var Communities = class {
|
|
|
22746
23628
|
async readBeacon(groupId, live, now) {
|
|
22747
23629
|
live.lastBeaconRead = now;
|
|
22748
23630
|
const keys = beaconKeys(live.session.state.rv, groupId);
|
|
22749
|
-
const
|
|
23631
|
+
const back = !live.beaconAt && this.wayBack(groupId, live);
|
|
23632
|
+
const records = await this.host.resolve(keys.identity.pubKeyZ32, !back, !back && this.doorBusy(groupId, live, now), back).catch(() => void 0);
|
|
22750
23633
|
if (records === void 0) {
|
|
22751
23634
|
live.beaconFailedAt = now;
|
|
22752
23635
|
live.beaconAskedAt ||= now;
|
|
@@ -22779,9 +23662,10 @@ var Communities = class {
|
|
|
22779
23662
|
async publishBeacon(groupId, live, now, listed) {
|
|
22780
23663
|
live.lastBeaconTry = now;
|
|
22781
23664
|
const keys = beaconKeys(live.session.state.rv, groupId);
|
|
23665
|
+
const back = live.hub && this.wayBack(groupId, live);
|
|
22782
23666
|
let existing = live.beacon, head = live.head;
|
|
22783
23667
|
if (live.lastBeaconRead !== now || live.beaconFailedAt === now) {
|
|
22784
|
-
const read = await this.host.resolve(keys.identity.pubKeyZ32,
|
|
23668
|
+
const read = await this.host.resolve(keys.identity.pubKeyZ32, !back, live.hub && !back, back).catch(() => void 0);
|
|
22785
23669
|
if (read === void 0 && live.session.roster.length > 1) throw new Error("The beacon could not be read; not published");
|
|
22786
23670
|
const records = read ?? [];
|
|
22787
23671
|
existing = live.hubClocks.inMyTime(readBeacon(keys, records), now);
|
|
@@ -22794,7 +23678,7 @@ var Communities = class {
|
|
|
22794
23678
|
since: live.hubSince || now
|
|
22795
23679
|
} : null, now, (key) => !this.gone(live, key));
|
|
22796
23680
|
const newest = newerHead(head, this.newestFrame(live), now);
|
|
22797
|
-
await this.host.publish(keys.identity, beaconRecords(keys, live.hubClocks.asWritten(hubs), newest),
|
|
23681
|
+
await this.host.publish(keys.identity, beaconRecords(keys, live.hubClocks.asWritten(hubs), newest), !back, live.hub && !back, back);
|
|
22798
23682
|
live.beacon = hubs;
|
|
22799
23683
|
live.head = newest;
|
|
22800
23684
|
this.noteHubs(live, now);
|
|
@@ -22926,12 +23810,13 @@ var Communities = class {
|
|
|
22926
23810
|
/** A member asks a hub for an edge: its key in the hub's lobby, and its side of the edge started. */
|
|
22927
23811
|
async askHub(groupId, live, hub, now) {
|
|
22928
23812
|
const keys = lobbyKeys(live.session.state.rv, groupId, hub);
|
|
22929
|
-
const
|
|
23813
|
+
const back = this.wayBack(groupId, live);
|
|
23814
|
+
const existing = readLobby(keys, await this.host.resolve(keys.identity.pubKeyZ32, !back, !back, back) ?? []);
|
|
22930
23815
|
const theirs = now - live.hubClocks.ahead(hub);
|
|
22931
23816
|
await this.host.publish(keys.identity, lobbyRecords(keys, mergeLobby(existing, {
|
|
22932
23817
|
key: live.session.myKey,
|
|
22933
23818
|
ts: theirs
|
|
22934
|
-
}, theirs)),
|
|
23819
|
+
}, theirs)), !back, !back, back);
|
|
22935
23820
|
(live.lobbyChecks ??= /* @__PURE__ */ new Map()).set(hub, {
|
|
22936
23821
|
at: now + LOBBY_CHECK_MS + Math.floor(this.random() * LOBBY_CHECK_JITTER_MS),
|
|
22937
23822
|
ts: Math.floor(theirs / 1e3) * 1e3
|
|
@@ -22954,7 +23839,8 @@ var Communities = class {
|
|
|
22954
23839
|
}
|
|
22955
23840
|
async pollLobby(groupId, live, now) {
|
|
22956
23841
|
const keys = lobbyKeys(live.session.state.rv, groupId, live.session.myKey);
|
|
22957
|
-
const
|
|
23842
|
+
const back = this.wayBack(groupId, live);
|
|
23843
|
+
const entries = readLobby(keys, await this.host.resolve(keys.identity.pubKeyZ32, !back, !back && this.doorBusy(groupId, live, now), back).catch(() => null) ?? []);
|
|
22958
23844
|
for (const { key, ts } of entries) {
|
|
22959
23845
|
if (now - ts > COMMUNITY_TOPOLOGY.lobbyFreshMs || key === live.session.myKey) continue;
|
|
22960
23846
|
if (live.session.wasRemoved(key)) {
|
|
@@ -23055,6 +23941,34 @@ var Communities = class {
|
|
|
23055
23941
|
return live.hub && (now < live.warmUntil || now < live.crowdUntil || now < live.lobbyBusyUntil || this.admitting(groupId, live, now));
|
|
23056
23942
|
}
|
|
23057
23943
|
/**
|
|
23944
|
+
* A member with no edge up in this community: its first reading of the beacon (who the hubs are) and its request in a
|
|
23945
|
+
* hub's lobby are its way back, and go within the relays' allowance for it (`PkarrRequestOptions.back`), not as
|
|
23946
|
+
* background looks. Those wait for every link that signals and every chat's look: a web app opened with 30 chats whose
|
|
23947
|
+
* contacts were online read none of its 5 communities' beacons in 3 minutes, dropped the edges of its last run at 60 s,
|
|
23948
|
+
* and got no community line, neither those written while it was closed nor new ones (bug hunt, 2026-10-13). Nor as a
|
|
23949
|
+
* group edge's signaling: the edges of its 15 groups, all offering at once, spent that allowance in the first second,
|
|
23950
|
+
* and the beacons were read a minute later or not at all (bug hunt, 2026-10-14). Bounded: the reading is asked again
|
|
23951
|
+
* only when refused, and a hub that does not take me is given up after its wait (`hubWaitMs`).
|
|
23952
|
+
*
|
|
23953
|
+
* A hub with no edge up too (an app back from a restart steps up at once where it was one, before any edge is up): its
|
|
23954
|
+
* beacon entry, read and written, and its reading of its lobby. As background looks, a daemon back as the hub of its 5
|
|
23955
|
+
* communities while its 30 chats signaled got back into them one at a time, 4 to 10 minutes, one not at all in 16: its
|
|
23956
|
+
* entry went unwritten, the other hubs took it for gone (`hubDead`) and its members asked them (bug hunt, 2026-10-14).
|
|
23957
|
+
* Not a hub alone in the beacon: nobody carries the group without it, and its door's looks stay background ones.
|
|
23958
|
+
*
|
|
23959
|
+
* Alone as my last reading of the beacon said, and only while that reading is fresh. One that aged out says nothing of
|
|
23960
|
+
* now: judged by my clock against it, every hub it listed went stale, and I was alone. An app back from a restart as a
|
|
23961
|
+
* member of a community whose hubs did not list it read the beacon once (its way back); its edge stayed down while its
|
|
23962
|
+
* other links took its relays' budget, its reading aged out, it stepped up as a hub "alone", and its readings and its
|
|
23963
|
+
* entry went as background looks the relays held: it never read the beacon again, nobody listed it, and it was not
|
|
23964
|
+
* back in that community in 10 minutes (bug hunt, 2026-10-17).
|
|
23965
|
+
*/
|
|
23966
|
+
wayBack(groupId, live) {
|
|
23967
|
+
const read = live.beaconAt, now = this.now();
|
|
23968
|
+
if (live.hub && now - read < COMMUNITY_TOPOLOGY.beaconFreshMs && !freshHubs(this.hubs(live), read).some((h) => h.key !== live.session.myKey)) return false;
|
|
23969
|
+
return ![...this.host.edges(groupId).values()].some((id) => this.host.linkReady(id, 2));
|
|
23970
|
+
}
|
|
23971
|
+
/**
|
|
23058
23972
|
* Which knock records to read now (none, most of the time): every one at once the first time
|
|
23059
23973
|
* (a hub just elected, an app back, a change of door), since a knock left while nobody was at the
|
|
23060
23974
|
* door may be in any; then the door the bell, and the others in turn; the other hubs one in turn, slowly.
|
|
@@ -23521,6 +24435,7 @@ var Communities = class {
|
|
|
23521
24435
|
noteCame(this.stored.get(id), came, m.sender !== session.myKey, mentioned);
|
|
23522
24436
|
},
|
|
23523
24437
|
app: (m) => this.deliver(id, () => m.frame.t === "group-have" ? this.files?.communityApp(id, m.sender, m.frame) : this.host.communityApp?.(id, m.sender, m.frame)),
|
|
24438
|
+
lasting: (m) => m.frame.t === "pin" && m.sender === session.admin ? "pin" : void 0,
|
|
23524
24439
|
pair: (m) => this.deliver(id, () => this.host.communityPair?.(id, m.sender, m.payload)),
|
|
23525
24440
|
changed: () => {
|
|
23526
24441
|
this.membershipChanged(id);
|
|
@@ -24768,6 +25683,8 @@ var Groups = class {
|
|
|
24768
25683
|
/** Admin side: entry sessions of a member key that timed out in a row, and when the last did. */
|
|
24769
25684
|
entryTimeouts = /* @__PURE__ */ new Map();
|
|
24770
25685
|
lastPoll = /* @__PURE__ */ new Map();
|
|
25686
|
+
/** Admin side: when a link nobody is using was last read. Those links share one `pollMs` between them, read in turn. */
|
|
25687
|
+
lastQuietPoll = 0;
|
|
24771
25688
|
/** Admin side: until when a group's link is looked at the warm pace. */
|
|
24772
25689
|
warmUntil = /* @__PURE__ */ new Map();
|
|
24773
25690
|
lastKnock = /* @__PURE__ */ new Map();
|
|
@@ -24798,6 +25715,12 @@ var Groups = class {
|
|
|
24798
25715
|
* whom, and when it was asked. Their sync goes once that answer has come.
|
|
24799
25716
|
*/
|
|
24800
25717
|
syncHeld = /* @__PURE__ */ new Map();
|
|
25718
|
+
/**
|
|
25719
|
+
* Per mesh group, the members told where I am since their edge came up (`edgeReady`), forgotten when a tick sees it
|
|
25720
|
+
* down. One whose edge is up but not told yet opened in the same moment as the one being told: it is asked of that
|
|
25721
|
+
* one, as if its edge were still down (`askOf`), and no gossip turn or ask for gaps goes to it before it is told.
|
|
25722
|
+
*/
|
|
25723
|
+
told = /* @__PURE__ */ new Map();
|
|
24801
25724
|
/** Per mesh group and member, when a message last came over its edge (its own, or one it handed on). */
|
|
24802
25725
|
lastHanded = /* @__PURE__ */ new Map();
|
|
24803
25726
|
/** Admin side, per group and member let in: when its welcome was last sent again, over its chat or the link (`REWELCOME_MS`). */
|
|
@@ -24863,16 +25786,16 @@ var Groups = class {
|
|
|
24863
25786
|
}
|
|
24864
25787
|
}, hubTimings);
|
|
24865
25788
|
this.typings = new GroupTypings(host);
|
|
24866
|
-
this.lineOrder = new GroupLineOrder(host)
|
|
25789
|
+
this.lineOrder = new GroupLineOrder(host, (linkId) => {
|
|
25790
|
+
const groupId = linkId.slice(MESSAGE_LINK("").length), session = this.sessions.get(groupId);
|
|
25791
|
+
return !!session && [...session.lacking().keys()].some((sender) => this.onTheirWay(groupId, sender, session.epoch));
|
|
25792
|
+
});
|
|
24867
25793
|
const placeAgain = host.placeAgain && (async (linkId, ids) => {
|
|
24868
25794
|
const done = host.placeAgain(linkId, ids);
|
|
24869
|
-
this.lineOrder.moved(linkId, ids);
|
|
25795
|
+
this.lineOrder.moved(linkId, ids, this.now());
|
|
24870
25796
|
await done;
|
|
24871
25797
|
});
|
|
24872
|
-
this.authorOrder = new GroupAuthorOrder({ placeAgain }, MESSAGE_LINK, (groupId, sender) =>
|
|
24873
|
-
const session = this.sessions.get(groupId);
|
|
24874
|
-
return !!session && session.missing(sender, this.gapsGone.get(groupId)?.get(sender)?.[session.epoch]) > 0;
|
|
24875
|
-
});
|
|
25798
|
+
this.authorOrder = new GroupAuthorOrder({ placeAgain }, MESSAGE_LINK, (groupId, sender) => this.onTheirWay(groupId, sender));
|
|
24876
25799
|
this.signals = new MeshSignals(host, {
|
|
24877
25800
|
session: (id) => this.sessions.get(id),
|
|
24878
25801
|
stored: (id) => this.stored.get(id),
|
|
@@ -25124,10 +26047,11 @@ var Groups = class {
|
|
|
25124
26047
|
if (this.isCommunity(groupId)) throw new Error("Share the group's link with them: anyone who opens it joins");
|
|
25125
26048
|
const session = this.session(groupId);
|
|
25126
26049
|
if (!session.isAdmin) throw new Error("Only the admin can invite");
|
|
26050
|
+
if (this.host.adminWork?.(groupId) === false) throw new Error(GROUP_ADMIN_OFF_ERROR);
|
|
25127
26051
|
const group = this.stored.get(groupId);
|
|
25128
26052
|
if (Object.entries(group.contacts ?? {}).some(([key, id]) => id === linkId && rosterHas(session.roster, key))) throw new Error("This contact is already a member");
|
|
25129
26053
|
if (session.roster.length + (this.invited.get(groupId)?.size ?? 0) >= GROUP_MEMBER_CAP.max) throw new Error(`A group holds ${GROUP_MEMBER_CAP.max} members at most`);
|
|
25130
|
-
if (!this.host.linkReady(linkId)) throw new Error(
|
|
26054
|
+
if (!this.host.linkReady(linkId)) throw new Error(this.host.linkOpen?.(linkId) === false ? "This contact is not connected right now. Invite them when they are online." : "Their app does not take part in groups: they need an updated Ghostly first.");
|
|
25131
26055
|
const blocked = this.growthBlocked(group, session, session.roster.length + (this.invited.get(groupId)?.size ?? 0) + 1, linkId);
|
|
25132
26056
|
if (blocked) throw new Error(blocked);
|
|
25133
26057
|
this.host.sendOnLink(linkId, session.inviteFrame());
|
|
@@ -25654,6 +26578,7 @@ var Groups = class {
|
|
|
25654
26578
|
this.lingering.delete(groupId);
|
|
25655
26579
|
this.relayAsked.delete(groupId);
|
|
25656
26580
|
this.syncHeld.delete(groupId);
|
|
26581
|
+
this.told.delete(groupId);
|
|
25657
26582
|
this.lastHanded.delete(groupId);
|
|
25658
26583
|
this.rewelcomed.delete(groupId);
|
|
25659
26584
|
this.hubs.forget(groupId);
|
|
@@ -25769,6 +26694,8 @@ var Groups = class {
|
|
|
25769
26694
|
this.ticking = true;
|
|
25770
26695
|
try {
|
|
25771
26696
|
this.authorOrder.tick(now);
|
|
26697
|
+
this.lineOrder.tick(now);
|
|
26698
|
+
const quiet = [];
|
|
25772
26699
|
for (const group of [...this.stored.values()]) {
|
|
25773
26700
|
if (group.left) {
|
|
25774
26701
|
if (now - group.left.at > LEFT_KEPT_MS) await this.forget(group.id);
|
|
@@ -25810,12 +26737,25 @@ var Groups = class {
|
|
|
25810
26737
|
const linkId = this.host.entries(group.id).get(key);
|
|
25811
26738
|
if (linkId) await this.host.closeEdge(linkId);
|
|
25812
26739
|
}
|
|
25813
|
-
const
|
|
25814
|
-
|
|
26740
|
+
const busy = (pending?.size ?? 0) >= MAX_PENDING_ENTRIES || session.roster.length + (pending?.size ?? 0) >= GROUP_MEMBER_CAP.max;
|
|
26741
|
+
const since = now - (this.lastPoll.get(group.id) ?? 0);
|
|
26742
|
+
if (busy) continue;
|
|
26743
|
+
if (now >= (this.warmUntil.get(group.id) ?? 0)) {
|
|
26744
|
+
if (since >= this.timings.pollMs) quiet.push(group.id);
|
|
26745
|
+
} else if (since >= this.timings.warmPollMs) {
|
|
25815
26746
|
this.lastPoll.set(group.id, now);
|
|
25816
26747
|
await this.answerKnocks(group, session, now).catch(() => {});
|
|
25817
26748
|
}
|
|
25818
26749
|
}
|
|
26750
|
+
if (quiet.length && now - this.lastQuietPoll >= this.timings.pollMs) {
|
|
26751
|
+
const groupId = quiet.reduce((a, b) => (this.lastPoll.get(b) ?? 0) < (this.lastPoll.get(a) ?? 0) ? b : a);
|
|
26752
|
+
const group = this.stored.get(groupId), session = this.sessions.get(groupId);
|
|
26753
|
+
if (group?.entry && !group.left && session?.isAdmin) {
|
|
26754
|
+
this.lastQuietPoll = now;
|
|
26755
|
+
this.lastPoll.set(groupId, now);
|
|
26756
|
+
await this.answerKnocks(group, session, now).catch(() => {});
|
|
26757
|
+
}
|
|
26758
|
+
}
|
|
25819
26759
|
for (const [key, until] of this.refused) if (until <= now) this.refused.delete(key);
|
|
25820
26760
|
for (const [key, { at }] of this.entryTimeouts) if (now - at > 36e5) this.entryTimeouts.delete(key);
|
|
25821
26761
|
for (const [groupId, kept] of this.lingering) if (now >= kept.until) await this.endLinger(groupId);
|
|
@@ -25919,7 +26859,16 @@ var Groups = class {
|
|
|
25919
26859
|
/** The member at the other end of an entry session (the joiner, on the admin's side) or of a chat an invitation went over. */
|
|
25920
26860
|
linkMember(groupId, linkId) {
|
|
25921
26861
|
for (const [peer, id] of this.host.entries(groupId)) if (id === linkId) return peer;
|
|
25922
|
-
return
|
|
26862
|
+
return this.chatMember(groupId, linkId);
|
|
26863
|
+
}
|
|
26864
|
+
/**
|
|
26865
|
+
* The member a chat an invitation went over is: one removed and invited back over the same chat has its old key on it
|
|
26866
|
+
* too (`contacts` keeps it for the removal notice), so the key in the roster, else the newest.
|
|
26867
|
+
*/
|
|
26868
|
+
chatMember(groupId, linkId) {
|
|
26869
|
+
const keys = Object.entries(this.stored.get(groupId)?.contacts ?? {}).filter(([, id]) => id === linkId).map(([key]) => key);
|
|
26870
|
+
const roster = this.sessions.get(groupId)?.roster;
|
|
26871
|
+
return roster && keys.find((key) => rosterHas(roster, key)) || keys[keys.length - 1];
|
|
25923
26872
|
}
|
|
25924
26873
|
/**
|
|
25925
26874
|
* My packet for the edge to `to`, handed to links that reach that member or may pass it on (WISP 902 · Group Mesh
|
|
@@ -26312,8 +27261,7 @@ var Groups = class {
|
|
|
26312
27261
|
}
|
|
26313
27262
|
case "group-leave": {
|
|
26314
27263
|
const session = this.sessions.get(g);
|
|
26315
|
-
const
|
|
26316
|
-
const member = Object.entries(group?.contacts ?? {}).find(([, id]) => id === linkId)?.[0];
|
|
27264
|
+
const member = this.chatMember(g, linkId);
|
|
26317
27265
|
if (!session?.isAdmin || !member || member === session.myKey) return;
|
|
26318
27266
|
if (rosterHas(session.roster, member)) await session.remove(member, Date.now(), { left: true }).catch(() => {});
|
|
26319
27267
|
try {
|
|
@@ -26457,6 +27405,9 @@ var Groups = class {
|
|
|
26457
27405
|
return;
|
|
26458
27406
|
}
|
|
26459
27407
|
if (!session || !group || session.status !== "active" || !rosterHas(session.roster, peerKey)) return;
|
|
27408
|
+
let told = this.told.get(groupId);
|
|
27409
|
+
if (!told) this.told.set(groupId, told = /* @__PURE__ */ new Set());
|
|
27410
|
+
told.add(peerKey);
|
|
26460
27411
|
const seen = !!group.unseen?.includes(peerKey);
|
|
26461
27412
|
if (seen) {
|
|
26462
27413
|
const unseen = group.unseen.filter((key) => key !== peerKey);
|
|
@@ -26466,7 +27417,7 @@ var Groups = class {
|
|
|
26466
27417
|
const legacy = this.hubs.enabled && this.hubs.edgeReady(groupId, session, group, peerKey, this.host.linkReady(linkId, 4), this.now());
|
|
26467
27418
|
if (large || legacy || seen) this.store.putGroup(group).catch(() => {});
|
|
26468
27419
|
if (!this.holdSync(groupId, peerKey)) try {
|
|
26469
|
-
this.host.sendOnLink(linkId, session.syncFrame(this.askOf(groupId, session, peerKey)));
|
|
27420
|
+
this.host.sendOnLink(linkId, session.syncFrame(this.askOf(groupId, session, peerKey), peerKey));
|
|
26470
27421
|
} catch {
|
|
26471
27422
|
return;
|
|
26472
27423
|
}
|
|
@@ -26602,18 +27553,21 @@ var Groups = class {
|
|
|
26602
27553
|
}
|
|
26603
27554
|
/**
|
|
26604
27555
|
* The unreachable members whose messages `via` should hand on to me: each is asked of one connected member at a
|
|
26605
|
-
* time, so a member whose edges open one after another is not sent the same messages by each of them.
|
|
27556
|
+
* time, so a member whose edges open one after another is not sent the same messages by each of them. Those whose
|
|
27557
|
+
* edge opened in the same moment, not told yet, are asked of `via` too, and their syncs wait for its answer
|
|
27558
|
+
* (`holdSync`): each told at once answered with its own lines alone, and a conversation two of them had while I was
|
|
27559
|
+
* away came as all of one's lines, then all of the other's.
|
|
26606
27560
|
*/
|
|
26607
27561
|
askOf(groupId, session, via) {
|
|
26608
27562
|
let asked = this.relayAsked.get(groupId);
|
|
26609
27563
|
if (!asked) this.relayAsked.set(groupId, asked = /* @__PURE__ */ new Map());
|
|
26610
|
-
const edges = this.host.edges(groupId), now = this.now();
|
|
27564
|
+
const edges = this.host.edges(groupId), now = this.now(), told = this.told.get(groupId);
|
|
26611
27565
|
const up = (key) => {
|
|
26612
27566
|
const edge = edges.get(key);
|
|
26613
27567
|
return !!edge && this.host.linkReady(edge);
|
|
26614
27568
|
};
|
|
26615
27569
|
const out = [];
|
|
26616
|
-
for (const key of
|
|
27570
|
+
for (const key of session.others.filter((key) => !up(key) || !told?.has(key))) {
|
|
26617
27571
|
if (key === via) continue;
|
|
26618
27572
|
const prior = asked.get(key);
|
|
26619
27573
|
if (prior && up(prior.via) && now - prior.at < 6e4) continue;
|
|
@@ -26694,7 +27648,7 @@ var Groups = class {
|
|
|
26694
27648
|
});
|
|
26695
27649
|
}
|
|
26696
27650
|
try {
|
|
26697
|
-
this.host.sendOnLink(edges.get(next), session.syncFrame([...ask]));
|
|
27651
|
+
this.host.sendOnLink(edges.get(next), session.syncFrame([...ask], next));
|
|
26698
27652
|
} catch {}
|
|
26699
27653
|
}
|
|
26700
27654
|
/**
|
|
@@ -26715,9 +27669,9 @@ var Groups = class {
|
|
|
26715
27669
|
for (const key of tried.keys()) if (!lacking.has(key)) tried.delete(key);
|
|
26716
27670
|
const out = [];
|
|
26717
27671
|
for (const [key, mark] of lacking) {
|
|
26718
|
-
if (key === via || !up(key)) continue;
|
|
27672
|
+
if (key === via || !up(key) || !session.mayHold(via, key)) continue;
|
|
26719
27673
|
const prior = asked.get(key);
|
|
26720
|
-
if (prior && prior.via !== via && up(prior.via) && now - prior.at < 6e4) continue;
|
|
27674
|
+
if (prior && prior.via !== via && up(prior.via) && now - prior.at < 6e4 && session.mayHold(prior.via, key)) continue;
|
|
26721
27675
|
let entry = tried.get(key);
|
|
26722
27676
|
if (!entry || entry.mark !== mark) tried.set(key, entry = {
|
|
26723
27677
|
mark,
|
|
@@ -26763,6 +27717,11 @@ var Groups = class {
|
|
|
26763
27717
|
entry.at = now;
|
|
26764
27718
|
return true;
|
|
26765
27719
|
}
|
|
27720
|
+
/** Whether some of a member's messages are still on their way (not known gone): in any epoch, or in those below `below`. */
|
|
27721
|
+
onTheirWay(groupId, sender, below = Infinity) {
|
|
27722
|
+
const gone = this.gapsGone.get(groupId)?.get(sender);
|
|
27723
|
+
return Object.entries(this.sessions.get(groupId)?.missOf(sender) ?? {}).some(([e, gaps]) => Number(e) < below && gaps.some((n) => n > (gone?.[e] ?? -1)));
|
|
27724
|
+
}
|
|
26766
27725
|
/**
|
|
26767
27726
|
* Messages nobody holds any more: every connected member, their author included, was asked for the same missing
|
|
26768
27727
|
* numbers `MESH_GAP_AGAIN_MS` ago or more and none arrived. An author keeps its last 32 and the others the group's
|
|
@@ -26776,7 +27735,7 @@ var Groups = class {
|
|
|
26776
27735
|
for (const [key, mark] of lacking) {
|
|
26777
27736
|
const entry = tried.get(key);
|
|
26778
27737
|
if (!entry || entry.mark !== mark || entry.at === void 0 || now - entry.at < 1e4) continue;
|
|
26779
|
-
if (!connected.includes(key) || !connected.every((member) => entry.of.has(member))) continue;
|
|
27738
|
+
if (!connected.includes(key) || !connected.every((member) => entry.of.has(member) || !session.mayHold(member, key))) continue;
|
|
26780
27739
|
let gone = this.gapsGone.get(groupId);
|
|
26781
27740
|
if (!gone) this.gapsGone.set(groupId, gone = /* @__PURE__ */ new Map());
|
|
26782
27741
|
const upTo = { ...gone.get(key) };
|
|
@@ -26826,7 +27785,9 @@ var Groups = class {
|
|
|
26826
27785
|
continue;
|
|
26827
27786
|
}
|
|
26828
27787
|
if (now - last.at < 1e4 || !up(last.via)) continue;
|
|
26829
|
-
if (session.filledSince(key, last.asked))
|
|
27788
|
+
if (!session.filledSince(key, last.asked)) continue;
|
|
27789
|
+
if (session.mayHold(last.via, key)) again.set(last.via, [...again.get(last.via) ?? [], key]);
|
|
27790
|
+
else fresh.set(key, mark);
|
|
26830
27791
|
}
|
|
26831
27792
|
const asked = this.relayAsked.get(groupId);
|
|
26832
27793
|
for (const [via, keys] of again) {
|
|
@@ -26847,18 +27808,19 @@ var Groups = class {
|
|
|
26847
27808
|
});
|
|
26848
27809
|
}
|
|
26849
27810
|
try {
|
|
26850
|
-
this.host.sendOnLink(edges.get(via), session.syncFrame(keys));
|
|
27811
|
+
this.host.sendOnLink(edges.get(via), session.syncFrame(keys, via));
|
|
26851
27812
|
} catch {}
|
|
26852
27813
|
}
|
|
26853
27814
|
if (!fresh.size || !connected.length) return;
|
|
26854
27815
|
const at = () => connected[(gossipStart(session.myKey) + this.gossipTurn++) % connected.length];
|
|
27816
|
+
const helps = (member) => !fresh.has(member) && [...fresh.keys()].some((key) => session.mayHold(member, key));
|
|
26855
27817
|
let via = at();
|
|
26856
|
-
for (let i = 1; i < connected.length &&
|
|
27818
|
+
for (let i = 1; i < connected.length && !helps(via); i++) via = at();
|
|
26857
27819
|
const keys = this.gapsOf(groupId, session, via, fresh);
|
|
26858
27820
|
if (!keys.length) return;
|
|
26859
27821
|
this.lastGossip.set(groupId, now);
|
|
26860
27822
|
try {
|
|
26861
|
-
this.host.sendOnLink(edges.get(via), session.syncFrame(keys));
|
|
27823
|
+
this.host.sendOnLink(edges.get(via), session.syncFrame(keys, via));
|
|
26862
27824
|
} catch {}
|
|
26863
27825
|
}
|
|
26864
27826
|
/**
|
|
@@ -26880,9 +27842,12 @@ var Groups = class {
|
|
|
26880
27842
|
const edges = this.host.edges(groupId);
|
|
26881
27843
|
if (!this.hubs.large(session) && session.others.some((key) => !edges.has(key))) this.reconcileEdges(groupId);
|
|
26882
27844
|
}
|
|
26883
|
-
const
|
|
26884
|
-
this.noteDown(groupId, session,
|
|
26885
|
-
const
|
|
27845
|
+
const down = this.unreachable(groupId, session);
|
|
27846
|
+
this.noteDown(groupId, session, down, now);
|
|
27847
|
+
const told = this.told.get(groupId);
|
|
27848
|
+
for (const key of down) told?.delete(key);
|
|
27849
|
+
const connected = session.others.filter((key) => !down.includes(key) && told?.has(key));
|
|
27850
|
+
const away = session.others.filter((key) => !connected.includes(key));
|
|
26886
27851
|
this.gapsAgain(groupId, session, now, connected);
|
|
26887
27852
|
if (now - (this.lastGossip.get(groupId) ?? -Infinity) < 6e4) continue;
|
|
26888
27853
|
const edges = this.host.edges(groupId);
|
|
@@ -26900,7 +27865,7 @@ var Groups = class {
|
|
|
26900
27865
|
const ask = [...away, ...this.gapsOf(groupId, session, via, lacking)];
|
|
26901
27866
|
if (!this.ownGap(groupId, via, lacking) && !ask.length) continue;
|
|
26902
27867
|
try {
|
|
26903
|
-
this.host.sendOnLink(edges.get(via), session.syncFrame(ask));
|
|
27868
|
+
this.host.sendOnLink(edges.get(via), session.syncFrame(ask, via));
|
|
26904
27869
|
} catch {}
|
|
26905
27870
|
}
|
|
26906
27871
|
}
|
|
@@ -27004,21 +27969,27 @@ var Groups = class {
|
|
|
27004
27969
|
...m.frame && { frame: m.frame }
|
|
27005
27970
|
};
|
|
27006
27971
|
const completed = !!m.completes && !!this.host.completeMessage;
|
|
27972
|
+
const row = {
|
|
27973
|
+
sender: m.sender,
|
|
27974
|
+
epoch: m.epoch,
|
|
27975
|
+
seq: m.seq,
|
|
27976
|
+
id: m.id
|
|
27977
|
+
};
|
|
27978
|
+
const above = completed || m.sender === session.myKey ? void 0 : this.authorOrder.above(state.id, row);
|
|
27979
|
+
const place = above === void 0 ? void 0 : await this.host.placeAbove?.(message.linkId, above).catch(() => void 0);
|
|
27980
|
+
const placedAbove = place === void 0 ? void 0 : above;
|
|
27007
27981
|
const placing = completed ? void 0 : this.lineOrder.placing(MESSAGE_LINK(state.id), {
|
|
27008
27982
|
id: m.id,
|
|
27009
27983
|
epoch: m.epoch,
|
|
27010
27984
|
line: false
|
|
27011
|
-
}, this.now());
|
|
27012
|
-
const stored = completed ? (await this.host.completeMessage(message), false) : await this.host.storeMessage(message);
|
|
27985
|
+
}, this.now(), placedAbove);
|
|
27986
|
+
const stored = completed ? (await this.host.completeMessage(message), false) : await this.host.storeMessage(message, m.live, place);
|
|
27013
27987
|
placing?.(stored !== false);
|
|
27014
27988
|
if (m.frame && (completed || stored !== false)) await this.frameKept(state.id, m.id, m.frame);
|
|
27015
27989
|
if (!completed && stored !== false) this.authorOrder.placed(state.id, {
|
|
27016
|
-
|
|
27017
|
-
epoch: m.epoch,
|
|
27018
|
-
seq: m.seq,
|
|
27019
|
-
id: m.id,
|
|
27990
|
+
...row,
|
|
27020
27991
|
at: this.now()
|
|
27021
|
-
});
|
|
27992
|
+
}, placedAbove);
|
|
27022
27993
|
const came = m.sender === session.myKey ? timestamp : cameAt(timestamp, stored, arrivalNow(this.now()));
|
|
27023
27994
|
this.lastMessageAt.set(state.id, Math.max(this.lastMessageAt.get(state.id) ?? 0, came));
|
|
27024
27995
|
if (mentioned) this.lastMentionAt.set(state.id, Math.max(this.lastMentionAt.get(state.id) ?? 0, came));
|
|
@@ -30241,7 +31212,10 @@ var SWAP_POLL_MS = 3e4;
|
|
|
30241
31212
|
var SWAP_SLOW_POLL_MS = 6e5;
|
|
30242
31213
|
/** How long a swap whose inputs were given back is still asked about, in case the mint made it after all. */
|
|
30243
31214
|
var SWAP_WATCH_MS = 36e5;
|
|
30244
|
-
|
|
31215
|
+
/** Cashu's library (174 KiB) loads with the wallet's first use of a mint or a token, not with the app's start. */
|
|
31216
|
+
var loadCashu = () => import("@cashu/cashu-ts");
|
|
31217
|
+
async function reviewedCashuFee(preview) {
|
|
31218
|
+
const { Amount } = await loadCashu();
|
|
30245
31219
|
const fee = preview.inputs.reduce((sum, proof) => sum + Amount.from(proof.amount).toNumber(), 0) - (preview.keepOutputs ?? []).reduce((sum, output) => sum + Amount.from(output.blindedMessage.amount).toNumber(), 0) - preview.amount.toNumber();
|
|
30246
31220
|
if (!Number.isSafeInteger(fee) || fee < 0) throw new Error("Invalid Cashu prepared fee");
|
|
30247
31221
|
return fee;
|
|
@@ -30297,9 +31271,9 @@ var toStored = (mint, p, reserved) => ({
|
|
|
30297
31271
|
});
|
|
30298
31272
|
/** The note of the ecash "Get test coins" brought (the engine plays its own sound for it). */
|
|
30299
31273
|
var TEST_COINS_NOTE = "Test coins from the test mint";
|
|
30300
|
-
var swapId = () => `swap-${Date.now()}-${
|
|
31274
|
+
var swapId = () => `swap-${Date.now()}-${crypto.randomUUID().slice(0, 8)}`;
|
|
30301
31275
|
var walletTx = (mint, kind, amount, fee, note) => ({
|
|
30302
|
-
id: `${Date.now()}-${
|
|
31276
|
+
id: `${Date.now()}-${crypto.randomUUID().slice(0, 8)}`,
|
|
30303
31277
|
timestamp: Date.now(),
|
|
30304
31278
|
mint,
|
|
30305
31279
|
kind,
|
|
@@ -30608,6 +31582,7 @@ var CashuWallet = class CashuWallet {
|
|
|
30608
31582
|
const quote = await wrap((await store(STORES$1.quotes, "readonly")).get(id));
|
|
30609
31583
|
if (!quote || quote.issuedUnclaimed || quote.held) return null;
|
|
30610
31584
|
const wallet = await this.wallet(quote.mint);
|
|
31585
|
+
const { OutputData, isMintOperationError } = await loadCashu();
|
|
30611
31586
|
const answer = await wallet.checkMintQuoteBolt11(quote.quote);
|
|
30612
31587
|
const { state } = answer;
|
|
30613
31588
|
if (state === "UNPAID") {
|
|
@@ -30651,7 +31626,10 @@ var CashuWallet = class CashuWallet {
|
|
|
30651
31626
|
/** The ecash of a paid invoice goes in with its history line, in the transaction that drops the quote. */
|
|
30652
31627
|
async storeMinted(quote, proofs) {
|
|
30653
31628
|
const minted = sats(proofs);
|
|
30654
|
-
const tx =
|
|
31629
|
+
const tx = {
|
|
31630
|
+
...walletTx(quote.mint, "lightning-in", minted, quote.amount - minted, quote.paymentId ? "Request paid over Lightning" : quote.testCoins ? TEST_COINS_NOTE : void 0),
|
|
31631
|
+
quote: quote.quote
|
|
31632
|
+
};
|
|
30655
31633
|
await transact([
|
|
30656
31634
|
STORES$1.proofs,
|
|
30657
31635
|
STORES$1.walletTx,
|
|
@@ -30687,7 +31665,7 @@ var CashuWallet = class CashuWallet {
|
|
|
30687
31665
|
if (await this.balanceAt(mint) < amount) continue;
|
|
30688
31666
|
return this.locked(mint, async () => {
|
|
30689
31667
|
const { inputs, keep, send, swap } = await this.split(mint, amount, await this.proofsAt(mint));
|
|
30690
|
-
const token = getEncodedToken({
|
|
31668
|
+
const token = (await loadCashu()).getEncodedToken({
|
|
30691
31669
|
mint,
|
|
30692
31670
|
proofs: send,
|
|
30693
31671
|
unit: UNIT$1
|
|
@@ -30721,10 +31699,10 @@ var CashuWallet = class CashuWallet {
|
|
|
30721
31699
|
return this.locked(mint, async () => {
|
|
30722
31700
|
const preview = await (await this.wallet(mint)).ops.send(amount, asProofLike(await this.proofsAt(mint))).includeFees(true).prepare();
|
|
30723
31701
|
return {
|
|
30724
|
-
fee: reviewedCashuFee(preview),
|
|
31702
|
+
fee: await reviewedCashuFee(preview),
|
|
30725
31703
|
prepared: {
|
|
30726
31704
|
mint,
|
|
30727
|
-
swap: serializeSwapPreview(preview)
|
|
31705
|
+
swap: (await loadCashu()).serializeSwapPreview(preview)
|
|
30728
31706
|
}
|
|
30729
31707
|
};
|
|
30730
31708
|
});
|
|
@@ -30732,8 +31710,8 @@ var CashuWallet = class CashuWallet {
|
|
|
30732
31710
|
/** Reserve before the mint call. A timeout keeps these inputs reserved for read-only recovery. */
|
|
30733
31711
|
async executeReviewedCashu(review, prepared) {
|
|
30734
31712
|
return this.locked(prepared.mint, async () => {
|
|
30735
|
-
const preview = deserializeSwapPreview(prepared.swap);
|
|
30736
|
-
if (prepared.mint !== review.provider || preview.amount.toNumber() !== review.amount || reviewedCashuFee(preview) !== review.fee) throw new PaymentPreflightError("Cashu preview does not match review");
|
|
31713
|
+
const preview = (await loadCashu()).deserializeSwapPreview(prepared.swap);
|
|
31714
|
+
if (prepared.mint !== review.provider || preview.amount.toNumber() !== review.amount || await reviewedCashuFee(preview) !== review.fee) throw new PaymentPreflightError("Cashu preview does not match review");
|
|
30737
31715
|
const tx = (await openDb()).transaction(STORES$1.proofs, "readwrite");
|
|
30738
31716
|
await new Promise((resolve, reject) => {
|
|
30739
31717
|
const proofs = tx.objectStore(STORES$1.proofs);
|
|
@@ -30763,6 +31741,7 @@ var CashuWallet = class CashuWallet {
|
|
|
30763
31741
|
const stored = await wrap((await store(STORES$1.payments, "readonly")).get(review.id));
|
|
30764
31742
|
if (stored?.token) return stored.token;
|
|
30765
31743
|
const wallet = await this.wallet(prepared.mint);
|
|
31744
|
+
const { OutputData } = await loadCashu();
|
|
30766
31745
|
const keep = (prepared.swap.keepOutputs ?? []).map(OutputData.deserialize);
|
|
30767
31746
|
const send = (prepared.swap.sendOutputs ?? []).map(OutputData.deserialize);
|
|
30768
31747
|
const outputs = [...keep, ...send];
|
|
@@ -30792,6 +31771,7 @@ var CashuWallet = class CashuWallet {
|
|
|
30792
31771
|
if (recovered.outputs.length === 0 && recovered.signatures.length === 0) return "none";
|
|
30793
31772
|
if (recovered.outputs.length !== outputs.length || recovered.signatures.length !== outputs.length) return "some";
|
|
30794
31773
|
const keys = (await wallet.mint.getKeys()).keysets;
|
|
31774
|
+
const { Amount } = await loadCashu();
|
|
30795
31775
|
const restored = [];
|
|
30796
31776
|
for (const output of outputs) {
|
|
30797
31777
|
const index = recovered.outputs.findIndex((o) => o.B_ === output.blindedMessage.B_);
|
|
@@ -30803,6 +31783,7 @@ var CashuWallet = class CashuWallet {
|
|
|
30803
31783
|
return restored;
|
|
30804
31784
|
}
|
|
30805
31785
|
async finishReviewedCashu(review, prepared, keep, send) {
|
|
31786
|
+
const { getEncodedToken, deserializeSwapPreview } = await loadCashu();
|
|
30806
31787
|
const token = getEncodedToken({
|
|
30807
31788
|
mint: prepared.mint,
|
|
30808
31789
|
proofs: send,
|
|
@@ -30845,6 +31826,7 @@ var CashuWallet = class CashuWallet {
|
|
|
30845
31826
|
* not SPENT), and the mint signed none of its outputs. Read-only: it asks, and never sends the swap.
|
|
30846
31827
|
*/
|
|
30847
31828
|
async reviewedCashuNeverSwapped(prepared) {
|
|
31829
|
+
const { OutputData, deserializeSwapPreview } = await loadCashu();
|
|
30848
31830
|
const preview = deserializeSwapPreview(prepared.swap);
|
|
30849
31831
|
if (!preview.inputs.length) return false;
|
|
30850
31832
|
const wallet = await this.wallet(prepared.mint);
|
|
@@ -30863,6 +31845,7 @@ var CashuWallet = class CashuWallet {
|
|
|
30863
31845
|
* pending. Read-only. A mint that does not restore cannot say, and the answer is no.
|
|
30864
31846
|
*/
|
|
30865
31847
|
async reviewedCashuSpentElsewhere(prepared) {
|
|
31848
|
+
const { OutputData, deserializeSwapPreview } = await loadCashu();
|
|
30866
31849
|
const preview = deserializeSwapPreview(prepared.swap);
|
|
30867
31850
|
if (!preview.inputs.length) return false;
|
|
30868
31851
|
const wallet = await this.wallet(prepared.mint);
|
|
@@ -30880,7 +31863,7 @@ var CashuWallet = class CashuWallet {
|
|
|
30880
31863
|
* are free again, and the rest of the wallet's ecash at that mint is checked the same way. Idempotent.
|
|
30881
31864
|
*/
|
|
30882
31865
|
async dropReviewedCashu(prepared) {
|
|
30883
|
-
const preview = deserializeSwapPreview(prepared.swap);
|
|
31866
|
+
const preview = (await loadCashu()).deserializeSwapPreview(prepared.swap);
|
|
30884
31867
|
await this.locked(prepared.mint, async () => {
|
|
30885
31868
|
const states = await (await this.wallet(prepared.mint)).checkProofsStates(preview.inputs.map((p) => ({
|
|
30886
31869
|
secret: p.secret,
|
|
@@ -30925,7 +31908,7 @@ var CashuWallet = class CashuWallet {
|
|
|
30925
31908
|
}
|
|
30926
31909
|
/** Gives back the inputs a reviewed swap reserved, once the mint proved it never happened. Idempotent. */
|
|
30927
31910
|
async releaseReviewedCashu(prepared) {
|
|
30928
|
-
const preview = deserializeSwapPreview(prepared.swap);
|
|
31911
|
+
const preview = (await loadCashu()).deserializeSwapPreview(prepared.swap);
|
|
30929
31912
|
await this.locked(prepared.mint, async () => {
|
|
30930
31913
|
await transact([STORES$1.proofs], (stores) => {
|
|
30931
31914
|
const proofs = stores[STORES$1.proofs];
|
|
@@ -30950,7 +31933,7 @@ var CashuWallet = class CashuWallet {
|
|
|
30950
31933
|
return (await wrap((await store(STORES$1.payments, "readonly")).get(id)))?.state === "reclaimed";
|
|
30951
31934
|
}
|
|
30952
31935
|
async reviewedCashuSpent(prepared) {
|
|
30953
|
-
const outputs = (prepared.swap.sendOutputs ?? []).map(OutputData.deserialize);
|
|
31936
|
+
const outputs = (prepared.swap.sendOutputs ?? []).map((await loadCashu()).OutputData.deserialize);
|
|
30954
31937
|
const states = await (await this.wallet(prepared.mint)).checkProofsStates(outputs.map((o) => ({
|
|
30955
31938
|
secret: new TextDecoder().decode(o.secret),
|
|
30956
31939
|
id: o.blindedMessage.id
|
|
@@ -30958,8 +31941,10 @@ var CashuWallet = class CashuWallet {
|
|
|
30958
31941
|
return states.length > 0 && states.length === outputs.length && states.every((s) => s.state === "SPENT");
|
|
30959
31942
|
}
|
|
30960
31943
|
/** Reads a token or a payment request without contacting a mint. Nothing here moves money. */
|
|
30961
|
-
inspect(text) {
|
|
31944
|
+
async inspect(text) {
|
|
30962
31945
|
const value = text.trim();
|
|
31946
|
+
if (!/^(cashu|creq)[AB]/i.test(value)) return null;
|
|
31947
|
+
const { decodePaymentRequest, getTokenMetadata } = await loadCashu();
|
|
30963
31948
|
try {
|
|
30964
31949
|
if (/^cashu[AB]/i.test(value)) {
|
|
30965
31950
|
const metadata = getTokenMetadata(value);
|
|
@@ -30994,6 +31979,7 @@ var CashuWallet = class CashuWallet {
|
|
|
30994
31979
|
let mint;
|
|
30995
31980
|
let faceValue;
|
|
30996
31981
|
let unit;
|
|
31982
|
+
const { getTokenMetadata, serializeSwapPreview } = await loadCashu();
|
|
30997
31983
|
try {
|
|
30998
31984
|
const metadata = getTokenMetadata(token);
|
|
30999
31985
|
unit = metadata.unit;
|
|
@@ -31064,6 +32050,7 @@ var CashuWallet = class CashuWallet {
|
|
|
31064
32050
|
*/
|
|
31065
32051
|
async split(mint, amount, proofs) {
|
|
31066
32052
|
const wallet = await this.wallet(mint);
|
|
32053
|
+
const { getEncodedToken, serializeSwapPreview } = await loadCashu();
|
|
31067
32054
|
const among = (chosen) => {
|
|
31068
32055
|
const secrets = new Set(chosen.map((p) => p.secret));
|
|
31069
32056
|
return proofs.filter((p) => secrets.has(p.secret));
|
|
@@ -31115,6 +32102,7 @@ var CashuWallet = class CashuWallet {
|
|
|
31115
32102
|
* `pollSwaps` settles it. Runs under the mint's lock.
|
|
31116
32103
|
*/
|
|
31117
32104
|
async completeSwap(swap) {
|
|
32105
|
+
const { deserializeSwapPreview, isMintOperationError } = await loadCashu();
|
|
31118
32106
|
try {
|
|
31119
32107
|
return await (await this.wallet(swap.mint)).completeSwap(deserializeSwapPreview(swapOf(swap)));
|
|
31120
32108
|
} catch (error) {
|
|
@@ -31142,6 +32130,7 @@ var CashuWallet = class CashuWallet {
|
|
|
31142
32130
|
*/
|
|
31143
32131
|
async swapOutcome(swap, refused = false) {
|
|
31144
32132
|
const wallet = await this.wallet(swap.mint);
|
|
32133
|
+
const { OutputData } = await loadCashu();
|
|
31145
32134
|
const saved = swapOf(swap);
|
|
31146
32135
|
const keep = (saved.keepOutputs ?? []).map(OutputData.deserialize);
|
|
31147
32136
|
const send = (saved.sendOutputs ?? []).map(OutputData.deserialize);
|
|
@@ -31300,6 +32289,15 @@ var CashuWallet = class CashuWallet {
|
|
|
31300
32289
|
settleSwaps(mints) {
|
|
31301
32290
|
return this.pollSwaps(mints);
|
|
31302
32291
|
}
|
|
32292
|
+
/**
|
|
32293
|
+
* A contact's ecash for this request of ours is being redeemed: written down, and the mint has not said how it ended,
|
|
32294
|
+
* or it has and its chat was not told yet. Released: the mint never made it in time, and the ecash went back. Stuck
|
|
32295
|
+
* past its watch hour (spent, and a mint that cannot say by what): kept to be asked about, but no longer expected to
|
|
32296
|
+
* pay the request, which another member may pay.
|
|
32297
|
+
*/
|
|
32298
|
+
async redeemingFor(requestId) {
|
|
32299
|
+
return (await wrap((await store(STORES$1.swaps, "readonly")).getAll())).some((s) => s.kind === "receive" && !s.released && s.payment?.requestId === requestId && !(s.stuck && Date.now() >= s.createdAt + SWAP_WATCH_MS));
|
|
32300
|
+
}
|
|
31303
32301
|
async pollSwaps(mints) {
|
|
31304
32302
|
if (!mints) {
|
|
31305
32303
|
if (this.swapTimer) clearTimeout(this.swapTimer);
|
|
@@ -31347,34 +32345,36 @@ var CashuWallet = class CashuWallet {
|
|
|
31347
32345
|
}));
|
|
31348
32346
|
if (stuck === 1) this.events.onChange();
|
|
31349
32347
|
later(wait);
|
|
31350
|
-
} else if (outcome.idle && swap.kind === "receive" && !swap.released)
|
|
31351
|
-
const {
|
|
31352
|
-
|
|
31353
|
-
const
|
|
31354
|
-
if (
|
|
31355
|
-
else if (swap.payment) later(SWAP_POLL_MS);
|
|
31356
|
-
this.events.onChange();
|
|
31357
|
-
}
|
|
31358
|
-
} catch (error) {
|
|
31359
|
-
swap = {
|
|
31360
|
-
...swap,
|
|
31361
|
-
attemptEndedAt: Date.now()
|
|
31362
|
-
};
|
|
31363
|
-
await wrap((await store(STORES$1.swaps, "readwrite")).put(swap));
|
|
31364
|
-
const refusal = isMintOperationError(error) ? await this.swapOutcome(swap, true).catch(() => void 0) : void 0;
|
|
31365
|
-
if (refusal?.state === "done") {
|
|
31366
|
-
if (await this.finishSwap(swap, refusal.keep, refusal.send, true)) {
|
|
32348
|
+
} else if (outcome.idle && swap.kind === "receive" && !swap.released) {
|
|
32349
|
+
const { deserializeSwapPreview, isMintOperationError } = await loadCashu();
|
|
32350
|
+
try {
|
|
32351
|
+
const { keep } = await (await this.wallet(mint)).completeSwap(deserializeSwapPreview(swapOf(swap)));
|
|
32352
|
+
if (await this.finishSwap(swap, keep, [], true)) {
|
|
31367
32353
|
const told = await this.tellSwap(swap, true);
|
|
31368
32354
|
if (swap.payment && told) await wrap((await store(STORES$1.swaps, "readwrite")).delete(swap.id));
|
|
32355
|
+
else if (swap.payment) later(SWAP_POLL_MS);
|
|
32356
|
+
this.events.onChange();
|
|
31369
32357
|
}
|
|
31370
|
-
|
|
31371
|
-
|
|
31372
|
-
|
|
31373
|
-
|
|
31374
|
-
|
|
31375
|
-
|
|
31376
|
-
|
|
31377
|
-
|
|
32358
|
+
} catch (error) {
|
|
32359
|
+
swap = {
|
|
32360
|
+
...swap,
|
|
32361
|
+
attemptEndedAt: Date.now()
|
|
32362
|
+
};
|
|
32363
|
+
await wrap((await store(STORES$1.swaps, "readwrite")).put(swap));
|
|
32364
|
+
const refusal = isMintOperationError(error) ? await this.swapOutcome(swap, true).catch(() => void 0) : void 0;
|
|
32365
|
+
if (refusal?.state === "done") {
|
|
32366
|
+
if (await this.finishSwap(swap, refusal.keep, refusal.send, true)) {
|
|
32367
|
+
const told = await this.tellSwap(swap, true);
|
|
32368
|
+
if (swap.payment && told) await wrap((await store(STORES$1.swaps, "readwrite")).delete(swap.id));
|
|
32369
|
+
}
|
|
32370
|
+
this.events.onChange();
|
|
32371
|
+
} else if (refusal?.state === "never") {
|
|
32372
|
+
await this.dropSwap(swap, refusal.gone);
|
|
32373
|
+
await this.tellSwap(swap, false);
|
|
32374
|
+
this.events.onChange();
|
|
32375
|
+
} else later(SWAP_POLL_MS);
|
|
32376
|
+
}
|
|
32377
|
+
} else if (outcome.pending && swap.kind === "receive" && !swap.released && Date.now() >= swapOverdueAt(swap)) {
|
|
31378
32378
|
if (Date.now() < swap.createdAt + SWAP_WATCH_MS) {
|
|
31379
32379
|
await wrap((await store(STORES$1.swaps, "readwrite")).put({
|
|
31380
32380
|
...swap,
|
|
@@ -31468,6 +32468,7 @@ var CashuWallet = class CashuWallet {
|
|
|
31468
32468
|
}
|
|
31469
32469
|
async melt(quoteId, mint, note, paymentId) {
|
|
31470
32470
|
const wallet = await this.wallet(mint);
|
|
32471
|
+
const { MeltChangeError, OutputData, isMintOperationError } = await loadCashu();
|
|
31471
32472
|
const quote = await wallet.checkMeltQuoteBolt11(quoteId);
|
|
31472
32473
|
if ((await wrap((await store(STORES$1.melts, "readonly")).getAll())).some((m) => m.quote === quote.quote || m.request === quote.request)) throw new Error("This invoice is already being paid");
|
|
31473
32474
|
const shown = this.shownQuotes.get(quoteId);
|
|
@@ -31561,6 +32562,7 @@ var CashuWallet = class CashuWallet {
|
|
|
31561
32562
|
*/
|
|
31562
32563
|
async settleMelt(melt) {
|
|
31563
32564
|
const wallet = await this.wallet(melt.mint);
|
|
32565
|
+
const { Amount, OutputData } = await loadCashu();
|
|
31564
32566
|
const quote = await wallet.checkMeltQuoteBolt11(melt.quote);
|
|
31565
32567
|
if (quote.state === "PAID") {
|
|
31566
32568
|
const signatures = (quote.change ?? []).filter((sig) => !Amount.from(sig.amount).isZero());
|
|
@@ -31666,7 +32668,7 @@ var CashuWallet = class CashuWallet {
|
|
|
31666
32668
|
out.push({
|
|
31667
32669
|
mint,
|
|
31668
32670
|
amount: total(proofs),
|
|
31669
|
-
token: getEncodedToken({
|
|
32671
|
+
token: (await loadCashu()).getEncodedToken({
|
|
31670
32672
|
mint,
|
|
31671
32673
|
proofs,
|
|
31672
32674
|
unit: UNIT$1
|
|
@@ -31679,7 +32681,7 @@ var CashuWallet = class CashuWallet {
|
|
|
31679
32681
|
let wallet = this.wallets.get(mint);
|
|
31680
32682
|
if (!wallet) {
|
|
31681
32683
|
wallet = (async () => {
|
|
31682
|
-
const instance = new Wallet(mint, { unit: UNIT$1 });
|
|
32684
|
+
const instance = new (await (loadCashu())).Wallet(mint, { unit: UNIT$1 });
|
|
31683
32685
|
await within(MINT_TIMEOUT_MS, instance.loadMint(), mint);
|
|
31684
32686
|
return instance;
|
|
31685
32687
|
})();
|
|
@@ -32319,12 +33321,14 @@ var PaymentDesk = class {
|
|
|
32319
33321
|
return request.linkId === linkId || !!request.group && this.host.groupOf?.(linkId) === request.group;
|
|
32320
33322
|
}
|
|
32321
33323
|
/**
|
|
32322
|
-
* Why a token cannot pay a request to the group, checked before anything is redeemed: already paid,
|
|
32323
|
-
*
|
|
33324
|
+
* Why a token cannot pay a request to the group, checked before anything is redeemed: already paid, another member's
|
|
33325
|
+
* ecash for it still being redeemed (the mint has not answered), or it would not settle it (too little, a mint the
|
|
33326
|
+
* request did not name). Undefined when it can.
|
|
32324
33327
|
*/
|
|
32325
|
-
refuseForGroup(request, payment) {
|
|
33328
|
+
async refuseForGroup(request, payment) {
|
|
32326
33329
|
if (request.state !== "pending") return "Already paid by another member of the group";
|
|
32327
|
-
|
|
33330
|
+
if (await this.wallet.redeemingFor?.(request.id)) return "Another member of the group is paying it";
|
|
33331
|
+
const token = await this.wallet.inspect?.(payment.endpoint[1]);
|
|
32328
33332
|
if (token && token.kind === "token" && (token.amount < request.amount || !(request.mints ?? []).includes(token.mint))) return "This ecash does not pay the request: the amount or the mint differs";
|
|
32329
33333
|
}
|
|
32330
33334
|
/**
|
|
@@ -32416,6 +33420,24 @@ var PaymentDesk = class {
|
|
|
32416
33420
|
}).catch(() => {});
|
|
32417
33421
|
}
|
|
32418
33422
|
/**
|
|
33423
|
+
* A Testnet invoice found in a chat message, paid from its card: the chat's contact is told as `sayPaid` does, with
|
|
33424
|
+
* no request (it may be one from their Receive screen, held until a payer says it paid).
|
|
33425
|
+
*/
|
|
33426
|
+
sayInvoicePaid(linkId, invoice) {
|
|
33427
|
+
const link = this.host.getLink(linkId);
|
|
33428
|
+
const amount = decodeBolt11(invoice)?.amountSat;
|
|
33429
|
+
if (!link || !amount) return;
|
|
33430
|
+
this.sendTo(link, {
|
|
33431
|
+
id: newId(),
|
|
33432
|
+
timestamp: Date.now(),
|
|
33433
|
+
amount: {
|
|
33434
|
+
value: String(amount),
|
|
33435
|
+
asset: UNIT
|
|
33436
|
+
},
|
|
33437
|
+
endpoint: [ENDPOINT.bolt11, invoice.trim()]
|
|
33438
|
+
}).catch(() => {});
|
|
33439
|
+
}
|
|
33440
|
+
/**
|
|
32419
33441
|
* Takes back ecash the peer never redeemed. If they did redeem it, the mint says so and the payment is settled.
|
|
32420
33442
|
* Resolves to what came back of Cashu ecash (and the mint's fee for it); undefined when nothing did, or not Cashu.
|
|
32421
33443
|
*/
|
|
@@ -32752,7 +33774,7 @@ var PaymentDesk = class {
|
|
|
32752
33774
|
});
|
|
32753
33775
|
return;
|
|
32754
33776
|
}
|
|
32755
|
-
const inspected = this.wallet.inspect?.(payment.endpoint[1]);
|
|
33777
|
+
const inspected = await this.wallet.inspect?.(payment.endpoint[1]);
|
|
32756
33778
|
if (payment.endpoint[0] === ENDPOINT.cashu && inspected !== void 0 && inspected?.kind !== "token") {
|
|
32757
33779
|
link.sendPaymentResult({
|
|
32758
33780
|
id: payment.id,
|
|
@@ -32771,7 +33793,7 @@ var PaymentDesk = class {
|
|
|
32771
33793
|
}
|
|
32772
33794
|
const forGroup = payment.requestId ? this.payments.get(payment.requestId) : void 0;
|
|
32773
33795
|
if (forGroup?.group && forGroup.kind === "request" && forGroup.direction === "out" && this.owns(forGroup, linkId)) {
|
|
32774
|
-
const refused = this.refuseForGroup(forGroup, payment);
|
|
33796
|
+
const refused = await this.refuseForGroup(forGroup, payment);
|
|
32775
33797
|
if (refused) {
|
|
32776
33798
|
link.sendPaymentResult({
|
|
32777
33799
|
id: payment.id,
|
|
@@ -33039,6 +34061,7 @@ var PaymentDesk = class {
|
|
|
33039
34061
|
*/
|
|
33040
34062
|
async receiveCheck(linkId, payment) {
|
|
33041
34063
|
const link = this.host.getLink(linkId);
|
|
34064
|
+
if (link && !payment.requestId && payment.endpoint[0] === ENDPOINT.bolt11) return this.receiveInvoicePaid(linkId, link, payment.endpoint[1]);
|
|
33042
34065
|
const request = payment.requestId ? this.payments.get(payment.requestId) : void 0;
|
|
33043
34066
|
if (!link || !request || request.kind !== "request" || request.direction !== "out" || !this.owns(request, linkId)) return;
|
|
33044
34067
|
const rail = request.target ? request.target.method : request.invoice ? "lightning" : void 0;
|
|
@@ -33061,6 +34084,22 @@ var PaymentDesk = class {
|
|
|
33061
34084
|
else await this.reconcileArkReceipts();
|
|
33062
34085
|
} catch {}
|
|
33063
34086
|
}
|
|
34087
|
+
/**
|
|
34088
|
+
* The contact says it paid an invoice of ours it found in the chat, with no request: a Receive-screen invoice is
|
|
34089
|
+
* held on a test mint until then (StoredQuote `held`), and that one alone is let go. A request's invoice is spoken
|
|
34090
|
+
* for by its own contact only (`receiveCheck`). Bounded: one look every 3 seconds per chat.
|
|
34091
|
+
*/
|
|
34092
|
+
async receiveInvoicePaid(linkId, link, invoice) {
|
|
34093
|
+
if (!link.allowsPayment("lightning")) return;
|
|
34094
|
+
const now = Date.now();
|
|
34095
|
+
if (now - (this.lastCheckFrom.get(`chat:${linkId}`) ?? 0) < 3e3) return;
|
|
34096
|
+
this.lastCheckFrom.set(`chat:${linkId}`, now);
|
|
34097
|
+
const wanted = invoice.trim().toLowerCase();
|
|
34098
|
+
try {
|
|
34099
|
+
const quote = (await this.wallet.quotes()).find((q) => q.held && !q.paymentId && q.invoice.toLowerCase() === wanted);
|
|
34100
|
+
if (quote) await this.wallet.vouch(quote.invoice);
|
|
34101
|
+
} catch {}
|
|
34102
|
+
}
|
|
33064
34103
|
/** A Lightning payment of a contact's request that was left pending (or unknown) settled. */
|
|
33065
34104
|
async onLightningResolved(op, paid) {
|
|
33066
34105
|
const found = op.paymentId ? this.payments.get(op.paymentId) : void 0;
|
|
@@ -34505,7 +35544,7 @@ function writtenAt(message, leads) {
|
|
|
34505
35544
|
}
|
|
34506
35545
|
var JOIN_NOTICE = /^👋 (?:.+ )?joined$/;
|
|
34507
35546
|
/** What a newly stored row says, or null when it is quiet. `type`: "message" for the contact's, "sent" for mine. */
|
|
34508
|
-
function messageAttention(type, message, { startedAt, newest, now }) {
|
|
35547
|
+
function messageAttention(type, message, { startedAt, newest, now, lateAt }) {
|
|
34509
35548
|
if (message.timestamp < startedAt || message.file || message.paymentId || JOIN_NOTICE.test(message.text)) return null;
|
|
34510
35549
|
if (message.event) {
|
|
34511
35550
|
const cue = groupCue(message);
|
|
@@ -34521,6 +35560,7 @@ function messageAttention(type, message, { startedAt, newest, now }) {
|
|
|
34521
35560
|
};
|
|
34522
35561
|
if (!message.text.trim()) return null;
|
|
34523
35562
|
if (newest !== void 0 && message.timestamp < newest - 6e4 && now - message.timestamp > 6e4) return null;
|
|
35563
|
+
if (lateAt !== void 0 && now - lateAt < 5e3 && now - message.timestamp > 6e4) return null;
|
|
34524
35564
|
return {
|
|
34525
35565
|
type,
|
|
34526
35566
|
mention: !!message.mentioned
|
|
@@ -34959,6 +35999,11 @@ var GhostlyNode = class GhostlyNode {
|
|
|
34959
35999
|
transfersRestored = false;
|
|
34960
36000
|
/** `start` has loaded the groups (EngineState.groupsLoaded). */
|
|
34961
36001
|
groupsLoaded = false;
|
|
36002
|
+
/** Settles as `start` has loaded the groups: a group's history reads as stored from then on (`EngineServer.handle`). */
|
|
36003
|
+
groupsLoadedNow;
|
|
36004
|
+
groupsRead = new Promise((resolve) => {
|
|
36005
|
+
this.groupsLoadedNow = resolve;
|
|
36006
|
+
});
|
|
34962
36007
|
feedbackStartedAt = Date.now();
|
|
34963
36008
|
feedbackIds = /* @__PURE__ */ new Set();
|
|
34964
36009
|
walletFeedbackReady = false;
|
|
@@ -34983,28 +36028,43 @@ var GhostlyNode = class GhostlyNode {
|
|
|
34983
36028
|
}
|
|
34984
36029
|
/** The newest time written in each chat while this engine runs: a message written well before it is a catch-up. */
|
|
34985
36030
|
newestAt = /* @__PURE__ */ new Map();
|
|
36031
|
+
/** Per group: when the last message written well before it came, came. What comes after it, as old, is its catch-up. */
|
|
36032
|
+
lateAt = /* @__PURE__ */ new Map();
|
|
34986
36033
|
/** The last place given in each chat while this engine runs (`arrivalKey`). */
|
|
34987
36034
|
placedAt = /* @__PURE__ */ new Map();
|
|
34988
36035
|
/**
|
|
34989
|
-
* Each 1:1 chat's contact: how far the time its latest live messages said was
|
|
34990
|
-
* of them). What tells a
|
|
36036
|
+
* Each 1:1 chat's contact, and each group's member (`clockOf`): how far the time its latest live messages said was
|
|
36037
|
+
* from when they came (`CLOCK_SAMPLES` of them). What tells a clock that is behind from a message that is late (`writtenAt`).
|
|
34991
36038
|
*/
|
|
34992
36039
|
clockLeads = /* @__PURE__ */ new Map();
|
|
34993
|
-
/**
|
|
34994
|
-
|
|
34995
|
-
|
|
34996
|
-
|
|
36040
|
+
/** Whose clock a row's time is: a 1:1 chat's contact, or the group's member who wrote it. */
|
|
36041
|
+
clockOf(message) {
|
|
36042
|
+
return message.linkId.startsWith("group:") ? `${message.linkId} ${message.member ?? ""}` : message.linkId;
|
|
36043
|
+
}
|
|
36044
|
+
/**
|
|
36045
|
+
* A contact's row came on the live session of a 1:1 chat, or a member's straight from its author (`live`, never one
|
|
36046
|
+
* handed on or caught up): what it says of that clock is kept.
|
|
36047
|
+
*/
|
|
36048
|
+
clockSample(message, live = false) {
|
|
36049
|
+
if (message.sender !== "peer" || message.sentAt === void 0 || message.event || message.via !== "datalink") return;
|
|
36050
|
+
if (message.linkId.startsWith("group:") && !(live && message.member)) return;
|
|
36051
|
+
const clock = this.clockOf(message);
|
|
36052
|
+
this.clockLeads.set(clock, [...this.clockLeads.get(clock) ?? [], message.sentAt - message.timestamp].slice(-5));
|
|
34997
36053
|
}
|
|
34998
36054
|
/** Only a new message, or mine going out, says anything (engine/attention.ts): judged by when it was written. */
|
|
34999
36055
|
messageFeedback(type, message, newest = this.newestAt.get(message.linkId)) {
|
|
36056
|
+
const written = writtenAt(message, this.clockLeads.get(this.clockOf(message))), now = Date.now();
|
|
36057
|
+
const group = type === "message" && message.linkId.startsWith("group:"), lateAt = group ? this.lateAt.get(message.linkId) : void 0;
|
|
35000
36058
|
const attention = messageAttention(type, {
|
|
35001
36059
|
...message,
|
|
35002
|
-
timestamp:
|
|
36060
|
+
timestamp: written
|
|
35003
36061
|
}, {
|
|
35004
36062
|
startedAt: this.feedbackStartedAt,
|
|
35005
36063
|
newest,
|
|
35006
|
-
now
|
|
36064
|
+
now,
|
|
36065
|
+
...lateAt !== void 0 && { lateAt }
|
|
35007
36066
|
});
|
|
36067
|
+
if (group && !message.event && now - written > 6e4) this.lateAt.set(message.linkId, now);
|
|
35008
36068
|
if (attention) this.feedback(attention.type, message.linkId + ":" + message.id, message.linkId, attention.mention, attention.cue);
|
|
35009
36069
|
}
|
|
35010
36070
|
/**
|
|
@@ -35549,6 +36609,8 @@ var GhostlyNode = class GhostlyNode {
|
|
|
35549
36609
|
}
|
|
35550
36610
|
}
|
|
35551
36611
|
});
|
|
36612
|
+
/** x401 credentials (WISP 3xx): kept for websites the headless CLI calls, never shared with a contact. */
|
|
36613
|
+
x401 = new X401Credentials();
|
|
35552
36614
|
/** Identity proofs: this profile's, and those shared in each paired chat (WISP 300). */
|
|
35553
36615
|
identities = new IdentityProofs({
|
|
35554
36616
|
ledger: (linkId) => {
|
|
@@ -35610,6 +36672,59 @@ var GhostlyNode = class GhostlyNode {
|
|
|
35610
36672
|
return this.transport.publishPayload(pubKeyZ32, payload, { background: true });
|
|
35611
36673
|
}
|
|
35612
36674
|
});
|
|
36675
|
+
/** The profile's contact link (WISP 8xx): one link that names nobody, its record kept on Pkarr while online. */
|
|
36676
|
+
contactLinks = new ContactLinks({
|
|
36677
|
+
online: () => this.networkOn,
|
|
36678
|
+
emit: () => this.emitState(),
|
|
36679
|
+
publish: (identity, records) => this.transport.publish(identity, records, { background: true }),
|
|
36680
|
+
resolve: async (pubKeyZ32) => (await this.transport.resolve(pubKeyZ32, { background: true }))?.records ?? null,
|
|
36681
|
+
ownInvite: (code) => {
|
|
36682
|
+
const params = decodeInviteCode(code);
|
|
36683
|
+
return !params || inviteOwnership(params, [...this.links.values()].map((l) => l.stored)).kind !== "new";
|
|
36684
|
+
},
|
|
36685
|
+
accept: async (request) => {
|
|
36686
|
+
const params = decodeInviteCode(request.invite);
|
|
36687
|
+
if (!params || inviteOwnership(params, [...this.links.values()].map((l) => l.stored)).kind !== "new") throw new Error("This request's invite is for a chat you already have.");
|
|
36688
|
+
const { linkId } = await this.joinLink({ inviteCode: request.invite });
|
|
36689
|
+
this.renameLink({
|
|
36690
|
+
linkId,
|
|
36691
|
+
label: request.name
|
|
36692
|
+
});
|
|
36693
|
+
await this.storeMessage({
|
|
36694
|
+
linkId,
|
|
36695
|
+
id: `peer_contact_${request.key.slice(0, 32)}`,
|
|
36696
|
+
text: request.text,
|
|
36697
|
+
sender: "peer",
|
|
36698
|
+
timestamp: Math.min(request.issued, request.receivedAt),
|
|
36699
|
+
via: "pkarr"
|
|
36700
|
+
});
|
|
36701
|
+
return linkId;
|
|
36702
|
+
}
|
|
36703
|
+
});
|
|
36704
|
+
/** Requests this profile sends through others' contact links (WISP 8xx). */
|
|
36705
|
+
contactAsks = new ContactAsks({
|
|
36706
|
+
online: () => this.networkOn,
|
|
36707
|
+
emit: () => this.emitState(),
|
|
36708
|
+
publish: (identity, records) => this.transport.publish(identity, records, { background: true }),
|
|
36709
|
+
resolve: async (pubKeyZ32) => (await this.transport.resolve(pubKeyZ32, { background: true }))?.records ?? null,
|
|
36710
|
+
createChat: async ({ label, text }) => {
|
|
36711
|
+
const chat = await this.createLink();
|
|
36712
|
+
if (label) this.renameLink({
|
|
36713
|
+
linkId: chat.linkId,
|
|
36714
|
+
label
|
|
36715
|
+
});
|
|
36716
|
+
await this.storeMessage({
|
|
36717
|
+
linkId: chat.linkId,
|
|
36718
|
+
id: `me_contact_${crypto.randomUUID()}`,
|
|
36719
|
+
text,
|
|
36720
|
+
sender: "me",
|
|
36721
|
+
timestamp: Date.now(),
|
|
36722
|
+
via: "pkarr"
|
|
36723
|
+
});
|
|
36724
|
+
return chat;
|
|
36725
|
+
},
|
|
36726
|
+
ownLink: (linkKeyZ32) => this.contactLinks.owns(linkKeyZ32)
|
|
36727
|
+
});
|
|
35613
36728
|
/** The Nostr social layer (profile, follows, notes, publication) on top of verified Nostr proofs. */
|
|
35614
36729
|
nostrSocial = new NostrSocial({
|
|
35615
36730
|
settings: () => this.settings.nostr,
|
|
@@ -35812,13 +36927,15 @@ var GhostlyNode = class GhostlyNode {
|
|
|
35812
36927
|
const live = this.links.get(linkId);
|
|
35813
36928
|
return !!live?.lastSyncAt && !!live.presence?.online && presenceSeenAt(live.presence) > live.lastSyncAt;
|
|
35814
36929
|
},
|
|
35815
|
-
publish: (identity, records, background, door) => this.groupTransport.publish(identity, records, {
|
|
36930
|
+
publish: (identity, records, background, door, back) => this.groupTransport.publish(identity, records, {
|
|
35816
36931
|
background,
|
|
35817
|
-
door
|
|
36932
|
+
door,
|
|
36933
|
+
back
|
|
35818
36934
|
}),
|
|
35819
|
-
resolve: async (pubKeyZ32, background, door) => (await this.groupTransport.resolve(pubKeyZ32, {
|
|
36935
|
+
resolve: async (pubKeyZ32, background, door, back) => (await this.groupTransport.resolve(pubKeyZ32, {
|
|
35820
36936
|
background,
|
|
35821
|
-
door
|
|
36937
|
+
door,
|
|
36938
|
+
back
|
|
35822
36939
|
}))?.records ?? null,
|
|
35823
36940
|
expectPeer: (linkId) => this.links.get(linkId)?.link?.expectPeer(),
|
|
35824
36941
|
signalIn: (linkId, payload, direct) => !!this.links.get(linkId)?.carried?.accept(payload, direct),
|
|
@@ -35832,7 +36949,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
35832
36949
|
else this.closeGroupLink(linkId).catch(() => {});
|
|
35833
36950
|
},
|
|
35834
36951
|
edgeNick: (linkId) => this.links.get(linkId)?.presence.nick || void 0,
|
|
35835
|
-
storeMessage: (message) => this.storeNewMessage(message),
|
|
36952
|
+
storeMessage: (message, live, place) => this.storeNewMessage(message, live, place),
|
|
35836
36953
|
completeMessage: (message) => this.completeGroupMessage(message),
|
|
35837
36954
|
forgetFrame: async (linkId, id) => {
|
|
35838
36955
|
await db.patchMessage(linkId, id, (stored) => stored.frame ? { frame: void 0 } : null, true);
|
|
@@ -35841,6 +36958,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
35841
36958
|
await db.patchMessage(linkId, id, () => ({ frame }), true);
|
|
35842
36959
|
},
|
|
35843
36960
|
placeAgain: (linkId, ids) => this.placeAgain(linkId, ids),
|
|
36961
|
+
placeAbove: (linkId, id) => this.placeAbove(linkId, id),
|
|
35844
36962
|
membersChanged: (groupId) => this.groupMembersChanged(groupId),
|
|
35845
36963
|
historyGone: (groupId) => this.groupHistoryGone(groupId),
|
|
35846
36964
|
emit: () => this.emitState(),
|
|
@@ -36255,34 +37373,44 @@ var GhostlyNode = class GhostlyNode {
|
|
|
36255
37373
|
await this.did.load();
|
|
36256
37374
|
setOwnDidSource(() => this.did.id);
|
|
36257
37375
|
this.did.start();
|
|
37376
|
+
await this.contactLinks.load();
|
|
37377
|
+
this.contactLinks.start();
|
|
37378
|
+
await this.contactAsks.load();
|
|
37379
|
+
this.contactAsks.start();
|
|
36258
37380
|
await this.nostrSocial.load();
|
|
36259
37381
|
await this.publicProfiles.load();
|
|
36260
37382
|
if (!this.limitedMode) await this.startWallets();
|
|
36261
37383
|
const history = /* @__PURE__ */ new Map();
|
|
36262
|
-
|
|
36263
|
-
|
|
36264
|
-
|
|
36265
|
-
|
|
37384
|
+
const links = await db.getLinks();
|
|
37385
|
+
for (const stored of links) history.set(stored.id, []);
|
|
37386
|
+
for (const stored of links) if (stored.group) this.links.set(stored.id, newLiveLink(stored, 0, linkFilesFrom(stored.id, [], [])));
|
|
37387
|
+
for (const group of await db.getGroups()) await this.settleHistory(`group:${group.id}`);
|
|
37388
|
+
await this.groups.load(this.limitedMode);
|
|
37389
|
+
this.groupsLoaded = true;
|
|
37390
|
+
this.groupsLoadedNow();
|
|
37391
|
+
this.emitState();
|
|
37392
|
+
for (const stored of links) {
|
|
37393
|
+
if (stored.group) continue;
|
|
37394
|
+
await this.settleHistory(stored.id);
|
|
37395
|
+
const messages = await db.getMessages(stored.id);
|
|
37396
|
+
const storedFiles = await fileStore.listForLink(stored.id);
|
|
36266
37397
|
const waiting = new Set(messages.flatMap((m) => m.sender === "me" && m.delivery === "waiting" && m.file ? [m.file.id] : []));
|
|
36267
37398
|
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" ? {
|
|
36268
37399
|
...file.transfer,
|
|
36269
37400
|
state: "failed",
|
|
36270
37401
|
error: "Transfer interrupted. Retry when connected."
|
|
36271
37402
|
} : file.transfer);
|
|
36272
|
-
|
|
37403
|
+
this.fileDesk.restore(stored.id, storedFiles);
|
|
36273
37404
|
const files = linkFilesFrom(stored.id, storedFiles, messages);
|
|
36274
37405
|
this.links.set(stored.id, newLiveLink(stored, messages[messages.length - 1]?.timestamp ?? 0, files));
|
|
36275
|
-
|
|
37406
|
+
this.links.get(stored.id).waitingSends = Promise.resolve(new Set(messages.filter(waitsForLive).map((m) => m.id)));
|
|
36276
37407
|
history.set(stored.id, messages);
|
|
36277
37408
|
for (const message of messages.slice(-20)) this.clockSample(message);
|
|
36278
37409
|
const note = latestReaction(messages);
|
|
36279
37410
|
if (note) this.reactionNotes.set(stored.id, note);
|
|
36280
|
-
if (stored.profile
|
|
37411
|
+
if (stored.profile) await this.outboxFor(stored.id).recover();
|
|
36281
37412
|
}
|
|
36282
37413
|
this.transfersRestored = true;
|
|
36283
|
-
for (const group of await db.getGroups()) await this.settleHistory(`group:${group.id}`);
|
|
36284
|
-
await this.groups.load(this.limitedMode);
|
|
36285
|
-
this.groupsLoaded = true;
|
|
36286
37414
|
for (const group of this.groups.views()) {
|
|
36287
37415
|
const note = latestReaction(await db.getMessages(`group:${group.id}`));
|
|
36288
37416
|
if (note) this.reactionNotes.set(`group:${group.id}`, note);
|
|
@@ -36674,14 +37802,16 @@ var GhostlyNode = class GhostlyNode {
|
|
|
36674
37802
|
/**
|
|
36675
37803
|
* Says goodbye on every live session (`paired-bye`): the contacts end theirs now and watch for this app to come back,
|
|
36676
37804
|
* instead of noticing a minute later. Synchronous: an app closing may have no time for anything after it.
|
|
37805
|
+
* `quitting`: the app quits (not a page that may be reloading), so a contact's call still ringing is declined too.
|
|
36677
37806
|
*/
|
|
36678
|
-
depart() {
|
|
36679
|
-
for (const live of this.links.values()) live.link?.depart();
|
|
37807
|
+
depart(options = {}) {
|
|
37808
|
+
for (const live of this.links.values()) live.link?.depart(options);
|
|
36680
37809
|
}
|
|
36681
37810
|
/**
|
|
36682
37811
|
* Tells every peer we are leaving. Best effort: the browser may already be closing. `quiet`: says nothing to anyone
|
|
36683
37812
|
* (no goodbye, no departure in the links' packets), as a device another device replaced must (WISP 06 § When a
|
|
36684
|
-
* device finds itself superseded: "Nothing is said to contacts and nothing more is published").
|
|
37813
|
+
* device finds itself superseded: "Nothing is said to contacts and nothing more is published"). `quitting`: the app
|
|
37814
|
+
* itself quits (the Desktop app, a CLI daemon), not a web page that may be reloading (`depart`).
|
|
36685
37815
|
*/
|
|
36686
37816
|
async shutdown(options = {}) {
|
|
36687
37817
|
const quiet = options.quiet === true || this.gatedOut;
|
|
@@ -36692,7 +37822,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
36692
37822
|
this.limitedTimer = null;
|
|
36693
37823
|
}
|
|
36694
37824
|
this.appShelf?.stop();
|
|
36695
|
-
if (!quiet) this.depart();
|
|
37825
|
+
if (!quiet) this.depart({ quitting: options.quitting === true });
|
|
36696
37826
|
const linksStopped = Promise.allSettled([...this.links.values()].map(async (live) => {
|
|
36697
37827
|
live.carried?.stop();
|
|
36698
37828
|
await Promise.all([live.link?.stop(!quiet), live.caps?.stop()]);
|
|
@@ -36726,6 +37856,8 @@ var GhostlyNode = class GhostlyNode {
|
|
|
36726
37856
|
this.restoreTimer = null;
|
|
36727
37857
|
this.identities.stop();
|
|
36728
37858
|
this.did.stop();
|
|
37859
|
+
this.contactLinks.stop();
|
|
37860
|
+
this.contactAsks.stop();
|
|
36729
37861
|
this.nostrSocial.stop();
|
|
36730
37862
|
this.publicProfiles.stop();
|
|
36731
37863
|
this.publicActivity.clear();
|
|
@@ -36763,6 +37895,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
36763
37895
|
const withdraw = Promise.allSettled([
|
|
36764
37896
|
this.hold.withdrawAll(),
|
|
36765
37897
|
this.did.deactivate(),
|
|
37898
|
+
this.contactLinks.withdraw(),
|
|
36766
37899
|
this.identities.revokeAll()
|
|
36767
37900
|
]);
|
|
36768
37901
|
await Promise.race([withdraw, new Promise((resolve) => setTimeout(resolve, CLEAR_WITHDRAW_MS))]);
|
|
@@ -36891,6 +38024,9 @@ var GhostlyNode = class GhostlyNode {
|
|
|
36891
38024
|
payments: this.desk.views(),
|
|
36892
38025
|
identityProofs: this.identities.views(),
|
|
36893
38026
|
did: this.did.view(),
|
|
38027
|
+
contactLink: this.contactLinks.view(),
|
|
38028
|
+
contactRequests: this.contactLinks.requests(),
|
|
38029
|
+
contactAsks: this.contactAsks.views(),
|
|
36894
38030
|
nostr: this.nostrSocial.state(),
|
|
36895
38031
|
edges: [...[...this.links.values()].filter((live) => live.stored.group && !live.stored.groupEntry).map((live) => this.viewOf(live)), ...this.communityPayViews(groups)],
|
|
36896
38032
|
groups: groups.map((group) => ({
|
|
@@ -37202,9 +38338,42 @@ var GhostlyNode = class GhostlyNode {
|
|
|
37202
38338
|
recheckIdentityProof(params) {
|
|
37203
38339
|
return this.identities.recheck(params);
|
|
37204
38340
|
}
|
|
38341
|
+
newX401HolderKey() {
|
|
38342
|
+
return this.x401.newKey();
|
|
38343
|
+
}
|
|
38344
|
+
addX401Credential(params) {
|
|
38345
|
+
return this.x401.add(params);
|
|
38346
|
+
}
|
|
38347
|
+
x401Credentials(params = {}) {
|
|
38348
|
+
return this.x401.list(params);
|
|
38349
|
+
}
|
|
38350
|
+
removeX401Credential(params) {
|
|
38351
|
+
return this.x401.remove(params);
|
|
38352
|
+
}
|
|
37205
38353
|
setDidListed(params) {
|
|
37206
38354
|
return this.did.setListed(params);
|
|
37207
38355
|
}
|
|
38356
|
+
/** A new contact link for the profile; the one before is turned off. */
|
|
38357
|
+
createContactLink() {
|
|
38358
|
+
return this.contactLinks.create();
|
|
38359
|
+
}
|
|
38360
|
+
turnOffContactLink() {
|
|
38361
|
+
return this.contactLinks.turnOff();
|
|
38362
|
+
}
|
|
38363
|
+
/** The contact link was shown or copied, or Requests opened: look for requests now and often for a while. */
|
|
38364
|
+
contactLinkShown() {
|
|
38365
|
+
this.contactLinks.shown();
|
|
38366
|
+
}
|
|
38367
|
+
acceptContactRequest({ id }) {
|
|
38368
|
+
return this.contactLinks.accept(id);
|
|
38369
|
+
}
|
|
38370
|
+
declineContactRequest({ id }) {
|
|
38371
|
+
return this.contactLinks.decline(id);
|
|
38372
|
+
}
|
|
38373
|
+
/** Sends a request through someone's contact link; the chat made for it. */
|
|
38374
|
+
askContactLink(params) {
|
|
38375
|
+
return this.contactAsks.ask(params);
|
|
38376
|
+
}
|
|
37208
38377
|
lookupIdentityDisplay(params) {
|
|
37209
38378
|
const r = this.links.get(params.linkId)?.stored.identities?.received.find((x) => x.id === params.id);
|
|
37210
38379
|
if (r?.binding.provider === "nostr") return this.nostrSocial.loadContact({
|
|
@@ -37536,6 +38705,8 @@ var GhostlyNode = class GhostlyNode {
|
|
|
37536
38705
|
groupHistoryGone(groupId) {
|
|
37537
38706
|
const linkId = `group:${groupId}`;
|
|
37538
38707
|
this.newestAt.delete(linkId);
|
|
38708
|
+
this.lateAt.delete(linkId);
|
|
38709
|
+
for (const clock of this.clockLeads.keys()) if (clock.startsWith(`${linkId} `)) this.clockLeads.delete(clock);
|
|
37539
38710
|
this.placedAt.delete(linkId);
|
|
37540
38711
|
this.reactionNotes.delete(linkId);
|
|
37541
38712
|
this.reactionNotesBefore.delete(linkId);
|
|
@@ -37699,6 +38870,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
37699
38870
|
this.nostrSocial.forgetLink(linkId);
|
|
37700
38871
|
this.hold.forgetLink(linkId);
|
|
37701
38872
|
this.fileDesk.drop(linkId);
|
|
38873
|
+
this.contactAsks.forgetChat(linkId).catch(() => {});
|
|
37702
38874
|
this.wakeLimiter.reset(linkId);
|
|
37703
38875
|
this.callWakeLimiter.reset(linkId);
|
|
37704
38876
|
if (live.stored.wakeToken) this.rotateWake().then(() => this.emitState());
|
|
@@ -38279,7 +39451,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
38279
39451
|
if (!queue) {
|
|
38280
39452
|
queue = new EditQueue({
|
|
38281
39453
|
read: () => db.getMessages(linkId),
|
|
38282
|
-
|
|
39454
|
+
rows: (ids) => db.getMessagesById(linkId, ids),
|
|
38283
39455
|
ready: () => {
|
|
38284
39456
|
const live = this.links.get(linkId), link = live?.link;
|
|
38285
39457
|
return !!link && (link.supportsEdits || link.textDelivery === "dht" && this.takesDhtEdits(live));
|
|
@@ -39684,7 +40856,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
39684
40856
|
* as a new message of mine, in the order they were written, carrying one more hop than it had and nothing of who wrote
|
|
39685
40857
|
* it or where. A text goes as written; a file from the bytes on this device (a copy per chat, nothing fetched again),
|
|
39686
40858
|
* through the chat's own path, so a large one still waits for the contact's consent. A group takes texts only. What one
|
|
39687
|
-
* chat refuses does not stop the others: each says what it got.
|
|
40859
|
+
* chat refuses does not stop the others, nor the rest of its own: each says what it got, and which of them (`sent`).
|
|
39688
40860
|
*/
|
|
39689
40861
|
async forwardMessages(params) {
|
|
39690
40862
|
const { linkId } = params;
|
|
@@ -39695,7 +40867,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
39695
40867
|
if (ids.length > 30) throw new Error(`Forward at most 30 messages at once`);
|
|
39696
40868
|
if (!targets.length) throw new Error("No chat to forward to");
|
|
39697
40869
|
if (targets.length > 5) throw new Error(`Forward to at most 5 chats at once`);
|
|
39698
|
-
if (linkId.startsWith("group:") ? !this.
|
|
40870
|
+
if (linkId.startsWith("group:") ? !this.groups.views().some((g) => g.id === linkId.slice(6)) : this.links.get(linkId)?.stored.group !== void 0 || !this.links.has(linkId)) throw new Error("Chat not found");
|
|
39699
40871
|
const chosen = await Promise.all(ids.map((id) => db.getMessage(linkId, id)));
|
|
39700
40872
|
if (chosen.some((m) => !m)) throw new Error("That message is not in this chat");
|
|
39701
40873
|
const messages = chosen.sort((a, b) => a.timestamp - b.timestamp);
|
|
@@ -39710,6 +40882,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
39710
40882
|
const result = {
|
|
39711
40883
|
to,
|
|
39712
40884
|
messageIds: [],
|
|
40885
|
+
sent: [],
|
|
39713
40886
|
error: null
|
|
39714
40887
|
};
|
|
39715
40888
|
results.push(result);
|
|
@@ -39731,11 +40904,20 @@ var GhostlyNode = class GhostlyNode {
|
|
|
39731
40904
|
text: message.text,
|
|
39732
40905
|
timestamp: now()
|
|
39733
40906
|
});
|
|
39734
|
-
if (sent.error)
|
|
39735
|
-
|
|
40907
|
+
if (sent.error) {
|
|
40908
|
+
note(sent.error);
|
|
40909
|
+
continue;
|
|
40910
|
+
}
|
|
40911
|
+
if (sent.messageId) result.messageIds.push(sent.messageId);
|
|
40912
|
+
result.sent.push(message.id);
|
|
39736
40913
|
continue;
|
|
39737
40914
|
}
|
|
39738
40915
|
if (groupId) {
|
|
40916
|
+
const check = this.groups.fileCheck(groupId);
|
|
40917
|
+
if (check.error) {
|
|
40918
|
+
note(check.error);
|
|
40919
|
+
continue;
|
|
40920
|
+
}
|
|
39739
40921
|
const original = message.file, file = {
|
|
39740
40922
|
id: groupFileId(groupId, "out"),
|
|
39741
40923
|
name: original.name,
|
|
@@ -39759,7 +40941,10 @@ var GhostlyNode = class GhostlyNode {
|
|
|
39759
40941
|
if (sent.error) {
|
|
39760
40942
|
await removeStored(file.id).catch(() => {});
|
|
39761
40943
|
note(sent.error);
|
|
39762
|
-
|
|
40944
|
+
continue;
|
|
40945
|
+
}
|
|
40946
|
+
if (sent.messageId) result.messageIds.push(sent.messageId);
|
|
40947
|
+
result.sent.push(message.id);
|
|
39763
40948
|
continue;
|
|
39764
40949
|
}
|
|
39765
40950
|
if (!live.link) {
|
|
@@ -39795,8 +40980,9 @@ var GhostlyNode = class GhostlyNode {
|
|
|
39795
40980
|
throw error;
|
|
39796
40981
|
}
|
|
39797
40982
|
result.messageIds.push(`me_${timestamp}`);
|
|
40983
|
+
result.sent.push(message.id);
|
|
39798
40984
|
} catch (error) {
|
|
39799
|
-
note(
|
|
40985
|
+
note(forwardError(error));
|
|
39800
40986
|
}
|
|
39801
40987
|
}
|
|
39802
40988
|
}
|
|
@@ -40655,12 +41841,18 @@ var GhostlyNode = class GhostlyNode {
|
|
|
40655
41841
|
const n = this.net(network);
|
|
40656
41842
|
return via === "cashu" ? this.wallet.quoteInvoice(invoice, n) : this.lightningCard(n, card).then((c) => c.quote(invoice));
|
|
40657
41843
|
}
|
|
40658
|
-
/**
|
|
40659
|
-
|
|
41844
|
+
/**
|
|
41845
|
+
* `confirmedReal`: the person confirmed a Mainnet payment as real money; without it one is refused. `linkId` +
|
|
41846
|
+
* `invoice`: the chat whose message held the invoice. Paid on Testnet, its contact is told (`sayInvoicePaid`): a test
|
|
41847
|
+
* mint's invoice counts only on the payer's word.
|
|
41848
|
+
*/
|
|
41849
|
+
async walletPayQuote({ quote, mint, note, confirmedReal, linkId, invoice }) {
|
|
40660
41850
|
const network = WALLET_NETWORKS.find((n) => this.lightnings[n].withQuote(quote));
|
|
40661
41851
|
const lightning = network && this.lightnings[network].withQuote(quote);
|
|
40662
41852
|
assertConfirmedReal(network ?? mintNetwork(mint), confirmedReal);
|
|
40663
|
-
|
|
41853
|
+
const paid = lightning ? await lightning.pay(quote, { note }) : await this.wallet.payQuote(quote, mint, note);
|
|
41854
|
+
if (paid && linkId && invoice && (network ?? mintNetwork(mint)) === "testnet") this.desk.sayInvoicePaid(linkId, invoice);
|
|
41855
|
+
return { paid };
|
|
40664
41856
|
}
|
|
40665
41857
|
/** A Lightning address or LNURL (LUD-16, LUD-06): resolved here, in the engine, so every platform fetches the same way. */
|
|
40666
41858
|
lnurlResolve({ text, network }) {
|
|
@@ -40789,8 +41981,8 @@ var GhostlyNode = class GhostlyNode {
|
|
|
40789
41981
|
bitcoinRefresh(params) {
|
|
40790
41982
|
return this.bitcoins[this.net(params?.network)].sources.refresh();
|
|
40791
41983
|
}
|
|
40792
|
-
walletInspectCashu({ text }) {
|
|
40793
|
-
return { inspection: this.wallet.inspect(text) };
|
|
41984
|
+
async walletInspectCashu({ text }) {
|
|
41985
|
+
return { inspection: await this.wallet.inspect(text) };
|
|
40794
41986
|
}
|
|
40795
41987
|
async walletReceiveToken({ token }) {
|
|
40796
41988
|
const { amount } = await this.wallet.receiveToken(token.trim());
|
|
@@ -42333,7 +43525,8 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42333
43525
|
carry: (payload) => this.groups.carrySignal(group, peer, payload),
|
|
42334
43526
|
open: () => !!live.link?.isDataLinkOpen,
|
|
42335
43527
|
look: () => live.link?.look(),
|
|
42336
|
-
direct: (payload) => this.groups.directSignal(group, peer, linkId, payload)
|
|
43528
|
+
direct: (payload) => this.groups.directSignal(group, peer, linkId, payload),
|
|
43529
|
+
after: live.carried?.lastDated()
|
|
42337
43530
|
});
|
|
42338
43531
|
const waiting = live.carried && this.groups.takeSignal(group, peer);
|
|
42339
43532
|
if (waiting) live.carried.accept(waiting);
|
|
@@ -42598,6 +43791,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42598
43791
|
} : void 0;
|
|
42599
43792
|
const resumed = stored.pairedPeerKey && stored.deliveryMode !== "dht" ? resumeHere(this.transportLogOf(live), this.transportsHere(), stored.peerTransports ?? live.caps?.peer?.transports) : {};
|
|
42600
43793
|
let dhtShown;
|
|
43794
|
+
let wasTaken = false;
|
|
42601
43795
|
live.link = new GhostLink({
|
|
42602
43796
|
paymentMethods: stored.paymentMethods,
|
|
42603
43797
|
paymentNetworks: this.chatNetworks(stored),
|
|
@@ -42687,6 +43881,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42687
43881
|
throw error;
|
|
42688
43882
|
}
|
|
42689
43883
|
if (!previous.pairedPeerKey) {
|
|
43884
|
+
if (!live.stored.group && live.stored.deliveryMode !== "dht") this.ensureNativeEndpoints(stored.id);
|
|
42690
43885
|
live.caps?.update().catch(() => {});
|
|
42691
43886
|
live.caps?.refresh(true);
|
|
42692
43887
|
}
|
|
@@ -42726,6 +43921,11 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42726
43921
|
onDhtDelivery: (view) => {
|
|
42727
43922
|
if (stored.profile && !stored.group) this.outboxes.get(linkId)?.flush().catch(() => {}).then(() => this.editQueues.get(linkId)?.flush()).catch(() => {});
|
|
42728
43923
|
this.observeTransport(linkId);
|
|
43924
|
+
const taken = !!live.link?.takenFromUs;
|
|
43925
|
+
if (taken !== wasTaken) {
|
|
43926
|
+
wasTaken = taken;
|
|
43927
|
+
if (!taken && live.stored.deliveryMode !== "dht" && this.keepsNativeEndpoints(linkId, live.stored)) this.ensureNativeEndpoints(linkId);
|
|
43928
|
+
}
|
|
42729
43929
|
const shown = JSON.stringify(view);
|
|
42730
43930
|
if (shown !== dhtShown) {
|
|
42731
43931
|
dhtShown = shown;
|
|
@@ -43093,7 +44293,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
43093
44293
|
get nativeFactories() {
|
|
43094
44294
|
const relay = this.relaysHyperdht ? this.hyperdhtRelay : "";
|
|
43095
44295
|
return {
|
|
43096
|
-
...relay ? { "hyperdht/1": async (seedB64) => (await import("./hyperdhtRelay-
|
|
44296
|
+
...relay ? { "hyperdht/1": async (seedB64) => (await import("./hyperdhtRelay-13aQ4KOf.js")).createRelayedHyperEndpoint(seedB64, relay) } : {},
|
|
43097
44297
|
...this.options.nativeTransports,
|
|
43098
44298
|
...this.ownIrohRelays ? { "iroh/1": (seedB64) => this.options.nativeTransports["iroh/1"](seedB64, this.settings.irohRelays?.length ? { relays: [...this.settings.irohRelays] } : {}) } : {},
|
|
43099
44299
|
...this.options.irohWeb ? { "iroh/1": (seedB64) => createIrohWebEndpoint(seedB64, { relays: this.irohRelays }) } : {}
|
|
@@ -43223,7 +44423,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
43223
44423
|
*/
|
|
43224
44424
|
async startNativeEndpoint(linkId, expected, key, factory, inUse) {
|
|
43225
44425
|
const live = this.links.get(linkId), link = live?.link;
|
|
43226
|
-
if (this.shuttingDown || !live?.stored.profile || live.stored.deliveryMode === "dht" || !link || link !== expected) return false;
|
|
44426
|
+
if (this.shuttingDown || !live?.stored.profile || live.stored.deliveryMode === "dht" || !link || link !== expected || link.takenFromUs) return false;
|
|
43227
44427
|
const group = !!live.stored.group;
|
|
43228
44428
|
if (group && !this.keepsGroupNative(live.stored)) return false;
|
|
43229
44429
|
if (link.availableTransports.includes(key)) return false;
|
|
@@ -43282,7 +44482,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
43282
44482
|
starting.then((late) => late.close(), () => {});
|
|
43283
44483
|
return false;
|
|
43284
44484
|
}
|
|
43285
|
-
if (this.shuttingDown || live.stored.deliveryMode === "dht" || live.link !== link || !this.links.has(linkId)) {
|
|
44485
|
+
if (this.shuttingDown || live.stored.deliveryMode === "dht" || live.link !== link || !this.links.has(linkId) || link.takenFromUs) {
|
|
43286
44486
|
await endpoint.close();
|
|
43287
44487
|
return false;
|
|
43288
44488
|
}
|
|
@@ -43463,8 +44663,9 @@ var GhostlyNode = class GhostlyNode {
|
|
|
43463
44663
|
* ahead of this clock nor above a row placed here since the app started. The admin's clock picks that time: two
|
|
43464
44664
|
* minutes ahead, "X joined" sat under everything said in the next two minutes; behind, it went above what the
|
|
43465
44665
|
* members had just read. Read late with nothing newer here (a member back after a while), it keeps its moment.
|
|
44666
|
+
* `place`: a received row's place given already (`placeAbove`): it takes it, and the next row still goes past the last.
|
|
43466
44667
|
*/
|
|
43467
|
-
placed(message) {
|
|
44668
|
+
placed(message, place) {
|
|
43468
44669
|
const now = Date.now(), last = this.placedAt.get(message.linkId) ?? 0;
|
|
43469
44670
|
if (message.event) {
|
|
43470
44671
|
if (message.sender !== "peer") return message;
|
|
@@ -43479,10 +44680,10 @@ var GhostlyNode = class GhostlyNode {
|
|
|
43479
44680
|
const sentAt = claimedTime(message.timestamp);
|
|
43480
44681
|
message = {
|
|
43481
44682
|
...message,
|
|
43482
|
-
timestamp: arrivalKey(now, last),
|
|
44683
|
+
timestamp: place ?? arrivalKey(now, last),
|
|
43483
44684
|
...sentAt !== void 0 && { sentAt }
|
|
43484
44685
|
};
|
|
43485
|
-
this.placedAt.set(message.linkId, message.timestamp);
|
|
44686
|
+
if (place === void 0) this.placedAt.set(message.linkId, message.timestamp);
|
|
43486
44687
|
} else this.placedAt.set(message.linkId, Math.max(last, Math.min(message.timestamp, now)));
|
|
43487
44688
|
return message;
|
|
43488
44689
|
}
|
|
@@ -43504,11 +44705,32 @@ var GhostlyNode = class GhostlyNode {
|
|
|
43504
44705
|
}));
|
|
43505
44706
|
if (moved.length) await this.messagesChanged(linkId, moved.map((m) => m.id));
|
|
43506
44707
|
}
|
|
44708
|
+
/**
|
|
44709
|
+
* A place just above this row of its history, below the one before it: where a group member's message that comes
|
|
44710
|
+
* after a later one of theirs is stored, so nothing has to move once the rest comes (WISP 400, requirement 10). At
|
|
44711
|
+
* most `AUTHOR_ORDER_ROOM` below the row, so the next older page finds room above it too; none when the two rows are
|
|
44712
|
+
* less than two milliseconds apart (it goes at the end, and is placed again).
|
|
44713
|
+
*/
|
|
44714
|
+
async placeAbove(linkId, id) {
|
|
44715
|
+
const row = await db.getMessage(linkId, id);
|
|
44716
|
+
if (!row) return void 0;
|
|
44717
|
+
const floor = (await db.getMessagePage(linkId, {
|
|
44718
|
+
limit: 1,
|
|
44719
|
+
before: {
|
|
44720
|
+
timestamp: row.timestamp,
|
|
44721
|
+
id: row.id
|
|
44722
|
+
}
|
|
44723
|
+
})).messages[0]?.timestamp ?? -Infinity;
|
|
44724
|
+
return row.timestamp - floor < 2 ? void 0 : Math.max(floor + 1, row.timestamp - 64);
|
|
44725
|
+
}
|
|
43507
44726
|
async storeMessage(message) {
|
|
43508
44727
|
await this.storeNewMessage(message);
|
|
43509
44728
|
}
|
|
43510
|
-
/**
|
|
43511
|
-
|
|
44729
|
+
/**
|
|
44730
|
+
* Stores a message; false when it was there already (or deleted here): nothing new came. `straight`: a group's,
|
|
44731
|
+
* straight from its author (`clockSample`). `place`: as `placed`.
|
|
44732
|
+
*/
|
|
44733
|
+
async storeNewMessage(message, straight = false, place) {
|
|
43512
44734
|
const edge = this.links.get(message.linkId)?.stored;
|
|
43513
44735
|
if (edge?.group && edge.groupPeer && !edge.groupEntry) message = {
|
|
43514
44736
|
...message,
|
|
@@ -43523,7 +44745,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
43523
44745
|
id: `${message.linkId}:${message.id}`,
|
|
43524
44746
|
...message.sender === "peer" ? { member: pay.member } : {}
|
|
43525
44747
|
};
|
|
43526
|
-
message = this.placed(message);
|
|
44748
|
+
message = this.placed(message, place);
|
|
43527
44749
|
const live = this.links.get(message.linkId);
|
|
43528
44750
|
if (live?.stored.deletedIds?.includes(message.id)) return false;
|
|
43529
44751
|
message = await this.resolveReply(message);
|
|
@@ -43553,9 +44775,9 @@ var GhostlyNode = class GhostlyNode {
|
|
|
43553
44775
|
}
|
|
43554
44776
|
if (!await db.addMessage(message)) return false;
|
|
43555
44777
|
if (live && waitsForLive(message)) live.waitingSends?.then((known) => known.add(message.id), () => {});
|
|
43556
|
-
this.clockSample(message);
|
|
44778
|
+
this.clockSample(message, straight);
|
|
43557
44779
|
const newest = this.newestAt.get(message.linkId);
|
|
43558
|
-
if (!message.event) this.newestAt.set(message.linkId, Math.max(newest ?? 0, writtenAt(message, this.clockLeads.get(message
|
|
44780
|
+
if (!message.event) this.newestAt.set(message.linkId, Math.max(newest ?? 0, writtenAt(message, this.clockLeads.get(this.clockOf(message)))));
|
|
43559
44781
|
if (message.sender === "peer") this.messageFeedback("message", message, newest);
|
|
43560
44782
|
if (live) live.lastMessageAt = Math.max(live.lastMessageAt, message.timestamp);
|
|
43561
44783
|
const early = message.sender === "peer" && message.id.startsWith("peer_") ? this.editBuffer.take(message.linkId, message.id.slice(5)) : void 0;
|
|
@@ -43822,8 +45044,8 @@ var EngineServer = class {
|
|
|
43822
45044
|
this.clients.delete(client);
|
|
43823
45045
|
});
|
|
43824
45046
|
}
|
|
43825
|
-
stop() {
|
|
43826
|
-
return this.node.shutdown();
|
|
45047
|
+
stop(options = {}) {
|
|
45048
|
+
return this.node.shutdown({ quitting: options.quitting === true });
|
|
43827
45049
|
}
|
|
43828
45050
|
detach(client) {
|
|
43829
45051
|
this.clients.delete(client);
|
|
@@ -43848,13 +45070,13 @@ var EngineServer = class {
|
|
|
43848
45070
|
params = { on: this.onCall.size > 0 };
|
|
43849
45071
|
}
|
|
43850
45072
|
try {
|
|
43851
|
-
await this.ready;
|
|
45073
|
+
await (readsGroupHistory(request) ? Promise.race([this.ready, this.node.groupsRead]) : this.ready);
|
|
43852
45074
|
const method = this.node[request.method];
|
|
43853
45075
|
if (typeof method !== "function") throw new Error(`Unknown method: ${request.method}`);
|
|
43854
45076
|
if (this.node.limited && !LIMITED_MODE_METHODS.has(String(request.method))) throw new Error(LIMITED_MODE_ERROR);
|
|
43855
45077
|
response.result = await method.call(this.node, params);
|
|
43856
45078
|
} catch (error) {
|
|
43857
|
-
response.error =
|
|
45079
|
+
response.error = errorLine(error);
|
|
43858
45080
|
}
|
|
43859
45081
|
try {
|
|
43860
45082
|
client.post(response);
|
|
@@ -43931,6 +45153,12 @@ var EngineServer = class {
|
|
|
43931
45153
|
for (const client of [...this.clients]) this.post(client, event);
|
|
43932
45154
|
}
|
|
43933
45155
|
};
|
|
45156
|
+
/** Whether a call only reads a group's stored history (`GhostlyNode.groupsRead`). */
|
|
45157
|
+
function readsGroupHistory(request) {
|
|
45158
|
+
if (request.method === "groupMessages") return true;
|
|
45159
|
+
const linkId = request.params?.linkId;
|
|
45160
|
+
return request.method === "messagePage" && typeof linkId === "string" && linkId.startsWith("group:");
|
|
45161
|
+
}
|
|
43934
45162
|
/** A call signal's kind (`o` for an offer), read as the client reads it; checked in full before it is replayed. */
|
|
43935
45163
|
function signalKind(signal) {
|
|
43936
45164
|
try {
|
|
@@ -43942,4 +45170,4 @@ function signalKind(signal) {
|
|
|
43942
45170
|
//#endregion
|
|
43943
45171
|
export { EngineServer };
|
|
43944
45172
|
|
|
43945
|
-
//# sourceMappingURL=server-
|
|
45173
|
+
//# sourceMappingURL=server-Cd1dLOB8.js.map
|