@ghostlytools/cli 1.2.2 → 1.2.4
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 +2 -1
- package/dist/assets/{arkade-_gS9wShF.js → arkade-CdnKGDbg.js} +2 -2
- package/dist/assets/{arkade-_gS9wShF.js.map → arkade-CdnKGDbg.js.map} +1 -1
- package/dist/assets/bark_ffi_wasm_bg-EY2uc0Bx.js.map +1 -1
- package/dist/assets/bitcoindevkit_bg-B-iLWrmX.js.map +1 -1
- package/dist/assets/fedimint_client_wasm_bg-BtHHlFpp.js.map +1 -1
- package/dist/assets/{hyperdhtRelay-CgYLaQz3.js → hyperdhtRelay-C1p0nkle.js} +3 -3
- package/dist/assets/{hyperdhtRelay-CgYLaQz3.js.map → hyperdhtRelay-C1p0nkle.js.map} +1 -1
- package/dist/assets/{mainline-1v8OTYvp.js → mainline-BH6s8HVi.js} +5 -2
- package/dist/assets/{mainline-1v8OTYvp.js.map → mainline-BH6s8HVi.js.map} +1 -1
- package/dist/assets/{server-Cy4ox7oD.js → server-0zfNpABQ.js} +1055 -185
- package/dist/assets/server-0zfNpABQ.js.map +1 -0
- package/dist/assets/{src-XzcHzneo.js → src-CbLkHfz8.js} +454 -80
- package/dist/assets/src-CbLkHfz8.js.map +1 -0
- package/dist/assets/{usdt-Deo3g0Nq.js → usdt-Qf4UuygS.js} +2 -2
- package/dist/assets/{usdt-Deo3g0Nq.js.map → usdt-Qf4UuygS.js.map} +1 -1
- package/dist/ghostly.mjs +362 -114
- package/dist/ghostly.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/assets/server-Cy4ox7oD.js.map +0 -1
- package/dist/assets/src-XzcHzneo.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-CbLkHfz8.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.4",
|
|
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",
|
|
@@ -795,6 +795,14 @@ var SECRET_RESULTS = /* @__PURE__ */ new Set([
|
|
|
795
795
|
//#region src/apiKit.ts
|
|
796
796
|
var node = (ctx) => ctx.runtime.server.node;
|
|
797
797
|
var state = (ctx) => node(ctx).getState();
|
|
798
|
+
/**
|
|
799
|
+
* The time of a send the engine names its row after (`me_<timestamp>`: a file, a payment, a request), its own even
|
|
800
|
+
* within one millisecond (a bot paying or sending several times at once over the socket, `file send` run in
|
|
801
|
+
* parallel), or the chat keeps only one. One clock for all of them: they share the row names. A file's resend after a
|
|
802
|
+
* drop finds its row by it, as a forward does.
|
|
803
|
+
*/
|
|
804
|
+
var lastSentAt = 0;
|
|
805
|
+
var sentAt = () => lastSentAt = Math.max(Date.now(), lastSentAt + 1);
|
|
798
806
|
function str(params, name, required = false) {
|
|
799
807
|
const value = params[name];
|
|
800
808
|
if (value === void 0 || value === null || value === "") {
|
|
@@ -804,10 +812,12 @@ function str(params, name, required = false) {
|
|
|
804
812
|
if (typeof value !== "string") throw new CliError("bad_request", `${name} must be a string`);
|
|
805
813
|
return value;
|
|
806
814
|
}
|
|
807
|
-
|
|
815
|
+
/** A number in `[min, max]`; `whole` takes only whole ones (a count of messages), as the engine needs them. */
|
|
816
|
+
function num(params, name, fallback, { min = 0, max = Number.MAX_SAFE_INTEGER, whole = false } = {}) {
|
|
808
817
|
const value = params[name];
|
|
809
818
|
if (value === void 0 || value === null) return fallback;
|
|
810
|
-
if (typeof value !== "number" || !Number.isFinite(value) || value < min || value > max) throw new CliError("bad_request", `${name} must be a number from ${min} to ${max}`);
|
|
819
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < min || value > max) throw new CliError("bad_request", `${name} must be a ${whole ? "whole " : ""}number from ${min} to ${max}`);
|
|
820
|
+
if (whole && !Number.isSafeInteger(value)) throw new CliError("bad_request", `${name} must be a whole number from ${min} to ${max}`);
|
|
811
821
|
return value;
|
|
812
822
|
}
|
|
813
823
|
function bool(params, name) {
|
|
@@ -1323,7 +1333,7 @@ var GroupFileDesk = class {
|
|
|
1323
1333
|
}
|
|
1324
1334
|
/**
|
|
1325
1335
|
* An error when this group took as many announcements of mine as it takes in a minute, or as its members take from
|
|
1326
|
-
* one member in `announceWindowMs` (past that they would
|
|
1336
|
+
* one member in `announceWindowMs` (past that they would not fetch the file by themselves); null when one may go now.
|
|
1327
1337
|
*/
|
|
1328
1338
|
mayAnnounce(groupId) {
|
|
1329
1339
|
const wait = Math.max(...this.sentPaces(groupId).map((pace) => pace.wait()));
|
|
@@ -1340,9 +1350,9 @@ var GroupFileDesk = class {
|
|
|
1340
1350
|
this.own.delete(`${groupId}\n${meta.d}`);
|
|
1341
1351
|
}
|
|
1342
1352
|
/**
|
|
1343
|
-
* A message of the group announces a file (mine, or a member's): the file its row shows
|
|
1344
|
-
*
|
|
1345
|
-
*
|
|
1353
|
+
* A message of the group announces a file (mine, or a member's): the file its row shows. A member's is kept here
|
|
1354
|
+
* unfetched, and fetched now when it is one this device takes by itself and the member announced no more than the
|
|
1355
|
+
* group fetches that way. The same message again (a catch-up, a whole copy) is the same file.
|
|
1346
1356
|
*/
|
|
1347
1357
|
async announced(groupId, m, me) {
|
|
1348
1358
|
const known = this.byMessage.get(`${groupId}\n${m.id}`);
|
|
@@ -1384,9 +1394,9 @@ var GroupFileDesk = class {
|
|
|
1384
1394
|
this.queueHave(kept);
|
|
1385
1395
|
return messageFile(kept);
|
|
1386
1396
|
}
|
|
1387
|
-
|
|
1397
|
+
const paced = this.pace(this.takenPace, `${groupId}\n${m.sender}`, GROUP_FILE_LIMITS.announceTaken, GROUP_FILE_LIMITS.announceWindowMs).take();
|
|
1388
1398
|
const id = groupFileId(groupId, "in");
|
|
1389
|
-
const auto = this.deps.settings().autoDownloads && fetchesByItself(m.file, this.keptAutomatically(groupId));
|
|
1399
|
+
const auto = paced && this.deps.settings().autoDownloads && fetchesByItself(m.file, this.keptAutomatically(groupId));
|
|
1390
1400
|
const holders = this.ahead.get(`${groupId}\n${m.id}`) ?? [];
|
|
1391
1401
|
this.ahead.delete(`${groupId}\n${m.id}`);
|
|
1392
1402
|
const kept = {
|
|
@@ -2700,7 +2710,7 @@ var FILE_METHODS = {
|
|
|
2700
2710
|
throw new CliError("not_found", `No file ${path}`);
|
|
2701
2711
|
}
|
|
2702
2712
|
if (!info.isFile()) throw new CliError("bad_request", `${path} is not a file`);
|
|
2703
|
-
await access(path, constants.R_OK);
|
|
2713
|
+
await access(path, constants$1.R_OK);
|
|
2704
2714
|
if ("group" in target) return sendToGroup(ctx, params, target.group, path, info.size);
|
|
2705
2715
|
const { link } = target;
|
|
2706
2716
|
if (link.profile && link.dataLink === "open" && !link.capabilities?.files) throw new CliError("unavailable", "The contact's app takes no files");
|
|
@@ -2723,7 +2733,7 @@ var FILE_METHODS = {
|
|
|
2723
2733
|
...voice ? { voice } : {},
|
|
2724
2734
|
...image ? { image } : {}
|
|
2725
2735
|
};
|
|
2726
|
-
const timestamp =
|
|
2736
|
+
const timestamp = sentAt();
|
|
2727
2737
|
const bytes = await fileBytes();
|
|
2728
2738
|
const digest = await bytes.stage(file.id, source);
|
|
2729
2739
|
await fileStore.put({
|
|
@@ -6172,7 +6182,8 @@ async function verifyIdToken(token, options) {
|
|
|
6172
6182
|
}
|
|
6173
6183
|
//#endregion
|
|
6174
6184
|
//#region ../browser/src/proofs/oidc/flow.ts
|
|
6175
|
-
|
|
6185
|
+
/** Optional platform prefix (desktop `d.<port>.`, Android deep link `a.`), then 32 random bytes. */
|
|
6186
|
+
var STATE = /^(?:d\.\d{1,5}\.|a\.)?[A-Za-z0-9_-]{43}$/;
|
|
6176
6187
|
function scopeFor$1(provider, reveal) {
|
|
6177
6188
|
const scopes = provider.id === "apple" ? [] : ["openid"];
|
|
6178
6189
|
const claims = [];
|
|
@@ -8727,13 +8738,15 @@ var IDENTITY_METHODS = {
|
|
|
8727
8738
|
},
|
|
8728
8739
|
async "identity.cancel"(ctx, params) {
|
|
8729
8740
|
const draftId = str(params, "draft", true);
|
|
8730
|
-
drafts.delete(draftId);
|
|
8741
|
+
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");
|
|
8731
8742
|
await node(ctx).cancelIdentityProof({ draftId });
|
|
8732
8743
|
return { cancelled: draftId };
|
|
8733
8744
|
},
|
|
8734
8745
|
async "identity.remove"(ctx, params) {
|
|
8735
|
-
|
|
8736
|
-
|
|
8746
|
+
const id = str(params, "id", true);
|
|
8747
|
+
if (!state(ctx).identityProofs.some((p) => p.id === id)) throw new CliError("not_found", `No identity proof ${JSON.stringify(id)} (ghostly identity list)`);
|
|
8748
|
+
await node(ctx).removeIdentityProof({ id });
|
|
8749
|
+
return { removed: id };
|
|
8737
8750
|
},
|
|
8738
8751
|
async "identity.share"(ctx, params) {
|
|
8739
8752
|
const link = chatOf(ctx, params);
|
|
@@ -8748,13 +8761,15 @@ var IDENTITY_METHODS = {
|
|
|
8748
8761
|
},
|
|
8749
8762
|
async "identity.withdraw"(ctx, params) {
|
|
8750
8763
|
const link = chatOf(ctx, params);
|
|
8764
|
+
const id = str(params, "id", true);
|
|
8765
|
+
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})`);
|
|
8751
8766
|
await node(ctx).withdrawIdentityProof({
|
|
8752
8767
|
linkId: link.id,
|
|
8753
|
-
id
|
|
8768
|
+
id
|
|
8754
8769
|
});
|
|
8755
8770
|
return {
|
|
8756
8771
|
chat: link.id,
|
|
8757
|
-
withdrawn:
|
|
8772
|
+
withdrawn: id
|
|
8758
8773
|
};
|
|
8759
8774
|
},
|
|
8760
8775
|
/** What a contact shared in a chat, as this device checked it, and what this side shares there. */
|
|
@@ -8990,6 +9005,15 @@ async function proxy(request, response, chunks, send, cookies) {
|
|
|
8990
9005
|
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)}`);
|
|
8991
9006
|
}
|
|
8992
9007
|
}
|
|
9008
|
+
/**
|
|
9009
|
+
* The service a command names, by its id: one of this profile's. The engine changes nothing for an id it does not
|
|
9010
|
+
* know and says nothing, so a mistyped id read as done ("shared": true, with a contact).
|
|
9011
|
+
*/
|
|
9012
|
+
function serviceOf(ctx, params) {
|
|
9013
|
+
const serviceId = str(params, "service", true);
|
|
9014
|
+
if (!state(ctx).services.some((s) => s.id === serviceId)) throw new CliError("not_found", `No service ${JSON.stringify(serviceId)} (ghostly service list)`);
|
|
9015
|
+
return serviceId;
|
|
9016
|
+
}
|
|
8993
9017
|
var SERVICE_METHODS = {
|
|
8994
9018
|
async "service.list"(ctx) {
|
|
8995
9019
|
return { services: state(ctx).services.map((s) => ({
|
|
@@ -9016,29 +9040,32 @@ var SERVICE_METHODS = {
|
|
|
9016
9040
|
});
|
|
9017
9041
|
},
|
|
9018
9042
|
async "service.remove"(ctx, params) {
|
|
9019
|
-
|
|
9020
|
-
|
|
9043
|
+
const serviceId = serviceOf(ctx, params);
|
|
9044
|
+
await node(ctx).removeService({ serviceId });
|
|
9045
|
+
return { removed: serviceId };
|
|
9021
9046
|
},
|
|
9022
9047
|
async "service.enable"(ctx, params) {
|
|
9048
|
+
const serviceId = serviceOf(ctx, params);
|
|
9023
9049
|
await node(ctx).setServiceEnabled({
|
|
9024
|
-
serviceId
|
|
9050
|
+
serviceId,
|
|
9025
9051
|
enabled: !bool(params, "off")
|
|
9026
9052
|
});
|
|
9027
9053
|
return {
|
|
9028
|
-
service:
|
|
9054
|
+
service: serviceId,
|
|
9029
9055
|
enabled: !bool(params, "off")
|
|
9030
9056
|
};
|
|
9031
9057
|
},
|
|
9032
9058
|
async "service.share"(ctx, params) {
|
|
9033
9059
|
const link = chatOf(ctx, params);
|
|
9034
9060
|
const shared = !bool(params, "off");
|
|
9061
|
+
const serviceId = serviceOf(ctx, params);
|
|
9035
9062
|
await node(ctx).setServiceShared({
|
|
9036
|
-
serviceId
|
|
9063
|
+
serviceId,
|
|
9037
9064
|
peerPubKeyZ32: link.peerPubKeyZ32,
|
|
9038
9065
|
shared
|
|
9039
9066
|
});
|
|
9040
9067
|
return {
|
|
9041
|
-
service:
|
|
9068
|
+
service: serviceId,
|
|
9042
9069
|
chat: link.id,
|
|
9043
9070
|
shared
|
|
9044
9071
|
};
|
|
@@ -9395,10 +9422,6 @@ async function readBackupHeader(source) {
|
|
|
9395
9422
|
line: start.slice(0, end + 1)
|
|
9396
9423
|
};
|
|
9397
9424
|
}
|
|
9398
|
-
/** How a backup is protected, read from its clear header without opening it: version 1 is always sealed. */
|
|
9399
|
-
async function backupProtection(source) {
|
|
9400
|
-
return (await readBackupHeader(source))?.header.protection ?? "passphrase";
|
|
9401
|
-
}
|
|
9402
9425
|
var DAMAGED = "This backup is damaged: it was changed or cut short";
|
|
9403
9426
|
/** Reads a version 2 backup record by record. Nothing of a frame is handed out before it passed its check. */
|
|
9404
9427
|
var BackupReader = class BackupReader {
|
|
@@ -9519,15 +9542,21 @@ var keptInLight = (size, voice) => size <= (voice ? LIGHT_VOICE_BYTES : LIGHT_FI
|
|
|
9519
9542
|
*/
|
|
9520
9543
|
var DEFAULT_PROFILE = "default";
|
|
9521
9544
|
var NAME = /^[a-z0-9][a-z0-9._-]{0,63}$/i;
|
|
9545
|
+
/**
|
|
9546
|
+
* An empty `--home` or `--profile` (an unset variable in a script) is a usage error: taken as not given, it ran the
|
|
9547
|
+
* command in the current folder, or as the current profile. An empty variable of the environment counts as unset.
|
|
9548
|
+
*/
|
|
9522
9549
|
function ghostlyHome(option) {
|
|
9523
|
-
|
|
9550
|
+
if (option === "") throw new CliError("usage", "--home needs a folder");
|
|
9551
|
+
return resolve(option ?? (process.env.GHOSTLY_HOME || join(homedir(), ".ghostly")));
|
|
9524
9552
|
}
|
|
9525
9553
|
function checkProfileName(name) {
|
|
9526
9554
|
if (!NAME.test(name) || name.includes("..")) throw new CliError("usage", `Not a profile name: ${JSON.stringify(name)} (letters, digits, ".", "_" and "-", up to 64)`);
|
|
9527
9555
|
return name;
|
|
9528
9556
|
}
|
|
9529
9557
|
function currentProfile(home, option) {
|
|
9530
|
-
if (option)
|
|
9558
|
+
if (option === "") throw new CliError("usage", "--profile needs a profile name");
|
|
9559
|
+
if (option !== void 0) return checkProfileName(option);
|
|
9531
9560
|
if (process.env.GHOSTLY_PROFILE) return checkProfileName(process.env.GHOSTLY_PROFILE);
|
|
9532
9561
|
const file = join(home, "current");
|
|
9533
9562
|
if (existsSync(file)) {
|
|
@@ -9804,11 +9833,24 @@ async function fileSource(path) {
|
|
|
9804
9833
|
close: () => handle.close()
|
|
9805
9834
|
};
|
|
9806
9835
|
}
|
|
9807
|
-
/**
|
|
9836
|
+
/** A version 1 backup is one JSON object, and `seal` writes its format first. */
|
|
9837
|
+
var VERSION_1_START = /^\s*\{\s*"format"\s*:\s*"ghostly-backup"/;
|
|
9838
|
+
/**
|
|
9839
|
+
* Whether the backup at `file` is sealed with a passphrase or was made without one, read from its clear header.
|
|
9840
|
+
* A file that is neither version is refused here, before any passphrase is asked for: the path is what is wrong.
|
|
9841
|
+
*/
|
|
9808
9842
|
async function backupFileProtection(file) {
|
|
9809
9843
|
const source = await fileSource(file);
|
|
9810
9844
|
try {
|
|
9811
|
-
|
|
9845
|
+
let found;
|
|
9846
|
+
try {
|
|
9847
|
+
found = await readBackupHeader(source);
|
|
9848
|
+
} catch (error) {
|
|
9849
|
+
throw new CliError("refused", error instanceof Error ? error.message : String(error));
|
|
9850
|
+
}
|
|
9851
|
+
if (found) return found.header.protection;
|
|
9852
|
+
if (!VERSION_1_START.test(new TextDecoder().decode(await source.read(0, 256)))) throw new CliError("refused", "This is not a Ghostly backup");
|
|
9853
|
+
return "passphrase";
|
|
9812
9854
|
} finally {
|
|
9813
9855
|
await source.close();
|
|
9814
9856
|
}
|
|
@@ -12312,6 +12354,15 @@ function paymentJson(p) {
|
|
|
12312
12354
|
}
|
|
12313
12355
|
var isInvoice = (text) => /^(lightning:)?ln(bc|tb|tbs|sb|bcrt)[0-9a-z]+$/i.test(text.trim());
|
|
12314
12356
|
var isLnurl = (text) => /^(lightning:)?lnurl[0-9a-z]+$/i.test(text.trim()) || /^[a-z0-9._+-]+@[a-z0-9.-]+\.[a-z]{2,}$/i.test(text.trim());
|
|
12357
|
+
/**
|
|
12358
|
+
* The engine refusing the mint URL the person typed (not a URL, or not https): their value, so `bad_request`, as
|
|
12359
|
+
* `service add` says; before, it was `engine` (exit 1), which a bot reads as the engine failing and may retry. Read by
|
|
12360
|
+
* its shape: the engine's `EngineError` carries `engineCode`.
|
|
12361
|
+
*/
|
|
12362
|
+
function badMintUrl(error) {
|
|
12363
|
+
const code = error instanceof Error ? error.engineCode : void 0;
|
|
12364
|
+
return code === "invalidMintUrl" || code === "mintNotHttps";
|
|
12365
|
+
}
|
|
12315
12366
|
var WALLET_METHODS = {
|
|
12316
12367
|
async "wallet.list"(ctx, params) {
|
|
12317
12368
|
await federationsOpen(ctx);
|
|
@@ -12398,7 +12449,8 @@ var WALLET_METHODS = {
|
|
|
12398
12449
|
const n = network(params);
|
|
12399
12450
|
const limit = num(params, "limit", 50, {
|
|
12400
12451
|
min: 1,
|
|
12401
|
-
max: 1e4
|
|
12452
|
+
max: 1e4,
|
|
12453
|
+
whole: true
|
|
12402
12454
|
});
|
|
12403
12455
|
await federationsOpen(ctx);
|
|
12404
12456
|
return {
|
|
@@ -12454,6 +12506,8 @@ var WALLET_METHODS = {
|
|
|
12454
12506
|
return node(ctx).walletAddMint({
|
|
12455
12507
|
url: str(params, "url", true),
|
|
12456
12508
|
...bool(params, "primary") ? { primary: true } : {}
|
|
12509
|
+
}).catch((error) => {
|
|
12510
|
+
throw badMintUrl(error) ? new CliError("bad_request", error.message) : error;
|
|
12457
12511
|
});
|
|
12458
12512
|
},
|
|
12459
12513
|
async "lightning.default"(ctx, params) {
|
|
@@ -12538,7 +12592,7 @@ var WALLET_METHODS = {
|
|
|
12538
12592
|
const { paymentId } = await node(ctx).sendPayment({
|
|
12539
12593
|
linkId: link.id,
|
|
12540
12594
|
amount,
|
|
12541
|
-
timestamp:
|
|
12595
|
+
timestamp: sentAt(),
|
|
12542
12596
|
network: n,
|
|
12543
12597
|
...str(params, "memo") ? { memo: str(params, "memo") } : {},
|
|
12544
12598
|
...real(params)
|
|
@@ -12559,7 +12613,7 @@ var WALLET_METHODS = {
|
|
|
12559
12613
|
const { paymentId } = await node(ctx).requestPayment({
|
|
12560
12614
|
linkId: link.id,
|
|
12561
12615
|
amount,
|
|
12562
|
-
timestamp:
|
|
12616
|
+
timestamp: sentAt(),
|
|
12563
12617
|
network: network(params),
|
|
12564
12618
|
...str(params, "memo") ? { memo: str(params, "memo") } : {},
|
|
12565
12619
|
...method ? { method } : {},
|
|
@@ -12746,6 +12800,33 @@ function mentionsFor(text, refs, group) {
|
|
|
12746
12800
|
}
|
|
12747
12801
|
return out;
|
|
12748
12802
|
}
|
|
12803
|
+
/** Per host: stops watching the invite it marked as the chat on screen. */
|
|
12804
|
+
var inviteShown = /* @__PURE__ */ new WeakMap();
|
|
12805
|
+
/**
|
|
12806
|
+
* Marks a new invite as the chat on screen (native listeners, the quick pace) only until it is paired: a paired chat
|
|
12807
|
+
* keeps its listeners anyway, and a daemon nobody looks at reads an away contact at the background pace, as its
|
|
12808
|
+
* other chats and the same chat after a restart do.
|
|
12809
|
+
*/
|
|
12810
|
+
function showUntilPaired(ctx, linkId) {
|
|
12811
|
+
inviteShown.get(ctx)?.();
|
|
12812
|
+
node(ctx).setActiveLink({ linkId });
|
|
12813
|
+
let seen = false;
|
|
12814
|
+
const done = (s) => {
|
|
12815
|
+
const link = s.links.find((l) => l.id === linkId);
|
|
12816
|
+
if (link) seen = true;
|
|
12817
|
+
return link ? !!link.peerParticipationKey : seen;
|
|
12818
|
+
};
|
|
12819
|
+
const stop = () => {
|
|
12820
|
+
off();
|
|
12821
|
+
if (inviteShown.get(ctx) === stop) inviteShown.delete(ctx);
|
|
12822
|
+
};
|
|
12823
|
+
const off = ctx.hub.onState((s) => {
|
|
12824
|
+
if (!done(s)) return;
|
|
12825
|
+
stop();
|
|
12826
|
+
node(ctx).setActiveLink({ linkId: null });
|
|
12827
|
+
});
|
|
12828
|
+
inviteShown.set(ctx, stop);
|
|
12829
|
+
}
|
|
12749
12830
|
var METHODS = {
|
|
12750
12831
|
...WALLET_METHODS,
|
|
12751
12832
|
...FILE_METHODS,
|
|
@@ -12822,7 +12903,7 @@ var METHODS = {
|
|
|
12822
12903
|
linkId,
|
|
12823
12904
|
label
|
|
12824
12905
|
});
|
|
12825
|
-
|
|
12906
|
+
showUntilPaired(ctx, linkId);
|
|
12826
12907
|
const progressOf = (s) => s.links.find((l) => l.id === linkId)?.pairingProgress;
|
|
12827
12908
|
const progress = await waitForState(ctx, (s) => {
|
|
12828
12909
|
const p = progressOf(s);
|
|
@@ -12856,7 +12937,7 @@ var METHODS = {
|
|
|
12856
12937
|
linkId,
|
|
12857
12938
|
label
|
|
12858
12939
|
});
|
|
12859
|
-
|
|
12940
|
+
showUntilPaired(ctx, linkId);
|
|
12860
12941
|
return { chat: linkId };
|
|
12861
12942
|
},
|
|
12862
12943
|
async "chat.list"(ctx, params) {
|
|
@@ -13082,13 +13163,15 @@ var METHODS = {
|
|
|
13082
13163
|
},
|
|
13083
13164
|
async "chat.retry"(ctx, params) {
|
|
13084
13165
|
const link = chatOf(ctx, params);
|
|
13166
|
+
const messageId = str(params, "message", true);
|
|
13167
|
+
if (!await node(ctx).getMessage(link.id, messageId)) throw new CliError("not_found", `No message ${messageId} in this chat`);
|
|
13085
13168
|
await node(ctx).retryMessage({
|
|
13086
13169
|
linkId: link.id,
|
|
13087
|
-
messageId
|
|
13170
|
+
messageId
|
|
13088
13171
|
});
|
|
13089
13172
|
return {
|
|
13090
13173
|
chat: link.id,
|
|
13091
|
-
messageId
|
|
13174
|
+
messageId
|
|
13092
13175
|
};
|
|
13093
13176
|
},
|
|
13094
13177
|
async "chat.delete"(ctx, params) {
|
|
@@ -13406,6 +13489,7 @@ var METHODS = {
|
|
|
13406
13489
|
if (args && "confirmedReal" in args && !bool(params, "confirmReal")) throw new CliError("confirm", `${method} spends real money: pass --confirm-real (confirmReal: true over the socket) to confirm it`);
|
|
13407
13490
|
if (method === "getMessages") return node(ctx).getMessages(str(args ?? {}, "linkId", true));
|
|
13408
13491
|
if (method === "getState") return state(ctx);
|
|
13492
|
+
if (method === "setActiveLink") inviteShown.get(ctx)?.();
|
|
13409
13493
|
const target = node(ctx)[method];
|
|
13410
13494
|
if (typeof target !== "function") throw new CliError("not_found", `The engine has no call ${method}`);
|
|
13411
13495
|
if (args === void 0) args = void 0;
|
|
@@ -13496,7 +13580,8 @@ async function historyOf(ctx, chat, params, json = messageJson) {
|
|
|
13496
13580
|
if (params.after !== void 0 && params.after !== null) return history(await node(ctx).getMessages(chat), params, json);
|
|
13497
13581
|
const limit = num(params, "limit", 50, {
|
|
13498
13582
|
min: 1,
|
|
13499
|
-
max: 1e4
|
|
13583
|
+
max: 1e4,
|
|
13584
|
+
whole: true
|
|
13500
13585
|
});
|
|
13501
13586
|
const before = params.before ?? void 0;
|
|
13502
13587
|
if (before !== void 0 && typeof before !== "number" && typeof before !== "string") throw new CliError("bad_request", "before must be a timestamp or a message id");
|
|
@@ -13515,7 +13600,8 @@ async function historyOf(ctx, chat, params, json = messageJson) {
|
|
|
13515
13600
|
function history(messages, params, json = messageJson) {
|
|
13516
13601
|
const limit = num(params, "limit", 50, {
|
|
13517
13602
|
min: 1,
|
|
13518
|
-
max: 1e4
|
|
13603
|
+
max: 1e4,
|
|
13604
|
+
whole: true
|
|
13519
13605
|
});
|
|
13520
13606
|
const before = params.before;
|
|
13521
13607
|
const after = params.after;
|
|
@@ -14772,6 +14858,7 @@ function taskFields(options) {
|
|
|
14772
14858
|
set("title", options.title);
|
|
14773
14859
|
set("status", options.status);
|
|
14774
14860
|
percent(options, "progress");
|
|
14861
|
+
if (options.progress !== void 0 && options.steps !== void 0) throw new CliError("usage", "--progress or --steps, not both");
|
|
14775
14862
|
set("progress", options.progress);
|
|
14776
14863
|
set("step", options.step);
|
|
14777
14864
|
set("branch", options.branch);
|
|
@@ -14976,6 +15063,7 @@ function usageWindowOf(value) {
|
|
|
14976
15063
|
function usageFields(options) {
|
|
14977
15064
|
const fields = { ...cardJson(options.json) };
|
|
14978
15065
|
percent(options, "left");
|
|
15066
|
+
if (options.left !== void 0 && (options.used !== void 0 || options.limit !== void 0)) throw new CliError("usage", "--left or --used with --limit, not both");
|
|
14979
15067
|
for (const key of [
|
|
14980
15068
|
"id",
|
|
14981
15069
|
"left",
|
|
@@ -15783,6 +15871,7 @@ var commands$4 = {
|
|
|
15783
15871
|
} },
|
|
15784
15872
|
params: ({ options }, a) => {
|
|
15785
15873
|
if (a.path === void 0 && options.clear !== true) throw new CliError("usage", "Missing <jpeg>: ghostly profile picture <jpeg> | --clear");
|
|
15874
|
+
if (a.path !== void 0 && options.clear === true) throw new CliError("usage", "--clear removes the picture: not with a <jpeg>");
|
|
15786
15875
|
return {
|
|
15787
15876
|
path: here(a.path),
|
|
15788
15877
|
clear: options.clear === true
|
|
@@ -15899,12 +15988,20 @@ var commands$3 = {
|
|
|
15899
15988
|
description: "A hub if their app stays online (the default)"
|
|
15900
15989
|
}
|
|
15901
15990
|
},
|
|
15902
|
-
params: ({ options }, a) =>
|
|
15903
|
-
|
|
15904
|
-
|
|
15905
|
-
|
|
15906
|
-
|
|
15907
|
-
|
|
15991
|
+
params: ({ options }, a) => {
|
|
15992
|
+
const roles = [
|
|
15993
|
+
"pin",
|
|
15994
|
+
"exclude",
|
|
15995
|
+
"auto"
|
|
15996
|
+
].filter((role) => options[role] === true);
|
|
15997
|
+
if (roles.length > 1) throw new CliError("usage", "--pin, --exclude or --auto: one of them");
|
|
15998
|
+
return {
|
|
15999
|
+
group: a.group,
|
|
16000
|
+
member: a.member,
|
|
16001
|
+
role: roles[0] ?? "auto",
|
|
16002
|
+
...secret(options)
|
|
16003
|
+
};
|
|
16004
|
+
}
|
|
15908
16005
|
},
|
|
15909
16006
|
"group invite": {
|
|
15910
16007
|
method: "group.invite",
|
|
@@ -15947,11 +16044,14 @@ var commands$3 = {
|
|
|
15947
16044
|
description: "Replace it: the old link stops working"
|
|
15948
16045
|
}
|
|
15949
16046
|
},
|
|
15950
|
-
params: ({ options }, a) =>
|
|
15951
|
-
|
|
15952
|
-
|
|
15953
|
-
|
|
15954
|
-
|
|
16047
|
+
params: ({ options }, a) => {
|
|
16048
|
+
if (options.off === true && options.reset === true) throw new CliError("usage", "--off turns the link off: not with --reset");
|
|
16049
|
+
return {
|
|
16050
|
+
group: a.group,
|
|
16051
|
+
on: options.off !== true,
|
|
16052
|
+
reset: options.reset === true
|
|
16053
|
+
};
|
|
16054
|
+
}
|
|
15955
16055
|
},
|
|
15956
16056
|
"group list": {
|
|
15957
16057
|
method: "group.list",
|
|
@@ -15971,6 +16071,7 @@ var commands$3 = {
|
|
|
15971
16071
|
} },
|
|
15972
16072
|
params: ({ options }, a) => {
|
|
15973
16073
|
if (a.path === void 0 && options.clear !== true) throw new CliError("usage", "Missing <jpeg>: ghostly group picture <group> <jpeg> | --clear");
|
|
16074
|
+
if (a.path !== void 0 && options.clear === true) throw new CliError("usage", "--clear removes the picture: not with a <jpeg>");
|
|
15974
16075
|
return {
|
|
15975
16076
|
group: a.group,
|
|
15976
16077
|
path: here(a.path),
|
|
@@ -17350,6 +17451,14 @@ var refuseAnswers = Number(process.env.GHOSTLY_CALL_REFUSE_ANSWERS) || 0;
|
|
|
17350
17451
|
var GATHER_MS = 5e3;
|
|
17351
17452
|
var SETTLE_MS = 400;
|
|
17352
17453
|
/**
|
|
17454
|
+
* With a candidate in (a host one comes in milliseconds), the server reflexive one is waited for this long at most,
|
|
17455
|
+
* not the whole of `GATHER_MS`: a STUN server that answers does in tens of milliseconds, and one that never does (UDP
|
|
17456
|
+
* to it blocked, as on many office, school and hotel networks) made every call ring 5 s late and connect 5 s after
|
|
17457
|
+
* the answer. The apps' calls and the chats' data link bound theirs the same way (`HOST_GATHER_MS`,
|
|
17458
|
+
* `OFFER_HOST_GATHER_MS`).
|
|
17459
|
+
*/
|
|
17460
|
+
var HOST_GATHER_MS = 2e3;
|
|
17461
|
+
/**
|
|
17353
17462
|
* One call's peer connection. Its SDP never travels: the compact signal does (`local`), and each side rebuilds the
|
|
17354
17463
|
* other's SDP from it (`buildSdpFromSignal`), so the payload types, SSRC and mids here are the ones that rebuilt SDP
|
|
17355
17464
|
* uses: audio is mid 0, Opus is 111 unless the contact's offer says otherwise.
|
|
@@ -17421,7 +17530,7 @@ var CallMedia = class CallMedia {
|
|
|
17421
17530
|
const track = pc.addTrack(audioSection);
|
|
17422
17531
|
let videoTrack = null;
|
|
17423
17532
|
if (offer?.m?.includes("v")) {
|
|
17424
|
-
const video = new ndc.Video(String(offer.m.indexOf("v")), "
|
|
17533
|
+
const video = new ndc.Video(String(offer.m.indexOf("v")), "Inactive");
|
|
17425
17534
|
video.addVP8Codec(offer.vp ?? 96);
|
|
17426
17535
|
videoTrack = pc.addTrack(video);
|
|
17427
17536
|
videoTrack.onMessage(() => {});
|
|
@@ -17440,6 +17549,7 @@ var CallMedia = class CallMedia {
|
|
|
17440
17549
|
ss: [ssrc]
|
|
17441
17550
|
};
|
|
17442
17551
|
if (offer?.m) local.m = [...offer.m];
|
|
17552
|
+
if (videoTrack) local.vr = 0;
|
|
17443
17553
|
const listeners = [];
|
|
17444
17554
|
track.onMessage((packet) => {
|
|
17445
17555
|
for (const listener of listeners) listener(packet);
|
|
@@ -17520,20 +17630,26 @@ function mdnsNote(signal, log) {
|
|
|
17520
17630
|
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`));
|
|
17521
17631
|
}
|
|
17522
17632
|
}
|
|
17523
|
-
/** Resolves once the local description has its candidates (see GATHER_MS). */
|
|
17633
|
+
/** Resolves once the local description has its candidates (see GATHER_MS and HOST_GATHER_MS). */
|
|
17524
17634
|
function gathering(pc) {
|
|
17525
17635
|
return new Promise((resolve) => {
|
|
17526
17636
|
let settle = null;
|
|
17637
|
+
let any = false;
|
|
17527
17638
|
const done = () => {
|
|
17528
17639
|
clearTimeout(cap);
|
|
17640
|
+
clearTimeout(bounded);
|
|
17529
17641
|
if (settle) clearTimeout(settle);
|
|
17530
17642
|
resolve();
|
|
17531
17643
|
};
|
|
17532
17644
|
const cap = setTimeout(done, GATHER_MS);
|
|
17645
|
+
const bounded = setTimeout(() => {
|
|
17646
|
+
if (any) done();
|
|
17647
|
+
}, HOST_GATHER_MS);
|
|
17533
17648
|
pc.onGatheringStateChange((state) => {
|
|
17534
17649
|
if (state === "complete") done();
|
|
17535
17650
|
});
|
|
17536
17651
|
pc.onLocalCandidate((candidate) => {
|
|
17652
|
+
any = true;
|
|
17537
17653
|
if (!settle && / typ srflx/.test(candidate)) settle = setTimeout(done, SETTLE_MS);
|
|
17538
17654
|
});
|
|
17539
17655
|
});
|
|
@@ -18189,23 +18305,15 @@ var EventHub = class {
|
|
|
18189
18305
|
request.onsuccess = () => resolve(request.result);
|
|
18190
18306
|
request.onerror = () => reject(request.error);
|
|
18191
18307
|
});
|
|
18192
|
-
const
|
|
18193
|
-
|
|
18194
|
-
|
|
18195
|
-
request.onsuccess = () => {
|
|
18196
|
-
const cursor = request.result;
|
|
18197
|
-
if (!cursor) return resolve(out);
|
|
18198
|
-
out.push({
|
|
18199
|
-
key: cursor.key,
|
|
18200
|
-
value: cursor.value
|
|
18201
|
-
});
|
|
18202
|
-
cursor.continue();
|
|
18203
|
-
};
|
|
18308
|
+
const store = this.db.transaction(SEEN).objectStore(SEEN);
|
|
18309
|
+
const all = (request) => new Promise((resolve, reject) => {
|
|
18310
|
+
request.onsuccess = () => resolve(request.result);
|
|
18204
18311
|
request.onerror = () => reject(request.error);
|
|
18205
18312
|
});
|
|
18206
|
-
|
|
18207
|
-
|
|
18208
|
-
|
|
18313
|
+
const [keys, values] = await Promise.all([all(store.getAllKeys()), all(store.getAll())]);
|
|
18314
|
+
for (let i = 0; i < keys.length; i++) {
|
|
18315
|
+
const [chat, id] = keys[i];
|
|
18316
|
+
this.seenOf(chat).set(id, values[i]);
|
|
18209
18317
|
}
|
|
18210
18318
|
this.mentionsNoted = this.firstRun || this.seen.get(FORMAT)?.get(MENTIONS_NOTED) === "yes";
|
|
18211
18319
|
}
|
|
@@ -18691,8 +18799,13 @@ var EventHub = class {
|
|
|
18691
18799
|
}
|
|
18692
18800
|
if (changes.length) this.persist(key, changes);
|
|
18693
18801
|
}
|
|
18802
|
+
/**
|
|
18803
|
+
* Relaxed: nobody waits for it, and a note lost to a power cut is made again from the history at the next start
|
|
18804
|
+
* (its event is then reported a second time, as after a crash between a message's row and its note). A disk sync
|
|
18805
|
+
* for each one held the whole daemon for milliseconds, three times per message sent.
|
|
18806
|
+
*/
|
|
18694
18807
|
persist(chat, changes) {
|
|
18695
|
-
const store = this.db.transaction(SEEN, "readwrite").objectStore(SEEN);
|
|
18808
|
+
const store = this.db.transaction(SEEN, "readwrite", { durability: "relaxed" }).objectStore(SEEN);
|
|
18696
18809
|
for (const [id, state] of changes) if (state === null) store.delete([chat, id]);
|
|
18697
18810
|
else store.put(state, [chat, id]);
|
|
18698
18811
|
}
|
|
@@ -19067,7 +19180,7 @@ function federationInfo(federationId, config) {
|
|
|
19067
19180
|
};
|
|
19068
19181
|
}
|
|
19069
19182
|
/** The first value a subscription gives that is final, or the last one within `waitMs`. */
|
|
19070
|
-
function watch(subscribe, final, waitMs) {
|
|
19183
|
+
function watch$1(subscribe, final, waitMs) {
|
|
19071
19184
|
return new Promise((resolve) => {
|
|
19072
19185
|
let last, done = false, cancel = () => {};
|
|
19073
19186
|
const finish = () => {
|
|
@@ -19138,13 +19251,13 @@ var RealClient = class {
|
|
|
19138
19251
|
return this.wallet.mint.tryCancelSpendNotes(operationId);
|
|
19139
19252
|
}
|
|
19140
19253
|
spendState(operationId, waitMs) {
|
|
19141
|
-
return watch((ok, fail) => this.wallet.mint.subscribeSpendNotes(operationId, (s) => ok(SPEND[s] ?? "created"), fail), spendFinal, waitMs);
|
|
19254
|
+
return watch$1((ok, fail) => this.wallet.mint.subscribeSpendNotes(operationId, (s) => ok(SPEND[s] ?? "created"), fail), spendFinal, waitMs);
|
|
19142
19255
|
}
|
|
19143
19256
|
redeem(notes, ghostly) {
|
|
19144
19257
|
return this.wallet.mint.reissueExternalNotes(notes, { ghostly });
|
|
19145
19258
|
}
|
|
19146
19259
|
redeemState(operationId, waitMs) {
|
|
19147
|
-
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) => {
|
|
19260
|
+
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) => {
|
|
19148
19261
|
ok("failed");
|
|
19149
19262
|
fail(error);
|
|
19150
19263
|
}), (s) => s === "done" || s === "failed", waitMs);
|
|
@@ -19157,7 +19270,7 @@ var RealClient = class {
|
|
|
19157
19270
|
};
|
|
19158
19271
|
}
|
|
19159
19272
|
receiveState(operationId, waitMs) {
|
|
19160
|
-
return watch((ok, fail) => this.wallet.lightning.subscribeLnReceive(operationId, (s) => ok(receiveOf(s)), fail), (s) => s === "claimed" || s === "canceled", waitMs);
|
|
19273
|
+
return watch$1((ok, fail) => this.wallet.lightning.subscribeLnReceive(operationId, (s) => ok(receiveOf(s)), fail), (s) => s === "claimed" || s === "canceled", waitMs);
|
|
19161
19274
|
}
|
|
19162
19275
|
async gatewayFee(amountMsats) {
|
|
19163
19276
|
await this.wallet.lightning.updateGatewayCache().catch(() => {});
|
|
@@ -19175,7 +19288,7 @@ var RealClient = class {
|
|
|
19175
19288
|
};
|
|
19176
19289
|
}
|
|
19177
19290
|
async payState(operationId, internal, waitMs) {
|
|
19178
|
-
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);
|
|
19291
|
+
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);
|
|
19179
19292
|
if (state && typeof state === "object") {
|
|
19180
19293
|
const s = state;
|
|
19181
19294
|
if (s.success?.preimage || s.preimage) return {
|
|
@@ -19510,6 +19623,10 @@ var SNAPSHOT = "snapshot.bin";
|
|
|
19510
19623
|
var JOURNAL = "journal.bin";
|
|
19511
19624
|
/** A journal past this is folded into the snapshot at the next quiet moment. */
|
|
19512
19625
|
var COMPACT_BYTES = 33554432;
|
|
19626
|
+
/** How long `close` waits for the transactions still running: one left open for good does not keep the process. */
|
|
19627
|
+
var CLOSE_WAIT_MS = 5e3;
|
|
19628
|
+
/** How long a relaxed transaction's entry may wait for a sync when no other transaction brings one. */
|
|
19629
|
+
var RELAXED_SYNC_MS = 1e3;
|
|
19513
19630
|
var BLOB = "__ghostlyBlob";
|
|
19514
19631
|
var RANGE = "__ghostlyRange";
|
|
19515
19632
|
var PROBE = "__ghostly_storage_probe__";
|
|
@@ -19532,12 +19649,14 @@ async function openPersistentIndexedDb(dir) {
|
|
|
19532
19649
|
let queue = Promise.resolve();
|
|
19533
19650
|
let queued = 0;
|
|
19534
19651
|
let schemaDirty = false;
|
|
19652
|
+
/** Set while the journal holds relaxed entries no sync has covered yet. */
|
|
19653
|
+
let relaxed = null;
|
|
19535
19654
|
const pending = /* @__PURE__ */ new WeakMap();
|
|
19536
19655
|
const snapshotPath = join(dir, SNAPSHOT);
|
|
19537
19656
|
const journalPath = join(dir, JOURNAL);
|
|
19538
19657
|
const snapshotFound = existsSync(snapshotPath), journalFound = existsSync(journalPath);
|
|
19539
|
-
if (snapshotFound) restore(classes, raw._databases, deserialize(readFileSync(snapshotPath)), (a, b) => factory.cmp(a, b));
|
|
19540
|
-
if (journalFound) replay(raw._databases, readFileSync(journalPath));
|
|
19658
|
+
if (snapshotFound) restore(classes, raw._databases, deserialize(readFileSync(snapshotPath)), keyOrder((a, b) => factory.cmp(a, b)));
|
|
19659
|
+
if (journalFound) replay(raw._databases, readFileSync(journalPath), keyOrder((a, b) => factory.cmp(a, b)));
|
|
19541
19660
|
restoring = false;
|
|
19542
19661
|
if (journalFound || !snapshotFound) await writeSnapshot();
|
|
19543
19662
|
const storeProto = classes.ObjectStore.prototype;
|
|
@@ -19584,7 +19703,7 @@ async function openPersistentIndexedDb(dir) {
|
|
|
19584
19703
|
if (this.mode === "versionchange") schemaDirty = true;
|
|
19585
19704
|
else {
|
|
19586
19705
|
const ops = pending.get(this._rollbackLog);
|
|
19587
|
-
if (ops?.length) append(ops);
|
|
19706
|
+
if (ops?.length) append(ops, this.durability === "relaxed");
|
|
19588
19707
|
}
|
|
19589
19708
|
pending.delete(this._rollbackLog);
|
|
19590
19709
|
if (schemaDirty) enqueue(writeSnapshot);
|
|
@@ -19606,34 +19725,54 @@ async function openPersistentIndexedDb(dir) {
|
|
|
19606
19725
|
});
|
|
19607
19726
|
return request;
|
|
19608
19727
|
};
|
|
19609
|
-
function append(ops) {
|
|
19728
|
+
function append(ops, lazy) {
|
|
19610
19729
|
if (queued === 0 && !ops.some((op) => op.kind === "put" && hasBlob(op.value))) {
|
|
19611
|
-
writeEntry(serialize(ops));
|
|
19730
|
+
writeEntry(serialize(ops), lazy);
|
|
19612
19731
|
return;
|
|
19613
19732
|
}
|
|
19614
19733
|
enqueue(async () => writeEntry(serialize(await Promise.all(ops.map(async (op) => op.kind === "put" ? {
|
|
19615
19734
|
...op,
|
|
19616
19735
|
value: await blobsToDisk(op.value)
|
|
19617
|
-
} : op)))));
|
|
19736
|
+
} : op))), lazy));
|
|
19618
19737
|
}
|
|
19619
19738
|
function enqueue(work) {
|
|
19620
19739
|
queued++;
|
|
19621
|
-
queue = queue.then(work).catch((
|
|
19622
|
-
process.stderr.write(`ghostly: storage write failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
19623
|
-
}).finally(() => {
|
|
19740
|
+
queue = queue.then(work).catch(failed).finally(() => {
|
|
19624
19741
|
queued--;
|
|
19625
19742
|
});
|
|
19626
19743
|
return queue;
|
|
19627
19744
|
}
|
|
19628
|
-
function
|
|
19745
|
+
function failed(error) {
|
|
19746
|
+
process.stderr.write(`ghostly: storage write failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
19747
|
+
}
|
|
19748
|
+
/**
|
|
19749
|
+
* `lazy`: a relaxed transaction's entry. A sync blocks the whole process for milliseconds, and what such a
|
|
19750
|
+
* transaction wrote can be made again (the event set's note of a message it already reported): written, it outlives
|
|
19751
|
+
* the process; a power cut may take its last second.
|
|
19752
|
+
*/
|
|
19753
|
+
function writeEntry(entry, lazy = false) {
|
|
19629
19754
|
if (!writing) return;
|
|
19630
19755
|
journalFd ??= openSync(journalPath, "a", 384);
|
|
19631
19756
|
const header = Buffer.alloc(4);
|
|
19632
19757
|
header.writeUInt32BE(entry.length);
|
|
19633
19758
|
writeSync(journalFd, Buffer.concat([header, entry]));
|
|
19634
|
-
|
|
19759
|
+
if (lazy) relaxed ??= setTimeout(() => {
|
|
19760
|
+
try {
|
|
19761
|
+
sync();
|
|
19762
|
+
} catch (error) {
|
|
19763
|
+
failed(error);
|
|
19764
|
+
}
|
|
19765
|
+
}, RELAXED_SYNC_MS).unref();
|
|
19766
|
+
else sync();
|
|
19635
19767
|
if (statSync(journalPath).size > COMPACT_BYTES) enqueue(writeSnapshot);
|
|
19636
19768
|
}
|
|
19769
|
+
function sync() {
|
|
19770
|
+
if (relaxed) {
|
|
19771
|
+
clearTimeout(relaxed);
|
|
19772
|
+
relaxed = null;
|
|
19773
|
+
}
|
|
19774
|
+
if (journalFd !== null) fsyncSync(journalFd);
|
|
19775
|
+
}
|
|
19637
19776
|
async function writeSnapshot() {
|
|
19638
19777
|
schemaDirty = false;
|
|
19639
19778
|
const snapshot = {
|
|
@@ -19676,12 +19815,29 @@ async function openPersistentIndexedDb(dir) {
|
|
|
19676
19815
|
closeSync(fd);
|
|
19677
19816
|
}
|
|
19678
19817
|
renameSync(temporary, snapshotPath);
|
|
19818
|
+
if (relaxed) {
|
|
19819
|
+
clearTimeout(relaxed);
|
|
19820
|
+
relaxed = null;
|
|
19821
|
+
}
|
|
19679
19822
|
if (journalFd !== null) {
|
|
19680
19823
|
closeSync(journalFd);
|
|
19681
19824
|
journalFd = null;
|
|
19682
19825
|
}
|
|
19683
19826
|
rmSync(journalPath, { force: true });
|
|
19684
19827
|
}
|
|
19828
|
+
/**
|
|
19829
|
+
* Resolves once no transaction is left to run. The engine does not wait for every write (`void db.deleteLink(…)`),
|
|
19830
|
+
* and fake-indexeddb runs a transaction on a later turn of the event loop: a command that closed in the turn it wrote
|
|
19831
|
+
* in (no chat to say goodbye to) took its last snapshot before the write, and answered done for what was not on disk.
|
|
19832
|
+
* Two quiet turns in a row: a write that begins when another ends (a chat's messages after its row) is seen too.
|
|
19833
|
+
*/
|
|
19834
|
+
async function settled() {
|
|
19835
|
+
const until = Date.now() + CLOSE_WAIT_MS;
|
|
19836
|
+
for (let quiet = 0; quiet < 2 && Date.now() < until;) {
|
|
19837
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
19838
|
+
quiet = [...raw._databases.values()].some((db) => db.transactions.some((transaction) => transaction._state !== "finished")) ? 0 : quiet + 1;
|
|
19839
|
+
}
|
|
19840
|
+
}
|
|
19685
19841
|
const installed = globalThis;
|
|
19686
19842
|
installed.indexedDB = factory;
|
|
19687
19843
|
installed.IDBKeyRange = IDBKeyRange$1;
|
|
@@ -19690,6 +19846,7 @@ async function openPersistentIndexedDb(dir) {
|
|
|
19690
19846
|
flush: () => queue,
|
|
19691
19847
|
compact: () => enqueue(writeSnapshot),
|
|
19692
19848
|
async close() {
|
|
19849
|
+
await settled();
|
|
19693
19850
|
await enqueue(writeSnapshot);
|
|
19694
19851
|
writing = false;
|
|
19695
19852
|
if (journalFd !== null) {
|
|
@@ -19781,17 +19938,49 @@ function dropIndexEntriesByRecord(classes, cmp) {
|
|
|
19781
19938
|
for (const candidate of gone.indexes.values()) if (candidate.records === this) index = candidate;
|
|
19782
19939
|
}
|
|
19783
19940
|
if (!gone || !index) return deleteByValue.call(this, key);
|
|
19784
|
-
|
|
19785
|
-
|
|
19786
|
-
|
|
19787
|
-
|
|
19788
|
-
|
|
19941
|
+
return dropEntries(index, gone.records, cmp);
|
|
19942
|
+
};
|
|
19943
|
+
}
|
|
19944
|
+
/** Drops from an index the entries of records that left its store, and gives back the ones it dropped. */
|
|
19945
|
+
function dropEntries(index, records, cmp) {
|
|
19946
|
+
const dropped = [];
|
|
19947
|
+
for (const entry of indexEntries(index, records, cmp)) {
|
|
19948
|
+
if (index.records.keysAreUnique) {
|
|
19949
|
+
const held = index.records.get(entry.key);
|
|
19950
|
+
if (!held || cmp(held.value, entry.value) !== 0) continue;
|
|
19951
|
+
}
|
|
19952
|
+
index.records.records.delete(entry);
|
|
19953
|
+
dropped.push(entry);
|
|
19954
|
+
}
|
|
19955
|
+
return dropped;
|
|
19956
|
+
}
|
|
19957
|
+
/**
|
|
19958
|
+
* `cmp` for keys that are stored already (a record's, an index entry's): numbers, strings and arrays of them are
|
|
19959
|
+
* ordered here, as IndexedDB orders them (a number before a string before an array; strings by code unit; arrays item
|
|
19960
|
+
* by item, then the shorter first). Anything else (a date, bytes, a value that is no key) is left to `cmp`, which
|
|
19961
|
+
* 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.
|
|
19962
|
+
*/
|
|
19963
|
+
function keyOrder(cmp) {
|
|
19964
|
+
const order = (a, b) => {
|
|
19965
|
+
const first = typeof a, second = typeof b;
|
|
19966
|
+
if (first === "number" && second === "number") return a === b ? 0 : a > b ? 1 : a < b ? -1 : cmp(a, b);
|
|
19967
|
+
if (first === "string" && second === "string") return a === b ? 0 : a > b ? 1 : -1;
|
|
19968
|
+
if (first === "number" && second === "string") return -1;
|
|
19969
|
+
if (first === "string" && second === "number") return 1;
|
|
19970
|
+
if (Array.isArray(a)) {
|
|
19971
|
+
if (second === "number" || second === "string") return 1;
|
|
19972
|
+
if (!Array.isArray(b)) return cmp(a, b);
|
|
19973
|
+
const length = Math.min(a.length, b.length);
|
|
19974
|
+
for (let at = 0; at < length; at++) {
|
|
19975
|
+
const item = order(a[at], b[at]);
|
|
19976
|
+
if (item !== 0) return item;
|
|
19789
19977
|
}
|
|
19790
|
-
|
|
19791
|
-
dropped.push(entry);
|
|
19978
|
+
return a.length === b.length ? 0 : a.length > b.length ? 1 : -1;
|
|
19792
19979
|
}
|
|
19793
|
-
return
|
|
19980
|
+
if (Array.isArray(b) && (first === "number" || first === "string")) return -1;
|
|
19981
|
+
return cmp(a, b);
|
|
19794
19982
|
};
|
|
19983
|
+
return order;
|
|
19795
19984
|
}
|
|
19796
19985
|
/**
|
|
19797
19986
|
* The snapshot's records come in key order (as `writeSnapshot` walks them), so each store's tree and each index's are
|
|
@@ -19888,21 +20077,27 @@ function fill(store, sorted) {
|
|
|
19888
20077
|
/**
|
|
19889
20078
|
* Replays whole entries; a torn last entry (the process died mid-write) was never acknowledged and is dropped.
|
|
19890
20079
|
*
|
|
19891
|
-
* A store's indexes are set aside while its ops replay and
|
|
20080
|
+
* A store's indexes are set aside while its ops replay and brought in step once at the end: fake-indexeddb drops an
|
|
19892
20081
|
* overwritten or deleted record from an index by walking every record of it, so a crash after a busy chat took
|
|
19893
|
-
* tens of seconds to minutes to start, one write at a time against the whole profile.
|
|
20082
|
+
* tens of seconds to minutes to start, one write at a time against the whole profile. Only the rows the journal
|
|
20083
|
+
* touched change in an index (what each was before goes, what it is now comes in): a journal of one message costs one
|
|
20084
|
+
* message, not the profile's history once per index. A store the journal cleared has its indexes built again whole.
|
|
19894
20085
|
*/
|
|
19895
|
-
function replay(databases, journal) {
|
|
20086
|
+
function replay(databases, journal, cmp) {
|
|
19896
20087
|
const touched = /* @__PURE__ */ new Map();
|
|
19897
20088
|
try {
|
|
19898
20089
|
replayEntries(databases, journal, touched);
|
|
19899
20090
|
} finally {
|
|
19900
|
-
for (const [store, indexes] of touched) {
|
|
20091
|
+
for (const [store, { indexes, was, now }] of touched) {
|
|
20092
|
+
const records = store.records;
|
|
20093
|
+
delete records.put;
|
|
20094
|
+
delete records.delete;
|
|
19901
20095
|
store.rawIndexes = indexes;
|
|
19902
20096
|
for (const index of indexes.values()) {
|
|
19903
|
-
index.records.clear();
|
|
20097
|
+
if (!was) index.records.clear();
|
|
19904
20098
|
if (!index.initialized) continue;
|
|
19905
|
-
|
|
20099
|
+
if (was) dropEntries(index, [...was], cmp);
|
|
20100
|
+
for (const record of was ? now : store.records.values()) if (!absent(index.keyPath, record.value)) index.storeRecord(record);
|
|
19906
20101
|
}
|
|
19907
20102
|
}
|
|
19908
20103
|
}
|
|
@@ -19922,19 +20117,50 @@ function replayEntries(databases, journal, touched) {
|
|
|
19922
20117
|
for (const op of ops) {
|
|
19923
20118
|
const store = databases.get(op.db)?.rawObjectStores.get(op.store);
|
|
19924
20119
|
if (!store) continue;
|
|
19925
|
-
|
|
19926
|
-
|
|
19927
|
-
store.rawIndexes = /* @__PURE__ */ new Map();
|
|
19928
|
-
}
|
|
20120
|
+
let change = touched.get(store);
|
|
20121
|
+
if (!change) touched.set(store, change = watch(store));
|
|
19929
20122
|
if (op.kind === "put") store.storeRecord({
|
|
19930
20123
|
key: op.key,
|
|
19931
20124
|
value: blobsFromDisk(op.value)
|
|
19932
20125
|
}, false);
|
|
19933
20126
|
else if (op.kind === "delete") store.deleteRecord(keyFromDisk(op.key));
|
|
19934
|
-
else
|
|
20127
|
+
else {
|
|
20128
|
+
change.was = null;
|
|
20129
|
+
store.clear();
|
|
20130
|
+
}
|
|
19935
20131
|
}
|
|
19936
20132
|
}
|
|
19937
20133
|
}
|
|
20134
|
+
/**
|
|
20135
|
+
* Sets a store's indexes aside and notes, as its records are written over and removed, which ones left and which
|
|
20136
|
+
* ones came: the record store hands back what a put replaced and what a delete removed, so a row costs no lookup.
|
|
20137
|
+
* A record that came and left again was never in an index.
|
|
20138
|
+
*/
|
|
20139
|
+
function watch(store) {
|
|
20140
|
+
const change = {
|
|
20141
|
+
indexes: store.rawIndexes,
|
|
20142
|
+
was: store.rawIndexes.size ? /* @__PURE__ */ new Set() : null,
|
|
20143
|
+
now: /* @__PURE__ */ new Set()
|
|
20144
|
+
};
|
|
20145
|
+
store.rawIndexes = /* @__PURE__ */ new Map();
|
|
20146
|
+
if (!change.was) return change;
|
|
20147
|
+
const { put, delete: remove } = store.records;
|
|
20148
|
+
const left = (record) => {
|
|
20149
|
+
if (!change.now.delete(record)) change.was?.add(record);
|
|
20150
|
+
};
|
|
20151
|
+
store.records.put = (next, keep) => {
|
|
20152
|
+
const replaced = put.call(store.records, next, keep);
|
|
20153
|
+
if (replaced) left(replaced);
|
|
20154
|
+
change.now.add(next);
|
|
20155
|
+
return replaced;
|
|
20156
|
+
};
|
|
20157
|
+
store.records.delete = (key) => {
|
|
20158
|
+
const removed = remove.call(store.records, key);
|
|
20159
|
+
removed.forEach(left);
|
|
20160
|
+
return removed;
|
|
20161
|
+
};
|
|
20162
|
+
return change;
|
|
20163
|
+
}
|
|
19938
20164
|
function keyToDisk(key) {
|
|
19939
20165
|
if (key instanceof IDBKeyRange$1) return { [RANGE]: {
|
|
19940
20166
|
lower: key.lower,
|
|
@@ -20188,14 +20414,14 @@ async function startRuntime(paths, options = {}) {
|
|
|
20188
20414
|
(await import("./assets/ghostly_iroh_web-Dvzh3Y6E.js")).initSync({ module: irohWasmBytes() });
|
|
20189
20415
|
const [{ registerFileBytes }, { NodeFileBytes }] = await Promise.all([import("./assets/fileBytes-BcYZhNgF.js").then((n) => n.o), import("./assets/fileBytes-Dbifu5I5.js")]);
|
|
20190
20416
|
registerFileBytes("native", async () => new NodeFileBytes(paths.files), true);
|
|
20191
|
-
const [{ EngineServer }, { createHyperEndpoint }] = await Promise.all([import("./assets/server-
|
|
20417
|
+
const [{ EngineServer }, { createHyperEndpoint }] = await Promise.all([import("./assets/server-0zfNpABQ.js").then((n) => n.t), import("./assets/endpoint-pgFkR2AS.js")]);
|
|
20192
20418
|
const network = hyperdhtNetwork();
|
|
20193
20419
|
const dht = mainlineNetwork();
|
|
20194
|
-
const mainline = dht.off ? null : new (await (import("./assets/mainline-
|
|
20420
|
+
const mainline = dht.off ? null : new (await (import("./assets/mainline-BH6s8HVi.js"))).Mainline({
|
|
20195
20421
|
bootstrap: dht.bootstrap,
|
|
20196
20422
|
host: dht.host
|
|
20197
20423
|
});
|
|
20198
|
-
const transport = mainline || pinned ? new (await (import("./assets/mainline-
|
|
20424
|
+
const transport = mainline || pinned ? new (await (import("./assets/mainline-BH6s8HVi.js"))).RelaysAndDht(mainline, pinned ? { relays: pinned } : {}, !!pinned) : null;
|
|
20199
20425
|
const server = new EngineServer({
|
|
20200
20426
|
...transport ? { transport } : {},
|
|
20201
20427
|
irohWeb: true,
|
|
@@ -20340,10 +20566,14 @@ var guarded = false;
|
|
|
20340
20566
|
/**
|
|
20341
20567
|
* A daemon says an error nobody caught (with its stack, on stderr: the daemon's log when detached) and keeps serving:
|
|
20342
20568
|
* one request gone wrong must not take the profile offline. One-shot commands do not call this; they still fail loudly.
|
|
20569
|
+
*
|
|
20570
|
+
* `outputs`: what it writes to. A write there that fails (a pipe whose reader is gone: the process that started the
|
|
20571
|
+
* daemon died) is dropped, not an error nobody caught: saying it would be one more failed write, and the next, with
|
|
20572
|
+
* no turn left for anything else (a full core, and no stop on SIGTERM).
|
|
20343
20573
|
*/
|
|
20344
20574
|
function keepServing(proc = process, write = (text) => {
|
|
20345
20575
|
process.stderr.write(text);
|
|
20346
|
-
}) {
|
|
20576
|
+
}, outputs = proc === process ? [process.stdout, process.stderr] : []) {
|
|
20347
20577
|
if (proc === process) {
|
|
20348
20578
|
if (guarded) return;
|
|
20349
20579
|
guarded = true;
|
|
@@ -20353,6 +20583,7 @@ function keepServing(proc = process, write = (text) => {
|
|
|
20353
20583
|
write(`ghostly: ${what}, the daemon keeps serving: ${error instanceof Error ? error.stack ?? error.message : String(error)}\n`);
|
|
20354
20584
|
} catch {}
|
|
20355
20585
|
};
|
|
20586
|
+
for (const output of outputs) output.on("error", () => {});
|
|
20356
20587
|
proc.on("unhandledRejection", log("unhandled rejection"));
|
|
20357
20588
|
proc.on("uncaughtException", log("uncaught exception"));
|
|
20358
20589
|
}
|
|
@@ -20556,6 +20787,18 @@ function readCursor(path) {
|
|
|
20556
20787
|
const value = Number(readFileSync(path, "utf8").trim());
|
|
20557
20788
|
return Number.isInteger(value) && value >= 0 ? value : void 0;
|
|
20558
20789
|
}
|
|
20790
|
+
/**
|
|
20791
|
+
* Whether a cursor can be kept at `path`, asked before anything starts: its folder is there and takes a file. Before, a
|
|
20792
|
+
* folder that was not there was found at the first event: the listener ran, said "event N failed: ENOENT" for each one
|
|
20793
|
+
* and never wrote the cursor, so the next start began at the journal's end and skipped what came while it was down.
|
|
20794
|
+
* Throws what the file system says (`asCliError` names the path and the code: not_found, refused).
|
|
20795
|
+
*/
|
|
20796
|
+
function checkCursor(path) {
|
|
20797
|
+
if (!path) return;
|
|
20798
|
+
const folder = dirname(resolve(path));
|
|
20799
|
+
if (!statSync(folder).isDirectory()) throw new CliError("not_found", `No such file or folder: ${folder}`);
|
|
20800
|
+
accessSync(folder, constants.W_OK | constants.X_OK);
|
|
20801
|
+
}
|
|
20559
20802
|
function writeCursor(path, seq) {
|
|
20560
20803
|
rmSync(path + ".tmp", { force: true });
|
|
20561
20804
|
writeFileSync(path + ".tmp", String(seq) + "\n", {
|
|
@@ -21097,7 +21340,7 @@ async function profileCommand(sub, argv) {
|
|
|
21097
21340
|
if (!file || !name) throw new CliError("usage", "ghostly profile restore <file> <new profile> [--passphrase-file f] [--use]");
|
|
21098
21341
|
noMoreThan(2, parsed.positionals, "profile restore <file> <new profile> [--passphrase-file f] [--use]");
|
|
21099
21342
|
const protection = await backupFileProtection(file);
|
|
21100
|
-
const paths = await restoreProfile(g.home, checkProfileName(name), file, protection === "none" ? void 0 : backupPassphrase(parsed.options["passphrase-file"]));
|
|
21343
|
+
const paths = await restoreProfile(g.home, checkProfileName(name), file, protection === "none" ? void 0 : backupPassphrase(parsed.options["passphrase-file"], RESTORE_PASSPHRASE));
|
|
21101
21344
|
if (parsed.options.use) selectProfile(g.home, name);
|
|
21102
21345
|
print({
|
|
21103
21346
|
restored: name,
|
|
@@ -21133,10 +21376,13 @@ function onStopSignals(stop) {
|
|
|
21133
21376
|
"SIGHUP"
|
|
21134
21377
|
]) process.on(signal, stop);
|
|
21135
21378
|
}
|
|
21379
|
+
var BACKUP_PASSPHRASE = "The backup passphrase comes from --passphrase-file or GHOSTLY_BACKUP_PASSPHRASE (at least 12 characters). For a backup that is not encrypted, say so with --no-passphrase";
|
|
21380
|
+
/** Restore takes no --no-passphrase: a backup made without one says so itself. */
|
|
21381
|
+
var RESTORE_PASSPHRASE = "This backup is encrypted: its passphrase comes from --passphrase-file or GHOSTLY_BACKUP_PASSPHRASE";
|
|
21136
21382
|
/** The backup passphrase: from a file or the environment, never the command line (it would sit in shell history). */
|
|
21137
|
-
function backupPassphrase(file) {
|
|
21383
|
+
function backupPassphrase(file, missing = BACKUP_PASSPHRASE) {
|
|
21138
21384
|
const value = typeof file === "string" ? readFileSync(file, "utf8").replace(/\r?\n$/, "") : process.env.GHOSTLY_BACKUP_PASSPHRASE;
|
|
21139
|
-
if (!value) throw new CliError("usage",
|
|
21385
|
+
if (!value) throw new CliError("usage", missing);
|
|
21140
21386
|
return value;
|
|
21141
21387
|
}
|
|
21142
21388
|
async function runDaemon(g) {
|
|
@@ -21361,6 +21607,7 @@ async function listenCommand(argv) {
|
|
|
21361
21607
|
description: ""
|
|
21362
21608
|
}
|
|
21363
21609
|
});
|
|
21610
|
+
noMoreThan(0, parsed.positionals, SPECIAL.find(([usage]) => usage.startsWith("listen "))[0]);
|
|
21364
21611
|
const g = globals(parsed);
|
|
21365
21612
|
pretty = false;
|
|
21366
21613
|
if (parsed.options.webhook) checkWebhook(String(parsed.options.webhook));
|
|
@@ -21369,6 +21616,7 @@ async function listenCommand(argv) {
|
|
|
21369
21616
|
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");
|
|
21370
21617
|
const from = parsed.options.from ?? [], groups = parsed.options.group ?? [];
|
|
21371
21618
|
const cursor = parsed.options.cursor;
|
|
21619
|
+
checkCursor(cursor);
|
|
21372
21620
|
let since = parsed.options.since ?? readCursor(cursor);
|
|
21373
21621
|
let handle = null;
|
|
21374
21622
|
const start = async (call, peerOf) => {
|