@ghostlytools/cli 1.1.4 → 1.1.5
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 +7 -2
- package/dist/assets/{fileBytes-DxiJHmTk.js → fileBytes-3AQ2tJzW.js} +5 -17
- package/dist/assets/{fileBytes-DxiJHmTk.js.map → fileBytes-3AQ2tJzW.js.map} +1 -1
- package/dist/assets/{fileBytes-BQ84L9Fe.js → fileBytes-CAZDydIe.js} +2 -2
- package/dist/assets/{fileBytes-BQ84L9Fe.js.map → fileBytes-CAZDydIe.js.map} +1 -1
- package/dist/assets/{fileBytesIdb-DQkQ10PS.js → fileBytesIdb-CmG00qE-.js} +3 -3
- package/dist/assets/{fileBytesIdb-DQkQ10PS.js.map → fileBytesIdb-CmG00qE-.js.map} +1 -1
- package/dist/assets/{fileBytesOpfs-BFfR_68b.js → fileBytesOpfs-CTgqt-p3.js} +3 -3
- package/dist/assets/{fileBytesOpfs-BFfR_68b.js.map → fileBytesOpfs-CTgqt-p3.js.map} +1 -1
- package/dist/assets/{hyperdhtRelay-BoxPHp7v.js → hyperdhtRelay-w9cwVqGZ.js} +3 -3
- package/dist/assets/{hyperdhtRelay-BoxPHp7v.js.map → hyperdhtRelay-w9cwVqGZ.js.map} +1 -1
- package/dist/assets/{idb-Co2lObpU.js → idb-BIdus0WX.js} +32 -8
- package/dist/assets/idb-BIdus0WX.js.map +1 -0
- package/dist/assets/joinTrace-CO8mlhNO.js +29 -0
- package/dist/assets/joinTrace-CO8mlhNO.js.map +1 -0
- package/dist/assets/{mainline-dkA94eh2.js → mainline-BabMhm8L.js} +2 -2
- package/dist/assets/{mainline-dkA94eh2.js.map → mainline-BabMhm8L.js.map} +1 -1
- package/dist/assets/{restoredRows-C1O9FfA4.js → restoredRows-lhTKGmCO.js} +2 -2
- package/dist/assets/{restoredRows-C1O9FfA4.js.map → restoredRows-lhTKGmCO.js.map} +1 -1
- package/dist/assets/rolldown-runtime-8H4AJuhK.js +14 -0
- package/dist/assets/{server-CXFTDnFI.js → server-TamKsfmC.js} +1400 -235
- package/dist/assets/server-TamKsfmC.js.map +1 -0
- package/dist/assets/{src-K3NijMq-.js → src-C6qQYrAH.js} +1721 -106
- package/dist/assets/src-C6qQYrAH.js.map +1 -0
- package/dist/ghostly.mjs +882 -21
- package/dist/ghostly.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/assets/idb-Co2lObpU.js.map +0 -1
- package/dist/assets/server-CXFTDnFI.js.map +0 -1
- package/dist/assets/src-K3NijMq-.js.map +0 -1
|
@@ -1961,13 +1961,13 @@ function turnName(name) {
|
|
|
1961
1961
|
}
|
|
1962
1962
|
return out;
|
|
1963
1963
|
}
|
|
1964
|
-
function u32$
|
|
1964
|
+
function u32$3(value) {
|
|
1965
1965
|
return Uint8Array.of(value >>> 24 & 255, value >>> 16 & 255, value >>> 8 & 255, value & 255);
|
|
1966
1966
|
}
|
|
1967
1967
|
/** What a release signs: `"ghostly-turn-release" || turnAddress(32) || turn(4) || toKey(32) || H(32)`. */
|
|
1968
1968
|
function turnReleaseMessage(address, turn, toKey, h) {
|
|
1969
1969
|
if (address.length !== 32 || toKey.length !== 32 || h.length !== 32) throw new Error("A release names 32-byte values");
|
|
1970
|
-
return concatBytes$1(RELEASE_DOMAIN, address, u32$
|
|
1970
|
+
return concatBytes$1(RELEASE_DOMAIN, address, u32$3(turn), toKey, h);
|
|
1971
1971
|
}
|
|
1972
1972
|
/** What the author signs: `"ghostly-turn" || turnAddress(32) || body[0..310]`. */
|
|
1973
1973
|
function turnSignedMessage(address, unsigned) {
|
|
@@ -1985,8 +1985,8 @@ function encodeTurnFields(fields) {
|
|
|
1985
1985
|
if (tombstone ? fields.active !== 255 : !held(fields.active)) throw new Error(tombstone ? "A tombstone names no active device" : "The active device has no slot");
|
|
1986
1986
|
const body = /* @__PURE__ */ new Uint8Array(310);
|
|
1987
1987
|
body[OFFSET.version] = 1;
|
|
1988
|
-
body.set(u32$
|
|
1989
|
-
body.set(u32$
|
|
1988
|
+
body.set(u32$3(fields.turn), OFFSET.turn);
|
|
1989
|
+
body.set(u32$3(fields.rev).subarray(1), OFFSET.rev);
|
|
1990
1990
|
body[OFFSET.author] = fields.author;
|
|
1991
1991
|
body[OFFSET.active] = fields.active;
|
|
1992
1992
|
body[OFFSET.count] = fields.slots.filter(Boolean).length;
|
|
@@ -2238,7 +2238,7 @@ var TURN_PUT_WINDOW_MS = 1e4;
|
|
|
2238
2238
|
var TURN_RAISE_READ_TIMEOUT_MS = 5e3;
|
|
2239
2239
|
/** `T`: how long a device that raised the turn waits, from the end of its put, before the read that may make it active. `T >= P + 2V`. */
|
|
2240
2240
|
var TURN_SETTLE_MS = 3e4;
|
|
2241
|
-
function compareBytes$
|
|
2241
|
+
function compareBytes$3(a, b) {
|
|
2242
2242
|
for (let i = 0; i < Math.min(a.length, b.length); i++) if (a[i] !== b[i]) return a[i] - b[i];
|
|
2243
2243
|
return a.length - b.length;
|
|
2244
2244
|
}
|
|
@@ -2249,7 +2249,7 @@ function compareBytes$2(a, b) {
|
|
|
2249
2249
|
* reader still agrees.
|
|
2250
2250
|
*/
|
|
2251
2251
|
function keptAtEqualSequence(a, b) {
|
|
2252
|
-
return compareBytes$
|
|
2252
|
+
return compareBytes$3(a.record.instance, b.record.instance) || compareBytes$3(a.record.body, b.record.body) || compareBytes$3(a.payload, b.payload);
|
|
2253
2253
|
}
|
|
2254
2254
|
var kept = (f) => ({
|
|
2255
2255
|
record: f.read.record,
|
|
@@ -3368,15 +3368,15 @@ var VERSION_TEXT = /^[0-9A-Za-z.+_-]{1,40}$/;
|
|
|
3368
3368
|
var PART = /^(db\/[A-Za-z0-9_-]{1,64}|local|devices|file\/[A-Za-z0-9_-]{43}|wallet\/[a-z0-9-]{1,32}\/[A-Za-z0-9_.-]{1,100})$/;
|
|
3369
3369
|
/** The largest part the reader accepts: a file of WISP 501 has no limit beneath this. */
|
|
3370
3370
|
var MAX_PART = 2 ** 50;
|
|
3371
|
-
var isCount = (value, max = Number.MAX_SAFE_INTEGER) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && value <= max;
|
|
3372
|
-
var isText = (value, pattern) => typeof value === "string" && pattern.test(value);
|
|
3371
|
+
var isCount$1 = (value, max = Number.MAX_SAFE_INTEGER) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && value <= max;
|
|
3372
|
+
var isText$2 = (value, pattern) => typeof value === "string" && pattern.test(value);
|
|
3373
3373
|
var isHandoffPartName = (name) => typeof name === "string" && PART.test(name);
|
|
3374
3374
|
/** The part a file is: `file/<its SHA-256, base64url>`. */
|
|
3375
3375
|
var filePartName = (sha256B64) => `file/${sha256B64}`;
|
|
3376
3376
|
/** The digest a file part names, or null for any other part. */
|
|
3377
3377
|
var filePartDigest = (name) => name.startsWith("file/") && DIGEST$1.test(name.slice(5)) ? name.slice(5) : null;
|
|
3378
3378
|
function isPart(value) {
|
|
3379
|
-
return Array.isArray(value) && value.length === 3 && isHandoffPartName(value[0]) && isCount(value[1], MAX_PART) && isText(value[2], DIGEST$1) && (filePartDigest(value[0]) === null || filePartDigest(value[0]) === value[2]);
|
|
3379
|
+
return Array.isArray(value) && value.length === 3 && isHandoffPartName(value[0]) && isCount$1(value[1], MAX_PART) && isText$2(value[2], DIGEST$1) && (filePartDigest(value[0]) === null || filePartDigest(value[0]) === value[2]);
|
|
3380
3380
|
}
|
|
3381
3381
|
/** Names compared as UTF-8 byte strings, as `H` sorts them. */
|
|
3382
3382
|
function compareNames(a, b) {
|
|
@@ -3404,7 +3404,7 @@ function sortParts(parts) {
|
|
|
3404
3404
|
* and every part the taker already held and keeps, as `[name, size, sha256]` sorted by name. Keys are base64url.
|
|
3405
3405
|
*/
|
|
3406
3406
|
function handoffDigest(turn, fromKey, toKey, parts) {
|
|
3407
|
-
if (!isCount(turn, TURN_MAX)) throw new Error("A turn is at most 2^32 - 2");
|
|
3407
|
+
if (!isCount$1(turn, TURN_MAX)) throw new Error("A turn is at most 2^32 - 2");
|
|
3408
3408
|
if (fromKey.length !== 32 || toKey.length !== 32) throw new Error("A device signing key is 32 bytes");
|
|
3409
3409
|
for (const part of parts) if (!isPart(part)) throw new Error("A part is [name, size, sha256]");
|
|
3410
3410
|
return sha256(utf8Encode(JSON.stringify([
|
|
@@ -3418,7 +3418,7 @@ function handoffDigest(turn, fromKey, toKey, parts) {
|
|
|
3418
3418
|
/** What `handoff-verified` signs: `["ghostly-handoff-verified", turnAddress, N + 1, H]`, the bytes in base64url. */
|
|
3419
3419
|
function handoffVerifiedMessage(turnAddress, turn, h) {
|
|
3420
3420
|
if (turnAddress.length !== 32 || h.length !== 32) throw new Error("A turn address and H are 32 bytes");
|
|
3421
|
-
if (!isCount(turn, TURN_MAX)) throw new Error("A turn is at most 2^32 - 2");
|
|
3421
|
+
if (!isCount$1(turn, TURN_MAX)) throw new Error("A turn is at most 2^32 - 2");
|
|
3422
3422
|
return utf8Encode(JSON.stringify([
|
|
3423
3423
|
"ghostly-handoff-verified",
|
|
3424
3424
|
toBase64Url(turnAddress),
|
|
@@ -3470,7 +3470,7 @@ function handoffStreamKey(ownSecret, peerPublic, k, transcriptHash) {
|
|
|
3470
3470
|
* does not open there. (XSalsa20-Poly1305 takes no associated data; the header inside the box does that job.)
|
|
3471
3471
|
*/
|
|
3472
3472
|
function pieceHeader(part, offset) {
|
|
3473
|
-
if (!isCount(offset, MAX_PART)) throw new Error("A piece's offset is a whole number of bytes");
|
|
3473
|
+
if (!isCount$1(offset, MAX_PART)) throw new Error("A piece's offset is a whole number of bytes");
|
|
3474
3474
|
const header = /* @__PURE__ */ new Uint8Array(16);
|
|
3475
3475
|
header.set(sha256(utf8Encode(`ghostly-handoff-piece/1${part}`)).subarray(0, 8), 0);
|
|
3476
3476
|
new DataView(header.buffer).setBigUint64(8, BigInt(offset));
|
|
@@ -3484,7 +3484,7 @@ function sealHandoffPiece(key, part, offset, piece, nonce = randomBytes(24)) {
|
|
|
3484
3484
|
if (key.length !== 32 || nonce.length !== 24) throw new Error("A stream key is 32 bytes and a nonce 24");
|
|
3485
3485
|
return concatBytes$1(nonce, xsalsa20poly1305(key, nonce).encrypt(concatBytes$1(pieceHeader(part, offset), piece)));
|
|
3486
3486
|
}
|
|
3487
|
-
var isPins = (value) => !!value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length <= 16 && Object.entries(value).every(([type, version]) => /^[a-z0-9-]{1,32}$/.test(type) && isText(version, VERSION_TEXT));
|
|
3487
|
+
var isPins = (value) => !!value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length <= 16 && Object.entries(value).every(([type, version]) => /^[a-z0-9-]{1,32}$/.test(type) && isText$2(version, VERSION_TEXT));
|
|
3488
3488
|
function handoffHelloFrame(hello) {
|
|
3489
3489
|
return {
|
|
3490
3490
|
t: HANDOFF_HELLO,
|
|
@@ -3493,12 +3493,12 @@ function handoffHelloFrame(hello) {
|
|
|
3493
3493
|
}
|
|
3494
3494
|
function readHandoffHello(frame) {
|
|
3495
3495
|
if (frame.t !== "handoff-hello" || frame.v !== 1) return null;
|
|
3496
|
-
if (!isText(frame.e, KEY$3) || fromBase64Url(frame.e).length !== 32) return null;
|
|
3497
|
-
if (!isText(frame.app, VERSION_TEXT) || !isCount(frame.db, 1e6) || !isPins(frame.pins)) return null;
|
|
3496
|
+
if (!isText$2(frame.e, KEY$3) || fromBase64Url(frame.e).length !== 32) return null;
|
|
3497
|
+
if (!isText$2(frame.app, VERSION_TEXT) || !isCount$1(frame.db, 1e6) || !isPins(frame.pins)) return null;
|
|
3498
3498
|
if (typeof frame.room !== "number" || !Number.isSafeInteger(frame.room) || frame.room < -1) return null;
|
|
3499
3499
|
if (typeof frame.metered !== "boolean") return null;
|
|
3500
|
-
if (frame.later !== void 0 && !isCount(frame.later)) return null;
|
|
3501
|
-
if (frame.id !== void 0 && !isText(frame.id, ID$5)) return null;
|
|
3500
|
+
if (frame.later !== void 0 && !isCount$1(frame.later)) return null;
|
|
3501
|
+
if (frame.id !== void 0 && !isText$2(frame.id, ID$5)) return null;
|
|
3502
3502
|
const kind = DEVICE_KINDS.includes(frame.kind) ? frame.kind : "web";
|
|
3503
3503
|
return {
|
|
3504
3504
|
v: 1,
|
|
@@ -3524,8 +3524,8 @@ var handoffOfferFrame = (turn, id, bytes) => ({
|
|
|
3524
3524
|
});
|
|
3525
3525
|
function readHandoffTurnFrame(frame) {
|
|
3526
3526
|
if (frame.t !== "handoff-request" && frame.t !== "handoff-offer") return null;
|
|
3527
|
-
if (!isCount(frame.turn, TURN_MAX) || !isText(frame.id, ID$5)) return null;
|
|
3528
|
-
if (frame.t === "handoff-offer") return isCount(frame.bytes) ? {
|
|
3527
|
+
if (!isCount$1(frame.turn, TURN_MAX) || !isText$2(frame.id, ID$5)) return null;
|
|
3528
|
+
if (frame.t === "handoff-offer") return isCount$1(frame.bytes) ? {
|
|
3529
3529
|
turn: frame.turn,
|
|
3530
3530
|
id: frame.id,
|
|
3531
3531
|
bytes: frame.bytes
|
|
@@ -3558,8 +3558,8 @@ function readHandoffPake(frame) {
|
|
|
3558
3558
|
n: 3,
|
|
3559
3559
|
wrong: true
|
|
3560
3560
|
} : null;
|
|
3561
|
-
if (!isText(frame.m, B64$4) || !frame.m.length || frame.m.length > 4096) return null;
|
|
3562
|
-
if (frame.n === 3 && !isText(frame.c, KEY$3)) return null;
|
|
3561
|
+
if (!isText$2(frame.m, B64$4) || !frame.m.length || frame.m.length > 4096) return null;
|
|
3562
|
+
if (frame.n === 3 && !isText$2(frame.c, KEY$3)) return null;
|
|
3563
3563
|
return {
|
|
3564
3564
|
n: frame.n,
|
|
3565
3565
|
m: frame.m,
|
|
@@ -3567,12 +3567,12 @@ function readHandoffPake(frame) {
|
|
|
3567
3567
|
};
|
|
3568
3568
|
}
|
|
3569
3569
|
function readHandoffHave(frame) {
|
|
3570
|
-
if (frame.t !== "handoff-have" || !Array.isArray(frame.d) || frame.d.length > 300 || !frame.d.every((d) => isText(d, DIGEST$1))) return null;
|
|
3570
|
+
if (frame.t !== "handoff-have" || !Array.isArray(frame.d) || frame.d.length > 300 || !frame.d.every((d) => isText$2(d, DIGEST$1))) return null;
|
|
3571
3571
|
const p = {};
|
|
3572
3572
|
if (frame.p !== void 0) {
|
|
3573
3573
|
if (!frame.p || typeof frame.p !== "object" || Array.isArray(frame.p) || Object.keys(frame.p).length > 300) return null;
|
|
3574
3574
|
for (const [name, offset] of Object.entries(frame.p)) {
|
|
3575
|
-
if (!isHandoffPartName(name) || !isCount(offset, MAX_PART)) return null;
|
|
3575
|
+
if (!isHandoffPartName(name) || !isCount$1(offset, MAX_PART)) return null;
|
|
3576
3576
|
p[name] = offset;
|
|
3577
3577
|
}
|
|
3578
3578
|
}
|
|
@@ -3615,7 +3615,7 @@ var handoffDataFrame = (part, offset, sealed) => ({
|
|
|
3615
3615
|
d: toBase64Url(sealed)
|
|
3616
3616
|
});
|
|
3617
3617
|
function readHandoffAck(frame) {
|
|
3618
|
-
if (frame.t !== "handoff-ack" || !isHandoffPartName(frame.p) || !isCount(frame.o, MAX_PART)) return null;
|
|
3618
|
+
if (frame.t !== "handoff-ack" || !isHandoffPartName(frame.p) || !isCount$1(frame.o, MAX_PART)) return null;
|
|
3619
3619
|
return {
|
|
3620
3620
|
part: frame.p,
|
|
3621
3621
|
offset: frame.o
|
|
@@ -3623,7 +3623,7 @@ function readHandoffAck(frame) {
|
|
|
3623
3623
|
}
|
|
3624
3624
|
/** `H` of a `handoff-verified` that B's key signed for this turn address and turn, or null. */
|
|
3625
3625
|
function readHandoffVerified(frame, turnAddress, turn, takerKey) {
|
|
3626
|
-
if (frame.t !== "handoff-verified" || !isText(frame.h, DIGEST$1) || !isText(frame.s, SIGNATURE)) return null;
|
|
3626
|
+
if (frame.t !== "handoff-verified" || !isText$2(frame.h, DIGEST$1) || !isText$2(frame.s, SIGNATURE)) return null;
|
|
3627
3627
|
const h = fromBase64Url(frame.h);
|
|
3628
3628
|
try {
|
|
3629
3629
|
return verify(fromBase64Url(frame.s), handoffVerifiedMessage(turnAddress, turn, h), takerKey) ? h : null;
|
|
@@ -3636,7 +3636,7 @@ var handoffReleaseFrame = (release) => ({
|
|
|
3636
3636
|
...release
|
|
3637
3637
|
});
|
|
3638
3638
|
function readHandoffDone(frame) {
|
|
3639
|
-
return frame.t === "handoff-done" && isCount(frame.seq) ? frame.seq : null;
|
|
3639
|
+
return frame.t === "handoff-done" && isCount$1(frame.seq) ? frame.seq : null;
|
|
3640
3640
|
}
|
|
3641
3641
|
/**
|
|
3642
3642
|
* Whether two devices can hand over (WISP 06 § Versions), seen from the taker: a taker whose database is older than
|
|
@@ -6141,6 +6141,8 @@ var DataLink = class {
|
|
|
6141
6141
|
disconnectTimer = null;
|
|
6142
6142
|
/** The offer this side answered in the current attempt, when it took it (this clock), and how many times it answered it again. */
|
|
6143
6143
|
answered = null;
|
|
6144
|
+
/** Answers refused since a connection last opened, each followed by a new offer at once (`ANSWER_REFUSED_REDIALS`). */
|
|
6145
|
+
refusedAnswers = 0;
|
|
6144
6146
|
constructor(options) {
|
|
6145
6147
|
this.options = options;
|
|
6146
6148
|
}
|
|
@@ -6246,8 +6248,8 @@ var DataLink = class {
|
|
|
6246
6248
|
sdp: this.remoteSdp
|
|
6247
6249
|
});
|
|
6248
6250
|
if (this.pc === pc && this.state === "offering") this.setState("connecting");
|
|
6249
|
-
} catch {
|
|
6250
|
-
if (this.pc === pc) this.
|
|
6251
|
+
} catch (error) {
|
|
6252
|
+
if (this.pc === pc) this.answerRefused(error);
|
|
6251
6253
|
}
|
|
6252
6254
|
}
|
|
6253
6255
|
}
|
|
@@ -6293,6 +6295,10 @@ var DataLink = class {
|
|
|
6293
6295
|
* else goes back to idle.
|
|
6294
6296
|
*/
|
|
6295
6297
|
failed() {
|
|
6298
|
+
if (this.state === "offering" && this.remoteSdp) {
|
|
6299
|
+
this.answerRefused("failed");
|
|
6300
|
+
return;
|
|
6301
|
+
}
|
|
6296
6302
|
const answered = this.state === "connecting" ? this.answered : null;
|
|
6297
6303
|
const left = answered ? CONNECT_TIMEOUT_MS - (Date.now() - answered.at) : 0;
|
|
6298
6304
|
const evidence = this.directEvidence();
|
|
@@ -6310,6 +6316,18 @@ var DataLink = class {
|
|
|
6310
6316
|
this.reset();
|
|
6311
6317
|
if (evidence) this.options.onDirect?.(evidence);
|
|
6312
6318
|
}
|
|
6319
|
+
/** The answer to this side's offer did not go in (`ANSWER_REFUSED_REDIALS`): the attempt ends, and the caller may offer again now. */
|
|
6320
|
+
answerRefused(error) {
|
|
6321
|
+
const again = this.refusedAnswers < 4;
|
|
6322
|
+
traceLink(this.options.myPubKeyZ32, "answer-refused", {
|
|
6323
|
+
error: error instanceof Error ? error.message : String(error),
|
|
6324
|
+
again
|
|
6325
|
+
});
|
|
6326
|
+
this.reset();
|
|
6327
|
+
if (!again) return;
|
|
6328
|
+
this.refusedAnswers++;
|
|
6329
|
+
this.options.onAnswerReplaced?.();
|
|
6330
|
+
}
|
|
6313
6331
|
async answer(offer) {
|
|
6314
6332
|
this.setState("answering");
|
|
6315
6333
|
this.options.setFastPoll(true);
|
|
@@ -6388,6 +6406,7 @@ var DataLink = class {
|
|
|
6388
6406
|
if (this.pc !== pc) return;
|
|
6389
6407
|
this.clearTimers();
|
|
6390
6408
|
this.answered = null;
|
|
6409
|
+
this.refusedAnswers = 0;
|
|
6391
6410
|
this.fullGather = false;
|
|
6392
6411
|
this.setState("open");
|
|
6393
6412
|
this.options.setFastPoll(false);
|
|
@@ -6761,6 +6780,311 @@ function rememberAlone(alone, key, present) {
|
|
|
6761
6780
|
}
|
|
6762
6781
|
var GROUP_ADMIN_OFF_ERROR = "Managing this group is off on this device. Turn on Manage groups from this device first.";
|
|
6763
6782
|
//#endregion
|
|
6783
|
+
//#region ../core/src/canonicalJson.ts
|
|
6784
|
+
/** True when `s` holds a lone UTF-16 surrogate, which I-JSON (and so JCS) does not allow. */
|
|
6785
|
+
function hasLoneSurrogate(s) {
|
|
6786
|
+
for (let i = 0; i < s.length; i++) {
|
|
6787
|
+
const c = s.charCodeAt(i);
|
|
6788
|
+
if (c >= 55296 && c <= 56319) {
|
|
6789
|
+
const next = s.charCodeAt(i + 1);
|
|
6790
|
+
if (next >= 56320 && next <= 57343) {
|
|
6791
|
+
i++;
|
|
6792
|
+
continue;
|
|
6793
|
+
}
|
|
6794
|
+
return true;
|
|
6795
|
+
}
|
|
6796
|
+
if (c >= 56320 && c <= 57343) return true;
|
|
6797
|
+
}
|
|
6798
|
+
return false;
|
|
6799
|
+
}
|
|
6800
|
+
function write(value, depth) {
|
|
6801
|
+
if (depth > 64) throw new Error("JSON nested deeper than 64 levels");
|
|
6802
|
+
if (value === null) return "null";
|
|
6803
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
6804
|
+
if (typeof value === "number") {
|
|
6805
|
+
if (!Number.isFinite(value)) throw new Error("JSON numbers must be finite");
|
|
6806
|
+
return JSON.stringify(value);
|
|
6807
|
+
}
|
|
6808
|
+
if (typeof value === "string") {
|
|
6809
|
+
if (hasLoneSurrogate(value)) throw new Error("A JSON string holds a lone surrogate");
|
|
6810
|
+
return JSON.stringify(value);
|
|
6811
|
+
}
|
|
6812
|
+
if (Array.isArray(value)) return `[${value.map((v) => write(v, depth + 1)).join(",")}]`;
|
|
6813
|
+
if (typeof value === "object") {
|
|
6814
|
+
const record = value;
|
|
6815
|
+
return `{${Object.keys(value).filter((k) => record[k] !== void 0).sort((a, b) => a < b ? -1 : a > b ? 1 : 0).map((k) => {
|
|
6816
|
+
if (hasLoneSurrogate(k)) throw new Error("A JSON key holds a lone surrogate");
|
|
6817
|
+
return `${JSON.stringify(k)}:${write(record[k], depth + 1)}`;
|
|
6818
|
+
}).join(",")}}`;
|
|
6819
|
+
}
|
|
6820
|
+
throw new Error(`Not a JSON value: ${typeof value}`);
|
|
6821
|
+
}
|
|
6822
|
+
/** The canonical text of a JSON value. Throws on a value JSON cannot hold (a non-finite number, a lone surrogate). */
|
|
6823
|
+
function canonicalJson(value) {
|
|
6824
|
+
return write(value, 0);
|
|
6825
|
+
}
|
|
6826
|
+
/** The canonical UTF-8 bytes of a JSON value. */
|
|
6827
|
+
function canonicalJsonBytes(value) {
|
|
6828
|
+
return utf8Encode(canonicalJson(value));
|
|
6829
|
+
}
|
|
6830
|
+
/**
|
|
6831
|
+
* Reads bytes that must be the canonical form of a JSON value: `not-json` when they are not UTF-8 JSON, `not-canonical`
|
|
6832
|
+
* when writing the value again gives other bytes.
|
|
6833
|
+
*/
|
|
6834
|
+
function readCanonicalJson(bytes) {
|
|
6835
|
+
let value;
|
|
6836
|
+
try {
|
|
6837
|
+
value = JSON.parse(utf8Decode(bytes));
|
|
6838
|
+
} catch (error) {
|
|
6839
|
+
return {
|
|
6840
|
+
ok: false,
|
|
6841
|
+
reason: "not-json",
|
|
6842
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
6843
|
+
};
|
|
6844
|
+
}
|
|
6845
|
+
let again;
|
|
6846
|
+
try {
|
|
6847
|
+
again = canonicalJsonBytes(value);
|
|
6848
|
+
} catch (error) {
|
|
6849
|
+
return {
|
|
6850
|
+
ok: false,
|
|
6851
|
+
reason: "not-canonical",
|
|
6852
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
6853
|
+
};
|
|
6854
|
+
}
|
|
6855
|
+
if (!bytesEqual(again, bytes)) return {
|
|
6856
|
+
ok: false,
|
|
6857
|
+
reason: "not-canonical"
|
|
6858
|
+
};
|
|
6859
|
+
return {
|
|
6860
|
+
ok: true,
|
|
6861
|
+
value
|
|
6862
|
+
};
|
|
6863
|
+
}
|
|
6864
|
+
//#endregion
|
|
6865
|
+
//#region ../core/src/appStatements.ts
|
|
6866
|
+
/** The prefixes of phase 1. The others of the WISP (reports, reviews, licences, rotation, publisher proofs) come later. */
|
|
6867
|
+
var APP_PREFIXES = {
|
|
6868
|
+
app: "ghostly-app/1",
|
|
6869
|
+
store: "ghostly-store/1",
|
|
6870
|
+
revoke: "ghostly-revoke/1"
|
|
6871
|
+
};
|
|
6872
|
+
var APP_STATEMENT_LIMITS = {
|
|
6873
|
+
/** A signature statement's canonical bytes: 52 + 86 characters of values and the keys around them. */
|
|
6874
|
+
signatureBytes: 1024,
|
|
6875
|
+
/** Digests named by one revocation. */
|
|
6876
|
+
revokeDigests: 64,
|
|
6877
|
+
/** A revocation's reason, in characters. */
|
|
6878
|
+
reason: 200,
|
|
6879
|
+
/** Revocations in one `ghostly-revoke.json`. */
|
|
6880
|
+
revocations: 4096
|
|
6881
|
+
};
|
|
6882
|
+
/** A z-base32 Ed25519 public key: 52 characters that decode to 32 bytes and encode back to the same text. */
|
|
6883
|
+
function isAppKey(value) {
|
|
6884
|
+
if (typeof value !== "string" || !/^[ybndrfg8ejkmcpqxot1uwisza345h769]{52}$/.test(value)) return false;
|
|
6885
|
+
const bytes = fromZ32(value);
|
|
6886
|
+
return bytes.length === 32 && toZ32(bytes) === value;
|
|
6887
|
+
}
|
|
6888
|
+
/** A SHA-256 in base64url without padding: 43 characters that decode to 32 bytes and encode back to the same text. */
|
|
6889
|
+
function isAppHash(value) {
|
|
6890
|
+
if (typeof value !== "string" || !/^[A-Za-z0-9_-]{43}$/.test(value)) return false;
|
|
6891
|
+
return toBase64Url(fromBase64Url(value)) === value;
|
|
6892
|
+
}
|
|
6893
|
+
/** An app reference, `<publisher key>/<name>`. */
|
|
6894
|
+
var APP_NAME = /^[a-z][a-z0-9-]{0,31}$/;
|
|
6895
|
+
function isAppRef(value) {
|
|
6896
|
+
if (typeof value !== "string") return false;
|
|
6897
|
+
return value.indexOf("/") === 52 && isAppKey(value.slice(0, 52)) && APP_NAME.test(value.slice(53));
|
|
6898
|
+
}
|
|
6899
|
+
function appRef(publisher, name) {
|
|
6900
|
+
return `${publisher}/${name}`;
|
|
6901
|
+
}
|
|
6902
|
+
/** The publisher key of a reference. */
|
|
6903
|
+
function appRefPublisher(ref) {
|
|
6904
|
+
return ref.slice(0, 52);
|
|
6905
|
+
}
|
|
6906
|
+
/**
|
|
6907
|
+
* The publisher fingerprint people see: the first 16 z-base32 characters of the key (80 bits), in four groups.
|
|
6908
|
+
*/
|
|
6909
|
+
function appFingerprint(key) {
|
|
6910
|
+
return (key.slice(0, 16).match(/.{1,4}/g) ?? []).join(" ");
|
|
6911
|
+
}
|
|
6912
|
+
/** The bytes a signature covers: the prefix, a zero byte, then the SHA-256 of the object's canonical bytes. */
|
|
6913
|
+
function appSignedBytes(prefix, canonical) {
|
|
6914
|
+
const p = utf8Encode(prefix);
|
|
6915
|
+
const out = new Uint8Array(p.length + 1 + 32);
|
|
6916
|
+
out.set(p, 0);
|
|
6917
|
+
out.set(sha256(canonical), p.length + 1);
|
|
6918
|
+
return out;
|
|
6919
|
+
}
|
|
6920
|
+
/** Signs an object under a prefix: its canonical bytes and the signature statement. */
|
|
6921
|
+
async function signAppObject(prefix, value, signer) {
|
|
6922
|
+
const bytes = canonicalJsonBytes(value);
|
|
6923
|
+
const sig = await signer.sign(appSignedBytes(prefix, bytes));
|
|
6924
|
+
return {
|
|
6925
|
+
bytes,
|
|
6926
|
+
signature: {
|
|
6927
|
+
alg: "ed25519",
|
|
6928
|
+
key: toZ32(signer.publicKey),
|
|
6929
|
+
sig: toBase64Url(sig)
|
|
6930
|
+
}
|
|
6931
|
+
};
|
|
6932
|
+
}
|
|
6933
|
+
/**
|
|
6934
|
+
* Ed25519 verification as RFC 8032 writes it (`zip215: false`): canonical encodings of the point and the scalar only,
|
|
6935
|
+
* so every parser (this one, a Rust one with `verify_strict`) accepts exactly the same signatures.
|
|
6936
|
+
*/
|
|
6937
|
+
function strictVerify(signature, message, publicKey) {
|
|
6938
|
+
try {
|
|
6939
|
+
return ed25519.verify(signature, message, publicKey, { zip215: false });
|
|
6940
|
+
} catch {
|
|
6941
|
+
return false;
|
|
6942
|
+
}
|
|
6943
|
+
}
|
|
6944
|
+
var exactKeys$1 = (value, keys, optional = []) => {
|
|
6945
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
6946
|
+
const have = Object.keys(value);
|
|
6947
|
+
return keys.every((k) => have.includes(k)) && have.every((k) => keys.includes(k) || optional.includes(k));
|
|
6948
|
+
};
|
|
6949
|
+
/** Checks the shape of a signature statement: exactly `alg`, `key` and `sig`, a 64-byte signature. */
|
|
6950
|
+
function isAppSignature(value) {
|
|
6951
|
+
if (!exactKeys$1(value, [
|
|
6952
|
+
"alg",
|
|
6953
|
+
"key",
|
|
6954
|
+
"sig"
|
|
6955
|
+
])) return false;
|
|
6956
|
+
if (value.alg !== "ed25519" || !isAppKey(value.key)) return false;
|
|
6957
|
+
return typeof value.sig === "string" && /^[A-Za-z0-9_-]{86}$/.test(value.sig) && toBase64Url(fromBase64Url(value.sig)) === value.sig;
|
|
6958
|
+
}
|
|
6959
|
+
/** Reads a signature statement from its bytes, which must be canonical. */
|
|
6960
|
+
function readAppSignature(bytes) {
|
|
6961
|
+
if (bytes.length > APP_STATEMENT_LIMITS.signatureBytes) return {
|
|
6962
|
+
ok: false,
|
|
6963
|
+
reason: "bad-signature-statement",
|
|
6964
|
+
detail: "Too large"
|
|
6965
|
+
};
|
|
6966
|
+
const read = readCanonicalJson(bytes);
|
|
6967
|
+
if (!read.ok) return {
|
|
6968
|
+
ok: false,
|
|
6969
|
+
reason: "bad-signature-statement",
|
|
6970
|
+
detail: read.reason
|
|
6971
|
+
};
|
|
6972
|
+
if (!isAppSignature(read.value)) return {
|
|
6973
|
+
ok: false,
|
|
6974
|
+
reason: "bad-signature-statement",
|
|
6975
|
+
detail: "Not {alg, key, sig}"
|
|
6976
|
+
};
|
|
6977
|
+
return {
|
|
6978
|
+
ok: true,
|
|
6979
|
+
signature: read.value
|
|
6980
|
+
};
|
|
6981
|
+
}
|
|
6982
|
+
/**
|
|
6983
|
+
* Verifies a signature over an object's canonical bytes under a prefix, made by `key`: `signature-key` when the
|
|
6984
|
+
* statement names another key, `bad-signature` when it does not verify.
|
|
6985
|
+
*/
|
|
6986
|
+
function verifyAppSignature(prefix, canonical, signature, key) {
|
|
6987
|
+
if (!isAppSignature(signature)) return {
|
|
6988
|
+
ok: false,
|
|
6989
|
+
reason: "bad-signature-statement"
|
|
6990
|
+
};
|
|
6991
|
+
if (signature.key !== key) return {
|
|
6992
|
+
ok: false,
|
|
6993
|
+
reason: "signature-key"
|
|
6994
|
+
};
|
|
6995
|
+
return strictVerify(fromBase64Url(signature.sig), appSignedBytes(prefix, canonical), fromZ32(key)) ? { ok: true } : {
|
|
6996
|
+
ok: false,
|
|
6997
|
+
reason: "bad-signature"
|
|
6998
|
+
};
|
|
6999
|
+
}
|
|
7000
|
+
var isSequence = (v) => typeof v === "number" && Number.isSafeInteger(v) && v >= 1;
|
|
7001
|
+
var isPlainLine = (v, max) => typeof v === "string" && [...v].length <= max && !/\p{Cc}/u.test(v);
|
|
7002
|
+
/** Checks a revocation statement's shape; the reason it is refused, or null. */
|
|
7003
|
+
function checkAppRevokeStatement(value) {
|
|
7004
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return "Not an object";
|
|
7005
|
+
const v = value;
|
|
7006
|
+
const byDigests = "digests" in v;
|
|
7007
|
+
if (!exactKeys$1(v, [
|
|
7008
|
+
"ghostlyRevoke",
|
|
7009
|
+
"app",
|
|
7010
|
+
byDigests ? "digests" : "upTo"
|
|
7011
|
+
], ["reason"])) return "Keys are not ghostlyRevoke, app, and exactly one of digests and upTo (reason optional)";
|
|
7012
|
+
if (v.ghostlyRevoke !== 1) return "ghostlyRevoke is not 1";
|
|
7013
|
+
if (!isAppRef(v.app)) return "app is not a reference";
|
|
7014
|
+
if (byDigests) {
|
|
7015
|
+
const d = v.digests;
|
|
7016
|
+
if (!Array.isArray(d) || d.length < 1 || d.length > APP_STATEMENT_LIMITS.revokeDigests) return "digests is not 1 to 64 digests";
|
|
7017
|
+
if (!d.every(isAppHash) || new Set(d).size !== d.length) return "digests holds a bad or repeated digest";
|
|
7018
|
+
} else if (!isSequence(v.upTo)) return "upTo is not a sequence";
|
|
7019
|
+
if (v.reason !== void 0 && !isPlainLine(v.reason, APP_STATEMENT_LIMITS.reason)) return "reason is not one line of 200 characters";
|
|
7020
|
+
return null;
|
|
7021
|
+
}
|
|
7022
|
+
/** Signs a revocation with the publisher's key. */
|
|
7023
|
+
async function signAppRevocation(statement, signer) {
|
|
7024
|
+
const problem = checkAppRevokeStatement(statement);
|
|
7025
|
+
if (problem) throw new Error(`Not a revocation: ${problem}`);
|
|
7026
|
+
if (toZ32(signer.publicKey) !== appRefPublisher(statement.app)) throw new Error("A revocation is signed by its app's publisher");
|
|
7027
|
+
const { signature } = await signAppObject(APP_PREFIXES.revoke, statement, signer);
|
|
7028
|
+
return {
|
|
7029
|
+
statement,
|
|
7030
|
+
signature
|
|
7031
|
+
};
|
|
7032
|
+
}
|
|
7033
|
+
/**
|
|
7034
|
+
* Reads one signed revocation (a parsed value, from `ghostly-revoke.json` or a store index's `revoked`): its shape, then
|
|
7035
|
+
* the signature under `ghostly-revoke/1` by the publisher its `app` names.
|
|
7036
|
+
*/
|
|
7037
|
+
function readAppRevocation(value) {
|
|
7038
|
+
if (!exactKeys$1(value, ["statement", "signature"])) return {
|
|
7039
|
+
ok: false,
|
|
7040
|
+
reason: "bad-revocation",
|
|
7041
|
+
detail: "Keys are not statement and signature"
|
|
7042
|
+
};
|
|
7043
|
+
const problem = checkAppRevokeStatement(value.statement);
|
|
7044
|
+
if (problem) return {
|
|
7045
|
+
ok: false,
|
|
7046
|
+
reason: "bad-revocation",
|
|
7047
|
+
detail: problem
|
|
7048
|
+
};
|
|
7049
|
+
if (!isAppSignature(value.signature)) return {
|
|
7050
|
+
ok: false,
|
|
7051
|
+
reason: "bad-signature-statement"
|
|
7052
|
+
};
|
|
7053
|
+
const statement = value.statement;
|
|
7054
|
+
const checked = verifyAppSignature(APP_PREFIXES.revoke, canonicalJsonBytes(statement), value.signature, appRefPublisher(statement.app));
|
|
7055
|
+
if (!checked.ok) return checked;
|
|
7056
|
+
return {
|
|
7057
|
+
ok: true,
|
|
7058
|
+
revocation: {
|
|
7059
|
+
statement,
|
|
7060
|
+
signature: value.signature
|
|
7061
|
+
}
|
|
7062
|
+
};
|
|
7063
|
+
}
|
|
7064
|
+
/**
|
|
7065
|
+
* Reads `ghostly-revoke.json`: canonical bytes holding a list of signed revocations. One that does not verify refuses the
|
|
7066
|
+
* file, as a store index's `revoked` does.
|
|
7067
|
+
*/
|
|
7068
|
+
function readAppRevocations(bytes) {
|
|
7069
|
+
const read = readCanonicalJson(bytes);
|
|
7070
|
+
if (!read.ok) return read;
|
|
7071
|
+
if (!Array.isArray(read.value) || read.value.length > APP_STATEMENT_LIMITS.revocations) return {
|
|
7072
|
+
ok: false,
|
|
7073
|
+
reason: "bad-revocation",
|
|
7074
|
+
detail: "Not a list of at most 4096 revocations"
|
|
7075
|
+
};
|
|
7076
|
+
const revocations = [];
|
|
7077
|
+
for (const entry of read.value) {
|
|
7078
|
+
const one = readAppRevocation(entry);
|
|
7079
|
+
if (!one.ok) return one;
|
|
7080
|
+
revocations.push(one.revocation);
|
|
7081
|
+
}
|
|
7082
|
+
return {
|
|
7083
|
+
ok: true,
|
|
7084
|
+
revocations
|
|
7085
|
+
};
|
|
7086
|
+
}
|
|
7087
|
+
//#endregion
|
|
6764
7088
|
//#region ../core/src/messageTime.ts
|
|
6765
7089
|
/**
|
|
6766
7090
|
* How far ahead of this device's clock a number a peer made from its clock may be (a pin's number, a status card's
|
|
@@ -6832,7 +7156,10 @@ var STATUS_CARD_LIMITS = {
|
|
|
6832
7156
|
/** A button's id: what a press names. */
|
|
6833
7157
|
buttonId: 32,
|
|
6834
7158
|
/** A button's label, in characters. */
|
|
6835
|
-
buttonLabel: 40
|
|
7159
|
+
buttonLabel: 40,
|
|
7160
|
+
/** An app card's title and version, in characters (WISP 405 § An app): the manifest's bounds. */
|
|
7161
|
+
appTitle: 40,
|
|
7162
|
+
appVersion: 32
|
|
6836
7163
|
};
|
|
6837
7164
|
/** How a button looks: `primary` is the one highlighted, `danger` warns; `neutral` when none is said. */
|
|
6838
7165
|
var BUTTON_STYLES = [
|
|
@@ -6898,8 +7225,8 @@ var sameButtonText = (a, b) => a.trim().toLowerCase() === b.trim().toLowerCase()
|
|
|
6898
7225
|
* them (one line, cleaned). `at` is the button that clashes, `with` the one it clashes with.
|
|
6899
7226
|
*/
|
|
6900
7227
|
function buttonLabelClash(buttons) {
|
|
6901
|
-
const labels = buttons.map((button) => isObject$
|
|
6902
|
-
const ids = buttons.map((button) => isObject$
|
|
7228
|
+
const labels = buttons.map((button) => isObject$4(button) ? cardLine(button.label, Infinity) : void 0);
|
|
7229
|
+
const ids = buttons.map((button) => isObject$4(button) && typeof button.id === "string" ? button.id : void 0);
|
|
6903
7230
|
for (const [at, label] of labels.entries()) {
|
|
6904
7231
|
if (!label) continue;
|
|
6905
7232
|
const twin = labels.findIndex((other, i) => i < at && !!other && sameButtonText(other, label));
|
|
@@ -6916,7 +7243,13 @@ function buttonLabelClash(buttons) {
|
|
|
6916
7243
|
};
|
|
6917
7244
|
}
|
|
6918
7245
|
}
|
|
6919
|
-
|
|
7246
|
+
/** An app card's id, made from its reference: the app's name, a dot, the first 16 characters of the publisher key. */
|
|
7247
|
+
function appCardId(ref) {
|
|
7248
|
+
return `${ref.slice(53)}.${ref.slice(0, 16)}`;
|
|
7249
|
+
}
|
|
7250
|
+
/** An app card's `sequence`: a whole number from 0 to 2^53 - 1. */
|
|
7251
|
+
var appSequence = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
7252
|
+
var isObject$4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6920
7253
|
/** One line of display text: runs of space (line breaks included) become one space, then the display rules for names. */
|
|
6921
7254
|
function cardLine(value, max) {
|
|
6922
7255
|
if (typeof value !== "string") return void 0;
|
|
@@ -6945,7 +7278,7 @@ function readLinks(raw) {
|
|
|
6945
7278
|
if (!Array.isArray(raw)) return [];
|
|
6946
7279
|
const links = [];
|
|
6947
7280
|
for (const entry of raw.slice(0, STATUS_CARD_LIMITS.links)) {
|
|
6948
|
-
if (!isObject$
|
|
7281
|
+
if (!isObject$4(entry)) continue;
|
|
6949
7282
|
const url = cardUrl(entry.url);
|
|
6950
7283
|
if (!url) continue;
|
|
6951
7284
|
const label = cardLine(entry.label, STATUS_CARD_LIMITS.linkLabel);
|
|
@@ -6968,7 +7301,7 @@ function readTags(raw) {
|
|
|
6968
7301
|
return tags;
|
|
6969
7302
|
}
|
|
6970
7303
|
function readRun(raw, now) {
|
|
6971
|
-
if (!isObject$
|
|
7304
|
+
if (!isObject$4(raw)) return void 0;
|
|
6972
7305
|
const at = time(raw.at, now + MESSAGE_CLOCK_SKEW_MS), result = oneOf(RUN_RESULTS, raw.result);
|
|
6973
7306
|
if (!at || !result) return void 0;
|
|
6974
7307
|
const summary = cardLine(raw.summary, STATUS_CARD_LIMITS.line);
|
|
@@ -6993,9 +7326,26 @@ function statusCardBytes(card) {
|
|
|
6993
7326
|
* routine's next run up to a year ahead). The message is always kept: without a card it shows its text.
|
|
6994
7327
|
*/
|
|
6995
7328
|
function readStatusCard(raw, now = Date.now()) {
|
|
6996
|
-
if (!isObject$
|
|
7329
|
+
if (!isObject$4(raw) || statusCardBytes(raw) > STATUS_CARD_LIMITS.bytes) return void 0;
|
|
6997
7330
|
const id = typeof raw.id === "string" && ID$4.test(raw.id) ? raw.id : void 0;
|
|
6998
7331
|
if (!id) return void 0;
|
|
7332
|
+
if (raw.kind === "app") {
|
|
7333
|
+
if (!isAppRef(raw.ref) || id !== appCardId(raw.ref) || !isAppHash(raw.digest) || !appSequence(raw.sequence)) return void 0;
|
|
7334
|
+
const title = cardLine(raw.title, STATUS_CARD_LIMITS.appTitle);
|
|
7335
|
+
if (!title) return void 0;
|
|
7336
|
+
const version = cardLine(raw.version, STATUS_CARD_LIMITS.appVersion), url = cardUrl(raw.url);
|
|
7337
|
+
return {
|
|
7338
|
+
kind: "app",
|
|
7339
|
+
id,
|
|
7340
|
+
ref: raw.ref,
|
|
7341
|
+
digest: raw.digest,
|
|
7342
|
+
sequence: raw.sequence,
|
|
7343
|
+
title,
|
|
7344
|
+
...version && { version },
|
|
7345
|
+
...url && { url },
|
|
7346
|
+
...raw.opened === true && { opened: true }
|
|
7347
|
+
};
|
|
7348
|
+
}
|
|
6999
7349
|
const latest = now + MESSAGE_CLOCK_SKEW_MS;
|
|
7000
7350
|
const links = readLinks(raw.links);
|
|
7001
7351
|
if (raw.kind === "task") {
|
|
@@ -7006,7 +7356,7 @@ function readStatusCard(raw, now = Date.now()) {
|
|
|
7006
7356
|
if (done === void 0 || total === void 0 || done > total) done = total = void 0;
|
|
7007
7357
|
const step = cardLine(raw.step, STATUS_CARD_LIMITS.line), branch = cardLine(raw.branch, STATUS_CARD_LIMITS.branch);
|
|
7008
7358
|
let pr;
|
|
7009
|
-
if (isObject$
|
|
7359
|
+
if (isObject$4(raw.pr)) {
|
|
7010
7360
|
const url = cardUrl(raw.pr.url);
|
|
7011
7361
|
if (url) {
|
|
7012
7362
|
const number = count(raw.pr.number, 1), additions = count(raw.pr.additions), deletions = count(raw.pr.deletions), files = count(raw.pr.files);
|
|
@@ -7024,7 +7374,7 @@ function readStatusCard(raw, now = Date.now()) {
|
|
|
7024
7374
|
}
|
|
7025
7375
|
const items = [];
|
|
7026
7376
|
if (Array.isArray(raw.items)) for (const entry of raw.items.slice(0, STATUS_CARD_LIMITS.items)) {
|
|
7027
|
-
if (!isObject$
|
|
7377
|
+
if (!isObject$4(entry)) continue;
|
|
7028
7378
|
const text = cardLine(entry.text, STATUS_CARD_LIMITS.line);
|
|
7029
7379
|
if (text) items.push({
|
|
7030
7380
|
text,
|
|
@@ -7083,7 +7433,7 @@ function readStatusCard(raw, now = Date.now()) {
|
|
|
7083
7433
|
if (!Array.isArray(raw.buttons)) return void 0;
|
|
7084
7434
|
const buttons = [], seen = /* @__PURE__ */ new Set();
|
|
7085
7435
|
for (const entry of raw.buttons.slice(0, STATUS_CARD_LIMITS.buttons)) {
|
|
7086
|
-
if (!isObject$
|
|
7436
|
+
if (!isObject$4(entry) || typeof entry.id !== "string" || !BUTTON_ID.test(entry.id) || seen.has(entry.id)) continue;
|
|
7087
7437
|
const label = cardLine(entry.label, STATUS_CARD_LIMITS.buttonLabel);
|
|
7088
7438
|
if (!label) continue;
|
|
7089
7439
|
seen.add(entry.id);
|
|
@@ -7112,10 +7462,10 @@ function readStatusCard(raw, now = Date.now()) {
|
|
|
7112
7462
|
* at once and nothing it said is lost on the way. Line breaks and invisible characters are cleaned, as a reader would.
|
|
7113
7463
|
*/
|
|
7114
7464
|
function checkStatusCard(raw, now = Date.now()) {
|
|
7115
|
-
if (!isObject$
|
|
7465
|
+
if (!isObject$4(raw)) return { error: "A card is an object" };
|
|
7116
7466
|
const bytes = statusCardBytes(raw);
|
|
7117
7467
|
if (bytes > STATUS_CARD_LIMITS.bytes) return { error: `The card takes ${bytes} bytes; at most ${STATUS_CARD_LIMITS.bytes}` };
|
|
7118
|
-
if (raw.kind !== "task" && raw.kind !== "routine" && raw.kind !== "buttons") return { error: "kind is task, routine or
|
|
7468
|
+
if (raw.kind !== "task" && raw.kind !== "routine" && raw.kind !== "buttons" && raw.kind !== "app") return { error: "kind is task, routine, buttons or app" };
|
|
7119
7469
|
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 -` };
|
|
7120
7470
|
const line = (field, value, max, required = false) => {
|
|
7121
7471
|
if (value === void 0 && !required) return null;
|
|
@@ -7129,20 +7479,29 @@ function checkStatusCard(raw, now = Date.now()) {
|
|
|
7129
7479
|
if (raw.links !== void 0) {
|
|
7130
7480
|
if (!Array.isArray(raw.links) || raw.links.length > STATUS_CARD_LIMITS.links) errors.push(`links is a list of at most ${STATUS_CARD_LIMITS.links}`);
|
|
7131
7481
|
else for (const [i, link] of raw.links.entries()) {
|
|
7132
|
-
if (!isObject$
|
|
7482
|
+
if (!isObject$4(link)) {
|
|
7133
7483
|
errors.push(`links[${i}] is an object`);
|
|
7134
7484
|
continue;
|
|
7135
7485
|
}
|
|
7136
7486
|
errors.push(url(`links[${i}].url`, link.url), line(`links[${i}].label`, link.label, STATUS_CARD_LIMITS.linkLabel));
|
|
7137
7487
|
}
|
|
7138
7488
|
}
|
|
7139
|
-
if (raw.kind === "
|
|
7489
|
+
if (raw.kind === "app") {
|
|
7490
|
+
if (raw.links !== void 0) errors.push("an app card takes no links");
|
|
7491
|
+
if (!isAppRef(raw.ref)) errors.push("ref is <publisher key in z-base32>/<name>");
|
|
7492
|
+
else if (raw.id !== appCardId(raw.ref)) errors.push(`id is ${appCardId(raw.ref)}, made from ref`);
|
|
7493
|
+
if (!isAppHash(raw.digest)) errors.push("digest is a SHA-256 in base64url without padding");
|
|
7494
|
+
if (!appSequence(raw.sequence)) errors.push("sequence is a whole number from 0 to 2^53 - 1");
|
|
7495
|
+
errors.push(line("title", raw.title, STATUS_CARD_LIMITS.appTitle, true), line("version", raw.version, STATUS_CARD_LIMITS.appVersion));
|
|
7496
|
+
if (raw.url !== void 0) errors.push(url("url", raw.url));
|
|
7497
|
+
if (raw.opened !== void 0 && raw.opened !== true) errors.push("opened is true or left out");
|
|
7498
|
+
} else if (raw.kind === "buttons") {
|
|
7140
7499
|
if (raw.links !== void 0) errors.push("buttons take no links");
|
|
7141
7500
|
if (!Array.isArray(raw.buttons) || !raw.buttons.length || raw.buttons.length > STATUS_CARD_LIMITS.buttons) errors.push(`buttons is a list of 1 to ${STATUS_CARD_LIMITS.buttons}`);
|
|
7142
7501
|
else {
|
|
7143
7502
|
const ids = /* @__PURE__ */ new Set();
|
|
7144
7503
|
for (const [i, button] of raw.buttons.entries()) {
|
|
7145
|
-
if (!isObject$
|
|
7504
|
+
if (!isObject$4(button)) {
|
|
7146
7505
|
errors.push(`buttons[${i}] is an object`);
|
|
7147
7506
|
continue;
|
|
7148
7507
|
}
|
|
@@ -7169,7 +7528,7 @@ function checkStatusCard(raw, now = Date.now()) {
|
|
|
7169
7528
|
}
|
|
7170
7529
|
errors.push(line("step", raw.step, STATUS_CARD_LIMITS.line), line("branch", raw.branch, STATUS_CARD_LIMITS.branch));
|
|
7171
7530
|
if (raw.pr !== void 0) {
|
|
7172
|
-
if (!isObject$
|
|
7531
|
+
if (!isObject$4(raw.pr)) errors.push("pr is an object");
|
|
7173
7532
|
else {
|
|
7174
7533
|
errors.push(url("pr.url", raw.pr.url), whole("pr.number", raw.pr.number, 1), whole("pr.additions", raw.pr.additions), whole("pr.deletions", raw.pr.deletions), whole("pr.files", raw.pr.files));
|
|
7175
7534
|
if (raw.pr.state !== void 0 && !oneOf(PR_STATES, raw.pr.state)) errors.push(`pr.state is one of ${PR_STATES.join(", ")}`);
|
|
@@ -7201,7 +7560,7 @@ function checkStatusCard(raw, now = Date.now()) {
|
|
|
7201
7560
|
if (raw.items !== void 0) {
|
|
7202
7561
|
if (!Array.isArray(raw.items) || raw.items.length > STATUS_CARD_LIMITS.items) errors.push(`items is a list of at most ${STATUS_CARD_LIMITS.items}`);
|
|
7203
7562
|
else for (const [i, item] of raw.items.entries()) {
|
|
7204
|
-
if (!isObject$
|
|
7563
|
+
if (!isObject$4(item)) {
|
|
7205
7564
|
errors.push(`items[${i}] is an object`);
|
|
7206
7565
|
continue;
|
|
7207
7566
|
}
|
|
@@ -7215,7 +7574,7 @@ function checkStatusCard(raw, now = Date.now()) {
|
|
|
7215
7574
|
if (raw.cron !== void 0 && !(typeof raw.cron === "string" && CRON.test(raw.cron.trim()))) errors.push(`cron is a cron line of at most ${STATUS_CARD_LIMITS.cron} characters`);
|
|
7216
7575
|
if (raw.nextRunAt !== void 0 && !(typeof raw.nextRunAt === "number" && Number.isSafeInteger(raw.nextRunAt) && raw.nextRunAt > 0 && raw.nextRunAt <= now + STATUS_CARD_LIMITS.nextRunMs)) errors.push("nextRunAt is a time in milliseconds, within a year");
|
|
7217
7576
|
const run = (field, value) => {
|
|
7218
|
-
if (!isObject$
|
|
7577
|
+
if (!isObject$4(value)) return errors.push(`${field} is an object`);
|
|
7219
7578
|
if (!(typeof value.at === "number" && Number.isSafeInteger(value.at) && value.at > 0 && value.at <= now + 3e5)) errors.push(`${field}.at is a time in milliseconds, not in the future`);
|
|
7220
7579
|
if (!oneOf(RUN_RESULTS, value.result)) errors.push(`${field}.result is one of ${RUN_RESULTS.join(", ")}`);
|
|
7221
7580
|
errors.push(line(`${field}.summary`, value.summary, STATUS_CARD_LIMITS.line));
|
|
@@ -7266,6 +7625,10 @@ function cardTimeUtc(ms) {
|
|
|
7266
7625
|
* and search finds. Plain lines, the first naming the card, no bars or layout that would read badly elsewhere.
|
|
7267
7626
|
*/
|
|
7268
7627
|
function statusCardText(card) {
|
|
7628
|
+
if (card.kind === "app") {
|
|
7629
|
+
const named = `${card.title}${card.version ? ` ${card.version}` : ""}`;
|
|
7630
|
+
return [card.opened ? `🧩 Opened ${named} in this chat (Ghostly app)` : `🧩 ${named} (Ghostly app)`, ...card.url ? [card.url] : []].join("\n");
|
|
7631
|
+
}
|
|
7269
7632
|
if (card.kind === "buttons") return `Reply: ${card.buttons.map((b) => b.label).join(" / ")}`;
|
|
7270
7633
|
if (card.kind === "task") {
|
|
7271
7634
|
const progress = taskProgress(card);
|
|
@@ -9806,6 +10169,8 @@ var TransportSwitch = class {
|
|
|
9806
10169
|
settled = "";
|
|
9807
10170
|
failed = "";
|
|
9808
10171
|
preparing = false;
|
|
10172
|
+
/** The coordinator's dial for the pending plan has started (`dialing`): from then on a newer choice waits for it. */
|
|
10173
|
+
dialStarted = false;
|
|
9809
10174
|
/** This side chose while no session was open: the next one begins with it as a choice. */
|
|
9810
10175
|
apart = false;
|
|
9811
10176
|
constructor(options) {
|
|
@@ -9903,6 +10268,7 @@ var TransportSwitch = class {
|
|
|
9903
10268
|
} else if (userIntent) this.intent = Math.max(this.intent, this.remote?.intent ?? 0) + 1;
|
|
9904
10269
|
this.failed = "";
|
|
9905
10270
|
this.announce();
|
|
10271
|
+
this.supersede();
|
|
9906
10272
|
this.reconcile();
|
|
9907
10273
|
}
|
|
9908
10274
|
/**
|
|
@@ -9970,6 +10336,30 @@ var TransportSwitch = class {
|
|
|
9970
10336
|
this.timer = null;
|
|
9971
10337
|
this.plan = null;
|
|
9972
10338
|
this.preparing = false;
|
|
10339
|
+
this.dialStarted = false;
|
|
10340
|
+
}
|
|
10341
|
+
/** The coordinator starts dialling the pending plan (a move back to WebRTC first waits for the old session to retire). */
|
|
10342
|
+
dialing(plan) {
|
|
10343
|
+
if (this.plan === plan) this.dialStarted = true;
|
|
10344
|
+
}
|
|
10345
|
+
/**
|
|
10346
|
+
* A newer choice (this side's, or the contact's policy) while a plan both sides agreed has not started its dial: a
|
|
10347
|
+
* move back to WebRTC waits for the WebRTC session it left to retire (`SWITCH_RETIRE_MS`), and nothing has been tried
|
|
10348
|
+
* yet. The coordinator drops that plan, so the chat does not move there only to move again; the other side is told
|
|
10349
|
+
* 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`).
|
|
10351
|
+
*/
|
|
10352
|
+
supersede() {
|
|
10353
|
+
const plan = this.plan;
|
|
10354
|
+
if (!plan || !this.preparing || this.dialStarted || !this.context || !this.remote || this.options.key > this.options.peerKey) return;
|
|
10355
|
+
if (this.choices(this.local(), this.remote)[0] === plan.choices[0]) return;
|
|
10356
|
+
this.send({
|
|
10357
|
+
t: "paired-switch-keep",
|
|
10358
|
+
id: plan.id
|
|
10359
|
+
});
|
|
10360
|
+
this.clearPlan();
|
|
10361
|
+
this.options.cancel();
|
|
10362
|
+
this.options.state();
|
|
9973
10363
|
}
|
|
9974
10364
|
fail(error) {
|
|
9975
10365
|
const target = this.plan?.choices[0], upgrade = !!this.plan?.upgrade;
|
|
@@ -10093,6 +10483,7 @@ var TransportSwitch = class {
|
|
|
10093
10483
|
this.remote = this.lastRemote = policy;
|
|
10094
10484
|
this.options.peer(policy);
|
|
10095
10485
|
if (changed && this.plan && !this.preparing) this.clearPlan();
|
|
10486
|
+
else if (changed) this.supersede();
|
|
10096
10487
|
this.reconcile();
|
|
10097
10488
|
return true;
|
|
10098
10489
|
}
|
|
@@ -10129,6 +10520,12 @@ var TransportSwitch = class {
|
|
|
10129
10520
|
return true;
|
|
10130
10521
|
}
|
|
10131
10522
|
if (frame.t === "paired-switch-keep") {
|
|
10523
|
+
if (this.preparing && this.remote && this.choices(this.local(), this.remote)[0] !== this.plan.choices[0]) {
|
|
10524
|
+
this.clearPlan();
|
|
10525
|
+
this.options.state();
|
|
10526
|
+
this.reconcile();
|
|
10527
|
+
return true;
|
|
10528
|
+
}
|
|
10132
10529
|
if (this.actual && this.plan.choices.includes(this.actual)) {
|
|
10133
10530
|
if (this.plan.choices[0] !== this.actual && !this.plan.upgrade) this.options.kept?.(this.plan.choices[0]);
|
|
10134
10531
|
this.settled = this.signature(this.plan.local, this.plan.remote);
|
|
@@ -11282,11 +11679,17 @@ var CAPS_READ_SPACING_MS = 15e3;
|
|
|
11282
11679
|
* already knows and looks the key up behind it, and a lookup that started before the publication landed finds the old
|
|
11283
11680
|
* one; a relay may serve an old copy for a while too. The envelope names each revision once, so one read was the last
|
|
11284
11681
|
* until the next envelope, minutes away, and the chat stayed on the DHT with no descriptors to dial.
|
|
11682
|
+
*
|
|
11683
|
+
* The first ones come soon: a contact's record that lists a native transport with no descriptor is the "Peer native
|
|
11684
|
+
* address unavailable" a dial ends with, and its revision with the descriptors is usually out a second or two after the
|
|
11685
|
+
* first (its endpoints were starting). Read 5 s, then 20 s after, two Desktops DHT direct with Iroh's relay out of reach
|
|
11686
|
+
* went live 11 to 28 s after their chats opened (2026-10-07). One read more than before, three in the first 7 s.
|
|
11285
11687
|
*/
|
|
11286
11688
|
var CAPS_READ_RETRY_MS = [
|
|
11287
|
-
|
|
11689
|
+
1e3,
|
|
11690
|
+
2e3,
|
|
11691
|
+
4e3,
|
|
11288
11692
|
15e3,
|
|
11289
|
-
3e4,
|
|
11290
11693
|
6e4
|
|
11291
11694
|
];
|
|
11292
11695
|
var NATIVE = ["iroh/1", "hyperdht/1"];
|
|
@@ -11878,7 +12281,7 @@ var KINDS$1 = [
|
|
|
11878
12281
|
var WIRE_ID = /^[A-Za-z0-9_-]{8,64}$/;
|
|
11879
12282
|
var MAILBOX = /^[A-Za-z0-9_-]{22}$/;
|
|
11880
12283
|
var SIG$3 = /^[A-Za-z0-9_-]{86}$/;
|
|
11881
|
-
var MAGIC$
|
|
12284
|
+
var MAGIC$2 = utf8Encode("GHLD");
|
|
11882
12285
|
var VERSION = 1;
|
|
11883
12286
|
var NONCE$2 = 24;
|
|
11884
12287
|
/** Why a bundle, manifest or pointer was refused. Never a reason to trust it a little. */
|
|
@@ -11944,7 +12347,7 @@ var HoldKeys = class {
|
|
|
11944
12347
|
new DataView(length.buffer).setUint32(0, header.length);
|
|
11945
12348
|
const nonce = randomBytes(NONCE$2);
|
|
11946
12349
|
const box = xsalsa20poly1305(this.sealKey, nonce).encrypt(concatBytes$1(length, header, body));
|
|
11947
|
-
return concatBytes$1(MAGIC$
|
|
12350
|
+
return concatBytes$1(MAGIC$2, new Uint8Array([VERSION]), nonce, box);
|
|
11948
12351
|
}
|
|
11949
12352
|
/**
|
|
11950
12353
|
* Opens a bundle the contact sealed for me. Everything is checked before anything is returned: the
|
|
@@ -11954,11 +12357,11 @@ var HoldKeys = class {
|
|
|
11954
12357
|
open(bytes, expect = {}) {
|
|
11955
12358
|
const now = expect.now ?? Date.now();
|
|
11956
12359
|
if (bytes.length > (expect.maxBytes ?? HOLD_LIMITS.maxBundleBytes)) throw new HoldRefusedError("size", "The held item is larger than allowed");
|
|
11957
|
-
if (bytes.length < MAGIC$
|
|
11958
|
-
if (bytes[MAGIC$
|
|
12360
|
+
if (bytes.length < MAGIC$2.length + 1 + NONCE$2 + 16 + 4 || !MAGIC$2.every((b, i) => bytes[i] === b)) throw new HoldRefusedError("format", "Not a held item");
|
|
12361
|
+
if (bytes[MAGIC$2.length] !== VERSION) throw new HoldRefusedError("format", "Held item from a newer Ghostly");
|
|
11959
12362
|
let plain;
|
|
11960
12363
|
try {
|
|
11961
|
-
plain = xsalsa20poly1305(this.sealKey, bytes.subarray(MAGIC$
|
|
12364
|
+
plain = xsalsa20poly1305(this.sealKey, bytes.subarray(MAGIC$2.length + 1, MAGIC$2.length + 1 + NONCE$2)).decrypt(bytes.subarray(MAGIC$2.length + 1 + NONCE$2));
|
|
11962
12365
|
} catch {
|
|
11963
12366
|
throw new HoldRefusedError("tampered", "The held item was changed or is not for this chat");
|
|
11964
12367
|
}
|
|
@@ -12574,6 +12977,11 @@ var BUTTONS_CAPABILITY = "buttons/1";
|
|
|
12574
12977
|
* (WISP 03), so an app back after an absence knows before any session whether a relay can carry it there at once.
|
|
12575
12978
|
*/
|
|
12576
12979
|
var UPGRADE_CAPABILITY = "upgrade/1";
|
|
12980
|
+
/**
|
|
12981
|
+
* Mini-apps in a 1:1 chat: `paired-app` frames on this session, live only (WISP 1200 § In a chat: `apps/1`,
|
|
12982
|
+
* `pairedApps.ts`). Offered only while the apps feature is on.
|
|
12983
|
+
*/
|
|
12984
|
+
var APPS_CAPABILITY = "apps/1";
|
|
12577
12985
|
/** Every capability this app knows on a session: what `receive` reports changes of. */
|
|
12578
12986
|
var KNOWN_SESSION_CAPABILITIES = [
|
|
12579
12987
|
CALLS_CAPABILITY,
|
|
@@ -12586,7 +12994,8 @@ var KNOWN_SESSION_CAPABILITIES = [
|
|
|
12586
12994
|
PIN_CAPABILITY,
|
|
12587
12995
|
STATUS_CARD_CAPABILITY,
|
|
12588
12996
|
BUTTONS_CAPABILITY,
|
|
12589
|
-
UPGRADE_CAPABILITY
|
|
12997
|
+
UPGRADE_CAPABILITY,
|
|
12998
|
+
APPS_CAPABILITY
|
|
12590
12999
|
];
|
|
12591
13000
|
var SESSION_CAPABILITIES_FRAME = "paired-capabilities";
|
|
12592
13001
|
var MAX_ENTRIES$1 = 32;
|
|
@@ -12653,6 +13062,269 @@ var SessionCapabilities = class {
|
|
|
12653
13062
|
}
|
|
12654
13063
|
};
|
|
12655
13064
|
//#endregion
|
|
13065
|
+
//#region ../core/src/pairedApps.ts
|
|
13066
|
+
/**
|
|
13067
|
+
* Mini-apps talking in a 1:1 chat (WISP 1200 § In a chat: `apps/1`): the same app on both sides sends JSON to the
|
|
13068
|
+
* other over the live session, in one frame with three forms.
|
|
13069
|
+
*
|
|
13070
|
+
* {"t":"paired-app","a":"<chat app id>","o":"open","v":"1.2.0"}
|
|
13071
|
+
* {"t":"paired-app","a":"<chat app id>","o":"close"}
|
|
13072
|
+
* {"t":"paired-app","a":"<chat app id>","d":<JSON value>}
|
|
13073
|
+
*
|
|
13074
|
+
* It needs `apps/1` in `paired-capabilities` on both sides. Live only: never on the DHT, never held, never queued for
|
|
13075
|
+
* later; while the session is not live a send fails with `offline`. An `open` goes when the person opens the app in
|
|
13076
|
+
* the chat, again on an update, and again on every session that comes back while the app is open. The receiver
|
|
13077
|
+
* enforces the limits (the sender's client may be the attacker): at most `APP_DATA_MAX_BYTES` of data, at most
|
|
13078
|
+
* `APP_RATE_LIMIT` frames a second per app, data only for an app open on both sides, at most `APP_PEER_OPEN_MAX`
|
|
13079
|
+
* apps the peer has open, and nothing unless both sides offer `apps/1`.
|
|
13080
|
+
*
|
|
13081
|
+
* The chat app id names one app in one chat: the first 16 bytes of HMAC-SHA-256 keyed by the two pinned participation
|
|
13082
|
+
* keys over the app reference `<publisher key in z-base32>/<name>`. One app's id differs in each chat, so ids cannot
|
|
13083
|
+
* be matched across a person's chats; it travels only inside the encrypted session.
|
|
13084
|
+
*/
|
|
13085
|
+
var APP_FRAME = "paired-app";
|
|
13086
|
+
var DOMAIN = "ghostly-apps/1";
|
|
13087
|
+
var APP_ID = /^[A-Za-z0-9_-]{22}$/;
|
|
13088
|
+
/** Semantic versioning 2.0.0, as a manifest's `version` is. */
|
|
13089
|
+
var SEMVER$1 = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
|
|
13090
|
+
/** A chat app id as it is on the wire: 22 base64url characters (16 bytes, no padding). */
|
|
13091
|
+
function isChatAppId(value) {
|
|
13092
|
+
return typeof value === "string" && APP_ID.test(value);
|
|
13093
|
+
}
|
|
13094
|
+
/** A version an `open` may carry: semantic versioning, at most `APP_VERSION_MAX` characters. */
|
|
13095
|
+
function isAppVersion(value) {
|
|
13096
|
+
return typeof value === "string" && value.length <= 64 && SEMVER$1.test(value);
|
|
13097
|
+
}
|
|
13098
|
+
function compareBytes$2(a, b) {
|
|
13099
|
+
for (let i = 0; i < Math.min(a.length, b.length); i++) if (a[i] !== b[i]) return a[i] - b[i];
|
|
13100
|
+
return a.length - b.length;
|
|
13101
|
+
}
|
|
13102
|
+
/**
|
|
13103
|
+
* The chat app id of `ref` in the chat between these two participation keys (raw, 32 bytes each, in either order):
|
|
13104
|
+
* HMAC-SHA-256 keyed by SHA-256(`ghostly-apps/1`, 0x00, the lower key, the higher key), over the UTF-8 reference;
|
|
13105
|
+
* its first 16 bytes, base64url without padding.
|
|
13106
|
+
*/
|
|
13107
|
+
function chatAppId(keyA, keyB, ref) {
|
|
13108
|
+
if (keyA.length !== 32 || keyB.length !== 32) throw new Error("A participation key is 32 bytes");
|
|
13109
|
+
if (!isAppRef(ref)) throw new Error("Not an app reference");
|
|
13110
|
+
const [low, high] = compareBytes$2(keyA, keyB) <= 0 ? [keyA, keyB] : [keyB, keyA];
|
|
13111
|
+
const key = sha256(concatBytes$1(utf8Encode(DOMAIN), new Uint8Array([0]), low, high));
|
|
13112
|
+
return toBase64Url(hmac(sha256, key, utf8Encode(ref)).slice(0, 16));
|
|
13113
|
+
}
|
|
13114
|
+
/** The size of a value as compact UTF-8 JSON; Infinity when it is not JSON (undefined, a function, a cycle, a BigInt). */
|
|
13115
|
+
function appDataBytes(value) {
|
|
13116
|
+
let json;
|
|
13117
|
+
try {
|
|
13118
|
+
json = JSON.stringify(value);
|
|
13119
|
+
} catch {
|
|
13120
|
+
return Infinity;
|
|
13121
|
+
}
|
|
13122
|
+
return json === void 0 ? Infinity : utf8Encode(json).length;
|
|
13123
|
+
}
|
|
13124
|
+
function appOpenFrame(app, version) {
|
|
13125
|
+
if (!isChatAppId(app) || !isAppVersion(version)) throw new Error("Not an app open");
|
|
13126
|
+
return {
|
|
13127
|
+
t: APP_FRAME,
|
|
13128
|
+
a: app,
|
|
13129
|
+
o: "open",
|
|
13130
|
+
v: version
|
|
13131
|
+
};
|
|
13132
|
+
}
|
|
13133
|
+
function appCloseFrame(app) {
|
|
13134
|
+
if (!isChatAppId(app)) throw new Error("Not an app close");
|
|
13135
|
+
return {
|
|
13136
|
+
t: APP_FRAME,
|
|
13137
|
+
a: app,
|
|
13138
|
+
o: "close"
|
|
13139
|
+
};
|
|
13140
|
+
}
|
|
13141
|
+
function appDataFrame(app, data) {
|
|
13142
|
+
if (!isChatAppId(app) || appDataBytes(data) > 32768) throw new Error("Not app data");
|
|
13143
|
+
return {
|
|
13144
|
+
t: APP_FRAME,
|
|
13145
|
+
a: app,
|
|
13146
|
+
d: data
|
|
13147
|
+
};
|
|
13148
|
+
}
|
|
13149
|
+
/**
|
|
13150
|
+
* A `paired-app` frame as the peer sent it, or why a reader must refuse it: no valid `a` (22 base64url characters),
|
|
13151
|
+
* both or neither of `o` and `d`, an unknown `o`, an `open` without a valid `v`, or a `d` over `APP_DATA_MAX_BYTES`.
|
|
13152
|
+
* Other keys are ignored, so a later version can add some.
|
|
13153
|
+
*/
|
|
13154
|
+
function readAppFrame(frame) {
|
|
13155
|
+
const refuse = (refusal) => ({
|
|
13156
|
+
ok: false,
|
|
13157
|
+
refusal
|
|
13158
|
+
});
|
|
13159
|
+
if (frame?.t !== "paired-app") return refuse("not-app");
|
|
13160
|
+
if (!isChatAppId(frame.a)) return refuse("bad-a");
|
|
13161
|
+
const own = (key) => Object.prototype.hasOwnProperty.call(frame, key);
|
|
13162
|
+
const hasO = own("o"), hasD = own("d");
|
|
13163
|
+
if (hasO && hasD) return refuse("o-and-d");
|
|
13164
|
+
if (!hasO && !hasD) return refuse("no-o-or-d");
|
|
13165
|
+
if (hasD) return appDataBytes(frame.d) <= 32768 ? {
|
|
13166
|
+
ok: true,
|
|
13167
|
+
frame: {
|
|
13168
|
+
t: APP_FRAME,
|
|
13169
|
+
a: frame.a,
|
|
13170
|
+
d: frame.d
|
|
13171
|
+
}
|
|
13172
|
+
} : refuse("too-large");
|
|
13173
|
+
if (frame.o === "close") return {
|
|
13174
|
+
ok: true,
|
|
13175
|
+
frame: {
|
|
13176
|
+
t: APP_FRAME,
|
|
13177
|
+
a: frame.a,
|
|
13178
|
+
o: "close"
|
|
13179
|
+
}
|
|
13180
|
+
};
|
|
13181
|
+
if (frame.o !== "open") return refuse("unknown-o");
|
|
13182
|
+
return isAppVersion(frame.v) ? {
|
|
13183
|
+
ok: true,
|
|
13184
|
+
frame: {
|
|
13185
|
+
t: APP_FRAME,
|
|
13186
|
+
a: frame.a,
|
|
13187
|
+
o: "open",
|
|
13188
|
+
v: frame.v
|
|
13189
|
+
}
|
|
13190
|
+
} : refuse("bad-v");
|
|
13191
|
+
}
|
|
13192
|
+
/** A `paired-app` frame as the peer sent it, or null when a reader must refuse it (`readAppFrame` says why). */
|
|
13193
|
+
function parseAppFrame(frame) {
|
|
13194
|
+
const read = readAppFrame(frame);
|
|
13195
|
+
return read.ok ? read.frame : null;
|
|
13196
|
+
}
|
|
13197
|
+
/** Frames per app in a sliding window, for at most `max` apps at once. */
|
|
13198
|
+
var RateWindows = class {
|
|
13199
|
+
limit;
|
|
13200
|
+
max;
|
|
13201
|
+
windows = /* @__PURE__ */ new Map();
|
|
13202
|
+
constructor(limit, max) {
|
|
13203
|
+
this.limit = limit;
|
|
13204
|
+
this.max = max;
|
|
13205
|
+
}
|
|
13206
|
+
/** Counts one frame for `app` at `now`; false when it is over the limit (not counted then). */
|
|
13207
|
+
take(app, now) {
|
|
13208
|
+
let window = this.windows.get(app);
|
|
13209
|
+
if (!window) {
|
|
13210
|
+
if (this.windows.size >= this.max) this.prune(now);
|
|
13211
|
+
if (this.windows.size >= this.max) return false;
|
|
13212
|
+
window = [];
|
|
13213
|
+
this.windows.set(app, window);
|
|
13214
|
+
}
|
|
13215
|
+
while (window.length && now - window[0] >= 1e3) window.shift();
|
|
13216
|
+
if (window.length >= this.limit) return false;
|
|
13217
|
+
window.push(now);
|
|
13218
|
+
return true;
|
|
13219
|
+
}
|
|
13220
|
+
prune(now) {
|
|
13221
|
+
for (const [app, window] of this.windows) if (!window.length || now - window[window.length - 1] >= 1e3) this.windows.delete(app);
|
|
13222
|
+
}
|
|
13223
|
+
clear() {
|
|
13224
|
+
this.windows.clear();
|
|
13225
|
+
}
|
|
13226
|
+
};
|
|
13227
|
+
/**
|
|
13228
|
+
* The apps of one chat on its live sessions. `open` (this side's apps, by chat app id, with their versions) lasts
|
|
13229
|
+
* across sessions, so each new one says `open` again; what the peer has open lasts one session.
|
|
13230
|
+
*/
|
|
13231
|
+
var AppSessions = class {
|
|
13232
|
+
open;
|
|
13233
|
+
now;
|
|
13234
|
+
peer = /* @__PURE__ */ new Map();
|
|
13235
|
+
/** What the peer had open on a session that ended, held for `APP_RESUME_GRACE_MS` until it says so again. */
|
|
13236
|
+
held = /* @__PURE__ */ new Map();
|
|
13237
|
+
received = new RateWindows(50, 64);
|
|
13238
|
+
sent = new RateWindows(48, 64);
|
|
13239
|
+
constructor(open = /* @__PURE__ */ new Map(), now = Date.now) {
|
|
13240
|
+
this.open = open;
|
|
13241
|
+
this.now = now;
|
|
13242
|
+
}
|
|
13243
|
+
/** The peer has this app open on this session, at this version. */
|
|
13244
|
+
peerVersion(app) {
|
|
13245
|
+
return this.peer.get(app);
|
|
13246
|
+
}
|
|
13247
|
+
/** The apps the peer has open on this session. */
|
|
13248
|
+
get peerOpen() {
|
|
13249
|
+
return this.peer;
|
|
13250
|
+
}
|
|
13251
|
+
/**
|
|
13252
|
+
* A `paired-app` frame from the peer, on a session where both sides offer `apps/1`. Counted before it is read; the
|
|
13253
|
+
* event to pass on, or null when it was dropped (malformed, over the rate, a data frame for an app not open on both
|
|
13254
|
+
* sides, an `open` past `APP_PEER_OPEN_MAX`, a `close` of an app the peer did not have open).
|
|
13255
|
+
*/
|
|
13256
|
+
receive(frame) {
|
|
13257
|
+
if (!isChatAppId(frame?.a) || !this.received.take(frame.a, this.now())) return null;
|
|
13258
|
+
const parsed = parseAppFrame(frame);
|
|
13259
|
+
if (!parsed) return null;
|
|
13260
|
+
const app = parsed.a;
|
|
13261
|
+
if ("d" in parsed) return this.open.has(app) && this.peer.has(app) ? {
|
|
13262
|
+
app,
|
|
13263
|
+
d: parsed.d
|
|
13264
|
+
} : null;
|
|
13265
|
+
if (parsed.o === "close") return this.peer.delete(app) || this.held.delete(app) ? {
|
|
13266
|
+
app,
|
|
13267
|
+
o: "close"
|
|
13268
|
+
} : null;
|
|
13269
|
+
if (!this.peer.has(app) && this.peer.size >= 16) return null;
|
|
13270
|
+
this.held.delete(app);
|
|
13271
|
+
this.peer.set(app, parsed.v);
|
|
13272
|
+
return {
|
|
13273
|
+
app,
|
|
13274
|
+
o: "open",
|
|
13275
|
+
v: parsed.v
|
|
13276
|
+
};
|
|
13277
|
+
}
|
|
13278
|
+
/** Whether a data frame for `app` may go now, the session being live with `apps/1` agreed; counted when it may. */
|
|
13279
|
+
canSend(app, data) {
|
|
13280
|
+
if (!this.open.has(app)) return "not-open";
|
|
13281
|
+
if (appDataBytes(data) > 32768) return "too-large";
|
|
13282
|
+
if (!this.peer.has(app)) return "peer-closed";
|
|
13283
|
+
return this.sent.take(app, this.now()) ? null : "too-fast";
|
|
13284
|
+
}
|
|
13285
|
+
/**
|
|
13286
|
+
* The session ended, or `apps/1` is no longer agreed: what the peer had open is gone, and each such app hears a
|
|
13287
|
+
* `close` with `offline`. This side's open apps stay, and say `open` again on the next session.
|
|
13288
|
+
*/
|
|
13289
|
+
sessionEnded() {
|
|
13290
|
+
const closed = [.../* @__PURE__ */ new Set([...this.held.keys(), ...this.peer.keys()])].map((app) => ({
|
|
13291
|
+
app,
|
|
13292
|
+
o: "close",
|
|
13293
|
+
offline: true
|
|
13294
|
+
}));
|
|
13295
|
+
this.peer.clear();
|
|
13296
|
+
this.held.clear();
|
|
13297
|
+
this.received.clear();
|
|
13298
|
+
this.sent.clear();
|
|
13299
|
+
return closed;
|
|
13300
|
+
}
|
|
13301
|
+
/**
|
|
13302
|
+
* The session ended or was replaced, and another may follow at once: what the peer had open is held, not closed (no
|
|
13303
|
+
* event now). Nothing is sent to a held app; an `open` on the next session takes it back, a `close` closes it, and
|
|
13304
|
+
* `heldExpired` closes the rest once `APP_RESUME_GRACE_MS` has passed.
|
|
13305
|
+
*/
|
|
13306
|
+
sessionSuspended() {
|
|
13307
|
+
for (const [app, version] of this.peer) this.held.set(app, version);
|
|
13308
|
+
this.peer.clear();
|
|
13309
|
+
this.received.clear();
|
|
13310
|
+
this.sent.clear();
|
|
13311
|
+
}
|
|
13312
|
+
/** Whether any of the peer's apps is held, waiting for its next session. */
|
|
13313
|
+
get holding() {
|
|
13314
|
+
return this.held.size > 0;
|
|
13315
|
+
}
|
|
13316
|
+
/** The grace is over: each app still held is closed, with `offline`. */
|
|
13317
|
+
heldExpired() {
|
|
13318
|
+
const closed = [...this.held.keys()].map((app) => ({
|
|
13319
|
+
app,
|
|
13320
|
+
o: "close",
|
|
13321
|
+
offline: true
|
|
13322
|
+
}));
|
|
13323
|
+
this.held.clear();
|
|
13324
|
+
return closed;
|
|
13325
|
+
}
|
|
13326
|
+
};
|
|
13327
|
+
//#endregion
|
|
12656
13328
|
//#region ../core/src/webPush.ts
|
|
12657
13329
|
var WebPushError = class extends Error {};
|
|
12658
13330
|
var RECORD_SIZE = 4096;
|
|
@@ -13013,6 +13685,8 @@ function parsePinnedFrame(frame) {
|
|
|
13013
13685
|
* DHT, never stored, never counted as unread, no message id (older apps drop it). While the person keeps typing,
|
|
13014
13686
|
* `start` is said again at most every `TYPING_REFRESH_MS`; the receiver shows it until `stop`, a message from that
|
|
13015
13687
|
* contact, the end of the session, or `TYPING_TIMEOUT_MS` without a new `start` (so a dropped link never leaves it on).
|
|
13688
|
+
* A start standing when a session ends (a transport switch, a reconnect) is said again once the next one agreed
|
|
13689
|
+
* `typing/1`, if the person's last word is still that recent.
|
|
13016
13690
|
*
|
|
13017
13691
|
* A `start` may say what the contact is doing (`kind`: typing, recording a voice note, thinking) and, for bots, a
|
|
13018
13692
|
* short `status` line. Both are optional: an app that knows neither shows "typing…", and so does a kind it does not
|
|
@@ -13076,12 +13750,21 @@ function parseTypingActivity(frame) {
|
|
|
13076
13750
|
return typingActivity(frame?.kind, frame?.status);
|
|
13077
13751
|
}
|
|
13078
13752
|
var sameActivity = (a, b) => a?.kind === b?.kind && a?.status === b?.status;
|
|
13079
|
-
/**
|
|
13753
|
+
/**
|
|
13754
|
+
* This side's word: when to say `start` again, and whether a `stop` is owed. One per chat; what was said is per session
|
|
13755
|
+
* (`reset`), what the person is doing is not: a new session (a transport switch, a reconnect) says a standing start
|
|
13756
|
+
* again (`resume`), since the contact's reader cleared it when the old session ended.
|
|
13757
|
+
*
|
|
13758
|
+
* The rate window is not per session: the reader's is per chat too (it is not cleared when a session ends), so a
|
|
13759
|
+
* window that started afresh on each session could put a start past the reader's limit just after a switch.
|
|
13760
|
+
*/
|
|
13080
13761
|
var TypingSender = class {
|
|
13081
13762
|
now;
|
|
13082
13763
|
startedAt = 0;
|
|
13083
13764
|
standing = null;
|
|
13084
13765
|
recent = [];
|
|
13766
|
+
/** What the person is doing, and when it last said so: kept across sessions, until it stops. */
|
|
13767
|
+
wanted = null;
|
|
13085
13768
|
constructor(now = Date.now) {
|
|
13086
13769
|
this.now = now;
|
|
13087
13770
|
}
|
|
@@ -13092,6 +13775,33 @@ var TypingSender = class {
|
|
|
13092
13775
|
typing(activity = { kind: "typing" }) {
|
|
13093
13776
|
const now = this.now();
|
|
13094
13777
|
const word = typingActivity(activity.kind, activity.status);
|
|
13778
|
+
this.wanted = {
|
|
13779
|
+
activity: word,
|
|
13780
|
+
at: now
|
|
13781
|
+
};
|
|
13782
|
+
return this.say(word, now);
|
|
13783
|
+
}
|
|
13784
|
+
/**
|
|
13785
|
+
* The person typed while nothing can be said (no session, or `typing/1` not agreed on it yet): kept, so the start
|
|
13786
|
+
* goes once a session can carry it (`resume`).
|
|
13787
|
+
*/
|
|
13788
|
+
want(activity = { kind: "typing" }) {
|
|
13789
|
+
this.wanted = {
|
|
13790
|
+
activity: typingActivity(activity.kind, activity.status),
|
|
13791
|
+
at: this.now()
|
|
13792
|
+
};
|
|
13793
|
+
}
|
|
13794
|
+
/**
|
|
13795
|
+
* A new session agreed `typing/1`: the `start` to say on it when the person was still at it, its last word less than
|
|
13796
|
+
* `TYPING_TIMEOUT_MS` ago (as long as the contact would still have shown it). Null otherwise, when a start already
|
|
13797
|
+
* went on this session, or when the window is full (the person's next keystroke says it).
|
|
13798
|
+
*/
|
|
13799
|
+
resume() {
|
|
13800
|
+
const wanted = this.wanted, now = this.now();
|
|
13801
|
+
if (!wanted || this.startedAt || now - wanted.at >= 6e3) return null;
|
|
13802
|
+
return this.say(wanted.activity, now);
|
|
13803
|
+
}
|
|
13804
|
+
say(word, now) {
|
|
13095
13805
|
if (this.startedAt && now - this.startedAt < 3e3 && sameActivity(this.standing, word)) return null;
|
|
13096
13806
|
this.recent = this.recent.filter((at) => now - at < TYPING_RATE_WINDOW_MS);
|
|
13097
13807
|
if (this.recent.length >= 6) return null;
|
|
@@ -13100,8 +13810,12 @@ var TypingSender = class {
|
|
|
13100
13810
|
this.standing = word;
|
|
13101
13811
|
return typingFrame("start", word);
|
|
13102
13812
|
}
|
|
13103
|
-
/**
|
|
13813
|
+
/**
|
|
13814
|
+
* The person stopped (cleared the text, sent it, left the chat): a `stop`, only when a `start` is standing on this
|
|
13815
|
+
* session. Nothing is said again on the next one either way.
|
|
13816
|
+
*/
|
|
13104
13817
|
stopped() {
|
|
13818
|
+
this.wanted = null;
|
|
13105
13819
|
if (!this.startedAt) return null;
|
|
13106
13820
|
this.recent.push(this.now());
|
|
13107
13821
|
this.startedAt = 0;
|
|
@@ -13112,7 +13826,7 @@ var TypingSender = class {
|
|
|
13112
13826
|
get active() {
|
|
13113
13827
|
return this.startedAt !== 0;
|
|
13114
13828
|
}
|
|
13115
|
-
/** The session ended: nothing is standing on the next one. */
|
|
13829
|
+
/** The session ended: nothing is standing on the next one until `resume` (or the next keystroke) says it. */
|
|
13116
13830
|
reset() {
|
|
13117
13831
|
this.startedAt = 0;
|
|
13118
13832
|
this.standing = null;
|
|
@@ -15280,6 +15994,11 @@ var QUIET_FRAMES = /* @__PURE__ */ new Set([
|
|
|
15280
15994
|
*/
|
|
15281
15995
|
var PONG_WAIT_MS = 4e3;
|
|
15282
15996
|
/**
|
|
15997
|
+
* Back in front after an absence (`wake`), a live session that ends this soon after it was lost while the app was
|
|
15998
|
+
* away (a phone that put it to sleep): the chat resumes as after a restart, not as a session the contact closed.
|
|
15999
|
+
*/
|
|
16000
|
+
var WAKE_LOST_MS = 1e4;
|
|
16001
|
+
/**
|
|
15283
16002
|
* A `paired-message` frame. The preview (`pv`) is left out when the frame would pass `MAX_PAIRED_MESSAGE_FRAME`
|
|
15284
16003
|
* with it: the text matters, the card does not. A reply (`r`, WISP 401 § Replies) is a few hundred bytes at most
|
|
15285
16004
|
* and always goes, and so does a forwarded message's hop count (`fw`, WISP 401 § Forwards) and a status card (`sc`,
|
|
@@ -15446,6 +16165,8 @@ var GhostLink = class {
|
|
|
15446
16165
|
/** Typing on this session (`typing/1`): when to say `start` again, and the contact's word with its timeout. */
|
|
15447
16166
|
typingSender = new TypingSender();
|
|
15448
16167
|
typingReceiver = new TypingReceiver((typing, activity) => this.options.events?.onPeerTyping?.(typing, activity));
|
|
16168
|
+
/** Mini-apps on this session (`apps/1`): this side's open apps (kept across sessions), the peer's, the limits. */
|
|
16169
|
+
apps;
|
|
15449
16170
|
/** Reaction frames the contact may send per window; the rest go unconfirmed and come again. */
|
|
15450
16171
|
reactionsReceived = new ReactionWindow(REACTION_LIMITS.receive);
|
|
15451
16172
|
/** Pin frames the contact may send per window; the rest go unconfirmed and come again. */
|
|
@@ -15474,7 +16195,8 @@ var GhostLink = class {
|
|
|
15474
16195
|
racing = false;
|
|
15475
16196
|
/**
|
|
15476
16197
|
* The WebRTC offer this side's dial made last, with fallback allowed: what a transport that comes up meanwhile joins.
|
|
15477
|
-
* `resume`: the dial that made it is this side back after an absence, over WebRTC (`resuming`)
|
|
16198
|
+
* `resume`: the dial that made it is this side back after an absence, over WebRTC (`resuming`); `woke`: the absence
|
|
16199
|
+
* was the app's in the background, not a restart (`resumeAfterAbsence`).
|
|
15478
16200
|
*/
|
|
15479
16201
|
offered;
|
|
15480
16202
|
/** The attempt (its epoch) whose direct transports were knocked on as its offer went out (`knockEarly`), and whether that is under way. */
|
|
@@ -15490,6 +16212,12 @@ var GhostLink = class {
|
|
|
15490
16212
|
answeringEpoch = -1;
|
|
15491
16213
|
lastAutoConnectAt = 0;
|
|
15492
16214
|
autoConnectFailures = 0;
|
|
16215
|
+
/** The look due when a failed dial's backoff ends (`lookAtRetry`). */
|
|
16216
|
+
retryLook = null;
|
|
16217
|
+
/** This side's offer is out to the data link and has not reached the relays yet. */
|
|
16218
|
+
offerUnsent = false;
|
|
16219
|
+
/** The last dial ended with its offer never out (`offerUnsent`): the contact saw nothing of it. */
|
|
16220
|
+
dialUnseen = false;
|
|
15493
16221
|
/** Native transports a record just read listed with no way to dial them (`record-undescribed`), until one describes them again. */
|
|
15494
16222
|
undescribed = /* @__PURE__ */ new Set();
|
|
15495
16223
|
/**
|
|
@@ -15546,8 +16274,15 @@ var GhostLink = class {
|
|
|
15546
16274
|
resuming;
|
|
15547
16275
|
/** Until then, a dial waits for the native endpoint `resuming` names to start (it is knocked on as it does). */
|
|
15548
16276
|
resumeWaitUntil = 0;
|
|
16277
|
+
/** What the last live session ran on, kept after it ended: a wake resumes on it (`resumeAfterAbsence`). */
|
|
16278
|
+
lastLiveOn;
|
|
16279
|
+
/** `resuming` is a wake's, not a restart's. */
|
|
16280
|
+
resumingWoke = false;
|
|
16281
|
+
/** Until then, a session that ends was lost while the app was away (`WAKE_LOST_MS`). */
|
|
16282
|
+
wokeUntil = 0;
|
|
15549
16283
|
constructor(options) {
|
|
15550
16284
|
this.options = options;
|
|
16285
|
+
this.apps = new AppSessions(options.appsOpen);
|
|
15551
16286
|
if (options.pairing?.credentials.signer && (options.dht || options.params.deliveryMode === "dht")) throw new Error("A link that signs through a signer has no DHT delivery");
|
|
15552
16287
|
this.deliveryMode = options.params.deliveryMode ?? "stream";
|
|
15553
16288
|
this.tracker = options.pairingProgress && options.pairing && !options.pairing.credentials.peerKey && this.deliveryMode !== "dht" ? new PairingTracker(options.pairingProgress.role, options.pairingProgress.startedAt, (progress) => {
|
|
@@ -15683,9 +16418,13 @@ var GhostLink = class {
|
|
|
15683
16418
|
this.tracker?.failed("publish", true, result.error);
|
|
15684
16419
|
return;
|
|
15685
16420
|
}
|
|
15686
|
-
if (result.signalOut)
|
|
16421
|
+
if (result.signalOut) {
|
|
16422
|
+
this.offerUnsent = false;
|
|
16423
|
+
this.dataLink.signalWentOut();
|
|
16424
|
+
}
|
|
15687
16425
|
this.tracker?.published();
|
|
15688
16426
|
this.publishRecovered();
|
|
16427
|
+
this.dialSeenNow();
|
|
15689
16428
|
},
|
|
15690
16429
|
onPeerAck: (ack) => events.onPeerAck?.(ack),
|
|
15691
16430
|
onCallSignal: (signal) => {
|
|
@@ -15780,6 +16519,10 @@ var GhostLink = class {
|
|
|
15780
16519
|
if (this.activeBinding) return;
|
|
15781
16520
|
events.onDataLinkState?.(state);
|
|
15782
16521
|
if (state === "open") this.afterRtc = void 0;
|
|
16522
|
+
if (state === "offering") {
|
|
16523
|
+
this.offerUnsent = true;
|
|
16524
|
+
this.dialUnseen = false;
|
|
16525
|
+
}
|
|
15783
16526
|
if (state === "answering") {
|
|
15784
16527
|
this.answeringEpoch = this.connectionEpoch;
|
|
15785
16528
|
if (this.resumingOverRtc) {
|
|
@@ -15793,6 +16536,8 @@ var GhostLink = class {
|
|
|
15793
16536
|
this.scheduleRace(this.offered.epoch, this.offered.at);
|
|
15794
16537
|
}
|
|
15795
16538
|
if (state !== "idle") return;
|
|
16539
|
+
this.dialUnseen = was === "offering" && this.offerUnsent;
|
|
16540
|
+
this.offerUnsent = false;
|
|
15796
16541
|
const unanswerable = was === "answering" && this.answeringEpoch === this.connectionEpoch && !this.channel && !this.stopped && !this.leaving;
|
|
15797
16542
|
if (unanswerable) this.rtcUnstarted();
|
|
15798
16543
|
this.clearRace();
|
|
@@ -15805,7 +16550,10 @@ var GhostLink = class {
|
|
|
15805
16550
|
return;
|
|
15806
16551
|
}
|
|
15807
16552
|
this.afterRtc = void 0;
|
|
15808
|
-
if (!this.dialing)
|
|
16553
|
+
if (!this.dialing) {
|
|
16554
|
+
this.attemptEnded();
|
|
16555
|
+
this.lookAtRetry();
|
|
16556
|
+
}
|
|
15809
16557
|
this.rejectWaiters(new GhostlyHttpError("unreachable", "Could not connect to the peer"));
|
|
15810
16558
|
if (unanswerable && this.dialsPastRtc()) this.redial();
|
|
15811
16559
|
},
|
|
@@ -16160,15 +16908,18 @@ var GhostLink = class {
|
|
|
16160
16908
|
this.dial().catch((error) => {
|
|
16161
16909
|
this.tracker?.failed("transport", true);
|
|
16162
16910
|
this.dialFailed(error instanceof Error ? error.message : String(error));
|
|
16911
|
+
this.lookAtRetry();
|
|
16163
16912
|
});
|
|
16164
16913
|
}
|
|
16165
16914
|
async stop(announce = true) {
|
|
16166
16915
|
this.stopped = true;
|
|
16167
16916
|
this.clearRace();
|
|
16917
|
+
if (this.retryLook) clearTimeout(this.retryLook);
|
|
16168
16918
|
if (this.dhtPinGrace) clearTimeout(this.dhtPinGrace);
|
|
16169
16919
|
if (this.waiting?.timer) clearTimeout(this.waiting.timer);
|
|
16170
16920
|
await this.dht?.stop();
|
|
16171
16921
|
this.disconnect();
|
|
16922
|
+
this.appsEnded();
|
|
16172
16923
|
await Promise.allSettled([...this.endpoints.values()].map((endpoint) => endpoint.close()));
|
|
16173
16924
|
this.endpoints.clear();
|
|
16174
16925
|
await this.session.stop(announce);
|
|
@@ -16560,6 +17311,7 @@ var GhostLink = class {
|
|
|
16560
17311
|
return;
|
|
16561
17312
|
}
|
|
16562
17313
|
this.endpoints.set(endpoint.transport, endpoint);
|
|
17314
|
+
traceLink(this.myPubKeyZ32, "endpoint-up", { transport: endpoint.transport });
|
|
16563
17315
|
endpoint.onConnection = ({ channel, binding }) => {
|
|
16564
17316
|
traceLink(this.myPubKeyZ32, "dialed-in", {
|
|
16565
17317
|
transport: binding.transport,
|
|
@@ -16570,6 +17322,7 @@ var GhostLink = class {
|
|
|
16570
17322
|
};
|
|
16571
17323
|
endpoint.onUnavailable = () => {
|
|
16572
17324
|
if (this.endpoints.get(endpoint.transport) !== endpoint) return;
|
|
17325
|
+
traceLink(this.myPubKeyZ32, "endpoint-gone", { transport: endpoint.transport });
|
|
16573
17326
|
this.endpoints.delete(endpoint.transport);
|
|
16574
17327
|
this.advertiseTransports();
|
|
16575
17328
|
this.options.events?.onTransportsChanged?.();
|
|
@@ -16705,7 +17458,10 @@ var GhostLink = class {
|
|
|
16705
17458
|
* simply be away. Two dials crossing are settled by key order (see `registerEndpoint`).
|
|
16706
17459
|
*/
|
|
16707
17460
|
async knock(transport) {
|
|
16708
|
-
if (this.resuming === transport)
|
|
17461
|
+
if (this.resuming === transport) {
|
|
17462
|
+
this.resuming = void 0;
|
|
17463
|
+
this.resumingWoke = false;
|
|
17464
|
+
}
|
|
16709
17465
|
const endpoint = this.endpoints.get(transport), descriptor = this.peerDescriptors[transport];
|
|
16710
17466
|
if (!endpoint || !descriptor || this.channel || this.dialing || this.stopped || this.streamBlocked || this.keyStopped) return;
|
|
16711
17467
|
if (!this.transportOffer().includes(transport)) return;
|
|
@@ -16858,8 +17614,11 @@ var GhostLink = class {
|
|
|
16858
17614
|
const remembered = this.peerTransports && rankTransports(local, this.peerFallback ? this.peerTransports : this.peerTransports.slice(0, 1), relayed);
|
|
16859
17615
|
const ranked = remembered?.length ? remembered : this.peerTransports ? rankTransports(local, this.peerTransports, relayed) : local.filter((t) => t === "webrtc/1");
|
|
16860
17616
|
const chosen = this.switcher.chosenTarget ?? this.choiceApart?.transport ?? this.resuming;
|
|
16861
|
-
const resume = this.resuming === "webrtc/1";
|
|
16862
|
-
if (chosen && chosen === this.resuming && ranked.includes(chosen))
|
|
17617
|
+
const resume = this.resuming === "webrtc/1", woke = resume && this.resumingWoke;
|
|
17618
|
+
if (chosen && chosen === this.resuming && ranked.includes(chosen)) {
|
|
17619
|
+
this.resuming = void 0;
|
|
17620
|
+
this.resumingWoke = false;
|
|
17621
|
+
}
|
|
16863
17622
|
const choices = chosen && ranked.includes(chosen) ? [chosen, ...ranked.filter((t) => t !== chosen)] : ranked;
|
|
16864
17623
|
if (!choices.length) {
|
|
16865
17624
|
if (this.options.packetTransports && this.autoConnectFailures <= PACKET_TRANSPORTS_FAST_ATTEMPTS) this.session.expectPeer();
|
|
@@ -16883,7 +17642,8 @@ var GhostLink = class {
|
|
|
16883
17642
|
this.offered = fallback ? standing ?? {
|
|
16884
17643
|
epoch,
|
|
16885
17644
|
at: offeredAt,
|
|
16886
|
-
...resume && { resume }
|
|
17645
|
+
...resume && { resume },
|
|
17646
|
+
...woke && { woke }
|
|
16887
17647
|
} : void 0;
|
|
16888
17648
|
const gate = this.dialGate;
|
|
16889
17649
|
this.dialGate = void 0;
|
|
@@ -17070,7 +17830,10 @@ var GhostLink = class {
|
|
|
17070
17830
|
if (this.channel || this.dialing || this.stopped || this.streamBlocked || this.keyStopped || !this.contactQuiet(1e4)) return;
|
|
17071
17831
|
if (!this.fallback || !this.peerFallback) return;
|
|
17072
17832
|
this.knockedEarly = epoch;
|
|
17073
|
-
if (this.resuming === "webrtc/1")
|
|
17833
|
+
if (this.resuming === "webrtc/1") {
|
|
17834
|
+
this.resuming = void 0;
|
|
17835
|
+
this.resumingWoke = false;
|
|
17836
|
+
}
|
|
17074
17837
|
const relayed = this.upgradesFromRelay() ? [] : this.relayedTransports;
|
|
17075
17838
|
const offer = this.transportOffer();
|
|
17076
17839
|
const direct = TRANSPORTS.filter((t) => t !== "webrtc/1" && offer.includes(t) && !!this.peerTransports?.includes(t) && !relayed.includes(t) && this.canDial(t));
|
|
@@ -17093,7 +17856,7 @@ var GhostLink = class {
|
|
|
17093
17856
|
maybeKnockEarly(epoch) {
|
|
17094
17857
|
const next = this.afterRtc;
|
|
17095
17858
|
if (!this.offered?.resume || this.offered.epoch !== epoch || this.knockedEarly === epoch || next?.epoch !== epoch || epoch !== this.connectionEpoch) return;
|
|
17096
|
-
if (this.dialing || this.channel || this.stopped || !this.offerOut || !this.contactQuiet()) return;
|
|
17859
|
+
if (this.dialing || this.channel || this.stopped || !this.offerOut || !(this.offered.woke || this.contactQuiet())) return;
|
|
17097
17860
|
const relayed = this.upgradesFromRelay() ? [] : this.relayedTransports;
|
|
17098
17861
|
const direct = next.rest.filter((t) => !relayed.includes(t) && this.canDial(t));
|
|
17099
17862
|
if (!direct.length) return;
|
|
@@ -17454,6 +18217,7 @@ var GhostLink = class {
|
|
|
17454
18217
|
failures: this.autoConnectFailures,
|
|
17455
18218
|
left: wait - (Date.now() - this.lastAutoConnectAt)
|
|
17456
18219
|
});
|
|
18220
|
+
this.lookAtRetry();
|
|
17457
18221
|
return;
|
|
17458
18222
|
}
|
|
17459
18223
|
traceLink(this.myPubKeyZ32, "dial", { failures: this.autoConnectFailures });
|
|
@@ -17462,6 +18226,7 @@ var GhostLink = class {
|
|
|
17462
18226
|
this.dial().catch((error) => {
|
|
17463
18227
|
this.tracker?.failed("transport", true);
|
|
17464
18228
|
this.dialFailed(error instanceof Error ? error.message : String(error));
|
|
18229
|
+
this.lookAtRetry();
|
|
17465
18230
|
});
|
|
17466
18231
|
}
|
|
17467
18232
|
/**
|
|
@@ -17481,8 +18246,42 @@ var GhostLink = class {
|
|
|
17481
18246
|
this.dial().catch((error) => {
|
|
17482
18247
|
this.tracker?.failed("transport", true);
|
|
17483
18248
|
this.dialFailed(error instanceof Error ? error.message : String(error));
|
|
18249
|
+
this.lookAtRetry();
|
|
17484
18250
|
});
|
|
17485
18251
|
}
|
|
18252
|
+
/**
|
|
18253
|
+
* A failed dial waits `dialWait` before the next, and only a look at the contact's record dials: the next one came at
|
|
18254
|
+
* the link's own pace (30 s in the background), which knows nothing of the backoff. A member let in to a group
|
|
18255
|
+
* reached one of its members 72 s after its welcome, 40 s of backoff and 32 s more for its next look (meshSignals on
|
|
18256
|
+
* CI, 2026-10-06). One look goes when the backoff ends (`liveAttempt.retryAt`), and its read decides, as any look's
|
|
18257
|
+
* does. A dial or a reset of the backoff since makes it moot: what comes next arms its own.
|
|
18258
|
+
*/
|
|
18259
|
+
lookAtRetry() {
|
|
18260
|
+
if (this.retryLook) clearTimeout(this.retryLook);
|
|
18261
|
+
this.retryLook = null;
|
|
18262
|
+
const from = this.lastAutoConnectAt;
|
|
18263
|
+
if (!from || this.stopped || this.leaving || this.streamBlocked || this.keyStopped || !this.options.autoConnect) return;
|
|
18264
|
+
this.retryLook = setTimeout(() => {
|
|
18265
|
+
this.retryLook = null;
|
|
18266
|
+
if (this.lastAutoConnectAt !== from || this.stopped || this.leaving || this.yielding || this.streamBlocked || this.keyStopped || this.channel || this.dialing || this.dataLink.state !== "idle") return;
|
|
18267
|
+
traceLink(this.myPubKeyZ32, "retry-look", { failures: this.autoConnectFailures });
|
|
18268
|
+
this.session.lookNow();
|
|
18269
|
+
}, Math.max(0, from + this.dialWait() - Date.now()));
|
|
18270
|
+
}
|
|
18271
|
+
/**
|
|
18272
|
+
* A packet of this link reached the relays after a dial whose offer never did (the relays failed, or their budget held
|
|
18273
|
+
* it, for the whole attempt): the contact saw nothing of that dial, so it is no reason to wait, and the next goes now.
|
|
18274
|
+
* Without this a dial made during an outage set the wait, three minutes once it had grown, from when the relays
|
|
18275
|
+
* answered again.
|
|
18276
|
+
*/
|
|
18277
|
+
dialSeenNow() {
|
|
18278
|
+
if (!this.dialUnseen) return;
|
|
18279
|
+
this.dialUnseen = false;
|
|
18280
|
+
if (this.channel || this.dialing || this.dataLink.state !== "idle") return;
|
|
18281
|
+
traceLink(this.myPubKeyZ32, "dial-unseen", { failures: this.autoConnectFailures });
|
|
18282
|
+
this.lastAutoConnectAt = 0;
|
|
18283
|
+
this.maybeAutoConnect(this.presence);
|
|
18284
|
+
}
|
|
17486
18285
|
/** How long after the last automatic dial the next one may go. A first pairing tries again sooner: the contact just read the invite and is waiting. */
|
|
17487
18286
|
dialWait() {
|
|
17488
18287
|
return !!this.tracker && !this.tracker.done && this.tracker.progress.stage !== "on-dht" ? Math.min(PAIRING_RETRY_MS * 2 ** this.autoConnectFailures, PAIRING_MAX_RETRY_MS) : Math.min(AUTO_CONNECT_RETRY_MS * 2 ** this.autoConnectFailures, AUTO_CONNECT_MAX_RETRY_MS);
|
|
@@ -17729,6 +18528,7 @@ var GhostLink = class {
|
|
|
17729
18528
|
if (this.options.statusCardSupport) offered.push(STATUS_CARD_CAPABILITY);
|
|
17730
18529
|
if (this.options.buttonsSupport) offered.push(BUTTONS_CAPABILITY);
|
|
17731
18530
|
if (this.options.upgradeSupport) offered.push(UPGRADE_CAPABILITY);
|
|
18531
|
+
if (this.options.appsSupport) offered.push(APPS_CAPABILITY);
|
|
17732
18532
|
if (this.options.deviceCapabilities) offered.push(...this.options.deviceCapabilities);
|
|
17733
18533
|
return offered;
|
|
17734
18534
|
}
|
|
@@ -17873,12 +18673,90 @@ var GhostLink = class {
|
|
|
17873
18673
|
*/
|
|
17874
18674
|
setTyping(typing, activity) {
|
|
17875
18675
|
if (!this.options.params.profile) return;
|
|
17876
|
-
|
|
18676
|
+
if (typing && !this.supportsTyping) {
|
|
18677
|
+
this.typingSender.want(activity);
|
|
18678
|
+
return;
|
|
18679
|
+
}
|
|
18680
|
+
const frame = typing ? this.typingSender.typing(activity) : this.typingSender.stopped();
|
|
17877
18681
|
if (!frame || !this.channel || !this.supportsTyping) return;
|
|
17878
18682
|
try {
|
|
17879
18683
|
this.channel.send(JSON.stringify(frame));
|
|
17880
18684
|
} catch {}
|
|
17881
18685
|
}
|
|
18686
|
+
resumeTyping() {
|
|
18687
|
+
const frame = this.typingSender.resume();
|
|
18688
|
+
if (!frame || !this.channel) return;
|
|
18689
|
+
try {
|
|
18690
|
+
this.channel.send(JSON.stringify(frame));
|
|
18691
|
+
} catch {}
|
|
18692
|
+
}
|
|
18693
|
+
/** Both sides offer `apps/1` on the open session: mini-apps can talk (WISP 1200 § In a chat). */
|
|
18694
|
+
get supportsApps() {
|
|
18695
|
+
return !!this.options.params.profile && this.isDataLinkOpen && this.sessionCapabilities.agreed("apps/1");
|
|
18696
|
+
}
|
|
18697
|
+
/** The apps the contact has open on this session (chat app id to version). */
|
|
18698
|
+
get peerApps() {
|
|
18699
|
+
return this.apps.peerOpen;
|
|
18700
|
+
}
|
|
18701
|
+
/**
|
|
18702
|
+
* This side opened an app in this chat (or updated it): `open` goes now when both sides offer `apps/1` on the live
|
|
18703
|
+
* session, and again on every session that comes back while the app stays open. Never on the DHT.
|
|
18704
|
+
*/
|
|
18705
|
+
openApp(app, version) {
|
|
18706
|
+
const frame = JSON.stringify(appOpenFrame(app, version));
|
|
18707
|
+
this.apps.open.set(app, version);
|
|
18708
|
+
if (this.supportsApps && this.channel) try {
|
|
18709
|
+
this.channel.send(frame);
|
|
18710
|
+
} catch {}
|
|
18711
|
+
}
|
|
18712
|
+
/** This side closed an app in this chat: `close` goes now when it can; nothing is said later. */
|
|
18713
|
+
closeApp(app) {
|
|
18714
|
+
const frame = JSON.stringify(appCloseFrame(app));
|
|
18715
|
+
if (!this.apps.open.delete(app)) return;
|
|
18716
|
+
if (this.supportsApps && this.channel) try {
|
|
18717
|
+
this.channel.send(frame);
|
|
18718
|
+
} catch {}
|
|
18719
|
+
}
|
|
18720
|
+
/**
|
|
18721
|
+
* One data frame of an app open on both sides, on the live session only. An error when it did not go: nothing is
|
|
18722
|
+
* kept to send later.
|
|
18723
|
+
*/
|
|
18724
|
+
sendAppData(app, data) {
|
|
18725
|
+
if (!this.apps.open.has(app)) return "not-open";
|
|
18726
|
+
if (appDataBytes(data) > 32768) return "too-large";
|
|
18727
|
+
if (!this.channel || !this.isDataLinkOpen) return "offline";
|
|
18728
|
+
if (!this.supportsApps) return "peer-closed";
|
|
18729
|
+
const refused = this.apps.canSend(app, data);
|
|
18730
|
+
if (refused) return refused;
|
|
18731
|
+
try {
|
|
18732
|
+
this.channel.send(JSON.stringify(appDataFrame(app, data)));
|
|
18733
|
+
return null;
|
|
18734
|
+
} catch {
|
|
18735
|
+
return "offline";
|
|
18736
|
+
}
|
|
18737
|
+
}
|
|
18738
|
+
/** What the contact had open is gone (`apps/1` is no longer agreed, or this link stopped): each app hears it now. */
|
|
18739
|
+
appsEnded() {
|
|
18740
|
+
if (this.appsGrace) {
|
|
18741
|
+
clearTimeout(this.appsGrace);
|
|
18742
|
+
this.appsGrace = null;
|
|
18743
|
+
}
|
|
18744
|
+
for (const event of this.apps.sessionEnded()) this.options.events?.onAppFrame?.(event);
|
|
18745
|
+
}
|
|
18746
|
+
appsGrace = null;
|
|
18747
|
+
/**
|
|
18748
|
+
* The session ended or a new one took its place (a transport switch, a resume, a re-dial): what the contact had open
|
|
18749
|
+
* is held for `APP_RESUME_GRACE_MS` (WISP 1200 § In a chat). The next session's `open` takes each back, and the app
|
|
18750
|
+
* hears only that open, not a close first; one not said again by then is closed with `offline`.
|
|
18751
|
+
*/
|
|
18752
|
+
appsSuspended() {
|
|
18753
|
+
this.apps.sessionSuspended();
|
|
18754
|
+
if (!this.apps.holding || this.appsGrace) return;
|
|
18755
|
+
this.appsGrace = setTimeout(() => {
|
|
18756
|
+
this.appsGrace = null;
|
|
18757
|
+
for (const event of this.apps.heldExpired()) this.options.events?.onAppFrame?.(event);
|
|
18758
|
+
}, this.options.appResumeGraceMs ?? 1e4);
|
|
18759
|
+
}
|
|
17882
18760
|
/** Whether files/3 was agreed on the session open now; kept to say when that changes. */
|
|
17883
18761
|
filesOpen = false;
|
|
17884
18762
|
emitFilesSession(again = false) {
|
|
@@ -17951,15 +18829,24 @@ var GhostLink = class {
|
|
|
17951
18829
|
this.channel.send(JSON.stringify(pending));
|
|
17952
18830
|
} catch {}
|
|
17953
18831
|
}
|
|
17954
|
-
if (changed.includes("typing/1")
|
|
17955
|
-
this.
|
|
17956
|
-
|
|
18832
|
+
if (changed.includes("typing/1")) {
|
|
18833
|
+
if (!this.supportsTyping) {
|
|
18834
|
+
this.typingSender.reset();
|
|
18835
|
+
this.typingReceiver.clear();
|
|
18836
|
+
} else this.resumeTyping();
|
|
17957
18837
|
}
|
|
17958
18838
|
if (changed.includes("react/1")) this.options.events?.onReactionsSupport?.(this.supportsReactions);
|
|
17959
18839
|
if (changed.includes("edit/1")) this.options.events?.onEditSupport?.(this.supportsEdits);
|
|
17960
18840
|
if (changed.includes("pin/1")) this.options.events?.onPinSupport?.(this.supportsPins);
|
|
17961
18841
|
if (changed.includes("wake/1")) this.options.events?.onWakeSupport?.(this.supportsWake);
|
|
17962
18842
|
if (changed.includes("upgrade/1")) this.switcher.reconsider();
|
|
18843
|
+
if (changed.includes("apps/1")) {
|
|
18844
|
+
if (this.supportsApps && this.channel) for (const [app, version] of this.apps.open) try {
|
|
18845
|
+
this.channel.send(JSON.stringify(appOpenFrame(app, version)));
|
|
18846
|
+
} catch {}
|
|
18847
|
+
if (!this.supportsApps) this.appsEnded();
|
|
18848
|
+
this.options.events?.onAppsSupport?.(this.supportsApps);
|
|
18849
|
+
}
|
|
17963
18850
|
const devices = this.options.deviceCapabilities;
|
|
17964
18851
|
if (devices && changed.some((capability) => devices.includes(capability))) this.options.events?.onDeviceCapabilities?.(devices.filter((capability) => this.supportsDevice(capability)));
|
|
17965
18852
|
this.emitPairingState();
|
|
@@ -18059,7 +18946,9 @@ var GhostLink = class {
|
|
|
18059
18946
|
if (transport === "webrtc/1") {
|
|
18060
18947
|
this.rtcCandidateWaiter = done;
|
|
18061
18948
|
this.rtcRetired().then(() => {
|
|
18062
|
-
if (active
|
|
18949
|
+
if (!active || epoch !== this.candidateEpoch || this.switcher.pending !== plan) return;
|
|
18950
|
+
this.switcher.dialing(plan);
|
|
18951
|
+
return this.dataLink.connect();
|
|
18063
18952
|
}).catch(done.reject);
|
|
18064
18953
|
} else {
|
|
18065
18954
|
const endpoint = this.endpoints.get(transport), descriptor = plan.remote.descriptors[transport];
|
|
@@ -18067,6 +18956,7 @@ var GhostLink = class {
|
|
|
18067
18956
|
done.reject(/* @__PURE__ */ new Error("Peer native address unavailable"));
|
|
18068
18957
|
return;
|
|
18069
18958
|
}
|
|
18959
|
+
this.switcher.dialing(plan);
|
|
18070
18960
|
endpoint.connect(descriptor).then(({ channel, binding }) => {
|
|
18071
18961
|
if (!active || epoch !== this.candidateEpoch || this.switcher.pending !== plan) {
|
|
18072
18962
|
channel.close();
|
|
@@ -18349,6 +19239,7 @@ var GhostLink = class {
|
|
|
18349
19239
|
this.sessionCapabilities.reset();
|
|
18350
19240
|
this.typingSender.reset();
|
|
18351
19241
|
this.typingReceiver.clear();
|
|
19242
|
+
this.appsSuspended();
|
|
18352
19243
|
this.pairedHttp?.close();
|
|
18353
19244
|
this.pairedHttp = new PairedHttp(channel, this.options.getHostedHttpService, this.options.localFetch);
|
|
18354
19245
|
this.emitPairingState();
|
|
@@ -18529,6 +19420,12 @@ var GhostLink = class {
|
|
|
18529
19420
|
if (this.supportsTyping) this.typingReceiver.receive(frame);
|
|
18530
19421
|
return;
|
|
18531
19422
|
}
|
|
19423
|
+
if (frame?.t === "paired-app") {
|
|
19424
|
+
if (!this.supportsApps) return;
|
|
19425
|
+
const event = this.apps.receive(frame);
|
|
19426
|
+
if (event) this.options.events?.onAppFrame?.(event);
|
|
19427
|
+
return;
|
|
19428
|
+
}
|
|
18532
19429
|
if (frame?.t === "paired-reaction") {
|
|
18533
19430
|
if (!this.supportsReactions || !this.reactionsReceived.take()) return;
|
|
18534
19431
|
const reaction = parseReactionFrame(frame);
|
|
@@ -18802,6 +19699,10 @@ var GhostLink = class {
|
|
|
18802
19699
|
peerLost(why) {
|
|
18803
19700
|
if (this.stopped || this.leaving || this.yielding) return;
|
|
18804
19701
|
traceLink(this.myPubKeyZ32, "peer-lost", { why });
|
|
19702
|
+
if (Date.now() < this.wokeUntil) {
|
|
19703
|
+
this.wokeUntil = 0;
|
|
19704
|
+
this.resumeAfterAbsence();
|
|
19705
|
+
}
|
|
18805
19706
|
this.lostUntil = Date.now() + WATCH_PEER_MS;
|
|
18806
19707
|
this.autoConnectFailures = 0;
|
|
18807
19708
|
this.lastAutoConnectAt = 0;
|
|
@@ -18877,12 +19778,68 @@ var GhostLink = class {
|
|
|
18877
19778
|
this.autoConnectFailures = 0;
|
|
18878
19779
|
this.lastAutoConnectAt = 0;
|
|
18879
19780
|
this.session.pollNow();
|
|
18880
|
-
if (
|
|
19781
|
+
if (this.channel) {
|
|
19782
|
+
this.wokeUntil = Date.now() + WAKE_LOST_MS;
|
|
19783
|
+
this.probeAfterWake(this.channel);
|
|
19784
|
+
} else {
|
|
19785
|
+
this.resumeAfterAbsence();
|
|
18881
19786
|
if (!this.session.publishedRecently(RECENTLY_PUBLISHED_MS)) this.session.refreshAdvertisement();
|
|
18882
19787
|
this.maybeAutoConnect(this.presence);
|
|
18883
19788
|
}
|
|
18884
19789
|
}
|
|
19790
|
+
/**
|
|
19791
|
+
* Back in front with no session, after one was live: the contact likely did not notice this app go (a phone puts a
|
|
19792
|
+
* backgrounded app to sleep without a word), and reads this chat at its slow pace (30 s on the relays in the
|
|
19793
|
+
* background), so an offer or a fresh packet of this side's waited about that long for it. The chat resumes as after a
|
|
19794
|
+
* restart (WISP 100, "Back after a restart"): this side dials whatever its key, on the transport it was live on, and
|
|
19795
|
+
* knocks on the contact's other transports as its offer goes out, which reaches the contact at once.
|
|
19796
|
+
*/
|
|
19797
|
+
resumeAfterAbsence() {
|
|
19798
|
+
const transport = this.lastLiveOn;
|
|
19799
|
+
if (!transport || this.resuming || this.departed || !this.options.pairing?.credentials.peerKey || !this.options.params.profile || this.deliveryMode === "dht") return;
|
|
19800
|
+
this.lastLiveOn = void 0;
|
|
19801
|
+
this.resuming = transport;
|
|
19802
|
+
this.resumingWoke = true;
|
|
19803
|
+
this.knockedEarly = -1;
|
|
19804
|
+
traceLink(this.myPubKeyZ32, "wake-resume", { transport });
|
|
19805
|
+
if (transport !== "webrtc/1") {
|
|
19806
|
+
this.resumeWaitUntil = Date.now() + RESUME_WAIT_MS;
|
|
19807
|
+
return;
|
|
19808
|
+
}
|
|
19809
|
+
if (this.dataLink.state === "answering" && this.answeringEpoch === this.connectionEpoch) {
|
|
19810
|
+
const epoch = this.connectionEpoch;
|
|
19811
|
+
setTimeout(() => this.maybeKnockAnswering(epoch), 0);
|
|
19812
|
+
return;
|
|
19813
|
+
}
|
|
19814
|
+
const offered = this.offered;
|
|
19815
|
+
if (!offered || offered.resume || offered.epoch !== this.connectionEpoch || !this.offerOut && !this.dialing) return;
|
|
19816
|
+
this.resuming = void 0;
|
|
19817
|
+
this.resumingWoke = false;
|
|
19818
|
+
offered.resume = true;
|
|
19819
|
+
offered.woke = true;
|
|
19820
|
+
if (!this.dialing) setTimeout(() => this.maybeKnockEarly(offered.epoch), 0);
|
|
19821
|
+
}
|
|
19822
|
+
/** A ping on the session right after a wake, with `PONG_WAIT_MS` for its answer: none, and the session is dead. */
|
|
19823
|
+
probeAfterWake(channel) {
|
|
19824
|
+
if (!this.peerAnswersPings || this.paired?.state.status !== "ready") return;
|
|
19825
|
+
this.unansweredPings++;
|
|
19826
|
+
try {
|
|
19827
|
+
this.pingSentAt = Date.now();
|
|
19828
|
+
channel.send(JSON.stringify({ t: "paired-ping" }));
|
|
19829
|
+
} catch {
|
|
19830
|
+
this.dropDeadSession(channel);
|
|
19831
|
+
return;
|
|
19832
|
+
}
|
|
19833
|
+
if (this.pongWait) clearTimeout(this.pongWait);
|
|
19834
|
+
this.pongWait = setTimeout(() => {
|
|
19835
|
+
this.pongWait = null;
|
|
19836
|
+
if (this.channel !== channel || this.stopped) return;
|
|
19837
|
+
traceLink(this.myPubKeyZ32, "wake-unanswered", {});
|
|
19838
|
+
this.dropDeadSession(channel);
|
|
19839
|
+
}, PONG_WAIT_MS);
|
|
19840
|
+
}
|
|
18885
19841
|
detach() {
|
|
19842
|
+
if (this.channel && this.paired?.state.status === "ready") this.lastLiveOn = this.activeBinding?.transport ?? "webrtc/1";
|
|
18886
19843
|
this.stopLiveness();
|
|
18887
19844
|
const wasNative = !!this.activeBinding;
|
|
18888
19845
|
this.applicationOpen = false;
|
|
@@ -18908,6 +19865,7 @@ var GhostLink = class {
|
|
|
18908
19865
|
this.sessionCapabilities.reset();
|
|
18909
19866
|
this.typingSender.reset();
|
|
18910
19867
|
this.typingReceiver.clear();
|
|
19868
|
+
this.appsSuspended();
|
|
18911
19869
|
this.emitFilesSession();
|
|
18912
19870
|
if (this.peerGroupVersions) {
|
|
18913
19871
|
this.peerGroupVersions = null;
|
|
@@ -20771,7 +21729,7 @@ var DID_GHOSTLY_SERVICE = "GhostlyIdentityProof";
|
|
|
20771
21729
|
var METHOD_NAME = /^[a-z0-9]+$/;
|
|
20772
21730
|
var Z32_KEY = /^[ybndrfg8ejkmcpqxot1uwisza345h769]{52}$/;
|
|
20773
21731
|
var B64URL = /^[A-Za-z0-9_-]*$/;
|
|
20774
|
-
var SEGMENT = /^[A-Za-z0-9._~-]{1,64}$/;
|
|
21732
|
+
var SEGMENT$1 = /^[A-Za-z0-9._~-]{1,64}$/;
|
|
20775
21733
|
var HEX64 = /^[a-f0-9]{64}$/;
|
|
20776
21734
|
var METHOD_LIST = "did:key, did:jwk, did:dht and did:web";
|
|
20777
21735
|
/** base64url without padding, strictly: decoding and encoding again gives the same text. */
|
|
@@ -20782,7 +21740,7 @@ function strictBase64Url(value) {
|
|
|
20782
21740
|
if (toBase64Url(bytes) !== value) throw new DidError("Not canonical base64url");
|
|
20783
21741
|
return bytes;
|
|
20784
21742
|
}
|
|
20785
|
-
var isObject$
|
|
21743
|
+
var isObject$3 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
20786
21744
|
/**
|
|
20787
21745
|
* The DID a person typed, checked and in canonical form, or a message they can act on. Methods other than
|
|
20788
21746
|
* the four Ghostly checks are refused with "not supported yet"; a DID URL (a `#fragment`, `?query` or
|
|
@@ -20856,7 +21814,7 @@ function parseWeb(id) {
|
|
|
20856
21814
|
if (!(port >= 1 && port <= 65535) || String(port) !== portMatch[2]) throw new DidError("did:web: that port is not valid");
|
|
20857
21815
|
}
|
|
20858
21816
|
if (path.length > 8) throw new DidError("did:web: at most 8 path segments");
|
|
20859
|
-
for (const segment of path) if (!SEGMENT.test(segment) || segment === "." || segment === "..") throw new DidError("did:web: each path segment is letters, digits, dots, dashes, underscores or tildes");
|
|
21817
|
+
for (const segment of path) if (!SEGMENT$1.test(segment) || segment === "." || segment === "..") throw new DidError("did:web: each path segment is letters, digits, dots, dashes, underscores or tildes");
|
|
20860
21818
|
return {
|
|
20861
21819
|
id: [`${host}${port ? `%3A${port}` : ""}`, ...path].join(":"),
|
|
20862
21820
|
host,
|
|
@@ -20900,7 +21858,7 @@ function didWebFileNames(text, did, statement) {
|
|
|
20900
21858
|
} catch {
|
|
20901
21859
|
return false;
|
|
20902
21860
|
}
|
|
20903
|
-
return isObject$
|
|
21861
|
+
return isObject$3(body) && body.ghostly === 1 && body.did === did && body.statement === statement;
|
|
20904
21862
|
}
|
|
20905
21863
|
/** The service entry a did.json can carry instead of the file. */
|
|
20906
21864
|
function didGhostlyServiceEntry(did, key, proofId) {
|
|
@@ -20982,7 +21940,7 @@ var JWK_CURVES = {
|
|
|
20982
21940
|
};
|
|
20983
21941
|
/** A public JWK. Other curves are `undefined`; a malformed one throws. */
|
|
20984
21942
|
function jwkKey(jwk) {
|
|
20985
|
-
if (!isObject$
|
|
21943
|
+
if (!isObject$3(jwk)) throw new DidError("That JWK is not an object");
|
|
20986
21944
|
const spec = typeof jwk.crv === "string" ? JWK_CURVES[jwk.crv] : void 0;
|
|
20987
21945
|
if (!spec || jwk.kty !== spec.kty) return void 0;
|
|
20988
21946
|
const coordinate = (v) => {
|
|
@@ -21027,7 +21985,7 @@ function staticKey(did) {
|
|
|
21027
21985
|
} catch {
|
|
21028
21986
|
throw new DidError("That did:jwk does not hold a JSON Web Key");
|
|
21029
21987
|
}
|
|
21030
|
-
if (!isObject$
|
|
21988
|
+
if (!isObject$3(jwk)) throw new DidError("That did:jwk does not hold a JSON Web Key");
|
|
21031
21989
|
if ("d" in jwk || "p" in jwk || "k" in jwk) throw new DidError("That did:jwk contains a private key. Never share it: make a new key");
|
|
21032
21990
|
const key = jwkKey(jwk);
|
|
21033
21991
|
if (!key) throw new DidError("That did:jwk is not an Ed25519, secp256k1 or P-256 key");
|
|
@@ -21090,13 +22048,13 @@ var list = (value, name) => {
|
|
|
21090
22048
|
* `GhostlyIdentityProof` services are kept. Keys Ghostly cannot use are counted in `skipped`.
|
|
21091
22049
|
*/
|
|
21092
22050
|
function parseDidDocument(did, raw) {
|
|
21093
|
-
if (!isObject$
|
|
22051
|
+
if (!isObject$3(raw)) throw new DidError("The DID document is not a JSON object");
|
|
21094
22052
|
if (raw.id !== did) throw new DidError(typeof raw.id === "string" ? `The DID document is for another DID (${raw.id.slice(0, 80)})` : "The DID document has no id");
|
|
21095
22053
|
const keys = /* @__PURE__ */ new Map();
|
|
21096
22054
|
const unusable = /* @__PURE__ */ new Set();
|
|
21097
22055
|
let skipped = 0;
|
|
21098
22056
|
const add = (vm) => {
|
|
21099
|
-
if (!isObject$
|
|
22057
|
+
if (!isObject$3(vm)) {
|
|
21100
22058
|
skipped++;
|
|
21101
22059
|
return;
|
|
21102
22060
|
}
|
|
@@ -21127,10 +22085,10 @@ function parseDidDocument(did, raw) {
|
|
|
21127
22085
|
}
|
|
21128
22086
|
const services = [];
|
|
21129
22087
|
for (const s of list(raw.service, "service")) {
|
|
21130
|
-
if (!isObject$
|
|
22088
|
+
if (!isObject$3(s)) continue;
|
|
21131
22089
|
const types = Array.isArray(s.type) ? s.type : [s.type];
|
|
21132
22090
|
const endpoint = s.serviceEndpoint;
|
|
21133
|
-
if (!types.includes("GhostlyIdentityProof") || !isObject$
|
|
22091
|
+
if (!types.includes("GhostlyIdentityProof") || !isObject$3(endpoint)) continue;
|
|
21134
22092
|
if (typeof endpoint.key === "string" && Z32_KEY.test(endpoint.key) && typeof endpoint.proof === "string" && HEX64.test(endpoint.proof)) services.push({
|
|
21135
22093
|
id: typeof s.id === "string" ? s.id.slice(0, 200) : "",
|
|
21136
22094
|
key: endpoint.key,
|
|
@@ -21173,7 +22131,7 @@ function parseDidJws(input) {
|
|
|
21173
22131
|
} catch {
|
|
21174
22132
|
throw new DidError("The JWS header is not base64url JSON");
|
|
21175
22133
|
}
|
|
21176
|
-
if (!isObject$
|
|
22134
|
+
if (!isObject$3(header)) throw new DidError("The JWS header is not a JSON object");
|
|
21177
22135
|
const alg = header.alg;
|
|
21178
22136
|
if (typeof alg !== "string" || !DID_JWS_ALGS[alg]) throw new DidError(`The JWS uses ${typeof alg === "string" ? alg.slice(0, 16) : "no algorithm"}: Ghostly checks EdDSA, ES256K and ES256`);
|
|
21179
22137
|
if (header.kid !== void 0 && (typeof header.kid !== "string" || header.kid.length > 640)) throw new DidError("The JWS kid is not a key id");
|
|
@@ -21985,7 +22943,7 @@ function compactSize(n) {
|
|
|
21985
22943
|
throw new Error("Too large");
|
|
21986
22944
|
}
|
|
21987
22945
|
var varBytes = (b) => concatBytes$1(compactSize(b.length), b);
|
|
21988
|
-
var u32$
|
|
22946
|
+
var u32$2 = (n) => {
|
|
21989
22947
|
const b = /* @__PURE__ */ new Uint8Array(4);
|
|
21990
22948
|
new DataView(b.buffer).setUint32(0, n >>> 0, true);
|
|
21991
22949
|
return b;
|
|
@@ -21996,14 +22954,14 @@ var u64 = (n) => {
|
|
|
21996
22954
|
return b;
|
|
21997
22955
|
};
|
|
21998
22956
|
function encodeTx(tx, withWitness = tx.inputs.some((i) => i.witness.length > 0)) {
|
|
21999
|
-
const parts = [u32$
|
|
22957
|
+
const parts = [u32$2(tx.version)];
|
|
22000
22958
|
if (withWitness) parts.push(Uint8Array.of(0, 1));
|
|
22001
22959
|
parts.push(compactSize(tx.inputs.length));
|
|
22002
|
-
for (const i of tx.inputs) parts.push(i.txid, u32$
|
|
22960
|
+
for (const i of tx.inputs) parts.push(i.txid, u32$2(i.vout), varBytes(i.scriptSig), u32$2(i.sequence));
|
|
22003
22961
|
parts.push(compactSize(tx.outputs.length));
|
|
22004
22962
|
for (const o of tx.outputs) parts.push(u64(o.value), varBytes(o.script));
|
|
22005
22963
|
if (withWitness) for (const i of tx.inputs) parts.push(encodeWitness(i.witness));
|
|
22006
|
-
parts.push(u32$
|
|
22964
|
+
parts.push(u32$2(tx.lockTime));
|
|
22007
22965
|
return concatBytes$1(...parts);
|
|
22008
22966
|
}
|
|
22009
22967
|
var encodeWitness = (stack) => concatBytes$1(compactSize(stack.length), ...stack.map(varBytes));
|
|
@@ -22169,12 +23127,12 @@ function decodeBase64(text) {
|
|
|
22169
23127
|
/** BIP 143, for witness v0. `scriptCode` is the P2PKH script of the key. */
|
|
22170
23128
|
function sighashV0(tx, scriptCode, hashType) {
|
|
22171
23129
|
const input = tx.inputs[0];
|
|
22172
|
-
return sha256d(u32$
|
|
23130
|
+
return sha256d(u32$2(tx.version), sha256d(...tx.inputs.map((i) => concatBytes$1(i.txid, u32$2(i.vout)))), sha256d(...tx.inputs.map((i) => u32$2(i.sequence))), input.txid, u32$2(input.vout), varBytes(scriptCode), u64(0n), u32$2(input.sequence), sha256d(...tx.outputs.map((o) => concatBytes$1(u64(o.value), varBytes(o.script)))), u32$2(tx.lockTime), u32$2(hashType));
|
|
22173
23131
|
}
|
|
22174
23132
|
/** BIP 341 key path, SIGHASH_DEFAULT or SIGHASH_ALL, no annex. */
|
|
22175
23133
|
function sighashV1(tx, scriptPubKey, hashType) {
|
|
22176
23134
|
const input = tx.inputs[0];
|
|
22177
|
-
return schnorr.utils.taggedHash("TapSighash", concatBytes$1(Uint8Array.of(0, hashType), u32$
|
|
23135
|
+
return schnorr.utils.taggedHash("TapSighash", concatBytes$1(Uint8Array.of(0, hashType), u32$2(tx.version), u32$2(tx.lockTime), sha256(concatBytes$1(input.txid, u32$2(input.vout))), sha256(u64(0n)), sha256(varBytes(scriptPubKey)), sha256(u32$2(input.sequence)), sha256(concatBytes$1(...tx.outputs.map((o) => concatBytes$1(u64(o.value), varBytes(o.script))))), Uint8Array.of(0), u32$2(0)));
|
|
22178
23136
|
}
|
|
22179
23137
|
/** The original sighash, for P2PKH: the input's script replaced by the spent output's, the rest as is. */
|
|
22180
23138
|
function sighashLegacy(tx, scriptPubKey, hashType) {
|
|
@@ -22185,7 +23143,7 @@ function sighashLegacy(tx, scriptPubKey, hashType) {
|
|
|
22185
23143
|
witness: [],
|
|
22186
23144
|
scriptSig: n === 0 ? scriptPubKey : /* @__PURE__ */ new Uint8Array(0)
|
|
22187
23145
|
}))
|
|
22188
|
-
}, false), u32$
|
|
23146
|
+
}, false), u32$2(hashType));
|
|
22189
23147
|
}
|
|
22190
23148
|
/** A check that throws on malformed input (a point off the curve…) simply does not hold. */
|
|
22191
23149
|
function holds(check) {
|
|
@@ -22370,11 +23328,11 @@ function verifyBip322(address, message, signature) {
|
|
|
22370
23328
|
*/
|
|
22371
23329
|
var MAX_MESSAGE = 16384;
|
|
22372
23330
|
var MAX_SIGNATURE = 16384;
|
|
22373
|
-
var MAGIC = utf8Encode("Bitcoin Signed Message:\n");
|
|
23331
|
+
var MAGIC$1 = utf8Encode("Bitcoin Signed Message:\n");
|
|
22374
23332
|
/** The digest `signmessage` signs: double SHA-256 of the length-prefixed magic and message. */
|
|
22375
23333
|
function legacyMessageHash(message) {
|
|
22376
23334
|
const bytes = utf8Encode(message);
|
|
22377
|
-
return sha256d(compactSize(MAGIC.length), MAGIC, compactSize(bytes.length), bytes);
|
|
23335
|
+
return sha256d(compactSize(MAGIC$1.length), MAGIC$1, compactSize(bytes.length), bytes);
|
|
22378
23336
|
}
|
|
22379
23337
|
/** Header bytes 27-34 are P2PKH (uncompressed 27-30, compressed 31-34); 35-42 are BIP 137 SegWit headers. */
|
|
22380
23338
|
var isLegacySignature = (bytes) => bytes.length === 65 && bytes[0] >= 27 && bytes[0] <= 42;
|
|
@@ -22501,8 +23459,8 @@ var Reader = class {
|
|
|
22501
23459
|
if (!this.done) fail("Trailing bytes in SSH data");
|
|
22502
23460
|
}
|
|
22503
23461
|
};
|
|
22504
|
-
var u32 = (n) => Uint8Array.of(n >>> 24, n >>> 16 & 255, n >>> 8 & 255, n & 255);
|
|
22505
|
-
var sshString = (b) => concatBytes$1(u32(b.length), b);
|
|
23462
|
+
var u32$1 = (n) => Uint8Array.of(n >>> 24, n >>> 16 & 255, n >>> 8 & 255, n & 255);
|
|
23463
|
+
var sshString = (b) => concatBytes$1(u32$1(b.length), b);
|
|
22506
23464
|
function strictBase64(text, max) {
|
|
22507
23465
|
if (text.length > max || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(text)) fail("Invalid base64");
|
|
22508
23466
|
const bytes = fromBase64(text);
|
|
@@ -22668,7 +23626,7 @@ async function verifySshSignature(armored, message, options = {}) {
|
|
|
22668
23626
|
userVerified: !!(flags & SK_USER_VERIFIED),
|
|
22669
23627
|
counter
|
|
22670
23628
|
};
|
|
22671
|
-
data = concatBytes$1(sha256(utf8Encode(key.application)), Uint8Array.of(flags), u32(counter), sha256(signed));
|
|
23629
|
+
data = concatBytes$1(sha256(utf8Encode(key.application)), Uint8Array.of(flags), u32$1(counter), sha256(signed));
|
|
22672
23630
|
}
|
|
22673
23631
|
r.end();
|
|
22674
23632
|
const verifyWith = async () => {
|
|
@@ -25184,7 +26142,7 @@ var pairAad = (f, to) => JSON.stringify([
|
|
|
25184
26142
|
f.ts,
|
|
25185
26143
|
to
|
|
25186
26144
|
]);
|
|
25187
|
-
var isObject = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
26145
|
+
var isObject$2 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
25188
26146
|
var messageSigned = (f) => utf8Encode(JSON.stringify([
|
|
25189
26147
|
"ghostly-group/2 msg",
|
|
25190
26148
|
f.g,
|
|
@@ -26114,7 +27072,7 @@ var CommunitySession = class {
|
|
|
26114
27072
|
sendApp(frame, nick, now = Date.now()) {
|
|
26115
27073
|
return this.serialize(async () => {
|
|
26116
27074
|
if (!this.isMember) return { error: this.state.statusReason ?? "You are not in this group" };
|
|
26117
|
-
if (!isObject(frame) || utf8Encode(JSON.stringify(frame)).length > COMMUNITY_LIMITS.appBytes) return { error: "Too large for the group" };
|
|
27075
|
+
if (!isObject$2(frame) || utf8Encode(JSON.stringify(frame)).length > COMMUNITY_LIMITS.appBytes) return { error: "Too large for the group" };
|
|
26118
27076
|
const sent = await this.sendPayload(() => ({ x: frame }), nick, now);
|
|
26119
27077
|
if ("error" in sent) return sent;
|
|
26120
27078
|
await this.say(sent.frame);
|
|
@@ -26163,7 +27121,7 @@ var CommunitySession = class {
|
|
|
26163
27121
|
return this.serialize(async () => {
|
|
26164
27122
|
if (!this.isMember) return { error: this.state.statusReason ?? "You are not in this group" };
|
|
26165
27123
|
if (to === this.myKey || !rosterHas(this.roster, to)) return { error: "Not a member of this group" };
|
|
26166
|
-
if (!isObject(payload)) return { error: "Nothing to send" };
|
|
27124
|
+
if (!isObject$2(payload)) return { error: "Nothing to send" };
|
|
26167
27125
|
const plain = utf8Encode(JSON.stringify(payload));
|
|
26168
27126
|
if (plain.length > COMMUNITY_LIMITS.appBytes) return { error: "Too large for the group" };
|
|
26169
27127
|
const sent = await this.sendPayload((header) => ({ p: {
|
|
@@ -26276,7 +27234,7 @@ var CommunitySession = class {
|
|
|
26276
27234
|
try {
|
|
26277
27235
|
parsed = plain === null ? null : JSON.parse(plain);
|
|
26278
27236
|
} catch {}
|
|
26279
|
-
if (!isObject(parsed) || isObject(parsed.p)) continue;
|
|
27237
|
+
if (!isObject$2(parsed) || isObject$2(parsed.p)) continue;
|
|
26280
27238
|
const o = readOriginal(parsed.o, f) ?? {
|
|
26281
27239
|
e: f.e,
|
|
26282
27240
|
h: f.h,
|
|
@@ -26424,7 +27382,7 @@ var CommunitySession = class {
|
|
|
26424
27382
|
} catch {
|
|
26425
27383
|
return false;
|
|
26426
27384
|
}
|
|
26427
|
-
if (!isObject(parsed)) return false;
|
|
27385
|
+
if (!isObject$2(parsed)) return false;
|
|
26428
27386
|
const original = parsed.o === void 0 ? null : readOriginal(parsed.o, raw);
|
|
26429
27387
|
if (parsed.o !== void 0 && !original) return false;
|
|
26430
27388
|
const first = original ? {
|
|
@@ -26442,8 +27400,8 @@ var CommunitySession = class {
|
|
|
26442
27400
|
const text = typeof parsed.text === "string" ? parsed.text.slice(0, COMMUNITY_LIMITS.textBytes) : void 0;
|
|
26443
27401
|
if ([
|
|
26444
27402
|
text !== void 0,
|
|
26445
|
-
isObject(parsed.x),
|
|
26446
|
-
isObject(parsed.p)
|
|
27403
|
+
isObject$2(parsed.x),
|
|
27404
|
+
isObject$2(parsed.p)
|
|
26447
27405
|
].filter(Boolean).length !== 1) return false;
|
|
26448
27406
|
const nick = typeof parsed.nick === "string" ? sanitizeNick(parsed.nick) : void 0;
|
|
26449
27407
|
if (nick && this.state.nicks[raw.s] !== nick) {
|
|
@@ -26468,7 +27426,7 @@ var CommunitySession = class {
|
|
|
26468
27426
|
...forwarded ? { forwarded } : {},
|
|
26469
27427
|
...card ? { card } : {}
|
|
26470
27428
|
});
|
|
26471
|
-
} else if (isObject(parsed.x)) await this.hooks.app?.({
|
|
27429
|
+
} else if (isObject$2(parsed.x)) await this.hooks.app?.({
|
|
26472
27430
|
id,
|
|
26473
27431
|
sender: raw.s,
|
|
26474
27432
|
epoch: raw.e,
|
|
@@ -26484,7 +27442,7 @@ var CommunitySession = class {
|
|
|
26484
27442
|
try {
|
|
26485
27443
|
opened = plain && plain.length <= COMMUNITY_LIMITS.appBytes ? JSON.parse(utf8Decode(plain)) : null;
|
|
26486
27444
|
} catch {}
|
|
26487
|
-
if (isObject(opened)) await this.hooks.pair?.({
|
|
27445
|
+
if (isObject$2(opened)) await this.hooks.pair?.({
|
|
26488
27446
|
id,
|
|
26489
27447
|
sender: raw.s,
|
|
26490
27448
|
epoch: raw.e,
|
|
@@ -26939,6 +27897,663 @@ var GroupTypingBoard = class {
|
|
|
26939
27897
|
}
|
|
26940
27898
|
};
|
|
26941
27899
|
//#endregion
|
|
26942
|
-
export { checkAtprotoProofRecord as $, HANDOFF_VERIFIED as $a, replySnippet as $i, freshHubs as $n, OwnInviteError as $o, IdentityCheckUnavailable as $r, fromBase64Url as $s, isPubkyKey as $t, GROUP_EDIT_TEXT_BYTES as A, CALL_SIGNAL_MAX_AGE_MS as Aa, parseVoiceMeta as Ai, findEndpoint as An, SET_UPDATE as Ao, newWakeToken as Ar, DEFAULT_RELAYS as As, encodeDidDhtPacket as At, sameSshKey as B, HTTP_SERVICE_PROTO as Ba, COMMUNITY_REACTION_FRAME as Bi, SIGNAL_FRESH_MS as Bn, enrollCancelFrame as Bo, HOLD_DATED_BACK_MS as Br, RELAY_PAYLOAD_MAX_BYTES as Bs, USDT_PUBLIC_RPC as Bt, groupWakes as C, GhostlyHttpError as Ca, safeBlobType as Ci, resolveLightningDestination as Cn, readHandoffHave as Co, nextPinNumber as Cr, firstDeviceSetSecret as Cs, parseDidSignaturePaste as Ct, GroupSession as D, LIMITS as Da, VOICE_LIMITS as Di, ENDPOINT as Dn, readHandoffVerified as Do, WAKE_CALL_INTERVAL_MS as Dr, signTurnRelease as Ds, didDhtDocument as Dt, GROUP_READ_NOTE as E, webLocalFetch as Ea, PLAYABLE_AUDIO as Ei, decodeBolt11 as En, readHandoffTurnFrame as Eo, readPin as Er, signTurnPacket as Es, strictBase64Url as Et, SSHSIG_MAX_ARMOR as F, callSignalHeardAt as Fa, RateWindow as Fi, INVITE_TAKEN as Fn, verifySetUpdate as Fo, vapidKeysMatch as Fr, traceLink as Fs, ONCHAIN_PROVIDER as Ft, decodeAnyBitcoinAddress as G, HANDOFF_DONE as Ga, queueReaction as Gi, MESH_HUBS as Gn, enrollHelloFrame as Go, readManifest as Gr, createIdentity as Gs, validatePaymentTarget as Gt, verifySshSignature as H, serviceIdFromName as Ha, REACTION_LIMITS as Hi, SIGNAL_REPLY_MS as Hn, enrollDigits as Ho, HoldKeys as Hr, newerPacket as Hs, assertTokenUnits as Ht, SSHSIG_NAMESPACE as I, extractParamsFromSdp as Ia, dhtEditId as Ii, LIVENESS_PING_MS as In, ENROLL_DONE as Io, BUTTONS_CAPABILITY as Ir, budgetRetryMs as Is, PaymentPreflightError as It, ATPROTO_PROOF_COLLECTION as J, HANDOFF_OFFER as Ja, readReaction as Ji, pickMeshHubs as Jn, readEnrollHello as Jo, capsDescriptors as Jr, publicKeyFromZ32 as Js, SPARK_PROVIDER as Jt, ATPROTO_DECLARED_SCOPE as K, HANDOFF_HAVE as Ka, reactionEmoji as Ki, mayBeHub as Kn, enrollProofFrame as Ko, CapsExchange as Kr, identityFromSeed as Ks, walletNetworkOf as Kt, SshSigError as L, parseCallSignal as La, parseLinkPreview as Li, pairedMessageFrame as Ln, ENROLL_GRANT as Lo, EDIT_CAPABILITY as Lr, isDiscoveryBudgetError as Ls, SEPOLIA_TEST_USDT as Lt, parseCommunityEdit as M, answersOffer as Ma, parseVideoMeta as Mi, parseCashuRequestPayload as Mn, setAckFrame as Mo, relayRequest as Mr, currentRelays as Ms, signDidDhtPacket as Mt, mentionsMember as N, buildSdpFromSignal as Na, EDIT_BUFFER_MS as Ni, parseFedimintRequestPayload as Nn, setUpdateFrame as No, wakeRequest as Nr, normalizeRelayUrl as Ns, ETHEREUM_USDT as Nt, COMMUNITY_EDIT_FRAME as O, decodeControl as Oa, baseMime as Oi, cashuRequestPayload as On, sealHandoffPiece as Oo, WakeLimiter as Or, turnKeys as Os, didDhtFromPublicKey as Ot, groupName as P, callRtcConfig as Pa, EDIT_RATE_WINDOW_MS as Pi, GhostLink as Pn, signTombstone as Po, checkPushEndpoint as Pr, setLinkTraceSink as Ps, EVM_TEST_CHAINS as Pt, atprotoProofRkey as Q, HANDOFF_TIMINGS as Qa, readReply as Qi, doorHubs as Qn, readDeviceInvite as Qo, IDENTITY_REVOCATION_LABEL as Qr, bytesEqual as Qs, PUBKY_PROOF_MAX_BYTES as Qt, parseSshPublicKey as R, signalHasVideo as Ra, forwardedAgain as Ri, CarriedTransport as Rn, ENROLL_HELLO as Ro, UPGRADE_CAPABILITY as Rr, withRequestOptions as Rs, SEPOLIA_TEST_USDT_FAUCET as Rt, groupWakeFrame as S, presenceSeenAt as Sa, readImageMeta as Si, requestLnurlInvoice as Sn, readHandoffDone as So, mayPin as Sr, TURN_MAX as Ss, parseDidDocument as St, GROUP_REACTED_FRAME as T, parseLocalTarget as Ta, sanitizeMime as Ti, engineText as Tn, readHandoffPake as To, pinNumberHolds as Tr, readTurnPacket as Ts, staticDidDocument as Tt, verifyBitcoinMessage as U, HANDOFF_ACK as Ua, ReactionWindow as Ui, groupSignalFrame as Un, enrollDoneFrame as Uo, HoldRefusedError as Ur, openRelayPayload as Us, assertWholeSats as Ut, sshSignCommand as V, LEGACY_SERVICES as Va, GROUP_REACTION_FRAME as Vi, SIGNAL_HOLD_MS as Vn, enrollCancelReason as Vo, HOLD_LIMITS as Vr, createRelayPayload as Vs, WALLET_NETWORKS as Vt, BitcoinAddressError as W, HANDOFF_CANCEL as Wa, nextReactionNumber as Wi, readGroupSignal as Wn, enrollGrantFrame as Wo, newMailbox as Wr, parseRelayPayload as Ws, formatPaymentAmount as Wt, atprotoDidMethod as X, HANDOFF_PIECE_BYTES as Xa, wireReaction as Xi, beaconKeys as Xn, createDeviceInvite as Xo, IDENTITY_MAX_EVIDENCE as Xr, MAX_AVATAR_LENGTH as Xs, sparkInvoiceDetails as Xt, ATPROTO_PROOF_SCOPE as Y, HANDOFF_PAKE as Ya, validReactionNumber as Yi, COMMUNITY_TOPOLOGY as Yn, verifyEnrollProof as Yo, dialDescriptors as Yr, verify as Ys, sparkAddressKind as Yt, atprotoProofRecord as Z, HANDOFF_REQUEST as Za, pairedReplyAuthor as Zi, beaconRecords as Zn, deviceInviteJoinerParams as Zo, IDENTITY_MAX_VALIDITY as Zr, sanitizeAvatar as Zs, isBitcoinAddress as Zt, knockRecords as _, verifyCommitSignature as _a, DHT_MESSAGE_TTL as _i, recordsFromTxt as _n, handoffStreamKey as _o, TypingSender as _r, TURN_RAISE_READ_TIMEOUT_MS as _s, didWebFileNames as _t, sealGroupTyping as a, checkStatusCard as aa, newIdentityBinding as ai, pubkyProofPath as an, handoffCancelFrame as ao, pickHubs as ar, DEVICES_CAPABILITY as as, verifyAtprotoRecordProof as at, GROUP_WAKE_FRAME as b, EXPECT_PEER_MS as ba, dhtFileId as bi, txtValue as bn, readHandoffAck as bo, GROUP_PIN_FRAME as br, turnPutSummary as bs, normalizeDid as bt, GROUP_READ_NOTE_COMMUNITY as c, arrivalKey as ca, lastSharedWithMe as ci, DOMAIN_FILE_MAX_BYTES as cn, handoffDataFrame as co, readBeaconHead as cr, deviceEchoFrame as cs, DID_JWS_MAX_LENGTH as ct, encodeCommunityLink as d, receivedTimestamp as da, validExternalKey as di, domainTxtRecord as dn, handoffHelloFrame as do, ChatFiles as dr, deviceLinkPairing as ds, checkDidSignature as dt, BUTTON_ID as ea, randomBytes as ec, IdentityExchange as ei, isPubkyProofFolder as en, filePartName as eo, lobbyKeys as er, createChatInvite as es, isAtprotoDid as et, KNOCK_TTL_MS as f, shownTime as fa, TRANSPORTS as fi, domainWellKnownFile as fn, handoffManifestFrames as fo, FILE_LIMITS as fr, devicePingFrame as fs, didGhostlyServiceEntry as ft, knockIdentity as g, rosterHas as ga, DHT_FILE_TOO_LARGE as gi, normalizeDomain as gn, handoffRetryAfter as go, TYPING_REFRESH_MS as gr, TURN_PUT_WINDOW_MS as gs, didWebFile as gt, entryParams as h, MEMBER_KEY as ha, irohRelayUrl as hi, nip05RootKey as hn, handoffReleaseFrame as ho, TYPING_KINDS as hr, webCryptoSigner as hs, didWebDocumentUrl as ht, openGroupTyping as i, buttonLabelClash as ia, utf8Encode as ic, identityStatement as ii, pubkyProofFile as in, handoffBusyFrame as io, newerHead as ir, inviteOwnership as is, AtprotoProofError as it, carryMentions as j, RTC_CONFIG as ja, voicePeaksOf as ji, isFederationId as jn, readSetUpdate as jo, parseWakeFrame as jr, RelayTransport as js, resolveDidDht as jt, GROUP_EDIT_FRAME as k, encodeControl as ka, fileMessageText as ki, fedimintRequestPayload as kn, SET_ACK as ko, deviceWakeRequest as kr, turnName as ks, encodeDidDhtDeactivated as kt, communityHasFrame as l, claimedTime as la, PeerProofs as li, decodeDnsResponse as ln, handoffDigest as lo, readLobby as lr, deviceEchoNonce as ls, DidError as lt, encodeGroupEntryLink as m, MAX_GROUP_CHAIN as ma, irohDescriptor as mi, isPublicAddress as mn, handoffPakeFrame as mo, transferStalled as mr, seedSigner as ms, didSignedMessages as mt, GroupTypingBoard as n, RUN_RESULTS as na, toZ32 as nc, emptyIdentityLedger as ni, normalizePubkyKey as nn, handoffAttemptFailed as no, mergeBeacon as nr, encodeInviteCode as ns, parseAtprotoDidDocument as nt, COMMUNITY_LIMITS as o, sameButtonText as oa, receivedIdentityStatus as oi, homeserverWebEndpoint as on, handoffContext as oo, rankHubs as or, ENROLL_CAPABILITY as os, DID_DOCUMENT_MAX_BYTES as ot, decodeGroupEntryLink as p, GROUP_MEMBER_CAP as pa, automaticTransport as pi, encodeDnsQuery as pn, handoffOfferFrame as po, transferEnded as pr, newDeviceSetSecret as ps, didJwsAlg as pt, ATPROTO_FULL_SCOPE as q, HANDOFF_HELLO as qa, reactionIsNewer as qi, meshHubs as qn, readEnrollGrant as qo, DHT_TEXT_CAPABILITY as qr, identityFromSeedB64 as qs, SPARK_NETWORKS as qt, groupTypingEpoch as r, STATUS_CARD_LIMITS as ra, utf8Decode as rc, identityRevocationValue as ri, pubkyProofCapability as rn, handoffAttemptSucceeded as ro, mergeLobby as rr, inviteLink as rs, shortAtprotoDid as rt, CommunitySession as s, statusCardText as sa, revokesIdentity as si, pubkyHomeserverOf as sn, handoffContextProof as so, readBeacon as sr, HANDOFF_CAPABILITY as ss, DID_FILE_MAX_BYTES as st, GROUP_TYPING_FRAME as t, ITEM_STATES as ta, toBase64Url as tc, boundVerifiedIdentity as ti, newPubkyProofFolder as tn, handoffAttemptAllowed as to, lobbyRecords as tr, decodeInviteCode as ts, normalizeAtprotoHandle as tt, decodeCommunityLink as u, heardTime as ua, emptyProofLedger as ui, dnsProofName as un, handoffEphemeral as uo, shouldBeHub as ur, deviceKeyZ32 as us, checkDidJws as ut, mergeKnocks as v, edgeParams as va, peekDhtMailbox as vi, recordsFromWellKnown as vn, handoffVersions as vo, sanitizeTypingStatus as vr, TURN_SETTLE_MS as vs, didWebFileUrl as vt, parseGroupWakeFrame as w, formatLocalTarget as wa, sanitizeFileName as wi, engineError as wn, readHandoffHello as wo, pinIsNewer as wr, nextTurnPosition as ws, signingKeys as wt, GroupWakeLimiter as x, RELAY_POLL_INTERVALS as xa, parseImageMeta as xi, wellKnownUrl as xn, readHandoffCancel as xo, PIN_LIMITS as xr, TURN_LAST_SEQUENCE as xs, parseDid as xt, readKnocks as y, DirectPathWatch as ya, DHT_FILE_CAPABILITY as yi, recordsName as yn, newHandoffId as yo, typingActivity as yr, classifyTurnRead as ys, didWebHost as yt, parseSshSignature as z, emptyLinkRecords as za, readForwarded as zi, GROUP_SIGNAL_FRAME as zn, ENROLL_PROOF as zo, HOLD_CAPABILITY as zr, PacketTooLargeError as zs, TEST_USDT_FAUCET_AMOUNT as zt };
|
|
27900
|
+
//#region ../core/src/appBundle.ts
|
|
27901
|
+
var APP_BUNDLE_MAGIC = "GHOSTLYAPP1";
|
|
27902
|
+
var MAGIC = utf8Encode(APP_BUNDLE_MAGIC);
|
|
27903
|
+
var APP_BUNDLE_LIMITS = {
|
|
27904
|
+
/** The whole bundle, phase 1. */
|
|
27905
|
+
bundleBytes: 16777216,
|
|
27906
|
+
files: 64,
|
|
27907
|
+
manifestBytes: 65536,
|
|
27908
|
+
/** A path, in bytes (paths are ASCII). */
|
|
27909
|
+
pathBytes: 128,
|
|
27910
|
+
iconBytes: 262144,
|
|
27911
|
+
screenshots: 8,
|
|
27912
|
+
/** Lengths in Unicode code points. */
|
|
27913
|
+
title: 40,
|
|
27914
|
+
tagline: 80,
|
|
27915
|
+
description: 2e3,
|
|
27916
|
+
releaseNotes: 500,
|
|
27917
|
+
version: 64,
|
|
27918
|
+
license: 128,
|
|
27919
|
+
/** An https URL, in characters. */
|
|
27920
|
+
url: 512,
|
|
27921
|
+
sources: 8
|
|
27922
|
+
};
|
|
27923
|
+
var APP_ICON_PATH = "icon.png";
|
|
27924
|
+
var SCREENSHOT_TYPES = /\.(png|jpg|webp)$/;
|
|
27925
|
+
/**
|
|
27926
|
+
* The permissions of phase 1 (WISP 1200 · Permissions). `internet`: the app reaches HTTPS and WSS servers of its
|
|
27927
|
+
* choosing (the client runs it in its network runner); without it, an app has no network.
|
|
27928
|
+
*/
|
|
27929
|
+
var APP_PERMISSIONS = [
|
|
27930
|
+
"chat",
|
|
27931
|
+
"internet",
|
|
27932
|
+
"name"
|
|
27933
|
+
];
|
|
27934
|
+
var APP_CLIENTS = [
|
|
27935
|
+
"web",
|
|
27936
|
+
"desktop",
|
|
27937
|
+
"extension"
|
|
27938
|
+
];
|
|
27939
|
+
var REQUIRED = [
|
|
27940
|
+
"ghostlyApp",
|
|
27941
|
+
"publisher",
|
|
27942
|
+
"name",
|
|
27943
|
+
"version",
|
|
27944
|
+
"sequence",
|
|
27945
|
+
"kind",
|
|
27946
|
+
"title",
|
|
27947
|
+
"tagline",
|
|
27948
|
+
"entry",
|
|
27949
|
+
"permissions",
|
|
27950
|
+
"runtime",
|
|
27951
|
+
"license",
|
|
27952
|
+
"files"
|
|
27953
|
+
];
|
|
27954
|
+
var OPTIONAL = [
|
|
27955
|
+
"description",
|
|
27956
|
+
"sources",
|
|
27957
|
+
"proofs",
|
|
27958
|
+
"homepage",
|
|
27959
|
+
"support",
|
|
27960
|
+
"releaseNotes"
|
|
27961
|
+
];
|
|
27962
|
+
/** Reserved for phase 2 and refused in phase 1. */
|
|
27963
|
+
var RESERVED = ["price", "recovery"];
|
|
27964
|
+
var SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
|
|
27965
|
+
var HOST = /^>=(0|[1-9]\d*)\.(0|[1-9]\d*)(\.(0|[1-9]\d*))?$/;
|
|
27966
|
+
var SEGMENT = /^[A-Za-z0-9._-]+$/;
|
|
27967
|
+
var codePoints = (s) => [...s].length;
|
|
27968
|
+
/** One line: no control character. */
|
|
27969
|
+
var isLine$1 = (v, max, min = 0) => typeof v === "string" && codePoints(v) >= min && codePoints(v) <= max && !/\p{Cc}/u.test(v);
|
|
27970
|
+
/** Text of several lines: no control character but the line feed. */
|
|
27971
|
+
var isText$1 = (v, max) => typeof v === "string" && codePoints(v) <= max && !/[^\P{Cc}\n]/u.test(v);
|
|
27972
|
+
var isSafeCount = (v, min) => typeof v === "number" && Number.isSafeInteger(v) && v >= min;
|
|
27973
|
+
var isObject$1 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
27974
|
+
/**
|
|
27975
|
+
* An https URL as WISP 1200 takes one anywhere: at most 512 characters, no user or password, and on jsDelivr only
|
|
27976
|
+
* `https://cdn.jsdelivr.net/gh/<owner>/<repo>@<40-hex commit>/...`, since a branch, a tag, a range or `latest` is
|
|
27977
|
+
* served from a copy that moves.
|
|
27978
|
+
*/
|
|
27979
|
+
function isAppUrl(value) {
|
|
27980
|
+
if (typeof value !== "string" || value.length > APP_BUNDLE_LIMITS.url || !value.startsWith("https://")) return false;
|
|
27981
|
+
let url;
|
|
27982
|
+
try {
|
|
27983
|
+
url = new URL(value);
|
|
27984
|
+
} catch {
|
|
27985
|
+
return false;
|
|
27986
|
+
}
|
|
27987
|
+
if (url.protocol !== "https:" || url.username || url.password || !url.hostname) return false;
|
|
27988
|
+
const host = url.hostname.toLowerCase();
|
|
27989
|
+
if (host === "jsdelivr.net" || host.endsWith(".jsdelivr.net")) return host === "cdn.jsdelivr.net" && /^\/gh\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+@[0-9a-f]{40}\/./.test(url.pathname);
|
|
27990
|
+
return true;
|
|
27991
|
+
}
|
|
27992
|
+
/**
|
|
27993
|
+
* An SPDX license expression, read lightly: identifiers (`MIT`, `GPL-3.0-or-later`, `LicenseRef-x`, `Apache-2.0+`)
|
|
27994
|
+
* joined by `AND` and `OR`, `WITH` an exception, and parentheses; or `proprietary`.
|
|
27995
|
+
*/
|
|
27996
|
+
function isAppLicense(value) {
|
|
27997
|
+
if (typeof value !== "string" || value.length < 1 || value.length > APP_BUNDLE_LIMITS.license) return false;
|
|
27998
|
+
const tokens = value.match(/\(|\)|[^\s()]+/g);
|
|
27999
|
+
if (!tokens || tokens.join("").length !== value.replace(/\s/g, "").length) return false;
|
|
28000
|
+
let i = 0;
|
|
28001
|
+
const id = () => {
|
|
28002
|
+
const t = tokens[i];
|
|
28003
|
+
if (t && /^[A-Za-z0-9][A-Za-z0-9.-]*\+?$/.test(t) && ![
|
|
28004
|
+
"AND",
|
|
28005
|
+
"OR",
|
|
28006
|
+
"WITH"
|
|
28007
|
+
].includes(t)) {
|
|
28008
|
+
i++;
|
|
28009
|
+
return true;
|
|
28010
|
+
}
|
|
28011
|
+
return false;
|
|
28012
|
+
};
|
|
28013
|
+
const term = (depth) => {
|
|
28014
|
+
if (tokens[i] === "(") {
|
|
28015
|
+
i++;
|
|
28016
|
+
if (depth > 8 || !expr(depth + 1) || tokens[i] !== ")") return false;
|
|
28017
|
+
i++;
|
|
28018
|
+
return true;
|
|
28019
|
+
}
|
|
28020
|
+
if (!id()) return false;
|
|
28021
|
+
if (tokens[i] === "WITH") {
|
|
28022
|
+
i++;
|
|
28023
|
+
return id();
|
|
28024
|
+
}
|
|
28025
|
+
return true;
|
|
28026
|
+
};
|
|
28027
|
+
const expr = (depth) => {
|
|
28028
|
+
if (!term(depth)) return false;
|
|
28029
|
+
while (tokens[i] === "AND" || tokens[i] === "OR") {
|
|
28030
|
+
i++;
|
|
28031
|
+
if (!term(depth)) return false;
|
|
28032
|
+
}
|
|
28033
|
+
return true;
|
|
28034
|
+
};
|
|
28035
|
+
return expr(0) && i === tokens.length;
|
|
28036
|
+
}
|
|
28037
|
+
/** A path of a bundle: relative, `/`-separated, `[A-Za-z0-9._-]` per segment, no `.` or `..`, at most 128 bytes. */
|
|
28038
|
+
function isAppPath(value) {
|
|
28039
|
+
if (typeof value !== "string" || value.length < 1 || value.length > APP_BUNDLE_LIMITS.pathBytes) return false;
|
|
28040
|
+
return value.split("/").every((s) => SEGMENT.test(s) && s !== "." && s !== "..");
|
|
28041
|
+
}
|
|
28042
|
+
var bad = (field, detail = `${field} is not valid`) => ({
|
|
28043
|
+
ok: false,
|
|
28044
|
+
reason: "bad-field",
|
|
28045
|
+
detail
|
|
28046
|
+
});
|
|
28047
|
+
/** Checks a parsed manifest against every rule and bound of WISP 1200 that the manifest alone decides. */
|
|
28048
|
+
function checkAppManifest(value) {
|
|
28049
|
+
if (!isObject$1(value)) return bad("manifest", "The manifest is not an object");
|
|
28050
|
+
if (value.ghostlyApp !== void 0 && value.ghostlyApp !== 1) return {
|
|
28051
|
+
ok: false,
|
|
28052
|
+
reason: "unsupported-format",
|
|
28053
|
+
detail: "ghostlyApp is not 1"
|
|
28054
|
+
};
|
|
28055
|
+
for (const key of Object.keys(value)) {
|
|
28056
|
+
if (RESERVED.includes(key)) return {
|
|
28057
|
+
ok: false,
|
|
28058
|
+
reason: "reserved-key",
|
|
28059
|
+
detail: key
|
|
28060
|
+
};
|
|
28061
|
+
if (!REQUIRED.includes(key) && !OPTIONAL.includes(key)) return {
|
|
28062
|
+
ok: false,
|
|
28063
|
+
reason: "unknown-key",
|
|
28064
|
+
detail: key
|
|
28065
|
+
};
|
|
28066
|
+
}
|
|
28067
|
+
for (const key of REQUIRED) if (!(key in value)) return {
|
|
28068
|
+
ok: false,
|
|
28069
|
+
reason: "missing-key",
|
|
28070
|
+
detail: key
|
|
28071
|
+
};
|
|
28072
|
+
const m = value;
|
|
28073
|
+
if (!isAppKey(m.publisher)) return bad("publisher");
|
|
28074
|
+
if (typeof m.name !== "string" || !APP_NAME.test(m.name)) return bad("name");
|
|
28075
|
+
if (typeof m.version !== "string" || m.version.length > APP_BUNDLE_LIMITS.version || !SEMVER.test(m.version)) return bad("version");
|
|
28076
|
+
if (!isSafeCount(m.sequence, 1)) return bad("sequence");
|
|
28077
|
+
if (m.kind !== "mini-app") return bad("kind");
|
|
28078
|
+
if (!isLine$1(m.title, APP_BUNDLE_LIMITS.title, 1)) return bad("title");
|
|
28079
|
+
if (!isLine$1(m.tagline, APP_BUNDLE_LIMITS.tagline, 1)) return bad("tagline");
|
|
28080
|
+
if (m.description !== void 0 && !isText$1(m.description, APP_BUNDLE_LIMITS.description)) return bad("description");
|
|
28081
|
+
if (m.releaseNotes !== void 0 && !isLine$1(m.releaseNotes, APP_BUNDLE_LIMITS.releaseNotes)) return bad("releaseNotes");
|
|
28082
|
+
const permissions = m.permissions;
|
|
28083
|
+
if (!Array.isArray(permissions) || new Set(permissions).size !== permissions.length || !permissions.every((p) => APP_PERMISSIONS.includes(p))) return bad("permissions");
|
|
28084
|
+
const runtime = m.runtime;
|
|
28085
|
+
if (!isObject$1(runtime) || Object.keys(runtime).length !== 2 || typeof runtime.host !== "string" || !HOST.test(runtime.host)) return bad("runtime");
|
|
28086
|
+
const clients = runtime.clients;
|
|
28087
|
+
if (!Array.isArray(clients) || clients.length < 1 || new Set(clients).size !== clients.length || !clients.every((c) => APP_CLIENTS.includes(c))) return bad("runtime");
|
|
28088
|
+
if (!isAppLicense(m.license)) return bad("license");
|
|
28089
|
+
if (m.sources !== void 0 && (!Array.isArray(m.sources) || m.sources.length > APP_BUNDLE_LIMITS.sources || !m.sources.every(isAppUrl))) return bad("sources");
|
|
28090
|
+
if (m.homepage !== void 0 && !isAppUrl(m.homepage)) return bad("homepage");
|
|
28091
|
+
if (m.support !== void 0 && !isAppUrl(m.support)) return bad("support");
|
|
28092
|
+
if (m.proofs !== void 0) {
|
|
28093
|
+
if (!Array.isArray(m.proofs)) return bad("proofs");
|
|
28094
|
+
if (m.proofs.length > 0) return {
|
|
28095
|
+
ok: false,
|
|
28096
|
+
reason: "proofs-unsupported",
|
|
28097
|
+
detail: "Publisher proofs come after phase 1"
|
|
28098
|
+
};
|
|
28099
|
+
}
|
|
28100
|
+
const files = m.files;
|
|
28101
|
+
if (!Array.isArray(files)) return bad("files");
|
|
28102
|
+
if (files.length > APP_BUNDLE_LIMITS.files) return {
|
|
28103
|
+
ok: false,
|
|
28104
|
+
reason: "too-many-files",
|
|
28105
|
+
detail: `${files.length} files`
|
|
28106
|
+
};
|
|
28107
|
+
const lower = /* @__PURE__ */ new Set();
|
|
28108
|
+
let previous = "";
|
|
28109
|
+
let screenshots = 0;
|
|
28110
|
+
for (const f of files) {
|
|
28111
|
+
if (!isObject$1(f) || Object.keys(f).length !== 3 || !isAppPath(f.path) || !isSafeCount(f.size, 0) || !isAppHash(f.sha256)) return isObject$1(f) && typeof f.path === "string" && !isAppPath(f.path) ? {
|
|
28112
|
+
ok: false,
|
|
28113
|
+
reason: "bad-path",
|
|
28114
|
+
detail: f.path
|
|
28115
|
+
} : bad("files", "A file is not {path, size, sha256}");
|
|
28116
|
+
if (lower.has(f.path.toLowerCase())) return {
|
|
28117
|
+
ok: false,
|
|
28118
|
+
reason: "path-collision",
|
|
28119
|
+
detail: f.path
|
|
28120
|
+
};
|
|
28121
|
+
lower.add(f.path.toLowerCase());
|
|
28122
|
+
if (f.path <= previous) return {
|
|
28123
|
+
ok: false,
|
|
28124
|
+
reason: "files-unsorted",
|
|
28125
|
+
detail: f.path
|
|
28126
|
+
};
|
|
28127
|
+
previous = f.path;
|
|
28128
|
+
if (f.size > APP_BUNDLE_LIMITS.bundleBytes) return bad("files", `${f.path} is larger than a bundle`);
|
|
28129
|
+
if (f.path.startsWith("screenshots/")) {
|
|
28130
|
+
if (!SCREENSHOT_TYPES.test(f.path)) return {
|
|
28131
|
+
ok: false,
|
|
28132
|
+
reason: "bad-path",
|
|
28133
|
+
detail: `${f.path}: a screenshot is .png, .jpg or .webp`
|
|
28134
|
+
};
|
|
28135
|
+
if (++screenshots > APP_BUNDLE_LIMITS.screenshots) return {
|
|
28136
|
+
ok: false,
|
|
28137
|
+
reason: "too-many-screenshots"
|
|
28138
|
+
};
|
|
28139
|
+
}
|
|
28140
|
+
if (f.path === "icon.png" && f.size > APP_BUNDLE_LIMITS.iconBytes) return {
|
|
28141
|
+
ok: false,
|
|
28142
|
+
reason: "bad-icon",
|
|
28143
|
+
detail: "icon.png is larger than 256 KiB"
|
|
28144
|
+
};
|
|
28145
|
+
}
|
|
28146
|
+
const entry = m.entry;
|
|
28147
|
+
if (typeof entry !== "string" || !entry.endsWith(".html") || !files.some((f) => isObject$1(f) && f.path === entry)) return {
|
|
28148
|
+
ok: false,
|
|
28149
|
+
reason: "bad-entry",
|
|
28150
|
+
detail: "The entry is not an .html file of the bundle"
|
|
28151
|
+
};
|
|
28152
|
+
return {
|
|
28153
|
+
ok: true,
|
|
28154
|
+
manifest: m
|
|
28155
|
+
};
|
|
28156
|
+
}
|
|
28157
|
+
/** The digest of a version: SHA-256 of its canonical manifest bytes, base64url. */
|
|
28158
|
+
function appDigest(manifestBytes) {
|
|
28159
|
+
return toBase64Url(sha256(manifestBytes));
|
|
28160
|
+
}
|
|
28161
|
+
var u32 = (b, at) => (b[at] << 24 >>> 0) + (b[at + 1] << 16) + (b[at + 2] << 8) + b[at + 3];
|
|
28162
|
+
/** Reads and checks a bundle (the order of the checks is part of the format: see above). */
|
|
28163
|
+
function readAppBundle(bytes) {
|
|
28164
|
+
if (bytes.length > APP_BUNDLE_LIMITS.bundleBytes) return {
|
|
28165
|
+
ok: false,
|
|
28166
|
+
reason: "too-large"
|
|
28167
|
+
};
|
|
28168
|
+
const head = bytes.subarray(0, MAGIC.length);
|
|
28169
|
+
if (!head.every((b, i) => b === MAGIC[i])) return {
|
|
28170
|
+
ok: false,
|
|
28171
|
+
reason: "magic"
|
|
28172
|
+
};
|
|
28173
|
+
if (head.length < MAGIC.length) return {
|
|
28174
|
+
ok: false,
|
|
28175
|
+
reason: "truncated"
|
|
28176
|
+
};
|
|
28177
|
+
let at = MAGIC.length;
|
|
28178
|
+
if (bytes.length < at + 4) return {
|
|
28179
|
+
ok: false,
|
|
28180
|
+
reason: "truncated"
|
|
28181
|
+
};
|
|
28182
|
+
const manifestLength = u32(bytes, at);
|
|
28183
|
+
at += 4;
|
|
28184
|
+
if (manifestLength > APP_BUNDLE_LIMITS.manifestBytes) return {
|
|
28185
|
+
ok: false,
|
|
28186
|
+
reason: "manifest-too-large"
|
|
28187
|
+
};
|
|
28188
|
+
if (bytes.length < at + manifestLength + 4) return {
|
|
28189
|
+
ok: false,
|
|
28190
|
+
reason: "truncated"
|
|
28191
|
+
};
|
|
28192
|
+
const manifestBytes = bytes.subarray(at, at + manifestLength);
|
|
28193
|
+
at += manifestLength;
|
|
28194
|
+
const statementLength = u32(bytes, at);
|
|
28195
|
+
at += 4;
|
|
28196
|
+
if (bytes.length < at + statementLength) return {
|
|
28197
|
+
ok: false,
|
|
28198
|
+
reason: "truncated"
|
|
28199
|
+
};
|
|
28200
|
+
const statementBytes = bytes.subarray(at, at + statementLength);
|
|
28201
|
+
at += statementLength;
|
|
28202
|
+
const read = readCanonicalJson(manifestBytes);
|
|
28203
|
+
if (!read.ok) return read;
|
|
28204
|
+
const checked = checkAppManifest(read.value);
|
|
28205
|
+
if (!checked.ok) return checked;
|
|
28206
|
+
const manifest = checked.manifest;
|
|
28207
|
+
const statement = readAppSignature(statementBytes);
|
|
28208
|
+
if (!statement.ok) return statement;
|
|
28209
|
+
const total = manifest.files.reduce((sum, f) => sum + f.size, 0);
|
|
28210
|
+
const rest = bytes.length - at;
|
|
28211
|
+
if (total > rest) return {
|
|
28212
|
+
ok: false,
|
|
28213
|
+
reason: "truncated"
|
|
28214
|
+
};
|
|
28215
|
+
if (total < rest) return {
|
|
28216
|
+
ok: false,
|
|
28217
|
+
reason: "trailing-bytes",
|
|
28218
|
+
detail: `${rest - total} bytes past the last file`
|
|
28219
|
+
};
|
|
28220
|
+
const signed = verifyAppSignature(APP_PREFIXES.app, manifestBytes, statement.signature, manifest.publisher);
|
|
28221
|
+
if (!signed.ok) return signed;
|
|
28222
|
+
const files = /* @__PURE__ */ new Map();
|
|
28223
|
+
for (const f of manifest.files) {
|
|
28224
|
+
const content = bytes.subarray(at, at + f.size);
|
|
28225
|
+
at += f.size;
|
|
28226
|
+
if (toBase64Url(sha256(content)) !== f.sha256) return {
|
|
28227
|
+
ok: false,
|
|
28228
|
+
reason: "file-hash",
|
|
28229
|
+
detail: f.path
|
|
28230
|
+
};
|
|
28231
|
+
files.set(f.path, content);
|
|
28232
|
+
}
|
|
28233
|
+
const icon = files.get(APP_ICON_PATH);
|
|
28234
|
+
if (icon) {
|
|
28235
|
+
const size = imageSize(icon);
|
|
28236
|
+
if (!size || icon[0] !== 137 || size.width !== size.height) return {
|
|
28237
|
+
ok: false,
|
|
28238
|
+
reason: "bad-icon",
|
|
28239
|
+
detail: "icon.png is not a square PNG"
|
|
28240
|
+
};
|
|
28241
|
+
}
|
|
28242
|
+
return {
|
|
28243
|
+
ok: true,
|
|
28244
|
+
bundle: {
|
|
28245
|
+
manifest,
|
|
28246
|
+
manifestBytes,
|
|
28247
|
+
digest: appDigest(manifestBytes),
|
|
28248
|
+
signature: statement.signature,
|
|
28249
|
+
files
|
|
28250
|
+
}
|
|
28251
|
+
};
|
|
28252
|
+
}
|
|
28253
|
+
/** Puts a bundle's parts together, checking nothing: for builders, and for tests that need a broken bundle. */
|
|
28254
|
+
function assembleAppBundle(manifestBytes, statementBytes, files) {
|
|
28255
|
+
const total = MAGIC.length + 8 + manifestBytes.length + statementBytes.length + files.reduce((s, f) => s + f.length, 0);
|
|
28256
|
+
const out = new Uint8Array(total);
|
|
28257
|
+
const view = new DataView(out.buffer);
|
|
28258
|
+
let at = 0;
|
|
28259
|
+
out.set(MAGIC, at);
|
|
28260
|
+
at += MAGIC.length;
|
|
28261
|
+
view.setUint32(at, manifestBytes.length);
|
|
28262
|
+
at += 4;
|
|
28263
|
+
out.set(manifestBytes, at);
|
|
28264
|
+
at += manifestBytes.length;
|
|
28265
|
+
view.setUint32(at, statementBytes.length);
|
|
28266
|
+
at += 4;
|
|
28267
|
+
out.set(statementBytes, at);
|
|
28268
|
+
at += statementBytes.length;
|
|
28269
|
+
for (const f of files) {
|
|
28270
|
+
out.set(f, at);
|
|
28271
|
+
at += f.length;
|
|
28272
|
+
}
|
|
28273
|
+
return out;
|
|
28274
|
+
}
|
|
28275
|
+
//#endregion
|
|
28276
|
+
//#region ../core/src/appStore.ts
|
|
28277
|
+
var APP_STORE_LIMITS = {
|
|
28278
|
+
/** The index's bytes, uncompressed. */
|
|
28279
|
+
indexBytes: 16777216,
|
|
28280
|
+
/** How far ahead `expires` may be. */
|
|
28281
|
+
expiresAheadS: 7776e3,
|
|
28282
|
+
name: 40,
|
|
28283
|
+
description: 2e3,
|
|
28284
|
+
title: 40,
|
|
28285
|
+
tagline: 80,
|
|
28286
|
+
/** `category`, `developer`, `submitter`. */
|
|
28287
|
+
label: 40,
|
|
28288
|
+
reason: 200,
|
|
28289
|
+
urls: 4,
|
|
28290
|
+
apps: 4096,
|
|
28291
|
+
removed: 4096,
|
|
28292
|
+
revoked: 4096
|
|
28293
|
+
};
|
|
28294
|
+
var APP_STORE_KINDS = ["curated", "indexed"];
|
|
28295
|
+
var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
28296
|
+
var isLine = (v, max, min = 0) => typeof v === "string" && [...v].length >= min && [...v].length <= max && !/\p{Cc}/u.test(v);
|
|
28297
|
+
var isText = (v, max) => typeof v === "string" && [...v].length <= max && !/[^\P{Cc}\n]/u.test(v);
|
|
28298
|
+
var isCount = (v, min) => typeof v === "number" && Number.isSafeInteger(v) && v >= min;
|
|
28299
|
+
var fine = { ok: true };
|
|
28300
|
+
function keysOf(value, required, optional) {
|
|
28301
|
+
for (const k of Object.keys(value)) if (!required.includes(k) && !optional.includes(k)) return {
|
|
28302
|
+
ok: false,
|
|
28303
|
+
reason: "unknown-key",
|
|
28304
|
+
detail: k
|
|
28305
|
+
};
|
|
28306
|
+
for (const k of required) if (!(k in value)) return {
|
|
28307
|
+
ok: false,
|
|
28308
|
+
reason: "missing-key",
|
|
28309
|
+
detail: k
|
|
28310
|
+
};
|
|
28311
|
+
return fine;
|
|
28312
|
+
}
|
|
28313
|
+
var LISTING_REQUIRED = [
|
|
28314
|
+
"ref",
|
|
28315
|
+
"sequence",
|
|
28316
|
+
"digest",
|
|
28317
|
+
"urls",
|
|
28318
|
+
"title",
|
|
28319
|
+
"tagline"
|
|
28320
|
+
];
|
|
28321
|
+
var LISTING_OPTIONAL = [
|
|
28322
|
+
"category",
|
|
28323
|
+
"developer",
|
|
28324
|
+
"submitter",
|
|
28325
|
+
"repo",
|
|
28326
|
+
"support"
|
|
28327
|
+
];
|
|
28328
|
+
/** Checks one entry of `apps`, which is also the whole of a `listing.json`. */
|
|
28329
|
+
function checkAppListing(value) {
|
|
28330
|
+
if (!isObject(value)) return {
|
|
28331
|
+
ok: false,
|
|
28332
|
+
reason: "bad-field",
|
|
28333
|
+
detail: "A listing is not an object"
|
|
28334
|
+
};
|
|
28335
|
+
const keys = keysOf(value, LISTING_REQUIRED, LISTING_OPTIONAL);
|
|
28336
|
+
if (!keys.ok) return keys;
|
|
28337
|
+
const bad = (field) => ({
|
|
28338
|
+
ok: false,
|
|
28339
|
+
reason: "bad-field",
|
|
28340
|
+
detail: `apps: ${field}`
|
|
28341
|
+
});
|
|
28342
|
+
if (!isAppRef(value.ref)) return bad("ref");
|
|
28343
|
+
if (!isCount(value.sequence, 1)) return bad("sequence");
|
|
28344
|
+
if (!isAppHash(value.digest)) return bad("digest");
|
|
28345
|
+
const urls = value.urls;
|
|
28346
|
+
if (!Array.isArray(urls) || urls.length < 1 || urls.length > APP_STORE_LIMITS.urls || !urls.every(isAppUrl)) return bad("urls");
|
|
28347
|
+
if (!isLine(value.title, APP_STORE_LIMITS.title, 1)) return bad("title");
|
|
28348
|
+
if (!isLine(value.tagline, APP_STORE_LIMITS.tagline, 1)) return bad("tagline");
|
|
28349
|
+
for (const k of [
|
|
28350
|
+
"category",
|
|
28351
|
+
"developer",
|
|
28352
|
+
"submitter"
|
|
28353
|
+
]) if (value[k] !== void 0 && !isLine(value[k], APP_STORE_LIMITS.label, 1)) return bad(k);
|
|
28354
|
+
for (const k of ["repo", "support"]) if (value[k] !== void 0 && !isAppUrl(value[k])) return bad(k);
|
|
28355
|
+
return fine;
|
|
28356
|
+
}
|
|
28357
|
+
function checkRemoval(value) {
|
|
28358
|
+
if (!isObject(value)) return {
|
|
28359
|
+
ok: false,
|
|
28360
|
+
reason: "bad-field",
|
|
28361
|
+
detail: "A removal is not an object"
|
|
28362
|
+
};
|
|
28363
|
+
const keys = keysOf(value, [
|
|
28364
|
+
"ref",
|
|
28365
|
+
"digest",
|
|
28366
|
+
"reason",
|
|
28367
|
+
"at"
|
|
28368
|
+
], []);
|
|
28369
|
+
if (!keys.ok) return keys;
|
|
28370
|
+
if (!isAppRef(value.ref) || !isAppHash(value.digest) || !isLine(value.reason, APP_STORE_LIMITS.reason, 1) || !isCount(value.at, 0)) return {
|
|
28371
|
+
ok: false,
|
|
28372
|
+
reason: "bad-field",
|
|
28373
|
+
detail: "removed: not {ref, digest, reason, at}"
|
|
28374
|
+
};
|
|
28375
|
+
return fine;
|
|
28376
|
+
}
|
|
28377
|
+
var STORE_REQUIRED = [
|
|
28378
|
+
"ghostlyStore",
|
|
28379
|
+
"key",
|
|
28380
|
+
"name",
|
|
28381
|
+
"kind",
|
|
28382
|
+
"sequence",
|
|
28383
|
+
"expires",
|
|
28384
|
+
"apps",
|
|
28385
|
+
"removed",
|
|
28386
|
+
"revoked"
|
|
28387
|
+
];
|
|
28388
|
+
/**
|
|
28389
|
+
* Checks an index's fields and bounds, and every revocation it carries (one that does not verify refuses the index:
|
|
28390
|
+
* the store copied it, so a broken one is the store's fault). The signature and `expires` are `readAppStore`'s.
|
|
28391
|
+
*/
|
|
28392
|
+
function checkAppStoreIndex(value) {
|
|
28393
|
+
if (!isObject(value)) return {
|
|
28394
|
+
ok: false,
|
|
28395
|
+
reason: "bad-field",
|
|
28396
|
+
detail: "The index is not an object"
|
|
28397
|
+
};
|
|
28398
|
+
if (value.ghostlyStore !== void 0 && value.ghostlyStore !== 1) return {
|
|
28399
|
+
ok: false,
|
|
28400
|
+
reason: "unsupported-format",
|
|
28401
|
+
detail: "ghostlyStore is not 1"
|
|
28402
|
+
};
|
|
28403
|
+
const keys = keysOf(value, STORE_REQUIRED, ["description"]);
|
|
28404
|
+
if (!keys.ok) return keys;
|
|
28405
|
+
const bad = (field) => ({
|
|
28406
|
+
ok: false,
|
|
28407
|
+
reason: "bad-field",
|
|
28408
|
+
detail: field
|
|
28409
|
+
});
|
|
28410
|
+
if (!isAppKey(value.key)) return bad("key");
|
|
28411
|
+
if (!isLine(value.name, APP_STORE_LIMITS.name, 1)) return bad("name");
|
|
28412
|
+
if (value.description !== void 0 && !isText(value.description, APP_STORE_LIMITS.description)) return bad("description");
|
|
28413
|
+
if (!APP_STORE_KINDS.includes(value.kind)) return bad("kind");
|
|
28414
|
+
if (!isCount(value.sequence, 1)) return bad("sequence");
|
|
28415
|
+
if (!isCount(value.expires, 0)) return bad("expires");
|
|
28416
|
+
const { apps, removed, revoked } = value;
|
|
28417
|
+
if (!Array.isArray(apps) || apps.length > APP_STORE_LIMITS.apps) return bad("apps");
|
|
28418
|
+
const refs = /* @__PURE__ */ new Set();
|
|
28419
|
+
for (const listing of apps) {
|
|
28420
|
+
const checked = checkAppListing(listing);
|
|
28421
|
+
if (!checked.ok) return checked;
|
|
28422
|
+
const ref = listing.ref;
|
|
28423
|
+
if (refs.has(ref)) return {
|
|
28424
|
+
ok: false,
|
|
28425
|
+
reason: "duplicate-app",
|
|
28426
|
+
detail: ref
|
|
28427
|
+
};
|
|
28428
|
+
refs.add(ref);
|
|
28429
|
+
}
|
|
28430
|
+
if (!Array.isArray(removed) || removed.length > APP_STORE_LIMITS.removed) return bad("removed");
|
|
28431
|
+
for (const entry of removed) {
|
|
28432
|
+
const checked = checkRemoval(entry);
|
|
28433
|
+
if (!checked.ok) return checked;
|
|
28434
|
+
}
|
|
28435
|
+
if (!Array.isArray(revoked) || revoked.length > APP_STORE_LIMITS.revoked) return bad("revoked");
|
|
28436
|
+
for (const entry of revoked) {
|
|
28437
|
+
const read = readAppRevocation(entry);
|
|
28438
|
+
if (!read.ok) return {
|
|
28439
|
+
ok: false,
|
|
28440
|
+
reason: "bad-revocation",
|
|
28441
|
+
detail: read.detail ?? read.reason
|
|
28442
|
+
};
|
|
28443
|
+
}
|
|
28444
|
+
return fine;
|
|
28445
|
+
}
|
|
28446
|
+
/**
|
|
28447
|
+
* Reads `ghostly-store.json` with its `ghostly-store.sig`, in this order: size, JSON, canonical bytes, fields and
|
|
28448
|
+
* revocations, the signature statement, the key the reader holds for this store (when it holds one), the signature,
|
|
28449
|
+
* then `expires` against `now` (seconds): more than 90 days ahead is refused, past is `expired`.
|
|
28450
|
+
*/
|
|
28451
|
+
function readAppStore(indexBytes, sigBytes, now, heldKey) {
|
|
28452
|
+
if (indexBytes.length > APP_STORE_LIMITS.indexBytes) return {
|
|
28453
|
+
ok: false,
|
|
28454
|
+
reason: "too-large"
|
|
28455
|
+
};
|
|
28456
|
+
const read = readCanonicalJson(indexBytes);
|
|
28457
|
+
if (!read.ok) return read;
|
|
28458
|
+
const checked = checkAppStoreIndex(read.value);
|
|
28459
|
+
if (!checked.ok) return checked;
|
|
28460
|
+
const index = read.value;
|
|
28461
|
+
const statement = readAppSignature(sigBytes);
|
|
28462
|
+
if (!statement.ok) return statement;
|
|
28463
|
+
if (heldKey !== void 0 && index.key !== heldKey) return {
|
|
28464
|
+
ok: false,
|
|
28465
|
+
reason: "store-key",
|
|
28466
|
+
detail: "Another key than the store's"
|
|
28467
|
+
};
|
|
28468
|
+
const signed = verifyAppSignature(APP_PREFIXES.store, indexBytes, statement.signature, index.key);
|
|
28469
|
+
if (!signed.ok) return signed;
|
|
28470
|
+
if (index.expires > now + APP_STORE_LIMITS.expiresAheadS) return {
|
|
28471
|
+
ok: false,
|
|
28472
|
+
reason: "expires-too-far"
|
|
28473
|
+
};
|
|
28474
|
+
return {
|
|
28475
|
+
ok: true,
|
|
28476
|
+
store: {
|
|
28477
|
+
index,
|
|
28478
|
+
digest: toBase64Url(sha256(indexBytes)),
|
|
28479
|
+
expired: index.expires < now
|
|
28480
|
+
}
|
|
28481
|
+
};
|
|
28482
|
+
}
|
|
28483
|
+
/** Signs an index with the store key: its canonical bytes and the `ghostly-store.sig` bytes. Throws on a bad index. */
|
|
28484
|
+
async function signAppStore(index, signer) {
|
|
28485
|
+
if (index.key !== toZ32(signer.publicKey)) throw new Error("A store index is signed by its own key");
|
|
28486
|
+
const checked = checkAppStoreIndex(JSON.parse(JSON.stringify(index)));
|
|
28487
|
+
if (!checked.ok) throw new Error(`Not a valid store index (${checked.reason}${checked.detail ? `: ${checked.detail}` : ""})`);
|
|
28488
|
+
const { bytes, signature } = await signAppObject(APP_PREFIXES.store, index, signer);
|
|
28489
|
+
return {
|
|
28490
|
+
indexBytes: bytes,
|
|
28491
|
+
sigBytes: canonicalJsonBytes(signature),
|
|
28492
|
+
signature
|
|
28493
|
+
};
|
|
28494
|
+
}
|
|
28495
|
+
//#endregion
|
|
28496
|
+
//#region ../core/src/appUpdates.ts
|
|
28497
|
+
function appUpdateDecision(installed, candidate) {
|
|
28498
|
+
if (installed.ref !== candidate.ref) return "other-app";
|
|
28499
|
+
if (candidate.sequence > installed.sequence) return "update";
|
|
28500
|
+
if (candidate.sequence < installed.sequence) return "rollback";
|
|
28501
|
+
return candidate.digest === installed.digest ? "same" : "equivocation";
|
|
28502
|
+
}
|
|
28503
|
+
/** The permissions an update adds: when there is any, the update waits for the person, who sees only these. */
|
|
28504
|
+
function addedAppPermissions(granted, requested) {
|
|
28505
|
+
return requested.filter((p) => !granted.includes(p));
|
|
28506
|
+
}
|
|
28507
|
+
function planAppUpdate(installed, candidate) {
|
|
28508
|
+
const decision = appUpdateDecision(installed, candidate);
|
|
28509
|
+
if (decision !== "update") return {
|
|
28510
|
+
action: "refuse",
|
|
28511
|
+
decision
|
|
28512
|
+
};
|
|
28513
|
+
const added = addedAppPermissions(installed.permissions, candidate.permissions);
|
|
28514
|
+
return added.length ? {
|
|
28515
|
+
action: "ask",
|
|
28516
|
+
added
|
|
28517
|
+
} : { action: "install" };
|
|
28518
|
+
}
|
|
28519
|
+
/**
|
|
28520
|
+
* The revocation that stops a version, or null: one signed by the app's own publisher (a reader verified it) that names
|
|
28521
|
+
* its digest, or reaches its `sequence` with `upTo`.
|
|
28522
|
+
*/
|
|
28523
|
+
function appRevocationFor(version, revocations) {
|
|
28524
|
+
const publisher = appRefPublisher(version.ref);
|
|
28525
|
+
for (const r of revocations) {
|
|
28526
|
+
const s = r.statement;
|
|
28527
|
+
if (s.app !== version.ref || r.signature.key !== publisher) continue;
|
|
28528
|
+
if ("digests" in s ? s.digests.includes(version.digest) : version.sequence <= s.upTo) return r;
|
|
28529
|
+
}
|
|
28530
|
+
return null;
|
|
28531
|
+
}
|
|
28532
|
+
function checkAppBeforeRun(version, stores, revocations = []) {
|
|
28533
|
+
const revocation = appRevocationFor(version, [...revocations, ...stores.flatMap((s) => s.revoked)]);
|
|
28534
|
+
if (revocation) return {
|
|
28535
|
+
status: "revoked",
|
|
28536
|
+
revocation
|
|
28537
|
+
};
|
|
28538
|
+
const by = stores.flatMap((s) => s.removed.filter((r) => r.ref === version.ref && r.digest === version.digest).map((r) => ({
|
|
28539
|
+
store: s.key,
|
|
28540
|
+
name: s.name,
|
|
28541
|
+
reason: r.reason,
|
|
28542
|
+
at: r.at
|
|
28543
|
+
})));
|
|
28544
|
+
return by.length ? {
|
|
28545
|
+
status: "removed",
|
|
28546
|
+
by
|
|
28547
|
+
} : { status: "ok" };
|
|
28548
|
+
}
|
|
28549
|
+
function appStoreDecision(held, read) {
|
|
28550
|
+
if (!held) return "new";
|
|
28551
|
+
if (held.key !== read.key) return "other-key";
|
|
28552
|
+
if (read.sequence > held.sequence) return "update";
|
|
28553
|
+
if (read.sequence < held.sequence) return "rollback";
|
|
28554
|
+
return read.digest === held.digest ? "same" : "equivocation";
|
|
28555
|
+
}
|
|
28556
|
+
//#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 };
|
|
26943
28558
|
|
|
26944
|
-
//# sourceMappingURL=src-
|
|
28559
|
+
//# sourceMappingURL=src-C6qQYrAH.js.map
|