diff --git a/Package.swift b/Package.swift index 836ecaa1..7fea6555 100644 --- a/Package.swift +++ b/Package.swift @@ -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 diff --git a/README.md b/README.md index 1ffbdc96..77e27223 100644 --- a/README.md +++ b/README.md @@ -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 --- @@ -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:** diff --git a/Sources/Fluid/Services/DebugLogger.swift b/Sources/Fluid/Services/DebugLogger.swift index 12fd90ac..9905417c 100644 --- a/Sources/Fluid/Services/DebugLogger.swift +++ b/Sources/Fluid/Services/DebugLogger.swift @@ -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, diff --git a/Sources/Fluid/Services/LocalAPI/LocalAPIModels.swift b/Sources/Fluid/Services/LocalAPI/LocalAPIModels.swift index f637366c..fc972f9d 100644 --- a/Sources/Fluid/Services/LocalAPI/LocalAPIModels.swift +++ b/Sources/Fluid/Services/LocalAPI/LocalAPIModels.swift @@ -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 diff --git a/Sources/Fluid/Services/LocalAPI/LocalAPIRouter.swift b/Sources/Fluid/Services/LocalAPI/LocalAPIRouter.swift index bc79f42a..53e62483 100644 --- a/Sources/Fluid/Services/LocalAPI/LocalAPIRouter.swift +++ b/Sources/Fluid/Services/LocalAPI/LocalAPIRouter.swift @@ -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 }) { diff --git a/Sources/Fluid/Services/LocalAPI/LocalAPIServer.swift b/Sources/Fluid/Services/LocalAPI/LocalAPIServer.swift index 2da5eb1a..b33dd953 100644 --- a/Sources/Fluid/Services/LocalAPI/LocalAPIServer.swift +++ b/Sources/Fluid/Services/LocalAPI/LocalAPIServer.swift @@ -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"