Home is now two sections, no clutter:
- Nearby — tap a device on the same Wi-Fi and the composer takes its
place (drop files, optionally write a note, send). Pair-across-networks
and public-room panels moved off the main page into the footer as a
single discrete link (/pair).
- Share a link — inline WeTransfer-style card. Pick a file, optionally
expand email / password / expiry, upload. Result shows QR + copy link
+ the password to share out-of-band.
The password gate is a server-side access control layer on top of the
existing E2E encryption: scrypt-hashed on create, verified on consume.
The encryption key still lives only in the URL fragment; the password
does not participate in the crypto.
- server: password_hash column (migration 0002), scrypt+timingSafeEqual
verify on /consume, requiresPassword surfaced on the HEAD response.
- web: Composer merges drop zone + text note into one surface; Receive
shows a password prompt when requiresPassword is true and recovers in
place on a wrong attempt.
- responsive: mobile-first paddings, DeviceChip shrinks on narrow widths,
no 2-col grids on the main page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Drop the standalone MinIO StatefulSet — the cluster already runs one
in the `minio` namespace, exposed at minio.arthurbarre.fr. Use that
with a scoped anydrop user + bucket instead of spinning up a second
instance.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds a "Via AnyDrop" flow for senders who need to reach someone not
present on the mesh. The file is sealed client-side (XChaCha20-Poly1305),
uploaded directly to an in-cluster MinIO bucket via a presigned PUT, and
handed off to the recipient as a URL whose fragment carries the key.
The server only ever sees ciphertext, opaque metadata blobs, and sizes.
- server: transfers table (drizzle migration), /api/transfers CRUD +
consume endpoint, presigned PUT/GET via @aws-sdk/client-s3, cleanup
loop that purges expired + exhausted blobs.
- web: @noble/ciphers sealFile/openFile, high-level sendCloud/receive
helpers, CloudSharePanel on Home, /r/:id receive page, /inbox page
for signed-in users (sent + received tabs).
- k8s: MinIO StatefulSet with bucket-init initContainer, S3 env vars
on the server Deployment (credentials pulled from minio-credentials
Secret).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace generic slate/indigo dark theme with a custom editorial
direction: warm paper neutrals, oxblood signal, Fraunces serif
display, Inter body, JetBrains Mono for codes. SVG paper-texture
noise overlay and thin rules across the app.
Refactored: Home, Settings, JoinRoom, Pair, Share, plus every
modal and panel (DropZone, DevicePairingPanel, PublicRoomPanel,
ProfileSetup, TextShareModal, ReceiveDialog, TransferProgress,
PeerList, PeerAvatar).
Also drops three pre-existing Uint8Array/BlobPart strictness
errors so the production build is green again.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- add top-level `hostname $(hostname)` directive (required by maddy 0.8)
- invoke as `maddy -config … run` (global flag before subcommand)
- fix dkim syntax: `dkim DOMAIN SELECTOR` (no key-size positional arg —
that was being parsed as a second selector, generating bogus keys)
- use bounce block on local_queue target instead of (local_routing) macro
Root cause of the CI "Cannot find module @anydrop/shared" error:
server/tsconfig.tsbuildinfo and shared/tsconfig.tsbuildinfo were checked in.
On the Gitea runner, tsc read those files, concluded the prior (local-machine)
build was still valid, and skipped emitting shared/dist — so there was nothing
to resolve by the time server's tsc ran.
- untrack the two .tsbuildinfo files
- gitignore *.tsbuildinfo
- dockerignore **/dist, **/*.tsbuildinfo (belt-and-suspenders)
- in both Dockerfiles, delete any stray tsbuildinfo + dist dirs before tsc
pnpm's workspace symlink (server/node_modules/@anydrop/shared → ../../../shared)
works locally but breaks on the Gitea Actions runner. TSC resolves the symlink
but cannot read through it, yielding TS2307 on "@anydrop/shared".
Fix: after building shared, copy its package.json + dist into
{server,web}/node_modules/@anydrop/shared as a plain directory before running
the dependent build. Module resolution becomes filesystem-local, independent
of BuildKit layer semantics or storage driver symlink handling.
Uses registerSW to detect new service worker versions and reload
automatically. Defers reload if file transfers are active, checking
every 30s until transfers complete.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Pairing:
- Always request fresh code when opening "Mon code" tab
- Clear stale pairCode on modal close/tab switch
- Show errors inline for invalid codes (don't close modal)
- Delete pair code after first use (server)
- Validate code length server-side
Notifications removed:
- Remove all showLocalNotification calls (peer-joined, transfer, text)
- Push setup now runs only once per session (no memory leak)
Other fixes:
- Clear pendingFilesRef on disconnect
- Set transfer status to "transferring" immediately on send start
- Select offline peer when tapping to wake
- Fix file receiver blob restoration race condition (await arrayBuffer)
- Clear selectedPeerId after share-target send
- Add returnValue to beforeunload handler
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
SignalingClient captured the profile at construction time, so
after pair-code-resolved updated the store, the reconnect still
sent the old groupId. Added updateProfile() method and call it
before reconnecting.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
iOS PWA (home screen) has separate localStorage from Safari, so
QR-based pairing breaks. Now pairing uses a 6-character code:
- Device A taps "Appairer" → shows a code like "A7K9XB"
- Device B taps "Appairer" → "Rejoindre" tab → enters the code
- Server resolves the code to the groupId, devices are linked
Codes expire after 5 minutes. Pairing is permanent.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
iOS Safari ignores wildcard */* in share_target accept.
List specific MIME types and file extensions so AnyDrop
appears in the iOS share sheet for photos, videos, docs, etc.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1. Peers (available devices)
2. Pair device + Public link (side by side buttons, modals for details)
3. File drop + Text send (only when a peer is selected)
4. Transfer progress
Removed the always-visible drop zone and join-code input from
PeerList. Pairing and public room now open as overlay modals.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Transfers now survive page refreshes:
- beforeunload warning prevents accidental refresh during transfer
- Sender's files cached in IndexedDB before sending
- Receiver's chunks persisted in IndexedDB as they arrive
- On reconnect, receiver sends transfer-resume with bytes received
- Sender resumes from offset, skipping already-sent data
- Old transfer data auto-cleaned after 1 hour
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds a groupId-based pairing system so devices can always see
each other regardless of network. Scan a QR code once from the
other device, and they're permanently linked via a shared group
stored in localStorage. No account, no email — just one-time QR
scan like Bluetooth pairing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The ICE candidate regex was matching the wrong part of the candidate
string. Also, iOS Safari blocks local IP detection via WebRTC (mDNS
obfuscation), so when no peers are found, show a prominent "create
link" button and a join-by-code input for easy cross-network pairing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
iCloud Private Relay hides iPhones' real public IP, breaking
IP-based LAN grouping. Devices now detect their local IP
(192.168.x.x) via WebRTC ICE candidates and send it to the
server. The server groups devices by local subnet as a fallback
when public IPs differ.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Offline push-subscribed devices appear dimmed in peer list
- Tap offline peer to send wake push notification
- Skip self-notification (own deviceId excluded)
- iOS/Android share sheet via Web Share Target API
- Online/offline indicator dot on peer avatars
- Web Push API for offline device notifications (VAPID)
- Custom service worker with push + share target handlers
- iOS/Android share sheet support via Web Share Target API
- Dedicated /share page with one-tap send to nearby peer
- Background tab notifications for incoming transfers
- Persistent deviceId per device
- Web Push API for offline device notifications
- Custom service worker with push event handling
- Local notifications for background tab transfers
- VAPID keys in K8s config
- Persistent deviceId per device
- Users set their own device name and optional profile photo
- Profile persisted in localStorage, no account needed
- Auto-detect device type from user agent
- Server validates client-provided profile defensively
- PeerAvatar shows photo or device icon
- ProfileSetup modal on first visit, editable from header
- Users set their own device name and optional profile picture
- Profile persisted in localStorage (no account needed)
- Auto-detect device type from user agent (iPhone, Mac, Android...)
- Server uses client-provided profile instead of generating names
- PeerAvatar shows photo or device icon instead of animal emojis
- ProfileSetup modal on first visit + editable from header