@ghostlytools/cli 1.3.1 → 1.3.3

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.
Files changed (72) hide show
  1. package/README.md +6 -6
  2. package/dist/assets/{api-Dq2QXQDk.js → api-B1fADtqd.js} +65 -20
  3. package/dist/assets/api-B1fADtqd.js.map +1 -0
  4. package/dist/assets/{api-TiB5QSG9.js → api-Bg7bh_6E.js} +2 -2
  5. package/dist/assets/{apiKit-CA2x5GJF.js → apiKit-BsUZPHLn.js} +44 -33
  6. package/dist/assets/apiKit-BsUZPHLn.js.map +1 -0
  7. package/dist/assets/{apps-BxV9i_Y4.js → apps-BaUNt-5c.js} +91 -24
  8. package/dist/assets/apps-BaUNt-5c.js.map +1 -0
  9. package/dist/assets/{apps-C-T8jeji.js → apps-DepomCiX.js} +1 -1
  10. package/dist/assets/{arkade-C_qDmHnm.js → arkade-CNnMaoEQ.js} +4 -4
  11. package/dist/assets/arkade-CNnMaoEQ.js.map +1 -0
  12. package/dist/assets/{backup-Bugnn3Ue.js → backup-B5Q1W-Ey.js} +1 -1
  13. package/dist/assets/{backup-CnMN2oUK.js → backup-Bbz3lyC5.js} +6 -6
  14. package/dist/assets/{backup-CnMN2oUK.js.map → backup-Bbz3lyC5.js.map} +1 -1
  15. package/dist/assets/buttons-CUd7j0ZA.js +2 -0
  16. package/dist/assets/{buttons-CvNBzh2S.js → buttons-DS1iURp8.js} +4 -4
  17. package/dist/assets/{buttons-CvNBzh2S.js.map → buttons-DS1iURp8.js.map} +1 -1
  18. package/dist/assets/{envelope-WjingYSc.js → envelope-fNC941z4.js} +2 -2
  19. package/dist/assets/{envelope-WjingYSc.js.map → envelope-fNC941z4.js.map} +1 -1
  20. package/dist/assets/{errors-DO5z6h_l.js → errors-B-zMxbcd.js} +6 -5
  21. package/dist/assets/{errors-DO5z6h_l.js.map → errors-B-zMxbcd.js.map} +1 -1
  22. package/dist/assets/{ghostly_iroh_web-Dvzh3Y6E.js → ghostly_iroh_web-BsAEFIjl.js} +30 -36
  23. package/dist/assets/ghostly_iroh_web-BsAEFIjl.js.map +1 -0
  24. package/dist/assets/ghostly_iroh_web_bg.wasm +0 -0
  25. package/dist/assets/{groupFiles-IEUW3DQz.js → groupFiles-6qnLCnOm.js} +82 -29
  26. package/dist/assets/groupFiles-6qnLCnOm.js.map +1 -0
  27. package/dist/assets/{host-CWt-jtqz.js → host-Dj3C1fn3.js} +305 -68
  28. package/dist/assets/host-Dj3C1fn3.js.map +1 -0
  29. package/dist/assets/{hyperdhtRelay-CPYwSLLS.js → hyperdhtRelay-CrgYQN-g.js} +2 -2
  30. package/dist/assets/{hyperdhtRelay-CPYwSLLS.js.map → hyperdhtRelay-CrgYQN-g.js.map} +1 -1
  31. package/dist/assets/{irohWeb-4oChIOIp.js → irohWeb-DSyLp5Re.js} +3 -3
  32. package/dist/assets/{irohWeb-4oChIOIp.js.map → irohWeb-DSyLp5Re.js.map} +1 -1
  33. package/dist/assets/{main-DbqcdShN.js → main-BDd11Yd1.js} +159 -54
  34. package/dist/assets/main-BDd11Yd1.js.map +1 -0
  35. package/dist/assets/{mainline-ZkiOnevs.js → mainline-CLTo0nGa.js} +2 -2
  36. package/dist/assets/{mainline-ZkiOnevs.js.map → mainline-CLTo0nGa.js.map} +1 -1
  37. package/dist/assets/{networkReason-CetKkTPo.js → networkReason-DwcadZz5.js} +2 -2
  38. package/dist/assets/{networkReason-CetKkTPo.js.map → networkReason-DwcadZz5.js.map} +1 -1
  39. package/dist/assets/{nwc-CgkqyvsE.js → nwc-BNFyyECH.js} +3 -3
  40. package/dist/assets/{nwc-CgkqyvsE.js.map → nwc-BNFyyECH.js.map} +1 -1
  41. package/dist/assets/{nwcProvider-4eVz_8HP.js → nwcProvider-BbjJOfLD.js} +2 -2
  42. package/dist/assets/{nwcProvider-4eVz_8HP.js.map → nwcProvider-BbjJOfLD.js.map} +1 -1
  43. package/dist/assets/{profiles-BHc9pEYg.js → profiles-gPCjLOQq.js} +2 -2
  44. package/dist/assets/{profiles-BHc9pEYg.js.map → profiles-gPCjLOQq.js.map} +1 -1
  45. package/dist/assets/{registry-DlAKO0Fy.js → registry-CoLHpxkb.js} +17 -32
  46. package/dist/assets/registry-CoLHpxkb.js.map +1 -0
  47. package/dist/assets/{registry-OBJBnHVQ.js → registry-DoTb6uEr.js} +1 -1
  48. package/dist/assets/{resolve-CouHhCpJ.js → resolve-BIDJ-UUl.js} +18 -3
  49. package/dist/assets/resolve-BIDJ-UUl.js.map +1 -0
  50. package/dist/assets/{server-Ss0gJ-nw.js → server-Bx3RGkBC.js} +380 -116
  51. package/dist/assets/server-Bx3RGkBC.js.map +1 -0
  52. package/dist/assets/{src-DDzSERyc.js → src-D7gcJElT.js} +470 -242
  53. package/dist/assets/src-D7gcJElT.js.map +1 -0
  54. package/dist/assets/{turnRefs-B87dtrHM.js → turnRefs-PN644M9o.js} +2 -2
  55. package/dist/assets/{turnRefs-B87dtrHM.js.map → turnRefs-PN644M9o.js.map} +1 -1
  56. package/dist/assets/{usdt-Fj8CQO2z.js → usdt-B2TLRlYa.js} +3 -3
  57. package/dist/assets/{usdt-Fj8CQO2z.js.map → usdt-B2TLRlYa.js.map} +1 -1
  58. package/dist/ghostly.mjs +1 -1
  59. package/package.json +1 -1
  60. package/dist/assets/api-Dq2QXQDk.js.map +0 -1
  61. package/dist/assets/apiKit-CA2x5GJF.js.map +0 -1
  62. package/dist/assets/apps-BxV9i_Y4.js.map +0 -1
  63. package/dist/assets/arkade-C_qDmHnm.js.map +0 -1
  64. package/dist/assets/buttons-Bj7Vf7Pn.js +0 -2
  65. package/dist/assets/ghostly_iroh_web-Dvzh3Y6E.js.map +0 -1
  66. package/dist/assets/groupFiles-IEUW3DQz.js.map +0 -1
  67. package/dist/assets/host-CWt-jtqz.js.map +0 -1
  68. package/dist/assets/main-DbqcdShN.js.map +0 -1
  69. package/dist/assets/registry-DlAKO0Fy.js.map +0 -1
  70. package/dist/assets/resolve-CouHhCpJ.js.map +0 -1
  71. package/dist/assets/server-Ss0gJ-nw.js.map +0 -1
  72. package/dist/assets/src-DDzSERyc.js.map +0 -1
@@ -1,7 +1,7 @@
1
1
  import { xsalsa20poly1305 } from "@noble/ciphers/salsa.js";
2
2
  import { ed25519, x25519 } from "@noble/curves/ed25519.js";
3
- import { hkdf } from "@noble/hashes/hkdf.js";
4
3
  import { sha256, sha512 } from "@noble/hashes/sha2.js";
4
+ import { hkdf } from "@noble/hashes/hkdf.js";
5
5
  import { base58, base64, base64urlnopad, bech32, bech32m, createBase58check } from "@scure/base";
6
6
  import { hmac } from "@noble/hashes/hmac.js";
7
7
  import { hexToBytes } from "@noble/hashes/utils.js";
@@ -123,6 +123,19 @@ function fromZ32(str) {
123
123
  }
124
124
  return out;
125
125
  }
126
+ /**
127
+ * A 32-byte key written as the one z-base-32 spelling `toZ32` produces: 52 characters that decode to 32 bytes and
128
+ * encode back to the same string. The last character carries four zero padding bits, so each key has exactly one
129
+ * spelling and keys may be compared as strings (WISP 02). Any other spelling is not a key here.
130
+ */
131
+ function isZ32Key(value) {
132
+ if (typeof value !== "string" || value.length !== 52) return false;
133
+ try {
134
+ return toZ32(fromZ32(value)) === value;
135
+ } catch {
136
+ return false;
137
+ }
138
+ }
126
139
  function randomBytes(length) {
127
140
  const bytes = new Uint8Array(length);
128
141
  crypto.getRandomValues(bytes);
@@ -255,11 +268,26 @@ function tryDecrypt(encoded, key) {
255
268
  return null;
256
269
  }
257
270
  }
258
- //#endregion
259
- //#region ../core/src/identity.ts
271
+ /**
272
+ * Public keys already derived, by the SHA-256 of their seed, the most recently used last. A chat's start derives the
273
+ * same few seeds' keys again and again (its link, its delivery, its capability record): ~0.25 ms each in V8, ~3.4x
274
+ * that in JSC. The seed itself is never kept, so a seed its owner zeroes leaves nothing behind here.
275
+ */
276
+ var publicKeys = /* @__PURE__ */ new Map();
277
+ function publicKeyOf(seed) {
278
+ const id = toBase64Url(sha256(seed));
279
+ let key = publicKeys.get(id);
280
+ if (key) publicKeys.delete(id);
281
+ else {
282
+ key = ed25519.getPublicKey(seed);
283
+ if (publicKeys.size >= 1024) publicKeys.delete(publicKeys.keys().next().value);
284
+ }
285
+ publicKeys.set(id, key);
286
+ return key.slice();
287
+ }
260
288
  function identityFromSeed(seed) {
261
289
  if (seed.length !== 32) throw new Error("Seed must be exactly 32 bytes");
262
- const publicKey = ed25519.getPublicKey(seed);
290
+ const publicKey = publicKeyOf(seed);
263
291
  return {
264
292
  seed,
265
293
  seedB64: toBase64Url(seed),
@@ -276,7 +304,7 @@ function createIdentity() {
276
304
  function publicKeyFromZ32(z32) {
277
305
  if (z32.length !== 52) throw new Error("Invalid public key: expected 52 z-base-32 characters");
278
306
  const bytes = fromZ32(z32);
279
- if (bytes.length !== 32) throw new Error("Invalid public key length");
307
+ if (toZ32(bytes) !== z32) throw new Error("Invalid public key: not canonical z-base-32");
280
308
  return bytes;
281
309
  }
282
310
  function sign(message, seed) {
@@ -3016,7 +3044,6 @@ function readGhostly(code) {
3016
3044
  };
3017
3045
  }
3018
3046
  var B64_KEY = /^[A-Za-z0-9_-]{43}$/;
3019
- var Z32_KEY$1 = /^[ybndrfg8ejkmcpqxot1uwisza345h769]{52}$/;
3020
3047
  function readSlashForm(code) {
3021
3048
  const dht = code.startsWith("pair2d/");
3022
3049
  const profile = dht || code.startsWith("pair1/") ? "paired-chat/1" : void 0;
@@ -3030,7 +3057,7 @@ function readSlashForm(code) {
3030
3057
  reason: "not-ghostly"
3031
3058
  };
3032
3059
  const [seedB64, peerPubKeyZ32, encKeyB64] = parts;
3033
- const bad = !B64_KEY.test(seedB64) ? "The seed" : !Z32_KEY$1.test(peerPubKeyZ32) ? "The contact's key" : !B64_KEY.test(encKeyB64) ? "The shared key" : null;
3060
+ const bad = !B64_KEY.test(seedB64) ? "The seed" : !isZ32Key(peerPubKeyZ32) ? "The contact's key" : !B64_KEY.test(encKeyB64) ? "The shared key" : null;
3034
3061
  if (bad) return {
3035
3062
  ok: false,
3036
3063
  reason: "damaged",
@@ -3313,7 +3340,7 @@ var ENROLL_CANCEL_REASONS = [
3313
3340
  "full",
3314
3341
  "failed"
3315
3342
  ];
3316
- var KEY$5 = /^[A-Za-z0-9_-]{43}$/;
3343
+ var KEY$3 = /^[A-Za-z0-9_-]{43}$/;
3317
3344
  var SIGNATURE$2 = /^[A-Za-z0-9_-]{86}$/;
3318
3345
  var HASH$3 = /^[0-9a-f]{64}$/;
3319
3346
  function checkKey(key, who) {
@@ -3363,7 +3390,7 @@ async function enrollHelloFrame(transcriptHash, inviterKey, joiner, about) {
3363
3390
  * key's over this session's tuple. Null for anything else.
3364
3391
  */
3365
3392
  function readEnrollHello(frame, transcriptHash, inviterKey, sessionKey) {
3366
- if (frame.t !== "enroll-hello" || typeof frame.k !== "string" || !KEY$5.test(frame.k) || typeof frame.s !== "string" || !SIGNATURE$2.test(frame.s)) return null;
3393
+ if (frame.t !== "enroll-hello" || typeof frame.k !== "string" || !KEY$3.test(frame.k) || typeof frame.s !== "string" || !SIGNATURE$2.test(frame.s)) return null;
3367
3394
  if (typeof frame.name !== "string" || frame.name.length > 64 || typeof frame.app !== "string" || frame.app.length > 32) return null;
3368
3395
  const key = fromBase64Url(frame.k);
3369
3396
  if (key.length !== 32 || toBase64Url(key) !== toBase64Url(sessionKey)) return null;
@@ -3445,7 +3472,7 @@ function enrollGrantFrame(grant) {
3445
3472
  * once and the inviter once, and a turn and `rev` an ordinary record can have. Null for anything else.
3446
3473
  */
3447
3474
  function readEnrollGrant(frame, inviterKey, joinerKey) {
3448
- if (frame.t !== "enroll-grant" || typeof frame.d !== "string" || !KEY$5.test(frame.d) || !Array.isArray(frame.set) || frame.set.length > 4) return null;
3475
+ if (frame.t !== "enroll-grant" || typeof frame.d !== "string" || !KEY$3.test(frame.d) || !Array.isArray(frame.set) || frame.set.length > 4) return null;
3449
3476
  if (!Number.isInteger(frame.turn) || frame.turn < 0 || frame.turn > TURN_MAX) return null;
3450
3477
  if (!Number.isInteger(frame.rev) || frame.rev < 0 || frame.rev >= TURN_REV_LIMIT) return null;
3451
3478
  const set = [];
@@ -3454,7 +3481,7 @@ function readEnrollGrant(frame, inviterKey, joinerKey) {
3454
3481
  set.push(null);
3455
3482
  continue;
3456
3483
  }
3457
- if (!Array.isArray(slot) || slot.length !== 2 || typeof slot[0] !== "string" || !KEY$5.test(slot[0]) || typeof slot[1] !== "string" || slot[1].length > 64) return null;
3484
+ if (!Array.isArray(slot) || slot.length !== 2 || typeof slot[0] !== "string" || !KEY$3.test(slot[0]) || typeof slot[1] !== "string" || slot[1].length > 64) return null;
3458
3485
  set.push({
3459
3486
  key: fromBase64Url(slot[0]),
3460
3487
  name: turnName(slot[1])
@@ -3491,7 +3518,7 @@ function enrollCancelReason(frame) {
3491
3518
  //#region ../core/src/setUpdate.ts
3492
3519
  var SET_UPDATE = "set-update";
3493
3520
  var SET_ACK = "set-ack";
3494
- var KEY$4 = /^[A-Za-z0-9_-]{43}$/;
3521
+ var KEY$2 = /^[A-Za-z0-9_-]{43}$/;
3495
3522
  var SIGNATURE$1 = /^[A-Za-z0-9_-]{86}$/;
3496
3523
  /** A tombstone packet is a relay payload of 706 bytes (WISP 06 § Record); a little room above it, nothing more. */
3497
3524
  var TOMB = /^[A-Za-z0-9_-]{100,1400}$/;
@@ -3543,7 +3570,7 @@ async function setUpdateFrame(oldAddress, update, signer) {
3543
3570
  * here: that needs the old turn address (`verifySetUpdate`).
3544
3571
  */
3545
3572
  function readSetUpdate(frame) {
3546
- if (frame.t !== "set-update" || typeof frame.d !== "string" || !KEY$4.test(frame.d) || typeof frame.by !== "string" || !KEY$4.test(frame.by)) return null;
3573
+ if (frame.t !== "set-update" || typeof frame.d !== "string" || !KEY$2.test(frame.d) || typeof frame.by !== "string" || !KEY$2.test(frame.by)) return null;
3547
3574
  if (typeof frame.s !== "string" || !SIGNATURE$1.test(frame.s) || typeof frame.tomb !== "string" || !TOMB.test(frame.tomb)) return null;
3548
3575
  if (!Array.isArray(frame.set) || frame.set.length > 4) return null;
3549
3576
  if (frame.rec !== void 0 && (typeof frame.rec !== "string" || !TOMB.test(frame.rec))) return null;
@@ -3557,7 +3584,7 @@ function readSetUpdate(frame) {
3557
3584
  signed.push(null);
3558
3585
  continue;
3559
3586
  }
3560
- if (!Array.isArray(slot) || slot.length !== 2 || typeof slot[0] !== "string" || !KEY$4.test(slot[0]) || typeof slot[1] !== "string" || slot[1].length > 64) return null;
3587
+ if (!Array.isArray(slot) || slot.length !== 2 || typeof slot[0] !== "string" || !KEY$2.test(slot[0]) || typeof slot[1] !== "string" || slot[1].length > 64) return null;
3561
3588
  if (utf8Encode(slot[1]).length > 16 || slot[1].includes("\0")) return null;
3562
3589
  signed.push([slot[0], slot[1]]);
3563
3590
  set.push({
@@ -3662,8 +3689,8 @@ var HANDOFF_CANCEL_REASONS = [
3662
3689
  "password",
3663
3690
  "stalled"
3664
3691
  ];
3665
- var KEY$3 = /^[A-Za-z0-9_-]{43}$/;
3666
- var DIGEST$2 = KEY$3;
3692
+ var KEY$1 = /^[A-Za-z0-9_-]{43}$/;
3693
+ var DIGEST$2 = KEY$1;
3667
3694
  var SIGNATURE = /^[A-Za-z0-9_-]{86}$/;
3668
3695
  var ID$5 = /^[A-Za-z0-9_-]{22}$/;
3669
3696
  var B64$4 = /^[A-Za-z0-9_-]*$/;
@@ -3798,7 +3825,7 @@ function handoffHelloFrame(hello) {
3798
3825
  }
3799
3826
  function readHandoffHello(frame) {
3800
3827
  if (frame.t !== "handoff-hello" || frame.v !== 1) return null;
3801
- if (!isText$2(frame.e, KEY$3) || fromBase64Url(frame.e).length !== 32) return null;
3828
+ if (!isText$2(frame.e, KEY$1) || fromBase64Url(frame.e).length !== 32) return null;
3802
3829
  if (!isText$2(frame.app, VERSION_TEXT) || !isCount$1(frame.db, 1e6) || !isPins(frame.pins)) return null;
3803
3830
  if (typeof frame.room !== "number" || !Number.isSafeInteger(frame.room) || frame.room < -1) return null;
3804
3831
  if (typeof frame.metered !== "boolean") return null;
@@ -3864,7 +3891,7 @@ function readHandoffPake(frame) {
3864
3891
  wrong: true
3865
3892
  } : null;
3866
3893
  if (!isText$2(frame.m, B64$4) || !frame.m.length || frame.m.length > 4096) return null;
3867
- if (frame.n === 3 && !isText$2(frame.c, KEY$3)) return null;
3894
+ if (frame.n === 3 && !isText$2(frame.c, KEY$1)) return null;
3868
3895
  return {
3869
3896
  n: frame.n,
3870
3897
  m: frame.m,
@@ -4656,8 +4683,12 @@ function buildSdpFromSignal(signal) {
4656
4683
  function sdpHasCandidates(sdp) {
4657
4684
  return /^a=candidate:/m.test(sdp ?? "");
4658
4685
  }
4659
- /** How long to keep collecting after the candidate we were waiting for showed up (a call's; a chat's data link sets its own). */
4660
- var ICE_SETTLE_MS = 400;
4686
+ /**
4687
+ * How long to keep collecting after the candidate we were waiting for showed up (a call's; a chat's data link sets its
4688
+ * own, the same). The host candidates are in before it, and a call's signal keeps the first reflexive one only
4689
+ * (`pickCallCandidates`): 400 ms held every call's offer, and its answer, back that long for nothing it carried.
4690
+ */
4691
+ var ICE_SETTLE_MS = 150;
4661
4692
  /** IPv4 ranges that are not reachable from the internet: this network, private, shared (CGNAT), loopback, link-local, the documentation ones, benchmarking, multicast and up. */
4662
4693
  var NOT_GLOBAL_IPV4 = [
4663
4694
  [0, 8],
@@ -7160,7 +7191,11 @@ function edgeParams(groupId, mySeed, myZ32, peerZ32) {
7160
7191
  var GROUP_MEMBER_CAP = { max: 32 };
7161
7192
  var MAX_GROUP_CHAIN = 1024;
7162
7193
  var GROUP_ID = /^[A-Za-z0-9_-]{22}$/;
7163
- var MEMBER_KEY = /^[ybndrfg8ejkmcpqxot1uwisza345h769]{52}$/;
7194
+ /**
7195
+ * A member's key, in its one canonical spelling (`isZ32Key`): a roster names members by string, so a key respelled in
7196
+ * its last character would be a second member for one key, and `publicKeyFromZ32` would throw on it past this check.
7197
+ */
7198
+ var MEMBER_KEY = { test: isZ32Key };
7164
7199
  var TAG$1 = /^[A-Za-z0-9_-]{43}$/;
7165
7200
  var SIG$6 = /^[A-Za-z0-9_-]{86}$/;
7166
7201
  var HASH$2 = /^[a-f0-9]{64}$/;
@@ -7226,6 +7261,8 @@ function parseCommit$1(raw) {
7226
7261
  sig: c.sig
7227
7262
  };
7228
7263
  }
7264
+ /** Only shape, no signature or parent: a commit as kept, its fields alone (anything else it carried dropped); null when malformed. */
7265
+ var parseGroupCommit = parseCommit$1;
7229
7266
  /** The roster a commit must carry, given the previous one; null when the change is not allowed. */
7230
7267
  function expectedRoster(previous, kind, by, subject) {
7231
7268
  if (kind === "create") return previous === null && !subject ? [[by, "admin"]] : null;
@@ -8854,6 +8891,147 @@ var RateWindow = class {
8854
8891
  }
8855
8892
  };
8856
8893
  //#endregion
8894
+ //#region ../core/src/image.ts
8895
+ var IMAGE_LIMITS = {
8896
+ /** Pixels on either side: the most a JPEG can say, well past what anyone sends as a picture. */
8897
+ maxEdge: 65535,
8898
+ /** How much of a file is read for its size: past a JPEG's EXIF, thumbnails and colour profile. */
8899
+ headBytes: 262144,
8900
+ /**
8901
+ * The most pixels a picture drawn in a chat may have: a 50-megapixel phone photo (8160 × 6120), about 200 MB once
8902
+ * decoded. A few kilobytes can say far more (a PNG of 23000 × 23000 is 64 KB and 2 GB decoded): past this a picture
8903
+ * is kept and saved, not drawn.
8904
+ */
8905
+ previewPixels: 5e7,
8906
+ /** The longest side a picture drawn in a chat may have: a wide panorama. */
8907
+ previewEdge: 16384
8908
+ };
8909
+ /** Whether a file is a picture, by its type. */
8910
+ function isImageMime(mime) {
8911
+ return mime.split(";")[0].trim().toLowerCase().startsWith("image/");
8912
+ }
8913
+ var edge = (n) => typeof n === "number" && Number.isSafeInteger(n) && n > 0 && n <= IMAGE_LIMITS.maxEdge;
8914
+ /** A peer's `image` description, or undefined when it is not one. It never refuses the file. */
8915
+ function parseImageMeta(value, mime) {
8916
+ if (mime !== void 0 && !isImageMime(mime)) return void 0;
8917
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
8918
+ const { width, height } = value;
8919
+ return edge(width) && edge(height) ? {
8920
+ width,
8921
+ height
8922
+ } : void 0;
8923
+ }
8924
+ var u16be = (b, at) => b[at] << 8 | b[at + 1];
8925
+ var u16le = (b, at) => b[at] | b[at + 1] << 8;
8926
+ var u24le = (b, at) => b[at] | b[at + 1] << 8 | b[at + 2] << 16;
8927
+ var u32be = (b, at) => (b[at] << 24 >>> 0) + (b[at + 1] << 16) + (b[at + 2] << 8) + b[at + 3];
8928
+ var ascii = (b, at, text) => [...text].every((c, i) => b[at + i] === c.charCodeAt(0));
8929
+ var sized = (width, height) => edge(width) && edge(height) ? {
8930
+ width,
8931
+ height
8932
+ } : void 0;
8933
+ /** SOFn markers: every frame type but DHT (C4), JPG (C8) and DAC (CC). */
8934
+ var JPEG_FRAME = /* @__PURE__ */ new Set([
8935
+ 192,
8936
+ 193,
8937
+ 194,
8938
+ 195,
8939
+ 197,
8940
+ 198,
8941
+ 199,
8942
+ 201,
8943
+ 202,
8944
+ 203,
8945
+ 205,
8946
+ 206,
8947
+ 207
8948
+ ]);
8949
+ /** The EXIF orientation in an APP1 segment's TIFF block (1 when it says none). */
8950
+ function exifOrientation(b, tiff, end) {
8951
+ if (tiff + 8 > end) return 1;
8952
+ const little = ascii(b, tiff, "II");
8953
+ if (!little && !ascii(b, tiff, "MM")) return 1;
8954
+ const u16 = (at) => little ? u16le(b, at) : u16be(b, at);
8955
+ const u32 = (at) => little ? u16le(b, at) + u16le(b, at + 2) * 65536 : u32be(b, at);
8956
+ if (u16(tiff + 2) !== 42) return 1;
8957
+ const ifd = tiff + u32(tiff + 4);
8958
+ if (ifd + 2 > end) return 1;
8959
+ const count = u16(ifd);
8960
+ for (let i = 0; i < count; i++) {
8961
+ const entry = ifd + 2 + i * 12;
8962
+ if (entry + 12 > end) return 1;
8963
+ if (u16(entry) === 274) {
8964
+ const value = u16(entry + 8);
8965
+ return value >= 1 && value <= 8 ? value : 1;
8966
+ }
8967
+ }
8968
+ return 1;
8969
+ }
8970
+ function jpegSize(b) {
8971
+ let at = 2, orientation = 1;
8972
+ while (at + 4 <= b.length) {
8973
+ if (b[at] !== 255) return void 0;
8974
+ const marker = b[at + 1];
8975
+ if (marker === 255) {
8976
+ at++;
8977
+ continue;
8978
+ }
8979
+ if (marker === 1 || marker >= 208 && marker <= 215) {
8980
+ at += 2;
8981
+ continue;
8982
+ }
8983
+ if (marker === 217 || marker === 218) return void 0;
8984
+ const length = u16be(b, at + 2);
8985
+ if (length < 2) return void 0;
8986
+ const end = Math.min(b.length, at + 2 + length);
8987
+ if (marker === 225 && ascii(b, at + 4, "Exif\0\0")) orientation = exifOrientation(b, at + 10, end);
8988
+ if (JPEG_FRAME.has(marker)) {
8989
+ if (at + 9 > b.length) return void 0;
8990
+ const height = u16be(b, at + 5), width = u16be(b, at + 7);
8991
+ return orientation >= 5 ? sized(height, width) : sized(width, height);
8992
+ }
8993
+ at += 2 + length;
8994
+ }
8995
+ }
8996
+ function webpSize(b) {
8997
+ if (b.length < 30) return void 0;
8998
+ if (ascii(b, 12, "VP8 ")) {
8999
+ if (b[23] !== 157 || b[24] !== 1 || b[25] !== 42) return void 0;
9000
+ return sized(u16le(b, 26) & 16383, u16le(b, 28) & 16383);
9001
+ }
9002
+ if (ascii(b, 12, "VP8L")) {
9003
+ if (b[20] !== 47) return void 0;
9004
+ const bits = b[21] | b[22] << 8 | b[23] << 16 | b[24] << 24;
9005
+ return sized((bits & 16383) + 1, (bits >>> 14 & 16383) + 1);
9006
+ }
9007
+ if (ascii(b, 12, "VP8X")) return sized(u24le(b, 24) + 1, u24le(b, 27) + 1);
9008
+ }
9009
+ /**
9010
+ * A picture's size as it is shown, read from its first bytes (PNG, JPEG, GIF, WebP), or undefined when they do not
9011
+ * say. Only the header is read: a JPEG whose frame header sits past `IMAGE_LIMITS.headBytes` goes without.
9012
+ */
9013
+ function imageSize(head) {
9014
+ const b = head;
9015
+ try {
9016
+ if (b.length >= 24 && b[0] === 137 && ascii(b, 1, "PNG\r\n\n") && ascii(b, 12, "IHDR")) return sized(u32be(b, 16), u32be(b, 20));
9017
+ if (b.length >= 10 && (ascii(b, 0, "GIF87a") || ascii(b, 0, "GIF89a"))) return sized(u16le(b, 6), u16le(b, 8));
9018
+ if (b.length >= 4 && b[0] === 255 && b[1] === 216 && b[2] === 255) return jpegSize(b);
9019
+ if (b.length >= 16 && ascii(b, 0, "RIFF") && ascii(b, 8, "WEBP")) return webpSize(b);
9020
+ } catch {}
9021
+ }
9022
+ /**
9023
+ * The description a picture is sent with, read from the start of the file, or undefined when it is not a picture
9024
+ * or its first bytes do not say (it then goes without one).
9025
+ */
9026
+ async function readImageMeta(source, mime) {
9027
+ if (!isImageMime(mime)) return void 0;
9028
+ try {
9029
+ return imageSize(new Uint8Array(await source.slice(0, IMAGE_LIMITS.headBytes).arrayBuffer()));
9030
+ } catch {
9031
+ return;
9032
+ }
9033
+ }
9034
+ //#endregion
8857
9035
  //#region ../core/src/video.ts
8858
9036
  var VIDEO_LIMITS = {
8859
9037
  /** The poster's bytes: with the rest of an offer, a frame stays far below the 60 KiB a session carries. */
@@ -8877,7 +9055,8 @@ var JPEG_START = [
8877
9055
  ];
8878
9056
  /**
8879
9057
  * A peer's `video` description, or undefined when it is not one. It never refuses the file. A poster that is
8880
- * too large or not a JPEG is dropped on its own; the length and size are kept.
9058
+ * too large, not a JPEG, or says it is more pixels than a sender draws (`posterEdge`) is dropped on its own; the
9059
+ * length and size are kept. It is shown before the file is accepted, so a few bytes must not ask for a huge bitmap.
8881
9060
  */
8882
9061
  function parseVideoMeta(value, mime) {
8883
9062
  if (mime !== void 0 && !isVideoMime(mime)) return void 0;
@@ -8893,7 +9072,8 @@ function parseVideoMeta(value, mime) {
8893
9072
  };
8894
9073
  if (typeof poster === "string" && poster.length <= Math.ceil(VIDEO_LIMITS.maxPosterBytes * 4 / 3) && BASE64URL.test(poster)) try {
8895
9074
  const bytes = fromBase64Url(poster);
8896
- if (bytes.length <= VIDEO_LIMITS.maxPosterBytes && JPEG_START.every((b, i) => bytes[i] === b)) meta.poster = poster;
9075
+ const size = bytes.length <= VIDEO_LIMITS.maxPosterBytes && JPEG_START.every((b, i) => bytes[i] === b) ? imageSize(bytes) : void 0;
9076
+ if (size && Math.max(size.width, size.height) <= VIDEO_LIMITS.posterEdge) meta.poster = poster;
8897
9077
  } catch {}
8898
9078
  return meta;
8899
9079
  }
@@ -9198,139 +9378,6 @@ var FileTransfers = class {
9198
9378
  }
9199
9379
  };
9200
9380
  //#endregion
9201
- //#region ../core/src/image.ts
9202
- var IMAGE_LIMITS = {
9203
- /** Pixels on either side: the most a JPEG can say, well past what anyone sends as a picture. */
9204
- maxEdge: 65535,
9205
- /** How much of a file is read for its size: past a JPEG's EXIF, thumbnails and colour profile. */
9206
- headBytes: 262144
9207
- };
9208
- /** Whether a file is a picture, by its type. */
9209
- function isImageMime(mime) {
9210
- return mime.split(";")[0].trim().toLowerCase().startsWith("image/");
9211
- }
9212
- var edge = (n) => typeof n === "number" && Number.isSafeInteger(n) && n > 0 && n <= IMAGE_LIMITS.maxEdge;
9213
- /** A peer's `image` description, or undefined when it is not one. It never refuses the file. */
9214
- function parseImageMeta(value, mime) {
9215
- if (mime !== void 0 && !isImageMime(mime)) return void 0;
9216
- if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
9217
- const { width, height } = value;
9218
- return edge(width) && edge(height) ? {
9219
- width,
9220
- height
9221
- } : void 0;
9222
- }
9223
- var u16be = (b, at) => b[at] << 8 | b[at + 1];
9224
- var u16le = (b, at) => b[at] | b[at + 1] << 8;
9225
- var u24le = (b, at) => b[at] | b[at + 1] << 8 | b[at + 2] << 16;
9226
- var u32be = (b, at) => (b[at] << 24 >>> 0) + (b[at + 1] << 16) + (b[at + 2] << 8) + b[at + 3];
9227
- var ascii = (b, at, text) => [...text].every((c, i) => b[at + i] === c.charCodeAt(0));
9228
- var sized = (width, height) => edge(width) && edge(height) ? {
9229
- width,
9230
- height
9231
- } : void 0;
9232
- /** SOFn markers: every frame type but DHT (C4), JPG (C8) and DAC (CC). */
9233
- var JPEG_FRAME = /* @__PURE__ */ new Set([
9234
- 192,
9235
- 193,
9236
- 194,
9237
- 195,
9238
- 197,
9239
- 198,
9240
- 199,
9241
- 201,
9242
- 202,
9243
- 203,
9244
- 205,
9245
- 206,
9246
- 207
9247
- ]);
9248
- /** The EXIF orientation in an APP1 segment's TIFF block (1 when it says none). */
9249
- function exifOrientation(b, tiff, end) {
9250
- if (tiff + 8 > end) return 1;
9251
- const little = ascii(b, tiff, "II");
9252
- if (!little && !ascii(b, tiff, "MM")) return 1;
9253
- const u16 = (at) => little ? u16le(b, at) : u16be(b, at);
9254
- const u32 = (at) => little ? u16le(b, at) + u16le(b, at + 2) * 65536 : u32be(b, at);
9255
- if (u16(tiff + 2) !== 42) return 1;
9256
- const ifd = tiff + u32(tiff + 4);
9257
- if (ifd + 2 > end) return 1;
9258
- const count = u16(ifd);
9259
- for (let i = 0; i < count; i++) {
9260
- const entry = ifd + 2 + i * 12;
9261
- if (entry + 12 > end) return 1;
9262
- if (u16(entry) === 274) {
9263
- const value = u16(entry + 8);
9264
- return value >= 1 && value <= 8 ? value : 1;
9265
- }
9266
- }
9267
- return 1;
9268
- }
9269
- function jpegSize(b) {
9270
- let at = 2, orientation = 1;
9271
- while (at + 4 <= b.length) {
9272
- if (b[at] !== 255) return void 0;
9273
- const marker = b[at + 1];
9274
- if (marker === 255) {
9275
- at++;
9276
- continue;
9277
- }
9278
- if (marker === 1 || marker >= 208 && marker <= 215) {
9279
- at += 2;
9280
- continue;
9281
- }
9282
- if (marker === 217 || marker === 218) return void 0;
9283
- const length = u16be(b, at + 2);
9284
- if (length < 2) return void 0;
9285
- const end = Math.min(b.length, at + 2 + length);
9286
- if (marker === 225 && ascii(b, at + 4, "Exif\0\0")) orientation = exifOrientation(b, at + 10, end);
9287
- if (JPEG_FRAME.has(marker)) {
9288
- if (at + 9 > b.length) return void 0;
9289
- const height = u16be(b, at + 5), width = u16be(b, at + 7);
9290
- return orientation >= 5 ? sized(height, width) : sized(width, height);
9291
- }
9292
- at += 2 + length;
9293
- }
9294
- }
9295
- function webpSize(b) {
9296
- if (b.length < 30) return void 0;
9297
- if (ascii(b, 12, "VP8 ")) {
9298
- if (b[23] !== 157 || b[24] !== 1 || b[25] !== 42) return void 0;
9299
- return sized(u16le(b, 26) & 16383, u16le(b, 28) & 16383);
9300
- }
9301
- if (ascii(b, 12, "VP8L")) {
9302
- if (b[20] !== 47) return void 0;
9303
- const bits = b[21] | b[22] << 8 | b[23] << 16 | b[24] << 24;
9304
- return sized((bits & 16383) + 1, (bits >>> 14 & 16383) + 1);
9305
- }
9306
- if (ascii(b, 12, "VP8X")) return sized(u24le(b, 24) + 1, u24le(b, 27) + 1);
9307
- }
9308
- /**
9309
- * A picture's size as it is shown, read from its first bytes (PNG, JPEG, GIF, WebP), or undefined when they do not
9310
- * say. Only the header is read: a JPEG whose frame header sits past `IMAGE_LIMITS.headBytes` goes without.
9311
- */
9312
- function imageSize(head) {
9313
- const b = head;
9314
- try {
9315
- if (b.length >= 24 && b[0] === 137 && ascii(b, 1, "PNG\r\n\n") && ascii(b, 12, "IHDR")) return sized(u32be(b, 16), u32be(b, 20));
9316
- if (b.length >= 10 && (ascii(b, 0, "GIF87a") || ascii(b, 0, "GIF89a"))) return sized(u16le(b, 6), u16le(b, 8));
9317
- if (b.length >= 4 && b[0] === 255 && b[1] === 216 && b[2] === 255) return jpegSize(b);
9318
- if (b.length >= 16 && ascii(b, 0, "RIFF") && ascii(b, 8, "WEBP")) return webpSize(b);
9319
- } catch {}
9320
- }
9321
- /**
9322
- * The description a picture is sent with, read from the start of the file, or undefined when it is not a picture
9323
- * or its first bytes do not say (it then goes without one).
9324
- */
9325
- async function readImageMeta(source, mime) {
9326
- if (!isImageMime(mime)) return void 0;
9327
- try {
9328
- return imageSize(new Uint8Array(await source.slice(0, IMAGE_LIMITS.headBytes).arrayBuffer()));
9329
- } catch {
9330
- return;
9331
- }
9332
- }
9333
- //#endregion
9334
9381
  //#region ../core/src/dhtFiles.ts
9335
9382
  /**
9336
9383
  * A file's bubble on the DHT floor (WISP 403 § Files): while the contact is away, a file's offer (its name, size, type,
@@ -11807,10 +11854,10 @@ var IDENTITY_MAX_VALIDITY = 3456e4;
11807
11854
  var IDENTITY_MAX_EVIDENCE = 16384;
11808
11855
  /** Unanswered challenges a verifier keeps per conversation. */
11809
11856
  var MAX_CHALLENGES = 8;
11857
+ var IDENTITY_FRAME_WINDOW_MS = 6e4;
11810
11858
  var IDENTITY_PROVIDER_ID = /^[a-z][a-z0-9-]{0,31}$/;
11811
11859
  /** Printable ASCII without spaces, so the statement can be built but never needs parsing. */
11812
11860
  var IDENTITY_SUBJECT = /^[\x21-\x7e]{1,512}$/;
11813
- var KEY$2 = /^[ybndrfg8ejkmcpqxot1uwisza345h769]{52}$/;
11814
11861
  var BINDING_NONCE = /^[A-Za-z0-9_-]{22}$/;
11815
11862
  var CHALLENGE_NONCE = /^[A-Za-z0-9_-]{43}$/;
11816
11863
  var HEX64$2 = /^[a-f0-9]{64}$/;
@@ -11832,7 +11879,7 @@ var iso = (seconds) => (/* @__PURE__ */ new Date(seconds * 1e3)).toISOString().r
11832
11879
  var isInt$2 = (v) => Number.isSafeInteger(v) && v > 0;
11833
11880
  function isIdentityBinding(value) {
11834
11881
  const b = value;
11835
- return !!b && typeof b === "object" && !Array.isArray(b) && Object.keys(b).sort().join(",") === "expiresAt,issuedAt,key,nonce,provider,subject,v" && b.v === 1 && typeof b.provider === "string" && IDENTITY_PROVIDER_ID.test(b.provider) && typeof b.subject === "string" && IDENTITY_SUBJECT.test(b.subject) && typeof b.key === "string" && KEY$2.test(b.key) && typeof b.nonce === "string" && BINDING_NONCE.test(b.nonce) && isInt$2(b.issuedAt) && isInt$2(b.expiresAt) && b.expiresAt > b.issuedAt && b.expiresAt - b.issuedAt <= 3456e4 && b.expiresAt < 253402300800;
11882
+ return !!b && typeof b === "object" && !Array.isArray(b) && Object.keys(b).sort().join(",") === "expiresAt,issuedAt,key,nonce,provider,subject,v" && b.v === 1 && typeof b.provider === "string" && IDENTITY_PROVIDER_ID.test(b.provider) && typeof b.subject === "string" && IDENTITY_SUBJECT.test(b.subject) && isZ32Key(b.key) && typeof b.nonce === "string" && BINDING_NONCE.test(b.nonce) && isInt$2(b.issuedAt) && isInt$2(b.expiresAt) && b.expiresAt > b.issuedAt && b.expiresAt - b.issuedAt <= 3456e4 && b.expiresAt < 253402300800;
11836
11883
  }
11837
11884
  /**
11838
11885
  * The canonical statement, one line, so it survives every tool and clipboard (no line-ending
@@ -11892,7 +11939,7 @@ function signIdentityPresentation(seed, proofId, scope, nonce, issuedAt) {
11892
11939
  return toBase64Url(sign(identityPresentationBytes(proofId, scope, nonce, issuedAt), seed));
11893
11940
  }
11894
11941
  function verifyIdentityPresentation(key, sig, proofId, scope, nonce, issuedAt) {
11895
- if (typeof sig !== "string" || !SIG$5.test(sig) || !KEY$2.test(key)) return false;
11942
+ if (typeof sig !== "string" || !SIG$5.test(sig) || !isZ32Key(key)) return false;
11896
11943
  return verify(fromBase64Url(sig), identityPresentationBytes(proofId, scope, nonce, issuedAt), publicKeyFromZ32(key));
11897
11944
  }
11898
11945
  var clean$1 = (value, max) => {
@@ -11945,6 +11992,18 @@ function receivedIdentityStatus(r, presenter, audience, now = nowSeconds()) {
11945
11992
  if (Math.min(r.binding.expiresAt, r.verified.expiresAt ?? Infinity) <= now) return "expired";
11946
11993
  return r.status === "unconfirmed" ? "unconfirmed" : "verified";
11947
11994
  }
11995
+ var standing = (r, now) => r.status !== "withdrawn" && r.status !== "revoked" && Math.min(r.binding.expiresAt, r.verified.expiresAt ?? Infinity) > now;
11996
+ /**
11997
+ * `received` with room for one more proof: `replaced` (the older proof of the same identity) dropped, then the oldest ones
11998
+ * no longer standing; undefined when every place is still taken.
11999
+ */
12000
+ function receivedWithRoom(received, id, replaced, now) {
12001
+ const kept = received.filter((r) => r.id !== id && !replaced(r));
12002
+ const gone = kept.filter((r) => !standing(r, now)).sort((a, b) => a.verifiedAt - b.verifiedAt);
12003
+ const drop = new Set(gone.slice(0, Math.max(0, kept.length - 16 + 1)));
12004
+ const out = kept.filter((r) => !drop.has(r));
12005
+ return out.length < 16 ? out : void 0;
12006
+ }
11948
12007
  var shortError = (e) => clean$1(e instanceof Error ? e.message : String(e), 160) ?? "Invalid proof";
11949
12008
  /**
11950
12009
  * A check this side could not finish because of what its own network did: a host it refused to contact, a server
@@ -11960,6 +12019,7 @@ var IdentityCheckUnavailable = class extends Error {
11960
12019
  };
11961
12020
  /** What the contact is told when the check failed with an IdentityCheckUnavailable. */
11962
12021
  var IDENTITY_CHECK_UNAVAILABLE = "Your contact's app could not check it. Try again later.";
12022
+ var TOO_MANY_RECEIVED = "Your contact keeps too many identities from you already";
11963
12023
  var peerError = (e) => e instanceof IdentityCheckUnavailable ? IDENTITY_CHECK_UNAVAILABLE : shortError(e);
11964
12024
  /**
11965
12025
  * The `idp-*` exchange of one conversation. Frames (all JSON, additive, over the paired channel,
@@ -12184,31 +12244,29 @@ var IdentityExchange = class {
12184
12244
  });
12185
12245
  return;
12186
12246
  }
12187
- case "idp-withdraw":
12247
+ case "idp-withdraw": {
12188
12248
  if (!id) return;
12189
- await this.options.storage.update((l) => {
12190
- const r = l.received.find((x) => x.id === id);
12191
- const next = {
12192
- ...l,
12193
- received: l.received.map((x) => x.id === id ? {
12194
- ...x,
12195
- status: "withdrawn"
12196
- } : x)
12197
- };
12198
- return r && r.status !== "withdrawn" ? this.log(next, {
12199
- type: "stopped",
12200
- side: "theirs",
12201
- proof: id,
12202
- provider: r.binding.provider,
12203
- subject: r.verified.subject,
12204
- reason: "withdrawn"
12205
- }) : next;
12206
- });
12249
+ const r = this.options.storage.read().received.find((x) => x.id === id);
12250
+ if (r && r.status !== "withdrawn") await this.options.storage.update((l) => this.log({
12251
+ ...l,
12252
+ received: l.received.map((x) => x.id === id ? {
12253
+ ...x,
12254
+ status: "withdrawn"
12255
+ } : x)
12256
+ }, {
12257
+ type: "stopped",
12258
+ side: "theirs",
12259
+ proof: id,
12260
+ provider: r.binding.provider,
12261
+ subject: r.verified.subject,
12262
+ reason: "withdrawn"
12263
+ }));
12207
12264
  this.send({
12208
12265
  t: "idp-withdrawn",
12209
12266
  id
12210
12267
  });
12211
12268
  return;
12269
+ }
12212
12270
  case "idp-withdrawn": {
12213
12271
  const s = id ? this.shared(id) : void 0;
12214
12272
  if (s?.status === "withdrawal-pending") await this.setShared({
@@ -12226,25 +12284,11 @@ var IdentityExchange = class {
12226
12284
  async onRequest(id, provider) {
12227
12285
  if (!id) return;
12228
12286
  if (typeof provider !== "string" || !this.options.providers().includes(provider)) {
12229
- const error = "Your contact's app cannot verify this kind of identity yet";
12230
- if (typeof provider === "string" && IDENTITY_PROVIDER_ID.test(provider)) await this.options.storage.update((l) => this.log(this.log(l, {
12231
- type: "shared",
12232
- side: "theirs",
12233
- proof: id,
12234
- provider
12235
- }), {
12236
- type: "result",
12237
- side: "theirs",
12238
- proof: id,
12239
- provider,
12240
- ok: false,
12241
- error: "This app cannot verify this kind of identity yet"
12242
- }));
12243
12287
  this.send({
12244
12288
  t: "idp-result",
12245
12289
  id,
12246
12290
  ok: false,
12247
- error
12291
+ error: "Your contact's app cannot verify this kind of identity yet"
12248
12292
  });
12249
12293
  return;
12250
12294
  }
@@ -12330,6 +12374,8 @@ var IdentityExchange = class {
12330
12374
  /** The same, as this side's own timeline says it. */
12331
12375
  let reason;
12332
12376
  try {
12377
+ const mayReplace = (r) => r.binding.provider === binding.provider && (r.binding.subject === binding.subject || r.verified.subject === binding.subject);
12378
+ if (!receivedWithRoom(this.options.storage.read().received, id, mayReplace, now)) throw new Error(TOO_MANY_RECEIVED);
12333
12379
  if (JSON.stringify(evidence ?? null).length > 16384) throw new Error("Evidence too large");
12334
12380
  if (!verifyIdentityPresentation(binding.key, sig, id, scope, nonce, issuedAt)) throw new Error("The proof key did not sign this conversation's challenge");
12335
12381
  if (binding.issuedAt > now + 300) throw new Error("The proof is dated in the future");
@@ -12345,6 +12391,12 @@ var IdentityExchange = class {
12345
12391
  const challenge = challengeOf(l);
12346
12392
  if (!challenge) throw new Error("Unknown or reused identity challenge");
12347
12393
  const challenges = l.challenges.filter((c) => c.nonce !== nonce);
12394
+ const subject = verified?.subject;
12395
+ const received = verified && receivedWithRoom(l.received, id, (r) => r.binding.provider === binding.provider && r.verified.subject === subject, now);
12396
+ if (verified && !received) {
12397
+ verified = void 0;
12398
+ error = reason = TOO_MANY_RECEIVED;
12399
+ }
12348
12400
  const logged = this.log(l, {
12349
12401
  type: "result",
12350
12402
  side: "theirs",
@@ -12355,7 +12407,7 @@ var IdentityExchange = class {
12355
12407
  ok: !!verified,
12356
12408
  ...reason ? { error: reason } : {}
12357
12409
  });
12358
- if (!verified) return {
12410
+ if (!verified || !received) return {
12359
12411
  ...logged,
12360
12412
  challenges
12361
12413
  };
@@ -12371,7 +12423,6 @@ var IdentityExchange = class {
12371
12423
  checkedAt: now,
12372
12424
  status: "verified"
12373
12425
  };
12374
- const received = l.received.filter((r) => r.id !== id && !(r.binding.provider === binding.provider && r.verified.subject === verified.subject));
12375
12426
  return {
12376
12427
  ...logged,
12377
12428
  challenges,
@@ -12461,8 +12512,7 @@ var CapsRefusedError = class extends Error {
12461
12512
  var IDENT = /^[a-z0-9][a-z0-9._-]*\/[0-9]+$/;
12462
12513
  var HEX64$1 = /^[0-9a-f]{64}$/;
12463
12514
  var SIG$4 = /^[A-Za-z0-9_-]{86}$/;
12464
- var Z32$2 = /^[ybndrfg8ejkmcpqxot1uwisza345h769]{52}$/;
12465
- var KEY$1 = /^[A-Za-z0-9_-]{43}$/;
12515
+ var KEY = /^[A-Za-z0-9_-]{43}$/;
12466
12516
  var hexToKey = (hex) => toBase64Url(Uint8Array.from(hex.match(/../g), (b) => parseInt(b, 16)));
12467
12517
  var keyToHex = (key) => Array.from(fromBase64Url(key), (b) => b.toString(16).padStart(2, "0")).join("");
12468
12518
  /**
@@ -12514,14 +12564,14 @@ function parseDescriptors(value) {
12514
12564
  const out = {};
12515
12565
  const iroh = raw["iroh/1"], hyper = raw["hyperdht/1"];
12516
12566
  if (iroh !== void 0) {
12517
- if (!iroh || typeof iroh.id !== "string" || !KEY$1.test(iroh.id) || iroh.relay !== void 0 && (typeof iroh.relay !== "string" || !RELAY_URL.test(iroh.relay))) return null;
12567
+ if (!iroh || typeof iroh.id !== "string" || !KEY.test(iroh.id) || iroh.relay !== void 0 && (typeof iroh.relay !== "string" || !RELAY_URL.test(iroh.relay))) return null;
12518
12568
  out["iroh/1"] = {
12519
12569
  id: iroh.id,
12520
12570
  ...iroh.relay !== void 0 ? { relay: iroh.relay } : {}
12521
12571
  };
12522
12572
  }
12523
12573
  if (hyper !== void 0) {
12524
- if (!hyper || typeof hyper.publicKey !== "string" || !KEY$1.test(hyper.publicKey) || hyper.relay !== void 0 && (typeof hyper.relay !== "string" || !RELAY_URL.test(hyper.relay))) return null;
12574
+ if (!hyper || typeof hyper.publicKey !== "string" || !KEY.test(hyper.publicKey) || hyper.relay !== void 0 && (typeof hyper.relay !== "string" || !RELAY_URL.test(hyper.relay))) return null;
12525
12575
  out["hyperdht/1"] = {
12526
12576
  publicKey: hyper.publicKey,
12527
12577
  ...hyper.relay !== void 0 ? { relay: hyper.relay } : {}
@@ -12665,7 +12715,7 @@ var CapsKeys = class {
12665
12715
  if (body.length < 10 || body.length > 16) throw new CapsRefusedError("format", "Malformed capability record.");
12666
12716
  const [version, rev, issued, author, versions, transports, capabilities, extensions, rawDescriptors, name, choice] = body;
12667
12717
  const descriptors = parseDescriptors(rawDescriptors);
12668
- if (version !== 1 || !Number.isSafeInteger(rev) || rev < 0 || !Number.isSafeInteger(issued) || issued <= 0 || typeof author !== "string" || !Z32$2.test(author) || !list$2(versions, CAPS_LIMITS.versions, (v) => Number.isSafeInteger(v) && v > 0) || !list$2(transports, CAPS_LIMITS.transports, ident) || transports.includes("dht/1") || !list$2(capabilities, CAPS_LIMITS.capabilities, ident) || !list$2(extensions, CAPS_LIMITS.extensions, ident) || !descriptors || typeof name !== "string" || utf8Encode(name).length > CAPS_LIMITS.nameBytes || choice !== void 0 && (!ident(choice) || choice === "dht/1")) throw new CapsRefusedError("format", "Malformed capability record.");
12718
+ if (version !== 1 || !Number.isSafeInteger(rev) || rev < 0 || !Number.isSafeInteger(issued) || issued <= 0 || !isZ32Key(author) || !list$2(versions, CAPS_LIMITS.versions, (v) => Number.isSafeInteger(v) && v > 0) || !list$2(transports, CAPS_LIMITS.transports, ident) || transports.includes("dht/1") || !list$2(capabilities, CAPS_LIMITS.capabilities, ident) || !list$2(extensions, CAPS_LIMITS.extensions, ident) || !descriptors || typeof name !== "string" || utf8Encode(name).length > CAPS_LIMITS.nameBytes || choice !== void 0 && (!ident(choice) || choice === "dht/1")) throw new CapsRefusedError("format", "Malformed capability record.");
12669
12719
  const signer = options.pinned ?? options.expected;
12670
12720
  if (signer && author !== signer) throw new CapsRefusedError("author", "The capability record is signed by another participation key than this contact's.");
12671
12721
  try {
@@ -13611,13 +13661,12 @@ var SESSION_RECEIVE_PENDING = {
13611
13661
  frames: 1024,
13612
13662
  bytes: 64 * SESSION_FRAME_MAX
13613
13663
  };
13614
- var KEY = /^[ybndrfg8ejkmcpqxot1uwisza345h769]{52}$/;
13615
13664
  var NONCE$1 = /^[A-Za-z0-9_-]{43}$/;
13616
13665
  var SIG$2 = /^[A-Za-z0-9_-]{86}$/;
13617
13666
  var FINGERPRINT = /^[a-f0-9]{64}$/;
13618
13667
  var isList = (v, maximum = 8) => Array.isArray(v) && v.length > 0 && v.length <= maximum && v.every((x) => typeof x === "string" && /^[a-z0-9/-]{1,40}$/.test(x)) && new Set(v).size === v.length;
13619
13668
  function parseOffer(v) {
13620
- if (!Array.isArray(v.versions) || v.versions.length === 0 || v.versions.length > 8 || !v.versions.every((n) => Number.isSafeInteger(n) && n > 0) || new Set(v.versions).size !== v.versions.length || !isList(v.transports) || !isList(v.capabilities, 32) || typeof v.key !== "string" || !KEY.test(v.key) || typeof v.nonce !== "string" || !NONCE$1.test(v.nonce)) return null;
13669
+ if (!Array.isArray(v.versions) || v.versions.length === 0 || v.versions.length > 8 || !v.versions.every((n) => Number.isSafeInteger(n) && n > 0) || new Set(v.versions).size !== v.versions.length || !isList(v.transports) || !isList(v.capabilities, 32) || !isZ32Key(v.key) || typeof v.nonce !== "string" || !NONCE$1.test(v.nonce)) return null;
13621
13670
  const extensions = isList(v.extensions, 32) ? v.extensions : void 0;
13622
13671
  return {
13623
13672
  t: "pair-offer",
@@ -13748,7 +13797,7 @@ var PairedSession = class {
13748
13797
  }
13749
13798
  start() {
13750
13799
  const binding = this.options.binding;
13751
- if (!(binding ? (binding.transport === "hyperdht/1" ? /^[a-f0-9]{128}$/.test(binding.context) : FINGERPRINT.test(binding.context)) && binding.identities.length === 2 && binding.identities.every((k) => FINGERPRINT.test(k)) && binding.identities[0] !== binding.identities[1] : this.options.fingerprints?.length === 2 && this.options.fingerprints.every((f) => FINGERPRINT.test(f))) || !this.offer.transports.includes(this.transport) || this.options.rendezvousKeys.some((k) => !KEY.test(k))) return this.fail("Invalid connection binding or unavailable transport");
13800
+ if (!(binding ? (binding.transport === "hyperdht/1" ? /^[a-f0-9]{128}$/.test(binding.context) : FINGERPRINT.test(binding.context)) && binding.identities.length === 2 && binding.identities.every((k) => FINGERPRINT.test(k)) && binding.identities[0] !== binding.identities[1] : this.options.fingerprints?.length === 2 && this.options.fingerprints.every((f) => FINGERPRINT.test(f))) || !this.offer.transports.includes(this.transport) || this.options.rendezvousKeys.some((k) => !isZ32Key(k))) return this.fail("Invalid connection binding or unavailable transport");
13752
13801
  this.channel.onMessage = (data) => {
13753
13802
  if (this.stopped) return;
13754
13803
  const size = data.length;
@@ -16930,7 +16979,7 @@ var CarriedTransport = class {
16930
16979
  var GROUP_FILE_LIMITS = {
16931
16980
  /** The largest file a group takes. */
16932
16981
  maxBytes: 104857600,
16933
- /** A file this size or smaller is fetched without asking (voice messages always are, up to `VOICE_LIMITS`). */
16982
+ /** A file this size or smaller is fetched without asking, a voice message too (one recorded is well inside it). */
16934
16983
  autoBytes: 8388608,
16935
16984
  /** What a device keeps of a group's files it fetched without asking; past it, every file is asked about. */
16936
16985
  autoBytesPerGroup: 268435456,
@@ -16938,6 +16987,10 @@ var GROUP_FILE_LIMITS = {
16938
16987
  serveBytesPerDay: 1073741824,
16939
16988
  /** Transfers a holder serves at once, all groups together. */
16940
16989
  serveAtOnce: 3,
16990
+ /** Bytes a served transfer gets confirmed (or the rest of its file) for it to count as moving in `wantWaitMs`. */
16991
+ serveMoveBytes: 16384,
16992
+ /** How long a served transfer keeps its place while all are taken and another member asks: past it, the place is theirs. */
16993
+ servePlaceMs: 3e5,
16941
16994
  /** File announcements a member sends to one group in a minute. */
16942
16995
  announcePerMinute: 8,
16943
16996
  /** File announcements of one member of a group fetched by themselves in `announceWindowMs`; past it, kept for a Download. */
@@ -16964,11 +17017,21 @@ var GROUP_FILE_LIMITS = {
16964
17017
  * left for the next holder too.
16965
17018
  */
16966
17019
  wantWaitMs: 3e4,
17020
+ /**
17021
+ * How long a served transfer with all its bytes confirmed waits for the member's word that its check passed once the
17022
+ * member's next want has overtaken it.
17023
+ */
17024
+ doneWaitMs: 5e3,
16967
17025
  /** A file session closes this long after its last transfer ends. */
16968
17026
  sessionIdleMs: 6e4,
16969
17027
  /** File sessions opening at once, and open at once, all groups together. */
16970
17028
  sessionsOpening: 2,
16971
17029
  sessionsOpen: 4,
17030
+ /**
17031
+ * What a transfer must bring in `wantWaitMs` (or the rest of its file, when less) to count as moving: a holder that
17032
+ * sends a few bytes at a time is left as one that stalls.
17033
+ */
17034
+ wantMoveBytes: 16384,
16972
17035
  /** The longest file name and type kept from a description. */
16973
17036
  nameChars: 255,
16974
17037
  mimeChars: 255
@@ -17053,11 +17116,11 @@ function groupFileFallback(file) {
17053
17116
  return `${fileMessageText(file)} (${formatFileSize(file.size)}): update Ghostly to get files in groups`;
17054
17117
  }
17055
17118
  /**
17056
- * Whether a device fetches a file by itself (WISP 503 § Automatic and asked downloads): a voice message always, any
17057
- * other file up to `autoBytes` while what it took that way from the group stays within `autoBytesPerGroup`.
17119
+ * Whether a device fetches a file by itself (WISP 503 § Automatic and asked downloads): up to `autoBytes` while what
17120
+ * it took that way from the group stays within `autoBytesPerGroup`. A voice message too: one recorded (15 minutes at
17121
+ * most, about 3.6 MB) always fits, and a bigger file the author called a voice message is asked about like any other.
17058
17122
  */
17059
17123
  function fetchesByItself(file, keptAutomatically, limits = GROUP_FILE_LIMITS) {
17060
- if (file.voice) return true;
17061
17124
  return file.size <= limits.autoBytes && keptAutomatically + file.size <= limits.autoBytesPerGroup;
17062
17125
  }
17063
17126
  //#endregion
@@ -17329,7 +17392,7 @@ var VALUE = {
17329
17392
  max: "(\\d[\\d.,\\u00a0\\u202f' ]*?)"
17330
17393
  };
17331
17394
  var escape = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
17332
- Object.keys(ENGINE_ERRORS).map((code) => {
17395
+ var PATTERNS = Object.keys(ENGINE_ERRORS).map((code) => {
17333
17396
  const names = [];
17334
17397
  const source = ENGINE_ERRORS[code].split(/(\{\w+\})/).map((part) => {
17335
17398
  const value = /^\{(\w+)\}$/.exec(part);
@@ -17343,6 +17406,18 @@ Object.keys(ENGINE_ERRORS).map((code) => {
17343
17406
  pattern: new RegExp(`^${source}$`)
17344
17407
  };
17345
17408
  });
17409
+ /** The code and values of a text the engine wrote, or null for any other text (it is then shown as it is). */
17410
+ function parseEngineError(text) {
17411
+ const trimmed = text.trim();
17412
+ for (const { code, names, pattern } of PATTERNS) {
17413
+ const match = pattern.exec(trimmed);
17414
+ if (match) return {
17415
+ code,
17416
+ values: Object.fromEntries(names.map((name, i) => [name, match[i + 1]]))
17417
+ };
17418
+ }
17419
+ return null;
17420
+ }
17346
17421
  //#endregion
17347
17422
  //#region ../core/src/ghostlink.ts
17348
17423
  /**
@@ -17699,6 +17774,24 @@ var GhostLink = class {
17699
17774
  /** Edit frames the contact may send per window; the rest go unconfirmed and come again. */
17700
17775
  editsReceived = new RateWindow(30, EDIT_RATE_WINDOW_MS);
17701
17776
  wakeReceived = new RateWindow(6, 6e4);
17777
+ /**
17778
+ * The contact's name and picture, each saved when it changes: said once a session and when changed, so a handful a
17779
+ * minute each. Past that only the newest waits, read once the window has room again.
17780
+ */
17781
+ peerSaid = {
17782
+ "paired-nick": {
17783
+ window: new RateWindow(6, 6e4),
17784
+ read: null,
17785
+ timer: null
17786
+ },
17787
+ "paired-avatar": {
17788
+ window: new RateWindow(6, 6e4),
17789
+ read: null,
17790
+ timer: null
17791
+ }
17792
+ };
17793
+ /** Identity-proof frames the contact may send per window; the rest are dropped before they are read. */
17794
+ identityReceived = new RateWindow(40, IDENTITY_FRAME_WINDOW_MS);
17702
17795
  livenessTimer = null;
17703
17796
  peerAnswersPings = false;
17704
17797
  /** When the ping awaiting its pong went, and the round trip last measured on this session. */
@@ -19167,8 +19260,9 @@ var GhostLink = class {
19167
19260
  }
19168
19261
  /**
19169
19262
  * Ends the idle live session on this transport and lets its listener go, for a chat in use that found every one
19170
- * taken. The contact hears a goodbye, as when an app quits, rather than finding out when its pings go unanswered; the
19171
- * record then lists the transport undescribed, so it stops dialling here. The chat goes on over the DHT (or WebRTC),
19263
+ * taken. The contact hears a goodbye that says so (`yield`, `peerYielded`), rather than finding out when its pings go
19264
+ * unanswered, and does not watch for this app as for one that quit; the record then lists the transport undescribed,
19265
+ * so it stops dialling here. The chat goes on over the DHT (or WebRTC),
19172
19266
  * and takes a listener again the same way once it is in use. False when the session may not end (see above).
19173
19267
  */
19174
19268
  async yieldEndpoint(transport, idleMs) {
@@ -19180,7 +19274,10 @@ var GhostLink = class {
19180
19274
  this.yielding = true;
19181
19275
  try {
19182
19276
  try {
19183
- this.channel?.send(JSON.stringify({ t: "paired-bye" }));
19277
+ this.channel?.send(JSON.stringify({
19278
+ t: "paired-bye",
19279
+ yield: true
19280
+ }));
19184
19281
  } catch {}
19185
19282
  await new Promise((resolve) => setTimeout(resolve, YIELD_BYE_MS));
19186
19283
  this.disconnect();
@@ -21230,7 +21327,10 @@ var GhostLink = class {
21230
21327
  return;
21231
21328
  }
21232
21329
  if (frame?.t === "paired-bye") {
21233
- if (paired.state.status === "ready") this.peerDeparted(channel);
21330
+ if (paired.state.status === "ready") {
21331
+ if (frame.yield === true) this.peerYielded(channel);
21332
+ else this.peerDeparted(channel);
21333
+ }
21234
21334
  return;
21235
21335
  }
21236
21336
  if (!frame || typeof frame !== "object") return;
@@ -21309,25 +21409,15 @@ var GhostLink = class {
21309
21409
  return;
21310
21410
  }
21311
21411
  if (typeof frame?.t === "string" && frame.t.startsWith("idp-") && this.identitySupport) {
21312
- await this.options.events?.onIdentityProof?.(frame);
21412
+ if (this.identityReceived.take()) await this.options.events?.onIdentityProof?.(frame);
21313
21413
  return;
21314
21414
  }
21315
21415
  if (typeof frame?.t === "string" && frame.t.startsWith("proof-") && this.peerProofSupport) {
21316
21416
  await this.options.events?.onPeerProof?.(frame);
21317
21417
  return;
21318
21418
  }
21319
- if (frame?.t === "paired-avatar") {
21320
- const avatar = sanitizeAvatar(frame.a);
21321
- if (avatar !== void 0) this.options.events?.onPeerAvatar?.(avatar);
21322
- return;
21323
- }
21324
- if (frame?.t === "paired-nick") {
21325
- const nick = sanitizeNick(frame.n);
21326
- if (typeof frame.n === "string") this.options.events?.onPeerNick?.(nick ?? null);
21327
- if (nick !== this.peerNickOverride) {
21328
- this.peerNickOverride = nick ?? null;
21329
- this.options.events?.onPresence?.(this.presence);
21330
- }
21419
+ if (frame?.t === "paired-avatar" || frame?.t === "paired-nick") {
21420
+ this.peerSays(channel, frame.t, frame);
21331
21421
  return;
21332
21422
  }
21333
21423
  if (frame?.t === "paired-capabilities") {
@@ -21684,6 +21774,18 @@ var GhostLink = class {
21684
21774
  this.peerLost("bye");
21685
21775
  }
21686
21776
  /**
21777
+ * The contact gave this session's listener up to another chat (`yieldEndpoint`): its app is still there, and its
21778
+ * record now lists the transport undescribed, so there is nothing to dial and no return to watch for. The session
21779
+ * ends now, and the chat reads the contact at its own pace; the contact dials once it takes a listener back. Taken
21780
+ * as a goodbye, it was watched as an app that quit: 89 reads of its key in the 2 minutes after a yield on the
21781
+ * Desktop, against 6 at the background pace (bug hunt, 2026-10-15).
21782
+ */
21783
+ peerYielded(channel) {
21784
+ if (this.channel !== channel || this.stopped) return;
21785
+ traceLink(this.myPubKeyZ32, "peer-yielded", {});
21786
+ this.disconnect();
21787
+ }
21788
+ /**
21687
21789
  * A packet of the contact's not seen before, fresh, while nothing is live, soon after a live session ended: it is back
21688
21790
  * (its app restarted), so the wait that the attempts since built up is over, and the next dial goes now. Once per
21689
21791
  * loss: a contact that is there but unreachable publishes often, and the wait between attempts is what spares the
@@ -21778,6 +21880,57 @@ var GhostLink = class {
21778
21880
  offered.woke = true;
21779
21881
  if (!this.dialing) setTimeout(() => this.maybeKnockEarly(offered.epoch), 0);
21780
21882
  }
21883
+ /**
21884
+ * A name or picture the contact said, once it is one (reading it costs little; saving it is what counts): past the
21885
+ * window the newest waits for room on this session, so the last word still counts.
21886
+ */
21887
+ peerSays(channel, kind, frame) {
21888
+ let read;
21889
+ if (kind === "paired-avatar") {
21890
+ const avatar = sanitizeAvatar(frame.a);
21891
+ if (avatar === void 0) return;
21892
+ read = () => this.options.events?.onPeerAvatar?.(avatar);
21893
+ } else {
21894
+ const nick = sanitizeNick(frame.n) ?? null, said = typeof frame.n === "string";
21895
+ read = () => {
21896
+ if (said) this.options.events?.onPeerNick?.(nick);
21897
+ if (nick !== this.peerNickOverride) {
21898
+ this.peerNickOverride = nick;
21899
+ this.options.events?.onPresence?.(this.presence);
21900
+ }
21901
+ };
21902
+ }
21903
+ const held = this.peerSaid[kind];
21904
+ if (!held.timer && held.window.take()) {
21905
+ read();
21906
+ return;
21907
+ }
21908
+ held.read = read;
21909
+ if (held.timer) return;
21910
+ const due = () => {
21911
+ held.timer = null;
21912
+ if (this.channel !== channel || !held.read) {
21913
+ held.read = null;
21914
+ return;
21915
+ }
21916
+ if (!held.window.take()) {
21917
+ held.timer = setTimeout(due, held.window.wait());
21918
+ return;
21919
+ }
21920
+ const next = held.read;
21921
+ held.read = null;
21922
+ next();
21923
+ };
21924
+ held.timer = setTimeout(due, held.window.wait());
21925
+ }
21926
+ /** What the contact said of itself and was still waiting goes with the session: the next one says it again. */
21927
+ dropPeerSaid() {
21928
+ for (const said of Object.values(this.peerSaid)) {
21929
+ if (said.timer) clearTimeout(said.timer);
21930
+ said.timer = null;
21931
+ said.read = null;
21932
+ }
21933
+ }
21781
21934
  /** A ping on the session right after a wake, with `PONG_WAIT_MS` for its answer: none, and the session is dead. */
21782
21935
  probeAfterWake(channel) {
21783
21936
  if (!this.peerAnswersPings || this.paired?.state.status !== "ready") return;
@@ -21820,6 +21973,7 @@ var GhostLink = class {
21820
21973
  this.channel = this.httpHost = this.httpClient = this.files = null;
21821
21974
  this.peerServicesOverride = null;
21822
21975
  this.peerNickOverride = null;
21976
+ this.dropPeerSaid();
21823
21977
  this.sessionCapabilities.reset();
21824
21978
  this.typingSender.reset();
21825
21979
  this.typingReceiver.clear();
@@ -22304,6 +22458,10 @@ function parsePayParams(body, destination) {
22304
22458
  if (body.tag !== "payRequest") throw new Error(`${destination.domain} answered with something other than a pay request${typeof body.tag === "string" ? ` (${reason(body.tag)})` : ""}`);
22305
22459
  if (typeof body.callback !== "string") throw new Error("The pay request has no callback");
22306
22460
  const callback = acceptUrl(body.callback);
22461
+ if (callback.host !== new URL(destination.url).host) {
22462
+ if (callback.protocol !== "https:") throw new Error("Lightning addresses and LNURLs must use HTTPS");
22463
+ if (isLocalHost(callback.hostname)) throw new Error(`${destination.domain} asks for the invoice at a local address`);
22464
+ }
22307
22465
  const { minSendable, maxSendable, metadata, commentAllowed } = body;
22308
22466
  if (!Number.isSafeInteger(minSendable) || !Number.isSafeInteger(maxSendable) || minSendable <= 0 || maxSendable < minSendable) throw new Error("The pay request has invalid amount limits");
22309
22467
  const minSat = Math.ceil(minSendable / 1e3), maxSat = Math.floor(maxSendable / 1e3);
@@ -22442,10 +22600,14 @@ function decodeSvcbRecords(dnsPacket) {
22442
22600
  }
22443
22601
  return records;
22444
22602
  }
22445
- /** The homeserver a Pubky key's packet names: the z-base-32 key its `_pubky` record points at, or undefined. */
22603
+ /**
22604
+ * The homeserver a Pubky key's packet names: the z-base-32 key its `_pubky` record points at, or undefined. The target
22605
+ * is read next as a key (`pubkyRecords` checks the homeserver's packet against it), so only a canonical one names a
22606
+ * homeserver; a respelled one is none, not a lookup that fails as "no records".
22607
+ */
22446
22608
  function pubkyHomeserverOf(dnsPacket, userZ32) {
22447
22609
  const record = decodeSvcbRecords(dnsPacket).find((r) => r.name === `_pubky.${userZ32}` || r.name === "_pubky");
22448
- return record && Z32$1.test(record.target) ? record.target : void 0;
22610
+ return record && isZ32Key(record.target) ? record.target : void 0;
22449
22611
  }
22450
22612
  /**
22451
22613
  * The homeserver's web endpoint from its own packet: of the HTTPS/SVCB records at its root, the lowest priority one
@@ -22722,6 +22884,8 @@ function assertTokenUnits(amount) {
22722
22884
  function assertWholeSats(amount) {
22723
22885
  if (!Number.isSafeInteger(amount) || amount <= 0 || amount > 0x775f05a074000) throw new Error("Enter a positive whole amount in sats");
22724
22886
  }
22887
+ /** Arkade and Bark addresses are bech32m with `ark` on Bitcoin and `tark` on the test networks (one case only). */
22888
+ var arkAddressPrefix = (network) => network === "bitcoin" ? "ark1" : "tark1";
22725
22889
  function validatePaymentTarget(value, now = Date.now()) {
22726
22890
  if (!value || typeof value !== "object") throw new Error("Invalid payment target");
22727
22891
  const t = value;
@@ -22760,6 +22924,7 @@ function validatePaymentTarget(value, now = Date.now()) {
22760
22924
  "regtest"
22761
22925
  ].includes(t.network)) throw new Error("Unsupported Bark network");
22762
22926
  if (typeof t.address !== "string" || !t.address || t.address.length > 4096 || typeof t.provider !== "string" || t.provider.length > 512) throw new Error("Invalid payment destination");
22927
+ if ((t.method === "arkade" || t.method === "bark") && (t.address !== t.address.toLowerCase() && t.address !== t.address.toUpperCase() || !t.address.toLowerCase().startsWith(arkAddressPrefix(t.network)))) throw new Error(`That is not an Ark address on ${t.network === "bitcoin" ? "Bitcoin" : t.network}`);
22763
22928
  if (t.method !== "bitcoin" && t.method !== "fedimint" && t.method !== "spark") {
22764
22929
  const url = new URL(t.provider);
22765
22930
  if (url.username || url.password || url.search || url.hash || url.protocol !== "https:" && !(url.protocol === "http:" && [
@@ -24402,7 +24567,7 @@ function shortAtprotoDid(did) {
24402
24567
  }
24403
24568
  /** The record key of a proof: its proof key (z-base-32, already a valid record key), so removal finds it. */
24404
24569
  function atprotoProofRkey(proofKey) {
24405
- if (!/^[ybndrfg8ejkmcpqxot1uwisza345h769]{52}$/.test(proofKey)) throw new Error("Invalid proof key");
24570
+ if (!isZ32Key(proofKey)) throw new Error("Invalid proof key");
24406
24571
  return proofKey;
24407
24572
  }
24408
24573
  var fragment = (id, did, name) => id === `#${name}` || id === `${did}#${name}`;
@@ -25824,6 +25989,28 @@ var fileAad = (f) => JSON.stringify([
25824
25989
  f.ts
25825
25990
  ]);
25826
25991
  var isFileBox = isSealedBox(MAX_FILE_BOX);
25992
+ /** The author's name (`MAX_NICK_LENGTH` characters of up to four bytes each), sealed; signed apart from `xs` (`ns`). */
25993
+ var MAX_NICK_BOX = Math.ceil(1088 / 3) + 4;
25994
+ var nickAad = (f) => JSON.stringify([
25995
+ "ghostly-group/1 nick",
25996
+ f.g,
25997
+ f.e,
25998
+ f.s,
25999
+ f.n,
26000
+ f.ts
26001
+ ]);
26002
+ var nickSigned = (f, nk) => utf8Encode(JSON.stringify([
26003
+ "ghostly-group/1 nick",
26004
+ f.g,
26005
+ f.e,
26006
+ f.s,
26007
+ f.n,
26008
+ f.ts,
26009
+ nk.n,
26010
+ nk.c
26011
+ ]));
26012
+ var isNickBox = isSealedBox(MAX_NICK_BOX);
26013
+ var isSignature = (v) => typeof v === "string" && v.length === 86 && B64$1.test(v);
25827
26014
  var B64$1 = /^[A-Za-z0-9_-]*$/;
25828
26015
  var secretAad$1 = (g, e, member) => JSON.stringify([
25829
26016
  "ghostly-group/1 secret",
@@ -25963,7 +26150,7 @@ var ownKey = (f) => `${f.e}:${f.n}`;
25963
26150
  /** What a frame weighs in a log, roughly: its boxes plus the fixed fields around them. */
25964
26151
  var frameKey$1 = (f) => `${f.s}:${f.e}:${f.n}`;
25965
26152
  /** What a kept frame counts for against `GROUP_LIMITS.relayBytes`. */
25966
- var frameBytes = (f) => f.c.length + (f.m?.c.length ?? 0) + (f.r?.c.length ?? 0) + (f.sc?.c.length ?? 0) + (f.fl?.c.length ?? 0) + 400;
26153
+ var frameBytes = (f) => f.c.length + (f.m?.c.length ?? 0) + (f.r?.c.length ?? 0) + (f.sc?.c.length ?? 0) + (f.fl?.c.length ?? 0) + (f.nk?.c.length ?? 0) + 400;
25967
26154
  /** Only the fields an edit frame has, as `clean` does for a message. */
25968
26155
  var cleanEdit = (f) => ({
25969
26156
  t: GROUP_EDIT_FRAME,
@@ -26007,7 +26194,14 @@ function clean(f) {
26007
26194
  c: f.fl.c
26008
26195
  } } : {},
26009
26196
  ...readFirstPlace(f.o, f.e) ? { o: readFirstPlace(f.o, f.e) } : {},
26010
- ...typeof f.xs === "string" && f.xs.length === 86 && B64$1.test(f.xs) ? { xs: f.xs } : {}
26197
+ ...typeof f.xs === "string" && f.xs.length === 86 && B64$1.test(f.xs) ? { xs: f.xs } : {},
26198
+ ...isNickBox(f.nk) && isSignature(f.ns) ? {
26199
+ nk: {
26200
+ n: f.nk.n,
26201
+ c: f.nk.c
26202
+ },
26203
+ ns: f.ns
26204
+ } : {}
26011
26205
  };
26012
26206
  }
26013
26207
  /** Whether the member whose sync this is lacks a frame: above what it has from that sender, or one it names as missing. */
@@ -26077,6 +26271,8 @@ var GroupSession = class {
26077
26271
  asked = /* @__PURE__ */ new Map();
26078
26272
  /** The signed leave of each member I last passed on, kept for an admin that was away (`byesHeld`). In memory only. */
26079
26273
  byesPassed = /* @__PURE__ */ new Map();
26274
+ /** Member → when the name I hold for it was said: its message's time, or the moment an edge said it (`heardName`). In memory only. */
26275
+ nickAt = /* @__PURE__ */ new Map();
26080
26276
  queue = Promise.resolve();
26081
26277
  /** One metadata frame that names a commit or an epoch I do not have yet: tried again when the chain moves. */
26082
26278
  pendingMeta;
@@ -26435,7 +26631,7 @@ var GroupSession = class {
26435
26631
  rewelcome(memberKey) {
26436
26632
  return this.serialize(async () => {
26437
26633
  this.requireAdmin();
26438
- if (!rosterHas(this.roster, memberKey) || memberKey === this.myKey) throw new Error("Not a member");
26634
+ if (!rosterHas(this.roster, memberKey) || memberKey === this.myKey || !isZ32Key(memberKey)) throw new Error("Not a member");
26439
26635
  const chain = this.state.chain;
26440
26636
  let admitted = chain.length - 1;
26441
26637
  while (admitted >= 0 && !(chain[admitted].k === "add" && chain[admitted].s === memberKey)) admitted--;
@@ -26517,7 +26713,7 @@ var GroupSession = class {
26517
26713
  await this.persist();
26518
26714
  for (const [key] of previous.m) {
26519
26715
  if (key === this.myKey) continue;
26520
- const member = rosterHas(roster, key);
26716
+ const member = rosterHas(roster, key) && isZ32Key(key);
26521
26717
  this.hooks.send(key, {
26522
26718
  t: "group-commit",
26523
26719
  g: this.id,
@@ -26695,6 +26891,8 @@ var GroupSession = class {
26695
26891
  ...extra.file ? { fl: encryptText(key, fileAad(header), JSON.stringify(extra.file)) } : {},
26696
26892
  ...o ? { o } : {}
26697
26893
  };
26894
+ const nick = sanitizeNick(this.hooks.nick?.());
26895
+ const nk = nick === void 0 ? void 0 : encryptText(key, nickAad(header), nick);
26698
26896
  return {
26699
26897
  t: "group-msg",
26700
26898
  ...unsigned,
@@ -26703,7 +26901,11 @@ var GroupSession = class {
26703
26901
  xs: toBase64Url(sign(messageSignedWhole({
26704
26902
  ...unsigned,
26705
26903
  ...boxes
26706
- }), this.identity.seed))
26904
+ }), this.identity.seed)),
26905
+ ...nk ? {
26906
+ nk,
26907
+ ns: toBase64Url(sign(nickSigned(header, nk), this.identity.seed))
26908
+ } : {}
26707
26909
  };
26708
26910
  }
26709
26911
  /** Into my bounded log, for members who missed it. */
@@ -26897,10 +27099,12 @@ var GroupSession = class {
26897
27099
  setNick(key, nick) {
26898
27100
  return this.serialize(async () => {
26899
27101
  const clean = sanitizeNick(nick);
27102
+ this.nickAt.set(key, this.clock());
26900
27103
  if ((this.state.nicks[key] ?? void 0) === clean) return;
26901
27104
  if (clean) this.state.nicks[key] = clean;
26902
27105
  else delete this.state.nicks[key];
26903
- await this.persist();
27106
+ if (key === this.myKey) await this.persist();
27107
+ else this.persistSoon();
26904
27108
  this.hooks.changed();
26905
27109
  });
26906
27110
  }
@@ -27132,6 +27336,7 @@ var GroupSession = class {
27132
27336
  const key = epochKeys(secret, this.id, raw.e).message;
27133
27337
  const text = decryptText(key, messageAad$1(raw), raw.nn, raw.c);
27134
27338
  if (text === null) return;
27339
+ if (this.heardName(key, raw)) this.hooks.changed();
27135
27340
  const firstFrame = first && {
27136
27341
  ...raw,
27137
27342
  e: first.e,
@@ -27245,6 +27450,26 @@ var GroupSession = class {
27245
27450
  return false;
27246
27451
  }
27247
27452
  }
27453
+ /**
27454
+ * The author's name in a message (`nk`), when the author's signature over it (`ns`) holds: any copy, handed on or
27455
+ * not, whole or not, since a member handing it on can only drop it. Taken only from a message newer than the one it
27456
+ * last came in, or than the moment an edge last said it (`setNick`), so a catch-up's older pages never bring back a
27457
+ * name the member changed since. Whether the name I hold changed.
27458
+ */
27459
+ heardName(key, raw) {
27460
+ if (!isNickBox(raw.nk) || !isSignature(raw.ns) || raw.ts <= (this.nickAt.get(raw.s) ?? 0)) return false;
27461
+ try {
27462
+ if (!verify(fromBase64Url(raw.ns), nickSigned(raw, raw.nk), publicKeyFromZ32(raw.s))) return false;
27463
+ } catch {
27464
+ return false;
27465
+ }
27466
+ const nick = sanitizeNick(decryptText(key, nickAad(raw), raw.nk.n, raw.nk.c));
27467
+ if (nick === void 0) return false;
27468
+ this.nickAt.set(raw.s, raw.ts);
27469
+ if (this.state.nicks[raw.s] === nick) return false;
27470
+ this.state.nicks[raw.s] = nick;
27471
+ return true;
27472
+ }
27248
27473
  openMentions(key, raw, text, everyone) {
27249
27474
  if (!isMentionsBox(raw.m)) return [];
27250
27475
  const plain = decryptText(key, mentionsAad(raw), raw.m.n, raw.m.c);
@@ -27436,7 +27661,7 @@ var GroupSession = class {
27436
27661
  await this.replayWaiting();
27437
27662
  }
27438
27663
  async receiveSync(from, frame) {
27439
- if (!rosterHas(this.roster, from) || !Number.isSafeInteger(frame.e) || frame.e < 0 || typeof frame.h !== "string") return;
27664
+ if (!rosterHas(this.roster, from) || !isZ32Key(from) || !Number.isSafeInteger(frame.e) || frame.e < 0 || typeof frame.h !== "string") return;
27440
27665
  let answered = this.syncsAnswered.get(from);
27441
27666
  if (!answered) this.syncsAnswered.set(from, answered = new RateWindow(GROUP_LIMITS.syncAnswers, GROUP_LIMITS.syncWindowMs, () => this.hooks.clock?.() ?? Date.now()));
27442
27667
  if (!answered.take()) return;
@@ -28972,7 +29197,7 @@ var CommunitySession = class {
28972
29197
  commit
28973
29198
  });
28974
29199
  if (FRESH.has(kind)) {
28975
- for (const [key] of roster) if (key !== this.myKey) this.hooks.addressed(key, {
29200
+ for (const [key] of roster) if (key !== this.myKey && isZ32Key(key)) this.hooks.addressed(key, {
28976
29201
  t: "group-secret",
28977
29202
  v: 2,
28978
29203
  g: this.id,
@@ -29019,7 +29244,7 @@ var CommunitySession = class {
29019
29244
  rewelcome(memberKey) {
29020
29245
  return this.serialize(async () => {
29021
29246
  this.requireMember();
29022
- if (!rosterHas(this.roster, memberKey) || memberKey === this.myKey) throw new Error("Not a member");
29247
+ if (!rosterHas(this.roster, memberKey) || memberKey === this.myKey || !isZ32Key(memberKey)) throw new Error("Not a member");
29023
29248
  for (let i = this.state.chain.length - 1; i >= 0; i--) {
29024
29249
  const c = this.state.chain[i];
29025
29250
  if (c.k === "add" && c.s === memberKey) return this.welcomeFor(memberKey, communityCommitHash(c));
@@ -29085,7 +29310,10 @@ var CommunitySession = class {
29085
29310
  seedB64: entry?.seedB64 ?? ""
29086
29311
  };
29087
29312
  await this.persist();
29088
- if (entry) for (const key of this.others) this.hooks.addressed(key, this.entryFrame(key));
29313
+ if (entry) for (const key of this.others) {
29314
+ const frame = this.entryFrame(key);
29315
+ if (frame) this.hooks.addressed(key, frame);
29316
+ }
29089
29317
  this.hooks.changed();
29090
29318
  return this.state.entry.key;
29091
29319
  });
@@ -29701,7 +29929,7 @@ var CommunitySession = class {
29701
29929
  for (const { from, frame } of ready) await this.receiveMessage(from, frame);
29702
29930
  }
29703
29931
  async receiveSync(from, frame) {
29704
- if (!this.isRecentMember(from) || !Number.isSafeInteger(frame.e) || typeof frame.h !== "string") return;
29932
+ if (!this.isRecentMember(from) || !isZ32Key(from) || !Number.isSafeInteger(frame.e) || typeof frame.h !== "string") return;
29705
29933
  let answered = this.syncsAnswered.get(from);
29706
29934
  if (!answered) this.syncsAnswered.set(from, answered = new RateWindow(COMMUNITY_LIMITS.syncAnswers, COMMUNITY_LIMITS.syncWindowMs, () => this.hooks.clock?.() ?? Date.now()));
29707
29935
  if (!answered.take()) return;
@@ -29816,7 +30044,7 @@ var CommunitySession = class {
29816
30044
  /** The current entry seed sealed to a member, when I hold it: sealed once per seed, not on every sync answer. */
29817
30045
  entryFrame(to) {
29818
30046
  const { key, seedB64 } = this.state.entry;
29819
- if (!key || !seedB64) return null;
30047
+ if (!key || !seedB64 || !isZ32Key(to)) return null;
29820
30048
  const sealed = this.sealedEntries.get(to);
29821
30049
  if (sealed?.seedB64 === seedB64 && sealed.frame.x === key) return { ...sealed.frame };
29822
30050
  const frame = {
@@ -30986,6 +31214,6 @@ function appStoreDecision(held, read) {
30986
31214
  return read.digest === held.digest ? "same" : "equivocation";
30987
31215
  }
30988
31216
  //#endregion
30989
- export { COMMUNITY_EDIT_FRAME as $, cardUrl as $a, budgetRetryMs as $c, PeerProofs as $i, GROUP_WANT_FRAME as $n, HTTP_SERVICE_PROTO as $o, deviceWakeRequest as $r, enrollCancelFrame as $s, didDhtFromPublicKey as $t, CommunitySession as A, COMMUNITY_REACTION_FRAME as Aa, TURN_PUT_WINDOW_MS as Ac, normalizeDomain as Ai, pubkyProofPath as An, DirectPathWatch as Ao, rankHubs as Ar, handoffStreamKey as As, verifyAtprotoRecordProof as At, knockRecords as B, wireReaction as Ba, signTurnPacket as Bc, IDENTITY_MAX_EVIDENCE as Bi, isFederationId as Bn, LIMITS as Bo, TYPING_REFRESH_MS as Br, readHandoffVerified as Bs, didWebDocumentUrl as Bt, sha256Paced as C, EDIT_BUFFER_MS as Ca, deviceEchoNonce as Cc, decodeDnsResponse as Ci, PUBKY_PROOF_MAX_BYTES as Cn, GROUP_MEMBER_CAP as Co, freshHubs as Cr, handoffEphemeral as Cs, atprotoProofRkey as Ct, openGroupTyping as D, parseLinkPreview as Da, newDeviceSetSecret as Dc, encodeDnsQuery as Di, normalizePubkyKey as Dn, rosterHas as Do, mergeLobby as Dr, handoffPakeFrame as Ds, parseAtprotoDidDocument as Dt, groupTypingEpoch as E, dhtEditId as Ea, devicePingFrame as Ec, domainWellKnownFile as Ei, newPubkyProofFolder as En, commitHash as Eo, mergeBeacon as Er, handoffOfferFrame as Es, normalizeAtprotoHandle as Et, KNOCK_TTL_MS as F, queueReaction as Fa, TURN_LAST_SEQUENCE as Fc, wellKnownUrl as Fi, decodeBolt11 as Fn, presenceSeenAt as Fo, ChatFiles as Fr, readHandoffDone as Fs, checkDidJws as Ft, groupWakeFrame as G, ITEM_STATES as Ga, turnStatedVerifier as Gc, boundVerifiedIdentity as Gi, LIVENESS_PING_MS as Gn, answersOffer as Go, PIN_LIMITS as Gr, setAckFrame as Gs, normalizeDid as Gt, readKnocks as H, readReply as Ha, turnInstance as Hc, IDENTITY_REVOCATION_LABEL as Hi, parseFedimintRequestPayload as Hn, encodeControl as Ho, sanitizeTypingStatus as Hr, SET_ACK as Hs, didWebFileNames as Ht, decodeGroupEntryLink as I, reactionEmoji as Ia, TURN_MAX as Ic, CapsExchange as Ii, ENDPOINT as In, GhostlyHttpError as Io, FILE_LIMITS as Ir, readHandoffHave as Is, checkDidSignature as It, GROUP_LIMITS as J, RUN_RESULTS as Ja, RelayTransport as Jc, identityStatement as Ji, pairedMessageFrame as Jn, callSignalHeardAt as Jo, pinIsNewer as Jr, verifySetUpdate as Js, parseDidSignaturePaste as Jt, groupWakes as K, PR_CHECKS as Ka, turnVerifierMark as Kc, emptyIdentityLedger as Ki, PAIRED_FRAME_TOO_LARGE as Kn, buildSdpFromSignal as Ko, mayPin as Kr, setUpdateFrame as Ks, parseDid as Kt, encodeGroupEntryLink as L, reactionIsNewer as La, firstDeviceSetSecret as Lc, DHT_TEXT_CAPABILITY as Li, cashuRequestPayload as Ln, formatLocalTarget as Lo, transferEnded as Lr, readHandoffHello as Ls, didGhostlyServiceEntry as Lt, communityHasFrame as M, REACTION_LIMITS as Ma, TURN_SETTLE_MS as Mc, recordsFromWellKnown as Mi, pubkyHomeserverOf as Mn, DHT_POLL_INTERVALS as Mo, readBeaconHead as Mr, newHandoffId as Ms, DID_FILE_MAX_BYTES as Mt, decodeCommunityLink as N, ReactionWindow as Na, classifyTurnRead as Nc, recordsName as Ni, requestLnurlInvoice as Nn, EXPECT_PEER_MS as No, readLobby as Nr, readHandoffAck as Ns, DID_JWS_MAX_LENGTH as Nt, sealGroupTyping as O, forwardedAgain as Oa, seedSigner as Oc, isPublicAddress as Oi, pubkyProofCapability as On, verifyCommitSignature as Oo, newerHead as Or, handoffReleaseFrame as Os, shortAtprotoDid as Ot, encodeCommunityLink as P, nextReactionNumber as Pa, turnPutSummary as Pc, txtValue as Pi, resolveLightningDestination as Pn, RELAY_POLL_INTERVALS as Po, shouldBeHub as Pr, readHandoffCancel as Ps, DidError as Pt, frameBytes as Q, cardLine as Qa, traceLink as Qc, lastSharedWithMe as Qi, GROUP_HAVE_FRAME as Qn, emptyLinkRecords as Qo, WakeLimiter as Qr, ENROLL_PROOF as Qs, didDhtDocument as Qt, entryParams as R, readReaction as Ra, nextTurnPosition as Rc, capsDescriptors as Ri, fedimintRequestPayload as Rn, parseLocalTarget as Ro, transferStalled as Rr, readHandoffPake as Rs, didJwsAlg as Rt, readAppBundleAsync as S, parseVideoMeta as Sa, deviceEchoFrame as Sc, DOMAIN_FILE_MAX_BYTES as Si, isBitcoinAddress as Sn, readCanonicalJson as So, doorHubs as Sr, handoffDigest as Ss, atprotoProofRecord as St, GroupTypingBoard as T, RateWindow as Ta, deviceLinkPairing as Tc, domainTxtRecord as Ti, isPubkyProofFolder as Tn, MEMBER_KEY as To, lobbyRecords as Tr, handoffManifestFrames as Ts, isAtprotoDid as Tt, GROUP_WAKE_FRAME as U, replySnippet as Ua, turnKeys as Uc, IdentityCheckUnavailable as Ui, GhostLink as Un, CALL_SIGNAL_MAX_AGE_MS as Uo, typingActivity as Ur, SET_UPDATE as Us, didWebFileUrl as Ut, mergeKnocks as V, pairedReplyAuthor as Va, signTurnRelease as Vc, IDENTITY_MAX_VALIDITY as Vi, parseCashuRequestPayload as Vn, decodeControl as Vo, TypingSender as Vr, sealHandoffPiece as Vs, didWebFile as Vt, GroupWakeLimiter as W, BUTTON_ID as Wa, turnName as Wc, IdentityExchange as Wi, INVITE_TAKEN as Wn, RTC_CONFIG as Wo, GROUP_PIN_FRAME as Wr, readSetUpdate as Ws, didWebHost as Wt, GROUP_READ_NOTE as X, appCardId as Xa, normalizeRelayUrl as Xc, receivedIdentityStatus as Xi, engineText as Xn, parseCallSignal as Xo, readPin as Xr, ENROLL_GRANT as Xs, staticDidDocument as Xt, GROUP_REACTED_FRAME as Y, STATUS_CARD_LIMITS as Ya, currentRelays as Yc, newIdentityBinding as Yi, engineError as Yn, extractParamsFromSdp as Yo, pinNumberHolds as Yr, ENROLL_DONE as Ys, signingKeys as Yt, GroupSession as Z, buttonLabelClash as Za, setLinkTraceSink as Zc, revokesIdentity as Zi, GROUP_FILE_LIMITS as Zn, signalHasVideo as Zo, WAKE_CALL_INTERVAL_MS as Zr, ENROLL_HELLO as Zs, strictBase64Url as Zt, assembleAppBundle as _, VOICE_LIMITS as _a, inviteOwnership as _c, HoldKeys as _i, randomBytes as _l, walletNetworkOf as _n, readAppRevocations as _o, meshHubs as _r, handoffBusyFrame as _s, ATPROTO_DECLARED_SCOPE as _t, planAppUpdate as a, irohRelayUrl as aa, enrollProofFrame as ac, vapidKeysMatch as ai, newerPacket as al, EVM_TEST_CHAINS as an, heardTime as ao, readGroupHave as ar, HANDOFF_HAVE as as, groupName as at, pacer as b, parseVoiceMeta as ba, ENROLL_CAPABILITY as bc, newMailbox as bi, utf8Decode as bl, sparkAddressKind as bn, canonicalJson as bo, beaconKeys as br, handoffContextProof as bs, ATPROTO_PROOF_SCOPE as bt, readAppStore as c, peekDhtMailbox as ca, verifyEnrollProof as cc, isAppVersion as ci, createIdentity as cl, SEPOLIA_TEST_USDT as cn, APP_PREFIXES as co, CarriedTransport as cr, HANDOFF_PAKE as cs, SshSigError as ct, APP_BUNDLE_LIMITS as d, parseImageMeta as da, readDeviceInvite as dc, EDIT_CAPABILITY as di, publicKeyFromZ32 as dl, USDT_PUBLIC_RPC as dn, appRef as do, SIGNAL_HOLD_MS as dr, HANDOFF_TIMINGS as ds, sameSshKey as dt, emptyProofLedger as ea, enrollCancelReason as ec, newWakeToken as ei, isDiscoveryBudgetError as el, encodeDidDhtDeactivated as en, checkStatusCard as eo, GROUP_WANT_NO_FRAME as er, LEGACY_SERVICES as es, GROUP_EDIT_FRAME as et, APP_BUNDLE_MAGIC as f, readImageMeta as fa, OwnInviteError as fc, STATUS_CARD_CAPABILITY as fi, verify as fl, WALLET_NETWORKS as fn, checkAppRevokeStatement as fo, SIGNAL_REPLY_MS as fr, HANDOFF_VERIFIED as fs, sshSignCommand as ft, appViewOf as g, PLAYABLE_AUDIO as ga, inviteLink as gc, HOLD_LIMITS as gi, fromBase64Url as gl, validatePaymentTarget as gn, readAppRevocation as go, mayBeHub as gr, handoffAttemptSucceeded as gs, decodeAnyBitcoinAddress as gt, appDigest as h, sanitizeMime as ha, encodeInviteCode as hc, HOLD_DATED_BACK_MS as hi, bytesEqual as hl, formatPaymentAmount as hn, isAppRef as ho, MESH_HUBS as hr, handoffAttemptFailed as hs, BitcoinAddressError as ht, checkAppBeforeRun as i, irohDescriptor as ia, enrollHelloFrame as ic, checkPushEndpoint as ii, createRelayPayload as il, ETHEREUM_USDT as in, claimedTime as io, readGroupFileMeta as ir, HANDOFF_DONE as is, mentionsMember as it, GROUP_READ_NOTE_COMMUNITY as j, GROUP_REACTION_FRAME as ja, TURN_RAISE_READ_TIMEOUT_MS as jc, recordsFromTxt as ji, homeserverWebEndpoint as jn, AWAITING_PEER_MS as jo, readBeacon as jr, handoffVersions as js, DID_DOCUMENT_MAX_BYTES as jt, COMMUNITY_LIMITS as k, readForwarded as ka, webCryptoSigner as kc, nip05RootKey as ki, pubkyProofFile as kn, edgeParams as ko, pickHubs as kr, handoffRetryAfter as ks, AtprotoProofError as kt, readAppStoreSteps as l, DHT_FILE_CAPABILITY as la, createDeviceInvite as lc, APPS_CAPABILITY as li, identityFromSeed as ll, SEPOLIA_TEST_USDT_FAUCET as ln, APP_STATEMENT_LIMITS as lo, GROUP_SIGNAL_FRAME as lr, HANDOFF_PIECE_BYTES as ls, parseSshPublicKey as lt, APP_PERMISSIONS as m, sanitizeFileName as ma, decodeInviteCode as mc, HOLD_CAPABILITY as mi, sanitizeAvatar as ml, assertWholeSats as mn, isAppKey as mo, readGroupSignal as mr, handoffAttemptAllowed as ms, verifyBitcoinMessage as mt, appStoreDecision as n, TRANSPORTS as na, enrollDoneFrame as nc, relayRequest as ni, PacketTooLargeError as nl, resolveDidDht as nn, statusCardText as no, groupFileFallback as nr, HANDOFF_ACK as ns, carryMentions as nt, APP_STORE_LIMITS as o, DHT_FILE_TOO_LARGE as oa, readEnrollGrant as oc, APP_DATA_MAX_BYTES as oi, openRelayPayload as ol, ONCHAIN_PROVIDER as on, receivedTimestamp as oo, readGroupWant as or, HANDOFF_HELLO as os, SSHSIG_MAX_ARMOR as ot, APP_ICON_PATH as p, formatFileSize as pa, createChatInvite as pc, UPGRADE_CAPABILITY as pi, MAX_AVATAR_LENGTH as pl, assertTokenUnits as pn, isAppHash as po, groupSignalFrame as pr, filePartName as ps, verifySshSignature as pt, parseGroupWakeFrame as q, PR_STATES as qa, DEFAULT_RELAYS as qc, identityRevocationValue as qi, pairedMessageFits as qn, callRtcConfig as qo, nextPinNumber as qr, signTombstone as qs, parseDidDocument as qt, appUpdateDecision as r, automaticTransport as ra, enrollGrantFrame as rc, wakeRequest as ri, RELAY_PAYLOAD_MAX_BYTES as rl, signDidDhtPacket as rn, arrivalKey as ro, isGroupMessageId as rr, HANDOFF_CANCEL as rs, parseCommunityEdit as rt, checkAppStoreIndex as s, DHT_MESSAGE_TTL as sa, readEnrollHello as sc, chatAppId as si, parseRelayPayload as sl, PaymentPreflightError as sn, shownTime as so, readGroupWantNo as sr, HANDOFF_OFFER as ss, SSHSIG_NAMESPACE as st, appRevocationFor as t, validExternalKey as ta, enrollDigits as tc, parseWakeFrame as ti, withRequestOptions as tl, encodeDidDhtPacket as tn, sameButtonText as to, fetchesByItself as tr, serviceIdFromName as ts, GROUP_EDIT_TEXT_BYTES as tt, signAppStore as u, dhtFileId as ua, deviceInviteJoinerParams as uc, BUTTONS_CAPABILITY as ui, identityFromSeedB64 as ul, TEST_USDT_FAUCET_AMOUNT as un, appFingerprint as uo, SIGNAL_FRESH_MS as ur, HANDOFF_REQUEST as us, parseSshSignature as ut, checkAppManifest as v, baseMime as va, ownInviteRefusal as vc, HoldRefusedError as vi, toBase64Url as vl, SPARK_NETWORKS as vn, signAppObject as vo, pickMeshHubs as vr, handoffCancelFrame as vs, ATPROTO_FULL_SCOPE as vt, GROUP_TYPING_FRAME as w, EDIT_RATE_WINDOW_MS as wa, deviceKeyZ32 as wc, dnsProofName as wi, isPubkyKey as wn, MAX_GROUP_CHAIN as wo, lobbyKeys as wr, handoffHelloFrame as ws, checkAtprotoProofRecord as wt, readAppBundle as x, voicePeaksOf as xa, HANDOFF_CAPABILITY as xc, readManifest as xi, utf8Encode as xl, sparkInvoiceDetails as xn, canonicalJsonBytes as xo, beaconRecords as xr, handoffDataFrame as xs, atprotoDidMethod as xt, isAppUrl as y, fileMessageText as ya, DEVICES_CAPABILITY as yc, isHoldUrl as yi, toZ32 as yl, SPARK_PROVIDER as yn, signAppRevocation as yo, COMMUNITY_TOPOLOGY as yr, handoffContext as ys, ATPROTO_PROOF_COLLECTION as yt, knockIdentity as z, validReactionNumber as za, readTurnPacket as zc, dialDescriptors as zi, findEndpoint as zn, webLocalFetch as zo, TYPING_KINDS as zr, readHandoffTurnFrame as zs, didSignedMessages as zt };
31217
+ export { COMMUNITY_EDIT_FRAME as $, buttonLabelClash as $a, currentRelays as $c, revokesIdentity as $i, GROUP_FILE_LIMITS as $n, extractParamsFromSdp as $o, WAKE_CALL_INTERVAL_MS as $r, ENROLL_DONE as $s, didDhtFromPublicKey as $t, CommunitySession as A, forwardedAgain as Aa, devicePingFrame as Ac, isPublicAddress as Ai, pubkyProofFile as An, rosterAdmin as Ao, newerHead as Ar, handoffOfferFrame as As, verifyAtprotoRecordProof as At, knockRecords as B, readReaction as Ba, TURN_MAX as Bc, capsDescriptors as Bi, findEndpoint as Bn, GhostlyHttpError as Bo, transferStalled as Br, readHandoffHave as Bs, didWebDocumentUrl as Bt, sha256Paced as C, parseImageMeta as Ca, DEVICES_CAPABILITY as Cc, readManifest as Ci, randomBytes as Cl, isBitcoinAddress as Cn, canonicalJsonBytes as Co, beaconRecords as Cr, handoffContext as Cs, atprotoProofRkey as Ct, openGroupTyping as D, RateWindow as Da, deviceEchoNonce as Dc, domainTxtRecord as Di, utf8Encode as Dl, newPubkyProofFolder as Dn, MEMBER_KEY as Do, lobbyRecords as Dr, handoffEphemeral as Ds, parseAtprotoDidDocument as Dt, groupTypingEpoch as E, EDIT_RATE_WINDOW_MS as Ea, deviceEchoFrame as Ec, dnsProofName as Ei, utf8Decode as El, isPubkyProofFolder as En, MAX_GROUP_CHAIN as Eo, lobbyKeys as Er, handoffDigest as Es, normalizeAtprotoHandle as Et, KNOCK_TTL_MS as F, ReactionWindow as Fa, TURN_RAISE_READ_TIMEOUT_MS as Fc, recordsName as Fi, resolveLightningDestination as Fn, AWAITING_PEER_MS as Fo, readLobby as Fr, handoffVersions as Fs, checkDidJws as Ft, groupWakeFrame as G, replySnippet as Ga, signTurnRelease as Gc, IdentityCheckUnavailable as Gi, INVITE_TAKEN as Gn, decodeControl as Go, typingActivity as Gr, sealHandoffPiece as Gs, normalizeDid as Gt, readKnocks as H, wireReaction as Ha, nextTurnPosition as Hc, IDENTITY_MAX_EVIDENCE as Hi, parseCashuRequestPayload as Hn, parseLocalTarget as Ho, TYPING_REFRESH_MS as Hr, readHandoffPake as Hs, didWebFileNames as Ht, decodeGroupEntryLink as I, nextReactionNumber as Ia, TURN_SETTLE_MS as Ic, txtValue as Ii, decodeBolt11 as In, DHT_POLL_INTERVALS as Io, shouldBeHub as Ir, newHandoffId as Is, checkDidSignature as It, GROUP_LIMITS as J, PR_CHECKS as Ja, turnName as Jc, emptyIdentityLedger as Ji, pairedMessageFits as Jn, RTC_CONFIG as Jo, mayPin as Jr, readSetUpdate as Js, parseDidSignaturePaste as Jt, groupWakes as K, BUTTON_ID as Ka, turnInstance as Kc, IdentityExchange as Ki, LIVENESS_PING_MS as Kn, encodeControl as Ko, GROUP_PIN_FRAME as Kr, SET_ACK as Ks, parseDid as Kt, encodeGroupEntryLink as L, queueReaction as La, classifyTurnRead as Lc, wellKnownUrl as Li, ENDPOINT as Ln, EXPECT_PEER_MS as Lo, ChatFiles as Lr, readHandoffAck as Ls, didGhostlyServiceEntry as Lt, communityHasFrame as M, COMMUNITY_REACTION_FRAME as Ma, seedSigner as Mc, normalizeDomain as Mi, homeserverWebEndpoint as Mn, verifyCommitSignature as Mo, rankHubs as Mr, handoffReleaseFrame as Ms, DID_FILE_MAX_BYTES as Mt, decodeCommunityLink as N, GROUP_REACTION_FRAME as Na, webCryptoSigner as Nc, recordsFromTxt as Ni, pubkyHomeserverOf as Nn, edgeParams as No, readBeacon as Nr, handoffRetryAfter as Ns, DID_JWS_MAX_LENGTH as Nt, sealGroupTyping as O, dhtEditId as Oa, deviceKeyZ32 as Oc, domainWellKnownFile as Oi, normalizePubkyKey as On, commitHash as Oo, mergeBeacon as Or, handoffHelloFrame as Os, shortAtprotoDid as Ot, encodeCommunityLink as P, REACTION_LIMITS as Pa, TURN_PUT_WINDOW_MS as Pc, recordsFromWellKnown as Pi, requestLnurlInvoice as Pn, DirectPathWatch as Po, readBeaconHead as Pr, handoffStreamKey as Ps, DidError as Pt, frameBytes as Q, appCardId as Qa, RelayTransport as Qc, receivedIdentityStatus as Qi, parseEngineError as Qn, callSignalHeardAt as Qo, readPin as Qr, verifySetUpdate as Qs, didDhtDocument as Qt, entryParams as R, reactionEmoji as Ra, turnPutSummary as Rc, CapsExchange as Ri, cashuRequestPayload as Rn, RELAY_POLL_INTERVALS as Ro, FILE_LIMITS as Rr, readHandoffCancel as Rs, didJwsAlg as Rt, readAppBundleAsync as S, parseVideoMeta as Sa, ownInviteRefusal as Sc, newMailbox as Si, isZ32Key as Sl, sparkInvoiceDetails as Sn, canonicalJson as So, beaconKeys as Sr, handoffCancelFrame as Ss, atprotoProofRecord as St, GroupTypingBoard as T, EDIT_BUFFER_MS as Ta, HANDOFF_CAPABILITY as Tc, decodeDnsResponse as Ti, toZ32 as Tl, isPubkyKey as Tn, GROUP_MEMBER_CAP as To, freshHubs as Tr, handoffDataFrame as Ts, isAtprotoDid as Tt, GROUP_WAKE_FRAME as U, pairedReplyAuthor as Ua, readTurnPacket as Uc, IDENTITY_MAX_VALIDITY as Ui, parseFedimintRequestPayload as Un, webLocalFetch as Uo, TypingSender as Ur, readHandoffTurnFrame as Us, didWebFileUrl as Ut, mergeKnocks as V, validReactionNumber as Va, firstDeviceSetSecret as Vc, dialDescriptors as Vi, isFederationId as Vn, formatLocalTarget as Vo, TYPING_KINDS as Vr, readHandoffHello as Vs, didWebFile as Vt, GroupWakeLimiter as W, readReply as Wa, signTurnPacket as Wc, IDENTITY_REVOCATION_LABEL as Wi, GhostLink as Wn, LIMITS as Wo, sanitizeTypingStatus as Wr, readHandoffVerified as Ws, didWebHost as Wt, GROUP_READ_NOTE as X, RUN_RESULTS as Xa, turnVerifierMark as Xc, identityStatement as Xi, engineError as Xn, buildSdpFromSignal as Xo, pinIsNewer as Xr, setUpdateFrame as Xs, staticDidDocument as Xt, GROUP_REACTED_FRAME as Y, PR_STATES as Ya, turnStatedVerifier as Yc, identityRevocationValue as Yi, pairedMessageFrame as Yn, answersOffer as Yo, nextPinNumber as Yr, setAckFrame as Ys, signingKeys as Yt, GroupSession as Z, STATUS_CARD_LIMITS as Za, DEFAULT_RELAYS as Zc, newIdentityBinding as Zi, engineText as Zn, callRtcConfig as Zo, pinNumberHolds as Zr, signTombstone as Zs, strictBase64Url as Zt, assembleAppBundle as _, VOICE_LIMITS as _a, createChatInvite as _c, HOLD_DATED_BACK_MS as _i, MAX_AVATAR_LENGTH as _l, validatePaymentTarget as _n, isAppRef as _o, MESH_HUBS as _r, filePartName as _s, ATPROTO_DECLARED_SCOPE as _t, planAppUpdate as a, automaticTransport as aa, enrollDigits as ac, wakeRequest as ai, withRequestOptions as al, EVM_TEST_CHAINS as an, arrivalKey as ao, isGroupMessageId as ar, serviceIdFromName as as, groupName as at, pacer as b, parseVoiceMeta as ba, inviteLink as bc, HoldRefusedError as bi, bytesEqual as bl, SPARK_PROVIDER as bn, signAppObject as bo, pickMeshHubs as br, handoffAttemptSucceeded as bs, ATPROTO_PROOF_SCOPE as bt, readAppStore as c, DHT_FILE_TOO_LARGE as ca, enrollHelloFrame as cc, APP_DATA_MAX_BYTES as ci, createRelayPayload as cl, SEPOLIA_TEST_USDT as cn, receivedTimestamp as co, readGroupWant as cr, HANDOFF_DONE as cs, SshSigError as ct, APP_BUNDLE_LIMITS as d, DHT_FILE_CAPABILITY as da, readEnrollHello as dc, APPS_CAPABILITY as di, parseRelayPayload as dl, USDT_PUBLIC_RPC as dn, APP_STATEMENT_LIMITS as do, GROUP_SIGNAL_FRAME as dr, HANDOFF_OFFER as ds, sameSshKey as dt, lastSharedWithMe as ea, ENROLL_GRANT as ec, WakeLimiter as ei, normalizeRelayUrl as el, encodeDidDhtDeactivated as en, cardLine as eo, GROUP_HAVE_FRAME as er, parseCallSignal as es, GROUP_EDIT_FRAME as et, APP_BUNDLE_MAGIC as f, dhtFileId as fa, verifyEnrollProof as fc, BUTTONS_CAPABILITY as fi, createIdentity as fl, WALLET_NETWORKS as fn, appFingerprint as fo, SIGNAL_FRESH_MS as fr, HANDOFF_PAKE as fs, sshSignCommand as ft, appViewOf as g, PLAYABLE_AUDIO as ga, OwnInviteError as gc, HOLD_CAPABILITY as gi, verify as gl, formatPaymentAmount as gn, isAppKey as go, readGroupSignal as gr, HANDOFF_VERIFIED as gs, decodeAnyBitcoinAddress as gt, appDigest as h, sanitizeMime as ha, readDeviceInvite as hc, UPGRADE_CAPABILITY as hi, publicKeyFromZ32 as hl, assertWholeSats as hn, isAppHash as ho, groupSignalFrame as hr, HANDOFF_TIMINGS as hs, BitcoinAddressError as ht, checkAppBeforeRun as i, TRANSPORTS as ia, enrollCancelReason as ic, relayRequest as ii, isDiscoveryBudgetError as il, ETHEREUM_USDT as in, statusCardText as io, groupFileFallback as ir, LEGACY_SERVICES as is, mentionsMember as it, GROUP_READ_NOTE_COMMUNITY as j, readForwarded as ja, newDeviceSetSecret as jc, nip05RootKey as ji, pubkyProofPath as jn, rosterHas as jo, pickHubs as jr, handoffPakeFrame as js, DID_DOCUMENT_MAX_BYTES as jt, COMMUNITY_LIMITS as k, parseLinkPreview as ka, deviceLinkPairing as kc, encodeDnsQuery as ki, pubkyProofCapability as kn, parseGroupCommit as ko, mergeLobby as kr, handoffManifestFrames as ks, AtprotoProofError as kt, readAppStoreSteps as l, DHT_MESSAGE_TTL as la, enrollProofFrame as lc, chatAppId as li, newerPacket as ll, SEPOLIA_TEST_USDT_FAUCET as ln, shownTime as lo, readGroupWantNo as lr, HANDOFF_HAVE as ls, parseSshPublicKey as lt, APP_PERMISSIONS as m, sanitizeFileName as ma, deviceInviteJoinerParams as mc, STATUS_CARD_CAPABILITY as mi, identityFromSeedB64 as ml, assertTokenUnits as mn, checkAppRevokeStatement as mo, SIGNAL_REPLY_MS as mr, HANDOFF_REQUEST as ms, verifyBitcoinMessage as mt, appStoreDecision as n, emptyProofLedger as na, ENROLL_PROOF as nc, newWakeToken as ni, traceLink as nl, resolveDidDht as nn, checkStatusCard as no, GROUP_WANT_NO_FRAME as nr, emptyLinkRecords as ns, carryMentions as nt, APP_STORE_LIMITS as o, irohDescriptor as oa, enrollDoneFrame as oc, checkPushEndpoint as oi, PacketTooLargeError as ol, ONCHAIN_PROVIDER as on, claimedTime as oo, readGroupFileMeta as or, HANDOFF_ACK as os, SSHSIG_MAX_ARMOR as ot, APP_ICON_PATH as p, formatFileSize as pa, createDeviceInvite as pc, EDIT_CAPABILITY as pi, identityFromSeed as pl, arkAddressPrefix as pn, appRef as po, SIGNAL_HOLD_MS as pr, HANDOFF_PIECE_BYTES as ps, verifySshSignature as pt, parseGroupWakeFrame as q, ITEM_STATES as qa, turnKeys as qc, boundVerifiedIdentity as qi, PAIRED_FRAME_TOO_LARGE as qn, CALL_SIGNAL_MAX_AGE_MS as qo, PIN_LIMITS as qr, SET_UPDATE as qs, parseDidDocument as qt, appUpdateDecision as r, validExternalKey as ra, enrollCancelFrame as rc, parseWakeFrame as ri, budgetRetryMs as rl, signDidDhtPacket as rn, sameButtonText as ro, fetchesByItself as rr, HTTP_SERVICE_PROTO as rs, parseCommunityEdit as rt, checkAppStoreIndex as s, irohRelayUrl as sa, enrollGrantFrame as sc, vapidKeysMatch as si, RELAY_PAYLOAD_MAX_BYTES as sl, PaymentPreflightError as sn, heardTime as so, readGroupHave as sr, HANDOFF_CANCEL as ss, SSHSIG_NAMESPACE as st, appRevocationFor as t, PeerProofs as ta, ENROLL_HELLO as tc, deviceWakeRequest as ti, setLinkTraceSink as tl, encodeDidDhtPacket as tn, cardUrl as to, GROUP_WANT_FRAME as tr, signalHasVideo as ts, GROUP_EDIT_TEXT_BYTES as tt, signAppStore as u, peekDhtMailbox as ua, readEnrollGrant as uc, isAppVersion as ui, openRelayPayload as ul, TEST_USDT_FAUCET_AMOUNT as un, APP_PREFIXES as uo, CarriedTransport as ur, HANDOFF_HELLO as us, parseSshSignature as ut, checkAppManifest as v, baseMime as va, decodeInviteCode as vc, HOLD_LIMITS as vi, sanitizeAvatar as vl, walletNetworkOf as vn, readAppRevocation as vo, mayBeHub as vr, handoffAttemptAllowed as vs, ATPROTO_FULL_SCOPE as vt, GROUP_TYPING_FRAME as w, readImageMeta as wa, ENROLL_CAPABILITY as wc, DOMAIN_FILE_MAX_BYTES as wi, toBase64Url as wl, PUBKY_PROOF_MAX_BYTES as wn, readCanonicalJson as wo, doorHubs as wr, handoffContextProof as ws, checkAtprotoProofRecord as wt, readAppBundle as x, voicePeaksOf as xa, inviteOwnership as xc, isHoldUrl as xi, fromBase64Url as xl, sparkAddressKind as xn, signAppRevocation as xo, COMMUNITY_TOPOLOGY as xr, handoffBusyFrame as xs, atprotoDidMethod as xt, isAppUrl as y, fileMessageText as ya, encodeInviteCode as yc, HoldKeys as yi, sanitizeDisplayText as yl, SPARK_NETWORKS as yn, readAppRevocations as yo, meshHubs as yr, handoffAttemptFailed as ys, ATPROTO_PROOF_COLLECTION as yt, knockIdentity as z, reactionIsNewer as za, TURN_LAST_SEQUENCE as zc, DHT_TEXT_CAPABILITY as zi, fedimintRequestPayload as zn, presenceSeenAt as zo, transferEnded as zr, readHandoffDone as zs, didSignedMessages as zt };
30990
31218
 
30991
- //# sourceMappingURL=src-DDzSERyc.js.map
31219
+ //# sourceMappingURL=src-D7gcJElT.js.map