Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version: 5.9
// swift-tools-version: 6.0
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ https://github.com/user-attachments/assets/c57ef6d5-f0a1-4a3f-a121-637533442c24
- **Local-First** — your voice and text never leave your machine unless you opt in to a cloud AI provider
- **Fastest Parakeet on Mac** — one of the fastest native implementations of Parakeet on macOS, with near-instant transcription and minimal latency
- **Configurable Overlay** — choose from pill-shaped to large overlay sizes to show live preview, or keep it minimal. Everything is optional
- **Everything is Optional** — AI enhancement, Fluid Intelligence, audio history, analytics, and beta builds are all opt-in. The core dictation experience works out of the box with zero configuration beyond permissions and a hotkey
- **Everything is Optional** — AI enhancement, Fluid Intelligence, audio history, cloud providers, and beta builds are opt-in. Anonymous health/usage analytics are enabled by default and can be disabled from Settings. The core dictation experience works out of the box with zero configuration beyond permissions and a hotkey

---

Expand Down Expand Up @@ -258,9 +258,9 @@ xcodebuild test -project Fluid.xcodeproj -scheme Fluid -destination 'platform=ma

FluidVoice is **local-first**. Your voice, audio, and transcribed text never leave your machine unless you explicitly opt in to a cloud AI provider.

### What's Collected (Opt-In)
### What's Collected (Default-On, Anonymous)

Anonymous analytics are enabled by default to track app health and feature usage. You can disable at any time from `Settings → Share Anonymous Analytics`.
Anonymous analytics are enabled by default to track app health and feature usage. You can disable them at any time from `Settings → Share Anonymous Analytics`. AI enhancement, audio history, cloud providers, and beta builds remain opt-in.

**Collected:**

Expand Down
6 changes: 2 additions & 4 deletions Sources/Fluid/Services/DebugLogger.swift
Original file line number Diff line number Diff line change
Expand Up @@ -92,13 +92,11 @@ class DebugLogger: ObservableObject {

let formattedLine = self.formatLogLine(timestamp: timestampString, level: level, source: source, message: message)

// Always persist diagnostics so issues can be debugged even if UI debug mode is off.
guard loggingEnabled else { return }

FileLogger.shared.append(line: formattedLine)
print(formattedLine)

// UI log panel still respects the in-app debug toggle.
guard loggingEnabled else { return }

let entry = LogEntry(
timestamp: timestamp,
level: level,
Expand Down
23 changes: 23 additions & 0 deletions Sources/Fluid/Services/LocalAPI/LocalAPIModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,29 @@ enum LocalAPI {
)
}

static func isAllowedHostHeader(_ rawHost: String?) -> Bool {
guard let rawHost, !rawHost.isEmpty else { return true }
let host = rawHost.lowercased()
return host == "localhost" ||
host == "127.0.0.1" ||
host == "[::1]" ||
host.hasPrefix("localhost:") ||
host.hasPrefix("127.0.0.1:") ||
host.hasPrefix("[::1]:")
}

static func isAllowedOriginHeader(_ rawOrigin: String?) -> Bool {
guard let rawOrigin, !rawOrigin.isEmpty else { return true }
guard let components = URLComponents(string: rawOrigin),
let scheme = components.scheme?.lowercased(),
let host = components.host?.lowercased()
else {
return false
}
guard scheme == "http" || scheme == "https" else { return false }
return host == "localhost" || host == "127.0.0.1" || host == "::1" || host == "[::1]"
}

static func boundedLimit(from request: Request, default defaultValue: Int = 100, maximum: Int = 1000) -> Int {
guard let raw = request.query["limit"], let value = Int(raw) else {
return defaultValue
Expand Down
7 changes: 7 additions & 0 deletions Sources/Fluid/Services/LocalAPI/LocalAPIRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ final class LocalAPIRouter {
}

func route(_ request: LocalAPI.Request) async -> LocalAPI.Response {
guard LocalAPI.isAllowedHostHeader(request.headers["host"]) else {
return LocalAPI.error("Invalid Host header.", status: 400)
}
guard LocalAPI.isAllowedOriginHeader(request.headers["origin"]) else {
return LocalAPI.error("Forbidden Origin header.", status: 403)
}

let key = RouteKey(method: request.method.uppercased(), path: request.path)
guard let handler = self.routes[key] else {
if self.routes.keys.contains(where: { $0.path == request.path }) {
Expand Down
1 change: 1 addition & 0 deletions Sources/Fluid/Services/LocalAPI/LocalAPIServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ private final class LocalAPIConnectionHandler {
case 200: return "OK"
case 204: return "No Content"
case 400: return "Bad Request"
case 403: return "Forbidden"
case 404: return "Not Found"
case 405: return "Method Not Allowed"
case 413: return "Payload Too Large"
Expand Down