@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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mikhail Kotelnikov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# @statewalker/httpeers-libp2p
|
|
2
|
+
|
|
3
|
+
The transport: nodes, identity, reachability, and `servePeer` — **the one
|
|
4
|
+
package in the extraction that knows what libp2p is.**
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
import { createNode, generateKey, servePeer } from "@statewalker/httpeers-libp2p";
|
|
8
|
+
import { nodeTransports } from "@statewalker/httpeers-libp2p/node";
|
|
9
|
+
|
|
10
|
+
const node = await createNode({ privateKey: await generateKey(), transports: nodeTransports() });
|
|
11
|
+
const peer = await servePeer({ node, mounts, access: guard });
|
|
12
|
+
|
|
13
|
+
await peer.call("12D3KooWOther", new Request("http://peer.local/hello"));
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Everything above this package speaks `FetchHandler` and `Mounts`. Nothing above
|
|
17
|
+
it imports `libp2p`.
|
|
18
|
+
|
|
19
|
+
## `servePeer` is the seam three assemblies collapse into
|
|
20
|
+
|
|
21
|
+
A hub, a Node member and a page were each wiring a node, a router, an inbound
|
|
22
|
+
handler and an outbound dialler by hand. `servePeer` is that arrangement, once:
|
|
23
|
+
it owns the wire and the router, and it takes `access` as a parameter rather
|
|
24
|
+
than knowing what a token is.
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
servePeer ──owns──> the protocol handler, the router, the outbound dialler
|
|
28
|
+
──takes──> access: (handler) => handler ← withAccess, from httpeers-access
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The two never import each other. `httpeers-access` has no transport and this
|
|
32
|
+
has no crypto policy; the graph runs `core → access` and `core → libp2p`, and
|
|
33
|
+
`tests/boundary.test.ts` fails if either starts pointing at the other.
|
|
34
|
+
|
|
35
|
+
`Peer.dispatch` is exposed deliberately — it is the inbound router *after*
|
|
36
|
+
identity binding and policy, which is what lets a local edge reuse the same
|
|
37
|
+
decision path instead of building a second one.
|
|
38
|
+
|
|
39
|
+
## Transports are a parameter, and that was a defect fix
|
|
40
|
+
|
|
41
|
+
`createNode` **requires** `transports`. The prototype hard-coded `tcp()` inside
|
|
42
|
+
its node factory, so importing the transport module at all dragged a Node-only
|
|
43
|
+
transport into a browser bundle — in a package whose whole claim was that one
|
|
44
|
+
implementation runs on both.
|
|
45
|
+
|
|
46
|
+
| Import | Holds | Why it cannot be at the root |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| `.` | `createNode`, `servePeer`, identity, reachability, duplex | — |
|
|
49
|
+
| `./node` | `nodeTransports()` (adds `tcp`), `fileBytesStore` | `@libp2p/tcp` cannot run in a browser; `node:fs` cannot either |
|
|
50
|
+
| `./browser` | `browserTransports()`, `idbBytesStore` | IndexedDB |
|
|
51
|
+
|
|
52
|
+
The root's boundary test asserts `@libp2p/tcp` and `node:` never appear there.
|
|
53
|
+
Note what is *not* forbidden: `@libp2p/webrtc` is at the root, because a page
|
|
54
|
+
needs it and a Node process can load it too — though only if its native
|
|
55
|
+
dependency actually built. See the note on `node-datachannel` at the repository
|
|
56
|
+
root.
|
|
57
|
+
|
|
58
|
+
An explicit export list, never `export *`: the barrel this code came from ended
|
|
59
|
+
with `export * from "./transport-duplex.js"`, which is how a package that
|
|
60
|
+
documented itself as isomorphic pulled libp2p, TCP, Noise and yamux into every
|
|
61
|
+
consumer's bundle.
|
|
62
|
+
|
|
63
|
+
## Reachability, which is most of what is here
|
|
64
|
+
|
|
65
|
+
A browser cannot listen for inbound TCP. It reserves a slot on a relay and
|
|
66
|
+
accepts an upgrade brokered over it, and the modules here are the steps of
|
|
67
|
+
that:
|
|
68
|
+
|
|
69
|
+
| Module | What it does |
|
|
70
|
+
|---|---|
|
|
71
|
+
| `reservation` | `dialRelay`, `waitForCircuitReservation`, `circuitAddrs`, `superviseRelay` (+ `reservationHealthy`, `renewalIntervalMs`) |
|
|
72
|
+
| `hop-reserve` | `requestRelayReservation` — ask the relay itself, in its own protocol |
|
|
73
|
+
| `timers` | the injectable `Timers` seam (`worker-timers` in a background tab) |
|
|
74
|
+
| `hub-link` | `reachHub`, `reachHubRelayed`, `reserveOnHub` (+ `HubReservationError`), `leaveRelay`, `superviseHubReservation` |
|
|
75
|
+
| `hub-relay` | `hubRelayService`, `membershipGater`, `releaseReservation` — a hub relaying for **its own members and nobody else** |
|
|
76
|
+
| `identity` | `generateKey`, `peerIdOf`, `signerOf`, the persisted `identityStore` |
|
|
77
|
+
| `duplex` | the second altitude — see below |
|
|
78
|
+
|
|
79
|
+
**A reservation yields two addresses and only one works.** The bare
|
|
80
|
+
`/p2p-circuit` entry is a *limited* connection on which libp2p silently refuses
|
|
81
|
+
the protocol; the `/webrtc`-suffixed one is the one to publish. `circuitAddrs`
|
|
82
|
+
returns both, labelled, so nobody has to rediscover which is which by pasting
|
|
83
|
+
the wrong one.
|
|
84
|
+
|
|
85
|
+
**Only the relay knows whether you are reserved.** `node.getMultiaddrs()` is
|
|
86
|
+
the node's own belief, and on 2026-09-19 a deployed hub held that belief for
|
|
87
|
+
hours while the relay held no reservation at all: the websocket was still up,
|
|
88
|
+
every member got `NO_RESERVATION`, and the supervisor — which decided from that
|
|
89
|
+
address list — saw nothing wrong. So `superviseRelay` asks the relay, on a
|
|
90
|
+
schedule derived from the TTL the relay itself granted (`renewalIntervalMs`: a
|
|
91
|
+
quarter of it, jittered down into its upper quarter; 22.5–30 minutes against a
|
|
92
|
+
two-hour TTL). The request is a circuit-relay v2 `HOP RESERVE` over the
|
|
93
|
+
existing connection (`requestRelayReservation`) — libp2p's own `addRelay` short
|
|
94
|
+
-circuits on its cached entry, is not reachable from `Libp2p`, and blacklists a
|
|
95
|
+
relay locally on failure. Because a relay's reservation store is keyed by peer,
|
|
96
|
+
that one request renews an entry that exists and re-creates one that does not,
|
|
97
|
+
reusing the slot and leaving live circuits alone.
|
|
98
|
+
|
|
99
|
+
The address list now proves a **loss** (it is gone) and never proves health (it
|
|
100
|
+
is there). `supervisor.state()` carries the whole picture — status,
|
|
101
|
+
`verifiedAt`, `expiresAt`, `lostSince`, consecutive failures, renewal and
|
|
102
|
+
restore counts, last error — and `reservationHealthy(state)` is the rule a
|
|
103
|
+
healthcheck should use: reserved, or lost for less than two minutes. Every
|
|
104
|
+
transition logs one line naming the relay and the reason; the old
|
|
105
|
+
implementation swallowed all of them.
|
|
106
|
+
|
|
107
|
+
`scripts/probe-hub.mjs <hubPeerId>` answers the same question from OUTSIDE, as
|
|
108
|
+
a member would: it dials `<relay>/p2p-circuit/p2p/<hub>` with an independent
|
|
109
|
+
libp2p client and prints one JSON line. Run it from this package's directory so
|
|
110
|
+
its dependencies resolve. That probe is how the incident was confirmed to be
|
|
111
|
+
real rather than a member-side fault, and it is the check to run first next
|
|
112
|
+
time.
|
|
113
|
+
|
|
114
|
+
**A refused reservation says which refusal it was.** libp2p reports every
|
|
115
|
+
failed reservation alike ("Some configured addresses failed to be listened
|
|
116
|
+
on"), with the relay's status only in the text. `reserveOnHub` reads it out and
|
|
117
|
+
throws a `HubReservationError` with `status` (the circuit-relay status, or
|
|
118
|
+
`null`) and `refusal`: `store-full` (`RESERVATION_REFUSED` — the hub's relay
|
|
119
|
+
holds as many reservations as it grants), `resource-limit`
|
|
120
|
+
(`RESOURCE_LIMIT_EXCEEDED`), `not-a-member` (`PERMISSION_DENIED`), `no-relay`
|
|
121
|
+
(the hub runs no relay service), `no-answer` (a timeout or a dropped link) or
|
|
122
|
+
`other`. It used to throw one message naming `PERMISSION_DENIED` whatever had
|
|
123
|
+
happened, and a full store was diagnosed in production as a membership problem.
|
|
124
|
+
|
|
125
|
+
**`membershipGater` takes a thunk, not a value.** A hub's node must exist
|
|
126
|
+
before the member store that answers "is this a member" does, and the thunk is
|
|
127
|
+
read at decision time, which is what closes that ordering cycle.
|
|
128
|
+
|
|
129
|
+
**A hub's reservation store is sized for a mesh, not for a public relay.**
|
|
130
|
+
libp2p's default holds 15 reservations for two hours each and keeps one after
|
|
131
|
+
its holder hangs up; a hub on it refused every member with
|
|
132
|
+
`RESERVATION_REFUSED` after fifteen distinct peers. `hubRelayService` sizes the
|
|
133
|
+
store at `HUB_MAX_RESERVATIONS` (4096; `{ maxReservations }` overrides it),
|
|
134
|
+
releases a reservation when its holder's last connection closes, and
|
|
135
|
+
`releaseReservation(relay, peerId)` frees a revoked member's slot at once. None
|
|
136
|
+
of this touches the per-circuit data limits (128 KiB, 2 min), which are what
|
|
137
|
+
keep a hub a signalling channel: the store says how *many* members are
|
|
138
|
+
reachable through the hub, not how *much* may cross it.
|
|
139
|
+
|
|
140
|
+
## `signerOf` — the bridge that stops a mesh naming nobody
|
|
141
|
+
|
|
142
|
+
A hub signs membership tokens with the same key its node speaks with. Build the
|
|
143
|
+
node from one key and mint with another, and every token's `mesh` claim names a
|
|
144
|
+
peer nobody is talking to — a failure that looks like a policy bug.
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
const signer = signerOf(hubKey); // { mesh: peerIdOf(hubKey), seed: key.raw.slice(0, 32) }
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The return type is declared **structurally** rather than imported from
|
|
151
|
+
`httpeers-access`, so this package does not depend on it. A mismatch is a
|
|
152
|
+
compile error in whichever assembly wires the two together, which is where it
|
|
153
|
+
belongs.
|
|
154
|
+
|
|
155
|
+
## The second altitude: duplex
|
|
156
|
+
|
|
157
|
+
A fetch-only contract cannot express a WebSocket — both sides talking with
|
|
158
|
+
neither input closed. `serveDuplex` / `openDuplex` run one `Duplex` per libp2p
|
|
159
|
+
stream, addressed by path through their own mount table, on a separate
|
|
160
|
+
protocol. This exists because the requirement did, not because `FetchHandler`
|
|
161
|
+
was inconvenient: it is a different shape, so it gets a different seam rather
|
|
162
|
+
than a flag.
|
|
163
|
+
|
|
164
|
+
## Tests
|
|
165
|
+
|
|
166
|
+
**68, plus 4 skipped** (the platform-entry exemptions in the boundary test).
|
|
167
|
+
`tests/hub-link.test.ts` pins how `reserveOnHub` reads each refusal out of
|
|
168
|
+
libp2p's error text. `tests/serve-peer.test.ts` stands up two real nodes over a real Noise
|
|
169
|
+
handshake and checks that what arrives carries the identity the **transport**
|
|
170
|
+
proved rather than anything the caller said.
|
|
171
|
+
|
|
172
|
+
The whole stack — relay, hub, members, tokens — is exercised together in
|
|
173
|
+
`@statewalker/httpeers-conformance`.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The browser profile.
|
|
3
|
+
*
|
|
4
|
+
* A page cannot listen and cannot open a TCP socket. It reaches the mesh by
|
|
5
|
+
* reserving on a relay and being dialled back through the circuit, so its
|
|
6
|
+
* transports are the three that make that work:
|
|
7
|
+
*
|
|
8
|
+
* webSockets — to reach the relay itself, which does listen.
|
|
9
|
+
* circuitRelayTransport — to be dialled through that reservation.
|
|
10
|
+
* webRTC — so that once two members have exchanged an offer
|
|
11
|
+
* over the circuit, the data goes directly and the
|
|
12
|
+
* hub stays a signalling channel.
|
|
13
|
+
*
|
|
14
|
+
* `tcp()` is absent because it cannot exist here, which is the whole reason
|
|
15
|
+
* this file is separate from `./node`.
|
|
16
|
+
*/
|
|
17
|
+
import type { BytesStore } from "./identity.js";
|
|
18
|
+
import type { TransportFactory } from "./transport.js";
|
|
19
|
+
/** Transports for a page. Fresh instances per call — libp2p's are not reusable. */
|
|
20
|
+
export declare function browserTransports(): TransportFactory[];
|
|
21
|
+
/**
|
|
22
|
+
* A `BytesStore` in IndexedDB — where a page keeps its identity.
|
|
23
|
+
*
|
|
24
|
+
* NOT `localStorage`, and the difference matters: a key is bytes, and
|
|
25
|
+
* `localStorage` stores strings, so every read and write would go through a
|
|
26
|
+
* base64 round trip that can silently truncate. IndexedDB stores a
|
|
27
|
+
* `Uint8Array` as itself.
|
|
28
|
+
*
|
|
29
|
+
* Written against the raw IndexedDB API rather than a wrapper so this package
|
|
30
|
+
* keeps its single dependency. It is thirty lines and it never changes.
|
|
31
|
+
*/
|
|
32
|
+
export declare function idbBytesStore(name?: string, store?: string): BytesStore;
|
|
33
|
+
//# sourceMappingURL=browser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAKH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAkCvD,mFAAmF;AACnF,wBAAgB,iBAAiB,IAAI,gBAAgB,EAAE,CAMtD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,IAAI,SAAa,EAAE,KAAK,SAAa,GAAG,UAAU,CA0C/E"}
|
package/dist/browser.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The browser profile.
|
|
3
|
+
*
|
|
4
|
+
* A page cannot listen and cannot open a TCP socket. It reaches the mesh by
|
|
5
|
+
* reserving on a relay and being dialled back through the circuit, so its
|
|
6
|
+
* transports are the three that make that work:
|
|
7
|
+
*
|
|
8
|
+
* webSockets — to reach the relay itself, which does listen.
|
|
9
|
+
* circuitRelayTransport — to be dialled through that reservation.
|
|
10
|
+
* webRTC — so that once two members have exchanged an offer
|
|
11
|
+
* over the circuit, the data goes directly and the
|
|
12
|
+
* hub stays a signalling channel.
|
|
13
|
+
*
|
|
14
|
+
* `tcp()` is absent because it cannot exist here, which is the whole reason
|
|
15
|
+
* this file is separate from `./node`.
|
|
16
|
+
*/
|
|
17
|
+
import { circuitRelayTransport } from "@libp2p/circuit-relay-v2";
|
|
18
|
+
import { webRTC } from "@libp2p/webrtc";
|
|
19
|
+
import { webSockets } from "@libp2p/websockets";
|
|
20
|
+
/** Transports for a page. Fresh instances per call — libp2p's are not reusable. */
|
|
21
|
+
export function browserTransports() {
|
|
22
|
+
return [
|
|
23
|
+
webSockets(),
|
|
24
|
+
circuitRelayTransport(),
|
|
25
|
+
webRTC(),
|
|
26
|
+
];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* A `BytesStore` in IndexedDB — where a page keeps its identity.
|
|
30
|
+
*
|
|
31
|
+
* NOT `localStorage`, and the difference matters: a key is bytes, and
|
|
32
|
+
* `localStorage` stores strings, so every read and write would go through a
|
|
33
|
+
* base64 round trip that can silently truncate. IndexedDB stores a
|
|
34
|
+
* `Uint8Array` as itself.
|
|
35
|
+
*
|
|
36
|
+
* Written against the raw IndexedDB API rather than a wrapper so this package
|
|
37
|
+
* keeps its single dependency. It is thirty lines and it never changes.
|
|
38
|
+
*/
|
|
39
|
+
export function idbBytesStore(name = "httpeers", store = "identity") {
|
|
40
|
+
const open = () => new Promise((resolve, reject) => {
|
|
41
|
+
const request = indexedDB.open(name, 1);
|
|
42
|
+
request.onupgradeneeded = () => {
|
|
43
|
+
if (!request.result.objectStoreNames.contains(store)) {
|
|
44
|
+
request.result.createObjectStore(store);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
request.onsuccess = () => resolve(request.result);
|
|
48
|
+
request.onerror = () => reject(request.error);
|
|
49
|
+
});
|
|
50
|
+
const run = async (mode, act) => {
|
|
51
|
+
const db = await open();
|
|
52
|
+
try {
|
|
53
|
+
return await new Promise((resolve, reject) => {
|
|
54
|
+
const request = act(db.transaction(store, mode).objectStore(store));
|
|
55
|
+
request.onsuccess = () => resolve(request.result);
|
|
56
|
+
request.onerror = () => reject(request.error);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
db.close();
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
return {
|
|
64
|
+
async get(key) {
|
|
65
|
+
const value = await run("readonly", (s) => s.get(key));
|
|
66
|
+
if (value === undefined)
|
|
67
|
+
return undefined;
|
|
68
|
+
return value instanceof Uint8Array ? value : new Uint8Array(value);
|
|
69
|
+
},
|
|
70
|
+
async set(key, value) {
|
|
71
|
+
await run("readwrite", (s) => s.put(value, key));
|
|
72
|
+
},
|
|
73
|
+
async delete(key) {
|
|
74
|
+
await run("readwrite", (s) => s.delete(key));
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
package/dist/duplex.d.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 09 — the second altitude: duplex streams over the same mesh.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS. `Handler = (Request) => Promise<Response>` cannot express a
|
|
5
|
+
* WebSocket, an A2UI message stream, or anything else where both ends keep
|
|
6
|
+
* talking. It never could: `fetch` is not the foundation of this stack but a
|
|
7
|
+
* NARROWING of it. `@statewalker/webrun-http-streams` builds fetch semantics on
|
|
8
|
+
* a `Duplex`, and `httpeers.core`'s transport performs that narrowing at two
|
|
9
|
+
* call sites. So adding duplex REMOVES a restriction rather than adding a
|
|
10
|
+
* layer — webrun-wire's ADR-0004 calls `Duplex` "the canonical adapter seam".
|
|
11
|
+
*
|
|
12
|
+
* Duplex = (input: AsyncIterable<Uint8Array>) => AsyncGenerator<Uint8Array>
|
|
13
|
+
*
|
|
14
|
+
* THREE THINGS THIS FILE IS CAREFUL ABOUT:
|
|
15
|
+
*
|
|
16
|
+
* 1. IDENTITY IS AN ARGUMENT, NOT A LOOKUP. The fetch path carries the
|
|
17
|
+
* proven peer in a `WeakMap<Request, …>` written inside the per-stream
|
|
18
|
+
* closure. A duplex has no `Request` to key on, so `DuplexContext.peerId`
|
|
19
|
+
* is passed to the handler. `serveConnections` gives us the only claim
|
|
20
|
+
* that cannot be forged by the payload: `ConnectionContext.remotePeer`.
|
|
21
|
+
* 2. A SEPARATE PROTOCOL ID. `/httpeers-duplex/1.0.0` lives beside
|
|
22
|
+
* `/httpeers/1.0.0`, so the fetch path is untouched and cannot regress.
|
|
23
|
+
* 3. THE OPEN FRAME IS THE EXISTING ENVELOPE. `encodeMessage`/`decodeMessage`
|
|
24
|
+
* from webrun-http-streams — JSON line, then body bytes. Nothing new on
|
|
25
|
+
* the wire.
|
|
26
|
+
*/
|
|
27
|
+
import type { Libp2p } from "@libp2p/interface";
|
|
28
|
+
import type { Duplex } from "@statewalker/webrun-streams";
|
|
29
|
+
export declare const DUPLEX_PROTOCOL = "/httpeers-duplex/1.0.0";
|
|
30
|
+
/** What a duplex handler is told. The peer is an argument because there is no Request to key a map on. */
|
|
31
|
+
export interface DuplexContext {
|
|
32
|
+
/** Proven by the Noise handshake. Never read from the payload. */
|
|
33
|
+
readonly peerId: string;
|
|
34
|
+
readonly path: string;
|
|
35
|
+
/** Whatever the caller put in the open frame. A CLAIM, never proof — see `peerId`. */
|
|
36
|
+
readonly token?: string;
|
|
37
|
+
}
|
|
38
|
+
export type DuplexHandler = (input: AsyncIterable<Uint8Array>, ctx: DuplexContext) => AsyncIterable<Uint8Array>;
|
|
39
|
+
export interface DuplexMounts {
|
|
40
|
+
/** Longest-prefix on segment boundaries, like the fetch router. */
|
|
41
|
+
provide(prefix: string, handler: DuplexHandler): () => void;
|
|
42
|
+
match(path: string): {
|
|
43
|
+
handler: DuplexHandler;
|
|
44
|
+
prefix: string;
|
|
45
|
+
} | null;
|
|
46
|
+
list(): string[];
|
|
47
|
+
}
|
|
48
|
+
export declare function createDuplexMounts(): DuplexMounts;
|
|
49
|
+
/** Thrown into the caller's stream when no mount claims the path. */
|
|
50
|
+
export declare const NO_MOUNT = "httpeers-duplex: no mount for ";
|
|
51
|
+
export interface ServeDuplexInit {
|
|
52
|
+
node: Libp2p;
|
|
53
|
+
mounts: DuplexMounts;
|
|
54
|
+
protocol?: string;
|
|
55
|
+
/** Left unset, libp2p's default (32) applies — the ceiling on concurrent inbound streams. */
|
|
56
|
+
maxInboundStreams?: number;
|
|
57
|
+
/** A relayed circuit is a limited connection; libp2p refuses this protocol on one unless told otherwise. */
|
|
58
|
+
runOnLimitedConnection?: boolean;
|
|
59
|
+
}
|
|
60
|
+
/** Register the duplex protocol. Returns the teardown `serveConnections` gives us. */
|
|
61
|
+
export declare function serveDuplex(init: ServeDuplexInit): Promise<() => Promise<void>>;
|
|
62
|
+
export interface OpenDuplexInit {
|
|
63
|
+
node: Libp2p;
|
|
64
|
+
peerId: string;
|
|
65
|
+
path: string;
|
|
66
|
+
token?: string;
|
|
67
|
+
protocol?: string;
|
|
68
|
+
runOnLimitedConnection?: boolean;
|
|
69
|
+
}
|
|
70
|
+
export interface PeerDuplex {
|
|
71
|
+
readonly peerId: string;
|
|
72
|
+
readonly path: string;
|
|
73
|
+
/** Feed it outbound bytes, iterate the inbound ones. */
|
|
74
|
+
readonly call: Duplex;
|
|
75
|
+
close(): Promise<void>;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Open a duplex stream to `(peerId, path)`.
|
|
79
|
+
*
|
|
80
|
+
* ADDRESSED BY A PAIR, NEVER BY A URL. A URL is the fetch layer's addressing
|
|
81
|
+
* scheme and a duplex stream has no request-line; reusing `peer://…` here
|
|
82
|
+
* would also re-open the URL-derived-peer hole the ghost's pin exists to
|
|
83
|
+
* close (`new URL("peer://a@b/x").host === "b"`, measured in rung 06).
|
|
84
|
+
*/
|
|
85
|
+
export declare function openDuplex(init: OpenDuplexInit): Promise<PeerDuplex>;
|
|
86
|
+
//# sourceMappingURL=duplex.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"duplex.d.ts","sourceRoot":"","sources":["../src/duplex.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAGhD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,6BAA6B,CAAC;AAO1D,eAAO,MAAM,eAAe,2BAA2B,CAAC;AAExD,0GAA0G;AAC1G,MAAM,WAAW,aAAa;IAC5B,kEAAkE;IAClE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,sFAAsF;IACtF,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,MAAM,aAAa,GAAG,CAC1B,KAAK,EAAE,aAAa,CAAC,UAAU,CAAC,EAChC,GAAG,EAAE,aAAa,KACf,aAAa,CAAC,UAAU,CAAC,CAAC;AAE/B,MAAM,WAAW,YAAY;IAC3B,mEAAmE;IACnE,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG,MAAM,IAAI,CAAC;IAC5D,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG;QAAE,OAAO,EAAE,aAAa,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACvE,IAAI,IAAI,MAAM,EAAE,CAAC;CAClB;AAED,wBAAgB,kBAAkB,IAAI,YAAY,CAuBjD;AAED,qEAAqE;AACrE,eAAO,MAAM,QAAQ,mCAAmC,CAAC;AAEzD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,YAAY,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,6FAA6F;IAC7F,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,4GAA4G;IAC5G,sBAAsB,CAAC,EAAE,OAAO,CAAC;CAClC;AAED,sFAAsF;AACtF,wBAAsB,WAAW,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,CA0BrF;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sBAAsB,CAAC,EAAE,OAAO,CAAC;CAClC;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,wDAAwD;IACxD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED;;;;;;;GAOG;AACH,wBAAsB,UAAU,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,UAAU,CAAC,CAmB1E"}
|
package/dist/duplex.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 09 — the second altitude: duplex streams over the same mesh.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS. `Handler = (Request) => Promise<Response>` cannot express a
|
|
5
|
+
* WebSocket, an A2UI message stream, or anything else where both ends keep
|
|
6
|
+
* talking. It never could: `fetch` is not the foundation of this stack but a
|
|
7
|
+
* NARROWING of it. `@statewalker/webrun-http-streams` builds fetch semantics on
|
|
8
|
+
* a `Duplex`, and `httpeers.core`'s transport performs that narrowing at two
|
|
9
|
+
* call sites. So adding duplex REMOVES a restriction rather than adding a
|
|
10
|
+
* layer — webrun-wire's ADR-0004 calls `Duplex` "the canonical adapter seam".
|
|
11
|
+
*
|
|
12
|
+
* Duplex = (input: AsyncIterable<Uint8Array>) => AsyncGenerator<Uint8Array>
|
|
13
|
+
*
|
|
14
|
+
* THREE THINGS THIS FILE IS CAREFUL ABOUT:
|
|
15
|
+
*
|
|
16
|
+
* 1. IDENTITY IS AN ARGUMENT, NOT A LOOKUP. The fetch path carries the
|
|
17
|
+
* proven peer in a `WeakMap<Request, …>` written inside the per-stream
|
|
18
|
+
* closure. A duplex has no `Request` to key on, so `DuplexContext.peerId`
|
|
19
|
+
* is passed to the handler. `serveConnections` gives us the only claim
|
|
20
|
+
* that cannot be forged by the payload: `ConnectionContext.remotePeer`.
|
|
21
|
+
* 2. A SEPARATE PROTOCOL ID. `/httpeers-duplex/1.0.0` lives beside
|
|
22
|
+
* `/httpeers/1.0.0`, so the fetch path is untouched and cannot regress.
|
|
23
|
+
* 3. THE OPEN FRAME IS THE EXISTING ENVELOPE. `encodeMessage`/`decodeMessage`
|
|
24
|
+
* from webrun-http-streams — JSON line, then body bytes. Nothing new on
|
|
25
|
+
* the wire.
|
|
26
|
+
*/
|
|
27
|
+
import { peerIdFromString } from "@libp2p/peer-id";
|
|
28
|
+
import { decodeMessage, encodeMessage } from "@statewalker/webrun-http-streams";
|
|
29
|
+
import { connect, serveConnections, } from "@statewalker/webrun-streams-libp2p";
|
|
30
|
+
export const DUPLEX_PROTOCOL = "/httpeers-duplex/1.0.0";
|
|
31
|
+
export function createDuplexMounts() {
|
|
32
|
+
const table = [];
|
|
33
|
+
const norm = (p) => (p.endsWith("/") && p !== "/" ? p.slice(0, -1) : p);
|
|
34
|
+
return {
|
|
35
|
+
provide(prefix, handler) {
|
|
36
|
+
const entry = { prefix: norm(prefix), handler };
|
|
37
|
+
table.push(entry);
|
|
38
|
+
table.sort((a, b) => b.prefix.length - a.prefix.length);
|
|
39
|
+
return () => {
|
|
40
|
+
const at = table.indexOf(entry);
|
|
41
|
+
if (at >= 0)
|
|
42
|
+
table.splice(at, 1);
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
match(path) {
|
|
46
|
+
const target = norm(path);
|
|
47
|
+
const hit = table.find((m) => m.prefix === "/" || target === m.prefix || target.startsWith(`${m.prefix}/`));
|
|
48
|
+
return hit == null ? null : { handler: hit.handler, prefix: hit.prefix };
|
|
49
|
+
},
|
|
50
|
+
list: () => table.map((m) => m.prefix),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/** Thrown into the caller's stream when no mount claims the path. */
|
|
54
|
+
export const NO_MOUNT = "httpeers-duplex: no mount for ";
|
|
55
|
+
/** Register the duplex protocol. Returns the teardown `serveConnections` gives us. */
|
|
56
|
+
export async function serveDuplex(init) {
|
|
57
|
+
return await serveConnections({
|
|
58
|
+
node: init.node,
|
|
59
|
+
protocol: init.protocol ?? DUPLEX_PROTOCOL,
|
|
60
|
+
...(init.maxInboundStreams == null ? {} : { maxInboundStreams: init.maxInboundStreams }),
|
|
61
|
+
...(init.runOnLimitedConnection == null
|
|
62
|
+
? {}
|
|
63
|
+
: { runOnLimitedConnection: init.runOnLimitedConnection }),
|
|
64
|
+
},
|
|
65
|
+
// Called ONCE PER INBOUND STREAM, which is what keeps the proven peer in
|
|
66
|
+
// the closure and out of the payload.
|
|
67
|
+
(context) => {
|
|
68
|
+
const peerId = context.remotePeer.toString();
|
|
69
|
+
return async function* (input) {
|
|
70
|
+
const { envelope, body } = await decodeMessage(input);
|
|
71
|
+
const found = init.mounts.match(envelope.path);
|
|
72
|
+
if (found == null)
|
|
73
|
+
throw new Error(`${NO_MOUNT}${envelope.path}`);
|
|
74
|
+
yield* found.handler(body, {
|
|
75
|
+
peerId,
|
|
76
|
+
path: envelope.path,
|
|
77
|
+
...(envelope.token == null ? {} : { token: envelope.token }),
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Open a duplex stream to `(peerId, path)`.
|
|
84
|
+
*
|
|
85
|
+
* ADDRESSED BY A PAIR, NEVER BY A URL. A URL is the fetch layer's addressing
|
|
86
|
+
* scheme and a duplex stream has no request-line; reusing `peer://…` here
|
|
87
|
+
* would also re-open the URL-derived-peer hole the ghost's pin exists to
|
|
88
|
+
* close (`new URL("peer://a@b/x").host === "b"`, measured in rung 06).
|
|
89
|
+
*/
|
|
90
|
+
export async function openDuplex(init) {
|
|
91
|
+
const conn = await connect({
|
|
92
|
+
node: init.node,
|
|
93
|
+
peer: peerIdFromString(init.peerId),
|
|
94
|
+
protocol: init.protocol ?? DUPLEX_PROTOCOL,
|
|
95
|
+
...(init.runOnLimitedConnection == null
|
|
96
|
+
? {}
|
|
97
|
+
: { runOnLimitedConnection: init.runOnLimitedConnection }),
|
|
98
|
+
});
|
|
99
|
+
const call = (input) => conn.call(encodeMessage({ path: init.path, ...(init.token == null ? {} : { token: init.token }) }, input));
|
|
100
|
+
return { peerId: init.peerId, path: init.path, call, close: conn.close };
|
|
101
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asking the RELAY whether this node is still reserved -- and renewing the
|
|
3
|
+
* reservation in the same breath.
|
|
4
|
+
*
|
|
5
|
+
* THE QUESTION THIS MODULE EXISTS TO ANSWER. `node.getMultiaddrs()` reports
|
|
6
|
+
* what the node's own transport reservation store believes, and in the
|
|
7
|
+
* 2026-09-19 incident that belief was wrong for hours: the relay held no
|
|
8
|
+
* reservation, the websocket was still up, every member got
|
|
9
|
+
* `NO_RESERVATION`, and the hub saw nothing to repair. The only authority on
|
|
10
|
+
* "is there a reservation" is the relay, so this asks it, over the wire.
|
|
11
|
+
*
|
|
12
|
+
* WHY NOT LIBP2P'S OWN CALL. `@libp2p/circuit-relay-v2`'s transport keeps a
|
|
13
|
+
* `ReservationStore` whose `addRelay(peer, type)` is the renewal entry point,
|
|
14
|
+
* and it is unusable as a probe for three separate reasons:
|
|
15
|
+
*
|
|
16
|
+
* 1. It SHORT-CIRCUITS. If it still holds an entry, the connection is open
|
|
17
|
+
* and the recorded expiry is more than ten minutes away, it returns the
|
|
18
|
+
* cached entry without sending a byte -- so a proactive call answers from
|
|
19
|
+
* the very belief we are trying to check.
|
|
20
|
+
* 2. It is NOT REACHABLE. The store is a private component of the transport;
|
|
21
|
+
* nothing on `Libp2p` exposes it.
|
|
22
|
+
* 3. Its failure paths are SURPRISING. A dial or protocol failure puts the
|
|
23
|
+
* relay in a "previously invalid" cuckoo filter (`relayFilter`) so later
|
|
24
|
+
* attempts are refused locally, and a discovered reservation can be
|
|
25
|
+
* refused with `HadEnoughRelaysError` when no pending slot is free.
|
|
26
|
+
*
|
|
27
|
+
* So we speak HOP ourselves. A `RESERVE` request on the relay's own protocol
|
|
28
|
+
* is two bytes, and the answer is the relay's, not ours.
|
|
29
|
+
*
|
|
30
|
+
* IT REUSES THE SLOT, IT DOES NOT LEAK A SECOND ONE. The relay's reservation
|
|
31
|
+
* store is keyed by peer id: `reserve()` finds the existing entry, resets its
|
|
32
|
+
* expiry signal in place and returns OK (see `server/reservation-store.js` in
|
|
33
|
+
* the installed 4.2.11). A peer cannot hold two reservations on one relay, so
|
|
34
|
+
* "renew" and "re-reserve" are the same request -- which is also why this one
|
|
35
|
+
* call both DETECTS a loss and REPAIRS it.
|
|
36
|
+
*
|
|
37
|
+
* IT DOES NOT DISTURB LIVE CIRCUITS. It opens one short-lived stream on a
|
|
38
|
+
* connection that already exists; already-established circuits are separate
|
|
39
|
+
* connections and are untouched, and the relay's refresh path does not close
|
|
40
|
+
* anything.
|
|
41
|
+
*
|
|
42
|
+
* THE CODEC IS HAND-WRITTEN, four fields wide, because it has to be:
|
|
43
|
+
* `@libp2p/circuit-relay-v2` publishes only `.` in its `exports` map, so its
|
|
44
|
+
* generated `pb/index.js` cannot be imported, and vendoring protons to
|
|
45
|
+
* regenerate a message we send ONE variant of would be more code than this.
|
|
46
|
+
* The wire format is pinned by `tests/hop-reserve.test.ts` against bytes taken
|
|
47
|
+
* from that generated encoder, and end to end by `tests/reservation-renewal.test.ts`
|
|
48
|
+
* against a real relay -- which is the check that actually matters, since a
|
|
49
|
+
* codec that agrees only with its own tests proves nothing.
|
|
50
|
+
*/
|
|
51
|
+
import type { Libp2p } from "@libp2p/interface";
|
|
52
|
+
/** circuit-relay v2 `Status`, by number. The relay answers one of these. */
|
|
53
|
+
export declare const HOP_STATUS_NAMES: Readonly<Record<number, string>>;
|
|
54
|
+
/** `Status.OK`. */
|
|
55
|
+
export declare const HOP_STATUS_OK = 100;
|
|
56
|
+
/** How long one RESERVE round trip may take, matching libp2p's own `DEFAULT_RESERVATION_COMPLETION_TIMEOUT`. */
|
|
57
|
+
export declare const HOP_RESERVE_TIMEOUT_MS = 5000;
|
|
58
|
+
/** What the relay granted. `expiresAt` is `null` when the relay sent no reservation body. */
|
|
59
|
+
export interface RelayReservationGrant {
|
|
60
|
+
/** ms since the epoch, from the relay's own `Reservation.expire` (Unix seconds). */
|
|
61
|
+
expiresAt: number | null;
|
|
62
|
+
/** `expiresAt` minus now, at the moment the answer arrived. `null` when unknown. */
|
|
63
|
+
ttlMs: number | null;
|
|
64
|
+
}
|
|
65
|
+
/** The relay answered, and the answer was not OK. `status` is its own `Status` number. */
|
|
66
|
+
export declare class RelayReservationRefusedError extends Error {
|
|
67
|
+
readonly relayPeerId: string;
|
|
68
|
+
readonly status: number;
|
|
69
|
+
constructor(relayPeerId: string, status: number);
|
|
70
|
+
}
|
|
71
|
+
/** There is no connection this node could ask the relay over. Not a refusal: nothing was asked. */
|
|
72
|
+
export declare class RelayNotConnectedError extends Error {
|
|
73
|
+
readonly relayPeerId: string;
|
|
74
|
+
constructor(relayPeerId: string);
|
|
75
|
+
}
|
|
76
|
+
/** The subset of `HopMessage` this module reads. Everything else is skipped. */
|
|
77
|
+
export interface HopAnswer {
|
|
78
|
+
/** `HopMessage.Type`: 0 RESERVE, 1 CONNECT, 2 STATUS. */
|
|
79
|
+
type?: number;
|
|
80
|
+
status?: number;
|
|
81
|
+
/** `Reservation.expire`, Unix SECONDS as the relay sent it. */
|
|
82
|
+
expire?: number;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Read the fields of a `HopMessage` this module needs, skipping the rest.
|
|
86
|
+
*
|
|
87
|
+
* Deliberately lenient about everything it does not use -- an unknown field,
|
|
88
|
+
* a voucher, a limit -- because a decoder that throws on a field a future
|
|
89
|
+
* relay adds would turn a healthy relay into a lost reservation.
|
|
90
|
+
*/
|
|
91
|
+
export declare function decodeHopAnswer(data: Uint8Array | {
|
|
92
|
+
subarray(): Uint8Array;
|
|
93
|
+
}): HopAnswer;
|
|
94
|
+
/**
|
|
95
|
+
* Request (or renew) this node's reservation on `relayPeerId`, and return what
|
|
96
|
+
* the relay granted. Throws `RelayReservationRefusedError` on any non-OK
|
|
97
|
+
* status, `RelayNotConnectedError` when there is nothing to ask over, and
|
|
98
|
+
* whatever the stream threw otherwise. Every one of those means "not reserved
|
|
99
|
+
* as far as the relay is concerned", which is the only judgement the caller
|
|
100
|
+
* needs.
|
|
101
|
+
*/
|
|
102
|
+
export declare function requestRelayReservation(node: Libp2p, relayPeerId: string, options?: {
|
|
103
|
+
signal?: AbortSignal;
|
|
104
|
+
}): Promise<RelayReservationGrant>;
|
|
105
|
+
//# sourceMappingURL=hop-reserve.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hop-reserve.d.ts","sourceRoot":"","sources":["../src/hop-reserve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AAGH,OAAO,KAAK,EAAc,MAAM,EAAU,MAAM,mBAAmB,CAAC;AAIpE,4EAA4E;AAC5E,eAAO,MAAM,gBAAgB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAU7D,CAAC;AAEF,mBAAmB;AACnB,eAAO,MAAM,aAAa,MAAM,CAAC;AAYjC,gHAAgH;AAChH,eAAO,MAAM,sBAAsB,OAAQ,CAAC;AAE5C,6FAA6F;AAC7F,MAAM,WAAW,qBAAqB;IACpC,oFAAoF;IACpF,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,oFAAoF;IACpF,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,0FAA0F;AAC1F,qBAAa,4BAA6B,SAAQ,KAAK;IAEnD,QAAQ,CAAC,WAAW,EAAE,MAAM;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM;IAFzB,YACW,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,EAOxB;CACF;AAED,mGAAmG;AACnG,qBAAa,sBAAuB,SAAQ,KAAK;IACnC,QAAQ,CAAC,WAAW,EAAE,MAAM;IAAxC,YAAqB,WAAW,EAAE,MAAM,EAKvC;CACF;AAED,gFAAgF;AAChF,MAAM,WAAW,SAAS;IACxB,yDAAyD;IACzD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,+DAA+D;IAC/D,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,UAAU,GAAG;IAAE,QAAQ,IAAI,UAAU,CAAA;CAAE,GAAG,SAAS,CAoBxF;AAED;;;;;;;GAOG;AACH,wBAAsB,uBAAuB,CAC3C,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,MAAM,EACnB,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,WAAW,CAAA;CAAO,GACrC,OAAO,CAAC,qBAAqB,CAAC,CA4BhC"}
|