Stabilize iOS/watchOS/tvOS apps and add cross-platform audit remediation

This commit is contained in:
Trey t
2026-02-11 12:54:40 -06:00
parent e40275e694
commit acce712261
77 changed files with 2940 additions and 765 deletions

2
SharedCore/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
.build/
.swiftpm/

35
SharedCore/Package.swift Normal file
View File

@@ -0,0 +1,35 @@
// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "SharedCore",
platforms: [
.iOS(.v16),
.watchOS(.v9),
.tvOS(.v16),
.macOS(.v13)
],
products: [
.library(
name: "SharedCore",
targets: ["SharedCore"]
)
],
targets: [
.target(
name: "SharedCore"
),
.testTarget(
name: "SharedCoreiOSTests",
dependencies: ["SharedCore"]
),
.testTarget(
name: "SharedCoreWatchOSTests",
dependencies: ["SharedCore"]
),
.testTarget(
name: "SharedCoreTVOSTests",
dependencies: ["SharedCore"]
)
]
)

View File

@@ -0,0 +1,6 @@
import Foundation
public enum AppNotifications {
public static let createdNewWorkout = Notification.Name("CreatedNewWorkout")
}

View File

@@ -0,0 +1,40 @@
import Foundation
public struct BoundedFIFOQueue<Element> {
private var storage: [Element] = []
public let maxCount: Int
public init(maxCount: Int) {
self.maxCount = max(1, maxCount)
}
public var count: Int {
storage.count
}
public var isEmpty: Bool {
storage.isEmpty
}
@discardableResult
public mutating func enqueue(_ element: Element) -> Int {
var droppedCount = 0
if storage.count >= maxCount {
droppedCount = storage.count - maxCount + 1
storage.removeFirst(droppedCount)
}
storage.append(element)
return droppedCount
}
public mutating func dequeueAll() -> [Element] {
let elements = storage
storage.removeAll()
return elements
}
public mutating func clear() {
storage.removeAll()
}
}

View File

@@ -0,0 +1,76 @@
import Foundation
import os
public enum RuntimeSeverity: String, Sendable {
case info
case warning
case error
}
public struct RuntimeEvent: Sendable {
public let severity: RuntimeSeverity
public let message: String
public let metadata: [String: String]
public let timestamp: Date
public init(severity: RuntimeSeverity, message: String, metadata: [String: String], timestamp: Date = Date()) {
self.severity = severity
self.message = message
self.metadata = metadata
self.timestamp = timestamp
}
}
public final class RuntimeReporter {
public typealias Sink = @Sendable (RuntimeEvent) -> Void
public static let shared = RuntimeReporter()
private let logger = Logger(subsystem: "com.werkout.sharedcore", category: "runtime")
private let lock = NSLock()
private var sink: Sink?
private init() {}
public func setSink(_ sink: Sink?) {
lock.lock()
self.sink = sink
lock.unlock()
}
public func recordError(_ message: String, metadata: [String: String] = [:]) {
record(.error, message: message, metadata: metadata)
}
public func recordWarning(_ message: String, metadata: [String: String] = [:]) {
record(.warning, message: message, metadata: metadata)
}
public func recordInfo(_ message: String, metadata: [String: String] = [:]) {
record(.info, message: message, metadata: metadata)
}
private func record(_ severity: RuntimeSeverity, message: String, metadata: [String: String]) {
let flattenedMetadata = metadata
.map { "\($0.key)=\($0.value)" }
.sorted()
.joined(separator: ",")
let logMessage = flattenedMetadata.isEmpty ? message : "\(message) | \(flattenedMetadata)"
switch severity {
case .info:
logger.info("\(logMessage, privacy: .public)")
case .warning:
logger.warning("\(logMessage, privacy: .public)")
case .error:
logger.error("\(logMessage, privacy: .public)")
}
lock.lock()
let sink = self.sink
lock.unlock()
sink?(RuntimeEvent(severity: severity, message: message, metadata: metadata))
}
}

View File

