@ghostlytools/cli 1.2.2 → 1.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/assets/{arkade-_gS9wShF.js → arkade-CdnKGDbg.js} +2 -2
- package/dist/assets/{arkade-_gS9wShF.js.map → arkade-CdnKGDbg.js.map} +1 -1
- package/dist/assets/bark_ffi_wasm_bg-EY2uc0Bx.js.map +1 -1
- package/dist/assets/bitcoindevkit_bg-B-iLWrmX.js.map +1 -1
- package/dist/assets/fedimint_client_wasm_bg-BtHHlFpp.js.map +1 -1
- package/dist/assets/{hyperdhtRelay-CgYLaQz3.js → hyperdhtRelay-C1p0nkle.js} +3 -3
- package/dist/assets/{hyperdhtRelay-CgYLaQz3.js.map → hyperdhtRelay-C1p0nkle.js.map} +1 -1
- package/dist/assets/{mainline-1v8OTYvp.js → mainline-BH6s8HVi.js} +5 -2
- package/dist/assets/{mainline-1v8OTYvp.js.map → mainline-BH6s8HVi.js.map} +1 -1
- package/dist/assets/{server-Cy4ox7oD.js → server-0zfNpABQ.js} +1055 -185
- package/dist/assets/server-0zfNpABQ.js.map +1 -0
- package/dist/assets/{src-XzcHzneo.js → src-CbLkHfz8.js} +454 -80
- package/dist/assets/src-CbLkHfz8.js.map +1 -0
- package/dist/assets/{usdt-Deo3g0Nq.js → usdt-Qf4UuygS.js} +2 -2
- package/dist/assets/{usdt-Deo3g0Nq.js.map → usdt-Qf4UuygS.js.map} +1 -1
- package/dist/ghostly.mjs +362 -114
- package/dist/ghostly.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/assets/server-Cy4ox7oD.js.map +0 -1
- package/dist/assets/src-XzcHzneo.js.map +0 -1
|
@@ -584,6 +584,7 @@ function withRequestOptions(transport, extra) {
|
|
|
584
584
|
if (transport.subscribe) wrapped.subscribe = (listener) => transport.subscribe(listener);
|
|
585
585
|
if (transport.networkChanged) wrapped.networkChanged = () => transport.networkChanged();
|
|
586
586
|
if (transport.readAnsweredAt) wrapped.readAnsweredAt = (pubKeyZ32) => transport.readAnsweredAt(pubKeyZ32);
|
|
587
|
+
if (transport.readHeldBackFor) wrapped.readHeldBackFor = (pubKeyZ32) => transport.readHeldBackFor(pubKeyZ32);
|
|
587
588
|
if (transport.onServerTime) wrapped.onServerTime = (listener) => transport.onServerTime(listener);
|
|
588
589
|
if (transport.configure) wrapped.configure = (options) => transport.configure(options);
|
|
589
590
|
if (transport.turnRead) wrapped.turnRead = (pubKeyZ32, options) => transport.turnRead(pubKeyZ32, options);
|
|
@@ -798,6 +799,11 @@ var isGroupSignal = (who) => who.group && !who.background && (who.signal || isWa
|
|
|
798
799
|
var watchLane = (who) => `${+who.urgent}${+who.background}`;
|
|
799
800
|
/** A group's read that is not a background one: an edge's, a private group's knock poll (`GROUP_RATION_WINDOW_MS`). */
|
|
800
801
|
var isGroupRead = (who) => who.group && !who.write && !who.background;
|
|
802
|
+
/**
|
|
803
|
+
* A read that takes its turn per key with the others (`READ_TURN_MS`): a background one, and a 1:1 chat's at any pace.
|
|
804
|
+
* Not a community door's bell.
|
|
805
|
+
*/
|
|
806
|
+
var isTurnRead = (who) => !who.write && !who.door && (who.background || !who.group);
|
|
801
807
|
/** A write that goes first once the budget frees a request (see `WRITE_FIRST_MS`): a chat's or a group's, never a background one. */
|
|
802
808
|
var firstWriter = (who) => !who.write || who.background ? null : who.group ? "group" : "chat";
|
|
803
809
|
/**
|
|
@@ -871,6 +877,29 @@ var WATCH_SHARE = 1 / 3;
|
|
|
871
877
|
* waits, the others wait too (`WATCH_SHARE`, `GROUP_RATION_WINDOW_MS`). One that stops asking loses it.
|
|
872
878
|
*/
|
|
873
879
|
var WATCH_TURN_MS = 5e3;
|
|
880
|
+
/**
|
|
881
|
+
* Background reads, and a 1:1 chat's at any pace, take turns per key the same way: while a key read longer ago asked
|
|
882
|
+
* within this long and was not read since, a read of another key waits (a chat's, at a pace that is not the background
|
|
883
|
+
* one, only for another chat's key). A saved chat that is not live reads its contact every 30 s in the
|
|
884
|
+
* background, and one the budget refused read again only at its next poll. A request frees 60 s after the one that took
|
|
885
|
+
* it, which is the poll after next of the chat that took it: the same chats took every request that freed. A daemon with
|
|
886
|
+
* 30 chats and 5 groups, back after a restart while its groups signaled, read some contacts once in 10 min, and those
|
|
887
|
+
* chats stayed down although both sides were online and offering (CLI bug hunt, 2026-10-12). A link's background read
|
|
888
|
+
* the budget refused reads again when it frees a request (`LinkSession`), so the key whose turn it is is there to take it.
|
|
889
|
+
* A chat not live after a restart looks fast while its offer is out, and those reads took no turn: the budget answered
|
|
890
|
+
* them from the packet read before, every 2 to 8 s for 10 minutes, while the chats that were live already, the groups'
|
|
891
|
+
* edges and the writes took every request that freed. A quarter of a daemon's 30 chats read their contact once in 10
|
|
892
|
+
* minutes, their contacts online and offering, and showed them offline at 6 minutes (CLI bug hunt, 2026-10-13).
|
|
893
|
+
*/
|
|
894
|
+
var READ_TURN_MS = 5e3;
|
|
895
|
+
/**
|
|
896
|
+
* A 1:1 chat's read the budget held back this long without a break is starved: it goes before the writes that wait
|
|
897
|
+
* (`WRITE_FIRST_MS`) and past the background share, within the minute, the key held back longest first. A daemon back
|
|
898
|
+
* after a restart spends its whole minute on writes (each chat's and edge's offer, its packets after the attempt), which
|
|
899
|
+
* go first, and requests free in bursts as the minute's first requests age out: every burst went to the writes that
|
|
900
|
+
* waited, and the reads of the chats that were not in an attempt never had their turn (CLI restart lab, 2026-10-13).
|
|
901
|
+
*/
|
|
902
|
+
var READ_STARVED_MS = 6e4;
|
|
874
903
|
/** How long a read that let an older one go first waits before it asks again. */
|
|
875
904
|
var TURN_WAIT_MS = 1e3;
|
|
876
905
|
/**
|
|
@@ -990,6 +1019,8 @@ var RelayTransport = class {
|
|
|
990
1019
|
* it is under the watch share (`WATCH_SHARE`) and while the groups' reads are rationed (`GROUP_RATION_WINDOW_MS`).
|
|
991
1020
|
*/
|
|
992
1021
|
readTurns = /* @__PURE__ */ new Map();
|
|
1022
|
+
/** Per key read in turn (`isTurnRead`), when a read of it last went out and when it last asked (`READ_TURN_MS`). */
|
|
1023
|
+
keyTurns = /* @__PURE__ */ new Map();
|
|
993
1024
|
/** Groups' reads (not background ones) on each relay in the last `GROUP_RATION_WINDOW_MS`. */
|
|
994
1025
|
spentGroupReads = /* @__PURE__ */ new Map();
|
|
995
1026
|
/** When a group's read last found each relay's minute full: its reads there are rationed for `GROUP_RATION_MS`. */
|
|
@@ -1009,6 +1040,11 @@ var RelayTransport = class {
|
|
|
1009
1040
|
/** Per relay and key, the newest packet that relay held back for its budget while another relay took it (`catchUpLater`). */
|
|
1010
1041
|
catchUp = /* @__PURE__ */ new Map();
|
|
1011
1042
|
catchUpTimer = null;
|
|
1043
|
+
/**
|
|
1044
|
+
* Per key whose reads the budget held back since its last read went out: when a request frees for it
|
|
1045
|
+
* (`readHeldBackFor`), how many times in a row, and since when (`READ_STARVED_MS`).
|
|
1046
|
+
*/
|
|
1047
|
+
heldBack = /* @__PURE__ */ new Map();
|
|
1012
1048
|
/** When the relays last answered a read of each key, for `FRESH_READ_MS`. */
|
|
1013
1049
|
readAt = /* @__PURE__ */ new Map();
|
|
1014
1050
|
freshReadMs;
|
|
@@ -1398,8 +1434,14 @@ var RelayTransport = class {
|
|
|
1398
1434
|
}
|
|
1399
1435
|
const resting = this.relays.every((r) => this.isCoolingDown(r, "GET") || this.breaker.blockedFor(r) > 0 || this.heldFor(r, who, Date.now(), pubKeyZ32) > 0);
|
|
1400
1436
|
const known = down ? this.newest.get(pubKeyZ32) : newerPacket(this.newest.get(pubKeyZ32), this.written.get(pubKeyZ32));
|
|
1401
|
-
if (resting && known)
|
|
1402
|
-
|
|
1437
|
+
if (resting && known) {
|
|
1438
|
+
if (!down && budgetWait < Infinity) this.heldBackUntil(pubKeyZ32, Date.now() + budgetWait);
|
|
1439
|
+
return known;
|
|
1440
|
+
}
|
|
1441
|
+
if (!down && budgetWait < Infinity) {
|
|
1442
|
+
this.heldBackUntil(pubKeyZ32, Date.now() + budgetWait);
|
|
1443
|
+
throw new DiscoveryBudgetError(budgetWait);
|
|
1444
|
+
}
|
|
1403
1445
|
throw new Error("No Pkarr relay reachable");
|
|
1404
1446
|
}
|
|
1405
1447
|
/** The order a key's read asks the relays in: in turn per key (see `resolve`), relays found slow lately last. */
|
|
@@ -1466,9 +1508,25 @@ var RelayTransport = class {
|
|
|
1466
1508
|
readAnsweredAt(pubKeyZ32) {
|
|
1467
1509
|
return this.readAt.get(pubKeyZ32);
|
|
1468
1510
|
}
|
|
1511
|
+
readHeldBackFor(pubKeyZ32) {
|
|
1512
|
+
const held = this.heldBack.get(pubKeyZ32);
|
|
1513
|
+
const left = held === void 0 ? 0 : held.until - Date.now();
|
|
1514
|
+
return left > 0 ? left : void 0;
|
|
1515
|
+
}
|
|
1516
|
+
heldBackUntil(pubKeyZ32, until) {
|
|
1517
|
+
const held = this.heldBack.get(pubKeyZ32), times = (held?.times ?? 0) + 1;
|
|
1518
|
+
this.heldBack.delete(pubKeyZ32);
|
|
1519
|
+
this.heldBack.set(pubKeyZ32, {
|
|
1520
|
+
until,
|
|
1521
|
+
times,
|
|
1522
|
+
since: held?.since ?? Date.now()
|
|
1523
|
+
});
|
|
1524
|
+
if (this.heldBack.size > READ_TURNS_KEPT) this.heldBack.delete(this.heldBack.keys().next().value);
|
|
1525
|
+
}
|
|
1469
1526
|
/** The relays answered a read of this key now; answers older than `FRESH_READ_MS` are dropped as the list grows. */
|
|
1470
1527
|
answeredRead(pubKeyZ32) {
|
|
1471
1528
|
const now = Date.now();
|
|
1529
|
+
this.heldBack.delete(pubKeyZ32);
|
|
1472
1530
|
this.readAt.delete(pubKeyZ32);
|
|
1473
1531
|
this.readAt.set(pubKeyZ32, now);
|
|
1474
1532
|
if (this.readAt.size > 64) for (const [key, at] of this.readAt) {
|
|
@@ -1584,6 +1642,7 @@ var RelayTransport = class {
|
|
|
1584
1642
|
if (isChat(who)) this.chatNeed(key).urgentAt = now;
|
|
1585
1643
|
}
|
|
1586
1644
|
if (isGroupRead(who)) this.readAsked(key, who, now);
|
|
1645
|
+
if (isTurnRead(who)) this.turnAsked(key, who, now);
|
|
1587
1646
|
if (this.heldFor(relay, who, now, key) > 0) {
|
|
1588
1647
|
if (isGroupRead(who) && (this.spent.get(relay)?.length ?? 0) >= this.limitOf(relay)) this.groupsFullAt.set(relay, now);
|
|
1589
1648
|
if (isChat(who)) this.chatNeed(key).refused.set(relay, now);
|
|
@@ -1607,6 +1666,12 @@ var RelayTransport = class {
|
|
|
1607
1666
|
lane: watchLane(who)
|
|
1608
1667
|
});
|
|
1609
1668
|
}
|
|
1669
|
+
if (isTurnRead(who)) this.keyTurns.set(key, {
|
|
1670
|
+
servedAt: now,
|
|
1671
|
+
askedAt: now,
|
|
1672
|
+
waiting: false,
|
|
1673
|
+
who
|
|
1674
|
+
});
|
|
1610
1675
|
return true;
|
|
1611
1676
|
}
|
|
1612
1677
|
/**
|
|
@@ -1618,7 +1683,7 @@ var RelayTransport = class {
|
|
|
1618
1683
|
* signaling goes over the limit by its allowance (`SIGNALING_ALLOWANCE_SHARE`), and so do a few of its reads of a
|
|
1619
1684
|
* contact that just went away (`DEPARTED_READS`).
|
|
1620
1685
|
*/
|
|
1621
|
-
heldFor(relay, who, now = Date.now(), key) {
|
|
1686
|
+
heldFor(relay, who, now = Date.now(), key, turns = true) {
|
|
1622
1687
|
const recent = (this.spent.get(relay) ?? []).filter((at) => now - at < 6e4);
|
|
1623
1688
|
const recentBackground = (this.spentBackground.get(relay) ?? []).filter((at) => now - at < 6e4);
|
|
1624
1689
|
this.spent.set(relay, recent);
|
|
@@ -1628,10 +1693,11 @@ var RelayTransport = class {
|
|
|
1628
1693
|
const departed = isDepartedRead(who) && key !== void 0 && recent.length >= limit && this.departedReads(key, now) < 6;
|
|
1629
1694
|
const share = isChatSignal(who) || departed ? SIGNALING_ALLOWANCE_SHARE : isGroupSignal(who) ? GROUP_SIGNALING_ALLOWANCE_SHARE : 0;
|
|
1630
1695
|
let wait = over(recent, limit + (share ? Math.max(0, Math.floor(limit * share) - (who.write ? 0 : 1)) : 0));
|
|
1696
|
+
const starved = key !== void 0 && this.starved(key, who, now);
|
|
1631
1697
|
const groupOver = (this.spentGroupOver.get(relay) ?? []).filter((at) => now - at < GROUP_OVER_PACE_MS);
|
|
1632
1698
|
this.spentGroupOver.set(relay, groupOver);
|
|
1633
1699
|
if (isGroupSignal(who) && !who.write && recent.length >= limit && groupOver.length > 0) wait = Math.max(wait, groupOver[groupOver.length - 1] + GROUP_OVER_PACE_MS - now);
|
|
1634
|
-
if (who.background) {
|
|
1700
|
+
if (who.background && !starved) {
|
|
1635
1701
|
wait = Math.max(wait, over(recentBackground, this.backgroundPerMinute));
|
|
1636
1702
|
const signaling = this.urgentAt + SIGNALING_WINDOW_MS - now;
|
|
1637
1703
|
if (signaling > 0 && !who.door) wait = Math.max(wait, Math.min(signaling, over(recentBackground, this.backgroundWhileSignaling)));
|
|
@@ -1666,8 +1732,9 @@ var RelayTransport = class {
|
|
|
1666
1732
|
const at = this.writeWaiting.get(`${lane} ${relay}`);
|
|
1667
1733
|
return at !== void 0 && now - at < 5e3 ? at + WRITE_FIRST_MS - now : 0;
|
|
1668
1734
|
};
|
|
1669
|
-
if (writer !== "chat" && !departed) wait = Math.max(wait, waiting("chat"));
|
|
1670
|
-
if (writer === null && who.group && !pastLimit || who.background) wait = Math.max(wait, waiting("group"));
|
|
1735
|
+
if (writer !== "chat" && !departed && !starved) wait = Math.max(wait, waiting("chat"));
|
|
1736
|
+
if ((writer === null && who.group && !pastLimit || who.background) && !starved) wait = Math.max(wait, waiting("group"));
|
|
1737
|
+
if (turns && wait <= 0 && isTurnRead(who) && this.turnAhead(relay, key, who, now)) wait = TURN_WAIT_MS;
|
|
1671
1738
|
return Math.max(wait, 0);
|
|
1672
1739
|
}
|
|
1673
1740
|
/** A group's read of `key` asks: it waits for its turn from now on, unless it is taken (`readsAhead`). */
|
|
@@ -1697,6 +1764,39 @@ var RelayTransport = class {
|
|
|
1697
1764
|
}
|
|
1698
1765
|
return ahead;
|
|
1699
1766
|
}
|
|
1767
|
+
/** A read of `key` that takes turns asks: it waits for its turn from now on, unless it is taken (`turnAhead`). */
|
|
1768
|
+
turnAsked(key, who, now) {
|
|
1769
|
+
for (const [other, turn] of this.keyTurns) if (now - Math.max(turn.askedAt, turn.servedAt) >= 6e4) this.keyTurns.delete(other);
|
|
1770
|
+
this.keyTurns.set(key, {
|
|
1771
|
+
servedAt: this.keyTurns.get(key)?.servedAt ?? -Infinity,
|
|
1772
|
+
askedAt: now,
|
|
1773
|
+
waiting: true,
|
|
1774
|
+
who
|
|
1775
|
+
});
|
|
1776
|
+
}
|
|
1777
|
+
/**
|
|
1778
|
+
* Whether a key read in turn longer ago than `key`, still asking, would take this relay's next request: one read in the
|
|
1779
|
+
* background for a background read, and a chat's that is starved (`READ_STARVED_MS`) for any. One asks again after
|
|
1780
|
+
* the budget held it back (a link's poll does, a one-off look does not), and keeps asking within `READ_TURN_MS` of its
|
|
1781
|
+
* last ask, or of when it was told the budget frees a request for it (`readHeldBackFor`).
|
|
1782
|
+
* One the budget holds back on its own account (a group's read, for the chats' reserve) keeps no turn: a group key
|
|
1783
|
+
* waiting for the chats held every chat's read back (CLI lab, 2026-10-13).
|
|
1784
|
+
*/
|
|
1785
|
+
turnAhead(relay, key, who, now) {
|
|
1786
|
+
const mine = key === void 0 ? -Infinity : this.keyTurns.get(key)?.servedAt ?? -Infinity;
|
|
1787
|
+
for (const [other, turn] of this.keyTurns) {
|
|
1788
|
+
const held = this.heldBack.get(other);
|
|
1789
|
+
if (other === key || !turn.waiting || turn.servedAt >= mine || !held || held.times < 2) continue;
|
|
1790
|
+
if (!(who.background && turn.who.background) && !this.starved(other, turn.who, now)) continue;
|
|
1791
|
+
if (now - Math.max(turn.askedAt, held.until) < READ_TURN_MS && this.heldFor(relay, turn.who, now, other, false) === 0) return true;
|
|
1792
|
+
}
|
|
1793
|
+
return false;
|
|
1794
|
+
}
|
|
1795
|
+
/** A 1:1 chat's read of `key` the budget has held back for `READ_STARVED_MS` without a break. */
|
|
1796
|
+
starved(key, who, now) {
|
|
1797
|
+
const since = this.heldBack.get(key)?.since;
|
|
1798
|
+
return !who.group && !who.write && !who.door && since !== void 0 && now - since >= READ_STARVED_MS;
|
|
1799
|
+
}
|
|
1700
1800
|
/** This contact's departed reads past the limit in the last minute (`DEPARTED_READS`). */
|
|
1701
1801
|
departedReads(key, now) {
|
|
1702
1802
|
const reads = (this.departedOver.get(key) ?? []).filter((at) => now - at < 6e4);
|
|
@@ -2003,6 +2103,33 @@ function turnSequence(turn, rev, author) {
|
|
|
2003
2103
|
if (!Number.isInteger(author) || author < 0 || author >= 4) throw new Error("An author is a slot, 0 to 3");
|
|
2004
2104
|
return turn * 2 ** 20 + rev * 4 + author;
|
|
2005
2105
|
}
|
|
2106
|
+
var VERIFIER_DOMAIN = "ghostly-turn-verifier/1";
|
|
2107
|
+
var VERIFIER_TAG = Uint8Array.of(103, 118);
|
|
2108
|
+
var VERIFIER_TAG_AT = 3;
|
|
2109
|
+
var VERIFIER_MARK_AT = 5;
|
|
2110
|
+
/** The mark of a verifier (its `setup` and `record` as stored), or of a profile that has none (null). 3 bytes. */
|
|
2111
|
+
function turnVerifierMark(verifier) {
|
|
2112
|
+
return sha256(utf8Encode(`${VERIFIER_DOMAIN}\n${verifier?.setup ?? ""}\n${verifier?.record ?? ""}`)).slice(0, 3);
|
|
2113
|
+
}
|
|
2114
|
+
/**
|
|
2115
|
+
* A record's `instance` that states `mark` (`turnVerifierMark`), or, with no mark, one that states nothing: 8 random
|
|
2116
|
+
* bytes that never carry the marker (a tombstone, a record written where the profile's verifier is not at hand).
|
|
2117
|
+
*/
|
|
2118
|
+
function turnInstance(mark, random = randomBytes) {
|
|
2119
|
+
if (mark && mark.length !== 3) throw new Error("A verifier mark is 3 bytes");
|
|
2120
|
+
const instance = random(8).slice(0, 8);
|
|
2121
|
+
if (instance.length !== 8) throw new Error("An instance is 8 bytes");
|
|
2122
|
+
if (mark) {
|
|
2123
|
+
instance.set(VERIFIER_TAG, VERIFIER_TAG_AT);
|
|
2124
|
+
instance.set(mark, VERIFIER_MARK_AT);
|
|
2125
|
+
} else if (turnStatedVerifier(instance)) instance[VERIFIER_TAG_AT] ^= 255;
|
|
2126
|
+
return instance;
|
|
2127
|
+
}
|
|
2128
|
+
/** The verifier mark an `instance` states, or null when it states none (a record written by an app from before). */
|
|
2129
|
+
function turnStatedVerifier(instance) {
|
|
2130
|
+
if (instance.length !== 8 || instance[VERIFIER_TAG_AT] !== VERIFIER_TAG[0] || instance[4] !== VERIFIER_TAG[1]) return null;
|
|
2131
|
+
return instance.slice(VERIFIER_MARK_AT);
|
|
2132
|
+
}
|
|
2006
2133
|
var TurnRecordError = class extends Error {
|
|
2007
2134
|
refusal;
|
|
2008
2135
|
constructor(refusal, detail) {
|
|
@@ -4309,6 +4436,7 @@ function parseCallSignal(json, now = Date.now()) {
|
|
|
4309
4436
|
if (raw.t !== "a" || typeof raw.re !== "number" || !Number.isFinite(raw.re)) return null;
|
|
4310
4437
|
restart.re = raw.re;
|
|
4311
4438
|
}
|
|
4439
|
+
const closed = raw.vr === 0 ? { vr: 0 } : {};
|
|
4312
4440
|
if (raw.o !== void 0 && (typeof raw.o !== "number" || !Number.isFinite(raw.o))) return null;
|
|
4313
4441
|
const answers = raw.t === "a" && raw.o !== void 0 ? { o: raw.o } : {};
|
|
4314
4442
|
return {
|
|
@@ -4324,6 +4452,7 @@ function parseCallSignal(json, now = Date.now()) {
|
|
|
4324
4452
|
...picture,
|
|
4325
4453
|
...payloadTypes,
|
|
4326
4454
|
...restart,
|
|
4455
|
+
...closed,
|
|
4327
4456
|
...answers,
|
|
4328
4457
|
...heard
|
|
4329
4458
|
};
|
|
@@ -4422,7 +4551,7 @@ function buildSdpFromSignal(signal) {
|
|
|
4422
4551
|
} else if (mediaType === "v") {
|
|
4423
4552
|
lines.push(`m=video 9 UDP/TLS/RTP/SAVPF ${vp8}`, "c=IN IP4 0.0.0.0", "a=rtcp:9 IN IP4 0.0.0.0");
|
|
4424
4553
|
for (const c of signal.c ?? []) lines.push(`a=candidate:${c}`);
|
|
4425
|
-
lines.push("a=end-of-candidates", `a=ice-ufrag:${signal.u}`, `a=ice-pwd:${signal.p}`, `a=fingerprint:${fingerprint}`, `a=setup:${signal.s}`, `a=mid:${mid}`, "a=sendrecv",
|
|
4554
|
+
lines.push("a=end-of-candidates", `a=ice-ufrag:${signal.u}`, `a=ice-pwd:${signal.p}`, `a=fingerprint:${fingerprint}`, `a=setup:${signal.s}`, `a=mid:${mid}`, ...signal.vr === 0 ? ["a=inactive"] : ["a=sendrecv", "a=msid:stream video0"], "a=rtcp-mux", "a=rtcp-rsize", `a=rtpmap:${vp8} VP8/90000`, `a=rtcp-fb:${vp8} ccm fir`, `a=rtcp-fb:${vp8} nack`, `a=rtcp-fb:${vp8} nack pli`, `a=rtcp-fb:${vp8} goog-remb`, ...signal.vr === 0 ? [] : [`a=ssrc:${videoSsrc} cname:pkarr`, `a=ssrc:${videoSsrc} msid:stream video0`]);
|
|
4426
4555
|
}
|
|
4427
4556
|
});
|
|
4428
4557
|
return lines.join("\r\n") + "\r\n";
|
|
@@ -5619,10 +5748,14 @@ var LinkSession = class {
|
|
|
5619
5748
|
pollAgain = false;
|
|
5620
5749
|
publishing = null;
|
|
5621
5750
|
publishAgain = false;
|
|
5751
|
+
/** What waits to go out is the presence heartbeat alone (`PRESENCE_HEARTBEAT`): nothing in the packet changed. */
|
|
5752
|
+
upkeepOnly = false;
|
|
5622
5753
|
pollTimer = null;
|
|
5623
5754
|
heartbeatTimer = null;
|
|
5624
5755
|
lastActivity = Date.now();
|
|
5625
5756
|
startedAt = Date.now();
|
|
5757
|
+
/** From when a peer never seen is awaited (`AWAITING_PEER_MS`): never later than this start, whatever the clock said when the chat was made. */
|
|
5758
|
+
awaitingSince;
|
|
5626
5759
|
intervals;
|
|
5627
5760
|
fastPollUntil = 0;
|
|
5628
5761
|
/** An offer to a saved contact is out: from then on its fast window looks less often (`OFFER_FAST_MS`); 0 for none. */
|
|
@@ -5677,6 +5810,7 @@ var LinkSession = class {
|
|
|
5677
5810
|
this.intervals = options.pollIntervals ?? DHT_POLL_INTERVALS;
|
|
5678
5811
|
this.events = options.events ?? {};
|
|
5679
5812
|
this.firstPublish = options.firstPublish ?? "at-start";
|
|
5813
|
+
this.awaitingSince = Math.min(options.awaitingSince ?? this.startedAt, this.startedAt);
|
|
5680
5814
|
}
|
|
5681
5815
|
get peerPresence() {
|
|
5682
5816
|
return this.presence;
|
|
@@ -5880,11 +6014,12 @@ var LinkSession = class {
|
|
|
5880
6014
|
t: timestamp,
|
|
5881
6015
|
m: text
|
|
5882
6016
|
}];
|
|
6017
|
+
this.upkeepOnly = false;
|
|
5883
6018
|
try {
|
|
5884
6019
|
if (await this.publishOnce(true) === 0) return "Message could not be published — DHT payload limit exceeded.";
|
|
5885
6020
|
} catch (error) {
|
|
5886
6021
|
if (this.publishRetryTimer) clearTimeout(this.publishRetryTimer);
|
|
5887
|
-
this.publishRetryTimer = setTimeout(() => void this.publish().catch(() => {}), this.retryDelay(error));
|
|
6022
|
+
this.publishRetryTimer = setTimeout(() => void this.publish(true).catch(() => {}), this.retryDelay(error));
|
|
5888
6023
|
if (!isDiscoveryBudgetError(error)) this.events.onStatus?.("error");
|
|
5889
6024
|
return null;
|
|
5890
6025
|
}
|
|
@@ -5940,7 +6075,7 @@ var LinkSession = class {
|
|
|
5940
6075
|
if (this.connected) return "connected";
|
|
5941
6076
|
if (Date.now() < this.fastPollUntil) return "fast";
|
|
5942
6077
|
if (Date.now() < this.watchUntil) return "active";
|
|
5943
|
-
if (!this.active && this.presence.lastPacketAt === 0 && Date.now() - this.
|
|
6078
|
+
if (!this.active && this.presence.lastPacketAt === 0 && Date.now() - this.awaitingSince < 6e5) return "active";
|
|
5944
6079
|
if (!this.active) return "background";
|
|
5945
6080
|
return Date.now() - this.lastActivity > 6e4 ? "idle" : "active";
|
|
5946
6081
|
}
|
|
@@ -5948,13 +6083,18 @@ var LinkSession = class {
|
|
|
5948
6083
|
if (this.heartbeatTimer) clearTimeout(this.heartbeatTimer);
|
|
5949
6084
|
this.heartbeatTimer = setTimeout(() => {
|
|
5950
6085
|
if (!this.running) return;
|
|
5951
|
-
if (this.getServices() !== void 0) this.publish().catch(() => {});
|
|
6086
|
+
if (this.getServices() !== void 0) this.publish(true).catch(() => {});
|
|
5952
6087
|
this.scheduleHeartbeat();
|
|
5953
6088
|
}, PRESENCE_HEARTBEAT);
|
|
5954
6089
|
}
|
|
5955
|
-
/**
|
|
5956
|
-
|
|
6090
|
+
/**
|
|
6091
|
+
* Coalesces publishes: at most one in flight and one queued behind it. `upkeep`: nothing new to say (the heartbeat, a
|
|
6092
|
+
* retry of what waits): with nothing else asked for since, the packet goes as upkeep (`publishOnce`).
|
|
6093
|
+
*/
|
|
6094
|
+
publish(upkeep = false) {
|
|
5957
6095
|
if (this.silent) return Promise.resolve();
|
|
6096
|
+
if (!upkeep) this.upkeepOnly = false;
|
|
6097
|
+
else if (!this.publishing && !this.publishRetryTimer) this.upkeepOnly = true;
|
|
5958
6098
|
if (this.publishing) {
|
|
5959
6099
|
this.publishAgain = true;
|
|
5960
6100
|
return this.publishing;
|
|
@@ -5965,12 +6105,12 @@ var LinkSession = class {
|
|
|
5965
6105
|
try {
|
|
5966
6106
|
do {
|
|
5967
6107
|
this.publishAgain = false;
|
|
5968
|
-
await this.publishOnce(this.running);
|
|
6108
|
+
await this.publishOnce(this.running, this.upkeepOnly);
|
|
5969
6109
|
} while (this.publishAgain && this.running);
|
|
5970
6110
|
} catch (error) {
|
|
5971
6111
|
if (this.running) {
|
|
5972
6112
|
if (!isDiscoveryBudgetError(error)) this.discoveryResult("publish", error);
|
|
5973
|
-
this.publishRetryTimer = setTimeout(() => void this.publish().catch(() => {}), this.retryDelay(error));
|
|
6113
|
+
this.publishRetryTimer = setTimeout(() => void this.publish(true).catch(() => {}), this.retryDelay(error));
|
|
5974
6114
|
}
|
|
5975
6115
|
throw error;
|
|
5976
6116
|
} finally {
|
|
@@ -5979,7 +6119,7 @@ var LinkSession = class {
|
|
|
5979
6119
|
})();
|
|
5980
6120
|
return this.publishing;
|
|
5981
6121
|
}
|
|
5982
|
-
async publishOnce(advertise) {
|
|
6122
|
+
async publishOnce(advertise, upkeep = false) {
|
|
5983
6123
|
const rtcSignal = advertise ? this.rtcSignal : null;
|
|
5984
6124
|
const built = buildLinkRecords(this.identity.pubKeyZ32, {
|
|
5985
6125
|
messages: this.sentBuffer,
|
|
@@ -5992,7 +6132,7 @@ var LinkSession = class {
|
|
|
5992
6132
|
}, this.encKey);
|
|
5993
6133
|
const started = Date.now();
|
|
5994
6134
|
try {
|
|
5995
|
-
const options = rtcSignal && rtcSignal !== this.rtcSignalOut ? { signal: true } : this.watching() ? { watch: true } : void 0;
|
|
6135
|
+
const options = rtcSignal && rtcSignal !== this.rtcSignalOut ? { signal: true } : this.watching() ? { watch: true } : upkeep && this.connected ? { background: true } : void 0;
|
|
5996
6136
|
await this.transport.publish(this.identity, built.records, options);
|
|
5997
6137
|
} catch (error) {
|
|
5998
6138
|
const ms = Date.now() - started, waiting = isDiscoveryBudgetError(error);
|
|
@@ -6056,13 +6196,15 @@ var LinkSession = class {
|
|
|
6056
6196
|
nextInMs: 0
|
|
6057
6197
|
});
|
|
6058
6198
|
const started = Date.now();
|
|
6199
|
+
let heldBackMs;
|
|
6059
6200
|
try {
|
|
6060
6201
|
const pace = this.pace();
|
|
6061
6202
|
const signal = pace === "fast" && this.fastStepsAfter > 0 && this.rtcSignalOut !== null;
|
|
6062
6203
|
const watch = !signal && this.watching();
|
|
6063
6204
|
const departed = !signal && Date.now() < this.departedUntil;
|
|
6205
|
+
const background = pace === "background" || pace === "connected";
|
|
6064
6206
|
const packet = await this.transport.resolve(this.peerPubKeyZ32, {
|
|
6065
|
-
background
|
|
6207
|
+
background,
|
|
6066
6208
|
urgent: pace === "fast",
|
|
6067
6209
|
...signal && { signal },
|
|
6068
6210
|
...watch && { watch },
|
|
@@ -6077,6 +6219,7 @@ var LinkSession = class {
|
|
|
6077
6219
|
const read = answered !== void 0 && answered >= started;
|
|
6078
6220
|
if (read) this.lastReadAt = started;
|
|
6079
6221
|
this.readMissed = !!this.transport.readAnsweredAt && !read;
|
|
6222
|
+
if (this.readMissed) heldBackMs = this.transport.readHeldBackFor?.(this.peerPubKeyZ32);
|
|
6080
6223
|
let receivedNew = false;
|
|
6081
6224
|
const batch = packet ? parseLinkRecords(packet, this.encKey) : null;
|
|
6082
6225
|
if (batch && isEmptyLinkPacket(batch)) {
|
|
@@ -6165,7 +6308,8 @@ var LinkSession = class {
|
|
|
6165
6308
|
ms: Date.now() - started,
|
|
6166
6309
|
error: String(error)
|
|
6167
6310
|
});
|
|
6168
|
-
if (
|
|
6311
|
+
if (isDiscoveryBudgetError(error)) heldBackMs = error.retryInMs;
|
|
6312
|
+
else {
|
|
6169
6313
|
this.discoveryResult("read", error);
|
|
6170
6314
|
this.events.onStatus?.("error");
|
|
6171
6315
|
}
|
|
@@ -6183,7 +6327,7 @@ var LinkSession = class {
|
|
|
6183
6327
|
this.poll();
|
|
6184
6328
|
} else if (this.running && !this.pollTimer) {
|
|
6185
6329
|
const interval = this.nextInterval();
|
|
6186
|
-
const wait = Math.max(MIN_POLL_GAP_MS, interval - (Date.now() - started));
|
|
6330
|
+
const wait = heldBackMs !== void 0 ? Math.min(interval, Math.max(BUDGET_RETRY_MIN_MS, heldBackMs)) : Math.max(MIN_POLL_GAP_MS, interval - (Date.now() - started));
|
|
6187
6331
|
this.events.onPoll?.({
|
|
6188
6332
|
polling: false,
|
|
6189
6333
|
nextInMs: wait
|
|
@@ -6351,6 +6495,8 @@ var DataLink = class {
|
|
|
6351
6495
|
answered = null;
|
|
6352
6496
|
/** Answers refused since a connection last opened, each followed by a new offer at once (`ANSWER_REFUSED_REDIALS`). */
|
|
6353
6497
|
refusedAnswers = 0;
|
|
6498
|
+
/** The last answer dropped as not for this side's offer (`answer-dropped`): each is said once. */
|
|
6499
|
+
droppedAnswerTs = 0;
|
|
6354
6500
|
constructor(options) {
|
|
6355
6501
|
this.options = options;
|
|
6356
6502
|
}
|
|
@@ -6459,6 +6605,12 @@ var DataLink = class {
|
|
|
6459
6605
|
} catch (error) {
|
|
6460
6606
|
if (this.pc === pc) this.answerRefused(error);
|
|
6461
6607
|
}
|
|
6608
|
+
} else if (signal.t === "a" && signal.ts !== this.droppedAnswerTs) {
|
|
6609
|
+
this.droppedAnswerTs = signal.ts;
|
|
6610
|
+
traceLink(this.options.myPubKeyZ32, "answer-dropped", {
|
|
6611
|
+
state: this.state,
|
|
6612
|
+
...this.myOfferTs && signal.o !== void 0 ? { offerMs: signal.o - this.myOfferTs } : {}
|
|
6613
|
+
});
|
|
6462
6614
|
}
|
|
6463
6615
|
}
|
|
6464
6616
|
/**
|
|
@@ -6532,7 +6684,10 @@ var DataLink = class {
|
|
|
6532
6684
|
again
|
|
6533
6685
|
});
|
|
6534
6686
|
this.reset();
|
|
6535
|
-
if (!again)
|
|
6687
|
+
if (!again) {
|
|
6688
|
+
this.options.onAnswersRefused?.();
|
|
6689
|
+
return;
|
|
6690
|
+
}
|
|
6536
6691
|
this.refusedAnswers++;
|
|
6537
6692
|
this.options.onAnswerReplaced?.();
|
|
6538
6693
|
}
|
|
@@ -8060,8 +8215,13 @@ var COMMUNITY_REACTION_FRAME = "reaction";
|
|
|
8060
8215
|
var REACTION_LIMITS = {
|
|
8061
8216
|
/** UTF-8 bytes of one emoji: a family or a subdivision flag fits, a run of emoji does not. */
|
|
8062
8217
|
emojiBytes: 32,
|
|
8063
|
-
/**
|
|
8218
|
+
/**
|
|
8219
|
+
* Reaction frames a receiver takes per window on one chat; the rest are dropped unconfirmed and come again. In a
|
|
8220
|
+
* group nothing comes again (a community's catch-up hands a frame once), so there the rest wait for the next window.
|
|
8221
|
+
*/
|
|
8064
8222
|
receive: 30,
|
|
8223
|
+
/** Group members' reactions past a window, kept per group until it has room: as many as a community's store hands back. */
|
|
8224
|
+
held: 256,
|
|
8065
8225
|
/** Reactions a sender says per window on one chat; the newest of each message waits for room. */
|
|
8066
8226
|
send: 20,
|
|
8067
8227
|
windowMs: 1e4,
|
|
@@ -9121,6 +9281,13 @@ var DHT_EXPIRY_SKEW_MS = PKARR_FUTURE_SKEW_MS;
|
|
|
9121
9281
|
*/
|
|
9122
9282
|
var DHT_ISSUED_BACK_MS = 12e4;
|
|
9123
9283
|
var MAX_ATTEMPTS = 8;
|
|
9284
|
+
/**
|
|
9285
|
+
* Envelope sequences are set aside this many at a time (`DhtDeliveryState.sequence` is the highest set aside): a
|
|
9286
|
+
* keep-alive, every four minutes per chat, and one the relays' budget turns away, every 30 s on a profile idling at
|
|
9287
|
+
* it, then cost no disk write of their own. A reader only needs the sequence to grow, so those a restart skips cost
|
|
9288
|
+
* nothing; one the budget turned away is not used again, as it may have gone out on the DHT (the CLI puts there too).
|
|
9289
|
+
*/
|
|
9290
|
+
var SEQUENCE_BLOCK = 64;
|
|
9124
9291
|
/** The wait before publication attempt `attempts + 1` of a text, or of a receipt the contact still asks for. */
|
|
9125
9292
|
var backoff = (attempts) => Math.min(6e4, 4e3 * 2 ** attempts);
|
|
9126
9293
|
/** The contact's mailbox is read this often while either side is DHT-only, and otherwise… */
|
|
@@ -9174,6 +9341,10 @@ var emptyDhtDeliveryState = () => ({
|
|
|
9174
9341
|
var DhtDelivery = class DhtDelivery {
|
|
9175
9342
|
options;
|
|
9176
9343
|
state;
|
|
9344
|
+
/** The state as last saved: a save that would write the same row again is skipped. */
|
|
9345
|
+
saved;
|
|
9346
|
+
/** The sequence of the last envelope signed here; the state's is the highest set aside (`SEQUENCE_BLOCK`). */
|
|
9347
|
+
sequence;
|
|
9177
9348
|
mode;
|
|
9178
9349
|
running = false;
|
|
9179
9350
|
ticking = false;
|
|
@@ -9228,6 +9399,8 @@ var DhtDelivery = class DhtDelivery {
|
|
|
9228
9399
|
const { peerRejected: _peerRejected, ...state } = structuredClone(options.state ?? emptyDhtDeliveryState());
|
|
9229
9400
|
this.state = state;
|
|
9230
9401
|
this.mode = options.mode;
|
|
9402
|
+
this.sequence = state.sequence;
|
|
9403
|
+
if (options.state) this.saved = JSON.stringify(state);
|
|
9231
9404
|
this.unsealedOut = state.sequence > 0 && !options.credentials.peerKey;
|
|
9232
9405
|
this.from = identityFromSeedB64(options.params.seedB64).pubKeyZ32;
|
|
9233
9406
|
this.to = options.params.peerPubKeyZ32;
|
|
@@ -9316,7 +9489,11 @@ var DhtDelivery = class DhtDelivery {
|
|
|
9316
9489
|
return operation;
|
|
9317
9490
|
}
|
|
9318
9491
|
async persist(next) {
|
|
9319
|
-
|
|
9492
|
+
const json = JSON.stringify(next);
|
|
9493
|
+
if (json !== this.saved) {
|
|
9494
|
+
await this.options.save(structuredClone(next));
|
|
9495
|
+
this.saved = json;
|
|
9496
|
+
}
|
|
9320
9497
|
this.state = next;
|
|
9321
9498
|
}
|
|
9322
9499
|
sealedKey(peer) {
|
|
@@ -9491,7 +9668,7 @@ var DhtDelivery = class DhtDelivery {
|
|
|
9491
9668
|
}
|
|
9492
9669
|
packetError(message, expires, reply, edit) {
|
|
9493
9670
|
try {
|
|
9494
|
-
this.records(this.body(this.
|
|
9671
|
+
this.records(this.body(this.sequence + 1, Date.now(), expires, message, this.state.receipt?.id ?? "abcdefghijklmnopqrstuv", reply, [], edit), this.options.credentials.peerKey ?? this.participation.pubKeyZ32);
|
|
9495
9672
|
return null;
|
|
9496
9673
|
} catch (error) {
|
|
9497
9674
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -9499,7 +9676,7 @@ var DhtDelivery = class DhtDelivery {
|
|
|
9499
9676
|
}
|
|
9500
9677
|
/** Whether a text with its hop count fits the packet and what a reader reads (`MAX_ENVELOPE_PLAINTEXT`). */
|
|
9501
9678
|
roomForHops(message, expires, reply, forwarded) {
|
|
9502
|
-
const body = this.body(this.
|
|
9679
|
+
const body = this.body(this.sequence + 1, Date.now(), expires, message, this.state.receipt?.id ?? "abcdefghijklmnopqrstuv", reply, [], void 0, forwarded);
|
|
9503
9680
|
try {
|
|
9504
9681
|
this.records(body, this.options.credentials.peerKey ?? this.participation.pubKeyZ32);
|
|
9505
9682
|
} catch {
|
|
@@ -9578,7 +9755,7 @@ var DhtDelivery = class DhtDelivery {
|
|
|
9578
9755
|
], expires = now + DHT_MESSAGE_TTL;
|
|
9579
9756
|
const hops = readForwarded(forwarded);
|
|
9580
9757
|
const fits = (element, withReply, withHops) => {
|
|
9581
|
-
const body = this.body(this.
|
|
9758
|
+
const body = this.body(this.sequence + 1, now, expires, message, this.state.receipt?.id ?? "abcdefghijklmnopqrstuv", withReply ? reply : void 0, [], void 0, withHops ? hops : void 0, element);
|
|
9582
9759
|
try {
|
|
9583
9760
|
this.records(body, this.options.credentials.peerKey ?? this.participation.pubKeyZ32);
|
|
9584
9761
|
} catch {
|
|
@@ -9763,16 +9940,18 @@ var DhtDelivery = class DhtDelivery {
|
|
|
9763
9940
|
const receipt = this.state.receipt && this.state.receipt.expires > now && this.state.receipt.attempts < MAX_ATTEMPTS ? this.state.receipt : void 0;
|
|
9764
9941
|
const receiptDue = !!receipt && !receipt.settled && (receipt.next ?? 0) <= now;
|
|
9765
9942
|
const reseal = this.resealDue;
|
|
9766
|
-
|
|
9943
|
+
const keepAlive = !force && (!pending || pending.next > now) && !receiptDue && !this.reactionsDue && !reseal;
|
|
9944
|
+
if (keepAlive && this.controlDue > now) return;
|
|
9767
9945
|
const expires = pending?.expires ?? now - 12e4 + CONTROL_TTL;
|
|
9768
9946
|
const issued = Math.min(now, Math.max(now - DHT_ISSUED_BACK_MS, expires - (pending ? DHT_MESSAGE_TTL : CONTROL_TTL), (pending?.message[1] ?? 0) - 3e4));
|
|
9769
|
-
const { body, records, reactions } = this.fitted(this.
|
|
9947
|
+
const { body, records, reactions } = this.fitted(this.sequence + 1, issued, expires, pending?.message ?? null, receipt?.id ?? null, pending?.reply, pending?.edit, pending?.forwarded, pending?.file);
|
|
9770
9948
|
const reactionsWereDue = this.reactionsDue;
|
|
9771
9949
|
this.reactionsDue = !reactions && !!this.options.reactions?.().length;
|
|
9772
9950
|
const identity = !reseal && this.state.peerPinned && this.state.peerPinned !== "can" && this.pinned()?.identity || this.identity;
|
|
9951
|
+
const sequence = body[1];
|
|
9773
9952
|
await this.persist({
|
|
9774
9953
|
...this.state,
|
|
9775
|
-
sequence:
|
|
9954
|
+
sequence: sequence > this.state.sequence ? sequence + SEQUENCE_BLOCK - 1 : this.state.sequence,
|
|
9776
9955
|
pending: pending ? {
|
|
9777
9956
|
...pending,
|
|
9778
9957
|
attempts: pending.attempts + 1,
|
|
@@ -9784,6 +9963,7 @@ var DhtDelivery = class DhtDelivery {
|
|
|
9784
9963
|
next: now + backoff(receipt.attempts)
|
|
9785
9964
|
} : this.state.receipt
|
|
9786
9965
|
});
|
|
9966
|
+
this.sequence = sequence;
|
|
9787
9967
|
const before = {
|
|
9788
9968
|
lastPublish: this.lastPublish,
|
|
9789
9969
|
controlDue: this.controlDue
|
|
@@ -9797,7 +9977,7 @@ var DhtDelivery = class DhtDelivery {
|
|
|
9797
9977
|
});
|
|
9798
9978
|
if (pending) this.lastPublished = DhtDelivery.facts(body, records, identity.pubKeyZ32);
|
|
9799
9979
|
try {
|
|
9800
|
-
await this.options.transport.publish(identity, records);
|
|
9980
|
+
await this.options.transport.publish(identity, records, keepAlive && this.live ? { background: true } : void 0);
|
|
9801
9981
|
} catch (error) {
|
|
9802
9982
|
this.reactionsDue ||= reactionsWereDue;
|
|
9803
9983
|
if (isDiscoveryBudgetError(error)) await this.heldBack(error, now, before, pending, receipt);
|
|
@@ -10459,6 +10639,43 @@ function irohDescriptor(descriptor, spelling = "dotted") {
|
|
|
10459
10639
|
};
|
|
10460
10640
|
}
|
|
10461
10641
|
/**
|
|
10642
|
+
* A relay server's URL, short: https, or wss for a browser's relay; plain http or ws only on this machine's loopback,
|
|
10643
|
+
* as a test relay is (the same rule as the relay settings).
|
|
10644
|
+
*/
|
|
10645
|
+
var RELAY_URL = /^((https|wss):\/\/[^\s]{1,120}|(http|ws):\/\/(127\.0\.0\.1|localhost|\[::1\])(:\d{1,5})?(\/[^\s]{0,100})?)$/;
|
|
10646
|
+
var HEX_KEY = /^[0-9a-f]{64}$/;
|
|
10647
|
+
/** An Iroh direct address as the adapters parse it: an IPv4 or bracketed IPv6 address and its port. */
|
|
10648
|
+
var SOCKET_ADDRESS = /^(\d{1,3}(\.\d{1,3}){3}|\[[0-9A-Fa-f:.]{2,45}(%[A-Za-z0-9._-]{1,16})?\]):\d{1,5}$/;
|
|
10649
|
+
var MAX_ADDRESSES = 8;
|
|
10650
|
+
/**
|
|
10651
|
+
* A contact's native descriptors as it tells them on a session (`paired-policy`, `paired-adapters`) or as this app
|
|
10652
|
+
* kept them: only what the adapters dial, each field in the shape its adapter makes. The keys are 64 hex characters,
|
|
10653
|
+
* a relay passes `RELAY_URL` (the rule its capability record is read by), an Iroh descriptor lists at most eight
|
|
10654
|
+
* socket addresses. A descriptor that is anything else is left out, as one the contact never told; a field this app
|
|
10655
|
+
* does not know is not kept.
|
|
10656
|
+
*/
|
|
10657
|
+
function checkedDescriptors(value) {
|
|
10658
|
+
const out = {};
|
|
10659
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return out;
|
|
10660
|
+
const raw = value;
|
|
10661
|
+
const relay = (v) => typeof v === "string" && RELAY_URL.test(v);
|
|
10662
|
+
const relayed = (v) => v === void 0 || typeof v === "boolean";
|
|
10663
|
+
const iroh = raw["iroh/1"];
|
|
10664
|
+
if (iroh && typeof iroh === "object" && typeof iroh.id === "string" && HEX_KEY.test(iroh.id) && (iroh.relay === void 0 || iroh.relay === null || relay(iroh.relay)) && relayed(iroh.relayed) && Array.isArray(iroh.addresses) && iroh.addresses.length <= MAX_ADDRESSES && iroh.addresses.every((a) => typeof a === "string" && SOCKET_ADDRESS.test(a))) out["iroh/1"] = {
|
|
10665
|
+
id: iroh.id,
|
|
10666
|
+
relay: iroh.relay ?? null,
|
|
10667
|
+
addresses: [...iroh.addresses],
|
|
10668
|
+
...iroh.relayed !== void 0 ? { relayed: iroh.relayed } : {}
|
|
10669
|
+
};
|
|
10670
|
+
const hyper = raw["hyperdht/1"];
|
|
10671
|
+
if (hyper && typeof hyper === "object" && typeof hyper.publicKey === "string" && HEX_KEY.test(hyper.publicKey) && (hyper.relay === void 0 || relay(hyper.relay)) && relayed(hyper.relayed)) out["hyperdht/1"] = {
|
|
10672
|
+
publicKey: hyper.publicKey,
|
|
10673
|
+
...hyper.relay !== void 0 ? { relay: hyper.relay } : {},
|
|
10674
|
+
...hyper.relayed !== void 0 ? { relayed: hyper.relayed } : {}
|
|
10675
|
+
};
|
|
10676
|
+
return out;
|
|
10677
|
+
}
|
|
10678
|
+
/**
|
|
10462
10679
|
* Relay only (WISP 100, "Relayed"): the transport reaches this contact only through a relay server, never
|
|
10463
10680
|
* directly. A descriptor says so with `relayed: true`, and an Iroh descriptor with no direct address is relayed
|
|
10464
10681
|
* too (a browser's, WISP 102). Either side being relay only makes the whole path relayed. Both sides hold both
|
|
@@ -10498,15 +10715,13 @@ function parsePolicy(raw) {
|
|
|
10498
10715
|
if (!raw || typeof raw !== "object") return null;
|
|
10499
10716
|
const p = raw;
|
|
10500
10717
|
if (!Number.isSafeInteger(p.revision) || p.revision < 0 || !Number.isSafeInteger(p.intent) || p.intent < 0 || !TRANSPORTS.includes(p.preferred) || typeof p.fallback !== "boolean" || !Array.isArray(p.available) || p.available.length > 3 || new Set(p.available).size !== p.available.length || p.available.some((t) => !TRANSPORTS.includes(t)) || !p.descriptors || typeof p.descriptors !== "object" || JSON.stringify(p.descriptors).length > 4096) return null;
|
|
10501
|
-
const descriptors = {};
|
|
10502
|
-
for (const t of ["iroh/1", "hyperdht/1"]) if (p.descriptors[t]) descriptors[t] = p.descriptors[t];
|
|
10503
10718
|
return {
|
|
10504
10719
|
revision: p.revision,
|
|
10505
10720
|
intent: p.intent,
|
|
10506
10721
|
preferred: p.preferred,
|
|
10507
10722
|
fallback: p.fallback,
|
|
10508
10723
|
available: [...p.available],
|
|
10509
|
-
descriptors
|
|
10724
|
+
descriptors: checkedDescriptors(p.descriptors)
|
|
10510
10725
|
};
|
|
10511
10726
|
}
|
|
10512
10727
|
/** Runs only inside a participation-authenticated channel. Both roles may propose;
|
|
@@ -12082,11 +12297,6 @@ var HEX64$1 = /^[0-9a-f]{64}$/;
|
|
|
12082
12297
|
var SIG$4 = /^[A-Za-z0-9_-]{86}$/;
|
|
12083
12298
|
var Z32$2 = /^[ybndrfg8ejkmcpqxot1uwisza345h769]{52}$/;
|
|
12084
12299
|
var KEY$1 = /^[A-Za-z0-9_-]{43}$/;
|
|
12085
|
-
/**
|
|
12086
|
-
* A relay server's URL, short: https, or wss for a browser's relay; plain http or ws only on this machine's loopback,
|
|
12087
|
-
* as a test relay is (the same rule as the relay settings).
|
|
12088
|
-
*/
|
|
12089
|
-
var RELAY = /^((https|wss):\/\/[^\s]{1,120}|(http|ws):\/\/(127\.0\.0\.1|localhost|\[::1\])(:\d{1,5})?(\/[^\s]{0,100})?)$/;
|
|
12090
12300
|
var hexToKey = (hex) => toBase64Url(Uint8Array.from(hex.match(/../g), (b) => parseInt(b, 16)));
|
|
12091
12301
|
var keyToHex = (key) => Array.from(fromBase64Url(key), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
12092
12302
|
/**
|
|
@@ -12098,12 +12308,12 @@ function capsDescriptors(descriptors) {
|
|
|
12098
12308
|
const iroh = descriptors?.["iroh/1"];
|
|
12099
12309
|
if (iroh && typeof iroh.id === "string" && HEX64$1.test(iroh.id)) out["iroh/1"] = {
|
|
12100
12310
|
id: hexToKey(iroh.id),
|
|
12101
|
-
...typeof iroh.relay === "string" &&
|
|
12311
|
+
...typeof iroh.relay === "string" && RELAY_URL.test(iroh.relay) ? { relay: iroh.relay } : {}
|
|
12102
12312
|
};
|
|
12103
12313
|
const hyper = descriptors?.["hyperdht/1"];
|
|
12104
12314
|
if (hyper && typeof hyper.publicKey === "string" && HEX64$1.test(hyper.publicKey)) out["hyperdht/1"] = {
|
|
12105
12315
|
publicKey: hexToKey(hyper.publicKey),
|
|
12106
|
-
...typeof hyper.relay === "string" &&
|
|
12316
|
+
...typeof hyper.relay === "string" && RELAY_URL.test(hyper.relay) ? { relay: hyper.relay } : {}
|
|
12107
12317
|
};
|
|
12108
12318
|
return out;
|
|
12109
12319
|
}
|
|
@@ -12138,14 +12348,14 @@ function parseDescriptors(value) {
|
|
|
12138
12348
|
const out = {};
|
|
12139
12349
|
const iroh = raw["iroh/1"], hyper = raw["hyperdht/1"];
|
|
12140
12350
|
if (iroh !== void 0) {
|
|
12141
|
-
if (!iroh || typeof iroh.id !== "string" || !KEY$1.test(iroh.id) || iroh.relay !== void 0 && (typeof iroh.relay !== "string" || !
|
|
12351
|
+
if (!iroh || typeof iroh.id !== "string" || !KEY$1.test(iroh.id) || iroh.relay !== void 0 && (typeof iroh.relay !== "string" || !RELAY_URL.test(iroh.relay))) return null;
|
|
12142
12352
|
out["iroh/1"] = {
|
|
12143
12353
|
id: iroh.id,
|
|
12144
12354
|
...iroh.relay !== void 0 ? { relay: iroh.relay } : {}
|
|
12145
12355
|
};
|
|
12146
12356
|
}
|
|
12147
12357
|
if (hyper !== void 0) {
|
|
12148
|
-
if (!hyper || typeof hyper.publicKey !== "string" || !KEY$1.test(hyper.publicKey) || hyper.relay !== void 0 && (typeof hyper.relay !== "string" || !
|
|
12358
|
+
if (!hyper || typeof hyper.publicKey !== "string" || !KEY$1.test(hyper.publicKey) || hyper.relay !== void 0 && (typeof hyper.relay !== "string" || !RELAY_URL.test(hyper.relay))) return null;
|
|
12149
12359
|
out["hyperdht/1"] = {
|
|
12150
12360
|
publicKey: hyper.publicKey,
|
|
12151
12361
|
...hyper.relay !== void 0 ? { relay: hyper.relay } : {}
|
|
@@ -15532,7 +15742,7 @@ var ChatFiles = class {
|
|
|
15532
15742
|
}
|
|
15533
15743
|
return;
|
|
15534
15744
|
case "pf-done":
|
|
15535
|
-
if (record.state !== "verifying") return;
|
|
15745
|
+
if (record.state !== "verifying" && record.confirmed !== record.file.size) return;
|
|
15536
15746
|
record.confirmed = record.file.size;
|
|
15537
15747
|
this.end(outgoing, "done");
|
|
15538
15748
|
return;
|
|
@@ -16451,7 +16661,7 @@ var GROUP_FILE_LIMITS = {
|
|
|
16451
16661
|
serveAtOnce: 3,
|
|
16452
16662
|
/** File announcements a member sends to one group in a minute. */
|
|
16453
16663
|
announcePerMinute: 8,
|
|
16454
|
-
/** File announcements
|
|
16664
|
+
/** File announcements of one member of a group fetched by themselves in `announceWindowMs`; past it, kept for a Download. */
|
|
16455
16665
|
announceTaken: 30,
|
|
16456
16666
|
announceWindowMs: 6e5,
|
|
16457
16667
|
/** Holders remembered per file. */
|
|
@@ -16759,6 +16969,7 @@ var ENGINE_ERRORS = {
|
|
|
16759
16969
|
notEnoughSats: "Not enough sats in your wallet",
|
|
16760
16970
|
noSharedMint: "You share no mint with this contact",
|
|
16761
16971
|
ecashAlreadySpent: "This ecash was already spent somewhere else",
|
|
16972
|
+
ecashBeingReceived: "This ecash is already being received",
|
|
16762
16973
|
ecashOtherUnit: "This ecash is in {unit}: only sat ecash can be redeemed here",
|
|
16763
16974
|
reviewedEcashSpent: "The ecash for this payment was already spent somewhere else. Nothing was sent, and your balance now shows what the mint still holds.",
|
|
16764
16975
|
reviewedSatsGone: "The sats this payment was reviewed with went to another payment. Nothing was sent: review it again.",
|
|
@@ -16867,8 +17078,6 @@ var PACKET_TRANSPORTS_FAST_ATTEMPTS = 3;
|
|
|
16867
17078
|
var DHT_PIN_GRACE_MS = 1e4;
|
|
16868
17079
|
/** Back after a restart (`resume`), dials wait this long at most for the native endpoint that carried the chat to start. */
|
|
16869
17080
|
var RESUME_WAIT_MS = 1e4;
|
|
16870
|
-
/** Two dials crossing: how long the lower key's side waits for its own to connect before it takes the contact's. */
|
|
16871
|
-
var CROSSED_WAIT_MS = 3e3;
|
|
16872
17081
|
/** A WebRTC attempt under way, not yet open: what a member's packet saying it has no WebRTC ends (`peerPacketTransports`). */
|
|
16873
17082
|
var WEBRTC_ATTEMPT = /* @__PURE__ */ new Set([
|
|
16874
17083
|
"offering",
|
|
@@ -16922,6 +17131,7 @@ var RTC_UNSTARTED_MS = 6e5;
|
|
|
16922
17131
|
var RACE_RETRY_MS = 2e3;
|
|
16923
17132
|
var DEMOTE_MS = 36e5;
|
|
16924
17133
|
var AUTO_CONNECT_MAX_RETRY_MS = 18e4;
|
|
17134
|
+
var ANSWERS_REFUSED_PAUSE_MS = 2e3;
|
|
16925
17135
|
/**
|
|
16926
17136
|
* A first pairing is different: the contact is right there, having just read the invite, so an attempt
|
|
16927
17137
|
* that did not make it in `PAIRING_ATTEMPT_MS` is given up and the next one starts after `PAIRING_RETRY_MS`
|
|
@@ -17197,6 +17407,9 @@ var GhostLink = class {
|
|
|
17197
17407
|
afterRtc;
|
|
17198
17408
|
/** When the transports after WebRTC are dialled while its offer is still out (`race`). */
|
|
17199
17409
|
raceTimer = null;
|
|
17410
|
+
/** Pauses taken after a run of refused answers since WebRTC last opened, and the one under way (`ANSWERS_REFUSED_PAUSES`). */
|
|
17411
|
+
refusedPauses = 0;
|
|
17412
|
+
refusedPause = null;
|
|
17200
17413
|
/** Set while `race` dials: the offer ending meanwhile is followed up by the race. */
|
|
17201
17414
|
racing = false;
|
|
17202
17415
|
/**
|
|
@@ -17205,10 +17418,15 @@ var GhostLink = class {
|
|
|
17205
17418
|
* was the app's in the background, not a restart (`resumeAfterAbsence`).
|
|
17206
17419
|
*/
|
|
17207
17420
|
offered;
|
|
17421
|
+
/**
|
|
17422
|
+
* The WebRTC offer this side's dial made last, fallback allowed or not: one made before the contact's record was read
|
|
17423
|
+
* races once it is (`raceStandingOffer`).
|
|
17424
|
+
*/
|
|
17425
|
+
offerMade;
|
|
17208
17426
|
/** The attempt (its epoch) whose direct transports were knocked on as its offer went out (`knockEarly`), and whether that is under way. */
|
|
17209
17427
|
knockedEarly = -1;
|
|
17210
17428
|
knockingEarly = false;
|
|
17211
|
-
/** When
|
|
17429
|
+
/** When this side's offer's answer was applied (`RACE_ANSWERED_MS`); 0 while the data link is not connecting on it. */
|
|
17212
17430
|
answeredAt = 0;
|
|
17213
17431
|
/** This side's offer is out unanswered, or answered and not connected yet: what is ranked after WebRTC may race it. */
|
|
17214
17432
|
get offerOut() {
|
|
@@ -17405,7 +17623,7 @@ var GhostLink = class {
|
|
|
17405
17623
|
}
|
|
17406
17624
|
}) : null;
|
|
17407
17625
|
this.streamWasBlocked = this.streamBlocked;
|
|
17408
|
-
this.peerDescriptors = options.native?.peerDescriptors
|
|
17626
|
+
this.peerDescriptors = checkedDescriptors(options.native?.peerDescriptors);
|
|
17409
17627
|
this.peerTransports = options.native?.peerTransports;
|
|
17410
17628
|
this.saidTransports = !!options.packetTransports && !!options.packetTransportsSaid;
|
|
17411
17629
|
this.peerPacketSaid = !!options.packetTransports && !!options.native?.peerTransports;
|
|
@@ -17421,6 +17639,7 @@ var GhostLink = class {
|
|
|
17421
17639
|
nick: options.nick,
|
|
17422
17640
|
lastSeenTimestamp: options.lastSeenTimestamp,
|
|
17423
17641
|
pollIntervals: options.pollIntervals,
|
|
17642
|
+
awaitingSince: options.madeAt,
|
|
17424
17643
|
getServices: options.getServices,
|
|
17425
17644
|
firstPublish: options.firstPublish ?? (this.tracker && options.pairingProgress?.role === "joiner" && options.rtcAvailable !== false || this.resuming ? "after-first-poll" : "at-start"),
|
|
17426
17645
|
events: {
|
|
@@ -17534,11 +17753,13 @@ var GhostLink = class {
|
|
|
17534
17753
|
offerStanding: (ts) => !this.leaving && !this.stopped && !this.streamBlocked && this.peerOfferTs() === ts,
|
|
17535
17754
|
onAnswerReplaced: () => this.redial(),
|
|
17536
17755
|
onDirect: (evidence) => events.onDirectEvidence?.(evidence),
|
|
17756
|
+
onAnswersRefused: () => this.answersRefused(),
|
|
17537
17757
|
onOpen: (channel) => {
|
|
17538
17758
|
if (this.streamBlocked || this.keyStopped) {
|
|
17539
17759
|
channel.close();
|
|
17540
17760
|
return;
|
|
17541
17761
|
}
|
|
17762
|
+
if (this.rtcStartedUnpinned) this.openedUnpinned.add(channel);
|
|
17542
17763
|
const plan = this.switcher.pending;
|
|
17543
17764
|
if (plan?.choices.includes("webrtc/1") && this.paired?.state.status === "ready") this.attachCandidate(channel, void 0, plan).catch(() => {});
|
|
17544
17765
|
else if (this.activeBinding) channel.close();
|
|
@@ -17554,9 +17775,13 @@ var GhostLink = class {
|
|
|
17554
17775
|
const was = this.lastDataLinkState;
|
|
17555
17776
|
traceLink(this.myPubKeyZ32, "datalink", { state });
|
|
17556
17777
|
this.trackDataLink(state);
|
|
17778
|
+
if (state === "offering" || state === "answering") this.rtcStartedUnpinned = !options.pairing?.credentials.peerKey && !options.pairing?.credentials.expectedPeerKey;
|
|
17557
17779
|
if (this.activeBinding) return;
|
|
17558
17780
|
events.onDataLinkState?.(state);
|
|
17559
|
-
if (state === "open")
|
|
17781
|
+
if (state === "open") {
|
|
17782
|
+
this.afterRtc = void 0;
|
|
17783
|
+
this.refusedPauses = 0;
|
|
17784
|
+
}
|
|
17560
17785
|
if (state === "offering") {
|
|
17561
17786
|
this.offerUnsent = true;
|
|
17562
17787
|
this.dialUnseen = false;
|
|
@@ -17570,9 +17795,9 @@ var GhostLink = class {
|
|
|
17570
17795
|
}
|
|
17571
17796
|
}
|
|
17572
17797
|
this.answeredAt = 0;
|
|
17573
|
-
if (state === "connecting" && was === "offering" && this.
|
|
17798
|
+
if (state === "connecting" && was === "offering" && this.offerMade?.epoch === this.connectionEpoch) {
|
|
17574
17799
|
this.answeredAt = Date.now();
|
|
17575
|
-
this.scheduleRace(this.offered.epoch, this.offered.at);
|
|
17800
|
+
if (this.offered?.epoch === this.connectionEpoch) this.scheduleRace(this.offered.epoch, this.offered.at);
|
|
17576
17801
|
}
|
|
17577
17802
|
if (state !== "idle") return;
|
|
17578
17803
|
this.dialUnseen = was === "offering" && this.offerUnsent;
|
|
@@ -17967,6 +18192,7 @@ var GhostLink = class {
|
|
|
17967
18192
|
async stop(announce = true) {
|
|
17968
18193
|
this.stopped = true;
|
|
17969
18194
|
this.clearRace();
|
|
18195
|
+
if (this.refusedPause) clearTimeout(this.refusedPause);
|
|
17970
18196
|
if (this.retryLook) clearTimeout(this.retryLook);
|
|
17971
18197
|
if (this.dhtPinGrace) clearTimeout(this.dhtPinGrace);
|
|
17972
18198
|
if (this.waiting?.timer) clearTimeout(this.waiting.timer);
|
|
@@ -18162,6 +18388,7 @@ var GhostLink = class {
|
|
|
18162
18388
|
}
|
|
18163
18389
|
if (held && heldOn && reidentified.has(heldOn) && this.restartedRedial(held, heldOn)) return;
|
|
18164
18390
|
if (this.redescribedDial()) return;
|
|
18391
|
+
if (!this.dialing) this.raceStandingOffer();
|
|
18165
18392
|
if (this.channel || this.dialing) return;
|
|
18166
18393
|
if (news || !again) {
|
|
18167
18394
|
this.autoConnectFailures = 0;
|
|
@@ -18739,6 +18966,10 @@ var GhostLink = class {
|
|
|
18739
18966
|
...resume && { resume },
|
|
18740
18967
|
...woke && { woke }
|
|
18741
18968
|
} : void 0;
|
|
18969
|
+
this.offerMade = {
|
|
18970
|
+
epoch,
|
|
18971
|
+
at: offeredAt
|
|
18972
|
+
};
|
|
18742
18973
|
const gate = this.dialGate;
|
|
18743
18974
|
this.dialGate = void 0;
|
|
18744
18975
|
const replaced = new Promise((resolve) => {
|
|
@@ -18750,10 +18981,12 @@ var GhostLink = class {
|
|
|
18750
18981
|
traceLink(this.myPubKeyZ32, "dial-held", {});
|
|
18751
18982
|
this.afterRtc = void 0;
|
|
18752
18983
|
this.offered = void 0;
|
|
18984
|
+
this.offerMade = void 0;
|
|
18753
18985
|
return;
|
|
18754
18986
|
}
|
|
18755
18987
|
if (this.dataLink.state !== "idle" || epoch !== this.connectionEpoch) {
|
|
18756
18988
|
this.scheduleRace(epoch, offeredAt);
|
|
18989
|
+
this.raceStandingOffer();
|
|
18757
18990
|
return;
|
|
18758
18991
|
}
|
|
18759
18992
|
this.rtcUnstarted();
|
|
@@ -18919,6 +19152,31 @@ var GhostLink = class {
|
|
|
18919
19152
|
}, Math.max(0, Math.min(...at) - now));
|
|
18920
19153
|
}
|
|
18921
19154
|
/**
|
|
19155
|
+
* This side's offer went out before the contact's record was read (a joiner's early one, `dialEarly`): nothing was
|
|
19156
|
+
* known of what the contact runs, so it was made with no fallback and nothing raced it. Once the record says, and both
|
|
19157
|
+
* sides allow it, the offer races what both run as one made then would: answered and not connected `RACE_ANSWERED_MS`
|
|
19158
|
+
* later, the relayed ones too. Behind a VPN that drops UDP, a first pairing went live over the relayed Iroh only once
|
|
19159
|
+
* the attempt timed out and the next one raced, 32 to 66 s in (zero's VPN lab, 2026-10-13).
|
|
19160
|
+
*/
|
|
19161
|
+
raceStandingOffer() {
|
|
19162
|
+
const made = this.offerMade, epoch = this.connectionEpoch;
|
|
19163
|
+
if (!made || made.epoch !== epoch || this.offered?.epoch === epoch || !this.offerOut || this.channel || this.stopped) return;
|
|
19164
|
+
if (!this.fallback || !this.peerFallback || !this.peerTransports) return;
|
|
19165
|
+
const relayed = relayedTransports(this.localDescriptors(), this.peerDescriptors);
|
|
19166
|
+
const rest = rankTransports(this.transportOffer(), this.peerTransports, relayed).filter((t) => t !== "webrtc/1");
|
|
19167
|
+
if (!rest.length) return;
|
|
19168
|
+
this.offered = {
|
|
19169
|
+
epoch,
|
|
19170
|
+
at: made.at
|
|
19171
|
+
};
|
|
19172
|
+
this.afterRtc = {
|
|
19173
|
+
epoch,
|
|
19174
|
+
rest
|
|
19175
|
+
};
|
|
19176
|
+
traceLink(this.myPubKeyZ32, "race-standing", { transports: rest });
|
|
19177
|
+
this.scheduleRace(epoch, made.at);
|
|
19178
|
+
}
|
|
19179
|
+
/**
|
|
18922
19180
|
* Back after an absence, this side answers an offer the contact made while it was away (it dials whatever its key):
|
|
18923
19181
|
* the contact looks for that answer at the pace of an offer out a while (every 8 s on the relays), and its attempt may
|
|
18924
19182
|
* end before it does. The direct transports both run are knocked on meanwhile, and the relayed ones where both apps
|
|
@@ -19051,6 +19309,7 @@ var GhostLink = class {
|
|
|
19051
19309
|
const left = next.rest.filter((t) => t !== "webrtc/1" && !due.includes(t));
|
|
19052
19310
|
if (!due.length) {
|
|
19053
19311
|
this.scheduleRace(epoch, offeredAt);
|
|
19312
|
+
this.raceStandingOffer();
|
|
19054
19313
|
return;
|
|
19055
19314
|
}
|
|
19056
19315
|
this.afterRtc = left.length ? {
|
|
@@ -19075,6 +19334,7 @@ var GhostLink = class {
|
|
|
19075
19334
|
if (live || epoch !== this.connectionEpoch || this.channel || this.stopped) return;
|
|
19076
19335
|
if (this.dataLink.state !== "idle") {
|
|
19077
19336
|
this.scheduleRace(epoch, offeredAt);
|
|
19337
|
+
this.raceStandingOffer();
|
|
19078
19338
|
return;
|
|
19079
19339
|
}
|
|
19080
19340
|
const rest = this.afterRtc;
|
|
@@ -19371,6 +19631,24 @@ var GhostLink = class {
|
|
|
19371
19631
|
});
|
|
19372
19632
|
this.autoDial();
|
|
19373
19633
|
}
|
|
19634
|
+
/** WebRTC here refused a run of answers to this side's offers (`ANSWERS_REFUSED_PAUSES`): the next offer goes after a short pause. */
|
|
19635
|
+
answersRefused() {
|
|
19636
|
+
if (this.refusedPause || this.stopped) return;
|
|
19637
|
+
if (this.refusedPauses >= 3) {
|
|
19638
|
+
traceLink(this.myPubKeyZ32, "answers-refused-out", {});
|
|
19639
|
+
this.options.events?.onAnswersRefused?.();
|
|
19640
|
+
return;
|
|
19641
|
+
}
|
|
19642
|
+
const ms = ANSWERS_REFUSED_PAUSE_MS * 2 ** this.refusedPauses++;
|
|
19643
|
+
traceLink(this.myPubKeyZ32, "answers-refused-pause", {
|
|
19644
|
+
pause: this.refusedPauses,
|
|
19645
|
+
ms
|
|
19646
|
+
});
|
|
19647
|
+
this.refusedPause = setTimeout(() => {
|
|
19648
|
+
this.refusedPause = null;
|
|
19649
|
+
this.redial();
|
|
19650
|
+
}, ms);
|
|
19651
|
+
}
|
|
19374
19652
|
/**
|
|
19375
19653
|
* A failed dial waits `dialWait` before the next, and only a look at the contact's record dials: the next one came at
|
|
19376
19654
|
* the link's own pace (30 s in the background), which knows nothing of the backoff. A member let in to a group
|
|
@@ -20132,8 +20410,8 @@ var GhostLink = class {
|
|
|
20132
20410
|
* Both sides dialled at once (two apps back after a restart, each knocking): the lower key's dial carries the chat, on
|
|
20133
20411
|
* both sides; the higher key's side takes it over as a replacement (`attachReplacement`). Here, on the lower key's
|
|
20134
20412
|
* side, the contact's connection waits: dropped once this side's own dial connects, taken if that dial fails, or has
|
|
20135
|
-
* not connected within `CROSSED_WAIT_MS` (
|
|
20136
|
-
*
|
|
20413
|
+
* not connected within `CROSSED_WAIT_MS` (it stalled, or goes to an endpoint the contact no longer runs, for as long
|
|
20414
|
+
* as it takes to give up).
|
|
20137
20415
|
*/
|
|
20138
20416
|
crossed = null;
|
|
20139
20417
|
parkCrossed(channel, binding) {
|
|
@@ -20150,7 +20428,7 @@ var GhostLink = class {
|
|
|
20150
20428
|
this.dialing = false;
|
|
20151
20429
|
this.attempt = null;
|
|
20152
20430
|
this.attach(channel, binding);
|
|
20153
|
-
},
|
|
20431
|
+
}, 500);
|
|
20154
20432
|
channel.onClose = () => {
|
|
20155
20433
|
if (this.crossed?.channel === channel) {
|
|
20156
20434
|
clearTimeout(timer);
|
|
@@ -20232,6 +20510,8 @@ var GhostLink = class {
|
|
|
20232
20510
|
dialedIn = /* @__PURE__ */ new WeakSet();
|
|
20233
20511
|
/** Connections whose session started with no key pinned nor named by the invite. */
|
|
20234
20512
|
openedUnpinned = /* @__PURE__ */ new WeakSet();
|
|
20513
|
+
/** The WebRTC offer under way was made or answered with no key pinned nor named by the invite (its channel opens later). */
|
|
20514
|
+
rtcStartedUnpinned = false;
|
|
20235
20515
|
/**
|
|
20236
20516
|
* `migration`: the channel is a candidate that takes over from the session open now once it authenticated. With a
|
|
20237
20517
|
* `plan`, a live switch both sides agreed; without one, a replacement (`attachReplacement`): the contact dialled
|
|
@@ -20671,11 +20951,7 @@ var GhostLink = class {
|
|
|
20671
20951
|
}
|
|
20672
20952
|
if (frame?.t === "paired-adapters") {
|
|
20673
20953
|
if (!frame.descriptors || typeof frame.descriptors !== "object" || JSON.stringify(frame.descriptors).length > 4096) return;
|
|
20674
|
-
const descriptors =
|
|
20675
|
-
for (const transport of ["iroh/1", "hyperdht/1"]) {
|
|
20676
|
-
const value = frame.descriptors[transport];
|
|
20677
|
-
if (value) descriptors[transport] = value;
|
|
20678
|
-
}
|
|
20954
|
+
const descriptors = checkedDescriptors(frame.descriptors);
|
|
20679
20955
|
const transports = [...this.paired?.peerTransports ?? []];
|
|
20680
20956
|
const fallback = this.paired?.peerAllowsFallback ?? false;
|
|
20681
20957
|
await this.options.events?.onTransportDiscovery?.(descriptors, transports, fallback);
|
|
@@ -24989,7 +25265,9 @@ var GROUP_LIMITS = {
|
|
|
24989
25265
|
*/
|
|
24990
25266
|
catchUpSlice: 16,
|
|
24991
25267
|
/** Between two slices, when the member's app cannot say it handled one (`handled` resolves `false`). */
|
|
24992
|
-
catchUpPauseMs: 500
|
|
25268
|
+
catchUpPauseMs: 500,
|
|
25269
|
+
/** A received message is in the saved state within this long (`persistSoon`): a burst of them is one save, not one each. */
|
|
25270
|
+
saveWithinMs: 1e3
|
|
24993
25271
|
};
|
|
24994
25272
|
/** What every member should know about who can read what, in the words the apps show. */
|
|
24995
25273
|
var GROUP_READ_NOTE = `Everyone in the group can read everything sent while they are a member. Someone removed keeps what they already received and cannot read what comes after; someone who joins later cannot read what came before. Messages go directly to each member; whoever was away gets what they missed from any member who has it (the group's last ${GROUP_LIMITS.relay} messages) when they meet again.`;
|
|
@@ -25138,6 +25416,12 @@ var byeSigned = (f) => utf8Encode(JSON.stringify([
|
|
|
25138
25416
|
f.e,
|
|
25139
25417
|
f.ts
|
|
25140
25418
|
]));
|
|
25419
|
+
var leftSigned = (commit) => utf8Encode(JSON.stringify([
|
|
25420
|
+
"ghostly-group/1 left",
|
|
25421
|
+
commit.g,
|
|
25422
|
+
commit.e,
|
|
25423
|
+
commitHash(commit)
|
|
25424
|
+
]));
|
|
25141
25425
|
var editSigned = (f) => utf8Encode(JSON.stringify([
|
|
25142
25426
|
"ghostly-group/1 edit",
|
|
25143
25427
|
f.g,
|
|
@@ -25170,6 +25454,7 @@ function readFirstPlace(o, epoch) {
|
|
|
25170
25454
|
/** One of my own frames, among mine: `e:n`. */
|
|
25171
25455
|
var ownKey = (f) => `${f.e}:${f.n}`;
|
|
25172
25456
|
/** What a frame weighs in a log, roughly: its boxes plus the fixed fields around them. */
|
|
25457
|
+
/** What a kept frame counts for against `GROUP_LIMITS.relayBytes`. */
|
|
25173
25458
|
var frameBytes = (f) => f.c.length + (f.m?.c.length ?? 0) + (f.r?.c.length ?? 0) + (f.sc?.c.length ?? 0) + (f.fl?.c.length ?? 0) + 400;
|
|
25174
25459
|
/** Only the fields an edit frame has, as `clean` does for a message. */
|
|
25175
25460
|
var cleanEdit = (f) => ({
|
|
@@ -25289,6 +25574,8 @@ var GroupSession = class {
|
|
|
25289
25574
|
pendingMeta;
|
|
25290
25575
|
/** While `handle` runs: the frames it took that a hub passes on (`handle`'s result). */
|
|
25291
25576
|
passOn = null;
|
|
25577
|
+
/** Set while a save of what received messages changed is due (`persistSoon`). */
|
|
25578
|
+
saveTimer;
|
|
25292
25579
|
/** Syncs answered per member, a few a minute (`GROUP_LIMITS.syncAnswers`). In memory only. */
|
|
25293
25580
|
syncsAnswered = /* @__PURE__ */ new Map();
|
|
25294
25581
|
/** Catch-up answers still going out, a slice at a time (`handOut`). */
|
|
@@ -25492,11 +25779,14 @@ var GroupSession = class {
|
|
|
25492
25779
|
const floor = this.hooks.seqFloor?.() ?? 0;
|
|
25493
25780
|
return Number.isSafeInteger(floor) && floor > 0 ? floor : 0;
|
|
25494
25781
|
}
|
|
25495
|
-
/**
|
|
25496
|
-
|
|
25782
|
+
/**
|
|
25783
|
+
* How many messages, per sender, are known to be missing in the current epoch. `past`: only the numbers above it,
|
|
25784
|
+
* for a caller that knows nobody holds the ones up to it any more.
|
|
25785
|
+
*/
|
|
25786
|
+
missing(sender, past = -1) {
|
|
25497
25787
|
const entry = this.state.seen[sender]?.[this.epoch];
|
|
25498
25788
|
if (!entry) return 0;
|
|
25499
|
-
const lowest = Math.max(0, entry.high - GROUP_LIMITS.window + 1);
|
|
25789
|
+
const lowest = Math.max(0, entry.high - GROUP_LIMITS.window + 1, past + 1);
|
|
25500
25790
|
return Math.max(0, entry.high - lowest + 1 - (1 + entry.window.filter((n) => n >= lowest).length));
|
|
25501
25791
|
}
|
|
25502
25792
|
/**
|
|
@@ -25508,7 +25798,7 @@ var GroupSession = class {
|
|
|
25508
25798
|
const out = /* @__PURE__ */ new Map();
|
|
25509
25799
|
for (const [sender, epochs] of Object.entries(this.state.seen)) {
|
|
25510
25800
|
if (sender === this.myKey || !rosterHas(this.roster, sender)) continue;
|
|
25511
|
-
const marks = Object.entries(epochs).
|
|
25801
|
+
const marks = Object.entries(epochs).map(([e, entry]) => [e, this.gaps(entry)]).filter(([, gaps]) => gaps.length).map(([e, gaps]) => `${e}:${gaps.join(".")}`);
|
|
25512
25802
|
if (marks.length) out.set(sender, marks.join(","));
|
|
25513
25803
|
}
|
|
25514
25804
|
return out;
|
|
@@ -25537,8 +25827,31 @@ var GroupSession = class {
|
|
|
25537
25827
|
return operation;
|
|
25538
25828
|
}
|
|
25539
25829
|
persist() {
|
|
25830
|
+
if (this.saveTimer) {
|
|
25831
|
+
clearTimeout(this.saveTimer);
|
|
25832
|
+
this.saveTimer = void 0;
|
|
25833
|
+
}
|
|
25540
25834
|
return this.hooks.save(structuredClone(this.state));
|
|
25541
25835
|
}
|
|
25836
|
+
/**
|
|
25837
|
+
* A save within `GROUP_LIMITS.saveWithinMs`, for what a received message changes (what was seen, what is kept to hand
|
|
25838
|
+
* on): the state holds the whole hand-on log, and a member back in a busy group wrote all of it once per message.
|
|
25839
|
+
* The message itself is stored before (`hooks.message`). An app killed before the save takes those frames again when
|
|
25840
|
+
* it syncs: its store has the messages already, and the state then catches up.
|
|
25841
|
+
*/
|
|
25842
|
+
persistSoon() {
|
|
25843
|
+
if (this.saveTimer) return;
|
|
25844
|
+
this.saveTimer = setTimeout(() => {
|
|
25845
|
+
this.saveTimer = void 0;
|
|
25846
|
+
this.serialize(() => this.hooks.save(structuredClone(this.state))).catch(() => {});
|
|
25847
|
+
}, GROUP_LIMITS.saveWithinMs);
|
|
25848
|
+
}
|
|
25849
|
+
/** Writes what is waiting to be saved now (before a restart, in tests). */
|
|
25850
|
+
flush() {
|
|
25851
|
+
return this.serialize(async () => {
|
|
25852
|
+
if (this.saveTimer) await this.persist();
|
|
25853
|
+
});
|
|
25854
|
+
}
|
|
25542
25855
|
secret(epoch) {
|
|
25543
25856
|
const s = this.state.secrets[epoch];
|
|
25544
25857
|
return s ? fromBase64Url(s) : void 0;
|
|
@@ -25589,11 +25902,43 @@ var GroupSession = class {
|
|
|
25589
25902
|
}];
|
|
25590
25903
|
});
|
|
25591
25904
|
}
|
|
25592
|
-
|
|
25905
|
+
/**
|
|
25906
|
+
* A welcome for a member already in the roster that never got the first one (its chat closed while it was let in):
|
|
25907
|
+
* nothing is committed. It carries the secrets of the epochs it was in, from its admission on, sealed to its key.
|
|
25908
|
+
*/
|
|
25909
|
+
rewelcome(memberKey) {
|
|
25910
|
+
return this.serialize(async () => {
|
|
25911
|
+
this.requireAdmin();
|
|
25912
|
+
if (!rosterHas(this.roster, memberKey) || memberKey === this.myKey) throw new Error("Not a member");
|
|
25913
|
+
const chain = this.state.chain;
|
|
25914
|
+
let admitted = chain.length - 1;
|
|
25915
|
+
while (admitted >= 0 && !(chain[admitted].k === "add" && chain[admitted].s === memberKey)) admitted--;
|
|
25916
|
+
if (admitted < 0) throw new Error("Not a member");
|
|
25917
|
+
const secrets = chain.slice(admitted).filter((c) => this.state.secrets[c.e] && rosterHas(c.m, memberKey)).map((c) => ({
|
|
25918
|
+
e: c.e,
|
|
25919
|
+
s: sealSecret(memberKey, this.secret(c.e), secretAad$1(this.id, c.e, memberKey))
|
|
25920
|
+
}));
|
|
25921
|
+
const pieces = chainPieces(chain);
|
|
25922
|
+
const tail = pieces.pop();
|
|
25923
|
+
return [...pieces.map((commits) => ({
|
|
25924
|
+
t: "group-chain",
|
|
25925
|
+
g: this.id,
|
|
25926
|
+
commits
|
|
25927
|
+
})), {
|
|
25928
|
+
t: "group-welcome",
|
|
25929
|
+
g: this.id,
|
|
25930
|
+
name: this.name,
|
|
25931
|
+
commits: tail,
|
|
25932
|
+
secrets
|
|
25933
|
+
}];
|
|
25934
|
+
});
|
|
25935
|
+
}
|
|
25936
|
+
/** `left`: the member asked to leave (on the admin's chat), and the commit says so (`GroupCommitFrame.left`). */
|
|
25937
|
+
remove(memberKey, now = Date.now(), { left = false } = {}) {
|
|
25593
25938
|
return this.serialize(async () => {
|
|
25594
25939
|
this.requireAdmin();
|
|
25595
25940
|
if (!rosterHas(this.roster, memberKey) || memberKey === this.myKey) throw new Error("Not a member");
|
|
25596
|
-
await this.commit("remove", memberKey, now);
|
|
25941
|
+
await this.commit("remove", memberKey, now, left);
|
|
25597
25942
|
});
|
|
25598
25943
|
}
|
|
25599
25944
|
transferAdmin(memberKey, now = Date.now()) {
|
|
@@ -25610,7 +25955,7 @@ var GroupSession = class {
|
|
|
25610
25955
|
await this.commit("rotate", void 0, now);
|
|
25611
25956
|
});
|
|
25612
25957
|
}
|
|
25613
|
-
async commit(kind, subject, now) {
|
|
25958
|
+
async commit(kind, subject, now, left = false) {
|
|
25614
25959
|
if (this.hooks.adminWork?.() === false) throw new Error(GROUP_ADMIN_OFF_ERROR);
|
|
25615
25960
|
if (this.hooks.adminTurn && !await this.hooks.adminTurn()) throw engineError("groupTurnUnconfirmed");
|
|
25616
25961
|
if (this.state.chain.length >= 1024) throw new Error("This group has reached its membership history limit. Create a new group.");
|
|
@@ -25634,6 +25979,8 @@ var GroupSession = class {
|
|
|
25634
25979
|
...draft,
|
|
25635
25980
|
c: confirmationTag(epochKeys(secret, this.id, epoch).confirm, commitUntaggedHash(draft))
|
|
25636
25981
|
}, this.identity.seed);
|
|
25982
|
+
const mark = left && kind === "remove" ? toBase64Url(sign(leftSigned(commit), this.identity.seed)) : void 0;
|
|
25983
|
+
if (mark) (this.state.leftMarks ??= {})[epoch] = mark;
|
|
25637
25984
|
this.state.chain.push(commit);
|
|
25638
25985
|
this.chainMovedAt = this.clock();
|
|
25639
25986
|
this.state.secrets[epoch] = toBase64Url(secret);
|
|
@@ -25649,7 +25996,8 @@ var GroupSession = class {
|
|
|
25649
25996
|
t: "group-commit",
|
|
25650
25997
|
g: this.id,
|
|
25651
25998
|
commit,
|
|
25652
|
-
...member ? { secret: sealSecret(key, secret, secretAad$1(this.id, epoch, key)) } : {}
|
|
25999
|
+
...member ? { secret: sealSecret(key, secret, secretAad$1(this.id, epoch, key)) } : {},
|
|
26000
|
+
...mark ? { left: mark } : {}
|
|
25653
26001
|
});
|
|
25654
26002
|
}
|
|
25655
26003
|
this.hooks.changed();
|
|
@@ -26049,7 +26397,7 @@ var GroupSession = class {
|
|
|
26049
26397
|
case "group-bye":
|
|
26050
26398
|
await this.receiveBye(from, raw);
|
|
26051
26399
|
break;
|
|
26052
|
-
case "group-leave": if (this.isAdmin && this.hooks.adminWork?.() !== false && rosterHas(this.roster, from) && from !== this.myKey) await this.commitUnlessTurnUnconfirmed("remove", from);
|
|
26400
|
+
case "group-leave": if (this.isAdmin && this.hooks.adminWork?.() !== false && rosterHas(this.roster, from) && from !== this.myKey) await this.commitUnlessTurnUnconfirmed("remove", from, true);
|
|
26053
26401
|
}
|
|
26054
26402
|
return this.state.status === "active" ? this.passOn : this.state.status === "removed" ? this.passOn.filter((f) => f.t === "group-commit") : [];
|
|
26055
26403
|
} finally {
|
|
@@ -26164,7 +26512,7 @@ var GroupSession = class {
|
|
|
26164
26512
|
return;
|
|
26165
26513
|
}
|
|
26166
26514
|
if (bye.e < this.admittedAt(bye.k)) return;
|
|
26167
|
-
if (this.isAdmin && this.hooks.adminWork?.() !== false && await this.commitUnlessTurnUnconfirmed("remove", bye.k)) return;
|
|
26515
|
+
if (this.isAdmin && this.hooks.adminWork?.() !== false && await this.commitUnlessTurnUnconfirmed("remove", bye.k, true)) return;
|
|
26168
26516
|
if (from !== bye.k && this.byesPassed.get(bye.k)?.sig === bye.sig) return;
|
|
26169
26517
|
this.byesPassed.set(bye.k, bye);
|
|
26170
26518
|
this.took(bye);
|
|
@@ -26189,9 +26537,9 @@ var GroupSession = class {
|
|
|
26189
26537
|
* An automatic commit (a member's leave): made, or not made when no fresh turn read says this device is the active
|
|
26190
26538
|
* one (`adminTurn`). Then false, and the caller goes on as a member would. Any other refusal is thrown as before.
|
|
26191
26539
|
*/
|
|
26192
|
-
async commitUnlessTurnUnconfirmed(kind, subject) {
|
|
26540
|
+
async commitUnlessTurnUnconfirmed(kind, subject, left = false) {
|
|
26193
26541
|
try {
|
|
26194
|
-
await this.commit(kind, subject, Date.now());
|
|
26542
|
+
await this.commit(kind, subject, Date.now(), left);
|
|
26195
26543
|
return true;
|
|
26196
26544
|
} catch (error) {
|
|
26197
26545
|
if (error instanceof EngineError && error.engineCode === "groupTurnUnconfirmed") return false;
|
|
@@ -26249,7 +26597,7 @@ var GroupSession = class {
|
|
|
26249
26597
|
if (firstFrame && rosterHas(this.state.chain[first.e]?.m ?? [], this.myKey) && this.isDuplicate(firstFrame)) {
|
|
26250
26598
|
this.markSeen(raw);
|
|
26251
26599
|
this.keep(clean(raw));
|
|
26252
|
-
|
|
26600
|
+
this.persistSoon();
|
|
26253
26601
|
if (rosterHas(this.roster, raw.s)) this.took(clean(raw));
|
|
26254
26602
|
return;
|
|
26255
26603
|
}
|
|
@@ -26272,7 +26620,8 @@ var GroupSession = class {
|
|
|
26272
26620
|
...forwarded ? { forwarded } : {},
|
|
26273
26621
|
...card ? { card } : {},
|
|
26274
26622
|
...file ? { file } : {},
|
|
26275
|
-
...completes ? { completes: true } : {}
|
|
26623
|
+
...completes ? { completes: true } : {},
|
|
26624
|
+
...whole ? { frame: clean(raw) } : {}
|
|
26276
26625
|
});
|
|
26277
26626
|
if (!whole) {
|
|
26278
26627
|
this.provisional.add(id);
|
|
@@ -26285,7 +26634,7 @@ var GroupSession = class {
|
|
|
26285
26634
|
this.markSeen(raw);
|
|
26286
26635
|
if (firstFrame && rosterHas(this.state.chain[firstFrame.e]?.m ?? [], this.myKey)) this.markSeen(firstFrame);
|
|
26287
26636
|
this.keep(clean(raw));
|
|
26288
|
-
|
|
26637
|
+
this.persistSoon();
|
|
26289
26638
|
if (rosterHas(this.roster, raw.s)) this.took(clean(raw));
|
|
26290
26639
|
}
|
|
26291
26640
|
/**
|
|
@@ -26452,6 +26801,7 @@ var GroupSession = class {
|
|
|
26452
26801
|
const result = verifyCommit(raw, this.state.chain[e - 1] ?? null, this.id);
|
|
26453
26802
|
if ("error" in result || result.commit.by !== known.by) return;
|
|
26454
26803
|
if (commitHash(result.commit) !== commitHash(known)) await this.fork(`Member ${from.slice(0, 8)} holds a different membership history for epoch ${e}`);
|
|
26804
|
+
else if (this.takeLeftMark(known, frame.left)) await this.persist();
|
|
26455
26805
|
return;
|
|
26456
26806
|
}
|
|
26457
26807
|
if (e > this.epoch + 1) {
|
|
@@ -26465,11 +26815,13 @@ var GroupSession = class {
|
|
|
26465
26815
|
if (result.error.startsWith("Commit does not follow") && verifyCommitSignature(raw)?.by === this.admin) await this.fork(`Member ${from.slice(0, 8)} holds a different membership history for epoch ${e}`);
|
|
26466
26816
|
return;
|
|
26467
26817
|
}
|
|
26818
|
+
const mark = this.takeLeftMark(result.commit, frame.left) ? frame.left : void 0;
|
|
26468
26819
|
await this.apply(result.commit, frame.secret);
|
|
26469
26820
|
this.took({
|
|
26470
26821
|
t: "group-commit",
|
|
26471
26822
|
g: this.id,
|
|
26472
|
-
commit: result.commit
|
|
26823
|
+
commit: result.commit,
|
|
26824
|
+
...mark ? { left: mark } : {}
|
|
26473
26825
|
});
|
|
26474
26826
|
if (this.state.status === "active" && !this.state.secrets[this.epoch]) this.ask(from);
|
|
26475
26827
|
const next = this.pendingCommits.get(this.epoch + 1);
|
|
@@ -26478,6 +26830,17 @@ var GroupSession = class {
|
|
|
26478
26830
|
await this.receiveCommit(from, next);
|
|
26479
26831
|
}
|
|
26480
26832
|
}
|
|
26833
|
+
/** Keeps the admin's mark on a commit that took out a member who left (`GroupCommitFrame.left`) when it holds; true when new. */
|
|
26834
|
+
takeLeftMark(commit, mark) {
|
|
26835
|
+
if (commit.k !== "remove" || typeof mark !== "string" || mark.length !== 86 || !B64$1.test(mark) || this.state.leftMarks?.[commit.e]) return false;
|
|
26836
|
+
try {
|
|
26837
|
+
if (!verify(fromBase64Url(mark), leftSigned(commit), publicKeyFromZ32(commit.by))) return false;
|
|
26838
|
+
} catch {
|
|
26839
|
+
return false;
|
|
26840
|
+
}
|
|
26841
|
+
(this.state.leftMarks ??= {})[commit.e] = mark;
|
|
26842
|
+
return true;
|
|
26843
|
+
}
|
|
26481
26844
|
async apply(commit, sealed) {
|
|
26482
26845
|
this.state.chain.push(commit);
|
|
26483
26846
|
this.chainMovedAt = this.clock();
|
|
@@ -26540,11 +26903,13 @@ var GroupSession = class {
|
|
|
26540
26903
|
const answer = [];
|
|
26541
26904
|
for (let e = frame.e + 1; e <= this.epoch; e++) {
|
|
26542
26905
|
const commit = this.state.chain[e], secret = this.secret(e);
|
|
26906
|
+
const left = this.state.leftMarks?.[e];
|
|
26543
26907
|
answer.push({
|
|
26544
26908
|
t: "group-commit",
|
|
26545
26909
|
g: this.id,
|
|
26546
26910
|
commit,
|
|
26547
|
-
...secret && rosterHas(commit.m, from) ? { secret: sealSecret(from, secret, secretAad$1(this.id, e, from)) } : {}
|
|
26911
|
+
...secret && rosterHas(commit.m, from) ? { secret: sealSecret(from, secret, secretAad$1(this.id, e, from)) } : {},
|
|
26912
|
+
...left ? { left } : {}
|
|
26548
26913
|
});
|
|
26549
26914
|
}
|
|
26550
26915
|
const theirs = new Set(Array.isArray(frame.secrets) ? frame.secrets.filter((n) => Number.isSafeInteger(n)) : []);
|
|
@@ -27534,6 +27899,15 @@ var CommunitySession = class {
|
|
|
27534
27899
|
return -1;
|
|
27535
27900
|
}
|
|
27536
27901
|
/**
|
|
27902
|
+
* Someone the chain removed (not one who left) and who is not back in: nothing it signs is heard any more, even for
|
|
27903
|
+
* an epoch it was in. What one who left wrote while in the group is still taken and handed on, for the epochs it was
|
|
27904
|
+
* in (WISP 903 § Leaving and removal): its goodbye reaches the members who were away.
|
|
27905
|
+
*/
|
|
27906
|
+
wasExpelled(key) {
|
|
27907
|
+
const i = this.outIndex(key);
|
|
27908
|
+
return i >= 0 && this.state.chain[i].k === "remove";
|
|
27909
|
+
}
|
|
27910
|
+
/**
|
|
27537
27911
|
* What someone the chain took out is told when it comes back (WISP 903 § Leaving and removal): the commits from where
|
|
27538
27912
|
* it says it is (`sync`: its tip, or its locator) up to the one that took it out, and nothing after. Later commits
|
|
27539
27913
|
* are not its business (a `link` commit names the new entry key); no secret, no stored frame, no seed goes with them.
|
|
@@ -28496,7 +28870,7 @@ var CommunitySession = class {
|
|
|
28496
28870
|
return false;
|
|
28497
28871
|
}
|
|
28498
28872
|
if (!verify(fromBase64Url(raw.sig), messageSigned(raw), publicKeyFromZ32(raw.s))) return false;
|
|
28499
|
-
if (this.
|
|
28873
|
+
if (this.wasExpelled(raw.s)) return false;
|
|
28500
28874
|
const found = this.commitByShort(raw.e, raw.h);
|
|
28501
28875
|
if (!found) {
|
|
28502
28876
|
this.park(from, raw);
|
|
@@ -28740,7 +29114,7 @@ var CommunitySession = class {
|
|
|
28740
29114
|
if (stored.s === from) continue;
|
|
28741
29115
|
const high = have[stored.s]?.[seenKey(stored.e, stored.h)];
|
|
28742
29116
|
if (Number.isSafeInteger(high) && high >= stored.n) continue;
|
|
28743
|
-
if (this.
|
|
29117
|
+
if (this.wasExpelled(stored.s)) continue;
|
|
28744
29118
|
const found = this.commitByShort(stored.e, stored.h);
|
|
28745
29119
|
if (!found) continue;
|
|
28746
29120
|
const reads = rosterHas(this.rosterAt(found.hash) ?? [], from), mine = stored.s === this.myKey && unheard.has(frameKey(stored));
|
|
@@ -29864,6 +30238,6 @@ function appStoreDecision(held, read) {
|
|
|
29864
30238
|
return read.digest === held.digest ? "same" : "equivocation";
|
|
29865
30239
|
}
|
|
29866
30240
|
//#endregion
|
|
29867
|
-
export { groupName as $, APP_PREFIXES as $a, sanitizeAvatar as $c, peekDhtMailbox as $i, readGroupWantNo as $n, HANDOFF_REQUEST as $o, isAppVersion as $r, deviceInviteJoinerParams as $s, EVM_TEST_CHAINS as $t, encodeCommunityLink as A, readReaction as Aa, turnKeys as Ac, dialDescriptors as Ai, ENDPOINT as An, decodeControl as Ao, transferStalled as Ar, sealHandoffPiece as As, checkDidSignature as At, GroupWakeLimiter as B, RUN_RESULTS as Ba, withRequestOptions as Bc, newIdentityBinding as Bi, pairedMessageFrame as Bn, signalHasVideo as Bo, pinIsNewer as Br, ENROLL_HELLO as Bs, parseDid as Bt, openGroupTyping as C, GROUP_REACTION_FRAME as Ca, TURN_LAST_SEQUENCE as Cc, recordsFromWellKnown as Ci, pubkyProofFile as Cn, RELAY_POLL_INTERVALS as Co, readBeacon as Cr, readHandoffCancel as Cs, AtprotoProofError as Ct, GROUP_READ_NOTE_COMMUNITY as D, queueReaction as Da, readTurnPacket as Dc, CapsExchange as Di, requestLnurlInvoice as Dn, parseLocalTarget as Do, ChatFiles as Dr, readHandoffPake as Ds, DID_JWS_MAX_LENGTH as Dt, CommunitySession as E, nextReactionNumber as Ea, nextTurnPosition as Ec, wellKnownUrl as Ei, pubkyHomeserverOf as En, formatLocalTarget as Eo, shouldBeHub as Er, readHandoffHello as Es, DID_FILE_MAX_BYTES as Et, knockIdentity as F, replySnippet as Fa, normalizeRelayUrl as Fc, IdentityExchange as Fi, parseCashuRequestPayload as Fn, buildSdpFromSignal as Fo, typingActivity as Fr, setUpdateFrame as Fs, didWebFile as Ft, GROUP_READ_NOTE as G, checkStatusCard as Ga, openRelayPayload as Gc, emptyProofLedger as Gi, GROUP_WANT_FRAME as Gn, HANDOFF_ACK as Go, deviceWakeRequest as Gr, enrollDoneFrame as Gs, strictBase64Url as Gt, groupWakes as H, buttonLabelClash as Ha, RELAY_PAYLOAD_MAX_BYTES as Hc, revokesIdentity as Hi, engineText as Hn, HTTP_SERVICE_PROTO as Ho, readPin as Hr, enrollCancelFrame as Hs, parseDidSignaturePaste as Ht, knockRecords as I, BUTTON_ID as Ia, setLinkTraceSink as Ic, boundVerifiedIdentity as Ii, parseFedimintRequestPayload as In, callRtcConfig as Io, GROUP_PIN_FRAME as Ir, signTombstone as Is, didWebFileNames as It, GROUP_EDIT_FRAME as J, arrivalKey as Ja, identityFromSeed as Jc, automaticTransport as Ji, groupFileFallback as Jn, HANDOFF_HAVE as Jo, relayRequest as Jr, enrollProofFrame as Js, encodeDidDhtDeactivated as Jt, GroupSession as K, sameButtonText as Ka, parseRelayPayload as Kc, validExternalKey as Ki, GROUP_WANT_NO_FRAME as Kn, HANDOFF_CANCEL as Ko, newWakeToken as Kr, enrollGrantFrame as Ks, didDhtDocument as Kt, mergeKnocks as L, ITEM_STATES as La, traceLink as Lc, emptyIdentityLedger as Li, GhostLink as Ln, callSignalHeardAt as Lo, PIN_LIMITS as Lr, verifySetUpdate as Ls, didWebFileUrl as Lt, decodeGroupEntryLink as M, wireReaction as Ma, DEFAULT_RELAYS as Mc, IDENTITY_MAX_VALIDITY as Mi, fedimintRequestPayload as Mn, CALL_SIGNAL_MAX_AGE_MS as Mo, TYPING_REFRESH_MS as Mr, SET_UPDATE as Ms, didJwsAlg as Mt, encodeGroupEntryLink as N, pairedReplyAuthor as Na, RelayTransport as Nc, IDENTITY_REVOCATION_LABEL as Ni, findEndpoint as Nn, RTC_CONFIG as No, TypingSender as Nr, readSetUpdate as Ns, didSignedMessages as Nt, communityHasFrame as O, reactionEmoji as Oa, signTurnPacket as Oc, DHT_TEXT_CAPABILITY as Oi, resolveLightningDestination as On, webLocalFetch as Oo, FILE_LIMITS as Or, readHandoffTurnFrame as Os, DidError as Ot, entryParams as P, readReply as Pa, currentRelays as Pc, IdentityCheckUnavailable as Pi, isFederationId as Pn, answersOffer as Po, sanitizeTypingStatus as Pr, setAckFrame as Ps, didWebDocumentUrl as Pt, mentionsMember as Q, shownTime as Qa, MAX_AVATAR_LENGTH as Qc, DHT_MESSAGE_TTL as Qi, readGroupWant as Qn, HANDOFF_PIECE_BYTES as Qo, chatAppId as Qr, createDeviceInvite as Qs, ETHEREUM_USDT as Qt, readKnocks as R, PR_CHECKS as Ra, budgetRetryMs as Rc, identityRevocationValue as Ri, INVITE_TAKEN as Rn, extractParamsFromSdp as Ro, mayPin as Rr, ENROLL_DONE as Rs, didWebHost as Rt, groupTypingEpoch as S, COMMUNITY_REACTION_FRAME as Sa, turnPutSummary as Sc, recordsFromTxt as Si, pubkyProofCapability as Sn, EXPECT_PEER_MS as So, rankHubs as Sr, readHandoffAck as Ss, shortAtprotoDid as St, COMMUNITY_LIMITS as T, ReactionWindow as Ta, firstDeviceSetSecret as Tc, txtValue as Ti, homeserverWebEndpoint as Tn, GhostlyHttpError as To, readLobby as Tr, readHandoffHave as Ts, DID_DOCUMENT_MAX_BYTES as Tt, parseGroupWakeFrame as U, cardLine as Ua, createRelayPayload as Uc, lastSharedWithMe as Ui, GROUP_FILE_LIMITS as Un, LEGACY_SERVICES as Uo, WAKE_CALL_INTERVAL_MS as Ur, enrollCancelReason as Us, signingKeys as Ut, groupWakeFrame as V, STATUS_CARD_LIMITS as Va, PacketTooLargeError as Vc, receivedIdentityStatus as Vi, engineError as Vn, emptyLinkRecords as Vo, pinNumberHolds as Vr, ENROLL_PROOF as Vs, parseDidDocument as Vt, GROUP_REACTED_FRAME as W, cardUrl as Wa, newerPacket as Wc, PeerProofs as Wi, GROUP_HAVE_FRAME as Wn, serviceIdFromName as Wo, WakeLimiter as Wr, enrollDigits as Ws, staticDidDocument as Wt, carryMentions as X, heardTime as Xa, publicKeyFromZ32 as Xc, irohRelayUrl as Xi, readGroupFileMeta as Xn, HANDOFF_OFFER as Xo, checkPushEndpoint as Xr, readEnrollHello as Xs, resolveDidDht as Xt, GROUP_EDIT_TEXT_BYTES as Y, claimedTime as Ya, identityFromSeedB64 as Yc, irohDescriptor as Yi, isGroupMessageId as Yn, HANDOFF_HELLO as Yo, wakeRequest as Yr, readEnrollGrant as Ys, encodeDidDhtPacket as Yt, parseCommunityEdit as Z, receivedTimestamp as Za, verify as Zc, DHT_FILE_TOO_LARGE as Zi, readGroupHave as Zn, HANDOFF_PAKE as Zo, vapidKeysMatch as Zr, verifyEnrollProof as Zs, signDidDhtPacket as Zt, checkAppManifest as _, RateWindow as _a, webCryptoSigner as _c, domainWellKnownFile as _i, PUBKY_PROOF_MAX_BYTES as _n, rosterHas as _o, lobbyRecords as _r, handoffReleaseFrame as _s, atprotoProofRkey as _t, planAppUpdate as a, safeBlobType as aa, inviteLink as ac, HOLD_CAPABILITY as ai, utf8Decode as al, USDT_PUBLIC_RPC as an, isAppRef as ao, groupSignalFrame as ar, handoffAttemptSucceeded as as, sameSshKey as at, GROUP_TYPING_FRAME as b, forwardedAgain as ba, TURN_SETTLE_MS as bc, nip05RootKey as bi, newPubkyProofFolder as bn, DirectPathWatch as bo, newerHead as br, handoffVersions as bs, normalizeAtprotoHandle as bt, readAppStore as c, PLAYABLE_AUDIO as ca, ENROLL_CAPABILITY as cc, HoldKeys as ci, assertWholeSats as cn, signAppObject as co, mayBeHub as cr, handoffContext as cs, verifyBitcoinMessage as ct, APP_BUNDLE_LIMITS as d, fileMessageText as da, deviceEchoNonce as dc, newMailbox as di, walletNetworkOf as dn, canonicalJsonBytes as do, COMMUNITY_TOPOLOGY as dr, handoffDigest as ds, ATPROTO_DECLARED_SCOPE as dt, DHT_FILE_CAPABILITY as ea, readDeviceInvite as ec, APPS_CAPABILITY as ei, bytesEqual as el, ONCHAIN_PROVIDER as en, APP_STATEMENT_LIMITS as eo, CarriedTransport as er, HANDOFF_TIMINGS as es, SSHSIG_MAX_ARMOR as et, APP_BUNDLE_MAGIC as f, parseVoiceMeta as fa, deviceKeyZ32 as fc, readManifest as fi, SPARK_NETWORKS as fn, readCanonicalJson as fo, beaconKeys as fr, handoffEphemeral as fs, ATPROTO_FULL_SCOPE as ft, assembleAppBundle as g, EDIT_RATE_WINDOW_MS as ga, seedSigner as gc, domainTxtRecord as gi, isBitcoinAddress as gn, commitHash as go, lobbyKeys as gr, handoffPakeFrame as gs, atprotoProofRecord as gt, appViewOf as h, EDIT_BUFFER_MS as ha, newDeviceSetSecret as hc, dnsProofName as hi, sparkInvoiceDetails as hn, MEMBER_KEY as ho, freshHubs as hr, handoffOfferFrame as hs, atprotoDidMethod as ht, checkAppBeforeRun as i, formatFileSize as ia, encodeInviteCode as ic, UPGRADE_CAPABILITY as ii, toZ32 as il, TEST_USDT_FAUCET_AMOUNT as in, isAppKey as io, SIGNAL_REPLY_MS as ir, handoffAttemptFailed as is, parseSshSignature as it, KNOCK_TTL_MS as j, validReactionNumber as ja, turnName as jc, IDENTITY_MAX_EVIDENCE as ji, cashuRequestPayload as jn, encodeControl as jo, TYPING_KINDS as jr, SET_ACK as js, didGhostlyServiceEntry as jt, decodeCommunityLink as k, reactionIsNewer as ka, signTurnRelease as kc, capsDescriptors as ki, decodeBolt11 as kn, LIMITS as ko, transferEnded as kr, readHandoffVerified as ks, checkDidJws as kt, readAppStoreSteps as l, VOICE_LIMITS as la, HANDOFF_CAPABILITY as lc, HoldRefusedError as li, formatPaymentAmount as ln, signAppRevocation as lo, meshHubs as lr, handoffContextProof as ls, BitcoinAddressError as lt, appDigest as m, parseVideoMeta as ma, devicePingFrame as mc, decodeDnsResponse as mi, sparkAddressKind as mn, MAX_GROUP_CHAIN as mo, doorHubs as mr, handoffManifestFrames as ms, ATPROTO_PROOF_SCOPE as mt, appStoreDecision as n, parseImageMeta as na, createChatInvite as nc, EDIT_CAPABILITY as ni, randomBytes as nl, SEPOLIA_TEST_USDT as nn, appRef as no, SIGNAL_FRESH_MS as nr, filePartName as ns, SshSigError as nt, APP_STORE_LIMITS as o, sanitizeFileName as oa, inviteOwnership as oc, HOLD_DATED_BACK_MS as oi, utf8Encode as ol, WALLET_NETWORKS as on, readAppRevocation as oo, readGroupSignal as or, handoffBusyFrame as os, sshSignCommand as ot, APP_ICON_PATH as p, voicePeaksOf as pa, deviceLinkPairing as pc, DOMAIN_FILE_MAX_BYTES as pi, SPARK_PROVIDER as pn, GROUP_MEMBER_CAP as po, beaconRecords as pr, handoffHelloFrame as ps, ATPROTO_PROOF_COLLECTION as pt, COMMUNITY_EDIT_FRAME as q, statusCardText as qa, createIdentity as qc, TRANSPORTS as qi, fetchesByItself as qn, HANDOFF_DONE as qo, parseWakeFrame as qr, enrollHelloFrame as qs, didDhtFromPublicKey as qt, appUpdateDecision as r, readImageMeta as ra, decodeInviteCode as rc, STATUS_CARD_CAPABILITY as ri, toBase64Url as rl, SEPOLIA_TEST_USDT_FAUCET as rn, checkAppRevokeStatement as ro, SIGNAL_HOLD_MS as rr, handoffAttemptAllowed as rs, parseSshPublicKey as rt, checkAppStoreIndex as s, sanitizeMime as sa, DEVICES_CAPABILITY as sc, HOLD_LIMITS as si, assertTokenUnits as sn, readAppRevocations as so, MESH_HUBS as sr, handoffCancelFrame as ss, verifySshSignature as st, appRevocationFor as t, dhtFileId as ta, OwnInviteError as tc, BUTTONS_CAPABILITY as ti, fromBase64Url as tl, PaymentPreflightError as tn, appFingerprint as to, GROUP_SIGNAL_FRAME as tr, HANDOFF_VERIFIED as ts, SSHSIG_NAMESPACE as tt, signAppStore as u, baseMime as ua, deviceEchoFrame as uc, isHoldUrl as ui, validatePaymentTarget as un, canonicalJson as uo, pickMeshHubs as ur, handoffDataFrame as us, decodeAnyBitcoinAddress as ut, isAppUrl as v, dhtEditId as va, TURN_PUT_WINDOW_MS as vc, encodeDnsQuery as vi, isPubkyKey as vn, verifyCommitSignature as vo, mergeBeacon as vr, handoffRetryAfter as vs, checkAtprotoProofRecord as vt, sealGroupTyping as w, REACTION_LIMITS as wa, TURN_MAX as wc, recordsName as wi, pubkyProofPath as wn, presenceSeenAt as wo, readBeaconHead as wr, readHandoffDone as ws, verifyAtprotoRecordProof as wt, GroupTypingBoard as x, readForwarded as xa, classifyTurnRead as xc, normalizeDomain as xi, normalizePubkyKey as xn, DHT_POLL_INTERVALS as xo, pickHubs as xr, newHandoffId as xs, parseAtprotoDidDocument as xt, readAppBundle as y, parseLinkPreview as ya, TURN_RAISE_READ_TIMEOUT_MS as yc, isPublicAddress as yi, isPubkyProofFolder as yn, edgeParams as yo, mergeLobby as yr, handoffStreamKey as ys, isAtprotoDid as yt, GROUP_WAKE_FRAME as z, PR_STATES as za, isDiscoveryBudgetError as zc, identityStatement as zi, LIVENESS_PING_MS as zn, parseCallSignal as zo, nextPinNumber as zr, ENROLL_GRANT as zs, normalizeDid as zt };
|
|
30241
|
+
export { parseCommunityEdit as $, receivedTimestamp as $a, identityFromSeed as $c, DHT_FILE_TOO_LARGE as $i, readGroupHave as $n, HANDOFF_PAKE as $o, vapidKeysMatch as $r, verifyEnrollProof as $s, signDidDhtPacket as $t, encodeCommunityLink as A, reactionEmoji as Aa, signTurnPacket as Ac, DHT_TEXT_CAPABILITY as Ai, resolveLightningDestination as An, webLocalFetch as Ao, FILE_LIMITS as Ar, readHandoffTurnFrame as As, DidError as At, GroupWakeLimiter as B, PR_CHECKS as Ba, normalizeRelayUrl as Bc, identityRevocationValue as Bi, INVITE_TAKEN as Bn, extractParamsFromSdp as Bo, mayPin as Br, ENROLL_DONE as Bs, didWebHost as Bt, openGroupTyping as C, readForwarded as Ca, classifyTurnRead as Cc, normalizeDomain as Ci, normalizePubkyKey as Cn, DHT_POLL_INTERVALS as Co, pickHubs as Cr, newHandoffId as Cs, parseAtprotoDidDocument as Ct, GROUP_READ_NOTE_COMMUNITY as D, ReactionWindow as Da, firstDeviceSetSecret as Dc, txtValue as Di, homeserverWebEndpoint as Dn, GhostlyHttpError as Do, readLobby as Dr, readHandoffHave as Ds, DID_DOCUMENT_MAX_BYTES as Dt, CommunitySession as E, REACTION_LIMITS as Ea, TURN_MAX as Ec, recordsName as Ei, pubkyProofPath as En, presenceSeenAt as Eo, readBeaconHead as Er, readHandoffDone as Es, verifyAtprotoRecordProof as Et, knockIdentity as F, pairedReplyAuthor as Fa, turnStatedVerifier as Fc, IDENTITY_REVOCATION_LABEL as Fi, findEndpoint as Fn, RTC_CONFIG as Fo, TypingSender as Fr, readSetUpdate as Fs, didSignedMessages as Ft, GROUP_REACTED_FRAME as G, cardLine as Ga, withRequestOptions as Gc, lastSharedWithMe as Gi, GROUP_FILE_LIMITS as Gn, LEGACY_SERVICES as Go, WAKE_CALL_INTERVAL_MS as Gr, enrollCancelReason as Gs, signingKeys as Gt, groupWakes as H, RUN_RESULTS as Ha, traceLink as Hc, newIdentityBinding as Hi, pairedMessageFrame as Hn, signalHasVideo as Ho, pinIsNewer as Hr, ENROLL_HELLO as Hs, parseDid as Ht, knockRecords as I, readReply as Ia, turnVerifierMark as Ic, IdentityCheckUnavailable as Ii, isFederationId as In, answersOffer as Io, sanitizeTypingStatus as Ir, setAckFrame as Is, didWebDocumentUrl as It, frameBytes as J, sameButtonText as Ja, createRelayPayload as Jc, validExternalKey as Ji, GROUP_WANT_NO_FRAME as Jn, HANDOFF_CANCEL as Jo, newWakeToken as Jr, enrollGrantFrame as Js, didDhtDocument as Jt, GROUP_READ_NOTE as K, cardUrl as Ka, PacketTooLargeError as Kc, PeerProofs as Ki, GROUP_HAVE_FRAME as Kn, serviceIdFromName as Ko, WakeLimiter as Kr, enrollDigits as Ks, staticDidDocument as Kt, mergeKnocks as L, replySnippet as La, DEFAULT_RELAYS as Lc, IdentityExchange as Li, parseCashuRequestPayload as Ln, buildSdpFromSignal as Lo, typingActivity as Lr, setUpdateFrame as Ls, didWebFile as Lt, decodeGroupEntryLink as M, readReaction as Ma, turnInstance as Mc, dialDescriptors as Mi, ENDPOINT as Mn, decodeControl as Mo, transferStalled as Mr, sealHandoffPiece as Ms, checkDidSignature as Mt, encodeGroupEntryLink as N, validReactionNumber as Na, turnKeys as Nc, IDENTITY_MAX_EVIDENCE as Ni, cashuRequestPayload as Nn, encodeControl as No, TYPING_KINDS as Nr, SET_ACK as Ns, didGhostlyServiceEntry as Nt, communityHasFrame as O, nextReactionNumber as Oa, nextTurnPosition as Oc, wellKnownUrl as Oi, pubkyHomeserverOf as On, formatLocalTarget as Oo, shouldBeHub as Or, readHandoffHello as Os, DID_FILE_MAX_BYTES as Ot, entryParams as P, wireReaction as Pa, turnName as Pc, IDENTITY_MAX_VALIDITY as Pi, fedimintRequestPayload as Pn, CALL_SIGNAL_MAX_AGE_MS as Po, TYPING_REFRESH_MS as Pr, SET_UPDATE as Ps, didJwsAlg as Pt, carryMentions as Q, heardTime as Qa, createIdentity as Qc, irohRelayUrl as Qi, readGroupFileMeta as Qn, HANDOFF_OFFER as Qo, checkPushEndpoint as Qr, readEnrollHello as Qs, resolveDidDht as Qt, readKnocks as R, BUTTON_ID as Ra, RelayTransport as Rc, boundVerifiedIdentity as Ri, parseFedimintRequestPayload as Rn, callRtcConfig as Ro, GROUP_PIN_FRAME as Rr, signTombstone as Rs, didWebFileNames as Rt, groupTypingEpoch as S, forwardedAgain as Sa, TURN_SETTLE_MS as Sc, nip05RootKey as Si, newPubkyProofFolder as Sn, DirectPathWatch as So, newerHead as Sr, handoffVersions as Ss, normalizeAtprotoHandle as St, COMMUNITY_LIMITS as T, GROUP_REACTION_FRAME as Ta, TURN_LAST_SEQUENCE as Tc, recordsFromWellKnown as Ti, pubkyProofFile as Tn, RELAY_POLL_INTERVALS as To, readBeacon as Tr, readHandoffCancel as Ts, AtprotoProofError as Tt, parseGroupWakeFrame as U, STATUS_CARD_LIMITS as Ua, budgetRetryMs as Uc, receivedIdentityStatus as Ui, engineError as Un, emptyLinkRecords as Uo, pinNumberHolds as Ur, ENROLL_PROOF as Us, parseDidDocument as Ut, groupWakeFrame as V, PR_STATES as Va, setLinkTraceSink as Vc, identityStatement as Vi, LIVENESS_PING_MS as Vn, parseCallSignal as Vo, nextPinNumber as Vr, ENROLL_GRANT as Vs, normalizeDid as Vt, GROUP_LIMITS as W, buttonLabelClash as Wa, isDiscoveryBudgetError as Wc, revokesIdentity as Wi, engineText as Wn, HTTP_SERVICE_PROTO as Wo, readPin as Wr, enrollCancelFrame as Ws, parseDidSignaturePaste as Wt, GROUP_EDIT_FRAME as X, arrivalKey as Xa, openRelayPayload as Xc, automaticTransport as Xi, groupFileFallback as Xn, HANDOFF_HAVE as Xo, relayRequest as Xr, enrollProofFrame as Xs, encodeDidDhtDeactivated as Xt, COMMUNITY_EDIT_FRAME as Y, statusCardText as Ya, newerPacket as Yc, TRANSPORTS as Yi, fetchesByItself as Yn, HANDOFF_DONE as Yo, parseWakeFrame as Yr, enrollHelloFrame as Ys, didDhtFromPublicKey as Yt, GROUP_EDIT_TEXT_BYTES as Z, claimedTime as Za, parseRelayPayload as Zc, irohDescriptor as Zi, isGroupMessageId as Zn, HANDOFF_HELLO as Zo, wakeRequest as Zr, readEnrollGrant as Zs, encodeDidDhtPacket as Zt, checkAppManifest as _, EDIT_BUFFER_MS as _a, newDeviceSetSecret as _c, dnsProofName as _i, sparkInvoiceDetails as _n, MEMBER_KEY as _o, freshHubs as _r, handoffOfferFrame as _s, atprotoDidMethod as _t, planAppUpdate as a, readImageMeta as aa, decodeInviteCode as ac, STATUS_CARD_CAPABILITY as ai, bytesEqual as al, SEPOLIA_TEST_USDT_FAUCET as an, checkAppRevokeStatement as ao, SIGNAL_HOLD_MS as ar, handoffAttemptAllowed as as, parseSshPublicKey as at, GROUP_TYPING_FRAME as b, dhtEditId as ba, TURN_PUT_WINDOW_MS as bc, encodeDnsQuery as bi, isPubkyKey as bn, verifyCommitSignature as bo, mergeBeacon as br, handoffRetryAfter as bs, checkAtprotoProofRecord as bt, readAppStore as c, sanitizeFileName as ca, inviteOwnership as cc, HOLD_DATED_BACK_MS as ci, toBase64Url as cl, WALLET_NETWORKS as cn, readAppRevocation as co, readGroupSignal as cr, handoffBusyFrame as cs, sshSignCommand as ct, APP_BUNDLE_LIMITS as d, VOICE_LIMITS as da, HANDOFF_CAPABILITY as dc, HoldRefusedError as di, utf8Encode as dl, formatPaymentAmount as dn, signAppRevocation as do, meshHubs as dr, handoffContextProof as ds, BitcoinAddressError as dt, DHT_MESSAGE_TTL as ea, createDeviceInvite as ec, chatAppId as ei, identityFromSeedB64 as el, ETHEREUM_USDT as en, shownTime as eo, readGroupWant as er, HANDOFF_PIECE_BYTES as es, mentionsMember as et, APP_BUNDLE_MAGIC as f, baseMime as fa, deviceEchoFrame as fc, isHoldUrl as fi, validatePaymentTarget as fn, canonicalJson as fo, pickMeshHubs as fr, handoffDataFrame as fs, decodeAnyBitcoinAddress as ft, assembleAppBundle as g, parseVideoMeta as ga, devicePingFrame as gc, decodeDnsResponse as gi, sparkAddressKind as gn, MAX_GROUP_CHAIN as go, doorHubs as gr, handoffManifestFrames as gs, ATPROTO_PROOF_SCOPE as gt, appViewOf as h, voicePeaksOf as ha, deviceLinkPairing as hc, DOMAIN_FILE_MAX_BYTES as hi, SPARK_PROVIDER as hn, GROUP_MEMBER_CAP as ho, beaconRecords as hr, handoffHelloFrame as hs, ATPROTO_PROOF_COLLECTION as ht, checkAppBeforeRun as i, parseImageMeta as ia, createChatInvite as ic, EDIT_CAPABILITY as ii, sanitizeAvatar as il, SEPOLIA_TEST_USDT as in, appRef as io, SIGNAL_FRESH_MS as ir, filePartName as is, SshSigError as it, KNOCK_TTL_MS as j, reactionIsNewer as ja, signTurnRelease as jc, capsDescriptors as ji, decodeBolt11 as jn, LIMITS as jo, transferEnded as jr, readHandoffVerified as js, checkDidJws as jt, decodeCommunityLink as k, queueReaction as ka, readTurnPacket as kc, CapsExchange as ki, requestLnurlInvoice as kn, parseLocalTarget as ko, ChatFiles as kr, readHandoffPake as ks, DID_JWS_MAX_LENGTH as kt, readAppStoreSteps as l, sanitizeMime as la, DEVICES_CAPABILITY as lc, HOLD_LIMITS as li, toZ32 as ll, assertTokenUnits as ln, readAppRevocations as lo, MESH_HUBS as lr, handoffCancelFrame as ls, verifySshSignature as lt, appDigest as m, parseVoiceMeta as ma, deviceKeyZ32 as mc, readManifest as mi, SPARK_NETWORKS as mn, readCanonicalJson as mo, beaconKeys as mr, handoffEphemeral as ms, ATPROTO_FULL_SCOPE as mt, appStoreDecision as n, DHT_FILE_CAPABILITY as na, readDeviceInvite as nc, APPS_CAPABILITY as ni, verify as nl, ONCHAIN_PROVIDER as nn, APP_STATEMENT_LIMITS as no, CarriedTransport as nr, HANDOFF_TIMINGS as ns, SSHSIG_MAX_ARMOR as nt, APP_STORE_LIMITS as o, formatFileSize as oa, encodeInviteCode as oc, UPGRADE_CAPABILITY as oi, fromBase64Url as ol, TEST_USDT_FAUCET_AMOUNT as on, isAppKey as oo, SIGNAL_REPLY_MS as or, handoffAttemptFailed as os, parseSshSignature as ot, APP_ICON_PATH as p, fileMessageText as pa, deviceEchoNonce as pc, newMailbox as pi, walletNetworkOf as pn, canonicalJsonBytes as po, COMMUNITY_TOPOLOGY as pr, handoffDigest as ps, ATPROTO_DECLARED_SCOPE as pt, GroupSession as q, checkStatusCard as qa, RELAY_PAYLOAD_MAX_BYTES as qc, emptyProofLedger as qi, GROUP_WANT_FRAME as qn, HANDOFF_ACK as qo, deviceWakeRequest as qr, enrollDoneFrame as qs, strictBase64Url as qt, appUpdateDecision as r, dhtFileId as ra, OwnInviteError as rc, BUTTONS_CAPABILITY as ri, MAX_AVATAR_LENGTH as rl, PaymentPreflightError as rn, appFingerprint as ro, GROUP_SIGNAL_FRAME as rr, HANDOFF_VERIFIED as rs, SSHSIG_NAMESPACE as rt, checkAppStoreIndex as s, safeBlobType as sa, inviteLink as sc, HOLD_CAPABILITY as si, randomBytes as sl, USDT_PUBLIC_RPC as sn, isAppRef as so, groupSignalFrame as sr, handoffAttemptSucceeded as ss, sameSshKey as st, appRevocationFor as t, peekDhtMailbox as ta, deviceInviteJoinerParams as tc, isAppVersion as ti, publicKeyFromZ32 as tl, EVM_TEST_CHAINS as tn, APP_PREFIXES as to, readGroupWantNo as tr, HANDOFF_REQUEST as ts, groupName as tt, signAppStore as u, PLAYABLE_AUDIO as ua, ENROLL_CAPABILITY as uc, HoldKeys as ui, utf8Decode as ul, assertWholeSats as un, signAppObject as uo, mayBeHub as ur, handoffContext as us, verifyBitcoinMessage as ut, isAppUrl as v, EDIT_RATE_WINDOW_MS as va, seedSigner as vc, domainTxtRecord as vi, isBitcoinAddress as vn, commitHash as vo, lobbyKeys as vr, handoffPakeFrame as vs, atprotoProofRecord as vt, sealGroupTyping as w, COMMUNITY_REACTION_FRAME as wa, turnPutSummary as wc, recordsFromTxt as wi, pubkyProofCapability as wn, EXPECT_PEER_MS as wo, rankHubs as wr, readHandoffAck as ws, shortAtprotoDid as wt, GroupTypingBoard as x, parseLinkPreview as xa, TURN_RAISE_READ_TIMEOUT_MS as xc, isPublicAddress as xi, isPubkyProofFolder as xn, edgeParams as xo, mergeLobby as xr, handoffStreamKey as xs, isAtprotoDid as xt, readAppBundle as y, RateWindow as ya, webCryptoSigner as yc, domainWellKnownFile as yi, PUBKY_PROOF_MAX_BYTES as yn, rosterHas as yo, lobbyRecords as yr, handoffReleaseFrame as ys, atprotoProofRkey as yt, GROUP_WAKE_FRAME as z, ITEM_STATES as za, currentRelays as zc, emptyIdentityLedger as zi, GhostLink as zn, callSignalHeardAt as zo, PIN_LIMITS as zr, verifySetUpdate as zs, didWebFileUrl as zt };
|
|
29868
30242
|
|
|
29869
|
-
//# sourceMappingURL=src-
|
|
30243
|
+
//# sourceMappingURL=src-CbLkHfz8.js.map
|