@ghostlytools/cli 1.2.4 → 1.3.0
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 +16 -6
- package/dist/assets/api-BiaX6l6f.js +3 -0
- package/dist/assets/api-CrxVxtY1.js +4071 -0
- package/dist/assets/api-CrxVxtY1.js.map +1 -0
- package/dist/assets/apiKit-CajiDxdK.js +196 -0
- package/dist/assets/apiKit-CajiDxdK.js.map +1 -0
- package/dist/assets/apps-CbfiCSIw.js +787 -0
- package/dist/assets/apps-CbfiCSIw.js.map +1 -0
- package/dist/assets/apps-t23gkc28.js +2 -0
- package/dist/assets/{arkade-CdnKGDbg.js → arkade-D4BVJFEQ.js} +2 -2
- package/dist/assets/{arkade-CdnKGDbg.js.map → arkade-D4BVJFEQ.js.map} +1 -1
- package/dist/assets/backup-BAMi2lAq.js +731 -0
- package/dist/assets/backup-BAMi2lAq.js.map +1 -0
- package/dist/assets/backup-Cg52WeQi.js +2 -0
- package/dist/assets/bark_ffi_wasm_bg-EY2uc0Bx.js.map +1 -1
- package/dist/assets/buttons-CWl2yNJu.js +2 -0
- package/dist/assets/buttons-CX1pKB0j.js +497 -0
- package/dist/assets/buttons-CX1pKB0j.js.map +1 -0
- package/dist/assets/{endpoint-pgFkR2AS.js → endpoint-Dn7H8qE1.js} +14 -8
- package/dist/assets/endpoint-Dn7H8qE1.js.map +1 -0
- package/dist/assets/envelope-BvU0fLEt.js +90 -0
- package/dist/assets/envelope-BvU0fLEt.js.map +1 -0
- package/dist/assets/errors-my6VJR_0.js +77 -0
- package/dist/assets/errors-my6VJR_0.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-BNTSDaoq.js +7685 -0
- package/dist/assets/groupFiles-BNTSDaoq.js.map +1 -0
- package/dist/assets/groupFiles-DBbjOLuD.js +2088 -0
- package/dist/assets/groupFiles-DBbjOLuD.js.map +1 -0
- package/dist/assets/host-Cmd205Dc.js +4648 -0
- package/dist/assets/host-Cmd205Dc.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-CEmBpT0Q.js} +26 -10
- package/dist/assets/hyperdhtRelay-CEmBpT0Q.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--YyEA-hX.js +545 -0
- package/dist/assets/irohWeb--YyEA-hX.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-BzNN6bkx.js +4302 -0
- package/dist/assets/main-BzNN6bkx.js.map +1 -0
- package/dist/assets/{mainline-BH6s8HVi.js → mainline-D5D5C-jT.js} +11 -6
- package/dist/assets/mainline-D5D5C-jT.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-DxZWHw6v.js +154 -0
- package/dist/assets/networkReason-DxZWHw6v.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-B6F4oa9K.js +139 -0
- package/dist/assets/profiles-B6F4oa9K.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-CBCEYKMN.js} +1103 -235
- package/dist/assets/server-CBCEYKMN.js.map +1 -0
- package/dist/assets/{src-CbLkHfz8.js → src-DhfGgAUu.js} +836 -2265
- package/dist/assets/src-DhfGgAUu.js.map +1 -0
- package/dist/assets/turnRefs-ChEfgbSX.js +323 -0
- package/dist/assets/turnRefs-ChEfgbSX.js.map +1 -0
- package/dist/assets/{usdt-Qf4UuygS.js → usdt-C1Xao2xH.js} +3 -3
- package/dist/assets/{usdt-Qf4UuygS.js.map → usdt-C1Xao2xH.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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { At as fromBase64, B as readStatusCard, C as downsamplePeaks, Ct as hasVisible, D as parseVideoMeta, Dt as trimEndOf, Et as stripHidden, Ft as toBase64Url, H as statusCardBytes, I as cardEditNumber, It as toZ32, Lt as utf8Decode, Mt as fromZ32, N as STATUS_CARD_LIMITS, Nt as randomBytes, O as BUTTON_ID, Ot as bytesEqual, Pt as toBase64, Rt as utf8Encode, St as wrapDataChannel, T as parseVoiceMeta, Tt as sanitizeNick, X as APP_PREFIXES, Y as APP_NAME, _t as LIMITS, bt as encodeControl, c as readGroupFileMeta, ct as readAppSignature, dt as verifyAppRevocation, et as appRefPublisher, f as imageSize, ft as verifyAppSignature, g as formatFileSize, gt as CHUNK_KIND, h as FileTransfers, ht as readCanonicalJson, it as isAppRef, jt as fromBase64Url, kt as concatBytes$1, lt as signAppObject, mt as canonicalJsonBytes, nt as isAppHash, ot as readAppRevocationShape, p as parseImageMeta, rt as isAppKey, t as GROUP_FILE_LIMITS, v as sanitizeFileName, vt as decodeChunk, wt as sanitizeDisplayText, xt as sendBody, y as sanitizeMime, yt as decodeControl } from "./groupFiles-DBbjOLuD.js";
|
|
1
2
|
import { xsalsa20poly1305 } from "@noble/ciphers/salsa.js";
|
|
2
3
|
import { ed25519, x25519 } from "@noble/curves/ed25519.js";
|
|
3
4
|
import { hkdf } from "@noble/hashes/hkdf.js";
|
|
@@ -12,165 +13,6 @@ import { p256, p384, p521 } from "@noble/curves/nist.js";
|
|
|
12
13
|
import { gcm } from "@noble/ciphers/aes.js";
|
|
13
14
|
import { ripemd160 } from "@noble/hashes/legacy.js";
|
|
14
15
|
import { bytesToNumberBE } from "@noble/curves/utils.js";
|
|
15
|
-
//#region ../core/src/bytes.ts
|
|
16
|
-
var textEncoder$1 = new TextEncoder();
|
|
17
|
-
var textDecoder$1 = new TextDecoder("utf-8", { fatal: true });
|
|
18
|
-
function utf8Encode(str) {
|
|
19
|
-
return textEncoder$1.encode(str);
|
|
20
|
-
}
|
|
21
|
-
function utf8Decode(bytes) {
|
|
22
|
-
return textDecoder$1.decode(bytes);
|
|
23
|
-
}
|
|
24
|
-
function concatBytes$1(...parts) {
|
|
25
|
-
let length = 0;
|
|
26
|
-
for (const p of parts) length += p.length;
|
|
27
|
-
const out = new Uint8Array(length);
|
|
28
|
-
let offset = 0;
|
|
29
|
-
for (const p of parts) {
|
|
30
|
-
out.set(p, offset);
|
|
31
|
-
offset += p.length;
|
|
32
|
-
}
|
|
33
|
-
return out;
|
|
34
|
-
}
|
|
35
|
-
function bytesEqual(a, b) {
|
|
36
|
-
if (a.length !== b.length) return false;
|
|
37
|
-
let diff = 0;
|
|
38
|
-
for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
|
|
39
|
-
return diff === 0;
|
|
40
|
-
}
|
|
41
|
-
/** `Uint8Array.prototype.toBase64` (Node 25+, Chrome 140+, Firefox 133+, Safari 18.2+): ~200x the JS below. */
|
|
42
|
-
var nativeToBase64 = Uint8Array.prototype.toBase64;
|
|
43
|
-
var tableOf = (chars) => Uint8Array.from(chars, (c) => c.charCodeAt(0));
|
|
44
|
-
var BASE64_TABLE = tableOf("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/");
|
|
45
|
-
var BASE64URL_TABLE = tableOf("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_");
|
|
46
|
-
/**
|
|
47
|
-
* Base64 where the runtime has no native encoder (Node 22 and 24, older WebViews): the characters go into bytes and
|
|
48
|
-
* become one string at the end. Every file chunk is encoded (files/3 `pf-data`), so this is on a transfer's path.
|
|
49
|
-
*/
|
|
50
|
-
function encodeBase64(bytes, table, pad) {
|
|
51
|
-
const out = new Uint8Array(Math.ceil(bytes.length / 3) * 4);
|
|
52
|
-
let o = 0, i = 0;
|
|
53
|
-
for (; i + 2 < bytes.length; i += 3) {
|
|
54
|
-
const n = bytes[i] << 16 | bytes[i + 1] << 8 | bytes[i + 2];
|
|
55
|
-
out[o++] = table[n >>> 18];
|
|
56
|
-
out[o++] = table[n >>> 12 & 63];
|
|
57
|
-
out[o++] = table[n >>> 6 & 63];
|
|
58
|
-
out[o++] = table[n & 63];
|
|
59
|
-
}
|
|
60
|
-
const rest = bytes.length - i;
|
|
61
|
-
if (rest) {
|
|
62
|
-
const n = bytes[i] << 16 | (rest === 2 ? bytes[i + 1] << 8 : 0);
|
|
63
|
-
out[o++] = table[n >>> 18];
|
|
64
|
-
out[o++] = table[n >>> 12 & 63];
|
|
65
|
-
if (rest === 2) out[o++] = table[n >>> 6 & 63];
|
|
66
|
-
else if (pad) out[o++] = 61;
|
|
67
|
-
if (pad) out[o++] = 61;
|
|
68
|
-
}
|
|
69
|
-
return textDecoder$1.decode(out.subarray(0, o));
|
|
70
|
-
}
|
|
71
|
-
function toBase64(bytes) {
|
|
72
|
-
return nativeToBase64 ? nativeToBase64.call(bytes) : encodeBase64(bytes, BASE64_TABLE, true);
|
|
73
|
-
}
|
|
74
|
-
function fromBase64(str) {
|
|
75
|
-
const binary = atob(str);
|
|
76
|
-
const bytes = new Uint8Array(binary.length);
|
|
77
|
-
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
78
|
-
return bytes;
|
|
79
|
-
}
|
|
80
|
-
function toBase64Url(bytes) {
|
|
81
|
-
return nativeToBase64 ? nativeToBase64.call(bytes, {
|
|
82
|
-
alphabet: "base64url",
|
|
83
|
-
omitPadding: true
|
|
84
|
-
}) : encodeBase64(bytes, BASE64URL_TABLE, false);
|
|
85
|
-
}
|
|
86
|
-
function fromBase64Url(str) {
|
|
87
|
-
let b = str.replace(/-/g, "+").replace(/_/g, "/");
|
|
88
|
-
while (b.length % 4) b += "=";
|
|
89
|
-
return fromBase64(b);
|
|
90
|
-
}
|
|
91
|
-
var Z32_ALPHABET = "ybndrfg8ejkmcpqxot1uwisza345h769";
|
|
92
|
-
function toZ32(bytes) {
|
|
93
|
-
let out = "";
|
|
94
|
-
let bits = 0;
|
|
95
|
-
let value = 0;
|
|
96
|
-
for (const byte of bytes) {
|
|
97
|
-
value = value << 8 | byte;
|
|
98
|
-
bits += 8;
|
|
99
|
-
while (bits >= 5) {
|
|
100
|
-
out += Z32_ALPHABET[value >>> bits - 5 & 31];
|
|
101
|
-
bits -= 5;
|
|
102
|
-
}
|
|
103
|
-
value &= (1 << bits) - 1;
|
|
104
|
-
}
|
|
105
|
-
if (bits > 0) out += Z32_ALPHABET[value << 5 - bits & 31];
|
|
106
|
-
return out;
|
|
107
|
-
}
|
|
108
|
-
function fromZ32(str) {
|
|
109
|
-
const out = new Uint8Array(Math.floor(str.length * 5 / 8));
|
|
110
|
-
let bits = 0;
|
|
111
|
-
let value = 0;
|
|
112
|
-
let index = 0;
|
|
113
|
-
for (const char of str) {
|
|
114
|
-
const v = Z32_ALPHABET.indexOf(char);
|
|
115
|
-
if (v < 0) throw new Error(`Invalid z-base-32 character: ${char}`);
|
|
116
|
-
value = value << 5 | v;
|
|
117
|
-
bits += 5;
|
|
118
|
-
if (bits >= 8) {
|
|
119
|
-
out[index++] = value >>> bits - 8 & 255;
|
|
120
|
-
bits -= 8;
|
|
121
|
-
}
|
|
122
|
-
value &= (1 << bits) - 1;
|
|
123
|
-
}
|
|
124
|
-
return out;
|
|
125
|
-
}
|
|
126
|
-
function randomBytes(length) {
|
|
127
|
-
const bytes = new Uint8Array(length);
|
|
128
|
-
crypto.getRandomValues(bytes);
|
|
129
|
-
return bytes;
|
|
130
|
-
}
|
|
131
|
-
/**
|
|
132
|
-
* Invisible and direction-changing characters. Unlike a file name, display
|
|
133
|
-
* text may legitimately hold emoji and Indic or Arabic joiners, so the two
|
|
134
|
-
* joiners stay and the rest of Cf goes: bidi overrides and isolates reorder
|
|
135
|
-
* what follows them, and the others hide text or let two different names
|
|
136
|
-
* render identically.
|
|
137
|
-
*/
|
|
138
|
-
var HIDDEN = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu;
|
|
139
|
-
/** Zero-width non-joiner and zero-width joiner. */
|
|
140
|
-
var KEEP = /[\u200c\u200d]/;
|
|
141
|
-
/** `text` without its invisible and direction-changing characters, at any length: for display text with a cut of its own. */
|
|
142
|
-
function stripHidden(text) {
|
|
143
|
-
return text.replace(HIDDEN, (char) => KEEP.test(char) ? char : "");
|
|
144
|
-
}
|
|
145
|
-
/**
|
|
146
|
-
* What draws nothing when it stands alone and `HIDDEN` leaves in, since text needs it beside something else: the
|
|
147
|
-
* joiners, the fillers, the variation selectors and the other default-ignorable code points, combining marks, white
|
|
148
|
-
* space and the blank Braille pattern.
|
|
149
|
-
*/
|
|
150
|
-
var UNSEEN = /^[\p{Default_Ignorable_Code_Point}\p{M}\p{White_Space}\u2800]*$/u;
|
|
151
|
-
/** Whether `text` holds a character that shows by itself: a name of joiners, fillers or marks alone has none. */
|
|
152
|
-
function hasVisible(text) {
|
|
153
|
-
return !UNSEEN.test(text);
|
|
154
|
-
}
|
|
155
|
-
/** Undefined when nothing visible is left, so callers can fall back. */
|
|
156
|
-
function sanitizeDisplayText(text, max) {
|
|
157
|
-
const clean = [...stripHidden(text)].slice(0, max).join("").trim();
|
|
158
|
-
return hasVisible(clean) ? clean : void 0;
|
|
159
|
-
}
|
|
160
|
-
/** A nickname as it arrived from a peer, ready to show. */
|
|
161
|
-
function sanitizeNick(nick) {
|
|
162
|
-
return typeof nick === "string" ? sanitizeDisplayText(nick, 64) : void 0;
|
|
163
|
-
}
|
|
164
|
-
/**
|
|
165
|
-
* `text` without the characters of `chars` it ends with, in one pass from the end. A `/[…]+$/` replace would retry
|
|
166
|
-
* from every character of a run that does not reach the end.
|
|
167
|
-
*/
|
|
168
|
-
function trimEndOf(text, chars) {
|
|
169
|
-
let end = text.length;
|
|
170
|
-
while (end > 0 && chars.includes(text[end - 1])) end--;
|
|
171
|
-
return end === text.length ? text : text.slice(0, end);
|
|
172
|
-
}
|
|
173
|
-
//#endregion
|
|
174
16
|
//#region ../core/src/avatar.ts
|
|
175
17
|
/**
|
|
176
18
|
* A profile picture as it travels between paired peers: a small square JPEG in a data URL. The picture
|
|
@@ -181,7 +23,7 @@ function trimEndOf(text, chars) {
|
|
|
181
23
|
var MAX_AVATAR_LENGTH = 48e3;
|
|
182
24
|
var PREFIX$1 = "data:image/jpeg;base64,";
|
|
183
25
|
/** Width and height from a JPEG's frame header, or undefined when there is none before the image data. */
|
|
184
|
-
function jpegSize
|
|
26
|
+
function jpegSize(bytes) {
|
|
185
27
|
if (bytes.length < 4 || bytes[0] !== 255 || bytes[1] !== 216) return;
|
|
186
28
|
let i = 2;
|
|
187
29
|
while (i + 4 <= bytes.length) {
|
|
@@ -216,7 +58,7 @@ function sanitizeAvatar(value) {
|
|
|
216
58
|
}
|
|
217
59
|
const bytes = new Uint8Array(binary.length);
|
|
218
60
|
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
219
|
-
const size = jpegSize
|
|
61
|
+
const size = jpegSize(bytes);
|
|
220
62
|
if (!size || size.width < 1 || size.height < 1 || size.width > 512 || size.height > 512) return;
|
|
221
63
|
return value;
|
|
222
64
|
}
|
|
@@ -234,7 +76,8 @@ function generateEncryptionKey() {
|
|
|
234
76
|
function encrypt(plaintext, key) {
|
|
235
77
|
if (key.length !== KEY_LENGTH) throw new Error("Invalid key length: expected 32 bytes");
|
|
236
78
|
const nonce = randomBytes(NONCE_LENGTH$3);
|
|
237
|
-
|
|
79
|
+
const box = xsalsa20poly1305(key, nonce).encrypt(utf8Encode(plaintext));
|
|
80
|
+
return toBase64(concatBytes$1(nonce, box));
|
|
238
81
|
}
|
|
239
82
|
/** The length of what `encrypt` returns for a plaintext of this many UTF-8 bytes: base64 of nonce, box and 16-byte tag. */
|
|
240
83
|
function sealedLength(plaintextBytes) {
|
|
@@ -515,27 +358,31 @@ function signRelayPayload(identity, dnsPacket, seq) {
|
|
|
515
358
|
new DataView(timestamp.buffer).setBigUint64(0, seq);
|
|
516
359
|
return concatBytes$1(signature, timestamp, dnsPacket);
|
|
517
360
|
}
|
|
518
|
-
/**
|
|
519
|
-
function
|
|
361
|
+
/** A relay payload's parts, its size checked: the signature, the sequence number and the DNS packet (not decoded). */
|
|
362
|
+
function splitRelayPayload(payload) {
|
|
520
363
|
if (payload.length < 84) throw new Error("Relay payload too short");
|
|
521
364
|
if (payload.length > 1072) throw new PacketTooLargeError(payload.length);
|
|
365
|
+
return {
|
|
366
|
+
signature: payload.subarray(0, SIGNATURE_LENGTH),
|
|
367
|
+
seq: new DataView(payload.buffer, payload.byteOffset + SIGNATURE_LENGTH, TIMESTAMP_LENGTH).getBigUint64(0),
|
|
368
|
+
dnsPacket: payload.subarray(72)
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
/** Checks a relay payload's size and signature. Returns its sequence number and DNS packet, not decoded. */
|
|
372
|
+
function openRelayPayload(pubKeyZ32, payload) {
|
|
373
|
+
const { signature, seq, dnsPacket } = splitRelayPayload(payload);
|
|
522
374
|
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
375
|
if (!verify(signature, signable(seq, dnsPacket), publicKey)) throw new Error("Invalid signature");
|
|
527
376
|
return {
|
|
528
377
|
seq,
|
|
529
378
|
dnsPacket
|
|
530
379
|
};
|
|
531
380
|
}
|
|
532
|
-
|
|
533
|
-
function parseRelayPayload(pubKeyZ32, payload) {
|
|
534
|
-
const { seq: timestampMicros, dnsPacket } = openRelayPayload(pubKeyZ32, payload);
|
|
381
|
+
function decodeRelayPayload(pubKeyZ32, seq, dnsPacket) {
|
|
535
382
|
const suffix = `.${pubKeyZ32}`;
|
|
536
383
|
return {
|
|
537
384
|
pubKeyZ32,
|
|
538
|
-
timestampMicros,
|
|
385
|
+
timestampMicros: seq,
|
|
539
386
|
records: decodeTxtPacket(dnsPacket).map((r) => ({
|
|
540
387
|
label: r.name.endsWith(suffix) ? r.name.slice(0, -suffix.length) : r.name,
|
|
541
388
|
value: r.value,
|
|
@@ -543,6 +390,20 @@ function parseRelayPayload(pubKeyZ32, payload) {
|
|
|
543
390
|
}))
|
|
544
391
|
};
|
|
545
392
|
}
|
|
393
|
+
/** Verifies and decodes a relay payload. Throws if the signature does not match. */
|
|
394
|
+
function parseRelayPayload(pubKeyZ32, payload) {
|
|
395
|
+
const { seq, dnsPacket } = openRelayPayload(pubKeyZ32, payload);
|
|
396
|
+
return decodeRelayPayload(pubKeyZ32, seq, dnsPacket);
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Decodes a payload whose signature this process made or already checked (one it is putting), without checking it
|
|
400
|
+
* again: an Ed25519 verify costs about 1 ms, and a CLI daemon at its relay budget checked its own packets ~500 times a
|
|
401
|
+
* minute. Never for bytes read from elsewhere: `parseRelayPayload` is.
|
|
402
|
+
*/
|
|
403
|
+
function readOwnRelayPayload(pubKeyZ32, payload) {
|
|
404
|
+
const { seq, dnsPacket } = splitRelayPayload(payload);
|
|
405
|
+
return decodeRelayPayload(pubKeyZ32, seq, dnsPacket);
|
|
406
|
+
}
|
|
546
407
|
/** How far ahead of this clock a packet's time may be and still count as now: clocks drift, not by this much. */
|
|
547
408
|
var PKARR_FUTURE_SKEW_MS = 6e5;
|
|
548
409
|
/**
|
|
@@ -832,6 +693,8 @@ function currentRelays(relays) {
|
|
|
832
693
|
* only thing that peer is waiting for) would go out only once the polling slowed down, half a minute later.
|
|
833
694
|
*/
|
|
834
695
|
var WRITE_FIRST_MS = 5e3;
|
|
696
|
+
/** The most of a relay's minute the chats' reserve takes, however many chats need it: the rest stays for everything else. */
|
|
697
|
+
var CHAT_RESERVE_MOST_SHARE = 5 / 6;
|
|
835
698
|
/**
|
|
836
699
|
* 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
700
|
* 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 +1030,22 @@ var RelayTransport = class {
|
|
|
1167
1030
|
}
|
|
1168
1031
|
/**
|
|
1169
1032
|
* Signs the records, dated by `nextTimestamp`, and hands the payload to `put` (the relays; the CLI adds the DHT).
|
|
1033
|
+
* When every relay's budget (or rate limit) holds this asker back, it throws that wait before signing anything
|
|
1034
|
+
* (`publishWait`): a daemon at its budget signed about 500 packets a minute that never went out. `elsewhere`: `put`
|
|
1035
|
+
* also takes the packet where no budget holds it back (the CLI's DHT), so it is signed whatever the relays say.
|
|
1170
1036
|
* `put` calls `conflict` for a relay that answered 409: it holds a packet under this key dated at or after this one,
|
|
1171
1037
|
* which this client has not read (an inviter's warm packet under the key it gave its contact, from a clock ahead of
|
|
1172
1038
|
* this one). That packet is read from that relay, and the records go again dated just past it, once. Without this
|
|
1173
1039
|
* the packet was refused until this device's clock passed the other one's, however long that took, and on a relay
|
|
1174
1040
|
* that did take it, readers still preferred the later-dated empty packet of another relay.
|
|
1175
1041
|
*/
|
|
1176
|
-
async publishDated(identity, records, put, options = {}, again = false) {
|
|
1177
|
-
const key = identity.pubKeyZ32
|
|
1042
|
+
async publishDated(identity, records, put, options = {}, again = false, elsewhere = false) {
|
|
1043
|
+
const key = identity.pubKeyZ32;
|
|
1044
|
+
if (!again && !elsewhere) {
|
|
1045
|
+
const held = this.publishWait(key, asker(options, true));
|
|
1046
|
+
if (held) throw new DiscoveryBudgetError(held.wait, `Publish held back on every relay: ${held.reasons.join("; ")}`);
|
|
1047
|
+
}
|
|
1048
|
+
const timestamp = this.nextTimestamp(key);
|
|
1178
1049
|
let redated;
|
|
1179
1050
|
const conflict = (relay) => {
|
|
1180
1051
|
if (again || redated) return;
|
|
@@ -1186,7 +1057,7 @@ var RelayTransport = class {
|
|
|
1186
1057
|
relay
|
|
1187
1058
|
});
|
|
1188
1059
|
if (this.lastTimestamp.get(key) !== timestamp) return true;
|
|
1189
|
-
await this.publishDated(identity, records, put, options, true);
|
|
1060
|
+
await this.publishDated(identity, records, put, options, true, elsewhere);
|
|
1190
1061
|
return true;
|
|
1191
1062
|
})();
|
|
1192
1063
|
redated.catch(() => {});
|
|
@@ -1198,6 +1069,46 @@ var RelayTransport = class {
|
|
|
1198
1069
|
if (!pending || !await pending) throw error;
|
|
1199
1070
|
}
|
|
1200
1071
|
}
|
|
1072
|
+
/**
|
|
1073
|
+
* 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,
|
|
1074
|
+
* without asking any: a relay the budget holds back gets its refused `take` (the asker's write then waits first in
|
|
1075
|
+
* line, as when `put` is refused), one rate limiting or throttling this address is a wait, one left alone for failing
|
|
1076
|
+
* is none. Null too when a relay fails another way (its network cooling down): the publish then goes, and fails, as
|
|
1077
|
+
* before, and when no relay is held back at all.
|
|
1078
|
+
*/
|
|
1079
|
+
publishWait(pubKeyZ32, who) {
|
|
1080
|
+
const probe = this.breaker.allDownProbe(this.relays);
|
|
1081
|
+
let wait = Infinity;
|
|
1082
|
+
const held = [], reasons = [];
|
|
1083
|
+
for (const relay of this.relays) {
|
|
1084
|
+
if (relay !== probe && this.networkCoolingDown(relay, "PUT")) return null;
|
|
1085
|
+
const limited = this.rateLimitedFor(relay);
|
|
1086
|
+
if (limited > 0) {
|
|
1087
|
+
wait = Math.min(wait, limited);
|
|
1088
|
+
reasons.push(`${relay}: Discovery relay is cooling down after a 429; retry shortly`);
|
|
1089
|
+
continue;
|
|
1090
|
+
}
|
|
1091
|
+
const blocked = relay === probe ? 0 : this.breaker.blockedFor(relay);
|
|
1092
|
+
if (blocked > 0) {
|
|
1093
|
+
if (this.breaker.blockedKind(relay) === "throttled") {
|
|
1094
|
+
wait = Math.min(wait, blocked);
|
|
1095
|
+
reasons.push(`${relay}: Discovery relay is throttling this address; retry shortly`);
|
|
1096
|
+
} else reasons.push(`${relay} is left alone after failing`);
|
|
1097
|
+
continue;
|
|
1098
|
+
}
|
|
1099
|
+
if (this.heldFor(relay, who, Date.now(), pubKeyZ32) <= 0) return null;
|
|
1100
|
+
held.push(relay);
|
|
1101
|
+
const left = this.heldFor(relay, who);
|
|
1102
|
+
wait = Math.min(wait, left);
|
|
1103
|
+
reasons.push(`${relay}: discovery budget reached; retry in ${Math.ceil(left / 1e3)} s`);
|
|
1104
|
+
}
|
|
1105
|
+
if (wait === Infinity) return null;
|
|
1106
|
+
for (const relay of held) this.take(relay, who, pubKeyZ32);
|
|
1107
|
+
return {
|
|
1108
|
+
wait,
|
|
1109
|
+
reasons
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1201
1112
|
/** One relay's packet for a key, read now: its time, kept for `nextTimestamp`. Null when it has none or does not answer. */
|
|
1202
1113
|
async heldAt(relay, pubKeyZ32, who) {
|
|
1203
1114
|
if (!this.take(relay, who, pubKeyZ32)) return null;
|
|
@@ -1217,10 +1128,22 @@ var RelayTransport = class {
|
|
|
1217
1128
|
sawTimestamp(pubKeyZ32, timestamp) {
|
|
1218
1129
|
if (timestamp > (this.seenTimestamp.get(pubKeyZ32) ?? 0n)) this.seenTimestamp.set(pubKeyZ32, timestamp);
|
|
1219
1130
|
}
|
|
1220
|
-
/**
|
|
1131
|
+
/**
|
|
1132
|
+
* Puts a payload `publishDated` just signed, dated `timestamp`, on every relay. Its signature is not checked: it was
|
|
1133
|
+
* made here (`publishPayload` checks one signed elsewhere, to read its sequence number). `conflict`: as `publishDated`
|
|
1134
|
+
* gives it.
|
|
1135
|
+
*/
|
|
1136
|
+
async putSigned(pubKeyZ32, payload, timestamp, options = {}, conflict) {
|
|
1137
|
+
if (this.relays.length === 0) throw new Error("No Pkarr relays configured");
|
|
1138
|
+
await this.putEverywhere(pubKeyZ32, payload, timestamp, options, conflict);
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* Puts a payload signed elsewhere (a did:dht document), byte for byte, on every relay; one signed here says its date
|
|
1142
|
+
* (`signedAt`) and is not checked again. `conflict`: as `publishDated` gives it.
|
|
1143
|
+
*/
|
|
1221
1144
|
async publishPayload(pubKeyZ32, payload, options = {}, conflict) {
|
|
1222
1145
|
if (this.relays.length === 0) throw new Error("No Pkarr relays configured");
|
|
1223
|
-
const
|
|
1146
|
+
const seq = options.signedAt ?? openRelayPayload(pubKeyZ32, payload).seq;
|
|
1224
1147
|
await this.putEverywhere(pubKeyZ32, payload, seq, options, conflict);
|
|
1225
1148
|
}
|
|
1226
1149
|
/**
|
|
@@ -1261,7 +1184,7 @@ var RelayTransport = class {
|
|
|
1261
1184
|
accepted = true;
|
|
1262
1185
|
noWriteWaits();
|
|
1263
1186
|
try {
|
|
1264
|
-
this.written.set(pubKeyZ32,
|
|
1187
|
+
this.written.set(pubKeyZ32, readOwnRelayPayload(pubKeyZ32, payload));
|
|
1265
1188
|
} catch {}
|
|
1266
1189
|
resolve();
|
|
1267
1190
|
}, () => {});
|
|
@@ -1723,9 +1646,9 @@ var RelayTransport = class {
|
|
|
1723
1646
|
if (rationed.length >= most) wait = Math.max(wait, rationed[rationed.length - most] + GROUP_RATION_WINDOW_MS - now);
|
|
1724
1647
|
if (this.readsAhead(key, now) > 0) wait = Math.max(wait, TURN_WAIT_MS);
|
|
1725
1648
|
}
|
|
1726
|
-
if (who.group) {
|
|
1649
|
+
if (who.group || who.background) {
|
|
1727
1650
|
const reserved = this.chatReservedFor(relay, now);
|
|
1728
|
-
if (reserved > 0) wait = Math.max(wait, Math.min(reserved, over(recent, limit - this.
|
|
1651
|
+
if (reserved > 0) wait = Math.max(wait, Math.min(reserved, over(recent, limit - this.chatReserveOf(relay, now))));
|
|
1729
1652
|
}
|
|
1730
1653
|
const writer = firstWriter(who);
|
|
1731
1654
|
const waiting = (lane) => {
|
|
@@ -1835,6 +1758,12 @@ var RelayTransport = class {
|
|
|
1835
1758
|
if (share === void 0) return this.perMinute;
|
|
1836
1759
|
return this.perMinuteGiven ? Math.min(this.perMinute, share) : share;
|
|
1837
1760
|
}
|
|
1761
|
+
/** What the chats that need the relays keep on this relay (`CHAT_RESERVE_PER_CHAT`), never less than `reserveOf`. */
|
|
1762
|
+
chatReserveOf(relay, now) {
|
|
1763
|
+
let needing = 0;
|
|
1764
|
+
for (const need of this.chatNeeds.values()) if (now - Math.max(need.urgentAt, need.refused.get(relay) ?? -Infinity) < 6e4) needing++;
|
|
1765
|
+
return Math.max(this.reserveOf(relay), Math.min(needing * 3, Math.floor(this.limitOf(relay) * CHAT_RESERVE_MOST_SHARE)));
|
|
1766
|
+
}
|
|
1838
1767
|
/** What groups leave a chat on this relay: `CHAT_RESERVE`, or a third of a smaller budget. */
|
|
1839
1768
|
reserveOf(relay) {
|
|
1840
1769
|
return Math.min(10, Math.floor(this.limitOf(relay) / 3));
|
|
@@ -3567,9 +3496,9 @@ var HANDOFF_CANCEL_REASONS = [
|
|
|
3567
3496
|
"stalled"
|
|
3568
3497
|
];
|
|
3569
3498
|
var KEY$3 = /^[A-Za-z0-9_-]{43}$/;
|
|
3570
|
-
var DIGEST$
|
|
3499
|
+
var DIGEST$1 = KEY$3;
|
|
3571
3500
|
var SIGNATURE = /^[A-Za-z0-9_-]{86}$/;
|
|
3572
|
-
var ID$
|
|
3501
|
+
var ID$4 = /^[A-Za-z0-9_-]{22}$/;
|
|
3573
3502
|
var B64$4 = /^[A-Za-z0-9_-]*$/;
|
|
3574
3503
|
var HEX = /^[0-9a-f]{64}$/;
|
|
3575
3504
|
var VERSION_TEXT = /^[0-9A-Za-z.+_-]{1,40}$/;
|
|
@@ -3583,9 +3512,9 @@ var isHandoffPartName = (name) => typeof name === "string" && PART.test(name);
|
|
|
3583
3512
|
/** The part a file is: `file/<its SHA-256, base64url>`. */
|
|
3584
3513
|
var filePartName = (sha256B64) => `file/${sha256B64}`;
|
|
3585
3514
|
/** The digest a file part names, or null for any other part. */
|
|
3586
|
-
var filePartDigest = (name) => name.startsWith("file/") && DIGEST$
|
|
3515
|
+
var filePartDigest = (name) => name.startsWith("file/") && DIGEST$1.test(name.slice(5)) ? name.slice(5) : null;
|
|
3587
3516
|
function isPart(value) {
|
|
3588
|
-
return Array.isArray(value) && value.length === 3 && isHandoffPartName(value[0]) && isCount$1(value[1], MAX_PART) && isText$2(value[2], DIGEST$
|
|
3517
|
+
return Array.isArray(value) && value.length === 3 && isHandoffPartName(value[0]) && isCount$1(value[1], MAX_PART) && isText$2(value[2], DIGEST$1) && (filePartDigest(value[0]) === null || filePartDigest(value[0]) === value[2]);
|
|
3589
3518
|
}
|
|
3590
3519
|
/** Names compared as UTF-8 byte strings, as `H` sorts them. */
|
|
3591
3520
|
function compareNames(a, b) {
|
|
@@ -3707,7 +3636,7 @@ function readHandoffHello(frame) {
|
|
|
3707
3636
|
if (typeof frame.room !== "number" || !Number.isSafeInteger(frame.room) || frame.room < -1) return null;
|
|
3708
3637
|
if (typeof frame.metered !== "boolean") return null;
|
|
3709
3638
|
if (frame.later !== void 0 && !isCount$1(frame.later)) return null;
|
|
3710
|
-
if (frame.id !== void 0 && !isText$2(frame.id, ID$
|
|
3639
|
+
if (frame.id !== void 0 && !isText$2(frame.id, ID$4)) return null;
|
|
3711
3640
|
const kind = DEVICE_KINDS.includes(frame.kind) ? frame.kind : "web";
|
|
3712
3641
|
return {
|
|
3713
3642
|
v: 1,
|
|
@@ -3733,7 +3662,7 @@ var handoffOfferFrame = (turn, id, bytes) => ({
|
|
|
3733
3662
|
});
|
|
3734
3663
|
function readHandoffTurnFrame(frame) {
|
|
3735
3664
|
if (frame.t !== "handoff-request" && frame.t !== "handoff-offer") return null;
|
|
3736
|
-
if (!isCount$1(frame.turn, TURN_MAX) || !isText$2(frame.id, ID$
|
|
3665
|
+
if (!isCount$1(frame.turn, TURN_MAX) || !isText$2(frame.id, ID$4)) return null;
|
|
3737
3666
|
if (frame.t === "handoff-offer") return isCount$1(frame.bytes) ? {
|
|
3738
3667
|
turn: frame.turn,
|
|
3739
3668
|
id: frame.id,
|
|
@@ -3776,7 +3705,7 @@ function readHandoffPake(frame) {
|
|
|
3776
3705
|
};
|
|
3777
3706
|
}
|
|
3778
3707
|
function readHandoffHave(frame) {
|
|
3779
|
-
if (frame.t !== "handoff-have" || !Array.isArray(frame.d) || frame.d.length > 300 || !frame.d.every((d) => isText$2(d, DIGEST$
|
|
3708
|
+
if (frame.t !== "handoff-have" || !Array.isArray(frame.d) || frame.d.length > 300 || !frame.d.every((d) => isText$2(d, DIGEST$1))) return null;
|
|
3780
3709
|
const p = {};
|
|
3781
3710
|
if (frame.p !== void 0) {
|
|
3782
3711
|
if (!frame.p || typeof frame.p !== "object" || Array.isArray(frame.p) || Object.keys(frame.p).length > 300) return null;
|
|
@@ -3832,7 +3761,7 @@ function readHandoffAck(frame) {
|
|
|
3832
3761
|
}
|
|
3833
3762
|
/** `H` of a `handoff-verified` that B's key signed for this turn address and turn, or null. */
|
|
3834
3763
|
function readHandoffVerified(frame, turnAddress, turn, takerKey) {
|
|
3835
|
-
if (frame.t !== "handoff-verified" || !isText$2(frame.h, DIGEST$
|
|
3764
|
+
if (frame.t !== "handoff-verified" || !isText$2(frame.h, DIGEST$1) || !isText$2(frame.s, SIGNATURE)) return null;
|
|
3836
3765
|
const h = fromBase64Url(frame.h);
|
|
3837
3766
|
try {
|
|
3838
3767
|
return verify(fromBase64Url(frame.s), handoffVerifiedMessage(turnAddress, turn, h), takerKey) ? h : null;
|
|
@@ -3900,7 +3829,7 @@ var handoffAttemptSucceeded = () => ({
|
|
|
3900
3829
|
/** Seconds until a locked-out device may try again (for `handoff-busy`). */
|
|
3901
3830
|
var handoffRetryAfter = (attempts, now) => attempts?.until !== void 0 && attempts.until > now ? Math.ceil((attempts.until - now) / 1e3) : 0;
|
|
3902
3831
|
var HTTP_SERVICE_PROTO = "ghostly-http/1";
|
|
3903
|
-
var MAX_NAME_LENGTH
|
|
3832
|
+
var MAX_NAME_LENGTH = 48;
|
|
3904
3833
|
var MAX_META_ENTRIES = 8;
|
|
3905
3834
|
var MAX_META_LENGTH = 64;
|
|
3906
3835
|
var SERVICE_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,31}$/;
|
|
@@ -3981,7 +3910,7 @@ function servicesFromWire(wire) {
|
|
|
3981
3910
|
id: e.i,
|
|
3982
3911
|
type: e.t
|
|
3983
3912
|
};
|
|
3984
|
-
const name = cleanString(e.n, MAX_NAME_LENGTH
|
|
3913
|
+
const name = cleanString(e.n, MAX_NAME_LENGTH);
|
|
3985
3914
|
const proto = cleanString(e.p, MAX_META_LENGTH);
|
|
3986
3915
|
if (name) ad.name = name;
|
|
3987
3916
|
if (proto) ad.proto = proto;
|
|
@@ -4827,304 +4756,6 @@ function buildDataSdp(signal) {
|
|
|
4827
4756
|
return lines.join("\r\n") + "\r\n";
|
|
4828
4757
|
}
|
|
4829
4758
|
//#endregion
|
|
4830
|
-
//#region ../core/src/frames.ts
|
|
4831
|
-
/**
|
|
4832
|
-
* Framing on the `ghostly/1` DataChannel (ordered, reliable).
|
|
4833
|
-
*
|
|
4834
|
-
* - Text messages are control frames: compact JSON with a `t` discriminator,
|
|
4835
|
-
* the same shape Ghostly already uses for `_msgs` and `_call`.
|
|
4836
|
-
* - Binary messages are body chunks that belong to a stream opened by a
|
|
4837
|
-
* control frame: `<kind u8><stream id u32 BE><flags u8><payload>`.
|
|
4838
|
-
*
|
|
4839
|
-
* Everything here handles data from the remote peer and must never trust it.
|
|
4840
|
-
*/
|
|
4841
|
-
var LIMITS = {
|
|
4842
|
-
/** Largest control frame accepted, headers included. */
|
|
4843
|
-
maxControlFrameBytes: 61440,
|
|
4844
|
-
/** Longest payment endpoint value (a Cashu token, an invoice) a payment frame may carry; a longer one is dropped. */
|
|
4845
|
-
maxPaymentEndpointChars: 32768,
|
|
4846
|
-
/** Largest DataChannel message sent. 16 KiB is safe across WebRTC stacks. */
|
|
4847
|
-
maxChunkMessageBytes: 16384,
|
|
4848
|
-
maxRequestBodyBytes: 8388608,
|
|
4849
|
-
maxResponseBodyBytes: 67108864,
|
|
4850
|
-
maxHeaderCount: 128,
|
|
4851
|
-
maxHeaderValueBytes: 16384,
|
|
4852
|
-
maxPathBytes: 8192,
|
|
4853
|
-
/** Requests a host serves at once for one peer. */
|
|
4854
|
-
maxConcurrentRequestsPerPeer: 32,
|
|
4855
|
-
/** Requests a client keeps in flight; the rest wait in a queue. */
|
|
4856
|
-
maxClientInFlight: 16,
|
|
4857
|
-
maxChatMessageBytes: 16384,
|
|
4858
|
-
maxFileBytes: 104857600,
|
|
4859
|
-
/** Files a peer may be sending us at once. */
|
|
4860
|
-
maxIncomingFilesPerPeer: 3,
|
|
4861
|
-
/** Received bytes kept for one peer, in flight included. Past it new files are refused. */
|
|
4862
|
-
maxStoredIncomingBytesPerPeer: 524288e3,
|
|
4863
|
-
requestTimeoutMs: 6e4,
|
|
4864
|
-
bodyIdleTimeoutMs: 3e4,
|
|
4865
|
-
/** Stop writing to the channel above this many buffered bytes. */
|
|
4866
|
-
sendHighWaterMark: 1048576,
|
|
4867
|
-
sendLowWaterMark: 262144
|
|
4868
|
-
};
|
|
4869
|
-
var CHUNK_KIND = {
|
|
4870
|
-
requestBody: 1,
|
|
4871
|
-
responseBody: 2,
|
|
4872
|
-
fileBody: 3
|
|
4873
|
-
};
|
|
4874
|
-
var CHUNK_HEADER_BYTES = 6;
|
|
4875
|
-
var MAX_CHUNK_PAYLOAD_BYTES = LIMITS.maxChunkMessageBytes - CHUNK_HEADER_BYTES;
|
|
4876
|
-
function encodeControl(frame) {
|
|
4877
|
-
const json = JSON.stringify(frame);
|
|
4878
|
-
if (utf8Encode(json).length > LIMITS.maxControlFrameBytes) throw new Error("Control frame too large");
|
|
4879
|
-
return json;
|
|
4880
|
-
}
|
|
4881
|
-
function isHeaderList(value) {
|
|
4882
|
-
if (!Array.isArray(value) || value.length > LIMITS.maxHeaderCount) return false;
|
|
4883
|
-
return value.every((h) => Array.isArray(h) && h.length === 2 && typeof h[0] === "string" && typeof h[1] === "string" && h[0].length <= 256 && h[1].length <= LIMITS.maxHeaderValueBytes);
|
|
4884
|
-
}
|
|
4885
|
-
function isStreamId(value) {
|
|
4886
|
-
return typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 4294967295;
|
|
4887
|
-
}
|
|
4888
|
-
function isPayId(value) {
|
|
4889
|
-
return typeof value === "string" && /^[A-Za-z0-9_-]{8,64}$/.test(value);
|
|
4890
|
-
}
|
|
4891
|
-
/** Decimal text, as in Paykit's Payment Amount: no floats on the wire. */
|
|
4892
|
-
function isAmount(value) {
|
|
4893
|
-
return typeof value === "string" && /^(0|[1-9]\d{0,15})(\.\d{1,8})?$/.test(value);
|
|
4894
|
-
}
|
|
4895
|
-
function isUnit(value) {
|
|
4896
|
-
return typeof value === "string" && /^[a-z0-9]{1,12}$/.test(value);
|
|
4897
|
-
}
|
|
4898
|
-
function isEndpoint(value) {
|
|
4899
|
-
return Array.isArray(value) && value.length === 2 && typeof value[0] === "string" && value[0].length <= 64 && /^[a-z0-9][a-z0-9-]*(?:\/[1-9][0-9]*)?$/.test(value[0]) && typeof value[1] === "string" && value[1].length <= LIMITS.maxPaymentEndpointChars;
|
|
4900
|
-
}
|
|
4901
|
-
/** Returns null for anything malformed; callers drop the frame. */
|
|
4902
|
-
function decodeControl(text) {
|
|
4903
|
-
if (text.length > LIMITS.maxControlFrameBytes || utf8Encode(text).length > LIMITS.maxControlFrameBytes) return null;
|
|
4904
|
-
let raw;
|
|
4905
|
-
try {
|
|
4906
|
-
raw = JSON.parse(text);
|
|
4907
|
-
} catch {
|
|
4908
|
-
return null;
|
|
4909
|
-
}
|
|
4910
|
-
if (!raw || typeof raw !== "object") return null;
|
|
4911
|
-
const f = raw;
|
|
4912
|
-
switch (f.t) {
|
|
4913
|
-
case "hello":
|
|
4914
|
-
if (typeof f.v !== "number") return null;
|
|
4915
|
-
return {
|
|
4916
|
-
t: "hello",
|
|
4917
|
-
v: f.v,
|
|
4918
|
-
svc: f.svc,
|
|
4919
|
-
nick: sanitizeNick(f.nick)
|
|
4920
|
-
};
|
|
4921
|
-
case "m":
|
|
4922
|
-
if (typeof f.ts !== "number" || typeof f.m !== "string") return null;
|
|
4923
|
-
if (utf8Encode(f.m).length > LIMITS.maxChatMessageBytes) return null;
|
|
4924
|
-
return {
|
|
4925
|
-
t: "m",
|
|
4926
|
-
ts: f.ts,
|
|
4927
|
-
m: f.m
|
|
4928
|
-
};
|
|
4929
|
-
case "call":
|
|
4930
|
-
if (typeof f.s !== "string" || utf8Encode(f.s).length > LIMITS.maxChatMessageBytes) return null;
|
|
4931
|
-
return {
|
|
4932
|
-
t: "call",
|
|
4933
|
-
s: f.s
|
|
4934
|
-
};
|
|
4935
|
-
case "svc": return {
|
|
4936
|
-
t: "svc",
|
|
4937
|
-
svc: f.svc
|
|
4938
|
-
};
|
|
4939
|
-
case "req":
|
|
4940
|
-
if (!isStreamId(f.id) || typeof f.s !== "string" || typeof f.m !== "string") return null;
|
|
4941
|
-
if (typeof f.p !== "string" || f.p.length > LIMITS.maxPathBytes || !isHeaderList(f.h)) return null;
|
|
4942
|
-
return {
|
|
4943
|
-
t: "req",
|
|
4944
|
-
id: f.id,
|
|
4945
|
-
s: f.s,
|
|
4946
|
-
m: f.m,
|
|
4947
|
-
p: f.p,
|
|
4948
|
-
h: f.h,
|
|
4949
|
-
b: f.b === true
|
|
4950
|
-
};
|
|
4951
|
-
case "res":
|
|
4952
|
-
if (!isStreamId(f.id) || typeof f.st !== "number" || !Number.isInteger(f.st)) return null;
|
|
4953
|
-
if (f.st < 100 || f.st > 599 || !isHeaderList(f.h)) return null;
|
|
4954
|
-
return {
|
|
4955
|
-
t: "res",
|
|
4956
|
-
id: f.id,
|
|
4957
|
-
st: f.st,
|
|
4958
|
-
h: f.h,
|
|
4959
|
-
b: f.b === true
|
|
4960
|
-
};
|
|
4961
|
-
case "file":
|
|
4962
|
-
if (!isStreamId(f.id) || typeof f.f !== "string" || typeof f.ts !== "number") return null;
|
|
4963
|
-
if (typeof f.n !== "string" || typeof f.m !== "string") return null;
|
|
4964
|
-
if (typeof f.s !== "number" || !Number.isSafeInteger(f.s) || f.s < 0) return null;
|
|
4965
|
-
return {
|
|
4966
|
-
t: "file",
|
|
4967
|
-
id: f.id,
|
|
4968
|
-
f: f.f,
|
|
4969
|
-
ts: f.ts,
|
|
4970
|
-
n: f.n,
|
|
4971
|
-
s: f.s,
|
|
4972
|
-
m: f.m
|
|
4973
|
-
};
|
|
4974
|
-
case "pay-req":
|
|
4975
|
-
case "pay": {
|
|
4976
|
-
if (!isPayId(f.id) || typeof f.ts !== "number" || !isAmount(f.v) || !isUnit(f.u)) return null;
|
|
4977
|
-
const memo = typeof f.memo === "string" ? f.memo.slice(0, 140) : void 0;
|
|
4978
|
-
if (f.t === "pay-req") {
|
|
4979
|
-
if (!Array.isArray(f.e) || f.e.length === 0 || f.e.length > 8 || !f.e.every(isEndpoint)) return null;
|
|
4980
|
-
return {
|
|
4981
|
-
t: "pay-req",
|
|
4982
|
-
id: f.id,
|
|
4983
|
-
ts: f.ts,
|
|
4984
|
-
v: f.v,
|
|
4985
|
-
u: f.u,
|
|
4986
|
-
memo,
|
|
4987
|
-
e: f.e,
|
|
4988
|
-
a: isPayId(f.a) ? f.a : void 0,
|
|
4989
|
-
n: f.n === "mainnet" || f.n === "testnet" ? f.n : void 0
|
|
4990
|
-
};
|
|
4991
|
-
}
|
|
4992
|
-
if (!isEndpoint(f.e) || f.rid !== void 0 && !isPayId(f.rid)) return null;
|
|
4993
|
-
return {
|
|
4994
|
-
t: "pay",
|
|
4995
|
-
id: f.id,
|
|
4996
|
-
ts: f.ts,
|
|
4997
|
-
rid: f.rid,
|
|
4998
|
-
v: f.v,
|
|
4999
|
-
u: f.u,
|
|
5000
|
-
memo,
|
|
5001
|
-
e: f.e
|
|
5002
|
-
};
|
|
5003
|
-
}
|
|
5004
|
-
case "pay-ask":
|
|
5005
|
-
if (!isPayId(f.id) || typeof f.ts !== "number" || !isAmount(f.v) || !isUnit(f.u)) return null;
|
|
5006
|
-
if (f.m !== "arkade" && f.m !== "usdt" && f.m !== "bark" && f.m !== "bitcoin" && f.m !== "fedimint" && f.m !== "spark") return null;
|
|
5007
|
-
return {
|
|
5008
|
-
t: "pay-ask",
|
|
5009
|
-
id: f.id,
|
|
5010
|
-
ts: f.ts,
|
|
5011
|
-
v: f.v,
|
|
5012
|
-
u: f.u,
|
|
5013
|
-
m: f.m,
|
|
5014
|
-
memo: typeof f.memo === "string" ? f.memo.slice(0, 140) : void 0,
|
|
5015
|
-
n: f.n === "mainnet" || f.n === "testnet" ? f.n : void 0
|
|
5016
|
-
};
|
|
5017
|
-
case "pay-res":
|
|
5018
|
-
if (!isPayId(f.id) || typeof f.ok !== "boolean") return null;
|
|
5019
|
-
if (f.v !== void 0 && !isAmount(f.v)) return null;
|
|
5020
|
-
return {
|
|
5021
|
-
t: "pay-res",
|
|
5022
|
-
id: f.id,
|
|
5023
|
-
ok: f.ok,
|
|
5024
|
-
v: f.v,
|
|
5025
|
-
err: typeof f.err === "string" ? f.err.slice(0, 200) : void 0,
|
|
5026
|
-
...f.c === true && !f.ok ? { c: true } : {}
|
|
5027
|
-
};
|
|
5028
|
-
case "rst":
|
|
5029
|
-
if (!isStreamId(f.id) || f.d !== "q" && f.d !== "s" && f.d !== "f") return null;
|
|
5030
|
-
return {
|
|
5031
|
-
t: "rst",
|
|
5032
|
-
id: f.id,
|
|
5033
|
-
d: f.d,
|
|
5034
|
-
e: typeof f.e === "string" ? f.e.slice(0, 256) : ""
|
|
5035
|
-
};
|
|
5036
|
-
case "ping":
|
|
5037
|
-
case "pong":
|
|
5038
|
-
if (typeof f.ts !== "number") return null;
|
|
5039
|
-
return {
|
|
5040
|
-
t: f.t,
|
|
5041
|
-
ts: f.ts
|
|
5042
|
-
};
|
|
5043
|
-
default: return null;
|
|
5044
|
-
}
|
|
5045
|
-
}
|
|
5046
|
-
function encodeChunk(chunk) {
|
|
5047
|
-
if (chunk.payload.length > MAX_CHUNK_PAYLOAD_BYTES) throw new Error("Chunk payload too large");
|
|
5048
|
-
const out = new Uint8Array(CHUNK_HEADER_BYTES + chunk.payload.length);
|
|
5049
|
-
const view = new DataView(out.buffer);
|
|
5050
|
-
view.setUint8(0, chunk.kind);
|
|
5051
|
-
view.setUint32(1, chunk.id);
|
|
5052
|
-
view.setUint8(5, chunk.end ? 1 : 0);
|
|
5053
|
-
out.set(chunk.payload, CHUNK_HEADER_BYTES);
|
|
5054
|
-
return out;
|
|
5055
|
-
}
|
|
5056
|
-
function decodeChunk(data) {
|
|
5057
|
-
if (data.length < CHUNK_HEADER_BYTES || data.length > LIMITS.maxChunkMessageBytes) return null;
|
|
5058
|
-
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
5059
|
-
const kind = view.getUint8(0);
|
|
5060
|
-
if (kind !== CHUNK_KIND.requestBody && kind !== CHUNK_KIND.responseBody && kind !== CHUNK_KIND.fileBody) return null;
|
|
5061
|
-
return {
|
|
5062
|
-
kind,
|
|
5063
|
-
id: view.getUint32(1),
|
|
5064
|
-
end: (view.getUint8(5) & 1) !== 0,
|
|
5065
|
-
payload: data.subarray(CHUNK_HEADER_BYTES)
|
|
5066
|
-
};
|
|
5067
|
-
}
|
|
5068
|
-
function wrapDataChannel(dc) {
|
|
5069
|
-
dc.binaryType = "arraybuffer";
|
|
5070
|
-
dc.bufferedAmountLowThreshold = LIMITS.sendLowWaterMark;
|
|
5071
|
-
const channel = {
|
|
5072
|
-
send(data) {
|
|
5073
|
-
if (dc.readyState !== "open") throw new Error("Data link is not open");
|
|
5074
|
-
if (typeof data === "string") dc.send(data);
|
|
5075
|
-
else dc.send(data);
|
|
5076
|
-
},
|
|
5077
|
-
get bufferedAmount() {
|
|
5078
|
-
return dc.bufferedAmount;
|
|
5079
|
-
},
|
|
5080
|
-
drained() {
|
|
5081
|
-
if (dc.bufferedAmount <= LIMITS.sendLowWaterMark || dc.readyState !== "open") return Promise.resolve();
|
|
5082
|
-
return new Promise((resolve) => {
|
|
5083
|
-
const done = () => {
|
|
5084
|
-
dc.removeEventListener("bufferedamountlow", done);
|
|
5085
|
-
dc.removeEventListener("close", done);
|
|
5086
|
-
resolve();
|
|
5087
|
-
};
|
|
5088
|
-
dc.addEventListener("bufferedamountlow", done);
|
|
5089
|
-
dc.addEventListener("close", done);
|
|
5090
|
-
});
|
|
5091
|
-
},
|
|
5092
|
-
close() {
|
|
5093
|
-
dc.close();
|
|
5094
|
-
},
|
|
5095
|
-
onMessage: null,
|
|
5096
|
-
onClose: null
|
|
5097
|
-
};
|
|
5098
|
-
dc.addEventListener("message", (event) => {
|
|
5099
|
-
const data = event.data;
|
|
5100
|
-
if (typeof data === "string") channel.onMessage?.(data);
|
|
5101
|
-
else if (data instanceof ArrayBuffer) channel.onMessage?.(new Uint8Array(data));
|
|
5102
|
-
});
|
|
5103
|
-
dc.addEventListener("close", () => channel.onClose?.());
|
|
5104
|
-
return channel;
|
|
5105
|
-
}
|
|
5106
|
-
/** Writes a body as chunks, honouring backpressure. Always ends with an END chunk. */
|
|
5107
|
-
async function sendBody(channel, kind, id, body, isCancelled = () => false) {
|
|
5108
|
-
for await (const part of body) for (let offset = 0; offset < part.length; offset += MAX_CHUNK_PAYLOAD_BYTES) {
|
|
5109
|
-
if (isCancelled()) return;
|
|
5110
|
-
if (channel.bufferedAmount > LIMITS.sendHighWaterMark) await channel.drained();
|
|
5111
|
-
const payload = part.subarray(offset, offset + MAX_CHUNK_PAYLOAD_BYTES);
|
|
5112
|
-
channel.send(encodeChunk({
|
|
5113
|
-
kind,
|
|
5114
|
-
id,
|
|
5115
|
-
end: false,
|
|
5116
|
-
payload
|
|
5117
|
-
}));
|
|
5118
|
-
}
|
|
5119
|
-
if (isCancelled()) return;
|
|
5120
|
-
channel.send(encodeChunk({
|
|
5121
|
-
kind,
|
|
5122
|
-
id,
|
|
5123
|
-
end: true,
|
|
5124
|
-
payload: /* @__PURE__ */ new Uint8Array(0)
|
|
5125
|
-
}));
|
|
5126
|
-
}
|
|
5127
|
-
//#endregion
|
|
5128
4759
|
//#region ../core/src/http.ts
|
|
5129
4760
|
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
|
|
5130
4761
|
"localhost",
|
|
@@ -5701,6 +5332,19 @@ var STALLED_LOOK_MS = 15e3;
|
|
|
5701
5332
|
* #1441, on a loaded host), and past it the race's DHT dial has brought the chat back anyway.
|
|
5702
5333
|
*/
|
|
5703
5334
|
var DEPARTED_READ_MS = EXPECT_PEER_MS;
|
|
5335
|
+
/** A chat whose contact was never seen (an invite just sent) keeps looking at the active pace this long. */
|
|
5336
|
+
var AWAITING_PEER_MS = 6e5;
|
|
5337
|
+
/**
|
|
5338
|
+
* An invite this side made that nobody used, once no longer awaited (`AWAITING_PEER_MS`), is read less often the older
|
|
5339
|
+
* it is: a fifth of its age between reads, up to this. Read at the background pace instead, 15 unused invites (their
|
|
5340
|
+
* keys and their mailboxes) asked the relays for 60 reads a minute for as long as the app ran: the relays' minute went
|
|
5341
|
+
* to invites nobody may ever open, and a join on one was noticed 90 s later (bug hunt 1013-1642, 2026-10-13).
|
|
5342
|
+
*/
|
|
5343
|
+
var UNUSED_INVITE_WAIT_MAX_MS = 18e4;
|
|
5344
|
+
/** The wait between reads of an unused invite of age `age` (`UNUSED_INVITE_WAIT_MAX_MS`), never shorter than the pace's `interval`. */
|
|
5345
|
+
function unusedInviteWait(age, interval) {
|
|
5346
|
+
return Math.max(interval, Math.min(UNUSED_INVITE_WAIT_MAX_MS, Math.round(age / 5)));
|
|
5347
|
+
}
|
|
5704
5348
|
var PUBLISH_RETRY_MS = 4e3;
|
|
5705
5349
|
/**
|
|
5706
5350
|
* A packet the relays' request budget held back goes again when the budget frees a request, and at least this often
|
|
@@ -5756,6 +5400,8 @@ var LinkSession = class {
|
|
|
5756
5400
|
startedAt = Date.now();
|
|
5757
5401
|
/** 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
5402
|
awaitingSince;
|
|
5403
|
+
/** This side's invite, nobody used yet (`LinkSessionOptions.invite`). */
|
|
5404
|
+
invite;
|
|
5759
5405
|
intervals;
|
|
5760
5406
|
fastPollUntil = 0;
|
|
5761
5407
|
/** 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 +5457,7 @@ var LinkSession = class {
|
|
|
5811
5457
|
this.events = options.events ?? {};
|
|
5812
5458
|
this.firstPublish = options.firstPublish ?? "at-start";
|
|
5813
5459
|
this.awaitingSince = Math.min(options.awaitingSince ?? this.startedAt, this.startedAt);
|
|
5460
|
+
this.invite = options.invite ?? (() => false);
|
|
5814
5461
|
}
|
|
5815
5462
|
get peerPresence() {
|
|
5816
5463
|
return this.presence;
|
|
@@ -5984,8 +5631,8 @@ var LinkSession = class {
|
|
|
5984
5631
|
async setRtcSignal(signal, reportFailure = false) {
|
|
5985
5632
|
if (this.rtcSignal === signal) return;
|
|
5986
5633
|
this.rtcSignal = signal;
|
|
5987
|
-
if (reportFailure) await this.publish();
|
|
5988
|
-
else await this.publish().catch(() => {});
|
|
5634
|
+
if (reportFailure) await this.publish(signal === null);
|
|
5635
|
+
else await this.publish(signal === null).catch(() => {});
|
|
5989
5636
|
if (signal) this.pollNow();
|
|
5990
5637
|
}
|
|
5991
5638
|
/**
|
|
@@ -6058,6 +5705,7 @@ var LinkSession = class {
|
|
|
6058
5705
|
const stepping = this.fastStepsAfter ? Date.now() - this.fastStepsAfter : -1;
|
|
6059
5706
|
if (pace === "fast" && stepping >= 0) return Math.min(4 * interval, Math.max(2 * interval, stepping));
|
|
6060
5707
|
if (pace === "fast" || pace === "connected" || since < 0) return interval;
|
|
5708
|
+
if (pace === "background" && this.presence.lastPacketAt === 0 && this.invite()) return unusedInviteWait(Date.now() - this.awaitingSince, interval);
|
|
6061
5709
|
return Math.min(interval, Math.max(2 * this.intervals.fast, since));
|
|
6062
5710
|
}
|
|
6063
5711
|
/**
|
|
@@ -6069,13 +5717,17 @@ var LinkSession = class {
|
|
|
6069
5717
|
watching() {
|
|
6070
5718
|
return Date.now() < this.watchUntil && this.rtcSignal === null && !(this.presence.online && this.presence.lastPacketAt > this.watchFrom);
|
|
6071
5719
|
}
|
|
5720
|
+
/** A contact never seen, whose invite was sent lately (`AWAITING_PEER_MS`): its join is due. */
|
|
5721
|
+
awaitingPeer() {
|
|
5722
|
+
return this.presence.lastPacketAt === 0 && Date.now() - this.awaitingSince < 6e5;
|
|
5723
|
+
}
|
|
6072
5724
|
/** How urgently this link looks right now. */
|
|
6073
5725
|
pace() {
|
|
6074
5726
|
if (Date.now() < this.stalledUntil) return "fast";
|
|
6075
5727
|
if (this.connected) return "connected";
|
|
6076
5728
|
if (Date.now() < this.fastPollUntil) return "fast";
|
|
6077
5729
|
if (Date.now() < this.watchUntil) return "active";
|
|
6078
|
-
if (!this.active && this.
|
|
5730
|
+
if (!this.active && this.awaitingPeer()) return "active";
|
|
6079
5731
|
if (!this.active) return "background";
|
|
6080
5732
|
return Date.now() - this.lastActivity > 6e4 ? "idle" : "active";
|
|
6081
5733
|
}
|
|
@@ -6132,7 +5784,9 @@ var LinkSession = class {
|
|
|
6132
5784
|
}, this.encKey);
|
|
6133
5785
|
const started = Date.now();
|
|
6134
5786
|
try {
|
|
6135
|
-
const
|
|
5787
|
+
const awaited = this.connected || this.presence.online || this.sentBuffer.length > 0 || this.callSignal !== null || this.awaitingPeer();
|
|
5788
|
+
const takesBack = !rtcSignal && this.rtcSignalOut !== null;
|
|
5789
|
+
const options = rtcSignal && rtcSignal !== this.rtcSignalOut ? { signal: true } : this.watching() ? { watch: true } : upkeep && (this.connected || takesBack) || !rtcSignal && !awaited ? { background: true } : void 0;
|
|
6136
5790
|
await this.transport.publish(this.identity, built.records, options);
|
|
6137
5791
|
} catch (error) {
|
|
6138
5792
|
const ms = Date.now() - started, waiting = isDiscoveryBudgetError(error);
|
|
@@ -6437,7 +6091,9 @@ var ICE_GATHERING_TIMEOUT_MS = 5e3;
|
|
|
6437
6091
|
* settle); where they do not, every offer of a first pairing waited the full 5 s, twice when the inviter offered too
|
|
6438
6092
|
* (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
6093
|
* 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`).
|
|
6094
|
+
* not, and the next offer of the chat then waits as long as it takes (`fullGather`). An answer is bounded the same way:
|
|
6095
|
+
* it waited the full 5 s, and two CLI daemons whose STUN server swallowed every request went live 10 s after the offer
|
|
6096
|
+
* rather than 2 s (calls bug hunt, 2026-10-12).
|
|
6441
6097
|
*/
|
|
6442
6098
|
var OFFER_HOST_GATHER_MS = 2e3;
|
|
6443
6099
|
/**
|
|
@@ -6447,6 +6103,14 @@ var OFFER_HOST_GATHER_MS = 2e3;
|
|
|
6447
6103
|
var GATHER_STALL_MS = 3e3;
|
|
6448
6104
|
/** How long a connection may stay `disconnected` before it is given up. */
|
|
6449
6105
|
var DISCONNECT_GRACE_MS = 12e3;
|
|
6106
|
+
/**
|
|
6107
|
+
* The same race a moment later: setRemoteDescription resolves, and the connection closes right after it (8 ms on a
|
|
6108
|
+
* loaded core). A connection that ends within this long of its answer going in was refused too: no network fails one
|
|
6109
|
+
* that soon (ICE gives up after tens of seconds), so it says nothing about direct connections (`DirectEvidence`).
|
|
6110
|
+
* Taken for a network that lets none through, an edge between two CLIs left WebRTC for the run, and a member whose
|
|
6111
|
+
* app has WebRTC alone (one from before native group links) was never reached again (groupCompat, 2026-10-12).
|
|
6112
|
+
*/
|
|
6113
|
+
var ANSWER_REFUSED_AFTER_MS = 1e3;
|
|
6450
6114
|
/** An offer this close to the end of the offerer's attempt is not answered again: the new answer would come too late. */
|
|
6451
6115
|
var REANSWER_MARGIN_MS = 15e3;
|
|
6452
6116
|
/**
|
|
@@ -6478,12 +6142,13 @@ var DataLink = class {
|
|
|
6478
6142
|
pc = null;
|
|
6479
6143
|
myOfferTs = 0;
|
|
6480
6144
|
/**
|
|
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
|
|
6145
|
+
* An offer or answer of this side's that went without a public candidate came to nothing (both descriptions were
|
|
6146
|
+
* exchanged, and no path connected): the next one waits for its STUN servers as long as it takes
|
|
6147
|
+
* (`ICE_GATHERING_TIMEOUT_MS`).
|
|
6483
6148
|
*/
|
|
6484
6149
|
fullGather = false;
|
|
6485
|
-
/** The offer out has no public candidate (as it went: the connection's description gains any that come later). */
|
|
6486
|
-
|
|
6150
|
+
/** The offer or answer out has no public candidate (as it went: the connection's description gains any that come later). */
|
|
6151
|
+
sentLacksPublic = false;
|
|
6487
6152
|
lastSignalTs = 0;
|
|
6488
6153
|
/** The peer's description as this connection was given it: Chrome shows it as `remoteDescription` only once applied. */
|
|
6489
6154
|
remoteSdp = null;
|
|
@@ -6529,7 +6194,7 @@ var DataLink = class {
|
|
|
6529
6194
|
this.options.setFastPoll(true, true);
|
|
6530
6195
|
}
|
|
6531
6196
|
this.myOfferTs = Date.now();
|
|
6532
|
-
this.
|
|
6197
|
+
this.sentLacksPublic = !PUBLIC_CANDIDATE.test(pc.localDescription?.sdp ?? "");
|
|
6533
6198
|
const signal = {
|
|
6534
6199
|
t: "o",
|
|
6535
6200
|
ts: this.myOfferTs,
|
|
@@ -6635,9 +6300,12 @@ var DataLink = class {
|
|
|
6635
6300
|
this.reset();
|
|
6636
6301
|
if (evidence) this.options.onDirect?.(evidence);
|
|
6637
6302
|
}
|
|
6638
|
-
/**
|
|
6303
|
+
/**
|
|
6304
|
+
* This side's offer went with no public candidate and was answered, or its answer went with none, and it came to
|
|
6305
|
+
* nothing: the next offer or answer gathers for as long as it takes.
|
|
6306
|
+
*/
|
|
6639
6307
|
noteNoPublic(ended) {
|
|
6640
|
-
if (ended && this.myOfferTs && this.
|
|
6308
|
+
if (ended && (this.myOfferTs || this.answered) && this.sentLacksPublic && this.state === "connecting") this.fullGather = true;
|
|
6641
6309
|
}
|
|
6642
6310
|
/**
|
|
6643
6311
|
* What this attempt says about direct connections if it ends now without opening: nothing unless both descriptions
|
|
@@ -6652,17 +6320,22 @@ var DataLink = class {
|
|
|
6652
6320
|
/**
|
|
6653
6321
|
* The attempt on this connection failed (it timed out, ICE failed or closed, its channel closed). An answer the
|
|
6654
6322
|
* 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.
|
|
6323
|
+
* else goes back to idle. `byConnection`: the connection said so itself, rather than a timer of this link's.
|
|
6656
6324
|
*/
|
|
6657
|
-
failed() {
|
|
6325
|
+
failed(byConnection = false) {
|
|
6658
6326
|
if (this.state === "offering" && this.remoteSdp) {
|
|
6659
6327
|
this.answerRefused("failed");
|
|
6660
6328
|
return;
|
|
6661
6329
|
}
|
|
6330
|
+
if (byConnection && this.state === "connecting" && this.myOfferTs && Date.now() - this.connectingSince < ANSWER_REFUSED_AFTER_MS) {
|
|
6331
|
+
this.answerRefused("failed");
|
|
6332
|
+
return;
|
|
6333
|
+
}
|
|
6662
6334
|
const answered = this.state === "connecting" ? this.answered : null;
|
|
6663
6335
|
const left = answered ? CONNECT_TIMEOUT_MS - (Date.now() - answered.at) : 0;
|
|
6664
|
-
const
|
|
6665
|
-
this.
|
|
6336
|
+
const givenUp = !!answered && this.options.offerStanding?.(answered.offer.ts) === false;
|
|
6337
|
+
const evidence = givenUp ? null : this.directEvidence();
|
|
6338
|
+
this.noteNoPublic(!givenUp);
|
|
6666
6339
|
if (answered && answered.again < 2 && left > REANSWER_MARGIN_MS && this.options.offerStanding?.(answered.offer.ts)) {
|
|
6667
6340
|
answered.again++;
|
|
6668
6341
|
traceLink(this.options.myPubKeyZ32, "reanswer", {
|
|
@@ -6703,6 +6376,7 @@ var DataLink = class {
|
|
|
6703
6376
|
await pc.setLocalDescription(await pc.createAnswer());
|
|
6704
6377
|
});
|
|
6705
6378
|
if (!pc) return;
|
|
6379
|
+
this.sentLacksPublic = !PUBLIC_CANDIDATE.test(pc.localDescription?.sdp ?? "");
|
|
6706
6380
|
const signal = {
|
|
6707
6381
|
t: "a",
|
|
6708
6382
|
ts: Date.now(),
|
|
@@ -6732,7 +6406,7 @@ var DataLink = class {
|
|
|
6732
6406
|
await waitForIceGathering(pc, ICE_GATHERING_TIMEOUT_MS, {
|
|
6733
6407
|
stallMs: GATHER_STALL_MS,
|
|
6734
6408
|
settleMs: 150,
|
|
6735
|
-
|
|
6409
|
+
...!this.fullGather ? { hostBoundMs: OFFER_HOST_GATHER_MS } : {}
|
|
6736
6410
|
});
|
|
6737
6411
|
if (this.pc !== pc) return null;
|
|
6738
6412
|
const ms = Date.now() - gathering;
|
|
@@ -6779,7 +6453,7 @@ var DataLink = class {
|
|
|
6779
6453
|
tracePair(this.options.myPubKeyZ32, pc);
|
|
6780
6454
|
});
|
|
6781
6455
|
dc.addEventListener("close", () => {
|
|
6782
|
-
if (this.pc === pc) this.failed();
|
|
6456
|
+
if (this.pc === pc) this.failed(true);
|
|
6783
6457
|
});
|
|
6784
6458
|
pc.addEventListener("connectionstatechange", () => {
|
|
6785
6459
|
if (this.pc !== pc) return;
|
|
@@ -6788,7 +6462,7 @@ var DataLink = class {
|
|
|
6788
6462
|
if (pc.connectionState === "connected") this.options.onDisconnected?.(false);
|
|
6789
6463
|
}
|
|
6790
6464
|
this.disconnectTimer = null;
|
|
6791
|
-
if (pc.connectionState === "failed" || pc.connectionState === "closed") this.failed();
|
|
6465
|
+
if (pc.connectionState === "failed" || pc.connectionState === "closed") this.failed(true);
|
|
6792
6466
|
else if (pc.connectionState === "disconnected") {
|
|
6793
6467
|
if (this.state === "open") this.options.onDisconnected?.(true);
|
|
6794
6468
|
this.disconnectTimer = setTimeout(() => {
|
|
@@ -7144,1053 +6818,63 @@ function rememberAlone(alone, key, present) {
|
|
|
7144
6818
|
}
|
|
7145
6819
|
var GROUP_ADMIN_OFF_ERROR = "Managing this group is off on this device. Turn on Manage groups from this device first.";
|
|
7146
6820
|
//#endregion
|
|
7147
|
-
//#region ../core/src/
|
|
7148
|
-
|
|
7149
|
-
|
|
7150
|
-
|
|
7151
|
-
|
|
7152
|
-
|
|
7153
|
-
|
|
7154
|
-
|
|
7155
|
-
|
|
7156
|
-
|
|
7157
|
-
|
|
7158
|
-
|
|
7159
|
-
|
|
7160
|
-
|
|
7161
|
-
|
|
7162
|
-
return false;
|
|
7163
|
-
}
|
|
7164
|
-
function write(value, depth) {
|
|
7165
|
-
if (depth > 64) throw new Error("JSON nested deeper than 64 levels");
|
|
7166
|
-
if (value === null) return "null";
|
|
7167
|
-
if (typeof value === "boolean") return value ? "true" : "false";
|
|
7168
|
-
if (typeof value === "number") {
|
|
7169
|
-
if (!Number.isFinite(value)) throw new Error("JSON numbers must be finite");
|
|
7170
|
-
return JSON.stringify(value);
|
|
7171
|
-
}
|
|
7172
|
-
if (typeof value === "string") {
|
|
7173
|
-
if (hasLoneSurrogate(value)) throw new Error("A JSON string holds a lone surrogate");
|
|
7174
|
-
return JSON.stringify(value);
|
|
7175
|
-
}
|
|
7176
|
-
if (Array.isArray(value)) return `[${value.map((v) => write(v, depth + 1)).join(",")}]`;
|
|
7177
|
-
if (typeof value === "object") {
|
|
7178
|
-
const record = value;
|
|
7179
|
-
return `{${Object.keys(value).filter((k) => record[k] !== void 0).sort((a, b) => a < b ? -1 : a > b ? 1 : 0).map((k) => {
|
|
7180
|
-
if (hasLoneSurrogate(k)) throw new Error("A JSON key holds a lone surrogate");
|
|
7181
|
-
return `${JSON.stringify(k)}:${write(record[k], depth + 1)}`;
|
|
7182
|
-
}).join(",")}}`;
|
|
7183
|
-
}
|
|
7184
|
-
throw new Error(`Not a JSON value: ${typeof value}`);
|
|
7185
|
-
}
|
|
7186
|
-
/** The canonical text of a JSON value. Throws on a value JSON cannot hold (a non-finite number, a lone surrogate). */
|
|
7187
|
-
function canonicalJson(value) {
|
|
7188
|
-
return write(value, 0);
|
|
7189
|
-
}
|
|
7190
|
-
/** The canonical UTF-8 bytes of a JSON value. */
|
|
7191
|
-
function canonicalJsonBytes(value) {
|
|
7192
|
-
return utf8Encode(canonicalJson(value));
|
|
7193
|
-
}
|
|
6821
|
+
//#region ../core/src/replies.ts
|
|
6822
|
+
var REPLY_LIMITS = {
|
|
6823
|
+
/** Code points of the line of the original, an ellipsis included when it was cut. */
|
|
6824
|
+
snippet: 120,
|
|
6825
|
+
/** Characters of the original's id. */
|
|
6826
|
+
id: 128,
|
|
6827
|
+
/** Characters of a line as it arrives, before it is cleaned: anything longer is not a line of a message. */
|
|
6828
|
+
raw: 2048
|
|
6829
|
+
};
|
|
6830
|
+
/** A paired chat's message id, a payment id or a file's wire id; a group message's `sender:epoch:seq`. */
|
|
6831
|
+
var REPLY_ID = /^[A-Za-z0-9_:.-]{1,128}$/;
|
|
6832
|
+
/** The two authors a paired chat has, from the reply author's side. */
|
|
6833
|
+
var PAIRED_REPLY_AUTHORS = ["sender", "recipient"];
|
|
6834
|
+
/** A picture sent inline has no line worth quoting. */
|
|
6835
|
+
var INLINE_PICTURE = /^data:image\//i;
|
|
7194
6836
|
/**
|
|
7195
|
-
*
|
|
7196
|
-
*
|
|
6837
|
+
* A line of a message to quote: plain text on one line (line breaks and runs of spaces become one space), without
|
|
6838
|
+
* control, invisible or direction characters (WISP 400, as for names), at most `REPLY_LIMITS.snippet` code points,
|
|
6839
|
+
* the last of them an ellipsis when it was cut.
|
|
7197
6840
|
*/
|
|
7198
|
-
function
|
|
7199
|
-
|
|
7200
|
-
|
|
7201
|
-
|
|
7202
|
-
|
|
7203
|
-
|
|
7204
|
-
ok: false,
|
|
7205
|
-
reason: "not-json",
|
|
7206
|
-
detail: error instanceof Error ? error.message : String(error)
|
|
7207
|
-
};
|
|
7208
|
-
}
|
|
7209
|
-
let again;
|
|
7210
|
-
try {
|
|
7211
|
-
again = canonicalJsonBytes(value);
|
|
7212
|
-
} catch (error) {
|
|
7213
|
-
return {
|
|
7214
|
-
ok: false,
|
|
7215
|
-
reason: "not-canonical",
|
|
7216
|
-
detail: error instanceof Error ? error.message : String(error)
|
|
7217
|
-
};
|
|
7218
|
-
}
|
|
7219
|
-
if (!bytesEqual(again, bytes)) return {
|
|
7220
|
-
ok: false,
|
|
7221
|
-
reason: "not-canonical"
|
|
7222
|
-
};
|
|
7223
|
-
return {
|
|
7224
|
-
ok: true,
|
|
7225
|
-
value
|
|
7226
|
-
};
|
|
7227
|
-
}
|
|
7228
|
-
//#endregion
|
|
7229
|
-
//#region ../core/src/appStatements.ts
|
|
7230
|
-
/** The prefixes of phase 1. The others of the WISP (reports, reviews, licences, rotation, publisher proofs) come later. */
|
|
7231
|
-
var APP_PREFIXES = {
|
|
7232
|
-
app: "ghostly-app/1",
|
|
7233
|
-
store: "ghostly-store/1",
|
|
7234
|
-
revoke: "ghostly-revoke/1"
|
|
7235
|
-
};
|
|
7236
|
-
var APP_STATEMENT_LIMITS = {
|
|
7237
|
-
/** A signature statement's canonical bytes: 52 + 86 characters of values and the keys around them. */
|
|
7238
|
-
signatureBytes: 1024,
|
|
7239
|
-
/** Digests named by one revocation. */
|
|
7240
|
-
revokeDigests: 64,
|
|
7241
|
-
/** A revocation's reason, in characters. */
|
|
7242
|
-
reason: 200,
|
|
7243
|
-
/** Revocations in one `ghostly-revoke.json`. */
|
|
7244
|
-
revocations: 4096
|
|
7245
|
-
};
|
|
7246
|
-
/** A z-base32 Ed25519 public key: 52 characters that decode to 32 bytes and encode back to the same text. */
|
|
7247
|
-
function isAppKey(value) {
|
|
7248
|
-
if (typeof value !== "string" || !/^[ybndrfg8ejkmcpqxot1uwisza345h769]{52}$/.test(value)) return false;
|
|
7249
|
-
const bytes = fromZ32(value);
|
|
7250
|
-
return bytes.length === 32 && toZ32(bytes) === value;
|
|
7251
|
-
}
|
|
7252
|
-
/** A SHA-256 in base64url without padding: 43 characters that decode to 32 bytes and encode back to the same text. */
|
|
7253
|
-
function isAppHash(value) {
|
|
7254
|
-
if (typeof value !== "string" || !/^[A-Za-z0-9_-]{43}$/.test(value)) return false;
|
|
7255
|
-
return toBase64Url(fromBase64Url(value)) === value;
|
|
7256
|
-
}
|
|
7257
|
-
/** An app reference, `<publisher key>/<name>`. */
|
|
7258
|
-
var APP_NAME = /^[a-z][a-z0-9-]{0,31}$/;
|
|
7259
|
-
function isAppRef(value) {
|
|
7260
|
-
if (typeof value !== "string") return false;
|
|
7261
|
-
return value.indexOf("/") === 52 && isAppKey(value.slice(0, 52)) && APP_NAME.test(value.slice(53));
|
|
7262
|
-
}
|
|
7263
|
-
function appRef(publisher, name) {
|
|
7264
|
-
return `${publisher}/${name}`;
|
|
7265
|
-
}
|
|
7266
|
-
/** The publisher key of a reference. */
|
|
7267
|
-
function appRefPublisher(ref) {
|
|
7268
|
-
return ref.slice(0, 52);
|
|
6841
|
+
function replySnippet(text) {
|
|
6842
|
+
if (INLINE_PICTURE.test(text.trimStart())) return "🖼️ Picture";
|
|
6843
|
+
const flat = text.slice(0, REPLY_LIMITS.raw * 2).replace(/\s+/gu, " ");
|
|
6844
|
+
const clean = sanitizeDisplayText(flat, REPLY_LIMITS.snippet + 1) ?? "";
|
|
6845
|
+
const points = [...clean];
|
|
6846
|
+
return points.length > REPLY_LIMITS.snippet ? `${points.slice(0, REPLY_LIMITS.snippet - 1).join("").trimEnd()}…` : clean;
|
|
7269
6847
|
}
|
|
7270
6848
|
/**
|
|
7271
|
-
*
|
|
6849
|
+
* A reply as a receiver takes it: an object with an id of this chat's shape, a line (cleaned and cut again here,
|
|
6850
|
+
* whatever the sender did) and an author `author` accepts. Anything else is no reply: the message stays, without it.
|
|
7272
6851
|
*/
|
|
7273
|
-
function
|
|
7274
|
-
|
|
7275
|
-
}
|
|
7276
|
-
|
|
7277
|
-
|
|
7278
|
-
|
|
7279
|
-
const out = new Uint8Array(p.length + 1 + 32);
|
|
7280
|
-
out.set(p, 0);
|
|
7281
|
-
out.set(sha256(canonical), p.length + 1);
|
|
7282
|
-
return out;
|
|
7283
|
-
}
|
|
7284
|
-
/** Signs an object under a prefix: its canonical bytes and the signature statement. */
|
|
7285
|
-
async function signAppObject(prefix, value, signer) {
|
|
7286
|
-
const bytes = canonicalJsonBytes(value);
|
|
7287
|
-
const sig = await signer.sign(appSignedBytes(prefix, bytes));
|
|
6852
|
+
function readReply(raw, author) {
|
|
6853
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
|
|
6854
|
+
const { i, s, f, b } = raw;
|
|
6855
|
+
if (typeof i !== "string" || !REPLY_ID.test(i)) return void 0;
|
|
6856
|
+
if (typeof s !== "string" || s.length > REPLY_LIMITS.raw) return void 0;
|
|
6857
|
+
if (typeof f !== "string" || !author(f)) return void 0;
|
|
7288
6858
|
return {
|
|
7289
|
-
|
|
7290
|
-
|
|
7291
|
-
|
|
7292
|
-
|
|
7293
|
-
sig: toBase64Url(sig)
|
|
7294
|
-
}
|
|
6859
|
+
i,
|
|
6860
|
+
s: replySnippet(s),
|
|
6861
|
+
f,
|
|
6862
|
+
...typeof b === "string" && BUTTON_ID.test(b) && { b }
|
|
7295
6863
|
};
|
|
7296
6864
|
}
|
|
7297
|
-
/**
|
|
7298
|
-
|
|
7299
|
-
|
|
7300
|
-
|
|
7301
|
-
|
|
7302
|
-
|
|
7303
|
-
|
|
7304
|
-
|
|
7305
|
-
|
|
7306
|
-
}
|
|
7307
|
-
}
|
|
7308
|
-
|
|
7309
|
-
|
|
7310
|
-
const have = Object.keys(value);
|
|
7311
|
-
return keys.every((k) => have.includes(k)) && have.every((k) => keys.includes(k) || optional.includes(k));
|
|
7312
|
-
};
|
|
7313
|
-
/** Checks the shape of a signature statement: exactly `alg`, `key` and `sig`, a 64-byte signature. */
|
|
7314
|
-
function isAppSignature(value) {
|
|
7315
|
-
if (!exactKeys$1(value, [
|
|
7316
|
-
"alg",
|
|
7317
|
-
"key",
|
|
7318
|
-
"sig"
|
|
7319
|
-
])) return false;
|
|
7320
|
-
if (value.alg !== "ed25519" || !isAppKey(value.key)) return false;
|
|
7321
|
-
return typeof value.sig === "string" && /^[A-Za-z0-9_-]{86}$/.test(value.sig) && toBase64Url(fromBase64Url(value.sig)) === value.sig;
|
|
7322
|
-
}
|
|
7323
|
-
/** Reads a signature statement from its bytes, which must be canonical. */
|
|
7324
|
-
function readAppSignature(bytes) {
|
|
7325
|
-
if (bytes.length > APP_STATEMENT_LIMITS.signatureBytes) return {
|
|
7326
|
-
ok: false,
|
|
7327
|
-
reason: "bad-signature-statement",
|
|
7328
|
-
detail: "Too large"
|
|
7329
|
-
};
|
|
7330
|
-
const read = readCanonicalJson(bytes);
|
|
7331
|
-
if (!read.ok) return {
|
|
7332
|
-
ok: false,
|
|
7333
|
-
reason: "bad-signature-statement",
|
|
7334
|
-
detail: read.reason
|
|
7335
|
-
};
|
|
7336
|
-
if (!isAppSignature(read.value)) return {
|
|
7337
|
-
ok: false,
|
|
7338
|
-
reason: "bad-signature-statement",
|
|
7339
|
-
detail: "Not {alg, key, sig}"
|
|
7340
|
-
};
|
|
7341
|
-
return {
|
|
7342
|
-
ok: true,
|
|
7343
|
-
signature: read.value
|
|
7344
|
-
};
|
|
7345
|
-
}
|
|
7346
|
-
/**
|
|
7347
|
-
* Verifies a signature over an object's canonical bytes under a prefix, made by `key`: `signature-key` when the
|
|
7348
|
-
* statement names another key, `bad-signature` when it does not verify.
|
|
7349
|
-
*/
|
|
7350
|
-
function verifyAppSignature(prefix, canonical, signature, key) {
|
|
7351
|
-
if (!isAppSignature(signature)) return {
|
|
7352
|
-
ok: false,
|
|
7353
|
-
reason: "bad-signature-statement"
|
|
7354
|
-
};
|
|
7355
|
-
if (signature.key !== key) return {
|
|
7356
|
-
ok: false,
|
|
7357
|
-
reason: "signature-key"
|
|
7358
|
-
};
|
|
7359
|
-
return strictVerify(fromBase64Url(signature.sig), appSignedBytes(prefix, canonical), fromZ32(key)) ? { ok: true } : {
|
|
7360
|
-
ok: false,
|
|
7361
|
-
reason: "bad-signature"
|
|
7362
|
-
};
|
|
7363
|
-
}
|
|
7364
|
-
var isSequence = (v) => typeof v === "number" && Number.isSafeInteger(v) && v >= 1;
|
|
7365
|
-
var isPlainLine = (v, max) => typeof v === "string" && [...v].length <= max && !/\p{Cc}/u.test(v);
|
|
7366
|
-
/** Checks a revocation statement's shape; the reason it is refused, or null. */
|
|
7367
|
-
function checkAppRevokeStatement(value) {
|
|
7368
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) return "Not an object";
|
|
7369
|
-
const v = value;
|
|
7370
|
-
const byDigests = "digests" in v;
|
|
7371
|
-
if (!exactKeys$1(v, [
|
|
7372
|
-
"ghostlyRevoke",
|
|
7373
|
-
"app",
|
|
7374
|
-
byDigests ? "digests" : "upTo"
|
|
7375
|
-
], ["reason"])) return "Keys are not ghostlyRevoke, app, and exactly one of digests and upTo (reason optional)";
|
|
7376
|
-
if (v.ghostlyRevoke !== 1) return "ghostlyRevoke is not 1";
|
|
7377
|
-
if (!isAppRef(v.app)) return "app is not a reference";
|
|
7378
|
-
if (byDigests) {
|
|
7379
|
-
const d = v.digests;
|
|
7380
|
-
if (!Array.isArray(d) || d.length < 1 || d.length > APP_STATEMENT_LIMITS.revokeDigests) return "digests is not 1 to 64 digests";
|
|
7381
|
-
if (!d.every(isAppHash) || new Set(d).size !== d.length) return "digests holds a bad or repeated digest";
|
|
7382
|
-
} else if (!isSequence(v.upTo)) return "upTo is not a sequence";
|
|
7383
|
-
if (v.reason !== void 0 && !isPlainLine(v.reason, APP_STATEMENT_LIMITS.reason)) return "reason is not one line of 200 characters";
|
|
7384
|
-
return null;
|
|
7385
|
-
}
|
|
7386
|
-
/** Signs a revocation with the publisher's key. */
|
|
7387
|
-
async function signAppRevocation(statement, signer) {
|
|
7388
|
-
const problem = checkAppRevokeStatement(statement);
|
|
7389
|
-
if (problem) throw new Error(`Not a revocation: ${problem}`);
|
|
7390
|
-
if (toZ32(signer.publicKey) !== appRefPublisher(statement.app)) throw new Error("A revocation is signed by its app's publisher");
|
|
7391
|
-
const { signature } = await signAppObject(APP_PREFIXES.revoke, statement, signer);
|
|
7392
|
-
return {
|
|
7393
|
-
statement,
|
|
7394
|
-
signature
|
|
7395
|
-
};
|
|
7396
|
-
}
|
|
7397
|
-
/** What a checked revocation is kept by: the SHA-256 of its canonical bytes (statement and signature), base64url. */
|
|
7398
|
-
function appRevocationId(revocation) {
|
|
7399
|
-
return toBase64Url(sha256(canonicalJsonBytes(revocation)));
|
|
7400
|
-
}
|
|
7401
|
-
/**
|
|
7402
|
-
* Reads one signed revocation (a parsed value, from `ghostly-revoke.json` or a store index's `revoked`): its shape, then
|
|
7403
|
-
* the signature under `ghostly-revoke/1` by the publisher its `app` names. One that `checked` holds verified before, so
|
|
7404
|
-
* its signature is not checked again; one that verifies now is added to it.
|
|
7405
|
-
*/
|
|
7406
|
-
function readAppRevocation(value, checked) {
|
|
7407
|
-
const read = readAppRevocationShape(value);
|
|
7408
|
-
if (!read.ok) return read;
|
|
7409
|
-
const verified = verifyAppRevocation(read.revocation, checked);
|
|
7410
|
-
return verified.ok ? read : verified;
|
|
7411
|
-
}
|
|
7412
|
-
/** The first half of `readAppRevocation`: the shape of a signed revocation, with no signature checked. */
|
|
7413
|
-
function readAppRevocationShape(value) {
|
|
7414
|
-
if (!exactKeys$1(value, ["statement", "signature"])) return {
|
|
7415
|
-
ok: false,
|
|
7416
|
-
reason: "bad-revocation",
|
|
7417
|
-
detail: "Keys are not statement and signature"
|
|
7418
|
-
};
|
|
7419
|
-
const problem = checkAppRevokeStatement(value.statement);
|
|
7420
|
-
if (problem) return {
|
|
7421
|
-
ok: false,
|
|
7422
|
-
reason: "bad-revocation",
|
|
7423
|
-
detail: problem
|
|
7424
|
-
};
|
|
7425
|
-
if (!isAppSignature(value.signature)) return {
|
|
7426
|
-
ok: false,
|
|
7427
|
-
reason: "bad-signature-statement"
|
|
7428
|
-
};
|
|
7429
|
-
return {
|
|
7430
|
-
ok: true,
|
|
7431
|
-
revocation: {
|
|
7432
|
-
statement: value.statement,
|
|
7433
|
-
signature: value.signature
|
|
7434
|
-
}
|
|
7435
|
-
};
|
|
7436
|
-
}
|
|
7437
|
-
/** The second half: the signature of a revocation of the right shape, unless `checked` holds it. */
|
|
7438
|
-
function verifyAppRevocation(revocation, checked) {
|
|
7439
|
-
const id = checked && appRevocationId(revocation);
|
|
7440
|
-
if (id !== void 0 && checked.has(id)) return { ok: true };
|
|
7441
|
-
const verified = verifyAppSignature(APP_PREFIXES.revoke, canonicalJsonBytes(revocation.statement), revocation.signature, appRefPublisher(revocation.statement.app));
|
|
7442
|
-
if (verified.ok && id !== void 0) checked.add(id);
|
|
7443
|
-
return verified;
|
|
7444
|
-
}
|
|
7445
|
-
/**
|
|
7446
|
-
* Reads `ghostly-revoke.json`: canonical bytes holding a list of signed revocations. One that does not verify refuses the
|
|
7447
|
-
* file, as a store index's `revoked` does.
|
|
7448
|
-
*/
|
|
7449
|
-
function readAppRevocations(bytes) {
|
|
7450
|
-
const read = readCanonicalJson(bytes);
|
|
7451
|
-
if (!read.ok) return read;
|
|
7452
|
-
if (!Array.isArray(read.value) || read.value.length > APP_STATEMENT_LIMITS.revocations) return {
|
|
7453
|
-
ok: false,
|
|
7454
|
-
reason: "bad-revocation",
|
|
7455
|
-
detail: "Not a list of at most 4096 revocations"
|
|
7456
|
-
};
|
|
7457
|
-
const revocations = [];
|
|
7458
|
-
for (const entry of read.value) {
|
|
7459
|
-
const one = readAppRevocation(entry);
|
|
7460
|
-
if (!one.ok) return one;
|
|
7461
|
-
revocations.push(one.revocation);
|
|
7462
|
-
}
|
|
7463
|
-
return {
|
|
7464
|
-
ok: true,
|
|
7465
|
-
revocations
|
|
7466
|
-
};
|
|
7467
|
-
}
|
|
7468
|
-
//#endregion
|
|
7469
|
-
//#region ../core/src/messageTime.ts
|
|
7470
|
-
/**
|
|
7471
|
-
* How far ahead of this device's clock a number a peer made from its clock may be (a pin's number, a status card's
|
|
7472
|
-
* times): clocks drift, but a time past this is the peer's choice, not its clock.
|
|
7473
|
-
*/
|
|
7474
|
-
var MESSAGE_CLOCK_SKEW_MS = 3e5;
|
|
7475
|
-
/**
|
|
7476
|
-
* The time a line of a group's history is kept under (a membership change, a rename): what its commit says, but at
|
|
7477
|
-
* most `MESSAGE_CLOCK_SKEW_MS` past this clock, and now when it is not a positive number. Messages do not use it:
|
|
7478
|
-
* they are placed where they arrive (below).
|
|
7479
|
-
*/
|
|
7480
|
-
function receivedTimestamp(ts, now = Date.now()) {
|
|
7481
|
-
if (!Number.isFinite(ts) || ts <= 0) return now;
|
|
7482
|
-
return Math.min(ts, now + MESSAGE_CLOCK_SKEW_MS);
|
|
7483
|
-
}
|
|
7484
|
-
/** The latest moment a date can hold. */
|
|
7485
|
-
var MAX_DATE_MS = 864e13;
|
|
7486
|
-
/** The time a sender says, as it is kept: a positive time a date can hold, or nothing. */
|
|
7487
|
-
function claimedTime(ts) {
|
|
7488
|
-
return typeof ts === "number" && Number.isFinite(ts) && ts > 0 && ts <= MAX_DATE_MS ? ts : void 0;
|
|
7489
|
-
}
|
|
7490
|
-
/**
|
|
7491
|
-
* The place of a row stored now: this clock, and past the last place given in its chat, so rows that come in the
|
|
7492
|
-
* same millisecond (a hold pickup, a group's catch-up) keep the order they came in.
|
|
7493
|
-
*/
|
|
7494
|
-
function arrivalKey(now, last = 0) {
|
|
7495
|
-
return Math.max(now, last + 1);
|
|
7496
|
-
}
|
|
7497
|
-
/** The time a message shows: what its sender said, never later than when it came. A row of mine shows its own time. */
|
|
7498
|
-
function shownTime(message) {
|
|
7499
|
-
return message.sentAt === void 0 ? message.timestamp : Math.min(message.sentAt, message.timestamp);
|
|
7500
|
-
}
|
|
7501
|
-
/**
|
|
7502
|
-
* When something a peer dated happened (an edit), for this device: what the peer says, now at the latest, and now
|
|
7503
|
-
* when it says nothing a date can hold.
|
|
7504
|
-
*/
|
|
7505
|
-
function heardTime(ts, now = Date.now()) {
|
|
7506
|
-
return Math.min(claimedTime(ts) ?? now, now);
|
|
7507
|
-
}
|
|
7508
|
-
//#endregion
|
|
7509
|
-
//#region ../core/src/statusCards.ts
|
|
7510
|
-
var STATUS_CARD_LIMITS = {
|
|
7511
|
-
/** A card's JSON, as sent (UTF-8 bytes): with the fallback text it stays well inside every message bound. */
|
|
7512
|
-
bytes: 8192,
|
|
7513
|
-
/** The sender's id for the card: stable across its updates. */
|
|
7514
|
-
id: 64,
|
|
7515
|
-
title: 120,
|
|
7516
|
-
/** The current step, a list item, a run's summary. */
|
|
7517
|
-
line: 200,
|
|
7518
|
-
branch: 120,
|
|
7519
|
-
schedule: 80,
|
|
7520
|
-
cron: 64,
|
|
7521
|
-
linkLabel: 40,
|
|
7522
|
-
url: 512,
|
|
7523
|
-
items: 20,
|
|
7524
|
-
links: 4,
|
|
7525
|
-
runs: 10,
|
|
7526
|
-
/** Any count on a card (a PR's number, lines, files; steps). */
|
|
7527
|
-
count: 1e9,
|
|
7528
|
-
/** How far ahead a routine's next run may be. */
|
|
7529
|
-
nextRunMs: 316224e5,
|
|
7530
|
-
/** Edits a message with a card takes (a text takes `MAX_EDITS_PER_MESSAGE`): a bot updates a long task often. */
|
|
7531
|
-
edits: 5e3,
|
|
7532
|
-
/** Labels on a task, and a label's length in characters. */
|
|
7533
|
-
tags: 3,
|
|
7534
|
-
tag: 24,
|
|
7535
|
-
/** Buttons on a message (WISP 406 · Message Buttons). */
|
|
7536
|
-
buttons: 6,
|
|
7537
|
-
/** A button's id: what a press names. */
|
|
7538
|
-
buttonId: 32,
|
|
7539
|
-
/** A button's label, in characters. */
|
|
7540
|
-
buttonLabel: 40,
|
|
7541
|
-
/** An app card's title and version, in characters (WISP 405 § An app): the manifest's bounds. */
|
|
7542
|
-
appTitle: 40,
|
|
7543
|
-
appVersion: 32,
|
|
7544
|
-
/** A usage card's label ("Claude") and account, in characters (WISP 405 § Usage). */
|
|
7545
|
-
usageLabel: 24,
|
|
7546
|
-
/** A usage window's name ("5 h", "week"), in characters. */
|
|
7547
|
-
usageWindow: 16,
|
|
7548
|
-
/** Windows beside a usage card's own, shown in its details. */
|
|
7549
|
-
usageWindows: 3
|
|
7550
|
-
};
|
|
7551
|
-
/** How a button looks: `primary` is the one highlighted, `danger` warns; `neutral` when none is said. */
|
|
7552
|
-
var BUTTON_STYLES = [
|
|
7553
|
-
"primary",
|
|
7554
|
-
"neutral",
|
|
7555
|
-
"danger"
|
|
7556
|
-
];
|
|
7557
|
-
var TASK_STATUSES = [
|
|
7558
|
-
"queued",
|
|
7559
|
-
"running",
|
|
7560
|
-
"blocked",
|
|
7561
|
-
"done",
|
|
7562
|
-
"failed",
|
|
7563
|
-
"cancelled"
|
|
7564
|
-
];
|
|
7565
|
-
/** A task that is still going: shown first, and counted on the chat's Tasks button. */
|
|
7566
|
-
var ACTIVE_TASK_STATUSES = [
|
|
7567
|
-
"queued",
|
|
7568
|
-
"running",
|
|
7569
|
-
"blocked"
|
|
7570
|
-
];
|
|
7571
|
-
var ITEM_STATES = [
|
|
7572
|
-
"pending",
|
|
7573
|
-
"running",
|
|
7574
|
-
"done",
|
|
7575
|
-
"failed",
|
|
7576
|
-
"skipped"
|
|
7577
|
-
];
|
|
7578
|
-
var ROUTINE_STATES = ["active", "paused"];
|
|
7579
|
-
var RUN_RESULTS = [
|
|
7580
|
-
"ok",
|
|
7581
|
-
"failed",
|
|
7582
|
-
"skipped"
|
|
7583
|
-
];
|
|
7584
|
-
/**
|
|
7585
|
-
* Where a task's pull request stands: `draft` (not ready for review), `open` (ready for review), `merged`, `closed`
|
|
7586
|
-
* (closed without a merge). A reader that does not know the field shows the pull request as before.
|
|
7587
|
-
*/
|
|
7588
|
-
var PR_STATES = [
|
|
7589
|
-
"draft",
|
|
7590
|
-
"open",
|
|
7591
|
-
"merged",
|
|
7592
|
-
"closed"
|
|
7593
|
-
];
|
|
7594
|
-
/** How a pull request's checks stand. */
|
|
7595
|
-
var PR_CHECKS = [
|
|
7596
|
-
"passing",
|
|
7597
|
-
"failing",
|
|
7598
|
-
"pending"
|
|
7599
|
-
];
|
|
7600
|
-
var ID$4 = /^[A-Za-z0-9_.:][A-Za-z0-9_.:-]{0,63}$/;
|
|
7601
|
-
/** A button's id: 1 to 32 of `A-Z a-z 0-9 _ . : -`, not starting with `-`. */
|
|
7602
|
-
var BUTTON_ID = /^[A-Za-z0-9_.:][A-Za-z0-9_.:-]{0,31}$/;
|
|
7603
|
-
var CRON = /^[0-9A-Za-z*,/?#\- ]{1,64}$/;
|
|
7604
|
-
/**
|
|
7605
|
-
* Whether a typed answer is a button's label or id (WISP 406 · Message Buttons): ignoring case and spaces at the ends.
|
|
7606
|
-
* Not the device's locale's case: the presser's and the author's apps must agree ("I" is not "ı" anywhere).
|
|
7607
|
-
*/
|
|
7608
|
-
var sameButtonText = (a, b) => a.trim().toLowerCase() === b.trim().toLowerCase();
|
|
7609
|
-
/**
|
|
7610
|
-
* Two buttons a typed answer could not tell apart, or none: a label that reads as an earlier label (`label`), or a
|
|
7611
|
-
* label that reads as another button's id (`id`), both as `sameButtonText` compares them. The labels as a reader keeps
|
|
7612
|
-
* them (one line, cleaned). `at` is the button that clashes, `with` the one it clashes with.
|
|
7613
|
-
*/
|
|
7614
|
-
function buttonLabelClash(buttons) {
|
|
7615
|
-
const labels = buttons.map((button) => isObject$4(button) ? cardLine(button.label, Infinity) : void 0);
|
|
7616
|
-
const ids = buttons.map((button) => isObject$4(button) && typeof button.id === "string" ? button.id : void 0);
|
|
7617
|
-
for (const [at, label] of labels.entries()) {
|
|
7618
|
-
if (!label) continue;
|
|
7619
|
-
const twin = labels.findIndex((other, i) => i < at && !!other && sameButtonText(other, label));
|
|
7620
|
-
if (twin !== -1) return {
|
|
7621
|
-
at,
|
|
7622
|
-
with: twin,
|
|
7623
|
-
kind: "label"
|
|
7624
|
-
};
|
|
7625
|
-
const named = ids.findIndex((id, i) => i !== at && id !== void 0 && sameButtonText(id, label));
|
|
7626
|
-
if (named !== -1) return {
|
|
7627
|
-
at,
|
|
7628
|
-
with: named,
|
|
7629
|
-
kind: "id"
|
|
7630
|
-
};
|
|
7631
|
-
}
|
|
7632
|
-
}
|
|
7633
|
-
/** An app card's id, made from its reference: the app's name, a dot, the first 16 characters of the publisher key. */
|
|
7634
|
-
function appCardId(ref) {
|
|
7635
|
-
return `${ref.slice(53)}.${ref.slice(0, 16)}`;
|
|
7636
|
-
}
|
|
7637
|
-
/** An app card's `sequence`: a whole number from 0 to 2^53 - 1. */
|
|
7638
|
-
var appSequence = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
7639
|
-
var isObject$4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7640
|
-
/** One line of display text: runs of space (line breaks included) become one space, then the display rules for names. */
|
|
7641
|
-
function cardLine(value, max) {
|
|
7642
|
-
if (typeof value !== "string") return void 0;
|
|
7643
|
-
return sanitizeDisplayText(value.replace(/\s+/gu, " "), max);
|
|
7644
|
-
}
|
|
7645
|
-
/**
|
|
7646
|
-
* An https link a card may show: no credentials, a host, no spaces or control characters, at most
|
|
7647
|
-
* `STATUS_CARD_LIMITS.url` characters. Returned as the parser writes it (a non-ASCII host in punycode).
|
|
7648
|
-
*/
|
|
7649
|
-
function cardUrl(value) {
|
|
7650
|
-
if (typeof value !== "string" || !value || value.length > STATUS_CARD_LIMITS.url || /[\s\p{Cc}\p{Cf}]/u.test(value)) return void 0;
|
|
7651
|
-
let url;
|
|
7652
|
-
try {
|
|
7653
|
-
url = new URL(value);
|
|
7654
|
-
} catch {
|
|
7655
|
-
return;
|
|
7656
|
-
}
|
|
7657
|
-
if (url.protocol !== "https:" || !url.hostname || url.username || url.password) return void 0;
|
|
7658
|
-
return url.href.length <= STATUS_CARD_LIMITS.url ? url.href : void 0;
|
|
7659
|
-
}
|
|
7660
|
-
var count = (value, min = 0) => typeof value === "number" && Number.isSafeInteger(value) && value >= min && value <= STATUS_CARD_LIMITS.count ? value : void 0;
|
|
7661
|
-
/** A time on a card as a reader keeps it: a positive whole number, never later than `latest`. */
|
|
7662
|
-
var time = (value, latest) => typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? Math.min(value, latest) : void 0;
|
|
7663
|
-
var oneOf = (values, value) => typeof value === "string" && values.includes(value) ? value : void 0;
|
|
7664
|
-
function readLinks(raw) {
|
|
7665
|
-
if (!Array.isArray(raw)) return [];
|
|
7666
|
-
const links = [];
|
|
7667
|
-
for (const entry of raw.slice(0, STATUS_CARD_LIMITS.links)) {
|
|
7668
|
-
if (!isObject$4(entry)) continue;
|
|
7669
|
-
const url = cardUrl(entry.url);
|
|
7670
|
-
if (!url) continue;
|
|
7671
|
-
const label = cardLine(entry.label, STATUS_CARD_LIMITS.linkLabel);
|
|
7672
|
-
links.push({
|
|
7673
|
-
url,
|
|
7674
|
-
...label && { label }
|
|
7675
|
-
});
|
|
7676
|
-
}
|
|
7677
|
-
return links;
|
|
7678
|
-
}
|
|
7679
|
-
/** A task's labels as a reader keeps them: the first `STATUS_CARD_LIMITS.tags` that hold, each one clean line, none twice. */
|
|
7680
|
-
function readTags(raw) {
|
|
7681
|
-
if (!Array.isArray(raw)) return [];
|
|
7682
|
-
const tags = [];
|
|
7683
|
-
for (const entry of raw) {
|
|
7684
|
-
const tag = cardLine(entry, STATUS_CARD_LIMITS.tag);
|
|
7685
|
-
if (tag && !tags.includes(tag)) tags.push(tag);
|
|
7686
|
-
if (tags.length === STATUS_CARD_LIMITS.tags) break;
|
|
7687
|
-
}
|
|
7688
|
-
return tags;
|
|
7689
|
-
}
|
|
7690
|
-
function readRun(raw, now) {
|
|
7691
|
-
if (!isObject$4(raw)) return void 0;
|
|
7692
|
-
const at = time(raw.at, now + MESSAGE_CLOCK_SKEW_MS), result = oneOf(RUN_RESULTS, raw.result);
|
|
7693
|
-
if (!at || !result) return void 0;
|
|
7694
|
-
const summary = cardLine(raw.summary, STATUS_CARD_LIMITS.line);
|
|
7695
|
-
return {
|
|
7696
|
-
at,
|
|
7697
|
-
result,
|
|
7698
|
-
...summary && { summary }
|
|
7699
|
-
};
|
|
7700
|
-
}
|
|
7701
|
-
/** A percent as a reader keeps it: a finite number, rounded and clamped into 0 to 100. */
|
|
7702
|
-
var percent = (value) => typeof value === "number" && Number.isFinite(value) ? Math.round(Math.min(100, Math.max(0, value))) : void 0;
|
|
7703
|
-
/** A time ahead of the reader (a reset, a next run) as it keeps it: whole, positive, at most a year ahead. */
|
|
7704
|
-
var ahead = (value, now) => typeof value === "number" && Number.isSafeInteger(value) && value > 0 && value <= now + STATUS_CARD_LIMITS.nextRunMs ? value : void 0;
|
|
7705
|
-
/** A usage card as a reader keeps it (WISP 405 § Usage): dropped without a percent left or a used of limit. */
|
|
7706
|
-
function readUsage(raw, id, now) {
|
|
7707
|
-
const left = percent(raw.left);
|
|
7708
|
-
let used = count(raw.used), limit = count(raw.limit, 1);
|
|
7709
|
-
if (used === void 0 || limit === void 0 || used > limit) used = limit = void 0;
|
|
7710
|
-
if (left === void 0 && limit === void 0) return void 0;
|
|
7711
|
-
const label = cardLine(raw.label, STATUS_CARD_LIMITS.usageLabel), account = cardLine(raw.account, STATUS_CARD_LIMITS.usageLabel);
|
|
7712
|
-
const window = cardLine(raw.window, STATUS_CARD_LIMITS.usageWindow);
|
|
7713
|
-
const resetsAt = ahead(raw.resetsAt, now), updatedAt = time(raw.updatedAt, now + MESSAGE_CLOCK_SKEW_MS);
|
|
7714
|
-
const windows = [];
|
|
7715
|
-
if (Array.isArray(raw.windows)) for (const entry of raw.windows.slice(0, STATUS_CARD_LIMITS.usageWindows)) {
|
|
7716
|
-
if (!isObject$4(entry)) continue;
|
|
7717
|
-
const name = cardLine(entry.window, STATUS_CARD_LIMITS.usageWindow), share = percent(entry.left);
|
|
7718
|
-
if (!name || share === void 0) continue;
|
|
7719
|
-
const at = ahead(entry.resetsAt, now);
|
|
7720
|
-
windows.push({
|
|
7721
|
-
window: name,
|
|
7722
|
-
left: share,
|
|
7723
|
-
...at && { resetsAt: at }
|
|
7724
|
-
});
|
|
7725
|
-
}
|
|
7726
|
-
return {
|
|
7727
|
-
kind: "usage",
|
|
7728
|
-
id,
|
|
7729
|
-
...left !== void 0 && { left },
|
|
7730
|
-
...used !== void 0 && limit !== void 0 && {
|
|
7731
|
-
used,
|
|
7732
|
-
limit
|
|
7733
|
-
},
|
|
7734
|
-
...label && { label },
|
|
7735
|
-
...account && { account },
|
|
7736
|
-
...window && { window },
|
|
7737
|
-
...resetsAt && { resetsAt },
|
|
7738
|
-
...updatedAt && { updatedAt },
|
|
7739
|
-
...windows.length && { windows }
|
|
7740
|
-
};
|
|
7741
|
-
}
|
|
7742
|
-
/** How much of a usage card's quota is left, 0 to 100: its own percent, else worked out from used of limit. */
|
|
7743
|
-
function usageLeft(card) {
|
|
7744
|
-
if (card.left !== void 0) return card.left;
|
|
7745
|
-
return card.used !== void 0 && card.limit ? Math.round((card.limit - card.used) / card.limit * 100) : 0;
|
|
7746
|
-
}
|
|
7747
|
-
/** The bytes a card takes on the wire: what `STATUS_CARD_LIMITS.bytes` bounds. */
|
|
7748
|
-
function statusCardBytes(card) {
|
|
7749
|
-
try {
|
|
7750
|
-
return utf8Encode(JSON.stringify(card) ?? "").length;
|
|
7751
|
-
} catch {
|
|
7752
|
-
return Infinity;
|
|
7753
|
-
}
|
|
7754
|
-
}
|
|
7755
|
-
/**
|
|
7756
|
-
* A card as a reader takes it from the wire (`sc`), or undefined when it cannot stand: not an object, past
|
|
7757
|
-
* `STATUS_CARD_LIMITS.bytes`, a kind this app does not know, no valid id, title or name, status or state. Fields that do
|
|
7758
|
-
* not hold are left out and lists are cut to their bound; times are kept no later than the reader's clock allows (a
|
|
7759
|
-
* routine's next run up to a year ahead). The message is always kept: without a card it shows its text.
|
|
7760
|
-
*/
|
|
7761
|
-
function readStatusCard(raw, now = Date.now()) {
|
|
7762
|
-
if (!isObject$4(raw) || statusCardBytes(raw) > STATUS_CARD_LIMITS.bytes) return void 0;
|
|
7763
|
-
const id = typeof raw.id === "string" && ID$4.test(raw.id) ? raw.id : void 0;
|
|
7764
|
-
if (!id) return void 0;
|
|
7765
|
-
if (raw.kind === "app") {
|
|
7766
|
-
if (!isAppRef(raw.ref) || id !== appCardId(raw.ref) || !isAppHash(raw.digest) || !appSequence(raw.sequence)) return void 0;
|
|
7767
|
-
const title = cardLine(raw.title, STATUS_CARD_LIMITS.appTitle);
|
|
7768
|
-
if (!title) return void 0;
|
|
7769
|
-
const version = cardLine(raw.version, STATUS_CARD_LIMITS.appVersion), url = cardUrl(raw.url);
|
|
7770
|
-
return {
|
|
7771
|
-
kind: "app",
|
|
7772
|
-
id,
|
|
7773
|
-
ref: raw.ref,
|
|
7774
|
-
digest: raw.digest,
|
|
7775
|
-
sequence: raw.sequence,
|
|
7776
|
-
title,
|
|
7777
|
-
...version && { version },
|
|
7778
|
-
...url && { url },
|
|
7779
|
-
...raw.opened === true && { opened: true }
|
|
7780
|
-
};
|
|
7781
|
-
}
|
|
7782
|
-
const latest = now + MESSAGE_CLOCK_SKEW_MS;
|
|
7783
|
-
if (raw.kind === "usage") return readUsage(raw, id, now);
|
|
7784
|
-
const links = readLinks(raw.links);
|
|
7785
|
-
if (raw.kind === "task") {
|
|
7786
|
-
const title = cardLine(raw.title, STATUS_CARD_LIMITS.title), status = oneOf(TASK_STATUSES, raw.status);
|
|
7787
|
-
if (!title || !status) return void 0;
|
|
7788
|
-
const progress = typeof raw.progress === "number" && Number.isFinite(raw.progress) ? Math.round(Math.min(100, Math.max(0, raw.progress))) : void 0;
|
|
7789
|
-
let done = count(raw.done), total = count(raw.total, 1);
|
|
7790
|
-
if (done === void 0 || total === void 0 || done > total) done = total = void 0;
|
|
7791
|
-
const step = cardLine(raw.step, STATUS_CARD_LIMITS.line), branch = cardLine(raw.branch, STATUS_CARD_LIMITS.branch);
|
|
7792
|
-
let pr;
|
|
7793
|
-
if (isObject$4(raw.pr)) {
|
|
7794
|
-
const url = cardUrl(raw.pr.url);
|
|
7795
|
-
if (url) {
|
|
7796
|
-
const number = count(raw.pr.number, 1), additions = count(raw.pr.additions), deletions = count(raw.pr.deletions), files = count(raw.pr.files);
|
|
7797
|
-
const state = oneOf(PR_STATES, raw.pr.state), checks = oneOf(PR_CHECKS, raw.pr.checks);
|
|
7798
|
-
pr = {
|
|
7799
|
-
url,
|
|
7800
|
-
...number !== void 0 && { number },
|
|
7801
|
-
...additions !== void 0 && { additions },
|
|
7802
|
-
...deletions !== void 0 && { deletions },
|
|
7803
|
-
...files !== void 0 && { files },
|
|
7804
|
-
...state && { state },
|
|
7805
|
-
...checks && { checks }
|
|
7806
|
-
};
|
|
7807
|
-
}
|
|
7808
|
-
}
|
|
7809
|
-
const items = [];
|
|
7810
|
-
if (Array.isArray(raw.items)) for (const entry of raw.items.slice(0, STATUS_CARD_LIMITS.items)) {
|
|
7811
|
-
if (!isObject$4(entry)) continue;
|
|
7812
|
-
const text = cardLine(entry.text, STATUS_CARD_LIMITS.line);
|
|
7813
|
-
if (text) items.push({
|
|
7814
|
-
text,
|
|
7815
|
-
state: oneOf(ITEM_STATES, entry.state) ?? "pending"
|
|
7816
|
-
});
|
|
7817
|
-
}
|
|
7818
|
-
const startedAt = time(raw.startedAt, latest), updatedAt = time(raw.updatedAt, latest);
|
|
7819
|
-
const tags = readTags(raw.tags);
|
|
7820
|
-
const parent = typeof raw.parent === "string" && ID$4.test(raw.parent) && raw.parent !== id ? raw.parent : void 0;
|
|
7821
|
-
return {
|
|
7822
|
-
kind: "task",
|
|
7823
|
-
id,
|
|
7824
|
-
title,
|
|
7825
|
-
status,
|
|
7826
|
-
...progress !== void 0 && { progress },
|
|
7827
|
-
...done !== void 0 && total !== void 0 && {
|
|
7828
|
-
done,
|
|
7829
|
-
total
|
|
7830
|
-
},
|
|
7831
|
-
...step && { step },
|
|
7832
|
-
...pr && { pr },
|
|
7833
|
-
...branch && { branch },
|
|
7834
|
-
...startedAt && { startedAt },
|
|
7835
|
-
...updatedAt && { updatedAt },
|
|
7836
|
-
...items.length && { items },
|
|
7837
|
-
...links.length && { links },
|
|
7838
|
-
...tags.length && { tags },
|
|
7839
|
-
...parent && { parent }
|
|
7840
|
-
};
|
|
7841
|
-
}
|
|
7842
|
-
if (raw.kind === "routine") {
|
|
7843
|
-
const name = cardLine(raw.name, STATUS_CARD_LIMITS.title), schedule = cardLine(raw.schedule, STATUS_CARD_LIMITS.schedule), state = oneOf(ROUTINE_STATES, raw.state);
|
|
7844
|
-
if (!name || !schedule || !state) return void 0;
|
|
7845
|
-
const cron = typeof raw.cron === "string" && CRON.test(raw.cron.trim()) ? raw.cron.trim() : void 0;
|
|
7846
|
-
const lastRun = readRun(raw.lastRun, now);
|
|
7847
|
-
const nextRunAt = typeof raw.nextRunAt === "number" && Number.isSafeInteger(raw.nextRunAt) && raw.nextRunAt > 0 && raw.nextRunAt <= now + STATUS_CARD_LIMITS.nextRunMs ? raw.nextRunAt : void 0;
|
|
7848
|
-
const runs = [];
|
|
7849
|
-
if (Array.isArray(raw.runs)) for (const entry of raw.runs.slice(0, STATUS_CARD_LIMITS.runs)) {
|
|
7850
|
-
const run = readRun(entry, now);
|
|
7851
|
-
if (run) runs.push(run);
|
|
7852
|
-
}
|
|
7853
|
-
return {
|
|
7854
|
-
kind: "routine",
|
|
7855
|
-
id,
|
|
7856
|
-
name,
|
|
7857
|
-
schedule,
|
|
7858
|
-
state,
|
|
7859
|
-
...cron && { cron },
|
|
7860
|
-
...lastRun && { lastRun },
|
|
7861
|
-
...nextRunAt && { nextRunAt },
|
|
7862
|
-
...runs.length && { runs },
|
|
7863
|
-
...links.length && { links }
|
|
7864
|
-
};
|
|
7865
|
-
}
|
|
7866
|
-
if (raw.kind === "buttons") {
|
|
7867
|
-
if (!Array.isArray(raw.buttons)) return void 0;
|
|
7868
|
-
const buttons = [], seen = /* @__PURE__ */ new Set();
|
|
7869
|
-
for (const entry of raw.buttons.slice(0, STATUS_CARD_LIMITS.buttons)) {
|
|
7870
|
-
if (!isObject$4(entry) || typeof entry.id !== "string" || !BUTTON_ID.test(entry.id) || seen.has(entry.id)) continue;
|
|
7871
|
-
const label = cardLine(entry.label, STATUS_CARD_LIMITS.buttonLabel);
|
|
7872
|
-
if (!label) continue;
|
|
7873
|
-
seen.add(entry.id);
|
|
7874
|
-
const style = oneOf(BUTTON_STYLES, entry.style);
|
|
7875
|
-
buttons.push({
|
|
7876
|
-
id: entry.id,
|
|
7877
|
-
label,
|
|
7878
|
-
...style && style !== "neutral" && { style },
|
|
7879
|
-
...entry.once === true && { once: true }
|
|
7880
|
-
});
|
|
7881
|
-
}
|
|
7882
|
-
if (!buttons.length) return void 0;
|
|
7883
|
-
const chosen = typeof raw.chosen === "string" && seen.has(raw.chosen) ? raw.chosen : void 0;
|
|
7884
|
-
return {
|
|
7885
|
-
kind: "buttons",
|
|
7886
|
-
id,
|
|
7887
|
-
buttons,
|
|
7888
|
-
...chosen && { chosen },
|
|
7889
|
-
...raw.closed === true && { closed: true }
|
|
7890
|
-
};
|
|
7891
|
-
}
|
|
7892
|
-
}
|
|
7893
|
-
/**
|
|
7894
|
-
* A card as a sender may send it, or why not: the reader's rule, but anything it would leave out or cut is refused
|
|
7895
|
-
* here instead (an unknown status, a link that is not https, a list past its bound, a line too long), so a bot learns
|
|
7896
|
-
* at once and nothing it said is lost on the way. Line breaks and invisible characters are cleaned, as a reader would.
|
|
7897
|
-
*/
|
|
7898
|
-
function checkStatusCard(raw, now = Date.now()) {
|
|
7899
|
-
if (!isObject$4(raw)) return { error: "A card is an object" };
|
|
7900
|
-
const bytes = statusCardBytes(raw);
|
|
7901
|
-
if (bytes > STATUS_CARD_LIMITS.bytes) return { error: `The card takes ${bytes} bytes; at most ${STATUS_CARD_LIMITS.bytes}` };
|
|
7902
|
-
if (raw.kind !== "task" && raw.kind !== "routine" && raw.kind !== "buttons" && raw.kind !== "app" && raw.kind !== "usage") return { error: "kind is task, routine, buttons, app or usage" };
|
|
7903
|
-
if (typeof raw.id !== "string" || !ID$4.test(raw.id)) return { error: `id is 1 to ${STATUS_CARD_LIMITS.id} of A-Z a-z 0-9 _ . : - and does not start with -` };
|
|
7904
|
-
const line = (field, value, max, required = false) => {
|
|
7905
|
-
if (value === void 0 && !required) return null;
|
|
7906
|
-
if (typeof value !== "string" || !cardLine(value, Infinity)) return `${field} is text`;
|
|
7907
|
-
if ([...cardLine(value, Infinity)].length > max) return `${field} is at most ${max} characters`;
|
|
7908
|
-
return null;
|
|
7909
|
-
};
|
|
7910
|
-
const url = (field, value) => cardUrl(value) ? null : `${field} must be an https link of at most ${STATUS_CARD_LIMITS.url} characters`;
|
|
7911
|
-
const whole = (field, value, min = 0) => value === void 0 || count(value, min) !== void 0 ? null : `${field} is a whole number from ${min}`;
|
|
7912
|
-
const errors = [];
|
|
7913
|
-
if (raw.links !== void 0) {
|
|
7914
|
-
if (!Array.isArray(raw.links) || raw.links.length > STATUS_CARD_LIMITS.links) errors.push(`links is a list of at most ${STATUS_CARD_LIMITS.links}`);
|
|
7915
|
-
else for (const [i, link] of raw.links.entries()) {
|
|
7916
|
-
if (!isObject$4(link)) {
|
|
7917
|
-
errors.push(`links[${i}] is an object`);
|
|
7918
|
-
continue;
|
|
7919
|
-
}
|
|
7920
|
-
errors.push(url(`links[${i}].url`, link.url), line(`links[${i}].label`, link.label, STATUS_CARD_LIMITS.linkLabel));
|
|
7921
|
-
}
|
|
7922
|
-
}
|
|
7923
|
-
const share = (field, value) => value === void 0 || typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 100 ? null : `${field} is a number from 0 to 100`;
|
|
7924
|
-
const reset = (field, value) => value === void 0 || ahead(value, now) !== void 0 ? null : `${field} is a time in milliseconds, within a year`;
|
|
7925
|
-
if (raw.kind === "usage") {
|
|
7926
|
-
if (raw.links !== void 0) errors.push("a usage card takes no links");
|
|
7927
|
-
if (raw.left === void 0 && raw.used === void 0) errors.push("left (a percent) or used and limit");
|
|
7928
|
-
errors.push(share("left", raw.left));
|
|
7929
|
-
if (raw.used === void 0 !== (raw.limit === void 0)) errors.push("used and limit go together");
|
|
7930
|
-
else if (raw.used !== void 0) {
|
|
7931
|
-
errors.push(whole("used", raw.used), whole("limit", raw.limit, 1));
|
|
7932
|
-
if (typeof raw.used === "number" && typeof raw.limit === "number" && raw.used > raw.limit) errors.push("used is at most limit");
|
|
7933
|
-
}
|
|
7934
|
-
errors.push(line("label", raw.label, STATUS_CARD_LIMITS.usageLabel), line("account", raw.account, STATUS_CARD_LIMITS.usageLabel), line("window", raw.window, STATUS_CARD_LIMITS.usageWindow), reset("resetsAt", raw.resetsAt));
|
|
7935
|
-
if (raw.updatedAt !== void 0 && !(typeof raw.updatedAt === "number" && Number.isSafeInteger(raw.updatedAt) && raw.updatedAt > 0 && raw.updatedAt <= now + 3e5)) errors.push("updatedAt is a time in milliseconds, not in the future");
|
|
7936
|
-
if (raw.windows !== void 0) {
|
|
7937
|
-
if (!Array.isArray(raw.windows) || raw.windows.length > STATUS_CARD_LIMITS.usageWindows) errors.push(`windows is a list of at most ${STATUS_CARD_LIMITS.usageWindows}`);
|
|
7938
|
-
else for (const [i, entry] of raw.windows.entries()) {
|
|
7939
|
-
if (!isObject$4(entry)) {
|
|
7940
|
-
errors.push(`windows[${i}] is an object`);
|
|
7941
|
-
continue;
|
|
7942
|
-
}
|
|
7943
|
-
errors.push(line(`windows[${i}].window`, entry.window, STATUS_CARD_LIMITS.usageWindow, true));
|
|
7944
|
-
errors.push(entry.left === void 0 ? `windows[${i}].left is a number from 0 to 100` : share(`windows[${i}].left`, entry.left), reset(`windows[${i}].resetsAt`, entry.resetsAt));
|
|
7945
|
-
}
|
|
7946
|
-
}
|
|
7947
|
-
} else if (raw.kind === "app") {
|
|
7948
|
-
if (raw.links !== void 0) errors.push("an app card takes no links");
|
|
7949
|
-
if (!isAppRef(raw.ref)) errors.push("ref is <publisher key in z-base32>/<name>");
|
|
7950
|
-
else if (raw.id !== appCardId(raw.ref)) errors.push(`id is ${appCardId(raw.ref)}, made from ref`);
|
|
7951
|
-
if (!isAppHash(raw.digest)) errors.push("digest is a SHA-256 in base64url without padding");
|
|
7952
|
-
if (!appSequence(raw.sequence)) errors.push("sequence is a whole number from 0 to 2^53 - 1");
|
|
7953
|
-
errors.push(line("title", raw.title, STATUS_CARD_LIMITS.appTitle, true), line("version", raw.version, STATUS_CARD_LIMITS.appVersion));
|
|
7954
|
-
if (raw.url !== void 0) errors.push(url("url", raw.url));
|
|
7955
|
-
if (raw.opened !== void 0 && raw.opened !== true) errors.push("opened is true or left out");
|
|
7956
|
-
} else if (raw.kind === "buttons") {
|
|
7957
|
-
if (raw.links !== void 0) errors.push("buttons take no links");
|
|
7958
|
-
if (!Array.isArray(raw.buttons) || !raw.buttons.length || raw.buttons.length > STATUS_CARD_LIMITS.buttons) errors.push(`buttons is a list of 1 to ${STATUS_CARD_LIMITS.buttons}`);
|
|
7959
|
-
else {
|
|
7960
|
-
const ids = /* @__PURE__ */ new Set();
|
|
7961
|
-
for (const [i, button] of raw.buttons.entries()) {
|
|
7962
|
-
if (!isObject$4(button)) {
|
|
7963
|
-
errors.push(`buttons[${i}] is an object`);
|
|
7964
|
-
continue;
|
|
7965
|
-
}
|
|
7966
|
-
if (typeof button.id !== "string" || !BUTTON_ID.test(button.id)) errors.push(`buttons[${i}].id is 1 to ${STATUS_CARD_LIMITS.buttonId} of A-Z a-z 0-9 _ . : - and does not start with -`);
|
|
7967
|
-
else if (ids.has(button.id)) errors.push(`buttons[${i}].id "${button.id}" is used twice`);
|
|
7968
|
-
else ids.add(button.id);
|
|
7969
|
-
errors.push(line(`buttons[${i}].label`, button.label, STATUS_CARD_LIMITS.buttonLabel, true));
|
|
7970
|
-
if (button.style !== void 0 && !oneOf(BUTTON_STYLES, button.style)) errors.push(`buttons[${i}].style is one of ${BUTTON_STYLES.join(", ")}`);
|
|
7971
|
-
if (button.once !== void 0 && button.once !== true) errors.push(`buttons[${i}].once is true or left out`);
|
|
7972
|
-
}
|
|
7973
|
-
const clash = buttonLabelClash(raw.buttons);
|
|
7974
|
-
if (clash) errors.push(`buttons[${clash.at}].label ${JSON.stringify(cardLine(raw.buttons[clash.at].label, Infinity))} ${clash.kind === "label" ? `repeats buttons[${clash.with}].label` : `is buttons[${clash.with}].id`} (ignoring case and spaces at the ends)`);
|
|
7975
|
-
if (raw.chosen !== void 0 && !(typeof raw.chosen === "string" && ids.has(raw.chosen))) errors.push("chosen names one of the buttons");
|
|
7976
|
-
}
|
|
7977
|
-
if (raw.closed !== void 0 && raw.closed !== true) errors.push("closed is true or left out");
|
|
7978
|
-
} else if (raw.kind === "task") {
|
|
7979
|
-
errors.push(line("title", raw.title, STATUS_CARD_LIMITS.title, true));
|
|
7980
|
-
if (!oneOf(TASK_STATUSES, raw.status)) errors.push(`status is one of ${TASK_STATUSES.join(", ")}`);
|
|
7981
|
-
if (raw.progress !== void 0 && !(typeof raw.progress === "number" && Number.isFinite(raw.progress) && raw.progress >= 0 && raw.progress <= 100)) errors.push("progress is a number from 0 to 100");
|
|
7982
|
-
if (raw.done === void 0 !== (raw.total === void 0)) errors.push("done and total go together");
|
|
7983
|
-
else if (raw.done !== void 0) {
|
|
7984
|
-
errors.push(whole("done", raw.done), whole("total", raw.total, 1));
|
|
7985
|
-
if (typeof raw.done === "number" && typeof raw.total === "number" && raw.done > raw.total) errors.push("done is at most total");
|
|
7986
|
-
}
|
|
7987
|
-
errors.push(line("step", raw.step, STATUS_CARD_LIMITS.line), line("branch", raw.branch, STATUS_CARD_LIMITS.branch));
|
|
7988
|
-
if (raw.pr !== void 0) {
|
|
7989
|
-
if (!isObject$4(raw.pr)) errors.push("pr is an object");
|
|
7990
|
-
else {
|
|
7991
|
-
errors.push(url("pr.url", raw.pr.url), whole("pr.number", raw.pr.number, 1), whole("pr.additions", raw.pr.additions), whole("pr.deletions", raw.pr.deletions), whole("pr.files", raw.pr.files));
|
|
7992
|
-
if (raw.pr.state !== void 0 && !oneOf(PR_STATES, raw.pr.state)) errors.push(`pr.state is one of ${PR_STATES.join(", ")}`);
|
|
7993
|
-
if (raw.pr.checks !== void 0 && !oneOf(PR_CHECKS, raw.pr.checks)) errors.push(`pr.checks is one of ${PR_CHECKS.join(", ")}`);
|
|
7994
|
-
}
|
|
7995
|
-
}
|
|
7996
|
-
if (raw.tags !== void 0) {
|
|
7997
|
-
if (!Array.isArray(raw.tags) || raw.tags.length > STATUS_CARD_LIMITS.tags) errors.push(`tags is a list of at most ${STATUS_CARD_LIMITS.tags}`);
|
|
7998
|
-
else {
|
|
7999
|
-
const seen = /* @__PURE__ */ new Set();
|
|
8000
|
-
for (const [i, tag] of raw.tags.entries()) {
|
|
8001
|
-
const problem = line(`tags[${i}]`, tag, STATUS_CARD_LIMITS.tag, true);
|
|
8002
|
-
errors.push(problem);
|
|
8003
|
-
if (problem) continue;
|
|
8004
|
-
const clean = cardLine(tag, Infinity);
|
|
8005
|
-
if (seen.has(clean)) errors.push(`tags[${i}] ${JSON.stringify(clean)} is there twice`);
|
|
8006
|
-
seen.add(clean);
|
|
8007
|
-
}
|
|
8008
|
-
}
|
|
8009
|
-
}
|
|
8010
|
-
if (raw.parent !== void 0) {
|
|
8011
|
-
if (typeof raw.parent !== "string" || !ID$4.test(raw.parent)) errors.push(`parent is another task's id: 1 to ${STATUS_CARD_LIMITS.id} of A-Z a-z 0-9 _ . : - and does not start with -`);
|
|
8012
|
-
else if (raw.parent === raw.id) errors.push("parent is another task's id, not its own");
|
|
8013
|
-
}
|
|
8014
|
-
for (const field of ["startedAt", "updatedAt"]) {
|
|
8015
|
-
const value = raw[field];
|
|
8016
|
-
if (value !== void 0 && !(typeof value === "number" && Number.isSafeInteger(value) && value > 0 && value <= now + 3e5)) errors.push(`${field} is a time in milliseconds, not in the future`);
|
|
8017
|
-
}
|
|
8018
|
-
if (raw.items !== void 0) {
|
|
8019
|
-
if (!Array.isArray(raw.items) || raw.items.length > STATUS_CARD_LIMITS.items) errors.push(`items is a list of at most ${STATUS_CARD_LIMITS.items}`);
|
|
8020
|
-
else for (const [i, item] of raw.items.entries()) {
|
|
8021
|
-
if (!isObject$4(item)) {
|
|
8022
|
-
errors.push(`items[${i}] is an object`);
|
|
8023
|
-
continue;
|
|
8024
|
-
}
|
|
8025
|
-
errors.push(line(`items[${i}].text`, item.text, STATUS_CARD_LIMITS.line, true));
|
|
8026
|
-
if (item.state !== void 0 && !oneOf(ITEM_STATES, item.state)) errors.push(`items[${i}].state is one of ${ITEM_STATES.join(", ")}`);
|
|
8027
|
-
}
|
|
8028
|
-
}
|
|
8029
|
-
} else {
|
|
8030
|
-
errors.push(line("name", raw.name, STATUS_CARD_LIMITS.title, true), line("schedule", raw.schedule, STATUS_CARD_LIMITS.schedule, true));
|
|
8031
|
-
if (!oneOf(ROUTINE_STATES, raw.state)) errors.push(`state is one of ${ROUTINE_STATES.join(", ")}`);
|
|
8032
|
-
if (raw.cron !== void 0 && !(typeof raw.cron === "string" && CRON.test(raw.cron.trim()))) errors.push(`cron is a cron line of at most ${STATUS_CARD_LIMITS.cron} characters`);
|
|
8033
|
-
if (raw.nextRunAt !== void 0 && !(typeof raw.nextRunAt === "number" && Number.isSafeInteger(raw.nextRunAt) && raw.nextRunAt > 0 && raw.nextRunAt <= now + STATUS_CARD_LIMITS.nextRunMs)) errors.push("nextRunAt is a time in milliseconds, within a year");
|
|
8034
|
-
const run = (field, value) => {
|
|
8035
|
-
if (!isObject$4(value)) return errors.push(`${field} is an object`);
|
|
8036
|
-
if (!(typeof value.at === "number" && Number.isSafeInteger(value.at) && value.at > 0 && value.at <= now + 3e5)) errors.push(`${field}.at is a time in milliseconds, not in the future`);
|
|
8037
|
-
if (!oneOf(RUN_RESULTS, value.result)) errors.push(`${field}.result is one of ${RUN_RESULTS.join(", ")}`);
|
|
8038
|
-
errors.push(line(`${field}.summary`, value.summary, STATUS_CARD_LIMITS.line));
|
|
8039
|
-
};
|
|
8040
|
-
if (raw.lastRun !== void 0) run("lastRun", raw.lastRun);
|
|
8041
|
-
if (raw.runs !== void 0) {
|
|
8042
|
-
if (!Array.isArray(raw.runs) || raw.runs.length > STATUS_CARD_LIMITS.runs) errors.push(`runs is a list of at most ${STATUS_CARD_LIMITS.runs}`);
|
|
8043
|
-
else raw.runs.forEach((entry, i) => run(`runs[${i}]`, entry));
|
|
8044
|
-
}
|
|
8045
|
-
}
|
|
8046
|
-
const error = errors.find((e) => typeof e === "string");
|
|
8047
|
-
if (error) return { error };
|
|
8048
|
-
const card = readStatusCard(raw, now);
|
|
8049
|
-
return card ? { card } : { error: "The card does not hold" };
|
|
8050
|
-
}
|
|
8051
|
-
/** The progress a task shows, 0 to 100: its own, else its steps done of total; undefined when it says neither. */
|
|
8052
|
-
function taskProgress(card) {
|
|
8053
|
-
if (card.progress !== void 0) return card.progress;
|
|
8054
|
-
return card.done !== void 0 && card.total ? Math.round(card.done / card.total * 100) : void 0;
|
|
8055
|
-
}
|
|
8056
|
-
var TASK_MARK = {
|
|
8057
|
-
queued: "⏳",
|
|
8058
|
-
running: "🔄",
|
|
8059
|
-
blocked: "⛔",
|
|
8060
|
-
done: "✅",
|
|
8061
|
-
failed: "❌",
|
|
8062
|
-
cancelled: "🚫"
|
|
8063
|
-
};
|
|
8064
|
-
var STATUS_WORD = {
|
|
8065
|
-
queued: "Queued",
|
|
8066
|
-
running: "Running",
|
|
8067
|
-
blocked: "Blocked",
|
|
8068
|
-
done: "Done",
|
|
8069
|
-
failed: "Failed",
|
|
8070
|
-
cancelled: "Cancelled"
|
|
8071
|
-
};
|
|
8072
|
-
var RESULT_WORD = {
|
|
8073
|
-
ok: "ok",
|
|
8074
|
-
failed: "failed",
|
|
8075
|
-
skipped: "skipped"
|
|
8076
|
-
};
|
|
8077
|
-
/** A time as the fallback text says it: in UTC, to the minute ("2026-09-30 01:00 UTC"). */
|
|
8078
|
-
function cardTimeUtc(ms) {
|
|
8079
|
-
return `${new Date(ms).toISOString().slice(0, 16).replace("T", " ")} UTC`;
|
|
8080
|
-
}
|
|
8081
|
-
/**
|
|
8082
|
-
* The text a card's message carries: what an app without cards shows, the DHT floor carries, the chat list previews
|
|
8083
|
-
* and search finds. Plain lines, the first naming the card, no bars or layout that would read badly elsewhere.
|
|
8084
|
-
*/
|
|
8085
|
-
function statusCardText(card) {
|
|
8086
|
-
if (card.kind === "app") {
|
|
8087
|
-
const named = `${card.title}${card.version ? ` ${card.version}` : ""}`;
|
|
8088
|
-
return [card.opened ? `🧩 Opened ${named} in this chat (Ghostly app)` : `🧩 ${named} (Ghostly app)`, ...card.url ? [card.url] : []].join("\n");
|
|
8089
|
-
}
|
|
8090
|
-
if (card.kind === "usage") {
|
|
8091
|
-
const left = `${usageLeft(card)}% left${card.window ? ` (${card.window})` : ""}`;
|
|
8092
|
-
return [
|
|
8093
|
-
`📊 ${card.label ?? "Usage"}`,
|
|
8094
|
-
...card.account ? [card.account] : [],
|
|
8095
|
-
left,
|
|
8096
|
-
...card.used !== void 0 && card.limit !== void 0 ? [`${card.used} of ${card.limit} used`] : [],
|
|
8097
|
-
...card.resetsAt ? [`resets ${cardTimeUtc(card.resetsAt)}`] : [],
|
|
8098
|
-
...(card.windows ?? []).map((w) => `${w.window} ${w.left}%`)
|
|
8099
|
-
].join(" · ");
|
|
8100
|
-
}
|
|
8101
|
-
if (card.kind === "buttons") return `Reply: ${card.buttons.map((b) => b.label).join(" / ")}`;
|
|
8102
|
-
if (card.kind === "task") {
|
|
8103
|
-
const progress = taskProgress(card);
|
|
8104
|
-
const state = [
|
|
8105
|
-
STATUS_WORD[card.status],
|
|
8106
|
-
...progress !== void 0 ? [`${progress}%`] : [],
|
|
8107
|
-
...card.done !== void 0 && card.total ? [`${card.done} of ${card.total} steps`] : []
|
|
8108
|
-
].join(" · ");
|
|
8109
|
-
const standing = card.pr && [...card.pr.state ? [card.pr.state] : [], ...card.pr.checks ? [`checks ${card.pr.checks}`] : []].join(", ");
|
|
8110
|
-
const pr = card.pr && [
|
|
8111
|
-
`PR${card.pr.number !== void 0 ? ` #${card.pr.number}` : ""}`,
|
|
8112
|
-
...card.pr.additions !== void 0 || card.pr.deletions !== void 0 ? [`+${card.pr.additions ?? 0} -${card.pr.deletions ?? 0}`] : [],
|
|
8113
|
-
...standing ? [`(${standing})`] : []
|
|
8114
|
-
].join(" ") + `: ${card.pr.url}`;
|
|
8115
|
-
const now = card.step && ACTIVE_TASK_STATUSES.includes(card.status) ? [`Now: ${card.step}`] : [];
|
|
8116
|
-
return [
|
|
8117
|
-
`${TASK_MARK[card.status]} ${card.title}`,
|
|
8118
|
-
state,
|
|
8119
|
-
...now,
|
|
8120
|
-
...pr ? [pr] : [],
|
|
8121
|
-
...card.tags?.length ? [`Tags: ${card.tags.join(", ")}`] : [],
|
|
8122
|
-
...card.parent ? [`Part of: ${card.parent}`] : []
|
|
8123
|
-
].join("\n");
|
|
8124
|
-
}
|
|
8125
|
-
const last = card.lastRun && `Last run: ${RESULT_WORD[card.lastRun.result]}, ${cardTimeUtc(card.lastRun.at)}${card.lastRun.summary ? ` · ${card.lastRun.summary}` : ""}`;
|
|
8126
|
-
return [
|
|
8127
|
-
`🔁 ${card.name}`,
|
|
8128
|
-
`${card.schedule} · ${card.state === "active" ? "active" : "paused"}`,
|
|
8129
|
-
...last ? [last] : [],
|
|
8130
|
-
...card.nextRunAt && card.state === "active" ? [`Next run: ${cardTimeUtc(card.nextRunAt)}`] : []
|
|
8131
|
-
].join("\n");
|
|
8132
|
-
}
|
|
8133
|
-
/** Whether a message's edit number holds: a text's bound, or a card message's when the edit carries a card. */
|
|
8134
|
-
function cardEditNumber(e) {
|
|
8135
|
-
return Number.isSafeInteger(e) && e >= 1 && e <= STATUS_CARD_LIMITS.edits;
|
|
8136
|
-
}
|
|
8137
|
-
//#endregion
|
|
8138
|
-
//#region ../core/src/replies.ts
|
|
8139
|
-
var REPLY_LIMITS = {
|
|
8140
|
-
/** Code points of the line of the original, an ellipsis included when it was cut. */
|
|
8141
|
-
snippet: 120,
|
|
8142
|
-
/** Characters of the original's id. */
|
|
8143
|
-
id: 128,
|
|
8144
|
-
/** Characters of a line as it arrives, before it is cleaned: anything longer is not a line of a message. */
|
|
8145
|
-
raw: 2048
|
|
8146
|
-
};
|
|
8147
|
-
/** A paired chat's message id, a payment id or a file's wire id; a group message's `sender:epoch:seq`. */
|
|
8148
|
-
var REPLY_ID = /^[A-Za-z0-9_:.-]{1,128}$/;
|
|
8149
|
-
/** The two authors a paired chat has, from the reply author's side. */
|
|
8150
|
-
var PAIRED_REPLY_AUTHORS = ["sender", "recipient"];
|
|
8151
|
-
/** A picture sent inline has no line worth quoting. */
|
|
8152
|
-
var INLINE_PICTURE = /^data:image\//i;
|
|
8153
|
-
/**
|
|
8154
|
-
* A line of a message to quote: plain text on one line (line breaks and runs of spaces become one space), without
|
|
8155
|
-
* control, invisible or direction characters (WISP 400, as for names), at most `REPLY_LIMITS.snippet` code points,
|
|
8156
|
-
* the last of them an ellipsis when it was cut.
|
|
8157
|
-
*/
|
|
8158
|
-
function replySnippet(text) {
|
|
8159
|
-
if (INLINE_PICTURE.test(text.trimStart())) return "🖼️ Picture";
|
|
8160
|
-
const clean = sanitizeDisplayText(text.slice(0, REPLY_LIMITS.raw * 2).replace(/\s+/gu, " "), REPLY_LIMITS.snippet + 1) ?? "";
|
|
8161
|
-
const points = [...clean];
|
|
8162
|
-
return points.length > REPLY_LIMITS.snippet ? `${points.slice(0, REPLY_LIMITS.snippet - 1).join("").trimEnd()}…` : clean;
|
|
8163
|
-
}
|
|
8164
|
-
/**
|
|
8165
|
-
* A reply as a receiver takes it: an object with an id of this chat's shape, a line (cleaned and cut again here,
|
|
8166
|
-
* whatever the sender did) and an author `author` accepts. Anything else is no reply: the message stays, without it.
|
|
8167
|
-
*/
|
|
8168
|
-
function readReply(raw, author) {
|
|
8169
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
|
|
8170
|
-
const { i, s, f, b } = raw;
|
|
8171
|
-
if (typeof i !== "string" || !REPLY_ID.test(i)) return void 0;
|
|
8172
|
-
if (typeof s !== "string" || s.length > REPLY_LIMITS.raw) return void 0;
|
|
8173
|
-
if (typeof f !== "string" || !author(f)) return void 0;
|
|
8174
|
-
return {
|
|
8175
|
-
i,
|
|
8176
|
-
s: replySnippet(s),
|
|
8177
|
-
f,
|
|
8178
|
-
...typeof b === "string" && BUTTON_ID.test(b) && { b }
|
|
8179
|
-
};
|
|
8180
|
-
}
|
|
8181
|
-
/** Who may have written the original in a paired chat. */
|
|
8182
|
-
var pairedReplyAuthor = (f) => PAIRED_REPLY_AUTHORS.includes(f);
|
|
8183
|
-
/** Who may have written the original in a group: a member key (whether they are still a member is the reader's to see). */
|
|
8184
|
-
var groupReplyAuthor = (f) => MEMBER_KEY.test(f);
|
|
8185
|
-
/** Only the wire fields, in a fixed order, the line cleaned: what the sender puts on a frame. */
|
|
8186
|
-
var wireReply = (reply) => ({
|
|
8187
|
-
i: reply.i,
|
|
8188
|
-
s: replySnippet(reply.s),
|
|
8189
|
-
f: reply.f,
|
|
8190
|
-
...reply.b && { b: reply.b }
|
|
8191
|
-
});
|
|
8192
|
-
//#endregion
|
|
8193
|
-
//#region ../core/src/reactions.ts
|
|
6865
|
+
/** Who may have written the original in a paired chat. */
|
|
6866
|
+
var pairedReplyAuthor = (f) => PAIRED_REPLY_AUTHORS.includes(f);
|
|
6867
|
+
/** Who may have written the original in a group: a member key (whether they are still a member is the reader's to see). */
|
|
6868
|
+
var groupReplyAuthor = (f) => MEMBER_KEY.test(f);
|
|
6869
|
+
/** Only the wire fields, in a fixed order, the line cleaned: what the sender puts on a frame. */
|
|
6870
|
+
var wireReply = (reply) => ({
|
|
6871
|
+
i: reply.i,
|
|
6872
|
+
s: replySnippet(reply.s),
|
|
6873
|
+
f: reply.f,
|
|
6874
|
+
...reply.b && { b: reply.b }
|
|
6875
|
+
});
|
|
6876
|
+
//#endregion
|
|
6877
|
+
//#region ../core/src/reactions.ts
|
|
8194
6878
|
/**
|
|
8195
6879
|
* Reactions to messages (WISP 400 § Reactions): one emoji per person per message, like WhatsApp. Whoever reacts says
|
|
8196
6880
|
* which message (the id both sides know it by, as a reply names it), the emoji, and a number of its own that only
|
|
@@ -8533,7 +7217,7 @@ function previewImageSize(value) {
|
|
|
8533
7217
|
if (binary.length > LINK_PREVIEW_LIMITS.imageBytes) return;
|
|
8534
7218
|
const bytes = new Uint8Array(binary.length);
|
|
8535
7219
|
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
8536
|
-
return jpegSize
|
|
7220
|
+
return jpegSize(bytes);
|
|
8537
7221
|
}
|
|
8538
7222
|
/** The thumbnail if it is one Ghostly shows: a JPEG data URL within the limits whose header declares a sane size. */
|
|
8539
7223
|
function sanitizePreviewImage(value) {
|
|
@@ -8672,521 +7356,27 @@ var RateWindow = class {
|
|
|
8672
7356
|
recent = [];
|
|
8673
7357
|
constructor(limit, windowMs, now = Date.now) {
|
|
8674
7358
|
this.limit = limit;
|
|
8675
|
-
this.windowMs = windowMs;
|
|
8676
|
-
this.now = now;
|
|
8677
|
-
}
|
|
8678
|
-
/** Counts one now; false (and not counted) when the window is full. */
|
|
8679
|
-
take() {
|
|
8680
|
-
const now = this.now();
|
|
8681
|
-
this.recent = this.recent.filter((at) => now - at < this.windowMs);
|
|
8682
|
-
if (this.recent.length >= this.limit) return false;
|
|
8683
|
-
this.recent.push(now);
|
|
8684
|
-
return true;
|
|
8685
|
-
}
|
|
8686
|
-
/** Milliseconds until the window has room again; 0 when it has room now. */
|
|
8687
|
-
wait() {
|
|
8688
|
-
const now = this.now();
|
|
8689
|
-
this.recent = this.recent.filter((at) => now - at < this.windowMs);
|
|
8690
|
-
return this.recent.length < this.limit ? 0 : this.recent[0] + this.windowMs - now;
|
|
8691
|
-
}
|
|
8692
|
-
reset() {
|
|
8693
|
-
this.recent = [];
|
|
8694
|
-
}
|
|
8695
|
-
};
|
|
8696
|
-
//#endregion
|
|
8697
|
-
//#region ../core/src/video.ts
|
|
8698
|
-
var VIDEO_LIMITS = {
|
|
8699
|
-
/** The poster's bytes: with the rest of an offer, a frame stays far below the 60 KiB a session carries. */
|
|
8700
|
-
maxPosterBytes: 12288,
|
|
8701
|
-
/** The poster's longest side, in pixels, as the sender draws it. */
|
|
8702
|
-
posterEdge: 320,
|
|
8703
|
-
/** Ten hours: longer is a mistake, not a video. */
|
|
8704
|
-
maxDurationMs: 36e6,
|
|
8705
|
-
/** Pixels on either side (8K and a bit). */
|
|
8706
|
-
maxEdge: 16384
|
|
8707
|
-
};
|
|
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
|
-
/** Whether a file is a video, by its type: shown in a video bubble whether or not it came with a description. */
|
|
8714
|
-
function isVideoMime(mime) {
|
|
8715
|
-
return mime.split(";")[0].trim().toLowerCase().startsWith("video/");
|
|
8716
|
-
}
|
|
8717
|
-
var BASE64URL = /^[A-Za-z0-9_-]+$/;
|
|
8718
|
-
var JPEG_START = [
|
|
8719
|
-
255,
|
|
8720
|
-
216,
|
|
8721
|
-
255
|
|
8722
|
-
];
|
|
8723
|
-
/**
|
|
8724
|
-
* A peer's `video` description, or undefined when it is not one. It never refuses the file. A poster that is
|
|
8725
|
-
* too large or not a JPEG is dropped on its own; the length and size are kept.
|
|
8726
|
-
*/
|
|
8727
|
-
function parseVideoMeta(value, mime) {
|
|
8728
|
-
if (mime !== void 0 && !isVideoMime(mime)) return void 0;
|
|
8729
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
8730
|
-
const { duration, width, height, poster } = value;
|
|
8731
|
-
if (typeof duration !== "number" || !Number.isSafeInteger(duration) || duration <= 0 || duration > VIDEO_LIMITS.maxDurationMs) return void 0;
|
|
8732
|
-
const edge = (n) => typeof n === "number" && Number.isSafeInteger(n) && n > 0 && n <= VIDEO_LIMITS.maxEdge;
|
|
8733
|
-
if (!edge(width) || !edge(height)) return void 0;
|
|
8734
|
-
const meta = {
|
|
8735
|
-
duration,
|
|
8736
|
-
width,
|
|
8737
|
-
height
|
|
8738
|
-
};
|
|
8739
|
-
if (typeof poster === "string" && poster.length <= Math.ceil(VIDEO_LIMITS.maxPosterBytes * 4 / 3) && BASE64URL.test(poster)) try {
|
|
8740
|
-
const bytes = fromBase64Url(poster);
|
|
8741
|
-
if (bytes.length <= VIDEO_LIMITS.maxPosterBytes && JPEG_START.every((b, i) => bytes[i] === b)) meta.poster = poster;
|
|
8742
|
-
} catch {}
|
|
8743
|
-
return meta;
|
|
8744
|
-
}
|
|
8745
|
-
/** `0:07`, `12:45`, `1:02:03`: as a player shows a video's length. */
|
|
8746
|
-
function formatVideoDuration(ms) {
|
|
8747
|
-
const total = Math.max(0, Math.floor((Number.isFinite(ms) ? ms : 0) / 1e3));
|
|
8748
|
-
const hours = Math.floor(total / 3600), minutes = Math.floor(total % 3600 / 60), seconds = String(total % 60).padStart(2, "0");
|
|
8749
|
-
return hours ? `${hours}:${String(minutes).padStart(2, "0")}:${seconds}` : `${minutes}:${seconds}`;
|
|
8750
|
-
}
|
|
8751
|
-
//#endregion
|
|
8752
|
-
//#region ../core/src/voice.ts
|
|
8753
|
-
var VOICE_LIMITS = {
|
|
8754
|
-
/** Fifteen minutes: at the bitrate recorded (32 kbit/s) about 3.6 MB, well inside a held item's 8 MiB. */
|
|
8755
|
-
maxDurationMs: 9e5,
|
|
8756
|
-
/** Bars drawn for a recording. */
|
|
8757
|
-
bars: 64,
|
|
8758
|
-
/** Bars accepted from a peer. */
|
|
8759
|
-
maxBars: 128,
|
|
8760
|
-
/** Asked of the recorder; opus speech is clear well below it. */
|
|
8761
|
-
audioBitsPerSecond: 32e3
|
|
8762
|
-
};
|
|
8763
|
-
/**
|
|
8764
|
-
* Audio a player may be handed as it is. None of these runs anything in the page's origin,
|
|
8765
|
-
* so received bytes of these types are served with their type (see `safeBlobType`).
|
|
8766
|
-
*/
|
|
8767
|
-
var PLAYABLE_AUDIO = /^audio\/(webm|ogg|opus|mp4|mpeg|mp3|aac|x-m4a|wav|x-wav|wave|flac|x-flac)$/;
|
|
8768
|
-
/** The part of a type before its parameters, in lower case: `audio/webm;codecs=opus` → `audio/webm`. */
|
|
8769
|
-
function baseMime(mime) {
|
|
8770
|
-
return mime.split(";")[0].trim().toLowerCase();
|
|
8771
|
-
}
|
|
8772
|
-
/**
|
|
8773
|
-
* A peer's `voice` description, or undefined when it is not one. It never refuses the file:
|
|
8774
|
-
* a bad description only means the file is shown as a file.
|
|
8775
|
-
*/
|
|
8776
|
-
function parseVoiceMeta(value, mime) {
|
|
8777
|
-
if (mime !== void 0 && !PLAYABLE_AUDIO.test(baseMime(mime))) return void 0;
|
|
8778
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
8779
|
-
const { duration, peaks } = value;
|
|
8780
|
-
if (typeof duration !== "number" || !Number.isSafeInteger(duration) || duration <= 0 || duration > VOICE_LIMITS.maxDurationMs + 5e3) return void 0;
|
|
8781
|
-
if (!Array.isArray(peaks) || peaks.length === 0 || peaks.length > VOICE_LIMITS.maxBars) return void 0;
|
|
8782
|
-
if (!peaks.every((peak) => typeof peak === "number" && Number.isInteger(peak) && peak >= 0 && peak <= 255)) return void 0;
|
|
8783
|
-
return {
|
|
8784
|
-
duration,
|
|
8785
|
-
peaks: [...peaks]
|
|
8786
|
-
};
|
|
8787
|
-
}
|
|
8788
|
-
/**
|
|
8789
|
-
* How the level is metered while recording (apps/ui/src/lib/voiceRecorder.ts): every `intervalMs`, the RMS of the last
|
|
8790
|
-
* `window` samples an AnalyserNode holds (at a 48 kHz context, about 21 ms of sound).
|
|
8791
|
-
*/
|
|
8792
|
-
var VOICE_METER = {
|
|
8793
|
-
intervalMs: 50,
|
|
8794
|
-
window: 1024,
|
|
8795
|
-
windowRate: 48e3
|
|
8796
|
-
};
|
|
8797
|
-
/** One reading of the meter, 0-1: the RMS of the samples, lifted because speech sits low on a linear scale. */
|
|
8798
|
-
function voiceLevel(samples) {
|
|
8799
|
-
if (samples.length === 0) return 0;
|
|
8800
|
-
let sum = 0;
|
|
8801
|
-
for (let i = 0; i < samples.length; i++) sum += samples[i] * samples[i];
|
|
8802
|
-
return Math.min(1, Math.sqrt(sum / samples.length) * 4);
|
|
8803
|
-
}
|
|
8804
|
-
/**
|
|
8805
|
-
* The readings the recorder would have taken of this sound (mono, -1 to 1), for a recording made elsewhere: one
|
|
8806
|
-
* every `intervalMs` of it, each over the window that ends there, as long in time as the analyser's.
|
|
8807
|
-
*/
|
|
8808
|
-
function voiceLevelsOf(mono, sampleRate) {
|
|
8809
|
-
if (!(sampleRate > 0) || mono.length === 0) return [];
|
|
8810
|
-
const window = Math.max(1, Math.round(VOICE_METER.window * sampleRate / VOICE_METER.windowRate));
|
|
8811
|
-
const levels = [];
|
|
8812
|
-
for (let step = 1;; step++) {
|
|
8813
|
-
const end = Math.round(step * VOICE_METER.intervalMs * sampleRate / 1e3);
|
|
8814
|
-
if (end > mono.length) break;
|
|
8815
|
-
levels.push(voiceLevel(mono.subarray(Math.max(0, end - window), end)));
|
|
8816
|
-
}
|
|
8817
|
-
return levels;
|
|
8818
|
-
}
|
|
8819
|
-
/** The waveform sent with a recording made elsewhere, drawn as the recorder draws its own. */
|
|
8820
|
-
function voicePeaksOf(mono, sampleRate, bars = VOICE_LIMITS.bars) {
|
|
8821
|
-
return downsamplePeaks(voiceLevelsOf(mono, sampleRate), bars);
|
|
8822
|
-
}
|
|
8823
|
-
/** Below this a recording is silence, and is drawn flat rather than stretched to fill the bars. */
|
|
8824
|
-
var QUIET = .02;
|
|
8825
|
-
/**
|
|
8826
|
-
* Bars from the loudness measured while recording (RMS, 0-1, oldest first): the loudest
|
|
8827
|
-
* sample of each stretch, scaled so the loudest bar is full height.
|
|
8828
|
-
*/
|
|
8829
|
-
function downsamplePeaks(levels, bars = VOICE_LIMITS.bars) {
|
|
8830
|
-
const count = Math.max(1, Math.min(VOICE_LIMITS.maxBars, Math.floor(bars)));
|
|
8831
|
-
const clean = Array.from(levels, (level) => Number.isFinite(level) ? Math.min(1, Math.max(0, level)) : 0);
|
|
8832
|
-
if (clean.length === 0) return new Array(count).fill(0);
|
|
8833
|
-
const loudest = Math.max(QUIET, ...clean);
|
|
8834
|
-
const out = [];
|
|
8835
|
-
for (let bar = 0; bar < count; bar++) {
|
|
8836
|
-
const from = Math.floor(bar * clean.length / count);
|
|
8837
|
-
const to = Math.max(from + 1, Math.floor((bar + 1) * clean.length / count));
|
|
8838
|
-
let peak = 0;
|
|
8839
|
-
for (let i = from; i < to && i < clean.length; i++) peak = Math.max(peak, clean[i]);
|
|
8840
|
-
out.push(Math.round(peak / loudest * 255));
|
|
8841
|
-
}
|
|
8842
|
-
return out;
|
|
8843
|
-
}
|
|
8844
|
-
/** `0:07`, `12:45`: minutes and seconds, as a player shows them. */
|
|
8845
|
-
function formatVoiceDuration(ms) {
|
|
8846
|
-
const total = Math.max(0, Math.floor((Number.isFinite(ms) ? ms : 0) / 1e3));
|
|
8847
|
-
return `${Math.floor(total / 60)}:${String(total % 60).padStart(2, "0")}`;
|
|
8848
|
-
}
|
|
8849
|
-
/** The line a file message shows in the chat list and in notifications. */
|
|
8850
|
-
function fileMessageText(file) {
|
|
8851
|
-
if (file.voice) return `🎤 Voice message (${formatVoiceDuration(file.voice.duration)})`;
|
|
8852
|
-
if (file.video) return `🎬 Video (${formatVideoDuration(file.video.duration)})`;
|
|
8853
|
-
return `📎 ${file.name}`;
|
|
8854
|
-
}
|
|
8855
|
-
//#endregion
|
|
8856
|
-
//#region ../core/src/files.ts
|
|
8857
|
-
var FILE_ID = /^[A-Za-z0-9_-]{8,64}$/;
|
|
8858
|
-
var MIME = /^[a-z0-9][a-z0-9!#$&^_.+-]{0,63}\/[a-z0-9][a-z0-9!#$&^_.+-]{0,63}$/i;
|
|
8859
|
-
var MAX_NAME_LENGTH = 200;
|
|
8860
|
-
/**
|
|
8861
|
-
* Invisible and direction-changing characters: bidi overrides turn
|
|
8862
|
-
* "invoice\u202Efdp.exe" into what reads as "invoiceexe.pdf", zero-width ones hide
|
|
8863
|
-
* text. Cf covers both; line and paragraph separators break the layout.
|
|
8864
|
-
*/
|
|
8865
|
-
var INVISIBLE = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu;
|
|
8866
|
-
/**
|
|
8867
|
-
* Names Windows keeps for devices, whatever the case, with any extension, and with spaces before the dot:
|
|
8868
|
-
* "CON", "nul.txt", "COM1 .log" open a device there, not a file.
|
|
8869
|
-
*/
|
|
8870
|
-
var WINDOWS_DEVICE = /^(CON|PRN|AUX|NUL|CONIN\$|CONOUT\$|COM[0-9¹²³]|LPT[0-9¹²³])\s*(\.|$)/i;
|
|
8871
|
-
/**
|
|
8872
|
-
* At most `MAX_NAME_LENGTH` characters. A longer name loses the end of its stem, not its extension (up to 15
|
|
8873
|
-
* characters after the last dot): "…report.pdf" stays a PDF to the system it is saved on.
|
|
8874
|
-
*/
|
|
8875
|
-
function shorten(name) {
|
|
8876
|
-
const chars = [...name];
|
|
8877
|
-
if (chars.length <= MAX_NAME_LENGTH) return name;
|
|
8878
|
-
const extension = [.../\.[^.\s]{1,15}$/u.exec(name)?.[0] ?? ""];
|
|
8879
|
-
return chars.slice(0, MAX_NAME_LENGTH - extension.length).join("").trimEnd() + extension.join("");
|
|
8880
|
-
}
|
|
8881
|
-
/** A file name is display text and a download suggestion, never a path. */
|
|
8882
|
-
function sanitizeFileName(name) {
|
|
8883
|
-
const clean = shorten(name.replace(INVISIBLE, "").replace(/[/\\:]/g, "").replace(/^[\s.]+/, "").trim());
|
|
8884
|
-
if (WINDOWS_DEVICE.test(clean)) return shorten("_" + clean);
|
|
8885
|
-
return clean || "file";
|
|
8886
|
-
}
|
|
8887
|
-
function sanitizeMime(mime) {
|
|
8888
|
-
return MIME.test(mime) ? mime.toLowerCase() : "application/octet-stream";
|
|
8889
|
-
}
|
|
8890
|
-
/** A size for people: B, KB, MB, GB, TB (powers of 1024), one decimal from KB up. */
|
|
8891
|
-
function formatFileSize(bytes) {
|
|
8892
|
-
if (bytes < 1024) return `${bytes} B`;
|
|
8893
|
-
const units = [
|
|
8894
|
-
"KB",
|
|
8895
|
-
"MB",
|
|
8896
|
-
"GB",
|
|
8897
|
-
"TB"
|
|
8898
|
-
];
|
|
8899
|
-
let value = bytes / 1024, unit = 0;
|
|
8900
|
-
while (value >= 1024 && unit < units.length - 1) {
|
|
8901
|
-
value /= 1024;
|
|
8902
|
-
unit++;
|
|
8903
|
-
}
|
|
8904
|
-
return `${value.toFixed(1)} ${units[unit]}`;
|
|
8905
|
-
}
|
|
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
|
-
var FileTransfers = class {
|
|
8919
|
-
channel;
|
|
8920
|
-
events;
|
|
8921
|
-
nextStreamId = 1;
|
|
8922
|
-
incoming = /* @__PURE__ */ new Map();
|
|
8923
|
-
outgoing = /* @__PURE__ */ new Map();
|
|
8924
|
-
constructor(channel, events) {
|
|
8925
|
-
this.channel = channel;
|
|
8926
|
-
this.events = events;
|
|
8927
|
-
}
|
|
8928
|
-
/** Streams a file to the peer. Resolves once every byte is handed to the channel. */
|
|
8929
|
-
async send(file, source) {
|
|
8930
|
-
if (file.size > LIMITS.maxFileBytes) throw new Error("File is too large");
|
|
8931
|
-
const id = this.nextStreamId++;
|
|
8932
|
-
const state = {
|
|
8933
|
-
fileId: file.id,
|
|
8934
|
-
cancelled: false
|
|
8935
|
-
};
|
|
8936
|
-
this.outgoing.set(id, state);
|
|
8937
|
-
try {
|
|
8938
|
-
this.channel.send(encodeControl({
|
|
8939
|
-
t: "file",
|
|
8940
|
-
id,
|
|
8941
|
-
f: file.id,
|
|
8942
|
-
ts: file.timestamp,
|
|
8943
|
-
n: sanitizeFileName(file.name),
|
|
8944
|
-
s: file.size,
|
|
8945
|
-
m: sanitizeMime(file.mime)
|
|
8946
|
-
}));
|
|
8947
|
-
let sent = 0;
|
|
8948
|
-
const counted = async function* () {
|
|
8949
|
-
for await (const part of source) {
|
|
8950
|
-
sent += part.length;
|
|
8951
|
-
yield part;
|
|
8952
|
-
this.events.onProgress?.(file.id, sent, "out");
|
|
8953
|
-
}
|
|
8954
|
-
}.call(this);
|
|
8955
|
-
await sendBody(this.channel, CHUNK_KIND.fileBody, id, counted, () => state.cancelled);
|
|
8956
|
-
if (state.cancelled) throw new Error("The peer refused the file");
|
|
8957
|
-
if (sent !== file.size) throw new Error("File changed while it was being sent");
|
|
8958
|
-
this.events.onComplete?.(file.id, "out");
|
|
8959
|
-
} catch (error) {
|
|
8960
|
-
if (!state.cancelled) try {
|
|
8961
|
-
this.channel.send(encodeControl({
|
|
8962
|
-
t: "rst",
|
|
8963
|
-
id,
|
|
8964
|
-
d: "f",
|
|
8965
|
-
e: "aborted"
|
|
8966
|
-
}));
|
|
8967
|
-
} catch {}
|
|
8968
|
-
this.events.onFailed?.(file.id, error instanceof Error ? error.message : String(error), "out");
|
|
8969
|
-
throw error;
|
|
8970
|
-
} finally {
|
|
8971
|
-
this.outgoing.delete(id);
|
|
8972
|
-
}
|
|
8973
|
-
}
|
|
8974
|
-
handleFile(frame) {
|
|
8975
|
-
if (this.incoming.has(frame.id)) return;
|
|
8976
|
-
const refuse = (reason) => {
|
|
8977
|
-
try {
|
|
8978
|
-
this.channel.send(encodeControl({
|
|
8979
|
-
t: "rst",
|
|
8980
|
-
id: frame.id,
|
|
8981
|
-
d: "f",
|
|
8982
|
-
e: reason
|
|
8983
|
-
}));
|
|
8984
|
-
} catch {}
|
|
8985
|
-
};
|
|
8986
|
-
if (!FILE_ID.test(frame.f)) return refuse("invalid file id");
|
|
8987
|
-
if (frame.s > LIMITS.maxFileBytes) return refuse("file too large");
|
|
8988
|
-
if (this.incoming.size >= LIMITS.maxIncomingFilesPerPeer) return refuse("too many transfers");
|
|
8989
|
-
const file = {
|
|
8990
|
-
id: frame.f,
|
|
8991
|
-
name: sanitizeFileName(frame.n),
|
|
8992
|
-
size: frame.s,
|
|
8993
|
-
mime: sanitizeMime(frame.m),
|
|
8994
|
-
timestamp: frame.ts
|
|
8995
|
-
};
|
|
8996
|
-
const sink = this.events.onIncoming(file);
|
|
8997
|
-
if (!sink || typeof sink === "string") return refuse(sink || "refused");
|
|
8998
|
-
const entry = {
|
|
8999
|
-
file,
|
|
9000
|
-
sink,
|
|
9001
|
-
received: 0,
|
|
9002
|
-
queue: Promise.resolve()
|
|
9003
|
-
};
|
|
9004
|
-
this.incoming.set(frame.id, entry);
|
|
9005
|
-
this.armIdleTimer(frame.id, entry);
|
|
9006
|
-
}
|
|
9007
|
-
handleChunk(chunk) {
|
|
9008
|
-
const entry = this.incoming.get(chunk.id);
|
|
9009
|
-
if (!entry) return;
|
|
9010
|
-
entry.received += chunk.payload.length;
|
|
9011
|
-
if (entry.received > entry.file.size) return this.failIncoming(chunk.id, "more data than announced", true);
|
|
9012
|
-
if (chunk.payload.length > 0) {
|
|
9013
|
-
const payload = chunk.payload.slice();
|
|
9014
|
-
entry.queue = entry.queue.then(() => entry.sink.write(payload));
|
|
9015
|
-
this.events.onProgress?.(entry.file.id, entry.received, "in");
|
|
9016
|
-
}
|
|
9017
|
-
if (!chunk.end) return this.armIdleTimer(chunk.id, entry);
|
|
9018
|
-
if (entry.received !== entry.file.size) return this.failIncoming(chunk.id, "incomplete file", false);
|
|
9019
|
-
clearTimeout(entry.idleTimer);
|
|
9020
|
-
this.incoming.delete(chunk.id);
|
|
9021
|
-
entry.queue.then(() => entry.sink.close()).then(() => this.events.onComplete?.(entry.file.id, "in"), () => {
|
|
9022
|
-
entry.sink.abort();
|
|
9023
|
-
this.events.onFailed?.(entry.file.id, "could not store the file", "in");
|
|
9024
|
-
});
|
|
9025
|
-
}
|
|
9026
|
-
handleReset(frame) {
|
|
9027
|
-
if (this.incoming.has(frame.id)) this.failIncoming(frame.id, frame.e || "cancelled by the sender", false);
|
|
9028
|
-
const sending = this.outgoing.get(frame.id);
|
|
9029
|
-
if (sending) sending.cancelled = true;
|
|
9030
|
-
}
|
|
9031
|
-
/** The link went away: nothing in flight can finish. */
|
|
9032
|
-
closeAll() {
|
|
9033
|
-
for (const id of [...this.incoming.keys()]) this.failIncoming(id, "connection lost", false);
|
|
9034
|
-
for (const sending of this.outgoing.values()) sending.cancelled = true;
|
|
9035
|
-
}
|
|
9036
|
-
armIdleTimer(id, entry) {
|
|
9037
|
-
clearTimeout(entry.idleTimer);
|
|
9038
|
-
entry.idleTimer = setTimeout(() => this.failIncoming(id, "transfer stalled", true), LIMITS.bodyIdleTimeoutMs);
|
|
9039
|
-
}
|
|
9040
|
-
failIncoming(id, reason, notifyPeer) {
|
|
9041
|
-
const entry = this.incoming.get(id);
|
|
9042
|
-
if (!entry) return;
|
|
9043
|
-
clearTimeout(entry.idleTimer);
|
|
9044
|
-
this.incoming.delete(id);
|
|
9045
|
-
entry.sink.abort();
|
|
9046
|
-
if (notifyPeer) try {
|
|
9047
|
-
this.channel.send(encodeControl({
|
|
9048
|
-
t: "rst",
|
|
9049
|
-
id,
|
|
9050
|
-
d: "f",
|
|
9051
|
-
e: reason
|
|
9052
|
-
}));
|
|
9053
|
-
} catch {}
|
|
9054
|
-
this.events.onFailed?.(entry.file.id, reason, "in");
|
|
9055
|
-
}
|
|
9056
|
-
};
|
|
9057
|
-
//#endregion
|
|
9058
|
-
//#region ../core/src/image.ts
|
|
9059
|
-
var IMAGE_LIMITS = {
|
|
9060
|
-
/** Pixels on either side: the most a JPEG can say, well past what anyone sends as a picture. */
|
|
9061
|
-
maxEdge: 65535,
|
|
9062
|
-
/** How much of a file is read for its size: past a JPEG's EXIF, thumbnails and colour profile. */
|
|
9063
|
-
headBytes: 262144
|
|
9064
|
-
};
|
|
9065
|
-
/** Whether a file is a picture, by its type. */
|
|
9066
|
-
function isImageMime(mime) {
|
|
9067
|
-
return mime.split(";")[0].trim().toLowerCase().startsWith("image/");
|
|
9068
|
-
}
|
|
9069
|
-
var edge = (n) => typeof n === "number" && Number.isSafeInteger(n) && n > 0 && n <= IMAGE_LIMITS.maxEdge;
|
|
9070
|
-
/** A peer's `image` description, or undefined when it is not one. It never refuses the file. */
|
|
9071
|
-
function parseImageMeta(value, mime) {
|
|
9072
|
-
if (mime !== void 0 && !isImageMime(mime)) return void 0;
|
|
9073
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
9074
|
-
const { width, height } = value;
|
|
9075
|
-
return edge(width) && edge(height) ? {
|
|
9076
|
-
width,
|
|
9077
|
-
height
|
|
9078
|
-
} : void 0;
|
|
9079
|
-
}
|
|
9080
|
-
var u16be = (b, at) => b[at] << 8 | b[at + 1];
|
|
9081
|
-
var u16le = (b, at) => b[at] | b[at + 1] << 8;
|
|
9082
|
-
var u24le = (b, at) => b[at] | b[at + 1] << 8 | b[at + 2] << 16;
|
|
9083
|
-
var u32be = (b, at) => (b[at] << 24 >>> 0) + (b[at + 1] << 16) + (b[at + 2] << 8) + b[at + 3];
|
|
9084
|
-
var ascii = (b, at, text) => [...text].every((c, i) => b[at + i] === c.charCodeAt(0));
|
|
9085
|
-
var sized = (width, height) => edge(width) && edge(height) ? {
|
|
9086
|
-
width,
|
|
9087
|
-
height
|
|
9088
|
-
} : void 0;
|
|
9089
|
-
/** SOFn markers: every frame type but DHT (C4), JPG (C8) and DAC (CC). */
|
|
9090
|
-
var JPEG_FRAME = /* @__PURE__ */ new Set([
|
|
9091
|
-
192,
|
|
9092
|
-
193,
|
|
9093
|
-
194,
|
|
9094
|
-
195,
|
|
9095
|
-
197,
|
|
9096
|
-
198,
|
|
9097
|
-
199,
|
|
9098
|
-
201,
|
|
9099
|
-
202,
|
|
9100
|
-
203,
|
|
9101
|
-
205,
|
|
9102
|
-
206,
|
|
9103
|
-
207
|
|
9104
|
-
]);
|
|
9105
|
-
/** The EXIF orientation in an APP1 segment's TIFF block (1 when it says none). */
|
|
9106
|
-
function exifOrientation(b, tiff, end) {
|
|
9107
|
-
if (tiff + 8 > end) return 1;
|
|
9108
|
-
const little = ascii(b, tiff, "II");
|
|
9109
|
-
if (!little && !ascii(b, tiff, "MM")) return 1;
|
|
9110
|
-
const u16 = (at) => little ? u16le(b, at) : u16be(b, at);
|
|
9111
|
-
const u32 = (at) => little ? u16le(b, at) + u16le(b, at + 2) * 65536 : u32be(b, at);
|
|
9112
|
-
if (u16(tiff + 2) !== 42) return 1;
|
|
9113
|
-
const ifd = tiff + u32(tiff + 4);
|
|
9114
|
-
if (ifd + 2 > end) return 1;
|
|
9115
|
-
const count = u16(ifd);
|
|
9116
|
-
for (let i = 0; i < count; i++) {
|
|
9117
|
-
const entry = ifd + 2 + i * 12;
|
|
9118
|
-
if (entry + 12 > end) return 1;
|
|
9119
|
-
if (u16(entry) === 274) {
|
|
9120
|
-
const value = u16(entry + 8);
|
|
9121
|
-
return value >= 1 && value <= 8 ? value : 1;
|
|
9122
|
-
}
|
|
9123
|
-
}
|
|
9124
|
-
return 1;
|
|
9125
|
-
}
|
|
9126
|
-
function jpegSize(b) {
|
|
9127
|
-
let at = 2, orientation = 1;
|
|
9128
|
-
while (at + 4 <= b.length) {
|
|
9129
|
-
if (b[at] !== 255) return void 0;
|
|
9130
|
-
const marker = b[at + 1];
|
|
9131
|
-
if (marker === 255) {
|
|
9132
|
-
at++;
|
|
9133
|
-
continue;
|
|
9134
|
-
}
|
|
9135
|
-
if (marker === 1 || marker >= 208 && marker <= 215) {
|
|
9136
|
-
at += 2;
|
|
9137
|
-
continue;
|
|
9138
|
-
}
|
|
9139
|
-
if (marker === 217 || marker === 218) return void 0;
|
|
9140
|
-
const length = u16be(b, at + 2);
|
|
9141
|
-
if (length < 2) return void 0;
|
|
9142
|
-
const end = Math.min(b.length, at + 2 + length);
|
|
9143
|
-
if (marker === 225 && ascii(b, at + 4, "Exif\0\0")) orientation = exifOrientation(b, at + 10, end);
|
|
9144
|
-
if (JPEG_FRAME.has(marker)) {
|
|
9145
|
-
if (at + 9 > b.length) return void 0;
|
|
9146
|
-
const height = u16be(b, at + 5), width = u16be(b, at + 7);
|
|
9147
|
-
return orientation >= 5 ? sized(height, width) : sized(width, height);
|
|
9148
|
-
}
|
|
9149
|
-
at += 2 + length;
|
|
7359
|
+
this.windowMs = windowMs;
|
|
7360
|
+
this.now = now;
|
|
9150
7361
|
}
|
|
9151
|
-
|
|
9152
|
-
|
|
9153
|
-
|
|
9154
|
-
|
|
9155
|
-
if (
|
|
9156
|
-
|
|
7362
|
+
/** Counts one now; false (and not counted) when the window is full. */
|
|
7363
|
+
take() {
|
|
7364
|
+
const now = this.now();
|
|
7365
|
+
this.recent = this.recent.filter((at) => now - at < this.windowMs);
|
|
7366
|
+
if (this.recent.length >= this.limit) return false;
|
|
7367
|
+
this.recent.push(now);
|
|
7368
|
+
return true;
|
|
9157
7369
|
}
|
|
9158
|
-
|
|
9159
|
-
|
|
9160
|
-
const
|
|
9161
|
-
|
|
7370
|
+
/** Milliseconds until the window has room again; 0 when it has room now. */
|
|
7371
|
+
wait() {
|
|
7372
|
+
const now = this.now();
|
|
7373
|
+
this.recent = this.recent.filter((at) => now - at < this.windowMs);
|
|
7374
|
+
return this.recent.length < this.limit ? 0 : this.recent[0] + this.windowMs - now;
|
|
9162
7375
|
}
|
|
9163
|
-
|
|
9164
|
-
|
|
9165
|
-
/**
|
|
9166
|
-
* A picture's size as it is shown, read from its first bytes (PNG, JPEG, GIF, WebP), or undefined when they do not
|
|
9167
|
-
* say. Only the header is read: a JPEG whose frame header sits past `IMAGE_LIMITS.headBytes` goes without.
|
|
9168
|
-
*/
|
|
9169
|
-
function imageSize(head) {
|
|
9170
|
-
const b = head;
|
|
9171
|
-
try {
|
|
9172
|
-
if (b.length >= 24 && b[0] === 137 && ascii(b, 1, "PNG\r\n\n") && ascii(b, 12, "IHDR")) return sized(u32be(b, 16), u32be(b, 20));
|
|
9173
|
-
if (b.length >= 10 && (ascii(b, 0, "GIF87a") || ascii(b, 0, "GIF89a"))) return sized(u16le(b, 6), u16le(b, 8));
|
|
9174
|
-
if (b.length >= 4 && b[0] === 255 && b[1] === 216 && b[2] === 255) return jpegSize(b);
|
|
9175
|
-
if (b.length >= 16 && ascii(b, 0, "RIFF") && ascii(b, 8, "WEBP")) return webpSize(b);
|
|
9176
|
-
} catch {}
|
|
9177
|
-
}
|
|
9178
|
-
/**
|
|
9179
|
-
* The description a picture is sent with, read from the start of the file, or undefined when it is not a picture
|
|
9180
|
-
* or its first bytes do not say (it then goes without one).
|
|
9181
|
-
*/
|
|
9182
|
-
async function readImageMeta(source, mime) {
|
|
9183
|
-
if (!isImageMime(mime)) return void 0;
|
|
9184
|
-
try {
|
|
9185
|
-
return imageSize(new Uint8Array(await source.slice(0, IMAGE_LIMITS.headBytes).arrayBuffer()));
|
|
9186
|
-
} catch {
|
|
9187
|
-
return;
|
|
7376
|
+
reset() {
|
|
7377
|
+
this.recent = [];
|
|
9188
7378
|
}
|
|
9189
|
-
}
|
|
7379
|
+
};
|
|
9190
7380
|
//#endregion
|
|
9191
7381
|
//#region ../core/src/dhtFiles.ts
|
|
9192
7382
|
/**
|
|
@@ -9355,6 +7545,7 @@ var DhtDelivery = class DhtDelivery {
|
|
|
9355
7545
|
fastUntil = 0;
|
|
9356
7546
|
live = false;
|
|
9357
7547
|
active = false;
|
|
7548
|
+
startedAt = Date.now();
|
|
9358
7549
|
/** The next read was asked for (a refresh, a fresh packet of the contact): it is not a background one. */
|
|
9359
7550
|
urgent = false;
|
|
9360
7551
|
/** Reads left that are signaling: the contact is leaving DHT only, and the live link waits on them (`expect`). */
|
|
@@ -10298,11 +8489,20 @@ var DhtDelivery = class DhtDelivery {
|
|
|
10298
8489
|
get pollMs() {
|
|
10299
8490
|
if (this.options.pollMs) return this.options.pollMs;
|
|
10300
8491
|
const now = Date.now();
|
|
8492
|
+
const unused = this.unusedInviteAge(now);
|
|
8493
|
+
if (unused !== void 0 && !this.active && now >= this.fastUntil) return unusedInviteWait(unused, this.mode === "dht" ? DHT_POLL_MS : STREAM_POLL_MS);
|
|
10301
8494
|
if (this.mode === "dht" || this.state.peerMode === "dht" && now < this.leavingUntil || now < this.fastUntil) return DHT_POLL_MS;
|
|
10302
8495
|
if (this.live) return LIVE_POLL_MS;
|
|
10303
8496
|
if (this.state.pending && this.state.pending.expires > now) return DHT_POLL_MS;
|
|
10304
8497
|
return this.active ? ACTIVE_DHT_POLL_MS : STREAM_POLL_MS;
|
|
10305
8498
|
}
|
|
8499
|
+
/** How long ago this side's invite was made, while nobody used it and it is no longer awaited (`AWAITING_PEER_MS`). */
|
|
8500
|
+
unusedInviteAge(now) {
|
|
8501
|
+
const madeAt = this.options.inviteMadeAt;
|
|
8502
|
+
if (madeAt === void 0 || this.options.credentials.peerKey || this.state.peerSequence) return void 0;
|
|
8503
|
+
const age = now - Math.min(madeAt, this.startedAt);
|
|
8504
|
+
return age >= 6e5 ? age : void 0;
|
|
8505
|
+
}
|
|
10306
8506
|
schedule() {
|
|
10307
8507
|
if (!this.running || this.ticking) return;
|
|
10308
8508
|
if (this.timer) clearTimeout(this.timer);
|
|
@@ -13836,6 +12036,11 @@ var SERVICES_CAPABILITY = "services/1";
|
|
|
13836
12036
|
* confirmed byte, checked by digest (`pf-offer` … frames, `chatFiles.ts`). Without it, files/2 and 100 MiB.
|
|
13837
12037
|
*/
|
|
13838
12038
|
var FILES_CAPABILITY = "files/3";
|
|
12039
|
+
/**
|
|
12040
|
+
* A file's bytes as binary messages (WISP 501 § files/4): beside `files/3`, on a session whose channel carries binary
|
|
12041
|
+
* messages, a chunk travels as it is (`encodeFileData`, `chatFiles.ts`) and not as base64url text in a `pf-data` frame.
|
|
12042
|
+
*/
|
|
12043
|
+
var FILES_BINARY_CAPABILITY = "files/4";
|
|
13839
12044
|
/** The typing indicator: `paired-typing` frames on this session, 1:1 chats only (WISP 401 § Typing, `pairedTyping.ts`). */
|
|
13840
12045
|
var TYPING_CAPABILITY = "typing/1";
|
|
13841
12046
|
/** Reactions to messages: `paired-reaction` frames on this session, 1:1 chats only (WISP 401 § Reactions, `reactions.ts`). */
|
|
@@ -13875,6 +12080,7 @@ var KNOWN_SESSION_CAPABILITIES = [
|
|
|
13875
12080
|
CALLS_CAPABILITY,
|
|
13876
12081
|
SERVICES_CAPABILITY,
|
|
13877
12082
|
FILES_CAPABILITY,
|
|
12083
|
+
FILES_BINARY_CAPABILITY,
|
|
13878
12084
|
TYPING_CAPABILITY,
|
|
13879
12085
|
REACTIONS_CAPABILITY,
|
|
13880
12086
|
EDIT_CAPABILITY,
|
|
@@ -13971,6 +12177,8 @@ var SessionCapabilities = class {
|
|
|
13971
12177
|
* be matched across a person's chats; it travels only inside the encrypted session.
|
|
13972
12178
|
*/
|
|
13973
12179
|
var APP_FRAME = "paired-app";
|
|
12180
|
+
/** The largest `d`, as compact UTF-8 JSON. The session's 60 KiB cap on any frame still holds above it. */
|
|
12181
|
+
var APP_DATA_MAX_BYTES = 32768;
|
|
13974
12182
|
var DOMAIN = "ghostly-apps/1";
|
|
13975
12183
|
var APP_ID = /^[A-Za-z0-9_-]{22}$/;
|
|
13976
12184
|
/** Semantic versioning 2.0.0, as a manifest's `version` is. */
|
|
@@ -14283,7 +12491,8 @@ function vapidAuthorization(endpoint, keys, subject, now = Date.now()) {
|
|
|
14283
12491
|
exp: Math.floor(now / 1e3) + TOKEN_SECONDS,
|
|
14284
12492
|
sub: subject
|
|
14285
12493
|
})}`;
|
|
14286
|
-
|
|
12494
|
+
const signature = p256.sign(utf8Encode(unsigned), fromBase64Url(keys.privateKey));
|
|
12495
|
+
return `vapid t=${unsigned}.${toBase64Url(signature)}, k=${keys.publicKey}`;
|
|
14287
12496
|
}
|
|
14288
12497
|
/**
|
|
14289
12498
|
* `plaintext` encrypted for a subscription (RFC 8291 with RFC 8188 `aes128gcm`, one record). `sender` fixes
|
|
@@ -14601,7 +12810,8 @@ var LINK_OR_MARKUP = /[a-z][a-z0-9+.-]*:\/\/|\bwww\.|\b(?:mailto|javascript|data
|
|
|
14601
12810
|
*/
|
|
14602
12811
|
function sanitizeTypingStatus(value) {
|
|
14603
12812
|
if (typeof value !== "string") return void 0;
|
|
14604
|
-
const
|
|
12813
|
+
const oneLine = value.slice(0, 160).replace(/\s+/g, " ");
|
|
12814
|
+
const clean = sanitizeDisplayText(oneLine, 40);
|
|
14605
12815
|
if (!clean || LINK_OR_MARKUP.test(clean)) return void 0;
|
|
14606
12816
|
return clean;
|
|
14607
12817
|
}
|
|
@@ -14810,6 +13020,11 @@ var FILE_LIMITS = {
|
|
|
14810
13020
|
chunkBytes: 16384,
|
|
14811
13021
|
/** Bytes a sender has sent and not seen confirmed: throughput is this per round trip. */
|
|
14812
13022
|
windowBytes: 1048576,
|
|
13023
|
+
/**
|
|
13024
|
+
* Bytes all of a chat's files have sent and not seen confirmed, together. Three files' windows at once were 3 MiB, and
|
|
13025
|
+
* as base64 text past what a session's receive queue holds (`SESSION_RECEIVE_PENDING`, 3.75 MiB).
|
|
13026
|
+
*/
|
|
13027
|
+
chatWindowBytes: 2097152,
|
|
14813
13028
|
/** A receiver asks its person before taking a file larger than this. */
|
|
14814
13029
|
askAboveBytes: 26214400,
|
|
14815
13030
|
/** Files taken without asking, per contact, while stored. Past it, every file is asked about. */
|
|
@@ -14845,7 +13060,7 @@ var FILE_LIMITS = {
|
|
|
14845
13060
|
resendBytes: 268435456
|
|
14846
13061
|
};
|
|
14847
13062
|
var ID = /^[A-Za-z0-9_-]{8,64}$/;
|
|
14848
|
-
var DIGEST
|
|
13063
|
+
var DIGEST = /^[A-Za-z0-9_-]{43}$/;
|
|
14849
13064
|
var DATA = /^[A-Za-z0-9_-]+$/;
|
|
14850
13065
|
var FILE_FRAMES = /* @__PURE__ */ new Set([
|
|
14851
13066
|
"pf-offer",
|
|
@@ -14878,6 +13093,37 @@ function transferStalled(record, movedAt, restored, now) {
|
|
|
14878
13093
|
}
|
|
14879
13094
|
var key = (direction, id) => `${direction}:${id}`;
|
|
14880
13095
|
var isInt$1 = (value, max = Number.MAX_SAFE_INTEGER) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && value <= max;
|
|
13096
|
+
var FILE_DATA_OFFSET_BYTES = 8;
|
|
13097
|
+
/** The binary message for the bytes of file `id` at `offset`. Throws on what no receiver would take. */
|
|
13098
|
+
function encodeFileData(id, offset, data) {
|
|
13099
|
+
if (!ID.test(id) || !isInt$1(offset) || !data.length || data.length > FILE_LIMITS.chunkBytes) throw new Error("Invalid file data");
|
|
13100
|
+
const head = 2 + id.length;
|
|
13101
|
+
const out = new Uint8Array(head + FILE_DATA_OFFSET_BYTES + data.length);
|
|
13102
|
+
out[0] = 4;
|
|
13103
|
+
out[1] = id.length;
|
|
13104
|
+
for (let i = 0; i < id.length; i++) out[2 + i] = id.charCodeAt(i);
|
|
13105
|
+
const view = new DataView(out.buffer);
|
|
13106
|
+
view.setUint32(head, Math.floor(offset / 2 ** 32));
|
|
13107
|
+
view.setUint32(head + 4, offset % 2 ** 32);
|
|
13108
|
+
out.set(data, head + FILE_DATA_OFFSET_BYTES);
|
|
13109
|
+
return out;
|
|
13110
|
+
}
|
|
13111
|
+
/** 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`. */
|
|
13112
|
+
function decodeFileData(message) {
|
|
13113
|
+
if (message.length < 2 || message[0] !== 4) return null;
|
|
13114
|
+
const head = 2 + message[1], body = head + FILE_DATA_OFFSET_BYTES;
|
|
13115
|
+
if (message.length <= body || message.length - body > FILE_LIMITS.chunkBytes) return null;
|
|
13116
|
+
const id = String.fromCharCode(...message.subarray(2, head));
|
|
13117
|
+
if (!ID.test(id)) return null;
|
|
13118
|
+
const view = new DataView(message.buffer, message.byteOffset, message.byteLength);
|
|
13119
|
+
const high = view.getUint32(head);
|
|
13120
|
+
if (high > 2097151) return null;
|
|
13121
|
+
return {
|
|
13122
|
+
id,
|
|
13123
|
+
offset: high * 2 ** 32 + view.getUint32(head + 4),
|
|
13124
|
+
data: message.subarray(body)
|
|
13125
|
+
};
|
|
13126
|
+
}
|
|
14881
13127
|
/** What a refusal means to the sender's person. */
|
|
14882
13128
|
function refusalText(why, room) {
|
|
14883
13129
|
switch (why) {
|
|
@@ -14914,6 +13160,10 @@ function refusalText(why, room) {
|
|
|
14914
13160
|
var ChatFiles = class {
|
|
14915
13161
|
host;
|
|
14916
13162
|
entries = /* @__PURE__ */ new Map();
|
|
13163
|
+
/** Files sending that may have bytes not confirmed yet: what `inFlight` adds up (the rest are left out as it goes). */
|
|
13164
|
+
moving = /* @__PURE__ */ new Set();
|
|
13165
|
+
/** Files that stopped sending for the chat's window alone: they go on once another's bytes are confirmed. */
|
|
13166
|
+
held = /* @__PURE__ */ new Set();
|
|
14917
13167
|
attached = false;
|
|
14918
13168
|
peerRoomValue = null;
|
|
14919
13169
|
constructor(host) {
|
|
@@ -14973,6 +13223,15 @@ var ChatFiles = class {
|
|
|
14973
13223
|
send(frame) {
|
|
14974
13224
|
return this.attached && this.host.send(frame);
|
|
14975
13225
|
}
|
|
13226
|
+
sendData(id, offset, data) {
|
|
13227
|
+
if (!this.host.sendData) return this.send({
|
|
13228
|
+
t: "pf-data",
|
|
13229
|
+
id,
|
|
13230
|
+
offset,
|
|
13231
|
+
data: toBase64Url(data)
|
|
13232
|
+
});
|
|
13233
|
+
return this.attached && this.host.sendData(id, offset, data);
|
|
13234
|
+
}
|
|
14976
13235
|
/** A session that agreed files/3 is open: say what fits here, then offer again what is not finished. */
|
|
14977
13236
|
attach() {
|
|
14978
13237
|
this.attached = true;
|
|
@@ -14991,6 +13250,8 @@ var ChatFiles = class {
|
|
|
14991
13250
|
if (!this.attached) return;
|
|
14992
13251
|
this.attached = false;
|
|
14993
13252
|
this.peerRoomValue = null;
|
|
13253
|
+
this.moving.clear();
|
|
13254
|
+
this.held.clear();
|
|
14994
13255
|
for (const entry of this.entries.values()) {
|
|
14995
13256
|
if (entry.out) {
|
|
14996
13257
|
entry.out.generation++;
|
|
@@ -15114,10 +13375,34 @@ var ChatFiles = class {
|
|
|
15114
13375
|
const at = offset - out.block.start;
|
|
15115
13376
|
return out.block.bytes.subarray(at, at + length);
|
|
15116
13377
|
}
|
|
15117
|
-
|
|
13378
|
+
/** Whether this file's own window lets it send more. */
|
|
13379
|
+
fileMayPump(entry) {
|
|
15118
13380
|
const { record } = entry, out = entry.out;
|
|
15119
13381
|
return this.attached && record.state === "active" && out.next < record.file.size && out.next - record.confirmed < FILE_LIMITS.windowBytes;
|
|
15120
13382
|
}
|
|
13383
|
+
shouldPump(entry) {
|
|
13384
|
+
return this.fileMayPump(entry) && this.inFlight() < FILE_LIMITS.chatWindowBytes;
|
|
13385
|
+
}
|
|
13386
|
+
/** Bytes this chat's files sent and have not seen confirmed. */
|
|
13387
|
+
inFlight() {
|
|
13388
|
+
let bytes = 0;
|
|
13389
|
+
for (const entry of this.moving) {
|
|
13390
|
+
const out = entry.out, { record } = entry;
|
|
13391
|
+
if (!out || record.state !== "active" || out.next <= record.confirmed) {
|
|
13392
|
+
this.moving.delete(entry);
|
|
13393
|
+
continue;
|
|
13394
|
+
}
|
|
13395
|
+
bytes += out.next - record.confirmed;
|
|
13396
|
+
}
|
|
13397
|
+
return bytes;
|
|
13398
|
+
}
|
|
13399
|
+
/** Bytes were confirmed, or a file stopped sending: the files the chat's window held go on, before the one that freed it. */
|
|
13400
|
+
wakeHeld() {
|
|
13401
|
+
for (const entry of [...this.held]) {
|
|
13402
|
+
this.held.delete(entry);
|
|
13403
|
+
this.pump(entry);
|
|
13404
|
+
}
|
|
13405
|
+
}
|
|
15121
13406
|
/**
|
|
15122
13407
|
* Whether the next `length` bytes go. Within the file's size they always do; past it the receiver is having parts
|
|
15123
13408
|
* sent again, which nothing it says bounds, so this side does.
|
|
@@ -15148,17 +13433,13 @@ var ChatFiles = class {
|
|
|
15148
13433
|
this.end(entry, "failed", "Your contact asked for the same part of this file too many times");
|
|
15149
13434
|
break;
|
|
15150
13435
|
}
|
|
15151
|
-
if (!this.
|
|
15152
|
-
t: "pf-data",
|
|
15153
|
-
id: entry.record.id,
|
|
15154
|
-
offset,
|
|
15155
|
-
data: toBase64Url(chunk)
|
|
15156
|
-
})) {
|
|
13436
|
+
if (!this.sendData(entry.record.id, offset, chunk)) {
|
|
15157
13437
|
refused = true;
|
|
15158
13438
|
break;
|
|
15159
13439
|
}
|
|
15160
13440
|
out.next = offset + chunk.length;
|
|
15161
13441
|
out.sent += chunk.length;
|
|
13442
|
+
this.moving.add(entry);
|
|
15162
13443
|
}
|
|
15163
13444
|
} catch (error) {
|
|
15164
13445
|
if (generation === out.generation) {
|
|
@@ -15173,7 +13454,7 @@ var ChatFiles = class {
|
|
|
15173
13454
|
if (generation === out.generation && this.shouldPump(entry)) {
|
|
15174
13455
|
if (refused) out.wake = setTimeout(() => void this.pump(entry), FILE_LIMITS.sendRetryMs);
|
|
15175
13456
|
else this.pump(entry);
|
|
15176
|
-
}
|
|
13457
|
+
} else if (generation === out.generation && this.fileMayPump(entry)) this.held.add(entry);
|
|
15177
13458
|
}
|
|
15178
13459
|
}
|
|
15179
13460
|
async sendSum(entry) {
|
|
@@ -15205,7 +13486,7 @@ var ChatFiles = class {
|
|
|
15205
13486
|
return [...this.entries.values()].filter((e) => e.record.direction === "in" && e.record.state === "asking").length;
|
|
15206
13487
|
}
|
|
15207
13488
|
activeIncoming(except) {
|
|
15208
|
-
return [...this.entries.values()].filter((e) => e !== except && e.record.direction === "in" && (e.record.state === "active" || e.record.state === "verifying")).length;
|
|
13489
|
+
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
13490
|
}
|
|
15210
13491
|
/**
|
|
15211
13492
|
* Removes what this side stored of an incoming file that ends here. Through its target, opened now if need be: after
|
|
@@ -15232,9 +13513,15 @@ var ChatFiles = class {
|
|
|
15232
13513
|
}
|
|
15233
13514
|
return incoming.target;
|
|
15234
13515
|
}
|
|
15235
|
-
/**
|
|
13516
|
+
/**
|
|
13517
|
+
* An accepted file goes when a session is open and fewer than three others arrive at once. One whose target is still
|
|
13518
|
+
* being opened counts among the three: counted only once active, every file offered or queued meanwhile went too, and
|
|
13519
|
+
* the windows of ten files and more at once overflowed the session's receive queue.
|
|
13520
|
+
*/
|
|
15236
13521
|
startIncoming(entry) {
|
|
15237
13522
|
if (!this.attached || transferEnded(entry.record) || entry.record.state === "asking" || entry.record.state === "paused" && entry.record.pausedBy === "me") return;
|
|
13523
|
+
const incoming = entry.in;
|
|
13524
|
+
if (incoming.starting) return;
|
|
15238
13525
|
if (this.activeIncoming(entry) >= FILE_LIMITS.maxActiveIncoming) {
|
|
15239
13526
|
if (entry.record.state !== "queued") {
|
|
15240
13527
|
entry.record.state = "queued";
|
|
@@ -15247,9 +13534,13 @@ var ChatFiles = class {
|
|
|
15247
13534
|
});
|
|
15248
13535
|
return;
|
|
15249
13536
|
}
|
|
13537
|
+
incoming.starting = true;
|
|
15250
13538
|
this.target(entry).then(() => {
|
|
15251
|
-
|
|
15252
|
-
if (entry.record.state === "paused" && entry.record.pausedBy === "me")
|
|
13539
|
+
incoming.starting = false;
|
|
13540
|
+
if (transferEnded(entry.record) || !this.attached || entry.record.state === "paused" && entry.record.pausedBy === "me") {
|
|
13541
|
+
this.startNext();
|
|
13542
|
+
return;
|
|
13543
|
+
}
|
|
15253
13544
|
entry.record.state = "active";
|
|
15254
13545
|
entry.record.pausedBy = void 0;
|
|
15255
13546
|
entry.in.gapAt = void 0;
|
|
@@ -15262,12 +13553,14 @@ var ChatFiles = class {
|
|
|
15262
13553
|
entry.in.heardAt = this.now();
|
|
15263
13554
|
this.armNudge(entry);
|
|
15264
13555
|
}, (error) => {
|
|
13556
|
+
incoming.starting = false;
|
|
15265
13557
|
this.end(entry, "failed", `Could not store the file: ${error instanceof Error ? error.message : String(error)}`);
|
|
15266
13558
|
this.send({
|
|
15267
13559
|
t: "pf-refuse",
|
|
15268
13560
|
id: entry.record.id,
|
|
15269
13561
|
why: "no-room"
|
|
15270
13562
|
});
|
|
13563
|
+
this.startNext();
|
|
15271
13564
|
});
|
|
15272
13565
|
}
|
|
15273
13566
|
/** The next file waiting its turn, once one ends. */
|
|
@@ -15506,7 +13799,9 @@ var ChatFiles = class {
|
|
|
15506
13799
|
if (record.state !== "active" || !incoming.target) return;
|
|
15507
13800
|
incoming.heardAt = this.now();
|
|
15508
13801
|
const { offset, data } = frame;
|
|
15509
|
-
|
|
13802
|
+
const binary = data instanceof Uint8Array;
|
|
13803
|
+
if (!isInt$1(offset) || !(binary || typeof data === "string") || !data.length) return;
|
|
13804
|
+
if (!binary && (data.length > Math.ceil(FILE_LIMITS.chunkBytes * 4 / 3) || !DATA.test(data))) return;
|
|
15510
13805
|
if (offset < incoming.written) {
|
|
15511
13806
|
if (incoming.stored === incoming.written) this.send({
|
|
15512
13807
|
t: "pf-got",
|
|
@@ -15526,7 +13821,7 @@ var ChatFiles = class {
|
|
|
15526
13821
|
}
|
|
15527
13822
|
return;
|
|
15528
13823
|
}
|
|
15529
|
-
const bytes = fromBase64Url(data);
|
|
13824
|
+
const bytes = binary ? data : fromBase64Url(data);
|
|
15530
13825
|
if (!bytes.length || bytes.length > FILE_LIMITS.chunkBytes || offset + bytes.length > record.file.size) return;
|
|
15531
13826
|
incoming.written += bytes.length;
|
|
15532
13827
|
const target = incoming.target;
|
|
@@ -15618,7 +13913,7 @@ var ChatFiles = class {
|
|
|
15618
13913
|
return;
|
|
15619
13914
|
}
|
|
15620
13915
|
if (record.state !== "active" || incoming.verifying || !incoming.target) return;
|
|
15621
|
-
if (frame.size !== record.file.size || typeof frame.digest !== "string" || !DIGEST
|
|
13916
|
+
if (frame.size !== record.file.size || typeof frame.digest !== "string" || !DIGEST.test(frame.digest)) return;
|
|
15622
13917
|
const digest = frame.digest;
|
|
15623
13918
|
await incoming.writing;
|
|
15624
13919
|
if (transferEnded(record) || record.state !== "active") return;
|
|
@@ -15711,6 +14006,7 @@ var ChatFiles = class {
|
|
|
15711
14006
|
record.state = "active";
|
|
15712
14007
|
record.pausedBy = record.waitingFor = void 0;
|
|
15713
14008
|
this.changed(outgoing);
|
|
14009
|
+
this.wakeHeld();
|
|
15714
14010
|
if (record.confirmed === record.file.size) this.sendSum(outgoing);
|
|
15715
14011
|
else {
|
|
15716
14012
|
this.armStall(outgoing);
|
|
@@ -15730,11 +14026,13 @@ var ChatFiles = class {
|
|
|
15730
14026
|
record.waitingFor = frame.why === "busy" ? "busy" : "consent";
|
|
15731
14027
|
}
|
|
15732
14028
|
this.changed(outgoing);
|
|
14029
|
+
this.wakeHeld();
|
|
15733
14030
|
return;
|
|
15734
14031
|
case "pf-got":
|
|
15735
14032
|
if (!isInt$1(frame.offset) || frame.offset <= record.confirmed || frame.offset > out.next) return;
|
|
15736
14033
|
record.confirmed = frame.offset;
|
|
15737
14034
|
this.changed(outgoing, true);
|
|
14035
|
+
this.wakeHeld();
|
|
15738
14036
|
if (record.confirmed === record.file.size) this.sendSum(outgoing);
|
|
15739
14037
|
else {
|
|
15740
14038
|
this.armStall(outgoing);
|
|
@@ -15764,6 +14062,8 @@ var ChatFiles = class {
|
|
|
15764
14062
|
clearTimeout(entry.out.wake);
|
|
15765
14063
|
entry.out.block = void 0;
|
|
15766
14064
|
entry.out.source = void 0;
|
|
14065
|
+
this.moving.delete(entry);
|
|
14066
|
+
this.held.delete(entry);
|
|
15767
14067
|
}
|
|
15768
14068
|
if (entry.in) {
|
|
15769
14069
|
const incoming = entry.in;
|
|
@@ -15773,6 +14073,7 @@ var ChatFiles = class {
|
|
|
15773
14073
|
});
|
|
15774
14074
|
}
|
|
15775
14075
|
this.changed(entry);
|
|
14076
|
+
if (entry.out) this.wakeHeld();
|
|
15776
14077
|
}
|
|
15777
14078
|
entry(direction, id) {
|
|
15778
14079
|
const entry = this.entries.get(key(direction, id));
|
|
@@ -15821,6 +14122,7 @@ var ChatFiles = class {
|
|
|
15821
14122
|
this.startNext();
|
|
15822
14123
|
}
|
|
15823
14124
|
this.changed(entry);
|
|
14125
|
+
if (entry.out) this.wakeHeld();
|
|
15824
14126
|
}
|
|
15825
14127
|
resume(direction, id) {
|
|
15826
14128
|
const entry = this.entry(direction, id), { record } = entry;
|
|
@@ -16439,6 +14741,8 @@ var CarriedTransport = class {
|
|
|
16439
14741
|
/** My last packet while the edge is down: what a member that just became reachable is handed. */
|
|
16440
14742
|
mine = null;
|
|
16441
14743
|
lastTimestamp = 0n;
|
|
14744
|
+
/** The date each packet `stamp` signed carries: the relays' transport takes it rather than check the signature again. */
|
|
14745
|
+
dated = /* @__PURE__ */ new WeakMap();
|
|
16442
14746
|
/** My newest packet, wherever it went: what the edge hands the member over itself once it is ready (`linkReady`). */
|
|
16443
14747
|
newest = null;
|
|
16444
14748
|
/** The newest packet of the member's that came through a carrier, and the timestamp of the one a read last returned. */
|
|
@@ -16518,7 +14822,9 @@ var CarriedTransport = class {
|
|
|
16518
14822
|
stamp(identity, records) {
|
|
16519
14823
|
const now = BigInt(Date.now()) * 1000n;
|
|
16520
14824
|
this.lastTimestamp = now > this.lastTimestamp ? now : this.lastTimestamp + 1n;
|
|
16521
|
-
|
|
14825
|
+
const payload = createRelayPayload(identity, records, this.lastTimestamp);
|
|
14826
|
+
this.dated.set(payload, this.lastTimestamp);
|
|
14827
|
+
return payload;
|
|
16522
14828
|
}
|
|
16523
14829
|
/**
|
|
16524
14830
|
* The relays get the very packet members carried or the edge itself handed over, byte for byte, where the transport
|
|
@@ -16528,7 +14834,11 @@ var CarriedTransport = class {
|
|
|
16528
14834
|
* 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
14835
|
*/
|
|
16530
14836
|
toRelays(identity, records, payload, options) {
|
|
16531
|
-
|
|
14837
|
+
const signedAt = this.dated.get(payload);
|
|
14838
|
+
return this.inner.publishPayload ? this.inner.publishPayload(identity.pubKeyZ32, payload, signedAt === void 0 ? options : {
|
|
14839
|
+
...options,
|
|
14840
|
+
signedAt
|
|
14841
|
+
}) : this.inner.publish(identity, records, options);
|
|
16532
14842
|
}
|
|
16533
14843
|
/**
|
|
16534
14844
|
* The packet the relays just took, handed to the member over the edge itself while it is up. An edge up reads nothing
|
|
@@ -16642,142 +14952,6 @@ var CarriedTransport = class {
|
|
|
16642
14952
|
}
|
|
16643
14953
|
};
|
|
16644
14954
|
//#endregion
|
|
16645
|
-
//#region ../core/src/groupFiles.ts
|
|
16646
|
-
/**
|
|
16647
|
-
* Files and voice messages in groups (WISP 503 · Group Files). A file is announced as a group message carrying its
|
|
16648
|
-
* description; its bytes go over `files/3` between two members, from a member that holds it to one that asks for it.
|
|
16649
|
-
* The limits below are the profile's first numbers, kept in one place so they are easy to change.
|
|
16650
|
-
*/
|
|
16651
|
-
var GROUP_FILE_LIMITS = {
|
|
16652
|
-
/** The largest file a group takes. */
|
|
16653
|
-
maxBytes: 104857600,
|
|
16654
|
-
/** A file this size or smaller is fetched without asking (voice messages always are, up to `VOICE_LIMITS`). */
|
|
16655
|
-
autoBytes: 8388608,
|
|
16656
|
-
/** What a device keeps of a group's files it fetched without asking; past it, every file is asked about. */
|
|
16657
|
-
autoBytesPerGroup: 268435456,
|
|
16658
|
-
/** Bytes of group files a device serves to others in a day, when serving is on. */
|
|
16659
|
-
serveBytesPerDay: 1073741824,
|
|
16660
|
-
/** Transfers a holder serves at once, all groups together. */
|
|
16661
|
-
serveAtOnce: 3,
|
|
16662
|
-
/** File announcements a member sends to one group in a minute. */
|
|
16663
|
-
announcePerMinute: 8,
|
|
16664
|
-
/** File announcements of one member of a group fetched by themselves in `announceWindowMs`; past it, kept for a Download. */
|
|
16665
|
-
announceTaken: 30,
|
|
16666
|
-
announceWindowMs: 6e5,
|
|
16667
|
-
/** Holders remembered per file. */
|
|
16668
|
-
holders: 8,
|
|
16669
|
-
/**
|
|
16670
|
-
* Message ids a `group-have` named before their message came (a member back from away hears who holds what as its
|
|
16671
|
-
* catch-up begins), remembered with their holders for when it does.
|
|
16672
|
-
*/
|
|
16673
|
-
heardAhead: 256,
|
|
16674
|
-
/** Message ids in one `group-have`. */
|
|
16675
|
-
haveIds: 32,
|
|
16676
|
-
/** A private group's `group-have` goes on an edge at most this often; a community's, from a member, every `communityHaveEveryMs`. */
|
|
16677
|
-
haveEveryMs: 1e4,
|
|
16678
|
-
communityHaveEveryMs: 6e4,
|
|
16679
|
-
/**
|
|
16680
|
-
* How long a want waits for an offer before the next holder is asked; a transfer that does not move for as long is
|
|
16681
|
-
* left for the next holder too.
|
|
16682
|
-
*/
|
|
16683
|
-
wantWaitMs: 3e4,
|
|
16684
|
-
/** A file session closes this long after its last transfer ends. */
|
|
16685
|
-
sessionIdleMs: 6e4,
|
|
16686
|
-
/** File sessions opening at once, and open at once, all groups together. */
|
|
16687
|
-
sessionsOpening: 2,
|
|
16688
|
-
sessionsOpen: 4,
|
|
16689
|
-
/** The longest file name and type kept from a description. */
|
|
16690
|
-
nameChars: 255,
|
|
16691
|
-
mimeChars: 255
|
|
16692
|
-
};
|
|
16693
|
-
/**
|
|
16694
|
-
* The frames of WISP 503 § Getting the bytes, on an edge between two apps that announced `GROUP_VERSION_FILES`:
|
|
16695
|
-
* `group-want {g, id}` asks a holder for the file of message `id`; `group-want-no {g, id, why}` is a holder's no;
|
|
16696
|
-
* `group-have {g, ids}` says which files a member keeps (in a community, an application frame `{t, ids}` instead).
|
|
16697
|
-
*/
|
|
16698
|
-
var GROUP_WANT_FRAME = "group-want";
|
|
16699
|
-
var GROUP_WANT_NO_FRAME = "group-want-no";
|
|
16700
|
-
var GROUP_HAVE_FRAME = "group-have";
|
|
16701
|
-
var REFUSALS = [
|
|
16702
|
-
"gone",
|
|
16703
|
-
"busy",
|
|
16704
|
-
"refused"
|
|
16705
|
-
];
|
|
16706
|
-
/** A group message's id, as a private group (`<s>:<e>:<n>`) or a community (`<s>:<e>:<h>:<n>`) names it. */
|
|
16707
|
-
var MESSAGE_ID = /^[a-z0-9]{52}:\d{1,15}(:[A-Za-z0-9_-]{1,64})?:\d{1,15}$/;
|
|
16708
|
-
var isGroupMessageId = (value) => typeof value === "string" && MESSAGE_ID.test(value);
|
|
16709
|
-
function frameOf(value, t, groupId) {
|
|
16710
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
16711
|
-
const f = value;
|
|
16712
|
-
return f.t === t && (groupId === void 0 || f.g === groupId) ? f : null;
|
|
16713
|
-
}
|
|
16714
|
-
/** A `group-want` of group `groupId`: the message whose file is asked for, or null. */
|
|
16715
|
-
function readGroupWant(value, groupId) {
|
|
16716
|
-
const f = frameOf(value, GROUP_WANT_FRAME, groupId);
|
|
16717
|
-
return f && isGroupMessageId(f.id) ? { id: f.id } : null;
|
|
16718
|
-
}
|
|
16719
|
-
/** A holder's `group-want-no` of group `groupId`, or null. A reason this app does not know reads as `busy`. */
|
|
16720
|
-
function readGroupWantNo(value, groupId) {
|
|
16721
|
-
const f = frameOf(value, GROUP_WANT_NO_FRAME, groupId);
|
|
16722
|
-
if (!f || !isGroupMessageId(f.id)) return null;
|
|
16723
|
-
return {
|
|
16724
|
-
id: f.id,
|
|
16725
|
-
why: typeof f.why === "string" && REFUSALS.includes(f.why) ? f.why : "busy"
|
|
16726
|
-
};
|
|
16727
|
-
}
|
|
16728
|
-
/**
|
|
16729
|
-
* The message ids of a `group-have`: of group `groupId` on an edge, or (no `groupId`) a community's application frame.
|
|
16730
|
-
* At most `haveIds` message ids; a frame with more, or anything else, is null.
|
|
16731
|
-
*/
|
|
16732
|
-
function readGroupHave(value, groupId) {
|
|
16733
|
-
const f = frameOf(value, GROUP_HAVE_FRAME, groupId);
|
|
16734
|
-
if (!f || !Array.isArray(f.ids) || !f.ids.length || f.ids.length > GROUP_FILE_LIMITS.haveIds || !f.ids.every(isGroupMessageId)) return null;
|
|
16735
|
-
return [...new Set(f.ids)];
|
|
16736
|
-
}
|
|
16737
|
-
var DIGEST = /^[A-Za-z0-9_-]{43}$/;
|
|
16738
|
-
/**
|
|
16739
|
-
* A description read from the wire, cleaned: the name made a display name (never a path), the type untrusted, the
|
|
16740
|
-
* size within the group limit, the digest the right shape. A malformed voice, video or picture description is dropped
|
|
16741
|
-
* on its own and the file is still a file; anything else malformed is no file at all (null), and the text shows.
|
|
16742
|
-
*/
|
|
16743
|
-
function readGroupFileMeta(value) {
|
|
16744
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
16745
|
-
const v = value;
|
|
16746
|
-
if (typeof v.name !== "string" || !v.name.trim() || [...v.name].length > GROUP_FILE_LIMITS.nameChars) return null;
|
|
16747
|
-
if (typeof v.mime !== "string" || v.mime.length > GROUP_FILE_LIMITS.mimeChars) return null;
|
|
16748
|
-
if (!Number.isSafeInteger(v.size) || v.size < 1 || v.size > GROUP_FILE_LIMITS.maxBytes) return null;
|
|
16749
|
-
if (typeof v.d !== "string" || !DIGEST.test(v.d)) return null;
|
|
16750
|
-
const mime = sanitizeMime(v.mime);
|
|
16751
|
-
const voice = v.voice === void 0 ? void 0 : parseVoiceMeta(v.voice, mime);
|
|
16752
|
-
const video = v.video === void 0 ? void 0 : parseVideoMeta(v.video, mime);
|
|
16753
|
-
const image = v.image === void 0 ? void 0 : parseImageMeta(v.image, mime);
|
|
16754
|
-
return {
|
|
16755
|
-
name: sanitizeFileName(v.name),
|
|
16756
|
-
mime,
|
|
16757
|
-
size: v.size,
|
|
16758
|
-
d: v.d,
|
|
16759
|
-
...voice ? { voice } : {},
|
|
16760
|
-
...video ? { video } : {},
|
|
16761
|
-
...image ? { image } : {}
|
|
16762
|
-
};
|
|
16763
|
-
}
|
|
16764
|
-
/**
|
|
16765
|
-
* The text an announcement carries when its author wrote no caption: what an app without group files shows instead
|
|
16766
|
-
* of the file. English, as wire text is; an app that knows the file shows the file, not this line.
|
|
16767
|
-
*/
|
|
16768
|
-
function groupFileFallback(file) {
|
|
16769
|
-
if (file.voice) return `🎤 Voice message (${formatVoiceDuration(file.voice.duration)}): update Ghostly to hear it`;
|
|
16770
|
-
return `${fileMessageText(file)} (${formatFileSize(file.size)}): update Ghostly to get files in groups`;
|
|
16771
|
-
}
|
|
16772
|
-
/**
|
|
16773
|
-
* Whether a device fetches a file by itself (WISP 503 § Automatic and asked downloads): a voice message always, any
|
|
16774
|
-
* other file up to `autoBytes` while what it took that way from the group stays within `autoBytesPerGroup`.
|
|
16775
|
-
*/
|
|
16776
|
-
function fetchesByItself(file, keptAutomatically, limits = GROUP_FILE_LIMITS) {
|
|
16777
|
-
if (file.voice) return true;
|
|
16778
|
-
return file.size <= limits.autoBytes && keptAutomatically + file.size <= limits.autoBytesPerGroup;
|
|
16779
|
-
}
|
|
16780
|
-
//#endregion
|
|
16781
14955
|
//#region ../core/src/pairingProgress.ts
|
|
16782
14956
|
var FIRST_STAGE = {
|
|
16783
14957
|
inviter: "publishing",
|
|
@@ -17078,6 +15252,14 @@ var PACKET_TRANSPORTS_FAST_ATTEMPTS = 3;
|
|
|
17078
15252
|
var DHT_PIN_GRACE_MS = 1e4;
|
|
17079
15253
|
/** Back after a restart (`resume`), dials wait this long at most for the native endpoint that carried the chat to start. */
|
|
17080
15254
|
var RESUME_WAIT_MS = 1e4;
|
|
15255
|
+
/**
|
|
15256
|
+
* A dial of the contact's that meets such a session waits this long for that session to answer a ping. It answers when
|
|
15257
|
+
* the dials crossed (the contact holds this side's dial too); it does not when the contact's app ended since and is
|
|
15258
|
+
* back: its dial then takes over. Refused outright, that dial left the contact waiting for the dead session's end, 16.6
|
|
15259
|
+
* 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
|
|
15260
|
+
* 7 ms on loopback and under 0.5 s over a relayed Iroh.
|
|
15261
|
+
*/
|
|
15262
|
+
var CROSSED_PROBE_MS = 1500;
|
|
17081
15263
|
/** A WebRTC attempt under way, not yet open: what a member's packet saying it has no WebRTC ends (`peerPacketTransports`). */
|
|
17082
15264
|
var WEBRTC_ATTEMPT = /* @__PURE__ */ new Set([
|
|
17083
15265
|
"offering",
|
|
@@ -17199,6 +15381,18 @@ var HANDLED_WAIT_MS = 1e4;
|
|
|
17199
15381
|
*/
|
|
17200
15382
|
var WAKE_LOST_MS = 1e4;
|
|
17201
15383
|
/**
|
|
15384
|
+
* What files/3 may hold on a session's channel (WISP 501, Backpressure): a quarter of its send budget (native and iroh
|
|
15385
|
+
* channels throw past 120 KiB), or the high water mark of a WebRTC channel, which has none.
|
|
15386
|
+
*/
|
|
15387
|
+
function filesCap(channel) {
|
|
15388
|
+
return channel.sendBudget ? channel.sendBudget / 4 : LIMITS.sendHighWaterMark;
|
|
15389
|
+
}
|
|
15390
|
+
/**
|
|
15391
|
+
* 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
|
|
15392
|
+
* their line (WISP 1200 § In a chat): a file waiting for `pf-got` holds its share meanwhile.
|
|
15393
|
+
*/
|
|
15394
|
+
var FILES_IN_USE_MS = 2e3;
|
|
15395
|
+
/**
|
|
17202
15396
|
* A `paired-message` frame. The preview (`pv`) is left out when the frame would pass `MAX_PAIRED_MESSAGE_FRAME`
|
|
17203
15397
|
* with it: the text matters, the card does not. A reply (`r`, WISP 401 § Replies) is a few hundred bytes at most
|
|
17204
15398
|
* and always goes, and so does a forwarded message's hop count (`fw`, WISP 401 § Forwards) and a status card (`sc`,
|
|
@@ -17546,6 +15740,7 @@ var GhostLink = class {
|
|
|
17546
15740
|
});
|
|
17547
15741
|
options.events?.onPairingProgress?.(progress);
|
|
17548
15742
|
}) : null;
|
|
15743
|
+
const invite = options.pairingProgress?.role === "inviter" && !!options.pairing && !options.pairing.credentials.peerKey;
|
|
17549
15744
|
this.dht = options.params.profile && options.pairing && options.dht ? new DhtDelivery({
|
|
17550
15745
|
params: options.params,
|
|
17551
15746
|
mode: this.deliveryMode,
|
|
@@ -17555,6 +15750,7 @@ var GhostLink = class {
|
|
|
17555
15750
|
save: options.dht.save,
|
|
17556
15751
|
pollMs: options.dht.pollMs,
|
|
17557
15752
|
firstControlAfterMs: options.dht.firstControlAfterMs,
|
|
15753
|
+
...invite && { inviteMadeAt: options.madeAt ?? Date.now() },
|
|
17558
15754
|
capsRev: options.dht.capsRev,
|
|
17559
15755
|
peerCapsRev: options.dht.peerCapsRev,
|
|
17560
15756
|
peerAcceptsText: options.dht.peerAcceptsText,
|
|
@@ -17570,6 +15766,7 @@ var GhostLink = class {
|
|
|
17570
15766
|
},
|
|
17571
15767
|
pin: async (key) => {
|
|
17572
15768
|
await options.pairing.pinPeer(key, true);
|
|
15769
|
+
if (invite) this.session.lookNow();
|
|
17573
15770
|
if (this.isDataLinkOpen || !this.tracker || this.tracker.done) return;
|
|
17574
15771
|
if (this.deliveryMode === "dht") {
|
|
17575
15772
|
this.tracker.onDht("chosen");
|
|
@@ -17640,6 +15837,7 @@ var GhostLink = class {
|
|
|
17640
15837
|
lastSeenTimestamp: options.lastSeenTimestamp,
|
|
17641
15838
|
pollIntervals: options.pollIntervals,
|
|
17642
15839
|
awaitingSince: options.madeAt,
|
|
15840
|
+
...invite && { invite: () => !options.pairing.credentials.peerKey },
|
|
17643
15841
|
getServices: options.getServices,
|
|
17644
15842
|
firstPublish: options.firstPublish ?? (this.tracker && options.pairingProgress?.role === "joiner" && options.rtcAvailable !== false || this.resuming ? "after-first-poll" : "at-start"),
|
|
17645
15843
|
events: {
|
|
@@ -18152,11 +16350,11 @@ var GhostLink = class {
|
|
|
18152
16350
|
}
|
|
18153
16351
|
start() {
|
|
18154
16352
|
if (this.saidTransports) this.publishPacketTransports();
|
|
18155
|
-
if (this.deliveryMode !== "dht") this.session.start();
|
|
18156
16353
|
if (this.resuming && this.deliveryMode !== "dht") {
|
|
18157
16354
|
this.session.expectPeer();
|
|
18158
16355
|
this.resumeWaitUntil = Date.now() + RESUME_WAIT_MS;
|
|
18159
16356
|
}
|
|
16357
|
+
if (this.deliveryMode !== "dht") this.session.start();
|
|
18160
16358
|
if (this.tracker && !this.tracker.done && this.options.pairingProgress?.role === "joiner") {
|
|
18161
16359
|
this.dht?.expect();
|
|
18162
16360
|
this.dialEarly();
|
|
@@ -18278,6 +16476,7 @@ var GhostLink = class {
|
|
|
18278
16476
|
disconnect() {
|
|
18279
16477
|
this.closeRetired();
|
|
18280
16478
|
this.dropCrossed();
|
|
16479
|
+
this.dropCrossedProbe();
|
|
18281
16480
|
this.dropBlockedDial();
|
|
18282
16481
|
this.switcher.stop();
|
|
18283
16482
|
this.cancelCandidate();
|
|
@@ -19284,6 +17483,18 @@ var GhostLink = class {
|
|
|
19284
17483
|
if (!this.options.params.profile || this.options.packetTransports) return true;
|
|
19285
17484
|
return this.transportOffer().some((t) => t === "webrtc/1" ? !this.peerTransports || this.peerTransports.includes(t) : this.canDial(t) && !!this.peerTransports?.includes(t));
|
|
19286
17485
|
}
|
|
17486
|
+
/**
|
|
17487
|
+
* Neither side can dial the other until something changes, past a first pairing: what the contact runs is known, and
|
|
17488
|
+
* no transport both run has WebRTC on both sides or an endpoint up on both (this side's, and the one its record
|
|
17489
|
+
* describes). A chat that gave its listener up to another (`yieldEndpoint`), or whose contact did, is so. It dials
|
|
17490
|
+
* again when that changes (a listener taken back, `registerEndpoint`; the record describing one, `learnPeerTransports`),
|
|
17491
|
+
* and reads at its own pace meanwhile: looks every 20 s for a dial with nothing to try, and the contact's fast looks
|
|
17492
|
+
* for an offer that could not come, read its key 2.5x to 4x as often as the background pace, for good (bug hunt,
|
|
17493
|
+
* 2026-10-12). A first pairing keeps both: the records are what it is reading for.
|
|
17494
|
+
*/
|
|
17495
|
+
nothingToDialEitherWay() {
|
|
17496
|
+
return !(!!this.tracker && !this.tracker.done && this.tracker.progress.stage !== "on-dht") && !!this.peerTransports && !this.anythingToDial();
|
|
17497
|
+
}
|
|
19287
17498
|
clearRace() {
|
|
19288
17499
|
if (this.raceTimer) clearTimeout(this.raceTimer);
|
|
19289
17500
|
this.raceTimer = null;
|
|
@@ -19583,7 +17794,7 @@ var GhostLink = class {
|
|
|
19583
17794
|
return;
|
|
19584
17795
|
}
|
|
19585
17796
|
} else if (role !== "joiner" && this.myPubKeyZ32 > this.options.params.peerPubKeyZ32 && !this.resuming && !this.dialsPastRtc()) {
|
|
19586
|
-
if (Date.now() - presenceSeenAt(presence) < 3e4 && presence.lastPacketAt !== this.offerAwaitedFor) {
|
|
17797
|
+
if (Date.now() - presenceSeenAt(presence) < 3e4 && presence.lastPacketAt !== this.offerAwaitedFor && !this.nothingToDialEitherWay()) {
|
|
19587
17798
|
this.offerAwaitedFor = presence.lastPacketAt;
|
|
19588
17799
|
this.session.expectPeer();
|
|
19589
17800
|
}
|
|
@@ -19661,6 +17872,7 @@ var GhostLink = class {
|
|
|
19661
17872
|
this.retryLook = null;
|
|
19662
17873
|
const from = this.lastAutoConnectAt;
|
|
19663
17874
|
if (!from || this.stopped || this.leaving || this.streamBlocked || this.keyStopped || !this.options.autoConnect) return;
|
|
17875
|
+
if (this.nothingToDialEitherWay()) return;
|
|
19664
17876
|
this.retryLook = setTimeout(() => {
|
|
19665
17877
|
this.retryLook = null;
|
|
19666
17878
|
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 +18132,7 @@ var GhostLink = class {
|
|
|
19920
18132
|
if (this.options.callsSupport) offered.push(CALLS_CAPABILITY);
|
|
19921
18133
|
if (this.options.servicesSupport) offered.push(SERVICES_CAPABILITY);
|
|
19922
18134
|
if (this.options.largeFilesSupport) offered.push(FILES_CAPABILITY);
|
|
18135
|
+
if (this.options.largeFilesSupport && this.options.binaryFilesSupport && this.channel?.binary) offered.push(FILES_BINARY_CAPABILITY);
|
|
19923
18136
|
if (this.options.typingSupport) offered.push(TYPING_CAPABILITY);
|
|
19924
18137
|
if (this.options.reactionsSupport) offered.push(REACTIONS_CAPABILITY);
|
|
19925
18138
|
if (this.options.editSupport) offered.push(EDIT_CAPABILITY);
|
|
@@ -19956,6 +18169,10 @@ var GhostLink = class {
|
|
|
19956
18169
|
get supportsLargeFiles() {
|
|
19957
18170
|
return this.isDataLinkOpen && this.sessionCapabilities.agreed("files/3");
|
|
19958
18171
|
}
|
|
18172
|
+
/** Both sides offer `files/4` beside `files/3` on the open session: a file's bytes go as binary messages. */
|
|
18173
|
+
get supportsBinaryFiles() {
|
|
18174
|
+
return this.supportsLargeFiles && this.sessionCapabilities.agreed("files/4");
|
|
18175
|
+
}
|
|
19959
18176
|
/** Both sides offer `typing/1` on the open session: typing can be said and shown. */
|
|
19960
18177
|
get supportsTyping() {
|
|
19961
18178
|
return this.isDataLinkOpen && this.sessionCapabilities.agreed("typing/1");
|
|
@@ -20119,13 +18336,18 @@ var GhostLink = class {
|
|
|
20119
18336
|
}
|
|
20120
18337
|
/**
|
|
20121
18338
|
* 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.
|
|
18339
|
+
* kept to send later. While the channel already holds a quarter of what it may queue (its send budget, or the high
|
|
18340
|
+
* water mark of a WebRTC channel), the frame is refused with `too-fast`, so the chat's messages and pings never wait
|
|
18341
|
+
* behind an app's data on a link slower than it sends (WISP 1200 § In a chat). While a file goes, that quarter sits
|
|
18342
|
+
* above what files may hold (`filesWritable`): files keep the channel at their own line for the whole transfer.
|
|
20123
18343
|
*/
|
|
20124
18344
|
sendAppData(app, data) {
|
|
20125
18345
|
if (!this.apps.open.has(app)) return "not-open";
|
|
20126
18346
|
if (appDataBytes(data) > 32768) return "too-large";
|
|
20127
18347
|
if (!this.channel || !this.isDataLinkOpen) return "offline";
|
|
20128
18348
|
if (!this.supportsApps) return "peer-closed";
|
|
18349
|
+
const files = this.filesUse?.channel === this.channel && (this.filesUse.waiting > 0 || Date.now() - this.filesUse.at < FILES_IN_USE_MS) ? filesCap(this.channel) : 0;
|
|
18350
|
+
if (this.channel.bufferedAmount >= files + (this.channel.sendBudget ?? LIMITS.sendHighWaterMark) / 4) return "too-fast";
|
|
20129
18351
|
const refused = this.apps.canSend(app, data);
|
|
20130
18352
|
if (refused) return refused;
|
|
20131
18353
|
try {
|
|
@@ -20176,19 +18398,57 @@ var GhostLink = class {
|
|
|
20176
18398
|
}
|
|
20177
18399
|
}
|
|
20178
18400
|
/**
|
|
18401
|
+
* A file's bytes to the contact, on the open session: one binary message while both sides agreed `files/4` on it,
|
|
18402
|
+
* the `pf-data` frame with them as base64url text otherwise (WISP 501 § files/4). The form is chosen for each chunk,
|
|
18403
|
+
* so a transfer follows the session it is on. False when files/3 cannot flow now, or the channel has no room.
|
|
18404
|
+
*/
|
|
18405
|
+
sendFileData(id, offset, data) {
|
|
18406
|
+
if (!this.supportsLargeFiles || !this.channel) return false;
|
|
18407
|
+
try {
|
|
18408
|
+
this.channel.send(this.supportsBinaryFiles ? encodeFileData(id, offset, data) : JSON.stringify({
|
|
18409
|
+
t: "pf-data",
|
|
18410
|
+
id,
|
|
18411
|
+
offset,
|
|
18412
|
+
data: toBase64Url(data)
|
|
18413
|
+
}));
|
|
18414
|
+
this.filesUsing(this.channel).at = Date.now();
|
|
18415
|
+
return true;
|
|
18416
|
+
} catch {
|
|
18417
|
+
return false;
|
|
18418
|
+
}
|
|
18419
|
+
}
|
|
18420
|
+
/** Files on a channel: when they last wrote on it, and how many wait for room. An app's data gets room above theirs. */
|
|
18421
|
+
filesUse = null;
|
|
18422
|
+
filesUsing(channel) {
|
|
18423
|
+
if (this.filesUse?.channel !== channel) this.filesUse = {
|
|
18424
|
+
channel,
|
|
18425
|
+
at: 0,
|
|
18426
|
+
waiting: 0
|
|
18427
|
+
};
|
|
18428
|
+
return this.filesUse;
|
|
18429
|
+
}
|
|
18430
|
+
/**
|
|
20179
18431
|
* 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
18432
|
* 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.
|
|
18433
|
+
* while a file goes; others keep under the high water mark, as files/2 does. An app's data gets a quarter above that
|
|
18434
|
+
* line meanwhile (`sendAppData`).
|
|
20182
18435
|
*/
|
|
20183
18436
|
filesWritable() {
|
|
20184
18437
|
const channel = this.channel;
|
|
20185
18438
|
if (!channel) return;
|
|
20186
|
-
const cap = channel
|
|
18439
|
+
const cap = filesCap(channel);
|
|
20187
18440
|
if (channel.bufferedAmount < cap) return;
|
|
18441
|
+
const use = this.filesUsing(channel);
|
|
18442
|
+
use.waiting++;
|
|
20188
18443
|
return (async () => {
|
|
20189
|
-
|
|
20190
|
-
|
|
20191
|
-
|
|
18444
|
+
try {
|
|
18445
|
+
while (this.channel === channel && channel.bufferedAmount >= cap) {
|
|
18446
|
+
await channel.drained().catch(() => {});
|
|
18447
|
+
if (this.channel === channel && channel.bufferedAmount >= cap) await new Promise((resolve) => setTimeout(resolve, 20));
|
|
18448
|
+
}
|
|
18449
|
+
} finally {
|
|
18450
|
+
use.waiting--;
|
|
18451
|
+
use.at = Date.now();
|
|
20192
18452
|
}
|
|
20193
18453
|
})();
|
|
20194
18454
|
}
|
|
@@ -20486,13 +18746,20 @@ var GhostLink = class {
|
|
|
20486
18746
|
}
|
|
20487
18747
|
const held = this.channel;
|
|
20488
18748
|
if (held && this.activeBinding && !this.dialedIn.has(held) && Date.now() - this.channelSince < 1e4 && this.myPubKeyZ32 < this.options.params.peerPubKeyZ32) {
|
|
20489
|
-
|
|
20490
|
-
|
|
20491
|
-
|
|
20492
|
-
|
|
20493
|
-
|
|
18749
|
+
if (this.paired?.state.status !== "ready") {
|
|
18750
|
+
traceLink(this.myPubKeyZ32, "dial-crossed", {
|
|
18751
|
+
transport: binding.transport,
|
|
18752
|
+
held: true
|
|
18753
|
+
});
|
|
18754
|
+
channel.close();
|
|
18755
|
+
return;
|
|
18756
|
+
}
|
|
18757
|
+
this.probeCrossed(held, channel, binding);
|
|
20494
18758
|
return;
|
|
20495
18759
|
}
|
|
18760
|
+
this.replaceWith(channel, binding);
|
|
18761
|
+
}
|
|
18762
|
+
replaceWith(channel, binding) {
|
|
20496
18763
|
traceLink(this.myPubKeyZ32, "replacing", {
|
|
20497
18764
|
transport: binding.transport,
|
|
20498
18765
|
held: this.paired?.state.transport
|
|
@@ -20503,6 +18770,62 @@ var GhostLink = class {
|
|
|
20503
18770
|
});
|
|
20504
18771
|
}
|
|
20505
18772
|
/**
|
|
18773
|
+
* The contact dialled in while this side's own fresh dial holds the chat (`CROSSED_FRESH_MS`): the dials crossed, or
|
|
18774
|
+
* the contact's app ended since that dial and is back. The held session is pinged; heard from within
|
|
18775
|
+
* `CROSSED_PROBE_MS`, the contact holds this side's dial too and its own closes. Not heard from, the contact's dial
|
|
18776
|
+
* takes over.
|
|
18777
|
+
*/
|
|
18778
|
+
crossedProbe = null;
|
|
18779
|
+
/** Frames heard on the open session, counted from its start: whether the contact answered `probeCrossed`. */
|
|
18780
|
+
framesHeard = 0;
|
|
18781
|
+
probeCrossed(held, channel, binding) {
|
|
18782
|
+
this.dropCrossedProbe();
|
|
18783
|
+
traceLink(this.myPubKeyZ32, "dial-crossed", {
|
|
18784
|
+
transport: binding.transport,
|
|
18785
|
+
held: true,
|
|
18786
|
+
probe: true
|
|
18787
|
+
});
|
|
18788
|
+
const heard = this.framesHeard;
|
|
18789
|
+
try {
|
|
18790
|
+
this.sendPing(held, false);
|
|
18791
|
+
} catch {}
|
|
18792
|
+
const timer = setTimeout(() => {
|
|
18793
|
+
if (this.crossedProbe?.channel !== channel) return;
|
|
18794
|
+
this.crossedProbe = null;
|
|
18795
|
+
if (this.stopped || this.leaving || this.candidate) {
|
|
18796
|
+
channel.close();
|
|
18797
|
+
return;
|
|
18798
|
+
}
|
|
18799
|
+
if (!this.channel) {
|
|
18800
|
+
this.attach(channel, binding);
|
|
18801
|
+
return;
|
|
18802
|
+
}
|
|
18803
|
+
if (this.channel === held && this.framesHeard !== heard) {
|
|
18804
|
+
channel.close();
|
|
18805
|
+
return;
|
|
18806
|
+
}
|
|
18807
|
+
traceLink(this.myPubKeyZ32, "crossed-held-silent", { transport: binding.transport });
|
|
18808
|
+
this.replaceWith(channel, binding);
|
|
18809
|
+
}, CROSSED_PROBE_MS);
|
|
18810
|
+
channel.onClose = () => {
|
|
18811
|
+
if (this.crossedProbe?.channel === channel) {
|
|
18812
|
+
clearTimeout(timer);
|
|
18813
|
+
this.crossedProbe = null;
|
|
18814
|
+
}
|
|
18815
|
+
};
|
|
18816
|
+
this.crossedProbe = {
|
|
18817
|
+
channel,
|
|
18818
|
+
timer
|
|
18819
|
+
};
|
|
18820
|
+
}
|
|
18821
|
+
dropCrossedProbe() {
|
|
18822
|
+
const probe = this.crossedProbe;
|
|
18823
|
+
if (!probe) return;
|
|
18824
|
+
this.crossedProbe = null;
|
|
18825
|
+
clearTimeout(probe.timer);
|
|
18826
|
+
probe.channel.close();
|
|
18827
|
+
}
|
|
18828
|
+
/**
|
|
20506
18829
|
* Connections that came in on this side's native endpoints. Their address was in the capability record, which before
|
|
20507
18830
|
* the pin anyone holding a copy of the invite could read: a key other than the pinned one on such a connection proves
|
|
20508
18831
|
* no more than on the DHT, so it closes that connection and nothing else.
|
|
@@ -20697,7 +19020,18 @@ var GhostLink = class {
|
|
|
20697
19020
|
if (this.channel !== channel) return;
|
|
20698
19021
|
this.unansweredPings = 0;
|
|
20699
19022
|
this.heardFromPeer();
|
|
20700
|
-
|
|
19023
|
+
this.framesHeard++;
|
|
19024
|
+
if (typeof data !== "string") {
|
|
19025
|
+
const chunk = this.supportsBinaryFiles ? decodeFileData(data) : null;
|
|
19026
|
+
if (!chunk) return;
|
|
19027
|
+
this.activityAt = Date.now();
|
|
19028
|
+
await this.options.events?.onFilesFrame?.({
|
|
19029
|
+
t: "pf-data",
|
|
19030
|
+
...chunk
|
|
19031
|
+
});
|
|
19032
|
+
return;
|
|
19033
|
+
}
|
|
19034
|
+
if (data.length > 61440) return;
|
|
20701
19035
|
let frame;
|
|
20702
19036
|
try {
|
|
20703
19037
|
frame = JSON.parse(data);
|
|
@@ -25454,6 +23788,7 @@ function readFirstPlace(o, epoch) {
|
|
|
25454
23788
|
/** One of my own frames, among mine: `e:n`. */
|
|
25455
23789
|
var ownKey = (f) => `${f.e}:${f.n}`;
|
|
25456
23790
|
/** What a frame weighs in a log, roughly: its boxes plus the fixed fields around them. */
|
|
23791
|
+
var frameKey$1 = (f) => `${f.s}:${f.e}:${f.n}`;
|
|
25457
23792
|
/** What a kept frame counts for against `GROUP_LIMITS.relayBytes`. */
|
|
25458
23793
|
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
23794
|
/** Only the fields an edit frame has, as `clean` does for a message. */
|
|
@@ -25574,8 +23909,15 @@ var GroupSession = class {
|
|
|
25574
23909
|
pendingMeta;
|
|
25575
23910
|
/** While `handle` runs: the frames it took that a hub passes on (`handle`'s result). */
|
|
25576
23911
|
passOn = null;
|
|
23912
|
+
/** The frames kept to hand on that the app holds with its messages, oldest first; undefined when the state holds them all. */
|
|
23913
|
+
onRows;
|
|
25577
23914
|
/** Set while a save of what received messages changed is due (`persistSoon`). */
|
|
25578
23915
|
saveTimer;
|
|
23916
|
+
/**
|
|
23917
|
+
* The frames of members who left that each member was handed (`frameKey`), so an app that does not take them (from
|
|
23918
|
+
* before revision 2026-10-13, and never marks them seen) is not handed them again at every sync. In memory only.
|
|
23919
|
+
*/
|
|
23920
|
+
leaversHanded = /* @__PURE__ */ new Map();
|
|
25579
23921
|
/** Syncs answered per member, a few a minute (`GROUP_LIMITS.syncAnswers`). In memory only. */
|
|
25580
23922
|
syncsAnswered = /* @__PURE__ */ new Map();
|
|
25581
23923
|
/** Catch-up answers still going out, a slice at a time (`handOut`). */
|
|
@@ -25588,10 +23930,21 @@ var GroupSession = class {
|
|
|
25588
23930
|
provisional;
|
|
25589
23931
|
/** When my chain last moved (a commit made or applied), by `hooks.clock`: frames are said again once it has settled. */
|
|
25590
23932
|
chainMovedAt;
|
|
25591
|
-
|
|
23933
|
+
/**
|
|
23934
|
+
* `onRows`: the frames the app holds with the messages it stored (`GroupIncomingMessage.frame`), oldest first. Given
|
|
23935
|
+
* (empty at first), the app is where frames are kept: the saved state carries none of them, and a message received
|
|
23936
|
+
* no longer writes them all again. Absent: they are kept in the state, as before.
|
|
23937
|
+
*/
|
|
23938
|
+
constructor(state, hooks, onRows) {
|
|
25592
23939
|
this.hooks = hooks;
|
|
25593
23940
|
this.state = state;
|
|
25594
23941
|
this.identity = identityFromSeedB64(state.seedB64);
|
|
23942
|
+
if (onRows) {
|
|
23943
|
+
const held = new Set(onRows.map(frameKey$1));
|
|
23944
|
+
if (state.relay?.length) state.relay = state.relay.filter((f) => !held.has(frameKey$1(f)));
|
|
23945
|
+
this.onRows = state.status === "active" ? onRows.filter((f) => this.handsOn(f.s, f.e)) : [];
|
|
23946
|
+
this.trimRelay();
|
|
23947
|
+
}
|
|
25595
23948
|
this.answers = new CatchUpAnswers({
|
|
25596
23949
|
send: (to, frame) => hooks.send(to, frame),
|
|
25597
23950
|
...hooks.handled && { handled: (to) => hooks.handled(to) },
|
|
@@ -26003,9 +24356,13 @@ var GroupSession = class {
|
|
|
26003
24356
|
this.hooks.changed();
|
|
26004
24357
|
return { secret };
|
|
26005
24358
|
}
|
|
26006
|
-
/**
|
|
24359
|
+
/**
|
|
24360
|
+
* Someone removed is not handed on any more, whatever they sent while in the roster; one who left still is, for the
|
|
24361
|
+
* epochs before its leave (WISP 902 § Catch-up).
|
|
24362
|
+
*/
|
|
26007
24363
|
pruneRelay() {
|
|
26008
|
-
if (this.state.relay?.length) this.state.relay = this.state.relay.filter((f) =>
|
|
24364
|
+
if (this.state.relay?.length) this.state.relay = this.state.relay.filter((f) => this.handsOn(f.s, f.e));
|
|
24365
|
+
if (this.onRows?.length) this.onRows = this.onRows.filter((f) => this.handsOn(f.s, f.e));
|
|
26009
24366
|
if (this.state.relayEdits?.length) this.state.relayEdits = this.state.relayEdits.filter((f) => rosterHas(this.roster, f.s));
|
|
26010
24367
|
}
|
|
26011
24368
|
/** 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 +24378,21 @@ var GroupSession = class {
|
|
|
26021
24378
|
}
|
|
26022
24379
|
/** A frame of another member, kept to hand on; the oldest go once the log is full. */
|
|
26023
24380
|
keep(frame) {
|
|
26024
|
-
|
|
26025
|
-
|
|
26026
|
-
|
|
26027
|
-
|
|
24381
|
+
if (this.onRows) {
|
|
24382
|
+
const key = frameKey$1(frame);
|
|
24383
|
+
if (this.onRows.some((f) => frameKey$1(f) === key)) return;
|
|
24384
|
+
this.onRows.push(frame);
|
|
24385
|
+
} else (this.state.relay ??= []).push(frame);
|
|
24386
|
+
this.trimRelay();
|
|
24387
|
+
}
|
|
24388
|
+
/** The frames kept to hand on, oldest first: the state's, then the ones the app holds with its messages. */
|
|
24389
|
+
kept() {
|
|
24390
|
+
return this.onRows ? [...this.state.relay ?? [], ...this.onRows] : this.state.relay ?? [];
|
|
24391
|
+
}
|
|
24392
|
+
trimRelay() {
|
|
24393
|
+
const old = this.state.relay ?? [], rows = this.onRows ?? [];
|
|
24394
|
+
let bytes = old.reduce((sum, f) => sum + frameBytes(f), 0) + rows.reduce((sum, f) => sum + frameBytes(f), 0);
|
|
24395
|
+
while (old.length + rows.length > GROUP_LIMITS.relay || bytes > GROUP_LIMITS.relayBytes) bytes -= frameBytes((old.length ? old : rows).shift());
|
|
26028
24396
|
}
|
|
26029
24397
|
pruneSecrets() {
|
|
26030
24398
|
const epochs = Object.keys(this.state.secrets).map(Number).sort((a, b) => a - b);
|
|
@@ -26060,6 +24428,7 @@ var GroupSession = class {
|
|
|
26060
24428
|
this.state.secrets = {};
|
|
26061
24429
|
this.state.sent = [];
|
|
26062
24430
|
this.state.relay = [];
|
|
24431
|
+
if (this.onRows) this.onRows = [];
|
|
26063
24432
|
this.state.relayEdits = [];
|
|
26064
24433
|
this.state.alone = [];
|
|
26065
24434
|
this.waiting = [];
|
|
@@ -26548,7 +24917,8 @@ var GroupSession = class {
|
|
|
26548
24917
|
}
|
|
26549
24918
|
/**
|
|
26550
24919
|
* 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),
|
|
24920
|
+
* the roster (someone removed is neither heard from nor handed on, #300), or from a member in it when its author left
|
|
24921
|
+
* after it (`handsOn`), and its boxes only with the author's `xs`.
|
|
26552
24922
|
*/
|
|
26553
24923
|
async receiveMessage(from, raw) {
|
|
26554
24924
|
if (!isMessageFrame$1(raw)) return;
|
|
@@ -26557,7 +24927,7 @@ var GroupSession = class {
|
|
|
26557
24927
|
return;
|
|
26558
24928
|
}
|
|
26559
24929
|
const relayed = raw.s !== from;
|
|
26560
|
-
if (relayed && (!rosterHas(this.roster, from) || !
|
|
24930
|
+
if (relayed && (!rosterHas(this.roster, from) || !this.handsOn(raw.s, raw.e))) return;
|
|
26561
24931
|
if (!verify(fromBase64Url(raw.sig), messageSigned$1(raw), publicKeyFromZ32(raw.s))) return;
|
|
26562
24932
|
if (raw.e > this.epoch) {
|
|
26563
24933
|
this.park(from, raw);
|
|
@@ -26595,10 +24965,11 @@ var GroupSession = class {
|
|
|
26595
24965
|
n: first.n
|
|
26596
24966
|
};
|
|
26597
24967
|
if (firstFrame && rosterHas(this.state.chain[first.e]?.m ?? [], this.myKey) && this.isDuplicate(firstFrame)) {
|
|
24968
|
+
await this.hooks.kept?.(groupMessageId(raw.s, first.e, first.n), clean(raw));
|
|
26598
24969
|
this.markSeen(raw);
|
|
26599
24970
|
this.keep(clean(raw));
|
|
26600
24971
|
this.persistSoon();
|
|
26601
|
-
if (
|
|
24972
|
+
if (this.handsOn(raw.s, raw.e)) this.took(clean(raw));
|
|
26602
24973
|
return;
|
|
26603
24974
|
}
|
|
26604
24975
|
const id = first && !this.provisional.has(own) ? groupMessageId(raw.s, first.e, first.n) : own;
|
|
@@ -26635,7 +25006,7 @@ var GroupSession = class {
|
|
|
26635
25006
|
if (firstFrame && rosterHas(this.state.chain[firstFrame.e]?.m ?? [], this.myKey)) this.markSeen(firstFrame);
|
|
26636
25007
|
this.keep(clean(raw));
|
|
26637
25008
|
this.persistSoon();
|
|
26638
|
-
if (
|
|
25009
|
+
if (this.handsOn(raw.s, raw.e)) this.took(clean(raw));
|
|
26639
25010
|
}
|
|
26640
25011
|
/**
|
|
26641
25012
|
* 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 +25040,7 @@ var GroupSession = class {
|
|
|
26669
25040
|
const k = validMentions(body.m, body.text, rosterAdmin(commit.m) === raw.s && this.admin === raw.s);
|
|
26670
25041
|
const sc = body.sc === void 0 ? void 0 : readStatusCard(body.sc);
|
|
26671
25042
|
if (!validEditNumber(raw.v) && !sc) return;
|
|
26672
|
-
const copy =
|
|
25043
|
+
const copy = this.kept().find((f) => f.s === raw.s && f.e === raw.e && f.n === raw.n);
|
|
26673
25044
|
const first = copy && readFirstPlace(copy.o, copy.e);
|
|
26674
25045
|
await this.hooks.edit?.({
|
|
26675
25046
|
id: first ? groupMessageId(raw.s, first.e, first.n) : groupMessageId(raw.s, raw.e, raw.n),
|
|
@@ -26830,6 +25201,16 @@ var GroupSession = class {
|
|
|
26830
25201
|
await this.receiveCommit(from, next);
|
|
26831
25202
|
}
|
|
26832
25203
|
}
|
|
25204
|
+
/**
|
|
25205
|
+
* Whether a frame of `author` under `epoch` is taken from another member, kept and handed on: while its author is in
|
|
25206
|
+
* the roster, or when it left (its removal marked `left`, and it is not back in) and the frame is from before that.
|
|
25207
|
+
*/
|
|
25208
|
+
handsOn(author, epoch) {
|
|
25209
|
+
if (rosterHas(this.roster, author)) return true;
|
|
25210
|
+
const chain = this.state.chain;
|
|
25211
|
+
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;
|
|
25212
|
+
return false;
|
|
25213
|
+
}
|
|
26833
25214
|
/** Keeps the admin's mark on a commit that took out a member who left (`GroupCommitFrame.left`) when it holds; true when new. */
|
|
26834
25215
|
takeLeftMark(commit, mark) {
|
|
26835
25216
|
if (commit.k !== "remove" || typeof mark !== "string" || mark.length !== 86 || !B64$1.test(mark) || this.state.leftMarks?.[commit.e]) return false;
|
|
@@ -26924,7 +25305,15 @@ var GroupSession = class {
|
|
|
26924
25305
|
});
|
|
26925
25306
|
const lacks = missingIn(frame, from);
|
|
26926
25307
|
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
|
|
25308
|
+
const handed = this.leaversHanded.get(from) ?? /* @__PURE__ */ new Set();
|
|
25309
|
+
const kept = !asked.size && !this.state.leftMarks ? [] : this.kept().filter((f) => {
|
|
25310
|
+
if (!this.state.chain[f.e] || !rosterHas(this.state.chain[f.e].m, from) || !lacks(f)) return false;
|
|
25311
|
+
if (asked.has(f.s)) return true;
|
|
25312
|
+
if (rosterHas(this.roster, f.s) || handed.has(frameKey$1(f)) || !this.handsOn(f.s, f.e)) return false;
|
|
25313
|
+
handed.add(frameKey$1(f));
|
|
25314
|
+
return true;
|
|
25315
|
+
});
|
|
25316
|
+
if (handed.size) this.leaversHanded.set(from, handed);
|
|
26928
25317
|
answer.push(...asWritten(this.ownLacked(from, frame), kept));
|
|
26929
25318
|
for (const author of asked) {
|
|
26930
25319
|
const edits = (this.state.relayEdits ?? []).filter((f) => f.s === author && this.state.chain[f.e] && rosterHas(this.state.chain[f.e].m, from));
|
|
@@ -27231,9 +25620,10 @@ function knockIdentity(link) {
|
|
|
27231
25620
|
function knockRecords(link, knocks) {
|
|
27232
25621
|
const nonce = randomBytes(NONCE_LENGTH);
|
|
27233
25622
|
const body = utf8Encode(JSON.stringify(knocks.slice(-6).map((k) => [k.key, k.ts])));
|
|
25623
|
+
const box = xchacha20poly1305(knockMaterial(link, "knock key"), nonce, utf8Encode(link.g)).encrypt(body);
|
|
27234
25624
|
return [{
|
|
27235
25625
|
label: KNOCK_LABEL,
|
|
27236
|
-
value: toBase64Url(concatBytes$1(nonce,
|
|
25626
|
+
value: toBase64Url(concatBytes$1(nonce, box))
|
|
27237
25627
|
}];
|
|
27238
25628
|
}
|
|
27239
25629
|
/** Knocks that open with the link's key and are well formed; anything else is nobody. */
|
|
@@ -27320,6 +25710,8 @@ var COMMUNITY_LIMITS = {
|
|
|
27320
25710
|
/** Commits in one welcome or chain piece. */
|
|
27321
25711
|
chainPiece: 100,
|
|
27322
25712
|
pendingLeaves: 64,
|
|
25713
|
+
/** Leaves whose mark (`group-left`) is kept, and the epochs one names. */
|
|
25714
|
+
leftMarks: 64,
|
|
27323
25715
|
/**
|
|
27324
25716
|
* Syncs of one member answered in `syncWindowMs`: each answer may carry the chain's tail, secrets and the whole
|
|
27325
25717
|
* store. A member catching up asks every five seconds at most, and every member syncs every thirty; one asking more
|
|
@@ -27436,6 +25828,14 @@ var leaveStatement = (g, s) => utf8Encode(JSON.stringify([
|
|
|
27436
25828
|
g,
|
|
27437
25829
|
s
|
|
27438
25830
|
]));
|
|
25831
|
+
/** What the member who committed a leave (`h`) had heard of the leaver then: `e:h16` → its highest number there. */
|
|
25832
|
+
var leftStatement = (g, h, hi) => utf8Encode(JSON.stringify([
|
|
25833
|
+
"ghostly-group/2 left",
|
|
25834
|
+
g,
|
|
25835
|
+
h,
|
|
25836
|
+
Object.entries(hi).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)
|
|
25837
|
+
]));
|
|
25838
|
+
var LEFT_EPOCH = /^\d{1,15}:[a-f0-9]{16}$/;
|
|
27439
25839
|
/** The secret of an epoch that adds, re-roles or re-links: from the previous one and the commit. */
|
|
27440
25840
|
function derivedSecret(previous, untaggedHash) {
|
|
27441
25841
|
return hkdf(sha256, previous, utf8Encode(untaggedHash), utf8Encode("ghostly-group/2 derived"), 32);
|
|
@@ -27899,13 +26299,58 @@ var CommunitySession = class {
|
|
|
27899
26299
|
return -1;
|
|
27900
26300
|
}
|
|
27901
26301
|
/**
|
|
27902
|
-
*
|
|
27903
|
-
*
|
|
27904
|
-
*
|
|
26302
|
+
* Whether a frame of someone the chain took out is still heard. A member removed is heard no more, even for an epoch
|
|
26303
|
+
* it was in. What one who left wrote while in the group is taken and handed on, its goodbye included, up to what the
|
|
26304
|
+
* member who committed the leave had heard of it then (its `group-left` mark), never past it: the leaver still holds
|
|
26305
|
+
* those epochs' secrets, and anything it signs later is not heard (WISP 903 § Leaving and removal). No mark here
|
|
26306
|
+
* (one not come yet, or a committer's app before it) and nothing of it is heard.
|
|
27905
26307
|
*/
|
|
27906
|
-
|
|
26308
|
+
heardAfterOut(f) {
|
|
26309
|
+
const i = this.outIndex(f.s);
|
|
26310
|
+
if (i < 0) return true;
|
|
26311
|
+
const commit = this.state.chain[i];
|
|
26312
|
+
if (commit.k !== "leave") return false;
|
|
26313
|
+
const high = this.state.left?.[communityCommitHash(commit)]?.hi[seenKey(f.e, f.h)];
|
|
26314
|
+
return high !== void 0 && f.n <= high;
|
|
26315
|
+
}
|
|
26316
|
+
/** The mark of a leave on my branch, as a frame to hand on (see `heardAfterOut`). */
|
|
26317
|
+
leftFrame(key) {
|
|
27907
26318
|
const i = this.outIndex(key);
|
|
27908
|
-
|
|
26319
|
+
if (i < 0 || this.state.chain[i].k !== "leave") return void 0;
|
|
26320
|
+
const h = communityCommitHash(this.state.chain[i]), mark = this.state.left?.[h];
|
|
26321
|
+
return mark && {
|
|
26322
|
+
t: "group-left",
|
|
26323
|
+
v: 2,
|
|
26324
|
+
g: this.id,
|
|
26325
|
+
h,
|
|
26326
|
+
hi: mark.hi,
|
|
26327
|
+
sig: mark.sig
|
|
26328
|
+
};
|
|
26329
|
+
}
|
|
26330
|
+
keepLeft(h, mark) {
|
|
26331
|
+
const left = { ...this.state.left ?? {} };
|
|
26332
|
+
delete left[h];
|
|
26333
|
+
left[h] = mark;
|
|
26334
|
+
const keys = Object.keys(left);
|
|
26335
|
+
for (const old of keys.slice(0, Math.max(0, keys.length - COMMUNITY_LIMITS.leftMarks))) delete left[old];
|
|
26336
|
+
this.state.left = left;
|
|
26337
|
+
}
|
|
26338
|
+
/** A leave's mark, from the member who committed it: kept when it is that member's, for a leave I hold. */
|
|
26339
|
+
receiveLeft(frame) {
|
|
26340
|
+
const { h, hi, sig } = frame;
|
|
26341
|
+
if (typeof h !== "string" || !HASH.test(h) || typeof sig !== "string" || !SIG.test(sig) || !isObject$2(hi)) return false;
|
|
26342
|
+
const entries = Object.entries(hi);
|
|
26343
|
+
if (entries.length > COMMUNITY_LIMITS.leftMarks || entries.some(([k, n]) => !LEFT_EPOCH.test(k) || !Number.isSafeInteger(n) || n < 0)) return false;
|
|
26344
|
+
const commit = this.known.get(h);
|
|
26345
|
+
if (!commit || commit.k !== "leave" || this.state.left?.[h]?.sig === sig) return false;
|
|
26346
|
+
const marks = hi;
|
|
26347
|
+
if (!verify(fromBase64Url(sig), leftStatement(this.id, h, marks), publicKeyFromZ32(commit.by))) return false;
|
|
26348
|
+
this.keepLeft(h, {
|
|
26349
|
+
hi: marks,
|
|
26350
|
+
sig
|
|
26351
|
+
});
|
|
26352
|
+
this.persistSoon();
|
|
26353
|
+
return true;
|
|
27909
26354
|
}
|
|
27910
26355
|
/**
|
|
27911
26356
|
* What someone the chain took out is told when it comes back (WISP 903 § Leaving and removal): the commits from where
|
|
@@ -28507,10 +26952,11 @@ var CommunitySession = class {
|
|
|
28507
26952
|
continue;
|
|
28508
26953
|
}
|
|
28509
26954
|
try {
|
|
28510
|
-
await this.commit("leave", {
|
|
26955
|
+
const { hash } = await this.commit("leave", {
|
|
28511
26956
|
s: request.s,
|
|
28512
26957
|
ls: request.ls
|
|
28513
26958
|
}, now);
|
|
26959
|
+
this.markLeft(hash, request.s);
|
|
28514
26960
|
done++;
|
|
28515
26961
|
} catch {}
|
|
28516
26962
|
this.state.pendingLeaves = this.state.pendingLeaves.filter((r) => r !== request);
|
|
@@ -28519,6 +26965,25 @@ var CommunitySession = class {
|
|
|
28519
26965
|
return done;
|
|
28520
26966
|
});
|
|
28521
26967
|
}
|
|
26968
|
+
/** Signs and says how far I had heard a member whose leave I just committed (`group-left`). */
|
|
26969
|
+
markLeft(h, leaver) {
|
|
26970
|
+
const hi = {};
|
|
26971
|
+
const heard = Object.entries(this.state.seen[leaver] ?? {}).filter(([k]) => LEFT_EPOCH.test(k)).slice(-COMMUNITY_LIMITS.leftMarks);
|
|
26972
|
+
for (const [k, entry] of heard) hi[k] = entry.high;
|
|
26973
|
+
const sig = toBase64Url(sign(leftStatement(this.id, h, hi), this.identity.seed));
|
|
26974
|
+
this.keepLeft(h, {
|
|
26975
|
+
hi,
|
|
26976
|
+
sig
|
|
26977
|
+
});
|
|
26978
|
+
this.hooks.broadcast({
|
|
26979
|
+
t: "group-left",
|
|
26980
|
+
v: 2,
|
|
26981
|
+
g: this.id,
|
|
26982
|
+
h,
|
|
26983
|
+
hi,
|
|
26984
|
+
sig
|
|
26985
|
+
});
|
|
26986
|
+
}
|
|
28522
26987
|
out(status, reason) {
|
|
28523
26988
|
this.state.status = status;
|
|
28524
26989
|
this.state.statusReason = reason;
|
|
@@ -28842,6 +27307,7 @@ var CommunitySession = class {
|
|
|
28842
27307
|
return false;
|
|
28843
27308
|
case "group-meta": return this.receiveMeta(from, raw);
|
|
28844
27309
|
case "group-leave": return this.receiveLeave(frame);
|
|
27310
|
+
case "group-left": return this.receiveLeft(frame);
|
|
28845
27311
|
case "group-entry": {
|
|
28846
27312
|
if (frame.to !== this.myKey || !isSealed(frame.s) || frame.x !== this.currentEntryKey()) return false;
|
|
28847
27313
|
const seed = openSecret(this.identity.seed, this.myKey, frame.s, entryAad(this.id, this.myKey));
|
|
@@ -28870,7 +27336,7 @@ var CommunitySession = class {
|
|
|
28870
27336
|
return false;
|
|
28871
27337
|
}
|
|
28872
27338
|
if (!verify(fromBase64Url(raw.sig), messageSigned(raw), publicKeyFromZ32(raw.s))) return false;
|
|
28873
|
-
if (this.
|
|
27339
|
+
if (!this.heardAfterOut(raw)) return false;
|
|
28874
27340
|
const found = this.commitByShort(raw.e, raw.h);
|
|
28875
27341
|
if (!found) {
|
|
28876
27342
|
this.park(from, raw);
|
|
@@ -29110,16 +27576,22 @@ var CommunitySession = class {
|
|
|
29110
27576
|
const have = frame.have && typeof frame.have === "object" ? frame.have : {};
|
|
29111
27577
|
const handed = /* @__PURE__ */ new Set();
|
|
29112
27578
|
const unheard = new Set(this.state.unheard ?? []);
|
|
27579
|
+
const marked = /* @__PURE__ */ new Set();
|
|
29113
27580
|
for (const stored of this.state.store) {
|
|
29114
27581
|
if (stored.s === from) continue;
|
|
29115
27582
|
const high = have[stored.s]?.[seenKey(stored.e, stored.h)];
|
|
29116
27583
|
if (Number.isSafeInteger(high) && high >= stored.n) continue;
|
|
29117
|
-
if (this.
|
|
27584
|
+
if (!this.heardAfterOut(stored)) continue;
|
|
29118
27585
|
const found = this.commitByShort(stored.e, stored.h);
|
|
29119
27586
|
if (!found) continue;
|
|
29120
27587
|
const reads = rosterHas(this.rosterAt(found.hash) ?? [], from), mine = stored.s === this.myKey && unheard.has(frameKey(stored));
|
|
29121
27588
|
if (!reads && !(mine && rosterHas(this.roster, from))) continue;
|
|
29122
27589
|
if (!this.mainIndex.has(found.hash)) this.handSide(from, found.hash, handed, answer);
|
|
27590
|
+
if (!rosterHas(this.roster, stored.s) && !marked.has(stored.s)) {
|
|
27591
|
+
marked.add(stored.s);
|
|
27592
|
+
const left = this.leftFrame(stored.s);
|
|
27593
|
+
if (left) answer.push(left);
|
|
27594
|
+
}
|
|
29123
27595
|
answer.push(stored);
|
|
29124
27596
|
if (mine && reads) unheard.delete(frameKey(stored));
|
|
29125
27597
|
}
|
|
@@ -29781,8 +28253,63 @@ function appDigest(manifestBytes) {
|
|
|
29781
28253
|
return toBase64Url(sha256(manifestBytes));
|
|
29782
28254
|
}
|
|
29783
28255
|
var u32 = (b, at) => (b[at] << 24 >>> 0) + (b[at + 1] << 16) + (b[at + 2] << 8) + b[at + 3];
|
|
29784
|
-
/**
|
|
29785
|
-
|
|
28256
|
+
/**
|
|
28257
|
+
* Lets the thread go for one turn: what waits (input, drawing, timers, port messages, IndexedDB answers) runs, then the
|
|
28258
|
+
* caller goes on. A message to itself, queued behind the page's other tasks. Never `scheduler.yield()`: in Chromium its
|
|
28259
|
+
* continuation outranks every normal task, so a paced read of 4096 revocations held the page's timers, its apps' broker
|
|
28260
|
+
* ports and IndexedDB for the whole 1.8 s. Never `setTimeout(0)` but as the last resort: from the fifth nested timer on
|
|
28261
|
+
* the browser waits at least 4 ms (HTML's clamp), which made that read take 60% longer than the work, and a hidden tab
|
|
28262
|
+
* runs timers once a second.
|
|
28263
|
+
*/
|
|
28264
|
+
function yieldThread() {
|
|
28265
|
+
if (typeof MessageChannel === "function") return new Promise((resolve) => {
|
|
28266
|
+
const channel = new MessageChannel();
|
|
28267
|
+
channel.port1.onmessage = () => {
|
|
28268
|
+
channel.port1.close();
|
|
28269
|
+
resolve();
|
|
28270
|
+
};
|
|
28271
|
+
channel.port2.postMessage(null);
|
|
28272
|
+
});
|
|
28273
|
+
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
28274
|
+
}
|
|
28275
|
+
/**
|
|
28276
|
+
* A pause for a loop of short steps: `await pause()` after each step lets the thread go once `sliceMs` of work has
|
|
28277
|
+
* run since the last time, and answers at once otherwise.
|
|
28278
|
+
*/
|
|
28279
|
+
function pacer(sliceMs) {
|
|
28280
|
+
let since = performance.now();
|
|
28281
|
+
return () => {
|
|
28282
|
+
if (performance.now() - since < sliceMs) return void 0;
|
|
28283
|
+
return yieldThread().then(() => {
|
|
28284
|
+
since = performance.now();
|
|
28285
|
+
});
|
|
28286
|
+
};
|
|
28287
|
+
}
|
|
28288
|
+
/** `sha256Paced` hashes up to this much at once: under a millisecond of the thread. */
|
|
28289
|
+
var HASH_AT_ONCE = 65536;
|
|
28290
|
+
/** Without `crypto.subtle`, `sha256Paced` hashes this much (a few ms), then lets the thread go. */
|
|
28291
|
+
var HASH_SLICE = 262144;
|
|
28292
|
+
/** `readAppBundleAsync` hashes files for this long at most before it lets the thread go, however small each file. */
|
|
28293
|
+
var HASH_SLICE_MS = 5;
|
|
28294
|
+
/**
|
|
28295
|
+
* SHA-256 of bytes that may be a whole bundle, for a client whose engine shares its page's thread: `sha256` in
|
|
28296
|
+
* 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
|
|
28297
|
+
* input for 200 ms. With `crypto.subtle` the bytes are hashed off the thread; without it (a page not served over
|
|
28298
|
+
* HTTPS), a slice at a time with a pause between slices. The bytes must not change until it answers.
|
|
28299
|
+
*/
|
|
28300
|
+
async function sha256Paced(bytes) {
|
|
28301
|
+
if (bytes.length <= HASH_AT_ONCE) return sha256(bytes);
|
|
28302
|
+
const subtle = globalThis.crypto?.subtle;
|
|
28303
|
+
const away = subtle ? await subtle.digest("SHA-256", bytes).catch(() => null) : null;
|
|
28304
|
+
if (away) return new Uint8Array(away);
|
|
28305
|
+
const hash = sha256.create();
|
|
28306
|
+
for (let at = 0; at < bytes.length; at += HASH_SLICE) {
|
|
28307
|
+
if (at) await yieldThread();
|
|
28308
|
+
hash.update(bytes.subarray(at, at + HASH_SLICE));
|
|
28309
|
+
}
|
|
28310
|
+
return hash.digest();
|
|
28311
|
+
}
|
|
28312
|
+
function readAppBundleHead(bytes) {
|
|
29786
28313
|
if (bytes.length > APP_BUNDLE_LIMITS.bundleBytes) return {
|
|
29787
28314
|
ok: false,
|
|
29788
28315
|
reason: "too-large"
|
|
@@ -29841,17 +28368,18 @@ function readAppBundle(bytes) {
|
|
|
29841
28368
|
};
|
|
29842
28369
|
const signed = verifyAppSignature(APP_PREFIXES.app, manifestBytes, statement.signature, manifest.publisher);
|
|
29843
28370
|
if (!signed.ok) return signed;
|
|
29844
|
-
|
|
29845
|
-
|
|
29846
|
-
|
|
29847
|
-
|
|
29848
|
-
|
|
29849
|
-
|
|
29850
|
-
|
|
29851
|
-
|
|
29852
|
-
|
|
29853
|
-
|
|
29854
|
-
|
|
28371
|
+
return {
|
|
28372
|
+
ok: true,
|
|
28373
|
+
head: {
|
|
28374
|
+
manifest,
|
|
28375
|
+
manifestBytes,
|
|
28376
|
+
signature: statement.signature,
|
|
28377
|
+
filesAt: at
|
|
28378
|
+
}
|
|
28379
|
+
};
|
|
28380
|
+
}
|
|
28381
|
+
/** The last check, once every file's hash held: the icon. */
|
|
28382
|
+
function closeAppBundle({ manifest, manifestBytes, signature }, files) {
|
|
29855
28383
|
const icon = files.get(APP_ICON_PATH);
|
|
29856
28384
|
if (icon) {
|
|
29857
28385
|
const size = imageSize(icon);
|
|
@@ -29872,11 +28400,54 @@ function readAppBundle(bytes) {
|
|
|
29872
28400
|
manifest,
|
|
29873
28401
|
manifestBytes,
|
|
29874
28402
|
digest: appDigest(manifestBytes),
|
|
29875
|
-
signature
|
|
28403
|
+
signature,
|
|
29876
28404
|
files
|
|
29877
28405
|
}
|
|
29878
28406
|
};
|
|
29879
28407
|
}
|
|
28408
|
+
/** Reads and checks a bundle (the order of the checks is part of the format: see above). */
|
|
28409
|
+
function readAppBundle(bytes) {
|
|
28410
|
+
const read = readAppBundleHead(bytes);
|
|
28411
|
+
if (!read.ok) return read;
|
|
28412
|
+
const files = /* @__PURE__ */ new Map();
|
|
28413
|
+
let at = read.head.filesAt;
|
|
28414
|
+
for (const f of read.head.manifest.files) {
|
|
28415
|
+
const content = bytes.subarray(at, at + f.size);
|
|
28416
|
+
at += f.size;
|
|
28417
|
+
if (toBase64Url(sha256(content)) !== f.sha256) return {
|
|
28418
|
+
ok: false,
|
|
28419
|
+
reason: "file-hash",
|
|
28420
|
+
detail: f.path
|
|
28421
|
+
};
|
|
28422
|
+
files.set(f.path, content);
|
|
28423
|
+
}
|
|
28424
|
+
return closeAppBundle(read.head, files);
|
|
28425
|
+
}
|
|
28426
|
+
/**
|
|
28427
|
+
* `readAppBundle` for a client whose engine shares its page's thread: the same checks in the same order, with the same
|
|
28428
|
+
* refusals, and each file hashed by `sha256Paced`, one after the other, with the thread let go between files every
|
|
28429
|
+
* `HASH_SLICE_MS` (64 files of 64 KiB, each hashed at once, held it for 50 ms). The bytes must not change until it
|
|
28430
|
+
* answers.
|
|
28431
|
+
*/
|
|
28432
|
+
async function readAppBundleAsync(bytes) {
|
|
28433
|
+
const read = readAppBundleHead(bytes);
|
|
28434
|
+
if (!read.ok) return read;
|
|
28435
|
+
const files = /* @__PURE__ */ new Map();
|
|
28436
|
+
const pause = pacer(HASH_SLICE_MS);
|
|
28437
|
+
let at = read.head.filesAt;
|
|
28438
|
+
for (const f of read.head.manifest.files) {
|
|
28439
|
+
const content = bytes.subarray(at, at + f.size);
|
|
28440
|
+
at += f.size;
|
|
28441
|
+
if (toBase64Url(await sha256Paced(content)) !== f.sha256) return {
|
|
28442
|
+
ok: false,
|
|
28443
|
+
reason: "file-hash",
|
|
28444
|
+
detail: f.path
|
|
28445
|
+
};
|
|
28446
|
+
files.set(f.path, content);
|
|
28447
|
+
await pause();
|
|
28448
|
+
}
|
|
28449
|
+
return closeAppBundle(read.head, files);
|
|
28450
|
+
}
|
|
29880
28451
|
/** Puts a bundle's parts together, checking nothing: for builders, and for tests that need a broken bundle. */
|
|
29881
28452
|
function assembleAppBundle(manifestBytes, statementBytes, files) {
|
|
29882
28453
|
const total = MAGIC.length + 8 + manifestBytes.length + statementBytes.length + files.reduce((s, f) => s + f.length, 0);
|
|
@@ -30238,6 +28809,6 @@ function appStoreDecision(held, read) {
|
|
|
30238
28809
|
return read.digest === held.digest ? "same" : "equivocation";
|
|
30239
28810
|
}
|
|
30240
28811
|
//#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 };
|
|
28812
|
+
export { COMMUNITY_EDIT_FRAME as $, HANDOFF_HELLO as $a, EDIT_RATE_WINDOW_MS as $i, SIGNAL_REPLY_MS as $n, readEnrollGrant as $o, STATUS_CARD_CAPABILITY as $r, parseRelayPayload as $s, didDhtFromPublicKey as $t, CommunitySession as A, RELAY_POLL_INTERVALS as Aa, IdentityCheckUnavailable as Ai, pubkyProofPath as An, readHandoffHello as Ao, typingActivity as Ar, nextTurnPosition as As, verifyAtprotoRecordProof as At, knockRecords as B, callRtcConfig as Ba, PeerProofs as Bi, isFederationId as Bn, signTombstone as Bo, deviceWakeRequest as Br, RelayTransport as Bs, didWebDocumentUrl as Bt, sha256Paced as C, rosterHas as Ca, CapsExchange as Ci, PUBKY_PROOF_MAX_BYTES as Cn, handoffStreamKey as Co, FILE_LIMITS as Cr, TURN_RAISE_READ_TIMEOUT_MS as Cs, atprotoProofRkey as Ct, openGroupTyping as D, AWAITING_PEER_MS as Da, IDENTITY_MAX_EVIDENCE as Di, normalizePubkyKey as Dn, readHandoffCancel as Do, TYPING_REFRESH_MS as Dr, TURN_LAST_SEQUENCE as Ds, parseAtprotoDidDocument as Dt, groupTypingEpoch as E, DirectPathWatch as Ea, dialDescriptors as Ei, newPubkyProofFolder as En, readHandoffAck as Eo, TYPING_KINDS as Er, turnPutSummary as Es, normalizeAtprotoHandle as Et, KNOCK_TTL_MS as F, webLocalFetch as Fa, identityStatement as Fi, decodeBolt11 as Fn, SET_ACK as Fo, pinIsNewer as Fr, turnKeys as Fs, checkDidJws as Ft, groupWakeFrame as G, emptyLinkRecords as Ga, irohDescriptor as Gi, LIVENESS_PING_MS as Gn, ENROLL_PROOF as Go, checkPushEndpoint as Gr, budgetRetryMs as Gs, normalizeDid as Gt, readKnocks as H, extractParamsFromSdp as Ha, validExternalKey as Hi, parseFedimintRequestPayload as Hn, ENROLL_DONE as Ho, parseWakeFrame as Hr, normalizeRelayUrl as Hs, didWebFileNames as Ht, decodeGroupEntryLink as I, CALL_SIGNAL_MAX_AGE_MS as Ia, newIdentityBinding as Ii, ENDPOINT as In, SET_UPDATE as Io, pinNumberHolds as Ir, turnName as Is, checkDidSignature as It, GROUP_LIMITS as J, serviceIdFromName as Ja, DHT_MESSAGE_TTL as Ji, engineText as Jn, enrollDigits as Jo, chatAppId as Jr, PacketTooLargeError as Js, parseDidSignaturePaste as Jt, groupWakes as K, HTTP_SERVICE_PROTO as Ka, irohRelayUrl as Ki, pairedMessageFrame as Kn, enrollCancelFrame as Ko, vapidKeysMatch as Kr, isDiscoveryBudgetError as Ks, parseDid as Kt, encodeGroupEntryLink as L, RTC_CONFIG as La, receivedIdentityStatus as Li, cashuRequestPayload as Ln, readSetUpdate as Lo, readPin as Lr, turnStatedVerifier as Ls, didGhostlyServiceEntry as Lt, communityHasFrame as M, GhostlyHttpError as Ma, boundVerifiedIdentity as Mi, pubkyHomeserverOf as Mn, readHandoffTurnFrame as Mo, PIN_LIMITS as Mr, signTurnPacket as Ms, DID_FILE_MAX_BYTES as Mt, decodeCommunityLink as N, formatLocalTarget as Na, emptyIdentityLedger as Ni, requestLnurlInvoice as Nn, readHandoffVerified as No, mayPin as Nr, signTurnRelease as Ns, DID_JWS_MAX_LENGTH as Nt, sealGroupTyping as O, DHT_POLL_INTERVALS as Oa, IDENTITY_MAX_VALIDITY as Oi, pubkyProofCapability as On, readHandoffDone as Oo, TypingSender as Or, TURN_MAX as Os, shortAtprotoDid as Ot, encodeCommunityLink as P, parseLocalTarget as Pa, identityRevocationValue as Pi, resolveLightningDestination as Pn, sealHandoffPiece as Po, nextPinNumber as Pr, turnInstance as Ps, DidError as Pt, frameBytes as Q, HANDOFF_HAVE as Qa, EDIT_BUFFER_MS as Qi, SIGNAL_HOLD_MS as Qn, enrollProofFrame as Qo, EDIT_CAPABILITY as Qr, openRelayPayload as Qs, didDhtDocument as Qt, entryParams as R, answersOffer as Ra, revokesIdentity as Ri, fedimintRequestPayload as Rn, setAckFrame as Ro, WAKE_CALL_INTERVAL_MS as Rr, turnVerifierMark as Rs, didJwsAlg as Rt, readAppBundleAsync as S, commitHash as Sa, wellKnownUrl as Si, isBitcoinAddress as Sn, handoffRetryAfter as So, ChatFiles as Sr, TURN_PUT_WINDOW_MS as Ss, atprotoProofRecord as St, GroupTypingBoard as T, edgeParams as Ta, capsDescriptors as Ti, isPubkyProofFolder as Tn, newHandoffId as To, transferStalled as Tr, classifyTurnRead as Ts, isAtprotoDid as Tt, GROUP_WAKE_FRAME as U, parseCallSignal as Ua, TRANSPORTS as Ui, GhostLink as Un, ENROLL_GRANT as Uo, relayRequest as Ur, setLinkTraceSink as Us, didWebFileUrl as Ut, mergeKnocks as V, callSignalHeardAt as Va, emptyProofLedger as Vi, parseCashuRequestPayload as Vn, verifySetUpdate as Vo, newWakeToken as Vr, currentRelays as Vs, didWebFile as Vt, GroupWakeLimiter as W, signalHasVideo as Wa, automaticTransport as Wi, INVITE_TAKEN as Wn, ENROLL_HELLO as Wo, wakeRequest as Wr, traceLink as Ws, didWebHost as Wt, GROUP_READ_NOTE as X, HANDOFF_CANCEL as Xa, DHT_FILE_CAPABILITY as Xi, GROUP_SIGNAL_FRAME as Xn, enrollGrantFrame as Xo, APPS_CAPABILITY as Xr, createRelayPayload as Xs, staticDidDocument as Xt, GROUP_REACTED_FRAME as Y, HANDOFF_ACK as Ya, peekDhtMailbox as Yi, CarriedTransport as Yn, enrollDoneFrame as Yo, isAppVersion as Yr, RELAY_PAYLOAD_MAX_BYTES as Ys, signingKeys as Yt, GroupSession as Z, HANDOFF_DONE as Za, dhtFileId as Zi, SIGNAL_FRESH_MS as Zn, enrollHelloFrame as Zo, BUTTONS_CAPABILITY as Zr, newerPacket as Zs, strictBase64Url as Zt, assembleAppBundle as _, readReply as _a, normalizeDomain as _i, walletNetworkOf as _n, handoffHelloFrame as _o, rankHubs as _r, deviceLinkPairing as _s, ATPROTO_DECLARED_SCOPE as _t, planAppUpdate as a, COMMUNITY_REACTION_FRAME as aa, MAX_AVATAR_LENGTH as ac, HoldRefusedError as ai, EVM_TEST_CHAINS as an, HANDOFF_VERIFIED as ao, pickMeshHubs as ar, OwnInviteError as as, groupName as at, pacer as b, MAX_GROUP_CHAIN as ba, recordsName as bi, sparkAddressKind as bn, handoffPakeFrame as bo, readLobby as br, seedSigner as bs, ATPROTO_PROOF_SCOPE as bt, readAppStore as c, ReactionWindow as ca, readManifest as ci, SEPOLIA_TEST_USDT as cn, handoffAttemptFailed as co, beaconRecords as cr, encodeInviteCode as cs, SshSigError as ct, APP_BUNDLE_LIMITS as d, reactionEmoji as da, dnsProofName as di, USDT_PUBLIC_RPC as dn, handoffCancelFrame as do, lobbyKeys as dr, DEVICES_CAPABILITY as ds, sameSshKey as dt, RateWindow as ea, createIdentity as ec, UPGRADE_CAPABILITY as ei, encodeDidDhtDeactivated as en, HANDOFF_OFFER as eo, groupSignalFrame as er, readEnrollHello as es, GROUP_EDIT_FRAME as et, APP_BUNDLE_MAGIC as f, reactionIsNewer as fa, domainTxtRecord as fi, WALLET_NETWORKS as fn, handoffContext as fo, lobbyRecords as fr, ENROLL_CAPABILITY as fs, sshSignCommand as ft, appViewOf as g, pairedReplyAuthor as ga, nip05RootKey as gi, validatePaymentTarget as gn, handoffEphemeral as go, pickHubs as gr, deviceKeyZ32 as gs, decodeAnyBitcoinAddress as gt, appDigest as h, wireReaction as ha, isPublicAddress as hi, formatPaymentAmount as hn, handoffDigest as ho, newerHead as hr, deviceEchoNonce as hs, BitcoinAddressError as ht, checkAppBeforeRun as i, readForwarded as ia, verify as ic, HoldKeys as ii, ETHEREUM_USDT as in, HANDOFF_TIMINGS as io, meshHubs as ir, readDeviceInvite as is, mentionsMember as it, GROUP_READ_NOTE_COMMUNITY as j, presenceSeenAt as ja, IdentityExchange as ji, homeserverWebEndpoint as jn, readHandoffPake as jo, GROUP_PIN_FRAME as jr, readTurnPacket as js, DID_DOCUMENT_MAX_BYTES as jt, COMMUNITY_LIMITS as k, EXPECT_PEER_MS as ka, IDENTITY_REVOCATION_LABEL as ki, pubkyProofFile as kn, readHandoffHave as ko, sanitizeTypingStatus as kr, firstDeviceSetSecret as ks, AtprotoProofError as kt, readAppStoreSteps as l, nextReactionNumber as la, DOMAIN_FILE_MAX_BYTES as li, SEPOLIA_TEST_USDT_FAUCET as ln, handoffAttemptSucceeded as lo, doorHubs as lr, inviteLink as ls, parseSshPublicKey as lt, APP_PERMISSIONS as m, validReactionNumber as ma, encodeDnsQuery as mi, assertWholeSats as mn, handoffDataFrame as mo, mergeLobby as mr, deviceEchoFrame as ms, verifyBitcoinMessage as mt, appStoreDecision as n, parseLinkPreview as na, identityFromSeedB64 as nc, HOLD_DATED_BACK_MS as ni, resolveDidDht as nn, HANDOFF_PIECE_BYTES as no, MESH_HUBS as nr, createDeviceInvite as ns, carryMentions as nt, APP_STORE_LIMITS as o, GROUP_REACTION_FRAME as oa, sanitizeAvatar as oc, isHoldUrl as oi, ONCHAIN_PROVIDER as on, filePartName as oo, COMMUNITY_TOPOLOGY as or, createChatInvite as os, SSHSIG_MAX_ARMOR as ot, APP_ICON_PATH as p, readReaction as pa, domainWellKnownFile as pi, assertTokenUnits as pn, handoffContextProof as po, mergeBeacon as pr, HANDOFF_CAPABILITY as ps, verifySshSignature as pt, parseGroupWakeFrame as q, LEGACY_SERVICES as qa, DHT_FILE_TOO_LARGE as qi, engineError as qn, enrollCancelReason as qo, APP_DATA_MAX_BYTES as qr, withRequestOptions as qs, parseDidDocument as qt, appUpdateDecision as r, forwardedAgain as ra, publicKeyFromZ32 as rc, HOLD_LIMITS as ri, signDidDhtPacket as rn, HANDOFF_REQUEST as ro, mayBeHub as rr, deviceInviteJoinerParams as rs, parseCommunityEdit as rt, checkAppStoreIndex as s, REACTION_LIMITS as sa, newMailbox as si, PaymentPreflightError as sn, handoffAttemptAllowed as so, beaconKeys as sr, decodeInviteCode as ss, SSHSIG_NAMESPACE as st, appRevocationFor as t, dhtEditId as ta, identityFromSeed as tc, HOLD_CAPABILITY as ti, encodeDidDhtPacket as tn, HANDOFF_PAKE as to, readGroupSignal as tr, verifyEnrollProof as ts, GROUP_EDIT_TEXT_BYTES as tt, signAppStore as u, queueReaction as ua, decodeDnsResponse as ui, TEST_USDT_FAUCET_AMOUNT as un, handoffBusyFrame as uo, freshHubs as ur, inviteOwnership as us, parseSshSignature as ut, checkAppManifest as v, replySnippet as va, recordsFromTxt as vi, SPARK_NETWORKS as vn, handoffManifestFrames as vo, readBeacon as vr, devicePingFrame as vs, ATPROTO_FULL_SCOPE as vt, GROUP_TYPING_FRAME as w, verifyCommitSignature as wa, DHT_TEXT_CAPABILITY as wi, isPubkyKey as wn, handoffVersions as wo, transferEnded as wr, TURN_SETTLE_MS as ws, checkAtprotoProofRecord as wt, readAppBundle as x, MEMBER_KEY as xa, txtValue as xi, sparkInvoiceDetails as xn, handoffReleaseFrame as xo, shouldBeHub as xr, webCryptoSigner as xs, atprotoDidMethod as xt, isAppUrl as y, GROUP_MEMBER_CAP as ya, recordsFromWellKnown as yi, SPARK_PROVIDER as yn, handoffOfferFrame as yo, readBeaconHead as yr, newDeviceSetSecret as ys, ATPROTO_PROOF_COLLECTION as yt, knockIdentity as z, buildSdpFromSignal as za, lastSharedWithMe as zi, findEndpoint as zn, setUpdateFrame as zo, WakeLimiter as zr, DEFAULT_RELAYS as zs, didSignedMessages as zt };
|
|
30242
28813
|
|
|
30243
|
-
//# sourceMappingURL=src-
|
|
28814
|
+
//# sourceMappingURL=src-DhfGgAUu.js.map
|