@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/duplex.ts ADDED
@@ -0,0 +1,172 @@
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
+
28
+ import type { Libp2p } from "@libp2p/interface";
29
+ import { peerIdFromString } from "@libp2p/peer-id";
30
+ import { decodeMessage, encodeMessage } from "@statewalker/webrun-http-streams";
31
+ import type { Duplex } from "@statewalker/webrun-streams";
32
+ import {
33
+ type ConnectionContext,
34
+ connect,
35
+ serveConnections,
36
+ } from "@statewalker/webrun-streams-libp2p";
37
+
38
+ export const DUPLEX_PROTOCOL = "/httpeers-duplex/1.0.0";
39
+
40
+ /** What a duplex handler is told. The peer is an argument because there is no Request to key a map on. */
41
+ export interface DuplexContext {
42
+ /** Proven by the Noise handshake. Never read from the payload. */
43
+ readonly peerId: string;
44
+ readonly path: string;
45
+ /** Whatever the caller put in the open frame. A CLAIM, never proof — see `peerId`. */
46
+ readonly token?: string;
47
+ }
48
+
49
+ export type DuplexHandler = (
50
+ input: AsyncIterable<Uint8Array>,
51
+ ctx: DuplexContext,
52
+ ) => AsyncIterable<Uint8Array>;
53
+
54
+ export interface DuplexMounts {
55
+ /** Longest-prefix on segment boundaries, like the fetch router. */
56
+ provide(prefix: string, handler: DuplexHandler): () => void;
57
+ match(path: string): { handler: DuplexHandler; prefix: string } | null;
58
+ list(): string[];
59
+ }
60
+
61
+ export function createDuplexMounts(): DuplexMounts {
62
+ const table: { prefix: string; handler: DuplexHandler }[] = [];
63
+ const norm = (p: string): string => (p.endsWith("/") && p !== "/" ? p.slice(0, -1) : p);
64
+
65
+ return {
66
+ provide(prefix, handler) {
67
+ const entry = { prefix: norm(prefix), handler };
68
+ table.push(entry);
69
+ table.sort((a, b) => b.prefix.length - a.prefix.length);
70
+ return () => {
71
+ const at = table.indexOf(entry);
72
+ if (at >= 0) table.splice(at, 1);
73
+ };
74
+ },
75
+ match(path) {
76
+ const target = norm(path);
77
+ const hit = table.find(
78
+ (m) => m.prefix === "/" || target === m.prefix || target.startsWith(`${m.prefix}/`),
79
+ );
80
+ return hit == null ? null : { handler: hit.handler, prefix: hit.prefix };
81
+ },
82
+ list: () => table.map((m) => m.prefix),
83
+ };
84
+ }
85
+
86
+ /** Thrown into the caller's stream when no mount claims the path. */
87
+ export const NO_MOUNT = "httpeers-duplex: no mount for ";
88
+
89
+ export interface ServeDuplexInit {
90
+ node: Libp2p;
91
+ mounts: DuplexMounts;
92
+ protocol?: string;
93
+ /** Left unset, libp2p's default (32) applies — the ceiling on concurrent inbound streams. */
94
+ maxInboundStreams?: number;
95
+ /** A relayed circuit is a limited connection; libp2p refuses this protocol on one unless told otherwise. */
96
+ runOnLimitedConnection?: boolean;
97
+ }
98
+
99
+ /** Register the duplex protocol. Returns the teardown `serveConnections` gives us. */
100
+ export async function serveDuplex(init: ServeDuplexInit): Promise<() => Promise<void>> {
101
+ return await serveConnections(
102
+ {
103
+ node: init.node,
104
+ protocol: init.protocol ?? DUPLEX_PROTOCOL,
105
+ ...(init.maxInboundStreams == null ? {} : { maxInboundStreams: init.maxInboundStreams }),
106
+ ...(init.runOnLimitedConnection == null
107
+ ? {}
108
+ : { runOnLimitedConnection: init.runOnLimitedConnection }),
109
+ },
110
+ // Called ONCE PER INBOUND STREAM, which is what keeps the proven peer in
111
+ // the closure and out of the payload.
112
+ (context: ConnectionContext): Duplex => {
113
+ const peerId = context.remotePeer.toString();
114
+ return async function* (input): AsyncGenerator<Uint8Array> {
115
+ const { envelope, body } = await decodeMessage<{ path: string; token?: string }>(input);
116
+ const found = init.mounts.match(envelope.path);
117
+ if (found == null) throw new Error(`${NO_MOUNT}${envelope.path}`);
118
+ yield* found.handler(body, {
119
+ peerId,
120
+ path: envelope.path,
121
+ ...(envelope.token == null ? {} : { token: envelope.token }),
122
+ });
123
+ };
124
+ },
125
+ );
126
+ }
127
+
128
+ export interface OpenDuplexInit {
129
+ node: Libp2p;
130
+ peerId: string;
131
+ path: string;
132
+ token?: string;
133
+ protocol?: string;
134
+ runOnLimitedConnection?: boolean;
135
+ }
136
+
137
+ export interface PeerDuplex {
138
+ readonly peerId: string;
139
+ readonly path: string;
140
+ /** Feed it outbound bytes, iterate the inbound ones. */
141
+ readonly call: Duplex;
142
+ close(): Promise<void>;
143
+ }
144
+
145
+ /**
146
+ * Open a duplex stream to `(peerId, path)`.
147
+ *
148
+ * ADDRESSED BY A PAIR, NEVER BY A URL. A URL is the fetch layer's addressing
149
+ * scheme and a duplex stream has no request-line; reusing `peer://…` here
150
+ * would also re-open the URL-derived-peer hole the ghost's pin exists to
151
+ * close (`new URL("peer://a@b/x").host === "b"`, measured in rung 06).
152
+ */
153
+ export async function openDuplex(init: OpenDuplexInit): Promise<PeerDuplex> {
154
+ const conn = await connect({
155
+ node: init.node,
156
+ peer: peerIdFromString(init.peerId),
157
+ protocol: init.protocol ?? DUPLEX_PROTOCOL,
158
+ ...(init.runOnLimitedConnection == null
159
+ ? {}
160
+ : { runOnLimitedConnection: init.runOnLimitedConnection }),
161
+ });
162
+
163
+ const call: Duplex = (input) =>
164
+ conn.call(
165
+ encodeMessage(
166
+ { path: init.path, ...(init.token == null ? {} : { token: init.token }) },
167
+ input,
168
+ ),
169
+ );
170
+
171
+ return { peerId: init.peerId, path: init.path, call, close: conn.close };
172
+ }
@@ -0,0 +1,295 @@
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
+
52
+ import { RELAY_V2_HOP_CODEC } from "@libp2p/circuit-relay-v2";
53
+ import type { Connection, Libp2p, Stream } from "@libp2p/interface";
54
+ import { peerIdFromString } from "@libp2p/peer-id";
55
+ import { pbStream } from "@libp2p/utils";
56
+
57
+ /** circuit-relay v2 `Status`, by number. The relay answers one of these. */
58
+ export const HOP_STATUS_NAMES: Readonly<Record<number, string>> = {
59
+ 0: "UNUSED",
60
+ 100: "OK",
61
+ 200: "RESERVATION_REFUSED",
62
+ 201: "RESOURCE_LIMIT_EXCEEDED",
63
+ 202: "PERMISSION_DENIED",
64
+ 203: "CONNECTION_FAILED",
65
+ 204: "NO_RESERVATION",
66
+ 400: "MALFORMED_MESSAGE",
67
+ 401: "UNEXPECTED_MESSAGE",
68
+ };
69
+
70
+ /** `Status.OK`. */
71
+ export const HOP_STATUS_OK = 100;
72
+
73
+ /**
74
+ * `HopMessage{ type: RESERVE }` on the wire: field 1, varint, value 0.
75
+ *
76
+ * protobuf3 would normally omit a zero-valued field; protons generates
77
+ * `if (obj.type != null) { w.uint32(8); ... }` for this `optional` field, so
78
+ * the tag IS written and a relay that requires it (go-libp2p does) sees it.
79
+ * Constant, because every request this module sends is this one.
80
+ */
81
+ const RESERVE_REQUEST = Uint8Array.from([0x08, 0x00]);
82
+
83
+ /** How long one RESERVE round trip may take, matching libp2p's own `DEFAULT_RESERVATION_COMPLETION_TIMEOUT`. */
84
+ export const HOP_RESERVE_TIMEOUT_MS = 5_000;
85
+
86
+ /** What the relay granted. `expiresAt` is `null` when the relay sent no reservation body. */
87
+ export interface RelayReservationGrant {
88
+ /** ms since the epoch, from the relay's own `Reservation.expire` (Unix seconds). */
89
+ expiresAt: number | null;
90
+ /** `expiresAt` minus now, at the moment the answer arrived. `null` when unknown. */
91
+ ttlMs: number | null;
92
+ }
93
+
94
+ /** The relay answered, and the answer was not OK. `status` is its own `Status` number. */
95
+ export class RelayReservationRefusedError extends Error {
96
+ constructor(
97
+ readonly relayPeerId: string,
98
+ readonly status: number,
99
+ ) {
100
+ super(
101
+ `hop: the relay (${relayPeerId}) refused a reservation with ` +
102
+ `${HOP_STATUS_NAMES[status] ?? `status ${status}`}`,
103
+ );
104
+ this.name = "RelayReservationRefusedError";
105
+ }
106
+ }
107
+
108
+ /** There is no connection this node could ask the relay over. Not a refusal: nothing was asked. */
109
+ export class RelayNotConnectedError extends Error {
110
+ constructor(readonly relayPeerId: string) {
111
+ super(
112
+ `hop: no open, unlimited connection to the relay (${relayPeerId}) to ask for a reservation over`,
113
+ );
114
+ this.name = "RelayNotConnectedError";
115
+ }
116
+ }
117
+
118
+ /** The subset of `HopMessage` this module reads. Everything else is skipped. */
119
+ export interface HopAnswer {
120
+ /** `HopMessage.Type`: 0 RESERVE, 1 CONNECT, 2 STATUS. */
121
+ type?: number;
122
+ status?: number;
123
+ /** `Reservation.expire`, Unix SECONDS as the relay sent it. */
124
+ expire?: number;
125
+ }
126
+
127
+ /**
128
+ * Read the fields of a `HopMessage` this module needs, skipping the rest.
129
+ *
130
+ * Deliberately lenient about everything it does not use -- an unknown field,
131
+ * a voucher, a limit -- because a decoder that throws on a field a future
132
+ * relay adds would turn a healthy relay into a lost reservation.
133
+ */
134
+ export function decodeHopAnswer(data: Uint8Array | { subarray(): Uint8Array }): HopAnswer {
135
+ const bytes = data instanceof Uint8Array ? data : data.subarray();
136
+ const answer: HopAnswer = {};
137
+ const reader = new Reader(bytes);
138
+ while (reader.hasMore()) {
139
+ const tag = reader.varint();
140
+ const field = tag >>> 3;
141
+ const wire = tag & 7;
142
+ if (field === 1 && wire === 0) answer.type = reader.varint();
143
+ else if (field === 5 && wire === 0) answer.status = reader.varint();
144
+ else if (field === 3 && wire === 2) {
145
+ const reservation = new Reader(reader.bytes());
146
+ while (reservation.hasMore()) {
147
+ const innerTag = reservation.varint();
148
+ if (innerTag >>> 3 === 1 && (innerTag & 7) === 0) answer.expire = reservation.varint();
149
+ else reservation.skip(innerTag & 7);
150
+ }
151
+ } else reader.skip(wire);
152
+ }
153
+ return answer;
154
+ }
155
+
156
+ /**
157
+ * Request (or renew) this node's reservation on `relayPeerId`, and return what
158
+ * the relay granted. Throws `RelayReservationRefusedError` on any non-OK
159
+ * status, `RelayNotConnectedError` when there is nothing to ask over, and
160
+ * whatever the stream threw otherwise. Every one of those means "not reserved
161
+ * as far as the relay is concerned", which is the only judgement the caller
162
+ * needs.
163
+ */
164
+ export async function requestRelayReservation(
165
+ node: Libp2p,
166
+ relayPeerId: string,
167
+ options: { signal?: AbortSignal } = {},
168
+ ): Promise<RelayReservationGrant> {
169
+ const connection = openConnectionTo(node, relayPeerId);
170
+ if (connection == null) throw new RelayNotConnectedError(relayPeerId);
171
+
172
+ const signal = options.signal ?? AbortSignal.timeout(HOP_RESERVE_TIMEOUT_MS);
173
+ const stream = await connection.newStream(RELAY_V2_HOP_CODEC, { signal });
174
+ let answer: HopAnswer;
175
+ try {
176
+ const hop = pbStream(framed(stream)).pb<HopAnswer>({
177
+ encode: () => RESERVE_REQUEST,
178
+ decode: decodeHopAnswer,
179
+ });
180
+ await hop.write({}, { signal });
181
+ answer = await hop.read({ signal });
182
+ } catch (error) {
183
+ stream.abort(error as Error);
184
+ throw error;
185
+ }
186
+ // Closing the write end is enough and is not awaited past its own timeout:
187
+ // the answer is already in hand, and a relay that is slow to acknowledge the
188
+ // close must not turn a granted reservation into a reported loss.
189
+ await stream.close({ signal }).catch(() => stream.abort(new Error("hop: close failed")));
190
+
191
+ if (answer.status !== HOP_STATUS_OK) {
192
+ throw new RelayReservationRefusedError(relayPeerId, answer.status ?? 0);
193
+ }
194
+ const expiresAt = answer.expire != null ? answer.expire * 1_000 : null;
195
+ return { expiresAt, ttlMs: expiresAt != null ? expiresAt - Date.now() : null };
196
+ }
197
+
198
+ /**
199
+ * A ONE-VERSION TYPE GAP, NOT A RUNTIME ONE.
200
+ *
201
+ * This workspace pins `@libp2p/interface` at 3.2.5 everywhere, and
202
+ * `@libp2p/utils` requires `^3.3.0` — whose `MessageStream` grew a
203
+ * `readableEnded` property. `pbStream` therefore refuses a 3.2.5 `Stream` by
204
+ * type while accepting it perfectly at runtime: the property is implemented by
205
+ * `AbstractMessageStream`, which every real libp2p stream extends, and
206
+ * `stream-utils.js` never reads it in the first place (checked in the
207
+ * installed 7.4.1). The narrow cast is the honest statement of that; widening
208
+ * the pin across seven packages to satisfy one call site would be a much
209
+ * larger change for the same behaviour.
210
+ */
211
+ function framed(stream: Stream): Parameters<typeof pbStream>[0] {
212
+ return stream as unknown as Parameters<typeof pbStream>[0];
213
+ }
214
+
215
+ /**
216
+ * A connection to the relay a reservation may be requested over: open, and not
217
+ * itself relayed.
218
+ *
219
+ * NOT RELAYED, because a relay refuses to reserve over a circuit
220
+ * (libp2p calls it `DoubleRelayError` on its own side), and a limited
221
+ * connection would in any case be spent on the attempt. `limits != null` is
222
+ * how libp2p marks one.
223
+ */
224
+ function openConnectionTo(node: Libp2p, relayPeerId: string): Connection | undefined {
225
+ let peerId: ReturnType<typeof peerIdFromString>;
226
+ try {
227
+ peerId = peerIdFromString(relayPeerId);
228
+ } catch {
229
+ return undefined;
230
+ }
231
+ return node
232
+ .getConnections(peerId)
233
+ .find((connection) => connection.status === "open" && connection.limits == null);
234
+ }
235
+
236
+ /** A protobuf reader for the handful of field types `HopMessage` uses. */
237
+ class Reader {
238
+ private pos = 0;
239
+ constructor(private readonly buf: Uint8Array) {}
240
+
241
+ hasMore(): boolean {
242
+ return this.pos < this.buf.length;
243
+ }
244
+
245
+ /**
246
+ * An unsigned varint, as a `number`.
247
+ *
248
+ * Assembled with multiplication rather than `<<`, because `<<` is a 32-bit
249
+ * operation in JavaScript and `Reservation.expire` is a uint64 of Unix
250
+ * seconds -- a shift would wrap it into a negative number and turn a valid
251
+ * expiry into nonsense. Ten bytes is the maximum a uint64 can occupy.
252
+ */
253
+ varint(): number {
254
+ let value = 0;
255
+ let shift = 1;
256
+ for (let i = 0; i < 10; i++) {
257
+ if (!this.hasMore()) throw new Error("hop: truncated varint");
258
+ const byte = this.buf[this.pos++];
259
+ value += (byte & 0x7f) * shift;
260
+ if ((byte & 0x80) === 0) return value;
261
+ shift *= 128;
262
+ }
263
+ throw new Error("hop: varint longer than 10 bytes");
264
+ }
265
+
266
+ /** A length-delimited field's payload. */
267
+ bytes(): Uint8Array {
268
+ const length = this.varint();
269
+ const end = this.pos + length;
270
+ if (end > this.buf.length) throw new Error("hop: truncated length-delimited field");
271
+ const slice = this.buf.subarray(this.pos, end);
272
+ this.pos = end;
273
+ return slice;
274
+ }
275
+
276
+ /** Step over a field of the given wire type. */
277
+ skip(wire: number): void {
278
+ switch (wire) {
279
+ case 0:
280
+ this.varint();
281
+ return;
282
+ case 1:
283
+ this.pos += 8;
284
+ return;
285
+ case 2:
286
+ this.bytes();
287
+ return;
288
+ case 5:
289
+ this.pos += 4;
290
+ return;
291
+ default:
292
+ throw new Error(`hop: unsupported protobuf wire type ${wire}`);
293
+ }
294
+ }
295
+ }