@ghostlytools/cli 1.3.9 → 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/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-DPy0eaH9.js → host-Drd-c4VO.js} +147 -25
- 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-Dt7xrqtb.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-p5DJnN7T.js → server-Cd1dLOB8.js} +1485 -240
- 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-DPy0eaH9.js.map +0 -1
- package/dist/assets/idb-B5L8wuOm.js.map +0 -1
- package/dist/assets/main-Dt7xrqtb.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-p5DJnN7T.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
|
}];
|
|
@@ -10233,6 +10955,8 @@ var BUNDLES_KEPT = {
|
|
|
10233
10955
|
};
|
|
10234
10956
|
/** The first bytes of a bundle: magic, the manifest's length and the largest manifest. */
|
|
10235
10957
|
var PEEK_BYTES = APP_BUNDLE_MAGIC.length + 4 + APP_BUNDLE_LIMITS.manifestBytes;
|
|
10958
|
+
/** What a peek reads first: the magic, the length and any usual manifest (about 1 KiB). A longer one is read to its end after. */
|
|
10959
|
+
var PEEK_FIRST_BYTES = 4096;
|
|
10236
10960
|
var bundleId = (digest) => `app-${digest}`;
|
|
10237
10961
|
/**
|
|
10238
10962
|
* The key of a scope's counter row (`AppScopeCount` as JSON in `value`, `size` 0): an app's key is never empty
|
|
@@ -10398,6 +11122,13 @@ function peekAppManifest(bytes) {
|
|
|
10398
11122
|
return null;
|
|
10399
11123
|
}
|
|
10400
11124
|
}
|
|
11125
|
+
/** Where a bundle's manifest ends, from its first bytes, or null when they are not a bundle's or name a manifest too long. */
|
|
11126
|
+
function peekManifestEnd(bytes) {
|
|
11127
|
+
const magic = utf8Encode(APP_BUNDLE_MAGIC);
|
|
11128
|
+
if (bytes.length < magic.length + 4 || !magic.every((b, i) => bytes[i] === b)) return null;
|
|
11129
|
+
const end = magic.length + 4 + new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(magic.length);
|
|
11130
|
+
return end <= PEEK_BYTES ? end : null;
|
|
11131
|
+
}
|
|
10401
11132
|
var Apps = class {
|
|
10402
11133
|
host;
|
|
10403
11134
|
staged = /* @__PURE__ */ new Map();
|
|
@@ -10856,6 +11587,23 @@ var Apps = class {
|
|
|
10856
11587
|
return read;
|
|
10857
11588
|
}
|
|
10858
11589
|
/**
|
|
11590
|
+
* The manifest at the start of the bundle at `url`, unchecked (`peekAppManifest`): its first `PEEK_FIRST_BYTES`, and
|
|
11591
|
+
* only for a longer manifest a second read from the start to exactly its end (from the start: one file's bytes, even
|
|
11592
|
+
* when it changed between the two).
|
|
11593
|
+
*/
|
|
11594
|
+
async peekAt(url) {
|
|
11595
|
+
const first = await this.host.fetch(url, {
|
|
11596
|
+
maxBytes: PEEK_FIRST_BYTES,
|
|
11597
|
+
peek: true
|
|
11598
|
+
});
|
|
11599
|
+
const end = peekManifestEnd(first);
|
|
11600
|
+
if (end === null || end <= first.length || first.length < PEEK_FIRST_BYTES) return peekAppManifest(first);
|
|
11601
|
+
return peekAppManifest(await this.host.fetch(url, {
|
|
11602
|
+
maxBytes: end,
|
|
11603
|
+
peek: true
|
|
11604
|
+
}));
|
|
11605
|
+
}
|
|
11606
|
+
/**
|
|
10859
11607
|
* Reads the index at `url`. With the index held for this store, bytes equal to it (by digest) are the held index: its
|
|
10860
11608
|
* signature and every revocation in it were checked when it was read, so only its expiry is taken again. Any other
|
|
10861
11609
|
* index is read with a pause every few ms (the engine shares the page's thread, and an index holds up to 4096
|
|
@@ -11415,10 +12163,7 @@ var Apps = class {
|
|
|
11415
12163
|
if (pinned === void 0) for (const url of (app.manifest.sources ?? []).filter(isAppFetchUrl)) {
|
|
11416
12164
|
if (candidates.has(url)) continue;
|
|
11417
12165
|
try {
|
|
11418
|
-
const peek =
|
|
11419
|
-
maxBytes: PEEK_BYTES,
|
|
11420
|
-
peek: true
|
|
11421
|
-
}));
|
|
12166
|
+
const peek = await this.peekAt(url);
|
|
11422
12167
|
if (peek && newer(peek)) candidates.set(url, { holds: peek });
|
|
11423
12168
|
} catch {}
|
|
11424
12169
|
}
|
|
@@ -11430,10 +12175,7 @@ var Apps = class {
|
|
|
11430
12175
|
if (want.peek) {
|
|
11431
12176
|
let peek;
|
|
11432
12177
|
try {
|
|
11433
|
-
peek =
|
|
11434
|
-
maxBytes: PEEK_BYTES,
|
|
11435
|
-
peek: true
|
|
11436
|
-
}));
|
|
12178
|
+
peek = await this.peekAt(url);
|
|
11437
12179
|
} catch {
|
|
11438
12180
|
continue;
|
|
11439
12181
|
}
|
|
@@ -11589,10 +12331,7 @@ var Apps = class {
|
|
|
11589
12331
|
if (app.store === void 0) urls.push(...app.manifest.sources ?? []);
|
|
11590
12332
|
for (const url of new Set(urls.filter(isAppFetchUrl))) {
|
|
11591
12333
|
try {
|
|
11592
|
-
if (
|
|
11593
|
-
maxBytes: PEEK_BYTES,
|
|
11594
|
-
peek: true
|
|
11595
|
-
}))?.digest !== app.digest) continue;
|
|
12334
|
+
if ((await this.peekAt(url))?.digest !== app.digest) continue;
|
|
11596
12335
|
} catch {
|
|
11597
12336
|
continue;
|
|
11598
12337
|
}
|
|
@@ -19805,6 +20544,11 @@ var Outbox = class {
|
|
|
19805
20544
|
get sameLink() {
|
|
19806
20545
|
return !this.stopped && this.closed === this.began;
|
|
19807
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
|
+
}
|
|
19808
20552
|
/** One message as stored now: by its id when the store can, else picked out of the chat's history. */
|
|
19809
20553
|
async one(id) {
|
|
19810
20554
|
return this.store.get ? this.store.get(id) : (await this.store.read()).find((m) => m.id === id);
|
|
@@ -19855,12 +20599,6 @@ var Outbox = class {
|
|
|
19855
20599
|
const unconfirmed = (m) => m.sender === "me" && m.via !== "hold" && m.delivery === "sent" && this.receipts.has(m.id);
|
|
19856
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);
|
|
19857
20601
|
}
|
|
19858
|
-
/**
|
|
19859
|
-
* Looks at each message, oldest first: it goes if the chat can carry it. The ones the chat could not carry are
|
|
19860
|
-
* handed back, and asked once more at the moment a later one is found able to go (`catchUp`): a link says it is open
|
|
19861
|
-
* a moment before it carries text, and a look that began in that moment sent the later messages and left the first
|
|
19862
|
-
* ones behind.
|
|
19863
|
-
*/
|
|
19864
20602
|
/** Whether this chat's waiting files go through this outbox (`Resender.sendFile`). */
|
|
19865
20603
|
get sendsFiles() {
|
|
19866
20604
|
return !!this.resender.sendFile;
|
|
@@ -19875,11 +20613,20 @@ var Outbox = class {
|
|
|
19875
20613
|
return false;
|
|
19876
20614
|
}
|
|
19877
20615
|
if (passed) await this.catchUp(passed);
|
|
19878
|
-
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
|
+
}
|
|
19879
20620
|
await this.resender.sendFile(message).catch(() => {});
|
|
19880
20621
|
this.forget(message.id);
|
|
19881
20622
|
return true;
|
|
19882
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
|
+
*/
|
|
19883
20630
|
async inOrder(messages) {
|
|
19884
20631
|
const passed = [];
|
|
19885
20632
|
for (const message of messages) {
|
|
@@ -19974,15 +20721,16 @@ var Outbox = class {
|
|
|
19974
20721
|
await this.store.update(id, "delivered");
|
|
19975
20722
|
this.clear(id);
|
|
19976
20723
|
this.forget(id);
|
|
19977
|
-
if (this.
|
|
20724
|
+
if (this.scheduled) this.flush().catch(() => {});
|
|
19978
20725
|
}
|
|
19979
20726
|
/**
|
|
19980
20727
|
* The chat can carry text again (its link came back, or the DHT path freed up): every queued message goes now,
|
|
19981
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
|
|
19982
|
-
* (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.
|
|
19983
20731
|
*/
|
|
19984
20732
|
flush({ reopened = false } = {}) {
|
|
19985
|
-
if (this.stopped || !reopened && !this.
|
|
20733
|
+
if (this.stopped || !reopened && !this.scheduled) return Promise.resolve();
|
|
19986
20734
|
const opened = reopened ? ++this.opened : void 0;
|
|
19987
20735
|
return this.turn(async () => {
|
|
19988
20736
|
if (this.stopped) return;
|
|
@@ -19990,15 +20738,47 @@ var Outbox = class {
|
|
|
19990
20738
|
await this.inOrder(this.due(await this.store.read(), opened));
|
|
19991
20739
|
});
|
|
19992
20740
|
}
|
|
19993
|
-
/**
|
|
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
|
+
*/
|
|
19994
20748
|
async check(id) {
|
|
19995
20749
|
if (this.stopped) return;
|
|
20750
|
+
if (this.waiting.get(id)?.timer || this.receipts.has(id)) return;
|
|
19996
20751
|
const messages = await this.store.read(), message = messages.find((m) => m.id === id);
|
|
19997
20752
|
if (!message || !(toGo(message) || this.file(message))) {
|
|
19998
20753
|
await this.attempt(id);
|
|
19999
20754
|
return;
|
|
20000
20755
|
}
|
|
20001
|
-
|
|
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);
|
|
20002
20782
|
}
|
|
20003
20783
|
/**
|
|
20004
20784
|
* A queued or waiting message is looked at; one sent and awaiting its receipt goes again now, if the chat can carry
|
|
@@ -20006,7 +20786,7 @@ var Outbox = class {
|
|
|
20006
20786
|
*/
|
|
20007
20787
|
async again(message, passed) {
|
|
20008
20788
|
if (message.sender !== "me" || message.via === "hold") return true;
|
|
20009
|
-
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);
|
|
20010
20790
|
if (message.delivery !== "sent" || !this.receipts.has(message.id)) return true;
|
|
20011
20791
|
this.clear(message.id);
|
|
20012
20792
|
if (this.resender.ready(message)) {
|
|
@@ -20050,10 +20830,15 @@ var Outbox = class {
|
|
|
20050
20830
|
/**
|
|
20051
20831
|
* Looks at one queued message: sends it if the chat can carry it, hands it to store-and-forward, or waits more.
|
|
20052
20832
|
* False when the chat could not carry it and it still waits. `passed`: older messages the chat could not carry a
|
|
20053
|
-
* 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.
|
|
20054
20835
|
*/
|
|
20055
|
-
async attempt(id, passed) {
|
|
20836
|
+
async attempt(id, passed, read) {
|
|
20056
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
|
+
}
|
|
20057
20842
|
const message = await this.one(id);
|
|
20058
20843
|
if (!message || message.delivery !== "queued" && message.delivery !== "waiting" || message.via === "hold") {
|
|
20059
20844
|
this.forget(id);
|
|
@@ -20105,10 +20890,12 @@ var Outbox = class {
|
|
|
20105
20890
|
schedule(id) {
|
|
20106
20891
|
const wait = this.waiting.get(id) ?? {
|
|
20107
20892
|
step: 0,
|
|
20108
|
-
since: this.now()
|
|
20893
|
+
since: this.now(),
|
|
20894
|
+
at: 0
|
|
20109
20895
|
};
|
|
20110
20896
|
if (wait.timer) clearTimeout(wait.timer);
|
|
20111
20897
|
const delay = this.policy.backoffMs[Math.min(wait.step, this.policy.backoffMs.length - 1)];
|
|
20898
|
+
wait.at = this.now() + delay;
|
|
20112
20899
|
wait.timer = setTimeout(() => {
|
|
20113
20900
|
wait.timer = void 0;
|
|
20114
20901
|
wait.step++;
|
|
@@ -20177,6 +20964,9 @@ var EditQueue = class {
|
|
|
20177
20964
|
restoredAt = -Infinity;
|
|
20178
20965
|
/** The rows known to have an edit on its way, each with when it was last told (`edited`), by `tick`. */
|
|
20179
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();
|
|
20180
20970
|
tick = 0;
|
|
20181
20971
|
/** The chat's history was read for the rows with an edit on its way: `known` has them all, until the next session. */
|
|
20182
20972
|
learnt = false;
|
|
@@ -20230,40 +21020,57 @@ var EditQueue = class {
|
|
|
20230
21020
|
}
|
|
20231
21021
|
/**
|
|
20232
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
|
|
20233
|
-
* read
|
|
20234
|
-
* 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.
|
|
20235
21025
|
*/
|
|
20236
|
-
async pending() {
|
|
21026
|
+
async pending(only) {
|
|
20237
21027
|
const waits = (m) => m.sender === "me" && !!m.edit?.pending;
|
|
20238
|
-
const
|
|
20239
|
-
if (!
|
|
21028
|
+
const read = this.deps.rows;
|
|
21029
|
+
if (!read || !this.learnt) {
|
|
20240
21030
|
const all = await this.deps.read(), rows = all.filter(waits);
|
|
20241
|
-
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
|
+
}
|
|
20242
21035
|
this.learnt = true;
|
|
20243
21036
|
return {
|
|
20244
21037
|
rows,
|
|
20245
21038
|
all
|
|
20246
21039
|
};
|
|
20247
21040
|
}
|
|
20248
|
-
const asOf = this.tick, ids = [...this.known.keys()];
|
|
20249
|
-
const read = await Promise.all(ids.map((id) => row(id)));
|
|
21041
|
+
const asOf = this.tick, ids = only ?? [...this.known.keys()];
|
|
20250
21042
|
const rows = [];
|
|
20251
|
-
read.forEach((m, i) => {
|
|
20252
|
-
if (m && waits(m))
|
|
20253
|
-
|
|
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
|
+
}
|
|
20254
21052
|
});
|
|
20255
21053
|
return { rows: rows.sort((a, b) => a.timestamp - b.timestamp) };
|
|
20256
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
|
+
}
|
|
20257
21064
|
/** The contact confirmed edit `seq` of my message with this wire id. */
|
|
20258
21065
|
async received(wireId, seq) {
|
|
20259
|
-
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);
|
|
20260
21067
|
if (!row?.edit?.pending || row.edit.seq > seq) return;
|
|
20261
21068
|
this.sent.delete(row.id);
|
|
20262
21069
|
await this.deps.settle(row.id, row.edit.seq);
|
|
20263
21070
|
}
|
|
20264
21071
|
/** The contact confirmed, on the DHT floor, the edit that went there under this id (`dhtEditId`). */
|
|
20265
21072
|
async receivedOnDht(id) {
|
|
20266
|
-
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);
|
|
20267
21074
|
if (!row) return;
|
|
20268
21075
|
this.sent.delete(row.id);
|
|
20269
21076
|
await this.deps.settle(row.id, row.edit.seq);
|
|
@@ -20274,7 +21081,7 @@ var EditQueue = class {
|
|
|
20274
21081
|
this.timer = void 0;
|
|
20275
21082
|
}
|
|
20276
21083
|
async run() {
|
|
20277
|
-
if (this.stopped) return;
|
|
21084
|
+
if (this.stopped || !this.deps.ready()) return;
|
|
20278
21085
|
const now = this.now();
|
|
20279
21086
|
const said = (m) => this.sent.get(m.id)?.seq === m.edit.seq ? 1 : 0;
|
|
20280
21087
|
const { rows, all } = await this.pending();
|
|
@@ -20330,6 +21137,7 @@ var EditQueue = class {
|
|
|
20330
21137
|
...message.card && { sc: message.card }
|
|
20331
21138
|
}, message)) continue;
|
|
20332
21139
|
if (edit.restore) this.restoredAt = now;
|
|
21140
|
+
this.dhtIds.set(message.id, dhtEditId(message.wireId, edit.seq));
|
|
20333
21141
|
this.sent.set(message.id, {
|
|
20334
21142
|
seq: edit.seq,
|
|
20335
21143
|
at: now,
|
|
@@ -20980,6 +21788,15 @@ var db = {
|
|
|
20980
21788
|
return wrap((await store(STORES$1.messages, "readonly")).get([linkId, id]));
|
|
20981
21789
|
},
|
|
20982
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
|
+
/**
|
|
20983
21800
|
* The newest `limit` messages of a chat before `before`, oldest first, in `getMessages`' order (time, then id), and
|
|
20984
21801
|
* whether older ones remain. Reads those rows only: a long chat's first page costs what a short one's does.
|
|
20985
21802
|
* `before`: a message's place (the page ends just before it), or a time (`id` left out: the page ends before it).
|
|
@@ -21513,6 +22330,12 @@ async function myPin(host, chat, messageId, remove, current, now = Date.now()) {
|
|
|
21513
22330
|
* about a catch-up, whose answers end within the three minutes the app waits for the first.
|
|
21514
22331
|
*/
|
|
21515
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;
|
|
21516
22339
|
AUTHOR_ORDER_WINDOW_MS / 2;
|
|
21517
22340
|
/** Whether `a` was written after `b` by the same member: a later epoch, or a higher number in the same one. */
|
|
21518
22341
|
function after(a, b) {
|
|
@@ -21522,10 +22345,12 @@ function after(a, b) {
|
|
|
21522
22345
|
* A member's messages in a private group in the order that member wrote them (WISP 400, requirement 10, and WISP 902
|
|
21523
22346
|
* § Catch-up). A message is placed where it arrives. When one comes of a member whose later messages were placed here
|
|
21524
22347
|
* moments ago (two catch-up answers raced: the author's own newest 32 and another member's hand-on of the older ones),
|
|
21525
|
-
*
|
|
21526
|
-
* the
|
|
21527
|
-
*
|
|
21528
|
-
*
|
|
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.
|
|
21529
22354
|
*/
|
|
21530
22355
|
var GroupAuthorOrder = class {
|
|
21531
22356
|
host;
|
|
@@ -21533,6 +22358,7 @@ var GroupAuthorOrder = class {
|
|
|
21533
22358
|
missing;
|
|
21534
22359
|
/** Per group: the members' rows followed, in the order they are placed in the history. */
|
|
21535
22360
|
rows = /* @__PURE__ */ new Map();
|
|
22361
|
+
came = 0;
|
|
21536
22362
|
/** Per group: the members with a row placed above an older one of theirs, since when, and when their last row came. */
|
|
21537
22363
|
due = /* @__PURE__ */ new Map();
|
|
21538
22364
|
running = /* @__PURE__ */ new Set();
|
|
@@ -21541,14 +22367,37 @@ var GroupAuthorOrder = class {
|
|
|
21541
22367
|
this.linkOf = linkOf;
|
|
21542
22368
|
this.missing = missing;
|
|
21543
22369
|
}
|
|
21544
|
-
/**
|
|
21545
|
-
|
|
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) {
|
|
21546
22387
|
if (!this.host.placeAgain) return;
|
|
21547
22388
|
const rows = (this.rows.get(groupId) ?? []).filter((r) => r.at > row.at - AUTHOR_ORDER_WINDOW_MS).slice(-575);
|
|
21548
|
-
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);
|
|
21549
22396
|
this.rows.set(groupId, rows);
|
|
21550
22397
|
let due = this.due.get(groupId);
|
|
21551
|
-
|
|
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);
|
|
21552
22401
|
if (!due) this.due.set(groupId, due = /* @__PURE__ */ new Map());
|
|
21553
22402
|
due.set(row.sender, {
|
|
21554
22403
|
since: due.get(row.sender)?.since ?? row.at,
|
|
@@ -21591,21 +22440,22 @@ var GroupAuthorOrder = class {
|
|
|
21591
22440
|
const newer = mine.filter((r) => !after(from, r)).sort((a, b) => after(a, b) ? 1 : after(b, a) ? -1 : 0);
|
|
21592
22441
|
const ids = new Set(newer.map((r) => r.id));
|
|
21593
22442
|
const first = rows.findIndex((r) => ids.has(r.id));
|
|
21594
|
-
let end = rows
|
|
21595
|
-
for (
|
|
21596
|
-
if (
|
|
21597
|
-
if (after(top,
|
|
21598
|
-
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;
|
|
21599
22448
|
break;
|
|
21600
22449
|
}
|
|
21601
|
-
top =
|
|
22450
|
+
top = r;
|
|
21602
22451
|
}
|
|
21603
|
-
const followers = rows.slice(first
|
|
22452
|
+
const followers = rows.slice(first).filter((r) => r.sender !== sender && r.n < end);
|
|
21604
22453
|
for (const r of followers) ids.add(r.id);
|
|
21605
22454
|
const moved = [...newer, ...followers];
|
|
21606
22455
|
this.rows.set(groupId, [...rows.filter((r) => !ids.has(r.id)), ...moved.map((r) => ({
|
|
21607
22456
|
...r,
|
|
21608
|
-
at: Math.max(r.at, now)
|
|
22457
|
+
at: Math.max(r.at, now),
|
|
22458
|
+
n: ++this.came
|
|
21609
22459
|
}))]);
|
|
21610
22460
|
await this.host.placeAgain(this.linkOf(groupId), moved.map((r) => r.id)).catch(() => {});
|
|
21611
22461
|
}
|
|
@@ -21615,8 +22465,8 @@ var GroupAuthorOrder = class {
|
|
|
21615
22465
|
}
|
|
21616
22466
|
}
|
|
21617
22467
|
};
|
|
21618
|
-
|
|
21619
|
-
|
|
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;
|
|
21620
22470
|
/**
|
|
21621
22471
|
* The lines of a group's history ("… joined", "… is no longer a member") among the messages of their moment, for a
|
|
21622
22472
|
* member back from away (WISP 902 § Catch-up). A message sealed under an epoch was written before its author knew of
|
|
@@ -21625,30 +22475,39 @@ var GroupAuthorOrder = class {
|
|
|
21625
22475
|
* its epoch) and, in a private group, the answers of several members race, so the messages of the epoch before can come
|
|
21626
22476
|
* after the line. Then the line, and the rows after it written knowing it, are placed again below them, in their order;
|
|
21627
22477
|
* no clock is read. A row only ever moves down, and only rows placed here in the last few minutes, so a live
|
|
21628
|
-
* 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.
|
|
21629
22481
|
*/
|
|
21630
22482
|
var GroupLineOrder = class {
|
|
21631
22483
|
host;
|
|
22484
|
+
onTheirWay;
|
|
21632
22485
|
/** Per history: the rows followed, in the order they are placed in it. */
|
|
21633
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();
|
|
21634
22489
|
due = /* @__PURE__ */ new Set();
|
|
21635
22490
|
running = /* @__PURE__ */ new Set();
|
|
21636
|
-
constructor(host) {
|
|
22491
|
+
constructor(host, onTheirWay = () => false) {
|
|
21637
22492
|
this.host = host;
|
|
22493
|
+
this.onTheirWay = onTheirWay;
|
|
21638
22494
|
}
|
|
21639
22495
|
/**
|
|
21640
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
|
|
21641
22497
|
* when, on the engine's clock. Call what this returns with whether it was new: a message below a line of a later
|
|
21642
|
-
* 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.
|
|
21643
22500
|
*/
|
|
21644
|
-
placing(linkId, row, at) {
|
|
22501
|
+
placing(linkId, row, at, above) {
|
|
21645
22502
|
if (!this.host.placeAgain) return () => {};
|
|
21646
22503
|
const followed = {
|
|
21647
22504
|
...row,
|
|
21648
22505
|
at
|
|
21649
22506
|
};
|
|
21650
22507
|
const rows = (this.rows.get(linkId) ?? []).filter((r) => r.at > at - AUTHOR_ORDER_WINDOW_MS).slice(-575);
|
|
21651
|
-
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);
|
|
21652
22511
|
this.rows.set(linkId, rows);
|
|
21653
22512
|
return (stored) => {
|
|
21654
22513
|
const now = this.rows.get(linkId);
|
|
@@ -21656,23 +22515,46 @@ var GroupLineOrder = class {
|
|
|
21656
22515
|
if (now) this.rows.set(linkId, now.filter((r) => r !== followed));
|
|
21657
22516
|
return;
|
|
21658
22517
|
}
|
|
21659
|
-
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);
|
|
21660
22523
|
};
|
|
21661
22524
|
}
|
|
21662
|
-
/** These rows were placed again below everything (`GroupAuthorOrder`), in this order. */
|
|
21663
|
-
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) {
|
|
21664
22527
|
const rows = this.rows.get(linkId);
|
|
21665
22528
|
if (!rows) return;
|
|
21666
22529
|
const moved = ids.map((id) => rows.find((r) => r.id === id)).filter((r) => !!r);
|
|
21667
22530
|
if (!moved.length) return;
|
|
21668
22531
|
this.rows.set(linkId, [...rows.filter((r) => !moved.includes(r)), ...moved]);
|
|
21669
|
-
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
|
+
}
|
|
21670
22545
|
}
|
|
21671
22546
|
/** The group's history is gone (left or forgotten). */
|
|
21672
22547
|
forget(linkId) {
|
|
21673
22548
|
this.rows.delete(linkId);
|
|
22549
|
+
this.waiting.delete(linkId);
|
|
21674
22550
|
this.due.delete(linkId);
|
|
21675
22551
|
}
|
|
22552
|
+
wait(linkId, at) {
|
|
22553
|
+
this.waiting.set(linkId, {
|
|
22554
|
+
since: this.waiting.get(linkId)?.since ?? at,
|
|
22555
|
+
last: at
|
|
22556
|
+
});
|
|
22557
|
+
}
|
|
21676
22558
|
/** The history's rows put in order; what comes while a move is written goes in the next turn. */
|
|
21677
22559
|
run(linkId) {
|
|
21678
22560
|
this.due.add(linkId);
|
|
@@ -21688,8 +22570,9 @@ var GroupLineOrder = class {
|
|
|
21688
22570
|
if (above < 0) order.push(row);
|
|
21689
22571
|
else order.splice(above, 0, row);
|
|
21690
22572
|
}
|
|
22573
|
+
const place = new Map(rows.map((r, i) => [r, i]));
|
|
21691
22574
|
let from = 0;
|
|
21692
|
-
while (from <
|
|
22575
|
+
while (from < order.length && (from === 0 || place.get(order[from]) > place.get(order[from - 1]))) from++;
|
|
21693
22576
|
if (from === rows.length) continue;
|
|
21694
22577
|
const last = rows[rows.length - 1].at;
|
|
21695
22578
|
for (const r of order.slice(from)) r.at = Math.max(r.at, last);
|
|
@@ -21875,6 +22758,14 @@ var CUT_OFF_STEP_UP_MS = 15e3;
|
|
|
21875
22758
|
/** A lobby request this recent is from a member still looking fast for my side of the edge: mine opens expecting it. */
|
|
21876
22759
|
var LOBBY_EXPECT_MS = 3e4;
|
|
21877
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
|
+
/**
|
|
21878
22769
|
* A lobby is one record every member asking a hub writes, each reading it and writing it back with its own request: two
|
|
21879
22770
|
* members writing at once (both cut off by the same leave, in the same second) each write what they read, and the later
|
|
21880
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
|
|
@@ -22335,10 +23226,10 @@ var Communities = class {
|
|
|
22335
23226
|
await this.require(groupId).rotate();
|
|
22336
23227
|
}
|
|
22337
23228
|
async setPicture(groupId, picture) {
|
|
22338
|
-
await this.require(groupId).setPicture(picture);
|
|
23229
|
+
await this.require(groupId).setPicture(picture, this.now());
|
|
22339
23230
|
}
|
|
22340
23231
|
async rename(groupId, name) {
|
|
22341
|
-
await this.require(groupId).rename(name);
|
|
23232
|
+
await this.require(groupId).rename(name, this.now());
|
|
22342
23233
|
}
|
|
22343
23234
|
/** A new link (the old one reaches nobody), or none. */
|
|
22344
23235
|
async replaceLink(groupId, off = false) {
|
|
@@ -22401,6 +23292,7 @@ var Communities = class {
|
|
|
22401
23292
|
/** Everything timed: beacon, hub election, lobbies, knocks, leaves, edges. The engine calls it every second or so. */
|
|
22402
23293
|
async tick(now = Date.now()) {
|
|
22403
23294
|
this.time = now;
|
|
23295
|
+
this.lineOrder.tick(now);
|
|
22404
23296
|
const active = [...this.live.values()].filter((l) => l.session.status === "active");
|
|
22405
23297
|
this.sharing = {
|
|
22406
23298
|
groups: Math.max(1, active.length),
|
|
@@ -22534,7 +23426,7 @@ var Communities = class {
|
|
|
22534
23426
|
const busy = this.admitting(groupId, live, now);
|
|
22535
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);
|
|
22536
23428
|
if (this.hubDropped(groupId, live, others, now)) live.lobbyBusyUntil = Math.max(live.lobbyBusyUntil, now + this.timings.lobbyBusyMs);
|
|
22537
|
-
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;
|
|
22538
23430
|
if (now - live.lastLobbyPoll >= lobbyEvery) {
|
|
22539
23431
|
live.lastLobbyPoll = now;
|
|
22540
23432
|
await this.pollLobby(groupId, live, now);
|
|
@@ -22578,6 +23470,9 @@ var Communities = class {
|
|
|
22578
23470
|
}
|
|
22579
23471
|
let kept = live.myHubs.filter((key) => (fresh.has(key) || this.recentHub(live, key)) && !live.hubsAvoided.has(key));
|
|
22580
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
|
+
}
|
|
22581
23476
|
const picked = pickHubs(me, others, now, new Set(live.hubsAvoided.keys()));
|
|
22582
23477
|
const bare = ![...edges.values()].some((id) => this.host.linkReady(id, 2));
|
|
22583
23478
|
for (const key of picked) if (kept.length < Math.max(1, picked.length) && !kept.includes(key)) {
|
|
@@ -22676,7 +23571,11 @@ var Communities = class {
|
|
|
22676
23571
|
const id = edges.get(key);
|
|
22677
23572
|
if (this.gone(live, key)) live.members.delete(key);
|
|
22678
23573
|
else if (id && this.host.linkReady(id, 2)) live.members.set(key, now);
|
|
22679
|
-
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
|
+
}
|
|
22680
23579
|
}
|
|
22681
23580
|
this.shed(groupId, live, now);
|
|
22682
23581
|
return live.members.size;
|
|
@@ -22729,7 +23628,8 @@ var Communities = class {
|
|
|
22729
23628
|
async readBeacon(groupId, live, now) {
|
|
22730
23629
|
live.lastBeaconRead = now;
|
|
22731
23630
|
const keys = beaconKeys(live.session.state.rv, groupId);
|
|
22732
|
-
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);
|
|
22733
23633
|
if (records === void 0) {
|
|
22734
23634
|
live.beaconFailedAt = now;
|
|
22735
23635
|
live.beaconAskedAt ||= now;
|
|
@@ -22762,9 +23662,10 @@ var Communities = class {
|
|
|
22762
23662
|
async publishBeacon(groupId, live, now, listed) {
|
|
22763
23663
|
live.lastBeaconTry = now;
|
|
22764
23664
|
const keys = beaconKeys(live.session.state.rv, groupId);
|
|
23665
|
+
const back = live.hub && this.wayBack(groupId, live);
|
|
22765
23666
|
let existing = live.beacon, head = live.head;
|
|
22766
23667
|
if (live.lastBeaconRead !== now || live.beaconFailedAt === now) {
|
|
22767
|
-
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);
|
|
22768
23669
|
if (read === void 0 && live.session.roster.length > 1) throw new Error("The beacon could not be read; not published");
|
|
22769
23670
|
const records = read ?? [];
|
|
22770
23671
|
existing = live.hubClocks.inMyTime(readBeacon(keys, records), now);
|
|
@@ -22777,7 +23678,7 @@ var Communities = class {
|
|
|
22777
23678
|
since: live.hubSince || now
|
|
22778
23679
|
} : null, now, (key) => !this.gone(live, key));
|
|
22779
23680
|
const newest = newerHead(head, this.newestFrame(live), now);
|
|
22780
|
-
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);
|
|
22781
23682
|
live.beacon = hubs;
|
|
22782
23683
|
live.head = newest;
|
|
22783
23684
|
this.noteHubs(live, now);
|
|
@@ -22909,12 +23810,13 @@ var Communities = class {
|
|
|
22909
23810
|
/** A member asks a hub for an edge: its key in the hub's lobby, and its side of the edge started. */
|
|
22910
23811
|
async askHub(groupId, live, hub, now) {
|
|
22911
23812
|
const keys = lobbyKeys(live.session.state.rv, groupId, hub);
|
|
22912
|
-
const
|
|
23813
|
+
const back = this.wayBack(groupId, live);
|
|
23814
|
+
const existing = readLobby(keys, await this.host.resolve(keys.identity.pubKeyZ32, !back, !back, back) ?? []);
|
|
22913
23815
|
const theirs = now - live.hubClocks.ahead(hub);
|
|
22914
23816
|
await this.host.publish(keys.identity, lobbyRecords(keys, mergeLobby(existing, {
|
|
22915
23817
|
key: live.session.myKey,
|
|
22916
23818
|
ts: theirs
|
|
22917
|
-
}, theirs)),
|
|
23819
|
+
}, theirs)), !back, !back, back);
|
|
22918
23820
|
(live.lobbyChecks ??= /* @__PURE__ */ new Map()).set(hub, {
|
|
22919
23821
|
at: now + LOBBY_CHECK_MS + Math.floor(this.random() * LOBBY_CHECK_JITTER_MS),
|
|
22920
23822
|
ts: Math.floor(theirs / 1e3) * 1e3
|
|
@@ -22937,7 +23839,8 @@ var Communities = class {
|
|
|
22937
23839
|
}
|
|
22938
23840
|
async pollLobby(groupId, live, now) {
|
|
22939
23841
|
const keys = lobbyKeys(live.session.state.rv, groupId, live.session.myKey);
|
|
22940
|
-
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) ?? []);
|
|
22941
23844
|
for (const { key, ts } of entries) {
|
|
22942
23845
|
if (now - ts > COMMUNITY_TOPOLOGY.lobbyFreshMs || key === live.session.myKey) continue;
|
|
22943
23846
|
if (live.session.wasRemoved(key)) {
|
|
@@ -23038,6 +23941,34 @@ var Communities = class {
|
|
|
23038
23941
|
return live.hub && (now < live.warmUntil || now < live.crowdUntil || now < live.lobbyBusyUntil || this.admitting(groupId, live, now));
|
|
23039
23942
|
}
|
|
23040
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
|
+
/**
|
|
23041
23972
|
* Which knock records to read now (none, most of the time): every one at once the first time
|
|
23042
23973
|
* (a hub just elected, an app back, a change of door), since a knock left while nobody was at the
|
|
23043
23974
|
* door may be in any; then the door the bell, and the others in turn; the other hubs one in turn, slowly.
|
|
@@ -23504,6 +24435,7 @@ var Communities = class {
|
|
|
23504
24435
|
noteCame(this.stored.get(id), came, m.sender !== session.myKey, mentioned);
|
|
23505
24436
|
},
|
|
23506
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,
|
|
23507
24439
|
pair: (m) => this.deliver(id, () => this.host.communityPair?.(id, m.sender, m.payload)),
|
|
23508
24440
|
changed: () => {
|
|
23509
24441
|
this.membershipChanged(id);
|
|
@@ -24751,6 +25683,8 @@ var Groups = class {
|
|
|
24751
25683
|
/** Admin side: entry sessions of a member key that timed out in a row, and when the last did. */
|
|
24752
25684
|
entryTimeouts = /* @__PURE__ */ new Map();
|
|
24753
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;
|
|
24754
25688
|
/** Admin side: until when a group's link is looked at the warm pace. */
|
|
24755
25689
|
warmUntil = /* @__PURE__ */ new Map();
|
|
24756
25690
|
lastKnock = /* @__PURE__ */ new Map();
|
|
@@ -24781,6 +25715,12 @@ var Groups = class {
|
|
|
24781
25715
|
* whom, and when it was asked. Their sync goes once that answer has come.
|
|
24782
25716
|
*/
|
|
24783
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();
|
|
24784
25724
|
/** Per mesh group and member, when a message last came over its edge (its own, or one it handed on). */
|
|
24785
25725
|
lastHanded = /* @__PURE__ */ new Map();
|
|
24786
25726
|
/** Admin side, per group and member let in: when its welcome was last sent again, over its chat or the link (`REWELCOME_MS`). */
|
|
@@ -24846,16 +25786,16 @@ var Groups = class {
|
|
|
24846
25786
|
}
|
|
24847
25787
|
}, hubTimings);
|
|
24848
25788
|
this.typings = new GroupTypings(host);
|
|
24849
|
-
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
|
+
});
|
|
24850
25793
|
const placeAgain = host.placeAgain && (async (linkId, ids) => {
|
|
24851
25794
|
const done = host.placeAgain(linkId, ids);
|
|
24852
|
-
this.lineOrder.moved(linkId, ids);
|
|
25795
|
+
this.lineOrder.moved(linkId, ids, this.now());
|
|
24853
25796
|
await done;
|
|
24854
25797
|
});
|
|
24855
|
-
this.authorOrder = new GroupAuthorOrder({ placeAgain }, MESSAGE_LINK, (groupId, sender) =>
|
|
24856
|
-
const session = this.sessions.get(groupId);
|
|
24857
|
-
return !!session && session.missing(sender, this.gapsGone.get(groupId)?.get(sender)?.[session.epoch]) > 0;
|
|
24858
|
-
});
|
|
25798
|
+
this.authorOrder = new GroupAuthorOrder({ placeAgain }, MESSAGE_LINK, (groupId, sender) => this.onTheirWay(groupId, sender));
|
|
24859
25799
|
this.signals = new MeshSignals(host, {
|
|
24860
25800
|
session: (id) => this.sessions.get(id),
|
|
24861
25801
|
stored: (id) => this.stored.get(id),
|
|
@@ -25107,10 +26047,11 @@ var Groups = class {
|
|
|
25107
26047
|
if (this.isCommunity(groupId)) throw new Error("Share the group's link with them: anyone who opens it joins");
|
|
25108
26048
|
const session = this.session(groupId);
|
|
25109
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);
|
|
25110
26051
|
const group = this.stored.get(groupId);
|
|
25111
26052
|
if (Object.entries(group.contacts ?? {}).some(([key, id]) => id === linkId && rosterHas(session.roster, key))) throw new Error("This contact is already a member");
|
|
25112
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`);
|
|
25113
|
-
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.");
|
|
25114
26055
|
const blocked = this.growthBlocked(group, session, session.roster.length + (this.invited.get(groupId)?.size ?? 0) + 1, linkId);
|
|
25115
26056
|
if (blocked) throw new Error(blocked);
|
|
25116
26057
|
this.host.sendOnLink(linkId, session.inviteFrame());
|
|
@@ -25637,6 +26578,7 @@ var Groups = class {
|
|
|
25637
26578
|
this.lingering.delete(groupId);
|
|
25638
26579
|
this.relayAsked.delete(groupId);
|
|
25639
26580
|
this.syncHeld.delete(groupId);
|
|
26581
|
+
this.told.delete(groupId);
|
|
25640
26582
|
this.lastHanded.delete(groupId);
|
|
25641
26583
|
this.rewelcomed.delete(groupId);
|
|
25642
26584
|
this.hubs.forget(groupId);
|
|
@@ -25752,6 +26694,8 @@ var Groups = class {
|
|
|
25752
26694
|
this.ticking = true;
|
|
25753
26695
|
try {
|
|
25754
26696
|
this.authorOrder.tick(now);
|
|
26697
|
+
this.lineOrder.tick(now);
|
|
26698
|
+
const quiet = [];
|
|
25755
26699
|
for (const group of [...this.stored.values()]) {
|
|
25756
26700
|
if (group.left) {
|
|
25757
26701
|
if (now - group.left.at > LEFT_KEPT_MS) await this.forget(group.id);
|
|
@@ -25793,12 +26737,25 @@ var Groups = class {
|
|
|
25793
26737
|
const linkId = this.host.entries(group.id).get(key);
|
|
25794
26738
|
if (linkId) await this.host.closeEdge(linkId);
|
|
25795
26739
|
}
|
|
25796
|
-
const
|
|
25797
|
-
|
|
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) {
|
|
25798
26746
|
this.lastPoll.set(group.id, now);
|
|
25799
26747
|
await this.answerKnocks(group, session, now).catch(() => {});
|
|
25800
26748
|
}
|
|
25801
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
|
+
}
|
|
25802
26759
|
for (const [key, until] of this.refused) if (until <= now) this.refused.delete(key);
|
|
25803
26760
|
for (const [key, { at }] of this.entryTimeouts) if (now - at > 36e5) this.entryTimeouts.delete(key);
|
|
25804
26761
|
for (const [groupId, kept] of this.lingering) if (now >= kept.until) await this.endLinger(groupId);
|
|
@@ -25902,7 +26859,16 @@ var Groups = class {
|
|
|
25902
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. */
|
|
25903
26860
|
linkMember(groupId, linkId) {
|
|
25904
26861
|
for (const [peer, id] of this.host.entries(groupId)) if (id === linkId) return peer;
|
|
25905
|
-
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];
|
|
25906
26872
|
}
|
|
25907
26873
|
/**
|
|
25908
26874
|
* My packet for the edge to `to`, handed to links that reach that member or may pass it on (WISP 902 · Group Mesh
|
|
@@ -26295,8 +27261,7 @@ var Groups = class {
|
|
|
26295
27261
|
}
|
|
26296
27262
|
case "group-leave": {
|
|
26297
27263
|
const session = this.sessions.get(g);
|
|
26298
|
-
const
|
|
26299
|
-
const member = Object.entries(group?.contacts ?? {}).find(([, id]) => id === linkId)?.[0];
|
|
27264
|
+
const member = this.chatMember(g, linkId);
|
|
26300
27265
|
if (!session?.isAdmin || !member || member === session.myKey) return;
|
|
26301
27266
|
if (rosterHas(session.roster, member)) await session.remove(member, Date.now(), { left: true }).catch(() => {});
|
|
26302
27267
|
try {
|
|
@@ -26440,6 +27405,9 @@ var Groups = class {
|
|
|
26440
27405
|
return;
|
|
26441
27406
|
}
|
|
26442
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);
|
|
26443
27411
|
const seen = !!group.unseen?.includes(peerKey);
|
|
26444
27412
|
if (seen) {
|
|
26445
27413
|
const unseen = group.unseen.filter((key) => key !== peerKey);
|
|
@@ -26449,7 +27417,7 @@ var Groups = class {
|
|
|
26449
27417
|
const legacy = this.hubs.enabled && this.hubs.edgeReady(groupId, session, group, peerKey, this.host.linkReady(linkId, 4), this.now());
|
|
26450
27418
|
if (large || legacy || seen) this.store.putGroup(group).catch(() => {});
|
|
26451
27419
|
if (!this.holdSync(groupId, peerKey)) try {
|
|
26452
|
-
this.host.sendOnLink(linkId, session.syncFrame(this.askOf(groupId, session, peerKey)));
|
|
27420
|
+
this.host.sendOnLink(linkId, session.syncFrame(this.askOf(groupId, session, peerKey), peerKey));
|
|
26453
27421
|
} catch {
|
|
26454
27422
|
return;
|
|
26455
27423
|
}
|
|
@@ -26585,18 +27553,21 @@ var Groups = class {
|
|
|
26585
27553
|
}
|
|
26586
27554
|
/**
|
|
26587
27555
|
* The unreachable members whose messages `via` should hand on to me: each is asked of one connected member at a
|
|
26588
|
-
* 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.
|
|
26589
27560
|
*/
|
|
26590
27561
|
askOf(groupId, session, via) {
|
|
26591
27562
|
let asked = this.relayAsked.get(groupId);
|
|
26592
27563
|
if (!asked) this.relayAsked.set(groupId, asked = /* @__PURE__ */ new Map());
|
|
26593
|
-
const edges = this.host.edges(groupId), now = this.now();
|
|
27564
|
+
const edges = this.host.edges(groupId), now = this.now(), told = this.told.get(groupId);
|
|
26594
27565
|
const up = (key) => {
|
|
26595
27566
|
const edge = edges.get(key);
|
|
26596
27567
|
return !!edge && this.host.linkReady(edge);
|
|
26597
27568
|
};
|
|
26598
27569
|
const out = [];
|
|
26599
|
-
for (const key of
|
|
27570
|
+
for (const key of session.others.filter((key) => !up(key) || !told?.has(key))) {
|
|
26600
27571
|
if (key === via) continue;
|
|
26601
27572
|
const prior = asked.get(key);
|
|
26602
27573
|
if (prior && up(prior.via) && now - prior.at < 6e4) continue;
|
|
@@ -26677,7 +27648,7 @@ var Groups = class {
|
|
|
26677
27648
|
});
|
|
26678
27649
|
}
|
|
26679
27650
|
try {
|
|
26680
|
-
this.host.sendOnLink(edges.get(next), session.syncFrame([...ask]));
|
|
27651
|
+
this.host.sendOnLink(edges.get(next), session.syncFrame([...ask], next));
|
|
26681
27652
|
} catch {}
|
|
26682
27653
|
}
|
|
26683
27654
|
/**
|
|
@@ -26698,9 +27669,9 @@ var Groups = class {
|
|
|
26698
27669
|
for (const key of tried.keys()) if (!lacking.has(key)) tried.delete(key);
|
|
26699
27670
|
const out = [];
|
|
26700
27671
|
for (const [key, mark] of lacking) {
|
|
26701
|
-
if (key === via || !up(key)) continue;
|
|
27672
|
+
if (key === via || !up(key) || !session.mayHold(via, key)) continue;
|
|
26702
27673
|
const prior = asked.get(key);
|
|
26703
|
-
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;
|
|
26704
27675
|
let entry = tried.get(key);
|
|
26705
27676
|
if (!entry || entry.mark !== mark) tried.set(key, entry = {
|
|
26706
27677
|
mark,
|
|
@@ -26746,6 +27717,11 @@ var Groups = class {
|
|
|
26746
27717
|
entry.at = now;
|
|
26747
27718
|
return true;
|
|
26748
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
|
+
}
|
|
26749
27725
|
/**
|
|
26750
27726
|
* Messages nobody holds any more: every connected member, their author included, was asked for the same missing
|
|
26751
27727
|
* numbers `MESH_GAP_AGAIN_MS` ago or more and none arrived. An author keeps its last 32 and the others the group's
|
|
@@ -26759,7 +27735,7 @@ var Groups = class {
|
|
|
26759
27735
|
for (const [key, mark] of lacking) {
|
|
26760
27736
|
const entry = tried.get(key);
|
|
26761
27737
|
if (!entry || entry.mark !== mark || entry.at === void 0 || now - entry.at < 1e4) continue;
|
|
26762
|
-
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;
|
|
26763
27739
|
let gone = this.gapsGone.get(groupId);
|
|
26764
27740
|
if (!gone) this.gapsGone.set(groupId, gone = /* @__PURE__ */ new Map());
|
|
26765
27741
|
const upTo = { ...gone.get(key) };
|
|
@@ -26809,7 +27785,9 @@ var Groups = class {
|
|
|
26809
27785
|
continue;
|
|
26810
27786
|
}
|
|
26811
27787
|
if (now - last.at < 1e4 || !up(last.via)) continue;
|
|
26812
|
-
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);
|
|
26813
27791
|
}
|
|
26814
27792
|
const asked = this.relayAsked.get(groupId);
|
|
26815
27793
|
for (const [via, keys] of again) {
|
|
@@ -26830,18 +27808,19 @@ var Groups = class {
|
|
|
26830
27808
|
});
|
|
26831
27809
|
}
|
|
26832
27810
|
try {
|
|
26833
|
-
this.host.sendOnLink(edges.get(via), session.syncFrame(keys));
|
|
27811
|
+
this.host.sendOnLink(edges.get(via), session.syncFrame(keys, via));
|
|
26834
27812
|
} catch {}
|
|
26835
27813
|
}
|
|
26836
27814
|
if (!fresh.size || !connected.length) return;
|
|
26837
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));
|
|
26838
27817
|
let via = at();
|
|
26839
|
-
for (let i = 1; i < connected.length &&
|
|
27818
|
+
for (let i = 1; i < connected.length && !helps(via); i++) via = at();
|
|
26840
27819
|
const keys = this.gapsOf(groupId, session, via, fresh);
|
|
26841
27820
|
if (!keys.length) return;
|
|
26842
27821
|
this.lastGossip.set(groupId, now);
|
|
26843
27822
|
try {
|
|
26844
|
-
this.host.sendOnLink(edges.get(via), session.syncFrame(keys));
|
|
27823
|
+
this.host.sendOnLink(edges.get(via), session.syncFrame(keys, via));
|
|
26845
27824
|
} catch {}
|
|
26846
27825
|
}
|
|
26847
27826
|
/**
|
|
@@ -26863,9 +27842,12 @@ var Groups = class {
|
|
|
26863
27842
|
const edges = this.host.edges(groupId);
|
|
26864
27843
|
if (!this.hubs.large(session) && session.others.some((key) => !edges.has(key))) this.reconcileEdges(groupId);
|
|
26865
27844
|
}
|
|
26866
|
-
const
|
|
26867
|
-
this.noteDown(groupId, session,
|
|
26868
|
-
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));
|
|
26869
27851
|
this.gapsAgain(groupId, session, now, connected);
|
|
26870
27852
|
if (now - (this.lastGossip.get(groupId) ?? -Infinity) < 6e4) continue;
|
|
26871
27853
|
const edges = this.host.edges(groupId);
|
|
@@ -26883,7 +27865,7 @@ var Groups = class {
|
|
|
26883
27865
|
const ask = [...away, ...this.gapsOf(groupId, session, via, lacking)];
|
|
26884
27866
|
if (!this.ownGap(groupId, via, lacking) && !ask.length) continue;
|
|
26885
27867
|
try {
|
|
26886
|
-
this.host.sendOnLink(edges.get(via), session.syncFrame(ask));
|
|
27868
|
+
this.host.sendOnLink(edges.get(via), session.syncFrame(ask, via));
|
|
26887
27869
|
} catch {}
|
|
26888
27870
|
}
|
|
26889
27871
|
}
|
|
@@ -26987,21 +27969,27 @@ var Groups = class {
|
|
|
26987
27969
|
...m.frame && { frame: m.frame }
|
|
26988
27970
|
};
|
|
26989
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;
|
|
26990
27981
|
const placing = completed ? void 0 : this.lineOrder.placing(MESSAGE_LINK(state.id), {
|
|
26991
27982
|
id: m.id,
|
|
26992
27983
|
epoch: m.epoch,
|
|
26993
27984
|
line: false
|
|
26994
|
-
}, this.now());
|
|
26995
|
-
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);
|
|
26996
27987
|
placing?.(stored !== false);
|
|
26997
27988
|
if (m.frame && (completed || stored !== false)) await this.frameKept(state.id, m.id, m.frame);
|
|
26998
27989
|
if (!completed && stored !== false) this.authorOrder.placed(state.id, {
|
|
26999
|
-
|
|
27000
|
-
epoch: m.epoch,
|
|
27001
|
-
seq: m.seq,
|
|
27002
|
-
id: m.id,
|
|
27990
|
+
...row,
|
|
27003
27991
|
at: this.now()
|
|
27004
|
-
});
|
|
27992
|
+
}, placedAbove);
|
|
27005
27993
|
const came = m.sender === session.myKey ? timestamp : cameAt(timestamp, stored, arrivalNow(this.now()));
|
|
27006
27994
|
this.lastMessageAt.set(state.id, Math.max(this.lastMessageAt.get(state.id) ?? 0, came));
|
|
27007
27995
|
if (mentioned) this.lastMentionAt.set(state.id, Math.max(this.lastMentionAt.get(state.id) ?? 0, came));
|
|
@@ -30224,7 +31212,10 @@ var SWAP_POLL_MS = 3e4;
|
|
|
30224
31212
|
var SWAP_SLOW_POLL_MS = 6e5;
|
|
30225
31213
|
/** How long a swap whose inputs were given back is still asked about, in case the mint made it after all. */
|
|
30226
31214
|
var SWAP_WATCH_MS = 36e5;
|
|
30227
|
-
|
|
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();
|
|
30228
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();
|
|
30229
31220
|
if (!Number.isSafeInteger(fee) || fee < 0) throw new Error("Invalid Cashu prepared fee");
|
|
30230
31221
|
return fee;
|
|
@@ -30280,9 +31271,9 @@ var toStored = (mint, p, reserved) => ({
|
|
|
30280
31271
|
});
|
|
30281
31272
|
/** The note of the ecash "Get test coins" brought (the engine plays its own sound for it). */
|
|
30282
31273
|
var TEST_COINS_NOTE = "Test coins from the test mint";
|
|
30283
|
-
var swapId = () => `swap-${Date.now()}-${
|
|
31274
|
+
var swapId = () => `swap-${Date.now()}-${crypto.randomUUID().slice(0, 8)}`;
|
|
30284
31275
|
var walletTx = (mint, kind, amount, fee, note) => ({
|
|
30285
|
-
id: `${Date.now()}-${
|
|
31276
|
+
id: `${Date.now()}-${crypto.randomUUID().slice(0, 8)}`,
|
|
30286
31277
|
timestamp: Date.now(),
|
|
30287
31278
|
mint,
|
|
30288
31279
|
kind,
|
|
@@ -30591,6 +31582,7 @@ var CashuWallet = class CashuWallet {
|
|
|
30591
31582
|
const quote = await wrap((await store(STORES$1.quotes, "readonly")).get(id));
|
|
30592
31583
|
if (!quote || quote.issuedUnclaimed || quote.held) return null;
|
|
30593
31584
|
const wallet = await this.wallet(quote.mint);
|
|
31585
|
+
const { OutputData, isMintOperationError } = await loadCashu();
|
|
30594
31586
|
const answer = await wallet.checkMintQuoteBolt11(quote.quote);
|
|
30595
31587
|
const { state } = answer;
|
|
30596
31588
|
if (state === "UNPAID") {
|
|
@@ -30634,7 +31626,10 @@ var CashuWallet = class CashuWallet {
|
|
|
30634
31626
|
/** The ecash of a paid invoice goes in with its history line, in the transaction that drops the quote. */
|
|
30635
31627
|
async storeMinted(quote, proofs) {
|
|
30636
31628
|
const minted = sats(proofs);
|
|
30637
|
-
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
|
+
};
|
|
30638
31633
|
await transact([
|
|
30639
31634
|
STORES$1.proofs,
|
|
30640
31635
|
STORES$1.walletTx,
|
|
@@ -30670,7 +31665,7 @@ var CashuWallet = class CashuWallet {
|
|
|
30670
31665
|
if (await this.balanceAt(mint) < amount) continue;
|
|
30671
31666
|
return this.locked(mint, async () => {
|
|
30672
31667
|
const { inputs, keep, send, swap } = await this.split(mint, amount, await this.proofsAt(mint));
|
|
30673
|
-
const token = getEncodedToken({
|
|
31668
|
+
const token = (await loadCashu()).getEncodedToken({
|
|
30674
31669
|
mint,
|
|
30675
31670
|
proofs: send,
|
|
30676
31671
|
unit: UNIT$1
|
|
@@ -30704,10 +31699,10 @@ var CashuWallet = class CashuWallet {
|
|
|
30704
31699
|
return this.locked(mint, async () => {
|
|
30705
31700
|
const preview = await (await this.wallet(mint)).ops.send(amount, asProofLike(await this.proofsAt(mint))).includeFees(true).prepare();
|
|
30706
31701
|
return {
|
|
30707
|
-
fee: reviewedCashuFee(preview),
|
|
31702
|
+
fee: await reviewedCashuFee(preview),
|
|
30708
31703
|
prepared: {
|
|
30709
31704
|
mint,
|
|
30710
|
-
swap: serializeSwapPreview(preview)
|
|
31705
|
+
swap: (await loadCashu()).serializeSwapPreview(preview)
|
|
30711
31706
|
}
|
|
30712
31707
|
};
|
|
30713
31708
|
});
|
|
@@ -30715,8 +31710,8 @@ var CashuWallet = class CashuWallet {
|
|
|
30715
31710
|
/** Reserve before the mint call. A timeout keeps these inputs reserved for read-only recovery. */
|
|
30716
31711
|
async executeReviewedCashu(review, prepared) {
|
|
30717
31712
|
return this.locked(prepared.mint, async () => {
|
|
30718
|
-
const preview = deserializeSwapPreview(prepared.swap);
|
|
30719
|
-
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");
|
|
30720
31715
|
const tx = (await openDb()).transaction(STORES$1.proofs, "readwrite");
|
|
30721
31716
|
await new Promise((resolve, reject) => {
|
|
30722
31717
|
const proofs = tx.objectStore(STORES$1.proofs);
|
|
@@ -30746,6 +31741,7 @@ var CashuWallet = class CashuWallet {
|
|
|
30746
31741
|
const stored = await wrap((await store(STORES$1.payments, "readonly")).get(review.id));
|
|
30747
31742
|
if (stored?.token) return stored.token;
|
|
30748
31743
|
const wallet = await this.wallet(prepared.mint);
|
|
31744
|
+
const { OutputData } = await loadCashu();
|
|
30749
31745
|
const keep = (prepared.swap.keepOutputs ?? []).map(OutputData.deserialize);
|
|
30750
31746
|
const send = (prepared.swap.sendOutputs ?? []).map(OutputData.deserialize);
|
|
30751
31747
|
const outputs = [...keep, ...send];
|
|
@@ -30775,6 +31771,7 @@ var CashuWallet = class CashuWallet {
|
|
|
30775
31771
|
if (recovered.outputs.length === 0 && recovered.signatures.length === 0) return "none";
|
|
30776
31772
|
if (recovered.outputs.length !== outputs.length || recovered.signatures.length !== outputs.length) return "some";
|
|
30777
31773
|
const keys = (await wallet.mint.getKeys()).keysets;
|
|
31774
|
+
const { Amount } = await loadCashu();
|
|
30778
31775
|
const restored = [];
|
|
30779
31776
|
for (const output of outputs) {
|
|
30780
31777
|
const index = recovered.outputs.findIndex((o) => o.B_ === output.blindedMessage.B_);
|
|
@@ -30786,6 +31783,7 @@ var CashuWallet = class CashuWallet {
|
|
|
30786
31783
|
return restored;
|
|
30787
31784
|
}
|
|
30788
31785
|
async finishReviewedCashu(review, prepared, keep, send) {
|
|
31786
|
+
const { getEncodedToken, deserializeSwapPreview } = await loadCashu();
|
|
30789
31787
|
const token = getEncodedToken({
|
|
30790
31788
|
mint: prepared.mint,
|
|
30791
31789
|
proofs: send,
|
|
@@ -30828,6 +31826,7 @@ var CashuWallet = class CashuWallet {
|
|
|
30828
31826
|
* not SPENT), and the mint signed none of its outputs. Read-only: it asks, and never sends the swap.
|
|
30829
31827
|
*/
|
|
30830
31828
|
async reviewedCashuNeverSwapped(prepared) {
|
|
31829
|
+
const { OutputData, deserializeSwapPreview } = await loadCashu();
|
|
30831
31830
|
const preview = deserializeSwapPreview(prepared.swap);
|
|
30832
31831
|
if (!preview.inputs.length) return false;
|
|
30833
31832
|
const wallet = await this.wallet(prepared.mint);
|
|
@@ -30846,6 +31845,7 @@ var CashuWallet = class CashuWallet {
|
|
|
30846
31845
|
* pending. Read-only. A mint that does not restore cannot say, and the answer is no.
|
|
30847
31846
|
*/
|
|
30848
31847
|
async reviewedCashuSpentElsewhere(prepared) {
|
|
31848
|
+
const { OutputData, deserializeSwapPreview } = await loadCashu();
|
|
30849
31849
|
const preview = deserializeSwapPreview(prepared.swap);
|
|
30850
31850
|
if (!preview.inputs.length) return false;
|
|
30851
31851
|
const wallet = await this.wallet(prepared.mint);
|
|
@@ -30863,7 +31863,7 @@ var CashuWallet = class CashuWallet {
|
|
|
30863
31863
|
* are free again, and the rest of the wallet's ecash at that mint is checked the same way. Idempotent.
|
|
30864
31864
|
*/
|
|
30865
31865
|
async dropReviewedCashu(prepared) {
|
|
30866
|
-
const preview = deserializeSwapPreview(prepared.swap);
|
|
31866
|
+
const preview = (await loadCashu()).deserializeSwapPreview(prepared.swap);
|
|
30867
31867
|
await this.locked(prepared.mint, async () => {
|
|
30868
31868
|
const states = await (await this.wallet(prepared.mint)).checkProofsStates(preview.inputs.map((p) => ({
|
|
30869
31869
|
secret: p.secret,
|
|
@@ -30908,7 +31908,7 @@ var CashuWallet = class CashuWallet {
|
|
|
30908
31908
|
}
|
|
30909
31909
|
/** Gives back the inputs a reviewed swap reserved, once the mint proved it never happened. Idempotent. */
|
|
30910
31910
|
async releaseReviewedCashu(prepared) {
|
|
30911
|
-
const preview = deserializeSwapPreview(prepared.swap);
|
|
31911
|
+
const preview = (await loadCashu()).deserializeSwapPreview(prepared.swap);
|
|
30912
31912
|
await this.locked(prepared.mint, async () => {
|
|
30913
31913
|
await transact([STORES$1.proofs], (stores) => {
|
|
30914
31914
|
const proofs = stores[STORES$1.proofs];
|
|
@@ -30933,7 +31933,7 @@ var CashuWallet = class CashuWallet {
|
|
|
30933
31933
|
return (await wrap((await store(STORES$1.payments, "readonly")).get(id)))?.state === "reclaimed";
|
|
30934
31934
|
}
|
|
30935
31935
|
async reviewedCashuSpent(prepared) {
|
|
30936
|
-
const outputs = (prepared.swap.sendOutputs ?? []).map(OutputData.deserialize);
|
|
31936
|
+
const outputs = (prepared.swap.sendOutputs ?? []).map((await loadCashu()).OutputData.deserialize);
|
|
30937
31937
|
const states = await (await this.wallet(prepared.mint)).checkProofsStates(outputs.map((o) => ({
|
|
30938
31938
|
secret: new TextDecoder().decode(o.secret),
|
|
30939
31939
|
id: o.blindedMessage.id
|
|
@@ -30941,8 +31941,10 @@ var CashuWallet = class CashuWallet {
|
|
|
30941
31941
|
return states.length > 0 && states.length === outputs.length && states.every((s) => s.state === "SPENT");
|
|
30942
31942
|
}
|
|
30943
31943
|
/** Reads a token or a payment request without contacting a mint. Nothing here moves money. */
|
|
30944
|
-
inspect(text) {
|
|
31944
|
+
async inspect(text) {
|
|
30945
31945
|
const value = text.trim();
|
|
31946
|
+
if (!/^(cashu|creq)[AB]/i.test(value)) return null;
|
|
31947
|
+
const { decodePaymentRequest, getTokenMetadata } = await loadCashu();
|
|
30946
31948
|
try {
|
|
30947
31949
|
if (/^cashu[AB]/i.test(value)) {
|
|
30948
31950
|
const metadata = getTokenMetadata(value);
|
|
@@ -30977,6 +31979,7 @@ var CashuWallet = class CashuWallet {
|
|
|
30977
31979
|
let mint;
|
|
30978
31980
|
let faceValue;
|
|
30979
31981
|
let unit;
|
|
31982
|
+
const { getTokenMetadata, serializeSwapPreview } = await loadCashu();
|
|
30980
31983
|
try {
|
|
30981
31984
|
const metadata = getTokenMetadata(token);
|
|
30982
31985
|
unit = metadata.unit;
|
|
@@ -31047,6 +32050,7 @@ var CashuWallet = class CashuWallet {
|
|
|
31047
32050
|
*/
|
|
31048
32051
|
async split(mint, amount, proofs) {
|
|
31049
32052
|
const wallet = await this.wallet(mint);
|
|
32053
|
+
const { getEncodedToken, serializeSwapPreview } = await loadCashu();
|
|
31050
32054
|
const among = (chosen) => {
|
|
31051
32055
|
const secrets = new Set(chosen.map((p) => p.secret));
|
|
31052
32056
|
return proofs.filter((p) => secrets.has(p.secret));
|
|
@@ -31098,6 +32102,7 @@ var CashuWallet = class CashuWallet {
|
|
|
31098
32102
|
* `pollSwaps` settles it. Runs under the mint's lock.
|
|
31099
32103
|
*/
|
|
31100
32104
|
async completeSwap(swap) {
|
|
32105
|
+
const { deserializeSwapPreview, isMintOperationError } = await loadCashu();
|
|
31101
32106
|
try {
|
|
31102
32107
|
return await (await this.wallet(swap.mint)).completeSwap(deserializeSwapPreview(swapOf(swap)));
|
|
31103
32108
|
} catch (error) {
|
|
@@ -31125,6 +32130,7 @@ var CashuWallet = class CashuWallet {
|
|
|
31125
32130
|
*/
|
|
31126
32131
|
async swapOutcome(swap, refused = false) {
|
|
31127
32132
|
const wallet = await this.wallet(swap.mint);
|
|
32133
|
+
const { OutputData } = await loadCashu();
|
|
31128
32134
|
const saved = swapOf(swap);
|
|
31129
32135
|
const keep = (saved.keepOutputs ?? []).map(OutputData.deserialize);
|
|
31130
32136
|
const send = (saved.sendOutputs ?? []).map(OutputData.deserialize);
|
|
@@ -31283,6 +32289,15 @@ var CashuWallet = class CashuWallet {
|
|
|
31283
32289
|
settleSwaps(mints) {
|
|
31284
32290
|
return this.pollSwaps(mints);
|
|
31285
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
|
+
}
|
|
31286
32301
|
async pollSwaps(mints) {
|
|
31287
32302
|
if (!mints) {
|
|
31288
32303
|
if (this.swapTimer) clearTimeout(this.swapTimer);
|
|
@@ -31330,34 +32345,36 @@ var CashuWallet = class CashuWallet {
|
|
|
31330
32345
|
}));
|
|
31331
32346
|
if (stuck === 1) this.events.onChange();
|
|
31332
32347
|
later(wait);
|
|
31333
|
-
} else if (outcome.idle && swap.kind === "receive" && !swap.released)
|
|
31334
|
-
const {
|
|
31335
|
-
|
|
31336
|
-
const
|
|
31337
|
-
if (
|
|
31338
|
-
else if (swap.payment) later(SWAP_POLL_MS);
|
|
31339
|
-
this.events.onChange();
|
|
31340
|
-
}
|
|
31341
|
-
} catch (error) {
|
|
31342
|
-
swap = {
|
|
31343
|
-
...swap,
|
|
31344
|
-
attemptEndedAt: Date.now()
|
|
31345
|
-
};
|
|
31346
|
-
await wrap((await store(STORES$1.swaps, "readwrite")).put(swap));
|
|
31347
|
-
const refusal = isMintOperationError(error) ? await this.swapOutcome(swap, true).catch(() => void 0) : void 0;
|
|
31348
|
-
if (refusal?.state === "done") {
|
|
31349
|
-
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)) {
|
|
31350
32353
|
const told = await this.tellSwap(swap, true);
|
|
31351
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();
|
|
31352
32357
|
}
|
|
31353
|
-
|
|
31354
|
-
|
|
31355
|
-
|
|
31356
|
-
|
|
31357
|
-
|
|
31358
|
-
|
|
31359
|
-
|
|
31360
|
-
|
|
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)) {
|
|
31361
32378
|
if (Date.now() < swap.createdAt + SWAP_WATCH_MS) {
|
|
31362
32379
|
await wrap((await store(STORES$1.swaps, "readwrite")).put({
|
|
31363
32380
|
...swap,
|
|
@@ -31451,6 +32468,7 @@ var CashuWallet = class CashuWallet {
|
|
|
31451
32468
|
}
|
|
31452
32469
|
async melt(quoteId, mint, note, paymentId) {
|
|
31453
32470
|
const wallet = await this.wallet(mint);
|
|
32471
|
+
const { MeltChangeError, OutputData, isMintOperationError } = await loadCashu();
|
|
31454
32472
|
const quote = await wallet.checkMeltQuoteBolt11(quoteId);
|
|
31455
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");
|
|
31456
32474
|
const shown = this.shownQuotes.get(quoteId);
|
|
@@ -31544,6 +32562,7 @@ var CashuWallet = class CashuWallet {
|
|
|
31544
32562
|
*/
|
|
31545
32563
|
async settleMelt(melt) {
|
|
31546
32564
|
const wallet = await this.wallet(melt.mint);
|
|
32565
|
+
const { Amount, OutputData } = await loadCashu();
|
|
31547
32566
|
const quote = await wallet.checkMeltQuoteBolt11(melt.quote);
|
|
31548
32567
|
if (quote.state === "PAID") {
|
|
31549
32568
|
const signatures = (quote.change ?? []).filter((sig) => !Amount.from(sig.amount).isZero());
|
|
@@ -31649,7 +32668,7 @@ var CashuWallet = class CashuWallet {
|
|
|
31649
32668
|
out.push({
|
|
31650
32669
|
mint,
|
|
31651
32670
|
amount: total(proofs),
|
|
31652
|
-
token: getEncodedToken({
|
|
32671
|
+
token: (await loadCashu()).getEncodedToken({
|
|
31653
32672
|
mint,
|
|
31654
32673
|
proofs,
|
|
31655
32674
|
unit: UNIT$1
|
|
@@ -31662,7 +32681,7 @@ var CashuWallet = class CashuWallet {
|
|
|
31662
32681
|
let wallet = this.wallets.get(mint);
|
|
31663
32682
|
if (!wallet) {
|
|
31664
32683
|
wallet = (async () => {
|
|
31665
|
-
const instance = new Wallet(mint, { unit: UNIT$1 });
|
|
32684
|
+
const instance = new (await (loadCashu())).Wallet(mint, { unit: UNIT$1 });
|
|
31666
32685
|
await within(MINT_TIMEOUT_MS, instance.loadMint(), mint);
|
|
31667
32686
|
return instance;
|
|
31668
32687
|
})();
|
|
@@ -32302,12 +33321,14 @@ var PaymentDesk = class {
|
|
|
32302
33321
|
return request.linkId === linkId || !!request.group && this.host.groupOf?.(linkId) === request.group;
|
|
32303
33322
|
}
|
|
32304
33323
|
/**
|
|
32305
|
-
* Why a token cannot pay a request to the group, checked before anything is redeemed: already paid,
|
|
32306
|
-
*
|
|
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.
|
|
32307
33327
|
*/
|
|
32308
|
-
refuseForGroup(request, payment) {
|
|
33328
|
+
async refuseForGroup(request, payment) {
|
|
32309
33329
|
if (request.state !== "pending") return "Already paid by another member of the group";
|
|
32310
|
-
|
|
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]);
|
|
32311
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";
|
|
32312
33333
|
}
|
|
32313
33334
|
/**
|
|
@@ -32399,6 +33420,24 @@ var PaymentDesk = class {
|
|
|
32399
33420
|
}).catch(() => {});
|
|
32400
33421
|
}
|
|
32401
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
|
+
/**
|
|
32402
33441
|
* Takes back ecash the peer never redeemed. If they did redeem it, the mint says so and the payment is settled.
|
|
32403
33442
|
* Resolves to what came back of Cashu ecash (and the mint's fee for it); undefined when nothing did, or not Cashu.
|
|
32404
33443
|
*/
|
|
@@ -32735,7 +33774,7 @@ var PaymentDesk = class {
|
|
|
32735
33774
|
});
|
|
32736
33775
|
return;
|
|
32737
33776
|
}
|
|
32738
|
-
const inspected = this.wallet.inspect?.(payment.endpoint[1]);
|
|
33777
|
+
const inspected = await this.wallet.inspect?.(payment.endpoint[1]);
|
|
32739
33778
|
if (payment.endpoint[0] === ENDPOINT.cashu && inspected !== void 0 && inspected?.kind !== "token") {
|
|
32740
33779
|
link.sendPaymentResult({
|
|
32741
33780
|
id: payment.id,
|
|
@@ -32754,7 +33793,7 @@ var PaymentDesk = class {
|
|
|
32754
33793
|
}
|
|
32755
33794
|
const forGroup = payment.requestId ? this.payments.get(payment.requestId) : void 0;
|
|
32756
33795
|
if (forGroup?.group && forGroup.kind === "request" && forGroup.direction === "out" && this.owns(forGroup, linkId)) {
|
|
32757
|
-
const refused = this.refuseForGroup(forGroup, payment);
|
|
33796
|
+
const refused = await this.refuseForGroup(forGroup, payment);
|
|
32758
33797
|
if (refused) {
|
|
32759
33798
|
link.sendPaymentResult({
|
|
32760
33799
|
id: payment.id,
|
|
@@ -33022,6 +34061,7 @@ var PaymentDesk = class {
|
|
|
33022
34061
|
*/
|
|
33023
34062
|
async receiveCheck(linkId, payment) {
|
|
33024
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]);
|
|
33025
34065
|
const request = payment.requestId ? this.payments.get(payment.requestId) : void 0;
|
|
33026
34066
|
if (!link || !request || request.kind !== "request" || request.direction !== "out" || !this.owns(request, linkId)) return;
|
|
33027
34067
|
const rail = request.target ? request.target.method : request.invoice ? "lightning" : void 0;
|
|
@@ -33044,6 +34084,22 @@ var PaymentDesk = class {
|
|
|
33044
34084
|
else await this.reconcileArkReceipts();
|
|
33045
34085
|
} catch {}
|
|
33046
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
|
+
}
|
|
33047
34103
|
/** A Lightning payment of a contact's request that was left pending (or unknown) settled. */
|
|
33048
34104
|
async onLightningResolved(op, paid) {
|
|
33049
34105
|
const found = op.paymentId ? this.payments.get(op.paymentId) : void 0;
|
|
@@ -34488,7 +35544,7 @@ function writtenAt(message, leads) {
|
|
|
34488
35544
|
}
|
|
34489
35545
|
var JOIN_NOTICE = /^👋 (?:.+ )?joined$/;
|
|
34490
35546
|
/** What a newly stored row says, or null when it is quiet. `type`: "message" for the contact's, "sent" for mine. */
|
|
34491
|
-
function messageAttention(type, message, { startedAt, newest, now }) {
|
|
35547
|
+
function messageAttention(type, message, { startedAt, newest, now, lateAt }) {
|
|
34492
35548
|
if (message.timestamp < startedAt || message.file || message.paymentId || JOIN_NOTICE.test(message.text)) return null;
|
|
34493
35549
|
if (message.event) {
|
|
34494
35550
|
const cue = groupCue(message);
|
|
@@ -34504,6 +35560,7 @@ function messageAttention(type, message, { startedAt, newest, now }) {
|
|
|
34504
35560
|
};
|
|
34505
35561
|
if (!message.text.trim()) return null;
|
|
34506
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;
|
|
34507
35564
|
return {
|
|
34508
35565
|
type,
|
|
34509
35566
|
mention: !!message.mentioned
|
|
@@ -34942,6 +35999,11 @@ var GhostlyNode = class GhostlyNode {
|
|
|
34942
35999
|
transfersRestored = false;
|
|
34943
36000
|
/** `start` has loaded the groups (EngineState.groupsLoaded). */
|
|
34944
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
|
+
});
|
|
34945
36007
|
feedbackStartedAt = Date.now();
|
|
34946
36008
|
feedbackIds = /* @__PURE__ */ new Set();
|
|
34947
36009
|
walletFeedbackReady = false;
|
|
@@ -34966,28 +36028,43 @@ var GhostlyNode = class GhostlyNode {
|
|
|
34966
36028
|
}
|
|
34967
36029
|
/** The newest time written in each chat while this engine runs: a message written well before it is a catch-up. */
|
|
34968
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();
|
|
34969
36033
|
/** The last place given in each chat while this engine runs (`arrivalKey`). */
|
|
34970
36034
|
placedAt = /* @__PURE__ */ new Map();
|
|
34971
36035
|
/**
|
|
34972
|
-
* Each 1:1 chat's contact: how far the time its latest live messages said was
|
|
34973
|
-
* 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`).
|
|
34974
36038
|
*/
|
|
34975
36039
|
clockLeads = /* @__PURE__ */ new Map();
|
|
34976
|
-
/**
|
|
34977
|
-
|
|
34978
|
-
|
|
34979
|
-
|
|
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));
|
|
34980
36053
|
}
|
|
34981
36054
|
/** Only a new message, or mine going out, says anything (engine/attention.ts): judged by when it was written. */
|
|
34982
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;
|
|
34983
36058
|
const attention = messageAttention(type, {
|
|
34984
36059
|
...message,
|
|
34985
|
-
timestamp:
|
|
36060
|
+
timestamp: written
|
|
34986
36061
|
}, {
|
|
34987
36062
|
startedAt: this.feedbackStartedAt,
|
|
34988
36063
|
newest,
|
|
34989
|
-
now
|
|
36064
|
+
now,
|
|
36065
|
+
...lateAt !== void 0 && { lateAt }
|
|
34990
36066
|
});
|
|
36067
|
+
if (group && !message.event && now - written > 6e4) this.lateAt.set(message.linkId, now);
|
|
34991
36068
|
if (attention) this.feedback(attention.type, message.linkId + ":" + message.id, message.linkId, attention.mention, attention.cue);
|
|
34992
36069
|
}
|
|
34993
36070
|
/**
|
|
@@ -35532,6 +36609,8 @@ var GhostlyNode = class GhostlyNode {
|
|
|
35532
36609
|
}
|
|
35533
36610
|
}
|
|
35534
36611
|
});
|
|
36612
|
+
/** x401 credentials (WISP 3xx): kept for websites the headless CLI calls, never shared with a contact. */
|
|
36613
|
+
x401 = new X401Credentials();
|
|
35535
36614
|
/** Identity proofs: this profile's, and those shared in each paired chat (WISP 300). */
|
|
35536
36615
|
identities = new IdentityProofs({
|
|
35537
36616
|
ledger: (linkId) => {
|
|
@@ -35593,6 +36672,59 @@ var GhostlyNode = class GhostlyNode {
|
|
|
35593
36672
|
return this.transport.publishPayload(pubKeyZ32, payload, { background: true });
|
|
35594
36673
|
}
|
|
35595
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
|
+
});
|
|
35596
36728
|
/** The Nostr social layer (profile, follows, notes, publication) on top of verified Nostr proofs. */
|
|
35597
36729
|
nostrSocial = new NostrSocial({
|
|
35598
36730
|
settings: () => this.settings.nostr,
|
|
@@ -35795,13 +36927,15 @@ var GhostlyNode = class GhostlyNode {
|
|
|
35795
36927
|
const live = this.links.get(linkId);
|
|
35796
36928
|
return !!live?.lastSyncAt && !!live.presence?.online && presenceSeenAt(live.presence) > live.lastSyncAt;
|
|
35797
36929
|
},
|
|
35798
|
-
publish: (identity, records, background, door) => this.groupTransport.publish(identity, records, {
|
|
36930
|
+
publish: (identity, records, background, door, back) => this.groupTransport.publish(identity, records, {
|
|
35799
36931
|
background,
|
|
35800
|
-
door
|
|
36932
|
+
door,
|
|
36933
|
+
back
|
|
35801
36934
|
}),
|
|
35802
|
-
resolve: async (pubKeyZ32, background, door) => (await this.groupTransport.resolve(pubKeyZ32, {
|
|
36935
|
+
resolve: async (pubKeyZ32, background, door, back) => (await this.groupTransport.resolve(pubKeyZ32, {
|
|
35803
36936
|
background,
|
|
35804
|
-
door
|
|
36937
|
+
door,
|
|
36938
|
+
back
|
|
35805
36939
|
}))?.records ?? null,
|
|
35806
36940
|
expectPeer: (linkId) => this.links.get(linkId)?.link?.expectPeer(),
|
|
35807
36941
|
signalIn: (linkId, payload, direct) => !!this.links.get(linkId)?.carried?.accept(payload, direct),
|
|
@@ -35815,7 +36949,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
35815
36949
|
else this.closeGroupLink(linkId).catch(() => {});
|
|
35816
36950
|
},
|
|
35817
36951
|
edgeNick: (linkId) => this.links.get(linkId)?.presence.nick || void 0,
|
|
35818
|
-
storeMessage: (message) => this.storeNewMessage(message),
|
|
36952
|
+
storeMessage: (message, live, place) => this.storeNewMessage(message, live, place),
|
|
35819
36953
|
completeMessage: (message) => this.completeGroupMessage(message),
|
|
35820
36954
|
forgetFrame: async (linkId, id) => {
|
|
35821
36955
|
await db.patchMessage(linkId, id, (stored) => stored.frame ? { frame: void 0 } : null, true);
|
|
@@ -35824,6 +36958,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
35824
36958
|
await db.patchMessage(linkId, id, () => ({ frame }), true);
|
|
35825
36959
|
},
|
|
35826
36960
|
placeAgain: (linkId, ids) => this.placeAgain(linkId, ids),
|
|
36961
|
+
placeAbove: (linkId, id) => this.placeAbove(linkId, id),
|
|
35827
36962
|
membersChanged: (groupId) => this.groupMembersChanged(groupId),
|
|
35828
36963
|
historyGone: (groupId) => this.groupHistoryGone(groupId),
|
|
35829
36964
|
emit: () => this.emitState(),
|
|
@@ -36238,34 +37373,44 @@ var GhostlyNode = class GhostlyNode {
|
|
|
36238
37373
|
await this.did.load();
|
|
36239
37374
|
setOwnDidSource(() => this.did.id);
|
|
36240
37375
|
this.did.start();
|
|
37376
|
+
await this.contactLinks.load();
|
|
37377
|
+
this.contactLinks.start();
|
|
37378
|
+
await this.contactAsks.load();
|
|
37379
|
+
this.contactAsks.start();
|
|
36241
37380
|
await this.nostrSocial.load();
|
|
36242
37381
|
await this.publicProfiles.load();
|
|
36243
37382
|
if (!this.limitedMode) await this.startWallets();
|
|
36244
37383
|
const history = /* @__PURE__ */ new Map();
|
|
36245
|
-
|
|
36246
|
-
|
|
36247
|
-
|
|
36248
|
-
|
|
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);
|
|
36249
37397
|
const waiting = new Set(messages.flatMap((m) => m.sender === "me" && m.delivery === "waiting" && m.file ? [m.file.id] : []));
|
|
36250
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" ? {
|
|
36251
37399
|
...file.transfer,
|
|
36252
37400
|
state: "failed",
|
|
36253
37401
|
error: "Transfer interrupted. Retry when connected."
|
|
36254
37402
|
} : file.transfer);
|
|
36255
|
-
|
|
37403
|
+
this.fileDesk.restore(stored.id, storedFiles);
|
|
36256
37404
|
const files = linkFilesFrom(stored.id, storedFiles, messages);
|
|
36257
37405
|
this.links.set(stored.id, newLiveLink(stored, messages[messages.length - 1]?.timestamp ?? 0, files));
|
|
36258
|
-
|
|
37406
|
+
this.links.get(stored.id).waitingSends = Promise.resolve(new Set(messages.filter(waitsForLive).map((m) => m.id)));
|
|
36259
37407
|
history.set(stored.id, messages);
|
|
36260
37408
|
for (const message of messages.slice(-20)) this.clockSample(message);
|
|
36261
37409
|
const note = latestReaction(messages);
|
|
36262
37410
|
if (note) this.reactionNotes.set(stored.id, note);
|
|
36263
|
-
if (stored.profile
|
|
37411
|
+
if (stored.profile) await this.outboxFor(stored.id).recover();
|
|
36264
37412
|
}
|
|
36265
37413
|
this.transfersRestored = true;
|
|
36266
|
-
for (const group of await db.getGroups()) await this.settleHistory(`group:${group.id}`);
|
|
36267
|
-
await this.groups.load(this.limitedMode);
|
|
36268
|
-
this.groupsLoaded = true;
|
|
36269
37414
|
for (const group of this.groups.views()) {
|
|
36270
37415
|
const note = latestReaction(await db.getMessages(`group:${group.id}`));
|
|
36271
37416
|
if (note) this.reactionNotes.set(`group:${group.id}`, note);
|
|
@@ -36657,14 +37802,16 @@ var GhostlyNode = class GhostlyNode {
|
|
|
36657
37802
|
/**
|
|
36658
37803
|
* Says goodbye on every live session (`paired-bye`): the contacts end theirs now and watch for this app to come back,
|
|
36659
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.
|
|
36660
37806
|
*/
|
|
36661
|
-
depart() {
|
|
36662
|
-
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);
|
|
36663
37809
|
}
|
|
36664
37810
|
/**
|
|
36665
37811
|
* Tells every peer we are leaving. Best effort: the browser may already be closing. `quiet`: says nothing to anyone
|
|
36666
37812
|
* (no goodbye, no departure in the links' packets), as a device another device replaced must (WISP 06 § When a
|
|
36667
|
-
* 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`).
|
|
36668
37815
|
*/
|
|
36669
37816
|
async shutdown(options = {}) {
|
|
36670
37817
|
const quiet = options.quiet === true || this.gatedOut;
|
|
@@ -36675,7 +37822,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
36675
37822
|
this.limitedTimer = null;
|
|
36676
37823
|
}
|
|
36677
37824
|
this.appShelf?.stop();
|
|
36678
|
-
if (!quiet) this.depart();
|
|
37825
|
+
if (!quiet) this.depart({ quitting: options.quitting === true });
|
|
36679
37826
|
const linksStopped = Promise.allSettled([...this.links.values()].map(async (live) => {
|
|
36680
37827
|
live.carried?.stop();
|
|
36681
37828
|
await Promise.all([live.link?.stop(!quiet), live.caps?.stop()]);
|
|
@@ -36709,6 +37856,8 @@ var GhostlyNode = class GhostlyNode {
|
|
|
36709
37856
|
this.restoreTimer = null;
|
|
36710
37857
|
this.identities.stop();
|
|
36711
37858
|
this.did.stop();
|
|
37859
|
+
this.contactLinks.stop();
|
|
37860
|
+
this.contactAsks.stop();
|
|
36712
37861
|
this.nostrSocial.stop();
|
|
36713
37862
|
this.publicProfiles.stop();
|
|
36714
37863
|
this.publicActivity.clear();
|
|
@@ -36746,6 +37895,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
36746
37895
|
const withdraw = Promise.allSettled([
|
|
36747
37896
|
this.hold.withdrawAll(),
|
|
36748
37897
|
this.did.deactivate(),
|
|
37898
|
+
this.contactLinks.withdraw(),
|
|
36749
37899
|
this.identities.revokeAll()
|
|
36750
37900
|
]);
|
|
36751
37901
|
await Promise.race([withdraw, new Promise((resolve) => setTimeout(resolve, CLEAR_WITHDRAW_MS))]);
|
|
@@ -36874,6 +38024,9 @@ var GhostlyNode = class GhostlyNode {
|
|
|
36874
38024
|
payments: this.desk.views(),
|
|
36875
38025
|
identityProofs: this.identities.views(),
|
|
36876
38026
|
did: this.did.view(),
|
|
38027
|
+
contactLink: this.contactLinks.view(),
|
|
38028
|
+
contactRequests: this.contactLinks.requests(),
|
|
38029
|
+
contactAsks: this.contactAsks.views(),
|
|
36877
38030
|
nostr: this.nostrSocial.state(),
|
|
36878
38031
|
edges: [...[...this.links.values()].filter((live) => live.stored.group && !live.stored.groupEntry).map((live) => this.viewOf(live)), ...this.communityPayViews(groups)],
|
|
36879
38032
|
groups: groups.map((group) => ({
|
|
@@ -37185,9 +38338,42 @@ var GhostlyNode = class GhostlyNode {
|
|
|
37185
38338
|
recheckIdentityProof(params) {
|
|
37186
38339
|
return this.identities.recheck(params);
|
|
37187
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
|
+
}
|
|
37188
38353
|
setDidListed(params) {
|
|
37189
38354
|
return this.did.setListed(params);
|
|
37190
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
|
+
}
|
|
37191
38377
|
lookupIdentityDisplay(params) {
|
|
37192
38378
|
const r = this.links.get(params.linkId)?.stored.identities?.received.find((x) => x.id === params.id);
|
|
37193
38379
|
if (r?.binding.provider === "nostr") return this.nostrSocial.loadContact({
|
|
@@ -37519,6 +38705,8 @@ var GhostlyNode = class GhostlyNode {
|
|
|
37519
38705
|
groupHistoryGone(groupId) {
|
|
37520
38706
|
const linkId = `group:${groupId}`;
|
|
37521
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);
|
|
37522
38710
|
this.placedAt.delete(linkId);
|
|
37523
38711
|
this.reactionNotes.delete(linkId);
|
|
37524
38712
|
this.reactionNotesBefore.delete(linkId);
|
|
@@ -37682,6 +38870,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
37682
38870
|
this.nostrSocial.forgetLink(linkId);
|
|
37683
38871
|
this.hold.forgetLink(linkId);
|
|
37684
38872
|
this.fileDesk.drop(linkId);
|
|
38873
|
+
this.contactAsks.forgetChat(linkId).catch(() => {});
|
|
37685
38874
|
this.wakeLimiter.reset(linkId);
|
|
37686
38875
|
this.callWakeLimiter.reset(linkId);
|
|
37687
38876
|
if (live.stored.wakeToken) this.rotateWake().then(() => this.emitState());
|
|
@@ -38262,7 +39451,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
38262
39451
|
if (!queue) {
|
|
38263
39452
|
queue = new EditQueue({
|
|
38264
39453
|
read: () => db.getMessages(linkId),
|
|
38265
|
-
|
|
39454
|
+
rows: (ids) => db.getMessagesById(linkId, ids),
|
|
38266
39455
|
ready: () => {
|
|
38267
39456
|
const live = this.links.get(linkId), link = live?.link;
|
|
38268
39457
|
return !!link && (link.supportsEdits || link.textDelivery === "dht" && this.takesDhtEdits(live));
|
|
@@ -39667,7 +40856,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
39667
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
|
|
39668
40857
|
* it or where. A text goes as written; a file from the bytes on this device (a copy per chat, nothing fetched again),
|
|
39669
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
|
|
39670
|
-
* 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`).
|
|
39671
40860
|
*/
|
|
39672
40861
|
async forwardMessages(params) {
|
|
39673
40862
|
const { linkId } = params;
|
|
@@ -39678,7 +40867,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
39678
40867
|
if (ids.length > 30) throw new Error(`Forward at most 30 messages at once`);
|
|
39679
40868
|
if (!targets.length) throw new Error("No chat to forward to");
|
|
39680
40869
|
if (targets.length > 5) throw new Error(`Forward to at most 5 chats at once`);
|
|
39681
|
-
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");
|
|
39682
40871
|
const chosen = await Promise.all(ids.map((id) => db.getMessage(linkId, id)));
|
|
39683
40872
|
if (chosen.some((m) => !m)) throw new Error("That message is not in this chat");
|
|
39684
40873
|
const messages = chosen.sort((a, b) => a.timestamp - b.timestamp);
|
|
@@ -39693,6 +40882,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
39693
40882
|
const result = {
|
|
39694
40883
|
to,
|
|
39695
40884
|
messageIds: [],
|
|
40885
|
+
sent: [],
|
|
39696
40886
|
error: null
|
|
39697
40887
|
};
|
|
39698
40888
|
results.push(result);
|
|
@@ -39714,11 +40904,20 @@ var GhostlyNode = class GhostlyNode {
|
|
|
39714
40904
|
text: message.text,
|
|
39715
40905
|
timestamp: now()
|
|
39716
40906
|
});
|
|
39717
|
-
if (sent.error)
|
|
39718
|
-
|
|
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);
|
|
39719
40913
|
continue;
|
|
39720
40914
|
}
|
|
39721
40915
|
if (groupId) {
|
|
40916
|
+
const check = this.groups.fileCheck(groupId);
|
|
40917
|
+
if (check.error) {
|
|
40918
|
+
note(check.error);
|
|
40919
|
+
continue;
|
|
40920
|
+
}
|
|
39722
40921
|
const original = message.file, file = {
|
|
39723
40922
|
id: groupFileId(groupId, "out"),
|
|
39724
40923
|
name: original.name,
|
|
@@ -39742,7 +40941,10 @@ var GhostlyNode = class GhostlyNode {
|
|
|
39742
40941
|
if (sent.error) {
|
|
39743
40942
|
await removeStored(file.id).catch(() => {});
|
|
39744
40943
|
note(sent.error);
|
|
39745
|
-
|
|
40944
|
+
continue;
|
|
40945
|
+
}
|
|
40946
|
+
if (sent.messageId) result.messageIds.push(sent.messageId);
|
|
40947
|
+
result.sent.push(message.id);
|
|
39746
40948
|
continue;
|
|
39747
40949
|
}
|
|
39748
40950
|
if (!live.link) {
|
|
@@ -39778,8 +40980,9 @@ var GhostlyNode = class GhostlyNode {
|
|
|
39778
40980
|
throw error;
|
|
39779
40981
|
}
|
|
39780
40982
|
result.messageIds.push(`me_${timestamp}`);
|
|
40983
|
+
result.sent.push(message.id);
|
|
39781
40984
|
} catch (error) {
|
|
39782
|
-
note(
|
|
40985
|
+
note(forwardError(error));
|
|
39783
40986
|
}
|
|
39784
40987
|
}
|
|
39785
40988
|
}
|
|
@@ -40638,12 +41841,18 @@ var GhostlyNode = class GhostlyNode {
|
|
|
40638
41841
|
const n = this.net(network);
|
|
40639
41842
|
return via === "cashu" ? this.wallet.quoteInvoice(invoice, n) : this.lightningCard(n, card).then((c) => c.quote(invoice));
|
|
40640
41843
|
}
|
|
40641
|
-
/**
|
|
40642
|
-
|
|
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 }) {
|
|
40643
41850
|
const network = WALLET_NETWORKS.find((n) => this.lightnings[n].withQuote(quote));
|
|
40644
41851
|
const lightning = network && this.lightnings[network].withQuote(quote);
|
|
40645
41852
|
assertConfirmedReal(network ?? mintNetwork(mint), confirmedReal);
|
|
40646
|
-
|
|
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 };
|
|
40647
41856
|
}
|
|
40648
41857
|
/** A Lightning address or LNURL (LUD-16, LUD-06): resolved here, in the engine, so every platform fetches the same way. */
|
|
40649
41858
|
lnurlResolve({ text, network }) {
|
|
@@ -40772,8 +41981,8 @@ var GhostlyNode = class GhostlyNode {
|
|
|
40772
41981
|
bitcoinRefresh(params) {
|
|
40773
41982
|
return this.bitcoins[this.net(params?.network)].sources.refresh();
|
|
40774
41983
|
}
|
|
40775
|
-
walletInspectCashu({ text }) {
|
|
40776
|
-
return { inspection: this.wallet.inspect(text) };
|
|
41984
|
+
async walletInspectCashu({ text }) {
|
|
41985
|
+
return { inspection: await this.wallet.inspect(text) };
|
|
40777
41986
|
}
|
|
40778
41987
|
async walletReceiveToken({ token }) {
|
|
40779
41988
|
const { amount } = await this.wallet.receiveToken(token.trim());
|
|
@@ -42316,7 +43525,8 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42316
43525
|
carry: (payload) => this.groups.carrySignal(group, peer, payload),
|
|
42317
43526
|
open: () => !!live.link?.isDataLinkOpen,
|
|
42318
43527
|
look: () => live.link?.look(),
|
|
42319
|
-
direct: (payload) => this.groups.directSignal(group, peer, linkId, payload)
|
|
43528
|
+
direct: (payload) => this.groups.directSignal(group, peer, linkId, payload),
|
|
43529
|
+
after: live.carried?.lastDated()
|
|
42320
43530
|
});
|
|
42321
43531
|
const waiting = live.carried && this.groups.takeSignal(group, peer);
|
|
42322
43532
|
if (waiting) live.carried.accept(waiting);
|
|
@@ -42581,6 +43791,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42581
43791
|
} : void 0;
|
|
42582
43792
|
const resumed = stored.pairedPeerKey && stored.deliveryMode !== "dht" ? resumeHere(this.transportLogOf(live), this.transportsHere(), stored.peerTransports ?? live.caps?.peer?.transports) : {};
|
|
42583
43793
|
let dhtShown;
|
|
43794
|
+
let wasTaken = false;
|
|
42584
43795
|
live.link = new GhostLink({
|
|
42585
43796
|
paymentMethods: stored.paymentMethods,
|
|
42586
43797
|
paymentNetworks: this.chatNetworks(stored),
|
|
@@ -42670,6 +43881,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42670
43881
|
throw error;
|
|
42671
43882
|
}
|
|
42672
43883
|
if (!previous.pairedPeerKey) {
|
|
43884
|
+
if (!live.stored.group && live.stored.deliveryMode !== "dht") this.ensureNativeEndpoints(stored.id);
|
|
42673
43885
|
live.caps?.update().catch(() => {});
|
|
42674
43886
|
live.caps?.refresh(true);
|
|
42675
43887
|
}
|
|
@@ -42709,6 +43921,11 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42709
43921
|
onDhtDelivery: (view) => {
|
|
42710
43922
|
if (stored.profile && !stored.group) this.outboxes.get(linkId)?.flush().catch(() => {}).then(() => this.editQueues.get(linkId)?.flush()).catch(() => {});
|
|
42711
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
|
+
}
|
|
42712
43929
|
const shown = JSON.stringify(view);
|
|
42713
43930
|
if (shown !== dhtShown) {
|
|
42714
43931
|
dhtShown = shown;
|
|
@@ -43076,7 +44293,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
43076
44293
|
get nativeFactories() {
|
|
43077
44294
|
const relay = this.relaysHyperdht ? this.hyperdhtRelay : "";
|
|
43078
44295
|
return {
|
|
43079
|
-
...relay ? { "hyperdht/1": async (seedB64) => (await import("./hyperdhtRelay-
|
|
44296
|
+
...relay ? { "hyperdht/1": async (seedB64) => (await import("./hyperdhtRelay-13aQ4KOf.js")).createRelayedHyperEndpoint(seedB64, relay) } : {},
|
|
43080
44297
|
...this.options.nativeTransports,
|
|
43081
44298
|
...this.ownIrohRelays ? { "iroh/1": (seedB64) => this.options.nativeTransports["iroh/1"](seedB64, this.settings.irohRelays?.length ? { relays: [...this.settings.irohRelays] } : {}) } : {},
|
|
43082
44299
|
...this.options.irohWeb ? { "iroh/1": (seedB64) => createIrohWebEndpoint(seedB64, { relays: this.irohRelays }) } : {}
|
|
@@ -43206,7 +44423,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
43206
44423
|
*/
|
|
43207
44424
|
async startNativeEndpoint(linkId, expected, key, factory, inUse) {
|
|
43208
44425
|
const live = this.links.get(linkId), link = live?.link;
|
|
43209
|
-
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;
|
|
43210
44427
|
const group = !!live.stored.group;
|
|
43211
44428
|
if (group && !this.keepsGroupNative(live.stored)) return false;
|
|
43212
44429
|
if (link.availableTransports.includes(key)) return false;
|
|
@@ -43265,7 +44482,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
43265
44482
|
starting.then((late) => late.close(), () => {});
|
|
43266
44483
|
return false;
|
|
43267
44484
|
}
|
|
43268
|
-
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) {
|
|
43269
44486
|
await endpoint.close();
|
|
43270
44487
|
return false;
|
|
43271
44488
|
}
|
|
@@ -43446,8 +44663,9 @@ var GhostlyNode = class GhostlyNode {
|
|
|
43446
44663
|
* ahead of this clock nor above a row placed here since the app started. The admin's clock picks that time: two
|
|
43447
44664
|
* minutes ahead, "X joined" sat under everything said in the next two minutes; behind, it went above what the
|
|
43448
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.
|
|
43449
44667
|
*/
|
|
43450
|
-
placed(message) {
|
|
44668
|
+
placed(message, place) {
|
|
43451
44669
|
const now = Date.now(), last = this.placedAt.get(message.linkId) ?? 0;
|
|
43452
44670
|
if (message.event) {
|
|
43453
44671
|
if (message.sender !== "peer") return message;
|
|
@@ -43462,10 +44680,10 @@ var GhostlyNode = class GhostlyNode {
|
|
|
43462
44680
|
const sentAt = claimedTime(message.timestamp);
|
|
43463
44681
|
message = {
|
|
43464
44682
|
...message,
|
|
43465
|
-
timestamp: arrivalKey(now, last),
|
|
44683
|
+
timestamp: place ?? arrivalKey(now, last),
|
|
43466
44684
|
...sentAt !== void 0 && { sentAt }
|
|
43467
44685
|
};
|
|
43468
|
-
this.placedAt.set(message.linkId, message.timestamp);
|
|
44686
|
+
if (place === void 0) this.placedAt.set(message.linkId, message.timestamp);
|
|
43469
44687
|
} else this.placedAt.set(message.linkId, Math.max(last, Math.min(message.timestamp, now)));
|
|
43470
44688
|
return message;
|
|
43471
44689
|
}
|
|
@@ -43487,11 +44705,32 @@ var GhostlyNode = class GhostlyNode {
|
|
|
43487
44705
|
}));
|
|
43488
44706
|
if (moved.length) await this.messagesChanged(linkId, moved.map((m) => m.id));
|
|
43489
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
|
+
}
|
|
43490
44726
|
async storeMessage(message) {
|
|
43491
44727
|
await this.storeNewMessage(message);
|
|
43492
44728
|
}
|
|
43493
|
-
/**
|
|
43494
|
-
|
|
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) {
|
|
43495
44734
|
const edge = this.links.get(message.linkId)?.stored;
|
|
43496
44735
|
if (edge?.group && edge.groupPeer && !edge.groupEntry) message = {
|
|
43497
44736
|
...message,
|
|
@@ -43506,7 +44745,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
43506
44745
|
id: `${message.linkId}:${message.id}`,
|
|
43507
44746
|
...message.sender === "peer" ? { member: pay.member } : {}
|
|
43508
44747
|
};
|
|
43509
|
-
message = this.placed(message);
|
|
44748
|
+
message = this.placed(message, place);
|
|
43510
44749
|
const live = this.links.get(message.linkId);
|
|
43511
44750
|
if (live?.stored.deletedIds?.includes(message.id)) return false;
|
|
43512
44751
|
message = await this.resolveReply(message);
|
|
@@ -43536,9 +44775,9 @@ var GhostlyNode = class GhostlyNode {
|
|
|
43536
44775
|
}
|
|
43537
44776
|
if (!await db.addMessage(message)) return false;
|
|
43538
44777
|
if (live && waitsForLive(message)) live.waitingSends?.then((known) => known.add(message.id), () => {});
|
|
43539
|
-
this.clockSample(message);
|
|
44778
|
+
this.clockSample(message, straight);
|
|
43540
44779
|
const newest = this.newestAt.get(message.linkId);
|
|
43541
|
-
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)))));
|
|
43542
44781
|
if (message.sender === "peer") this.messageFeedback("message", message, newest);
|
|
43543
44782
|
if (live) live.lastMessageAt = Math.max(live.lastMessageAt, message.timestamp);
|
|
43544
44783
|
const early = message.sender === "peer" && message.id.startsWith("peer_") ? this.editBuffer.take(message.linkId, message.id.slice(5)) : void 0;
|
|
@@ -43805,8 +45044,8 @@ var EngineServer = class {
|
|
|
43805
45044
|
this.clients.delete(client);
|
|
43806
45045
|
});
|
|
43807
45046
|
}
|
|
43808
|
-
stop() {
|
|
43809
|
-
return this.node.shutdown();
|
|
45047
|
+
stop(options = {}) {
|
|
45048
|
+
return this.node.shutdown({ quitting: options.quitting === true });
|
|
43810
45049
|
}
|
|
43811
45050
|
detach(client) {
|
|
43812
45051
|
this.clients.delete(client);
|
|
@@ -43831,13 +45070,13 @@ var EngineServer = class {
|
|
|
43831
45070
|
params = { on: this.onCall.size > 0 };
|
|
43832
45071
|
}
|
|
43833
45072
|
try {
|
|
43834
|
-
await this.ready;
|
|
45073
|
+
await (readsGroupHistory(request) ? Promise.race([this.ready, this.node.groupsRead]) : this.ready);
|
|
43835
45074
|
const method = this.node[request.method];
|
|
43836
45075
|
if (typeof method !== "function") throw new Error(`Unknown method: ${request.method}`);
|
|
43837
45076
|
if (this.node.limited && !LIMITED_MODE_METHODS.has(String(request.method))) throw new Error(LIMITED_MODE_ERROR);
|
|
43838
45077
|
response.result = await method.call(this.node, params);
|
|
43839
45078
|
} catch (error) {
|
|
43840
|
-
response.error =
|
|
45079
|
+
response.error = errorLine(error);
|
|
43841
45080
|
}
|
|
43842
45081
|
try {
|
|
43843
45082
|
client.post(response);
|
|
@@ -43914,6 +45153,12 @@ var EngineServer = class {
|
|
|
43914
45153
|
for (const client of [...this.clients]) this.post(client, event);
|
|
43915
45154
|
}
|
|
43916
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
|
+
}
|
|
43917
45162
|
/** A call signal's kind (`o` for an offer), read as the client reads it; checked in full before it is replayed. */
|
|
43918
45163
|
function signalKind(signal) {
|
|
43919
45164
|
try {
|
|
@@ -43925,4 +45170,4 @@ function signalKind(signal) {
|
|
|
43925
45170
|
//#endregion
|
|
43926
45171
|
export { EngineServer };
|
|
43927
45172
|
|
|
43928
|
-
//# sourceMappingURL=server-
|
|
45173
|
+
//# sourceMappingURL=server-Cd1dLOB8.js.map
|