Security
How WolfWave keeps your Twitch and Discord tokens safe. MacOS Keychain storage, App Sandbox, OAuth Device Code flow, and EdDSA-signed Sparkle updates.
Your tokens never leave your Mac. All sensitive data is handled using macOS system APIs. Keychain, App Sandbox, OAuth Device Code, and EdDSA-signed updates.
Listening History (Opt-In, On-Device)
The Listening History + Stats + Monthly Wrap features are off by default and never run unless you turn them on in Settings → History & Stats or during onboarding.
- On-device only. The play log is an append-only NDJSON file inside the app's sandbox. Nothing is uploaded, no server is contacted.
- Smart recording. A track is only logged after it crosses the half-length or 4-minute mark, so skips don't pad the stats.
- Inspectable. Open the History & Stats settings to see what's stored. The data never leaves the device unless you choose to share an exported Monthly Wrap image.
- Reset anytime. Clearing history removes the entire log; no shadow copies elsewhere.
!stats (Twitch chat) reads from this log and replies only while your stream is live.
Code Signing & Notarization
WolfWave is distributed as a signed and notarized DMG:
- Developer ID Application certificate for distribution signing
- Hardened Runtime enabled with all exceptions explicitly disabled
- App Sandbox enabled with scoped entitlements
- Apple notarization ensures the app is free of known malware
- Stapled ticket allows offline Gatekeeper verification
The app is signed and notarized by Apple. No Gatekeeper warnings on first launch.
Credential Storage
All sensitive credentials are stored in the macOS Keychain:
- Twitch OAuth access token. Used for Helix API + EventSub
- Twitch refresh token. For silent token refresh
- Twitch broadcaster + bot account info. IDs and usernames associated with the connected account
The local WebSocket server binds to all interfaces so a second-PC OBS or a phone browser on the same LAN can connect. Every accepted connection. Loopback or LAN. Must present the wolfwave.token.<hex> Sec-WebSocket-Protocol token, minted on first launch and stored in the macOS Keychain. See WebSocket Server Security for details.
Tokens are never written to UserDefaults or stored on disk in plain text.
Keychain Integration
The KeychainService provides a secure interface for credential management:
// Store a credential
KeychainService.shared.store(token: "your-token", for: .twitchOAuth)
// Retrieve a credential
let token = KeychainService.shared.retrieve(for: .twitchOAuth)
// Delete a credential
KeychainService.shared.delete(for: .twitchOAuth)Twitch Authentication
WolfWave uses the OAuth Device Code Flow for Twitch authentication:
- The app requests a device code from Twitch
- User is directed to authorize on Twitch's website
- The app polls for completion and receives tokens
- Tokens are securely stored in Keychain
This flow:
- Never requires the user to enter credentials in the app
- Uses Twitch's secure authorization pages
- Provides standard OAuth2 token security
Token Validation
On app launch, stored tokens are validated:
- Invalid or expired tokens trigger re-authentication
- Token refresh is handled automatically when possible
- Users are prompted to re-authorize if needed
Build Configuration
API keys are stored in Config.xcconfig:
- This file is gitignored to prevent accidental commits
- Each developer uses their own keys
- Values are baked into
Info.plistat build time
| Key | Purpose | Sensitivity |
|---|---|---|
TWITCH_CLIENT_ID | Identifies the app to Twitch | Public (not a secret) |
DISCORD_CLIENT_ID | Identifies the app to Discord | Public (not a secret) |
GITHUB_REPO_OWNER | GitHub owner used for Sparkle updates + issue links | Public (branding) |
GITHUB_REPO_NAME | GitHub repo name used for Sparkle updates + issue links | Public (branding) |
DOCS_URL | Documentation site root; privacy/terms/changelog derive from it | Public (branding) |
COMMUNITY_DISCORD_URL | Community Discord invite in tray menu | Public (branding) |
COPYRIGHT_HOLDER | Legal entity in About + Monthly Wrap footers | Public (branding) |
All values are public. OAuth client identifiers and branding strings, not
secrets. The branding keys (DOCS_URL, COMMUNITY_DISCORD_URL,
COPYRIGHT_HOLDER, plus the two GITHUB_REPO_*) are optional fork
overrides; each falls back to the upstream MrDemonWolf default if blank.
Entitlements
WolfWave runs inside the macOS App Sandbox with the minimum required entitlements:
| Entitlement | Purpose |
|---|---|
app-sandbox | Required for notarization |
network.client | Twitch API, WebSocket, iTunes artwork API |
automation.apple-events | ScriptingBridge to Apple Music |
scripting-targets | Scoped to Music.app only |
keychain-access-groups | Secure credential storage |
temporary-exception.sbpl | Discord IPC socket access (narrowed to specific socket path in v1.0.1) |
WebSocket Server Security
The built-in WebSocket server binds to all interfaces (LAN-reachable) so two-PC streaming setups and phone browsers can connect. Access is gated by a per-install auth token:
- The token is a 32-byte random hex string, minted on first launch by
WebSocketAuthToken.currentOrCreate()and persisted in the macOS Keychain. - Every accepted connection. Loopback and LAN. Must present
Sec-WebSocket-Protocol: wolfwave.token.<hex>on the handshake; the listener rejects everything else. WidgetHTTPServiceserveswidget.htmland injects the token automatically for loopback peers; remote browsers must supply?token=…in the URL.- Tokens are redacted to the first 4 characters in log lines, never written in full.
- The Discord IPC channel is a separate local Unix socket (
$TMPDIR/discord-ipc-{0..9}). Never LAN-reachable.
JSON Response Validation
All Twitch API and EventSub JSON parsing uses explicit do/catch with structured logging:
- Malformed or unexpected responses are logged with details rather than silently ignored
- Parse failures are handled gracefully without crashing or leaving the service in a broken state
EventSub Metadata Validation
Every incoming EventSub message is validated before processing:
message_idandmessage_timestampare required on every message- Messages older than 10 minutes are rejected to prevent replay attacks
- Subscription type is verified before processing notifications. Unexpected types are ignored
Network Security
All external network traffic uses encrypted transport:
| Endpoint | Protocol | Purpose |
|---|---|---|
api.twitch.tv/helix | HTTPS | Twitch Helix API |
id.twitch.tv/oauth2 | HTTPS | Twitch OAuth |
eventsub.wss.twitch.tv/ws | WSS | Twitch EventSub WebSocket |
itunes.apple.com/search | HTTPS | Album artwork for Discord Rich Presence |
github.com/MrDemonWolf/wolfwave/releases/... | HTTPS | Sparkle appcast + signed updates |
0.0.0.0:8765 | WebSocket (LAN) | Now-playing broadcast. Token-gated via Sec-WebSocket-Protocol; same-Mac OBS gets the token automatically, remote browsers must supply ?token=… |
0.0.0.0:8766 | HTTP (LAN) | Widget HTML server. Same token-gating rules |
| Local Unix socket | IPC | Discord Rich Presence ($TMPDIR/discord-ipc-{0..9}) |
The WebSocket and Widget HTTP listeners bind to all interfaces so a second-PC OBS or a phone browser on the same network can connect. Connections must present the 32-byte hex auth token (minted on first launch, stored in macOS Keychain, never logged in full).
Sparkle Updates
Sparkle is configured with privacy-conscious defaults:
- EdDSA-signed appcast. Every release is signed with an Ed25519 key; the public key (
SUPublicEDKey) ships inInfo.plistand verifies the signature before any update is applied. - Explicit consent for downloads ,
automaticallyDownloadsUpdates = false. Sparkle never writes bytes to disk until you click Install. - No anonymous system profile transmitted ,
SUSendProfileInfois not enabled, so background checks transmit only the appcast request itself (User-Agent + macOS version, no app-specific telemetry). - DEBUG builds. Sparkle is initialized with
startingUpdater: false; manual checks read from a bundleddev-appcast.xml, so the developer experience never reaches the production feed. - Homebrew installs. Sparkle is disabled entirely; Homebrew manages updates.
Best Practices
When contributing to WolfWave:
- Never log sensitive data - Avoid logging tokens or credentials
- Use KeychainService - Always use the Keychain for credential storage
- Validate inputs - Sanitize user inputs before processing
- Handle errors gracefully - Don't expose internal errors to users
OBS Widget
How WolfWave's now-playing OBS overlay is built. A Tailwind + TypeScript workspace that compiles into a single self-contained widget.html with smooth play/stop transitions.
Changelog
Every WolfWave release. New features, fixes, and breaking changes. Auto-updating via Sparkle. macOS menu bar app for Apple Music streamers.