Skip to content
andrewtheguyPublic

About

End-to-end encrypted peer-to-peer file and folder transfer, in the browser or a terminal on Linux, macOS and Windows. WebRTC over rotating-PIN Nostr signaling, hand-carried codes, or a Tor onion service. A static site and one self-contained CLI: no backend, no accounts.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

pTransfer

pTransfer is a web application for sending encrypted files and folders with PIN-based Nostr signaling. It uses WebRTC for direct P2P connections, or a Tor onion service published from the browser tab.

Demo: Launch pTransfer

Features

  • 100% Static - No Backend Required: The entire app is a static site that can be hosted on any static hosting service (GitHub Pages, Netlify, Vercel, S3, etc.). No server-side code, no database, no backend infrastructure needed.
  • Works offline: No internet required after page load when using Code Exchange on same local network
  • Three transfer modes: PIN Exchange (default), Code Exchange, or Tor Onion Service — see Transport Layer
  • Tor Onion Service: The sending tab publishes a v3 onion service of its own and mints a one-time password; the receiver needs only those two strings. No pTransfer or Nostr relay and no direct connection between the two networks; the path is Tor's own — a Snowflake bridge (which uses STUN only on the webrtc bridge), Tor relays, and the HSDirs carrying the descriptor. There is no pTransfer rendezvous record, though Tor's descriptor remains retrievable by anyone holding the onion address until it expires. Slower, capped at 100 MiB and best kept far smaller. See Tor Transport
  • Flexible signaling: Nostr (default) or Code Exchange — you hand over the offer (QR/copy-paste) and the receiver hands the response back the same way; the response only enters the sender's page when the sender scans or pastes it. With internet, Code Exchange can connect across different networks when ICE finds a direct route; without internet, it can connect over the same local network.
  • Rotating PIN pairing (Nostr): A case-sensitive 12-character PIN (letters and digits only) that rotates every 2 minutes locates the sender and drives a SPAKE2 password-authenticated key exchange; nothing published to relays can be used to guess the PIN offline
  • Anonymous signaling (PIN Exchange, experimental): An advanced option on the send tab that carries the handshake through Tor, inside the browser, to Nostr relays run as onion services — so no Nostr relay sees either device's IP address. The PIN comes out 16 characters instead of 12, and that length is the whole signal: the receiver's page recognizes it and follows, with nothing to turn on and nothing to agree in advance. It starts slowly and fails more often. A direct file transfer is still a WebRTC connection; only when none can be made does the file go through Tor, over the same Tor fallback Code Exchange's anonymous option uses. See Anonymous Signaling
  • Anonymous signaling and relay (Code Exchange, experimental): A separate advanced option keeps Code Exchange's fallback off the clearnet. The offer and answer are still carried by hand and direct WebRTC is still tried first, but both sides prepare Tor in the background; if the direct route fails, onion-service Nostr relays carry the encrypted control channel and a temporary onion service published by the sender carries the file. Both devices need internet, the fallback remains capped at 100 MiB, and this option is slower and less reliable than the ordinary public-relay fallback. See Code Exchange
  • Confirmation code (Nostr): After entering the PIN, the receiver is shown an 8-character code the sender must type in before anything is sent — so someone who spots the PIN over your shoulder cannot quietly take the file
  • File or folder transfer: Send a file, or a ZIP archive created from multiple files/a folder. Compression happens only where it pays: a direct P2P connection carries everything as it is, while the slow paths (Tor and the Nostr relay fallback) deflate a single file and restore it on receipt, and a ZIP deflates its own entries. Files whose format is compressed already — audio, video, compressed images, archives — are never deflated, on any path. The 2 GiB limit applies both to the selected input and to the encoded wire payload: a selection over 2 GiB is refused up front, while one very close to the limit can still fail during transfer if incompressible deflate output or ZIP headers push the wire payload over it. On the direct P2P path, the sender reads selected files lazily without scratch storage and receivers keep payloads up to 100 MiB in memory before spilling to OPFS. The ordinary Nostr fallback instead materializes payloads in memory. File-relay fallbacks and the Tor file transport are capped at 100 MiB. See Browser Requirements. For files larger than 2 GiB, use beam-rs
  • End-to-end encryption: All transfers use AES-256-GCM encryption
  • No accounts required: Ephemeral keypairs generated per transfer
  • PWA Support: Install as a Progressive Web App for offline access
  • English and Spanish: The site is in English at / and in Spanish under /es/ (/es/send, /es/about, …), each page with its own URL for search engines; the CLI speaks Spanish with --lang es, or when the environment's locale does — see Language

