cairn-ts 0.2.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 (76) hide show
  1. package/README.md +43 -0
  2. package/dist/index.cjs +1883 -0
  3. package/dist/index.d.cts +572 -0
  4. package/dist/index.d.ts +572 -0
  5. package/dist/index.js +1827 -0
  6. package/eslint.config.js +24 -0
  7. package/package.json +54 -0
  8. package/src/channel.ts +277 -0
  9. package/src/config.ts +161 -0
  10. package/src/crypto/aead.ts +80 -0
  11. package/src/crypto/double-ratchet.ts +355 -0
  12. package/src/crypto/exchange.ts +51 -0
  13. package/src/crypto/hkdf.ts +33 -0
  14. package/src/crypto/identity.ts +84 -0
  15. package/src/crypto/index.ts +20 -0
  16. package/src/crypto/noise.ts +415 -0
  17. package/src/crypto/sas.ts +36 -0
  18. package/src/crypto/spake2.ts +169 -0
  19. package/src/discovery/index.ts +38 -0
  20. package/src/discovery/manager.ts +138 -0
  21. package/src/discovery/rendezvous.ts +189 -0
  22. package/src/discovery/tracker.ts +251 -0
  23. package/src/errors.ts +166 -0
  24. package/src/index.ts +57 -0
  25. package/src/mesh/index.ts +48 -0
  26. package/src/mesh/relay.ts +100 -0
  27. package/src/mesh/routing-table.ts +196 -0
  28. package/src/node.ts +619 -0
  29. package/src/pairing/adapter.ts +51 -0
  30. package/src/pairing/index.ts +40 -0
  31. package/src/pairing/link.ts +127 -0
  32. package/src/pairing/payload.ts +98 -0
  33. package/src/pairing/pin.ts +115 -0
  34. package/src/pairing/psk.ts +49 -0
  35. package/src/pairing/qr.ts +52 -0
  36. package/src/pairing/rate-limit.ts +134 -0
  37. package/src/pairing/sas-flow.ts +45 -0
  38. package/src/pairing/state-machine.ts +438 -0
  39. package/src/pairing/unpairing.ts +50 -0
  40. package/src/protocol/custom-handler.ts +52 -0
  41. package/src/protocol/envelope.ts +138 -0
  42. package/src/protocol/index.ts +36 -0
  43. package/src/protocol/message-types.ts +74 -0
  44. package/src/protocol/version.ts +98 -0
  45. package/src/server/index.ts +67 -0
  46. package/src/server/management.ts +285 -0
  47. package/src/server/store-forward.ts +266 -0
  48. package/src/session/backoff.ts +58 -0
  49. package/src/session/heartbeat.ts +79 -0
  50. package/src/session/index.ts +26 -0
  51. package/src/session/message-queue.ts +133 -0
  52. package/src/session/network-monitor.ts +130 -0
  53. package/src/session/state-machine.ts +122 -0
  54. package/src/session.ts +223 -0
  55. package/src/transport/fallback.ts +475 -0
  56. package/src/transport/index.ts +46 -0
  57. package/src/transport/libp2p-node.ts +158 -0
  58. package/src/transport/nat.ts +348 -0
  59. package/tests/conformance/cbor-vectors.test.ts +250 -0
  60. package/tests/integration/pairing-session.test.ts +317 -0
  61. package/tests/unit/config-api.test.ts +310 -0
  62. package/tests/unit/crypto.test.ts +407 -0
  63. package/tests/unit/discovery.test.ts +618 -0
  64. package/tests/unit/double-ratchet.test.ts +185 -0
  65. package/tests/unit/mesh.test.ts +349 -0
  66. package/tests/unit/noise.test.ts +346 -0
  67. package/tests/unit/pairing-extras.test.ts +402 -0
  68. package/tests/unit/pairing.test.ts +572 -0
  69. package/tests/unit/protocol.test.ts +438 -0
  70. package/tests/unit/reconnection.test.ts +402 -0
  71. package/tests/unit/scaffolding.test.ts +142 -0
  72. package/tests/unit/server.test.ts +492 -0
  73. package/tests/unit/sessions.test.ts +595 -0
  74. package/tests/unit/transport.test.ts +604 -0
  75. package/tsconfig.json +20 -0
  76. package/vitest.config.ts +15 -0
