@statewalker/httpeers-libp2p 0.1.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 (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +173 -0
  3. package/dist/browser.d.ts +33 -0
  4. package/dist/browser.d.ts.map +1 -0
  5. package/dist/browser.js +77 -0
  6. package/dist/duplex.d.ts +86 -0
  7. package/dist/duplex.d.ts.map +1 -0
  8. package/dist/duplex.js +101 -0
  9. package/dist/hop-reserve.d.ts +105 -0
  10. package/dist/hop-reserve.d.ts.map +1 -0
  11. package/dist/hop-reserve.js +269 -0
  12. package/dist/hub-link.d.ts +131 -0
  13. package/dist/hub-link.d.ts.map +1 -0
  14. package/dist/hub-link.js +217 -0
  15. package/dist/hub-relay.d.ts +129 -0
  16. package/dist/hub-relay.d.ts.map +1 -0
  17. package/dist/hub-relay.js +154 -0
  18. package/dist/identity.d.ts +84 -0
  19. package/dist/identity.d.ts.map +1 -0
  20. package/dist/identity.js +91 -0
  21. package/dist/index.d.ts +28 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +26 -0
  24. package/dist/link.d.ts +49 -0
  25. package/dist/link.d.ts.map +1 -0
  26. package/dist/link.js +88 -0
  27. package/dist/multiaddr-parts.d.ts +21 -0
  28. package/dist/multiaddr-parts.d.ts.map +1 -0
  29. package/dist/multiaddr-parts.js +27 -0
  30. package/dist/node.d.ts +33 -0
  31. package/dist/node.d.ts.map +1 -0
  32. package/dist/node.js +60 -0
  33. package/dist/reservation.d.ts +245 -0
  34. package/dist/reservation.d.ts.map +1 -0
  35. package/dist/reservation.js +396 -0
  36. package/dist/serve-peer.d.ts +91 -0
  37. package/dist/serve-peer.d.ts.map +1 -0
  38. package/dist/serve-peer.js +68 -0
  39. package/dist/timers.d.ts +36 -0
  40. package/dist/timers.d.ts.map +1 -0
  41. package/dist/timers.js +38 -0
  42. package/dist/transport.d.ts +314 -0
  43. package/dist/transport.d.ts.map +1 -0
  44. package/dist/transport.js +410 -0
  45. package/package.json +73 -0
  46. package/src/browser.ts +118 -0
  47. package/src/duplex.ts +172 -0
  48. package/src/hop-reserve.ts +295 -0
  49. package/src/hub-link.ts +281 -0
  50. package/src/hub-relay.ts +188 -0
  51. package/src/identity.ts +155 -0
  52. package/src/index.ts +114 -0
  53. package/src/link.ts +143 -0
  54. package/src/multiaddr-parts.ts +29 -0
  55. package/src/node.ts +63 -0
  56. package/src/reservation.ts +564 -0
  57. package/src/serve-peer.ts +141 -0
  58. package/src/timers.ts +47 -0
  59. package/src/transport.ts +538 -0
package/src/index.ts ADDED
@@ -0,0 +1,114 @@
1
+ /**
2
+ * The transport — the one package in the extraction that knows what libp2p is.
3
+ *
4
+ * An explicit list, never `export *`: the barrel this code came from ended
5
+ * with `export * from "./transport-duplex.js"`, which is how a package that
6
+ * documented itself as isomorphic pulled libp2p, TCP, Noise and yamux into
7
+ * every consumer's bundle.
8
+ */
9
+
10
+ /**
11
+ * The SECOND ALTITUDE: duplex streams over the mesh.
12
+ *
13
+ * A fetch-only contract cannot express a WebSocket — both sides talking with
14
+ * neither input closed. These run one `Duplex` per libp2p stream, addressed by
15
+ * `(peerId, path)`, with identity captured per stream by closure exactly as
16
+ * the fetch path does it.
17
+ */
18
+ export {
19
+ createDuplexMounts,
20
+ DUPLEX_PROTOCOL,
21
+ type DuplexContext,
22
+ type DuplexHandler,
23
+ type DuplexMounts,
24
+ NO_MOUNT,
25
+ type OpenDuplexInit,
26
+ openDuplex,
27
+ type PeerDuplex,
28
+ type ServeDuplexInit,
29
+ serveDuplex,
30
+ } from "./duplex.js";
31
+ export {
32
+ decodeHopAnswer,
33
+ HOP_RESERVE_TIMEOUT_MS,
34
+ HOP_STATUS_NAMES,
35
+ HOP_STATUS_OK,
36
+ type HopAnswer,
37
+ RelayNotConnectedError,
38
+ type RelayReservationGrant,
39
+ RelayReservationRefusedError,
40
+ requestRelayReservation,
41
+ } from "./hop-reserve.js";
42
+ export {
43
+ HubReservationError,
44
+ type HubReservationRefusal,
45
+ hubRoute,
46
+ leaveRelay,
47
+ reachHub,
48
+ reachHubRelayed,
49
+ reserveOnHub,
50
+ type SuperviseHubReservationInit,
51
+ superviseHubReservation,
52
+ } from "./hub-link.js";
53
+ export {
54
+ HUB_MAX_RESERVATIONS,
55
+ type HubRelayComponents,
56
+ type HubRelayServiceInit,
57
+ hubRelayService,
58
+ type IsMember,
59
+ membershipGater,
60
+ releaseReservation,
61
+ } from "./hub-relay.js";
62
+ export {
63
+ type BytesStore,
64
+ decodeKey,
65
+ encodeKey,
66
+ type GenerateKeyInit,
67
+ generateKey,
68
+ type IdentityStore,
69
+ type IdentityStoreInit,
70
+ identityStore,
71
+ peerIdOf,
72
+ type SignerLike,
73
+ signerOf,
74
+ } from "./identity.js";
75
+ export type { CallOnLimitedConnection } from "./link.js";
76
+ export { lastPeerIdOf } from "./multiaddr-parts.js";
77
+ export {
78
+ type CircuitAddrs,
79
+ circuitAddrs,
80
+ DEFAULT_RENEWAL_INTERVAL_MS,
81
+ dialRelay,
82
+ MAX_RENEWAL_INTERVAL_MS,
83
+ MIN_RENEWAL_INTERVAL_MS,
84
+ RESERVATION_LOSS_GRACE_MS,
85
+ RESERVATION_POLL_ATTEMPTS,
86
+ RESERVATION_POLL_INTERVAL_MS,
87
+ type RelayLog,
88
+ type RelayReservationEvent,
89
+ type RelayReservationState,
90
+ type RelayReservationStatus,
91
+ type RelaySupervisor,
92
+ renewalDelayMs,
93
+ renewalIntervalMs,
94
+ reservationHealthy,
95
+ retryDelayMs,
96
+ type SuperviseRelayInit,
97
+ superviseRelay,
98
+ type WaitForCircuitReservationInit,
99
+ waitForCircuitReservation,
100
+ } from "./reservation.js";
101
+ export { type Peer, type ServePeerInit, servePeer } from "./serve-peer.js";
102
+ export { globalTimers, type TimerHandle, type Timers } from "./timers.js";
103
+ export {
104
+ type CreateNodeInit,
105
+ type CreateRemoteInit,
106
+ createNode,
107
+ createRemote,
108
+ DEFAULT_DRAIN_TIMEOUT_MS,
109
+ DEFAULT_MAX_STREAMS,
110
+ PROTOCOL,
111
+ type ServeTransportInit,
112
+ serveTransport,
113
+ type TransportFactory,
114
+ } from "./transport.js";
package/src/link.ts ADDED
@@ -0,0 +1,143 @@
1
+ /**
2
+ * libp2p, as a `PeerLink`.
3
+ *
4
+ * This is the whole of what makes the bridge libp2p-specific: how to open a
5
+ * duplex to a peer, and how to accept one. Everything the bridge does with
6
+ * those duplexes -- binding the proven peer, one stream per call, the permit,
7
+ * the timeout -- lives in `@statewalker/httpeers-bridge` and is shared with
8
+ * every other transport, including the MessagePort one the tests use.
9
+ *
10
+ * `serveConnections` is used rather than `node.handle` by hand because it
11
+ * already threads a `ConnectionContext`, and `context.remotePeer` is the
12
+ * answer the Noise handshake established. That value is what the bridge binds
13
+ * to every inbound request, and it is the reason a peer cannot lie about who
14
+ * it is no matter what headers it sets.
15
+ */
16
+
17
+ import type { Libp2p } from "@libp2p/interface";
18
+ import { peerIdFromString } from "@libp2p/peer-id";
19
+ import { multiaddr } from "@multiformats/multiaddr";
20
+ import type { PeerConnection, PeerLink } from "@statewalker/httpeers-bridge";
21
+ import type { PeerIdStr, ProvenPeer } from "@statewalker/httpeers-core";
22
+ import type { Duplex } from "@statewalker/webrun-streams";
23
+ import {
24
+ type ConnectionContext,
25
+ connect,
26
+ serveConnections,
27
+ } from "@statewalker/webrun-streams-libp2p";
28
+ import { DEFAULT_DRAIN_TIMEOUT_MS, DEFAULT_MAX_STREAMS, PROTOCOL } from "./transport.js";
29
+
30
+ export interface Libp2pLinkInit {
31
+ node: Libp2p;
32
+ /** Defaults to `PROTOCOL`. Two meshes on one node need two protocols. */
33
+ protocol?: string;
34
+ drainTimeoutMs?: number;
35
+ maxInboundStreams?: number;
36
+ maxOutboundStreams?: number;
37
+ /**
38
+ * ACCEPT streams on LIMITED connections (a relay circuit). Off by default:
39
+ * libp2p refuses, and that refusal is what keeps application traffic off a
40
+ * relay's small budget. A hub reached over the public relay sets it; a member
41
+ * never does, so its hub's relay limits still bound member-to-member traffic.
42
+ */
43
+ serveOnLimitedConnection?: boolean;
44
+ /** OPEN streams on limited connections, to the peers this allows. See `CallOnLimitedConnection`. */
45
+ callOnLimitedConnection?: CallOnLimitedConnection;
46
+ }
47
+
48
+ /**
49
+ * Which peers a call may reach over a LIMITED connection: `true` for all of
50
+ * them, or a predicate evaluated per target when the call opens its stream.
51
+ *
52
+ * A PREDICATE, NOT A BOOLEAN, because the one caller that needs it -- a member
53
+ * whose hub is reachable only through the relay -- must say "the hub" and
54
+ * nothing wider: `(peerId) => peerId === hubPeerId`. Separate from
55
+ * `serveOnLimitedConnection` for the same reason: a member calls its hub over
56
+ * the circuit without opening its own serving side to circuits.
57
+ */
58
+ export type CallOnLimitedConnection = boolean | ((peerId: PeerIdStr) => boolean);
59
+
60
+ /** Wrap a running libp2p node as the one seam the bridge needs. */
61
+ export function libp2pLink(init: Libp2pLinkInit): PeerLink {
62
+ const {
63
+ node,
64
+ protocol = PROTOCOL,
65
+ drainTimeoutMs = DEFAULT_DRAIN_TIMEOUT_MS,
66
+ maxInboundStreams = DEFAULT_MAX_STREAMS,
67
+ maxOutboundStreams = DEFAULT_MAX_STREAMS,
68
+ serveOnLimitedConnection,
69
+ callOnLimitedConnection = false,
70
+ } = init;
71
+
72
+ return {
73
+ async open(peerId: PeerIdStr): Promise<PeerConnection> {
74
+ // A BARE `/p2p/<id>` dial, deliberately: a peer dialled once by full
75
+ // multiaddr is dialable again by id alone, and composing an address here
76
+ // would duplicate the routing decisions `reservation.ts` already made.
77
+ const limited =
78
+ typeof callOnLimitedConnection === "function"
79
+ ? callOnLimitedConnection(peerId)
80
+ : callOnLimitedConnection;
81
+ const conn = await connect({
82
+ node: limited ? keptCircuitFirst(node, peerId) : node,
83
+ peer: multiaddr(`/p2p/${peerId}`),
84
+ protocol,
85
+ drainTimeoutMs,
86
+ maxOutboundStreams,
87
+ ...(limited ? { runOnLimitedConnection: true } : {}),
88
+ });
89
+ return { call: conn.call as Duplex, close: async () => await conn.close() };
90
+ },
91
+
92
+ async serve(handlerFor: (peer: ProvenPeer) => Duplex): Promise<() => Promise<void>> {
93
+ return serveConnections(
94
+ {
95
+ node,
96
+ protocol,
97
+ drainTimeoutMs,
98
+ maxInboundStreams,
99
+ maxOutboundStreams,
100
+ runOnLimitedConnection: serveOnLimitedConnection,
101
+ },
102
+ (context: ConnectionContext) => handlerFor(context.remotePeer.toString()) as never,
103
+ );
104
+ },
105
+ };
106
+ }
107
+
108
+ /**
109
+ * `node`, except that a stream to `peerId` opens on the LIMITED connection
110
+ * already held to it when that is the only kind there is. Used only for a
111
+ * target `callOnLimitedConnection` allows.
112
+ *
113
+ * WHY THE FLAG ALONE IS NOT ENOUGH. libp2p 3.3.8 does not reuse a limited
114
+ * connection for a dial: `findExistingConnection`
115
+ * (`connection-manager/utils.js`) returns only connections with
116
+ * `limits == null`, so `dialProtocol` to a peer reachable only by a kept relay
117
+ * circuit dials AGAIN, from the peer store -- a second circuit per call, through
118
+ * whichever of the relay's addresses identify taught it. The
119
+ * relay-fallback conformance test measured exactly that (two new connections
120
+ * for two requests) before this existed. The kept connection is the one the
121
+ * caller chose; the stream goes there.
122
+ *
123
+ * An UNLIMITED connection still wins when there is one (a WebRTC upgrade that
124
+ * did come up): that case is left to libp2p's own dial, which reuses it.
125
+ *
126
+ * A NARROW STAND-IN, not a wrapper: `connect` (`webrun-streams-libp2p`) reads
127
+ * nothing from the node but `dialProtocol`, and giving that package a way to
128
+ * take a `Connection` would be a change in another repository for one caller.
129
+ */
130
+ function keptCircuitFirst(node: Libp2p, peerId: PeerIdStr): Libp2p {
131
+ const dialer: Pick<Libp2p, "dialProtocol"> = {
132
+ dialProtocol: async (peer, protocols, options) => {
133
+ const open = node.getConnections(peerIdFromString(peerId)).filter((c) => c.status === "open");
134
+ const kept = open.some((c) => c.limits == null)
135
+ ? undefined
136
+ : open.find((c) => c.limits != null);
137
+ return kept != null
138
+ ? kept.newStream(protocols, options)
139
+ : node.dialProtocol(peer, protocols, options);
140
+ },
141
+ };
142
+ return dialer as Libp2p;
143
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Reading a peer id out of a multiaddr, without `Array.prototype.findLast`.
3
+ *
4
+ * The prototype used `findLast`, which needs `lib: ES2023`. Raising this
5
+ * package's lib would raise the RUNTIME floor for every consumer — the emitted
6
+ * code calls the method regardless of what the types allow — for one line of
7
+ * convenience. A reverse loop costs nothing and keeps the floor at ES2022,
8
+ * matching every other package here.
9
+ *
10
+ * To reverse: set `"lib": ["ES2023"]` in tsconfig.json and inline
11
+ * `.getComponents().findLast((c) => c.name === "p2p")?.value` at the two call
12
+ * sites (`reservation.ts`, `hub-link.ts`).
13
+ *
14
+ * LAST, not first, and that is the whole point: a circuit address is
15
+ * `/…/p2p/<relay>/p2p-circuit/p2p/<target>`, so the FIRST `p2p` component is
16
+ * the relay and the last is whoever the address finally names. Taking the
17
+ * first would hang up on the relay when asked to hang up on a peer.
18
+ */
19
+
20
+ import type { Multiaddr } from "@multiformats/multiaddr";
21
+
22
+ export function lastPeerIdOf(addr: Multiaddr): string | undefined {
23
+ const components = addr.getComponents();
24
+ for (let i = components.length - 1; i >= 0; i--) {
25
+ const component = components[i];
26
+ if (component?.name === "p2p") return component.value;
27
+ }
28
+ return undefined;
29
+ }
package/src/node.ts ADDED
@@ -0,0 +1,63 @@
1
+ /**
2
+ * The Node profile. Everything here is why the root export is not this.
3
+ *
4
+ * `tcp()` cannot run in a browser, and the prototype hard-coded it inside its
5
+ * node factory — so importing the transport module at all dragged a Node-only
6
+ * transport into a browser bundle, in a package whose whole claim is that one
7
+ * implementation runs on both. Separating the profiles is the fix, and the
8
+ * root's boundary test asserts `@libp2p/tcp` never appears there.
9
+ */
10
+
11
+ import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
12
+ import { join } from "node:path";
13
+ import { tcp } from "@libp2p/tcp";
14
+ import type { BytesStore } from "./identity.js";
15
+ import type { TransportFactory } from "./transport.js";
16
+
17
+ /**
18
+ * Transports for a server: TCP, and nothing a page could not also do.
19
+ *
20
+ * Listed as a function rather than a constant so a caller gets fresh factory
21
+ * instances — libp2p's are not reusable across nodes.
22
+ */
23
+ export function nodeTransports(): TransportFactory[] {
24
+ return [tcp() as unknown as TransportFactory];
25
+ }
26
+
27
+ /**
28
+ * A `BytesStore` on the filesystem — where a server keeps its identity.
29
+ *
30
+ * WRITE-THEN-MOVE, not write-in-place. A key file truncated by a crash
31
+ * mid-write is not a corrupt file you notice, it is a mesh that silently
32
+ * re-founds itself on the next start: `identityStore.loadOrCreate` sees
33
+ * unreadable bytes, generates a new key, and every token the old identity
34
+ * issued stops verifying. A rename is atomic on POSIX, so the file is either
35
+ * the old key or the new one and never half of either.
36
+ *
37
+ * The prototype's snapshot store wrote in place; extraction prototype 04
38
+ * measured the consequence and this is the shape that survived it.
39
+ */
40
+ export function fileBytesStore(dir: string): BytesStore {
41
+ return {
42
+ async get(key) {
43
+ try {
44
+ return new Uint8Array(await readFile(join(dir, key)));
45
+ } catch (error) {
46
+ if ((error as { code?: string }).code === "ENOENT") return undefined;
47
+ throw error;
48
+ }
49
+ },
50
+ async set(key, value) {
51
+ await mkdir(dir, { recursive: true });
52
+ const target = join(dir, key);
53
+ // Same directory, so the rename cannot cross a filesystem boundary —
54
+ // which is the one case where it would stop being atomic.
55
+ const staging = `${target}.${process.pid}.${Date.now()}.tmp`;
56
+ await writeFile(staging, value, { mode: 0o600 });
57
+ await rename(staging, target);
58
+ },
59
+ async delete(key) {
60
+ await rm(join(dir, key), { force: true });
61
+ },
62
+ };
63
+ }