@@ -0,0 +1,103 @@
import Foundation
public enum TokenSecurity {
public static let defaultRotationWindow: TimeInterval = 60 * 60
private static let authPrefixes = ["token", "bearer"]
// Basic high-entropy hex token detector for accidental commits.
private static let tokenRegex = try? NSRegularExpression(pattern: "\\b[a-fA-F0-9]{32,}\\b")
public static func containsPotentialHardcodedToken(in text: String) -> Bool {
guard let tokenRegex else {
return false
}
let range = NSRange(location: 0, length: text.utf16.count)
return tokenRegex.firstMatch(in: text, options: [], range: range) != nil
}
public static func isRedactedToken(_ token: String?) -> Bool {
guard let token else { return false }
let upper = token.uppercased()
return upper.contains("REDACTED") || upper.contains("YOUR_TOKEN") || upper.contains("PLACEHOLDER")
}
public static func sanitizeToken(_ token: String?) -> String? {
guard let rawToken = token?.trimmingCharacters(in: .whitespacesAndNewlines),
rawToken.isEmpty == false else {
return nil
}
let normalized = normalizeAuthPrefix(rawToken)
guard normalized.isEmpty == false,
isRedactedToken(normalized) == false else {
return nil
}
return normalized
}
public static func jwtExpiration(_ token: String) -> Date? {
let segments = token.split(separator: ".")
guard segments.count == 3 else {
return nil
}
let payloadSegment = String(segments[1])
guard let payloadData = base64URLDecode(payloadSegment),
let object = try? JSONSerialization.jsonObject(with: payloadData) as? [String: Any],
let exp = object["exp"] as? TimeInterval else {
return nil
}
return Date(timeIntervalSince1970: exp)
}
public static func isExpired(_ token: String?, now: Date = Date()) -> Bool {
guard let token = sanitizeToken(token) else {
return true
}
guard let expiration = jwtExpiration(token) else {
// Non-JWT tokens cannot be locally validated for expiry.
return false
}
return expiration <= now
}
public static func shouldRotate(_ token: String?, now: Date = Date(), rotationWindow: TimeInterval = defaultRotationWindow) -> Bool {
guard let token = sanitizeToken(token),
let expiration = jwtExpiration(token) else {
return false
}
return expiration.timeIntervalSince(now) <= rotationWindow
}
private static func base64URLDecode(_ input: String) -> Data? {
var value = input
.replacingOccurrences(of: "-", with: "+")
.replacingOccurrences(of: "_", with: "/")
let remainder = value.count % 4
if remainder > 0 {
value.append(String(repeating: "=", count: 4 - remainder))
}
return Data(base64Encoded: value)
}
private static func normalizeAuthPrefix(_ token: String) -> String {
let lowercased = token.lowercased()
for prefix in authPrefixes {
if lowercased == prefix {
return ""
}
let prefixed = "\(prefix) "
if lowercased.hasPrefix(prefixed) {
return String(token.dropFirst(prefixed.count)).trimmingCharacters(in: .whitespacesAndNewlines)
}
}
return token
}
}

View File

@@ -0,0 +1,40 @@
import Foundation
public enum WatchPayloadValidationError: Error, Equatable {
case emptyPayload
case payloadTooLarge(actualBytes: Int, maxBytes: Int)
case decodeFailure
}
public enum WatchPayloadValidation {
public static let defaultMaxPayloadBytes = 256 * 1024
public static func validate(_ payload: Data, maxBytes: Int = defaultMaxPayloadBytes) -> WatchPayloadValidationError? {
guard payload.isEmpty == false else {
return .emptyPayload
}
guard payload.count <= maxBytes else {
return .payloadTooLarge(actualBytes: payload.count, maxBytes: maxBytes)
}
return nil
}
public static func decode<T: Decodable>(
_ type: T.Type,
from payload: Data,
maxBytes: Int = defaultMaxPayloadBytes,
decoder: JSONDecoder = JSONDecoder()
) throws -> T {
if let validationError = validate(payload, maxBytes: maxBytes) {
throw validationError
}
do {
return try decoder.decode(T.self, from: payload)
} catch {
throw WatchPayloadValidationError.decodeFailure
}
}
}

View File

