This commit is contained in:
Akshay Kolli
2026-06-30 01:12:19 -07:00
commit 4c1c6b2f37
55 changed files with 13180 additions and 0 deletions

55
docs/ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,55 @@
# Architecture
ClipBored is a single-process AppKit utility built with Swift Package Manager.
## Runtime Shape
- `ClipBoredApp` creates `NSApplication`, sets accessory activation, installs `AppDelegate`, and starts the run loop.
- `AppDelegate` wires shared services, status menu items, settings observers, and global shortcuts.
- `ClipboardMonitorService` polls `NSPasteboard.changeCount` on a utility queue with adaptive active/idle intervals.
- `ClipboardStore` keeps the in-memory item list and persists rows to SQLite on a serial queue.
- `ClipboardCacheService` stores bounded image previews under Application Support and keeps a small `NSCache`.
- `ShortcutManager` registers Carbon hotkeys for app-wide commands.
- `ClipboardPanelController` owns the bottom panel lifecycle and target-app tracking.
- `ClipboardPanelViewModel` filters, sorts, selects, copies, pastes, pins, organizes, deletes, opens, and reveals items.
- `SettingsWindowController` exposes native controls for capture, privacy, performance, shortcuts, and data management.
## Data Flow
1. The monitor notices a pasteboard change.
2. Source app metadata is checked against ignored apps.
3. Pasteboard content is normalized into a `ClipboardItem`.
4. Sensitive text is skipped when exclusion is enabled.
5. Copied images run local Vision OCR only when `Search in image labels` is enabled.
6. The store deduplicates, preserves pinned and collection-assigned items, enforces limits, and persists the mutation.
7. The panel view model receives store updates and recomputes the visible list.
## Persistence
History is stored in SQLite at:
```text
~/Library/Application Support/ClipBored/history.sqlite
```
Images are stored under:
```text
~/Library/Application Support/ClipBored/images/
```
Restorable non-image payloads such as audio clips, rich text, and PDFs are stored under:
```text
~/Library/Application Support/ClipBored/attachments/
```
Legacy JSON import still exists for migration from early builds.
Textual SQLite fields, including optional collection names and image OCR text, are encrypted and decrypted at the `ClipboardStore` boundary. App-managed image cache files, URL preview thumbnails, audio clips, rich text sidecars, and PDF attachments are encrypted and decrypted at the `ClipboardCacheService` boundary. The encryption key is stored in Keychain when available, with an owner-only app-local fallback key if Keychain access blocks or fails. Full history clears remove the local fallback key when present and reset cached key state after SQLite deletion succeeds. Runtime `ClipboardItem` values remain plaintext in memory so search, duplicate detection, copy, paste, organization, and cache cleanup operate normally. Opening or revealing encrypted media creates a temporary decrypted copy for macOS handoff; stale temporary previews are cleared on launch, cache/history clear, and quit.
## Size And Power Constraints
The release build intentionally avoids SwiftUI, Combine, Swift Concurrency, third-party packages, bundled media, and app resources beyond `Info.plist`.
The build script uses `-Osize`, whole-module optimization, disabled reflection metadata, linker dead stripping, symbol stripping, and hardened-runtime signing. The current public targets, enforced by `scripts/build-macos-app.sh`, are a 1 MiB executable and a 1.8 MB app bundle.

82
docs/RELEASE.md Normal file
View File

