19 Commits

Author SHA1 Message Date
ordinarthur
641ec629f5 feat(billing): track scheduled cancellation + surface it in Settings
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m14s
Stripe portal "cancel" schedules a cancel_at timestamp while keeping the
sub active until period end. Without persisting that state the UI kept
saying "renews on X" even though the user had already canceled.

New `users.plan_cancels_at` column populated from the subscription
webhook (handles both `cancel_at` and legacy `cancel_at_period_end`).
Settings now shows a banner "Subscription scheduled to end on X" and
flips the footer label from "renews" to "ends" while the cancellation
window is open.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 14:01:24 +02:00
ordinarthur
4719d25f8f chore(server): load .env in dev via tsx --env-file
Phase 3 adds Stripe env vars (keys + price IDs) we want hydrated
locally without leaking them into the shell profile.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 13:33:33 +02:00
ordinarthur
dc184c4608 feat(server): stripe billing + plan limits (Phase 3)
- schema: stripe_subscription_id, plan_status, plan_expires_at on users
- lib/plans.ts: Free (2 GB · 7d · 1 dl) vs Pro (20 GB · 90d · 100 dl)
- http/billing.ts: POST /api/billing/{checkout,portal}
- http/webhook.ts: verified Stripe webhook → syncs plan lifecycle
- http/transfers.ts: enforces per-user plan limits instead of hardcoded caps
- http/me.ts: exposes plan + status + planExpiresAt to the client

Ops prerequisite: STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET,
STRIPE_PRICE_MONTHLY, STRIPE_PRICE_YEARLY env vars in anydrop-app-secrets.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 13:25:55 +02:00
ordinarthur
35aca309c9 refactor: remove cross-network device pairing
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 48s
Remove dead pair-code flow end-to-end (client, server, protocol) and
the orphaned PublicRoomPanel/DevicePairingPanel/Pair components that
were no longer reachable from the UI.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 13:18:02 +02:00
ordinarthur
2452f2642a feat: inbox of received transfers in /settings
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 47s
Auto-claim on receive so /settings shows transfers others sent you.
Filename decryption stays client-side using the key stored in localStorage
when the share link is opened.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 12:47:10 +02:00
ordinarthur
dbd500b0b5 feat: simpler Home + password-protected cloud links
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 51s
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>
2026-04-20 11:54:05 +02:00
ordinarthur
0b639dfc3c feat: encrypted cloud relay (Phase 2)
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>
2026-04-20 11:09:58 +02:00
ordinarthur
ef2725aebf fix(build): stop committing tsbuildinfo, wipe before tsc on docker
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m21s
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
2026-04-20 10:11:00 +02:00
ordinarthur
0b90d9cdb5 fix(docker): materialize @anydrop/shared as real dir, not workspace symlink
Some checks failed
Build & Deploy / build-and-deploy (push) Failing after 11s
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.
2026-04-20 10:05:47 +02:00
ordinarthur
2913618ee6 feat: stealth accounts + data layer (Phase 1)
Some checks failed
Build & Deploy / build-and-deploy (push) Failing after 1m47s
2026-04-20 09:57:22 +02:00
ordinarthur
dd37b49ce4 fix: pairing flow + code review bugfixes + remove notifications
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 45s
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>
2026-04-14 13:22:33 +02:00
ordinarthur
d8721b6d0c feat: pairing via short code instead of QR only
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 45s
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>
2026-04-14 13:04:58 +02:00
ordinarthur
d6f7a2374b feat: permanent device pairing for cross-network sharing
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 43s
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>
2026-04-14 12:39:21 +02:00
ordinarthur
99a182a831 feat: LAN detection fallback via WebRTC local IP
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 41s
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>
2026-04-14 12:27:36 +02:00
ordinarthur
d1c4b3d196 feat: offline peers visible + push wake + iOS share sheet + no self-notification
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 44s
- 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
2026-04-14 12:12:08 +02:00
ordinarthur
fd249abbf1 feat: push notifications + background transfer alerts
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m1s
- 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
2026-04-14 12:03:43 +02:00
ordinarthur
e3085ea7a2 feat: local device profile replaces animal naming
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 35s
- 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
2026-04-14 11:53:03 +02:00
ordinarthur
4dbdfedae0 feat: local device profile (name + photo) replaces animal naming
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 43s
- 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
2026-04-14 11:47:38 +02:00
ordinarthur
9d6e4da4ae feat: initial commit with full deployment setup
Some checks failed
Build & Deploy / build-and-deploy (push) Failing after 8s
Includes React PWA frontend, WebSocket signaling server, shared types,
K8s manifests, Gitea CI/CD workflow, nginx config, and Dockerfiles.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-14 10:30:45 +02:00