@torrent-tv/proxy 2.89.7 → 2.89.8
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/.claude/commands/opsx/apply.md +155 -155
- package/.claude/commands/opsx/archive.md +160 -160
- package/.claude/commands/opsx/explore.md +174 -174
- package/.claude/commands/opsx/propose.md +109 -109
- package/.claude/commands/opsx/sync.md +143 -143
- package/.claude/skills/openspec-apply-change/SKILL.md +159 -159
- package/.claude/skills/openspec-archive-change/SKILL.md +117 -117
- package/.claude/skills/openspec-explore/SKILL.md +289 -289
- package/.claude/skills/openspec-propose/SKILL.md +113 -113
- package/.claude/skills/openspec-sync-specs/SKILL.md +147 -147
- package/.dockerignore +5 -5
- package/AGENTS.md +358 -338
- package/CHANGELOG.md +2340 -2335
- package/Dockerfile +30 -30
- package/LICENSE +16 -16
- package/README.md +369 -369
- package/assets/calibration/NOTICE.md +100 -100
- package/assets/calibration/audio/NOTICE.md +19 -19
- package/assets/calibration/audio/manifest.json +98 -98
- package/assets/diagnostics/sctpstate.gdb +31 -31
- package/bin/cli.js +684 -684
- package/bin/thread-pool.js +30 -30
- package/biome.json +412 -412
- package/docs/browser-proxy-contract.md +155 -155
- package/docs/container-architecture.md +273 -273
- package/docs/download-architecture.md +261 -261
- package/docs/encode-architecture.md +765 -765
- package/docs/encode-run-state.md +96 -96
- package/docs/logs.md +97 -97
- package/docs/playback-start-forecast.md +113 -113
- package/docs/socks5-egress-research.md +114 -114
- package/docs/storage-architecture.md +273 -273
- package/knip.json +14 -14
- package/openspec/changes/adaptive-bitrate/design.md +92 -92
- package/openspec/changes/adaptive-bitrate/proposal.md +56 -56
- package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +51 -51
- package/openspec/changes/adaptive-bitrate/tasks.md +40 -40
- package/openspec/changes/binary-distribution/.openspec.yaml +2 -2
- package/openspec/changes/binary-distribution/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/design.md +81 -81
- package/openspec/changes/chunked-request-bodies/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +30 -30
- package/openspec/changes/chunked-request-bodies/tasks.md +40 -40
- package/openspec/changes/cold-start/design.md +98 -98
- package/openspec/changes/cold-start/proposal.md +62 -62
- package/openspec/changes/cold-start/specs/cold-start/spec.md +47 -47
- package/openspec/changes/cold-start/tasks.md +48 -48
- package/openspec/changes/disk-cap/.openspec.yaml +2 -2
- package/openspec/changes/disk-cap/proposal.md +37 -37
- package/openspec/changes/disk-cap/specs/disk-cap/spec.md +25 -25
- package/openspec/changes/disk-cap/tasks.md +19 -19
- package/openspec/changes/proxy-observability/.openspec.yaml +2 -2
- package/openspec/changes/proxy-observability/design.md +38 -38
- package/openspec/changes/proxy-observability/proposal.md +49 -49
- package/openspec/changes/proxy-observability/specs/observability/spec.md +33 -33
- package/openspec/changes/proxy-observability/tasks.md +21 -21
- package/openspec/changes/seek-debounce/design.md +118 -118
- package/openspec/changes/seek-debounce/proposal.md +59 -59
- package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +41 -41
- package/openspec/changes/seek-debounce/tasks.md +33 -33
- package/openspec/changes/subtitle-language/.openspec.yaml +2 -2
- package/openspec/changes/subtitle-language/proposal.md +50 -50
- package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +34 -34
- package/openspec/changes/subtitle-language/tasks.md +26 -26
- package/openspec/changes/track-selection/.openspec.yaml +2 -2
- package/openspec/changes/track-selection/design.md +47 -47
- package/openspec/changes/track-selection/proposal.md +45 -45
- package/openspec/changes/track-selection/specs/track-selection/spec.md +41 -41
- package/openspec/changes/track-selection/tasks.md +20 -20
- package/openspec/changes/transcode-quality/.openspec.yaml +2 -2
- package/openspec/changes/transcode-quality/proposal.md +51 -51
- package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +139 -139
- package/openspec/changes/transcode-quality/tasks.md +63 -63
- package/openspec/config.yaml +38 -38
- package/package.json +51 -50
- package/public/.well-known/appspecific/com.chrome.devtools.json +1 -1
- package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
- package/research/double-spawn-2026-09-10.md +171 -171
- package/research/handover-2026-09-13.md +209 -209
- package/research/handover-reader-claims-removal-2026-09-12.md +166 -166
- package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +175 -175
- package/research/preset-benchmark-size-scaling-2026-08-22.md +55 -55
- package/research/priority-map-is-the-truth-2026-09-12.md +225 -225
- package/routes/api/client-logs/post.js +86 -86
- package/routes/api/delivery-sink/get.js +72 -72
- package/routes/api/link-probe/get.js +51 -51
- package/routes/api/playback-plan/audio-tracks/post.js +29 -29
- package/routes/api/playback-plan/post.js +53 -53
- package/routes/api/sources/files/get.js +106 -106
- package/routes/api/sources/post.js +36 -36
- package/routes/api/sources/stats/get.js +132 -132
- package/routes/api/sources/warm/post.js +217 -217
- package/routes/api/subtitles/get.js +230 -230
- package/routes/api/transcode-sessions/fragment-far/post.js +60 -60
- package/routes/api/transcode-sessions/net-report/post.js +138 -138
- package/routes/api/transcode-sessions/post.js +178 -178
- package/routes/api/transcode-sessions/progress/get.js +27 -27
- package/routes/api/transcode-sessions/release/post.js +41 -41
- package/routes/api/transcode-sessions/seek/post.js +63 -63
- package/routes/health/get.js +13 -13
- package/routes/healthz/get.js +13 -13
- package/routes/stream/get.js +416 -416
- package/routes/transcode/audio-file/get.js +64 -64
- package/routes/transcode/audio-warm/get.js +95 -95
- package/routes/transcode/session-file/get.js +52 -52
- package/routes/transcode/variant-file/get.js +95 -95
- package/routes/transcode/variant-warm/get.js +84 -84
- package/scripts/build-audio-samples.mjs +31 -31
- package/scripts/render-run-graph.js +173 -173
- package/server.js +748 -748
- package/services/encode/CoverageMap.js +428 -428
- package/services/encode/CushionReport.js +345 -345
- package/services/encode/EncodeAdmission.js +376 -376
- package/services/encode/EncodeOrchestrator.js +1192 -1192
- package/services/encode/EncodePlan.js +1368 -1368
- package/services/encode/EncodeRun.js +36 -12
- package/services/encode/Encoder.js +142 -142
- package/services/encode/EncoderSelection.js +38 -38
- package/services/encode/LocalObservations.js +359 -359
- package/services/encode/NvencEncoder.js +50 -50
- package/services/encode/OutputOpening.js +784 -784
- package/services/encode/OutputTimes.js +579 -579
- package/services/encode/QsvEncoder.js +58 -58
- package/services/encode/Renditions.js +2283 -2283
- package/services/encode/RunClock.js +87 -87
- package/services/encode/RunProgress.js +79 -79
- package/services/encode/SegmentDemand.js +95 -95
- package/services/encode/SoftwareEncoder.js +121 -121
- package/services/encode/V4l2m2mEncoder.js +53 -53
- package/services/encode/VaapiEncoder.js +66 -66
- package/services/encode/args.js +418 -418
- package/services/encode/audio-calibration.js +67 -67
- package/services/encode/audio-work-rate.js +15 -15
- package/services/encode/available-share.js +89 -89
- package/services/encode/calibration.js +361 -361
- package/services/encode/contention.js +128 -128
- package/services/encode/decode-cost-fit.js +363 -363
- package/services/encode/encode-exit.js +116 -116
- package/services/encode/encode-run-state.js +551 -551
- package/services/encode/encoder-readings.js +43 -43
- package/services/encode/fingerprint.js +322 -322
- package/services/encode/host-load.js +174 -174
- package/services/encode/hwaccel.js +1894 -1894
- package/services/encode/index.js +9 -9
- package/services/encode/learned-median.js +90 -90
- package/services/encode/output/EncodedOutput.js +57 -57
- package/services/encode/output/Output.js +131 -131
- package/services/encode/output/OutputCatalog.js +414 -414
- package/services/encode/output/OutputSpec.js +390 -390
- package/services/encode/output/Timeline.js +308 -308
- package/services/encode/output/cut-grid.js +107 -107
- package/services/encode/output/index.js +24 -24
- package/services/encode/output/ladder.js +32 -32
- package/services/encode/output/playlists.js +284 -284
- package/services/encode/output/rates.js +125 -125
- package/services/encode/output-key-format.js +21 -21
- package/services/encode/quality/EncodeCost.js +1043 -1043
- package/services/encode/quality/HostLoad.js +525 -525
- package/services/encode/quality/HostTimings.js +249 -249
- package/services/encode/quality/OutputQualityState.js +24 -24
- package/services/encode/quality/QualityController.js +758 -758
- package/services/encode/quality/QualityOffer.js +416 -416
- package/services/encode/quality/drain-threat.js +47 -47
- package/services/encode/quality/link-budget.js +337 -337
- package/services/encode/quality/output-format.js +319 -319
- package/services/encode/quality/serving-output.js +216 -216
- package/services/encode/quality/visible-rung.js +57 -57
- package/services/encode/run-budget.js +97 -97
- package/services/encode/run-command.js +757 -757
- package/services/encode/run-costs.js +155 -155
- package/services/encode/run-inputs.js +53 -53
- package/services/encode/segment-formats/fmp4.js +476 -476
- package/services/encode/segment-formats/init-compat.js +324 -324
- package/services/encode/segment-formats/mpegts.js +134 -134
- package/services/encode/start-stop-cost.js +178 -178
- package/services/encode/throughput.js +97 -97
- package/services/encode/torrent-cost.js +88 -88
- package/services/identity/installation-id.js +38 -38
- package/services/media/ContainerOrchestrator.js +202 -202
- package/services/media/KeyframeTables.js +356 -356
- package/services/media/SourceFile.js +298 -298
- package/services/media/SubtitleCues.js +592 -592
- package/services/media/SubtitleOrchestrator.js +167 -167
- package/services/media/audio-inventory.js +299 -299
- package/services/media/container/AviContainer.js +268 -268
- package/services/media/container/Container.js +632 -632
- package/services/media/container/ContainerFactory.js +38 -38
- package/services/media/container/KeyframeTable.js +272 -272
- package/services/media/container/MatroskaContainer.js +1031 -1031
- package/services/media/container/Mp4Container.js +1354 -1354
- package/services/media/container/SubtitleFileContainer.js +339 -339
- package/services/media/container/ebml-reader.js +159 -159
- package/services/media/container/ebml-stream.js +174 -174
- package/services/media/container/matroska-clusters.js +1165 -1165
- package/services/media/container/unavailable.js +77 -77
- package/services/media/ffmpeg-banner.js +260 -260
- package/services/media/keyframe-probe.js +157 -157
- package/services/media/media-info-probe.js +93 -93
- package/services/media/playback-planner.js +931 -931
- package/services/media/tracks/AudioTrack.js +143 -143
- package/services/media/tracks/ContainerTrack.js +84 -84
- package/services/media/tracks/ImageSubtitleTrack.js +19 -19
- package/services/media/tracks/SubtitleTrack.js +38 -38
- package/services/media/tracks/TextSubtitleTrack.js +295 -295
- package/services/media/tracks/VideoTrack.js +33 -33
- package/services/media/tracks/audio-bound.js +233 -233
- package/services/media/tracks/index.js +15 -15
- package/services/media/tracks/language-detect.js +174 -174
- package/services/server/OutputLifecycle.js +418 -418
- package/services/server/SegmentServing.js +1152 -1152
- package/services/server/ViewerRequests.js +785 -785
- package/services/server/WaitLedger.js +142 -142
- package/services/server/controllers/PlaybackController.js +40 -40
- package/services/server/controllers/SubtitleController.js +109 -109
- package/services/server/http-responses.js +60 -60
- package/services/server/transcode-session-files.js +325 -325
- package/services/server/wire-outputs.js +1029 -1029
- package/services/storage/Diagnostics.js +202 -202
- package/services/storage/MachineBudget.js +235 -235
- package/services/storage/allowance.js +107 -107
- package/services/storage/core-dumps.js +106 -106
- package/services/storage/files/CompletedFiles.js +391 -391
- package/services/storage/files/piece-from-whole-file.js +118 -118
- package/services/storage/free.js +71 -71
- package/services/storage/keep.js +48 -48
- package/services/storage/machine-memory.js +56 -56
- package/services/storage/memory-report.js +575 -575
- package/services/storage/piece-store/piece-disk-store.js +539 -539
- package/services/storage/piece-store/piece-lru.js +495 -495
- package/services/storage/piece-store/shared-piece-store.js +1937 -1937
- package/services/storage/returns.js +103 -103
- package/services/storage/segment-store/SegmentStore.js +1294 -1294
- package/services/storage/wire.js +175 -175
- package/services/torrent/Contents.js +382 -382
- package/services/torrent/demand/DemandRegister.js +202 -202
- package/services/torrent/demand/Urgency.js +137 -137
- package/services/torrent/demand/Window.js +118 -118
- package/services/torrent/demand/index.js +8 -8
- package/services/torrent/demand/pieces.js +140 -140
- package/services/torrent/download/SwarmSelection.js +386 -386
- package/services/torrent/download/registry.js +102 -102
- package/services/torrent/download/withdraw-claim.js +80 -80
- package/services/torrent/episode-naming.js +182 -182
- package/services/torrent/files.js +455 -455
- package/services/torrent/naming.js +619 -619
- package/services/torrent/piece-store-of.js +29 -29
- package/services/torrent/supply-margin.js +264 -264
- package/services/torrent/torrent-pool.js +2688 -2688
- package/services/torrent/worker/background-fill.js +213 -213
- package/services/torrent/worker/channel.js +271 -271
- package/services/torrent/worker/client.js +836 -836
- package/services/torrent/worker/fastest-wires.js +342 -342
- package/services/torrent/worker/handle-state.js +23 -23
- package/services/torrent/worker/held-bytes.js +231 -231
- package/services/torrent/worker/install-webrtc-shim.js +32 -32
- package/services/torrent/worker/piece-reader.js +1078 -1078
- package/services/torrent/worker/pool-adapter.js +371 -371
- package/services/torrent/worker/protocol.js +184 -184
- package/services/torrent/worker/resume-warm.js +107 -107
- package/services/torrent/worker/webrtc-shim.js +219 -219
- package/services/torrent/worker/worker.js +1137 -1137
- package/services/transport/data-channel-handler.js +1373 -1373
- package/services/transport/delivery-probe.js +842 -842
- package/services/transport/health-collector.js +53 -53
- package/services/transport/nat-classifier.js +231 -231
- package/services/transport/packet-witness.js +808 -808
- package/services/transport/port-mapper.js +235 -235
- package/services/transport/registry-api.js +81 -81
- package/services/transport/tunnel-client.js +447 -447
- package/services/transport/usrsctp-state.js +175 -175
- package/services/transport/webrtc-manager.js +484 -484
- package/services/viewer/Assignments.js +340 -340
- package/services/viewer/PriorityMap.js +421 -421
- package/services/viewer/PriorityOrchestrator.js +325 -325
- package/services/viewer/Viewer.js +691 -691
- package/services/viewer/Viewers.js +368 -368
- package/services/viewer/active-output.js +37 -37
- package/services/viewer/audio-start.js +46 -46
- package/services/viewer/buffer-trend.js +81 -81
- package/services/viewer/choices.js +647 -647
- package/services/viewer/media-time.js +355 -355
- package/services/viewer/playback-readiness.js +635 -635
- package/services/viewer/positions.js +126 -126
- package/services/viewer/report-intake.js +46 -46
- package/store/source-registry.js +71 -71
- package/test/asset-paths.test.js +28 -28
- package/test/audio-buffer-continuity.test.js +77 -77
- package/test/audio-calibration.test.js +23 -23
- package/test/audio-inventory.test.js +212 -212
- package/test/audio-track-bitrate.test.js +40 -40
- package/test/auto-quality-step.test.js +762 -762
- package/test/available-share.test.js +76 -76
- package/test/background-fill.test.js +190 -190
- package/test/client-log-file.test.js +110 -110
- package/test/coalesce.test.js +39 -39
- package/test/completed-files.test.js +115 -115
- package/test/concurrent-cost.test.js +148 -148
- package/test/container-media-info.test.js +228 -228
- package/test/contention.test.js +76 -76
- package/test/core-dumps.test.js +57 -57
- package/test/coverage-follows-the-disk.test.js +233 -228
- package/test/coverage-map.test.js +195 -195
- package/test/cue-push-carries-what-the-walk-found.test.js +58 -58
- package/test/cut-times-timeline.test.js +65 -65
- package/test/cuts-follow-published-grid.test.js +109 -109
- package/test/data-channel-frame.test.js +63 -63
- package/test/data-channel-viewer-lifecycle.test.js +92 -92
- package/test/dead-torrent-handle.test.js +52 -52
- package/test/declared-rates.test.js +116 -116
- package/test/declared-tracks.test.js +43 -43
- package/test/decode-cost-fit.test.js +202 -202
- package/test/decode-cost.test.js +475 -475
- package/test/delivery-probe.test.js +393 -393
- package/test/demand-register.test.js +206 -206
- package/test/dht-bootstrap.test.js +72 -72
- package/test/drain-threat.test.js +65 -65
- package/test/duplicate-add.test.js +48 -48
- package/test/encode-admission.test.js +146 -146
- package/test/encode-cost.test.js +275 -275
- package/test/encode-exit.test.js +111 -111
- package/test/encode-orchestrator.test.js +426 -426
- package/test/encode-plan-viewers.test.js +725 -725
- package/test/encode-plan.test.js +545 -545
- package/test/encode-run-state.test.js +314 -314
- package/test/encode-run.test.js +20 -0
- package/test/encode-slope.test.js +46 -46
- package/test/encoded-output.test.js +39 -39
- package/test/encoder-kinds.test.js +122 -122
- package/test/encoder-readings.test.js +68 -68
- package/test/episode-naming.test.js +131 -131
- package/test/every-claimant-is-told-its-share.test.js +152 -152
- package/test/failed-encoder-still-serves-what-exists.test.js +354 -354
- package/test/failed-start-breaker.test.js +202 -202
- package/test/fastest-wires.test.js +143 -143
- package/test/ffmpeg-stream-counts.test.js +78 -78
- package/test/file-edges.test.js +191 -191
- package/test/fixtures/audio-frame-boundary.json +1 -1
- package/test/fmp4-self-contained.test.js +80 -80
- package/test/fmp4-timestamps.test.js +140 -140
- package/test/fmp4-track-completeness.test.js +41 -41
- package/test/format-keyed-outputs.test.js +78 -78
- package/test/health-metrics.test.js +36 -38
- package/test/held-read-ownership.test.js +77 -77
- package/test/held-request-width.test.js +157 -157
- package/test/helpers/encode-run.js +248 -240
- package/test/helpers/held-file.js +77 -77
- package/test/helpers/manager.js +62 -62
- package/test/helpers/matroska-file.js +296 -296
- package/test/helpers/output-spec.js +63 -63
- package/test/host-load.test.js +68 -68
- package/test/index-accuracy-is-about-the-file.test.js +128 -128
- package/test/init-compat.test.js +219 -219
- package/test/input-lost-quiets-the-plan.test.js +261 -261
- package/test/input-unavailable.test.js +48 -48
- package/test/installation-id.test.js +39 -39
- package/test/keeping-period.test.js +83 -83
- package/test/keyframe-index-accuracy.test.js +188 -188
- package/test/keyframe-table-budget.test.js +123 -123
- package/test/keyframes-belong-to-the-file.test.js +249 -249
- package/test/learned-median.test.js +37 -37
- package/test/letting-go-asks-who-is-watching.test.js +191 -191
- package/test/limit-row-by-area.test.js +60 -60
- package/test/link-budget.test.js +111 -111
- package/test/link-probe.test.js +56 -56
- package/test/local-encoding-data.test.js +273 -273
- package/test/logger-repeats.test.js +134 -134
- package/test/look-ahead-coverage.test.js +56 -56
- package/test/manager-surface.test.js +291 -291
- package/test/matroska-cluster-discovery.test.js +323 -323
- package/test/matroska-cues-track.test.js +192 -192
- package/test/media-time.test.js +62 -62
- package/test/memory-budget.test.js +390 -390
- package/test/move-cost.test.js +216 -216
- package/test/mp4-subtitles.test.js +173 -173
- package/test/one-authority.test.js +285 -285
- package/test/one-budget.test.js +149 -149
- package/test/one-output-one-session.test.js +273 -273
- package/test/one-resource-divided.test.js +185 -185
- package/test/one-serving-output-per-height.test.js +458 -458
- package/test/orchestrator-wired.test.js +207 -207
- package/test/output-catalog.test.js +39 -39
- package/test/output-shape.test.js +21 -21
- package/test/output-spec.test.js +251 -251
- package/test/output-speed.test.js +92 -87
- package/test/outputs-being-prepared.test.js +40 -40
- package/test/packet-witness-ring.test.js +236 -236
- package/test/packet-witness.test.js +148 -148
- package/test/piece-disk-store.test.js +401 -401
- package/test/piece-from-whole-file.test.js +129 -129
- package/test/piece-lru.test.js +319 -319
- package/test/piece-reader.test.js +162 -162
- package/test/piece-selection-offset.test.js +85 -85
- package/test/piece-store-eviction.test.js +595 -595
- package/test/piece-store-never-refuses.test.js +153 -153
- package/test/piece-store-reservations.test.js +403 -403
- package/test/piece-store-slow-disk.test.js +159 -159
- package/test/piece-tail.test.js +97 -97
- package/test/plan-host-timings.test.js +68 -68
- package/test/plan-is-steady.test.js +225 -225
- package/test/playback-readiness.test.js +518 -518
- package/test/playback-start-regressions.test.js +101 -101
- package/test/priority-map-download.test.js +174 -174
- package/test/priority-map-emptied.test.js +204 -204
- package/test/priority-map-per-output.test.js +215 -215
- package/test/priority-map.test.js +163 -163
- package/test/probe-wedge-certainty.test.js +60 -60
- package/test/produced-copy-choice.test.js +213 -213
- package/test/published-timeline.test.js +83 -83
- package/test/quality-offer.test.js +160 -160
- package/test/quality-variants.test.js +1274 -1270
- package/test/rate-control-in-the-output.test.js +181 -180
- package/test/read-survives-withdrawal.test.js +164 -164
- package/test/read-window.test.js +365 -365
- package/test/reads-not-yet-arrived.test.js +218 -218
- package/test/resume-warm.test.js +39 -39
- package/test/run-graph-drift.test.js +37 -37
- package/test/run-inputs.test.js +80 -80
- package/test/run-intervals.test.js +100 -100
- package/test/run-position-follows-published-grid.test.js +89 -89
- package/test/run-progress.test.js +64 -64
- package/test/same-height-switch.test.js +663 -663
- package/test/seek-frees-the-viewers-requests.test.js +217 -217
- package/test/seek-landing.test.js +109 -109
- package/test/seek-target-not-superseded.test.js +195 -195
- package/test/segment-closed-on-both-branches.test.js +114 -114
- package/test/segment-cut-times.test.js +63 -63
- package/test/segment-demand.test.js +80 -80
- package/test/segment-hold-supersede.test.js +91 -91
- package/test/segment-judged-by-its-own-header.test.js +222 -222
- package/test/segment-serve-wiring.test.js +481 -481
- package/test/segment-store-eviction.test.js +247 -247
- package/test/segment-store.test.js +352 -352
- package/test/segment-true-start.test.js +86 -86
- package/test/segments-are-shared.test.js +176 -176
- package/test/send-chunk-bytes.test.js +86 -86
- package/test/serving-existing-output.test.js +111 -111
- package/test/serving-output.test.js +102 -102
- package/test/shared-piece-store.test.js +310 -310
- package/test/sidecar-naming.test.js +142 -142
- package/test/soundtrack-rate-bound.test.js +289 -289
- package/test/source-file.test.js +133 -133
- package/test/source-files-route.test.js +113 -113
- package/test/source-registry.test.js +18 -18
- package/test/split-a-stretch.test.js +139 -139
- package/test/stale-request-after-seek.test.js +158 -158
- package/test/startup-readings.test.js +124 -124
- package/test/stream-route.test.js +237 -237
- package/test/subtitle-cue-framing.test.js +200 -200
- package/test/subtitle-cue-source.test.js +102 -102
- package/test/subtitle-cue-walk.test.js +385 -385
- package/test/subtitle-cursor.test.js +63 -63
- package/test/subtitle-defaults.test.js +118 -118
- package/test/subtitle-language.test.js +252 -252
- package/test/subtitle-metadata.test.js +15 -15
- package/test/subtitle-plan-waits.test.js +125 -125
- package/test/subtitle-track-numbering.test.js +391 -391
- package/test/supply-margin.test.js +190 -190
- package/test/swarm-follows-readers.test.js +174 -174
- package/test/swarm-reach.test.js +130 -130
- package/test/swarm-selection.test.js +220 -220
- package/test/tail-duplication.test.js +167 -167
- package/test/the-channel-has-both-ends.test.js +91 -91
- package/test/the-torrent-thread-serves-bytes.test.js +107 -107
- package/test/thread-pool.test.js +53 -53
- package/test/timeline.test.js +84 -84
- package/test/torrent-contents.test.js +235 -235
- package/test/torrent-cost.test.js +71 -71
- package/test/torrent-files.test.js +221 -221
- package/test/torrent-naming.test.js +255 -255
- package/test/tracks-begin-together.test.js +209 -209
- package/test/tunnel-renewal.test.js +167 -167
- package/test/two-viewers-one-picture.test.js +508 -508
- package/test/upload-hurry.test.js +186 -186
- package/test/usrsctp-state.test.js +131 -131
- package/test/video-facts.test.js +102 -102
- package/test/viewer-assignments.test.js +143 -143
- package/test/viewer-outputs.test.js +285 -285
- package/test/viewer-presence.test.js +116 -116
- package/test/viewer-requests-admission.test.js +92 -92
- package/test/viewer-starts-on-map.test.js +27 -27
- package/test/viewer-states.test.js +192 -189
- package/test/viewer-subtitle-subscription.test.js +96 -96
- package/test/viewer.test.js +139 -139
- package/test/viewing-generations.test.js +390 -390
- package/test/visible-rung.test.js +37 -37
- package/test/wait-ledger.test.js +100 -100
- package/test/webrtc-shim.test.js +149 -149
- package/test/wedge-certainty.test.js +131 -131
- package/test/whole-file-held-read.test.js +30 -30
- package/test/withdraw-piece-claim.test.js +212 -212
- package/test/worker-channel.test.js +158 -158
- package/utils/client-log-file.js +151 -151
- package/utils/coalesce.js +41 -41
- package/utils/logger.js +312 -312
- package/utils/parse-range.js +25 -25
- package/utils/perf.js +59 -59
- package/utils/torrent-source-key.js +38 -38
|
@@ -1,235 +1,235 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Automatic port mapping (UPnP IGD / NAT-PMP / PCP) for the proxy.
|
|
3
|
-
*
|
|
4
|
-
* Opens a port on the user's home router so the proxy is reachable from the
|
|
5
|
-
* internet without any manual port forwarding. Uses `@silentbot1/nat-api`
|
|
6
|
-
* (the same library WebTorrent already uses for the torrent port, so this adds
|
|
7
|
-
* no new dependency surface on the host).
|
|
8
|
-
*
|
|
9
|
-
* Strictly best-effort: a router without UPnP/NAT-PMP — or one that declines —
|
|
10
|
-
* is a normal case, not an error. Mapping failure never blocks proxy startup
|
|
11
|
-
* and never throws to the caller. Reachability of the mapped endpoint is
|
|
12
|
-
* verified separately (server-side dial-back probe — a later stage).
|
|
13
|
-
*
|
|
14
|
-
* The mapping is created with a TTL and auto-renewed while the proxy runs
|
|
15
|
-
* (`autoUpdate`), and removed on graceful shutdown via {@link stop}. If the
|
|
16
|
-
* process dies without calling `stop()`, the router drops the mapping when the
|
|
17
|
-
* lease expires.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import NatAPI from "@silentbot1/nat-api";
|
|
21
|
-
import { logger } from "../../utils/logger.js";
|
|
22
|
-
|
|
23
|
-
// nat-api clamps ttl to a 1200 s minimum; it auto-renews at (ttl - 600) s.
|
|
24
|
-
const DEFAULT_TTL_SECONDS = 7200;
|
|
25
|
-
// SSDP discovery can hang on networks with no responding gateway — bound it so
|
|
26
|
-
// startup is never delayed waiting for a router that will not answer.
|
|
27
|
-
const START_TIMEOUT_MS = 10_000;
|
|
28
|
-
// A slow/unreachable router must not hang shutdown; lease expiry is the backstop.
|
|
29
|
-
const STOP_TIMEOUT_MS = 5_000;
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* @param {unknown} error
|
|
33
|
-
* @returns {string}
|
|
34
|
-
*/
|
|
35
|
-
function describeError(error) {
|
|
36
|
-
return error instanceof Error ? error.message : String(error);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Reject after `ms` if `promise` has not settled, so a hung NAT operation
|
|
41
|
-
* cannot block startup or shutdown.
|
|
42
|
-
*
|
|
43
|
-
* @template T
|
|
44
|
-
* @param {Promise<T>} promise
|
|
45
|
-
* @param {number} ms
|
|
46
|
-
* @param {string} label
|
|
47
|
-
* @returns {Promise<T>}
|
|
48
|
-
*/
|
|
49
|
-
function withTimeout(promise, ms, label) {
|
|
50
|
-
return new Promise((resolve, reject) => {
|
|
51
|
-
const timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms} ms`)), ms);
|
|
52
|
-
timer.unref?.();
|
|
53
|
-
promise.then(
|
|
54
|
-
(value) => {
|
|
55
|
-
clearTimeout(timer);
|
|
56
|
-
resolve(value);
|
|
57
|
-
},
|
|
58
|
-
(error) => {
|
|
59
|
-
clearTimeout(timer);
|
|
60
|
-
reject(error);
|
|
61
|
-
}
|
|
62
|
-
);
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* @typedef {object} MappedEndpoint
|
|
68
|
-
* @property {string | null} externalIp - Public IP as seen by NAT-PMP/UPnP, or null if unknown.
|
|
69
|
-
* @property {number} externalPort
|
|
70
|
-
* @property {"TCP" | "UDP"} protocol
|
|
71
|
-
*/
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* @typedef {object} PortMapper
|
|
75
|
-
* @property {() => Promise<void>} start - Create the mapping (best-effort, never throws).
|
|
76
|
-
* @property {() => Promise<void>} stop - Remove the mapping and stop auto-renew (idempotent).
|
|
77
|
-
* @property {() => MappedEndpoint | null} getMappedEndpoint - The active mapping, or null.
|
|
78
|
-
*/
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Create a port mapper for a single local port.
|
|
82
|
-
*
|
|
83
|
-
* @param {object} opts
|
|
84
|
-
* @param {number} opts.port - The local port to expose (used as both public and private port).
|
|
85
|
-
* @param {"TCP" | "UDP"} [opts.protocol] - Protocol to map. Defaults to "TCP" (the HTTP/stream port).
|
|
86
|
-
* @param {string} [opts.description] - Human-readable label shown in the router's port-mapping table.
|
|
87
|
-
* @param {number} [opts.ttlSeconds] - Lease time in seconds. Defaults to {@link DEFAULT_TTL_SECONDS}.
|
|
88
|
-
* @param {number} [opts.portRangeEnd] - When set and > `port`, map the whole
|
|
89
|
-
* contiguous range `[port..portRangeEnd]` (used for the WebRTC UDP range, so
|
|
90
|
-
* whichever port a session binds is reachable). Single port otherwise.
|
|
91
|
-
* @returns {PortMapper}
|
|
92
|
-
*/
|
|
93
|
-
export function createPortMapper({
|
|
94
|
-
port,
|
|
95
|
-
protocol = "TCP",
|
|
96
|
-
description = "torrent-tv proxy",
|
|
97
|
-
ttlSeconds = DEFAULT_TTL_SECONDS,
|
|
98
|
-
portRangeEnd
|
|
99
|
-
} = {}) {
|
|
100
|
-
const rangeEnd = Number.isInteger(portRangeEnd) && portRangeEnd > port ? portRangeEnd : null;
|
|
101
|
-
const portList = rangeEnd
|
|
102
|
-
? Array.from({ length: rangeEnd - port + 1 }, (_, i) => port + i)
|
|
103
|
-
: [port];
|
|
104
|
-
const label = rangeEnd ? `${protocol} ${port}-${rangeEnd}` : `${protocol} ${port}`;
|
|
105
|
-
|
|
106
|
-
/** @type {InstanceType<typeof NatAPI> | null} */
|
|
107
|
-
let nat = null;
|
|
108
|
-
/** @type {MappedEndpoint | null} */
|
|
109
|
-
let mappedEndpoint = null;
|
|
110
|
-
let started = false;
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Destroy the NatAPI instance, swallowing errors. `destroy()` unmaps every
|
|
114
|
-
* open port and clears the auto-renew timers.
|
|
115
|
-
*
|
|
116
|
-
* @param {InstanceType<typeof NatAPI>} instance
|
|
117
|
-
* @returns {Promise<void>}
|
|
118
|
-
*/
|
|
119
|
-
async function safeDestroy(instance, { logRemoval = false } = {}) {
|
|
120
|
-
try {
|
|
121
|
-
await withTimeout(instance.destroy(), STOP_TIMEOUT_MS, "destroy");
|
|
122
|
-
if (logRemoval) {
|
|
123
|
-
logger.info(`port-mapper: removed mapping for ${label}`);
|
|
124
|
-
}
|
|
125
|
-
} catch (error) {
|
|
126
|
-
// Lease expiry (ttl) is the backstop if we cannot unmap cleanly.
|
|
127
|
-
logger.warn(`port-mapper: failed to remove ${label} mapping cleanly: ${describeError(error)}`);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* @returns {Promise<void>}
|
|
133
|
-
*/
|
|
134
|
-
async function start() {
|
|
135
|
-
if (started) {
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
started = true;
|
|
139
|
-
|
|
140
|
-
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
141
|
-
logger.warn(`port-mapper: invalid port ${port}; skipping port mapping`);
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
let instance;
|
|
146
|
-
let mappedCount = 0;
|
|
147
|
-
try {
|
|
148
|
-
instance = new NatAPI({ ttl: ttlSeconds, autoUpdate: true, description });
|
|
149
|
-
// Map every port in the range, best-effort per port (one port failing
|
|
150
|
-
// must not abort the rest). All share the one NatAPI instance, so its
|
|
151
|
-
// auto-renew covers them and a single destroy() unmaps all.
|
|
152
|
-
for (const p of portList) {
|
|
153
|
-
try {
|
|
154
|
-
await withTimeout(
|
|
155
|
-
instance.map({ publicPort: p, privatePort: p, protocol, description, ttl: ttlSeconds }),
|
|
156
|
-
START_TIMEOUT_MS,
|
|
157
|
-
`map ${p}`
|
|
158
|
-
);
|
|
159
|
-
mappedCount++;
|
|
160
|
-
if (mappedCount === 1) {
|
|
161
|
-
// The UPnP SSDP emitter gains one listener per map()/renewal; a
|
|
162
|
-
// 10-port range exceeds Node's default limit of 10 and floods the
|
|
163
|
-
// log with MaxListenersExceededWarning. The client is created
|
|
164
|
-
// lazily by the first map(), so lift the limit right after it.
|
|
165
|
-
instance._upnpClient?.ssdp?.setMaxListeners?.(0);
|
|
166
|
-
}
|
|
167
|
-
} catch (error) {
|
|
168
|
-
logger.warn(`port-mapper: failed to map ${protocol} ${p}: ${describeError(error)}`);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
} catch (error) {
|
|
172
|
-
mappedEndpoint = null;
|
|
173
|
-
logger.warn(`port-mapper: no port mapping available (${describeError(error)}); continuing without it`);
|
|
174
|
-
if (instance) {
|
|
175
|
-
await safeDestroy(instance);
|
|
176
|
-
}
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
if (mappedCount === 0) {
|
|
181
|
-
// No UPnP/NAT-PMP on this router, or it declined. Normal, non-fatal: the
|
|
182
|
-
// proxy still works on LAN and wherever hole punching succeeds.
|
|
183
|
-
mappedEndpoint = null;
|
|
184
|
-
logger.warn(`port-mapper: no ports mapped for ${label}; continuing without it`);
|
|
185
|
-
await safeDestroy(instance);
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Mapping succeeded — keep the instance so its auto-renew timers stay alive
|
|
190
|
-
// and stop() can remove the mappings later.
|
|
191
|
-
nat = instance;
|
|
192
|
-
|
|
193
|
-
// Discover the external IP (best-effort; the mapping is valid without it).
|
|
194
|
-
let externalIp = null;
|
|
195
|
-
try {
|
|
196
|
-
externalIp = await withTimeout(instance.externalIp(), START_TIMEOUT_MS, "externalIp");
|
|
197
|
-
} catch (error) {
|
|
198
|
-
logger.warn(`port-mapper: mapped ${label} but could not read external IP: ${describeError(error)}`);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
mappedEndpoint = { externalIp: externalIp || null, externalPort: port, protocol };
|
|
202
|
-
const counts = portList.length > 1 ? ` (${mappedCount}/${portList.length} ports)` : "";
|
|
203
|
-
if (externalIp) {
|
|
204
|
-
logger.success(
|
|
205
|
-
`port-mapper: mapped ${externalIp} → ${label}${counts} (ttl ${ttlSeconds}s, auto-renew)`
|
|
206
|
-
);
|
|
207
|
-
} else {
|
|
208
|
-
logger.info(
|
|
209
|
-
`port-mapper: mapped ${label}${counts} (external IP unknown; ttl ${ttlSeconds}s, auto-renew)`
|
|
210
|
-
);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* @returns {Promise<void>}
|
|
216
|
-
*/
|
|
217
|
-
async function stop() {
|
|
218
|
-
if (!nat) {
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
const instance = nat;
|
|
222
|
-
nat = null;
|
|
223
|
-
mappedEndpoint = null;
|
|
224
|
-
await safeDestroy(instance, { logRemoval: true });
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* @returns {MappedEndpoint | null}
|
|
229
|
-
*/
|
|
230
|
-
function getMappedEndpoint() {
|
|
231
|
-
return mappedEndpoint;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
return { start, stop, getMappedEndpoint };
|
|
235
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @file Automatic port mapping (UPnP IGD / NAT-PMP / PCP) for the proxy.
|
|
3
|
+
*
|
|
4
|
+
* Opens a port on the user's home router so the proxy is reachable from the
|
|
5
|
+
* internet without any manual port forwarding. Uses `@silentbot1/nat-api`
|
|
6
|
+
* (the same library WebTorrent already uses for the torrent port, so this adds
|
|
7
|
+
* no new dependency surface on the host).
|
|
8
|
+
*
|
|
9
|
+
* Strictly best-effort: a router without UPnP/NAT-PMP — or one that declines —
|
|
10
|
+
* is a normal case, not an error. Mapping failure never blocks proxy startup
|
|
11
|
+
* and never throws to the caller. Reachability of the mapped endpoint is
|
|
12
|
+
* verified separately (server-side dial-back probe — a later stage).
|
|
13
|
+
*
|
|
14
|
+
* The mapping is created with a TTL and auto-renewed while the proxy runs
|
|
15
|
+
* (`autoUpdate`), and removed on graceful shutdown via {@link stop}. If the
|
|
16
|
+
* process dies without calling `stop()`, the router drops the mapping when the
|
|
17
|
+
* lease expires.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import NatAPI from "@silentbot1/nat-api";
|
|
21
|
+
import { logger } from "../../utils/logger.js";
|
|
22
|
+
|
|
23
|
+
// nat-api clamps ttl to a 1200 s minimum; it auto-renews at (ttl - 600) s.
|
|
24
|
+
const DEFAULT_TTL_SECONDS = 7200;
|
|
25
|
+
// SSDP discovery can hang on networks with no responding gateway — bound it so
|
|
26
|
+
// startup is never delayed waiting for a router that will not answer.
|
|
27
|
+
const START_TIMEOUT_MS = 10_000;
|
|
28
|
+
// A slow/unreachable router must not hang shutdown; lease expiry is the backstop.
|
|
29
|
+
const STOP_TIMEOUT_MS = 5_000;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {unknown} error
|
|
33
|
+
* @returns {string}
|
|
34
|
+
*/
|
|
35
|
+
function describeError(error) {
|
|
36
|
+
return error instanceof Error ? error.message : String(error);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Reject after `ms` if `promise` has not settled, so a hung NAT operation
|
|
41
|
+
* cannot block startup or shutdown.
|
|
42
|
+
*
|
|
43
|
+
* @template T
|
|
44
|
+
* @param {Promise<T>} promise
|
|
45
|
+
* @param {number} ms
|
|
46
|
+
* @param {string} label
|
|
47
|
+
* @returns {Promise<T>}
|
|
48
|
+
*/
|
|
49
|
+
function withTimeout(promise, ms, label) {
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
const timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms} ms`)), ms);
|
|
52
|
+
timer.unref?.();
|
|
53
|
+
promise.then(
|
|
54
|
+
(value) => {
|
|
55
|
+
clearTimeout(timer);
|
|
56
|
+
resolve(value);
|
|
57
|
+
},
|
|
58
|
+
(error) => {
|
|
59
|
+
clearTimeout(timer);
|
|
60
|
+
reject(error);
|
|
61
|
+
}
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @typedef {object} MappedEndpoint
|
|
68
|
+
* @property {string | null} externalIp - Public IP as seen by NAT-PMP/UPnP, or null if unknown.
|
|
69
|
+
* @property {number} externalPort
|
|
70
|
+
* @property {"TCP" | "UDP"} protocol
|
|
71
|
+
*/
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @typedef {object} PortMapper
|
|
75
|
+
* @property {() => Promise<void>} start - Create the mapping (best-effort, never throws).
|
|
76
|
+
* @property {() => Promise<void>} stop - Remove the mapping and stop auto-renew (idempotent).
|
|
77
|
+
* @property {() => MappedEndpoint | null} getMappedEndpoint - The active mapping, or null.
|
|
78
|
+
*/
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Create a port mapper for a single local port.
|
|
82
|
+
*
|
|
83
|
+
* @param {object} opts
|
|
84
|
+
* @param {number} opts.port - The local port to expose (used as both public and private port).
|
|
85
|
+
* @param {"TCP" | "UDP"} [opts.protocol] - Protocol to map. Defaults to "TCP" (the HTTP/stream port).
|
|
86
|
+
* @param {string} [opts.description] - Human-readable label shown in the router's port-mapping table.
|
|
87
|
+
* @param {number} [opts.ttlSeconds] - Lease time in seconds. Defaults to {@link DEFAULT_TTL_SECONDS}.
|
|
88
|
+
* @param {number} [opts.portRangeEnd] - When set and > `port`, map the whole
|
|
89
|
+
* contiguous range `[port..portRangeEnd]` (used for the WebRTC UDP range, so
|
|
90
|
+
* whichever port a session binds is reachable). Single port otherwise.
|
|
91
|
+
* @returns {PortMapper}
|
|
92
|
+
*/
|
|
93
|
+
export function createPortMapper({
|
|
94
|
+
port,
|
|
95
|
+
protocol = "TCP",
|
|
96
|
+
description = "torrent-tv proxy",
|
|
97
|
+
ttlSeconds = DEFAULT_TTL_SECONDS,
|
|
98
|
+
portRangeEnd
|
|
99
|
+
} = {}) {
|
|
100
|
+
const rangeEnd = Number.isInteger(portRangeEnd) && portRangeEnd > port ? portRangeEnd : null;
|
|
101
|
+
const portList = rangeEnd
|
|
102
|
+
? Array.from({ length: rangeEnd - port + 1 }, (_, i) => port + i)
|
|
103
|
+
: [port];
|
|
104
|
+
const label = rangeEnd ? `${protocol} ${port}-${rangeEnd}` : `${protocol} ${port}`;
|
|
105
|
+
|
|
106
|
+
/** @type {InstanceType<typeof NatAPI> | null} */
|
|
107
|
+
let nat = null;
|
|
108
|
+
/** @type {MappedEndpoint | null} */
|
|
109
|
+
let mappedEndpoint = null;
|
|
110
|
+
let started = false;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Destroy the NatAPI instance, swallowing errors. `destroy()` unmaps every
|
|
114
|
+
* open port and clears the auto-renew timers.
|
|
115
|
+
*
|
|
116
|
+
* @param {InstanceType<typeof NatAPI>} instance
|
|
117
|
+
* @returns {Promise<void>}
|
|
118
|
+
*/
|
|
119
|
+
async function safeDestroy(instance, { logRemoval = false } = {}) {
|
|
120
|
+
try {
|
|
121
|
+
await withTimeout(instance.destroy(), STOP_TIMEOUT_MS, "destroy");
|
|
122
|
+
if (logRemoval) {
|
|
123
|
+
logger.info(`port-mapper: removed mapping for ${label}`);
|
|
124
|
+
}
|
|
125
|
+
} catch (error) {
|
|
126
|
+
// Lease expiry (ttl) is the backstop if we cannot unmap cleanly.
|
|
127
|
+
logger.warn(`port-mapper: failed to remove ${label} mapping cleanly: ${describeError(error)}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @returns {Promise<void>}
|
|
133
|
+
*/
|
|
134
|
+
async function start() {
|
|
135
|
+
if (started) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
started = true;
|
|
139
|
+
|
|
140
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
141
|
+
logger.warn(`port-mapper: invalid port ${port}; skipping port mapping`);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let instance;
|
|
146
|
+
let mappedCount = 0;
|
|
147
|
+
try {
|
|
148
|
+
instance = new NatAPI({ ttl: ttlSeconds, autoUpdate: true, description });
|
|
149
|
+
// Map every port in the range, best-effort per port (one port failing
|
|
150
|
+
// must not abort the rest). All share the one NatAPI instance, so its
|
|
151
|
+
// auto-renew covers them and a single destroy() unmaps all.
|
|
152
|
+
for (const p of portList) {
|
|
153
|
+
try {
|
|
154
|
+
await withTimeout(
|
|
155
|
+
instance.map({ publicPort: p, privatePort: p, protocol, description, ttl: ttlSeconds }),
|
|
156
|
+
START_TIMEOUT_MS,
|
|
157
|
+
`map ${p}`
|
|
158
|
+
);
|
|
159
|
+
mappedCount++;
|
|
160
|
+
if (mappedCount === 1) {
|
|
161
|
+
// The UPnP SSDP emitter gains one listener per map()/renewal; a
|
|
162
|
+
// 10-port range exceeds Node's default limit of 10 and floods the
|
|
163
|
+
// log with MaxListenersExceededWarning. The client is created
|
|
164
|
+
// lazily by the first map(), so lift the limit right after it.
|
|
165
|
+
instance._upnpClient?.ssdp?.setMaxListeners?.(0);
|
|
166
|
+
}
|
|
167
|
+
} catch (error) {
|
|
168
|
+
logger.warn(`port-mapper: failed to map ${protocol} ${p}: ${describeError(error)}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
} catch (error) {
|
|
172
|
+
mappedEndpoint = null;
|
|
173
|
+
logger.warn(`port-mapper: no port mapping available (${describeError(error)}); continuing without it`);
|
|
174
|
+
if (instance) {
|
|
175
|
+
await safeDestroy(instance);
|
|
176
|
+
}
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (mappedCount === 0) {
|
|
181
|
+
// No UPnP/NAT-PMP on this router, or it declined. Normal, non-fatal: the
|
|
182
|
+
// proxy still works on LAN and wherever hole punching succeeds.
|
|
183
|
+
mappedEndpoint = null;
|
|
184
|
+
logger.warn(`port-mapper: no ports mapped for ${label}; continuing without it`);
|
|
185
|
+
await safeDestroy(instance);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Mapping succeeded — keep the instance so its auto-renew timers stay alive
|
|
190
|
+
// and stop() can remove the mappings later.
|
|
191
|
+
nat = instance;
|
|
192
|
+
|
|
193
|
+
// Discover the external IP (best-effort; the mapping is valid without it).
|
|
194
|
+
let externalIp = null;
|
|
195
|
+
try {
|
|
196
|
+
externalIp = await withTimeout(instance.externalIp(), START_TIMEOUT_MS, "externalIp");
|
|
197
|
+
} catch (error) {
|
|
198
|
+
logger.warn(`port-mapper: mapped ${label} but could not read external IP: ${describeError(error)}`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
mappedEndpoint = { externalIp: externalIp || null, externalPort: port, protocol };
|
|
202
|
+
const counts = portList.length > 1 ? ` (${mappedCount}/${portList.length} ports)` : "";
|
|
203
|
+
if (externalIp) {
|
|
204
|
+
logger.success(
|
|
205
|
+
`port-mapper: mapped ${externalIp} → ${label}${counts} (ttl ${ttlSeconds}s, auto-renew)`
|
|
206
|
+
);
|
|
207
|
+
} else {
|
|
208
|
+
logger.info(
|
|
209
|
+
`port-mapper: mapped ${label}${counts} (external IP unknown; ttl ${ttlSeconds}s, auto-renew)`
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* @returns {Promise<void>}
|
|
216
|
+
*/
|
|
217
|
+
async function stop() {
|
|
218
|
+
if (!nat) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const instance = nat;
|
|
222
|
+
nat = null;
|
|
223
|
+
mappedEndpoint = null;
|
|
224
|
+
await safeDestroy(instance, { logRemoval: true });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* @returns {MappedEndpoint | null}
|
|
229
|
+
*/
|
|
230
|
+
function getMappedEndpoint() {
|
|
231
|
+
return mappedEndpoint;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return { start, stop, getMappedEndpoint };
|
|
235
|
+
}
|
|
@@ -1,81 +1,81 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file HTTP client for the registry server API.
|
|
3
|
-
*
|
|
4
|
-
* Handles proxy registration with the registry server.
|
|
5
|
-
* The auth token is sent as the `x-proxy-token` request header.
|
|
6
|
-
*
|
|
7
|
-
* Liveness is tracked via the WebSocket tunnel connection — no heartbeat
|
|
8
|
-
* HTTP polling is needed. The proxy re-registers on every tunnel reconnect
|
|
9
|
-
* so the server's in-memory store stays consistent after restarts.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Parameters required to register this proxy with the registry server.
|
|
14
|
-
*
|
|
15
|
-
* @typedef {Object} RegisterClientParams
|
|
16
|
-
* @property {string} serverUrl - Base URL of the registry server (e.g. "http://my-server:8080").
|
|
17
|
-
* @property {string} id - Stable unique identifier for this proxy instance.
|
|
18
|
-
* @property {string} name - Human-readable display name shown in the server UI.
|
|
19
|
-
* @property {string} baseUrl - Publicly reachable base URL of this proxy's HTTP server.
|
|
20
|
-
* @property {string} token - Auth token sent as the `x-proxy-token` header.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* The summary record returned by the server after a successful registration.
|
|
25
|
-
*
|
|
26
|
-
* @typedef {Object} ProxyClientSummary
|
|
27
|
-
* @property {string} id - Stable unique identifier.
|
|
28
|
-
* @property {string} name - Display name.
|
|
29
|
-
* @property {string} baseUrl - Advertised base URL.
|
|
30
|
-
* @property {string} createdAt - ISO timestamp of first registration.
|
|
31
|
-
* @property {string} lastSeenAt - ISO timestamp of this registration.
|
|
32
|
-
*/
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Build a full URL to a registry API endpoint.
|
|
36
|
-
*
|
|
37
|
-
* @param {string} serverUrl - Base URL of the registry server.
|
|
38
|
-
* @param {string} pathname - Relative path (e.g. "api/proxy-clients/register").
|
|
39
|
-
* @returns {URL}
|
|
40
|
-
*/
|
|
41
|
-
function buildRegistryUrl(serverUrl, pathname) {
|
|
42
|
-
return new URL(pathname, ensureBaseUrl(serverUrl));
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Ensure the server URL ends with a trailing slash so that relative
|
|
47
|
-
* paths in `new URL(pathname, base)` resolve correctly.
|
|
48
|
-
*
|
|
49
|
-
* @param {string} serverUrl
|
|
50
|
-
* @returns {string}
|
|
51
|
-
*/
|
|
52
|
-
function ensureBaseUrl(serverUrl) {
|
|
53
|
-
return serverUrl.endsWith("/") ? serverUrl : `${serverUrl}/`;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Register this proxy with the registry server.
|
|
58
|
-
*
|
|
59
|
-
* Throws if the server responds with a non-2xx status. The caller is
|
|
60
|
-
* responsible for retrying — see `cli.js` → `registerWithRetry`.
|
|
61
|
-
*
|
|
62
|
-
* @param {RegisterClientParams} params
|
|
63
|
-
* @returns {Promise<{ client: ProxyClientSummary }>}
|
|
64
|
-
*/
|
|
65
|
-
export async function registerClient({ serverUrl, id, name, baseUrl, token }) {
|
|
66
|
-
const response = await fetch(buildRegistryUrl(serverUrl, "api/proxy-clients/register"), {
|
|
67
|
-
method: "POST",
|
|
68
|
-
headers: {
|
|
69
|
-
"Content-Type": "application/json",
|
|
70
|
-
"x-proxy-token": token
|
|
71
|
-
},
|
|
72
|
-
body: JSON.stringify({ id, name, baseUrl })
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
if (!response.ok) {
|
|
76
|
-
const bodyText = await response.text();
|
|
77
|
-
throw new Error(`Registration failed (${response.status}): ${bodyText}`);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return response.json();
|
|
81
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @file HTTP client for the registry server API.
|
|
3
|
+
*
|
|
4
|
+
* Handles proxy registration with the registry server.
|
|
5
|
+
* The auth token is sent as the `x-proxy-token` request header.
|
|
6
|
+
*
|
|
7
|
+
* Liveness is tracked via the WebSocket tunnel connection — no heartbeat
|
|
8
|
+
* HTTP polling is needed. The proxy re-registers on every tunnel reconnect
|
|
9
|
+
* so the server's in-memory store stays consistent after restarts.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Parameters required to register this proxy with the registry server.
|
|
14
|
+
*
|
|
15
|
+
* @typedef {Object} RegisterClientParams
|
|
16
|
+
* @property {string} serverUrl - Base URL of the registry server (e.g. "http://my-server:8080").
|
|
17
|
+
* @property {string} id - Stable unique identifier for this proxy instance.
|
|
18
|
+
* @property {string} name - Human-readable display name shown in the server UI.
|
|
19
|
+
* @property {string} baseUrl - Publicly reachable base URL of this proxy's HTTP server.
|
|
20
|
+
* @property {string} token - Auth token sent as the `x-proxy-token` header.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The summary record returned by the server after a successful registration.
|
|
25
|
+
*
|
|
26
|
+
* @typedef {Object} ProxyClientSummary
|
|
27
|
+
* @property {string} id - Stable unique identifier.
|
|
28
|
+
* @property {string} name - Display name.
|
|
29
|
+
* @property {string} baseUrl - Advertised base URL.
|
|
30
|
+
* @property {string} createdAt - ISO timestamp of first registration.
|
|
31
|
+
* @property {string} lastSeenAt - ISO timestamp of this registration.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Build a full URL to a registry API endpoint.
|
|
36
|
+
*
|
|
37
|
+
* @param {string} serverUrl - Base URL of the registry server.
|
|
38
|
+
* @param {string} pathname - Relative path (e.g. "api/proxy-clients/register").
|
|
39
|
+
* @returns {URL}
|
|
40
|
+
*/
|
|
41
|
+
function buildRegistryUrl(serverUrl, pathname) {
|
|
42
|
+
return new URL(pathname, ensureBaseUrl(serverUrl));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Ensure the server URL ends with a trailing slash so that relative
|
|
47
|
+
* paths in `new URL(pathname, base)` resolve correctly.
|
|
48
|
+
*
|
|
49
|
+
* @param {string} serverUrl
|
|
50
|
+
* @returns {string}
|
|
51
|
+
*/
|
|
52
|
+
function ensureBaseUrl(serverUrl) {
|
|
53
|
+
return serverUrl.endsWith("/") ? serverUrl : `${serverUrl}/`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Register this proxy with the registry server.
|
|
58
|
+
*
|
|
59
|
+
* Throws if the server responds with a non-2xx status. The caller is
|
|
60
|
+
* responsible for retrying — see `cli.js` → `registerWithRetry`.
|
|
61
|
+
*
|
|
62
|
+
* @param {RegisterClientParams} params
|
|
63
|
+
* @returns {Promise<{ client: ProxyClientSummary }>}
|
|
64
|
+
*/
|
|
65
|
+
export async function registerClient({ serverUrl, id, name, baseUrl, token }) {
|
|
66
|
+
const response = await fetch(buildRegistryUrl(serverUrl, "api/proxy-clients/register"), {
|
|
67
|
+
method: "POST",
|
|
68
|
+
headers: {
|
|
69
|
+
"Content-Type": "application/json",
|
|
70
|
+
"x-proxy-token": token
|
|
71
|
+
},
|
|
72
|
+
body: JSON.stringify({ id, name, baseUrl })
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
const bodyText = await response.text();
|
|
77
|
+
throw new Error(`Registration failed (${response.status}): ${bodyText}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return response.json();
|
|
81
|
+
}
|