343 lines
12 KiB
Markdown
343 lines
12 KiB
Markdown
# CleanCopy
|
||||
|
|
|
|||
|
|
CleanCopy is a lightweight, headless Windows utility that sits silently in the system tray, automatically sanitizing your clipboard text on trigger. Designed with a strict focus on privacy, developer ergonomics, and performance, CleanCopy operates with a minimal footprint and compiles to a native installer.
|
|||
|
|
|
|||
|
|
## Features
|
|||
|
|
|
|||
|
|
- Automatically removes marketing/analytics trackers (e.g., `utm_*`, `gclid`, `fbclid`, `si`, `msclkid`) from copied URLs while keeping functional query arguments. Users can also define **custom tracking parameters** via a comma-separated input in the Rules tab.
|
|||
|
|
- Converts curly quotes (`"`/`"`/`'`/`'`), en-dashes (`–`), and em-dashes (`—`) to standard straight quotes (`"`, `'`) and double hyphens (`--`).
|
|||
|
|
- Joins single-newline wraps (frequently introduced by chat apps like WhatsApp or Messenger) into continuous lines while preserving double-newline paragraphs and Markdown layout structure. Also handles **hyphenated line breaks** (e.g., `"inno-\nvation"` → `"innovation"`).
|
|||
|
|
- Removes zero-width spaces, null bytes, and other invisible Unicode characters that can cause issues when pasting.
|
|||
|
|
- Plays a system beep and shows a Windows notification each time the clipboard is sanitized.
|
|||
|
|
- Optional autostart toggle via `tauri-plugin-autostart`.
|
|||
|
|
- A dark-themed settings panel styled around the VS Code _Lavender Dimmed_ color scheme, using the local _CaskaydiaCove Nerd Font_.
|
|||
|
|
- Integrates secure Ed25519-signature-based offline licensing tied directly to a hardware-fingerprinted machine ID (HWID).
|
|||
|
|
|
|||
|
|
## Getting Started
|
|||
|
|
|
|||
|
|
### Prerequisites
|
|||
|
|
|
|||
|
|
1. **Node.js** (v18+)
|
|||
|
|
2. **Rust & Cargo** (latest stable release)
|
|||
|
|
3. **Windows C++ Build Tools** (Visual Studio Build Tools with C++ desktop workload enabled)
|
|||
|
|
|
|||
|
|
### Installation
|
|||
|
|
|
|||
|
|
Clone the repository and install npm packages from the app directory:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
cd app
|
|||
|
|
npm install
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Running in Development
|
|||
|
|
|
|||
|
|
Start the development server (starts Vite and launches the Tauri background tray app):
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
npm run tauri dev
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Testing & Code Quality
|
|||
|
|
|
|||
|
|
### Running Frontend Tests
|
|||
|
|
|
|||
|
|
Run the Vitest test suite covering React components, state management, and UI behavior:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
npm run test
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Linting Frontend Code
|
|||
|
|
|
|||
|
|
Run ESLint across all frontend TypeScript/TSX files:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
npm run lint
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
To auto-fix fixable issues:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
npm run lint:fix
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Formatting Frontend Code
|
|||
|
|
|
|||
|
|
Run Prettier to format all frontend files:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
npm run format
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
To check formatting without writing (useful for CI):
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
npm run format:check
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Running Rust Tests
|
|||
|
|
|
|||
|
|
Run unit tests for clipboard cleansing rules and cryptographic license validation:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
cargo test --workspace
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Running Rust Linter
|
|||
|
|
|
|||
|
|
Verify code quality and check for warnings using Cargo Clippy:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
cargo clippy --workspace -- -D warnings
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Building Frontend
|
|||
|
|
|
|||
|
|
Verify that the TypeScript compilation and Vite assets packager complete without warning:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
npm run build
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Architecture Notes
|
|||
|
|
|
|||
|
|
### Clipboard Simulation
|
|||
|
|
|
|||
|
|
When the global hotkey is pressed (`Alt + Shift + C` by default), CleanCopy uses the Windows `keybd_event` API to simulate a `Ctrl+C` keystroke. It releases the Alt/Shift modifiers first to avoid polluting the copied selection, then waits 150ms for the OS clipboard to update before running the sanitization pipeline.
|
|||
|
|
|
|||
|
|
### Profile-Based Licensing
|
|||
|
|
|
|||
|
|
The file `licensing_profiles.json` is compiled directly into the Rust binary via `include_str!`. Profile selection at runtime:
|
|||
|
|
|
|||
|
|
| Build mode | Profile | Activation URL |
|
|||
|
|
| ----------------------- | ------------- | --------------------------------------------------- |
|
|||
|
|
| `cargo test` | `testing` | `http://localhost:3000/api/activate` |
|
|||
|
|
| `cargo build` (debug) | `development` | `http://localhost:3000/api/activate` |
|
|||
|
|
| `cargo build` (release) | `production` | `https://cleancopy.stevanfreeborn.com/api/activate` |
|
|||
|
|
|
|||
|
|
### License Enforcement
|
|||
|
|
|
|||
|
|
When no valid license is found, CleanCopy locks the UI to the License tab only — the General and Rules tabs are disabled. If the global hotkey is pressed while unlicensed, it opens the settings window to the License tab instead of cleaning the clipboard.
|
|||
|
|
|
|||
|
|
## Licensing System
|
|||
|
|
|
|||
|
|
CleanCopy utilizes a unified, database-free Stripe licensing proxy designed to support a suite of multiple desktop applications.
|
|||
|
|
|
|||
|
|
### The Activation Flow
|
|||
|
|
|
|||
|
|
1. The user buys CleanCopy on your landing page via Stripe Checkout.
|
|||
|
|
2. The user copies their Stripe Payment ID (e.g. `pi_3MtgK...`) or Charge ID (e.g. `ch_3MtgK...`) from their Stripe receipt.
|
|||
|
|
3. The user pastes the Stripe ID into CleanCopy's License tab.
|
|||
|
|
4. CleanCopy pings server. The server:
|
|||
|
|
- Verifies the purchase with Stripe's API.
|
|||
|
|
- Checks that the payment is for the correct `product_id`.
|
|||
|
|
- Signs the user's Hardware ID (HWID) using a private Ed25519 signing key.
|
|||
|
|
- Returns a signed activation token.
|
|||
|
|
5. CleanCopy verifies the signature using the embedded public key, saving it to `license.lic`. On subsequent startups, CleanCopy validates this file offline with zero network requests.
|
|||
|
|
|
|||
|
|
### Local Development Testing
|
|||
|
|
|
|||
|
|
To test the activation flow locally without deploying a live server:
|
|||
|
|
|
|||
|
|
#### Option A: Run the Local Mock Server (Recommended)
|
|||
|
|
|
|||
|
|
1. Start the mock licensing server:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
cd app/src-tauri
|
|||
|
|
cargo run --bin mock_license_server
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- CleanCopy detects debug mode and automatically redirects its requests to `http://localhost:3000`.
|
|||
|
|
- In the Settings -> License UI, enter any key (e.g. `pi_test_12345`) and click **Activate**.
|
|||
|
|
- To test purchase failures, enter a key starting with `pi_invalid` (e.g. `pi_invalid_999`).
|
|||
|
|
|
|||
|
|
#### Option B: Offline Sign & Bypass
|
|||
|
|
|
|||
|
|
- Copy the HWID displayed in Settings -> License.
|
|||
|
|
- Run the license tool to sign it manually with the private key:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
cd app/src-tauri
|
|||
|
|
cargo run --bin license_tool issue 04a0664dfa85f71119e9f4fc971a953ad49f82b752087deab6bee816c1d37d6d <YOUR_HWID>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- Paste the returned token into the activation UI.
|
|||
|
|
- Delete the `license.lic` file from your local AppData directory:
|
|||
|
|
- **Path:** `C:\Users\<user>\AppData\Roaming\com.cleancopy.app\license.lic`
|
|||
|
|
|
|||
|
|
### The CLI License Tool (`license_tool`)
|
|||
|
|
|
|||
|
|
CleanCopy includes a built-in Rust command-line utility `license_tool.rs` for key generation and offline license issuance.
|
|||
|
|
|
|||
|
|
#### Generating Keypairs (`generate-keys`)
|
|||
|
|
|
|||
|
|
Generate fresh, cryptographically secure Ed25519 public/private key pairs. The tool uses `getrandom` to extract secure entropy from the host OS (Windows, macOS, or Linux).
|
|||
|
|
|
|||
|
|
Run the tool with an optional profile name (prefix) and optional target folder:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
cd app/src-tauri
|
|||
|
|
cargo run --bin license_tool generate-keys [profile_name] [output_dir]
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Example:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
cargo run --bin license_tool generate-keys development
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- Generates keys and automatically persists them as `licensing/development.priv` and `licensing/development.pub` under your `.gitignored` project root folder.
|
|||
|
|
- Outputs the key hex values directly to the console.
|
|||
|
|
|
|||
|
|
#### Issuing Offline Activation Keys (`issue`)
|
|||
|
|
|
|||
|
|
Issue signed license tokens manually for air-gapped users, secure enterprise environments, or local automated virtual-machine testing.
|
|||
|
|
|
|||
|
|
Run the tool specifying the profile's private key and the user's Hardware ID (HWID):
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
cd app/src-tauri
|
|||
|
|
cargo run --bin license_tool issue <private-key-hex> <device-hwid> [product_id] [license_key]
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Example:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
cargo run --bin license_tool issue 04a0664dfa85f71119e9f4fc971a953ad49f82b752087deab6bee816c1d37d6d 9a0b396c42da0860b246aaa3b8e0cafb86789e062a8947957bb88a46f08477b6
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- Outputs a period-separated activation token: `PAYLOAD_B64.SIGNATURE_B64`. The customer pastes this token into their Settings panel, which is verified offline against the compiled public key.
|
|||
|
|
|
|||
|
|
#### Purging License Status (`deactivate`)
|
|||
|
|
|
|||
|
|
Easily reset your development machine back to the "Unregistered" state to verify and test the activation UI/flow repeatedly without manually locating files.
|
|||
|
|
|
|||
|
|
Run the tool with the `deactivate` subcommand:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
cd app/src-tauri
|
|||
|
|
cargo run --bin license_tool deactivate
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- Locates the local AppData directory, checks if `license.lic` is present, and deletes it.
|
|||
|
|
|
|||
|
|
#### Diagnostics: Manually Querying a User's HWID
|
|||
|
|
|
|||
|
|
If a customer is offline and cannot run the app to copy their Hardware ID from the License tab, can instruct them to run this single line in **PowerShell**:
|
|||
|
|
|
|||
|
|
```powershell
|
|||
|
|
$guid = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Cryptography').MachineGuid;
|
|||
|
|
$serial = (Get-CimInstance -ClassName Win32_BaseBoard).SerialNumber;
|
|||
|
|
$hasher = [System.Security.Cryptography.HashAlgorithm]::Create('SHA256');
|
|||
|
|
$bytes = [System.Text.Encoding]::UTF8.GetBytes("$guid$serial");
|
|||
|
|
$hash = $hasher.ComputeHash($bytes);
|
|||
|
|
-join ($hash | ForEach-Object { "{0:x2}" -f $_ })
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
This queries the registry and bios serial, SHA-256 hashes them, and prints the matching HWID.
|
|||
|
|
|
|||
|
|
### Key Rotation
|
|||
|
|
|
|||
|
|
If your signing key is compromised or you need to rotate for any reason, CleanCopy supports seamless key rotation via the `deprecated_public_keys` field in `licensing_profiles.json`.
|
|||
|
|
|
|||
|
|
#### How It Works
|
|||
|
|
|
|||
|
|
Each profile contains:
|
|||
|
|
|
|||
|
|
- `public_key_hex`: The current signing key (used for all new activations)
|
|||
|
|
- `deprecated_public_keys`: Array of previous keys that remain valid for existing users
|
|||
|
|
|
|||
|
|
When verifying a license, CleanCopy tries the current key first, then falls back to each deprecated key. Users with tokens signed by an old key continue to work without re-activation.
|
|||
|
|
|
|||
|
|
#### Rotation Steps
|
|||
|
|
|
|||
|
|
- Generate a new keypair:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
cd app/src-tauri
|
|||
|
|
cargo run --bin license_tool generate-keys production
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- Update `licensing_profiles.json`:
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"production": {
|
|||
|
|
"activation_url": "https://cleancopy.stevanfreeborn.com/api/activate",
|
|||
|
|
"public_key_hex": "<new key>",
|
|||
|
|
"deprecated_public_keys": ["<old key>"]
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- Update the signing key on your activation server to use the new private key.
|
|||
|
|
- Rebuild and distribute the updated app. Existing users' licenses validate against the deprecated key. New activations use the current key.
|
|||
|
|
- After the migration period, remove the old key from `deprecated_public_keys` and rebuild.
|
|||
|
|
|
|||
|
|
## Logging and Diagnostics
|
|||
|
|
|
|||
|
|
CleanCopy uses the `tauri-plugin-log` crate for structured logging at the `Info` level.
|
|||
|
|
|
|||
|
|
### Log Output
|
|||
|
|
|
|||
|
|
Logs are written to two destinations simultaneously:
|
|||
|
|
|
|||
|
|
| Destination | Purpose |
|
|||
|
|
| ------------ | -------------------------------------------------------- |
|
|||
|
|
| **Stdout** | Visible in the terminal when running `npm run tauri dev` |
|
|||
|
|
| **Log file** | Persistent file for production builds |
|
|||
|
|
|
|||
|
|
### Log File Location
|
|||
|
|
|
|||
|
|
The log file is named `app.log` and stored in the Tauri log directory:
|
|||
|
|
|
|||
|
|
| Platform | Path |
|
|||
|
|
| -------- | ------------------------------------------ |
|
|||
|
|
| Windows | `%APPDATA%/com.cleancopy.app/logs/app.log` |
|
|||
|
|
|
|||
|
|
### What Gets Logged
|
|||
|
|
|
|||
|
|
- **Startup**: Config loading, license validation, hotkey registration
|
|||
|
|
- **Clipboard events**: Each clean operation with input/output text
|
|||
|
|
- **License activation**: Proxy requests, token verification results
|
|||
|
|
- **Errors**: File I/O failures, signature verification failures, network errors
|
|||
|
|
|
|||
|
|
### Viewing Logs
|
|||
|
|
|
|||
|
|
**Real-time (development):**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
npm run tauri dev
|
|||
|
|
# Logs appear in the terminal
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Log file (production):**
|
|||
|
|
|
|||
|
|
```powershell
|
|||
|
|
# Windows PowerShell
|
|||
|
|
Get-Content "$env:APPDATA\com.cleancopy.app\logs\app.log" -Tail 50 -Wait
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Log Level
|
|||
|
|
|
|||
|
|
The log level is hardcoded to `Info`. To change it for debugging, modify the `.level()` call in `app/src-tauri/src/lib.rs`:
|
|||
|
|
|
|||
|
|
```rust
|
|||
|
|
.level(log::LevelFilter::Debug)
|
|||
|
|
.level(log::LevelFilter::Warn)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Production Distribution
|
|||
|
|
|
|||
|
|
To bundle a production-ready MSI installer and release binary:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
npm run tauri build
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
The compiled assets will be bundled in:
|
|||
|
|
|
|||
|
|
`app/src-tauri/target/release/bundle/msi/`
|