@@ -0,0 +1,475 @@
1
+ import { CairnError, TransportExhaustedError } from '../errors.js';
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Transport type (9-level fallback chain)
5
+ // ---------------------------------------------------------------------------
6
+
7
+ /** Transport type in the 9-level fallback chain (spec section 2). */
8
+ export type FallbackTransportType =
9
+ | 'quic'
10
+ | 'stun-udp'
11
+ | 'tcp'
12
+ | 'turn-udp'
13
+ | 'turn-tcp'
14
+ | 'websocket-tls'
15
+ | 'webtransport'
16
+ | 'circuit-relay-v2'
17
+ | 'https-long-polling';
18
+
19
+ /** All transport types in priority order. */
20
+ const ALL_IN_ORDER: FallbackTransportType[] = [
21
+ 'quic',
22
+ 'stun-udp',
23
+ 'tcp',
24
+ 'turn-udp',
25
+ 'turn-tcp',
26
+ 'websocket-tls',
27
+ 'webtransport',
28
+ 'circuit-relay-v2',
29
+ 'https-long-polling',
30
+ ];
31
+
32
+ /** Priority number (1 = best, 9 = worst). */
33
+ export function transportPriority(t: FallbackTransportType): number {
34
+ return ALL_IN_ORDER.indexOf(t) + 1;
35
+ }
36
+
37
+ /** Human-readable display name. */
38
+ export function transportDisplayName(t: FallbackTransportType): string {
39
+ const names: Record<FallbackTransportType, string> = {
40
+ 'quic': 'Direct QUIC v1',
41
+ 'stun-udp': 'STUN-assisted UDP hole punch',
42
+ 'tcp': 'Direct TCP',
43
+ 'turn-udp': 'TURN relay (UDP)',
44
+ 'turn-tcp': 'TURN relay (TCP)',
45
+ 'websocket-tls': 'WebSocket/TLS (443)',
46
+ 'webtransport': 'WebTransport/HTTP3 (443)',
47
+ 'circuit-relay-v2': 'Circuit Relay v2',
48
+ 'https-long-polling': 'HTTPS long-polling (443)',
49
+ };
50
+ return names[t];
51
+ }
52
+
53
+ /** Whether this transport is available in Tier 0 (zero-config). */
54
+ export function isTier0Available(t: FallbackTransportType): boolean {
55
+ return t === 'quic' || t === 'stun-udp' || t === 'tcp' || t === 'circuit-relay-v2';
56
+ }
57
+
58
+ /** Get all transport types in priority order. */
59
+ export function allTransportsInOrder(): FallbackTransportType[] {
60
+ return [...ALL_IN_ORDER];
61
+ }
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // TransportAttempt — single entry in the fallback chain
65
+ // ---------------------------------------------------------------------------
66
+
67
+ /** Configuration for a single transport attempt in the fallback chain. */
68
+ export interface TransportAttempt {
69
+ priority: number;
70
+ transportType: FallbackTransportType;
71
+ timeoutMs: number;
72
+ available: boolean;
73
+ }
74
+
75
+ /** Result of attempting a single transport in the fallback chain. */
76
+ export interface TransportAttemptResult {
77
+ transportType: FallbackTransportType;
78
+ error?: string;
79
+ skipped: boolean;
80
+ durationMs: number;
81
+ }
82
+
83
+ function formatAttemptResult(r: TransportAttemptResult): string {
84
+ const name = transportDisplayName(r.transportType);
85
+ if (r.skipped) {
86
+ return `${name}: skipped (not configured)`;
87
+ }
88
+ if (r.error) {
89
+ return `${name}: failed (${r.error}) [${r.durationMs}ms]`;
90
+ }
91
+ return `${name}: success [${r.durationMs}ms]`;
92
+ }
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // FallbackChain — the 9-level transport priority chain engine
96
+ // ---------------------------------------------------------------------------
97
+
98
+ /** Default per-transport timeout in milliseconds. */
99
+ export const DEFAULT_TRANSPORT_TIMEOUT_MS = 10_000;
100
+
101
+ /**
102
+ * Executes the 9-level transport priority chain (spec section 2).
103
+ *
104
+ * Supports both sequential and parallel (ICE-style) probing modes.
105
+ * In parallel mode, multiple transports are attempted concurrently
106
+ * and the first success wins.
107
+ */
108
+ export class FallbackChain {
109
+ private readonly _transports: TransportAttempt[];
110
+ private readonly _parallelMode: boolean;
111
+
112
+ private constructor(transports: TransportAttempt[], parallelMode: boolean) {
113
+ this._transports = transports;
114
+ this._parallelMode = parallelMode;
115
+ }
116
+
117
+ /**
118
+ * Create a new fallback chain with the full 9-level priority list.
119
+ *
120
+ * `hasTurn` / `hasRelay443` control whether TURN and port-443 relays
121
+ * are available (Tier 1+ infrastructure).
122
+ */
123
+ static create(
124
+ perTransportTimeoutMs: number,
125
+ hasTurn: boolean,
126
+ hasRelay443: boolean,
127
+ parallelMode: boolean,
128
+ ): FallbackChain {
129
+ const transports: TransportAttempt[] = ALL_IN_ORDER.map((tt) => {
130
+ let available: boolean;
131
+ if (tt === 'turn-udp' || tt === 'turn-tcp') {
132
+ available = hasTurn;
133
+ } else if (tt === 'websocket-tls' || tt === 'webtransport' || tt === 'https-long-polling') {
134
+ available = hasRelay443;
135
+ } else {
136
+ available = true;
137
+ }
138
+ return {
139
+ priority: transportPriority(tt),
140
+ transportType: tt,
141
+ timeoutMs: perTransportTimeoutMs,
142
+ available,
143
+ };
144
+ });
145
+ return new FallbackChain(transports, parallelMode);
146
+ }
147
+
148
+ /** Create a Tier 0 (zero-config) fallback chain. Only priorities 1-3 and 8 available. */
149
+ static tier0(perTransportTimeoutMs: number = DEFAULT_TRANSPORT_TIMEOUT_MS): FallbackChain {
150
+ return FallbackChain.create(perTransportTimeoutMs, false, false, false);
151
+ }
152
+
153
+ /** Get the transport attempts in priority order. */
154
+ get transports(): readonly TransportAttempt[] {
155
+ return this._transports;
156
+ }
157
+
158
+ /** Whether parallel probing is enabled. */
159
+ get parallelMode(): boolean {
160
+ return this._parallelMode;
161
+ }
162
+
163
+ /**
164
+ * Execute the fallback chain, attempting each transport in order.
165
+ *
166
+ * The `attemptFn` is called for each available transport and should
167
+ * return a value on success or throw on failure. The first success is
168
+ * returned. If all fail, throws `TransportExhaustedError`.
169
+ *
170
+ * In parallel mode, available transports are attempted concurrently
171
+ * and the first success wins.
172
+ */
173
+ async execute<T>(
174
+ attemptFn: (transportType: FallbackTransportType, timeoutMs: number) => Promise<T>,
175
+ ): Promise<{ transportType: FallbackTransportType; value: T }> {
176
+ if (this._parallelMode) {
177
+ return this.executeParallel(attemptFn);
178
+ }
179
+ return this.executeSequential(attemptFn);
180
+ }
181
+
182
+ /** Sequential execution: attempt each transport in priority order. */
183
+ private async executeSequential<T>(
184
+ attemptFn: (transportType: FallbackTransportType, timeoutMs: number) => Promise<T>,
185
+ ): Promise<{ transportType: FallbackTransportType; value: T }> {
186
+ const results: TransportAttemptResult[] = [];
187
+
188
+ for (const attempt of this._transports) {
189
+ if (!attempt.available) {
190
+ results.push({
191
+ transportType: attempt.transportType,
192
+ skipped: true,
193
+ durationMs: 0,
194
+ });
195
+ continue;
196
+ }
197
+
198
+ const start = Date.now();
199
+ try {
200
+ const value = await attemptFn(attempt.transportType, attempt.timeoutMs);
201
+ return { transportType: attempt.transportType, value };
202
+ } catch (e) {
203
+ const elapsed = Date.now() - start;
204
+ results.push({
205
+ transportType: attempt.transportType,
206
+ error: e instanceof Error ? e.message : String(e),
207
+ skipped: false,
208
+ durationMs: elapsed,
209
+ });
210
+ }
211
+ }
212
+
213
+ throw buildTransportExhaustedError(results);
214
+ }
215
+
216
+ /** Parallel (ICE-style) execution: attempt all available concurrently, first success wins. */
217
+ private async executeParallel<T>(
218
+ attemptFn: (transportType: FallbackTransportType, timeoutMs: number) => Promise<T>,
219
+ ): Promise<{ transportType: FallbackTransportType; value: T }> {
220
+ const skippedResults: TransportAttemptResult[] = [];
221
+ const promises: Promise<{ transportType: FallbackTransportType; value: T }>[] = [];
222
+ const abortController = new AbortController();
223
+
224
+ for (const attempt of this._transports) {
225
+ if (!attempt.available) {
226
+ skippedResults.push({
227
+ transportType: attempt.transportType,
228
+ skipped: true,
229
+ durationMs: 0,
230
+ });
231
+ continue;
232
+ }
233
+
234
+ const transportType = attempt.transportType;
235
+ const timeoutMs = attempt.timeoutMs;
236
+
237
+ promises.push(
238
+ (async () => {
239
+ const value = await attemptFn(transportType, timeoutMs);
240
+ return { transportType, value };
241
+ })(),
242
+ );
243
+ }
244
+
245
+ if (promises.length === 0) {
246
+ throw buildTransportExhaustedError(skippedResults);
247
+ }
248
+
249
+ // Race all promises; collect failures
250
+ const failedResults: TransportAttemptResult[] = [];
251
+ const settled = await Promise.allSettled(promises);
252
+
253
+ // First check for any success
254
+ for (const result of settled) {
255
+ if (result.status === 'fulfilled') {
256
+ abortController.abort();
257
+ return result.value;
258
+ }
259
+ }
260
+
261
+ // All failed — collect errors
262
+ for (const result of settled) {
263
+ if (result.status === 'rejected') {
264
+ const err = result.reason;
265
+ failedResults.push({
266
+ transportType: 'quic', // placeholder — we reconstruct below
267
+ error: err instanceof Error ? err.message : String(err),
268
+ skipped: false,
269
+ durationMs: 0,
270
+ });
271
+ }
272
+ }
273
+
274
+ throw buildTransportExhaustedError([...skippedResults, ...failedResults]);
275
+ }
276
+ }
277
+
278
+ /** Build a TransportExhaustedError with detailed diagnostics. */
279
+ function buildTransportExhaustedError(results: TransportAttemptResult[]): TransportExhaustedError {
280
+ const details = results.map(formatAttemptResult).join('; ');
281
+ const hasSkipped = results.some((r) => r.skipped);
282
+ const suggestion = hasSkipped
283
+ ? 'deploy companion infrastructure (TURN relay, WebSocket relay on port 443) to enable additional transport fallbacks'
284
+ : 'check network connectivity and firewall rules';
285
+
286
+ return new TransportExhaustedError(
287
+ `all transports exhausted: ${details}`,
288
+ { details, suggestion, results },
289
+ );
290
+ }
291
+
292
+ // ---------------------------------------------------------------------------
293
+ // ConnectionQuality — metrics
294
+ // ---------------------------------------------------------------------------
295
+
296
+ /** Connection quality metrics (spec FR-4.5, spec section 6). */
297
+ export interface ConnectionQuality {
298
+ /** Round-trip latency in ms. */
299
+ latencyMs: number;
300
+ /** Jitter (latency variance) in ms. */
301
+ jitterMs: number;
302
+ /** Packet loss ratio (0.0 = none, 1.0 = total loss). */
303
+ packetLossRatio: number;
304
+ }
305
+
306
+ /** Default connection quality (no metrics yet). */
307
+ export function defaultConnectionQuality(): ConnectionQuality {
308
+ return { latencyMs: 0, jitterMs: 0, packetLossRatio: 0 };
309
+ }
310
+
311
+ /** Thresholds that trigger proactive transport migration (spec FR-4.5). */
312
+ export interface QualityThresholds {
313
+ maxLatencyMs: number;
314
+ maxJitterMs: number;
315
+ maxPacketLoss: number;
316
+ }
317
+
318
+ /** Default quality thresholds. */
319
+ export function defaultQualityThresholds(): QualityThresholds {
320
+ return { maxLatencyMs: 500, maxJitterMs: 100, maxPacketLoss: 0.05 };
321
+ }
322
+
323
+ // ---------------------------------------------------------------------------
324
+ // DegradationEvent
325
+ // ---------------------------------------------------------------------------
326
+
327
+ /** Which quality threshold was exceeded. */
328
+ export type DegradationReason = 'high_latency' | 'high_jitter' | 'high_packet_loss';
329
+
330
+ /** Degradation event emitted when connection quality drops below thresholds. */
331
+ export interface DegradationEvent {
332
+ quality: ConnectionQuality;
333
+ reason: DegradationReason;
334
+ }
335
+
336
+ // ---------------------------------------------------------------------------
337
+ // ConnectionQualityMonitor
338
+ // ---------------------------------------------------------------------------
339
+
340
+ export type DegradationListener = (event: DegradationEvent) => void;
341
+
342
+ /**
343
+ * Monitors connection quality and emits degradation events (spec FR-4.5).
344
+ *
345
+ * When any metric exceeds its threshold, a DegradationEvent is emitted
346
+ * to trigger the TransportMigrator.
347
+ */
348
+ export class ConnectionQualityMonitor {
349
+ private readonly _thresholds: QualityThresholds;
350
+ private readonly _sampleIntervalMs: number;
351
+ private readonly _listeners: DegradationListener[] = [];
352
+
353
+ constructor(thresholds?: QualityThresholds, sampleIntervalMs: number = 1000) {
354
+ this._thresholds = thresholds ?? defaultQualityThresholds();
355
+ this._sampleIntervalMs = sampleIntervalMs;
356
+ }
357
+
358
+ get thresholds(): QualityThresholds {
359
+ return this._thresholds;
360
+ }
361
+
362
+ get sampleIntervalMs(): number {
363
+ return this._sampleIntervalMs;
364
+ }
365
+
366
+ onDegradation(listener: DegradationListener): void {
367
+ this._listeners.push(listener);
368
+ }
369
+
370
+ /** Report a new quality sample. Checks thresholds and emits degradation events. */
371
+ reportSample(quality: ConnectionQuality): void {
372
+ if (quality.latencyMs > this._thresholds.maxLatencyMs) {
373
+ this.emit({ quality, reason: 'high_latency' });
374
+ }
375
+ if (quality.jitterMs > this._thresholds.maxJitterMs) {
376
+ this.emit({ quality, reason: 'high_jitter' });
377
+ }
378
+ if (quality.packetLossRatio > this._thresholds.maxPacketLoss) {
379
+ this.emit({ quality, reason: 'high_packet_loss' });
380
+ }
381
+ }
382
+
383
+ /** Check whether a quality sample exceeds any threshold (without emitting). */
384
+ isDegraded(quality: ConnectionQuality): boolean {
385
+ return (
386
+ quality.latencyMs > this._thresholds.maxLatencyMs ||
387
+ quality.jitterMs > this._thresholds.maxJitterMs ||
388
+ quality.packetLossRatio > this._thresholds.maxPacketLoss
389
+ );
390
+ }
391
+
392
+ private emit(event: DegradationEvent): void {
393
+ for (const listener of this._listeners) {
394
+ listener(event);
395
+ }
396
+ }
397
+ }
398
+
399
+ // ---------------------------------------------------------------------------
400
+ // MigrationEvent
401
+ // ---------------------------------------------------------------------------
402
+
403
+ /** Migration event indicating a better transport is available. */
404
+ export interface MigrationEvent {
405
+ from: FallbackTransportType;
406
+ to: FallbackTransportType;
407
+ }
408
+
409
+ // ---------------------------------------------------------------------------
410
+ // TransportMigrator
411
+ // ---------------------------------------------------------------------------
412
+
413
+ export type MigrationListener = (event: MigrationEvent) => void;
414
+
415
+ /**
416
+ * Probes for better transports and triggers mid-session migration
417
+ * (spec FR-4.3, spec section 3).
418
+ *
419
+ * "Transport migration is invisible to the application."
420
+ */
421
+ export class TransportMigrator {
422
+ private readonly _probeIntervalMs: number;
423
+ private _currentTransport: FallbackTransportType;
424
+ private readonly _listeners: MigrationListener[] = [];
425
+
426
+ constructor(probeIntervalMs: number, currentTransport: FallbackTransportType) {
427
+ this._probeIntervalMs = probeIntervalMs;
428
+ this._currentTransport = currentTransport;
429
+ }
430
+
431
+ get probeIntervalMs(): number {
432
+ return this._probeIntervalMs;
433
+ }
434
+
435
+ get currentTransport(): FallbackTransportType {
436
+ return this._currentTransport;
437
+ }
438
+
439
+ /** Update the active transport after a successful migration. */
440
+ setCurrentTransport(transport: FallbackTransportType): void {
441
+ this._currentTransport = transport;
442
+ }
443
+
444
+ onMigration(listener: MigrationListener): void {
445
+ this._listeners.push(listener);
446
+ }
447
+
448
+ /** Get the list of transports to probe (those with better priority). */
449
+ transportsToProbe(): FallbackTransportType[] {
450
+ const currentPriority = transportPriority(this._currentTransport);
451
+ return allTransportsInOrder().filter((t) => transportPriority(t) < currentPriority);
452
+ }
453
+
454
+ /** Report that a probe found a better transport. Emits a migration event. */
455
+ reportBetterTransport(betterTransport: FallbackTransportType): void {
456
+ const betterPriority = transportPriority(betterTransport);
457
+ const currentPriority = transportPriority(this._currentTransport);
458
+
459
+ if (betterPriority >= currentPriority) {
460
+ throw new CairnError(
461
+ 'TRANSPORT',
462
+ `proposed transport ${transportDisplayName(betterTransport)} (priority ${betterPriority}) is not better than current ${transportDisplayName(this._currentTransport)} (priority ${currentPriority})`,
463
+ );
464
+ }
465
+
466
+ const event: MigrationEvent = {
467
+ from: this._currentTransport,
468
+ to: betterTransport,
469
+ };
470
+
471
+ for (const listener of this._listeners) {
472
+ listener(event);
473
+ }
474
+ }
475
+ }
@@ -0,0 +1,46 @@
1
+ // Transport module — libp2p integration, fallback chain, NAT (task 035)
2
+
3
+ export type { FallbackTransportType } from './fallback.js';
4
+ export {
5
+ transportPriority,
6
+ transportDisplayName,
7
+ isTier0Available,
8
+ allTransportsInOrder,
9
+ DEFAULT_TRANSPORT_TIMEOUT_MS,
10
+ FallbackChain,
11
+ defaultConnectionQuality,
12
+ defaultQualityThresholds,
13
+ ConnectionQualityMonitor,
14
+ TransportMigrator,
15
+ } from './fallback.js';
16
+ export type {
17
+ TransportAttempt,
18
+ TransportAttemptResult,
19
+ ConnectionQuality,
20
+ QualityThresholds,
21
+ DegradationReason,
22
+ DegradationEvent,
23
+ DegradationListener,
24
+ MigrationEvent,
25
+ MigrationListener,
26
+ } from './fallback.js';
27
+
28
+ export type { NatType, NetworkInfo, StunMappedAddress, StunServerConfig } from './nat.js';
29
+ export {
30
+ defaultNetworkInfo,
31
+ buildBindingRequest,
32
+ parseBindingResponse,
33
+ classifyNat,
34
+ DEFAULT_STUN_SERVERS,
35
+ NatDetector,
36
+ } from './nat.js';
37
+
38
+ export type { TransportConfig, CreateNodeOptions } from './libp2p-node.js';
39
+ export {
40
+ defaultTransportConfig,
41
+ isNodeEnvironment,
42
+ isBrowserEnvironment,
43
+ createCairnNode,
44
+ BROWSER_TRANSPORT_CHAIN,
45
+ NODEJS_TRANSPORT_CHAIN,
46
+ } from './libp2p-node.js';
@@ -0,0 +1,158 @@
1
+ import type { Libp2p } from 'libp2p';
2
+
3
+ import type { FallbackTransportType } from './fallback.js';
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Transport config
7
+ // ---------------------------------------------------------------------------
8
+
9
+ /** Per-transport enable/disable flags and timeout settings. */
10
+ export interface TransportConfig {
11
+ /** Enable QUIC v1 — priority 1 in the fallback chain. (Node.js only) */
12
+ quicEnabled: boolean;
13
+ /** Enable TCP — priority 3 in the fallback chain. (Node.js only) */
14
+ tcpEnabled: boolean;
15
+ /** Enable WebSocket over TLS — priority 6 in the fallback chain. */
16
+ websocketEnabled: boolean;
17
+ /** Enable WebTransport over HTTP/3 — priority 7 in the fallback chain. */
18
+ webtransportEnabled: boolean;
19
+ /** Enable WebRTC — browser environment. */
20
+ webrtcEnabled: boolean;
21
+ /** Enable Circuit Relay v2 — priority 8. */
22
+ circuitRelayEnabled: boolean;
23
+ /** Per-transport connection timeout in ms. */
24
+ perTransportTimeoutMs: number;
25
+ /** STUN server URLs for NAT detection. */
26
+ stunServers: string[];
27
+ /** TURN server URLs (enables priorities 4-5). */
28
+ turnServers: string[];
29
+ }
30
+
31
+ /** Default transport configuration. */
32
+ export function defaultTransportConfig(): TransportConfig {
33
+ return {
34
+ quicEnabled: true,
35
+ tcpEnabled: true,
36
+ websocketEnabled: true,
37
+ webtransportEnabled: true,
38
+ webrtcEnabled: true,
39
+ circuitRelayEnabled: true,
40
+ perTransportTimeoutMs: 10_000,
41
+ stunServers: [
42
+ 'stun:stun.l.google.com:19302',
43
+ 'stun:stun1.l.google.com:19302',
44
+ ],
45
+ turnServers: [],
46
+ };
47
+ }
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // Environment detection
51
+ // ---------------------------------------------------------------------------
52
+
53
+ /** Detect whether we are running in a Node.js environment. */
54
+ export function isNodeEnvironment(): boolean {
55
+ return (
56
+ typeof globalThis.process !== 'undefined' &&
57
+ typeof globalThis.process.versions !== 'undefined' &&
58
+ typeof globalThis.process.versions.node !== 'undefined'
59
+ );
60
+ }
61
+
62
+ /** Detect whether we are running in a browser environment. */
63
+ export function isBrowserEnvironment(): boolean {
64
+ return typeof globalThis.window !== 'undefined' && typeof globalThis.document !== 'undefined';
65
+ }
66
+
67
+ // ---------------------------------------------------------------------------
68
+ // libp2p node creation
69
+ // ---------------------------------------------------------------------------
70
+
71
+ /** Options for creating a cairn libp2p node. */
72
+ export interface CreateNodeOptions {
73
+ config?: Partial<TransportConfig>;
74
+ }
75
+
76
+ /**
77
+ * Create a libp2p node with environment-conditional transports.
78
+ *
79
+ * - Node.js: TCP, WebSocket, Circuit Relay v2 (yamux + noise)
80
+ * - Browser: WebRTC, WebSocket, WebTransport, Circuit Relay v2 (yamux + noise)
81
+ *
82
+ * Uses dynamic `import()` so browser bundles don't include Node.js-only packages.
83
+ */
84
+ export async function createCairnNode(options?: CreateNodeOptions): Promise<Libp2p> {
85
+ const config = { ...defaultTransportConfig(), ...options?.config };
86
+ const { createLibp2p } = await import('libp2p');
87
+ const { yamux } = await import('@libp2p/yamux');
88
+ const { noise } = await import('@chainsafe/libp2p-noise');
89
+
90
+ const transports: unknown[] = [];
91
+
92
+ if (isNodeEnvironment()) {
93
+ // Node.js transports
94
+ if (config.tcpEnabled) {
95
+ const { tcp } = await import('@libp2p/tcp');
96
+ transports.push(tcp());
97
+ }
98
+ if (config.websocketEnabled) {
99
+ const { webSockets } = await import('@libp2p/websockets');
100
+ transports.push(webSockets());
101
+ }
102
+ } else {
103
+ // Browser transports
104
+ if (config.webrtcEnabled) {
105
+ const { webRTC } = await import('@libp2p/webrtc');
106
+ transports.push(webRTC());
107
+ }
108
+ if (config.websocketEnabled) {
109
+ const { webSockets } = await import('@libp2p/websockets');
110
+ transports.push(webSockets());
111
+ }
112
+ if (config.webtransportEnabled) {
113
+ const { webTransport } = await import('@libp2p/webtransport');
114
+ transports.push(webTransport());
115
+ }
116
+ }
117
+
118
+ if (config.circuitRelayEnabled) {
119
+ const { circuitRelayTransport } = await import('@libp2p/circuit-relay-v2');
120
+ transports.push(circuitRelayTransport());
121
+ }
122
+
123
+ const node = await createLibp2p({
124
+ transports: transports as any[],
125
+ streamMuxers: [yamux()],
126
+ connectionEncrypters: [noise()],
127
+ });
128
+
129
+ return node;
130
+ }
131
+
132
+ // ---------------------------------------------------------------------------
133
+ // Browser transport chain (3 levels)
134
+ // ---------------------------------------------------------------------------
135
+
136
+ /** Browser transport chain: WebRTC -> WebSocket -> WebTransport. */
137
+ export const BROWSER_TRANSPORT_CHAIN: FallbackTransportType[] = [
138
+ 'quic', // WebRTC direct maps conceptually to the "best effort direct" slot
139
+ 'websocket-tls',
140
+ 'webtransport',
141
+ ];
142
+
143
+ // ---------------------------------------------------------------------------
144
+ // Node.js transport chain (9 levels)
145
+ // ---------------------------------------------------------------------------
146
+
147
+ /** Node.js transport chain: full 9-level priority. */
148
+ export const NODEJS_TRANSPORT_CHAIN: FallbackTransportType[] = [
149
+ 'quic',
150
+ 'stun-udp',
151
+ 'tcp',
152
+ 'turn-udp',
153
+ 'turn-tcp',
154
+ 'websocket-tls',
155
+ 'webtransport',
156
+ 'circuit-relay-v2',
157
+ 'https-long-polling',
158
+ ];