@ghostlytools/cli 1.2.0 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -10
- package/SKILL.md +15 -6
- package/dist/assets/{arkade-3szZ1IpP.js → arkade-_gS9wShF.js} +2 -2
- package/dist/assets/{arkade-3szZ1IpP.js.map → arkade-_gS9wShF.js.map} +1 -1
- package/dist/assets/{hyperdhtRelay-DmTrpmba.js → hyperdhtRelay-CgYLaQz3.js} +3 -3
- package/dist/assets/{hyperdhtRelay-DmTrpmba.js.map → hyperdhtRelay-CgYLaQz3.js.map} +1 -1
- package/dist/assets/{mainline-BueFJx3f.js → mainline-1v8OTYvp.js} +2 -2
- package/dist/assets/{mainline-BueFJx3f.js.map → mainline-1v8OTYvp.js.map} +1 -1
- package/dist/assets/{server-BGXmSYx_.js → server-Cy4ox7oD.js} +722 -147
- package/dist/assets/server-Cy4ox7oD.js.map +1 -0
- package/dist/assets/{src-BMUcYQ7b.js → src-XzcHzneo.js} +1016 -752
- package/dist/assets/src-XzcHzneo.js.map +1 -0
- package/dist/assets/{usdt-DklGBSeq.js → usdt-Deo3g0Nq.js} +2 -2
- package/dist/assets/{usdt-DklGBSeq.js.map → usdt-Deo3g0Nq.js.map} +1 -1
- package/dist/ghostly.mjs +831 -203
- package/dist/ghostly.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/assets/server-BGXmSYx_.js.map +0 -1
- package/dist/assets/src-BMUcYQ7b.js.map +0 -1
|
@@ -142,9 +142,20 @@ var KEEP = /[\u200c\u200d]/;
|
|
|
142
142
|
function stripHidden(text) {
|
|
143
143
|
return text.replace(HIDDEN, (char) => KEEP.test(char) ? char : "");
|
|
144
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* What draws nothing when it stands alone and `HIDDEN` leaves in, since text needs it beside something else: the
|
|
147
|
+
* joiners, the fillers, the variation selectors and the other default-ignorable code points, combining marks, white
|
|
148
|
+
* space and the blank Braille pattern.
|
|
149
|
+
*/
|
|
150
|
+
var UNSEEN = /^[\p{Default_Ignorable_Code_Point}\p{M}\p{White_Space}\u2800]*$/u;
|
|
151
|
+
/** Whether `text` holds a character that shows by itself: a name of joiners, fillers or marks alone has none. */
|
|
152
|
+
function hasVisible(text) {
|
|
153
|
+
return !UNSEEN.test(text);
|
|
154
|
+
}
|
|
145
155
|
/** Undefined when nothing visible is left, so callers can fall back. */
|
|
146
156
|
function sanitizeDisplayText(text, max) {
|
|
147
|
-
|
|
157
|
+
const clean = [...stripHidden(text)].slice(0, max).join("").trim();
|
|
158
|
+
return hasVisible(clean) ? clean : void 0;
|
|
148
159
|
}
|
|
149
160
|
/** A nickname as it arrived from a peer, ready to show. */
|
|
150
161
|
function sanitizeNick(nick) {
|
|
@@ -764,12 +775,15 @@ var asker = (options, write) => ({
|
|
|
764
775
|
write,
|
|
765
776
|
signal: !!options.signal,
|
|
766
777
|
watch: !!options.watch && !write,
|
|
767
|
-
watchWrite: !!options.watch && write
|
|
778
|
+
watchWrite: !!options.watch && write,
|
|
779
|
+
departed: !!options.departed && !write
|
|
768
780
|
});
|
|
769
781
|
/** A 1:1 chat's request: neither a group's nor a background one. */
|
|
770
782
|
var isChat = (who) => !who.background && !who.group;
|
|
771
783
|
/** A 1:1 chat's offer or answer, or its read for the answer to its offer: it may use the allowance (`SIGNALING_ALLOWANCE_SHARE`). */
|
|
772
784
|
var isChatSignal = (who) => who.signal && isChat(who);
|
|
785
|
+
/** A 1:1 chat's read of a contact that just went away: a few may use the chat's allowance (`DEPARTED_READS`). */
|
|
786
|
+
var isDepartedRead = (who) => who.departed && isChat(who);
|
|
773
787
|
/** A group's edge looking fast for a signal (`GROUP_BURST_MS`). */
|
|
774
788
|
var isGroupUrgentRead = (who) => who.group && who.urgent && !who.write && !who.background;
|
|
775
789
|
/**
|
|
@@ -887,7 +901,8 @@ var CATCH_UP_ASKER = {
|
|
|
887
901
|
write: true,
|
|
888
902
|
signal: false,
|
|
889
903
|
watch: false,
|
|
890
|
-
watchWrite: false
|
|
904
|
+
watchWrite: false,
|
|
905
|
+
departed: false
|
|
891
906
|
};
|
|
892
907
|
/** Keys whose last relay is remembered for their next read (`resolve`): a profile's links and lookups, with room. */
|
|
893
908
|
var READ_TURNS_KEPT = 512;
|
|
@@ -968,6 +983,8 @@ var RelayTransport = class {
|
|
|
968
983
|
spentWatch = /* @__PURE__ */ new Map();
|
|
969
984
|
/** Groups' reads past the limit on each relay in the last `GROUP_OVER_PACE_MS` (`GROUP_SIGNALING_ALLOWANCE_SHARE`). */
|
|
970
985
|
spentGroupOver = /* @__PURE__ */ new Map();
|
|
986
|
+
/** Per contact, its departed reads past the limit in the last minute, every relay together (`DEPARTED_READS`). */
|
|
987
|
+
departedOver = /* @__PURE__ */ new Map();
|
|
971
988
|
/**
|
|
972
989
|
* Per key a group reads (not in the background), when a read of it last went out and when it last asked: whose turn
|
|
973
990
|
* it is under the watch share (`WATCH_SHARE`) and while the groups' reads are rationed (`GROUP_RATION_WINDOW_MS`).
|
|
@@ -1575,6 +1592,7 @@ var RelayTransport = class {
|
|
|
1575
1592
|
}
|
|
1576
1593
|
if (writer) this.writeWaiting.delete(`${writer} ${relay}`);
|
|
1577
1594
|
if (isGroupSignal(who) && !who.write && this.spent.get(relay).length >= this.limitOf(relay)) this.spentGroupOver.get(relay).push(now);
|
|
1595
|
+
if (isDepartedRead(who) && this.spent.get(relay).length >= this.limitOf(relay)) this.departedOverAdd(key, now);
|
|
1578
1596
|
this.spent.get(relay).push(now);
|
|
1579
1597
|
if (who.background) this.spentBackground.get(relay).push(now);
|
|
1580
1598
|
if (isGroupUrgentRead(who)) this.spentGroupUrgent.get(relay).push(now);
|
|
@@ -1597,7 +1615,8 @@ var RelayTransport = class {
|
|
|
1597
1615
|
*
|
|
1598
1616
|
* Who goes first: a chat's refused write holds back everything but chat writes; a group's refused write holds back
|
|
1599
1617
|
* group reads and background requests, never a chat's. Background requests yield to a link that signals. A chat's
|
|
1600
|
-
* signaling goes over the limit by its allowance (`SIGNALING_ALLOWANCE_SHARE`)
|
|
1618
|
+
* signaling goes over the limit by its allowance (`SIGNALING_ALLOWANCE_SHARE`), and so do a few of its reads of a
|
|
1619
|
+
* contact that just went away (`DEPARTED_READS`).
|
|
1601
1620
|
*/
|
|
1602
1621
|
heldFor(relay, who, now = Date.now(), key) {
|
|
1603
1622
|
const recent = (this.spent.get(relay) ?? []).filter((at) => now - at < 6e4);
|
|
@@ -1606,7 +1625,8 @@ var RelayTransport = class {
|
|
|
1606
1625
|
this.spentBackground.set(relay, recentBackground);
|
|
1607
1626
|
const over = (list, limit) => list.length >= limit ? list[list.length - limit] + 6e4 - now : 0;
|
|
1608
1627
|
const limit = this.limitOf(relay);
|
|
1609
|
-
const
|
|
1628
|
+
const departed = isDepartedRead(who) && key !== void 0 && recent.length >= limit && this.departedReads(key, now) < 6;
|
|
1629
|
+
const share = isChatSignal(who) || departed ? SIGNALING_ALLOWANCE_SHARE : isGroupSignal(who) ? GROUP_SIGNALING_ALLOWANCE_SHARE : 0;
|
|
1610
1630
|
let wait = over(recent, limit + (share ? Math.max(0, Math.floor(limit * share) - (who.write ? 0 : 1)) : 0));
|
|
1611
1631
|
const groupOver = (this.spentGroupOver.get(relay) ?? []).filter((at) => now - at < GROUP_OVER_PACE_MS);
|
|
1612
1632
|
this.spentGroupOver.set(relay, groupOver);
|
|
@@ -1646,7 +1666,7 @@ var RelayTransport = class {
|
|
|
1646
1666
|
const at = this.writeWaiting.get(`${lane} ${relay}`);
|
|
1647
1667
|
return at !== void 0 && now - at < 5e3 ? at + WRITE_FIRST_MS - now : 0;
|
|
1648
1668
|
};
|
|
1649
|
-
if (writer !== "chat") wait = Math.max(wait, waiting("chat"));
|
|
1669
|
+
if (writer !== "chat" && !departed) wait = Math.max(wait, waiting("chat"));
|
|
1650
1670
|
if (writer === null && who.group && !pastLimit || who.background) wait = Math.max(wait, waiting("group"));
|
|
1651
1671
|
return Math.max(wait, 0);
|
|
1652
1672
|
}
|
|
@@ -1677,6 +1697,18 @@ var RelayTransport = class {
|
|
|
1677
1697
|
}
|
|
1678
1698
|
return ahead;
|
|
1679
1699
|
}
|
|
1700
|
+
/** This contact's departed reads past the limit in the last minute (`DEPARTED_READS`). */
|
|
1701
|
+
departedReads(key, now) {
|
|
1702
|
+
const reads = (this.departedOver.get(key) ?? []).filter((at) => now - at < 6e4);
|
|
1703
|
+
if (reads.length) this.departedOver.set(key, reads);
|
|
1704
|
+
else this.departedOver.delete(key);
|
|
1705
|
+
return reads.length;
|
|
1706
|
+
}
|
|
1707
|
+
/** One more, the contacts with none in the last minute dropped. */
|
|
1708
|
+
departedOverAdd(key, now) {
|
|
1709
|
+
for (const [other, reads] of this.departedOver) if (now - reads[reads.length - 1] >= 6e4) this.departedOver.delete(other);
|
|
1710
|
+
this.departedOver.set(key, [...this.departedOver.get(key) ?? [], now]);
|
|
1711
|
+
}
|
|
1680
1712
|
chatNeed(key) {
|
|
1681
1713
|
let need = this.chatNeeds.get(key);
|
|
1682
1714
|
if (!need) this.chatNeeds.set(key, need = {
|
|
@@ -1987,7 +2019,8 @@ var isZero = (bytes) => bytes.every((b) => b === 0);
|
|
|
1987
2019
|
* A device's name as its slot holds it: UTF-8, cut at a character boundary to 16 bytes, and with no NUL (the padding).
|
|
1988
2020
|
* A character is what a person sees as one (a grapheme), so a cut never leaves half an emoji or a lone joiner.
|
|
1989
2021
|
* A name is display text a device chooses for itself and every device of the set shows: it is cleaned as a nickname is
|
|
1990
|
-
* (`sanitizeDisplayText`: no control, invisible or direction-changing character
|
|
2022
|
+
* (`sanitizeDisplayText`: no control, invisible or direction-changing character, and empty when nothing of it shows)
|
|
2023
|
+
* and trimmed, made and read alike.
|
|
1991
2024
|
*/
|
|
1992
2025
|
function turnName(name) {
|
|
1993
2026
|
const clean = stripHidden(name).trim();
|
|
@@ -2000,7 +2033,7 @@ function turnName(name) {
|
|
|
2000
2033
|
out += character;
|
|
2001
2034
|
length += size;
|
|
2002
2035
|
}
|
|
2003
|
-
return out.trimEnd();
|
|
2036
|
+
return hasVisible(out) ? out.trimEnd() : "";
|
|
2004
2037
|
}
|
|
2005
2038
|
function u32$3(value) {
|
|
2006
2039
|
return Uint8Array.of(value >>> 24 & 255, value >>> 16 & 255, value >>> 8 & 255, value & 255);
|
|
@@ -5532,6 +5565,13 @@ var WATCH_PEER_MS = 12e4;
|
|
|
5532
5565
|
* past the 12 s such a connection is given (`DISCONNECT_GRACE_MS`), by when it is back or the session is gone.
|
|
5533
5566
|
*/
|
|
5534
5567
|
var STALLED_LOOK_MS = 15e3;
|
|
5568
|
+
/**
|
|
5569
|
+
* For this long after its contact went away from a live session, a chat's reads of it say so (`PkarrRequestOptions.departed`):
|
|
5570
|
+
* a few may go over the relays' budget (`DEPARTED_READS` in relay.ts). The fast window that follows a goodbye
|
|
5571
|
+
* (`EXPECT_PEER_MS`): an app that restarts has its offer out well within it (2.5 to 10 s after its goodbye in the lab of
|
|
5572
|
+
* #1441, on a loaded host), and past it the race's DHT dial has brought the chat back anyway.
|
|
5573
|
+
*/
|
|
5574
|
+
var DEPARTED_READ_MS = EXPECT_PEER_MS;
|
|
5535
5575
|
var PUBLISH_RETRY_MS = 4e3;
|
|
5536
5576
|
/**
|
|
5537
5577
|
* A packet the relays' request budget held back goes again when the budget frees a request, and at least this often
|
|
@@ -5592,6 +5632,8 @@ var LinkSession = class {
|
|
|
5592
5632
|
watchUntil = 0;
|
|
5593
5633
|
/** The time of the peer's packet read last when it went away (`watchPeer`): a newer one that advertises says it is back. */
|
|
5594
5634
|
watchFrom = 0;
|
|
5635
|
+
/** Until when the reads of a contact that went away are its first ones (`DEPARTED_READ_MS`). */
|
|
5636
|
+
departedUntil = 0;
|
|
5595
5637
|
publishRetryTimer = null;
|
|
5596
5638
|
active = false;
|
|
5597
5639
|
connected = false;
|
|
@@ -5749,6 +5791,7 @@ var LinkSession = class {
|
|
|
5749
5791
|
watchPeer() {
|
|
5750
5792
|
this.watchUntil = Math.max(this.watchUntil, Date.now() + WATCH_PEER_MS);
|
|
5751
5793
|
this.watchFrom = this.presence.lastPacketAt;
|
|
5794
|
+
this.departedUntil = Date.now() + DEPARTED_READ_MS;
|
|
5752
5795
|
this.expectPeer();
|
|
5753
5796
|
}
|
|
5754
5797
|
/** The data link carries chat and services while it is up. */
|
|
@@ -6017,11 +6060,13 @@ var LinkSession = class {
|
|
|
6017
6060
|
const pace = this.pace();
|
|
6018
6061
|
const signal = pace === "fast" && this.fastStepsAfter > 0 && this.rtcSignalOut !== null;
|
|
6019
6062
|
const watch = !signal && this.watching();
|
|
6063
|
+
const departed = !signal && Date.now() < this.departedUntil;
|
|
6020
6064
|
const packet = await this.transport.resolve(this.peerPubKeyZ32, {
|
|
6021
6065
|
background: pace === "background" || pace === "connected",
|
|
6022
6066
|
urgent: pace === "fast",
|
|
6023
6067
|
...signal && { signal },
|
|
6024
|
-
...watch && { watch }
|
|
6068
|
+
...watch && { watch },
|
|
6069
|
+
...departed && { departed }
|
|
6025
6070
|
});
|
|
6026
6071
|
if (!this.running) return;
|
|
6027
6072
|
this.discoveryResult("read");
|
|
@@ -7194,11 +7239,23 @@ async function signAppRevocation(statement, signer) {
|
|
|
7194
7239
|
signature
|
|
7195
7240
|
};
|
|
7196
7241
|
}
|
|
7242
|
+
/** What a checked revocation is kept by: the SHA-256 of its canonical bytes (statement and signature), base64url. */
|
|
7243
|
+
function appRevocationId(revocation) {
|
|
7244
|
+
return toBase64Url(sha256(canonicalJsonBytes(revocation)));
|
|
7245
|
+
}
|
|
7197
7246
|
/**
|
|
7198
7247
|
* Reads one signed revocation (a parsed value, from `ghostly-revoke.json` or a store index's `revoked`): its shape, then
|
|
7199
|
-
* the signature under `ghostly-revoke/1` by the publisher its `app` names.
|
|
7248
|
+
* the signature under `ghostly-revoke/1` by the publisher its `app` names. One that `checked` holds verified before, so
|
|
7249
|
+
* its signature is not checked again; one that verifies now is added to it.
|
|
7200
7250
|
*/
|
|
7201
|
-
function readAppRevocation(value) {
|
|
7251
|
+
function readAppRevocation(value, checked) {
|
|
7252
|
+
const read = readAppRevocationShape(value);
|
|
7253
|
+
if (!read.ok) return read;
|
|
7254
|
+
const verified = verifyAppRevocation(read.revocation, checked);
|
|
7255
|
+
return verified.ok ? read : verified;
|
|
7256
|
+
}
|
|
7257
|
+
/** The first half of `readAppRevocation`: the shape of a signed revocation, with no signature checked. */
|
|
7258
|
+
function readAppRevocationShape(value) {
|
|
7202
7259
|
if (!exactKeys$1(value, ["statement", "signature"])) return {
|
|
7203
7260
|
ok: false,
|
|
7204
7261
|
reason: "bad-revocation",
|
|
@@ -7214,17 +7271,22 @@ function readAppRevocation(value) {
|
|
|
7214
7271
|
ok: false,
|
|
7215
7272
|
reason: "bad-signature-statement"
|
|
7216
7273
|
};
|
|
7217
|
-
const statement = value.statement;
|
|
7218
|
-
const checked = verifyAppSignature(APP_PREFIXES.revoke, canonicalJsonBytes(statement), value.signature, appRefPublisher(statement.app));
|
|
7219
|
-
if (!checked.ok) return checked;
|
|
7220
7274
|
return {
|
|
7221
7275
|
ok: true,
|
|
7222
7276
|
revocation: {
|
|
7223
|
-
statement,
|
|
7277
|
+
statement: value.statement,
|
|
7224
7278
|
signature: value.signature
|
|
7225
7279
|
}
|
|
7226
7280
|
};
|
|
7227
7281
|
}
|
|
7282
|
+
/** The second half: the signature of a revocation of the right shape, unless `checked` holds it. */
|
|
7283
|
+
function verifyAppRevocation(revocation, checked) {
|
|
7284
|
+
const id = checked && appRevocationId(revocation);
|
|
7285
|
+
if (id !== void 0 && checked.has(id)) return { ok: true };
|
|
7286
|
+
const verified = verifyAppSignature(APP_PREFIXES.revoke, canonicalJsonBytes(revocation.statement), revocation.signature, appRefPublisher(revocation.statement.app));
|
|
7287
|
+
if (verified.ok && id !== void 0) checked.add(id);
|
|
7288
|
+
return verified;
|
|
7289
|
+
}
|
|
7228
7290
|
/**
|
|
7229
7291
|
* Reads `ghostly-revoke.json`: canonical bytes holding a list of signed revocations. One that does not verify refuses the
|
|
7230
7292
|
* file, as a store index's `revoked` does.
|
|
@@ -9380,6 +9442,16 @@ var DhtDelivery = class DhtDelivery {
|
|
|
9380
9442
|
}
|
|
9381
9443
|
this.fastUntil = 0;
|
|
9382
9444
|
this.signalReads = 0;
|
|
9445
|
+
if (this.options.credentials.peerKey && (!this.state.wasLive || this.state.inviteTaken)) this.serialize(async () => {
|
|
9446
|
+
const taken = this.state.inviteTaken;
|
|
9447
|
+
if (taken) traceLink(this.from, "invite-untaken", {});
|
|
9448
|
+
const { inviteTaken: _taken, ...kept } = this.state;
|
|
9449
|
+
await this.persist({
|
|
9450
|
+
...kept,
|
|
9451
|
+
wasLive: true
|
|
9452
|
+
});
|
|
9453
|
+
if (taken) this.changed();
|
|
9454
|
+
}).catch(() => {});
|
|
9383
9455
|
if (this.options.credentials.peerKey && this.state.peerMode) this.tick();
|
|
9384
9456
|
else this.schedule();
|
|
9385
9457
|
}
|
|
@@ -9835,14 +9907,15 @@ var DhtDelivery = class DhtDelivery {
|
|
|
9835
9907
|
}
|
|
9836
9908
|
/**
|
|
9837
9909
|
* The contact's envelope is sealed to another key (`open`): it pinned someone else who used the same invite first, so
|
|
9838
|
-
* nothing of this side's will ever be read. Said
|
|
9839
|
-
*
|
|
9840
|
-
*
|
|
9910
|
+
* nothing of this side's will ever be read. Said only while this side is not proven the one it pinned: no envelope
|
|
9911
|
+
* sealed to this side read (`peerPinned`), no text confirmed, never live (`wasLive`). The hint is sealed under the
|
|
9912
|
+
* invite's key, which any copy of it holds, so past that proof it says nothing of the contact: a passive warning,
|
|
9913
|
+
* taken back by the contact's next envelope that opens here, or by a live session.
|
|
9841
9914
|
*/
|
|
9842
9915
|
async sealedToAnother(sender) {
|
|
9843
9916
|
const expected = this.options.credentials.peerKey ?? this.options.credentials.expectedPeerKey;
|
|
9844
9917
|
if (this.state.inviteTaken || !expected || sender !== expected) return;
|
|
9845
|
-
if (this.state.peerPinned
|
|
9918
|
+
if (this.state.peerPinned || this.state.confirmed || this.state.wasLive || this.live) return;
|
|
9846
9919
|
traceLink(this.from, "invite-taken", {});
|
|
9847
9920
|
await this.persist({
|
|
9848
9921
|
...this.state,
|
|
@@ -12535,219 +12608,491 @@ async function fitSignedPairedSignalWith(signal, signer, from, to, fits) {
|
|
|
12535
12608
|
}
|
|
12536
12609
|
}
|
|
12537
12610
|
//#endregion
|
|
12538
|
-
//#region ../core/src/
|
|
12539
|
-
|
|
12540
|
-
|
|
12541
|
-
|
|
12542
|
-
|
|
12543
|
-
|
|
12544
|
-
|
|
12545
|
-
|
|
12546
|
-
|
|
12547
|
-
*
|
|
12548
|
-
|
|
12549
|
-
|
|
12550
|
-
|
|
12551
|
-
|
|
12552
|
-
|
|
12553
|
-
|
|
12554
|
-
|
|
12555
|
-
|
|
12556
|
-
|
|
12557
|
-
|
|
12558
|
-
|
|
12559
|
-
|
|
12560
|
-
|
|
12561
|
-
|
|
12562
|
-
|
|
12563
|
-
* a day covers any clock that is merely wrong. Nothing else about a pointer or a bundle is held against this clock.
|
|
12564
|
-
*/
|
|
12565
|
-
clockSkewMs: 864e5
|
|
12566
|
-
};
|
|
12567
|
-
/**
|
|
12568
|
-
* How far before its own clock a sender dates its pointer and its manifest, and ends the lifetimes it gives. A reader
|
|
12569
|
-
* refuses nothing for those dates: `rev` orders pointers and `seq` orders items, both kept by the reader. Apps up to
|
|
12570
|
-
* 1.0.2 refuse a pointer or a manifest dated more than a minute past their clock, and an expiry more than a minute
|
|
12571
|
-
* past one lifetime from now, each without a word: the items of a sender whose clock ran two minutes fast were never
|
|
12572
|
-
* fetched. Dated back, a sender up to about eleven minutes ahead is read by them.
|
|
12573
|
-
*/
|
|
12574
|
-
var HOLD_DATED_BACK_MS = 6e5;
|
|
12575
|
-
var KINDS$1 = [
|
|
12576
|
-
"text",
|
|
12577
|
-
"file",
|
|
12578
|
-
"pay-req",
|
|
12579
|
-
"manifest"
|
|
12611
|
+
//#region ../core/src/domainProofs.ts
|
|
12612
|
+
var DNS_LABEL = "_ghostly";
|
|
12613
|
+
var WELL_KNOWN_PATH = "/.well-known/ghostly.json";
|
|
12614
|
+
var NIP05_PATH = "/.well-known/nostr.json?name=_";
|
|
12615
|
+
/** Everything read from a domain is bounded; these match the fetch limits. */
|
|
12616
|
+
var DOMAIN_FILE_MAX_BYTES = 16384;
|
|
12617
|
+
var z32 = /^[ybndrfg8ejkmcpqxot1uwisza345h769]{52}$/;
|
|
12618
|
+
var hex64 = /^[a-f0-9]{64}$/;
|
|
12619
|
+
/** Names that never resolve publicly (RFC 6761, 6762, 7686, 8375, 9476) or that
|
|
12620
|
+
* only ever mean a private network. A proof for them could only point inward. */
|
|
12621
|
+
var SPECIAL_USE = [
|
|
12622
|
+
"localhost",
|
|
12623
|
+
"local",
|
|
12624
|
+
"internal",
|
|
12625
|
+
"lan",
|
|
12626
|
+
"home",
|
|
12627
|
+
"corp",
|
|
12628
|
+
"intranet",
|
|
12629
|
+
"private",
|
|
12630
|
+
"test",
|
|
12631
|
+
"example",
|
|
12632
|
+
"invalid",
|
|
12633
|
+
"onion",
|
|
12634
|
+
"alt",
|
|
12635
|
+
"arpa"
|
|
12580
12636
|
];
|
|
12581
|
-
var WIRE_ID = /^[A-Za-z0-9_-]{8,64}$/;
|
|
12582
|
-
var MAILBOX = /^[A-Za-z0-9_-]{22}$/;
|
|
12583
|
-
var SIG$3 = /^[A-Za-z0-9_-]{86}$/;
|
|
12584
|
-
var MAGIC$2 = utf8Encode("GHLD");
|
|
12585
|
-
var VERSION = 1;
|
|
12586
|
-
var NONCE$2 = 24;
|
|
12587
|
-
/** Why a bundle, manifest or pointer was refused. Never a reason to trust it a little. */
|
|
12588
|
-
var HoldRefusedError = class extends Error {
|
|
12589
|
-
reason;
|
|
12590
|
-
constructor(reason, message) {
|
|
12591
|
-
super(message);
|
|
12592
|
-
this.reason = reason;
|
|
12593
|
-
}
|
|
12594
|
-
};
|
|
12595
|
-
var bodyDigest = (body) => toBase64Url(sha256(body));
|
|
12596
|
-
var newMailbox = () => toBase64Url(randomBytes(16)).slice(0, 22);
|
|
12597
12637
|
/**
|
|
12598
|
-
*
|
|
12599
|
-
*
|
|
12638
|
+
* Normalizes what a person typed ("Example.COM.", "https://example.com/") into
|
|
12639
|
+
* a lowercase ASCII host name, or throws with a message fit for the UI.
|
|
12640
|
+
* Internationalized names become punycode (xn--), the form DNS carries.
|
|
12600
12641
|
*/
|
|
12601
|
-
|
|
12602
|
-
|
|
12603
|
-
|
|
12604
|
-
|
|
12605
|
-
|
|
12606
|
-
|
|
12607
|
-
identity;
|
|
12608
|
-
/** Where the contact publishes its pointer. */
|
|
12609
|
-
peerAddress;
|
|
12610
|
-
seed;
|
|
12611
|
-
sealKey;
|
|
12612
|
-
constructor(params, participationSeedB64, peerKey) {
|
|
12613
|
-
this.from = identityFromSeedB64(params.seedB64).pubKeyZ32;
|
|
12614
|
-
this.to = params.peerPubKeyZ32;
|
|
12615
|
-
const mine = identityFromSeedB64(participationSeedB64);
|
|
12616
|
-
this.seed = mine.seed;
|
|
12617
|
-
this.me = mine.pubKeyZ32;
|
|
12618
|
-
this.peer = peerKey;
|
|
12619
|
-
const secret = fromBase64Url(params.encKeyB64);
|
|
12620
|
-
const context = JSON.stringify(["ghostly-hold/1", [this.from, this.to].sort()]);
|
|
12621
|
-
const derive = (label) => hkdf(sha256, secret, utf8Encode(context), utf8Encode(label), 32);
|
|
12622
|
-
this.identity = identityFromSeed(derive(`pointer:${this.from}`));
|
|
12623
|
-
this.peerAddress = identityFromSeed(derive(`pointer:${this.to}`)).pubKeyZ32;
|
|
12624
|
-
const shared = x25519.getSharedSecret(ed25519.utils.toMontgomerySecret(this.seed), ed25519.utils.toMontgomery(publicKeyFromZ32(peerKey)));
|
|
12625
|
-
this.sealKey = hkdf(sha256, shared, derive("envelope"), utf8Encode("ghostly-hold-envelope/1"), 32);
|
|
12626
|
-
}
|
|
12627
|
-
/** Seals one item for the contact: signed with my participation key, encrypted to our shared key. */
|
|
12628
|
-
seal(item, body) {
|
|
12629
|
-
const fields = [
|
|
12630
|
-
VERSION,
|
|
12631
|
-
this.from,
|
|
12632
|
-
this.to,
|
|
12633
|
-
this.me,
|
|
12634
|
-
this.peer,
|
|
12635
|
-
item.mailbox,
|
|
12636
|
-
item.seq,
|
|
12637
|
-
item.id,
|
|
12638
|
-
item.ts,
|
|
12639
|
-
item.kind,
|
|
12640
|
-
item.meta ?? null,
|
|
12641
|
-
bodyDigest(body),
|
|
12642
|
-
item.expires
|
|
12643
|
-
];
|
|
12644
|
-
const signature = toBase64Url(sign(utf8Encode(JSON.stringify(["ghostly-hold-bundle", fields])), this.seed));
|
|
12645
|
-
const header = utf8Encode(JSON.stringify([fields, signature]));
|
|
12646
|
-
const length = /* @__PURE__ */ new Uint8Array(4);
|
|
12647
|
-
new DataView(length.buffer).setUint32(0, header.length);
|
|
12648
|
-
const nonce = randomBytes(NONCE$2);
|
|
12649
|
-
const box = xsalsa20poly1305(this.sealKey, nonce).encrypt(concatBytes$1(length, header, body));
|
|
12650
|
-
return concatBytes$1(MAGIC$2, new Uint8Array([VERSION]), nonce, box);
|
|
12651
|
-
}
|
|
12652
|
-
/**
|
|
12653
|
-
* Opens a bundle the contact sealed for me. Everything is checked before anything is returned: the
|
|
12654
|
-
* authenticated encryption, the header's shape, that it names this link, this contact and me, the
|
|
12655
|
-
* signature, the body digest and the limits of its kind.
|
|
12656
|
-
*/
|
|
12657
|
-
open(bytes, expect = {}) {
|
|
12658
|
-
const now = expect.now ?? Date.now();
|
|
12659
|
-
if (bytes.length > (expect.maxBytes ?? HOLD_LIMITS.maxBundleBytes)) throw new HoldRefusedError("size", "The held item is larger than allowed");
|
|
12660
|
-
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");
|
|
12661
|
-
if (bytes[MAGIC$2.length] !== VERSION) throw new HoldRefusedError("format", "Held item from a newer Ghostly");
|
|
12662
|
-
let plain;
|
|
12663
|
-
try {
|
|
12664
|
-
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));
|
|
12665
|
-
} catch {
|
|
12666
|
-
throw new HoldRefusedError("tampered", "The held item was changed or is not for this chat");
|
|
12667
|
-
}
|
|
12668
|
-
if (plain.length < 4) throw new HoldRefusedError("format", "Not a held item");
|
|
12669
|
-
const headerLength = new DataView(plain.buffer, plain.byteOffset, 4).getUint32(0);
|
|
12670
|
-
if (headerLength > 131072 || 4 + headerLength > plain.length) throw new HoldRefusedError("format", "Not a held item");
|
|
12671
|
-
let parsed;
|
|
12642
|
+
function normalizeDomain(input) {
|
|
12643
|
+
if (typeof input !== "string") throw new Error("Enter a domain name, like example.com");
|
|
12644
|
+
let value = input.trim();
|
|
12645
|
+
if (!value || value.length > 300) throw new Error("Enter a domain name, like example.com");
|
|
12646
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(value)) {
|
|
12647
|
+
let url;
|
|
12672
12648
|
try {
|
|
12673
|
-
|
|
12649
|
+
url = new URL(value);
|
|
12674
12650
|
} catch {
|
|
12675
|
-
throw new
|
|
12651
|
+
throw new Error("Enter a domain name, like example.com");
|
|
12676
12652
|
}
|
|
12677
|
-
if (
|
|
12678
|
-
|
|
12679
|
-
|
|
12680
|
-
if (
|
|
12681
|
-
if (from !== this.to || to !== this.from || recipient !== this.me) throw new HoldRefusedError("not-for-me", "The held item is for another chat");
|
|
12682
|
-
if (author !== this.peer) throw new HoldRefusedError("author", "The held item was not signed by this contact");
|
|
12683
|
-
if (expect.mailbox && mailbox !== expect.mailbox) throw new HoldRefusedError("not-for-me", "The held item is from another mailbox");
|
|
12684
|
-
let valid;
|
|
12653
|
+
if (url.protocol !== "https:" || url.username || url.password || url.port || url.pathname !== "/" && url.pathname !== "" || url.search || url.hash) throw new Error("Enter only the domain name, without a path or port");
|
|
12654
|
+
value = url.hostname;
|
|
12655
|
+
} else {
|
|
12656
|
+
if (/[/:@?#\s\\]/.test(value)) throw new Error("Enter only the domain name, without a path or port");
|
|
12685
12657
|
try {
|
|
12686
|
-
|
|
12658
|
+
value = new URL(`https://${value}`).hostname;
|
|
12687
12659
|
} catch {
|
|
12688
|
-
valid
|
|
12660
|
+
throw new Error("That is not a valid domain name");
|
|
12689
12661
|
}
|
|
12690
|
-
if (!valid) throw new HoldRefusedError("signature", "The held item's signature does not check out");
|
|
12691
|
-
const body = plain.subarray(4 + headerLength);
|
|
12692
|
-
if (bodyDigest(body) !== digest) throw new HoldRefusedError("digest", "The held item's content does not match its header");
|
|
12693
|
-
if (expires <= ts || expires > now + HOLD_LIMITS.ttlMs + HOLD_LIMITS.clockSkewMs) throw new HoldRefusedError("limits", "The held item's lifetime is out of bounds");
|
|
12694
|
-
const header = {
|
|
12695
|
-
v: 1,
|
|
12696
|
-
from,
|
|
12697
|
-
to,
|
|
12698
|
-
author,
|
|
12699
|
-
recipient,
|
|
12700
|
-
mailbox,
|
|
12701
|
-
seq,
|
|
12702
|
-
id,
|
|
12703
|
-
ts,
|
|
12704
|
-
kind,
|
|
12705
|
-
meta,
|
|
12706
|
-
digest,
|
|
12707
|
-
expires
|
|
12708
|
-
};
|
|
12709
|
-
checkKind(header, body);
|
|
12710
|
-
return {
|
|
12711
|
-
header,
|
|
12712
|
-
body
|
|
12713
|
-
};
|
|
12714
|
-
}
|
|
12715
|
-
/** My pointer, as records to publish under `identity`. Throws when it cannot fit a packet. */
|
|
12716
|
-
pointerRecords(pointer, now = Date.now()) {
|
|
12717
|
-
const body = [
|
|
12718
|
-
VERSION,
|
|
12719
|
-
pointer.rev,
|
|
12720
|
-
pointer.issued,
|
|
12721
|
-
pointer.expires,
|
|
12722
|
-
pointer.manifestUrl,
|
|
12723
|
-
pointer.top,
|
|
12724
|
-
pointer.ack,
|
|
12725
|
-
pointer.count,
|
|
12726
|
-
pointer.bytes,
|
|
12727
|
-
pointer.refused.slice(-32)
|
|
12728
|
-
];
|
|
12729
|
-
const signature = toBase64Url(sign(utf8Encode(JSON.stringify([
|
|
12730
|
-
"ghostly-hold-pointer",
|
|
12731
|
-
this.from,
|
|
12732
|
-
this.to,
|
|
12733
|
-
body
|
|
12734
|
-
])), this.seed));
|
|
12735
|
-
const ttl = Math.min(86400, Math.max(60, Math.ceil((pointer.expires - now) / 1e3)));
|
|
12736
|
-
const records = [{
|
|
12737
|
-
label: "_hold",
|
|
12738
|
-
value: encrypt(JSON.stringify([body, signature]), this.sealKey),
|
|
12739
|
-
ttl
|
|
12740
|
-
}];
|
|
12741
|
-
if (measureRecords(this.identity.pubKeyZ32, records) > 1e3) throw new Error("The storage address is too long for a DHT packet. Use a shorter endpoint or prefix.");
|
|
12742
|
-
return records;
|
|
12743
12662
|
}
|
|
12744
|
-
|
|
12745
|
-
|
|
12746
|
-
|
|
12747
|
-
|
|
12748
|
-
|
|
12749
|
-
|
|
12750
|
-
|
|
12663
|
+
value = value.toLowerCase().replace(/\.$/, "");
|
|
12664
|
+
if (value.length > 244) throw new Error("That domain name is too long");
|
|
12665
|
+
const labels = value.split(".");
|
|
12666
|
+
if (labels.length < 2) throw new Error("Use a full domain name, like example.com");
|
|
12667
|
+
for (const label of labels) if (!/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/.test(label)) throw new Error("That is not a valid domain name");
|
|
12668
|
+
const tld = labels[labels.length - 1];
|
|
12669
|
+
if (/^[0-9]+$/.test(tld) || /^\[/.test(value)) throw new Error("Use a domain name, not an IP address");
|
|
12670
|
+
if (SPECIAL_USE.includes(tld) || labels.length >= 2 && labels.slice(-2).join(".") === "home.arpa") throw new Error("That name only works on private networks");
|
|
12671
|
+
return value;
|
|
12672
|
+
}
|
|
12673
|
+
var dnsProofName = (domain) => `${DNS_LABEL}.${normalizeDomain(domain)}`;
|
|
12674
|
+
var wellKnownUrl = (domain, method = "https") => `https://${normalizeDomain(domain)}${method === "nip05" ? NIP05_PATH : WELL_KNOWN_PATH}`;
|
|
12675
|
+
function validDomainRecord(r) {
|
|
12676
|
+
if (!r || typeof r.key !== "string" || typeof r.proof !== "string" || !z32.test(r.key) || !hex64.test(r.proof)) return false;
|
|
12677
|
+
try {
|
|
12678
|
+
return toZ32(fromZ32(r.key)) === r.key;
|
|
12679
|
+
} catch {
|
|
12680
|
+
return false;
|
|
12681
|
+
}
|
|
12682
|
+
}
|
|
12683
|
+
/** The TXT record value, in the `tag=value; …` style people know from DKIM. */
|
|
12684
|
+
function domainTxtRecord(r) {
|
|
12685
|
+
if (!validDomainRecord(r)) throw new Error("Invalid domain record");
|
|
12686
|
+
return `v=ghostly1; key=${r.key}; proof=${r.proof}`;
|
|
12687
|
+
}
|
|
12688
|
+
/** Parses one TXT string. Undefined for anything that is not ours or not exactly
|
|
12689
|
+
* well-formed; unknown extra tags are ignored for forward compatibility. */
|
|
12690
|
+
function parseDomainTxt(value) {
|
|
12691
|
+
if (typeof value !== "string" || utf8Encode(value).length > 1024) return;
|
|
12692
|
+
const tags = /* @__PURE__ */ new Map();
|
|
12693
|
+
const parts = value.split(";").map((p) => p.trim()).filter(Boolean);
|
|
12694
|
+
for (const part of parts) {
|
|
12695
|
+
const eq = part.indexOf("=");
|
|
12696
|
+
if (eq < 1) return;
|
|
12697
|
+
const name = part.slice(0, eq).trim().toLowerCase();
|
|
12698
|
+
if (tags.has(name)) return;
|
|
12699
|
+
tags.set(name, part.slice(eq + 1).trim());
|
|
12700
|
+
}
|
|
12701
|
+
if (!parts[0]?.toLowerCase().startsWith("v=") || tags.get("v") !== "ghostly1") return;
|
|
12702
|
+
const r = {
|
|
12703
|
+
key: tags.get("key"),
|
|
12704
|
+
proof: tags.get("proof")
|
|
12705
|
+
};
|
|
12706
|
+
return validDomainRecord(r) ? r : void 0;
|
|
12707
|
+
}
|
|
12708
|
+
var addRecord = (list, r) => {
|
|
12709
|
+
if (!list.some((x) => x.key === r.key && x.proof === r.proof)) list.push(r);
|
|
12710
|
+
};
|
|
12711
|
+
/** All records among the TXT strings of `_ghostly.<domain>`; other strings are skipped. */
|
|
12712
|
+
function recordsFromTxt(values) {
|
|
12713
|
+
const out = [];
|
|
12714
|
+
for (const value of values.slice(0, 32)) {
|
|
12715
|
+
const r = parseDomainTxt(value);
|
|
12716
|
+
if (r) addRecord(out, r);
|
|
12717
|
+
}
|
|
12718
|
+
return out.slice(0, 8);
|
|
12719
|
+
}
|
|
12720
|
+
/** The body of `/.well-known/ghostly.json`. */
|
|
12721
|
+
function domainWellKnownFile(records) {
|
|
12722
|
+
if (!records.length || records.length > 8 || !records.every(validDomainRecord)) throw new Error("Invalid domain records");
|
|
12723
|
+
return JSON.stringify({
|
|
12724
|
+
ghostly: 1,
|
|
12725
|
+
proofs: records.map((r) => ({
|
|
12726
|
+
key: r.key,
|
|
12727
|
+
proof: r.proof
|
|
12728
|
+
}))
|
|
12729
|
+
}, null, 2) + "\n";
|
|
12730
|
+
}
|
|
12731
|
+
function parseJson(text) {
|
|
12732
|
+
if (typeof text !== "string" || utf8Encode(text).length > 16384) throw new Error("File too large");
|
|
12733
|
+
return JSON.parse(text);
|
|
12734
|
+
}
|
|
12735
|
+
/** Records from `ghostly.json`. Throws on a malformed file; [] when it names none. */
|
|
12736
|
+
function recordsFromWellKnown(text) {
|
|
12737
|
+
const body = parseJson(text);
|
|
12738
|
+
if (!body || typeof body !== "object" || Array.isArray(body) || body.ghostly !== 1 || !Array.isArray(body.proofs)) throw new Error("Not a Ghostly domain file");
|
|
12739
|
+
const out = [];
|
|
12740
|
+
for (const entry of body.proofs.slice(0, 32)) {
|
|
12741
|
+
if (!entry || typeof entry !== "object") continue;
|
|
12742
|
+
const r = {
|
|
12743
|
+
key: entry.key,
|
|
12744
|
+
proof: entry.proof
|
|
12745
|
+
};
|
|
12746
|
+
if (validDomainRecord(r)) addRecord(out, r);
|
|
12747
|
+
}
|
|
12748
|
+
return out.slice(0, 8);
|
|
12749
|
+
}
|
|
12750
|
+
/** NIP-05: only the root name `_` speaks for the domain itself. Undefined when absent. */
|
|
12751
|
+
function nip05RootKey(text) {
|
|
12752
|
+
const body = parseJson(text);
|
|
12753
|
+
if (!body || typeof body !== "object" || !body.names || typeof body.names !== "object" || Array.isArray(body.names)) throw new Error("Not a NIP-05 file");
|
|
12754
|
+
const root = body.names["_"];
|
|
12755
|
+
return typeof root === "string" && hex64.test(root) ? root : void 0;
|
|
12756
|
+
}
|
|
12757
|
+
var recordsName = (records, r) => records.some((x) => x.key === r.key && x.proof === r.proof);
|
|
12758
|
+
var DNS_TYPE = {
|
|
12759
|
+
A: 1,
|
|
12760
|
+
CNAME: 5,
|
|
12761
|
+
TXT: 16,
|
|
12762
|
+
AAAA: 28
|
|
12763
|
+
};
|
|
12764
|
+
/** A recursive query for one name and type. ID 0, as RFC 8484 recommends for caching;
|
|
12765
|
+
* the AD bit asks the resolver to report DNSSEC validation (RFC 6840 §5.7). */
|
|
12766
|
+
function encodeDnsQuery(name, type) {
|
|
12767
|
+
const labels = name.replace(/\.$/, "").split(".");
|
|
12768
|
+
const parts = [Uint8Array.of(0, 0, 1, 32, 0, 1, 0, 0, 0, 0, 0, 0)];
|
|
12769
|
+
for (const label of labels) {
|
|
12770
|
+
const bytes = utf8Encode(label);
|
|
12771
|
+
if (!bytes.length || bytes.length > 63) throw new Error("Invalid DNS name");
|
|
12772
|
+
parts.push(Uint8Array.of(bytes.length), bytes);
|
|
12773
|
+
}
|
|
12774
|
+
const code = DNS_TYPE[type];
|
|
12775
|
+
parts.push(Uint8Array.of(0, code >> 8, code & 255, 0, 1));
|
|
12776
|
+
return concatBytes$1(...parts);
|
|
12777
|
+
}
|
|
12778
|
+
function readName(data, start) {
|
|
12779
|
+
const labels = [];
|
|
12780
|
+
let pos = start, next = -1, jumps = 0, length = 0;
|
|
12781
|
+
for (;;) {
|
|
12782
|
+
if (pos >= data.length) throw new Error("DNS name out of bounds");
|
|
12783
|
+
const len = data[pos];
|
|
12784
|
+
if ((len & 192) === 192) {
|
|
12785
|
+
if (pos + 1 >= data.length) throw new Error("DNS pointer out of bounds");
|
|
12786
|
+
if (next < 0) next = pos + 2;
|
|
12787
|
+
pos = (len & 63) << 8 | data[pos + 1];
|
|
12788
|
+
if (++jumps > 32) throw new Error("DNS name loops");
|
|
12789
|
+
continue;
|
|
12790
|
+
}
|
|
12791
|
+
if (len & 192) throw new Error("Unsupported DNS label");
|
|
12792
|
+
pos += 1;
|
|
12793
|
+
if (len === 0) break;
|
|
12794
|
+
if (pos + len > data.length) throw new Error("DNS label out of bounds");
|
|
12795
|
+
length += len + 1;
|
|
12796
|
+
if (length > 255) throw new Error("DNS name too long");
|
|
12797
|
+
labels.push(utf8Decode(data.subarray(pos, pos + len)).toLowerCase());
|
|
12798
|
+
pos += len;
|
|
12799
|
+
}
|
|
12800
|
+
return {
|
|
12801
|
+
name: labels.join("."),
|
|
12802
|
+
next: next < 0 ? pos : next
|
|
12803
|
+
};
|
|
12804
|
+
}
|
|
12805
|
+
/**
|
|
12806
|
+
* Decodes a resolver's answer to `encodeDnsQuery(name, type)`. Refuses a reply
|
|
12807
|
+
* to another question or a truncated one, and keeps only records of the asked
|
|
12808
|
+
* type owned by the name or by the CNAME chain the answer itself follows.
|
|
12809
|
+
*/
|
|
12810
|
+
function decodeDnsResponse(data, name, type) {
|
|
12811
|
+
if (data.length < 12) throw new Error("DNS response too short");
|
|
12812
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
12813
|
+
const flags = view.getUint16(2);
|
|
12814
|
+
if (!(flags & 32768) || flags & 30720 || flags & 512) throw new Error("Unusable DNS response");
|
|
12815
|
+
const questions = view.getUint16(4), count = view.getUint16(6);
|
|
12816
|
+
const want = name.replace(/\.$/, "").toLowerCase(), code = DNS_TYPE[type];
|
|
12817
|
+
if (questions !== 1) throw new Error("DNS response is for another question");
|
|
12818
|
+
const q = readName(data, 12);
|
|
12819
|
+
if (q.name !== want || q.next + 4 > data.length || view.getUint16(q.next) !== code) throw new Error("DNS response is for another question");
|
|
12820
|
+
let pos = q.next + 4;
|
|
12821
|
+
const all = [];
|
|
12822
|
+
for (let i = 0; i < count && i < 64; i++) {
|
|
12823
|
+
const owner = readName(data, pos);
|
|
12824
|
+
pos = owner.next;
|
|
12825
|
+
if (pos + 10 > data.length) throw new Error("DNS record out of bounds");
|
|
12826
|
+
const rtype = view.getUint16(pos), ttl = view.getUint32(pos + 4), len = view.getUint16(pos + 8);
|
|
12827
|
+
pos += 10;
|
|
12828
|
+
if (pos + len > data.length) throw new Error("DNS record out of bounds");
|
|
12829
|
+
const rdata = rtype === DNS_TYPE.CNAME ? utf8Encode(readName(data, pos).name) : data.slice(pos, pos + len);
|
|
12830
|
+
all.push({
|
|
12831
|
+
name: owner.name,
|
|
12832
|
+
type: rtype,
|
|
12833
|
+
ttl,
|
|
12834
|
+
data: rdata
|
|
12835
|
+
});
|
|
12836
|
+
pos += len;
|
|
12837
|
+
}
|
|
12838
|
+
const owners = /* @__PURE__ */ new Set([want]);
|
|
12839
|
+
for (let hop = 0; hop < 8; hop++) {
|
|
12840
|
+
const cname = all.find((a) => a.type === DNS_TYPE.CNAME && owners.has(a.name) && !owners.has(utf8Decode(a.data)));
|
|
12841
|
+
if (!cname) break;
|
|
12842
|
+
owners.add(utf8Decode(cname.data));
|
|
12843
|
+
}
|
|
12844
|
+
return {
|
|
12845
|
+
rcode: flags & 15,
|
|
12846
|
+
authenticated: !!(flags & 32),
|
|
12847
|
+
answers: all.filter((a) => a.type === code && owners.has(a.name))
|
|
12848
|
+
};
|
|
12849
|
+
}
|
|
12850
|
+
/** The TXT character-strings of one record, joined as RFC 7208 §3.3 does. */
|
|
12851
|
+
function txtValue(rdata) {
|
|
12852
|
+
const parts = [];
|
|
12853
|
+
for (let p = 0; p < rdata.length;) {
|
|
12854
|
+
const len = rdata[p];
|
|
12855
|
+
if (p + 1 + len > rdata.length) throw new Error("DNS text out of bounds");
|
|
12856
|
+
parts.push(rdata.subarray(p + 1, p + 1 + len));
|
|
12857
|
+
p += 1 + len;
|
|
12858
|
+
}
|
|
12859
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(concatBytes$1(...parts));
|
|
12860
|
+
}
|
|
12861
|
+
/** False for loopback, private, link-local, shared (CGNAT), multicast, reserved
|
|
12862
|
+
* and unspecified addresses, including IPv4 embedded in IPv6. */
|
|
12863
|
+
function isPublicAddress(address) {
|
|
12864
|
+
if (address.length === 4) {
|
|
12865
|
+
const [a, b, c] = address;
|
|
12866
|
+
return !(a === 0 || a === 10 || a === 127 || a >= 224 || a === 100 && b >= 64 && b <= 127 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 192 && b === 0 && c === 0 || a === 198 && (b === 18 || b === 19));
|
|
12867
|
+
}
|
|
12868
|
+
if (address.length !== 16) return false;
|
|
12869
|
+
const zeroTo = (n) => address.subarray(0, n).every((x) => x === 0);
|
|
12870
|
+
if (zeroTo(15) && (address[15] === 0 || address[15] === 1)) return false;
|
|
12871
|
+
if (zeroTo(10) && address[10] === 255 && address[11] === 255) return isPublicAddress(address.subarray(12));
|
|
12872
|
+
if (address[0] === 0 && address[1] === 100 && address[2] === 255 && address[3] === 155 && address.subarray(4, 12).every((x) => x === 0)) return isPublicAddress(address.subarray(12));
|
|
12873
|
+
if (zeroTo(12)) return false;
|
|
12874
|
+
if ((address[0] & 254) === 252) return false;
|
|
12875
|
+
if (address[0] === 254 && (address[1] & 192) === 128) return false;
|
|
12876
|
+
if (address[0] === 254 && (address[1] & 192) === 192) return false;
|
|
12877
|
+
if (address[0] === 255) return false;
|
|
12878
|
+
if (address[0] === 1 && address.subarray(1, 8).every((x) => x === 0)) return false;
|
|
12879
|
+
return (address[0] & 224) === 32;
|
|
12880
|
+
}
|
|
12881
|
+
//#endregion
|
|
12882
|
+
//#region ../core/src/storeForward.ts
|
|
12883
|
+
/**
|
|
12884
|
+
* Store-and-forward for an away contact (WISP 404 draft, `hold/1`).
|
|
12885
|
+
*
|
|
12886
|
+
* What a person sends while the contact is away is sealed into a *bundle* (signed by the sender's
|
|
12887
|
+
* participation key, then encrypted to a key only the two of them can derive), put in the sender's
|
|
12888
|
+
* own storage (WISP 1000), and named in a *manifest* the storage also holds. A small signed, encrypted
|
|
12889
|
+
* *pointer* record on a per-link Pkarr key tells the contact where the manifest is (a URL that expires)
|
|
12890
|
+
* and which of the contact's own bundles have been received. Storage and relays see ciphertext, sizes
|
|
12891
|
+
* and timing only. Nothing here promises forward secrecy or retention beyond the bounds below.
|
|
12892
|
+
*/
|
|
12893
|
+
var HOLD_CAPABILITY = "hold/1";
|
|
12894
|
+
var HOLD_LIMITS = {
|
|
12895
|
+
/** One bundle, ciphertext and header included: a picture, not a video. */
|
|
12896
|
+
maxBundleBytes: 8388608,
|
|
12897
|
+
/** All bundles held for one contact at once. */
|
|
12898
|
+
maxMailboxBytes: 67108864,
|
|
12899
|
+
maxMailboxBundles: 64,
|
|
12900
|
+
/** A bundle not picked up by then is dropped by its sender. The longest a presigned URL lives, too. */
|
|
12901
|
+
ttlMs: 6048e5,
|
|
12902
|
+
maxTextBytes: 16384,
|
|
12903
|
+
maxPaymentRequestBytes: 65536,
|
|
12904
|
+
maxManifestBytes: 65536,
|
|
12905
|
+
/**
|
|
12906
|
+
* How far past one lifetime from now an expiry may reach by the reader's clock: the sender's clock may be ahead, and
|
|
12907
|
+
* a day covers any clock that is merely wrong. Nothing else about a pointer or a bundle is held against this clock.
|
|
12908
|
+
*/
|
|
12909
|
+
clockSkewMs: 864e5
|
|
12910
|
+
};
|
|
12911
|
+
/**
|
|
12912
|
+
* How far before its own clock a sender dates its pointer and its manifest, and ends the lifetimes it gives. A reader
|
|
12913
|
+
* refuses nothing for those dates: `rev` orders pointers and `seq` orders items, both kept by the reader. Apps up to
|
|
12914
|
+
* 1.0.2 refuse a pointer or a manifest dated more than a minute past their clock, and an expiry more than a minute
|
|
12915
|
+
* past one lifetime from now, each without a word: the items of a sender whose clock ran two minutes fast were never
|
|
12916
|
+
* fetched. Dated back, a sender up to about eleven minutes ahead is read by them.
|
|
12917
|
+
*/
|
|
12918
|
+
var HOLD_DATED_BACK_MS = 6e5;
|
|
12919
|
+
var KINDS$1 = [
|
|
12920
|
+
"text",
|
|
12921
|
+
"file",
|
|
12922
|
+
"pay-req",
|
|
12923
|
+
"manifest"
|
|
12924
|
+
];
|
|
12925
|
+
var WIRE_ID = /^[A-Za-z0-9_-]{8,64}$/;
|
|
12926
|
+
var MAILBOX = /^[A-Za-z0-9_-]{22}$/;
|
|
12927
|
+
var SIG$3 = /^[A-Za-z0-9_-]{86}$/;
|
|
12928
|
+
var MAGIC$2 = utf8Encode("GHLD");
|
|
12929
|
+
var VERSION = 1;
|
|
12930
|
+
var NONCE$2 = 24;
|
|
12931
|
+
/** Why a bundle, manifest or pointer was refused. Never a reason to trust it a little. */
|
|
12932
|
+
var HoldRefusedError = class extends Error {
|
|
12933
|
+
reason;
|
|
12934
|
+
constructor(reason, message) {
|
|
12935
|
+
super(message);
|
|
12936
|
+
this.reason = reason;
|
|
12937
|
+
}
|
|
12938
|
+
};
|
|
12939
|
+
var bodyDigest = (body) => toBase64Url(sha256(body));
|
|
12940
|
+
var newMailbox = () => toBase64Url(randomBytes(16)).slice(0, 22);
|
|
12941
|
+
/**
|
|
12942
|
+
* Everything derived once per link for this feature: my pointer key, the contact's pointer address and
|
|
12943
|
+
* the key that seals bundles and pointers between the two participation keys. Needs a pinned contact.
|
|
12944
|
+
*/
|
|
12945
|
+
var HoldKeys = class {
|
|
12946
|
+
from;
|
|
12947
|
+
to;
|
|
12948
|
+
me;
|
|
12949
|
+
peer;
|
|
12950
|
+
/** Publishes my pointer. */
|
|
12951
|
+
identity;
|
|
12952
|
+
/** Where the contact publishes its pointer. */
|
|
12953
|
+
peerAddress;
|
|
12954
|
+
seed;
|
|
12955
|
+
sealKey;
|
|
12956
|
+
constructor(params, participationSeedB64, peerKey) {
|
|
12957
|
+
this.from = identityFromSeedB64(params.seedB64).pubKeyZ32;
|
|
12958
|
+
this.to = params.peerPubKeyZ32;
|
|
12959
|
+
const mine = identityFromSeedB64(participationSeedB64);
|
|
12960
|
+
this.seed = mine.seed;
|
|
12961
|
+
this.me = mine.pubKeyZ32;
|
|
12962
|
+
this.peer = peerKey;
|
|
12963
|
+
const secret = fromBase64Url(params.encKeyB64);
|
|
12964
|
+
const context = JSON.stringify(["ghostly-hold/1", [this.from, this.to].sort()]);
|
|
12965
|
+
const derive = (label) => hkdf(sha256, secret, utf8Encode(context), utf8Encode(label), 32);
|
|
12966
|
+
this.identity = identityFromSeed(derive(`pointer:${this.from}`));
|
|
12967
|
+
this.peerAddress = identityFromSeed(derive(`pointer:${this.to}`)).pubKeyZ32;
|
|
12968
|
+
const shared = x25519.getSharedSecret(ed25519.utils.toMontgomerySecret(this.seed), ed25519.utils.toMontgomery(publicKeyFromZ32(peerKey)));
|
|
12969
|
+
this.sealKey = hkdf(sha256, shared, derive("envelope"), utf8Encode("ghostly-hold-envelope/1"), 32);
|
|
12970
|
+
}
|
|
12971
|
+
/** Seals one item for the contact: signed with my participation key, encrypted to our shared key. */
|
|
12972
|
+
seal(item, body) {
|
|
12973
|
+
const fields = [
|
|
12974
|
+
VERSION,
|
|
12975
|
+
this.from,
|
|
12976
|
+
this.to,
|
|
12977
|
+
this.me,
|
|
12978
|
+
this.peer,
|
|
12979
|
+
item.mailbox,
|
|
12980
|
+
item.seq,
|
|
12981
|
+
item.id,
|
|
12982
|
+
item.ts,
|
|
12983
|
+
item.kind,
|
|
12984
|
+
item.meta ?? null,
|
|
12985
|
+
bodyDigest(body),
|
|
12986
|
+
item.expires
|
|
12987
|
+
];
|
|
12988
|
+
const signature = toBase64Url(sign(utf8Encode(JSON.stringify(["ghostly-hold-bundle", fields])), this.seed));
|
|
12989
|
+
const header = utf8Encode(JSON.stringify([fields, signature]));
|
|
12990
|
+
const length = /* @__PURE__ */ new Uint8Array(4);
|
|
12991
|
+
new DataView(length.buffer).setUint32(0, header.length);
|
|
12992
|
+
const nonce = randomBytes(NONCE$2);
|
|
12993
|
+
const box = xsalsa20poly1305(this.sealKey, nonce).encrypt(concatBytes$1(length, header, body));
|
|
12994
|
+
return concatBytes$1(MAGIC$2, new Uint8Array([VERSION]), nonce, box);
|
|
12995
|
+
}
|
|
12996
|
+
/**
|
|
12997
|
+
* Opens a bundle the contact sealed for me. Everything is checked before anything is returned: the
|
|
12998
|
+
* authenticated encryption, the header's shape, that it names this link, this contact and me, the
|
|
12999
|
+
* signature, the body digest and the limits of its kind. `loopback`: see `isHoldUrl`.
|
|
13000
|
+
*/
|
|
13001
|
+
open(bytes, expect = {}) {
|
|
13002
|
+
const now = expect.now ?? Date.now();
|
|
13003
|
+
if (bytes.length > (expect.maxBytes ?? HOLD_LIMITS.maxBundleBytes)) throw new HoldRefusedError("size", "The held item is larger than allowed");
|
|
13004
|
+
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");
|
|
13005
|
+
if (bytes[MAGIC$2.length] !== VERSION) throw new HoldRefusedError("format", "Held item from a newer Ghostly");
|
|
13006
|
+
let plain;
|
|
13007
|
+
try {
|
|
13008
|
+
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));
|
|
13009
|
+
} catch {
|
|
13010
|
+
throw new HoldRefusedError("tampered", "The held item was changed or is not for this chat");
|
|
13011
|
+
}
|
|
13012
|
+
if (plain.length < 4) throw new HoldRefusedError("format", "Not a held item");
|
|
13013
|
+
const headerLength = new DataView(plain.buffer, plain.byteOffset, 4).getUint32(0);
|
|
13014
|
+
if (headerLength > 131072 || 4 + headerLength > plain.length) throw new HoldRefusedError("format", "Not a held item");
|
|
13015
|
+
let parsed;
|
|
13016
|
+
try {
|
|
13017
|
+
parsed = JSON.parse(utf8Decode(plain.subarray(4, 4 + headerLength)));
|
|
13018
|
+
} catch {
|
|
13019
|
+
throw new HoldRefusedError("format", "Not a held item");
|
|
13020
|
+
}
|
|
13021
|
+
if (!Array.isArray(parsed) || parsed.length !== 2 || !Array.isArray(parsed[0]) || parsed[0].length !== 13 || typeof parsed[1] !== "string" || !SIG$3.test(parsed[1])) throw new HoldRefusedError("format", "Not a held item");
|
|
13022
|
+
const [fields, signature] = parsed;
|
|
13023
|
+
const [v, from, to, author, recipient, mailbox, seq, id, ts, kind, meta, digest, expires] = fields;
|
|
13024
|
+
if (v !== VERSION || typeof from !== "string" || typeof to !== "string" || typeof author !== "string" || typeof recipient !== "string" || typeof mailbox !== "string" || !MAILBOX.test(mailbox) || !Number.isSafeInteger(seq) || seq < 0 || typeof id !== "string" || !WIRE_ID.test(id) || !Number.isSafeInteger(ts) || ts <= 0 || typeof kind !== "string" || !KINDS$1.includes(kind) || typeof digest !== "string" || !Number.isSafeInteger(expires)) throw new HoldRefusedError("format", "Not a held item");
|
|
13025
|
+
if (from !== this.to || to !== this.from || recipient !== this.me) throw new HoldRefusedError("not-for-me", "The held item is for another chat");
|
|
13026
|
+
if (author !== this.peer) throw new HoldRefusedError("author", "The held item was not signed by this contact");
|
|
13027
|
+
if (expect.mailbox && mailbox !== expect.mailbox) throw new HoldRefusedError("not-for-me", "The held item is from another mailbox");
|
|
13028
|
+
let valid;
|
|
13029
|
+
try {
|
|
13030
|
+
valid = verify(fromBase64Url(signature), utf8Encode(JSON.stringify(["ghostly-hold-bundle", fields])), publicKeyFromZ32(author));
|
|
13031
|
+
} catch {
|
|
13032
|
+
valid = false;
|
|
13033
|
+
}
|
|
13034
|
+
if (!valid) throw new HoldRefusedError("signature", "The held item's signature does not check out");
|
|
13035
|
+
const body = plain.subarray(4 + headerLength);
|
|
13036
|
+
if (bodyDigest(body) !== digest) throw new HoldRefusedError("digest", "The held item's content does not match its header");
|
|
13037
|
+
if (expires <= ts || expires > now + HOLD_LIMITS.ttlMs + HOLD_LIMITS.clockSkewMs) throw new HoldRefusedError("limits", "The held item's lifetime is out of bounds");
|
|
13038
|
+
const header = {
|
|
13039
|
+
v: 1,
|
|
13040
|
+
from,
|
|
13041
|
+
to,
|
|
13042
|
+
author,
|
|
13043
|
+
recipient,
|
|
13044
|
+
mailbox,
|
|
13045
|
+
seq,
|
|
13046
|
+
id,
|
|
13047
|
+
ts,
|
|
13048
|
+
kind,
|
|
13049
|
+
meta,
|
|
13050
|
+
digest,
|
|
13051
|
+
expires
|
|
13052
|
+
};
|
|
13053
|
+
checkKind(header, body, expect.loopback);
|
|
13054
|
+
return {
|
|
13055
|
+
header,
|
|
13056
|
+
body
|
|
13057
|
+
};
|
|
13058
|
+
}
|
|
13059
|
+
/** My pointer, as records to publish under `identity`. Throws when it cannot fit a packet. */
|
|
13060
|
+
pointerRecords(pointer, now = Date.now()) {
|
|
13061
|
+
const body = [
|
|
13062
|
+
VERSION,
|
|
13063
|
+
pointer.rev,
|
|
13064
|
+
pointer.issued,
|
|
13065
|
+
pointer.expires,
|
|
13066
|
+
pointer.manifestUrl,
|
|
13067
|
+
pointer.top,
|
|
13068
|
+
pointer.ack,
|
|
13069
|
+
pointer.count,
|
|
13070
|
+
pointer.bytes,
|
|
13071
|
+
pointer.refused.slice(-32)
|
|
13072
|
+
];
|
|
13073
|
+
const signature = toBase64Url(sign(utf8Encode(JSON.stringify([
|
|
13074
|
+
"ghostly-hold-pointer",
|
|
13075
|
+
this.from,
|
|
13076
|
+
this.to,
|
|
13077
|
+
body
|
|
13078
|
+
])), this.seed));
|
|
13079
|
+
const ttl = Math.min(86400, Math.max(60, Math.ceil((pointer.expires - now) / 1e3)));
|
|
13080
|
+
const records = [{
|
|
13081
|
+
label: "_hold",
|
|
13082
|
+
value: encrypt(JSON.stringify([body, signature]), this.sealKey),
|
|
13083
|
+
ttl
|
|
13084
|
+
}];
|
|
13085
|
+
if (measureRecords(this.identity.pubKeyZ32, records) > 1e3) throw new Error("The storage address is too long for a DHT packet. Use a shorter endpoint or prefix.");
|
|
13086
|
+
return records;
|
|
13087
|
+
}
|
|
13088
|
+
/**
|
|
13089
|
+
* The contact's pointer out of its packet, or null when the packet is not one of its pointers. Its `issued` is the
|
|
13090
|
+
* contact's clock and refuses nothing: an older pointer cannot take a newer one's place because of `rev`. The
|
|
13091
|
+
* manifest's address is held to what an item's is (`isHoldUrl`): it is fetched as soon as the pointer is read.
|
|
13092
|
+
*/
|
|
13093
|
+
readPointer(packet, loopback = false) {
|
|
13094
|
+
if (packet.pubKeyZ32 !== this.peerAddress || measureRecords(packet.pubKeyZ32, packet.records) > 1e3) return null;
|
|
13095
|
+
const records = packet.records.filter((r) => r.label === "_hold");
|
|
12751
13096
|
if (records.length !== 1) return null;
|
|
12752
13097
|
const plain = tryDecrypt(records[0].value, this.sealKey);
|
|
12753
13098
|
if (!plain) return null;
|
|
@@ -12768,7 +13113,7 @@ var HoldKeys = class {
|
|
|
12768
13113
|
ack,
|
|
12769
13114
|
count,
|
|
12770
13115
|
bytes
|
|
12771
|
-
].every((n) => Number.isSafeInteger(n) && n >= 0) || manifestUrl !== null && (
|
|
13116
|
+
].every((n) => Number.isSafeInteger(n) && n >= 0) || manifestUrl !== null && !isHoldUrl(manifestUrl, loopback) || !Array.isArray(refused) || refused.length > 32 || !refused.every((n) => Number.isSafeInteger(n) && n > 0)) return null;
|
|
12772
13117
|
try {
|
|
12773
13118
|
if (!verify(fromBase64Url(signature), utf8Encode(JSON.stringify([
|
|
12774
13119
|
"ghostly-hold-pointer",
|
|
@@ -12792,22 +13137,31 @@ var HoldKeys = class {
|
|
|
12792
13137
|
};
|
|
12793
13138
|
}
|
|
12794
13139
|
};
|
|
12795
|
-
/**
|
|
12796
|
-
|
|
13140
|
+
/**
|
|
13141
|
+
* A URL a bundle or manifest may be fetched from. The contact chose it and this device fetches it on its own, so it
|
|
13142
|
+
* is HTTPS, without a user or password, at a name or a public IPv4 address: never this machine or its network.
|
|
13143
|
+
* `loopback` adds plain HTTP to this machine, for a test against a local server; no shipped app passes it.
|
|
13144
|
+
*/
|
|
13145
|
+
function isHoldUrl(value, loopback = false) {
|
|
12797
13146
|
if (typeof value !== "string" || value.length > 2048) return false;
|
|
13147
|
+
let url;
|
|
12798
13148
|
try {
|
|
12799
|
-
|
|
12800
|
-
return url.protocol === "https:" || url.protocol === "http:" && [
|
|
12801
|
-
"localhost",
|
|
12802
|
-
"127.0.0.1",
|
|
12803
|
-
"[::1]"
|
|
12804
|
-
].includes(url.hostname);
|
|
13149
|
+
url = new URL(value);
|
|
12805
13150
|
} catch {
|
|
12806
13151
|
return false;
|
|
12807
13152
|
}
|
|
13153
|
+
if (url.username || url.password) return false;
|
|
13154
|
+
const host = url.hostname.toLowerCase().replace(/\.$/, "");
|
|
13155
|
+
if (loopback && url.protocol === "http:" && [
|
|
13156
|
+
"localhost",
|
|
13157
|
+
"127.0.0.1",
|
|
13158
|
+
"[::1]"
|
|
13159
|
+
].includes(host)) return true;
|
|
13160
|
+
if (url.protocol !== "https:" || !host || host.startsWith("[") || host === "localhost" || host.endsWith(".localhost")) return false;
|
|
13161
|
+
return !/^\d+\.\d+\.\d+\.\d+$/.test(host) || isPublicAddress(Uint8Array.from(host.split("."), Number));
|
|
12808
13162
|
}
|
|
12809
13163
|
/** The limits of each kind, on the way in. */
|
|
12810
|
-
function checkKind(header, body) {
|
|
13164
|
+
function checkKind(header, body, loopback = false) {
|
|
12811
13165
|
const bad = (message) => new HoldRefusedError("limits", message);
|
|
12812
13166
|
switch (header.kind) {
|
|
12813
13167
|
case "text":
|
|
@@ -12828,12 +13182,12 @@ function checkKind(header, body) {
|
|
|
12828
13182
|
}
|
|
12829
13183
|
case "manifest":
|
|
12830
13184
|
if (body.length !== 0) throw bad("A manifest carries no body");
|
|
12831
|
-
readManifest(header.meta);
|
|
13185
|
+
readManifest(header.meta, loopback);
|
|
12832
13186
|
return;
|
|
12833
13187
|
}
|
|
12834
13188
|
}
|
|
12835
13189
|
/** The entries of a manifest, checked: strictly increasing sequences, bounded sizes, fetchable URLs. */
|
|
12836
|
-
function readManifest(meta) {
|
|
13190
|
+
function readManifest(meta, loopback = false) {
|
|
12837
13191
|
const bad = (message) => new HoldRefusedError("limits", message);
|
|
12838
13192
|
const entries = meta?.entries;
|
|
12839
13193
|
if (!Array.isArray(entries) || entries.length > HOLD_LIMITS.maxMailboxBundles) throw bad("The manifest lists too many items");
|
|
@@ -12842,7 +13196,7 @@ function readManifest(meta) {
|
|
|
12842
13196
|
for (const entry of entries) {
|
|
12843
13197
|
if (!Array.isArray(entry) || entry.length !== 6) throw bad("The manifest is malformed");
|
|
12844
13198
|
const [seq, id, kind, bytes, url, expires] = entry;
|
|
12845
|
-
if (!Number.isSafeInteger(seq) || seq <= last || typeof id !== "string" || !WIRE_ID.test(id) || typeof kind !== "string" || !KINDS$1.includes(kind) || kind === "manifest" || !Number.isSafeInteger(bytes) || bytes < 0 || bytes > HOLD_LIMITS.maxBundleBytes || !isHoldUrl(url) || !Number.isSafeInteger(expires)) throw bad("The manifest is malformed");
|
|
13199
|
+
if (!Number.isSafeInteger(seq) || seq <= last || typeof id !== "string" || !WIRE_ID.test(id) || typeof kind !== "string" || !KINDS$1.includes(kind) || kind === "manifest" || !Number.isSafeInteger(bytes) || bytes < 0 || bytes > HOLD_LIMITS.maxBundleBytes || !isHoldUrl(url, loopback) || !Number.isSafeInteger(expires)) throw bad("The manifest is malformed");
|
|
12846
13200
|
last = seq;
|
|
12847
13201
|
total += bytes;
|
|
12848
13202
|
if (total > HOLD_LIMITS.maxMailboxBytes) throw bad("The manifest lists more than a mailbox may hold");
|
|
@@ -14238,6 +14592,8 @@ var TypingReceiver = class {
|
|
|
14238
14592
|
* receiver says where it stands (accept from an offset, wait, done, refuse) and the sender goes on from
|
|
14239
14593
|
* there. Nothing but the offset a receiver confirms moves a sender forward. A receiver may say `pf-accept`
|
|
14240
14594
|
* again at any time (nothing arrived for a while, or its person asked again): the sender goes on from that offset.
|
|
14595
|
+
* What a sender sends again that way is bounded (`FILE_LIMITS.resendBytes`, and the host's `resend`): past it the
|
|
14596
|
+
* sender ends the transfer.
|
|
14241
14597
|
*/
|
|
14242
14598
|
var FILE_LIMITS = {
|
|
14243
14599
|
/** Bytes in one `pf-data` frame: a frame stays far below the 60 KiB a session carries. */
|
|
@@ -14271,7 +14627,12 @@ var FILE_LIMITS = {
|
|
|
14271
14627
|
/** A sender reads its file this much at a time. */
|
|
14272
14628
|
readBytes: 1048576,
|
|
14273
14629
|
/** The session refused a data frame (its send budget was full): the sender looks again after this long. */
|
|
14274
|
-
sendRetryMs: 250
|
|
14630
|
+
sendRetryMs: 250,
|
|
14631
|
+
/**
|
|
14632
|
+
* What a sender sends of one file beyond its size (parts sent again after a drop, a restart of the receiver, or a
|
|
14633
|
+
* `pf-accept` from an earlier offset): twice the file's size and this much. Past it the sender ends the transfer (WISP 501 § Resume).
|
|
14634
|
+
*/
|
|
14635
|
+
resendBytes: 268435456
|
|
14275
14636
|
};
|
|
14276
14637
|
var ID = /^[A-Za-z0-9_-]{8,64}$/;
|
|
14277
14638
|
var DIGEST$1 = /^[A-Za-z0-9_-]{43}$/;
|
|
@@ -14357,6 +14718,7 @@ var ChatFiles = class {
|
|
|
14357
14718
|
const entry = { record: { ...record } };
|
|
14358
14719
|
if (record.direction === "out") entry.out = {
|
|
14359
14720
|
next: record.confirmed,
|
|
14721
|
+
sent: 0,
|
|
14360
14722
|
generation: 0
|
|
14361
14723
|
};
|
|
14362
14724
|
else entry.in = {
|
|
@@ -14388,6 +14750,10 @@ var ChatFiles = class {
|
|
|
14388
14750
|
const entry = this.entries.get(key(direction, id));
|
|
14389
14751
|
return entry ? this.moved(entry) : 0;
|
|
14390
14752
|
}
|
|
14753
|
+
/** Bytes of data sent for a file so far, parts sent again included: what sending it cost, whatever the receiver confirmed. */
|
|
14754
|
+
sent(id) {
|
|
14755
|
+
return this.entries.get(key("out", id))?.out?.sent ?? 0;
|
|
14756
|
+
}
|
|
14391
14757
|
moved(entry) {
|
|
14392
14758
|
return entry.in && !transferEnded(entry.record) ? Math.max(entry.in.stored, entry.record.confirmed) : entry.record.confirmed;
|
|
14393
14759
|
}
|
|
@@ -14468,6 +14834,7 @@ var ChatFiles = class {
|
|
|
14468
14834
|
},
|
|
14469
14835
|
out: {
|
|
14470
14836
|
next: 0,
|
|
14837
|
+
sent: 0,
|
|
14471
14838
|
generation: 0
|
|
14472
14839
|
}
|
|
14473
14840
|
};
|
|
@@ -14541,6 +14908,15 @@ var ChatFiles = class {
|
|
|
14541
14908
|
const { record } = entry, out = entry.out;
|
|
14542
14909
|
return this.attached && record.state === "active" && out.next < record.file.size && out.next - record.confirmed < FILE_LIMITS.windowBytes;
|
|
14543
14910
|
}
|
|
14911
|
+
/**
|
|
14912
|
+
* Whether the next `length` bytes go. Within the file's size they always do; past it the receiver is having parts
|
|
14913
|
+
* sent again, which nothing it says bounds, so this side does.
|
|
14914
|
+
*/
|
|
14915
|
+
mayResend(entry, length) {
|
|
14916
|
+
const { record } = entry, size = record.file.size, sent = entry.out.sent + length;
|
|
14917
|
+
if (sent <= size) return true;
|
|
14918
|
+
return sent - size <= 2 * size + FILE_LIMITS.resendBytes && this.host.resend?.({ ...record }, sent) !== false;
|
|
14919
|
+
}
|
|
14544
14920
|
async pump(entry) {
|
|
14545
14921
|
const out = entry.out;
|
|
14546
14922
|
if (out.pumping === out.generation) return;
|
|
@@ -14554,6 +14930,14 @@ var ChatFiles = class {
|
|
|
14554
14930
|
const room = this.host.writable?.();
|
|
14555
14931
|
if (room) await room;
|
|
14556
14932
|
if (generation !== out.generation || !this.shouldPump(entry) || out.next !== offset) break;
|
|
14933
|
+
if (!this.mayResend(entry, chunk.length)) {
|
|
14934
|
+
this.send({
|
|
14935
|
+
t: "pf-abort",
|
|
14936
|
+
id: entry.record.id
|
|
14937
|
+
});
|
|
14938
|
+
this.end(entry, "failed", "Your contact asked for the same part of this file too many times");
|
|
14939
|
+
break;
|
|
14940
|
+
}
|
|
14557
14941
|
if (!this.send({
|
|
14558
14942
|
t: "pf-data",
|
|
14559
14943
|
id: entry.record.id,
|
|
@@ -14564,6 +14948,7 @@ var ChatFiles = class {
|
|
|
14564
14948
|
break;
|
|
14565
14949
|
}
|
|
14566
14950
|
out.next = offset + chunk.length;
|
|
14951
|
+
out.sent += chunk.length;
|
|
14567
14952
|
}
|
|
14568
14953
|
} catch (error) {
|
|
14569
14954
|
if (generation === out.generation) {
|
|
@@ -15272,6 +15657,7 @@ var ChatFiles = class {
|
|
|
15272
15657
|
clearTimeout(entry.out.wake);
|
|
15273
15658
|
entry.out = {
|
|
15274
15659
|
next: 0,
|
|
15660
|
+
sent: 0,
|
|
15275
15661
|
generation: entry.out.generation + 1
|
|
15276
15662
|
};
|
|
15277
15663
|
this.changed(entry);
|
|
@@ -16830,6 +17216,8 @@ var GhostLink = class {
|
|
|
16830
17216
|
}
|
|
16831
17217
|
/** The attempt (`connectionEpoch`) in which the data link began answering the contact's offer. */
|
|
16832
17218
|
answeringEpoch = -1;
|
|
17219
|
+
/** Set while a dial waits for its WebRTC offer to be made: ends that wait, the contact's offer being answered instead. */
|
|
17220
|
+
offerReplaced;
|
|
16833
17221
|
lastAutoConnectAt = 0;
|
|
16834
17222
|
autoConnectFailures = 0;
|
|
16835
17223
|
/** The look due when a failed dial's backoff ends (`lookAtRetry`). */
|
|
@@ -17175,6 +17563,7 @@ var GhostLink = class {
|
|
|
17175
17563
|
}
|
|
17176
17564
|
if (state === "answering") {
|
|
17177
17565
|
this.answeringEpoch = this.connectionEpoch;
|
|
17566
|
+
this.offerReplaced?.();
|
|
17178
17567
|
if (this.resumingOverRtc) {
|
|
17179
17568
|
const epoch = this.connectionEpoch;
|
|
17180
17569
|
setTimeout(() => this.maybeKnockAnswering(epoch), 0);
|
|
@@ -18352,7 +18741,12 @@ var GhostLink = class {
|
|
|
18352
18741
|
} : void 0;
|
|
18353
18742
|
const gate = this.dialGate;
|
|
18354
18743
|
this.dialGate = void 0;
|
|
18355
|
-
|
|
18744
|
+
const replaced = new Promise((resolve) => {
|
|
18745
|
+
this.offerReplaced = resolve;
|
|
18746
|
+
});
|
|
18747
|
+
const made = await Promise.race([this.dataLink.connect(gate), replaced]);
|
|
18748
|
+
this.offerReplaced = void 0;
|
|
18749
|
+
if (made === "held") {
|
|
18356
18750
|
traceLink(this.myPubKeyZ32, "dial-held", {});
|
|
18357
18751
|
this.afterRtc = void 0;
|
|
18358
18752
|
this.offered = void 0;
|
|
@@ -20919,535 +21313,308 @@ function decodeBolt11(text) {
|
|
|
20919
21313
|
at += 3 + length;
|
|
20920
21314
|
if (tag === "d") description = new TextDecoder().decode(wordsToBytes(field)).trim() || void 0;
|
|
20921
21315
|
else if (tag === "x") expiry = wordsToInt(field);
|
|
20922
|
-
else if (tag === "p" && length === 52) paymentHash = [...wordsToBytes(field)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
20923
|
-
else if (tag === "h" && length === 52) descriptionHash = [...wordsToBytes(field)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
20924
|
-
}
|
|
20925
|
-
return {
|
|
20926
|
-
invoice,
|
|
20927
|
-
network: NETWORKS[prefix[1]],
|
|
20928
|
-
amountMsat,
|
|
20929
|
-
amountSat: amountMsat === null ? null : Number((amountMsat + 999n) / 1000n),
|
|
20930
|
-
createdAt,
|
|
20931
|
-
expiresAt: createdAt + expiry,
|
|
20932
|
-
description,
|
|
20933
|
-
descriptionHash,
|
|
20934
|
-
paymentHash
|
|
20935
|
-
};
|
|
20936
|
-
}
|
|
20937
|
-
//#endregion
|
|
20938
|
-
//#region ../core/src/lnurl.ts
|
|
20939
|
-
var LNURL_LIMITS = {
|
|
20940
|
-
timeoutMs: 15e3,
|
|
20941
|
-
maxBytes: 65536,
|
|
20942
|
-
maxUrlLength: 2048,
|
|
20943
|
-
maxMetadataLength: 8192,
|
|
20944
|
-
maxComment: 2e3
|
|
20945
|
-
};
|
|
20946
|
-
var ADDRESS = /^([a-z0-9._+-]{1,64})@([a-z0-9-]+(?:\.[a-z0-9-]+)*(?::\d{1,5})?)$/i;
|
|
20947
|
-
var LNURL_BECH32 = /^lnurl1[02-9ac-hj-np-z]{6,}$/i;
|
|
20948
|
-
var isLoopback = (hostname) => [
|
|
20949
|
-
"localhost",
|
|
20950
|
-
"127.0.0.1",
|
|
20951
|
-
"[::1]"
|
|
20952
|
-
].includes(hostname);
|
|
20953
|
-
/** HTTPS, or plain HTTP only for a hidden service or a test server on this machine; never credentials. */
|
|
20954
|
-
function acceptUrl(text) {
|
|
20955
|
-
if (text.length > LNURL_LIMITS.maxUrlLength) throw new Error("That LNURL is too long");
|
|
20956
|
-
let url;
|
|
20957
|
-
try {
|
|
20958
|
-
url = new URL(text);
|
|
20959
|
-
} catch {
|
|
20960
|
-
throw new Error("That LNURL does not point to a valid address");
|
|
20961
|
-
}
|
|
20962
|
-
const plainAllowed = url.hostname.endsWith(".onion") || isLoopback(url.hostname);
|
|
20963
|
-
if (url.protocol !== "https:" && !(url.protocol === "http:" && plainAllowed)) throw new Error("Lightning addresses and LNURLs must use HTTPS");
|
|
20964
|
-
if (url.username || url.password) throw new Error("That LNURL carries credentials, which is not allowed");
|
|
20965
|
-
return url;
|
|
20966
|
-
}
|
|
20967
|
-
/** Reads what was pasted as a Lightning address or an LNURL; null when it is neither. Throws when it is one, but a bad one. */
|
|
20968
|
-
function parseLightningDestination(input) {
|
|
20969
|
-
const text = input.trim().replace(/^(lightning:|⚡\s*)/i, "").trim();
|
|
20970
|
-
if (!text) return null;
|
|
20971
|
-
if (LNURL_BECH32.test(text)) {
|
|
20972
|
-
let decoded;
|
|
20973
|
-
try {
|
|
20974
|
-
decoded = bech32.decode(text.toLowerCase(), LNURL_LIMITS.maxUrlLength);
|
|
20975
|
-
} catch {
|
|
20976
|
-
throw new Error("That LNURL is not valid");
|
|
20977
|
-
}
|
|
20978
|
-
const url = acceptUrl(new TextDecoder().decode(bech32.fromWords(decoded.words)));
|
|
20979
|
-
return {
|
|
20980
|
-
kind: "lnurl",
|
|
20981
|
-
text: text.toLowerCase(),
|
|
20982
|
-
url: url.href,
|
|
20983
|
-
domain: url.host
|
|
20984
|
-
};
|
|
20985
|
-
}
|
|
20986
|
-
if (/^lnurlp:\/\//i.test(text)) {
|
|
20987
|
-
const bare = text.slice(9);
|
|
20988
|
-
const host = bare.split(/[/?#]/)[0].toLowerCase();
|
|
20989
|
-
const url = acceptUrl(`${host.endsWith(".onion") || isLoopback(host.replace(/:\d+$/, "")) ? "http" : "https"}://${bare}`);
|
|
20990
|
-
return {
|
|
20991
|
-
kind: "lnurl",
|
|
20992
|
-
text: `lnurlp://${bare}`,
|
|
20993
|
-
url: url.href,
|
|
20994
|
-
domain: url.host
|
|
20995
|
-
};
|
|
20996
|
-
}
|
|
20997
|
-
const address = ADDRESS.exec(text);
|
|
20998
|
-
if (!address) return null;
|
|
20999
|
-
const name = address[1].toLowerCase(), domain = address[2].toLowerCase();
|
|
21000
|
-
const hostname = domain.replace(/:\d+$/, "");
|
|
21001
|
-
if (!hostname.includes(".") && !isLoopback(hostname)) return null;
|
|
21002
|
-
const url = acceptUrl(`${hostname.endsWith(".onion") || isLoopback(hostname) ? "http" : "https"}://${domain}/.well-known/lnurlp/${name}`);
|
|
21003
|
-
return {
|
|
21004
|
-
kind: "address",
|
|
21005
|
-
text: `${name}@${domain}`,
|
|
21006
|
-
url: url.href,
|
|
21007
|
-
domain: url.host,
|
|
21008
|
-
name
|
|
21009
|
-
};
|
|
21010
|
-
}
|
|
21011
|
-
var hex$1 = (bytes) => Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
21012
|
-
var reason = (value) => typeof value === "string" ? value.replace(/\s+/g, " ").slice(0, 200) : "";
|
|
21013
|
-
var isRecord = (value) => !!value && typeof value === "object" && !Array.isArray(value);
|
|
21014
|
-
async function readBounded(response, maxBytes) {
|
|
21015
|
-
const reader = response.body?.getReader?.();
|
|
21016
|
-
if (!reader) {
|
|
21017
|
-
const text = await response.text();
|
|
21018
|
-
if (text.length > maxBytes) throw new Error("The server's answer is too large");
|
|
21019
|
-
return text;
|
|
21020
|
-
}
|
|
21021
|
-
const chunks = [];
|
|
21022
|
-
let size = 0;
|
|
21023
|
-
for (;;) {
|
|
21024
|
-
const { done, value } = await reader.read();
|
|
21025
|
-
if (done) break;
|
|
21026
|
-
size += value.byteLength;
|
|
21027
|
-
if (size > maxBytes) {
|
|
21028
|
-
await reader.cancel().catch(() => {});
|
|
21029
|
-
throw new Error("The server's answer is too large");
|
|
21030
|
-
}
|
|
21031
|
-
chunks.push(value);
|
|
21032
|
-
}
|
|
21033
|
-
const all = new Uint8Array(size);
|
|
21034
|
-
let at = 0;
|
|
21035
|
-
for (const chunk of chunks) {
|
|
21036
|
-
all.set(chunk, at);
|
|
21037
|
-
at += chunk.byteLength;
|
|
21038
|
-
}
|
|
21039
|
-
return new TextDecoder().decode(all);
|
|
21040
|
-
}
|
|
21041
|
-
/** One GET to an LNURL service: bounded in time and size, no credentials, no referrer, and the answer must be JSON. */
|
|
21042
|
-
async function fetchLnurlJson(url, options = {}) {
|
|
21043
|
-
const target = acceptUrl(url);
|
|
21044
|
-
const doFetch = options.fetch ?? globalThis.fetch;
|
|
21045
|
-
if (!doFetch) throw new Error("No way to reach the network here");
|
|
21046
|
-
const controller = new AbortController();
|
|
21047
|
-
const timer = setTimeout(() => controller.abort(/* @__PURE__ */ new Error("The server took too long to answer")), options.timeoutMs ?? LNURL_LIMITS.timeoutMs);
|
|
21048
|
-
const onAbort = () => controller.abort(options.signal?.reason);
|
|
21049
|
-
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
21050
|
-
try {
|
|
21051
|
-
let response;
|
|
21052
|
-
try {
|
|
21053
|
-
response = await doFetch(target.href, {
|
|
21054
|
-
signal: controller.signal,
|
|
21055
|
-
headers: { accept: "application/json" },
|
|
21056
|
-
credentials: "omit",
|
|
21057
|
-
referrerPolicy: "no-referrer",
|
|
21058
|
-
redirect: "follow"
|
|
21059
|
-
});
|
|
21060
|
-
} catch {
|
|
21061
|
-
if (controller.signal.aborted) throw controller.signal.reason instanceof Error ? controller.signal.reason : /* @__PURE__ */ new Error("The request was cancelled");
|
|
21062
|
-
throw engineError("lnurlUnreachable", { host: target.host });
|
|
21063
|
-
}
|
|
21064
|
-
if (response.url) acceptUrl(response.url);
|
|
21065
|
-
const text = await readBounded(response, options.maxBytes ?? LNURL_LIMITS.maxBytes);
|
|
21066
|
-
let body;
|
|
21067
|
-
try {
|
|
21068
|
-
body = JSON.parse(text);
|
|
21069
|
-
} catch {
|
|
21070
|
-
throw new Error(`${target.host} did not answer with JSON${response.ok ? "" : ` (HTTP ${response.status})`}`);
|
|
21071
|
-
}
|
|
21072
|
-
if (isRecord(body) && body.status === "ERROR") throw new Error(`${target.host} refused: ${reason(body.reason) || "no reason given"}`);
|
|
21073
|
-
if (!response.ok) throw new Error(`${target.host} answered HTTP ${response.status}`);
|
|
21074
|
-
return body;
|
|
21075
|
-
} finally {
|
|
21076
|
-
clearTimeout(timer);
|
|
21077
|
-
options.signal?.removeEventListener("abort", onAbort);
|
|
21078
|
-
}
|
|
21079
|
-
}
|
|
21080
|
-
/** Checks a service's `payRequest` answer (LUD-06), and reads what a person needs to see of it. */
|
|
21081
|
-
function parsePayParams(body, destination) {
|
|
21082
|
-
if (!isRecord(body)) throw new Error(`${destination.domain} did not answer with a pay request`);
|
|
21083
|
-
if (body.tag !== "payRequest") throw new Error(`${destination.domain} answered with something other than a pay request${typeof body.tag === "string" ? ` (${reason(body.tag)})` : ""}`);
|
|
21084
|
-
if (typeof body.callback !== "string") throw new Error("The pay request has no callback");
|
|
21085
|
-
const callback = acceptUrl(body.callback);
|
|
21086
|
-
const { minSendable, maxSendable, metadata, commentAllowed } = body;
|
|
21087
|
-
if (!Number.isSafeInteger(minSendable) || !Number.isSafeInteger(maxSendable) || minSendable <= 0 || maxSendable < minSendable) throw new Error("The pay request has invalid amount limits");
|
|
21088
|
-
const minSat = Math.ceil(minSendable / 1e3), maxSat = Math.floor(maxSendable / 1e3);
|
|
21089
|
-
if (minSat > maxSat) throw new Error("The pay request only takes fractions of a sat, which cannot be paid here");
|
|
21090
|
-
if (typeof metadata !== "string" || metadata.length > LNURL_LIMITS.maxMetadataLength) throw new Error("The pay request has no usable metadata");
|
|
21091
|
-
let entries;
|
|
21092
|
-
try {
|
|
21093
|
-
entries = JSON.parse(metadata);
|
|
21094
|
-
} catch {
|
|
21095
|
-
throw new Error("The pay request's metadata is not JSON");
|
|
21096
|
-
}
|
|
21097
|
-
if (!Array.isArray(entries) || !entries.every((e) => Array.isArray(e) && typeof e[0] === "string")) throw new Error("The pay request's metadata is malformed");
|
|
21098
|
-
const entry = (type) => entries.find((e) => e[0] === type)?.[1];
|
|
21099
|
-
const description = entry("text/plain");
|
|
21100
|
-
if (typeof description !== "string") throw new Error("The pay request says nothing about what it is for (no text/plain metadata)");
|
|
21101
|
-
const identifier = [entry("text/identifier"), entry("text/email")].find((v) => typeof v === "string")?.toLowerCase();
|
|
21102
|
-
if (destination.kind === "address" && identifier && identifier !== destination.text) throw new Error(`${destination.domain} answered for ${identifier}, not ${destination.text}`);
|
|
21103
|
-
const comment = Number.isSafeInteger(commentAllowed) && commentAllowed > 0 ? Math.min(commentAllowed, LNURL_LIMITS.maxComment) : 0;
|
|
21104
|
-
return {
|
|
21105
|
-
destination,
|
|
21106
|
-
callback: callback.href,
|
|
21107
|
-
callbackDomain: callback.host,
|
|
21108
|
-
minSendable,
|
|
21109
|
-
maxSendable,
|
|
21110
|
-
minSat,
|
|
21111
|
-
maxSat,
|
|
21112
|
-
metadata,
|
|
21113
|
-
descriptionHash: hex$1(sha256(new TextEncoder().encode(metadata))),
|
|
21114
|
-
description: description.slice(0, 280),
|
|
21115
|
-
identifier,
|
|
21116
|
-
commentAllowed: comment
|
|
21117
|
-
};
|
|
21118
|
-
}
|
|
21119
|
-
/** Where to ask for the invoice of `amountSat` (and a comment, LUD-12). Throws when the amount or comment is not allowed. */
|
|
21120
|
-
function invoiceCallbackUrl(params, amountSat, comment) {
|
|
21121
|
-
if (!Number.isSafeInteger(amountSat) || amountSat <= 0) throw new Error("Enter a whole number of sats");
|
|
21122
|
-
if (amountSat < params.minSat || amountSat > params.maxSat) throw params.minSat === params.maxSat ? engineError("lnurlExactly", {
|
|
21123
|
-
domain: params.destination.domain,
|
|
21124
|
-
amount: params.minSat.toLocaleString()
|
|
21125
|
-
}) : engineError("lnurlRange", {
|
|
21126
|
-
domain: params.destination.domain,
|
|
21127
|
-
min: params.minSat.toLocaleString(),
|
|
21128
|
-
max: params.maxSat.toLocaleString()
|
|
21129
|
-
});
|
|
21130
|
-
const url = new URL(params.callback);
|
|
21131
|
-
url.searchParams.set("amount", String(amountSat * 1e3));
|
|
21132
|
-
const text = comment?.trim();
|
|
21133
|
-
if (text) {
|
|
21134
|
-
if (!params.commentAllowed) throw new Error(`${params.destination.domain} does not take a comment`);
|
|
21135
|
-
if (text.length > params.commentAllowed) throw new Error(`The comment can be at most ${params.commentAllowed} characters`);
|
|
21136
|
-
url.searchParams.set("comment", text);
|
|
21137
|
-
}
|
|
21138
|
-
return url.href;
|
|
21139
|
-
}
|
|
21140
|
-
/**
|
|
21141
|
-
* Checks the invoice a service answered with (LUD-06): the amount asked, a payment hash, not expired, and
|
|
21142
|
-
* a commitment to the metadata the person was shown: the `h` tag is its sha256, or the invoice carries the
|
|
21143
|
-
* metadata itself as its description (the same commitment, unhashed).
|
|
21144
|
-
*/
|
|
21145
|
-
function parseInvoiceCallback(body, params, amountSat) {
|
|
21146
|
-
if (!isRecord(body) || typeof body.pr !== "string") throw new Error(`${params.callbackDomain} did not answer with an invoice`);
|
|
21147
|
-
const invoice = decodeBolt11(body.pr);
|
|
21148
|
-
if (!invoice) throw new Error(`${params.callbackDomain} answered with an invoice that does not decode`);
|
|
21149
|
-
if (invoice.amountMsat !== BigInt(amountSat) * 1000n) throw new Error(`The invoice asks for ${invoice.amountSat === null ? "any amount" : `${invoice.amountSat.toLocaleString()} sats`}, not the ${amountSat.toLocaleString()} sats you chose`);
|
|
21150
|
-
if (!invoice.paymentHash) throw new Error("The invoice has no payment hash");
|
|
21151
|
-
if (invoice.descriptionHash !== params.descriptionHash && invoice.description !== params.metadata) throw new Error("The invoice does not commit to what the service said the payment is for");
|
|
21152
|
-
if (invoice.expiresAt * 1e3 < Date.now()) throw new Error("The invoice the service answered with has already expired");
|
|
21153
|
-
let successAction;
|
|
21154
|
-
const action = body.successAction;
|
|
21155
|
-
if (isRecord(action)) {
|
|
21156
|
-
if (action.tag === "message" && typeof action.message === "string") successAction = {
|
|
21157
|
-
tag: "message",
|
|
21158
|
-
message: reason(action.message)
|
|
21159
|
-
};
|
|
21160
|
-
else if (action.tag === "url" && typeof action.url === "string" && /^https:\/\//i.test(action.url) && action.url.length <= LNURL_LIMITS.maxUrlLength) successAction = {
|
|
21161
|
-
tag: "url",
|
|
21162
|
-
description: reason(action.description),
|
|
21163
|
-
url: action.url
|
|
21164
|
-
};
|
|
21316
|
+
else if (tag === "p" && length === 52) paymentHash = [...wordsToBytes(field)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
21317
|
+
else if (tag === "h" && length === 52) descriptionHash = [...wordsToBytes(field)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
21165
21318
|
}
|
|
21166
21319
|
return {
|
|
21167
21320
|
invoice,
|
|
21168
|
-
|
|
21321
|
+
network: NETWORKS[prefix[1]],
|
|
21322
|
+
amountMsat,
|
|
21323
|
+
amountSat: amountMsat === null ? null : Number((amountMsat + 999n) / 1000n),
|
|
21324
|
+
createdAt,
|
|
21325
|
+
expiresAt: createdAt + expiry,
|
|
21326
|
+
description,
|
|
21327
|
+
descriptionHash,
|
|
21328
|
+
paymentHash
|
|
21169
21329
|
};
|
|
21170
21330
|
}
|
|
21171
|
-
/** Fetches and checks what a Lightning address or LNURL asks for. The service's domain learns of it. */
|
|
21172
|
-
async function resolveLightningDestination(text, options = {}) {
|
|
21173
|
-
const destination = parseLightningDestination(text);
|
|
21174
|
-
if (!destination) throw new Error("That is not a Lightning address or an LNURL");
|
|
21175
|
-
return parsePayParams(await fetchLnurlJson(destination.url, options), destination);
|
|
21176
|
-
}
|
|
21177
|
-
/** Asks the service for an invoice of `amountSat`, and checks it before it is handed to any wallet. */
|
|
21178
|
-
async function requestLnurlInvoice(params, amountSat, comment, options = {}) {
|
|
21179
|
-
return parseInvoiceCallback(await fetchLnurlJson(invoiceCallbackUrl(params, amountSat, comment), options), params, amountSat);
|
|
21180
|
-
}
|
|
21181
21331
|
//#endregion
|
|
21182
|
-
//#region ../core/src/
|
|
21183
|
-
var
|
|
21184
|
-
|
|
21185
|
-
|
|
21186
|
-
|
|
21187
|
-
|
|
21188
|
-
|
|
21189
|
-
|
|
21190
|
-
|
|
21191
|
-
|
|
21192
|
-
var
|
|
21193
|
-
|
|
21194
|
-
|
|
21195
|
-
|
|
21196
|
-
|
|
21197
|
-
|
|
21198
|
-
|
|
21199
|
-
"intranet",
|
|
21200
|
-
"private",
|
|
21201
|
-
"test",
|
|
21202
|
-
"example",
|
|
21203
|
-
"invalid",
|
|
21204
|
-
"onion",
|
|
21205
|
-
"alt",
|
|
21206
|
-
"arpa"
|
|
21332
|
+
//#region ../core/src/lnurl.ts
|
|
21333
|
+
var LNURL_LIMITS = {
|
|
21334
|
+
timeoutMs: 15e3,
|
|
21335
|
+
maxBytes: 65536,
|
|
21336
|
+
maxRedirects: 5,
|
|
21337
|
+
maxUrlLength: 2048,
|
|
21338
|
+
maxMetadataLength: 8192,
|
|
21339
|
+
maxComment: 2e3
|
|
21340
|
+
};
|
|
21341
|
+
var ADDRESS = /^([a-z0-9._+-]{1,64})@([a-z0-9-]+(?:\.[a-z0-9-]+)*(?::\d{1,5})?)$/i;
|
|
21342
|
+
var LNURL_BECH32 = /^lnurl1[02-9ac-hj-np-z]{6,}$/i;
|
|
21343
|
+
var REDIRECTS = [
|
|
21344
|
+
301,
|
|
21345
|
+
302,
|
|
21346
|
+
303,
|
|
21347
|
+
307,
|
|
21348
|
+
308
|
|
21207
21349
|
];
|
|
21208
21350
|
/**
|
|
21209
|
-
*
|
|
21210
|
-
*
|
|
21211
|
-
*
|
|
21351
|
+
* A host that is not on the public internet, as far as its name tells: this machine, a single label, `.local` and its
|
|
21352
|
+
* kin, private and reserved IP addresses. (What a public name resolves to is checked where the client resolves names
|
|
21353
|
+
* itself, as Ghostly Desktop does.)
|
|
21212
21354
|
*/
|
|
21213
|
-
function
|
|
21214
|
-
|
|
21215
|
-
|
|
21216
|
-
if (
|
|
21217
|
-
|
|
21218
|
-
|
|
21219
|
-
|
|
21220
|
-
|
|
21221
|
-
} catch {
|
|
21222
|
-
throw new Error("Enter a domain name, like example.com");
|
|
21223
|
-
}
|
|
21224
|
-
if (url.protocol !== "https:" || url.username || url.password || url.port || url.pathname !== "/" && url.pathname !== "" || url.search || url.hash) throw new Error("Enter only the domain name, without a path or port");
|
|
21225
|
-
value = url.hostname;
|
|
21226
|
-
} else {
|
|
21227
|
-
if (/[/:@?#\s\\]/.test(value)) throw new Error("Enter only the domain name, without a path or port");
|
|
21228
|
-
try {
|
|
21229
|
-
value = new URL(`https://${value}`).hostname;
|
|
21230
|
-
} catch {
|
|
21231
|
-
throw new Error("That is not a valid domain name");
|
|
21232
|
-
}
|
|
21355
|
+
function isLocalHost(hostname) {
|
|
21356
|
+
const host = hostname.toLowerCase().replace(/\.$/, "").replace(/^\[|\]$/g, "");
|
|
21357
|
+
if (!host.includes(".") && !host.includes(":")) return true;
|
|
21358
|
+
if (/(^|\.)(localhost|local|internal|lan|home\.arpa)$/.test(host)) return true;
|
|
21359
|
+
const v4 = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
|
|
21360
|
+
if (v4) {
|
|
21361
|
+
const [a, b] = [Number(v4[1]), Number(v4[2])];
|
|
21362
|
+
return a === 0 || a === 10 || a === 127 || a >= 224 || a === 169 && b === 254 || a === 172 && b >= 16 && b < 32 || a === 192 && b === 168 || a === 100 && b >= 64 && b < 128 || a === 198 && (b === 18 || b === 19) || a === 192 && b === 0;
|
|
21233
21363
|
}
|
|
21234
|
-
|
|
21235
|
-
|
|
21236
|
-
const labels = value.split(".");
|
|
21237
|
-
if (labels.length < 2) throw new Error("Use a full domain name, like example.com");
|
|
21238
|
-
for (const label of labels) if (!/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/.test(label)) throw new Error("That is not a valid domain name");
|
|
21239
|
-
const tld = labels[labels.length - 1];
|
|
21240
|
-
if (/^[0-9]+$/.test(tld) || /^\[/.test(value)) throw new Error("Use a domain name, not an IP address");
|
|
21241
|
-
if (SPECIAL_USE.includes(tld) || labels.length >= 2 && labels.slice(-2).join(".") === "home.arpa") throw new Error("That name only works on private networks");
|
|
21242
|
-
return value;
|
|
21364
|
+
if (host.includes(":")) return !/^[23][0-9a-f]{0,3}:/.test(host) || host.includes(".") || /^2001:0?db8:/.test(host);
|
|
21365
|
+
return false;
|
|
21243
21366
|
}
|
|
21244
|
-
var
|
|
21245
|
-
|
|
21246
|
-
|
|
21247
|
-
|
|
21367
|
+
var isLoopback = (hostname) => [
|
|
21368
|
+
"localhost",
|
|
21369
|
+
"127.0.0.1",
|
|
21370
|
+
"[::1]"
|
|
21371
|
+
].includes(hostname);
|
|
21372
|
+
/** HTTPS, or plain HTTP only for a hidden service or a test server on this machine; never credentials. */
|
|
21373
|
+
function acceptUrl(text) {
|
|
21374
|
+
if (text.length > LNURL_LIMITS.maxUrlLength) throw new Error("That LNURL is too long");
|
|
21375
|
+
let url;
|
|
21248
21376
|
try {
|
|
21249
|
-
|
|
21377
|
+
url = new URL(text);
|
|
21250
21378
|
} catch {
|
|
21251
|
-
|
|
21252
|
-
}
|
|
21253
|
-
}
|
|
21254
|
-
/** The TXT record value, in the `tag=value; …` style people know from DKIM. */
|
|
21255
|
-
function domainTxtRecord(r) {
|
|
21256
|
-
if (!validDomainRecord(r)) throw new Error("Invalid domain record");
|
|
21257
|
-
return `v=ghostly1; key=${r.key}; proof=${r.proof}`;
|
|
21258
|
-
}
|
|
21259
|
-
/** Parses one TXT string. Undefined for anything that is not ours or not exactly
|
|
21260
|
-
* well-formed; unknown extra tags are ignored for forward compatibility. */
|
|
21261
|
-
function parseDomainTxt(value) {
|
|
21262
|
-
if (typeof value !== "string" || utf8Encode(value).length > 1024) return;
|
|
21263
|
-
const tags = /* @__PURE__ */ new Map();
|
|
21264
|
-
const parts = value.split(";").map((p) => p.trim()).filter(Boolean);
|
|
21265
|
-
for (const part of parts) {
|
|
21266
|
-
const eq = part.indexOf("=");
|
|
21267
|
-
if (eq < 1) return;
|
|
21268
|
-
const name = part.slice(0, eq).trim().toLowerCase();
|
|
21269
|
-
if (tags.has(name)) return;
|
|
21270
|
-
tags.set(name, part.slice(eq + 1).trim());
|
|
21379
|
+
throw new Error("That LNURL does not point to a valid address");
|
|
21271
21380
|
}
|
|
21272
|
-
|
|
21273
|
-
|
|
21274
|
-
|
|
21275
|
-
|
|
21276
|
-
};
|
|
21277
|
-
return validDomainRecord(r) ? r : void 0;
|
|
21381
|
+
const plainAllowed = url.hostname.endsWith(".onion") || isLoopback(url.hostname);
|
|
21382
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && plainAllowed)) throw new Error("Lightning addresses and LNURLs must use HTTPS");
|
|
21383
|
+
if (url.username || url.password) throw new Error("That LNURL carries credentials, which is not allowed");
|
|
21384
|
+
return url;
|
|
21278
21385
|
}
|
|
21279
|
-
|
|
21280
|
-
|
|
21281
|
-
|
|
21282
|
-
|
|
21283
|
-
|
|
21284
|
-
|
|
21285
|
-
|
|
21286
|
-
|
|
21287
|
-
|
|
21386
|
+
/** Reads what was pasted as a Lightning address or an LNURL; null when it is neither. Throws when it is one, but a bad one. */
|
|
21387
|
+
function parseLightningDestination(input) {
|
|
21388
|
+
const text = input.trim().replace(/^(lightning:|⚡\s*)/i, "").trim();
|
|
21389
|
+
if (!text) return null;
|
|
21390
|
+
if (LNURL_BECH32.test(text)) {
|
|
21391
|
+
let decoded;
|
|
21392
|
+
try {
|
|
21393
|
+
decoded = bech32.decode(text.toLowerCase(), LNURL_LIMITS.maxUrlLength);
|
|
21394
|
+
} catch {
|
|
21395
|
+
throw new Error("That LNURL is not valid");
|
|
21396
|
+
}
|
|
21397
|
+
const url = acceptUrl(new TextDecoder().decode(bech32.fromWords(decoded.words)));
|
|
21398
|
+
return {
|
|
21399
|
+
kind: "lnurl",
|
|
21400
|
+
text: text.toLowerCase(),
|
|
21401
|
+
url: url.href,
|
|
21402
|
+
domain: url.host
|
|
21403
|
+
};
|
|
21288
21404
|
}
|
|
21289
|
-
|
|
21290
|
-
|
|
21291
|
-
|
|
21292
|
-
|
|
21293
|
-
|
|
21294
|
-
|
|
21295
|
-
|
|
21296
|
-
|
|
21297
|
-
|
|
21298
|
-
proof: r.proof
|
|
21299
|
-
}))
|
|
21300
|
-
}, null, 2) + "\n";
|
|
21301
|
-
}
|
|
21302
|
-
function parseJson(text) {
|
|
21303
|
-
if (typeof text !== "string" || utf8Encode(text).length > 16384) throw new Error("File too large");
|
|
21304
|
-
return JSON.parse(text);
|
|
21305
|
-
}
|
|
21306
|
-
/** Records from `ghostly.json`. Throws on a malformed file; [] when it names none. */
|
|
21307
|
-
function recordsFromWellKnown(text) {
|
|
21308
|
-
const body = parseJson(text);
|
|
21309
|
-
if (!body || typeof body !== "object" || Array.isArray(body) || body.ghostly !== 1 || !Array.isArray(body.proofs)) throw new Error("Not a Ghostly domain file");
|
|
21310
|
-
const out = [];
|
|
21311
|
-
for (const entry of body.proofs.slice(0, 32)) {
|
|
21312
|
-
if (!entry || typeof entry !== "object") continue;
|
|
21313
|
-
const r = {
|
|
21314
|
-
key: entry.key,
|
|
21315
|
-
proof: entry.proof
|
|
21405
|
+
if (/^lnurlp:\/\//i.test(text)) {
|
|
21406
|
+
const bare = text.slice(9);
|
|
21407
|
+
const host = bare.split(/[/?#]/)[0].toLowerCase();
|
|
21408
|
+
const url = acceptUrl(`${host.endsWith(".onion") || isLoopback(host.replace(/:\d+$/, "")) ? "http" : "https"}://${bare}`);
|
|
21409
|
+
return {
|
|
21410
|
+
kind: "lnurl",
|
|
21411
|
+
text: `lnurlp://${bare}`,
|
|
21412
|
+
url: url.href,
|
|
21413
|
+
domain: url.host
|
|
21316
21414
|
};
|
|
21317
|
-
if (validDomainRecord(r)) addRecord(out, r);
|
|
21318
21415
|
}
|
|
21319
|
-
|
|
21320
|
-
|
|
21321
|
-
|
|
21322
|
-
|
|
21323
|
-
|
|
21324
|
-
|
|
21325
|
-
|
|
21326
|
-
|
|
21416
|
+
const address = ADDRESS.exec(text);
|
|
21417
|
+
if (!address) return null;
|
|
21418
|
+
const name = address[1].toLowerCase(), domain = address[2].toLowerCase();
|
|
21419
|
+
const hostname = domain.replace(/:\d+$/, "");
|
|
21420
|
+
if (!hostname.includes(".") && !isLoopback(hostname)) return null;
|
|
21421
|
+
const url = acceptUrl(`${hostname.endsWith(".onion") || isLoopback(hostname) ? "http" : "https"}://${domain}/.well-known/lnurlp/${name}`);
|
|
21422
|
+
return {
|
|
21423
|
+
kind: "address",
|
|
21424
|
+
text: `${name}@${domain}`,
|
|
21425
|
+
url: url.href,
|
|
21426
|
+
domain: url.host,
|
|
21427
|
+
name
|
|
21428
|
+
};
|
|
21327
21429
|
}
|
|
21328
|
-
var
|
|
21329
|
-
var
|
|
21330
|
-
|
|
21331
|
-
|
|
21332
|
-
|
|
21333
|
-
|
|
21334
|
-
|
|
21335
|
-
|
|
21336
|
-
|
|
21337
|
-
|
|
21338
|
-
const
|
|
21339
|
-
|
|
21340
|
-
for (
|
|
21341
|
-
const
|
|
21342
|
-
if (
|
|
21343
|
-
|
|
21430
|
+
var hex$1 = (bytes) => Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
21431
|
+
var reason = (value) => typeof value === "string" ? value.replace(/\s+/g, " ").slice(0, 200) : "";
|
|
21432
|
+
var isRecord = (value) => !!value && typeof value === "object" && !Array.isArray(value);
|
|
21433
|
+
async function readBounded(response, maxBytes) {
|
|
21434
|
+
const reader = response.body?.getReader?.();
|
|
21435
|
+
if (!reader) {
|
|
21436
|
+
const text = await response.text();
|
|
21437
|
+
if (text.length > maxBytes) throw new Error("The server's answer is too large");
|
|
21438
|
+
return text;
|
|
21439
|
+
}
|
|
21440
|
+
const chunks = [];
|
|
21441
|
+
let size = 0;
|
|
21442
|
+
for (;;) {
|
|
21443
|
+
const { done, value } = await reader.read();
|
|
21444
|
+
if (done) break;
|
|
21445
|
+
size += value.byteLength;
|
|
21446
|
+
if (size > maxBytes) {
|
|
21447
|
+
await reader.cancel().catch(() => {});
|
|
21448
|
+
throw new Error("The server's answer is too large");
|
|
21449
|
+
}
|
|
21450
|
+
chunks.push(value);
|
|
21344
21451
|
}
|
|
21345
|
-
const
|
|
21346
|
-
|
|
21347
|
-
|
|
21452
|
+
const all = new Uint8Array(size);
|
|
21453
|
+
let at = 0;
|
|
21454
|
+
for (const chunk of chunks) {
|
|
21455
|
+
all.set(chunk, at);
|
|
21456
|
+
at += chunk.byteLength;
|
|
21457
|
+
}
|
|
21458
|
+
return new TextDecoder().decode(all);
|
|
21348
21459
|
}
|
|
21349
|
-
|
|
21350
|
-
|
|
21351
|
-
|
|
21352
|
-
|
|
21353
|
-
|
|
21354
|
-
|
|
21355
|
-
|
|
21356
|
-
|
|
21357
|
-
|
|
21358
|
-
|
|
21359
|
-
|
|
21360
|
-
|
|
21460
|
+
/** One GET to an LNURL service (and to each checked redirect of it): bounded in time and size, no credentials, no referrer, and the answer must be JSON. */
|
|
21461
|
+
async function fetchLnurlJson(url, options = {}) {
|
|
21462
|
+
const target = acceptUrl(url);
|
|
21463
|
+
const doFetch = options.fetch ?? globalThis.fetch;
|
|
21464
|
+
if (!doFetch) throw new Error("No way to reach the network here");
|
|
21465
|
+
const controller = new AbortController();
|
|
21466
|
+
const timer = setTimeout(() => controller.abort(/* @__PURE__ */ new Error("The server took too long to answer")), options.timeoutMs ?? LNURL_LIMITS.timeoutMs);
|
|
21467
|
+
const onAbort = () => controller.abort(options.signal?.reason);
|
|
21468
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
21469
|
+
try {
|
|
21470
|
+
let response;
|
|
21471
|
+
for (let at = target, hops = 0;; hops++) {
|
|
21472
|
+
try {
|
|
21473
|
+
response = await doFetch(at.href, {
|
|
21474
|
+
signal: controller.signal,
|
|
21475
|
+
headers: { accept: "application/json" },
|
|
21476
|
+
credentials: "omit",
|
|
21477
|
+
referrerPolicy: "no-referrer",
|
|
21478
|
+
redirect: "manual"
|
|
21479
|
+
});
|
|
21480
|
+
} catch {
|
|
21481
|
+
if (controller.signal.aborted) throw controller.signal.reason instanceof Error ? controller.signal.reason : /* @__PURE__ */ new Error("The request was cancelled");
|
|
21482
|
+
throw engineError("lnurlUnreachable", { host: target.host });
|
|
21483
|
+
}
|
|
21484
|
+
if (response.url) acceptUrl(response.url);
|
|
21485
|
+
if (response.type === "opaqueredirect") throw new Error(`${target.host} redirects to another address, which cannot be checked here`);
|
|
21486
|
+
const location = REDIRECTS.includes(response.status) ? response.headers.get("location") : null;
|
|
21487
|
+
if (!location) break;
|
|
21488
|
+
await response.body?.cancel().catch(() => {});
|
|
21489
|
+
if (hops >= LNURL_LIMITS.maxRedirects) throw new Error(`${target.host} redirects too many times`);
|
|
21490
|
+
let next;
|
|
21491
|
+
try {
|
|
21492
|
+
next = new URL(location, at);
|
|
21493
|
+
} catch {
|
|
21494
|
+
throw new Error(`${target.host} redirects to an invalid address`);
|
|
21495
|
+
}
|
|
21496
|
+
next = acceptUrl(next.href);
|
|
21497
|
+
if (next.host !== at.host) {
|
|
21498
|
+
if (next.protocol !== "https:") throw new Error("Lightning addresses and LNURLs must use HTTPS");
|
|
21499
|
+
if (isLocalHost(next.hostname)) throw new Error(`${target.host} redirects to a local address`);
|
|
21500
|
+
}
|
|
21501
|
+
at = next;
|
|
21361
21502
|
}
|
|
21362
|
-
|
|
21363
|
-
|
|
21364
|
-
|
|
21365
|
-
|
|
21366
|
-
|
|
21367
|
-
|
|
21368
|
-
|
|
21369
|
-
|
|
21503
|
+
const text = await readBounded(response, options.maxBytes ?? LNURL_LIMITS.maxBytes);
|
|
21504
|
+
let body;
|
|
21505
|
+
try {
|
|
21506
|
+
body = JSON.parse(text);
|
|
21507
|
+
} catch {
|
|
21508
|
+
throw new Error(`${target.host} did not answer with JSON${response.ok ? "" : ` (HTTP ${response.status})`}`);
|
|
21509
|
+
}
|
|
21510
|
+
if (isRecord(body) && body.status === "ERROR") throw new Error(`${target.host} refused: ${reason(body.reason) || "no reason given"}`);
|
|
21511
|
+
if (!response.ok) throw new Error(`${target.host} answered HTTP ${response.status}`);
|
|
21512
|
+
return body;
|
|
21513
|
+
} finally {
|
|
21514
|
+
clearTimeout(timer);
|
|
21515
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
21516
|
+
}
|
|
21517
|
+
}
|
|
21518
|
+
/** Checks a service's `payRequest` answer (LUD-06), and reads what a person needs to see of it. */
|
|
21519
|
+
function parsePayParams(body, destination) {
|
|
21520
|
+
if (!isRecord(body)) throw new Error(`${destination.domain} did not answer with a pay request`);
|
|
21521
|
+
if (body.tag !== "payRequest") throw new Error(`${destination.domain} answered with something other than a pay request${typeof body.tag === "string" ? ` (${reason(body.tag)})` : ""}`);
|
|
21522
|
+
if (typeof body.callback !== "string") throw new Error("The pay request has no callback");
|
|
21523
|
+
const callback = acceptUrl(body.callback);
|
|
21524
|
+
const { minSendable, maxSendable, metadata, commentAllowed } = body;
|
|
21525
|
+
if (!Number.isSafeInteger(minSendable) || !Number.isSafeInteger(maxSendable) || minSendable <= 0 || maxSendable < minSendable) throw new Error("The pay request has invalid amount limits");
|
|
21526
|
+
const minSat = Math.ceil(minSendable / 1e3), maxSat = Math.floor(maxSendable / 1e3);
|
|
21527
|
+
if (minSat > maxSat) throw new Error("The pay request only takes fractions of a sat, which cannot be paid here");
|
|
21528
|
+
if (typeof metadata !== "string" || metadata.length > LNURL_LIMITS.maxMetadataLength) throw new Error("The pay request has no usable metadata");
|
|
21529
|
+
let entries;
|
|
21530
|
+
try {
|
|
21531
|
+
entries = JSON.parse(metadata);
|
|
21532
|
+
} catch {
|
|
21533
|
+
throw new Error("The pay request's metadata is not JSON");
|
|
21370
21534
|
}
|
|
21535
|
+
if (!Array.isArray(entries) || !entries.every((e) => Array.isArray(e) && typeof e[0] === "string")) throw new Error("The pay request's metadata is malformed");
|
|
21536
|
+
const entry = (type) => entries.find((e) => e[0] === type)?.[1];
|
|
21537
|
+
const description = entry("text/plain");
|
|
21538
|
+
if (typeof description !== "string") throw new Error("The pay request says nothing about what it is for (no text/plain metadata)");
|
|
21539
|
+
const identifier = [entry("text/identifier"), entry("text/email")].find((v) => typeof v === "string")?.toLowerCase();
|
|
21540
|
+
if (destination.kind === "address" && identifier && identifier !== destination.text) throw new Error(`${destination.domain} answered for ${identifier}, not ${destination.text}`);
|
|
21541
|
+
const comment = Number.isSafeInteger(commentAllowed) && commentAllowed > 0 ? Math.min(commentAllowed, LNURL_LIMITS.maxComment) : 0;
|
|
21371
21542
|
return {
|
|
21372
|
-
|
|
21373
|
-
|
|
21543
|
+
destination,
|
|
21544
|
+
callback: callback.href,
|
|
21545
|
+
callbackDomain: callback.host,
|
|
21546
|
+
minSendable,
|
|
21547
|
+
maxSendable,
|
|
21548
|
+
minSat,
|
|
21549
|
+
maxSat,
|
|
21550
|
+
metadata,
|
|
21551
|
+
descriptionHash: hex$1(sha256(new TextEncoder().encode(metadata))),
|
|
21552
|
+
description: description.slice(0, 280),
|
|
21553
|
+
identifier,
|
|
21554
|
+
commentAllowed: comment
|
|
21374
21555
|
};
|
|
21375
21556
|
}
|
|
21557
|
+
/** Where to ask for the invoice of `amountSat` (and a comment, LUD-12). Throws when the amount or comment is not allowed. */
|
|
21558
|
+
function invoiceCallbackUrl(params, amountSat, comment) {
|
|
21559
|
+
if (!Number.isSafeInteger(amountSat) || amountSat <= 0) throw new Error("Enter a whole number of sats");
|
|
21560
|
+
if (amountSat < params.minSat || amountSat > params.maxSat) throw params.minSat === params.maxSat ? engineError("lnurlExactly", {
|
|
21561
|
+
domain: params.destination.domain,
|
|
21562
|
+
amount: params.minSat.toLocaleString()
|
|
21563
|
+
}) : engineError("lnurlRange", {
|
|
21564
|
+
domain: params.destination.domain,
|
|
21565
|
+
min: params.minSat.toLocaleString(),
|
|
21566
|
+
max: params.maxSat.toLocaleString()
|
|
21567
|
+
});
|
|
21568
|
+
const url = new URL(params.callback);
|
|
21569
|
+
url.searchParams.set("amount", String(amountSat * 1e3));
|
|
21570
|
+
const text = comment?.trim();
|
|
21571
|
+
if (text) {
|
|
21572
|
+
if (!params.commentAllowed) throw new Error(`${params.destination.domain} does not take a comment`);
|
|
21573
|
+
if (text.length > params.commentAllowed) throw new Error(`The comment can be at most ${params.commentAllowed} characters`);
|
|
21574
|
+
url.searchParams.set("comment", text);
|
|
21575
|
+
}
|
|
21576
|
+
return url.href;
|
|
21577
|
+
}
|
|
21376
21578
|
/**
|
|
21377
|
-
*
|
|
21378
|
-
* to
|
|
21379
|
-
*
|
|
21579
|
+
* Checks the invoice a service answered with (LUD-06): the amount asked, a payment hash, not expired, and
|
|
21580
|
+
* a commitment to the metadata the person was shown: the `h` tag is its sha256, or the invoice carries the
|
|
21581
|
+
* metadata itself as its description (the same commitment, unhashed).
|
|
21380
21582
|
*/
|
|
21381
|
-
function
|
|
21382
|
-
if (
|
|
21383
|
-
const
|
|
21384
|
-
|
|
21385
|
-
if (
|
|
21386
|
-
|
|
21387
|
-
|
|
21388
|
-
if (
|
|
21389
|
-
|
|
21390
|
-
|
|
21391
|
-
|
|
21392
|
-
|
|
21393
|
-
|
|
21394
|
-
|
|
21395
|
-
|
|
21396
|
-
if (
|
|
21397
|
-
|
|
21398
|
-
|
|
21399
|
-
|
|
21400
|
-
|
|
21401
|
-
all.push({
|
|
21402
|
-
name: owner.name,
|
|
21403
|
-
type: rtype,
|
|
21404
|
-
ttl,
|
|
21405
|
-
data: rdata
|
|
21406
|
-
});
|
|
21407
|
-
pos += len;
|
|
21408
|
-
}
|
|
21409
|
-
const owners = /* @__PURE__ */ new Set([want]);
|
|
21410
|
-
for (let hop = 0; hop < 8; hop++) {
|
|
21411
|
-
const cname = all.find((a) => a.type === DNS_TYPE.CNAME && owners.has(a.name) && !owners.has(utf8Decode(a.data)));
|
|
21412
|
-
if (!cname) break;
|
|
21413
|
-
owners.add(utf8Decode(cname.data));
|
|
21583
|
+
function parseInvoiceCallback(body, params, amountSat) {
|
|
21584
|
+
if (!isRecord(body) || typeof body.pr !== "string") throw new Error(`${params.callbackDomain} did not answer with an invoice`);
|
|
21585
|
+
const invoice = decodeBolt11(body.pr);
|
|
21586
|
+
if (!invoice) throw new Error(`${params.callbackDomain} answered with an invoice that does not decode`);
|
|
21587
|
+
if (invoice.amountMsat !== BigInt(amountSat) * 1000n) throw new Error(`The invoice asks for ${invoice.amountSat === null ? "any amount" : `${invoice.amountSat.toLocaleString()} sats`}, not the ${amountSat.toLocaleString()} sats you chose`);
|
|
21588
|
+
if (!invoice.paymentHash) throw new Error("The invoice has no payment hash");
|
|
21589
|
+
if (invoice.descriptionHash !== params.descriptionHash && invoice.description !== params.metadata) throw new Error("The invoice does not commit to what the service said the payment is for");
|
|
21590
|
+
if (invoice.expiresAt * 1e3 < Date.now()) throw new Error("The invoice the service answered with has already expired");
|
|
21591
|
+
let successAction;
|
|
21592
|
+
const action = body.successAction;
|
|
21593
|
+
if (isRecord(action)) {
|
|
21594
|
+
if (action.tag === "message" && typeof action.message === "string") successAction = {
|
|
21595
|
+
tag: "message",
|
|
21596
|
+
message: reason(action.message)
|
|
21597
|
+
};
|
|
21598
|
+
else if (action.tag === "url" && typeof action.url === "string" && /^https:\/\//i.test(action.url) && action.url.length <= LNURL_LIMITS.maxUrlLength) successAction = {
|
|
21599
|
+
tag: "url",
|
|
21600
|
+
description: reason(action.description),
|
|
21601
|
+
url: action.url
|
|
21602
|
+
};
|
|
21414
21603
|
}
|
|
21415
21604
|
return {
|
|
21416
|
-
|
|
21417
|
-
|
|
21418
|
-
answers: all.filter((a) => a.type === code && owners.has(a.name))
|
|
21605
|
+
invoice,
|
|
21606
|
+
successAction
|
|
21419
21607
|
};
|
|
21420
21608
|
}
|
|
21421
|
-
/**
|
|
21422
|
-
function
|
|
21423
|
-
const
|
|
21424
|
-
|
|
21425
|
-
|
|
21426
|
-
if (p + 1 + len > rdata.length) throw new Error("DNS text out of bounds");
|
|
21427
|
-
parts.push(rdata.subarray(p + 1, p + 1 + len));
|
|
21428
|
-
p += 1 + len;
|
|
21429
|
-
}
|
|
21430
|
-
return new TextDecoder("utf-8", { fatal: true }).decode(concatBytes$1(...parts));
|
|
21609
|
+
/** Fetches and checks what a Lightning address or LNURL asks for. The service's domain learns of it. */
|
|
21610
|
+
async function resolveLightningDestination(text, options = {}) {
|
|
21611
|
+
const destination = parseLightningDestination(text);
|
|
21612
|
+
if (!destination) throw new Error("That is not a Lightning address or an LNURL");
|
|
21613
|
+
return parsePayParams(await fetchLnurlJson(destination.url, options), destination);
|
|
21431
21614
|
}
|
|
21432
|
-
/**
|
|
21433
|
-
|
|
21434
|
-
|
|
21435
|
-
if (address.length === 4) {
|
|
21436
|
-
const [a, b, c] = address;
|
|
21437
|
-
return !(a === 0 || a === 10 || a === 127 || a >= 224 || a === 100 && b >= 64 && b <= 127 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 192 && b === 0 && c === 0 || a === 198 && (b === 18 || b === 19));
|
|
21438
|
-
}
|
|
21439
|
-
if (address.length !== 16) return false;
|
|
21440
|
-
const zeroTo = (n) => address.subarray(0, n).every((x) => x === 0);
|
|
21441
|
-
if (zeroTo(15) && (address[15] === 0 || address[15] === 1)) return false;
|
|
21442
|
-
if (zeroTo(10) && address[10] === 255 && address[11] === 255) return isPublicAddress(address.subarray(12));
|
|
21443
|
-
if (address[0] === 0 && address[1] === 100 && address[2] === 255 && address[3] === 155 && address.subarray(4, 12).every((x) => x === 0)) return isPublicAddress(address.subarray(12));
|
|
21444
|
-
if (zeroTo(12)) return false;
|
|
21445
|
-
if ((address[0] & 254) === 252) return false;
|
|
21446
|
-
if (address[0] === 254 && (address[1] & 192) === 128) return false;
|
|
21447
|
-
if (address[0] === 254 && (address[1] & 192) === 192) return false;
|
|
21448
|
-
if (address[0] === 255) return false;
|
|
21449
|
-
if (address[0] === 1 && address.subarray(1, 8).every((x) => x === 0)) return false;
|
|
21450
|
-
return (address[0] & 224) === 32;
|
|
21615
|
+
/** Asks the service for an invoice of `amountSat`, and checks it before it is handed to any wallet. */
|
|
21616
|
+
async function requestLnurlInvoice(params, amountSat, comment, options = {}) {
|
|
21617
|
+
return parseInvoiceCallback(await fetchLnurlJson(invoiceCallbackUrl(params, amountSat, comment), options), params, amountSat);
|
|
21451
21618
|
}
|
|
21452
21619
|
//#endregion
|
|
21453
21620
|
//#region ../core/src/pkdns.ts
|
|
@@ -25763,7 +25930,7 @@ var GroupSession = class {
|
|
|
25763
25930
|
const text = edit.text.trim();
|
|
25764
25931
|
if (!validEditText(text)) return { error: text ? "Message exceeds 16 KiB" : "An edit cannot be empty" };
|
|
25765
25932
|
if (!this.secret(ref.e)) return { error: "This message is too old to edit: its epoch's key is gone" };
|
|
25766
|
-
const named = validMentions(wireMentions(edit.mentions ?? []), text, rosterAdmin(commit.m) === this.myKey);
|
|
25933
|
+
const named = validMentions(wireMentions(edit.mentions ?? []), text, rosterAdmin(commit.m) === this.myKey && this.isAdmin);
|
|
25767
25934
|
const plain = JSON.stringify({
|
|
25768
25935
|
text,
|
|
25769
25936
|
...named.length ? { m: named } : {},
|
|
@@ -26087,7 +26254,7 @@ var GroupSession = class {
|
|
|
26087
26254
|
return;
|
|
26088
26255
|
}
|
|
26089
26256
|
const id = first && !this.provisional.has(own) ? groupMessageId(raw.s, first.e, first.n) : own;
|
|
26090
|
-
const mentions = this.openMentions(key, raw, text, rosterAdmin(commit.m) === raw.s);
|
|
26257
|
+
const mentions = this.openMentions(key, raw, text, rosterAdmin(commit.m) === raw.s && this.admin === raw.s);
|
|
26091
26258
|
const reply = this.openReply(key, raw);
|
|
26092
26259
|
const forwarded = readForwarded(raw.f);
|
|
26093
26260
|
const card = this.openCard(key, raw);
|
|
@@ -26150,7 +26317,7 @@ var GroupSession = class {
|
|
|
26150
26317
|
return;
|
|
26151
26318
|
}
|
|
26152
26319
|
if (!body || typeof body !== "object" || !validEditText(body.text)) return;
|
|
26153
|
-
const k = validMentions(body.m, body.text, rosterAdmin(commit.m) === raw.s);
|
|
26320
|
+
const k = validMentions(body.m, body.text, rosterAdmin(commit.m) === raw.s && this.admin === raw.s);
|
|
26154
26321
|
const sc = body.sc === void 0 ? void 0 : readStatusCard(body.sc);
|
|
26155
26322
|
if (!validEditNumber(raw.v) && !sc) return;
|
|
26156
26323
|
const copy = (this.state.relay ?? []).find((f) => f.s === raw.s && f.e === raw.e && f.n === raw.n);
|
|
@@ -26778,6 +26945,10 @@ var COMMUNITY_LIMITS = {
|
|
|
26778
26945
|
side: 512,
|
|
26779
26946
|
/** Commits waiting for their parent. */
|
|
26780
26947
|
pendingCommits: 64,
|
|
26948
|
+
/** Epoch secrets waiting for their commit. */
|
|
26949
|
+
pendingSecrets: 32,
|
|
26950
|
+
/** How long a commit waits for its parent, or a secret for its commit: many catch-ups; after it, a catch-up brings both again. */
|
|
26951
|
+
pendingMs: 12e4,
|
|
26781
26952
|
waiting: 64,
|
|
26782
26953
|
waitingBytes: 1048576,
|
|
26783
26954
|
replay: 256,
|
|
@@ -26837,6 +27008,17 @@ var FRESH = /* @__PURE__ */ new Set([
|
|
|
26837
27008
|
"rotate"
|
|
26838
27009
|
]);
|
|
26839
27010
|
/**
|
|
27011
|
+
* Commits that make the membership history longer and the group no smaller. A committer signs one only while the
|
|
27012
|
+
* history keeps room for everyone else to get out after it: two commits a member (a leave or a remove is one, and a
|
|
27013
|
+
* leaving admin hands the role over first). The history is never compacted, and one that filled with people in it
|
|
27014
|
+
* took no leave, remove or handover any more. Its own rule: a history up to `COMMUNITY_LIMITS.chain` verifies as before.
|
|
27015
|
+
*/
|
|
27016
|
+
var GROWS = /* @__PURE__ */ new Set([
|
|
27017
|
+
"add",
|
|
27018
|
+
"rotate",
|
|
27019
|
+
"link"
|
|
27020
|
+
]);
|
|
27021
|
+
/**
|
|
26840
27022
|
* Kinds only the admin commits. They are final: a branch that drops one never replaces the branch
|
|
26841
27023
|
* that has it, however long, so no member can undo a removal, a hand-over, a rotation or a link change
|
|
26842
27024
|
* by outgrowing it with admissions.
|
|
@@ -27515,13 +27697,20 @@ var CommunitySession = class {
|
|
|
27515
27697
|
if (typeof p !== "string" || !Number.isSafeInteger(e)) return false;
|
|
27516
27698
|
const parent = this.known.get(p), parentRoster = parent ? this.rosterAt(p) : void 0;
|
|
27517
27699
|
if (!parent || !parentRoster) {
|
|
27518
|
-
|
|
27519
|
-
|
|
27700
|
+
const commit = parseCommit(raw);
|
|
27701
|
+
if (!commit || commit.g !== this.id || !verify(fromBase64Url(commit.sig), utf8Encode(tuple(commit)), publicKeyFromZ32(commit.by))) return false;
|
|
27702
|
+
if (this.wasAdmin(commit.by)) {
|
|
27703
|
+
const now = this.hooks.clock?.() ?? Date.now(), key = p + ":" + String(e);
|
|
27704
|
+
this.dropStale(this.pendingCommits, now);
|
|
27705
|
+
this.pendingCommits.delete(key);
|
|
27706
|
+
if (this.pendingCommits.size >= COMMUNITY_LIMITS.pendingCommits) this.pendingCommits.delete(this.pendingCommits.keys().next().value);
|
|
27707
|
+
this.pendingCommits.set(key, {
|
|
27520
27708
|
from,
|
|
27521
|
-
commit
|
|
27709
|
+
commit,
|
|
27710
|
+
at: now
|
|
27522
27711
|
});
|
|
27523
|
-
this.ask(from);
|
|
27524
27712
|
}
|
|
27713
|
+
this.ask(from);
|
|
27525
27714
|
return false;
|
|
27526
27715
|
}
|
|
27527
27716
|
const result = verifyCommunityCommit(raw, parent, parentRoster, this.id);
|
|
@@ -27557,7 +27746,7 @@ var CommunitySession = class {
|
|
|
27557
27746
|
const sealed = this.pendingSecrets.get(h);
|
|
27558
27747
|
if (sealed) {
|
|
27559
27748
|
this.pendingSecrets.delete(h);
|
|
27560
|
-
this.takeSecret(h, sealed);
|
|
27749
|
+
this.takeSecret(h, sealed.sealed);
|
|
27561
27750
|
}
|
|
27562
27751
|
await this.chooseBranch();
|
|
27563
27752
|
for (const [key, pending] of [...this.pendingCommits]) if (key.startsWith(h + ":")) {
|
|
@@ -27571,6 +27760,18 @@ var CommunitySession = class {
|
|
|
27571
27760
|
}
|
|
27572
27761
|
return true;
|
|
27573
27762
|
}
|
|
27763
|
+
/** Is, or was on the branch I follow, the admin (and was not taken out since). */
|
|
27764
|
+
wasAdmin(key) {
|
|
27765
|
+
if (this.wasRemoved(key)) return false;
|
|
27766
|
+
return this.admin === key || this.state.chain.some((c, i) => i === 0 ? c.by === key : c.k === "role" && c.s === key);
|
|
27767
|
+
}
|
|
27768
|
+
/** What waited longer than `pendingMs` goes (the oldest first: a Map keeps the order they came in). */
|
|
27769
|
+
dropStale(pending, now) {
|
|
27770
|
+
for (const [key, { at }] of pending) {
|
|
27771
|
+
if (now - at < COMMUNITY_LIMITS.pendingMs) break;
|
|
27772
|
+
pending.delete(key);
|
|
27773
|
+
}
|
|
27774
|
+
}
|
|
27574
27775
|
/**
|
|
27575
27776
|
* Follows the best branch among the known tips (see `better`); reorganizes the main branch when it
|
|
27576
27777
|
* changed. A branch that keeps the admin's changes, or a longer one, wins however far back it parts
|
|
@@ -27673,7 +27874,15 @@ var CommunitySession = class {
|
|
|
27673
27874
|
takeSecret(h, sealed) {
|
|
27674
27875
|
const c = this.known.get(h);
|
|
27675
27876
|
if (!c) {
|
|
27676
|
-
if (
|
|
27877
|
+
if (!HASH.test(h) || !isSealed(sealed)) return false;
|
|
27878
|
+
const now = this.hooks.clock?.() ?? Date.now();
|
|
27879
|
+
this.dropStale(this.pendingSecrets, now);
|
|
27880
|
+
this.pendingSecrets.delete(h);
|
|
27881
|
+
if (this.pendingSecrets.size >= COMMUNITY_LIMITS.pendingSecrets) this.pendingSecrets.delete(this.pendingSecrets.keys().next().value);
|
|
27882
|
+
this.pendingSecrets.set(h, {
|
|
27883
|
+
sealed,
|
|
27884
|
+
at: now
|
|
27885
|
+
});
|
|
27677
27886
|
return false;
|
|
27678
27887
|
}
|
|
27679
27888
|
if (this.state.secrets[h] || !isSealed(sealed) || !rosterHas(this.rosterAt(h) ?? [], this.myKey)) return false;
|
|
@@ -27738,6 +27947,7 @@ var CommunitySession = class {
|
|
|
27738
27947
|
};
|
|
27739
27948
|
const roster = communityRoster(this.roster, draftBase);
|
|
27740
27949
|
if (!roster) throw new Error("This change is not allowed");
|
|
27950
|
+
if (GROWS.has(kind) && this.state.chain.length + 1 + 2 * (roster.length - 1) > COMMUNITY_LIMITS.chain) throw new Error("This group has reached its membership history limit. Create a new group.");
|
|
27741
27951
|
const draft = {
|
|
27742
27952
|
...draftBase,
|
|
27743
27953
|
m: rosterHash(roster)
|
|
@@ -28443,7 +28653,9 @@ var CommunitySession = class {
|
|
|
28443
28653
|
}
|
|
28444
28654
|
/** Something is missing here: the current epoch's secret, or the epoch of frames waiting. */
|
|
28445
28655
|
get needsCatchUp() {
|
|
28446
|
-
|
|
28656
|
+
if (!this.isMember) return false;
|
|
28657
|
+
this.dropStale(this.pendingCommits, this.hooks.clock?.() ?? Date.now());
|
|
28658
|
+
return !this.state.secrets[this.topHash] || this.waiting.length > 0 || this.pendingCommits.size > 0;
|
|
28447
28659
|
}
|
|
28448
28660
|
/** Asks a member I have an edge to for what I lack (the engine calls it now and then while `needsCatchUp`). */
|
|
28449
28661
|
catchUp(to) {
|
|
@@ -28877,6 +29089,8 @@ var APP_BUNDLE_LIMITS = {
|
|
|
28877
29089
|
/** A path, in bytes (paths are ASCII). */
|
|
28878
29090
|
pathBytes: 128,
|
|
28879
29091
|
iconBytes: 262144,
|
|
29092
|
+
/** The icon's width and height, in pixels: a client decodes it whole to draw it, whatever its bytes. */
|
|
29093
|
+
iconEdge: 1024,
|
|
28880
29094
|
screenshots: 8,
|
|
28881
29095
|
/** Lengths in Unicode code points. */
|
|
28882
29096
|
title: 40,
|
|
@@ -29272,6 +29486,11 @@ function readAppBundle(bytes) {
|
|
|
29272
29486
|
reason: "bad-icon",
|
|
29273
29487
|
detail: "icon.png is not a square PNG"
|
|
29274
29488
|
};
|
|
29489
|
+
if (size.width > APP_BUNDLE_LIMITS.iconEdge) return {
|
|
29490
|
+
ok: false,
|
|
29491
|
+
reason: "bad-icon",
|
|
29492
|
+
detail: "icon.png is larger than 1024 × 1024 pixels"
|
|
29493
|
+
};
|
|
29275
29494
|
}
|
|
29276
29495
|
return {
|
|
29277
29496
|
ok: true,
|
|
@@ -29425,6 +29644,13 @@ var STORE_REQUIRED = [
|
|
|
29425
29644
|
"removed",
|
|
29426
29645
|
"revoked"
|
|
29427
29646
|
];
|
|
29647
|
+
/** Runs a check written in steps to its end, in one go. */
|
|
29648
|
+
function inOneRun(steps) {
|
|
29649
|
+
for (;;) {
|
|
29650
|
+
const step = steps.next();
|
|
29651
|
+
if (step.done) return step.value;
|
|
29652
|
+
}
|
|
29653
|
+
}
|
|
29428
29654
|
/**
|
|
29429
29655
|
* Checks an index's fields and bounds, and every revocation it carries (one that does not verify refuses the index:
|
|
29430
29656
|
* the store copied it, so a broken one is the store's fault). The signature and `expires` are `readAppStore`'s. An
|
|
@@ -29432,6 +29658,14 @@ var STORE_REQUIRED = [
|
|
|
29432
29658
|
* statement and keeps its exact keys.
|
|
29433
29659
|
*/
|
|
29434
29660
|
function checkAppStoreIndex(value, options = {}) {
|
|
29661
|
+
const fields = inOneRun(checkAppStoreFieldsSteps(value, options));
|
|
29662
|
+
return fields.ok ? inOneRun(checkAppStoreRevocationsSteps(value)) : fields;
|
|
29663
|
+
}
|
|
29664
|
+
/**
|
|
29665
|
+
* The first half of `checkAppStoreIndex`: the fields and bounds, and the shape of every revocation, with no signature
|
|
29666
|
+
* checked. A step after each listing, removal and revocation (see `readAppStoreSteps`).
|
|
29667
|
+
*/
|
|
29668
|
+
function* checkAppStoreFieldsSteps(value, options = {}) {
|
|
29435
29669
|
if (!isObject(value)) return {
|
|
29436
29670
|
ok: false,
|
|
29437
29671
|
reason: "bad-field",
|
|
@@ -29468,38 +29702,65 @@ function checkAppStoreIndex(value, options = {}) {
|
|
|
29468
29702
|
detail: ref
|
|
29469
29703
|
};
|
|
29470
29704
|
refs.add(ref);
|
|
29705
|
+
yield;
|
|
29471
29706
|
}
|
|
29472
29707
|
if (!Array.isArray(removed) || removed.length > APP_STORE_LIMITS.removed) return bad("removed");
|
|
29473
29708
|
for (const entry of removed) {
|
|
29474
29709
|
const checked = checkRemoval(entry, options);
|
|
29475
29710
|
if (!checked.ok) return checked;
|
|
29711
|
+
yield;
|
|
29476
29712
|
}
|
|
29477
29713
|
if (!Array.isArray(revoked) || revoked.length > APP_STORE_LIMITS.revoked) return bad("revoked");
|
|
29478
29714
|
for (const entry of revoked) {
|
|
29479
|
-
const read =
|
|
29715
|
+
const read = readAppRevocationShape(entry);
|
|
29480
29716
|
if (!read.ok) return {
|
|
29481
29717
|
ok: false,
|
|
29482
29718
|
reason: "bad-revocation",
|
|
29483
29719
|
detail: read.detail ?? read.reason
|
|
29484
29720
|
};
|
|
29721
|
+
yield;
|
|
29722
|
+
}
|
|
29723
|
+
return fine;
|
|
29724
|
+
}
|
|
29725
|
+
/** The second half: the signature of each revocation of an index whose fields hold, one step each. */
|
|
29726
|
+
function* checkAppStoreRevocationsSteps(index, checked) {
|
|
29727
|
+
for (const revocation of index.revoked) {
|
|
29728
|
+
const verified = verifyAppRevocation(revocation, checked);
|
|
29729
|
+
if (!verified.ok) return {
|
|
29730
|
+
ok: false,
|
|
29731
|
+
reason: "bad-revocation",
|
|
29732
|
+
detail: verified.reason
|
|
29733
|
+
};
|
|
29734
|
+
yield;
|
|
29485
29735
|
}
|
|
29486
29736
|
return fine;
|
|
29487
29737
|
}
|
|
29488
29738
|
/**
|
|
29489
|
-
* Reads `ghostly-store.json` with its `ghostly-store.sig`, in this order: size, JSON, canonical bytes, fields and
|
|
29490
|
-
*
|
|
29491
|
-
* then `expires` against `now` (seconds): more than 90 days ahead is
|
|
29492
|
-
* is
|
|
29739
|
+
* Reads `ghostly-store.json` with its `ghostly-store.sig`, in this order: size, JSON, canonical bytes, fields and the
|
|
29740
|
+
* shape of each revocation, the signature statement, the key the reader holds for this store (when it holds one), the
|
|
29741
|
+
* signature, the signature of each revocation, then `expires` against `now` (seconds): more than 90 days ahead is
|
|
29742
|
+
* refused, past is `expired`. The index's own signature comes before its revocations': an index nobody signed costs
|
|
29743
|
+
* its reader one signature check, whatever it carries. A key it does not know is ignored (a later store's optional
|
|
29744
|
+
* field) and stays in the bytes the signature covers.
|
|
29493
29745
|
*/
|
|
29494
29746
|
function readAppStore(indexBytes, sigBytes, now, heldKey) {
|
|
29747
|
+
return inOneRun(readAppStoreSteps(indexBytes, sigBytes, now, heldKey));
|
|
29748
|
+
}
|
|
29749
|
+
/**
|
|
29750
|
+
* `readAppStore` in steps, for a reader that shares its thread (a page): it stops after each listing, removal and
|
|
29751
|
+
* revocation, and after each revocation's signature check, so the caller can let the thread go between two `next()`
|
|
29752
|
+
* and an index of 4096 revocations never holds it for seconds. With `checked`, a revocation it holds is not checked again.
|
|
29753
|
+
*/
|
|
29754
|
+
function* readAppStoreSteps(indexBytes, sigBytes, now, heldKey, checked) {
|
|
29495
29755
|
if (indexBytes.length > APP_STORE_LIMITS.indexBytes) return {
|
|
29496
29756
|
ok: false,
|
|
29497
29757
|
reason: "too-large"
|
|
29498
29758
|
};
|
|
29499
29759
|
const read = readCanonicalJson(indexBytes);
|
|
29500
29760
|
if (!read.ok) return read;
|
|
29501
|
-
|
|
29502
|
-
|
|
29761
|
+
yield;
|
|
29762
|
+
const fields = yield* checkAppStoreFieldsSteps(read.value);
|
|
29763
|
+
if (!fields.ok) return fields;
|
|
29503
29764
|
const index = read.value;
|
|
29504
29765
|
const statement = readAppSignature(sigBytes);
|
|
29505
29766
|
if (!statement.ok) return statement;
|
|
@@ -29510,6 +29771,9 @@ function readAppStore(indexBytes, sigBytes, now, heldKey) {
|
|
|
29510
29771
|
};
|
|
29511
29772
|
const signed = verifyAppSignature(APP_PREFIXES.store, indexBytes, statement.signature, index.key);
|
|
29512
29773
|
if (!signed.ok) return signed;
|
|
29774
|
+
yield;
|
|
29775
|
+
const revocations = yield* checkAppStoreRevocationsSteps(index, checked);
|
|
29776
|
+
if (!revocations.ok) return revocations;
|
|
29513
29777
|
if (index.expires > now + APP_STORE_LIMITS.expiresAheadS) return {
|
|
29514
29778
|
ok: false,
|
|
29515
29779
|
reason: "expires-too-far"
|
|
@@ -29600,6 +29864,6 @@ function appStoreDecision(held, read) {
|
|
|
29600
29864
|
return read.digest === held.digest ? "same" : "equivocation";
|
|
29601
29865
|
}
|
|
29602
29866
|
//#endregion
|
|
29603
|
-
export { SSHSIG_NAMESPACE as $, readAppRevocations as $a, parseImageMeta as $i, engineError as $n, handoffContextProof as $o, pinNumberHolds as $r, HANDOFF_CAPABILITY as $s, PaymentPreflightError as $t, decodeGroupEntryLink as A, pairedReplyAuthor as Aa, withRequestOptions as Ac, IDENTITY_REVOCATION_LABEL as Ai, encodeDnsQuery as An, signalHasVideo as Ao, mergeBeacon as Ar, ENROLL_HELLO as As, didJwsAlg as At, groupWakes as B, statusCardText as Ba, publicKeyFromZ32 as Bc, lastSharedWithMe as Bi, resolveLightningDestination as Bn, HANDOFF_OFFER as Bo, FILE_LIMITS as Br, readEnrollHello as Bs, parseDidSignaturePaste as Bt, COMMUNITY_LIMITS as C, nextReactionNumber as Ca, RelayTransport as Cc, readManifest as Ci, homeserverWebEndpoint as Cn, RTC_CONFIG as Co, COMMUNITY_TOPOLOGY as Cr, readSetUpdate as Cs, DID_DOCUMENT_MAX_BYTES as Ct, decodeCommunityLink as D, readReaction as Da, traceLink as Dc, dialDescriptors as Di, dnsProofName as Dn, callSignalHeardAt as Do, freshHubs as Dr, verifySetUpdate as Ds, checkDidJws as Dt, communityHasFrame as E, reactionIsNewer as Ea, setLinkTraceSink as Ec, capsDescriptors as Ei, decodeDnsResponse as En, callRtcConfig as Eo, doorHubs as Er, signTombstone as Es, DidError as Et, mergeKnocks as F, RUN_RESULTS as Fa, openRelayPayload as Fc, identityRevocationValue as Fi, recordsFromWellKnown as Fn, HANDOFF_ACK as Fo, readBeacon as Fr, enrollDoneFrame as Fs, didWebFileUrl as Ft, COMMUNITY_EDIT_FRAME as G, shownTime as Ga, fromBase64Url as Gc, automaticTransport as Gi, findEndpoint as Gn, HANDOFF_VERIFIED as Go, TypingSender as Gr, OwnInviteError as Gs, didDhtFromPublicKey as Gt, GROUP_REACTED_FRAME as H, claimedTime as Ha, MAX_AVATAR_LENGTH as Hc, emptyProofLedger as Hi, ENDPOINT as Hn, HANDOFF_PIECE_BYTES as Ho, transferStalled as Hr, createDeviceInvite as Hs, staticDidDocument as Ht, readKnocks as I, STATUS_CARD_LIMITS as Ia, parseRelayPayload as Ic, identityStatement as Ii, recordsName as In, HANDOFF_CANCEL as Io, readBeaconHead as Ir, enrollGrantFrame as Is, didWebHost as It, carryMentions as J, appFingerprint as Ja, toZ32 as Jc, DHT_FILE_TOO_LARGE as Ji, parseFedimintRequestPayload as Jn, handoffAttemptFailed as Jo, GROUP_PIN_FRAME as Jr, encodeInviteCode as Js, resolveDidDht as Jt, GROUP_EDIT_FRAME as K, APP_PREFIXES as Ka, randomBytes as Kc, irohDescriptor as Ki, isFederationId as Kn, filePartName as Ko, sanitizeTypingStatus as Kr, createChatInvite as Ks, encodeDidDhtDeactivated as Kt, GROUP_WAKE_FRAME as L, buttonLabelClash as La, createIdentity as Lc, newIdentityBinding as Li, txtValue as Ln, HANDOFF_DONE as Lo, readLobby as Lr, enrollHelloFrame as Ls, normalizeDid as Lt, entryParams as M, replySnippet as Ma, RELAY_PAYLOAD_MAX_BYTES as Mc, IdentityExchange as Mi, nip05RootKey as Mn, HTTP_SERVICE_PROTO as Mo, newerHead as Mr, enrollCancelFrame as Ms, didWebDocumentUrl as Mt, knockIdentity as N, BUTTON_ID as Na, createRelayPayload as Nc, boundVerifiedIdentity as Ni, normalizeDomain as Nn, LEGACY_SERVICES as No, pickHubs as Nr, enrollCancelReason as Ns, didWebFile as Nt, encodeCommunityLink as O, validReactionNumber as Oa, budgetRetryMs as Oc, IDENTITY_MAX_EVIDENCE as Oi, domainTxtRecord as On, extractParamsFromSdp as Oo, lobbyKeys as Or, ENROLL_DONE as Os, checkDidSignature as Ot, knockRecords as P, ITEM_STATES as Pa, newerPacket as Pc, emptyIdentityLedger as Pi, recordsFromTxt as Pn, serviceIdFromName as Po, rankHubs as Pr, enrollDigits as Ps, didWebFileNames as Pt, SSHSIG_MAX_ARMOR as Q, isAppRef as Qa, dhtFileId as Qi, pairedMessageFrame as Qn, handoffContext as Qo, pinIsNewer as Qr, ENROLL_CAPABILITY as Qs, ONCHAIN_PROVIDER as Qt, GroupWakeLimiter as R, checkStatusCard as Ra, identityFromSeed as Rc, receivedIdentityStatus as Ri, wellKnownUrl as Rn, HANDOFF_HAVE as Ro, shouldBeHub as Rr, enrollProofFrame as Rs, parseDid as Rt, sealGroupTyping as S, ReactionWindow as Sa, DEFAULT_RELAYS as Sc, newMailbox as Si, pubkyProofPath as Sn, CALL_SIGNAL_MAX_AGE_MS as So, pickMeshHubs as Sr, SET_UPDATE as Ss, verifyAtprotoRecordProof as St, GROUP_READ_NOTE_COMMUNITY as T, reactionEmoji as Ta, normalizeRelayUrl as Tc, DHT_TEXT_CAPABILITY as Ti, DOMAIN_FILE_MAX_BYTES as Tn, buildSdpFromSignal as To, beaconRecords as Tr, setUpdateFrame as Ts, DID_JWS_MAX_LENGTH as Tt, GROUP_READ_NOTE as U, heardTime as Ua, sanitizeAvatar as Uc, validExternalKey as Ui, cashuRequestPayload as Un, HANDOFF_REQUEST as Uo, TYPING_KINDS as Ur, deviceInviteJoinerParams as Us, strictBase64Url as Ut, parseGroupWakeFrame as V, arrivalKey as Va, verify as Vc, PeerProofs as Vi, decodeBolt11 as Vn, HANDOFF_PAKE as Vo, transferEnded as Vr, verifyEnrollProof as Vs, signingKeys as Vt, GroupSession as W, receivedTimestamp as Wa, bytesEqual as Wc, TRANSPORTS as Wi, fedimintRequestPayload as Wn, HANDOFF_TIMINGS as Wo, TYPING_REFRESH_MS as Wr, readDeviceInvite as Ws, didDhtDocument as Wt, mentionsMember as X, checkAppRevokeStatement as Xa, utf8Encode as Xc, peekDhtMailbox as Xi, INVITE_TAKEN as Xn, handoffBusyFrame as Xo, mayPin as Xr, inviteOwnership as Xs, ETHEREUM_USDT as Xt, parseCommunityEdit as Y, appRef as Ya, utf8Decode as Yc, DHT_MESSAGE_TTL as Yi, GhostLink as Yn, handoffAttemptSucceeded as Yo, PIN_LIMITS as Yr, inviteLink as Ys, signDidDhtPacket as Yt, groupName as Z, isAppKey as Za, DHT_FILE_CAPABILITY as Zi, LIVENESS_PING_MS as Zn, handoffCancelFrame as Zo, nextPinNumber as Zr, DEVICES_CAPABILITY as Zs, EVM_TEST_CHAINS as Zt, readAppBundle as _, forwardedAgain as _a, readTurnPacket as _c, HOLD_CAPABILITY as _i, isPubkyProofFolder as _n, parseLocalTarget as _o, groupSignalFrame as _r, readHandoffPake as _s, isAtprotoDid as _t, planAppUpdate as a, PLAYABLE_AUDIO as aa, newDeviceSetSecret as ac, parseWakeFrame as ai, assertTokenUnits as an, MAX_GROUP_CHAIN as ao, fetchesByItself as ar, handoffOfferFrame as as, verifySshSignature as at, groupTypingEpoch as b, GROUP_REACTION_FRAME as ba, turnKeys as bc, HoldKeys as bi, pubkyProofCapability as bn, decodeControl as bo, mayBeHub as br, sealHandoffPiece as bs, shortAtprotoDid as bt, signAppStore as c, fileMessageText as ca, TURN_PUT_WINDOW_MS as cc, checkPushEndpoint as ci, validatePaymentTarget as cn, verifyCommitSignature as co, readGroupFileMeta as cr, handoffRetryAfter as cs, decodeAnyBitcoinAddress as ct, APP_ICON_PATH as d, parseVideoMeta as da, classifyTurnRead as dc, isAppVersion as di, SPARK_PROVIDER as dn, DHT_POLL_INTERVALS as do, readGroupWantNo as dr, newHandoffId as ds, ATPROTO_PROOF_COLLECTION as dt, readImageMeta as ea, deviceEchoFrame as ec, readPin as ei, SEPOLIA_TEST_USDT as en, signAppObject as eo, engineText as er, handoffDataFrame as es, SshSigError as et, appDigest as f, EDIT_BUFFER_MS as fa, turnPutSummary as fc, APPS_CAPABILITY as fi, sparkAddressKind as fn, EXPECT_PEER_MS as fo, CarriedTransport as fr, readHandoffAck as fs, ATPROTO_PROOF_SCOPE as ft, isAppUrl as g, parseLinkPreview as ga, nextTurnPosition as gc, UPGRADE_CAPABILITY as gi, isPubkyKey as gn, formatLocalTarget as go, SIGNAL_REPLY_MS as gr, readHandoffHello as gs, checkAtprotoProofRecord as gt, checkAppManifest as h, dhtEditId as ha, firstDeviceSetSecret as hc, STATUS_CARD_CAPABILITY as hi, PUBKY_PROOF_MAX_BYTES as hn, GhostlyHttpError as ho, SIGNAL_HOLD_MS as hr, readHandoffHave as hs, atprotoProofRkey as ht, checkAppBeforeRun as i, sanitizeMime as ia, devicePingFrame as ic, newWakeToken as ii, WALLET_NETWORKS as in, GROUP_MEMBER_CAP as io, GROUP_WANT_NO_FRAME as ir, handoffManifestFrames as is, sshSignCommand as it, encodeGroupEntryLink as j, readReply as ja, PacketTooLargeError as jc, IdentityCheckUnavailable as ji, isPublicAddress as jn, emptyLinkRecords as jo, mergeLobby as jr, ENROLL_PROOF as js, didSignedMessages as jt, KNOCK_TTL_MS as k, wireReaction as ka, isDiscoveryBudgetError as kc, IDENTITY_MAX_VALIDITY as ki, domainWellKnownFile as kn, parseCallSignal as ko, lobbyRecords as kr, ENROLL_GRANT as ks, didGhostlyServiceEntry as kt, APP_BUNDLE_LIMITS as l, parseVoiceMeta as la, TURN_RAISE_READ_TIMEOUT_MS as lc, vapidKeysMatch as li, walletNetworkOf as ln, edgeParams as lo, readGroupHave as lr, handoffStreamKey as ls, ATPROTO_DECLARED_SCOPE as lt, assembleAppBundle as m, RateWindow as ma, TURN_MAX as mc, EDIT_CAPABILITY as mi, isBitcoinAddress as mn, presenceSeenAt as mo, SIGNAL_FRESH_MS as mr, readHandoffDone as ms, atprotoProofRecord as mt, appStoreDecision as n, safeBlobType as na, deviceKeyZ32 as nc, WakeLimiter as ni, TEST_USDT_FAUCET_AMOUNT as nn, canonicalJson as no, GROUP_HAVE_FRAME as nr, handoffEphemeral as ns, parseSshSignature as nt, checkAppStoreIndex as o, VOICE_LIMITS as oa, seedSigner as oc, relayRequest as oi, assertWholeSats as on, MEMBER_KEY as oo, groupFileFallback as or, handoffPakeFrame as os, verifyBitcoinMessage as ot, appViewOf as p, EDIT_RATE_WINDOW_MS as pa, TURN_LAST_SEQUENCE as pc, BUTTONS_CAPABILITY as pi, sparkInvoiceDetails as pn, RELAY_POLL_INTERVALS as po, GROUP_SIGNAL_FRAME as pr, readHandoffCancel as ps, atprotoDidMethod as pt, GROUP_EDIT_TEXT_BYTES as q, APP_STATEMENT_LIMITS as qa, toBase64Url as qc, irohRelayUrl as qi, parseCashuRequestPayload as qn, handoffAttemptAllowed as qo, typingActivity as qr, decodeInviteCode as qs, encodeDidDhtPacket as qt, appUpdateDecision as r, sanitizeFileName as ra, deviceLinkPairing as rc, deviceWakeRequest as ri, USDT_PUBLIC_RPC as rn, canonicalJsonBytes as ro, GROUP_WANT_FRAME as rr, handoffHelloFrame as rs, sameSshKey as rt, readAppStore as s, baseMime as sa, webCryptoSigner as sc, wakeRequest as si, formatPaymentAmount as sn, rosterHas as so, isGroupMessageId as sr, handoffReleaseFrame as ss, BitcoinAddressError as st, appRevocationFor as t, formatFileSize as ta, deviceEchoNonce as tc, WAKE_CALL_INTERVAL_MS as ti, SEPOLIA_TEST_USDT_FAUCET as tn, signAppRevocation as to, GROUP_FILE_LIMITS as tr, handoffDigest as ts, parseSshPublicKey as tt, APP_BUNDLE_MAGIC as u, voicePeaksOf as ua, TURN_SETTLE_MS as uc, chatAppId as ui, SPARK_NETWORKS as un, DirectPathWatch as uo, readGroupWant as ur, handoffVersions as us, ATPROTO_FULL_SCOPE as ut, GROUP_TYPING_FRAME as v, readForwarded as va, signTurnPacket as vc, HOLD_DATED_BACK_MS as vi, newPubkyProofFolder as vn, webLocalFetch as vo, readGroupSignal as vr, readHandoffTurnFrame as vs, normalizeAtprotoHandle as vt, CommunitySession as w, queueReaction as wa, currentRelays as wc, CapsExchange as wi, pubkyHomeserverOf as wn, answersOffer as wo, beaconKeys as wr, setAckFrame as ws, DID_FILE_MAX_BYTES as wt, openGroupTyping as x, REACTION_LIMITS as xa, turnName as xc, HoldRefusedError as xi, pubkyProofFile as xn, encodeControl as xo, meshHubs as xr, SET_ACK as xs, AtprotoProofError as xt, GroupTypingBoard as y, COMMUNITY_REACTION_FRAME as ya, signTurnRelease as yc, HOLD_LIMITS as yi, normalizePubkyKey as yn, LIMITS as yo, MESH_HUBS as yr, readHandoffVerified as ys, parseAtprotoDidDocument as yt, groupWakeFrame as z, sameButtonText as za, identityFromSeedB64 as zc, revokesIdentity as zi, requestLnurlInvoice as zn, HANDOFF_HELLO as zo, ChatFiles as zr, readEnrollGrant as zs, parseDidDocument as zt };
|
|
29867
|
+
export { groupName as $, APP_PREFIXES as $a, sanitizeAvatar as $c, peekDhtMailbox as $i, readGroupWantNo as $n, HANDOFF_REQUEST as $o, isAppVersion as $r, deviceInviteJoinerParams as $s, EVM_TEST_CHAINS as $t, encodeCommunityLink as A, readReaction as Aa, turnKeys as Ac, dialDescriptors as Ai, ENDPOINT as An, decodeControl as Ao, transferStalled as Ar, sealHandoffPiece as As, checkDidSignature as At, GroupWakeLimiter as B, RUN_RESULTS as Ba, withRequestOptions as Bc, newIdentityBinding as Bi, pairedMessageFrame as Bn, signalHasVideo as Bo, pinIsNewer as Br, ENROLL_HELLO as Bs, parseDid as Bt, openGroupTyping as C, GROUP_REACTION_FRAME as Ca, TURN_LAST_SEQUENCE as Cc, recordsFromWellKnown as Ci, pubkyProofFile as Cn, RELAY_POLL_INTERVALS as Co, readBeacon as Cr, readHandoffCancel as Cs, AtprotoProofError as Ct, GROUP_READ_NOTE_COMMUNITY as D, queueReaction as Da, readTurnPacket as Dc, CapsExchange as Di, requestLnurlInvoice as Dn, parseLocalTarget as Do, ChatFiles as Dr, readHandoffPake as Ds, DID_JWS_MAX_LENGTH as Dt, CommunitySession as E, nextReactionNumber as Ea, nextTurnPosition as Ec, wellKnownUrl as Ei, pubkyHomeserverOf as En, formatLocalTarget as Eo, shouldBeHub as Er, readHandoffHello as Es, DID_FILE_MAX_BYTES as Et, knockIdentity as F, replySnippet as Fa, normalizeRelayUrl as Fc, IdentityExchange as Fi, parseCashuRequestPayload as Fn, buildSdpFromSignal as Fo, typingActivity as Fr, setUpdateFrame as Fs, didWebFile as Ft, GROUP_READ_NOTE as G, checkStatusCard as Ga, openRelayPayload as Gc, emptyProofLedger as Gi, GROUP_WANT_FRAME as Gn, HANDOFF_ACK as Go, deviceWakeRequest as Gr, enrollDoneFrame as Gs, strictBase64Url as Gt, groupWakes as H, buttonLabelClash as Ha, RELAY_PAYLOAD_MAX_BYTES as Hc, revokesIdentity as Hi, engineText as Hn, HTTP_SERVICE_PROTO as Ho, readPin as Hr, enrollCancelFrame as Hs, parseDidSignaturePaste as Ht, knockRecords as I, BUTTON_ID as Ia, setLinkTraceSink as Ic, boundVerifiedIdentity as Ii, parseFedimintRequestPayload as In, callRtcConfig as Io, GROUP_PIN_FRAME as Ir, signTombstone as Is, didWebFileNames as It, GROUP_EDIT_FRAME as J, arrivalKey as Ja, identityFromSeed as Jc, automaticTransport as Ji, groupFileFallback as Jn, HANDOFF_HAVE as Jo, relayRequest as Jr, enrollProofFrame as Js, encodeDidDhtDeactivated as Jt, GroupSession as K, sameButtonText as Ka, parseRelayPayload as Kc, validExternalKey as Ki, GROUP_WANT_NO_FRAME as Kn, HANDOFF_CANCEL as Ko, newWakeToken as Kr, enrollGrantFrame as Ks, didDhtDocument as Kt, mergeKnocks as L, ITEM_STATES as La, traceLink as Lc, emptyIdentityLedger as Li, GhostLink as Ln, callSignalHeardAt as Lo, PIN_LIMITS as Lr, verifySetUpdate as Ls, didWebFileUrl as Lt, decodeGroupEntryLink as M, wireReaction as Ma, DEFAULT_RELAYS as Mc, IDENTITY_MAX_VALIDITY as Mi, fedimintRequestPayload as Mn, CALL_SIGNAL_MAX_AGE_MS as Mo, TYPING_REFRESH_MS as Mr, SET_UPDATE as Ms, didJwsAlg as Mt, encodeGroupEntryLink as N, pairedReplyAuthor as Na, RelayTransport as Nc, IDENTITY_REVOCATION_LABEL as Ni, findEndpoint as Nn, RTC_CONFIG as No, TypingSender as Nr, readSetUpdate as Ns, didSignedMessages as Nt, communityHasFrame as O, reactionEmoji as Oa, signTurnPacket as Oc, DHT_TEXT_CAPABILITY as Oi, resolveLightningDestination as On, webLocalFetch as Oo, FILE_LIMITS as Or, readHandoffTurnFrame as Os, DidError as Ot, entryParams as P, readReply as Pa, currentRelays as Pc, IdentityCheckUnavailable as Pi, isFederationId as Pn, answersOffer as Po, sanitizeTypingStatus as Pr, setAckFrame as Ps, didWebDocumentUrl as Pt, mentionsMember as Q, shownTime as Qa, MAX_AVATAR_LENGTH as Qc, DHT_MESSAGE_TTL as Qi, readGroupWant as Qn, HANDOFF_PIECE_BYTES as Qo, chatAppId as Qr, createDeviceInvite as Qs, ETHEREUM_USDT as Qt, readKnocks as R, PR_CHECKS as Ra, budgetRetryMs as Rc, identityRevocationValue as Ri, INVITE_TAKEN as Rn, extractParamsFromSdp as Ro, mayPin as Rr, ENROLL_DONE as Rs, didWebHost as Rt, groupTypingEpoch as S, COMMUNITY_REACTION_FRAME as Sa, turnPutSummary as Sc, recordsFromTxt as Si, pubkyProofCapability as Sn, EXPECT_PEER_MS as So, rankHubs as Sr, readHandoffAck as Ss, shortAtprotoDid as St, COMMUNITY_LIMITS as T, ReactionWindow as Ta, firstDeviceSetSecret as Tc, txtValue as Ti, homeserverWebEndpoint as Tn, GhostlyHttpError as To, readLobby as Tr, readHandoffHave as Ts, DID_DOCUMENT_MAX_BYTES as Tt, parseGroupWakeFrame as U, cardLine as Ua, createRelayPayload as Uc, lastSharedWithMe as Ui, GROUP_FILE_LIMITS as Un, LEGACY_SERVICES as Uo, WAKE_CALL_INTERVAL_MS as Ur, enrollCancelReason as Us, signingKeys as Ut, groupWakeFrame as V, STATUS_CARD_LIMITS as Va, PacketTooLargeError as Vc, receivedIdentityStatus as Vi, engineError as Vn, emptyLinkRecords as Vo, pinNumberHolds as Vr, ENROLL_PROOF as Vs, parseDidDocument as Vt, GROUP_REACTED_FRAME as W, cardUrl as Wa, newerPacket as Wc, PeerProofs as Wi, GROUP_HAVE_FRAME as Wn, serviceIdFromName as Wo, WakeLimiter as Wr, enrollDigits as Ws, staticDidDocument as Wt, carryMentions as X, heardTime as Xa, publicKeyFromZ32 as Xc, irohRelayUrl as Xi, readGroupFileMeta as Xn, HANDOFF_OFFER as Xo, checkPushEndpoint as Xr, readEnrollHello as Xs, resolveDidDht as Xt, GROUP_EDIT_TEXT_BYTES as Y, claimedTime as Ya, identityFromSeedB64 as Yc, irohDescriptor as Yi, isGroupMessageId as Yn, HANDOFF_HELLO as Yo, wakeRequest as Yr, readEnrollGrant as Ys, encodeDidDhtPacket as Yt, parseCommunityEdit as Z, receivedTimestamp as Za, verify as Zc, DHT_FILE_TOO_LARGE as Zi, readGroupHave as Zn, HANDOFF_PAKE as Zo, vapidKeysMatch as Zr, verifyEnrollProof as Zs, signDidDhtPacket as Zt, checkAppManifest as _, RateWindow as _a, webCryptoSigner as _c, domainWellKnownFile as _i, PUBKY_PROOF_MAX_BYTES as _n, rosterHas as _o, lobbyRecords as _r, handoffReleaseFrame as _s, atprotoProofRkey as _t, planAppUpdate as a, safeBlobType as aa, inviteLink as ac, HOLD_CAPABILITY as ai, utf8Decode as al, USDT_PUBLIC_RPC as an, isAppRef as ao, groupSignalFrame as ar, handoffAttemptSucceeded as as, sameSshKey as at, GROUP_TYPING_FRAME as b, forwardedAgain as ba, TURN_SETTLE_MS as bc, nip05RootKey as bi, newPubkyProofFolder as bn, DirectPathWatch as bo, newerHead as br, handoffVersions as bs, normalizeAtprotoHandle as bt, readAppStore as c, PLAYABLE_AUDIO as ca, ENROLL_CAPABILITY as cc, HoldKeys as ci, assertWholeSats as cn, signAppObject as co, mayBeHub as cr, handoffContext as cs, verifyBitcoinMessage as ct, APP_BUNDLE_LIMITS as d, fileMessageText as da, deviceEchoNonce as dc, newMailbox as di, walletNetworkOf as dn, canonicalJsonBytes as do, COMMUNITY_TOPOLOGY as dr, handoffDigest as ds, ATPROTO_DECLARED_SCOPE as dt, DHT_FILE_CAPABILITY as ea, readDeviceInvite as ec, APPS_CAPABILITY as ei, bytesEqual as el, ONCHAIN_PROVIDER as en, APP_STATEMENT_LIMITS as eo, CarriedTransport as er, HANDOFF_TIMINGS as es, SSHSIG_MAX_ARMOR as et, APP_BUNDLE_MAGIC as f, parseVoiceMeta as fa, deviceKeyZ32 as fc, readManifest as fi, SPARK_NETWORKS as fn, readCanonicalJson as fo, beaconKeys as fr, handoffEphemeral as fs, ATPROTO_FULL_SCOPE as ft, assembleAppBundle as g, EDIT_RATE_WINDOW_MS as ga, seedSigner as gc, domainTxtRecord as gi, isBitcoinAddress as gn, commitHash as go, lobbyKeys as gr, handoffPakeFrame as gs, atprotoProofRecord as gt, appViewOf as h, EDIT_BUFFER_MS as ha, newDeviceSetSecret as hc, dnsProofName as hi, sparkInvoiceDetails as hn, MEMBER_KEY as ho, freshHubs as hr, handoffOfferFrame as hs, atprotoDidMethod as ht, checkAppBeforeRun as i, formatFileSize as ia, encodeInviteCode as ic, UPGRADE_CAPABILITY as ii, toZ32 as il, TEST_USDT_FAUCET_AMOUNT as in, isAppKey as io, SIGNAL_REPLY_MS as ir, handoffAttemptFailed as is, parseSshSignature as it, KNOCK_TTL_MS as j, validReactionNumber as ja, turnName as jc, IDENTITY_MAX_EVIDENCE as ji, cashuRequestPayload as jn, encodeControl as jo, TYPING_KINDS as jr, SET_ACK as js, didGhostlyServiceEntry as jt, decodeCommunityLink as k, reactionIsNewer as ka, signTurnRelease as kc, capsDescriptors as ki, decodeBolt11 as kn, LIMITS as ko, transferEnded as kr, readHandoffVerified as ks, checkDidJws as kt, readAppStoreSteps as l, VOICE_LIMITS as la, HANDOFF_CAPABILITY as lc, HoldRefusedError as li, formatPaymentAmount as ln, signAppRevocation as lo, meshHubs as lr, handoffContextProof as ls, BitcoinAddressError as lt, appDigest as m, parseVideoMeta as ma, devicePingFrame as mc, decodeDnsResponse as mi, sparkAddressKind as mn, MAX_GROUP_CHAIN as mo, doorHubs as mr, handoffManifestFrames as ms, ATPROTO_PROOF_SCOPE as mt, appStoreDecision as n, parseImageMeta as na, createChatInvite as nc, EDIT_CAPABILITY as ni, randomBytes as nl, SEPOLIA_TEST_USDT as nn, appRef as no, SIGNAL_FRESH_MS as nr, filePartName as ns, SshSigError as nt, APP_STORE_LIMITS as o, sanitizeFileName as oa, inviteOwnership as oc, HOLD_DATED_BACK_MS as oi, utf8Encode as ol, WALLET_NETWORKS as on, readAppRevocation as oo, readGroupSignal as or, handoffBusyFrame as os, sshSignCommand as ot, APP_ICON_PATH as p, voicePeaksOf as pa, deviceLinkPairing as pc, DOMAIN_FILE_MAX_BYTES as pi, SPARK_PROVIDER as pn, GROUP_MEMBER_CAP as po, beaconRecords as pr, handoffHelloFrame as ps, ATPROTO_PROOF_COLLECTION as pt, COMMUNITY_EDIT_FRAME as q, statusCardText as qa, createIdentity as qc, TRANSPORTS as qi, fetchesByItself as qn, HANDOFF_DONE as qo, parseWakeFrame as qr, enrollHelloFrame as qs, didDhtFromPublicKey as qt, appUpdateDecision as r, readImageMeta as ra, decodeInviteCode as rc, STATUS_CARD_CAPABILITY as ri, toBase64Url as rl, SEPOLIA_TEST_USDT_FAUCET as rn, checkAppRevokeStatement as ro, SIGNAL_HOLD_MS as rr, handoffAttemptAllowed as rs, parseSshPublicKey as rt, checkAppStoreIndex as s, sanitizeMime as sa, DEVICES_CAPABILITY as sc, HOLD_LIMITS as si, assertTokenUnits as sn, readAppRevocations as so, MESH_HUBS as sr, handoffCancelFrame as ss, verifySshSignature as st, appRevocationFor as t, dhtFileId as ta, OwnInviteError as tc, BUTTONS_CAPABILITY as ti, fromBase64Url as tl, PaymentPreflightError as tn, appFingerprint as to, GROUP_SIGNAL_FRAME as tr, HANDOFF_VERIFIED as ts, SSHSIG_NAMESPACE as tt, signAppStore as u, baseMime as ua, deviceEchoFrame as uc, isHoldUrl as ui, validatePaymentTarget as un, canonicalJson as uo, pickMeshHubs as ur, handoffDataFrame as us, decodeAnyBitcoinAddress as ut, isAppUrl as v, dhtEditId as va, TURN_PUT_WINDOW_MS as vc, encodeDnsQuery as vi, isPubkyKey as vn, verifyCommitSignature as vo, mergeBeacon as vr, handoffRetryAfter as vs, checkAtprotoProofRecord as vt, sealGroupTyping as w, REACTION_LIMITS as wa, TURN_MAX as wc, recordsName as wi, pubkyProofPath as wn, presenceSeenAt as wo, readBeaconHead as wr, readHandoffDone as ws, verifyAtprotoRecordProof as wt, GroupTypingBoard as x, readForwarded as xa, classifyTurnRead as xc, normalizeDomain as xi, normalizePubkyKey as xn, DHT_POLL_INTERVALS as xo, pickHubs as xr, newHandoffId as xs, parseAtprotoDidDocument as xt, readAppBundle as y, parseLinkPreview as ya, TURN_RAISE_READ_TIMEOUT_MS as yc, isPublicAddress as yi, isPubkyProofFolder as yn, edgeParams as yo, mergeLobby as yr, handoffStreamKey as ys, isAtprotoDid as yt, GROUP_WAKE_FRAME as z, PR_STATES as za, isDiscoveryBudgetError as zc, identityStatement as zi, LIVENESS_PING_MS as zn, parseCallSignal as zo, nextPinNumber as zr, ENROLL_GRANT as zs, normalizeDid as zt };
|
|
29604
29868
|
|
|
29605
|
-
//# sourceMappingURL=src-
|
|
29869
|
+
//# sourceMappingURL=src-XzcHzneo.js.map
|