@leofcoin/peernet 1.2.30 → 3.0.0

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 (120) hide show
  1. package/README.md +13 -0
  2. package/docs/automatic-transport.md +37 -0
  3. package/exports/browser/{browser-DvU1xNFS.js → browser-CCfR9KOM.js} +1 -1
  4. package/exports/browser/{client-86Tnax66.js → client-C_pBoi1g.js} +254 -78
  5. package/exports/browser/{identity-CYVPDmkf.js → identity-Cf8KKnho.js} +511 -391
  6. package/exports/browser/identity.js +1 -1
  7. package/exports/browser/{index-BD0Anx7_.js → index-dTQVgkpf.js} +1 -1
  8. package/exports/browser/{messages-D7JxkkDW.js → messages-BGHaihSi.js} +2 -2
  9. package/exports/browser/{peernet-C4rOvyyU.js → peernet-Db43ag2D.js} +68 -45
  10. package/exports/browser/peernet.js +2 -2
  11. package/exports/store.js +0 -3
  12. package/package.json +10 -6
  13. package/.esdoc.json +0 -10
  14. package/.gitattributes +0 -2
  15. package/.github/workflows/release.yml +0 -34
  16. package/.prettierrc +0 -7
  17. package/.travis.yml +0 -27
  18. package/BREAKING_CHANGES.md +0 -35
  19. package/deploy.js +0 -8
  20. package/exports/browser/browser-D-r0O9Qn.js +0 -53
  21. package/exports/browser/browser-_hiyXwPp.js +0 -147
  22. package/exports/browser/client-BFxJts6j.js +0 -2224
  23. package/exports/browser/client-BVhUamQG.js +0 -2200
  24. package/exports/browser/client-BzvWzzLv.js +0 -2224
  25. package/exports/browser/client-C9GBz2Tf.js +0 -2224
  26. package/exports/browser/client-CQzuu6At.js +0 -2224
  27. package/exports/browser/client-CVkFejdx.js +0 -2224
  28. package/exports/browser/client-Cj_UUO8W.js +0 -2224
  29. package/exports/browser/client-CrGaeBjj.js +0 -2224
  30. package/exports/browser/client-DDC8C510.js +0 -2224
  31. package/exports/browser/client-ERocgKET.js +0 -2224
  32. package/exports/browser/client-a8rws3cu.js +0 -2224
  33. package/exports/browser/identity-ChyyZSve.js +0 -8397
  34. package/exports/browser/index-Bxr5Iztg.js +0 -37
  35. package/exports/browser/messages-BFCmg6Vb.js +0 -212
  36. package/exports/browser/messages-BHIC2mQu.js +0 -212
  37. package/exports/browser/messages-CimzJ4wr.js +0 -210
  38. package/exports/browser/messages-Cjoa_LGO.js +0 -210
  39. package/exports/browser/messages-DH0tgNM9.js +0 -207
  40. package/exports/browser/messages-DVHcNvs_.js +0 -212
  41. package/exports/browser/messages-DqL_g2qw.js +0 -210
  42. package/exports/browser/messages-UKnuelZ7.js +0 -207
  43. package/exports/browser/messages-XgZZUMma.js +0 -212
  44. package/exports/browser/messages-kfFyctPL.js +0 -210
  45. package/exports/browser/messages-psROnfRi.js +0 -212
  46. package/exports/browser/peernet-B9VL4Xwe.js +0 -9522
  47. package/exports/browser/peernet-C-HNdrcd.js +0 -9528
  48. package/exports/browser/peernet-Ca1iM2tN.js +0 -9336
  49. package/exports/browser/peernet-CbI0-jFn.js +0 -9651
  50. package/exports/browser/peernet-CtHScDyh.js +0 -9524
  51. package/exports/browser/peernet-DfuxUK3u.js +0 -9525
  52. package/exports/browser/peernet-DiEcNDLD.js +0 -9655
  53. package/exports/browser/peernet-DtuRXOzP.js +0 -9653
  54. package/exports/browser/peernet-r_9IyzTe.js +0 -9336
  55. package/exports/browser/peernet-rcD5XL1y.js +0 -9590
  56. package/exports/browser/peernet-rw8HJGOz.js +0 -9597
  57. package/exports/messages-CHKVVTVE.js +0 -204
  58. package/exports/messages-DaUtFfBh.js +0 -207
  59. package/index.html +0 -19
  60. package/rollup.config.js +0 -143
  61. package/src/dht/dht.ts +0 -142
  62. package/src/discovery/peer-discovery.js +0 -75
  63. package/src/errors/errors.js +0 -12
  64. package/src/file-transfer.ts +0 -194
  65. package/src/handlers/data.js +0 -15
  66. package/src/handlers/message.js +0 -34
  67. package/src/identity.ts +0 -172
  68. package/src/messages/chat.ts +0 -13
  69. package/src/messages/data-response.ts +0 -13
  70. package/src/messages/data.ts +0 -17
  71. package/src/messages/dht-response.ts +0 -13
  72. package/src/messages/dht.ts +0 -21
  73. package/src/messages/file-link.ts +0 -17
  74. package/src/messages/file.ts +0 -17
  75. package/src/messages/peer-response.ts +0 -13
  76. package/src/messages/peer.ts +0 -13
  77. package/src/messages/peernet.ts +0 -13
  78. package/src/messages/ps.ts +0 -13
  79. package/src/messages/request.ts +0 -13
  80. package/src/messages/response.ts +0 -13
  81. package/src/messages.js +0 -12
  82. package/src/peer-info.js +0 -9
  83. package/src/peernet.ts +0 -1113
  84. package/src/prompts/password/browser.js +0 -1
  85. package/src/prompts/password/node.js +0 -6
  86. package/src/prompts/password.js +0 -1
  87. package/src/proto/chat-message.proto.js +0 -6
  88. package/src/proto/data-response.proto.js +0 -4
  89. package/src/proto/data.proto.js +0 -4
  90. package/src/proto/dht-response.proto.js +0 -4
  91. package/src/proto/dht.proto.js +0 -4
  92. package/src/proto/file-link.proto.js +0 -5
  93. package/src/proto/file.proto.js +0 -10
  94. package/src/proto/peer-response.proto.js +0 -3
  95. package/src/proto/peer.proto.js +0 -3
  96. package/src/proto/peernet.proto.js +0 -7
  97. package/src/proto/ps.proto.js +0 -4
  98. package/src/proto/request.proto.js +0 -4
  99. package/src/proto/response.proto.js +0 -3
  100. package/src/types.ts +0 -25
  101. package/src/utils/utils.js +0 -77
  102. package/test/all.js +0 -5
  103. package/test/client.js +0 -15
  104. package/test/codec.js +0 -56
  105. package/test/file-transfer.test.js +0 -81
  106. package/test/fresh-empty-store.js +0 -15
  107. package/test/hash.js +0 -14
  108. package/test/index.js +0 -6
  109. package/test/lastBlock.js +0 -7
  110. package/test/messages.js +0 -26
  111. package/test/peer-data-exchange.test.js +0 -52
  112. package/test/peer-file-exchange.test.js +0 -78
  113. package/test/peernet.js +0 -17
  114. package/test/peernet.test.js +0 -440
  115. package/test-output.log +0 -84
  116. package/test.js +0 -63
  117. package/test2.js +0 -13
  118. package/test3.js +0 -16
  119. package/test4.js +0 -7
  120. package/tsconfig.json +0 -13
