@ghostlytools/cli 1.3.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -3
- package/dist/assets/{api-CrxVxtY1.js → api-Dq2QXQDk.js} +141 -40
- package/dist/assets/api-Dq2QXQDk.js.map +1 -0
- package/dist/assets/{api-BiaX6l6f.js → api-TiB5QSG9.js} +2 -2
- package/dist/assets/{apiKit-CajiDxdK.js → apiKit-CA2x5GJF.js} +7 -3
- package/dist/assets/apiKit-CA2x5GJF.js.map +1 -0
- package/dist/assets/{apps-CbfiCSIw.js → apps-BxV9i_Y4.js} +3 -4
- package/dist/assets/{apps-CbfiCSIw.js.map → apps-BxV9i_Y4.js.map} +1 -1
- package/dist/assets/{apps-t23gkc28.js → apps-C-T8jeji.js} +1 -1
- package/dist/assets/{arkade-D4BVJFEQ.js → arkade-C_qDmHnm.js} +2 -2
- package/dist/assets/{arkade-D4BVJFEQ.js.map → arkade-C_qDmHnm.js.map} +1 -1
- package/dist/assets/{backup-Cg52WeQi.js → backup-Bugnn3Ue.js} +1 -1
- package/dist/assets/{backup-BAMi2lAq.js → backup-CnMN2oUK.js} +8 -8
- package/dist/assets/backup-CnMN2oUK.js.map +1 -0
- package/dist/assets/bark_ffi_wasm_bg-EY2uc0Bx.js.map +1 -1
- package/dist/assets/bitcoind-CZchoh43.js +349 -0
- package/dist/assets/bitcoind-CZchoh43.js.map +1 -0
- package/dist/assets/bitcoindProvider-DGw3xBSa.js +97 -0
- package/dist/assets/bitcoindProvider-DGw3xBSa.js.map +1 -0
- package/dist/assets/bitcoindevkit_bg-B-iLWrmX.js.map +1 -1
- package/dist/assets/buttons-Bj7Vf7Pn.js +2 -0
- package/dist/assets/{buttons-CX1pKB0j.js → buttons-CvNBzh2S.js} +4 -5
- package/dist/assets/{buttons-CX1pKB0j.js.map → buttons-CvNBzh2S.js.map} +1 -1
- package/dist/assets/{endpoint-Dn7H8qE1.js → endpoint-BOTFMpfS.js} +44 -12
- package/dist/assets/endpoint-BOTFMpfS.js.map +1 -0
- package/dist/assets/{envelope-BvU0fLEt.js → envelope-WjingYSc.js} +2 -2
- package/dist/assets/{envelope-BvU0fLEt.js.map → envelope-WjingYSc.js.map} +1 -1
- package/dist/assets/{errors-my6VJR_0.js → errors-DO5z6h_l.js} +29 -1
- package/dist/assets/errors-DO5z6h_l.js.map +1 -0
- package/dist/assets/groupFiles-IEUW3DQz.js +3202 -0
- package/dist/assets/groupFiles-IEUW3DQz.js.map +1 -0
- package/dist/assets/{host-Cmd205Dc.js → host-CWt-jtqz.js} +41 -16
- package/dist/assets/host-CWt-jtqz.js.map +1 -0
- package/dist/assets/{hyperdhtRelay-CEmBpT0Q.js → hyperdhtRelay-CPYwSLLS.js} +2 -3
- package/dist/assets/{hyperdhtRelay-CEmBpT0Q.js.map → hyperdhtRelay-CPYwSLLS.js.map} +1 -1
- package/dist/assets/{irohWeb--YyEA-hX.js → irohWeb-4oChIOIp.js} +2 -3
- package/dist/assets/{irohWeb--YyEA-hX.js.map → irohWeb-4oChIOIp.js.map} +1 -1
- package/dist/assets/{main-BzNN6bkx.js → main-DbqcdShN.js} +142 -35
- package/dist/assets/main-DbqcdShN.js.map +1 -0
- package/dist/assets/{mainline-D5D5C-jT.js → mainline-ZkiOnevs.js} +2 -2
- package/dist/assets/{mainline-D5D5C-jT.js.map → mainline-ZkiOnevs.js.map} +1 -1
- package/dist/assets/{networkReason-DxZWHw6v.js → networkReason-CetKkTPo.js} +2 -2
- package/dist/assets/{networkReason-DxZWHw6v.js.map → networkReason-CetKkTPo.js.map} +1 -1
- package/dist/assets/nwc-CgkqyvsE.js +612 -0
- package/dist/assets/nwc-CgkqyvsE.js.map +1 -0
- package/dist/assets/nwcProvider-4eVz_8HP.js +77 -0
- package/dist/assets/nwcProvider-4eVz_8HP.js.map +1 -0
- package/dist/assets/{profiles-B6F4oa9K.js → profiles-BHc9pEYg.js} +2 -2
- package/dist/assets/{profiles-B6F4oa9K.js.map → profiles-BHc9pEYg.js.map} +1 -1
- package/dist/assets/registry-DlAKO0Fy.js +3793 -0
- package/dist/assets/registry-DlAKO0Fy.js.map +1 -0
- package/dist/assets/registry-OBJBnHVQ.js +2 -0
- package/dist/assets/resolve-CouHhCpJ.js +591 -0
- package/dist/assets/resolve-CouHhCpJ.js.map +1 -0
- package/dist/assets/{server-CBCEYKMN.js → server-Ss0gJ-nw.js} +184 -1181
- package/dist/assets/server-Ss0gJ-nw.js.map +1 -0
- package/dist/assets/{src-DhfGgAUu.js → src-DDzSERyc.js} +2249 -72
- package/dist/assets/src-DDzSERyc.js.map +1 -0
- package/dist/assets/{turnRefs-ChEfgbSX.js → turnRefs-B87dtrHM.js} +2 -2
- package/dist/assets/{turnRefs-ChEfgbSX.js.map → turnRefs-B87dtrHM.js.map} +1 -1
- package/dist/assets/types-C4_KafL_.js +122 -0
- package/dist/assets/types-C4_KafL_.js.map +1 -0
- package/dist/assets/{usdt-C1Xao2xH.js → usdt-Fj8CQO2z.js} +3 -3
- package/dist/assets/{usdt-C1Xao2xH.js.map → usdt-Fj8CQO2z.js.map} +1 -1
- package/dist/ghostly.mjs +1 -1
- package/package.json +1 -1
- package/dist/assets/api-CrxVxtY1.js.map +0 -1
- package/dist/assets/apiKit-CajiDxdK.js.map +0 -1
- package/dist/assets/backup-BAMi2lAq.js.map +0 -1
- package/dist/assets/buttons-CWl2yNJu.js +0 -2
- package/dist/assets/endpoint-Dn7H8qE1.js.map +0 -1
- package/dist/assets/errors-my6VJR_0.js.map +0 -1
- package/dist/assets/groupFiles-BNTSDaoq.js +0 -7685
- package/dist/assets/groupFiles-BNTSDaoq.js.map +0 -1
- package/dist/assets/groupFiles-DBbjOLuD.js +0 -2088
- package/dist/assets/groupFiles-DBbjOLuD.js.map +0 -1
- package/dist/assets/host-Cmd205Dc.js.map +0 -1
- package/dist/assets/main-BzNN6bkx.js.map +0 -1
- package/dist/assets/server-CBCEYKMN.js.map +0 -1
- package/dist/assets/src-DhfGgAUu.js.map +0 -1
|
@@ -1,4 +1,3 @@
|
|
|
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";
|
|
2
1
|
import { xsalsa20poly1305 } from "@noble/ciphers/salsa.js";
|
|
3
2
|
import { ed25519, x25519 } from "@noble/curves/ed25519.js";
|
|
4
3
|
import { hkdf } from "@noble/hashes/hkdf.js";
|
|
@@ -13,6 +12,165 @@ import { p256, p384, p521 } from "@noble/curves/nist.js";
|
|
|
13
12
|
import { gcm } from "@noble/ciphers/aes.js";
|
|
14
13
|
import { ripemd160 } from "@noble/hashes/legacy.js";
|
|
15
14
|
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
|
|
16
174
|
//#region ../core/src/avatar.ts
|
|
17
175
|
/**
|
|
18
176
|
* A profile picture as it travels between paired peers: a small square JPEG in a data URL. The picture
|
|
@@ -23,7 +181,7 @@ import { bytesToNumberBE } from "@noble/curves/utils.js";
|
|
|
23
181
|
var MAX_AVATAR_LENGTH = 48e3;
|
|
24
182
|
var PREFIX$1 = "data:image/jpeg;base64,";
|
|
25
183
|
/** Width and height from a JPEG's frame header, or undefined when there is none before the image data. */
|
|
26
|
-
function jpegSize(bytes) {
|
|
184
|
+
function jpegSize$1(bytes) {
|
|
27
185
|
if (bytes.length < 4 || bytes[0] !== 255 || bytes[1] !== 216) return;
|
|
28
186
|
let i = 2;
|
|
29
187
|
while (i + 4 <= bytes.length) {
|
|
@@ -58,7 +216,7 @@ function sanitizeAvatar(value) {
|
|
|
58
216
|
}
|
|
59
217
|
const bytes = new Uint8Array(binary.length);
|
|
60
218
|
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
61
|
-
const size = jpegSize(bytes);
|
|
219
|
+
const size = jpegSize$1(bytes);
|
|
62
220
|
if (!size || size.width < 1 || size.height < 1 || size.width > 512 || size.height > 512) return;
|
|
63
221
|
return value;
|
|
64
222
|
}
|
|
@@ -76,8 +234,7 @@ function generateEncryptionKey() {
|
|
|
76
234
|
function encrypt(plaintext, key) {
|
|
77
235
|
if (key.length !== KEY_LENGTH) throw new Error("Invalid key length: expected 32 bytes");
|
|
78
236
|
const nonce = randomBytes(NONCE_LENGTH$3);
|
|
79
|
-
|
|
80
|
-
return toBase64(concatBytes$1(nonce, box));
|
|
237
|
+
return toBase64(concatBytes$1(nonce, xsalsa20poly1305(key, nonce).encrypt(utf8Encode(plaintext))));
|
|
81
238
|
}
|
|
82
239
|
/** The length of what `encrypt` returns for a plaintext of this many UTF-8 bytes: base64 of nonce, box and 16-byte tag. */
|
|
83
240
|
function sealedLength(plaintextBytes) {
|
|
@@ -561,9 +718,13 @@ var RelayBreaker = class {
|
|
|
561
718
|
this.circuits.delete(relay);
|
|
562
719
|
if (recovered) this.options.onRecover?.(relay);
|
|
563
720
|
}
|
|
564
|
-
/**
|
|
721
|
+
/**
|
|
722
|
+
* The relay failed a request; a probe that fails trips it again at once. While it is left alone and no probe is out,
|
|
723
|
+
* a failure is a request sent before it tripped (a start burst): already counted in, it trips nothing again.
|
|
724
|
+
*/
|
|
565
725
|
failure(relay, kind, reason) {
|
|
566
726
|
const circuit = this.circuit(relay);
|
|
727
|
+
if (!circuit.probing && circuit.openUntil > this.now()) return;
|
|
567
728
|
circuit.failures++;
|
|
568
729
|
circuit.kind = kind;
|
|
569
730
|
circuit.reason = reason;
|
|
@@ -2989,6 +3150,12 @@ var OwnInviteError = class extends Error {
|
|
|
2989
3150
|
this.name = "OwnInviteError";
|
|
2990
3151
|
}
|
|
2991
3152
|
};
|
|
3153
|
+
/** The refusal an error (or an error's message, as an RPC hands it over) carries, or null when it is another error. */
|
|
3154
|
+
function ownInviteRefusal(error) {
|
|
3155
|
+
if (error instanceof OwnInviteError) return { linkId: error.linkId };
|
|
3156
|
+
const match = (error instanceof Error ? error.message : typeof error === "string" ? error : "").match(/^own-invite(?: (\S+))?: /);
|
|
3157
|
+
return match ? { linkId: match[1] ?? null } : null;
|
|
3158
|
+
}
|
|
2992
3159
|
/** The `ghostly1z…` code of a device invite. */
|
|
2993
3160
|
function encodeDeviceInvite(invite) {
|
|
2994
3161
|
const fields = [
|
|
@@ -3496,9 +3663,9 @@ var HANDOFF_CANCEL_REASONS = [
|
|
|
3496
3663
|
"stalled"
|
|
3497
3664
|
];
|
|
3498
3665
|
var KEY$3 = /^[A-Za-z0-9_-]{43}$/;
|
|
3499
|
-
var DIGEST$
|
|
3666
|
+
var DIGEST$2 = KEY$3;
|
|
3500
3667
|
var SIGNATURE = /^[A-Za-z0-9_-]{86}$/;
|
|
3501
|
-
var ID$
|
|
3668
|
+
var ID$5 = /^[A-Za-z0-9_-]{22}$/;
|
|
3502
3669
|
var B64$4 = /^[A-Za-z0-9_-]*$/;
|
|
3503
3670
|
var HEX = /^[0-9a-f]{64}$/;
|
|
3504
3671
|
var VERSION_TEXT = /^[0-9A-Za-z.+_-]{1,40}$/;
|
|
@@ -3512,9 +3679,9 @@ var isHandoffPartName = (name) => typeof name === "string" && PART.test(name);
|
|
|
3512
3679
|
/** The part a file is: `file/<its SHA-256, base64url>`. */
|
|
3513
3680
|
var filePartName = (sha256B64) => `file/${sha256B64}`;
|
|
3514
3681
|
/** The digest a file part names, or null for any other part. */
|
|
3515
|
-
var filePartDigest = (name) => name.startsWith("file/") && DIGEST$
|
|
3682
|
+
var filePartDigest = (name) => name.startsWith("file/") && DIGEST$2.test(name.slice(5)) ? name.slice(5) : null;
|
|
3516
3683
|
function isPart(value) {
|
|
3517
|
-
return Array.isArray(value) && value.length === 3 && isHandoffPartName(value[0]) && isCount$1(value[1], MAX_PART) && isText$2(value[2], DIGEST$
|
|
3684
|
+
return Array.isArray(value) && value.length === 3 && isHandoffPartName(value[0]) && isCount$1(value[1], MAX_PART) && isText$2(value[2], DIGEST$2) && (filePartDigest(value[0]) === null || filePartDigest(value[0]) === value[2]);
|
|
3518
3685
|
}
|
|
3519
3686
|
/** Names compared as UTF-8 byte strings, as `H` sorts them. */
|
|
3520
3687
|
function compareNames(a, b) {
|
|
@@ -3636,7 +3803,7 @@ function readHandoffHello(frame) {
|
|
|
3636
3803
|
if (typeof frame.room !== "number" || !Number.isSafeInteger(frame.room) || frame.room < -1) return null;
|
|
3637
3804
|
if (typeof frame.metered !== "boolean") return null;
|
|
3638
3805
|
if (frame.later !== void 0 && !isCount$1(frame.later)) return null;
|
|
3639
|
-
if (frame.id !== void 0 && !isText$2(frame.id, ID$
|
|
3806
|
+
if (frame.id !== void 0 && !isText$2(frame.id, ID$5)) return null;
|
|
3640
3807
|
const kind = DEVICE_KINDS.includes(frame.kind) ? frame.kind : "web";
|
|
3641
3808
|
return {
|
|
3642
3809
|
v: 1,
|
|
@@ -3662,7 +3829,7 @@ var handoffOfferFrame = (turn, id, bytes) => ({
|
|
|
3662
3829
|
});
|
|
3663
3830
|
function readHandoffTurnFrame(frame) {
|
|
3664
3831
|
if (frame.t !== "handoff-request" && frame.t !== "handoff-offer") return null;
|
|
3665
|
-
if (!isCount$1(frame.turn, TURN_MAX) || !isText$2(frame.id, ID$
|
|
3832
|
+
if (!isCount$1(frame.turn, TURN_MAX) || !isText$2(frame.id, ID$5)) return null;
|
|
3666
3833
|
if (frame.t === "handoff-offer") return isCount$1(frame.bytes) ? {
|
|
3667
3834
|
turn: frame.turn,
|
|
3668
3835
|
id: frame.id,
|
|
@@ -3705,7 +3872,7 @@ function readHandoffPake(frame) {
|
|
|
3705
3872
|
};
|
|
3706
3873
|
}
|
|
3707
3874
|
function readHandoffHave(frame) {
|
|
3708
|
-
if (frame.t !== "handoff-have" || !Array.isArray(frame.d) || frame.d.length > 300 || !frame.d.every((d) => isText$2(d, DIGEST$
|
|
3875
|
+
if (frame.t !== "handoff-have" || !Array.isArray(frame.d) || frame.d.length > 300 || !frame.d.every((d) => isText$2(d, DIGEST$2))) return null;
|
|
3709
3876
|
const p = {};
|
|
3710
3877
|
if (frame.p !== void 0) {
|
|
3711
3878
|
if (!frame.p || typeof frame.p !== "object" || Array.isArray(frame.p) || Object.keys(frame.p).length > 300) return null;
|
|
@@ -3761,7 +3928,7 @@ function readHandoffAck(frame) {
|
|
|
3761
3928
|
}
|
|
3762
3929
|
/** `H` of a `handoff-verified` that B's key signed for this turn address and turn, or null. */
|
|
3763
3930
|
function readHandoffVerified(frame, turnAddress, turn, takerKey) {
|
|
3764
|
-
if (frame.t !== "handoff-verified" || !isText$2(frame.h, DIGEST$
|
|
3931
|
+
if (frame.t !== "handoff-verified" || !isText$2(frame.h, DIGEST$2) || !isText$2(frame.s, SIGNATURE)) return null;
|
|
3765
3932
|
const h = fromBase64Url(frame.h);
|
|
3766
3933
|
try {
|
|
3767
3934
|
return verify(fromBase64Url(frame.s), handoffVerifiedMessage(turnAddress, turn, h), takerKey) ? h : null;
|
|
@@ -3829,7 +3996,7 @@ var handoffAttemptSucceeded = () => ({
|
|
|
3829
3996
|
/** Seconds until a locked-out device may try again (for `handoff-busy`). */
|
|
3830
3997
|
var handoffRetryAfter = (attempts, now) => attempts?.until !== void 0 && attempts.until > now ? Math.ceil((attempts.until - now) / 1e3) : 0;
|
|
3831
3998
|
var HTTP_SERVICE_PROTO = "ghostly-http/1";
|
|
3832
|
-
var MAX_NAME_LENGTH = 48;
|
|
3999
|
+
var MAX_NAME_LENGTH$1 = 48;
|
|
3833
4000
|
var MAX_META_ENTRIES = 8;
|
|
3834
4001
|
var MAX_META_LENGTH = 64;
|
|
3835
4002
|
var SERVICE_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,31}$/;
|
|
@@ -3910,7 +4077,7 @@ function servicesFromWire(wire) {
|
|
|
3910
4077
|
id: e.i,
|
|
3911
4078
|
type: e.t
|
|
3912
4079
|
};
|
|
3913
|
-
const name = cleanString(e.n, MAX_NAME_LENGTH);
|
|
4080
|
+
const name = cleanString(e.n, MAX_NAME_LENGTH$1);
|
|
3914
4081
|
const proto = cleanString(e.p, MAX_META_LENGTH);
|
|
3915
4082
|
if (name) ad.name = name;
|
|
3916
4083
|
if (proto) ad.proto = proto;
|
|
@@ -4756,6 +4923,325 @@ function buildDataSdp(signal) {
|
|
|
4756
4923
|
return lines.join("\r\n") + "\r\n";
|
|
4757
4924
|
}
|
|
4758
4925
|
//#endregion
|
|
4926
|
+
//#region ../core/src/frames.ts
|
|
4927
|
+
/**
|
|
4928
|
+
* Framing on the `ghostly/1` DataChannel (ordered, reliable).
|
|
4929
|
+
*
|
|
4930
|
+
* - Text messages are control frames: compact JSON with a `t` discriminator,
|
|
4931
|
+
* the same shape Ghostly already uses for `_msgs` and `_call`.
|
|
4932
|
+
* - Binary messages are body chunks that belong to a stream opened by a
|
|
4933
|
+
* control frame: `<kind u8><stream id u32 BE><flags u8><payload>`.
|
|
4934
|
+
*
|
|
4935
|
+
* Everything here handles data from the remote peer and must never trust it.
|
|
4936
|
+
*/
|
|
4937
|
+
var LIMITS = {
|
|
4938
|
+
/** Largest control frame accepted, headers included. */
|
|
4939
|
+
maxControlFrameBytes: 61440,
|
|
4940
|
+
/** Longest payment endpoint value (a Cashu token, an invoice) a payment frame may carry; a longer one is dropped. */
|
|
4941
|
+
maxPaymentEndpointChars: 32768,
|
|
4942
|
+
/** Largest DataChannel message sent. 16 KiB is safe across WebRTC stacks. */
|
|
4943
|
+
maxChunkMessageBytes: 16384,
|
|
4944
|
+
maxRequestBodyBytes: 8388608,
|
|
4945
|
+
maxResponseBodyBytes: 67108864,
|
|
4946
|
+
maxHeaderCount: 128,
|
|
4947
|
+
maxHeaderValueBytes: 16384,
|
|
4948
|
+
maxPathBytes: 8192,
|
|
4949
|
+
/** Requests a host serves at once for one peer. */
|
|
4950
|
+
maxConcurrentRequestsPerPeer: 32,
|
|
4951
|
+
/** Requests a client keeps in flight; the rest wait in a queue. */
|
|
4952
|
+
maxClientInFlight: 16,
|
|
4953
|
+
maxChatMessageBytes: 16384,
|
|
4954
|
+
maxFileBytes: 104857600,
|
|
4955
|
+
/** Files a peer may be sending us at once. */
|
|
4956
|
+
maxIncomingFilesPerPeer: 3,
|
|
4957
|
+
/** Received bytes kept for one peer, in flight included. Past it new files are refused. */
|
|
4958
|
+
maxStoredIncomingBytesPerPeer: 524288e3,
|
|
4959
|
+
requestTimeoutMs: 6e4,
|
|
4960
|
+
bodyIdleTimeoutMs: 3e4,
|
|
4961
|
+
/** Stop writing to the channel above this many buffered bytes. */
|
|
4962
|
+
sendHighWaterMark: 1048576,
|
|
4963
|
+
sendLowWaterMark: 262144
|
|
4964
|
+
};
|
|
4965
|
+
var CHUNK_KIND = {
|
|
4966
|
+
requestBody: 1,
|
|
4967
|
+
responseBody: 2,
|
|
4968
|
+
fileBody: 3
|
|
4969
|
+
};
|
|
4970
|
+
var CHUNK_HEADER_BYTES = 6;
|
|
4971
|
+
var MAX_CHUNK_PAYLOAD_BYTES = LIMITS.maxChunkMessageBytes - CHUNK_HEADER_BYTES;
|
|
4972
|
+
function encodeControl(frame) {
|
|
4973
|
+
const json = JSON.stringify(frame);
|
|
4974
|
+
if (utf8Encode(json).length > LIMITS.maxControlFrameBytes) throw new Error("Control frame too large");
|
|
4975
|
+
return json;
|
|
4976
|
+
}
|
|
4977
|
+
function isHeaderList(value) {
|
|
4978
|
+
if (!Array.isArray(value) || value.length > LIMITS.maxHeaderCount) return false;
|
|
4979
|
+
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);
|
|
4980
|
+
}
|
|
4981
|
+
function isStreamId(value) {
|
|
4982
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 4294967295;
|
|
4983
|
+
}
|
|
4984
|
+
function isPayId(value) {
|
|
4985
|
+
return typeof value === "string" && /^[A-Za-z0-9_-]{8,64}$/.test(value);
|
|
4986
|
+
}
|
|
4987
|
+
/** Decimal text, as in Paykit's Payment Amount: no floats on the wire. */
|
|
4988
|
+
function isAmount(value) {
|
|
4989
|
+
return typeof value === "string" && /^(0|[1-9]\d{0,15})(\.\d{1,8})?$/.test(value);
|
|
4990
|
+
}
|
|
4991
|
+
function isUnit(value) {
|
|
4992
|
+
return typeof value === "string" && /^[a-z0-9]{1,12}$/.test(value);
|
|
4993
|
+
}
|
|
4994
|
+
function isEndpoint(value) {
|
|
4995
|
+
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;
|
|
4996
|
+
}
|
|
4997
|
+
/** Returns null for anything malformed; callers drop the frame. */
|
|
4998
|
+
function decodeControl(text) {
|
|
4999
|
+
if (text.length > LIMITS.maxControlFrameBytes || utf8Encode(text).length > LIMITS.maxControlFrameBytes) return null;
|
|
5000
|
+
let raw;
|
|
5001
|
+
try {
|
|
5002
|
+
raw = JSON.parse(text);
|
|
5003
|
+
} catch {
|
|
5004
|
+
return null;
|
|
5005
|
+
}
|
|
5006
|
+
if (!raw || typeof raw !== "object") return null;
|
|
5007
|
+
const f = raw;
|
|
5008
|
+
switch (f.t) {
|
|
5009
|
+
case "hello":
|
|
5010
|
+
if (typeof f.v !== "number") return null;
|
|
5011
|
+
return {
|
|
5012
|
+
t: "hello",
|
|
5013
|
+
v: f.v,
|
|
5014
|
+
svc: f.svc,
|
|
5015
|
+
nick: sanitizeNick(f.nick)
|
|
5016
|
+
};
|
|
5017
|
+
case "m":
|
|
5018
|
+
if (typeof f.ts !== "number" || typeof f.m !== "string") return null;
|
|
5019
|
+
if (utf8Encode(f.m).length > LIMITS.maxChatMessageBytes) return null;
|
|
5020
|
+
return {
|
|
5021
|
+
t: "m",
|
|
5022
|
+
ts: f.ts,
|
|
5023
|
+
m: f.m
|
|
5024
|
+
};
|
|
5025
|
+
case "call":
|
|
5026
|
+
if (typeof f.s !== "string" || utf8Encode(f.s).length > LIMITS.maxChatMessageBytes) return null;
|
|
5027
|
+
return {
|
|
5028
|
+
t: "call",
|
|
5029
|
+
s: f.s
|
|
5030
|
+
};
|
|
5031
|
+
case "svc": return {
|
|
5032
|
+
t: "svc",
|
|
5033
|
+
svc: f.svc
|
|
5034
|
+
};
|
|
5035
|
+
case "req":
|
|
5036
|
+
if (!isStreamId(f.id) || typeof f.s !== "string" || typeof f.m !== "string") return null;
|
|
5037
|
+
if (typeof f.p !== "string" || f.p.length > LIMITS.maxPathBytes || !isHeaderList(f.h)) return null;
|
|
5038
|
+
return {
|
|
5039
|
+
t: "req",
|
|
5040
|
+
id: f.id,
|
|
5041
|
+
s: f.s,
|
|
5042
|
+
m: f.m,
|
|
5043
|
+
p: f.p,
|
|
5044
|
+
h: f.h,
|
|
5045
|
+
b: f.b === true
|
|
5046
|
+
};
|
|
5047
|
+
case "res":
|
|
5048
|
+
if (!isStreamId(f.id) || typeof f.st !== "number" || !Number.isInteger(f.st)) return null;
|
|
5049
|
+
if (f.st < 100 || f.st > 599 || !isHeaderList(f.h)) return null;
|
|
5050
|
+
return {
|
|
5051
|
+
t: "res",
|
|
5052
|
+
id: f.id,
|
|
5053
|
+
st: f.st,
|
|
5054
|
+
h: f.h,
|
|
5055
|
+
b: f.b === true
|
|
5056
|
+
};
|
|
5057
|
+
case "file":
|
|
5058
|
+
if (!isStreamId(f.id) || typeof f.f !== "string" || typeof f.ts !== "number") return null;
|
|
5059
|
+
if (typeof f.n !== "string" || typeof f.m !== "string") return null;
|
|
5060
|
+
if (typeof f.s !== "number" || !Number.isSafeInteger(f.s) || f.s < 0) return null;
|
|
5061
|
+
return {
|
|
5062
|
+
t: "file",
|
|
5063
|
+
id: f.id,
|
|
5064
|
+
f: f.f,
|
|
5065
|
+
ts: f.ts,
|
|
5066
|
+
n: f.n,
|
|
5067
|
+
s: f.s,
|
|
5068
|
+
m: f.m
|
|
5069
|
+
};
|
|
5070
|
+
case "pay-req":
|
|
5071
|
+
case "pay": {
|
|
5072
|
+
if (!isPayId(f.id) || typeof f.ts !== "number" || !isAmount(f.v) || !isUnit(f.u)) return null;
|
|
5073
|
+
const memo = typeof f.memo === "string" ? f.memo.slice(0, 140) : void 0;
|
|
5074
|
+
if (f.t === "pay-req") {
|
|
5075
|
+
if (!Array.isArray(f.e) || f.e.length === 0 || f.e.length > 8 || !f.e.every(isEndpoint)) return null;
|
|
5076
|
+
return {
|
|
5077
|
+
t: "pay-req",
|
|
5078
|
+
id: f.id,
|
|
5079
|
+
ts: f.ts,
|
|
5080
|
+
v: f.v,
|
|
5081
|
+
u: f.u,
|
|
5082
|
+
memo,
|
|
5083
|
+
e: f.e,
|
|
5084
|
+
a: isPayId(f.a) ? f.a : void 0,
|
|
5085
|
+
n: f.n === "mainnet" || f.n === "testnet" ? f.n : void 0
|
|
5086
|
+
};
|
|
5087
|
+
}
|
|
5088
|
+
if (!isEndpoint(f.e) || f.rid !== void 0 && !isPayId(f.rid)) return null;
|
|
5089
|
+
return {
|
|
5090
|
+
t: "pay",
|
|
5091
|
+
id: f.id,
|
|
5092
|
+
ts: f.ts,
|
|
5093
|
+
rid: f.rid,
|
|
5094
|
+
v: f.v,
|
|
5095
|
+
u: f.u,
|
|
5096
|
+
memo,
|
|
5097
|
+
e: f.e
|
|
5098
|
+
};
|
|
5099
|
+
}
|
|
5100
|
+
case "pay-ask":
|
|
5101
|
+
if (!isPayId(f.id) || typeof f.ts !== "number" || !isAmount(f.v) || !isUnit(f.u)) return null;
|
|
5102
|
+
if (f.m !== "arkade" && f.m !== "usdt" && f.m !== "bark" && f.m !== "bitcoin" && f.m !== "fedimint" && f.m !== "spark") return null;
|
|
5103
|
+
return {
|
|
5104
|
+
t: "pay-ask",
|
|
5105
|
+
id: f.id,
|
|
5106
|
+
ts: f.ts,
|
|
5107
|
+
v: f.v,
|
|
5108
|
+
u: f.u,
|
|
5109
|
+
m: f.m,
|
|
5110
|
+
memo: typeof f.memo === "string" ? f.memo.slice(0, 140) : void 0,
|
|
5111
|
+
n: f.n === "mainnet" || f.n === "testnet" ? f.n : void 0
|
|
5112
|
+
};
|
|
5113
|
+
case "pay-res":
|
|
5114
|
+
if (!isPayId(f.id) || typeof f.ok !== "boolean") return null;
|
|
5115
|
+
if (f.v !== void 0 && !isAmount(f.v)) return null;
|
|
5116
|
+
return {
|
|
5117
|
+
t: "pay-res",
|
|
5118
|
+
id: f.id,
|
|
5119
|
+
ok: f.ok,
|
|
5120
|
+
v: f.v,
|
|
5121
|
+
err: typeof f.err === "string" ? f.err.slice(0, 200) : void 0,
|
|
5122
|
+
...f.c === true && !f.ok ? { c: true } : {}
|
|
5123
|
+
};
|
|
5124
|
+
case "rst":
|
|
5125
|
+
if (!isStreamId(f.id) || f.d !== "q" && f.d !== "s" && f.d !== "f") return null;
|
|
5126
|
+
return {
|
|
5127
|
+
t: "rst",
|
|
5128
|
+
id: f.id,
|
|
5129
|
+
d: f.d,
|
|
5130
|
+
e: typeof f.e === "string" ? f.e.slice(0, 256) : ""
|
|
5131
|
+
};
|
|
5132
|
+
case "ping":
|
|
5133
|
+
case "pong":
|
|
5134
|
+
if (typeof f.ts !== "number") return null;
|
|
5135
|
+
return {
|
|
5136
|
+
t: f.t,
|
|
5137
|
+
ts: f.ts
|
|
5138
|
+
};
|
|
5139
|
+
default: return null;
|
|
5140
|
+
}
|
|
5141
|
+
}
|
|
5142
|
+
function encodeChunk(chunk) {
|
|
5143
|
+
if (chunk.payload.length > MAX_CHUNK_PAYLOAD_BYTES) throw new Error("Chunk payload too large");
|
|
5144
|
+
const out = new Uint8Array(CHUNK_HEADER_BYTES + chunk.payload.length);
|
|
5145
|
+
const view = new DataView(out.buffer);
|
|
5146
|
+
view.setUint8(0, chunk.kind);
|
|
5147
|
+
view.setUint32(1, chunk.id);
|
|
5148
|
+
view.setUint8(5, chunk.end ? 1 : 0);
|
|
5149
|
+
out.set(chunk.payload, CHUNK_HEADER_BYTES);
|
|
5150
|
+
return out;
|
|
5151
|
+
}
|
|
5152
|
+
function decodeChunk(data) {
|
|
5153
|
+
if (data.length < CHUNK_HEADER_BYTES || data.length > LIMITS.maxChunkMessageBytes) return null;
|
|
5154
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
5155
|
+
const kind = view.getUint8(0);
|
|
5156
|
+
if (kind !== CHUNK_KIND.requestBody && kind !== CHUNK_KIND.responseBody && kind !== CHUNK_KIND.fileBody) return null;
|
|
5157
|
+
return {
|
|
5158
|
+
kind,
|
|
5159
|
+
id: view.getUint32(1),
|
|
5160
|
+
end: (view.getUint8(5) & 1) !== 0,
|
|
5161
|
+
payload: data.subarray(CHUNK_HEADER_BYTES)
|
|
5162
|
+
};
|
|
5163
|
+
}
|
|
5164
|
+
function wrapDataChannel(dc) {
|
|
5165
|
+
dc.binaryType = "arraybuffer";
|
|
5166
|
+
dc.bufferedAmountLowThreshold = LIMITS.sendLowWaterMark;
|
|
5167
|
+
const channel = {
|
|
5168
|
+
send(data) {
|
|
5169
|
+
if (dc.readyState !== "open") throw new Error("Data link is not open");
|
|
5170
|
+
if (typeof data === "string") dc.send(data);
|
|
5171
|
+
else dc.send(data);
|
|
5172
|
+
},
|
|
5173
|
+
get bufferedAmount() {
|
|
5174
|
+
return dc.bufferedAmount;
|
|
5175
|
+
},
|
|
5176
|
+
drained() {
|
|
5177
|
+
if (dc.bufferedAmount <= LIMITS.sendLowWaterMark || dc.readyState !== "open") return Promise.resolve();
|
|
5178
|
+
return new Promise((resolve) => {
|
|
5179
|
+
const done = () => {
|
|
5180
|
+
dc.removeEventListener("bufferedamountlow", done);
|
|
5181
|
+
dc.removeEventListener("close", done);
|
|
5182
|
+
resolve();
|
|
5183
|
+
};
|
|
5184
|
+
dc.addEventListener("bufferedamountlow", done);
|
|
5185
|
+
dc.addEventListener("close", done);
|
|
5186
|
+
});
|
|
5187
|
+
},
|
|
5188
|
+
close() {
|
|
5189
|
+
dc.close();
|
|
5190
|
+
},
|
|
5191
|
+
binary: true,
|
|
5192
|
+
onMessage: null,
|
|
5193
|
+
onClose: null
|
|
5194
|
+
};
|
|
5195
|
+
dc.addEventListener("message", (event) => {
|
|
5196
|
+
const data = event.data;
|
|
5197
|
+
if (typeof data === "string") channel.onMessage?.(data);
|
|
5198
|
+
else if (data instanceof ArrayBuffer) channel.onMessage?.(new Uint8Array(data));
|
|
5199
|
+
});
|
|
5200
|
+
dc.addEventListener("close", () => channel.onClose?.());
|
|
5201
|
+
return channel;
|
|
5202
|
+
}
|
|
5203
|
+
/**
|
|
5204
|
+
* Resolves once the channel has room for a body's next chunk; undefined when it has now. A channel with a send budget
|
|
5205
|
+
* (native and iroh ones throw past 120 KiB) gives a body a quarter of it, as files/3 keeps to, so a ping or a message
|
|
5206
|
+
* still fits beside it; others keep under the high water mark.
|
|
5207
|
+
*/
|
|
5208
|
+
function bodyRoom(channel, isCancelled) {
|
|
5209
|
+
if (!channel.sendBudget) return channel.bufferedAmount > LIMITS.sendHighWaterMark ? channel.drained() : void 0;
|
|
5210
|
+
const line = channel.sendBudget / 4;
|
|
5211
|
+
if (channel.bufferedAmount < line) return;
|
|
5212
|
+
return (async () => {
|
|
5213
|
+
while (!isCancelled() && channel.bufferedAmount >= line) {
|
|
5214
|
+
await channel.drained().catch(() => {});
|
|
5215
|
+
if (!isCancelled() && channel.bufferedAmount >= line) await new Promise((resolve) => setTimeout(resolve, 20));
|
|
5216
|
+
}
|
|
5217
|
+
})();
|
|
5218
|
+
}
|
|
5219
|
+
/** Writes a body as chunks, honouring backpressure. Always ends with an END chunk. */
|
|
5220
|
+
async function sendBody(channel, kind, id, body, isCancelled = () => false) {
|
|
5221
|
+
for await (const part of body) for (let offset = 0; offset < part.length; offset += MAX_CHUNK_PAYLOAD_BYTES) {
|
|
5222
|
+
if (isCancelled()) return;
|
|
5223
|
+
const room = bodyRoom(channel, isCancelled);
|
|
5224
|
+
if (room) {
|
|
5225
|
+
await room;
|
|
5226
|
+
if (isCancelled()) return;
|
|
5227
|
+
}
|
|
5228
|
+
const payload = part.subarray(offset, offset + MAX_CHUNK_PAYLOAD_BYTES);
|
|
5229
|
+
channel.send(encodeChunk({
|
|
5230
|
+
kind,
|
|
5231
|
+
id,
|
|
5232
|
+
end: false,
|
|
5233
|
+
payload
|
|
5234
|
+
}));
|
|
5235
|
+
}
|
|
5236
|
+
if (isCancelled()) return;
|
|
5237
|
+
channel.send(encodeChunk({
|
|
5238
|
+
kind,
|
|
5239
|
+
id,
|
|
5240
|
+
end: true,
|
|
5241
|
+
payload: /* @__PURE__ */ new Uint8Array(0)
|
|
5242
|
+
}));
|
|
5243
|
+
}
|
|
5244
|
+
//#endregion
|
|
4759
5245
|
//#region ../core/src/http.ts
|
|
4760
5246
|
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
|
|
4761
5247
|
"localhost",
|
|
@@ -5705,7 +6191,7 @@ var LinkSession = class {
|
|
|
5705
6191
|
const stepping = this.fastStepsAfter ? Date.now() - this.fastStepsAfter : -1;
|
|
5706
6192
|
if (pace === "fast" && stepping >= 0) return Math.min(4 * interval, Math.max(2 * interval, stepping));
|
|
5707
6193
|
if (pace === "fast" || pace === "connected" || since < 0) return interval;
|
|
5708
|
-
if (pace === "background" && this.presence.
|
|
6194
|
+
if (pace === "background" && !this.presence.online && this.invite()) return unusedInviteWait(Date.now() - this.awaitingSince, interval);
|
|
5709
6195
|
return Math.min(interval, Math.max(2 * this.intervals.fast, since));
|
|
5710
6196
|
}
|
|
5711
6197
|
/**
|
|
@@ -6791,32 +7277,1023 @@ function verifyChain(raw, groupId) {
|
|
|
6791
7277
|
if ("error" in result) return result;
|
|
6792
7278
|
chain.push(result.commit);
|
|
6793
7279
|
}
|
|
6794
|
-
return { chain };
|
|
7280
|
+
return { chain };
|
|
7281
|
+
}
|
|
7282
|
+
/**
|
|
7283
|
+
* What a commit is refused with while admin work is off on this device (WISP 06 § Forced takeover: after a forced
|
|
7284
|
+
* takeover or a restore, until the person turns on "Manage groups from this device").
|
|
7285
|
+
*/
|
|
7286
|
+
/**
|
|
7287
|
+
* The highest sequence number a frame of my own key, sent by another copy of my profile, may make me adopt (WISP 06):
|
|
7288
|
+
* the highest floor a copy can have (2^32 - 2^24) and room above it. The community beacon packs the counter in 32 bits.
|
|
7289
|
+
*/
|
|
7290
|
+
var OWN_FRAME_LIMIT = 2 ** 32 - 2 ** 23;
|
|
7291
|
+
/**
|
|
7292
|
+
* Frames said again (WISP 902, both profiles): a member's own frames that no edge took when it said them are
|
|
7293
|
+
* remembered (`kept`, the latest) and, once the chain it follows is newer than such a frame's and has not moved for
|
|
7294
|
+
* `settledMs`, looked at once for saying again where the members let in while it was behind can read them. A catch-up
|
|
7295
|
+
* brings commits one by one, and a frame said again at the first of them would still miss those let in by the next.
|
|
7296
|
+
*/
|
|
7297
|
+
var SAID_AGAIN = {
|
|
7298
|
+
settledMs: 5e3,
|
|
7299
|
+
kept: 64
|
|
7300
|
+
};
|
|
7301
|
+
/** `alone` with `key` added last, keeping only keys still `present` (frames still in the log), the latest `kept`. */
|
|
7302
|
+
function rememberAlone(alone, key, present) {
|
|
7303
|
+
return [...(alone ?? []).filter((k) => k !== key && present(k)), key].slice(-SAID_AGAIN.kept);
|
|
7304
|
+
}
|
|
7305
|
+
var GROUP_ADMIN_OFF_ERROR = "Managing this group is off on this device. Turn on Manage groups from this device first.";
|
|
7306
|
+
//#endregion
|
|
7307
|
+
//#region ../core/src/canonicalJson.ts
|
|
7308
|
+
/** True when `s` holds a lone UTF-16 surrogate, which I-JSON (and so JCS) does not allow. */
|
|
7309
|
+
function hasLoneSurrogate(s) {
|
|
7310
|
+
for (let i = 0; i < s.length; i++) {
|
|
7311
|
+
const c = s.charCodeAt(i);
|
|
7312
|
+
if (c >= 55296 && c <= 56319) {
|
|
7313
|
+
const next = s.charCodeAt(i + 1);
|
|
7314
|
+
if (next >= 56320 && next <= 57343) {
|
|
7315
|
+
i++;
|
|
7316
|
+
continue;
|
|
7317
|
+
}
|
|
7318
|
+
return true;
|
|
7319
|
+
}
|
|
7320
|
+
if (c >= 56320 && c <= 57343) return true;
|
|
7321
|
+
}
|
|
7322
|
+
return false;
|
|
7323
|
+
}
|
|
7324
|
+
function write(value, depth) {
|
|
7325
|
+
if (depth > 64) throw new Error("JSON nested deeper than 64 levels");
|
|
7326
|
+
if (value === null) return "null";
|
|
7327
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
7328
|
+
if (typeof value === "number") {
|
|
7329
|
+
if (!Number.isFinite(value)) throw new Error("JSON numbers must be finite");
|
|
7330
|
+
return JSON.stringify(value);
|
|
7331
|
+
}
|
|
7332
|
+
if (typeof value === "string") {
|
|
7333
|
+
if (hasLoneSurrogate(value)) throw new Error("A JSON string holds a lone surrogate");
|
|
7334
|
+
return JSON.stringify(value);
|
|
7335
|
+
}
|
|
7336
|
+
if (Array.isArray(value)) return `[${value.map((v) => write(v, depth + 1)).join(",")}]`;
|
|
7337
|
+
if (typeof value === "object") {
|
|
7338
|
+
const record = value;
|
|
7339
|
+
return `{${Object.keys(value).filter((k) => record[k] !== void 0).sort((a, b) => a < b ? -1 : a > b ? 1 : 0).map((k) => {
|
|
7340
|
+
if (hasLoneSurrogate(k)) throw new Error("A JSON key holds a lone surrogate");
|
|
7341
|
+
return `${JSON.stringify(k)}:${write(record[k], depth + 1)}`;
|
|
7342
|
+
}).join(",")}}`;
|
|
7343
|
+
}
|
|
7344
|
+
throw new Error(`Not a JSON value: ${typeof value}`);
|
|
7345
|
+
}
|
|
7346
|
+
/** The canonical text of a JSON value. Throws on a value JSON cannot hold (a non-finite number, a lone surrogate). */
|
|
7347
|
+
function canonicalJson(value) {
|
|
7348
|
+
return write(value, 0);
|
|
7349
|
+
}
|
|
7350
|
+
/** The canonical UTF-8 bytes of a JSON value. */
|
|
7351
|
+
function canonicalJsonBytes(value) {
|
|
7352
|
+
return utf8Encode(canonicalJson(value));
|
|
7353
|
+
}
|
|
7354
|
+
/**
|
|
7355
|
+
* Reads bytes that must be the canonical form of a JSON value: `not-json` when they are not UTF-8 JSON, `not-canonical`
|
|
7356
|
+
* when writing the value again gives other bytes.
|
|
7357
|
+
*/
|
|
7358
|
+
function readCanonicalJson(bytes) {
|
|
7359
|
+
let value;
|
|
7360
|
+
try {
|
|
7361
|
+
value = JSON.parse(utf8Decode(bytes));
|
|
7362
|
+
} catch (error) {
|
|
7363
|
+
return {
|
|
7364
|
+
ok: false,
|
|
7365
|
+
reason: "not-json",
|
|
7366
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
7367
|
+
};
|
|
7368
|
+
}
|
|
7369
|
+
let again;
|
|
7370
|
+
try {
|
|
7371
|
+
again = canonicalJsonBytes(value);
|
|
7372
|
+
} catch (error) {
|
|
7373
|
+
return {
|
|
7374
|
+
ok: false,
|
|
7375
|
+
reason: "not-canonical",
|
|
7376
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
7377
|
+
};
|
|
7378
|
+
}
|
|
7379
|
+
if (!bytesEqual(again, bytes)) return {
|
|
7380
|
+
ok: false,
|
|
7381
|
+
reason: "not-canonical"
|
|
7382
|
+
};
|
|
7383
|
+
return {
|
|
7384
|
+
ok: true,
|
|
7385
|
+
value
|
|
7386
|
+
};
|
|
7387
|
+
}
|
|
7388
|
+
//#endregion
|
|
7389
|
+
//#region ../core/src/appStatements.ts
|
|
7390
|
+
/** The prefixes of phase 1. The others of the WISP (reports, reviews, licences, rotation, publisher proofs) come later. */
|
|
7391
|
+
var APP_PREFIXES = {
|
|
7392
|
+
app: "ghostly-app/1",
|
|
7393
|
+
store: "ghostly-store/1",
|
|
7394
|
+
revoke: "ghostly-revoke/1"
|
|
7395
|
+
};
|
|
7396
|
+
var APP_STATEMENT_LIMITS = {
|
|
7397
|
+
/** A signature statement's canonical bytes: 52 + 86 characters of values and the keys around them. */
|
|
7398
|
+
signatureBytes: 1024,
|
|
7399
|
+
/** Digests named by one revocation. */
|
|
7400
|
+
revokeDigests: 64,
|
|
7401
|
+
/** A revocation's reason, in characters. */
|
|
7402
|
+
reason: 200,
|
|
7403
|
+
/** Revocations in one `ghostly-revoke.json`. */
|
|
7404
|
+
revocations: 4096
|
|
7405
|
+
};
|
|
7406
|
+
/** A z-base32 Ed25519 public key: 52 characters that decode to 32 bytes and encode back to the same text. */
|
|
7407
|
+
function isAppKey(value) {
|
|
7408
|
+
if (typeof value !== "string" || !/^[ybndrfg8ejkmcpqxot1uwisza345h769]{52}$/.test(value)) return false;
|
|
7409
|
+
const bytes = fromZ32(value);
|
|
7410
|
+
return bytes.length === 32 && toZ32(bytes) === value;
|
|
7411
|
+
}
|
|
7412
|
+
/** A SHA-256 in base64url without padding: 43 characters that decode to 32 bytes and encode back to the same text. */
|
|
7413
|
+
function isAppHash(value) {
|
|
7414
|
+
if (typeof value !== "string" || !/^[A-Za-z0-9_-]{43}$/.test(value)) return false;
|
|
7415
|
+
return toBase64Url(fromBase64Url(value)) === value;
|
|
7416
|
+
}
|
|
7417
|
+
/** An app reference, `<publisher key>/<name>`. */
|
|
7418
|
+
var APP_NAME = /^[a-z][a-z0-9-]{0,31}$/;
|
|
7419
|
+
function isAppRef(value) {
|
|
7420
|
+
if (typeof value !== "string") return false;
|
|
7421
|
+
return value.indexOf("/") === 52 && isAppKey(value.slice(0, 52)) && APP_NAME.test(value.slice(53));
|
|
7422
|
+
}
|
|
7423
|
+
function appRef(publisher, name) {
|
|
7424
|
+
return `${publisher}/${name}`;
|
|
7425
|
+
}
|
|
7426
|
+
/** The publisher key of a reference. */
|
|
7427
|
+
function appRefPublisher(ref) {
|
|
7428
|
+
return ref.slice(0, 52);
|
|
7429
|
+
}
|
|
7430
|
+
/**
|
|
7431
|
+
* The publisher fingerprint people see: the first 16 z-base32 characters of the key (80 bits), in four groups.
|
|
7432
|
+
*/
|
|
7433
|
+
function appFingerprint(key) {
|
|
7434
|
+
return (key.slice(0, 16).match(/.{1,4}/g) ?? []).join(" ");
|
|
7435
|
+
}
|
|
7436
|
+
/** The bytes a signature covers: the prefix, a zero byte, then the SHA-256 of the object's canonical bytes. */
|
|
7437
|
+
function appSignedBytes(prefix, canonical) {
|
|
7438
|
+
const p = utf8Encode(prefix);
|
|
7439
|
+
const out = new Uint8Array(p.length + 1 + 32);
|
|
7440
|
+
out.set(p, 0);
|
|
7441
|
+
out.set(sha256(canonical), p.length + 1);
|
|
7442
|
+
return out;
|
|
7443
|
+
}
|
|
7444
|
+
/** Signs an object under a prefix: its canonical bytes and the signature statement. */
|
|
7445
|
+
async function signAppObject(prefix, value, signer) {
|
|
7446
|
+
const bytes = canonicalJsonBytes(value);
|
|
7447
|
+
const sig = await signer.sign(appSignedBytes(prefix, bytes));
|
|
7448
|
+
return {
|
|
7449
|
+
bytes,
|
|
7450
|
+
signature: {
|
|
7451
|
+
alg: "ed25519",
|
|
7452
|
+
key: toZ32(signer.publicKey),
|
|
7453
|
+
sig: toBase64Url(sig)
|
|
7454
|
+
}
|
|
7455
|
+
};
|
|
7456
|
+
}
|
|
7457
|
+
/**
|
|
7458
|
+
* Ed25519 verification as RFC 8032 writes it (`zip215: false`): canonical encodings of the point and the scalar only,
|
|
7459
|
+
* so every parser (this one, a Rust one with `verify_strict`) accepts exactly the same signatures.
|
|
7460
|
+
*/
|
|
7461
|
+
function strictVerify(signature, message, publicKey) {
|
|
7462
|
+
try {
|
|
7463
|
+
return ed25519.verify(signature, message, publicKey, { zip215: false });
|
|
7464
|
+
} catch {
|
|
7465
|
+
return false;
|
|
7466
|
+
}
|
|
7467
|
+
}
|
|
7468
|
+
var exactKeys$1 = (value, keys, optional = []) => {
|
|
7469
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
7470
|
+
const have = Object.keys(value);
|
|
7471
|
+
return keys.every((k) => have.includes(k)) && have.every((k) => keys.includes(k) || optional.includes(k));
|
|
7472
|
+
};
|
|
7473
|
+
/** Checks the shape of a signature statement: exactly `alg`, `key` and `sig`, a 64-byte signature. */
|
|
7474
|
+
function isAppSignature(value) {
|
|
7475
|
+
if (!exactKeys$1(value, [
|
|
7476
|
+
"alg",
|
|
7477
|
+
"key",
|
|
7478
|
+
"sig"
|
|
7479
|
+
])) return false;
|
|
7480
|
+
if (value.alg !== "ed25519" || !isAppKey(value.key)) return false;
|
|
7481
|
+
return typeof value.sig === "string" && /^[A-Za-z0-9_-]{86}$/.test(value.sig) && toBase64Url(fromBase64Url(value.sig)) === value.sig;
|
|
7482
|
+
}
|
|
7483
|
+
/** Reads a signature statement from its bytes, which must be canonical. */
|
|
7484
|
+
function readAppSignature(bytes) {
|
|
7485
|
+
if (bytes.length > APP_STATEMENT_LIMITS.signatureBytes) return {
|
|
7486
|
+
ok: false,
|
|
7487
|
+
reason: "bad-signature-statement",
|
|
7488
|
+
detail: "Too large"
|
|
7489
|
+
};
|
|
7490
|
+
const read = readCanonicalJson(bytes);
|
|
7491
|
+
if (!read.ok) return {
|
|
7492
|
+
ok: false,
|
|
7493
|
+
reason: "bad-signature-statement",
|
|
7494
|
+
detail: read.reason
|
|
7495
|
+
};
|
|
7496
|
+
if (!isAppSignature(read.value)) return {
|
|
7497
|
+
ok: false,
|
|
7498
|
+
reason: "bad-signature-statement",
|
|
7499
|
+
detail: "Not {alg, key, sig}"
|
|
7500
|
+
};
|
|
7501
|
+
return {
|
|
7502
|
+
ok: true,
|
|
7503
|
+
signature: read.value
|
|
7504
|
+
};
|
|
7505
|
+
}
|
|
7506
|
+
/**
|
|
7507
|
+
* Verifies a signature over an object's canonical bytes under a prefix, made by `key`: `signature-key` when the
|
|
7508
|
+
* statement names another key, `bad-signature` when it does not verify.
|
|
7509
|
+
*/
|
|
7510
|
+
function verifyAppSignature(prefix, canonical, signature, key) {
|
|
7511
|
+
if (!isAppSignature(signature)) return {
|
|
7512
|
+
ok: false,
|
|
7513
|
+
reason: "bad-signature-statement"
|
|
7514
|
+
};
|
|
7515
|
+
if (signature.key !== key) return {
|
|
7516
|
+
ok: false,
|
|
7517
|
+
reason: "signature-key"
|
|
7518
|
+
};
|
|
7519
|
+
return strictVerify(fromBase64Url(signature.sig), appSignedBytes(prefix, canonical), fromZ32(key)) ? { ok: true } : {
|
|
7520
|
+
ok: false,
|
|
7521
|
+
reason: "bad-signature"
|
|
7522
|
+
};
|
|
7523
|
+
}
|
|
7524
|
+
var isSequence = (v) => typeof v === "number" && Number.isSafeInteger(v) && v >= 1;
|
|
7525
|
+
var isPlainLine = (v, max) => typeof v === "string" && [...v].length <= max && !/\p{Cc}/u.test(v);
|
|
7526
|
+
/** Checks a revocation statement's shape; the reason it is refused, or null. */
|
|
7527
|
+
function checkAppRevokeStatement(value) {
|
|
7528
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return "Not an object";
|
|
7529
|
+
const v = value;
|
|
7530
|
+
const byDigests = "digests" in v;
|
|
7531
|
+
if (!exactKeys$1(v, [
|
|
7532
|
+
"ghostlyRevoke",
|
|
7533
|
+
"app",
|
|
7534
|
+
byDigests ? "digests" : "upTo"
|
|
7535
|
+
], ["reason"])) return "Keys are not ghostlyRevoke, app, and exactly one of digests and upTo (reason optional)";
|
|
7536
|
+
if (v.ghostlyRevoke !== 1) return "ghostlyRevoke is not 1";
|
|
7537
|
+
if (!isAppRef(v.app)) return "app is not a reference";
|
|
7538
|
+
if (byDigests) {
|
|
7539
|
+
const d = v.digests;
|
|
7540
|
+
if (!Array.isArray(d) || d.length < 1 || d.length > APP_STATEMENT_LIMITS.revokeDigests) return "digests is not 1 to 64 digests";
|
|
7541
|
+
if (!d.every(isAppHash) || new Set(d).size !== d.length) return "digests holds a bad or repeated digest";
|
|
7542
|
+
} else if (!isSequence(v.upTo)) return "upTo is not a sequence";
|
|
7543
|
+
if (v.reason !== void 0 && !isPlainLine(v.reason, APP_STATEMENT_LIMITS.reason)) return "reason is not one line of 200 characters";
|
|
7544
|
+
return null;
|
|
7545
|
+
}
|
|
7546
|
+
/** Signs a revocation with the publisher's key. */
|
|
7547
|
+
async function signAppRevocation(statement, signer) {
|
|
7548
|
+
const problem = checkAppRevokeStatement(statement);
|
|
7549
|
+
if (problem) throw new Error(`Not a revocation: ${problem}`);
|
|
7550
|
+
if (toZ32(signer.publicKey) !== appRefPublisher(statement.app)) throw new Error("A revocation is signed by its app's publisher");
|
|
7551
|
+
const { signature } = await signAppObject(APP_PREFIXES.revoke, statement, signer);
|
|
7552
|
+
return {
|
|
7553
|
+
statement,
|
|
7554
|
+
signature
|
|
7555
|
+
};
|
|
7556
|
+
}
|
|
7557
|
+
/** What a checked revocation is kept by: the SHA-256 of its canonical bytes (statement and signature), base64url. */
|
|
7558
|
+
function appRevocationId(revocation) {
|
|
7559
|
+
return toBase64Url(sha256(canonicalJsonBytes(revocation)));
|
|
7560
|
+
}
|
|
7561
|
+
/**
|
|
7562
|
+
* Reads one signed revocation (a parsed value, from `ghostly-revoke.json` or a store index's `revoked`): its shape, then
|
|
7563
|
+
* the signature under `ghostly-revoke/1` by the publisher its `app` names. One that `checked` holds verified before, so
|
|
7564
|
+
* its signature is not checked again; one that verifies now is added to it.
|
|
7565
|
+
*/
|
|
7566
|
+
function readAppRevocation(value, checked) {
|
|
7567
|
+
const read = readAppRevocationShape(value);
|
|
7568
|
+
if (!read.ok) return read;
|
|
7569
|
+
const verified = verifyAppRevocation(read.revocation, checked);
|
|
7570
|
+
return verified.ok ? read : verified;
|
|
7571
|
+
}
|
|
7572
|
+
/** The first half of `readAppRevocation`: the shape of a signed revocation, with no signature checked. */
|
|
7573
|
+
function readAppRevocationShape(value) {
|
|
7574
|
+
if (!exactKeys$1(value, ["statement", "signature"])) return {
|
|
7575
|
+
ok: false,
|
|
7576
|
+
reason: "bad-revocation",
|
|
7577
|
+
detail: "Keys are not statement and signature"
|
|
7578
|
+
};
|
|
7579
|
+
const problem = checkAppRevokeStatement(value.statement);
|
|
7580
|
+
if (problem) return {
|
|
7581
|
+
ok: false,
|
|
7582
|
+
reason: "bad-revocation",
|
|
7583
|
+
detail: problem
|
|
7584
|
+
};
|
|
7585
|
+
if (!isAppSignature(value.signature)) return {
|
|
7586
|
+
ok: false,
|
|
7587
|
+
reason: "bad-signature-statement"
|
|
7588
|
+
};
|
|
7589
|
+
return {
|
|
7590
|
+
ok: true,
|
|
7591
|
+
revocation: {
|
|
7592
|
+
statement: value.statement,
|
|
7593
|
+
signature: value.signature
|
|
7594
|
+
}
|
|
7595
|
+
};
|
|
7596
|
+
}
|
|
7597
|
+
/** The second half: the signature of a revocation of the right shape, unless `checked` holds it. */
|
|
7598
|
+
function verifyAppRevocation(revocation, checked) {
|
|
7599
|
+
const id = checked && appRevocationId(revocation);
|
|
7600
|
+
if (id !== void 0 && checked.has(id)) return { ok: true };
|
|
7601
|
+
const verified = verifyAppSignature(APP_PREFIXES.revoke, canonicalJsonBytes(revocation.statement), revocation.signature, appRefPublisher(revocation.statement.app));
|
|
7602
|
+
if (verified.ok && id !== void 0) checked.add(id);
|
|
7603
|
+
return verified;
|
|
7604
|
+
}
|
|
7605
|
+
/**
|
|
7606
|
+
* Reads `ghostly-revoke.json`: canonical bytes holding a list of signed revocations. One that does not verify refuses the
|
|
7607
|
+
* file, as a store index's `revoked` does.
|
|
7608
|
+
*/
|
|
7609
|
+
function readAppRevocations(bytes) {
|
|
7610
|
+
const read = readCanonicalJson(bytes);
|
|
7611
|
+
if (!read.ok) return read;
|
|
7612
|
+
if (!Array.isArray(read.value) || read.value.length > APP_STATEMENT_LIMITS.revocations) return {
|
|
7613
|
+
ok: false,
|
|
7614
|
+
reason: "bad-revocation",
|
|
7615
|
+
detail: "Not a list of at most 4096 revocations"
|
|
7616
|
+
};
|
|
7617
|
+
const revocations = [];
|
|
7618
|
+
for (const entry of read.value) {
|
|
7619
|
+
const one = readAppRevocation(entry);
|
|
7620
|
+
if (!one.ok) return one;
|
|
7621
|
+
revocations.push(one.revocation);
|
|
7622
|
+
}
|
|
7623
|
+
return {
|
|
7624
|
+
ok: true,
|
|
7625
|
+
revocations
|
|
7626
|
+
};
|
|
7627
|
+
}
|
|
7628
|
+
//#endregion
|
|
7629
|
+
//#region ../core/src/messageTime.ts
|
|
7630
|
+
/**
|
|
7631
|
+
* 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
|
|
7632
|
+
* times): clocks drift, but a time past this is the peer's choice, not its clock.
|
|
7633
|
+
*/
|
|
7634
|
+
var MESSAGE_CLOCK_SKEW_MS = 3e5;
|
|
7635
|
+
/**
|
|
7636
|
+
* The time a line of a group's history is kept under (a membership change, a rename): what its commit says, but at
|
|
7637
|
+
* most `MESSAGE_CLOCK_SKEW_MS` past this clock, and now when it is not a positive number. Messages do not use it:
|
|
7638
|
+
* they are placed where they arrive (below).
|
|
7639
|
+
*/
|
|
7640
|
+
function receivedTimestamp(ts, now = Date.now()) {
|
|
7641
|
+
if (!Number.isFinite(ts) || ts <= 0) return now;
|
|
7642
|
+
return Math.min(ts, now + MESSAGE_CLOCK_SKEW_MS);
|
|
7643
|
+
}
|
|
7644
|
+
/** The latest moment a date can hold. */
|
|
7645
|
+
var MAX_DATE_MS = 864e13;
|
|
7646
|
+
/** The time a sender says, as it is kept: a positive time a date can hold, or nothing. */
|
|
7647
|
+
function claimedTime(ts) {
|
|
7648
|
+
return typeof ts === "number" && Number.isFinite(ts) && ts > 0 && ts <= MAX_DATE_MS ? ts : void 0;
|
|
7649
|
+
}
|
|
7650
|
+
/**
|
|
7651
|
+
* The place of a row stored now: this clock, and past the last place given in its chat, so rows that come in the
|
|
7652
|
+
* same millisecond (a hold pickup, a group's catch-up) keep the order they came in.
|
|
7653
|
+
*/
|
|
7654
|
+
function arrivalKey(now, last = 0) {
|
|
7655
|
+
return Math.max(now, last + 1);
|
|
7656
|
+
}
|
|
7657
|
+
/** The time a message shows: what its sender said, never later than when it came. A row of mine shows its own time. */
|
|
7658
|
+
function shownTime(message) {
|
|
7659
|
+
return message.sentAt === void 0 ? message.timestamp : Math.min(message.sentAt, message.timestamp);
|
|
7660
|
+
}
|
|
7661
|
+
/**
|
|
7662
|
+
* When something a peer dated happened (an edit), for this device: what the peer says, now at the latest, and now
|
|
7663
|
+
* when it says nothing a date can hold.
|
|
7664
|
+
*/
|
|
7665
|
+
function heardTime(ts, now = Date.now()) {
|
|
7666
|
+
return Math.min(claimedTime(ts) ?? now, now);
|
|
7667
|
+
}
|
|
7668
|
+
//#endregion
|
|
7669
|
+
//#region ../core/src/statusCards.ts
|
|
7670
|
+
var STATUS_CARD_LIMITS = {
|
|
7671
|
+
/** A card's JSON, as sent (UTF-8 bytes): with the fallback text it stays well inside every message bound. */
|
|
7672
|
+
bytes: 8192,
|
|
7673
|
+
/** The sender's id for the card: stable across its updates. */
|
|
7674
|
+
id: 64,
|
|
7675
|
+
title: 120,
|
|
7676
|
+
/** The current step, a list item, a run's summary. */
|
|
7677
|
+
line: 200,
|
|
7678
|
+
branch: 120,
|
|
7679
|
+
schedule: 80,
|
|
7680
|
+
cron: 64,
|
|
7681
|
+
linkLabel: 40,
|
|
7682
|
+
url: 512,
|
|
7683
|
+
items: 20,
|
|
7684
|
+
links: 4,
|
|
7685
|
+
runs: 10,
|
|
7686
|
+
/** Any count on a card (a PR's number, lines, files; steps). */
|
|
7687
|
+
count: 1e9,
|
|
7688
|
+
/** How far ahead a routine's next run may be. */
|
|
7689
|
+
nextRunMs: 316224e5,
|
|
7690
|
+
/** Edits a message with a card takes (a text takes `MAX_EDITS_PER_MESSAGE`): a bot updates a long task often. */
|
|
7691
|
+
edits: 5e3,
|
|
7692
|
+
/** Labels on a task, and a label's length in characters. */
|
|
7693
|
+
tags: 3,
|
|
7694
|
+
tag: 24,
|
|
7695
|
+
/** Buttons on a message (WISP 406 · Message Buttons). */
|
|
7696
|
+
buttons: 6,
|
|
7697
|
+
/** A button's id: what a press names. */
|
|
7698
|
+
buttonId: 32,
|
|
7699
|
+
/** A button's label, in characters. */
|
|
7700
|
+
buttonLabel: 40,
|
|
7701
|
+
/** An app card's title and version, in characters (WISP 405 § An app): the manifest's bounds. */
|
|
7702
|
+
appTitle: 40,
|
|
7703
|
+
appVersion: 32,
|
|
7704
|
+
/** A usage card's label ("Claude") and account, in characters (WISP 405 § Usage). */
|
|
7705
|
+
usageLabel: 24,
|
|
7706
|
+
/** A usage window's name ("5 h", "week"), in characters. */
|
|
7707
|
+
usageWindow: 16,
|
|
7708
|
+
/** Windows beside a usage card's own, shown in its details. */
|
|
7709
|
+
usageWindows: 3
|
|
7710
|
+
};
|
|
7711
|
+
/** How a button looks: `primary` is the one highlighted, `danger` warns; `neutral` when none is said. */
|
|
7712
|
+
var BUTTON_STYLES = [
|
|
7713
|
+
"primary",
|
|
7714
|
+
"neutral",
|
|
7715
|
+
"danger"
|
|
7716
|
+
];
|
|
7717
|
+
var TASK_STATUSES = [
|
|
7718
|
+
"queued",
|
|
7719
|
+
"running",
|
|
7720
|
+
"blocked",
|
|
7721
|
+
"done",
|
|
7722
|
+
"failed",
|
|
7723
|
+
"cancelled"
|
|
7724
|
+
];
|
|
7725
|
+
/** A task that is still going: shown first, and counted on the chat's Tasks button. */
|
|
7726
|
+
var ACTIVE_TASK_STATUSES = [
|
|
7727
|
+
"queued",
|
|
7728
|
+
"running",
|
|
7729
|
+
"blocked"
|
|
7730
|
+
];
|
|
7731
|
+
var ITEM_STATES = [
|
|
7732
|
+
"pending",
|
|
7733
|
+
"running",
|
|
7734
|
+
"done",
|
|
7735
|
+
"failed",
|
|
7736
|
+
"skipped"
|
|
7737
|
+
];
|
|
7738
|
+
var ROUTINE_STATES = ["active", "paused"];
|
|
7739
|
+
var RUN_RESULTS = [
|
|
7740
|
+
"ok",
|
|
7741
|
+
"failed",
|
|
7742
|
+
"skipped"
|
|
7743
|
+
];
|
|
7744
|
+
/**
|
|
7745
|
+
* Where a task's pull request stands: `draft` (not ready for review), `open` (ready for review), `merged`, `closed`
|
|
7746
|
+
* (closed without a merge). A reader that does not know the field shows the pull request as before.
|
|
7747
|
+
*/
|
|
7748
|
+
var PR_STATES = [
|
|
7749
|
+
"draft",
|
|
7750
|
+
"open",
|
|
7751
|
+
"merged",
|
|
7752
|
+
"closed"
|
|
7753
|
+
];
|
|
7754
|
+
/** How a pull request's checks stand. */
|
|
7755
|
+
var PR_CHECKS = [
|
|
7756
|
+
"passing",
|
|
7757
|
+
"failing",
|
|
7758
|
+
"pending"
|
|
7759
|
+
];
|
|
7760
|
+
var ID$4 = /^[A-Za-z0-9_.:][A-Za-z0-9_.:-]{0,63}$/;
|
|
7761
|
+
/** A button's id: 1 to 32 of `A-Z a-z 0-9 _ . : -`, not starting with `-`. */
|
|
7762
|
+
var BUTTON_ID = /^[A-Za-z0-9_.:][A-Za-z0-9_.:-]{0,31}$/;
|
|
7763
|
+
var CRON = /^[0-9A-Za-z*,/?#\- ]{1,64}$/;
|
|
7764
|
+
/**
|
|
7765
|
+
* Whether a typed answer is a button's label or id (WISP 406 · Message Buttons): ignoring case and spaces at the ends.
|
|
7766
|
+
* Not the device's locale's case: the presser's and the author's apps must agree ("I" is not "ı" anywhere).
|
|
7767
|
+
*/
|
|
7768
|
+
var sameButtonText = (a, b) => a.trim().toLowerCase() === b.trim().toLowerCase();
|
|
7769
|
+
/**
|
|
7770
|
+
* Two buttons a typed answer could not tell apart, or none: a label that reads as an earlier label (`label`), or a
|
|
7771
|
+
* label that reads as another button's id (`id`), both as `sameButtonText` compares them. The labels as a reader keeps
|
|
7772
|
+
* them (one line, cleaned). `at` is the button that clashes, `with` the one it clashes with.
|
|
7773
|
+
*/
|
|
7774
|
+
function buttonLabelClash(buttons) {
|
|
7775
|
+
const labels = buttons.map((button) => isObject$4(button) ? cardLine(button.label, Infinity) : void 0);
|
|
7776
|
+
const ids = buttons.map((button) => isObject$4(button) && typeof button.id === "string" ? button.id : void 0);
|
|
7777
|
+
for (const [at, label] of labels.entries()) {
|
|
7778
|
+
if (!label) continue;
|
|
7779
|
+
const twin = labels.findIndex((other, i) => i < at && !!other && sameButtonText(other, label));
|
|
7780
|
+
if (twin !== -1) return {
|
|
7781
|
+
at,
|
|
7782
|
+
with: twin,
|
|
7783
|
+
kind: "label"
|
|
7784
|
+
};
|
|
7785
|
+
const named = ids.findIndex((id, i) => i !== at && id !== void 0 && sameButtonText(id, label));
|
|
7786
|
+
if (named !== -1) return {
|
|
7787
|
+
at,
|
|
7788
|
+
with: named,
|
|
7789
|
+
kind: "id"
|
|
7790
|
+
};
|
|
7791
|
+
}
|
|
7792
|
+
}
|
|
7793
|
+
/** An app card's id, made from its reference: the app's name, a dot, the first 16 characters of the publisher key. */
|
|
7794
|
+
function appCardId(ref) {
|
|
7795
|
+
return `${ref.slice(53)}.${ref.slice(0, 16)}`;
|
|
7796
|
+
}
|
|
7797
|
+
/** An app card's `sequence`: a whole number from 0 to 2^53 - 1. */
|
|
7798
|
+
var appSequence = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
7799
|
+
var isObject$4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7800
|
+
/** One line of display text: runs of space (line breaks included) become one space, then the display rules for names. */
|
|
7801
|
+
function cardLine(value, max) {
|
|
7802
|
+
if (typeof value !== "string") return void 0;
|
|
7803
|
+
return sanitizeDisplayText(value.replace(/\s+/gu, " "), max);
|
|
7804
|
+
}
|
|
7805
|
+
/**
|
|
7806
|
+
* An https link a card may show: no credentials, a host, no spaces or control characters, at most
|
|
7807
|
+
* `STATUS_CARD_LIMITS.url` characters. Returned as the parser writes it (a non-ASCII host in punycode).
|
|
7808
|
+
*/
|
|
7809
|
+
function cardUrl(value) {
|
|
7810
|
+
if (typeof value !== "string" || !value || value.length > STATUS_CARD_LIMITS.url || /[\s\p{Cc}\p{Cf}]/u.test(value)) return void 0;
|
|
7811
|
+
let url;
|
|
7812
|
+
try {
|
|
7813
|
+
url = new URL(value);
|
|
7814
|
+
} catch {
|
|
7815
|
+
return;
|
|
7816
|
+
}
|
|
7817
|
+
if (url.protocol !== "https:" || !url.hostname || url.username || url.password) return void 0;
|
|
7818
|
+
return url.href.length <= STATUS_CARD_LIMITS.url ? url.href : void 0;
|
|
7819
|
+
}
|
|
7820
|
+
var count = (value, min = 0) => typeof value === "number" && Number.isSafeInteger(value) && value >= min && value <= STATUS_CARD_LIMITS.count ? value : void 0;
|
|
7821
|
+
/** A time on a card as a reader keeps it: a positive whole number, never later than `latest`. */
|
|
7822
|
+
var time = (value, latest) => typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? Math.min(value, latest) : void 0;
|
|
7823
|
+
var oneOf = (values, value) => typeof value === "string" && values.includes(value) ? value : void 0;
|
|
7824
|
+
function readLinks(raw) {
|
|
7825
|
+
if (!Array.isArray(raw)) return [];
|
|
7826
|
+
const links = [];
|
|
7827
|
+
for (const entry of raw.slice(0, STATUS_CARD_LIMITS.links)) {
|
|
7828
|
+
if (!isObject$4(entry)) continue;
|
|
7829
|
+
const url = cardUrl(entry.url);
|
|
7830
|
+
if (!url) continue;
|
|
7831
|
+
const label = cardLine(entry.label, STATUS_CARD_LIMITS.linkLabel);
|
|
7832
|
+
links.push({
|
|
7833
|
+
url,
|
|
7834
|
+
...label && { label }
|
|
7835
|
+
});
|
|
7836
|
+
}
|
|
7837
|
+
return links;
|
|
7838
|
+
}
|
|
7839
|
+
/** A task's labels as a reader keeps them: the first `STATUS_CARD_LIMITS.tags` that hold, each one clean line, none twice. */
|
|
7840
|
+
function readTags(raw) {
|
|
7841
|
+
if (!Array.isArray(raw)) return [];
|
|
7842
|
+
const tags = [];
|
|
7843
|
+
for (const entry of raw) {
|
|
7844
|
+
const tag = cardLine(entry, STATUS_CARD_LIMITS.tag);
|
|
7845
|
+
if (tag && !tags.includes(tag)) tags.push(tag);
|
|
7846
|
+
if (tags.length === STATUS_CARD_LIMITS.tags) break;
|
|
7847
|
+
}
|
|
7848
|
+
return tags;
|
|
7849
|
+
}
|
|
7850
|
+
function readRun(raw, now) {
|
|
7851
|
+
if (!isObject$4(raw)) return void 0;
|
|
7852
|
+
const at = time(raw.at, now + MESSAGE_CLOCK_SKEW_MS), result = oneOf(RUN_RESULTS, raw.result);
|
|
7853
|
+
if (!at || !result) return void 0;
|
|
7854
|
+
const summary = cardLine(raw.summary, STATUS_CARD_LIMITS.line);
|
|
7855
|
+
return {
|
|
7856
|
+
at,
|
|
7857
|
+
result,
|
|
7858
|
+
...summary && { summary }
|
|
7859
|
+
};
|
|
7860
|
+
}
|
|
7861
|
+
/** A percent as a reader keeps it: a finite number, rounded and clamped into 0 to 100. */
|
|
7862
|
+
var percent = (value) => typeof value === "number" && Number.isFinite(value) ? Math.round(Math.min(100, Math.max(0, value))) : void 0;
|
|
7863
|
+
/** A time ahead of the reader (a reset, a next run) as it keeps it: whole, positive, at most a year ahead. */
|
|
7864
|
+
var ahead = (value, now) => typeof value === "number" && Number.isSafeInteger(value) && value > 0 && value <= now + STATUS_CARD_LIMITS.nextRunMs ? value : void 0;
|
|
7865
|
+
/** A usage card as a reader keeps it (WISP 405 § Usage): dropped without a percent left or a used of limit. */
|
|
7866
|
+
function readUsage(raw, id, now) {
|
|
7867
|
+
const left = percent(raw.left);
|
|
7868
|
+
let used = count(raw.used), limit = count(raw.limit, 1);
|
|
7869
|
+
if (used === void 0 || limit === void 0 || used > limit) used = limit = void 0;
|
|
7870
|
+
if (left === void 0 && limit === void 0) return void 0;
|
|
7871
|
+
const label = cardLine(raw.label, STATUS_CARD_LIMITS.usageLabel), account = cardLine(raw.account, STATUS_CARD_LIMITS.usageLabel);
|
|
7872
|
+
const window = cardLine(raw.window, STATUS_CARD_LIMITS.usageWindow);
|
|
7873
|
+
const resetsAt = ahead(raw.resetsAt, now), updatedAt = time(raw.updatedAt, now + MESSAGE_CLOCK_SKEW_MS);
|
|
7874
|
+
const windows = [];
|
|
7875
|
+
if (Array.isArray(raw.windows)) for (const entry of raw.windows.slice(0, STATUS_CARD_LIMITS.usageWindows)) {
|
|
7876
|
+
if (!isObject$4(entry)) continue;
|
|
7877
|
+
const name = cardLine(entry.window, STATUS_CARD_LIMITS.usageWindow), share = percent(entry.left);
|
|
7878
|
+
if (!name || share === void 0) continue;
|
|
7879
|
+
const at = ahead(entry.resetsAt, now);
|
|
7880
|
+
windows.push({
|
|
7881
|
+
window: name,
|
|
7882
|
+
left: share,
|
|
7883
|
+
...at && { resetsAt: at }
|
|
7884
|
+
});
|
|
7885
|
+
}
|
|
7886
|
+
return {
|
|
7887
|
+
kind: "usage",
|
|
7888
|
+
id,
|
|
7889
|
+
...left !== void 0 && { left },
|
|
7890
|
+
...used !== void 0 && limit !== void 0 && {
|
|
7891
|
+
used,
|
|
7892
|
+
limit
|
|
7893
|
+
},
|
|
7894
|
+
...label && { label },
|
|
7895
|
+
...account && { account },
|
|
7896
|
+
...window && { window },
|
|
7897
|
+
...resetsAt && { resetsAt },
|
|
7898
|
+
...updatedAt && { updatedAt },
|
|
7899
|
+
...windows.length && { windows }
|
|
7900
|
+
};
|
|
7901
|
+
}
|
|
7902
|
+
/** How much of a usage card's quota is left, 0 to 100: its own percent, else worked out from used of limit. */
|
|
7903
|
+
function usageLeft(card) {
|
|
7904
|
+
if (card.left !== void 0) return card.left;
|
|
7905
|
+
return card.used !== void 0 && card.limit ? Math.round((card.limit - card.used) / card.limit * 100) : 0;
|
|
7906
|
+
}
|
|
7907
|
+
/** The bytes a card takes on the wire: what `STATUS_CARD_LIMITS.bytes` bounds. */
|
|
7908
|
+
function statusCardBytes(card) {
|
|
7909
|
+
try {
|
|
7910
|
+
return utf8Encode(JSON.stringify(card) ?? "").length;
|
|
7911
|
+
} catch {
|
|
7912
|
+
return Infinity;
|
|
7913
|
+
}
|
|
6795
7914
|
}
|
|
6796
7915
|
/**
|
|
6797
|
-
*
|
|
6798
|
-
*
|
|
6799
|
-
|
|
6800
|
-
|
|
6801
|
-
* The highest sequence number a frame of my own key, sent by another copy of my profile, may make me adopt (WISP 06):
|
|
6802
|
-
* the highest floor a copy can have (2^32 - 2^24) and room above it. The community beacon packs the counter in 32 bits.
|
|
7916
|
+
* A card as a reader takes it from the wire (`sc`), or undefined when it cannot stand: not an object, past
|
|
7917
|
+
* `STATUS_CARD_LIMITS.bytes`, a kind this app does not know, no valid id, title or name, status or state. Fields that do
|
|
7918
|
+
* not hold are left out and lists are cut to their bound; times are kept no later than the reader's clock allows (a
|
|
7919
|
+
* routine's next run up to a year ahead). The message is always kept: without a card it shows its text.
|
|
6803
7920
|
*/
|
|
6804
|
-
|
|
7921
|
+
function readStatusCard(raw, now = Date.now()) {
|
|
7922
|
+
if (!isObject$4(raw) || statusCardBytes(raw) > STATUS_CARD_LIMITS.bytes) return void 0;
|
|
7923
|
+
const id = typeof raw.id === "string" && ID$4.test(raw.id) ? raw.id : void 0;
|
|
7924
|
+
if (!id) return void 0;
|
|
7925
|
+
if (raw.kind === "app") {
|
|
7926
|
+
if (!isAppRef(raw.ref) || id !== appCardId(raw.ref) || !isAppHash(raw.digest) || !appSequence(raw.sequence)) return void 0;
|
|
7927
|
+
const title = cardLine(raw.title, STATUS_CARD_LIMITS.appTitle);
|
|
7928
|
+
if (!title) return void 0;
|
|
7929
|
+
const version = cardLine(raw.version, STATUS_CARD_LIMITS.appVersion), url = cardUrl(raw.url);
|
|
7930
|
+
return {
|
|
7931
|
+
kind: "app",
|
|
7932
|
+
id,
|
|
7933
|
+
ref: raw.ref,
|
|
7934
|
+
digest: raw.digest,
|
|
7935
|
+
sequence: raw.sequence,
|
|
7936
|
+
title,
|
|
7937
|
+
...version && { version },
|
|
7938
|
+
...url && { url },
|
|
7939
|
+
...raw.opened === true && { opened: true }
|
|
7940
|
+
};
|
|
7941
|
+
}
|
|
7942
|
+
const latest = now + MESSAGE_CLOCK_SKEW_MS;
|
|
7943
|
+
if (raw.kind === "usage") return readUsage(raw, id, now);
|
|
7944
|
+
const links = readLinks(raw.links);
|
|
7945
|
+
if (raw.kind === "task") {
|
|
7946
|
+
const title = cardLine(raw.title, STATUS_CARD_LIMITS.title), status = oneOf(TASK_STATUSES, raw.status);
|
|
7947
|
+
if (!title || !status) return void 0;
|
|
7948
|
+
const progress = typeof raw.progress === "number" && Number.isFinite(raw.progress) ? Math.round(Math.min(100, Math.max(0, raw.progress))) : void 0;
|
|
7949
|
+
let done = count(raw.done), total = count(raw.total, 1);
|
|
7950
|
+
if (done === void 0 || total === void 0 || done > total) done = total = void 0;
|
|
7951
|
+
const step = cardLine(raw.step, STATUS_CARD_LIMITS.line), branch = cardLine(raw.branch, STATUS_CARD_LIMITS.branch);
|
|
7952
|
+
let pr;
|
|
7953
|
+
if (isObject$4(raw.pr)) {
|
|
7954
|
+
const url = cardUrl(raw.pr.url);
|
|
7955
|
+
if (url) {
|
|
7956
|
+
const number = count(raw.pr.number, 1), additions = count(raw.pr.additions), deletions = count(raw.pr.deletions), files = count(raw.pr.files);
|
|
7957
|
+
const state = oneOf(PR_STATES, raw.pr.state), checks = oneOf(PR_CHECKS, raw.pr.checks);
|
|
7958
|
+
pr = {
|
|
7959
|
+
url,
|
|
7960
|
+
...number !== void 0 && { number },
|
|
7961
|
+
...additions !== void 0 && { additions },
|
|
7962
|
+
...deletions !== void 0 && { deletions },
|
|
7963
|
+
...files !== void 0 && { files },
|
|
7964
|
+
...state && { state },
|
|
7965
|
+
...checks && { checks }
|
|
7966
|
+
};
|
|
7967
|
+
}
|
|
7968
|
+
}
|
|
7969
|
+
const items = [];
|
|
7970
|
+
if (Array.isArray(raw.items)) for (const entry of raw.items.slice(0, STATUS_CARD_LIMITS.items)) {
|
|
7971
|
+
if (!isObject$4(entry)) continue;
|
|
7972
|
+
const text = cardLine(entry.text, STATUS_CARD_LIMITS.line);
|
|
7973
|
+
if (text) items.push({
|
|
7974
|
+
text,
|
|
7975
|
+
state: oneOf(ITEM_STATES, entry.state) ?? "pending"
|
|
7976
|
+
});
|
|
7977
|
+
}
|
|
7978
|
+
const startedAt = time(raw.startedAt, latest), updatedAt = time(raw.updatedAt, latest);
|
|
7979
|
+
const tags = readTags(raw.tags);
|
|
7980
|
+
const parent = typeof raw.parent === "string" && ID$4.test(raw.parent) && raw.parent !== id ? raw.parent : void 0;
|
|
7981
|
+
return {
|
|
7982
|
+
kind: "task",
|
|
7983
|
+
id,
|
|
7984
|
+
title,
|
|
7985
|
+
status,
|
|
7986
|
+
...progress !== void 0 && { progress },
|
|
7987
|
+
...done !== void 0 && total !== void 0 && {
|
|
7988
|
+
done,
|
|
7989
|
+
total
|
|
7990
|
+
},
|
|
7991
|
+
...step && { step },
|
|
7992
|
+
...pr && { pr },
|
|
7993
|
+
...branch && { branch },
|
|
7994
|
+
...startedAt && { startedAt },
|
|
7995
|
+
...updatedAt && { updatedAt },
|
|
7996
|
+
...items.length && { items },
|
|
7997
|
+
...links.length && { links },
|
|
7998
|
+
...tags.length && { tags },
|
|
7999
|
+
...parent && { parent }
|
|
8000
|
+
};
|
|
8001
|
+
}
|
|
8002
|
+
if (raw.kind === "routine") {
|
|
8003
|
+
const name = cardLine(raw.name, STATUS_CARD_LIMITS.title), schedule = cardLine(raw.schedule, STATUS_CARD_LIMITS.schedule), state = oneOf(ROUTINE_STATES, raw.state);
|
|
8004
|
+
if (!name || !schedule || !state) return void 0;
|
|
8005
|
+
const cron = typeof raw.cron === "string" && CRON.test(raw.cron.trim()) ? raw.cron.trim() : void 0;
|
|
8006
|
+
const lastRun = readRun(raw.lastRun, now);
|
|
8007
|
+
const nextRunAt = typeof raw.nextRunAt === "number" && Number.isSafeInteger(raw.nextRunAt) && raw.nextRunAt > 0 && raw.nextRunAt <= now + STATUS_CARD_LIMITS.nextRunMs ? raw.nextRunAt : void 0;
|
|
8008
|
+
const runs = [];
|
|
8009
|
+
if (Array.isArray(raw.runs)) for (const entry of raw.runs.slice(0, STATUS_CARD_LIMITS.runs)) {
|
|
8010
|
+
const run = readRun(entry, now);
|
|
8011
|
+
if (run) runs.push(run);
|
|
8012
|
+
}
|
|
8013
|
+
return {
|
|
8014
|
+
kind: "routine",
|
|
8015
|
+
id,
|
|
8016
|
+
name,
|
|
8017
|
+
schedule,
|
|
8018
|
+
state,
|
|
8019
|
+
...cron && { cron },
|
|
8020
|
+
...lastRun && { lastRun },
|
|
8021
|
+
...nextRunAt && { nextRunAt },
|
|
8022
|
+
...runs.length && { runs },
|
|
8023
|
+
...links.length && { links }
|
|
8024
|
+
};
|
|
8025
|
+
}
|
|
8026
|
+
if (raw.kind === "buttons") {
|
|
8027
|
+
if (!Array.isArray(raw.buttons)) return void 0;
|
|
8028
|
+
const buttons = [], seen = /* @__PURE__ */ new Set();
|
|
8029
|
+
for (const entry of raw.buttons.slice(0, STATUS_CARD_LIMITS.buttons)) {
|
|
8030
|
+
if (!isObject$4(entry) || typeof entry.id !== "string" || !BUTTON_ID.test(entry.id) || seen.has(entry.id)) continue;
|
|
8031
|
+
const label = cardLine(entry.label, STATUS_CARD_LIMITS.buttonLabel);
|
|
8032
|
+
if (!label) continue;
|
|
8033
|
+
seen.add(entry.id);
|
|
8034
|
+
const style = oneOf(BUTTON_STYLES, entry.style);
|
|
8035
|
+
buttons.push({
|
|
8036
|
+
id: entry.id,
|
|
8037
|
+
label,
|
|
8038
|
+
...style && style !== "neutral" && { style },
|
|
8039
|
+
...entry.once === true && { once: true }
|
|
8040
|
+
});
|
|
8041
|
+
}
|
|
8042
|
+
if (!buttons.length) return void 0;
|
|
8043
|
+
const chosen = typeof raw.chosen === "string" && seen.has(raw.chosen) ? raw.chosen : void 0;
|
|
8044
|
+
return {
|
|
8045
|
+
kind: "buttons",
|
|
8046
|
+
id,
|
|
8047
|
+
buttons,
|
|
8048
|
+
...chosen && { chosen },
|
|
8049
|
+
...raw.closed === true && { closed: true }
|
|
8050
|
+
};
|
|
8051
|
+
}
|
|
8052
|
+
}
|
|
6805
8053
|
/**
|
|
6806
|
-
*
|
|
6807
|
-
*
|
|
6808
|
-
*
|
|
6809
|
-
* brings commits one by one, and a frame said again at the first of them would still miss those let in by the next.
|
|
8054
|
+
* 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
|
|
8055
|
+
* here instead (an unknown status, a link that is not https, a list past its bound, a line too long), so a bot learns
|
|
8056
|
+
* at once and nothing it said is lost on the way. Line breaks and invisible characters are cleaned, as a reader would.
|
|
6810
8057
|
*/
|
|
6811
|
-
|
|
6812
|
-
|
|
6813
|
-
|
|
8058
|
+
function checkStatusCard(raw, now = Date.now()) {
|
|
8059
|
+
if (!isObject$4(raw)) return { error: "A card is an object" };
|
|
8060
|
+
const bytes = statusCardBytes(raw);
|
|
8061
|
+
if (bytes > STATUS_CARD_LIMITS.bytes) return { error: `The card takes ${bytes} bytes; at most ${STATUS_CARD_LIMITS.bytes}` };
|
|
8062
|
+
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" };
|
|
8063
|
+
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 -` };
|
|
8064
|
+
const line = (field, value, max, required = false) => {
|
|
8065
|
+
if (value === void 0 && !required) return null;
|
|
8066
|
+
if (typeof value !== "string" || !cardLine(value, Infinity)) return `${field} is text`;
|
|
8067
|
+
if ([...cardLine(value, Infinity)].length > max) return `${field} is at most ${max} characters`;
|
|
8068
|
+
return null;
|
|
8069
|
+
};
|
|
8070
|
+
const url = (field, value) => cardUrl(value) ? null : `${field} must be an https link of at most ${STATUS_CARD_LIMITS.url} characters`;
|
|
8071
|
+
const whole = (field, value, min = 0) => value === void 0 || count(value, min) !== void 0 ? null : `${field} is a whole number from ${min}`;
|
|
8072
|
+
const errors = [];
|
|
8073
|
+
if (raw.links !== void 0) {
|
|
8074
|
+
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}`);
|
|
8075
|
+
else for (const [i, link] of raw.links.entries()) {
|
|
8076
|
+
if (!isObject$4(link)) {
|
|
8077
|
+
errors.push(`links[${i}] is an object`);
|
|
8078
|
+
continue;
|
|
8079
|
+
}
|
|
8080
|
+
errors.push(url(`links[${i}].url`, link.url), line(`links[${i}].label`, link.label, STATUS_CARD_LIMITS.linkLabel));
|
|
8081
|
+
}
|
|
8082
|
+
}
|
|
8083
|
+
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`;
|
|
8084
|
+
const reset = (field, value) => value === void 0 || ahead(value, now) !== void 0 ? null : `${field} is a time in milliseconds, within a year`;
|
|
8085
|
+
if (raw.kind === "usage") {
|
|
8086
|
+
if (raw.links !== void 0) errors.push("a usage card takes no links");
|
|
8087
|
+
if (raw.left === void 0 && raw.used === void 0) errors.push("left (a percent) or used and limit");
|
|
8088
|
+
errors.push(share("left", raw.left));
|
|
8089
|
+
if (raw.used === void 0 !== (raw.limit === void 0)) errors.push("used and limit go together");
|
|
8090
|
+
else if (raw.used !== void 0) {
|
|
8091
|
+
errors.push(whole("used", raw.used), whole("limit", raw.limit, 1));
|
|
8092
|
+
if (typeof raw.used === "number" && typeof raw.limit === "number" && raw.used > raw.limit) errors.push("used is at most limit");
|
|
8093
|
+
}
|
|
8094
|
+
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));
|
|
8095
|
+
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");
|
|
8096
|
+
if (raw.windows !== void 0) {
|
|
8097
|
+
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}`);
|
|
8098
|
+
else for (const [i, entry] of raw.windows.entries()) {
|
|
8099
|
+
if (!isObject$4(entry)) {
|
|
8100
|
+
errors.push(`windows[${i}] is an object`);
|
|
8101
|
+
continue;
|
|
8102
|
+
}
|
|
8103
|
+
errors.push(line(`windows[${i}].window`, entry.window, STATUS_CARD_LIMITS.usageWindow, true));
|
|
8104
|
+
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));
|
|
8105
|
+
}
|
|
8106
|
+
}
|
|
8107
|
+
} else if (raw.kind === "app") {
|
|
8108
|
+
if (raw.links !== void 0) errors.push("an app card takes no links");
|
|
8109
|
+
if (!isAppRef(raw.ref)) errors.push("ref is <publisher key in z-base32>/<name>");
|
|
8110
|
+
else if (raw.id !== appCardId(raw.ref)) errors.push(`id is ${appCardId(raw.ref)}, made from ref`);
|
|
8111
|
+
if (!isAppHash(raw.digest)) errors.push("digest is a SHA-256 in base64url without padding");
|
|
8112
|
+
if (!appSequence(raw.sequence)) errors.push("sequence is a whole number from 0 to 2^53 - 1");
|
|
8113
|
+
errors.push(line("title", raw.title, STATUS_CARD_LIMITS.appTitle, true), line("version", raw.version, STATUS_CARD_LIMITS.appVersion));
|
|
8114
|
+
if (raw.url !== void 0) errors.push(url("url", raw.url));
|
|
8115
|
+
if (raw.opened !== void 0 && raw.opened !== true) errors.push("opened is true or left out");
|
|
8116
|
+
} else if (raw.kind === "buttons") {
|
|
8117
|
+
if (raw.links !== void 0) errors.push("buttons take no links");
|
|
8118
|
+
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}`);
|
|
8119
|
+
else {
|
|
8120
|
+
const ids = /* @__PURE__ */ new Set();
|
|
8121
|
+
for (const [i, button] of raw.buttons.entries()) {
|
|
8122
|
+
if (!isObject$4(button)) {
|
|
8123
|
+
errors.push(`buttons[${i}] is an object`);
|
|
8124
|
+
continue;
|
|
8125
|
+
}
|
|
8126
|
+
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 -`);
|
|
8127
|
+
else if (ids.has(button.id)) errors.push(`buttons[${i}].id "${button.id}" is used twice`);
|
|
8128
|
+
else ids.add(button.id);
|
|
8129
|
+
errors.push(line(`buttons[${i}].label`, button.label, STATUS_CARD_LIMITS.buttonLabel, true));
|
|
8130
|
+
if (button.style !== void 0 && !oneOf(BUTTON_STYLES, button.style)) errors.push(`buttons[${i}].style is one of ${BUTTON_STYLES.join(", ")}`);
|
|
8131
|
+
if (button.once !== void 0 && button.once !== true) errors.push(`buttons[${i}].once is true or left out`);
|
|
8132
|
+
}
|
|
8133
|
+
const clash = buttonLabelClash(raw.buttons);
|
|
8134
|
+
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)`);
|
|
8135
|
+
if (raw.chosen !== void 0 && !(typeof raw.chosen === "string" && ids.has(raw.chosen))) errors.push("chosen names one of the buttons");
|
|
8136
|
+
}
|
|
8137
|
+
if (raw.closed !== void 0 && raw.closed !== true) errors.push("closed is true or left out");
|
|
8138
|
+
} else if (raw.kind === "task") {
|
|
8139
|
+
errors.push(line("title", raw.title, STATUS_CARD_LIMITS.title, true));
|
|
8140
|
+
if (!oneOf(TASK_STATUSES, raw.status)) errors.push(`status is one of ${TASK_STATUSES.join(", ")}`);
|
|
8141
|
+
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");
|
|
8142
|
+
if (raw.done === void 0 !== (raw.total === void 0)) errors.push("done and total go together");
|
|
8143
|
+
else if (raw.done !== void 0) {
|
|
8144
|
+
errors.push(whole("done", raw.done), whole("total", raw.total, 1));
|
|
8145
|
+
if (typeof raw.done === "number" && typeof raw.total === "number" && raw.done > raw.total) errors.push("done is at most total");
|
|
8146
|
+
}
|
|
8147
|
+
errors.push(line("step", raw.step, STATUS_CARD_LIMITS.line), line("branch", raw.branch, STATUS_CARD_LIMITS.branch));
|
|
8148
|
+
if (raw.pr !== void 0) {
|
|
8149
|
+
if (!isObject$4(raw.pr)) errors.push("pr is an object");
|
|
8150
|
+
else {
|
|
8151
|
+
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));
|
|
8152
|
+
if (raw.pr.state !== void 0 && !oneOf(PR_STATES, raw.pr.state)) errors.push(`pr.state is one of ${PR_STATES.join(", ")}`);
|
|
8153
|
+
if (raw.pr.checks !== void 0 && !oneOf(PR_CHECKS, raw.pr.checks)) errors.push(`pr.checks is one of ${PR_CHECKS.join(", ")}`);
|
|
8154
|
+
}
|
|
8155
|
+
}
|
|
8156
|
+
if (raw.tags !== void 0) {
|
|
8157
|
+
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}`);
|
|
8158
|
+
else {
|
|
8159
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8160
|
+
for (const [i, tag] of raw.tags.entries()) {
|
|
8161
|
+
const problem = line(`tags[${i}]`, tag, STATUS_CARD_LIMITS.tag, true);
|
|
8162
|
+
errors.push(problem);
|
|
8163
|
+
if (problem) continue;
|
|
8164
|
+
const clean = cardLine(tag, Infinity);
|
|
8165
|
+
if (seen.has(clean)) errors.push(`tags[${i}] ${JSON.stringify(clean)} is there twice`);
|
|
8166
|
+
seen.add(clean);
|
|
8167
|
+
}
|
|
8168
|
+
}
|
|
8169
|
+
}
|
|
8170
|
+
if (raw.parent !== void 0) {
|
|
8171
|
+
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 -`);
|
|
8172
|
+
else if (raw.parent === raw.id) errors.push("parent is another task's id, not its own");
|
|
8173
|
+
}
|
|
8174
|
+
for (const field of ["startedAt", "updatedAt"]) {
|
|
8175
|
+
const value = raw[field];
|
|
8176
|
+
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`);
|
|
8177
|
+
}
|
|
8178
|
+
if (raw.items !== void 0) {
|
|
8179
|
+
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}`);
|
|
8180
|
+
else for (const [i, item] of raw.items.entries()) {
|
|
8181
|
+
if (!isObject$4(item)) {
|
|
8182
|
+
errors.push(`items[${i}] is an object`);
|
|
8183
|
+
continue;
|
|
8184
|
+
}
|
|
8185
|
+
errors.push(line(`items[${i}].text`, item.text, STATUS_CARD_LIMITS.line, true));
|
|
8186
|
+
if (item.state !== void 0 && !oneOf(ITEM_STATES, item.state)) errors.push(`items[${i}].state is one of ${ITEM_STATES.join(", ")}`);
|
|
8187
|
+
}
|
|
8188
|
+
}
|
|
8189
|
+
} else {
|
|
8190
|
+
errors.push(line("name", raw.name, STATUS_CARD_LIMITS.title, true), line("schedule", raw.schedule, STATUS_CARD_LIMITS.schedule, true));
|
|
8191
|
+
if (!oneOf(ROUTINE_STATES, raw.state)) errors.push(`state is one of ${ROUTINE_STATES.join(", ")}`);
|
|
8192
|
+
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`);
|
|
8193
|
+
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");
|
|
8194
|
+
const run = (field, value) => {
|
|
8195
|
+
if (!isObject$4(value)) return errors.push(`${field} is an object`);
|
|
8196
|
+
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`);
|
|
8197
|
+
if (!oneOf(RUN_RESULTS, value.result)) errors.push(`${field}.result is one of ${RUN_RESULTS.join(", ")}`);
|
|
8198
|
+
errors.push(line(`${field}.summary`, value.summary, STATUS_CARD_LIMITS.line));
|
|
8199
|
+
};
|
|
8200
|
+
if (raw.lastRun !== void 0) run("lastRun", raw.lastRun);
|
|
8201
|
+
if (raw.runs !== void 0) {
|
|
8202
|
+
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}`);
|
|
8203
|
+
else raw.runs.forEach((entry, i) => run(`runs[${i}]`, entry));
|
|
8204
|
+
}
|
|
8205
|
+
}
|
|
8206
|
+
const error = errors.find((e) => typeof e === "string");
|
|
8207
|
+
if (error) return { error };
|
|
8208
|
+
const card = readStatusCard(raw, now);
|
|
8209
|
+
return card ? { card } : { error: "The card does not hold" };
|
|
8210
|
+
}
|
|
8211
|
+
/** The progress a task shows, 0 to 100: its own, else its steps done of total; undefined when it says neither. */
|
|
8212
|
+
function taskProgress(card) {
|
|
8213
|
+
if (card.progress !== void 0) return card.progress;
|
|
8214
|
+
return card.done !== void 0 && card.total ? Math.round(card.done / card.total * 100) : void 0;
|
|
8215
|
+
}
|
|
8216
|
+
var TASK_MARK = {
|
|
8217
|
+
queued: "⏳",
|
|
8218
|
+
running: "🔄",
|
|
8219
|
+
blocked: "⛔",
|
|
8220
|
+
done: "✅",
|
|
8221
|
+
failed: "❌",
|
|
8222
|
+
cancelled: "🚫"
|
|
6814
8223
|
};
|
|
6815
|
-
|
|
6816
|
-
|
|
6817
|
-
|
|
8224
|
+
var STATUS_WORD = {
|
|
8225
|
+
queued: "Queued",
|
|
8226
|
+
running: "Running",
|
|
8227
|
+
blocked: "Blocked",
|
|
8228
|
+
done: "Done",
|
|
8229
|
+
failed: "Failed",
|
|
8230
|
+
cancelled: "Cancelled"
|
|
8231
|
+
};
|
|
8232
|
+
var RESULT_WORD = {
|
|
8233
|
+
ok: "ok",
|
|
8234
|
+
failed: "failed",
|
|
8235
|
+
skipped: "skipped"
|
|
8236
|
+
};
|
|
8237
|
+
/** A time as the fallback text says it: in UTC, to the minute ("2026-09-30 01:00 UTC"). */
|
|
8238
|
+
function cardTimeUtc(ms) {
|
|
8239
|
+
return `${new Date(ms).toISOString().slice(0, 16).replace("T", " ")} UTC`;
|
|
8240
|
+
}
|
|
8241
|
+
/**
|
|
8242
|
+
* The text a card's message carries: what an app without cards shows, the DHT floor carries, the chat list previews
|
|
8243
|
+
* and search finds. Plain lines, the first naming the card, no bars or layout that would read badly elsewhere.
|
|
8244
|
+
*/
|
|
8245
|
+
function statusCardText(card) {
|
|
8246
|
+
if (card.kind === "app") {
|
|
8247
|
+
const named = `${card.title}${card.version ? ` ${card.version}` : ""}`;
|
|
8248
|
+
return [card.opened ? `🧩 Opened ${named} in this chat (Ghostly app)` : `🧩 ${named} (Ghostly app)`, ...card.url ? [card.url] : []].join("\n");
|
|
8249
|
+
}
|
|
8250
|
+
if (card.kind === "usage") {
|
|
8251
|
+
const left = `${usageLeft(card)}% left${card.window ? ` (${card.window})` : ""}`;
|
|
8252
|
+
return [
|
|
8253
|
+
`📊 ${card.label ?? "Usage"}`,
|
|
8254
|
+
...card.account ? [card.account] : [],
|
|
8255
|
+
left,
|
|
8256
|
+
...card.used !== void 0 && card.limit !== void 0 ? [`${card.used} of ${card.limit} used`] : [],
|
|
8257
|
+
...card.resetsAt ? [`resets ${cardTimeUtc(card.resetsAt)}`] : [],
|
|
8258
|
+
...(card.windows ?? []).map((w) => `${w.window} ${w.left}%`)
|
|
8259
|
+
].join(" · ");
|
|
8260
|
+
}
|
|
8261
|
+
if (card.kind === "buttons") return `Reply: ${card.buttons.map((b) => b.label).join(" / ")}`;
|
|
8262
|
+
if (card.kind === "task") {
|
|
8263
|
+
const progress = taskProgress(card);
|
|
8264
|
+
const state = [
|
|
8265
|
+
STATUS_WORD[card.status],
|
|
8266
|
+
...progress !== void 0 ? [`${progress}%`] : [],
|
|
8267
|
+
...card.done !== void 0 && card.total ? [`${card.done} of ${card.total} steps`] : []
|
|
8268
|
+
].join(" · ");
|
|
8269
|
+
const standing = card.pr && [...card.pr.state ? [card.pr.state] : [], ...card.pr.checks ? [`checks ${card.pr.checks}`] : []].join(", ");
|
|
8270
|
+
const pr = card.pr && [
|
|
8271
|
+
`PR${card.pr.number !== void 0 ? ` #${card.pr.number}` : ""}`,
|
|
8272
|
+
...card.pr.additions !== void 0 || card.pr.deletions !== void 0 ? [`+${card.pr.additions ?? 0} -${card.pr.deletions ?? 0}`] : [],
|
|
8273
|
+
...standing ? [`(${standing})`] : []
|
|
8274
|
+
].join(" ") + `: ${card.pr.url}`;
|
|
8275
|
+
const now = card.step && ACTIVE_TASK_STATUSES.includes(card.status) ? [`Now: ${card.step}`] : [];
|
|
8276
|
+
return [
|
|
8277
|
+
`${TASK_MARK[card.status]} ${card.title}`,
|
|
8278
|
+
state,
|
|
8279
|
+
...now,
|
|
8280
|
+
...pr ? [pr] : [],
|
|
8281
|
+
...card.tags?.length ? [`Tags: ${card.tags.join(", ")}`] : [],
|
|
8282
|
+
...card.parent ? [`Part of: ${card.parent}`] : []
|
|
8283
|
+
].join("\n");
|
|
8284
|
+
}
|
|
8285
|
+
const last = card.lastRun && `Last run: ${RESULT_WORD[card.lastRun.result]}, ${cardTimeUtc(card.lastRun.at)}${card.lastRun.summary ? ` · ${card.lastRun.summary}` : ""}`;
|
|
8286
|
+
return [
|
|
8287
|
+
`🔁 ${card.name}`,
|
|
8288
|
+
`${card.schedule} · ${card.state === "active" ? "active" : "paused"}`,
|
|
8289
|
+
...last ? [last] : [],
|
|
8290
|
+
...card.nextRunAt && card.state === "active" ? [`Next run: ${cardTimeUtc(card.nextRunAt)}`] : []
|
|
8291
|
+
].join("\n");
|
|
8292
|
+
}
|
|
8293
|
+
/** Whether a message's edit number holds: a text's bound, or a card message's when the edit carries a card. */
|
|
8294
|
+
function cardEditNumber(e) {
|
|
8295
|
+
return Number.isSafeInteger(e) && e >= 1 && e <= STATUS_CARD_LIMITS.edits;
|
|
6818
8296
|
}
|
|
6819
|
-
var GROUP_ADMIN_OFF_ERROR = "Managing this group is off on this device. Turn on Manage groups from this device first.";
|
|
6820
8297
|
//#endregion
|
|
6821
8298
|
//#region ../core/src/replies.ts
|
|
6822
8299
|
var REPLY_LIMITS = {
|
|
@@ -6840,8 +8317,7 @@ var INLINE_PICTURE = /^data:image\//i;
|
|
|
6840
8317
|
*/
|
|
6841
8318
|
function replySnippet(text) {
|
|
6842
8319
|
if (INLINE_PICTURE.test(text.trimStart())) return "🖼️ Picture";
|
|
6843
|
-
const
|
|
6844
|
-
const clean = sanitizeDisplayText(flat, REPLY_LIMITS.snippet + 1) ?? "";
|
|
8320
|
+
const clean = sanitizeDisplayText(text.slice(0, REPLY_LIMITS.raw * 2).replace(/\s+/gu, " "), REPLY_LIMITS.snippet + 1) ?? "";
|
|
6845
8321
|
const points = [...clean];
|
|
6846
8322
|
return points.length > REPLY_LIMITS.snippet ? `${points.slice(0, REPLY_LIMITS.snippet - 1).join("").trimEnd()}…` : clean;
|
|
6847
8323
|
}
|
|
@@ -7217,7 +8693,7 @@ function previewImageSize(value) {
|
|
|
7217
8693
|
if (binary.length > LINK_PREVIEW_LIMITS.imageBytes) return;
|
|
7218
8694
|
const bytes = new Uint8Array(binary.length);
|
|
7219
8695
|
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
7220
|
-
return jpegSize(bytes);
|
|
8696
|
+
return jpegSize$1(bytes);
|
|
7221
8697
|
}
|
|
7222
8698
|
/** The thumbnail if it is one Ghostly shows: a JPEG data URL within the limits whose header declares a sane size. */
|
|
7223
8699
|
function sanitizePreviewImage(value) {
|
|
@@ -7378,6 +8854,483 @@ var RateWindow = class {
|
|
|
7378
8854
|
}
|
|
7379
8855
|
};
|
|
7380
8856
|
//#endregion
|
|
8857
|
+
//#region ../core/src/video.ts
|
|
8858
|
+
var VIDEO_LIMITS = {
|
|
8859
|
+
/** The poster's bytes: with the rest of an offer, a frame stays far below the 60 KiB a session carries. */
|
|
8860
|
+
maxPosterBytes: 12288,
|
|
8861
|
+
/** The poster's longest side, in pixels, as the sender draws it. */
|
|
8862
|
+
posterEdge: 320,
|
|
8863
|
+
/** Ten hours: longer is a mistake, not a video. */
|
|
8864
|
+
maxDurationMs: 36e6,
|
|
8865
|
+
/** Pixels on either side (8K and a bit). */
|
|
8866
|
+
maxEdge: 16384
|
|
8867
|
+
};
|
|
8868
|
+
/** Whether a file is a video, by its type: shown in a video bubble whether or not it came with a description. */
|
|
8869
|
+
function isVideoMime(mime) {
|
|
8870
|
+
return mime.split(";")[0].trim().toLowerCase().startsWith("video/");
|
|
8871
|
+
}
|
|
8872
|
+
var BASE64URL = /^[A-Za-z0-9_-]+$/;
|
|
8873
|
+
var JPEG_START = [
|
|
8874
|
+
255,
|
|
8875
|
+
216,
|
|
8876
|
+
255
|
|
8877
|
+
];
|
|
8878
|
+
/**
|
|
8879
|
+
* A peer's `video` description, or undefined when it is not one. It never refuses the file. A poster that is
|
|
8880
|
+
* too large or not a JPEG is dropped on its own; the length and size are kept.
|
|
8881
|
+
*/
|
|
8882
|
+
function parseVideoMeta(value, mime) {
|
|
8883
|
+
if (mime !== void 0 && !isVideoMime(mime)) return void 0;
|
|
8884
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
8885
|
+
const { duration, width, height, poster } = value;
|
|
8886
|
+
if (typeof duration !== "number" || !Number.isSafeInteger(duration) || duration <= 0 || duration > VIDEO_LIMITS.maxDurationMs) return void 0;
|
|
8887
|
+
const edge = (n) => typeof n === "number" && Number.isSafeInteger(n) && n > 0 && n <= VIDEO_LIMITS.maxEdge;
|
|
8888
|
+
if (!edge(width) || !edge(height)) return void 0;
|
|
8889
|
+
const meta = {
|
|
8890
|
+
duration,
|
|
8891
|
+
width,
|
|
8892
|
+
height
|
|
8893
|
+
};
|
|
8894
|
+
if (typeof poster === "string" && poster.length <= Math.ceil(VIDEO_LIMITS.maxPosterBytes * 4 / 3) && BASE64URL.test(poster)) try {
|
|
8895
|
+
const bytes = fromBase64Url(poster);
|
|
8896
|
+
if (bytes.length <= VIDEO_LIMITS.maxPosterBytes && JPEG_START.every((b, i) => bytes[i] === b)) meta.poster = poster;
|
|
8897
|
+
} catch {}
|
|
8898
|
+
return meta;
|
|
8899
|
+
}
|
|
8900
|
+
/** `0:07`, `12:45`, `1:02:03`: as a player shows a video's length. */
|
|
8901
|
+
function formatVideoDuration(ms) {
|
|
8902
|
+
const total = Math.max(0, Math.floor((Number.isFinite(ms) ? ms : 0) / 1e3));
|
|
8903
|
+
const hours = Math.floor(total / 3600), minutes = Math.floor(total % 3600 / 60), seconds = String(total % 60).padStart(2, "0");
|
|
8904
|
+
return hours ? `${hours}:${String(minutes).padStart(2, "0")}:${seconds}` : `${minutes}:${seconds}`;
|
|
8905
|
+
}
|
|
8906
|
+
//#endregion
|
|
8907
|
+
//#region ../core/src/voice.ts
|
|
8908
|
+
var VOICE_LIMITS = {
|
|
8909
|
+
/** Fifteen minutes: at the bitrate recorded (32 kbit/s) about 3.6 MB, well inside a held item's 8 MiB. */
|
|
8910
|
+
maxDurationMs: 9e5,
|
|
8911
|
+
/** Bars drawn for a recording. */
|
|
8912
|
+
bars: 64,
|
|
8913
|
+
/** Bars accepted from a peer. */
|
|
8914
|
+
maxBars: 128,
|
|
8915
|
+
/** Asked of the recorder; opus speech is clear well below it. */
|
|
8916
|
+
audioBitsPerSecond: 32e3
|
|
8917
|
+
};
|
|
8918
|
+
/**
|
|
8919
|
+
* Audio a player may be handed as it is. None of these runs anything in the page's origin,
|
|
8920
|
+
* so received bytes of these types are served with their type (see `safeBlobType`).
|
|
8921
|
+
*/
|
|
8922
|
+
var PLAYABLE_AUDIO = /^audio\/(webm|ogg|opus|mp4|mpeg|mp3|aac|x-m4a|wav|x-wav|wave|flac|x-flac)$/;
|
|
8923
|
+
/** The part of a type before its parameters, in lower case: `audio/webm;codecs=opus` → `audio/webm`. */
|
|
8924
|
+
function baseMime(mime) {
|
|
8925
|
+
return mime.split(";")[0].trim().toLowerCase();
|
|
8926
|
+
}
|
|
8927
|
+
/**
|
|
8928
|
+
* A peer's `voice` description, or undefined when it is not one. It never refuses the file:
|
|
8929
|
+
* a bad description only means the file is shown as a file.
|
|
8930
|
+
*/
|
|
8931
|
+
function parseVoiceMeta(value, mime) {
|
|
8932
|
+
if (mime !== void 0 && !PLAYABLE_AUDIO.test(baseMime(mime))) return void 0;
|
|
8933
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
8934
|
+
const { duration, peaks } = value;
|
|
8935
|
+
if (typeof duration !== "number" || !Number.isSafeInteger(duration) || duration <= 0 || duration > VOICE_LIMITS.maxDurationMs + 5e3) return void 0;
|
|
8936
|
+
if (!Array.isArray(peaks) || peaks.length === 0 || peaks.length > VOICE_LIMITS.maxBars) return void 0;
|
|
8937
|
+
if (!peaks.every((peak) => typeof peak === "number" && Number.isInteger(peak) && peak >= 0 && peak <= 255)) return void 0;
|
|
8938
|
+
return {
|
|
8939
|
+
duration,
|
|
8940
|
+
peaks: [...peaks]
|
|
8941
|
+
};
|
|
8942
|
+
}
|
|
8943
|
+
/**
|
|
8944
|
+
* How the level is metered while recording (apps/ui/src/lib/voiceRecorder.ts): every `intervalMs`, the RMS of the last
|
|
8945
|
+
* `window` samples an AnalyserNode holds (at a 48 kHz context, about 21 ms of sound).
|
|
8946
|
+
*/
|
|
8947
|
+
var VOICE_METER = {
|
|
8948
|
+
intervalMs: 50,
|
|
8949
|
+
window: 1024,
|
|
8950
|
+
windowRate: 48e3
|
|
8951
|
+
};
|
|
8952
|
+
/** One reading of the meter, 0-1: the RMS of the samples, lifted because speech sits low on a linear scale. */
|
|
8953
|
+
function voiceLevel(samples) {
|
|
8954
|
+
if (samples.length === 0) return 0;
|
|
8955
|
+
let sum = 0;
|
|
8956
|
+
for (let i = 0; i < samples.length; i++) sum += samples[i] * samples[i];
|
|
8957
|
+
return Math.min(1, Math.sqrt(sum / samples.length) * 4);
|
|
8958
|
+
}
|
|
8959
|
+
/**
|
|
8960
|
+
* The readings the recorder would have taken of this sound (mono, -1 to 1), for a recording made elsewhere: one
|
|
8961
|
+
* every `intervalMs` of it, each over the window that ends there, as long in time as the analyser's.
|
|
8962
|
+
*/
|
|
8963
|
+
function voiceLevelsOf(mono, sampleRate) {
|
|
8964
|
+
if (!(sampleRate > 0) || mono.length === 0) return [];
|
|
8965
|
+
const window = Math.max(1, Math.round(VOICE_METER.window * sampleRate / VOICE_METER.windowRate));
|
|
8966
|
+
const levels = [];
|
|
8967
|
+
for (let step = 1;; step++) {
|
|
8968
|
+
const end = Math.round(step * VOICE_METER.intervalMs * sampleRate / 1e3);
|
|
8969
|
+
if (end > mono.length) break;
|
|
8970
|
+
levels.push(voiceLevel(mono.subarray(Math.max(0, end - window), end)));
|
|
8971
|
+
}
|
|
8972
|
+
return levels;
|
|
8973
|
+
}
|
|
8974
|
+
/** The waveform sent with a recording made elsewhere, drawn as the recorder draws its own. */
|
|
8975
|
+
function voicePeaksOf(mono, sampleRate, bars = VOICE_LIMITS.bars) {
|
|
8976
|
+
return downsamplePeaks(voiceLevelsOf(mono, sampleRate), bars);
|
|
8977
|
+
}
|
|
8978
|
+
/** Below this a recording is silence, and is drawn flat rather than stretched to fill the bars. */
|
|
8979
|
+
var QUIET = .02;
|
|
8980
|
+
/**
|
|
8981
|
+
* Bars from the loudness measured while recording (RMS, 0-1, oldest first): the loudest
|
|
8982
|
+
* sample of each stretch, scaled so the loudest bar is full height.
|
|
8983
|
+
*/
|
|
8984
|
+
function downsamplePeaks(levels, bars = VOICE_LIMITS.bars) {
|
|
8985
|
+
const count = Math.max(1, Math.min(VOICE_LIMITS.maxBars, Math.floor(bars)));
|
|
8986
|
+
const clean = Array.from(levels, (level) => Number.isFinite(level) ? Math.min(1, Math.max(0, level)) : 0);
|
|
8987
|
+
if (clean.length === 0) return new Array(count).fill(0);
|
|
8988
|
+
const loudest = Math.max(QUIET, ...clean);
|
|
8989
|
+
const out = [];
|
|
8990
|
+
for (let bar = 0; bar < count; bar++) {
|
|
8991
|
+
const from = Math.floor(bar * clean.length / count);
|
|
8992
|
+
const to = Math.max(from + 1, Math.floor((bar + 1) * clean.length / count));
|
|
8993
|
+
let peak = 0;
|
|
8994
|
+
for (let i = from; i < to && i < clean.length; i++) peak = Math.max(peak, clean[i]);
|
|
8995
|
+
out.push(Math.round(peak / loudest * 255));
|
|
8996
|
+
}
|
|
8997
|
+
return out;
|
|
8998
|
+
}
|
|
8999
|
+
/** `0:07`, `12:45`: minutes and seconds, as a player shows them. */
|
|
9000
|
+
function formatVoiceDuration(ms) {
|
|
9001
|
+
const total = Math.max(0, Math.floor((Number.isFinite(ms) ? ms : 0) / 1e3));
|
|
9002
|
+
return `${Math.floor(total / 60)}:${String(total % 60).padStart(2, "0")}`;
|
|
9003
|
+
}
|
|
9004
|
+
/** The line a file message shows in the chat list and in notifications. */
|
|
9005
|
+
function fileMessageText(file) {
|
|
9006
|
+
if (file.voice) return `🎤 Voice message (${formatVoiceDuration(file.voice.duration)})`;
|
|
9007
|
+
if (file.video) return `🎬 Video (${formatVideoDuration(file.video.duration)})`;
|
|
9008
|
+
return `📎 ${file.name}`;
|
|
9009
|
+
}
|
|
9010
|
+
//#endregion
|
|
9011
|
+
//#region ../core/src/files.ts
|
|
9012
|
+
var FILE_ID = /^[A-Za-z0-9_-]{8,64}$/;
|
|
9013
|
+
var MIME = /^[a-z0-9][a-z0-9!#$&^_.+-]{0,63}\/[a-z0-9][a-z0-9!#$&^_.+-]{0,63}$/i;
|
|
9014
|
+
var MAX_NAME_LENGTH = 200;
|
|
9015
|
+
/**
|
|
9016
|
+
* Invisible and direction-changing characters: bidi overrides turn
|
|
9017
|
+
* "invoice\u202Efdp.exe" into what reads as "invoiceexe.pdf", zero-width ones hide
|
|
9018
|
+
* text. Cf covers both; line and paragraph separators break the layout.
|
|
9019
|
+
*/
|
|
9020
|
+
var INVISIBLE = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu;
|
|
9021
|
+
/**
|
|
9022
|
+
* Names Windows keeps for devices, whatever the case, with any extension, and with spaces before the dot:
|
|
9023
|
+
* "CON", "nul.txt", "COM1 .log" open a device there, not a file.
|
|
9024
|
+
*/
|
|
9025
|
+
var WINDOWS_DEVICE = /^(CON|PRN|AUX|NUL|CONIN\$|CONOUT\$|COM[0-9¹²³]|LPT[0-9¹²³])\s*(\.|$)/i;
|
|
9026
|
+
/**
|
|
9027
|
+
* At most `MAX_NAME_LENGTH` characters. A longer name loses the end of its stem, not its extension (up to 15
|
|
9028
|
+
* characters after the last dot): "…report.pdf" stays a PDF to the system it is saved on.
|
|
9029
|
+
*/
|
|
9030
|
+
function shorten(name) {
|
|
9031
|
+
const chars = [...name];
|
|
9032
|
+
if (chars.length <= MAX_NAME_LENGTH) return name;
|
|
9033
|
+
const extension = [.../\.[^.\s]{1,15}$/u.exec(name)?.[0] ?? ""];
|
|
9034
|
+
return chars.slice(0, MAX_NAME_LENGTH - extension.length).join("").trimEnd() + extension.join("");
|
|
9035
|
+
}
|
|
9036
|
+
/** A file name is display text and a download suggestion, never a path. */
|
|
9037
|
+
function sanitizeFileName(name) {
|
|
9038
|
+
const clean = shorten(name.replace(INVISIBLE, "").replace(/[/\\:]/g, "").replace(/^[\s.]+/, "").trim());
|
|
9039
|
+
if (WINDOWS_DEVICE.test(clean)) return shorten("_" + clean);
|
|
9040
|
+
return clean || "file";
|
|
9041
|
+
}
|
|
9042
|
+
function sanitizeMime(mime) {
|
|
9043
|
+
return MIME.test(mime) ? mime.toLowerCase() : "application/octet-stream";
|
|
9044
|
+
}
|
|
9045
|
+
/** A size for people: B, KB, MB, GB, TB (powers of 1024), one decimal from KB up. */
|
|
9046
|
+
function formatFileSize(bytes) {
|
|
9047
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
9048
|
+
const units = [
|
|
9049
|
+
"KB",
|
|
9050
|
+
"MB",
|
|
9051
|
+
"GB",
|
|
9052
|
+
"TB"
|
|
9053
|
+
];
|
|
9054
|
+
let value = bytes / 1024, unit = 0;
|
|
9055
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
9056
|
+
value /= 1024;
|
|
9057
|
+
unit++;
|
|
9058
|
+
}
|
|
9059
|
+
return `${value.toFixed(1)} ${units[unit]}`;
|
|
9060
|
+
}
|
|
9061
|
+
var FileTransfers = class {
|
|
9062
|
+
channel;
|
|
9063
|
+
events;
|
|
9064
|
+
nextStreamId = 1;
|
|
9065
|
+
incoming = /* @__PURE__ */ new Map();
|
|
9066
|
+
outgoing = /* @__PURE__ */ new Map();
|
|
9067
|
+
constructor(channel, events) {
|
|
9068
|
+
this.channel = channel;
|
|
9069
|
+
this.events = events;
|
|
9070
|
+
}
|
|
9071
|
+
/** Streams a file to the peer. Resolves once every byte is handed to the channel. */
|
|
9072
|
+
async send(file, source) {
|
|
9073
|
+
if (file.size > LIMITS.maxFileBytes) throw new Error("File is too large");
|
|
9074
|
+
const id = this.nextStreamId++;
|
|
9075
|
+
const state = {
|
|
9076
|
+
fileId: file.id,
|
|
9077
|
+
cancelled: false
|
|
9078
|
+
};
|
|
9079
|
+
this.outgoing.set(id, state);
|
|
9080
|
+
try {
|
|
9081
|
+
this.channel.send(encodeControl({
|
|
9082
|
+
t: "file",
|
|
9083
|
+
id,
|
|
9084
|
+
f: file.id,
|
|
9085
|
+
ts: file.timestamp,
|
|
9086
|
+
n: sanitizeFileName(file.name),
|
|
9087
|
+
s: file.size,
|
|
9088
|
+
m: sanitizeMime(file.mime)
|
|
9089
|
+
}));
|
|
9090
|
+
let sent = 0;
|
|
9091
|
+
const counted = async function* () {
|
|
9092
|
+
for await (const part of source) {
|
|
9093
|
+
sent += part.length;
|
|
9094
|
+
yield part;
|
|
9095
|
+
this.events.onProgress?.(file.id, sent, "out");
|
|
9096
|
+
}
|
|
9097
|
+
}.call(this);
|
|
9098
|
+
await sendBody(this.channel, CHUNK_KIND.fileBody, id, counted, () => state.cancelled);
|
|
9099
|
+
if (state.cancelled) throw new Error("The peer refused the file");
|
|
9100
|
+
if (sent !== file.size) throw new Error("File changed while it was being sent");
|
|
9101
|
+
this.events.onComplete?.(file.id, "out");
|
|
9102
|
+
} catch (error) {
|
|
9103
|
+
if (!state.cancelled) try {
|
|
9104
|
+
this.channel.send(encodeControl({
|
|
9105
|
+
t: "rst",
|
|
9106
|
+
id,
|
|
9107
|
+
d: "f",
|
|
9108
|
+
e: "aborted"
|
|
9109
|
+
}));
|
|
9110
|
+
} catch {}
|
|
9111
|
+
this.events.onFailed?.(file.id, error instanceof Error ? error.message : String(error), "out");
|
|
9112
|
+
throw error;
|
|
9113
|
+
} finally {
|
|
9114
|
+
this.outgoing.delete(id);
|
|
9115
|
+
}
|
|
9116
|
+
}
|
|
9117
|
+
handleFile(frame) {
|
|
9118
|
+
if (this.incoming.has(frame.id)) return;
|
|
9119
|
+
const refuse = (reason) => {
|
|
9120
|
+
try {
|
|
9121
|
+
this.channel.send(encodeControl({
|
|
9122
|
+
t: "rst",
|
|
9123
|
+
id: frame.id,
|
|
9124
|
+
d: "f",
|
|
9125
|
+
e: reason
|
|
9126
|
+
}));
|
|
9127
|
+
} catch {}
|
|
9128
|
+
};
|
|
9129
|
+
if (!FILE_ID.test(frame.f)) return refuse("invalid file id");
|
|
9130
|
+
if (frame.s > LIMITS.maxFileBytes) return refuse("file too large");
|
|
9131
|
+
if (this.incoming.size >= LIMITS.maxIncomingFilesPerPeer) return refuse("too many transfers");
|
|
9132
|
+
const file = {
|
|
9133
|
+
id: frame.f,
|
|
9134
|
+
name: sanitizeFileName(frame.n),
|
|
9135
|
+
size: frame.s,
|
|
9136
|
+
mime: sanitizeMime(frame.m),
|
|
9137
|
+
timestamp: frame.ts
|
|
9138
|
+
};
|
|
9139
|
+
const sink = this.events.onIncoming(file);
|
|
9140
|
+
if (!sink || typeof sink === "string") return refuse(sink || "refused");
|
|
9141
|
+
const entry = {
|
|
9142
|
+
file,
|
|
9143
|
+
sink,
|
|
9144
|
+
received: 0,
|
|
9145
|
+
queue: Promise.resolve()
|
|
9146
|
+
};
|
|
9147
|
+
this.incoming.set(frame.id, entry);
|
|
9148
|
+
this.armIdleTimer(frame.id, entry);
|
|
9149
|
+
}
|
|
9150
|
+
handleChunk(chunk) {
|
|
9151
|
+
const entry = this.incoming.get(chunk.id);
|
|
9152
|
+
if (!entry) return;
|
|
9153
|
+
entry.received += chunk.payload.length;
|
|
9154
|
+
if (entry.received > entry.file.size) return this.failIncoming(chunk.id, "more data than announced", true);
|
|
9155
|
+
if (chunk.payload.length > 0) {
|
|
9156
|
+
const payload = chunk.payload.slice();
|
|
9157
|
+
entry.queue = entry.queue.then(() => entry.sink.write(payload));
|
|
9158
|
+
this.events.onProgress?.(entry.file.id, entry.received, "in");
|
|
9159
|
+
}
|
|
9160
|
+
if (!chunk.end) return this.armIdleTimer(chunk.id, entry);
|
|
9161
|
+
if (entry.received !== entry.file.size) return this.failIncoming(chunk.id, "incomplete file", false);
|
|
9162
|
+
clearTimeout(entry.idleTimer);
|
|
9163
|
+
this.incoming.delete(chunk.id);
|
|
9164
|
+
entry.queue.then(() => entry.sink.close()).then(() => this.events.onComplete?.(entry.file.id, "in"), () => {
|
|
9165
|
+
entry.sink.abort();
|
|
9166
|
+
this.events.onFailed?.(entry.file.id, "could not store the file", "in");
|
|
9167
|
+
});
|
|
9168
|
+
}
|
|
9169
|
+
handleReset(frame) {
|
|
9170
|
+
if (this.incoming.has(frame.id)) this.failIncoming(frame.id, frame.e || "cancelled by the sender", false);
|
|
9171
|
+
const sending = this.outgoing.get(frame.id);
|
|
9172
|
+
if (sending) sending.cancelled = true;
|
|
9173
|
+
}
|
|
9174
|
+
/** The link went away: nothing in flight can finish. */
|
|
9175
|
+
closeAll() {
|
|
9176
|
+
for (const id of [...this.incoming.keys()]) this.failIncoming(id, "connection lost", false);
|
|
9177
|
+
for (const sending of this.outgoing.values()) sending.cancelled = true;
|
|
9178
|
+
}
|
|
9179
|
+
armIdleTimer(id, entry) {
|
|
9180
|
+
clearTimeout(entry.idleTimer);
|
|
9181
|
+
entry.idleTimer = setTimeout(() => this.failIncoming(id, "transfer stalled", true), LIMITS.bodyIdleTimeoutMs);
|
|
9182
|
+
}
|
|
9183
|
+
failIncoming(id, reason, notifyPeer) {
|
|
9184
|
+
const entry = this.incoming.get(id);
|
|
9185
|
+
if (!entry) return;
|
|
9186
|
+
clearTimeout(entry.idleTimer);
|
|
9187
|
+
this.incoming.delete(id);
|
|
9188
|
+
entry.sink.abort();
|
|
9189
|
+
if (notifyPeer) try {
|
|
9190
|
+
this.channel.send(encodeControl({
|
|
9191
|
+
t: "rst",
|
|
9192
|
+
id,
|
|
9193
|
+
d: "f",
|
|
9194
|
+
e: reason
|
|
9195
|
+
}));
|
|
9196
|
+
} catch {}
|
|
9197
|
+
this.events.onFailed?.(entry.file.id, reason, "in");
|
|
9198
|
+
}
|
|
9199
|
+
};
|
|
9200
|
+
//#endregion
|
|
9201
|
+
//#region ../core/src/image.ts
|
|
9202
|
+
var IMAGE_LIMITS = {
|
|
9203
|
+
/** Pixels on either side: the most a JPEG can say, well past what anyone sends as a picture. */
|
|
9204
|
+
maxEdge: 65535,
|
|
9205
|
+
/** How much of a file is read for its size: past a JPEG's EXIF, thumbnails and colour profile. */
|
|
9206
|
+
headBytes: 262144
|
|
9207
|
+
};
|
|
9208
|
+
/** Whether a file is a picture, by its type. */
|
|
9209
|
+
function isImageMime(mime) {
|
|
9210
|
+
return mime.split(";")[0].trim().toLowerCase().startsWith("image/");
|
|
9211
|
+
}
|
|
9212
|
+
var edge = (n) => typeof n === "number" && Number.isSafeInteger(n) && n > 0 && n <= IMAGE_LIMITS.maxEdge;
|
|
9213
|
+
/** A peer's `image` description, or undefined when it is not one. It never refuses the file. */
|
|
9214
|
+
function parseImageMeta(value, mime) {
|
|
9215
|
+
if (mime !== void 0 && !isImageMime(mime)) return void 0;
|
|
9216
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
9217
|
+
const { width, height } = value;
|
|
9218
|
+
return edge(width) && edge(height) ? {
|
|
9219
|
+
width,
|
|
9220
|
+
height
|
|
9221
|
+
} : void 0;
|
|
9222
|
+
}
|
|
9223
|
+
var u16be = (b, at) => b[at] << 8 | b[at + 1];
|
|
9224
|
+
var u16le = (b, at) => b[at] | b[at + 1] << 8;
|
|
9225
|
+
var u24le = (b, at) => b[at] | b[at + 1] << 8 | b[at + 2] << 16;
|
|
9226
|
+
var u32be = (b, at) => (b[at] << 24 >>> 0) + (b[at + 1] << 16) + (b[at + 2] << 8) + b[at + 3];
|
|
9227
|
+
var ascii = (b, at, text) => [...text].every((c, i) => b[at + i] === c.charCodeAt(0));
|
|
9228
|
+
var sized = (width, height) => edge(width) && edge(height) ? {
|
|
9229
|
+
width,
|
|
9230
|
+
height
|
|
9231
|
+
} : void 0;
|
|
9232
|
+
/** SOFn markers: every frame type but DHT (C4), JPG (C8) and DAC (CC). */
|
|
9233
|
+
var JPEG_FRAME = /* @__PURE__ */ new Set([
|
|
9234
|
+
192,
|
|
9235
|
+
193,
|
|
9236
|
+
194,
|
|
9237
|
+
195,
|
|
9238
|
+
197,
|
|
9239
|
+
198,
|
|
9240
|
+
199,
|
|
9241
|
+
201,
|
|
9242
|
+
202,
|
|
9243
|
+
203,
|
|
9244
|
+
205,
|
|
9245
|
+
206,
|
|
9246
|
+
207
|
|
9247
|
+
]);
|
|
9248
|
+
/** The EXIF orientation in an APP1 segment's TIFF block (1 when it says none). */
|
|
9249
|
+
function exifOrientation(b, tiff, end) {
|
|
9250
|
+
if (tiff + 8 > end) return 1;
|
|
9251
|
+
const little = ascii(b, tiff, "II");
|
|
9252
|
+
if (!little && !ascii(b, tiff, "MM")) return 1;
|
|
9253
|
+
const u16 = (at) => little ? u16le(b, at) : u16be(b, at);
|
|
9254
|
+
const u32 = (at) => little ? u16le(b, at) + u16le(b, at + 2) * 65536 : u32be(b, at);
|
|
9255
|
+
if (u16(tiff + 2) !== 42) return 1;
|
|
9256
|
+
const ifd = tiff + u32(tiff + 4);
|
|
9257
|
+
if (ifd + 2 > end) return 1;
|
|
9258
|
+
const count = u16(ifd);
|
|
9259
|
+
for (let i = 0; i < count; i++) {
|
|
9260
|
+
const entry = ifd + 2 + i * 12;
|
|
9261
|
+
if (entry + 12 > end) return 1;
|
|
9262
|
+
if (u16(entry) === 274) {
|
|
9263
|
+
const value = u16(entry + 8);
|
|
9264
|
+
return value >= 1 && value <= 8 ? value : 1;
|
|
9265
|
+
}
|
|
9266
|
+
}
|
|
9267
|
+
return 1;
|
|
9268
|
+
}
|
|
9269
|
+
function jpegSize(b) {
|
|
9270
|
+
let at = 2, orientation = 1;
|
|
9271
|
+
while (at + 4 <= b.length) {
|
|
9272
|
+
if (b[at] !== 255) return void 0;
|
|
9273
|
+
const marker = b[at + 1];
|
|
9274
|
+
if (marker === 255) {
|
|
9275
|
+
at++;
|
|
9276
|
+
continue;
|
|
9277
|
+
}
|
|
9278
|
+
if (marker === 1 || marker >= 208 && marker <= 215) {
|
|
9279
|
+
at += 2;
|
|
9280
|
+
continue;
|
|
9281
|
+
}
|
|
9282
|
+
if (marker === 217 || marker === 218) return void 0;
|
|
9283
|
+
const length = u16be(b, at + 2);
|
|
9284
|
+
if (length < 2) return void 0;
|
|
9285
|
+
const end = Math.min(b.length, at + 2 + length);
|
|
9286
|
+
if (marker === 225 && ascii(b, at + 4, "Exif\0\0")) orientation = exifOrientation(b, at + 10, end);
|
|
9287
|
+
if (JPEG_FRAME.has(marker)) {
|
|
9288
|
+
if (at + 9 > b.length) return void 0;
|
|
9289
|
+
const height = u16be(b, at + 5), width = u16be(b, at + 7);
|
|
9290
|
+
return orientation >= 5 ? sized(height, width) : sized(width, height);
|
|
9291
|
+
}
|
|
9292
|
+
at += 2 + length;
|
|
9293
|
+
}
|
|
9294
|
+
}
|
|
9295
|
+
function webpSize(b) {
|
|
9296
|
+
if (b.length < 30) return void 0;
|
|
9297
|
+
if (ascii(b, 12, "VP8 ")) {
|
|
9298
|
+
if (b[23] !== 157 || b[24] !== 1 || b[25] !== 42) return void 0;
|
|
9299
|
+
return sized(u16le(b, 26) & 16383, u16le(b, 28) & 16383);
|
|
9300
|
+
}
|
|
9301
|
+
if (ascii(b, 12, "VP8L")) {
|
|
9302
|
+
if (b[20] !== 47) return void 0;
|
|
9303
|
+
const bits = b[21] | b[22] << 8 | b[23] << 16 | b[24] << 24;
|
|
9304
|
+
return sized((bits & 16383) + 1, (bits >>> 14 & 16383) + 1);
|
|
9305
|
+
}
|
|
9306
|
+
if (ascii(b, 12, "VP8X")) return sized(u24le(b, 24) + 1, u24le(b, 27) + 1);
|
|
9307
|
+
}
|
|
9308
|
+
/**
|
|
9309
|
+
* A picture's size as it is shown, read from its first bytes (PNG, JPEG, GIF, WebP), or undefined when they do not
|
|
9310
|
+
* say. Only the header is read: a JPEG whose frame header sits past `IMAGE_LIMITS.headBytes` goes without.
|
|
9311
|
+
*/
|
|
9312
|
+
function imageSize(head) {
|
|
9313
|
+
const b = head;
|
|
9314
|
+
try {
|
|
9315
|
+
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));
|
|
9316
|
+
if (b.length >= 10 && (ascii(b, 0, "GIF87a") || ascii(b, 0, "GIF89a"))) return sized(u16le(b, 6), u16le(b, 8));
|
|
9317
|
+
if (b.length >= 4 && b[0] === 255 && b[1] === 216 && b[2] === 255) return jpegSize(b);
|
|
9318
|
+
if (b.length >= 16 && ascii(b, 0, "RIFF") && ascii(b, 8, "WEBP")) return webpSize(b);
|
|
9319
|
+
} catch {}
|
|
9320
|
+
}
|
|
9321
|
+
/**
|
|
9322
|
+
* The description a picture is sent with, read from the start of the file, or undefined when it is not a picture
|
|
9323
|
+
* or its first bytes do not say (it then goes without one).
|
|
9324
|
+
*/
|
|
9325
|
+
async function readImageMeta(source, mime) {
|
|
9326
|
+
if (!isImageMime(mime)) return void 0;
|
|
9327
|
+
try {
|
|
9328
|
+
return imageSize(new Uint8Array(await source.slice(0, IMAGE_LIMITS.headBytes).arrayBuffer()));
|
|
9329
|
+
} catch {
|
|
9330
|
+
return;
|
|
9331
|
+
}
|
|
9332
|
+
}
|
|
9333
|
+
//#endregion
|
|
7381
9334
|
//#region ../core/src/dhtFiles.ts
|
|
7382
9335
|
/**
|
|
7383
9336
|
* A file's bubble on the DHT floor (WISP 403 § Files): while the contact is away, a file's offer (its name, size, type,
|
|
@@ -7617,6 +9570,14 @@ var DhtDelivery = class DhtDelivery {
|
|
|
7617
9570
|
};
|
|
7618
9571
|
}
|
|
7619
9572
|
/**
|
|
9573
|
+
* This side joined, and its inviter never answered (`DhtDeliveryState.unanswered`): no envelope of its newer than the
|
|
9574
|
+
* one that pinned it, none sealed to this side, no text confirmed, never live (as `sealedToAnother` asks too).
|
|
9575
|
+
*/
|
|
9576
|
+
get unanswered() {
|
|
9577
|
+
const pinnedAt = this.state.unanswered;
|
|
9578
|
+
return pinnedAt !== void 0 && this.state.peerSequence <= pinnedAt && !this.state.peerPinned && !this.state.confirmed && !this.state.wasLive && !this.live;
|
|
9579
|
+
}
|
|
9580
|
+
/**
|
|
7620
9581
|
* Something signed by a participation key other than the pinned one came in over the chat's invite-derived keys (this
|
|
7621
9582
|
* mailbox, the link's signals, a connection to an endpoint whose address a copy of the invite could read): ignored,
|
|
7622
9583
|
* and said in the view as a passive warning.
|
|
@@ -8325,6 +10286,7 @@ var DhtDelivery = class DhtDelivery {
|
|
|
8325
10286
|
}
|
|
8326
10287
|
if (message !== null && (!Array.isArray(message) || message.length !== 3 || typeof message[0] !== "string" || !ID$2.test(message[0]) || !Number.isSafeInteger(message[1]) || message[1] <= 0 || message[1] > issued + 3e4 || typeof message[2] !== "string" || utf8Encode(message[2]).length > 256 || expires - issued > 3e5)) return "none";
|
|
8327
10288
|
if (receipt !== null && (typeof receipt !== "string" || !ID$2.test(receipt))) return "none";
|
|
10289
|
+
const unanswered = !this.options.credentials.peerKey && !!this.options.credentials.expectedPeerKey && !sender && !this.live;
|
|
8328
10290
|
await this.options.pin(author);
|
|
8329
10291
|
this.options.credentials.peerKey = author;
|
|
8330
10292
|
this.options.credentials.requireSignedSignals = true;
|
|
@@ -8375,6 +10337,7 @@ var DhtDelivery = class DhtDelivery {
|
|
|
8375
10337
|
receipt: nextReceipt,
|
|
8376
10338
|
confirmed,
|
|
8377
10339
|
...peerPinned && { peerPinned },
|
|
10340
|
+
...unanswered && { unanswered: sequence },
|
|
8378
10341
|
pending: confirmed && this.state.pending?.message[0] === confirmed ? void 0 : this.state.pending,
|
|
8379
10342
|
...newest > (this.state.reactionsTaken ?? 0) && { reactionsTaken: newest }
|
|
8380
10343
|
});
|
|
@@ -8496,10 +10459,13 @@ var DhtDelivery = class DhtDelivery {
|
|
|
8496
10459
|
if (this.state.pending && this.state.pending.expires > now) return DHT_POLL_MS;
|
|
8497
10460
|
return this.active ? ACTIVE_DHT_POLL_MS : STREAM_POLL_MS;
|
|
8498
10461
|
}
|
|
8499
|
-
/**
|
|
10462
|
+
/**
|
|
10463
|
+
* How long ago this side's invite was made (or joined), while nobody used it (or its inviter never answered,
|
|
10464
|
+
* `unanswered`) and it is no longer awaited (`AWAITING_PEER_MS`).
|
|
10465
|
+
*/
|
|
8500
10466
|
unusedInviteAge(now) {
|
|
8501
10467
|
const madeAt = this.options.inviteMadeAt;
|
|
8502
|
-
if (madeAt === void 0 || this.options.credentials.peerKey || this.state.peerSequence) return void 0;
|
|
10468
|
+
if (madeAt === void 0 || (this.options.credentials.peerKey || this.state.peerSequence) && !this.unanswered) return void 0;
|
|
8503
10469
|
const age = now - Math.min(madeAt, this.startedAt);
|
|
8504
10470
|
return age >= 6e5 ? age : void 0;
|
|
8505
10471
|
}
|
|
@@ -11631,6 +13597,8 @@ function readManifest(meta, loopback = false) {
|
|
|
11631
13597
|
*/
|
|
11632
13598
|
var OFFER_EXTENSIONS = ["ping/1"];
|
|
11633
13599
|
var MAX_HANDSHAKE_BYTES = 4096;
|
|
13600
|
+
/** One frame from the peer, at most (UTF-16 units for text, bytes for binary): a bigger one breaks the protocol. */
|
|
13601
|
+
var SESSION_FRAME_MAX = 61440;
|
|
11634
13602
|
/**
|
|
11635
13603
|
* Frames received and not handled yet, at most: their count and their size together. Frames are handled one at a time,
|
|
11636
13604
|
* each awaited (a group frame is decrypted, checked and stored before the next), and the peer sends at the network's
|
|
@@ -11641,7 +13609,7 @@ var MAX_HANDSHAKE_BYTES = 4096;
|
|
|
11641
13609
|
*/
|
|
11642
13610
|
var SESSION_RECEIVE_PENDING = {
|
|
11643
13611
|
frames: 1024,
|
|
11644
|
-
bytes:
|
|
13612
|
+
bytes: 64 * SESSION_FRAME_MAX
|
|
11645
13613
|
};
|
|
11646
13614
|
var KEY = /^[ybndrfg8ejkmcpqxot1uwisza345h769]{52}$/;
|
|
11647
13615
|
var NONCE$1 = /^[A-Za-z0-9_-]{43}$/;
|
|
@@ -11992,6 +13960,9 @@ var PairedHttp = class {
|
|
|
11992
13960
|
return channel.bufferedAmount;
|
|
11993
13961
|
},
|
|
11994
13962
|
drained: () => channel.drained(),
|
|
13963
|
+
get sendBudget() {
|
|
13964
|
+
return channel.sendBudget;
|
|
13965
|
+
},
|
|
11995
13966
|
close: () => {},
|
|
11996
13967
|
onMessage: null,
|
|
11997
13968
|
onClose: null
|
|
@@ -12491,8 +14462,7 @@ function vapidAuthorization(endpoint, keys, subject, now = Date.now()) {
|
|
|
12491
14462
|
exp: Math.floor(now / 1e3) + TOKEN_SECONDS,
|
|
12492
14463
|
sub: subject
|
|
12493
14464
|
})}`;
|
|
12494
|
-
|
|
12495
|
-
return `vapid t=${unsigned}.${toBase64Url(signature)}, k=${keys.publicKey}`;
|
|
14465
|
+
return `vapid t=${unsigned}.${toBase64Url(p256.sign(utf8Encode(unsigned), fromBase64Url(keys.privateKey)))}, k=${keys.publicKey}`;
|
|
12496
14466
|
}
|
|
12497
14467
|
/**
|
|
12498
14468
|
* `plaintext` encrypted for a subscription (RFC 8291 with RFC 8188 `aes128gcm`, one record). `sender` fixes
|
|
@@ -12810,8 +14780,7 @@ var LINK_OR_MARKUP = /[a-z][a-z0-9+.-]*:\/\/|\bwww\.|\b(?:mailto|javascript|data
|
|
|
12810
14780
|
*/
|
|
12811
14781
|
function sanitizeTypingStatus(value) {
|
|
12812
14782
|
if (typeof value !== "string") return void 0;
|
|
12813
|
-
const
|
|
12814
|
-
const clean = sanitizeDisplayText(oneLine, 40);
|
|
14783
|
+
const clean = sanitizeDisplayText(value.slice(0, 160).replace(/\s+/g, " "), 40);
|
|
12815
14784
|
if (!clean || LINK_OR_MARKUP.test(clean)) return void 0;
|
|
12816
14785
|
return clean;
|
|
12817
14786
|
}
|
|
@@ -13060,7 +15029,7 @@ var FILE_LIMITS = {
|
|
|
13060
15029
|
resendBytes: 268435456
|
|
13061
15030
|
};
|
|
13062
15031
|
var ID = /^[A-Za-z0-9_-]{8,64}$/;
|
|
13063
|
-
var DIGEST = /^[A-Za-z0-9_-]{43}$/;
|
|
15032
|
+
var DIGEST$1 = /^[A-Za-z0-9_-]{43}$/;
|
|
13064
15033
|
var DATA = /^[A-Za-z0-9_-]+$/;
|
|
13065
15034
|
var FILE_FRAMES = /* @__PURE__ */ new Set([
|
|
13066
15035
|
"pf-offer",
|
|
@@ -13913,7 +15882,7 @@ var ChatFiles = class {
|
|
|
13913
15882
|
return;
|
|
13914
15883
|
}
|
|
13915
15884
|
if (record.state !== "active" || incoming.verifying || !incoming.target) return;
|
|
13916
|
-
if (frame.size !== record.file.size || typeof frame.digest !== "string" || !DIGEST.test(frame.digest)) return;
|
|
15885
|
+
if (frame.size !== record.file.size || typeof frame.digest !== "string" || !DIGEST$1.test(frame.digest)) return;
|
|
13917
15886
|
const digest = frame.digest;
|
|
13918
15887
|
await incoming.writing;
|
|
13919
15888
|
if (transferEnded(record) || record.state !== "active") return;
|
|
@@ -14952,6 +16921,146 @@ var CarriedTransport = class {
|
|
|
14952
16921
|
}
|
|
14953
16922
|
};
|
|
14954
16923
|
//#endregion
|
|
16924
|
+
//#region ../core/src/groupFiles.ts
|
|
16925
|
+
/**
|
|
16926
|
+
* Files and voice messages in groups (WISP 503 · Group Files). A file is announced as a group message carrying its
|
|
16927
|
+
* description; its bytes go over `files/3` between two members, from a member that holds it to one that asks for it.
|
|
16928
|
+
* The limits below are the profile's first numbers, kept in one place so they are easy to change.
|
|
16929
|
+
*/
|
|
16930
|
+
var GROUP_FILE_LIMITS = {
|
|
16931
|
+
/** The largest file a group takes. */
|
|
16932
|
+
maxBytes: 104857600,
|
|
16933
|
+
/** A file this size or smaller is fetched without asking (voice messages always are, up to `VOICE_LIMITS`). */
|
|
16934
|
+
autoBytes: 8388608,
|
|
16935
|
+
/** What a device keeps of a group's files it fetched without asking; past it, every file is asked about. */
|
|
16936
|
+
autoBytesPerGroup: 268435456,
|
|
16937
|
+
/** Bytes of group files a device serves to others in a day, when serving is on. */
|
|
16938
|
+
serveBytesPerDay: 1073741824,
|
|
16939
|
+
/** Transfers a holder serves at once, all groups together. */
|
|
16940
|
+
serveAtOnce: 3,
|
|
16941
|
+
/** File announcements a member sends to one group in a minute. */
|
|
16942
|
+
announcePerMinute: 8,
|
|
16943
|
+
/** File announcements of one member of a group fetched by themselves in `announceWindowMs`; past it, kept for a Download. */
|
|
16944
|
+
announceTaken: 30,
|
|
16945
|
+
announceWindowMs: 6e5,
|
|
16946
|
+
/** Holders remembered per file. */
|
|
16947
|
+
holders: 8,
|
|
16948
|
+
/**
|
|
16949
|
+
* Message ids a `group-have` named before their message came (a member back from away hears who holds what as its
|
|
16950
|
+
* catch-up begins), remembered with their holders for when it does.
|
|
16951
|
+
*/
|
|
16952
|
+
heardAhead: 256,
|
|
16953
|
+
/** Message ids in one `group-have`. */
|
|
16954
|
+
haveIds: 32,
|
|
16955
|
+
/**
|
|
16956
|
+
* `group-have` frames a private group's edge takes in `haveWindowMs`: what is to say is gathered for a second, then
|
|
16957
|
+
* goes. A community's, from a member, goes every `communityHaveEveryMs`.
|
|
16958
|
+
*/
|
|
16959
|
+
haveFrames: 10,
|
|
16960
|
+
haveWindowMs: 1e4,
|
|
16961
|
+
communityHaveEveryMs: 6e4,
|
|
16962
|
+
/**
|
|
16963
|
+
* How long a want waits for an offer before the next holder is asked; a transfer that does not move for as long is
|
|
16964
|
+
* left for the next holder too.
|
|
16965
|
+
*/
|
|
16966
|
+
wantWaitMs: 3e4,
|
|
16967
|
+
/** A file session closes this long after its last transfer ends. */
|
|
16968
|
+
sessionIdleMs: 6e4,
|
|
16969
|
+
/** File sessions opening at once, and open at once, all groups together. */
|
|
16970
|
+
sessionsOpening: 2,
|
|
16971
|
+
sessionsOpen: 4,
|
|
16972
|
+
/** The longest file name and type kept from a description. */
|
|
16973
|
+
nameChars: 255,
|
|
16974
|
+
mimeChars: 255
|
|
16975
|
+
};
|
|
16976
|
+
/**
|
|
16977
|
+
* The frames of WISP 503 § Getting the bytes, on an edge between two apps that announced `GROUP_VERSION_FILES`:
|
|
16978
|
+
* `group-want {g, id}` asks a holder for the file of message `id`; `group-want-no {g, id, why}` is a holder's no;
|
|
16979
|
+
* `group-have {g, ids}` says which files a member keeps (in a community, an application frame `{t, ids}` instead).
|
|
16980
|
+
*/
|
|
16981
|
+
var GROUP_WANT_FRAME = "group-want";
|
|
16982
|
+
var GROUP_WANT_NO_FRAME = "group-want-no";
|
|
16983
|
+
var GROUP_HAVE_FRAME = "group-have";
|
|
16984
|
+
var REFUSALS = [
|
|
16985
|
+
"gone",
|
|
16986
|
+
"busy",
|
|
16987
|
+
"refused"
|
|
16988
|
+
];
|
|
16989
|
+
/** A group message's id, as a private group (`<s>:<e>:<n>`) or a community (`<s>:<e>:<h>:<n>`) names it. */
|
|
16990
|
+
var MESSAGE_ID = /^[a-z0-9]{52}:\d{1,15}(:[A-Za-z0-9_-]{1,64})?:\d{1,15}$/;
|
|
16991
|
+
var isGroupMessageId = (value) => typeof value === "string" && MESSAGE_ID.test(value);
|
|
16992
|
+
function frameOf(value, t, groupId) {
|
|
16993
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
16994
|
+
const f = value;
|
|
16995
|
+
return f.t === t && (groupId === void 0 || f.g === groupId) ? f : null;
|
|
16996
|
+
}
|
|
16997
|
+
/** A `group-want` of group `groupId`: the message whose file is asked for, or null. */
|
|
16998
|
+
function readGroupWant(value, groupId) {
|
|
16999
|
+
const f = frameOf(value, GROUP_WANT_FRAME, groupId);
|
|
17000
|
+
return f && isGroupMessageId(f.id) ? { id: f.id } : null;
|
|
17001
|
+
}
|
|
17002
|
+
/** A holder's `group-want-no` of group `groupId`, or null. A reason this app does not know reads as `busy`. */
|
|
17003
|
+
function readGroupWantNo(value, groupId) {
|
|
17004
|
+
const f = frameOf(value, GROUP_WANT_NO_FRAME, groupId);
|
|
17005
|
+
if (!f || !isGroupMessageId(f.id)) return null;
|
|
17006
|
+
return {
|
|
17007
|
+
id: f.id,
|
|
17008
|
+
why: typeof f.why === "string" && REFUSALS.includes(f.why) ? f.why : "busy"
|
|
17009
|
+
};
|
|
17010
|
+
}
|
|
17011
|
+
/**
|
|
17012
|
+
* The message ids of a `group-have`: of group `groupId` on an edge, or (no `groupId`) a community's application frame.
|
|
17013
|
+
* At most `haveIds` message ids; a frame with more, or anything else, is null.
|
|
17014
|
+
*/
|
|
17015
|
+
function readGroupHave(value, groupId) {
|
|
17016
|
+
const f = frameOf(value, GROUP_HAVE_FRAME, groupId);
|
|
17017
|
+
if (!f || !Array.isArray(f.ids) || !f.ids.length || f.ids.length > GROUP_FILE_LIMITS.haveIds || !f.ids.every(isGroupMessageId)) return null;
|
|
17018
|
+
return [...new Set(f.ids)];
|
|
17019
|
+
}
|
|
17020
|
+
var DIGEST = /^[A-Za-z0-9_-]{43}$/;
|
|
17021
|
+
/**
|
|
17022
|
+
* A description read from the wire, cleaned: the name made a display name (never a path), the type untrusted, the
|
|
17023
|
+
* size within the group limit, the digest the right shape. A malformed voice, video or picture description is dropped
|
|
17024
|
+
* on its own and the file is still a file; anything else malformed is no file at all (null), and the text shows.
|
|
17025
|
+
*/
|
|
17026
|
+
function readGroupFileMeta(value) {
|
|
17027
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
17028
|
+
const v = value;
|
|
17029
|
+
if (typeof v.name !== "string" || !v.name.trim() || [...v.name].length > GROUP_FILE_LIMITS.nameChars) return null;
|
|
17030
|
+
if (typeof v.mime !== "string" || v.mime.length > GROUP_FILE_LIMITS.mimeChars) return null;
|
|
17031
|
+
if (!Number.isSafeInteger(v.size) || v.size < 1 || v.size > GROUP_FILE_LIMITS.maxBytes) return null;
|
|
17032
|
+
if (typeof v.d !== "string" || !DIGEST.test(v.d)) return null;
|
|
17033
|
+
const mime = sanitizeMime(v.mime);
|
|
17034
|
+
const voice = v.voice === void 0 ? void 0 : parseVoiceMeta(v.voice, mime);
|
|
17035
|
+
const video = v.video === void 0 ? void 0 : parseVideoMeta(v.video, mime);
|
|
17036
|
+
const image = v.image === void 0 ? void 0 : parseImageMeta(v.image, mime);
|
|
17037
|
+
return {
|
|
17038
|
+
name: sanitizeFileName(v.name),
|
|
17039
|
+
mime,
|
|
17040
|
+
size: v.size,
|
|
17041
|
+
d: v.d,
|
|
17042
|
+
...voice ? { voice } : {},
|
|
17043
|
+
...video ? { video } : {},
|
|
17044
|
+
...image ? { image } : {}
|
|
17045
|
+
};
|
|
17046
|
+
}
|
|
17047
|
+
/**
|
|
17048
|
+
* The text an announcement carries when its author wrote no caption: what an app without group files shows instead
|
|
17049
|
+
* of the file. English, as wire text is; an app that knows the file shows the file, not this line.
|
|
17050
|
+
*/
|
|
17051
|
+
function groupFileFallback(file) {
|
|
17052
|
+
if (file.voice) return `🎤 Voice message (${formatVoiceDuration(file.voice.duration)}): update Ghostly to hear it`;
|
|
17053
|
+
return `${fileMessageText(file)} (${formatFileSize(file.size)}): update Ghostly to get files in groups`;
|
|
17054
|
+
}
|
|
17055
|
+
/**
|
|
17056
|
+
* Whether a device fetches a file by itself (WISP 503 § Automatic and asked downloads): a voice message always, any
|
|
17057
|
+
* other file up to `autoBytes` while what it took that way from the group stays within `autoBytesPerGroup`.
|
|
17058
|
+
*/
|
|
17059
|
+
function fetchesByItself(file, keptAutomatically, limits = GROUP_FILE_LIMITS) {
|
|
17060
|
+
if (file.voice) return true;
|
|
17061
|
+
return file.size <= limits.autoBytes && keptAutomatically + file.size <= limits.autoBytesPerGroup;
|
|
17062
|
+
}
|
|
17063
|
+
//#endregion
|
|
14955
17064
|
//#region ../core/src/pairingProgress.ts
|
|
14956
17065
|
var FIRST_STAGE = {
|
|
14957
17066
|
inviter: "publishing",
|
|
@@ -15393,6 +17502,12 @@ function filesCap(channel) {
|
|
|
15393
17502
|
*/
|
|
15394
17503
|
var FILES_IN_USE_MS = 2e3;
|
|
15395
17504
|
/**
|
|
17505
|
+
* How far an app's data may get ahead of a file's bytes while the file waits for room on the channel (WISP 1200 § In a
|
|
17506
|
+
* chat): a frame goes only while the app has sent less than this more than the file since. A small frame (a Chess move)
|
|
17507
|
+
* still goes at once; one streaming at its limit gets about half the link, and the file the other half.
|
|
17508
|
+
*/
|
|
17509
|
+
var APP_AHEAD_OF_FILES = 16384;
|
|
17510
|
+
/**
|
|
15396
17511
|
* A `paired-message` frame. The preview (`pv`) is left out when the frame would pass `MAX_PAIRED_MESSAGE_FRAME`
|
|
15397
17512
|
* with it: the text matters, the card does not. A reply (`r`, WISP 401 § Replies) is a few hundred bytes at most
|
|
15398
17513
|
* and always goes, and so does a forwarded message's hop count (`fw`, WISP 401 § Forwards) and a status card (`sc`,
|
|
@@ -15422,6 +17537,16 @@ function pairedMessageFrame(id, ts, m, preview, reply, forwarded, card) {
|
|
|
15422
17537
|
});
|
|
15423
17538
|
return withPreview.length <= 57344 ? withPreview : plain;
|
|
15424
17539
|
}
|
|
17540
|
+
/** A text whose `paired-message` frame would pass what a session takes: it never arrives, and is refused at send. */
|
|
17541
|
+
var PAIRED_FRAME_TOO_LARGE = "Message too long once encoded. Shorten it or send it as a file.";
|
|
17542
|
+
/**
|
|
17543
|
+
* The `paired-message` frame of this text fits a session (`SESSION_FRAME_MAX`, UTF-16 units of the JSON). Within 16 KiB
|
|
17544
|
+
* of UTF-8 a text can still pass it: JSON writes each control character as six (`\u0001`). The preview is left out,
|
|
17545
|
+
* as the frame drops one that does not fit.
|
|
17546
|
+
*/
|
|
17547
|
+
function pairedMessageFits(ts, m, reply, forwarded, card) {
|
|
17548
|
+
return pairedMessageFrame("A".repeat(22), ts, m, void 0, reply, forwarded, card).length <= SESSION_FRAME_MAX;
|
|
17549
|
+
}
|
|
15425
17550
|
/**
|
|
15426
17551
|
* A native descriptor from the contact's capability record against the one known (a session's, or an older record's).
|
|
15427
17552
|
* The record carries only how to dial, never an address: the Iroh endpoint id and the relay it is homed on, the
|
|
@@ -15740,7 +17865,9 @@ var GhostLink = class {
|
|
|
15740
17865
|
});
|
|
15741
17866
|
options.events?.onPairingProgress?.(progress);
|
|
15742
17867
|
}) : null;
|
|
15743
|
-
const invite = options.pairingProgress
|
|
17868
|
+
const invite = !!options.pairingProgress && !!options.pairing && !options.pairing.credentials.peerKey;
|
|
17869
|
+
let unanswered = options.dht?.state?.unanswered !== void 0;
|
|
17870
|
+
const backsOff = invite || unanswered;
|
|
15744
17871
|
this.dht = options.params.profile && options.pairing && options.dht ? new DhtDelivery({
|
|
15745
17872
|
params: options.params,
|
|
15746
17873
|
mode: this.deliveryMode,
|
|
@@ -15750,7 +17877,7 @@ var GhostLink = class {
|
|
|
15750
17877
|
save: options.dht.save,
|
|
15751
17878
|
pollMs: options.dht.pollMs,
|
|
15752
17879
|
firstControlAfterMs: options.dht.firstControlAfterMs,
|
|
15753
|
-
...
|
|
17880
|
+
...backsOff && { inviteMadeAt: options.madeAt ?? Date.now() },
|
|
15754
17881
|
capsRev: options.dht.capsRev,
|
|
15755
17882
|
peerCapsRev: options.dht.peerCapsRev,
|
|
15756
17883
|
peerAcceptsText: options.dht.peerAcceptsText,
|
|
@@ -15809,6 +17936,9 @@ var GhostLink = class {
|
|
|
15809
17936
|
},
|
|
15810
17937
|
changed: (view) => {
|
|
15811
17938
|
options.events?.onDhtDelivery?.(view);
|
|
17939
|
+
const stillUnanswered = this.dht.unanswered;
|
|
17940
|
+
if (unanswered && !stillUnanswered) this.session.lookNow();
|
|
17941
|
+
unanswered = stillUnanswered;
|
|
15812
17942
|
if (view.inviteTaken) this.tracker?.failed("taken", false, INVITE_TAKEN);
|
|
15813
17943
|
else this.tracker?.untaken();
|
|
15814
17944
|
this.streamBlockChanged();
|
|
@@ -15837,7 +17967,7 @@ var GhostLink = class {
|
|
|
15837
17967
|
lastSeenTimestamp: options.lastSeenTimestamp,
|
|
15838
17968
|
pollIntervals: options.pollIntervals,
|
|
15839
17969
|
awaitingSince: options.madeAt,
|
|
15840
|
-
...
|
|
17970
|
+
...backsOff && { invite: () => !options.pairing.credentials.peerKey || !!this.dht?.unanswered },
|
|
15841
17971
|
getServices: options.getServices,
|
|
15842
17972
|
firstPublish: options.firstPublish ?? (this.tracker && options.pairingProgress?.role === "joiner" && options.rtcAvailable !== false || this.resuming ? "after-first-poll" : "at-start"),
|
|
15843
17973
|
events: {
|
|
@@ -17372,9 +19502,30 @@ var GhostLink = class {
|
|
|
17372
19502
|
epoch,
|
|
17373
19503
|
rest
|
|
17374
19504
|
};
|
|
19505
|
+
if (!this.peerTransports.includes("webrtc/1")) {
|
|
19506
|
+
traceLink(this.myPubKeyZ32, "offer-unanswerable", { transports: rest });
|
|
19507
|
+
this.clearRace();
|
|
19508
|
+
this.raceTimer = setTimeout(() => {
|
|
19509
|
+
this.raceTimer = null;
|
|
19510
|
+
this.endUnanswerableOffer(epoch);
|
|
19511
|
+
}, 0);
|
|
19512
|
+
return;
|
|
19513
|
+
}
|
|
17375
19514
|
traceLink(this.myPubKeyZ32, "race-standing", { transports: rest });
|
|
17376
19515
|
this.scheduleRace(epoch, made.at);
|
|
17377
19516
|
}
|
|
19517
|
+
/** The offer ends (`raceStandingOffer`), and with it going idle what is ranked after WebRTC is dialled (`dialAfterRtc`). */
|
|
19518
|
+
endUnanswerableOffer(epoch) {
|
|
19519
|
+
if (epoch !== this.connectionEpoch || this.channel || this.stopped || !this.offerOut || this.afterRtc?.epoch !== epoch) return;
|
|
19520
|
+
if (this.dialing) {
|
|
19521
|
+
this.raceTimer = setTimeout(() => {
|
|
19522
|
+
this.raceTimer = null;
|
|
19523
|
+
this.endUnanswerableOffer(epoch);
|
|
19524
|
+
}, RACE_RETRY_MS);
|
|
19525
|
+
return;
|
|
19526
|
+
}
|
|
19527
|
+
this.dataLink.close();
|
|
19528
|
+
}
|
|
17378
19529
|
/**
|
|
17379
19530
|
* Back after an absence, this side answers an offer the contact made while it was away (it dials whatever its key):
|
|
17380
19531
|
* the contact looks for that answer at the pace of an offer out a while (every 8 s on the relays), and its attempt may
|
|
@@ -17557,6 +19708,13 @@ var GhostLink = class {
|
|
|
17557
19708
|
this.attemptEnded();
|
|
17558
19709
|
this.rejectWaiters(new GhostlyHttpError("unreachable", "Could not connect to the peer"));
|
|
17559
19710
|
}
|
|
19711
|
+
/**
|
|
19712
|
+
* This side does not dial on its own: the contact's app does (the lower key), and none of what makes this side dial
|
|
19713
|
+
* anyway holds (a first pairing's role, a restart, WebRTC set aside). Its own dial, chosen by hand, is not retried.
|
|
19714
|
+
*/
|
|
19715
|
+
awaitsContactDial() {
|
|
19716
|
+
return !(!!this.tracker && !this.tracker.done && this.tracker.progress.stage !== "on-dht" ? this.options.pairingProgress?.role : void 0) && this.myPubKeyZ32 > this.options.params.peerPubKeyZ32 && !this.resuming && !this.dialsPastRtc();
|
|
19717
|
+
}
|
|
17560
19718
|
/** A transport of the attempt under way did not connect. */
|
|
17561
19719
|
attemptFailed(transport, error) {
|
|
17562
19720
|
if (this.isDataLinkOpen) return;
|
|
@@ -17579,7 +19737,7 @@ var GhostLink = class {
|
|
|
17579
19737
|
side: attempt.side,
|
|
17580
19738
|
failed: attempt.failed,
|
|
17581
19739
|
...!attempt.failed.length && reason ? { reason } : {},
|
|
17582
|
-
...attempt.side === "dialled" ? { retryAt: Math.max(Date.now(), this.lastAutoConnectAt + this.dialWait()) } : {}
|
|
19740
|
+
...attempt.side === "dialled" && !this.awaitsContactDial() ? { retryAt: Math.max(Date.now(), this.lastAutoConnectAt + this.dialWait()) } : {}
|
|
17583
19741
|
};
|
|
17584
19742
|
traceLink(this.myPubKeyZ32, "attempt-ended", {
|
|
17585
19743
|
side: attempt.side,
|
|
@@ -17618,9 +19776,11 @@ var GhostLink = class {
|
|
|
17618
19776
|
if (this.pairedPending.size >= 128) this.pairedPending.delete(this.pairedPending.keys().next().value);
|
|
17619
19777
|
const id = stableId ?? toBase64Url(randomBytes(16));
|
|
17620
19778
|
if (!/^[A-Za-z0-9_-]{22}$/.test(id)) return "Invalid message ID";
|
|
19779
|
+
const frame = pairedMessageFrame(id, timestamp, trimmed, preview, reply, forwarded, card);
|
|
19780
|
+
if (frame.length > 61440) return PAIRED_FRAME_TOO_LARGE;
|
|
17621
19781
|
this.pairedPending.set(id, timestamp);
|
|
17622
19782
|
try {
|
|
17623
|
-
this.channel.send(
|
|
19783
|
+
this.channel.send(frame);
|
|
17624
19784
|
return null;
|
|
17625
19785
|
} catch {
|
|
17626
19786
|
this.pairedPending.delete(id);
|
|
@@ -17786,14 +19946,13 @@ var GhostLink = class {
|
|
|
17786
19946
|
if (presence.lastPacketAt === this.departed.packet && Date.now() < this.departed.until) return;
|
|
17787
19947
|
this.departed = null;
|
|
17788
19948
|
}
|
|
17789
|
-
|
|
17790
|
-
if (role === "inviter") {
|
|
19949
|
+
if ((!!this.tracker && !this.tracker.done && this.tracker.progress.stage !== "on-dht" ? this.options.pairingProgress?.role : void 0) === "inviter") {
|
|
17791
19950
|
this.peerSeenAt ||= Date.now();
|
|
17792
19951
|
if (Date.now() - this.peerSeenAt < 2500 && !this.nothingToAwait()) {
|
|
17793
19952
|
this.session.expectPeer();
|
|
17794
19953
|
return;
|
|
17795
19954
|
}
|
|
17796
|
-
} else if (
|
|
19955
|
+
} else if (this.awaitsContactDial()) {
|
|
17797
19956
|
if (Date.now() - presenceSeenAt(presence) < 3e4 && presence.lastPacketAt !== this.offerAwaitedFor && !this.nothingToDialEitherWay()) {
|
|
17798
19957
|
this.offerAwaitedFor = presence.lastPacketAt;
|
|
17799
19958
|
this.session.expectPeer();
|
|
@@ -18339,23 +20498,30 @@ var GhostLink = class {
|
|
|
18339
20498
|
* kept to send later. While the channel already holds a quarter of what it may queue (its send budget, or the high
|
|
18340
20499
|
* water mark of a WebRTC channel), the frame is refused with `too-fast`, so the chat's messages and pings never wait
|
|
18341
20500
|
* behind an app's data on a link slower than it sends (WISP 1200 § In a chat). While a file goes, that quarter sits
|
|
18342
|
-
* above what files may hold (`filesWritable`): files keep the channel at their own line for the whole transfer.
|
|
20501
|
+
* above what files may hold (`filesWritable`): files keep the channel at their own line for the whole transfer. While
|
|
20502
|
+
* the file waits for room, the app gets no further than `APP_AHEAD_OF_FILES` ahead of it, so it cannot refill the
|
|
20503
|
+
* channel each time it drains to files' line and keep the file from moving.
|
|
18343
20504
|
*/
|
|
18344
20505
|
sendAppData(app, data) {
|
|
18345
20506
|
if (!this.apps.open.has(app)) return "not-open";
|
|
18346
20507
|
if (appDataBytes(data) > 32768) return "too-large";
|
|
18347
20508
|
if (!this.channel || !this.isDataLinkOpen) return "offline";
|
|
18348
20509
|
if (!this.supportsApps) return "peer-closed";
|
|
18349
|
-
const
|
|
18350
|
-
|
|
20510
|
+
const use = this.filesUse?.channel === this.channel ? this.filesUse : null;
|
|
20511
|
+
const files = use && (use.waiting > 0 || Date.now() - use.at < FILES_IN_USE_MS) ? use : null;
|
|
20512
|
+
if (use && !files) use.appAhead = 0;
|
|
20513
|
+
if (this.channel.bufferedAmount >= (files ? filesCap(this.channel) : 0) + (this.channel.sendBudget ?? LIMITS.sendHighWaterMark) / 4) return "too-fast";
|
|
20514
|
+
if (files && files.waiting > 0 && files.appAhead >= APP_AHEAD_OF_FILES) return "too-fast";
|
|
18351
20515
|
const refused = this.apps.canSend(app, data);
|
|
18352
20516
|
if (refused) return refused;
|
|
20517
|
+
const frame = JSON.stringify(appDataFrame(app, data));
|
|
18353
20518
|
try {
|
|
18354
|
-
this.channel.send(
|
|
18355
|
-
return null;
|
|
20519
|
+
this.channel.send(frame);
|
|
18356
20520
|
} catch {
|
|
18357
20521
|
return "offline";
|
|
18358
20522
|
}
|
|
20523
|
+
if (files) files.appAhead = Math.min(files.appAhead + frame.length, APP_AHEAD_OF_FILES + APP_DATA_MAX_BYTES);
|
|
20524
|
+
return null;
|
|
18359
20525
|
}
|
|
18360
20526
|
/** What the contact had open is gone (`apps/1` is no longer agreed, or this link stopped): each app hears it now. */
|
|
18361
20527
|
appsEnded() {
|
|
@@ -18405,25 +20571,32 @@ var GhostLink = class {
|
|
|
18405
20571
|
sendFileData(id, offset, data) {
|
|
18406
20572
|
if (!this.supportsLargeFiles || !this.channel) return false;
|
|
18407
20573
|
try {
|
|
18408
|
-
this.
|
|
20574
|
+
const frame = this.supportsBinaryFiles ? encodeFileData(id, offset, data) : JSON.stringify({
|
|
18409
20575
|
t: "pf-data",
|
|
18410
20576
|
id,
|
|
18411
20577
|
offset,
|
|
18412
20578
|
data: toBase64Url(data)
|
|
18413
|
-
})
|
|
18414
|
-
this.
|
|
20579
|
+
});
|
|
20580
|
+
this.channel.send(frame);
|
|
20581
|
+
const use = this.filesUsing(this.channel);
|
|
20582
|
+
use.at = Date.now();
|
|
20583
|
+
use.appAhead = Math.max(0, use.appAhead - (typeof frame === "string" ? frame.length : frame.byteLength));
|
|
18415
20584
|
return true;
|
|
18416
20585
|
} catch {
|
|
18417
20586
|
return false;
|
|
18418
20587
|
}
|
|
18419
20588
|
}
|
|
18420
|
-
/**
|
|
20589
|
+
/**
|
|
20590
|
+
* Files on a channel: when they last wrote on it, how many wait for room, and how many bytes an app's data has sent
|
|
20591
|
+
* on it beyond theirs since. An app's data gets room above theirs.
|
|
20592
|
+
*/
|
|
18421
20593
|
filesUse = null;
|
|
18422
20594
|
filesUsing(channel) {
|
|
18423
20595
|
if (this.filesUse?.channel !== channel) this.filesUse = {
|
|
18424
20596
|
channel,
|
|
18425
20597
|
at: 0,
|
|
18426
|
-
waiting: 0
|
|
20598
|
+
waiting: 0,
|
|
20599
|
+
appAhead: 0
|
|
18427
20600
|
};
|
|
18428
20601
|
return this.filesUse;
|
|
18429
20602
|
}
|
|
@@ -25620,10 +27793,9 @@ function knockIdentity(link) {
|
|
|
25620
27793
|
function knockRecords(link, knocks) {
|
|
25621
27794
|
const nonce = randomBytes(NONCE_LENGTH);
|
|
25622
27795
|
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);
|
|
25624
27796
|
return [{
|
|
25625
27797
|
label: KNOCK_LABEL,
|
|
25626
|
-
value: toBase64Url(concatBytes$1(nonce,
|
|
27798
|
+
value: toBase64Url(concatBytes$1(nonce, xchacha20poly1305(knockMaterial(link, "knock key"), nonce, utf8Encode(link.g)).encrypt(body)))
|
|
25627
27799
|
}];
|
|
25628
27800
|
}
|
|
25629
27801
|
/** Knocks that open with the link's key and are well formed; anything else is nobody. */
|
|
@@ -27245,8 +29417,13 @@ var CommunitySession = class {
|
|
|
27245
29417
|
messageIdOf(f) {
|
|
27246
29418
|
return f.s === this.myKey && this.resealedFrom.get(frameKey(f)) || communityMessageId(f.s, f.e, f.h, f.n);
|
|
27247
29419
|
}
|
|
27248
|
-
/**
|
|
27249
|
-
|
|
29420
|
+
/**
|
|
29421
|
+
* Where I am and what I have, for the member at the other end of an edge that just opened. `to`: that member, now
|
|
29422
|
+
* asked: my answer to its own sync asks nothing more for a while. A second sync went, and its answer took the place of
|
|
29423
|
+
* what was left of the first from its start: a member back after 256 messages was handed 288 (2026-10-14).
|
|
29424
|
+
*/
|
|
29425
|
+
syncFrame(to) {
|
|
29426
|
+
if (to) this.asked.set(to, this.hooks.clock?.() ?? Date.now());
|
|
27250
29427
|
const have = {};
|
|
27251
29428
|
const recent = new Set(this.state.store.map((f) => seenKey(f.e, f.h)));
|
|
27252
29429
|
for (const [sender, byKey] of Object.entries(this.state.seen)) for (const [key, entry] of Object.entries(byKey)) {
|
|
@@ -28809,6 +30986,6 @@ function appStoreDecision(held, read) {
|
|
|
28809
30986
|
return read.digest === held.digest ? "same" : "equivocation";
|
|
28810
30987
|
}
|
|
28811
30988
|
//#endregion
|
|
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 };
|
|
30989
|
+
export { COMMUNITY_EDIT_FRAME as $, cardUrl as $a, budgetRetryMs as $c, PeerProofs as $i, GROUP_WANT_FRAME as $n, HTTP_SERVICE_PROTO as $o, deviceWakeRequest as $r, enrollCancelFrame as $s, didDhtFromPublicKey as $t, CommunitySession as A, COMMUNITY_REACTION_FRAME as Aa, TURN_PUT_WINDOW_MS as Ac, normalizeDomain as Ai, pubkyProofPath as An, DirectPathWatch as Ao, rankHubs as Ar, handoffStreamKey as As, verifyAtprotoRecordProof as At, knockRecords as B, wireReaction as Ba, signTurnPacket as Bc, IDENTITY_MAX_EVIDENCE as Bi, isFederationId as Bn, LIMITS as Bo, TYPING_REFRESH_MS as Br, readHandoffVerified as Bs, didWebDocumentUrl as Bt, sha256Paced as C, EDIT_BUFFER_MS as Ca, deviceEchoNonce as Cc, decodeDnsResponse as Ci, PUBKY_PROOF_MAX_BYTES as Cn, GROUP_MEMBER_CAP as Co, freshHubs as Cr, handoffEphemeral as Cs, atprotoProofRkey as Ct, openGroupTyping as D, parseLinkPreview as Da, newDeviceSetSecret as Dc, encodeDnsQuery as Di, normalizePubkyKey as Dn, rosterHas as Do, mergeLobby as Dr, handoffPakeFrame as Ds, parseAtprotoDidDocument as Dt, groupTypingEpoch as E, dhtEditId as Ea, devicePingFrame as Ec, domainWellKnownFile as Ei, newPubkyProofFolder as En, commitHash as Eo, mergeBeacon as Er, handoffOfferFrame as Es, normalizeAtprotoHandle as Et, KNOCK_TTL_MS as F, queueReaction as Fa, TURN_LAST_SEQUENCE as Fc, wellKnownUrl as Fi, decodeBolt11 as Fn, presenceSeenAt as Fo, ChatFiles as Fr, readHandoffDone as Fs, checkDidJws as Ft, groupWakeFrame as G, ITEM_STATES as Ga, turnStatedVerifier as Gc, boundVerifiedIdentity as Gi, LIVENESS_PING_MS as Gn, answersOffer as Go, PIN_LIMITS as Gr, setAckFrame as Gs, normalizeDid as Gt, readKnocks as H, readReply as Ha, turnInstance as Hc, IDENTITY_REVOCATION_LABEL as Hi, parseFedimintRequestPayload as Hn, encodeControl as Ho, sanitizeTypingStatus as Hr, SET_ACK as Hs, didWebFileNames as Ht, decodeGroupEntryLink as I, reactionEmoji as Ia, TURN_MAX as Ic, CapsExchange as Ii, ENDPOINT as In, GhostlyHttpError as Io, FILE_LIMITS as Ir, readHandoffHave as Is, checkDidSignature as It, GROUP_LIMITS as J, RUN_RESULTS as Ja, RelayTransport as Jc, identityStatement as Ji, pairedMessageFrame as Jn, callSignalHeardAt as Jo, pinIsNewer as Jr, verifySetUpdate as Js, parseDidSignaturePaste as Jt, groupWakes as K, PR_CHECKS as Ka, turnVerifierMark as Kc, emptyIdentityLedger as Ki, PAIRED_FRAME_TOO_LARGE as Kn, buildSdpFromSignal as Ko, mayPin as Kr, setUpdateFrame as Ks, parseDid as Kt, encodeGroupEntryLink as L, reactionIsNewer as La, firstDeviceSetSecret as Lc, DHT_TEXT_CAPABILITY as Li, cashuRequestPayload as Ln, formatLocalTarget as Lo, transferEnded as Lr, readHandoffHello as Ls, didGhostlyServiceEntry as Lt, communityHasFrame as M, REACTION_LIMITS as Ma, TURN_SETTLE_MS as Mc, recordsFromWellKnown as Mi, pubkyHomeserverOf as Mn, DHT_POLL_INTERVALS as Mo, readBeaconHead as Mr, newHandoffId as Ms, DID_FILE_MAX_BYTES as Mt, decodeCommunityLink as N, ReactionWindow as Na, classifyTurnRead as Nc, recordsName as Ni, requestLnurlInvoice as Nn, EXPECT_PEER_MS as No, readLobby as Nr, readHandoffAck as Ns, DID_JWS_MAX_LENGTH as Nt, sealGroupTyping as O, forwardedAgain as Oa, seedSigner as Oc, isPublicAddress as Oi, pubkyProofCapability as On, verifyCommitSignature as Oo, newerHead as Or, handoffReleaseFrame as Os, shortAtprotoDid as Ot, encodeCommunityLink as P, nextReactionNumber as Pa, turnPutSummary as Pc, txtValue as Pi, resolveLightningDestination as Pn, RELAY_POLL_INTERVALS as Po, shouldBeHub as Pr, readHandoffCancel as Ps, DidError as Pt, frameBytes as Q, cardLine as Qa, traceLink as Qc, lastSharedWithMe as Qi, GROUP_HAVE_FRAME as Qn, emptyLinkRecords as Qo, WakeLimiter as Qr, ENROLL_PROOF as Qs, didDhtDocument as Qt, entryParams as R, readReaction as Ra, nextTurnPosition as Rc, capsDescriptors as Ri, fedimintRequestPayload as Rn, parseLocalTarget as Ro, transferStalled as Rr, readHandoffPake as Rs, didJwsAlg as Rt, readAppBundleAsync as S, parseVideoMeta as Sa, deviceEchoFrame as Sc, DOMAIN_FILE_MAX_BYTES as Si, isBitcoinAddress as Sn, readCanonicalJson as So, doorHubs as Sr, handoffDigest as Ss, atprotoProofRecord as St, GroupTypingBoard as T, RateWindow as Ta, deviceLinkPairing as Tc, domainTxtRecord as Ti, isPubkyProofFolder as Tn, MEMBER_KEY as To, lobbyRecords as Tr, handoffManifestFrames as Ts, isAtprotoDid as Tt, GROUP_WAKE_FRAME as U, replySnippet as Ua, turnKeys as Uc, IdentityCheckUnavailable as Ui, GhostLink as Un, CALL_SIGNAL_MAX_AGE_MS as Uo, typingActivity as Ur, SET_UPDATE as Us, didWebFileUrl as Ut, mergeKnocks as V, pairedReplyAuthor as Va, signTurnRelease as Vc, IDENTITY_MAX_VALIDITY as Vi, parseCashuRequestPayload as Vn, decodeControl as Vo, TypingSender as Vr, sealHandoffPiece as Vs, didWebFile as Vt, GroupWakeLimiter as W, BUTTON_ID as Wa, turnName as Wc, IdentityExchange as Wi, INVITE_TAKEN as Wn, RTC_CONFIG as Wo, GROUP_PIN_FRAME as Wr, readSetUpdate as Ws, didWebHost as Wt, GROUP_READ_NOTE as X, appCardId as Xa, normalizeRelayUrl as Xc, receivedIdentityStatus as Xi, engineText as Xn, parseCallSignal as Xo, readPin as Xr, ENROLL_GRANT as Xs, staticDidDocument as Xt, GROUP_REACTED_FRAME as Y, STATUS_CARD_LIMITS as Ya, currentRelays as Yc, newIdentityBinding as Yi, engineError as Yn, extractParamsFromSdp as Yo, pinNumberHolds as Yr, ENROLL_DONE as Ys, signingKeys as Yt, GroupSession as Z, buttonLabelClash as Za, setLinkTraceSink as Zc, revokesIdentity as Zi, GROUP_FILE_LIMITS as Zn, signalHasVideo as Zo, WAKE_CALL_INTERVAL_MS as Zr, ENROLL_HELLO as Zs, strictBase64Url as Zt, assembleAppBundle as _, VOICE_LIMITS as _a, inviteOwnership as _c, HoldKeys as _i, randomBytes as _l, walletNetworkOf as _n, readAppRevocations as _o, meshHubs as _r, handoffBusyFrame as _s, ATPROTO_DECLARED_SCOPE as _t, planAppUpdate as a, irohRelayUrl as aa, enrollProofFrame as ac, vapidKeysMatch as ai, newerPacket as al, EVM_TEST_CHAINS as an, heardTime as ao, readGroupHave as ar, HANDOFF_HAVE as as, groupName as at, pacer as b, parseVoiceMeta as ba, ENROLL_CAPABILITY as bc, newMailbox as bi, utf8Decode as bl, sparkAddressKind as bn, canonicalJson as bo, beaconKeys as br, handoffContextProof as bs, ATPROTO_PROOF_SCOPE as bt, readAppStore as c, peekDhtMailbox as ca, verifyEnrollProof as cc, isAppVersion as ci, createIdentity as cl, SEPOLIA_TEST_USDT as cn, APP_PREFIXES as co, CarriedTransport as cr, HANDOFF_PAKE as cs, SshSigError as ct, APP_BUNDLE_LIMITS as d, parseImageMeta as da, readDeviceInvite as dc, EDIT_CAPABILITY as di, publicKeyFromZ32 as dl, USDT_PUBLIC_RPC as dn, appRef as do, SIGNAL_HOLD_MS as dr, HANDOFF_TIMINGS as ds, sameSshKey as dt, emptyProofLedger as ea, enrollCancelReason as ec, newWakeToken as ei, isDiscoveryBudgetError as el, encodeDidDhtDeactivated as en, checkStatusCard as eo, GROUP_WANT_NO_FRAME as er, LEGACY_SERVICES as es, GROUP_EDIT_FRAME as et, APP_BUNDLE_MAGIC as f, readImageMeta as fa, OwnInviteError as fc, STATUS_CARD_CAPABILITY as fi, verify as fl, WALLET_NETWORKS as fn, checkAppRevokeStatement as fo, SIGNAL_REPLY_MS as fr, HANDOFF_VERIFIED as fs, sshSignCommand as ft, appViewOf as g, PLAYABLE_AUDIO as ga, inviteLink as gc, HOLD_LIMITS as gi, fromBase64Url as gl, validatePaymentTarget as gn, readAppRevocation as go, mayBeHub as gr, handoffAttemptSucceeded as gs, decodeAnyBitcoinAddress as gt, appDigest as h, sanitizeMime as ha, encodeInviteCode as hc, HOLD_DATED_BACK_MS as hi, bytesEqual as hl, formatPaymentAmount as hn, isAppRef as ho, MESH_HUBS as hr, handoffAttemptFailed as hs, BitcoinAddressError as ht, checkAppBeforeRun as i, irohDescriptor as ia, enrollHelloFrame as ic, checkPushEndpoint as ii, createRelayPayload as il, ETHEREUM_USDT as in, claimedTime as io, readGroupFileMeta as ir, HANDOFF_DONE as is, mentionsMember as it, GROUP_READ_NOTE_COMMUNITY as j, GROUP_REACTION_FRAME as ja, TURN_RAISE_READ_TIMEOUT_MS as jc, recordsFromTxt as ji, homeserverWebEndpoint as jn, AWAITING_PEER_MS as jo, readBeacon as jr, handoffVersions as js, DID_DOCUMENT_MAX_BYTES as jt, COMMUNITY_LIMITS as k, readForwarded as ka, webCryptoSigner as kc, nip05RootKey as ki, pubkyProofFile as kn, edgeParams as ko, pickHubs as kr, handoffRetryAfter as ks, AtprotoProofError as kt, readAppStoreSteps as l, DHT_FILE_CAPABILITY as la, createDeviceInvite as lc, APPS_CAPABILITY as li, identityFromSeed as ll, SEPOLIA_TEST_USDT_FAUCET as ln, APP_STATEMENT_LIMITS as lo, GROUP_SIGNAL_FRAME as lr, HANDOFF_PIECE_BYTES as ls, parseSshPublicKey as lt, APP_PERMISSIONS as m, sanitizeFileName as ma, decodeInviteCode as mc, HOLD_CAPABILITY as mi, sanitizeAvatar as ml, assertWholeSats as mn, isAppKey as mo, readGroupSignal as mr, handoffAttemptAllowed as ms, verifyBitcoinMessage as mt, appStoreDecision as n, TRANSPORTS as na, enrollDoneFrame as nc, relayRequest as ni, PacketTooLargeError as nl, resolveDidDht as nn, statusCardText as no, groupFileFallback as nr, HANDOFF_ACK as ns, carryMentions as nt, APP_STORE_LIMITS as o, DHT_FILE_TOO_LARGE as oa, readEnrollGrant as oc, APP_DATA_MAX_BYTES as oi, openRelayPayload as ol, ONCHAIN_PROVIDER as on, receivedTimestamp as oo, readGroupWant as or, HANDOFF_HELLO as os, SSHSIG_MAX_ARMOR as ot, APP_ICON_PATH as p, formatFileSize as pa, createChatInvite as pc, UPGRADE_CAPABILITY as pi, MAX_AVATAR_LENGTH as pl, assertTokenUnits as pn, isAppHash as po, groupSignalFrame as pr, filePartName as ps, verifySshSignature as pt, parseGroupWakeFrame as q, PR_STATES as qa, DEFAULT_RELAYS as qc, identityRevocationValue as qi, pairedMessageFits as qn, callRtcConfig as qo, nextPinNumber as qr, signTombstone as qs, parseDidDocument as qt, appUpdateDecision as r, automaticTransport as ra, enrollGrantFrame as rc, wakeRequest as ri, RELAY_PAYLOAD_MAX_BYTES as rl, signDidDhtPacket as rn, arrivalKey as ro, isGroupMessageId as rr, HANDOFF_CANCEL as rs, parseCommunityEdit as rt, checkAppStoreIndex as s, DHT_MESSAGE_TTL as sa, readEnrollHello as sc, chatAppId as si, parseRelayPayload as sl, PaymentPreflightError as sn, shownTime as so, readGroupWantNo as sr, HANDOFF_OFFER as ss, SSHSIG_NAMESPACE as st, appRevocationFor as t, validExternalKey as ta, enrollDigits as tc, parseWakeFrame as ti, withRequestOptions as tl, encodeDidDhtPacket as tn, sameButtonText as to, fetchesByItself as tr, serviceIdFromName as ts, GROUP_EDIT_TEXT_BYTES as tt, signAppStore as u, dhtFileId as ua, deviceInviteJoinerParams as uc, BUTTONS_CAPABILITY as ui, identityFromSeedB64 as ul, TEST_USDT_FAUCET_AMOUNT as un, appFingerprint as uo, SIGNAL_FRESH_MS as ur, HANDOFF_REQUEST as us, parseSshSignature as ut, checkAppManifest as v, baseMime as va, ownInviteRefusal as vc, HoldRefusedError as vi, toBase64Url as vl, SPARK_NETWORKS as vn, signAppObject as vo, pickMeshHubs as vr, handoffCancelFrame as vs, ATPROTO_FULL_SCOPE as vt, GROUP_TYPING_FRAME as w, EDIT_RATE_WINDOW_MS as wa, deviceKeyZ32 as wc, dnsProofName as wi, isPubkyKey as wn, MAX_GROUP_CHAIN as wo, lobbyKeys as wr, handoffHelloFrame as ws, checkAtprotoProofRecord as wt, readAppBundle as x, voicePeaksOf as xa, HANDOFF_CAPABILITY as xc, readManifest as xi, utf8Encode as xl, sparkInvoiceDetails as xn, canonicalJsonBytes as xo, beaconRecords as xr, handoffDataFrame as xs, atprotoDidMethod as xt, isAppUrl as y, fileMessageText as ya, DEVICES_CAPABILITY as yc, isHoldUrl as yi, toZ32 as yl, SPARK_PROVIDER as yn, signAppRevocation as yo, COMMUNITY_TOPOLOGY as yr, handoffContext as ys, ATPROTO_PROOF_COLLECTION as yt, knockIdentity as z, validReactionNumber as za, readTurnPacket as zc, dialDescriptors as zi, findEndpoint as zn, webLocalFetch as zo, TYPING_KINDS as zr, readHandoffTurnFrame as zs, didSignedMessages as zt };
|
|
28813
30990
|
|
|
28814
|
-
//# sourceMappingURL=src-
|
|
30991
|
+
//# sourceMappingURL=src-DDzSERyc.js.map
|