@ghostlytools/cli 1.1.5 → 1.1.7
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 +1 -0
- package/dist/assets/{endpoint-CaSbSFDw.js → endpoint-D_3bYafG.js} +76 -3
- package/dist/assets/endpoint-D_3bYafG.js.map +1 -0
- package/dist/assets/{hyperdhtRelay-w9cwVqGZ.js → hyperdhtRelay-DvXgebV7.js} +3 -3
- package/dist/assets/{hyperdhtRelay-w9cwVqGZ.js.map → hyperdhtRelay-DvXgebV7.js.map} +1 -1
- package/dist/assets/idb-BIdus0WX.js.map +1 -1
- package/dist/assets/{mainline-BabMhm8L.js → mainline-B7wHcLaW.js} +2 -2
- package/dist/assets/{mainline-BabMhm8L.js.map → mainline-B7wHcLaW.js.map} +1 -1
- package/dist/assets/{server-TamKsfmC.js → server-GJW_5wGI.js} +370 -66
- package/dist/assets/server-GJW_5wGI.js.map +1 -0
- package/dist/assets/{src-C6qQYrAH.js → src-Cp3o_yHN.js} +608 -70
- package/dist/assets/src-Cp3o_yHN.js.map +1 -0
- package/dist/ghostly.mjs +1068 -49
- package/dist/ghostly.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/assets/endpoint-CaSbSFDw.js.map +0 -1
- package/dist/assets/server-TamKsfmC.js.map +0 -1
- package/dist/assets/src-C6qQYrAH.js.map +0 -1
|
@@ -897,6 +897,8 @@ function normalizeRelayUrl(input) {
|
|
|
897
897
|
return null;
|
|
898
898
|
}
|
|
899
899
|
}
|
|
900
|
+
/** A relay the breaker leaves alone for failing (`RelayBreaker`): nothing was asked of it now. */
|
|
901
|
+
var RelayLeftAloneError = class extends Error {};
|
|
900
902
|
var RelayTransport = class {
|
|
901
903
|
relays;
|
|
902
904
|
timeoutMs;
|
|
@@ -1185,8 +1187,11 @@ var RelayTransport = class {
|
|
|
1185
1187
|
return;
|
|
1186
1188
|
}
|
|
1187
1189
|
const held = results.map((r) => r.status === "rejected" && isDiscoveryBudgetError(r.reason) ? r.reason : null);
|
|
1190
|
+
const leftAlone = (r) => r.status === "rejected" && r.reason instanceof RelayLeftAloneError;
|
|
1191
|
+
const waits = held.filter((e) => e !== null);
|
|
1188
1192
|
const reasons = results.map((r) => r.status === "rejected" ? String(r.reason) : "").join("; ");
|
|
1189
|
-
if (
|
|
1193
|
+
if (waits.length === results.length) reject(new DiscoveryBudgetError(Math.min(...waits.map((e) => e.retryInMs)), `Publish held back on every relay: ${reasons}`));
|
|
1194
|
+
else if (waits.length && results.every((r, i) => held[i] !== null || leftAlone(r))) reject(new DiscoveryBudgetError(Math.min(...waits.map((e) => e.retryInMs)), `Publish held back on every relay not left alone: ${reasons}`));
|
|
1190
1195
|
else reject(/* @__PURE__ */ new Error(`Publish failed on every relay: ${reasons}`));
|
|
1191
1196
|
});
|
|
1192
1197
|
});
|
|
@@ -1484,7 +1489,7 @@ var RelayTransport = class {
|
|
|
1484
1489
|
const blocked = probe ? 0 : this.breaker.blockedFor(relay);
|
|
1485
1490
|
if (blocked > 0) {
|
|
1486
1491
|
if (this.breaker.blockedKind(relay) === "throttled") throw new DiscoveryBudgetError(blocked, "Discovery relay is throttling this address; retry shortly");
|
|
1487
|
-
throw new
|
|
1492
|
+
throw new RelayLeftAloneError(`${relay} is left alone after failing; asked again in ${Math.max(1, Math.ceil(this.breaker.askedAgainIn(relay, this.relays) / 1e3))} s`);
|
|
1488
1493
|
}
|
|
1489
1494
|
if (!this.take(relay, who, pubKeyZ32)) throw new DiscoveryBudgetError(this.heldFor(relay, who));
|
|
1490
1495
|
if (probe) this.breaker.beginAllDown(relay);
|
|
@@ -3358,7 +3363,7 @@ var HANDOFF_CANCEL_REASONS = [
|
|
|
3358
3363
|
"stalled"
|
|
3359
3364
|
];
|
|
3360
3365
|
var KEY$3 = /^[A-Za-z0-9_-]{43}$/;
|
|
3361
|
-
var DIGEST$
|
|
3366
|
+
var DIGEST$2 = KEY$3;
|
|
3362
3367
|
var SIGNATURE = /^[A-Za-z0-9_-]{86}$/;
|
|
3363
3368
|
var ID$5 = /^[A-Za-z0-9_-]{22}$/;
|
|
3364
3369
|
var B64$4 = /^[A-Za-z0-9_-]*$/;
|
|
@@ -3374,9 +3379,9 @@ var isHandoffPartName = (name) => typeof name === "string" && PART.test(name);
|
|
|
3374
3379
|
/** The part a file is: `file/<its SHA-256, base64url>`. */
|
|
3375
3380
|
var filePartName = (sha256B64) => `file/${sha256B64}`;
|
|
3376
3381
|
/** The digest a file part names, or null for any other part. */
|
|
3377
|
-
var filePartDigest = (name) => name.startsWith("file/") && DIGEST$
|
|
3382
|
+
var filePartDigest = (name) => name.startsWith("file/") && DIGEST$2.test(name.slice(5)) ? name.slice(5) : null;
|
|
3378
3383
|
function isPart(value) {
|
|
3379
|
-
return Array.isArray(value) && value.length === 3 && isHandoffPartName(value[0]) && isCount$1(value[1], MAX_PART) && isText$2(value[2], DIGEST$
|
|
3384
|
+
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]);
|
|
3380
3385
|
}
|
|
3381
3386
|
/** Names compared as UTF-8 byte strings, as `H` sorts them. */
|
|
3382
3387
|
function compareNames(a, b) {
|
|
@@ -3567,7 +3572,7 @@ function readHandoffPake(frame) {
|
|
|
3567
3572
|
};
|
|
3568
3573
|
}
|
|
3569
3574
|
function readHandoffHave(frame) {
|
|
3570
|
-
if (frame.t !== "handoff-have" || !Array.isArray(frame.d) || frame.d.length > 300 || !frame.d.every((d) => isText$2(d, DIGEST$
|
|
3575
|
+
if (frame.t !== "handoff-have" || !Array.isArray(frame.d) || frame.d.length > 300 || !frame.d.every((d) => isText$2(d, DIGEST$2))) return null;
|
|
3571
3576
|
const p = {};
|
|
3572
3577
|
if (frame.p !== void 0) {
|
|
3573
3578
|
if (!frame.p || typeof frame.p !== "object" || Array.isArray(frame.p) || Object.keys(frame.p).length > 300) return null;
|
|
@@ -3623,7 +3628,7 @@ function readHandoffAck(frame) {
|
|
|
3623
3628
|
}
|
|
3624
3629
|
/** `H` of a `handoff-verified` that B's key signed for this turn address and turn, or null. */
|
|
3625
3630
|
function readHandoffVerified(frame, turnAddress, turn, takerKey) {
|
|
3626
|
-
if (frame.t !== "handoff-verified" || !isText$2(frame.h, DIGEST$
|
|
3631
|
+
if (frame.t !== "handoff-verified" || !isText$2(frame.h, DIGEST$2) || !isText$2(frame.s, SIGNATURE)) return null;
|
|
3627
3632
|
const h = fromBase64Url(frame.h);
|
|
3628
3633
|
try {
|
|
3629
3634
|
return verify(fromBase64Url(frame.s), handoffVerifiedMessage(turnAddress, turn, h), takerKey) ? h : null;
|
|
@@ -7159,7 +7164,13 @@ var STATUS_CARD_LIMITS = {
|
|
|
7159
7164
|
buttonLabel: 40,
|
|
7160
7165
|
/** An app card's title and version, in characters (WISP 405 § An app): the manifest's bounds. */
|
|
7161
7166
|
appTitle: 40,
|
|
7162
|
-
appVersion: 32
|
|
7167
|
+
appVersion: 32,
|
|
7168
|
+
/** A usage card's label ("Claude") and account, in characters (WISP 405 § Usage). */
|
|
7169
|
+
usageLabel: 24,
|
|
7170
|
+
/** A usage window's name ("5 h", "week"), in characters. */
|
|
7171
|
+
usageWindow: 16,
|
|
7172
|
+
/** Windows beside a usage card's own, shown in its details. */
|
|
7173
|
+
usageWindows: 3
|
|
7163
7174
|
};
|
|
7164
7175
|
/** How a button looks: `primary` is the one highlighted, `danger` warns; `neutral` when none is said. */
|
|
7165
7176
|
var BUTTON_STYLES = [
|
|
@@ -7311,6 +7322,52 @@ function readRun(raw, now) {
|
|
|
7311
7322
|
...summary && { summary }
|
|
7312
7323
|
};
|
|
7313
7324
|
}
|
|
7325
|
+
/** A percent as a reader keeps it: a finite number, rounded and clamped into 0 to 100. */
|
|
7326
|
+
var percent = (value) => typeof value === "number" && Number.isFinite(value) ? Math.round(Math.min(100, Math.max(0, value))) : void 0;
|
|
7327
|
+
/** A time ahead of the reader (a reset, a next run) as it keeps it: whole, positive, at most a year ahead. */
|
|
7328
|
+
var ahead = (value, now) => typeof value === "number" && Number.isSafeInteger(value) && value > 0 && value <= now + STATUS_CARD_LIMITS.nextRunMs ? value : void 0;
|
|
7329
|
+
/** A usage card as a reader keeps it (WISP 405 § Usage): dropped without a percent left or a used of limit. */
|
|
7330
|
+
function readUsage(raw, id, now) {
|
|
7331
|
+
const left = percent(raw.left);
|
|
7332
|
+
let used = count(raw.used), limit = count(raw.limit, 1);
|
|
7333
|
+
if (used === void 0 || limit === void 0 || used > limit) used = limit = void 0;
|
|
7334
|
+
if (left === void 0 && limit === void 0) return void 0;
|
|
7335
|
+
const label = cardLine(raw.label, STATUS_CARD_LIMITS.usageLabel), account = cardLine(raw.account, STATUS_CARD_LIMITS.usageLabel);
|
|
7336
|
+
const window = cardLine(raw.window, STATUS_CARD_LIMITS.usageWindow);
|
|
7337
|
+
const resetsAt = ahead(raw.resetsAt, now), updatedAt = time(raw.updatedAt, now + MESSAGE_CLOCK_SKEW_MS);
|
|
7338
|
+
const windows = [];
|
|
7339
|
+
if (Array.isArray(raw.windows)) for (const entry of raw.windows.slice(0, STATUS_CARD_LIMITS.usageWindows)) {
|
|
7340
|
+
if (!isObject$4(entry)) continue;
|
|
7341
|
+
const name = cardLine(entry.window, STATUS_CARD_LIMITS.usageWindow), share = percent(entry.left);
|
|
7342
|
+
if (!name || share === void 0) continue;
|
|
7343
|
+
const at = ahead(entry.resetsAt, now);
|
|
7344
|
+
windows.push({
|
|
7345
|
+
window: name,
|
|
7346
|
+
left: share,
|
|
7347
|
+
...at && { resetsAt: at }
|
|
7348
|
+
});
|
|
7349
|
+
}
|
|
7350
|
+
return {
|
|
7351
|
+
kind: "usage",
|
|
7352
|
+
id,
|
|
7353
|
+
...left !== void 0 && { left },
|
|
7354
|
+
...used !== void 0 && limit !== void 0 && {
|
|
7355
|
+
used,
|
|
7356
|
+
limit
|
|
7357
|
+
},
|
|
7358
|
+
...label && { label },
|
|
7359
|
+
...account && { account },
|
|
7360
|
+
...window && { window },
|
|
7361
|
+
...resetsAt && { resetsAt },
|
|
7362
|
+
...updatedAt && { updatedAt },
|
|
7363
|
+
...windows.length && { windows }
|
|
7364
|
+
};
|
|
7365
|
+
}
|
|
7366
|
+
/** How much of a usage card's quota is left, 0 to 100: its own percent, else worked out from used of limit. */
|
|
7367
|
+
function usageLeft(card) {
|
|
7368
|
+
if (card.left !== void 0) return card.left;
|
|
7369
|
+
return card.used !== void 0 && card.limit ? Math.round((card.limit - card.used) / card.limit * 100) : 0;
|
|
7370
|
+
}
|
|
7314
7371
|
/** The bytes a card takes on the wire: what `STATUS_CARD_LIMITS.bytes` bounds. */
|
|
7315
7372
|
function statusCardBytes(card) {
|
|
7316
7373
|
try {
|
|
@@ -7347,6 +7404,7 @@ function readStatusCard(raw, now = Date.now()) {
|
|
|
7347
7404
|
};
|
|
7348
7405
|
}
|
|
7349
7406
|
const latest = now + MESSAGE_CLOCK_SKEW_MS;
|
|
7407
|
+
if (raw.kind === "usage") return readUsage(raw, id, now);
|
|
7350
7408
|
const links = readLinks(raw.links);
|
|
7351
7409
|
if (raw.kind === "task") {
|
|
7352
7410
|
const title = cardLine(raw.title, STATUS_CARD_LIMITS.title), status = oneOf(TASK_STATUSES, raw.status);
|
|
@@ -7465,7 +7523,7 @@ function checkStatusCard(raw, now = Date.now()) {
|
|
|
7465
7523
|
if (!isObject$4(raw)) return { error: "A card is an object" };
|
|
7466
7524
|
const bytes = statusCardBytes(raw);
|
|
7467
7525
|
if (bytes > STATUS_CARD_LIMITS.bytes) return { error: `The card takes ${bytes} bytes; at most ${STATUS_CARD_LIMITS.bytes}` };
|
|
7468
|
-
if (raw.kind !== "task" && raw.kind !== "routine" && raw.kind !== "buttons" && raw.kind !== "app") return { error: "kind is task, routine, buttons or
|
|
7526
|
+
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" };
|
|
7469
7527
|
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 -` };
|
|
7470
7528
|
const line = (field, value, max, required = false) => {
|
|
7471
7529
|
if (value === void 0 && !required) return null;
|
|
@@ -7486,7 +7544,31 @@ function checkStatusCard(raw, now = Date.now()) {
|
|
|
7486
7544
|
errors.push(url(`links[${i}].url`, link.url), line(`links[${i}].label`, link.label, STATUS_CARD_LIMITS.linkLabel));
|
|
7487
7545
|
}
|
|
7488
7546
|
}
|
|
7489
|
-
|
|
7547
|
+
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`;
|
|
7548
|
+
const reset = (field, value) => value === void 0 || ahead(value, now) !== void 0 ? null : `${field} is a time in milliseconds, within a year`;
|
|
7549
|
+
if (raw.kind === "usage") {
|
|
7550
|
+
if (raw.links !== void 0) errors.push("a usage card takes no links");
|
|
7551
|
+
if (raw.left === void 0 && raw.used === void 0) errors.push("left (a percent) or used and limit");
|
|
7552
|
+
errors.push(share("left", raw.left));
|
|
7553
|
+
if (raw.used === void 0 !== (raw.limit === void 0)) errors.push("used and limit go together");
|
|
7554
|
+
else if (raw.used !== void 0) {
|
|
7555
|
+
errors.push(whole("used", raw.used), whole("limit", raw.limit, 1));
|
|
7556
|
+
if (typeof raw.used === "number" && typeof raw.limit === "number" && raw.used > raw.limit) errors.push("used is at most limit");
|
|
7557
|
+
}
|
|
7558
|
+
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));
|
|
7559
|
+
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");
|
|
7560
|
+
if (raw.windows !== void 0) {
|
|
7561
|
+
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}`);
|
|
7562
|
+
else for (const [i, entry] of raw.windows.entries()) {
|
|
7563
|
+
if (!isObject$4(entry)) {
|
|
7564
|
+
errors.push(`windows[${i}] is an object`);
|
|
7565
|
+
continue;
|
|
7566
|
+
}
|
|
7567
|
+
errors.push(line(`windows[${i}].window`, entry.window, STATUS_CARD_LIMITS.usageWindow, true));
|
|
7568
|
+
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));
|
|
7569
|
+
}
|
|
7570
|
+
}
|
|
7571
|
+
} else if (raw.kind === "app") {
|
|
7490
7572
|
if (raw.links !== void 0) errors.push("an app card takes no links");
|
|
7491
7573
|
if (!isAppRef(raw.ref)) errors.push("ref is <publisher key in z-base32>/<name>");
|
|
7492
7574
|
else if (raw.id !== appCardId(raw.ref)) errors.push(`id is ${appCardId(raw.ref)}, made from ref`);
|
|
@@ -7629,6 +7711,17 @@ function statusCardText(card) {
|
|
|
7629
7711
|
const named = `${card.title}${card.version ? ` ${card.version}` : ""}`;
|
|
7630
7712
|
return [card.opened ? `🧩 Opened ${named} in this chat (Ghostly app)` : `🧩 ${named} (Ghostly app)`, ...card.url ? [card.url] : []].join("\n");
|
|
7631
7713
|
}
|
|
7714
|
+
if (card.kind === "usage") {
|
|
7715
|
+
const left = `${usageLeft(card)}% left${card.window ? ` (${card.window})` : ""}`;
|
|
7716
|
+
return [
|
|
7717
|
+
`📊 ${card.label ?? "Usage"}`,
|
|
7718
|
+
...card.account ? [card.account] : [],
|
|
7719
|
+
left,
|
|
7720
|
+
...card.used !== void 0 && card.limit !== void 0 ? [`${card.used} of ${card.limit} used`] : [],
|
|
7721
|
+
...card.resetsAt ? [`resets ${cardTimeUtc(card.resetsAt)}`] : [],
|
|
7722
|
+
...(card.windows ?? []).map((w) => `${w.window} ${w.left}%`)
|
|
7723
|
+
].join(" · ");
|
|
7724
|
+
}
|
|
7632
7725
|
if (card.kind === "buttons") return `Reply: ${card.buttons.map((b) => b.label).join(" / ")}`;
|
|
7633
7726
|
if (card.kind === "task") {
|
|
7634
7727
|
const progress = taskProgress(card);
|
|
@@ -10171,6 +10264,12 @@ var TransportSwitch = class {
|
|
|
10171
10264
|
preparing = false;
|
|
10172
10265
|
/** The coordinator's dial for the pending plan has started (`dialing`): from then on a newer choice waits for it. */
|
|
10173
10266
|
dialStarted = false;
|
|
10267
|
+
/**
|
|
10268
|
+
* The contact's `paired-policy` says `replaces`: its app reads a `paired-switch-keep` for a plan a newer choice
|
|
10269
|
+
* replaced as that (`supersede`). An app before 1.1.5 reads it as its target missed and says the chat was kept on its
|
|
10270
|
+
* transport; 1.1.5 reads it right but does not say so.
|
|
10271
|
+
*/
|
|
10272
|
+
remoteReplaces = false;
|
|
10174
10273
|
/** This side chose while no session was open: the next one begins with it as a choice. */
|
|
10175
10274
|
apart = false;
|
|
10176
10275
|
constructor(options) {
|
|
@@ -10226,10 +10325,12 @@ var TransportSwitch = class {
|
|
|
10226
10325
|
context: this.context
|
|
10227
10326
|
});
|
|
10228
10327
|
}
|
|
10328
|
+
/** Older apps read the policy's own fields only, and drop `replaces`. */
|
|
10229
10329
|
announce() {
|
|
10230
10330
|
this.send({
|
|
10231
10331
|
t: "paired-policy",
|
|
10232
|
-
policy: this.local()
|
|
10332
|
+
policy: this.local(),
|
|
10333
|
+
replaces: true
|
|
10233
10334
|
});
|
|
10234
10335
|
}
|
|
10235
10336
|
/**
|
|
@@ -10246,6 +10347,7 @@ var TransportSwitch = class {
|
|
|
10246
10347
|
if (!migrated) {
|
|
10247
10348
|
this.failed = "";
|
|
10248
10349
|
this.remote = null;
|
|
10350
|
+
this.remoteReplaces = false;
|
|
10249
10351
|
}
|
|
10250
10352
|
if (this.apart) {
|
|
10251
10353
|
this.intent = Math.max(this.intent, this.lastRemote?.intent ?? 0) + 1;
|
|
@@ -10330,6 +10432,7 @@ var TransportSwitch = class {
|
|
|
10330
10432
|
this.clearPlan();
|
|
10331
10433
|
this.context = "";
|
|
10332
10434
|
this.remote = null;
|
|
10435
|
+
this.remoteReplaces = false;
|
|
10333
10436
|
}
|
|
10334
10437
|
clearPlan() {
|
|
10335
10438
|
if (this.timer) clearTimeout(this.timer);
|
|
@@ -10347,11 +10450,12 @@ var TransportSwitch = class {
|
|
|
10347
10450
|
* move back to WebRTC waits for the WebRTC session it left to retire (`SWITCH_RETIRE_MS`), and nothing has been tried
|
|
10348
10451
|
* yet. The coordinator drops that plan, so the chat does not move there only to move again; the other side is told
|
|
10349
10452
|
* with `paired-switch-keep`, and whatever the newer policies call for is planned next. A plan already dialling goes
|
|
10350
|
-
* on: a choice made then is planned once it ended (`replanIfMoved`).
|
|
10453
|
+
* on: a choice made then is planned once it ended (`replanIfMoved`). So does a plan the contact's app would take
|
|
10454
|
+
* that keep for a target missed (its policy did not say `replaces`): the newer choice is planned once it landed.
|
|
10351
10455
|
*/
|
|
10352
10456
|
supersede() {
|
|
10353
10457
|
const plan = this.plan;
|
|
10354
|
-
if (!plan || !this.preparing || this.dialStarted || !this.context || !this.remote || this.options.key > this.options.peerKey) return;
|
|
10458
|
+
if (!plan || !this.preparing || this.dialStarted || !this.context || !this.remote || this.options.key > this.options.peerKey || !this.remoteReplaces) return;
|
|
10355
10459
|
if (this.choices(this.local(), this.remote)[0] === plan.choices[0]) return;
|
|
10356
10460
|
this.send({
|
|
10357
10461
|
t: "paired-switch-keep",
|
|
@@ -10481,6 +10585,7 @@ var TransportSwitch = class {
|
|
|
10481
10585
|
if (!policy || this.remote && (policy.revision < this.remote.revision || policy.revision === this.remote.revision && JSON.stringify(policy) !== JSON.stringify(this.remote))) return true;
|
|
10482
10586
|
const changed = !this.remote || policy.revision !== this.remote.revision;
|
|
10483
10587
|
this.remote = this.lastRemote = policy;
|
|
10588
|
+
this.remoteReplaces = frame.replaces === true;
|
|
10484
10589
|
this.options.peer(policy);
|
|
10485
10590
|
if (changed && this.plan && !this.preparing) this.clearPlan();
|
|
10486
10591
|
else if (changed) this.supersede();
|
|
@@ -12567,6 +12672,18 @@ function readManifest(meta) {
|
|
|
12567
12672
|
*/
|
|
12568
12673
|
var OFFER_EXTENSIONS = ["ping/1"];
|
|
12569
12674
|
var MAX_HANDSHAKE_BYTES = 4096;
|
|
12675
|
+
/**
|
|
12676
|
+
* Frames received and not handled yet, at most: their count and their size together. Frames are handled one at a time,
|
|
12677
|
+
* each awaited (a group frame is decrypted, checked and stored before the next), and the peer sends at the network's
|
|
12678
|
+
* pace. A member back in a group answers a sync with a burst: a commit per epoch, its kept messages, the ones it hands
|
|
12679
|
+
* on, edits, reactions. With a bound of 64 frames a phone handling them a few milliseconds each failed its session
|
|
12680
|
+
* mid catch-up ("Session receive limit exceeded" then, "Session receive queue full" now, 2026-10-07). The size bound keeps the old worst case in memory
|
|
12681
|
+
* (64 frames of 60 KiB); the count only bounds a flood of tiny frames.
|
|
12682
|
+
*/
|
|
12683
|
+
var SESSION_RECEIVE_PENDING = {
|
|
12684
|
+
frames: 1024,
|
|
12685
|
+
bytes: 3932160
|
|
12686
|
+
};
|
|
12570
12687
|
var KEY = /^[ybndrfg8ejkmcpqxot1uwisza345h769]{52}$/;
|
|
12571
12688
|
var NONCE$1 = /^[A-Za-z0-9_-]{43}$/;
|
|
12572
12689
|
var SIG$2 = /^[A-Za-z0-9_-]{86}$/;
|
|
@@ -12598,6 +12715,11 @@ var PairedSession = class {
|
|
|
12598
12715
|
state = { status: "negotiating" };
|
|
12599
12716
|
/** The peer said nothing more within `authTimeoutMs`: the connection carried nothing, which proves nothing about the peer. */
|
|
12600
12717
|
authTimedOut = false;
|
|
12718
|
+
/**
|
|
12719
|
+
* The peer sent faster than this side handles, past `SESSION_RECEIVE_PENDING`: the session ends, which is no proof
|
|
12720
|
+
* of anything wrong with the peer (a burst of catch-up on a slow device), so the chat dials again.
|
|
12721
|
+
*/
|
|
12722
|
+
overloaded = false;
|
|
12601
12723
|
/** The seed's identity; null when a signer signs. */
|
|
12602
12724
|
identity;
|
|
12603
12725
|
signer;
|
|
@@ -12614,6 +12736,7 @@ var PairedSession = class {
|
|
|
12614
12736
|
confirming = false;
|
|
12615
12737
|
admission = null;
|
|
12616
12738
|
pending = 0;
|
|
12739
|
+
pendingBytes = 0;
|
|
12617
12740
|
queue = Promise.resolve();
|
|
12618
12741
|
timer = null;
|
|
12619
12742
|
constructor(channel, options) {
|
|
@@ -12701,7 +12824,12 @@ var PairedSession = class {
|
|
|
12701
12824
|
if (!(binding ? (binding.transport === "hyperdht/1" ? /^[a-f0-9]{128}$/.test(binding.context) : FINGERPRINT.test(binding.context)) && binding.identities.length === 2 && binding.identities.every((k) => FINGERPRINT.test(k)) && binding.identities[0] !== binding.identities[1] : this.options.fingerprints?.length === 2 && this.options.fingerprints.every((f) => FINGERPRINT.test(f))) || !this.offer.transports.includes(this.transport) || this.options.rendezvousKeys.some((k) => !KEY.test(k))) return this.fail("Invalid connection binding or unavailable transport");
|
|
12702
12825
|
this.channel.onMessage = (data) => {
|
|
12703
12826
|
if (this.stopped) return;
|
|
12704
|
-
|
|
12827
|
+
const size = data.length;
|
|
12828
|
+
if (size > 61440) return this.fail("Session frame too large");
|
|
12829
|
+
if (++this.pending > SESSION_RECEIVE_PENDING.frames || (this.pendingBytes += size) > SESSION_RECEIVE_PENDING.bytes) {
|
|
12830
|
+
this.overloaded = true;
|
|
12831
|
+
return this.fail("Session receive queue full");
|
|
12832
|
+
}
|
|
12705
12833
|
this.queue = this.queue.then(async () => {
|
|
12706
12834
|
if (this.stopped) return;
|
|
12707
12835
|
if (typeof data === "string" && data.startsWith("{\"t\":\"pair-")) {
|
|
@@ -12710,6 +12838,7 @@ var PairedSession = class {
|
|
|
12710
12838
|
} else if (this.state.status === "ready") await this.options.onApplication(data);
|
|
12711
12839
|
}).catch(() => this.fail("Invalid session negotiation")).finally(() => {
|
|
12712
12840
|
this.pending--;
|
|
12841
|
+
this.pendingBytes -= size;
|
|
12713
12842
|
});
|
|
12714
12843
|
};
|
|
12715
12844
|
this.timer = setTimeout(() => {
|
|
@@ -13950,7 +14079,7 @@ var FILE_LIMITS = {
|
|
|
13950
14079
|
sendRetryMs: 250
|
|
13951
14080
|
};
|
|
13952
14081
|
var ID = /^[A-Za-z0-9_-]{8,64}$/;
|
|
13953
|
-
var DIGEST = /^[A-Za-z0-9_-]{43}$/;
|
|
14082
|
+
var DIGEST$1 = /^[A-Za-z0-9_-]{43}$/;
|
|
13954
14083
|
var DATA = /^[A-Za-z0-9_-]+$/;
|
|
13955
14084
|
var FILE_FRAMES = /* @__PURE__ */ new Set([
|
|
13956
14085
|
"pf-offer",
|
|
@@ -14694,7 +14823,7 @@ var ChatFiles = class {
|
|
|
14694
14823
|
return;
|
|
14695
14824
|
}
|
|
14696
14825
|
if (record.state !== "active" || incoming.verifying || !incoming.target) return;
|
|
14697
|
-
if (frame.size !== record.file.size || typeof frame.digest !== "string" || !DIGEST.test(frame.digest)) return;
|
|
14826
|
+
if (frame.size !== record.file.size || typeof frame.digest !== "string" || !DIGEST$1.test(frame.digest)) return;
|
|
14698
14827
|
const digest = frame.digest;
|
|
14699
14828
|
await incoming.writing;
|
|
14700
14829
|
if (transferEnded(record) || record.state !== "active") return;
|
|
@@ -15707,6 +15836,142 @@ var CarriedTransport = class {
|
|
|
15707
15836
|
}
|
|
15708
15837
|
};
|
|
15709
15838
|
//#endregion
|
|
15839
|
+
//#region ../core/src/groupFiles.ts
|
|
15840
|
+
/**
|
|
15841
|
+
* Files and voice messages in groups (WISP 503 · Group Files). A file is announced as a group message carrying its
|
|
15842
|
+
* description; its bytes go over `files/3` between two members, from a member that holds it to one that asks for it.
|
|
15843
|
+
* The limits below are the profile's first numbers, kept in one place so they are easy to change.
|
|
15844
|
+
*/
|
|
15845
|
+
var GROUP_FILE_LIMITS = {
|
|
15846
|
+
/** The largest file a group takes. */
|
|
15847
|
+
maxBytes: 104857600,
|
|
15848
|
+
/** A file this size or smaller is fetched without asking (voice messages always are, up to `VOICE_LIMITS`). */
|
|
15849
|
+
autoBytes: 8388608,
|
|
15850
|
+
/** What a device keeps of a group's files it fetched without asking; past it, every file is asked about. */
|
|
15851
|
+
autoBytesPerGroup: 268435456,
|
|
15852
|
+
/** Bytes of group files a device serves to others in a day, when serving is on. */
|
|
15853
|
+
serveBytesPerDay: 1073741824,
|
|
15854
|
+
/** Transfers a holder serves at once, all groups together. */
|
|
15855
|
+
serveAtOnce: 3,
|
|
15856
|
+
/** File announcements a member sends to one group in a minute. */
|
|
15857
|
+
announcePerMinute: 8,
|
|
15858
|
+
/** File announcements taken from one member of a group in `announceWindowMs`; past it, shown as text, unfetched. */
|
|
15859
|
+
announceTaken: 30,
|
|
15860
|
+
announceWindowMs: 6e5,
|
|
15861
|
+
/** Holders remembered per file. */
|
|
15862
|
+
holders: 8,
|
|
15863
|
+
/**
|
|
15864
|
+
* Message ids a `group-have` named before their message came (a member back from away hears who holds what as its
|
|
15865
|
+
* catch-up begins), remembered with their holders for when it does.
|
|
15866
|
+
*/
|
|
15867
|
+
heardAhead: 256,
|
|
15868
|
+
/** Message ids in one `group-have`. */
|
|
15869
|
+
haveIds: 32,
|
|
15870
|
+
/** A private group's `group-have` goes on an edge at most this often; a community's, from a member, every `communityHaveEveryMs`. */
|
|
15871
|
+
haveEveryMs: 1e4,
|
|
15872
|
+
communityHaveEveryMs: 6e4,
|
|
15873
|
+
/**
|
|
15874
|
+
* How long a want waits for an offer before the next holder is asked; a transfer that does not move for as long is
|
|
15875
|
+
* left for the next holder too.
|
|
15876
|
+
*/
|
|
15877
|
+
wantWaitMs: 3e4,
|
|
15878
|
+
/** A file session closes this long after its last transfer ends. */
|
|
15879
|
+
sessionIdleMs: 6e4,
|
|
15880
|
+
/** File sessions opening at once, and open at once, all groups together. */
|
|
15881
|
+
sessionsOpening: 2,
|
|
15882
|
+
sessionsOpen: 4,
|
|
15883
|
+
/** The longest file name and type kept from a description. */
|
|
15884
|
+
nameChars: 255,
|
|
15885
|
+
mimeChars: 255
|
|
15886
|
+
};
|
|
15887
|
+
/**
|
|
15888
|
+
* The frames of WISP 503 § Getting the bytes, on an edge between two apps that announced `GROUP_VERSION_FILES`:
|
|
15889
|
+
* `group-want {g, id}` asks a holder for the file of message `id`; `group-want-no {g, id, why}` is a holder's no;
|
|
15890
|
+
* `group-have {g, ids}` says which files a member keeps (in a community, an application frame `{t, ids}` instead).
|
|
15891
|
+
*/
|
|
15892
|
+
var GROUP_WANT_FRAME = "group-want";
|
|
15893
|
+
var GROUP_WANT_NO_FRAME = "group-want-no";
|
|
15894
|
+
var GROUP_HAVE_FRAME = "group-have";
|
|
15895
|
+
var REFUSALS = [
|
|
15896
|
+
"gone",
|
|
15897
|
+
"busy",
|
|
15898
|
+
"refused"
|
|
15899
|
+
];
|
|
15900
|
+
/** A group message's id, as a private group (`<s>:<e>:<n>`) or a community (`<s>:<e>:<h>:<n>`) names it. */
|
|
15901
|
+
var MESSAGE_ID = /^[a-z0-9]{52}:\d{1,15}(:[A-Za-z0-9_-]{1,64})?:\d{1,15}$/;
|
|
15902
|
+
var isGroupMessageId = (value) => typeof value === "string" && MESSAGE_ID.test(value);
|
|
15903
|
+
function frameOf(value, t, groupId) {
|
|
15904
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
15905
|
+
const f = value;
|
|
15906
|
+
return f.t === t && (groupId === void 0 || f.g === groupId) ? f : null;
|
|
15907
|
+
}
|
|
15908
|
+
/** A `group-want` of group `groupId`: the message whose file is asked for, or null. */
|
|
15909
|
+
function readGroupWant(value, groupId) {
|
|
15910
|
+
const f = frameOf(value, GROUP_WANT_FRAME, groupId);
|
|
15911
|
+
return f && isGroupMessageId(f.id) ? { id: f.id } : null;
|
|
15912
|
+
}
|
|
15913
|
+
/** A holder's `group-want-no` of group `groupId`, or null. A reason this app does not know reads as `busy`. */
|
|
15914
|
+
function readGroupWantNo(value, groupId) {
|
|
15915
|
+
const f = frameOf(value, GROUP_WANT_NO_FRAME, groupId);
|
|
15916
|
+
if (!f || !isGroupMessageId(f.id)) return null;
|
|
15917
|
+
return {
|
|
15918
|
+
id: f.id,
|
|
15919
|
+
why: typeof f.why === "string" && REFUSALS.includes(f.why) ? f.why : "busy"
|
|
15920
|
+
};
|
|
15921
|
+
}
|
|
15922
|
+
/**
|
|
15923
|
+
* The message ids of a `group-have`: of group `groupId` on an edge, or (no `groupId`) a community's application frame.
|
|
15924
|
+
* At most `haveIds` message ids; a frame with more, or anything else, is null.
|
|
15925
|
+
*/
|
|
15926
|
+
function readGroupHave(value, groupId) {
|
|
15927
|
+
const f = frameOf(value, GROUP_HAVE_FRAME, groupId);
|
|
15928
|
+
if (!f || !Array.isArray(f.ids) || !f.ids.length || f.ids.length > GROUP_FILE_LIMITS.haveIds || !f.ids.every(isGroupMessageId)) return null;
|
|
15929
|
+
return [...new Set(f.ids)];
|
|
15930
|
+
}
|
|
15931
|
+
var DIGEST = /^[A-Za-z0-9_-]{43}$/;
|
|
15932
|
+
/**
|
|
15933
|
+
* A description read from the wire, cleaned: the name made a display name (never a path), the type untrusted, the
|
|
15934
|
+
* size within the group limit, the digest the right shape. A malformed voice, video or picture description is dropped
|
|
15935
|
+
* on its own and the file is still a file; anything else malformed is no file at all (null), and the text shows.
|
|
15936
|
+
*/
|
|
15937
|
+
function readGroupFileMeta(value) {
|
|
15938
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
15939
|
+
const v = value;
|
|
15940
|
+
if (typeof v.name !== "string" || !v.name.trim() || [...v.name].length > GROUP_FILE_LIMITS.nameChars) return null;
|
|
15941
|
+
if (typeof v.mime !== "string" || v.mime.length > GROUP_FILE_LIMITS.mimeChars) return null;
|
|
15942
|
+
if (!Number.isSafeInteger(v.size) || v.size < 1 || v.size > GROUP_FILE_LIMITS.maxBytes) return null;
|
|
15943
|
+
if (typeof v.d !== "string" || !DIGEST.test(v.d)) return null;
|
|
15944
|
+
const mime = sanitizeMime(v.mime);
|
|
15945
|
+
const voice = v.voice === void 0 ? void 0 : parseVoiceMeta(v.voice, mime);
|
|
15946
|
+
const video = v.video === void 0 ? void 0 : parseVideoMeta(v.video, mime);
|
|
15947
|
+
const image = v.image === void 0 ? void 0 : parseImageMeta(v.image, mime);
|
|
15948
|
+
return {
|
|
15949
|
+
name: sanitizeFileName(v.name),
|
|
15950
|
+
mime,
|
|
15951
|
+
size: v.size,
|
|
15952
|
+
d: v.d,
|
|
15953
|
+
...voice ? { voice } : {},
|
|
15954
|
+
...video ? { video } : {},
|
|
15955
|
+
...image ? { image } : {}
|
|
15956
|
+
};
|
|
15957
|
+
}
|
|
15958
|
+
/**
|
|
15959
|
+
* The text an announcement carries when its author wrote no caption: what an app without group files shows instead
|
|
15960
|
+
* of the file. English, as wire text is; an app that knows the file shows the file, not this line.
|
|
15961
|
+
*/
|
|
15962
|
+
function groupFileFallback(file) {
|
|
15963
|
+
if (file.voice) return `🎤 Voice message (${formatVoiceDuration(file.voice.duration)}): update Ghostly to hear it`;
|
|
15964
|
+
return `${fileMessageText(file)} (${formatFileSize(file.size)}): update Ghostly to get files in groups`;
|
|
15965
|
+
}
|
|
15966
|
+
/**
|
|
15967
|
+
* Whether a device fetches a file by itself (WISP 503 § Automatic and asked downloads): a voice message always, any
|
|
15968
|
+
* other file up to `autoBytes` while what it took that way from the group stays within `autoBytesPerGroup`.
|
|
15969
|
+
*/
|
|
15970
|
+
function fetchesByItself(file, keptAutomatically, limits = GROUP_FILE_LIMITS) {
|
|
15971
|
+
if (file.voice) return true;
|
|
15972
|
+
return file.size <= limits.autoBytes && keptAutomatically + file.size <= limits.autoBytesPerGroup;
|
|
15973
|
+
}
|
|
15974
|
+
//#endregion
|
|
15710
15975
|
//#region ../core/src/pairingProgress.ts
|
|
15711
15976
|
var FIRST_STAGE = {
|
|
15712
15977
|
inviter: "publishing",
|
|
@@ -15993,6 +16258,8 @@ var QUIET_FRAMES = /* @__PURE__ */ new Set([
|
|
|
15993
16258
|
* `disconnected`) otherwise went on until its consent checks gave up, 27 s after the crash (CI run 36741701666).
|
|
15994
16259
|
*/
|
|
15995
16260
|
var PONG_WAIT_MS = 4e3;
|
|
16261
|
+
/** How long `handled` waits for the contact to answer its ping before it says it cannot tell. */
|
|
16262
|
+
var HANDLED_WAIT_MS = 1e4;
|
|
15996
16263
|
/**
|
|
15997
16264
|
* Back in front after an absence (`wake`), a live session that ends this soon after it was lost while the app was
|
|
15998
16265
|
* away (a phone that put it to sleep): the chat resumes as after a restart, not as a session the contact closed.
|
|
@@ -16182,6 +16449,16 @@ var GhostLink = class {
|
|
|
16182
16449
|
/** The contact's transport policy as last seen on this session, to tell its explicit choices apart. */
|
|
16183
16450
|
peerPolicySeen = null;
|
|
16184
16451
|
unansweredPings = 0;
|
|
16452
|
+
/**
|
|
16453
|
+
* Pings sent on this session and pongs heard on it, counted from its start: the contact answers each ping in the order
|
|
16454
|
+
* frames come, so the pong to ping n says it handled everything sent before it (`handled`).
|
|
16455
|
+
*/
|
|
16456
|
+
pingsSent = 0;
|
|
16457
|
+
pongsHeard = 0;
|
|
16458
|
+
/** The liveness ping whose pong measures the round trip: its number in `pingsSent`. */
|
|
16459
|
+
rttPing = 0;
|
|
16460
|
+
/** `handled` callers, each waiting for the pong to its ping. */
|
|
16461
|
+
handledWaiters = [];
|
|
16185
16462
|
/** The last ping's `PONG_WAIT_MS`: running until something comes back from the contact. */
|
|
16186
16463
|
pongWait = null;
|
|
16187
16464
|
/** The last ping's wait ran out with nothing back, and nothing came since (`restartedRedial`). */
|
|
@@ -16814,6 +17091,13 @@ var GhostLink = class {
|
|
|
16814
17091
|
unproven(channel) {
|
|
16815
17092
|
return this.dialedIn.has(channel) && !!this.options.pairing?.credentials.peerKey;
|
|
16816
17093
|
}
|
|
17094
|
+
/**
|
|
17095
|
+
* A session that started before the pin and was refused for another key: the pin came by another path meanwhile (the
|
|
17096
|
+
* DHT's first contact), and whoever this session reached held only a copy of the invite. It proves nothing either.
|
|
17097
|
+
*/
|
|
17098
|
+
pinnedSince(channel, paired) {
|
|
17099
|
+
return this.openedUnpinned.has(channel) && !!paired.state.keyMismatch;
|
|
17100
|
+
}
|
|
16817
17101
|
get textDelivery() {
|
|
16818
17102
|
if (this.isDataLinkOpen) return "stream";
|
|
16819
17103
|
if (!this.dht || this.securityRejected || this.keyStopped) return "unavailable";
|
|
@@ -17904,6 +18188,16 @@ var GhostLink = class {
|
|
|
17904
18188
|
dialsPastRtc() {
|
|
17905
18189
|
return (this.demotedUntil.get("webrtc/1") ?? 0) > Date.now() && this.fallback && this.peerFallback && TRANSPORTS.some((t) => this.canDial(t) && !!this.peerTransports?.includes(t) && this.transportOffer().includes(t));
|
|
17906
18190
|
}
|
|
18191
|
+
/**
|
|
18192
|
+
* A first pairing's inviter has no offer of the joiner's to wait for: WebRTC is missing here, or the joiner's record
|
|
18193
|
+
* says its app has none (a Linux Desktop's WebKitGTK), so its first packet carries none. A native transport the record
|
|
18194
|
+
* describes is dialled now. Waiting out `INVITER_DIAL_GRACE_MS` left two Linux Desktops 2.5 s more apart, and the
|
|
18195
|
+
* joiner, still reading, often went on without dialling for seconds (dht-direct on CI, 2026-10-07). Should the joiner
|
|
18196
|
+
* dial too, the two dials cross and the lower key's carries the chat (`parkCrossed`, `attachReplacement`).
|
|
18197
|
+
*/
|
|
18198
|
+
nothingToAwait() {
|
|
18199
|
+
return !(this.availableTransports.includes("webrtc/1") && (!this.peerTransports || this.peerTransports.includes("webrtc/1"))) && TRANSPORTS.some((t) => this.canDial(t) && !!this.peerTransports?.includes(t) && this.transportOffer().includes(t));
|
|
18200
|
+
}
|
|
17907
18201
|
/** Whether a native dial of `transport` can go now: this side's endpoint is up and the contact's descriptor known. */
|
|
17908
18202
|
canDial(transport) {
|
|
17909
18203
|
return transport !== "webrtc/1" && this.endpoints.has(transport) && !!this.peerDescriptors[transport];
|
|
@@ -18187,8 +18481,8 @@ var GhostLink = class {
|
|
|
18187
18481
|
* Only the lower key offers, so two peers coming online together do not collide. A first pairing is
|
|
18188
18482
|
* the exception: the joiner offers, whatever its key, in the very packet that says it is here (one hop
|
|
18189
18483
|
* less than presence, then the inviter's offer), and the inviter waits for that offer. An inviter that
|
|
18190
|
-
* sees the joiner but no offer for `INVITER_DIAL_GRACE_MS` (an app from before this rule) offers itself
|
|
18191
|
-
* the lower-key rule settles any collision, on both sides.
|
|
18484
|
+
* sees the joiner but no offer for `INVITER_DIAL_GRACE_MS` (an app from before this rule) offers itself,
|
|
18485
|
+
* and dials at once when no offer can come (`nothingToAwait`); the lower-key rule settles any collision, on both sides.
|
|
18192
18486
|
*/
|
|
18193
18487
|
maybeAutoConnect(presence) {
|
|
18194
18488
|
if (this.stopped || this.leaving || this.yielding || this.streamBlocked || this.keyStopped || !this.options.autoConnect || !presence.online || this.channel || this.dialing || this.dataLink.state !== "idle") return;
|
|
@@ -18200,7 +18494,7 @@ var GhostLink = class {
|
|
|
18200
18494
|
const role = !!this.tracker && !this.tracker.done && this.tracker.progress.stage !== "on-dht" ? this.options.pairingProgress?.role : void 0;
|
|
18201
18495
|
if (role === "inviter") {
|
|
18202
18496
|
this.peerSeenAt ||= Date.now();
|
|
18203
|
-
if (Date.now() - this.peerSeenAt < 2500) {
|
|
18497
|
+
if (Date.now() - this.peerSeenAt < 2500 && !this.nothingToAwait()) {
|
|
18204
18498
|
this.session.expectPeer();
|
|
18205
18499
|
return;
|
|
18206
18500
|
}
|
|
@@ -18877,7 +19171,8 @@ var GhostLink = class {
|
|
|
18877
19171
|
2,
|
|
18878
19172
|
3,
|
|
18879
19173
|
4,
|
|
18880
|
-
5
|
|
19174
|
+
5,
|
|
19175
|
+
6
|
|
18881
19176
|
]
|
|
18882
19177
|
}));
|
|
18883
19178
|
} catch {}
|
|
@@ -19098,6 +19393,8 @@ var GhostLink = class {
|
|
|
19098
19393
|
* no more than on the DHT, so it closes that connection and nothing else.
|
|
19099
19394
|
*/
|
|
19100
19395
|
dialedIn = /* @__PURE__ */ new WeakSet();
|
|
19396
|
+
/** Connections whose session started with no key pinned nor named by the invite. */
|
|
19397
|
+
openedUnpinned = /* @__PURE__ */ new WeakSet();
|
|
19101
19398
|
/**
|
|
19102
19399
|
* `migration`: the channel is a candidate that takes over from the session open now once it authenticated. With a
|
|
19103
19400
|
* `plan`, a live switch both sides agreed; without one, a replacement (`attachReplacement`): the contact dialled
|
|
@@ -19126,6 +19423,7 @@ var GhostLink = class {
|
|
|
19126
19423
|
}
|
|
19127
19424
|
};
|
|
19128
19425
|
const unproven = this.unproven(channel);
|
|
19426
|
+
if (!this.options.pairing.credentials.peerKey && !this.options.pairing.credentials.expectedPeerKey) this.openedUnpinned.add(channel);
|
|
19129
19427
|
if (!migration) {
|
|
19130
19428
|
this.activeBinding = binding;
|
|
19131
19429
|
this.channel = channel;
|
|
@@ -19153,13 +19451,15 @@ var GhostLink = class {
|
|
|
19153
19451
|
lightningPaymentsSupport: this.paymentEnabled("lightning"),
|
|
19154
19452
|
paymentsSupport: PAYMENT_METHODS.some((m) => this.paymentEnabled(m)) && !!this.options.events?.onPayment && !!this.options.events?.onPaymentRequest && !!this.options.events?.onPaymentResult,
|
|
19155
19453
|
onState: () => {
|
|
19156
|
-
if (this.channel === channel && (!this.unproven(channel) || paired.state.status === "ready")) this.emitPairingState();
|
|
19454
|
+
if (this.channel === channel && (!this.unproven(channel) && !this.pinnedSince(channel, paired) || paired.state.status === "ready")) this.emitPairingState();
|
|
19157
19455
|
},
|
|
19158
19456
|
onFailure: () => {
|
|
19159
19457
|
if (paired.state.keyMismatch) this.options.events?.onPeerKeyRefused?.();
|
|
19160
|
-
if (this.unproven(channel) || paired.authTimedOut) {
|
|
19458
|
+
if (this.unproven(channel) || this.pinnedSince(channel, paired) || paired.authTimedOut || paired.overloaded) {
|
|
19161
19459
|
if (paired.state.keyMismatch) this.dht?.foreignKeySeen("stream");
|
|
19162
19460
|
if (this.unproven(channel)) traceLink(this.myPubKeyZ32, "dialed-in-refused", { keyMismatch: !!paired.state.keyMismatch });
|
|
19461
|
+
else if (this.pinnedSince(channel, paired)) traceLink(this.myPubKeyZ32, "pinned-since-refused", { transport: binding?.transport ?? "webrtc/1" });
|
|
19462
|
+
else if (paired.overloaded) traceLink(this.myPubKeyZ32, "receive-overload", { transport: binding?.transport ?? "webrtc/1" });
|
|
19163
19463
|
else traceLink(this.myPubKeyZ32, "auth-timeout", { transport: binding?.transport ?? "webrtc/1" });
|
|
19164
19464
|
migration?.reject(new Error(paired.state.error ?? "Candidate authentication failed"));
|
|
19165
19465
|
channel.close();
|
|
@@ -19296,11 +19596,13 @@ var GhostLink = class {
|
|
|
19296
19596
|
}
|
|
19297
19597
|
if (frame?.t === "paired-pong") {
|
|
19298
19598
|
this.peerAnswersPings = true;
|
|
19299
|
-
|
|
19599
|
+
const answered = ++this.pongsHeard;
|
|
19600
|
+
if (this.pingSentAt && answered >= this.rttPing) {
|
|
19300
19601
|
this.rtt = Date.now() - this.pingSentAt;
|
|
19301
19602
|
this.pingSentAt = 0;
|
|
19302
19603
|
this.options.events?.onRtt?.(this.rtt);
|
|
19303
19604
|
}
|
|
19605
|
+
for (const waiter of this.handledWaiters.filter((w) => w.ping <= answered)) waiter.done(true);
|
|
19304
19606
|
return;
|
|
19305
19607
|
}
|
|
19306
19608
|
if (frame?.t === "paired-bye") {
|
|
@@ -19623,8 +19925,7 @@ var GhostLink = class {
|
|
|
19623
19925
|
this.stopLiveness();
|
|
19624
19926
|
this.peerAnswersPings = peerAnswersPings;
|
|
19625
19927
|
const ping = () => {
|
|
19626
|
-
this.
|
|
19627
|
-
channel.send(JSON.stringify({ t: "paired-ping" }));
|
|
19928
|
+
this.sendPing(channel, true);
|
|
19628
19929
|
if (!this.peerAnswersPings || this.pongWait) return;
|
|
19629
19930
|
this.pongWait = setTimeout(() => {
|
|
19630
19931
|
this.pongWait = null;
|
|
@@ -19662,6 +19963,43 @@ var GhostLink = class {
|
|
|
19662
19963
|
this.peerAnswersPings = false;
|
|
19663
19964
|
this.pingSentAt = 0;
|
|
19664
19965
|
this.rtt = void 0;
|
|
19966
|
+
this.pingsSent = this.pongsHeard = this.rttPing = 0;
|
|
19967
|
+
for (const waiter of [...this.handledWaiters]) waiter.done(false);
|
|
19968
|
+
}
|
|
19969
|
+
/** `live`: a liveness ping, whose pong measures the round trip. */
|
|
19970
|
+
sendPing(channel, live) {
|
|
19971
|
+
if (live) this.pingSentAt = Date.now();
|
|
19972
|
+
channel.send(JSON.stringify({ t: "paired-ping" }));
|
|
19973
|
+
this.pingsSent++;
|
|
19974
|
+
if (live) this.rttPing = this.pingsSent;
|
|
19975
|
+
}
|
|
19976
|
+
/**
|
|
19977
|
+
* Resolves once the contact's app handled every frame sent on this session so far: it answers a ping in the order
|
|
19978
|
+
* frames come (every app since 1.0), so the pong to one sent now says so, however slowly it handles them. A group
|
|
19979
|
+
* member's catch-up answer goes a slice at a time on it (`GroupSessionHooks.handled`). `false` when it cannot tell:
|
|
19980
|
+
* no session, a contact that answers no ping, or no answer within `HANDLED_WAIT_MS`.
|
|
19981
|
+
*/
|
|
19982
|
+
handled() {
|
|
19983
|
+
const channel = this.channel;
|
|
19984
|
+
if (!channel || !this.peerAnswersPings || this.paired?.state.status !== "ready") return Promise.resolve(false);
|
|
19985
|
+
try {
|
|
19986
|
+
this.sendPing(channel, false);
|
|
19987
|
+
} catch {
|
|
19988
|
+
return Promise.resolve(false);
|
|
19989
|
+
}
|
|
19990
|
+
return new Promise((resolve) => {
|
|
19991
|
+
const waiter = {
|
|
19992
|
+
ping: this.pingsSent,
|
|
19993
|
+
done: (exact) => {
|
|
19994
|
+
clearTimeout(timer);
|
|
19995
|
+
const at = this.handledWaiters.indexOf(waiter);
|
|
19996
|
+
if (at >= 0) this.handledWaiters.splice(at, 1);
|
|
19997
|
+
resolve(exact);
|
|
19998
|
+
}
|
|
19999
|
+
};
|
|
20000
|
+
const timer = setTimeout(() => waiter.done(false), HANDLED_WAIT_MS);
|
|
20001
|
+
this.handledWaiters.push(waiter);
|
|
20002
|
+
});
|
|
19665
20003
|
}
|
|
19666
20004
|
/** Something came back on the session: the ping's wait is over. */
|
|
19667
20005
|
heardFromPeer() {
|
|
@@ -19824,8 +20162,7 @@ var GhostLink = class {
|
|
|
19824
20162
|
if (!this.peerAnswersPings || this.paired?.state.status !== "ready") return;
|
|
19825
20163
|
this.unansweredPings++;
|
|
19826
20164
|
try {
|
|
19827
|
-
this.
|
|
19828
|
-
channel.send(JSON.stringify({ t: "paired-ping" }));
|
|
20165
|
+
this.sendPing(channel, true);
|
|
19829
20166
|
} catch {
|
|
19830
20167
|
this.dropDeadSession(channel);
|
|
19831
20168
|
return;
|
|
@@ -24036,6 +24373,64 @@ function indexOf(points, words, from) {
|
|
|
24036
24373
|
}
|
|
24037
24374
|
return -1;
|
|
24038
24375
|
}
|
|
24376
|
+
/**
|
|
24377
|
+
* Catch-up answers, a slice at a time (`CATCH_UP_SLICE`), with two slices at most not handled yet by the member's app
|
|
24378
|
+
* (`hooks.handled`): the next once it handled the one before the last, so the slices follow each other without a round
|
|
24379
|
+
* trip between them. All at once, a member back in a busy group was handed over a hundred frames on one session, and an
|
|
24380
|
+
* app that holds 64 waiting ended that session mid catch-up ("Session receive limit exceeded", 2026-10-07). The frames
|
|
24381
|
+
* and their order are what they were: only when they go changes.
|
|
24382
|
+
*/
|
|
24383
|
+
var CatchUpAnswers = class {
|
|
24384
|
+
hooks;
|
|
24385
|
+
/** Answers still going out, per member: what is left of each. */
|
|
24386
|
+
going = /* @__PURE__ */ new Map();
|
|
24387
|
+
constructor(hooks) {
|
|
24388
|
+
this.hooks = hooks;
|
|
24389
|
+
}
|
|
24390
|
+
/**
|
|
24391
|
+
* The first two slices go now, the rest without holding the group's other frames back. A newer answer to the same
|
|
24392
|
+
* member takes the place of what is left of the last; a frame the edge did not take ends it (the member asks again
|
|
24393
|
+
* when its edge opens).
|
|
24394
|
+
*/
|
|
24395
|
+
handOut(to, frames) {
|
|
24396
|
+
const handled = this.hooks.handled;
|
|
24397
|
+
if (!handled) {
|
|
24398
|
+
for (const frame of frames) this.hooks.send(to, frame);
|
|
24399
|
+
return;
|
|
24400
|
+
}
|
|
24401
|
+
const going = this.going.get(to);
|
|
24402
|
+
if (going) {
|
|
24403
|
+
going.splice(0, going.length, ...frames);
|
|
24404
|
+
return;
|
|
24405
|
+
}
|
|
24406
|
+
const rest = [...frames], told = [];
|
|
24407
|
+
const next = () => {
|
|
24408
|
+
if (!this.sendSlice(to, rest)) return false;
|
|
24409
|
+
if (rest.length) told.push(handled(to).catch(() => false));
|
|
24410
|
+
return true;
|
|
24411
|
+
};
|
|
24412
|
+
if (!next() || !next() || !rest.length) return;
|
|
24413
|
+
this.going.set(to, rest);
|
|
24414
|
+
(async () => {
|
|
24415
|
+
try {
|
|
24416
|
+
while (rest.length) {
|
|
24417
|
+
if (!await told.shift()) await new Promise((resolve) => setTimeout(resolve, 500));
|
|
24418
|
+
if (!this.hooks.active() || !next()) break;
|
|
24419
|
+
}
|
|
24420
|
+
} finally {
|
|
24421
|
+
this.going.delete(to);
|
|
24422
|
+
}
|
|
24423
|
+
})();
|
|
24424
|
+
}
|
|
24425
|
+
/** The next slice of `rest` to `to`: false, and nothing left, when the edge did not take one. */
|
|
24426
|
+
sendSlice(to, rest) {
|
|
24427
|
+
for (const frame of rest.splice(0, 16)) if (this.hooks.send(to, frame) === false) {
|
|
24428
|
+
rest.length = 0;
|
|
24429
|
+
return false;
|
|
24430
|
+
}
|
|
24431
|
+
return true;
|
|
24432
|
+
}
|
|
24433
|
+
};
|
|
24039
24434
|
//#endregion
|
|
24040
24435
|
//#region ../core/src/groupSession.ts
|
|
24041
24436
|
/**
|
|
@@ -24087,7 +24482,15 @@ var GROUP_LIMITS = {
|
|
|
24087
24482
|
syncAnswers: 8,
|
|
24088
24483
|
syncWindowMs: 6e4,
|
|
24089
24484
|
/** Messages delivered from a copy that was not whole, remembered until a whole copy completes them. */
|
|
24090
|
-
provisional: 256
|
|
24485
|
+
provisional: 256,
|
|
24486
|
+
/**
|
|
24487
|
+
* Frames of one catch-up answer in a slice (`GroupSessionHooks.handled`), two slices at most not handled yet by the
|
|
24488
|
+
* member's app: the next goes once it handled the one before the last. An app holds 64 frames waiting at most before
|
|
24489
|
+
* 2026-10-07 (more ends the session), and a member back in a busy group was handed over a hundred at once.
|
|
24490
|
+
*/
|
|
24491
|
+
catchUpSlice: 16,
|
|
24492
|
+
/** Between two slices, when the member's app cannot say it handled one (`handled` resolves `false`). */
|
|
24493
|
+
catchUpPauseMs: 500
|
|
24091
24494
|
};
|
|
24092
24495
|
/** What every member should know about who can read what, in the words the apps show. */
|
|
24093
24496
|
var GROUP_READ_NOTE = `Everyone in the group can read everything sent while they are a member. Someone removed keeps what they already received and cannot read what comes after; someone who joins later cannot read what came before. Messages go directly to each member; whoever was away gets what they missed from any member who has it (the group's last ${GROUP_LIMITS.relay} messages) when they meet again.`;
|
|
@@ -24126,6 +24529,17 @@ var cardAad = (f) => JSON.stringify([
|
|
|
24126
24529
|
f.ts
|
|
24127
24530
|
]);
|
|
24128
24531
|
var isCardBox = isSealedBox(MAX_CARD_BOX);
|
|
24532
|
+
/** A file's description as JSON: a name and a type of 255 characters each, 128 voice bars or a 12 KiB poster, sealed. */
|
|
24533
|
+
var MAX_FILE_BOX = Math.ceil((GROUP_FILE_LIMITS.nameChars * 4 + GROUP_FILE_LIMITS.mimeChars + 17e3) * 4 / 3) + 4;
|
|
24534
|
+
var fileAad = (f) => JSON.stringify([
|
|
24535
|
+
"ghostly-group/1 file",
|
|
24536
|
+
f.g,
|
|
24537
|
+
f.e,
|
|
24538
|
+
f.s,
|
|
24539
|
+
f.n,
|
|
24540
|
+
f.ts
|
|
24541
|
+
]);
|
|
24542
|
+
var isFileBox = isSealedBox(MAX_FILE_BOX);
|
|
24129
24543
|
var B64$1 = /^[A-Za-z0-9_-]*$/;
|
|
24130
24544
|
var secretAad$1 = (g, e, member) => JSON.stringify([
|
|
24131
24545
|
"ghostly-group/1 secret",
|
|
@@ -24174,6 +24588,11 @@ var messageSignedWhole = (f) => utf8Encode(JSON.stringify([
|
|
|
24174
24588
|
f.sc.n,
|
|
24175
24589
|
f.sc.c
|
|
24176
24590
|
] : [],
|
|
24591
|
+
...f.fl ? [
|
|
24592
|
+
"fl",
|
|
24593
|
+
f.fl.n,
|
|
24594
|
+
f.fl.c
|
|
24595
|
+
] : [],
|
|
24177
24596
|
...f.o ? [
|
|
24178
24597
|
"o",
|
|
24179
24598
|
f.o.e,
|
|
@@ -24252,7 +24671,7 @@ function readFirstPlace(o, epoch) {
|
|
|
24252
24671
|
/** One of my own frames, among mine: `e:n`. */
|
|
24253
24672
|
var ownKey = (f) => `${f.e}:${f.n}`;
|
|
24254
24673
|
/** What a frame weighs in a log, roughly: its boxes plus the fixed fields around them. */
|
|
24255
|
-
var frameBytes = (f) => f.c.length + (f.m?.c.length ?? 0) + (f.r?.c.length ?? 0) + (f.sc?.c.length ?? 0) + 400;
|
|
24674
|
+
var frameBytes = (f) => f.c.length + (f.m?.c.length ?? 0) + (f.r?.c.length ?? 0) + (f.sc?.c.length ?? 0) + (f.fl?.c.length ?? 0) + 400;
|
|
24256
24675
|
/** Only the fields an edit frame has, as `clean` does for a message. */
|
|
24257
24676
|
var cleanEdit = (f) => ({
|
|
24258
24677
|
t: GROUP_EDIT_FRAME,
|
|
@@ -24291,6 +24710,10 @@ function clean(f) {
|
|
|
24291
24710
|
n: f.sc.n,
|
|
24292
24711
|
c: f.sc.c
|
|
24293
24712
|
} } : {},
|
|
24713
|
+
...isFileBox(f.fl) ? { fl: {
|
|
24714
|
+
n: f.fl.n,
|
|
24715
|
+
c: f.fl.c
|
|
24716
|
+
} } : {},
|
|
24294
24717
|
...readFirstPlace(f.o, f.e) ? { o: readFirstPlace(f.o, f.e) } : {},
|
|
24295
24718
|
...typeof f.xs === "string" && f.xs.length === 86 && B64$1.test(f.xs) ? { xs: f.xs } : {}
|
|
24296
24719
|
};
|
|
@@ -24367,6 +24790,8 @@ var GroupSession = class {
|
|
|
24367
24790
|
passOn = null;
|
|
24368
24791
|
/** Syncs answered per member, a few a minute (`GROUP_LIMITS.syncAnswers`). In memory only. */
|
|
24369
24792
|
syncsAnswered = /* @__PURE__ */ new Map();
|
|
24793
|
+
/** Catch-up answers still going out, a slice at a time (`handOut`). */
|
|
24794
|
+
answers;
|
|
24370
24795
|
/**
|
|
24371
24796
|
* Messages delivered from a copy that was not whole (handed on without its author's whole signature): not seen, so
|
|
24372
24797
|
* a sync still asks for them and a whole copy completes them. Oldest first, saved as `GroupState.provisional`, so a
|
|
@@ -24379,6 +24804,11 @@ var GroupSession = class {
|
|
|
24379
24804
|
this.hooks = hooks;
|
|
24380
24805
|
this.state = state;
|
|
24381
24806
|
this.identity = identityFromSeedB64(state.seedB64);
|
|
24807
|
+
this.answers = new CatchUpAnswers({
|
|
24808
|
+
send: (to, frame) => hooks.send(to, frame),
|
|
24809
|
+
...hooks.handled && { handled: (to) => hooks.handled(to) },
|
|
24810
|
+
active: () => this.state.status === "active"
|
|
24811
|
+
});
|
|
24382
24812
|
this.provisional = new Set((state.provisional ?? []).slice(-GROUP_LIMITS.provisional));
|
|
24383
24813
|
this.chainMovedAt = this.clock();
|
|
24384
24814
|
}
|
|
@@ -24503,6 +24933,15 @@ var GroupSession = class {
|
|
|
24503
24933
|
get others() {
|
|
24504
24934
|
return this.roster.map(([k]) => k).filter((k) => k !== this.myKey);
|
|
24505
24935
|
}
|
|
24936
|
+
/**
|
|
24937
|
+
* Whether `key` could read message `messageId` (`<s>:<e>:<n>`) and still can: in the roster of its epoch, and in the
|
|
24938
|
+
* roster now. A holder serves a group file only to such a member (WISP 503 § Asking).
|
|
24939
|
+
*/
|
|
24940
|
+
couldRead(key, messageId) {
|
|
24941
|
+
const e = Number(messageId.split(":")[1]);
|
|
24942
|
+
const then = this.state.chain.find((c) => c.e === e);
|
|
24943
|
+
return !!then && rosterHas(then.m, key) && rosterHas(this.roster, key);
|
|
24944
|
+
}
|
|
24506
24945
|
role(key) {
|
|
24507
24946
|
return this.roster.find(([k]) => k === key)?.[1];
|
|
24508
24947
|
}
|
|
@@ -24751,14 +25190,18 @@ var GroupSession = class {
|
|
|
24751
25190
|
* text that name members (everyone: the admin only); what does not hold is left out. `reply`: the message it
|
|
24752
25191
|
* answers, sealed apart from the text (`r`) like the mentions, so an older app still reads the text. `forwarded`: the
|
|
24753
25192
|
* hop count of a forwarded text (`f`, WISP 902 § Forwards). `card`: a status card, checked by the caller, sealed in a
|
|
24754
|
-
* box of its own (`sc`, WISP 405 · Status Cards); the text is its fallback.
|
|
25193
|
+
* box of its own (`sc`, WISP 405 · Status Cards); the text is its fallback. `file`: a file or voice message the text
|
|
25194
|
+
* announces (`fl`, WISP 503 · Group Files), its description checked by the caller; the text is its caption, or the
|
|
25195
|
+
* line an older app shows.
|
|
24755
25196
|
*/
|
|
24756
|
-
sendText(text, now = Date.now(), mentions = [], reply, forwarded, card) {
|
|
25197
|
+
sendText(text, now = Date.now(), mentions = [], reply, forwarded, card, file) {
|
|
24757
25198
|
return this.serialize(async () => {
|
|
24758
25199
|
if (this.state.status !== "active") return { error: this.state.statusReason ?? "You are no longer in this group" };
|
|
24759
25200
|
const trimmed = text.trim();
|
|
24760
25201
|
if (!trimmed) return { error: "Nothing to send" };
|
|
24761
25202
|
if (utf8Encode(trimmed).length > GROUP_LIMITS.textBytes) return { error: "Message exceeds 16 KiB" };
|
|
25203
|
+
const described = file === void 0 ? void 0 : readGroupFileMeta(file);
|
|
25204
|
+
if (file !== void 0 && !described) return { error: "That file cannot go to a group" };
|
|
24762
25205
|
const epoch = this.epoch;
|
|
24763
25206
|
if (!this.secret(epoch)) return { error: "This epoch's key has not arrived yet. Wait for a member to catch you up." };
|
|
24764
25207
|
const n = this.nextSeq(epoch);
|
|
@@ -24769,7 +25212,8 @@ var GroupSession = class {
|
|
|
24769
25212
|
mentions: named,
|
|
24770
25213
|
reply: answers || void 0,
|
|
24771
25214
|
forwarded: fw,
|
|
24772
|
-
card
|
|
25215
|
+
card,
|
|
25216
|
+
file: described ?? void 0
|
|
24773
25217
|
});
|
|
24774
25218
|
this.logOwn(frame);
|
|
24775
25219
|
const id = groupMessageId(this.myKey, epoch, n);
|
|
@@ -24783,7 +25227,8 @@ var GroupSession = class {
|
|
|
24783
25227
|
...named.length ? { mentions: named } : {},
|
|
24784
25228
|
...answers ? { reply: answers } : {},
|
|
24785
25229
|
...fw ? { forwarded: fw } : {},
|
|
24786
|
-
...card ? { card } : {}
|
|
25230
|
+
...card ? { card } : {},
|
|
25231
|
+
...described ? { file: described } : {}
|
|
24787
25232
|
});
|
|
24788
25233
|
await this.say(frame);
|
|
24789
25234
|
return { id };
|
|
@@ -24824,6 +25269,7 @@ var GroupSession = class {
|
|
|
24824
25269
|
...extra.reply ? { r: encryptText(key, replyAad(header), JSON.stringify(extra.reply)) } : {},
|
|
24825
25270
|
...extra.forwarded ? { f: extra.forwarded } : {},
|
|
24826
25271
|
...extra.card ? { sc: encryptText(key, cardAad(header), JSON.stringify(extra.card)) } : {},
|
|
25272
|
+
...extra.file ? { fl: encryptText(key, fileAad(header), JSON.stringify(extra.file)) } : {},
|
|
24827
25273
|
...o ? { o } : {}
|
|
24828
25274
|
};
|
|
24829
25275
|
return {
|
|
@@ -24893,7 +25339,8 @@ var GroupSession = class {
|
|
|
24893
25339
|
mentions,
|
|
24894
25340
|
reply: this.openReply(key, f),
|
|
24895
25341
|
forwarded: readForwarded(f.f) ? f.f : void 0,
|
|
24896
|
-
card: this.openCard(key, f)
|
|
25342
|
+
card: this.openCard(key, f),
|
|
25343
|
+
file: this.openFile(key, f)
|
|
24897
25344
|
}, readFirstPlace(f.o, f.e) ?? {
|
|
24898
25345
|
e: f.e,
|
|
24899
25346
|
n: f.n
|
|
@@ -25183,9 +25630,16 @@ var GroupSession = class {
|
|
|
25183
25630
|
} catch {
|
|
25184
25631
|
return;
|
|
25185
25632
|
}
|
|
25633
|
+
if (bye.e < this.admittedAt(bye.k)) return;
|
|
25186
25634
|
if (this.isAdmin && this.hooks.adminWork?.() !== false && await this.commitUnlessTurnUnconfirmed("remove", bye.k)) return;
|
|
25187
25635
|
this.took(bye);
|
|
25188
25636
|
}
|
|
25637
|
+
/** The epoch of the commit that last admitted `key` (0 for the creator, whom no commit admits). */
|
|
25638
|
+
admittedAt(key) {
|
|
25639
|
+
const chain = this.state.chain;
|
|
25640
|
+
for (let e = chain.length - 1; e > 0; e--) if (chain[e].k === "add" && chain[e].s === key) return e;
|
|
25641
|
+
return 0;
|
|
25642
|
+
}
|
|
25189
25643
|
/**
|
|
25190
25644
|
* An automatic commit (a member's leave): made, or not made when no fresh turn read says this device is the active
|
|
25191
25645
|
* one (`adminTurn`). Then false, and the caller goes on as a member would. Any other refusal is thrown as before.
|
|
@@ -25228,6 +25682,7 @@ var GroupSession = class {
|
|
|
25228
25682
|
delete raw.r;
|
|
25229
25683
|
delete raw.f;
|
|
25230
25684
|
delete raw.sc;
|
|
25685
|
+
delete raw.fl;
|
|
25231
25686
|
delete raw.o;
|
|
25232
25687
|
delete raw.xs;
|
|
25233
25688
|
}
|
|
@@ -25258,6 +25713,7 @@ var GroupSession = class {
|
|
|
25258
25713
|
const reply = this.openReply(key, raw);
|
|
25259
25714
|
const forwarded = readForwarded(raw.f);
|
|
25260
25715
|
const card = this.openCard(key, raw);
|
|
25716
|
+
const file = this.openFile(key, raw);
|
|
25261
25717
|
const completes = whole && this.provisional.delete(id);
|
|
25262
25718
|
await this.hooks.message({
|
|
25263
25719
|
id,
|
|
@@ -25270,6 +25726,7 @@ var GroupSession = class {
|
|
|
25270
25726
|
...reply ? { reply } : {},
|
|
25271
25727
|
...forwarded ? { forwarded } : {},
|
|
25272
25728
|
...card ? { card } : {},
|
|
25729
|
+
...file ? { file } : {},
|
|
25273
25730
|
...completes ? { completes: true } : {}
|
|
25274
25731
|
});
|
|
25275
25732
|
if (!whole) {
|
|
@@ -25371,6 +25828,17 @@ var GroupSession = class {
|
|
|
25371
25828
|
return;
|
|
25372
25829
|
}
|
|
25373
25830
|
}
|
|
25831
|
+
/** The file a message announces (WISP 503); a box that does not open, or a description that does not hold, is no file. */
|
|
25832
|
+
openFile(key, raw) {
|
|
25833
|
+
if (!isFileBox(raw.fl)) return void 0;
|
|
25834
|
+
const plain = decryptText(key, fileAad(raw), raw.fl.n, raw.fl.c);
|
|
25835
|
+
if (plain === null) return void 0;
|
|
25836
|
+
try {
|
|
25837
|
+
return readGroupFileMeta(JSON.parse(plain)) ?? void 0;
|
|
25838
|
+
} catch {
|
|
25839
|
+
return;
|
|
25840
|
+
}
|
|
25841
|
+
}
|
|
25374
25842
|
/** The message a text answers; a box that does not open, or does not hold one, is no reply, not no message. */
|
|
25375
25843
|
openReply(key, raw) {
|
|
25376
25844
|
if (!isReplyBox(raw.r)) return void 0;
|
|
@@ -25524,9 +25992,10 @@ var GroupSession = class {
|
|
|
25524
25992
|
});
|
|
25525
25993
|
return;
|
|
25526
25994
|
}
|
|
25995
|
+
const answer = [];
|
|
25527
25996
|
for (let e = frame.e + 1; e <= this.epoch; e++) {
|
|
25528
25997
|
const commit = this.state.chain[e], secret = this.secret(e);
|
|
25529
|
-
|
|
25998
|
+
answer.push({
|
|
25530
25999
|
t: "group-commit",
|
|
25531
26000
|
g: this.id,
|
|
25532
26001
|
commit,
|
|
@@ -25538,7 +26007,7 @@ var GroupSession = class {
|
|
|
25538
26007
|
e,
|
|
25539
26008
|
s: sealSecret(from, this.secret(e), secretAad$1(this.id, e, from))
|
|
25540
26009
|
}));
|
|
25541
|
-
for (let i = 0; i < secrets.length; i += GROUP_LIMITS.secretsPerFrame)
|
|
26010
|
+
for (let i = 0; i < secrets.length; i += GROUP_LIMITS.secretsPerFrame) answer.push({
|
|
25542
26011
|
t: "group-secrets",
|
|
25543
26012
|
g: this.id,
|
|
25544
26013
|
secrets: secrets.slice(i, i + GROUP_LIMITS.secretsPerFrame)
|
|
@@ -25546,12 +26015,24 @@ var GroupSession = class {
|
|
|
25546
26015
|
const lacks = missingIn(frame, from);
|
|
25547
26016
|
const asked = new Set(Array.isArray(frame.ask) ? frame.ask.filter((k) => typeof k === "string" && k !== from && k !== this.myKey && rosterHas(this.roster, k)) : []);
|
|
25548
26017
|
const kept = asked.size ? (this.state.relay ?? []).filter((f) => asked.has(f.s) && this.state.chain[f.e] && rosterHas(this.state.chain[f.e].m, from) && lacks(f)) : [];
|
|
25549
|
-
|
|
26018
|
+
answer.push(...asWritten(this.ownLacked(from, frame), kept));
|
|
25550
26019
|
for (const author of asked) {
|
|
25551
26020
|
const edits = (this.state.relayEdits ?? []).filter((f) => f.s === author && this.state.chain[f.e] && rosterHas(this.state.chain[f.e].m, from));
|
|
25552
|
-
|
|
26021
|
+
answer.push(...edits.slice(-GROUP_LIMITS.handOnEdits));
|
|
25553
26022
|
}
|
|
25554
|
-
this.
|
|
26023
|
+
const meta = this.metaFor(from, frame.mt);
|
|
26024
|
+
if (meta) answer.push(meta);
|
|
26025
|
+
this.handOut(from, answer);
|
|
26026
|
+
}
|
|
26027
|
+
/**
|
|
26028
|
+
* A catch-up answer, a slice at a time (`GROUP_LIMITS.catchUpSlice`), with two slices at most not handled yet by the
|
|
26029
|
+
* member's app (`hooks.handled`), as a community's goes (`CatchUpAnswers`). All at once, a member back in a busy group
|
|
26030
|
+
* was handed over a hundred frames on one session, and an app that holds 64 waiting ended that session mid catch-up
|
|
26031
|
+
* ("Session receive limit exceeded", 2026-10-07). A newer answer to the same member takes the place of what is left of
|
|
26032
|
+
* the last; a frame the edge did not take ends it (the member asks again when its edge opens).
|
|
26033
|
+
*/
|
|
26034
|
+
handOut(to, frames) {
|
|
26035
|
+
this.answers.handOut(to, frames);
|
|
25555
26036
|
}
|
|
25556
26037
|
/** My own messages a member's sync says it lacks, from my bounded log, for the epochs it was a member of. */
|
|
25557
26038
|
handOwn(to, frame) {
|
|
@@ -25630,10 +26111,14 @@ var GroupSession = class {
|
|
|
25630
26111
|
}
|
|
25631
26112
|
/** My statement, to a member whose sync says it holds an older one (or none); nothing to an app that says nothing. */
|
|
25632
26113
|
offerMeta(to, theirTag) {
|
|
26114
|
+
const frame = this.metaFor(to, theirTag);
|
|
26115
|
+
if (frame) this.hooks.send(to, frame);
|
|
26116
|
+
}
|
|
26117
|
+
metaFor(to, theirTag) {
|
|
25633
26118
|
const meta = this.state.meta, theirs = parseGroupMetaTag(theirTag);
|
|
25634
|
-
if (!meta || theirs === null || !groupMetaNewer(meta, theirs) || !rosterHas(this.roster, to)) return;
|
|
26119
|
+
if (!meta || theirs === null || !groupMetaNewer(meta, theirs) || !rosterHas(this.roster, to)) return null;
|
|
25635
26120
|
const secret = this.secret(this.epoch);
|
|
25636
|
-
|
|
26121
|
+
return secret ? wrapGroupMeta(meta, this.epoch, epochKeys(secret, this.id, this.epoch).message) : null;
|
|
25637
26122
|
}
|
|
25638
26123
|
/**
|
|
25639
26124
|
* A metadata statement: kept when it is newer than mine, its commit is on my chain, its signer was
|
|
@@ -25917,7 +26402,15 @@ var COMMUNITY_LIMITS = {
|
|
|
25917
26402
|
* waits for the window.
|
|
25918
26403
|
*/
|
|
25919
26404
|
syncAnswers: 16,
|
|
25920
|
-
syncWindowMs: 6e4
|
|
26405
|
+
syncWindowMs: 6e4,
|
|
26406
|
+
/**
|
|
26407
|
+
* Frames of one catch-up answer in a slice (`CommunitySessionHooks.handled`), two slices at most not handled yet by
|
|
26408
|
+
* the member's app: a member back after the store filled was handed 258 at once, and an app holds 64 waiting at most
|
|
26409
|
+
* before 2026-10-07 (more ends the session). As a private group's (`GROUP_LIMITS.catchUpSlice`).
|
|
26410
|
+
*/
|
|
26411
|
+
catchUpSlice: 16,
|
|
26412
|
+
/** Between two slices, when the member's app cannot say it handled one (`handled` resolves `false`). */
|
|
26413
|
+
catchUpPauseMs: 500
|
|
25921
26414
|
};
|
|
25922
26415
|
/** What every member should know about who can read what, and what the link does, in the words the apps show. */
|
|
25923
26416
|
var GROUP_READ_NOTE_COMMUNITY = `Anyone with the group's link can join, and any member can let people in. Everyone in the group can read everything sent while they are a member; someone removed cannot read what comes after, someone who joins later cannot read what came before. Members who were away get the last ${COMMUNITY_LIMITS.store} messages from whoever is there when they return.`;
|
|
@@ -26227,6 +26720,8 @@ var CommunitySession = class {
|
|
|
26227
26720
|
asked = /* @__PURE__ */ new Map();
|
|
26228
26721
|
/** Syncs answered per member, a few a minute (`COMMUNITY_LIMITS.syncAnswers`). In memory only. */
|
|
26229
26722
|
syncsAnswered = /* @__PURE__ */ new Map();
|
|
26723
|
+
/** Catch-up answers still going out, a slice at a time (`receiveSync`). */
|
|
26724
|
+
answers;
|
|
26230
26725
|
queue = Promise.resolve();
|
|
26231
26726
|
/** When the commit I follow last changed (`adopt`), by `hooks.clock`. */
|
|
26232
26727
|
chainMovedAt = 0;
|
|
@@ -26239,6 +26734,11 @@ var CommunitySession = class {
|
|
|
26239
26734
|
this.hooks = hooks;
|
|
26240
26735
|
this.state = state;
|
|
26241
26736
|
this.identity = identityFromSeedB64(state.seedB64);
|
|
26737
|
+
this.answers = new CatchUpAnswers({
|
|
26738
|
+
send: (to, frame) => hooks.direct(to, frame),
|
|
26739
|
+
...hooks.handled && { handled: (to) => hooks.handled(to) },
|
|
26740
|
+
active: () => this.state.status === "active"
|
|
26741
|
+
});
|
|
26242
26742
|
this.rebuild();
|
|
26243
26743
|
if ((state.status === "active" || state.status === "lost") && this.bestBranch().tip !== this.topHash) this.serialize(() => this.chooseBranch());
|
|
26244
26744
|
}
|
|
@@ -26412,6 +26912,15 @@ var CommunitySession = class {
|
|
|
26412
26912
|
for (const roster of this.rosters.values()) if (rosterHas(roster, key)) return true;
|
|
26413
26913
|
return false;
|
|
26414
26914
|
}
|
|
26915
|
+
/**
|
|
26916
|
+
* Whether `key` could read message `messageId` (`<s>:<e>:<h>:<n>`) and still can: a member of the roster it was sealed
|
|
26917
|
+
* for, and of the roster now. A holder serves a group file only to such a member (WISP 503 § Asking).
|
|
26918
|
+
*/
|
|
26919
|
+
couldRead(key, messageId) {
|
|
26920
|
+
const [, e, h] = messageId.split(":");
|
|
26921
|
+
const found = h === void 0 ? void 0 : this.commitByShort(Number(e), h);
|
|
26922
|
+
return !!found && rosterHas(this.rosterAt(found.hash) ?? [], key) && rosterHas(this.roster, key);
|
|
26923
|
+
}
|
|
26415
26924
|
/** Someone the chain took out (removed or left) and who is not back in. */
|
|
26416
26925
|
wasRemoved(key) {
|
|
26417
26926
|
return this.outIndex(key) >= 0;
|
|
@@ -27029,23 +27538,29 @@ var CommunitySession = class {
|
|
|
27029
27538
|
* count against the text's 16 KiB, so the box stays within what older apps accept. `reply`: the message it
|
|
27030
27539
|
* answers (`r`), counted the same way. `forwarded`: a forwarded text's hop count (`fw`, WISP 903 § Forwards). `card`:
|
|
27031
27540
|
* a status card, checked by the caller (`sc`, WISP 405 · Status Cards), counted the same way; the text is its fallback.
|
|
27541
|
+
* `file`: a file or voice message the text announces (`fl`, WISP 503 · Group Files), counted the same way; the text is
|
|
27542
|
+
* its caption, or the line an older app shows.
|
|
27032
27543
|
*/
|
|
27033
|
-
sendText(text, nick, now = Date.now(), mentions = [], reply, forwarded, card) {
|
|
27544
|
+
sendText(text, nick, now = Date.now(), mentions = [], reply, forwarded, card, file) {
|
|
27034
27545
|
return this.serialize(async () => {
|
|
27035
27546
|
if (!this.isMember) return { error: this.state.statusReason ?? "You are not in this group" };
|
|
27036
27547
|
const trimmed = text.trim();
|
|
27037
27548
|
if (!trimmed) return { error: "Nothing to send" };
|
|
27549
|
+
const described = file === void 0 ? void 0 : readGroupFileMeta(file);
|
|
27550
|
+
if (file !== void 0 && !described) return { error: "That file cannot go to a group" };
|
|
27038
27551
|
const named = validMentions(wireMentions(mentions), trimmed, false);
|
|
27039
27552
|
const answers = reply && readReply(wireReply(reply), groupReplyAuthor);
|
|
27040
27553
|
const replyBytes = answers ? utf8Encode(JSON.stringify(answers)).length : 0;
|
|
27554
|
+
const fileBytes = described ? utf8Encode(JSON.stringify(described)).length + 8 : 0;
|
|
27041
27555
|
const hops = readForwarded(forwarded);
|
|
27042
|
-
if (utf8Encode(trimmed).length + mentionsBytes(named) + replyBytes + (hops ? 16 : 0) + (card ? statusCardBytes(card) + 8 : 0) > COMMUNITY_LIMITS.textBytes) return { error: "Message exceeds 16 KiB" };
|
|
27556
|
+
if (utf8Encode(trimmed).length + mentionsBytes(named) + replyBytes + fileBytes + (hops ? 16 : 0) + (card ? statusCardBytes(card) + 8 : 0) > COMMUNITY_LIMITS.textBytes) return { error: "Message exceeds 16 KiB" };
|
|
27043
27557
|
const sent = await this.sendPayload(() => ({
|
|
27044
27558
|
text: trimmed,
|
|
27045
27559
|
...named.length ? { m: named } : {},
|
|
27046
27560
|
...answers ? { r: answers } : {},
|
|
27047
27561
|
...hops ? { fw: hops } : {},
|
|
27048
|
-
...card ? { sc: card } : {}
|
|
27562
|
+
...card ? { sc: card } : {},
|
|
27563
|
+
...described ? { fl: described } : {}
|
|
27049
27564
|
}), nick, now);
|
|
27050
27565
|
if ("error" in sent) return sent;
|
|
27051
27566
|
await this.hooks.message({
|
|
@@ -27058,7 +27573,8 @@ var CommunitySession = class {
|
|
|
27058
27573
|
...named.length ? { mentions: named } : {},
|
|
27059
27574
|
...answers ? { reply: answers } : {},
|
|
27060
27575
|
...hops ? { forwarded: hops } : {},
|
|
27061
|
-
...card ? { card } : {}
|
|
27576
|
+
...card ? { card } : {},
|
|
27577
|
+
...described ? { file: described } : {}
|
|
27062
27578
|
});
|
|
27063
27579
|
await this.say(sent.frame);
|
|
27064
27580
|
return { id: sent.id };
|
|
@@ -27414,6 +27930,7 @@ var CommunitySession = class {
|
|
|
27414
27930
|
const reply = parsed.r === void 0 ? void 0 : readReply(parsed.r, groupReplyAuthor);
|
|
27415
27931
|
const forwarded = readForwarded(parsed.fw);
|
|
27416
27932
|
const card = parsed.sc === void 0 ? void 0 : readStatusCard(parsed.sc);
|
|
27933
|
+
const file = parsed.fl === void 0 ? void 0 : readGroupFileMeta(parsed.fl) ?? void 0;
|
|
27417
27934
|
await this.hooks.message({
|
|
27418
27935
|
id,
|
|
27419
27936
|
sender: raw.s,
|
|
@@ -27424,7 +27941,8 @@ var CommunitySession = class {
|
|
|
27424
27941
|
...mentions.length ? { mentions } : {},
|
|
27425
27942
|
...reply ? { reply } : {},
|
|
27426
27943
|
...forwarded ? { forwarded } : {},
|
|
27427
|
-
...card ? { card } : {}
|
|
27944
|
+
...card ? { card } : {},
|
|
27945
|
+
...file ? { file } : {}
|
|
27428
27946
|
});
|
|
27429
27947
|
} else if (isObject$2(parsed.x)) await this.hooks.app?.({
|
|
27430
27948
|
id,
|
|
@@ -27516,10 +28034,15 @@ var CommunitySession = class {
|
|
|
27516
28034
|
this.ask(to);
|
|
27517
28035
|
}
|
|
27518
28036
|
ask(from) {
|
|
28037
|
+
const sync = this.question(from);
|
|
28038
|
+
if (sync) this.hooks.direct(from, sync);
|
|
28039
|
+
}
|
|
28040
|
+
/** My sync for a member, unless I asked it a moment ago. */
|
|
28041
|
+
question(from) {
|
|
27519
28042
|
const now = this.hooks.clock?.() ?? Date.now();
|
|
27520
|
-
if (now - (this.asked.get(from) ?? 0) < 5e3) return;
|
|
28043
|
+
if (now - (this.asked.get(from) ?? 0) < 5e3) return null;
|
|
27521
28044
|
this.asked.set(from, now);
|
|
27522
|
-
|
|
28045
|
+
return this.syncFrame();
|
|
27523
28046
|
}
|
|
27524
28047
|
async replayWaiting() {
|
|
27525
28048
|
await this.metaFollowsChain();
|
|
@@ -27537,6 +28060,11 @@ var CommunitySession = class {
|
|
|
27537
28060
|
let answered = this.syncsAnswered.get(from);
|
|
27538
28061
|
if (!answered) this.syncsAnswered.set(from, answered = new RateWindow(COMMUNITY_LIMITS.syncAnswers, COMMUNITY_LIMITS.syncWindowMs, () => this.hooks.clock?.() ?? Date.now()));
|
|
27539
28062
|
if (!answered.take()) return;
|
|
28063
|
+
const answer = [];
|
|
28064
|
+
const ask = () => {
|
|
28065
|
+
const sync = this.question(from);
|
|
28066
|
+
if (sync) answer.push(sync);
|
|
28067
|
+
};
|
|
27540
28068
|
const theirs = this.known.get(frame.h);
|
|
27541
28069
|
let start;
|
|
27542
28070
|
if (theirs && this.mainIndex.has(frame.h)) start = this.mainIndex.get(frame.h) + 1;
|
|
@@ -27546,18 +28074,18 @@ var CommunitySession = class {
|
|
|
27546
28074
|
} else {
|
|
27547
28075
|
const common = (Array.isArray(frame.loc) ? frame.loc : []).find((x) => typeof x === "string" && this.mainIndex.has(x));
|
|
27548
28076
|
start = common !== void 0 ? this.mainIndex.get(common) + 1 : Math.max(0, this.state.chain.length - COMMUNITY_LIMITS.side);
|
|
27549
|
-
|
|
28077
|
+
ask();
|
|
27550
28078
|
}
|
|
27551
28079
|
for (let i = start; i < this.state.chain.length; i++) {
|
|
27552
28080
|
const commit = this.state.chain[i], h = communityCommitHash(commit);
|
|
27553
|
-
|
|
28081
|
+
answer.push({
|
|
27554
28082
|
t: "group-commit",
|
|
27555
28083
|
v: 2,
|
|
27556
28084
|
g: this.id,
|
|
27557
28085
|
commit
|
|
27558
28086
|
});
|
|
27559
28087
|
const secret = this.state.secrets[h];
|
|
27560
|
-
if (secret && rosterHas(this.rosterAt(h) ?? [], from) && FRESH.has(commit.k))
|
|
28088
|
+
if (secret && rosterHas(this.rosterAt(h) ?? [], from) && FRESH.has(commit.k)) answer.push({
|
|
27561
28089
|
t: "group-secret",
|
|
27562
28090
|
v: 2,
|
|
27563
28091
|
g: this.id,
|
|
@@ -27571,7 +28099,7 @@ var CommunitySession = class {
|
|
|
27571
28099
|
h,
|
|
27572
28100
|
s: sealSecret(from, fromBase64Url(this.state.secrets[h]), secretAad(this.id, h, from))
|
|
27573
28101
|
}));
|
|
27574
|
-
if (secrets.length)
|
|
28102
|
+
if (secrets.length) answer.push({
|
|
27575
28103
|
t: "group-secrets",
|
|
27576
28104
|
v: 2,
|
|
27577
28105
|
g: this.id,
|
|
@@ -27588,8 +28116,8 @@ var CommunitySession = class {
|
|
|
27588
28116
|
if (!reads && !(mine && rosterHas(this.roster, from))) continue;
|
|
27589
28117
|
const high = have[stored.s]?.[seenKey(stored.e, stored.h)];
|
|
27590
28118
|
if (Number.isSafeInteger(high) && high >= stored.n) continue;
|
|
27591
|
-
if (!this.mainIndex.has(found.hash)) this.handSide(from, found.hash, handed);
|
|
27592
|
-
|
|
28119
|
+
if (!this.mainIndex.has(found.hash)) this.handSide(from, found.hash, handed, answer);
|
|
28120
|
+
answer.push(stored);
|
|
27593
28121
|
if (mine && reads) unheard.delete(frameKey(stored));
|
|
27594
28122
|
}
|
|
27595
28123
|
if (unheard.size !== (this.state.unheard?.length ?? 0)) {
|
|
@@ -27598,30 +28126,32 @@ var CommunitySession = class {
|
|
|
27598
28126
|
}
|
|
27599
28127
|
if (rosterHas(this.roster, from)) {
|
|
27600
28128
|
const entry = this.entryFrame(from);
|
|
27601
|
-
if (entry)
|
|
28129
|
+
if (entry) answer.push(entry);
|
|
27602
28130
|
}
|
|
27603
|
-
for (const request of this.state.pendingLeaves)
|
|
28131
|
+
for (const request of this.state.pendingLeaves) answer.push({
|
|
27604
28132
|
t: "group-leave",
|
|
27605
28133
|
v: 2,
|
|
27606
28134
|
g: this.id,
|
|
27607
28135
|
...request
|
|
27608
28136
|
});
|
|
27609
|
-
this.
|
|
27610
|
-
|
|
28137
|
+
const meta = this.metaFor(from, frame.mt);
|
|
28138
|
+
if (meta) answer.push(meta);
|
|
28139
|
+
ask();
|
|
28140
|
+
this.answers.handOut(from, answer);
|
|
27611
28141
|
}
|
|
27612
|
-
/** The commits of a branch I do not follow, from where it leaves mine, and the secrets I hold of those `to` was in
|
|
27613
|
-
handSide(to, tip, handed) {
|
|
28142
|
+
/** The commits of a branch I do not follow, from where it leaves mine, and the secrets I hold of those `to` was in, onto `answer`. */
|
|
28143
|
+
handSide(to, tip, handed, answer) {
|
|
27614
28144
|
for (const h of this.pathToMain(tip) ?? []) {
|
|
27615
28145
|
if (handed.has(h)) continue;
|
|
27616
28146
|
handed.add(h);
|
|
27617
|
-
|
|
28147
|
+
answer.push({
|
|
27618
28148
|
t: "group-commit",
|
|
27619
28149
|
v: 2,
|
|
27620
28150
|
g: this.id,
|
|
27621
28151
|
commit: this.known.get(h)
|
|
27622
28152
|
});
|
|
27623
28153
|
const secret = this.state.secrets[h];
|
|
27624
|
-
if (secret && rosterHas(this.rosterAt(h) ?? [], to))
|
|
28154
|
+
if (secret && rosterHas(this.rosterAt(h) ?? [], to)) answer.push({
|
|
27625
28155
|
t: "group-secret",
|
|
27626
28156
|
v: 2,
|
|
27627
28157
|
g: this.id,
|
|
@@ -27720,12 +28250,11 @@ var CommunitySession = class {
|
|
|
27720
28250
|
const meta = this.state.meta, secret = this.state.secrets[this.topHash];
|
|
27721
28251
|
return meta && secret ? wrapGroupMeta(meta, this.topHash, epochKeys(fromBase64Url(secret), this.id, this.epoch).message, true) : null;
|
|
27722
28252
|
}
|
|
27723
|
-
/** My statement,
|
|
27724
|
-
|
|
28253
|
+
/** My statement, for a member whose sync says it holds an older one (or none); none for an app that says nothing. */
|
|
28254
|
+
metaFor(to, theirTag) {
|
|
27725
28255
|
const meta = this.state.meta, theirs = parseGroupMetaTag(theirTag);
|
|
27726
|
-
if (!meta || theirs === null || !groupMetaNewer(meta, theirs) || !this.isMember || !rosterHas(this.roster, to)) return;
|
|
27727
|
-
|
|
27728
|
-
if (frame) this.hooks.direct(to, frame);
|
|
28256
|
+
if (!meta || theirs === null || !groupMetaNewer(meta, theirs) || !this.isMember || !rosterHas(this.roster, to)) return null;
|
|
28257
|
+
return this.metaFrame();
|
|
27729
28258
|
}
|
|
27730
28259
|
/**
|
|
27731
28260
|
* A metadata statement (from its author's edge or a hub's): kept when it is newer than mine, its
|
|
@@ -27936,6 +28465,13 @@ var APP_CLIENTS = [
|
|
|
27936
28465
|
"desktop",
|
|
27937
28466
|
"extension"
|
|
27938
28467
|
];
|
|
28468
|
+
/**
|
|
28469
|
+
* Where an app shows (WISP 1200 · Manifest, `view`). `chat`, the default when the manifest has none: inside a 1:1 chat
|
|
28470
|
+
* only, beside it on a wide screen. `full`: full screen, alone or from a chat, never beside one.
|
|
28471
|
+
*/
|
|
28472
|
+
var APP_VIEWS = ["chat", "full"];
|
|
28473
|
+
/** The view of a manifest: `chat` when it names none. */
|
|
28474
|
+
var appViewOf = (manifest) => manifest.view ?? "chat";
|
|
27939
28475
|
var REQUIRED = [
|
|
27940
28476
|
"ghostlyApp",
|
|
27941
28477
|
"publisher",
|
|
@@ -27953,6 +28489,7 @@ var REQUIRED = [
|
|
|
27953
28489
|
];
|
|
27954
28490
|
var OPTIONAL = [
|
|
27955
28491
|
"description",
|
|
28492
|
+
"view",
|
|
27956
28493
|
"sources",
|
|
27957
28494
|
"proofs",
|
|
27958
28495
|
"homepage",
|
|
@@ -28081,6 +28618,7 @@ function checkAppManifest(value) {
|
|
|
28081
28618
|
if (m.releaseNotes !== void 0 && !isLine$1(m.releaseNotes, APP_BUNDLE_LIMITS.releaseNotes)) return bad("releaseNotes");
|
|
28082
28619
|
const permissions = m.permissions;
|
|
28083
28620
|
if (!Array.isArray(permissions) || new Set(permissions).size !== permissions.length || !permissions.every((p) => APP_PERMISSIONS.includes(p))) return bad("permissions");
|
|
28621
|
+
if (m.view !== void 0 && !APP_VIEWS.includes(m.view)) return bad("view", `view is "chat" or "full"`);
|
|
28084
28622
|
const runtime = m.runtime;
|
|
28085
28623
|
if (!isObject$1(runtime) || Object.keys(runtime).length !== 2 || typeof runtime.host !== "string" || !HOST.test(runtime.host)) return bad("runtime");
|
|
28086
28624
|
const clients = runtime.clients;
|
|
@@ -28554,6 +29092,6 @@ function appStoreDecision(held, read) {
|
|
|
28554
29092
|
return read.digest === held.digest ? "same" : "equivocation";
|
|
28555
29093
|
}
|
|
28556
29094
|
//#endregion
|
|
28557
|
-
export { parseSshSignature as $, parseLocalTarget as $a, parseLinkPreview as $i, GROUP_SIGNAL_FRAME as $n, readHandoffPake as $o, EDIT_CAPABILITY as $r, readTurnPacket as $s, TEST_USDT_FAUCET_AMOUNT as $t, knockIdentity as A, APP_STATEMENT_LIMITS as Aa, toZ32 as Ac, irohDescriptor as Ai, normalizeDomain as An, handoffAttemptFailed as Ao, TYPING_REFRESH_MS as Ar, encodeInviteCode as As, didWebFile as At, GROUP_READ_NOTE as B, canonicalJsonBytes as Ba, sanitizeFileName as Bi, decodeBolt11 as Bn, handoffManifestFrames as Bo, readPin as Br, devicePingFrame as Bs, strictBase64Url as Bt, communityHasFrame as C, statusCardText as Ca, verify as Cc, revokesIdentity as Ci, decodeDnsResponse as Cn, HANDOFF_PAKE as Co, readLobby as Cr, verifyEnrollProof as Cs, DidError as Ct, decodeGroupEntryLink as D, receivedTimestamp as Da, fromBase64Url as Dc, validExternalKey as Di, encodeDnsQuery as Dn, HANDOFF_VERIFIED as Do, transferEnded as Dr, OwnInviteError as Ds, didJwsAlg as Dt, KNOCK_TTL_MS as E, heardTime as Ea, bytesEqual as Ec, emptyProofLedger as Ei, domainWellKnownFile as En, HANDOFF_TIMINGS as Eo, FILE_LIMITS as Er, readDeviceInvite as Es, didGhostlyServiceEntry as Et, GroupWakeLimiter as F, isAppRef as Fa, DHT_FILE_CAPABILITY as Fi, wellKnownUrl as Fn, handoffContextProof as Fo, PIN_LIMITS as Fr, HANDOFF_CAPABILITY as Fs, parseDid as Ft, carryMentions as G, verifyCommitSignature as Ga, fileMessageText as Gi, isFederationId as Gn, handoffStreamKey as Go, parseWakeFrame as Gr, TURN_RAISE_READ_TIMEOUT_MS as Gs, resolveDidDht as Gt, COMMUNITY_EDIT_FRAME as H, MAX_GROUP_CHAIN as Ha, PLAYABLE_AUDIO as Hi, cashuRequestPayload as Hn, handoffPakeFrame as Ho, WakeLimiter as Hr, seedSigner as Hs, didDhtFromPublicKey as Ht, groupWakeFrame as I, readAppRevocations as Ia, dhtFileId as Ii, requestLnurlInvoice as In, handoffDataFrame as Io, mayPin as Ir, deviceEchoFrame as Is, parseDidDocument as It, groupName as J, EXPECT_PEER_MS as Ja, parseVideoMeta as Ji, GhostLink as Jn, readHandoffAck as Jo, checkPushEndpoint as Jr, turnPutSummary as Js, EVM_TEST_CHAINS as Jt, parseCommunityEdit as K, edgeParams as Ka, parseVoiceMeta as Ki, parseCashuRequestPayload as Kn, handoffVersions as Ko, relayRequest as Kr, TURN_SETTLE_MS as Ks, signDidDhtPacket as Kt, groupWakes as L, signAppObject as La, parseImageMeta as Li, resolveLightningDestination as Ln, handoffDigest as Lo, nextPinNumber as Lr, deviceEchoNonce as Ls, parseDidSignaturePaste as Lt, mergeKnocks as M, appRef as Ma, utf8Encode as Mc, DHT_FILE_TOO_LARGE as Mi, recordsFromWellKnown as Mn, handoffBusyFrame as Mo, sanitizeTypingStatus as Mr, inviteOwnership as Ms, didWebFileUrl as Mt, readKnocks as N, checkAppRevokeStatement as Na, DHT_MESSAGE_TTL as Ni, recordsName as Nn, handoffCancelFrame as No, typingActivity as Nr, DEVICES_CAPABILITY as Ns, didWebHost as Nt, encodeGroupEntryLink as O, shownTime as Oa, randomBytes as Oc, TRANSPORTS as Oi, isPublicAddress as On, filePartName as Oo, transferStalled as Or, createChatInvite as Os, didSignedMessages as Ot, GROUP_WAKE_FRAME as P, isAppKey as Pa, peekDhtMailbox as Pi, txtValue as Pn, handoffContext as Po, GROUP_PIN_FRAME as Pr, ENROLL_CAPABILITY as Ps, normalizeDid as Pt, parseSshPublicKey as Q, formatLocalTarget as Qa, dhtEditId as Qi, CarriedTransport as Qn, readHandoffHello as Qo, BUTTONS_CAPABILITY as Qr, nextTurnPosition as Qs, SEPOLIA_TEST_USDT_FAUCET as Qt, parseGroupWakeFrame as R, signAppRevocation as Ra, readImageMeta as Ri, engineError as Rn, handoffEphemeral as Ro, pinIsNewer as Rr, deviceKeyZ32 as Rs, signingKeys as Rt, GROUP_READ_NOTE_COMMUNITY as S, sameButtonText as Sa, publicKeyFromZ32 as Sc, receivedIdentityStatus as Si, DOMAIN_FILE_MAX_BYTES as Sn, HANDOFF_OFFER as So, readBeaconHead as Sr, readEnrollHello as Ss, DID_JWS_MAX_LENGTH as St, encodeCommunityLink as T, claimedTime as Ta, sanitizeAvatar as Tc, PeerProofs as Ti, domainTxtRecord as Tn, HANDOFF_REQUEST as To, ChatFiles as Tr, deviceInviteJoinerParams as Ts, checkDidSignature as Tt, GROUP_EDIT_FRAME as U, MEMBER_KEY as Ua, VOICE_LIMITS as Ui, fedimintRequestPayload as Un, handoffReleaseFrame as Uo, deviceWakeRequest as Ur, webCryptoSigner as Us, encodeDidDhtDeactivated as Ut, GroupSession as V, GROUP_MEMBER_CAP as Va, sanitizeMime as Vi, ENDPOINT as Vn, handoffOfferFrame as Vo, WAKE_CALL_INTERVAL_MS as Vr, newDeviceSetSecret as Vs, didDhtDocument as Vt, GROUP_EDIT_TEXT_BYTES as W, rosterHas as Wa, baseMime as Wi, findEndpoint as Wn, handoffRetryAfter as Wo, newWakeToken as Wr, TURN_PUT_WINDOW_MS as Ws, encodeDidDhtPacket as Wt, SSHSIG_NAMESPACE as X, presenceSeenAt as Xa, EDIT_RATE_WINDOW_MS as Xi, LIVENESS_PING_MS as Xn, readHandoffDone as Xo, chatAppId as Xr, TURN_MAX as Xs, PaymentPreflightError as Xt, SSHSIG_MAX_ARMOR as Y, RELAY_POLL_INTERVALS as Ya, EDIT_BUFFER_MS as Yi, INVITE_TAKEN as Yn, readHandoffCancel as Yo, vapidKeysMatch as Yr, TURN_LAST_SEQUENCE as Ys, ONCHAIN_PROVIDER as Yt, SshSigError as Z, GhostlyHttpError as Za, RateWindow as Zi, pairedMessageFrame as Zn, readHandoffHave as Zo, isAppVersion as Zr, firstDeviceSetSecret as Zs, SEPOLIA_TEST_USDT as Zt, groupTypingEpoch as _, ITEM_STATES as _a, openRelayPayload as _c, boundVerifiedIdentity as _i, pubkyProofCapability as _n, HANDOFF_ACK as _o, mergeLobby as _r, enrollDoneFrame as _s, shortAtprotoDid as _t, checkAppStoreIndex as a, ReactionWindow as aa, RelayTransport as ac, HoldRefusedError as ai, validatePaymentTarget as an, RTC_CONFIG as ao, MESH_HUBS as ar, readSetUpdate as as, decodeAnyBitcoinAddress as at, COMMUNITY_LIMITS as b, buttonLabelClash as ba, identityFromSeed as bc, identityStatement as bi, homeserverWebEndpoint as bn, HANDOFF_HAVE as bo, rankHubs as br, enrollProofFrame as bs, DID_DOCUMENT_MAX_BYTES as bt, APP_BUNDLE_LIMITS as c, reactionEmoji as ca, setLinkTraceSink as cc, CapsExchange as ci, SPARK_PROVIDER as cn, callRtcConfig as co, pickMeshHubs as cr, signTombstone as cs, ATPROTO_PROOF_COLLECTION as ct, assembleAppBundle as d, validReactionNumber as da, isDiscoveryBudgetError as dc, dialDescriptors as di, isBitcoinAddress as dn, parseCallSignal as do, beaconRecords as dr, ENROLL_GRANT as ds, atprotoProofRecord as dt, forwardedAgain as ea, signTurnPacket as ec, UPGRADE_CAPABILITY as ei, USDT_PUBLIC_RPC as en, webLocalFetch as eo, SIGNAL_FRESH_MS as er, readHandoffTurnFrame as es, sameSshKey as et, checkAppManifest as f, wireReaction as fa, withRequestOptions as fc, IDENTITY_MAX_EVIDENCE as fi, PUBKY_PROOF_MAX_BYTES as fn, signalHasVideo as fo, doorHubs as fr, ENROLL_HELLO as fs, atprotoProofRkey as ft, GroupTypingBoard as g, BUTTON_ID as ga, newerPacket as gc, IdentityExchange as gi, normalizePubkyKey as gn, serviceIdFromName as go, mergeBeacon as gr, enrollDigits as gs, parseAtprotoDidDocument as gt, GROUP_TYPING_FRAME as h, replySnippet as ha, createRelayPayload as hc, IdentityCheckUnavailable as hi, newPubkyProofFolder as hn, LEGACY_SERVICES as ho, lobbyRecords as hr, enrollCancelReason as hs, normalizeAtprotoHandle as ht, planAppUpdate as i, REACTION_LIMITS as ia, DEFAULT_RELAYS as ic, HoldKeys as ii, formatPaymentAmount as in, CALL_SIGNAL_MAX_AGE_MS as io, readGroupSignal as ir, SET_UPDATE as is, BitcoinAddressError as it, knockRecords as j, appFingerprint as ja, utf8Decode as jc, irohRelayUrl as ji, recordsFromTxt as jn, handoffAttemptSucceeded as jo, TypingSender as jr, inviteLink as js, didWebFileNames as jt, entryParams as k, APP_PREFIXES as ka, toBase64Url as kc, automaticTransport as ki, nip05RootKey as kn, handoffAttemptAllowed as ko, TYPING_KINDS as kr, decodeInviteCode as ks, didWebDocumentUrl as kt, APP_BUNDLE_MAGIC as l, reactionIsNewer as la, traceLink as lc, DHT_TEXT_CAPABILITY as li, sparkAddressKind as ln, callSignalHeardAt as lo, COMMUNITY_TOPOLOGY as lr, verifySetUpdate as ls, ATPROTO_PROOF_SCOPE as lt, readAppBundle as m, readReply as ma, RELAY_PAYLOAD_MAX_BYTES as mc, IDENTITY_REVOCATION_LABEL as mi, isPubkyProofFolder as mn, HTTP_SERVICE_PROTO as mo, lobbyKeys as mr, enrollCancelFrame as ms, isAtprotoDid as mt, appUpdateDecision as n, COMMUNITY_REACTION_FRAME as na, turnKeys as nc, HOLD_DATED_BACK_MS as ni, assertTokenUnits as nn, decodeControl as no, SIGNAL_REPLY_MS as nr, sealHandoffPiece as ns, verifySshSignature as nt, readAppStore as o, nextReactionNumber as oa, currentRelays as oc, newMailbox as oi, walletNetworkOf as on, answersOffer as oo, mayBeHub as or, setAckFrame as os, ATPROTO_DECLARED_SCOPE as ot, isAppUrl as p, pairedReplyAuthor as pa, PacketTooLargeError as pc, IDENTITY_MAX_VALIDITY as pi, isPubkyKey as pn, emptyLinkRecords as po, freshHubs as pr, ENROLL_PROOF as ps, checkAtprotoProofRecord as pt, mentionsMember as q, DirectPathWatch as qa, voicePeaksOf as qi, parseFedimintRequestPayload as qn, newHandoffId as qo, wakeRequest as qr, classifyTurnRead as qs, ETHEREUM_USDT as qt, checkAppBeforeRun as r, GROUP_REACTION_FRAME as ra, turnName as rc, HOLD_LIMITS as ri, assertWholeSats as rn, encodeControl as ro, groupSignalFrame as rr, SET_ACK as rs, verifyBitcoinMessage as rt, signAppStore as s, queueReaction as sa, normalizeRelayUrl as sc, readManifest as si, SPARK_NETWORKS as sn, buildSdpFromSignal as so, meshHubs as sr, setUpdateFrame as ss, ATPROTO_FULL_SCOPE as st, appStoreDecision as t, readForwarded as ta, signTurnRelease as tc, HOLD_CAPABILITY as ti, WALLET_NETWORKS as tn, LIMITS as to, SIGNAL_HOLD_MS as tr, readHandoffVerified as ts, sshSignCommand as tt, appDigest as u, readReaction as ua, budgetRetryMs as uc, capsDescriptors as ui, sparkInvoiceDetails as un, extractParamsFromSdp as uo, beaconKeys as ur, ENROLL_DONE as us, atprotoDidMethod as ut, openGroupTyping as v, RUN_RESULTS as va, parseRelayPayload as vc, emptyIdentityLedger as vi, pubkyProofFile as vn, HANDOFF_CANCEL as vo, newerHead as vr, enrollGrantFrame as vs, AtprotoProofError as vt, decodeCommunityLink as w, arrivalKey as wa, MAX_AVATAR_LENGTH as wc, lastSharedWithMe as wi, dnsProofName as wn, HANDOFF_PIECE_BYTES as wo, shouldBeHub as wr, createDeviceInvite as ws, checkDidJws as wt, CommunitySession as x, checkStatusCard as xa, identityFromSeedB64 as xc, newIdentityBinding as xi, pubkyHomeserverOf as xn, HANDOFF_HELLO as xo, readBeacon as xr, readEnrollGrant as xs, DID_FILE_MAX_BYTES as xt, sealGroupTyping as y, STATUS_CARD_LIMITS as ya, createIdentity as yc, identityRevocationValue as yi, pubkyProofPath as yn, HANDOFF_DONE as yo, pickHubs as yr, enrollHelloFrame as ys, verifyAtprotoRecordProof as yt, GROUP_REACTED_FRAME as z, canonicalJson as za, safeBlobType as zi, engineText as zn, handoffHelloFrame as zo, pinNumberHolds as zr, deviceLinkPairing as zs, staticDidDocument as zt };
|
|
29095
|
+
export { parseSshPublicKey as $, signAppRevocation as $a, formatFileSize as $i, GROUP_FILE_LIMITS as $n, handoffEphemeral as $o, WAKE_CALL_INTERVAL_MS as $r, deviceKeyZ32 as $s, SEPOLIA_TEST_USDT_FAUCET as $t, entryParams as A, replySnippet as Aa, createRelayPayload as Ac, IdentityExchange as Ai, nip05RootKey as An, LEGACY_SERVICES as Ao, newerHead as Ar, enrollCancelReason as As, didWebDocumentUrl as At, GROUP_REACTED_FRAME as B, claimedTime as Ba, sanitizeAvatar as Bc, emptyProofLedger as Bi, engineText as Bn, HANDOFF_REQUEST as Bo, transferStalled as Br, deviceInviteJoinerParams as Bs, staticDidDocument as Bt, GROUP_READ_NOTE_COMMUNITY as C, reactionEmoji as Ca, setLinkTraceSink as Cc, DHT_TEXT_CAPABILITY as Ci, DOMAIN_FILE_MAX_BYTES as Cn, callRtcConfig as Co, beaconRecords as Cr, signTombstone as Cs, DID_JWS_MAX_LENGTH as Ct, KNOCK_TTL_MS as D, wireReaction as Da, withRequestOptions as Dc, IDENTITY_MAX_VALIDITY as Di, domainWellKnownFile as Dn, signalHasVideo as Do, lobbyRecords as Dr, ENROLL_HELLO as Ds, didGhostlyServiceEntry as Dt, encodeCommunityLink as E, validReactionNumber as Ea, isDiscoveryBudgetError as Ec, IDENTITY_MAX_EVIDENCE as Ei, domainTxtRecord as En, parseCallSignal as Eo, lobbyKeys as Er, ENROLL_GRANT as Es, checkDidSignature as Et, GROUP_WAKE_FRAME as F, buttonLabelClash as Fa, identityFromSeed as Fc, newIdentityBinding as Fi, txtValue as Fn, HANDOFF_HAVE as Fo, readLobby as Fr, enrollProofFrame as Fs, normalizeDid as Ft, GROUP_EDIT_TEXT_BYTES as G, APP_STATEMENT_LIMITS as Ga, toZ32 as Gc, irohRelayUrl as Gi, findEndpoint as Gn, handoffAttemptFailed as Go, typingActivity as Gr, encodeInviteCode as Gs, encodeDidDhtPacket as Gt, GroupSession as H, receivedTimestamp as Ha, fromBase64Url as Hc, TRANSPORTS as Hi, ENDPOINT as Hn, HANDOFF_VERIFIED as Ho, TYPING_REFRESH_MS as Hr, OwnInviteError as Hs, didDhtDocument as Ht, GroupWakeLimiter as I, checkStatusCard as Ia, identityFromSeedB64 as Ic, receivedIdentityStatus as Ii, wellKnownUrl as In, HANDOFF_HELLO as Io, shouldBeHub as Ir, readEnrollGrant as Is, parseDid as It, mentionsMember as J, checkAppRevokeStatement as Ja, peekDhtMailbox as Ji, parseFedimintRequestPayload as Jn, handoffCancelFrame as Jo, mayPin as Jr, DEVICES_CAPABILITY as Js, ETHEREUM_USDT as Jt, carryMentions as K, appFingerprint as Ka, utf8Decode as Kc, DHT_FILE_TOO_LARGE as Ki, isFederationId as Kn, handoffAttemptSucceeded as Ko, GROUP_PIN_FRAME as Kr, inviteLink as Ks, resolveDidDht as Kt, groupWakeFrame as L, sameButtonText as La, publicKeyFromZ32 as Lc, revokesIdentity as Li, requestLnurlInvoice as Ln, HANDOFF_OFFER as Lo, ChatFiles as Lr, readEnrollHello as Ls, parseDidDocument as Lt, knockRecords as M, ITEM_STATES as Ma, openRelayPayload as Mc, emptyIdentityLedger as Mi, recordsFromTxt as Mn, HANDOFF_ACK as Mo, rankHubs as Mr, enrollDoneFrame as Ms, didWebFileNames as Mt, mergeKnocks as N, RUN_RESULTS as Na, parseRelayPayload as Nc, identityRevocationValue as Ni, recordsFromWellKnown as Nn, HANDOFF_CANCEL as No, readBeacon as Nr, enrollGrantFrame as Ns, didWebFileUrl as Nt, decodeGroupEntryLink as O, pairedReplyAuthor as Oa, PacketTooLargeError as Oc, IDENTITY_REVOCATION_LABEL as Oi, encodeDnsQuery as On, emptyLinkRecords as Oo, mergeBeacon as Or, ENROLL_PROOF as Os, didJwsAlg as Ot, readKnocks as P, STATUS_CARD_LIMITS as Pa, createIdentity as Pc, identityStatement as Pi, recordsName as Pn, HANDOFF_DONE as Po, readBeaconHead as Pr, enrollHelloFrame as Ps, didWebHost as Pt, SshSigError as Q, signAppObject as Qa, readImageMeta as Qi, pairedMessageFrame as Qn, handoffDigest as Qo, readPin as Qr, deviceEchoNonce as Qs, SEPOLIA_TEST_USDT as Qt, groupWakes as R, statusCardText as Ra, verify as Rc, lastSharedWithMe as Ri, resolveLightningDestination as Rn, HANDOFF_PAKE as Ro, FILE_LIMITS as Rr, verifyEnrollProof as Rs, parseDidSignaturePaste as Rt, CommunitySession as S, queueReaction as Sa, normalizeRelayUrl as Sc, CapsExchange as Si, pubkyHomeserverOf as Sn, buildSdpFromSignal as So, beaconKeys as Sr, setUpdateFrame as Ss, DID_FILE_MAX_BYTES as St, decodeCommunityLink as T, readReaction as Ta, budgetRetryMs as Tc, dialDescriptors as Ti, dnsProofName as Tn, extractParamsFromSdp as To, freshHubs as Tr, ENROLL_DONE as Ts, checkDidJws as Tt, COMMUNITY_EDIT_FRAME as U, shownTime as Ua, randomBytes as Uc, automaticTransport as Ui, cashuRequestPayload as Un, filePartName as Uo, TypingSender as Ur, createChatInvite as Us, didDhtFromPublicKey as Ut, GROUP_READ_NOTE as V, heardTime as Va, bytesEqual as Vc, validExternalKey as Vi, decodeBolt11 as Vn, HANDOFF_TIMINGS as Vo, TYPING_KINDS as Vr, readDeviceInvite as Vs, strictBase64Url as Vt, GROUP_EDIT_FRAME as W, APP_PREFIXES as Wa, toBase64Url as Wc, irohDescriptor as Wi, fedimintRequestPayload as Wn, handoffAttemptAllowed as Wo, sanitizeTypingStatus as Wr, decodeInviteCode as Ws, encodeDidDhtDeactivated as Wt, SSHSIG_MAX_ARMOR as X, isAppRef as Xa, dhtFileId as Xi, INVITE_TAKEN as Xn, handoffContextProof as Xo, pinIsNewer as Xr, HANDOFF_CAPABILITY as Xs, ONCHAIN_PROVIDER as Xt, groupName as Y, isAppKey as Ya, DHT_FILE_CAPABILITY as Yi, GhostLink as Yn, handoffContext as Yo, nextPinNumber as Yr, ENROLL_CAPABILITY as Ys, EVM_TEST_CHAINS as Yt, SSHSIG_NAMESPACE as Z, readAppRevocations as Za, parseImageMeta as Zi, LIVENESS_PING_MS as Zn, handoffDataFrame as Zo, pinNumberHolds as Zr, deviceEchoFrame as Zs, PaymentPreflightError as Zt, GroupTypingBoard as _, COMMUNITY_REACTION_FRAME as _a, turnKeys as _c, HOLD_LIMITS as _i, normalizePubkyKey as _n, decodeControl as _o, MESH_HUBS as _r, sealHandoffPiece as _s, parseAtprotoDidDocument as _t, checkAppStoreIndex as a, baseMime as aa, TURN_PUT_WINDOW_MS as ac, wakeRequest as ai, formatPaymentAmount as an, rosterHas as ao, isGroupMessageId as ar, handoffRetryAfter as as, BitcoinAddressError as at, sealGroupTyping as b, ReactionWindow as ba, RelayTransport as bc, newMailbox as bi, pubkyProofPath as bn, RTC_CONFIG as bo, pickMeshHubs as br, readSetUpdate as bs, verifyAtprotoRecordProof as bt, APP_BUNDLE_LIMITS as c, voicePeaksOf as ca, classifyTurnRead as cc, chatAppId as ci, SPARK_NETWORKS as cn, DirectPathWatch as co, readGroupWant as cr, newHandoffId as cs, ATPROTO_FULL_SCOPE as ct, appViewOf as d, EDIT_RATE_WINDOW_MS as da, TURN_MAX as dc, BUTTONS_CAPABILITY as di, sparkInvoiceDetails as dn, presenceSeenAt as do, GROUP_SIGNAL_FRAME as dr, readHandoffDone as ds, atprotoDidMethod as dt, safeBlobType as ea, deviceLinkPairing as ec, WakeLimiter as ei, TEST_USDT_FAUCET_AMOUNT as en, canonicalJson as eo, GROUP_HAVE_FRAME as er, handoffHelloFrame as es, parseSshSignature as et, assembleAppBundle as f, RateWindow as fa, firstDeviceSetSecret as fc, EDIT_CAPABILITY as fi, isBitcoinAddress as fn, GhostlyHttpError as fo, SIGNAL_FRESH_MS as fr, readHandoffHave as fs, atprotoProofRecord as ft, GROUP_TYPING_FRAME as g, readForwarded as ga, signTurnRelease as gc, HOLD_DATED_BACK_MS as gi, newPubkyProofFolder as gn, LIMITS as go, readGroupSignal as gr, readHandoffVerified as gs, normalizeAtprotoHandle as gt, readAppBundle as h, forwardedAgain as ha, signTurnPacket as hc, HOLD_CAPABILITY as hi, isPubkyProofFolder as hn, webLocalFetch as ho, groupSignalFrame as hr, readHandoffTurnFrame as hs, isAtprotoDid as ht, planAppUpdate as i, VOICE_LIMITS as ia, webCryptoSigner as ic, relayRequest as ii, assertWholeSats as in, MEMBER_KEY as io, groupFileFallback as ir, handoffReleaseFrame as is, verifyBitcoinMessage as it, knockIdentity as j, BUTTON_ID as ja, newerPacket as jc, boundVerifiedIdentity as ji, normalizeDomain as jn, serviceIdFromName as jo, pickHubs as jr, enrollDigits as js, didWebFile as jt, encodeGroupEntryLink as k, readReply as ka, RELAY_PAYLOAD_MAX_BYTES as kc, IdentityCheckUnavailable as ki, isPublicAddress as kn, HTTP_SERVICE_PROTO as ko, mergeLobby as kr, enrollCancelFrame as ks, didSignedMessages as kt, APP_BUNDLE_MAGIC as l, parseVideoMeta as la, turnPutSummary as lc, isAppVersion as li, SPARK_PROVIDER as ln, EXPECT_PEER_MS as lo, readGroupWantNo as lr, readHandoffAck as ls, ATPROTO_PROOF_COLLECTION as lt, isAppUrl as m, parseLinkPreview as ma, readTurnPacket as mc, UPGRADE_CAPABILITY as mi, isPubkyKey as mn, parseLocalTarget as mo, SIGNAL_REPLY_MS as mr, readHandoffPake as ms, checkAtprotoProofRecord as mt, appUpdateDecision as n, sanitizeMime as na, newDeviceSetSecret as nc, newWakeToken as ni, WALLET_NETWORKS as nn, GROUP_MEMBER_CAP as no, GROUP_WANT_NO_FRAME as nr, handoffOfferFrame as ns, sshSignCommand as nt, readAppStore as o, fileMessageText as oa, TURN_RAISE_READ_TIMEOUT_MS as oc, checkPushEndpoint as oi, validatePaymentTarget as on, verifyCommitSignature as oo, readGroupFileMeta as or, handoffStreamKey as os, decodeAnyBitcoinAddress as ot, checkAppManifest as p, dhtEditId as pa, nextTurnPosition as pc, STATUS_CARD_CAPABILITY as pi, PUBKY_PROOF_MAX_BYTES as pn, formatLocalTarget as po, SIGNAL_HOLD_MS as pr, readHandoffHello as ps, atprotoProofRkey as pt, parseCommunityEdit as q, appRef as qa, utf8Encode as qc, DHT_MESSAGE_TTL as qi, parseCashuRequestPayload as qn, handoffBusyFrame as qo, PIN_LIMITS as qr, inviteOwnership as qs, signDidDhtPacket as qt, checkAppBeforeRun as r, PLAYABLE_AUDIO as ra, seedSigner as rc, parseWakeFrame as ri, assertTokenUnits as rn, MAX_GROUP_CHAIN as ro, fetchesByItself as rr, handoffPakeFrame as rs, verifySshSignature as rt, signAppStore as s, parseVoiceMeta as sa, TURN_SETTLE_MS as sc, vapidKeysMatch as si, walletNetworkOf as sn, edgeParams as so, readGroupHave as sr, handoffVersions as ss, ATPROTO_DECLARED_SCOPE as st, appStoreDecision as t, sanitizeFileName as ta, devicePingFrame as tc, deviceWakeRequest as ti, USDT_PUBLIC_RPC as tn, canonicalJsonBytes as to, GROUP_WANT_FRAME as tr, handoffManifestFrames as ts, sameSshKey as tt, appDigest as u, EDIT_BUFFER_MS as ua, TURN_LAST_SEQUENCE as uc, APPS_CAPABILITY as ui, sparkAddressKind as un, RELAY_POLL_INTERVALS as uo, CarriedTransport as ur, readHandoffCancel as us, ATPROTO_PROOF_SCOPE as ut, groupTypingEpoch as v, GROUP_REACTION_FRAME as va, turnName as vc, HoldKeys as vi, pubkyProofCapability as vn, encodeControl as vo, mayBeHub as vr, SET_ACK as vs, shortAtprotoDid as vt, communityHasFrame as w, reactionIsNewer as wa, traceLink as wc, capsDescriptors as wi, decodeDnsResponse as wn, callSignalHeardAt as wo, doorHubs as wr, verifySetUpdate as ws, DidError as wt, COMMUNITY_LIMITS as x, nextReactionNumber as xa, currentRelays as xc, readManifest as xi, homeserverWebEndpoint as xn, answersOffer as xo, COMMUNITY_TOPOLOGY as xr, setAckFrame as xs, DID_DOCUMENT_MAX_BYTES as xt, openGroupTyping as y, REACTION_LIMITS as ya, DEFAULT_RELAYS as yc, HoldRefusedError as yi, pubkyProofFile as yn, CALL_SIGNAL_MAX_AGE_MS as yo, meshHubs as yr, SET_UPDATE as ys, AtprotoProofError as yt, parseGroupWakeFrame as z, arrivalKey as za, MAX_AVATAR_LENGTH as zc, PeerProofs as zi, engineError as zn, HANDOFF_PIECE_BYTES as zo, transferEnded as zr, createDeviceInvite as zs, signingKeys as zt };
|
|
28558
29096
|
|
|
28559
|
-
//# sourceMappingURL=src-
|
|
29097
|
+
//# sourceMappingURL=src-Cp3o_yHN.js.map
|