@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.
- package/LICENSE +21 -0
- package/README.md +173 -0
- package/dist/browser.d.ts +33 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +77 -0
- package/dist/duplex.d.ts +86 -0
- package/dist/duplex.d.ts.map +1 -0
- package/dist/duplex.js +101 -0
- package/dist/hop-reserve.d.ts +105 -0
- package/dist/hop-reserve.d.ts.map +1 -0
- package/dist/hop-reserve.js +269 -0
- package/dist/hub-link.d.ts +131 -0
- package/dist/hub-link.d.ts.map +1 -0
- package/dist/hub-link.js +217 -0
- package/dist/hub-relay.d.ts +129 -0
- package/dist/hub-relay.d.ts.map +1 -0
- package/dist/hub-relay.js +154 -0
- package/dist/identity.d.ts +84 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +91 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/link.d.ts +49 -0
- package/dist/link.d.ts.map +1 -0
- package/dist/link.js +88 -0
- package/dist/multiaddr-parts.d.ts +21 -0
- package/dist/multiaddr-parts.d.ts.map +1 -0
- package/dist/multiaddr-parts.js +27 -0
- package/dist/node.d.ts +33 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +60 -0
- package/dist/reservation.d.ts +245 -0
- package/dist/reservation.d.ts.map +1 -0
- package/dist/reservation.js +396 -0
- package/dist/serve-peer.d.ts +91 -0
- package/dist/serve-peer.d.ts.map +1 -0
- package/dist/serve-peer.js +68 -0
- package/dist/timers.d.ts +36 -0
- package/dist/timers.d.ts.map +1 -0
- package/dist/timers.js +38 -0
- package/dist/transport.d.ts +314 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +410 -0
- package/package.json +73 -0
- package/src/browser.ts +118 -0
- package/src/duplex.ts +172 -0
- package/src/hop-reserve.ts +295 -0
- package/src/hub-link.ts +281 -0
- package/src/hub-relay.ts +188 -0
- package/src/identity.ts +155 -0
- package/src/index.ts +114 -0
- package/src/link.ts +143 -0
- package/src/multiaddr-parts.ts +29 -0
- package/src/node.ts +63 -0
- package/src/reservation.ts +564 -0
- package/src/serve-peer.ts +141 -0
- package/src/timers.ts +47 -0
- 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
|
+
}
|