@ghostlytools/cli 1.2.4 → 1.3.1
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 +25 -9
- package/dist/assets/api-Dq2QXQDk.js +4172 -0
- package/dist/assets/api-Dq2QXQDk.js.map +1 -0
- package/dist/assets/api-TiB5QSG9.js +3 -0
- package/dist/assets/apiKit-CA2x5GJF.js +200 -0
- package/dist/assets/apiKit-CA2x5GJF.js.map +1 -0
- package/dist/assets/apps-BxV9i_Y4.js +786 -0
- package/dist/assets/apps-BxV9i_Y4.js.map +1 -0
- package/dist/assets/apps-C-T8jeji.js +2 -0
- package/dist/assets/{arkade-CdnKGDbg.js → arkade-C_qDmHnm.js} +2 -2
- package/dist/assets/{arkade-CdnKGDbg.js.map → arkade-C_qDmHnm.js.map} +1 -1
- package/dist/assets/backup-Bugnn3Ue.js +2 -0
- package/dist/assets/backup-CnMN2oUK.js +731 -0
- package/dist/assets/backup-CnMN2oUK.js.map +1 -0
- package/dist/assets/bark_ffi_wasm_bg-EY2uc0Bx.js.map +1 -1
- package/dist/assets/bitcoind-CZchoh43.js +349 -0
- package/dist/assets/bitcoind-CZchoh43.js.map +1 -0
- package/dist/assets/bitcoindProvider-DGw3xBSa.js +97 -0
- package/dist/assets/bitcoindProvider-DGw3xBSa.js.map +1 -0
- package/dist/assets/bitcoindevkit_bg-B-iLWrmX.js.map +1 -1
- package/dist/assets/buttons-Bj7Vf7Pn.js +2 -0
- package/dist/assets/buttons-CvNBzh2S.js +496 -0
- package/dist/assets/buttons-CvNBzh2S.js.map +1 -0
- package/dist/assets/{endpoint-pgFkR2AS.js → endpoint-BOTFMpfS.js} +56 -18
- package/dist/assets/endpoint-BOTFMpfS.js.map +1 -0
- package/dist/assets/envelope-WjingYSc.js +90 -0
- package/dist/assets/envelope-WjingYSc.js.map +1 -0
- package/dist/assets/errors-DO5z6h_l.js +105 -0
- package/dist/assets/errors-DO5z6h_l.js.map +1 -0
- package/dist/assets/fedimint_client_wasm_bg-BtHHlFpp.js.map +1 -1
- package/dist/assets/{fileBytes-Dbifu5I5.js → fileBytes-CfSXjTFa.js} +2 -2
- package/dist/assets/{fileBytes-Dbifu5I5.js.map → fileBytes-CfSXjTFa.js.map} +1 -1
- package/dist/assets/fileBytes-CgbibpbV.js +2 -0
- package/dist/assets/{fileBytes-BcYZhNgF.js → fileBytes-Di7VZtmU.js} +6 -19
- package/dist/assets/fileBytes-Di7VZtmU.js.map +1 -0
- package/dist/assets/fileBytesIdb-CFP-DSkZ.js +2 -0
- package/dist/assets/{fileBytesIdb-DzSaqI0A.js → fileBytesIdb-D5u6H8X_.js} +4 -4
- package/dist/assets/{fileBytesIdb-DzSaqI0A.js.map → fileBytesIdb-D5u6H8X_.js.map} +1 -1
- package/dist/assets/{fileBytesOpfs-Dx8RZiV5.js → fileBytesOpfs-Dd1N4ERH.js} +3 -3
- package/dist/assets/{fileBytesOpfs-Dx8RZiV5.js.map → fileBytesOpfs-Dd1N4ERH.js.map} +1 -1
- package/dist/assets/groupFiles-IEUW3DQz.js +3202 -0
- package/dist/assets/groupFiles-IEUW3DQz.js.map +1 -0
- package/dist/assets/host-CWt-jtqz.js +4673 -0
- package/dist/assets/host-CWt-jtqz.js.map +1 -0
- package/dist/assets/hyperdhtRelay-BT_wRxab.js +21 -0
- package/dist/assets/hyperdhtRelay-BT_wRxab.js.map +1 -0
- package/dist/assets/{hyperdhtRelay-C1p0nkle.js → hyperdhtRelay-CPYwSLLS.js} +25 -10
- package/dist/assets/hyperdhtRelay-CPYwSLLS.js.map +1 -0
- package/dist/assets/{idb-CTF6LF5B.js → idb-B5L8wuOm.js} +7 -5
- package/dist/assets/idb-B5L8wuOm.js.map +1 -0
- package/dist/assets/irohWeb-4oChIOIp.js +544 -0
- package/dist/assets/irohWeb-4oChIOIp.js.map +1 -0
- package/dist/assets/{joinTrace-CO8mlhNO.js → joinTrace-CjRDFy85.js} +2 -7
- package/dist/assets/{joinTrace-CO8mlhNO.js.map → joinTrace-CjRDFy85.js.map} +1 -1
- package/dist/assets/joinTrace-D-fnt2p3.js +2 -0
- package/dist/assets/main-DbqcdShN.js +4409 -0
- package/dist/assets/main-DbqcdShN.js.map +1 -0
- package/dist/assets/{mainline-BH6s8HVi.js → mainline-ZkiOnevs.js} +11 -6
- package/dist/assets/mainline-ZkiOnevs.js.map +1 -0
- package/dist/assets/mints-BrlY773j.js +74 -0
- package/dist/assets/mints-BrlY773j.js.map +1 -0
- package/dist/assets/mints-CqTdA4Gk.js +2 -0
- package/dist/assets/networkReason-CetKkTPo.js +154 -0
- package/dist/assets/networkReason-CetKkTPo.js.map +1 -0
- package/dist/assets/nwc-CgkqyvsE.js +612 -0
- package/dist/assets/nwc-CgkqyvsE.js.map +1 -0
- package/dist/assets/nwcProvider-4eVz_8HP.js +77 -0
- package/dist/assets/nwcProvider-4eVz_8HP.js.map +1 -0
- package/dist/assets/privateFolder-Bk9C9oFv.js +38 -0
- package/dist/assets/privateFolder-Bk9C9oFv.js.map +1 -0
- package/dist/assets/profiles-BHc9pEYg.js +139 -0
- package/dist/assets/profiles-BHc9pEYg.js.map +1 -0
- package/dist/assets/registry-DlAKO0Fy.js +3793 -0
- package/dist/assets/registry-DlAKO0Fy.js.map +1 -0
- package/dist/assets/registry-OBJBnHVQ.js +2 -0
- package/dist/assets/resolve-CouHhCpJ.js +591 -0
- package/dist/assets/resolve-CouHhCpJ.js.map +1 -0
- package/dist/assets/{restoredRows-C_84q0Pd.js → restoredRows-Db3oUTko.js} +2 -2
- package/dist/assets/{restoredRows-C_84q0Pd.js.map → restoredRows-Db3oUTko.js.map} +1 -1
- package/dist/assets/{server-0zfNpABQ.js → server-Ss0gJ-nw.js} +1275 -1404
- package/dist/assets/server-Ss0gJ-nw.js.map +1 -0
- package/dist/assets/{src-CbLkHfz8.js → src-DDzSERyc.js} +882 -134
- package/dist/assets/src-DDzSERyc.js.map +1 -0
- package/dist/assets/turnRefs-B87dtrHM.js +323 -0
- package/dist/assets/turnRefs-B87dtrHM.js.map +1 -0
- package/dist/assets/types-C4_KafL_.js +122 -0
- package/dist/assets/types-C4_KafL_.js.map +1 -0
- package/dist/assets/{usdt-Qf4UuygS.js → usdt-Fj8CQO2z.js} +3 -3
- package/dist/assets/{usdt-Qf4UuygS.js.map → usdt-Fj8CQO2z.js.map} +1 -1
- package/dist/ghostly.mjs +4 -22077
- package/dist/ghostly.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/assets/endpoint-pgFkR2AS.js.map +0 -1
- package/dist/assets/fileBytes-BcYZhNgF.js.map +0 -1
- package/dist/assets/hyperdhtRelay-C1p0nkle.js.map +0 -1
- package/dist/assets/idb-CTF6LF5B.js.map +0 -1
- package/dist/assets/mainline-BH6s8HVi.js.map +0 -1
- package/dist/assets/rolldown-runtime-8H4AJuhK.js +0 -14
- package/dist/assets/server-0zfNpABQ.js.map +0 -1
- package/dist/assets/src-CbLkHfz8.js.map +0 -1
|
@@ -1,23 +1,28 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { $ as
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { n as
|
|
1
|
+
import { n as traceJoin } from "./joinTrace-CjRDFy85.js";
|
|
2
|
+
import { $c as budgetRetryMs, $i as PeerProofs, $o as HTTP_SERVICE_PROTO, $r as deviceWakeRequest, $s as enrollCancelFrame, $t as didDhtFromPublicKey, A as CommunitySession, Aa as COMMUNITY_REACTION_FRAME, Ao as DirectPathWatch, Ar as rankHubs, As as handoffStreamKey, B as knockRecords, Ba as wireReaction, Bc as signTurnPacket, Bn as isFederationId, Bo as LIMITS, Bs as readHandoffVerified, C as sha256Paced, Ca as EDIT_BUFFER_MS, Cc as deviceEchoNonce, Co as GROUP_MEMBER_CAP, Cr as freshHubs, Cs as handoffEphemeral, D as openGroupTyping, Da as parseLinkPreview, Dc as newDeviceSetSecret, Do as rosterHas, Dr as mergeLobby, Ds as handoffPakeFrame, E as groupTypingEpoch, Ea as dhtEditId, Ec as devicePingFrame, Eo as commitHash, Er as mergeBeacon, Es as handoffOfferFrame, Et as normalizeAtprotoHandle, Fa as queueReaction, Fc as TURN_LAST_SEQUENCE, Fn as decodeBolt11, Fo as presenceSeenAt, Fr as ChatFiles, Fs as readHandoffDone, G as groupWakeFrame, Gc as turnStatedVerifier, Gn as LIVENESS_PING_MS, Gr as PIN_LIMITS, Gs as setAckFrame, H as readKnocks, Ha as readReply, Hc as turnInstance, Hi as IDENTITY_REVOCATION_LABEL, Hn as parseFedimintRequestPayload, Ho as encodeControl, I as decodeGroupEntryLink, Ia as reactionEmoji, Ic as TURN_MAX, Ii as CapsExchange, In as ENDPOINT, Io as GhostlyHttpError, Ir as FILE_LIMITS, Is as readHandoffHave, J as GROUP_LIMITS, Jc as RelayTransport, Ji as identityStatement, Jn as pairedMessageFrame, Jr as pinIsNewer, Js as verifySetUpdate, K as groupWakes, Kc as turnVerifierMark, Ki as emptyIdentityLedger, Kn as PAIRED_FRAME_TOO_LARGE, Kr as mayPin, Ks as setUpdateFrame, L as encodeGroupEntryLink, La as reactionIsNewer, Lc as firstDeviceSetSecret, Li as DHT_TEXT_CAPABILITY, Ln as cashuRequestPayload, Lo as formatLocalTarget, Lr as transferEnded, Ls as readHandoffHello, M as communityHasFrame, Ma as REACTION_LIMITS, Mc as TURN_SETTLE_MS, Mo as DHT_POLL_INTERVALS, Mr as readBeaconHead, Ms as newHandoffId, N as decodeCommunityLink, Na as ReactionWindow, Nc as classifyTurnRead, Nr as readLobby, Ns as readHandoffAck, O as sealGroupTyping, Oa as forwardedAgain, Oc as seedSigner, Oo as verifyCommitSignature, Or as newerHead, Os as handoffReleaseFrame, P as encodeCommunityLink, Pa as nextReactionNumber, Pc as turnPutSummary, Po as RELAY_POLL_INTERVALS, Pr as shouldBeHub, Ps as readHandoffCancel, Q as frameBytes, Qc as traceLink, Qi as lastSharedWithMe, Qo as emptyLinkRecords, Qr as WakeLimiter, Qt as didDhtDocument, R as entryParams, Ra as readReaction, Rc as nextTurnPosition, Ri as capsDescriptors, Rn as fedimintRequestPayload, Ro as parseLocalTarget, Rr as transferStalled, Rs as readHandoffPake, S as readAppBundleAsync, Sa as parseVideoMeta, Sc as deviceEchoFrame, Sn as isBitcoinAddress, So as readCanonicalJson, Sr as doorHubs, Ss as handoffDigest, T as GroupTypingBoard, Ta as RateWindow, Tc as deviceLinkPairing, To as MEMBER_KEY, Tr as lobbyRecords, Ts as handoffManifestFrames, Tt as isAtprotoDid, Ua as replySnippet, Uc as turnKeys, Un as GhostLink, Ur as typingActivity, V as mergeKnocks, Va as pairedReplyAuthor, Vc as signTurnRelease, Vn as parseCashuRequestPayload, Vo as decodeControl, Vr as TypingSender, Vs as sealHandoffPiece, W as GroupWakeLimiter, Wa as BUTTON_ID, Wc as turnName, Wi as IdentityExchange, Wn as INVITE_TAKEN, Wo as RTC_CONFIG, Ws as readSetUpdate, X as GROUP_READ_NOTE, Xc as normalizeRelayUrl, Xi as receivedIdentityStatus, Xn as engineText, Xo as parseCallSignal, Xr as readPin, Ya as STATUS_CARD_LIMITS, Yc as currentRelays, Yi as newIdentityBinding, Yn as engineError, Yr as pinNumberHolds, Z as GroupSession, Zi as revokesIdentity, Zn as GROUP_FILE_LIMITS, Zr as WAKE_CALL_INTERVAL_MS, _c as inviteOwnership, _i as HoldKeys, _l as randomBytes, _n as walletNetworkOf, _r as meshHubs, _s as handoffBusyFrame, a as planAppUpdate, ac as enrollProofFrame, ai as vapidKeysMatch, an as EVM_TEST_CHAINS, ao as heardTime, as as HANDOFF_HAVE, at as groupName, b as pacer, ba as parseVoiceMeta, bc as ENROLL_CAPABILITY, bi as newMailbox, bl as utf8Decode, bn as sparkAddressKind, bo as canonicalJson, br as beaconKeys, bs as handoffContextProof, ca as peekDhtMailbox, cc as verifyEnrollProof, ci as isAppVersion, cl as createIdentity, cn as SEPOLIA_TEST_USDT, cr as CarriedTransport, cs as HANDOFF_PAKE, d as APP_BUNDLE_LIMITS, da as parseImageMeta, dc as readDeviceInvite, di as EDIT_CAPABILITY, dl as publicKeyFromZ32, dn as USDT_PUBLIC_RPC, do as appRef, ds as HANDOFF_TIMINGS, ea as emptyProofLedger, ec as enrollCancelReason, ei as newWakeToken, el as isDiscoveryBudgetError, en as encodeDidDhtDeactivated, eo as checkStatusCard, es as LEGACY_SERVICES, f as APP_BUNDLE_MAGIC, fc as OwnInviteError, fi as STATUS_CARD_CAPABILITY, fl as verify, fn as WALLET_NETWORKS, fs as HANDOFF_VERIFIED, g as appViewOf, gi as HOLD_LIMITS, gl as fromBase64Url, gn as validatePaymentTarget, go as readAppRevocation, gr as mayBeHub, gs as handoffAttemptSucceeded, h as appDigest, ha as sanitizeMime, hc as encodeInviteCode, hi as HOLD_DATED_BACK_MS, hl as bytesEqual, hn as formatPaymentAmount, ho as isAppRef, hr as MESH_HUBS, hs as handoffAttemptFailed, i as checkAppBeforeRun, ic as enrollHelloFrame, ii as checkPushEndpoint, il as createRelayPayload, in as ETHEREUM_USDT, io as claimedTime, ir as readGroupFileMeta, is as HANDOFF_DONE, it as mentionsMember, j as GROUP_READ_NOTE_COMMUNITY, ja as GROUP_REACTION_FRAME, jc as TURN_RAISE_READ_TIMEOUT_MS, jr as readBeacon, js as handoffVersions, k as COMMUNITY_LIMITS, ka as readForwarded, kc as webCryptoSigner, ko as edgeParams, kr as pickHubs, ks as handoffRetryAfter, l as readAppStoreSteps, la as DHT_FILE_CAPABILITY, lc as createDeviceInvite, li as APPS_CAPABILITY, ll as identityFromSeed, lo as APP_STATEMENT_LIMITS, ls as HANDOFF_PIECE_BYTES, ma as sanitizeFileName, mc as decodeInviteCode, mi as HOLD_CAPABILITY, ml as sanitizeAvatar, mn as assertWholeSats, mo as isAppKey, mr as readGroupSignal, ms as handoffAttemptAllowed, n as appStoreDecision, na as TRANSPORTS, nc as enrollDoneFrame, ni as relayRequest, nl as PacketTooLargeError, no as statusCardText, nr as groupFileFallback, ns as HANDOFF_ACK, nt as carryMentions, o as APP_STORE_LIMITS, oc as readEnrollGrant, on as ONCHAIN_PROVIDER, oo as receivedTimestamp, os as HANDOFF_HELLO, p as APP_ICON_PATH, pa as formatFileSize, pc as createChatInvite, pi as UPGRADE_CAPABILITY, pn as assertTokenUnits, po as isAppHash, pr as groupSignalFrame, ps as filePartName, q as parseGroupWakeFrame, qc as DEFAULT_RELAYS, qi as identityRevocationValue, qn as pairedMessageFits, qr as nextPinNumber, qs as signTombstone, r as appUpdateDecision, ra as automaticTransport, rc as enrollGrantFrame, ri as wakeRequest, rn as signDidDhtPacket, ro as arrivalKey, rs as HANDOFF_CANCEL, rt as parseCommunityEdit, sa as DHT_MESSAGE_TTL, sc as readEnrollHello, si as chatAppId, sn as PaymentPreflightError, so as shownTime, ss as HANDOFF_OFFER, t as appRevocationFor, ta as validExternalKey, tc as enrollDigits, ti as parseWakeFrame, tl as withRequestOptions, tn as encodeDidDhtPacket, to as sameButtonText, ts as serviceIdFromName, tt as GROUP_EDIT_TEXT_BYTES, ua as dhtFileId, uc as deviceInviteJoinerParams, ui as BUTTONS_CAPABILITY, ul as identityFromSeedB64, un as TEST_USDT_FAUCET_AMOUNT, uo as appFingerprint, us as HANDOFF_REQUEST, vi as HoldRefusedError, vl as toBase64Url, vn as SPARK_NETWORKS, vr as pickMeshHubs, vs as handoffCancelFrame, wa as EDIT_RATE_WINDOW_MS, wc as deviceKeyZ32, wn as isPubkyKey, wr as lobbyKeys, ws as handoffHelloFrame, xc as HANDOFF_CAPABILITY, xi as readManifest, xl as utf8Encode, xn as sparkInvoiceDetails, xr as beaconRecords, xs as handoffDataFrame, y as isAppUrl, ya as fileMessageText, yc as DEVICES_CAPABILITY, yi as isHoldUrl, yl as toZ32, yn as SPARK_PROVIDER, yr as COMMUNITY_TOPOLOGY, ys as handoffContext, z as knockIdentity, za as validReactionNumber, zc as readTurnPacket, zi as dialDescriptors, zn as findEndpoint, zo as webLocalFetch, zs as readHandoffTurnFrame } from "./src-DDzSERyc.js";
|
|
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
|
+
import { t as bitcoindRpc } from "./bitcoindProvider-DGw3xBSa.js";
|
|
5
|
+
import { t as nwc } from "./nwcProvider-4eVz_8HP.js";
|
|
6
|
+
import { a as clearProfileStores, c as fileStore, d as profileOpenFailure, f as store, i as STORES$1, l as newestWalletTx, m as wrap, n as APP_STORAGE_SCOPE_INDEX, o as databaseName, p as transact, r as CARD_INDEX, s as deleteDatabase, t as APP_STORAGE_CHAT_INDEX, u as openDb } from "./idb-B5L8wuOm.js";
|
|
7
|
+
import { a as sealSeed, i as newDeviceKey, o as unsealSeed, r as intentRepository } from "./networkReason-CetKkTPo.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-IEUW3DQz.js";
|
|
9
|
+
import { i as fedimintDatabase, n as createIrohWebEndpoint, o as loadFedimintSdk, r as irohRelayProblem, t as DEFAULT_IROH_RELAYS } from "./irohWeb-4oChIOIp.js";
|
|
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, 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-CouHhCpJ.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-Di7VZtmU.js";
|
|
13
|
+
import { n as MAX_BACKUP_BYTES, t as BACKUP_MEDIA_TYPE } from "./envelope-WjingYSc.js";
|
|
14
|
+
import { t as hyperdhtRelayProblem } from "./hyperdhtRelay-BT_wRxab.js";
|
|
7
15
|
import { hkdf } from "@noble/hashes/hkdf.js";
|
|
8
16
|
import { sha256 } from "@noble/hashes/sha2.js";
|
|
9
17
|
import { base64, base64url, base64urlnopad, bech32, hex } from "@scure/base";
|
|
10
18
|
import { bytesToHex, concatBytes, hexToBytes, utf8ToBytes } from "@noble/hashes/utils.js";
|
|
11
19
|
import { chacha20poly1305 } from "@noble/ciphers/chacha.js";
|
|
12
20
|
import { secp256k1 } from "@noble/curves/secp256k1.js";
|
|
13
|
-
import {
|
|
14
|
-
import { decode, noteEncode, npubEncode } from "nostr-tools/nip19";
|
|
15
|
-
import { finalizeEvent, getPublicKey, verifyEvent } from "nostr-tools/pure";
|
|
21
|
+
import { verifyEvent } from "nostr-tools/pure";
|
|
16
22
|
import { generateMnemonic, mnemonicToSeedSync, validateMnemonic } from "@scure/bip39";
|
|
23
|
+
import { wordlist } from "@scure/bip39/wordlists/english.js";
|
|
17
24
|
import { Amount, MeltChangeError, OutputData, Wallet, decodePaymentRequest, deserializeSwapPreview, getEncodedToken, getTokenMetadata, isMintOperationError, serializeSwapPreview } from "@cashu/cashu-ts";
|
|
18
|
-
import {
|
|
19
|
-
import * as nip04 from "nostr-tools/nip04";
|
|
20
|
-
import * as nip44 from "nostr-tools/nip44";
|
|
25
|
+
import { decode, noteEncode, npubEncode } from "nostr-tools/nip19";
|
|
21
26
|
/** Evidence older than this says nothing about the clock now. */
|
|
22
27
|
var CLOCK_EVIDENCE_MS = 18e5;
|
|
23
28
|
/** Sources kept of each kind: a profile has a few relays, and may have hundreds of contacts. */
|
|
@@ -178,9 +183,23 @@ async function migrateWalletNetworks(now = Date.now()) {
|
|
|
178
183
|
return report;
|
|
179
184
|
}
|
|
180
185
|
//#endregion
|
|
186
|
+
//#region ../browser/src/engine/paymentAdapters/viewNews.ts
|
|
187
|
+
/**
|
|
188
|
+
* Whether a wallet's view reads otherwise after a poll: only then is the poll news to the engine, which makes the whole
|
|
189
|
+
* wallet view again for it. Compared as written, not by reference: a poll builds its history and expiry anew each time.
|
|
190
|
+
* A view that cannot be written out is news.
|
|
191
|
+
*/
|
|
192
|
+
function viewNews(before, after) {
|
|
193
|
+
try {
|
|
194
|
+
return JSON.stringify(before) !== JSON.stringify(after);
|
|
195
|
+
} catch {
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
//#endregion
|
|
181
200
|
//#region ../browser/src/engine/paymentAdapters/usdtWallet.ts
|
|
182
201
|
/** The Ethereum libraries, a large part of the app: loaded once a USDT wallet is made, opened or restored, not with the engine. */
|
|
183
|
-
var loadUsdt = () => import("./usdt-
|
|
202
|
+
var loadUsdt = () => import("./usdt-Fj8CQO2z.js");
|
|
184
203
|
/** Every new profile starts with this wallet: an address to receive on, nothing to set up. */
|
|
185
204
|
var DEFAULT_USDT = {
|
|
186
205
|
network: "ethereum",
|
|
@@ -217,6 +236,7 @@ var UsdtWallet = class {
|
|
|
217
236
|
balance: "0",
|
|
218
237
|
gasBalance: "0"
|
|
219
238
|
};
|
|
239
|
+
/** `changed(false)`: a balance read that found nothing new; the receipts are checked, the wallet view stays. */
|
|
220
240
|
constructor(network, changed) {
|
|
221
241
|
this.network = network;
|
|
222
242
|
this.changed = changed;
|
|
@@ -403,6 +423,7 @@ var UsdtWallet = class {
|
|
|
403
423
|
clearTimeout(this.timer);
|
|
404
424
|
const adapter = this.adapter;
|
|
405
425
|
if (!adapter) return;
|
|
426
|
+
const before = this.view;
|
|
406
427
|
try {
|
|
407
428
|
const address = await this.gate.within(adapter.address()), balances = await this.gate.within(adapter.balances());
|
|
408
429
|
if (adapter !== this.adapter) return;
|
|
@@ -422,7 +443,7 @@ var UsdtWallet = class {
|
|
|
422
443
|
error: "RPC unavailable. Balance may be stale."
|
|
423
444
|
};
|
|
424
445
|
}
|
|
425
|
-
this.changed();
|
|
446
|
+
this.changed(viewNews(before, this.view));
|
|
426
447
|
if (this.adapter) this.timer = setTimeout(() => void this.refresh(), 8e3);
|
|
427
448
|
}
|
|
428
449
|
/** Testnet on Sepolia: 1,000 TEST-USDT from Aave's faucet, paid with this wallet's own test ETH. */
|
|
@@ -529,21 +550,6 @@ var UsdtWallet = class {
|
|
|
529
550
|
}
|
|
530
551
|
};
|
|
531
552
|
//#endregion
|
|
532
|
-
//#region ../browser/src/shared/ice.ts
|
|
533
|
-
/**
|
|
534
|
-
* What is wrong with a TURN/STUN server, or null. Browsers refuse to create any WebRTC connection when
|
|
535
|
-
* one server in the list is malformed, so a bad entry here would silently cut off calls and files.
|
|
536
|
-
*/
|
|
537
|
-
function iceServerProblem(server) {
|
|
538
|
-
const urls = server.urls.split(/[\s,]+/).filter(Boolean);
|
|
539
|
-
if (!urls.length) return "Enter the TURN server address";
|
|
540
|
-
for (const url of urls) {
|
|
541
|
-
if (!/^(stun|turns?):[^\s/?#]+(\?transport=(udp|tcp))?$/i.test(url)) return `“${url}” is not a TURN or STUN address (turn:host:port)`;
|
|
542
|
-
if (/^turns?:/i.test(url) && (!server.username?.trim() || !server.credential?.trim())) return "A TURN server needs its username and credential";
|
|
543
|
-
}
|
|
544
|
-
return null;
|
|
545
|
-
}
|
|
546
|
-
//#endregion
|
|
547
553
|
//#region ../browser/src/engine/paymentAdapters/backup.ts
|
|
548
554
|
/**
|
|
549
555
|
* Wallet database versions a backup may carry. 3 is what SDK 0.4.74-0.4.76 made (Ghostly 1.1.3 and earlier);
|
|
@@ -642,7 +648,7 @@ async function restoreArkDatabase(walletId, snapshot) {
|
|
|
642
648
|
//#endregion
|
|
643
649
|
//#region ../browser/src/engine/paymentAdapters/arkWallet.ts
|
|
644
650
|
/** The Ark SDK, a large part of the app: loaded once an Ark wallet is made, opened or restored, not with the engine. */
|
|
645
|
-
var loadArkade = () => import("./arkade-
|
|
651
|
+
var loadArkade = () => import("./arkade-C_qDmHnm.js");
|
|
646
652
|
/** How long a Testnet wallet about to make way for another waits for its server to say what it holds. */
|
|
647
653
|
var RETIRE_READ_MS = 1e4;
|
|
648
654
|
/** Every new profile starts with this wallet: nothing to set up before receiving. */
|
|
@@ -693,6 +699,7 @@ var ArkWallet = class {
|
|
|
693
699
|
locked: true,
|
|
694
700
|
balance: 0
|
|
695
701
|
};
|
|
702
|
+
/** `changed(false)`: a balance read that found nothing new; the receipts are checked, the wallet view stays. */
|
|
696
703
|
constructor(network, changed) {
|
|
697
704
|
this.network = network;
|
|
698
705
|
this.changed = changed;
|
|
@@ -1001,6 +1008,7 @@ var ArkWallet = class {
|
|
|
1001
1008
|
});
|
|
1002
1009
|
if (this.adapter !== adapter) return;
|
|
1003
1010
|
const balancesRead = this.view.read || !failed.some((what) => what === "balance" || what === "incoming" || what === "recoverable");
|
|
1011
|
+
const before = this.view;
|
|
1004
1012
|
this.view = {
|
|
1005
1013
|
configured: true,
|
|
1006
1014
|
locked: false,
|
|
@@ -1018,7 +1026,7 @@ var ArkWallet = class {
|
|
|
1018
1026
|
...expiresAt !== void 0 ? { expiresAt } : {},
|
|
1019
1027
|
error: failed.length ? `Could not read the ${failed.join(", ")} from the Ark provider. Last values may be stale.` : void 0
|
|
1020
1028
|
};
|
|
1021
|
-
this.changed();
|
|
1029
|
+
this.changed(viewNews(before, this.view));
|
|
1022
1030
|
if (this.adapter) this.timer = setTimeout(() => void this.refresh(), 1e4);
|
|
1023
1031
|
}
|
|
1024
1032
|
/** Swept outputs back into the balance (refused while expired ones wait for the sweep); the next refresh shows them. */
|
|
@@ -1086,10 +1094,10 @@ var NETWORK = {
|
|
|
1086
1094
|
signet: "Signet",
|
|
1087
1095
|
regtest: "Regtest"
|
|
1088
1096
|
};
|
|
1089
|
-
var loading$
|
|
1097
|
+
var loading$4;
|
|
1090
1098
|
/** Loads the WebAssembly once, on first use: profiles that never open a Bark wallet never fetch its 7.7 MB. */
|
|
1091
1099
|
function loadBarkSdk() {
|
|
1092
|
-
return loading$
|
|
1100
|
+
return loading$4 ??= (async () => {
|
|
1093
1101
|
const [bark, wasm] = await Promise.all([import("@secondts/bark/web"), import("./bark_ffi_wasm_bg-EY2uc0Bx.js")]);
|
|
1094
1102
|
await bark.default({ module_or_path: wasm.default });
|
|
1095
1103
|
return {
|
|
@@ -1128,7 +1136,7 @@ function loadBarkSdk() {
|
|
|
1128
1136
|
}
|
|
1129
1137
|
};
|
|
1130
1138
|
})().catch((error) => {
|
|
1131
|
-
loading$
|
|
1139
|
+
loading$4 = void 0;
|
|
1132
1140
|
throw error;
|
|
1133
1141
|
});
|
|
1134
1142
|
}
|
|
@@ -1493,6 +1501,7 @@ var BarkWallet = class {
|
|
|
1493
1501
|
locked: true,
|
|
1494
1502
|
balance: 0
|
|
1495
1503
|
};
|
|
1504
|
+
/** `changed(false)`: a balance read that found nothing new; the receipts are checked, the wallet view stays. */
|
|
1496
1505
|
constructor(network, changed, sdk = loadBarkSdk) {
|
|
1497
1506
|
this.network = network;
|
|
1498
1507
|
this.changed = changed;
|
|
@@ -1778,6 +1787,7 @@ var BarkWallet = class {
|
|
|
1778
1787
|
}
|
|
1779
1788
|
if (this.adapter !== adapter) return;
|
|
1780
1789
|
const balancesRead = this.view.read || balance !== void 0 && onchain !== void 0;
|
|
1790
|
+
const before = this.view;
|
|
1781
1791
|
this.view = {
|
|
1782
1792
|
...this.idle(adapter.config),
|
|
1783
1793
|
locked: false,
|
|
@@ -1792,7 +1802,7 @@ var BarkWallet = class {
|
|
|
1792
1802
|
expiry,
|
|
1793
1803
|
error: failed.length ? `Could not read the ${failed.join(", ")} from the Bark server. Last values may be stale.` : void 0
|
|
1794
1804
|
};
|
|
1795
|
-
this.changed();
|
|
1805
|
+
this.changed(viewNews(before, this.view));
|
|
1796
1806
|
if (this.adapter) this.timer = setTimeout(() => void this.refresh(), 1e4);
|
|
1797
1807
|
}
|
|
1798
1808
|
/** On-chain coins into Ark: the board confirms on-chain, then they are spendable. */
|
|
@@ -2649,10 +2659,10 @@ var FedimintAdapter = class {
|
|
|
2649
2659
|
};
|
|
2650
2660
|
//#endregion
|
|
2651
2661
|
//#region ../browser/src/engine/paymentAdapters/providers/breezSdk.ts
|
|
2652
|
-
var loading$
|
|
2662
|
+
var loading$3;
|
|
2653
2663
|
/** Loads the WebAssembly once, on first use: profiles that never pick Breez never fetch its 12.8 MB. */
|
|
2654
2664
|
function loadBreezSdk() {
|
|
2655
|
-
return loading$
|
|
2665
|
+
return loading$3 ??= (async () => {
|
|
2656
2666
|
const breez = await import("./web-DwgdhfcY.js");
|
|
2657
2667
|
await breez.default();
|
|
2658
2668
|
return { async connect({ network, mnemonic, apiKey, storage }) {
|
|
@@ -2668,11 +2678,11 @@ function loadBreezSdk() {
|
|
|
2668
2678
|
});
|
|
2669
2679
|
} };
|
|
2670
2680
|
})().catch((error) => {
|
|
2671
|
-
loading$
|
|
2681
|
+
loading$3 = void 0;
|
|
2672
2682
|
throw error;
|
|
2673
2683
|
});
|
|
2674
2684
|
}
|
|
2675
|
-
var hex$
|
|
2685
|
+
var hex$8 = (bytes) => Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
2676
2686
|
var breezNetwork = (network) => network === "bitcoin" ? "mainnet" : "regtest";
|
|
2677
2687
|
var DISCONNECT_MS = 1e4;
|
|
2678
2688
|
/** One SDK instance per wallet storage: two over one database would each think the leaves are theirs. */
|
|
@@ -2684,7 +2694,7 @@ var open = /* @__PURE__ */ new Map();
|
|
|
2684
2694
|
*/
|
|
2685
2695
|
async function openBreez(params, sdk = loadBreezSdk, storage = breezDatabase) {
|
|
2686
2696
|
const name = await storage(params.network, params.mnemonic);
|
|
2687
|
-
const key = `${name}|${params.apiKey ? hex$
|
|
2697
|
+
const key = `${name}|${params.apiKey ? hex$8(sha256(new TextEncoder().encode(params.apiKey))).slice(0, 8) : ""}`;
|
|
2688
2698
|
let entry = open.get(key);
|
|
2689
2699
|
if (!entry) {
|
|
2690
2700
|
const wallet = sdk().then((module) => module.connect({
|
|
@@ -3018,6 +3028,7 @@ var SparkWallet = class {
|
|
|
3018
3028
|
locked: true,
|
|
3019
3029
|
balance: 0
|
|
3020
3030
|
};
|
|
3031
|
+
/** `changed(false)`: a balance read that found nothing new; the receipts are checked, the wallet view stays. */
|
|
3021
3032
|
constructor(network, changed, sdk = loadBreezSdk) {
|
|
3022
3033
|
this.network = network;
|
|
3023
3034
|
this.changed = changed;
|
|
@@ -3233,6 +3244,7 @@ var SparkWallet = class {
|
|
|
3233
3244
|
const history = await read("history", () => adapter.history(), this.view.history);
|
|
3234
3245
|
if (this.adapter !== adapter) return;
|
|
3235
3246
|
const balancesRead = this.view.read || !failed.includes("balance");
|
|
3247
|
+
const before = this.view;
|
|
3236
3248
|
this.view = {
|
|
3237
3249
|
...this.idle(this.saved),
|
|
3238
3250
|
locked: false,
|
|
@@ -3242,7 +3254,7 @@ var SparkWallet = class {
|
|
|
3242
3254
|
history,
|
|
3243
3255
|
error: failed.length ? `Could not read the ${failed.join(", ")} from Spark. Last values may be stale.` : void 0
|
|
3244
3256
|
};
|
|
3245
|
-
this.changed();
|
|
3257
|
+
this.changed(viewNews(before, this.view));
|
|
3246
3258
|
if (this.adapter) this.timer = setTimeout(() => void this.refresh(), sparkTiming.pollMs);
|
|
3247
3259
|
}
|
|
3248
3260
|
/** Where a contact pays one request: a Spark invoice made for it (its amount, its memo, a quarter of an hour). */
|
|
@@ -3354,6 +3366,17 @@ var PaymentCoordinator = class {
|
|
|
3354
3366
|
adapters;
|
|
3355
3367
|
changed;
|
|
3356
3368
|
active = /* @__PURE__ */ new Map();
|
|
3369
|
+
/**
|
|
3370
|
+
* The reviews still pending (an Approve may send them), by id: what `dropStale` looks at, so that its minute's sweep
|
|
3371
|
+
* does not read every review ever made. `null` until the store was read once. An id leaves when the sweep finds its
|
|
3372
|
+
* review no longer pending, or gone.
|
|
3373
|
+
*/
|
|
3374
|
+
pending = null;
|
|
3375
|
+
/** Reviews read from the store past the coordinator (`refreshWallet`'s list): the pending ones are swept too. */
|
|
3376
|
+
notePending(reviews) {
|
|
3377
|
+
const pending = this.pending ??= /* @__PURE__ */ new Set();
|
|
3378
|
+
for (const review of reviews) if (review.state === "pending") pending.add(review.id);
|
|
3379
|
+
}
|
|
3357
3380
|
constructor(repository, adapters, changed = () => {}) {
|
|
3358
3381
|
this.repository = repository;
|
|
3359
3382
|
this.adapters = adapters;
|
|
@@ -3384,6 +3407,7 @@ var PaymentCoordinator = class {
|
|
|
3384
3407
|
review,
|
|
3385
3408
|
prepared
|
|
3386
3409
|
});
|
|
3410
|
+
this.pending?.add(review.id);
|
|
3387
3411
|
this.changed(review);
|
|
3388
3412
|
return review;
|
|
3389
3413
|
}
|
|
@@ -3454,12 +3478,20 @@ var PaymentCoordinator = class {
|
|
|
3454
3478
|
*/
|
|
3455
3479
|
async dropStale(now = Date.now(), unpayable = () => false) {
|
|
3456
3480
|
const dropped = [];
|
|
3457
|
-
|
|
3458
|
-
|
|
3481
|
+
if (!this.pending) this.notePending((await this.repository.list()).map(({ review }) => review));
|
|
3482
|
+
const pending = this.pending;
|
|
3483
|
+
for (const id of [...pending]) {
|
|
3484
|
+
const review = (await this.repository.get(id))?.review;
|
|
3485
|
+
if (!review || review.state !== "pending") {
|
|
3486
|
+
pending.delete(id);
|
|
3487
|
+
continue;
|
|
3488
|
+
}
|
|
3489
|
+
if (review.expiresAt > now && !unpayable(review)) continue;
|
|
3459
3490
|
try {
|
|
3460
3491
|
const cancelled = await this.cancel(review.id);
|
|
3461
3492
|
if (cancelled.state === "cancelled") dropped.push(cancelled);
|
|
3462
3493
|
} catch {}
|
|
3494
|
+
pending.delete(id);
|
|
3463
3495
|
}
|
|
3464
3496
|
return dropped;
|
|
3465
3497
|
}
|
|
@@ -3791,7 +3823,7 @@ var PAGE = 100;
|
|
|
3791
3823
|
var PAGES = 10;
|
|
3792
3824
|
/** Without any record of a payment, it is called failed only this long after its invoice expired. */
|
|
3793
3825
|
var LOST_AFTER_MS = 36e5;
|
|
3794
|
-
var hex$
|
|
3826
|
+
var hex$7 = (bytes) => Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
3795
3827
|
var sats$4 = (value) => {
|
|
3796
3828
|
const n = Number(value);
|
|
3797
3829
|
if (!Number.isSafeInteger(n) || n < 0) throw new Error("Breez returned an invalid amount");
|
|
@@ -3806,7 +3838,7 @@ function idempotencyKey(paymentHash, attempt) {
|
|
|
3806
3838
|
const b = sha256(new TextEncoder().encode(`ghostly-breez-pay:${paymentHash}:${attempt}`)).slice(0, 16);
|
|
3807
3839
|
b[6] = b[6] & 15 | 64;
|
|
3808
3840
|
b[8] = b[8] & 63 | 128;
|
|
3809
|
-
const h = hex$
|
|
3841
|
+
const h = hex$7(b);
|
|
3810
3842
|
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
|
|
3811
3843
|
}
|
|
3812
3844
|
/** The payment hash a Breez payment is about, when it is one (Lightning, or a Spark HTLC). */
|
|
@@ -4049,7 +4081,7 @@ var appLoader = async () => {
|
|
|
4049
4081
|
wasm: await fetch(url.default)
|
|
4050
4082
|
};
|
|
4051
4083
|
};
|
|
4052
|
-
var loading$
|
|
4084
|
+
var loading$2;
|
|
4053
4085
|
/**
|
|
4054
4086
|
* Loads the WebAssembly once, on first use: profiles that never set up a BDK wallet never fetch its 2 MB.
|
|
4055
4087
|
* The package is wasm-pack's `bundler` build, whose entry imports the `.wasm` as an ES module (which
|
|
@@ -4057,7 +4089,7 @@ var loading$1;
|
|
|
4057
4089
|
* that entry would.
|
|
4058
4090
|
*/
|
|
4059
4091
|
function loadBdk(loader = appLoader) {
|
|
4060
|
-
return loading$
|
|
4092
|
+
return loading$2 ??= (async () => {
|
|
4061
4093
|
const { glue, wasm } = await loader();
|
|
4062
4094
|
const imports = { [WASM_IMPORTS]: glue };
|
|
4063
4095
|
let instance;
|
|
@@ -4071,7 +4103,7 @@ function loadBdk(loader = appLoader) {
|
|
|
4071
4103
|
instance.exports.__wbindgen_start?.();
|
|
4072
4104
|
return glue;
|
|
4073
4105
|
})().catch((error) => {
|
|
4074
|
-
loading$
|
|
4106
|
+
loading$2 = void 0;
|
|
4075
4107
|
throw error;
|
|
4076
4108
|
});
|
|
4077
4109
|
}
|
|
@@ -4137,7 +4169,7 @@ var idbBdkStore = {
|
|
|
4137
4169
|
});
|
|
4138
4170
|
}
|
|
4139
4171
|
};
|
|
4140
|
-
var hex$
|
|
4172
|
+
var hex$6 = (bytes) => Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
4141
4173
|
var unhex = (text) => {
|
|
4142
4174
|
if (!/^([0-9a-f]{2})+$/.test(text)) throw new Error("Not hex");
|
|
4143
4175
|
return Uint8Array.from(text.match(/../g), (h) => parseInt(h, 16));
|
|
@@ -4351,7 +4383,7 @@ var BdkOnchain = class BdkOnchain {
|
|
|
4351
4383
|
throw new SourceConfigError(`bad descriptor: the recovery phrase does not make a ${SCRIPT[config.script].label} wallet (${message$3(error)})`, error);
|
|
4352
4384
|
}
|
|
4353
4385
|
const fingerprint = /\[([0-9a-f]{8})\//.exec(publicDescriptor)?.[1] ?? "";
|
|
4354
|
-
const key = `bdkWallet-${config.network}-${hex$
|
|
4386
|
+
const key = `bdkWallet-${config.network}-${hex$6(sha256(new TextEncoder().encode(publicDescriptor))).slice(0, 32)}`;
|
|
4355
4387
|
const walletStore = deps.store ?? idbBdkStore;
|
|
4356
4388
|
let stored = await walletStore.load(key);
|
|
4357
4389
|
if (stored) try {
|
|
@@ -4449,7 +4481,7 @@ var BdkOnchain = class BdkOnchain {
|
|
|
4449
4481
|
amount,
|
|
4450
4482
|
fee,
|
|
4451
4483
|
feeRate,
|
|
4452
|
-
signed: hex$
|
|
4484
|
+
signed: hex$6(tx.to_bytes())
|
|
4453
4485
|
};
|
|
4454
4486
|
}
|
|
4455
4487
|
/** What was signed is what was reviewed: one output of exactly the amount to the address, the rest ours. */
|
|
@@ -4702,435 +4734,6 @@ var bdk = {
|
|
|
4702
4734
|
}
|
|
4703
4735
|
};
|
|
4704
4736
|
//#endregion
|
|
4705
|
-
//#region ../browser/src/engine/paymentAdapters/providers/bitcoind.ts
|
|
4706
|
-
var BitcoindRpcError = class extends Error {
|
|
4707
|
-
kind;
|
|
4708
|
-
code;
|
|
4709
|
-
constructor(kind, message, code) {
|
|
4710
|
-
super(message);
|
|
4711
|
-
this.kind = kind;
|
|
4712
|
-
this.code = code;
|
|
4713
|
-
this.name = "BitcoindRpcError";
|
|
4714
|
-
}
|
|
4715
|
-
/** The node never ran the call: it was not sent, could not connect, or the credentials were refused. */
|
|
4716
|
-
get neverRan() {
|
|
4717
|
-
return this.kind === "refused" || this.kind === "connect" || this.kind === "auth";
|
|
4718
|
-
}
|
|
4719
|
-
};
|
|
4720
|
-
/** The Tauri command, through the Desktop host. */
|
|
4721
|
-
function tauriTransport$1(invoke) {
|
|
4722
|
-
if (!invoke) throw new Error("Bitcoin Core needs the Ghostly desktop app: a browser cannot reach its RPC");
|
|
4723
|
-
return (call) => invoke("bitcoind_rpc", { ...call });
|
|
4724
|
-
}
|
|
4725
|
-
/** Bitcoin Core's JSON-RPC error codes this module acts on (src/rpc/protocol.h). */
|
|
4726
|
-
var RPC = {
|
|
4727
|
-
INVALID_ADDRESS_OR_KEY: -5,
|
|
4728
|
-
VERIFY_ERROR: -25,
|
|
4729
|
-
VERIFY_REJECTED: -26,
|
|
4730
|
-
VERIFY_ALREADY_IN_CHAIN: -27,
|
|
4731
|
-
DESERIALIZATION_ERROR: -22
|
|
4732
|
-
};
|
|
4733
|
-
var CHAINS = {
|
|
4734
|
-
main: "bitcoin",
|
|
4735
|
-
test: "testnet",
|
|
4736
|
-
testnet4: "testnet",
|
|
4737
|
-
signet: "signet",
|
|
4738
|
-
regtest: "regtest"
|
|
4739
|
-
};
|
|
4740
|
-
/** A fee rate for the test networks when the node has too few blocks to estimate one (regtest always). */
|
|
4741
|
-
var TEST_FALLBACK_FEE_RATE = 2;
|
|
4742
|
-
var TXID = /^[0-9a-f]{64}$/;
|
|
4743
|
-
var WALLET_NAME = /^[A-Za-z0-9._-]{0,64}$/;
|
|
4744
|
-
/** Address encodings, for btc-signer to read the outputs back as addresses. */
|
|
4745
|
-
var TESTNET_ENCODING = {
|
|
4746
|
-
bech32: "tb",
|
|
4747
|
-
pubKeyHash: 111,
|
|
4748
|
-
scriptHash: 196,
|
|
4749
|
-
wif: 239
|
|
4750
|
-
};
|
|
4751
|
-
var BTC_NETWORKS = {
|
|
4752
|
-
bitcoin: {
|
|
4753
|
-
bech32: "bc",
|
|
4754
|
-
pubKeyHash: 0,
|
|
4755
|
-
scriptHash: 5,
|
|
4756
|
-
wif: 128
|
|
4757
|
-
},
|
|
4758
|
-
testnet: TESTNET_ENCODING,
|
|
4759
|
-
signet: TESTNET_ENCODING,
|
|
4760
|
-
mutinynet: TESTNET_ENCODING,
|
|
4761
|
-
regtest: {
|
|
4762
|
-
...TESTNET_ENCODING,
|
|
4763
|
-
bech32: "bcrt"
|
|
4764
|
-
}
|
|
4765
|
-
};
|
|
4766
|
-
var TX_OPTS = {
|
|
4767
|
-
allowUnknownOutputs: true,
|
|
4768
|
-
allowUnknownInputs: true,
|
|
4769
|
-
disableScriptCheck: true,
|
|
4770
|
-
allowLegacyWitnessUtxo: true
|
|
4771
|
-
};
|
|
4772
|
-
/** Whole sats as the exact decimal string bitcoind reads as BTC (never a float). */
|
|
4773
|
-
var satsToBtc = (sats) => {
|
|
4774
|
-
if (!Number.isSafeInteger(sats) || sats < 0) throw new Error("Amounts are whole sats");
|
|
4775
|
-
const digits = sats.toString().padStart(9, "0");
|
|
4776
|
-
return `${digits.slice(0, -8)}.${digits.slice(-8)}`;
|
|
4777
|
-
};
|
|
4778
|
-
/** A BTC amount from bitcoind (a JSON number with at most 8 decimals) as whole sats. */
|
|
4779
|
-
var btcToSats = (btc) => {
|
|
4780
|
-
if (typeof btc !== "number" || !Number.isFinite(btc)) throw new Error("The node returned an amount that is not a number");
|
|
4781
|
-
return Math.round(btc * 1e8);
|
|
4782
|
-
};
|
|
4783
|
-
var object = (value, what) => {
|
|
4784
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`The node returned no ${what}`);
|
|
4785
|
-
return value;
|
|
4786
|
-
};
|
|
4787
|
-
var text$2 = (value, what) => {
|
|
4788
|
-
if (typeof value !== "string" || !value) throw new Error(`The node returned no ${what}`);
|
|
4789
|
-
return value;
|
|
4790
|
-
};
|
|
4791
|
-
var BitcoindOnchain = class BitcoindOnchain {
|
|
4792
|
-
config;
|
|
4793
|
-
transport;
|
|
4794
|
-
closed = false;
|
|
4795
|
-
network;
|
|
4796
|
-
constructor(config, transport) {
|
|
4797
|
-
this.config = config;
|
|
4798
|
-
this.transport = transport;
|
|
4799
|
-
}
|
|
4800
|
-
/** Connects and checks the wallet is there and the node is on a network of `mode`. */
|
|
4801
|
-
static async connect(config, mode, transport, signal) {
|
|
4802
|
-
const provider = new BitcoindOnchain(config, transport);
|
|
4803
|
-
signal?.addEventListener("abort", () => void provider.close(), { once: true });
|
|
4804
|
-
const { network } = await provider.info();
|
|
4805
|
-
if (networkMode(network) !== mode) throw new Error(mode === "mainnet" ? `This node is on ${network}, not Bitcoin mainnet` : "This node is on Bitcoin mainnet: set it up as a Mainnet wallet instead");
|
|
4806
|
-
if (object(await provider.call("getwalletinfo"), "wallet information").private_keys_enabled === false) throw new Error("This wallet has no private keys: it cannot sign a payment");
|
|
4807
|
-
return provider;
|
|
4808
|
-
}
|
|
4809
|
-
async call(method, ...params) {
|
|
4810
|
-
if (this.closed) throw new BitcoindRpcError("refused", "This Bitcoin Core source is closed");
|
|
4811
|
-
try {
|
|
4812
|
-
return await this.transport({
|
|
4813
|
-
...this.config,
|
|
4814
|
-
method,
|
|
4815
|
-
params
|
|
4816
|
-
});
|
|
4817
|
-
} catch (error) {
|
|
4818
|
-
if (error instanceof BitcoindRpcError) throw error;
|
|
4819
|
-
const failure = error;
|
|
4820
|
-
if (failure && typeof failure === "object" && typeof failure.kind === "string" && typeof failure.message === "string") throw new BitcoindRpcError(failure.kind, failure.message, failure.code);
|
|
4821
|
-
throw new BitcoindRpcError("transport", error instanceof Error ? error.message : String(error));
|
|
4822
|
-
}
|
|
4823
|
-
}
|
|
4824
|
-
async info() {
|
|
4825
|
-
const chain = object(await this.call("getblockchaininfo"), "chain information").chain;
|
|
4826
|
-
const network = typeof chain === "string" ? CHAINS[chain] : void 0;
|
|
4827
|
-
if (!network) throw new Error(`Ghostly does not know the chain "${String(chain).slice(0, 20)}"`);
|
|
4828
|
-
this.network = network;
|
|
4829
|
-
return {
|
|
4830
|
-
network,
|
|
4831
|
-
alias: this.config.wallet || "Bitcoin Core"
|
|
4832
|
-
};
|
|
4833
|
-
}
|
|
4834
|
-
async receiveAddress() {
|
|
4835
|
-
return text$2(await this.call("getnewaddress", "Ghostly"), "address");
|
|
4836
|
-
}
|
|
4837
|
-
async balance() {
|
|
4838
|
-
const mine = object(object(await this.call("getbalances"), "balances").mine, "balances");
|
|
4839
|
-
return {
|
|
4840
|
-
confirmed: btcToSats(mine.trusted),
|
|
4841
|
-
unconfirmed: btcToSats(mine.untrusted_pending)
|
|
4842
|
-
};
|
|
4843
|
-
}
|
|
4844
|
-
async feeRate(request) {
|
|
4845
|
-
if (request.feeRate !== void 0) {
|
|
4846
|
-
if (!Number.isFinite(request.feeRate) || request.feeRate < 1 || request.feeRate > 1e4) throw new Error("The fee rate must be between 1 and 10,000 sat/vB");
|
|
4847
|
-
return request.feeRate;
|
|
4848
|
-
}
|
|
4849
|
-
const estimate = object(await this.call("estimatesmartfee", 6), "fee estimate");
|
|
4850
|
-
if (typeof estimate.feerate === "number" && estimate.feerate > 0) return Math.max(1, Math.ceil(estimate.feerate * 1e5 * 1e3) / 1e3);
|
|
4851
|
-
if ((this.network ?? (await this.info()).network) === "bitcoin") throw new Error("Your node cannot estimate a fee yet. Nothing was sent.");
|
|
4852
|
-
return TEST_FALLBACK_FEE_RATE;
|
|
4853
|
-
}
|
|
4854
|
-
async prepareSend(request) {
|
|
4855
|
-
if (!Number.isSafeInteger(request.amount) || request.amount <= 0) throw new Error("Amounts are whole sats");
|
|
4856
|
-
const network = this.network ?? (await this.info()).network;
|
|
4857
|
-
const feeRate = await this.feeRate(request);
|
|
4858
|
-
const funded = object(await this.call("walletcreatefundedpsbt", [], [{ [request.address]: satsToBtc(request.amount) }], 0, {
|
|
4859
|
-
fee_rate: feeRate,
|
|
4860
|
-
lockUnspents: true,
|
|
4861
|
-
replaceable: true
|
|
4862
|
-
}), "funded transaction");
|
|
4863
|
-
const inputs = this.outpoints(Transaction.fromPSBT(base64.decode(text$2(funded.psbt, "funded transaction")), TX_OPTS));
|
|
4864
|
-
try {
|
|
4865
|
-
const fee = btcToSats(funded.fee);
|
|
4866
|
-
if (fee > request.feeCap) throw new Error(`The fee (${fee} sats) is above your limit of ${request.feeCap}`);
|
|
4867
|
-
const signed = object(await this.call("walletprocesspsbt", funded.psbt, true, "ALL"), "signed transaction");
|
|
4868
|
-
if (signed.complete !== true) throw new Error("The wallet could not sign this payment (is it locked, or watch-only?)");
|
|
4869
|
-
const final = object(await this.call("finalizepsbt", signed.psbt, false), "finalized transaction");
|
|
4870
|
-
if (final.complete !== true) throw new Error("The wallet could not finalize this payment");
|
|
4871
|
-
return this.check(Transaction.fromPSBT(base64.decode(text$2(final.psbt, "finalized transaction")), TX_OPTS), request, inputs, feeRate, network);
|
|
4872
|
-
} catch (error) {
|
|
4873
|
-
await this.unlock(inputs).catch(() => {});
|
|
4874
|
-
throw error;
|
|
4875
|
-
}
|
|
4876
|
-
}
|
|
4877
|
-
/** The transaction the node signed is exactly the one asked for, before anyone reviews it. */
|
|
4878
|
-
check(tx, request, funded, feeRate, network) {
|
|
4879
|
-
const same = (a, b) => a.length === b.length && a.every((o, i) => o.txid === b[i].txid && o.vout === b[i].vout);
|
|
4880
|
-
if (!same(this.outpoints(tx), funded)) throw new Error("The node signed other coins than it funded the payment with");
|
|
4881
|
-
const script = (() => {
|
|
4882
|
-
try {
|
|
4883
|
-
return hex.encode(OutScript.encode(Address(BTC_NETWORKS[network]).decode(request.address)));
|
|
4884
|
-
} catch {
|
|
4885
|
-
return;
|
|
4886
|
-
}
|
|
4887
|
-
})();
|
|
4888
|
-
let paid = 0;
|
|
4889
|
-
for (let i = 0; i < tx.outputsLength; i++) {
|
|
4890
|
-
const output = tx.getOutput(i);
|
|
4891
|
-
if (script !== void 0 && output.script && hex.encode(output.script) === script) paid += Number(output.amount ?? 0n);
|
|
4892
|
-
}
|
|
4893
|
-
if (paid !== request.amount) throw new Error("The node built a transaction that does not pay the address and amount asked for");
|
|
4894
|
-
const fee = Number(tx.fee);
|
|
4895
|
-
if (!Number.isSafeInteger(fee) || fee < 0 || fee > request.feeCap) throw new Error(`The fee (${fee} sats) is above your limit of ${request.feeCap}`);
|
|
4896
|
-
const txid = tx.id;
|
|
4897
|
-
if (!TXID.test(txid)) throw new Error("The node returned a transaction without a valid id");
|
|
4898
|
-
return {
|
|
4899
|
-
txid,
|
|
4900
|
-
address: request.address,
|
|
4901
|
-
amount: request.amount,
|
|
4902
|
-
fee,
|
|
4903
|
-
feeRate,
|
|
4904
|
-
signed: hex.encode(tx.extract())
|
|
4905
|
-
};
|
|
4906
|
-
}
|
|
4907
|
-
outpoints(tx) {
|
|
4908
|
-
const out = [];
|
|
4909
|
-
for (let i = 0; i < tx.inputsLength; i++) {
|
|
4910
|
-
const input = tx.getInput(i);
|
|
4911
|
-
if (!input.txid || input.index === void 0) throw new Error("The node returned a transaction with an unreadable input");
|
|
4912
|
-
out.push({
|
|
4913
|
-
txid: hex.encode(input.txid),
|
|
4914
|
-
vout: input.index
|
|
4915
|
-
});
|
|
4916
|
-
}
|
|
4917
|
-
return out;
|
|
4918
|
-
}
|
|
4919
|
-
async unlock(outpoints) {
|
|
4920
|
-
if (outpoints.length) await this.call("lockunspent", true, outpoints);
|
|
4921
|
-
}
|
|
4922
|
-
signedTx(prepared) {
|
|
4923
|
-
if (!/^[0-9a-f]+$/.test(prepared.signed)) throw new Error("This prepared payment has no signed transaction");
|
|
4924
|
-
const tx = Transaction.fromRaw(hex.decode(prepared.signed), TX_OPTS);
|
|
4925
|
-
if (tx.id !== prepared.txid) throw new Error("This prepared payment's transaction is not the one reviewed");
|
|
4926
|
-
return tx;
|
|
4927
|
-
}
|
|
4928
|
-
async broadcast(prepared) {
|
|
4929
|
-
const tx = this.signedTx(prepared);
|
|
4930
|
-
let txid;
|
|
4931
|
-
try {
|
|
4932
|
-
txid = await this.call("sendrawtransaction", prepared.signed);
|
|
4933
|
-
} catch (error) {
|
|
4934
|
-
if (!(error instanceof BitcoindRpcError)) throw error;
|
|
4935
|
-
if (error.kind === "rpc" && error.code === RPC.VERIFY_ALREADY_IN_CHAIN) return prepared.txid;
|
|
4936
|
-
const rejected = error.kind === "rpc" && [
|
|
4937
|
-
RPC.VERIFY_ERROR,
|
|
4938
|
-
RPC.VERIFY_REJECTED,
|
|
4939
|
-
RPC.DESERIALIZATION_ERROR
|
|
4940
|
-
].includes(error.code ?? 0);
|
|
4941
|
-
if (!error.neverRan && !rejected) throw error;
|
|
4942
|
-
const seen = rejected ? await this.known(prepared.txid) : await this.known(prepared.txid).catch(() => void 0);
|
|
4943
|
-
if (seen) {
|
|
4944
|
-
if (["mempool", "confirmed"].includes((await this.status(prepared)).state)) return prepared.txid;
|
|
4945
|
-
throw error;
|
|
4946
|
-
}
|
|
4947
|
-
if (seen === false) await this.unlock(this.outpoints(tx)).catch(() => {});
|
|
4948
|
-
throw new NothingSpentError(`Your node did not send it: ${error.message}`);
|
|
4949
|
-
}
|
|
4950
|
-
if (txid !== prepared.txid) throw new Error("The node broadcast a different transaction than the one reviewed");
|
|
4951
|
-
return prepared.txid;
|
|
4952
|
-
}
|
|
4953
|
-
/** Whether the wallet knows this transaction (it does once its node accepted it, even if it was dropped since). */
|
|
4954
|
-
async known(txid) {
|
|
4955
|
-
try {
|
|
4956
|
-
await this.call("gettransaction", txid);
|
|
4957
|
-
return true;
|
|
4958
|
-
} catch (error) {
|
|
4959
|
-
if (error instanceof BitcoindRpcError && error.kind === "rpc" && error.code === RPC.INVALID_ADDRESS_OR_KEY) return false;
|
|
4960
|
-
throw error;
|
|
4961
|
-
}
|
|
4962
|
-
}
|
|
4963
|
-
async status(prepared) {
|
|
4964
|
-
if (!TXID.test(prepared.txid)) throw new Error("Not a transaction id");
|
|
4965
|
-
let tx;
|
|
4966
|
-
try {
|
|
4967
|
-
tx = object(await this.call("gettransaction", prepared.txid), "transaction");
|
|
4968
|
-
} catch (error) {
|
|
4969
|
-
if (error instanceof BitcoindRpcError && error.kind === "rpc" && error.code === RPC.INVALID_ADDRESS_OR_KEY) return {
|
|
4970
|
-
state: "missing",
|
|
4971
|
-
confirmations: 0
|
|
4972
|
-
};
|
|
4973
|
-
throw error;
|
|
4974
|
-
}
|
|
4975
|
-
const confirmations = typeof tx.confirmations === "number" ? tx.confirmations : 0;
|
|
4976
|
-
if (confirmations > 0) return {
|
|
4977
|
-
state: "confirmed",
|
|
4978
|
-
confirmations
|
|
4979
|
-
};
|
|
4980
|
-
if (confirmations < 0) return {
|
|
4981
|
-
state: "conflicted",
|
|
4982
|
-
confirmations: 0
|
|
4983
|
-
};
|
|
4984
|
-
if (Array.isArray(tx.mempoolconflicts) && tx.mempoolconflicts.length > 0) return {
|
|
4985
|
-
state: "conflicted",
|
|
4986
|
-
confirmations: 0
|
|
4987
|
-
};
|
|
4988
|
-
try {
|
|
4989
|
-
await this.call("getmempoolentry", prepared.txid);
|
|
4990
|
-
return {
|
|
4991
|
-
state: "mempool",
|
|
4992
|
-
confirmations: 0
|
|
4993
|
-
};
|
|
4994
|
-
} catch (error) {
|
|
4995
|
-
if (error instanceof BitcoindRpcError && error.kind === "rpc" && error.code === RPC.INVALID_ADDRESS_OR_KEY) return {
|
|
4996
|
-
state: "missing",
|
|
4997
|
-
confirmations: 0
|
|
4998
|
-
};
|
|
4999
|
-
throw error;
|
|
5000
|
-
}
|
|
5001
|
-
}
|
|
5002
|
-
async release(prepared) {
|
|
5003
|
-
await this.unlock(this.outpoints(this.signedTx(prepared)));
|
|
5004
|
-
}
|
|
5005
|
-
async history(limit) {
|
|
5006
|
-
const count = Math.max(1, Math.min(200, Math.floor(limit) * 3));
|
|
5007
|
-
const entries = await this.call("listtransactions", "*", count, 0, true);
|
|
5008
|
-
if (!Array.isArray(entries)) throw new Error("The node returned no transaction list");
|
|
5009
|
-
const byTxid = /* @__PURE__ */ new Map();
|
|
5010
|
-
for (const raw of entries) {
|
|
5011
|
-
if (!raw || typeof raw.txid !== "string" || !TXID.test(raw.txid) || raw.category === "orphan") continue;
|
|
5012
|
-
const tx = byTxid.get(raw.txid) ?? {
|
|
5013
|
-
txid: raw.txid,
|
|
5014
|
-
amount: 0,
|
|
5015
|
-
confirmations: typeof raw.confirmations === "number" ? Math.max(0, raw.confirmations) : 0
|
|
5016
|
-
};
|
|
5017
|
-
tx.amount += btcToSats(raw.amount);
|
|
5018
|
-
if (raw.category === "send" && typeof raw.fee === "number" && tx.fee === void 0) tx.fee = Math.abs(btcToSats(raw.fee));
|
|
5019
|
-
if (typeof raw.time === "number") tx.timestamp = raw.time * 1e3;
|
|
5020
|
-
byTxid.delete(raw.txid);
|
|
5021
|
-
byTxid.set(raw.txid, tx);
|
|
5022
|
-
}
|
|
5023
|
-
return [...byTxid.values()].reverse().slice(0, Math.max(0, limit));
|
|
5024
|
-
}
|
|
5025
|
-
/** The wallet's receives on this address, newest first: only `receive` entries paying exactly it count. */
|
|
5026
|
-
async received(address) {
|
|
5027
|
-
const entries = await this.call("listtransactions", "*", 200, 0, true);
|
|
5028
|
-
if (!Array.isArray(entries)) throw new Error("The node returned no transaction list");
|
|
5029
|
-
const byTxid = /* @__PURE__ */ new Map();
|
|
5030
|
-
for (const raw of entries) {
|
|
5031
|
-
if (!raw || raw.category !== "receive" || raw.address !== address || typeof raw.txid !== "string" || !TXID.test(raw.txid)) continue;
|
|
5032
|
-
const tx = byTxid.get(raw.txid) ?? {
|
|
5033
|
-
txid: raw.txid,
|
|
5034
|
-
amount: 0,
|
|
5035
|
-
confirmations: typeof raw.confirmations === "number" ? Math.max(0, raw.confirmations) : 0
|
|
5036
|
-
};
|
|
5037
|
-
tx.amount += btcToSats(raw.amount);
|
|
5038
|
-
byTxid.delete(raw.txid);
|
|
5039
|
-
byTxid.set(raw.txid, tx);
|
|
5040
|
-
}
|
|
5041
|
-
return [...byTxid.values()].reverse();
|
|
5042
|
-
}
|
|
5043
|
-
async close() {
|
|
5044
|
-
this.closed = true;
|
|
5045
|
-
}
|
|
5046
|
-
};
|
|
5047
|
-
/** `user:password` in the password field (the .cookie file's contents) when no user is given. */
|
|
5048
|
-
function credentials({ config, secrets }) {
|
|
5049
|
-
const password = secrets.password ?? "";
|
|
5050
|
-
const user = config.user?.trim() ?? "";
|
|
5051
|
-
if (!user && password.includes(":")) {
|
|
5052
|
-
const at = password.indexOf(":");
|
|
5053
|
-
return {
|
|
5054
|
-
user: password.slice(0, at),
|
|
5055
|
-
password: password.slice(at + 1)
|
|
5056
|
-
};
|
|
5057
|
-
}
|
|
5058
|
-
return {
|
|
5059
|
-
user,
|
|
5060
|
-
password
|
|
5061
|
-
};
|
|
5062
|
-
}
|
|
5063
|
-
var bitcoindRpc = {
|
|
5064
|
-
id: "bitcoind",
|
|
5065
|
-
label: "Bitcoin Core",
|
|
5066
|
-
kind: "onchain",
|
|
5067
|
-
description: "A wallet on your own Bitcoin Core node, over its RPC. Your node holds the keys and signs. Needs the desktop app.",
|
|
5068
|
-
networks: [
|
|
5069
|
-
"bitcoin",
|
|
5070
|
-
"testnet",
|
|
5071
|
-
"signet",
|
|
5072
|
-
"regtest"
|
|
5073
|
-
],
|
|
5074
|
-
platforms: ["desktop"],
|
|
5075
|
-
experimental: true,
|
|
5076
|
-
fields: [
|
|
5077
|
-
{
|
|
5078
|
-
name: "url",
|
|
5079
|
-
label: "RPC address",
|
|
5080
|
-
kind: "url",
|
|
5081
|
-
placeholder: "http://127.0.0.1:8332",
|
|
5082
|
-
defaults: {
|
|
5083
|
-
mainnet: "http://127.0.0.1:8332",
|
|
5084
|
-
testnet: "http://127.0.0.1:38332"
|
|
5085
|
-
},
|
|
5086
|
-
help: "Your node's rpcbind and rpcport: 8332 mainnet, 38332 signet, 48332 testnet4, 18443 regtest."
|
|
5087
|
-
},
|
|
5088
|
-
{
|
|
5089
|
-
name: "wallet",
|
|
5090
|
-
label: "Wallet name",
|
|
5091
|
-
kind: "text",
|
|
5092
|
-
optional: true,
|
|
5093
|
-
placeholder: "ghostly",
|
|
5094
|
-
help: "A wallet loaded in the node (bitcoin-cli listwallets). Empty: its default wallet."
|
|
5095
|
-
},
|
|
5096
|
-
{
|
|
5097
|
-
name: "user",
|
|
5098
|
-
label: "RPC user",
|
|
5099
|
-
kind: "text",
|
|
5100
|
-
optional: true,
|
|
5101
|
-
help: "Leave empty to paste the .cookie file below."
|
|
5102
|
-
},
|
|
5103
|
-
{
|
|
5104
|
-
name: "password",
|
|
5105
|
-
label: "RPC password or cookie",
|
|
5106
|
-
kind: "secret",
|
|
5107
|
-
help: "rpcpassword, or the contents of the node's .cookie file (it changes when bitcoind restarts)."
|
|
5108
|
-
}
|
|
5109
|
-
],
|
|
5110
|
-
validate(settings) {
|
|
5111
|
-
const url = (() => {
|
|
5112
|
-
try {
|
|
5113
|
-
return new URL(settings.config.url?.trim() ?? "");
|
|
5114
|
-
} catch {
|
|
5115
|
-
return;
|
|
5116
|
-
}
|
|
5117
|
-
})();
|
|
5118
|
-
if (!url || !["http:", "https:"].includes(url.protocol)) throw new Error("Enter the node's RPC address, like http://127.0.0.1:8332");
|
|
5119
|
-
if (url.username || url.password) throw new Error("Put the RPC user and password in their own fields, not in the address");
|
|
5120
|
-
if (url.search || url.hash) throw new Error("The RPC address cannot have a query or fragment");
|
|
5121
|
-
if (!WALLET_NAME.test(settings.config.wallet?.trim() ?? "") || [".", ".."].includes(settings.config.wallet?.trim() ?? "")) throw new Error("A wallet name has only letters, digits, '-', '_' and '.'");
|
|
5122
|
-
const { user, password } = credentials(settings);
|
|
5123
|
-
if (!user || !password) throw new Error("Enter the RPC user and password, or paste the .cookie file");
|
|
5124
|
-
},
|
|
5125
|
-
async create(settings, host) {
|
|
5126
|
-
return BitcoindOnchain.connect({
|
|
5127
|
-
url: settings.config.url.trim(),
|
|
5128
|
-
wallet: settings.config.wallet?.trim() ?? "",
|
|
5129
|
-
...credentials(settings)
|
|
5130
|
-
}, host.mode, tauriTransport$1(host.invoke), host.signal);
|
|
5131
|
-
}
|
|
5132
|
-
};
|
|
5133
|
-
//#endregion
|
|
5134
4737
|
//#region ../browser/src/engine/paymentAdapters/providers/fedimint.ts
|
|
5135
4738
|
var FEDIMINT_SOURCE = "fedimint";
|
|
5136
4739
|
/** How long an invoice made here stays payable. */
|
|
@@ -5702,15 +5305,15 @@ var CLN_RUNE_COMMAND = `lightning-cli createrune restrictions='[[${CLN_METHODS.m
|
|
|
5702
5305
|
var PAY_TIMEOUT_S$1 = 60;
|
|
5703
5306
|
var CALL_TIMEOUT_MS = 3e4;
|
|
5704
5307
|
var PAY_CALL_TIMEOUT_MS = 9e4;
|
|
5705
|
-
var INVOICE_EXPIRY_S$
|
|
5706
|
-
var NETWORKS$
|
|
5308
|
+
var INVOICE_EXPIRY_S$1 = 3600;
|
|
5309
|
+
var NETWORKS$1 = {
|
|
5707
5310
|
bitcoin: "bitcoin",
|
|
5708
5311
|
testnet: "testnet",
|
|
5709
5312
|
testnet4: "testnet",
|
|
5710
5313
|
signet: "signet",
|
|
5711
5314
|
regtest: "regtest"
|
|
5712
5315
|
};
|
|
5713
|
-
var HEX64$
|
|
5316
|
+
var HEX64$3 = /^[0-9a-f]{64}$/;
|
|
5714
5317
|
var NODE_ID = /^0[23][0-9a-f]{64}$/;
|
|
5715
5318
|
var msatToSat = (msat) => {
|
|
5716
5319
|
const value = typeof msat === "number" ? msat : typeof msat === "string" ? Number(msat.replace(/msat$/, "")) : NaN;
|
|
@@ -5788,7 +5391,7 @@ var CoreLightning = class CoreLightning {
|
|
|
5788
5391
|
}
|
|
5789
5392
|
async info() {
|
|
5790
5393
|
const info = await this.rpc.call("getinfo", {}, { timeoutMs: CALL_TIMEOUT_MS });
|
|
5791
|
-
const network = NETWORKS$
|
|
5394
|
+
const network = NETWORKS$1[info?.network];
|
|
5792
5395
|
if (!network) throw new Error(`The node is on a network Ghostly does not know (${String(info?.network).slice(0, 20)})`);
|
|
5793
5396
|
if (this.nodeId && info.id !== this.nodeId) throw new Error("The node answered with another id");
|
|
5794
5397
|
return {
|
|
@@ -5814,7 +5417,7 @@ var CoreLightning = class CoreLightning {
|
|
|
5814
5417
|
amount_msat: amount * 1e3,
|
|
5815
5418
|
label,
|
|
5816
5419
|
description: (memo || "Ghostly").slice(0, 200),
|
|
5817
|
-
expiry: INVOICE_EXPIRY_S$
|
|
5420
|
+
expiry: INVOICE_EXPIRY_S$1
|
|
5818
5421
|
}, { timeoutMs: CALL_TIMEOUT_MS });
|
|
5819
5422
|
const decoded = typeof created?.bolt11 === "string" && created.bolt11.length < 8e3 ? decodeBolt11(created.bolt11) : null;
|
|
5820
5423
|
if (!decoded || decoded.paymentHash !== created.payment_hash || decoded.amountSat !== amount) throw new Error("The node returned an invoice that does not match the request");
|
|
@@ -5914,7 +5517,7 @@ var CoreLightning = class CoreLightning {
|
|
|
5914
5517
|
}
|
|
5915
5518
|
/** What the node's own record of this payment hash says: `none` when it never tried. */
|
|
5916
5519
|
async lookup(paymentHash) {
|
|
5917
|
-
if (!HEX64$
|
|
5520
|
+
if (!HEX64$3.test(paymentHash)) throw new Error("Invalid payment hash");
|
|
5918
5521
|
const { pays } = await this.rpc.call("listpays", { payment_hash: paymentHash }, { timeoutMs: CALL_TIMEOUT_MS });
|
|
5919
5522
|
const mine = (pays ?? []).filter((p) => p.payment_hash === paymentHash);
|
|
5920
5523
|
const complete = mine.find((p) => p.status === "complete");
|
|
@@ -5987,672 +5590,6 @@ var coreLightning = {
|
|
|
5987
5590
|
return CoreLightning.connect(settingsOf(settings), host.signal);
|
|
5988
5591
|
}
|
|
5989
5592
|
};
|
|
5990
|
-
//#endregion
|
|
5991
|
-
//#region ../browser/src/engine/paymentAdapters/providers/nwc.ts
|
|
5992
|
-
/**
|
|
5993
|
-
* Lightning through a wallet reached over Nostr Wallet Connect (NIP-47): Alby Hub, a node's NWC service, a
|
|
5994
|
-
* hosted wallet. The person pastes the wallet's `nostr+walletconnect://` URI; requests are signed with the
|
|
5995
|
-
* key it carries, encrypted to the wallet (NIP-44, or NIP-04 when its info event offers nothing else) and
|
|
5996
|
-
* sent through its relay; the answers come back the same way. That wallet holds the sats, applies its own
|
|
5997
|
-
* fee limit, and is custodial or not depending on what it is.
|
|
5998
|
-
*
|
|
5999
|
-
* Money safety (PROVIDERS.md): a request that never reached the relay, or an error code the wallet answers
|
|
6000
|
-
* before it tries to pay, is `NothingSpentError`. A `pay_invoice` whose answer does not come back in time,
|
|
6001
|
-
* or any other error, is an unknown outcome, and `paymentStatus` asks `lookup_invoice` until it ends.
|
|
6002
|
-
*/
|
|
6003
|
-
var KIND = {
|
|
6004
|
-
info: 13194,
|
|
6005
|
-
request: 23194,
|
|
6006
|
-
response: 23195
|
|
6007
|
-
};
|
|
6008
|
-
/** A relay message, an encrypted answer, an info event, an invoice: nothing bigger is read. */
|
|
6009
|
-
var MAX_MESSAGE = 262144;
|
|
6010
|
-
var MAX_CONTENT = 65536;
|
|
6011
|
-
var MAX_INFO = 4096;
|
|
6012
|
-
var MAX_INVOICE = 7089;
|
|
6013
|
-
var MAX_URI = 2048;
|
|
6014
|
-
var MAX_RELAYS = 5;
|
|
6015
|
-
var CONNECT_MS = 1e4;
|
|
6016
|
-
var ACK_MS = 1e4;
|
|
6017
|
-
var REQUEST_MS$1 = 2e4;
|
|
6018
|
-
/** A payment may take a while to route; an answer after this is an unknown outcome, looked up later. */
|
|
6019
|
-
var PAY_MS = 9e4;
|
|
6020
|
-
/** The wallet must not start a payment it receives after this (NIP-47 `expiration`). */
|
|
6021
|
-
var PAY_EXPIRY_S = 60;
|
|
6022
|
-
/** A payment the wallet never heard of is failed once its invoice can no longer be paid, with room for clocks. */
|
|
6023
|
-
var EXPIRED_MARGIN_MS = 6e5;
|
|
6024
|
-
var INVOICE_EXPIRY_S$1 = 3600;
|
|
6025
|
-
/** Error codes a wallet answers before it tries to pay: nothing can have left it. (`BAD_REQUEST`: Alby Hub, an invoice it cannot read.) */
|
|
6026
|
-
var REFUSED_BEFORE_PAYING = /* @__PURE__ */ new Set([
|
|
6027
|
-
"RATE_LIMITED",
|
|
6028
|
-
"NOT_IMPLEMENTED",
|
|
6029
|
-
"INSUFFICIENT_BALANCE",
|
|
6030
|
-
"QUOTA_EXCEEDED",
|
|
6031
|
-
"RESTRICTED",
|
|
6032
|
-
"UNAUTHORIZED",
|
|
6033
|
-
"UNSUPPORTED_ENCRYPTION",
|
|
6034
|
-
"BAD_REQUEST"
|
|
6035
|
-
]);
|
|
6036
|
-
var NETWORKS$1 = {
|
|
6037
|
-
mainnet: "bitcoin",
|
|
6038
|
-
bitcoin: "bitcoin",
|
|
6039
|
-
testnet: "testnet",
|
|
6040
|
-
testnet3: "testnet",
|
|
6041
|
-
testnet4: "testnet",
|
|
6042
|
-
signet: "signet",
|
|
6043
|
-
mutinynet: "mutinynet",
|
|
6044
|
-
regtest: "regtest"
|
|
6045
|
-
};
|
|
6046
|
-
var HEX64$3 = /^[0-9a-f]{64}$/;
|
|
6047
|
-
var hex$6 = (bytes) => Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
6048
|
-
var bytes$2 = (text) => Uint8Array.from(text.match(/../g), (b) => parseInt(b, 16));
|
|
6049
|
-
var now$1 = () => Math.floor(Date.now() / 1e3);
|
|
6050
|
-
var isMsat = (value) => Number.isSafeInteger(value) && value >= 0;
|
|
6051
|
-
/** Only secure relays, except on this machine (a local test relay). */
|
|
6052
|
-
function checkRelay(address) {
|
|
6053
|
-
let relay;
|
|
6054
|
-
try {
|
|
6055
|
-
relay = new URL(address);
|
|
6056
|
-
} catch {
|
|
6057
|
-
throw new Error("The connection URI has a relay that is not a URL");
|
|
6058
|
-
}
|
|
6059
|
-
if (relay.username || relay.password || relay.hash) throw new Error("The connection URI has a relay with credentials in it");
|
|
6060
|
-
const loopback = [
|
|
6061
|
-
"127.0.0.1",
|
|
6062
|
-
"localhost",
|
|
6063
|
-
"[::1]"
|
|
6064
|
-
].includes(relay.hostname);
|
|
6065
|
-
if (!(relay.protocol === "wss:" || relay.protocol === "ws:" && loopback)) throw new Error("Use a wallet whose relay is secure (wss://)");
|
|
6066
|
-
return relay.href;
|
|
6067
|
-
}
|
|
6068
|
-
/** Reads a `nostr+walletconnect://` URI. Its errors never repeat it: it holds a secret. */
|
|
6069
|
-
function parseNwcUri(input) {
|
|
6070
|
-
const text = (input ?? "").trim();
|
|
6071
|
-
if (text.length > MAX_URI) throw new Error("That connection URI is too long");
|
|
6072
|
-
const match = /^(?:nostr\+walletconnect|nostrwalletconnect):(?:\/\/)?([0-9a-fA-F]{64})\/?\?([^#]*)$/.exec(text);
|
|
6073
|
-
if (!match) throw new Error("That is not a Nostr Wallet Connect URI (nostr+walletconnect://…)");
|
|
6074
|
-
const params = new URLSearchParams(match[2]);
|
|
6075
|
-
const secret = (params.get("secret") ?? "").toLowerCase();
|
|
6076
|
-
if (!HEX64$3.test(secret) || /^0+$/.test(secret)) throw new Error("The connection URI has no valid secret");
|
|
6077
|
-
const relays = [...new Set(params.getAll("relay").map(checkRelay))];
|
|
6078
|
-
if (!relays.length) throw new Error("The connection URI names no relay");
|
|
6079
|
-
if (relays.length > MAX_RELAYS) throw new Error("The connection URI names too many relays");
|
|
6080
|
-
return {
|
|
6081
|
-
walletPubkey: match[1].toLowerCase(),
|
|
6082
|
-
relays,
|
|
6083
|
-
secret: bytes$2(secret)
|
|
6084
|
-
};
|
|
6085
|
-
}
|
|
6086
|
-
/** The request never reached the relay (not connected, refused by it): the wallet cannot have seen it. */
|
|
6087
|
-
var NotDelivered = class extends Error {
|
|
6088
|
-
constructor(message) {
|
|
6089
|
-
super(message);
|
|
6090
|
-
this.name = "NotDelivered";
|
|
6091
|
-
}
|
|
6092
|
-
};
|
|
6093
|
-
/** An error the wallet answered, with its NIP-47 code. */
|
|
6094
|
-
var NwcError = class extends Error {
|
|
6095
|
-
code;
|
|
6096
|
-
constructor(code, message) {
|
|
6097
|
-
super(message);
|
|
6098
|
-
this.code = code;
|
|
6099
|
-
this.name = "NwcError";
|
|
6100
|
-
}
|
|
6101
|
-
};
|
|
6102
|
-
/** No answer in time. The request may or may not have been acted on. */
|
|
6103
|
-
var NoAnswer = class extends Error {
|
|
6104
|
-
constructor(method) {
|
|
6105
|
-
super(`The wallet did not answer ${method} in time`);
|
|
6106
|
-
this.name = "NoAnswer";
|
|
6107
|
-
}
|
|
6108
|
-
};
|
|
6109
|
-
/**
|
|
6110
|
-
* A minimal NIP-01 client for one relay: it knows exactly whether an event was written to an open socket
|
|
6111
|
-
* and whether the relay acknowledged it, which is what tells "nothing sent" from "unknown" apart.
|
|
6112
|
-
* It reconnects when asked to send again, and subscriptions follow it.
|
|
6113
|
-
*/
|
|
6114
|
-
var RelayConnection = class {
|
|
6115
|
-
url;
|
|
6116
|
-
WebSocketImpl;
|
|
6117
|
-
socket;
|
|
6118
|
-
opening;
|
|
6119
|
-
subs = /* @__PURE__ */ new Map();
|
|
6120
|
-
acks = /* @__PURE__ */ new Map();
|
|
6121
|
-
closed = false;
|
|
6122
|
-
constructor(url, WebSocketImpl = globalThis.WebSocket) {
|
|
6123
|
-
this.url = url;
|
|
6124
|
-
this.WebSocketImpl = WebSocketImpl;
|
|
6125
|
-
}
|
|
6126
|
-
open() {
|
|
6127
|
-
if (this.closed) return Promise.reject(/* @__PURE__ */ new Error("The wallet connection is closed"));
|
|
6128
|
-
if (this.socket?.readyState === 1) return Promise.resolve(this.socket);
|
|
6129
|
-
if (!this.opening) {
|
|
6130
|
-
const opening = this.connectSocket();
|
|
6131
|
-
this.opening = opening;
|
|
6132
|
-
opening.catch(() => {}).finally(() => {
|
|
6133
|
-
if (this.opening === opening) this.opening = void 0;
|
|
6134
|
-
});
|
|
6135
|
-
}
|
|
6136
|
-
return this.opening;
|
|
6137
|
-
}
|
|
6138
|
-
connectSocket() {
|
|
6139
|
-
return new Promise((resolve, reject) => {
|
|
6140
|
-
let settled = false;
|
|
6141
|
-
const socket = new this.WebSocketImpl(this.url);
|
|
6142
|
-
const done = (error) => {
|
|
6143
|
-
if (settled) return;
|
|
6144
|
-
settled = true;
|
|
6145
|
-
clearTimeout(timer);
|
|
6146
|
-
if (error) {
|
|
6147
|
-
try {
|
|
6148
|
-
socket.close();
|
|
6149
|
-
} catch {}
|
|
6150
|
-
reject(error);
|
|
6151
|
-
return;
|
|
6152
|
-
}
|
|
6153
|
-
if (this.closed) {
|
|
6154
|
-
socket.close();
|
|
6155
|
-
reject(/* @__PURE__ */ new Error("The wallet connection is closed"));
|
|
6156
|
-
return;
|
|
6157
|
-
}
|
|
6158
|
-
this.socket = socket;
|
|
6159
|
-
for (const [id, sub] of this.subs) this.write([
|
|
6160
|
-
"REQ",
|
|
6161
|
-
id,
|
|
6162
|
-
sub.filter
|
|
6163
|
-
]);
|
|
6164
|
-
resolve(socket);
|
|
6165
|
-
};
|
|
6166
|
-
const timer = setTimeout(() => done(/* @__PURE__ */ new Error("The wallet's relay did not answer")), CONNECT_MS);
|
|
6167
|
-
socket.onopen = () => done();
|
|
6168
|
-
socket.onerror = () => done(/* @__PURE__ */ new Error("Could not reach the wallet's relay"));
|
|
6169
|
-
socket.onclose = () => {
|
|
6170
|
-
done(/* @__PURE__ */ new Error("The wallet's relay closed the connection"));
|
|
6171
|
-
if (this.socket !== socket) return;
|
|
6172
|
-
this.socket = void 0;
|
|
6173
|
-
for (const [id, ack] of this.acks) {
|
|
6174
|
-
this.acks.delete(id);
|
|
6175
|
-
ack.fail(/* @__PURE__ */ new Error("The relay connection dropped before it confirmed the request"));
|
|
6176
|
-
}
|
|
6177
|
-
};
|
|
6178
|
-
socket.onmessage = (message) => {
|
|
6179
|
-
if (this.socket === socket) this.receive(message.data);
|
|
6180
|
-
};
|
|
6181
|
-
});
|
|
6182
|
-
}
|
|
6183
|
-
subscribe(id, filter, sub) {
|
|
6184
|
-
this.subs.set(id, {
|
|
6185
|
-
...sub,
|
|
6186
|
-
filter
|
|
6187
|
-
});
|
|
6188
|
-
this.write([
|
|
6189
|
-
"REQ",
|
|
6190
|
-
id,
|
|
6191
|
-
filter
|
|
6192
|
-
]);
|
|
6193
|
-
}
|
|
6194
|
-
unsubscribe(id) {
|
|
6195
|
-
if (this.subs.delete(id)) this.write(["CLOSE", id]);
|
|
6196
|
-
}
|
|
6197
|
-
/**
|
|
6198
|
-
* Events matching `filter` the relay has stored, until it says that is all (or `ms` pass). With
|
|
6199
|
-
* `waitForOne`, a relay that has none yet is listened to until one arrives live, within `ms`.
|
|
6200
|
-
*/
|
|
6201
|
-
async fetch(filter, { ms = CONNECT_MS, waitForOne = false } = {}) {
|
|
6202
|
-
await this.open();
|
|
6203
|
-
const id = `q${hex$6(crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(6)))}`, events = [];
|
|
6204
|
-
try {
|
|
6205
|
-
await new Promise((resolve) => {
|
|
6206
|
-
const timer = setTimeout(resolve, ms);
|
|
6207
|
-
let stored = false;
|
|
6208
|
-
const finish = () => {
|
|
6209
|
-
clearTimeout(timer);
|
|
6210
|
-
resolve();
|
|
6211
|
-
};
|
|
6212
|
-
this.subscribe(id, filter, {
|
|
6213
|
-
event: (e) => {
|
|
6214
|
-
if (events.length < 20) events.push(e);
|
|
6215
|
-
if (stored) finish();
|
|
6216
|
-
},
|
|
6217
|
-
eose: () => {
|
|
6218
|
-
stored = true;
|
|
6219
|
-
if (events.length || !waitForOne) finish();
|
|
6220
|
-
}
|
|
6221
|
-
});
|
|
6222
|
-
});
|
|
6223
|
-
} finally {
|
|
6224
|
-
this.unsubscribe(id);
|
|
6225
|
-
}
|
|
6226
|
-
return events;
|
|
6227
|
-
}
|
|
6228
|
-
/**
|
|
6229
|
-
* Resolves once the relay accepted the event. `NotDelivered` when it certainly did not reach it (no open
|
|
6230
|
-
* socket, refused); any other rejection means it may have.
|
|
6231
|
-
*/
|
|
6232
|
-
async publish(event) {
|
|
6233
|
-
let socket;
|
|
6234
|
-
try {
|
|
6235
|
-
socket = await this.open();
|
|
6236
|
-
} catch (error) {
|
|
6237
|
-
throw new NotDelivered(error instanceof Error ? error.message : "Could not reach the wallet's relay");
|
|
6238
|
-
}
|
|
6239
|
-
return new Promise((resolve, reject) => {
|
|
6240
|
-
const timer = setTimeout(() => {
|
|
6241
|
-
this.acks.delete(event.id);
|
|
6242
|
-
reject(/* @__PURE__ */ new Error("The relay did not confirm the request"));
|
|
6243
|
-
}, ACK_MS);
|
|
6244
|
-
this.acks.set(event.id, {
|
|
6245
|
-
ok: () => {
|
|
6246
|
-
clearTimeout(timer);
|
|
6247
|
-
resolve();
|
|
6248
|
-
},
|
|
6249
|
-
fail: (error) => {
|
|
6250
|
-
clearTimeout(timer);
|
|
6251
|
-
reject(error);
|
|
6252
|
-
}
|
|
6253
|
-
});
|
|
6254
|
-
if (socket.readyState !== 1 || socket !== this.socket) {
|
|
6255
|
-
this.acks.delete(event.id);
|
|
6256
|
-
clearTimeout(timer);
|
|
6257
|
-
reject(new NotDelivered("The relay connection closed"));
|
|
6258
|
-
return;
|
|
6259
|
-
}
|
|
6260
|
-
socket.send(JSON.stringify(["EVENT", event]));
|
|
6261
|
-
});
|
|
6262
|
-
}
|
|
6263
|
-
close() {
|
|
6264
|
-
this.closed = true;
|
|
6265
|
-
for (const [id, ack] of this.acks) {
|
|
6266
|
-
this.acks.delete(id);
|
|
6267
|
-
ack.fail(/* @__PURE__ */ new Error("The wallet connection is closed"));
|
|
6268
|
-
}
|
|
6269
|
-
this.subs.clear();
|
|
6270
|
-
try {
|
|
6271
|
-
this.socket?.close();
|
|
6272
|
-
} catch {}
|
|
6273
|
-
this.socket = void 0;
|
|
6274
|
-
}
|
|
6275
|
-
write(message) {
|
|
6276
|
-
if (this.socket?.readyState === 1) this.socket.send(JSON.stringify(message));
|
|
6277
|
-
}
|
|
6278
|
-
receive(data) {
|
|
6279
|
-
if (typeof data !== "string" || data.length > MAX_MESSAGE) return;
|
|
6280
|
-
let message;
|
|
6281
|
-
try {
|
|
6282
|
-
message = JSON.parse(data);
|
|
6283
|
-
} catch {
|
|
6284
|
-
return;
|
|
6285
|
-
}
|
|
6286
|
-
if (!Array.isArray(message) || typeof message[1] !== "string") return;
|
|
6287
|
-
const [type, id] = message;
|
|
6288
|
-
if (type === "EVENT") this.subs.get(id)?.event(message[2]);
|
|
6289
|
-
else if (type === "EOSE") this.subs.get(id)?.eose?.();
|
|
6290
|
-
else if (type === "OK") {
|
|
6291
|
-
const ack = this.acks.get(id);
|
|
6292
|
-
if (!ack) return;
|
|
6293
|
-
this.acks.delete(id);
|
|
6294
|
-
const reason = typeof message[3] === "string" ? message[3].slice(0, 120) : "";
|
|
6295
|
-
if (message[2] === true || reason.startsWith("duplicate:")) ack.ok();
|
|
6296
|
-
else ack.fail(new NotDelivered(`The relay refused the request${reason ? ` (${reason})` : ""}`));
|
|
6297
|
-
}
|
|
6298
|
-
}
|
|
6299
|
-
};
|
|
6300
|
-
var NwcLightning = class NwcLightning {
|
|
6301
|
-
connection;
|
|
6302
|
-
relay;
|
|
6303
|
-
encryption;
|
|
6304
|
-
options;
|
|
6305
|
-
/** From the info event, then narrowed to what this connection may do (`get_info`). */
|
|
6306
|
-
capabilities;
|
|
6307
|
-
clientPubkey;
|
|
6308
|
-
subscription = `nwc${hex$6(crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(6)))}`;
|
|
6309
|
-
pending = /* @__PURE__ */ new Map();
|
|
6310
|
-
conversationKey;
|
|
6311
|
-
network = "bitcoin";
|
|
6312
|
-
alias;
|
|
6313
|
-
closed = false;
|
|
6314
|
-
constructor(connection, relay, encryption, methods, options) {
|
|
6315
|
-
this.connection = connection;
|
|
6316
|
-
this.relay = relay;
|
|
6317
|
-
this.encryption = encryption;
|
|
6318
|
-
this.options = options;
|
|
6319
|
-
this.clientPubkey = getPublicKey(connection.secret);
|
|
6320
|
-
if (encryption === "nip44_v2") this.conversationKey = nip44.v2.utils.getConversationKey(connection.secret, connection.walletPubkey);
|
|
6321
|
-
this.capabilities = this.capabilitiesOf(methods);
|
|
6322
|
-
}
|
|
6323
|
-
/**
|
|
6324
|
-
* Reaches the wallet through the first relay that answers, reads its info event (the methods and the
|
|
6325
|
-
* encryption it offers), listens for its answers, and asks `get_info` for its network.
|
|
6326
|
-
*/
|
|
6327
|
-
static async connect(uri, options = {}) {
|
|
6328
|
-
const connection = parseNwcUri(uri);
|
|
6329
|
-
options.signal?.throwIfAborted();
|
|
6330
|
-
let relay, lastError;
|
|
6331
|
-
for (const url of connection.relays) {
|
|
6332
|
-
const candidate = new RelayConnection(url, options.WebSocket);
|
|
6333
|
-
try {
|
|
6334
|
-
await candidate.open();
|
|
6335
|
-
relay = candidate;
|
|
6336
|
-
break;
|
|
6337
|
-
} catch (error) {
|
|
6338
|
-
candidate.close();
|
|
6339
|
-
lastError = error;
|
|
6340
|
-
}
|
|
6341
|
-
}
|
|
6342
|
-
if (!relay) throw lastError instanceof Error ? lastError : /* @__PURE__ */ new Error("Could not reach the wallet's relay");
|
|
6343
|
-
let wallet;
|
|
6344
|
-
try {
|
|
6345
|
-
const info = NwcLightning.newestInfo(await relay.fetch({
|
|
6346
|
-
kinds: [KIND.info],
|
|
6347
|
-
authors: [connection.walletPubkey],
|
|
6348
|
-
limit: 5
|
|
6349
|
-
}, {
|
|
6350
|
-
waitForOne: true,
|
|
6351
|
-
ms: options.infoMs
|
|
6352
|
-
}), connection.walletPubkey);
|
|
6353
|
-
if (!info) throw new Error(`The wallet has not published its NWC info on ${new URL(relay.url).host}: check the URI, or that the wallet is running`);
|
|
6354
|
-
const offered = (info.tags.find((t) => t[0] === "encryption")?.slice(1).join(" ") ?? "").split(/\s+/).filter(Boolean);
|
|
6355
|
-
const encryption = offered.includes("nip44_v2") ? "nip44_v2" : !offered.length || offered.includes("nip04") ? "nip04" : void 0;
|
|
6356
|
-
if (!encryption) throw new Error("The wallet offers no encryption Ghostly speaks (NIP-44 or NIP-04)");
|
|
6357
|
-
const methods = new Set(info.content.split(/\s+/).filter(Boolean));
|
|
6358
|
-
wallet = new NwcLightning(connection, relay, encryption, methods, options);
|
|
6359
|
-
wallet.listen();
|
|
6360
|
-
if (options.signal) {
|
|
6361
|
-
const opened = wallet;
|
|
6362
|
-
if (options.signal.aborted) throw options.signal.reason;
|
|
6363
|
-
options.signal.addEventListener("abort", () => void opened.close(), { once: true });
|
|
6364
|
-
}
|
|
6365
|
-
await wallet.readInfo(methods);
|
|
6366
|
-
return wallet;
|
|
6367
|
-
} catch (error) {
|
|
6368
|
-
if (wallet) await wallet.close();
|
|
6369
|
-
else relay.close();
|
|
6370
|
-
throw error;
|
|
6371
|
-
}
|
|
6372
|
-
}
|
|
6373
|
-
static newestInfo(events, walletPubkey) {
|
|
6374
|
-
return events.filter((e) => e && e.kind === KIND.info && e.pubkey === walletPubkey && typeof e.content === "string" && e.content.length <= MAX_INFO && Array.isArray(e.tags) && verifyEvent(e)).sort((a, b) => b.created_at - a.created_at)[0];
|
|
6375
|
-
}
|
|
6376
|
-
capabilitiesOf(methods) {
|
|
6377
|
-
const lookup = methods.has("lookup_invoice");
|
|
6378
|
-
return {
|
|
6379
|
-
receive: methods.has("make_invoice"),
|
|
6380
|
-
send: methods.has("pay_invoice") && lookup,
|
|
6381
|
-
balance: methods.has("get_balance"),
|
|
6382
|
-
lookup
|
|
6383
|
-
};
|
|
6384
|
-
}
|
|
6385
|
-
/**
|
|
6386
|
-
* `get_info` says the network and the methods this connection may use (the info event lists the
|
|
6387
|
-
* service's). Without it the wallet is taken to be on Bitcoin: a test wallet is then refused in the
|
|
6388
|
-
* Testnet mode and its invoices in the Mainnet one, while a real-money wallet never passes for a test one.
|
|
6389
|
-
*/
|
|
6390
|
-
async readInfo(offered) {
|
|
6391
|
-
if (!offered.has("get_info")) return;
|
|
6392
|
-
let info;
|
|
6393
|
-
try {
|
|
6394
|
-
info = await this.request("get_info", {});
|
|
6395
|
-
} catch (error) {
|
|
6396
|
-
if (error instanceof NwcError && [
|
|
6397
|
-
"RESTRICTED",
|
|
6398
|
-
"UNAUTHORIZED",
|
|
6399
|
-
"NOT_IMPLEMENTED"
|
|
6400
|
-
].includes(error.code)) return;
|
|
6401
|
-
throw error;
|
|
6402
|
-
}
|
|
6403
|
-
if (info.network !== void 0) {
|
|
6404
|
-
const network = typeof info.network === "string" ? NETWORKS$1[info.network.toLowerCase()] : void 0;
|
|
6405
|
-
if (!network) throw new Error("The wallet is on a network Ghostly does not know");
|
|
6406
|
-
this.network = network;
|
|
6407
|
-
}
|
|
6408
|
-
if (typeof info.alias === "string" && info.alias.trim()) this.alias = info.alias.trim().slice(0, 40);
|
|
6409
|
-
if (Array.isArray(info.methods) && info.methods.every((m) => typeof m === "string")) this.capabilities = this.capabilitiesOf(new Set(info.methods));
|
|
6410
|
-
}
|
|
6411
|
-
listen() {
|
|
6412
|
-
this.relay.subscribe(this.subscription, {
|
|
6413
|
-
kinds: [KIND.response],
|
|
6414
|
-
authors: [this.connection.walletPubkey],
|
|
6415
|
-
"#p": [this.clientPubkey],
|
|
6416
|
-
since: now$1() - 60
|
|
6417
|
-
}, { event: (e) => this.answer(e) });
|
|
6418
|
-
}
|
|
6419
|
-
encrypt(text) {
|
|
6420
|
-
return this.conversationKey ? nip44.v2.encrypt(text, this.conversationKey) : nip04.encrypt(this.connection.secret, this.connection.walletPubkey, text);
|
|
6421
|
-
}
|
|
6422
|
-
decrypt(content) {
|
|
6423
|
-
if (this.conversationKey && !content.includes("?iv=")) return nip44.v2.decrypt(content, this.conversationKey);
|
|
6424
|
-
return nip04.decrypt(this.connection.secret, this.connection.walletPubkey, content);
|
|
6425
|
-
}
|
|
6426
|
-
/**
|
|
6427
|
-
* Sends one request and waits for its answer. `NotDelivered` when it never reached the relay; `NwcError`
|
|
6428
|
-
* for an error the wallet answered; `NoAnswer` (or a relay error) when the outcome is unknown.
|
|
6429
|
-
*/
|
|
6430
|
-
async request(method, params, { ms = this.options.requestMs ?? REQUEST_MS$1, expiresIn } = {}) {
|
|
6431
|
-
if (this.closed) throw new NotDelivered("The wallet connection is closed");
|
|
6432
|
-
const tags = [["p", this.connection.walletPubkey]];
|
|
6433
|
-
if (this.conversationKey) tags.push(["encryption", "nip44_v2"]);
|
|
6434
|
-
if (expiresIn) tags.push(["expiration", String(now$1() + expiresIn)]);
|
|
6435
|
-
const event = finalizeEvent({
|
|
6436
|
-
kind: KIND.request,
|
|
6437
|
-
created_at: now$1(),
|
|
6438
|
-
tags,
|
|
6439
|
-
content: this.encrypt(JSON.stringify({
|
|
6440
|
-
method,
|
|
6441
|
-
params
|
|
6442
|
-
}))
|
|
6443
|
-
}, this.connection.secret);
|
|
6444
|
-
const answer = new Promise((resolve, reject) => {
|
|
6445
|
-
const timer = setTimeout(() => {
|
|
6446
|
-
this.pending.delete(event.id);
|
|
6447
|
-
reject(new NoAnswer(method));
|
|
6448
|
-
}, ms);
|
|
6449
|
-
this.pending.set(event.id, {
|
|
6450
|
-
method,
|
|
6451
|
-
resolve,
|
|
6452
|
-
reject,
|
|
6453
|
-
timer
|
|
6454
|
-
});
|
|
6455
|
-
});
|
|
6456
|
-
const delivered = this.relay.publish(event).then(() => void 0, (error) => {
|
|
6457
|
-
const waiting = this.pending.get(event.id);
|
|
6458
|
-
if (!(error instanceof NotDelivered) || !waiting) return void 0;
|
|
6459
|
-
clearTimeout(waiting.timer);
|
|
6460
|
-
this.pending.delete(event.id);
|
|
6461
|
-
return error;
|
|
6462
|
-
});
|
|
6463
|
-
return Promise.race([answer, delivered.then((error) => error ? Promise.reject(error) : answer)]);
|
|
6464
|
-
}
|
|
6465
|
-
/** Reads an answer: from the wallet, signed, about a request of ours, of the method asked. Anything else is dropped. */
|
|
6466
|
-
answer(raw) {
|
|
6467
|
-
const event = raw;
|
|
6468
|
-
if (!event || typeof event !== "object" || event.kind !== KIND.response || event.pubkey !== this.connection.walletPubkey) return;
|
|
6469
|
-
if (typeof event.content !== "string" || event.content.length > MAX_CONTENT || !Array.isArray(event.tags)) return;
|
|
6470
|
-
const ref = event.tags.find((t) => Array.isArray(t) && t[0] === "e")?.[1];
|
|
6471
|
-
const pending = typeof ref === "string" ? this.pending.get(ref) : void 0;
|
|
6472
|
-
if (!pending || !event.tags.some((t) => Array.isArray(t) && t[0] === "p" && t[1] === this.clientPubkey)) return;
|
|
6473
|
-
if (!verifyEvent(event)) return;
|
|
6474
|
-
let body;
|
|
6475
|
-
try {
|
|
6476
|
-
body = JSON.parse(this.decrypt(event.content));
|
|
6477
|
-
} catch {
|
|
6478
|
-
return;
|
|
6479
|
-
}
|
|
6480
|
-
if (!body || typeof body !== "object" || body.result_type !== pending.method) return;
|
|
6481
|
-
clearTimeout(pending.timer);
|
|
6482
|
-
this.pending.delete(ref);
|
|
6483
|
-
if (body.error) {
|
|
6484
|
-
const code = typeof body.error.code === "string" ? body.error.code.slice(0, 40) : "OTHER";
|
|
6485
|
-
const message = typeof body.error.message === "string" && body.error.message.trim() ? body.error.message.trim().slice(0, 200) : code;
|
|
6486
|
-
pending.reject(new NwcError(code, message));
|
|
6487
|
-
} else if (body.result && typeof body.result === "object" && !Array.isArray(body.result)) pending.resolve(body.result);
|
|
6488
|
-
else pending.reject(new NwcError("OTHER", "The wallet sent an empty answer"));
|
|
6489
|
-
}
|
|
6490
|
-
async lookup(paymentHash) {
|
|
6491
|
-
const tx = await this.request("lookup_invoice", { payment_hash: paymentHash });
|
|
6492
|
-
if (tx.payment_hash !== void 0 && String(tx.payment_hash).toLowerCase() !== paymentHash) throw new Error("The wallet answered about another invoice");
|
|
6493
|
-
return tx;
|
|
6494
|
-
}
|
|
6495
|
-
async info() {
|
|
6496
|
-
let balance;
|
|
6497
|
-
if (this.capabilities.balance) {
|
|
6498
|
-
const result = await this.request("get_balance", {});
|
|
6499
|
-
if (!isMsat(result.balance)) throw new Error("The wallet returned an invalid balance");
|
|
6500
|
-
balance = Math.floor(result.balance / 1e3);
|
|
6501
|
-
}
|
|
6502
|
-
return {
|
|
6503
|
-
network: this.network,
|
|
6504
|
-
alias: `${this.alias ?? "NWC wallet"} via ${new URL(this.relay.url).host}`,
|
|
6505
|
-
balance
|
|
6506
|
-
};
|
|
6507
|
-
}
|
|
6508
|
-
async createInvoice(amount, memo) {
|
|
6509
|
-
if (!Number.isSafeInteger(amount) || amount <= 0 || !Number.isSafeInteger(amount * 1e3)) throw new Error("Invalid amount");
|
|
6510
|
-
const result = await this.request("make_invoice", {
|
|
6511
|
-
amount: amount * 1e3,
|
|
6512
|
-
...memo ? { description: memo.slice(0, 200) } : {},
|
|
6513
|
-
expiry: INVOICE_EXPIRY_S$1
|
|
6514
|
-
});
|
|
6515
|
-
const text = typeof result.invoice === "string" && result.invoice.length <= MAX_INVOICE ? result.invoice : "";
|
|
6516
|
-
const decoded = decodeBolt11(text);
|
|
6517
|
-
if (!decoded?.paymentHash || decoded.amountMsat !== BigInt(amount) * 1000n) throw new Error("The wallet returned an invoice that does not match the request");
|
|
6518
|
-
if (result.payment_hash !== void 0 && String(result.payment_hash).toLowerCase() !== decoded.paymentHash) throw new Error("The wallet returned an invoice that does not match its payment hash");
|
|
6519
|
-
return {
|
|
6520
|
-
invoice: decoded.invoice,
|
|
6521
|
-
paymentHash: decoded.paymentHash,
|
|
6522
|
-
amount,
|
|
6523
|
-
expiresAt: decoded.expiresAt * 1e3
|
|
6524
|
-
};
|
|
6525
|
-
}
|
|
6526
|
-
async invoiceStatus(invoice) {
|
|
6527
|
-
let tx;
|
|
6528
|
-
try {
|
|
6529
|
-
tx = await this.lookup(invoice.paymentHash);
|
|
6530
|
-
} catch (error) {
|
|
6531
|
-
if (error instanceof NwcError && error.code === "NOT_FOUND" && invoice.expiresAt < Date.now()) return { state: "expired" };
|
|
6532
|
-
throw error;
|
|
6533
|
-
}
|
|
6534
|
-
const expired = invoice.expiresAt < Date.now();
|
|
6535
|
-
if (tx.type === "outgoing") return { state: expired ? "expired" : "open" };
|
|
6536
|
-
const state = tx.state ?? (tx.settled_at ? "settled" : "pending");
|
|
6537
|
-
if (state === "settled") return {
|
|
6538
|
-
state: "paid",
|
|
6539
|
-
amount: isMsat(tx.amount) ? Math.floor(tx.amount / 1e3) : invoice.amount
|
|
6540
|
-
};
|
|
6541
|
-
if (state === "expired" || state === "failed") return { state: "expired" };
|
|
6542
|
-
return { state: expired ? "expired" : "open" };
|
|
6543
|
-
}
|
|
6544
|
-
/**
|
|
6545
|
-
* Pays through the wallet. NIP-47 carries no fee limit: the wallet applies its own, and the fee it
|
|
6546
|
-
* reports is passed on as it is.
|
|
6547
|
-
*/
|
|
6548
|
-
async payInvoice(invoice, _maxFee) {
|
|
6549
|
-
const decoded = decodeBolt11(invoice);
|
|
6550
|
-
if (!decoded?.paymentHash || decoded.amountSat === null) throw new NothingSpentError("That invoice cannot be paid through NWC");
|
|
6551
|
-
if (!this.capabilities.send) throw new NothingSpentError("This wallet connection may not pay and look up invoices");
|
|
6552
|
-
let result;
|
|
6553
|
-
try {
|
|
6554
|
-
result = await this.request("pay_invoice", { invoice: decoded.invoice }, {
|
|
6555
|
-
ms: this.options.payMs ?? PAY_MS,
|
|
6556
|
-
expiresIn: PAY_EXPIRY_S
|
|
6557
|
-
});
|
|
6558
|
-
} catch (error) {
|
|
6559
|
-
if (error instanceof NotDelivered) throw new NothingSpentError(`The payment did not reach the wallet: ${error.message}`);
|
|
6560
|
-
if (error instanceof NwcError && REFUSED_BEFORE_PAYING.has(error.code)) throw new NothingSpentError(`The wallet refused the payment: ${error.message}`);
|
|
6561
|
-
if (error instanceof NwcError) return this.confirmFailure(decoded.invoice, decoded.paymentHash, error);
|
|
6562
|
-
throw error;
|
|
6563
|
-
}
|
|
6564
|
-
const preimage = typeof result.preimage === "string" ? result.preimage.toLowerCase() : "";
|
|
6565
|
-
if (!HEX64$3.test(preimage) || hex$6(sha256(bytes$2(preimage))) !== decoded.paymentHash) throw new Error("The wallet said it paid, without a proof that matches the invoice");
|
|
6566
|
-
return {
|
|
6567
|
-
state: "paid",
|
|
6568
|
-
preimage,
|
|
6569
|
-
fee: isMsat(result.fees_paid) ? Math.ceil(result.fees_paid / 1e3) : void 0
|
|
6570
|
-
};
|
|
6571
|
-
}
|
|
6572
|
-
/**
|
|
6573
|
-
* An error after the wallet may have tried (`PAYMENT_FAILED` "may be due to a timeout", `INTERNAL`,
|
|
6574
|
-
* `OTHER`…): its own record decides. Only a payment it records as failed is `NothingSpentError`; one it
|
|
6575
|
-
* records as paid is paid; anything else is pending, or unknown when it cannot be asked.
|
|
6576
|
-
*/
|
|
6577
|
-
async confirmFailure(invoice, paymentHash, failure) {
|
|
6578
|
-
let status;
|
|
6579
|
-
try {
|
|
6580
|
-
status = await this.paymentStatus({
|
|
6581
|
-
invoice,
|
|
6582
|
-
paymentHash
|
|
6583
|
-
});
|
|
6584
|
-
} catch {
|
|
6585
|
-
throw new Error(`The wallet said the payment failed (${failure.message}), and cannot confirm it yet`);
|
|
6586
|
-
}
|
|
6587
|
-
if (status.state === "failed") throw new NothingSpentError(`The payment failed: ${failure.message}`);
|
|
6588
|
-
return status.state === "paid" ? {
|
|
6589
|
-
state: "paid",
|
|
6590
|
-
fee: status.fee,
|
|
6591
|
-
preimage: status.preimage
|
|
6592
|
-
} : { state: "pending" };
|
|
6593
|
-
}
|
|
6594
|
-
async paymentStatus(payment) {
|
|
6595
|
-
let tx;
|
|
6596
|
-
try {
|
|
6597
|
-
tx = await this.lookup(payment.paymentHash);
|
|
6598
|
-
} catch (error) {
|
|
6599
|
-
if (!(error instanceof NwcError && error.code === "NOT_FOUND")) throw error;
|
|
6600
|
-
return { state: (decodeBolt11(payment.invoice)?.expiresAt ?? Infinity) * 1e3 + EXPIRED_MARGIN_MS < Date.now() ? "failed" : "pending" };
|
|
6601
|
-
}
|
|
6602
|
-
if (tx.type === "incoming") return { state: "pending" };
|
|
6603
|
-
const preimage = typeof tx.preimage === "string" && HEX64$3.test(tx.preimage.toLowerCase()) ? tx.preimage.toLowerCase() : void 0;
|
|
6604
|
-
const proven = !!preimage && hex$6(sha256(bytes$2(preimage))) === payment.paymentHash;
|
|
6605
|
-
const state = tx.state ?? (tx.settled_at || proven ? "settled" : "pending");
|
|
6606
|
-
const fee = isMsat(tx.fees_paid) ? Math.ceil(tx.fees_paid / 1e3) : void 0;
|
|
6607
|
-
if (state === "settled") {
|
|
6608
|
-
if (preimage && !proven) throw new Error("The wallet's proof of payment does not match the invoice");
|
|
6609
|
-
return {
|
|
6610
|
-
state: "paid",
|
|
6611
|
-
fee,
|
|
6612
|
-
preimage
|
|
6613
|
-
};
|
|
6614
|
-
}
|
|
6615
|
-
if (state === "failed" || state === "expired") return { state: "failed" };
|
|
6616
|
-
return { state: "pending" };
|
|
6617
|
-
}
|
|
6618
|
-
async close() {
|
|
6619
|
-
if (this.closed) return;
|
|
6620
|
-
this.closed = true;
|
|
6621
|
-
for (const [id, pending] of this.pending) {
|
|
6622
|
-
clearTimeout(pending.timer);
|
|
6623
|
-
this.pending.delete(id);
|
|
6624
|
-
pending.reject(/* @__PURE__ */ new Error("The wallet connection is closed"));
|
|
6625
|
-
}
|
|
6626
|
-
this.relay.close();
|
|
6627
|
-
}
|
|
6628
|
-
};
|
|
6629
|
-
var nwc = {
|
|
6630
|
-
id: "nwc",
|
|
6631
|
-
label: "Nostr Wallet Connect",
|
|
6632
|
-
kind: "lightning",
|
|
6633
|
-
description: "A wallet you connect with its NWC URI (Alby Hub, a node's NWC service, a hosted wallet). That wallet holds the sats and sets its own fee limit: custodial or not depends on the wallet.",
|
|
6634
|
-
networks: [
|
|
6635
|
-
"bitcoin",
|
|
6636
|
-
"testnet",
|
|
6637
|
-
"signet",
|
|
6638
|
-
"mutinynet",
|
|
6639
|
-
"regtest"
|
|
6640
|
-
],
|
|
6641
|
-
platforms: PROVIDER_PLATFORMS,
|
|
6642
|
-
fields: [{
|
|
6643
|
-
name: "uri",
|
|
6644
|
-
label: "Connection URI",
|
|
6645
|
-
kind: "secret",
|
|
6646
|
-
placeholder: "nostr+walletconnect://…",
|
|
6647
|
-
help: "From your wallet's app connections. Allow making, paying and looking up invoices, and reading the balance."
|
|
6648
|
-
}],
|
|
6649
|
-
validate({ secrets }) {
|
|
6650
|
-
parseNwcUri(secrets.uri ?? "");
|
|
6651
|
-
},
|
|
6652
|
-
async create({ secrets }, host) {
|
|
6653
|
-
return NwcLightning.connect(secrets.uri, { signal: host.signal });
|
|
6654
|
-
}
|
|
6655
|
-
};
|
|
6656
5593
|
/** What the recommended macaroon grants: `lncli bakemacaroon info:read invoices:read invoices:write offchain:read offchain:write`. */
|
|
6657
5594
|
var LND_SCOPE = [
|
|
6658
5595
|
"info:read",
|
|
@@ -7767,6 +6704,28 @@ function defaultRegistry() {
|
|
|
7767
6704
|
};
|
|
7768
6705
|
}
|
|
7769
6706
|
//#endregion
|
|
6707
|
+
//#region ../browser/src/proofs/load.ts
|
|
6708
|
+
/**
|
|
6709
|
+
* The identity proof providers, loaded on first need and not with the app: they and their libraries (OpenPGP, the
|
|
6710
|
+
* Bitcoin signer, the DID, AT Protocol and OpenID Connect checks) are a large part of the code, and the first screen
|
|
6711
|
+
* draws none of it. The engine loads them with the profile's proofs; the UI once it draws something about identities.
|
|
6712
|
+
*/
|
|
6713
|
+
var registry;
|
|
6714
|
+
var loading$1;
|
|
6715
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
6716
|
+
function loadIdentityProviders() {
|
|
6717
|
+
return loading$1 ??= import("./registry-OBJBnHVQ.js").then((m) => {
|
|
6718
|
+
registry = m;
|
|
6719
|
+
for (const listener of listeners) listener();
|
|
6720
|
+
return m;
|
|
6721
|
+
}, (error) => {
|
|
6722
|
+
loading$1 = void 0;
|
|
6723
|
+
throw error;
|
|
6724
|
+
});
|
|
6725
|
+
}
|
|
6726
|
+
/** The providers (built in, from plugins, the test ones when asked for), once loaded; none before. */
|
|
6727
|
+
var loadedIdentityProviders = () => registry?.identityProviders() ?? [];
|
|
6728
|
+
//#endregion
|
|
7770
6729
|
//#region ../browser/src/engine/identities.ts
|
|
7771
6730
|
var KEY$2 = "identityProofs";
|
|
7772
6731
|
var REVOCATIONS = "identityRevocations";
|
|
@@ -7807,14 +6766,17 @@ var IdentityProofs = class {
|
|
|
7807
6766
|
/** Chats whose current connection already got the hello: "ready" is reported more than once per connection. */
|
|
7808
6767
|
greeted = /* @__PURE__ */ new Set();
|
|
7809
6768
|
fetch;
|
|
7810
|
-
constructor(host, providers =
|
|
6769
|
+
constructor(host, providers = loadedIdentityProviders) {
|
|
7811
6770
|
this.host = host;
|
|
7812
6771
|
this.providers = providers;
|
|
7813
6772
|
this.fetch = boundedIdentityFetch({ online: host.online });
|
|
7814
6773
|
}
|
|
6774
|
+
/** The profile's proofs, and the providers that check and show them: loaded here, not with the app. */
|
|
7815
6775
|
async load() {
|
|
6776
|
+
const providers = loadIdentityProviders();
|
|
7816
6777
|
const settings = await store(STORES$1.settings, "readonly");
|
|
7817
6778
|
[this.proofs, this.revocations] = await Promise.all([wrap(settings.get(KEY$2)).then((v) => v ?? []), wrap(settings.get(REVOCATIONS)).then((v) => v ?? [])]);
|
|
6779
|
+
await providers;
|
|
7818
6780
|
}
|
|
7819
6781
|
/** Background work: republish revocations, look up the contacts' proofs for revocations. */
|
|
7820
6782
|
start() {
|
|
@@ -11154,18 +10116,39 @@ var DEFAULT_APP_STORES = [{
|
|
|
11154
10116
|
url: "https://raw.githubusercontent.com/MiguelMedeiros/ghostly-store/HEAD/ghostly-store.json",
|
|
11155
10117
|
key: "y379ia3t1urwudj8o4w1qwmuwp1mcxyf956b5r7pqup7stce6diy"
|
|
11156
10118
|
}];
|
|
10119
|
+
/**
|
|
10120
|
+
* The apps that come with Ghostly. A short list kept here on purpose, never "everything in the official store": an
|
|
10121
|
+
* entry is added with the pin of its bundle in apps/ui/src/assets/apps/built-in.json, which the build reads from the
|
|
10122
|
+
* app's own repository (apps/ui/scripts/built-in-apps.mjs); no bundle is kept in this repository.
|
|
10123
|
+
*
|
|
10124
|
+
* Chess: Ghostly's own publisher key for it (fingerprint odcg w6wj w8dy nqop), version 2.3.0 (`sequence` 4), at the
|
|
10125
|
+
* commit of its repository that the official store lists.
|
|
10126
|
+
*/
|
|
10127
|
+
var BUILT_IN_APPS = [{
|
|
10128
|
+
publisher: "odcgw6wjw8dynqop84r47jbjcdqossbgrfiejd367e14hgxmjcho",
|
|
10129
|
+
name: "chess",
|
|
10130
|
+
digest: "EEnFmP0grTkTe_9qgapQsEEZzWB9FcpoimWkHnfecVs",
|
|
10131
|
+
url: "https://cdn.jsdelivr.net/gh/MiguelMedeiros/ghostly-chess@c665ab194478145fd9b9a4777e6c0d1893969649/app.ghostlyapp",
|
|
10132
|
+
title: "Chess",
|
|
10133
|
+
tagline: "Play chess with a contact, live in your chat"
|
|
10134
|
+
}];
|
|
11157
10135
|
var APP_STORAGE_LIMITS = {
|
|
11158
10136
|
/** A key, in UTF-8 bytes. */
|
|
11159
10137
|
keyBytes: 256,
|
|
11160
10138
|
/** A value, as compact JSON in UTF-8 bytes. */
|
|
11161
10139
|
valueBytes: 65536,
|
|
11162
10140
|
/**
|
|
11163
|
-
* Counted for each key beside its own bytes and its value's: what the row costs on the device (the app's reference
|
|
11164
|
-
* the scope
|
|
11165
|
-
* held 20 times its 5 MiB.
|
|
10141
|
+
* Counted for each key beside its own bytes and its value's: what the row costs on the device (the app's reference and
|
|
10142
|
+
* the scope, in the row's own key and in its two indexes). Without it a scope of one-byte values held 20 times its 5 MiB.
|
|
11166
10143
|
*/
|
|
11167
10144
|
rowBytes: 128,
|
|
11168
|
-
/**
|
|
10145
|
+
/**
|
|
10146
|
+
* How many times a key's bytes count. The device keeps a key four times (the row's own key, its value and both
|
|
10147
|
+
* indexes), three of them as UTF-16, about 10 bytes for each byte of the key: counted once, a scope of 256-byte keys
|
|
10148
|
+
* took 40 to 55 MB for its 5 MiB.
|
|
10149
|
+
*/
|
|
10150
|
+
keyTimes: 8,
|
|
10151
|
+
/** Every key and value of one app in one scope, with `rowBytes` for each key and its bytes `keyTimes` over. */
|
|
11169
10152
|
scopeBytes: 5242880
|
|
11170
10153
|
};
|
|
11171
10154
|
var APP_CHECK_TIMINGS = {
|
|
@@ -11178,22 +10161,74 @@ var APP_CHECK_TIMINGS = {
|
|
|
11178
10161
|
/** Revocations are checked this long at a time before the page's thread is let go. */
|
|
11179
10162
|
var REVOCATIONS_SLICE_MS = 10;
|
|
11180
10163
|
/**
|
|
10164
|
+
* `ghostly-revoke.json` files whose result is kept, so an unchanged one is not checked again (the oldest dropped first):
|
|
10165
|
+
* at most this many, and of the ones whose list is held at most one file's limit of bytes in all. A publisher's file is
|
|
10166
|
+
* small; nine large ones beside one app's URLs, or one that changes at every check, would stay for the engine's life.
|
|
10167
|
+
*/
|
|
10168
|
+
var REVOCATION_FILES_KEPT = {
|
|
10169
|
+
max: 64,
|
|
10170
|
+
bytes: APP_FETCH_LIMITS.revocationsBytes
|
|
10171
|
+
};
|
|
10172
|
+
/**
|
|
11181
10173
|
* Revocations of store indexes whose signature was checked, kept by `appRevocationId` so an index that changes around
|
|
11182
10174
|
* them does not have each checked again (the one read longest ago dropped first): four indexes' worth, about 1 MiB.
|
|
11183
10175
|
*/
|
|
11184
10176
|
var STORE_REVOCATIONS_KEPT = 4 * APP_STORE_LIMITS.revoked;
|
|
11185
|
-
/** `ghostly-revoke.json` files whose result is kept, so an unchanged one is not checked again (the oldest dropped first). */
|
|
11186
|
-
var REVOCATION_FILES_KEPT = 64;
|
|
11187
10177
|
/** A fetched bundle kept for its install screen: at most this many, for this long. */
|
|
11188
10178
|
var STAGED = {
|
|
11189
10179
|
max: 4,
|
|
11190
10180
|
ms: 6e5
|
|
11191
10181
|
};
|
|
10182
|
+
/**
|
|
10183
|
+
* Checked bundles kept in memory: the most recently used `min`, and beyond them each one a running app used (`entry`,
|
|
10184
|
+
* `file`, `fileAt`) within `ms`. The engine is not told when an app closes; one not used for that long goes, and is read
|
|
10185
|
+
* and checked again if it still runs.
|
|
10186
|
+
*/
|
|
10187
|
+
var BUNDLES_KEPT = {
|
|
10188
|
+
min: 2,
|
|
10189
|
+
ms: 6e4
|
|
10190
|
+
};
|
|
11192
10191
|
/** The first bytes of a bundle: magic, the manifest's length and the largest manifest. */
|
|
11193
10192
|
var PEEK_BYTES = APP_BUNDLE_MAGIC.length + 4 + APP_BUNDLE_LIMITS.manifestBytes;
|
|
11194
10193
|
var bundleId = (digest) => `app-${digest}`;
|
|
11195
|
-
/**
|
|
11196
|
-
|
|
10194
|
+
/**
|
|
10195
|
+
* The key of a scope's counter row (`AppScopeCount` as JSON in `value`, `size` 0): an app's key is never empty
|
|
10196
|
+
* (`keyOf`), and the app's own rows of a scope are `keysOf`, which leaves it out.
|
|
10197
|
+
*/
|
|
10198
|
+
var COUNT_KEY = "";
|
|
10199
|
+
var NO_ROWS = {
|
|
10200
|
+
rows: 0,
|
|
10201
|
+
keyBytes: 0,
|
|
10202
|
+
size: 0
|
|
10203
|
+
};
|
|
10204
|
+
/**
|
|
10205
|
+
* What rows take of their scope's 5 MiB: `rowBytes` each, their keys `keyTimes` over and their values. Not kept, so a
|
|
10206
|
+
* count from before counts alike.
|
|
10207
|
+
*/
|
|
10208
|
+
var costOf = ({ rows, keyBytes, size }) => rows * APP_STORAGE_LIMITS.rowBytes + size + (APP_STORAGE_LIMITS.keyTimes - 1) * keyBytes;
|
|
10209
|
+
/** One row as a count, `sign` -1 to take it away. */
|
|
10210
|
+
var countOf = ({ key, size }, sign = 1) => ({
|
|
10211
|
+
rows: sign,
|
|
10212
|
+
keyBytes: sign * byteLength(key),
|
|
10213
|
+
size: sign * size
|
|
10214
|
+
});
|
|
10215
|
+
var plus = (a, b) => ({
|
|
10216
|
+
rows: a.rows + b.rows,
|
|
10217
|
+
keyBytes: a.keyBytes + b.keyBytes,
|
|
10218
|
+
size: a.size + b.size
|
|
10219
|
+
});
|
|
10220
|
+
function readCount(value) {
|
|
10221
|
+
try {
|
|
10222
|
+
const c = JSON.parse(value);
|
|
10223
|
+
return [
|
|
10224
|
+
c.rows,
|
|
10225
|
+
c.keyBytes,
|
|
10226
|
+
c.size
|
|
10227
|
+
].every((n) => Number.isSafeInteger(n) && n >= 0) ? c : null;
|
|
10228
|
+
} catch {
|
|
10229
|
+
return null;
|
|
10230
|
+
}
|
|
10231
|
+
}
|
|
11197
10232
|
/**
|
|
11198
10233
|
* A listing's URLs this client reads, in order, a jsDelivr copy first: one this client reads names a full commit, so it
|
|
11199
10234
|
* holds exactly the version the store reviewed, where a raw `HEAD` may have moved on to one it has not (WISP 1200 §
|
|
@@ -11209,6 +10244,46 @@ function fail(code, words) {
|
|
|
11209
10244
|
throw new Error(`${code}: ${words}`);
|
|
11210
10245
|
}
|
|
11211
10246
|
/**
|
|
10247
|
+
* The key, in `STORES.settings`, of what the person did about the apps that come with Ghostly (one of
|
|
10248
|
+
* `PROFILE_SETTINGS_KEYS`: "Clear all data" drops it, and the profile is offered them as a new one is).
|
|
10249
|
+
*/
|
|
10250
|
+
var BUILT_IN_APPS_KEY = "appBuiltIns";
|
|
10251
|
+
/** The marks as kept, read with nothing taken on trust: a record that is not one is no mark. */
|
|
10252
|
+
function builtInMarks(kept) {
|
|
10253
|
+
const removed = {};
|
|
10254
|
+
const held = kept?.removed;
|
|
10255
|
+
if (held && typeof held === "object") {
|
|
10256
|
+
for (const [ref, at] of Object.entries(held)) if (isAppRef(ref) && typeof at === "number") removed[ref] = at;
|
|
10257
|
+
}
|
|
10258
|
+
return { removed };
|
|
10259
|
+
}
|
|
10260
|
+
/** Queues, on the settings store of an open transaction, a change of the built-in apps' marks. */
|
|
10261
|
+
function markBuiltIns(settings, change) {
|
|
10262
|
+
const read = settings.get(BUILT_IN_APPS_KEY);
|
|
10263
|
+
read.onsuccess = () => {
|
|
10264
|
+
const marks = builtInMarks(read.result);
|
|
10265
|
+
change(marks);
|
|
10266
|
+
settings.put(marks, BUILT_IN_APPS_KEY);
|
|
10267
|
+
};
|
|
10268
|
+
}
|
|
10269
|
+
var builtInRef = (app) => appRef(app.publisher, app.name);
|
|
10270
|
+
/**
|
|
10271
|
+
* The entries of the built-in list this profile is offered (WISP 1200 § Built-in apps): each reference once, not
|
|
10272
|
+
* installed (`installed`, by reference) and not removed by the person. An entry that is not a whole one (its key, its
|
|
10273
|
+
* name, its digest, a URL off the list of `appFetch.ts`, no title or tagline) is skipped, as a build with no bundle
|
|
10274
|
+
* for it would be.
|
|
10275
|
+
*/
|
|
10276
|
+
function builtInsDue(list, installed, marks) {
|
|
10277
|
+
const due = /* @__PURE__ */ new Map();
|
|
10278
|
+
for (const app of list) {
|
|
10279
|
+
if (!isAppKey(app.publisher) || !isAppHash(app.digest) || !isAppFetchUrl(app.url) || !app.title || !app.tagline) continue;
|
|
10280
|
+
const ref = builtInRef(app);
|
|
10281
|
+
if (!isAppRef(ref) || due.has(ref) || installed.has(ref) || marks.removed[ref] !== void 0) continue;
|
|
10282
|
+
due.set(ref, app);
|
|
10283
|
+
}
|
|
10284
|
+
return [...due.values()];
|
|
10285
|
+
}
|
|
10286
|
+
/**
|
|
11212
10287
|
* Why a pasted link is not read: a github.com page that is neither a repository nor a file of one (`github-link`), so
|
|
11213
10288
|
* the person is not told GitHub is off the list; any other host (`host`).
|
|
11214
10289
|
*/
|
|
@@ -11231,8 +10306,26 @@ function withRevocations(app, read) {
|
|
|
11231
10306
|
revocations: [...merged.values()]
|
|
11232
10307
|
} : app;
|
|
11233
10308
|
}
|
|
11234
|
-
/** Rows of one app (`[ref]`), or of one app in one scope (`[ref, scope]`), in `STORES.appStorage
|
|
10309
|
+
/** Rows of one app (`[ref]`), or of one app in one scope (`[ref, scope]`), in `STORES.appStorage`, counter rows included. */
|
|
11235
10310
|
var rowsOf = (...prefix) => IDBKeyRange.bound(prefix, [...prefix, []]);
|
|
10311
|
+
/** The app's own rows of one scope: its counter row left out. */
|
|
10312
|
+
var keysOf = (ref, scope) => IDBKeyRange.bound([
|
|
10313
|
+
ref,
|
|
10314
|
+
scope,
|
|
10315
|
+
COUNT_KEY
|
|
10316
|
+
], [
|
|
10317
|
+
ref,
|
|
10318
|
+
scope,
|
|
10319
|
+
[]
|
|
10320
|
+
], true);
|
|
10321
|
+
/** A scope's counter row (`COUNT_KEY`). */
|
|
10322
|
+
var counterRow = (ref, scope, count) => ({
|
|
10323
|
+
ref,
|
|
10324
|
+
scope,
|
|
10325
|
+
key: COUNT_KEY,
|
|
10326
|
+
value: JSON.stringify(count),
|
|
10327
|
+
size: 0
|
|
10328
|
+
});
|
|
11236
10329
|
function done(tx) {
|
|
11237
10330
|
return new Promise((resolve, reject) => {
|
|
11238
10331
|
tx.oncomplete = () => resolve();
|
|
@@ -11265,8 +10358,10 @@ function peekAppManifest(bytes) {
|
|
|
11265
10358
|
var Apps = class {
|
|
11266
10359
|
host;
|
|
11267
10360
|
staged = /* @__PURE__ */ new Map();
|
|
11268
|
-
/** Bundles read back and checked, by digest, least recently used first: the ones running are read once, not on every `file`. */
|
|
10361
|
+
/** Bundles read back and checked, by digest, least recently used first (`BUNDLES_KEPT`): the ones running are read once, not on every `file`. */
|
|
11269
10362
|
verified = /* @__PURE__ */ new Map();
|
|
10363
|
+
/** When a running app last used its bundle, by digest (`BUNDLES_KEPT`): an icon drawn is not a use. */
|
|
10364
|
+
runUsed = /* @__PURE__ */ new Map();
|
|
11270
10365
|
/** Reads of a bundle under way, by digest: icons drawn at once share one read. */
|
|
11271
10366
|
verifying = /* @__PURE__ */ new Map();
|
|
11272
10367
|
/**
|
|
@@ -11274,7 +10369,7 @@ var Apps = class {
|
|
|
11274
10369
|
* page, in a chat's cards and in the composer's Apps is read without the whole bundle again.
|
|
11275
10370
|
*/
|
|
11276
10371
|
icons = /* @__PURE__ */ new Map();
|
|
11277
|
-
/**
|
|
10372
|
+
/** What each `ref\0scope` holds, read once from its counter row and kept in step by the writes here. */
|
|
11278
10373
|
usage = /* @__PURE__ */ new Map();
|
|
11279
10374
|
queues = /* @__PURE__ */ new Map();
|
|
11280
10375
|
timer = null;
|
|
@@ -11298,7 +10393,7 @@ var Apps = class {
|
|
|
11298
10393
|
* index listing the same is not peeked again at every check.
|
|
11299
10394
|
*/
|
|
11300
10395
|
listingMisses = /* @__PURE__ */ new Map();
|
|
11301
|
-
/** `ghostly-revoke.json` files checked, by the SHA-256 of their bytes: their revocations, or null when refused. */
|
|
10396
|
+
/** `ghostly-revoke.json` files checked, by the SHA-256 of their bytes: their revocations, or null when refused, and the bytes the held list counts for. */
|
|
11302
10397
|
revocationFiles = /* @__PURE__ */ new Map();
|
|
11303
10398
|
/** Revocations read in a store's index and verified, by `appRevocationId`: at most `STORE_REVOCATIONS_KEPT`. */
|
|
11304
10399
|
storeRevocations = /* @__PURE__ */ new Set();
|
|
@@ -11391,6 +10486,59 @@ var Apps = class {
|
|
|
11391
10486
|
if (!isAppRef(ref)) fail("bad-ref", "Not an app reference");
|
|
11392
10487
|
return await this.app(ref) ?? fail("not-installed", "This app is not installed");
|
|
11393
10488
|
}
|
|
10489
|
+
/** The built-in list's entry of this reference, if it has one. */
|
|
10490
|
+
builtIn(ref) {
|
|
10491
|
+
return (this.host.builtIns ?? BUILT_IN_APPS).find((app) => builtInRef(app) === ref);
|
|
10492
|
+
}
|
|
10493
|
+
async builtInMarks() {
|
|
10494
|
+
return builtInMarks(await wrap((await store(STORES$1.settings, "readonly")).get(BUILT_IN_APPS_KEY)));
|
|
10495
|
+
}
|
|
10496
|
+
/**
|
|
10497
|
+
* The built-in apps this profile is offered (WISP 1200 § Built-in apps): shown with the installed ones, with nothing
|
|
10498
|
+
* stored and nothing granted. Opening one is an install like any other, from the bundle the build carries.
|
|
10499
|
+
*/
|
|
10500
|
+
async builtIns() {
|
|
10501
|
+
const list = this.host.builtIns ?? BUILT_IN_APPS;
|
|
10502
|
+
if (!list.length || !this.host.readBuiltIn) return [];
|
|
10503
|
+
const [installed, marks] = await Promise.all([this.installed(), this.builtInMarks()]);
|
|
10504
|
+
return builtInsDue(list, new Set(installed.map((a) => a.ref)), marks).map((app) => ({
|
|
10505
|
+
ref: builtInRef(app),
|
|
10506
|
+
publisher: app.publisher,
|
|
10507
|
+
name: app.name,
|
|
10508
|
+
fingerprint: appFingerprint(app.publisher),
|
|
10509
|
+
digest: app.digest,
|
|
10510
|
+
url: app.url,
|
|
10511
|
+
title: app.title,
|
|
10512
|
+
tagline: app.tagline
|
|
10513
|
+
}));
|
|
10514
|
+
}
|
|
10515
|
+
/**
|
|
10516
|
+
* The bundle the build carries for a built-in app, checked as a fetched one is, and against its entry: the digest the
|
|
10517
|
+
* list pins and the entry's reference. Anything else in that file is not this app, and nothing of it is kept.
|
|
10518
|
+
*/
|
|
10519
|
+
async bundled(app) {
|
|
10520
|
+
if (!this.host.readBuiltIn) fail("not-carried", "This build does not carry this app");
|
|
10521
|
+
let bytes;
|
|
10522
|
+
try {
|
|
10523
|
+
bytes = await this.host.readBuiltIn(app);
|
|
10524
|
+
} catch {
|
|
10525
|
+
fail("not-carried", "This build does not carry this app");
|
|
10526
|
+
}
|
|
10527
|
+
if (!(bytes instanceof Uint8Array) || bytes.length > APP_BUNDLE_LIMITS.bundleBytes) fail("not-carried", "This build does not carry this app");
|
|
10528
|
+
const read = await readAppBundleAsync(bytes);
|
|
10529
|
+
if (!read.ok) fail(read.reason, `This is not an app Ghostly can install${read.detail ? ` (${read.detail})` : ""}`);
|
|
10530
|
+
if (read.bundle.digest !== app.digest || versionOf(read.bundle.manifest, read.bundle.digest).ref !== builtInRef(app)) fail("unlisted", "The app this build carries is not the one it names");
|
|
10531
|
+
return {
|
|
10532
|
+
bundle: read.bundle,
|
|
10533
|
+
bytes
|
|
10534
|
+
};
|
|
10535
|
+
}
|
|
10536
|
+
/** The built-in entry of this reference while the profile is offered it (`builtIns`), else undefined. */
|
|
10537
|
+
async offered(ref) {
|
|
10538
|
+
const app = this.builtIn(ref);
|
|
10539
|
+
if (!app || !this.host.readBuiltIn) return void 0;
|
|
10540
|
+
return builtInsDue([app], await this.app(ref) ? /* @__PURE__ */ new Set([ref]) : /* @__PURE__ */ new Set(), await this.builtInMarks())[0];
|
|
10541
|
+
}
|
|
11394
10542
|
/**
|
|
11395
10543
|
* An installed app that may still run (WISP 1200 § Takedowns): a running app whose version its publisher revoked, or
|
|
11396
10544
|
* a store of the person's removed (unless this run was started with Run anyway), is cut off with `stopped`, whether
|
|
@@ -11543,7 +10691,7 @@ var Apps = class {
|
|
|
11543
10691
|
*/
|
|
11544
10692
|
async writeBundle(digest, bytes) {
|
|
11545
10693
|
const id = bundleId(digest);
|
|
11546
|
-
const expected = toBase64Url(
|
|
10694
|
+
const expected = toBase64Url(await sha256Paced(bytes));
|
|
11547
10695
|
let files;
|
|
11548
10696
|
let stored;
|
|
11549
10697
|
try {
|
|
@@ -11588,6 +10736,7 @@ var Apps = class {
|
|
|
11588
10736
|
}
|
|
11589
10737
|
async removeBundle(kind, digest) {
|
|
11590
10738
|
this.verified.delete(digest);
|
|
10739
|
+
this.runUsed.delete(digest);
|
|
11591
10740
|
this.icons.delete(digest);
|
|
11592
10741
|
await (await fileBytesOf(kind))?.remove(bundleId(digest)).catch(() => {});
|
|
11593
10742
|
}
|
|
@@ -11597,11 +10746,13 @@ var Apps = class {
|
|
|
11597
10746
|
if (app?.digest !== digest && app?.pending?.digest !== digest) await this.removeBundle(kind, digest);
|
|
11598
10747
|
}
|
|
11599
10748
|
/** The installed bundle read back and checked again before it runs (WISP 1200 § Signatures: "again before it runs one"). */
|
|
11600
|
-
verifiedBundle(app) {
|
|
10749
|
+
verifiedBundle(app, running) {
|
|
10750
|
+
if (running) this.runUsed.set(app.digest, this.now());
|
|
11601
10751
|
const kept = this.verified.get(app.digest);
|
|
11602
10752
|
if (kept) {
|
|
11603
10753
|
this.verified.delete(app.digest);
|
|
11604
10754
|
this.verified.set(app.digest, kept);
|
|
10755
|
+
this.dropUnusedBundles();
|
|
11605
10756
|
return Promise.resolve(kept);
|
|
11606
10757
|
}
|
|
11607
10758
|
const under = this.verifying.get(app.digest);
|
|
@@ -11609,11 +10760,10 @@ var Apps = class {
|
|
|
11609
10760
|
const reading = (async () => {
|
|
11610
10761
|
const bytes = await this.readBundleBytes(app.bytes, app.digest);
|
|
11611
10762
|
if (!bytes) fail("needs-files", "This app's files are not on this device yet");
|
|
11612
|
-
const read =
|
|
10763
|
+
const read = await readAppBundleAsync(bytes);
|
|
11613
10764
|
if (!read.ok || read.bundle.digest !== app.digest) fail("damaged", "This app's stored files do not match what was installed");
|
|
11614
10765
|
this.verified.set(app.digest, read.bundle);
|
|
11615
|
-
|
|
11616
|
-
while (this.verified.size > room) this.verified.delete(this.verified.keys().next().value);
|
|
10766
|
+
this.dropUnusedBundles();
|
|
11617
10767
|
this.icons.set(app.digest, read.bundle.files.get("icon.png")?.slice() ?? null);
|
|
11618
10768
|
return read.bundle;
|
|
11619
10769
|
})().finally(() => {
|
|
@@ -11622,9 +10772,21 @@ var Apps = class {
|
|
|
11622
10772
|
this.verifying.set(app.digest, reading);
|
|
11623
10773
|
return reading;
|
|
11624
10774
|
}
|
|
10775
|
+
/**
|
|
10776
|
+
* Apps running at once do not push each other's bundle out, which every `file` read would then read and check again
|
|
10777
|
+
* whole; one closed (or only drawn as an icon) goes once two newer ones are kept, at the next bundle read or hit.
|
|
10778
|
+
*/
|
|
10779
|
+
dropUnusedBundles() {
|
|
10780
|
+
for (const digest of this.verified.keys()) {
|
|
10781
|
+
if (this.verified.size <= BUNDLES_KEPT.min) break;
|
|
10782
|
+
if (this.now() - (this.runUsed.get(digest) ?? -Infinity) < BUNDLES_KEPT.ms) continue;
|
|
10783
|
+
this.verified.delete(digest);
|
|
10784
|
+
this.runUsed.delete(digest);
|
|
10785
|
+
}
|
|
10786
|
+
}
|
|
11625
10787
|
async fetchBundle(url) {
|
|
11626
10788
|
const bytes = await this.host.fetch(url, { maxBytes: APP_BUNDLE_LIMITS.bundleBytes });
|
|
11627
|
-
const read =
|
|
10789
|
+
const read = await readAppBundleAsync(bytes);
|
|
11628
10790
|
if (!read.ok) fail(read.reason, `This is not an app Ghostly can install${read.detail ? ` (${read.detail})` : ""}`);
|
|
11629
10791
|
return {
|
|
11630
10792
|
bundle: read.bundle,
|
|
@@ -11640,7 +10802,7 @@ var Apps = class {
|
|
|
11640
10802
|
*/
|
|
11641
10803
|
async readStoreAt(url, heldKey, held) {
|
|
11642
10804
|
const [indexBytes, sigBytes] = await Promise.all([this.host.fetch(url, { maxBytes: APP_FETCH_LIMITS.storeIndexBytes }), this.host.fetch(besideUrl(url, "ghostly-store.sig"), { maxBytes: APP_FETCH_LIMITS.sigBytes })]);
|
|
11643
|
-
if (held?.index && held.digest !== void 0 && toBase64Url(
|
|
10805
|
+
if (held?.index && held.digest !== void 0 && toBase64Url(await sha256Paced(indexBytes)) === held.digest) return {
|
|
11644
10806
|
ok: true,
|
|
11645
10807
|
store: {
|
|
11646
10808
|
index: held.index,
|
|
@@ -11649,14 +10811,11 @@ var Apps = class {
|
|
|
11649
10811
|
}
|
|
11650
10812
|
};
|
|
11651
10813
|
const steps = readAppStoreSteps(indexBytes, sigBytes, this.nowS(), heldKey, this.checkedRevocations);
|
|
11652
|
-
|
|
10814
|
+
const pause = pacer(REVOCATIONS_SLICE_MS);
|
|
11653
10815
|
for (;;) {
|
|
11654
10816
|
const step = steps.next();
|
|
11655
10817
|
if (step.done) return step.value;
|
|
11656
|
-
|
|
11657
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
11658
|
-
since = performance.now();
|
|
11659
|
-
}
|
|
10818
|
+
await pause();
|
|
11660
10819
|
}
|
|
11661
10820
|
}
|
|
11662
10821
|
/**
|
|
@@ -11680,19 +10839,16 @@ var Apps = class {
|
|
|
11680
10839
|
* engine shares the page's thread, and each entry is a signature check. The same bytes are not checked again.
|
|
11681
10840
|
*/
|
|
11682
10841
|
async readRevocationsFile(bytes) {
|
|
11683
|
-
const id = toBase64Url(
|
|
10842
|
+
const id = toBase64Url(await sha256Paced(bytes));
|
|
11684
10843
|
const known = this.revocationFiles.get(id);
|
|
11685
|
-
if (known !== void 0) return known;
|
|
10844
|
+
if (known !== void 0) return known.revocations;
|
|
11686
10845
|
const read = readCanonicalJson(bytes);
|
|
11687
10846
|
let out = null;
|
|
11688
10847
|
if (read.ok && Array.isArray(read.value) && read.value.length <= APP_STATEMENT_LIMITS.revocations) {
|
|
11689
10848
|
out = [];
|
|
11690
|
-
|
|
10849
|
+
const pause = pacer(REVOCATIONS_SLICE_MS);
|
|
11691
10850
|
for (const entry of read.value) {
|
|
11692
|
-
|
|
11693
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
11694
|
-
since = performance.now();
|
|
11695
|
-
}
|
|
10851
|
+
await pause();
|
|
11696
10852
|
const one = readAppRevocation(entry);
|
|
11697
10853
|
if (!one.ok) {
|
|
11698
10854
|
out = null;
|
|
@@ -11702,8 +10858,17 @@ var Apps = class {
|
|
|
11702
10858
|
}
|
|
11703
10859
|
}
|
|
11704
10860
|
this.revocationFiles.delete(id);
|
|
11705
|
-
this.revocationFiles.set(id,
|
|
11706
|
-
|
|
10861
|
+
this.revocationFiles.set(id, {
|
|
10862
|
+
revocations: out,
|
|
10863
|
+
bytes: out ? bytes.length : 0
|
|
10864
|
+
});
|
|
10865
|
+
let held = 0;
|
|
10866
|
+
for (const file of this.revocationFiles.values()) held += file.bytes;
|
|
10867
|
+
for (const [old, file] of this.revocationFiles) {
|
|
10868
|
+
if (this.revocationFiles.size <= REVOCATION_FILES_KEPT.max && held <= REVOCATION_FILES_KEPT.bytes) break;
|
|
10869
|
+
this.revocationFiles.delete(old);
|
|
10870
|
+
held -= file.bytes;
|
|
10871
|
+
}
|
|
11707
10872
|
return out;
|
|
11708
10873
|
}
|
|
11709
10874
|
summary(s) {
|
|
@@ -11881,6 +11046,12 @@ var Apps = class {
|
|
|
11881
11046
|
let urls;
|
|
11882
11047
|
let expect = null;
|
|
11883
11048
|
let fromStore;
|
|
11049
|
+
if ("builtIn" in source) {
|
|
11050
|
+
if (!isAppRef(source.builtIn)) fail("bad-ref", "Not an app reference");
|
|
11051
|
+
const app = await this.offered(source.builtIn) ?? fail("not-offered", "This app does not come with Ghostly here");
|
|
11052
|
+
const { bundle, bytes } = await this.bundled(app);
|
|
11053
|
+
return this.stage(bundle, bytes, app.url, void 0, true);
|
|
11054
|
+
}
|
|
11884
11055
|
if ("url" in source) urls = [typeof source.url === "string" && appPasteUrl(source.url, "app.ghostlyapp") || notReadable(source.url, "Apps are read only from raw.githubusercontent.com, or from cdn.jsdelivr.net at a commit, over HTTPS")];
|
|
11885
11056
|
else if ("store" in source) {
|
|
11886
11057
|
const s = await this.storeRecord(source.store);
|
|
@@ -11898,6 +11069,11 @@ var Apps = class {
|
|
|
11898
11069
|
const { ref, url, sequence, digest } = source.card;
|
|
11899
11070
|
if (!isAppRef(ref)) fail("bad-ref", "Not an app reference");
|
|
11900
11071
|
if (!isAppFetchUrl(url)) fail("host", "A card's app is read only from raw.githubusercontent.com, or from cdn.jsdelivr.net at a commit");
|
|
11072
|
+
const carried = typeof digest === "string" ? await this.offered(ref) : void 0;
|
|
11073
|
+
if (carried && carried.digest === digest) {
|
|
11074
|
+
const held = await this.bundled(carried).catch(() => null);
|
|
11075
|
+
if (held) return this.stage(held.bundle, held.bytes, carried.url, void 0, true);
|
|
11076
|
+
}
|
|
11901
11077
|
urls = [url];
|
|
11902
11078
|
expect = {
|
|
11903
11079
|
ref,
|
|
@@ -11929,7 +11105,7 @@ var Apps = class {
|
|
|
11929
11105
|
const oldest = this.staged.values().next().value;
|
|
11930
11106
|
if (oldest) this.sweep = setTimeout(() => this.sweepStaged(), STAGED.ms - (this.now() - oldest.at));
|
|
11931
11107
|
}
|
|
11932
|
-
async stage(bundle, bytes, from, fromStore) {
|
|
11108
|
+
async stage(bundle, bytes, from, fromStore, carried = false) {
|
|
11933
11109
|
for (const [digest, s] of this.staged) if (this.now() - s.at > STAGED.ms) this.staged.delete(digest);
|
|
11934
11110
|
this.staged.delete(bundle.digest);
|
|
11935
11111
|
while (this.staged.size >= STAGED.max) this.staged.delete(this.staged.keys().next().value);
|
|
@@ -11960,7 +11136,8 @@ var Apps = class {
|
|
|
11960
11136
|
asks,
|
|
11961
11137
|
run: this.runStatus(version, this.storeViews(stores), installed?.revocations),
|
|
11962
11138
|
listedBy,
|
|
11963
|
-
unknownPublisher: !listedBy.some((s) => s.kind === "curated")
|
|
11139
|
+
unknownPublisher: !listedBy.some((s) => s.kind === "curated"),
|
|
11140
|
+
...carried && { carried: true }
|
|
11964
11141
|
};
|
|
11965
11142
|
}
|
|
11966
11143
|
/**
|
|
@@ -12014,6 +11191,7 @@ var Apps = class {
|
|
|
12014
11191
|
const kind = await this.writeBundle(digest, bytes);
|
|
12015
11192
|
const next = this.versionRecord(installed, bundle, kind, from, fromStore);
|
|
12016
11193
|
await this.putApp(next);
|
|
11194
|
+
if (!installed && this.builtIn(version.ref)) await this.keepBuiltIn(version.ref);
|
|
12017
11195
|
if (installed && installed.digest !== digest) await this.removeBundle(installed.bytes, installed.digest);
|
|
12018
11196
|
if (installed?.pending && next.pending?.digest !== installed.pending.digest && installed.pending.digest !== digest) await this.removeBundle(installed.pending.bytes, installed.pending.digest);
|
|
12019
11197
|
this.staged.delete(digest);
|
|
@@ -12052,7 +11230,7 @@ var Apps = class {
|
|
|
12052
11230
|
const installed = await this.installedOrFail(ref);
|
|
12053
11231
|
const pending = installed.pending ?? fail("no-update", "No update waits for this app");
|
|
12054
11232
|
const bytes = await this.readBundleBytes(pending.bytes, pending.digest);
|
|
12055
|
-
const read = bytes ?
|
|
11233
|
+
const read = bytes ? await readAppBundleAsync(bytes) : null;
|
|
12056
11234
|
if (!read?.ok || read.bundle.digest !== pending.digest) {
|
|
12057
11235
|
const { pending: _p, ...rest } = installed;
|
|
12058
11236
|
await this.putApp(rest);
|
|
@@ -12287,6 +11465,26 @@ var Apps = class {
|
|
|
12287
11465
|
const app = await this.installedOrFail(ref);
|
|
12288
11466
|
const stores = await this.stores();
|
|
12289
11467
|
if (await this.hasBundle(app.bytes, app.digest)) return this.runCheckOf(app, stores);
|
|
11468
|
+
const carried = this.builtIn(app.ref);
|
|
11469
|
+
if (carried && carried.digest === app.digest) {
|
|
11470
|
+
const held = await this.bundled(carried).catch(() => null);
|
|
11471
|
+
if (held) {
|
|
11472
|
+
const kind = await this.writeBundle(app.digest, held.bytes);
|
|
11473
|
+
const now = await this.app(app.ref);
|
|
11474
|
+
if (!now || now.digest !== app.digest) {
|
|
11475
|
+
await this.removeBundle(kind, app.digest);
|
|
11476
|
+
fail("not-installed", "This app is not installed");
|
|
11477
|
+
}
|
|
11478
|
+
if (now.bytes !== kind) await this.putApp({
|
|
11479
|
+
...now,
|
|
11480
|
+
bytes: kind
|
|
11481
|
+
});
|
|
11482
|
+
return this.runCheckOf({
|
|
11483
|
+
...now,
|
|
11484
|
+
bytes: kind
|
|
11485
|
+
}, stores);
|
|
11486
|
+
}
|
|
11487
|
+
}
|
|
12290
11488
|
const urls = [app.from];
|
|
12291
11489
|
for (const s of stores) {
|
|
12292
11490
|
if (app.store !== void 0 && s.key !== app.store) continue;
|
|
@@ -12294,6 +11492,14 @@ var Apps = class {
|
|
|
12294
11492
|
}
|
|
12295
11493
|
if (app.store === void 0) urls.push(...app.manifest.sources ?? []);
|
|
12296
11494
|
for (const url of new Set(urls.filter(isAppFetchUrl))) {
|
|
11495
|
+
try {
|
|
11496
|
+
if (peekAppManifest(await this.host.fetch(url, {
|
|
11497
|
+
maxBytes: PEEK_BYTES,
|
|
11498
|
+
peek: true
|
|
11499
|
+
}))?.digest !== app.digest) continue;
|
|
11500
|
+
} catch {
|
|
11501
|
+
continue;
|
|
11502
|
+
}
|
|
12297
11503
|
let fetched;
|
|
12298
11504
|
try {
|
|
12299
11505
|
fetched = await this.fetchBundle(url);
|
|
@@ -12341,7 +11547,7 @@ var Apps = class {
|
|
|
12341
11547
|
if (run.status === "removed" && runAnyway !== true) fail("removed", `Removed by ${run.by[0].name}: ${run.by[0].reason}`);
|
|
12342
11548
|
if (run.status === "needs-files") fail("needs-files", "This app's files are not on this device yet");
|
|
12343
11549
|
if (run.status === "removed") this.anyway.add(`${app.ref} ${app.digest}`);
|
|
12344
|
-
const bundle = await this.verifiedBundle(app);
|
|
11550
|
+
const bundle = await this.verifiedBundle(app, true);
|
|
12345
11551
|
const entry = bundle.files.get(bundle.manifest.entry) ?? fail("damaged", "The entry is missing");
|
|
12346
11552
|
this.ran.add(app.ref);
|
|
12347
11553
|
const versions = this.started.get(app.ref) ?? /* @__PURE__ */ new Map();
|
|
@@ -12357,6 +11563,29 @@ var Apps = class {
|
|
|
12357
11563
|
};
|
|
12358
11564
|
}
|
|
12359
11565
|
/**
|
|
11566
|
+
* Where a file of the installed bundle lies in this device's file storage, for a client that hands the bytes to the
|
|
11567
|
+
* app itself (Desktop: Rust reads them, checks their SHA-256 and gives them to the app's window, so they never cross
|
|
11568
|
+
* the Ghostly window). The same checks as `file`; null where the bundle is not in such storage, and for the icon,
|
|
11569
|
+
* which `file` answers from memory.
|
|
11570
|
+
*/
|
|
11571
|
+
async fileAt({ ref, path, digest }) {
|
|
11572
|
+
if (path === "icon.png") return null;
|
|
11573
|
+
const app = await this.runningOrFail(ref, digest);
|
|
11574
|
+
if (digest !== void 0 && digest !== app.digest) fail("no-file", "No such file in this app");
|
|
11575
|
+
if (app.bytes !== "native") return null;
|
|
11576
|
+
const bundle = await this.verifiedBundle(app, true);
|
|
11577
|
+
const file = typeof path === "string" ? bundle.files.get(path) : void 0;
|
|
11578
|
+
const listed = bundle.manifest.files.find((f) => f.path === path);
|
|
11579
|
+
if (!file || !listed) fail("no-file", "No such file in this app");
|
|
11580
|
+
return {
|
|
11581
|
+
space: fileSpace(),
|
|
11582
|
+
id: bundleId(app.digest),
|
|
11583
|
+
offset: file.byteOffset,
|
|
11584
|
+
length: file.length,
|
|
11585
|
+
sha256: listed.sha256
|
|
11586
|
+
};
|
|
11587
|
+
}
|
|
11588
|
+
/**
|
|
12360
11589
|
* A file of the installed bundle, for the broker's `file` (`ghostly.file(path)`): a copy, which the broker transfers to
|
|
12361
11590
|
* the app as it is. `digest`: the version the frame runs.
|
|
12362
11591
|
*/
|
|
@@ -12366,7 +11595,7 @@ var Apps = class {
|
|
|
12366
11595
|
const icon = path === "icon.png" ? this.icons.get(app.digest) : void 0;
|
|
12367
11596
|
if (icon) return icon.slice();
|
|
12368
11597
|
if (icon === null) fail("no-file", "No such file in this app");
|
|
12369
|
-
const bundle = await this.verifiedBundle(app);
|
|
11598
|
+
const bundle = await this.verifiedBundle(app, path !== APP_ICON_PATH);
|
|
12370
11599
|
if (typeof path !== "string" || !bundle.files.has(path)) fail("no-file", "No such file in this app");
|
|
12371
11600
|
return bundle.files.get(path).slice();
|
|
12372
11601
|
}
|
|
@@ -12397,6 +11626,7 @@ var Apps = class {
|
|
|
12397
11626
|
run: await this.runCheckOf(app, stores),
|
|
12398
11627
|
listedBy,
|
|
12399
11628
|
unknownPublisher: !listedBy.some((s) => s.kind === "curated"),
|
|
11629
|
+
...this.builtIn(app.ref)?.digest === app.digest && { builtIn: true },
|
|
12400
11630
|
...app.pending && { pending: {
|
|
12401
11631
|
sequence: app.pending.sequence,
|
|
12402
11632
|
version: app.pending.manifest.version,
|
|
@@ -12416,23 +11646,44 @@ var Apps = class {
|
|
|
12416
11646
|
}
|
|
12417
11647
|
/**
|
|
12418
11648
|
* Uninstalls an app: its bundle (and a waiting update's), its record and its storage in every scope. The person chose
|
|
12419
|
-
* it, after the offer of an export (`exportData`).
|
|
11649
|
+
* it, after the offer of an export (`exportData`). A built-in app is marked removed in the same transaction, and one
|
|
11650
|
+
* the person never opened is only marked: no start and no update offers it again (WISP 1200 § Built-in apps).
|
|
12420
11651
|
*/
|
|
12421
11652
|
async uninstall({ ref }) {
|
|
12422
|
-
|
|
12423
|
-
const
|
|
12424
|
-
|
|
12425
|
-
|
|
11653
|
+
if (!isAppRef(ref)) fail("bad-ref", "Not an app reference");
|
|
11654
|
+
const app = await this.app(ref);
|
|
11655
|
+
const builtIn = this.builtIn(ref) !== void 0;
|
|
11656
|
+
if (!app && !builtIn) fail("not-installed", "This app is not installed");
|
|
11657
|
+
const tx = (await openDb()).transaction([
|
|
11658
|
+
STORES$1.apps,
|
|
11659
|
+
STORES$1.appStorage,
|
|
11660
|
+
...builtIn ? [STORES$1.settings] : []
|
|
11661
|
+
], "readwrite");
|
|
11662
|
+
tx.objectStore(STORES$1.apps).delete(ref);
|
|
11663
|
+
tx.objectStore(STORES$1.appStorage).delete(rowsOf(ref));
|
|
11664
|
+
if (builtIn) markBuiltIns(tx.objectStore(STORES$1.settings), (marks) => {
|
|
11665
|
+
marks.removed[ref] ??= this.now();
|
|
11666
|
+
});
|
|
12426
11667
|
try {
|
|
12427
11668
|
await done(tx);
|
|
12428
11669
|
} finally {
|
|
12429
|
-
this.held.delete(
|
|
11670
|
+
this.held.delete(ref);
|
|
12430
11671
|
}
|
|
12431
|
-
for (const key of [...this.usage.keys()]) if (key.startsWith(`${
|
|
12432
|
-
this.started.delete(
|
|
11672
|
+
for (const key of [...this.usage.keys()]) if (key.startsWith(`${ref}\0`)) this.usage.delete(key);
|
|
11673
|
+
this.started.delete(ref);
|
|
11674
|
+
if (!app) return;
|
|
12433
11675
|
await this.removeBundle(app.bytes, app.digest);
|
|
12434
11676
|
if (app.pending) await this.removeBundle(app.pending.bytes, app.pending.digest);
|
|
12435
11677
|
}
|
|
11678
|
+
/** Takes the "removed by the person" mark off a built-in app, if it has one. */
|
|
11679
|
+
async keepBuiltIn(ref) {
|
|
11680
|
+
if ((await this.builtInMarks()).removed[ref] === void 0) return;
|
|
11681
|
+
const tx = (await openDb()).transaction(STORES$1.settings, "readwrite");
|
|
11682
|
+
markBuiltIns(tx.objectStore(STORES$1.settings), (marks) => {
|
|
11683
|
+
delete marks.removed[ref];
|
|
11684
|
+
});
|
|
11685
|
+
await done(tx);
|
|
11686
|
+
}
|
|
12436
11687
|
scopeOf(scope) {
|
|
12437
11688
|
if (scope === "alone" || typeof scope === "string" && this.host.isChat(scope)) return scope;
|
|
12438
11689
|
fail("bad-scope", "Not a chat of this profile");
|
|
@@ -12451,13 +11702,24 @@ var Apps = class {
|
|
|
12451
11702
|
});
|
|
12452
11703
|
return run;
|
|
12453
11704
|
}
|
|
11705
|
+
/**
|
|
11706
|
+
* What a scope holds: its counter row, when the number of rows agrees with it (one key count, no value read). A
|
|
11707
|
+
* scope without one (written before it was kept) or where it disagrees (rows written by an older client) is
|
|
11708
|
+
* counted from its rows once.
|
|
11709
|
+
*/
|
|
12454
11710
|
async usageOf(ref, scope) {
|
|
12455
11711
|
const id = `${ref}\0${scope}`;
|
|
12456
|
-
|
|
12457
|
-
if (
|
|
12458
|
-
|
|
12459
|
-
|
|
12460
|
-
|
|
11712
|
+
const cached = this.usage.get(id);
|
|
11713
|
+
if (cached) return cached;
|
|
11714
|
+
const rows = await store(STORES$1.appStorage, "readonly");
|
|
11715
|
+
const [counter, n] = await Promise.all([wrap(rows.get([
|
|
11716
|
+
ref,
|
|
11717
|
+
scope,
|
|
11718
|
+
COUNT_KEY
|
|
11719
|
+
])), wrap(rows.count(keysOf(ref, scope)))]);
|
|
11720
|
+
let used = counter ? readCount(counter.value) : n ? null : NO_ROWS;
|
|
11721
|
+
if (!used || used.rows !== n) used = (await wrap((await store(STORES$1.appStorage, "readonly")).getAll(keysOf(ref, scope)))).reduce((sum, r) => plus(sum, countOf(r)), NO_ROWS);
|
|
11722
|
+
this.usage.set(id, used);
|
|
12461
11723
|
return used;
|
|
12462
11724
|
}
|
|
12463
11725
|
async storageGet({ ref, scope, key, digest }) {
|
|
@@ -12471,8 +11733,8 @@ var Apps = class {
|
|
|
12471
11733
|
}
|
|
12472
11734
|
/**
|
|
12473
11735
|
* Keeps a JSON value under a key: at most 64 KiB, and 5 MiB for every key of this app in this scope, each counted
|
|
12474
|
-
*
|
|
12475
|
-
*
|
|
11736
|
+
* as `costOf` says. A scope already past that (filled under an older count) still takes a write that does not grow
|
|
11737
|
+
* it, so the app can make room.
|
|
12476
11738
|
*/
|
|
12477
11739
|
async storageSet({ ref, scope, key, value, digest }) {
|
|
12478
11740
|
const app = await this.runningOrFail(ref, digest);
|
|
@@ -12495,20 +11757,24 @@ var Apps = class {
|
|
|
12495
11757
|
where,
|
|
12496
11758
|
name
|
|
12497
11759
|
]));
|
|
12498
|
-
const next =
|
|
12499
|
-
|
|
11760
|
+
const next = plus(old ? plus(used, countOf(old, -1)) : used, countOf({
|
|
11761
|
+
key: name,
|
|
11762
|
+
size
|
|
11763
|
+
}));
|
|
11764
|
+
if (costOf(next) > APP_STORAGE_LIMITS.scopeBytes && costOf(next) > costOf(used)) fail("full", "This app's storage in this chat is full (5 MiB)");
|
|
12500
11765
|
const writing = await store(STORES$1.appStorage, "readwrite");
|
|
12501
11766
|
if (where !== "alone" && !this.host.isChat(where)) {
|
|
12502
11767
|
this.usage.delete(`${app.ref}\0${where}`);
|
|
12503
11768
|
fail("bad-scope", "Not a chat of this profile");
|
|
12504
11769
|
}
|
|
12505
|
-
|
|
11770
|
+
writing.put({
|
|
12506
11771
|
ref: app.ref,
|
|
12507
11772
|
scope: where,
|
|
12508
11773
|
key: name,
|
|
12509
11774
|
value: text,
|
|
12510
11775
|
size
|
|
12511
|
-
})
|
|
11776
|
+
});
|
|
11777
|
+
await wrap(writing.put(counterRow(app.ref, where, next)));
|
|
12512
11778
|
this.usage.set(`${app.ref}\0${where}`, next);
|
|
12513
11779
|
});
|
|
12514
11780
|
}
|
|
@@ -12523,39 +11789,73 @@ var Apps = class {
|
|
|
12523
11789
|
name
|
|
12524
11790
|
]));
|
|
12525
11791
|
if (!old) return;
|
|
12526
|
-
|
|
11792
|
+
const next = plus(used, countOf(old, -1));
|
|
11793
|
+
const writing = await store(STORES$1.appStorage, "readwrite");
|
|
11794
|
+
writing.delete([
|
|
12527
11795
|
app.ref,
|
|
12528
11796
|
where,
|
|
12529
11797
|
name
|
|
12530
|
-
])
|
|
12531
|
-
|
|
11798
|
+
]);
|
|
11799
|
+
await (next.rows ? wrap(writing.put(counterRow(app.ref, where, next))) : wrap(writing.delete([
|
|
11800
|
+
app.ref,
|
|
11801
|
+
where,
|
|
11802
|
+
COUNT_KEY
|
|
11803
|
+
])));
|
|
11804
|
+
this.usage.set(`${app.ref}\0${where}`, next);
|
|
12532
11805
|
});
|
|
12533
11806
|
}
|
|
12534
11807
|
async storageKeys({ ref, scope, digest }) {
|
|
12535
11808
|
const app = await this.runningOrFail(ref, digest);
|
|
12536
|
-
return (await wrap((await store(STORES$1.appStorage, "readonly")).getAllKeys(
|
|
11809
|
+
return (await wrap((await store(STORES$1.appStorage, "readonly")).getAllKeys(keysOf(app.ref, this.scopeOf(scope))))).map((k) => k[2]);
|
|
12537
11810
|
}
|
|
12538
11811
|
/** An app's storage in every scope, as export files (WISP 1200 § Where installed apps live), offered before an uninstall. */
|
|
12539
11812
|
async exportData({ ref }) {
|
|
12540
|
-
|
|
12541
|
-
|
|
12542
|
-
const scopes = /* @__PURE__ */ new Map();
|
|
12543
|
-
for (const row of rows) {
|
|
12544
|
-
const out = scopes.get(row.scope) ?? {
|
|
11813
|
+
return (await this.exportDataText({ ref })).map(({ app, scope, entries }) => {
|
|
11814
|
+
const out = {
|
|
12545
11815
|
ghostlyAppData: 1,
|
|
12546
|
-
app
|
|
12547
|
-
scope
|
|
11816
|
+
app,
|
|
11817
|
+
scope,
|
|
12548
11818
|
entries: {}
|
|
12549
11819
|
};
|
|
12550
|
-
Object.defineProperty(out.entries,
|
|
12551
|
-
value: JSON.parse(
|
|
11820
|
+
for (const [key, json] of entries) Object.defineProperty(out.entries, key, {
|
|
11821
|
+
value: JSON.parse(json),
|
|
12552
11822
|
enumerable: true,
|
|
12553
11823
|
writable: true,
|
|
12554
11824
|
configurable: true
|
|
12555
11825
|
});
|
|
12556
|
-
|
|
11826
|
+
return out;
|
|
11827
|
+
});
|
|
11828
|
+
}
|
|
11829
|
+
/**
|
|
11830
|
+
* `exportData` with each value as the JSON text it is kept as, for a file written without parsing and writing every
|
|
11831
|
+
* value again. Read a scope at a time, the thread let go between them: ten chats of 5 MiB read at once held it for
|
|
11832
|
+
* the whole read.
|
|
11833
|
+
*/
|
|
11834
|
+
async exportDataText({ ref }) {
|
|
11835
|
+
const app = await this.installedOrFail(ref);
|
|
11836
|
+
const scopes = [];
|
|
11837
|
+
const index = (await store(STORES$1.appStorage, "readonly")).index(APP_STORAGE_SCOPE_INDEX);
|
|
11838
|
+
await new Promise((resolve, reject) => {
|
|
11839
|
+
const request = index.openKeyCursor(rowsOf(app.ref), "nextunique");
|
|
11840
|
+
request.onerror = () => reject(request.error);
|
|
11841
|
+
request.onsuccess = () => {
|
|
11842
|
+
const cursor = request.result;
|
|
11843
|
+
if (!cursor) return resolve();
|
|
11844
|
+
scopes.push(cursor.key[1]);
|
|
11845
|
+
cursor.continue();
|
|
11846
|
+
};
|
|
11847
|
+
});
|
|
11848
|
+
const out = [];
|
|
11849
|
+
for (const [i, scope] of scopes.entries()) {
|
|
11850
|
+
if (i) await new Promise((resolve) => setTimeout(resolve, 0));
|
|
11851
|
+
const rows = await wrap((await store(STORES$1.appStorage, "readonly")).getAll(keysOf(app.ref, scope)));
|
|
11852
|
+
if (rows.length) out.push({
|
|
11853
|
+
app: app.ref,
|
|
11854
|
+
scope,
|
|
11855
|
+
entries: rows.map((row) => [row.key, row.value])
|
|
11856
|
+
});
|
|
12557
11857
|
}
|
|
12558
|
-
return
|
|
11858
|
+
return out;
|
|
12559
11859
|
}
|
|
12560
11860
|
};
|
|
12561
11861
|
//#endregion
|
|
@@ -16680,7 +15980,7 @@ function handoffNotices(was, handoff, session) {
|
|
|
16680
15980
|
* device link must offer every transport both devices run: a Linux Desktop has no WebRTC, and an Iroh relay that
|
|
16681
15981
|
* one side cannot reach leaves HyperDHT as the only way through.
|
|
16682
15982
|
*/
|
|
16683
|
-
var NATIVE_RETRY_MS = 1e4;
|
|
15983
|
+
var NATIVE_RETRY_MS$1 = 1e4;
|
|
16684
15984
|
var NATIVE_RETRY_MAX_MS = 12e4;
|
|
16685
15985
|
/** How often a standby reads the turn while its screen shows (WISP 06 § When a device checks), and the jitter on it. */
|
|
16686
15986
|
var STANDBY_TURN_EVERY_MS = 6e5;
|
|
@@ -16989,7 +16289,7 @@ var DeviceLinks = class {
|
|
|
16989
16289
|
}
|
|
16990
16290
|
retryNative(running) {
|
|
16991
16291
|
if (running.nativeRetry || this.stopped || this.running.get(running.id) !== running) return;
|
|
16992
|
-
const wait = Math.min(NATIVE_RETRY_MS * 2 ** running.nativeMisses, NATIVE_RETRY_MAX_MS);
|
|
16292
|
+
const wait = Math.min(NATIVE_RETRY_MS$1 * 2 ** running.nativeMisses, NATIVE_RETRY_MAX_MS);
|
|
16993
16293
|
running.nativeMisses++;
|
|
16994
16294
|
running.nativeRetry = setTimeout(() => {
|
|
16995
16295
|
running.nativeRetry = void 0;
|
|
@@ -19363,6 +18663,7 @@ var FileDesk = class {
|
|
|
19363
18663
|
openSource: (record) => this.openSource(linkId, record),
|
|
19364
18664
|
changed: (record, transferred, progress) => this.changed(linkId, record, transferred, progress),
|
|
19365
18665
|
room: async () => (await fileBytes()).room(),
|
|
18666
|
+
...this.deps.sendData && { sendData: (id, offset, data) => this.deps.sendData(linkId, id, offset, data) },
|
|
19366
18667
|
writable: () => this.deps.writable?.(linkId)
|
|
19367
18668
|
});
|
|
19368
18669
|
this.chats.set(linkId, made);
|
|
@@ -19382,6 +18683,7 @@ var FileDesk = class {
|
|
|
19382
18683
|
records.push(file.wire3);
|
|
19383
18684
|
if (file.wire3.direction === "in") chat.local.set(file.wire3.id, file.id);
|
|
19384
18685
|
if (!transferEnded(file.wire3)) this.restored.add(file.id);
|
|
18686
|
+
if (file.wire3.state === "done") continue;
|
|
19385
18687
|
this.deps.transfers.set(file.id, this.view(chat, file.wire3, file.wire3.confirmed, transferStalled(file.wire3, Date.now(), this.restored.has(file.id), Date.now())));
|
|
19386
18688
|
}
|
|
19387
18689
|
chat.files.restore(records);
|
|
@@ -19822,7 +19124,7 @@ var FileDesk = class {
|
|
|
19822
19124
|
view.room = this.deps.transfers.get(id)?.room;
|
|
19823
19125
|
this.room(id);
|
|
19824
19126
|
}
|
|
19825
|
-
this.deps.transfers.set(id, view);
|
|
19127
|
+
if (record.state !== "done" || this.underway.has(id) || this.deps.transfers.has(id)) this.deps.transfers.set(id, view);
|
|
19826
19128
|
if (!transferEnded(record)) {
|
|
19827
19129
|
this.underway.add(id);
|
|
19828
19130
|
this.ended.delete(id);
|
|
@@ -20350,6 +19652,10 @@ var Outbox = class {
|
|
|
20350
19652
|
get sameLink() {
|
|
20351
19653
|
return !this.stopped && this.closed === this.began;
|
|
20352
19654
|
}
|
|
19655
|
+
/** One message as stored now: by its id when the store can, else picked out of the chat's history. */
|
|
19656
|
+
async one(id) {
|
|
19657
|
+
return this.store.get ? this.store.get(id) : (await this.store.read()).find((m) => m.id === id);
|
|
19658
|
+
}
|
|
20353
19659
|
/** A new message a turn could not send (the link closed under it, or the ones before it still wait): it waits too. */
|
|
20354
19660
|
async waitsItsTurn(message) {
|
|
20355
19661
|
await this.store.update(message.id, "waiting", "Waits for the messages before it to go.", {
|
|
@@ -20454,7 +19760,7 @@ var Outbox = class {
|
|
|
20454
19760
|
this.busy.add(id);
|
|
20455
19761
|
let again;
|
|
20456
19762
|
try {
|
|
20457
|
-
let message = read ??
|
|
19763
|
+
let message = read ?? await this.one(id);
|
|
20458
19764
|
if (!message?.wireId || !message.delivery || message.delivery === "delivered") return;
|
|
20459
19765
|
const wait = this.waiting.get(id);
|
|
20460
19766
|
if (wait?.timer) {
|
|
@@ -20495,7 +19801,7 @@ var Outbox = class {
|
|
|
20495
19801
|
again = this.reopenedWhileBusy.delete(id);
|
|
20496
19802
|
}
|
|
20497
19803
|
if (again) {
|
|
20498
|
-
const message =
|
|
19804
|
+
const message = await this.one(id);
|
|
20499
19805
|
if (message) await this.again(message);
|
|
20500
19806
|
}
|
|
20501
19807
|
}
|
|
@@ -20505,7 +19811,7 @@ var Outbox = class {
|
|
|
20505
19811
|
* nothing about it is unconfirmed; it counts no attempt until it goes.
|
|
20506
19812
|
*/
|
|
20507
19813
|
async wait(id, reason) {
|
|
20508
|
-
const message =
|
|
19814
|
+
const message = await this.one(id);
|
|
20509
19815
|
if (!message || this.stopped) return;
|
|
20510
19816
|
await this.store.update(id, "waiting", reason, { resendUntil: message.resendUntil ?? this.now() + this.policy.windowMs });
|
|
20511
19817
|
this.schedule(id);
|
|
@@ -20595,7 +19901,7 @@ var Outbox = class {
|
|
|
20595
19901
|
*/
|
|
20596
19902
|
async attempt(id, passed) {
|
|
20597
19903
|
if (this.stopped || this.busy.has(id)) return true;
|
|
20598
|
-
const message =
|
|
19904
|
+
const message = await this.one(id);
|
|
20599
19905
|
if (!message || message.delivery !== "queued" && message.delivery !== "waiting" || message.via === "hold") {
|
|
20600
19906
|
this.forget(id);
|
|
20601
19907
|
return true;
|
|
@@ -20622,7 +19928,7 @@ var Outbox = class {
|
|
|
20622
19928
|
}
|
|
20623
19929
|
/** After a failed attempt: queued again, unless its window or its attempts are spent. */
|
|
20624
19930
|
async requeue(id, reason) {
|
|
20625
|
-
const message =
|
|
19931
|
+
const message = await this.one(id);
|
|
20626
19932
|
if (message && message.delivery !== "delivered") await this.queue(message, reason);
|
|
20627
19933
|
}
|
|
20628
19934
|
async queue(message, reason) {
|
|
@@ -20670,7 +19976,7 @@ var Outbox = class {
|
|
|
20670
19976
|
this.turn(async () => {
|
|
20671
19977
|
if (this.receipts.get(id) !== timer) return;
|
|
20672
19978
|
this.receipts.delete(id);
|
|
20673
|
-
const message =
|
|
19979
|
+
const message = await this.one(id);
|
|
20674
19980
|
if (message?.via === "pkarr") await this.expired(message);
|
|
20675
19981
|
else await this.requeue(id, "No receipt received yet.");
|
|
20676
19982
|
}).catch(() => {});
|
|
@@ -21357,8 +20663,9 @@ var db = {
|
|
|
21357
20663
|
tx.onerror = tx.onabort = () => reject(tx.error ?? /* @__PURE__ */ new Error("Link storage failed"));
|
|
21358
20664
|
});
|
|
21359
20665
|
},
|
|
21360
|
-
|
|
21361
|
-
|
|
20666
|
+
/** `relaxed`: for what a power cut may cost (a round trip, a note made again later), no disk sync of its own. */
|
|
20667
|
+
async patchLink(linkId, patch, relaxed = false) {
|
|
20668
|
+
const tx = (await openDb()).transaction(STORES$1.links, "readwrite", relaxed ? RELAXED : void 0);
|
|
21362
20669
|
const links = tx.objectStore(STORES$1.links);
|
|
21363
20670
|
const request = links.get(linkId);
|
|
21364
20671
|
request.onsuccess = () => {
|
|
@@ -21619,6 +20926,32 @@ var db = {
|
|
|
21619
20926
|
});
|
|
21620
20927
|
return result;
|
|
21621
20928
|
},
|
|
20929
|
+
/**
|
|
20930
|
+
* `patchMessage` on these rows of one history, all in one transaction: one disk sync for all of them. Resolves to
|
|
20931
|
+
* the rows changed, in the order of `ids`.
|
|
20932
|
+
*/
|
|
20933
|
+
async patchMessages(linkId, ids, change) {
|
|
20934
|
+
const tx = (await openDb()).transaction(STORES$1.messages, "readwrite");
|
|
20935
|
+
const messages = tx.objectStore(STORES$1.messages);
|
|
20936
|
+
const results = [];
|
|
20937
|
+
ids.forEach((id, i) => {
|
|
20938
|
+
const request = messages.get([linkId, id]);
|
|
20939
|
+
request.onsuccess = () => {
|
|
20940
|
+
const message = request.result;
|
|
20941
|
+
const patch = message && change(message);
|
|
20942
|
+
if (!patch) return;
|
|
20943
|
+
messages.put(results[i] = {
|
|
20944
|
+
...message,
|
|
20945
|
+
...patch
|
|
20946
|
+
});
|
|
20947
|
+
};
|
|
20948
|
+
});
|
|
20949
|
+
await new Promise((resolve, reject) => {
|
|
20950
|
+
tx.oncomplete = () => resolve();
|
|
20951
|
+
tx.onerror = tx.onabort = () => reject(tx.error ?? /* @__PURE__ */ new Error("Message storage failed"));
|
|
20952
|
+
});
|
|
20953
|
+
return results.filter((row) => !!row);
|
|
20954
|
+
},
|
|
21622
20955
|
async deleteMessage(linkId, messageId) {
|
|
21623
20956
|
await wrap((await store(STORES$1.messages, "readwrite")).delete([linkId, messageId]));
|
|
21624
20957
|
},
|
|
@@ -22021,6 +21354,202 @@ async function myPin(host, chat, messageId, remove, current, now = Date.now()) {
|
|
|
22021
21354
|
};
|
|
22022
21355
|
}
|
|
22023
21356
|
//#endregion
|
|
21357
|
+
//#region ../browser/src/engine/groupAuthorOrder.ts
|
|
21358
|
+
/**
|
|
21359
|
+
* How long a member's message may still be placed again below that member's older ones (WISP 400, requirement 10):
|
|
21360
|
+
* about a catch-up, whose answers end within the three minutes the app waits for the first.
|
|
21361
|
+
*/
|
|
21362
|
+
var AUTHOR_ORDER_WINDOW_MS = 18e4;
|
|
21363
|
+
AUTHOR_ORDER_WINDOW_MS / 2;
|
|
21364
|
+
/** Whether `a` was written after `b` by the same member: a later epoch, or a higher number in the same one. */
|
|
21365
|
+
function after(a, b) {
|
|
21366
|
+
return a.epoch > b.epoch || a.epoch === b.epoch && a.seq > b.seq;
|
|
21367
|
+
}
|
|
21368
|
+
/**
|
|
21369
|
+
* A member's messages in a private group in the order that member wrote them (WISP 400, requirement 10, and WISP 902
|
|
21370
|
+
* § Catch-up). A message is placed where it arrives. When one comes of a member whose later messages were placed here
|
|
21371
|
+
* moments ago (two catch-up answers raced: the author's own newest 32 and another member's hand-on of the older ones),
|
|
21372
|
+
* those later ones are placed again below it, in the order they were written, and the lines that came after them before
|
|
21373
|
+
* the older one (another member's, mine) go along below them, in their order: a reply to the newest stays below it. A
|
|
21374
|
+
* row only ever moves down, so nobody else's lines change order. While messages of that member are still missing
|
|
21375
|
+
* (`missing`), the rows wait for them, so each row moves about once in a whole catch-up.
|
|
21376
|
+
*/
|
|
21377
|
+
var GroupAuthorOrder = class {
|
|
21378
|
+
host;
|
|
21379
|
+
linkOf;
|
|
21380
|
+
missing;
|
|
21381
|
+
/** Per group: the members' rows followed, in the order they are placed in the history. */
|
|
21382
|
+
rows = /* @__PURE__ */ new Map();
|
|
21383
|
+
/** Per group: the members with a row placed above an older one of theirs, since when, and when their last row came. */
|
|
21384
|
+
due = /* @__PURE__ */ new Map();
|
|
21385
|
+
running = /* @__PURE__ */ new Set();
|
|
21386
|
+
constructor(host, linkOf, missing = () => false) {
|
|
21387
|
+
this.host = host;
|
|
21388
|
+
this.linkOf = linkOf;
|
|
21389
|
+
this.missing = missing;
|
|
21390
|
+
}
|
|
21391
|
+
/** A member's message was stored in the group's history, new: `at` is when, on the engine's clock. */
|
|
21392
|
+
placed(groupId, row) {
|
|
21393
|
+
if (!this.host.placeAgain) return;
|
|
21394
|
+
const rows = (this.rows.get(groupId) ?? []).filter((r) => r.at > row.at - AUTHOR_ORDER_WINDOW_MS).slice(-575);
|
|
21395
|
+
rows.push(row);
|
|
21396
|
+
this.rows.set(groupId, rows);
|
|
21397
|
+
let due = this.due.get(groupId);
|
|
21398
|
+
if (!due?.has(row.sender) && !rows.some((r) => r.sender === row.sender && after(r, row))) return;
|
|
21399
|
+
if (!due) this.due.set(groupId, due = /* @__PURE__ */ new Map());
|
|
21400
|
+
due.set(row.sender, {
|
|
21401
|
+
since: due.get(row.sender)?.since ?? row.at,
|
|
21402
|
+
last: row.at
|
|
21403
|
+
});
|
|
21404
|
+
if (!this.missing(groupId, row.sender)) this.run(groupId, row.at);
|
|
21405
|
+
}
|
|
21406
|
+
/** Time passed: rows nothing more is on its way for, or that waited long enough, are placed again. */
|
|
21407
|
+
tick(now) {
|
|
21408
|
+
for (const groupId of this.due.keys()) this.run(groupId, now);
|
|
21409
|
+
}
|
|
21410
|
+
/** The group's history is gone (left or forgotten). */
|
|
21411
|
+
forget(groupId) {
|
|
21412
|
+
this.rows.delete(groupId);
|
|
21413
|
+
this.due.delete(groupId);
|
|
21414
|
+
}
|
|
21415
|
+
/**
|
|
21416
|
+
* One group's rows put in order, a member at a time; what comes meanwhile is put in order by the next turn, so a
|
|
21417
|
+
* catch-up's frames that come together move each row once, not once per frame.
|
|
21418
|
+
*/
|
|
21419
|
+
async run(groupId, now) {
|
|
21420
|
+
if (this.running.has(groupId)) return;
|
|
21421
|
+
this.running.add(groupId);
|
|
21422
|
+
try {
|
|
21423
|
+
for (;;) {
|
|
21424
|
+
const due = this.due.get(groupId);
|
|
21425
|
+
const ready = [...due ?? []].filter(([sender, { since, last }]) => now - last >= 15e3 || now - since >= 9e4 || !this.missing(groupId, sender)).map(([sender]) => sender);
|
|
21426
|
+
if (!ready.length) break;
|
|
21427
|
+
for (const sender of ready) due.delete(sender);
|
|
21428
|
+
if (!due.size) this.due.delete(groupId);
|
|
21429
|
+
for (const sender of ready) {
|
|
21430
|
+
const rows = this.rows.get(groupId) ?? [];
|
|
21431
|
+
const mine = rows.filter((r) => r.sender === sender);
|
|
21432
|
+
let from, below;
|
|
21433
|
+
for (const r of [...mine].reverse()) {
|
|
21434
|
+
if (below && after(r, below) && (!from || after(from, r))) from = r;
|
|
21435
|
+
if (!below || after(below, r)) below = r;
|
|
21436
|
+
}
|
|
21437
|
+
if (!from) continue;
|
|
21438
|
+
const newer = mine.filter((r) => !after(from, r)).sort((a, b) => after(a, b) ? 1 : after(b, a) ? -1 : 0);
|
|
21439
|
+
const ids = new Set(newer.map((r) => r.id));
|
|
21440
|
+
const first = rows.findIndex((r) => ids.has(r.id));
|
|
21441
|
+
let end = rows.length;
|
|
21442
|
+
for (let i = first + 1, top = rows[first]; i < rows.length; i++) {
|
|
21443
|
+
if (rows[i].sender !== sender) continue;
|
|
21444
|
+
if (after(top, rows[i])) {
|
|
21445
|
+
end = i;
|
|
21446
|
+
break;
|
|
21447
|
+
}
|
|
21448
|
+
top = rows[i];
|
|
21449
|
+
}
|
|
21450
|
+
const followers = rows.slice(first, end).filter((r) => r.sender !== sender);
|
|
21451
|
+
for (const r of followers) ids.add(r.id);
|
|
21452
|
+
const moved = [...newer, ...followers];
|
|
21453
|
+
this.rows.set(groupId, [...rows.filter((r) => !ids.has(r.id)), ...moved.map((r) => ({
|
|
21454
|
+
...r,
|
|
21455
|
+
at: Math.max(r.at, now)
|
|
21456
|
+
}))]);
|
|
21457
|
+
await this.host.placeAgain(this.linkOf(groupId), moved.map((r) => r.id)).catch(() => {});
|
|
21458
|
+
}
|
|
21459
|
+
}
|
|
21460
|
+
} finally {
|
|
21461
|
+
this.running.delete(groupId);
|
|
21462
|
+
}
|
|
21463
|
+
}
|
|
21464
|
+
};
|
|
21465
|
+
//#endregion
|
|
21466
|
+
//#region ../browser/src/engine/groupLineOrder.ts
|
|
21467
|
+
/**
|
|
21468
|
+
* The lines of a group's history ("… joined", "… is no longer a member") among the messages of their moment, for a
|
|
21469
|
+
* member back from away (WISP 902 § Catch-up). A message sealed under an epoch was written before its author knew of
|
|
21470
|
+
* the next commit, so the line of that commit goes below it; a message under the commit's epoch or a later one was
|
|
21471
|
+
* written knowing it, and stays below the line. A catch-up brings the commits first (a commit opens the messages of
|
|
21472
|
+
* its epoch) and, in a private group, the answers of several members race, so the messages of the epoch before can come
|
|
21473
|
+
* after the line. Then the line, and the rows after it written knowing it, are placed again below them, in their order;
|
|
21474
|
+
* no clock is read. A row only ever moves down, and only rows placed here in the last few minutes, so a live
|
|
21475
|
+
* conversation read long ago never moves. Moves made meanwhile go in one turn: a row moves about once a catch-up page.
|
|
21476
|
+
*/
|
|
21477
|
+
var GroupLineOrder = class {
|
|
21478
|
+
host;
|
|
21479
|
+
/** Per history: the rows followed, in the order they are placed in it. */
|
|
21480
|
+
rows = /* @__PURE__ */ new Map();
|
|
21481
|
+
due = /* @__PURE__ */ new Set();
|
|
21482
|
+
running = /* @__PURE__ */ new Set();
|
|
21483
|
+
constructor(host) {
|
|
21484
|
+
this.host = host;
|
|
21485
|
+
}
|
|
21486
|
+
/**
|
|
21487
|
+
* A row is being stored, a line of the history or a message sealed under this epoch, and takes its place now: `at` is
|
|
21488
|
+
* when, on the engine's clock. Call what this returns with whether it was new: a message below a line of a later
|
|
21489
|
+
* epoch takes the line (and the rows after it written knowing it) below it.
|
|
21490
|
+
*/
|
|
21491
|
+
placing(linkId, row, at) {
|
|
21492
|
+
if (!this.host.placeAgain) return () => {};
|
|
21493
|
+
const followed = {
|
|
21494
|
+
...row,
|
|
21495
|
+
at
|
|
21496
|
+
};
|
|
21497
|
+
const rows = (this.rows.get(linkId) ?? []).filter((r) => r.at > at - AUTHOR_ORDER_WINDOW_MS).slice(-575);
|
|
21498
|
+
rows.push(followed);
|
|
21499
|
+
this.rows.set(linkId, rows);
|
|
21500
|
+
return (stored) => {
|
|
21501
|
+
const now = this.rows.get(linkId);
|
|
21502
|
+
if (!stored) {
|
|
21503
|
+
if (now) this.rows.set(linkId, now.filter((r) => r !== followed));
|
|
21504
|
+
return;
|
|
21505
|
+
}
|
|
21506
|
+
if (!followed.line && now?.some((r) => r.line && r.epoch > followed.epoch && now.indexOf(r) < now.indexOf(followed))) this.run(linkId);
|
|
21507
|
+
};
|
|
21508
|
+
}
|
|
21509
|
+
/** These rows were placed again below everything (`GroupAuthorOrder`), in this order. */
|
|
21510
|
+
moved(linkId, ids) {
|
|
21511
|
+
const rows = this.rows.get(linkId);
|
|
21512
|
+
if (!rows) return;
|
|
21513
|
+
const moved = ids.map((id) => rows.find((r) => r.id === id)).filter((r) => !!r);
|
|
21514
|
+
if (!moved.length) return;
|
|
21515
|
+
this.rows.set(linkId, [...rows.filter((r) => !moved.includes(r)), ...moved]);
|
|
21516
|
+
if (moved.some((r) => !r.line)) this.run(linkId);
|
|
21517
|
+
}
|
|
21518
|
+
/** The group's history is gone (left or forgotten). */
|
|
21519
|
+
forget(linkId) {
|
|
21520
|
+
this.rows.delete(linkId);
|
|
21521
|
+
this.due.delete(linkId);
|
|
21522
|
+
}
|
|
21523
|
+
/** The history's rows put in order; what comes while a move is written goes in the next turn. */
|
|
21524
|
+
run(linkId) {
|
|
21525
|
+
this.due.add(linkId);
|
|
21526
|
+
if (this.running.has(linkId)) return;
|
|
21527
|
+
this.running.add(linkId);
|
|
21528
|
+
(async () => {
|
|
21529
|
+
try {
|
|
21530
|
+
while (this.due.delete(linkId)) {
|
|
21531
|
+
const rows = this.rows.get(linkId) ?? [];
|
|
21532
|
+
const order = [];
|
|
21533
|
+
for (const row of rows) {
|
|
21534
|
+
const above = row.line ? -1 : order.findIndex((r) => r.line && r.epoch > row.epoch);
|
|
21535
|
+
if (above < 0) order.push(row);
|
|
21536
|
+
else order.splice(above, 0, row);
|
|
21537
|
+
}
|
|
21538
|
+
let from = 0;
|
|
21539
|
+
while (from < rows.length && rows[from] === order[from]) from++;
|
|
21540
|
+
if (from === rows.length) continue;
|
|
21541
|
+
const last = rows[rows.length - 1].at;
|
|
21542
|
+
for (const r of order.slice(from)) r.at = Math.max(r.at, last);
|
|
21543
|
+
this.rows.set(linkId, order);
|
|
21544
|
+
await this.host.placeAgain(linkId, order.slice(from).map((r) => r.id)).catch(() => {});
|
|
21545
|
+
}
|
|
21546
|
+
} finally {
|
|
21547
|
+
this.running.delete(linkId);
|
|
21548
|
+
}
|
|
21549
|
+
})();
|
|
21550
|
+
}
|
|
21551
|
+
};
|
|
21552
|
+
//#endregion
|
|
22024
21553
|
//#region ../browser/src/engine/hubClocks.ts
|
|
22025
21554
|
/**
|
|
22026
21555
|
* A hub dates its beacon entry by its own clock, and a reader judges it by its own. A hub's time closer than this to
|
|
@@ -22274,11 +21803,14 @@ var Communities = class {
|
|
|
22274
21803
|
/** My frames an edge took, and which of them carry an edit (`frame id → editKey`). */
|
|
22275
21804
|
frames = new FramesTaken();
|
|
22276
21805
|
carriers = /* @__PURE__ */ new Map();
|
|
21806
|
+
/** The lines of each history among the messages of their moment (`GroupLineOrder`). */
|
|
21807
|
+
lineOrder;
|
|
22277
21808
|
constructor(host, store, timings = COMMUNITY_TIMINGS, random = Math.random) {
|
|
22278
21809
|
this.host = host;
|
|
22279
21810
|
this.store = store;
|
|
22280
21811
|
this.timings = timings;
|
|
22281
21812
|
this.random = random;
|
|
21813
|
+
this.lineOrder = new GroupLineOrder(host);
|
|
22282
21814
|
}
|
|
22283
21815
|
/** Application frames and pair payloads handed to the engine, one after the other per group. */
|
|
22284
21816
|
deliveries = /* @__PURE__ */ new Map();
|
|
@@ -22377,6 +21909,7 @@ var Communities = class {
|
|
|
22377
21909
|
accepted: true,
|
|
22378
21910
|
viaLink: true,
|
|
22379
21911
|
stage: this.joinStage(group),
|
|
21912
|
+
since: group.joining.since,
|
|
22380
21913
|
...this.knockingWith.get(group.id) ? { waiting: this.knockingWith.get(group.id) } : {}
|
|
22381
21914
|
} : void 0;
|
|
22382
21915
|
if (invitation && !this.keptWhileKnocking(live)) return [{
|
|
@@ -22590,22 +22123,45 @@ var Communities = class {
|
|
|
22590
22123
|
/**
|
|
22591
22124
|
* Leaves: the request goes to the hubs, which keep it and commit it (any member can), and the
|
|
22592
22125
|
* group goes from this device at once. An admin hands the role to a member first. Something must
|
|
22593
|
-
* be connected to carry the request.
|
|
22126
|
+
* be connected to carry the request. Answers the member the role went to, when it went to one.
|
|
22594
22127
|
*/
|
|
22595
22128
|
async leave(groupId) {
|
|
22596
22129
|
const live = this.live.get(groupId);
|
|
22597
|
-
if (!live)
|
|
22130
|
+
if (!live) {
|
|
22131
|
+
await this.forget(groupId);
|
|
22132
|
+
return;
|
|
22133
|
+
}
|
|
22598
22134
|
const s = live.session;
|
|
22599
|
-
if (s.status !== "active" || s.others.length === 0)
|
|
22135
|
+
if (s.status !== "active" || s.others.length === 0) {
|
|
22136
|
+
await this.forget(groupId);
|
|
22137
|
+
return;
|
|
22138
|
+
}
|
|
22600
22139
|
const edges = [...this.host.edges(groupId).entries()].filter(([, id]) => this.host.linkReady(id, 2));
|
|
22601
22140
|
if (!edges.length) throw new Error("Nobody in the group is connected right now to take your leave. Try again in a moment.");
|
|
22141
|
+
let next;
|
|
22602
22142
|
if (s.isAdmin) {
|
|
22603
22143
|
const connected = new Set(edges.map(([key]) => key));
|
|
22604
|
-
|
|
22144
|
+
next = s.others.find((key) => connected.has(key)) ?? s.others[0];
|
|
22605
22145
|
await s.transferAdmin(next);
|
|
22606
22146
|
}
|
|
22607
22147
|
await s.leave();
|
|
22608
22148
|
await this.forget(groupId);
|
|
22149
|
+
return next;
|
|
22150
|
+
}
|
|
22151
|
+
/**
|
|
22152
|
+
* Delete from this device: left first, as Leave does. With nothing connected to carry the request, a member's leave
|
|
22153
|
+
* goes unsaid and the group goes anyway, as a private group's failed goodbye does: the others still list the device,
|
|
22154
|
+
* but members who never open the app again kept it here for good. An admin with members is still refused: gone with
|
|
22155
|
+
* the role, the group would have no admin.
|
|
22156
|
+
*/
|
|
22157
|
+
async deleteHere(groupId) {
|
|
22158
|
+
try {
|
|
22159
|
+
await this.leave(groupId);
|
|
22160
|
+
} catch (error) {
|
|
22161
|
+
const s = this.live.get(groupId)?.session;
|
|
22162
|
+
if (!s || s.isAdmin && s.others.length) throw error;
|
|
22163
|
+
await this.forget(groupId);
|
|
22164
|
+
}
|
|
22609
22165
|
}
|
|
22610
22166
|
successor(groupId) {
|
|
22611
22167
|
const s = this.live.get(groupId)?.session;
|
|
@@ -22677,6 +22233,7 @@ var Communities = class {
|
|
|
22677
22233
|
this.lastMentionAt.delete(groupId);
|
|
22678
22234
|
this.knockingWith.delete(groupId);
|
|
22679
22235
|
this.knockRead.delete(groupId);
|
|
22236
|
+
this.lineOrder.forget(MESSAGE_LINK$1(groupId));
|
|
22680
22237
|
for (const linkId of [...this.host.edges(groupId).values(), ...this.host.entries(groupId).values()]) await this.host.closeEdge(linkId);
|
|
22681
22238
|
await this.store.deleteGroup(groupId);
|
|
22682
22239
|
await this.files?.drop(groupId);
|
|
@@ -23727,7 +23284,7 @@ var Communities = class {
|
|
|
23727
23284
|
const live = this.live.get(groupId);
|
|
23728
23285
|
if (!live || live.session.wasRemoved(peerKey)) return;
|
|
23729
23286
|
if (live.hub && !this.peerIsHub(live, peerKey) && rosterHas(live.session.roster, peerKey)) live.members.set(peerKey, this.now());
|
|
23730
|
-
this.sendTo(linkId, live.session.syncFrame());
|
|
23287
|
+
this.sendTo(linkId, live.session.syncFrame(peerKey));
|
|
23731
23288
|
this.host.emit();
|
|
23732
23289
|
}
|
|
23733
23290
|
edgeNick(groupId, peerKey, nick) {
|
|
@@ -23763,6 +23320,11 @@ var Communities = class {
|
|
|
23763
23320
|
sender: m.sender,
|
|
23764
23321
|
file: m.file
|
|
23765
23322
|
}, session.myKey) : void 0;
|
|
23323
|
+
const placing = this.lineOrder.placing(MESSAGE_LINK$1(id), {
|
|
23324
|
+
id: m.id,
|
|
23325
|
+
epoch: m.epoch,
|
|
23326
|
+
line: false
|
|
23327
|
+
}, this.now());
|
|
23766
23328
|
const stored = await this.host.storeMessage({
|
|
23767
23329
|
linkId: MESSAGE_LINK$1(id),
|
|
23768
23330
|
id: m.id,
|
|
@@ -23777,6 +23339,7 @@ var Communities = class {
|
|
|
23777
23339
|
...m.card && { card: m.card },
|
|
23778
23340
|
...file && { file }
|
|
23779
23341
|
});
|
|
23342
|
+
placing(stored !== false);
|
|
23780
23343
|
const came = m.sender === session.myKey ? timestamp : cameAt(timestamp, stored, arrivalNow(this.now()));
|
|
23781
23344
|
this.lastMessageAt.set(id, Math.max(this.lastMessageAt.get(id) ?? 0, came));
|
|
23782
23345
|
if (mentioned) this.lastMentionAt.set(id, Math.max(this.lastMentionAt.get(id) ?? 0, came));
|
|
@@ -23873,7 +23436,16 @@ var Communities = class {
|
|
|
23873
23436
|
else if (s.status === "active" && before !== after) {
|
|
23874
23437
|
const at = receivedTimestamp(top.ts, when);
|
|
23875
23438
|
for (const [key] of after) if (!rosterHas(before, key) && key !== s.myKey) await this.event(groupId, "joined", `${name(key)} joined`, at, top.e, key);
|
|
23876
|
-
|
|
23439
|
+
const left = (key) => {
|
|
23440
|
+
const chain = s.state.chain;
|
|
23441
|
+
for (let i = chain.length - 1; i >= 0; i--) if ((chain[i].k === "remove" || chain[i].k === "leave") && chain[i].s === key) return chain[i].k === "leave";
|
|
23442
|
+
return false;
|
|
23443
|
+
};
|
|
23444
|
+
for (const [key] of before) {
|
|
23445
|
+
if (rosterHas(after, key)) continue;
|
|
23446
|
+
if (left(key)) await this.event(groupId, "left", `${name(key)} left`, at, top.e, key);
|
|
23447
|
+
else await this.event(groupId, "gone", `${name(key)} is no longer a member`, at, top.e, key);
|
|
23448
|
+
}
|
|
23877
23449
|
if (top.k === "role" && before.find(([k]) => k === top.s)?.[1] !== "admin") await this.event(groupId, "admin", `${name(top.s)} ${top.s === s.myKey ? "are" : "is"} now the admin`, at, top.e, top.s);
|
|
23878
23450
|
if (top.k === "rotate") await this.event(groupId, "rotated", "Keys rotated: a fresh epoch", at, top.e);
|
|
23879
23451
|
}
|
|
@@ -23882,16 +23454,21 @@ var Communities = class {
|
|
|
23882
23454
|
/** A line in the group's history. Its id carries its time: two lines at the same moment get distinct ones (`eventTime`). */
|
|
23883
23455
|
async event(groupId, event, text, timestamp, epoch, member) {
|
|
23884
23456
|
timestamp = eventTime(this.eventTimes, groupId, timestamp);
|
|
23885
|
-
|
|
23457
|
+
const id = `event:${epoch}:${event}:${member ?? ""}:${timestamp}`;
|
|
23458
|
+
this.lineOrder.placing(MESSAGE_LINK$1(groupId), {
|
|
23459
|
+
id,
|
|
23460
|
+
epoch,
|
|
23461
|
+
line: true
|
|
23462
|
+
}, this.now())(await this.host.storeMessage({
|
|
23886
23463
|
linkId: MESSAGE_LINK$1(groupId),
|
|
23887
|
-
id
|
|
23464
|
+
id,
|
|
23888
23465
|
text,
|
|
23889
23466
|
sender: "peer",
|
|
23890
23467
|
event,
|
|
23891
23468
|
member,
|
|
23892
23469
|
timestamp,
|
|
23893
23470
|
via: "datalink"
|
|
23894
|
-
});
|
|
23471
|
+
}) !== false);
|
|
23895
23472
|
}
|
|
23896
23473
|
eventTimes = /* @__PURE__ */ new Map();
|
|
23897
23474
|
};
|
|
@@ -24899,6 +24476,11 @@ var INVITED_KEPT_MS = LEFT_KEPT_MS;
|
|
|
24899
24476
|
*/
|
|
24900
24477
|
var ACCEPT_AGAIN_MS = 6e4;
|
|
24901
24478
|
var ACCEPT_AGAINS = 5;
|
|
24479
|
+
/**
|
|
24480
|
+
* Admin side: a member let in over its chat, its edge not up, is sent its welcome again at most this often. The welcome
|
|
24481
|
+
* is the whole chain and the sealed secrets (megabytes, tens of ms to build); its app says its accept once a minute.
|
|
24482
|
+
*/
|
|
24483
|
+
var REWELCOME_MS = ACCEPT_AGAIN_MS / 2;
|
|
24902
24484
|
/** Invitee side: the invitations one contact chat may have waiting here at a time. More are answered as declined. */
|
|
24903
24485
|
var MAX_INVITATIONS_PER_CHAT = 4;
|
|
24904
24486
|
/** What an invitation says of its group, kept as a claim within what a group can be: an epoch from 0, a roster of 1 to the most a group holds. */
|
|
@@ -24915,9 +24497,13 @@ var FAREWELL_ANSWERS = 3;
|
|
|
24915
24497
|
var FAREWELL_COMMITS = 64;
|
|
24916
24498
|
/**
|
|
24917
24499
|
* A member whose messages were just asked of another (`Groups.askOf`) is told where I am only once that one's answer
|
|
24918
|
-
* has come: no message
|
|
24500
|
+
* has come: no message of it for this long, or `MESH_SYNC_HOLD_MAX_MS` after it was asked. Told at once, its own
|
|
24919
24501
|
* answer raced the other's, which holds the same messages among everyone's in the order they were written, and a
|
|
24920
24502
|
* member back in a group of five read each member's lines apart, the newest above older ones (WISP 902 § Catch-up).
|
|
24503
|
+
* A message of the answer is one written before the asking: what the member writes meanwhile is no reason to wait, so
|
|
24504
|
+
* the longest wait only has to cover a whole answer (the 256 messages a member keeps for the others and its own last
|
|
24505
|
+
* 32) at the pace of a phone with a long history, 600 ms a frame. At ten seconds, an answer of 200 messages at four a
|
|
24506
|
+
* second was cut at the 40th, and the others' newest lines landed above what was still coming.
|
|
24921
24507
|
*/
|
|
24922
24508
|
var MESH_SYNC_HOLD_MS = 1500;
|
|
24923
24509
|
/** Where a member's gossip turns start among the members it is connected to: a number of its own, from its key. */
|
|
@@ -25031,6 +24617,8 @@ var Groups = class {
|
|
|
25031
24617
|
syncHeld = /* @__PURE__ */ new Map();
|
|
25032
24618
|
/** Per mesh group and member, when a message last came over its edge (its own, or one it handed on). */
|
|
25033
24619
|
lastHanded = /* @__PURE__ */ new Map();
|
|
24620
|
+
/** Admin side, per group and member let in: when its welcome was last sent again, over its chat or the link (`REWELCOME_MS`). */
|
|
24621
|
+
rewelcomed = /* @__PURE__ */ new Map();
|
|
25034
24622
|
lastGossip = /* @__PURE__ */ new Map();
|
|
25035
24623
|
/**
|
|
25036
24624
|
* Per mesh group, each reachable member some of whose messages never reached me (`GroupSession.lacking`): what was
|
|
@@ -25062,6 +24650,10 @@ var Groups = class {
|
|
|
25062
24650
|
communities;
|
|
25063
24651
|
/** My messages and edits an edge of a private group took. */
|
|
25064
24652
|
frames = new FramesTaken();
|
|
24653
|
+
/** Each member's messages in the order that member wrote them, though catch-up answers race (WISP 400, requirement 10). */
|
|
24654
|
+
authorOrder;
|
|
24655
|
+
/** The lines of each history among the messages of their moment (`GroupLineOrder`). */
|
|
24656
|
+
lineOrder;
|
|
25065
24657
|
/** Hubs of the private groups past 16 members (WISP 902 · Group Mesh § Hubs). */
|
|
25066
24658
|
hubs;
|
|
25067
24659
|
/** Groups whose edges are being reconciled because their hubs changed: one at a time. */
|
|
@@ -25088,6 +24680,16 @@ var Groups = class {
|
|
|
25088
24680
|
}
|
|
25089
24681
|
}, hubTimings);
|
|
25090
24682
|
this.typings = new GroupTypings(host);
|
|
24683
|
+
this.lineOrder = new GroupLineOrder(host);
|
|
24684
|
+
const placeAgain = host.placeAgain && (async (linkId, ids) => {
|
|
24685
|
+
const done = host.placeAgain(linkId, ids);
|
|
24686
|
+
this.lineOrder.moved(linkId, ids);
|
|
24687
|
+
await done;
|
|
24688
|
+
});
|
|
24689
|
+
this.authorOrder = new GroupAuthorOrder({ placeAgain }, MESSAGE_LINK, (groupId, sender) => {
|
|
24690
|
+
const session = this.sessions.get(groupId);
|
|
24691
|
+
return !!session && session.missing(sender, this.gapsGone.get(groupId)?.get(sender)?.[session.epoch]) > 0;
|
|
24692
|
+
});
|
|
25091
24693
|
this.signals = new MeshSignals(host, {
|
|
25092
24694
|
session: (id) => this.sessions.get(id),
|
|
25093
24695
|
stored: (id) => this.stored.get(id),
|
|
@@ -25125,13 +24727,14 @@ var Groups = class {
|
|
|
25125
24727
|
await this.communities.load(all.filter((g) => g.community || g.joining));
|
|
25126
24728
|
for (const group of all.filter((g) => !g.community && !g.joining)) {
|
|
25127
24729
|
this.stored.set(group.id, group);
|
|
25128
|
-
if (group.state) this.attach(group.state);
|
|
25129
|
-
if (group.invited) this.invited.set(group.id, new Set(Object.keys(group.invited)));
|
|
25130
24730
|
const history = await this.store.getMessages(MESSAGE_LINK(group.id)), said = history.filter((m) => !m.event && !m.groupPay);
|
|
25131
|
-
|
|
24731
|
+
const framed = history.filter((m) => m.frame);
|
|
24732
|
+
this.framesKept.set(group.id, framed.map((m) => ({
|
|
25132
24733
|
id: m.id,
|
|
25133
24734
|
bytes: frameBytes(m.frame)
|
|
25134
24735
|
})));
|
|
24736
|
+
if (group.state) this.attach(group.state, framed.map((m) => m.frame));
|
|
24737
|
+
if (group.invited) this.invited.set(group.id, new Set(Object.keys(group.invited)));
|
|
25135
24738
|
const last = said[said.length - 1], lastPeer = [...said].reverse().find((m) => m.sender !== "me");
|
|
25136
24739
|
if (last) this.lastMessageAt.set(group.id, last.timestamp);
|
|
25137
24740
|
const peerAt = cameOrWritten(lastPeer?.timestamp, lastPeer ? group.came?.peer : void 0);
|
|
@@ -25208,6 +24811,7 @@ var Groups = class {
|
|
|
25208
24811
|
members: Math.min(Math.max(invitation.n, 0), GROUP_MEMBER_CAP.max),
|
|
25209
24812
|
accepted: !!invitation.seedB64,
|
|
25210
24813
|
...this.acceptSaid.get(group.id)?.unanswered ? { unanswered: true } : {},
|
|
24814
|
+
...invitation.takenBack ? { takenBack: true } : {},
|
|
25211
24815
|
...invitation.entry ? {
|
|
25212
24816
|
viaLink: true,
|
|
25213
24817
|
stage: this.joinStage(group)
|
|
@@ -25389,6 +24993,7 @@ var Groups = class {
|
|
|
25389
24993
|
async accept(groupId) {
|
|
25390
24994
|
const group = this.stored.get(groupId);
|
|
25391
24995
|
if (!group?.invitation) throw new Error("No invitation to accept");
|
|
24996
|
+
if (group.invitation.takenBack) throw new Error("This invitation was taken back");
|
|
25392
24997
|
if (!this.host.linkReady(group.invitation.linkId)) throw new Error("The contact who invited you is not connected. Try again when they are.");
|
|
25393
24998
|
const seedB64 = group.invitation.seedB64 ?? createIdentity().seedB64;
|
|
25394
24999
|
group.invitation = {
|
|
@@ -25412,6 +25017,7 @@ var Groups = class {
|
|
|
25412
25017
|
if (this.isCommunity(groupId)) return this.communities.decline(groupId);
|
|
25413
25018
|
const group = this.stored.get(groupId);
|
|
25414
25019
|
if (!group?.invitation) throw new Error("No invitation to decline");
|
|
25020
|
+
if (group.invitation.takenBack) return this.forget(groupId, false);
|
|
25415
25021
|
if (!group.previous) return this.forget(groupId);
|
|
25416
25022
|
await this.dropInvitation(group, this.sayDecline(groupId, group.invitation.linkId));
|
|
25417
25023
|
}
|
|
@@ -25427,6 +25033,25 @@ var Groups = class {
|
|
|
25427
25033
|
await this.store.putGroup(previous);
|
|
25428
25034
|
this.host.emit();
|
|
25429
25035
|
}
|
|
25036
|
+
/**
|
|
25037
|
+
* Invitee side: who sent the invitation took it back. A group I kept under its id comes back as it was; otherwise the
|
|
25038
|
+
* invitation stays a week, marked, so the list says it was taken back where it was (an accept said for it stops).
|
|
25039
|
+
*/
|
|
25040
|
+
async takeBackFrom(group) {
|
|
25041
|
+
if (group.previous) return this.dropInvitation(group);
|
|
25042
|
+
if (group.invitation.takenBack) return;
|
|
25043
|
+
const { seedB64: _unused, ...invitation } = group.invitation;
|
|
25044
|
+
group.invitation = {
|
|
25045
|
+
...invitation,
|
|
25046
|
+
pieces: [],
|
|
25047
|
+
takenBack: this.now()
|
|
25048
|
+
};
|
|
25049
|
+
this.acceptSaid.delete(group.id);
|
|
25050
|
+
await this.store.putGroup(group);
|
|
25051
|
+
const kept = [...this.stored.values()].filter((x) => x.invitation?.takenBack && !x.state && x.invitation.linkId === invitation.linkId).reverse().sort((a, b) => b.invitation.takenBack - a.invitation.takenBack);
|
|
25052
|
+
for (const old of kept.slice(MAX_INVITATIONS_PER_CHAT)) await this.forget(old.id, false);
|
|
25053
|
+
this.host.emit();
|
|
25054
|
+
}
|
|
25430
25055
|
/** Invitee side: says no to an invitation. With the inviter's chat down (its app is closed, say), returns the answer owed, said when that chat is next up. */
|
|
25431
25056
|
sayDecline(groupId, linkId) {
|
|
25432
25057
|
try {
|
|
@@ -25585,19 +25210,24 @@ var Groups = class {
|
|
|
25585
25210
|
* Leaves, and the group is gone from this device at once: its row, its history, its files and every
|
|
25586
25211
|
* edge but the one to the admin, which stays until the admin's commit removing me comes back (or a
|
|
25587
25212
|
* week passes), so a leave said while the admin was away still reaches it. An admin with other
|
|
25588
|
-
* members hands the role to one who is online first; alone, the group simply goes.
|
|
25213
|
+
* members hands the role to one who is online first; alone, the group simply goes. Answers the member the role went
|
|
25214
|
+
* to, when it went to one.
|
|
25589
25215
|
*/
|
|
25590
25216
|
async leave(groupId) {
|
|
25591
25217
|
if (this.isCommunity(groupId)) return this.communities.leave(groupId);
|
|
25592
25218
|
const session = this.session(groupId);
|
|
25593
25219
|
const group = this.stored.get(groupId);
|
|
25594
|
-
if (session.status !== "active")
|
|
25220
|
+
if (session.status !== "active") {
|
|
25221
|
+
await this.forget(groupId);
|
|
25222
|
+
return;
|
|
25223
|
+
}
|
|
25595
25224
|
if (session.others.length === 0) {
|
|
25596
25225
|
await session.leave();
|
|
25597
|
-
|
|
25226
|
+
await this.forget(groupId);
|
|
25227
|
+
return;
|
|
25598
25228
|
}
|
|
25229
|
+
const next = session.isAdmin ? this.successor(groupId) : void 0;
|
|
25599
25230
|
if (session.isAdmin) {
|
|
25600
|
-
const next = this.successor(groupId);
|
|
25601
25231
|
if (!next) throw new Error("You are the admin and nobody else in the group is online to take over. Try again when a member is, or make someone the admin first.");
|
|
25602
25232
|
await session.transferAdmin(next);
|
|
25603
25233
|
}
|
|
@@ -25619,6 +25249,8 @@ var Groups = class {
|
|
|
25619
25249
|
this.lastMessageAt.delete(groupId);
|
|
25620
25250
|
this.lastPeerMessageAt.delete(groupId);
|
|
25621
25251
|
this.lastMentionAt.delete(groupId);
|
|
25252
|
+
this.authorOrder.forget(groupId);
|
|
25253
|
+
this.lineOrder.forget(MESSAGE_LINK(groupId));
|
|
25622
25254
|
await this.store.deleteGroup(groupId);
|
|
25623
25255
|
await this.store.putGroup(group);
|
|
25624
25256
|
await this.files?.drop(groupId);
|
|
@@ -25649,6 +25281,7 @@ var Groups = class {
|
|
|
25649
25281
|
} catch {}
|
|
25650
25282
|
this.reconcileEdges(groupId);
|
|
25651
25283
|
this.host.emit();
|
|
25284
|
+
return next;
|
|
25652
25285
|
}
|
|
25653
25286
|
/** The admin heard me: the tombstone of a group I left can go. */
|
|
25654
25287
|
async leaveConfirmed(groupId) {
|
|
@@ -25812,19 +25445,20 @@ var Groups = class {
|
|
|
25812
25445
|
}
|
|
25813
25446
|
/** `answer`: false when an invitation goes because its inviter took it back, so no decline is said for it. */
|
|
25814
25447
|
async forget(groupId, answer = true) {
|
|
25815
|
-
if (this.isCommunity(groupId)) return this.communities.
|
|
25448
|
+
if (this.isCommunity(groupId)) return this.communities.deleteHere(groupId);
|
|
25816
25449
|
const session = this.sessions.get(groupId);
|
|
25817
25450
|
if (session?.status === "active") try {
|
|
25818
25451
|
await this.leave(groupId);
|
|
25819
25452
|
} catch (e) {
|
|
25820
25453
|
if (session.isAdmin && session.others.length) throw e;
|
|
25821
25454
|
}
|
|
25822
|
-
const gone = this.stored.get(groupId), invitation = answer && gone?.invitation && !gone.invitation.entry ? gone.invitation : void 0;
|
|
25455
|
+
const gone = this.stored.get(groupId), invitation = answer && gone?.invitation && !gone.invitation.entry && !gone.invitation.takenBack ? gone.invitation : void 0;
|
|
25823
25456
|
const owed = invitation && this.sayDecline(groupId, invitation.linkId);
|
|
25824
25457
|
this.sessions.delete(groupId);
|
|
25825
25458
|
this.stored.delete(groupId);
|
|
25826
25459
|
this.takeBack(groupId);
|
|
25827
25460
|
this.acceptSaid.delete(groupId);
|
|
25461
|
+
this.entryHosts.delete(groupId);
|
|
25828
25462
|
this.lastRoster.delete(groupId);
|
|
25829
25463
|
this.lastTold.delete(groupId);
|
|
25830
25464
|
this.pendingEntries.delete(groupId);
|
|
@@ -25838,6 +25472,7 @@ var Groups = class {
|
|
|
25838
25472
|
this.relayAsked.delete(groupId);
|
|
25839
25473
|
this.syncHeld.delete(groupId);
|
|
25840
25474
|
this.lastHanded.delete(groupId);
|
|
25475
|
+
this.rewelcomed.delete(groupId);
|
|
25841
25476
|
this.hubs.forget(groupId);
|
|
25842
25477
|
this.removedAt.delete(groupId);
|
|
25843
25478
|
this.lastGossip.delete(groupId);
|
|
@@ -25850,6 +25485,8 @@ var Groups = class {
|
|
|
25850
25485
|
this.hereHeard.delete(groupId);
|
|
25851
25486
|
this.hereActed.delete(groupId);
|
|
25852
25487
|
this.signals.forget(groupId);
|
|
25488
|
+
this.authorOrder.forget(groupId);
|
|
25489
|
+
this.lineOrder.forget(MESSAGE_LINK(groupId));
|
|
25853
25490
|
for (const linkId of [...this.host.edges(groupId).values(), ...this.host.entries(groupId).values()]) await this.host.closeEdge(linkId);
|
|
25854
25491
|
await this.store.deleteGroup(groupId);
|
|
25855
25492
|
if (owed && !this.stored.has(groupId)) {
|
|
@@ -25948,6 +25585,7 @@ var Groups = class {
|
|
|
25948
25585
|
if (this.ticking) return;
|
|
25949
25586
|
this.ticking = true;
|
|
25950
25587
|
try {
|
|
25588
|
+
this.authorOrder.tick(now);
|
|
25951
25589
|
for (const group of [...this.stored.values()]) {
|
|
25952
25590
|
if (group.left) {
|
|
25953
25591
|
if (now - group.left.at > LEFT_KEPT_MS) await this.forget(group.id);
|
|
@@ -25957,6 +25595,10 @@ var Groups = class {
|
|
|
25957
25595
|
await this.declineOwed(group, now);
|
|
25958
25596
|
if (!group.state) continue;
|
|
25959
25597
|
}
|
|
25598
|
+
if (group.invitation?.takenBack && !group.state) {
|
|
25599
|
+
if (now - group.invitation.takenBack > LEFT_KEPT_MS) await this.forget(group.id, false);
|
|
25600
|
+
continue;
|
|
25601
|
+
}
|
|
25960
25602
|
if (group.invitation?.entry && !group.state) {
|
|
25961
25603
|
const every = now - group.createdAt > this.timings.patienceMs ? this.timings.slowKnockMs : this.timings.knockMs;
|
|
25962
25604
|
if (!(this.host.linkReady(group.invitation.linkId) || !!this.host.linkSeen?.(group.invitation.linkId)) && (now - (this.lastKnock.get(group.id) ?? 0) >= every || now >= (this.knockCheckAt.get(group.id) ?? Infinity))) await this.knock(group, now).catch(() => {});
|
|
@@ -26075,7 +25717,7 @@ var Groups = class {
|
|
|
26075
25717
|
}
|
|
26076
25718
|
const session = this.sessions.get(groupId), group = this.stored.get(groupId);
|
|
26077
25719
|
if (!session?.isAdmin || !group || !this.pendingEntries.get(groupId)?.has(peer)) return;
|
|
26078
|
-
if (this.growthBlocked(group, session, session.roster.length + 1, linkId)) {
|
|
25720
|
+
if (!rosterHas(session.roster, peer) && this.growthBlocked(group, session, session.roster.length + 1, linkId)) {
|
|
26079
25721
|
this.pendingEntries.get(groupId)?.delete(peer);
|
|
26080
25722
|
this.refused.set(peer, Date.now() + REFUSED_FOR_MS);
|
|
26081
25723
|
this.host.closeEdge(linkId);
|
|
@@ -26122,14 +25764,22 @@ var Groups = class {
|
|
|
26122
25764
|
return this.knocked.has(group.id) ? "knocked" : "knocking";
|
|
26123
25765
|
}
|
|
26124
25766
|
entryOf(group) {
|
|
26125
|
-
|
|
25767
|
+
if (!group.entry) return void 0;
|
|
25768
|
+
const { seedB64 } = group.entry;
|
|
25769
|
+
let host = this.entryHosts.get(group.id);
|
|
25770
|
+
if (host?.seedB64 !== seedB64) this.entryHosts.set(group.id, host = {
|
|
25771
|
+
seedB64,
|
|
25772
|
+
key: identityFromSeedB64(seedB64).pubKeyZ32
|
|
25773
|
+
});
|
|
25774
|
+
return {
|
|
26126
25775
|
link: {
|
|
26127
25776
|
g: group.id,
|
|
26128
|
-
host:
|
|
25777
|
+
host: host.key
|
|
26129
25778
|
},
|
|
26130
|
-
seedB64
|
|
26131
|
-
}
|
|
25779
|
+
seedB64
|
|
25780
|
+
};
|
|
26132
25781
|
}
|
|
25782
|
+
entryHosts = /* @__PURE__ */ new Map();
|
|
26133
25783
|
/** Joiner side: the entry session kept after the welcome goes (the edge to the admin is up, or it was kept long enough). */
|
|
26134
25784
|
async endLinger(groupId) {
|
|
26135
25785
|
const kept = this.lingering.get(groupId);
|
|
@@ -26194,11 +25844,13 @@ var Groups = class {
|
|
|
26194
25844
|
});
|
|
26195
25845
|
let pending = this.pendingEntries.get(group.id);
|
|
26196
25846
|
for (const { key, ts } of knocks) {
|
|
26197
|
-
|
|
25847
|
+
const again = this.unwelcomed(group, session, key);
|
|
25848
|
+
if (now - ts > 18e4 || rosterHas(session.roster, key) && !again || this.refused.has(key) || pending?.has(key) || key === entry.link.host) continue;
|
|
25849
|
+
if (again && this.rewelcomedLately(group.id, key)) continue;
|
|
26198
25850
|
const entryIds = new Set(this.host.entries(group.id).values());
|
|
26199
25851
|
const contactsInvited = [...this.invited.get(group.id) ?? []].filter((id) => !entryIds.has(id)).length;
|
|
26200
25852
|
const size = session.roster.length + contactsInvited + (pending?.size ?? 0);
|
|
26201
|
-
if ((pending?.size ?? 0) >= MAX_PENDING_ENTRIES || size >= GROUP_MEMBER_CAP.max || this.growthBlocked(group, session, size + 1)) break;
|
|
25853
|
+
if ((pending?.size ?? 0) >= MAX_PENDING_ENTRIES || !again && (size >= GROUP_MEMBER_CAP.max || this.growthBlocked(group, session, size + 1))) break;
|
|
26202
25854
|
if (!pending) this.pendingEntries.set(group.id, pending = /* @__PURE__ */ new Map());
|
|
26203
25855
|
pending.set(key, now);
|
|
26204
25856
|
this.warmUntil.set(group.id, now + this.timings.warmMs);
|
|
@@ -26258,9 +25910,9 @@ var Groups = class {
|
|
|
26258
25910
|
}
|
|
26259
25911
|
if (existing?.invitation?.seedB64 && (existing.invitation.linkId !== linkId || existing.invitation.admin !== frame.admin)) return;
|
|
26260
25912
|
if (existing?.left && (existing.left.admin !== frame.admin || existing.contacts?.[existing.left.admin] !== linkId)) return;
|
|
26261
|
-
if (existing?.invitation && !existing.state && existing.invitation.linkId !== linkId && !existing.invitation.genesis) return;
|
|
26262
|
-
if ([...this.stored.values()].filter((x) => x.invitation).length >= 32) return;
|
|
26263
|
-
if (!existing?.invitation && [...this.stored.values()].filter((x) => x.invitation?.linkId === linkId).length >= MAX_INVITATIONS_PER_CHAT) {
|
|
25913
|
+
if (existing?.invitation && !existing.state && existing.invitation.linkId !== linkId && !existing.invitation.genesis && !existing.invitation.takenBack) return;
|
|
25914
|
+
if ([...this.stored.values()].filter((x) => x.invitation && !x.invitation.takenBack).length >= 32) return;
|
|
25915
|
+
if (!(existing?.invitation && !existing.invitation.takenBack && existing.invitation.linkId === linkId) && [...this.stored.values()].filter((x) => x.invitation?.linkId === linkId && !x.invitation.takenBack).length >= MAX_INVITATIONS_PER_CHAT) {
|
|
26264
25916
|
try {
|
|
26265
25917
|
this.host.sendOnLink(linkId, {
|
|
26266
25918
|
t: "group-decline",
|
|
@@ -26312,6 +25964,8 @@ var Groups = class {
|
|
|
26312
25964
|
} catch {}
|
|
26313
25965
|
const edge = this.host.edges(g).get(frame.key);
|
|
26314
25966
|
if (letIn && session.isAdmin && !(edge && this.host.linkReady(edge))) {
|
|
25967
|
+
if (this.rewelcomedLately(g, frame.key)) return;
|
|
25968
|
+
this.noteRewelcome(g, frame.key);
|
|
26315
25969
|
const welcome = await session.rewelcome(frame.key).catch(() => []);
|
|
26316
25970
|
try {
|
|
26317
25971
|
for (const piece of welcome) this.host.sendOnLink(linkId, piece);
|
|
@@ -26320,18 +25974,35 @@ var Groups = class {
|
|
|
26320
25974
|
return;
|
|
26321
25975
|
}
|
|
26322
25976
|
const group = this.stored.get(g);
|
|
26323
|
-
if (frame.key === session.myKey
|
|
25977
|
+
if (frame.key === session.myKey) return;
|
|
26324
25978
|
const entryPeer = this.hostEntry(g, linkId);
|
|
26325
25979
|
if ([...this.host.entries(g).values()].includes(linkId) && entryPeer !== frame.key) return;
|
|
26326
|
-
|
|
25980
|
+
const again = !!entryPeer && this.unwelcomed(group, session, frame.key);
|
|
25981
|
+
if (rosterHas(session.roster, frame.key) && !again) return;
|
|
25982
|
+
if (again && this.rewelcomedLately(g, frame.key)) {
|
|
25983
|
+
this.pendingEntries.get(g)?.delete(frame.key);
|
|
25984
|
+
await this.host.closeEdge(linkId);
|
|
25985
|
+
return;
|
|
25986
|
+
}
|
|
25987
|
+
if (again) this.noteRewelcome(g, frame.key);
|
|
25988
|
+
if (!again && (session.roster.length >= GROUP_MEMBER_CAP.max || this.growthBlocked(group, session, session.roster.length + 1, linkId))) return;
|
|
26327
25989
|
if (!entryPeer) await session.setNick(frame.key, this.host.contactName(linkId));
|
|
26328
|
-
this.meet(g, frame.key);
|
|
25990
|
+
if (!again) this.meet(g, frame.key);
|
|
26329
25991
|
if (this.host.linkReady(linkId, 3)) this.markLarge(group, frame.key);
|
|
26330
|
-
|
|
25992
|
+
let welcome;
|
|
25993
|
+
try {
|
|
25994
|
+
welcome = again ? await session.rewelcome(frame.key) : await session.admit(frame.key);
|
|
25995
|
+
} catch (error) {
|
|
25996
|
+
if (!again) throw error;
|
|
25997
|
+
this.pendingEntries.get(g)?.delete(frame.key);
|
|
25998
|
+
await this.host.closeEdge(linkId);
|
|
25999
|
+
return;
|
|
26000
|
+
}
|
|
26331
26001
|
if (!entryPeer) group.contacts = {
|
|
26332
26002
|
...group.contacts,
|
|
26333
26003
|
[frame.key]: linkId
|
|
26334
26004
|
};
|
|
26005
|
+
if (!again) group.unseen = [...(group.unseen ?? []).filter((key) => rosterHas(session.roster, key)), frame.key];
|
|
26335
26006
|
this.answered(group, linkId);
|
|
26336
26007
|
await this.store.putGroup(group);
|
|
26337
26008
|
for (const piece of welcome) this.host.sendOnLink(linkId, piece);
|
|
@@ -26354,7 +26025,7 @@ var Groups = class {
|
|
|
26354
26025
|
case "group-decline": {
|
|
26355
26026
|
const mine = this.stored.get(g);
|
|
26356
26027
|
if (mine?.invitation && !mine.invitation.entry && !mine.state && mine.invitation.linkId === linkId) {
|
|
26357
|
-
await this.
|
|
26028
|
+
await this.takeBackFrom(mine);
|
|
26358
26029
|
return;
|
|
26359
26030
|
}
|
|
26360
26031
|
const entryPeer = this.hostEntry(g, linkId);
|
|
@@ -26363,6 +26034,7 @@ var Groups = class {
|
|
|
26363
26034
|
await this.host.closeEdge(linkId);
|
|
26364
26035
|
}
|
|
26365
26036
|
const group = this.stored.get(g);
|
|
26037
|
+
if (group && await this.declinedUnseen(g, group, linkId)) return;
|
|
26366
26038
|
if (!group || !this.answered(group, linkId)) return;
|
|
26367
26039
|
await this.store.putGroup(group);
|
|
26368
26040
|
this.host.emit();
|
|
@@ -26526,7 +26198,8 @@ var Groups = class {
|
|
|
26526
26198
|
return;
|
|
26527
26199
|
}
|
|
26528
26200
|
const taken = await session.handle(peerKey, frame);
|
|
26529
|
-
|
|
26201
|
+
const askedAt = this.askedAt(groupId, peerKey);
|
|
26202
|
+
if (taken.some((f) => f.t === "group-msg" && f.ts < askedAt)) {
|
|
26530
26203
|
let handed = this.lastHanded.get(groupId);
|
|
26531
26204
|
if (!handed) this.lastHanded.set(groupId, handed = /* @__PURE__ */ new Map());
|
|
26532
26205
|
handed.set(peerKey, this.now());
|
|
@@ -26599,9 +26272,14 @@ var Groups = class {
|
|
|
26599
26272
|
return;
|
|
26600
26273
|
}
|
|
26601
26274
|
if (!session || !group || session.status !== "active" || !rosterHas(session.roster, peerKey)) return;
|
|
26275
|
+
const seen = !!group.unseen?.includes(peerKey);
|
|
26276
|
+
if (seen) {
|
|
26277
|
+
const unseen = group.unseen.filter((key) => key !== peerKey);
|
|
26278
|
+
group.unseen = unseen.length ? unseen : void 0;
|
|
26279
|
+
}
|
|
26602
26280
|
const large = this.host.linkReady(linkId, 3) && this.markLarge(group, peerKey);
|
|
26603
26281
|
const legacy = this.hubs.enabled && this.hubs.edgeReady(groupId, session, group, peerKey, this.host.linkReady(linkId, 4), this.now());
|
|
26604
|
-
if (large || legacy) this.store.putGroup(group).catch(() => {});
|
|
26282
|
+
if (large || legacy || seen) this.store.putGroup(group).catch(() => {});
|
|
26605
26283
|
if (!this.holdSync(groupId, peerKey)) try {
|
|
26606
26284
|
this.host.sendOnLink(linkId, session.syncFrame(this.askOf(groupId, session, peerKey)));
|
|
26607
26285
|
} catch {
|
|
@@ -26624,6 +26302,41 @@ var Groups = class {
|
|
|
26624
26302
|
this.host.edgeUp?.(groupId, peerKey);
|
|
26625
26303
|
this.host.emit();
|
|
26626
26304
|
}
|
|
26305
|
+
/**
|
|
26306
|
+
* Admin side: a `group-decline` over the contact chat that let in a member never seen on an edge since. Its welcome
|
|
26307
|
+
* was lost and its app declined the invitation it still showed: it never had the group's keys, so it goes from the
|
|
26308
|
+
* roster (and may be invited again). True when one went.
|
|
26309
|
+
*/
|
|
26310
|
+
async declinedUnseen(groupId, group, linkId) {
|
|
26311
|
+
const session = this.sessions.get(groupId);
|
|
26312
|
+
if (!session?.isAdmin || session.status !== "active" || !group.unseen) return false;
|
|
26313
|
+
const key = group.unseen.find((k) => group.contacts?.[k] === linkId && rosterHas(session.roster, k));
|
|
26314
|
+
if (!key) return false;
|
|
26315
|
+
const edge = this.host.edges(groupId).get(key);
|
|
26316
|
+
if (edge && this.host.linkReady(edge)) return false;
|
|
26317
|
+
const unseen = group.unseen.filter((k) => k !== key);
|
|
26318
|
+
group.unseen = unseen.length ? unseen : void 0;
|
|
26319
|
+
await this.remove(groupId, key).catch(() => {});
|
|
26320
|
+
await this.store.putGroup(group);
|
|
26321
|
+
this.host.emit();
|
|
26322
|
+
return true;
|
|
26323
|
+
}
|
|
26324
|
+
/** Admin side: a member in the roster never seen on an edge since it was let in, whose edge is not up now. */
|
|
26325
|
+
/** This member was sent its welcome again less than `REWELCOME_MS` ago, over its chat or through the link. */
|
|
26326
|
+
rewelcomedLately(groupId, key) {
|
|
26327
|
+
const last = this.rewelcomed.get(groupId)?.get(key);
|
|
26328
|
+
return last !== void 0 && this.now() - last < REWELCOME_MS;
|
|
26329
|
+
}
|
|
26330
|
+
noteRewelcome(groupId, key) {
|
|
26331
|
+
let sent = this.rewelcomed.get(groupId);
|
|
26332
|
+
if (!sent) this.rewelcomed.set(groupId, sent = /* @__PURE__ */ new Map());
|
|
26333
|
+
sent.set(key, this.now());
|
|
26334
|
+
}
|
|
26335
|
+
unwelcomed(group, session, key) {
|
|
26336
|
+
if (!session.isAdmin || !group.unseen?.includes(key) || !rosterHas(session.roster, key)) return false;
|
|
26337
|
+
const edge = this.host.edges(group.id).get(key);
|
|
26338
|
+
return !(edge && this.host.linkReady(edge));
|
|
26339
|
+
}
|
|
26627
26340
|
/** Records that a member's app takes rosters past eight; true when that is news (the caller saves). */
|
|
26628
26341
|
markLarge(group, key) {
|
|
26629
26342
|
if (group.large?.includes(key)) return false;
|
|
@@ -26731,13 +26444,19 @@ var Groups = class {
|
|
|
26731
26444
|
if (out.length) this.lastGossip.set(groupId, now);
|
|
26732
26445
|
return out;
|
|
26733
26446
|
}
|
|
26447
|
+
/** When `via` was last asked for another member's messages (`askOf`), or 0: what it sends written before that is its answer. */
|
|
26448
|
+
askedAt(groupId, via) {
|
|
26449
|
+
let at = 0;
|
|
26450
|
+
for (const asked of this.relayAsked.get(groupId)?.values() ?? []) if (asked.via === via && asked.at > at) at = asked.at;
|
|
26451
|
+
return at;
|
|
26452
|
+
}
|
|
26734
26453
|
/**
|
|
26735
26454
|
* Whether the answer of the member asked for someone's messages may still be coming: its edge up, asked under
|
|
26736
|
-
* `MESH_SYNC_HOLD_MAX_MS` ago, and a message
|
|
26455
|
+
* `MESH_SYNC_HOLD_MAX_MS` ago, and a message of its answer (or the asking) under `MESH_SYNC_HOLD_MS` ago.
|
|
26737
26456
|
*/
|
|
26738
26457
|
answering(groupId, { via, at }, now) {
|
|
26739
26458
|
const edge = this.host.edges(groupId).get(via);
|
|
26740
|
-
if (!edge || !this.host.linkReady(edge) || now - at >=
|
|
26459
|
+
if (!edge || !this.host.linkReady(edge) || now - at >= 18e4) return false;
|
|
26741
26460
|
return now - Math.max(at, this.lastHanded.get(groupId)?.get(via) ?? 0) < MESH_SYNC_HOLD_MS;
|
|
26742
26461
|
}
|
|
26743
26462
|
/**
|
|
@@ -26757,21 +26476,41 @@ var Groups = class {
|
|
|
26757
26476
|
held.set(peerKey, { ...asked });
|
|
26758
26477
|
return true;
|
|
26759
26478
|
}
|
|
26760
|
-
/**
|
|
26479
|
+
/**
|
|
26480
|
+
* The syncs that waited for another member's answer (`holdSync`), once it has come: for what it did not hand on.
|
|
26481
|
+
* One member at a time: the first is told and asked for the others too, which wait for its answer in turn. Told
|
|
26482
|
+
* together, each answered with its own messages alone, and when the member asked first had been away too (it held
|
|
26483
|
+
* little of what was missed) a conversation came as one member's lines, then the next's.
|
|
26484
|
+
*/
|
|
26761
26485
|
heldSyncs(groupId, session, now) {
|
|
26762
26486
|
const held = this.syncHeld.get(groupId);
|
|
26763
26487
|
if (!held?.size) return;
|
|
26764
|
-
const edges = this.host.edges(groupId);
|
|
26765
|
-
|
|
26766
|
-
|
|
26488
|
+
const edges = this.host.edges(groupId), asked = this.relayAsked.get(groupId);
|
|
26489
|
+
const due = [];
|
|
26490
|
+
for (const [key, of] of held) {
|
|
26491
|
+
if (this.answering(groupId, of, now)) continue;
|
|
26767
26492
|
held.delete(key);
|
|
26768
|
-
if (
|
|
26493
|
+
if (asked?.get(key)?.via === of.via) asked.delete(key);
|
|
26769
26494
|
const edge = edges.get(key);
|
|
26770
|
-
if (
|
|
26771
|
-
|
|
26772
|
-
|
|
26773
|
-
|
|
26495
|
+
if (edge && this.host.linkReady(edge) && rosterHas(session.roster, key)) due.push(key);
|
|
26496
|
+
}
|
|
26497
|
+
const [next, ...rest] = due;
|
|
26498
|
+
if (!next) return;
|
|
26499
|
+
const ask = new Set(this.askOf(groupId, session, next));
|
|
26500
|
+
for (const key of rest) {
|
|
26501
|
+
ask.add(key);
|
|
26502
|
+
asked?.set(key, {
|
|
26503
|
+
via: next,
|
|
26504
|
+
at: now
|
|
26505
|
+
});
|
|
26506
|
+
held.set(key, {
|
|
26507
|
+
via: next,
|
|
26508
|
+
at: now
|
|
26509
|
+
});
|
|
26774
26510
|
}
|
|
26511
|
+
try {
|
|
26512
|
+
this.host.sendOnLink(edges.get(next), session.syncFrame([...ask]));
|
|
26513
|
+
} catch {}
|
|
26775
26514
|
}
|
|
26776
26515
|
/**
|
|
26777
26516
|
* Reachable members whose messages below the highest seen never reached me, for `via` to hand on: their own answer
|
|
@@ -27024,7 +26763,8 @@ var Groups = class {
|
|
|
27024
26763
|
if (!session) throw new Error("Group not found");
|
|
27025
26764
|
return session;
|
|
27026
26765
|
}
|
|
27027
|
-
|
|
26766
|
+
/** `frames`: what the group's rows hold to hand on, oldest first (none yet for a group just made, joined or given back). */
|
|
26767
|
+
attach(state, frames = []) {
|
|
27028
26768
|
const session = new GroupSession(state, {
|
|
27029
26769
|
clock: () => this.now(),
|
|
27030
26770
|
seqFloor: () => this.host.seqFloor?.() ?? 0,
|
|
@@ -27078,8 +26818,21 @@ var Groups = class {
|
|
|
27078
26818
|
...m.frame && { frame: m.frame }
|
|
27079
26819
|
};
|
|
27080
26820
|
const completed = !!m.completes && !!this.host.completeMessage;
|
|
26821
|
+
const placing = completed ? void 0 : this.lineOrder.placing(MESSAGE_LINK(state.id), {
|
|
26822
|
+
id: m.id,
|
|
26823
|
+
epoch: m.epoch,
|
|
26824
|
+
line: false
|
|
26825
|
+
}, this.now());
|
|
27081
26826
|
const stored = completed ? (await this.host.completeMessage(message), false) : await this.host.storeMessage(message);
|
|
26827
|
+
placing?.(stored !== false);
|
|
27082
26828
|
if (m.frame && (completed || stored !== false)) await this.frameKept(state.id, m.id, m.frame);
|
|
26829
|
+
if (!completed && stored !== false) this.authorOrder.placed(state.id, {
|
|
26830
|
+
sender: m.sender,
|
|
26831
|
+
epoch: m.epoch,
|
|
26832
|
+
seq: m.seq,
|
|
26833
|
+
id: m.id,
|
|
26834
|
+
at: this.now()
|
|
26835
|
+
});
|
|
27083
26836
|
const came = m.sender === session.myKey ? timestamp : cameAt(timestamp, stored, arrivalNow(this.now()));
|
|
27084
26837
|
this.lastMessageAt.set(state.id, Math.max(this.lastMessageAt.get(state.id) ?? 0, came));
|
|
27085
26838
|
if (mentioned) this.lastMentionAt.set(state.id, Math.max(this.lastMentionAt.get(state.id) ?? 0, came));
|
|
@@ -27087,6 +26840,10 @@ var Groups = class {
|
|
|
27087
26840
|
noteCame(this.stored.get(state.id), came, m.sender !== session.myKey, mentioned);
|
|
27088
26841
|
this.typings.messageFrom(state.id, m.sender);
|
|
27089
26842
|
},
|
|
26843
|
+
kept: async (id, frame) => {
|
|
26844
|
+
await this.host.keepFrame?.(MESSAGE_LINK(state.id), id, frame).catch(() => {});
|
|
26845
|
+
await this.frameKept(state.id, id, frame);
|
|
26846
|
+
},
|
|
27090
26847
|
edit: (e) => this.host.groupEdit?.(state.id, e),
|
|
27091
26848
|
changed: () => {
|
|
27092
26849
|
if (session.status === "removed" && this.hubs.isHub(state.id) && !this.removedAt.has(state.id)) this.removedAt.set(state.id, this.now());
|
|
@@ -27099,7 +26856,7 @@ var Groups = class {
|
|
|
27099
26856
|
const edge = this.host.edges(state.id).get(to);
|
|
27100
26857
|
return edge && this.host.linkHandled ? this.host.linkHandled(edge) : Promise.resolve(false);
|
|
27101
26858
|
} }
|
|
27102
|
-
});
|
|
26859
|
+
}, frames);
|
|
27103
26860
|
this.sessions.set(state.id, session);
|
|
27104
26861
|
this.lastRoster.set(state.id, session.roster);
|
|
27105
26862
|
this.lastTold.set(state.id, {
|
|
@@ -27128,7 +26885,13 @@ var Groups = class {
|
|
|
27128
26885
|
for (const [key] of after) if (!rosterHas(before, key) && key !== session.myKey) await this.event(groupId, "joined", `${name(key)} joined`, when, top.e, key);
|
|
27129
26886
|
const gone = before.filter(([key]) => !rosterHas(after, key)).map(([key]) => key);
|
|
27130
26887
|
this.hubs.removed(groupId, gone, this.now());
|
|
27131
|
-
|
|
26888
|
+
const left = (key) => {
|
|
26889
|
+
const chain = session.state.chain;
|
|
26890
|
+
for (let e = chain.length - 1; e > 0; e--) if (chain[e].k === "remove" && chain[e].s === key) return !!session.state.leftMarks?.[e];
|
|
26891
|
+
return false;
|
|
26892
|
+
};
|
|
26893
|
+
for (const key of gone) if (left(key)) await this.event(groupId, "left", `${name(key)} left`, when, top.e, key);
|
|
26894
|
+
else await this.event(groupId, "gone", `${name(key)} is no longer a member`, when, top.e, key);
|
|
27132
26895
|
if (top.k === "role") await this.event(groupId, "admin", `${name(top.s)} ${top.s === session.myKey ? "are" : "is"} now the admin`, when, top.e, top.s);
|
|
27133
26896
|
if (top.k === "rotate") await this.event(groupId, "rotated", "Keys rotated: a fresh epoch", when, top.e);
|
|
27134
26897
|
}
|
|
@@ -27183,16 +26946,21 @@ var Groups = class {
|
|
|
27183
26946
|
/** A line in the group's history; lines at the same time (two members gone in one change) get distinct ones, see `eventTime`. */
|
|
27184
26947
|
async event(groupId, event, text, timestamp, epoch, member) {
|
|
27185
26948
|
timestamp = eventTime(this.eventTimes, groupId, timestamp);
|
|
27186
|
-
|
|
26949
|
+
const id = `event:${epoch}:${event}:${timestamp}`;
|
|
26950
|
+
this.lineOrder.placing(MESSAGE_LINK(groupId), {
|
|
26951
|
+
id,
|
|
26952
|
+
epoch,
|
|
26953
|
+
line: true
|
|
26954
|
+
}, this.now())(await this.host.storeMessage({
|
|
27187
26955
|
linkId: MESSAGE_LINK(groupId),
|
|
27188
|
-
id
|
|
26956
|
+
id,
|
|
27189
26957
|
text,
|
|
27190
26958
|
sender: "peer",
|
|
27191
26959
|
event,
|
|
27192
26960
|
member,
|
|
27193
26961
|
timestamp,
|
|
27194
26962
|
via: "datalink"
|
|
27195
|
-
});
|
|
26963
|
+
}) !== false);
|
|
27196
26964
|
}
|
|
27197
26965
|
eventTimes = /* @__PURE__ */ new Map();
|
|
27198
26966
|
};
|
|
@@ -29146,8 +28914,6 @@ function resumeHere(log, runs, peer) {
|
|
|
29146
28914
|
const shared = runs.find((transport) => peer?.includes(transport));
|
|
29147
28915
|
return shared ? { resume: shared } : {};
|
|
29148
28916
|
}
|
|
29149
|
-
/** A round trip that moved less than this is not news in the history. */
|
|
29150
|
-
var RTT_STEP_MS = 10;
|
|
29151
28917
|
var sequence = 0;
|
|
29152
28918
|
var lineId = (at) => `${at.toString(36)}-${(++sequence).toString(36)}`;
|
|
29153
28919
|
/** A row that says the chat is live on its transport from then on. */
|
|
@@ -29551,7 +29317,7 @@ var TransportLog = class {
|
|
|
29551
29317
|
changed = true;
|
|
29552
29318
|
}
|
|
29553
29319
|
const stretch = [...this.history].reverse().find((e) => e.kind === "live" || e.kind === "switched" || e.kind === "down");
|
|
29554
|
-
if (stretch && stretch.kind !== "down" && stretch.transport === on && (stretch.rttMs === void 0 || Math.abs(stretch.rttMs - ms) >=
|
|
29320
|
+
if (stretch && stretch.kind !== "down" && stretch.transport === on && (stretch.rttMs === void 0 || Math.abs(stretch.rttMs - ms) >= 10)) {
|
|
29555
29321
|
stretch.rttMs = ms;
|
|
29556
29322
|
changed = true;
|
|
29557
29323
|
}
|
|
@@ -34483,24 +34249,6 @@ function messageAttention(type, message, { startedAt, newest, now }) {
|
|
|
34483
34249
|
};
|
|
34484
34250
|
}
|
|
34485
34251
|
//#endregion
|
|
34486
|
-
//#region ../browser/src/shared/hyperdhtRelay.ts
|
|
34487
|
-
/** Only `wss://`, or `ws://` to this machine (a relay for tests or development). */
|
|
34488
|
-
function hyperdhtRelayProblem(url) {
|
|
34489
|
-
let parsed;
|
|
34490
|
-
try {
|
|
34491
|
-
parsed = new URL(url);
|
|
34492
|
-
} catch {
|
|
34493
|
-
return "Enter a relay address (wss://…)";
|
|
34494
|
-
}
|
|
34495
|
-
if (parsed.protocol === "wss:") return null;
|
|
34496
|
-
if (parsed.protocol === "ws:" && [
|
|
34497
|
-
"127.0.0.1",
|
|
34498
|
-
"localhost",
|
|
34499
|
-
"[::1]"
|
|
34500
|
-
].includes(parsed.hostname)) return null;
|
|
34501
|
-
return "Use a wss:// relay address";
|
|
34502
|
-
}
|
|
34503
|
-
//#endregion
|
|
34504
34252
|
//#region ../browser/src/shared/pushRelay.ts
|
|
34505
34253
|
/**
|
|
34506
34254
|
* The push relay a browser hands wake-ups to when it may not post to a push service itself (WISP 401 § Wake-up
|
|
@@ -34651,10 +34399,10 @@ var NATIVE_SLOTS = 8;
|
|
|
34651
34399
|
var IROH_WEB_SLOTS = 16;
|
|
34652
34400
|
/** Of a transport's listeners, the ones a group's links never hold: 1:1 chats keep that room. */
|
|
34653
34401
|
var CHAT_NATIVE_SLOTS = 4;
|
|
34654
|
-
/** How often a group link that found no free native slot tries again. */
|
|
34655
|
-
var
|
|
34656
|
-
/** The longest a group link whose native listener failed to start waits before trying again (its wait doubles up to it). */
|
|
34657
|
-
var
|
|
34402
|
+
/** How often a group link that found no free native slot tries again, and the first wait of a listener that failed to start. */
|
|
34403
|
+
var NATIVE_RETRY_MS = 15e3;
|
|
34404
|
+
/** The longest a chat or a group link whose native listener failed to start waits before trying again (its wait doubles up to it). */
|
|
34405
|
+
var NATIVE_FAILED_MAX_MS = 3e5;
|
|
34658
34406
|
/** "Clear all data": how long taking things back from the network may hold up the clear, in all. */
|
|
34659
34407
|
var CLEAR_WITHDRAW_MS = 6e3;
|
|
34660
34408
|
/**
|
|
@@ -34683,11 +34431,19 @@ var DEFAULT_SETTINGS = {
|
|
|
34683
34431
|
*/
|
|
34684
34432
|
var CARD_RESTORE_MOST = 50;
|
|
34685
34433
|
var CARD_RESTORE_MS = 6048e5;
|
|
34434
|
+
/**
|
|
34435
|
+
* A round trip that moved is shown at once, and written to disk by itself (relaxed, no disk sync of its own) at most
|
|
34436
|
+
* this often: a 15 s sample on an internet path moves by 10 ms most of the time. A power cut loses at most this much
|
|
34437
|
+
* of the round trip's drift, never a row; any other change to the story writes the latest round trip with it.
|
|
34438
|
+
*/
|
|
34439
|
+
var RTT_SAVE_MS = 3e5;
|
|
34686
34440
|
/** A reaction said on the live session and not confirmed is said again after this long. */
|
|
34687
34441
|
var REACTION_RESEND_MS = 3e4;
|
|
34688
34442
|
/** A file or a payment request of mine that waits for the chat to be live (a text that waits is its outbox's). */
|
|
34689
34443
|
var waitsForLive = (m) => m.sender === "me" && m.delivery === "waiting" && (!!m.file || !!m.paymentId);
|
|
34690
34444
|
/** This side's participation key in a chat: derived from its seed once, not at every state built. */
|
|
34445
|
+
/** A read of the contact's packet that found it as the last one did: online the same, the same packet, name and services. */
|
|
34446
|
+
var samePresence = (a, b) => a.online === b.online && a.lastPacketAt === b.lastPacketAt && a.seenAt === b.seenAt && a.nick === b.nick && JSON.stringify(a.services) === JSON.stringify(b.services);
|
|
34691
34447
|
function participationKeyOf(live) {
|
|
34692
34448
|
const seed = live.stored.participationSeed;
|
|
34693
34449
|
if (!seed) return void 0;
|
|
@@ -34865,7 +34621,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
34865
34621
|
* Everything that publishes, dials, polls or settles asks this, not the stored setting.
|
|
34866
34622
|
*/
|
|
34867
34623
|
get networkOn() {
|
|
34868
|
-
return !!this.settings.online && !this.limitedMode;
|
|
34624
|
+
return !!this.settings.online && !this.limitedMode && !this.options.offline;
|
|
34869
34625
|
}
|
|
34870
34626
|
transport;
|
|
34871
34627
|
/** The same, for groups' requests: a relay budget keeps a chat's share from them (`CHAT_RESERVE`). */
|
|
@@ -34910,6 +34666,8 @@ var GhostlyNode = class GhostlyNode {
|
|
|
34910
34666
|
});
|
|
34911
34667
|
/** `start` has put the kept transfers back (EngineState.transfersRestored). */
|
|
34912
34668
|
transfersRestored = false;
|
|
34669
|
+
/** `start` has loaded the groups (EngineState.groupsLoaded). */
|
|
34670
|
+
groupsLoaded = false;
|
|
34913
34671
|
feedbackStartedAt = Date.now();
|
|
34914
34672
|
feedbackIds = /* @__PURE__ */ new Set();
|
|
34915
34673
|
walletFeedbackReady = false;
|
|
@@ -34993,9 +34751,15 @@ var GhostlyNode = class GhostlyNode {
|
|
|
34993
34751
|
groupWakeReceived = /* @__PURE__ */ new Map();
|
|
34994
34752
|
requestCounts = /* @__PURE__ */ new Map();
|
|
34995
34753
|
transfers = /* @__PURE__ */ new Map();
|
|
34754
|
+
/**
|
|
34755
|
+
* Views that go in the next state sent, and then away (`GroupFilesHost.brief`): dropped once a state carried them,
|
|
34756
|
+
* unless another view took their place meanwhile.
|
|
34757
|
+
*/
|
|
34758
|
+
briefViews = /* @__PURE__ */ new Map();
|
|
34996
34759
|
/** files/3 in every chat: offers, resumable transfers, checked by digest (WISP 501 rev 0.3). */
|
|
34997
34760
|
fileDesk = new FileDesk({
|
|
34998
34761
|
send: (linkId, frame) => this.links.get(linkId)?.link?.sendFilesFrame(frame) ?? false,
|
|
34762
|
+
sendData: (linkId, id, offset, data) => this.links.get(linkId)?.link?.sendFileData(id, offset, data) ?? false,
|
|
34999
34763
|
writable: (linkId) => this.links.get(linkId)?.link?.filesWritable(),
|
|
35000
34764
|
receivedBytes: (linkId) => this.links.get(linkId)?.files.receivedBytes ?? 0,
|
|
35001
34765
|
wireIds: (linkId) => this.links.get(linkId)?.files.wireIds,
|
|
@@ -35075,24 +34839,24 @@ var GhostlyNode = class GhostlyNode {
|
|
|
35075
34839
|
* Every wallet twice, one per network (real money, test coins), both open at once. Nothing is parked: a payment
|
|
35076
34840
|
* goes through the wallet of its own network, and a wallet of one network never pays for the other.
|
|
35077
34841
|
*/
|
|
35078
|
-
usdtWallets = perNetwork((network) => new UsdtWallet(network, () => {
|
|
35079
|
-
this.refreshWallet();
|
|
34842
|
+
usdtWallets = perNetwork((network) => new UsdtWallet(network, (viewChanged = true) => {
|
|
34843
|
+
if (viewChanged) this.refreshWallet();
|
|
35080
34844
|
this.desk.reconcileUsdtReceipts().catch(() => {});
|
|
35081
34845
|
}));
|
|
35082
|
-
arkWallets = perNetwork((network) => new ArkWallet(network, () => {
|
|
35083
|
-
this.refreshWallet();
|
|
34846
|
+
arkWallets = perNetwork((network) => new ArkWallet(network, (viewChanged = true) => {
|
|
34847
|
+
if (viewChanged) this.refreshWallet();
|
|
35084
34848
|
this.desk.reconcileArkReceipts().catch(() => {});
|
|
35085
34849
|
}));
|
|
35086
|
-
barkWallets = perNetwork((network) => new BarkWallet(network, () => {
|
|
35087
|
-
this.refreshWallet();
|
|
34850
|
+
barkWallets = perNetwork((network) => new BarkWallet(network, (viewChanged = true) => {
|
|
34851
|
+
if (viewChanged) this.refreshWallet();
|
|
35088
34852
|
this.desk.reconcileBarkReceipts().catch(() => {});
|
|
35089
34853
|
}));
|
|
35090
34854
|
fedimintWallets = perNetwork((network) => new FedimintWallet(network, {
|
|
35091
34855
|
changed: () => void this.refreshWallet(),
|
|
35092
34856
|
received: (paymentId) => void this.desk.onLightningPaid({ paymentId })
|
|
35093
34857
|
}, () => (this.options.fedimintSdk ?? loadFedimintSdk)()));
|
|
35094
|
-
sparkWallets = perNetwork((network) => new SparkWallet(network, () => {
|
|
35095
|
-
this.refreshWallet();
|
|
34858
|
+
sparkWallets = perNetwork((network) => new SparkWallet(network, (viewChanged = true) => {
|
|
34859
|
+
if (viewChanged) this.refreshWallet();
|
|
35096
34860
|
this.desk.reconcileSparkReceipts().catch(() => {});
|
|
35097
34861
|
}));
|
|
35098
34862
|
/** What a call naming no network acts on (a caller from before wallets had their own): Mainnet. */
|
|
@@ -35419,29 +35183,38 @@ var GhostlyNode = class GhostlyNode {
|
|
|
35419
35183
|
if (live.stored.deletedIds?.includes(`peer_${wire.wireId}`)) return null;
|
|
35420
35184
|
live.files.wireIds.add(wire.wireId);
|
|
35421
35185
|
live.files.receivedBytes += wire.size;
|
|
35422
|
-
await
|
|
35423
|
-
|
|
35424
|
-
|
|
35425
|
-
|
|
35426
|
-
|
|
35427
|
-
|
|
35428
|
-
|
|
35429
|
-
|
|
35430
|
-
|
|
35431
|
-
|
|
35432
|
-
|
|
35433
|
-
|
|
35434
|
-
|
|
35435
|
-
|
|
35436
|
-
|
|
35437
|
-
|
|
35438
|
-
|
|
35439
|
-
|
|
35440
|
-
|
|
35441
|
-
|
|
35442
|
-
|
|
35443
|
-
|
|
35444
|
-
|
|
35186
|
+
const storage = await fileBytes();
|
|
35187
|
+
try {
|
|
35188
|
+
await storage.stage(file.id, new Blob([bytes]));
|
|
35189
|
+
await fileStore.put({
|
|
35190
|
+
id: file.id,
|
|
35191
|
+
linkId,
|
|
35192
|
+
bytes: storage.kind,
|
|
35193
|
+
createdAt: Date.now(),
|
|
35194
|
+
direction: "in",
|
|
35195
|
+
wireId: wire.wireId,
|
|
35196
|
+
digest,
|
|
35197
|
+
metadata: {
|
|
35198
|
+
name: wire.name,
|
|
35199
|
+
size: wire.size,
|
|
35200
|
+
mime: wire.mime,
|
|
35201
|
+
timestamp: wire.timestamp,
|
|
35202
|
+
voice: wire.voice,
|
|
35203
|
+
video: wire.video,
|
|
35204
|
+
image: wire.image
|
|
35205
|
+
},
|
|
35206
|
+
transfer: {
|
|
35207
|
+
state: "done",
|
|
35208
|
+
transferred: wire.size,
|
|
35209
|
+
size: wire.size
|
|
35210
|
+
}
|
|
35211
|
+
});
|
|
35212
|
+
} catch (error) {
|
|
35213
|
+
await storage.remove(file.id).catch(() => {});
|
|
35214
|
+
live.files.wireIds.delete(wire.wireId);
|
|
35215
|
+
live.files.receivedBytes -= wire.size;
|
|
35216
|
+
throw error;
|
|
35217
|
+
}
|
|
35445
35218
|
this.transfers.set(file.id, {
|
|
35446
35219
|
state: "done",
|
|
35447
35220
|
transferred: wire.size,
|
|
@@ -35771,6 +35544,10 @@ var GhostlyNode = class GhostlyNode {
|
|
|
35771
35544
|
forgetFrame: async (linkId, id) => {
|
|
35772
35545
|
await db.patchMessage(linkId, id, (stored) => stored.frame ? { frame: void 0 } : null, true);
|
|
35773
35546
|
},
|
|
35547
|
+
keepFrame: async (linkId, id, frame) => {
|
|
35548
|
+
await db.patchMessage(linkId, id, () => ({ frame }), true);
|
|
35549
|
+
},
|
|
35550
|
+
placeAgain: (linkId, ids) => this.placeAgain(linkId, ids),
|
|
35774
35551
|
membersChanged: (groupId) => this.groupMembersChanged(groupId),
|
|
35775
35552
|
historyGone: (groupId) => this.groupHistoryGone(groupId),
|
|
35776
35553
|
emit: () => this.emitState(),
|
|
@@ -35791,11 +35568,16 @@ var GhostlyNode = class GhostlyNode {
|
|
|
35791
35568
|
groupFiles: {
|
|
35792
35569
|
store: storedGroupFiles,
|
|
35793
35570
|
transfers: this.transfers,
|
|
35571
|
+
brief: (id) => {
|
|
35572
|
+
const view = this.transfers.get(id);
|
|
35573
|
+
if (view) this.briefViews.set(id, view);
|
|
35574
|
+
},
|
|
35794
35575
|
settings: () => ({
|
|
35795
35576
|
autoDownloads: this.settings.autoDownloads !== false,
|
|
35796
35577
|
serveFiles: this.settings.serveFiles !== false
|
|
35797
35578
|
}),
|
|
35798
35579
|
sendFiles: (linkId, frame) => this.links.get(linkId)?.link?.sendFilesFrame(frame) ?? false,
|
|
35580
|
+
sendData: (linkId, id, offset, data) => this.links.get(linkId)?.link?.sendFileData(id, offset, data) ?? false,
|
|
35799
35581
|
writable: (linkId) => this.links.get(linkId)?.link?.filesWritable()
|
|
35800
35582
|
}
|
|
35801
35583
|
});
|
|
@@ -36060,6 +35842,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
36060
35842
|
const wallets = this.withHomes(walletInstances(networks));
|
|
36061
35843
|
const intents = (await intentRepository.list()).map((saved) => saved.review);
|
|
36062
35844
|
this.noteOpenReviews(intents);
|
|
35845
|
+
this.paymentCoordinator.notePending(intents);
|
|
36063
35846
|
this.walletView = {
|
|
36064
35847
|
...networks.mainnet,
|
|
36065
35848
|
networks,
|
|
@@ -36188,7 +35971,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
36188
35971
|
const messages = stored.group ? [] : await db.getMessages(stored.id);
|
|
36189
35972
|
const storedFiles = stored.group ? [] : await fileStore.listForLink(stored.id);
|
|
36190
35973
|
const waiting = new Set(messages.flatMap((m) => m.sender === "me" && m.delivery === "waiting" && m.file ? [m.file.id] : []));
|
|
36191
|
-
for (const file of storedFiles) if (file.transfer && !file.wire3 && !waiting.has(file.id)) this.transfers.set(file.id, file.transfer.state === "transferring" ? {
|
|
35974
|
+
for (const file of storedFiles) if (file.transfer && file.transfer.state !== "done" && !file.wire3 && !waiting.has(file.id)) this.transfers.set(file.id, file.transfer.state === "transferring" ? {
|
|
36192
35975
|
...file.transfer,
|
|
36193
35976
|
state: "failed",
|
|
36194
35977
|
error: "Transfer interrupted. Retry when connected."
|
|
@@ -36206,6 +35989,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
36206
35989
|
this.transfersRestored = true;
|
|
36207
35990
|
for (const group of await db.getGroups()) await this.settleHistory(`group:${group.id}`);
|
|
36208
35991
|
await this.groups.load(this.limitedMode);
|
|
35992
|
+
this.groupsLoaded = true;
|
|
36209
35993
|
for (const group of this.groups.views()) {
|
|
36210
35994
|
const note = latestReaction(await db.getMessages(`group:${group.id}`));
|
|
36211
35995
|
if (note) this.reactionNotes.set(`group:${group.id}`, note);
|
|
@@ -36224,7 +36008,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
36224
36008
|
this.prepareSpare(STARTUP_QUIET_MS);
|
|
36225
36009
|
}
|
|
36226
36010
|
if (!this.limitedMode) this.openStartedWallets(fresh);
|
|
36227
|
-
if (this.appsOn) this.appStore.start().catch(() => {});
|
|
36011
|
+
if (this.appsOn && !this.appsWireOnly) this.appStore.start().catch(() => {});
|
|
36228
36012
|
if (!this.options.singleDevice && this.networkOn && knownDeviceGate()?.state === "active") this.startDeviceSet().catch(() => {});
|
|
36229
36013
|
if (!this.options.singleDevice && this.limitedMode && knownDeviceGate()?.state === "active") this.readTurnWhileLimited();
|
|
36230
36014
|
if (this.restoreCheck === "checking" && this.limitedMode) this.checkRestoredTurn(0);
|
|
@@ -36689,7 +36473,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
36689
36473
|
appShelf;
|
|
36690
36474
|
/** The installed apps and stores, refused while the apps feature is off. */
|
|
36691
36475
|
get appStore() {
|
|
36692
|
-
if (!this.appsOn) throw this.appsOff();
|
|
36476
|
+
if (!this.appsOn || this.appsWireOnly) throw this.appsOff();
|
|
36693
36477
|
return this.appShelf ??= new Apps({
|
|
36694
36478
|
fetch: boundedAppFetch({
|
|
36695
36479
|
fetcher: this.options.appFetch,
|
|
@@ -36700,12 +36484,16 @@ var GhostlyNode = class GhostlyNode {
|
|
|
36700
36484
|
return !!stored && !stored.group && !!stored.profile && !!stored.pairedPeerKey;
|
|
36701
36485
|
},
|
|
36702
36486
|
online: () => this.networkOn && !this.shuttingDown,
|
|
36703
|
-
checked: () => this.events.onAppsChecked?.()
|
|
36487
|
+
checked: () => this.events.onAppsChecked?.(),
|
|
36488
|
+
...this.options.builtInApp && { readBuiltIn: this.options.builtInApp }
|
|
36704
36489
|
});
|
|
36705
36490
|
}
|
|
36706
36491
|
appList() {
|
|
36707
36492
|
return this.appStore.list();
|
|
36708
36493
|
}
|
|
36494
|
+
appBuiltIns() {
|
|
36495
|
+
return this.appStore.builtIns();
|
|
36496
|
+
}
|
|
36709
36497
|
appStoreList() {
|
|
36710
36498
|
return this.appStore.listStores();
|
|
36711
36499
|
}
|
|
@@ -36751,6 +36539,9 @@ var GhostlyNode = class GhostlyNode {
|
|
|
36751
36539
|
appFile(params) {
|
|
36752
36540
|
return this.appStore.file(params);
|
|
36753
36541
|
}
|
|
36542
|
+
appFileAt(params) {
|
|
36543
|
+
return this.appStore.fileAt(params);
|
|
36544
|
+
}
|
|
36754
36545
|
appStorageGet(params) {
|
|
36755
36546
|
return this.appStore.storageGet(params);
|
|
36756
36547
|
}
|
|
@@ -36766,6 +36557,9 @@ var GhostlyNode = class GhostlyNode {
|
|
|
36766
36557
|
appDataExport(params) {
|
|
36767
36558
|
return this.appStore.exportData(params);
|
|
36768
36559
|
}
|
|
36560
|
+
appDataExportText(params) {
|
|
36561
|
+
return this.appStore.exportDataText(params);
|
|
36562
|
+
}
|
|
36769
36563
|
getState() {
|
|
36770
36564
|
const groups = this.groups.views();
|
|
36771
36565
|
return {
|
|
@@ -36793,6 +36587,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
36793
36587
|
})).sort((a, b) => a.createdAt - b.createdAt),
|
|
36794
36588
|
transfers: Object.fromEntries(this.transfers),
|
|
36795
36589
|
transfersRestored: this.transfersRestored,
|
|
36590
|
+
...this.groupsLoaded && { groupsLoaded: true },
|
|
36796
36591
|
wallet: this.walletView,
|
|
36797
36592
|
payments: this.desk.views(),
|
|
36798
36593
|
identityProofs: this.identities.views(),
|
|
@@ -37645,7 +37440,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
37645
37440
|
this.transport.networkChanged?.();
|
|
37646
37441
|
this.directPath.reset();
|
|
37647
37442
|
}
|
|
37648
|
-
if (params.network) for (const failed of this.
|
|
37443
|
+
if (params.network) for (const failed of this.nativeFailed.values()) {
|
|
37649
37444
|
failed.at = 0;
|
|
37650
37445
|
failed.wait = 0;
|
|
37651
37446
|
}
|
|
@@ -37770,6 +37565,10 @@ var GhostlyNode = class GhostlyNode {
|
|
|
37770
37565
|
error: `Message exceeds ${LIMITS.maxChatMessageBytes} UTF-8 bytes.`,
|
|
37771
37566
|
refused: true
|
|
37772
37567
|
};
|
|
37568
|
+
if (!pairedMessageFits(timestamp, trimmed, reply && pairedWireReply(reply), forwarded, card)) return {
|
|
37569
|
+
error: PAIRED_FRAME_TOO_LARGE,
|
|
37570
|
+
refused: true
|
|
37571
|
+
};
|
|
37773
37572
|
const stop = this.chatStopped(live);
|
|
37774
37573
|
if (stop) return { error: stop };
|
|
37775
37574
|
if (!link.isDataLinkOpen && link.dhtDelivery?.inviteTaken) return {
|
|
@@ -37859,14 +37658,23 @@ var GhostlyNode = class GhostlyNode {
|
|
|
37859
37658
|
/**
|
|
37860
37659
|
* The message of a chat a reply answers, by its id here or the id both sides know it by (`replyRef`): what the
|
|
37861
37660
|
* reply keeps of it, or why there is nothing to reply to. A group's reply needs the author's member key too.
|
|
37661
|
+
* Read alone by its id, or a chat's `peer_`/`me_` row for a wire id, as `resolveReply` does: a group's message goes
|
|
37662
|
+
* by its row's id, so a miss there is not here; a chat's other rows (a file's, a payment's) are looked for in the
|
|
37663
|
+
* whole chat.
|
|
37862
37664
|
*/
|
|
37863
37665
|
async replyFor(linkId, messageId) {
|
|
37864
37666
|
if (typeof messageId !== "string" || !messageId) return "No message to reply to";
|
|
37865
|
-
const
|
|
37866
|
-
|
|
37667
|
+
const group = linkId.startsWith("group:");
|
|
37668
|
+
let original = await db.getMessage(linkId, messageId);
|
|
37669
|
+
for (const rowId of group ? [] : [`peer_${messageId}`, `me_${messageId}`]) {
|
|
37670
|
+
if (original) break;
|
|
37671
|
+
const row = await db.getMessage(linkId, rowId);
|
|
37672
|
+
if (row && replyRef(row) === messageId) original = row;
|
|
37673
|
+
}
|
|
37674
|
+
if (!original && !group) original = (await db.getMessages(linkId)).find((m) => replyRef(m) === messageId);
|
|
37867
37675
|
const ref = original && replyRef(original);
|
|
37868
37676
|
if (!original || !ref) return "That message is not in this chat, or cannot be replied to";
|
|
37869
|
-
if (
|
|
37677
|
+
if (group && !original.member) return "That message cannot be replied to";
|
|
37870
37678
|
return replyTo(original, ref);
|
|
37871
37679
|
}
|
|
37872
37680
|
/**
|
|
@@ -38204,6 +38012,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
38204
38012
|
const chat = this.links.get(linkId)?.stored;
|
|
38205
38013
|
outbox = new Outbox({
|
|
38206
38014
|
read: () => db.getMessages(linkId),
|
|
38015
|
+
get: (id) => db.getMessage(linkId, id),
|
|
38207
38016
|
update: async (id, delivery, error, extra) => {
|
|
38208
38017
|
if (delivery === "delivered") await db.patchMessage(linkId, id, (message) => message.delivery === "delivered" ? null : {
|
|
38209
38018
|
...message.delivery && {
|
|
@@ -39285,19 +39094,40 @@ var GhostlyNode = class GhostlyNode {
|
|
|
39285
39094
|
const cue = transportCue(mark, log.entries, log.history);
|
|
39286
39095
|
if (cue) this.cueFeedback(cue, linkId);
|
|
39287
39096
|
}
|
|
39288
|
-
|
|
39097
|
+
/** `toDisk: false` shows the story and leaves the disk as it is; `relaxed`: written with no disk sync of its own. */
|
|
39098
|
+
saveTransportLog(live, log, { toDisk = true, relaxed = false } = {}) {
|
|
39289
39099
|
const transportLog = log.entries.map((e) => ({ ...e })), transportHistory = log.history.map((e) => ({ ...e }));
|
|
39290
39100
|
live.stored = {
|
|
39291
39101
|
...live.stored,
|
|
39292
39102
|
transportLog,
|
|
39293
39103
|
transportHistory
|
|
39294
39104
|
};
|
|
39295
|
-
|
|
39296
|
-
|
|
39297
|
-
|
|
39298
|
-
|
|
39105
|
+
if (toDisk) {
|
|
39106
|
+
live.rttUnsaved = false;
|
|
39107
|
+
db.patchLink(live.stored.id, {
|
|
39108
|
+
transportLog,
|
|
39109
|
+
transportHistory
|
|
39110
|
+
}, relaxed).catch(() => {});
|
|
39111
|
+
}
|
|
39299
39112
|
this.emitState();
|
|
39300
39113
|
}
|
|
39114
|
+
/** A round trip measured on the chat's transport: in the view at once, on disk at most every `RTT_SAVE_MS`. */
|
|
39115
|
+
noteRtt(live, ms) {
|
|
39116
|
+
const moved = live.rttPosted === void 0 || Math.abs(live.rttPosted - ms) >= 10;
|
|
39117
|
+
const log = this.transportLogOf(live);
|
|
39118
|
+
if (!log) {
|
|
39119
|
+
if (moved) this.emitState();
|
|
39120
|
+
return;
|
|
39121
|
+
}
|
|
39122
|
+
const changed = log.rtt(ms);
|
|
39123
|
+
if (changed) live.rttUnsaved = true;
|
|
39124
|
+
const now = Date.now();
|
|
39125
|
+
if (live.rttUnsaved && (live.rttSavedAt === void 0 || now - live.rttSavedAt >= RTT_SAVE_MS)) {
|
|
39126
|
+
live.rttSavedAt = now;
|
|
39127
|
+
this.saveTransportLog(live, log, { relaxed: true });
|
|
39128
|
+
} else if (changed) this.saveTransportLog(live, log, { toDisk: false });
|
|
39129
|
+
else if (moved) this.emitState();
|
|
39130
|
+
}
|
|
39301
39131
|
/** Which ways of paying one chat allows. */
|
|
39302
39132
|
/** `networks`: for a way of paying, the networks this chat takes it on (the Accept side's cards); the others are kept. */
|
|
39303
39133
|
async setChatPaymentMethods({ linkId, methods, networks }) {
|
|
@@ -39329,7 +39159,9 @@ var GhostlyNode = class GhostlyNode {
|
|
|
39329
39159
|
async connect({ linkId }) {
|
|
39330
39160
|
const link = this.links.get(linkId)?.link;
|
|
39331
39161
|
if (!link) throw new Error("You are offline. Go online before reconnecting.");
|
|
39332
|
-
|
|
39162
|
+
const live = this.links.get(linkId), native = this.ensureNativeEndpoints(linkId);
|
|
39163
|
+
const { preferredTransport: preferred, transportFallback: fallback } = live.stored;
|
|
39164
|
+
if (this.reachedOnlyNatively(live) || fallback === false && preferred && preferred !== "webrtc/1") await native;
|
|
39333
39165
|
await link.connect();
|
|
39334
39166
|
}
|
|
39335
39167
|
disconnect({ linkId }) {
|
|
@@ -39839,11 +39671,22 @@ var GhostlyNode = class GhostlyNode {
|
|
|
39839
39671
|
}
|
|
39840
39672
|
/** Mini-apps are on in this build (`APPS_ENABLED`) or for this engine (`apps`). */
|
|
39841
39673
|
get appsOn() {
|
|
39842
|
-
return this.options.apps ?? true;
|
|
39674
|
+
return !!(this.options.apps ?? true);
|
|
39675
|
+
}
|
|
39676
|
+
/** `apps/1` only (`apps: "wire"`): this host runs no mini-app and keeps no store, so nothing is installed here to check. */
|
|
39677
|
+
get appsWireOnly() {
|
|
39678
|
+
return this.options.apps === "wire";
|
|
39843
39679
|
}
|
|
39844
|
-
/** Why an app call is refused: this client runs no mini-app (`apps: false`), or the build has them off. */
|
|
39680
|
+
/** Why an app call is refused: this client runs no mini-app (`apps: false`, or `"wire"` for the store's), or the build has them off. */
|
|
39845
39681
|
appsOff() {
|
|
39846
|
-
return /* @__PURE__ */ new Error(this.options.apps === false ? "Apps do not run on this client" : "Apps are unavailable in this release");
|
|
39682
|
+
return /* @__PURE__ */ new Error(this.options.apps === false || this.appsWireOnly ? "Apps do not run on this client" : "Apps are unavailable in this release");
|
|
39683
|
+
}
|
|
39684
|
+
/** Before an app's frames go to a contact: a version removed or revoked here says nothing more (WISP 1200 § Takedowns). */
|
|
39685
|
+
async appChatRunnable(ref, digest) {
|
|
39686
|
+
if (!this.appsWireOnly) await this.appStore.chatRunnable({
|
|
39687
|
+
ref,
|
|
39688
|
+
digest
|
|
39689
|
+
});
|
|
39847
39690
|
}
|
|
39848
39691
|
/** A paired 1:1 chat and the chat app id of `ref` in it, from the two pinned participation keys (WISP 1200 § In a chat). */
|
|
39849
39692
|
appChat(linkId, ref) {
|
|
@@ -39869,10 +39712,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
39869
39712
|
async appOpen({ linkId, ref, version, digest }) {
|
|
39870
39713
|
const { live, app } = this.appChat(linkId, ref);
|
|
39871
39714
|
if (!isAppVersion(version)) throw new Error("Not an app version");
|
|
39872
|
-
await this.
|
|
39873
|
-
ref,
|
|
39874
|
-
digest
|
|
39875
|
-
});
|
|
39715
|
+
await this.appChatRunnable(ref, digest);
|
|
39876
39716
|
const open = live.appsOpen ??= /* @__PURE__ */ new Map();
|
|
39877
39717
|
if (live.link) live.link.openApp(app, version);
|
|
39878
39718
|
else open.set(app, version);
|
|
@@ -39888,10 +39728,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
39888
39728
|
async appSend({ linkId, ref, data, digest }) {
|
|
39889
39729
|
const { live, app } = this.appChat(linkId, ref);
|
|
39890
39730
|
if (!live.appsOpen?.has(app)) return { error: "not-open" };
|
|
39891
|
-
await this.
|
|
39892
|
-
ref,
|
|
39893
|
-
digest
|
|
39894
|
-
});
|
|
39731
|
+
await this.appChatRunnable(ref, digest);
|
|
39895
39732
|
return { error: live.link ? live.link.sendAppData(app, data) : "offline" };
|
|
39896
39733
|
}
|
|
39897
39734
|
/** Every page went away: no app runs here any more, so each open one is closed (`close` goes where it can). */
|
|
@@ -39998,8 +39835,10 @@ var GhostlyNode = class GhostlyNode {
|
|
|
39998
39835
|
this.links.get(edge)?.link?.sendGroupFrame(frame);
|
|
39999
39836
|
} catch {}
|
|
40000
39837
|
}
|
|
40001
|
-
|
|
40002
|
-
|
|
39838
|
+
/** `admin`: the member a leaving admin handed the role to. */
|
|
39839
|
+
async leaveGroup({ groupId }) {
|
|
39840
|
+
const admin = await this.groups.leave(groupId);
|
|
39841
|
+
return admin ? { admin } : {};
|
|
40003
39842
|
}
|
|
40004
39843
|
removeGroupMember({ groupId, key }) {
|
|
40005
39844
|
return this.groups.remove(groupId, key);
|
|
@@ -41018,6 +40857,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
41018
40857
|
const settings = rest;
|
|
41019
40858
|
delete settings.backupReminders;
|
|
41020
40859
|
const wasOnline = this.networkOn;
|
|
40860
|
+
if (Array.isArray(settings.iceServers)) settings.iceServers = settings.iceServers.map(iceServerSetting);
|
|
41021
40861
|
for (const server of settings.iceServers ?? []) {
|
|
41022
40862
|
const problem = iceServerProblem(server);
|
|
41023
40863
|
if (problem) throw new Error(problem);
|
|
@@ -41986,7 +41826,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
41986
41826
|
this.groupWakeReceived.delete(linkId);
|
|
41987
41827
|
this.quietEdges.delete(linkId);
|
|
41988
41828
|
this.groupNativeWaiting.delete(linkId);
|
|
41989
|
-
this.
|
|
41829
|
+
this.nativeFailed.delete(linkId);
|
|
41990
41830
|
this.groupNativeRetryAt = 0;
|
|
41991
41831
|
if (live.stored.wakeToken && this.meshEdge(live.stored)) {
|
|
41992
41832
|
const group = live.stored.group, peer = live.stored.groupPeer;
|
|
@@ -42102,7 +41942,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42102
41942
|
if (this.groupEntryTimer || this.shuttingDown || this.options.deferGroups) return;
|
|
42103
41943
|
this.groupEntryTimer = setInterval(() => {
|
|
42104
41944
|
this.groups.tick().catch(() => {});
|
|
42105
|
-
this.
|
|
41945
|
+
this.retryNative();
|
|
42106
41946
|
}, 1e3);
|
|
42107
41947
|
}
|
|
42108
41948
|
stopGroupEntries() {
|
|
@@ -42145,6 +41985,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42145
41985
|
usdtPaymentsSupport: !entry,
|
|
42146
41986
|
barkPaymentsSupport: !entry,
|
|
42147
41987
|
largeFilesSupport: !entry,
|
|
41988
|
+
binaryFilesSupport: !entry,
|
|
42148
41989
|
params: stored,
|
|
42149
41990
|
rtcAvailable: typeof RTCPeerConnection !== "undefined" && !this.edgeRtcOff.has(linkId),
|
|
42150
41991
|
resume: !entry && stored.edgeLive ? resumeOn : void 0,
|
|
@@ -42250,10 +42091,11 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42250
42091
|
role,
|
|
42251
42092
|
packetAt: presence.lastPacketAt
|
|
42252
42093
|
});
|
|
42094
|
+
const same = samePresence(live.presence, presence);
|
|
42253
42095
|
live.presence = presence;
|
|
42254
42096
|
if (!entry && presence.nick && !live.link?.isDataLinkOpen) this.groups.edgeNick(group, peer, presence.nick);
|
|
42255
42097
|
if (!entry) this.edgeRtcPresence(linkId, presence);
|
|
42256
|
-
this.emitState();
|
|
42098
|
+
if (!same) this.emitState();
|
|
42257
42099
|
},
|
|
42258
42100
|
onPairingState: (state) => {
|
|
42259
42101
|
live.pairing = state;
|
|
@@ -42340,7 +42182,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42340
42182
|
this.edgeRtcOff.delete(linkId);
|
|
42341
42183
|
if (live) traceLink(live.myPubKeyZ32, "edge-rtc-back", {});
|
|
42342
42184
|
this.groupNativeWaiting.delete(linkId);
|
|
42343
|
-
this.
|
|
42185
|
+
this.nativeFailed.delete(linkId);
|
|
42344
42186
|
this.groupNativeRetryAt = 0;
|
|
42345
42187
|
this.restartEdge(linkId);
|
|
42346
42188
|
}
|
|
@@ -42381,6 +42223,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42381
42223
|
callsSupport: this.options.callsSupport ?? typeof RTCPeerConnection !== "undefined",
|
|
42382
42224
|
callsMissing: this.options.callsUnavailable,
|
|
42383
42225
|
largeFilesSupport: true,
|
|
42226
|
+
binaryFilesSupport: true,
|
|
42384
42227
|
typingSupport: true,
|
|
42385
42228
|
reactionsSupport: this.options.reactions !== false,
|
|
42386
42229
|
editSupport: !GhostlyNode.testNoEdit(),
|
|
@@ -42532,11 +42375,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42532
42375
|
this.emitState();
|
|
42533
42376
|
},
|
|
42534
42377
|
onLiveAttempt: () => this.emitState(),
|
|
42535
|
-
onRtt: (ms) =>
|
|
42536
|
-
const log = this.transportLogOf(live);
|
|
42537
|
-
if (log?.rtt(ms)) this.saveTransportLog(live, log);
|
|
42538
|
-
else this.emitState();
|
|
42539
|
-
},
|
|
42378
|
+
onRtt: (ms) => this.noteRtt(live, ms),
|
|
42540
42379
|
onDiscoveryError: (error) => {
|
|
42541
42380
|
if ((error ?? void 0) !== live.discoveryError) {
|
|
42542
42381
|
live.discoveryError = error ?? void 0;
|
|
@@ -42658,6 +42497,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42658
42497
|
this.emitState();
|
|
42659
42498
|
},
|
|
42660
42499
|
onPresence: (presence) => {
|
|
42500
|
+
let same = samePresence(live.presence, presence);
|
|
42661
42501
|
live.presence = presence;
|
|
42662
42502
|
if (!stored.profile && presence.nick && presence.nick !== live.stored.peerNick) {
|
|
42663
42503
|
live.stored = {
|
|
@@ -42665,8 +42505,9 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42665
42505
|
peerNick: presence.nick
|
|
42666
42506
|
};
|
|
42667
42507
|
db.patchLink(linkId, { peerNick: presence.nick });
|
|
42508
|
+
same = false;
|
|
42668
42509
|
}
|
|
42669
|
-
this.emitState();
|
|
42510
|
+
if (!same) this.emitState();
|
|
42670
42511
|
},
|
|
42671
42512
|
onMessage: (message) => {
|
|
42672
42513
|
if (live.stored.inviteCode) {
|
|
@@ -42737,6 +42578,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42737
42578
|
}
|
|
42738
42579
|
});
|
|
42739
42580
|
const link = live.link;
|
|
42581
|
+
if (linkId === this.activeLinkId) link.setChatActive(true);
|
|
42740
42582
|
link.start();
|
|
42741
42583
|
this.emitState();
|
|
42742
42584
|
if (resumed.resume) {
|
|
@@ -42861,7 +42703,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42861
42703
|
get nativeFactories() {
|
|
42862
42704
|
const relay = this.relaysHyperdht ? this.hyperdhtRelay : "";
|
|
42863
42705
|
return {
|
|
42864
|
-
...relay ? { "hyperdht/1": async (seedB64) => (await import("./hyperdhtRelay-
|
|
42706
|
+
...relay ? { "hyperdht/1": async (seedB64) => (await import("./hyperdhtRelay-CPYwSLLS.js")).createRelayedHyperEndpoint(seedB64, relay) } : {},
|
|
42865
42707
|
...this.options.nativeTransports,
|
|
42866
42708
|
...this.ownIrohRelays ? { "iroh/1": (seedB64) => this.options.nativeTransports["iroh/1"](seedB64, this.settings.irohRelays?.length ? { relays: [...this.settings.irohRelays] } : {}) } : {},
|
|
42867
42709
|
...this.options.irohWeb ? { "iroh/1": (seedB64) => createIrohWebEndpoint(seedB64, { relays: this.irohRelays }) } : {}
|
|
@@ -42962,17 +42804,18 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42962
42804
|
return Promise.all(starts).then((full) => {
|
|
42963
42805
|
const live = this.links.get(linkId), link = live?.link;
|
|
42964
42806
|
if (this.shuttingDown || !live || !link || link !== expected) return;
|
|
42965
|
-
|
|
42807
|
+
const group = !!live.stored.group;
|
|
42808
|
+
if (group && this.keepsGroupNative(live.stored)) {
|
|
42966
42809
|
if (full.includes("no-slot") && !link.availableTransports.some((t) => t !== "webrtc/1")) this.groupNativeWaiting.add(linkId);
|
|
42967
42810
|
else this.groupNativeWaiting.delete(linkId);
|
|
42968
|
-
|
|
42969
|
-
|
|
42970
|
-
|
|
42971
|
-
|
|
42972
|
-
|
|
42973
|
-
|
|
42974
|
-
}
|
|
42975
|
-
} else this.
|
|
42811
|
+
}
|
|
42812
|
+
if ((group ? this.keepsGroupNative(live.stored) : this.keepsNativeEndpoints(linkId, live.stored)) && full.includes("failed")) {
|
|
42813
|
+
const wait = Math.min(Math.max(NATIVE_RETRY_MS, (this.nativeFailed.get(linkId)?.wait ?? 0) * 2), NATIVE_FAILED_MAX_MS);
|
|
42814
|
+
this.nativeFailed.set(linkId, {
|
|
42815
|
+
at: Date.now() + wait,
|
|
42816
|
+
wait
|
|
42817
|
+
});
|
|
42818
|
+
} else this.nativeFailed.delete(linkId);
|
|
42976
42819
|
this.emitState();
|
|
42977
42820
|
});
|
|
42978
42821
|
}
|
|
@@ -43130,25 +42973,25 @@ var GhostlyNode = class GhostlyNode {
|
|
|
43130
42973
|
/** Group links waiting for a free native slot (`ensureNativeEndpoints`), by link id. */
|
|
43131
42974
|
groupNativeWaiting = /* @__PURE__ */ new Set();
|
|
43132
42975
|
groupNativeRetryAt = 0;
|
|
43133
|
-
/**
|
|
43134
|
-
|
|
42976
|
+
/** Chats and group links whose native listener failed to start, by link id: when each tries again, and how long it waited last. */
|
|
42977
|
+
nativeFailed = /* @__PURE__ */ new Map();
|
|
43135
42978
|
/**
|
|
43136
42979
|
* Now and then, the group links that found no free native slot try again: a chat or another group may have let one go.
|
|
43137
|
-
*
|
|
42980
|
+
* Chats and group links whose listener failed to start try again once their wait is over.
|
|
43138
42981
|
*/
|
|
43139
|
-
|
|
42982
|
+
retryNative() {
|
|
43140
42983
|
const now = Date.now();
|
|
43141
|
-
for (const [linkId, failed] of this.
|
|
42984
|
+
for (const [linkId, failed] of this.nativeFailed) {
|
|
43142
42985
|
if (now < failed.at) continue;
|
|
43143
42986
|
if (!this.links.get(linkId)?.link) {
|
|
43144
|
-
this.
|
|
42987
|
+
this.nativeFailed.delete(linkId);
|
|
43145
42988
|
continue;
|
|
43146
42989
|
}
|
|
43147
42990
|
failed.at = now + failed.wait;
|
|
43148
42991
|
this.ensureNativeEndpoints(linkId);
|
|
43149
42992
|
}
|
|
43150
42993
|
if (!this.groupNativeWaiting.size || Date.now() < this.groupNativeRetryAt) return;
|
|
43151
|
-
this.groupNativeRetryAt = Date.now() +
|
|
42994
|
+
this.groupNativeRetryAt = Date.now() + NATIVE_RETRY_MS;
|
|
43152
42995
|
for (const linkId of [...this.groupNativeWaiting]) {
|
|
43153
42996
|
if (!this.links.get(linkId)?.link) {
|
|
43154
42997
|
this.groupNativeWaiting.delete(linkId);
|
|
@@ -43253,6 +43096,24 @@ var GhostlyNode = class GhostlyNode {
|
|
|
43253
43096
|
} else this.placedAt.set(message.linkId, Math.max(last, Math.min(message.timestamp, now)));
|
|
43254
43097
|
return message;
|
|
43255
43098
|
}
|
|
43099
|
+
/**
|
|
43100
|
+
* These rows, in this order, placed again below everything in their history, as if they came now: a group member's
|
|
43101
|
+
* messages that came above an older one of theirs (WISP 400, requirement 10), and the lines that came after them. The
|
|
43102
|
+
* places are taken before anything is awaited, so a row that comes meanwhile goes below them. Each still shows the
|
|
43103
|
+
* time it showed. All of them in one write: one disk sync, not one a row.
|
|
43104
|
+
*/
|
|
43105
|
+
async placeAgain(linkId, ids) {
|
|
43106
|
+
const places = new Map(ids.map((id) => {
|
|
43107
|
+
const timestamp = arrivalKey(Date.now(), this.placedAt.get(linkId));
|
|
43108
|
+
this.placedAt.set(linkId, timestamp);
|
|
43109
|
+
return [id, timestamp];
|
|
43110
|
+
}));
|
|
43111
|
+
const moved = await db.patchMessages(linkId, [...places.keys()], (m) => ({
|
|
43112
|
+
timestamp: places.get(m.id),
|
|
43113
|
+
sentAt: shownTime(m)
|
|
43114
|
+
}));
|
|
43115
|
+
if (moved.length) await this.messagesChanged(linkId, moved.map((m) => m.id));
|
|
43116
|
+
}
|
|
43256
43117
|
async storeMessage(message) {
|
|
43257
43118
|
await this.storeNewMessage(message);
|
|
43258
43119
|
}
|
|
@@ -43372,7 +43233,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
43372
43233
|
transportWait: live.link?.transportWait,
|
|
43373
43234
|
liveAttempt: live.link?.liveAttempt,
|
|
43374
43235
|
liveDialer: live.stored.profile && !live.stored.group ? live.link?.dialer : void 0,
|
|
43375
|
-
transportRttMs: live.link?.rttMs,
|
|
43236
|
+
transportRttMs: live.rttPosted = live.link?.rttMs,
|
|
43376
43237
|
transportRelayed: live.link?.relayedPath,
|
|
43377
43238
|
transportLive: live.transportLog?.liveNow(),
|
|
43378
43239
|
transportLog: stored.id === this.activeLinkId && stored.profile && !stored.group ? stored.transportLog ?? [] : void 0,
|
|
@@ -43430,6 +43291,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
43430
43291
|
this.stateTimer = null;
|
|
43431
43292
|
if (this.gatedOut) return;
|
|
43432
43293
|
this.events.onState(this.getState());
|
|
43294
|
+
this.dropBriefViews();
|
|
43433
43295
|
if (this.deviceLinks) this.syncDeviceTokens();
|
|
43434
43296
|
}, delayMs);
|
|
43435
43297
|
}
|
|
@@ -43441,11 +43303,16 @@ var GhostlyNode = class GhostlyNode {
|
|
|
43441
43303
|
}
|
|
43442
43304
|
if (this.gatedOut) return;
|
|
43443
43305
|
this.events.onState(this.getState());
|
|
43306
|
+
this.dropBriefViews();
|
|
43307
|
+
}
|
|
43308
|
+
/** A state went with the brief views: they go, so the next states do not carry them (`briefViews`). */
|
|
43309
|
+
dropBriefViews() {
|
|
43310
|
+
for (const [id, view] of this.briefViews) if (this.transfers.get(id) === view) this.transfers.delete(id);
|
|
43311
|
+
this.briefViews.clear();
|
|
43444
43312
|
}
|
|
43445
43313
|
};
|
|
43446
43314
|
//#endregion
|
|
43447
43315
|
//#region ../browser/src/engine/server.ts
|
|
43448
|
-
var server_exports = /* @__PURE__ */ __exportAll({ EngineServer: () => EngineServer });
|
|
43449
43316
|
var EngineServer = class {
|
|
43450
43317
|
/** The whole engine: never made on a device that is not the active one (`devices/peer.ts` decides, WISP 06 § The gate). */
|
|
43451
43318
|
gated = false;
|
|
@@ -43517,6 +43384,10 @@ var EngineServer = class {
|
|
|
43517
43384
|
get historyMark() {
|
|
43518
43385
|
return this.changes;
|
|
43519
43386
|
}
|
|
43387
|
+
/** Whether a chat's or group's history changed after `mark` (`historyMark`); with none, since this engine was made. */
|
|
43388
|
+
historyChanged(id, mark = 0) {
|
|
43389
|
+
return (this.changedAt.get(id) ?? 0) > mark;
|
|
43390
|
+
}
|
|
43520
43391
|
/**
|
|
43521
43392
|
* `read`: the chats and groups whose whole history the client read itself, after `mark` (`historyMark`). One that has
|
|
43522
43393
|
* not changed since is not read and sent again: the client hears only what changes in it from now on.
|
|
@@ -43679,6 +43550,6 @@ function signalKind(signal) {
|
|
|
43679
43550
|
}
|
|
43680
43551
|
}
|
|
43681
43552
|
//#endregion
|
|
43682
|
-
export { EngineServer
|
|
43553
|
+
export { EngineServer };
|
|
43683
43554
|
|
|
43684
|
-
//# sourceMappingURL=server-
|
|
43555
|
+
//# sourceMappingURL=server-Ss0gJ-nw.js.map
|