@@ -0,0 +1,50 @@
import Foundation
public struct WorkoutValidationIssue: Equatable {
public let code: String
public let message: String
public init(code: String, message: String) {
self.code = code
self.message = message
}
}
public enum WorkoutValidation {
public static func validateSupersets(_ supersets: [[String: Any]]) -> [WorkoutValidationIssue] {
var issues = [WorkoutValidationIssue]()
if supersets.isEmpty {
issues.append(WorkoutValidationIssue(code: "empty_supersets", message: "Workout requires at least one superset."))
return issues
}
for (supersetIndex, superset) in supersets.enumerated() {
let rounds = superset["rounds"] as? Int ?? 0
if rounds <= 0 {
issues.append(WorkoutValidationIssue(code: "invalid_rounds", message: "Superset \(supersetIndex + 1) must have at least one round."))
}
let exercises = superset["exercises"] as? [[String: Any]] ?? []
if exercises.isEmpty {
issues.append(WorkoutValidationIssue(code: "empty_exercises", message: "Superset \(supersetIndex + 1) must contain at least one exercise."))
continue
}
for (exerciseIndex, exercise) in exercises.enumerated() {
let reps = exercise["reps"] as? Int ?? 0
let duration = exercise["duration"] as? Int ?? 0
if reps <= 0 && duration <= 0 {
issues.append(
WorkoutValidationIssue(
code: "invalid_exercise_payload",
message: "Exercise \(exerciseIndex + 1) in superset \(supersetIndex + 1) needs reps or duration."
)
)
}
}
}
return issues
}
}

View File

@@ -0,0 +1,20 @@
import XCTest
@testable import SharedCore
final class RuntimeReporterTests: XCTestCase {
func testReporterInvokesSinkWithMetadata() {
let expectation = expectation(description: "sink called")
RuntimeReporter.shared.setSink { event in
XCTAssertEqual(event.severity, .error)
XCTAssertEqual(event.message, "network failure")
XCTAssertEqual(event.metadata["status"], "500")
expectation.fulfill()
}
RuntimeReporter.shared.recordError("network failure", metadata: ["status": "500"])
waitForExpectations(timeout: 1.0)
RuntimeReporter.shared.setSink(nil)
}
}

View File

@@ -0,0 +1,36 @@
import XCTest
@testable import SharedCore
final class BoundedFIFOQueueTests: XCTestCase {
func testDisconnectReconnectFlushPreservesOrder() {
var queue = BoundedFIFOQueue<Int>(maxCount: 5)
_ = queue.enqueue(10)
_ = queue.enqueue(20)
_ = queue.enqueue(30)
XCTAssertEqual(queue.dequeueAll(), [10, 20, 30])
XCTAssertTrue(queue.isEmpty)
}
func testOverflowDropsOldestPayloads() {
var queue = BoundedFIFOQueue<Int>(maxCount: 3)
XCTAssertEqual(queue.enqueue(1), 0)
XCTAssertEqual(queue.enqueue(2), 0)
XCTAssertEqual(queue.enqueue(3), 0)
XCTAssertEqual(queue.enqueue(4), 1)
XCTAssertEqual(queue.enqueue(5), 1)
XCTAssertEqual(queue.dequeueAll(), [3, 4, 5])
}
func testMaxCountHasLowerBoundOfOne() {
var queue = BoundedFIFOQueue<Int>(maxCount: 0)
_ = queue.enqueue(1)
XCTAssertEqual(queue.enqueue(2), 1)
XCTAssertEqual(queue.dequeueAll(), [2])
}
}

View File

@@ -0,0 +1,47 @@
import XCTest
@testable import SharedCore
final class WatchPayloadValidationTests: XCTestCase {
private struct MockPayload: Codable, Equatable {
let name: String
let count: Int
}
func testValidateRejectsEmptyPayload() {
let error = WatchPayloadValidation.validate(Data())
XCTAssertEqual(error, .emptyPayload)
}
func testValidateRejectsOversizedPayload() {
let payload = Data(repeating: 0, count: 9)
let error = WatchPayloadValidation.validate(payload, maxBytes: 8)
XCTAssertEqual(error, .payloadTooLarge(actualBytes: 9, maxBytes: 8))
}
func testDecodeAcceptsValidPayload() throws {
let payload = try JSONEncoder().encode(MockPayload(name: "set", count: 12))
let decoded = try WatchPayloadValidation.decode(MockPayload.self, from: payload)
XCTAssertEqual(decoded, MockPayload(name: "set", count: 12))
}
func testDecodeRejectsInvalidJSON() throws {
let invalid = Data("not-json".utf8)
XCTAssertThrowsError(try WatchPayloadValidation.decode(MockPayload.self, from: invalid)) { error in
XCTAssertEqual(error as? WatchPayloadValidationError, .decodeFailure)
}
}
func testDecodeRejectsOversizedPayloadBeforeDecoding() {
let payload = Data(repeating: 1, count: 32)
XCTAssertThrowsError(
try WatchPayloadValidation.decode(MockPayload.self, from: payload, maxBytes: 16)
) { error in
XCTAssertEqual(
error as? WatchPayloadValidationError,
.payloadTooLarge(actualBytes: 32, maxBytes: 16)
)
}
}
}

