Eight CE review agents audited v3.0 in parallel. Found and fixed: (1) P0 self-await deadlock in finalize() — `writePromise = writePromise.then(() => finalize())` while finalize internally awaited writePromise → recipient hung at "Finalizing…" forever, never closing the sink. Dropped the redundant await; the chain itself serializes ordering. This was a complete success-path break — every transfer would have hung. (2) P1 security: recipient now enforces its own deriveCaps().recipientMax against sender-declared metadata.size, rejecting upfront with a clear browser-upgrade hint instead of OOM-crashing mid-transfer on the memory path. (3) High: handleCancel during cold-start now sets startupAbortedRef so a Cancel mid-await actually revokes the beam doc instead of leaking it. (4) High: public accept() now has an idempotency latch so React Strict Mode double-invoke / programmatic re-entry can't open two save pickers or leak the first FSA writable. The latch resets on save-picker cancel so the user can retry. (5) P1 reliability + security: new sweepOrphanedOpfsBeams() runs on every recipient page load — evicts beam-*.tmp files older than 30 minutes from OPFS so partial plaintext from a prior tab crash doesn't persist on shared devices. (6) parseMetadata now validates baseIV decodes to exactly 8 bytes (11 b64u chars) — catches malformed envelopes at the trust boundary. (7) BEAM_WIRE_FORMAT constant extracted from four duplicated string literals. (8) Sender + recipient chat dispatch converted from if-chains to exhaustive switch with never default — adding a new BeamChatFrame variant becomes a compile error instead of a silent drop. (9) chatChannel.onclose handlers now gated by terminated/aborted so successful transfers don't flip the chat pill to red "Disconnected" on the post-success teardown. All 258 tests still passing (209 frontend + 49 functions). Deferred to v3.1 with empirical groundwork: signal-based backpressure across the chat channel for slow-disk recipients, Wake Lock + visibilitychange for multi-GB mobile transfers, navigator.storage.estimate() quota probe at sink open, ciphertextSize ceiling unification across server + tryFetch + connectAndNegotiate, and the QR code on /beam (flagged as MVP gap for mobile-to-mobile pasting).
File caps tier up to 10 GB by detected browser capability — and the server bandwidth cost stays exactly zero because the bytes still flow direct browser-to-browser over WebRTC. Three recipient code paths: (1) Chrome/Edge/Opera get the File System Access path — the recipient picks the save location via showSaveFilePicker and we stream decrypted chunks straight to disk via a WritableStream, ~10 KB peak memory regardless of file size; (2) Firefox/Safari modern get the OPFS path — chunks land in an origin-private file then trigger a browser download, ~2× disk during transit but bounded RAM; (3) older browsers fall back to the current in-memory path with a capability-tightened cap (500 MB desktop, 250 MB mobile). The sender now reads via file.stream() instead of file.arrayBuffer(), so its memory peak is also a single chunk regardless of file size. Wire format upgrades to chunked-v1: each 64 KB plaintext chunk gets its own AES-GCM box with a deterministic IV (8-byte session-random base concatenated with a 32-bit big-endian chunk counter — 2^96 distinct IVs per key, well beyond any practical collision concern). New @ghostx/crypto primitives: generateChunkBaseIV / deriveChunkIV / encryptChunk / decryptChunk. Sender and recipient both use the WebRTC DataChannel's bufferedAmountLow event for backpressure-aware streaming. Capability detection module (./capabilities) returns the receivePath / cap / human-readable label plus an upgradeHint() — the sender page now displays "Up to {X}" derived from caps + a "Want larger files?" line that points users on suboptimal paths at Chrome/Edge/Opera on desktop. Server's MAX_FILE_PLAINTEXT_BYTES rises to 10 GB (was 500 MB) with a chunked-tag overhead buffer. Tests: 36 crypto tests now cover deriveChunkIV, encryptChunk/decryptChunk round-trips, IV uniqueness, wrong-index/wrong-base rejection, tampered-ciphertext rejection. parseMetadata tests rewritten against the chunked-v1 envelope shape with fmt+baseIV+chunkSize boundary cases. 209 frontend + 49 functions = 258 tests passing. Breaking wire format vs v2.27 — beams created on v3.0 use chunked-v1 envelopes and won't be readable by v2.x clients, but since beams expire in 15 min and require both peers online simultaneously, there's no real cross-version compatibility window in practice.
Previously chat only worked while the file was streaming. Now WebRTC auto-connects the moment the recipient clicks through the 'Continue to open the beam' gate, so the chat channel is alive before they see the file. Both sides can message each other while the recipient reviews the metadata + decides whether to accept. Hitting Accept now fires a 'go' control frame over the chat channel (instead of triggering the SDP handshake — that already happened in the background); the sender starts streaming on receipt. Chat keeps working through streaming, after the file lands, and until either tab closes or the 15-minute beam expires. Wire envelope is a tagged union {kind: 'msg'|'go'|'typing'}: text messages, the start-the-transfer control signal, and typing indicators all multiplex onto the single 'beam-chat' DataChannel, AES-GCM-256 encrypted with the same URL-fragment key. New presence: 'is typing…' indicator on both sides (debounced 1.5s idle), persistent connection pill that flips Connected → Disconnected on channel close so you know when the other side actually leaves vs is just quiet. Capability detection + chunked AEAD for 10GB+ files is queued for v3.0 — those two ship together so detection routes to a real code path instead of being informational UI.
Both sides of a beam now know when the other is alive. Sender sees 'Recipient online' the instant the recipient opens the link (via a new markBeamRecipientOpened Cloud Function that stamps a first-write-wins timestamp on the beam doc), and flips to 'Recipient connected' once the WebRTC DataChannel opens. Recipient sees the same connection status mirrored on their side. Alongside the file channel, GhostBeam now opens a second WebRTC DataChannel labelled 'beam-chat' — both sides can send short text messages back and forth during and after the transfer. Messages are AES-GCM-256 encrypted with the same URL-fragment key as the file payload and stream direct browser-to-browser; the chat content never touches our servers. Same hardening pass as the file channel: terminated/aborted latches, cleanup detaches handlers, length-capped at 2000 chars. Recipient surfaces (/send/:id, /beam/:id) also gained a discreet 'Powered by GhostX' footer linking to sibling tools — the recipient just had a great free experience, no signup required, and that's the warmest possible cross-promotion moment. Plus second-pass review fixes: timingSafeEqual on ownerToken compare with hex-shape precheck, strict typeof-string already-answered guard, submitBeamAnswer rate-limit consistent with createBeam, reapExpiredBeams retryCount=1, recipient ciphertextSize bounds at fetch, sender teardownTimer 5s→15s for low-end decrypt, getDoc 750ms retry on stale not-found, exhaustive mapWirePhase switches, mobile-OOM warning in AcceptCard, full unit coverage on parseMetadata + parseSessionDescription + CF helpers (BEAM_ID_RE tightened to match the alphabet exactly).
New product at /beam. Drop a file, share a one-time link, the recipient opens it, and the file streams direct browser-to-browser over WebRTC. The bytes never touch our servers — we mediate only an AES-GCM-encrypted SDP handshake. AES-GCM-256 key lives only in the URL fragment; both the file metadata and the SDP envelope are encrypted before they reach Firestore. Up to 500 MB per beam, 15-minute beam lifetime, both sender and recipient must be online simultaneously. NAT-traversal via Google STUN; clients behind symmetric NATs get a clear fallback message pointing at GhostSend. No TURN server (intentional — keeps the product free without ongoing bandwidth cost).
Removed the legacy ghostsign.app entries from the callable CORS allowlist, the Cloud Storage bucket CORS, and the Firebase Hosting `ghostsign-redirect` target. ghostx.tools is the only domain we serve from now on. Test fixtures and press-page copy updated to match. The legacy `ghostsign-redirect.web.app` site is still live in Firebase but no longer wired into this repo's deploys.
GhostSend now supports an optional password on every send. The link is one factor, the password (shared on a different channel) is the second — a leaked link alone won't open the payload. AES key is derived from the password via PBKDF2-SHA256 with 600,000 iterations (~250 ms on a laptop, ~1-3 s on mobile) so brute-forcing a leaked ciphertext is impractical. The salt rides in the URL fragment with a `p1:` version prefix; the password itself never crosses the wire. Wrong-password attempts get a fresh prompt without re-burning the share. Reaper schedule cut from hourly to every 5 minutes — unconsumed shares get deleted from our servers within ~5 min of TTL elapsing instead of up to 2 hours. GhostBeam queued as the next product: WebRTC peer-to-peer file transfer where the bytes stream directly browser-to-browser and never hit our servers.
Merged GhostNote and GhostShare into a single product at /send. One composer, two modes: paste a secret (text) or drop a file up to 100 MB. Same trust model — AES-GCM-256 in your browser, the decryption key never reaches our servers (it lives only in the URL fragment), atomic burn-after-read on first open, 1h / 24h / 7d expiry. Two SEO landing pages — /privnote-alternative and /wetransfer-alternative — render the same composer with competitor-targeted comparison tables and FAQs. Legacy /note and /share routes 301 to /send so existing links still resolve. Trust copy now explicitly contrasts GhostSend vs WhatsApp / Snapchat / iMessage / iCloud where the operator sees or retains the bytes.
Drop a file up to 100 MB. Get a self-destructing link. The recipient downloads it once, then it's gone forever. AES-GCM-256 encrypted in your browser; both the file bytes AND the filename are encrypted before they leave your machine. The decryption key lives only in the URL fragment so our servers never see it. Pick a 1h / 24h / 7d expiry — whichever comes first, the row + the ciphertext blob are deleted. No signup, no email, no logs.
Opening the Products menu now starts prefetching the JS chunks for sibling products in the background, so clicking through to GhostPDF / GhostQR / GhostNote / GhostSheet feels instant instead of waiting on a chunk fetch. Uses `requestIdleCallback` so it never competes with the page you're actually on, and only fires once per session per product.
Replaced the npm xlsx@0.18.5 dependency with the SheetJS CDN tarball v0.20.3, which closes CVE-2023-30533 (prototype pollution) and the known malformed-xlsx denial-of-service path. Bounded sheet parsing at 5,000,000 cells so a malicious workbook can't OOM the tab via a billion-row `!ref` header. Pasted HTML is now rejected outright (the paste box is for spreadsheet text, not markup). Formula cells are stripped before export so a re-shared file can't carry a smuggled =cmd|… formula. Cell edits now namespace by sheet (no more cross-sheet edit highlights), preserve leading-zero strings (ZIP codes / SSNs stay strings), guard against IME composition (Japanese / Chinese / Korean input no longer drops half-typed text), and force plain-text paste into cells. Extracted a shared `useDropdown` primitive into @ghostx/ui.
Drop an .xlsx, .xls, .csv, .ods, or .tsv (or paste tab-separated data) and see it instantly. Multi-sheet tabs, frozen header, full-text search across cells, click any cell to edit, then download as .xlsx / .csv / .tsv / .json. SheetJS does the parsing entirely in your browser — your file never leaves your machine. No Office install, no Google account, no upload.
Paste a password, an API key, or any secret. Get a self-destructing link. The recipient opens it once, then it's gone forever. AES-GCM encrypted in your browser; the key lives only in the URL fragment so our servers never see the plaintext. Pick a 1h / 24h / 7d expiry — whichever comes first, the row is deleted.
Generate styled QR codes for URLs, WiFi networks, vCards, email, SMS, plain text, or calendar events. Logo embed, color customization, PNG / SVG / PDF downloads — all in your browser. No watermark, no tracking pixel, no expiration.
Trimmed the GhostSign homepage by ~35% so the dropzone shows above the fold on more devices. The 'Why is GhostSign free?' section now matches the GhostPDF hub layout — same three cards, same vision. Verify link slid one spot left in the header so the most-used signing actions sit closer to the logo.
Refactored every PDF tool page to use a new `useObjectUrl` / `useObjectUrls` hook pair. Removes ~70 lines of repetitive boilerplate per page, eliminates a class of subtle memory-leak bugs around `URL.revokeObjectURL`, and clears the entire batch of React Compiler lint warnings.
GhostSign and GhostPDF now own their own chrome. The top-left logo, the cross-product link, and the donate popup all adapt to whichever surface you're on. Foundations for adding GhostNote / GhostShare etc. without touching layout code.
Public press kit at /press with brand assets, key facts, and pull-quotes. Added humans.txt, .well-known/security.txt, and the IndexNow API key so Bing, Yandex, and DuckDuckGo can be pinged on every deploy instead of waiting for their own crawl schedule.
Every /pdf/* tool now ships HowTo + FAQPage + BreadcrumbList JSON-LD into the initial HTML, plus a body section with 'How it works', 6 FAQs, and a related-tools rail. Tightened 16 oversized titles under Google's truncation budget.
Compress, merge, split, rotate, organize pages, add page numbers, watermark, crop, protect with a password, unlock, strip metadata, convert PDF↔Word, PDF↔Image, and extract text — every tool runs entirely in your browser. No upload endpoint. Free forever.
GhostSign code reorganized into a pnpm workspace with shared `@ghostx/*` packages (theme, brand, UI primitives, PDF core, signature, crypto, certificate). Sets up the family so each new product (GhostPDF, GhostNote, …) can share the same infrastructure without copy-paste.
GhostSign got the ability to send documents to other people for signature. The whole flow is AES-GCM-256 encrypted in your browser; the decryption key lives only in the magic-link URL fragment, which by web spec never reaches a server. No accounts for senders or recipients. Auto-deletes in 7 days.
Free in-browser PDF signing — no account, no upload, no watermark. Solo signing is 100% client-side: drop the PDF, place your signature, download the signed file with an audit-trail certificate. Built because every other free e-sign tool wanted our credit card.