@ghostlytools/cli 1.1.7 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -19
- package/SKILL.md +15 -7
- package/dist/assets/arkade-CtCpFaF7.js +362 -0
- package/dist/assets/arkade-CtCpFaF7.js.map +1 -0
- package/dist/assets/{endpoint-D_3bYafG.js → endpoint-pgFkR2AS.js} +47 -10
- package/dist/assets/endpoint-pgFkR2AS.js.map +1 -0
- package/dist/assets/{fileBytes-3AQ2tJzW.js → fileBytes-BcYZhNgF.js} +3 -3
- package/dist/assets/{fileBytes-3AQ2tJzW.js.map → fileBytes-BcYZhNgF.js.map} +1 -1
- package/dist/assets/{fileBytes-CAZDydIe.js → fileBytes-Dbifu5I5.js} +2 -2
- package/dist/assets/{fileBytes-CAZDydIe.js.map → fileBytes-Dbifu5I5.js.map} +1 -1
- package/dist/assets/{fileBytesIdb-CmG00qE-.js → fileBytesIdb-DzSaqI0A.js} +3 -3
- package/dist/assets/{fileBytesIdb-CmG00qE-.js.map → fileBytesIdb-DzSaqI0A.js.map} +1 -1
- package/dist/assets/{fileBytesOpfs-CTgqt-p3.js → fileBytesOpfs-Dx8RZiV5.js} +3 -3
- package/dist/assets/{fileBytesOpfs-CTgqt-p3.js.map → fileBytesOpfs-Dx8RZiV5.js.map} +1 -1
- package/dist/assets/{hyperdhtRelay-DvXgebV7.js → hyperdhtRelay-C0XRWySP.js} +3 -3
- package/dist/assets/{hyperdhtRelay-DvXgebV7.js.map → hyperdhtRelay-C0XRWySP.js.map} +1 -1
- package/dist/assets/{idb-BIdus0WX.js → idb-CTF6LF5B.js} +62 -6
- package/dist/assets/idb-CTF6LF5B.js.map +1 -0
- package/dist/assets/{mainline-B7wHcLaW.js → mainline-DVYdrus6.js} +2 -2
- package/dist/assets/{mainline-B7wHcLaW.js.map → mainline-DVYdrus6.js.map} +1 -1
- package/dist/assets/{restoredRows-lhTKGmCO.js → restoredRows-C_84q0Pd.js} +2 -2
- package/dist/assets/{restoredRows-lhTKGmCO.js.map → restoredRows-C_84q0Pd.js.map} +1 -1
- package/dist/assets/{server-GJW_5wGI.js → server-D5FBZYW8.js} +1514 -1088
- package/dist/assets/server-D5FBZYW8.js.map +1 -0
- package/dist/assets/{src-Cp3o_yHN.js → src-aKGjb_yz.js} +855 -262
- package/dist/assets/src-aKGjb_yz.js.map +1 -0
- package/dist/assets/usdt-CUfucwZf.js +410 -0
- package/dist/assets/usdt-CUfucwZf.js.map +1 -0
- package/dist/ghostly.mjs +1326 -262
- package/dist/ghostly.mjs.map +1 -1
- package/package.json +4 -4
- package/dist/assets/endpoint-D_3bYafG.js.map +0 -1
- package/dist/assets/idb-BIdus0WX.js.map +0 -1
- package/dist/assets/server-GJW_5wGI.js.map +0 -1
- package/dist/assets/src-Cp3o_yHN.js.map +0 -1
|
@@ -38,11 +38,38 @@ function bytesEqual(a, b) {
|
|
|
38
38
|
for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
|
|
39
39
|
return diff === 0;
|
|
40
40
|
}
|
|
41
|
+
/** `Uint8Array.prototype.toBase64` (Node 25+, Chrome 140+, Firefox 133+, Safari 18.2+): ~200x the JS below. */
|
|
42
|
+
var nativeToBase64 = Uint8Array.prototype.toBase64;
|
|
43
|
+
var tableOf = (chars) => Uint8Array.from(chars, (c) => c.charCodeAt(0));
|
|
44
|
+
var BASE64_TABLE = tableOf("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/");
|
|
45
|
+
var BASE64URL_TABLE = tableOf("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_");
|
|
46
|
+
/**
|
|
47
|
+
* Base64 where the runtime has no native encoder (Node 22 and 24, older WebViews): the characters go into bytes and
|
|
48
|
+
* become one string at the end. Every file chunk is encoded (files/3 `pf-data`), so this is on a transfer's path.
|
|
49
|
+
*/
|
|
50
|
+
function encodeBase64(bytes, table, pad) {
|
|
51
|
+
const out = new Uint8Array(Math.ceil(bytes.length / 3) * 4);
|
|
52
|
+
let o = 0, i = 0;
|
|
53
|
+
for (; i + 2 < bytes.length; i += 3) {
|
|
54
|
+
const n = bytes[i] << 16 | bytes[i + 1] << 8 | bytes[i + 2];
|
|
55
|
+
out[o++] = table[n >>> 18];
|
|
56
|
+
out[o++] = table[n >>> 12 & 63];
|
|
57
|
+
out[o++] = table[n >>> 6 & 63];
|
|
58
|
+
out[o++] = table[n & 63];
|
|
59
|
+
}
|
|
60
|
+
const rest = bytes.length - i;
|
|
61
|
+
if (rest) {
|
|
62
|
+
const n = bytes[i] << 16 | (rest === 2 ? bytes[i + 1] << 8 : 0);
|
|
63
|
+
out[o++] = table[n >>> 18];
|
|
64
|
+
out[o++] = table[n >>> 12 & 63];
|
|
65
|
+
if (rest === 2) out[o++] = table[n >>> 6 & 63];
|
|
66
|
+
else if (pad) out[o++] = 61;
|
|
67
|
+
if (pad) out[o++] = 61;
|
|
68
|
+
}
|
|
69
|
+
return textDecoder$1.decode(out.subarray(0, o));
|
|
70
|
+
}
|
|
41
71
|
function toBase64(bytes) {
|
|
42
|
-
|
|
43
|
-
const CHUNK = 32768;
|
|
44
|
-
for (let i = 0; i < bytes.length; i += CHUNK) binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
|
|
45
|
-
return btoa(binary);
|
|
72
|
+
return nativeToBase64 ? nativeToBase64.call(bytes) : encodeBase64(bytes, BASE64_TABLE, true);
|
|
46
73
|
}
|
|
47
74
|
function fromBase64(str) {
|
|
48
75
|
const binary = atob(str);
|
|
@@ -51,7 +78,10 @@ function fromBase64(str) {
|
|
|
51
78
|
return bytes;
|
|
52
79
|
}
|
|
53
80
|
function toBase64Url(bytes) {
|
|
54
|
-
return
|
|
81
|
+
return nativeToBase64 ? nativeToBase64.call(bytes, {
|
|
82
|
+
alphabet: "base64url",
|
|
83
|
+
omitPadding: true
|
|
84
|
+
}) : encodeBase64(bytes, BASE64URL_TABLE, false);
|
|
55
85
|
}
|
|
56
86
|
function fromBase64Url(str) {
|
|
57
87
|
let b = str.replace(/-/g, "+").replace(/_/g, "/");
|
|
@@ -108,9 +138,13 @@ function randomBytes(length) {
|
|
|
108
138
|
var HIDDEN = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu;
|
|
109
139
|
/** Zero-width non-joiner and zero-width joiner. */
|
|
110
140
|
var KEEP = /[\u200c\u200d]/;
|
|
141
|
+
/** `text` without its invisible and direction-changing characters, at any length: for display text with a cut of its own. */
|
|
142
|
+
function stripHidden(text) {
|
|
143
|
+
return text.replace(HIDDEN, (char) => KEEP.test(char) ? char : "");
|
|
144
|
+
}
|
|
111
145
|
/** Undefined when nothing visible is left, so callers can fall back. */
|
|
112
146
|
function sanitizeDisplayText(text, max) {
|
|
113
|
-
return [...text
|
|
147
|
+
return [...stripHidden(text)].slice(0, max).join("").trim() || void 0;
|
|
114
148
|
}
|
|
115
149
|
/** A nickname as it arrived from a peer, ready to show. */
|
|
116
150
|
function sanitizeNick(nick) {
|
|
@@ -730,12 +764,15 @@ var asker = (options, write) => ({
|
|
|
730
764
|
write,
|
|
731
765
|
signal: !!options.signal,
|
|
732
766
|
watch: !!options.watch && !write,
|
|
733
|
-
watchWrite: !!options.watch && write
|
|
767
|
+
watchWrite: !!options.watch && write,
|
|
768
|
+
departed: !!options.departed && !write
|
|
734
769
|
});
|
|
735
770
|
/** A 1:1 chat's request: neither a group's nor a background one. */
|
|
736
771
|
var isChat = (who) => !who.background && !who.group;
|
|
737
772
|
/** A 1:1 chat's offer or answer, or its read for the answer to its offer: it may use the allowance (`SIGNALING_ALLOWANCE_SHARE`). */
|
|
738
773
|
var isChatSignal = (who) => who.signal && isChat(who);
|
|
774
|
+
/** A 1:1 chat's read of a contact that just went away: a few may use the chat's allowance (`DEPARTED_READS`). */
|
|
775
|
+
var isDepartedRead = (who) => who.departed && isChat(who);
|
|
739
776
|
/** A group's edge looking fast for a signal (`GROUP_BURST_MS`). */
|
|
740
777
|
var isGroupUrgentRead = (who) => who.group && who.urgent && !who.write && !who.background;
|
|
741
778
|
/**
|
|
@@ -853,7 +890,8 @@ var CATCH_UP_ASKER = {
|
|
|
853
890
|
write: true,
|
|
854
891
|
signal: false,
|
|
855
892
|
watch: false,
|
|
856
|
-
watchWrite: false
|
|
893
|
+
watchWrite: false,
|
|
894
|
+
departed: false
|
|
857
895
|
};
|
|
858
896
|
/** Keys whose last relay is remembered for their next read (`resolve`): a profile's links and lookups, with room. */
|
|
859
897
|
var READ_TURNS_KEPT = 512;
|
|
@@ -934,6 +972,8 @@ var RelayTransport = class {
|
|
|
934
972
|
spentWatch = /* @__PURE__ */ new Map();
|
|
935
973
|
/** Groups' reads past the limit on each relay in the last `GROUP_OVER_PACE_MS` (`GROUP_SIGNALING_ALLOWANCE_SHARE`). */
|
|
936
974
|
spentGroupOver = /* @__PURE__ */ new Map();
|
|
975
|
+
/** Per contact, its departed reads past the limit in the last minute, every relay together (`DEPARTED_READS`). */
|
|
976
|
+
departedOver = /* @__PURE__ */ new Map();
|
|
937
977
|
/**
|
|
938
978
|
* Per key a group reads (not in the background), when a read of it last went out and when it last asked: whose turn
|
|
939
979
|
* it is under the watch share (`WATCH_SHARE`) and while the groups' reads are rationed (`GROUP_RATION_WINDOW_MS`).
|
|
@@ -1541,6 +1581,7 @@ var RelayTransport = class {
|
|
|
1541
1581
|
}
|
|
1542
1582
|
if (writer) this.writeWaiting.delete(`${writer} ${relay}`);
|
|
1543
1583
|
if (isGroupSignal(who) && !who.write && this.spent.get(relay).length >= this.limitOf(relay)) this.spentGroupOver.get(relay).push(now);
|
|
1584
|
+
if (isDepartedRead(who) && this.spent.get(relay).length >= this.limitOf(relay)) this.departedOverAdd(key, now);
|
|
1544
1585
|
this.spent.get(relay).push(now);
|
|
1545
1586
|
if (who.background) this.spentBackground.get(relay).push(now);
|
|
1546
1587
|
if (isGroupUrgentRead(who)) this.spentGroupUrgent.get(relay).push(now);
|
|
@@ -1563,7 +1604,8 @@ var RelayTransport = class {
|
|
|
1563
1604
|
*
|
|
1564
1605
|
* Who goes first: a chat's refused write holds back everything but chat writes; a group's refused write holds back
|
|
1565
1606
|
* group reads and background requests, never a chat's. Background requests yield to a link that signals. A chat's
|
|
1566
|
-
* signaling goes over the limit by its allowance (`SIGNALING_ALLOWANCE_SHARE`)
|
|
1607
|
+
* signaling goes over the limit by its allowance (`SIGNALING_ALLOWANCE_SHARE`), and so do a few of its reads of a
|
|
1608
|
+
* contact that just went away (`DEPARTED_READS`).
|
|
1567
1609
|
*/
|
|
1568
1610
|
heldFor(relay, who, now = Date.now(), key) {
|
|
1569
1611
|
const recent = (this.spent.get(relay) ?? []).filter((at) => now - at < 6e4);
|
|
@@ -1572,7 +1614,8 @@ var RelayTransport = class {
|
|
|
1572
1614
|
this.spentBackground.set(relay, recentBackground);
|
|
1573
1615
|
const over = (list, limit) => list.length >= limit ? list[list.length - limit] + 6e4 - now : 0;
|
|
1574
1616
|
const limit = this.limitOf(relay);
|
|
1575
|
-
const
|
|
1617
|
+
const departed = isDepartedRead(who) && key !== void 0 && recent.length >= limit && this.departedReads(key, now) < 6;
|
|
1618
|
+
const share = isChatSignal(who) || departed ? SIGNALING_ALLOWANCE_SHARE : isGroupSignal(who) ? GROUP_SIGNALING_ALLOWANCE_SHARE : 0;
|
|
1576
1619
|
let wait = over(recent, limit + (share ? Math.max(0, Math.floor(limit * share) - (who.write ? 0 : 1)) : 0));
|
|
1577
1620
|
const groupOver = (this.spentGroupOver.get(relay) ?? []).filter((at) => now - at < GROUP_OVER_PACE_MS);
|
|
1578
1621
|
this.spentGroupOver.set(relay, groupOver);
|
|
@@ -1612,7 +1655,7 @@ var RelayTransport = class {
|
|
|
1612
1655
|
const at = this.writeWaiting.get(`${lane} ${relay}`);
|
|
1613
1656
|
return at !== void 0 && now - at < 5e3 ? at + WRITE_FIRST_MS - now : 0;
|
|
1614
1657
|
};
|
|
1615
|
-
if (writer !== "chat") wait = Math.max(wait, waiting("chat"));
|
|
1658
|
+
if (writer !== "chat" && !departed) wait = Math.max(wait, waiting("chat"));
|
|
1616
1659
|
if (writer === null && who.group && !pastLimit || who.background) wait = Math.max(wait, waiting("group"));
|
|
1617
1660
|
return Math.max(wait, 0);
|
|
1618
1661
|
}
|
|
@@ -1643,6 +1686,18 @@ var RelayTransport = class {
|
|
|
1643
1686
|
}
|
|
1644
1687
|
return ahead;
|
|
1645
1688
|
}
|
|
1689
|
+
/** This contact's departed reads past the limit in the last minute (`DEPARTED_READS`). */
|
|
1690
|
+
departedReads(key, now) {
|
|
1691
|
+
const reads = (this.departedOver.get(key) ?? []).filter((at) => now - at < 6e4);
|
|
1692
|
+
if (reads.length) this.departedOver.set(key, reads);
|
|
1693
|
+
else this.departedOver.delete(key);
|
|
1694
|
+
return reads.length;
|
|
1695
|
+
}
|
|
1696
|
+
/** One more, the contacts with none in the last minute dropped. */
|
|
1697
|
+
departedOverAdd(key, now) {
|
|
1698
|
+
for (const [other, reads] of this.departedOver) if (now - reads[reads.length - 1] >= 6e4) this.departedOver.delete(other);
|
|
1699
|
+
this.departedOver.set(key, [...this.departedOver.get(key) ?? [], now]);
|
|
1700
|
+
}
|
|
1646
1701
|
chatNeed(key) {
|
|
1647
1702
|
let need = this.chatNeeds.get(key);
|
|
1648
1703
|
if (!need) this.chatNeeds.set(key, need = {
|
|
@@ -1952,9 +2007,11 @@ var isZero = (bytes) => bytes.every((b) => b === 0);
|
|
|
1952
2007
|
/**
|
|
1953
2008
|
* A device's name as its slot holds it: UTF-8, cut at a character boundary to 16 bytes, and with no NUL (the padding).
|
|
1954
2009
|
* A character is what a person sees as one (a grapheme), so a cut never leaves half an emoji or a lone joiner.
|
|
2010
|
+
* A name is display text a device chooses for itself and every device of the set shows: it is cleaned as a nickname is
|
|
2011
|
+
* (`sanitizeDisplayText`: no control, invisible or direction-changing character) and trimmed, made and read alike.
|
|
1955
2012
|
*/
|
|
1956
2013
|
function turnName(name) {
|
|
1957
|
-
const clean = name.
|
|
2014
|
+
const clean = stripHidden(name).trim();
|
|
1958
2015
|
const Segmenter = typeof Intl === "undefined" ? void 0 : Intl.Segmenter;
|
|
1959
2016
|
const characters = Segmenter ? Array.from(new Segmenter(void 0, { granularity: "grapheme" }).segment(clean), (s) => s.segment) : Array.from(clean);
|
|
1960
2017
|
let out = "", length = 0;
|
|
@@ -1964,7 +2021,7 @@ function turnName(name) {
|
|
|
1964
2021
|
out += character;
|
|
1965
2022
|
length += size;
|
|
1966
2023
|
}
|
|
1967
|
-
return out;
|
|
2024
|
+
return out.trimEnd();
|
|
1968
2025
|
}
|
|
1969
2026
|
function u32$3(value) {
|
|
1970
2027
|
return Uint8Array.of(value >>> 24 & 255, value >>> 16 & 255, value >>> 8 & 255, value & 255);
|
|
@@ -2069,7 +2126,7 @@ function readTurnBody(body, address) {
|
|
|
2069
2126
|
if (slots.some((other) => other && bytesEqual(other.key, key))) refuse("slots", "one key in two slots");
|
|
2070
2127
|
slots.push({
|
|
2071
2128
|
key,
|
|
2072
|
-
name: text
|
|
2129
|
+
name: turnName(text)
|
|
2073
2130
|
});
|
|
2074
2131
|
}
|
|
2075
2132
|
if (slots.filter(Boolean).length !== count) refuse("count");
|
|
@@ -3212,13 +3269,17 @@ function encodeSetSlots(set) {
|
|
|
3212
3269
|
}
|
|
3213
3270
|
/** The bytes the remover signs: the old turn address, then the frame's fields, the tombstone by its digest. */
|
|
3214
3271
|
function setUpdateMessage(oldAddress, update) {
|
|
3272
|
+
return signedBytes(oldAddress, update, encodeSetSlots(update.set));
|
|
3273
|
+
}
|
|
3274
|
+
/** `slots`: the set as the frame carries it. A reader hands over the frame's own, a name cleaned for display is not what was signed. */
|
|
3275
|
+
function signedBytes(oldAddress, update, slots) {
|
|
3215
3276
|
if (oldAddress.length !== 32) throw new Error("A turn address is 32 bytes");
|
|
3216
3277
|
if (update.d.length !== 32) throw new Error("The device-set secret is 32 bytes");
|
|
3217
3278
|
return utf8Encode(JSON.stringify([
|
|
3218
3279
|
"ghostly-set-update",
|
|
3219
3280
|
toBase64Url(oldAddress),
|
|
3220
3281
|
toBase64Url(update.d),
|
|
3221
|
-
|
|
3282
|
+
slots,
|
|
3222
3283
|
update.turn,
|
|
3223
3284
|
update.rev,
|
|
3224
3285
|
toBase64Url(sha256(update.tomb))
|
|
@@ -3254,16 +3315,19 @@ function readSetUpdate(frame) {
|
|
|
3254
3315
|
if (!Number.isInteger(frame.turn) || frame.turn < 0 || frame.turn > TURN_MAX) return null;
|
|
3255
3316
|
if (!Number.isInteger(frame.rev) || frame.rev < 0 || frame.rev >= TURN_REV_LIMIT) return null;
|
|
3256
3317
|
const set = [];
|
|
3318
|
+
const signed = [];
|
|
3257
3319
|
for (const slot of frame.set) {
|
|
3258
3320
|
if (slot === null) {
|
|
3259
3321
|
set.push(null);
|
|
3322
|
+
signed.push(null);
|
|
3260
3323
|
continue;
|
|
3261
3324
|
}
|
|
3262
3325
|
if (!Array.isArray(slot) || slot.length !== 2 || typeof slot[0] !== "string" || !KEY$4.test(slot[0]) || typeof slot[1] !== "string" || slot[1].length > 64) return null;
|
|
3263
|
-
if (
|
|
3326
|
+
if (utf8Encode(slot[1]).length > 16 || slot[1].includes("\0")) return null;
|
|
3327
|
+
signed.push([slot[0], slot[1]]);
|
|
3264
3328
|
set.push({
|
|
3265
3329
|
key: fromBase64Url(slot[0]),
|
|
3266
|
-
name: slot[1]
|
|
3330
|
+
name: turnName(slot[1])
|
|
3267
3331
|
});
|
|
3268
3332
|
}
|
|
3269
3333
|
const keys = set.flatMap((slot) => slot ? [toBase64Url(slot.key)] : []);
|
|
@@ -3271,6 +3335,7 @@ function readSetUpdate(frame) {
|
|
|
3271
3335
|
return {
|
|
3272
3336
|
d: fromBase64Url(frame.d),
|
|
3273
3337
|
set,
|
|
3338
|
+
signed,
|
|
3274
3339
|
turn: frame.turn,
|
|
3275
3340
|
rev: frame.rev,
|
|
3276
3341
|
tomb: fromBase64Url(frame.tomb),
|
|
@@ -3282,7 +3347,7 @@ function readSetUpdate(frame) {
|
|
|
3282
3347
|
/** Whether `by` signed this update for the turn address `oldAddress`. */
|
|
3283
3348
|
function verifySetUpdate(update, oldAddress) {
|
|
3284
3349
|
try {
|
|
3285
|
-
return verify(update.signature,
|
|
3350
|
+
return verify(update.signature, signedBytes(oldAddress, update, update.signed ?? encodeSetSlots(update.set)), update.by);
|
|
3286
3351
|
} catch {
|
|
3287
3352
|
return false;
|
|
3288
3353
|
}
|
|
@@ -4438,6 +4503,13 @@ function waitForIceGathering(pc, timeoutMs = 1e4, { stallMs, fresh = false, sett
|
|
|
4438
4503
|
}
|
|
4439
4504
|
//#endregion
|
|
4440
4505
|
//#region ../core/src/signal.ts
|
|
4506
|
+
/**
|
|
4507
|
+
* Signaling for the WebRTC data link, carried in the `_rtc` record. It follows
|
|
4508
|
+
* the convention `_call` established: only what is unique to the session
|
|
4509
|
+
* travels through Pkarr and the SDP is rebuilt locally. The DTLS fingerprint
|
|
4510
|
+
* arrives inside a packet that is signed by the peer's key and encrypted with
|
|
4511
|
+
* the link key, which is what binds the WebRTC connection to the peer identity.
|
|
4512
|
+
*/
|
|
4441
4513
|
var DATA_CHANNEL_LABEL = "ghostly/1";
|
|
4442
4514
|
/** An offer older than this is ignored. */
|
|
4443
4515
|
var RTC_SIGNAL_MAX_AGE_MS = 12e4;
|
|
@@ -4484,17 +4556,34 @@ var CANDIDATE_TYPE_NAME = {
|
|
|
4484
4556
|
s: "srflx",
|
|
4485
4557
|
r: "relay"
|
|
4486
4558
|
};
|
|
4559
|
+
/**
|
|
4560
|
+
* The signal carries two host candidates, and a computer often has more interfaces than that: Wi-Fi, Ethernet, VM
|
|
4561
|
+
* bridges, Tailscale, a VPN. The browser lists them in its own order, and the first two are not always ones the
|
|
4562
|
+
* contact can reach: on a Mac with two VM bridges they are the bridges (addresses that exist on that Mac only), and
|
|
4563
|
+
* the Wi-Fi and Ethernet addresses never left it, so a contact on the same network was reached only if the router
|
|
4564
|
+
* sends a packet for its own public address back in (2026-10-02). So host candidates go in this order of preference:
|
|
4565
|
+
* those a server reflexive candidate was gathered from (`raddr`: an interface with a way out, the network a contact
|
|
4566
|
+
* nearby shares), then the others, and in each case those the browser marks as costly last (`network-cost` 50 or
|
|
4567
|
+
* more: a VPN, or an interface it does not know), as a call's candidates do (`pickCallCandidates`). A host on a
|
|
4568
|
+
* global IPv4 address has a way out too: it gets no reflexive candidate (Chromium drops one equal to a host), and the
|
|
4569
|
+
* gathering wait ends on it (`isGlobalIpv4Host`). That wait ends 400 ms after the first reflexive candidate, so only
|
|
4570
|
+
* the interfaces whose STUN answer came by then are known to have a way out. With no reflexive candidate, or none
|
|
4571
|
+
* that says where it came from, the order is the browser's, as before.
|
|
4572
|
+
*/
|
|
4573
|
+
function hostRank(parts, reflexiveBases) {
|
|
4574
|
+
const cost = Number(parts[parts.indexOf("network-cost") + 1]);
|
|
4575
|
+
const costly = parts.includes("network-cost") && cost >= 50;
|
|
4576
|
+
const wayOut = reflexiveBases.has(parts[4]) || isGlobalIpv4Host(parts.join(" "));
|
|
4577
|
+
return (costly ? 2 : 0) + (wayOut ? 0 : 1);
|
|
4578
|
+
}
|
|
4487
4579
|
function extractRtcParams(sdp) {
|
|
4488
4580
|
let u = "";
|
|
4489
4581
|
let p = "";
|
|
4490
4582
|
let f = "";
|
|
4491
4583
|
let s = "";
|
|
4492
|
-
|
|
4493
|
-
const
|
|
4494
|
-
|
|
4495
|
-
s: 0,
|
|
4496
|
-
r: 0
|
|
4497
|
-
};
|
|
4584
|
+
/** UDP component-1 candidates of a type the signal carries, in the SDP's order. */
|
|
4585
|
+
const found = [];
|
|
4586
|
+
const reflexiveBases = /* @__PURE__ */ new Set();
|
|
4498
4587
|
for (const line of sdp.split(/\r?\n/)) if (line.startsWith("a=ice-ufrag:") && !u) u = line.slice(12);
|
|
4499
4588
|
else if (line.startsWith("a=ice-pwd:") && !p) p = line.slice(10);
|
|
4500
4589
|
else if (line.toLowerCase().startsWith("a=fingerprint:sha-256 ") && !f) f = line.slice(22).replace(/:/g, "").toLowerCase();
|
|
@@ -4503,11 +4592,33 @@ function extractRtcParams(sdp) {
|
|
|
4503
4592
|
const parts = line.slice(12).split(" ");
|
|
4504
4593
|
if (parts.length < 8 || parts[1] !== "1" || parts[2].toLowerCase() !== "udp") continue;
|
|
4505
4594
|
const type = CANDIDATE_TYPES[parts[7]];
|
|
4506
|
-
if (!type
|
|
4595
|
+
if (!type) continue;
|
|
4507
4596
|
const compact = `${type},${parts[4]},${parts[5]}`;
|
|
4508
|
-
if (
|
|
4509
|
-
|
|
4510
|
-
|
|
4597
|
+
if (found.some((c) => c.compact === compact)) continue;
|
|
4598
|
+
found.push({
|
|
4599
|
+
type,
|
|
4600
|
+
parts,
|
|
4601
|
+
compact
|
|
4602
|
+
});
|
|
4603
|
+
const base = type === "s" && parts.includes("raddr") ? parts[parts.indexOf("raddr") + 1] : void 0;
|
|
4604
|
+
if (base && base !== "0.0.0.0" && base !== "::") reflexiveBases.add(base);
|
|
4605
|
+
}
|
|
4606
|
+
const hosts = found.filter((c) => c.type === "h");
|
|
4607
|
+
const preferred = new Set(hosts.map((c, at) => ({
|
|
4608
|
+
c,
|
|
4609
|
+
at,
|
|
4610
|
+
rank: hostRank(c.parts, reflexiveBases)
|
|
4611
|
+
})).sort((a, b) => a.rank - b.rank || a.at - b.at).slice(0, CANDIDATE_LIMITS.h).map(({ c }) => c));
|
|
4612
|
+
const candidates = [];
|
|
4613
|
+
const counts = {
|
|
4614
|
+
h: 0,
|
|
4615
|
+
s: 0,
|
|
4616
|
+
r: 0
|
|
4617
|
+
};
|
|
4618
|
+
for (const c of found) {
|
|
4619
|
+
if (c.type === "h" ? !preferred.has(c) : counts[c.type] >= CANDIDATE_LIMITS[c.type]) continue;
|
|
4620
|
+
counts[c.type]++;
|
|
4621
|
+
candidates.push(c.compact);
|
|
4511
4622
|
}
|
|
4512
4623
|
return {
|
|
4513
4624
|
u,
|
|
@@ -4589,6 +4700,8 @@ function buildDataSdp(signal) {
|
|
|
4589
4700
|
var LIMITS = {
|
|
4590
4701
|
/** Largest control frame accepted, headers included. */
|
|
4591
4702
|
maxControlFrameBytes: 61440,
|
|
4703
|
+
/** Longest payment endpoint value (a Cashu token, an invoice) a payment frame may carry; a longer one is dropped. */
|
|
4704
|
+
maxPaymentEndpointChars: 32768,
|
|
4592
4705
|
/** Largest DataChannel message sent. 16 KiB is safe across WebRTC stacks. */
|
|
4593
4706
|
maxChunkMessageBytes: 16384,
|
|
4594
4707
|
maxRequestBodyBytes: 8388608,
|
|
@@ -4642,7 +4755,7 @@ function isUnit(value) {
|
|
|
4642
4755
|
return typeof value === "string" && /^[a-z0-9]{1,12}$/.test(value);
|
|
4643
4756
|
}
|
|
4644
4757
|
function isEndpoint(value) {
|
|
4645
|
-
return Array.isArray(value) && value.length === 2 && typeof value[0] === "string" && value[0].length <= 64 && /^[a-z0-9][a-z0-9-]*(?:\/[1-9][0-9]*)?$/.test(value[0]) && typeof value[1] === "string" && value[1].length <=
|
|
4758
|
+
return Array.isArray(value) && value.length === 2 && typeof value[0] === "string" && value[0].length <= 64 && /^[a-z0-9][a-z0-9-]*(?:\/[1-9][0-9]*)?$/.test(value[0]) && typeof value[1] === "string" && value[1].length <= LIMITS.maxPaymentEndpointChars;
|
|
4646
4759
|
}
|
|
4647
4760
|
/** Returns null for anything malformed; callers drop the frame. */
|
|
4648
4761
|
function decodeControl(text) {
|
|
@@ -5435,6 +5548,18 @@ var EXPECT_PEER_MS = 3e4;
|
|
|
5435
5548
|
* or the session dropped): an app that restarts is back within this, and its dial or offer is seen in seconds.
|
|
5436
5549
|
*/
|
|
5437
5550
|
var WATCH_PEER_MS = 12e4;
|
|
5551
|
+
/**
|
|
5552
|
+
* The longest a link reads fast for a data link whose connection went `disconnected` (`setDataLinkStalled`): a little
|
|
5553
|
+
* past the 12 s such a connection is given (`DISCONNECT_GRACE_MS`), by when it is back or the session is gone.
|
|
5554
|
+
*/
|
|
5555
|
+
var STALLED_LOOK_MS = 15e3;
|
|
5556
|
+
/**
|
|
5557
|
+
* For this long after its contact went away from a live session, a chat's reads of it say so (`PkarrRequestOptions.departed`):
|
|
5558
|
+
* a few may go over the relays' budget (`DEPARTED_READS` in relay.ts). The fast window that follows a goodbye
|
|
5559
|
+
* (`EXPECT_PEER_MS`): an app that restarts has its offer out well within it (2.5 to 10 s after its goodbye in the lab of
|
|
5560
|
+
* #1441, on a loaded host), and past it the race's DHT dial has brought the chat back anyway.
|
|
5561
|
+
*/
|
|
5562
|
+
var DEPARTED_READ_MS = EXPECT_PEER_MS;
|
|
5438
5563
|
var PUBLISH_RETRY_MS = 4e3;
|
|
5439
5564
|
/**
|
|
5440
5565
|
* A packet the relays' request budget held back goes again when the budget frees a request, and at least this often
|
|
@@ -5471,6 +5596,9 @@ var LinkSession = class {
|
|
|
5471
5596
|
/** A group link's `_tr` value this side publishes (`setTransports`), and the last the peer's packet carried. */
|
|
5472
5597
|
transports = null;
|
|
5473
5598
|
lastTransportsIn = null;
|
|
5599
|
+
/** When a read first found the peer's packet advertising with no `_tr` (0: the last packet that said carried one). */
|
|
5600
|
+
transportsAbsentSince = 0;
|
|
5601
|
+
transportsGone = false;
|
|
5474
5602
|
running = false;
|
|
5475
5603
|
/** Stopped without a last packet (`stop(false)`): nothing more goes out. */
|
|
5476
5604
|
silent = false;
|
|
@@ -5492,9 +5620,13 @@ var LinkSession = class {
|
|
|
5492
5620
|
watchUntil = 0;
|
|
5493
5621
|
/** The time of the peer's packet read last when it went away (`watchPeer`): a newer one that advertises says it is back. */
|
|
5494
5622
|
watchFrom = 0;
|
|
5623
|
+
/** Until when the reads of a contact that went away are its first ones (`DEPARTED_READ_MS`). */
|
|
5624
|
+
departedUntil = 0;
|
|
5495
5625
|
publishRetryTimer = null;
|
|
5496
5626
|
active = false;
|
|
5497
5627
|
connected = false;
|
|
5628
|
+
/** The open data link's connection is `disconnected`, and read fast until then (`setDataLinkStalled`). */
|
|
5629
|
+
stalledUntil = 0;
|
|
5498
5630
|
firstPublish;
|
|
5499
5631
|
firstPublishTimer = null;
|
|
5500
5632
|
firstPollDone = false;
|
|
@@ -5584,6 +5716,10 @@ var LinkSession = class {
|
|
|
5584
5716
|
if (!this.running || this.lastPublishedAt > 0 || this.publishing) return;
|
|
5585
5717
|
this.publish().catch(() => {});
|
|
5586
5718
|
}
|
|
5719
|
+
/** This side's packet goes out again as it is: another writer of its key (a second device on a device code) may have replaced it. */
|
|
5720
|
+
republish() {
|
|
5721
|
+
if (this.running) this.publish().catch(() => {});
|
|
5722
|
+
}
|
|
5587
5723
|
/** Stops the loops. With `announce`, first tells the peer we are gone. */
|
|
5588
5724
|
async stop(announce = true) {
|
|
5589
5725
|
if (!this.running) return;
|
|
@@ -5643,13 +5779,28 @@ var LinkSession = class {
|
|
|
5643
5779
|
watchPeer() {
|
|
5644
5780
|
this.watchUntil = Math.max(this.watchUntil, Date.now() + WATCH_PEER_MS);
|
|
5645
5781
|
this.watchFrom = this.presence.lastPacketAt;
|
|
5782
|
+
this.departedUntil = Date.now() + DEPARTED_READ_MS;
|
|
5646
5783
|
this.expectPeer();
|
|
5647
5784
|
}
|
|
5648
5785
|
/** The data link carries chat and services while it is up. */
|
|
5649
5786
|
setDataLinkOpen(open) {
|
|
5650
5787
|
this.connected = open;
|
|
5788
|
+
this.stalledUntil = 0;
|
|
5651
5789
|
if (!open) this.pollNow();
|
|
5652
5790
|
}
|
|
5791
|
+
/**
|
|
5792
|
+
* The open data link's connection went `disconnected` (true), or came back: the contact's app may have crashed, and
|
|
5793
|
+
* once it starts again its offer is on Pkarr, which nothing on a dead data link says. Read fast for as long as it
|
|
5794
|
+
* lasts (the connection is given up or back within seconds), not at the connected pace. One read as it went
|
|
5795
|
+
* `disconnected` found only an offer already out: an app back a moment later (a tab opened again by hand) waited for
|
|
5796
|
+
* the connection's grace to run out (web/restart-relink.spec.ts, 20 s on a busy machine). Never longer than
|
|
5797
|
+
* `STALLED_LOOK_MS`, whatever becomes of the connection.
|
|
5798
|
+
*/
|
|
5799
|
+
setDataLinkStalled(stalled) {
|
|
5800
|
+
if (stalled === Date.now() < this.stalledUntil) return;
|
|
5801
|
+
this.stalledUntil = stalled ? Date.now() + STALLED_LOOK_MS : 0;
|
|
5802
|
+
if (stalled) this.pollNow();
|
|
5803
|
+
}
|
|
5653
5804
|
async setCallSignal(signal) {
|
|
5654
5805
|
this.callSignal = signal;
|
|
5655
5806
|
this.lastActivity = Date.now();
|
|
@@ -5773,6 +5924,7 @@ var LinkSession = class {
|
|
|
5773
5924
|
}
|
|
5774
5925
|
/** How urgently this link looks right now. */
|
|
5775
5926
|
pace() {
|
|
5927
|
+
if (Date.now() < this.stalledUntil) return "fast";
|
|
5776
5928
|
if (this.connected) return "connected";
|
|
5777
5929
|
if (Date.now() < this.fastPollUntil) return "fast";
|
|
5778
5930
|
if (Date.now() < this.watchUntil) return "active";
|
|
@@ -5845,7 +5997,10 @@ var LinkSession = class {
|
|
|
5845
5997
|
ms,
|
|
5846
5998
|
rtc: !!rtcSignal,
|
|
5847
5999
|
error: error instanceof Error ? error.message : String(error),
|
|
5848
|
-
...waiting && {
|
|
6000
|
+
...waiting && {
|
|
6001
|
+
waiting,
|
|
6002
|
+
retryInMs: error.retryInMs
|
|
6003
|
+
}
|
|
5849
6004
|
});
|
|
5850
6005
|
throw error;
|
|
5851
6006
|
}
|
|
@@ -5893,11 +6048,13 @@ var LinkSession = class {
|
|
|
5893
6048
|
const pace = this.pace();
|
|
5894
6049
|
const signal = pace === "fast" && this.fastStepsAfter > 0 && this.rtcSignalOut !== null;
|
|
5895
6050
|
const watch = !signal && this.watching();
|
|
6051
|
+
const departed = !signal && Date.now() < this.departedUntil;
|
|
5896
6052
|
const packet = await this.transport.resolve(this.peerPubKeyZ32, {
|
|
5897
6053
|
background: pace === "background" || pace === "connected",
|
|
5898
6054
|
urgent: pace === "fast",
|
|
5899
6055
|
...signal && { signal },
|
|
5900
|
-
...watch && { watch }
|
|
6056
|
+
...watch && { watch },
|
|
6057
|
+
...departed && { departed }
|
|
5901
6058
|
});
|
|
5902
6059
|
if (!this.running) return;
|
|
5903
6060
|
this.discoveryResult("read");
|
|
@@ -5939,9 +6096,20 @@ var LinkSession = class {
|
|
|
5939
6096
|
nick: batch.nick,
|
|
5940
6097
|
services: online ? batch.services : null
|
|
5941
6098
|
};
|
|
5942
|
-
if (batch.transports !== null
|
|
5943
|
-
this.
|
|
5944
|
-
this.
|
|
6099
|
+
if (batch.transports !== null) {
|
|
6100
|
+
this.transportsAbsentSince = 0;
|
|
6101
|
+
this.transportsGone = false;
|
|
6102
|
+
if (batch.transports !== this.lastTransportsIn) {
|
|
6103
|
+
this.lastTransportsIn = batch.transports;
|
|
6104
|
+
this.events.onPeerTransports?.(batch.transports);
|
|
6105
|
+
}
|
|
6106
|
+
} else if (batch.services !== null && !this.transportsGone) {
|
|
6107
|
+
this.transportsAbsentSince ||= Date.now();
|
|
6108
|
+
if (Date.now() - this.transportsAbsentSince >= 12e4) {
|
|
6109
|
+
this.transportsGone = true;
|
|
6110
|
+
this.lastTransportsIn = null;
|
|
6111
|
+
this.events.onPeerTransportsGone?.();
|
|
6112
|
+
}
|
|
5945
6113
|
}
|
|
5946
6114
|
this.events.onPresence?.(this.presence);
|
|
5947
6115
|
if (read && readBefore && batch.packetTimestamp !== wasSeen) this.events.onPeerClock?.(batch.packetTimestamp, readBefore, Date.now());
|
|
@@ -6125,6 +6293,29 @@ var GATHER_STALL_MS = 3e3;
|
|
|
6125
6293
|
var DISCONNECT_GRACE_MS = 12e3;
|
|
6126
6294
|
/** An offer this close to the end of the offerer's attempt is not answered again: the new answer would come too late. */
|
|
6127
6295
|
var REANSWER_MARGIN_MS = 15e3;
|
|
6296
|
+
/**
|
|
6297
|
+
* Traces the candidate pair an opened data link uses, by type only (host, srflx, prflx, relay; never an address): two
|
|
6298
|
+
* devices on one network should meet host to host. A connection without stats (a test's fake) traces nothing.
|
|
6299
|
+
*/
|
|
6300
|
+
async function tracePair(me, pc) {
|
|
6301
|
+
try {
|
|
6302
|
+
if (typeof pc.getStats !== "function") return;
|
|
6303
|
+
const stats = await pc.getStats();
|
|
6304
|
+
const byId = /* @__PURE__ */ new Map();
|
|
6305
|
+
stats.forEach((s) => byId.set(s.id, s));
|
|
6306
|
+
let pairId;
|
|
6307
|
+
for (const s of byId.values()) if (s.type === "transport" && s.selectedCandidatePairId) pairId = s.selectedCandidatePairId;
|
|
6308
|
+
const pair = pairId !== void 0 ? byId.get(pairId) : [...byId.values()].find((s) => s.type === "candidate-pair" && s.nominated && s.state === "succeeded");
|
|
6309
|
+
if (!pair) return;
|
|
6310
|
+
const kind = (id) => byId.get(id)?.candidateType ?? "?";
|
|
6311
|
+
const rtt = typeof pair.currentRoundTripTime === "number" ? Math.round(pair.currentRoundTripTime * 1e3) : void 0;
|
|
6312
|
+
traceLink(me, "rtc-pair", {
|
|
6313
|
+
local: kind(pair.localCandidateId),
|
|
6314
|
+
remote: kind(pair.remoteCandidateId),
|
|
6315
|
+
...rtt !== void 0 && { rttMs: rtt }
|
|
6316
|
+
});
|
|
6317
|
+
} catch {}
|
|
6318
|
+
}
|
|
6128
6319
|
var DataLink = class {
|
|
6129
6320
|
options;
|
|
6130
6321
|
state = "idle";
|
|
@@ -6224,7 +6415,7 @@ var DataLink = class {
|
|
|
6224
6415
|
if (signal.t === "o") {
|
|
6225
6416
|
if (this.state === "offering") {
|
|
6226
6417
|
if (this.options.myPubKeyZ32 < this.options.peerPubKeyZ32) return;
|
|
6227
|
-
}
|
|
6418
|
+
}
|
|
6228
6419
|
this.lastSignalTs = signal.ts;
|
|
6229
6420
|
const wasOpen = this.state === "open";
|
|
6230
6421
|
this.teardown();
|
|
@@ -6354,7 +6545,7 @@ var DataLink = class {
|
|
|
6354
6545
|
this.options.publishSignal(JSON.stringify(signal));
|
|
6355
6546
|
this.setState("connecting");
|
|
6356
6547
|
} catch {
|
|
6357
|
-
this.reset();
|
|
6548
|
+
if (this.answered?.offer === offer) this.reset();
|
|
6358
6549
|
}
|
|
6359
6550
|
}
|
|
6360
6551
|
/**
|
|
@@ -6418,6 +6609,7 @@ var DataLink = class {
|
|
|
6418
6609
|
this.options.publishSignal(null);
|
|
6419
6610
|
this.options.onDirect?.("open");
|
|
6420
6611
|
this.options.onOpen(wrapDataChannel(dc));
|
|
6612
|
+
tracePair(this.options.myPubKeyZ32, pc);
|
|
6421
6613
|
});
|
|
6422
6614
|
dc.addEventListener("close", () => {
|
|
6423
6615
|
if (this.pc === pc) this.failed();
|
|
@@ -7844,8 +8036,12 @@ var REACTION_LIMITS = {
|
|
|
7844
8036
|
/** Reactions a sender says per window on one chat; the newest of each message waits for room. */
|
|
7845
8037
|
send: 20,
|
|
7846
8038
|
windowMs: 1e4,
|
|
7847
|
-
/**
|
|
7848
|
-
|
|
8039
|
+
/**
|
|
8040
|
+
* A reaction to a message not here yet waits this long for it, at most `buffer` of them per sender in each chat: a
|
|
8041
|
+
* member back in a busy group hears every member's latest reactions as their edges open, before the catch-up (over a
|
|
8042
|
+
* minute for a full log) brings what they name.
|
|
8043
|
+
*/
|
|
8044
|
+
bufferMs: 3e5,
|
|
7849
8045
|
buffer: 64,
|
|
7850
8046
|
/** Reactions of this side not confirmed yet, per chat: the oldest is dropped past it. */
|
|
7851
8047
|
pending: 32,
|
|
@@ -8966,11 +9162,19 @@ var DhtDelivery = class DhtDelivery {
|
|
|
8966
9162
|
timer = null;
|
|
8967
9163
|
/** The first control envelope of a start that waits (`firstControlAfterMs`). */
|
|
8968
9164
|
controlTimer = null;
|
|
9165
|
+
/** A new capability revision to name once the publication spacing allows (`announce`). */
|
|
9166
|
+
announceTimer = null;
|
|
8969
9167
|
chain = Promise.resolve();
|
|
8970
9168
|
lastPublish = 0;
|
|
8971
9169
|
/** The sequence of the last envelope of the contact that was past its expiry (traced once). */
|
|
8972
9170
|
expiredSeen = 0;
|
|
8973
9171
|
controlDue = 0;
|
|
9172
|
+
/**
|
|
9173
|
+
* This side's envelope in the invite's mailbox is sealed with the invite key alone (published before the pin). Once the
|
|
9174
|
+
* contact is pinned, one sealed to it replaces it as soon as the publication spacing allows (`resealDue`): another
|
|
9175
|
+
* holder of the invite reads there that it was taken (`sealedToAnother`), instead of pinning this side on it.
|
|
9176
|
+
*/
|
|
9177
|
+
unsealedOut;
|
|
8974
9178
|
/** Until when the relays' request budget holds publications back: what is due then goes at once. */
|
|
8975
9179
|
budgetUntil = 0;
|
|
8976
9180
|
errors = {};
|
|
@@ -8995,6 +9199,7 @@ var DhtDelivery = class DhtDelivery {
|
|
|
8995
9199
|
const { peerRejected: _peerRejected, ...state } = structuredClone(options.state ?? emptyDhtDeliveryState());
|
|
8996
9200
|
this.state = state;
|
|
8997
9201
|
this.mode = options.mode;
|
|
9202
|
+
this.unsealedOut = state.sequence > 0 && !options.credentials.peerKey;
|
|
8998
9203
|
this.from = identityFromSeedB64(options.params.seedB64).pubKeyZ32;
|
|
8999
9204
|
this.to = options.params.peerPubKeyZ32;
|
|
9000
9205
|
this.participation = identityFromSeedB64(options.credentials.seedB64);
|
|
@@ -9010,7 +9215,7 @@ var DhtDelivery = class DhtDelivery {
|
|
|
9010
9215
|
mode: this.mode,
|
|
9011
9216
|
peerMode: this.state.peerMode,
|
|
9012
9217
|
authenticated: !!this.options.credentials.peerKey,
|
|
9013
|
-
error:
|
|
9218
|
+
error: [this.errors.read, this.errors.publish].filter(Boolean).join(". ") || void 0,
|
|
9014
9219
|
pendingUntil: this.state.pending?.expires,
|
|
9015
9220
|
maxTextBytes: 256,
|
|
9016
9221
|
...this.lastPublished && { lastPublished: this.lastPublished },
|
|
@@ -9144,7 +9349,8 @@ var DhtDelivery = class DhtDelivery {
|
|
|
9144
9349
|
this.running = false;
|
|
9145
9350
|
if (this.timer) clearTimeout(this.timer);
|
|
9146
9351
|
if (this.controlTimer) clearTimeout(this.controlTimer);
|
|
9147
|
-
this.
|
|
9352
|
+
if (this.announceTimer) clearTimeout(this.announceTimer);
|
|
9353
|
+
this.timer = this.controlTimer = this.announceTimer = null;
|
|
9148
9354
|
await this.chain;
|
|
9149
9355
|
}
|
|
9150
9356
|
async setMode(mode) {
|
|
@@ -9417,6 +9623,14 @@ var DhtDelivery = class DhtDelivery {
|
|
|
9417
9623
|
await this.serialize(async () => {
|
|
9418
9624
|
const rev = this.options.capsRev?.();
|
|
9419
9625
|
if (rev === void 0 || rev === this.namedRev) return;
|
|
9626
|
+
const wait = this.lastPublish + 4e3 - Date.now();
|
|
9627
|
+
if (wait > 0) {
|
|
9628
|
+
this.announceTimer ??= setTimeout(() => {
|
|
9629
|
+
this.announceTimer = null;
|
|
9630
|
+
if (!this.live) this.announce();
|
|
9631
|
+
}, wait);
|
|
9632
|
+
return;
|
|
9633
|
+
}
|
|
9420
9634
|
try {
|
|
9421
9635
|
await this.publish(true);
|
|
9422
9636
|
} catch {}
|
|
@@ -9509,13 +9723,14 @@ var DhtDelivery = class DhtDelivery {
|
|
|
9509
9723
|
const pending = this.state.pending && this.state.pending.expires > now && this.state.pending.attempts < MAX_ATTEMPTS ? this.state.pending : void 0;
|
|
9510
9724
|
const receipt = this.state.receipt && this.state.receipt.expires > now && this.state.receipt.attempts < MAX_ATTEMPTS ? this.state.receipt : void 0;
|
|
9511
9725
|
const receiptDue = !!receipt && !receipt.settled && (receipt.next ?? 0) <= now;
|
|
9512
|
-
|
|
9726
|
+
const reseal = this.resealDue;
|
|
9727
|
+
if (!force && (!pending || pending.next > now) && !receiptDue && !this.reactionsDue && !reseal && this.controlDue > now) return;
|
|
9513
9728
|
const expires = pending?.expires ?? now - 12e4 + CONTROL_TTL;
|
|
9514
9729
|
const issued = Math.min(now, Math.max(now - DHT_ISSUED_BACK_MS, expires - (pending ? DHT_MESSAGE_TTL : CONTROL_TTL), (pending?.message[1] ?? 0) - 3e4));
|
|
9515
9730
|
const { body, records, reactions } = this.fitted(this.state.sequence + 1, issued, expires, pending?.message ?? null, receipt?.id ?? null, pending?.reply, pending?.edit, pending?.forwarded, pending?.file);
|
|
9516
9731
|
const reactionsWereDue = this.reactionsDue;
|
|
9517
9732
|
this.reactionsDue = !reactions && !!this.options.reactions?.().length;
|
|
9518
|
-
const identity = this.state.peerPinned && this.state.peerPinned !== "can" && this.pinned()?.identity || this.identity;
|
|
9733
|
+
const identity = !reseal && this.state.peerPinned && this.state.peerPinned !== "can" && this.pinned()?.identity || this.identity;
|
|
9519
9734
|
await this.persist({
|
|
9520
9735
|
...this.state,
|
|
9521
9736
|
sequence: body[1],
|
|
@@ -9550,9 +9765,14 @@ var DhtDelivery = class DhtDelivery {
|
|
|
9550
9765
|
throw error;
|
|
9551
9766
|
}
|
|
9552
9767
|
this.namedRev = body[8] ?? void 0;
|
|
9768
|
+
if (identity === this.identity) this.unsealedOut = !this.options.credentials.peerKey;
|
|
9553
9769
|
delete this.errors.publish;
|
|
9554
9770
|
this.changed();
|
|
9555
9771
|
}
|
|
9772
|
+
/** The contact is pinned and this side's envelope in the invite's mailbox is still unsealed (`unsealedOut`). */
|
|
9773
|
+
get resealDue() {
|
|
9774
|
+
return this.unsealedOut && !!this.options.credentials.peerKey;
|
|
9775
|
+
}
|
|
9556
9776
|
/**
|
|
9557
9777
|
* The relays' request budget held the envelope back: nothing went out, so it was no attempt (a text keeps its eight),
|
|
9558
9778
|
* and what it carried (a text, a receipt, a new mode) goes the moment the budget frees a request: the text and the
|
|
@@ -9823,6 +10043,7 @@ var DhtDelivery = class DhtDelivery {
|
|
|
9823
10043
|
const signal = this.signalReads > 0;
|
|
9824
10044
|
this.urgent = false;
|
|
9825
10045
|
if (signal) this.signalReads--;
|
|
10046
|
+
if (!this.options.credentials.peerKey && !this.options.credentials.expectedPeerKey && !this.state.sequence) await this.publishOnTick();
|
|
9826
10047
|
const readAt = Date.now();
|
|
9827
10048
|
try {
|
|
9828
10049
|
await this.read(background, signal);
|
|
@@ -9832,12 +10053,7 @@ var DhtDelivery = class DhtDelivery {
|
|
|
9832
10053
|
if (!isDiscoveryBudgetError(error) && readAt >= this.networkBackAt) this.errors.read = `Could not read DHT delivery: ${error instanceof Error ? error.message : String(error)}`;
|
|
9833
10054
|
}
|
|
9834
10055
|
if (this.state.peerMode !== "dht") this.signalReads = 0;
|
|
9835
|
-
|
|
9836
|
-
try {
|
|
9837
|
-
await this.publish();
|
|
9838
|
-
} catch (error) {
|
|
9839
|
-
if (!isDiscoveryBudgetError(error) && publishAt >= this.networkBackAt) this.errors.publish = `Could not publish DHT delivery: ${error instanceof Error ? error.message : String(error)}`;
|
|
9840
|
-
}
|
|
10056
|
+
await this.publishOnTick();
|
|
9841
10057
|
this.changed();
|
|
9842
10058
|
});
|
|
9843
10059
|
} finally {
|
|
@@ -9850,6 +10066,14 @@ var DhtDelivery = class DhtDelivery {
|
|
|
9850
10066
|
}
|
|
9851
10067
|
this.schedule();
|
|
9852
10068
|
}
|
|
10069
|
+
async publishOnTick() {
|
|
10070
|
+
const publishAt = Date.now();
|
|
10071
|
+
try {
|
|
10072
|
+
await this.publish();
|
|
10073
|
+
} catch (error) {
|
|
10074
|
+
if (!isDiscoveryBudgetError(error) && publishAt >= this.networkBackAt) this.errors.publish = `Could not publish DHT delivery: ${error instanceof Error ? error.message : String(error)}`;
|
|
10075
|
+
}
|
|
10076
|
+
}
|
|
9853
10077
|
/** How long until the next read of the contact's mailbox (WISP 403, poll pace). */
|
|
9854
10078
|
get pollMs() {
|
|
9855
10079
|
if (this.options.pollMs) return this.options.pollMs;
|
|
@@ -9863,7 +10087,7 @@ var DhtDelivery = class DhtDelivery {
|
|
|
9863
10087
|
if (!this.running || this.ticking) return;
|
|
9864
10088
|
if (this.timer) clearTimeout(this.timer);
|
|
9865
10089
|
const held = this.budgetUntil - Date.now();
|
|
9866
|
-
const due = this.reactionsDue ? Math.max(0, this.lastPublish + 4e3 - Date.now()) : Infinity;
|
|
10090
|
+
const due = this.reactionsDue || this.resealDue && !this.errors.publish ? Math.max(0, this.lastPublish + 4e3 - Date.now(), held) : Infinity;
|
|
9867
10091
|
this.timer = setTimeout(() => void this.tick(), Math.min(held > 0 ? Math.min(this.pollMs, held) : this.pollMs, due));
|
|
9868
10092
|
}
|
|
9869
10093
|
};
|
|
@@ -12044,7 +12268,7 @@ var CapsKeys = class {
|
|
|
12044
12268
|
capabilities,
|
|
12045
12269
|
extensions,
|
|
12046
12270
|
descriptors,
|
|
12047
|
-
name,
|
|
12271
|
+
name: sanitizeNick(name) ?? "",
|
|
12048
12272
|
...choice !== void 0 ? { choice } : {}
|
|
12049
12273
|
};
|
|
12050
12274
|
}
|
|
@@ -12053,14 +12277,17 @@ var emptyCapsState = () => ({ rev: 0 });
|
|
|
12053
12277
|
var digestOf = (content) => toBase64Url(sha256(utf8Encode(JSON.stringify(content)))).slice(0, 22);
|
|
12054
12278
|
/**
|
|
12055
12279
|
* Publishes this side's record (at first start, at every change, hourly) and reads the contact's (at
|
|
12056
|
-
* pairing, when an envelope names a newer revision, when the chat drops to the DHT).
|
|
12057
|
-
*
|
|
12280
|
+
* pairing, when an envelope names a newer revision, when the chat drops to the DHT). Publications go one
|
|
12281
|
+
* at a time, and reads one at a time beside them (a read never waits for a put); reads close together are merged.
|
|
12058
12282
|
*/
|
|
12059
12283
|
var CapsExchange = class {
|
|
12060
12284
|
options;
|
|
12061
12285
|
state;
|
|
12062
12286
|
keys;
|
|
12287
|
+
/** Publications, one at a time; reads of the contact's record (another key) have their own, so neither waits for the other. */
|
|
12063
12288
|
chain = Promise.resolve();
|
|
12289
|
+
readChain = Promise.resolve();
|
|
12290
|
+
saveChain = Promise.resolve();
|
|
12064
12291
|
timer = null;
|
|
12065
12292
|
running = false;
|
|
12066
12293
|
lastRead = 0;
|
|
@@ -12090,14 +12317,21 @@ var CapsExchange = class {
|
|
|
12090
12317
|
get address() {
|
|
12091
12318
|
return this.keys.identity.pubKeyZ32;
|
|
12092
12319
|
}
|
|
12093
|
-
serialize(run) {
|
|
12094
|
-
const next = this
|
|
12095
|
-
this
|
|
12320
|
+
serialize(run, chain = "chain") {
|
|
12321
|
+
const next = this[chain].then(run);
|
|
12322
|
+
this[chain] = next.catch(() => {});
|
|
12096
12323
|
return next;
|
|
12097
12324
|
}
|
|
12098
|
-
|
|
12099
|
-
|
|
12100
|
-
this.
|
|
12325
|
+
/** Saves are one at a time, each over the state as it is then: a read and a publication change different fields. */
|
|
12326
|
+
persist(patch) {
|
|
12327
|
+
return this.serialize(async () => {
|
|
12328
|
+
const next = {
|
|
12329
|
+
...this.state,
|
|
12330
|
+
...patch
|
|
12331
|
+
};
|
|
12332
|
+
await this.options.save(structuredClone(next));
|
|
12333
|
+
this.state = next;
|
|
12334
|
+
}, "saveChain");
|
|
12101
12335
|
}
|
|
12102
12336
|
start() {
|
|
12103
12337
|
if (this.running || this.ended) return;
|
|
@@ -12114,7 +12348,8 @@ var CapsExchange = class {
|
|
|
12114
12348
|
if (this.readTimer) clearTimeout(this.readTimer);
|
|
12115
12349
|
if (this.retryTimer) clearTimeout(this.retryTimer);
|
|
12116
12350
|
this.timer = this.readTimer = this.retryTimer = null;
|
|
12117
|
-
await this.chain.
|
|
12351
|
+
await Promise.all([this.chain, this.readChain]);
|
|
12352
|
+
await this.saveChain;
|
|
12118
12353
|
}
|
|
12119
12354
|
/**
|
|
12120
12355
|
* Publishes when the content changed, when the contact was pinned since the last publication (the
|
|
@@ -12139,7 +12374,6 @@ var CapsExchange = class {
|
|
|
12139
12374
|
const { records, dropped } = this.keys.seal(content, rev, peerKey, now);
|
|
12140
12375
|
this.dropped = dropped;
|
|
12141
12376
|
await this.persist({
|
|
12142
|
-
...this.state,
|
|
12143
12377
|
rev,
|
|
12144
12378
|
digest,
|
|
12145
12379
|
sealedFor: peerKey,
|
|
@@ -12151,10 +12385,7 @@ var CapsExchange = class {
|
|
|
12151
12385
|
this.schedule(isDiscoveryBudgetError(error) ? budgetRetryMs(error, 1e3, 6e4) : 6e4);
|
|
12152
12386
|
throw error;
|
|
12153
12387
|
}
|
|
12154
|
-
await this.persist({
|
|
12155
|
-
...this.state,
|
|
12156
|
-
publishedAt: now
|
|
12157
|
-
});
|
|
12388
|
+
await this.persist({ publishedAt: now });
|
|
12158
12389
|
this.lastOut = content;
|
|
12159
12390
|
if (changed || unpublished) this.options.published?.(rev);
|
|
12160
12391
|
if (dialable) {
|
|
@@ -12212,14 +12443,11 @@ var CapsExchange = class {
|
|
|
12212
12443
|
return this.state.peer;
|
|
12213
12444
|
}
|
|
12214
12445
|
if (this.state.peer && record.rev === this.state.peer.rev && record.author === this.state.peer.author) return this.state.peer;
|
|
12215
|
-
await this.persist({
|
|
12216
|
-
...this.state,
|
|
12217
|
-
peer: record
|
|
12218
|
-
});
|
|
12446
|
+
await this.persist({ peer: record });
|
|
12219
12447
|
this.retries = 0;
|
|
12220
12448
|
this.options.changed?.(record);
|
|
12221
12449
|
return record;
|
|
12222
|
-
}).finally(() => this.readAgainIfBehind());
|
|
12450
|
+
}, "readChain").finally(() => this.readAgainIfBehind());
|
|
12223
12451
|
}
|
|
12224
12452
|
/**
|
|
12225
12453
|
* Read again in a moment, a few times at most, while the record known is short of what the contact has: of the
|
|
@@ -14420,9 +14648,14 @@ var ChatFiles = class {
|
|
|
14420
14648
|
/**
|
|
14421
14649
|
* Removes what this side stored of an incoming file that ends here. Through its target, opened now if need be: after
|
|
14422
14650
|
* a restart a transfer has none until bytes arrive again, and one cancelled before that kept its part on the disk.
|
|
14651
|
+
* After the write under way, and with the ones queued behind it stopped: a write landing after the removal would
|
|
14652
|
+
* store a part of the file again.
|
|
14423
14653
|
*/
|
|
14424
14654
|
discardStored(entry) {
|
|
14425
|
-
|
|
14655
|
+
const incoming = entry.in;
|
|
14656
|
+
if (!incoming) return Promise.resolve();
|
|
14657
|
+
incoming.dropped = true;
|
|
14658
|
+
return incoming.writing.then(() => this.target(entry)).then((t) => t.discard()).catch(() => {});
|
|
14426
14659
|
}
|
|
14427
14660
|
target(entry) {
|
|
14428
14661
|
const incoming = entry.in;
|
|
@@ -14736,7 +14969,7 @@ var ChatFiles = class {
|
|
|
14736
14969
|
incoming.written += bytes.length;
|
|
14737
14970
|
const target = incoming.target;
|
|
14738
14971
|
incoming.writing = incoming.writing.then(async () => {
|
|
14739
|
-
if (transferEnded(record)) return;
|
|
14972
|
+
if (transferEnded(record) || incoming.dropped) return;
|
|
14740
14973
|
await (await target).append(bytes);
|
|
14741
14974
|
incoming.stored = offset + bytes.length;
|
|
14742
14975
|
if (incoming.stored === incoming.written || incoming.stored - incoming.acked >= FILE_LIMITS.ackEveryBytes) {
|
|
@@ -14906,7 +15139,10 @@ var ChatFiles = class {
|
|
|
14906
15139
|
switch (frame.t) {
|
|
14907
15140
|
case "pf-accept":
|
|
14908
15141
|
if (!isInt$1(frame.offset, record.file.size)) return;
|
|
14909
|
-
if (record.state === "paused" && record.pausedBy === "me")
|
|
15142
|
+
if (record.state === "paused" && record.pausedBy === "me") {
|
|
15143
|
+
this.send(this.offerFrame(record, true));
|
|
15144
|
+
return;
|
|
15145
|
+
}
|
|
14910
15146
|
out.generation++;
|
|
14911
15147
|
record.confirmed = frame.offset;
|
|
14912
15148
|
out.next = frame.offset;
|
|
@@ -14920,6 +15156,7 @@ var ChatFiles = class {
|
|
|
14920
15156
|
}
|
|
14921
15157
|
return;
|
|
14922
15158
|
case "pf-wait":
|
|
15159
|
+
if (record.state === "paused" && record.pausedBy === "me") return;
|
|
14923
15160
|
out.generation++;
|
|
14924
15161
|
out.next = record.confirmed;
|
|
14925
15162
|
clearTimeout(out.timer);
|
|
@@ -14966,7 +15203,13 @@ var ChatFiles = class {
|
|
|
14966
15203
|
entry.out.block = void 0;
|
|
14967
15204
|
entry.out.source = void 0;
|
|
14968
15205
|
}
|
|
14969
|
-
if (entry.in)
|
|
15206
|
+
if (entry.in) {
|
|
15207
|
+
const incoming = entry.in;
|
|
15208
|
+
clearTimeout(incoming.timer);
|
|
15209
|
+
incoming.writing.finally(() => {
|
|
15210
|
+
if (transferEnded(record)) incoming.target = void 0;
|
|
15211
|
+
});
|
|
15212
|
+
}
|
|
14970
15213
|
this.changed(entry);
|
|
14971
15214
|
}
|
|
14972
15215
|
entry(direction, id) {
|
|
@@ -16014,6 +16257,15 @@ var PairingTracker = class {
|
|
|
16014
16257
|
const { stage, reason } = this.progress;
|
|
16015
16258
|
if (stage === "publishing" || stage === "failed" && reason === "publish") this.set(this.idle());
|
|
16016
16259
|
}
|
|
16260
|
+
/** This side's first packet waits for the relays' request budget, which frees one at `until`: said, still `publishing`. */
|
|
16261
|
+
heldBack(until) {
|
|
16262
|
+
if (this.progress.stage !== "publishing") return;
|
|
16263
|
+
this.progress = {
|
|
16264
|
+
...this.progress,
|
|
16265
|
+
relayWaitUntil: until
|
|
16266
|
+
};
|
|
16267
|
+
this.changed(this.progress);
|
|
16268
|
+
}
|
|
16017
16269
|
/** The contact's packet was seen: they are on the network. */
|
|
16018
16270
|
sawPeer() {
|
|
16019
16271
|
if (this.peerSeen) return;
|
|
@@ -16045,7 +16297,7 @@ var PairingTracker = class {
|
|
|
16045
16297
|
retryable: true,
|
|
16046
16298
|
...detail ? { detail } : {}
|
|
16047
16299
|
});
|
|
16048
|
-
else this.set(this.idle(), { detail: detail ??
|
|
16300
|
+
else this.set(this.idle(), { detail: detail ?? reason });
|
|
16049
16301
|
return;
|
|
16050
16302
|
}
|
|
16051
16303
|
this.set("failed", {
|
|
@@ -16127,6 +16379,124 @@ var PairingTracker = class {
|
|
|
16127
16379
|
}
|
|
16128
16380
|
};
|
|
16129
16381
|
//#endregion
|
|
16382
|
+
//#region ../core/src/engineErrors.ts
|
|
16383
|
+
/**
|
|
16384
|
+
* The engine's known errors: a stable code each, and its English text with the values in it. The engine throws
|
|
16385
|
+
* `engineError(code, values)`; the text is the one it always wrote, so the CLI and every log keep printing the same.
|
|
16386
|
+
* The app's pages get only the text (it crosses the engine's RPC and is kept on payments and operations as a string),
|
|
16387
|
+
* and `parseEngineError` finds the code and the values back in it, to say it in the app's language. A text that is
|
|
16388
|
+
* none of these (a mint's own words, a provider's) stays as it is.
|
|
16389
|
+
*
|
|
16390
|
+
* `{name}` in a template is a value: a host or domain (no spaces), a network ("Mainnet" or "Testnet"), a chain's name,
|
|
16391
|
+
* an ecash unit ("usd"), or an amount (digits, with the separators of whatever locale wrote it).
|
|
16392
|
+
*/
|
|
16393
|
+
var ENGINE_ERRORS = {
|
|
16394
|
+
mintUnreachable: "Could not reach {host}. Check the address: it should be a Cashu mint.",
|
|
16395
|
+
lnurlUnreachable: "Could not reach {host}. It may be down, or not allow web apps to read it (CORS).",
|
|
16396
|
+
hostUnreachable: "Could not reach {host}",
|
|
16397
|
+
networkUnreachable: "Could not reach the network",
|
|
16398
|
+
hostTimedOut: "{host} did not answer in time",
|
|
16399
|
+
networkTimedOut: "The network did not answer in time",
|
|
16400
|
+
testMintOnMainnet: "This is a test mint, and its sats are worth nothing: add it to a Testnet Cashu wallet",
|
|
16401
|
+
realMintOnTestnet: "This mint holds real sats: add it to a Mainnet Cashu wallet",
|
|
16402
|
+
mintHoldsSats: "Move your sats out of this mint before removing it",
|
|
16403
|
+
lastMint: "This is the last mint of your {network} Cashu wallet: remove the wallet to remove it",
|
|
16404
|
+
invalidMintUrl: "That is not a valid mint URL",
|
|
16405
|
+
mintNotHttps: "Mints must use https",
|
|
16406
|
+
notEnoughSats: "Not enough sats in your wallet",
|
|
16407
|
+
noSharedMint: "You share no mint with this contact",
|
|
16408
|
+
ecashAlreadySpent: "This ecash was already spent somewhere else",
|
|
16409
|
+
ecashOtherUnit: "This ecash is in {unit}: only sat ecash can be redeemed here",
|
|
16410
|
+
reviewedEcashSpent: "The ecash for this payment was already spent somewhere else. Nothing was sent, and your balance now shows what the mint still holds.",
|
|
16411
|
+
reviewedSatsGone: "The sats this payment was reviewed with went to another payment. Nothing was sent: review it again.",
|
|
16412
|
+
lightningFeeTooHigh: "The Lightning fee ({fee} sats) is too high",
|
|
16413
|
+
invoiceExpired: "That invoice has expired",
|
|
16414
|
+
lnurlExactly: "{domain} asks for exactly {amount} sats",
|
|
16415
|
+
lnurlRange: "{domain} takes between {min} and {max} sats",
|
|
16416
|
+
feeAboveLimit: "The fee exceeds your limit",
|
|
16417
|
+
cashuFeeAboveLimit: "Cashu fee exceeds your limit",
|
|
16418
|
+
invalidMaxFee: "Invalid maximum fee",
|
|
16419
|
+
methodUnavailable: "This payment method is unavailable",
|
|
16420
|
+
unknownPaymentIntent: "Unknown payment intent",
|
|
16421
|
+
paymentExpired: "Payment request expired or has an invalid expiry",
|
|
16422
|
+
requestHasPayment: "This request already has a payment. Reconcile it before trying again.",
|
|
16423
|
+
requestPaymentInFlight: "This request already has a payment; reconcile it instead",
|
|
16424
|
+
requestNotAwaiting: "This request is no longer awaiting payment. Check its status before spending.",
|
|
16425
|
+
reconnectBeforeApprove: "Reconnect the data link before approving. Your review was saved.",
|
|
16426
|
+
cannotSubmitAgain: "This payment cannot be submitted again. Reconcile its existing transaction.",
|
|
16427
|
+
alreadySubmitted: "This payment was already submitted or could not be saved",
|
|
16428
|
+
sentAfterReview: "A payment from this wallet was sent after this review was made. Create a new review",
|
|
16429
|
+
cannotCancelSubmitted: "A submitted payment cannot be cancelled; reconcile it instead",
|
|
16430
|
+
outcomeUnknown: "Outcome unknown. Check the existing payment; do not send another.",
|
|
16431
|
+
notYetConfirmed: "Not yet confirmed. No second payment was sent.",
|
|
16432
|
+
couldNotVerify: "Could not verify this payment yet. No second payment was sent.",
|
|
16433
|
+
paymentTakenBack: "This payment was taken back. The sats are in your wallet.",
|
|
16434
|
+
parkedSigned: "Needs your decision: this payment was signed before this copy of your profile took over. It is never sent again from here. Check it before you pay again.",
|
|
16435
|
+
contactAway: "Your contact is offline. Nothing was sent: pay once the chat is live.",
|
|
16436
|
+
realMoneyUnconfirmed: "This pays with real money: confirm it with Send real money first. Nothing was sent.",
|
|
16437
|
+
mainnetPaymentOnTestnet: "This is a Mainnet payment (real money): a Testnet wallet never pays it. Use a Mainnet wallet.",
|
|
16438
|
+
testnetPaymentOnMainnet: "This is a Testnet payment (test coins): a Mainnet wallet never pays it. Use a Testnet wallet.",
|
|
16439
|
+
usdtRpcUnavailable: "USDT RPC unavailable",
|
|
16440
|
+
usdtNotEnoughTokens: "Insufficient token balance",
|
|
16441
|
+
usdtNotEnoughGas: "Insufficient ETH for gas",
|
|
16442
|
+
usdtGasAboveLimit: "Estimated maximum gas exceeds your limit",
|
|
16443
|
+
usdtNeedsSepoliaGas: "This needs a little Sepolia ETH for gas first",
|
|
16444
|
+
fedimintNotInvite: "That is not a Fedimint invite code: it starts with fed1",
|
|
16445
|
+
fedimintAlreadyJoined: "You already joined this federation",
|
|
16446
|
+
fedimintConnecting: "Wait for the federation to connect",
|
|
16447
|
+
fedimintNotEnough: "Not enough in this federation",
|
|
16448
|
+
fedimintRealOnTestnet: "This federation holds real bitcoin: it belongs in a Mainnet Fedimint wallet",
|
|
16449
|
+
fedimintTestOnMainnet: "This federation is on {chain}, a test network: it belongs in a Testnet Fedimint wallet",
|
|
16450
|
+
walletAway: "{wallet} can't be used here. Use it on {device}.",
|
|
16451
|
+
walletAwayUnnamed: "{wallet} can't be used here: its home is another device.",
|
|
16452
|
+
groupTurnUnconfirmed: "Can't check which device is active, so the group was not changed. Try again in a moment."
|
|
16453
|
+
};
|
|
16454
|
+
/**
|
|
16455
|
+
* An error the engine knows: `engineCode` travels with it inside the engine, the text everywhere. Not `code`: the
|
|
16456
|
+
* CLI reads a `code` as its own exit code, and Node's errors carry one of theirs.
|
|
16457
|
+
*/
|
|
16458
|
+
var EngineError = class extends Error {
|
|
16459
|
+
engineCode;
|
|
16460
|
+
values;
|
|
16461
|
+
constructor(engineCode, values = {}) {
|
|
16462
|
+
super(engineText(engineCode, values));
|
|
16463
|
+
this.engineCode = engineCode;
|
|
16464
|
+
this.values = values;
|
|
16465
|
+
}
|
|
16466
|
+
};
|
|
16467
|
+
/** The English text of `code` with its values in it, exactly as the engine writes it. */
|
|
16468
|
+
function engineText(code, values = {}) {
|
|
16469
|
+
return ENGINE_ERRORS[code].replace(/\{(\w+)\}/g, (_, name) => name in values ? String(values[name]) : `{${name}}`);
|
|
16470
|
+
}
|
|
16471
|
+
var engineError = (code, values) => new EngineError(code, values);
|
|
16472
|
+
/** What each kind of value looks like in a text: never a space in a host, a network by its name, an amount as digits. */
|
|
16473
|
+
var VALUE = {
|
|
16474
|
+
host: "(\\S+)",
|
|
16475
|
+
domain: "(\\S+)",
|
|
16476
|
+
chain: "(\\S+)",
|
|
16477
|
+
unit: "(\\S+)",
|
|
16478
|
+
network: "(Mainnet|Testnet)",
|
|
16479
|
+
amount: "(\\d[\\d.,\\u00a0\\u202f' ]*?)",
|
|
16480
|
+
fee: "(\\d[\\d.,\\u00a0\\u202f' ]*?)",
|
|
16481
|
+
min: "(\\d[\\d.,\\u00a0\\u202f' ]*?)",
|
|
16482
|
+
max: "(\\d[\\d.,\\u00a0\\u202f' ]*?)"
|
|
16483
|
+
};
|
|
16484
|
+
var escape = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
16485
|
+
Object.keys(ENGINE_ERRORS).map((code) => {
|
|
16486
|
+
const names = [];
|
|
16487
|
+
const source = ENGINE_ERRORS[code].split(/(\{\w+\})/).map((part) => {
|
|
16488
|
+
const value = /^\{(\w+)\}$/.exec(part);
|
|
16489
|
+
if (!value) return escape(part);
|
|
16490
|
+
names.push(value[1]);
|
|
16491
|
+
return VALUE[value[1]] ?? "(.+?)";
|
|
16492
|
+
}).join("");
|
|
16493
|
+
return {
|
|
16494
|
+
code,
|
|
16495
|
+
names,
|
|
16496
|
+
pattern: new RegExp(`^${source}$`)
|
|
16497
|
+
};
|
|
16498
|
+
});
|
|
16499
|
+
//#endregion
|
|
16130
16500
|
//#region ../core/src/ghostlink.ts
|
|
16131
16501
|
/**
|
|
16132
16502
|
* One link to one peer, complete: Pkarr presence and signaling, the WebRTC
|
|
@@ -16258,7 +16628,7 @@ var QUIET_FRAMES = /* @__PURE__ */ new Set([
|
|
|
16258
16628
|
* `disconnected`) otherwise went on until its consent checks gave up, 27 s after the crash (CI run 36741701666).
|
|
16259
16629
|
*/
|
|
16260
16630
|
var PONG_WAIT_MS = 4e3;
|
|
16261
|
-
/** How long `handled` waits for the contact to answer its ping before it says it
|
|
16631
|
+
/** How long `handled` waits for the contact to answer its ping before it says it is late. */
|
|
16262
16632
|
var HANDLED_WAIT_MS = 1e4;
|
|
16263
16633
|
/**
|
|
16264
16634
|
* Back in front after an absence (`wake`), a live session that ends this soon after it was lost while the app was
|
|
@@ -16321,6 +16691,8 @@ function recordDescriptor(transport, known, record, newer, listed) {
|
|
|
16321
16691
|
relay: r.relay
|
|
16322
16692
|
} : record;
|
|
16323
16693
|
}
|
|
16694
|
+
/** How many keys' signals a one-time enrollment link remembers: past these, further keys are not counted. */
|
|
16695
|
+
var SIGNAL_SIGNERS_MAX = 16;
|
|
16324
16696
|
var PAYMENT_METHODS = [
|
|
16325
16697
|
"cashu",
|
|
16326
16698
|
"lightning",
|
|
@@ -16388,6 +16760,10 @@ var GhostLink = class {
|
|
|
16388
16760
|
signalPublishFailed = false;
|
|
16389
16761
|
peerDescriptors;
|
|
16390
16762
|
peerTransports;
|
|
16763
|
+
/** A group link: this side published a `_tr` on it, in this run or an earlier one (`packetTransportsSaid`). */
|
|
16764
|
+
saidTransports;
|
|
16765
|
+
/** A group link: what is known of the member's transports came from its packet (`_tr`), read now or kept from before. */
|
|
16766
|
+
peerPacketSaid;
|
|
16391
16767
|
peerFallback;
|
|
16392
16768
|
preferred;
|
|
16393
16769
|
fallback;
|
|
@@ -16487,6 +16863,8 @@ var GhostLink = class {
|
|
|
16487
16863
|
}
|
|
16488
16864
|
/** The attempt (`connectionEpoch`) in which the data link began answering the contact's offer. */
|
|
16489
16865
|
answeringEpoch = -1;
|
|
16866
|
+
/** Set while a dial waits for its WebRTC offer to be made: ends that wait, the contact's offer being answered instead. */
|
|
16867
|
+
offerReplaced;
|
|
16490
16868
|
lastAutoConnectAt = 0;
|
|
16491
16869
|
autoConnectFailures = 0;
|
|
16492
16870
|
/** The look due when a failed dial's backoff ends (`lookAtRetry`). */
|
|
@@ -16534,6 +16912,30 @@ var GhostLink = class {
|
|
|
16534
16912
|
lostUntil = 0;
|
|
16535
16913
|
/** Counts the signals a signer was asked to sign: only the latest one goes out. */
|
|
16536
16914
|
signalTurn = 0;
|
|
16915
|
+
/** Puts this side's packet out again as it is: another writer of its key may have replaced it. */
|
|
16916
|
+
republish() {
|
|
16917
|
+
if (!this.stopped) this.session.republish();
|
|
16918
|
+
}
|
|
16919
|
+
/** The keys that signed a signal in the other end's packet, on a link that tells of refused keys (`onPeerKeyRefused`). */
|
|
16920
|
+
signalSigners = /* @__PURE__ */ new Set();
|
|
16921
|
+
/**
|
|
16922
|
+
* A one-time enrollment link (WISP 06 § Adding a device: one session per invite): every device that reads the code
|
|
16923
|
+
* writes the same packet, each signing its signals with its own device signing key, and the later one replaces the
|
|
16924
|
+
* earlier. A validly signed signal by a key other than the one pinned, or than the first one seen there, is a second
|
|
16925
|
+
* device on the code: said once per key, before a pin as after it.
|
|
16926
|
+
*/
|
|
16927
|
+
noteSignalSigner(signal) {
|
|
16928
|
+
const events = this.options.events, credentials = this.options.pairing?.credentials;
|
|
16929
|
+
if (!events?.onPeerKeyRefused || !credentials?.signer || !this.options.params.profile || this.signalSigners.size >= SIGNAL_SIGNERS_MAX) return;
|
|
16930
|
+
if (!verifyPairedSignal(signal, this.options.params.peerPubKeyZ32, this.myPubKeyZ32, void 0, true)) return;
|
|
16931
|
+
const key = JSON.parse(signal).auth.key;
|
|
16932
|
+
if (this.signalSigners.has(key)) return;
|
|
16933
|
+
this.signalSigners.add(key);
|
|
16934
|
+
if (this.signalSigners.size > 1 || credentials.peerKey && key !== credentials.peerKey) {
|
|
16935
|
+
traceLink(this.myPubKeyZ32, "signal-second-key", { keys: this.signalSigners.size });
|
|
16936
|
+
events.onPeerKeyRefused();
|
|
16937
|
+
}
|
|
16938
|
+
}
|
|
16537
16939
|
/** A signal ready to go: on the live session during a switch to WebRTC, in this side's packet otherwise. */
|
|
16538
16940
|
signalOut(signed, report) {
|
|
16539
16941
|
if (this.switcher?.pending?.choices.includes("webrtc/1") && this.channel && this.paired?.state.status === "ready") {
|
|
@@ -16652,6 +17054,8 @@ var GhostLink = class {
|
|
|
16652
17054
|
this.streamWasBlocked = this.streamBlocked;
|
|
16653
17055
|
this.peerDescriptors = options.native?.peerDescriptors ?? {};
|
|
16654
17056
|
this.peerTransports = options.native?.peerTransports;
|
|
17057
|
+
this.saidTransports = !!options.packetTransports && !!options.packetTransportsSaid;
|
|
17058
|
+
this.peerPacketSaid = !!options.packetTransports && !!options.native?.peerTransports;
|
|
16655
17059
|
this.peerFallback = options.native?.peerFallback ?? false;
|
|
16656
17060
|
this.preferred = options.native?.preferred ?? "webrtc/1";
|
|
16657
17061
|
this.fallback = options.native?.fallback ?? true;
|
|
@@ -16665,7 +17069,7 @@ var GhostLink = class {
|
|
|
16665
17069
|
lastSeenTimestamp: options.lastSeenTimestamp,
|
|
16666
17070
|
pollIntervals: options.pollIntervals,
|
|
16667
17071
|
getServices: options.getServices,
|
|
16668
|
-
firstPublish: options.firstPublish ?? (this.tracker && options.pairingProgress?.role === "joiner" || this.resuming ? "after-first-poll" : "at-start"),
|
|
17072
|
+
firstPublish: options.firstPublish ?? (this.tracker && options.pairingProgress?.role === "joiner" && options.rtcAvailable !== false || this.resuming ? "after-first-poll" : "at-start"),
|
|
16669
17073
|
events: {
|
|
16670
17074
|
onFirstPoll: () => {
|
|
16671
17075
|
const online = this.session.peerPresence.online, early = this.firstLookDone;
|
|
@@ -16690,7 +17094,10 @@ var GhostLink = class {
|
|
|
16690
17094
|
this.maybeAutoConnect(presence);
|
|
16691
17095
|
},
|
|
16692
17096
|
onPublish: (result) => {
|
|
16693
|
-
if (result.waiting)
|
|
17097
|
+
if (result.waiting) {
|
|
17098
|
+
if (result.retryInMs !== void 0) this.tracker?.heldBack(Date.now() + result.retryInMs);
|
|
17099
|
+
return;
|
|
17100
|
+
}
|
|
16694
17101
|
if (result.error) {
|
|
16695
17102
|
this.tracker?.failed("publish", true, result.error);
|
|
16696
17103
|
return;
|
|
@@ -16722,6 +17129,7 @@ var GhostLink = class {
|
|
|
16722
17129
|
},
|
|
16723
17130
|
onPeerClock: (packetAt, readBefore, readAt) => events.onPeerClock?.(packetAt, readBefore, readAt),
|
|
16724
17131
|
onPeerTransports: (value) => this.peerPacketTransports(value),
|
|
17132
|
+
onPeerTransportsGone: () => this.peerPacketTransportsGone(),
|
|
16725
17133
|
onDiscoveryError: (error) => events.onDiscoveryError?.(error),
|
|
16726
17134
|
onStatus: (status) => events.onStatus?.(status),
|
|
16727
17135
|
onPoll: (poll) => events.onPoll?.(poll)
|
|
@@ -16802,6 +17210,7 @@ var GhostLink = class {
|
|
|
16802
17210
|
}
|
|
16803
17211
|
if (state === "answering") {
|
|
16804
17212
|
this.answeringEpoch = this.connectionEpoch;
|
|
17213
|
+
this.offerReplaced?.();
|
|
16805
17214
|
if (this.resumingOverRtc) {
|
|
16806
17215
|
const epoch = this.connectionEpoch;
|
|
16807
17216
|
setTimeout(() => this.maybeKnockAnswering(epoch), 0);
|
|
@@ -16836,9 +17245,13 @@ var GhostLink = class {
|
|
|
16836
17245
|
},
|
|
16837
17246
|
attemptTimeoutMs: () => this.tracker && !this.tracker.done ? PAIRING_ATTEMPT_MS : void 0,
|
|
16838
17247
|
onDisconnected: (disconnected) => {
|
|
16839
|
-
if (!disconnected
|
|
17248
|
+
if (!disconnected) {
|
|
17249
|
+
this.session.setDataLinkStalled(false);
|
|
17250
|
+
return;
|
|
17251
|
+
}
|
|
17252
|
+
if (this.activeBinding || !this.channel) return;
|
|
16840
17253
|
traceLink(this.myPubKeyZ32, "rtc-disconnected", {});
|
|
16841
|
-
this.session.
|
|
17254
|
+
this.session.setDataLinkStalled(true);
|
|
16842
17255
|
}
|
|
16843
17256
|
});
|
|
16844
17257
|
this.switcher = new TransportSwitch({
|
|
@@ -16955,6 +17368,7 @@ var GhostLink = class {
|
|
|
16955
17368
|
const firstContact = !!this.options.ownRecords || !!this.options.pairing && !this.options.pairing.credentials.peerKey;
|
|
16956
17369
|
const sight = seen && seen.after === null && !firstContact ? void 0 : seen;
|
|
16957
17370
|
const options = this.options, credentials = options.pairing?.credentials;
|
|
17371
|
+
this.noteSignalSigner(signal);
|
|
16958
17372
|
const verified = options.params.profile ? verifyPairedSignal(signal, options.params.peerPubKeyZ32, this.myPubKeyZ32, credentials?.peerKey, credentials?.requireSignedSignals) : signal;
|
|
16959
17373
|
if (verified && this.predatesLastSession(verified, sight)) return;
|
|
16960
17374
|
if (verified) this.dataLink.handleSignal(verified, sight);
|
|
@@ -17159,6 +17573,7 @@ var GhostLink = class {
|
|
|
17159
17573
|
this.emitDeliveryState();
|
|
17160
17574
|
}
|
|
17161
17575
|
start() {
|
|
17576
|
+
if (this.saidTransports) this.publishPacketTransports();
|
|
17162
17577
|
if (this.deliveryMode !== "dht") this.session.start();
|
|
17163
17578
|
if (this.resuming && this.deliveryMode !== "dht") {
|
|
17164
17579
|
this.session.expectPeer();
|
|
@@ -17177,6 +17592,7 @@ var GhostLink = class {
|
|
|
17177
17592
|
*/
|
|
17178
17593
|
dialEarly() {
|
|
17179
17594
|
if (!this.options.params.profile || !this.options.autoConnect || this.deliveryMode === "dht" || this.streamBlocked || this.dialing || this.channel) return;
|
|
17595
|
+
if (this.options.rtcAvailable === false) return;
|
|
17180
17596
|
const startedAt = this.options.pairingProgress?.startedAt ?? 0;
|
|
17181
17597
|
if (Date.now() - startedAt > 6e4) return;
|
|
17182
17598
|
this.dialGate = new Promise((resolve) => {
|
|
@@ -17626,11 +18042,20 @@ var GhostLink = class {
|
|
|
17626
18042
|
this.maybeAutoConnect(this.presence);
|
|
17627
18043
|
}
|
|
17628
18044
|
}
|
|
17629
|
-
/**
|
|
18045
|
+
/**
|
|
18046
|
+
* A group link's `_tr` in its packet: what this side runs and how to dial it, once it runs a native endpoint there.
|
|
18047
|
+
* Having said one, it goes on saying what it runs with no endpoint up too (`["webrtc/1"]`): the member's app keeps the
|
|
18048
|
+
* last value it read and acts only on a packet that carries one, so a `_tr` that just stopped left it dialling a
|
|
18049
|
+
* native transport nobody listens on, across restarts of both apps (an edge whose WebRTC failed once, node.ts
|
|
18050
|
+
* `edgeWithoutRtc`, in the next run of the app). Never an empty list: an app with no WebRTC whose endpoint is not up
|
|
18051
|
+
* yet says nothing, as before, and what it said in the run before holds.
|
|
18052
|
+
*/
|
|
17630
18053
|
publishPacketTransports() {
|
|
17631
18054
|
if (!this.options.packetTransports) return;
|
|
17632
|
-
const value = this.endpoints.size ? encodePacketTransports(this.availableTransports, this.localDescriptors()) : null;
|
|
17633
18055
|
const signer = this.options.pairing?.credentials.signer;
|
|
18056
|
+
if (this.endpoints.size && !signer) this.saidTransports = true;
|
|
18057
|
+
const available = this.availableTransports;
|
|
18058
|
+
const value = this.endpoints.size || this.saidTransports && available.length ? encodePacketTransports(available, this.localDescriptors()) : null;
|
|
17634
18059
|
if (!signer) {
|
|
17635
18060
|
this.session.setTransports(value);
|
|
17636
18061
|
return;
|
|
@@ -17673,10 +18098,42 @@ var GhostLink = class {
|
|
|
17673
18098
|
if (!said) return;
|
|
17674
18099
|
const transports = said.transports, descriptors = dialDescriptors(said.descriptors);
|
|
17675
18100
|
traceLink(this.myPubKeyZ32, "packet-transports", { transports });
|
|
18101
|
+
this.peerPacketSaid = true;
|
|
17676
18102
|
if (!transports.includes("webrtc/1") && !this.channel && WEBRTC_ATTEMPT.has(this.dataLink.state)) this.disconnect();
|
|
17677
18103
|
this.options.events?.onPacketTransports?.(transports, descriptors);
|
|
17678
18104
|
this.learnPeerTransports(transports, descriptors, true);
|
|
17679
18105
|
}
|
|
18106
|
+
/**
|
|
18107
|
+
* The member's packet advertises and has carried no `_tr` for `TRANSPORTS_GONE_MS`: its app publishes none, which is
|
|
18108
|
+
* what an app with WebRTC that runs no native endpoint on this link does. What an earlier packet said (kept by the
|
|
18109
|
+
* owner across restarts) no longer holds: an app that said `["iroh/1"]` while its WebRTC failed and came back with
|
|
18110
|
+
* WebRTC, from before it said so itself (`packetTransportsSaid`). Left as it was, this side ranked its transports
|
|
18111
|
+
* against that list, found a native one nobody listens on or none at all, and never offered WebRTC again.
|
|
18112
|
+
* Not on a device link: there only a signed value counts, and a missing one is signed by nobody.
|
|
18113
|
+
*/
|
|
18114
|
+
peerPacketTransportsGone() {
|
|
18115
|
+
if (!this.options.packetTransports || this.stopped || !this.peerPacketSaid || this.options.pairing?.credentials.signer) return;
|
|
18116
|
+
this.peerPacketSaid = false;
|
|
18117
|
+
traceLink(this.myPubKeyZ32, "packet-transports-gone", {});
|
|
18118
|
+
this.options.events?.onPacketTransportsGone?.();
|
|
18119
|
+
if (this.channel) return;
|
|
18120
|
+
this.peerTransports = void 0;
|
|
18121
|
+
this.peerRecordTransports = void 0;
|
|
18122
|
+
this.peerFallback = false;
|
|
18123
|
+
this.peerDescriptors = {};
|
|
18124
|
+
this.undescribed.clear();
|
|
18125
|
+
for (const t of TRANSPORTS) if (t !== "webrtc/1") {
|
|
18126
|
+
this.nativeFailures.delete(t);
|
|
18127
|
+
this.demotedUntil.delete(t);
|
|
18128
|
+
}
|
|
18129
|
+
if (this.resuming && this.resuming !== "webrtc/1") {
|
|
18130
|
+
this.resuming = void 0;
|
|
18131
|
+
this.resumingWoke = false;
|
|
18132
|
+
}
|
|
18133
|
+
this.autoConnectFailures = 0;
|
|
18134
|
+
this.lastAutoConnectAt = 0;
|
|
18135
|
+
this.notifyWait();
|
|
18136
|
+
}
|
|
17680
18137
|
/** A connection the contact dialled on one of this side's native endpoints. */
|
|
17681
18138
|
takeDialIn(channel, binding) {
|
|
17682
18139
|
if (this.streamBlocked) {
|
|
@@ -17931,7 +18388,12 @@ var GhostLink = class {
|
|
|
17931
18388
|
} : void 0;
|
|
17932
18389
|
const gate = this.dialGate;
|
|
17933
18390
|
this.dialGate = void 0;
|
|
17934
|
-
|
|
18391
|
+
const replaced = new Promise((resolve) => {
|
|
18392
|
+
this.offerReplaced = resolve;
|
|
18393
|
+
});
|
|
18394
|
+
const made = await Promise.race([this.dataLink.connect(gate), replaced]);
|
|
18395
|
+
this.offerReplaced = void 0;
|
|
18396
|
+
if (made === "held") {
|
|
17935
18397
|
traceLink(this.myPubKeyZ32, "dial-held", {});
|
|
17936
18398
|
this.afterRtc = void 0;
|
|
17937
18399
|
this.offered = void 0;
|
|
@@ -18202,6 +18664,15 @@ var GhostLink = class {
|
|
|
18202
18664
|
canDial(transport) {
|
|
18203
18665
|
return transport !== "webrtc/1" && this.endpoints.has(transport) && !!this.peerDescriptors[transport];
|
|
18204
18666
|
}
|
|
18667
|
+
/**
|
|
18668
|
+
* Whether a dial has anything to try: WebRTC here (the contact's record, once read, listing it), or a native transport
|
|
18669
|
+
* both apps run with this side's endpoint up and the contact's descriptor read. A group link counts every dial: its
|
|
18670
|
+
* fast looks for the member's `_tr` end after a few (`PACKET_TRANSPORTS_FAST_ATTEMPTS`).
|
|
18671
|
+
*/
|
|
18672
|
+
anythingToDial() {
|
|
18673
|
+
if (!this.options.params.profile || this.options.packetTransports) return true;
|
|
18674
|
+
return this.transportOffer().some((t) => t === "webrtc/1" ? !this.peerTransports || this.peerTransports.includes(t) : this.canDial(t) && !!this.peerTransports?.includes(t));
|
|
18675
|
+
}
|
|
18205
18676
|
clearRace() {
|
|
18206
18677
|
if (this.raceTimer) clearTimeout(this.raceTimer);
|
|
18207
18678
|
this.raceTimer = null;
|
|
@@ -18385,7 +18856,7 @@ var GhostLink = class {
|
|
|
18385
18856
|
async request(serviceId, request) {
|
|
18386
18857
|
if (this.options.params.profile) {
|
|
18387
18858
|
await this.connect();
|
|
18388
|
-
if (!this.supportsServices) throw new GhostlyHttpError("unavailable", this.isDataLinkOpen ? "Your contact's app cannot share
|
|
18859
|
+
if (!this.supportsServices) throw new GhostlyHttpError("unavailable", this.isDataLinkOpen ? "Your contact's app cannot share services in this chat, or needs an updated Ghostly" : "Shared services need a live connection");
|
|
18389
18860
|
if (!this.pairedHttp) throw new GhostlyHttpError("closed", "Data link is closed");
|
|
18390
18861
|
return this.pairedHttp.client.request(serviceId, request);
|
|
18391
18862
|
}
|
|
@@ -18515,10 +18986,20 @@ var GhostLink = class {
|
|
|
18515
18986
|
return;
|
|
18516
18987
|
}
|
|
18517
18988
|
traceLink(this.myPubKeyZ32, "dial", { failures: this.autoConnectFailures });
|
|
18989
|
+
this.autoDial();
|
|
18990
|
+
}
|
|
18991
|
+
/**
|
|
18992
|
+
* An automatic dial. One with nothing to try (`anythingToDial`) still says why (`liveAttempt`) and keeps the pace, but
|
|
18993
|
+
* is no failed attempt: the wait between attempts does not grow, and a first pairing does not turn `on-dht` for it.
|
|
18994
|
+
* Two Linux Desktops pairing over a slow DHT, neither having read the other's record yet, counted three such dials in
|
|
18995
|
+
* 12 s: the pairing turned `on-dht` and the next dial waited 160 s, though nothing had been tried.
|
|
18996
|
+
*/
|
|
18997
|
+
autoDial() {
|
|
18998
|
+
const tries = this.anythingToDial();
|
|
18518
18999
|
this.lastAutoConnectAt = Date.now();
|
|
18519
|
-
this.autoConnectFailures++;
|
|
19000
|
+
if (tries) this.autoConnectFailures++;
|
|
18520
19001
|
this.dial().catch((error) => {
|
|
18521
|
-
this.tracker?.failed("transport", true);
|
|
19002
|
+
if (tries) this.tracker?.failed("transport", true);
|
|
18522
19003
|
this.dialFailed(error instanceof Error ? error.message : String(error));
|
|
18523
19004
|
this.lookAtRetry();
|
|
18524
19005
|
});
|
|
@@ -18535,13 +19016,7 @@ var GhostLink = class {
|
|
|
18535
19016
|
failures: this.autoConnectFailures,
|
|
18536
19017
|
again: true
|
|
18537
19018
|
});
|
|
18538
|
-
this.
|
|
18539
|
-
this.autoConnectFailures++;
|
|
18540
|
-
this.dial().catch((error) => {
|
|
18541
|
-
this.tracker?.failed("transport", true);
|
|
18542
|
-
this.dialFailed(error instanceof Error ? error.message : String(error));
|
|
18543
|
-
this.lookAtRetry();
|
|
18544
|
-
});
|
|
19019
|
+
this.autoDial();
|
|
18545
19020
|
}
|
|
18546
19021
|
/**
|
|
18547
19022
|
* A failed dial waits `dialWait` before the next, and only a look at the contact's record dials: the next one came at
|
|
@@ -19190,8 +19665,17 @@ var GhostLink = class {
|
|
|
19190
19665
|
}));
|
|
19191
19666
|
} catch {}
|
|
19192
19667
|
}
|
|
19668
|
+
/**
|
|
19669
|
+
* The contact said goodbye on the last live session (its app quit or stopped) and has published nothing since but its
|
|
19670
|
+
* leave packet: its app is not running, so a dial now waits for nobody until it gives up.
|
|
19671
|
+
*/
|
|
19672
|
+
get contactAway() {
|
|
19673
|
+
const presence = this.session.peerPresence;
|
|
19674
|
+
return !this.isDataLinkOpen && !!this.departed && (presence.lastPacketAt === this.departed.packet || !presence.online);
|
|
19675
|
+
}
|
|
19193
19676
|
async requirePaymentSupport() {
|
|
19194
19677
|
if (!PAYMENT_METHODS.some((m) => this.paymentEnabled(m))) throw new Error("Payments are turned off in this chat.");
|
|
19678
|
+
if (this.contactAway) throw engineError("contactAway");
|
|
19195
19679
|
await this.connect();
|
|
19196
19680
|
if (!this.supportsPayments) throw new Error("This contact does not support payments. Both peers need an updated Ghostly.");
|
|
19197
19681
|
}
|
|
@@ -19977,7 +20461,8 @@ var GhostLink = class {
|
|
|
19977
20461
|
* Resolves once the contact's app handled every frame sent on this session so far: it answers a ping in the order
|
|
19978
20462
|
* frames come (every app since 1.0), so the pong to one sent now says so, however slowly it handles them. A group
|
|
19979
20463
|
* member's catch-up answer goes a slice at a time on it (`GroupSessionHooks.handled`). `false` when it cannot tell:
|
|
19980
|
-
* no session, a contact that answers no ping
|
|
20464
|
+
* no session, or a contact that answers no ping. `"late"` with no answer within `HANDLED_WAIT_MS`: a contact that
|
|
20465
|
+
* answers pings is still handling what came before (a phone with a long history), not one that cannot say.
|
|
19981
20466
|
*/
|
|
19982
20467
|
handled() {
|
|
19983
20468
|
const channel = this.channel;
|
|
@@ -19997,7 +20482,7 @@ var GhostLink = class {
|
|
|
19997
20482
|
resolve(exact);
|
|
19998
20483
|
}
|
|
19999
20484
|
};
|
|
20000
|
-
const timer = setTimeout(() => waiter.done(
|
|
20485
|
+
const timer = setTimeout(() => waiter.done("late"), HANDLED_WAIT_MS);
|
|
20001
20486
|
this.handledWaiters.push(waiter);
|
|
20002
20487
|
});
|
|
20003
20488
|
}
|
|
@@ -20491,131 +20976,41 @@ function decodeBolt11(text) {
|
|
|
20491
20976
|
};
|
|
20492
20977
|
}
|
|
20493
20978
|
//#endregion
|
|
20494
|
-
//#region ../core/src/engineErrors.ts
|
|
20495
|
-
/**
|
|
20496
|
-
* The engine's known errors: a stable code each, and its English text with the values in it. The engine throws
|
|
20497
|
-
* `engineError(code, values)`; the text is the one it always wrote, so the CLI and every log keep printing the same.
|
|
20498
|
-
* The app's pages get only the text (it crosses the engine's RPC and is kept on payments and operations as a string),
|
|
20499
|
-
* and `parseEngineError` finds the code and the values back in it, to say it in the app's language. A text that is
|
|
20500
|
-
* none of these (a mint's own words, a provider's) stays as it is.
|
|
20501
|
-
*
|
|
20502
|
-
* `{name}` in a template is a value: a host or domain (no spaces), a network ("Mainnet" or "Testnet"), a chain's name,
|
|
20503
|
-
* or an amount (digits, with the separators of whatever locale wrote it).
|
|
20504
|
-
*/
|
|
20505
|
-
var ENGINE_ERRORS = {
|
|
20506
|
-
mintUnreachable: "Could not reach {host}. Check the address: it should be a Cashu mint.",
|
|
20507
|
-
lnurlUnreachable: "Could not reach {host}. It may be down, or not allow web apps to read it (CORS).",
|
|
20508
|
-
hostUnreachable: "Could not reach {host}",
|
|
20509
|
-
networkUnreachable: "Could not reach the network",
|
|
20510
|
-
hostTimedOut: "{host} did not answer in time",
|
|
20511
|
-
networkTimedOut: "The network did not answer in time",
|
|
20512
|
-
testMintOnMainnet: "This is a test mint, and its sats are worth nothing: add it to a Testnet Cashu wallet",
|
|
20513
|
-
realMintOnTestnet: "This mint holds real sats: add it to a Mainnet Cashu wallet",
|
|
20514
|
-
mintHoldsSats: "Move your sats out of this mint before removing it",
|
|
20515
|
-
lastMint: "This is the last mint of your {network} Cashu wallet: remove the wallet to remove it",
|
|
20516
|
-
invalidMintUrl: "That is not a valid mint URL",
|
|
20517
|
-
mintNotHttps: "Mints must use https",
|
|
20518
|
-
notEnoughSats: "Not enough sats in your wallet",
|
|
20519
|
-
noSharedMint: "You share no mint with this contact",
|
|
20520
|
-
ecashAlreadySpent: "This ecash was already spent somewhere else",
|
|
20521
|
-
reviewedEcashSpent: "The ecash for this payment was already spent somewhere else. Nothing was sent, and your balance now shows what the mint still holds.",
|
|
20522
|
-
reviewedSatsGone: "The sats this payment was reviewed with went to another payment. Nothing was sent: review it again.",
|
|
20523
|
-
lightningFeeTooHigh: "The Lightning fee ({fee} sats) is too high",
|
|
20524
|
-
invoiceExpired: "That invoice has expired",
|
|
20525
|
-
lnurlExactly: "{domain} asks for exactly {amount} sats",
|
|
20526
|
-
lnurlRange: "{domain} takes between {min} and {max} sats",
|
|
20527
|
-
feeAboveLimit: "The fee exceeds your limit",
|
|
20528
|
-
cashuFeeAboveLimit: "Cashu fee exceeds your limit",
|
|
20529
|
-
invalidMaxFee: "Invalid maximum fee",
|
|
20530
|
-
methodUnavailable: "This payment method is unavailable",
|
|
20531
|
-
unknownPaymentIntent: "Unknown payment intent",
|
|
20532
|
-
paymentExpired: "Payment request expired or has an invalid expiry",
|
|
20533
|
-
requestHasPayment: "This request already has a payment. Reconcile it before trying again.",
|
|
20534
|
-
requestPaymentInFlight: "This request already has a payment; reconcile it instead",
|
|
20535
|
-
requestNotAwaiting: "This request is no longer awaiting payment. Check its status before spending.",
|
|
20536
|
-
reconnectBeforeApprove: "Reconnect the data link before approving. Your review was saved.",
|
|
20537
|
-
cannotSubmitAgain: "This payment cannot be submitted again. Reconcile its existing transaction.",
|
|
20538
|
-
alreadySubmitted: "This payment was already submitted or could not be saved",
|
|
20539
|
-
sentAfterReview: "A payment from this wallet was sent after this review was made. Create a new review",
|
|
20540
|
-
cannotCancelSubmitted: "A submitted payment cannot be cancelled; reconcile it instead",
|
|
20541
|
-
outcomeUnknown: "Outcome unknown. Check the existing payment; do not send another.",
|
|
20542
|
-
notYetConfirmed: "Not yet confirmed. No second payment was sent.",
|
|
20543
|
-
couldNotVerify: "Could not verify this payment yet. No second payment was sent.",
|
|
20544
|
-
paymentTakenBack: "This payment was taken back. The sats are in your wallet.",
|
|
20545
|
-
parkedSigned: "Needs your decision: this payment was signed before this copy of your profile took over. It is never sent again from here. Check it before you pay again.",
|
|
20546
|
-
realMoneyUnconfirmed: "This pays with real money: confirm it with Send real money first. Nothing was sent.",
|
|
20547
|
-
mainnetPaymentOnTestnet: "This is a Mainnet payment (real money): a Testnet wallet never pays it. Use a Mainnet wallet.",
|
|
20548
|
-
testnetPaymentOnMainnet: "This is a Testnet payment (test coins): a Mainnet wallet never pays it. Use a Testnet wallet.",
|
|
20549
|
-
usdtRpcUnavailable: "USDT RPC unavailable",
|
|
20550
|
-
usdtNotEnoughTokens: "Insufficient token balance",
|
|
20551
|
-
usdtNotEnoughGas: "Insufficient ETH for gas",
|
|
20552
|
-
usdtGasAboveLimit: "Estimated maximum gas exceeds your limit",
|
|
20553
|
-
usdtNeedsSepoliaGas: "This needs a little Sepolia ETH for gas first",
|
|
20554
|
-
fedimintNotInvite: "That is not a Fedimint invite code: it starts with fed1",
|
|
20555
|
-
fedimintAlreadyJoined: "You already joined this federation",
|
|
20556
|
-
fedimintConnecting: "Wait for the federation to connect",
|
|
20557
|
-
fedimintNotEnough: "Not enough in this federation",
|
|
20558
|
-
fedimintRealOnTestnet: "This federation holds real bitcoin: it belongs in a Mainnet Fedimint wallet",
|
|
20559
|
-
fedimintTestOnMainnet: "This federation is on {chain}, a test network: it belongs in a Testnet Fedimint wallet",
|
|
20560
|
-
walletAway: "{wallet} can't be used here. Use it on {device}.",
|
|
20561
|
-
walletAwayUnnamed: "{wallet} can't be used here: its home is another device.",
|
|
20562
|
-
groupTurnUnconfirmed: "Can't check which device is active, so the group was not changed. Try again in a moment."
|
|
20563
|
-
};
|
|
20564
|
-
/**
|
|
20565
|
-
* An error the engine knows: `engineCode` travels with it inside the engine, the text everywhere. Not `code`: the
|
|
20566
|
-
* CLI reads a `code` as its own exit code, and Node's errors carry one of theirs.
|
|
20567
|
-
*/
|
|
20568
|
-
var EngineError = class extends Error {
|
|
20569
|
-
engineCode;
|
|
20570
|
-
values;
|
|
20571
|
-
constructor(engineCode, values = {}) {
|
|
20572
|
-
super(engineText(engineCode, values));
|
|
20573
|
-
this.engineCode = engineCode;
|
|
20574
|
-
this.values = values;
|
|
20575
|
-
}
|
|
20576
|
-
};
|
|
20577
|
-
/** The English text of `code` with its values in it, exactly as the engine writes it. */
|
|
20578
|
-
function engineText(code, values = {}) {
|
|
20579
|
-
return ENGINE_ERRORS[code].replace(/\{(\w+)\}/g, (_, name) => name in values ? String(values[name]) : `{${name}}`);
|
|
20580
|
-
}
|
|
20581
|
-
var engineError = (code, values) => new EngineError(code, values);
|
|
20582
|
-
/** What each kind of value looks like in a text: never a space in a host, a network by its name, an amount as digits. */
|
|
20583
|
-
var VALUE = {
|
|
20584
|
-
host: "(\\S+)",
|
|
20585
|
-
domain: "(\\S+)",
|
|
20586
|
-
chain: "(\\S+)",
|
|
20587
|
-
network: "(Mainnet|Testnet)",
|
|
20588
|
-
amount: "(\\d[\\d.,\\u00a0\\u202f' ]*?)",
|
|
20589
|
-
fee: "(\\d[\\d.,\\u00a0\\u202f' ]*?)",
|
|
20590
|
-
min: "(\\d[\\d.,\\u00a0\\u202f' ]*?)",
|
|
20591
|
-
max: "(\\d[\\d.,\\u00a0\\u202f' ]*?)"
|
|
20592
|
-
};
|
|
20593
|
-
var escape = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
20594
|
-
Object.keys(ENGINE_ERRORS).map((code) => {
|
|
20595
|
-
const names = [];
|
|
20596
|
-
const source = ENGINE_ERRORS[code].split(/(\{\w+\})/).map((part) => {
|
|
20597
|
-
const value = /^\{(\w+)\}$/.exec(part);
|
|
20598
|
-
if (!value) return escape(part);
|
|
20599
|
-
names.push(value[1]);
|
|
20600
|
-
return VALUE[value[1]] ?? "(.+?)";
|
|
20601
|
-
}).join("");
|
|
20602
|
-
return {
|
|
20603
|
-
code,
|
|
20604
|
-
names,
|
|
20605
|
-
pattern: new RegExp(`^${source}$`)
|
|
20606
|
-
};
|
|
20607
|
-
});
|
|
20608
|
-
//#endregion
|
|
20609
20979
|
//#region ../core/src/lnurl.ts
|
|
20610
20980
|
var LNURL_LIMITS = {
|
|
20611
20981
|
timeoutMs: 15e3,
|
|
20612
20982
|
maxBytes: 65536,
|
|
20983
|
+
maxRedirects: 5,
|
|
20613
20984
|
maxUrlLength: 2048,
|
|
20614
20985
|
maxMetadataLength: 8192,
|
|
20615
20986
|
maxComment: 2e3
|
|
20616
20987
|
};
|
|
20617
20988
|
var ADDRESS = /^([a-z0-9._+-]{1,64})@([a-z0-9-]+(?:\.[a-z0-9-]+)*(?::\d{1,5})?)$/i;
|
|
20618
20989
|
var LNURL_BECH32 = /^lnurl1[02-9ac-hj-np-z]{6,}$/i;
|
|
20990
|
+
var REDIRECTS = [
|
|
20991
|
+
301,
|
|
20992
|
+
302,
|
|
20993
|
+
303,
|
|
20994
|
+
307,
|
|
20995
|
+
308
|
|
20996
|
+
];
|
|
20997
|
+
/**
|
|
20998
|
+
* A host that is not on the public internet, as far as its name tells: this machine, a single label, `.local` and its
|
|
20999
|
+
* kin, private and reserved IP addresses. (What a public name resolves to is checked where the client resolves names
|
|
21000
|
+
* itself, as Ghostly Desktop does.)
|
|
21001
|
+
*/
|
|
21002
|
+
function isLocalHost(hostname) {
|
|
21003
|
+
const host = hostname.toLowerCase().replace(/\.$/, "").replace(/^\[|\]$/g, "");
|
|
21004
|
+
if (!host.includes(".") && !host.includes(":")) return true;
|
|
21005
|
+
if (/(^|\.)(localhost|local|internal|lan|home\.arpa)$/.test(host)) return true;
|
|
21006
|
+
const v4 = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
|
|
21007
|
+
if (v4) {
|
|
21008
|
+
const [a, b] = [Number(v4[1]), Number(v4[2])];
|
|
21009
|
+
return a === 0 || a === 10 || a === 127 || a >= 224 || a === 169 && b === 254 || a === 172 && b >= 16 && b < 32 || a === 192 && b === 168 || a === 100 && b >= 64 && b < 128 || a === 198 && (b === 18 || b === 19) || a === 192 && b === 0;
|
|
21010
|
+
}
|
|
21011
|
+
if (host.includes(":")) return !/^[23][0-9a-f]{0,3}:/.test(host) || host.includes(".") || /^2001:0?db8:/.test(host);
|
|
21012
|
+
return false;
|
|
21013
|
+
}
|
|
20619
21014
|
var isLoopback = (hostname) => [
|
|
20620
21015
|
"localhost",
|
|
20621
21016
|
"127.0.0.1",
|
|
@@ -20709,7 +21104,7 @@ async function readBounded(response, maxBytes) {
|
|
|
20709
21104
|
}
|
|
20710
21105
|
return new TextDecoder().decode(all);
|
|
20711
21106
|
}
|
|
20712
|
-
/** One GET to an LNURL service: bounded in time and size, no credentials, no referrer, and the answer must be JSON. */
|
|
21107
|
+
/** One GET to an LNURL service (and to each checked redirect of it): bounded in time and size, no credentials, no referrer, and the answer must be JSON. */
|
|
20713
21108
|
async function fetchLnurlJson(url, options = {}) {
|
|
20714
21109
|
const target = acceptUrl(url);
|
|
20715
21110
|
const doFetch = options.fetch ?? globalThis.fetch;
|
|
@@ -20720,19 +21115,38 @@ async function fetchLnurlJson(url, options = {}) {
|
|
|
20720
21115
|
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
20721
21116
|
try {
|
|
20722
21117
|
let response;
|
|
20723
|
-
|
|
20724
|
-
|
|
20725
|
-
|
|
20726
|
-
|
|
20727
|
-
|
|
20728
|
-
|
|
20729
|
-
|
|
20730
|
-
|
|
20731
|
-
|
|
20732
|
-
|
|
20733
|
-
|
|
21118
|
+
for (let at = target, hops = 0;; hops++) {
|
|
21119
|
+
try {
|
|
21120
|
+
response = await doFetch(at.href, {
|
|
21121
|
+
signal: controller.signal,
|
|
21122
|
+
headers: { accept: "application/json" },
|
|
21123
|
+
credentials: "omit",
|
|
21124
|
+
referrerPolicy: "no-referrer",
|
|
21125
|
+
redirect: "manual"
|
|
21126
|
+
});
|
|
21127
|
+
} catch {
|
|
21128
|
+
if (controller.signal.aborted) throw controller.signal.reason instanceof Error ? controller.signal.reason : /* @__PURE__ */ new Error("The request was cancelled");
|
|
21129
|
+
throw engineError("lnurlUnreachable", { host: target.host });
|
|
21130
|
+
}
|
|
21131
|
+
if (response.url) acceptUrl(response.url);
|
|
21132
|
+
if (response.type === "opaqueredirect") throw new Error(`${target.host} redirects to another address, which cannot be checked here`);
|
|
21133
|
+
const location = REDIRECTS.includes(response.status) ? response.headers.get("location") : null;
|
|
21134
|
+
if (!location) break;
|
|
21135
|
+
await response.body?.cancel().catch(() => {});
|
|
21136
|
+
if (hops >= LNURL_LIMITS.maxRedirects) throw new Error(`${target.host} redirects too many times`);
|
|
21137
|
+
let next;
|
|
21138
|
+
try {
|
|
21139
|
+
next = new URL(location, at);
|
|
21140
|
+
} catch {
|
|
21141
|
+
throw new Error(`${target.host} redirects to an invalid address`);
|
|
21142
|
+
}
|
|
21143
|
+
next = acceptUrl(next.href);
|
|
21144
|
+
if (next.host !== at.host) {
|
|
21145
|
+
if (next.protocol !== "https:") throw new Error("Lightning addresses and LNURLs must use HTTPS");
|
|
21146
|
+
if (isLocalHost(next.hostname)) throw new Error(`${target.host} redirects to a local address`);
|
|
21147
|
+
}
|
|
21148
|
+
at = next;
|
|
20734
21149
|
}
|
|
20735
|
-
if (response.url) acceptUrl(response.url);
|
|
20736
21150
|
const text = await readBounded(response, options.maxBytes ?? LNURL_LIMITS.maxBytes);
|
|
20737
21151
|
let body;
|
|
20738
21152
|
try {
|
|
@@ -24404,9 +24818,10 @@ var CatchUpAnswers = class {
|
|
|
24404
24818
|
return;
|
|
24405
24819
|
}
|
|
24406
24820
|
const rest = [...frames], told = [];
|
|
24821
|
+
const ask = () => handled(to).catch(() => false);
|
|
24407
24822
|
const next = () => {
|
|
24408
24823
|
if (!this.sendSlice(to, rest)) return false;
|
|
24409
|
-
if (rest.length) told.push(
|
|
24824
|
+
if (rest.length) told.push(ask());
|
|
24410
24825
|
return true;
|
|
24411
24826
|
};
|
|
24412
24827
|
if (!next() || !next() || !rest.length) return;
|
|
@@ -24414,7 +24829,9 @@ var CatchUpAnswers = class {
|
|
|
24414
24829
|
(async () => {
|
|
24415
24830
|
try {
|
|
24416
24831
|
while (rest.length) {
|
|
24417
|
-
|
|
24832
|
+
let answer = await told.shift();
|
|
24833
|
+
while (answer === "late" && this.hooks.active()) answer = await ask();
|
|
24834
|
+
if (!answer) await new Promise((resolve) => setTimeout(resolve, 500));
|
|
24418
24835
|
if (!this.hooks.active() || !next()) break;
|
|
24419
24836
|
}
|
|
24420
24837
|
} finally {
|
|
@@ -24783,6 +25200,8 @@ var GroupSession = class {
|
|
|
24783
25200
|
pendingCommits = /* @__PURE__ */ new Map();
|
|
24784
25201
|
/** When each member was last asked to catch me up, so a stream of unreadable frames is one question, not a loop. */
|
|
24785
25202
|
asked = /* @__PURE__ */ new Map();
|
|
25203
|
+
/** The signed leave of each member I last passed on, kept for an admin that was away (`byesHeld`). In memory only. */
|
|
25204
|
+
byesPassed = /* @__PURE__ */ new Map();
|
|
24786
25205
|
queue = Promise.resolve();
|
|
24787
25206
|
/** One metadata frame that names a commit or an epoch I do not have yet: tried again when the chain moves. */
|
|
24788
25207
|
pendingMeta;
|
|
@@ -24998,6 +25417,38 @@ var GroupSession = class {
|
|
|
24998
25417
|
const lowest = Math.max(0, entry.high - GROUP_LIMITS.window + 1);
|
|
24999
25418
|
return Math.max(0, entry.high - lowest + 1 - (1 + entry.window.filter((n) => n >= lowest).length));
|
|
25000
25419
|
}
|
|
25420
|
+
/**
|
|
25421
|
+
* The other members some of whose messages below the highest seen never arrived (what a sync names in `miss`), each
|
|
25422
|
+
* with a mark that changes when what is missing does: an author answers only from its own last `outlog`, so the rest
|
|
25423
|
+
* is for another member to hand on.
|
|
25424
|
+
*/
|
|
25425
|
+
lacking() {
|
|
25426
|
+
const out = /* @__PURE__ */ new Map();
|
|
25427
|
+
for (const [sender, epochs] of Object.entries(this.state.seen)) {
|
|
25428
|
+
if (sender === this.myKey || !rosterHas(this.roster, sender)) continue;
|
|
25429
|
+
const marks = Object.entries(epochs).filter(([, entry]) => this.gaps(entry).length).map(([e, entry]) => `${e}:${entry.high}:${entry.window.length}`);
|
|
25430
|
+
if (marks.length) out.set(sender, marks.join(","));
|
|
25431
|
+
}
|
|
25432
|
+
return out;
|
|
25433
|
+
}
|
|
25434
|
+
/** What a sync names in `miss` for one sender now: per epoch, the newest `GROUP_LIMITS.miss` numbers that never arrived. */
|
|
25435
|
+
missOf(sender) {
|
|
25436
|
+
const out = {};
|
|
25437
|
+
for (const [e, entry] of Object.entries(this.state.seen[sender] ?? {})) {
|
|
25438
|
+
const gaps = this.gaps(entry);
|
|
25439
|
+
if (gaps.length) out[e] = gaps;
|
|
25440
|
+
}
|
|
25441
|
+
return out;
|
|
25442
|
+
}
|
|
25443
|
+
/** Whether any of `asked` (per epoch, as `missOf` gave them) arrived since: the member asked holds what is missing. */
|
|
25444
|
+
filledSince(sender, asked) {
|
|
25445
|
+
return Object.entries(asked).some(([e, numbers]) => {
|
|
25446
|
+
const entry = this.state.seen[sender]?.[e];
|
|
25447
|
+
if (!entry) return false;
|
|
25448
|
+
const got = new Set(entry.window);
|
|
25449
|
+
return numbers.some((n) => got.has(n));
|
|
25450
|
+
});
|
|
25451
|
+
}
|
|
25001
25452
|
serialize(run) {
|
|
25002
25453
|
const operation = this.queue.then(run);
|
|
25003
25454
|
this.queue = operation.then(() => {}, () => {});
|
|
@@ -25632,8 +26083,20 @@ var GroupSession = class {
|
|
|
25632
26083
|
}
|
|
25633
26084
|
if (bye.e < this.admittedAt(bye.k)) return;
|
|
25634
26085
|
if (this.isAdmin && this.hooks.adminWork?.() !== false && await this.commitUnlessTurnUnconfirmed("remove", bye.k)) return;
|
|
26086
|
+
if (from !== bye.k && this.byesPassed.get(bye.k)?.sig === bye.sig) return;
|
|
26087
|
+
this.byesPassed.set(bye.k, bye);
|
|
25635
26088
|
this.took(bye);
|
|
25636
26089
|
}
|
|
26090
|
+
/**
|
|
26091
|
+
* The signed leaves I passed on whose members are still in the roster: the admin was away when they went round, so
|
|
26092
|
+
* they are handed to it again when its edge opens. One goes with the commit that removes its member.
|
|
26093
|
+
*/
|
|
26094
|
+
byesHeld() {
|
|
26095
|
+
const held = [];
|
|
26096
|
+
for (const [key, bye] of this.byesPassed) if (rosterHas(this.roster, key) && bye.e >= this.admittedAt(key)) held.push(bye);
|
|
26097
|
+
else this.byesPassed.delete(key);
|
|
26098
|
+
return held;
|
|
26099
|
+
}
|
|
25637
26100
|
/** The epoch of the commit that last admitted `key` (0 for the creator, whom no commit admits). */
|
|
25638
26101
|
admittedAt(key) {
|
|
25639
26102
|
const chain = this.state.chain;
|
|
@@ -26155,7 +26618,7 @@ var GroupSession = class {
|
|
|
26155
26618
|
this.state.meta = opened.meta;
|
|
26156
26619
|
await this.persist();
|
|
26157
26620
|
const change = !before && !rosterHas(commit.m, this.myKey) ? null : groupMetaChange(before, opened.meta, this.state.name);
|
|
26158
|
-
if (change) this.hooks.metaChanged?.(
|
|
26621
|
+
if (change) this.hooks.metaChanged?.(this.metaAuthor(before, s), change, opened.meta.ts);
|
|
26159
26622
|
this.hooks.changed();
|
|
26160
26623
|
this.took({
|
|
26161
26624
|
t: "group-meta",
|
|
@@ -26165,6 +26628,16 @@ var GroupSession = class {
|
|
|
26165
26628
|
c: frame.c
|
|
26166
26629
|
});
|
|
26167
26630
|
}
|
|
26631
|
+
/**
|
|
26632
|
+
* Who made the change a statement brings me: its signer, when they were the admin all along since the statement I
|
|
26633
|
+
* held (or, with none, since I got in). A new admin signs again what the last one set (`metaFollowsChain`): to a
|
|
26634
|
+
* member that missed that, it is no change of theirs it can tell apart, so nobody is named.
|
|
26635
|
+
*/
|
|
26636
|
+
metaAuthor(before, s) {
|
|
26637
|
+
const chain = this.state.chain, from = before ? before.e : chain.findIndex((c) => rosterHas(c.m, this.myKey));
|
|
26638
|
+
for (let e = Math.max(0, from); e <= s.e; e++) if (rosterAdmin(chain[e].m) !== s.by) return void 0;
|
|
26639
|
+
return s.by;
|
|
26640
|
+
}
|
|
26168
26641
|
/** After the chain or my secrets moved: the waiting statement, and, if I became the admin, the name and picture signed again as mine. */
|
|
26169
26642
|
async metaFollowsChain() {
|
|
26170
26643
|
const pending = this.pendingMeta;
|
|
@@ -26528,8 +27001,11 @@ function parseCommit(raw) {
|
|
|
26528
27001
|
if (c.ls !== void 0) out.ls = c.ls;
|
|
26529
27002
|
return out;
|
|
26530
27003
|
}
|
|
26531
|
-
/**
|
|
26532
|
-
|
|
27004
|
+
/**
|
|
27005
|
+
* The roster a commit of this kind makes of the previous one; null when it is not allowed. `trusted`: the commit was
|
|
27006
|
+
* verified when it was accepted (a replay of the stored main branch), so a leave's statement is not checked again.
|
|
27007
|
+
*/
|
|
27008
|
+
function communityRoster(previous, c, trusted = false) {
|
|
26533
27009
|
if (c.k === "create") return previous === null && !c.s && !!c.x && !!c.n ? [[c.by, "admin"]] : null;
|
|
26534
27010
|
if (!previous || !rosterHas(previous, c.by)) return null;
|
|
26535
27011
|
const admin = rosterAdmin(previous);
|
|
@@ -26539,7 +27015,7 @@ function communityRoster(previous, c) {
|
|
|
26539
27015
|
return sortRoster([...previous, [c.s, "member"]]);
|
|
26540
27016
|
case "leave":
|
|
26541
27017
|
if (!c.s || c.s === c.by || c.s === admin || !rosterHas(previous, c.s) || !c.ls) return null;
|
|
26542
|
-
if (!verify(fromBase64Url(c.ls), leaveStatement(c.g, c.s), publicKeyFromZ32(c.s))) return null;
|
|
27018
|
+
if (!trusted && !verify(fromBase64Url(c.ls), leaveStatement(c.g, c.s), publicKeyFromZ32(c.s))) return null;
|
|
26543
27019
|
return previous.filter(([k]) => k !== c.s);
|
|
26544
27020
|
case "remove":
|
|
26545
27021
|
if (c.by !== admin || !c.s || c.s === c.by || !rosterHas(previous, c.s)) return null;
|
|
@@ -26712,6 +27188,8 @@ var CommunitySession = class {
|
|
|
26712
27188
|
known = /* @__PURE__ */ new Map();
|
|
26713
27189
|
/** Commit hash → roster after it, for the window of the main branch and the side commits. */
|
|
26714
27190
|
rosters = /* @__PURE__ */ new Map();
|
|
27191
|
+
/** Commit hash → roster after it, for a few main-branch commits older than the window that `couldRead` was asked about. */
|
|
27192
|
+
pastRosters = /* @__PURE__ */ new Map();
|
|
26715
27193
|
mainIndex = /* @__PURE__ */ new Map();
|
|
26716
27194
|
pendingCommits = /* @__PURE__ */ new Map();
|
|
26717
27195
|
pendingSecrets = /* @__PURE__ */ new Map();
|
|
@@ -26720,6 +27198,8 @@ var CommunitySession = class {
|
|
|
26720
27198
|
asked = /* @__PURE__ */ new Map();
|
|
26721
27199
|
/** Syncs answered per member, a few a minute (`COMMUNITY_LIMITS.syncAnswers`). In memory only. */
|
|
26722
27200
|
syncsAnswered = /* @__PURE__ */ new Map();
|
|
27201
|
+
/** The entry seed sealed to each member, for the seed it seals (`entryFrame`): every sync answer carries it. In memory only. */
|
|
27202
|
+
sealedEntries = /* @__PURE__ */ new Map();
|
|
26723
27203
|
/** Catch-up answers still going out, a slice at a time (`receiveSync`). */
|
|
26724
27204
|
answers;
|
|
26725
27205
|
queue = Promise.resolve();
|
|
@@ -26918,8 +27398,26 @@ var CommunitySession = class {
|
|
|
26918
27398
|
*/
|
|
26919
27399
|
couldRead(key, messageId) {
|
|
26920
27400
|
const [, e, h] = messageId.split(":");
|
|
26921
|
-
|
|
26922
|
-
|
|
27401
|
+
if (h === void 0 || !rosterHas(this.roster, key)) return false;
|
|
27402
|
+
const found = this.commitByShort(Number(e), h);
|
|
27403
|
+
if (found) return rosterHas(this.rosterAt(found.hash) ?? [], key);
|
|
27404
|
+
const i = this.state.chain.length - 1 - (this.epoch - Number(e));
|
|
27405
|
+
const commit = Number.isSafeInteger(i) && i >= 0 ? this.state.chain[i] : void 0;
|
|
27406
|
+
const hash = commit && communityCommitHash(commit);
|
|
27407
|
+
return !!hash && hash.startsWith(h) && rosterHas(this.pastRoster(i, hash), key);
|
|
27408
|
+
}
|
|
27409
|
+
/** The roster after an old commit of the main branch, replayed and kept apart from the window's (a few, the latest asked). */
|
|
27410
|
+
pastRoster(i, hash) {
|
|
27411
|
+
const known = this.pastRosters.get(hash);
|
|
27412
|
+
if (known) return known;
|
|
27413
|
+
let roster = null;
|
|
27414
|
+
for (let j = 0; j <= i && (j === 0 || roster); j++) roster = communityRoster(roster, this.state.chain[j]);
|
|
27415
|
+
this.pastRosters.set(hash, roster ?? []);
|
|
27416
|
+
for (const old of this.pastRosters.keys()) {
|
|
27417
|
+
if (this.pastRosters.size <= 8) break;
|
|
27418
|
+
this.pastRosters.delete(old);
|
|
27419
|
+
}
|
|
27420
|
+
return roster ?? [];
|
|
26923
27421
|
}
|
|
26924
27422
|
/** Someone the chain took out (removed or left) and who is not back in. */
|
|
26925
27423
|
wasRemoved(key) {
|
|
@@ -26968,7 +27466,10 @@ var CommunitySession = class {
|
|
|
26968
27466
|
const lowest = Math.max(0, entry.high - COMMUNITY_LIMITS.replay + 1);
|
|
26969
27467
|
return Math.max(0, entry.high - lowest + 1 - (1 + entry.window.filter((n) => n >= lowest).length));
|
|
26970
27468
|
}
|
|
26971
|
-
/**
|
|
27469
|
+
/**
|
|
27470
|
+
* Replays the main branch (rosters kept for its window) and places the side commits. The main branch was verified
|
|
27471
|
+
* commit by commit when it was accepted: its signatures are not checked again.
|
|
27472
|
+
*/
|
|
26972
27473
|
rebuild() {
|
|
26973
27474
|
this.known.clear();
|
|
26974
27475
|
this.rosters.clear();
|
|
@@ -26976,7 +27477,7 @@ var CommunitySession = class {
|
|
|
26976
27477
|
let roster = null;
|
|
26977
27478
|
const keepFrom = Math.max(0, this.state.chain.length - 1 - COMMUNITY_LIMITS.window);
|
|
26978
27479
|
this.state.chain.forEach((commit, i) => {
|
|
26979
|
-
roster = communityRoster(roster, commit);
|
|
27480
|
+
roster = communityRoster(roster, commit, true);
|
|
26980
27481
|
if (!roster) throw new Error("Stored membership chain does not replay");
|
|
26981
27482
|
const h = communityCommitHash(commit);
|
|
26982
27483
|
this.known.set(h, commit);
|
|
@@ -27005,7 +27506,7 @@ var CommunitySession = class {
|
|
|
27005
27506
|
const i = this.mainIndex.get(h);
|
|
27006
27507
|
if (i === void 0) return void 0;
|
|
27007
27508
|
let roster = null;
|
|
27008
|
-
for (let j = 0; j <= i; j++) roster = communityRoster(roster, this.state.chain[j]);
|
|
27509
|
+
for (let j = 0; j <= i; j++) roster = communityRoster(roster, this.state.chain[j], true);
|
|
27009
27510
|
if (roster) this.rosters.set(h, roster);
|
|
27010
27511
|
return roster ?? void 0;
|
|
27011
27512
|
}
|
|
@@ -28109,13 +28610,14 @@ var CommunitySession = class {
|
|
|
28109
28610
|
const handed = /* @__PURE__ */ new Set();
|
|
28110
28611
|
const unheard = new Set(this.state.unheard ?? []);
|
|
28111
28612
|
for (const stored of this.state.store) {
|
|
28112
|
-
if (stored.s === from
|
|
28613
|
+
if (stored.s === from) continue;
|
|
28614
|
+
const high = have[stored.s]?.[seenKey(stored.e, stored.h)];
|
|
28615
|
+
if (Number.isSafeInteger(high) && high >= stored.n) continue;
|
|
28616
|
+
if (this.wasRemoved(stored.s)) continue;
|
|
28113
28617
|
const found = this.commitByShort(stored.e, stored.h);
|
|
28114
28618
|
if (!found) continue;
|
|
28115
28619
|
const reads = rosterHas(this.rosterAt(found.hash) ?? [], from), mine = stored.s === this.myKey && unheard.has(frameKey(stored));
|
|
28116
28620
|
if (!reads && !(mine && rosterHas(this.roster, from))) continue;
|
|
28117
|
-
const high = have[stored.s]?.[seenKey(stored.e, stored.h)];
|
|
28118
|
-
if (Number.isSafeInteger(high) && high >= stored.n) continue;
|
|
28119
28621
|
if (!this.mainIndex.has(found.hash)) this.handSide(from, found.hash, handed, answer);
|
|
28120
28622
|
answer.push(stored);
|
|
28121
28623
|
if (mine && reads) unheard.delete(frameKey(stored));
|
|
@@ -28161,11 +28663,13 @@ var CommunitySession = class {
|
|
|
28161
28663
|
});
|
|
28162
28664
|
}
|
|
28163
28665
|
}
|
|
28164
|
-
/** The current entry seed sealed to a member, when I hold it. */
|
|
28666
|
+
/** The current entry seed sealed to a member, when I hold it: sealed once per seed, not on every sync answer. */
|
|
28165
28667
|
entryFrame(to) {
|
|
28166
28668
|
const { key, seedB64 } = this.state.entry;
|
|
28167
28669
|
if (!key || !seedB64) return null;
|
|
28168
|
-
|
|
28670
|
+
const sealed = this.sealedEntries.get(to);
|
|
28671
|
+
if (sealed?.seedB64 === seedB64 && sealed.frame.x === key) return { ...sealed.frame };
|
|
28672
|
+
const frame = {
|
|
28169
28673
|
t: "group-entry",
|
|
28170
28674
|
v: 2,
|
|
28171
28675
|
g: this.id,
|
|
@@ -28173,6 +28677,11 @@ var CommunitySession = class {
|
|
|
28173
28677
|
x: key,
|
|
28174
28678
|
s: sealSecret(to, fromBase64Url(seedB64), entryAad(this.id, to))
|
|
28175
28679
|
};
|
|
28680
|
+
this.sealedEntries.set(to, {
|
|
28681
|
+
seedB64,
|
|
28682
|
+
frame
|
|
28683
|
+
});
|
|
28684
|
+
return { ...frame };
|
|
28176
28685
|
}
|
|
28177
28686
|
/** A newer entry seed, from a member who holds it (see `replaceLink`): only the one the chain names. */
|
|
28178
28687
|
takeEntrySeed(seedB64) {
|
|
@@ -28294,10 +28803,26 @@ var CommunitySession = class {
|
|
|
28294
28803
|
this.state.meta = opened.meta;
|
|
28295
28804
|
await this.persist();
|
|
28296
28805
|
const change = !before && !rosterHas(this.rosterAt(s.h) ?? [], this.myKey) ? null : groupMetaChange(before, opened.meta, this.state.name);
|
|
28297
|
-
if (change) this.hooks.metaChanged?.(
|
|
28806
|
+
if (change) this.hooks.metaChanged?.(this.metaAuthor(before, s, at), change, opened.meta.ts);
|
|
28298
28807
|
this.hooks.changed();
|
|
28299
28808
|
return true;
|
|
28300
28809
|
}
|
|
28810
|
+
/**
|
|
28811
|
+
* Who made the change a statement brings me (`at`: its commit's place on my main branch): its signer, when they were
|
|
28812
|
+
* the admin all along since the statement I held (or, with none, since I got in). A new admin signs again what the
|
|
28813
|
+
* last one set (`metaFollowsChain`): to a member that missed that, it is no change of theirs it can tell apart.
|
|
28814
|
+
*/
|
|
28815
|
+
metaAuthor(before, s, at) {
|
|
28816
|
+
const from = before ? this.mainIndex.get(before.h) : void 0;
|
|
28817
|
+
let roster = before && from !== void 0 ? this.rosterAt(before.h) ?? null : null, counted = from !== void 0;
|
|
28818
|
+
for (let i = from ?? 0; i <= at; i++) {
|
|
28819
|
+
if (i !== from) roster = communityRoster(roster, this.state.chain[i]);
|
|
28820
|
+
if (!roster) return void 0;
|
|
28821
|
+
counted ||= rosterHas(roster, this.myKey);
|
|
28822
|
+
if (counted && rosterAdmin(roster) !== s.by) return void 0;
|
|
28823
|
+
}
|
|
28824
|
+
return s.by;
|
|
28825
|
+
}
|
|
28301
28826
|
/** After the chain or my secrets moved: the waiting statement, and, if I became the admin, the name and picture signed again as mine. */
|
|
28302
28827
|
async metaFollowsChain() {
|
|
28303
28828
|
const pending = this.pendingMeta;
|
|
@@ -28460,11 +28985,18 @@ var APP_PERMISSIONS = [
|
|
|
28460
28985
|
"internet",
|
|
28461
28986
|
"name"
|
|
28462
28987
|
];
|
|
28988
|
+
/**
|
|
28989
|
+
* The clients a writer may name in `runtime.clients` (WISP 1200 · Manifest). A reader also accepts a client name it does
|
|
28990
|
+
* not know (`APP_CLIENT_NAME`), since `runtime` is advisory and a later client (Android, say) must not make every older
|
|
28991
|
+
* reader refuse the bundle.
|
|
28992
|
+
*/
|
|
28463
28993
|
var APP_CLIENTS = [
|
|
28464
28994
|
"web",
|
|
28465
28995
|
"desktop",
|
|
28466
28996
|
"extension"
|
|
28467
28997
|
];
|
|
28998
|
+
/** A client name a reader accepts in `runtime.clients`, a later client's included. */
|
|
28999
|
+
var APP_CLIENT_NAME = /^[a-z][a-z0-9-]{0,31}$/;
|
|
28468
29000
|
/**
|
|
28469
29001
|
* Where an app shows (WISP 1200 · Manifest, `view`). `chat`, the default when the manifest has none: inside a 1:1 chat
|
|
28470
29002
|
* only, beside it on a wide screen. `full`: full screen, alone or from a chat, never beside one.
|
|
@@ -28496,8 +29028,39 @@ var OPTIONAL = [
|
|
|
28496
29028
|
"support",
|
|
28497
29029
|
"releaseNotes"
|
|
28498
29030
|
];
|
|
28499
|
-
/**
|
|
29031
|
+
/**
|
|
29032
|
+
* Reserved for phase 2 and refused in phase 1, by a reader too: an app that names a price or a recovery key asks for
|
|
29033
|
+
* something this client cannot honour, so it is not one to ignore.
|
|
29034
|
+
*/
|
|
28500
29035
|
var RESERVED = ["price", "recovery"];
|
|
29036
|
+
/**
|
|
29037
|
+
* Keys refused in every mode and at any level, wherever a reader ignores unknown keys: kept and passed on, they would
|
|
29038
|
+
* name an object's prototype the moment anything spreads or assigns a manifest or a listing.
|
|
29039
|
+
*/
|
|
29040
|
+
var APP_FORBIDDEN_KEYS = [
|
|
29041
|
+
"__proto__",
|
|
29042
|
+
"constructor",
|
|
29043
|
+
"prototype"
|
|
29044
|
+
];
|
|
29045
|
+
/**
|
|
29046
|
+
* The path of an `APP_FORBIDDEN_KEYS` key anywhere in a parsed JSON value (`later.0.__proto__`), the values of ignored
|
|
29047
|
+
* keys included, or undefined: WISP 1200 refuses them at any level. `JSON.parse` makes `__proto__` an own key, so
|
|
29048
|
+
* `Object.keys` sees it. A stack, not recursion: a `listing.json` is read with `JSON.parse` alone, at any depth.
|
|
29049
|
+
*/
|
|
29050
|
+
function findAppForbiddenKey(value) {
|
|
29051
|
+
const stack = [[value, ""]];
|
|
29052
|
+
for (let next = stack.pop(); next; next = stack.pop()) {
|
|
29053
|
+
const [v, path] = next;
|
|
29054
|
+
if (typeof v !== "object" || v === null) continue;
|
|
29055
|
+
for (const key of Object.keys(v)) {
|
|
29056
|
+
const at = path ? `${path}.${key}` : key;
|
|
29057
|
+
if (APP_FORBIDDEN_KEYS.includes(key)) return at;
|
|
29058
|
+
stack.push([v[key], at]);
|
|
29059
|
+
}
|
|
29060
|
+
}
|
|
29061
|
+
}
|
|
29062
|
+
/** At most this many clients in `runtime.clients`. */
|
|
29063
|
+
var MAX_CLIENTS = 16;
|
|
28501
29064
|
var SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
|
|
28502
29065
|
var HOST = /^>=(0|[1-9]\d*)\.(0|[1-9]\d*)(\.(0|[1-9]\d*))?$/;
|
|
28503
29066
|
var SEGMENT = /^[A-Za-z0-9._-]+$/;
|
|
@@ -28581,21 +29144,30 @@ var bad = (field, detail = `${field} is not valid`) => ({
|
|
|
28581
29144
|
reason: "bad-field",
|
|
28582
29145
|
detail
|
|
28583
29146
|
});
|
|
28584
|
-
/**
|
|
28585
|
-
|
|
29147
|
+
/**
|
|
29148
|
+
* Checks a parsed manifest against every rule and bound of WISP 1200 that the manifest alone decides. An unknown key is
|
|
29149
|
+
* ignored unless `strict` (`AppManifestOptions`).
|
|
29150
|
+
*/
|
|
29151
|
+
function checkAppManifest(value, options = {}) {
|
|
28586
29152
|
if (!isObject$1(value)) return bad("manifest", "The manifest is not an object");
|
|
28587
29153
|
if (value.ghostlyApp !== void 0 && value.ghostlyApp !== 1) return {
|
|
28588
29154
|
ok: false,
|
|
28589
29155
|
reason: "unsupported-format",
|
|
28590
29156
|
detail: "ghostlyApp is not 1"
|
|
28591
29157
|
};
|
|
29158
|
+
const forbidden = findAppForbiddenKey(value);
|
|
29159
|
+
if (forbidden !== void 0) return {
|
|
29160
|
+
ok: false,
|
|
29161
|
+
reason: "unknown-key",
|
|
29162
|
+
detail: forbidden
|
|
29163
|
+
};
|
|
28592
29164
|
for (const key of Object.keys(value)) {
|
|
28593
29165
|
if (RESERVED.includes(key)) return {
|
|
28594
29166
|
ok: false,
|
|
28595
29167
|
reason: "reserved-key",
|
|
28596
29168
|
detail: key
|
|
28597
29169
|
};
|
|
28598
|
-
if (!REQUIRED.includes(key) && !OPTIONAL.includes(key)) return {
|
|
29170
|
+
if (options.strict && !REQUIRED.includes(key) && !OPTIONAL.includes(key)) return {
|
|
28599
29171
|
ok: false,
|
|
28600
29172
|
reason: "unknown-key",
|
|
28601
29173
|
detail: key
|
|
@@ -28620,9 +29192,18 @@ function checkAppManifest(value) {
|
|
|
28620
29192
|
if (!Array.isArray(permissions) || new Set(permissions).size !== permissions.length || !permissions.every((p) => APP_PERMISSIONS.includes(p))) return bad("permissions");
|
|
28621
29193
|
if (m.view !== void 0 && !APP_VIEWS.includes(m.view)) return bad("view", `view is "chat" or "full"`);
|
|
28622
29194
|
const runtime = m.runtime;
|
|
28623
|
-
if (!isObject$1(runtime)
|
|
29195
|
+
if (!isObject$1(runtime)) return bad("runtime");
|
|
29196
|
+
for (const key of Object.keys(runtime)) {
|
|
29197
|
+
if (key === "host" || key === "clients") continue;
|
|
29198
|
+
if (options.strict) return {
|
|
29199
|
+
ok: false,
|
|
29200
|
+
reason: "unknown-key",
|
|
29201
|
+
detail: `runtime.${key}`
|
|
29202
|
+
};
|
|
29203
|
+
}
|
|
29204
|
+
if (typeof runtime.host !== "string" || !HOST.test(runtime.host)) return bad("runtime");
|
|
28624
29205
|
const clients = runtime.clients;
|
|
28625
|
-
if (!Array.isArray(clients) || clients.length < 1 || new Set(clients).size !== clients.length || !clients.every((c) => APP_CLIENTS.includes(c))) return bad("runtime");
|
|
29206
|
+
if (!Array.isArray(clients) || clients.length < 1 || clients.length > MAX_CLIENTS || new Set(clients).size !== clients.length || !clients.every((c) => options.strict ? APP_CLIENTS.includes(c) : typeof c === "string" && APP_CLIENT_NAME.test(c))) return bad("runtime");
|
|
28626
29207
|
if (!isAppLicense(m.license)) return bad("license");
|
|
28627
29208
|
if (m.sources !== void 0 && (!Array.isArray(m.sources) || m.sources.length > APP_BUNDLE_LIMITS.sources || !m.sources.every(isAppUrl))) return bad("sources");
|
|
28628
29209
|
if (m.homepage !== void 0 && !isAppUrl(m.homepage)) return bad("homepage");
|
|
@@ -28835,8 +29416,14 @@ var isLine = (v, max, min = 0) => typeof v === "string" && [...v].length >= min
|
|
|
28835
29416
|
var isText = (v, max) => typeof v === "string" && [...v].length <= max && !/[^\P{Cc}\n]/u.test(v);
|
|
28836
29417
|
var isCount = (v, min) => typeof v === "number" && Number.isSafeInteger(v) && v >= min;
|
|
28837
29418
|
var fine = { ok: true };
|
|
28838
|
-
function keysOf(value, required, optional) {
|
|
28839
|
-
|
|
29419
|
+
function keysOf(value, required, optional, options = {}) {
|
|
29420
|
+
const forbidden = findAppForbiddenKey(value);
|
|
29421
|
+
if (forbidden !== void 0) return {
|
|
29422
|
+
ok: false,
|
|
29423
|
+
reason: "unknown-key",
|
|
29424
|
+
detail: forbidden
|
|
29425
|
+
};
|
|
29426
|
+
for (const k of Object.keys(value)) if (options.strict && !required.includes(k) && !optional.includes(k)) return {
|
|
28840
29427
|
ok: false,
|
|
28841
29428
|
reason: "unknown-key",
|
|
28842
29429
|
detail: k
|
|
@@ -28863,14 +29450,14 @@ var LISTING_OPTIONAL = [
|
|
|
28863
29450
|
"repo",
|
|
28864
29451
|
"support"
|
|
28865
29452
|
];
|
|
28866
|
-
/** Checks one entry of `apps`, which is also the whole of a `listing.json`. */
|
|
28867
|
-
function checkAppListing(value) {
|
|
29453
|
+
/** Checks one entry of `apps`, which is also the whole of a `listing.json`. An unknown key is ignored unless `strict`. */
|
|
29454
|
+
function checkAppListing(value, options = {}) {
|
|
28868
29455
|
if (!isObject(value)) return {
|
|
28869
29456
|
ok: false,
|
|
28870
29457
|
reason: "bad-field",
|
|
28871
29458
|
detail: "A listing is not an object"
|
|
28872
29459
|
};
|
|
28873
|
-
const keys = keysOf(value, LISTING_REQUIRED, LISTING_OPTIONAL);
|
|
29460
|
+
const keys = keysOf(value, LISTING_REQUIRED, LISTING_OPTIONAL, options);
|
|
28874
29461
|
if (!keys.ok) return keys;
|
|
28875
29462
|
const bad = (field) => ({
|
|
28876
29463
|
ok: false,
|
|
@@ -28892,7 +29479,7 @@ function checkAppListing(value) {
|
|
|
28892
29479
|
for (const k of ["repo", "support"]) if (value[k] !== void 0 && !isAppUrl(value[k])) return bad(k);
|
|
28893
29480
|
return fine;
|
|
28894
29481
|
}
|
|
28895
|
-
function checkRemoval(value) {
|
|
29482
|
+
function checkRemoval(value, options) {
|
|
28896
29483
|
if (!isObject(value)) return {
|
|
28897
29484
|
ok: false,
|
|
28898
29485
|
reason: "bad-field",
|
|
@@ -28903,7 +29490,7 @@ function checkRemoval(value) {
|
|
|
28903
29490
|
"digest",
|
|
28904
29491
|
"reason",
|
|
28905
29492
|
"at"
|
|
28906
|
-
], []);
|
|
29493
|
+
], [], options);
|
|
28907
29494
|
if (!keys.ok) return keys;
|
|
28908
29495
|
if (!isAppRef(value.ref) || !isAppHash(value.digest) || !isLine(value.reason, APP_STORE_LIMITS.reason, 1) || !isCount(value.at, 0)) return {
|
|
28909
29496
|
ok: false,
|
|
@@ -28925,9 +29512,11 @@ var STORE_REQUIRED = [
|
|
|
28925
29512
|
];
|
|
28926
29513
|
/**
|
|
28927
29514
|
* Checks an index's fields and bounds, and every revocation it carries (one that does not verify refuses the index:
|
|
28928
|
-
* the store copied it, so a broken one is the store's fault). The signature and `expires` are `readAppStore`'s.
|
|
29515
|
+
* the store copied it, so a broken one is the store's fault). The signature and `expires` are `readAppStore`'s. An
|
|
29516
|
+
* unknown key of the index, a listing or a removal is ignored unless `strict`; a revocation is its publisher's signed
|
|
29517
|
+
* statement and keeps its exact keys.
|
|
28929
29518
|
*/
|
|
28930
|
-
function checkAppStoreIndex(value) {
|
|
29519
|
+
function checkAppStoreIndex(value, options = {}) {
|
|
28931
29520
|
if (!isObject(value)) return {
|
|
28932
29521
|
ok: false,
|
|
28933
29522
|
reason: "bad-field",
|
|
@@ -28938,7 +29527,7 @@ function checkAppStoreIndex(value) {
|
|
|
28938
29527
|
reason: "unsupported-format",
|
|
28939
29528
|
detail: "ghostlyStore is not 1"
|
|
28940
29529
|
};
|
|
28941
|
-
const keys = keysOf(value, STORE_REQUIRED, ["description"]);
|
|
29530
|
+
const keys = keysOf(value, STORE_REQUIRED, ["description"], options);
|
|
28942
29531
|
if (!keys.ok) return keys;
|
|
28943
29532
|
const bad = (field) => ({
|
|
28944
29533
|
ok: false,
|
|
@@ -28955,7 +29544,7 @@ function checkAppStoreIndex(value) {
|
|
|
28955
29544
|
if (!Array.isArray(apps) || apps.length > APP_STORE_LIMITS.apps) return bad("apps");
|
|
28956
29545
|
const refs = /* @__PURE__ */ new Set();
|
|
28957
29546
|
for (const listing of apps) {
|
|
28958
|
-
const checked = checkAppListing(listing);
|
|
29547
|
+
const checked = checkAppListing(listing, options);
|
|
28959
29548
|
if (!checked.ok) return checked;
|
|
28960
29549
|
const ref = listing.ref;
|
|
28961
29550
|
if (refs.has(ref)) return {
|
|
@@ -28967,7 +29556,7 @@ function checkAppStoreIndex(value) {
|
|
|
28967
29556
|
}
|
|
28968
29557
|
if (!Array.isArray(removed) || removed.length > APP_STORE_LIMITS.removed) return bad("removed");
|
|
28969
29558
|
for (const entry of removed) {
|
|
28970
|
-
const checked = checkRemoval(entry);
|
|
29559
|
+
const checked = checkRemoval(entry, options);
|
|
28971
29560
|
if (!checked.ok) return checked;
|
|
28972
29561
|
}
|
|
28973
29562
|
if (!Array.isArray(revoked) || revoked.length > APP_STORE_LIMITS.revoked) return bad("revoked");
|
|
@@ -28984,7 +29573,8 @@ function checkAppStoreIndex(value) {
|
|
|
28984
29573
|
/**
|
|
28985
29574
|
* Reads `ghostly-store.json` with its `ghostly-store.sig`, in this order: size, JSON, canonical bytes, fields and
|
|
28986
29575
|
* revocations, the signature statement, the key the reader holds for this store (when it holds one), the signature,
|
|
28987
|
-
* then `expires` against `now` (seconds): more than 90 days ahead is refused, past is `expired`.
|
|
29576
|
+
* then `expires` against `now` (seconds): more than 90 days ahead is refused, past is `expired`. A key it does not know
|
|
29577
|
+
* is ignored (a later store's optional field) and stays in the bytes the signature covers.
|
|
28988
29578
|
*/
|
|
28989
29579
|
function readAppStore(indexBytes, sigBytes, now, heldKey) {
|
|
28990
29580
|
if (indexBytes.length > APP_STORE_LIMITS.indexBytes) return {
|
|
@@ -29018,10 +29608,13 @@ function readAppStore(indexBytes, sigBytes, now, heldKey) {
|
|
|
29018
29608
|
}
|
|
29019
29609
|
};
|
|
29020
29610
|
}
|
|
29021
|
-
/**
|
|
29611
|
+
/**
|
|
29612
|
+
* Signs an index with the store key: its canonical bytes and the `ghostly-store.sig` bytes. Throws on a bad index, and
|
|
29613
|
+
* on a key this format does not define: a writer is strict, so a misspelt key is never signed.
|
|
29614
|
+
*/
|
|
29022
29615
|
async function signAppStore(index, signer) {
|
|
29023
29616
|
if (index.key !== toZ32(signer.publicKey)) throw new Error("A store index is signed by its own key");
|
|
29024
|
-
const checked = checkAppStoreIndex(JSON.parse(JSON.stringify(index)));
|
|
29617
|
+
const checked = checkAppStoreIndex(JSON.parse(JSON.stringify(index)), { strict: true });
|
|
29025
29618
|
if (!checked.ok) throw new Error(`Not a valid store index (${checked.reason}${checked.detail ? `: ${checked.detail}` : ""})`);
|
|
29026
29619
|
const { bytes, signature } = await signAppObject(APP_PREFIXES.store, index, signer);
|
|
29027
29620
|
return {
|
|
@@ -29092,6 +29685,6 @@ function appStoreDecision(held, read) {
|
|
|
29092
29685
|
return read.digest === held.digest ? "same" : "equivocation";
|
|
29093
29686
|
}
|
|
29094
29687
|
//#endregion
|
|
29095
|
-
export { parseSshPublicKey as $, signAppRevocation as $a, formatFileSize as $i, GROUP_FILE_LIMITS as $n, handoffEphemeral as $o, WAKE_CALL_INTERVAL_MS as $r, deviceKeyZ32 as $s, SEPOLIA_TEST_USDT_FAUCET as $t, entryParams as A, replySnippet as Aa, createRelayPayload as Ac, IdentityExchange as Ai, nip05RootKey as An, LEGACY_SERVICES as Ao, newerHead as Ar, enrollCancelReason as As, didWebDocumentUrl as At, GROUP_REACTED_FRAME as B, claimedTime as Ba, sanitizeAvatar as Bc, emptyProofLedger as Bi, engineText as Bn, HANDOFF_REQUEST as Bo, transferStalled as Br, deviceInviteJoinerParams as Bs, staticDidDocument as Bt, GROUP_READ_NOTE_COMMUNITY as C, reactionEmoji as Ca, setLinkTraceSink as Cc, DHT_TEXT_CAPABILITY as Ci, DOMAIN_FILE_MAX_BYTES as Cn, callRtcConfig as Co, beaconRecords as Cr, signTombstone as Cs, DID_JWS_MAX_LENGTH as Ct, KNOCK_TTL_MS as D, wireReaction as Da, withRequestOptions as Dc, IDENTITY_MAX_VALIDITY as Di, domainWellKnownFile as Dn, signalHasVideo as Do, lobbyRecords as Dr, ENROLL_HELLO as Ds, didGhostlyServiceEntry as Dt, encodeCommunityLink as E, validReactionNumber as Ea, isDiscoveryBudgetError as Ec, IDENTITY_MAX_EVIDENCE as Ei, domainTxtRecord as En, parseCallSignal as Eo, lobbyKeys as Er, ENROLL_GRANT as Es, checkDidSignature as Et, GROUP_WAKE_FRAME as F, buttonLabelClash as Fa, identityFromSeed as Fc, newIdentityBinding as Fi, txtValue as Fn, HANDOFF_HAVE as Fo, readLobby as Fr, enrollProofFrame as Fs, normalizeDid as Ft, GROUP_EDIT_TEXT_BYTES as G, APP_STATEMENT_LIMITS as Ga, toZ32 as Gc, irohRelayUrl as Gi, findEndpoint as Gn, handoffAttemptFailed as Go, typingActivity as Gr, encodeInviteCode as Gs, encodeDidDhtPacket as Gt, GroupSession as H, receivedTimestamp as Ha, fromBase64Url as Hc, TRANSPORTS as Hi, ENDPOINT as Hn, HANDOFF_VERIFIED as Ho, TYPING_REFRESH_MS as Hr, OwnInviteError as Hs, didDhtDocument as Ht, GroupWakeLimiter as I, checkStatusCard as Ia, identityFromSeedB64 as Ic, receivedIdentityStatus as Ii, wellKnownUrl as In, HANDOFF_HELLO as Io, shouldBeHub as Ir, readEnrollGrant as Is, parseDid as It, mentionsMember as J, checkAppRevokeStatement as Ja, peekDhtMailbox as Ji, parseFedimintRequestPayload as Jn, handoffCancelFrame as Jo, mayPin as Jr, DEVICES_CAPABILITY as Js, ETHEREUM_USDT as Jt, carryMentions as K, appFingerprint as Ka, utf8Decode as Kc, DHT_FILE_TOO_LARGE as Ki, isFederationId as Kn, handoffAttemptSucceeded as Ko, GROUP_PIN_FRAME as Kr, inviteLink as Ks, resolveDidDht as Kt, groupWakeFrame as L, sameButtonText as La, publicKeyFromZ32 as Lc, revokesIdentity as Li, requestLnurlInvoice as Ln, HANDOFF_OFFER as Lo, ChatFiles as Lr, readEnrollHello as Ls, parseDidDocument as Lt, knockRecords as M, ITEM_STATES as Ma, openRelayPayload as Mc, emptyIdentityLedger as Mi, recordsFromTxt as Mn, HANDOFF_ACK as Mo, rankHubs as Mr, enrollDoneFrame as Ms, didWebFileNames as Mt, mergeKnocks as N, RUN_RESULTS as Na, parseRelayPayload as Nc, identityRevocationValue as Ni, recordsFromWellKnown as Nn, HANDOFF_CANCEL as No, readBeacon as Nr, enrollGrantFrame as Ns, didWebFileUrl as Nt, decodeGroupEntryLink as O, pairedReplyAuthor as Oa, PacketTooLargeError as Oc, IDENTITY_REVOCATION_LABEL as Oi, encodeDnsQuery as On, emptyLinkRecords as Oo, mergeBeacon as Or, ENROLL_PROOF as Os, didJwsAlg as Ot, readKnocks as P, STATUS_CARD_LIMITS as Pa, createIdentity as Pc, identityStatement as Pi, recordsName as Pn, HANDOFF_DONE as Po, readBeaconHead as Pr, enrollHelloFrame as Ps, didWebHost as Pt, SshSigError as Q, signAppObject as Qa, readImageMeta as Qi, pairedMessageFrame as Qn, handoffDigest as Qo, readPin as Qr, deviceEchoNonce as Qs, SEPOLIA_TEST_USDT as Qt, groupWakes as R, statusCardText as Ra, verify as Rc, lastSharedWithMe as Ri, resolveLightningDestination as Rn, HANDOFF_PAKE as Ro, FILE_LIMITS as Rr, verifyEnrollProof as Rs, parseDidSignaturePaste as Rt, CommunitySession as S, queueReaction as Sa, normalizeRelayUrl as Sc, CapsExchange as Si, pubkyHomeserverOf as Sn, buildSdpFromSignal as So, beaconKeys as Sr, setUpdateFrame as Ss, DID_FILE_MAX_BYTES as St, decodeCommunityLink as T, readReaction as Ta, budgetRetryMs as Tc, dialDescriptors as Ti, dnsProofName as Tn, extractParamsFromSdp as To, freshHubs as Tr, ENROLL_DONE as Ts, checkDidJws as Tt, COMMUNITY_EDIT_FRAME as U, shownTime as Ua, randomBytes as Uc, automaticTransport as Ui, cashuRequestPayload as Un, filePartName as Uo, TypingSender as Ur, createChatInvite as Us, didDhtFromPublicKey as Ut, GROUP_READ_NOTE as V, heardTime as Va, bytesEqual as Vc, validExternalKey as Vi, decodeBolt11 as Vn, HANDOFF_TIMINGS as Vo, TYPING_KINDS as Vr, readDeviceInvite as Vs, strictBase64Url as Vt, GROUP_EDIT_FRAME as W, APP_PREFIXES as Wa, toBase64Url as Wc, irohDescriptor as Wi, fedimintRequestPayload as Wn, handoffAttemptAllowed as Wo, sanitizeTypingStatus as Wr, decodeInviteCode as Ws, encodeDidDhtDeactivated as Wt, SSHSIG_MAX_ARMOR as X, isAppRef as Xa, dhtFileId as Xi, INVITE_TAKEN as Xn, handoffContextProof as Xo, pinIsNewer as Xr, HANDOFF_CAPABILITY as Xs, ONCHAIN_PROVIDER as Xt, groupName as Y, isAppKey as Ya, DHT_FILE_CAPABILITY as Yi, GhostLink as Yn, handoffContext as Yo, nextPinNumber as Yr, ENROLL_CAPABILITY as Ys, EVM_TEST_CHAINS as Yt, SSHSIG_NAMESPACE as Z, readAppRevocations as Za, parseImageMeta as Zi, LIVENESS_PING_MS as Zn, handoffDataFrame as Zo, pinNumberHolds as Zr, deviceEchoFrame as Zs, PaymentPreflightError as Zt, GroupTypingBoard as _, COMMUNITY_REACTION_FRAME as _a, turnKeys as _c, HOLD_LIMITS as _i, normalizePubkyKey as _n, decodeControl as _o, MESH_HUBS as _r, sealHandoffPiece as _s, parseAtprotoDidDocument as _t, checkAppStoreIndex as a, baseMime as aa, TURN_PUT_WINDOW_MS as ac, wakeRequest as ai, formatPaymentAmount as an, rosterHas as ao, isGroupMessageId as ar, handoffRetryAfter as as, BitcoinAddressError as at, sealGroupTyping as b, ReactionWindow as ba, RelayTransport as bc, newMailbox as bi, pubkyProofPath as bn, RTC_CONFIG as bo, pickMeshHubs as br, readSetUpdate as bs, verifyAtprotoRecordProof as bt, APP_BUNDLE_LIMITS as c, voicePeaksOf as ca, classifyTurnRead as cc, chatAppId as ci, SPARK_NETWORKS as cn, DirectPathWatch as co, readGroupWant as cr, newHandoffId as cs, ATPROTO_FULL_SCOPE as ct, appViewOf as d, EDIT_RATE_WINDOW_MS as da, TURN_MAX as dc, BUTTONS_CAPABILITY as di, sparkInvoiceDetails as dn, presenceSeenAt as do, GROUP_SIGNAL_FRAME as dr, readHandoffDone as ds, atprotoDidMethod as dt, safeBlobType as ea, deviceLinkPairing as ec, WakeLimiter as ei, TEST_USDT_FAUCET_AMOUNT as en, canonicalJson as eo, GROUP_HAVE_FRAME as er, handoffHelloFrame as es, parseSshSignature as et, assembleAppBundle as f, RateWindow as fa, firstDeviceSetSecret as fc, EDIT_CAPABILITY as fi, isBitcoinAddress as fn, GhostlyHttpError as fo, SIGNAL_FRESH_MS as fr, readHandoffHave as fs, atprotoProofRecord as ft, GROUP_TYPING_FRAME as g, readForwarded as ga, signTurnRelease as gc, HOLD_DATED_BACK_MS as gi, newPubkyProofFolder as gn, LIMITS as go, readGroupSignal as gr, readHandoffVerified as gs, normalizeAtprotoHandle as gt, readAppBundle as h, forwardedAgain as ha, signTurnPacket as hc, HOLD_CAPABILITY as hi, isPubkyProofFolder as hn, webLocalFetch as ho, groupSignalFrame as hr, readHandoffTurnFrame as hs, isAtprotoDid as ht, planAppUpdate as i, VOICE_LIMITS as ia, webCryptoSigner as ic, relayRequest as ii, assertWholeSats as in, MEMBER_KEY as io, groupFileFallback as ir, handoffReleaseFrame as is, verifyBitcoinMessage as it, knockIdentity as j, BUTTON_ID as ja, newerPacket as jc, boundVerifiedIdentity as ji, normalizeDomain as jn, serviceIdFromName as jo, pickHubs as jr, enrollDigits as js, didWebFile as jt, encodeGroupEntryLink as k, readReply as ka, RELAY_PAYLOAD_MAX_BYTES as kc, IdentityCheckUnavailable as ki, isPublicAddress as kn, HTTP_SERVICE_PROTO as ko, mergeLobby as kr, enrollCancelFrame as ks, didSignedMessages as kt, APP_BUNDLE_MAGIC as l, parseVideoMeta as la, turnPutSummary as lc, isAppVersion as li, SPARK_PROVIDER as ln, EXPECT_PEER_MS as lo, readGroupWantNo as lr, readHandoffAck as ls, ATPROTO_PROOF_COLLECTION as lt, isAppUrl as m, parseLinkPreview as ma, readTurnPacket as mc, UPGRADE_CAPABILITY as mi, isPubkyKey as mn, parseLocalTarget as mo, SIGNAL_REPLY_MS as mr, readHandoffPake as ms, checkAtprotoProofRecord as mt, appUpdateDecision as n, sanitizeMime as na, newDeviceSetSecret as nc, newWakeToken as ni, WALLET_NETWORKS as nn, GROUP_MEMBER_CAP as no, GROUP_WANT_NO_FRAME as nr, handoffOfferFrame as ns, sshSignCommand as nt, readAppStore as o, fileMessageText as oa, TURN_RAISE_READ_TIMEOUT_MS as oc, checkPushEndpoint as oi, validatePaymentTarget as on, verifyCommitSignature as oo, readGroupFileMeta as or, handoffStreamKey as os, decodeAnyBitcoinAddress as ot, checkAppManifest as p, dhtEditId as pa, nextTurnPosition as pc, STATUS_CARD_CAPABILITY as pi, PUBKY_PROOF_MAX_BYTES as pn, formatLocalTarget as po, SIGNAL_HOLD_MS as pr, readHandoffHello as ps, atprotoProofRkey as pt, parseCommunityEdit as q, appRef as qa, utf8Encode as qc, DHT_MESSAGE_TTL as qi, parseCashuRequestPayload as qn, handoffBusyFrame as qo, PIN_LIMITS as qr, inviteOwnership as qs, signDidDhtPacket as qt, checkAppBeforeRun as r, PLAYABLE_AUDIO as ra, seedSigner as rc, parseWakeFrame as ri, assertTokenUnits as rn, MAX_GROUP_CHAIN as ro, fetchesByItself as rr, handoffPakeFrame as rs, verifySshSignature as rt, signAppStore as s, parseVoiceMeta as sa, TURN_SETTLE_MS as sc, vapidKeysMatch as si, walletNetworkOf as sn, edgeParams as so, readGroupHave as sr, handoffVersions as ss, ATPROTO_DECLARED_SCOPE as st, appStoreDecision as t, sanitizeFileName as ta, devicePingFrame as tc, deviceWakeRequest as ti, USDT_PUBLIC_RPC as tn, canonicalJsonBytes as to, GROUP_WANT_FRAME as tr, handoffManifestFrames as ts, sameSshKey as tt, appDigest as u, EDIT_BUFFER_MS as ua, TURN_LAST_SEQUENCE as uc, APPS_CAPABILITY as ui, sparkAddressKind as un, RELAY_POLL_INTERVALS as uo, CarriedTransport as ur, readHandoffCancel as us, ATPROTO_PROOF_SCOPE as ut, groupTypingEpoch as v, GROUP_REACTION_FRAME as va, turnName as vc, HoldKeys as vi, pubkyProofCapability as vn, encodeControl as vo, mayBeHub as vr, SET_ACK as vs, shortAtprotoDid as vt, communityHasFrame as w, reactionIsNewer as wa, traceLink as wc, capsDescriptors as wi, decodeDnsResponse as wn, callSignalHeardAt as wo, doorHubs as wr, verifySetUpdate as ws, DidError as wt, COMMUNITY_LIMITS as x, nextReactionNumber as xa, currentRelays as xc, readManifest as xi, homeserverWebEndpoint as xn, answersOffer as xo, COMMUNITY_TOPOLOGY as xr, setAckFrame as xs, DID_DOCUMENT_MAX_BYTES as xt, openGroupTyping as y, REACTION_LIMITS as ya, DEFAULT_RELAYS as yc, HoldRefusedError as yi, pubkyProofFile as yn, CALL_SIGNAL_MAX_AGE_MS as yo, meshHubs as yr, SET_UPDATE as ys, AtprotoProofError as yt, parseGroupWakeFrame as z, arrivalKey as za, MAX_AVATAR_LENGTH as zc, PeerProofs as zi, engineError as zn, HANDOFF_PIECE_BYTES as zo, transferEnded as zr, createDeviceInvite as zs, signingKeys as zt };
|
|
29688
|
+
export { SSHSIG_NAMESPACE as $, appRef as $a, randomBytes as $c, parseImageMeta as $i, engineError as $n, filePartName as $o, pinNumberHolds as $r, createChatInvite as $s, PaymentPreflightError as $t, decodeGroupEntryLink as A, pairedReplyAuthor as Aa, RelayTransport as Ac, IDENTITY_REVOCATION_LABEL as Ai, encodeDnsQuery as An, RTC_CONFIG as Ao, mergeBeacon as Ar, readSetUpdate as As, didJwsAlg as At, groupWakes as B, cardLine as Ba, createRelayPayload as Bc, lastSharedWithMe as Bi, resolveLightningDestination as Bn, LEGACY_SERVICES as Bo, FILE_LIMITS as Br, enrollCancelReason as Bs, parseDidSignaturePaste as Bt, COMMUNITY_LIMITS as C, nextReactionNumber as Ca, nextTurnPosition as Cc, readManifest as Ci, homeserverWebEndpoint as Cn, formatLocalTarget as Co, COMMUNITY_TOPOLOGY as Cr, readHandoffHello as Cs, DID_DOCUMENT_MAX_BYTES as Ct, decodeCommunityLink as D, readReaction as Da, turnKeys as Dc, dialDescriptors as Di, dnsProofName as Dn, decodeControl as Do, freshHubs as Dr, sealHandoffPiece as Ds, checkDidJws as Dt, communityHasFrame as E, reactionIsNewer as Ea, signTurnRelease as Ec, capsDescriptors as Ei, decodeDnsResponse as En, LIMITS as Eo, doorHubs as Er, readHandoffVerified as Es, DidError as Et, mergeKnocks as F, PR_CHECKS as Fa, budgetRetryMs as Fc, identityRevocationValue as Fi, recordsFromWellKnown as Fn, extractParamsFromSdp as Fo, readBeacon as Fr, ENROLL_DONE as Fs, didWebFileUrl as Ft, COMMUNITY_EDIT_FRAME as G, arrivalKey as Ga, identityFromSeed as Gc, automaticTransport as Gi, findEndpoint as Gn, HANDOFF_HAVE as Go, TypingSender as Gr, enrollProofFrame as Gs, didDhtFromPublicKey as Gt, GROUP_REACTED_FRAME as H, checkStatusCard as Ha, openRelayPayload as Hc, emptyProofLedger as Hi, ENDPOINT as Hn, HANDOFF_ACK as Ho, transferStalled as Hr, enrollDoneFrame as Hs, staticDidDocument as Ht, readKnocks as I, PR_STATES as Ia, isDiscoveryBudgetError as Ic, identityStatement as Ii, recordsName as In, parseCallSignal as Io, readBeaconHead as Ir, ENROLL_GRANT as Is, didWebHost as It, carryMentions as J, receivedTimestamp as Ja, verify as Jc, DHT_FILE_TOO_LARGE as Ji, parseFedimintRequestPayload as Jn, HANDOFF_PAKE as Jo, GROUP_PIN_FRAME as Jr, verifyEnrollProof as Js, resolveDidDht as Jt, GROUP_EDIT_FRAME as K, claimedTime as Ka, identityFromSeedB64 as Kc, irohDescriptor as Ki, isFederationId as Kn, HANDOFF_HELLO as Ko, sanitizeTypingStatus as Kr, readEnrollGrant as Ks, encodeDidDhtDeactivated as Kt, GROUP_WAKE_FRAME as L, RUN_RESULTS as La, withRequestOptions as Lc, newIdentityBinding as Li, txtValue as Ln, signalHasVideo as Lo, readLobby as Lr, ENROLL_HELLO as Ls, normalizeDid as Lt, entryParams as M, replySnippet as Ma, normalizeRelayUrl as Mc, IdentityExchange as Mi, nip05RootKey as Mn, buildSdpFromSignal as Mo, newerHead as Mr, setUpdateFrame as Ms, didWebDocumentUrl as Mt, knockIdentity as N, BUTTON_ID as Na, setLinkTraceSink as Nc, boundVerifiedIdentity as Ni, normalizeDomain as Nn, callRtcConfig as No, pickHubs as Nr, signTombstone as Ns, didWebFile as Nt, encodeCommunityLink as O, validReactionNumber as Oa, turnName as Oc, IDENTITY_MAX_EVIDENCE as Oi, domainTxtRecord as On, encodeControl as Oo, lobbyKeys as Or, SET_ACK as Os, checkDidSignature as Ot, knockRecords as P, ITEM_STATES as Pa, traceLink as Pc, emptyIdentityLedger as Pi, recordsFromTxt as Pn, callSignalHeardAt as Po, rankHubs as Pr, verifySetUpdate as Ps, didWebFileNames as Pt, SSHSIG_MAX_ARMOR as Q, appFingerprint as Qa, fromBase64Url as Qc, dhtFileId as Qi, pairedMessageFrame as Qn, HANDOFF_VERIFIED as Qo, pinIsNewer as Qr, OwnInviteError as Qs, ONCHAIN_PROVIDER as Qt, GroupWakeLimiter as R, STATUS_CARD_LIMITS as Ra, PacketTooLargeError as Rc, receivedIdentityStatus as Ri, wellKnownUrl as Rn, emptyLinkRecords as Ro, shouldBeHub as Rr, ENROLL_PROOF as Rs, parseDid as Rt, sealGroupTyping as S, ReactionWindow as Sa, firstDeviceSetSecret as Sc, newMailbox as Si, pubkyProofPath as Sn, GhostlyHttpError as So, pickMeshHubs as Sr, readHandoffHave as Ss, verifyAtprotoRecordProof as St, GROUP_READ_NOTE_COMMUNITY as T, reactionEmoji as Ta, signTurnPacket as Tc, DHT_TEXT_CAPABILITY as Ti, DOMAIN_FILE_MAX_BYTES as Tn, webLocalFetch as To, beaconRecords as Tr, readHandoffTurnFrame as Ts, DID_JWS_MAX_LENGTH as Tt, GROUP_READ_NOTE as U, sameButtonText as Ua, parseRelayPayload as Uc, validExternalKey as Ui, cashuRequestPayload as Un, HANDOFF_CANCEL as Uo, TYPING_KINDS as Ur, enrollGrantFrame as Us, strictBase64Url as Ut, parseGroupWakeFrame as V, cardUrl as Va, newerPacket as Vc, PeerProofs as Vi, decodeBolt11 as Vn, serviceIdFromName as Vo, transferEnded as Vr, enrollDigits as Vs, signingKeys as Vt, GroupSession as W, statusCardText as Wa, createIdentity as Wc, TRANSPORTS as Wi, fedimintRequestPayload as Wn, HANDOFF_DONE as Wo, TYPING_REFRESH_MS as Wr, enrollHelloFrame as Ws, didDhtDocument as Wt, mentionsMember as X, APP_PREFIXES as Xa, sanitizeAvatar as Xc, peekDhtMailbox as Xi, INVITE_TAKEN as Xn, HANDOFF_REQUEST as Xo, mayPin as Xr, deviceInviteJoinerParams as Xs, ETHEREUM_USDT as Xt, parseCommunityEdit as Y, shownTime as Ya, MAX_AVATAR_LENGTH as Yc, DHT_MESSAGE_TTL as Yi, GhostLink as Yn, HANDOFF_PIECE_BYTES as Yo, PIN_LIMITS as Yr, createDeviceInvite as Ys, signDidDhtPacket as Yt, groupName as Z, APP_STATEMENT_LIMITS as Za, bytesEqual as Zc, DHT_FILE_CAPABILITY as Zi, LIVENESS_PING_MS as Zn, HANDOFF_TIMINGS as Zo, nextPinNumber as Zr, readDeviceInvite as Zs, EVM_TEST_CHAINS as Zt, readAppBundle as _, forwardedAgain as _a, TURN_SETTLE_MS as _c, HOLD_CAPABILITY as _i, isPubkyProofFolder as _n, DirectPathWatch as _o, groupSignalFrame as _r, handoffVersions as _s, isAtprotoDid as _t, planAppUpdate as a, PLAYABLE_AUDIO as aa, ENROLL_CAPABILITY as ac, parseWakeFrame as ai, assertTokenUnits as an, signAppObject as ao, fetchesByItself as ar, handoffContext as as, verifySshSignature as at, groupTypingEpoch as b, GROUP_REACTION_FRAME as ba, TURN_LAST_SEQUENCE as bc, HoldKeys as bi, pubkyProofCapability as bn, RELAY_POLL_INTERVALS as bo, mayBeHub as br, readHandoffCancel as bs, shortAtprotoDid as bt, signAppStore as c, fileMessageText as ca, deviceEchoNonce as cc, checkPushEndpoint as ci, validatePaymentTarget as cn, canonicalJsonBytes as co, readGroupFileMeta as cr, handoffDigest as cs, decodeAnyBitcoinAddress as ct, APP_ICON_PATH as d, parseVideoMeta as da, devicePingFrame as dc, isAppVersion as di, SPARK_PROVIDER as dn, MAX_GROUP_CHAIN as do, readGroupWantNo as dr, handoffManifestFrames as ds, ATPROTO_PROOF_COLLECTION as dt, readImageMeta as ea, decodeInviteCode as ec, readPin as ei, toBase64Url as el, SEPOLIA_TEST_USDT as en, checkAppRevokeStatement as eo, engineText as er, handoffAttemptAllowed as es, SshSigError as et, appDigest as f, EDIT_BUFFER_MS as fa, newDeviceSetSecret as fc, APPS_CAPABILITY as fi, sparkAddressKind as fn, MEMBER_KEY as fo, CarriedTransport as fr, handoffOfferFrame as fs, ATPROTO_PROOF_SCOPE as ft, isAppUrl as g, parseLinkPreview as ga, TURN_RAISE_READ_TIMEOUT_MS as gc, UPGRADE_CAPABILITY as gi, isPubkyKey as gn, edgeParams as go, SIGNAL_REPLY_MS as gr, handoffStreamKey as gs, checkAtprotoProofRecord as gt, checkAppManifest as h, dhtEditId as ha, TURN_PUT_WINDOW_MS as hc, STATUS_CARD_CAPABILITY as hi, PUBKY_PROOF_MAX_BYTES as hn, verifyCommitSignature as ho, SIGNAL_HOLD_MS as hr, handoffRetryAfter as hs, atprotoProofRkey as ht, checkAppBeforeRun as i, sanitizeMime as ia, DEVICES_CAPABILITY as ic, newWakeToken as ii, WALLET_NETWORKS as in, readAppRevocations as io, GROUP_WANT_NO_FRAME as ir, handoffCancelFrame as is, sshSignCommand as it, encodeGroupEntryLink as j, readReply as ja, currentRelays as jc, IdentityCheckUnavailable as ji, isPublicAddress as jn, answersOffer as jo, mergeLobby as jr, setAckFrame as js, didSignedMessages as jt, KNOCK_TTL_MS as k, wireReaction as ka, DEFAULT_RELAYS as kc, IDENTITY_MAX_VALIDITY as ki, domainWellKnownFile as kn, CALL_SIGNAL_MAX_AGE_MS as ko, lobbyRecords as kr, SET_UPDATE as ks, didGhostlyServiceEntry as kt, APP_BUNDLE_LIMITS as l, parseVoiceMeta as la, deviceKeyZ32 as lc, vapidKeysMatch as li, walletNetworkOf as ln, readCanonicalJson as lo, readGroupHave as lr, handoffEphemeral as ls, ATPROTO_DECLARED_SCOPE as lt, assembleAppBundle as m, RateWindow as ma, webCryptoSigner as mc, EDIT_CAPABILITY as mi, isBitcoinAddress as mn, rosterHas as mo, SIGNAL_FRESH_MS as mr, handoffReleaseFrame as ms, atprotoProofRecord as mt, appStoreDecision as n, safeBlobType as na, inviteLink as nc, WakeLimiter as ni, utf8Decode as nl, TEST_USDT_FAUCET_AMOUNT as nn, isAppRef as no, GROUP_HAVE_FRAME as nr, handoffAttemptSucceeded as ns, parseSshSignature as nt, checkAppStoreIndex as o, VOICE_LIMITS as oa, HANDOFF_CAPABILITY as oc, relayRequest as oi, assertWholeSats as on, signAppRevocation as oo, groupFileFallback as or, handoffContextProof as os, verifyBitcoinMessage as ot, appViewOf as p, EDIT_RATE_WINDOW_MS as pa, seedSigner as pc, BUTTONS_CAPABILITY as pi, sparkInvoiceDetails as pn, commitHash as po, GROUP_SIGNAL_FRAME as pr, handoffPakeFrame as ps, atprotoDidMethod as pt, GROUP_EDIT_TEXT_BYTES as q, heardTime as qa, publicKeyFromZ32 as qc, irohRelayUrl as qi, parseCashuRequestPayload as qn, HANDOFF_OFFER as qo, typingActivity as qr, readEnrollHello as qs, encodeDidDhtPacket as qt, appUpdateDecision as r, sanitizeFileName as ra, inviteOwnership as rc, deviceWakeRequest as ri, utf8Encode as rl, USDT_PUBLIC_RPC as rn, readAppRevocation as ro, GROUP_WANT_FRAME as rr, handoffBusyFrame as rs, sameSshKey as rt, readAppStore as s, baseMime as sa, deviceEchoFrame as sc, wakeRequest as si, formatPaymentAmount as sn, canonicalJson as so, isGroupMessageId as sr, handoffDataFrame as ss, BitcoinAddressError as st, appRevocationFor as t, formatFileSize as ta, encodeInviteCode as tc, WAKE_CALL_INTERVAL_MS as ti, toZ32 as tl, SEPOLIA_TEST_USDT_FAUCET as tn, isAppKey as to, GROUP_FILE_LIMITS as tr, handoffAttemptFailed as ts, parseSshPublicKey as tt, APP_BUNDLE_MAGIC as u, voicePeaksOf as ua, deviceLinkPairing as uc, chatAppId as ui, SPARK_NETWORKS as un, GROUP_MEMBER_CAP as uo, readGroupWant as ur, handoffHelloFrame as us, ATPROTO_FULL_SCOPE as ut, GROUP_TYPING_FRAME as v, readForwarded as va, classifyTurnRead as vc, HOLD_DATED_BACK_MS as vi, newPubkyProofFolder as vn, DHT_POLL_INTERVALS as vo, readGroupSignal as vr, newHandoffId as vs, normalizeAtprotoHandle as vt, CommunitySession as w, queueReaction as wa, readTurnPacket as wc, CapsExchange as wi, pubkyHomeserverOf as wn, parseLocalTarget as wo, beaconKeys as wr, readHandoffPake as ws, DID_FILE_MAX_BYTES as wt, openGroupTyping as x, REACTION_LIMITS as xa, TURN_MAX as xc, HoldRefusedError as xi, pubkyProofFile as xn, presenceSeenAt as xo, meshHubs as xr, readHandoffDone as xs, AtprotoProofError as xt, GroupTypingBoard as y, COMMUNITY_REACTION_FRAME as ya, turnPutSummary as yc, HOLD_LIMITS as yi, normalizePubkyKey as yn, EXPECT_PEER_MS as yo, MESH_HUBS as yr, readHandoffAck as ys, parseAtprotoDidDocument as yt, groupWakeFrame as z, buttonLabelClash as za, RELAY_PAYLOAD_MAX_BYTES as zc, revokesIdentity as zi, requestLnurlInvoice as zn, HTTP_SERVICE_PROTO as zo, ChatFiles as zr, enrollCancelFrame as zs, parseDidDocument as zt };
|
|
29096
29689
|
|
|
29097
|
-
//# sourceMappingURL=src-
|
|
29690
|
+
//# sourceMappingURL=src-aKGjb_yz.js.map
|