@@ -0,0 +1,82 @@
# Release Guide
This guide covers the local release build, optional Developer ID signing, and optional notarization flow.
## Local Validation
Run:
```bash
./scripts/check.sh
```
This runs the unit test suite, builds `build/ClipBored.app`, applies an ad-hoc hardened-runtime signature, enforces size gates, and verifies the app signature.
## Local Archive
Run:
```bash
./scripts/release-macos-app.sh
```
Without signing credentials, this creates:
```text
build/ClipBored.app
build/ClipBored.zip
```
The app remains ad-hoc signed and is suitable for local validation only.
## Developer ID Signing
Set a Developer ID Application identity:
```bash
export DEVELOPER_ID_APPLICATION="Developer ID Application: Example, Inc. (TEAMID)"
./scripts/release-macos-app.sh
```
The script rebuilds the app, re-signs it with hardened runtime and timestamping, verifies the signature, and writes `build/ClipBored.zip`.
## Notarization
Preferred: configure a notarytool keychain profile once:
```bash
xcrun notarytool store-credentials "clipbored-notary" \
--apple-id "developer@example.com" \
--team-id "TEAMID" \
--password "app-specific-password"
```
Then run:
```bash
export DEVELOPER_ID_APPLICATION="Developer ID Application: Example, Inc. (TEAMID)"
export NOTARYTOOL_PROFILE="clipbored-notary"
./scripts/release-macos-app.sh
```
Alternative environment-only notarization:
```bash
export DEVELOPER_ID_APPLICATION="Developer ID Application: Example, Inc. (TEAMID)"
export APPLE_ID="developer@example.com"
export APPLE_TEAM_ID="TEAMID"
export APPLE_APP_SPECIFIC_PASSWORD="app-specific-password"
./scripts/release-macos-app.sh
```
When notarization succeeds, the script staples the ticket to `build/ClipBored.app`, validates the staple, and recreates `build/ClipBored.zip`.
## Final Manual Checks
Before publishing, run the checklist in [SMOKE_TEST.md](SMOKE_TEST.md), then confirm:
```bash
codesign --verify --deep --strict --verbose=2 build/ClipBored.app
xcrun stapler validate build/ClipBored.app
spctl --assess --type execute --verbose=4 build/ClipBored.app
```

26
docs/ROADMAP.md Normal file
View File

@@ -0,0 +1,26 @@
# Roadmap
This roadmap keeps future work aligned with the project's constraints: small executable, low idle power, local-only storage, native macOS UI, and no feature regressions.
## Near Term
- Add an idle power measurement note using Instruments or Activity Monitor alongside `scripts/idle-soak-report.sh`.
- Add tagged release notes once a first public version is cut.
## Privacy And Security
- Keep improving secure cleanup semantics for cleared cache/history/key material where macOS storage behavior allows it.
- Keep the current no-network/no-telemetry posture unless the project explicitly changes direction.
## Product Polish
- Improve keyboard focus states and VoiceOver labels.
- Add a compact mode for narrower displays.
- Add import/export only if the storage and privacy story remains clear.
## Performance
- Keep measuring binary size after each feature.
- Avoid continuous background file scans.
- Revisit polling intervals only with measured idle wakeup evidence.
- Keep image decoding lazy and cache bounded.

54
docs/SECURITY.md Normal file
View File

