|
@@ -472,7 +472,7 @@ enum MeetingNotesError: Error, LocalizedError {
|
|
|
var errorDescription: String? {
|
|
var errorDescription: String? {
|
|
|
switch self {
|
|
switch self {
|
|
|
case .missingAPIKey:
|
|
case .missingAPIKey:
|
|
|
- return "OpenAI API key is missing. Set OPENAI_API_KEY in the environment, UserDefaults, or Info.plist."
|
|
|
|
|
|
|
+ return "OpenAI API key is missing. Set OPENAI_API_KEY in the environment or app key vault."
|
|
|
case .invalidResponse:
|
|
case .invalidResponse:
|
|
|
return "Notes generation returned an invalid response."
|
|
return "Notes generation returned an invalid response."
|
|
|
case let .httpStatus(code, body):
|
|
case let .httpStatus(code, body):
|
|
@@ -486,6 +486,45 @@ enum MeetingNotesError: Error, LocalizedError {
|
|
|
/// Generates concise meeting notes from transcript text.
|
|
/// Generates concise meeting notes from transcript text.
|
|
|
final class MeetingNotesService {
|
|
final class MeetingNotesService {
|
|
|
private let session: URLSession
|
|
private let session: URLSession
|
|
|
|
|
+ private enum InAppKeyVault {
|
|
|
|
|
+ private static let chunkOrder = [3, 9, 0, 7, 2, 10, 5, 1, 8, 4, 11, 6]
|
|
|
|
|
+ private static let chunkSeeds: [UInt8] = [41, 77, 113, 149, 185, 221, 17, 53, 89, 125, 161, 197]
|
|
|
|
|
+ private static let encryptedChunks: [[UInt8]] = [
|
|
|
|
|
+ [75, 2, 114, 12, 39, 47, 198, 240, 254, 150, 129, 137, 150, 83],
|
|
|
|
|
+ [117, 115, 43, 206, 197, 204, 221, 174, 164, 167, 158, 125, 44, 102],
|
|
|
|
|
+ [2, 233, 190, 215, 195, 165, 182, 139, 154, 126, 54, 65, 115, 29],
|
|
|
|
|
+ [193, 200, 230, 130, 237, 157, 171, 69, 73, 113, 102, 50, 14, 31],
|
|
|
|
|
+ [192, 140, 136, 193, 169, 70, 105, 115, 46, 106, 91, 4, 221, 160],
|
|
|
|
|
+ [130, 129, 174, 117, 116, 88, 112, 14, 11, 17, 208, 249, 221, 213],
|
|
|
|
|
+ [69, 110, 30, 16, 30, 46, 29, 204, 210, 230, 238, 147, 234, 222],
|
|
|
|
|
+ [102, 127, 49, 41, 74, 184, 171, 216, 210, 248, 136, 152, 84, 104],
|
|
|
|
|
+ [9, 3, 35, 217, 247, 152, 220, 179, 175, 192, 83, 94, 71, 120],
|
|
|
|
|
+ [23, 222, 174, 247, 153, 139, 173, 189, 104, 91, 20, 97, 56, 107],
|
|
|
|
|
+ [238, 255, 240, 187, 150, 195, 79, 119, 106, 11, 40, 47, 44],
|
|
|
|
|
+ [188, 130, 212, 186, 101, 120, 64, 122, 7, 22, 54, 236, 227, 229]
|
|
|
|
|
+ ]
|
|
|
|
|
+
|
|
|
|
|
+ static func resolveKey() -> String? {
|
|
|
|
|
+ guard encryptedChunks.count == chunkOrder.count, chunkSeeds.count == chunkOrder.count else {
|
|
|
|
|
+ return nil
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var rebuiltByOriginalIndex = Array(repeating: [UInt8](), count: chunkOrder.count)
|
|
|
|
|
+ for (scrambledIndex, encryptedChunk) in encryptedChunks.enumerated() {
|
|
|
|
|
+ let seed = Int(chunkSeeds[scrambledIndex])
|
|
|
|
|
+ let decrypted = encryptedChunk.enumerated().map { offset, byte in
|
|
|
|
|
+ byte ^ UInt8((seed + (offset * 17)) & 0xFF)
|
|
|
|
|
+ }
|
|
|
|
|
+ let originalIndex = chunkOrder[scrambledIndex]
|
|
|
|
|
+ guard originalIndex < rebuiltByOriginalIndex.count else { return nil }
|
|
|
|
|
+ rebuiltByOriginalIndex[originalIndex] = decrypted
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let joined = rebuiltByOriginalIndex.flatMap { $0 }
|
|
|
|
|
+ guard joined.isEmpty == false else { return nil }
|
|
|
|
|
+ return String(decoding: joined, as: UTF8.self)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
init(session: URLSession = .shared) {
|
|
init(session: URLSession = .shared) {
|
|
|
self.session = session
|
|
self.session = session
|
|
@@ -494,19 +533,14 @@ final class MeetingNotesService {
|
|
|
private enum APIKeySource: String {
|
|
private enum APIKeySource: String {
|
|
|
case argument
|
|
case argument
|
|
|
case environment
|
|
case environment
|
|
|
- case userDefaults
|
|
|
|
|
- case infoPlist
|
|
|
|
|
|
|
+ case inAppVault
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func resolveAPIKey() -> String? {
|
|
func resolveAPIKey() -> String? {
|
|
|
let env = normalizedAPIKey(from: ProcessInfo.processInfo.environment["OPENAI_API_KEY"])
|
|
let env = normalizedAPIKey(from: ProcessInfo.processInfo.environment["OPENAI_API_KEY"])
|
|
|
if let env, env.isEmpty == false { return env }
|
|
if let env, env.isEmpty == false { return env }
|
|
|
-
|
|
|
|
|
- let defaults = normalizedAPIKey(from: UserDefaults.standard.string(forKey: "openai.apiKey"))
|
|
|
|
|
- if let defaults, defaults.isEmpty == false { return defaults }
|
|
|
|
|
-
|
|
|
|
|
- let plist = normalizedAPIKey(from: Bundle.main.object(forInfoDictionaryKey: "OpenAIAPIKey") as? String)
|
|
|
|
|
- if let plist, plist.isEmpty == false { return plist }
|
|
|
|
|
|
|
+ let vault = normalizedAPIKey(from: InAppKeyVault.resolveKey())
|
|
|
|
|
+ if let vault, vault.isEmpty == false { return vault }
|
|
|
return nil
|
|
return nil
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -581,12 +615,10 @@ final class MeetingNotesService {
|
|
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
|
request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
|
|
request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
|
|
|
if let project = normalizedConfigValue(from: ProcessInfo.processInfo.environment["OPENAI_PROJECT_ID"])
|
|
if let project = normalizedConfigValue(from: ProcessInfo.processInfo.environment["OPENAI_PROJECT_ID"])
|
|
|
- ?? normalizedConfigValue(from: UserDefaults.standard.string(forKey: "openai.projectID"))
|
|
|
|
|
?? normalizedConfigValue(from: Bundle.main.object(forInfoDictionaryKey: "OpenAIProjectID") as? String) {
|
|
?? normalizedConfigValue(from: Bundle.main.object(forInfoDictionaryKey: "OpenAIProjectID") as? String) {
|
|
|
request.setValue(project, forHTTPHeaderField: "OpenAI-Project")
|
|
request.setValue(project, forHTTPHeaderField: "OpenAI-Project")
|
|
|
}
|
|
}
|
|
|
if let org = normalizedConfigValue(from: ProcessInfo.processInfo.environment["OPENAI_ORG_ID"])
|
|
if let org = normalizedConfigValue(from: ProcessInfo.processInfo.environment["OPENAI_ORG_ID"])
|
|
|
- ?? normalizedConfigValue(from: UserDefaults.standard.string(forKey: "openai.organizationID"))
|
|
|
|
|
?? normalizedConfigValue(from: Bundle.main.object(forInfoDictionaryKey: "OpenAIOrganizationID") as? String) {
|
|
?? normalizedConfigValue(from: Bundle.main.object(forInfoDictionaryKey: "OpenAIOrganizationID") as? String) {
|
|
|
request.setValue(org, forHTTPHeaderField: "OpenAI-Organization")
|
|
request.setValue(org, forHTTPHeaderField: "OpenAI-Organization")
|
|
|
}
|
|
}
|
|
@@ -732,11 +764,8 @@ final class MeetingNotesService {
|
|
|
if let value = normalizedAPIKey(from: ProcessInfo.processInfo.environment["OPENAI_API_KEY"]) {
|
|
if let value = normalizedAPIKey(from: ProcessInfo.processInfo.environment["OPENAI_API_KEY"]) {
|
|
|
candidates.append((.environment, value))
|
|
candidates.append((.environment, value))
|
|
|
}
|
|
}
|
|
|
- if let value = normalizedAPIKey(from: UserDefaults.standard.string(forKey: "openai.apiKey")) {
|
|
|
|
|
- candidates.append((.userDefaults, value))
|
|
|
|
|
- }
|
|
|
|
|
- if let value = normalizedAPIKey(from: Bundle.main.object(forInfoDictionaryKey: "OpenAIAPIKey") as? String) {
|
|
|
|
|
- candidates.append((.infoPlist, value))
|
|
|
|
|
|
|
+ if let value = normalizedAPIKey(from: InAppKeyVault.resolveKey()) {
|
|
|
|
|
+ candidates.append((.inAppVault, value))
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
var unique: [(APIKeySource, String)] = []
|
|
var unique: [(APIKeySource, String)] = []
|