package/README.md CHANGED
@@ -10,6 +10,19 @@ Peer-to-peer networking for Leofcoin and distributed applications.
10
10
 
11
11
  ## Installation
12
12
 
13
+ ### Breaking content-address format in 2.0
14
+
15
+ Peernet 2.x uses `@leofcoin/codec-format-interface` 2.x. All generated content
16
+ addresses use canonical `codec → digest byte length → digest` headers. Length-first
17
+ hashes are not supported by the hash parser. Stored 1.x content-address indexes
18
+ and linked manifests are not automatically migrated or rewritten.
19
+
20
+ Upgrade all message producers and consumers together. For a chain deployment,
21
+ coordinate the messages, workers, browser node and genesis migration before
22
+ starting against new state. Back up existing stores and owner identities; this
23
+ package does not delete or reset them. Do not mix old content-addressed chainstate
24
+ with the new format or treat an installation update as a completed network reset.
25
+
13
26
  ```sh
14
27
  npm install @leofcoin/peernet
15
28
  ```
@@ -0,0 +1,37 @@
1
+ # Peernet 3: automatic relay discovery
2
+
3
+ Peernet 3 adopts the published `@netpeer/swarm@0.10.0` transport. Normal nodes keep
4
+ the same startup API and configured discovery stars; no WebTransport URL, relay
5
+ secret, public certificate or extra relay process is required on ordinary peers.
6
+ Explicit `transport` restrictions/overrides remain supported.
7
+
8
+ Release verification builds both Node and browser bundles, keeps native transport
9
+ loaders out of browser resolution, cleans stale generated chunks, and tests a
10
+ packed artifact in a clean npm installation with registry-only dependencies.
11
+
12
+ This is a major release because the supported Node runtime moves from 19+ to
13
+ **24.15+**, matching the new native backend and Leofcoin launcher. The canonical
14
+ codec-first message format from peernet 2 is unchanged. This release does not
15
+ regenerate genesis, change allocations, replace identities, or delete stores.
16
+
17
+ ## Operators
18
+
19
+ Upgrade discovery infrastructure and peer software together for the new fallback
20
+ handshakes. Public stars running swarm 0.10 can host their relay within the star
21
+ process and advertise it only after QUIC is listening. Existing DNS, trusted TLS
22
+ and UDP firewall configuration are infrastructure prerequisites, not client setup.
23
+
24
+ WebRTC remains the default direct transport. A ready relay is discovered from the
25
+ configured star and used on fallback, with tickets requested over that same star
26
+ connection. Circuit fallback also requires an actual reciprocal handshake before
27
+ reporting a peer connected; larger circuit payloads use shared PeerCore chunking.
28
+
29
+ Transport connectivity is not proof of chain synchronization or finality. Check
30
+ real block/state synchronization and finalized transactions after public rollout.
31
+
32
+ ## Leofcoin release order
33
+
34
+ Publish peernet 3 only after review and build/tests. Then update chain's peernet
35
+ minimum to `^3.0.0` using the actual npm registry metadata and regenerate the
36
+ monorepo lockfile. Rebuild and verify a standalone launcher install before
37
+ publishing chain/launch-chain. Do not commit local tarball or Git overrides.
@@ -1,4 +1,4 @@
1
- import { m as getDefaultExportFromCjs } from './identity-CYVPDmkf.js';
1
+ import { m as getDefaultExportFromCjs } from './identity-Cf8KKnho.js';
2
2
 