@@ -0,0 +1,54 @@
# Security Notes
ClipBored is designed as a local macOS utility. Its primary privacy promise is that clipboard data stays on the machine.
## Current Protections
- No networking or telemetry in production source.
- No shell/process execution.
- No Apple Events scripting.
- Hardened runtime is applied by the local build script, and the release script supports Developer ID signing plus notarization when credentials are configured.
- Clipboard persistence uses prepared SQLite statements and bound values.
- Textual SQLite fields, including optional local image OCR text, are encrypted with AES-GCM using a Keychain-held key when Keychain access is available.
- App-managed image cache files, audio clips, rich text sidecars, and PDF attachments are encrypted with the same encryption service.
- If Keychain access blocks or fails, ClipBored uses an owner-only app-local fallback key so clipboard capture and persistence continue without a Keychain UI stall.
- Full history clears remove the app-local fallback key when present and reset cached key state after the database clear succeeds.
- App-owned storage directories are restricted to the current user, and saved history/cache files are written with owner-only permissions where the filesystem supports POSIX modes.
- ClipBored marks its own pasteboard writes so copy/paste actions from history are not re-captured as new clipboard events.
- Sensitive-content exclusion can skip common high-risk values:
- private key blocks
- bearer tokens
- GitHub tokens
- Slack tokens
- AWS access key IDs
- Stripe keys
- OpenAI-style API keys
- Google API keys
- JSON Web Tokens
- Luhn-valid credit-card-like values
- OTP-like values from known authenticator/password-manager sources
- long high-entropy token-like strings
- obvious password/secret keywords and common secret assignment forms
- Default ignored apps include common password managers and authenticators.
## Known Limitations
- SQLite item metadata such as identifiers, kinds, timestamps, pin state, and use counts is not encrypted.
- The app-local fallback key prevents plaintext app-managed history/media files, but it does not protect against a process or user account that can read the full ClipBored Application Support directory before history is cleared.
- Opening or revealing encrypted images, audio clips, or PDFs creates temporary decrypted preview files so macOS can hand them to other apps. ClipBored clears stale preview files on launch, cache/history clear, and quit.
- Existing plaintext SQLite rows and legacy sidecar files are migrated when encryption becomes available, but system snapshots, backups, live temporary previews, or filesystem remnants may retain older plaintext copies.
- The local development build is ad-hoc signed; use `scripts/release-macos-app.sh` with Developer ID credentials for notarized distribution builds.
- Accessibility permission is required for automatic paste simulation.
- Sensitive-content detection is heuristic and can miss novel formats or produce false positives.
- Local image OCR is opt-in through `Search in image labels`; recognized text stays local but can still contain sensitive clipboard-derived content.
- Local filesystem access by another process or user account with sufficient permissions can expose metadata, fallback keys, and live temporary decrypted previews.
## Release Hardening Checklist
- Run `swift test -q`.
- Run `./scripts/build-macos-app.sh` or `./scripts/release-macos-app.sh`.
- Verify `codesign --verify --deep --strict --verbose=2 build/ClipBored.app`.
- Verify hardened runtime appears in `codesign -d --verbose=4 build/ClipBored.app`.
- For distribution, verify `xcrun stapler validate build/ClipBored.app` and `spctl --assess --type execute --verbose=4 build/ClipBored.app`.
- Confirm no new `URLSession`, process execution, Apple Events, telemetry, or remote sync APIs were introduced.
- Review any new persistence paths for unencrypted sensitive data.

86
docs/SMOKE_TEST.md Normal file
View File