View File

@@ -0,0 +1,39 @@
import XCTest
@testable import SharedCore
final class WorkoutValidationTests: XCTestCase {
func testValidateSupersetsRejectsEmptyPayload() {
let issues = WorkoutValidation.validateSupersets([])
XCTAssertEqual(issues.first?.code, "empty_supersets")
}
func testValidateSupersetsRejectsInvalidRoundsAndExercisePayload() {
let supersets: [[String: Any]] = [
[
"rounds": 0,
"exercises": [
["reps": 0, "duration": 0]
]
]
]
let issues = WorkoutValidation.validateSupersets(supersets)
XCTAssertTrue(issues.contains(where: { $0.code == "invalid_rounds" }))
XCTAssertTrue(issues.contains(where: { $0.code == "invalid_exercise_payload" }))
}
func testValidateSupersetsAcceptsValidPayload() {
let supersets: [[String: Any]] = [
[
"rounds": 3,
"exercises": [
["reps": 12, "duration": 0],
["reps": 0, "duration": 30]
]
]
]
let issues = WorkoutValidation.validateSupersets(supersets)
XCTAssertTrue(issues.isEmpty)
}
}

View File

@@ -0,0 +1,65 @@
import XCTest
@testable import SharedCore
final class TokenSecurityTests: XCTestCase {
func testSanitizeTokenRejectsEmptyAndRedactedValues() {
XCTAssertNil(TokenSecurity.sanitizeToken(nil))
XCTAssertNil(TokenSecurity.sanitizeToken(" "))
XCTAssertNil(TokenSecurity.sanitizeToken("REDACTED_TOKEN"))
XCTAssertEqual(TokenSecurity.sanitizeToken(" abc123 "), "abc123")
}
func testSanitizeTokenNormalizesAuthorizationPrefixes() {
XCTAssertEqual(TokenSecurity.sanitizeToken("Token abc123"), "abc123")
XCTAssertEqual(TokenSecurity.sanitizeToken("bearer xyz789"), "xyz789")
XCTAssertNil(TokenSecurity.sanitizeToken("Token "))
}
func testContainsPotentialHardcodedTokenDetectsLongHexBlob() {
let content = "private let token = \"0123456789abcdef0123456789abcdef\""
XCTAssertTrue(TokenSecurity.containsPotentialHardcodedToken(in: content))
}
func testJWTExpirationAndRotationWindow() throws {
let now = Date(timeIntervalSince1970: 1_700_000_000)
let expiration = now.addingTimeInterval(30 * 60)
let token = try makeJWT(exp: expiration)
XCTAssertEqual(TokenSecurity.jwtExpiration(token), expiration)
XCTAssertFalse(TokenSecurity.isExpired(token, now: now))
XCTAssertTrue(TokenSecurity.shouldRotate(token, now: now, rotationWindow: 60 * 60))
}
func testExpiredJWTReturnsExpired() throws {
let now = Date(timeIntervalSince1970: 1_700_000_000)
let expiration = now.addingTimeInterval(-10)
let token = try makeJWT(exp: expiration)
XCTAssertTrue(TokenSecurity.isExpired(token, now: now))
}
func testMalformedTokenDoesNotCrashAndDoesNotTriggerRotation() {
let malformed = "not-a-jwt"
XCTAssertNil(TokenSecurity.jwtExpiration(malformed))
XCTAssertFalse(TokenSecurity.isExpired(malformed))
XCTAssertFalse(TokenSecurity.shouldRotate(malformed))
}
private func makeJWT(exp: Date) throws -> String {
let header = ["alg": "HS256", "typ": "JWT"]
let payload = ["exp": Int(exp.timeIntervalSince1970)]
let headerData = try JSONSerialization.data(withJSONObject: header)
let payloadData = try JSONSerialization.data(withJSONObject: payload)
return "\(base64URL(headerData)).\(base64URL(payloadData)).signature"
}
private func base64URL(_ data: Data) -> String {
data.base64EncodedString()
.replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "/", with: "_")
.replacingOccurrences(of: "=", with: "")
}
}