@ghostlytools/cli 1.2.4 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -9
- package/dist/assets/api-Dq2QXQDk.js +4172 -0
- package/dist/assets/api-Dq2QXQDk.js.map +1 -0
- package/dist/assets/api-TiB5QSG9.js +3 -0
- package/dist/assets/apiKit-CA2x5GJF.js +200 -0
- package/dist/assets/apiKit-CA2x5GJF.js.map +1 -0
- package/dist/assets/apps-BxV9i_Y4.js +786 -0
- package/dist/assets/apps-BxV9i_Y4.js.map +1 -0
- package/dist/assets/apps-C-T8jeji.js +2 -0
- package/dist/assets/{arkade-CdnKGDbg.js → arkade-C_qDmHnm.js} +2 -2
- package/dist/assets/{arkade-CdnKGDbg.js.map → arkade-C_qDmHnm.js.map} +1 -1
- package/dist/assets/backup-Bugnn3Ue.js +2 -0
- package/dist/assets/backup-CnMN2oUK.js +731 -0
- package/dist/assets/backup-CnMN2oUK.js.map +1 -0
- package/dist/assets/bark_ffi_wasm_bg-EY2uc0Bx.js.map +1 -1
- package/dist/assets/bitcoind-CZchoh43.js +349 -0
- package/dist/assets/bitcoind-CZchoh43.js.map +1 -0
- package/dist/assets/bitcoindProvider-DGw3xBSa.js +97 -0
- package/dist/assets/bitcoindProvider-DGw3xBSa.js.map +1 -0
- package/dist/assets/bitcoindevkit_bg-B-iLWrmX.js.map +1 -1
- package/dist/assets/buttons-Bj7Vf7Pn.js +2 -0
- package/dist/assets/buttons-CvNBzh2S.js +496 -0
- package/dist/assets/buttons-CvNBzh2S.js.map +1 -0
- package/dist/assets/{endpoint-pgFkR2AS.js → endpoint-BOTFMpfS.js} +56 -18
- package/dist/assets/endpoint-BOTFMpfS.js.map +1 -0
- package/dist/assets/envelope-WjingYSc.js +90 -0
- package/dist/assets/envelope-WjingYSc.js.map +1 -0
- package/dist/assets/errors-DO5z6h_l.js +105 -0
- package/dist/assets/errors-DO5z6h_l.js.map +1 -0
- package/dist/assets/fedimint_client_wasm_bg-BtHHlFpp.js.map +1 -1
- package/dist/assets/{fileBytes-Dbifu5I5.js → fileBytes-CfSXjTFa.js} +2 -2
- package/dist/assets/{fileBytes-Dbifu5I5.js.map → fileBytes-CfSXjTFa.js.map} +1 -1
- package/dist/assets/fileBytes-CgbibpbV.js +2 -0
- package/dist/assets/{fileBytes-BcYZhNgF.js → fileBytes-Di7VZtmU.js} +6 -19
- package/dist/assets/fileBytes-Di7VZtmU.js.map +1 -0
- package/dist/assets/fileBytesIdb-CFP-DSkZ.js +2 -0
- package/dist/assets/{fileBytesIdb-DzSaqI0A.js → fileBytesIdb-D5u6H8X_.js} +4 -4
- package/dist/assets/{fileBytesIdb-DzSaqI0A.js.map → fileBytesIdb-D5u6H8X_.js.map} +1 -1
- package/dist/assets/{fileBytesOpfs-Dx8RZiV5.js → fileBytesOpfs-Dd1N4ERH.js} +3 -3
- package/dist/assets/{fileBytesOpfs-Dx8RZiV5.js.map → fileBytesOpfs-Dd1N4ERH.js.map} +1 -1
- package/dist/assets/groupFiles-IEUW3DQz.js +3202 -0
- package/dist/assets/groupFiles-IEUW3DQz.js.map +1 -0
- package/dist/assets/host-CWt-jtqz.js +4673 -0
- package/dist/assets/host-CWt-jtqz.js.map +1 -0
- package/dist/assets/hyperdhtRelay-BT_wRxab.js +21 -0
- package/dist/assets/hyperdhtRelay-BT_wRxab.js.map +1 -0
- package/dist/assets/{hyperdhtRelay-C1p0nkle.js → hyperdhtRelay-CPYwSLLS.js} +25 -10
- package/dist/assets/hyperdhtRelay-CPYwSLLS.js.map +1 -0
- package/dist/assets/{idb-CTF6LF5B.js → idb-B5L8wuOm.js} +7 -5
- package/dist/assets/idb-B5L8wuOm.js.map +1 -0
- package/dist/assets/irohWeb-4oChIOIp.js +544 -0
- package/dist/assets/irohWeb-4oChIOIp.js.map +1 -0
- package/dist/assets/{joinTrace-CO8mlhNO.js → joinTrace-CjRDFy85.js} +2 -7
- package/dist/assets/{joinTrace-CO8mlhNO.js.map → joinTrace-CjRDFy85.js.map} +1 -1
- package/dist/assets/joinTrace-D-fnt2p3.js +2 -0
- package/dist/assets/main-DbqcdShN.js +4409 -0
- package/dist/assets/main-DbqcdShN.js.map +1 -0
- package/dist/assets/{mainline-BH6s8HVi.js → mainline-ZkiOnevs.js} +11 -6
- package/dist/assets/mainline-ZkiOnevs.js.map +1 -0
- package/dist/assets/mints-BrlY773j.js +74 -0
- package/dist/assets/mints-BrlY773j.js.map +1 -0
- package/dist/assets/mints-CqTdA4Gk.js +2 -0
- package/dist/assets/networkReason-CetKkTPo.js +154 -0
- package/dist/assets/networkReason-CetKkTPo.js.map +1 -0
- package/dist/assets/nwc-CgkqyvsE.js +612 -0
- package/dist/assets/nwc-CgkqyvsE.js.map +1 -0
- package/dist/assets/nwcProvider-4eVz_8HP.js +77 -0
- package/dist/assets/nwcProvider-4eVz_8HP.js.map +1 -0
- package/dist/assets/privateFolder-Bk9C9oFv.js +38 -0
- package/dist/assets/privateFolder-Bk9C9oFv.js.map +1 -0
- package/dist/assets/profiles-BHc9pEYg.js +139 -0
- package/dist/assets/profiles-BHc9pEYg.js.map +1 -0
- package/dist/assets/registry-DlAKO0Fy.js +3793 -0
- package/dist/assets/registry-DlAKO0Fy.js.map +1 -0
- package/dist/assets/registry-OBJBnHVQ.js +2 -0
- package/dist/assets/resolve-CouHhCpJ.js +591 -0
- package/dist/assets/resolve-CouHhCpJ.js.map +1 -0
- package/dist/assets/{restoredRows-C_84q0Pd.js → restoredRows-Db3oUTko.js} +2 -2
- package/dist/assets/{restoredRows-C_84q0Pd.js.map → restoredRows-Db3oUTko.js.map} +1 -1
- package/dist/assets/{server-0zfNpABQ.js → server-Ss0gJ-nw.js} +1275 -1404
- package/dist/assets/server-Ss0gJ-nw.js.map +1 -0
- package/dist/assets/{src-CbLkHfz8.js → src-DDzSERyc.js} +882 -134
- package/dist/assets/src-DDzSERyc.js.map +1 -0
- package/dist/assets/turnRefs-B87dtrHM.js +323 -0
- package/dist/assets/turnRefs-B87dtrHM.js.map +1 -0
- package/dist/assets/types-C4_KafL_.js +122 -0
- package/dist/assets/types-C4_KafL_.js.map +1 -0
- package/dist/assets/{usdt-Qf4UuygS.js → usdt-Fj8CQO2z.js} +3 -3
- package/dist/assets/{usdt-Qf4UuygS.js.map → usdt-Fj8CQO2z.js.map} +1 -1
- package/dist/ghostly.mjs +4 -22077
- package/dist/ghostly.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/assets/endpoint-pgFkR2AS.js.map +0 -1
- package/dist/assets/fileBytes-BcYZhNgF.js.map +0 -1
- package/dist/assets/hyperdhtRelay-C1p0nkle.js.map +0 -1
- package/dist/assets/idb-CTF6LF5B.js.map +0 -1
- package/dist/assets/mainline-BH6s8HVi.js.map +0 -1
- package/dist/assets/rolldown-runtime-8H4AJuhK.js +0 -14
- package/dist/assets/server-0zfNpABQ.js.map +0 -1
- package/dist/assets/src-CbLkHfz8.js.map +0 -1
|
@@ -515,27 +515,31 @@ function signRelayPayload(identity, dnsPacket, seq) {
|
|
|
515
515
|
new DataView(timestamp.buffer).setBigUint64(0, seq);
|
|
516
516
|
return concatBytes$1(signature, timestamp, dnsPacket);
|
|
517
517
|
}
|
|
518
|
-
/**
|
|
519
|
-
function
|
|
518
|
+
/** A relay payload's parts, its size checked: the signature, the sequence number and the DNS packet (not decoded). */
|
|
519
|
+
function splitRelayPayload(payload) {
|
|
520
520
|
if (payload.length < 84) throw new Error("Relay payload too short");
|
|
521
521
|
if (payload.length > 1072) throw new PacketTooLargeError(payload.length);
|
|
522
|
+
return {
|
|
523
|
+
signature: payload.subarray(0, SIGNATURE_LENGTH),
|
|
524
|
+
seq: new DataView(payload.buffer, payload.byteOffset + SIGNATURE_LENGTH, TIMESTAMP_LENGTH).getBigUint64(0),
|
|
525
|
+
dnsPacket: payload.subarray(72)
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
/** Checks a relay payload's size and signature. Returns its sequence number and DNS packet, not decoded. */
|
|
529
|
+
function openRelayPayload(pubKeyZ32, payload) {
|
|
530
|
+
const { signature, seq, dnsPacket } = splitRelayPayload(payload);
|
|
522
531
|
const publicKey = publicKeyFromZ32(pubKeyZ32);
|
|
523
|
-
const signature = payload.subarray(0, SIGNATURE_LENGTH);
|
|
524
|
-
const seq = new DataView(payload.buffer, payload.byteOffset + SIGNATURE_LENGTH, TIMESTAMP_LENGTH).getBigUint64(0);
|
|
525
|
-
const dnsPacket = payload.subarray(72);
|
|
526
532
|
if (!verify(signature, signable(seq, dnsPacket), publicKey)) throw new Error("Invalid signature");
|
|
527
533
|
return {
|
|
528
534
|
seq,
|
|
529
535
|
dnsPacket
|
|
530
536
|
};
|
|
531
537
|
}
|
|
532
|
-
|
|
533
|
-
function parseRelayPayload(pubKeyZ32, payload) {
|
|
534
|
-
const { seq: timestampMicros, dnsPacket } = openRelayPayload(pubKeyZ32, payload);
|
|
538
|
+
function decodeRelayPayload(pubKeyZ32, seq, dnsPacket) {
|
|
535
539
|
const suffix = `.${pubKeyZ32}`;
|
|
536
540
|
return {
|
|
537
541
|
pubKeyZ32,
|
|
538
|
-
timestampMicros,
|
|
542
|
+
timestampMicros: seq,
|
|
539
543
|
records: decodeTxtPacket(dnsPacket).map((r) => ({
|
|
540
544
|
label: r.name.endsWith(suffix) ? r.name.slice(0, -suffix.length) : r.name,
|
|
541
545
|
value: r.value,
|
|
@@ -543,6 +547,20 @@ function parseRelayPayload(pubKeyZ32, payload) {
|
|
|
543
547
|
}))
|
|
544
548
|
};
|
|
545
549
|
}
|
|
550
|
+
/** Verifies and decodes a relay payload. Throws if the signature does not match. */
|
|
551
|
+
function parseRelayPayload(pubKeyZ32, payload) {
|
|
552
|
+
const { seq, dnsPacket } = openRelayPayload(pubKeyZ32, payload);
|
|
553
|
+
return decodeRelayPayload(pubKeyZ32, seq, dnsPacket);
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Decodes a payload whose signature this process made or already checked (one it is putting), without checking it
|
|
557
|
+
* again: an Ed25519 verify costs about 1 ms, and a CLI daemon at its relay budget checked its own packets ~500 times a
|
|
558
|
+
* minute. Never for bytes read from elsewhere: `parseRelayPayload` is.
|
|
559
|
+
*/
|
|
560
|
+
function readOwnRelayPayload(pubKeyZ32, payload) {
|
|
561
|
+
const { seq, dnsPacket } = splitRelayPayload(payload);
|
|
562
|
+
return decodeRelayPayload(pubKeyZ32, seq, dnsPacket);
|
|
563
|
+
}
|
|
546
564
|
/** How far ahead of this clock a packet's time may be and still count as now: clocks drift, not by this much. */
|
|
547
565
|
var PKARR_FUTURE_SKEW_MS = 6e5;
|
|
548
566
|
/**
|
|
@@ -700,9 +718,13 @@ var RelayBreaker = class {
|
|
|
700
718
|
this.circuits.delete(relay);
|
|
701
719
|
if (recovered) this.options.onRecover?.(relay);
|
|
702
720
|
}
|
|
703
|
-
/**
|
|
721
|
+
/**
|
|
722
|
+
* The relay failed a request; a probe that fails trips it again at once. While it is left alone and no probe is out,
|
|
723
|
+
* a failure is a request sent before it tripped (a start burst): already counted in, it trips nothing again.
|
|
724
|
+
*/
|
|
704
725
|
failure(relay, kind, reason) {
|
|
705
726
|
const circuit = this.circuit(relay);
|
|
727
|
+
if (!circuit.probing && circuit.openUntil > this.now()) return;
|
|
706
728
|
circuit.failures++;
|
|
707
729
|
circuit.kind = kind;
|
|
708
730
|
circuit.reason = reason;
|
|
@@ -832,6 +854,8 @@ function currentRelays(relays) {
|
|
|
832
854
|
* only thing that peer is waiting for) would go out only once the polling slowed down, half a minute later.
|
|
833
855
|
*/
|
|
834
856
|
var WRITE_FIRST_MS = 5e3;
|
|
857
|
+
/** The most of a relay's minute the chats' reserve takes, however many chats need it: the rest stays for everything else. */
|
|
858
|
+
var CHAT_RESERVE_MOST_SHARE = 5 / 6;
|
|
835
859
|
/**
|
|
836
860
|
* A 1:1 chat's signaling (a new offer or answer, and its reads for the answer to its offer, `signal`) may go over a
|
|
837
861
|
* relay's minute by this fraction of it: 6 on a relay of 30, 12 on one of 60, 1 on relay.pkarr.org's 5 (it allows 10),
|
|
@@ -1167,14 +1191,22 @@ var RelayTransport = class {
|
|
|
1167
1191
|
}
|
|
1168
1192
|
/**
|
|
1169
1193
|
* Signs the records, dated by `nextTimestamp`, and hands the payload to `put` (the relays; the CLI adds the DHT).
|
|
1194
|
+
* When every relay's budget (or rate limit) holds this asker back, it throws that wait before signing anything
|
|
1195
|
+
* (`publishWait`): a daemon at its budget signed about 500 packets a minute that never went out. `elsewhere`: `put`
|
|
1196
|
+
* also takes the packet where no budget holds it back (the CLI's DHT), so it is signed whatever the relays say.
|
|
1170
1197
|
* `put` calls `conflict` for a relay that answered 409: it holds a packet under this key dated at or after this one,
|
|
1171
1198
|
* which this client has not read (an inviter's warm packet under the key it gave its contact, from a clock ahead of
|
|
1172
1199
|
* this one). That packet is read from that relay, and the records go again dated just past it, once. Without this
|
|
1173
1200
|
* the packet was refused until this device's clock passed the other one's, however long that took, and on a relay
|
|
1174
1201
|
* that did take it, readers still preferred the later-dated empty packet of another relay.
|
|
1175
1202
|
*/
|
|
1176
|
-
async publishDated(identity, records, put, options = {}, again = false) {
|
|
1177
|
-
const key = identity.pubKeyZ32
|
|
1203
|
+
async publishDated(identity, records, put, options = {}, again = false, elsewhere = false) {
|
|
1204
|
+
const key = identity.pubKeyZ32;
|
|
1205
|
+
if (!again && !elsewhere) {
|
|
1206
|
+
const held = this.publishWait(key, asker(options, true));
|
|
1207
|
+
if (held) throw new DiscoveryBudgetError(held.wait, `Publish held back on every relay: ${held.reasons.join("; ")}`);
|
|
1208
|
+
}
|
|
1209
|
+
const timestamp = this.nextTimestamp(key);
|
|
1178
1210
|
let redated;
|
|
1179
1211
|
const conflict = (relay) => {
|
|
1180
1212
|
if (again || redated) return;
|
|
@@ -1186,7 +1218,7 @@ var RelayTransport = class {
|
|
|
1186
1218
|
relay
|
|
1187
1219
|
});
|
|
1188
1220
|
if (this.lastTimestamp.get(key) !== timestamp) return true;
|
|
1189
|
-
await this.publishDated(identity, records, put, options, true);
|
|
1221
|
+
await this.publishDated(identity, records, put, options, true, elsewhere);
|
|
1190
1222
|
return true;
|
|
1191
1223
|
})();
|
|
1192
1224
|
redated.catch(() => {});
|
|
@@ -1198,6 +1230,46 @@ var RelayTransport = class {
|
|
|
1198
1230
|
if (!pending || !await pending) throw error;
|
|
1199
1231
|
}
|
|
1200
1232
|
}
|
|
1233
|
+
/**
|
|
1234
|
+
* How long until some relay takes a publish of this asker, and why each holds it back; null when one would now. Walks the relays as `put` does,
|
|
1235
|
+
* without asking any: a relay the budget holds back gets its refused `take` (the asker's write then waits first in
|
|
1236
|
+
* line, as when `put` is refused), one rate limiting or throttling this address is a wait, one left alone for failing
|
|
1237
|
+
* is none. Null too when a relay fails another way (its network cooling down): the publish then goes, and fails, as
|
|
1238
|
+
* before, and when no relay is held back at all.
|
|
1239
|
+
*/
|
|
1240
|
+
publishWait(pubKeyZ32, who) {
|
|
1241
|
+
const probe = this.breaker.allDownProbe(this.relays);
|
|
1242
|
+
let wait = Infinity;
|
|
1243
|
+
const held = [], reasons = [];
|
|
1244
|
+
for (const relay of this.relays) {
|
|
1245
|
+
if (relay !== probe && this.networkCoolingDown(relay, "PUT")) return null;
|
|
1246
|
+
const limited = this.rateLimitedFor(relay);
|
|
1247
|
+
if (limited > 0) {
|
|
1248
|
+
wait = Math.min(wait, limited);
|
|
1249
|
+
reasons.push(`${relay}: Discovery relay is cooling down after a 429; retry shortly`);
|
|
1250
|
+
continue;
|
|
1251
|
+
}
|
|
1252
|
+
const blocked = relay === probe ? 0 : this.breaker.blockedFor(relay);
|
|
1253
|
+
if (blocked > 0) {
|
|
1254
|
+
if (this.breaker.blockedKind(relay) === "throttled") {
|
|
1255
|
+
wait = Math.min(wait, blocked);
|
|
1256
|
+
reasons.push(`${relay}: Discovery relay is throttling this address; retry shortly`);
|
|
1257
|
+
} else reasons.push(`${relay} is left alone after failing`);
|
|
1258
|
+
continue;
|
|
1259
|
+
}
|
|
1260
|
+
if (this.heldFor(relay, who, Date.now(), pubKeyZ32) <= 0) return null;
|
|
1261
|
+
held.push(relay);
|
|
1262
|
+
const left = this.heldFor(relay, who);
|
|
1263
|
+
wait = Math.min(wait, left);
|
|
1264
|
+
reasons.push(`${relay}: discovery budget reached; retry in ${Math.ceil(left / 1e3)} s`);
|
|
1265
|
+
}
|
|
1266
|
+
if (wait === Infinity) return null;
|
|
1267
|
+
for (const relay of held) this.take(relay, who, pubKeyZ32);
|
|
1268
|
+
return {
|
|
1269
|
+
wait,
|
|
1270
|
+
reasons
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1201
1273
|
/** One relay's packet for a key, read now: its time, kept for `nextTimestamp`. Null when it has none or does not answer. */
|
|
1202
1274
|
async heldAt(relay, pubKeyZ32, who) {
|
|
1203
1275
|
if (!this.take(relay, who, pubKeyZ32)) return null;
|
|
@@ -1217,10 +1289,22 @@ var RelayTransport = class {
|
|
|
1217
1289
|
sawTimestamp(pubKeyZ32, timestamp) {
|
|
1218
1290
|
if (timestamp > (this.seenTimestamp.get(pubKeyZ32) ?? 0n)) this.seenTimestamp.set(pubKeyZ32, timestamp);
|
|
1219
1291
|
}
|
|
1220
|
-
/**
|
|
1292
|
+
/**
|
|
1293
|
+
* Puts a payload `publishDated` just signed, dated `timestamp`, on every relay. Its signature is not checked: it was
|
|
1294
|
+
* made here (`publishPayload` checks one signed elsewhere, to read its sequence number). `conflict`: as `publishDated`
|
|
1295
|
+
* gives it.
|
|
1296
|
+
*/
|
|
1297
|
+
async putSigned(pubKeyZ32, payload, timestamp, options = {}, conflict) {
|
|
1298
|
+
if (this.relays.length === 0) throw new Error("No Pkarr relays configured");
|
|
1299
|
+
await this.putEverywhere(pubKeyZ32, payload, timestamp, options, conflict);
|
|
1300
|
+
}
|
|
1301
|
+
/**
|
|
1302
|
+
* Puts a payload signed elsewhere (a did:dht document), byte for byte, on every relay; one signed here says its date
|
|
1303
|
+
* (`signedAt`) and is not checked again. `conflict`: as `publishDated` gives it.
|
|
1304
|
+
*/
|
|
1221
1305
|
async publishPayload(pubKeyZ32, payload, options = {}, conflict) {
|
|
1222
1306
|
if (this.relays.length === 0) throw new Error("No Pkarr relays configured");
|
|
1223
|
-
const
|
|
1307
|
+
const seq = options.signedAt ?? openRelayPayload(pubKeyZ32, payload).seq;
|
|
1224
1308
|
await this.putEverywhere(pubKeyZ32, payload, seq, options, conflict);
|
|
1225
1309
|
}
|
|
1226
1310
|
/**
|
|
@@ -1261,7 +1345,7 @@ var RelayTransport = class {
|
|
|
1261
1345
|
accepted = true;
|
|
1262
1346
|
noWriteWaits();
|
|
1263
1347
|
try {
|
|
1264
|
-
this.written.set(pubKeyZ32,
|
|
1348
|
+
this.written.set(pubKeyZ32, readOwnRelayPayload(pubKeyZ32, payload));
|
|
1265
1349
|
} catch {}
|
|
1266
1350
|
resolve();
|
|
1267
1351
|
}, () => {});
|
|
@@ -1723,9 +1807,9 @@ var RelayTransport = class {
|
|
|
1723
1807
|
if (rationed.length >= most) wait = Math.max(wait, rationed[rationed.length - most] + GROUP_RATION_WINDOW_MS - now);
|
|
1724
1808
|
if (this.readsAhead(key, now) > 0) wait = Math.max(wait, TURN_WAIT_MS);
|
|
1725
1809
|
}
|
|
1726
|
-
if (who.group) {
|
|
1810
|
+
if (who.group || who.background) {
|
|
1727
1811
|
const reserved = this.chatReservedFor(relay, now);
|
|
1728
|
-
if (reserved > 0) wait = Math.max(wait, Math.min(reserved, over(recent, limit - this.
|
|
1812
|
+
if (reserved > 0) wait = Math.max(wait, Math.min(reserved, over(recent, limit - this.chatReserveOf(relay, now))));
|
|
1729
1813
|
}
|
|
1730
1814
|
const writer = firstWriter(who);
|
|
1731
1815
|
const waiting = (lane) => {
|
|
@@ -1835,6 +1919,12 @@ var RelayTransport = class {
|
|
|
1835
1919
|
if (share === void 0) return this.perMinute;
|
|
1836
1920
|
return this.perMinuteGiven ? Math.min(this.perMinute, share) : share;
|
|
1837
1921
|
}
|
|
1922
|
+
/** What the chats that need the relays keep on this relay (`CHAT_RESERVE_PER_CHAT`), never less than `reserveOf`. */
|
|
1923
|
+
chatReserveOf(relay, now) {
|
|
1924
|
+
let needing = 0;
|
|
1925
|
+
for (const need of this.chatNeeds.values()) if (now - Math.max(need.urgentAt, need.refused.get(relay) ?? -Infinity) < 6e4) needing++;
|
|
1926
|
+
return Math.max(this.reserveOf(relay), Math.min(needing * 3, Math.floor(this.limitOf(relay) * CHAT_RESERVE_MOST_SHARE)));
|
|
1927
|
+
}
|
|
1838
1928
|
/** What groups leave a chat on this relay: `CHAT_RESERVE`, or a third of a smaller budget. */
|
|
1839
1929
|
reserveOf(relay) {
|
|
1840
1930
|
return Math.min(10, Math.floor(this.limitOf(relay) / 3));
|
|
@@ -3060,6 +3150,12 @@ var OwnInviteError = class extends Error {
|
|
|
3060
3150
|
this.name = "OwnInviteError";
|
|
3061
3151
|
}
|
|
3062
3152
|
};
|
|
3153
|
+
/** The refusal an error (or an error's message, as an RPC hands it over) carries, or null when it is another error. */
|
|
3154
|
+
function ownInviteRefusal(error) {
|
|
3155
|
+
if (error instanceof OwnInviteError) return { linkId: error.linkId };
|
|
3156
|
+
const match = (error instanceof Error ? error.message : typeof error === "string" ? error : "").match(/^own-invite(?: (\S+))?: /);
|
|
3157
|
+
return match ? { linkId: match[1] ?? null } : null;
|
|
3158
|
+
}
|
|
3063
3159
|
/** The `ghostly1z…` code of a device invite. */
|
|
3064
3160
|
function encodeDeviceInvite(invite) {
|
|
3065
3161
|
const fields = [
|
|
@@ -5092,6 +5188,7 @@ function wrapDataChannel(dc) {
|
|
|
5092
5188
|
close() {
|
|
5093
5189
|
dc.close();
|
|
5094
5190
|
},
|
|
5191
|
+
binary: true,
|
|
5095
5192
|
onMessage: null,
|
|
5096
5193
|
onClose: null
|
|
5097
5194
|
};
|
|
@@ -5103,11 +5200,31 @@ function wrapDataChannel(dc) {
|
|
|
5103
5200
|
dc.addEventListener("close", () => channel.onClose?.());
|
|
5104
5201
|
return channel;
|
|
5105
5202
|
}
|
|
5203
|
+
/**
|
|
5204
|
+
* Resolves once the channel has room for a body's next chunk; undefined when it has now. A channel with a send budget
|
|
5205
|
+
* (native and iroh ones throw past 120 KiB) gives a body a quarter of it, as files/3 keeps to, so a ping or a message
|
|
5206
|
+
* still fits beside it; others keep under the high water mark.
|
|
5207
|
+
*/
|
|
5208
|
+
function bodyRoom(channel, isCancelled) {
|
|
5209
|
+
if (!channel.sendBudget) return channel.bufferedAmount > LIMITS.sendHighWaterMark ? channel.drained() : void 0;
|
|
5210
|
+
const line = channel.sendBudget / 4;
|
|
5211
|
+
if (channel.bufferedAmount < line) return;
|
|
5212
|
+
return (async () => {
|
|
5213
|
+
while (!isCancelled() && channel.bufferedAmount >= line) {
|
|
5214
|
+
await channel.drained().catch(() => {});
|
|
5215
|
+
if (!isCancelled() && channel.bufferedAmount >= line) await new Promise((resolve) => setTimeout(resolve, 20));
|
|
5216
|
+
}
|
|
5217
|
+
})();
|
|
5218
|
+
}
|
|
5106
5219
|
/** Writes a body as chunks, honouring backpressure. Always ends with an END chunk. */
|
|
5107
5220
|
async function sendBody(channel, kind, id, body, isCancelled = () => false) {
|
|
5108
5221
|
for await (const part of body) for (let offset = 0; offset < part.length; offset += MAX_CHUNK_PAYLOAD_BYTES) {
|
|
5109
5222
|
if (isCancelled()) return;
|
|
5110
|
-
|
|
5223
|
+
const room = bodyRoom(channel, isCancelled);
|
|
5224
|
+
if (room) {
|
|
5225
|
+
await room;
|
|
5226
|
+
if (isCancelled()) return;
|
|
5227
|
+
}
|
|
5111
5228
|
const payload = part.subarray(offset, offset + MAX_CHUNK_PAYLOAD_BYTES);
|
|
5112
5229
|
channel.send(encodeChunk({
|
|
5113
5230
|
kind,
|
|
@@ -5701,6 +5818,19 @@ var STALLED_LOOK_MS = 15e3;
|
|
|
5701
5818
|
* #1441, on a loaded host), and past it the race's DHT dial has brought the chat back anyway.
|
|
5702
5819
|
*/
|
|
5703
5820
|
var DEPARTED_READ_MS = EXPECT_PEER_MS;
|
|
5821
|
+
/** A chat whose contact was never seen (an invite just sent) keeps looking at the active pace this long. */
|
|
5822
|
+
var AWAITING_PEER_MS = 6e5;
|
|
5823
|
+
/**
|
|
5824
|
+
* An invite this side made that nobody used, once no longer awaited (`AWAITING_PEER_MS`), is read less often the older
|
|
5825
|
+
* it is: a fifth of its age between reads, up to this. Read at the background pace instead, 15 unused invites (their
|
|
5826
|
+
* keys and their mailboxes) asked the relays for 60 reads a minute for as long as the app ran: the relays' minute went
|
|
5827
|
+
* to invites nobody may ever open, and a join on one was noticed 90 s later (bug hunt 1013-1642, 2026-10-13).
|
|
5828
|
+
*/
|
|
5829
|
+
var UNUSED_INVITE_WAIT_MAX_MS = 18e4;
|
|
5830
|
+
/** The wait between reads of an unused invite of age `age` (`UNUSED_INVITE_WAIT_MAX_MS`), never shorter than the pace's `interval`. */
|
|
5831
|
+
function unusedInviteWait(age, interval) {
|
|
5832
|
+
return Math.max(interval, Math.min(UNUSED_INVITE_WAIT_MAX_MS, Math.round(age / 5)));
|
|
5833
|
+
}
|
|
5704
5834
|
var PUBLISH_RETRY_MS = 4e3;
|
|
5705
5835
|
/**
|
|
5706
5836
|
* A packet the relays' request budget held back goes again when the budget frees a request, and at least this often
|
|
@@ -5756,6 +5886,8 @@ var LinkSession = class {
|
|
|
5756
5886
|
startedAt = Date.now();
|
|
5757
5887
|
/** 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
5888
|
awaitingSince;
|
|
5889
|
+
/** This side's invite, nobody used yet (`LinkSessionOptions.invite`). */
|
|
5890
|
+
invite;
|
|
5759
5891
|
intervals;
|
|
5760
5892
|
fastPollUntil = 0;
|
|
5761
5893
|
/** An offer to a saved contact is out: from then on its fast window looks less often (`OFFER_FAST_MS`); 0 for none. */
|
|
@@ -5811,6 +5943,7 @@ var LinkSession = class {
|
|
|
5811
5943
|
this.events = options.events ?? {};
|
|
5812
5944
|
this.firstPublish = options.firstPublish ?? "at-start";
|
|
5813
5945
|
this.awaitingSince = Math.min(options.awaitingSince ?? this.startedAt, this.startedAt);
|
|
5946
|
+
this.invite = options.invite ?? (() => false);
|
|
5814
5947
|
}
|
|
5815
5948
|
get peerPresence() {
|
|
5816
5949
|
return this.presence;
|
|
@@ -5984,8 +6117,8 @@ var LinkSession = class {
|
|
|
5984
6117
|
async setRtcSignal(signal, reportFailure = false) {
|
|
5985
6118
|
if (this.rtcSignal === signal) return;
|
|
5986
6119
|
this.rtcSignal = signal;
|
|
5987
|
-
if (reportFailure) await this.publish();
|
|
5988
|
-
else await this.publish().catch(() => {});
|
|
6120
|
+
if (reportFailure) await this.publish(signal === null);
|
|
6121
|
+
else await this.publish(signal === null).catch(() => {});
|
|
5989
6122
|
if (signal) this.pollNow();
|
|
5990
6123
|
}
|
|
5991
6124
|
/**
|
|
@@ -6058,6 +6191,7 @@ var LinkSession = class {
|
|
|
6058
6191
|
const stepping = this.fastStepsAfter ? Date.now() - this.fastStepsAfter : -1;
|
|
6059
6192
|
if (pace === "fast" && stepping >= 0) return Math.min(4 * interval, Math.max(2 * interval, stepping));
|
|
6060
6193
|
if (pace === "fast" || pace === "connected" || since < 0) return interval;
|
|
6194
|
+
if (pace === "background" && !this.presence.online && this.invite()) return unusedInviteWait(Date.now() - this.awaitingSince, interval);
|
|
6061
6195
|
return Math.min(interval, Math.max(2 * this.intervals.fast, since));
|
|
6062
6196
|
}
|
|
6063
6197
|
/**
|
|
@@ -6069,13 +6203,17 @@ var LinkSession = class {
|
|
|
6069
6203
|
watching() {
|
|
6070
6204
|
return Date.now() < this.watchUntil && this.rtcSignal === null && !(this.presence.online && this.presence.lastPacketAt > this.watchFrom);
|
|
6071
6205
|
}
|
|
6206
|
+
/** A contact never seen, whose invite was sent lately (`AWAITING_PEER_MS`): its join is due. */
|
|
6207
|
+
awaitingPeer() {
|
|
6208
|
+
return this.presence.lastPacketAt === 0 && Date.now() - this.awaitingSince < 6e5;
|
|
6209
|
+
}
|
|
6072
6210
|
/** How urgently this link looks right now. */
|
|
6073
6211
|
pace() {
|
|
6074
6212
|
if (Date.now() < this.stalledUntil) return "fast";
|
|
6075
6213
|
if (this.connected) return "connected";
|
|
6076
6214
|
if (Date.now() < this.fastPollUntil) return "fast";
|
|
6077
6215
|
if (Date.now() < this.watchUntil) return "active";
|
|
6078
|
-
if (!this.active && this.
|
|
6216
|
+
if (!this.active && this.awaitingPeer()) return "active";
|
|
6079
6217
|
if (!this.active) return "background";
|
|
6080
6218
|
return Date.now() - this.lastActivity > 6e4 ? "idle" : "active";
|
|
6081
6219
|
}
|
|
@@ -6132,7 +6270,9 @@ var LinkSession = class {
|
|
|
6132
6270
|
}, this.encKey);
|
|
6133
6271
|
const started = Date.now();
|
|
6134
6272
|
try {
|
|
6135
|
-
const
|
|
6273
|
+
const awaited = this.connected || this.presence.online || this.sentBuffer.length > 0 || this.callSignal !== null || this.awaitingPeer();
|
|
6274
|
+
const takesBack = !rtcSignal && this.rtcSignalOut !== null;
|
|
6275
|
+
const options = rtcSignal && rtcSignal !== this.rtcSignalOut ? { signal: true } : this.watching() ? { watch: true } : upkeep && (this.connected || takesBack) || !rtcSignal && !awaited ? { background: true } : void 0;
|
|
6136
6276
|
await this.transport.publish(this.identity, built.records, options);
|
|
6137
6277
|
} catch (error) {
|
|
6138
6278
|
const ms = Date.now() - started, waiting = isDiscoveryBudgetError(error);
|
|
@@ -6437,7 +6577,9 @@ var ICE_GATHERING_TIMEOUT_MS = 5e3;
|
|
|
6437
6577
|
* settle); where they do not, every offer of a first pairing waited the full 5 s, twice when the inviter offered too
|
|
6438
6578
|
* (One's e2e runs: 13 offers of 89 at 5 s, 9 more at 1.3-3 s, 2026-10-05). An offer without one still connects to most
|
|
6439
6579
|
* contacts (their checks learn this side's address from its own); behind a restricted NAT on the contact's side it may
|
|
6440
|
-
* not, and the next offer of the chat then waits as long as it takes (`fullGather`).
|
|
6580
|
+
* not, and the next offer of the chat then waits as long as it takes (`fullGather`). An answer is bounded the same way:
|
|
6581
|
+
* it waited the full 5 s, and two CLI daemons whose STUN server swallowed every request went live 10 s after the offer
|
|
6582
|
+
* rather than 2 s (calls bug hunt, 2026-10-12).
|
|
6441
6583
|
*/
|
|
6442
6584
|
var OFFER_HOST_GATHER_MS = 2e3;
|
|
6443
6585
|
/**
|
|
@@ -6447,6 +6589,14 @@ var OFFER_HOST_GATHER_MS = 2e3;
|
|
|
6447
6589
|
var GATHER_STALL_MS = 3e3;
|
|
6448
6590
|
/** How long a connection may stay `disconnected` before it is given up. */
|
|
6449
6591
|
var DISCONNECT_GRACE_MS = 12e3;
|
|
6592
|
+
/**
|
|
6593
|
+
* The same race a moment later: setRemoteDescription resolves, and the connection closes right after it (8 ms on a
|
|
6594
|
+
* loaded core). A connection that ends within this long of its answer going in was refused too: no network fails one
|
|
6595
|
+
* that soon (ICE gives up after tens of seconds), so it says nothing about direct connections (`DirectEvidence`).
|
|
6596
|
+
* Taken for a network that lets none through, an edge between two CLIs left WebRTC for the run, and a member whose
|
|
6597
|
+
* app has WebRTC alone (one from before native group links) was never reached again (groupCompat, 2026-10-12).
|
|
6598
|
+
*/
|
|
6599
|
+
var ANSWER_REFUSED_AFTER_MS = 1e3;
|
|
6450
6600
|
/** An offer this close to the end of the offerer's attempt is not answered again: the new answer would come too late. */
|
|
6451
6601
|
var REANSWER_MARGIN_MS = 15e3;
|
|
6452
6602
|
/**
|
|
@@ -6478,12 +6628,13 @@ var DataLink = class {
|
|
|
6478
6628
|
pc = null;
|
|
6479
6629
|
myOfferTs = 0;
|
|
6480
6630
|
/**
|
|
6481
|
-
* An offer of this side's that went without a public candidate came to nothing (
|
|
6482
|
-
* connected): the next one waits for its STUN servers as long as it takes
|
|
6631
|
+
* An offer or answer of this side's that went without a public candidate came to nothing (both descriptions were
|
|
6632
|
+
* exchanged, and no path connected): the next one waits for its STUN servers as long as it takes
|
|
6633
|
+
* (`ICE_GATHERING_TIMEOUT_MS`).
|
|
6483
6634
|
*/
|
|
6484
6635
|
fullGather = false;
|
|
6485
|
-
/** The offer out has no public candidate (as it went: the connection's description gains any that come later). */
|
|
6486
|
-
|
|
6636
|
+
/** The offer or answer out has no public candidate (as it went: the connection's description gains any that come later). */
|
|
6637
|
+
sentLacksPublic = false;
|
|
6487
6638
|
lastSignalTs = 0;
|
|
6488
6639
|
/** The peer's description as this connection was given it: Chrome shows it as `remoteDescription` only once applied. */
|
|
6489
6640
|
remoteSdp = null;
|
|
@@ -6529,7 +6680,7 @@ var DataLink = class {
|
|
|
6529
6680
|
this.options.setFastPoll(true, true);
|
|
6530
6681
|
}
|
|
6531
6682
|
this.myOfferTs = Date.now();
|
|
6532
|
-
this.
|
|
6683
|
+
this.sentLacksPublic = !PUBLIC_CANDIDATE.test(pc.localDescription?.sdp ?? "");
|
|
6533
6684
|
const signal = {
|
|
6534
6685
|
t: "o",
|
|
6535
6686
|
ts: this.myOfferTs,
|
|
@@ -6635,9 +6786,12 @@ var DataLink = class {
|
|
|
6635
6786
|
this.reset();
|
|
6636
6787
|
if (evidence) this.options.onDirect?.(evidence);
|
|
6637
6788
|
}
|
|
6638
|
-
/**
|
|
6789
|
+
/**
|
|
6790
|
+
* This side's offer went with no public candidate and was answered, or its answer went with none, and it came to
|
|
6791
|
+
* nothing: the next offer or answer gathers for as long as it takes.
|
|
6792
|
+
*/
|
|
6639
6793
|
noteNoPublic(ended) {
|
|
6640
|
-
if (ended && this.myOfferTs && this.
|
|
6794
|
+
if (ended && (this.myOfferTs || this.answered) && this.sentLacksPublic && this.state === "connecting") this.fullGather = true;
|
|
6641
6795
|
}
|
|
6642
6796
|
/**
|
|
6643
6797
|
* What this attempt says about direct connections if it ends now without opening: nothing unless both descriptions
|
|
@@ -6652,17 +6806,22 @@ var DataLink = class {
|
|
|
6652
6806
|
/**
|
|
6653
6807
|
* The attempt on this connection failed (it timed out, ICE failed or closed, its channel closed). An answer the
|
|
6654
6808
|
* offerer may not have read yet is made again for the same offer while it still stands (`REANSWERS`); anything
|
|
6655
|
-
* else goes back to idle.
|
|
6809
|
+
* else goes back to idle. `byConnection`: the connection said so itself, rather than a timer of this link's.
|
|
6656
6810
|
*/
|
|
6657
|
-
failed() {
|
|
6811
|
+
failed(byConnection = false) {
|
|
6658
6812
|
if (this.state === "offering" && this.remoteSdp) {
|
|
6659
6813
|
this.answerRefused("failed");
|
|
6660
6814
|
return;
|
|
6661
6815
|
}
|
|
6816
|
+
if (byConnection && this.state === "connecting" && this.myOfferTs && Date.now() - this.connectingSince < ANSWER_REFUSED_AFTER_MS) {
|
|
6817
|
+
this.answerRefused("failed");
|
|
6818
|
+
return;
|
|
6819
|
+
}
|
|
6662
6820
|
const answered = this.state === "connecting" ? this.answered : null;
|
|
6663
6821
|
const left = answered ? CONNECT_TIMEOUT_MS - (Date.now() - answered.at) : 0;
|
|
6664
|
-
const
|
|
6665
|
-
this.
|
|
6822
|
+
const givenUp = !!answered && this.options.offerStanding?.(answered.offer.ts) === false;
|
|
6823
|
+
const evidence = givenUp ? null : this.directEvidence();
|
|
6824
|
+
this.noteNoPublic(!givenUp);
|
|
6666
6825
|
if (answered && answered.again < 2 && left > REANSWER_MARGIN_MS && this.options.offerStanding?.(answered.offer.ts)) {
|
|
6667
6826
|
answered.again++;
|
|
6668
6827
|
traceLink(this.options.myPubKeyZ32, "reanswer", {
|
|
@@ -6703,6 +6862,7 @@ var DataLink = class {
|
|
|
6703
6862
|
await pc.setLocalDescription(await pc.createAnswer());
|
|
6704
6863
|
});
|
|
6705
6864
|
if (!pc) return;
|
|
6865
|
+
this.sentLacksPublic = !PUBLIC_CANDIDATE.test(pc.localDescription?.sdp ?? "");
|
|
6706
6866
|
const signal = {
|
|
6707
6867
|
t: "a",
|
|
6708
6868
|
ts: Date.now(),
|
|
@@ -6732,7 +6892,7 @@ var DataLink = class {
|
|
|
6732
6892
|
await waitForIceGathering(pc, ICE_GATHERING_TIMEOUT_MS, {
|
|
6733
6893
|
stallMs: GATHER_STALL_MS,
|
|
6734
6894
|
settleMs: 150,
|
|
6735
|
-
|
|
6895
|
+
...!this.fullGather ? { hostBoundMs: OFFER_HOST_GATHER_MS } : {}
|
|
6736
6896
|
});
|
|
6737
6897
|
if (this.pc !== pc) return null;
|
|
6738
6898
|
const ms = Date.now() - gathering;
|
|
@@ -6779,7 +6939,7 @@ var DataLink = class {
|
|
|
6779
6939
|
tracePair(this.options.myPubKeyZ32, pc);
|
|
6780
6940
|
});
|
|
6781
6941
|
dc.addEventListener("close", () => {
|
|
6782
|
-
if (this.pc === pc) this.failed();
|
|
6942
|
+
if (this.pc === pc) this.failed(true);
|
|
6783
6943
|
});
|
|
6784
6944
|
pc.addEventListener("connectionstatechange", () => {
|
|
6785
6945
|
if (this.pc !== pc) return;
|
|
@@ -6788,7 +6948,7 @@ var DataLink = class {
|
|
|
6788
6948
|
if (pc.connectionState === "connected") this.options.onDisconnected?.(false);
|
|
6789
6949
|
}
|
|
6790
6950
|
this.disconnectTimer = null;
|
|
6791
|
-
if (pc.connectionState === "failed" || pc.connectionState === "closed") this.failed();
|
|
6951
|
+
if (pc.connectionState === "failed" || pc.connectionState === "closed") this.failed(true);
|
|
6792
6952
|
else if (pc.connectionState === "disconnected") {
|
|
6793
6953
|
if (this.state === "open") this.options.onDisconnected?.(true);
|
|
6794
6954
|
this.disconnectTimer = setTimeout(() => {
|
|
@@ -8705,11 +8865,6 @@ var VIDEO_LIMITS = {
|
|
|
8705
8865
|
/** Pixels on either side (8K and a bit). */
|
|
8706
8866
|
maxEdge: 16384
|
|
8707
8867
|
};
|
|
8708
|
-
/**
|
|
8709
|
-
* Video a player may be handed as it is. None of these runs anything in the page's origin, so received bytes
|
|
8710
|
-
* of these types are served with their type (see `safeBlobType`): WebKit will not play what is typed as bytes.
|
|
8711
|
-
*/
|
|
8712
|
-
var PLAYABLE_VIDEO = /^video\/(mp4|webm|quicktime|ogg|x-m4v)$/;
|
|
8713
8868
|
/** Whether a file is a video, by its type: shown in a video bubble whether or not it came with a description. */
|
|
8714
8869
|
function isVideoMime(mime) {
|
|
8715
8870
|
return mime.split(";")[0].trim().toLowerCase().startsWith("video/");
|
|
@@ -8903,18 +9058,6 @@ function formatFileSize(bytes) {
|
|
|
8903
9058
|
}
|
|
8904
9059
|
return `${value.toFixed(1)} ${units[unit]}`;
|
|
8905
9060
|
}
|
|
8906
|
-
/** Raster images a UI may show inline. SVG is left out on purpose: it can carry scripts. */
|
|
8907
|
-
var PREVIEWABLE_IMAGE = /^image\/(png|jpe?g|gif|webp)$/;
|
|
8908
|
-
/**
|
|
8909
|
-
* The type received bytes are served with. The peer picks the announced type,
|
|
8910
|
-
* and a blob: URL typed text/html or image/svg+xml would run in the app's
|
|
8911
|
-
* origin, so anything but a previewable image or playable audio or video is opaque bytes.
|
|
8912
|
-
* Audio and video keep their type because WebKit will not play what is typed as bytes.
|
|
8913
|
-
*/
|
|
8914
|
-
function safeBlobType(mime) {
|
|
8915
|
-
const clean = sanitizeMime(mime);
|
|
8916
|
-
return PREVIEWABLE_IMAGE.test(clean) || PLAYABLE_AUDIO.test(clean) || PLAYABLE_VIDEO.test(clean) ? clean : "application/octet-stream";
|
|
8917
|
-
}
|
|
8918
9061
|
var FileTransfers = class {
|
|
8919
9062
|
channel;
|
|
8920
9063
|
events;
|
|
@@ -9355,6 +9498,7 @@ var DhtDelivery = class DhtDelivery {
|
|
|
9355
9498
|
fastUntil = 0;
|
|
9356
9499
|
live = false;
|
|
9357
9500
|
active = false;
|
|
9501
|
+
startedAt = Date.now();
|
|
9358
9502
|
/** The next read was asked for (a refresh, a fresh packet of the contact): it is not a background one. */
|
|
9359
9503
|
urgent = false;
|
|
9360
9504
|
/** Reads left that are signaling: the contact is leaving DHT only, and the live link waits on them (`expect`). */
|
|
@@ -9426,6 +9570,14 @@ var DhtDelivery = class DhtDelivery {
|
|
|
9426
9570
|
};
|
|
9427
9571
|
}
|
|
9428
9572
|
/**
|
|
9573
|
+
* This side joined, and its inviter never answered (`DhtDeliveryState.unanswered`): no envelope of its newer than the
|
|
9574
|
+
* one that pinned it, none sealed to this side, no text confirmed, never live (as `sealedToAnother` asks too).
|
|
9575
|
+
*/
|
|
9576
|
+
get unanswered() {
|
|
9577
|
+
const pinnedAt = this.state.unanswered;
|
|
9578
|
+
return pinnedAt !== void 0 && this.state.peerSequence <= pinnedAt && !this.state.peerPinned && !this.state.confirmed && !this.state.wasLive && !this.live;
|
|
9579
|
+
}
|
|
9580
|
+
/**
|
|
9429
9581
|
* Something signed by a participation key other than the pinned one came in over the chat's invite-derived keys (this
|
|
9430
9582
|
* mailbox, the link's signals, a connection to an endpoint whose address a copy of the invite could read): ignored,
|
|
9431
9583
|
* and said in the view as a passive warning.
|
|
@@ -10134,6 +10286,7 @@ var DhtDelivery = class DhtDelivery {
|
|
|
10134
10286
|
}
|
|
10135
10287
|
if (message !== null && (!Array.isArray(message) || message.length !== 3 || typeof message[0] !== "string" || !ID$2.test(message[0]) || !Number.isSafeInteger(message[1]) || message[1] <= 0 || message[1] > issued + 3e4 || typeof message[2] !== "string" || utf8Encode(message[2]).length > 256 || expires - issued > 3e5)) return "none";
|
|
10136
10288
|
if (receipt !== null && (typeof receipt !== "string" || !ID$2.test(receipt))) return "none";
|
|
10289
|
+
const unanswered = !this.options.credentials.peerKey && !!this.options.credentials.expectedPeerKey && !sender && !this.live;
|
|
10137
10290
|
await this.options.pin(author);
|
|
10138
10291
|
this.options.credentials.peerKey = author;
|
|
10139
10292
|
this.options.credentials.requireSignedSignals = true;
|
|
@@ -10184,6 +10337,7 @@ var DhtDelivery = class DhtDelivery {
|
|
|
10184
10337
|
receipt: nextReceipt,
|
|
10185
10338
|
confirmed,
|
|
10186
10339
|
...peerPinned && { peerPinned },
|
|
10340
|
+
...unanswered && { unanswered: sequence },
|
|
10187
10341
|
pending: confirmed && this.state.pending?.message[0] === confirmed ? void 0 : this.state.pending,
|
|
10188
10342
|
...newest > (this.state.reactionsTaken ?? 0) && { reactionsTaken: newest }
|
|
10189
10343
|
});
|
|
@@ -10298,11 +10452,23 @@ var DhtDelivery = class DhtDelivery {
|
|
|
10298
10452
|
get pollMs() {
|
|
10299
10453
|
if (this.options.pollMs) return this.options.pollMs;
|
|
10300
10454
|
const now = Date.now();
|
|
10455
|
+
const unused = this.unusedInviteAge(now);
|
|
10456
|
+
if (unused !== void 0 && !this.active && now >= this.fastUntil) return unusedInviteWait(unused, this.mode === "dht" ? DHT_POLL_MS : STREAM_POLL_MS);
|
|
10301
10457
|
if (this.mode === "dht" || this.state.peerMode === "dht" && now < this.leavingUntil || now < this.fastUntil) return DHT_POLL_MS;
|
|
10302
10458
|
if (this.live) return LIVE_POLL_MS;
|
|
10303
10459
|
if (this.state.pending && this.state.pending.expires > now) return DHT_POLL_MS;
|
|
10304
10460
|
return this.active ? ACTIVE_DHT_POLL_MS : STREAM_POLL_MS;
|
|
10305
10461
|
}
|
|
10462
|
+
/**
|
|
10463
|
+
* How long ago this side's invite was made (or joined), while nobody used it (or its inviter never answered,
|
|
10464
|
+
* `unanswered`) and it is no longer awaited (`AWAITING_PEER_MS`).
|
|
10465
|
+
*/
|
|
10466
|
+
unusedInviteAge(now) {
|
|
10467
|
+
const madeAt = this.options.inviteMadeAt;
|
|
10468
|
+
if (madeAt === void 0 || (this.options.credentials.peerKey || this.state.peerSequence) && !this.unanswered) return void 0;
|
|
10469
|
+
const age = now - Math.min(madeAt, this.startedAt);
|
|
10470
|
+
return age >= 6e5 ? age : void 0;
|
|
10471
|
+
}
|
|
10306
10472
|
schedule() {
|
|
10307
10473
|
if (!this.running || this.ticking) return;
|
|
10308
10474
|
if (this.timer) clearTimeout(this.timer);
|
|
@@ -13431,6 +13597,8 @@ function readManifest(meta, loopback = false) {
|
|
|
13431
13597
|
*/
|
|
13432
13598
|
var OFFER_EXTENSIONS = ["ping/1"];
|
|
13433
13599
|
var MAX_HANDSHAKE_BYTES = 4096;
|
|
13600
|
+
/** One frame from the peer, at most (UTF-16 units for text, bytes for binary): a bigger one breaks the protocol. */
|
|
13601
|
+
var SESSION_FRAME_MAX = 61440;
|
|
13434
13602
|
/**
|
|
13435
13603
|
* Frames received and not handled yet, at most: their count and their size together. Frames are handled one at a time,
|
|
13436
13604
|
* each awaited (a group frame is decrypted, checked and stored before the next), and the peer sends at the network's
|
|
@@ -13441,7 +13609,7 @@ var MAX_HANDSHAKE_BYTES = 4096;
|
|
|
13441
13609
|
*/
|
|
13442
13610
|
var SESSION_RECEIVE_PENDING = {
|
|
13443
13611
|
frames: 1024,
|
|
13444
|
-
bytes:
|
|
13612
|
+
bytes: 64 * SESSION_FRAME_MAX
|
|
13445
13613
|
};
|
|
13446
13614
|
var KEY = /^[ybndrfg8ejkmcpqxot1uwisza345h769]{52}$/;
|
|
13447
13615
|
var NONCE$1 = /^[A-Za-z0-9_-]{43}$/;
|
|
@@ -13792,6 +13960,9 @@ var PairedHttp = class {
|
|
|
13792
13960
|
return channel.bufferedAmount;
|
|
13793
13961
|
},
|
|
13794
13962
|
drained: () => channel.drained(),
|
|
13963
|
+
get sendBudget() {
|
|
13964
|
+
return channel.sendBudget;
|
|
13965
|
+
},
|
|
13795
13966
|
close: () => {},
|
|
13796
13967
|
onMessage: null,
|
|
13797
13968
|
onClose: null
|
|
@@ -13836,6 +14007,11 @@ var SERVICES_CAPABILITY = "services/1";
|
|
|
13836
14007
|
* confirmed byte, checked by digest (`pf-offer` … frames, `chatFiles.ts`). Without it, files/2 and 100 MiB.
|
|
13837
14008
|
*/
|
|
13838
14009
|
var FILES_CAPABILITY = "files/3";
|
|
14010
|
+
/**
|
|
14011
|
+
* A file's bytes as binary messages (WISP 501 § files/4): beside `files/3`, on a session whose channel carries binary
|
|
14012
|
+
* messages, a chunk travels as it is (`encodeFileData`, `chatFiles.ts`) and not as base64url text in a `pf-data` frame.
|
|
14013
|
+
*/
|
|
14014
|
+
var FILES_BINARY_CAPABILITY = "files/4";
|
|
13839
14015
|
/** The typing indicator: `paired-typing` frames on this session, 1:1 chats only (WISP 401 § Typing, `pairedTyping.ts`). */
|
|
13840
14016
|
var TYPING_CAPABILITY = "typing/1";
|
|
13841
14017
|
/** Reactions to messages: `paired-reaction` frames on this session, 1:1 chats only (WISP 401 § Reactions, `reactions.ts`). */
|
|
@@ -13875,6 +14051,7 @@ var KNOWN_SESSION_CAPABILITIES = [
|
|
|
13875
14051
|
CALLS_CAPABILITY,
|
|
13876
14052
|
SERVICES_CAPABILITY,
|
|
13877
14053
|
FILES_CAPABILITY,
|
|
14054
|
+
FILES_BINARY_CAPABILITY,
|
|
13878
14055
|
TYPING_CAPABILITY,
|
|
13879
14056
|
REACTIONS_CAPABILITY,
|
|
13880
14057
|
EDIT_CAPABILITY,
|
|
@@ -13971,6 +14148,8 @@ var SessionCapabilities = class {
|
|
|
13971
14148
|
* be matched across a person's chats; it travels only inside the encrypted session.
|
|
13972
14149
|
*/
|
|
13973
14150
|
var APP_FRAME = "paired-app";
|
|
14151
|
+
/** The largest `d`, as compact UTF-8 JSON. The session's 60 KiB cap on any frame still holds above it. */
|
|
14152
|
+
var APP_DATA_MAX_BYTES = 32768;
|
|
13974
14153
|
var DOMAIN = "ghostly-apps/1";
|
|
13975
14154
|
var APP_ID = /^[A-Za-z0-9_-]{22}$/;
|
|
13976
14155
|
/** Semantic versioning 2.0.0, as a manifest's `version` is. */
|
|
@@ -14810,6 +14989,11 @@ var FILE_LIMITS = {
|
|
|
14810
14989
|
chunkBytes: 16384,
|
|
14811
14990
|
/** Bytes a sender has sent and not seen confirmed: throughput is this per round trip. */
|
|
14812
14991
|
windowBytes: 1048576,
|
|
14992
|
+
/**
|
|
14993
|
+
* Bytes all of a chat's files have sent and not seen confirmed, together. Three files' windows at once were 3 MiB, and
|
|
14994
|
+
* as base64 text past what a session's receive queue holds (`SESSION_RECEIVE_PENDING`, 3.75 MiB).
|
|
14995
|
+
*/
|
|
14996
|
+
chatWindowBytes: 2097152,
|
|
14813
14997
|
/** A receiver asks its person before taking a file larger than this. */
|
|
14814
14998
|
askAboveBytes: 26214400,
|
|
14815
14999
|
/** Files taken without asking, per contact, while stored. Past it, every file is asked about. */
|
|
@@ -14878,6 +15062,37 @@ function transferStalled(record, movedAt, restored, now) {
|
|
|
14878
15062
|
}
|
|
14879
15063
|
var key = (direction, id) => `${direction}:${id}`;
|
|
14880
15064
|
var isInt$1 = (value, max = Number.MAX_SAFE_INTEGER) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && value <= max;
|
|
15065
|
+
var FILE_DATA_OFFSET_BYTES = 8;
|
|
15066
|
+
/** The binary message for the bytes of file `id` at `offset`. Throws on what no receiver would take. */
|
|
15067
|
+
function encodeFileData(id, offset, data) {
|
|
15068
|
+
if (!ID.test(id) || !isInt$1(offset) || !data.length || data.length > FILE_LIMITS.chunkBytes) throw new Error("Invalid file data");
|
|
15069
|
+
const head = 2 + id.length;
|
|
15070
|
+
const out = new Uint8Array(head + FILE_DATA_OFFSET_BYTES + data.length);
|
|
15071
|
+
out[0] = 4;
|
|
15072
|
+
out[1] = id.length;
|
|
15073
|
+
for (let i = 0; i < id.length; i++) out[2 + i] = id.charCodeAt(i);
|
|
15074
|
+
const view = new DataView(out.buffer);
|
|
15075
|
+
view.setUint32(head, Math.floor(offset / 2 ** 32));
|
|
15076
|
+
view.setUint32(head + 4, offset % 2 ** 32);
|
|
15077
|
+
out.set(data, head + FILE_DATA_OFFSET_BYTES);
|
|
15078
|
+
return out;
|
|
15079
|
+
}
|
|
15080
|
+
/** What a binary message from the contact says, or null when it is not a well-formed file data message. `data` is a view of `message`. */
|
|
15081
|
+
function decodeFileData(message) {
|
|
15082
|
+
if (message.length < 2 || message[0] !== 4) return null;
|
|
15083
|
+
const head = 2 + message[1], body = head + FILE_DATA_OFFSET_BYTES;
|
|
15084
|
+
if (message.length <= body || message.length - body > FILE_LIMITS.chunkBytes) return null;
|
|
15085
|
+
const id = String.fromCharCode(...message.subarray(2, head));
|
|
15086
|
+
if (!ID.test(id)) return null;
|
|
15087
|
+
const view = new DataView(message.buffer, message.byteOffset, message.byteLength);
|
|
15088
|
+
const high = view.getUint32(head);
|
|
15089
|
+
if (high > 2097151) return null;
|
|
15090
|
+
return {
|
|
15091
|
+
id,
|
|
15092
|
+
offset: high * 2 ** 32 + view.getUint32(head + 4),
|
|
15093
|
+
data: message.subarray(body)
|
|
15094
|
+
};
|
|
15095
|
+
}
|
|
14881
15096
|
/** What a refusal means to the sender's person. */
|
|
14882
15097
|
function refusalText(why, room) {
|
|
14883
15098
|
switch (why) {
|
|
@@ -14914,6 +15129,10 @@ function refusalText(why, room) {
|
|
|
14914
15129
|
var ChatFiles = class {
|
|
14915
15130
|
host;
|
|
14916
15131
|
entries = /* @__PURE__ */ new Map();
|
|
15132
|
+
/** Files sending that may have bytes not confirmed yet: what `inFlight` adds up (the rest are left out as it goes). */
|
|
15133
|
+
moving = /* @__PURE__ */ new Set();
|
|
15134
|
+
/** Files that stopped sending for the chat's window alone: they go on once another's bytes are confirmed. */
|
|
15135
|
+
held = /* @__PURE__ */ new Set();
|
|
14917
15136
|
attached = false;
|
|
14918
15137
|
peerRoomValue = null;
|
|
14919
15138
|
constructor(host) {
|
|
@@ -14973,6 +15192,15 @@ var ChatFiles = class {
|
|
|
14973
15192
|
send(frame) {
|
|
14974
15193
|
return this.attached && this.host.send(frame);
|
|
14975
15194
|
}
|
|
15195
|
+
sendData(id, offset, data) {
|
|
15196
|
+
if (!this.host.sendData) return this.send({
|
|
15197
|
+
t: "pf-data",
|
|
15198
|
+
id,
|
|
15199
|
+
offset,
|
|
15200
|
+
data: toBase64Url(data)
|
|
15201
|
+
});
|
|
15202
|
+
return this.attached && this.host.sendData(id, offset, data);
|
|
15203
|
+
}
|
|
14976
15204
|
/** A session that agreed files/3 is open: say what fits here, then offer again what is not finished. */
|
|
14977
15205
|
attach() {
|
|
14978
15206
|
this.attached = true;
|
|
@@ -14991,6 +15219,8 @@ var ChatFiles = class {
|
|
|
14991
15219
|
if (!this.attached) return;
|
|
14992
15220
|
this.attached = false;
|
|
14993
15221
|
this.peerRoomValue = null;
|
|
15222
|
+
this.moving.clear();
|
|
15223
|
+
this.held.clear();
|
|
14994
15224
|
for (const entry of this.entries.values()) {
|
|
14995
15225
|
if (entry.out) {
|
|
14996
15226
|
entry.out.generation++;
|
|
@@ -15114,10 +15344,34 @@ var ChatFiles = class {
|
|
|
15114
15344
|
const at = offset - out.block.start;
|
|
15115
15345
|
return out.block.bytes.subarray(at, at + length);
|
|
15116
15346
|
}
|
|
15117
|
-
|
|
15347
|
+
/** Whether this file's own window lets it send more. */
|
|
15348
|
+
fileMayPump(entry) {
|
|
15118
15349
|
const { record } = entry, out = entry.out;
|
|
15119
15350
|
return this.attached && record.state === "active" && out.next < record.file.size && out.next - record.confirmed < FILE_LIMITS.windowBytes;
|
|
15120
15351
|
}
|
|
15352
|
+
shouldPump(entry) {
|
|
15353
|
+
return this.fileMayPump(entry) && this.inFlight() < FILE_LIMITS.chatWindowBytes;
|
|
15354
|
+
}
|
|
15355
|
+
/** Bytes this chat's files sent and have not seen confirmed. */
|
|
15356
|
+
inFlight() {
|
|
15357
|
+
let bytes = 0;
|
|
15358
|
+
for (const entry of this.moving) {
|
|
15359
|
+
const out = entry.out, { record } = entry;
|
|
15360
|
+
if (!out || record.state !== "active" || out.next <= record.confirmed) {
|
|
15361
|
+
this.moving.delete(entry);
|
|
15362
|
+
continue;
|
|
15363
|
+
}
|
|
15364
|
+
bytes += out.next - record.confirmed;
|
|
15365
|
+
}
|
|
15366
|
+
return bytes;
|
|
15367
|
+
}
|
|
15368
|
+
/** Bytes were confirmed, or a file stopped sending: the files the chat's window held go on, before the one that freed it. */
|
|
15369
|
+
wakeHeld() {
|
|
15370
|
+
for (const entry of [...this.held]) {
|
|
15371
|
+
this.held.delete(entry);
|
|
15372
|
+
this.pump(entry);
|
|
15373
|
+
}
|
|
15374
|
+
}
|
|
15121
15375
|
/**
|
|
15122
15376
|
* Whether the next `length` bytes go. Within the file's size they always do; past it the receiver is having parts
|
|
15123
15377
|
* sent again, which nothing it says bounds, so this side does.
|
|
@@ -15148,17 +15402,13 @@ var ChatFiles = class {
|
|
|
15148
15402
|
this.end(entry, "failed", "Your contact asked for the same part of this file too many times");
|
|
15149
15403
|
break;
|
|
15150
15404
|
}
|
|
15151
|
-
if (!this.
|
|
15152
|
-
t: "pf-data",
|
|
15153
|
-
id: entry.record.id,
|
|
15154
|
-
offset,
|
|
15155
|
-
data: toBase64Url(chunk)
|
|
15156
|
-
})) {
|
|
15405
|
+
if (!this.sendData(entry.record.id, offset, chunk)) {
|
|
15157
15406
|
refused = true;
|
|
15158
15407
|
break;
|
|
15159
15408
|
}
|
|
15160
15409
|
out.next = offset + chunk.length;
|
|
15161
15410
|
out.sent += chunk.length;
|
|
15411
|
+
this.moving.add(entry);
|
|
15162
15412
|
}
|
|
15163
15413
|
} catch (error) {
|
|
15164
15414
|
if (generation === out.generation) {
|
|
@@ -15173,7 +15423,7 @@ var ChatFiles = class {
|
|
|
15173
15423
|
if (generation === out.generation && this.shouldPump(entry)) {
|
|
15174
15424
|
if (refused) out.wake = setTimeout(() => void this.pump(entry), FILE_LIMITS.sendRetryMs);
|
|
15175
15425
|
else this.pump(entry);
|
|
15176
|
-
}
|
|
15426
|
+
} else if (generation === out.generation && this.fileMayPump(entry)) this.held.add(entry);
|
|
15177
15427
|
}
|
|
15178
15428
|
}
|
|
15179
15429
|
async sendSum(entry) {
|
|
@@ -15205,7 +15455,7 @@ var ChatFiles = class {
|
|
|
15205
15455
|
return [...this.entries.values()].filter((e) => e.record.direction === "in" && e.record.state === "asking").length;
|
|
15206
15456
|
}
|
|
15207
15457
|
activeIncoming(except) {
|
|
15208
|
-
return [...this.entries.values()].filter((e) => e !== except && e.record.direction === "in" && (e.record.state === "active" || e.record.state === "verifying")).length;
|
|
15458
|
+
return [...this.entries.values()].filter((e) => e !== except && e.record.direction === "in" && (e.record.state === "active" || e.record.state === "verifying" || e.in?.starting)).length;
|
|
15209
15459
|
}
|
|
15210
15460
|
/**
|
|
15211
15461
|
* Removes what this side stored of an incoming file that ends here. Through its target, opened now if need be: after
|
|
@@ -15232,9 +15482,15 @@ var ChatFiles = class {
|
|
|
15232
15482
|
}
|
|
15233
15483
|
return incoming.target;
|
|
15234
15484
|
}
|
|
15235
|
-
/**
|
|
15485
|
+
/**
|
|
15486
|
+
* An accepted file goes when a session is open and fewer than three others arrive at once. One whose target is still
|
|
15487
|
+
* being opened counts among the three: counted only once active, every file offered or queued meanwhile went too, and
|
|
15488
|
+
* the windows of ten files and more at once overflowed the session's receive queue.
|
|
15489
|
+
*/
|
|
15236
15490
|
startIncoming(entry) {
|
|
15237
15491
|
if (!this.attached || transferEnded(entry.record) || entry.record.state === "asking" || entry.record.state === "paused" && entry.record.pausedBy === "me") return;
|
|
15492
|
+
const incoming = entry.in;
|
|
15493
|
+
if (incoming.starting) return;
|
|
15238
15494
|
if (this.activeIncoming(entry) >= FILE_LIMITS.maxActiveIncoming) {
|
|
15239
15495
|
if (entry.record.state !== "queued") {
|
|
15240
15496
|
entry.record.state = "queued";
|
|
@@ -15247,9 +15503,13 @@ var ChatFiles = class {
|
|
|
15247
15503
|
});
|
|
15248
15504
|
return;
|
|
15249
15505
|
}
|
|
15506
|
+
incoming.starting = true;
|
|
15250
15507
|
this.target(entry).then(() => {
|
|
15251
|
-
|
|
15252
|
-
if (entry.record.state === "paused" && entry.record.pausedBy === "me")
|
|
15508
|
+
incoming.starting = false;
|
|
15509
|
+
if (transferEnded(entry.record) || !this.attached || entry.record.state === "paused" && entry.record.pausedBy === "me") {
|
|
15510
|
+
this.startNext();
|
|
15511
|
+
return;
|
|
15512
|
+
}
|
|
15253
15513
|
entry.record.state = "active";
|
|
15254
15514
|
entry.record.pausedBy = void 0;
|
|
15255
15515
|
entry.in.gapAt = void 0;
|
|
@@ -15262,12 +15522,14 @@ var ChatFiles = class {
|
|
|
15262
15522
|
entry.in.heardAt = this.now();
|
|
15263
15523
|
this.armNudge(entry);
|
|
15264
15524
|
}, (error) => {
|
|
15525
|
+
incoming.starting = false;
|
|
15265
15526
|
this.end(entry, "failed", `Could not store the file: ${error instanceof Error ? error.message : String(error)}`);
|
|
15266
15527
|
this.send({
|
|
15267
15528
|
t: "pf-refuse",
|
|
15268
15529
|
id: entry.record.id,
|
|
15269
15530
|
why: "no-room"
|
|
15270
15531
|
});
|
|
15532
|
+
this.startNext();
|
|
15271
15533
|
});
|
|
15272
15534
|
}
|
|
15273
15535
|
/** The next file waiting its turn, once one ends. */
|
|
@@ -15506,7 +15768,9 @@ var ChatFiles = class {
|
|
|
15506
15768
|
if (record.state !== "active" || !incoming.target) return;
|
|
15507
15769
|
incoming.heardAt = this.now();
|
|
15508
15770
|
const { offset, data } = frame;
|
|
15509
|
-
|
|
15771
|
+
const binary = data instanceof Uint8Array;
|
|
15772
|
+
if (!isInt$1(offset) || !(binary || typeof data === "string") || !data.length) return;
|
|
15773
|
+
if (!binary && (data.length > Math.ceil(FILE_LIMITS.chunkBytes * 4 / 3) || !DATA.test(data))) return;
|
|
15510
15774
|
if (offset < incoming.written) {
|
|
15511
15775
|
if (incoming.stored === incoming.written) this.send({
|
|
15512
15776
|
t: "pf-got",
|
|
@@ -15526,7 +15790,7 @@ var ChatFiles = class {
|
|
|
15526
15790
|
}
|
|
15527
15791
|
return;
|
|
15528
15792
|
}
|
|
15529
|
-
const bytes = fromBase64Url(data);
|
|
15793
|
+
const bytes = binary ? data : fromBase64Url(data);
|
|
15530
15794
|
if (!bytes.length || bytes.length > FILE_LIMITS.chunkBytes || offset + bytes.length > record.file.size) return;
|
|
15531
15795
|
incoming.written += bytes.length;
|
|
15532
15796
|
const target = incoming.target;
|
|
@@ -15711,6 +15975,7 @@ var ChatFiles = class {
|
|
|
15711
15975
|
record.state = "active";
|
|
15712
15976
|
record.pausedBy = record.waitingFor = void 0;
|
|
15713
15977
|
this.changed(outgoing);
|
|
15978
|
+
this.wakeHeld();
|
|
15714
15979
|
if (record.confirmed === record.file.size) this.sendSum(outgoing);
|
|
15715
15980
|
else {
|
|
15716
15981
|
this.armStall(outgoing);
|
|
@@ -15730,11 +15995,13 @@ var ChatFiles = class {
|
|
|
15730
15995
|
record.waitingFor = frame.why === "busy" ? "busy" : "consent";
|
|
15731
15996
|
}
|
|
15732
15997
|
this.changed(outgoing);
|
|
15998
|
+
this.wakeHeld();
|
|
15733
15999
|
return;
|
|
15734
16000
|
case "pf-got":
|
|
15735
16001
|
if (!isInt$1(frame.offset) || frame.offset <= record.confirmed || frame.offset > out.next) return;
|
|
15736
16002
|
record.confirmed = frame.offset;
|
|
15737
16003
|
this.changed(outgoing, true);
|
|
16004
|
+
this.wakeHeld();
|
|
15738
16005
|
if (record.confirmed === record.file.size) this.sendSum(outgoing);
|
|
15739
16006
|
else {
|
|
15740
16007
|
this.armStall(outgoing);
|
|
@@ -15764,6 +16031,8 @@ var ChatFiles = class {
|
|
|
15764
16031
|
clearTimeout(entry.out.wake);
|
|
15765
16032
|
entry.out.block = void 0;
|
|
15766
16033
|
entry.out.source = void 0;
|
|
16034
|
+
this.moving.delete(entry);
|
|
16035
|
+
this.held.delete(entry);
|
|
15767
16036
|
}
|
|
15768
16037
|
if (entry.in) {
|
|
15769
16038
|
const incoming = entry.in;
|
|
@@ -15773,6 +16042,7 @@ var ChatFiles = class {
|
|
|
15773
16042
|
});
|
|
15774
16043
|
}
|
|
15775
16044
|
this.changed(entry);
|
|
16045
|
+
if (entry.out) this.wakeHeld();
|
|
15776
16046
|
}
|
|
15777
16047
|
entry(direction, id) {
|
|
15778
16048
|
const entry = this.entries.get(key(direction, id));
|
|
@@ -15821,6 +16091,7 @@ var ChatFiles = class {
|
|
|
15821
16091
|
this.startNext();
|
|
15822
16092
|
}
|
|
15823
16093
|
this.changed(entry);
|
|
16094
|
+
if (entry.out) this.wakeHeld();
|
|
15824
16095
|
}
|
|
15825
16096
|
resume(direction, id) {
|
|
15826
16097
|
const entry = this.entry(direction, id), { record } = entry;
|
|
@@ -16439,6 +16710,8 @@ var CarriedTransport = class {
|
|
|
16439
16710
|
/** My last packet while the edge is down: what a member that just became reachable is handed. */
|
|
16440
16711
|
mine = null;
|
|
16441
16712
|
lastTimestamp = 0n;
|
|
16713
|
+
/** The date each packet `stamp` signed carries: the relays' transport takes it rather than check the signature again. */
|
|
16714
|
+
dated = /* @__PURE__ */ new WeakMap();
|
|
16442
16715
|
/** My newest packet, wherever it went: what the edge hands the member over itself once it is ready (`linkReady`). */
|
|
16443
16716
|
newest = null;
|
|
16444
16717
|
/** The newest packet of the member's that came through a carrier, and the timestamp of the one a read last returned. */
|
|
@@ -16518,7 +16791,9 @@ var CarriedTransport = class {
|
|
|
16518
16791
|
stamp(identity, records) {
|
|
16519
16792
|
const now = BigInt(Date.now()) * 1000n;
|
|
16520
16793
|
this.lastTimestamp = now > this.lastTimestamp ? now : this.lastTimestamp + 1n;
|
|
16521
|
-
|
|
16794
|
+
const payload = createRelayPayload(identity, records, this.lastTimestamp);
|
|
16795
|
+
this.dated.set(payload, this.lastTimestamp);
|
|
16796
|
+
return payload;
|
|
16522
16797
|
}
|
|
16523
16798
|
/**
|
|
16524
16799
|
* The relays get the very packet members carried or the edge itself handed over, byte for byte, where the transport
|
|
@@ -16528,7 +16803,11 @@ var CarriedTransport = class {
|
|
|
16528
16803
|
* its restart and then wait 40 s for (7 of 16 goodbyes on the admin in the member-back runs, 1 of 16 on dev, 2026-10-03).
|
|
16529
16804
|
*/
|
|
16530
16805
|
toRelays(identity, records, payload, options) {
|
|
16531
|
-
|
|
16806
|
+
const signedAt = this.dated.get(payload);
|
|
16807
|
+
return this.inner.publishPayload ? this.inner.publishPayload(identity.pubKeyZ32, payload, signedAt === void 0 ? options : {
|
|
16808
|
+
...options,
|
|
16809
|
+
signedAt
|
|
16810
|
+
}) : this.inner.publish(identity, records, options);
|
|
16532
16811
|
}
|
|
16533
16812
|
/**
|
|
16534
16813
|
* The packet the relays just took, handed to the member over the edge itself while it is up. An edge up reads nothing
|
|
@@ -16673,8 +16952,12 @@ var GROUP_FILE_LIMITS = {
|
|
|
16673
16952
|
heardAhead: 256,
|
|
16674
16953
|
/** Message ids in one `group-have`. */
|
|
16675
16954
|
haveIds: 32,
|
|
16676
|
-
/**
|
|
16677
|
-
|
|
16955
|
+
/**
|
|
16956
|
+
* `group-have` frames a private group's edge takes in `haveWindowMs`: what is to say is gathered for a second, then
|
|
16957
|
+
* goes. A community's, from a member, goes every `communityHaveEveryMs`.
|
|
16958
|
+
*/
|
|
16959
|
+
haveFrames: 10,
|
|
16960
|
+
haveWindowMs: 1e4,
|
|
16678
16961
|
communityHaveEveryMs: 6e4,
|
|
16679
16962
|
/**
|
|
16680
16963
|
* How long a want waits for an offer before the next holder is asked; a transfer that does not move for as long is
|
|
@@ -17078,6 +17361,14 @@ var PACKET_TRANSPORTS_FAST_ATTEMPTS = 3;
|
|
|
17078
17361
|
var DHT_PIN_GRACE_MS = 1e4;
|
|
17079
17362
|
/** Back after a restart (`resume`), dials wait this long at most for the native endpoint that carried the chat to start. */
|
|
17080
17363
|
var RESUME_WAIT_MS = 1e4;
|
|
17364
|
+
/**
|
|
17365
|
+
* A dial of the contact's that meets such a session waits this long for that session to answer a ping. It answers when
|
|
17366
|
+
* the dials crossed (the contact holds this side's dial too); it does not when the contact's app ended since and is
|
|
17367
|
+
* back: its dial then takes over. Refused outright, that dial left the contact waiting for the dead session's end, 16.6
|
|
17368
|
+
* to 18.9 s (CLI daemons over HyperDHT, the contact killed 1 to 3 s after this side's knock, 5 of 5). A round trip is
|
|
17369
|
+
* 7 ms on loopback and under 0.5 s over a relayed Iroh.
|
|
17370
|
+
*/
|
|
17371
|
+
var CROSSED_PROBE_MS = 1500;
|
|
17081
17372
|
/** A WebRTC attempt under way, not yet open: what a member's packet saying it has no WebRTC ends (`peerPacketTransports`). */
|
|
17082
17373
|
var WEBRTC_ATTEMPT = /* @__PURE__ */ new Set([
|
|
17083
17374
|
"offering",
|
|
@@ -17199,6 +17490,24 @@ var HANDLED_WAIT_MS = 1e4;
|
|
|
17199
17490
|
*/
|
|
17200
17491
|
var WAKE_LOST_MS = 1e4;
|
|
17201
17492
|
/**
|
|
17493
|
+
* What files/3 may hold on a session's channel (WISP 501, Backpressure): a quarter of its send budget (native and iroh
|
|
17494
|
+
* channels throw past 120 KiB), or the high water mark of a WebRTC channel, which has none.
|
|
17495
|
+
*/
|
|
17496
|
+
function filesCap(channel) {
|
|
17497
|
+
return channel.sendBudget ? channel.sendBudget / 4 : LIMITS.sendHighWaterMark;
|
|
17498
|
+
}
|
|
17499
|
+
/**
|
|
17500
|
+
* How long after files last wrote on a channel, or last waited for room on it, an app's data keeps its own quarter above
|
|
17501
|
+
* their line (WISP 1200 § In a chat): a file waiting for `pf-got` holds its share meanwhile.
|
|
17502
|
+
*/
|
|
17503
|
+
var FILES_IN_USE_MS = 2e3;
|
|
17504
|
+
/**
|
|
17505
|
+
* How far an app's data may get ahead of a file's bytes while the file waits for room on the channel (WISP 1200 § In a
|
|
17506
|
+
* chat): a frame goes only while the app has sent less than this more than the file since. A small frame (a Chess move)
|
|
17507
|
+
* still goes at once; one streaming at its limit gets about half the link, and the file the other half.
|
|
17508
|
+
*/
|
|
17509
|
+
var APP_AHEAD_OF_FILES = 16384;
|
|
17510
|
+
/**
|
|
17202
17511
|
* A `paired-message` frame. The preview (`pv`) is left out when the frame would pass `MAX_PAIRED_MESSAGE_FRAME`
|
|
17203
17512
|
* with it: the text matters, the card does not. A reply (`r`, WISP 401 § Replies) is a few hundred bytes at most
|
|
17204
17513
|
* and always goes, and so does a forwarded message's hop count (`fw`, WISP 401 § Forwards) and a status card (`sc`,
|
|
@@ -17228,6 +17537,16 @@ function pairedMessageFrame(id, ts, m, preview, reply, forwarded, card) {
|
|
|
17228
17537
|
});
|
|
17229
17538
|
return withPreview.length <= 57344 ? withPreview : plain;
|
|
17230
17539
|
}
|
|
17540
|
+
/** A text whose `paired-message` frame would pass what a session takes: it never arrives, and is refused at send. */
|
|
17541
|
+
var PAIRED_FRAME_TOO_LARGE = "Message too long once encoded. Shorten it or send it as a file.";
|
|
17542
|
+
/**
|
|
17543
|
+
* The `paired-message` frame of this text fits a session (`SESSION_FRAME_MAX`, UTF-16 units of the JSON). Within 16 KiB
|
|
17544
|
+
* of UTF-8 a text can still pass it: JSON writes each control character as six (`\u0001`). The preview is left out,
|
|
17545
|
+
* as the frame drops one that does not fit.
|
|
17546
|
+
*/
|
|
17547
|
+
function pairedMessageFits(ts, m, reply, forwarded, card) {
|
|
17548
|
+
return pairedMessageFrame("A".repeat(22), ts, m, void 0, reply, forwarded, card).length <= SESSION_FRAME_MAX;
|
|
17549
|
+
}
|
|
17231
17550
|
/**
|
|
17232
17551
|
* A native descriptor from the contact's capability record against the one known (a session's, or an older record's).
|
|
17233
17552
|
* The record carries only how to dial, never an address: the Iroh endpoint id and the relay it is homed on, the
|
|
@@ -17546,6 +17865,9 @@ var GhostLink = class {
|
|
|
17546
17865
|
});
|
|
17547
17866
|
options.events?.onPairingProgress?.(progress);
|
|
17548
17867
|
}) : null;
|
|
17868
|
+
const invite = !!options.pairingProgress && !!options.pairing && !options.pairing.credentials.peerKey;
|
|
17869
|
+
let unanswered = options.dht?.state?.unanswered !== void 0;
|
|
17870
|
+
const backsOff = invite || unanswered;
|
|
17549
17871
|
this.dht = options.params.profile && options.pairing && options.dht ? new DhtDelivery({
|
|
17550
17872
|
params: options.params,
|
|
17551
17873
|
mode: this.deliveryMode,
|
|
@@ -17555,6 +17877,7 @@ var GhostLink = class {
|
|
|
17555
17877
|
save: options.dht.save,
|
|
17556
17878
|
pollMs: options.dht.pollMs,
|
|
17557
17879
|
firstControlAfterMs: options.dht.firstControlAfterMs,
|
|
17880
|
+
...backsOff && { inviteMadeAt: options.madeAt ?? Date.now() },
|
|
17558
17881
|
capsRev: options.dht.capsRev,
|
|
17559
17882
|
peerCapsRev: options.dht.peerCapsRev,
|
|
17560
17883
|
peerAcceptsText: options.dht.peerAcceptsText,
|
|
@@ -17570,6 +17893,7 @@ var GhostLink = class {
|
|
|
17570
17893
|
},
|
|
17571
17894
|
pin: async (key) => {
|
|
17572
17895
|
await options.pairing.pinPeer(key, true);
|
|
17896
|
+
if (invite) this.session.lookNow();
|
|
17573
17897
|
if (this.isDataLinkOpen || !this.tracker || this.tracker.done) return;
|
|
17574
17898
|
if (this.deliveryMode === "dht") {
|
|
17575
17899
|
this.tracker.onDht("chosen");
|
|
@@ -17612,6 +17936,9 @@ var GhostLink = class {
|
|
|
17612
17936
|
},
|
|
17613
17937
|
changed: (view) => {
|
|
17614
17938
|
options.events?.onDhtDelivery?.(view);
|
|
17939
|
+
const stillUnanswered = this.dht.unanswered;
|
|
17940
|
+
if (unanswered && !stillUnanswered) this.session.lookNow();
|
|
17941
|
+
unanswered = stillUnanswered;
|
|
17615
17942
|
if (view.inviteTaken) this.tracker?.failed("taken", false, INVITE_TAKEN);
|
|
17616
17943
|
else this.tracker?.untaken();
|
|
17617
17944
|
this.streamBlockChanged();
|
|
@@ -17640,6 +17967,7 @@ var GhostLink = class {
|
|
|
17640
17967
|
lastSeenTimestamp: options.lastSeenTimestamp,
|
|
17641
17968
|
pollIntervals: options.pollIntervals,
|
|
17642
17969
|
awaitingSince: options.madeAt,
|
|
17970
|
+
...backsOff && { invite: () => !options.pairing.credentials.peerKey || !!this.dht?.unanswered },
|
|
17643
17971
|
getServices: options.getServices,
|
|
17644
17972
|
firstPublish: options.firstPublish ?? (this.tracker && options.pairingProgress?.role === "joiner" && options.rtcAvailable !== false || this.resuming ? "after-first-poll" : "at-start"),
|
|
17645
17973
|
events: {
|
|
@@ -18152,11 +18480,11 @@ var GhostLink = class {
|
|
|
18152
18480
|
}
|
|
18153
18481
|
start() {
|
|
18154
18482
|
if (this.saidTransports) this.publishPacketTransports();
|
|
18155
|
-
if (this.deliveryMode !== "dht") this.session.start();
|
|
18156
18483
|
if (this.resuming && this.deliveryMode !== "dht") {
|
|
18157
18484
|
this.session.expectPeer();
|
|
18158
18485
|
this.resumeWaitUntil = Date.now() + RESUME_WAIT_MS;
|
|
18159
18486
|
}
|
|
18487
|
+
if (this.deliveryMode !== "dht") this.session.start();
|
|
18160
18488
|
if (this.tracker && !this.tracker.done && this.options.pairingProgress?.role === "joiner") {
|
|
18161
18489
|
this.dht?.expect();
|
|
18162
18490
|
this.dialEarly();
|
|
@@ -18278,6 +18606,7 @@ var GhostLink = class {
|
|
|
18278
18606
|
disconnect() {
|
|
18279
18607
|
this.closeRetired();
|
|
18280
18608
|
this.dropCrossed();
|
|
18609
|
+
this.dropCrossedProbe();
|
|
18281
18610
|
this.dropBlockedDial();
|
|
18282
18611
|
this.switcher.stop();
|
|
18283
18612
|
this.cancelCandidate();
|
|
@@ -19173,9 +19502,30 @@ var GhostLink = class {
|
|
|
19173
19502
|
epoch,
|
|
19174
19503
|
rest
|
|
19175
19504
|
};
|
|
19505
|
+
if (!this.peerTransports.includes("webrtc/1")) {
|
|
19506
|
+
traceLink(this.myPubKeyZ32, "offer-unanswerable", { transports: rest });
|
|
19507
|
+
this.clearRace();
|
|
19508
|
+
this.raceTimer = setTimeout(() => {
|
|
19509
|
+
this.raceTimer = null;
|
|
19510
|
+
this.endUnanswerableOffer(epoch);
|
|
19511
|
+
}, 0);
|
|
19512
|
+
return;
|
|
19513
|
+
}
|
|
19176
19514
|
traceLink(this.myPubKeyZ32, "race-standing", { transports: rest });
|
|
19177
19515
|
this.scheduleRace(epoch, made.at);
|
|
19178
19516
|
}
|
|
19517
|
+
/** The offer ends (`raceStandingOffer`), and with it going idle what is ranked after WebRTC is dialled (`dialAfterRtc`). */
|
|
19518
|
+
endUnanswerableOffer(epoch) {
|
|
19519
|
+
if (epoch !== this.connectionEpoch || this.channel || this.stopped || !this.offerOut || this.afterRtc?.epoch !== epoch) return;
|
|
19520
|
+
if (this.dialing) {
|
|
19521
|
+
this.raceTimer = setTimeout(() => {
|
|
19522
|
+
this.raceTimer = null;
|
|
19523
|
+
this.endUnanswerableOffer(epoch);
|
|
19524
|
+
}, RACE_RETRY_MS);
|
|
19525
|
+
return;
|
|
19526
|
+
}
|
|
19527
|
+
this.dataLink.close();
|
|
19528
|
+
}
|
|
19179
19529
|
/**
|
|
19180
19530
|
* Back after an absence, this side answers an offer the contact made while it was away (it dials whatever its key):
|
|
19181
19531
|
* 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
|
|
@@ -19284,6 +19634,18 @@ var GhostLink = class {
|
|
|
19284
19634
|
if (!this.options.params.profile || this.options.packetTransports) return true;
|
|
19285
19635
|
return this.transportOffer().some((t) => t === "webrtc/1" ? !this.peerTransports || this.peerTransports.includes(t) : this.canDial(t) && !!this.peerTransports?.includes(t));
|
|
19286
19636
|
}
|
|
19637
|
+
/**
|
|
19638
|
+
* Neither side can dial the other until something changes, past a first pairing: what the contact runs is known, and
|
|
19639
|
+
* no transport both run has WebRTC on both sides or an endpoint up on both (this side's, and the one its record
|
|
19640
|
+
* describes). A chat that gave its listener up to another (`yieldEndpoint`), or whose contact did, is so. It dials
|
|
19641
|
+
* again when that changes (a listener taken back, `registerEndpoint`; the record describing one, `learnPeerTransports`),
|
|
19642
|
+
* and reads at its own pace meanwhile: looks every 20 s for a dial with nothing to try, and the contact's fast looks
|
|
19643
|
+
* for an offer that could not come, read its key 2.5x to 4x as often as the background pace, for good (bug hunt,
|
|
19644
|
+
* 2026-10-12). A first pairing keeps both: the records are what it is reading for.
|
|
19645
|
+
*/
|
|
19646
|
+
nothingToDialEitherWay() {
|
|
19647
|
+
return !(!!this.tracker && !this.tracker.done && this.tracker.progress.stage !== "on-dht") && !!this.peerTransports && !this.anythingToDial();
|
|
19648
|
+
}
|
|
19287
19649
|
clearRace() {
|
|
19288
19650
|
if (this.raceTimer) clearTimeout(this.raceTimer);
|
|
19289
19651
|
this.raceTimer = null;
|
|
@@ -19346,6 +19708,13 @@ var GhostLink = class {
|
|
|
19346
19708
|
this.attemptEnded();
|
|
19347
19709
|
this.rejectWaiters(new GhostlyHttpError("unreachable", "Could not connect to the peer"));
|
|
19348
19710
|
}
|
|
19711
|
+
/**
|
|
19712
|
+
* This side does not dial on its own: the contact's app does (the lower key), and none of what makes this side dial
|
|
19713
|
+
* anyway holds (a first pairing's role, a restart, WebRTC set aside). Its own dial, chosen by hand, is not retried.
|
|
19714
|
+
*/
|
|
19715
|
+
awaitsContactDial() {
|
|
19716
|
+
return !(!!this.tracker && !this.tracker.done && this.tracker.progress.stage !== "on-dht" ? this.options.pairingProgress?.role : void 0) && this.myPubKeyZ32 > this.options.params.peerPubKeyZ32 && !this.resuming && !this.dialsPastRtc();
|
|
19717
|
+
}
|
|
19349
19718
|
/** A transport of the attempt under way did not connect. */
|
|
19350
19719
|
attemptFailed(transport, error) {
|
|
19351
19720
|
if (this.isDataLinkOpen) return;
|
|
@@ -19368,7 +19737,7 @@ var GhostLink = class {
|
|
|
19368
19737
|
side: attempt.side,
|
|
19369
19738
|
failed: attempt.failed,
|
|
19370
19739
|
...!attempt.failed.length && reason ? { reason } : {},
|
|
19371
|
-
...attempt.side === "dialled" ? { retryAt: Math.max(Date.now(), this.lastAutoConnectAt + this.dialWait()) } : {}
|
|
19740
|
+
...attempt.side === "dialled" && !this.awaitsContactDial() ? { retryAt: Math.max(Date.now(), this.lastAutoConnectAt + this.dialWait()) } : {}
|
|
19372
19741
|
};
|
|
19373
19742
|
traceLink(this.myPubKeyZ32, "attempt-ended", {
|
|
19374
19743
|
side: attempt.side,
|
|
@@ -19407,9 +19776,11 @@ var GhostLink = class {
|
|
|
19407
19776
|
if (this.pairedPending.size >= 128) this.pairedPending.delete(this.pairedPending.keys().next().value);
|
|
19408
19777
|
const id = stableId ?? toBase64Url(randomBytes(16));
|
|
19409
19778
|
if (!/^[A-Za-z0-9_-]{22}$/.test(id)) return "Invalid message ID";
|
|
19779
|
+
const frame = pairedMessageFrame(id, timestamp, trimmed, preview, reply, forwarded, card);
|
|
19780
|
+
if (frame.length > 61440) return PAIRED_FRAME_TOO_LARGE;
|
|
19410
19781
|
this.pairedPending.set(id, timestamp);
|
|
19411
19782
|
try {
|
|
19412
|
-
this.channel.send(
|
|
19783
|
+
this.channel.send(frame);
|
|
19413
19784
|
return null;
|
|
19414
19785
|
} catch {
|
|
19415
19786
|
this.pairedPending.delete(id);
|
|
@@ -19575,15 +19946,14 @@ var GhostLink = class {
|
|
|
19575
19946
|
if (presence.lastPacketAt === this.departed.packet && Date.now() < this.departed.until) return;
|
|
19576
19947
|
this.departed = null;
|
|
19577
19948
|
}
|
|
19578
|
-
|
|
19579
|
-
if (role === "inviter") {
|
|
19949
|
+
if ((!!this.tracker && !this.tracker.done && this.tracker.progress.stage !== "on-dht" ? this.options.pairingProgress?.role : void 0) === "inviter") {
|
|
19580
19950
|
this.peerSeenAt ||= Date.now();
|
|
19581
19951
|
if (Date.now() - this.peerSeenAt < 2500 && !this.nothingToAwait()) {
|
|
19582
19952
|
this.session.expectPeer();
|
|
19583
19953
|
return;
|
|
19584
19954
|
}
|
|
19585
|
-
} else if (
|
|
19586
|
-
if (Date.now() - presenceSeenAt(presence) < 3e4 && presence.lastPacketAt !== this.offerAwaitedFor) {
|
|
19955
|
+
} else if (this.awaitsContactDial()) {
|
|
19956
|
+
if (Date.now() - presenceSeenAt(presence) < 3e4 && presence.lastPacketAt !== this.offerAwaitedFor && !this.nothingToDialEitherWay()) {
|
|
19587
19957
|
this.offerAwaitedFor = presence.lastPacketAt;
|
|
19588
19958
|
this.session.expectPeer();
|
|
19589
19959
|
}
|
|
@@ -19661,6 +20031,7 @@ var GhostLink = class {
|
|
|
19661
20031
|
this.retryLook = null;
|
|
19662
20032
|
const from = this.lastAutoConnectAt;
|
|
19663
20033
|
if (!from || this.stopped || this.leaving || this.streamBlocked || this.keyStopped || !this.options.autoConnect) return;
|
|
20034
|
+
if (this.nothingToDialEitherWay()) return;
|
|
19664
20035
|
this.retryLook = setTimeout(() => {
|
|
19665
20036
|
this.retryLook = null;
|
|
19666
20037
|
if (this.lastAutoConnectAt !== from || this.stopped || this.leaving || this.yielding || this.streamBlocked || this.keyStopped || this.channel || this.dialing || this.dataLink.state !== "idle") return;
|
|
@@ -19920,6 +20291,7 @@ var GhostLink = class {
|
|
|
19920
20291
|
if (this.options.callsSupport) offered.push(CALLS_CAPABILITY);
|
|
19921
20292
|
if (this.options.servicesSupport) offered.push(SERVICES_CAPABILITY);
|
|
19922
20293
|
if (this.options.largeFilesSupport) offered.push(FILES_CAPABILITY);
|
|
20294
|
+
if (this.options.largeFilesSupport && this.options.binaryFilesSupport && this.channel?.binary) offered.push(FILES_BINARY_CAPABILITY);
|
|
19923
20295
|
if (this.options.typingSupport) offered.push(TYPING_CAPABILITY);
|
|
19924
20296
|
if (this.options.reactionsSupport) offered.push(REACTIONS_CAPABILITY);
|
|
19925
20297
|
if (this.options.editSupport) offered.push(EDIT_CAPABILITY);
|
|
@@ -19956,6 +20328,10 @@ var GhostLink = class {
|
|
|
19956
20328
|
get supportsLargeFiles() {
|
|
19957
20329
|
return this.isDataLinkOpen && this.sessionCapabilities.agreed("files/3");
|
|
19958
20330
|
}
|
|
20331
|
+
/** Both sides offer `files/4` beside `files/3` on the open session: a file's bytes go as binary messages. */
|
|
20332
|
+
get supportsBinaryFiles() {
|
|
20333
|
+
return this.supportsLargeFiles && this.sessionCapabilities.agreed("files/4");
|
|
20334
|
+
}
|
|
19959
20335
|
/** Both sides offer `typing/1` on the open session: typing can be said and shown. */
|
|
19960
20336
|
get supportsTyping() {
|
|
19961
20337
|
return this.isDataLinkOpen && this.sessionCapabilities.agreed("typing/1");
|
|
@@ -20119,21 +20495,33 @@ var GhostLink = class {
|
|
|
20119
20495
|
}
|
|
20120
20496
|
/**
|
|
20121
20497
|
* One data frame of an app open on both sides, on the live session only. An error when it did not go: nothing is
|
|
20122
|
-
* kept to send later.
|
|
20498
|
+
* kept to send later. While the channel already holds a quarter of what it may queue (its send budget, or the high
|
|
20499
|
+
* water mark of a WebRTC channel), the frame is refused with `too-fast`, so the chat's messages and pings never wait
|
|
20500
|
+
* behind an app's data on a link slower than it sends (WISP 1200 § In a chat). While a file goes, that quarter sits
|
|
20501
|
+
* above what files may hold (`filesWritable`): files keep the channel at their own line for the whole transfer. While
|
|
20502
|
+
* the file waits for room, the app gets no further than `APP_AHEAD_OF_FILES` ahead of it, so it cannot refill the
|
|
20503
|
+
* channel each time it drains to files' line and keep the file from moving.
|
|
20123
20504
|
*/
|
|
20124
20505
|
sendAppData(app, data) {
|
|
20125
20506
|
if (!this.apps.open.has(app)) return "not-open";
|
|
20126
20507
|
if (appDataBytes(data) > 32768) return "too-large";
|
|
20127
20508
|
if (!this.channel || !this.isDataLinkOpen) return "offline";
|
|
20128
20509
|
if (!this.supportsApps) return "peer-closed";
|
|
20510
|
+
const use = this.filesUse?.channel === this.channel ? this.filesUse : null;
|
|
20511
|
+
const files = use && (use.waiting > 0 || Date.now() - use.at < FILES_IN_USE_MS) ? use : null;
|
|
20512
|
+
if (use && !files) use.appAhead = 0;
|
|
20513
|
+
if (this.channel.bufferedAmount >= (files ? filesCap(this.channel) : 0) + (this.channel.sendBudget ?? LIMITS.sendHighWaterMark) / 4) return "too-fast";
|
|
20514
|
+
if (files && files.waiting > 0 && files.appAhead >= APP_AHEAD_OF_FILES) return "too-fast";
|
|
20129
20515
|
const refused = this.apps.canSend(app, data);
|
|
20130
20516
|
if (refused) return refused;
|
|
20517
|
+
const frame = JSON.stringify(appDataFrame(app, data));
|
|
20131
20518
|
try {
|
|
20132
|
-
this.channel.send(
|
|
20133
|
-
return null;
|
|
20519
|
+
this.channel.send(frame);
|
|
20134
20520
|
} catch {
|
|
20135
20521
|
return "offline";
|
|
20136
20522
|
}
|
|
20523
|
+
if (files) files.appAhead = Math.min(files.appAhead + frame.length, APP_AHEAD_OF_FILES + APP_DATA_MAX_BYTES);
|
|
20524
|
+
return null;
|
|
20137
20525
|
}
|
|
20138
20526
|
/** What the contact had open is gone (`apps/1` is no longer agreed, or this link stopped): each app hears it now. */
|
|
20139
20527
|
appsEnded() {
|
|
@@ -20176,19 +20564,64 @@ var GhostLink = class {
|
|
|
20176
20564
|
}
|
|
20177
20565
|
}
|
|
20178
20566
|
/**
|
|
20567
|
+
* A file's bytes to the contact, on the open session: one binary message while both sides agreed `files/4` on it,
|
|
20568
|
+
* the `pf-data` frame with them as base64url text otherwise (WISP 501 § files/4). The form is chosen for each chunk,
|
|
20569
|
+
* so a transfer follows the session it is on. False when files/3 cannot flow now, or the channel has no room.
|
|
20570
|
+
*/
|
|
20571
|
+
sendFileData(id, offset, data) {
|
|
20572
|
+
if (!this.supportsLargeFiles || !this.channel) return false;
|
|
20573
|
+
try {
|
|
20574
|
+
const frame = this.supportsBinaryFiles ? encodeFileData(id, offset, data) : JSON.stringify({
|
|
20575
|
+
t: "pf-data",
|
|
20576
|
+
id,
|
|
20577
|
+
offset,
|
|
20578
|
+
data: toBase64Url(data)
|
|
20579
|
+
});
|
|
20580
|
+
this.channel.send(frame);
|
|
20581
|
+
const use = this.filesUsing(this.channel);
|
|
20582
|
+
use.at = Date.now();
|
|
20583
|
+
use.appAhead = Math.max(0, use.appAhead - (typeof frame === "string" ? frame.length : frame.byteLength));
|
|
20584
|
+
return true;
|
|
20585
|
+
} catch {
|
|
20586
|
+
return false;
|
|
20587
|
+
}
|
|
20588
|
+
}
|
|
20589
|
+
/**
|
|
20590
|
+
* Files on a channel: when they last wrote on it, how many wait for room, and how many bytes an app's data has sent
|
|
20591
|
+
* on it beyond theirs since. An app's data gets room above theirs.
|
|
20592
|
+
*/
|
|
20593
|
+
filesUse = null;
|
|
20594
|
+
filesUsing(channel) {
|
|
20595
|
+
if (this.filesUse?.channel !== channel) this.filesUse = {
|
|
20596
|
+
channel,
|
|
20597
|
+
at: 0,
|
|
20598
|
+
waiting: 0,
|
|
20599
|
+
appAhead: 0
|
|
20600
|
+
};
|
|
20601
|
+
return this.filesUse;
|
|
20602
|
+
}
|
|
20603
|
+
/**
|
|
20179
20604
|
* Resolves once the open session can take another files/3 data frame; undefined when it can now. A channel with a send budget (native and
|
|
20180
20605
|
* iroh ones throw past 120 KiB) gives files a quarter of it, so a ping, a message or any other frame still fits
|
|
20181
|
-
* while a file goes; others keep under the high water mark, as files/2 does.
|
|
20606
|
+
* while a file goes; others keep under the high water mark, as files/2 does. An app's data gets a quarter above that
|
|
20607
|
+
* line meanwhile (`sendAppData`).
|
|
20182
20608
|
*/
|
|
20183
20609
|
filesWritable() {
|
|
20184
20610
|
const channel = this.channel;
|
|
20185
20611
|
if (!channel) return;
|
|
20186
|
-
const cap = channel
|
|
20612
|
+
const cap = filesCap(channel);
|
|
20187
20613
|
if (channel.bufferedAmount < cap) return;
|
|
20614
|
+
const use = this.filesUsing(channel);
|
|
20615
|
+
use.waiting++;
|
|
20188
20616
|
return (async () => {
|
|
20189
|
-
|
|
20190
|
-
|
|
20191
|
-
|
|
20617
|
+
try {
|
|
20618
|
+
while (this.channel === channel && channel.bufferedAmount >= cap) {
|
|
20619
|
+
await channel.drained().catch(() => {});
|
|
20620
|
+
if (this.channel === channel && channel.bufferedAmount >= cap) await new Promise((resolve) => setTimeout(resolve, 20));
|
|
20621
|
+
}
|
|
20622
|
+
} finally {
|
|
20623
|
+
use.waiting--;
|
|
20624
|
+
use.at = Date.now();
|
|
20192
20625
|
}
|
|
20193
20626
|
})();
|
|
20194
20627
|
}
|
|
@@ -20486,13 +20919,20 @@ var GhostLink = class {
|
|
|
20486
20919
|
}
|
|
20487
20920
|
const held = this.channel;
|
|
20488
20921
|
if (held && this.activeBinding && !this.dialedIn.has(held) && Date.now() - this.channelSince < 1e4 && this.myPubKeyZ32 < this.options.params.peerPubKeyZ32) {
|
|
20489
|
-
|
|
20490
|
-
|
|
20491
|
-
|
|
20492
|
-
|
|
20493
|
-
|
|
20922
|
+
if (this.paired?.state.status !== "ready") {
|
|
20923
|
+
traceLink(this.myPubKeyZ32, "dial-crossed", {
|
|
20924
|
+
transport: binding.transport,
|
|
20925
|
+
held: true
|
|
20926
|
+
});
|
|
20927
|
+
channel.close();
|
|
20928
|
+
return;
|
|
20929
|
+
}
|
|
20930
|
+
this.probeCrossed(held, channel, binding);
|
|
20494
20931
|
return;
|
|
20495
20932
|
}
|
|
20933
|
+
this.replaceWith(channel, binding);
|
|
20934
|
+
}
|
|
20935
|
+
replaceWith(channel, binding) {
|
|
20496
20936
|
traceLink(this.myPubKeyZ32, "replacing", {
|
|
20497
20937
|
transport: binding.transport,
|
|
20498
20938
|
held: this.paired?.state.transport
|
|
@@ -20503,6 +20943,62 @@ var GhostLink = class {
|
|
|
20503
20943
|
});
|
|
20504
20944
|
}
|
|
20505
20945
|
/**
|
|
20946
|
+
* The contact dialled in while this side's own fresh dial holds the chat (`CROSSED_FRESH_MS`): the dials crossed, or
|
|
20947
|
+
* the contact's app ended since that dial and is back. The held session is pinged; heard from within
|
|
20948
|
+
* `CROSSED_PROBE_MS`, the contact holds this side's dial too and its own closes. Not heard from, the contact's dial
|
|
20949
|
+
* takes over.
|
|
20950
|
+
*/
|
|
20951
|
+
crossedProbe = null;
|
|
20952
|
+
/** Frames heard on the open session, counted from its start: whether the contact answered `probeCrossed`. */
|
|
20953
|
+
framesHeard = 0;
|
|
20954
|
+
probeCrossed(held, channel, binding) {
|
|
20955
|
+
this.dropCrossedProbe();
|
|
20956
|
+
traceLink(this.myPubKeyZ32, "dial-crossed", {
|
|
20957
|
+
transport: binding.transport,
|
|
20958
|
+
held: true,
|
|
20959
|
+
probe: true
|
|
20960
|
+
});
|
|
20961
|
+
const heard = this.framesHeard;
|
|
20962
|
+
try {
|
|
20963
|
+
this.sendPing(held, false);
|
|
20964
|
+
} catch {}
|
|
20965
|
+
const timer = setTimeout(() => {
|
|
20966
|
+
if (this.crossedProbe?.channel !== channel) return;
|
|
20967
|
+
this.crossedProbe = null;
|
|
20968
|
+
if (this.stopped || this.leaving || this.candidate) {
|
|
20969
|
+
channel.close();
|
|
20970
|
+
return;
|
|
20971
|
+
}
|
|
20972
|
+
if (!this.channel) {
|
|
20973
|
+
this.attach(channel, binding);
|
|
20974
|
+
return;
|
|
20975
|
+
}
|
|
20976
|
+
if (this.channel === held && this.framesHeard !== heard) {
|
|
20977
|
+
channel.close();
|
|
20978
|
+
return;
|
|
20979
|
+
}
|
|
20980
|
+
traceLink(this.myPubKeyZ32, "crossed-held-silent", { transport: binding.transport });
|
|
20981
|
+
this.replaceWith(channel, binding);
|
|
20982
|
+
}, CROSSED_PROBE_MS);
|
|
20983
|
+
channel.onClose = () => {
|
|
20984
|
+
if (this.crossedProbe?.channel === channel) {
|
|
20985
|
+
clearTimeout(timer);
|
|
20986
|
+
this.crossedProbe = null;
|
|
20987
|
+
}
|
|
20988
|
+
};
|
|
20989
|
+
this.crossedProbe = {
|
|
20990
|
+
channel,
|
|
20991
|
+
timer
|
|
20992
|
+
};
|
|
20993
|
+
}
|
|
20994
|
+
dropCrossedProbe() {
|
|
20995
|
+
const probe = this.crossedProbe;
|
|
20996
|
+
if (!probe) return;
|
|
20997
|
+
this.crossedProbe = null;
|
|
20998
|
+
clearTimeout(probe.timer);
|
|
20999
|
+
probe.channel.close();
|
|
21000
|
+
}
|
|
21001
|
+
/**
|
|
20506
21002
|
* Connections that came in on this side's native endpoints. Their address was in the capability record, which before
|
|
20507
21003
|
* the pin anyone holding a copy of the invite could read: a key other than the pinned one on such a connection proves
|
|
20508
21004
|
* no more than on the DHT, so it closes that connection and nothing else.
|
|
@@ -20697,7 +21193,18 @@ var GhostLink = class {
|
|
|
20697
21193
|
if (this.channel !== channel) return;
|
|
20698
21194
|
this.unansweredPings = 0;
|
|
20699
21195
|
this.heardFromPeer();
|
|
20700
|
-
|
|
21196
|
+
this.framesHeard++;
|
|
21197
|
+
if (typeof data !== "string") {
|
|
21198
|
+
const chunk = this.supportsBinaryFiles ? decodeFileData(data) : null;
|
|
21199
|
+
if (!chunk) return;
|
|
21200
|
+
this.activityAt = Date.now();
|
|
21201
|
+
await this.options.events?.onFilesFrame?.({
|
|
21202
|
+
t: "pf-data",
|
|
21203
|
+
...chunk
|
|
21204
|
+
});
|
|
21205
|
+
return;
|
|
21206
|
+
}
|
|
21207
|
+
if (data.length > 61440) return;
|
|
20701
21208
|
let frame;
|
|
20702
21209
|
try {
|
|
20703
21210
|
frame = JSON.parse(data);
|
|
@@ -25454,6 +25961,7 @@ function readFirstPlace(o, epoch) {
|
|
|
25454
25961
|
/** One of my own frames, among mine: `e:n`. */
|
|
25455
25962
|
var ownKey = (f) => `${f.e}:${f.n}`;
|
|
25456
25963
|
/** What a frame weighs in a log, roughly: its boxes plus the fixed fields around them. */
|
|
25964
|
+
var frameKey$1 = (f) => `${f.s}:${f.e}:${f.n}`;
|
|
25457
25965
|
/** What a kept frame counts for against `GROUP_LIMITS.relayBytes`. */
|
|
25458
25966
|
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;
|
|
25459
25967
|
/** Only the fields an edit frame has, as `clean` does for a message. */
|
|
@@ -25574,8 +26082,15 @@ var GroupSession = class {
|
|
|
25574
26082
|
pendingMeta;
|
|
25575
26083
|
/** While `handle` runs: the frames it took that a hub passes on (`handle`'s result). */
|
|
25576
26084
|
passOn = null;
|
|
26085
|
+
/** The frames kept to hand on that the app holds with its messages, oldest first; undefined when the state holds them all. */
|
|
26086
|
+
onRows;
|
|
25577
26087
|
/** Set while a save of what received messages changed is due (`persistSoon`). */
|
|
25578
26088
|
saveTimer;
|
|
26089
|
+
/**
|
|
26090
|
+
* The frames of members who left that each member was handed (`frameKey`), so an app that does not take them (from
|
|
26091
|
+
* before revision 2026-10-13, and never marks them seen) is not handed them again at every sync. In memory only.
|
|
26092
|
+
*/
|
|
26093
|
+
leaversHanded = /* @__PURE__ */ new Map();
|
|
25579
26094
|
/** Syncs answered per member, a few a minute (`GROUP_LIMITS.syncAnswers`). In memory only. */
|
|
25580
26095
|
syncsAnswered = /* @__PURE__ */ new Map();
|
|
25581
26096
|
/** Catch-up answers still going out, a slice at a time (`handOut`). */
|
|
@@ -25588,10 +26103,21 @@ var GroupSession = class {
|
|
|
25588
26103
|
provisional;
|
|
25589
26104
|
/** When my chain last moved (a commit made or applied), by `hooks.clock`: frames are said again once it has settled. */
|
|
25590
26105
|
chainMovedAt;
|
|
25591
|
-
|
|
26106
|
+
/**
|
|
26107
|
+
* `onRows`: the frames the app holds with the messages it stored (`GroupIncomingMessage.frame`), oldest first. Given
|
|
26108
|
+
* (empty at first), the app is where frames are kept: the saved state carries none of them, and a message received
|
|
26109
|
+
* no longer writes them all again. Absent: they are kept in the state, as before.
|
|
26110
|
+
*/
|
|
26111
|
+
constructor(state, hooks, onRows) {
|
|
25592
26112
|
this.hooks = hooks;
|
|
25593
26113
|
this.state = state;
|
|
25594
26114
|
this.identity = identityFromSeedB64(state.seedB64);
|
|
26115
|
+
if (onRows) {
|
|
26116
|
+
const held = new Set(onRows.map(frameKey$1));
|
|
26117
|
+
if (state.relay?.length) state.relay = state.relay.filter((f) => !held.has(frameKey$1(f)));
|
|
26118
|
+
this.onRows = state.status === "active" ? onRows.filter((f) => this.handsOn(f.s, f.e)) : [];
|
|
26119
|
+
this.trimRelay();
|
|
26120
|
+
}
|
|
25595
26121
|
this.answers = new CatchUpAnswers({
|
|
25596
26122
|
send: (to, frame) => hooks.send(to, frame),
|
|
25597
26123
|
...hooks.handled && { handled: (to) => hooks.handled(to) },
|
|
@@ -26003,9 +26529,13 @@ var GroupSession = class {
|
|
|
26003
26529
|
this.hooks.changed();
|
|
26004
26530
|
return { secret };
|
|
26005
26531
|
}
|
|
26006
|
-
/**
|
|
26532
|
+
/**
|
|
26533
|
+
* Someone removed is not handed on any more, whatever they sent while in the roster; one who left still is, for the
|
|
26534
|
+
* epochs before its leave (WISP 902 § Catch-up).
|
|
26535
|
+
*/
|
|
26007
26536
|
pruneRelay() {
|
|
26008
|
-
if (this.state.relay?.length) this.state.relay = this.state.relay.filter((f) =>
|
|
26537
|
+
if (this.state.relay?.length) this.state.relay = this.state.relay.filter((f) => this.handsOn(f.s, f.e));
|
|
26538
|
+
if (this.onRows?.length) this.onRows = this.onRows.filter((f) => this.handsOn(f.s, f.e));
|
|
26009
26539
|
if (this.state.relayEdits?.length) this.state.relayEdits = this.state.relayEdits.filter((f) => rosterHas(this.roster, f.s));
|
|
26010
26540
|
}
|
|
26011
26541
|
/** Another member's edit, kept to hand on: the latest per message; the oldest go once the log is full. False: one as new was kept. */
|
|
@@ -26021,10 +26551,21 @@ var GroupSession = class {
|
|
|
26021
26551
|
}
|
|
26022
26552
|
/** A frame of another member, kept to hand on; the oldest go once the log is full. */
|
|
26023
26553
|
keep(frame) {
|
|
26024
|
-
|
|
26025
|
-
|
|
26026
|
-
|
|
26027
|
-
|
|
26554
|
+
if (this.onRows) {
|
|
26555
|
+
const key = frameKey$1(frame);
|
|
26556
|
+
if (this.onRows.some((f) => frameKey$1(f) === key)) return;
|
|
26557
|
+
this.onRows.push(frame);
|
|
26558
|
+
} else (this.state.relay ??= []).push(frame);
|
|
26559
|
+
this.trimRelay();
|
|
26560
|
+
}
|
|
26561
|
+
/** The frames kept to hand on, oldest first: the state's, then the ones the app holds with its messages. */
|
|
26562
|
+
kept() {
|
|
26563
|
+
return this.onRows ? [...this.state.relay ?? [], ...this.onRows] : this.state.relay ?? [];
|
|
26564
|
+
}
|
|
26565
|
+
trimRelay() {
|
|
26566
|
+
const old = this.state.relay ?? [], rows = this.onRows ?? [];
|
|
26567
|
+
let bytes = old.reduce((sum, f) => sum + frameBytes(f), 0) + rows.reduce((sum, f) => sum + frameBytes(f), 0);
|
|
26568
|
+
while (old.length + rows.length > GROUP_LIMITS.relay || bytes > GROUP_LIMITS.relayBytes) bytes -= frameBytes((old.length ? old : rows).shift());
|
|
26028
26569
|
}
|
|
26029
26570
|
pruneSecrets() {
|
|
26030
26571
|
const epochs = Object.keys(this.state.secrets).map(Number).sort((a, b) => a - b);
|
|
@@ -26060,6 +26601,7 @@ var GroupSession = class {
|
|
|
26060
26601
|
this.state.secrets = {};
|
|
26061
26602
|
this.state.sent = [];
|
|
26062
26603
|
this.state.relay = [];
|
|
26604
|
+
if (this.onRows) this.onRows = [];
|
|
26063
26605
|
this.state.relayEdits = [];
|
|
26064
26606
|
this.state.alone = [];
|
|
26065
26607
|
this.waiting = [];
|
|
@@ -26548,7 +27090,8 @@ var GroupSession = class {
|
|
|
26548
27090
|
}
|
|
26549
27091
|
/**
|
|
26550
27092
|
* A message, from its author's edge or handed on by another member. Handed on, it is taken only while both are in
|
|
26551
|
-
* the roster (someone removed is neither heard from nor handed on, #300),
|
|
27093
|
+
* the roster (someone removed is neither heard from nor handed on, #300), or from a member in it when its author left
|
|
27094
|
+
* after it (`handsOn`), and its boxes only with the author's `xs`.
|
|
26552
27095
|
*/
|
|
26553
27096
|
async receiveMessage(from, raw) {
|
|
26554
27097
|
if (!isMessageFrame$1(raw)) return;
|
|
@@ -26557,7 +27100,7 @@ var GroupSession = class {
|
|
|
26557
27100
|
return;
|
|
26558
27101
|
}
|
|
26559
27102
|
const relayed = raw.s !== from;
|
|
26560
|
-
if (relayed && (!rosterHas(this.roster, from) || !
|
|
27103
|
+
if (relayed && (!rosterHas(this.roster, from) || !this.handsOn(raw.s, raw.e))) return;
|
|
26561
27104
|
if (!verify(fromBase64Url(raw.sig), messageSigned$1(raw), publicKeyFromZ32(raw.s))) return;
|
|
26562
27105
|
if (raw.e > this.epoch) {
|
|
26563
27106
|
this.park(from, raw);
|
|
@@ -26595,10 +27138,11 @@ var GroupSession = class {
|
|
|
26595
27138
|
n: first.n
|
|
26596
27139
|
};
|
|
26597
27140
|
if (firstFrame && rosterHas(this.state.chain[first.e]?.m ?? [], this.myKey) && this.isDuplicate(firstFrame)) {
|
|
27141
|
+
await this.hooks.kept?.(groupMessageId(raw.s, first.e, first.n), clean(raw));
|
|
26598
27142
|
this.markSeen(raw);
|
|
26599
27143
|
this.keep(clean(raw));
|
|
26600
27144
|
this.persistSoon();
|
|
26601
|
-
if (
|
|
27145
|
+
if (this.handsOn(raw.s, raw.e)) this.took(clean(raw));
|
|
26602
27146
|
return;
|
|
26603
27147
|
}
|
|
26604
27148
|
const id = first && !this.provisional.has(own) ? groupMessageId(raw.s, first.e, first.n) : own;
|
|
@@ -26635,7 +27179,7 @@ var GroupSession = class {
|
|
|
26635
27179
|
if (firstFrame && rosterHas(this.state.chain[firstFrame.e]?.m ?? [], this.myKey)) this.markSeen(firstFrame);
|
|
26636
27180
|
this.keep(clean(raw));
|
|
26637
27181
|
this.persistSoon();
|
|
26638
|
-
if (
|
|
27182
|
+
if (this.handsOn(raw.s, raw.e)) this.took(clean(raw));
|
|
26639
27183
|
}
|
|
26640
27184
|
/**
|
|
26641
27185
|
* An edit of a member's own message, from its author's edge or handed on by another member: believed only while its
|
|
@@ -26669,7 +27213,7 @@ var GroupSession = class {
|
|
|
26669
27213
|
const k = validMentions(body.m, body.text, rosterAdmin(commit.m) === raw.s && this.admin === raw.s);
|
|
26670
27214
|
const sc = body.sc === void 0 ? void 0 : readStatusCard(body.sc);
|
|
26671
27215
|
if (!validEditNumber(raw.v) && !sc) return;
|
|
26672
|
-
const copy =
|
|
27216
|
+
const copy = this.kept().find((f) => f.s === raw.s && f.e === raw.e && f.n === raw.n);
|
|
26673
27217
|
const first = copy && readFirstPlace(copy.o, copy.e);
|
|
26674
27218
|
await this.hooks.edit?.({
|
|
26675
27219
|
id: first ? groupMessageId(raw.s, first.e, first.n) : groupMessageId(raw.s, raw.e, raw.n),
|
|
@@ -26830,6 +27374,16 @@ var GroupSession = class {
|
|
|
26830
27374
|
await this.receiveCommit(from, next);
|
|
26831
27375
|
}
|
|
26832
27376
|
}
|
|
27377
|
+
/**
|
|
27378
|
+
* Whether a frame of `author` under `epoch` is taken from another member, kept and handed on: while its author is in
|
|
27379
|
+
* the roster, or when it left (its removal marked `left`, and it is not back in) and the frame is from before that.
|
|
27380
|
+
*/
|
|
27381
|
+
handsOn(author, epoch) {
|
|
27382
|
+
if (rosterHas(this.roster, author)) return true;
|
|
27383
|
+
const chain = this.state.chain;
|
|
27384
|
+
for (let e = chain.length - 1; e > 0; e--) if (chain[e].k === "remove" && chain[e].s === author) return !!this.state.leftMarks?.[e] && epoch < e;
|
|
27385
|
+
return false;
|
|
27386
|
+
}
|
|
26833
27387
|
/** Keeps the admin's mark on a commit that took out a member who left (`GroupCommitFrame.left`) when it holds; true when new. */
|
|
26834
27388
|
takeLeftMark(commit, mark) {
|
|
26835
27389
|
if (commit.k !== "remove" || typeof mark !== "string" || mark.length !== 86 || !B64$1.test(mark) || this.state.leftMarks?.[commit.e]) return false;
|
|
@@ -26924,7 +27478,15 @@ var GroupSession = class {
|
|
|
26924
27478
|
});
|
|
26925
27479
|
const lacks = missingIn(frame, from);
|
|
26926
27480
|
const asked = new Set(Array.isArray(frame.ask) ? frame.ask.filter((k) => typeof k === "string" && k !== from && k !== this.myKey && rosterHas(this.roster, k)) : []);
|
|
26927
|
-
const
|
|
27481
|
+
const handed = this.leaversHanded.get(from) ?? /* @__PURE__ */ new Set();
|
|
27482
|
+
const kept = !asked.size && !this.state.leftMarks ? [] : this.kept().filter((f) => {
|
|
27483
|
+
if (!this.state.chain[f.e] || !rosterHas(this.state.chain[f.e].m, from) || !lacks(f)) return false;
|
|
27484
|
+
if (asked.has(f.s)) return true;
|
|
27485
|
+
if (rosterHas(this.roster, f.s) || handed.has(frameKey$1(f)) || !this.handsOn(f.s, f.e)) return false;
|
|
27486
|
+
handed.add(frameKey$1(f));
|
|
27487
|
+
return true;
|
|
27488
|
+
});
|
|
27489
|
+
if (handed.size) this.leaversHanded.set(from, handed);
|
|
26928
27490
|
answer.push(...asWritten(this.ownLacked(from, frame), kept));
|
|
26929
27491
|
for (const author of asked) {
|
|
26930
27492
|
const edits = (this.state.relayEdits ?? []).filter((f) => f.s === author && this.state.chain[f.e] && rosterHas(this.state.chain[f.e].m, from));
|
|
@@ -27320,6 +27882,8 @@ var COMMUNITY_LIMITS = {
|
|
|
27320
27882
|
/** Commits in one welcome or chain piece. */
|
|
27321
27883
|
chainPiece: 100,
|
|
27322
27884
|
pendingLeaves: 64,
|
|
27885
|
+
/** Leaves whose mark (`group-left`) is kept, and the epochs one names. */
|
|
27886
|
+
leftMarks: 64,
|
|
27323
27887
|
/**
|
|
27324
27888
|
* Syncs of one member answered in `syncWindowMs`: each answer may carry the chain's tail, secrets and the whole
|
|
27325
27889
|
* store. A member catching up asks every five seconds at most, and every member syncs every thirty; one asking more
|
|
@@ -27436,6 +28000,14 @@ var leaveStatement = (g, s) => utf8Encode(JSON.stringify([
|
|
|
27436
28000
|
g,
|
|
27437
28001
|
s
|
|
27438
28002
|
]));
|
|
28003
|
+
/** What the member who committed a leave (`h`) had heard of the leaver then: `e:h16` → its highest number there. */
|
|
28004
|
+
var leftStatement = (g, h, hi) => utf8Encode(JSON.stringify([
|
|
28005
|
+
"ghostly-group/2 left",
|
|
28006
|
+
g,
|
|
28007
|
+
h,
|
|
28008
|
+
Object.entries(hi).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)
|
|
28009
|
+
]));
|
|
28010
|
+
var LEFT_EPOCH = /^\d{1,15}:[a-f0-9]{16}$/;
|
|
27439
28011
|
/** The secret of an epoch that adds, re-roles or re-links: from the previous one and the commit. */
|
|
27440
28012
|
function derivedSecret(previous, untaggedHash) {
|
|
27441
28013
|
return hkdf(sha256, previous, utf8Encode(untaggedHash), utf8Encode("ghostly-group/2 derived"), 32);
|
|
@@ -27899,13 +28471,58 @@ var CommunitySession = class {
|
|
|
27899
28471
|
return -1;
|
|
27900
28472
|
}
|
|
27901
28473
|
/**
|
|
27902
|
-
*
|
|
27903
|
-
*
|
|
27904
|
-
*
|
|
28474
|
+
* Whether a frame of someone the chain took out is still heard. A member removed is heard no more, even for an epoch
|
|
28475
|
+
* it was in. What one who left wrote while in the group is taken and handed on, its goodbye included, up to what the
|
|
28476
|
+
* member who committed the leave had heard of it then (its `group-left` mark), never past it: the leaver still holds
|
|
28477
|
+
* those epochs' secrets, and anything it signs later is not heard (WISP 903 § Leaving and removal). No mark here
|
|
28478
|
+
* (one not come yet, or a committer's app before it) and nothing of it is heard.
|
|
27905
28479
|
*/
|
|
27906
|
-
|
|
28480
|
+
heardAfterOut(f) {
|
|
28481
|
+
const i = this.outIndex(f.s);
|
|
28482
|
+
if (i < 0) return true;
|
|
28483
|
+
const commit = this.state.chain[i];
|
|
28484
|
+
if (commit.k !== "leave") return false;
|
|
28485
|
+
const high = this.state.left?.[communityCommitHash(commit)]?.hi[seenKey(f.e, f.h)];
|
|
28486
|
+
return high !== void 0 && f.n <= high;
|
|
28487
|
+
}
|
|
28488
|
+
/** The mark of a leave on my branch, as a frame to hand on (see `heardAfterOut`). */
|
|
28489
|
+
leftFrame(key) {
|
|
27907
28490
|
const i = this.outIndex(key);
|
|
27908
|
-
|
|
28491
|
+
if (i < 0 || this.state.chain[i].k !== "leave") return void 0;
|
|
28492
|
+
const h = communityCommitHash(this.state.chain[i]), mark = this.state.left?.[h];
|
|
28493
|
+
return mark && {
|
|
28494
|
+
t: "group-left",
|
|
28495
|
+
v: 2,
|
|
28496
|
+
g: this.id,
|
|
28497
|
+
h,
|
|
28498
|
+
hi: mark.hi,
|
|
28499
|
+
sig: mark.sig
|
|
28500
|
+
};
|
|
28501
|
+
}
|
|
28502
|
+
keepLeft(h, mark) {
|
|
28503
|
+
const left = { ...this.state.left ?? {} };
|
|
28504
|
+
delete left[h];
|
|
28505
|
+
left[h] = mark;
|
|
28506
|
+
const keys = Object.keys(left);
|
|
28507
|
+
for (const old of keys.slice(0, Math.max(0, keys.length - COMMUNITY_LIMITS.leftMarks))) delete left[old];
|
|
28508
|
+
this.state.left = left;
|
|
28509
|
+
}
|
|
28510
|
+
/** A leave's mark, from the member who committed it: kept when it is that member's, for a leave I hold. */
|
|
28511
|
+
receiveLeft(frame) {
|
|
28512
|
+
const { h, hi, sig } = frame;
|
|
28513
|
+
if (typeof h !== "string" || !HASH.test(h) || typeof sig !== "string" || !SIG.test(sig) || !isObject$2(hi)) return false;
|
|
28514
|
+
const entries = Object.entries(hi);
|
|
28515
|
+
if (entries.length > COMMUNITY_LIMITS.leftMarks || entries.some(([k, n]) => !LEFT_EPOCH.test(k) || !Number.isSafeInteger(n) || n < 0)) return false;
|
|
28516
|
+
const commit = this.known.get(h);
|
|
28517
|
+
if (!commit || commit.k !== "leave" || this.state.left?.[h]?.sig === sig) return false;
|
|
28518
|
+
const marks = hi;
|
|
28519
|
+
if (!verify(fromBase64Url(sig), leftStatement(this.id, h, marks), publicKeyFromZ32(commit.by))) return false;
|
|
28520
|
+
this.keepLeft(h, {
|
|
28521
|
+
hi: marks,
|
|
28522
|
+
sig
|
|
28523
|
+
});
|
|
28524
|
+
this.persistSoon();
|
|
28525
|
+
return true;
|
|
27909
28526
|
}
|
|
27910
28527
|
/**
|
|
27911
28528
|
* What someone the chain took out is told when it comes back (WISP 903 § Leaving and removal): the commits from where
|
|
@@ -28507,10 +29124,11 @@ var CommunitySession = class {
|
|
|
28507
29124
|
continue;
|
|
28508
29125
|
}
|
|
28509
29126
|
try {
|
|
28510
|
-
await this.commit("leave", {
|
|
29127
|
+
const { hash } = await this.commit("leave", {
|
|
28511
29128
|
s: request.s,
|
|
28512
29129
|
ls: request.ls
|
|
28513
29130
|
}, now);
|
|
29131
|
+
this.markLeft(hash, request.s);
|
|
28514
29132
|
done++;
|
|
28515
29133
|
} catch {}
|
|
28516
29134
|
this.state.pendingLeaves = this.state.pendingLeaves.filter((r) => r !== request);
|
|
@@ -28519,6 +29137,25 @@ var CommunitySession = class {
|
|
|
28519
29137
|
return done;
|
|
28520
29138
|
});
|
|
28521
29139
|
}
|
|
29140
|
+
/** Signs and says how far I had heard a member whose leave I just committed (`group-left`). */
|
|
29141
|
+
markLeft(h, leaver) {
|
|
29142
|
+
const hi = {};
|
|
29143
|
+
const heard = Object.entries(this.state.seen[leaver] ?? {}).filter(([k]) => LEFT_EPOCH.test(k)).slice(-COMMUNITY_LIMITS.leftMarks);
|
|
29144
|
+
for (const [k, entry] of heard) hi[k] = entry.high;
|
|
29145
|
+
const sig = toBase64Url(sign(leftStatement(this.id, h, hi), this.identity.seed));
|
|
29146
|
+
this.keepLeft(h, {
|
|
29147
|
+
hi,
|
|
29148
|
+
sig
|
|
29149
|
+
});
|
|
29150
|
+
this.hooks.broadcast({
|
|
29151
|
+
t: "group-left",
|
|
29152
|
+
v: 2,
|
|
29153
|
+
g: this.id,
|
|
29154
|
+
h,
|
|
29155
|
+
hi,
|
|
29156
|
+
sig
|
|
29157
|
+
});
|
|
29158
|
+
}
|
|
28522
29159
|
out(status, reason) {
|
|
28523
29160
|
this.state.status = status;
|
|
28524
29161
|
this.state.statusReason = reason;
|
|
@@ -28780,8 +29417,13 @@ var CommunitySession = class {
|
|
|
28780
29417
|
messageIdOf(f) {
|
|
28781
29418
|
return f.s === this.myKey && this.resealedFrom.get(frameKey(f)) || communityMessageId(f.s, f.e, f.h, f.n);
|
|
28782
29419
|
}
|
|
28783
|
-
/**
|
|
28784
|
-
|
|
29420
|
+
/**
|
|
29421
|
+
* Where I am and what I have, for the member at the other end of an edge that just opened. `to`: that member, now
|
|
29422
|
+
* asked: my answer to its own sync asks nothing more for a while. A second sync went, and its answer took the place of
|
|
29423
|
+
* what was left of the first from its start: a member back after 256 messages was handed 288 (2026-10-14).
|
|
29424
|
+
*/
|
|
29425
|
+
syncFrame(to) {
|
|
29426
|
+
if (to) this.asked.set(to, this.hooks.clock?.() ?? Date.now());
|
|
28785
29427
|
const have = {};
|
|
28786
29428
|
const recent = new Set(this.state.store.map((f) => seenKey(f.e, f.h)));
|
|
28787
29429
|
for (const [sender, byKey] of Object.entries(this.state.seen)) for (const [key, entry] of Object.entries(byKey)) {
|
|
@@ -28842,6 +29484,7 @@ var CommunitySession = class {
|
|
|
28842
29484
|
return false;
|
|
28843
29485
|
case "group-meta": return this.receiveMeta(from, raw);
|
|
28844
29486
|
case "group-leave": return this.receiveLeave(frame);
|
|
29487
|
+
case "group-left": return this.receiveLeft(frame);
|
|
28845
29488
|
case "group-entry": {
|
|
28846
29489
|
if (frame.to !== this.myKey || !isSealed(frame.s) || frame.x !== this.currentEntryKey()) return false;
|
|
28847
29490
|
const seed = openSecret(this.identity.seed, this.myKey, frame.s, entryAad(this.id, this.myKey));
|
|
@@ -28870,7 +29513,7 @@ var CommunitySession = class {
|
|
|
28870
29513
|
return false;
|
|
28871
29514
|
}
|
|
28872
29515
|
if (!verify(fromBase64Url(raw.sig), messageSigned(raw), publicKeyFromZ32(raw.s))) return false;
|
|
28873
|
-
if (this.
|
|
29516
|
+
if (!this.heardAfterOut(raw)) return false;
|
|
28874
29517
|
const found = this.commitByShort(raw.e, raw.h);
|
|
28875
29518
|
if (!found) {
|
|
28876
29519
|
this.park(from, raw);
|
|
@@ -29110,16 +29753,22 @@ var CommunitySession = class {
|
|
|
29110
29753
|
const have = frame.have && typeof frame.have === "object" ? frame.have : {};
|
|
29111
29754
|
const handed = /* @__PURE__ */ new Set();
|
|
29112
29755
|
const unheard = new Set(this.state.unheard ?? []);
|
|
29756
|
+
const marked = /* @__PURE__ */ new Set();
|
|
29113
29757
|
for (const stored of this.state.store) {
|
|
29114
29758
|
if (stored.s === from) continue;
|
|
29115
29759
|
const high = have[stored.s]?.[seenKey(stored.e, stored.h)];
|
|
29116
29760
|
if (Number.isSafeInteger(high) && high >= stored.n) continue;
|
|
29117
|
-
if (this.
|
|
29761
|
+
if (!this.heardAfterOut(stored)) continue;
|
|
29118
29762
|
const found = this.commitByShort(stored.e, stored.h);
|
|
29119
29763
|
if (!found) continue;
|
|
29120
29764
|
const reads = rosterHas(this.rosterAt(found.hash) ?? [], from), mine = stored.s === this.myKey && unheard.has(frameKey(stored));
|
|
29121
29765
|
if (!reads && !(mine && rosterHas(this.roster, from))) continue;
|
|
29122
29766
|
if (!this.mainIndex.has(found.hash)) this.handSide(from, found.hash, handed, answer);
|
|
29767
|
+
if (!rosterHas(this.roster, stored.s) && !marked.has(stored.s)) {
|
|
29768
|
+
marked.add(stored.s);
|
|
29769
|
+
const left = this.leftFrame(stored.s);
|
|
29770
|
+
if (left) answer.push(left);
|
|
29771
|
+
}
|
|
29123
29772
|
answer.push(stored);
|
|
29124
29773
|
if (mine && reads) unheard.delete(frameKey(stored));
|
|
29125
29774
|
}
|
|
@@ -29781,8 +30430,63 @@ function appDigest(manifestBytes) {
|
|
|
29781
30430
|
return toBase64Url(sha256(manifestBytes));
|
|
29782
30431
|
}
|
|
29783
30432
|
var u32 = (b, at) => (b[at] << 24 >>> 0) + (b[at + 1] << 16) + (b[at + 2] << 8) + b[at + 3];
|
|
29784
|
-
/**
|
|
29785
|
-
|
|
30433
|
+
/**
|
|
30434
|
+
* Lets the thread go for one turn: what waits (input, drawing, timers, port messages, IndexedDB answers) runs, then the
|
|
30435
|
+
* caller goes on. A message to itself, queued behind the page's other tasks. Never `scheduler.yield()`: in Chromium its
|
|
30436
|
+
* continuation outranks every normal task, so a paced read of 4096 revocations held the page's timers, its apps' broker
|
|
30437
|
+
* ports and IndexedDB for the whole 1.8 s. Never `setTimeout(0)` but as the last resort: from the fifth nested timer on
|
|
30438
|
+
* the browser waits at least 4 ms (HTML's clamp), which made that read take 60% longer than the work, and a hidden tab
|
|
30439
|
+
* runs timers once a second.
|
|
30440
|
+
*/
|
|
30441
|
+
function yieldThread() {
|
|
30442
|
+
if (typeof MessageChannel === "function") return new Promise((resolve) => {
|
|
30443
|
+
const channel = new MessageChannel();
|
|
30444
|
+
channel.port1.onmessage = () => {
|
|
30445
|
+
channel.port1.close();
|
|
30446
|
+
resolve();
|
|
30447
|
+
};
|
|
30448
|
+
channel.port2.postMessage(null);
|
|
30449
|
+
});
|
|
30450
|
+
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
30451
|
+
}
|
|
30452
|
+
/**
|
|
30453
|
+
* A pause for a loop of short steps: `await pause()` after each step lets the thread go once `sliceMs` of work has
|
|
30454
|
+
* run since the last time, and answers at once otherwise.
|
|
30455
|
+
*/
|
|
30456
|
+
function pacer(sliceMs) {
|
|
30457
|
+
let since = performance.now();
|
|
30458
|
+
return () => {
|
|
30459
|
+
if (performance.now() - since < sliceMs) return void 0;
|
|
30460
|
+
return yieldThread().then(() => {
|
|
30461
|
+
since = performance.now();
|
|
30462
|
+
});
|
|
30463
|
+
};
|
|
30464
|
+
}
|
|
30465
|
+
/** `sha256Paced` hashes up to this much at once: under a millisecond of the thread. */
|
|
30466
|
+
var HASH_AT_ONCE = 65536;
|
|
30467
|
+
/** Without `crypto.subtle`, `sha256Paced` hashes this much (a few ms), then lets the thread go. */
|
|
30468
|
+
var HASH_SLICE = 262144;
|
|
30469
|
+
/** `readAppBundleAsync` hashes files for this long at most before it lets the thread go, however small each file. */
|
|
30470
|
+
var HASH_SLICE_MS = 5;
|
|
30471
|
+
/**
|
|
30472
|
+
* SHA-256 of bytes that may be a whole bundle, for a client whose engine shares its page's thread: `sha256` in
|
|
30473
|
+
* JavaScript takes about 12 ms per MiB on a desktop CPU, so 16 MiB in one run is a page that draws nothing and takes no
|
|
30474
|
+
* input for 200 ms. With `crypto.subtle` the bytes are hashed off the thread; without it (a page not served over
|
|
30475
|
+
* HTTPS), a slice at a time with a pause between slices. The bytes must not change until it answers.
|
|
30476
|
+
*/
|
|
30477
|
+
async function sha256Paced(bytes) {
|
|
30478
|
+
if (bytes.length <= HASH_AT_ONCE) return sha256(bytes);
|
|
30479
|
+
const subtle = globalThis.crypto?.subtle;
|
|
30480
|
+
const away = subtle ? await subtle.digest("SHA-256", bytes).catch(() => null) : null;
|
|
30481
|
+
if (away) return new Uint8Array(away);
|
|
30482
|
+
const hash = sha256.create();
|
|
30483
|
+
for (let at = 0; at < bytes.length; at += HASH_SLICE) {
|
|
30484
|
+
if (at) await yieldThread();
|
|
30485
|
+
hash.update(bytes.subarray(at, at + HASH_SLICE));
|
|
30486
|
+
}
|
|
30487
|
+
return hash.digest();
|
|
30488
|
+
}
|
|
30489
|
+
function readAppBundleHead(bytes) {
|
|
29786
30490
|
if (bytes.length > APP_BUNDLE_LIMITS.bundleBytes) return {
|
|
29787
30491
|
ok: false,
|
|
29788
30492
|
reason: "too-large"
|
|
@@ -29841,17 +30545,18 @@ function readAppBundle(bytes) {
|
|
|
29841
30545
|
};
|
|
29842
30546
|
const signed = verifyAppSignature(APP_PREFIXES.app, manifestBytes, statement.signature, manifest.publisher);
|
|
29843
30547
|
if (!signed.ok) return signed;
|
|
29844
|
-
|
|
29845
|
-
|
|
29846
|
-
|
|
29847
|
-
|
|
29848
|
-
|
|
29849
|
-
|
|
29850
|
-
|
|
29851
|
-
|
|
29852
|
-
|
|
29853
|
-
|
|
29854
|
-
|
|
30548
|
+
return {
|
|
30549
|
+
ok: true,
|
|
30550
|
+
head: {
|
|
30551
|
+
manifest,
|
|
30552
|
+
manifestBytes,
|
|
30553
|
+
signature: statement.signature,
|
|
30554
|
+
filesAt: at
|
|
30555
|
+
}
|
|
30556
|
+
};
|
|
30557
|
+
}
|
|
30558
|
+
/** The last check, once every file's hash held: the icon. */
|
|
30559
|
+
function closeAppBundle({ manifest, manifestBytes, signature }, files) {
|
|
29855
30560
|
const icon = files.get(APP_ICON_PATH);
|
|
29856
30561
|
if (icon) {
|
|
29857
30562
|
const size = imageSize(icon);
|
|
@@ -29872,11 +30577,54 @@ function readAppBundle(bytes) {
|
|
|
29872
30577
|
manifest,
|
|
29873
30578
|
manifestBytes,
|
|
29874
30579
|
digest: appDigest(manifestBytes),
|
|
29875
|
-
signature
|
|
30580
|
+
signature,
|
|
29876
30581
|
files
|
|
29877
30582
|
}
|
|
29878
30583
|
};
|
|
29879
30584
|
}
|
|
30585
|
+
/** Reads and checks a bundle (the order of the checks is part of the format: see above). */
|
|
30586
|
+
function readAppBundle(bytes) {
|
|
30587
|
+
const read = readAppBundleHead(bytes);
|
|
30588
|
+
if (!read.ok) return read;
|
|
30589
|
+
const files = /* @__PURE__ */ new Map();
|
|
30590
|
+
let at = read.head.filesAt;
|
|
30591
|
+
for (const f of read.head.manifest.files) {
|
|
30592
|
+
const content = bytes.subarray(at, at + f.size);
|
|
30593
|
+
at += f.size;
|
|
30594
|
+
if (toBase64Url(sha256(content)) !== f.sha256) return {
|
|
30595
|
+
ok: false,
|
|
30596
|
+
reason: "file-hash",
|
|
30597
|
+
detail: f.path
|
|
30598
|
+
};
|
|
30599
|
+
files.set(f.path, content);
|
|
30600
|
+
}
|
|
30601
|
+
return closeAppBundle(read.head, files);
|
|
30602
|
+
}
|
|
30603
|
+
/**
|
|
30604
|
+
* `readAppBundle` for a client whose engine shares its page's thread: the same checks in the same order, with the same
|
|
30605
|
+
* refusals, and each file hashed by `sha256Paced`, one after the other, with the thread let go between files every
|
|
30606
|
+
* `HASH_SLICE_MS` (64 files of 64 KiB, each hashed at once, held it for 50 ms). The bytes must not change until it
|
|
30607
|
+
* answers.
|
|
30608
|
+
*/
|
|
30609
|
+
async function readAppBundleAsync(bytes) {
|
|
30610
|
+
const read = readAppBundleHead(bytes);
|
|
30611
|
+
if (!read.ok) return read;
|
|
30612
|
+
const files = /* @__PURE__ */ new Map();
|
|
30613
|
+
const pause = pacer(HASH_SLICE_MS);
|
|
30614
|
+
let at = read.head.filesAt;
|
|
30615
|
+
for (const f of read.head.manifest.files) {
|
|
30616
|
+
const content = bytes.subarray(at, at + f.size);
|
|
30617
|
+
at += f.size;
|
|
30618
|
+
if (toBase64Url(await sha256Paced(content)) !== f.sha256) return {
|
|
30619
|
+
ok: false,
|
|
30620
|
+
reason: "file-hash",
|
|
30621
|
+
detail: f.path
|
|
30622
|
+
};
|
|
30623
|
+
files.set(f.path, content);
|
|
30624
|
+
await pause();
|
|
30625
|
+
}
|
|
30626
|
+
return closeAppBundle(read.head, files);
|
|
30627
|
+
}
|
|
29880
30628
|
/** Puts a bundle's parts together, checking nothing: for builders, and for tests that need a broken bundle. */
|
|
29881
30629
|
function assembleAppBundle(manifestBytes, statementBytes, files) {
|
|
29882
30630
|
const total = MAGIC.length + 8 + manifestBytes.length + statementBytes.length + files.reduce((s, f) => s + f.length, 0);
|
|
@@ -30238,6 +30986,6 @@ function appStoreDecision(held, read) {
|
|
|
30238
30986
|
return read.digest === held.digest ? "same" : "equivocation";
|
|
30239
30987
|
}
|
|
30240
30988
|
//#endregion
|
|
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 };
|
|
30989
|
+
export { COMMUNITY_EDIT_FRAME as $, cardUrl as $a, budgetRetryMs as $c, PeerProofs as $i, GROUP_WANT_FRAME as $n, HTTP_SERVICE_PROTO as $o, deviceWakeRequest as $r, enrollCancelFrame as $s, didDhtFromPublicKey as $t, CommunitySession as A, COMMUNITY_REACTION_FRAME as Aa, TURN_PUT_WINDOW_MS as Ac, normalizeDomain as Ai, pubkyProofPath as An, DirectPathWatch as Ao, rankHubs as Ar, handoffStreamKey as As, verifyAtprotoRecordProof as At, knockRecords as B, wireReaction as Ba, signTurnPacket as Bc, IDENTITY_MAX_EVIDENCE as Bi, isFederationId as Bn, LIMITS as Bo, TYPING_REFRESH_MS as Br, readHandoffVerified as Bs, didWebDocumentUrl as Bt, sha256Paced as C, EDIT_BUFFER_MS as Ca, deviceEchoNonce as Cc, decodeDnsResponse as Ci, PUBKY_PROOF_MAX_BYTES as Cn, GROUP_MEMBER_CAP as Co, freshHubs as Cr, handoffEphemeral as Cs, atprotoProofRkey as Ct, openGroupTyping as D, parseLinkPreview as Da, newDeviceSetSecret as Dc, encodeDnsQuery as Di, normalizePubkyKey as Dn, rosterHas as Do, mergeLobby as Dr, handoffPakeFrame as Ds, parseAtprotoDidDocument as Dt, groupTypingEpoch as E, dhtEditId as Ea, devicePingFrame as Ec, domainWellKnownFile as Ei, newPubkyProofFolder as En, commitHash as Eo, mergeBeacon as Er, handoffOfferFrame as Es, normalizeAtprotoHandle as Et, KNOCK_TTL_MS as F, queueReaction as Fa, TURN_LAST_SEQUENCE as Fc, wellKnownUrl as Fi, decodeBolt11 as Fn, presenceSeenAt as Fo, ChatFiles as Fr, readHandoffDone as Fs, checkDidJws as Ft, groupWakeFrame as G, ITEM_STATES as Ga, turnStatedVerifier as Gc, boundVerifiedIdentity as Gi, LIVENESS_PING_MS as Gn, answersOffer as Go, PIN_LIMITS as Gr, setAckFrame as Gs, normalizeDid as Gt, readKnocks as H, readReply as Ha, turnInstance as Hc, IDENTITY_REVOCATION_LABEL as Hi, parseFedimintRequestPayload as Hn, encodeControl as Ho, sanitizeTypingStatus as Hr, SET_ACK as Hs, didWebFileNames as Ht, decodeGroupEntryLink as I, reactionEmoji as Ia, TURN_MAX as Ic, CapsExchange as Ii, ENDPOINT as In, GhostlyHttpError as Io, FILE_LIMITS as Ir, readHandoffHave as Is, checkDidSignature as It, GROUP_LIMITS as J, RUN_RESULTS as Ja, RelayTransport as Jc, identityStatement as Ji, pairedMessageFrame as Jn, callSignalHeardAt as Jo, pinIsNewer as Jr, verifySetUpdate as Js, parseDidSignaturePaste as Jt, groupWakes as K, PR_CHECKS as Ka, turnVerifierMark as Kc, emptyIdentityLedger as Ki, PAIRED_FRAME_TOO_LARGE as Kn, buildSdpFromSignal as Ko, mayPin as Kr, setUpdateFrame as Ks, parseDid as Kt, encodeGroupEntryLink as L, reactionIsNewer as La, firstDeviceSetSecret as Lc, DHT_TEXT_CAPABILITY as Li, cashuRequestPayload as Ln, formatLocalTarget as Lo, transferEnded as Lr, readHandoffHello as Ls, didGhostlyServiceEntry as Lt, communityHasFrame as M, REACTION_LIMITS as Ma, TURN_SETTLE_MS as Mc, recordsFromWellKnown as Mi, pubkyHomeserverOf as Mn, DHT_POLL_INTERVALS as Mo, readBeaconHead as Mr, newHandoffId as Ms, DID_FILE_MAX_BYTES as Mt, decodeCommunityLink as N, ReactionWindow as Na, classifyTurnRead as Nc, recordsName as Ni, requestLnurlInvoice as Nn, EXPECT_PEER_MS as No, readLobby as Nr, readHandoffAck as Ns, DID_JWS_MAX_LENGTH as Nt, sealGroupTyping as O, forwardedAgain as Oa, seedSigner as Oc, isPublicAddress as Oi, pubkyProofCapability as On, verifyCommitSignature as Oo, newerHead as Or, handoffReleaseFrame as Os, shortAtprotoDid as Ot, encodeCommunityLink as P, nextReactionNumber as Pa, turnPutSummary as Pc, txtValue as Pi, resolveLightningDestination as Pn, RELAY_POLL_INTERVALS as Po, shouldBeHub as Pr, readHandoffCancel as Ps, DidError as Pt, frameBytes as Q, cardLine as Qa, traceLink as Qc, lastSharedWithMe as Qi, GROUP_HAVE_FRAME as Qn, emptyLinkRecords as Qo, WakeLimiter as Qr, ENROLL_PROOF as Qs, didDhtDocument as Qt, entryParams as R, readReaction as Ra, nextTurnPosition as Rc, capsDescriptors as Ri, fedimintRequestPayload as Rn, parseLocalTarget as Ro, transferStalled as Rr, readHandoffPake as Rs, didJwsAlg as Rt, readAppBundleAsync as S, parseVideoMeta as Sa, deviceEchoFrame as Sc, DOMAIN_FILE_MAX_BYTES as Si, isBitcoinAddress as Sn, readCanonicalJson as So, doorHubs as Sr, handoffDigest as Ss, atprotoProofRecord as St, GroupTypingBoard as T, RateWindow as Ta, deviceLinkPairing as Tc, domainTxtRecord as Ti, isPubkyProofFolder as Tn, MEMBER_KEY as To, lobbyRecords as Tr, handoffManifestFrames as Ts, isAtprotoDid as Tt, GROUP_WAKE_FRAME as U, replySnippet as Ua, turnKeys as Uc, IdentityCheckUnavailable as Ui, GhostLink as Un, CALL_SIGNAL_MAX_AGE_MS as Uo, typingActivity as Ur, SET_UPDATE as Us, didWebFileUrl as Ut, mergeKnocks as V, pairedReplyAuthor as Va, signTurnRelease as Vc, IDENTITY_MAX_VALIDITY as Vi, parseCashuRequestPayload as Vn, decodeControl as Vo, TypingSender as Vr, sealHandoffPiece as Vs, didWebFile as Vt, GroupWakeLimiter as W, BUTTON_ID as Wa, turnName as Wc, IdentityExchange as Wi, INVITE_TAKEN as Wn, RTC_CONFIG as Wo, GROUP_PIN_FRAME as Wr, readSetUpdate as Ws, didWebHost as Wt, GROUP_READ_NOTE as X, appCardId as Xa, normalizeRelayUrl as Xc, receivedIdentityStatus as Xi, engineText as Xn, parseCallSignal as Xo, readPin as Xr, ENROLL_GRANT as Xs, staticDidDocument as Xt, GROUP_REACTED_FRAME as Y, STATUS_CARD_LIMITS as Ya, currentRelays as Yc, newIdentityBinding as Yi, engineError as Yn, extractParamsFromSdp as Yo, pinNumberHolds as Yr, ENROLL_DONE as Ys, signingKeys as Yt, GroupSession as Z, buttonLabelClash as Za, setLinkTraceSink as Zc, revokesIdentity as Zi, GROUP_FILE_LIMITS as Zn, signalHasVideo as Zo, WAKE_CALL_INTERVAL_MS as Zr, ENROLL_HELLO as Zs, strictBase64Url as Zt, assembleAppBundle as _, VOICE_LIMITS as _a, inviteOwnership as _c, HoldKeys as _i, randomBytes as _l, walletNetworkOf as _n, readAppRevocations as _o, meshHubs as _r, handoffBusyFrame as _s, ATPROTO_DECLARED_SCOPE as _t, planAppUpdate as a, irohRelayUrl as aa, enrollProofFrame as ac, vapidKeysMatch as ai, newerPacket as al, EVM_TEST_CHAINS as an, heardTime as ao, readGroupHave as ar, HANDOFF_HAVE as as, groupName as at, pacer as b, parseVoiceMeta as ba, ENROLL_CAPABILITY as bc, newMailbox as bi, utf8Decode as bl, sparkAddressKind as bn, canonicalJson as bo, beaconKeys as br, handoffContextProof as bs, ATPROTO_PROOF_SCOPE as bt, readAppStore as c, peekDhtMailbox as ca, verifyEnrollProof as cc, isAppVersion as ci, createIdentity as cl, SEPOLIA_TEST_USDT as cn, APP_PREFIXES as co, CarriedTransport as cr, HANDOFF_PAKE as cs, SshSigError as ct, APP_BUNDLE_LIMITS as d, parseImageMeta as da, readDeviceInvite as dc, EDIT_CAPABILITY as di, publicKeyFromZ32 as dl, USDT_PUBLIC_RPC as dn, appRef as do, SIGNAL_HOLD_MS as dr, HANDOFF_TIMINGS as ds, sameSshKey as dt, emptyProofLedger as ea, enrollCancelReason as ec, newWakeToken as ei, isDiscoveryBudgetError as el, encodeDidDhtDeactivated as en, checkStatusCard as eo, GROUP_WANT_NO_FRAME as er, LEGACY_SERVICES as es, GROUP_EDIT_FRAME as et, APP_BUNDLE_MAGIC as f, readImageMeta as fa, OwnInviteError as fc, STATUS_CARD_CAPABILITY as fi, verify as fl, WALLET_NETWORKS as fn, checkAppRevokeStatement as fo, SIGNAL_REPLY_MS as fr, HANDOFF_VERIFIED as fs, sshSignCommand as ft, appViewOf as g, PLAYABLE_AUDIO as ga, inviteLink as gc, HOLD_LIMITS as gi, fromBase64Url as gl, validatePaymentTarget as gn, readAppRevocation as go, mayBeHub as gr, handoffAttemptSucceeded as gs, decodeAnyBitcoinAddress as gt, appDigest as h, sanitizeMime as ha, encodeInviteCode as hc, HOLD_DATED_BACK_MS as hi, bytesEqual as hl, formatPaymentAmount as hn, isAppRef as ho, MESH_HUBS as hr, handoffAttemptFailed as hs, BitcoinAddressError as ht, checkAppBeforeRun as i, irohDescriptor as ia, enrollHelloFrame as ic, checkPushEndpoint as ii, createRelayPayload as il, ETHEREUM_USDT as in, claimedTime as io, readGroupFileMeta as ir, HANDOFF_DONE as is, mentionsMember as it, GROUP_READ_NOTE_COMMUNITY as j, GROUP_REACTION_FRAME as ja, TURN_RAISE_READ_TIMEOUT_MS as jc, recordsFromTxt as ji, homeserverWebEndpoint as jn, AWAITING_PEER_MS as jo, readBeacon as jr, handoffVersions as js, DID_DOCUMENT_MAX_BYTES as jt, COMMUNITY_LIMITS as k, readForwarded as ka, webCryptoSigner as kc, nip05RootKey as ki, pubkyProofFile as kn, edgeParams as ko, pickHubs as kr, handoffRetryAfter as ks, AtprotoProofError as kt, readAppStoreSteps as l, DHT_FILE_CAPABILITY as la, createDeviceInvite as lc, APPS_CAPABILITY as li, identityFromSeed as ll, SEPOLIA_TEST_USDT_FAUCET as ln, APP_STATEMENT_LIMITS as lo, GROUP_SIGNAL_FRAME as lr, HANDOFF_PIECE_BYTES as ls, parseSshPublicKey as lt, APP_PERMISSIONS as m, sanitizeFileName as ma, decodeInviteCode as mc, HOLD_CAPABILITY as mi, sanitizeAvatar as ml, assertWholeSats as mn, isAppKey as mo, readGroupSignal as mr, handoffAttemptAllowed as ms, verifyBitcoinMessage as mt, appStoreDecision as n, TRANSPORTS as na, enrollDoneFrame as nc, relayRequest as ni, PacketTooLargeError as nl, resolveDidDht as nn, statusCardText as no, groupFileFallback as nr, HANDOFF_ACK as ns, carryMentions as nt, APP_STORE_LIMITS as o, DHT_FILE_TOO_LARGE as oa, readEnrollGrant as oc, APP_DATA_MAX_BYTES as oi, openRelayPayload as ol, ONCHAIN_PROVIDER as on, receivedTimestamp as oo, readGroupWant as or, HANDOFF_HELLO as os, SSHSIG_MAX_ARMOR as ot, APP_ICON_PATH as p, formatFileSize as pa, createChatInvite as pc, UPGRADE_CAPABILITY as pi, MAX_AVATAR_LENGTH as pl, assertTokenUnits as pn, isAppHash as po, groupSignalFrame as pr, filePartName as ps, verifySshSignature as pt, parseGroupWakeFrame as q, PR_STATES as qa, DEFAULT_RELAYS as qc, identityRevocationValue as qi, pairedMessageFits as qn, callRtcConfig as qo, nextPinNumber as qr, signTombstone as qs, parseDidDocument as qt, appUpdateDecision as r, automaticTransport as ra, enrollGrantFrame as rc, wakeRequest as ri, RELAY_PAYLOAD_MAX_BYTES as rl, signDidDhtPacket as rn, arrivalKey as ro, isGroupMessageId as rr, HANDOFF_CANCEL as rs, parseCommunityEdit as rt, checkAppStoreIndex as s, DHT_MESSAGE_TTL as sa, readEnrollHello as sc, chatAppId as si, parseRelayPayload as sl, PaymentPreflightError as sn, shownTime as so, readGroupWantNo as sr, HANDOFF_OFFER as ss, SSHSIG_NAMESPACE as st, appRevocationFor as t, validExternalKey as ta, enrollDigits as tc, parseWakeFrame as ti, withRequestOptions as tl, encodeDidDhtPacket as tn, sameButtonText as to, fetchesByItself as tr, serviceIdFromName as ts, GROUP_EDIT_TEXT_BYTES as tt, signAppStore as u, dhtFileId as ua, deviceInviteJoinerParams as uc, BUTTONS_CAPABILITY as ui, identityFromSeedB64 as ul, TEST_USDT_FAUCET_AMOUNT as un, appFingerprint as uo, SIGNAL_FRESH_MS as ur, HANDOFF_REQUEST as us, parseSshSignature as ut, checkAppManifest as v, baseMime as va, ownInviteRefusal as vc, HoldRefusedError as vi, toBase64Url as vl, SPARK_NETWORKS as vn, signAppObject as vo, pickMeshHubs as vr, handoffCancelFrame as vs, ATPROTO_FULL_SCOPE as vt, GROUP_TYPING_FRAME as w, EDIT_RATE_WINDOW_MS as wa, deviceKeyZ32 as wc, dnsProofName as wi, isPubkyKey as wn, MAX_GROUP_CHAIN as wo, lobbyKeys as wr, handoffHelloFrame as ws, checkAtprotoProofRecord as wt, readAppBundle as x, voicePeaksOf as xa, HANDOFF_CAPABILITY as xc, readManifest as xi, utf8Encode as xl, sparkInvoiceDetails as xn, canonicalJsonBytes as xo, beaconRecords as xr, handoffDataFrame as xs, atprotoDidMethod as xt, isAppUrl as y, fileMessageText as ya, DEVICES_CAPABILITY as yc, isHoldUrl as yi, toZ32 as yl, SPARK_PROVIDER as yn, signAppRevocation as yo, COMMUNITY_TOPOLOGY as yr, handoffContext as ys, ATPROTO_PROOF_COLLECTION as yt, knockIdentity as z, validReactionNumber as za, readTurnPacket as zc, dialDescriptors as zi, findEndpoint as zn, webLocalFetch as zo, TYPING_KINDS as zr, readHandoffTurnFrame as zs, didSignedMessages as zt };
|
|
30242
30990
|
|
|
30243
|
-
//# sourceMappingURL=src-
|
|
30991
|
+
//# sourceMappingURL=src-DDzSERyc.js.map
|