0.14.0: torrent — make a torrent out of a directory, and get it seeded - #28
Merged
Conversation
Two programs do the work, the same way `dl` leans on yt-dlp and `vid` on ffmpeg: `create-torrent` writes the .torrent and torlnk seeds it. What this adds is the part neither of them does. `create-torrent` writes a file and never prints a hash; torlnk takes a magnet rather than a file. The two do not actually meet without something in between. The info hash is computed here rather than by adding a bencode parser: it is a SHA-1 over the bencoded `info` dictionary, and the only hard requirement is that the bytes are the ORIGINAL ones — decode and re-encode and the hash changes the moment a client wrote a key in an order we did not reproduce. So src/torrent.ts scans bencode for the span and hashes it verbatim, in about forty lines, with a test that checks the result against the info hash a real client computed for a torrent it made itself. Trackers are the part that looks cosmetic and is not. A browser can only ever be a WebRTC peer, so a torrent with no wss:// tracker is invisible to every web player — it is on the DHT, desktop clients find it, and the browser sees a torrent with no peers, which reads as a dead torrent rather than a missing tracker. The default list carries both kinds and every entry was checked rather than copied: of the announce list the WebTorrent tooling ships by default, tracker.leechers-paradise.org has no DNS at all, coppersurfer.tk and empire-js.us both time out on a UDP connect, and tracker.btorrent.xyz serves a self-signed certificate a browser refuses outright. What is left is two WSS trackers that complete a WebSocket handshake and four UDP trackers that return a connection id. `--private` is opt-in and named, because a private torrent is excluded from the DHT by every client that honours the flag — the opposite of the reason to make one here. `seed` hands the magnet to torlnk, which is the process that stays running: its serve API by default, or a watch directory as the offline handoff. Seed duration is deliberately not a flag here — torlnk's --seed-time is a daemon setting rather than a per-torrent one, and inventing a flag that silently does nothing is worse than not having one. Verified end to end: create on a real directory produces a torrent whose info hash matches what create-torrent's own reader reports. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LkEHoAhsoDhqVeJ1MJH9yD
A web seed (BEP 19) is a plain HTTP URL every peer can pull the same bytes from, which makes the torrent downloadable the moment it exists — before any peer has it, and with nothing staying running. That is the answer to "I have this at a URL already" that does not involve seeding it a second time. The caveat is worth the line it takes: the URL must serve the exact bytes the torrent was made from, so a redirect to a re-encoded copy produces a torrent that fails its hash check — which reads as corruption rather than as a misconfigured seed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LkEHoAhsoDhqVeJ1MJH9yD
ThreatCrush Security Scan15 finding(s) HIGH/CRITICAL: 5 | MEDIUM: 1 | LOW: 9
Snippets are redacted; ThreatCrush never prints matched credential material. |
ralyodio
added a commit
that referenced
this pull request
Aug 29, 2026
The free-names branch was cut when master was 0.12.0 and bumped to 0.13.0 in its feature commit, the convention here. By the time it merged, `torrent` (#28) and porkbun's `check`/`register` (#29) had taken master to 0.14.0, and the squash replayed the older bump on top — so a release that added a command published a lower version than the one before it. 0.15.0 rather than 0.14.1: free-names is a new command, and a new command is a minor here. Worth knowing for next time: a hand-maintained version in the feature commit only works while one branch is in flight. A squash does not conflict on it when the surrounding lines still match, so nothing catches the regression at merge time — only reading the tag afterwards does. Claude-Session: https://claude.ai/code/session_01XGe9mWC6FvUjT6p1HCVjba Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Split out of #27, which merged before these commits landed. Depends on nothing in flight.
create-torrentwrites a .torrent and never prints a hash; torlnk takes a magnet rather than a file. The two do not meet without something in between.The info hash is computed here, not by adding a dependency
A SHA-1 over the bencoded
infodictionary. The hard requirement is that the bytes be the original ones — decode and re-encode, and the hash changes the moment a client wrote a key in an order we did not reproduce, which makes it a different torrent. Sosrc/torrent.tsscans bencode for the span and hashes it verbatim, in about forty lines.The test that matters asserts our hash equals the one a real client computed for a torrent it made itself, so an off-by-one at either end of the span fails loudly rather than producing a plausible-looking wrong magnet.
Trackers are the part that looks cosmetic and is not
A browser can only ever be a WebRTC peer, so a torrent with no
wss://tracker is invisible to every web player — it is on the DHT, desktop clients find it, and the browser sees a torrent with no peers, which reads as a dead torrent rather than as a missing tracker.Every entry in the default list was checked rather than copied. Of the announce list the WebTorrent tooling ships by default:
tracker.leechers-paradise.orgtracker.coppersurfer.tktracker.empire-js.ustracker.btorrent.xyzWhat is left is two WSS trackers that complete a WebSocket handshake and four UDP trackers that return a connection id.
Notes
--privateis opt-in and named: a private torrent is excluded from the DHT by every client honouring the flag — the opposite of the reason to make one here.--webseedembeds HTTP URLs already serving the same bytes (BEP 19), so the torrent is downloadable before any peer has it. The URL must serve the exact bytes, or the torrent fails its hash check in a way that looks like corruption.--seed-timeis a daemon setting, not per-torrent, and a flag that silently does nothing is worse than none.Testing
pnpm test— 491 pass (15 new),pnpm typecheckclean.Verified against a live torlnk
servedaemon, not just the suite:torrent seedcreated the torrent, computed the magnet,POST /addreturned 200, and torlnk picked it up and found peers.One limitation found while verifying, worth knowing before relying on
seed: torlnk does not verify pre-existing local data when a magnet is added. With the files already sitting in its--todirectory under the torrent's own name, it still reportedstatus: downloading, progress: 0rather than seeding what was already on disk. Soseedreliably hands the torrent off, but torlnk currently only seeds what it downloaded itself. That is an upstream gap rather than one this PR can close.🤖 Generated with Claude Code
https://claude.ai/code/session_01LkEHoAhsoDhqVeJ1MJH9yD