@dcasegr/skein 0.1.2 → 0.2.8

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.
@@ -83,6 +83,25 @@ export declare class HostDb {
83
83
  setStatus(handle: string, status: Status): boolean;
84
84
  /** Remove the row (not its store or wallet); false if there was none. */
85
85
  remove(handle: string): boolean;
86
+ /** A liveness beat kept (#138): the sender's row on `handle`'s `topic` replaced. */
87
+ putBeat(handle: string, topic: string, b: {
88
+ sender: string;
89
+ at: number;
90
+ from: string;
91
+ frame: Uint8Array;
92
+ }): void;
93
+ /** The liveness beats kept for `handle` (on `topic` only, when named). */
94
+ beatsOf(handle: string, topic?: string): Array<{
95
+ topic: string;
96
+ sender: string;
97
+ at: number;
98
+ from: string;
99
+ frame: Uint8Array;
100
+ }>;
101
+ /** Forget `handle`'s liveness beats: all, a topic's, or one sender's on a topic. */
102
+ dropBeats(handle: string, topic?: string, sender?: string): void;
103
+ /** Forget the liveness beats dated before `before` (ms). → how many rows went. */
104
+ pruneBeats(before: number): number;
86
105
  /** The enabled mailbox instance for identity `owner` (hex). */
87
106
  /** A beat kept from `peer` on `handle`'s overlay `topic` (sync): upserted with its addresses (kept when none are known now). */
88
107
  seenPeer(handle: string, topic: string, peer: string, addrs: string[], now: number): void;
@@ -20,7 +20,8 @@
20
20
  // the host last read it (asleep since when: the grace), the periods it ticked
21
21
  // (each period's log record, which its tick commits to), and the payments its
22
22
  // wallet made (the `payment` events). And overlay sync's: the peers each
23
- // instance's overlay topics heard beats from (`topic_peers`).
23
+ // instance's overlay topics heard beats from (`topic_peers`). And the liveness
24
+ // tool's (#138): the beats it kept, reloaded at a start (`live_beats`).
24
25
  import { DatabaseSync } from "node:sqlite";
25
26
  const DDL = `
26
27
  CREATE TABLE IF NOT EXISTS instances (
@@ -134,6 +135,17 @@ CREATE TABLE IF NOT EXISTS topic_peers (
134
135
  addrs TEXT NOT NULL, -- JSON array of multiaddrs (may be empty)
135
136
  last_seen INTEGER NOT NULL, -- ms: the last beat kept from it
136
137
  PRIMARY KEY (handle, topic, peer_id)
138
+ ) WITHOUT ROWID;
139
+ -- The liveness tool's beats (#138, liveness.ts): per instance and liveness topic, the latest beat kept from each
140
+ -- sender, the frame as received; a start feeds them through the tool again (signature and window re-checked).
141
+ CREATE TABLE IF NOT EXISTS live_beats (
142
+ handle TEXT NOT NULL,
143
+ topic TEXT NOT NULL, -- the liveness topic (e.g. <topic>-live)
144
+ sender TEXT NOT NULL, -- the beat's identity key (hex)
145
+ at INTEGER NOT NULL, -- ms: the beat's own time
146
+ peer_id TEXT NOT NULL, -- the libp2p peer that published it
147
+ frame BLOB NOT NULL, -- the beacon frame (p2p.ts beaconFrame)
148
+ PRIMARY KEY (handle, topic, sender)
137
149
  ) WITHOUT ROWID;`;
138
150
  /**
139
151
  * A handle: one hostname label (lower-case letters, digits and "-", at most
@@ -249,6 +261,31 @@ export class HostDb {
249
261
  remove(handle) {
250
262
  return Number(this.db.prepare("DELETE FROM instances WHERE handle = ?").run(handle).changes) > 0;
251
263
  }
264
+ /** A liveness beat kept (#138): the sender's row on `handle`'s `topic` replaced. */
265
+ putBeat(handle, topic, b) {
266
+ this.db.prepare("INSERT INTO live_beats (handle, topic, sender, at, peer_id, frame) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT (handle, topic, sender) DO UPDATE SET at = excluded.at, peer_id = excluded.peer_id, frame = excluded.frame")
267
+ .run(handle, topic, b.sender, b.at, b.from, b.frame);
268
+ }
269
+ /** The liveness beats kept for `handle` (on `topic` only, when named). */
270
+ beatsOf(handle, topic) {
271
+ const rows = topic === undefined
272
+ ? this.db.prepare("SELECT topic, sender, at, peer_id, frame FROM live_beats WHERE handle = ? ORDER BY topic, sender").all(handle)
273
+ : this.db.prepare("SELECT topic, sender, at, peer_id, frame FROM live_beats WHERE handle = ? AND topic = ? ORDER BY sender").all(handle, topic);
274
+ return rows.map((r) => ({ topic: String(r.topic), sender: String(r.sender), at: Number(r.at), from: String(r.peer_id), frame: new Uint8Array(r.frame) }));
275
+ }
276
+ /** Forget `handle`'s liveness beats: all, a topic's, or one sender's on a topic. */
277
+ dropBeats(handle, topic, sender) {
278
+ if (topic === undefined)
279
+ this.db.prepare("DELETE FROM live_beats WHERE handle = ?").run(handle);
280
+ else if (sender === undefined)
281
+ this.db.prepare("DELETE FROM live_beats WHERE handle = ? AND topic = ?").run(handle, topic);
282
+ else
283
+ this.db.prepare("DELETE FROM live_beats WHERE handle = ? AND topic = ? AND sender = ?").run(handle, topic, sender);
284
+ }
285
+ /** Forget the liveness beats dated before `before` (ms). → how many rows went. */
286
+ pruneBeats(before) {
287
+ return Number(this.db.prepare("DELETE FROM live_beats WHERE at < ?").run(before).changes);
288
+ }
252
289
  /** The enabled mailbox instance for identity `owner` (hex). */
253
290
  /** A beat kept from `peer` on `handle`'s overlay `topic` (sync): upserted with its addresses (kept when none are known now). */
254
291
  seenPeer(handle, topic, peer, addrs, now) {
@@ -31,6 +31,20 @@ export interface LiveBeat {
31
31
  body: Uint8Array;
32
32
  from: string;
33
33
  }
34
+ /** A beat as the store keeps it: the frame as received (re-verified on reload), its time, sender (hex), and the peer that published it. */
35
+ export interface StoredBeat {
36
+ topic: string;
37
+ sender: string;
38
+ at: number;
39
+ from: string;
40
+ frame: Uint8Array;
41
+ }
42
+ /** Where the beats kept are persisted (host.db `live_beats`, instances.ts HostDb). */
43
+ export interface BeatStore {
44
+ putBeat(handle: string, topic: string, beat: Omit<StoredBeat, "topic">): void;
45
+ beatsOf(handle: string, topic?: string): StoredBeat[];
46
+ dropBeats(handle: string, topic?: string, sender?: string): void;
47
+ }
34
48
  /** A message's verdict for GossipSub: a verified beat is accepted (forwarded), a bad one rejected, a stale one ignored. */
35
49
  export type LiveVerdict = "accept" | "reject" | "ignore";
36
50
  /** The tool's state: per instance, the apps' liveness and the beats kept per topic (in memory). */
@@ -41,7 +55,11 @@ export declare class Liveness {
41
55
  private readonly now;
42
56
  /** Told each beat newly kept (shruggr/skein#150: the overlay's sync reads a peer's tip anchor from it). */
43
57
  kept?: (handle: string, topic: string, beat: LiveBeat) => void;
44
- constructor(now?: () => number);
58
+ /** The store the kept beats are persisted in (absent: memory only). */
59
+ private readonly store?;
60
+ constructor(now?: () => number, store?: BeatStore);
61
+ /** A store write: its failure (a closed host.db at shutdown) changes nothing in memory nor any verdict. */
62
+ private persist;
45
63
  /** `handle`'s apps keep this liveness now: the beats of topics none keeps any more are dropped. */
46
64
  set(handle: string, lives: Live[]): void;
47
65
  /** The topics `handle`'s apps keep liveness for (each once). */
@@ -60,6 +78,8 @@ export declare class Liveness {
60
78
  read(handle: string, app: string, topic: string): LiveBeat[] | undefined;
61
79
  /** Every beat live on `topic` at `handle` (newer than the longest window on it), whichever app keeps it. */
62
80
  live(handle: string, topic: string): LiveBeat[];
63
- /** Forget `handle`: its liveness and its beats. */
81
+ /** Forget `handle`: its liveness and its beats (the stored ones too). */
64
82
  drop(handle: string): void;
83
+ /** Forget `handle` in memory only (the host stopping): the stored beats stay for the next start. */
84
+ forget(handle: string): void;
65
85
  }
@@ -19,8 +19,12 @@
19
19
  // (identity key), with the peer that published it (`from`); the node's
20
20
  // OWN beats on the topic are written into the same set (gossip does not
21
21
  // echo them: P2PHost.beacons);
22
- // - in memory only: empty after a restart until the next beats; dropped at
23
- // the app's unliveness or uninstall.
22
+ // - in memory, and in the host's store when it gives one (host.db
23
+ // `live_beats`: the frame as received, its time, sender and peer): when
24
+ // the apps' liveness is set at a start, the frames kept for its topics go
25
+ // through `observe` again (signature re-checked, window re-applied; the
26
+ // ones that no longer count deleted); dropped at the app's unliveness or
27
+ // uninstall.
24
28
  //
25
29
  // The set is served to the app's page by the host itself, no program in
26
30
  // between (Router: GET /<app>/.live/<topic> on the instance's origin).
@@ -73,7 +77,18 @@ export class Liveness {
73
77
  now;
74
78
  /** Told each beat newly kept (shruggr/skein#150: the overlay's sync reads a peer's tip anchor from it). */
75
79
  kept;
76
- constructor(now = Date.now) { this.now = now; }
80
+ /** The store the kept beats are persisted in (absent: memory only). */
81
+ store;
82
+ constructor(now = Date.now, store) { this.now = now; this.store = store; }
83
+ /** A store write: its failure (a closed host.db at shutdown) changes nothing in memory nor any verdict. */
84
+ persist(f) {
85
+ if (!this.store)
86
+ return;
87
+ try {
88
+ f(this.store);
89
+ }
90
+ catch { /* memory stays right; the row is caught up at the next beat */ }
91
+ }
77
92
  /** `handle`'s apps keep this liveness now: the beats of topics none keeps any more are dropped. */
78
93
  set(handle, lives) {
79
94
  if (!lives.length) {
@@ -86,6 +101,20 @@ export class Liveness {
86
101
  for (const t of kept.keys())
87
102
  if (!lives.some((l) => l.topic === t))
88
103
  kept.delete(t);
104
+ if (!this.store)
105
+ return;
106
+ let rows = [];
107
+ this.persist((s) => { rows = s.beatsOf(handle); });
108
+ for (const t of new Set(rows.map((r) => r.topic)))
109
+ if (!lives.some((l) => l.topic === t))
110
+ this.persist((s) => s.dropBeats(handle, t));
111
+ // The beats kept before (a restart): each frame judged again by `observe`; one that no longer counts is deleted.
112
+ for (const r of rows) {
113
+ if (!lives.some((l) => l.topic === r.topic))
114
+ continue;
115
+ if (this.observe(handle, r.topic, r.frame, r.from) !== "accept")
116
+ this.persist((s) => s.dropBeats(handle, r.topic, r.sender));
117
+ }
89
118
  }
90
119
  /** The topics `handle`'s apps keep liveness for (each once). */
91
120
  topics(handle) { return [...new Set((this.lives.get(handle) ?? []).map((l) => l.topic))]; }
@@ -123,12 +152,15 @@ export class Liveness {
123
152
  if (!was || b.at > was.at) {
124
153
  const beat = { sender, at: b.at, body: b.body, from };
125
154
  bySender.set(sender, beat);
155
+ this.persist((s) => s.putBeat(handle, topic, { sender, at: b.at, from, frame }));
126
156
  this.kept?.(handle, topic, beat);
127
157
  }
128
158
  // The stale ones go as the set is touched.
129
159
  for (const [k, x] of bySender)
130
- if (now - x.at >= window)
160
+ if (now - x.at >= window) {
131
161
  bySender.delete(k);
162
+ this.persist((s) => s.dropBeats(handle, topic, k));
163
+ }
132
164
  return "accept";
133
165
  }
134
166
  /** The beats live for `app`'s liveness on `topic` (newer than its window), newest first; undefined: the app keeps no liveness for the topic. */
@@ -147,8 +179,13 @@ export class Liveness {
147
179
  const now = this.now();
148
180
  return [...(this.beats.get(handle)?.get(topic)?.values() ?? [])].filter((b) => now - b.at < window);
149
181
  }
150
- /** Forget `handle`: its liveness and its beats. */
182
+ /** Forget `handle`: its liveness and its beats (the stored ones too). */
151
183
  drop(handle) {
184
+ this.forget(handle);
185
+ this.persist((s) => s.dropBeats(handle));
186
+ }
187
+ /** Forget `handle` in memory only (the host stopping): the stored beats stay for the next start. */
188
+ forget(handle) {
152
189
  this.lives.delete(handle);
153
190
  this.beats.delete(handle);
154
191
  }
@@ -1,9 +1,12 @@
1
1
  import type { Libp2p, PeerId } from "@libp2p/interface";
2
- import type { PrivateKey } from "@bsv/sdk";
2
+ import type { ComponentLogger } from "@libp2p/interface";
3
+ import { type KadDHT } from "@libp2p/kad-dht";
4
+ import { type Multiaddr } from "@multiformats/multiaddr";
5
+ import { PrivateKey } from "@bsv/sdk";
3
6
  import type { Store } from "../runtime/store.ts";
4
7
  import { CID } from "multiformats/cid";
5
- import { Liveness, type Live, type LiveBeat } from "./liveness.ts";
6
- export type DhtMode = "off" | "client" | "server";
8
+ import { Liveness, type BeatStore, type Live, type LiveBeat } from "./liveness.ts";
9
+ export type DhtMode = "off" | "auto" | "client" | "server";
7
10
  /** The host-wide settings: every node gets these. */
8
11
  export interface P2PHostConfig {
9
12
  listen: string[];
@@ -11,12 +14,20 @@ export interface P2PHostConfig {
11
14
  dht: DhtMode;
12
15
  relays: string[];
13
16
  mdns: boolean;
17
+ /** UPnP/NAT-PMP port mapping (@libp2p/upnp-nat): SKEIN_LIBP2P_UPNP, on by default in hostP2PConfig; absent: off. */
18
+ upnp?: boolean;
14
19
  /** PEM certificate and key for the WebSocket listener (WSS). */
15
20
  tls?: {
16
21
  cert: string;
17
22
  key: string;
18
23
  };
24
+ /** The network (SKEIN_NETWORK's names): the skein DHT's protocol names it (skeinDhtProtocol); default main. */
25
+ network?: string;
19
26
  }
27
+ /** SKEIN_NETWORK's names as the networks' own (the Teranode block topic's, the skein DHT's). */
28
+ export declare const NETWORK_NAMES: Record<string, string>;
29
+ /** The skein DHT's protocol for a network: the overlay's provider records, never merged with another network's. */
30
+ export declare const skeinDhtProtocol: (network?: string) => string;
20
31
  /** One instance's libp2p config (the genesis's `libp2p`): what the router subscribes and serves for it. */
21
32
  export interface P2PInstanceConfig {
22
33
  topics: string[];
@@ -24,8 +35,18 @@ export interface P2PInstanceConfig {
24
35
  /** This instance's listen addresses (over the host's): a fixed port belongs to one node. */
25
36
  listen?: string[];
26
37
  }
27
- /** The default listeners: loopback TCP and WebSocket, ports chosen by the OS (plain WS on loopback: dev only). */
38
+ /**
39
+ * The default TCP port (2026-11-01): one fixed port on every install, so a user who opens one port knows
40
+ * which. 7534 is "SKEI" on a phone keypad; unassigned in the IANA registry (not IPFS's 4001, not
41
+ * Teranode's 9905/9915).
42
+ */
43
+ export declare const DEFAULT_PORT = 7534;
44
+ /** The default listeners: TCP on every interface, IPv4 and IPv6, on DEFAULT_PORT; WebSocket on loopback, its port the OS's (plain WS off loopback: dev only). */
28
45
  export declare const DEFAULT_LISTEN: string[];
46
+ /** Loopback only, ports chosen by the OS: the router's listeners when it is given no host settings (tests, an embedded router). */
47
+ export declare const LOOPBACK_LISTEN: string[];
48
+ /** `listen` with DEFAULT_PORT made an OS-chosen port (the fallback when the default port is taken). */
49
+ export declare const anyPort: (listen: string[]) => string[];
29
50
  /** What the front door is called with for an inbound message or stream frame (#51). */
30
51
  export interface InboundCall {
31
52
  transport: "libp2p";
@@ -85,7 +106,16 @@ export interface P2POptions {
85
106
  hostProtocols?: Record<string, (handle: string, remote: PeerId, frame: Uint8Array) => Promise<Uint8Array>>;
86
107
  /** Each beat the liveness tool newly keeps from another peer (shruggr/skein#150). */
87
108
  beat?(handle: string, topic: string, beat: LiveBeat): void;
109
+ /** Where the liveness tool persists the beats it keeps (host.db `live_beats`), reloaded when an instance's liveness is set. Absent: memory only. */
110
+ beats?: BeatStore;
111
+ /**
112
+ * The host node's peer key (#156): on a one-key host the key every instance's node uses (keyOf's
113
+ * answer for every handle), so the instances join the host node. Absent: a fresh key (a test's).
114
+ */
115
+ hostKey?: PrivateKey;
88
116
  }
117
+ /** A host topic's judge (hostTopic): each message's data and publisher → GossipSub's verdict. */
118
+ export type HostTopicHandler = (data: Uint8Array, from: PeerId) => "accept" | "reject" | "ignore";
89
119
  /** A bsv secp256k1 key as a libp2p private key. */
90
120
  export declare function libp2pKey(k: PrivateKey): import("@libp2p/interface").PrivateKey;
91
121
  /** The peer ID of a key: the identity multihash of its protobuf (secp256k1, compressed). */
@@ -94,7 +124,20 @@ export declare function peerIdOf(k: PrivateKey): PeerId;
94
124
  export declare function keyOfPeerId(id: string | Uint8Array): string | undefined;
95
125
  /** A topic name's rendezvous CID: v1 raw over sha2-256 of the name (go-libp2p's nsToCid). */
96
126
  export declare function topicCid(topic: string): Promise<CID>;
97
- /** The host-wide settings: the environment's SKEIN_LIBP2P_*, else $SKEIN_HOME/host.env's. */
127
+ /**
128
+ * The network's overlay bootstrap peers (SKEIN_NETWORK's names): the hosted Open Exchange node per network,
129
+ * dialled beside SKEIN_LIBP2P_BOOTSTRAP's (unioned, as TERANODE_BOOTSTRAP is with SKEIN_TERANODE_BOOTSTRAP).
130
+ * A node whose own peer id is one of them skips it (discover()). Mainnet and regtest have none yet.
131
+ */
132
+ export declare const OVERLAY_BOOTSTRAP: Record<string, string[]>;
133
+ /** SKEIN_LIBP2P_BOOTSTRAP over the network's defaults: unset, the network's; a list, unioned with them; `off`, none at all. */
134
+ export declare function overlayBootstrap(network: string, named: string | undefined): string[];
135
+ /**
136
+ * The bootstrap addresses a node dials now: not its own peer id (the hosted node is its network's default
137
+ * bootstrap), not a peer already connected; an address that does not parse is reported and skipped.
138
+ */
139
+ export declare function bootstrapDials(bootstrap: string[], self: string, connected: Set<string>, bad?: (a: string) => void): string[];
140
+ /** The host-wide settings: the environment's SKEIN_LIBP2P_* (and SKEIN_NETWORK), else $SKEIN_HOME/host.env's. */
98
141
  export declare function hostP2PConfig(vars: Record<string, string | undefined>, home?: string): P2PHostConfig;
99
142
  /** The libp2p config a genesis carries (`libp2p: {topics?, protocols?, listen?}`), or undefined: no node. */
100
143
  export declare function libp2pOf(g: Record<string, unknown> | null | undefined): P2PInstanceConfig | undefined;
@@ -214,7 +257,7 @@ export declare function foldBeacons(events: Iterable<BeaconEvent>, into?: Beacon
214
257
  export declare const TOPIC_REQUIRED = "topic required";
215
258
  export declare class P2PHost {
216
259
  readonly o: P2POptions;
217
- /** The liveness tool's state (#138): the apps' liveness and the beats kept, in memory. */
260
+ /** The liveness tool's state (#138): the apps' liveness and the beats kept (persisted in `o.beats` when given). */
218
261
  readonly live: Liveness;
219
262
  /** The nodes, by peer ID (#160: one per key). */
220
263
  private nodes;
@@ -225,10 +268,35 @@ export declare class P2PHost {
225
268
  /** Each key's declares, in order (one at a time). */
226
269
  private starting;
227
270
  private stopped;
271
+ /** The host node's key (hostKey, else a fresh one) and its peer ID. */
272
+ private readonly hostKey;
273
+ readonly hostId: string;
228
274
  constructor(o: P2POptions);
229
275
  private say;
230
- /** A node's name in a log line: its one handle (as before #160), else its first. */
276
+ /** A node's name in a log line: its one handle (as before #160), else its first; the host node with none: the router. */
231
277
  private nameOf;
278
+ /**
279
+ * The host node: the node of the host key, started (once) with no handles when no instance has
280
+ * started it yet; an instance on that key that starts later joins it (#160), one that started
281
+ * earlier made it.
282
+ */
283
+ hostNode(): Promise<Libp2p>;
284
+ /** The host node's record, once it runs. */
285
+ private hostOf;
286
+ /**
287
+ * A host topic on the host node (#156: the Teranode block topic): subscribed, each message judged
288
+ * by `take` alone (its verdict GossipSub's), never the front door, never advertised on the skein
289
+ * DHT. Undefined `take`: the topic left.
290
+ */
291
+ hostTopic(topic: string, take: HostTopicHandler | undefined): Promise<void>;
292
+ /** The peers the host node knows subscribe `topic` (GossipSub's view). */
293
+ hostSubscribers(topic: string): string[];
294
+ /** The host node's skein DHT's mode now (client | server; undefined: off or no host node yet). */
295
+ hostDhtMode(): "client" | "server" | undefined;
296
+ /** The host node's confirmed public addresses (AutoNAT's, or UPnP's once AutoNAT confirmed them): libp2p announces only confirmed ones; relay addresses aside. */
297
+ hostPublicAddrs(): string[];
298
+ /** The host node's DHT service, the skein DHT (undefined: SKEIN_LIBP2P_DHT=off, or no host node yet). */
299
+ hostDht(): KadDHT | undefined;
232
300
  /** The node of an instance, if it has one. */
233
301
  node(handle: string): Libp2p | undefined;
234
302
  /** An instance's peer ID (derived: whether or not its node runs). */
@@ -255,7 +323,7 @@ export declare class P2PHost {
255
323
  private subscribe;
256
324
  /** The topics `handle` takes: its config's and its apps' liveness topics (#138). */
257
325
  private takes;
258
- /** Subscribe what the handles' configs and liveness tools (#138) take, unsubscribe the rest: the topics added and removed. */
326
+ /** Subscribe what the handles' configs and liveness tools (#138) take, and the host's own topics (#156), unsubscribe the rest: the topics added and removed. */
259
327
  private resubscribe;
260
328
  /** Handle the protocols the handles declare, unhandle those none declares any more (the host's own aside). */
261
329
  private rehandle;
@@ -271,6 +339,7 @@ export declare class P2PHost {
271
339
  private hostServed;
272
340
  private handleHostProtocol;
273
341
  private handleProtocol;
342
+ /** A node for `key` (its private key `pk`): with `handle` and its config, or the host node with none (#156). */
274
343
  private start;
275
344
  /** Keep the bootstrap peers connected; with the DHT on, advertise each topic and dial its providers. */
276
345
  private discover;
@@ -335,3 +404,33 @@ export declare class P2PHost {
335
404
  private drop;
336
405
  stop(): Promise<void>;
337
406
  }
407
+ /** A multiaddr on a loopback or private network (dev: the DHT keeps such addresses). */
408
+ export declare function isPrivateAddr(ma: Multiaddr): boolean;
409
+ /**
410
+ * kad-DHT's clientMode for a mode of the skein DHT (2026-11-01; @libp2p/kad-dht 16.4.5 kad-dht.js): `auto` leaves it
411
+ * unset — the DHT's own switch (a client, a server while the node has a confirmed public address);
412
+ * `client` true — only ever a client; `server` false — a server from its start (its start sets the mode),
413
+ * never switched back. An explicit false is NOT auto in this version: the switch listens only when unset.
414
+ */
415
+ export declare function dhtClientMode(mode: Exclude<DhtMode, "off">): {
416
+ clientMode?: boolean;
417
+ };
418
+ /**
419
+ * The libp2p logger, with the UPnP service's outcome told once per distinct line (2026-11-01): the gateway
420
+ * search's result, the public IP found, each mapping made or refused, an error. The service reports its
421
+ * outcome nowhere else (its log only); every other component's log is the default, untouched.
422
+ */
423
+ export declare function upnpTold(tell: (line: string) => void): ComponentLogger;
424
+ /** A node's DHT service, the skein DHT (#156), or undefined (SKEIN_LIBP2P_DHT=off). */
425
+ export declare function dhtOf(node: Libp2p): KadDHT | undefined;
426
+ /** The providers a DHT finds for `cid` (its PROVIDER events' peers), on that DHT alone. */
427
+ export declare function providersOf(dht: KadDHT, cid: CID, options?: {
428
+ signal?: AbortSignal;
429
+ }): AsyncIterable<{
430
+ id: PeerId;
431
+ multiaddrs: Multiaddr[];
432
+ }>;
433
+ /** Run a DHT operation's events to the end. */
434
+ export declare function drain(events: AsyncIterable<unknown>): Promise<void>;
435
+ /** A DHT's routing table size (undefined: no such DHT). */
436
+ export declare function tableSize(dht: KadDHT | undefined): number | undefined;