@ghostlytools/cli 1.2.1 → 1.2.3
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 +4 -2
- package/SKILL.md +2 -1
- package/dist/assets/{arkade-CtCpFaF7.js → arkade-B-ALutmU.js} +2 -2
- package/dist/assets/{arkade-CtCpFaF7.js.map → arkade-B-ALutmU.js.map} +1 -1
- package/dist/assets/{hyperdhtRelay-C0XRWySP.js → hyperdhtRelay-hKC9DpIH.js} +3 -3
- package/dist/assets/{hyperdhtRelay-C0XRWySP.js.map → hyperdhtRelay-hKC9DpIH.js.map} +1 -1
- package/dist/assets/{mainline-DVYdrus6.js → mainline-DriRu_hb.js} +2 -2
- package/dist/assets/{mainline-DVYdrus6.js.map → mainline-DriRu_hb.js.map} +1 -1
- package/dist/assets/{server-D5FBZYW8.js → server-DHMXMwNn.js} +1177 -215
- package/dist/assets/server-DHMXMwNn.js.map +1 -0
- package/dist/assets/{src-aKGjb_yz.js → src-_FzJkeAJ.js} +1458 -1136
- package/dist/assets/src-_FzJkeAJ.js.map +1 -0
- package/dist/assets/{usdt-CUfucwZf.js → usdt-D0lO2pRR.js} +2 -2
- package/dist/assets/{usdt-CUfucwZf.js.map → usdt-D0lO2pRR.js.map} +1 -1
- package/dist/ghostly.mjs +593 -145
- package/dist/ghostly.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/assets/server-D5FBZYW8.js.map +0 -1
- package/dist/assets/src-aKGjb_yz.js.map +0 -1
package/dist/ghostly.mjs
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { t as __exportAll } from "./assets/rolldown-runtime-8H4AJuhK.js";
|
|
3
|
-
import { $ as
|
|
3
|
+
import { $n as readGroupHave, An as resolveLightningDestination, At as DidError, Ba as PR_CHECKS, Bo as extractParamsFromSdp, Bt as didWebHost, Ci as normalizeDomain, Cn as normalizePubkyKey, Ct as parseAtprotoDidDocument, Di as txtValue, Dn as homeserverWebEndpoint, Do as GhostlyHttpError, Dt as DID_DOCUMENT_MAX_BYTES$1, Ei as recordsName, En as pubkyProofPath, Et as verifyAtprotoRecordProof, Fo as RTC_CONFIG, Ft as didSignedMessages, Ga as cardLine, Gn as GROUP_FILE_LIMITS, Gt as signingKeys, Ha as RUN_RESULTS, Ho as signalHasVideo, Ht as parseDid, Ii as IdentityCheckUnavailable, Io as answersOffer, Ir as sanitizeTypingStatus, It as didWebDocumentUrl, Jn as GROUP_WANT_NO_FRAME, Ka as cardUrl, Kn as GROUP_HAVE_FRAME, Kt as staticDidDocument, La as replySnippet, Lo as buildSdpFromSignal, Lt as didWebFile, Mt as checkDidSignature, Nr as TYPING_KINDS, Nt as didGhostlyServiceEntry, Oi as wellKnownUrl, On as pubkyHomeserverOf, Ot as DID_FILE_MAX_BYTES, Po as CALL_SIGNAL_MAX_AGE_MS, Pr as TYPING_REFRESH_MS, Pt as didJwsAlg, Qi as irohRelayUrl, Qr as checkPushEndpoint, Qt as resolveDidDht, Ri as boundVerifiedIdentity, Ro as callRtcConfig, Rt as didWebFileNames, Si as nip05RootKey, Sn as newPubkyProofFolder, St as normalizeAtprotoHandle, Ti as recordsFromWellKnown, Tn as pubkyProofFile, Tt as AtprotoProofError, Ua as STATUS_CARD_LIMITS, Un as engineError, Ut as parseDidDocument, Va as PR_STATES, Vc as setLinkTraceSink, Vi as identityStatement, Vo as parseCallSignal, Vt as normalizeDid, Wa as buttonLabelClash, Wn as engineText, Wt as parseDidSignaturePaste, Xc as openRelayPayload, Xn as groupFileFallback, Yn as fetchesByItself, Zi as irohDescriptor, Zn as isGroupMessageId, _ as checkAppManifest, _i as dnsProofName, _t as atprotoDidMethod, aa as readImageMeta, ao as checkAppRevokeStatement, at as parseSshPublicKey, bi as encodeDnsQuery, bn as isPubkyKey, bt as checkAtprotoProofRecord, c as readAppStore, ca as sanitizeFileName, cl as toBase64Url, cn as WALLET_NETWORKS, ct as sshSignCommand, d as APP_BUNDLE_LIMITS, da as VOICE_LIMITS, dl as utf8Encode, do as signAppRevocation, dt as BitcoinAddressError, er as readGroupWant, fa as baseMime, fo as canonicalJson, ft as decodeAnyBitcoinAddress, g as assembleAppBundle, gi as decodeDnsResponse, gt as ATPROTO_PROOF_SCOPE, ha as voicePeaksOf, hi as DOMAIN_FILE_MAX_BYTES, ht as ATPROTO_PROOF_COLLECTION, il as sanitizeAvatar, io as appRef, it as SshSigError, jn as decodeBolt11, jo as LIMITS, jr as transferEnded, jt as checkDidJws, kn as requestLnurlInvoice, kr as ChatFiles, la as sanitizeMime, ll as toZ32, lo as readAppRevocations, lt as verifySshSignature, ma as parseVoiceMeta, mt as ATPROTO_FULL_SCOPE, n as appStoreDecision, no as APP_STATEMENT_LIMITS, ol as fromBase64Url$1, ot as parseSshSignature, pa as fileMessageText, pn as walletNetworkOf, po as canonicalJsonBytes, pt as ATPROTO_DECLARED_SCOPE, qa as checkStatusCard, qc as RELAY_PAYLOAD_MAX_BYTES, qt as strictBase64Url, ri as BUTTONS_CAPABILITY, rl as MAX_AVATAR_LENGTH, ro as appFingerprint, rt as SSHSIG_NAMESPACE, s as checkAppStoreIndex, sc as inviteLink, sl as randomBytes$1, st as sameSshKey, to as APP_PREFIXES, tr as readGroupWantNo, tt as groupName, u as signAppStore, ua as PLAYABLE_AUDIO, ul as utf8Decode, uo as signAppObject, ut as verifyBitcoinMessage, v as isAppUrl, vc as seedSigner, vi as domainTxtRecord, vt as atprotoProofRecord, wi as recordsFromTxt, wn as pubkyProofCapability, wt as shortAtprotoDid, xi as isPublicAddress, xn as isPubkyProofFolder, xt as isAtprotoDid, y as readAppBundle, ya as RateWindow, yi as domainWellKnownFile, yn as PUBKY_PROOF_MAX_BYTES, yt as atprotoProofRkey, za as ITEM_STATES, zo as callSignalHeardAt, zt as didWebFileUrl } from "./assets/src-_FzJkeAJ.js";
|
|
4
4
|
import { c as fileStore, f as store$1, i as STORES, m as wrap, o as databaseName, p as transact, u as openDb } from "./assets/idb-CTF6LF5B.js";
|
|
5
5
|
import { a as fileBytesOf, i as fileBytes, l as FILE_BYTES_STEP } from "./assets/fileBytes-BcYZhNgF.js";
|
|
6
6
|
import { createRequire } from "node:module";
|
|
7
7
|
import { spawn } from "node:child_process";
|
|
8
8
|
import { connect, createServer, isIP } from "node:net";
|
|
9
|
-
import { appendFileSync, chmodSync, closeSync, existsSync, fsyncSync, lstatSync, mkdirSync, openAsBlob, openSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, writeSync } from "node:fs";
|
|
9
|
+
import { accessSync, appendFileSync, chmodSync, closeSync, constants, existsSync, fsyncSync, lstatSync, mkdirSync, openAsBlob, openSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, writeSync } from "node:fs";
|
|
10
10
|
import { ed25519 } from "@noble/curves/ed25519.js";
|
|
11
11
|
import { sha256 } from "@noble/hashes/sha2.js";
|
|
12
12
|
import { createBase58check } from "@scure/base";
|
|
13
13
|
import { schnorr } from "@noble/curves/secp256k1.js";
|
|
14
14
|
import { wordlist } from "@scure/bip39/wordlists/english.js";
|
|
15
|
-
import { access, constants, link, mkdir, open, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
15
|
+
import { access, constants as constants$1, link, mkdir, open, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
16
16
|
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
17
17
|
import { createHash, createPrivateKey, generateKeyPairSync, randomBytes, timingSafeEqual } from "node:crypto";
|
|
18
18
|
import { decode, npubEncode } from "nostr-tools/nip19";
|
|
@@ -35,7 +35,7 @@ import { IDBFactory, IDBKeyRange as IDBKeyRange$1, IDBTransaction } from "fake-i
|
|
|
35
35
|
import { format } from "node:util";
|
|
36
36
|
var package_default = {
|
|
37
37
|
name: "@ghostlytools/cli",
|
|
38
|
-
version: "1.2.
|
|
38
|
+
version: "1.2.3",
|
|
39
39
|
type: "module",
|
|
40
40
|
description: "Ghostly for bots and terminals: the app's own engine, headless on Node, behind a daemon and a `ghostly` command with JSON output.",
|
|
41
41
|
license: "MIT",
|
|
@@ -285,15 +285,21 @@ function parseArgs(argv, spec, idSlot = () => false) {
|
|
|
285
285
|
}
|
|
286
286
|
/** The global option a word names (`--profile`, `--profile=bot`, `-p`), else undefined. */
|
|
287
287
|
function globalOption(arg) {
|
|
288
|
+
const name = globalName(arg);
|
|
289
|
+
return name ? GLOBAL_OPTIONS[name] : void 0;
|
|
290
|
+
}
|
|
291
|
+
/** The name of the global option a word names (`profile` for `-p`), else undefined. */
|
|
292
|
+
function globalName(arg) {
|
|
288
293
|
const eq = arg.indexOf("=");
|
|
289
294
|
const name = arg.startsWith("--") ? arg.slice(2, eq === -1 ? void 0 : eq) : arg.length === 2 && arg.startsWith("-") ? Object.entries(GLOBAL_OPTIONS).find(([, o]) => o.short === arg.slice(1))?.[0] : void 0;
|
|
290
|
-
return name && Object.hasOwn(GLOBAL_OPTIONS, name) ?
|
|
295
|
+
return name && Object.hasOwn(GLOBAL_OPTIONS, name) ? name : void 0;
|
|
291
296
|
}
|
|
292
297
|
/**
|
|
293
298
|
* Global options given before the command (`ghostly --profile bot send …`) move after it, where every command's
|
|
294
299
|
* parser reads them. Stops at `--` and at the first word that is not a global option. They go before the first word
|
|
295
300
|
* after the command's that starts with a dash (its first option, or the `--` that ends them), else at the end: never
|
|
296
|
-
* between an option and its value, which may itself be `--` (`--pretty profile set --name --`).
|
|
301
|
+
* between an option and its value, which may itself be `--` (`--pretty profile set --name --`). One that takes a value
|
|
302
|
+
* and is the last word has none: a usage error.
|
|
297
303
|
*/
|
|
298
304
|
function liftGlobals(argv) {
|
|
299
305
|
const lifted = [];
|
|
@@ -304,6 +310,7 @@ function liftGlobals(argv) {
|
|
|
304
310
|
const option = globalOption(arg);
|
|
305
311
|
if (!option || arg === "--") break;
|
|
306
312
|
const takesValue = option.type !== "boolean" && eq === -1;
|
|
313
|
+
if (takesValue && i + 1 >= argv.length) throw new CliError("usage", `--${globalName(arg)} needs a value`);
|
|
307
314
|
lifted.push(...argv.slice(i, i + (takesValue ? 2 : 1)));
|
|
308
315
|
i += takesValue ? 2 : 1;
|
|
309
316
|
}
|
|
@@ -335,14 +342,26 @@ var SECRET_CONTEXT = /priv(?:ate)?[\s_-]*key|secret|seed|mnemonic|nsec|xprv|\bwi
|
|
|
335
342
|
/** How far from a 64-hex string a word like "seed" still counts as saying what it is. */
|
|
336
343
|
var CONTEXT_REACH = 48;
|
|
337
344
|
var CASHU_TOKEN = /\bcashu([AB])([A-Za-z0-9_\-+/]{20,}={0,2})/g;
|
|
345
|
+
/**
|
|
346
|
+
* Characters that take no room on screen: format characters (zero-width space, soft hyphen, word joiner, the joiners,
|
|
347
|
+
* direction marks), control characters that are not white space, variation selectors, the grapheme joiner and the
|
|
348
|
+
* Hangul fillers. A page, a PDF or a terminal puts some of them into long strings as places to break a line.
|
|
349
|
+
*/
|
|
350
|
+
var INVISIBLE = /(?![\t\n\v\f\r\x85])[\p{Cc}\p{Cf}]|\u034f|[\u115f\u1160\u3164\uffa0]|[\ufe00-\ufe0f]|[\u{e0100}-\u{e01ef}]/gu;
|
|
338
351
|
var base58check = createBase58check(sha256);
|
|
339
|
-
/**
|
|
352
|
+
/**
|
|
353
|
+
* The first secret in `text`, keys and seeds before ecash; null for anything else. The text is read as it is and as
|
|
354
|
+
* it reads without its invisible characters: a key with one inside is whole again once the reader takes it out.
|
|
355
|
+
*/
|
|
340
356
|
function findSecret(text) {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
if (
|
|
344
|
-
if (
|
|
345
|
-
|
|
357
|
+
const visible = text.replace(INVISIBLE, "");
|
|
358
|
+
const readings = visible === text ? [text] : [text, visible];
|
|
359
|
+
if (readings.some(hasMnemonic)) return { kind: "mnemonic" };
|
|
360
|
+
if (readings.some((reading) => NSEC.test(reading))) return { kind: "nsec" };
|
|
361
|
+
if (readings.some((reading) => EXTENDED_PRIVATE.test(reading) || hasWif(reading))) return { kind: "bitcoin-key" };
|
|
362
|
+
if (readings.some(hasHexSecret)) return { kind: "hex-key" };
|
|
363
|
+
const tokens = readings.map(findCashu);
|
|
364
|
+
return tokens.find((found) => found?.kind === "cashu" && found.amount !== null) ?? tokens.find((found) => found) ?? null;
|
|
346
365
|
}
|
|
347
366
|
/**
|
|
348
367
|
* 12 to 24 words in a row from the English BIP39 list whose checksum holds, anywhere in the text; or a message that
|
|
@@ -875,7 +894,8 @@ function findGroup(groups, ref) {
|
|
|
875
894
|
* A chat or a group, where a command takes either: `group:<id>` is a group; anything else a chat first (`findChat`),
|
|
876
895
|
* then a group. A chat found only by the name its contact gave itself never takes what also names a group: a group's
|
|
877
896
|
* own id is that group, and a prefix or the name of a group this profile is in is refused, with both ids. With neither,
|
|
878
|
-
*
|
|
897
|
+
* one `not_found` names both and where to look (a contact's key: no chat with it); a name that matches more than one
|
|
898
|
+
* chat or group stays that error.
|
|
879
899
|
*/
|
|
880
900
|
function findChatOrGroup(links, groups, ref) {
|
|
881
901
|
if (ref.startsWith("group:")) return { group: findGroup(groups, ref.slice(6)) };
|
|
@@ -887,7 +907,9 @@ function findChatOrGroup(links, groups, ref) {
|
|
|
887
907
|
try {
|
|
888
908
|
return { group: findGroup(groups, ref) };
|
|
889
909
|
} catch (inner) {
|
|
890
|
-
|
|
910
|
+
if (!(inner instanceof CliError) || inner.code !== "not_found") throw inner;
|
|
911
|
+
if (CONTACT_KEY.test(ref)) throw error;
|
|
912
|
+
throw new CliError("not_found", `No chat or group ${JSON.stringify(ref)} (ghostly chat list, ghostly group list)`);
|
|
891
913
|
}
|
|
892
914
|
}
|
|
893
915
|
if (!chat.theirName) return { link: chat.link };
|
|
@@ -916,6 +938,16 @@ function chatOrGroup(ctx, ref) {
|
|
|
916
938
|
};
|
|
917
939
|
}
|
|
918
940
|
var groupOf = (ctx, params) => findGroup(state(ctx).groups, str(params, "group", true));
|
|
941
|
+
/**
|
|
942
|
+
* A message of a chat or a group (`group:<id>`) named by its id here, read as one row; only a name that is no such
|
|
943
|
+
* row's key (the id both sides know, `wireId`) reads the whole history. `keep`: only a message it accepts (mine, or
|
|
944
|
+
* someone else's). The history is synchronous work on the daemon's one thread, about 120 ms at 10,000 messages.
|
|
945
|
+
*/
|
|
946
|
+
async function messageOf(ctx, linkId, ref, keep = () => true) {
|
|
947
|
+
const found = await node(ctx).getMessage(linkId, ref);
|
|
948
|
+
if (found && keep(found)) return found;
|
|
949
|
+
return (linkId.startsWith("group:") ? await node(ctx).groupMessages({ groupId: linkId.slice(6) }) : await node(ctx).getMessages(linkId)).find((m) => keep(m) && m.wireId === ref);
|
|
950
|
+
}
|
|
919
951
|
/** Resolves with `check`'s first defined answer: now, or on a later state; `timeout` error after `ms`. */
|
|
920
952
|
function waitForState(ctx, check, ms, what) {
|
|
921
953
|
const now = check(state(ctx));
|
|
@@ -1131,6 +1163,12 @@ var storedGroupFiles = {
|
|
|
1131
1163
|
read: (file, offset, length) => readStored(file, offset, length),
|
|
1132
1164
|
room: async () => (await fileBytes()).room()
|
|
1133
1165
|
};
|
|
1166
|
+
/**
|
|
1167
|
+
* Ended transfers an edge keeps the record of. A late frame for one of them is still answered as files/3 says (an abort
|
|
1168
|
+
* to an asker going on with a transfer cancelled here); older ones are forgotten, so an edge's records stay few
|
|
1169
|
+
* however many transfers it has seen.
|
|
1170
|
+
*/
|
|
1171
|
+
var ENDED_KEPT = 8;
|
|
1134
1172
|
/** Why a want waits, as the person reads it (the app shows its own words for each `wait`). */
|
|
1135
1173
|
var WAITING_NOTES = {
|
|
1136
1174
|
nobody: "Nobody you are connected to has this file yet",
|
|
@@ -1164,7 +1202,10 @@ var GroupFileDesk = class {
|
|
|
1164
1202
|
constructor(deps) {
|
|
1165
1203
|
this.deps = deps;
|
|
1166
1204
|
}
|
|
1167
|
-
/**
|
|
1205
|
+
/**
|
|
1206
|
+
* At start: each group's files as they were kept. One fetched by itself, or asked for, and not here yet is asked for
|
|
1207
|
+
* again; with automatic downloads off, one chosen by itself before waits for a Download instead.
|
|
1208
|
+
*/
|
|
1168
1209
|
async load(groupIds) {
|
|
1169
1210
|
for (const groupId of groupIds) for (const file of await this.deps.store.list(groupId).catch(() => [])) {
|
|
1170
1211
|
if (!file.group || !file.metadata || !file.digest || this.kept.has(file.id)) continue;
|
|
@@ -1205,7 +1246,8 @@ var GroupFileDesk = class {
|
|
|
1205
1246
|
retry: true,
|
|
1206
1247
|
error: file.transfer.error ?? DAMAGED$1
|
|
1207
1248
|
});
|
|
1208
|
-
else if (kept.
|
|
1249
|
+
else if (kept.asked || kept.auto && this.deps.settings().autoDownloads) this.wants.set(kept.id, this.newWant());
|
|
1250
|
+
else if (kept.auto) await this.stop(kept, false);
|
|
1209
1251
|
else this.show(kept, {
|
|
1210
1252
|
state: "transferring",
|
|
1211
1253
|
stage: "asking",
|
|
@@ -1230,8 +1272,58 @@ var GroupFileDesk = class {
|
|
|
1230
1272
|
}
|
|
1231
1273
|
}
|
|
1232
1274
|
/**
|
|
1275
|
+
* The person stops a file that is coming, or that this device would fetch (Cancel, Decline): nobody is asked for it
|
|
1276
|
+
* any more, the transfer under way ends, what came of it is deleted, and it waits for a Download. Paused (`keep`),
|
|
1277
|
+
* what came stays, and Resume (`download`) goes on from there.
|
|
1278
|
+
*/
|
|
1279
|
+
async halt(fileId, keep = false) {
|
|
1280
|
+
const kept = this.kept.get(fileId);
|
|
1281
|
+
if (!kept) throw new Error("No such file");
|
|
1282
|
+
if (keep && !this.wants.has(fileId)) return;
|
|
1283
|
+
await this.stop(kept, keep);
|
|
1284
|
+
}
|
|
1285
|
+
/** Automatic downloads were turned off: files chosen by themselves and not here yet stop coming, and wait for a Download. */
|
|
1286
|
+
async autoDownloadsOff() {
|
|
1287
|
+
for (const kept of [...this.kept.values()]) if (kept.auto && !kept.asked && this.wants.has(kept.id)) await this.stop(kept, false);
|
|
1288
|
+
}
|
|
1289
|
+
async stop(kept, keep) {
|
|
1290
|
+
if (kept.held || kept.mine) return;
|
|
1291
|
+
const want = this.wants.get(kept.id);
|
|
1292
|
+
this.wants.delete(kept.id);
|
|
1293
|
+
const edge = want?.wire && want.linkId ? this.edgesById.get(want.linkId) : void 0;
|
|
1294
|
+
const record = edge?.files.get("in", want.wire);
|
|
1295
|
+
const { size } = kept.meta;
|
|
1296
|
+
if (!keep) {
|
|
1297
|
+
kept.stored = 0;
|
|
1298
|
+
kept.fresh = true;
|
|
1299
|
+
}
|
|
1300
|
+
if (edge && record && !transferEnded(record)) {
|
|
1301
|
+
if (!keep) kept.dropped = record.id;
|
|
1302
|
+
edge.files.cancel("in", record.id);
|
|
1303
|
+
edge.taking.delete(record.id);
|
|
1304
|
+
} else if (!keep) kept.idle = (kept.idle ?? Promise.resolve()).then(() => this.deps.store.write(kept.id)).then((writer) => writer.discard()).catch(() => {});
|
|
1305
|
+
this.show(kept, keep ? {
|
|
1306
|
+
state: "transferring",
|
|
1307
|
+
stage: "paused",
|
|
1308
|
+
pausedBy: "me",
|
|
1309
|
+
transferred: kept.stored ?? 0,
|
|
1310
|
+
size,
|
|
1311
|
+
direction: "in"
|
|
1312
|
+
} : {
|
|
1313
|
+
state: "transferring",
|
|
1314
|
+
stage: "asking",
|
|
1315
|
+
transferred: 0,
|
|
1316
|
+
size,
|
|
1317
|
+
direction: "in"
|
|
1318
|
+
});
|
|
1319
|
+
if (kept.auto || kept.asked) {
|
|
1320
|
+
kept.auto = kept.asked = false;
|
|
1321
|
+
await this.deps.store.patch(kept.id, { group: this.groupField(kept) }).catch(() => {});
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
/**
|
|
1233
1325
|
* An error when this group took as many announcements of mine as it takes in a minute, or as its members take from
|
|
1234
|
-
* one member in `announceWindowMs` (past that they would
|
|
1326
|
+
* one member in `announceWindowMs` (past that they would not fetch the file by themselves); null when one may go now.
|
|
1235
1327
|
*/
|
|
1236
1328
|
mayAnnounce(groupId) {
|
|
1237
1329
|
const wait = Math.max(...this.sentPaces(groupId).map((pace) => pace.wait()));
|
|
@@ -1248,9 +1340,9 @@ var GroupFileDesk = class {
|
|
|
1248
1340
|
this.own.delete(`${groupId}\n${meta.d}`);
|
|
1249
1341
|
}
|
|
1250
1342
|
/**
|
|
1251
|
-
* A message of the group announces a file (mine, or a member's): the file its row shows
|
|
1252
|
-
*
|
|
1253
|
-
*
|
|
1343
|
+
* A message of the group announces a file (mine, or a member's): the file its row shows. A member's is kept here
|
|
1344
|
+
* unfetched, and fetched now when it is one this device takes by itself and the member announced no more than the
|
|
1345
|
+
* group fetches that way. The same message again (a catch-up, a whole copy) is the same file.
|
|
1254
1346
|
*/
|
|
1255
1347
|
async announced(groupId, m, me) {
|
|
1256
1348
|
const known = this.byMessage.get(`${groupId}\n${m.id}`);
|
|
@@ -1292,9 +1384,9 @@ var GroupFileDesk = class {
|
|
|
1292
1384
|
this.queueHave(kept);
|
|
1293
1385
|
return messageFile(kept);
|
|
1294
1386
|
}
|
|
1295
|
-
|
|
1387
|
+
const paced = this.pace(this.takenPace, `${groupId}\n${m.sender}`, GROUP_FILE_LIMITS.announceTaken, GROUP_FILE_LIMITS.announceWindowMs).take();
|
|
1296
1388
|
const id = groupFileId(groupId, "in");
|
|
1297
|
-
const auto = this.deps.settings().autoDownloads && fetchesByItself(m.file, this.keptAutomatically(groupId));
|
|
1389
|
+
const auto = paced && this.deps.settings().autoDownloads && fetchesByItself(m.file, this.keptAutomatically(groupId));
|
|
1298
1390
|
const holders = this.ahead.get(`${groupId}\n${m.id}`) ?? [];
|
|
1299
1391
|
this.ahead.delete(`${groupId}\n${m.id}`);
|
|
1300
1392
|
const kept = {
|
|
@@ -1696,20 +1788,35 @@ var GroupFileDesk = class {
|
|
|
1696
1788
|
const reserved = active.reduce((sum, a) => sum + a.size, 0);
|
|
1697
1789
|
return this.served.bytes + reserved + kept.meta.size > GROUP_FILE_LIMITS.serveBytesPerDay ? "busy" : null;
|
|
1698
1790
|
}
|
|
1699
|
-
/**
|
|
1791
|
+
/**
|
|
1792
|
+
* Transfers served now, all groups together: to whom, what each takes of the day (its size, or what it sent when that
|
|
1793
|
+
* is more), and whether bytes are still to go.
|
|
1794
|
+
*/
|
|
1700
1795
|
servingNow() {
|
|
1701
1796
|
const active = [];
|
|
1702
1797
|
for (const edge of this.edgesById.values()) for (const wire of edge.serving.keys()) {
|
|
1703
1798
|
const record = edge.files.get("out", wire);
|
|
1704
1799
|
if (record && !transferEnded(record)) active.push({
|
|
1705
1800
|
peer: edge.peer,
|
|
1706
|
-
size: record.file.size,
|
|
1801
|
+
size: Math.max(record.file.size, edge.files.sent(wire)),
|
|
1707
1802
|
sending: record.confirmed < record.file.size
|
|
1708
1803
|
});
|
|
1709
1804
|
}
|
|
1710
1805
|
return active;
|
|
1711
1806
|
}
|
|
1712
1807
|
/**
|
|
1808
|
+
* An asker has a served transfer send parts again (WISP 503 § Serving limits): every byte sent counts toward the day,
|
|
1809
|
+
* so it goes on only while the day has room for it.
|
|
1810
|
+
*/
|
|
1811
|
+
mayResend() {
|
|
1812
|
+
const today = Math.floor(this.deps.now() / 864e5);
|
|
1813
|
+
if (this.served.day !== today) this.served = {
|
|
1814
|
+
day: today,
|
|
1815
|
+
bytes: 0
|
|
1816
|
+
};
|
|
1817
|
+
return this.served.bytes + this.servingNow().reduce((sum, a) => sum + a.size, 0) < GROUP_FILE_LIMITS.serveBytesPerDay;
|
|
1818
|
+
}
|
|
1819
|
+
/**
|
|
1713
1820
|
* Served transfers that no longer count end, their places free for others (WISP 503 § Serving limits): the asker's
|
|
1714
1821
|
* session closed, it is not in the roster any more, or its bytes have not moved for `wantWaitMs` (the asker has moved
|
|
1715
1822
|
* on by then).
|
|
@@ -1762,6 +1869,7 @@ var GroupFileDesk = class {
|
|
|
1762
1869
|
serving: /* @__PURE__ */ new Map(),
|
|
1763
1870
|
servedMoved: /* @__PURE__ */ new Map(),
|
|
1764
1871
|
taking: /* @__PURE__ */ new Map(),
|
|
1872
|
+
ended: [],
|
|
1765
1873
|
have: /* @__PURE__ */ new Set(),
|
|
1766
1874
|
haveAt: 0,
|
|
1767
1875
|
busy: /* @__PURE__ */ new Set()
|
|
@@ -1779,7 +1887,8 @@ var GroupFileDesk = class {
|
|
|
1779
1887
|
openSource: (record) => this.openSource(edge, record),
|
|
1780
1888
|
changed: (record, transferred) => this.transferChanged(edge, record, transferred),
|
|
1781
1889
|
room: () => this.deps.store.room(),
|
|
1782
|
-
writable: () => this.deps.writable?.(linkId)
|
|
1890
|
+
writable: () => this.deps.writable?.(linkId),
|
|
1891
|
+
resend: (record) => !edge.serving.has(record.id) || this.mayResend()
|
|
1783
1892
|
});
|
|
1784
1893
|
this.edgesById.set(linkId, edge);
|
|
1785
1894
|
return edge;
|
|
@@ -1818,7 +1927,7 @@ var GroupFileDesk = class {
|
|
|
1818
1927
|
if (resumed) kept.fresh = kept.unclear = true;
|
|
1819
1928
|
else kept.damaged.add(edge.peer);
|
|
1820
1929
|
}
|
|
1821
|
-
return kept.idle = (damaged ? writer.discard() : writer.close()).catch(() => {});
|
|
1930
|
+
return kept.idle = (damaged || kept.dropped === record.id ? writer.discard() : writer.close()).catch(() => {});
|
|
1822
1931
|
}
|
|
1823
1932
|
};
|
|
1824
1933
|
}
|
|
@@ -1831,7 +1940,14 @@ var GroupFileDesk = class {
|
|
|
1831
1940
|
digest: async () => kept.meta.d
|
|
1832
1941
|
};
|
|
1833
1942
|
}
|
|
1943
|
+
/** A transfer on the edge ended: its record stays among the last `ENDED_KEPT`, and the oldest past that is forgotten. */
|
|
1944
|
+
transferOver(edge, record) {
|
|
1945
|
+
if (edge.ended.some(([direction, id]) => direction === record.direction && id === record.id)) return;
|
|
1946
|
+
edge.ended.push([record.direction, record.id]);
|
|
1947
|
+
while (edge.ended.length > ENDED_KEPT) edge.files.forget(...edge.ended.shift());
|
|
1948
|
+
}
|
|
1834
1949
|
transferChanged(edge, record, transferred) {
|
|
1950
|
+
if (transferEnded(record)) this.transferOver(edge, record);
|
|
1835
1951
|
if (record.direction === "out") {
|
|
1836
1952
|
if (transferEnded(record) && edge.serving.has(record.id)) {
|
|
1837
1953
|
const today = Math.floor(this.deps.now() / 864e5);
|
|
@@ -1839,7 +1955,7 @@ var GroupFileDesk = class {
|
|
|
1839
1955
|
day: today,
|
|
1840
1956
|
bytes: 0
|
|
1841
1957
|
};
|
|
1842
|
-
this.served.bytes += record.
|
|
1958
|
+
this.served.bytes += edge.files.sent(record.id);
|
|
1843
1959
|
edge.serving.delete(record.id);
|
|
1844
1960
|
edge.servedMoved.delete(record.id);
|
|
1845
1961
|
const serving = this.deps.settings().serveFiles;
|
|
@@ -2584,7 +2700,7 @@ var FILE_METHODS = {
|
|
|
2584
2700
|
throw new CliError("not_found", `No file ${path}`);
|
|
2585
2701
|
}
|
|
2586
2702
|
if (!info.isFile()) throw new CliError("bad_request", `${path} is not a file`);
|
|
2587
|
-
await access(path, constants.R_OK);
|
|
2703
|
+
await access(path, constants$1.R_OK);
|
|
2588
2704
|
if ("group" in target) return sendToGroup(ctx, params, target.group, path, info.size);
|
|
2589
2705
|
const { link } = target;
|
|
2590
2706
|
if (link.profile && link.dataLink === "open" && !link.capabilities?.files) throw new CliError("unavailable", "The contact's app takes no files");
|
|
@@ -2671,15 +2787,16 @@ var FILE_METHODS = {
|
|
|
2671
2787
|
/**
|
|
2672
2788
|
* files/3: answer an offer, or pause, resume or cancel a transfer, either way; `resend` a file sent from here that
|
|
2673
2789
|
* stopped moving (or failed), `request` again one that stopped arriving. Without a chat, the file's id names it.
|
|
2674
|
-
* A group's file (WISP 503) takes `accept` (download one this device did not fetch by itself) and `
|
|
2790
|
+
* A group's file (WISP 503) takes `accept` (download one this device did not fetch by itself), `request`, and `cancel`,
|
|
2791
|
+
* `decline`, `pause` and `resume` for one that is coming.
|
|
2675
2792
|
*/
|
|
2676
2793
|
async "file.action"(ctx, params) {
|
|
2677
2794
|
const fileId = str(params, "file", true);
|
|
2678
2795
|
const { linkId, where } = fileChat(ctx, params, fileId);
|
|
2679
2796
|
const action = oneOf(params, "action", ACTIONS, "accept");
|
|
2680
2797
|
if ("group" in where) {
|
|
2681
|
-
if (action
|
|
2682
|
-
if (state(ctx).transfers[fileId]?.state === "done") throw new CliError("refused", "Nothing to ask for: the file is all here");
|
|
2798
|
+
if (action === "resend") throw new CliError("bad_request", "A group's file is downloaded (file accept) or asked for again (file request)");
|
|
2799
|
+
if (state(ctx).transfers[fileId]?.state === "done") throw new CliError("refused", action === "accept" || action === "request" || action === "resume" ? "Nothing to ask for: the file is all here" : "Nothing to stop: the file is all here");
|
|
2683
2800
|
try {
|
|
2684
2801
|
await node(ctx).fileAction({
|
|
2685
2802
|
linkId,
|
|
@@ -3346,8 +3463,7 @@ function pressable(link) {
|
|
|
3346
3463
|
return peer ? peer.includes(BUTTONS_CAPABILITY) : null;
|
|
3347
3464
|
}
|
|
3348
3465
|
/** The message of mine with buttons that `button update` names, by its id here (or the id both sides know). */
|
|
3349
|
-
function buttonsMessage(
|
|
3350
|
-
const message = messages.find((m) => m.sender === "me" && m.id === ref) ?? messages.find((m) => m.sender === "me" && m.wireId === ref);
|
|
3466
|
+
function buttonsMessage(message, ref, group) {
|
|
3351
3467
|
if (!message) throw new CliError("not_found", `No message ${JSON.stringify(ref)} of yours in this ${group ? "group" : "chat"}: the messageId send gave`);
|
|
3352
3468
|
if (message.card?.kind !== "buttons") throw new CliError("bad_request", "That message has no buttons: send one with send --button id:Label");
|
|
3353
3469
|
return message;
|
|
@@ -3367,7 +3483,7 @@ async function updateButtons(ctx, params) {
|
|
|
3367
3483
|
const secret = findSecret(newText);
|
|
3368
3484
|
if (secret) throw new CliError("confirm", `The text looks like ${secret.kind === "cashu" ? "a Cashu token (money anyone who reads it can take)" : "a secret (a seed or a private key)"}; update with --force if you mean to`, { kind: secret.kind });
|
|
3369
3485
|
}
|
|
3370
|
-
const message = buttonsMessage(
|
|
3486
|
+
const message = buttonsMessage(await messageOf(ctx, target.linkId, ref, (m) => m.sender === "me"), ref, target.group);
|
|
3371
3487
|
if ((message.edit?.seq ?? 0) >= STATUS_CARD_LIMITS.edits) throw new CliError("refused", `This message took ${STATUS_CARD_LIMITS.edits} updates, the most one takes: ask again with a new send`, {
|
|
3372
3488
|
messageId: message.id,
|
|
3373
3489
|
edits: message.edit.seq
|
|
@@ -3392,7 +3508,7 @@ async function updateButtons(ctx, params) {
|
|
|
3392
3508
|
card
|
|
3393
3509
|
});
|
|
3394
3510
|
if (result.error) throw new CliError(result.refused ? "refused" : "unavailable", result.error);
|
|
3395
|
-
let edited =
|
|
3511
|
+
let edited = await node(ctx).getMessage(target.linkId, message.id);
|
|
3396
3512
|
if (wait === "confirmed" && edited?.edit?.pending) edited = await waitForEdit(ctx, target.linkId, message.id, ms);
|
|
3397
3513
|
const edges = wait === "sent" && edited?.edit ? await waitForGroupFrame(ctx, target.id, message.id, edited.edit.seq, ms) : void 0;
|
|
3398
3514
|
return {
|
|
@@ -3415,8 +3531,7 @@ async function pressButton(ctx, params) {
|
|
|
3415
3531
|
const ref = str(params, "message", true);
|
|
3416
3532
|
const buttonId = str(params, "button", true);
|
|
3417
3533
|
const where = target.group ? "group" : "chat";
|
|
3418
|
-
const
|
|
3419
|
-
const message = theirs.find((m) => m.id === ref) ?? theirs.find((m) => m.wireId === ref);
|
|
3534
|
+
const message = await messageOf(ctx, target.linkId, ref, (m) => m.sender !== "me");
|
|
3420
3535
|
if (!message) throw new CliError("not_found", `No message ${JSON.stringify(ref)} from someone else in this ${where}: the id its event or history gave`);
|
|
3421
3536
|
if (message.card?.kind !== "buttons") throw new CliError("bad_request", "That message has no buttons");
|
|
3422
3537
|
const button = message.card.buttons.find((b) => b.id === buttonId);
|
|
@@ -3575,7 +3690,7 @@ function isLive(link) {
|
|
|
3575
3690
|
if (link.textDelivery !== "stream" || link.dataLink !== "open") return false;
|
|
3576
3691
|
return !link.profile || link.pairing?.status === "ready" && !!link.pairing.transport;
|
|
3577
3692
|
}
|
|
3578
|
-
function chatJson(link) {
|
|
3693
|
+
function chatJson(link, showSecret) {
|
|
3579
3694
|
const label = link.label?.trim() || null;
|
|
3580
3695
|
const peerName = link.peerNick?.trim() || null;
|
|
3581
3696
|
const live = isLive(link);
|
|
@@ -3593,15 +3708,15 @@ function chatJson(link) {
|
|
|
3593
3708
|
pairing: link.pairing?.status ?? null,
|
|
3594
3709
|
stage: link.pairingProgress?.stage ?? null,
|
|
3595
3710
|
verified: !!link.peerVerified,
|
|
3596
|
-
invite: link.pairing?.status === "ready" ? null : link.inviteCode
|
|
3711
|
+
invite: link.pairing?.status === "ready" || !link.inviteCode ? null : showSecret === true ? link.inviteCode : "<hidden>",
|
|
3597
3712
|
compatibility: !link.profile,
|
|
3598
3713
|
peerPicture: !!link.peerAvatar
|
|
3599
3714
|
};
|
|
3600
3715
|
}
|
|
3601
3716
|
/** A chat's details: the summary plus what its connection panel shows. */
|
|
3602
|
-
function chatDetailsJson(link) {
|
|
3717
|
+
function chatDetailsJson(link, showSecret) {
|
|
3603
3718
|
return {
|
|
3604
|
-
...chatJson(link),
|
|
3719
|
+
...chatJson(link, showSecret),
|
|
3605
3720
|
progress: link.pairingProgress ?? null,
|
|
3606
3721
|
transports: {
|
|
3607
3722
|
mine: link.availableTransports ?? [],
|
|
@@ -6057,7 +6172,8 @@ async function verifyIdToken(token, options) {
|
|
|
6057
6172
|
}
|
|
6058
6173
|
//#endregion
|
|
6059
6174
|
//#region ../browser/src/proofs/oidc/flow.ts
|
|
6060
|
-
|
|
6175
|
+
/** Optional platform prefix (desktop `d.<port>.`, Android deep link `a.`), then 32 random bytes. */
|
|
6176
|
+
var STATE = /^(?:d\.\d{1,5}\.|a\.)?[A-Za-z0-9_-]{43}$/;
|
|
6061
6177
|
function scopeFor$1(provider, reveal) {
|
|
6062
6178
|
const scopes = provider.id === "apple" ? [] : ["openid"];
|
|
6063
6179
|
const claims = [];
|
|
@@ -8612,13 +8728,15 @@ var IDENTITY_METHODS = {
|
|
|
8612
8728
|
},
|
|
8613
8729
|
async "identity.cancel"(ctx, params) {
|
|
8614
8730
|
const draftId = str(params, "draft", true);
|
|
8615
|
-
drafts.delete(draftId);
|
|
8731
|
+
if (!drafts.delete(draftId)) throw new CliError("not_found", "No such draft here: it is kept by the daemon that made it, for an hour");
|
|
8616
8732
|
await node(ctx).cancelIdentityProof({ draftId });
|
|
8617
8733
|
return { cancelled: draftId };
|
|
8618
8734
|
},
|
|
8619
8735
|
async "identity.remove"(ctx, params) {
|
|
8620
|
-
|
|
8621
|
-
|
|
8736
|
+
const id = str(params, "id", true);
|
|
8737
|
+
if (!state(ctx).identityProofs.some((p) => p.id === id)) throw new CliError("not_found", `No identity proof ${JSON.stringify(id)} (ghostly identity list)`);
|
|
8738
|
+
await node(ctx).removeIdentityProof({ id });
|
|
8739
|
+
return { removed: id };
|
|
8622
8740
|
},
|
|
8623
8741
|
async "identity.share"(ctx, params) {
|
|
8624
8742
|
const link = chatOf(ctx, params);
|
|
@@ -8633,13 +8751,15 @@ var IDENTITY_METHODS = {
|
|
|
8633
8751
|
},
|
|
8634
8752
|
async "identity.withdraw"(ctx, params) {
|
|
8635
8753
|
const link = chatOf(ctx, params);
|
|
8754
|
+
const id = str(params, "id", true);
|
|
8755
|
+
if (!link.identities?.shared.some((s) => s.id === id)) throw new CliError("not_found", `No identity proof ${JSON.stringify(id)} shared in this chat (ghostly identity contact ${link.id})`);
|
|
8636
8756
|
await node(ctx).withdrawIdentityProof({
|
|
8637
8757
|
linkId: link.id,
|
|
8638
|
-
id
|
|
8758
|
+
id
|
|
8639
8759
|
});
|
|
8640
8760
|
return {
|
|
8641
8761
|
chat: link.id,
|
|
8642
|
-
withdrawn:
|
|
8762
|
+
withdrawn: id
|
|
8643
8763
|
};
|
|
8644
8764
|
},
|
|
8645
8765
|
/** What a contact shared in a chat, as this device checked it, and what this side shares there. */
|
|
@@ -8875,6 +8995,15 @@ async function proxy(request, response, chunks, send, cookies) {
|
|
|
8875
8995
|
plain(response, gone ? 503 : 502, gone ? "This service is not reachable. Services exist while their ghost is online." : `The request failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
8876
8996
|
}
|
|
8877
8997
|
}
|
|
8998
|
+
/**
|
|
8999
|
+
* The service a command names, by its id: one of this profile's. The engine changes nothing for an id it does not
|
|
9000
|
+
* know and says nothing, so a mistyped id read as done ("shared": true, with a contact).
|
|
9001
|
+
*/
|
|
9002
|
+
function serviceOf(ctx, params) {
|
|
9003
|
+
const serviceId = str(params, "service", true);
|
|
9004
|
+
if (!state(ctx).services.some((s) => s.id === serviceId)) throw new CliError("not_found", `No service ${JSON.stringify(serviceId)} (ghostly service list)`);
|
|
9005
|
+
return serviceId;
|
|
9006
|
+
}
|
|
8878
9007
|
var SERVICE_METHODS = {
|
|
8879
9008
|
async "service.list"(ctx) {
|
|
8880
9009
|
return { services: state(ctx).services.map((s) => ({
|
|
@@ -8901,29 +9030,32 @@ var SERVICE_METHODS = {
|
|
|
8901
9030
|
});
|
|
8902
9031
|
},
|
|
8903
9032
|
async "service.remove"(ctx, params) {
|
|
8904
|
-
|
|
8905
|
-
|
|
9033
|
+
const serviceId = serviceOf(ctx, params);
|
|
9034
|
+
await node(ctx).removeService({ serviceId });
|
|
9035
|
+
return { removed: serviceId };
|
|
8906
9036
|
},
|
|
8907
9037
|
async "service.enable"(ctx, params) {
|
|
9038
|
+
const serviceId = serviceOf(ctx, params);
|
|
8908
9039
|
await node(ctx).setServiceEnabled({
|
|
8909
|
-
serviceId
|
|
9040
|
+
serviceId,
|
|
8910
9041
|
enabled: !bool(params, "off")
|
|
8911
9042
|
});
|
|
8912
9043
|
return {
|
|
8913
|
-
service:
|
|
9044
|
+
service: serviceId,
|
|
8914
9045
|
enabled: !bool(params, "off")
|
|
8915
9046
|
};
|
|
8916
9047
|
},
|
|
8917
9048
|
async "service.share"(ctx, params) {
|
|
8918
9049
|
const link = chatOf(ctx, params);
|
|
8919
9050
|
const shared = !bool(params, "off");
|
|
9051
|
+
const serviceId = serviceOf(ctx, params);
|
|
8920
9052
|
await node(ctx).setServiceShared({
|
|
8921
|
-
serviceId
|
|
9053
|
+
serviceId,
|
|
8922
9054
|
peerPubKeyZ32: link.peerPubKeyZ32,
|
|
8923
9055
|
shared
|
|
8924
9056
|
});
|
|
8925
9057
|
return {
|
|
8926
|
-
service:
|
|
9058
|
+
service: serviceId,
|
|
8927
9059
|
chat: link.id,
|
|
8928
9060
|
shared
|
|
8929
9061
|
};
|
|
@@ -12533,6 +12665,25 @@ function checkTextSize(text) {
|
|
|
12533
12665
|
max: LIMITS.maxChatMessageBytes
|
|
12534
12666
|
});
|
|
12535
12667
|
}
|
|
12668
|
+
/**
|
|
12669
|
+
* The engine's state as `engine getState` prints it without `--show-secret`: the settings' credentials, a group's
|
|
12670
|
+
* entry link (anyone who reads it can join) and a pending chat's invite code (whoever reads it can take the chat)
|
|
12671
|
+
* are `<hidden>`.
|
|
12672
|
+
*/
|
|
12673
|
+
function hideStateSecrets(state) {
|
|
12674
|
+
return {
|
|
12675
|
+
...state,
|
|
12676
|
+
settings: redactSettings(state.settings),
|
|
12677
|
+
...state.groups ? { groups: state.groups.map((g) => g.entryLink ? {
|
|
12678
|
+
...g,
|
|
12679
|
+
entryLink: "<hidden>"
|
|
12680
|
+
} : g) } : {},
|
|
12681
|
+
...state.links ? { links: state.links.map((l) => l.inviteCode ? {
|
|
12682
|
+
...l,
|
|
12683
|
+
inviteCode: "<hidden>"
|
|
12684
|
+
} : l) } : {}
|
|
12685
|
+
};
|
|
12686
|
+
}
|
|
12536
12687
|
/** Settings as a bot may read them: storage credentials and other secrets masked unless asked for. */
|
|
12537
12688
|
function redactSettings(settings, showSecret = false) {
|
|
12538
12689
|
if (showSecret) return settings;
|
|
@@ -12560,7 +12711,9 @@ var SETTABLE = {
|
|
|
12560
12711
|
online: "boolean",
|
|
12561
12712
|
shareProfile: "boolean",
|
|
12562
12713
|
sendTyping: "boolean",
|
|
12563
|
-
nick: "string"
|
|
12714
|
+
nick: "string",
|
|
12715
|
+
autoDownloads: "boolean",
|
|
12716
|
+
serveFiles: "boolean"
|
|
12564
12717
|
};
|
|
12565
12718
|
var TRANSPORT_NAMES = {
|
|
12566
12719
|
auto: "auto",
|
|
@@ -12723,13 +12876,14 @@ var METHODS = {
|
|
|
12723
12876
|
node(ctx).setActiveLink({ linkId });
|
|
12724
12877
|
return { chat: linkId };
|
|
12725
12878
|
},
|
|
12726
|
-
async "chat.list"(ctx) {
|
|
12727
|
-
|
|
12879
|
+
async "chat.list"(ctx, params) {
|
|
12880
|
+
const showSecret = bool(params, "showSecret");
|
|
12881
|
+
return { chats: state(ctx).links.map((link) => chatJson(link, showSecret)).sort((a, b) => b.lastMessageAt - a.lastMessageAt || b.createdAt - a.createdAt) };
|
|
12728
12882
|
},
|
|
12729
12883
|
async "chat.get"(ctx, params) {
|
|
12730
12884
|
const link = chatOf(ctx, params);
|
|
12731
12885
|
return {
|
|
12732
|
-
...chatDetailsJson(link),
|
|
12886
|
+
...chatDetailsJson(link, bool(params, "showSecret")),
|
|
12733
12887
|
heldUntil: holdOf(ctx, link.id)
|
|
12734
12888
|
};
|
|
12735
12889
|
},
|
|
@@ -12795,7 +12949,7 @@ var METHODS = {
|
|
|
12795
12949
|
messageId,
|
|
12796
12950
|
text
|
|
12797
12951
|
});
|
|
12798
|
-
if (result.error) throw editRefused(await
|
|
12952
|
+
if (result.error) throw editRefused(await messageOf(ctx, link.id, messageId), messageId, "chat", result.error, result.refused);
|
|
12799
12953
|
const id = result.messageId ?? messageId;
|
|
12800
12954
|
let message = await node(ctx).getMessage(link.id, id);
|
|
12801
12955
|
if (wait === "confirmed" && message?.edit?.pending) message = await waitForEdit(ctx, link.id, id, num(params, "timeout", 30, {
|
|
@@ -12849,7 +13003,7 @@ var METHODS = {
|
|
|
12849
13003
|
},
|
|
12850
13004
|
/** WISP 400 § Reactions: one emoji per person per message, a new one replaces mine; `remove` takes mine back. */
|
|
12851
13005
|
async "chat.react"(ctx, params) {
|
|
12852
|
-
const link = chatOf(ctx, params);
|
|
13006
|
+
const link = notAGroup(ctx, params, () => chatOf(ctx, params), "group react", "<message> <emoji>");
|
|
12853
13007
|
return {
|
|
12854
13008
|
chat: link.id,
|
|
12855
13009
|
...await react(ctx, link.id, params)
|
|
@@ -12886,10 +13040,12 @@ var METHODS = {
|
|
|
12886
13040
|
if (!messageIds.length) throw new CliError("bad_request", "Name the messages to forward (their ids, from history or an event)");
|
|
12887
13041
|
const to = list(params, "to").map((ref) => chatOrGroup(ctx, ref));
|
|
12888
13042
|
if (!to.length) throw new CliError("bad_request", "Forward to which chat? Name one or more with --to");
|
|
12889
|
-
const
|
|
12890
|
-
|
|
12891
|
-
|
|
12892
|
-
|
|
13043
|
+
const chosen = [];
|
|
13044
|
+
for (const id of messageIds) {
|
|
13045
|
+
const message = await node(ctx).getMessage(from.linkId, id);
|
|
13046
|
+
if (!message) throw new CliError("not_found", `No message ${id} in this ${from.group ? "group" : "chat"}`);
|
|
13047
|
+
chosen.push(message);
|
|
13048
|
+
}
|
|
12893
13049
|
if (!bool(params, "force")) for (const message of chosen) {
|
|
12894
13050
|
const secret = message.file ? null : findSecret(message.text);
|
|
12895
13051
|
if (secret) throw new CliError("confirm", `A message looks like ${secret.kind === "cashu" ? "a Cashu token (money anyone who reads it can take)" : "a secret (a seed or a private key)"}; forward it with --force if you mean to`, {
|
|
@@ -12923,7 +13079,7 @@ var METHODS = {
|
|
|
12923
13079
|
await waitForGroupFrame(ctx, to[i].id, messageId, void 0, ms);
|
|
12924
13080
|
continue;
|
|
12925
13081
|
}
|
|
12926
|
-
const sent =
|
|
13082
|
+
const sent = await node(ctx).getMessage(to[i].id, messageId);
|
|
12927
13083
|
if (sent?.file) await FILE_METHODS["file.wait"](ctx, {
|
|
12928
13084
|
chat: to[i].id,
|
|
12929
13085
|
file: sent.file.id,
|
|
@@ -12943,19 +13099,21 @@ var METHODS = {
|
|
|
12943
13099
|
},
|
|
12944
13100
|
async "chat.retry"(ctx, params) {
|
|
12945
13101
|
const link = chatOf(ctx, params);
|
|
13102
|
+
const messageId = str(params, "message", true);
|
|
13103
|
+
if (!await node(ctx).getMessage(link.id, messageId)) throw new CliError("not_found", `No message ${messageId} in this chat`);
|
|
12946
13104
|
await node(ctx).retryMessage({
|
|
12947
13105
|
linkId: link.id,
|
|
12948
|
-
messageId
|
|
13106
|
+
messageId
|
|
12949
13107
|
});
|
|
12950
13108
|
return {
|
|
12951
13109
|
chat: link.id,
|
|
12952
|
-
messageId
|
|
13110
|
+
messageId
|
|
12953
13111
|
};
|
|
12954
13112
|
},
|
|
12955
13113
|
async "chat.delete"(ctx, params) {
|
|
12956
13114
|
const link = chatOf(ctx, params);
|
|
12957
13115
|
const messageId = str(params, "message", true);
|
|
12958
|
-
if (!
|
|
13116
|
+
if (!await node(ctx).getMessage(link.id, messageId)) throw new CliError("not_found", `No message ${messageId} in this chat`);
|
|
12959
13117
|
await node(ctx).deleteMessage({
|
|
12960
13118
|
linkId: link.id,
|
|
12961
13119
|
messageId
|
|
@@ -13071,7 +13229,13 @@ var METHODS = {
|
|
|
13071
13229
|
};
|
|
13072
13230
|
},
|
|
13073
13231
|
async "group.join"(ctx, params) {
|
|
13074
|
-
|
|
13232
|
+
let groupId;
|
|
13233
|
+
try {
|
|
13234
|
+
({groupId} = await node(ctx).joinGroupByLink({ link: str(params, "link", true) }));
|
|
13235
|
+
} catch (error) {
|
|
13236
|
+
if (error instanceof Error && error.message === "This is not a link to a group") throw new CliError("bad_request", error.message);
|
|
13237
|
+
throw error;
|
|
13238
|
+
}
|
|
13075
13239
|
return { group: groupId };
|
|
13076
13240
|
},
|
|
13077
13241
|
async "group.list"(ctx, params) {
|
|
@@ -13140,15 +13304,16 @@ var METHODS = {
|
|
|
13140
13304
|
}
|
|
13141
13305
|
const mentions = mentionsFor(text, list(params, "mentions"), group);
|
|
13142
13306
|
const wait = oneOf(params, "wait", ["none", "sent"], "none");
|
|
13143
|
-
const
|
|
13307
|
+
const linkId = `group:${group.id}`;
|
|
13308
|
+
const before = (await node(ctx).getMessage(linkId, messageId))?.edit?.seq ?? 0;
|
|
13144
13309
|
const result = await node(ctx).editMessage({
|
|
13145
|
-
linkId
|
|
13310
|
+
linkId,
|
|
13146
13311
|
messageId,
|
|
13147
13312
|
text,
|
|
13148
13313
|
...mentions.length ? { mentions } : {}
|
|
13149
13314
|
});
|
|
13150
|
-
if (result.error) throw editRefused(await
|
|
13151
|
-
const message =
|
|
13315
|
+
if (result.error) throw editRefused(await messageOf(ctx, linkId, messageId), messageId, "group", result.error, result.refused);
|
|
13316
|
+
const message = await node(ctx).getMessage(linkId, messageId);
|
|
13152
13317
|
const edits = message?.edit?.seq ?? 0;
|
|
13153
13318
|
if (edits === before) return {
|
|
13154
13319
|
group: group.id,
|
|
@@ -13314,12 +13479,32 @@ function groupSendRefused(error) {
|
|
|
13314
13479
|
return new CliError("unavailable", error);
|
|
13315
13480
|
}
|
|
13316
13481
|
/**
|
|
13482
|
+
* A chat command given a group's id, prefix or name: `not_found` that says it is a group, with the group command that
|
|
13483
|
+
* takes it (`react` on a group, where `group react` is meant). A name no chat and no group has stays the chat's error.
|
|
13484
|
+
*/
|
|
13485
|
+
function notAGroup(ctx, params, chat, command, rest) {
|
|
13486
|
+
try {
|
|
13487
|
+
return chat();
|
|
13488
|
+
} catch (error) {
|
|
13489
|
+
if (!(error instanceof CliError) || error.code !== "not_found") throw error;
|
|
13490
|
+
const ref = str(params, "chat", true);
|
|
13491
|
+
let group;
|
|
13492
|
+
try {
|
|
13493
|
+
group = findGroup(state(ctx).groups, ref);
|
|
13494
|
+
} catch {
|
|
13495
|
+
throw error;
|
|
13496
|
+
}
|
|
13497
|
+
const shown = /^[\w.:-]+$/.test(ref) ? ref : JSON.stringify(ref);
|
|
13498
|
+
throw new CliError("not_found", `${JSON.stringify(ref)} is a group, not a chat: ghostly ${command} ${shown} ${rest}`, { group: group.id });
|
|
13499
|
+
}
|
|
13500
|
+
}
|
|
13501
|
+
/**
|
|
13317
13502
|
* An edit the engine refused. A message that is not in the chat or group at all is `not_found` (exit 3), naming the id
|
|
13318
13503
|
* to give, as `button update` and `task update` do; before, a mistyped or lost id read "Only your own text messages
|
|
13319
13504
|
* can be edited" (`refused`, exit 1), as if the message were someone else's.
|
|
13320
13505
|
*/
|
|
13321
|
-
function editRefused(
|
|
13322
|
-
if (refused && !
|
|
13506
|
+
function editRefused(message, ref, where, error, refused) {
|
|
13507
|
+
if (refused && !message) return new CliError("not_found", `No message ${JSON.stringify(ref)} in this ${where}: give the messageId ${where === "group" ? "group send" : "send"} printed`);
|
|
13323
13508
|
return new CliError(refused ? "refused" : "unavailable", error);
|
|
13324
13509
|
}
|
|
13325
13510
|
/**
|
|
@@ -14482,6 +14667,11 @@ var showSecret = { "show-secret": {
|
|
|
14482
14667
|
type: "boolean",
|
|
14483
14668
|
description: "Print the group's entry link (anyone who has it can join)"
|
|
14484
14669
|
} };
|
|
14670
|
+
/** A pending chat's invite code lets whoever reads it take the chat: printed only when asked for. */
|
|
14671
|
+
var showInvite = { "show-secret": {
|
|
14672
|
+
type: "boolean",
|
|
14673
|
+
description: "Print a pending chat's invite code (whoever has it can take the chat)"
|
|
14674
|
+
} };
|
|
14485
14675
|
var secret = (options) => options["show-secret"] === true ? { showSecret: true } : {};
|
|
14486
14676
|
/**
|
|
14487
14677
|
* `--for` of the typing commands: the seconds it is kept on, 1 to 600, and never with `--stop`. A usage error (exit 2)
|
|
@@ -15050,8 +15240,10 @@ var commands$6 = {
|
|
|
15050
15240
|
},
|
|
15051
15241
|
"chat list": {
|
|
15052
15242
|
method: "chat.list",
|
|
15053
|
-
usage: "chat list",
|
|
15054
|
-
summary: "Chats, newest first"
|
|
15243
|
+
usage: "chat list [--show-secret]",
|
|
15244
|
+
summary: "Chats, newest first (a pending invite code only with --show-secret)",
|
|
15245
|
+
options: showInvite,
|
|
15246
|
+
params: ({ options }) => secret(options)
|
|
15055
15247
|
},
|
|
15056
15248
|
"chat remove": {
|
|
15057
15249
|
method: "chat.remove",
|
|
@@ -15079,10 +15271,14 @@ var commands$6 = {
|
|
|
15079
15271
|
},
|
|
15080
15272
|
"chat show": {
|
|
15081
15273
|
method: "chat.get",
|
|
15082
|
-
usage: "chat show <chat>",
|
|
15083
|
-
summary: "One chat and its connection",
|
|
15274
|
+
usage: "chat show <chat> [--show-secret]",
|
|
15275
|
+
summary: "One chat and its connection (its pending invite code only with --show-secret)",
|
|
15084
15276
|
args: ["chat"],
|
|
15085
|
-
|
|
15277
|
+
options: showInvite,
|
|
15278
|
+
params: ({ options }, { chat }) => ({
|
|
15279
|
+
chat,
|
|
15280
|
+
...secret(options)
|
|
15281
|
+
})
|
|
15086
15282
|
},
|
|
15087
15283
|
"chat transport": {
|
|
15088
15284
|
method: "chat.transport",
|
|
@@ -17173,6 +17369,14 @@ var refuseAnswers = Number(process.env.GHOSTLY_CALL_REFUSE_ANSWERS) || 0;
|
|
|
17173
17369
|
var GATHER_MS = 5e3;
|
|
17174
17370
|
var SETTLE_MS = 400;
|
|
17175
17371
|
/**
|
|
17372
|
+
* With a candidate in (a host one comes in milliseconds), the server reflexive one is waited for this long at most,
|
|
17373
|
+
* not the whole of `GATHER_MS`: a STUN server that answers does in tens of milliseconds, and one that never does (UDP
|
|
17374
|
+
* to it blocked, as on many office, school and hotel networks) made every call ring 5 s late and connect 5 s after
|
|
17375
|
+
* the answer. The apps' calls and the chats' data link bound theirs the same way (`HOST_GATHER_MS`,
|
|
17376
|
+
* `OFFER_HOST_GATHER_MS`).
|
|
17377
|
+
*/
|
|
17378
|
+
var HOST_GATHER_MS = 2e3;
|
|
17379
|
+
/**
|
|
17176
17380
|
* One call's peer connection. Its SDP never travels: the compact signal does (`local`), and each side rebuilds the
|
|
17177
17381
|
* other's SDP from it (`buildSdpFromSignal`), so the payload types, SSRC and mids here are the ones that rebuilt SDP
|
|
17178
17382
|
* uses: audio is mid 0, Opus is 111 unless the contact's offer says otherwise.
|
|
@@ -17244,7 +17448,7 @@ var CallMedia = class CallMedia {
|
|
|
17244
17448
|
const track = pc.addTrack(audioSection);
|
|
17245
17449
|
let videoTrack = null;
|
|
17246
17450
|
if (offer?.m?.includes("v")) {
|
|
17247
|
-
const video = new ndc.Video(String(offer.m.indexOf("v")), "
|
|
17451
|
+
const video = new ndc.Video(String(offer.m.indexOf("v")), "Inactive");
|
|
17248
17452
|
video.addVP8Codec(offer.vp ?? 96);
|
|
17249
17453
|
videoTrack = pc.addTrack(video);
|
|
17250
17454
|
videoTrack.onMessage(() => {});
|
|
@@ -17263,6 +17467,7 @@ var CallMedia = class CallMedia {
|
|
|
17263
17467
|
ss: [ssrc]
|
|
17264
17468
|
};
|
|
17265
17469
|
if (offer?.m) local.m = [...offer.m];
|
|
17470
|
+
if (videoTrack) local.vr = 0;
|
|
17266
17471
|
const listeners = [];
|
|
17267
17472
|
track.onMessage((packet) => {
|
|
17268
17473
|
for (const listener of listeners) listener(packet);
|
|
@@ -17343,20 +17548,26 @@ function mdnsNote(signal, log) {
|
|
|
17343
17548
|
lookup(host, { all: true }).then((found) => log(`${host} resolves here to ${found.map((a) => a.address).join(", ")}`), () => log(`${host} does not resolve here: the call connects only if the contact's checks reach this side`));
|
|
17344
17549
|
}
|
|
17345
17550
|
}
|
|
17346
|
-
/** Resolves once the local description has its candidates (see GATHER_MS). */
|
|
17551
|
+
/** Resolves once the local description has its candidates (see GATHER_MS and HOST_GATHER_MS). */
|
|
17347
17552
|
function gathering(pc) {
|
|
17348
17553
|
return new Promise((resolve) => {
|
|
17349
17554
|
let settle = null;
|
|
17555
|
+
let any = false;
|
|
17350
17556
|
const done = () => {
|
|
17351
17557
|
clearTimeout(cap);
|
|
17558
|
+
clearTimeout(bounded);
|
|
17352
17559
|
if (settle) clearTimeout(settle);
|
|
17353
17560
|
resolve();
|
|
17354
17561
|
};
|
|
17355
17562
|
const cap = setTimeout(done, GATHER_MS);
|
|
17563
|
+
const bounded = setTimeout(() => {
|
|
17564
|
+
if (any) done();
|
|
17565
|
+
}, HOST_GATHER_MS);
|
|
17356
17566
|
pc.onGatheringStateChange((state) => {
|
|
17357
17567
|
if (state === "complete") done();
|
|
17358
17568
|
});
|
|
17359
17569
|
pc.onLocalCandidate((candidate) => {
|
|
17570
|
+
any = true;
|
|
17360
17571
|
if (!settle && / typ srflx/.test(candidate)) settle = setTimeout(done, SETTLE_MS);
|
|
17361
17572
|
});
|
|
17362
17573
|
});
|
|
@@ -18012,23 +18223,15 @@ var EventHub = class {
|
|
|
18012
18223
|
request.onsuccess = () => resolve(request.result);
|
|
18013
18224
|
request.onerror = () => reject(request.error);
|
|
18014
18225
|
});
|
|
18015
|
-
const
|
|
18016
|
-
|
|
18017
|
-
|
|
18018
|
-
request.onsuccess = () => {
|
|
18019
|
-
const cursor = request.result;
|
|
18020
|
-
if (!cursor) return resolve(out);
|
|
18021
|
-
out.push({
|
|
18022
|
-
key: cursor.key,
|
|
18023
|
-
value: cursor.value
|
|
18024
|
-
});
|
|
18025
|
-
cursor.continue();
|
|
18026
|
-
};
|
|
18226
|
+
const store = this.db.transaction(SEEN).objectStore(SEEN);
|
|
18227
|
+
const all = (request) => new Promise((resolve, reject) => {
|
|
18228
|
+
request.onsuccess = () => resolve(request.result);
|
|
18027
18229
|
request.onerror = () => reject(request.error);
|
|
18028
18230
|
});
|
|
18029
|
-
|
|
18030
|
-
|
|
18031
|
-
|
|
18231
|
+
const [keys, values] = await Promise.all([all(store.getAllKeys()), all(store.getAll())]);
|
|
18232
|
+
for (let i = 0; i < keys.length; i++) {
|
|
18233
|
+
const [chat, id] = keys[i];
|
|
18234
|
+
this.seenOf(chat).set(id, values[i]);
|
|
18032
18235
|
}
|
|
18033
18236
|
this.mentionsNoted = this.firstRun || this.seen.get(FORMAT)?.get(MENTIONS_NOTED) === "yes";
|
|
18034
18237
|
}
|
|
@@ -18514,8 +18717,13 @@ var EventHub = class {
|
|
|
18514
18717
|
}
|
|
18515
18718
|
if (changes.length) this.persist(key, changes);
|
|
18516
18719
|
}
|
|
18720
|
+
/**
|
|
18721
|
+
* Relaxed: nobody waits for it, and a note lost to a power cut is made again from the history at the next start
|
|
18722
|
+
* (its event is then reported a second time, as after a crash between a message's row and its note). A disk sync
|
|
18723
|
+
* for each one held the whole daemon for milliseconds, three times per message sent.
|
|
18724
|
+
*/
|
|
18517
18725
|
persist(chat, changes) {
|
|
18518
|
-
const store = this.db.transaction(SEEN, "readwrite").objectStore(SEEN);
|
|
18726
|
+
const store = this.db.transaction(SEEN, "readwrite", { durability: "relaxed" }).objectStore(SEEN);
|
|
18519
18727
|
for (const [id, state] of changes) if (state === null) store.delete([chat, id]);
|
|
18520
18728
|
else store.put(state, [chat, id]);
|
|
18521
18729
|
}
|
|
@@ -18890,7 +19098,7 @@ function federationInfo(federationId, config) {
|
|
|
18890
19098
|
};
|
|
18891
19099
|
}
|
|
18892
19100
|
/** The first value a subscription gives that is final, or the last one within `waitMs`. */
|
|
18893
|
-
function watch(subscribe, final, waitMs) {
|
|
19101
|
+
function watch$1(subscribe, final, waitMs) {
|
|
18894
19102
|
return new Promise((resolve) => {
|
|
18895
19103
|
let last, done = false, cancel = () => {};
|
|
18896
19104
|
const finish = () => {
|
|
@@ -18961,13 +19169,13 @@ var RealClient = class {
|
|
|
18961
19169
|
return this.wallet.mint.tryCancelSpendNotes(operationId);
|
|
18962
19170
|
}
|
|
18963
19171
|
spendState(operationId, waitMs) {
|
|
18964
|
-
return watch((ok, fail) => this.wallet.mint.subscribeSpendNotes(operationId, (s) => ok(SPEND[s] ?? "created"), fail), spendFinal, waitMs);
|
|
19172
|
+
return watch$1((ok, fail) => this.wallet.mint.subscribeSpendNotes(operationId, (s) => ok(SPEND[s] ?? "created"), fail), spendFinal, waitMs);
|
|
18965
19173
|
}
|
|
18966
19174
|
redeem(notes, ghostly) {
|
|
18967
19175
|
return this.wallet.mint.reissueExternalNotes(notes, { ghostly });
|
|
18968
19176
|
}
|
|
18969
19177
|
redeemState(operationId, waitMs) {
|
|
18970
|
-
return watch((ok, fail) => this.wallet.mint.subscribeReissueExternalNotes(operationId, (s) => ok(typeof s === "string" ? REDEEM[s] ?? "created" : s && typeof s === "object" && "Failed" in s ? "failed" : "created"), (error) => {
|
|
19178
|
+
return watch$1((ok, fail) => this.wallet.mint.subscribeReissueExternalNotes(operationId, (s) => ok(typeof s === "string" ? REDEEM[s] ?? "created" : s && typeof s === "object" && "Failed" in s ? "failed" : "created"), (error) => {
|
|
18971
19179
|
ok("failed");
|
|
18972
19180
|
fail(error);
|
|
18973
19181
|
}), (s) => s === "done" || s === "failed", waitMs);
|
|
@@ -18980,7 +19188,7 @@ var RealClient = class {
|
|
|
18980
19188
|
};
|
|
18981
19189
|
}
|
|
18982
19190
|
receiveState(operationId, waitMs) {
|
|
18983
|
-
return watch((ok, fail) => this.wallet.lightning.subscribeLnReceive(operationId, (s) => ok(receiveOf(s)), fail), (s) => s === "claimed" || s === "canceled", waitMs);
|
|
19191
|
+
return watch$1((ok, fail) => this.wallet.lightning.subscribeLnReceive(operationId, (s) => ok(receiveOf(s)), fail), (s) => s === "claimed" || s === "canceled", waitMs);
|
|
18984
19192
|
}
|
|
18985
19193
|
async gatewayFee(amountMsats) {
|
|
18986
19194
|
await this.wallet.lightning.updateGatewayCache().catch(() => {});
|
|
@@ -18998,7 +19206,7 @@ var RealClient = class {
|
|
|
18998
19206
|
};
|
|
18999
19207
|
}
|
|
19000
19208
|
async payState(operationId, internal, waitMs) {
|
|
19001
|
-
const state = await watch((ok, fail) => internal ? this.wallet.lightning.subscribeInternalPayment(operationId, ok, fail) : this.wallet.lightning.subscribeLnPay(operationId, ok, fail), (s) => typeof s === "object" && !!s && ("success" in s || "preimage" in s || "refunded" in s || "refund_success" in s || "unexpected_error" in s || "funding_failed" in s) || s === "canceled", waitMs);
|
|
19209
|
+
const state = await watch$1((ok, fail) => internal ? this.wallet.lightning.subscribeInternalPayment(operationId, ok, fail) : this.wallet.lightning.subscribeLnPay(operationId, ok, fail), (s) => typeof s === "object" && !!s && ("success" in s || "preimage" in s || "refunded" in s || "refund_success" in s || "unexpected_error" in s || "funding_failed" in s) || s === "canceled", waitMs);
|
|
19002
19210
|
if (state && typeof state === "object") {
|
|
19003
19211
|
const s = state;
|
|
19004
19212
|
if (s.success?.preimage || s.preimage) return {
|
|
@@ -19333,6 +19541,10 @@ var SNAPSHOT = "snapshot.bin";
|
|
|
19333
19541
|
var JOURNAL = "journal.bin";
|
|
19334
19542
|
/** A journal past this is folded into the snapshot at the next quiet moment. */
|
|
19335
19543
|
var COMPACT_BYTES = 33554432;
|
|
19544
|
+
/** How long `close` waits for the transactions still running: one left open for good does not keep the process. */
|
|
19545
|
+
var CLOSE_WAIT_MS = 5e3;
|
|
19546
|
+
/** How long a relaxed transaction's entry may wait for a sync when no other transaction brings one. */
|
|
19547
|
+
var RELAXED_SYNC_MS = 1e3;
|
|
19336
19548
|
var BLOB = "__ghostlyBlob";
|
|
19337
19549
|
var RANGE = "__ghostlyRange";
|
|
19338
19550
|
var PROBE = "__ghostly_storage_probe__";
|
|
@@ -19348,18 +19560,21 @@ async function openPersistentIndexedDb(dir) {
|
|
|
19348
19560
|
const factory = new IDBFactory();
|
|
19349
19561
|
const raw = factory;
|
|
19350
19562
|
const classes = await probeClasses(factory);
|
|
19563
|
+
dropIndexEntriesByRecord(classes, (a, b) => factory.cmp(a, b));
|
|
19351
19564
|
let writing = true;
|
|
19352
19565
|
let restoring = true;
|
|
19353
19566
|
let journalFd = null;
|
|
19354
19567
|
let queue = Promise.resolve();
|
|
19355
19568
|
let queued = 0;
|
|
19356
19569
|
let schemaDirty = false;
|
|
19570
|
+
/** Set while the journal holds relaxed entries no sync has covered yet. */
|
|
19571
|
+
let relaxed = null;
|
|
19357
19572
|
const pending = /* @__PURE__ */ new WeakMap();
|
|
19358
19573
|
const snapshotPath = join(dir, SNAPSHOT);
|
|
19359
19574
|
const journalPath = join(dir, JOURNAL);
|
|
19360
19575
|
const snapshotFound = existsSync(snapshotPath), journalFound = existsSync(journalPath);
|
|
19361
|
-
if (snapshotFound) restore(classes, raw._databases, deserialize(readFileSync(snapshotPath)), (a, b) => factory.cmp(a, b));
|
|
19362
|
-
if (journalFound) replay(raw._databases, readFileSync(journalPath));
|
|
19576
|
+
if (snapshotFound) restore(classes, raw._databases, deserialize(readFileSync(snapshotPath)), keyOrder((a, b) => factory.cmp(a, b)));
|
|
19577
|
+
if (journalFound) replay(raw._databases, readFileSync(journalPath), keyOrder((a, b) => factory.cmp(a, b)));
|
|
19363
19578
|
restoring = false;
|
|
19364
19579
|
if (journalFound || !snapshotFound) await writeSnapshot();
|
|
19365
19580
|
const storeProto = classes.ObjectStore.prototype;
|
|
@@ -19406,7 +19621,7 @@ async function openPersistentIndexedDb(dir) {
|
|
|
19406
19621
|
if (this.mode === "versionchange") schemaDirty = true;
|
|
19407
19622
|
else {
|
|
19408
19623
|
const ops = pending.get(this._rollbackLog);
|
|
19409
|
-
if (ops?.length) append(ops);
|
|
19624
|
+
if (ops?.length) append(ops, this.durability === "relaxed");
|
|
19410
19625
|
}
|
|
19411
19626
|
pending.delete(this._rollbackLog);
|
|
19412
19627
|
if (schemaDirty) enqueue(writeSnapshot);
|
|
@@ -19428,34 +19643,54 @@ async function openPersistentIndexedDb(dir) {
|
|
|
19428
19643
|
});
|
|
19429
19644
|
return request;
|
|
19430
19645
|
};
|
|
19431
|
-
function append(ops) {
|
|
19646
|
+
function append(ops, lazy) {
|
|
19432
19647
|
if (queued === 0 && !ops.some((op) => op.kind === "put" && hasBlob(op.value))) {
|
|
19433
|
-
writeEntry(serialize(ops));
|
|
19648
|
+
writeEntry(serialize(ops), lazy);
|
|
19434
19649
|
return;
|
|
19435
19650
|
}
|
|
19436
19651
|
enqueue(async () => writeEntry(serialize(await Promise.all(ops.map(async (op) => op.kind === "put" ? {
|
|
19437
19652
|
...op,
|
|
19438
19653
|
value: await blobsToDisk(op.value)
|
|
19439
|
-
} : op)))));
|
|
19654
|
+
} : op))), lazy));
|
|
19440
19655
|
}
|
|
19441
19656
|
function enqueue(work) {
|
|
19442
19657
|
queued++;
|
|
19443
|
-
queue = queue.then(work).catch((
|
|
19444
|
-
process.stderr.write(`ghostly: storage write failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
19445
|
-
}).finally(() => {
|
|
19658
|
+
queue = queue.then(work).catch(failed).finally(() => {
|
|
19446
19659
|
queued--;
|
|
19447
19660
|
});
|
|
19448
19661
|
return queue;
|
|
19449
19662
|
}
|
|
19450
|
-
function
|
|
19663
|
+
function failed(error) {
|
|
19664
|
+
process.stderr.write(`ghostly: storage write failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
19665
|
+
}
|
|
19666
|
+
/**
|
|
19667
|
+
* `lazy`: a relaxed transaction's entry. A sync blocks the whole process for milliseconds, and what such a
|
|
19668
|
+
* transaction wrote can be made again (the event set's note of a message it already reported): written, it outlives
|
|
19669
|
+
* the process; a power cut may take its last second.
|
|
19670
|
+
*/
|
|
19671
|
+
function writeEntry(entry, lazy = false) {
|
|
19451
19672
|
if (!writing) return;
|
|
19452
19673
|
journalFd ??= openSync(journalPath, "a", 384);
|
|
19453
19674
|
const header = Buffer.alloc(4);
|
|
19454
19675
|
header.writeUInt32BE(entry.length);
|
|
19455
19676
|
writeSync(journalFd, Buffer.concat([header, entry]));
|
|
19456
|
-
|
|
19677
|
+
if (lazy) relaxed ??= setTimeout(() => {
|
|
19678
|
+
try {
|
|
19679
|
+
sync();
|
|
19680
|
+
} catch (error) {
|
|
19681
|
+
failed(error);
|
|
19682
|
+
}
|
|
19683
|
+
}, RELAXED_SYNC_MS).unref();
|
|
19684
|
+
else sync();
|
|
19457
19685
|
if (statSync(journalPath).size > COMPACT_BYTES) enqueue(writeSnapshot);
|
|
19458
19686
|
}
|
|
19687
|
+
function sync() {
|
|
19688
|
+
if (relaxed) {
|
|
19689
|
+
clearTimeout(relaxed);
|
|
19690
|
+
relaxed = null;
|
|
19691
|
+
}
|
|
19692
|
+
if (journalFd !== null) fsyncSync(journalFd);
|
|
19693
|
+
}
|
|
19459
19694
|
async function writeSnapshot() {
|
|
19460
19695
|
schemaDirty = false;
|
|
19461
19696
|
const snapshot = {
|
|
@@ -19498,12 +19733,29 @@ async function openPersistentIndexedDb(dir) {
|
|
|
19498
19733
|
closeSync(fd);
|
|
19499
19734
|
}
|
|
19500
19735
|
renameSync(temporary, snapshotPath);
|
|
19736
|
+
if (relaxed) {
|
|
19737
|
+
clearTimeout(relaxed);
|
|
19738
|
+
relaxed = null;
|
|
19739
|
+
}
|
|
19501
19740
|
if (journalFd !== null) {
|
|
19502
19741
|
closeSync(journalFd);
|
|
19503
19742
|
journalFd = null;
|
|
19504
19743
|
}
|
|
19505
19744
|
rmSync(journalPath, { force: true });
|
|
19506
19745
|
}
|
|
19746
|
+
/**
|
|
19747
|
+
* Resolves once no transaction is left to run. The engine does not wait for every write (`void db.deleteLink(…)`),
|
|
19748
|
+
* and fake-indexeddb runs a transaction on a later turn of the event loop: a command that closed in the turn it wrote
|
|
19749
|
+
* in (no chat to say goodbye to) took its last snapshot before the write, and answered done for what was not on disk.
|
|
19750
|
+
* Two quiet turns in a row: a write that begins when another ends (a chat's messages after its row) is seen too.
|
|
19751
|
+
*/
|
|
19752
|
+
async function settled() {
|
|
19753
|
+
const until = Date.now() + CLOSE_WAIT_MS;
|
|
19754
|
+
for (let quiet = 0; quiet < 2 && Date.now() < until;) {
|
|
19755
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
19756
|
+
quiet = [...raw._databases.values()].some((db) => db.transactions.some((transaction) => transaction._state !== "finished")) ? 0 : quiet + 1;
|
|
19757
|
+
}
|
|
19758
|
+
}
|
|
19507
19759
|
const installed = globalThis;
|
|
19508
19760
|
installed.indexedDB = factory;
|
|
19509
19761
|
installed.IDBKeyRange = IDBKeyRange$1;
|
|
@@ -19512,6 +19764,7 @@ async function openPersistentIndexedDb(dir) {
|
|
|
19512
19764
|
flush: () => queue,
|
|
19513
19765
|
compact: () => enqueue(writeSnapshot),
|
|
19514
19766
|
async close() {
|
|
19767
|
+
await settled();
|
|
19515
19768
|
await enqueue(writeSnapshot);
|
|
19516
19769
|
writing = false;
|
|
19517
19770
|
if (journalFd !== null) {
|
|
@@ -19535,7 +19788,8 @@ async function probeClasses(factory) {
|
|
|
19535
19788
|
const classes = {
|
|
19536
19789
|
Database: rawDb.constructor,
|
|
19537
19790
|
ObjectStore: rawStore.constructor,
|
|
19538
|
-
Index: rawStore.rawIndexes.get("i").constructor
|
|
19791
|
+
Index: rawStore.rawIndexes.get("i").constructor,
|
|
19792
|
+
RecordStore: rawStore.records.constructor
|
|
19539
19793
|
};
|
|
19540
19794
|
db.close();
|
|
19541
19795
|
await new Promise((resolve) => {
|
|
@@ -19544,6 +19798,108 @@ async function probeClasses(factory) {
|
|
|
19544
19798
|
});
|
|
19545
19799
|
return classes;
|
|
19546
19800
|
}
|
|
19801
|
+
/** The classes whose index upkeep is already by record: they are fake-indexeddb's own, shared by every store opened. */
|
|
19802
|
+
var byRecord = /* @__PURE__ */ new WeakSet();
|
|
19803
|
+
/**
|
|
19804
|
+
* fake-indexeddb drops an overwritten or deleted record from an index by walking every entry of it (an index is in
|
|
19805
|
+
* its own key's order, not the record's): each delivery mark, edit or reaction cost time with every message of the
|
|
19806
|
+
* profile, 170 ms at 10,000, and a long chat slowed every other one. The store has the records it just replaced or
|
|
19807
|
+
* removed in hand, and they say which entries to drop; a call that comes any other way still walks.
|
|
19808
|
+
*/
|
|
19809
|
+
function dropIndexEntriesByRecord(classes, cmp) {
|
|
19810
|
+
const storeProto = classes.ObjectStore.prototype;
|
|
19811
|
+
const recordsProto = classes.RecordStore.prototype;
|
|
19812
|
+
if (byRecord.has(storeProto)) return;
|
|
19813
|
+
byRecord.add(storeProto);
|
|
19814
|
+
const { storeRecord, deleteRecord } = storeProto;
|
|
19815
|
+
const { put, delete: remove, deleteByValue } = recordsProto;
|
|
19816
|
+
let gone = null;
|
|
19817
|
+
storeProto.storeRecord = function(record, noOverwrite, rollbackLog) {
|
|
19818
|
+
if (!this.rawIndexes.size) return storeRecord.call(this, record, noOverwrite, rollbackLog);
|
|
19819
|
+
const records = this.records, indexes = this.rawIndexes, outer = gone;
|
|
19820
|
+
records.put = (next, keep) => {
|
|
19821
|
+
const replaced = put.call(this.records, next, keep);
|
|
19822
|
+
gone = {
|
|
19823
|
+
indexes,
|
|
19824
|
+
records: replaced ? [replaced] : []
|
|
19825
|
+
};
|
|
19826
|
+
return replaced;
|
|
19827
|
+
};
|
|
19828
|
+
try {
|
|
19829
|
+
return storeRecord.call(this, record, noOverwrite, rollbackLog);
|
|
19830
|
+
} finally {
|
|
19831
|
+
delete records.put;
|
|
19832
|
+
gone = outer;
|
|
19833
|
+
}
|
|
19834
|
+
};
|
|
19835
|
+
storeProto.deleteRecord = function(key, rollbackLog) {
|
|
19836
|
+
if (!this.rawIndexes.size) return deleteRecord.call(this, key, rollbackLog);
|
|
19837
|
+
const records = this.records, indexes = this.rawIndexes, outer = gone;
|
|
19838
|
+
records.delete = (range) => {
|
|
19839
|
+
const removed = remove.call(this.records, range);
|
|
19840
|
+
gone = {
|
|
19841
|
+
indexes,
|
|
19842
|
+
records: removed
|
|
19843
|
+
};
|
|
19844
|
+
return removed;
|
|
19845
|
+
};
|
|
19846
|
+
try {
|
|
19847
|
+
deleteRecord.call(this, key, rollbackLog);
|
|
19848
|
+
} finally {
|
|
19849
|
+
delete records.delete;
|
|
19850
|
+
gone = outer;
|
|
19851
|
+
}
|
|
19852
|
+
};
|
|
19853
|
+
recordsProto.deleteByValue = function(key) {
|
|
19854
|
+
let index;
|
|
19855
|
+
if (gone) {
|
|
19856
|
+
for (const candidate of gone.indexes.values()) if (candidate.records === this) index = candidate;
|
|
19857
|
+
}
|
|
19858
|
+
if (!gone || !index) return deleteByValue.call(this, key);
|
|
19859
|
+
return dropEntries(index, gone.records, cmp);
|
|
19860
|
+
};
|
|
19861
|
+
}
|
|
19862
|
+
/** Drops from an index the entries of records that left its store, and gives back the ones it dropped. */
|
|
19863
|
+
function dropEntries(index, records, cmp) {
|
|
19864
|
+
const dropped = [];
|
|
19865
|
+
for (const entry of indexEntries(index, records, cmp)) {
|
|
19866
|
+
if (index.records.keysAreUnique) {
|
|
19867
|
+
const held = index.records.get(entry.key);
|
|
19868
|
+
if (!held || cmp(held.value, entry.value) !== 0) continue;
|
|
19869
|
+
}
|
|
19870
|
+
index.records.records.delete(entry);
|
|
19871
|
+
dropped.push(entry);
|
|
19872
|
+
}
|
|
19873
|
+
return dropped;
|
|
19874
|
+
}
|
|
19875
|
+
/**
|
|
19876
|
+
* `cmp` for keys that are stored already (a record's, an index entry's): numbers, strings and arrays of them are
|
|
19877
|
+
* ordered here, as IndexedDB orders them (a number before a string before an array; strings by code unit; arrays item
|
|
19878
|
+
* by item, then the shorter first). Anything else (a date, bytes, a value that is no key) is left to `cmp`, which
|
|
19879
|
+
* checks and copies both keys at every call: on the way back from a snapshot that was a third of a large store's time.
|
|
19880
|
+
*/
|
|
19881
|
+
function keyOrder(cmp) {
|
|
19882
|
+
const order = (a, b) => {
|
|
19883
|
+
const first = typeof a, second = typeof b;
|
|
19884
|
+
if (first === "number" && second === "number") return a === b ? 0 : a > b ? 1 : a < b ? -1 : cmp(a, b);
|
|
19885
|
+
if (first === "string" && second === "string") return a === b ? 0 : a > b ? 1 : -1;
|
|
19886
|
+
if (first === "number" && second === "string") return -1;
|
|
19887
|
+
if (first === "string" && second === "number") return 1;
|
|
19888
|
+
if (Array.isArray(a)) {
|
|
19889
|
+
if (second === "number" || second === "string") return 1;
|
|
19890
|
+
if (!Array.isArray(b)) return cmp(a, b);
|
|
19891
|
+
const length = Math.min(a.length, b.length);
|
|
19892
|
+
for (let at = 0; at < length; at++) {
|
|
19893
|
+
const item = order(a[at], b[at]);
|
|
19894
|
+
if (item !== 0) return item;
|
|
19895
|
+
}
|
|
19896
|
+
return a.length === b.length ? 0 : a.length > b.length ? 1 : -1;
|
|
19897
|
+
}
|
|
19898
|
+
if (Array.isArray(b) && (first === "number" || first === "string")) return -1;
|
|
19899
|
+
return cmp(a, b);
|
|
19900
|
+
};
|
|
19901
|
+
return order;
|
|
19902
|
+
}
|
|
19547
19903
|
/**
|
|
19548
19904
|
* The snapshot's records come in key order (as `writeSnapshot` walks them), so each store's tree and each index's are
|
|
19549
19905
|
* built whole from sorted arrays: storing record by record compared every key down the tree, a 70,000-message profile
|
|
@@ -19639,21 +19995,27 @@ function fill(store, sorted) {
|
|
|
19639
19995
|
/**
|
|
19640
19996
|
* Replays whole entries; a torn last entry (the process died mid-write) was never acknowledged and is dropped.
|
|
19641
19997
|
*
|
|
19642
|
-
* A store's indexes are set aside while its ops replay and
|
|
19998
|
+
* A store's indexes are set aside while its ops replay and brought in step once at the end: fake-indexeddb drops an
|
|
19643
19999
|
* overwritten or deleted record from an index by walking every record of it, so a crash after a busy chat took
|
|
19644
|
-
* tens of seconds to minutes to start, one write at a time against the whole profile.
|
|
20000
|
+
* tens of seconds to minutes to start, one write at a time against the whole profile. Only the rows the journal
|
|
20001
|
+
* touched change in an index (what each was before goes, what it is now comes in): a journal of one message costs one
|
|
20002
|
+
* message, not the profile's history once per index. A store the journal cleared has its indexes built again whole.
|
|
19645
20003
|
*/
|
|
19646
|
-
function replay(databases, journal) {
|
|
20004
|
+
function replay(databases, journal, cmp) {
|
|
19647
20005
|
const touched = /* @__PURE__ */ new Map();
|
|
19648
20006
|
try {
|
|
19649
20007
|
replayEntries(databases, journal, touched);
|
|
19650
20008
|
} finally {
|
|
19651
|
-
for (const [store, indexes] of touched) {
|
|
20009
|
+
for (const [store, { indexes, was, now }] of touched) {
|
|
20010
|
+
const records = store.records;
|
|
20011
|
+
delete records.put;
|
|
20012
|
+
delete records.delete;
|
|
19652
20013
|
store.rawIndexes = indexes;
|
|
19653
20014
|
for (const index of indexes.values()) {
|
|
19654
|
-
index.records.clear();
|
|
20015
|
+
if (!was) index.records.clear();
|
|
19655
20016
|
if (!index.initialized) continue;
|
|
19656
|
-
|
|
20017
|
+
if (was) dropEntries(index, [...was], cmp);
|
|
20018
|
+
for (const record of was ? now : store.records.values()) if (!absent(index.keyPath, record.value)) index.storeRecord(record);
|
|
19657
20019
|
}
|
|
19658
20020
|
}
|
|
19659
20021
|
}
|
|
@@ -19673,19 +20035,50 @@ function replayEntries(databases, journal, touched) {
|
|
|
19673
20035
|
for (const op of ops) {
|
|
19674
20036
|
const store = databases.get(op.db)?.rawObjectStores.get(op.store);
|
|
19675
20037
|
if (!store) continue;
|
|
19676
|
-
|
|
19677
|
-
|
|
19678
|
-
store.rawIndexes = /* @__PURE__ */ new Map();
|
|
19679
|
-
}
|
|
20038
|
+
let change = touched.get(store);
|
|
20039
|
+
if (!change) touched.set(store, change = watch(store));
|
|
19680
20040
|
if (op.kind === "put") store.storeRecord({
|
|
19681
20041
|
key: op.key,
|
|
19682
20042
|
value: blobsFromDisk(op.value)
|
|
19683
20043
|
}, false);
|
|
19684
20044
|
else if (op.kind === "delete") store.deleteRecord(keyFromDisk(op.key));
|
|
19685
|
-
else
|
|
20045
|
+
else {
|
|
20046
|
+
change.was = null;
|
|
20047
|
+
store.clear();
|
|
20048
|
+
}
|
|
19686
20049
|
}
|
|
19687
20050
|
}
|
|
19688
20051
|
}
|
|
20052
|
+
/**
|
|
20053
|
+
* Sets a store's indexes aside and notes, as its records are written over and removed, which ones left and which
|
|
20054
|
+
* ones came: the record store hands back what a put replaced and what a delete removed, so a row costs no lookup.
|
|
20055
|
+
* A record that came and left again was never in an index.
|
|
20056
|
+
*/
|
|
20057
|
+
function watch(store) {
|
|
20058
|
+
const change = {
|
|
20059
|
+
indexes: store.rawIndexes,
|
|
20060
|
+
was: store.rawIndexes.size ? /* @__PURE__ */ new Set() : null,
|
|
20061
|
+
now: /* @__PURE__ */ new Set()
|
|
20062
|
+
};
|
|
20063
|
+
store.rawIndexes = /* @__PURE__ */ new Map();
|
|
20064
|
+
if (!change.was) return change;
|
|
20065
|
+
const { put, delete: remove } = store.records;
|
|
20066
|
+
const left = (record) => {
|
|
20067
|
+
if (!change.now.delete(record)) change.was?.add(record);
|
|
20068
|
+
};
|
|
20069
|
+
store.records.put = (next, keep) => {
|
|
20070
|
+
const replaced = put.call(store.records, next, keep);
|
|
20071
|
+
if (replaced) left(replaced);
|
|
20072
|
+
change.now.add(next);
|
|
20073
|
+
return replaced;
|
|
20074
|
+
};
|
|
20075
|
+
store.records.delete = (key) => {
|
|
20076
|
+
const removed = remove.call(store.records, key);
|
|
20077
|
+
removed.forEach(left);
|
|
20078
|
+
return removed;
|
|
20079
|
+
};
|
|
20080
|
+
return change;
|
|
20081
|
+
}
|
|
19689
20082
|
function keyToDisk(key) {
|
|
19690
20083
|
if (key instanceof IDBKeyRange$1) return { [RANGE]: {
|
|
19691
20084
|
lower: key.lower,
|
|
@@ -19939,14 +20332,14 @@ async function startRuntime(paths, options = {}) {
|
|
|
19939
20332
|
(await import("./assets/ghostly_iroh_web-Dvzh3Y6E.js")).initSync({ module: irohWasmBytes() });
|
|
19940
20333
|
const [{ registerFileBytes }, { NodeFileBytes }] = await Promise.all([import("./assets/fileBytes-BcYZhNgF.js").then((n) => n.o), import("./assets/fileBytes-Dbifu5I5.js")]);
|
|
19941
20334
|
registerFileBytes("native", async () => new NodeFileBytes(paths.files), true);
|
|
19942
|
-
const [{ EngineServer }, { createHyperEndpoint }] = await Promise.all([import("./assets/server-
|
|
20335
|
+
const [{ EngineServer }, { createHyperEndpoint }] = await Promise.all([import("./assets/server-DHMXMwNn.js").then((n) => n.t), import("./assets/endpoint-pgFkR2AS.js")]);
|
|
19943
20336
|
const network = hyperdhtNetwork();
|
|
19944
20337
|
const dht = mainlineNetwork();
|
|
19945
|
-
const mainline = dht.off ? null : new (await (import("./assets/mainline-
|
|
20338
|
+
const mainline = dht.off ? null : new (await (import("./assets/mainline-DriRu_hb.js"))).Mainline({
|
|
19946
20339
|
bootstrap: dht.bootstrap,
|
|
19947
20340
|
host: dht.host
|
|
19948
20341
|
});
|
|
19949
|
-
const transport = mainline || pinned ? new (await (import("./assets/mainline-
|
|
20342
|
+
const transport = mainline || pinned ? new (await (import("./assets/mainline-DriRu_hb.js"))).RelaysAndDht(mainline, pinned ? { relays: pinned } : {}, !!pinned) : null;
|
|
19950
20343
|
const server = new EngineServer({
|
|
19951
20344
|
...transport ? { transport } : {},
|
|
19952
20345
|
irohWeb: true,
|
|
@@ -20091,10 +20484,14 @@ var guarded = false;
|
|
|
20091
20484
|
/**
|
|
20092
20485
|
* A daemon says an error nobody caught (with its stack, on stderr: the daemon's log when detached) and keeps serving:
|
|
20093
20486
|
* one request gone wrong must not take the profile offline. One-shot commands do not call this; they still fail loudly.
|
|
20487
|
+
*
|
|
20488
|
+
* `outputs`: what it writes to. A write there that fails (a pipe whose reader is gone: the process that started the
|
|
20489
|
+
* daemon died) is dropped, not an error nobody caught: saying it would be one more failed write, and the next, with
|
|
20490
|
+
* no turn left for anything else (a full core, and no stop on SIGTERM).
|
|
20094
20491
|
*/
|
|
20095
20492
|
function keepServing(proc = process, write = (text) => {
|
|
20096
20493
|
process.stderr.write(text);
|
|
20097
|
-
}) {
|
|
20494
|
+
}, outputs = proc === process ? [process.stdout, process.stderr] : []) {
|
|
20098
20495
|
if (proc === process) {
|
|
20099
20496
|
if (guarded) return;
|
|
20100
20497
|
guarded = true;
|
|
@@ -20104,6 +20501,7 @@ function keepServing(proc = process, write = (text) => {
|
|
|
20104
20501
|
write(`ghostly: ${what}, the daemon keeps serving: ${error instanceof Error ? error.stack ?? error.message : String(error)}\n`);
|
|
20105
20502
|
} catch {}
|
|
20106
20503
|
};
|
|
20504
|
+
for (const output of outputs) output.on("error", () => {});
|
|
20107
20505
|
proc.on("unhandledRejection", log("unhandled rejection"));
|
|
20108
20506
|
proc.on("uncaughtException", log("uncaught exception"));
|
|
20109
20507
|
}
|
|
@@ -20307,6 +20705,18 @@ function readCursor(path) {
|
|
|
20307
20705
|
const value = Number(readFileSync(path, "utf8").trim());
|
|
20308
20706
|
return Number.isInteger(value) && value >= 0 ? value : void 0;
|
|
20309
20707
|
}
|
|
20708
|
+
/**
|
|
20709
|
+
* Whether a cursor can be kept at `path`, asked before anything starts: its folder is there and takes a file. Before, a
|
|
20710
|
+
* folder that was not there was found at the first event: the listener ran, said "event N failed: ENOENT" for each one
|
|
20711
|
+
* and never wrote the cursor, so the next start began at the journal's end and skipped what came while it was down.
|
|
20712
|
+
* Throws what the file system says (`asCliError` names the path and the code: not_found, refused).
|
|
20713
|
+
*/
|
|
20714
|
+
function checkCursor(path) {
|
|
20715
|
+
if (!path) return;
|
|
20716
|
+
const folder = dirname(resolve(path));
|
|
20717
|
+
if (!statSync(folder).isDirectory()) throw new CliError("not_found", `No such file or folder: ${folder}`);
|
|
20718
|
+
accessSync(folder, constants.W_OK | constants.X_OK);
|
|
20719
|
+
}
|
|
20310
20720
|
function writeCursor(path, seq) {
|
|
20311
20721
|
rmSync(path + ".tmp", { force: true });
|
|
20312
20722
|
writeFileSync(path + ".tmp", String(seq) + "\n", {
|
|
@@ -20603,7 +21013,7 @@ var SPECIAL = [
|
|
|
20603
21013
|
"The profile's settings",
|
|
20604
21014
|
{ "show-secret": o("boolean", "Show credentials too") }
|
|
20605
21015
|
],
|
|
20606
|
-
["settings set <key> <json-value>", "Change one: relays, irohRelays, hyperdhtRelay, readRelays, iceServers, publicProfiles, online, shareProfile, sendTyping, nick"],
|
|
21016
|
+
["settings set <key> <json-value>", "Change one: relays, irohRelays, hyperdhtRelay, readRelays, iceServers, publicProfiles, online, shareProfile, sendTyping, autoDownloads, serveFiles, nick"],
|
|
20607
21017
|
[
|
|
20608
21018
|
"engine <method> [json-params | -] [--confirm-real] [--show-secret]",
|
|
20609
21019
|
"Any call of the app's engine, with its own parameters",
|
|
@@ -20649,6 +21059,14 @@ function help() {
|
|
|
20649
21059
|
"2 usage, 3 not found, 4 timed out, 5 needs --force/--yes/--confirm-real. See packages/cli/README.md."
|
|
20650
21060
|
].join("\n");
|
|
20651
21061
|
}
|
|
21062
|
+
/** What `ghostly help help` answers. */
|
|
21063
|
+
var HELP_USAGE = [
|
|
21064
|
+
"Usage: ghostly help [<command>...]",
|
|
21065
|
+
"",
|
|
21066
|
+
" Help for every command, or for one command or a group of them (also ghostly <command> --help)",
|
|
21067
|
+
"",
|
|
21068
|
+
"Options for every command: --profile, -p <name> --home <dir> --pretty --help, -h"
|
|
21069
|
+
].join("\n");
|
|
20652
21070
|
/** Every command's help row: usage, summary and, for the table and text commands, their options. */
|
|
20653
21071
|
function helpRows() {
|
|
20654
21072
|
return [
|
|
@@ -20675,6 +21093,7 @@ function commandWords(usage) {
|
|
|
20675
21093
|
* options; for a group (`ghostly help file`), the group's commands.
|
|
20676
21094
|
*/
|
|
20677
21095
|
function commandHelp(words) {
|
|
21096
|
+
if (words[0] === "help") return HELP_USAGE;
|
|
20678
21097
|
const named = (list) => helpRows().filter((row) => {
|
|
20679
21098
|
const own = commandWords(row.usage);
|
|
20680
21099
|
return list.every((word, i) => own[i] === word);
|
|
@@ -20954,32 +21373,42 @@ async function daemonCommand(argv) {
|
|
|
20954
21373
|
}
|
|
20955
21374
|
if (sub === "stop") {
|
|
20956
21375
|
knownProfile(g);
|
|
21376
|
+
const stopFor = seconds(parsed, 20);
|
|
20957
21377
|
const client = await connectDaemon(g.paths.socket);
|
|
20958
21378
|
if (!client) throw new CliError("not_found", `No daemon runs profile ${g.profile}`);
|
|
20959
21379
|
print({
|
|
20960
21380
|
stopped: true,
|
|
20961
|
-
pid: await stopDaemon(g, client,
|
|
21381
|
+
pid: await stopDaemon(g, client, stopFor)
|
|
20962
21382
|
});
|
|
20963
21383
|
return;
|
|
20964
21384
|
}
|
|
20965
21385
|
if (sub === "restart") {
|
|
21386
|
+
const stopFor = seconds(parsed, 20), startFor = seconds(parsed, 60);
|
|
20966
21387
|
const client = await connectDaemon(g.paths.socket);
|
|
20967
21388
|
const before = client ? (await client.call("status").catch(() => null))?.version ?? null : null;
|
|
20968
21389
|
print({
|
|
20969
21390
|
restarted: true,
|
|
20970
|
-
stopped: client ? await stopDaemon(g, client,
|
|
21391
|
+
stopped: client ? await stopDaemon(g, client, stopFor) : null,
|
|
20971
21392
|
before,
|
|
20972
21393
|
version: VERSION,
|
|
20973
|
-
...await startDetached(g,
|
|
21394
|
+
...await startDetached(g, startFor)
|
|
20974
21395
|
});
|
|
20975
21396
|
return;
|
|
20976
21397
|
}
|
|
20977
21398
|
if (sub) throw new CliError("usage", "ghostly daemon [--detach] | daemon status | daemon stop | daemon restart");
|
|
21399
|
+
const startFor = seconds(parsed, 60);
|
|
20978
21400
|
if (!parsed.options.detach) {
|
|
20979
21401
|
await runDaemon(g);
|
|
20980
21402
|
return;
|
|
20981
21403
|
}
|
|
20982
|
-
print(await startDetached(g,
|
|
21404
|
+
print(await startDetached(g, startFor));
|
|
21405
|
+
}
|
|
21406
|
+
/** `--timeout <s>`, or `fallback` without one: seconds above 0 (0 or less would not wait at all). */
|
|
21407
|
+
function seconds(parsed, fallback) {
|
|
21408
|
+
const value = parsed.options.timeout;
|
|
21409
|
+
if (value === void 0) return fallback;
|
|
21410
|
+
if (typeof value !== "number" || !(value > 0)) throw new CliError("usage", "--timeout takes a number of seconds above 0");
|
|
21411
|
+
return value;
|
|
20983
21412
|
}
|
|
20984
21413
|
/**
|
|
20985
21414
|
* Asks the daemon to stop and waits until its process is gone, not only until it let go of the profile (it releases
|
|
@@ -21039,6 +21468,10 @@ async function startDetached(g, seconds) {
|
|
|
21039
21468
|
}
|
|
21040
21469
|
if (!client) {
|
|
21041
21470
|
heldBy(g, child.pid);
|
|
21471
|
+
if (child.exitCode === null && child.pid !== void 0 && processAlive(child.pid)) throw new CliError("timeout", `The daemon (process ${child.pid}) is still starting; ghostly daemon status shows when it is up`, {
|
|
21472
|
+
pid: child.pid,
|
|
21473
|
+
log: g.paths.log
|
|
21474
|
+
});
|
|
21042
21475
|
throw new CliError("engine", `The daemon did not start; see ${g.paths.log}`);
|
|
21043
21476
|
}
|
|
21044
21477
|
client.close();
|
|
@@ -21089,6 +21522,7 @@ async function listenCommand(argv) {
|
|
|
21089
21522
|
description: ""
|
|
21090
21523
|
}
|
|
21091
21524
|
});
|
|
21525
|
+
noMoreThan(0, parsed.positionals, SPECIAL.find(([usage]) => usage.startsWith("listen "))[0]);
|
|
21092
21526
|
const g = globals(parsed);
|
|
21093
21527
|
pretty = false;
|
|
21094
21528
|
if (parsed.options.webhook) checkWebhook(String(parsed.options.webhook));
|
|
@@ -21097,6 +21531,7 @@ async function listenCommand(argv) {
|
|
|
21097
21531
|
if (turns && types.length) throw new CliError("usage", "--turns takes the place of --type: a turn is a message received, or a group message that mentions you");
|
|
21098
21532
|
const from = parsed.options.from ?? [], groups = parsed.options.group ?? [];
|
|
21099
21533
|
const cursor = parsed.options.cursor;
|
|
21534
|
+
checkCursor(cursor);
|
|
21100
21535
|
let since = parsed.options.since ?? readCursor(cursor);
|
|
21101
21536
|
let handle = null;
|
|
21102
21537
|
const start = async (call, peerOf) => {
|
|
@@ -21226,17 +21661,7 @@ async function engineCommand(argv) {
|
|
|
21226
21661
|
params,
|
|
21227
21662
|
confirmReal
|
|
21228
21663
|
}));
|
|
21229
|
-
if (method === "getState" && !showSecret && result && typeof result === "object")
|
|
21230
|
-
const state = result;
|
|
21231
|
-
result = {
|
|
21232
|
-
...state,
|
|
21233
|
-
settings: redactSettings(state.settings),
|
|
21234
|
-
...state.groups ? { groups: state.groups.map((g) => g.entryLink ? {
|
|
21235
|
-
...g,
|
|
21236
|
-
entryLink: "<hidden>"
|
|
21237
|
-
} : g) } : {}
|
|
21238
|
-
};
|
|
21239
|
-
}
|
|
21664
|
+
if (method === "getState" && !showSecret && result && typeof result === "object") result = hideStateSecrets(result);
|
|
21240
21665
|
print(result);
|
|
21241
21666
|
}
|
|
21242
21667
|
async function settingsCommand(sub, argv) {
|
|
@@ -21469,6 +21894,7 @@ async function main(input) {
|
|
|
21469
21894
|
return 0;
|
|
21470
21895
|
}
|
|
21471
21896
|
if (argv[0] === "--version" || argv[0] === "version") {
|
|
21897
|
+
pretty = argv.slice(1).some((word) => word === "--pretty" || word === "--pretty=true");
|
|
21472
21898
|
print({ version: VERSION });
|
|
21473
21899
|
return 0;
|
|
21474
21900
|
}
|
|
@@ -21564,7 +21990,29 @@ async function run(argv) {
|
|
|
21564
21990
|
}
|
|
21565
21991
|
}
|
|
21566
21992
|
//#endregion
|
|
21993
|
+
//#region src/runtime/webStorage.ts
|
|
21994
|
+
/**
|
|
21995
|
+
* Node 25 and newer define `globalThis.localStorage` as a getter that, without `--localstorage-file`, answers
|
|
21996
|
+
* `undefined` and writes an ExperimentalWarning on stderr the first time it is read. The engine reads it where a
|
|
21997
|
+
* browser keeps a setting (`globalThis.localStorage?.getItem(...)`), so every command that opens a profile printed the
|
|
21998
|
+
* warning. Without the flag the CLI has no localStorage, as on Node 22: the getter is replaced by `undefined` before
|
|
21999
|
+
* anything reads it. With the flag, Node's storage stays.
|
|
22000
|
+
*/
|
|
22001
|
+
function quietWebStorage(scope = globalThis, execArgv = process.execArgv, nodeOptions = process.env.NODE_OPTIONS) {
|
|
22002
|
+
const own = Object.getOwnPropertyDescriptor(scope, "localStorage");
|
|
22003
|
+
if (!own?.get || !own.configurable) return;
|
|
22004
|
+
const flag = (arg) => arg === "--localstorage-file" || arg.startsWith("--localstorage-file=");
|
|
22005
|
+
if (execArgv.some(flag) || (nodeOptions ?? "").split(/\s+/).some(flag)) return;
|
|
22006
|
+
Object.defineProperty(scope, "localStorage", {
|
|
22007
|
+
value: void 0,
|
|
22008
|
+
writable: true,
|
|
22009
|
+
configurable: true,
|
|
22010
|
+
enumerable: false
|
|
22011
|
+
});
|
|
22012
|
+
}
|
|
22013
|
+
//#endregion
|
|
21567
22014
|
//#region src/bin.ts
|
|
22015
|
+
quietWebStorage();
|
|
21568
22016
|
run(process.argv.slice(2));
|
|
21569
22017
|
//#endregion
|
|
21570
22018
|
export { BACKUP_MEDIA_TYPE as $, requireTurn as A, paymentWireId as At, networkLabel as B, breezDatabasesFor as C, storedGroupFiles as Ct, sourceKey as D, replyRef as Dt, ProviderSources as E, receivedPairedReply as Et, sealSeed as F, streamStored as Ft, BITCOIN_INVOICE_ON_TESTNET as G, cashuMint as H, unsealSeed as I, fakesLightning as J, DEFAULT_MINTS as K, ModeChanged as L, setSingleWriterGate as M, readStored as Mt, intentRepository as N, removeStored as Nt, awayFrom as O, replyTo as Ot, newDeviceKey as P, storedSize as Pt, paysItsOwnInvoices as Q, ModeGate as R, breezDatabaseInUse as S, groupOfFile as St, lightningPaying as T, pairedWireReply as Tt, MESSAGE_DETAILS_MAX_BYTES as U, CASHU_MINT_SOURCE as V, WALLET_TYPES as W, isWorthlessMint as X, isTestMint as Y, mintNetwork as Z, LightningCards as _, verifyIdentity as _t, irohRelayProblem as a, reserveAdapterIds as at, normalizePhrase as b, groupFileId as bt, createFailure as c, SourceConfigError as ct, paymentNetwork as d, networkMode as dt, MAX_BACKUP_BYTES as et, paymentNetworksOf as f, redact as ft, CASHU_CARD as g, boundedIdentityFetch as gt, rpcHost as h, setOwnDidSource as ht, createIrohWebEndpoint as i, registeredOnchainProviders as it, setAwayWallets as j, FileAppender as jt, refuseAway as k, paymentAlias as kt, createTiming as l, SourceUnreachableError as lt, networkReason as m, resolveAtprotoDid as mt, loadFedimintSdk as n, onAdaptersChanged as nt, WALLET_NAMES as o, NothingSpentError as ot, walletInstances as p, assertPublicServer as pt, TEST_MINT as q, DEFAULT_IROH_RELAYS as r, registeredLightningProviders as rt, assertConfirmedReal as s, PROVIDER_PLATFORMS as st, fedimintDatabase as t, identityProviders as tt, crossNetwork as u, isNothingSpentError as ut, cardsKey as v, chosenResolver as vt, dropBreezDatabase as w, groupReply as wt, breezDatabase as x, groupFileText as xt, isRecoveryPhrase as y, GroupFileDesk as yt, WrongNetworkError as z };
|