Command line

The same code runs in a terminal on Linux, macOS and Windows. For files larger than 2 GiB, use beam-rs instead. cli/ is a Bun application that imports the web app's src/lib directly: the protocol, the crypto, and the Tor client are the ones the browser tab runs, so a change to the app is a change to the CLI by construction. What the CLI adds is what a process has and a page does not — files, a cache directory, and plain HTTP to the Tor directory authorities, which turns the minutes-long browser bootstrap into seconds.

Today it sends and receives files and folders in all three of the tab's transfer modes — PIN Exchange, Code Exchange, and a Tor onion service — run from the same code.

Each release ships it as one self-contained executable for Linux x64, Linux arm64, Apple silicon macOS and Windows x64, under Releases. The installer picks the one for the machine it runs on, checks it against the checksum GitHub published for it, and puts it on your PATH as ptransfer:

curl -fsSL https://raw.githubusercontent.com/andrewtheguy/ptransfer/main/install.sh | bash

That is ~/.local/bin on both, with no sudo, and the installer warns when another ptransfer earlier on your PATH would run instead of it. Arguments go after bash -s --: a release tag installs that release instead of the latest, --prerelease takes the newest prerelease, and --download-only leaves the binary in the current directory rather than installing it. By hand, download ptransfer-linux-amd64, ptransfer-linux-arm64 or ptransfer-macos-arm64 and chmod +x it.

On Windows, in PowerShell — Windows PowerShell 5.1 or PowerShell 7:

irm https://raw.githubusercontent.com/andrewtheguy/ptransfer/main/install.ps1 | iex

It checks the download against GitHub's checksum the same way, installs it as %LOCALAPPDATA%\Programs\ptransfer\ptransfer.exe without administrator rights, and adds that folder to your user PATH. iex passes no arguments, so for options run it as a script block: -Tag v<version> installs that release, -Prerelease the newest prerelease, and -DownloadOnly leaves the executable in the current directory:

