@ghostlytools/cli 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ghostly.mjs CHANGED
@@ -50,7 +50,7 @@ var __exportAll = (all, no_symbols) => {
50
50
  };
51
51
  var package_default = {
52
52
  name: "@ghostlytools/cli",
53
- version: "1.0.1",
53
+ version: "1.0.2",
54
54
  type: "module",
55
55
  description: "Ghostly for bots and terminals: the app's own engine, headless on Node, behind a daemon and a `ghostly` command with JSON output.",
56
56
  license: "MIT",
@@ -94,7 +94,7 @@ var package_default = {
94
94
  "@scure/bip39": "2.4.0",
95
95
  "@scure/btc-signer": "2.4.1",
96
96
  "@secondts/bark": "0.25.0",
97
- "@synonymdev/pubky": "0.13.0",
97
+ "@synonymdev/pubky": "0.14.0",
98
98
  "@tetherto/wdk-wallet-evm": "1.0.0-beta.19",
99
99
  "b4a": "1.9.0",
100
100
  "bittorrent-dht": "11.0.12",
@@ -4032,7 +4032,87 @@ var LinkSession = class {
4032
4032
  }
4033
4033
  };
4034
4034
  //#endregion
4035
- //#region ../core/src/datalink.ts
4035
+ //#region ../core/src/directPath.ts
4036
+ /** Evidence older than this says nothing about the network now. */
4037
+ var DIRECT_EVIDENCE_MS = 18e5;
4038
+ /** Server reflexive candidates on one address with this many different ports: a mapping per destination. */
4039
+ var SYMMETRIC_PORTS = 3;
4040
+ /**
4041
+ * Whether a local description shows a NAT that maps each destination apart: the app asks several STUN servers from one
4042
+ * socket, and a NAT that keeps one mapping answers with one address and port (the browser lists it once). Three ports
4043
+ * on one address, not two: a device on two interfaces behind one router (Wi-Fi and a cable) has two.
4044
+ */
4045
+ function symmetricNat(sdp) {
4046
+ const ports = /* @__PURE__ */ new Map();
4047
+ for (const [, address, port] of (sdp ?? "").matchAll(/^a=candidate:\S+ \d+ udp \d+ (\S+) (\d+) typ srflx\b/gim)) {
4048
+ const seen = ports.get(address) ?? /* @__PURE__ */ new Set();
4049
+ ports.set(address, seen.add(port));
4050
+ }
4051
+ return [...ports.values()].some((seen) => seen.size >= SYMMETRIC_PORTS);
4052
+ }
4053
+ /** The rule alone: whether `failures` (every one after the last `open`) say direct connections are blocked at `now`. */
4054
+ function directBlocked(failures, now) {
4055
+ const fresh = failures.filter((f) => now - f.at < DIRECT_EVIDENCE_MS);
4056
+ if (fresh.filter((f) => f.kind !== "no-path").length >= 2) return true;
4057
+ return new Set(fresh.map((f) => f.peer)).size >= 3;
4058
+ }
4059
+ var DirectPathWatch = class {
4060
+ onChange;
4061
+ now;
4062
+ failures = [];
4063
+ /** Contacts a WebRTC connection is open with now. */
4064
+ open = /* @__PURE__ */ new Set();
4065
+ was = false;
4066
+ timer = null;
4067
+ /** `onChange`: `blocked` changed (evidence came, a connection opened, the network changed, the evidence aged out). */
4068
+ constructor(onChange = () => {}, now = Date.now) {
4069
+ this.onChange = onChange;
4070
+ this.now = now;
4071
+ }
4072
+ /** One WebRTC attempt with `peer` ended this way (or its open connection closed). */
4073
+ note(peer, evidence) {
4074
+ if (evidence === "open") {
4075
+ this.open.add(peer);
4076
+ this.failures = [];
4077
+ } else if (evidence === "closed") this.open.delete(peer);
4078
+ else this.failures = [...this.failures, {
4079
+ peer,
4080
+ kind: evidence,
4081
+ at: this.now()
4082
+ }].slice(-64);
4083
+ this.settle();
4084
+ }
4085
+ /** The network changed: what failed on the old one says nothing about this one. */
4086
+ reset() {
4087
+ this.failures = [];
4088
+ this.settle();
4089
+ }
4090
+ get blocked() {
4091
+ return !this.open.size && directBlocked(this.failures, this.now());
4092
+ }
4093
+ /** Stops the timer that lets the evidence age out. */
4094
+ close() {
4095
+ if (this.timer) clearTimeout(this.timer);
4096
+ this.timer = null;
4097
+ }
4098
+ settle() {
4099
+ this.close();
4100
+ const blocked = this.blocked;
4101
+ if (blocked) {
4102
+ const oldest = Math.min(...this.failures.map((f) => f.at));
4103
+ this.timer = setTimeout(() => {
4104
+ this.failures = this.failures.filter((f) => this.now() - f.at < DIRECT_EVIDENCE_MS);
4105
+ this.settle();
4106
+ }, Math.max(1e3, oldest + DIRECT_EVIDENCE_MS - this.now()));
4107
+ this.timer.unref?.();
4108
+ }
4109
+ if (blocked === this.was) return;
4110
+ this.was = blocked;
4111
+ this.onChange(blocked);
4112
+ }
4113
+ };
4114
+ /** A candidate another network can reach: server reflexive (STUN answered), relayed (TURN), or learnt from the peer. */
4115
+ var PUBLIC_CANDIDATE = /^a=candidate:.* typ (srflx|relay|prflx)\b/m;
4036
4116
  var CONNECT_TIMEOUT_MS = 9e4;
4037
4117
  var ICE_GATHERING_TIMEOUT_MS = 5e3;
4038
4118
  /**
@@ -4052,6 +4132,8 @@ var DataLink = class {
4052
4132
  lastSignalTs = 0;
4053
4133
  /** The peer's description as this connection was given it: Chrome shows it as `remoteDescription` only once applied. */
4054
4134
  remoteSdp = null;
4135
+ /** When the connection had both descriptions (`connecting`). */
4136
+ connectingSince = 0;
4055
4137
  connectTimer = null;
4056
4138
  disconnectTimer = null;
4057
4139
  /** The offer this side answered in the current attempt, and how many times it answered it again. */
@@ -4093,7 +4175,10 @@ var DataLink = class {
4093
4175
  this.lastSignalTs = signal.ts;
4094
4176
  const wasOpen = this.state === "open";
4095
4177
  this.teardown();
4096
- if (wasOpen) this.options.onClose();
4178
+ if (wasOpen) {
4179
+ this.options.onDirect?.("closed");
4180
+ this.options.onClose();
4181
+ }
4097
4182
  this.answered = {
4098
4183
  offer: signal,
4099
4184
  again: 0
@@ -4134,8 +4219,20 @@ var DataLink = class {
4134
4219
  }
4135
4220
  }
4136
4221
  close() {
4222
+ const evidence = Date.now() - this.connectingSince >= 5e3 ? this.directEvidence() : null;
4137
4223
  this.answered = null;
4138
4224
  this.reset();
4225
+ if (evidence) this.options.onDirect?.(evidence);
4226
+ }
4227
+ /**
4228
+ * What this attempt says about direct connections if it ends now without opening: nothing unless both descriptions
4229
+ * were exchanged (this side's offer answered, or its answer out). Then, what this device gathered: no public
4230
+ * candidate, a public port per STUN server, or (for the side that offered) candidates that read as reachable.
4231
+ */
4232
+ directEvidence() {
4233
+ if (this.state !== "connecting" || !this.pc) return null;
4234
+ const local = this.pc.localDescription?.sdp ?? "";
4235
+ return !PUBLIC_CANDIDATE.test(local) ? "no-public" : symmetricNat(local) ? "symmetric" : this.myOfferTs ? "no-path" : null;
4139
4236
  }
4140
4237
  /**
4141
4238
  * The attempt on this connection failed (it timed out, ICE failed or closed, its channel closed). An answer the
@@ -4145,6 +4242,7 @@ var DataLink = class {
4145
4242
  failed() {
4146
4243
  const answered = this.state === "connecting" ? this.answered : null;
4147
4244
  const left = answered ? CONNECT_TIMEOUT_MS - (Date.now() - answered.offer.ts) : 0;
4245
+ const evidence = this.directEvidence();
4148
4246
  if (answered && answered.again < 2 && left > REANSWER_MARGIN_MS && this.options.offerStanding?.(answered.offer.ts)) {
4149
4247
  answered.again++;
4150
4248
  traceLink(this.options.myPubKeyZ32, "reanswer", {
@@ -4156,6 +4254,7 @@ var DataLink = class {
4156
4254
  return;
4157
4255
  }
4158
4256
  this.reset();
4257
+ if (evidence) this.options.onDirect?.(evidence);
4159
4258
  }
4160
4259
  async answer(offer) {
4161
4260
  this.setState("answering");
@@ -4213,6 +4312,7 @@ var DataLink = class {
4213
4312
  });
4214
4313
  if (attempt >= 3) {
4215
4314
  this.reset();
4315
+ this.options.onDirect?.("no-public");
4216
4316
  return null;
4217
4317
  }
4218
4318
  stalled = pc;
@@ -4233,6 +4333,7 @@ var DataLink = class {
4233
4333
  this.setState("open");
4234
4334
  this.options.setFastPoll(false);
4235
4335
  this.options.publishSignal(null);
4336
+ this.options.onDirect?.("open");
4236
4337
  this.options.onOpen(wrapDataChannel(dc));
4237
4338
  });
4238
4339
  dc.addEventListener("close", () => {
@@ -4295,11 +4396,15 @@ var DataLink = class {
4295
4396
  this.setState("idle");
4296
4397
  this.options.setFastPoll(false);
4297
4398
  this.options.publishSignal(null);
4298
- if (wasOpen) this.options.onClose();
4399
+ if (wasOpen) {
4400
+ this.options.onDirect?.("closed");
4401
+ this.options.onClose();
4402
+ }
4299
4403
  }
4300
4404
  setState(state) {
4301
4405
  if (this.state === state) return;
4302
4406
  this.state = state;
4407
+ if (state === "connecting") this.connectingSince = Date.now();
4303
4408
  this.options.onState?.(state);
4304
4409
  }
4305
4410
  };
@@ -4575,19 +4680,43 @@ function verifyChain(raw, groupId) {
4575
4680
  //#endregion
4576
4681
  //#region ../core/src/messageTime.ts
4577
4682
  /**
4578
- * How far ahead of this device's clock a received message may say it was sent: clocks drift, but a time past this
4579
- * is the peer's choice, not its clock.
4683
+ * How far ahead of this device's clock a number a peer made from its clock may be (a pin's number, a status card's
4684
+ * times): clocks drift, but a time past this is the peer's choice, not its clock.
4580
4685
  */
4581
4686
  var MESSAGE_CLOCK_SKEW_MS = 3e5;
4582
4687
  /**
4583
- * The time a received message is kept under. A peer picks the `ts` of what it sends, and one far in the future
4584
- * would pin its chat to the top of the list (and past 8.64e15 ms no date can hold it): it is taken as the moment
4585
- * of receipt at the latest. A time that is not a positive number is taken as now.
4688
+ * The time a line of a group's history is kept under (a membership change, a rename): what its commit says, but at
4689
+ * most `MESSAGE_CLOCK_SKEW_MS` past this clock, and now when it is not a positive number. Messages do not use it:
4690
+ * they are placed where they arrive (below).
4586
4691
  */
4587
4692
  function receivedTimestamp(ts, now = Date.now()) {
4588
4693
  if (!Number.isFinite(ts) || ts <= 0) return now;
4589
4694
  return Math.min(ts, now + MESSAGE_CLOCK_SKEW_MS);
4590
4695
  }
4696
+ /** The latest moment a date can hold. */
4697
+ var MAX_DATE_MS = 864e13;
4698
+ /** The time a sender says, as it is kept: a positive time a date can hold, or nothing. */
4699
+ function claimedTime(ts) {
4700
+ return typeof ts === "number" && Number.isFinite(ts) && ts > 0 && ts <= MAX_DATE_MS ? ts : void 0;
4701
+ }
4702
+ /**
4703
+ * The place of a row stored now: this clock, and past the last place given in its chat, so rows that come in the
4704
+ * same millisecond (a hold pickup, a group's catch-up) keep the order they came in.
4705
+ */
4706
+ function arrivalKey(now, last = 0) {
4707
+ return Math.max(now, last + 1);
4708
+ }
4709
+ /** The time a message shows: what its sender said, never later than when it came. A row of mine shows its own time. */
4710
+ function shownTime(message) {
4711
+ return message.sentAt === void 0 ? message.timestamp : Math.min(message.sentAt, message.timestamp);
4712
+ }
4713
+ /**
4714
+ * When something a peer dated happened (an edit), for this device: what the peer says, now at the latest, and now
4715
+ * when it says nothing a date can hold.
4716
+ */
4717
+ function heardTime(ts, now = Date.now()) {
4718
+ return Math.min(claimedTime(ts) ?? now, now);
4719
+ }
4591
4720
  //#endregion
4592
4721
  //#region ../core/src/statusCards.ts
4593
4722
  var STATUS_CARD_LIMITS = {
@@ -6945,6 +7074,10 @@ var PairedFiles = class {
6945
7074
  this.channel = channel;
6946
7075
  this.events = events;
6947
7076
  }
7077
+ /** A file going either way on this session right now. */
7078
+ get busy() {
7079
+ return this.incoming.size > 0 || this.outgoing.size > 0;
7080
+ }
6948
7081
  sendFrame(frame) {
6949
7082
  if (this.closed) throw new Error("Connection lost");
6950
7083
  this.channel.send(JSON.stringify(frame));
@@ -7196,6 +7329,31 @@ var TRANSPORTS = [
7196
7329
  "hyperdht/1",
7197
7330
  "webrtc/1"
7198
7331
  ];
7332
+ function irohRelayUrl(value, spelling = "dotted") {
7333
+ let url;
7334
+ try {
7335
+ url = new URL(value);
7336
+ } catch {
7337
+ return value;
7338
+ }
7339
+ const host = url.hostname, name = host.replace(/\.$/, "");
7340
+ if (!name.includes(".") && name !== "localhost") return value;
7341
+ if (name.startsWith("[") || /^\d+(\.\d+){3}$/.test(name)) return value;
7342
+ const wanted = spelling === "plain" || name === "localhost" ? name : `${name}.`;
7343
+ if (wanted === host) return value;
7344
+ url.hostname = wanted;
7345
+ return url.href;
7346
+ }
7347
+ /** An Iroh descriptor with its relay in one spelling (`irohRelayUrl`); anything else as it is. */
7348
+ function irohDescriptor(descriptor, spelling = "dotted") {
7349
+ const relay = descriptor?.relay;
7350
+ if (typeof relay !== "string") return descriptor;
7351
+ const canonical = irohRelayUrl(relay, spelling);
7352
+ return canonical === relay ? descriptor : {
7353
+ ...descriptor,
7354
+ relay: canonical
7355
+ };
7356
+ }
7199
7357
  /**
7200
7358
  * Relay only (WISP 100, "Relayed"): the transport reaches this contact only through a relay server, never
7201
7359
  * directly. A descriptor says so with `relayed: true`, and an Iroh descriptor with no direct address is relayed
@@ -10044,7 +10202,7 @@ function vapidKeysMatch(keys) {
10044
10202
  }
10045
10203
  /**
10046
10204
  * The push services a browser subscribes with: Google (Chrome, Edge on Android, Brave, Opera), Apple (Safari), Mozilla
10047
- * (Firefox), Microsoft (Edge on Windows). The same list as the push relay's (services/push-relay/relay.mjs).
10205
+ * (Firefox), Microsoft (Edge on Windows). The same list as the push relay's (infra/services/push-relay/relay.mjs).
10048
10206
  */
10049
10207
  var PUSH_SERVICE_HOSTS = [
10050
10208
  /^fcm\.googleapis\.com$/,
@@ -11633,6 +11791,13 @@ function parsePairedCallFrame(frame, now = Date.now()) {
11633
11791
  if (typeof signal !== "string" || signal.length > 8192) return null;
11634
11792
  return parseCallSignal(signal, now) ? signal : null;
11635
11793
  }
11794
+ function signalKind(signal) {
11795
+ try {
11796
+ return JSON.parse(signal).t;
11797
+ } catch {
11798
+ return;
11799
+ }
11800
+ }
11636
11801
  /**
11637
11802
  * This side's latest call signal. What could not go (the session dropped between two frames) goes on the
11638
11803
  * next session while it is still fresh, as the `_call` record of a compatibility chat is read on the next
@@ -11641,11 +11806,16 @@ function parsePairedCallFrame(frame, now = Date.now()) {
11641
11806
  var PairedCalls = class {
11642
11807
  now;
11643
11808
  latest = null;
11809
+ /** When this side last said anything about a call, a clear included. */
11810
+ saidAt = 0;
11811
+ /** The contact's latest signal on a session: its kind, and when it came. */
11812
+ peer = null;
11644
11813
  constructor(now = Date.now) {
11645
11814
  this.now = now;
11646
11815
  }
11647
11816
  /** A new signal to send (null clears it). Returns the frame to send now, if any. */
11648
11817
  set(signal) {
11818
+ this.saidAt = this.now();
11649
11819
  if (signal === null) {
11650
11820
  this.latest = null;
11651
11821
  return null;
@@ -11653,13 +11823,34 @@ var PairedCalls = class {
11653
11823
  if (signal.length > 8192) throw new Error("Call signal too large");
11654
11824
  this.latest = {
11655
11825
  signal,
11656
- at: this.now()
11826
+ at: this.saidAt
11657
11827
  };
11658
11828
  return {
11659
11829
  t: PAIRED_CALL_FRAME,
11660
11830
  s: signal
11661
11831
  };
11662
11832
  }
11833
+ /** A signal the contact sent (one `parsePairedCallFrame` took). */
11834
+ heard(signal) {
11835
+ this.peer = {
11836
+ kind: signalKind(signal),
11837
+ at: this.now()
11838
+ };
11839
+ }
11840
+ /**
11841
+ * Whether a call is on, as the latest signal of either side says: an answer or a picture change, or an offer still
11842
+ * fresh enough to ring. A hang-up, a clear, or an offer nobody answered in time ends it. Read by kind, as `hangUp` is:
11843
+ * the media runs on a connection of its own, so an hour-long call shows nothing else on the session.
11844
+ */
11845
+ get on() {
11846
+ const own = {
11847
+ kind: this.latest ? signalKind(this.latest.signal) : "h",
11848
+ at: this.saidAt
11849
+ };
11850
+ const last = this.peer && this.peer.at > own.at ? this.peer : own;
11851
+ if (last.kind === "a" || last.kind === "v") return true;
11852
+ return last.kind === "o" && this.now() - last.at <= 12e4;
11853
+ }
11663
11854
  /**
11664
11855
  * This app is going away: a call it placed or answered is hung up, as its person would (its latest signal an
11665
11856
  * offer, an answer, or a picture change, which only a call on sends). Returns the hang-up to send before the
@@ -11799,14 +11990,16 @@ function unpack(bytes, withLoad, max) {
11799
11990
  /**
11800
11991
  * The hubs that take turns at the door (WISP 9xx § Admission): those that said so lately and have
11801
11992
  * been hubs for a minute, so that every hub, having read the beacon at least once since, agrees on
11802
- * the same set. Only when there is none, every hub that said so lately. And when none did, a hub asking (`me`)
11993
+ * the same set. Only when there is none (a group's first minute), the one that has been a hub longest. And when none said so lately, a hub asking (`me`)
11803
11994
  * that sees no other fresh hub in the beacon is the door alone, listed or not: its own entry may not have reached the
11804
11995
  * relays yet (a new group's first hub, whose write the relays' budget holds back), and there is nobody to agree with.
11805
11996
  */
11806
11997
  function doorHubs(hubs, now = Date.now(), me) {
11807
11998
  const lately = hubs.filter((h) => now - h.ts < COMMUNITY_TOPOLOGY.beaconEveryMs * 1.5);
11808
11999
  const settled = lately.filter((h) => now - (h.since ?? h.ts) >= 6e4);
11809
- const door = (settled.length ? settled : lately).map((h) => h.key);
12000
+ const since = (h) => Math.floor((h.since ?? h.ts) / 1e3);
12001
+ const first = [...lately].sort((a, b) => since(a) - since(b) || (a.key < b.key ? -1 : 1)).slice(0, 1);
12002
+ const door = (settled.length ? settled : first).map((h) => h.key);
11810
12003
  if (door.length || me === void 0 || freshHubs(hubs, now).some((h) => h.key !== me)) return door;
11811
12004
  return [me];
11812
12005
  }
@@ -12222,6 +12415,20 @@ var RACE_DIRECT_MS = 8e3;
12222
12415
  * over a relay stays there (no probing while live, WISP 100), where the answer would have made it direct.
12223
12416
  */
12224
12417
  var RACE_RELAYED_MS = 4e4;
12418
+ /**
12419
+ * A WebRTC offer this side made that was answered, and has not connected this long after the answer was applied: where
12420
+ * a path exists ICE finds it in a second or two, so there is none (a VPN's NAT, a firewall that drops UDP, and no TURN
12421
+ * server), and every transport ranked after WebRTC is dialled now, the relayed ones too, the attempt still standing.
12422
+ * Without it the chat waited for ICE to give up (about 15 s in Chromium, the attempt's 90 s where the connection never
12423
+ * says `failed`), then for the next attempt.
12424
+ */
12425
+ var RACE_ANSWERED_MS = 6e3;
12426
+ /**
12427
+ * WebRTC that could not start an attempt at all (no candidate gathered: UDP blocked, a VPN's browser extension; or no
12428
+ * connection could be made) is tried last for this long: each try costs its stall time (`GATHER_STALL_MS` three times
12429
+ * over) before the transport that works is dialled.
12430
+ */
12431
+ var RTC_UNSTARTED_MS = 6e5;
12225
12432
  /** A transport due in the race that cannot be dialled yet (its endpoint still starting, no address of the contact's) is looked at again this often. */
12226
12433
  var RACE_RETRY_MS = 2e3;
12227
12434
  var DEMOTE_MS = 36e5;
@@ -12249,6 +12456,35 @@ var LIVENESS_PING_MS = 15e3;
12249
12456
  */
12250
12457
  var SWITCH_RETIRE_MS = 3e3;
12251
12458
  /**
12459
+ * Frames that keep a session going rather than say something of the two people's: what the contact sent last outside
12460
+ * these is when the chat was last in use (`canYieldEndpoint`). Every other frame (a text, a receipt, typing, a
12461
+ * reaction, an edit, a file, a call, a payment) is use; what this side sends is answered with one of them.
12462
+ */
12463
+ /** How long a session giving its listener up waits after its goodbye before it closes, so the goodbye gets there. */
12464
+ var YIELD_BYE_MS = 300;
12465
+ var QUIET_FRAMES = /* @__PURE__ */ new Set([
12466
+ "paired-ping",
12467
+ "paired-pong",
12468
+ "paired-bye",
12469
+ "paired-policy",
12470
+ "paired-switch-plan",
12471
+ "paired-switch-ready",
12472
+ "paired-switch-failed",
12473
+ "paired-switch-keep",
12474
+ "paired-rtc",
12475
+ "paired-adapters",
12476
+ "paired-nick",
12477
+ "paired-avatar",
12478
+ "paired-capabilities",
12479
+ "paired-wake",
12480
+ "paired-hold",
12481
+ "paired-payments",
12482
+ "paired-groups",
12483
+ "paired-services",
12484
+ "paired-reconnect",
12485
+ "paired-reconnect-ack"
12486
+ ]);
12487
+ /**
12252
12488
  * A ping with nothing at all back this long after it went: the contact may have crashed and started again, with its new
12253
12489
  * offer waiting on the relays, while this side still holds the old session and reads them at a live chat's pace (30 s).
12254
12490
  * They are read now. Nothing more: the session is let go only by `LIVENESS_MISSED_PINGS`, or by the contact's offer
@@ -12300,7 +12536,8 @@ function recordDescriptor(transport, known, record, newer, listed) {
12300
12536
  if (!known) return record;
12301
12537
  const k = known, r = record;
12302
12538
  if (transport === "iroh/1" ? k.id !== r.id : k.publicKey !== r.publicKey) return newer ? record : known;
12303
- if (!r.relay || r.relay === k.relay || k.relay && !newer) return known;
12539
+ const same = r.relay === k.relay || transport === "iroh/1" && typeof r.relay === "string" && typeof k.relay === "string" && irohRelayUrl(r.relay) === irohRelayUrl(k.relay);
12540
+ if (!r.relay || same || k.relay && !newer) return known;
12304
12541
  return transport === "iroh/1" ? {
12305
12542
  ...k,
12306
12543
  relay: r.relay
@@ -12440,6 +12677,14 @@ var GhostLink = class {
12440
12677
  racing = false;
12441
12678
  /** The WebRTC offer this side's dial made last, with fallback allowed: what a transport that comes up meanwhile joins. */
12442
12679
  offered;
12680
+ /** When that offer's answer was applied (`RACE_ANSWERED_MS`); 0 while the data link is not connecting on it. */
12681
+ answeredAt = 0;
12682
+ /** This side's offer is out unanswered, or answered and not connected yet: what is ranked after WebRTC may race it. */
12683
+ get offerOut() {
12684
+ return this.dataLink.state === "offering" || this.dataLink.state === "connecting" && this.answeredAt > 0;
12685
+ }
12686
+ /** The attempt (`connectionEpoch`) in which the data link began answering the contact's offer. */
12687
+ answeringEpoch = -1;
12443
12688
  lastAutoConnectAt = 0;
12444
12689
  autoConnectFailures = 0;
12445
12690
  /** Native transports a record just read listed with no way to dial them (`record-undescribed`), until one describes them again. */
@@ -12459,6 +12704,10 @@ var GhostLink = class {
12459
12704
  peerSeenAt = 0;
12460
12705
  /** When the channel carrying the chat now was attached. */
12461
12706
  channelSince = 0;
12707
+ /** When the contact last sent something other than the session's upkeep (`QUIET_FRAMES`) on a session. */
12708
+ activityAt = 0;
12709
+ /** Set while `yieldEndpoint` ends the session to let its listener go: nothing dials meanwhile. */
12710
+ yielding = false;
12462
12711
  /**
12463
12712
  * The contact said goodbye on the live session (`paired-bye`): its app is going away, likely to restart. Its packet
12464
12713
  * then was `packet`; it is dialled again once a newer one shows it back, or after `until`.
@@ -12660,6 +12909,7 @@ var GhostLink = class {
12660
12909
  setFastPoll: (fast, offer) => this.session.setFastPoll(fast, !!offer && !!options.pairing?.credentials.peerKey),
12661
12910
  offerStanding: (ts) => !this.leaving && !this.stopped && !this.streamBlocked && this.peerOfferTs() === ts,
12662
12911
  onAnswerReplaced: () => this.redial(),
12912
+ onDirect: (evidence) => events.onDirectEvidence?.(evidence),
12663
12913
  onOpen: (channel) => {
12664
12914
  if (this.streamBlocked || this.keyStopped) {
12665
12915
  channel.close();
@@ -12677,12 +12927,21 @@ var GhostLink = class {
12677
12927
  if (wasLive) this.peerLost("closed");
12678
12928
  },
12679
12929
  onState: (state) => {
12930
+ const was = this.lastDataLinkState;
12680
12931
  traceLink(this.myPubKeyZ32, "datalink", { state });
12681
12932
  this.trackDataLink(state);
12682
12933
  if (this.activeBinding) return;
12683
12934
  events.onDataLinkState?.(state);
12684
12935
  if (state === "open") this.afterRtc = void 0;
12936
+ if (state === "answering") this.answeringEpoch = this.connectionEpoch;
12937
+ this.answeredAt = 0;
12938
+ if (state === "connecting" && was === "offering" && this.offered?.epoch === this.connectionEpoch) {
12939
+ this.answeredAt = Date.now();
12940
+ this.scheduleRace(this.offered.epoch, this.offered.at);
12941
+ }
12685
12942
  if (state !== "idle") return;
12943
+ const unanswerable = was === "answering" && this.answeringEpoch === this.connectionEpoch && !this.channel && !this.stopped && !this.leaving;
12944
+ if (unanswerable) this.rtcUnstarted();
12686
12945
  this.clearRace();
12687
12946
  if (this.racing) return;
12688
12947
  const next = this.afterRtc;
@@ -12695,6 +12954,7 @@ var GhostLink = class {
12695
12954
  this.afterRtc = void 0;
12696
12955
  if (!this.dialing) this.attemptEnded();
12697
12956
  this.rejectWaiters(new GhostlyHttpError("unreachable", "Could not connect to the peer"));
12957
+ if (unanswerable && this.dialsPastRtc()) this.redial();
12698
12958
  },
12699
12959
  attemptTimeoutMs: () => this.tracker && !this.tracker.done ? PAIRING_ATTEMPT_MS : void 0,
12700
12960
  onDisconnected: (disconnected) => {
@@ -13542,6 +13802,46 @@ var GhostLink = class {
13542
13802
  this.advertiseTransports();
13543
13803
  this.options.events?.onTransportsChanged?.();
13544
13804
  }
13805
+ /** When the chat was last in use on its session: the contact's last frame of use, or the session's start. */
13806
+ get lastActivityAt() {
13807
+ return Math.max(this.activityAt, this.channelSince);
13808
+ }
13809
+ /**
13810
+ * Whether the live session on this transport may end so another chat takes its listener (`yieldEndpoint`): open, and
13811
+ * unused, for `idleMs` at least (a session that just started has been in use for none of it); no call on, no file
13812
+ * moving, nothing being dialled or switched. `canReleaseEndpoint` never gives up a live session; this does, once idle.
13813
+ */
13814
+ canYieldEndpoint(transport, idleMs) {
13815
+ if (this.activeBinding?.transport !== transport || !this.isDataLinkOpen || this.paired?.state.status !== "ready") return false;
13816
+ if (this.candidate || this.dialing || this.switcher.pending || this.transitionTarget) return false;
13817
+ if (this.pairedCalls.on || this.pairedFiles?.busy) return false;
13818
+ return Date.now() - this.lastActivityAt >= idleMs;
13819
+ }
13820
+ /**
13821
+ * Ends the idle live session on this transport and lets its listener go, for a chat in use that found every one
13822
+ * taken. The contact hears a goodbye, as when an app quits, rather than finding out when its pings go unanswered; the
13823
+ * record then lists the transport undescribed, so it stops dialling here. The chat goes on over the DHT (or WebRTC),
13824
+ * and takes a listener again the same way once it is in use. False when the session may not end (see above).
13825
+ */
13826
+ async yieldEndpoint(transport, idleMs) {
13827
+ if (!this.canYieldEndpoint(transport, idleMs)) return false;
13828
+ traceLink(this.myPubKeyZ32, "native-yield", {
13829
+ transport,
13830
+ idle: Date.now() - this.lastActivityAt
13831
+ });
13832
+ this.yielding = true;
13833
+ try {
13834
+ try {
13835
+ this.channel?.send(JSON.stringify({ t: "paired-bye" }));
13836
+ } catch {}
13837
+ await new Promise((resolve) => setTimeout(resolve, YIELD_BYE_MS));
13838
+ this.disconnect();
13839
+ await this.releaseEndpoint(transport);
13840
+ } finally {
13841
+ this.yielding = false;
13842
+ }
13843
+ return !this.endpoints.has(transport);
13844
+ }
13545
13845
  advertiseTransports() {
13546
13846
  this.publishPacketTransports();
13547
13847
  if (this.paired?.state.status !== "ready" || !this.options.params.profile) return;
@@ -13641,11 +13941,15 @@ var GhostLink = class {
13641
13941
  at: offeredAt
13642
13942
  } : void 0;
13643
13943
  await this.dataLink.connect();
13644
- if (!this.afterRtc || this.dataLink.state !== "idle" || epoch !== this.connectionEpoch) {
13944
+ if (this.dataLink.state !== "idle" || epoch !== this.connectionEpoch) {
13645
13945
  this.scheduleRace(epoch, offeredAt);
13646
13946
  return;
13647
13947
  }
13948
+ this.rtcUnstarted();
13949
+ const next = this.afterRtc;
13950
+ if (!next) return;
13648
13951
  this.afterRtc = void 0;
13952
+ for (const joined of next.rest) if (!ordered.includes(joined)) ordered.push(joined);
13649
13953
  continue;
13650
13954
  }
13651
13955
  const result = await this.dialNative(transport, epoch);
@@ -13730,9 +14034,10 @@ var GhostLink = class {
13730
14034
  scheduleRace(epoch, offeredAt) {
13731
14035
  this.clearRace();
13732
14036
  const next = this.afterRtc;
13733
- if (!next || next.epoch !== epoch || epoch !== this.connectionEpoch || this.dataLink.state !== "offering") return;
14037
+ if (!next || next.epoch !== epoch || epoch !== this.connectionEpoch || !this.offerOut) return;
13734
14038
  const relayed = this.relayedTransports, now = Date.now();
13735
- const at = next.rest.filter((t) => t !== "webrtc/1").map((t) => Math.max(offeredAt + (relayed.includes(t) ? RACE_RELAYED_MS : RACE_DIRECT_MS), this.canDial(t) ? 0 : now + RACE_RETRY_MS));
14039
+ const answered = this.answeredAt ? this.answeredAt + RACE_ANSWERED_MS : Infinity;
14040
+ const at = next.rest.filter((t) => t !== "webrtc/1").map((t) => Math.max(Math.min(offeredAt + (relayed.includes(t) ? RACE_RELAYED_MS : RACE_DIRECT_MS), answered), this.canDial(t) ? 0 : now + RACE_RETRY_MS));
13736
14041
  if (!at.length) return;
13737
14042
  this.raceTimer = setTimeout(() => {
13738
14043
  this.raceTimer = null;
@@ -13745,7 +14050,7 @@ var GhostLink = class {
13745
14050
  */
13746
14051
  joinRace(transport) {
13747
14052
  const offered = this.offered;
13748
- if (!offered || offered.epoch !== this.connectionEpoch || this.dataLink.state !== "offering" || this.channel) return;
14053
+ if (!offered || offered.epoch !== this.connectionEpoch || !this.offerOut || this.channel) return;
13749
14054
  if (!this.peerFallback || !this.peerTransports?.includes(transport) || !this.transportOffer().includes(transport)) return;
13750
14055
  const rest = this.afterRtc?.epoch === offered.epoch ? this.afterRtc.rest : [];
13751
14056
  if (rest.includes(transport)) return;
@@ -13756,6 +14061,19 @@ var GhostLink = class {
13756
14061
  traceLink(this.myPubKeyZ32, "race-join", { transport });
13757
14062
  if (!this.racing) this.scheduleRace(offered.epoch, offered.at);
13758
14063
  }
14064
+ /** WebRTC could not start an attempt here: it goes last in the dial order for `RTC_UNSTARTED_MS`, while another transport remains. */
14065
+ rtcUnstarted() {
14066
+ traceLink(this.myPubKeyZ32, "rtc-unstarted", {});
14067
+ this.demotedUntil.set("webrtc/1", Date.now() + RTC_UNSTARTED_MS);
14068
+ }
14069
+ /**
14070
+ * Whether this side dials although its key says it waits for the contact's offer: WebRTC cannot start here for now
14071
+ * (`rtcUnstarted`), so an offer of the contact's finds no answer, and this side has a native transport to dial, with
14072
+ * fallback allowed on both sides.
14073
+ */
14074
+ dialsPastRtc() {
14075
+ return (this.demotedUntil.get("webrtc/1") ?? 0) > Date.now() && this.fallback && this.peerFallback && TRANSPORTS.some((t) => this.canDial(t) && !!this.peerTransports?.includes(t) && this.transportOffer().includes(t));
14076
+ }
13759
14077
  /** Whether a native dial of `transport` can go now: this side's endpoint is up and the contact's descriptor known. */
13760
14078
  canDial(transport) {
13761
14079
  return transport !== "webrtc/1" && this.endpoints.has(transport) && !!this.peerDescriptors[transport];
@@ -13771,8 +14089,9 @@ var GhostLink = class {
13771
14089
  */
13772
14090
  async race(epoch, offeredAt) {
13773
14091
  const next = this.afterRtc;
13774
- if (!next || next.epoch !== epoch || epoch !== this.connectionEpoch || this.channel || this.dialing || this.stopped || this.leaving || this.streamBlocked || this.keyStopped || this.dataLink.state !== "offering") return;
13775
- const relayed = this.relayedTransports, late = Date.now() - offeredAt >= RACE_RELAYED_MS;
14092
+ if (!next || next.epoch !== epoch || epoch !== this.connectionEpoch || this.channel || this.dialing || this.stopped || this.leaving || this.streamBlocked || this.keyStopped || !this.offerOut) return;
14093
+ const relayed = this.relayedTransports;
14094
+ const late = Date.now() - offeredAt >= 4e4 || this.answeredAt > 0 && Date.now() - this.answeredAt >= 6e3;
13776
14095
  const due = next.rest.filter((t) => this.canDial(t) && (late || !relayed.includes(t)));
13777
14096
  const left = next.rest.filter((t) => t !== "webrtc/1" && !due.includes(t));
13778
14097
  if (!due.length) {
@@ -14014,7 +14333,7 @@ var GhostLink = class {
14014
14333
  * the lower-key rule settles any collision, on both sides.
14015
14334
  */
14016
14335
  maybeAutoConnect(presence) {
14017
- if (this.stopped || this.leaving || this.streamBlocked || this.keyStopped || !this.options.autoConnect || !presence.online || this.channel || this.dialing || this.dataLink.state !== "idle") return;
14336
+ if (this.stopped || this.leaving || this.yielding || this.streamBlocked || this.keyStopped || !this.options.autoConnect || !presence.online || this.channel || this.dialing || this.dataLink.state !== "idle") return;
14018
14337
  if (this.resuming && this.resuming !== "webrtc/1" && !this.endpoints.has(this.resuming) && Date.now() < this.resumeWaitUntil) return;
14019
14338
  if (this.departed) {
14020
14339
  if (presence.lastPacketAt === this.departed.packet && Date.now() < this.departed.until) return;
@@ -14027,7 +14346,7 @@ var GhostLink = class {
14027
14346
  this.session.expectPeer();
14028
14347
  return;
14029
14348
  }
14030
- } else if (role !== "joiner" && this.myPubKeyZ32 > this.options.params.peerPubKeyZ32 && !this.resuming) {
14349
+ } else if (role !== "joiner" && this.myPubKeyZ32 > this.options.params.peerPubKeyZ32 && !this.resuming && !this.dialsPastRtc()) {
14031
14350
  if (Date.now() - presence.lastPacketAt < 3e4 && presence.lastPacketAt !== this.offerAwaitedFor) {
14032
14351
  this.offerAwaitedFor = presence.lastPacketAt;
14033
14352
  this.session.expectPeer();
@@ -14954,6 +15273,7 @@ var GhostLink = class {
14954
15273
  } catch {
14955
15274
  return;
14956
15275
  }
15276
+ if (frame && typeof frame === "object" && !QUIET_FRAMES.has(String(frame.t))) this.activityAt = Date.now();
14957
15277
  if (frame?.t === "paired-ping") {
14958
15278
  try {
14959
15279
  channel.send(JSON.stringify({ t: "paired-pong" }));
@@ -15125,6 +15445,7 @@ var GhostLink = class {
15125
15445
  }
15126
15446
  if (frame?.t === "paired-call") {
15127
15447
  const signal = this.supportsCalls ? parsePairedCallFrame(frame) : null;
15448
+ if (signal) this.pairedCalls.heard(signal);
15128
15449
  if (signal) this.options.events?.onCallSignal?.(signal);
15129
15450
  return;
15130
15451
  }
@@ -15326,7 +15647,7 @@ var GhostLink = class {
15326
15647
  * once, and the contact is watched closely for a while, since an app that went away is usually back in seconds.
15327
15648
  */
15328
15649
  peerLost(why) {
15329
- if (this.stopped || this.leaving) return;
15650
+ if (this.stopped || this.leaving || this.yielding) return;
15330
15651
  traceLink(this.myPubKeyZ32, "peer-lost", { why });
15331
15652
  this.lostUntil = Date.now() + WATCH_PEER_MS;
15332
15653
  this.autoConnectFailures = 0;
@@ -20842,6 +21163,7 @@ var GroupSession = class {
20842
21163
  if (frame.e > this.epoch) {
20843
21164
  this.ask(from);
20844
21165
  this.offerMeta(from, frame.mt);
21166
+ this.handOwn(from, frame);
20845
21167
  return;
20846
21168
  }
20847
21169
  if (frame.h !== commitHash(this.state.chain[frame.e])) {
@@ -20872,7 +21194,7 @@ var GroupSession = class {
20872
21194
  secrets: secrets.slice(i, i + GROUP_LIMITS.secretsPerFrame)
20873
21195
  });
20874
21196
  const lacks = missingIn(frame, from);
20875
- for (const sent of this.state.sent) if (rosterHas(this.state.chain[sent.e].m, from) && lacks(sent)) this.hooks.send(from, sent);
21197
+ this.handOwn(from, frame);
20876
21198
  const asked = new Set(Array.isArray(frame.ask) ? frame.ask.filter((k) => typeof k === "string" && k !== from && k !== this.myKey && rosterHas(this.roster, k)) : []);
20877
21199
  if (asked.size) {
20878
21200
  for (const kept of this.state.relay ?? []) if (asked.has(kept.s) && this.state.chain[kept.e] && rosterHas(this.state.chain[kept.e].m, from) && lacks(kept)) this.hooks.send(from, kept);
@@ -20883,6 +21205,11 @@ var GroupSession = class {
20883
21205
  }
20884
21206
  this.offerMeta(from, frame.mt);
20885
21207
  }
21208
+ /** My own messages a member's sync says it lacks, from my bounded log, for the epochs it was a member of. */
21209
+ handOwn(to, frame) {
21210
+ const lacks = missingIn(frame, to);
21211
+ for (const sent of this.state.sent) if (this.state.chain[sent.e] && rosterHas(this.state.chain[sent.e].m, to) && lacks(sent)) this.hooks.send(to, sent);
21212
+ }
20886
21213
  /** Sets (or, with null, removes) the group's picture: only the admin, signed under the current commit. */
20887
21214
  setPicture(picture, now = Date.now()) {
20888
21215
  return this.serialize(async () => {
@@ -20947,7 +21274,7 @@ var GroupSession = class {
20947
21274
  for (const key of this.others) this.hooks.send(key, frame);
20948
21275
  }
20949
21276
  const change = groupMetaChange(before, meta, this.state.name);
20950
- if (change) this.hooks.metaChanged?.(this.myKey, change);
21277
+ if (change) this.hooks.metaChanged?.(this.myKey, change, meta.ts);
20951
21278
  this.hooks.changed();
20952
21279
  }
20953
21280
  /** My statement, to a member whose sync says it holds an older one (or none); nothing to an app that says nothing. */
@@ -20992,7 +21319,7 @@ var GroupSession = class {
20992
21319
  this.state.meta = opened.meta;
20993
21320
  await this.persist();
20994
21321
  const change = !before && !rosterHas(commit.m, this.myKey) ? null : groupMetaChange(before, opened.meta, this.state.name);
20995
- if (change) this.hooks.metaChanged?.(s.by, change);
21322
+ if (change) this.hooks.metaChanged?.(s.by, change, opened.meta.ts);
20996
21323
  this.hooks.changed();
20997
21324
  this.took({
20998
21325
  t: "group-meta",
@@ -21715,12 +22042,44 @@ var CommunitySession = class {
21715
22042
  }
21716
22043
  /** Someone the chain took out (removed or left) and who is not back in. */
21717
22044
  wasRemoved(key) {
21718
- if (rosterHas(this.roster, key)) return false;
22045
+ return this.outIndex(key) >= 0;
22046
+ }
22047
+ /** When the chain took them out (the commit's time, by its signer's clock), if it did and they are not back in. */
22048
+ outAt(key) {
22049
+ const i = this.outIndex(key);
22050
+ return i < 0 ? void 0 : this.state.chain[i].ts;
22051
+ }
22052
+ outIndex(key) {
22053
+ if (rosterHas(this.roster, key)) return -1;
21719
22054
  for (let i = this.state.chain.length - 1; i >= 0; i--) {
21720
22055
  const c = this.state.chain[i];
21721
- if ((c.k === "remove" || c.k === "leave") && c.s === key) return true;
22056
+ if ((c.k === "remove" || c.k === "leave") && c.s === key) return i;
21722
22057
  }
21723
- return false;
22058
+ return -1;
22059
+ }
22060
+ /**
22061
+ * What someone the chain took out is told when it comes back (WISP 9xx § Leaving and removal): the commits from where
22062
+ * it says it is (`sync`: its tip, or its locator) up to the one that took it out, and nothing after. Later commits
22063
+ * are not its business (a `link` commit names the new entry key); no secret, no stored frame, no seed goes with them.
22064
+ * Empty when it is a member, was never one, or already holds that commit.
22065
+ */
22066
+ farewell(key, sync) {
22067
+ if (!this.isMember) return [];
22068
+ const chain = this.state.chain, out = this.outIndex(key);
22069
+ if (out < 0) return [];
22070
+ let start;
22071
+ if (typeof sync.h === "string" && this.mainIndex.has(sync.h)) start = this.mainIndex.get(sync.h) + 1;
22072
+ else {
22073
+ const path = typeof sync.h === "string" && this.known.has(sync.h) ? this.pathToMain(sync.h) : null;
22074
+ const common = (Array.isArray(sync.loc) ? sync.loc : []).find((x) => typeof x === "string" && this.mainIndex.has(x));
22075
+ start = path ? this.mainIndex.get(this.known.get(path[0]).p) + 1 : common !== void 0 ? this.mainIndex.get(common) + 1 : Math.max(0, out + 1 - COMMUNITY_LIMITS.side);
22076
+ }
22077
+ return chain.slice(start, out + 1).map((commit) => ({
22078
+ t: "group-commit",
22079
+ v: 2,
22080
+ g: this.id,
22081
+ commit
22082
+ }));
21724
22083
  }
21725
22084
  missing(sender) {
21726
22085
  const entry = this.state.seen[sender]?.[seenKey$1(this.epoch, shortHash(this.topHash))];
@@ -22812,7 +23171,7 @@ var CommunitySession = class {
22812
23171
  const frame = this.metaFrame();
22813
23172
  if (frame) this.hooks.broadcast(frame);
22814
23173
  const change = groupMetaChange(before, meta, this.state.name);
22815
- if (change) this.hooks.metaChanged?.(this.myKey, change);
23174
+ if (change) this.hooks.metaChanged?.(this.myKey, change, meta.ts);
22816
23175
  this.hooks.changed();
22817
23176
  }
22818
23177
  /** My statement sealed under my current epoch, when I hold its secret. */
@@ -22865,7 +23224,7 @@ var CommunitySession = class {
22865
23224
  this.state.meta = opened.meta;
22866
23225
  await this.persist();
22867
23226
  const change = !before && !rosterHas(this.rosterAt(s.h) ?? [], this.myKey) ? null : groupMetaChange(before, opened.meta, this.state.name);
22868
- if (change) this.hooks.metaChanged?.(s.by, change);
23227
+ if (change) this.hooks.metaChanged?.(s.by, change, opened.meta.ts);
22869
23228
  this.hooks.changed();
22870
23229
  return true;
22871
23230
  }
@@ -24906,7 +25265,7 @@ async function sendCard(ctx, params, kind) {
24906
25265
  text,
24907
25266
  card
24908
25267
  });
24909
- if (result.error || !result.messageId) throw new CliError("unavailable", result.error ?? "Nothing to send");
25268
+ if (result.error || !result.messageId) throw new CliError(result.refused ? "refused" : "unavailable", result.error ?? "Nothing to send");
24910
25269
  const edges = wait === "sent" ? await waitForGroupFrame(ctx, target.id, result.messageId, void 0, ms) : void 0;
24911
25270
  return answer(target, kind, id, result.messageId, {
24912
25271
  card,
@@ -25306,10 +25665,20 @@ function resumeHolds(ctx) {
25306
25665
  }
25307
25666
  //#endregion
25308
25667
  //#region src/views.ts
25668
+ /**
25669
+ * A live session carries the chat now, in this run: its link is open and, on a paired chat, the session authenticated
25670
+ * on a transport. The engine's own rule for a chat's connection line (`observeTransport`). `textDelivery` alone says
25671
+ * how a text would go, and nothing the engine remembers from an earlier run (a chat that was live when the app last
25672
+ * ran, WISP 100) counts: until the contact answers again the link is not open and the pairing is `connecting`.
25673
+ */
25674
+ function isLive(link) {
25675
+ if (link.textDelivery !== "stream" || link.dataLink !== "open") return false;
25676
+ return !link.profile || link.pairing?.status === "ready" && !!link.pairing.transport;
25677
+ }
25309
25678
  function chatJson(link) {
25310
25679
  const label = link.label?.trim() || null;
25311
25680
  const peerName = link.peerNick?.trim() || null;
25312
- const live = link.textDelivery === "stream";
25681
+ const live = isLive(link);
25313
25682
  return {
25314
25683
  id: link.id,
25315
25684
  name: label ?? peerName,
@@ -25364,6 +25733,7 @@ function messageJson(message) {
25364
25733
  from: message.sender,
25365
25734
  text: message.text,
25366
25735
  timestamp: message.timestamp,
25736
+ ...message.sentAt !== void 0 ? { sentAt: message.sentAt } : {},
25367
25737
  delivery: message.delivery ?? (message.sender === "peer" ? null : "sent"),
25368
25738
  deliveryError: message.deliveryError ?? null,
25369
25739
  via: message.via,
@@ -30599,7 +30969,7 @@ var SERVICE_METHODS = {
30599
30969
  const link = chatOf(ctx, params);
30600
30970
  return {
30601
30971
  chat: link.id,
30602
- live: link.textDelivery === "stream",
30972
+ live: isLive(link),
30603
30973
  services: link.peerServices ?? []
30604
30974
  };
30605
30975
  },
@@ -33433,7 +33803,7 @@ var METHODS = {
33433
33803
  webrtc: ctx.runtime.webrtc,
33434
33804
  calls: ctx.runtime.callsUnavailable === null,
33435
33805
  chats: s.links.length,
33436
- live: s.links.filter((l) => l.textDelivery === "stream").length,
33806
+ live: s.links.filter(isLive).length,
33437
33807
  groups: s.groups.length,
33438
33808
  discovery: {
33439
33809
  protocol: s.transport.protocol,
@@ -33844,14 +34214,15 @@ var METHODS = {
33844
34214
  return chatJson(await waitForState(ctx, (s) => {
33845
34215
  const now = s.links.find((l) => l.id === link.id);
33846
34216
  if (!now) throw new CliError("not_found", `Chat ${link.id} was removed`);
33847
- return (until === "live" ? now.textDelivery === "stream" : until === "text" ? now.textDelivery === "stream" || now.textDelivery === "dht" || now.textDelivery === "hold" : !!now.pairingProgress?.peerSeen || now.pairing?.status === "ready" || now.textDelivery === "stream") ? now : void 0;
34217
+ return (until === "live" ? isLive(now) : until === "text" ? now.textDelivery === "stream" || now.textDelivery === "dht" || now.textDelivery === "hold" : !!now.pairingProgress?.peerSeen || now.pairing?.status === "ready" || isLive(now)) ? now : void 0;
33848
34218
  }, num(params, "timeout", 60, {
33849
34219
  min: 1,
33850
34220
  max: 86400
33851
34221
  }) * 1e3, `chat ${link.id} to be ${until}`));
33852
34222
  },
33853
34223
  async "group.create"(ctx, params) {
33854
- const name = str(params, "name", true);
34224
+ const name = groupName(str(params, "name", true));
34225
+ if (!name) throw new CliError("bad_request", `A group's name is 1 to 64 characters on one line`);
33855
34226
  const profile = oneOf(params, "profile", ["community", "mesh"], "community");
33856
34227
  const { groupId } = await node(ctx).createGroup({
33857
34228
  name,
@@ -33899,7 +34270,7 @@ var METHODS = {
33899
34270
  ...replyTo ? { replyTo } : {},
33900
34271
  ...card ? { card } : {}
33901
34272
  });
33902
- if (result.error) throw new CliError("unavailable", result.error);
34273
+ if (result.error) throw result.refused ? new CliError("refused", result.error) : groupSendRefused(result.error);
33903
34274
  const messageId = result.messageId ?? null;
33904
34275
  const edges = messageId ? wait === "sent" ? await waitForGroupFrame(ctx, group.id, messageId, void 0, num(params, "timeout", 30, {
33905
34276
  min: 1,
@@ -33911,7 +34282,7 @@ var METHODS = {
33911
34282
  return {
33912
34283
  group: group.id,
33913
34284
  messageId,
33914
- sent: true,
34285
+ sent: edges > 0,
33915
34286
  edges,
33916
34287
  ...card ? {
33917
34288
  buttons: card.id,
@@ -33934,6 +34305,7 @@ var METHODS = {
33934
34305
  }
33935
34306
  const mentions = mentionsFor(text, list(params, "mentions"), group);
33936
34307
  const wait = oneOf(params, "wait", ["none", "sent"], "none");
34308
+ const before = (await node(ctx).groupMessages({ groupId: group.id })).find((m) => m.id === messageId)?.edit?.seq ?? 0;
33937
34309
  const result = await node(ctx).editMessage({
33938
34310
  linkId: `group:${group.id}`,
33939
34311
  messageId,
@@ -33943,6 +34315,14 @@ var METHODS = {
33943
34315
  if (result.error) throw editRefused(await node(ctx).groupMessages({ groupId: group.id }), messageId, "group", result.error, result.refused);
33944
34316
  const message = (await node(ctx).groupMessages({ groupId: group.id })).find((m) => m.id === messageId);
33945
34317
  const edits = message?.edit?.seq ?? 0;
34318
+ if (edits === before) return {
34319
+ group: group.id,
34320
+ messageId,
34321
+ edits,
34322
+ sent: false,
34323
+ edges: 0,
34324
+ unchanged: true
34325
+ };
33946
34326
  const edges = !edits ? 0 : wait === "sent" ? await waitForGroupFrame(ctx, group.id, messageId, edits, num(params, "timeout", 30, {
33947
34327
  min: 1,
33948
34328
  max: 3600
@@ -34062,7 +34442,7 @@ async function react(ctx, linkId, params) {
34062
34442
  messageId,
34063
34443
  emoji: emoji ?? ""
34064
34444
  });
34065
- if (result.error) throw new CliError(/not in this chat|No reaction of yours/.test(result.error) ? "not_found" : "bad_request", result.error);
34445
+ if (result.error) throw new CliError(result.refused ? "refused" : /not in this chat|No reaction of yours/.test(result.error) ? "not_found" : "bad_request", result.error);
34066
34446
  return {
34067
34447
  messageId,
34068
34448
  emoji: emoji || null,
@@ -34088,6 +34468,17 @@ function typingWord(params, typing) {
34088
34468
  };
34089
34469
  }
34090
34470
  /**
34471
+ * A group message the engine did not send, by what a bot can do about it: a `--reply` to a message this group does not
34472
+ * have is `not_found` (exit 3), as `group react`, `group edit` and `pin` answer for one; out of the group (removed,
34473
+ * left, a forked history) is `refused`: it will not go later either. Anything else may go on a later try
34474
+ * (`unavailable`): the epoch's key is still on its way. All three were `unavailable` (exit 1) before.
34475
+ */
34476
+ function groupSendRefused(error) {
34477
+ if (/not in this chat|cannot be replied to/.test(error)) return new CliError("not_found", error);
34478
+ if (/^You (were removed from|left|are no longer in) this group$|Membership changes are halted/.test(error)) return new CliError("refused", error);
34479
+ return new CliError("unavailable", error);
34480
+ }
34481
+ /**
34091
34482
  * An edit the engine refused. A message that is not in the chat or group at all is `not_found` (exit 3), naming the id
34092
34483
  * to give, as `button update` and `task update` do; before, a mistyped or lost id read "Only your own text messages
34093
34484
  * can be edited" (`refused`, exit 1), as if the message were someone else's.
@@ -39066,14 +39457,14 @@ async function startRuntime(paths, options = {}) {
39066
39457
  (await import("./assets/ghostly_iroh_web-CPa_YEfb.js")).initSync({ module: irohWasmBytes() });
39067
39458
  const [{ registerFileBytes }, { NodeFileBytes }] = await Promise.all([Promise.resolve().then(() => fileBytes_exports), import("./assets/fileBytes-CLv0gryb.js")]);
39068
39459
  registerFileBytes("native", async () => new NodeFileBytes(paths.files), true);
39069
- const [{ EngineServer }, { createHyperEndpoint }] = await Promise.all([import("./assets/server-C-8wbsAX.js").then((n) => n.t), import("./assets/endpoint-CaSbSFDw.js")]);
39460
+ const [{ EngineServer }, { createHyperEndpoint }] = await Promise.all([import("./assets/server-Cajp8VLY.js").then((n) => n.t), import("./assets/endpoint-CaSbSFDw.js")]);
39070
39461
  const network = hyperdhtNetwork();
39071
39462
  const dht = mainlineNetwork();
39072
- const mainline = dht.off ? null : new (await (import("./assets/mainline-B0mUxYPH.js"))).Mainline({
39463
+ const mainline = dht.off ? null : new (await (import("./assets/mainline-_roUnpDA.js"))).Mainline({
39073
39464
  bootstrap: dht.bootstrap,
39074
39465
  host: dht.host
39075
39466
  });
39076
- const transport = mainline || pinned ? new (await (import("./assets/mainline-B0mUxYPH.js"))).RelaysAndDht(mainline, pinned ? { relays: pinned } : {}, !!pinned) : null;
39467
+ const transport = mainline || pinned ? new (await (import("./assets/mainline-_roUnpDA.js"))).RelaysAndDht(mainline, pinned ? { relays: pinned } : {}, !!pinned) : null;
39077
39468
  const server = new EngineServer({
39078
39469
  ...transport ? { transport } : {},
39079
39470
  irohWeb: true,
@@ -40606,6 +40997,6 @@ async function run(argv) {
40606
40997
  //#region src/bin.ts
40607
40998
  run(process.argv.slice(2));
40608
40999
  //#endregion
40609
- export { boundedIdentityFetch as $, EXPECT_PEER_MS as $i, LIVENESS_PING_MS as $n, IDENTITY_REVOCATION_LABEL as $r, GroupWakeLimiter as $t, TEST_MINT as A, parseRelayPayload as Aa, ReactionWindow as Ai, assertTokenUnits as An, nextPinNumber as Ar, transact as At, registeredLightningProviders as B, toZ32 as Ba, replySnippet as Bi, isPubkyKey as Bn, vapidKeysMatch as Br, communityHasFrame as Bt, networkLabel as C, normalizeRelayUrl as Ca, dhtEditId as Ci, ONCHAIN_PROVIDER as Cn, transferEnded as Cr, STORES as Ct, WALLET_TYPES as D, PacketTooLargeError as Da, COMMUNITY_REACTION_FRAME as Di, TEST_USDT_FAUCET_AMOUNT as Dn, GROUP_PIN_FRAME as Dr, fileStore as Dt, MESSAGE_DETAILS_MAX_BYTES as E, withRequestOptions as Ea, readForwarded as Ei, SEPOLIA_TEST_USDT_FAUCET as En, typingActivity as Er, deleteDatabase as Et, paysItsOwnInvoices as F, sanitizeAvatar as Fa, readReaction as Fi, SPARK_NETWORKS as Fn, WakeLimiter as Fr, openGroupTyping as Ft, SourceConfigError as G, statusCardText as Gi, cashuRequestPayload as Gn, HoldKeys as Gr, decodeGroupEntryLink as Gt, reserveAdapterIds as H, utf8Encode as Ha, STATUS_CARD_LIMITS as Hi, engineText as Hn, EDIT_CAPABILITY as Hr, decodeCommunityLink as Ht, BACKUP_MEDIA_TYPE as I, bytesEqual as Ia, validReactionNumber as Ii, SPARK_PROVIDER as In, newWakeToken as Ir, sealGroupTyping as It, networkMode as J, MAX_GROUP_CHAIN as Ji, isFederationId as Jn, readManifest as Jr, knockIdentity as Jt, SourceUnreachableError as K, receivedTimestamp as Ki, fedimintRequestPayload as Kn, HoldRefusedError as Kr, encodeGroupEntryLink as Kt, MAX_BACKUP_BYTES as L, fromBase64Url$1 as La, wireReaction as Li, sparkAddressKind as Ln, relayRequest as Lr, COMMUNITY_LIMITS as Lt, isTestMint as M, identityFromSeed as Ma, queueReaction as Mi, formatPaymentAmount as Mn, pinNumberHolds as Mr, GROUP_TYPING_FRAME as Mt, isWorthlessMint as N, identityFromSeedB64 as Na, reactionEmoji as Ni, validatePaymentTarget as Nn, readPin as Nr, GroupTypingBoard as Nt, BITCOIN_INVOICE_ON_TESTNET as O, createRelayPayload as Oa, GROUP_REACTION_FRAME as Oi, USDT_PUBLIC_RPC as On, PIN_LIMITS as Or, openDb as Ot, mintNetwork as P, publicKeyFromZ32 as Pa, reactionIsNewer as Pi, walletNetworkOf as Pn, WAKE_CALL_INTERVAL_MS as Pr, groupTypingEpoch as Pt, setOwnDidSource as Q, edgeParams as Qi, INVITE_TAKEN as Qn, dialDescriptors as Qr, GROUP_WAKE_FRAME as Qt, identityProviders as R, randomBytes$1 as Ra, pairedReplyAuthor as Ri, sparkInvoiceDetails as Rn, wakeRequest as Rr, CommunitySession as Rt, WrongNetworkError as S, currentRelays as Sa, RateWindow as Si, EVM_TEST_CHAINS as Sn, FILE_LIMITS as Sr, digestText as St, cashuMint as T, isDiscoveryBudgetError as Ta, forwardedAgain as Ti, SEPOLIA_TEST_USDT as Tn, TypingSender as Tr, databaseName as Tt, NothingSpentError as U, __exportAll as Ua, checkStatusCard as Ui, decodeBolt11 as Un, HOLD_CAPABILITY as Ur, encodeCommunityLink as Ut, registeredOnchainProviders as V, utf8Decode as Va, BUTTON_ID as Vi, engineError as Vn, BUTTONS_CAPABILITY as Vr, communityMessageId as Vt, PROVIDER_PLATFORMS as W, sameButtonText as Wi, ENDPOINT as Wn, HOLD_LIMITS as Wr, KNOCK_TTL_MS as Wt, assertPublicServer as X, rosterHas as Xi, parseFedimintRequestPayload as Xn, DHT_TEXT_CAPABILITY as Xr, mergeKnocks as Xt, redact as Y, MEMBER_KEY as Yi, parseCashuRequestPayload as Yn, CapsExchange as Yr, knockRecords as Yt, resolveAtprotoDid as Z, verifyCommitSignature as Zi, GhostLink as Zn, capsDescriptors as Zr, readKnocks as Zt, newDeviceKey as _, decodeInviteCode as _a, parseVideoMeta as _i, didDhtFromPublicKey as _n, readBeacon as _r, fileBytes as _t, createFailure as a, webLocalFetch as aa, receivedIdentityStatus as ai, GroupSession as an, COMMUNITY_TOPOLOGY as ar, replyRef as at, ModeChanged as b, DEFAULT_RELAYS as ba, EDIT_BUFFER_MS as bi, signDidDhtPacket as bn, shouldBeHub as br, removeFileBytes as bt, paymentNetwork as c, encodeControl as ca, PeerProofs as ci, GROUP_EDIT_FRAME as cn, doorHubs as cr, paymentWireId as ct, networkReason as d, emptyLinkRecords as da, TRANSPORTS as di, parseCommunityEdit as dn, lobbyRecords as dr, removeStored as dt, RELAY_POLL_INTERVALS as ea, IdentityExchange as ei, groupWakeFrame as en, pairedMessageFrame as er, verifyIdentity as et, rpcHost as f, HTTP_SERVICE_PROTO as fa, automaticTransport as fi, mentionsMember as fn, mergeBeacon as fr, storedSize as ft, intentRepository as g, createChatInvite as ga, parseVoiceMeta as gi, didDhtDocument as gn, rankHubs as gr, checkFileId as gt, ProviderSources as h, OwnInviteError as ha, fileMessageText as hi, normalizeAtprotoHandle as hn, pickHubs as hr, blobDigest as ht, assertConfirmedReal as i, parseLocalTarget as ia, newIdentityBinding as ii, GROUP_READ_NOTE as in, pickMeshHubs as ir, receivedPairedReply as it, fakesLightning as j, createIdentity as ja, nextReactionNumber as ji, assertWholeSats as jn, pinIsNewer as jr, wrap as jt, DEFAULT_MINTS as k, newerPacket as ka, REACTION_LIMITS as ki, WALLET_NETWORKS as kn, mayPin as kr, store$1 as kt, paymentNetworksOf as l, RTC_CONFIG as la, emptyProofLedger as li, GROUP_EDIT_TEXT_BYTES as ln, freshHubs as lr, FileAppender as lt, LightningCards as m, serviceIdFromName as ma, safeBlobType as mi, isAtprotoDid as mn, newerHead as mr, SMALL_FILE_BYTES as mt, loadFedimintSdk as n, GhostlyHttpError as na, identityRevocationValue as ni, parseGroupWakeFrame as nn, mayBeHub as nr, groupReply as nt, createTiming as o, LIMITS as oa, revokesIdentity as oi, groupMessageId as on, beaconKeys as or, replyTo as ot, CASHU_CARD as p, LEGACY_SERVICES as pa, parseImageMeta as pi, groupName as pn, mergeLobby as pr, streamStored as pt, isNothingSpentError as q, GROUP_MEMBER_CAP as qi, findEndpoint as qn, newMailbox as qr, entryParams as qt, WALLET_NAMES as r, formatLocalTarget as ra, identityStatement as ri, GROUP_REACTED_FRAME as rn, meshHubs as rr, pairedWireReply as rt, crossNetwork as s, decodeControl as sa, lastSharedWithMe as si, COMMUNITY_EDIT_FRAME as sn, beaconRecords as sr, paymentAlias as st, fedimintDatabase as t, traceLink as ta, emptyIdentityLedger as ti, groupWakes as tn, MESH_HUBS as tr, chosenResolver as tt, walletInstances as u, parseCallSignal as ua, validExternalKey as ui, carryMentions as un, lobbyKeys as ur, readStored as ut, sealSeed as v, encodeInviteCode as va, DHT_MESSAGE_TTL as vi, encodeDidDhtDeactivated as vn, readBeaconHead as vr, fileBytesOf as vt, CASHU_MINT_SOURCE as w, budgetRetryMs as wa, parseLinkPreview as wi, PaymentPreflightError as wn, transferStalled as wr, clearProfileStores as wt, ModeGate as x, RelayTransport as xa, EDIT_RATE_WINDOW_MS as xi, ETHEREUM_USDT as xn, ChatFiles as xr, FILE_BYTES_STEP as xt, unsealSeed as y, inviteOwnership as ya, peekDhtMailbox as yi, encodeDidDhtPacket as yn, readLobby as yr, fileSpace as yt, onAdaptersChanged as z, toBase64Url$1 as za, readReply as zi, isBitcoinAddress as zn, checkPushEndpoint as zr, GROUP_READ_NOTE_COMMUNITY as zt };
41000
+ export { boundedIdentityFetch as $, GROUP_MEMBER_CAP as $i, LIVENESS_PING_MS as $n, IDENTITY_REVOCATION_LABEL as $r, GroupWakeLimiter as $t, TEST_MINT as A, normalizeRelayUrl as Aa, GROUP_REACTION_FRAME as Ai, assertTokenUnits as An, nextPinNumber as Ar, transact as At, registeredLightningProviders as B, identityFromSeedB64 as Ba, pairedReplyAuthor as Bi, isPubkyKey as Bn, vapidKeysMatch as Br, communityHasFrame as Bt, networkLabel as C, createChatInvite as Ca, EDIT_RATE_WINDOW_MS as Ci, ONCHAIN_PROVIDER as Cn, transferEnded as Cr, STORES as Ct, WALLET_TYPES as D, DEFAULT_RELAYS as Da, forwardedAgain as Di, TEST_USDT_FAUCET_AMOUNT as Dn, GROUP_PIN_FRAME as Dr, fileStore as Dt, MESSAGE_DETAILS_MAX_BYTES as E, inviteOwnership as Ea, parseLinkPreview as Ei, SEPOLIA_TEST_USDT_FAUCET as En, typingActivity as Er, deleteDatabase as Et, paysItsOwnInvoices as F, createRelayPayload as Fa, reactionEmoji as Fi, SPARK_NETWORKS as Fn, WakeLimiter as Fr, openGroupTyping as Ft, SourceConfigError as G, randomBytes$1 as Ga, checkStatusCard as Gi, cashuRequestPayload as Gn, HoldKeys as Gr, decodeGroupEntryLink as Gt, reserveAdapterIds as H, sanitizeAvatar as Ha, replySnippet as Hi, engineText as Hn, EDIT_CAPABILITY as Hr, decodeCommunityLink as Ht, BACKUP_MEDIA_TYPE as I, newerPacket as Ia, reactionIsNewer as Ii, SPARK_PROVIDER as In, newWakeToken as Ir, sealGroupTyping as It, networkMode as J, utf8Decode as Ja, arrivalKey as Ji, isFederationId as Jn, readManifest as Jr, knockIdentity as Jt, SourceUnreachableError as K, toBase64Url$1 as Ka, sameButtonText as Ki, fedimintRequestPayload as Kn, HoldRefusedError as Kr, encodeGroupEntryLink as Kt, MAX_BACKUP_BYTES as L, parseRelayPayload as La, readReaction as Li, sparkAddressKind as Ln, relayRequest as Lr, COMMUNITY_LIMITS as Lt, isTestMint as M, isDiscoveryBudgetError as Ma, ReactionWindow as Mi, formatPaymentAmount as Mn, pinNumberHolds as Mr, GROUP_TYPING_FRAME as Mt, isWorthlessMint as N, withRequestOptions as Na, nextReactionNumber as Ni, validatePaymentTarget as Nn, readPin as Nr, GroupTypingBoard as Nt, BITCOIN_INVOICE_ON_TESTNET as O, RelayTransport as Oa, readForwarded as Oi, USDT_PUBLIC_RPC as On, PIN_LIMITS as Or, openDb as Ot, mintNetwork as P, PacketTooLargeError as Pa, queueReaction as Pi, walletNetworkOf as Pn, WAKE_CALL_INTERVAL_MS as Pr, groupTypingEpoch as Pt, setOwnDidSource as Q, shownTime as Qi, INVITE_TAKEN as Qn, dialDescriptors as Qr, GROUP_WAKE_FRAME as Qt, identityProviders as R, createIdentity as Ra, validReactionNumber as Ri, sparkInvoiceDetails as Rn, wakeRequest as Rr, CommunitySession as Rt, WrongNetworkError as S, OwnInviteError as Sa, EDIT_BUFFER_MS as Si, EVM_TEST_CHAINS as Sn, FILE_LIMITS as Sr, digestText as St, cashuMint as T, encodeInviteCode as Ta, dhtEditId as Ti, SEPOLIA_TEST_USDT as Tn, TypingSender as Tr, databaseName as Tt, NothingSpentError as U, bytesEqual as Ua, BUTTON_ID as Ui, decodeBolt11 as Un, HOLD_CAPABILITY as Ur, encodeCommunityLink as Ut, registeredOnchainProviders as V, publicKeyFromZ32 as Va, readReply as Vi, engineError as Vn, BUTTONS_CAPABILITY as Vr, communityMessageId as Vt, PROVIDER_PLATFORMS as W, fromBase64Url$1 as Wa, STATUS_CARD_LIMITS as Wi, ENDPOINT as Wn, HOLD_LIMITS as Wr, KNOCK_TTL_MS as Wt, assertPublicServer as X, __exportAll as Xa, heardTime as Xi, parseFedimintRequestPayload as Xn, DHT_TEXT_CAPABILITY as Xr, mergeKnocks as Xt, redact as Y, utf8Encode as Ya, claimedTime as Yi, parseCashuRequestPayload as Yn, CapsExchange as Yr, knockRecords as Yt, resolveAtprotoDid as Z, receivedTimestamp as Zi, GhostLink as Zn, capsDescriptors as Zr, readKnocks as Zt, newDeviceKey as _, parseCallSignal as _a, fileMessageText as _i, didDhtFromPublicKey as _n, readBeacon as _r, fileBytes as _t, createFailure as a, DirectPathWatch as aa, receivedIdentityStatus as ai, GroupSession as an, COMMUNITY_TOPOLOGY as ar, replyRef as at, ModeChanged as b, LEGACY_SERVICES as ba, DHT_MESSAGE_TTL as bi, signDidDhtPacket as bn, shouldBeHub as br, removeFileBytes as bt, paymentNetwork as c, traceLink as ca, PeerProofs as ci, GROUP_EDIT_FRAME as cn, doorHubs as cr, paymentWireId as ct, networkReason as d, parseLocalTarget as da, TRANSPORTS as di, parseCommunityEdit as dn, lobbyRecords as dr, removeStored as dt, MAX_GROUP_CHAIN as ea, IdentityExchange as ei, groupWakeFrame as en, pairedMessageFrame as er, verifyIdentity as et, rpcHost as f, webLocalFetch as fa, automaticTransport as fi, mentionsMember as fn, mergeBeacon as fr, storedSize as ft, intentRepository as g, RTC_CONFIG as ga, safeBlobType as gi, didDhtDocument as gn, rankHubs as gr, checkFileId as gt, ProviderSources as h, encodeControl as ha, parseImageMeta as hi, normalizeAtprotoHandle as hn, pickHubs as hr, blobDigest as ht, assertConfirmedReal as i, edgeParams as ia, newIdentityBinding as ii, GROUP_READ_NOTE as in, pickMeshHubs as ir, receivedPairedReply as it, fakesLightning as j, budgetRetryMs as ja, REACTION_LIMITS as ji, assertWholeSats as jn, pinIsNewer as jr, wrap as jt, DEFAULT_MINTS as k, currentRelays as ka, COMMUNITY_REACTION_FRAME as ki, WALLET_NETWORKS as kn, mayPin as kr, store$1 as kt, paymentNetworksOf as l, GhostlyHttpError as la, emptyProofLedger as li, GROUP_EDIT_TEXT_BYTES as ln, freshHubs as lr, FileAppender as lt, LightningCards as m, decodeControl as ma, irohRelayUrl as mi, isAtprotoDid as mn, newerHead as mr, SMALL_FILE_BYTES as mt, loadFedimintSdk as n, rosterHas as na, identityRevocationValue as ni, parseGroupWakeFrame as nn, mayBeHub as nr, groupReply as nt, createTiming as o, EXPECT_PEER_MS as oa, revokesIdentity as oi, groupMessageId as on, beaconKeys as or, replyTo as ot, CASHU_CARD as p, LIMITS as pa, irohDescriptor as pi, groupName as pn, mergeLobby as pr, streamStored as pt, isNothingSpentError as q, toZ32 as qa, statusCardText as qi, findEndpoint as qn, newMailbox as qr, entryParams as qt, WALLET_NAMES as r, verifyCommitSignature as ra, identityStatement as ri, GROUP_REACTED_FRAME as rn, meshHubs as rr, pairedWireReply as rt, crossNetwork as s, RELAY_POLL_INTERVALS as sa, lastSharedWithMe as si, COMMUNITY_EDIT_FRAME as sn, beaconRecords as sr, paymentAlias as st, fedimintDatabase as t, MEMBER_KEY as ta, emptyIdentityLedger as ti, groupWakes as tn, MESH_HUBS as tr, chosenResolver as tt, walletInstances as u, formatLocalTarget as ua, validExternalKey as ui, carryMentions as un, lobbyKeys as ur, readStored as ut, sealSeed as v, emptyLinkRecords as va, parseVoiceMeta as vi, encodeDidDhtDeactivated as vn, readBeaconHead as vr, fileBytesOf as vt, CASHU_MINT_SOURCE as w, decodeInviteCode as wa, RateWindow as wi, PaymentPreflightError as wn, transferStalled as wr, clearProfileStores as wt, ModeGate as x, serviceIdFromName as xa, peekDhtMailbox as xi, ETHEREUM_USDT as xn, ChatFiles as xr, FILE_BYTES_STEP as xt, unsealSeed as y, HTTP_SERVICE_PROTO as ya, parseVideoMeta as yi, encodeDidDhtPacket as yn, readLobby as yr, fileSpace as yt, onAdaptersChanged as z, identityFromSeed as za, wireReaction as zi, isBitcoinAddress as zn, checkPushEndpoint as zr, GROUP_READ_NOTE_COMMUNITY as zt };
40610
41001
 
40611
41002
  //# sourceMappingURL=ghostly.mjs.map