WIP
This commit is contained in:
55
docs/ARCHITECTURE.md
Normal file
55
docs/ARCHITECTURE.md
Normal 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
82
docs/RELEASE.md
Normal 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
26
docs/ROADMAP.md
Normal 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
54
docs/SECURITY.md
Normal 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
86
docs/SMOKE_TEST.md
Normal 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.
|
||||
Reference in New Issue
Block a user