3
3
  function _mergeNamespaces(n, m) {
4
4
  m.forEach(function (e) {
@@ -1,5 +1,5 @@
1
- import { L as LittlePubSub, d as deflate_1, i as inflate_1, c as createDebugger } from './peernet-CbI0-jFn.js';
2
- import './identity-CYVPDmkf.js';
1
+ import { L as LittlePubSub, d as deflate_1, i as inflate_1, c as createDebugger } from './peernet-Db43ag2D.js';
2
+ import './identity-Cf8KKnho.js';
3
3
  import 'crypto';
4
4
  import './value-C3vAp-wb.js';
5
5
 
@@ -232,7 +232,7 @@ class SocketRequestClient {
232
232
  const init = async () => {
233
233
  // @ts-ignore
234
234
  if (!globalThis.WebSocket && !this.#experimentalWebsocket)
235
- globalThis.WebSocket = (await import('./browser-DvU1xNFS.js').then(function (n) { return n.b; })).default.w3cwebsocket;
235
+ globalThis.WebSocket = (await import('./browser-CCfR9KOM.js').then(function (n) { return n.b; })).default.w3cwebsocket;
236
236
  const client = new WebSocket(this.#url, this.#protocol);
237
237
  if (this.#experimentalWebsocket) {
238
238
  client.addEventListener('error', this.onerror);
@@ -819,8 +819,61 @@ class WebRTCPeer {
819
819
  }
820
820
  }
821
821
 
822
- const getWebTransportCtor = () => {
822
+ // WebTransport streams carry bytes, not application message boundaries.
823
+ const MAX_FRAME_BYTES = 64 * 1024;
824
+ const encodeFrame = (payload) => {
825
+ if (!(payload instanceof Uint8Array) || !payload.length || payload.length > MAX_FRAME_BYTES) {
826
+ throw new Error('invalid transport frame length');
827
+ }
828
+ const frame = new Uint8Array(payload.length + 4);
829
+ new DataView(frame.buffer).setUint32(0, payload.length);
830
+ frame.set(payload, 4);
831
+ return frame;
832
+ };
833
+ class FrameReader {
834
+ constructor(reader) { this.reader = reader; this.chunk = new Uint8Array(); this.offset = 0; }
835
+ async exact(length, allowEnd = false) {
836
+ const result = new Uint8Array(length);
837
+ let written = 0;
838
+ while (written < length) {
839
+ if (this.offset === this.chunk.length) {
840
+ const { done, value } = await this.reader.read();
841
+ if (done) {
842
+ if (allowEnd && written === 0)
843
+ return null;
844
+ throw new Error('truncated transport frame');
845
+ }
846
+ this.chunk = value;
847
+ this.offset = 0;
848
+ if (!value?.length)
849
+ continue;
850
+ }
851
+ const count = Math.min(length - written, this.chunk.length - this.offset);
852
+ result.set(this.chunk.subarray(this.offset, this.offset + count), written);
853
+ this.offset += count;
854
+ written += count;
855
+ }
856
+ return result;
857
+ }
858
+ async read() {
859
+ const header = await this.exact(4, true);
860
+ if (!header)
861
+ return null;
862
+ const length = new DataView(header.buffer).getUint32(0);
863
+ if (!length || length > MAX_FRAME_BYTES)
864
+ throw new Error('invalid transport frame length');
865
+ return this.exact(length);
866
+ }
867
+ }
868
+ const jsonBytes = (value) => new TextEncoder().encode(JSON.stringify(value));
869
+ const parseJson = (bytes) => JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
870
+
871
+ const getWebTransportCtor = async () => {
823
872
  const candidate = globalThis;
873
+ if (!candidate.WebTransport && candidate.process?.versions?.node) {
874
+ const { WebTransport } = await import('@fails-components/webtransport');
875
+ return WebTransport;
876
+ }
824
877
  if (!candidate.WebTransport) {
825
878
  throw new Error('WebTransport is not available in this runtime');
826
879
  }
@@ -854,7 +907,12 @@ class WebTransportPeer {
854
907
  kind: 'webtransport-bidi',
855
908
  isConnected: () => this.connected,
856
909
  sendRaw: (payload) => {
857
- void this.#sendRaw(payload);
910
+ void this.#sendRaw(payload).catch(error => {
911
+ if (!this.#destroyed) {
912
+ this.#emit('error', error);
913
+ this.destroy();
914
+ }
915
+ });
858
916
  }
859
917
  }, {
860
918
  maxMessageSize: MAX_MESSAGE_SIZE,
@@ -865,7 +923,7 @@ class WebTransportPeer {
865
923
  }
866
924
  });
867
925
  queueMicrotask(() => {
868
- void this.#open(options.url);
926
+ void this.#open(options);
869
927
  });
870
928
  }
871
929
  #createMessageId() {
@@ -892,41 +950,55 @@ class WebTransportPeer {
892
950
  for (const handler of listeners)
893
951
  handler(...args);
894
952
  }
895
- async #open(url) {
953
+ async #open(options) {
896
954
  try {
897
- const WebTransportCtor = getWebTransportCtor();
898
- const session = new WebTransportCtor(url);
955
+ const ticket = await options.getTicket();
956
+ if (this.#destroyed)
957
+ return;
958
+ const WebTransportCtor = await getWebTransportCtor();
959
+ const session = new WebTransportCtor(options.url);
899
960
  this.#session = session;
961
+ void session.closed.then(() => this.destroy(), error => {
962
+ if (!this.#destroyed) {
963
+ this.#emit('error', error);
964
+ this.destroy();
965
+ }
966
+ });
900
967
  await session.ready;
968
+ if (this.#destroyed)
969
+ return;
901
970
  const stream = (await session.createBidirectionalStream());
971
+ if (this.#destroyed)
972
+ return;
902
973
  this.#reader = stream.readable.getReader();
903
974
  this.#writer = stream.writable.getWriter();
975
+ const frames = new FrameReader(this.#reader);
976
+ await this.#writer.write(encodeFrame(jsonBytes({ protocol: 'swarm-relay/1', ticket })));
977
+ const ready = parseJson(await frames.read());
978
+ if (ready.protocol !== 'swarm-relay/1' || ready.peerId !== this.peerId || String(ready.version) !== this.version) {
979
+ throw new Error('invalid WebTransport relay handshake');
980
+ }
981
+ if (this.#destroyed)
982
+ return;
904
983
  this.#connected = true;
905
984
  this.#emit('connect');
906
- void this.#readLoop();
907
- void session.closed.catch((error) => {
908
- if (this.#destroyed)
909
- return;
910
- this.#emit('error', error instanceof Error ? error : new Error(String(error)));
911
- });
985
+ void this.#readLoop(frames);
912
986
  }
913
987
  catch (error) {
914
988
  this.#emit('error', error instanceof Error ? error : new Error(String(error)));
915
989
  this.destroy();
916
990
  }
917
991
  }
918
- async #readLoop() {
992
+ async #readLoop(frames) {
919
993
  const reader = this.#reader;
920
994
  if (!reader)
921
995
  return;
922
996
  try {
923
997
  while (!this.#destroyed) {
924
998
  // eslint-disable-next-line no-await-in-loop
925
- const { done, value } = await reader.read();
926
- if (done)
927
- break;
999
+ const value = await frames.read();
928
1000
  if (!value)
929
- continue;
1001
+ break;
930
1002
  this.bw.down += value.length;
931
1003
  this.#emit('data', value);
932
1004
  }
@@ -942,7 +1014,7 @@ class WebTransportPeer {
942
1014
  async #sendRaw(payload) {
943
1015
  if (!this.#writer)
944
1016
  throw new Error('WebTransport writer is not ready');
945
- await this.#writer.write(payload);
1017
+ await this.#writer.write(encodeFrame(payload));
946
1018
  }
947
1019
  on(event, handler) {
948
1020
  const listeners = this.#listeners.get(event) ?? new Set();
@@ -972,13 +1044,13 @@ class WebTransportPeer {
972
1044
  this.#destroyed = true;
973
1045
  this.#connected = false;
974
1046
  try {
975
- void this.#reader?.cancel?.();
1047
+ void this.#reader?.cancel?.().catch(() => { });
976
1048
  }
977
1049
  catch (e) { }
978
1050
  this.#reader?.releaseLock?.();
979
1051
  this.#reader = null;
980
1052
  try {
981
- void this.#writer?.close?.();
1053
+ void this.#writer?.close?.().catch(() => { });
982
1054
  }
983
1055
  catch (e) { }
984
1056
  this.#writer?.releaseLock?.();
@@ -1006,6 +1078,7 @@ class CircuitPeer {
1006
1078
  #destroyed = false;
1007
1079
  #connected = false;
1008
1080
  #sendViaCircuit;
1081
+ #core;
1009
1082
  initiator = true;
1010
1083
  peerId;
1011
1084
  channelName;
@@ -1019,12 +1092,40 @@ class CircuitPeer {
1019
1092
  this.channelName = `circuit:${options.from}:${options.to}`;
1020
1093
  this.version = String(options.version);
1021
1094
  this.#sendViaCircuit = options.sendViaCircuit;
1022
- setTimeout(() => {
1023
- if (this.#destroyed)
1024
- return;
1025
- this.#connected = true;
1026
- this.#emit('connect');
1027
- }, 0);
1095
+ this.#core = new PeerCore({
1096
+ kind: 'circuit',
1097
+ isConnected: () => this.connected,
1098
+ sendRaw: data => {
1099
+ void this.#sendViaCircuit({ id: this.#createMessageId(), data: Array.from(data) }).catch(error => {
1100
+ if (this.#destroyed)
1101
+ return;
1102
+ this.#emit('error', error);
1103
+ this.destroy();
1104
+ });
1105
+ }
1106
+ }, {
1107
+ // JSON byte arrays expand up to four characters per byte. Keep the whole
1108
+ // WebSocket frame below its default 64 KiB limit, including wrapper/ID.
1109
+ maxMessageSize: 14_000,
1110
+ compressionThreshold: 0.98,
1111
+ getPubSub,
1112
+ onPayloadSend: bytes => { this.bw.up += bytes; }
1113
+ });
1114
+ queueMicrotask(async () => {
1115
+ try {
1116
+ await options.handshake();
1117
+ if (this.#destroyed)
1118
+ return;
1119
+ this.#connected = true;
1120
+ this.#emit('connect');
1121
+ }
1122
+ catch (error) {
1123
+ if (this.#destroyed)
1124
+ return;
1125
+ this.#emit('error', error);
1126
+ this.destroy();
1127
+ }
1128
+ });
1028
1129
  }
1029
1130
  #createMessageId() {
1030
1131
  const randomUUID = globalThis.crypto?.randomUUID;
@@ -1054,43 +1155,21 @@ class CircuitPeer {
1054
1155
  // Circuit transport does not use SDP/ICE signaling.
1055
1156
  }
1056
1157
  send(data, id = this.#createMessageId()) {
1158
+ if (new TextEncoder().encode(id).length > 1024) {
1159
+ this.#emit('error', new Error('circuit message id exceeds 1024 bytes'));
1160
+ return;
1161
+ }
1057
1162
  if (this.#destroyed) {
1058
1163
  this.#emit('error', new Error('circuit peer is destroyed'));
1059
1164
  return;
1060
1165
  }
1061
- this.bw.up += data.length;
1062
- void this.#sendViaCircuit({ id, data: Array.from(data) }).catch((error) => {
1063
- this.#emit('error', error instanceof Error ? error : new Error(String(error)));
1064
- });
1166
+ this.#core.send(data, id);
1065
1167
  }
1066
1168
  request(data, id = this.#createMessageId()) {
1067
- const pubsub = getPubSub();
1068
- if (!pubsub) {
1069
- return Promise.reject(new Error('globalThis.pubsub is not available'));
1169
+ if (new TextEncoder().encode(id).length > 1024) {
1170
+ return Promise.reject(new Error('circuit message id exceeds 1024 bytes'));
1070
1171
  }
1071
- return new Promise((resolve, reject) => {
1072
- let timeout;
1073
- let settled = false;
1074
- const finish = (fn) => {
1075
- if (settled)
1076
- return;
1077
- settled = true;
1078
- clearTimeout(timeout);
1079
- try {
1080
- pubsub.unsubscribe(id, onResponse);
1081
- }
1082
- catch (e) { }
1083
- fn();
1084
- };
1085
- const onResponse = ({ data: response }) => {
1086
- finish(() => resolve(response));
1087
- };
1088
- timeout = setTimeout(() => {
1089
- finish(() => reject(new Error(`request for ${id} timed out`)));
1090
- }, 30_000);
1091
- pubsub.subscribe(id, onResponse);
1092
- this.send(data, id);
1093
- });
1172
+ return this.#core.request(data, id);
1094
1173
  }
1095
1174
  async getNetworkStats() {
1096
1175
  return null;
@@ -1163,6 +1242,8 @@ class Client {
1163
1242
  #fallbackCircuitMethod = 'swarm:fallback:send';
1164
1243
  #testHooks;
1165
1244
  #webTransportUrlTemplate;
1245
+ #discoverWebTransport = true;
1246
+ #relayUrls = new WeakMap();
1166
1247
  #circuitHandlers = new Map();
1167
1248
  #pendingCircuitRequests = new Map();
1168
1249
  #circuitRequestTimeoutMs = 30_000;
@@ -1201,7 +1282,9 @@ class Client {
1201
1282
  ? options.transport.kinds
1202
1283
  : options.transport?.kind
1203
1284
  ? [options.transport.kind]
1204
- : ['webrtc', 'webtransport'];
1285
+ : (options.transport?.webtransport?.urlTemplate
1286
+ ? ['webrtc', 'webtransport']
1287
+ : ['webrtc']);
1205
1288
  this.#supportedTransports = new Set(this.#normalizeTransportKinds(configuredKinds));
1206
1289
  if (!this.#supportedTransports.size)
1207
1290
  this.#supportedTransports.add('webrtc');
@@ -1212,6 +1295,11 @@ class Client {
1212
1295
  ? 'webrtc'
1213
1296
  : 'webtransport';
1214
1297
  this.#webTransportUrlTemplate = options.transport?.webtransport?.urlTemplate;
1298
+ this.#discoverWebTransport = !options.transport?.kind && !options.transport?.kinds ||
1299
+ !!options.transport?.kinds?.includes('webtransport') || options.transport?.kind === 'webtransport';
1300
+ if (this.#discoverWebTransport && (typeof globalThis.WebTransport === 'function' || globalThis.process?.versions?.node)) {
1301
+ this.#supportedTransports.add('webtransport');
1302
+ }
1215
1303
  const wrtcBackend = options.transport?.webrtc?.backend ||
1216
1304
  globalThis.process?.env?.NETPEER_SWARM_WRTC_BACKEND ||
1217
1305
  'vandeurenglenn';
@@ -1229,6 +1317,9 @@ class Client {
1229
1317
  if (options?.retry)
1230
1318
  this.#retryOptions = { ...this.#retryOptions, ...options.retry };
1231
1319
  this.#circuitHandlers.set(this.#fallbackCircuitMethod, (payload, context) => this.#handleFallbackCircuitPayload(payload, context));
1320
+ this.#circuitHandlers.set('swarm:fallback:hello', () => ({
1321
+ peerId: this.peerId, version: this.version, encoding: 'peer-core/1'
1322
+ }));
1232
1323
  if (!this.#testHooks?.skipInit) {
1233
1324
  this._init();
1234
1325
  }
@@ -1367,6 +1458,30 @@ class Client {
1367
1458
  const onServerData = (message) => {
1368
1459
  globalThis.pubsub.publish('server:data', message);
1369
1460
  };
1461
+ const onRelayReady = (message) => {
1462
+ if (!this.#discoverWebTransport || message?.protocol !== 'swarm-relay/1' || typeof message.url !== 'string')
1463
+ return;
1464
+ let url;
1465
+ try {
1466
+ url = new URL(message.url);
1467
+ }
1468
+ catch {
1469
+ return;
1470
+ }
1471
+ if (url.protocol !== 'https:' || url.username || url.password || url.hash)
1472
+ return;
1473
+ if (this.#relayUrls.get(starConnection) === url.href)
1474
+ return;
1475
+ this.#relayUrls.set(starConnection, url.href);
1476
+ this.#supportedTransports.add('webtransport');
1477
+ // Update capabilities on this same authenticated star connection. The
1478
+ // server sends relay metadata before its peer snapshot, so fallback can
1479
+ // resolve a real endpoint without users configuring transport URLs.
1480
+ starConnection.send({ url: 'join', params: {
1481
+ version: this.version, peerId: this.peerId,
1482
+ transport: { kind: this.#preferredTransportKind, kinds: [...this.#supportedTransports] }
1483
+ } });
1484
+ };
1370
1485
  starConnection.pubsub.subscribe('peer:joined', onPeerJoined);
1371
1486
  starConnection.pubsub.subscribe('peer:left', onPeerLeft);
1372
1487
  starConnection.pubsub.subscribe('star:joined', onStarJoined);
@@ -1374,6 +1489,7 @@ class Client {
1374
1489
  starConnection.pubsub.subscribe('signal', onSignal);
1375
1490
  starConnection.pubsub.subscribe('circuit', onCircuit);
1376
1491
  starConnection.pubsub.subscribe('server:data', onServerData);
1492
+ starConnection.pubsub.subscribe('relay:ready', onRelayReady);
1377
1493
  this.#starListeners[starId] = [
1378
1494
  { topic: 'peer:joined', handler: onPeerJoined },
1379
1495
  { topic: 'peer:left', handler: onPeerLeft },
@@ -1381,7 +1497,8 @@ class Client {
1381
1497
  { topic: 'star:left', handler: onStarLeft },
1382
1498
  { topic: 'signal', handler: onSignal },
1383
1499
  { topic: 'circuit', handler: onCircuit },
1384
- { topic: 'server:data', handler: onServerData }
1500
+ { topic: 'server:data', handler: onServerData },
1501
+ { topic: 'relay:ready', handler: onRelayReady }
1385
1502
  ];
1386
1503
  }
1387
1504
  #starJoined = (id) => {
@@ -1587,12 +1704,12 @@ class Client {
1587
1704
  peer = this.#createRTCPeerConnection(peerId, attempt.star, attempt.version, attempt.initiator);
1588
1705
  }
1589
1706
  else if (transportKind === 'webtransport') {
1590
- const url = this.#resolveWebTransportUrl(peerId);
1707
+ const url = this.#resolveWebTransportUrl(peerId, attempt.star);
1591
1708
  if (!url) {
1592
1709
  this.#advanceTransportAttempt(peerId);
1593
1710
  return;
1594
1711
  }
1595
- peer = this.#createWebTransportConnection(peerId, url, attempt.version);
1712
+ peer = this.#createWebTransportConnection(peerId, url, attempt.version, attempt.star);
1596
1713
  }
1597
1714
  else {
1598
1715
  peer = this.#createCircuitConnection(peerId, attempt.version);
@@ -1639,7 +1756,25 @@ class Client {
1639
1756
  if (!peer) {
1640
1757
  peer = this.#createCircuitConnection(context.from, this.version);
1641
1758
  }
1642
- this.#noticeMessage(decoded.data, decoded.id, context.from, peer);
1759
+ if (!peer)
1760
+ throw new Error('circuit peer unavailable');
1761
+ if (!peer.connected) {
1762
+ const waitingPeer = peer;
1763
+ await new Promise((resolve, reject) => {
1764
+ const finish = (error) => {
1765
+ clearTimeout(timer);
1766
+ waitingPeer.off('connect', onConnect);
1767
+ waitingPeer.off('close', onClose);
1768
+ error ? reject(error) : resolve();
1769
+ };
1770
+ const onConnect = () => finish();
1771
+ const onClose = () => finish(new Error('circuit peer closed during handshake'));
1772
+ const timer = setTimeout(() => finish(new Error('circuit handshake timed out')), this.#transportConnectTimeoutMs);
1773
+ waitingPeer.on('connect', onConnect);
1774
+ waitingPeer.on('close', onClose);
1775
+ });
1776
+ }
1777
+ this.#peerData(peer, decoded.data);
1643
1778
  return { ok: true };
1644
1779
  }
1645
1780
  #createCircuitId() {
@@ -1697,15 +1832,22 @@ class Client {
1697
1832
  this.#pendingCircuitRequests.delete(id);
1698
1833
  reject(new Error(`circuit request for ${id} timed out`));
1699
1834
  }, timeoutMs);
1700
- this.#pendingCircuitRequests.set(id, { resolve, reject, timeout });
1701
- this.#sendCircuit(starConnection, {
1702
- to: peerId,
1703
- from: this.peerId,
1704
- id,
1705
- phase: 'request',
1706
- method,
1707
- data: this.#encodeCircuitData(data)
1708
- });
1835
+ this.#pendingCircuitRequests.set(id, { resolve, reject, timeout, peerId, starConnection });
1836
+ try {
1837
+ this.#sendCircuit(starConnection, {
1838
+ to: peerId,
1839
+ from: this.peerId,
1840
+ id,
1841
+ phase: 'request',
1842
+ method,
1843
+ data: this.#encodeCircuitData(data)
1844
+ });
1845
+ }
1846
+ catch (error) {
1847
+ clearTimeout(timeout);
1848
+ this.#pendingCircuitRequests.delete(id);
1849
+ reject(error);
1850
+ }
1709
1851
  });
1710
1852
  }
1711
1853
  #inComingCircuit = async ({ from, to, id, phase, method, data, error }, starConnection) => {
@@ -1715,6 +1857,10 @@ class Client {
1715
1857
  const pending = this.#pendingCircuitRequests.get(id);
1716
1858
  if (!pending)
1717
1859
  return;
1860
+ if (pending.starConnection !== starConnection)
1861
+ return;
1862
+ if (from !== pending.peerId && !(from === 'server' && error))
1863
+ return;
1718
1864
  clearTimeout(pending.timeout);
1719
1865
  this.#pendingCircuitRequests.delete(id);
1720
1866
  if (error) {
@@ -1788,17 +1934,16 @@ class Client {
1788
1934
  connectWebTransport(peerId, url) {
1789
1935
  this.#createWebTransportConnection(peerId, url, this.version);
1790
1936
  }
1791
- #resolveWebTransportUrl(peerId) {
1792
- const urlTemplate = this.#webTransportUrlTemplate;
1937
+ #resolveWebTransportUrl(peerId, starConnection) {
1938
+ const urlTemplate = this.#webTransportUrlTemplate || (starConnection && this.#relayUrls.get(starConnection));
1793
1939
  if (!urlTemplate) {
1794
- console.warn('WebTransport requires transport.webtransport.urlTemplate in client options');
1795
1940
  return null;
1796
1941
  }
1797
1942
  return urlTemplate
1798
1943
  .replace('{peerId}', encodeURIComponent(peerId))
1799
1944
  .replace('{selfId}', encodeURIComponent(this.peerId));
1800
1945
  }
1801
- #createWebTransportConnection(peerId, url, version) {
1946
+ #createWebTransportConnection(peerId, url, version, starConnection) {
1802
1947
  if (this.#connections[peerId]) {
1803
1948
  debug(`peer ${peerId} already connected`);
1804
1949
  return null;
@@ -1823,7 +1968,27 @@ class Client {
1823
1968
  from: this.peerId,
1824
1969
  to: peerId,
1825
1970
  version,
1826
- url
1971
+ url,
1972
+ getTicket: async () => {
1973
+ const star = starConnection || this.#pickStarConnection();
1974
+ if (!star)
1975
+ throw new Error('no connected star available for WebTransport ticket');
1976
+ let timer;
1977
+ try {
1978
+ const response = await Promise.race([
1979
+ star.request({ url: 'webtransport-ticket', params: { from: this.peerId, to: peerId } }),
1980
+ new Promise((_, reject) => {
1981
+ timer = setTimeout(() => reject(new Error('WebTransport ticket request timed out')), this.#transportConnectTimeoutMs);
1982
+ })
1983
+ ]);
1984
+ if (typeof response?.ticket !== 'string')
1985
+ throw new Error('star did not authorize WebTransport relay');
1986
+ return response.ticket;
1987
+ }
1988
+ finally {
1989
+ clearTimeout(timer);
1990
+ }
1991
+ }
1827
1992
  });
1828
1993
  peer.on('connect', () => this.#peerConnect(peer));
1829
1994
  peer.on('close', () => this.#peerClose(peer));
@@ -1857,6 +2022,12 @@ class Client {
1857
2022
  from: this.peerId,
1858
2023
  to: peerId,
1859
2024
  version,
2025
+ handshake: async () => {
2026
+ const response = await this.circuitRequest(peerId, 'swarm:fallback:hello', undefined, this.#transportConnectTimeoutMs);
2027
+ if (response?.peerId !== peerId || String(response?.version) !== String(version) || response.encoding !== 'peer-core/1') {
2028
+ throw new Error('invalid circuit handshake response');
2029
+ }
2030
+ },
1860
2031
  sendViaCircuit: (payload) => this.circuitRequest(peerId, this.#fallbackCircuitMethod, payload)
1861
2032
  });
1862
2033
  peer.on('connect', () => this.#peerConnect(peer));
@@ -2173,6 +2344,11 @@ class Client {
2173
2344
  peer.destroy();
2174
2345
  };
2175
2346
  async close() {
2347
+ for (const pending of this.#pendingCircuitRequests.values()) {
2348
+ clearTimeout(pending.timeout);
2349
+ pending.reject(new Error('client closed'));
2350
+ }
2351
+ this.#pendingCircuitRequests.clear();
2176
2352
  for (const peerId of this.#peerTransportAttempts.keys()) {
2177
2353
  this.#clearTransportAttempt(peerId);
2178
2354
  }