@ghostlytools/cli 1.3.0 → 1.3.2
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-BiaX6l6f.js → api-SYPJgFRU.js} +2 -2
- package/dist/assets/{api-CrxVxtY1.js → api-aup3Amud.js} +141 -40
- package/dist/assets/api-aup3Amud.js.map +1 -0
- package/dist/assets/{apiKit-CajiDxdK.js → apiKit-DYkVEs6r.js} +30 -30
- package/dist/assets/apiKit-DYkVEs6r.js.map +1 -0
- package/dist/assets/{apps-t23gkc28.js → apps-DcZKu29s.js} +1 -1
- package/dist/assets/{apps-CbfiCSIw.js → apps-Rbk2HHXb.js} +3 -4
- package/dist/assets/{apps-CbfiCSIw.js.map → apps-Rbk2HHXb.js.map} +1 -1
- package/dist/assets/{arkade-D4BVJFEQ.js → arkade-CuWlNnCz.js} +4 -4
- package/dist/assets/arkade-CuWlNnCz.js.map +1 -0
- package/dist/assets/{backup-BAMi2lAq.js → backup-CJWLDHuS.js} +8 -8
- package/dist/assets/backup-CJWLDHuS.js.map +1 -0
- package/dist/assets/{backup-Cg52WeQi.js → backup-CSMw2GHv.js} +1 -1
- 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-CGRqZ7z0.js +2 -0
- package/dist/assets/{buttons-CX1pKB0j.js → buttons-QEnAGWoC.js} +4 -5
- package/dist/assets/{buttons-CX1pKB0j.js.map → buttons-QEnAGWoC.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-K27mzoyu.js} +2 -2
- package/dist/assets/{envelope-BvU0fLEt.js.map → envelope-K27mzoyu.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/fedimint_client_wasm_bg-BtHHlFpp.js.map +1 -1
- package/dist/assets/groupFiles-DHTrTA4I.js +3229 -0
- package/dist/assets/groupFiles-DHTrTA4I.js.map +1 -0
- package/dist/assets/{host-Cmd205Dc.js → host-C7hA-Za7.js} +135 -42
- package/dist/assets/host-C7hA-Za7.js.map +1 -0
- package/dist/assets/{hyperdhtRelay-CEmBpT0Q.js → hyperdhtRelay-BVoWY2ua.js} +2 -3
- package/dist/assets/{hyperdhtRelay-CEmBpT0Q.js.map → hyperdhtRelay-BVoWY2ua.js.map} +1 -1
- package/dist/assets/{irohWeb--YyEA-hX.js → irohWeb-BJPk9bMn.js} +2 -3
- package/dist/assets/{irohWeb--YyEA-hX.js.map → irohWeb-BJPk9bMn.js.map} +1 -1
- package/dist/assets/{main-BzNN6bkx.js → main-DC8cUkDx.js} +142 -35
- package/dist/assets/main-DC8cUkDx.js.map +1 -0
- package/dist/assets/{mainline-D5D5C-jT.js → mainline-CxbxpoU4.js} +2 -2
- package/dist/assets/{mainline-D5D5C-jT.js.map → mainline-CxbxpoU4.js.map} +1 -1
- package/dist/assets/{networkReason-DxZWHw6v.js → networkReason-D42iNuUB.js} +2 -2
- package/dist/assets/{networkReason-DxZWHw6v.js.map → networkReason-D42iNuUB.js.map} +1 -1
- package/dist/assets/nwc-Cm4sYgLv.js +612 -0
- package/dist/assets/nwc-Cm4sYgLv.js.map +1 -0
- package/dist/assets/nwcProvider-0D7vdPDd.js +77 -0
- package/dist/assets/nwcProvider-0D7vdPDd.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-CLolR0kM.js +2 -0
- package/dist/assets/{groupFiles-BNTSDaoq.js → registry-CVdSZW5l.js} +19 -3911
- package/dist/assets/registry-CVdSZW5l.js.map +1 -0
- package/dist/assets/resolve-BnlEajYz.js +591 -0
- package/dist/assets/resolve-BnlEajYz.js.map +1 -0
- package/dist/assets/{server-CBCEYKMN.js → server-CJNEKxzE.js} +284 -1202
- package/dist/assets/server-CJNEKxzE.js.map +1 -0
- package/dist/assets/{src-DhfGgAUu.js → src-yMUQvdyL.js} +2264 -71
- package/dist/assets/src-yMUQvdyL.js.map +1 -0
- package/dist/assets/{turnRefs-ChEfgbSX.js → turnRefs-B2a8to9q.js} +2 -2
- package/dist/assets/{turnRefs-ChEfgbSX.js.map → turnRefs-B2a8to9q.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-DesFeDiG.js} +3 -3
- package/dist/assets/{usdt-C1Xao2xH.js.map → usdt-DesFeDiG.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/arkade-D4BVJFEQ.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.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
|
/**
|
|
@@ -6740,6 +7226,8 @@ function parseCommit$1(raw) {
|
|
|
6740
7226
|
sig: c.sig
|
|
6741
7227
|
};
|
|
6742
7228
|
}
|
|
7229
|
+
/** Only shape, no signature or parent: a commit as kept, its fields alone (anything else it carried dropped); null when malformed. */
|
|
7230
|
+
var parseGroupCommit = parseCommit$1;
|
|
6743
7231
|
/** The roster a commit must carry, given the previous one; null when the change is not allowed. */
|
|
6744
7232
|
function expectedRoster(previous, kind, by, subject) {
|
|
6745
7233
|
if (kind === "create") return previous === null && !subject ? [[by, "admin"]] : null;
|
|
@@ -6794,29 +7282,1020 @@ function verifyChain(raw, groupId) {
|
|
|
6794
7282
|
return { chain };
|
|
6795
7283
|
}
|
|
6796
7284
|
/**
|
|
6797
|
-
* What a commit is refused with while admin work is off on this device (WISP 06 § Forced takeover: after a forced
|
|
6798
|
-
* takeover or a restore, until the person turns on "Manage groups from this device").
|
|
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.
|
|
7285
|
+
* What a commit is refused with while admin work is off on this device (WISP 06 § Forced takeover: after a forced
|
|
7286
|
+
* takeover or a restore, until the person turns on "Manage groups from this device").
|
|
7287
|
+
*/
|
|
7288
|
+
/**
|
|
7289
|
+
* The highest sequence number a frame of my own key, sent by another copy of my profile, may make me adopt (WISP 06):
|
|
7290
|
+
* the highest floor a copy can have (2^32 - 2^24) and room above it. The community beacon packs the counter in 32 bits.
|
|
7291
|
+
*/
|
|
7292
|
+
var OWN_FRAME_LIMIT = 2 ** 32 - 2 ** 23;
|
|
7293
|
+
/**
|
|
7294
|
+
* Frames said again (WISP 902, both profiles): a member's own frames that no edge took when it said them are
|
|
7295
|
+
* remembered (`kept`, the latest) and, once the chain it follows is newer than such a frame's and has not moved for
|
|
7296
|
+
* `settledMs`, looked at once for saying again where the members let in while it was behind can read them. A catch-up
|
|
7297
|
+
* brings commits one by one, and a frame said again at the first of them would still miss those let in by the next.
|
|
7298
|
+
*/
|
|
7299
|
+
var SAID_AGAIN = {
|
|
7300
|
+
settledMs: 5e3,
|
|
7301
|
+
kept: 64
|
|
7302
|
+
};
|
|
7303
|
+
/** `alone` with `key` added last, keeping only keys still `present` (frames still in the log), the latest `kept`. */
|
|
7304
|
+
function rememberAlone(alone, key, present) {
|
|
7305
|
+
return [...(alone ?? []).filter((k) => k !== key && present(k)), key].slice(-SAID_AGAIN.kept);
|
|
7306
|
+
}
|
|
7307
|
+
var GROUP_ADMIN_OFF_ERROR = "Managing this group is off on this device. Turn on Manage groups from this device first.";
|
|
7308
|
+
//#endregion
|
|
7309
|
+
//#region ../core/src/canonicalJson.ts
|
|
7310
|
+
/** True when `s` holds a lone UTF-16 surrogate, which I-JSON (and so JCS) does not allow. */
|
|
7311
|
+
function hasLoneSurrogate(s) {
|
|
7312
|
+
for (let i = 0; i < s.length; i++) {
|
|
7313
|
+
const c = s.charCodeAt(i);
|
|
7314
|
+
if (c >= 55296 && c <= 56319) {
|
|
7315
|
+
const next = s.charCodeAt(i + 1);
|
|
7316
|
+
if (next >= 56320 && next <= 57343) {
|
|
7317
|
+
i++;
|
|
7318
|
+
continue;
|
|
7319
|
+
}
|
|
7320
|
+
return true;
|
|
7321
|
+
}
|
|
7322
|
+
if (c >= 56320 && c <= 57343) return true;
|
|
7323
|
+
}
|
|
7324
|
+
return false;
|
|
7325
|
+
}
|
|
7326
|
+
function write(value, depth) {
|
|
7327
|
+
if (depth > 64) throw new Error("JSON nested deeper than 64 levels");
|
|
7328
|
+
if (value === null) return "null";
|
|
7329
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
7330
|
+
if (typeof value === "number") {
|
|
7331
|
+
if (!Number.isFinite(value)) throw new Error("JSON numbers must be finite");
|
|
7332
|
+
return JSON.stringify(value);
|
|
7333
|
+
}
|
|
7334
|
+
if (typeof value === "string") {
|
|
7335
|
+
if (hasLoneSurrogate(value)) throw new Error("A JSON string holds a lone surrogate");
|
|
7336
|
+
return JSON.stringify(value);
|
|
7337
|
+
}
|
|
7338
|
+
if (Array.isArray(value)) return `[${value.map((v) => write(v, depth + 1)).join(",")}]`;
|
|
7339
|
+
if (typeof value === "object") {
|
|
7340
|
+
const record = value;
|
|
7341
|
+
return `{${Object.keys(value).filter((k) => record[k] !== void 0).sort((a, b) => a < b ? -1 : a > b ? 1 : 0).map((k) => {
|
|
7342
|
+
if (hasLoneSurrogate(k)) throw new Error("A JSON key holds a lone surrogate");
|
|
7343
|
+
return `${JSON.stringify(k)}:${write(record[k], depth + 1)}`;
|
|
7344
|
+
}).join(",")}}`;
|
|
7345
|
+
}
|
|
7346
|
+
throw new Error(`Not a JSON value: ${typeof value}`);
|
|
7347
|
+
}
|
|
7348
|
+
/** The canonical text of a JSON value. Throws on a value JSON cannot hold (a non-finite number, a lone surrogate). */
|
|
7349
|
+
function canonicalJson(value) {
|
|
7350
|
+
return write(value, 0);
|
|
7351
|
+
}
|
|
7352
|
+
/** The canonical UTF-8 bytes of a JSON value. */
|
|
7353
|
+
function canonicalJsonBytes(value) {
|
|
7354
|
+
return utf8Encode(canonicalJson(value));
|
|
7355
|
+
}
|
|
7356
|
+
/**
|
|
7357
|
+
* Reads bytes that must be the canonical form of a JSON value: `not-json` when they are not UTF-8 JSON, `not-canonical`
|
|
7358
|
+
* when writing the value again gives other bytes.
|
|
7359
|
+
*/
|
|
7360
|
+
function readCanonicalJson(bytes) {
|
|
7361
|
+
let value;
|
|
7362
|
+
try {
|
|
7363
|
+
value = JSON.parse(utf8Decode(bytes));
|
|
7364
|
+
} catch (error) {
|
|
7365
|
+
return {
|
|
7366
|
+
ok: false,
|
|
7367
|
+
reason: "not-json",
|
|
7368
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
7369
|
+
};
|
|
7370
|
+
}
|
|
7371
|
+
let again;
|
|
7372
|
+
try {
|
|
7373
|
+
again = canonicalJsonBytes(value);
|
|
7374
|
+
} catch (error) {
|
|
7375
|
+
return {
|
|
7376
|
+
ok: false,
|
|
7377
|
+
reason: "not-canonical",
|
|
7378
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
7379
|
+
};
|
|
7380
|
+
}
|
|
7381
|
+
if (!bytesEqual(again, bytes)) return {
|
|
7382
|
+
ok: false,
|
|
7383
|
+
reason: "not-canonical"
|
|
7384
|
+
};
|
|
7385
|
+
return {
|
|
7386
|
+
ok: true,
|
|
7387
|
+
value
|
|
7388
|
+
};
|
|
7389
|
+
}
|
|
7390
|
+
//#endregion
|
|
7391
|
+
//#region ../core/src/appStatements.ts
|
|
7392
|
+
/** The prefixes of phase 1. The others of the WISP (reports, reviews, licences, rotation, publisher proofs) come later. */
|
|
7393
|
+
var APP_PREFIXES = {
|
|
7394
|
+
app: "ghostly-app/1",
|
|
7395
|
+
store: "ghostly-store/1",
|
|
7396
|
+
revoke: "ghostly-revoke/1"
|
|
7397
|
+
};
|
|
7398
|
+
var APP_STATEMENT_LIMITS = {
|
|
7399
|
+
/** A signature statement's canonical bytes: 52 + 86 characters of values and the keys around them. */
|
|
7400
|
+
signatureBytes: 1024,
|
|
7401
|
+
/** Digests named by one revocation. */
|
|
7402
|
+
revokeDigests: 64,
|
|
7403
|
+
/** A revocation's reason, in characters. */
|
|
7404
|
+
reason: 200,
|
|
7405
|
+
/** Revocations in one `ghostly-revoke.json`. */
|
|
7406
|
+
revocations: 4096
|
|
7407
|
+
};
|
|
7408
|
+
/** A z-base32 Ed25519 public key: 52 characters that decode to 32 bytes and encode back to the same text. */
|
|
7409
|
+
function isAppKey(value) {
|
|
7410
|
+
if (typeof value !== "string" || !/^[ybndrfg8ejkmcpqxot1uwisza345h769]{52}$/.test(value)) return false;
|
|
7411
|
+
const bytes = fromZ32(value);
|
|
7412
|
+
return bytes.length === 32 && toZ32(bytes) === value;
|
|
7413
|
+
}
|
|
7414
|
+
/** A SHA-256 in base64url without padding: 43 characters that decode to 32 bytes and encode back to the same text. */
|
|
7415
|
+
function isAppHash(value) {
|
|
7416
|
+
if (typeof value !== "string" || !/^[A-Za-z0-9_-]{43}$/.test(value)) return false;
|
|
7417
|
+
return toBase64Url(fromBase64Url(value)) === value;
|
|
7418
|
+
}
|
|
7419
|
+
/** An app reference, `<publisher key>/<name>`. */
|
|
7420
|
+
var APP_NAME = /^[a-z][a-z0-9-]{0,31}$/;
|
|
7421
|
+
function isAppRef(value) {
|
|
7422
|
+
if (typeof value !== "string") return false;
|
|
7423
|
+
return value.indexOf("/") === 52 && isAppKey(value.slice(0, 52)) && APP_NAME.test(value.slice(53));
|
|
7424
|
+
}
|
|
7425
|
+
function appRef(publisher, name) {
|
|
7426
|
+
return `${publisher}/${name}`;
|
|
7427
|
+
}
|
|
7428
|
+
/** The publisher key of a reference. */
|
|
7429
|
+
function appRefPublisher(ref) {
|
|
7430
|
+
return ref.slice(0, 52);
|
|
7431
|
+
}
|
|
7432
|
+
/**
|
|
7433
|
+
* The publisher fingerprint people see: the first 16 z-base32 characters of the key (80 bits), in four groups.
|
|
7434
|
+
*/
|
|
7435
|
+
function appFingerprint(key) {
|
|
7436
|
+
return (key.slice(0, 16).match(/.{1,4}/g) ?? []).join(" ");
|
|
7437
|
+
}
|
|
7438
|
+
/** The bytes a signature covers: the prefix, a zero byte, then the SHA-256 of the object's canonical bytes. */
|
|
7439
|
+
function appSignedBytes(prefix, canonical) {
|
|
7440
|
+
const p = utf8Encode(prefix);
|
|
7441
|
+
const out = new Uint8Array(p.length + 1 + 32);
|
|
7442
|
+
out.set(p, 0);
|
|
7443
|
+
out.set(sha256(canonical), p.length + 1);
|
|
7444
|
+
return out;
|
|
7445
|
+
}
|
|
7446
|
+
/** Signs an object under a prefix: its canonical bytes and the signature statement. */
|
|
7447
|
+
async function signAppObject(prefix, value, signer) {
|
|
7448
|
+
const bytes = canonicalJsonBytes(value);
|
|
7449
|
+
const sig = await signer.sign(appSignedBytes(prefix, bytes));
|
|
7450
|
+
return {
|
|
7451
|
+
bytes,
|
|
7452
|
+
signature: {
|
|
7453
|
+
alg: "ed25519",
|
|
7454
|
+
key: toZ32(signer.publicKey),
|
|
7455
|
+
sig: toBase64Url(sig)
|
|
7456
|
+
}
|
|
7457
|
+
};
|
|
7458
|
+
}
|
|
7459
|
+
/**
|
|
7460
|
+
* Ed25519 verification as RFC 8032 writes it (`zip215: false`): canonical encodings of the point and the scalar only,
|
|
7461
|
+
* so every parser (this one, a Rust one with `verify_strict`) accepts exactly the same signatures.
|
|
7462
|
+
*/
|
|
7463
|
+
function strictVerify(signature, message, publicKey) {
|
|
7464
|
+
try {
|
|
7465
|
+
return ed25519.verify(signature, message, publicKey, { zip215: false });
|
|
7466
|
+
} catch {
|
|
7467
|
+
return false;
|
|
7468
|
+
}
|
|
7469
|
+
}
|
|
7470
|
+
var exactKeys$1 = (value, keys, optional = []) => {
|
|
7471
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
7472
|
+
const have = Object.keys(value);
|
|
7473
|
+
return keys.every((k) => have.includes(k)) && have.every((k) => keys.includes(k) || optional.includes(k));
|
|
7474
|
+
};
|
|
7475
|
+
/** Checks the shape of a signature statement: exactly `alg`, `key` and `sig`, a 64-byte signature. */
|
|
7476
|
+
function isAppSignature(value) {
|
|
7477
|
+
if (!exactKeys$1(value, [
|
|
7478
|
+
"alg",
|
|
7479
|
+
"key",
|
|
7480
|
+
"sig"
|
|
7481
|
+
])) return false;
|
|
7482
|
+
if (value.alg !== "ed25519" || !isAppKey(value.key)) return false;
|
|
7483
|
+
return typeof value.sig === "string" && /^[A-Za-z0-9_-]{86}$/.test(value.sig) && toBase64Url(fromBase64Url(value.sig)) === value.sig;
|
|
7484
|
+
}
|
|
7485
|
+
/** Reads a signature statement from its bytes, which must be canonical. */
|
|
7486
|
+
function readAppSignature(bytes) {
|
|
7487
|
+
if (bytes.length > APP_STATEMENT_LIMITS.signatureBytes) return {
|
|
7488
|
+
ok: false,
|
|
7489
|
+
reason: "bad-signature-statement",
|
|
7490
|
+
detail: "Too large"
|
|
7491
|
+
};
|
|
7492
|
+
const read = readCanonicalJson(bytes);
|
|
7493
|
+
if (!read.ok) return {
|
|
7494
|
+
ok: false,
|
|
7495
|
+
reason: "bad-signature-statement",
|
|
7496
|
+
detail: read.reason
|
|
7497
|
+
};
|
|
7498
|
+
if (!isAppSignature(read.value)) return {
|
|
7499
|
+
ok: false,
|
|
7500
|
+
reason: "bad-signature-statement",
|
|
7501
|
+
detail: "Not {alg, key, sig}"
|
|
7502
|
+
};
|
|
7503
|
+
return {
|
|
7504
|
+
ok: true,
|
|
7505
|
+
signature: read.value
|
|
7506
|
+
};
|
|
7507
|
+
}
|
|
7508
|
+
/**
|
|
7509
|
+
* Verifies a signature over an object's canonical bytes under a prefix, made by `key`: `signature-key` when the
|
|
7510
|
+
* statement names another key, `bad-signature` when it does not verify.
|
|
7511
|
+
*/
|
|
7512
|
+
function verifyAppSignature(prefix, canonical, signature, key) {
|
|
7513
|
+
if (!isAppSignature(signature)) return {
|
|
7514
|
+
ok: false,
|
|
7515
|
+
reason: "bad-signature-statement"
|
|
7516
|
+
};
|
|
7517
|
+
if (signature.key !== key) return {
|
|
7518
|
+
ok: false,
|
|
7519
|
+
reason: "signature-key"
|
|
7520
|
+
};
|
|
7521
|
+
return strictVerify(fromBase64Url(signature.sig), appSignedBytes(prefix, canonical), fromZ32(key)) ? { ok: true } : {
|
|
7522
|
+
ok: false,
|
|
7523
|
+
reason: "bad-signature"
|
|
7524
|
+
};
|
|
7525
|
+
}
|
|
7526
|
+
var isSequence = (v) => typeof v === "number" && Number.isSafeInteger(v) && v >= 1;
|
|
7527
|
+
var isPlainLine = (v, max) => typeof v === "string" && [...v].length <= max && !/\p{Cc}/u.test(v);
|
|
7528
|
+
/** Checks a revocation statement's shape; the reason it is refused, or null. */
|
|
7529
|
+
function checkAppRevokeStatement(value) {
|
|
7530
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return "Not an object";
|
|
7531
|
+
const v = value;
|
|
7532
|
+
const byDigests = "digests" in v;
|
|
7533
|
+
if (!exactKeys$1(v, [
|
|
7534
|
+
"ghostlyRevoke",
|
|
7535
|
+
"app",
|
|
7536
|
+
byDigests ? "digests" : "upTo"
|
|
7537
|
+
], ["reason"])) return "Keys are not ghostlyRevoke, app, and exactly one of digests and upTo (reason optional)";
|
|
7538
|
+
if (v.ghostlyRevoke !== 1) return "ghostlyRevoke is not 1";
|
|
7539
|
+
if (!isAppRef(v.app)) return "app is not a reference";
|
|
7540
|
+
if (byDigests) {
|
|
7541
|
+
const d = v.digests;
|
|
7542
|
+
if (!Array.isArray(d) || d.length < 1 || d.length > APP_STATEMENT_LIMITS.revokeDigests) return "digests is not 1 to 64 digests";
|
|
7543
|
+
if (!d.every(isAppHash) || new Set(d).size !== d.length) return "digests holds a bad or repeated digest";
|
|
7544
|
+
} else if (!isSequence(v.upTo)) return "upTo is not a sequence";
|
|
7545
|
+
if (v.reason !== void 0 && !isPlainLine(v.reason, APP_STATEMENT_LIMITS.reason)) return "reason is not one line of 200 characters";
|
|
7546
|
+
return null;
|
|
7547
|
+
}
|
|
7548
|
+
/** Signs a revocation with the publisher's key. */
|
|
7549
|
+
async function signAppRevocation(statement, signer) {
|
|
7550
|
+
const problem = checkAppRevokeStatement(statement);
|
|
7551
|
+
if (problem) throw new Error(`Not a revocation: ${problem}`);
|
|
7552
|
+
if (toZ32(signer.publicKey) !== appRefPublisher(statement.app)) throw new Error("A revocation is signed by its app's publisher");
|
|
7553
|
+
const { signature } = await signAppObject(APP_PREFIXES.revoke, statement, signer);
|
|
7554
|
+
return {
|
|
7555
|
+
statement,
|
|
7556
|
+
signature
|
|
7557
|
+
};
|
|
7558
|
+
}
|
|
7559
|
+
/** What a checked revocation is kept by: the SHA-256 of its canonical bytes (statement and signature), base64url. */
|
|
7560
|
+
function appRevocationId(revocation) {
|
|
7561
|
+
return toBase64Url(sha256(canonicalJsonBytes(revocation)));
|
|
7562
|
+
}
|
|
7563
|
+
/**
|
|
7564
|
+
* Reads one signed revocation (a parsed value, from `ghostly-revoke.json` or a store index's `revoked`): its shape, then
|
|
7565
|
+
* the signature under `ghostly-revoke/1` by the publisher its `app` names. One that `checked` holds verified before, so
|
|
7566
|
+
* its signature is not checked again; one that verifies now is added to it.
|
|
7567
|
+
*/
|
|
7568
|
+
function readAppRevocation(value, checked) {
|
|
7569
|
+
const read = readAppRevocationShape(value);
|
|
7570
|
+
if (!read.ok) return read;
|
|
7571
|
+
const verified = verifyAppRevocation(read.revocation, checked);
|
|
7572
|
+
return verified.ok ? read : verified;
|
|
7573
|
+
}
|
|
7574
|
+
/** The first half of `readAppRevocation`: the shape of a signed revocation, with no signature checked. */
|
|
7575
|
+
function readAppRevocationShape(value) {
|
|
7576
|
+
if (!exactKeys$1(value, ["statement", "signature"])) return {
|
|
7577
|
+
ok: false,
|
|
7578
|
+
reason: "bad-revocation",
|
|
7579
|
+
detail: "Keys are not statement and signature"
|
|
7580
|
+
};
|
|
7581
|
+
const problem = checkAppRevokeStatement(value.statement);
|
|
7582
|
+
if (problem) return {
|
|
7583
|
+
ok: false,
|
|
7584
|
+
reason: "bad-revocation",
|
|
7585
|
+
detail: problem
|
|
7586
|
+
};
|
|
7587
|
+
if (!isAppSignature(value.signature)) return {
|
|
7588
|
+
ok: false,
|
|
7589
|
+
reason: "bad-signature-statement"
|
|
7590
|
+
};
|
|
7591
|
+
return {
|
|
7592
|
+
ok: true,
|
|
7593
|
+
revocation: {
|
|
7594
|
+
statement: value.statement,
|
|
7595
|
+
signature: value.signature
|
|
7596
|
+
}
|
|
7597
|
+
};
|
|
7598
|
+
}
|
|
7599
|
+
/** The second half: the signature of a revocation of the right shape, unless `checked` holds it. */
|
|
7600
|
+
function verifyAppRevocation(revocation, checked) {
|
|
7601
|
+
const id = checked && appRevocationId(revocation);
|
|
7602
|
+
if (id !== void 0 && checked.has(id)) return { ok: true };
|
|
7603
|
+
const verified = verifyAppSignature(APP_PREFIXES.revoke, canonicalJsonBytes(revocation.statement), revocation.signature, appRefPublisher(revocation.statement.app));
|
|
7604
|
+
if (verified.ok && id !== void 0) checked.add(id);
|
|
7605
|
+
return verified;
|
|
7606
|
+
}
|
|
7607
|
+
/**
|
|
7608
|
+
* Reads `ghostly-revoke.json`: canonical bytes holding a list of signed revocations. One that does not verify refuses the
|
|
7609
|
+
* file, as a store index's `revoked` does.
|
|
7610
|
+
*/
|
|
7611
|
+
function readAppRevocations(bytes) {
|
|
7612
|
+
const read = readCanonicalJson(bytes);
|
|
7613
|
+
if (!read.ok) return read;
|
|
7614
|
+
if (!Array.isArray(read.value) || read.value.length > APP_STATEMENT_LIMITS.revocations) return {
|
|
7615
|
+
ok: false,
|
|
7616
|
+
reason: "bad-revocation",
|
|
7617
|
+
detail: "Not a list of at most 4096 revocations"
|
|
7618
|
+
};
|
|
7619
|
+
const revocations = [];
|
|
7620
|
+
for (const entry of read.value) {
|
|
7621
|
+
const one = readAppRevocation(entry);
|
|
7622
|
+
if (!one.ok) return one;
|
|
7623
|
+
revocations.push(one.revocation);
|
|
7624
|
+
}
|
|
7625
|
+
return {
|
|
7626
|
+
ok: true,
|
|
7627
|
+
revocations
|
|
7628
|
+
};
|
|
7629
|
+
}
|
|
7630
|
+
//#endregion
|
|
7631
|
+
//#region ../core/src/messageTime.ts
|
|
7632
|
+
/**
|
|
7633
|
+
* 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
|
|
7634
|
+
* times): clocks drift, but a time past this is the peer's choice, not its clock.
|
|
7635
|
+
*/
|
|
7636
|
+
var MESSAGE_CLOCK_SKEW_MS = 3e5;
|
|
7637
|
+
/**
|
|
7638
|
+
* The time a line of a group's history is kept under (a membership change, a rename): what its commit says, but at
|
|
7639
|
+
* most `MESSAGE_CLOCK_SKEW_MS` past this clock, and now when it is not a positive number. Messages do not use it:
|
|
7640
|
+
* they are placed where they arrive (below).
|
|
7641
|
+
*/
|
|
7642
|
+
function receivedTimestamp(ts, now = Date.now()) {
|
|
7643
|
+
if (!Number.isFinite(ts) || ts <= 0) return now;
|
|
7644
|
+
return Math.min(ts, now + MESSAGE_CLOCK_SKEW_MS);
|
|
7645
|
+
}
|
|
7646
|
+
/** The latest moment a date can hold. */
|
|
7647
|
+
var MAX_DATE_MS = 864e13;
|
|
7648
|
+
/** The time a sender says, as it is kept: a positive time a date can hold, or nothing. */
|
|
7649
|
+
function claimedTime(ts) {
|
|
7650
|
+
return typeof ts === "number" && Number.isFinite(ts) && ts > 0 && ts <= MAX_DATE_MS ? ts : void 0;
|
|
7651
|
+
}
|
|
7652
|
+
/**
|
|
7653
|
+
* The place of a row stored now: this clock, and past the last place given in its chat, so rows that come in the
|
|
7654
|
+
* same millisecond (a hold pickup, a group's catch-up) keep the order they came in.
|
|
7655
|
+
*/
|
|
7656
|
+
function arrivalKey(now, last = 0) {
|
|
7657
|
+
return Math.max(now, last + 1);
|
|
7658
|
+
}
|
|
7659
|
+
/** The time a message shows: what its sender said, never later than when it came. A row of mine shows its own time. */
|
|
7660
|
+
function shownTime(message) {
|
|
7661
|
+
return message.sentAt === void 0 ? message.timestamp : Math.min(message.sentAt, message.timestamp);
|
|
7662
|
+
}
|
|
7663
|
+
/**
|
|
7664
|
+
* When something a peer dated happened (an edit), for this device: what the peer says, now at the latest, and now
|
|
7665
|
+
* when it says nothing a date can hold.
|
|
7666
|
+
*/
|
|
7667
|
+
function heardTime(ts, now = Date.now()) {
|
|
7668
|
+
return Math.min(claimedTime(ts) ?? now, now);
|
|
7669
|
+
}
|
|
7670
|
+
//#endregion
|
|
7671
|
+
//#region ../core/src/statusCards.ts
|
|
7672
|
+
var STATUS_CARD_LIMITS = {
|
|
7673
|
+
/** A card's JSON, as sent (UTF-8 bytes): with the fallback text it stays well inside every message bound. */
|
|
7674
|
+
bytes: 8192,
|
|
7675
|
+
/** The sender's id for the card: stable across its updates. */
|
|
7676
|
+
id: 64,
|
|
7677
|
+
title: 120,
|
|
7678
|
+
/** The current step, a list item, a run's summary. */
|
|
7679
|
+
line: 200,
|
|
7680
|
+
branch: 120,
|
|
7681
|
+
schedule: 80,
|
|
7682
|
+
cron: 64,
|
|
7683
|
+
linkLabel: 40,
|
|
7684
|
+
url: 512,
|
|
7685
|
+
items: 20,
|
|
7686
|
+
links: 4,
|
|
7687
|
+
runs: 10,
|
|
7688
|
+
/** Any count on a card (a PR's number, lines, files; steps). */
|
|
7689
|
+
count: 1e9,
|
|
7690
|
+
/** How far ahead a routine's next run may be. */
|
|
7691
|
+
nextRunMs: 316224e5,
|
|
7692
|
+
/** Edits a message with a card takes (a text takes `MAX_EDITS_PER_MESSAGE`): a bot updates a long task often. */
|
|
7693
|
+
edits: 5e3,
|
|
7694
|
+
/** Labels on a task, and a label's length in characters. */
|
|
7695
|
+
tags: 3,
|
|
7696
|
+
tag: 24,
|
|
7697
|
+
/** Buttons on a message (WISP 406 · Message Buttons). */
|
|
7698
|
+
buttons: 6,
|
|
7699
|
+
/** A button's id: what a press names. */
|
|
7700
|
+
buttonId: 32,
|
|
7701
|
+
/** A button's label, in characters. */
|
|
7702
|
+
buttonLabel: 40,
|
|
7703
|
+
/** An app card's title and version, in characters (WISP 405 § An app): the manifest's bounds. */
|
|
7704
|
+
appTitle: 40,
|
|
7705
|
+
appVersion: 32,
|
|
7706
|
+
/** A usage card's label ("Claude") and account, in characters (WISP 405 § Usage). */
|
|
7707
|
+
usageLabel: 24,
|
|
7708
|
+
/** A usage window's name ("5 h", "week"), in characters. */
|
|
7709
|
+
usageWindow: 16,
|
|
7710
|
+
/** Windows beside a usage card's own, shown in its details. */
|
|
7711
|
+
usageWindows: 3
|
|
7712
|
+
};
|
|
7713
|
+
/** How a button looks: `primary` is the one highlighted, `danger` warns; `neutral` when none is said. */
|
|
7714
|
+
var BUTTON_STYLES = [
|
|
7715
|
+
"primary",
|
|
7716
|
+
"neutral",
|
|
7717
|
+
"danger"
|
|
7718
|
+
];
|
|
7719
|
+
var TASK_STATUSES = [
|
|
7720
|
+
"queued",
|
|
7721
|
+
"running",
|
|
7722
|
+
"blocked",
|
|
7723
|
+
"done",
|
|
7724
|
+
"failed",
|
|
7725
|
+
"cancelled"
|
|
7726
|
+
];
|
|
7727
|
+
/** A task that is still going: shown first, and counted on the chat's Tasks button. */
|
|
7728
|
+
var ACTIVE_TASK_STATUSES = [
|
|
7729
|
+
"queued",
|
|
7730
|
+
"running",
|
|
7731
|
+
"blocked"
|
|
7732
|
+
];
|
|
7733
|
+
var ITEM_STATES = [
|
|
7734
|
+
"pending",
|
|
7735
|
+
"running",
|
|
7736
|
+
"done",
|
|
7737
|
+
"failed",
|
|
7738
|
+
"skipped"
|
|
7739
|
+
];
|
|
7740
|
+
var ROUTINE_STATES = ["active", "paused"];
|
|
7741
|
+
var RUN_RESULTS = [
|
|
7742
|
+
"ok",
|
|
7743
|
+
"failed",
|
|
7744
|
+
"skipped"
|
|
7745
|
+
];
|
|
7746
|
+
/**
|
|
7747
|
+
* Where a task's pull request stands: `draft` (not ready for review), `open` (ready for review), `merged`, `closed`
|
|
7748
|
+
* (closed without a merge). A reader that does not know the field shows the pull request as before.
|
|
7749
|
+
*/
|
|
7750
|
+
var PR_STATES = [
|
|
7751
|
+
"draft",
|
|
7752
|
+
"open",
|
|
7753
|
+
"merged",
|
|
7754
|
+
"closed"
|
|
7755
|
+
];
|
|
7756
|
+
/** How a pull request's checks stand. */
|
|
7757
|
+
var PR_CHECKS = [
|
|
7758
|
+
"passing",
|
|
7759
|
+
"failing",
|
|
7760
|
+
"pending"
|
|
7761
|
+
];
|
|
7762
|
+
var ID$4 = /^[A-Za-z0-9_.:][A-Za-z0-9_.:-]{0,63}$/;
|
|
7763
|
+
/** A button's id: 1 to 32 of `A-Z a-z 0-9 _ . : -`, not starting with `-`. */
|
|
7764
|
+
var BUTTON_ID = /^[A-Za-z0-9_.:][A-Za-z0-9_.:-]{0,31}$/;
|
|
7765
|
+
var CRON = /^[0-9A-Za-z*,/?#\- ]{1,64}$/;
|
|
7766
|
+
/**
|
|
7767
|
+
* Whether a typed answer is a button's label or id (WISP 406 · Message Buttons): ignoring case and spaces at the ends.
|
|
7768
|
+
* Not the device's locale's case: the presser's and the author's apps must agree ("I" is not "ı" anywhere).
|
|
7769
|
+
*/
|
|
7770
|
+
var sameButtonText = (a, b) => a.trim().toLowerCase() === b.trim().toLowerCase();
|
|
7771
|
+
/**
|
|
7772
|
+
* Two buttons a typed answer could not tell apart, or none: a label that reads as an earlier label (`label`), or a
|
|
7773
|
+
* label that reads as another button's id (`id`), both as `sameButtonText` compares them. The labels as a reader keeps
|
|
7774
|
+
* them (one line, cleaned). `at` is the button that clashes, `with` the one it clashes with.
|
|
7775
|
+
*/
|
|
7776
|
+
function buttonLabelClash(buttons) {
|
|
7777
|
+
const labels = buttons.map((button) => isObject$4(button) ? cardLine(button.label, Infinity) : void 0);
|
|
7778
|
+
const ids = buttons.map((button) => isObject$4(button) && typeof button.id === "string" ? button.id : void 0);
|
|
7779
|
+
for (const [at, label] of labels.entries()) {
|
|
7780
|
+
if (!label) continue;
|
|
7781
|
+
const twin = labels.findIndex((other, i) => i < at && !!other && sameButtonText(other, label));
|
|
7782
|
+
if (twin !== -1) return {
|
|
7783
|
+
at,
|
|
7784
|
+
with: twin,
|
|
7785
|
+
kind: "label"
|
|
7786
|
+
};
|
|
7787
|
+
const named = ids.findIndex((id, i) => i !== at && id !== void 0 && sameButtonText(id, label));
|
|
7788
|
+
if (named !== -1) return {
|
|
7789
|
+
at,
|
|
7790
|
+
with: named,
|
|
7791
|
+
kind: "id"
|
|
7792
|
+
};
|
|
7793
|
+
}
|
|
7794
|
+
}
|
|
7795
|
+
/** An app card's id, made from its reference: the app's name, a dot, the first 16 characters of the publisher key. */
|
|
7796
|
+
function appCardId(ref) {
|
|
7797
|
+
return `${ref.slice(53)}.${ref.slice(0, 16)}`;
|
|
7798
|
+
}
|
|
7799
|
+
/** An app card's `sequence`: a whole number from 0 to 2^53 - 1. */
|
|
7800
|
+
var appSequence = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
7801
|
+
var isObject$4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7802
|
+
/** One line of display text: runs of space (line breaks included) become one space, then the display rules for names. */
|
|
7803
|
+
function cardLine(value, max) {
|
|
7804
|
+
if (typeof value !== "string") return void 0;
|
|
7805
|
+
return sanitizeDisplayText(value.replace(/\s+/gu, " "), max);
|
|
7806
|
+
}
|
|
7807
|
+
/**
|
|
7808
|
+
* An https link a card may show: no credentials, a host, no spaces or control characters, at most
|
|
7809
|
+
* `STATUS_CARD_LIMITS.url` characters. Returned as the parser writes it (a non-ASCII host in punycode).
|
|
7810
|
+
*/
|
|
7811
|
+
function cardUrl(value) {
|
|
7812
|
+
if (typeof value !== "string" || !value || value.length > STATUS_CARD_LIMITS.url || /[\s\p{Cc}\p{Cf}]/u.test(value)) return void 0;
|
|
7813
|
+
let url;
|
|
7814
|
+
try {
|
|
7815
|
+
url = new URL(value);
|
|
7816
|
+
} catch {
|
|
7817
|
+
return;
|
|
7818
|
+
}
|
|
7819
|
+
if (url.protocol !== "https:" || !url.hostname || url.username || url.password) return void 0;
|
|
7820
|
+
return url.href.length <= STATUS_CARD_LIMITS.url ? url.href : void 0;
|
|
7821
|
+
}
|
|
7822
|
+
var count = (value, min = 0) => typeof value === "number" && Number.isSafeInteger(value) && value >= min && value <= STATUS_CARD_LIMITS.count ? value : void 0;
|
|
7823
|
+
/** A time on a card as a reader keeps it: a positive whole number, never later than `latest`. */
|
|
7824
|
+
var time = (value, latest) => typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? Math.min(value, latest) : void 0;
|
|
7825
|
+
var oneOf = (values, value) => typeof value === "string" && values.includes(value) ? value : void 0;
|
|
7826
|
+
function readLinks(raw) {
|
|
7827
|
+
if (!Array.isArray(raw)) return [];
|
|
7828
|
+
const links = [];
|
|
7829
|
+
for (const entry of raw.slice(0, STATUS_CARD_LIMITS.links)) {
|
|
7830
|
+
if (!isObject$4(entry)) continue;
|
|
7831
|
+
const url = cardUrl(entry.url);
|
|
7832
|
+
if (!url) continue;
|
|
7833
|
+
const label = cardLine(entry.label, STATUS_CARD_LIMITS.linkLabel);
|
|
7834
|
+
links.push({
|
|
7835
|
+
url,
|
|
7836
|
+
...label && { label }
|
|
7837
|
+
});
|
|
7838
|
+
}
|
|
7839
|
+
return links;
|
|
7840
|
+
}
|
|
7841
|
+
/** A task's labels as a reader keeps them: the first `STATUS_CARD_LIMITS.tags` that hold, each one clean line, none twice. */
|
|
7842
|
+
function readTags(raw) {
|
|
7843
|
+
if (!Array.isArray(raw)) return [];
|
|
7844
|
+
const tags = [];
|
|
7845
|
+
for (const entry of raw) {
|
|
7846
|
+
const tag = cardLine(entry, STATUS_CARD_LIMITS.tag);
|
|
7847
|
+
if (tag && !tags.includes(tag)) tags.push(tag);
|
|
7848
|
+
if (tags.length === STATUS_CARD_LIMITS.tags) break;
|
|
7849
|
+
}
|
|
7850
|
+
return tags;
|
|
7851
|
+
}
|
|
7852
|
+
function readRun(raw, now) {
|
|
7853
|
+
if (!isObject$4(raw)) return void 0;
|
|
7854
|
+
const at = time(raw.at, now + MESSAGE_CLOCK_SKEW_MS), result = oneOf(RUN_RESULTS, raw.result);
|
|
7855
|
+
if (!at || !result) return void 0;
|
|
7856
|
+
const summary = cardLine(raw.summary, STATUS_CARD_LIMITS.line);
|
|
7857
|
+
return {
|
|
7858
|
+
at,
|
|
7859
|
+
result,
|
|
7860
|
+
...summary && { summary }
|
|
7861
|
+
};
|
|
7862
|
+
}
|
|
7863
|
+
/** A percent as a reader keeps it: a finite number, rounded and clamped into 0 to 100. */
|
|
7864
|
+
var percent = (value) => typeof value === "number" && Number.isFinite(value) ? Math.round(Math.min(100, Math.max(0, value))) : void 0;
|
|
7865
|
+
/** A time ahead of the reader (a reset, a next run) as it keeps it: whole, positive, at most a year ahead. */
|
|
7866
|
+
var ahead = (value, now) => typeof value === "number" && Number.isSafeInteger(value) && value > 0 && value <= now + STATUS_CARD_LIMITS.nextRunMs ? value : void 0;
|
|
7867
|
+
/** A usage card as a reader keeps it (WISP 405 § Usage): dropped without a percent left or a used of limit. */
|
|
7868
|
+
function readUsage(raw, id, now) {
|
|
7869
|
+
const left = percent(raw.left);
|
|
7870
|
+
let used = count(raw.used), limit = count(raw.limit, 1);
|
|
7871
|
+
if (used === void 0 || limit === void 0 || used > limit) used = limit = void 0;
|
|
7872
|
+
if (left === void 0 && limit === void 0) return void 0;
|
|
7873
|
+
const label = cardLine(raw.label, STATUS_CARD_LIMITS.usageLabel), account = cardLine(raw.account, STATUS_CARD_LIMITS.usageLabel);
|
|
7874
|
+
const window = cardLine(raw.window, STATUS_CARD_LIMITS.usageWindow);
|
|
7875
|
+
const resetsAt = ahead(raw.resetsAt, now), updatedAt = time(raw.updatedAt, now + MESSAGE_CLOCK_SKEW_MS);
|
|
7876
|
+
const windows = [];
|
|
7877
|
+
if (Array.isArray(raw.windows)) for (const entry of raw.windows.slice(0, STATUS_CARD_LIMITS.usageWindows)) {
|
|
7878
|
+
if (!isObject$4(entry)) continue;
|
|
7879
|
+
const name = cardLine(entry.window, STATUS_CARD_LIMITS.usageWindow), share = percent(entry.left);
|
|
7880
|
+
if (!name || share === void 0) continue;
|
|
7881
|
+
const at = ahead(entry.resetsAt, now);
|
|
7882
|
+
windows.push({
|
|
7883
|
+
window: name,
|
|
7884
|
+
left: share,
|
|
7885
|
+
...at && { resetsAt: at }
|
|
7886
|
+
});
|
|
7887
|
+
}
|
|
7888
|
+
return {
|
|
7889
|
+
kind: "usage",
|
|
7890
|
+
id,
|
|
7891
|
+
...left !== void 0 && { left },
|
|
7892
|
+
...used !== void 0 && limit !== void 0 && {
|
|
7893
|
+
used,
|
|
7894
|
+
limit
|
|
7895
|
+
},
|
|
7896
|
+
...label && { label },
|
|
7897
|
+
...account && { account },
|
|
7898
|
+
...window && { window },
|
|
7899
|
+
...resetsAt && { resetsAt },
|
|
7900
|
+
...updatedAt && { updatedAt },
|
|
7901
|
+
...windows.length && { windows }
|
|
7902
|
+
};
|
|
7903
|
+
}
|
|
7904
|
+
/** How much of a usage card's quota is left, 0 to 100: its own percent, else worked out from used of limit. */
|
|
7905
|
+
function usageLeft(card) {
|
|
7906
|
+
if (card.left !== void 0) return card.left;
|
|
7907
|
+
return card.used !== void 0 && card.limit ? Math.round((card.limit - card.used) / card.limit * 100) : 0;
|
|
7908
|
+
}
|
|
7909
|
+
/** The bytes a card takes on the wire: what `STATUS_CARD_LIMITS.bytes` bounds. */
|
|
7910
|
+
function statusCardBytes(card) {
|
|
7911
|
+
try {
|
|
7912
|
+
return utf8Encode(JSON.stringify(card) ?? "").length;
|
|
7913
|
+
} catch {
|
|
7914
|
+
return Infinity;
|
|
7915
|
+
}
|
|
7916
|
+
}
|
|
7917
|
+
/**
|
|
7918
|
+
* A card as a reader takes it from the wire (`sc`), or undefined when it cannot stand: not an object, past
|
|
7919
|
+
* `STATUS_CARD_LIMITS.bytes`, a kind this app does not know, no valid id, title or name, status or state. Fields that do
|
|
7920
|
+
* not hold are left out and lists are cut to their bound; times are kept no later than the reader's clock allows (a
|
|
7921
|
+
* routine's next run up to a year ahead). The message is always kept: without a card it shows its text.
|
|
6803
7922
|
*/
|
|
6804
|
-
|
|
7923
|
+
function readStatusCard(raw, now = Date.now()) {
|
|
7924
|
+
if (!isObject$4(raw) || statusCardBytes(raw) > STATUS_CARD_LIMITS.bytes) return void 0;
|
|
7925
|
+
const id = typeof raw.id === "string" && ID$4.test(raw.id) ? raw.id : void 0;
|
|
7926
|
+
if (!id) return void 0;
|
|
7927
|
+
if (raw.kind === "app") {
|
|
7928
|
+
if (!isAppRef(raw.ref) || id !== appCardId(raw.ref) || !isAppHash(raw.digest) || !appSequence(raw.sequence)) return void 0;
|
|
7929
|
+
const title = cardLine(raw.title, STATUS_CARD_LIMITS.appTitle);
|
|
7930
|
+
if (!title) return void 0;
|
|
7931
|
+
const version = cardLine(raw.version, STATUS_CARD_LIMITS.appVersion), url = cardUrl(raw.url);
|
|
7932
|
+
return {
|
|
7933
|
+
kind: "app",
|
|
7934
|
+
id,
|
|
7935
|
+
ref: raw.ref,
|
|
7936
|
+
digest: raw.digest,
|
|
7937
|
+
sequence: raw.sequence,
|
|
7938
|
+
title,
|
|
7939
|
+
...version && { version },
|
|
7940
|
+
...url && { url },
|
|
7941
|
+
...raw.opened === true && { opened: true }
|
|
7942
|
+
};
|
|
7943
|
+
}
|
|
7944
|
+
const latest = now + MESSAGE_CLOCK_SKEW_MS;
|
|
7945
|
+
if (raw.kind === "usage") return readUsage(raw, id, now);
|
|
7946
|
+
const links = readLinks(raw.links);
|
|
7947
|
+
if (raw.kind === "task") {
|
|
7948
|
+
const title = cardLine(raw.title, STATUS_CARD_LIMITS.title), status = oneOf(TASK_STATUSES, raw.status);
|
|
7949
|
+
if (!title || !status) return void 0;
|
|
7950
|
+
const progress = typeof raw.progress === "number" && Number.isFinite(raw.progress) ? Math.round(Math.min(100, Math.max(0, raw.progress))) : void 0;
|
|
7951
|
+
let done = count(raw.done), total = count(raw.total, 1);
|
|
7952
|
+
if (done === void 0 || total === void 0 || done > total) done = total = void 0;
|
|
7953
|
+
const step = cardLine(raw.step, STATUS_CARD_LIMITS.line), branch = cardLine(raw.branch, STATUS_CARD_LIMITS.branch);
|
|
7954
|
+
let pr;
|
|
7955
|
+
if (isObject$4(raw.pr)) {
|
|
7956
|
+
const url = cardUrl(raw.pr.url);
|
|
7957
|
+
if (url) {
|
|
7958
|
+
const number = count(raw.pr.number, 1), additions = count(raw.pr.additions), deletions = count(raw.pr.deletions), files = count(raw.pr.files);
|
|
7959
|
+
const state = oneOf(PR_STATES, raw.pr.state), checks = oneOf(PR_CHECKS, raw.pr.checks);
|
|
7960
|
+
pr = {
|
|
7961
|
+
url,
|
|
7962
|
+
...number !== void 0 && { number },
|
|
7963
|
+
...additions !== void 0 && { additions },
|
|
7964
|
+
...deletions !== void 0 && { deletions },
|
|
7965
|
+
...files !== void 0 && { files },
|
|
7966
|
+
...state && { state },
|
|
7967
|
+
...checks && { checks }
|
|
7968
|
+
};
|
|
7969
|
+
}
|
|
7970
|
+
}
|
|
7971
|
+
const items = [];
|
|
7972
|
+
if (Array.isArray(raw.items)) for (const entry of raw.items.slice(0, STATUS_CARD_LIMITS.items)) {
|
|
7973
|
+
if (!isObject$4(entry)) continue;
|
|
7974
|
+
const text = cardLine(entry.text, STATUS_CARD_LIMITS.line);
|
|
7975
|
+
if (text) items.push({
|
|
7976
|
+
text,
|
|
7977
|
+
state: oneOf(ITEM_STATES, entry.state) ?? "pending"
|
|
7978
|
+
});
|
|
7979
|
+
}
|
|
7980
|
+
const startedAt = time(raw.startedAt, latest), updatedAt = time(raw.updatedAt, latest);
|
|
7981
|
+
const tags = readTags(raw.tags);
|
|
7982
|
+
const parent = typeof raw.parent === "string" && ID$4.test(raw.parent) && raw.parent !== id ? raw.parent : void 0;
|
|
7983
|
+
return {
|
|
7984
|
+
kind: "task",
|
|
7985
|
+
id,
|
|
7986
|
+
title,
|
|
7987
|
+
status,
|
|
7988
|
+
...progress !== void 0 && { progress },
|
|
7989
|
+
...done !== void 0 && total !== void 0 && {
|
|
7990
|
+
done,
|
|
7991
|
+
total
|
|
7992
|
+
},
|
|
7993
|
+
...step && { step },
|
|
7994
|
+
...pr && { pr },
|
|
7995
|
+
...branch && { branch },
|
|
7996
|
+
...startedAt && { startedAt },
|
|
7997
|
+
...updatedAt && { updatedAt },
|
|
7998
|
+
...items.length && { items },
|
|
7999
|
+
...links.length && { links },
|
|
8000
|
+
...tags.length && { tags },
|
|
8001
|
+
...parent && { parent }
|
|
8002
|
+
};
|
|
8003
|
+
}
|
|
8004
|
+
if (raw.kind === "routine") {
|
|
8005
|
+
const name = cardLine(raw.name, STATUS_CARD_LIMITS.title), schedule = cardLine(raw.schedule, STATUS_CARD_LIMITS.schedule), state = oneOf(ROUTINE_STATES, raw.state);
|
|
8006
|
+
if (!name || !schedule || !state) return void 0;
|
|
8007
|
+
const cron = typeof raw.cron === "string" && CRON.test(raw.cron.trim()) ? raw.cron.trim() : void 0;
|
|
8008
|
+
const lastRun = readRun(raw.lastRun, now);
|
|
8009
|
+
const nextRunAt = typeof raw.nextRunAt === "number" && Number.isSafeInteger(raw.nextRunAt) && raw.nextRunAt > 0 && raw.nextRunAt <= now + STATUS_CARD_LIMITS.nextRunMs ? raw.nextRunAt : void 0;
|
|
8010
|
+
const runs = [];
|
|
8011
|
+
if (Array.isArray(raw.runs)) for (const entry of raw.runs.slice(0, STATUS_CARD_LIMITS.runs)) {
|
|
8012
|
+
const run = readRun(entry, now);
|
|
8013
|
+
if (run) runs.push(run);
|
|
8014
|
+
}
|
|
8015
|
+
return {
|
|
8016
|
+
kind: "routine",
|
|
8017
|
+
id,
|
|
8018
|
+
name,
|
|
8019
|
+
schedule,
|
|
8020
|
+
state,
|
|
8021
|
+
...cron && { cron },
|
|
8022
|
+
...lastRun && { lastRun },
|
|
8023
|
+
...nextRunAt && { nextRunAt },
|
|
8024
|
+
...runs.length && { runs },
|
|
8025
|
+
...links.length && { links }
|
|
8026
|
+
};
|
|
8027
|
+
}
|
|
8028
|
+
if (raw.kind === "buttons") {
|
|
8029
|
+
if (!Array.isArray(raw.buttons)) return void 0;
|
|
8030
|
+
const buttons = [], seen = /* @__PURE__ */ new Set();
|
|
8031
|
+
for (const entry of raw.buttons.slice(0, STATUS_CARD_LIMITS.buttons)) {
|
|
8032
|
+
if (!isObject$4(entry) || typeof entry.id !== "string" || !BUTTON_ID.test(entry.id) || seen.has(entry.id)) continue;
|
|
8033
|
+
const label = cardLine(entry.label, STATUS_CARD_LIMITS.buttonLabel);
|
|
8034
|
+
if (!label) continue;
|
|
8035
|
+
seen.add(entry.id);
|
|
8036
|
+
const style = oneOf(BUTTON_STYLES, entry.style);
|
|
8037
|
+
buttons.push({
|
|
8038
|
+
id: entry.id,
|
|
8039
|
+
label,
|
|
8040
|
+
...style && style !== "neutral" && { style },
|
|
8041
|
+
...entry.once === true && { once: true }
|
|
8042
|
+
});
|
|
8043
|
+
}
|
|
8044
|
+
if (!buttons.length) return void 0;
|
|
8045
|
+
const chosen = typeof raw.chosen === "string" && seen.has(raw.chosen) ? raw.chosen : void 0;
|
|
8046
|
+
return {
|
|
8047
|
+
kind: "buttons",
|
|
8048
|
+
id,
|
|
8049
|
+
buttons,
|
|
8050
|
+
...chosen && { chosen },
|
|
8051
|
+
...raw.closed === true && { closed: true }
|
|
8052
|
+
};
|
|
8053
|
+
}
|
|
8054
|
+
}
|
|
6805
8055
|
/**
|
|
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.
|
|
8056
|
+
* 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
|
|
8057
|
+
* here instead (an unknown status, a link that is not https, a list past its bound, a line too long), so a bot learns
|
|
8058
|
+
* at once and nothing it said is lost on the way. Line breaks and invisible characters are cleaned, as a reader would.
|
|
6810
8059
|
*/
|
|
6811
|
-
|
|
6812
|
-
|
|
6813
|
-
|
|
8060
|
+
function checkStatusCard(raw, now = Date.now()) {
|
|
8061
|
+
if (!isObject$4(raw)) return { error: "A card is an object" };
|
|
8062
|
+
const bytes = statusCardBytes(raw);
|
|
8063
|
+
if (bytes > STATUS_CARD_LIMITS.bytes) return { error: `The card takes ${bytes} bytes; at most ${STATUS_CARD_LIMITS.bytes}` };
|
|
8064
|
+
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" };
|
|
8065
|
+
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 -` };
|
|
8066
|
+
const line = (field, value, max, required = false) => {
|
|
8067
|
+
if (value === void 0 && !required) return null;
|
|
8068
|
+
if (typeof value !== "string" || !cardLine(value, Infinity)) return `${field} is text`;
|
|
8069
|
+
if ([...cardLine(value, Infinity)].length > max) return `${field} is at most ${max} characters`;
|
|
8070
|
+
return null;
|
|
8071
|
+
};
|
|
8072
|
+
const url = (field, value) => cardUrl(value) ? null : `${field} must be an https link of at most ${STATUS_CARD_LIMITS.url} characters`;
|
|
8073
|
+
const whole = (field, value, min = 0) => value === void 0 || count(value, min) !== void 0 ? null : `${field} is a whole number from ${min}`;
|
|
8074
|
+
const errors = [];
|
|
8075
|
+
if (raw.links !== void 0) {
|
|
8076
|
+
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}`);
|
|
8077
|
+
else for (const [i, link] of raw.links.entries()) {
|
|
8078
|
+
if (!isObject$4(link)) {
|
|
8079
|
+
errors.push(`links[${i}] is an object`);
|
|
8080
|
+
continue;
|
|
8081
|
+
}
|
|
8082
|
+
errors.push(url(`links[${i}].url`, link.url), line(`links[${i}].label`, link.label, STATUS_CARD_LIMITS.linkLabel));
|
|
8083
|
+
}
|
|
8084
|
+
}
|
|
8085
|
+
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`;
|
|
8086
|
+
const reset = (field, value) => value === void 0 || ahead(value, now) !== void 0 ? null : `${field} is a time in milliseconds, within a year`;
|
|
8087
|
+
if (raw.kind === "usage") {
|
|
8088
|
+
if (raw.links !== void 0) errors.push("a usage card takes no links");
|
|
8089
|
+
if (raw.left === void 0 && raw.used === void 0) errors.push("left (a percent) or used and limit");
|
|
8090
|
+
errors.push(share("left", raw.left));
|
|
8091
|
+
if (raw.used === void 0 !== (raw.limit === void 0)) errors.push("used and limit go together");
|
|
8092
|
+
else if (raw.used !== void 0) {
|
|
8093
|
+
errors.push(whole("used", raw.used), whole("limit", raw.limit, 1));
|
|
8094
|
+
if (typeof raw.used === "number" && typeof raw.limit === "number" && raw.used > raw.limit) errors.push("used is at most limit");
|
|
8095
|
+
}
|
|
8096
|
+
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));
|
|
8097
|
+
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");
|
|
8098
|
+
if (raw.windows !== void 0) {
|
|
8099
|
+
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}`);
|
|
8100
|
+
else for (const [i, entry] of raw.windows.entries()) {
|
|
8101
|
+
if (!isObject$4(entry)) {
|
|
8102
|
+
errors.push(`windows[${i}] is an object`);
|
|
8103
|
+
continue;
|
|
8104
|
+
}
|
|
8105
|
+
errors.push(line(`windows[${i}].window`, entry.window, STATUS_CARD_LIMITS.usageWindow, true));
|
|
8106
|
+
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));
|
|
8107
|
+
}
|
|
8108
|
+
}
|
|
8109
|
+
} else if (raw.kind === "app") {
|
|
8110
|
+
if (raw.links !== void 0) errors.push("an app card takes no links");
|
|
8111
|
+
if (!isAppRef(raw.ref)) errors.push("ref is <publisher key in z-base32>/<name>");
|
|
8112
|
+
else if (raw.id !== appCardId(raw.ref)) errors.push(`id is ${appCardId(raw.ref)}, made from ref`);
|
|
8113
|
+
if (!isAppHash(raw.digest)) errors.push("digest is a SHA-256 in base64url without padding");
|
|
8114
|
+
if (!appSequence(raw.sequence)) errors.push("sequence is a whole number from 0 to 2^53 - 1");
|
|
8115
|
+
errors.push(line("title", raw.title, STATUS_CARD_LIMITS.appTitle, true), line("version", raw.version, STATUS_CARD_LIMITS.appVersion));
|
|
8116
|
+
if (raw.url !== void 0) errors.push(url("url", raw.url));
|
|
8117
|
+
if (raw.opened !== void 0 && raw.opened !== true) errors.push("opened is true or left out");
|
|
8118
|
+
} else if (raw.kind === "buttons") {
|
|
8119
|
+
if (raw.links !== void 0) errors.push("buttons take no links");
|
|
8120
|
+
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}`);
|
|
8121
|
+
else {
|
|
8122
|
+
const ids = /* @__PURE__ */ new Set();
|
|
8123
|
+
for (const [i, button] of raw.buttons.entries()) {
|
|
8124
|
+
if (!isObject$4(button)) {
|
|
8125
|
+
errors.push(`buttons[${i}] is an object`);
|
|
8126
|
+
continue;
|
|
8127
|
+
}
|
|
8128
|
+
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 -`);
|
|
8129
|
+
else if (ids.has(button.id)) errors.push(`buttons[${i}].id "${button.id}" is used twice`);
|
|
8130
|
+
else ids.add(button.id);
|
|
8131
|
+
errors.push(line(`buttons[${i}].label`, button.label, STATUS_CARD_LIMITS.buttonLabel, true));
|
|
8132
|
+
if (button.style !== void 0 && !oneOf(BUTTON_STYLES, button.style)) errors.push(`buttons[${i}].style is one of ${BUTTON_STYLES.join(", ")}`);
|
|
8133
|
+
if (button.once !== void 0 && button.once !== true) errors.push(`buttons[${i}].once is true or left out`);
|
|
8134
|
+
}
|
|
8135
|
+
const clash = buttonLabelClash(raw.buttons);
|
|
8136
|
+
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)`);
|
|
8137
|
+
if (raw.chosen !== void 0 && !(typeof raw.chosen === "string" && ids.has(raw.chosen))) errors.push("chosen names one of the buttons");
|
|
8138
|
+
}
|
|
8139
|
+
if (raw.closed !== void 0 && raw.closed !== true) errors.push("closed is true or left out");
|
|
8140
|
+
} else if (raw.kind === "task") {
|
|
8141
|
+
errors.push(line("title", raw.title, STATUS_CARD_LIMITS.title, true));
|
|
8142
|
+
if (!oneOf(TASK_STATUSES, raw.status)) errors.push(`status is one of ${TASK_STATUSES.join(", ")}`);
|
|
8143
|
+
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");
|
|
8144
|
+
if (raw.done === void 0 !== (raw.total === void 0)) errors.push("done and total go together");
|
|
8145
|
+
else if (raw.done !== void 0) {
|
|
8146
|
+
errors.push(whole("done", raw.done), whole("total", raw.total, 1));
|
|
8147
|
+
if (typeof raw.done === "number" && typeof raw.total === "number" && raw.done > raw.total) errors.push("done is at most total");
|
|
8148
|
+
}
|
|
8149
|
+
errors.push(line("step", raw.step, STATUS_CARD_LIMITS.line), line("branch", raw.branch, STATUS_CARD_LIMITS.branch));
|
|
8150
|
+
if (raw.pr !== void 0) {
|
|
8151
|
+
if (!isObject$4(raw.pr)) errors.push("pr is an object");
|
|
8152
|
+
else {
|
|
8153
|
+
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));
|
|
8154
|
+
if (raw.pr.state !== void 0 && !oneOf(PR_STATES, raw.pr.state)) errors.push(`pr.state is one of ${PR_STATES.join(", ")}`);
|
|
8155
|
+
if (raw.pr.checks !== void 0 && !oneOf(PR_CHECKS, raw.pr.checks)) errors.push(`pr.checks is one of ${PR_CHECKS.join(", ")}`);
|
|
8156
|
+
}
|
|
8157
|
+
}
|
|
8158
|
+
if (raw.tags !== void 0) {
|
|
8159
|
+
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}`);
|
|
8160
|
+
else {
|
|
8161
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8162
|
+
for (const [i, tag] of raw.tags.entries()) {
|
|
8163
|
+
const problem = line(`tags[${i}]`, tag, STATUS_CARD_LIMITS.tag, true);
|
|
8164
|
+
errors.push(problem);
|
|
8165
|
+
if (problem) continue;
|
|
8166
|
+
const clean = cardLine(tag, Infinity);
|
|
8167
|
+
if (seen.has(clean)) errors.push(`tags[${i}] ${JSON.stringify(clean)} is there twice`);
|
|
8168
|
+
seen.add(clean);
|
|
8169
|
+
}
|
|
8170
|
+
}
|
|
8171
|
+
}
|
|
8172
|
+
if (raw.parent !== void 0) {
|
|
8173
|
+
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 -`);
|
|
8174
|
+
else if (raw.parent === raw.id) errors.push("parent is another task's id, not its own");
|
|
8175
|
+
}
|
|
8176
|
+
for (const field of ["startedAt", "updatedAt"]) {
|
|
8177
|
+
const value = raw[field];
|
|
8178
|
+
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`);
|
|
8179
|
+
}
|
|
8180
|
+
if (raw.items !== void 0) {
|
|
8181
|
+
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}`);
|
|
8182
|
+
else for (const [i, item] of raw.items.entries()) {
|
|
8183
|
+
if (!isObject$4(item)) {
|
|
8184
|
+
errors.push(`items[${i}] is an object`);
|
|
8185
|
+
continue;
|
|
8186
|
+
}
|
|
8187
|
+
errors.push(line(`items[${i}].text`, item.text, STATUS_CARD_LIMITS.line, true));
|
|
8188
|
+
if (item.state !== void 0 && !oneOf(ITEM_STATES, item.state)) errors.push(`items[${i}].state is one of ${ITEM_STATES.join(", ")}`);
|
|
8189
|
+
}
|
|
8190
|
+
}
|
|
8191
|
+
} else {
|
|
8192
|
+
errors.push(line("name", raw.name, STATUS_CARD_LIMITS.title, true), line("schedule", raw.schedule, STATUS_CARD_LIMITS.schedule, true));
|
|
8193
|
+
if (!oneOf(ROUTINE_STATES, raw.state)) errors.push(`state is one of ${ROUTINE_STATES.join(", ")}`);
|
|
8194
|
+
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`);
|
|
8195
|
+
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");
|
|
8196
|
+
const run = (field, value) => {
|
|
8197
|
+
if (!isObject$4(value)) return errors.push(`${field} is an object`);
|
|
8198
|
+
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`);
|
|
8199
|
+
if (!oneOf(RUN_RESULTS, value.result)) errors.push(`${field}.result is one of ${RUN_RESULTS.join(", ")}`);
|
|
8200
|
+
errors.push(line(`${field}.summary`, value.summary, STATUS_CARD_LIMITS.line));
|
|
8201
|
+
};
|
|
8202
|
+
if (raw.lastRun !== void 0) run("lastRun", raw.lastRun);
|
|
8203
|
+
if (raw.runs !== void 0) {
|
|
8204
|
+
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}`);
|
|
8205
|
+
else raw.runs.forEach((entry, i) => run(`runs[${i}]`, entry));
|
|
8206
|
+
}
|
|
8207
|
+
}
|
|
8208
|
+
const error = errors.find((e) => typeof e === "string");
|
|
8209
|
+
if (error) return { error };
|
|
8210
|
+
const card = readStatusCard(raw, now);
|
|
8211
|
+
return card ? { card } : { error: "The card does not hold" };
|
|
8212
|
+
}
|
|
8213
|
+
/** The progress a task shows, 0 to 100: its own, else its steps done of total; undefined when it says neither. */
|
|
8214
|
+
function taskProgress(card) {
|
|
8215
|
+
if (card.progress !== void 0) return card.progress;
|
|
8216
|
+
return card.done !== void 0 && card.total ? Math.round(card.done / card.total * 100) : void 0;
|
|
8217
|
+
}
|
|
8218
|
+
var TASK_MARK = {
|
|
8219
|
+
queued: "⏳",
|
|
8220
|
+
running: "🔄",
|
|
8221
|
+
blocked: "⛔",
|
|
8222
|
+
done: "✅",
|
|
8223
|
+
failed: "❌",
|
|
8224
|
+
cancelled: "🚫"
|
|
6814
8225
|
};
|
|
6815
|
-
|
|
6816
|
-
|
|
6817
|
-
|
|
8226
|
+
var STATUS_WORD = {
|
|
8227
|
+
queued: "Queued",
|
|
8228
|
+
running: "Running",
|
|
8229
|
+
blocked: "Blocked",
|
|
8230
|
+
done: "Done",
|
|
8231
|
+
failed: "Failed",
|
|
8232
|
+
cancelled: "Cancelled"
|
|
8233
|
+
};
|
|
8234
|
+
var RESULT_WORD = {
|
|
8235
|
+
ok: "ok",
|
|
8236
|
+
failed: "failed",
|
|
8237
|
+
skipped: "skipped"
|
|
8238
|
+
};
|
|
8239
|
+
/** A time as the fallback text says it: in UTC, to the minute ("2026-09-30 01:00 UTC"). */
|
|
8240
|
+
function cardTimeUtc(ms) {
|
|
8241
|
+
return `${new Date(ms).toISOString().slice(0, 16).replace("T", " ")} UTC`;
|
|
8242
|
+
}
|
|
8243
|
+
/**
|
|
8244
|
+
* The text a card's message carries: what an app without cards shows, the DHT floor carries, the chat list previews
|
|
8245
|
+
* and search finds. Plain lines, the first naming the card, no bars or layout that would read badly elsewhere.
|
|
8246
|
+
*/
|
|
8247
|
+
function statusCardText(card) {
|
|
8248
|
+
if (card.kind === "app") {
|
|
8249
|
+
const named = `${card.title}${card.version ? ` ${card.version}` : ""}`;
|
|
8250
|
+
return [card.opened ? `🧩 Opened ${named} in this chat (Ghostly app)` : `🧩 ${named} (Ghostly app)`, ...card.url ? [card.url] : []].join("\n");
|
|
8251
|
+
}
|
|
8252
|
+
if (card.kind === "usage") {
|
|
8253
|
+
const left = `${usageLeft(card)}% left${card.window ? ` (${card.window})` : ""}`;
|
|
8254
|
+
return [
|
|
8255
|
+
`📊 ${card.label ?? "Usage"}`,
|
|
8256
|
+
...card.account ? [card.account] : [],
|
|
8257
|
+
left,
|
|
8258
|
+
...card.used !== void 0 && card.limit !== void 0 ? [`${card.used} of ${card.limit} used`] : [],
|
|
8259
|
+
...card.resetsAt ? [`resets ${cardTimeUtc(card.resetsAt)}`] : [],
|
|
8260
|
+
...(card.windows ?? []).map((w) => `${w.window} ${w.left}%`)
|
|
8261
|
+
].join(" · ");
|
|
8262
|
+
}
|
|
8263
|
+
if (card.kind === "buttons") return `Reply: ${card.buttons.map((b) => b.label).join(" / ")}`;
|
|
8264
|
+
if (card.kind === "task") {
|
|
8265
|
+
const progress = taskProgress(card);
|
|
8266
|
+
const state = [
|
|
8267
|
+
STATUS_WORD[card.status],
|
|
8268
|
+
...progress !== void 0 ? [`${progress}%`] : [],
|
|
8269
|
+
...card.done !== void 0 && card.total ? [`${card.done} of ${card.total} steps`] : []
|
|
8270
|
+
].join(" · ");
|
|
8271
|
+
const standing = card.pr && [...card.pr.state ? [card.pr.state] : [], ...card.pr.checks ? [`checks ${card.pr.checks}`] : []].join(", ");
|
|
8272
|
+
const pr = card.pr && [
|
|
8273
|
+
`PR${card.pr.number !== void 0 ? ` #${card.pr.number}` : ""}`,
|
|
8274
|
+
...card.pr.additions !== void 0 || card.pr.deletions !== void 0 ? [`+${card.pr.additions ?? 0} -${card.pr.deletions ?? 0}`] : [],
|
|
8275
|
+
...standing ? [`(${standing})`] : []
|
|
8276
|
+
].join(" ") + `: ${card.pr.url}`;
|
|
8277
|
+
const now = card.step && ACTIVE_TASK_STATUSES.includes(card.status) ? [`Now: ${card.step}`] : [];
|
|
8278
|
+
return [
|
|
8279
|
+
`${TASK_MARK[card.status]} ${card.title}`,
|
|
8280
|
+
state,
|
|
8281
|
+
...now,
|
|
8282
|
+
...pr ? [pr] : [],
|
|
8283
|
+
...card.tags?.length ? [`Tags: ${card.tags.join(", ")}`] : [],
|
|
8284
|
+
...card.parent ? [`Part of: ${card.parent}`] : []
|
|
8285
|
+
].join("\n");
|
|
8286
|
+
}
|
|
8287
|
+
const last = card.lastRun && `Last run: ${RESULT_WORD[card.lastRun.result]}, ${cardTimeUtc(card.lastRun.at)}${card.lastRun.summary ? ` · ${card.lastRun.summary}` : ""}`;
|
|
8288
|
+
return [
|
|
8289
|
+
`🔁 ${card.name}`,
|
|
8290
|
+
`${card.schedule} · ${card.state === "active" ? "active" : "paused"}`,
|
|
8291
|
+
...last ? [last] : [],
|
|
8292
|
+
...card.nextRunAt && card.state === "active" ? [`Next run: ${cardTimeUtc(card.nextRunAt)}`] : []
|
|
8293
|
+
].join("\n");
|
|
8294
|
+
}
|
|
8295
|
+
/** Whether a message's edit number holds: a text's bound, or a card message's when the edit carries a card. */
|
|
8296
|
+
function cardEditNumber(e) {
|
|
8297
|
+
return Number.isSafeInteger(e) && e >= 1 && e <= STATUS_CARD_LIMITS.edits;
|
|
6818
8298
|
}
|
|
6819
|
-
var GROUP_ADMIN_OFF_ERROR = "Managing this group is off on this device. Turn on Manage groups from this device first.";
|
|
6820
8299
|
//#endregion
|
|
6821
8300
|
//#region ../core/src/replies.ts
|
|
6822
8301
|
var REPLY_LIMITS = {
|
|
@@ -6840,8 +8319,7 @@ var INLINE_PICTURE = /^data:image\//i;
|
|
|
6840
8319
|
*/
|
|
6841
8320
|
function replySnippet(text) {
|
|
6842
8321
|
if (INLINE_PICTURE.test(text.trimStart())) return "🖼️ Picture";
|
|
6843
|
-
const
|
|
6844
|
-
const clean = sanitizeDisplayText(flat, REPLY_LIMITS.snippet + 1) ?? "";
|
|
8322
|
+
const clean = sanitizeDisplayText(text.slice(0, REPLY_LIMITS.raw * 2).replace(/\s+/gu, " "), REPLY_LIMITS.snippet + 1) ?? "";
|
|
6845
8323
|
const points = [...clean];
|
|
6846
8324
|
return points.length > REPLY_LIMITS.snippet ? `${points.slice(0, REPLY_LIMITS.snippet - 1).join("").trimEnd()}…` : clean;
|
|
6847
8325
|
}
|
|
@@ -7217,7 +8695,7 @@ function previewImageSize(value) {
|
|
|
7217
8695
|
if (binary.length > LINK_PREVIEW_LIMITS.imageBytes) return;
|
|
7218
8696
|
const bytes = new Uint8Array(binary.length);
|
|
7219
8697
|
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
7220
|
-
return jpegSize(bytes);
|
|
8698
|
+
return jpegSize$1(bytes);
|
|
7221
8699
|
}
|
|
7222
8700
|
/** The thumbnail if it is one Ghostly shows: a JPEG data URL within the limits whose header declares a sane size. */
|
|
7223
8701
|
function sanitizePreviewImage(value) {
|
|
@@ -7378,6 +8856,485 @@ var RateWindow = class {
|
|
|
7378
8856
|
}
|
|
7379
8857
|
};
|
|
7380
8858
|
//#endregion
|
|
8859
|
+
//#region ../core/src/image.ts
|
|
8860
|
+
var IMAGE_LIMITS = {
|
|
8861
|
+
/** Pixels on either side: the most a JPEG can say, well past what anyone sends as a picture. */
|
|
8862
|
+
maxEdge: 65535,
|
|
8863
|
+
/** How much of a file is read for its size: past a JPEG's EXIF, thumbnails and colour profile. */
|
|
8864
|
+
headBytes: 262144
|
|
8865
|
+
};
|
|
8866
|
+
/** Whether a file is a picture, by its type. */
|
|
8867
|
+
function isImageMime(mime) {
|
|
8868
|
+
return mime.split(";")[0].trim().toLowerCase().startsWith("image/");
|
|
8869
|
+
}
|
|
8870
|
+
var edge = (n) => typeof n === "number" && Number.isSafeInteger(n) && n > 0 && n <= IMAGE_LIMITS.maxEdge;
|
|
8871
|
+
/** A peer's `image` description, or undefined when it is not one. It never refuses the file. */
|
|
8872
|
+
function parseImageMeta(value, mime) {
|
|
8873
|
+
if (mime !== void 0 && !isImageMime(mime)) return void 0;
|
|
8874
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
8875
|
+
const { width, height } = value;
|
|
8876
|
+
return edge(width) && edge(height) ? {
|
|
8877
|
+
width,
|
|
8878
|
+
height
|
|
8879
|
+
} : void 0;
|
|
8880
|
+
}
|
|
8881
|
+
var u16be = (b, at) => b[at] << 8 | b[at + 1];
|
|
8882
|
+
var u16le = (b, at) => b[at] | b[at + 1] << 8;
|
|
8883
|
+
var u24le = (b, at) => b[at] | b[at + 1] << 8 | b[at + 2] << 16;
|
|
8884
|
+
var u32be = (b, at) => (b[at] << 24 >>> 0) + (b[at + 1] << 16) + (b[at + 2] << 8) + b[at + 3];
|
|
8885
|
+
var ascii = (b, at, text) => [...text].every((c, i) => b[at + i] === c.charCodeAt(0));
|
|
8886
|
+
var sized = (width, height) => edge(width) && edge(height) ? {
|
|
8887
|
+
width,
|
|
8888
|
+
height
|
|
8889
|
+
} : void 0;
|
|
8890
|
+
/** SOFn markers: every frame type but DHT (C4), JPG (C8) and DAC (CC). */
|
|
8891
|
+
var JPEG_FRAME = /* @__PURE__ */ new Set([
|
|
8892
|
+
192,
|
|
8893
|
+
193,
|
|
8894
|
+
194,
|
|
8895
|
+
195,
|
|
8896
|
+
197,
|
|
8897
|
+
198,
|
|
8898
|
+
199,
|
|
8899
|
+
201,
|
|
8900
|
+
202,
|
|
8901
|
+
203,
|
|
8902
|
+
205,
|
|
8903
|
+
206,
|
|
8904
|
+
207
|
|
8905
|
+
]);
|
|
8906
|
+
/** The EXIF orientation in an APP1 segment's TIFF block (1 when it says none). */
|
|
8907
|
+
function exifOrientation(b, tiff, end) {
|
|
8908
|
+
if (tiff + 8 > end) return 1;
|
|
8909
|
+
const little = ascii(b, tiff, "II");
|
|
8910
|
+
if (!little && !ascii(b, tiff, "MM")) return 1;
|
|
8911
|
+
const u16 = (at) => little ? u16le(b, at) : u16be(b, at);
|
|
8912
|
+
const u32 = (at) => little ? u16le(b, at) + u16le(b, at + 2) * 65536 : u32be(b, at);
|
|
8913
|
+
if (u16(tiff + 2) !== 42) return 1;
|
|
8914
|
+
const ifd = tiff + u32(tiff + 4);
|
|
8915
|
+
if (ifd + 2 > end) return 1;
|
|
8916
|
+
const count = u16(ifd);
|
|
8917
|
+
for (let i = 0; i < count; i++) {
|
|
8918
|
+
const entry = ifd + 2 + i * 12;
|
|
8919
|
+
if (entry + 12 > end) return 1;
|
|
8920
|
+
if (u16(entry) === 274) {
|
|
8921
|
+
const value = u16(entry + 8);
|
|
8922
|
+
return value >= 1 && value <= 8 ? value : 1;
|
|
8923
|
+
}
|
|
8924
|
+
}
|
|
8925
|
+
return 1;
|
|
8926
|
+
}
|
|
8927
|
+
function jpegSize(b) {
|
|
8928
|
+
let at = 2, orientation = 1;
|
|
8929
|
+
while (at + 4 <= b.length) {
|
|
8930
|
+
if (b[at] !== 255) return void 0;
|
|
8931
|
+
const marker = b[at + 1];
|
|
8932
|
+
if (marker === 255) {
|
|
8933
|
+
at++;
|
|
8934
|
+
continue;
|
|
8935
|
+
}
|
|
8936
|
+
if (marker === 1 || marker >= 208 && marker <= 215) {
|
|
8937
|
+
at += 2;
|
|
8938
|
+
continue;
|
|
8939
|
+
}
|
|
8940
|
+
if (marker === 217 || marker === 218) return void 0;
|
|
8941
|
+
const length = u16be(b, at + 2);
|
|
8942
|
+
if (length < 2) return void 0;
|
|
8943
|
+
const end = Math.min(b.length, at + 2 + length);
|
|
8944
|
+
if (marker === 225 && ascii(b, at + 4, "Exif\0\0")) orientation = exifOrientation(b, at + 10, end);
|
|
8945
|
+
if (JPEG_FRAME.has(marker)) {
|
|
8946
|
+
if (at + 9 > b.length) return void 0;
|
|
8947
|
+
const height = u16be(b, at + 5), width = u16be(b, at + 7);
|
|
8948
|
+
return orientation >= 5 ? sized(height, width) : sized(width, height);
|
|
8949
|
+
}
|
|
8950
|
+
at += 2 + length;
|
|
8951
|
+
}
|
|
8952
|
+
}
|
|
8953
|
+
function webpSize(b) {
|
|
8954
|
+
if (b.length < 30) return void 0;
|
|
8955
|
+
if (ascii(b, 12, "VP8 ")) {
|
|
8956
|
+
if (b[23] !== 157 || b[24] !== 1 || b[25] !== 42) return void 0;
|
|
8957
|
+
return sized(u16le(b, 26) & 16383, u16le(b, 28) & 16383);
|
|
8958
|
+
}
|
|
8959
|
+
if (ascii(b, 12, "VP8L")) {
|
|
8960
|
+
if (b[20] !== 47) return void 0;
|
|
8961
|
+
const bits = b[21] | b[22] << 8 | b[23] << 16 | b[24] << 24;
|
|
8962
|
+
return sized((bits & 16383) + 1, (bits >>> 14 & 16383) + 1);
|
|
8963
|
+
}
|
|
8964
|
+
if (ascii(b, 12, "VP8X")) return sized(u24le(b, 24) + 1, u24le(b, 27) + 1);
|
|
8965
|
+
}
|
|
8966
|
+
/**
|
|
8967
|
+
* A picture's size as it is shown, read from its first bytes (PNG, JPEG, GIF, WebP), or undefined when they do not
|
|
8968
|
+
* say. Only the header is read: a JPEG whose frame header sits past `IMAGE_LIMITS.headBytes` goes without.
|
|
8969
|
+
*/
|
|
8970
|
+
function imageSize(head) {
|
|
8971
|
+
const b = head;
|
|
8972
|
+
try {
|
|
8973
|
+
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));
|
|
8974
|
+
if (b.length >= 10 && (ascii(b, 0, "GIF87a") || ascii(b, 0, "GIF89a"))) return sized(u16le(b, 6), u16le(b, 8));
|
|
8975
|
+
if (b.length >= 4 && b[0] === 255 && b[1] === 216 && b[2] === 255) return jpegSize(b);
|
|
8976
|
+
if (b.length >= 16 && ascii(b, 0, "RIFF") && ascii(b, 8, "WEBP")) return webpSize(b);
|
|
8977
|
+
} catch {}
|
|
8978
|
+
}
|
|
8979
|
+
/**
|
|
8980
|
+
* The description a picture is sent with, read from the start of the file, or undefined when it is not a picture
|
|
8981
|
+
* or its first bytes do not say (it then goes without one).
|
|
8982
|
+
*/
|
|
8983
|
+
async function readImageMeta(source, mime) {
|
|
8984
|
+
if (!isImageMime(mime)) return void 0;
|
|
8985
|
+
try {
|
|
8986
|
+
return imageSize(new Uint8Array(await source.slice(0, IMAGE_LIMITS.headBytes).arrayBuffer()));
|
|
8987
|
+
} catch {
|
|
8988
|
+
return;
|
|
8989
|
+
}
|
|
8990
|
+
}
|
|
8991
|
+
//#endregion
|
|
8992
|
+
//#region ../core/src/video.ts
|
|
8993
|
+
var VIDEO_LIMITS = {
|
|
8994
|
+
/** The poster's bytes: with the rest of an offer, a frame stays far below the 60 KiB a session carries. */
|
|
8995
|
+
maxPosterBytes: 12288,
|
|
8996
|
+
/** The poster's longest side, in pixels, as the sender draws it. */
|
|
8997
|
+
posterEdge: 320,
|
|
8998
|
+
/** Ten hours: longer is a mistake, not a video. */
|
|
8999
|
+
maxDurationMs: 36e6,
|
|
9000
|
+
/** Pixels on either side (8K and a bit). */
|
|
9001
|
+
maxEdge: 16384
|
|
9002
|
+
};
|
|
9003
|
+
/** Whether a file is a video, by its type: shown in a video bubble whether or not it came with a description. */
|
|
9004
|
+
function isVideoMime(mime) {
|
|
9005
|
+
return mime.split(";")[0].trim().toLowerCase().startsWith("video/");
|
|
9006
|
+
}
|
|
9007
|
+
var BASE64URL = /^[A-Za-z0-9_-]+$/;
|
|
9008
|
+
var JPEG_START = [
|
|
9009
|
+
255,
|
|
9010
|
+
216,
|
|
9011
|
+
255
|
|
9012
|
+
];
|
|
9013
|
+
/**
|
|
9014
|
+
* A peer's `video` description, or undefined when it is not one. It never refuses the file. A poster that is
|
|
9015
|
+
* too large, not a JPEG, or says it is more pixels than a sender draws (`posterEdge`) is dropped on its own; the
|
|
9016
|
+
* length and size are kept. It is shown before the file is accepted, so a few bytes must not ask for a huge bitmap.
|
|
9017
|
+
*/
|
|
9018
|
+
function parseVideoMeta(value, mime) {
|
|
9019
|
+
if (mime !== void 0 && !isVideoMime(mime)) return void 0;
|
|
9020
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
9021
|
+
const { duration, width, height, poster } = value;
|
|
9022
|
+
if (typeof duration !== "number" || !Number.isSafeInteger(duration) || duration <= 0 || duration > VIDEO_LIMITS.maxDurationMs) return void 0;
|
|
9023
|
+
const edge = (n) => typeof n === "number" && Number.isSafeInteger(n) && n > 0 && n <= VIDEO_LIMITS.maxEdge;
|
|
9024
|
+
if (!edge(width) || !edge(height)) return void 0;
|
|
9025
|
+
const meta = {
|
|
9026
|
+
duration,
|
|
9027
|
+
width,
|
|
9028
|
+
height
|
|
9029
|
+
};
|
|
9030
|
+
if (typeof poster === "string" && poster.length <= Math.ceil(VIDEO_LIMITS.maxPosterBytes * 4 / 3) && BASE64URL.test(poster)) try {
|
|
9031
|
+
const bytes = fromBase64Url(poster);
|
|
9032
|
+
const size = bytes.length <= VIDEO_LIMITS.maxPosterBytes && JPEG_START.every((b, i) => bytes[i] === b) ? imageSize(bytes) : void 0;
|
|
9033
|
+
if (size && Math.max(size.width, size.height) <= VIDEO_LIMITS.posterEdge) meta.poster = poster;
|
|
9034
|
+
} catch {}
|
|
9035
|
+
return meta;
|
|
9036
|
+
}
|
|
9037
|
+
/** `0:07`, `12:45`, `1:02:03`: as a player shows a video's length. */
|
|
9038
|
+
function formatVideoDuration(ms) {
|
|
9039
|
+
const total = Math.max(0, Math.floor((Number.isFinite(ms) ? ms : 0) / 1e3));
|
|
9040
|
+
const hours = Math.floor(total / 3600), minutes = Math.floor(total % 3600 / 60), seconds = String(total % 60).padStart(2, "0");
|
|
9041
|
+
return hours ? `${hours}:${String(minutes).padStart(2, "0")}:${seconds}` : `${minutes}:${seconds}`;
|
|
9042
|
+
}
|
|
9043
|
+
//#endregion
|
|
9044
|
+
//#region ../core/src/voice.ts
|
|
9045
|
+
var VOICE_LIMITS = {
|
|
9046
|
+
/** Fifteen minutes: at the bitrate recorded (32 kbit/s) about 3.6 MB, well inside a held item's 8 MiB. */
|
|
9047
|
+
maxDurationMs: 9e5,
|
|
9048
|
+
/** Bars drawn for a recording. */
|
|
9049
|
+
bars: 64,
|
|
9050
|
+
/** Bars accepted from a peer. */
|
|
9051
|
+
maxBars: 128,
|
|
9052
|
+
/** Asked of the recorder; opus speech is clear well below it. */
|
|
9053
|
+
audioBitsPerSecond: 32e3
|
|
9054
|
+
};
|
|
9055
|
+
/**
|
|
9056
|
+
* Audio a player may be handed as it is. None of these runs anything in the page's origin,
|
|
9057
|
+
* so received bytes of these types are served with their type (see `safeBlobType`).
|
|
9058
|
+
*/
|
|
9059
|
+
var PLAYABLE_AUDIO = /^audio\/(webm|ogg|opus|mp4|mpeg|mp3|aac|x-m4a|wav|x-wav|wave|flac|x-flac)$/;
|
|
9060
|
+
/** The part of a type before its parameters, in lower case: `audio/webm;codecs=opus` → `audio/webm`. */
|
|
9061
|
+
function baseMime(mime) {
|
|
9062
|
+
return mime.split(";")[0].trim().toLowerCase();
|
|
9063
|
+
}
|
|
9064
|
+
/**
|
|
9065
|
+
* A peer's `voice` description, or undefined when it is not one. It never refuses the file:
|
|
9066
|
+
* a bad description only means the file is shown as a file.
|
|
9067
|
+
*/
|
|
9068
|
+
function parseVoiceMeta(value, mime) {
|
|
9069
|
+
if (mime !== void 0 && !PLAYABLE_AUDIO.test(baseMime(mime))) return void 0;
|
|
9070
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
9071
|
+
const { duration, peaks } = value;
|
|
9072
|
+
if (typeof duration !== "number" || !Number.isSafeInteger(duration) || duration <= 0 || duration > VOICE_LIMITS.maxDurationMs + 5e3) return void 0;
|
|
9073
|
+
if (!Array.isArray(peaks) || peaks.length === 0 || peaks.length > VOICE_LIMITS.maxBars) return void 0;
|
|
9074
|
+
if (!peaks.every((peak) => typeof peak === "number" && Number.isInteger(peak) && peak >= 0 && peak <= 255)) return void 0;
|
|
9075
|
+
return {
|
|
9076
|
+
duration,
|
|
9077
|
+
peaks: [...peaks]
|
|
9078
|
+
};
|
|
9079
|
+
}
|
|
9080
|
+
/**
|
|
9081
|
+
* How the level is metered while recording (apps/ui/src/lib/voiceRecorder.ts): every `intervalMs`, the RMS of the last
|
|
9082
|
+
* `window` samples an AnalyserNode holds (at a 48 kHz context, about 21 ms of sound).
|
|
9083
|
+
*/
|
|
9084
|
+
var VOICE_METER = {
|
|
9085
|
+
intervalMs: 50,
|
|
9086
|
+
window: 1024,
|
|
9087
|
+
windowRate: 48e3
|
|
9088
|
+
};
|
|
9089
|
+
/** One reading of the meter, 0-1: the RMS of the samples, lifted because speech sits low on a linear scale. */
|
|
9090
|
+
function voiceLevel(samples) {
|
|
9091
|
+
if (samples.length === 0) return 0;
|
|
9092
|
+
let sum = 0;
|
|
9093
|
+
for (let i = 0; i < samples.length; i++) sum += samples[i] * samples[i];
|
|
9094
|
+
return Math.min(1, Math.sqrt(sum / samples.length) * 4);
|
|
9095
|
+
}
|
|
9096
|
+
/**
|
|
9097
|
+
* The readings the recorder would have taken of this sound (mono, -1 to 1), for a recording made elsewhere: one
|
|
9098
|
+
* every `intervalMs` of it, each over the window that ends there, as long in time as the analyser's.
|
|
9099
|
+
*/
|
|
9100
|
+
function voiceLevelsOf(mono, sampleRate) {
|
|
9101
|
+
if (!(sampleRate > 0) || mono.length === 0) return [];
|
|
9102
|
+
const window = Math.max(1, Math.round(VOICE_METER.window * sampleRate / VOICE_METER.windowRate));
|
|
9103
|
+
const levels = [];
|
|
9104
|
+
for (let step = 1;; step++) {
|
|
9105
|
+
const end = Math.round(step * VOICE_METER.intervalMs * sampleRate / 1e3);
|
|
9106
|
+
if (end > mono.length) break;
|
|
9107
|
+
levels.push(voiceLevel(mono.subarray(Math.max(0, end - window), end)));
|
|
9108
|
+
}
|
|
9109
|
+
return levels;
|
|
9110
|
+
}
|
|
9111
|
+
/** The waveform sent with a recording made elsewhere, drawn as the recorder draws its own. */
|
|
9112
|
+
function voicePeaksOf(mono, sampleRate, bars = VOICE_LIMITS.bars) {
|
|
9113
|
+
return downsamplePeaks(voiceLevelsOf(mono, sampleRate), bars);
|
|
9114
|
+
}
|
|
9115
|
+
/** Below this a recording is silence, and is drawn flat rather than stretched to fill the bars. */
|
|
9116
|
+
var QUIET = .02;
|
|
9117
|
+
/**
|
|
9118
|
+
* Bars from the loudness measured while recording (RMS, 0-1, oldest first): the loudest
|
|
9119
|
+
* sample of each stretch, scaled so the loudest bar is full height.
|
|
9120
|
+
*/
|
|
9121
|
+
function downsamplePeaks(levels, bars = VOICE_LIMITS.bars) {
|
|
9122
|
+
const count = Math.max(1, Math.min(VOICE_LIMITS.maxBars, Math.floor(bars)));
|
|
9123
|
+
const clean = Array.from(levels, (level) => Number.isFinite(level) ? Math.min(1, Math.max(0, level)) : 0);
|
|
9124
|
+
if (clean.length === 0) return new Array(count).fill(0);
|
|
9125
|
+
const loudest = Math.max(QUIET, ...clean);
|
|
9126
|
+
const out = [];
|
|
9127
|
+
for (let bar = 0; bar < count; bar++) {
|
|
9128
|
+
const from = Math.floor(bar * clean.length / count);
|
|
9129
|
+
const to = Math.max(from + 1, Math.floor((bar + 1) * clean.length / count));
|
|
9130
|
+
let peak = 0;
|
|
9131
|
+
for (let i = from; i < to && i < clean.length; i++) peak = Math.max(peak, clean[i]);
|
|
9132
|
+
out.push(Math.round(peak / loudest * 255));
|
|
9133
|
+
}
|
|
9134
|
+
return out;
|
|
9135
|
+
}
|
|
9136
|
+
/** `0:07`, `12:45`: minutes and seconds, as a player shows them. */
|
|
9137
|
+
function formatVoiceDuration(ms) {
|
|
9138
|
+
const total = Math.max(0, Math.floor((Number.isFinite(ms) ? ms : 0) / 1e3));
|
|
9139
|
+
return `${Math.floor(total / 60)}:${String(total % 60).padStart(2, "0")}`;
|
|
9140
|
+
}
|
|
9141
|
+
/** The line a file message shows in the chat list and in notifications. */
|
|
9142
|
+
function fileMessageText(file) {
|
|
9143
|
+
if (file.voice) return `🎤 Voice message (${formatVoiceDuration(file.voice.duration)})`;
|
|
9144
|
+
if (file.video) return `🎬 Video (${formatVideoDuration(file.video.duration)})`;
|
|
9145
|
+
return `📎 ${file.name}`;
|
|
9146
|
+
}
|
|
9147
|
+
//#endregion
|
|
9148
|
+
//#region ../core/src/files.ts
|
|
9149
|
+
var FILE_ID = /^[A-Za-z0-9_-]{8,64}$/;
|
|
9150
|
+
var MIME = /^[a-z0-9][a-z0-9!#$&^_.+-]{0,63}\/[a-z0-9][a-z0-9!#$&^_.+-]{0,63}$/i;
|
|
9151
|
+
var MAX_NAME_LENGTH = 200;
|
|
9152
|
+
/**
|
|
9153
|
+
* Invisible and direction-changing characters: bidi overrides turn
|
|
9154
|
+
* "invoice\u202Efdp.exe" into what reads as "invoiceexe.pdf", zero-width ones hide
|
|
9155
|
+
* text. Cf covers both; line and paragraph separators break the layout.
|
|
9156
|
+
*/
|
|
9157
|
+
var INVISIBLE = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu;
|
|
9158
|
+
/**
|
|
9159
|
+
* Names Windows keeps for devices, whatever the case, with any extension, and with spaces before the dot:
|
|
9160
|
+
* "CON", "nul.txt", "COM1 .log" open a device there, not a file.
|
|
9161
|
+
*/
|
|
9162
|
+
var WINDOWS_DEVICE = /^(CON|PRN|AUX|NUL|CONIN\$|CONOUT\$|COM[0-9¹²³]|LPT[0-9¹²³])\s*(\.|$)/i;
|
|
9163
|
+
/**
|
|
9164
|
+
* At most `MAX_NAME_LENGTH` characters. A longer name loses the end of its stem, not its extension (up to 15
|
|
9165
|
+
* characters after the last dot): "…report.pdf" stays a PDF to the system it is saved on.
|
|
9166
|
+
*/
|
|
9167
|
+
function shorten(name) {
|
|
9168
|
+
const chars = [...name];
|
|
9169
|
+
if (chars.length <= MAX_NAME_LENGTH) return name;
|
|
9170
|
+
const extension = [.../\.[^.\s]{1,15}$/u.exec(name)?.[0] ?? ""];
|
|
9171
|
+
return chars.slice(0, MAX_NAME_LENGTH - extension.length).join("").trimEnd() + extension.join("");
|
|
9172
|
+
}
|
|
9173
|
+
/** A file name is display text and a download suggestion, never a path. */
|
|
9174
|
+
function sanitizeFileName(name) {
|
|
9175
|
+
const clean = shorten(name.replace(INVISIBLE, "").replace(/[/\\:]/g, "").replace(/^[\s.]+/, "").trim());
|
|
9176
|
+
if (WINDOWS_DEVICE.test(clean)) return shorten("_" + clean);
|
|
9177
|
+
return clean || "file";
|
|
9178
|
+
}
|
|
9179
|
+
function sanitizeMime(mime) {
|
|
9180
|
+
return MIME.test(mime) ? mime.toLowerCase() : "application/octet-stream";
|
|
9181
|
+
}
|
|
9182
|
+
/** A size for people: B, KB, MB, GB, TB (powers of 1024), one decimal from KB up. */
|
|
9183
|
+
function formatFileSize(bytes) {
|
|
9184
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
9185
|
+
const units = [
|
|
9186
|
+
"KB",
|
|
9187
|
+
"MB",
|
|
9188
|
+
"GB",
|
|
9189
|
+
"TB"
|
|
9190
|
+
];
|
|
9191
|
+
let value = bytes / 1024, unit = 0;
|
|
9192
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
9193
|
+
value /= 1024;
|
|
9194
|
+
unit++;
|
|
9195
|
+
}
|
|
9196
|
+
return `${value.toFixed(1)} ${units[unit]}`;
|
|
9197
|
+
}
|
|
9198
|
+
var FileTransfers = class {
|
|
9199
|
+
channel;
|
|
9200
|
+
events;
|
|
9201
|
+
nextStreamId = 1;
|
|
9202
|
+
incoming = /* @__PURE__ */ new Map();
|
|
9203
|
+
outgoing = /* @__PURE__ */ new Map();
|
|
9204
|
+
constructor(channel, events) {
|
|
9205
|
+
this.channel = channel;
|
|
9206
|
+
this.events = events;
|
|
9207
|
+
}
|
|
9208
|
+
/** Streams a file to the peer. Resolves once every byte is handed to the channel. */
|
|
9209
|
+
async send(file, source) {
|
|
9210
|
+
if (file.size > LIMITS.maxFileBytes) throw new Error("File is too large");
|
|
9211
|
+
const id = this.nextStreamId++;
|
|
9212
|
+
const state = {
|
|
9213
|
+
fileId: file.id,
|
|
9214
|
+
cancelled: false
|
|
9215
|
+
};
|
|
9216
|
+
this.outgoing.set(id, state);
|
|
9217
|
+
try {
|
|
9218
|
+
this.channel.send(encodeControl({
|
|
9219
|
+
t: "file",
|
|
9220
|
+
id,
|
|
9221
|
+
f: file.id,
|
|
9222
|
+
ts: file.timestamp,
|
|
9223
|
+
n: sanitizeFileName(file.name),
|
|
9224
|
+
s: file.size,
|
|
9225
|
+
m: sanitizeMime(file.mime)
|
|
9226
|
+
}));
|
|
9227
|
+
let sent = 0;
|
|
9228
|
+
const counted = async function* () {
|
|
9229
|
+
for await (const part of source) {
|
|
9230
|
+
sent += part.length;
|
|
9231
|
+
yield part;
|
|
9232
|
+
this.events.onProgress?.(file.id, sent, "out");
|
|
9233
|
+
}
|
|
9234
|
+
}.call(this);
|
|
9235
|
+
await sendBody(this.channel, CHUNK_KIND.fileBody, id, counted, () => state.cancelled);
|
|
9236
|
+
if (state.cancelled) throw new Error("The peer refused the file");
|
|
9237
|
+
if (sent !== file.size) throw new Error("File changed while it was being sent");
|
|
9238
|
+
this.events.onComplete?.(file.id, "out");
|
|
9239
|
+
} catch (error) {
|
|
9240
|
+
if (!state.cancelled) try {
|
|
9241
|
+
this.channel.send(encodeControl({
|
|
9242
|
+
t: "rst",
|
|
9243
|
+
id,
|
|
9244
|
+
d: "f",
|
|
9245
|
+
e: "aborted"
|
|
9246
|
+
}));
|
|
9247
|
+
} catch {}
|
|
9248
|
+
this.events.onFailed?.(file.id, error instanceof Error ? error.message : String(error), "out");
|
|
9249
|
+
throw error;
|
|
9250
|
+
} finally {
|
|
9251
|
+
this.outgoing.delete(id);
|
|
9252
|
+
}
|
|
9253
|
+
}
|
|
9254
|
+
handleFile(frame) {
|
|
9255
|
+
if (this.incoming.has(frame.id)) return;
|
|
9256
|
+
const refuse = (reason) => {
|
|
9257
|
+
try {
|
|
9258
|
+
this.channel.send(encodeControl({
|
|
9259
|
+
t: "rst",
|
|
9260
|
+
id: frame.id,
|
|
9261
|
+
d: "f",
|
|
9262
|
+
e: reason
|
|
9263
|
+
}));
|
|
9264
|
+
} catch {}
|
|
9265
|
+
};
|
|
9266
|
+
if (!FILE_ID.test(frame.f)) return refuse("invalid file id");
|
|
9267
|
+
if (frame.s > LIMITS.maxFileBytes) return refuse("file too large");
|
|
9268
|
+
if (this.incoming.size >= LIMITS.maxIncomingFilesPerPeer) return refuse("too many transfers");
|
|
9269
|
+
const file = {
|
|
9270
|
+
id: frame.f,
|
|
9271
|
+
name: sanitizeFileName(frame.n),
|
|
9272
|
+
size: frame.s,
|
|
9273
|
+
mime: sanitizeMime(frame.m),
|
|
9274
|
+
timestamp: frame.ts
|
|
9275
|
+
};
|
|
9276
|
+
const sink = this.events.onIncoming(file);
|
|
9277
|
+
if (!sink || typeof sink === "string") return refuse(sink || "refused");
|
|
9278
|
+
const entry = {
|
|
9279
|
+
file,
|
|
9280
|
+
sink,
|
|
9281
|
+
received: 0,
|
|
9282
|
+
queue: Promise.resolve()
|
|
9283
|
+
};
|
|
9284
|
+
this.incoming.set(frame.id, entry);
|
|
9285
|
+
this.armIdleTimer(frame.id, entry);
|
|
9286
|
+
}
|
|
9287
|
+
handleChunk(chunk) {
|
|
9288
|
+
const entry = this.incoming.get(chunk.id);
|
|
9289
|
+
if (!entry) return;
|
|
9290
|
+
entry.received += chunk.payload.length;
|
|
9291
|
+
if (entry.received > entry.file.size) return this.failIncoming(chunk.id, "more data than announced", true);
|
|
9292
|
+
if (chunk.payload.length > 0) {
|
|
9293
|
+
const payload = chunk.payload.slice();
|
|
9294
|
+
entry.queue = entry.queue.then(() => entry.sink.write(payload));
|
|
9295
|
+
this.events.onProgress?.(entry.file.id, entry.received, "in");
|
|
9296
|
+
}
|
|
9297
|
+
if (!chunk.end) return this.armIdleTimer(chunk.id, entry);
|
|
9298
|
+
if (entry.received !== entry.file.size) return this.failIncoming(chunk.id, "incomplete file", false);
|
|
9299
|
+
clearTimeout(entry.idleTimer);
|
|
9300
|
+
this.incoming.delete(chunk.id);
|
|
9301
|
+
entry.queue.then(() => entry.sink.close()).then(() => this.events.onComplete?.(entry.file.id, "in"), () => {
|
|
9302
|
+
entry.sink.abort();
|
|
9303
|
+
this.events.onFailed?.(entry.file.id, "could not store the file", "in");
|
|
9304
|
+
});
|
|
9305
|
+
}
|
|
9306
|
+
handleReset(frame) {
|
|
9307
|
+
if (this.incoming.has(frame.id)) this.failIncoming(frame.id, frame.e || "cancelled by the sender", false);
|
|
9308
|
+
const sending = this.outgoing.get(frame.id);
|
|
9309
|
+
if (sending) sending.cancelled = true;
|
|
9310
|
+
}
|
|
9311
|
+
/** The link went away: nothing in flight can finish. */
|
|
9312
|
+
closeAll() {
|
|
9313
|
+
for (const id of [...this.incoming.keys()]) this.failIncoming(id, "connection lost", false);
|
|
9314
|
+
for (const sending of this.outgoing.values()) sending.cancelled = true;
|
|
9315
|
+
}
|
|
9316
|
+
armIdleTimer(id, entry) {
|
|
9317
|
+
clearTimeout(entry.idleTimer);
|
|
9318
|
+
entry.idleTimer = setTimeout(() => this.failIncoming(id, "transfer stalled", true), LIMITS.bodyIdleTimeoutMs);
|
|
9319
|
+
}
|
|
9320
|
+
failIncoming(id, reason, notifyPeer) {
|
|
9321
|
+
const entry = this.incoming.get(id);
|
|
9322
|
+
if (!entry) return;
|
|
9323
|
+
clearTimeout(entry.idleTimer);
|
|
9324
|
+
this.incoming.delete(id);
|
|
9325
|
+
entry.sink.abort();
|
|
9326
|
+
if (notifyPeer) try {
|
|
9327
|
+
this.channel.send(encodeControl({
|
|
9328
|
+
t: "rst",
|
|
9329
|
+
id,
|
|
9330
|
+
d: "f",
|
|
9331
|
+
e: reason
|
|
9332
|
+
}));
|
|
9333
|
+
} catch {}
|
|
9334
|
+
this.events.onFailed?.(entry.file.id, reason, "in");
|
|
9335
|
+
}
|
|
9336
|
+
};
|
|
9337
|
+
//#endregion
|
|
7381
9338
|
//#region ../core/src/dhtFiles.ts
|
|
7382
9339
|
/**
|
|
7383
9340
|
* 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 +9574,14 @@ var DhtDelivery = class DhtDelivery {
|
|
|
7617
9574
|
};
|
|
7618
9575
|
}
|
|
7619
9576
|
/**
|
|
9577
|
+
* This side joined, and its inviter never answered (`DhtDeliveryState.unanswered`): no envelope of its newer than the
|
|
9578
|
+
* one that pinned it, none sealed to this side, no text confirmed, never live (as `sealedToAnother` asks too).
|
|
9579
|
+
*/
|
|
9580
|
+
get unanswered() {
|
|
9581
|
+
const pinnedAt = this.state.unanswered;
|
|
9582
|
+
return pinnedAt !== void 0 && this.state.peerSequence <= pinnedAt && !this.state.peerPinned && !this.state.confirmed && !this.state.wasLive && !this.live;
|
|
9583
|
+
}
|
|
9584
|
+
/**
|
|
7620
9585
|
* Something signed by a participation key other than the pinned one came in over the chat's invite-derived keys (this
|
|
7621
9586
|
* mailbox, the link's signals, a connection to an endpoint whose address a copy of the invite could read): ignored,
|
|
7622
9587
|
* and said in the view as a passive warning.
|
|
@@ -8325,6 +10290,7 @@ var DhtDelivery = class DhtDelivery {
|
|
|
8325
10290
|
}
|
|
8326
10291
|
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
10292
|
if (receipt !== null && (typeof receipt !== "string" || !ID$2.test(receipt))) return "none";
|
|
10293
|
+
const unanswered = !this.options.credentials.peerKey && !!this.options.credentials.expectedPeerKey && !sender && !this.live;
|
|
8328
10294
|
await this.options.pin(author);
|
|
8329
10295
|
this.options.credentials.peerKey = author;
|
|
8330
10296
|
this.options.credentials.requireSignedSignals = true;
|
|
@@ -8375,6 +10341,7 @@ var DhtDelivery = class DhtDelivery {
|
|
|
8375
10341
|
receipt: nextReceipt,
|
|
8376
10342
|
confirmed,
|
|
8377
10343
|
...peerPinned && { peerPinned },
|
|
10344
|
+
...unanswered && { unanswered: sequence },
|
|
8378
10345
|
pending: confirmed && this.state.pending?.message[0] === confirmed ? void 0 : this.state.pending,
|
|
8379
10346
|
...newest > (this.state.reactionsTaken ?? 0) && { reactionsTaken: newest }
|
|
8380
10347
|
});
|
|
@@ -8496,10 +10463,13 @@ var DhtDelivery = class DhtDelivery {
|
|
|
8496
10463
|
if (this.state.pending && this.state.pending.expires > now) return DHT_POLL_MS;
|
|
8497
10464
|
return this.active ? ACTIVE_DHT_POLL_MS : STREAM_POLL_MS;
|
|
8498
10465
|
}
|
|
8499
|
-
/**
|
|
10466
|
+
/**
|
|
10467
|
+
* How long ago this side's invite was made (or joined), while nobody used it (or its inviter never answered,
|
|
10468
|
+
* `unanswered`) and it is no longer awaited (`AWAITING_PEER_MS`).
|
|
10469
|
+
*/
|
|
8500
10470
|
unusedInviteAge(now) {
|
|
8501
10471
|
const madeAt = this.options.inviteMadeAt;
|
|
8502
|
-
if (madeAt === void 0 || this.options.credentials.peerKey || this.state.peerSequence) return void 0;
|
|
10472
|
+
if (madeAt === void 0 || (this.options.credentials.peerKey || this.state.peerSequence) && !this.unanswered) return void 0;
|
|
8503
10473
|
const age = now - Math.min(madeAt, this.startedAt);
|
|
8504
10474
|
return age >= 6e5 ? age : void 0;
|
|
8505
10475
|
}
|
|
@@ -11631,6 +13601,8 @@ function readManifest(meta, loopback = false) {
|
|
|
11631
13601
|
*/
|
|
11632
13602
|
var OFFER_EXTENSIONS = ["ping/1"];
|
|
11633
13603
|
var MAX_HANDSHAKE_BYTES = 4096;
|
|
13604
|
+
/** One frame from the peer, at most (UTF-16 units for text, bytes for binary): a bigger one breaks the protocol. */
|
|
13605
|
+
var SESSION_FRAME_MAX = 61440;
|
|
11634
13606
|
/**
|
|
11635
13607
|
* Frames received and not handled yet, at most: their count and their size together. Frames are handled one at a time,
|
|
11636
13608
|
* each awaited (a group frame is decrypted, checked and stored before the next), and the peer sends at the network's
|
|
@@ -11641,7 +13613,7 @@ var MAX_HANDSHAKE_BYTES = 4096;
|
|
|
11641
13613
|
*/
|
|
11642
13614
|
var SESSION_RECEIVE_PENDING = {
|
|
11643
13615
|
frames: 1024,
|
|
11644
|
-
bytes:
|
|
13616
|
+
bytes: 64 * SESSION_FRAME_MAX
|
|
11645
13617
|
};
|
|
11646
13618
|
var KEY = /^[ybndrfg8ejkmcpqxot1uwisza345h769]{52}$/;
|
|
11647
13619
|
var NONCE$1 = /^[A-Za-z0-9_-]{43}$/;
|
|
@@ -11992,6 +13964,9 @@ var PairedHttp = class {
|
|
|
11992
13964
|
return channel.bufferedAmount;
|
|
11993
13965
|
},
|
|
11994
13966
|
drained: () => channel.drained(),
|
|
13967
|
+
get sendBudget() {
|
|
13968
|
+
return channel.sendBudget;
|
|
13969
|
+
},
|
|
11995
13970
|
close: () => {},
|
|
11996
13971
|
onMessage: null,
|
|
11997
13972
|
onClose: null
|
|
@@ -12491,8 +14466,7 @@ function vapidAuthorization(endpoint, keys, subject, now = Date.now()) {
|
|
|
12491
14466
|
exp: Math.floor(now / 1e3) + TOKEN_SECONDS,
|
|
12492
14467
|
sub: subject
|
|
12493
14468
|
})}`;
|
|
12494
|
-
|
|
12495
|
-
return `vapid t=${unsigned}.${toBase64Url(signature)}, k=${keys.publicKey}`;
|
|
14469
|
+
return `vapid t=${unsigned}.${toBase64Url(p256.sign(utf8Encode(unsigned), fromBase64Url(keys.privateKey)))}, k=${keys.publicKey}`;
|
|
12496
14470
|
}
|
|
12497
14471
|
/**
|
|
12498
14472
|
* `plaintext` encrypted for a subscription (RFC 8291 with RFC 8188 `aes128gcm`, one record). `sender` fixes
|
|
@@ -12810,8 +14784,7 @@ var LINK_OR_MARKUP = /[a-z][a-z0-9+.-]*:\/\/|\bwww\.|\b(?:mailto|javascript|data
|
|
|
12810
14784
|
*/
|
|
12811
14785
|
function sanitizeTypingStatus(value) {
|
|
12812
14786
|
if (typeof value !== "string") return void 0;
|
|
12813
|
-
const
|
|
12814
|
-
const clean = sanitizeDisplayText(oneLine, 40);
|
|
14787
|
+
const clean = sanitizeDisplayText(value.slice(0, 160).replace(/\s+/g, " "), 40);
|
|
12815
14788
|
if (!clean || LINK_OR_MARKUP.test(clean)) return void 0;
|
|
12816
14789
|
return clean;
|
|
12817
14790
|
}
|
|
@@ -13060,7 +15033,7 @@ var FILE_LIMITS = {
|
|
|
13060
15033
|
resendBytes: 268435456
|
|
13061
15034
|
};
|
|
13062
15035
|
var ID = /^[A-Za-z0-9_-]{8,64}$/;
|
|
13063
|
-
var DIGEST = /^[A-Za-z0-9_-]{43}$/;
|
|
15036
|
+
var DIGEST$1 = /^[A-Za-z0-9_-]{43}$/;
|
|
13064
15037
|
var DATA = /^[A-Za-z0-9_-]+$/;
|
|
13065
15038
|
var FILE_FRAMES = /* @__PURE__ */ new Set([
|
|
13066
15039
|
"pf-offer",
|
|
@@ -13913,7 +15886,7 @@ var ChatFiles = class {
|
|
|
13913
15886
|
return;
|
|
13914
15887
|
}
|
|
13915
15888
|
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;
|
|
15889
|
+
if (frame.size !== record.file.size || typeof frame.digest !== "string" || !DIGEST$1.test(frame.digest)) return;
|
|
13917
15890
|
const digest = frame.digest;
|
|
13918
15891
|
await incoming.writing;
|
|
13919
15892
|
if (transferEnded(record) || record.state !== "active") return;
|
|
@@ -14952,6 +16925,151 @@ var CarriedTransport = class {
|
|
|
14952
16925
|
}
|
|
14953
16926
|
};
|
|
14954
16927
|
//#endregion
|
|
16928
|
+
//#region ../core/src/groupFiles.ts
|
|
16929
|
+
/**
|
|
16930
|
+
* Files and voice messages in groups (WISP 503 · Group Files). A file is announced as a group message carrying its
|
|
16931
|
+
* description; its bytes go over `files/3` between two members, from a member that holds it to one that asks for it.
|
|
16932
|
+
* The limits below are the profile's first numbers, kept in one place so they are easy to change.
|
|
16933
|
+
*/
|
|
16934
|
+
var GROUP_FILE_LIMITS = {
|
|
16935
|
+
/** The largest file a group takes. */
|
|
16936
|
+
maxBytes: 104857600,
|
|
16937
|
+
/** A file this size or smaller is fetched without asking, a voice message too (one recorded is well inside it). */
|
|
16938
|
+
autoBytes: 8388608,
|
|
16939
|
+
/** What a device keeps of a group's files it fetched without asking; past it, every file is asked about. */
|
|
16940
|
+
autoBytesPerGroup: 268435456,
|
|
16941
|
+
/** Bytes of group files a device serves to others in a day, when serving is on. */
|
|
16942
|
+
serveBytesPerDay: 1073741824,
|
|
16943
|
+
/** Transfers a holder serves at once, all groups together. */
|
|
16944
|
+
serveAtOnce: 3,
|
|
16945
|
+
/** File announcements a member sends to one group in a minute. */
|
|
16946
|
+
announcePerMinute: 8,
|
|
16947
|
+
/** File announcements of one member of a group fetched by themselves in `announceWindowMs`; past it, kept for a Download. */
|
|
16948
|
+
announceTaken: 30,
|
|
16949
|
+
announceWindowMs: 6e5,
|
|
16950
|
+
/** Holders remembered per file. */
|
|
16951
|
+
holders: 8,
|
|
16952
|
+
/**
|
|
16953
|
+
* Message ids a `group-have` named before their message came (a member back from away hears who holds what as its
|
|
16954
|
+
* catch-up begins), remembered with their holders for when it does.
|
|
16955
|
+
*/
|
|
16956
|
+
heardAhead: 256,
|
|
16957
|
+
/** Message ids in one `group-have`. */
|
|
16958
|
+
haveIds: 32,
|
|
16959
|
+
/**
|
|
16960
|
+
* `group-have` frames a private group's edge takes in `haveWindowMs`: what is to say is gathered for a second, then
|
|
16961
|
+
* goes. A community's, from a member, goes every `communityHaveEveryMs`.
|
|
16962
|
+
*/
|
|
16963
|
+
haveFrames: 10,
|
|
16964
|
+
haveWindowMs: 1e4,
|
|
16965
|
+
communityHaveEveryMs: 6e4,
|
|
16966
|
+
/**
|
|
16967
|
+
* How long a want waits for an offer before the next holder is asked; a transfer that does not move for as long is
|
|
16968
|
+
* left for the next holder too.
|
|
16969
|
+
*/
|
|
16970
|
+
wantWaitMs: 3e4,
|
|
16971
|
+
/**
|
|
16972
|
+
* How long a served transfer with all its bytes confirmed waits for the member's word that its check passed once the
|
|
16973
|
+
* member's next want has overtaken it.
|
|
16974
|
+
*/
|
|
16975
|
+
doneWaitMs: 5e3,
|
|
16976
|
+
/** A file session closes this long after its last transfer ends. */
|
|
16977
|
+
sessionIdleMs: 6e4,
|
|
16978
|
+
/** File sessions opening at once, and open at once, all groups together. */
|
|
16979
|
+
sessionsOpening: 2,
|
|
16980
|
+
sessionsOpen: 4,
|
|
16981
|
+
/** The longest file name and type kept from a description. */
|
|
16982
|
+
nameChars: 255,
|
|
16983
|
+
mimeChars: 255
|
|
16984
|
+
};
|
|
16985
|
+
/**
|
|
16986
|
+
* The frames of WISP 503 § Getting the bytes, on an edge between two apps that announced `GROUP_VERSION_FILES`:
|
|
16987
|
+
* `group-want {g, id}` asks a holder for the file of message `id`; `group-want-no {g, id, why}` is a holder's no;
|
|
16988
|
+
* `group-have {g, ids}` says which files a member keeps (in a community, an application frame `{t, ids}` instead).
|
|
16989
|
+
*/
|
|
16990
|
+
var GROUP_WANT_FRAME = "group-want";
|
|
16991
|
+
var GROUP_WANT_NO_FRAME = "group-want-no";
|
|
16992
|
+
var GROUP_HAVE_FRAME = "group-have";
|
|
16993
|
+
var REFUSALS = [
|
|
16994
|
+
"gone",
|
|
16995
|
+
"busy",
|
|
16996
|
+
"refused"
|
|
16997
|
+
];
|
|
16998
|
+
/** A group message's id, as a private group (`<s>:<e>:<n>`) or a community (`<s>:<e>:<h>:<n>`) names it. */
|
|
16999
|
+
var MESSAGE_ID = /^[a-z0-9]{52}:\d{1,15}(:[A-Za-z0-9_-]{1,64})?:\d{1,15}$/;
|
|
17000
|
+
var isGroupMessageId = (value) => typeof value === "string" && MESSAGE_ID.test(value);
|
|
17001
|
+
function frameOf(value, t, groupId) {
|
|
17002
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
17003
|
+
const f = value;
|
|
17004
|
+
return f.t === t && (groupId === void 0 || f.g === groupId) ? f : null;
|
|
17005
|
+
}
|
|
17006
|
+
/** A `group-want` of group `groupId`: the message whose file is asked for, or null. */
|
|
17007
|
+
function readGroupWant(value, groupId) {
|
|
17008
|
+
const f = frameOf(value, GROUP_WANT_FRAME, groupId);
|
|
17009
|
+
return f && isGroupMessageId(f.id) ? { id: f.id } : null;
|
|
17010
|
+
}
|
|
17011
|
+
/** A holder's `group-want-no` of group `groupId`, or null. A reason this app does not know reads as `busy`. */
|
|
17012
|
+
function readGroupWantNo(value, groupId) {
|
|
17013
|
+
const f = frameOf(value, GROUP_WANT_NO_FRAME, groupId);
|
|
17014
|
+
if (!f || !isGroupMessageId(f.id)) return null;
|
|
17015
|
+
return {
|
|
17016
|
+
id: f.id,
|
|
17017
|
+
why: typeof f.why === "string" && REFUSALS.includes(f.why) ? f.why : "busy"
|
|
17018
|
+
};
|
|
17019
|
+
}
|
|
17020
|
+
/**
|
|
17021
|
+
* The message ids of a `group-have`: of group `groupId` on an edge, or (no `groupId`) a community's application frame.
|
|
17022
|
+
* At most `haveIds` message ids; a frame with more, or anything else, is null.
|
|
17023
|
+
*/
|
|
17024
|
+
function readGroupHave(value, groupId) {
|
|
17025
|
+
const f = frameOf(value, GROUP_HAVE_FRAME, groupId);
|
|
17026
|
+
if (!f || !Array.isArray(f.ids) || !f.ids.length || f.ids.length > GROUP_FILE_LIMITS.haveIds || !f.ids.every(isGroupMessageId)) return null;
|
|
17027
|
+
return [...new Set(f.ids)];
|
|
17028
|
+
}
|
|
17029
|
+
var DIGEST = /^[A-Za-z0-9_-]{43}$/;
|
|
17030
|
+
/**
|
|
17031
|
+
* A description read from the wire, cleaned: the name made a display name (never a path), the type untrusted, the
|
|
17032
|
+
* size within the group limit, the digest the right shape. A malformed voice, video or picture description is dropped
|
|
17033
|
+
* on its own and the file is still a file; anything else malformed is no file at all (null), and the text shows.
|
|
17034
|
+
*/
|
|
17035
|
+
function readGroupFileMeta(value) {
|
|
17036
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
17037
|
+
const v = value;
|
|
17038
|
+
if (typeof v.name !== "string" || !v.name.trim() || [...v.name].length > GROUP_FILE_LIMITS.nameChars) return null;
|
|
17039
|
+
if (typeof v.mime !== "string" || v.mime.length > GROUP_FILE_LIMITS.mimeChars) return null;
|
|
17040
|
+
if (!Number.isSafeInteger(v.size) || v.size < 1 || v.size > GROUP_FILE_LIMITS.maxBytes) return null;
|
|
17041
|
+
if (typeof v.d !== "string" || !DIGEST.test(v.d)) return null;
|
|
17042
|
+
const mime = sanitizeMime(v.mime);
|
|
17043
|
+
const voice = v.voice === void 0 ? void 0 : parseVoiceMeta(v.voice, mime);
|
|
17044
|
+
const video = v.video === void 0 ? void 0 : parseVideoMeta(v.video, mime);
|
|
17045
|
+
const image = v.image === void 0 ? void 0 : parseImageMeta(v.image, mime);
|
|
17046
|
+
return {
|
|
17047
|
+
name: sanitizeFileName(v.name),
|
|
17048
|
+
mime,
|
|
17049
|
+
size: v.size,
|
|
17050
|
+
d: v.d,
|
|
17051
|
+
...voice ? { voice } : {},
|
|
17052
|
+
...video ? { video } : {},
|
|
17053
|
+
...image ? { image } : {}
|
|
17054
|
+
};
|
|
17055
|
+
}
|
|
17056
|
+
/**
|
|
17057
|
+
* The text an announcement carries when its author wrote no caption: what an app without group files shows instead
|
|
17058
|
+
* of the file. English, as wire text is; an app that knows the file shows the file, not this line.
|
|
17059
|
+
*/
|
|
17060
|
+
function groupFileFallback(file) {
|
|
17061
|
+
if (file.voice) return `🎤 Voice message (${formatVoiceDuration(file.voice.duration)}): update Ghostly to hear it`;
|
|
17062
|
+
return `${fileMessageText(file)} (${formatFileSize(file.size)}): update Ghostly to get files in groups`;
|
|
17063
|
+
}
|
|
17064
|
+
/**
|
|
17065
|
+
* Whether a device fetches a file by itself (WISP 503 § Automatic and asked downloads): up to `autoBytes` while what
|
|
17066
|
+
* it took that way from the group stays within `autoBytesPerGroup`. A voice message too: one recorded (15 minutes at
|
|
17067
|
+
* most, about 3.6 MB) always fits, and a bigger file the author called a voice message is asked about like any other.
|
|
17068
|
+
*/
|
|
17069
|
+
function fetchesByItself(file, keptAutomatically, limits = GROUP_FILE_LIMITS) {
|
|
17070
|
+
return file.size <= limits.autoBytes && keptAutomatically + file.size <= limits.autoBytesPerGroup;
|
|
17071
|
+
}
|
|
17072
|
+
//#endregion
|
|
14955
17073
|
//#region ../core/src/pairingProgress.ts
|
|
14956
17074
|
var FIRST_STAGE = {
|
|
14957
17075
|
inviter: "publishing",
|
|
@@ -15393,6 +17511,12 @@ function filesCap(channel) {
|
|
|
15393
17511
|
*/
|
|
15394
17512
|
var FILES_IN_USE_MS = 2e3;
|
|
15395
17513
|
/**
|
|
17514
|
+
* 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
|
|
17515
|
+
* chat): a frame goes only while the app has sent less than this more than the file since. A small frame (a Chess move)
|
|
17516
|
+
* still goes at once; one streaming at its limit gets about half the link, and the file the other half.
|
|
17517
|
+
*/
|
|
17518
|
+
var APP_AHEAD_OF_FILES = 16384;
|
|
17519
|
+
/**
|
|
15396
17520
|
* A `paired-message` frame. The preview (`pv`) is left out when the frame would pass `MAX_PAIRED_MESSAGE_FRAME`
|
|
15397
17521
|
* with it: the text matters, the card does not. A reply (`r`, WISP 401 § Replies) is a few hundred bytes at most
|
|
15398
17522
|
* and always goes, and so does a forwarded message's hop count (`fw`, WISP 401 § Forwards) and a status card (`sc`,
|
|
@@ -15422,6 +17546,16 @@ function pairedMessageFrame(id, ts, m, preview, reply, forwarded, card) {
|
|
|
15422
17546
|
});
|
|
15423
17547
|
return withPreview.length <= 57344 ? withPreview : plain;
|
|
15424
17548
|
}
|
|
17549
|
+
/** A text whose `paired-message` frame would pass what a session takes: it never arrives, and is refused at send. */
|
|
17550
|
+
var PAIRED_FRAME_TOO_LARGE = "Message too long once encoded. Shorten it or send it as a file.";
|
|
17551
|
+
/**
|
|
17552
|
+
* The `paired-message` frame of this text fits a session (`SESSION_FRAME_MAX`, UTF-16 units of the JSON). Within 16 KiB
|
|
17553
|
+
* of UTF-8 a text can still pass it: JSON writes each control character as six (`\u0001`). The preview is left out,
|
|
17554
|
+
* as the frame drops one that does not fit.
|
|
17555
|
+
*/
|
|
17556
|
+
function pairedMessageFits(ts, m, reply, forwarded, card) {
|
|
17557
|
+
return pairedMessageFrame("A".repeat(22), ts, m, void 0, reply, forwarded, card).length <= SESSION_FRAME_MAX;
|
|
17558
|
+
}
|
|
15425
17559
|
/**
|
|
15426
17560
|
* A native descriptor from the contact's capability record against the one known (a session's, or an older record's).
|
|
15427
17561
|
* The record carries only how to dial, never an address: the Iroh endpoint id and the relay it is homed on, the
|
|
@@ -15740,7 +17874,9 @@ var GhostLink = class {
|
|
|
15740
17874
|
});
|
|
15741
17875
|
options.events?.onPairingProgress?.(progress);
|
|
15742
17876
|
}) : null;
|
|
15743
|
-
const invite = options.pairingProgress
|
|
17877
|
+
const invite = !!options.pairingProgress && !!options.pairing && !options.pairing.credentials.peerKey;
|
|
17878
|
+
let unanswered = options.dht?.state?.unanswered !== void 0;
|
|
17879
|
+
const backsOff = invite || unanswered;
|
|
15744
17880
|
this.dht = options.params.profile && options.pairing && options.dht ? new DhtDelivery({
|
|
15745
17881
|
params: options.params,
|
|
15746
17882
|
mode: this.deliveryMode,
|
|
@@ -15750,7 +17886,7 @@ var GhostLink = class {
|
|
|
15750
17886
|
save: options.dht.save,
|
|
15751
17887
|
pollMs: options.dht.pollMs,
|
|
15752
17888
|
firstControlAfterMs: options.dht.firstControlAfterMs,
|
|
15753
|
-
...
|
|
17889
|
+
...backsOff && { inviteMadeAt: options.madeAt ?? Date.now() },
|
|
15754
17890
|
capsRev: options.dht.capsRev,
|
|
15755
17891
|
peerCapsRev: options.dht.peerCapsRev,
|
|
15756
17892
|
peerAcceptsText: options.dht.peerAcceptsText,
|
|
@@ -15809,6 +17945,9 @@ var GhostLink = class {
|
|
|
15809
17945
|
},
|
|
15810
17946
|
changed: (view) => {
|
|
15811
17947
|
options.events?.onDhtDelivery?.(view);
|
|
17948
|
+
const stillUnanswered = this.dht.unanswered;
|
|
17949
|
+
if (unanswered && !stillUnanswered) this.session.lookNow();
|
|
17950
|
+
unanswered = stillUnanswered;
|
|
15812
17951
|
if (view.inviteTaken) this.tracker?.failed("taken", false, INVITE_TAKEN);
|
|
15813
17952
|
else this.tracker?.untaken();
|
|
15814
17953
|
this.streamBlockChanged();
|
|
@@ -15837,7 +17976,7 @@ var GhostLink = class {
|
|
|
15837
17976
|
lastSeenTimestamp: options.lastSeenTimestamp,
|
|
15838
17977
|
pollIntervals: options.pollIntervals,
|
|
15839
17978
|
awaitingSince: options.madeAt,
|
|
15840
|
-
...
|
|
17979
|
+
...backsOff && { invite: () => !options.pairing.credentials.peerKey || !!this.dht?.unanswered },
|
|
15841
17980
|
getServices: options.getServices,
|
|
15842
17981
|
firstPublish: options.firstPublish ?? (this.tracker && options.pairingProgress?.role === "joiner" && options.rtcAvailable !== false || this.resuming ? "after-first-poll" : "at-start"),
|
|
15843
17982
|
events: {
|
|
@@ -17372,9 +19511,30 @@ var GhostLink = class {
|
|
|
17372
19511
|
epoch,
|
|
17373
19512
|
rest
|
|
17374
19513
|
};
|
|
19514
|
+
if (!this.peerTransports.includes("webrtc/1")) {
|
|
19515
|
+
traceLink(this.myPubKeyZ32, "offer-unanswerable", { transports: rest });
|
|
19516
|
+
this.clearRace();
|
|
19517
|
+
this.raceTimer = setTimeout(() => {
|
|
19518
|
+
this.raceTimer = null;
|
|
19519
|
+
this.endUnanswerableOffer(epoch);
|
|
19520
|
+
}, 0);
|
|
19521
|
+
return;
|
|
19522
|
+
}
|
|
17375
19523
|
traceLink(this.myPubKeyZ32, "race-standing", { transports: rest });
|
|
17376
19524
|
this.scheduleRace(epoch, made.at);
|
|
17377
19525
|
}
|
|
19526
|
+
/** The offer ends (`raceStandingOffer`), and with it going idle what is ranked after WebRTC is dialled (`dialAfterRtc`). */
|
|
19527
|
+
endUnanswerableOffer(epoch) {
|
|
19528
|
+
if (epoch !== this.connectionEpoch || this.channel || this.stopped || !this.offerOut || this.afterRtc?.epoch !== epoch) return;
|
|
19529
|
+
if (this.dialing) {
|
|
19530
|
+
this.raceTimer = setTimeout(() => {
|
|
19531
|
+
this.raceTimer = null;
|
|
19532
|
+
this.endUnanswerableOffer(epoch);
|
|
19533
|
+
}, RACE_RETRY_MS);
|
|
19534
|
+
return;
|
|
19535
|
+
}
|
|
19536
|
+
this.dataLink.close();
|
|
19537
|
+
}
|
|
17378
19538
|
/**
|
|
17379
19539
|
* Back after an absence, this side answers an offer the contact made while it was away (it dials whatever its key):
|
|
17380
19540
|
* 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 +19717,13 @@ var GhostLink = class {
|
|
|
17557
19717
|
this.attemptEnded();
|
|
17558
19718
|
this.rejectWaiters(new GhostlyHttpError("unreachable", "Could not connect to the peer"));
|
|
17559
19719
|
}
|
|
19720
|
+
/**
|
|
19721
|
+
* This side does not dial on its own: the contact's app does (the lower key), and none of what makes this side dial
|
|
19722
|
+
* anyway holds (a first pairing's role, a restart, WebRTC set aside). Its own dial, chosen by hand, is not retried.
|
|
19723
|
+
*/
|
|
19724
|
+
awaitsContactDial() {
|
|
19725
|
+
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();
|
|
19726
|
+
}
|
|
17560
19727
|
/** A transport of the attempt under way did not connect. */
|
|
17561
19728
|
attemptFailed(transport, error) {
|
|
17562
19729
|
if (this.isDataLinkOpen) return;
|
|
@@ -17579,7 +19746,7 @@ var GhostLink = class {
|
|
|
17579
19746
|
side: attempt.side,
|
|
17580
19747
|
failed: attempt.failed,
|
|
17581
19748
|
...!attempt.failed.length && reason ? { reason } : {},
|
|
17582
|
-
...attempt.side === "dialled" ? { retryAt: Math.max(Date.now(), this.lastAutoConnectAt + this.dialWait()) } : {}
|
|
19749
|
+
...attempt.side === "dialled" && !this.awaitsContactDial() ? { retryAt: Math.max(Date.now(), this.lastAutoConnectAt + this.dialWait()) } : {}
|
|
17583
19750
|
};
|
|
17584
19751
|
traceLink(this.myPubKeyZ32, "attempt-ended", {
|
|
17585
19752
|
side: attempt.side,
|
|
@@ -17618,9 +19785,11 @@ var GhostLink = class {
|
|
|
17618
19785
|
if (this.pairedPending.size >= 128) this.pairedPending.delete(this.pairedPending.keys().next().value);
|
|
17619
19786
|
const id = stableId ?? toBase64Url(randomBytes(16));
|
|
17620
19787
|
if (!/^[A-Za-z0-9_-]{22}$/.test(id)) return "Invalid message ID";
|
|
19788
|
+
const frame = pairedMessageFrame(id, timestamp, trimmed, preview, reply, forwarded, card);
|
|
19789
|
+
if (frame.length > 61440) return PAIRED_FRAME_TOO_LARGE;
|
|
17621
19790
|
this.pairedPending.set(id, timestamp);
|
|
17622
19791
|
try {
|
|
17623
|
-
this.channel.send(
|
|
19792
|
+
this.channel.send(frame);
|
|
17624
19793
|
return null;
|
|
17625
19794
|
} catch {
|
|
17626
19795
|
this.pairedPending.delete(id);
|
|
@@ -17786,14 +19955,13 @@ var GhostLink = class {
|
|
|
17786
19955
|
if (presence.lastPacketAt === this.departed.packet && Date.now() < this.departed.until) return;
|
|
17787
19956
|
this.departed = null;
|
|
17788
19957
|
}
|
|
17789
|
-
|
|
17790
|
-
if (role === "inviter") {
|
|
19958
|
+
if ((!!this.tracker && !this.tracker.done && this.tracker.progress.stage !== "on-dht" ? this.options.pairingProgress?.role : void 0) === "inviter") {
|
|
17791
19959
|
this.peerSeenAt ||= Date.now();
|
|
17792
19960
|
if (Date.now() - this.peerSeenAt < 2500 && !this.nothingToAwait()) {
|
|
17793
19961
|
this.session.expectPeer();
|
|
17794
19962
|
return;
|
|
17795
19963
|
}
|
|
17796
|
-
} else if (
|
|
19964
|
+
} else if (this.awaitsContactDial()) {
|
|
17797
19965
|
if (Date.now() - presenceSeenAt(presence) < 3e4 && presence.lastPacketAt !== this.offerAwaitedFor && !this.nothingToDialEitherWay()) {
|
|
17798
19966
|
this.offerAwaitedFor = presence.lastPacketAt;
|
|
17799
19967
|
this.session.expectPeer();
|
|
@@ -18339,23 +20507,30 @@ var GhostLink = class {
|
|
|
18339
20507
|
* kept to send later. While the channel already holds a quarter of what it may queue (its send budget, or the high
|
|
18340
20508
|
* water mark of a WebRTC channel), the frame is refused with `too-fast`, so the chat's messages and pings never wait
|
|
18341
20509
|
* 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.
|
|
20510
|
+
* above what files may hold (`filesWritable`): files keep the channel at their own line for the whole transfer. While
|
|
20511
|
+
* the file waits for room, the app gets no further than `APP_AHEAD_OF_FILES` ahead of it, so it cannot refill the
|
|
20512
|
+
* channel each time it drains to files' line and keep the file from moving.
|
|
18343
20513
|
*/
|
|
18344
20514
|
sendAppData(app, data) {
|
|
18345
20515
|
if (!this.apps.open.has(app)) return "not-open";
|
|
18346
20516
|
if (appDataBytes(data) > 32768) return "too-large";
|
|
18347
20517
|
if (!this.channel || !this.isDataLinkOpen) return "offline";
|
|
18348
20518
|
if (!this.supportsApps) return "peer-closed";
|
|
18349
|
-
const
|
|
18350
|
-
|
|
20519
|
+
const use = this.filesUse?.channel === this.channel ? this.filesUse : null;
|
|
20520
|
+
const files = use && (use.waiting > 0 || Date.now() - use.at < FILES_IN_USE_MS) ? use : null;
|
|
20521
|
+
if (use && !files) use.appAhead = 0;
|
|
20522
|
+
if (this.channel.bufferedAmount >= (files ? filesCap(this.channel) : 0) + (this.channel.sendBudget ?? LIMITS.sendHighWaterMark) / 4) return "too-fast";
|
|
20523
|
+
if (files && files.waiting > 0 && files.appAhead >= APP_AHEAD_OF_FILES) return "too-fast";
|
|
18351
20524
|
const refused = this.apps.canSend(app, data);
|
|
18352
20525
|
if (refused) return refused;
|
|
20526
|
+
const frame = JSON.stringify(appDataFrame(app, data));
|
|
18353
20527
|
try {
|
|
18354
|
-
this.channel.send(
|
|
18355
|
-
return null;
|
|
20528
|
+
this.channel.send(frame);
|
|
18356
20529
|
} catch {
|
|
18357
20530
|
return "offline";
|
|
18358
20531
|
}
|
|
20532
|
+
if (files) files.appAhead = Math.min(files.appAhead + frame.length, APP_AHEAD_OF_FILES + APP_DATA_MAX_BYTES);
|
|
20533
|
+
return null;
|
|
18359
20534
|
}
|
|
18360
20535
|
/** What the contact had open is gone (`apps/1` is no longer agreed, or this link stopped): each app hears it now. */
|
|
18361
20536
|
appsEnded() {
|
|
@@ -18405,25 +20580,32 @@ var GhostLink = class {
|
|
|
18405
20580
|
sendFileData(id, offset, data) {
|
|
18406
20581
|
if (!this.supportsLargeFiles || !this.channel) return false;
|
|
18407
20582
|
try {
|
|
18408
|
-
this.
|
|
20583
|
+
const frame = this.supportsBinaryFiles ? encodeFileData(id, offset, data) : JSON.stringify({
|
|
18409
20584
|
t: "pf-data",
|
|
18410
20585
|
id,
|
|
18411
20586
|
offset,
|
|
18412
20587
|
data: toBase64Url(data)
|
|
18413
|
-
})
|
|
18414
|
-
this.
|
|
20588
|
+
});
|
|
20589
|
+
this.channel.send(frame);
|
|
20590
|
+
const use = this.filesUsing(this.channel);
|
|
20591
|
+
use.at = Date.now();
|
|
20592
|
+
use.appAhead = Math.max(0, use.appAhead - (typeof frame === "string" ? frame.length : frame.byteLength));
|
|
18415
20593
|
return true;
|
|
18416
20594
|
} catch {
|
|
18417
20595
|
return false;
|
|
18418
20596
|
}
|
|
18419
20597
|
}
|
|
18420
|
-
/**
|
|
20598
|
+
/**
|
|
20599
|
+
* 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
|
|
20600
|
+
* on it beyond theirs since. An app's data gets room above theirs.
|
|
20601
|
+
*/
|
|
18421
20602
|
filesUse = null;
|
|
18422
20603
|
filesUsing(channel) {
|
|
18423
20604
|
if (this.filesUse?.channel !== channel) this.filesUse = {
|
|
18424
20605
|
channel,
|
|
18425
20606
|
at: 0,
|
|
18426
|
-
waiting: 0
|
|
20607
|
+
waiting: 0,
|
|
20608
|
+
appAhead: 0
|
|
18427
20609
|
};
|
|
18428
20610
|
return this.filesUse;
|
|
18429
20611
|
}
|
|
@@ -20131,6 +22313,10 @@ function parsePayParams(body, destination) {
|
|
|
20131
22313
|
if (body.tag !== "payRequest") throw new Error(`${destination.domain} answered with something other than a pay request${typeof body.tag === "string" ? ` (${reason(body.tag)})` : ""}`);
|
|
20132
22314
|
if (typeof body.callback !== "string") throw new Error("The pay request has no callback");
|
|
20133
22315
|
const callback = acceptUrl(body.callback);
|
|
22316
|
+
if (callback.host !== new URL(destination.url).host) {
|
|
22317
|
+
if (callback.protocol !== "https:") throw new Error("Lightning addresses and LNURLs must use HTTPS");
|
|
22318
|
+
if (isLocalHost(callback.hostname)) throw new Error(`${destination.domain} asks for the invoice at a local address`);
|
|
22319
|
+
}
|
|
20134
22320
|
const { minSendable, maxSendable, metadata, commentAllowed } = body;
|
|
20135
22321
|
if (!Number.isSafeInteger(minSendable) || !Number.isSafeInteger(maxSendable) || minSendable <= 0 || maxSendable < minSendable) throw new Error("The pay request has invalid amount limits");
|
|
20136
22322
|
const minSat = Math.ceil(minSendable / 1e3), maxSat = Math.floor(maxSendable / 1e3);
|
|
@@ -20549,6 +22735,8 @@ function assertTokenUnits(amount) {
|
|
|
20549
22735
|
function assertWholeSats(amount) {
|
|
20550
22736
|
if (!Number.isSafeInteger(amount) || amount <= 0 || amount > 0x775f05a074000) throw new Error("Enter a positive whole amount in sats");
|
|
20551
22737
|
}
|
|
22738
|
+
/** Arkade and Bark addresses are bech32m with `ark` on Bitcoin and `tark` on the test networks (one case only). */
|
|
22739
|
+
var arkAddressPrefix = (network) => network === "bitcoin" ? "ark1" : "tark1";
|
|
20552
22740
|
function validatePaymentTarget(value, now = Date.now()) {
|
|
20553
22741
|
if (!value || typeof value !== "object") throw new Error("Invalid payment target");
|
|
20554
22742
|
const t = value;
|
|
@@ -20587,6 +22775,7 @@ function validatePaymentTarget(value, now = Date.now()) {
|
|
|
20587
22775
|
"regtest"
|
|
20588
22776
|
].includes(t.network)) throw new Error("Unsupported Bark network");
|
|
20589
22777
|
if (typeof t.address !== "string" || !t.address || t.address.length > 4096 || typeof t.provider !== "string" || t.provider.length > 512) throw new Error("Invalid payment destination");
|
|
22778
|
+
if ((t.method === "arkade" || t.method === "bark") && (t.address !== t.address.toLowerCase() && t.address !== t.address.toUpperCase() || !t.address.toLowerCase().startsWith(arkAddressPrefix(t.network)))) throw new Error(`That is not an Ark address on ${t.network === "bitcoin" ? "Bitcoin" : t.network}`);
|
|
20590
22779
|
if (t.method !== "bitcoin" && t.method !== "fedimint" && t.method !== "spark") {
|
|
20591
22780
|
const url = new URL(t.provider);
|
|
20592
22781
|
if (url.username || url.password || url.search || url.hash || url.protocol !== "https:" && !(url.protocol === "http:" && [
|
|
@@ -25620,10 +27809,9 @@ function knockIdentity(link) {
|
|
|
25620
27809
|
function knockRecords(link, knocks) {
|
|
25621
27810
|
const nonce = randomBytes(NONCE_LENGTH);
|
|
25622
27811
|
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
27812
|
return [{
|
|
25625
27813
|
label: KNOCK_LABEL,
|
|
25626
|
-
value: toBase64Url(concatBytes$1(nonce,
|
|
27814
|
+
value: toBase64Url(concatBytes$1(nonce, xchacha20poly1305(knockMaterial(link, "knock key"), nonce, utf8Encode(link.g)).encrypt(body)))
|
|
25627
27815
|
}];
|
|
25628
27816
|
}
|
|
25629
27817
|
/** Knocks that open with the link's key and are well formed; anything else is nobody. */
|
|
@@ -27245,8 +29433,13 @@ var CommunitySession = class {
|
|
|
27245
29433
|
messageIdOf(f) {
|
|
27246
29434
|
return f.s === this.myKey && this.resealedFrom.get(frameKey(f)) || communityMessageId(f.s, f.e, f.h, f.n);
|
|
27247
29435
|
}
|
|
27248
|
-
/**
|
|
27249
|
-
|
|
29436
|
+
/**
|
|
29437
|
+
* Where I am and what I have, for the member at the other end of an edge that just opened. `to`: that member, now
|
|
29438
|
+
* asked: my answer to its own sync asks nothing more for a while. A second sync went, and its answer took the place of
|
|
29439
|
+
* what was left of the first from its start: a member back after 256 messages was handed 288 (2026-10-14).
|
|
29440
|
+
*/
|
|
29441
|
+
syncFrame(to) {
|
|
29442
|
+
if (to) this.asked.set(to, this.hooks.clock?.() ?? Date.now());
|
|
27250
29443
|
const have = {};
|
|
27251
29444
|
const recent = new Set(this.state.store.map((f) => seenKey(f.e, f.h)));
|
|
27252
29445
|
for (const [sender, byKey] of Object.entries(this.state.seen)) for (const [key, entry] of Object.entries(byKey)) {
|
|
@@ -28809,6 +31002,6 @@ function appStoreDecision(held, read) {
|
|
|
28809
31002
|
return read.digest === held.digest ? "same" : "equivocation";
|
|
28810
31003
|
}
|
|
28811
31004
|
//#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 };
|
|
31005
|
+
export { COMMUNITY_EDIT_FRAME as $, cardLine as $a, normalizeRelayUrl as $c, lastSharedWithMe as $i, GROUP_HAVE_FRAME as $n, parseCallSignal as $o, WakeLimiter as $r, ENROLL_GRANT as $s, didDhtFromPublicKey as $t, CommunitySession as A, readForwarded as Aa, newDeviceSetSecret as Ac, nip05RootKey as Ai, pubkyProofFile as An, rosterHas as Ao, pickHubs as Ar, handoffPakeFrame as As, verifyAtprotoRecordProof as At, knockRecords as B, validReactionNumber as Ba, firstDeviceSetSecret as Bc, dialDescriptors as Bi, findEndpoint as Bn, formatLocalTarget as Bo, TYPING_KINDS as Br, readHandoffHello as Bs, didWebDocumentUrl as Bt, sha256Paced as C, readImageMeta as Ca, ENROLL_CAPABILITY as Cc, DOMAIN_FILE_MAX_BYTES as Ci, utf8Decode as Cl, isBitcoinAddress as Cn, readCanonicalJson as Co, doorHubs as Cr, handoffContextProof as Cs, atprotoProofRkey as Ct, openGroupTyping as D, dhtEditId as Da, deviceKeyZ32 as Dc, domainWellKnownFile as Di, newPubkyProofFolder as Dn, commitHash as Do, mergeBeacon as Dr, handoffHelloFrame as Ds, parseAtprotoDidDocument as Dt, groupTypingEpoch as E, RateWindow as Ea, deviceEchoNonce as Ec, domainTxtRecord as Ei, isPubkyProofFolder as En, MEMBER_KEY as Eo, lobbyRecords as Er, handoffEphemeral as Es, normalizeAtprotoHandle as Et, KNOCK_TTL_MS as F, nextReactionNumber as Fa, TURN_SETTLE_MS as Fc, txtValue as Fi, resolveLightningDestination as Fn, DHT_POLL_INTERVALS as Fo, shouldBeHub as Fr, newHandoffId as Fs, checkDidJws as Ft, groupWakeFrame as G, BUTTON_ID as Ga, turnInstance as Gc, IdentityExchange as Gi, INVITE_TAKEN as Gn, encodeControl as Go, GROUP_PIN_FRAME as Gr, SET_ACK as Gs, normalizeDid as Gt, readKnocks as H, pairedReplyAuthor as Ha, readTurnPacket as Hc, IDENTITY_MAX_VALIDITY as Hi, parseCashuRequestPayload as Hn, webLocalFetch as Ho, TypingSender as Hr, readHandoffTurnFrame as Hs, didWebFileNames as Ht, decodeGroupEntryLink as I, queueReaction as Ia, classifyTurnRead as Ic, wellKnownUrl as Ii, decodeBolt11 as In, EXPECT_PEER_MS as Io, ChatFiles as Ir, readHandoffAck as Is, checkDidSignature as It, GROUP_LIMITS as J, PR_STATES as Ja, turnStatedVerifier as Jc, identityRevocationValue as Ji, pairedMessageFits as Jn, answersOffer as Jo, nextPinNumber as Jr, setAckFrame as Js, parseDidSignaturePaste as Jt, groupWakes as K, ITEM_STATES as Ka, turnKeys as Kc, boundVerifiedIdentity as Ki, LIVENESS_PING_MS as Kn, CALL_SIGNAL_MAX_AGE_MS as Ko, PIN_LIMITS as Kr, SET_UPDATE as Ks, parseDid as Kt, encodeGroupEntryLink as L, reactionEmoji as La, turnPutSummary as Lc, CapsExchange as Li, ENDPOINT as Ln, RELAY_POLL_INTERVALS as Lo, FILE_LIMITS as Lr, readHandoffCancel as Ls, didGhostlyServiceEntry as Lt, communityHasFrame as M, GROUP_REACTION_FRAME as Ma, webCryptoSigner as Mc, recordsFromTxt as Mi, homeserverWebEndpoint as Mn, edgeParams as Mo, readBeacon as Mr, handoffRetryAfter as Ms, DID_FILE_MAX_BYTES as Mt, decodeCommunityLink as N, REACTION_LIMITS as Na, TURN_PUT_WINDOW_MS as Nc, recordsFromWellKnown as Ni, pubkyHomeserverOf as Nn, DirectPathWatch as No, readBeaconHead as Nr, handoffStreamKey as Ns, DID_JWS_MAX_LENGTH as Nt, sealGroupTyping as O, parseLinkPreview as Oa, deviceLinkPairing as Oc, encodeDnsQuery as Oi, normalizePubkyKey as On, parseGroupCommit as Oo, mergeLobby as Or, handoffManifestFrames as Os, shortAtprotoDid as Ot, encodeCommunityLink as P, ReactionWindow as Pa, TURN_RAISE_READ_TIMEOUT_MS as Pc, recordsName as Pi, requestLnurlInvoice as Pn, AWAITING_PEER_MS as Po, readLobby as Pr, handoffVersions as Ps, DidError as Pt, frameBytes as Q, buttonLabelClash as Qa, currentRelays as Qc, revokesIdentity as Qi, GROUP_FILE_LIMITS as Qn, extractParamsFromSdp as Qo, WAKE_CALL_INTERVAL_MS as Qr, ENROLL_DONE as Qs, didDhtDocument as Qt, entryParams as R, reactionIsNewer as Ra, TURN_LAST_SEQUENCE as Rc, DHT_TEXT_CAPABILITY as Ri, cashuRequestPayload as Rn, presenceSeenAt as Ro, transferEnded as Rr, readHandoffDone as Rs, didJwsAlg as Rt, readAppBundleAsync as S, parseImageMeta as Sa, DEVICES_CAPABILITY as Sc, readManifest as Si, toZ32 as Sl, sparkInvoiceDetails as Sn, canonicalJsonBytes as So, beaconRecords as Sr, handoffContext as Ss, atprotoProofRecord as St, GroupTypingBoard as T, EDIT_RATE_WINDOW_MS as Ta, deviceEchoFrame as Tc, dnsProofName as Ti, isPubkyKey as Tn, MAX_GROUP_CHAIN as To, lobbyKeys as Tr, handoffDigest as Ts, isAtprotoDid as Tt, GROUP_WAKE_FRAME as U, readReply as Ua, signTurnPacket as Uc, IDENTITY_REVOCATION_LABEL as Ui, parseFedimintRequestPayload as Un, LIMITS as Uo, sanitizeTypingStatus as Ur, readHandoffVerified as Us, didWebFileUrl as Ut, mergeKnocks as V, wireReaction as Va, nextTurnPosition as Vc, IDENTITY_MAX_EVIDENCE as Vi, isFederationId as Vn, parseLocalTarget as Vo, TYPING_REFRESH_MS as Vr, readHandoffPake as Vs, didWebFile as Vt, GroupWakeLimiter as W, replySnippet as Wa, signTurnRelease as Wc, IdentityCheckUnavailable as Wi, GhostLink as Wn, decodeControl as Wo, typingActivity as Wr, sealHandoffPiece as Ws, didWebHost as Wt, GROUP_READ_NOTE as X, STATUS_CARD_LIMITS as Xa, DEFAULT_RELAYS as Xc, newIdentityBinding as Xi, engineError as Xn, callRtcConfig as Xo, pinNumberHolds as Xr, signTombstone as Xs, staticDidDocument as Xt, GROUP_REACTED_FRAME as Y, RUN_RESULTS as Ya, turnVerifierMark as Yc, identityStatement as Yi, pairedMessageFrame as Yn, buildSdpFromSignal as Yo, pinIsNewer as Yr, setUpdateFrame as Ys, signingKeys as Yt, GroupSession as Z, appCardId as Za, RelayTransport as Zc, receivedIdentityStatus as Zi, engineText as Zn, callSignalHeardAt as Zo, readPin as Zr, verifySetUpdate as Zs, strictBase64Url as Zt, assembleAppBundle as _, baseMime as _a, decodeInviteCode as _c, HOLD_LIMITS as _i, sanitizeAvatar as _l, validatePaymentTarget as _n, readAppRevocation as _o, mayBeHub as _r, handoffAttemptAllowed as _s, ATPROTO_DECLARED_SCOPE as _t, planAppUpdate as a, irohDescriptor as aa, enrollDoneFrame as ac, checkPushEndpoint as ai, PacketTooLargeError as al, EVM_TEST_CHAINS as an, claimedTime as ao, readGroupFileMeta as ar, HANDOFF_ACK as as, groupName as at, pacer as b, voicePeaksOf as ba, inviteOwnership as bc, isHoldUrl as bi, randomBytes as bl, SPARK_PROVIDER as bn, signAppRevocation as bo, COMMUNITY_TOPOLOGY as br, handoffBusyFrame as bs, ATPROTO_PROOF_SCOPE as bt, readAppStore as c, DHT_MESSAGE_TTL as ca, enrollProofFrame as cc, chatAppId as ci, newerPacket as cl, SEPOLIA_TEST_USDT as cn, shownTime as co, readGroupWantNo as cr, HANDOFF_HAVE as cs, SshSigError as ct, APP_BUNDLE_LIMITS as d, dhtFileId as da, verifyEnrollProof as dc, BUTTONS_CAPABILITY as di, createIdentity as dl, USDT_PUBLIC_RPC as dn, appFingerprint as do, SIGNAL_FRESH_MS as dr, HANDOFF_PAKE as ds, sameSshKey as dt, PeerProofs as ea, ENROLL_HELLO as ec, deviceWakeRequest as ei, setLinkTraceSink as el, encodeDidDhtDeactivated as en, cardUrl as eo, GROUP_WANT_FRAME as er, signalHasVideo as es, GROUP_EDIT_FRAME as et, APP_BUNDLE_MAGIC as f, formatFileSize as fa, createDeviceInvite as fc, EDIT_CAPABILITY as fi, identityFromSeed as fl, WALLET_NETWORKS as fn, appRef as fo, SIGNAL_HOLD_MS as fr, HANDOFF_PIECE_BYTES as fs, sshSignCommand as ft, appViewOf as g, VOICE_LIMITS as ga, createChatInvite as gc, HOLD_DATED_BACK_MS as gi, MAX_AVATAR_LENGTH as gl, formatPaymentAmount as gn, isAppRef as go, MESH_HUBS as gr, filePartName as gs, decodeAnyBitcoinAddress as gt, appDigest as h, PLAYABLE_AUDIO as ha, OwnInviteError as hc, HOLD_CAPABILITY as hi, verify as hl, assertWholeSats as hn, isAppKey as ho, readGroupSignal as hr, HANDOFF_VERIFIED as hs, BitcoinAddressError as ht, checkAppBeforeRun as i, automaticTransport as ia, enrollDigits as ic, wakeRequest as ii, withRequestOptions as il, ETHEREUM_USDT as in, arrivalKey as io, isGroupMessageId as ir, serviceIdFromName as is, mentionsMember as it, GROUP_READ_NOTE_COMMUNITY as j, COMMUNITY_REACTION_FRAME as ja, seedSigner as jc, normalizeDomain as ji, pubkyProofPath as jn, verifyCommitSignature as jo, rankHubs as jr, handoffReleaseFrame as js, DID_DOCUMENT_MAX_BYTES as jt, COMMUNITY_LIMITS as k, forwardedAgain as ka, devicePingFrame as kc, isPublicAddress as ki, pubkyProofCapability as kn, rosterAdmin as ko, newerHead as kr, handoffOfferFrame as ks, AtprotoProofError as kt, readAppStoreSteps as l, peekDhtMailbox as la, readEnrollGrant as lc, isAppVersion as li, openRelayPayload as ll, SEPOLIA_TEST_USDT_FAUCET as ln, APP_PREFIXES as lo, CarriedTransport as lr, HANDOFF_HELLO as ls, parseSshPublicKey as lt, APP_PERMISSIONS as m, sanitizeMime as ma, readDeviceInvite as mc, UPGRADE_CAPABILITY as mi, publicKeyFromZ32 as ml, assertTokenUnits as mn, isAppHash as mo, groupSignalFrame as mr, HANDOFF_TIMINGS as ms, verifyBitcoinMessage as mt, appStoreDecision as n, validExternalKey as na, enrollCancelFrame as nc, parseWakeFrame as ni, budgetRetryMs as nl, resolveDidDht as nn, sameButtonText as no, fetchesByItself as nr, HTTP_SERVICE_PROTO as ns, carryMentions as nt, APP_STORE_LIMITS as o, irohRelayUrl as oa, enrollGrantFrame as oc, vapidKeysMatch as oi, RELAY_PAYLOAD_MAX_BYTES as ol, ONCHAIN_PROVIDER as on, heardTime as oo, readGroupHave as or, HANDOFF_CANCEL as os, SSHSIG_MAX_ARMOR as ot, APP_ICON_PATH as p, sanitizeFileName as pa, deviceInviteJoinerParams as pc, STATUS_CARD_CAPABILITY as pi, identityFromSeedB64 as pl, arkAddressPrefix as pn, checkAppRevokeStatement as po, SIGNAL_REPLY_MS as pr, HANDOFF_REQUEST as ps, verifySshSignature as pt, parseGroupWakeFrame as q, PR_CHECKS as qa, turnName as qc, emptyIdentityLedger as qi, PAIRED_FRAME_TOO_LARGE as qn, RTC_CONFIG as qo, mayPin as qr, readSetUpdate as qs, parseDidDocument as qt, appUpdateDecision as r, TRANSPORTS as ra, enrollCancelReason as rc, relayRequest as ri, isDiscoveryBudgetError as rl, signDidDhtPacket as rn, statusCardText as ro, groupFileFallback as rr, LEGACY_SERVICES as rs, parseCommunityEdit as rt, checkAppStoreIndex as s, DHT_FILE_TOO_LARGE as sa, enrollHelloFrame as sc, APP_DATA_MAX_BYTES as si, createRelayPayload as sl, PaymentPreflightError as sn, receivedTimestamp as so, readGroupWant as sr, HANDOFF_DONE as ss, SSHSIG_NAMESPACE as st, appRevocationFor as t, emptyProofLedger as ta, ENROLL_PROOF as tc, newWakeToken as ti, traceLink as tl, encodeDidDhtPacket as tn, checkStatusCard as to, GROUP_WANT_NO_FRAME as tr, emptyLinkRecords as ts, GROUP_EDIT_TEXT_BYTES as tt, signAppStore as u, DHT_FILE_CAPABILITY as ua, readEnrollHello as uc, APPS_CAPABILITY as ui, parseRelayPayload as ul, TEST_USDT_FAUCET_AMOUNT as un, APP_STATEMENT_LIMITS as uo, GROUP_SIGNAL_FRAME as ur, HANDOFF_OFFER as us, parseSshSignature as ut, checkAppManifest as v, fileMessageText as va, encodeInviteCode as vc, HoldKeys as vi, bytesEqual as vl, walletNetworkOf as vn, readAppRevocations as vo, meshHubs as vr, handoffAttemptFailed as vs, ATPROTO_FULL_SCOPE as vt, GROUP_TYPING_FRAME as w, EDIT_BUFFER_MS as wa, HANDOFF_CAPABILITY as wc, decodeDnsResponse as wi, utf8Encode as wl, PUBKY_PROOF_MAX_BYTES as wn, GROUP_MEMBER_CAP as wo, freshHubs as wr, handoffDataFrame as ws, checkAtprotoProofRecord as wt, readAppBundle as x, parseVideoMeta as xa, ownInviteRefusal as xc, newMailbox as xi, toBase64Url as xl, sparkAddressKind as xn, canonicalJson as xo, beaconKeys as xr, handoffCancelFrame as xs, atprotoDidMethod as xt, isAppUrl as y, parseVoiceMeta as ya, inviteLink as yc, HoldRefusedError as yi, fromBase64Url as yl, SPARK_NETWORKS as yn, signAppObject as yo, pickMeshHubs as yr, handoffAttemptSucceeded as ys, ATPROTO_PROOF_COLLECTION as yt, knockIdentity as z, readReaction as za, TURN_MAX as zc, capsDescriptors as zi, fedimintRequestPayload as zn, GhostlyHttpError as zo, transferStalled as zr, readHandoffHave as zs, didSignedMessages as zt };
|
|
28813
31006
|
|
|
28814
|
-
//# sourceMappingURL=src-
|
|
31007
|
+
//# sourceMappingURL=src-yMUQvdyL.js.map
|