@@ -0,0 +1,86 @@
# Manual Smoke Test Checklist
Use this checklist before a release or after changes to panel, pasteboard, settings, permissions, storage, launch-at-login, or packaging behavior.
## Setup
1. Build the app:
```bash
./scripts/check.sh
```
2. Quit any running ClipBored copy.
3. Open `build/ClipBored.app`.
4. Confirm ClipBored appears in the menu bar when `Show ClipBored in the menu bar` is enabled.
## Capture
1. Copy plain text from TextEdit, Notes, or a browser.
2. Open the panel with `Command + Option + V`.
3. Confirm the copied text appears in Most Recent.
4. Copy a URL and confirm it appears as a Link; if the source provides a local preview image, confirm the Link card uses that preview.
5. Copy an image and confirm it appears as an Image with a thumbnail.
6. Enable `Search in image labels`, copy an image containing readable text, and confirm searching for that text finds the Image.
7. Copy a sound clip and confirm it appears as Audio.
8. Copy a PDF or PDF selection and confirm it appears as a PDF.
9. Copy one Finder file and confirm it appears as a File.
10. Copy multiple Finder files at once and confirm they appear as one grouped File item with the file count.
11. Copy formatted text from a browser or Mail message and confirm it appears as Rich Text rather than flattened plain text.
12. Disable Images, Audio, Rich Text, PDFs, or Files in Settings > Capture, copy that type again, and confirm it is not captured.
## Panel
1. Open the panel and confirm the search field is focused.
2. Type a query and confirm results filter immediately.
3. Use arrow keys to move selection while the search field is focused.
4. Press `Esc` once with a non-empty search field and confirm search clears.
5. Press `Esc` again and confirm the panel closes.
6. Reopen the panel, change sort segments, and confirm each segment updates results.
7. Right-click a card, choose Add to Collection > New Collection..., enter `Client Work`, and confirm a Client Work chip appears with the item count.
8. Right-click another card and confirm Add to Collection offers Client Work as a reusable destination.
9. Select the Client Work chip and confirm the rail filters to assigned items; quit and reopen ClipBored and confirm the assignment persists.
10. Double-click an item and confirm it attempts to paste or falls back to copy without creating a duplicate history entry.
## Copy And Paste
1. Select a text item and press the Copy button. Confirm the system clipboard contains that text.
2. Select a URL item and confirm the system clipboard contains both string and URL data by pasting into a browser address bar.
3. Select one-file and multi-file File items and paste into Finder or an app that accepts file references. Confirm all files are preserved for the multi-file item.
4. Select an audio item and paste into an app that accepts sound pasteboard data.
5. Select a PDF item and paste into Preview, Finder, or an app that accepts PDF pasteboard data.
6. Select a rich text item and paste into TextEdit rich text mode or Mail. Confirm basic formatting is preserved and plain-text paste still works in a text-only field.
7. Without Accessibility permission, confirm paste actions copy and show the permission fallback status.
8. With Accessibility permission granted, confirm paste returns focus to the previous app and inserts the selected item.
## Settings
1. Open Settings with `Command + ,`.
2. Change history length, default sort, polling profile, cache limit, ignored apps, and allowed content types; quit and reopen the app; confirm settings persist.
3. Change the open-panel shortcut and confirm the old shortcut no longer opens the panel and the new shortcut does.
4. Toggle `Pause clipboard capture`, copy text, and confirm paused capture does not record it.
5. Toggle `Exclude likely secrets`, copy a representative token, and confirm it is not recorded.
6. Use `Open Accessibility Settings` and confirm System Settings opens to the permission area or fallback settings app.
7. Use `Clear Clipboard History` and `Clear Thumbnail Cache`; confirm each shows a warning confirmation before deleting data.
## Storage And Privacy
1. Open the data folder from Settings > Data.
2. Confirm `history.sqlite` exists after capture.
3. Copy unique text and confirm `strings ~/Library/Application\ Support/ClipBored/history.sqlite | grep "unique text"` does not find it.
4. Copy uniquely identifiable rich text/audio/PDF data and confirm `strings ~/Library/Application\ Support/ClipBored/attachments/* | grep "unique text"` does not find it.
5. If `history-encryption.key` exists, confirm it is readable only by the current user.
6. Confirm image files are under `images/` and rich text/audio/PDF attachments are under `attachments/`.
7. Confirm app storage is local to `~/Library/Application Support/ClipBored`.
8. Open or reveal an encrypted image/audio/PDF, then quit ClipBored and confirm `/tmp/ClipBored/Previews` is removed.
9. Use `Clear Clipboard History` and confirm saved history, app-managed attachments, temporary previews, and `history-encryption.key` are removed when that fallback key exists.
10. Confirm quitting with `Clear history on quit` enabled removes history and app-managed cache/attachment files.
## Launch And Lifecycle
1. Enable Launch at Login, log out and back in, and confirm ClipBored starts.
2. Disable Launch at Login and confirm it no longer starts after the next login.
3. Right-click the menu-bar icon and confirm the status menu opens with capture state, clip count, Show Clipboard, Settings, Pause/Resume Capture, and Quit.
4. Control-click the menu-bar icon and confirm the same status menu opens without toggling the panel.
5. Toggle Pause/Resume Capture from the status menu and confirm the status row changes.
6. Quit ClipBored from the menu bar and confirm no `ClipBored` process remains.