& ([scriptblock]::Create((irm https://raw.githubusercontent.com/andrewtheguy/ptransfer/main/install.ps1))) -Prerelease

By hand, download ptransfer-windows-amd64.exe. It is not signed, so a copy a browser downloaded gets a SmartScreen warning the first time it runs; the installer's download does not. There is no Arm build; Windows on Arm's x64 emulation may run this one, untested. WSL counts as Linux and takes install.sh.

The commands below run it from a checkout as bun run cli; the executable takes the same commands as ptransfer. bun run build:cli builds it locally.

bun run cli with no command opens a terminal UI, which is the tab's own screens drawn in a terminal: pick files and folders in a file browser, then one of the tab's three transfer modes; or paste what a sender gave you and let it work out which of the three it is. On Windows, Windows Terminal — the default terminal on Windows 11 — is the one it is tested in and the one to use. It works in the older console host too, which Windows 10 and Windows Server open PowerShell and cmd in unless Windows Terminal is set as the default, copying included, though that is checked by hand rather than on every change.

Every command below is the line-oriented interface instead, which is what a pipe and a script want and what stays out of the terminal UI's way. PIN Exchange reads out a PIN and takes back a confirmation code; Code Exchange carries its codes as text — a terminal has no camera — which is also the text the tab's Copy Data gives and its Paste tab takes, so either end of either mode can be a tab:

# sender: shows a PIN to read out — a fresh one every couple of minutes until
# someone takes it — then asks for the confirmation code the receiver shows
bun run cli send --pin ./report.pdf
bun run cli send --pin --anonymous ./report.pdf  # the handshake through Tor

# receiver: asks for the PIN, prints the confirmation code to read back, and
# saves report.pdf once the sender has typed it
bun run cli receive --pin

# sender: prints a code for the receiver, then asks for their response
bun run cli send --code ./report.pdf
bun run cli send --code --anonymous ./report.pdf  # the Tor fallback instead

# receiver: asks for the sender's code, prints a response to hand back, and
# saves report.pdf in the current directory, or in the folder --out names
bun run cli receive --code
bun run cli receive --code --out ~/Downloads

The file goes over a direct WebRTC connection (node-datachannel's, under Bun). When none opens, a file up to 100 MiB takes the fallback the sender's code names: public Nostr relays, or with --anonymous Nostr relays run as onion services and a Tor onion service for the file. Codes are pasted at a prompt — whitespace and line wrapping are ignored — or piped in; either one is taken as soon as it is whole, so a script can drive both ends. The sender only acts on a response to its own code, and asks again for one that is not. receive --code --simulate-no-direct answers with no network routes, so a test can exercise the fallback without a hostile network.

# sender: prints an .onion address and a one-time password, then waits
bun run cli send --tor ./report.pdf

# receiver: asks for the password (or reads it from a pipe)
bun run cli receive --onion <address>.onion
bun run cli receive --onion <address>.onion --out ~/Downloads

send takes any number of files and folders. One file is sent as itself; several, or a folder, go as one ZIP generated while it is sent — the tab's own archive code — with each folder's contents under the folder's name, so send --code ./photos ./notes.txt arrives as files_<timestamp>.zip holding photos/… and notes.txt. Symbolic links inside a folder, and other files that are not regular files, are left out and named on the way. Files are read only once a receiver connects; one that has been replaced or changed length since the command started stops the transfer rather than going out in its place.

Codes and passwords are read from standard input rather than a flag, so they stay out of shell history and the process list; what the other side needs goes to standard output, one line each, and everything else to standard error. The receiver never overwrites a file: a Code Exchange receiver refuses a name that is taken before it answers, and a Tor receiver declines, leaving the sender waiting for another try. The folder --out names must already exist; it is never created, so a typo fails instead of saving somewhere unexpected.

It also carries a live self-check of the Tor path:

bun run cli tor-test

It downloads the Tor directory from the authorities, or reuses the copy it cached while that still describes the network. The cache is ~/Library/Caches/ptransfer on macOS; on Linux it is $XDG_CACHE_HOME/ptransfer when XDG_CACHE_HOME is an absolute path, and ~/.cache/ptransfer otherwise. The command then bootstraps the Tor client over a Snowflake bridge — the websocket one by default, or with --bridge webrtc a volunteer proxy, which is slower but harder to block, the same two the tab offers — fetches a page from the Tor Project's onion site (--url picks another), publishes a v3 onion service of its own, connects back to it through the network, and prints how long each step took. send and receive bootstrap the same way, from the same cache. A Code Exchange sender keeps the relay cache there too — what earlier transfers learned about which public Nostr relays work, the records the tab keeps in IndexedDB — in relay-cache.json. That one moves on its own: PTRANSFER_RELAY_CACHE=<directory> puts it somewhere else, and PTRANSFER_RELAY_CACHE=off keeps it in memory for one run, so nothing about which relays this machine has probed is left on disk.

Browser Requirements

Receiving a P2P payload over 100 MiB uses the origin-private file system (OPFS). P2P senders never need OPFS: direct files are read lazily from the picker and sent as they are, and multi-file/folder ZIP output is packaged and sent on the fly. OPFS requires:

  • A secure context: the app must be served over HTTPS (or localhost) — OPFS and the Web Crypto API are unavailable otherwise
  • FileSystemFileHandle.createWritable: available in Chromium browsers since 86, Firefox since 111 (desktop and Android), Samsung Internet since 21, and Safari/iOS since 26 — see the support matrix. Note this is a stricter requirement than the general OPFS feature (Baseline since March 2023): Safari had OPFS from 16.4 but only gained createWritable, the part this app needs, in 26

Support is feature-detected at runtime; on an unsupported browser, receiving a P2P payload that crosses 100 MiB fails with a clear error rather than degrading. P2P payloads of 100 MiB or less are buffered in memory and do not need OPFS (a secure context is still required for the Web Crypto API). The Nostr and Tor fallback paths never use OPFS because their payload cap is 100 MiB.

Version Compatibility

Sender and receiver need the same protocol version. It is separate from a release: releases move on without it, and two of them on the same protocol version work together. The web app shows it in the footer's version menu, over the commit that tab was built from; ptransfer --version prints it beside the CLI's release version. Between two protocol versions, compatibility is not guaranteed.

The web app and the CLI in cli/ are one codebase, so they share one protocol version, PROTOCOL_VERSION in src/lib/protocol-version.ts, and it moves with any change to what goes on the wire. That is specified in docs/ — PIN Exchange and the shared transfer layer in docs/INTEROP_PROTOCOL.md, and each other mode in its own document — so that a change to it is a deliberate one. The protocol version itself is not sent; the Tor handshake alone carries a version of its own on the wire and refuses a mismatch.

Only the CLI carries a release number, the package.json version its binaries are tagged and published under, which moves when something a CLI user depends on changes. The web app has none: it is deployed from a commit, and that commit is what the footer names.

How It Works

Sending Files or Folders

  1. Select the "Files" or "Folder" tab
  2. Drag and drop files or click to select a file/folder. A single file, or the combined input for multiple files or a folder (zipped while sending), can be up to 2 GiB. The encoded wire payload must also fit within 2 GiB, so a selection extremely close to the limit may fail if compression or ZIP bookkeeping makes it grow. For anything larger, use beam-rs
  3. Choose PIN Exchange, Code Exchange, or Tor Onion Service
  4. For PIN Exchange, click "Start PIN Exchange". The 12-character PIN appears alongside a QR code the receiver can scan to open the receive page with the PIN already filled in; reading the PIN out or copying it works just as well. The PIN rotates automatically every 2 minutes while keeping the immediately previous PIN bucket valid, and the QR follows each rotation; a countdown under the PIN shows when the next one appears. Selecting "Generate a new PIN" replaces it immediately and invalidates all retained older PIN generations.
  5. Optionally, open "Advanced options" before starting and turn on Anonymous signaling, then pick which Snowflake bridge this tab reaches Tor through. The PIN becomes 16 characters and the handshake runs through Tor to onion-service relays; the recipient's page detects that from the PIN alone. Expect a slow start and more failures, and note that the file itself still travels over a direct connection whenever one forms; if none can, it goes through Tor instead of public relays.
  6. Once someone claims the transfer, ask them for the confirmation code on their screen and enter it. Nothing is sent until it matches, so a stranger who saw the PIN cannot receive the file.
  7. For Code Exchange, click "Start Code Exchange" and give the receiver the QR/copy-paste signaling payload, then scan or paste the response they show you. Its ordinary fallback uses public Nostr relays; the experimental Anonymous signaling and relay advanced option prepares an onion-only fallback instead and requires both devices to have internet.
  8. For Tor Onion Service, pick which Snowflake bridge the tab reaches Tor through and click "Publish Onion Service". Bootstrapping Tor and publishing the descriptor takes a while — minutes on a first run — after which the page shows an .onion address and a one-time password. Send the receiver both; the service answers only while the tab is open. A selection over 100 MiB cannot be sent this way, and anything over 1 MiB is flagged as slow rather than refused.

Receiving

There is one input for all three modes — no mode to choose. The page works out which exchange the sender used from what you give it.

  1. Scan the sender's QR code, or paste the PIN, onion address, or code they sent you, and click "Receive". A PIN QR can also be scanned with the phone's own camera app, which opens this page with the PIN filled in
  2. If that PIN was 16 characters, the sender turned on anonymous signaling: the page says so, asks which Snowflake bridge to reach Tor through, and then takes a while to start. Nothing else about the steps below changes
  3. If it was a PIN, read the confirmation code that appears back to the sender — nothing is sent until it matches
  4. If it was a connection code, show the sender the response code that appears, for them to scan or paste
  5. If it was an onion address, enter the one-time password the sender gave you and choose a bridge, then click "Connect over Tor"
  6. Click "Download File" to save

Security

  • SPAKE2 password-authenticated key exchange (Nostr): The PIN drives a balanced PAKE (RFC 9382 over P-256), so every published value — the blinded key-exchange elements and the sealed handshake payloads — is useless for testing PIN guesses offline. The only way to test a guess is a live claim the sender verifies online, and the sender caps those verifications per PIN generation
  • AES-256-GCM authenticated encryption
  • PIN Exchange carries the codes (Nostr): Once the receiver's confirmation code matches, the sender's Code Exchange offer and the receiver's answer travel sealed under a key derived from the SPAKE2 shared secret, and the file is encrypted under the ECDH key those codes agree — both built from fresh ephemeral keys on both devices, so a PIN recovered after the fact decrypts nothing. Because it is the same session, PIN Exchange gets Code Exchange's relay fallbacks too
  • PIN authenticates, then expires (Nostr): The sender mints a fresh 12-character PIN every 2 minutes (16 with anonymous signaling on, of which 12 are secret rather than 8) and honors only PINs published in its current or immediately previous 2-minute bucket. The mutual claim/confirm handshake is sealed with keys only a matching PAKE session holds, defeating relay man-in-the-middle. The first verified claim locks the transfer to that receiver; the PIN itself is never transmitted
  • Split PIN, public locator (Nostr): Only the first 3 characters — treated as public — derive the rotating #h tag that locates the rendezvous event, so no published value is a commitment to the rest of the PIN. A standard PIN therefore has 8 secret data characters (~46.3 bits), while an anonymous-signaling PIN has 12; either can be tested only by online guesses throttled by the sender against a PIN that is live for 2–4 minutes and worthless once a claim locks
  • Confirmation code stops front-running (Nostr): The receiver derives an 8-character code from the SPAKE2 shared secret and shows it once the sender's confirm verifies. The sender publishes no connection offer and sends no file bytes until its operator types a matching code. The code proves agreement with whichever claimant won the first-claim lock — and anyone who saw the PIN can win that race and compute the code — so the protection comes from where the sender learns it: the intended receiver, over a channel the attacker does not control (in person, a call). If a front-runner holds the lock, the intended receiver got no confirm, has no code to give, and the transfer stalls without sending anything. It doubles as a key-confirmation check, since tampered key-exchange elements would show different codes on the two screens
  • The handshake is bound to what is being sent (Nostr): The PAKE transcript keys every session to both parties' Nostr identities and the transfer id, the sealed claim/confirm echo a hash of the whole rendezvous record, and the file metadata delivered in the confirm is hashed into the confirmation code. An attacker who knows a live PIN therefore cannot republish the transfer under its own identity with a rewritten file name or type and still have the two codes match
  • Metadata after the handshake (Nostr): File name, size, and MIME type are never published to relays in plaintext — they travel inside the sealed confirm, encrypted under a key only the authenticated peer holds, so relays carry only ciphertext they cannot decrypt
  • Ephemeral identities: New Nostr keypairs and PAKE scalars generated per transfer
  • Expiration windows: Each PIN is honored until the end of the immediately following 2-minute bucket (roughly 2–4 minutes, depending on when it was minted); rendezvous events carry a matching NIP-40 expiration tag for relays that honor it, and the sender stops waiting after 30 minutes (a resource backstop — bucket expiry, not the wait window, bounds PIN exposure)
  • Code Exchange signaling: QR payloads are time-bucketed obfuscated, not cryptographically confidential; file data is encrypted with an ECDH-derived AES key after the QR/clipboard exchange

Tech Stack

  • React 19 + TypeScript
  • Vite
  • Tailwind CSS + shadcn/ui
  • nostr-tools for Nostr protocol
  • Web Crypto API for cryptographic operations, plus @noble/curves for the SPAKE2 group math Web Crypto cannot express

Development

Requires Bun 1.4.4.

# Install dependencies
bun install

# Start development server
bun run dev

# Build for production
bun run build

Cloudflare Pages

Use bun install --frozen-lockfile && bun run build as the build command and dist as the output directory.

Routing (Required)

The app uses BrowserRouter only. Configure hosting to rewrite unknown paths to index.html.

The build also writes one HTML file per indexed page and language — about.html, es/index.html, es/about.html, … — each with its own lang, title, description, canonical link and hreflang alternates, plus sitemap.xml. Serve them at their clean paths (/about, /es/, /es/about), which Cloudflare Pages does by default. The site's origin for those absolute links is SITE_ORIGIN in src/i18n/pages.ts.

Deployment Path Requirement (Multi-QR Code Exchange)

Multi-QR URLs are generated from window.location.origin and then append /r#....

  • Supported: deployment at the domain root (for example https://example.com)
  • Not supported: deployment under a subpath (for example https://example.com/my-app)

If the app is served from a subpath, scanned Multi-QR links will point to the domain root route and can 404.

Transport Layer

When WebRTC succeeds, both WebRTC modes share the same transfer protocol, and it flows one way: content goes out in 128 KiB AES-256-GCM chunks, with the chunk index authenticated as AES-GCM additional data, as fast as the data channel takes them. The sender ends with the chunk and byte counts and is complete once they have left its buffer; the receiver verifies every chunk and the final counts on its own and closes the connection when it has the file. A sender that gives up — cancelled, a file that is no longer readable — tells the receiver why; a receiver that gives up just hangs up. The protocol has no separate whole-file checksum and no receipt: whether the file arrived is for the two people to confirm. Code Exchange's ordinary Nostr fallback is different: it uses 48 KiB payload chunks, an encrypted manifest/control channel, and a whole-file SHA-256 check. The Tor Onion Service mode and the anonymous Tor fallback run the same transfer protocol over a Tor framed stream rather than a WebRTC data channel.

Transfer modes (sender chooses):

  • PIN Exchange (default): Requires internet. Decentralized Nostr relay signaling that carries a Code Exchange offer and answer for you, so devices can be on different networks and a failed direct connection falls back exactly as Code Exchange's does. An experimental advanced option, anonymous signaling, carries that signaling through Tor to onion-service relays instead, so no Nostr relay sees either device's IP address; the sender turns it on, the longer PIN it mints tells the receiving side to follow, and its fallback is the Tor one rather than public storage relays. See Anonymous Signaling.
  • Tor Onion Service: Requires internet. The sending tab publishes a v3 onion service and mints a one-time password; the two peers meet at a rendezvous point inside the Tor network, so neither learns the other's address. No pTransfer or Nostr relay is involved and the two networks never connect directly, but the transfer does travel through a Snowflake bridge (STUN-assisted on the webrtc bridge), Tor relays, and HSDirs. The same SPAKE2 exchange used by PIN mode authenticates the password, and the file is encrypted again inside the circuit. Slower than the other two and capped at 100 MiB per transfer, with no resume — the page flags anything over 1 MiB as slow without refusing it. Its handshake carries a version of its own and refuses a mismatch. See Tor Transport.
  • Code Exchange: No internet required for the ordinary mode. Both the sender's offer and the receiver's response are handed over via QR scan or copy/paste (camera optional); relays never carry signaling in this mode. With internet, STUN assists direct candidate discovery and the devices can connect across different networks when a direct ICE route exists. Without internet, devices must be able to reach each other directly, normally on the same local network. Its experimental Anonymous signaling and relay option does require internet on both devices because it prepares Tor for the fallback.

Data Transfer: WebRTC P2P first. STUN may help the peers discover a direct route; TURN relaying is not configured. In ordinary Code Exchange, when a direct P2P connection cannot be established and the offer named relays, the encrypted file (up to 100 MiB) is relayed through public Nostr relays automatically — the Nostr relay stand-in for TURN. No file data is uploaded ahead of time: the relay path runs only once the direct connection has failed, so a transfer that connects directly never puts file bytes on a storage relay. The fallback is unavailable when the offer named no relays or the file exceeds the 100 MiB cap, and it can still fail if too few storage relays work. With Anonymous signaling and relay selected, the same failure instead uses onion-service Nostr relays for coordination and a temporary Tor onion service for the file; that path has the same cap and remains best-effort. PIN Exchange runs the same session with its codes carried over the sealed handshake, so it falls back the same way: through public Nostr relays ordinarily, through Tor with anonymous signaling on. When a transfer cannot complete, the UI suggests transferring offline via animated QR codes with Secure QR Transfer, a separate tool for side-by-side devices. See Nostr File Relay for the ordinary relay transport and Code Exchange for the Tor-backed variant.

See Architecture for detailed transfer flows and encryption specifics.

Receive Input

One input accepts all three modes; the page works out which one the sender used from what it is given:

  • PIN Exchange: the rotating PIN from the sender's screen — typed, pasted, or scanned from the PIN QR. Signaling then runs over Nostr, and a confirmation code appears for the receiver to read back.
  • Tor Onion Service: the .onion address from the sender's screen, pasted or scanned. Its checksum is verified locally, and a password field then appears for the one-time password the sender gave you separately — nothing is bootstrapped until both check out.
  • Code Exchange: the sender's signaling payload, scanned from their QR codes or pasted as copied text. The response code that appears goes back to the sender the same way, carrying a tag derived from the shared key, the exact code it answers, and the response's own contents — the sender checks it automatically and refuses a response belonging to another transfer or altered on the way back, with nothing for either person to read out or type. If direct WebRTC later fails, files up to 100 MiB can also use the selected automatic fallback: public Nostr storage relays ordinarily, or Tor when the sender enabled the experimental anonymous option.

Language

Every string a person reads is written where it is used, once per language, as t({ en: '…', es: '…' }) from src/lib/i18n.ts — in React components through useT() from src/i18n/locale-context.ts. Adding a locale to LOCALES makes the type checker list every string that still needs it.

  • Web: the URL picks the language. A path under /es/ is Spanish, any other English; the pages are declared once and matched on the path without its prefix, so switching language from the navigation bar keeps a transfer in progress. In-app links go through LocaleLink and useLocaleNavigate to stay in the language. Links handed to the other side (a PIN link, an onion link, multi-QR links) carry no prefix; a Spanish sender's add &l=es to their fragment (/receive#p=<PIN>&l=es), and the page they open switches to Spanish before it renders. An English sender's links are unchanged.
  • CLI: --lang en or --lang es, with any command or none (ptransfer --lang es, ptransfer send --pin --lang es report.pdf), picks the language. Without it, the language comes from LC_ALL, LC_MESSAGES or LANG, the first one set (LANG=es_ES.UTF-8 ptransfer), and on Windows, which sets none, from the user's locale. The label: value lines a script reads from standard output keep their English labels in every language.
  • What travels to the peer, such as the reason a sender stopped, stays in English: the other side's language is not known.
  • A status or error message is put into words when it happens. Switching language during a transfer changes the page at once, and every message after the switch comes in the new language, but the one already on screen stays in the old language until the next one replaces it.

Documentation

License

MIT

About

End-to-end encrypted peer-to-peer file and folder transfer, in the browser or a terminal on Linux, macOS and Windows. WebRTC over rotating-PIN Nostr signaling, hand-carried codes, or a Tor onion service. A static site and one self-contained CLI: no backend, no accounts.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages