@ghostlytools/cli 1.2.0 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -10
- package/SKILL.md +15 -6
- package/dist/assets/{arkade-3szZ1IpP.js → arkade-_gS9wShF.js} +2 -2
- package/dist/assets/{arkade-3szZ1IpP.js.map → arkade-_gS9wShF.js.map} +1 -1
- package/dist/assets/{hyperdhtRelay-DmTrpmba.js → hyperdhtRelay-CgYLaQz3.js} +3 -3
- package/dist/assets/{hyperdhtRelay-DmTrpmba.js.map → hyperdhtRelay-CgYLaQz3.js.map} +1 -1
- package/dist/assets/{mainline-BueFJx3f.js → mainline-1v8OTYvp.js} +2 -2
- package/dist/assets/{mainline-BueFJx3f.js.map → mainline-1v8OTYvp.js.map} +1 -1
- package/dist/assets/{server-BGXmSYx_.js → server-Cy4ox7oD.js} +722 -147
- package/dist/assets/server-Cy4ox7oD.js.map +1 -0
- package/dist/assets/{src-BMUcYQ7b.js → src-XzcHzneo.js} +1016 -752
- package/dist/assets/src-XzcHzneo.js.map +1 -0
- package/dist/assets/{usdt-DklGBSeq.js → usdt-Deo3g0Nq.js} +2 -2
- package/dist/assets/{usdt-DklGBSeq.js.map → usdt-Deo3g0Nq.js.map} +1 -1
- package/dist/ghostly.mjs +831 -203
- package/dist/ghostly.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/assets/server-BGXmSYx_.js.map +0 -1
- package/dist/assets/src-BMUcYQ7b.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.js";
|
|
2
|
-
import { $
|
|
2
|
+
import { $ as groupName, $c as sanitizeAvatar, $i as peekDhtMailbox, $o as HANDOFF_REQUEST, $r as isAppVersion, $s as deviceInviteJoinerParams, $t as EVM_TEST_CHAINS, A as encodeCommunityLink, Aa as readReaction, Ac as turnKeys, Ai as dialDescriptors, An as ENDPOINT, Ao as decodeControl, Ar as transferStalled, As as sealHandoffPiece, B as GroupWakeLimiter, Bc as withRequestOptions, Bi as newIdentityBinding, Bn as pairedMessageFrame, Br as pinIsNewer, C as openGroupTyping, Ca as GROUP_REACTION_FRAME, Cc as TURN_LAST_SEQUENCE, Co as RELAY_POLL_INTERVALS, Cr as readBeacon, Cs as readHandoffCancel, D as GROUP_READ_NOTE_COMMUNITY, Da as queueReaction, Dc as readTurnPacket, Di as CapsExchange, Do as parseLocalTarget, Dr as ChatFiles, Ds as readHandoffPake, E as CommunitySession, Ea as nextReactionNumber, Ec as nextTurnPosition, Eo as formatLocalTarget, Er as shouldBeHub, Es as readHandoffHello, F as knockIdentity, Fa as replySnippet, Fc as normalizeRelayUrl, Fi as IdentityExchange, Fn as parseCashuRequestPayload, Fr as typingActivity, Fs as setUpdateFrame, G as GROUP_READ_NOTE, Ga as checkStatusCard, Gi as emptyProofLedger, Go as HANDOFF_ACK, Gr as deviceWakeRequest, Gs as enrollDoneFrame, H as groupWakes, Hi as revokesIdentity, Hn as engineText, Ho as HTTP_SERVICE_PROTO, Hr as readPin, Hs as enrollCancelFrame, I as knockRecords, Ia as BUTTON_ID, In as parseFedimintRequestPayload, Is as signTombstone, Ja as arrivalKey, Jc as identityFromSeed, Ji as automaticTransport, Jn as groupFileFallback, Jo as HANDOFF_HAVE, Jr as relayRequest, Js as enrollProofFrame, Jt as encodeDidDhtDeactivated, K as GroupSession, Ka as sameButtonText, Ki as validExternalKey, Ko as HANDOFF_CANCEL, Kr as newWakeToken, Ks as enrollGrantFrame, Kt as didDhtDocument, L as mergeKnocks, Lc as traceLink, Li as emptyIdentityLedger, Ln as GhostLink, Lr as PIN_LIMITS, Ls as verifySetUpdate, M as decodeGroupEntryLink, Ma as wireReaction, Mc as DEFAULT_RELAYS, Mn as fedimintRequestPayload, N as encodeGroupEntryLink, Na as pairedReplyAuthor, Nc as RelayTransport, Ni as IDENTITY_REVOCATION_LABEL, Nn as findEndpoint, No as RTC_CONFIG, Nr as TypingSender, Ns as readSetUpdate, O as communityHasFrame, Oa as reactionEmoji, Oc as signTurnPacket, Oi as DHT_TEXT_CAPABILITY, Oo as webLocalFetch, Or as FILE_LIMITS, Os as readHandoffTurnFrame, P as entryParams, Pa as readReply, Pc as currentRelays, Pn as isFederationId, Ps as setAckFrame, Q as mentionsMember, Qa as shownTime, Qi as DHT_MESSAGE_TTL, Qo as HANDOFF_PIECE_BYTES, Qr as chatAppId, Qs as createDeviceInvite, Qt as ETHEREUM_USDT, R as readKnocks, Rc as budgetRetryMs, Ri as identityRevocationValue, Rn as INVITE_TAKEN, Rr as mayPin, S as groupTypingEpoch, Sa as COMMUNITY_REACTION_FRAME, Sc as turnPutSummary, Sr as rankHubs, Ss as readHandoffAck, T as COMMUNITY_LIMITS, Ta as ReactionWindow, Tc as firstDeviceSetSecret, To as GhostlyHttpError, Tr as readLobby, Ts as readHandoffHave, U as parseGroupWakeFrame, Uc as createRelayPayload, Ui as lastSharedWithMe, Un as GROUP_FILE_LIMITS, Uo as LEGACY_SERVICES, Ur as WAKE_CALL_INTERVAL_MS, Us as enrollCancelReason, V as groupWakeFrame, Va as STATUS_CARD_LIMITS, Vc as PacketTooLargeError, Vi as receivedIdentityStatus, Vn as engineError, Vo as emptyLinkRecords, Vr as pinNumberHolds, Wi as PeerProofs, Wo as serviceIdFromName, Wr as WakeLimiter, Ws as enrollDigits, X as carryMentions, Xa as heardTime, Xc as publicKeyFromZ32, Xn as readGroupFileMeta, Xo as HANDOFF_OFFER, Xr as checkPushEndpoint, Xs as readEnrollHello, Y as GROUP_EDIT_TEXT_BYTES, Ya as claimedTime, Yc as identityFromSeedB64, Yo as HANDOFF_HELLO, Yr as wakeRequest, Ys as readEnrollGrant, Yt as encodeDidDhtPacket, Z as parseCommunityEdit, Za as receivedTimestamp, Zc as verify, Zo as HANDOFF_PAKE, Zr as vapidKeysMatch, Zs as verifyEnrollProof, Zt as signDidDhtPacket, _a as RateWindow, _c as webCryptoSigner, _o as rosterHas, _r as lobbyRecords, _s as handoffReleaseFrame, a as planAppUpdate, aa as safeBlobType, ai as HOLD_CAPABILITY, al as utf8Decode, an as USDT_PUBLIC_RPC, ao as isAppRef, ar as groupSignalFrame, as as handoffAttemptSucceeded, ba as forwardedAgain, bc as TURN_SETTLE_MS, bo as DirectPathWatch, br as newerHead, bs as handoffVersions, bt as normalizeAtprotoHandle, cc as ENROLL_CAPABILITY, ci as HoldKeys, cn as assertWholeSats, cr as mayBeHub, cs as handoffContext, d as APP_BUNDLE_LIMITS, da as fileMessageText, dc as deviceEchoNonce, di as newMailbox, dn as walletNetworkOf, dr as COMMUNITY_TOPOLOGY, ds as handoffDigest, ea as DHT_FILE_CAPABILITY, ec as readDeviceInvite, ei as APPS_CAPABILITY, el as bytesEqual, en as ONCHAIN_PROVIDER, eo as APP_STATEMENT_LIMITS, er as CarriedTransport, es as HANDOFF_TIMINGS, f as APP_BUNDLE_MAGIC, fa as parseVoiceMeta, fc as deviceKeyZ32, fi as readManifest, fn as SPARK_NETWORKS, fo as readCanonicalJson, fr as beaconKeys, fs as handoffEphemeral, ga as EDIT_RATE_WINDOW_MS, gc as seedSigner, gn as isBitcoinAddress, go as commitHash, gr as lobbyKeys, gs as handoffPakeFrame, h as appViewOf, ha as EDIT_BUFFER_MS, hc as newDeviceSetSecret, hn as sparkInvoiceDetails, ho as MEMBER_KEY, hr as freshHubs, hs as handoffOfferFrame, i as checkAppBeforeRun, ia as formatFileSize, ic as encodeInviteCode, ii as UPGRADE_CAPABILITY, il as toZ32, in as TEST_USDT_FAUCET_AMOUNT, io as isAppKey, is as handoffAttemptFailed, ja as validReactionNumber, jc as turnName, jn as cashuRequestPayload, jo as encodeControl, k as decodeCommunityLink, ka as reactionIsNewer, kc as signTurnRelease, ki as capsDescriptors, kn as decodeBolt11, ko as LIMITS, kr as transferEnded, ks as readHandoffVerified, l as readAppStoreSteps, lc as HANDOFF_CAPABILITY, li as HoldRefusedError, ln as formatPaymentAmount, lr as meshHubs, ls as handoffContextProof, m as appDigest, ma as parseVideoMeta, mc as devicePingFrame, mn as sparkAddressKind, mr as doorHubs, ms as handoffManifestFrames, n as appStoreDecision, na as parseImageMeta, nc as createChatInvite, ni as EDIT_CAPABILITY, nl as randomBytes, nn as SEPOLIA_TEST_USDT, no as appRef, ns as filePartName, o as APP_STORE_LIMITS, oa as sanitizeFileName, oc as inviteOwnership, oi as HOLD_DATED_BACK_MS, ol as utf8Encode, on as WALLET_NETWORKS, oo as readAppRevocation, or as readGroupSignal, os as handoffBusyFrame, pc as deviceLinkPairing, pn as SPARK_PROVIDER, po as GROUP_MEMBER_CAP, pr as beaconRecords, ps as handoffHelloFrame, qa as statusCardText, qc as createIdentity, qi as TRANSPORTS, qo as HANDOFF_DONE, qr as parseWakeFrame, qs as enrollHelloFrame, qt as didDhtFromPublicKey, r as appUpdateDecision, rc as decodeInviteCode, ri as STATUS_CARD_CAPABILITY, rl as toBase64Url, rs as handoffAttemptAllowed, sa as sanitizeMime, sc as DEVICES_CAPABILITY, si as HOLD_LIMITS, sn as assertTokenUnits, sr as MESH_HUBS, ss as handoffCancelFrame, t as appRevocationFor, ta as dhtFileId, tc as OwnInviteError, ti as BUTTONS_CAPABILITY, tl as fromBase64Url, tn as PaymentPreflightError, to as appFingerprint, ts as HANDOFF_VERIFIED, uc as deviceEchoFrame, ui as isHoldUrl, un as validatePaymentTarget, uo as canonicalJson, ur as pickMeshHubs, us as handoffDataFrame, v as isAppUrl, va as dhtEditId, vn as isPubkyKey, vo as verifyCommitSignature, vr as mergeBeacon, vs as handoffRetryAfter, w as sealGroupTyping, wa as REACTION_LIMITS, wc as TURN_MAX, wo as presenceSeenAt, wr as readBeaconHead, ws as readHandoffDone, x as GroupTypingBoard, xa as readForwarded, xc as classifyTurnRead, xo as DHT_POLL_INTERVALS, xr as pickHubs, xs as newHandoffId, y as readAppBundle, ya as parseLinkPreview, yc as TURN_RAISE_READ_TIMEOUT_MS, yo as edgeParams, yr as mergeLobby, ys as handoffStreamKey, yt as isAtprotoDid, zc as isDiscoveryBudgetError, zi as identityStatement, zn as LIVENESS_PING_MS, zo as parseCallSignal, zr as nextPinNumber } from "./src-XzcHzneo.js";
|
|
3
3
|
import { a as clearProfileStores, c as fileStore, d as profileOpenFailure, f as store, i as STORES$1, l as newestWalletTx, m as wrap, n as APP_STORAGE_SCOPE_INDEX, o as databaseName, p as transact, r as CARD_INDEX, s as deleteDatabase, t as APP_STORAGE_CHAT_INDEX, u as openDb } from "./idb-CTF6LF5B.js";
|
|
4
4
|
import { a as fileBytesOf, c as removeFileBytes, i as fileBytes, l as FILE_BYTES_STEP, n as blobDigest } from "./fileBytes-BcYZhNgF.js";
|
|
5
5
|
import { $ as BACKUP_MEDIA_TYPE, A as requireTurn, At as paymentWireId, B as networkLabel, C as breezDatabasesFor, Ct as storedGroupFiles, D as sourceKey, Dt as replyRef, E as ProviderSources, Et as receivedPairedReply, F as sealSeed, Ft as streamStored, G as BITCOIN_INVOICE_ON_TESTNET, H as cashuMint, I as unsealSeed, J as fakesLightning, K as DEFAULT_MINTS, L as ModeChanged, M as setSingleWriterGate, Mt as readStored$1, N as intentRepository, Nt as removeStored, O as awayFrom, Ot as replyTo, P as newDeviceKey, Pt as storedSize, Q as paysItsOwnInvoices, R as ModeGate, S as breezDatabaseInUse, St as groupOfFile, T as lightningPaying, Tt as pairedWireReply, V as CASHU_MINT_SOURCE, W as WALLET_TYPES, X as isWorthlessMint, Y as isTestMint, Z as mintNetwork, _ as LightningCards, _t as verifyIdentity, a as irohRelayProblem, at as reserveAdapterIds, b as normalizePhrase, bt as groupFileId, c as createFailure, ct as SourceConfigError, d as paymentNetwork, dt as networkMode, et as MAX_BACKUP_BYTES, f as paymentNetworksOf, ft as redact, g as CASHU_CARD, gt as boundedIdentityFetch, ht as setOwnDidSource, i as createIrohWebEndpoint, it as registeredOnchainProviders, j as setAwayWallets, jt as FileAppender, k as refuseAway, kt as paymentAlias, l as createTiming, lt as SourceUnreachableError, mt as resolveAtprotoDid, n as loadFedimintSdk, nt as onAdaptersChanged, o as WALLET_NAMES, ot as NothingSpentError, p as walletInstances, pt as assertPublicServer, q as TEST_MINT, r as DEFAULT_IROH_RELAYS, rt as registeredLightningProviders, s as assertConfirmedReal, st as PROVIDER_PLATFORMS, t as fedimintDatabase, tt as identityProviders, u as crossNetwork, ut as isNothingSpentError, v as cardsKey, vt as chosenResolver, w as dropBreezDatabase, wt as groupReply, x as breezDatabase, xt as groupFileText, y as isRecoveryPhrase, yt as GroupFileDesk, z as WrongNetworkError } from "../ghostly.mjs";
|
|
@@ -180,7 +180,7 @@ async function migrateWalletNetworks(now = Date.now()) {
|
|
|
180
180
|
//#endregion
|
|
181
181
|
//#region ../browser/src/engine/paymentAdapters/usdtWallet.ts
|
|
182
182
|
/** The Ethereum libraries, a large part of the app: loaded once a USDT wallet is made, opened or restored, not with the engine. */
|
|
183
|
-
var loadUsdt = () => import("./usdt-
|
|
183
|
+
var loadUsdt = () => import("./usdt-Deo3g0Nq.js");
|
|
184
184
|
/** Every new profile starts with this wallet: an address to receive on, nothing to set up. */
|
|
185
185
|
var DEFAULT_USDT = {
|
|
186
186
|
network: "ethereum",
|
|
@@ -642,7 +642,9 @@ async function restoreArkDatabase(walletId, snapshot) {
|
|
|
642
642
|
//#endregion
|
|
643
643
|
//#region ../browser/src/engine/paymentAdapters/arkWallet.ts
|
|
644
644
|
/** The Ark SDK, a large part of the app: loaded once an Ark wallet is made, opened or restored, not with the engine. */
|
|
645
|
-
var loadArkade = () => import("./arkade-
|
|
645
|
+
var loadArkade = () => import("./arkade-_gS9wShF.js");
|
|
646
|
+
/** How long a Testnet wallet about to make way for another waits for its server to say what it holds. */
|
|
647
|
+
var RETIRE_READ_MS = 1e4;
|
|
646
648
|
/** Every new profile starts with this wallet: nothing to set up before receiving. */
|
|
647
649
|
var DEFAULT_ARK = {
|
|
648
650
|
network: "bitcoin",
|
|
@@ -1048,10 +1050,23 @@ var ArkWallet = class {
|
|
|
1048
1050
|
async retirable(refusal) {
|
|
1049
1051
|
const saved = this.saved;
|
|
1050
1052
|
if ((await wrap((await store(STORES$1.intents, "readonly")).getAll())).some((i) => this.ours(i))) throw new Error(refusal);
|
|
1051
|
-
if (this.adapter && await this.gate.within(this.adapter
|
|
1053
|
+
if (this.adapter && await this.gate.within(this.held(this.adapter)) > 0) throw new Error(refusal);
|
|
1052
1054
|
await this.lock();
|
|
1053
1055
|
return saved;
|
|
1054
1056
|
}
|
|
1057
|
+
/**
|
|
1058
|
+
* What the wallet holds, asked of its server. A test network's server that stopped answering must not hold the
|
|
1059
|
+
* wallet on it (one that never answered does not either): past RETIRE_READ_MS the last balance read stands.
|
|
1060
|
+
* Mainnet waits for its server's own word.
|
|
1061
|
+
*/
|
|
1062
|
+
held(adapter) {
|
|
1063
|
+
const asked = adapter.balance();
|
|
1064
|
+
if (this.network !== "testnet") return asked;
|
|
1065
|
+
return new Promise((resolve, reject) => {
|
|
1066
|
+
const timer = setTimeout(() => resolve(this.view.balance), RETIRE_READ_MS);
|
|
1067
|
+
asked.then(resolve, reject).finally(() => clearTimeout(timer));
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1055
1070
|
/** A payment of this wallet's network (the other network's wallet has its own). */
|
|
1056
1071
|
ours(intent) {
|
|
1057
1072
|
return intent.review.method === "arkade" && arkMode(intent.review.network) === this.network;
|
|
@@ -1876,13 +1891,24 @@ var BarkWallet = class {
|
|
|
1876
1891
|
async retirable(refusal) {
|
|
1877
1892
|
const saved = this.saved;
|
|
1878
1893
|
if ((await intentRepository.list()).some((i) => this.ours(i))) throw new Error(refusal);
|
|
1879
|
-
if (this.adapter)
|
|
1880
|
-
const b = await this.gate.within(this.adapter.balance());
|
|
1881
|
-
if (b.spendableSats + b.pendingInRoundSats + b.pendingBoardSats + b.pendingExitSats + b.pendingLightningSendSats + b.claimableLightningReceiveSats > 0) throw new Error(refusal);
|
|
1882
|
-
}
|
|
1894
|
+
if (this.adapter && await this.gate.within(this.held(this.adapter)) > 0) throw new Error(refusal);
|
|
1883
1895
|
await this.lock();
|
|
1884
1896
|
return saved;
|
|
1885
1897
|
}
|
|
1898
|
+
/**
|
|
1899
|
+
* What the wallet holds, asked of its server: spendable, on its way in and on its way out. A test network's server
|
|
1900
|
+
* that stopped answering must not hold the wallet on it (one that never answered does not either): past
|
|
1901
|
+
* RETIRE_READ_MS the last reading stands, whose three sums are those same six parts. Mainnet waits for its
|
|
1902
|
+
* server's own word.
|
|
1903
|
+
*/
|
|
1904
|
+
held(adapter) {
|
|
1905
|
+
const asked = adapter.balance().then((b) => b.spendableSats + b.pendingInRoundSats + b.pendingBoardSats + b.pendingExitSats + b.pendingLightningSendSats + b.claimableLightningReceiveSats);
|
|
1906
|
+
if (this.network !== "testnet") return asked;
|
|
1907
|
+
return new Promise((resolve, reject) => {
|
|
1908
|
+
const timer = setTimeout(() => resolve(this.view.balance + (this.view.pending ?? 0) + (this.view.exiting ?? 0)), RETIRE_READ_MS);
|
|
1909
|
+
asked.then(resolve, reject).finally(() => clearTimeout(timer));
|
|
1910
|
+
});
|
|
1911
|
+
}
|
|
1886
1912
|
/** A payment of this wallet's network (the other network's wallet has its own). */
|
|
1887
1913
|
ours(intent) {
|
|
1888
1914
|
return intent.review.method === "bark" && barkMode(intent.review.network) === this.network;
|
|
@@ -11143,6 +11169,15 @@ var APP_CHECK_TIMINGS = {
|
|
|
11143
11169
|
/** A check at start is skipped when every app was checked this recently (a page reloaded several times). */
|
|
11144
11170
|
freshMs: 36e5
|
|
11145
11171
|
};
|
|
11172
|
+
/** Revocations are checked this long at a time before the page's thread is let go. */
|
|
11173
|
+
var REVOCATIONS_SLICE_MS = 10;
|
|
11174
|
+
/**
|
|
11175
|
+
* Revocations of store indexes whose signature was checked, kept by `appRevocationId` so an index that changes around
|
|
11176
|
+
* them does not have each checked again (the one read longest ago dropped first): four indexes' worth, about 1 MiB.
|
|
11177
|
+
*/
|
|
11178
|
+
var STORE_REVOCATIONS_KEPT = 4 * APP_STORE_LIMITS.revoked;
|
|
11179
|
+
/** `ghostly-revoke.json` files whose result is kept, so an unchanged one is not checked again (the oldest dropped first). */
|
|
11180
|
+
var REVOCATION_FILES_KEPT = 64;
|
|
11146
11181
|
/** A fetched bundle kept for its install screen: at most this many, for this long. */
|
|
11147
11182
|
var STAGED = {
|
|
11148
11183
|
max: 4,
|
|
@@ -11227,7 +11262,7 @@ var Apps = class {
|
|
|
11227
11262
|
/** Reads of a bundle under way, by digest: icons drawn at once share one read. */
|
|
11228
11263
|
verifying = /* @__PURE__ */ new Map();
|
|
11229
11264
|
/**
|
|
11230
|
-
* The `icon.png` of each checked bundle, by digest (at most 256 KiB, or null for none): every icon shown on the Apps
|
|
11265
|
+
* The `icon.png` of each checked bundle, by digest (at most 256 KiB and 1024 × 1024 pixels, or null for none): every icon shown on the Apps
|
|
11231
11266
|
* page, in a chat's cards and in the composer's Apps is read without the whole bundle again.
|
|
11232
11267
|
*/
|
|
11233
11268
|
icons = /* @__PURE__ */ new Map();
|
|
@@ -11240,12 +11275,36 @@ var Apps = class {
|
|
|
11240
11275
|
checking = null;
|
|
11241
11276
|
/** Runs the person started with Run anyway (`<ref> <digest>`), for this engine's life: a removal does not cut them off. */
|
|
11242
11277
|
anyway = /* @__PURE__ */ new Set();
|
|
11278
|
+
/** The apps started here (`entry`), for this engine's life: one of them uninstalled since may still have a frame open. */
|
|
11279
|
+
ran = /* @__PURE__ */ new Set();
|
|
11280
|
+
/**
|
|
11281
|
+
* The versions started here (`entry`), by reference: digest to `sequence`, for this engine's life. A frame goes on
|
|
11282
|
+
* running the version it started with whatever is installed since, so that version is the one judged when it asks.
|
|
11283
|
+
*/
|
|
11284
|
+
started = /* @__PURE__ */ new Map();
|
|
11243
11285
|
views = null;
|
|
11286
|
+
held = /* @__PURE__ */ new Map();
|
|
11287
|
+
heldStores = null;
|
|
11244
11288
|
/**
|
|
11245
11289
|
* Listing URLs whose peek showed nothing to take (`<store> <ref> <url>` → `<index digest> <listed digest>`): the same
|
|
11246
11290
|
* index listing the same is not peeked again at every check.
|
|
11247
11291
|
*/
|
|
11248
11292
|
listingMisses = /* @__PURE__ */ new Map();
|
|
11293
|
+
/** `ghostly-revoke.json` files checked, by the SHA-256 of their bytes: their revocations, or null when refused. */
|
|
11294
|
+
revocationFiles = /* @__PURE__ */ new Map();
|
|
11295
|
+
/** Revocations read in a store's index and verified, by `appRevocationId`: at most `STORE_REVOCATIONS_KEPT`. */
|
|
11296
|
+
storeRevocations = /* @__PURE__ */ new Set();
|
|
11297
|
+
checkedRevocations = {
|
|
11298
|
+
has: (id) => {
|
|
11299
|
+
if (!this.storeRevocations.delete(id)) return false;
|
|
11300
|
+
this.storeRevocations.add(id);
|
|
11301
|
+
return true;
|
|
11302
|
+
},
|
|
11303
|
+
add: (id) => {
|
|
11304
|
+
this.storeRevocations.add(id);
|
|
11305
|
+
if (this.storeRevocations.size > STORE_REVOCATIONS_KEPT) this.storeRevocations.delete(this.storeRevocations.values().next().value);
|
|
11306
|
+
}
|
|
11307
|
+
};
|
|
11249
11308
|
stopped = false;
|
|
11250
11309
|
constructor(host) {
|
|
11251
11310
|
this.host = host;
|
|
@@ -11269,6 +11328,9 @@ var Apps = class {
|
|
|
11269
11328
|
if (this.sweep) clearTimeout(this.sweep);
|
|
11270
11329
|
this.sweep = null;
|
|
11271
11330
|
this.staged.clear();
|
|
11331
|
+
this.held.clear();
|
|
11332
|
+
this.heldStores = null;
|
|
11333
|
+
this.views = null;
|
|
11272
11334
|
}
|
|
11273
11335
|
schedule(ms, atStart = false) {
|
|
11274
11336
|
if (this.timer) clearTimeout(this.timer);
|
|
@@ -11300,8 +11362,22 @@ var Apps = class {
|
|
|
11300
11362
|
async installed() {
|
|
11301
11363
|
return wrap((await store(STORES$1.apps, "readonly")).getAll());
|
|
11302
11364
|
}
|
|
11303
|
-
|
|
11304
|
-
|
|
11365
|
+
/**
|
|
11366
|
+
* An installed app's record, read once and again after it is written here (the only writer, as for `heldViews`). An
|
|
11367
|
+
* app's every storage call, file read and chat frame asks for it. A reference not installed is not kept.
|
|
11368
|
+
*/
|
|
11369
|
+
app(ref) {
|
|
11370
|
+
const kept = this.held.get(ref);
|
|
11371
|
+
if (kept) return kept;
|
|
11372
|
+
const read = store(STORES$1.apps, "readonly").then((s) => wrap(s.get(ref)));
|
|
11373
|
+
this.held.set(ref, read);
|
|
11374
|
+
const drop = () => {
|
|
11375
|
+
if (this.held.get(ref) === read) this.held.delete(ref);
|
|
11376
|
+
};
|
|
11377
|
+
read.then((app) => {
|
|
11378
|
+
if (!app) drop();
|
|
11379
|
+
}, drop);
|
|
11380
|
+
return read;
|
|
11305
11381
|
}
|
|
11306
11382
|
async installedOrFail(ref) {
|
|
11307
11383
|
if (!isAppRef(ref)) fail("bad-ref", "Not an app reference");
|
|
@@ -11312,25 +11388,55 @@ var Apps = class {
|
|
|
11312
11388
|
* a store of the person's removed (unless this run was started with Run anyway), is cut off with `stopped`, whether
|
|
11313
11389
|
* or not the page stops its frame.
|
|
11314
11390
|
*/
|
|
11315
|
-
async stillRunnable(app) {
|
|
11316
|
-
const
|
|
11391
|
+
async stillRunnable(app, digest) {
|
|
11392
|
+
const version = this.versionRunning(app, digest);
|
|
11393
|
+
const run = this.runStatus(version, await this.heldViews(), app.revocations);
|
|
11317
11394
|
if (run.status === "revoked") fail("stopped", "Its publisher revoked this version");
|
|
11318
|
-
if (run.status === "removed" && !this.anyway.has(`${
|
|
11395
|
+
if (run.status === "removed" && !this.anyway.has(`${version.ref} ${version.digest}`)) fail("stopped", `Removed by ${run.by[0].name}: ${run.by[0].reason}`);
|
|
11319
11396
|
return app;
|
|
11320
11397
|
}
|
|
11321
|
-
|
|
11322
|
-
|
|
11398
|
+
/**
|
|
11399
|
+
* The version a frame runs: the one it names by `digest` (what `entry` gave it), which an update installed since does
|
|
11400
|
+
* not change. None named: the installed one. One never started here does not run.
|
|
11401
|
+
*/
|
|
11402
|
+
versionRunning(app, digest) {
|
|
11403
|
+
if (digest === void 0 || digest === app.digest) return app;
|
|
11404
|
+
const sequence = typeof digest === "string" ? this.started.get(app.ref)?.get(digest) : void 0;
|
|
11405
|
+
if (sequence === void 0) fail("stopped", "This version is not running");
|
|
11406
|
+
return {
|
|
11407
|
+
ref: app.ref,
|
|
11408
|
+
sequence,
|
|
11409
|
+
digest
|
|
11410
|
+
};
|
|
11411
|
+
}
|
|
11412
|
+
/** The versions started here that are not the installed one any more. */
|
|
11413
|
+
startedBefore(app) {
|
|
11414
|
+
return [...this.started.get(app.ref) ?? []].filter(([digest]) => digest !== app.digest).map(([digest, sequence]) => ({
|
|
11415
|
+
ref: app.ref,
|
|
11416
|
+
sequence,
|
|
11417
|
+
digest
|
|
11418
|
+
}));
|
|
11419
|
+
}
|
|
11420
|
+
async runningOrFail(ref, digest) {
|
|
11421
|
+
return this.stillRunnable(await this.installedOrFail(ref), digest);
|
|
11323
11422
|
}
|
|
11324
11423
|
/**
|
|
11325
11424
|
* Before an app's frames go to a contact (`appOpen`, `appSend`): refused with `stopped` as its storage is. A reference
|
|
11326
|
-
* not installed here (a bot that speaks an app's messages) is not this store's to refuse
|
|
11425
|
+
* not installed here (a bot that speaks an app's messages) is not this store's to refuse, unless it ran here and was
|
|
11426
|
+
* uninstalled since: the person removed it, so a frame of it still open says nothing more (`not-installed`). `digest`:
|
|
11427
|
+
* the version the frame runs (`versionRunning`).
|
|
11327
11428
|
*/
|
|
11328
|
-
async chatRunnable({ ref }) {
|
|
11429
|
+
async chatRunnable({ ref, digest }) {
|
|
11329
11430
|
const app = isAppRef(ref) ? await this.app(ref) : void 0;
|
|
11330
|
-
if (app
|
|
11431
|
+
if (!app && this.ran.has(ref)) fail("not-installed", "This app is not installed");
|
|
11432
|
+
if (app) await this.stillRunnable(app, digest);
|
|
11331
11433
|
}
|
|
11332
11434
|
async putApp(app) {
|
|
11333
|
-
|
|
11435
|
+
try {
|
|
11436
|
+
await wrap((await store(STORES$1.apps, "readwrite")).put(app));
|
|
11437
|
+
} finally {
|
|
11438
|
+
this.held.delete(app.ref);
|
|
11439
|
+
}
|
|
11334
11440
|
}
|
|
11335
11441
|
/**
|
|
11336
11442
|
* Writes an app's record back only while that install is still there (the same `installedAt`), read and written in
|
|
@@ -11345,33 +11451,58 @@ var Apps = class {
|
|
|
11345
11451
|
tx.objectStore(STORES$1.apps).put(app);
|
|
11346
11452
|
put = true;
|
|
11347
11453
|
};
|
|
11348
|
-
|
|
11454
|
+
try {
|
|
11455
|
+
await done(tx);
|
|
11456
|
+
} finally {
|
|
11457
|
+
this.held.delete(app.ref);
|
|
11458
|
+
}
|
|
11349
11459
|
return put;
|
|
11350
11460
|
}
|
|
11461
|
+
/**
|
|
11462
|
+
* The stores' records by key, read once and kept in step by the writes here (the only writer: pages in other tabs
|
|
11463
|
+
* share this engine). A record holds its store's whole index, up to 4 MiB, and IndexedDB reads and writes it whole:
|
|
11464
|
+
* the Apps page, the update check and an app's every storage call and chat frame ask for them.
|
|
11465
|
+
*/
|
|
11466
|
+
records() {
|
|
11467
|
+
return this.heldStores ??= store(STORES$1.appStores, "readonly").then((s) => wrap(s.getAll())).then((all) => new Map(all.map((r) => [r.key, r])), (error) => {
|
|
11468
|
+
this.heldStores = null;
|
|
11469
|
+
throw error;
|
|
11470
|
+
});
|
|
11471
|
+
}
|
|
11351
11472
|
async storeRecord(key) {
|
|
11352
|
-
return
|
|
11473
|
+
return (await this.records()).get(key);
|
|
11474
|
+
}
|
|
11475
|
+
/** A write of the stores' records: what is asked while it runs waits for it, as a read of IndexedDB would; one that fails leaves what was held. */
|
|
11476
|
+
async writeStores(write, apply) {
|
|
11477
|
+
const before = this.records();
|
|
11478
|
+
const written = before.then(async (held) => {
|
|
11479
|
+
await wrap(write(await store(STORES$1.appStores, "readwrite")));
|
|
11480
|
+
apply(held);
|
|
11481
|
+
this.views = null;
|
|
11482
|
+
});
|
|
11483
|
+
const after = written.then(() => before, () => before);
|
|
11484
|
+
this.heldStores = after;
|
|
11485
|
+
after.catch(() => {
|
|
11486
|
+
if (this.heldStores === after) this.heldStores = null;
|
|
11487
|
+
});
|
|
11488
|
+
await written;
|
|
11353
11489
|
}
|
|
11354
|
-
|
|
11355
|
-
|
|
11356
|
-
this.views = null;
|
|
11490
|
+
putStore(record) {
|
|
11491
|
+
return this.writeStores((records) => records.put(record), (held) => held.set(record.key, record));
|
|
11357
11492
|
}
|
|
11358
|
-
|
|
11359
|
-
|
|
11360
|
-
this.views = null;
|
|
11493
|
+
deleteStore(key) {
|
|
11494
|
+
return this.writeStores((records) => records.delete(key), (held) => held.delete(key));
|
|
11361
11495
|
}
|
|
11362
|
-
/**
|
|
11363
|
-
* The stores' views, read once and again after a store is written here (the only writer: pages in other tabs share
|
|
11364
|
-
* this engine). An app's every storage call and chat frame checks them, and an index may be 4 MiB.
|
|
11365
|
-
*/
|
|
11496
|
+
/** The stores' views, made once and again after a store is written here: an app's every storage call and chat frame checks them. */
|
|
11366
11497
|
heldViews() {
|
|
11367
11498
|
return this.views ??= this.stores().then((s) => this.storeViews(s), (error) => {
|
|
11368
11499
|
this.views = null;
|
|
11369
11500
|
throw error;
|
|
11370
11501
|
});
|
|
11371
11502
|
}
|
|
11372
|
-
/** The stores the person has (a removed default is not one). */
|
|
11503
|
+
/** The stores the person has (a removed default is not one), in the order of their keys, as IndexedDB lists them. */
|
|
11373
11504
|
async stores() {
|
|
11374
|
-
return (await
|
|
11505
|
+
return [...(await this.records()).values()].filter((s) => !s.removed).sort((a, b) => a.key < b.key ? -1 : 1);
|
|
11375
11506
|
}
|
|
11376
11507
|
storeViews(stores) {
|
|
11377
11508
|
return stores.filter((s) => s.index).map((s) => ({
|
|
@@ -11489,7 +11620,10 @@ var Apps = class {
|
|
|
11489
11620
|
}
|
|
11490
11621
|
/**
|
|
11491
11622
|
* Reads the index at `url`. With the index held for this store, bytes equal to it (by digest) are the held index: its
|
|
11492
|
-
* signature and every revocation in it were checked when it was read, so only its expiry is taken again.
|
|
11623
|
+
* signature and every revocation in it were checked when it was read, so only its expiry is taken again. Any other
|
|
11624
|
+
* index is read with a pause every few ms (the engine shares the page's thread, and an index holds up to 4096
|
|
11625
|
+
* listings and 4096 revocations, each of these a signature check), and a revocation checked in an earlier read, of
|
|
11626
|
+
* this store or another, a preview included, is not checked again.
|
|
11493
11627
|
*/
|
|
11494
11628
|
async readStoreAt(url, heldKey, held) {
|
|
11495
11629
|
const [indexBytes, sigBytes] = await Promise.all([this.host.fetch(url, { maxBytes: APP_FETCH_LIMITS.storeIndexBytes }), this.host.fetch(besideUrl(url, "ghostly-store.sig"), { maxBytes: APP_FETCH_LIMITS.sigBytes })]);
|
|
@@ -11501,7 +11635,16 @@ var Apps = class {
|
|
|
11501
11635
|
expired: held.index.expires < this.nowS()
|
|
11502
11636
|
}
|
|
11503
11637
|
};
|
|
11504
|
-
|
|
11638
|
+
const steps = readAppStoreSteps(indexBytes, sigBytes, this.nowS(), heldKey, this.checkedRevocations);
|
|
11639
|
+
let since = performance.now();
|
|
11640
|
+
for (;;) {
|
|
11641
|
+
const step = steps.next();
|
|
11642
|
+
if (step.done) return step.value;
|
|
11643
|
+
if (performance.now() - since >= REVOCATIONS_SLICE_MS) {
|
|
11644
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
11645
|
+
since = performance.now();
|
|
11646
|
+
}
|
|
11647
|
+
}
|
|
11505
11648
|
}
|
|
11506
11649
|
/**
|
|
11507
11650
|
* The revocations its publisher signed, read beside each URL (`ghostly-revoke.json`); none there is not an error.
|
|
@@ -11513,12 +11656,43 @@ var Apps = class {
|
|
|
11513
11656
|
if (read.has(url)) continue;
|
|
11514
11657
|
read.add(url);
|
|
11515
11658
|
try {
|
|
11516
|
-
const
|
|
11517
|
-
if (
|
|
11659
|
+
const file = await this.readRevocationsFile(await this.host.fetch(url, { maxBytes: APP_FETCH_LIMITS.revocationsBytes }));
|
|
11660
|
+
if (file) (out ??= []).push(...file.filter((r) => r.statement.app === ref));
|
|
11518
11661
|
} catch {}
|
|
11519
11662
|
}
|
|
11520
11663
|
return out;
|
|
11521
11664
|
}
|
|
11665
|
+
/**
|
|
11666
|
+
* `readAppRevocations` (one entry that does not verify refuses the file, null here), with a pause every few ms: the
|
|
11667
|
+
* engine shares the page's thread, and each entry is a signature check. The same bytes are not checked again.
|
|
11668
|
+
*/
|
|
11669
|
+
async readRevocationsFile(bytes) {
|
|
11670
|
+
const id = toBase64Url(sha256(bytes));
|
|
11671
|
+
const known = this.revocationFiles.get(id);
|
|
11672
|
+
if (known !== void 0) return known;
|
|
11673
|
+
const read = readCanonicalJson(bytes);
|
|
11674
|
+
let out = null;
|
|
11675
|
+
if (read.ok && Array.isArray(read.value) && read.value.length <= APP_STATEMENT_LIMITS.revocations) {
|
|
11676
|
+
out = [];
|
|
11677
|
+
let since = performance.now();
|
|
11678
|
+
for (const entry of read.value) {
|
|
11679
|
+
if (performance.now() - since >= REVOCATIONS_SLICE_MS) {
|
|
11680
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
11681
|
+
since = performance.now();
|
|
11682
|
+
}
|
|
11683
|
+
const one = readAppRevocation(entry);
|
|
11684
|
+
if (!one.ok) {
|
|
11685
|
+
out = null;
|
|
11686
|
+
break;
|
|
11687
|
+
}
|
|
11688
|
+
out.push(one.revocation);
|
|
11689
|
+
}
|
|
11690
|
+
}
|
|
11691
|
+
this.revocationFiles.delete(id);
|
|
11692
|
+
this.revocationFiles.set(id, out);
|
|
11693
|
+
for (const old of this.revocationFiles.keys()) if (this.revocationFiles.size > REVOCATION_FILES_KEPT) this.revocationFiles.delete(old);
|
|
11694
|
+
return out;
|
|
11695
|
+
}
|
|
11522
11696
|
summary(s) {
|
|
11523
11697
|
const index = s.index;
|
|
11524
11698
|
return {
|
|
@@ -11675,8 +11849,10 @@ var Apps = class {
|
|
|
11675
11849
|
problem: error instanceof AppFetchError ? error.code : "network"
|
|
11676
11850
|
};
|
|
11677
11851
|
}
|
|
11678
|
-
|
|
11679
|
-
|
|
11852
|
+
const now = await this.storeRecord(s.key);
|
|
11853
|
+
if (!now || now.removed) continue;
|
|
11854
|
+
if (now === s && next.index === s.index && next.digest === s.digest && next.problem === s.problem && next.equivocation === s.equivocation) (await this.records()).set(s.key, next);
|
|
11855
|
+
else await this.putStore(next);
|
|
11680
11856
|
out.push(this.summary(next));
|
|
11681
11857
|
}
|
|
11682
11858
|
return out;
|
|
@@ -12055,7 +12231,11 @@ var Apps = class {
|
|
|
12055
12231
|
}
|
|
12056
12232
|
const after = await this.readRevocations(app.ref, [app.from, ...app.manifest.sources ?? []], revokeRead);
|
|
12057
12233
|
const revocations = before || after ? [...before ?? [], ...after ?? []] : null;
|
|
12058
|
-
const versions = [
|
|
12234
|
+
const versions = [
|
|
12235
|
+
app,
|
|
12236
|
+
...app.pending ? [versionOf(app.pending.manifest, app.pending.digest)] : [],
|
|
12237
|
+
...this.startedBefore(app)
|
|
12238
|
+
];
|
|
12059
12239
|
const stays = (app.revocations ?? []).filter((r) => !revocations || versions.some((v) => appRevocationFor(v, [r])));
|
|
12060
12240
|
const merged = new Map([...stays, ...revocations ?? []].map((r) => [canonicalJson(r), r]));
|
|
12061
12241
|
if ((await this.app(app.ref))?.store === void 0 && app.store !== void 0) {
|
|
@@ -12146,6 +12326,9 @@ var Apps = class {
|
|
|
12146
12326
|
if (run.status === "removed") this.anyway.add(`${app.ref} ${app.digest}`);
|
|
12147
12327
|
const bundle = await this.verifiedBundle(app);
|
|
12148
12328
|
const entry = bundle.files.get(bundle.manifest.entry) ?? fail("damaged", "The entry is missing");
|
|
12329
|
+
this.ran.add(app.ref);
|
|
12330
|
+
const versions = this.started.get(app.ref) ?? /* @__PURE__ */ new Map();
|
|
12331
|
+
this.started.set(app.ref, versions.set(app.digest, app.sequence));
|
|
12149
12332
|
return {
|
|
12150
12333
|
ref: app.ref,
|
|
12151
12334
|
digest: app.digest,
|
|
@@ -12156,9 +12339,10 @@ var Apps = class {
|
|
|
12156
12339
|
entry: utf8Decode(entry)
|
|
12157
12340
|
};
|
|
12158
12341
|
}
|
|
12159
|
-
/** A file of the installed bundle, for the broker's `file` (`ghostly.file(path)`). */
|
|
12160
|
-
async file({ ref, path }) {
|
|
12161
|
-
const app = path === "icon.png" ? await this.installedOrFail(ref) : await this.runningOrFail(ref);
|
|
12342
|
+
/** A file of the installed bundle, for the broker's `file` (`ghostly.file(path)`). `digest`: the version the frame runs. */
|
|
12343
|
+
async file({ ref, path, digest }) {
|
|
12344
|
+
const app = path === "icon.png" ? await this.installedOrFail(ref) : await this.runningOrFail(ref, digest);
|
|
12345
|
+
if (path !== "icon.png" && digest !== void 0 && digest !== app.digest) fail("no-file", "No such file in this app");
|
|
12162
12346
|
const icon = path === "icon.png" ? this.icons.get(app.digest) : void 0;
|
|
12163
12347
|
if (icon) return icon.slice();
|
|
12164
12348
|
if (icon === null) fail("no-file", "No such file in this app");
|
|
@@ -12169,6 +12353,10 @@ var Apps = class {
|
|
|
12169
12353
|
async view(app, stores) {
|
|
12170
12354
|
const { manifest } = app;
|
|
12171
12355
|
const listedBy = this.listedBy(app.ref, stores);
|
|
12356
|
+
const stopped = this.startedBefore(app).map((v) => ({
|
|
12357
|
+
digest: v.digest,
|
|
12358
|
+
run: this.runStatus(v, this.storeViews(stores), app.revocations)
|
|
12359
|
+
})).filter((v) => v.run.status !== "ok");
|
|
12172
12360
|
return {
|
|
12173
12361
|
ref: app.ref,
|
|
12174
12362
|
name: app.name,
|
|
@@ -12198,7 +12386,8 @@ var Apps = class {
|
|
|
12198
12386
|
sequence: app.equivocation.sequence,
|
|
12199
12387
|
at: app.equivocation.at
|
|
12200
12388
|
} },
|
|
12201
|
-
...app.checkedAt !== void 0 && { checkedAt: app.checkedAt }
|
|
12389
|
+
...app.checkedAt !== void 0 && { checkedAt: app.checkedAt },
|
|
12390
|
+
...stopped.length && { stopped }
|
|
12202
12391
|
};
|
|
12203
12392
|
}
|
|
12204
12393
|
async list() {
|
|
@@ -12214,8 +12403,13 @@ var Apps = class {
|
|
|
12214
12403
|
const tx = (await openDb()).transaction([STORES$1.apps, STORES$1.appStorage], "readwrite");
|
|
12215
12404
|
tx.objectStore(STORES$1.apps).delete(app.ref);
|
|
12216
12405
|
tx.objectStore(STORES$1.appStorage).delete(rowsOf(app.ref));
|
|
12217
|
-
|
|
12406
|
+
try {
|
|
12407
|
+
await done(tx);
|
|
12408
|
+
} finally {
|
|
12409
|
+
this.held.delete(app.ref);
|
|
12410
|
+
}
|
|
12218
12411
|
for (const key of [...this.usage.keys()]) if (key.startsWith(`${app.ref}\0`)) this.usage.delete(key);
|
|
12412
|
+
this.started.delete(app.ref);
|
|
12219
12413
|
await this.removeBundle(app.bytes, app.digest);
|
|
12220
12414
|
if (app.pending) await this.removeBundle(app.pending.bytes, app.pending.digest);
|
|
12221
12415
|
}
|
|
@@ -12246,8 +12440,8 @@ var Apps = class {
|
|
|
12246
12440
|
}
|
|
12247
12441
|
return used;
|
|
12248
12442
|
}
|
|
12249
|
-
async storageGet({ ref, scope, key }) {
|
|
12250
|
-
const app = await this.runningOrFail(ref);
|
|
12443
|
+
async storageGet({ ref, scope, key, digest }) {
|
|
12444
|
+
const app = await this.runningOrFail(ref, digest);
|
|
12251
12445
|
const row = await wrap((await store(STORES$1.appStorage, "readonly")).get([
|
|
12252
12446
|
app.ref,
|
|
12253
12447
|
this.scopeOf(scope),
|
|
@@ -12256,8 +12450,8 @@ var Apps = class {
|
|
|
12256
12450
|
return row ? { value: JSON.parse(row.value) } : null;
|
|
12257
12451
|
}
|
|
12258
12452
|
/** Keeps a JSON value under a key: at most 64 KiB, and 5 MiB for every key of this app in this scope. */
|
|
12259
|
-
async storageSet({ ref, scope, key, value }) {
|
|
12260
|
-
const app = await this.runningOrFail(ref);
|
|
12453
|
+
async storageSet({ ref, scope, key, value, digest }) {
|
|
12454
|
+
const app = await this.runningOrFail(ref, digest);
|
|
12261
12455
|
const where = this.scopeOf(scope), name = this.keyOf(key);
|
|
12262
12456
|
let text;
|
|
12263
12457
|
try {
|
|
@@ -12293,8 +12487,8 @@ var Apps = class {
|
|
|
12293
12487
|
this.usage.set(`${app.ref}\0${where}`, next);
|
|
12294
12488
|
});
|
|
12295
12489
|
}
|
|
12296
|
-
async storageDelete({ ref, scope, key }) {
|
|
12297
|
-
const app = await this.runningOrFail(ref);
|
|
12490
|
+
async storageDelete({ ref, scope, key, digest }) {
|
|
12491
|
+
const app = await this.runningOrFail(ref, digest);
|
|
12298
12492
|
const where = this.scopeOf(scope), name = this.keyOf(key);
|
|
12299
12493
|
await this.serial(`${app.ref}\0${where}`, async () => {
|
|
12300
12494
|
const used = await this.usageOf(app.ref, where);
|
|
@@ -12312,8 +12506,8 @@ var Apps = class {
|
|
|
12312
12506
|
this.usage.set(`${app.ref}\0${where}`, used - old.size);
|
|
12313
12507
|
});
|
|
12314
12508
|
}
|
|
12315
|
-
async storageKeys({ ref, scope }) {
|
|
12316
|
-
const app = await this.runningOrFail(ref);
|
|
12509
|
+
async storageKeys({ ref, scope, digest }) {
|
|
12510
|
+
const app = await this.runningOrFail(ref, digest);
|
|
12317
12511
|
return (await wrap((await store(STORES$1.appStorage, "readonly")).getAllKeys(rowsOf(app.ref, this.scopeOf(scope))))).map((k) => k[2]);
|
|
12318
12512
|
}
|
|
12319
12513
|
/** An app's storage in every scope, as export files (WISP 1200 § Where installed apps live), offered before an uninstall. */
|
|
@@ -14629,6 +14823,8 @@ var EnrollInviter = class extends Enrollment {
|
|
|
14629
14823
|
stopDoneWait = null;
|
|
14630
14824
|
/** A joiner was seen on the link: from then it has as long as a joiner waits for the proof to open a session. */
|
|
14631
14825
|
seen = false;
|
|
14826
|
+
/** The person answered the digits: one answer is taken, before the record is read or written for it. */
|
|
14827
|
+
answered = false;
|
|
14632
14828
|
constructor(options) {
|
|
14633
14829
|
super(options, {
|
|
14634
14830
|
role: "inviter",
|
|
@@ -14743,7 +14939,8 @@ var EnrollInviter = class extends Enrollment {
|
|
|
14743
14939
|
* and nothing was written.
|
|
14744
14940
|
*/
|
|
14745
14941
|
async confirm(match) {
|
|
14746
|
-
if (this.over || this.view.step !== "confirm" || !this.joiner) throw new Error("There are no digits to confirm");
|
|
14942
|
+
if (this.over || this.answered || this.view.step !== "confirm" || !this.joiner) throw new Error("There are no digits to confirm");
|
|
14943
|
+
this.answered = true;
|
|
14747
14944
|
if (!match) {
|
|
14748
14945
|
await this.fail("digits", "digits", this.joiner.name);
|
|
14749
14946
|
return this.view;
|
|
@@ -20422,6 +20619,11 @@ var EditQueue = class {
|
|
|
20422
20619
|
sent = /* @__PURE__ */ new Map();
|
|
20423
20620
|
/** When a card that goes again last went (`CARD_RESTORE_GAP_MS`). */
|
|
20424
20621
|
restoredAt = -Infinity;
|
|
20622
|
+
/** The rows known to have an edit on its way, each with when it was last told (`edited`), by `tick`. */
|
|
20623
|
+
known = /* @__PURE__ */ new Map();
|
|
20624
|
+
tick = 0;
|
|
20625
|
+
/** The chat's history was read for the rows with an edit on its way: `known` has them all, until the next session. */
|
|
20626
|
+
learnt = false;
|
|
20425
20627
|
timer;
|
|
20426
20628
|
flushing = null;
|
|
20427
20629
|
again = false;
|
|
@@ -20464,17 +20666,48 @@ var EditQueue = class {
|
|
|
20464
20666
|
*/
|
|
20465
20667
|
reopened() {
|
|
20466
20668
|
this.sent.clear();
|
|
20669
|
+
this.learnt = false;
|
|
20670
|
+
}
|
|
20671
|
+
/** This row has an edit to send (it was just written so): the next look reads it. */
|
|
20672
|
+
edited(id) {
|
|
20673
|
+
this.known.set(id, ++this.tick);
|
|
20674
|
+
}
|
|
20675
|
+
/**
|
|
20676
|
+
* My rows with an edit on its way, by time. `all`: the chat's whole history, when it was read for them (no `row` to
|
|
20677
|
+
* read one by, or nothing learnt yet in this session). Otherwise the known rows are read alone, and one that waits
|
|
20678
|
+
* no more is forgotten, unless it was told again while it was read.
|
|
20679
|
+
*/
|
|
20680
|
+
async pending() {
|
|
20681
|
+
const waits = (m) => m.sender === "me" && !!m.edit?.pending;
|
|
20682
|
+
const row = this.deps.row;
|
|
20683
|
+
if (!row || !this.learnt) {
|
|
20684
|
+
const all = await this.deps.read(), rows = all.filter(waits);
|
|
20685
|
+
for (const m of rows) if (!this.known.has(m.id)) this.known.set(m.id, this.tick);
|
|
20686
|
+
this.learnt = true;
|
|
20687
|
+
return {
|
|
20688
|
+
rows,
|
|
20689
|
+
all
|
|
20690
|
+
};
|
|
20691
|
+
}
|
|
20692
|
+
const asOf = this.tick, ids = [...this.known.keys()];
|
|
20693
|
+
const read = await Promise.all(ids.map((id) => row(id)));
|
|
20694
|
+
const rows = [];
|
|
20695
|
+
read.forEach((m, i) => {
|
|
20696
|
+
if (m && waits(m)) rows.push(m);
|
|
20697
|
+
else if ((this.known.get(ids[i]) ?? 0) <= asOf) this.known.delete(ids[i]);
|
|
20698
|
+
});
|
|
20699
|
+
return { rows: rows.sort((a, b) => a.timestamp - b.timestamp) };
|
|
20467
20700
|
}
|
|
20468
20701
|
/** The contact confirmed edit `seq` of my message with this wire id. */
|
|
20469
20702
|
async received(wireId, seq) {
|
|
20470
|
-
const row = (await this.
|
|
20703
|
+
const row = (await this.pending()).rows.find((m) => m.wireId === wireId);
|
|
20471
20704
|
if (!row?.edit?.pending || row.edit.seq > seq) return;
|
|
20472
20705
|
this.sent.delete(row.id);
|
|
20473
20706
|
await this.deps.settle(row.id, row.edit.seq);
|
|
20474
20707
|
}
|
|
20475
20708
|
/** The contact confirmed, on the DHT floor, the edit that went there under this id (`dhtEditId`). */
|
|
20476
20709
|
async receivedOnDht(id) {
|
|
20477
|
-
const row = (await this.
|
|
20710
|
+
const row = (await this.pending()).rows.find((m) => m.wireId && dhtEditId(m.wireId, m.edit.seq) === id);
|
|
20478
20711
|
if (!row) return;
|
|
20479
20712
|
this.sent.delete(row.id);
|
|
20480
20713
|
await this.deps.settle(row.id, row.edit.seq);
|
|
@@ -20488,13 +20721,13 @@ var EditQueue = class {
|
|
|
20488
20721
|
if (this.stopped) return;
|
|
20489
20722
|
const now = this.now();
|
|
20490
20723
|
const said = (m) => this.sent.get(m.id)?.seq === m.edit.seq ? 1 : 0;
|
|
20491
|
-
const rows = await this.
|
|
20724
|
+
const { rows, all } = await this.pending();
|
|
20725
|
+
const due = rows.filter((m) => m.wireId).sort((a, b) => said(a) - said(b) || a.edit.at - b.edit.at);
|
|
20492
20726
|
const newest = /* @__PURE__ */ new Map();
|
|
20493
|
-
for (const m of
|
|
20727
|
+
if (due.some((m) => m.edit.restore)) for (const m of all ?? await this.deps.read()) {
|
|
20494
20728
|
const key = m.sender === "me" && cardKey(m);
|
|
20495
20729
|
if (key && m.timestamp > (newest.get(key) ?? -Infinity)) newest.set(key, m.timestamp);
|
|
20496
20730
|
}
|
|
20497
|
-
const due = rows.filter((m) => m.sender === "me" && m.edit?.pending && m.wireId).sort((a, b) => said(a) - said(b) || a.edit.at - b.edit.at);
|
|
20498
20731
|
const gap = this.deps.restoreGapMs ?? CARD_RESTORE_GAP_MS;
|
|
20499
20732
|
let next = Infinity;
|
|
20500
20733
|
for (const message of due) {
|
|
@@ -21297,9 +21530,8 @@ var db = {
|
|
|
21297
21530
|
/** The group and its history. */
|
|
21298
21531
|
async deleteGroup(groupId) {
|
|
21299
21532
|
await wrap((await store(STORES$1.groups, "readwrite")).delete(groupId));
|
|
21300
|
-
const
|
|
21301
|
-
|
|
21302
|
-
await Promise.all(keys.map((key) => wrap(messages.delete(key))));
|
|
21533
|
+
const link = `group:${groupId}`;
|
|
21534
|
+
await wrap((await store(STORES$1.messages, "readwrite")).delete(IDBKeyRange.bound([link], [link, []])));
|
|
21303
21535
|
},
|
|
21304
21536
|
async getServices() {
|
|
21305
21537
|
return wrap((await store(STORES$1.services, "readonly")).getAll());
|
|
@@ -21374,6 +21606,11 @@ function noteAfterChange(current, message) {
|
|
|
21374
21606
|
snippet
|
|
21375
21607
|
};
|
|
21376
21608
|
}
|
|
21609
|
+
/** The note as its message's row has it now, when that reaction still stands there (not replaced, not taken back). */
|
|
21610
|
+
function noteIfStanding(current, message) {
|
|
21611
|
+
const r = message.reactions?.[current.by];
|
|
21612
|
+
return r?.e && r.e === current.emoji && r.at === current.at ? note(message, current.by, r.e, r.at) : void 0;
|
|
21613
|
+
}
|
|
21377
21614
|
/**
|
|
21378
21615
|
* My reactions in a private group to say again to an edge that opened, in case it missed them: the newest `limit`,
|
|
21379
21616
|
* newest first (what the member is likeliest to have missed lands before its pace runs out), of this membership only.
|
|
@@ -21392,11 +21629,10 @@ function groupReactionsToResend(messages, limit) {
|
|
|
21392
21629
|
}] : [];
|
|
21393
21630
|
}).sort((a, b) => b.n - a.n).slice(0, limit);
|
|
21394
21631
|
}
|
|
21395
|
-
/** This side's highest number in a chat: the next one goes past it
|
|
21396
|
-
function lastMine(messages
|
|
21632
|
+
/** This side's highest number in a chat's history: the next one goes past it. */
|
|
21633
|
+
function lastMine(messages) {
|
|
21397
21634
|
let last = 0;
|
|
21398
21635
|
for (const m of messages) last = Math.max(last, m.reactions?.me?.n ?? 0);
|
|
21399
|
-
for (const p of pending) last = Math.max(last, p.n);
|
|
21400
21636
|
return last;
|
|
21401
21637
|
}
|
|
21402
21638
|
var Reactions = class {
|
|
@@ -21407,6 +21643,11 @@ var Reactions = class {
|
|
|
21407
21643
|
queue = Promise.resolve();
|
|
21408
21644
|
/** A group member's reactions taken per window (chat and member key), as a paired chat's session limits them. */
|
|
21409
21645
|
memberPace = /* @__PURE__ */ new Map();
|
|
21646
|
+
/**
|
|
21647
|
+
* This side's highest number per chat: read from its history the first time I react in it, then each number given
|
|
21648
|
+
* out. It only has to grow, so a row deleted since leaves it too high and nothing is wrong.
|
|
21649
|
+
*/
|
|
21650
|
+
last = /* @__PURE__ */ new Map();
|
|
21410
21651
|
constructor(host) {
|
|
21411
21652
|
this.host = host;
|
|
21412
21653
|
}
|
|
@@ -21428,17 +21669,19 @@ var Reactions = class {
|
|
|
21428
21669
|
if (typeof messageId !== "string" || !messageId) return { error: "No message to react to" };
|
|
21429
21670
|
const e = emoji === "" ? "" : reactionEmoji(emoji);
|
|
21430
21671
|
if (e === null) return { error: "A reaction is one emoji" };
|
|
21431
|
-
const
|
|
21432
|
-
const
|
|
21433
|
-
const id = target && replyRef(target, group);
|
|
21672
|
+
const target = await this.named(chat, messageId);
|
|
21673
|
+
const id = target && replyRef(target, isGroup$1(chat));
|
|
21434
21674
|
if (!target || !id) return { error: "That message is not in this chat, or takes no reaction" };
|
|
21435
21675
|
const current = target.reactions?.me;
|
|
21436
21676
|
if (!e && !current?.e) return { error: "No reaction of yours to take back" };
|
|
21677
|
+
let last = this.last.get(chat) ?? lastMine(await this.host.messages(chat));
|
|
21678
|
+
for (const p of pending) last = Math.max(last, p.n);
|
|
21437
21679
|
const reaction = {
|
|
21438
21680
|
id,
|
|
21439
21681
|
e,
|
|
21440
|
-
n: nextReactionNumber(
|
|
21682
|
+
n: nextReactionNumber(last, this.now())
|
|
21441
21683
|
};
|
|
21684
|
+
this.last.set(chat, reaction.n);
|
|
21442
21685
|
return {
|
|
21443
21686
|
reaction,
|
|
21444
21687
|
message: await this.write(chat, target, "me", reaction) ?? target
|
|
@@ -21477,6 +21720,12 @@ var Reactions = class {
|
|
|
21477
21720
|
for (const { by, reaction } of mine) await this.write(message.linkId, message, by, reaction);
|
|
21478
21721
|
});
|
|
21479
21722
|
}
|
|
21723
|
+
/** The row I react to: by its id here, as the pages name it, read alone; else by the id both sides know it by. */
|
|
21724
|
+
async named(chat, id) {
|
|
21725
|
+
if (this.host.message) return await this.host.message(chat, id) ?? this.find(chat, id);
|
|
21726
|
+
const messages = await this.host.messages(chat);
|
|
21727
|
+
return messages.find((m) => m.id === id) ?? reactionTarget(messages, id, isGroup$1(chat));
|
|
21728
|
+
}
|
|
21480
21729
|
/**
|
|
21481
21730
|
* The row a received reaction names. Most often the row of that id (a group's), or of `peer_` or `me_` and that id (a
|
|
21482
21731
|
* chat's): those are read alone. Any other (a file's, a payment's, one not here yet) is looked for in the whole chat;
|
|
@@ -21493,9 +21742,10 @@ var Reactions = class {
|
|
|
21493
21742
|
}
|
|
21494
21743
|
return reactionTarget(await this.host.messages(chat), id, group);
|
|
21495
21744
|
}
|
|
21496
|
-
/** A chat is gone: nothing waits for it any more. */
|
|
21745
|
+
/** A chat is gone: nothing waits for it any more, and my highest number in it is read again if it comes back. */
|
|
21497
21746
|
forget(chat) {
|
|
21498
21747
|
this.waiting.delete(chat);
|
|
21748
|
+
this.last.delete(chat);
|
|
21499
21749
|
}
|
|
21500
21750
|
wait(chat, by, reaction) {
|
|
21501
21751
|
const now = this.now();
|
|
@@ -21552,13 +21802,16 @@ function pinOfFormerMember(pin, inRoster) {
|
|
|
21552
21802
|
function mayPinIn(group) {
|
|
21553
21803
|
return !!group?.myKey && group.status === "active" && mayPin(group.profile, group.myKey, group.members.find((m) => m.role === "admin")?.key);
|
|
21554
21804
|
}
|
|
21555
|
-
/** The row a pin names, when it is here: the row of that id (a group's), or of `peer_`/`me_` and that id, or any other. */
|
|
21805
|
+
/** The row a pin names, when it is here: the row of that id (a group's), or of `peer_`/`me_` and that id, or any other of a chat's. */
|
|
21556
21806
|
async function pinnedRow(host, chat, id) {
|
|
21557
21807
|
if (!id) return void 0;
|
|
21558
21808
|
const group = isGroup(chat);
|
|
21559
|
-
if (host.message)
|
|
21560
|
-
const
|
|
21561
|
-
|
|
21809
|
+
if (host.message) {
|
|
21810
|
+
for (const rowId of group ? [id] : [`peer_${id}`, `me_${id}`]) {
|
|
21811
|
+
const row = await host.message(chat, rowId);
|
|
21812
|
+
if (row && replyRef(row, group) === id) return row;
|
|
21813
|
+
}
|
|
21814
|
+
if (group) return void 0;
|
|
21562
21815
|
}
|
|
21563
21816
|
return reactionTarget(await host.messages(chat), id, group);
|
|
21564
21817
|
}
|
|
@@ -21783,6 +22036,17 @@ var FAREWELL_RETRY_MS = 6e4;
|
|
|
21783
22036
|
var FAREWELL_TOLD_MS = 6e5;
|
|
21784
22037
|
var FAREWELL_SAY_MS = 1e4;
|
|
21785
22038
|
/**
|
|
22039
|
+
* A key asking in my lobby that no commit I hold names may be a member let in by a commit I have not heard yet: a hub
|
|
22040
|
+
* keeps an edge to it for a moment, on which it says where it is and takes commits (`wantAhead`). At most `AHEAD_EDGES`
|
|
22041
|
+
* at a time, each awaited `AHEAD_WAIT_MS` and closed `AHEAD_LINGER_MS` after it came up unless a commit named the key
|
|
22042
|
+
* by then; the same key is not waited for again for a while (longer once its edge was up).
|
|
22043
|
+
*/
|
|
22044
|
+
var AHEAD_EDGES = 4;
|
|
22045
|
+
var AHEAD_WAIT_MS = 12e4;
|
|
22046
|
+
var AHEAD_LINGER_MS = 2e4;
|
|
22047
|
+
var AHEAD_RETRY_MS = 6e4;
|
|
22048
|
+
var AHEAD_HEARD_MS = 6e5;
|
|
22049
|
+
/**
|
|
21786
22050
|
* A key asking from before the commit that took it out was there and was sent that commit: not waited for. The two
|
|
21787
22051
|
* times are two devices' clocks (the asker's, the commit's signer's), so "before" allows this much between them:
|
|
21788
22052
|
* a returning member whose clock runs behind the admin's is told all the same.
|
|
@@ -22254,6 +22518,7 @@ var Communities = class {
|
|
|
22254
22518
|
await this.publishBeacon(group.id, live, now, false).catch(() => {});
|
|
22255
22519
|
}
|
|
22256
22520
|
live.farewells.clear();
|
|
22521
|
+
live.ahead.clear();
|
|
22257
22522
|
await this.reconcile(group.id, live, now);
|
|
22258
22523
|
continue;
|
|
22259
22524
|
}
|
|
@@ -22347,6 +22612,7 @@ var Communities = class {
|
|
|
22347
22612
|
if (this.leftItself(live, h.key) ? h.ts - FAREWELL_SKEW_MS > out : h.ts + FAREWELL_SKEW_MS > out) this.wantFarewell(groupId, live, h.key, now);
|
|
22348
22613
|
}
|
|
22349
22614
|
this.endFarewells(live, now);
|
|
22615
|
+
this.endAhead(groupId, live, now);
|
|
22350
22616
|
if (live.hub) {
|
|
22351
22617
|
const busy = this.admitting(groupId, live, now);
|
|
22352
22618
|
if (s.state.pendingLeaves.some((r) => r.s !== me && this.recentHub(live, r.s))) live.lobbyBusyUntil = Math.max(live.lobbyBusyUntil, now + this.timings.lobbyBusyMs);
|
|
@@ -22425,8 +22691,9 @@ var Communities = class {
|
|
|
22425
22691
|
}
|
|
22426
22692
|
await this.keepLooking(groupId, live, now);
|
|
22427
22693
|
for (const [key, since] of live.pendingEntries) {
|
|
22428
|
-
const linkId = this.host.entries(groupId).get(key);
|
|
22429
|
-
|
|
22694
|
+
const linkId = this.host.entries(groupId).get(key), up = !!linkId && this.host.linkReady(linkId, 2);
|
|
22695
|
+
const elsewhere = rosterHas(s.roster, key) && since < this.addedAt(s, key) + this.timings.knockFallbackMs;
|
|
22696
|
+
if (!up && (elsewhere || now - since > ENTRY_UNANSWERED_MS)) live.pendingEntries.delete(key);
|
|
22430
22697
|
else if (now - since > ENTRY_TIMEOUT_MS$1) {
|
|
22431
22698
|
live.pendingEntries.delete(key);
|
|
22432
22699
|
this.refused.set(key, now + REFUSED_FOR_MS$1);
|
|
@@ -22485,7 +22752,7 @@ var Communities = class {
|
|
|
22485
22752
|
const edges = this.host.edges(groupId);
|
|
22486
22753
|
if (now - live.beaconAt <= 2 * this.timings.beaconReadMs) {
|
|
22487
22754
|
const hubs = new Set(freshHubs(this.hubs(live), now).map((h) => h.key));
|
|
22488
|
-
for (const [key, id] of edges) if (!hubs.has(key) && !live.members.has(key) && this.host.linkReady(id, 2) && !this.gone(live, key)) live.members.set(key, now);
|
|
22755
|
+
for (const [key, id] of edges) if (!hubs.has(key) && !live.members.has(key) && this.host.linkReady(id, 2) && rosterHas(live.session.roster, key) && !this.gone(live, key)) live.members.set(key, now);
|
|
22489
22756
|
for (const key of [...live.members.keys()]) if (hubs.has(key)) live.members.delete(key);
|
|
22490
22757
|
}
|
|
22491
22758
|
for (const [key, since] of live.members) {
|
|
@@ -22623,16 +22890,21 @@ var Communities = class {
|
|
|
22623
22890
|
for (let i = chain.length - 1; i >= 0; i--) if ((chain[i].k === "leave" || chain[i].k === "remove") && chain[i].s === key) return chain[i].k === "leave";
|
|
22624
22891
|
return false;
|
|
22625
22892
|
}
|
|
22626
|
-
/**
|
|
22627
|
-
|
|
22628
|
-
|
|
22893
|
+
/**
|
|
22894
|
+
* A frame goes on an edge to a member; to someone the chain took out, only the commit that took it out. What the
|
|
22895
|
+
* group says (`said`: a frame of mine for everyone, or one a hub relays) goes to the members of the roster only:
|
|
22896
|
+
* a key no commit names hears none of it, and one let in on a branch that lost hears the commits.
|
|
22897
|
+
*/
|
|
22898
|
+
mayHear(live, key, frame, said = false) {
|
|
22629
22899
|
const f = frame;
|
|
22630
|
-
return f.t === "group-commit" && (f.commit?.k === "remove" || f.commit?.k === "leave") && f.commit.s === key;
|
|
22900
|
+
if (live?.session.wasRemoved(key)) return f.t === "group-commit" && (f.commit?.k === "remove" || f.commit?.k === "leave") && f.commit.s === key;
|
|
22901
|
+
if (!live || !said || rosterHas(live.session.roster, key)) return true;
|
|
22902
|
+
return f.t === "group-commit" && live.session.isRecentMember(key);
|
|
22631
22903
|
}
|
|
22632
|
-
/** The edges of the group that may hear `frame
|
|
22904
|
+
/** The edges of the group that may hear `frame`, said to everyone. */
|
|
22633
22905
|
hearers(groupId, frame) {
|
|
22634
22906
|
const live = this.live.get(groupId);
|
|
22635
|
-
return [...this.host.edges(groupId)].filter(([key]) => this.mayHear(live, key, frame));
|
|
22907
|
+
return [...this.host.edges(groupId)].filter(([key]) => this.mayHear(live, key, frame, true));
|
|
22636
22908
|
}
|
|
22637
22909
|
/** As a hub: someone the chain took out is asking (in my lobby, or listing itself as a hub). An edge to tell it, within bounds. */
|
|
22638
22910
|
wantFarewell(groupId, live, key, now) {
|
|
@@ -22653,6 +22925,34 @@ var Communities = class {
|
|
|
22653
22925
|
for (const [key, at] of live.farewellSaid) if (now - at >= FAREWELL_SAY_MS) live.farewellSaid.delete(key);
|
|
22654
22926
|
}
|
|
22655
22927
|
/**
|
|
22928
|
+
* As a hub: a key no commit I hold names is asking in my lobby. It may be a member ahead of me on the roster, who needs
|
|
22929
|
+
* the edge for me to find out (its answer to my `group-sync` carries the commit); or nobody, since whoever was once
|
|
22930
|
+
* given the group's rendezvous secret can write a key there. An edge, within bounds, that is no member's: it takes
|
|
22931
|
+
* no place among my members, and nothing the group says goes on it (`mayHear`).
|
|
22932
|
+
*/
|
|
22933
|
+
wantAhead(groupId, live, key, now) {
|
|
22934
|
+
if (live.ahead.has(key) || now < (live.aheadQuiet.get(key) ?? 0)) return;
|
|
22935
|
+
if (live.ahead.size >= AHEAD_EDGES || this.capacity(groupId, live, now) <= 0) return;
|
|
22936
|
+
live.ahead.set(key, { since: now });
|
|
22937
|
+
if (!this.host.edges(groupId).has(key)) live.expect.add(key);
|
|
22938
|
+
}
|
|
22939
|
+
/** Those edges end: a commit named the key (a member of mine now, or out), it never came, or its moment is over; and a hub no more keeps none. */
|
|
22940
|
+
endAhead(groupId, live, now) {
|
|
22941
|
+
const edges = this.host.edges(groupId);
|
|
22942
|
+
for (const [key, a] of live.ahead) {
|
|
22943
|
+
const member = rosterHas(live.session.roster, key), named = member || live.session.wasRemoved(key);
|
|
22944
|
+
if (!named && live.hub) {
|
|
22945
|
+
const id = edges.get(key);
|
|
22946
|
+
if (!a.up && id && this.host.linkReady(id, 2)) a.up = now;
|
|
22947
|
+
if (a.up ? now - a.up < AHEAD_LINGER_MS : now - a.since < AHEAD_WAIT_MS) continue;
|
|
22948
|
+
}
|
|
22949
|
+
live.ahead.delete(key);
|
|
22950
|
+
if (!named) live.aheadQuiet.set(key, now + (a.up ? AHEAD_HEARD_MS : AHEAD_RETRY_MS));
|
|
22951
|
+
else if (member && live.hub && live.members.size < this.capacity(groupId, live, now)) live.members.set(key, now);
|
|
22952
|
+
}
|
|
22953
|
+
for (const [key, until] of live.aheadQuiet) if (until <= now) live.aheadQuiet.delete(key);
|
|
22954
|
+
}
|
|
22955
|
+
/**
|
|
22656
22956
|
* Someone the chain took out says where it is (`group-sync`): it is handed the commits up to the one that took it
|
|
22657
22957
|
* out, and nothing else (`CommunitySession.farewell`). Nothing else it says is heard.
|
|
22658
22958
|
*/
|
|
@@ -22716,6 +23016,10 @@ var Communities = class {
|
|
|
22716
23016
|
if (ts + FAREWELL_SKEW_MS > (live.session.outAt(key) ?? Infinity)) this.wantFarewell(groupId, live, key, now);
|
|
22717
23017
|
continue;
|
|
22718
23018
|
}
|
|
23019
|
+
if (!rosterHas(live.session.roster, key)) {
|
|
23020
|
+
this.wantAhead(groupId, live, key, now);
|
|
23021
|
+
continue;
|
|
23022
|
+
}
|
|
22719
23023
|
live.emptySince = now;
|
|
22720
23024
|
if (!live.members.has(key) && live.members.size >= this.capacity(groupId, live, now)) continue;
|
|
22721
23025
|
if (!live.members.has(key)) {
|
|
@@ -22736,6 +23040,7 @@ var Communities = class {
|
|
|
22736
23040
|
for (const h of freshHubs(this.hubs(live), now)) if (h.key !== s.myKey && !this.hubDead(groupId, live, h, now)) wanted.add(h.key);
|
|
22737
23041
|
for (const key of live.members.keys()) wanted.add(key);
|
|
22738
23042
|
for (const key of live.farewells.keys()) wanted.add(key);
|
|
23043
|
+
for (const key of live.ahead.keys()) wanted.add(key);
|
|
22739
23044
|
} else for (const key of live.myHubs) wanted.add(key);
|
|
22740
23045
|
for (const [key, id] of existing) if (this.host.linkReady(id, 2) && (live.members.has(key) || this.recentHub(live, key)) && !s.wasRemoved(key)) wanted.add(key);
|
|
22741
23046
|
}
|
|
@@ -22856,8 +23161,7 @@ var Communities = class {
|
|
|
22856
23161
|
if (now - ts > 18e4 || this.refused.has(key) || live.pendingEntries.has(key) || key === link.host || key === s.myKey) continue;
|
|
22857
23162
|
const stillKnocking = now - ts < 2 * this.timings.slowKnockMs;
|
|
22858
23163
|
if (rosterHas(s.roster, key)) {
|
|
22859
|
-
|
|
22860
|
-
if (!stillKnocking || ts < added + this.timings.knockFallbackMs) continue;
|
|
23164
|
+
if (!stillKnocking || ts < this.addedAt(s, key) + this.timings.knockFallbackMs) continue;
|
|
22861
23165
|
}
|
|
22862
23166
|
const seen = live.knocksSeen.get(key);
|
|
22863
23167
|
const changed = !!seen && seen.doors !== doorSig;
|
|
@@ -22887,6 +23191,10 @@ var Communities = class {
|
|
|
22887
23191
|
}
|
|
22888
23192
|
for (const [key, { first }] of live.knocksSeen) if (now - first > 36e4) live.knocksSeen.delete(key);
|
|
22889
23193
|
}
|
|
23194
|
+
/** When the chain last added this member (0: never). */
|
|
23195
|
+
addedAt(s, key) {
|
|
23196
|
+
return [...s.state.chain].reverse().find((c) => c.k === "add" && c.s === key)?.ts ?? 0;
|
|
23197
|
+
}
|
|
22890
23198
|
/** Leave requests: the door commits them (see `answerKnocks`); any hub after a while. */
|
|
22891
23199
|
async commitLeaves(live, otherHubs, now) {
|
|
22892
23200
|
const s = live.session;
|
|
@@ -23133,6 +23441,7 @@ var Communities = class {
|
|
|
23133
23441
|
if (f.t === "group-sync" && live.session.isMember) this.sayFarewell(groupId, live, peerKey, f);
|
|
23134
23442
|
return;
|
|
23135
23443
|
}
|
|
23444
|
+
if (!live.session.isRecentMember(peerKey) && f.t !== "group-commit" && f.t !== "group-msg" && f.t !== "group-secrets" && !(f.t === "group-secret" && f.to === live.session.myKey)) return;
|
|
23136
23445
|
if ((f.t === "group-secret" || f.t === "group-entry") && typeof f.to === "string" && f.to !== live.session.myKey) {
|
|
23137
23446
|
if (!live.hub) return;
|
|
23138
23447
|
const id = `${String(f.t)}:${f.to}:${String(f.h ?? f.x)}`;
|
|
@@ -23187,7 +23496,7 @@ var Communities = class {
|
|
|
23187
23496
|
edgeReady(groupId, peerKey, linkId) {
|
|
23188
23497
|
const live = this.live.get(groupId);
|
|
23189
23498
|
if (!live || live.session.wasRemoved(peerKey)) return;
|
|
23190
|
-
if (live.hub && !this.peerIsHub(live, peerKey)) live.members.set(peerKey, this.now());
|
|
23499
|
+
if (live.hub && !this.peerIsHub(live, peerKey) && rosterHas(live.session.roster, peerKey)) live.members.set(peerKey, this.now());
|
|
23191
23500
|
this.sendTo(linkId, live.session.syncFrame());
|
|
23192
23501
|
this.host.emit();
|
|
23193
23502
|
}
|
|
@@ -23302,6 +23611,8 @@ var Communities = class {
|
|
|
23302
23611
|
farewells: /* @__PURE__ */ new Map(),
|
|
23303
23612
|
farewellQuiet: /* @__PURE__ */ new Map(),
|
|
23304
23613
|
farewellSaid: /* @__PURE__ */ new Map(),
|
|
23614
|
+
ahead: /* @__PURE__ */ new Map(),
|
|
23615
|
+
aheadQuiet: /* @__PURE__ */ new Map(),
|
|
23305
23616
|
knocksScanned: false,
|
|
23306
23617
|
knockCursor: 0,
|
|
23307
23618
|
lastShardPoll: 0,
|
|
@@ -24506,6 +24817,8 @@ var Groups = class {
|
|
|
24506
24817
|
removedAt = /* @__PURE__ */ new Map();
|
|
24507
24818
|
/** Who is typing in each private group, and this side's word there (WISP 902 · Group Mesh § Typing). */
|
|
24508
24819
|
typings;
|
|
24820
|
+
/** A member's pins taken per window in a private group (group and member key). */
|
|
24821
|
+
pinPaces = /* @__PURE__ */ new Map();
|
|
24509
24822
|
/** Edge signaling through members (WISP 902 · Group Mesh § Signaling through members). */
|
|
24510
24823
|
signals;
|
|
24511
24824
|
/** Files and voice messages in every group, private or community (WISP 503 · Group Files). */
|
|
@@ -24571,6 +24884,13 @@ var Groups = class {
|
|
|
24571
24884
|
if (!asItIs) await this.files?.sweep(all.map((g) => g.id));
|
|
24572
24885
|
for (const group of this.stored.values()) for (const [, linkId] of this.host.entries(group.id)) if (!group.invitation?.entry || group.invitation.linkId !== linkId) await this.host.closeEdge(linkId);
|
|
24573
24886
|
}
|
|
24887
|
+
/** The pins taken per window from one member's edge in a private group, as a paired chat's session limits them. */
|
|
24888
|
+
pinPace(groupId, member) {
|
|
24889
|
+
const key = `${groupId}\n${member}`;
|
|
24890
|
+
let pace = this.pinPaces.get(key);
|
|
24891
|
+
if (!pace) this.pinPaces.set(key, pace = new ReactionWindow(PIN_LIMITS.receive, PIN_LIMITS.windowMs));
|
|
24892
|
+
return pace;
|
|
24893
|
+
}
|
|
24574
24894
|
/** A group's pin now (WISP 400 § Pinned message), the latest one taken, unpinned or not. */
|
|
24575
24895
|
pinOf(groupId) {
|
|
24576
24896
|
return this.record(groupId)?.pin;
|
|
@@ -24883,6 +25203,15 @@ var Groups = class {
|
|
|
24883
25203
|
if (!groupOfFile(fileId, [...this.stored.keys(), ...this.communities.ids()]) || !this.files) throw new Error("No such file");
|
|
24884
25204
|
return this.files.download(fileId);
|
|
24885
25205
|
}
|
|
25206
|
+
/** The person stops a group's file that is coming or would be fetched (Cancel, Decline), or pauses it (`keep`). */
|
|
25207
|
+
haltFile(fileId, keep = false) {
|
|
25208
|
+
if (!groupOfFile(fileId, [...this.stored.keys(), ...this.communities.ids()]) || !this.files) throw new Error("No such file");
|
|
25209
|
+
return this.files.halt(fileId, keep);
|
|
25210
|
+
}
|
|
25211
|
+
/** Automatic downloads were turned off: the groups' files chosen by themselves and not here yet stop coming. */
|
|
25212
|
+
autoDownloadsOff() {
|
|
25213
|
+
return this.files?.autoDownloadsOff() ?? Promise.resolve();
|
|
25214
|
+
}
|
|
24886
25215
|
/** An edge's files/3 session opened or closed (WISP 503: the bytes of a group's files go over it). */
|
|
24887
25216
|
fileSession(groupId, peerKey, linkId, open) {
|
|
24888
25217
|
this.files?.session(groupId, peerKey, linkId, open);
|
|
@@ -25181,6 +25510,7 @@ var Groups = class {
|
|
|
25181
25510
|
this.gapsAsked.delete(groupId);
|
|
25182
25511
|
this.gapsLooked.delete(groupId);
|
|
25183
25512
|
this.typings.forget(groupId);
|
|
25513
|
+
for (const key of this.pinPaces.keys()) if (key.startsWith(`${groupId}\n`)) this.pinPaces.delete(key);
|
|
25184
25514
|
this.downSince.delete(groupId);
|
|
25185
25515
|
this.hereHeard.delete(groupId);
|
|
25186
25516
|
this.hereActed.delete(groupId);
|
|
@@ -25569,6 +25899,8 @@ var Groups = class {
|
|
|
25569
25899
|
return;
|
|
25570
25900
|
}
|
|
25571
25901
|
if (existing?.invitation?.seedB64 && (existing.invitation.linkId !== linkId || existing.invitation.admin !== frame.admin)) return;
|
|
25902
|
+
if (existing?.left && (existing.left.admin !== frame.admin || existing.contacts?.[existing.left.admin] !== linkId)) return;
|
|
25903
|
+
if (existing?.invitation && !existing.state && existing.invitation.linkId !== linkId && !existing.invitation.genesis) return;
|
|
25572
25904
|
if ([...this.stored.values()].filter((x) => x.invitation).length >= 32) return;
|
|
25573
25905
|
if (!existing?.invitation && [...this.stored.values()].filter((x) => x.invitation?.linkId === linkId).length >= MAX_INVITATIONS_PER_CHAT) {
|
|
25574
25906
|
try {
|
|
@@ -25584,6 +25916,7 @@ var Groups = class {
|
|
|
25584
25916
|
...existing?.state?.nicks
|
|
25585
25917
|
};
|
|
25586
25918
|
const previous = existing?.state ? existing : existing?.previous;
|
|
25919
|
+
const genesis = existing?.state ? commitHash(existing.state.chain[0]) : existing?.invitation?.genesis;
|
|
25587
25920
|
const invitation = {
|
|
25588
25921
|
id: g,
|
|
25589
25922
|
createdAt: Date.now(),
|
|
@@ -25592,7 +25925,8 @@ var Groups = class {
|
|
|
25592
25925
|
admin: frame.admin,
|
|
25593
25926
|
linkId,
|
|
25594
25927
|
...invitationClaims(frame),
|
|
25595
|
-
pieces: []
|
|
25928
|
+
pieces: [],
|
|
25929
|
+
...genesis ? { genesis } : {}
|
|
25596
25930
|
},
|
|
25597
25931
|
...Object.keys(known).length ? { formerNames: known } : {},
|
|
25598
25932
|
...previous ? { previous } : {}
|
|
@@ -25676,6 +26010,20 @@ var Groups = class {
|
|
|
25676
26010
|
group.invitation.pieces = [];
|
|
25677
26011
|
return;
|
|
25678
26012
|
}
|
|
26013
|
+
if (group.invitation.genesis && commitHash(joined.state.chain[0]) !== group.invitation.genesis) {
|
|
26014
|
+
try {
|
|
26015
|
+
this.host.sendOnLink(linkId, {
|
|
26016
|
+
t: "group-decline",
|
|
26017
|
+
g
|
|
26018
|
+
});
|
|
26019
|
+
} catch {}
|
|
26020
|
+
delete group.invitation.seedB64;
|
|
26021
|
+
group.invitation.pieces = [];
|
|
26022
|
+
this.acceptSaid.delete(g);
|
|
26023
|
+
await this.store.putGroup(group);
|
|
26024
|
+
this.host.emit();
|
|
26025
|
+
return;
|
|
26026
|
+
}
|
|
25679
26027
|
const viaLink = !!group.invitation.entry;
|
|
25680
26028
|
this.meet(g, group.invitation.admin);
|
|
25681
26029
|
const member = {
|
|
@@ -25791,6 +26139,8 @@ var Groups = class {
|
|
|
25791
26139
|
return;
|
|
25792
26140
|
}
|
|
25793
26141
|
if (t === "group-pin") {
|
|
26142
|
+
const said = this.pinPace(groupId, peerKey).take() ? readPin(frame) : null;
|
|
26143
|
+
if (!said || !pinIsNewer(this.record(groupId)?.pin, said)) return;
|
|
25794
26144
|
const signed = session.signedPin(frame);
|
|
25795
26145
|
if (signed && rosterHas(session.roster, peerKey)) await this.host.groupPin?.(groupId, peerKey, signed);
|
|
25796
26146
|
return;
|
|
@@ -26708,6 +27058,14 @@ var GroupEdits = class {
|
|
|
26708
27058
|
receivePace = /* @__PURE__ */ new Map();
|
|
26709
27059
|
timers = /* @__PURE__ */ new Map();
|
|
26710
27060
|
flushing = /* @__PURE__ */ new Map();
|
|
27061
|
+
/** Per group: my rows known to have an edit to say, each with when it was last told (`edit`), by `tick`. */
|
|
27062
|
+
known = /* @__PURE__ */ new Map();
|
|
27063
|
+
tick = 0;
|
|
27064
|
+
/**
|
|
27065
|
+
* The groups whose history was read for the rows with an edit to say: `known` has them all, and from then on those
|
|
27066
|
+
* rows alone are read (`host.message`). An edit costs what it does in a short group, however long this one is.
|
|
27067
|
+
*/
|
|
27068
|
+
learnt = /* @__PURE__ */ new Set();
|
|
26711
27069
|
stopped = false;
|
|
26712
27070
|
constructor(host) {
|
|
26713
27071
|
this.host = host;
|
|
@@ -26758,6 +27116,7 @@ var GroupEdits = class {
|
|
|
26758
27116
|
mentions: mentions.length ? mentions : void 0
|
|
26759
27117
|
};
|
|
26760
27118
|
});
|
|
27119
|
+
if (this.host.message) this.told(groupId).set(messageId, ++this.tick);
|
|
26761
27120
|
await this.host.changed(chat, messageId);
|
|
26762
27121
|
await this.flush(groupId).catch(() => {});
|
|
26763
27122
|
return {
|
|
@@ -26778,7 +27137,7 @@ var GroupEdits = class {
|
|
|
26778
27137
|
this.timers.delete(groupId);
|
|
26779
27138
|
if (this.stopped || !this.host.membership(groupId)) return;
|
|
26780
27139
|
const chat = chatOf(groupId), now = this.now();
|
|
26781
|
-
const due = (await this.
|
|
27140
|
+
const due = (await this.pending(groupId)).sort((a, b) => a.edit.at - b.edit.at);
|
|
26782
27141
|
let pace = this.sendPace.get(groupId);
|
|
26783
27142
|
if (!pace) this.sendPace.set(groupId, pace = new RateWindow(10, EDIT_RATE_WINDOW_MS, this.now));
|
|
26784
27143
|
let retry = Infinity;
|
|
@@ -26809,6 +27168,33 @@ var GroupEdits = class {
|
|
|
26809
27168
|
this.flush(groupId).catch(() => {});
|
|
26810
27169
|
}, Math.max(50, retry)));
|
|
26811
27170
|
}
|
|
27171
|
+
/**
|
|
27172
|
+
* My rows of a group with an edit to say. The group's whole history is read for them once (always, with no
|
|
27173
|
+
* `host.message` to read one by); after that the known rows alone, and one that waits no more is forgotten, unless
|
|
27174
|
+
* it was told again while it was read.
|
|
27175
|
+
*/
|
|
27176
|
+
async pending(groupId) {
|
|
27177
|
+
const waits = (m) => m.sender === "me" && !!m.edit?.pending;
|
|
27178
|
+
const chat = chatOf(groupId), read = this.host.message;
|
|
27179
|
+
if (!read || !this.learnt.has(groupId)) {
|
|
27180
|
+
const rows = (await this.host.messages(chat)).filter(waits);
|
|
27181
|
+
if (!read) return rows;
|
|
27182
|
+
for (const m of rows) if (!this.told(groupId).has(m.id)) this.told(groupId).set(m.id, this.tick);
|
|
27183
|
+
this.learnt.add(groupId);
|
|
27184
|
+
return rows;
|
|
27185
|
+
}
|
|
27186
|
+
const known = this.told(groupId), asOf = this.tick, ids = [...known.keys()];
|
|
27187
|
+
return (await Promise.all(ids.map((id) => read(chat, id)))).filter((m, i) => {
|
|
27188
|
+
if (m && waits(m)) return true;
|
|
27189
|
+
if ((known.get(ids[i]) ?? 0) <= asOf) known.delete(ids[i]);
|
|
27190
|
+
return false;
|
|
27191
|
+
});
|
|
27192
|
+
}
|
|
27193
|
+
told(groupId) {
|
|
27194
|
+
let known = this.known.get(groupId);
|
|
27195
|
+
if (!known) this.known.set(groupId, known = /* @__PURE__ */ new Map());
|
|
27196
|
+
return known;
|
|
27197
|
+
}
|
|
26812
27198
|
async find(chat, id) {
|
|
26813
27199
|
return this.host.message ? this.host.message(chat, id) : (await this.host.messages(chat)).find((m) => m.id === id);
|
|
26814
27200
|
}
|
|
@@ -26894,6 +27280,8 @@ var GroupEdits = class {
|
|
|
26894
27280
|
forget(groupId) {
|
|
26895
27281
|
clearTimeout(this.timers.get(groupId));
|
|
26896
27282
|
this.timers.delete(groupId);
|
|
27283
|
+
this.known.delete(groupId);
|
|
27284
|
+
this.learnt.delete(groupId);
|
|
26897
27285
|
this.buffer.forget(chatOf(groupId));
|
|
26898
27286
|
}
|
|
26899
27287
|
stop() {
|
|
@@ -27865,7 +28253,7 @@ var HoldEngine = class {
|
|
|
27865
28253
|
this.host.changed();
|
|
27866
28254
|
return;
|
|
27867
28255
|
}
|
|
27868
|
-
const pointer = packet ? keys.readPointer(packet) : null;
|
|
28256
|
+
const pointer = packet ? keys.readPointer(packet, this.host.loopbackStorage?.()) : null;
|
|
27869
28257
|
if (!pointer) return;
|
|
27870
28258
|
let hold = this.state(linkId);
|
|
27871
28259
|
if (pointer.rev < hold.peerPointerRev) return;
|
|
@@ -27904,11 +28292,12 @@ var HoldEngine = class {
|
|
|
27904
28292
|
try {
|
|
27905
28293
|
const manifest = keys.open(await this.fetchBytes(pointer.manifestUrl, HOLD_LIMITS.maxManifestBytes), {
|
|
27906
28294
|
maxBytes: HOLD_LIMITS.maxManifestBytes,
|
|
27907
|
-
now
|
|
28295
|
+
now,
|
|
28296
|
+
loopback: this.host.loopbackStorage?.()
|
|
27908
28297
|
});
|
|
27909
28298
|
if (manifest.header.kind !== "manifest") throw new HoldRefusedError("format", "Not a manifest");
|
|
27910
28299
|
mailbox = manifest.header.mailbox;
|
|
27911
|
-
entries = readManifest(manifest.header.meta).filter(([seq]) => seq > this.state(linkId).inSeq).sort((a, b) => a[0] - b[0]);
|
|
28300
|
+
entries = readManifest(manifest.header.meta, this.host.loopbackStorage?.()).filter(([seq]) => seq > this.state(linkId).inSeq).sort((a, b) => a[0] - b[0]);
|
|
27912
28301
|
} catch (error) {
|
|
27913
28302
|
if (error instanceof HoldRefusedError) {
|
|
27914
28303
|
await this.save(linkId, {
|
|
@@ -28050,9 +28439,11 @@ var HoldEngine = class {
|
|
|
28050
28439
|
}
|
|
28051
28440
|
}
|
|
28052
28441
|
async readBytes(url, max, signal) {
|
|
28442
|
+
if (!isHoldUrl(url, this.host.loopbackStorage?.())) throw new Error("The storage address is not one this app reads");
|
|
28053
28443
|
const response = await (this.host.fetch ?? ((...args) => fetch(...args)))(url, {
|
|
28054
28444
|
cache: "no-store",
|
|
28055
28445
|
credentials: "omit",
|
|
28446
|
+
redirect: "error",
|
|
28056
28447
|
signal
|
|
28057
28448
|
});
|
|
28058
28449
|
if (!response.ok) throw new Error(`The storage answered ${response.status}`);
|
|
@@ -30682,6 +31073,9 @@ var CashuWallet = class CashuWallet {
|
|
|
30682
31073
|
* taken back.
|
|
30683
31074
|
*/
|
|
30684
31075
|
var UNIT = "sat";
|
|
31076
|
+
/** A contact's requests in one chat: the new ones taken per minute, and how many may wait to be paid. */
|
|
31077
|
+
var REQUESTS_PER_MINUTE = 20;
|
|
31078
|
+
var OPEN_REQUESTS = 50;
|
|
30685
31079
|
var satsWord = (n) => `${n} sat${n === 1 ? "" : "s"}`;
|
|
30686
31080
|
/**
|
|
30687
31081
|
* A refused send's review, once its ecash came back: why, and exactly what came back. Taking ecash back is a swap at
|
|
@@ -30748,10 +31142,32 @@ var allows = (link, method) => method === "arkade" ? link.supportsArkPayments :
|
|
|
30748
31142
|
var fedimintSats = (msats) => Math.floor(msats / 1e3);
|
|
30749
31143
|
/** The distinct wallets of a per-network set (one object may stand for both). */
|
|
30750
31144
|
var distinct = (each) => [...new Set(WALLET_NETWORKS.map((n) => each[n]))];
|
|
31145
|
+
/**
|
|
31146
|
+
* Every payment this device keeps, with the copy of them the pages get. The copy is made again only after a payment
|
|
31147
|
+
* changed: a state about anything else (typing, a file's progress) carries the one already made.
|
|
31148
|
+
*/
|
|
31149
|
+
var PaymentBook = class extends Map {
|
|
31150
|
+
shown = null;
|
|
31151
|
+
set(id, payment) {
|
|
31152
|
+
this.shown = null;
|
|
31153
|
+
return super.set(id, payment);
|
|
31154
|
+
}
|
|
31155
|
+
delete(id) {
|
|
31156
|
+
this.shown = null;
|
|
31157
|
+
return super.delete(id);
|
|
31158
|
+
}
|
|
31159
|
+
clear() {
|
|
31160
|
+
this.shown = null;
|
|
31161
|
+
super.clear();
|
|
31162
|
+
}
|
|
31163
|
+
views() {
|
|
31164
|
+
return this.shown ??= Object.fromEntries([...this].map(([id, { token: _token, ...view }]) => [id, view]));
|
|
31165
|
+
}
|
|
31166
|
+
};
|
|
30751
31167
|
var PaymentDesk = class {
|
|
30752
31168
|
wallet;
|
|
30753
31169
|
host;
|
|
30754
|
-
payments =
|
|
31170
|
+
payments = new PaymentBook();
|
|
30755
31171
|
paying = /* @__PURE__ */ new Map();
|
|
30756
31172
|
receiving = /* @__PURE__ */ new Map();
|
|
30757
31173
|
reclaims = /* @__PURE__ */ new Map();
|
|
@@ -30813,8 +31229,9 @@ var PaymentDesk = class {
|
|
|
30813
31229
|
records() {
|
|
30814
31230
|
return [...this.payments.values()];
|
|
30815
31231
|
}
|
|
31232
|
+
/** What the pages see of every payment. The same record until a payment changes: read it, never write to it. */
|
|
30816
31233
|
views() {
|
|
30817
|
-
return
|
|
31234
|
+
return this.payments.views();
|
|
30818
31235
|
}
|
|
30819
31236
|
async forgetLink(linkId) {
|
|
30820
31237
|
const paymentStore = await store(STORES$1.payments, "readwrite");
|
|
@@ -30823,6 +31240,7 @@ var PaymentDesk = class {
|
|
|
30823
31240
|
this.payments.delete(payment.id);
|
|
30824
31241
|
wrap(paymentStore.delete(payment.id));
|
|
30825
31242
|
}
|
|
31243
|
+
this.requestsFrom.delete(linkId);
|
|
30826
31244
|
}
|
|
30827
31245
|
/** `confirmedReal`: the person confirmed a Mainnet send as real money; without it one is refused. */
|
|
30828
31246
|
async send(params) {
|
|
@@ -31477,9 +31895,24 @@ var PaymentDesk = class {
|
|
|
31477
31895
|
endpoints: [...payment.invoice ? [[ENDPOINT.bolt11, payment.invoice]] : [], ...payment.mints?.length ? [[ENDPOINT.cashu, cashuRequestPayload(payment.mints)]] : []]
|
|
31478
31896
|
};
|
|
31479
31897
|
}
|
|
31898
|
+
/** New requests taken from each chat's contact lately. */
|
|
31899
|
+
requestsFrom = /* @__PURE__ */ new Map();
|
|
31900
|
+
/**
|
|
31901
|
+
* Whether this chat has room for one more request of its contact: twenty new ones a minute, and fifty waiting to be
|
|
31902
|
+
* paid at most. One left out is not lost: the contact's app sends its open requests again on every session.
|
|
31903
|
+
*/
|
|
31904
|
+
roomForRequest(linkId) {
|
|
31905
|
+
let recent = this.requestsFrom.get(linkId);
|
|
31906
|
+
if (!recent) this.requestsFrom.set(linkId, recent = new RateWindow(REQUESTS_PER_MINUTE, 6e4));
|
|
31907
|
+
if (!recent.take()) return false;
|
|
31908
|
+
let open = 0;
|
|
31909
|
+
for (const p of this.payments.values()) if (p.linkId === linkId && p.kind === "request" && p.direction === "in" && p.state === "pending" && ++open >= OPEN_REQUESTS) return false;
|
|
31910
|
+
return true;
|
|
31911
|
+
}
|
|
31480
31912
|
/** `held`: picked up from the contact's storage while it was away (WISP 404): only what this device allows counts, and only Cashu or Lightning. */
|
|
31481
31913
|
async onPaymentRequest(linkId, request, held = false) {
|
|
31482
31914
|
if (this.find(linkId, request.id)?.linkId === linkId) return;
|
|
31915
|
+
if (!this.roomForRequest(linkId)) return;
|
|
31483
31916
|
if (held && (findEndpoint(request.endpoints, ENDPOINT.usdt) || findEndpoint(request.endpoints, ENDPOINT.arkade) || findEndpoint(request.endpoints, ENDPOINT.bark) || findEndpoint(request.endpoints, ENDPOINT.bitcoin) || findEndpoint(request.endpoints, ENDPOINT.fedimint) || findEndpoint(request.endpoints, ENDPOINT.spark))) return;
|
|
31484
31917
|
if (findEndpoint(request.endpoints, ENDPOINT.usdt)) return this.receiveUsdtRequest(linkId, request);
|
|
31485
31918
|
const amount = parseSats(request.amount.value, request.amount.asset);
|
|
@@ -32043,6 +32476,22 @@ var PaymentDesk = class {
|
|
|
32043
32476
|
endpoint: [ENDPOINT.usdt, JSON.stringify({ txid: review.txid })]
|
|
32044
32477
|
});
|
|
32045
32478
|
}
|
|
32479
|
+
/**
|
|
32480
|
+
* One payment pays one request: a receipt for a request that already has one, under another payment id,
|
|
32481
|
+
* is not another payment. Nothing is stored or shown for it; a paid request is answered again.
|
|
32482
|
+
*/
|
|
32483
|
+
repeatedReceipt(link, request, id) {
|
|
32484
|
+
let repeated = false;
|
|
32485
|
+
for (const p of this.payments.values()) if (p.id !== id && p.kind === "payment" && p.direction === "in" && p.requestId === request.id && p.linkId === request.linkId && p.state !== "failed") {
|
|
32486
|
+
repeated = true;
|
|
32487
|
+
break;
|
|
32488
|
+
}
|
|
32489
|
+
if (repeated && request.state === "settled") link?.sendPaymentResult({
|
|
32490
|
+
id: request.id,
|
|
32491
|
+
ok: true
|
|
32492
|
+
});
|
|
32493
|
+
return repeated;
|
|
32494
|
+
}
|
|
32046
32495
|
async receiveUsdt(linkId, payment) {
|
|
32047
32496
|
const request = payment.requestId ? this.payments.get(payment.requestId) : void 0;
|
|
32048
32497
|
if (!this.host.getLink(linkId)?.supportsUsdtPayments || request?.target?.method !== "usdt" || request.linkId !== linkId || request.direction !== "out" || request.kind !== "request" || payment.amount.asset !== request.unit || payment.amount.value !== String(request.amount)) return;
|
|
@@ -32055,6 +32504,7 @@ var PaymentDesk = class {
|
|
|
32055
32504
|
return;
|
|
32056
32505
|
}
|
|
32057
32506
|
if (typeof txid !== "string" || !/^0x[a-fA-F0-9]{64}$/.test(txid) || existing?.state === "settled") return;
|
|
32507
|
+
if (this.repeatedReceipt(this.host.getLink(linkId), request, payment.id)) return;
|
|
32058
32508
|
txid = txid.toLowerCase();
|
|
32059
32509
|
if ([...this.payments.values()].some((p) => p.txid?.toLowerCase() === txid && (p.linkId !== linkId || (p.kind === "request" ? p.id !== request.id : p.requestId !== request.id)))) return;
|
|
32060
32510
|
await this.save({
|
|
@@ -32222,7 +32672,7 @@ var PaymentDesk = class {
|
|
|
32222
32672
|
}
|
|
32223
32673
|
if ([...this.payments.values()].some((p) => p.txid === txid && (p.linkId !== linkId || (p.kind === "request" ? p.id !== request.id : p.requestId !== request.id)))) return;
|
|
32224
32674
|
if (typeof txid !== "string" || !/^[a-f0-9]{64}$/.test(txid) || existing?.state === "settled") return;
|
|
32225
|
-
if (
|
|
32675
|
+
if (this.repeatedReceipt(link, request, payment.id)) return;
|
|
32226
32676
|
await this.save({
|
|
32227
32677
|
id: payment.id,
|
|
32228
32678
|
linkId,
|
|
@@ -32340,6 +32790,7 @@ var PaymentDesk = class {
|
|
|
32340
32790
|
const existing = this.payments.get(payment.id);
|
|
32341
32791
|
if (existing && (existing.linkId !== linkId || existing.direction !== "in" || existing.requestId !== request.id)) return;
|
|
32342
32792
|
if (existing || parseSats(payment.amount.value, payment.amount.asset) !== request.amount) return;
|
|
32793
|
+
if (this.repeatedReceipt(link, request, payment.id)) return;
|
|
32343
32794
|
await this.save({
|
|
32344
32795
|
id: payment.id,
|
|
32345
32796
|
linkId,
|
|
@@ -32476,6 +32927,7 @@ var PaymentDesk = class {
|
|
|
32476
32927
|
return;
|
|
32477
32928
|
}
|
|
32478
32929
|
if (typeof hint !== "string" || !/^[0-9a-f]{64}$/.test(hint)) return;
|
|
32930
|
+
if (this.repeatedReceipt(link, request, payment.id)) return;
|
|
32479
32931
|
const settled = request.state === "settled";
|
|
32480
32932
|
await this.save({
|
|
32481
32933
|
id: payment.id,
|
|
@@ -32824,6 +33276,7 @@ var PaymentDesk = class {
|
|
|
32824
33276
|
const existing = this.payments.get(payment.id);
|
|
32825
33277
|
if (existing && (existing.linkId !== linkId || existing.direction !== "in" || existing.requestId !== request.id)) return;
|
|
32826
33278
|
if (existing || parseSats(payment.amount.value, payment.amount.asset) !== request.amount) return;
|
|
33279
|
+
if (this.repeatedReceipt(link, request, payment.id)) return;
|
|
32827
33280
|
const settled = request.state === "settled";
|
|
32828
33281
|
await this.save({
|
|
32829
33282
|
id: payment.id,
|
|
@@ -32947,6 +33400,20 @@ var PaymentDesk = class {
|
|
|
32947
33400
|
});
|
|
32948
33401
|
}
|
|
32949
33402
|
/**
|
|
33403
|
+
* A contact's receipt that still waits for its money, dropped by hand (its chat line was deleted here): the
|
|
33404
|
+
* request it answers can take another receipt. Money that does arrive still pays the request.
|
|
33405
|
+
*/
|
|
33406
|
+
async dropReceipt(id) {
|
|
33407
|
+
const payment = this.payments.get(id), request = payment?.requestId ? this.payments.get(payment.requestId) : void 0;
|
|
33408
|
+
if (payment?.kind !== "payment" || payment.direction !== "in" || payment.state !== "pending" || !payment.target) return;
|
|
33409
|
+
if (request?.kind !== "request" || request.direction !== "out" || request.linkId !== payment.linkId || request.target?.method !== payment.target.method) return;
|
|
33410
|
+
await this.save({
|
|
33411
|
+
...payment,
|
|
33412
|
+
state: "failed",
|
|
33413
|
+
error: "Removed here"
|
|
33414
|
+
});
|
|
33415
|
+
}
|
|
33416
|
+
/**
|
|
32950
33417
|
* A request of ours closed for good: the wallet it was paid through was removed. Nothing sends it again (replay,
|
|
32951
33418
|
* a group edge opening), and the contact is told now when the chat is open (a request to a group: every member
|
|
32952
33419
|
* reachable), so their app stops offering to pay it. `reason`: shown here; `told`: what the contact reads. Returns
|
|
@@ -33847,7 +34314,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
33847
34314
|
cue: "downloaded",
|
|
33848
34315
|
key: fileId
|
|
33849
34316
|
}, this.chatOf(linkId));
|
|
33850
|
-
this.noteFileEnd(linkId, fileId, record.state === "done" ? void 0 : record.error ?? record.state);
|
|
34317
|
+
this.noteFileEnd(linkId, fileId, record.state === "done" ? void 0 : record.error ?? record.state, !seen);
|
|
33851
34318
|
}
|
|
33852
34319
|
});
|
|
33853
34320
|
stateTimer = null;
|
|
@@ -34029,6 +34496,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
34029
34496
|
descriptors: () => this.providers().lightning,
|
|
34030
34497
|
host: () => this.providerHost(network),
|
|
34031
34498
|
hasMints: () => this.networkMints(network).length > 0,
|
|
34499
|
+
fetch: (...request) => (this.options.lnurlFetch ?? globalThis.fetch)(...request),
|
|
34032
34500
|
events: {
|
|
34033
34501
|
changed: () => void this.refreshWallet(),
|
|
34034
34502
|
received: (op) => void this.desk.onLightningPaid(op),
|
|
@@ -34180,12 +34648,12 @@ var GhostlyNode = class GhostlyNode {
|
|
|
34180
34648
|
await this.messagesChanged(linkId, [messageId]);
|
|
34181
34649
|
this.emitState();
|
|
34182
34650
|
},
|
|
34183
|
-
text: async (linkId, messageId) => (await db.
|
|
34651
|
+
text: async (linkId, messageId) => (await db.getMessage(linkId, messageId))?.text ?? null,
|
|
34184
34652
|
reply: async (linkId, messageId) => {
|
|
34185
|
-
const message =
|
|
34653
|
+
const message = await db.getMessage(linkId, messageId);
|
|
34186
34654
|
return message && GhostlyNode.wireReply(message);
|
|
34187
34655
|
},
|
|
34188
|
-
forwarded: async (linkId, messageId) => (await db.
|
|
34656
|
+
forwarded: async (linkId, messageId) => (await db.getMessage(linkId, messageId))?.forwarded,
|
|
34189
34657
|
file: async (fileId) => {
|
|
34190
34658
|
const stored = await fileStore.get(fileId);
|
|
34191
34659
|
if (!stored?.metadata) return null;
|
|
@@ -34289,6 +34757,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
34289
34757
|
},
|
|
34290
34758
|
receivePaymentRequest: (linkId, request) => this.desk.onPaymentRequest(linkId, request, true),
|
|
34291
34759
|
changed: () => this.emitState(),
|
|
34760
|
+
loopbackStorage: () => this.options.holdLoopback === true,
|
|
34292
34761
|
ttlMs: () => {
|
|
34293
34762
|
try {
|
|
34294
34763
|
const raw = typeof localStorage !== "undefined" ? localStorage.getItem("ghostly-test-hold-ttl") : null;
|
|
@@ -34459,6 +34928,8 @@ var GhostlyNode = class GhostlyNode {
|
|
|
34459
34928
|
* again those not confirmed after a while.
|
|
34460
34929
|
*/
|
|
34461
34930
|
reactionNotes = /* @__PURE__ */ new Map();
|
|
34931
|
+
/** Each chat's notes shown before the one shown now, oldest first, the latest `REACTION_NOTES_KEPT` of this run. */
|
|
34932
|
+
reactionNotesBefore = /* @__PURE__ */ new Map();
|
|
34462
34933
|
reactionsSent = /* @__PURE__ */ new Map();
|
|
34463
34934
|
reactionPace = /* @__PURE__ */ new Map();
|
|
34464
34935
|
reactionTimers = /* @__PURE__ */ new Map();
|
|
@@ -34480,9 +34951,12 @@ var GhostlyNode = class GhostlyNode {
|
|
|
34480
34951
|
message: (chat, id) => db.getMessage(chat, id),
|
|
34481
34952
|
patch: (chat, id, change) => db.patchMessage(chat, id, change),
|
|
34482
34953
|
changed: async (chat, id, note) => {
|
|
34483
|
-
|
|
34484
|
-
|
|
34485
|
-
|
|
34954
|
+
const shown = this.reactionNotes.get(chat);
|
|
34955
|
+
if (note) {
|
|
34956
|
+
if (shown && (shown.message !== note.message || shown.by !== note.by)) this.reactionNotesBefore.set(chat, [...this.reactionNotesBefore.get(chat) ?? [], shown].slice(-8));
|
|
34957
|
+
this.reactionNotes.set(chat, note);
|
|
34958
|
+
} else if (shown?.message === id) {
|
|
34959
|
+
const before = await this.reactionNoteNow(chat, shown);
|
|
34486
34960
|
if (before) this.reactionNotes.set(chat, before);
|
|
34487
34961
|
else this.reactionNotes.delete(chat);
|
|
34488
34962
|
}
|
|
@@ -34491,6 +34965,19 @@ var GhostlyNode = class GhostlyNode {
|
|
|
34491
34965
|
},
|
|
34492
34966
|
notify: (chat, key) => this.feedback("reaction", `${chat}:${key}`, chat)
|
|
34493
34967
|
});
|
|
34968
|
+
/**
|
|
34969
|
+
* The chat list's reaction once one to its message was taken back: the same one if it still stands, else the latest
|
|
34970
|
+
* shown before it that does, each read from its own row. Only past those is the whole history read for an older one.
|
|
34971
|
+
*/
|
|
34972
|
+
async reactionNoteNow(chat, shown) {
|
|
34973
|
+
const before = this.reactionNotesBefore.get(chat) ?? [];
|
|
34974
|
+
for (let note = shown; note; note = before.pop()) {
|
|
34975
|
+
const row = note.message ? await db.getMessage(chat, note.message) : void 0;
|
|
34976
|
+
const standing = row && noteIfStanding(note, row);
|
|
34977
|
+
if (standing) return standing;
|
|
34978
|
+
}
|
|
34979
|
+
return latestReaction(await db.getMessages(chat));
|
|
34980
|
+
}
|
|
34494
34981
|
/** Private groups (WISP 900): sessions, admission on contact chats, and the pairwise edges that carry them. */
|
|
34495
34982
|
groups = new Groups({
|
|
34496
34983
|
sendOnLink: (linkId, frame) => {
|
|
@@ -35507,8 +35994,11 @@ var GhostlyNode = class GhostlyNode {
|
|
|
35507
35994
|
appCheckUpdates() {
|
|
35508
35995
|
return this.appStore.checkUpdates();
|
|
35509
35996
|
}
|
|
35510
|
-
|
|
35511
|
-
|
|
35997
|
+
/** Uninstalls an app. One running then is closed in every chat (the contact hears `close`), and the pages are told, to stop its frames. */
|
|
35998
|
+
async appUninstall(params) {
|
|
35999
|
+
await this.appStore.uninstall(params);
|
|
36000
|
+
this.appsCloseOf(params.ref);
|
|
36001
|
+
this.events.onAppsChecked?.();
|
|
35512
36002
|
}
|
|
35513
36003
|
appRunCheck(params) {
|
|
35514
36004
|
return this.appStore.runCheck(params);
|
|
@@ -35594,17 +36084,27 @@ var GhostlyNode = class GhostlyNode {
|
|
|
35594
36084
|
...this.settings.wakeMutedGroups?.includes(group.id) && { wakeMuted: true }
|
|
35595
36085
|
};
|
|
35596
36086
|
}
|
|
36087
|
+
/** Per community, the members a payment here names, as read from one copy of the payments (`PaymentDesk.views`). */
|
|
36088
|
+
communityPaid;
|
|
35597
36089
|
/** Community members payments go to (or came from), as links the payment bubbles and the composer look up by key. */
|
|
35598
36090
|
communityPayViews(groups) {
|
|
35599
|
-
const
|
|
35600
|
-
|
|
35601
|
-
const
|
|
35602
|
-
|
|
35603
|
-
const
|
|
35604
|
-
|
|
35605
|
-
|
|
36091
|
+
const payments = this.desk.views();
|
|
36092
|
+
if (this.communityPaid?.of !== payments) {
|
|
36093
|
+
const members = /* @__PURE__ */ new Map();
|
|
36094
|
+
for (const p of Object.values(payments)) {
|
|
36095
|
+
const at = parsePayLink(p.linkId);
|
|
36096
|
+
if (at?.member) {
|
|
36097
|
+
const set = members.get(at.groupId) ?? /* @__PURE__ */ new Set();
|
|
36098
|
+
set.add(at.member);
|
|
36099
|
+
members.set(at.groupId, set);
|
|
36100
|
+
}
|
|
35606
36101
|
}
|
|
36102
|
+
this.communityPaid = {
|
|
36103
|
+
of: payments,
|
|
36104
|
+
members
|
|
36105
|
+
};
|
|
35607
36106
|
}
|
|
36107
|
+
const withPayments = this.communityPaid.members;
|
|
35608
36108
|
return groups.filter((g) => g.profile === "community" && g.status === "active" && g.myKey).flatMap((g) => this.communityPay.views(g.id, g.myKey, g.members.map((m) => m.key), withPayments.get(g.id) ?? /* @__PURE__ */ new Set()));
|
|
35609
36109
|
}
|
|
35610
36110
|
/** The payment composer opened on a member of a community: ask what they take, if they did not say lately. */
|
|
@@ -36187,6 +36687,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
36187
36687
|
this.newestAt.delete(linkId);
|
|
36188
36688
|
this.placedAt.delete(linkId);
|
|
36189
36689
|
this.reactionNotes.delete(linkId);
|
|
36690
|
+
this.reactionNotesBefore.delete(linkId);
|
|
36190
36691
|
this.events.onMessages(linkId, []);
|
|
36191
36692
|
}
|
|
36192
36693
|
async setGroupWakeMuted(groupId, muted) {
|
|
@@ -36734,7 +37235,9 @@ var GhostlyNode = class GhostlyNode {
|
|
|
36734
37235
|
card: edited.card
|
|
36735
37236
|
}));
|
|
36736
37237
|
await this.messagesChanged(linkId, [message.id]);
|
|
36737
|
-
this.editsFor(linkId)
|
|
37238
|
+
const edits = this.editsFor(linkId);
|
|
37239
|
+
edits.edited(message.id);
|
|
37240
|
+
edits.flush().catch(() => {});
|
|
36738
37241
|
return {
|
|
36739
37242
|
error: null,
|
|
36740
37243
|
messageId: message.id
|
|
@@ -36855,7 +37358,10 @@ var GhostlyNode = class GhostlyNode {
|
|
|
36855
37358
|
},
|
|
36856
37359
|
...done
|
|
36857
37360
|
};
|
|
36858
|
-
}))?.edit?.pending)
|
|
37361
|
+
}))?.edit?.pending) {
|
|
37362
|
+
changed.push(message.id);
|
|
37363
|
+
this.editsFor(linkId).edited(message.id);
|
|
37364
|
+
}
|
|
36859
37365
|
}
|
|
36860
37366
|
if (changed.length) await this.messagesChanged(linkId, changed);
|
|
36861
37367
|
}
|
|
@@ -36865,6 +37371,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
36865
37371
|
if (!queue) {
|
|
36866
37372
|
queue = new EditQueue({
|
|
36867
37373
|
read: () => db.getMessages(linkId),
|
|
37374
|
+
row: (id) => db.getMessage(linkId, id),
|
|
36868
37375
|
ready: () => {
|
|
36869
37376
|
const live = this.links.get(linkId), link = live?.link;
|
|
36870
37377
|
return !!link && (link.supportsEdits || link.textDelivery === "dht" && this.takesDhtEdits(live));
|
|
@@ -36891,7 +37398,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
36891
37398
|
async retryMessage({ linkId, messageId }) {
|
|
36892
37399
|
const live = this.links.get(linkId);
|
|
36893
37400
|
if (!live?.stored.profile) throw new Error("Retry is only available for paired chat messages");
|
|
36894
|
-
const message =
|
|
37401
|
+
const message = await db.getMessage(linkId, messageId);
|
|
36895
37402
|
if (message?.via === "hold") {
|
|
36896
37403
|
if (await this.hold.retry(linkId, messageId)) return;
|
|
36897
37404
|
if (!this.holdingFor(live)) throw new Error("Held messages need S3 storage (Profile → Backups) and a contact that allows them.");
|
|
@@ -37011,7 +37518,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
37011
37518
|
*/
|
|
37012
37519
|
async messageDetails({ linkId, messageId }) {
|
|
37013
37520
|
if (typeof linkId !== "string" || typeof messageId !== "string") throw new Error("Chat not found");
|
|
37014
|
-
const message =
|
|
37521
|
+
const message = await db.getMessage(linkId, messageId);
|
|
37015
37522
|
if (!message) return null;
|
|
37016
37523
|
const live = this.links.get(linkId), stored = live?.stored;
|
|
37017
37524
|
const group = linkId.startsWith("group:") ? (await db.getGroups()).find((g) => g.id === linkId.slice(6)) : void 0;
|
|
@@ -37168,12 +37675,34 @@ var GhostlyNode = class GhostlyNode {
|
|
|
37168
37675
|
}));
|
|
37169
37676
|
return Promise.resolve();
|
|
37170
37677
|
}
|
|
37171
|
-
/**
|
|
37172
|
-
|
|
37173
|
-
|
|
37678
|
+
/**
|
|
37679
|
+
* The message that carries a file, read by its id: a file of mine is `me_<its time>`, a received one is named by its
|
|
37680
|
+
* wire id (files/3, a held file) or by its sender's time (files/2). The chat's history is read only when none of
|
|
37681
|
+
* those rows carries it, so a file costs the same in a long chat as in a short one.
|
|
37682
|
+
*/
|
|
37683
|
+
async fileMessage(linkId, fileId) {
|
|
37684
|
+
const stored = await fileStore.get(fileId).catch(() => void 0);
|
|
37685
|
+
const at = stored?.metadata?.timestamp, wireId = stored?.wireId;
|
|
37686
|
+
const ids = !stored ? [] : fileId.startsWith(`${linkId}-out-`) ? [`me_${at}`] : [
|
|
37687
|
+
`peer_${wireId}`,
|
|
37688
|
+
`peer_${at}`,
|
|
37689
|
+
`peer_${at}_${wireId}`
|
|
37690
|
+
];
|
|
37691
|
+
for (const id of ids) {
|
|
37692
|
+
const message = await db.getMessage(linkId, id);
|
|
37693
|
+
if (message?.file?.id === fileId) return message;
|
|
37694
|
+
}
|
|
37695
|
+
return (await db.getMessages(linkId)).find((m) => m.file?.id === fileId);
|
|
37696
|
+
}
|
|
37697
|
+
/**
|
|
37698
|
+
* A file transfer ended: when, on the message that carries the file. `again`: it ended in an earlier run and is shown
|
|
37699
|
+
* again as the chat connects, so what its message says of its end stays (only a receipt it never got is given).
|
|
37700
|
+
*/
|
|
37701
|
+
async noteFileEnd(linkId, fileId, error, again = false) {
|
|
37702
|
+
const message = await this.fileMessage(linkId, fileId);
|
|
37174
37703
|
if (!message) return;
|
|
37175
37704
|
const now = Date.now();
|
|
37176
|
-
await this.noteDetails(linkId, message.id, (details) => error ? message.sender === "me" && details.sends?.length ? {
|
|
37705
|
+
if (!again) await this.noteDetails(linkId, message.id, (details) => error ? message.sender === "me" && details.sends?.length ? {
|
|
37177
37706
|
...details,
|
|
37178
37707
|
sends: details.sends.map((s, i) => i === details.sends.length - 1 ? {
|
|
37179
37708
|
...s,
|
|
@@ -37196,12 +37725,22 @@ var GhostlyNode = class GhostlyNode {
|
|
|
37196
37725
|
}
|
|
37197
37726
|
}
|
|
37198
37727
|
/**
|
|
37728
|
+
* The message of mine that carries a payment or a request, read by its id: `me_<the time the desk keeps for it>`.
|
|
37729
|
+
* The chat's history is read only when that row does not carry it, so it costs the same in a long chat as in a short one.
|
|
37730
|
+
*/
|
|
37731
|
+
async paymentMessage(linkId, paymentId) {
|
|
37732
|
+
const at = this.desk.payment(paymentId)?.createdAt;
|
|
37733
|
+
const message = at === void 0 ? void 0 : await db.getMessage(linkId, `me_${at}`);
|
|
37734
|
+
if (message?.sender === "me" && message.paymentId === paymentId) return message;
|
|
37735
|
+
return (await db.getMessages(linkId)).find((m) => m.sender === "me" && m.paymentId === paymentId);
|
|
37736
|
+
}
|
|
37737
|
+
/**
|
|
37199
37738
|
* The contact's app answered for a payment of mine (`pay-res`): its message gets two ticks, as a text gets from its
|
|
37200
37739
|
* receipt. A payment goes with no delivery state of its own, so its mark read one tick for good. 1:1 chats only: a
|
|
37201
37740
|
* group's payments live in the group's history, under other ids.
|
|
37202
37741
|
*/
|
|
37203
37742
|
async notePaymentReceipt(linkId, paymentId) {
|
|
37204
|
-
const message =
|
|
37743
|
+
const message = await this.paymentMessage(linkId, paymentId);
|
|
37205
37744
|
if (!message || message.delivery === "delivered") return;
|
|
37206
37745
|
if (await db.patchMessage(linkId, message.id, (m) => m.delivery === "delivered" ? null : {
|
|
37207
37746
|
delivery: "delivered",
|
|
@@ -37223,6 +37762,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
37223
37762
|
(async () => {
|
|
37224
37763
|
const message = await db.getMessage(linkId, messageId);
|
|
37225
37764
|
if (message?.paymentId && message.delivery === "waiting") await this.desk.withdraw(message.paymentId).catch(() => {});
|
|
37765
|
+
if (message?.paymentId && message.sender === "peer") await this.desk.dropReceipt(message.paymentId).catch(() => {});
|
|
37226
37766
|
if (message?.sender === "me" && message.file && message.delivery === "waiting" && (message.fileOnFloor === "sent" || message.fileOnFloor === "seen")) {
|
|
37227
37767
|
const wireId = message.file.id.slice(`${linkId}-out-`.length);
|
|
37228
37768
|
live.link?.settleDht(dhtFileId(wireId)).catch(() => {});
|
|
@@ -37371,7 +37911,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
37371
37911
|
});
|
|
37372
37912
|
(async () => {
|
|
37373
37913
|
if (!reply || !forwarded) {
|
|
37374
|
-
const message =
|
|
37914
|
+
const message = await db.getMessage(live.stored.id, `me_${timestamp}`);
|
|
37375
37915
|
reply ??= message && GhostlyNode.wireReply(message);
|
|
37376
37916
|
forwarded ??= message?.forwarded;
|
|
37377
37917
|
}
|
|
@@ -37418,9 +37958,10 @@ var GhostlyNode = class GhostlyNode {
|
|
|
37418
37958
|
}
|
|
37419
37959
|
fileAction({ linkId, fileId, action }) {
|
|
37420
37960
|
if (typeof linkId === "string" && linkId.startsWith("group:")) {
|
|
37421
|
-
if (action !== "accept" && action !== "request") throw new Error("A group's file is downloaded (accept) or asked for again (request)");
|
|
37422
37961
|
if (!fileId.startsWith(`group-${linkId.slice(6)}-`)) throw new Error("No such file in this group");
|
|
37423
|
-
return this.groups.downloadFile(fileId);
|
|
37962
|
+
if (action === "accept" || action === "request" || action === "resume") return this.groups.downloadFile(fileId);
|
|
37963
|
+
if (action === "cancel" || action === "decline" || action === "pause") return this.groups.haltFile(fileId, action === "pause");
|
|
37964
|
+
throw new Error("A group's file is not sent again from here");
|
|
37424
37965
|
}
|
|
37425
37966
|
const live = this.links.get(linkId);
|
|
37426
37967
|
if (!live) throw new Error("No such chat");
|
|
@@ -37509,7 +38050,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
37509
38050
|
const error = await link.announceFile(offer, message.timestamp, GhostlyNode.wireReply(message), message.forwarded);
|
|
37510
38051
|
const said = !error && link.dhtPendingId === dhtFileId(offer.wireId);
|
|
37511
38052
|
if (!said && error !== "This file's name is too long to say over the DHT. It is sent when you are live.") return;
|
|
37512
|
-
await db.patchMessage(linkId, message.id, (current) => current.delivery !== "waiting" || current.fileOnFloor ? null : {
|
|
38053
|
+
const noted = await db.patchMessage(linkId, message.id, (current) => current.delivery !== "waiting" || current.fileOnFloor ? null : {
|
|
37513
38054
|
fileOnFloor: said ? "sent" : "unfit",
|
|
37514
38055
|
...said && { details: withSend(current.details, {
|
|
37515
38056
|
at,
|
|
@@ -37517,17 +38058,32 @@ var GhostlyNode = class GhostlyNode {
|
|
|
37517
38058
|
result: "sent"
|
|
37518
38059
|
}) }
|
|
37519
38060
|
});
|
|
38061
|
+
if (said && noted) await live.floorFiles?.then((known) => known.set(dhtFileId(offer.wireId), message.id), () => {});
|
|
37520
38062
|
await this.messagesChanged(linkId, [message.id]);
|
|
37521
38063
|
}
|
|
37522
38064
|
/** A receipt on the DHT floor that names a waiting file's offer (`dhtFileId`): its bubble is in place on the contact's side. */
|
|
37523
38065
|
async fileSeenOnFloor(linkId, id) {
|
|
37524
|
-
const
|
|
37525
|
-
|
|
37526
|
-
|
|
37527
|
-
if (
|
|
38066
|
+
const live = this.links.get(linkId);
|
|
38067
|
+
if (!live) return;
|
|
38068
|
+
const said = await this.floorFilesOf(live), messageId = said.get(id);
|
|
38069
|
+
if (!messageId) return;
|
|
38070
|
+
said.delete(id);
|
|
38071
|
+
if (await db.patchMessage(linkId, messageId, (current) => current.fileOnFloor !== "sent" ? null : {
|
|
37528
38072
|
fileOnFloor: "seen",
|
|
37529
38073
|
...current.delivery === "waiting" && { deliveryError: "Your contact sees it. The file goes when you are live." }
|
|
37530
|
-
})) await this.messagesChanged(linkId, [
|
|
38074
|
+
})) await this.messagesChanged(linkId, [messageId]);
|
|
38075
|
+
}
|
|
38076
|
+
/**
|
|
38077
|
+
* The files of mine said on the floor and not seen there yet, their message by their floor id (`dhtFileId`). Read from
|
|
38078
|
+
* the chat's history once in a run, then kept as files are said (`sayFileOnFloor`): a receipt that names none of them
|
|
38079
|
+
* costs the same in a long chat as in a short one.
|
|
38080
|
+
*/
|
|
38081
|
+
floorFilesOf(live) {
|
|
38082
|
+
const linkId = live.stored.id, prefix = `${linkId}-out-`;
|
|
38083
|
+
return live.floorFiles ??= db.getMessages(linkId).then((history) => new Map(history.filter((m) => m.sender === "me" && m.file?.id.startsWith(prefix) && m.fileOnFloor === "sent").map((m) => [dhtFileId(m.file.id.slice(prefix.length)), m.id])), (error) => {
|
|
38084
|
+
live.floorFiles = void 0;
|
|
38085
|
+
throw error;
|
|
38086
|
+
});
|
|
37531
38087
|
}
|
|
37532
38088
|
/**
|
|
37533
38089
|
* A file the contact said on the DHT floor (WISP 403 § Files): its bubble takes its place now, waiting for its bytes,
|
|
@@ -38121,8 +38677,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
38121
38677
|
error: "No button to press",
|
|
38122
38678
|
refused: true
|
|
38123
38679
|
};
|
|
38124
|
-
const
|
|
38125
|
-
const message = history.find((m) => m.id === messageId);
|
|
38680
|
+
const message = await db.getMessage(linkId, messageId);
|
|
38126
38681
|
if (!message) return {
|
|
38127
38682
|
error: "That message is not in this chat",
|
|
38128
38683
|
refused: true
|
|
@@ -38131,6 +38686,8 @@ var GhostlyNode = class GhostlyNode {
|
|
|
38131
38686
|
error: "Buttons on your own message do nothing",
|
|
38132
38687
|
refused: true
|
|
38133
38688
|
};
|
|
38689
|
+
const card = messageButtons(message);
|
|
38690
|
+
const history = card && !card.closed && card.buttons.some((b) => b.once) ? await db.getMessages(linkId) : [];
|
|
38134
38691
|
const state = buttonsState(message, replyRef(message), history);
|
|
38135
38692
|
const button = state?.card.buttons.find((b) => b.id === buttonId);
|
|
38136
38693
|
if (!state || !button) return {
|
|
@@ -38179,8 +38736,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
38179
38736
|
if (!targets.length) throw new Error("No chat to forward to");
|
|
38180
38737
|
if (targets.length > 5) throw new Error(`Forward to at most 5 chats at once`);
|
|
38181
38738
|
if (linkId.startsWith("group:") ? !this.membership(linkId.slice(6)) : this.links.get(linkId)?.stored.group !== void 0 || !this.links.has(linkId)) throw new Error("Chat not found");
|
|
38182
|
-
const
|
|
38183
|
-
const chosen = ids.map((id) => history.find((m) => m.id === id));
|
|
38739
|
+
const chosen = await Promise.all(ids.map((id) => db.getMessage(linkId, id)));
|
|
38184
38740
|
if (chosen.some((m) => !m)) throw new Error("That message is not in this chat");
|
|
38185
38741
|
const messages = chosen.sort((a, b) => a.timestamp - b.timestamp);
|
|
38186
38742
|
for (const message of messages) {
|
|
@@ -38512,11 +39068,11 @@ var GhostlyNode = class GhostlyNode {
|
|
|
38512
39068
|
const live = typeof linkId === "string" ? this.links.get(linkId) : void 0;
|
|
38513
39069
|
if (!live || live.stored.group || !live.stored.profile) throw new Error("No such chat");
|
|
38514
39070
|
if (!isAppRef(ref)) throw new Error("Not an app reference");
|
|
38515
|
-
const
|
|
38516
|
-
if (!
|
|
39071
|
+
const mine = participationKeyOf(live), { pairedPeerKey } = live.stored;
|
|
39072
|
+
if (!mine || !pairedPeerKey) throw new Error("This chat is not paired yet");
|
|
38517
39073
|
return {
|
|
38518
39074
|
live,
|
|
38519
|
-
app: chatAppId(
|
|
39075
|
+
app: chatAppId(publicKeyFromZ32(mine), publicKeyFromZ32(pairedPeerKey), ref)
|
|
38520
39076
|
};
|
|
38521
39077
|
}
|
|
38522
39078
|
/** The chat app id of an app in a paired chat: how the contact's frames name it. */
|
|
@@ -38527,10 +39083,13 @@ var GhostlyNode = class GhostlyNode {
|
|
|
38527
39083
|
* This side opened an app in a paired chat, or updated it: `open` goes to the contact now when the session is live,
|
|
38528
39084
|
* and again on every session that comes back while it stays open.
|
|
38529
39085
|
*/
|
|
38530
|
-
async appOpen({ linkId, ref, version }) {
|
|
39086
|
+
async appOpen({ linkId, ref, version, digest }) {
|
|
38531
39087
|
const { live, app } = this.appChat(linkId, ref);
|
|
38532
39088
|
if (!isAppVersion(version)) throw new Error("Not an app version");
|
|
38533
|
-
await this.appStore.chatRunnable({
|
|
39089
|
+
await this.appStore.chatRunnable({
|
|
39090
|
+
ref,
|
|
39091
|
+
digest
|
|
39092
|
+
});
|
|
38534
39093
|
const open = live.appsOpen ??= /* @__PURE__ */ new Map();
|
|
38535
39094
|
if (live.link) live.link.openApp(app, version);
|
|
38536
39095
|
else open.set(app, version);
|
|
@@ -38543,10 +39102,13 @@ var GhostlyNode = class GhostlyNode {
|
|
|
38543
39102
|
else live.appsOpen?.delete(app);
|
|
38544
39103
|
}
|
|
38545
39104
|
/** One data frame of an app open on both sides, on the live session only; nothing is kept to send later. */
|
|
38546
|
-
async appSend({ linkId, ref, data }) {
|
|
39105
|
+
async appSend({ linkId, ref, data, digest }) {
|
|
38547
39106
|
const { live, app } = this.appChat(linkId, ref);
|
|
38548
39107
|
if (!live.appsOpen?.has(app)) return { error: "not-open" };
|
|
38549
|
-
await this.appStore.chatRunnable({
|
|
39108
|
+
await this.appStore.chatRunnable({
|
|
39109
|
+
ref,
|
|
39110
|
+
digest
|
|
39111
|
+
});
|
|
38550
39112
|
return { error: live.link ? live.link.sendAppData(app, data) : "offline" };
|
|
38551
39113
|
}
|
|
38552
39114
|
/** Every page went away: no app runs here any more, so each open one is closed (`close` goes where it can). */
|
|
@@ -38554,6 +39116,16 @@ var GhostlyNode = class GhostlyNode {
|
|
|
38554
39116
|
for (const live of this.links.values()) for (const app of [...live.appsOpen?.keys() ?? []]) if (live.link) live.link.closeApp(app);
|
|
38555
39117
|
else live.appsOpen?.delete(app);
|
|
38556
39118
|
}
|
|
39119
|
+
/** An app that went (uninstalled) is closed in every chat it was open in, as `appClose` does. */
|
|
39120
|
+
appsCloseOf(ref) {
|
|
39121
|
+
for (const [linkId, live] of this.links) {
|
|
39122
|
+
if (!live.appsOpen?.size) continue;
|
|
39123
|
+
const { app } = this.appChat(linkId, ref);
|
|
39124
|
+
if (!live.appsOpen.has(app)) continue;
|
|
39125
|
+
if (live.link) live.link.closeApp(app);
|
|
39126
|
+
else live.appsOpen.delete(app);
|
|
39127
|
+
}
|
|
39128
|
+
}
|
|
38557
39129
|
/** Keeps a pin of a 1:1 chat when it is newer than the chat's; `out`: mine, to say until the contact confirms it. */
|
|
38558
39130
|
async keepLinkPin(linkId, pin, out) {
|
|
38559
39131
|
const live = this.links.get(linkId);
|
|
@@ -38616,6 +39188,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
38616
39188
|
const view = this.groups.views().find((g) => g.id === groupId);
|
|
38617
39189
|
if (!pin || !view?.myKey || view.status !== "active" || !view.members.some((m) => m.key === member)) return;
|
|
38618
39190
|
if (!mayPin(view.profile, member, view.members.find((m) => m.role === "admin")?.key)) return;
|
|
39191
|
+
if (!pinIsNewer(this.groups.pinOf(groupId), pin)) return;
|
|
38619
39192
|
const kept = await this.pinCame(`group:${groupId}`, member === view.myKey ? "me" : member, pin);
|
|
38620
39193
|
if (!await this.groups.setPin(groupId, {
|
|
38621
39194
|
...kept,
|
|
@@ -38903,7 +39476,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
38903
39476
|
async closeRequest(paymentId, reason) {
|
|
38904
39477
|
const request = this.desk.payment(paymentId);
|
|
38905
39478
|
if (!request || !await this.desk.close(paymentId, reason, "your contact removed the wallet it was paid to")) return;
|
|
38906
|
-
const message =
|
|
39479
|
+
const message = await this.paymentMessage(request.linkId, paymentId);
|
|
38907
39480
|
if (message) await this.hold.forget(request.linkId, message.id).catch(() => {});
|
|
38908
39481
|
}
|
|
38909
39482
|
/**
|
|
@@ -39742,6 +40315,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
39742
40315
|
if (settings[key] !== false) delete this.settings[key];
|
|
39743
40316
|
else this.settings[key] = false;
|
|
39744
40317
|
await db.putSettings(this.settings);
|
|
40318
|
+
if (key === "autoDownloads" && settings[key] === false) await this.groups.autoDownloadsOff();
|
|
39745
40319
|
}
|
|
39746
40320
|
if (settings.publicProfiles !== void 0) {
|
|
39747
40321
|
if (settings.publicProfiles !== false) delete this.settings.publicProfiles;
|
|
@@ -41289,10 +41863,11 @@ var GhostlyNode = class GhostlyNode {
|
|
|
41289
41863
|
this.notePaymentReceipt(linkId, result.id).catch(() => {});
|
|
41290
41864
|
},
|
|
41291
41865
|
onFileStored: async (wire) => {
|
|
41292
|
-
|
|
41293
|
-
|
|
41294
|
-
if (
|
|
41295
|
-
return
|
|
41866
|
+
if (!stored.group && !this.links.get(linkId)?.files.wireIds.has(wire.id)) return void 0;
|
|
41867
|
+
const held = (await fileStore.listForLink(linkId)).find((file) => file.direction === "in" && file.wireId === wire.id);
|
|
41868
|
+
if (!held?.digest || !held.metadata) return void 0;
|
|
41869
|
+
if (held.metadata.name !== wire.name || held.metadata.size !== wire.size || held.metadata.mime !== wire.mime || held.metadata.timestamp !== wire.timestamp) return void 0;
|
|
41870
|
+
return held.digest;
|
|
41296
41871
|
},
|
|
41297
41872
|
onFileIncoming: (file) => this.receiveFile(linkId, file),
|
|
41298
41873
|
onFileProgress: (id, transferred, direction) => this.fileProgress(this.localFileId(linkId, id, direction), transferred),
|
|
@@ -41431,7 +42006,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
41431
42006
|
get nativeFactories() {
|
|
41432
42007
|
const relay = this.relaysHyperdht ? this.hyperdhtRelay : "";
|
|
41433
42008
|
return {
|
|
41434
|
-
...relay ? { "hyperdht/1": async (seedB64) => (await import("./hyperdhtRelay-
|
|
42009
|
+
...relay ? { "hyperdht/1": async (seedB64) => (await import("./hyperdhtRelay-CgYLaQz3.js")).createRelayedHyperEndpoint(seedB64, relay) } : {},
|
|
41435
42010
|
...this.options.nativeTransports,
|
|
41436
42011
|
...this.ownIrohRelays ? { "iroh/1": (seedB64) => this.options.nativeTransports["iroh/1"](seedB64, this.settings.irohRelays?.length ? { relays: [...this.settings.irohRelays] } : {}) } : {},
|
|
41437
42012
|
...this.options.irohWeb ? { "iroh/1": (seedB64) => createIrohWebEndpoint(seedB64, { relays: this.irohRelays }) } : {}
|
|
@@ -42236,4 +42811,4 @@ function signalKind(signal) {
|
|
|
42236
42811
|
//#endregion
|
|
42237
42812
|
export { EngineServer, hyperdhtRelayProblem as n, server_exports as t };
|
|
42238
42813
|
|
|
42239
|
-
//# sourceMappingURL=server-
|
|
42814
|
+
//# sourceMappingURL=server-Cy4ox7oD.js.map
|