@hasna/recordings 0.3.13 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +223 -37
- package/bun.lock +37 -38
- package/dist/cli/index.js +1884 -615
- package/dist/cli/macos-shortcut.d.ts +2 -2
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/pg-migrations.d.ts.map +1 -1
- package/dist/db/recordings.d.ts +6 -0
- package/dist/db/recordings.d.ts.map +1 -1
- package/dist/http/client.d.ts +183 -22
- package/dist/http/client.d.ts.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2858 -2288
- package/dist/lib/audio-artifact-storage.d.ts +98 -0
- package/dist/lib/audio-artifact-storage.d.ts.map +1 -0
- package/dist/lib/capture-probe.d.ts +3 -3
- package/dist/lib/capture-probe.d.ts.map +1 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/local-opt-in.d.ts +132 -0
- package/dist/lib/local-opt-in.d.ts.map +1 -0
- package/dist/lib/macos-bundle.d.ts +1 -1
- package/dist/lib/persistence-probe.d.ts +56 -17
- package/dist/lib/persistence-probe.d.ts.map +1 -1
- package/dist/lib/transcriber.d.ts.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +1689 -456
- package/dist/sdk/index.d.ts +13 -5
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/index.js +904 -1
- package/dist/sdk/resolve.d.ts +53 -0
- package/dist/sdk/resolve.d.ts.map +1 -0
- package/dist/sdk/v1.generated.d.ts +6 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/client-ip.d.ts +39 -0
- package/dist/server/client-ip.d.ts.map +1 -0
- package/dist/server/cloud-config.d.ts +12 -0
- package/dist/server/cloud-config.d.ts.map +1 -1
- package/dist/server/cloud-readiness.d.ts.map +1 -1
- package/dist/server/cloud.d.ts +20 -2
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.js +566 -284
- package/dist/server/migrate-command.d.ts +1 -1
- package/dist/server/migrate-command.d.ts.map +1 -1
- package/dist/server/openapi.d.ts +21 -0
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/repo.d.ts.map +1 -1
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/storage.d.ts +2 -2
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +1334 -871
- package/dist/store.d.ts +12 -3
- package/dist/store.d.ts.map +1 -1
- package/dist/types/index.d.ts +15 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +9 -8
- package/packaging/macos/build_release_pkg.sh +3 -3
- package/packaging/macos/managed_bootstrap.sh +4 -4
- package/packaging/macos/scripts/postinstall +2 -2
- package/packaging/macos/scripts/preinstall +2 -2
- package/scripts/build_companion_cli.sh +7 -1
- package/scripts/install_macos_app.sh +27 -24
- package/scripts/macos_artifact.ts +20 -16
- package/scripts/migrate.ts +4 -20
- package/scripts/smoke_macos_app.sh +21 -21
- package/src/native/Recordings/App/ContentView.swift +53 -68
- package/src/native/Recordings/App/MenuBarStatusView.swift +171 -61
- package/src/native/Recordings/App/RecordWorkspaceView.swift +47 -449
- package/src/native/Recordings/App/RecorderSettingsView.swift +75 -0
- package/src/native/Recordings/App/RecordingDetailView.swift +3 -4
- package/src/native/Recordings/App/RecordingsApp.swift +185 -9
- package/src/native/Recordings/App/RecordingsListView.swift +60 -112
- package/src/native/Recordings/App/RecordingsStore.swift +124 -109
- package/src/native/Recordings/App/Theme.swift +73 -61
- package/src/native/Recordings/Package.swift +1 -0
- package/src/native/Recordings/RecordingsLib/CaptureMonitor.swift +65 -0
- package/src/native/Recordings/RecordingsLib/Info.plist +3 -3
- package/src/native/Recordings/RecordingsLib/OpenAIAPIKeyStore.swift +75 -21
- package/src/native/Recordings/RecordingsLib/Recording.swift +14 -2
- package/src/native/Recordings/RecordingsLib/RecordingEngine.swift +316 -34
- package/src/native/Recordings/RecordingsLib/RecordingGlassBackground.swift +34 -0
- package/src/native/Recordings/RecordingsLib/RecordingProvider.swift +102 -0
- package/src/native/Recordings/RecordingsLib/RecordingsCLI.swift +17 -10
- package/src/native/Recordings/RecordingsLib/ServiceAPIConfiguration.swift +107 -0
- package/src/native/Recordings/RecordingsLib/ServiceConnectionView.swift +76 -0
- package/src/native/Recordings/RecordingsLib/SettingsView.swift +45 -149
- package/src/native/Recordings/RecordingsLib/SettingsWindowController.swift +48 -0
- package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +13 -4
- package/src/native/Recordings/RecordingsTests/OpenAIAPIKeyStoreTests.swift +43 -38
- package/src/native/Recordings/RecordingsTests/RecorderSurfaceTests.swift +60 -0
- package/src/native/Recordings/RecordingsTests/RecordingDateTests.swift +26 -0
- package/src/native/Recordings/RecordingsTests/RecordingProviderTests.swift +195 -0
- package/src/native/Recordings/RecordingsTests/RecordingStartGateTests.swift +1 -1
- package/src/native/Recordings/RecordingsTests/RecordingStartTimingTests.swift +30 -0
- package/src/native/Recordings/RecordingsTests/ServiceAPIConfigurationTests.swift +75 -0
- package/src/native/Recordings/RecordingsTests/SettingsWindowControllerTests.swift +28 -0
- package/src/native/Recordings/Updater/BootstrapPreflight/BootstrapPreflightMain.swift +1 -1
- package/src/native/Recordings/Updater/Broker/ApplicationNamespace.swift +2 -2
- package/src/native/Recordings/Updater/Broker/BrokerMain.swift +1 -1
- package/src/native/Recordings/Updater/Broker/CanonicalTreeCopy.swift +1 -1
- package/src/native/Recordings/Updater/Broker/InstallJournal.swift +1 -1
- package/src/native/Recordings/Updater/BrokerTests/ActivationRecoveryPolicyTests.swift +2 -2
- package/src/native/Recordings/Updater/Protocol/UpdateProtocol.swift +1 -1
- package/src/native/Recordings/Updater/VerifierLauncher/RecordingsVerifierLauncher.c +2 -2
- package/src/native/Recordings/build.sh +6 -6
- package/src/native/Recordings/App/SidebarView.swift +0 -212
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Foundation
|
|
2
|
+
import Security
|
|
2
3
|
|
|
3
4
|
struct ProcessingModelSelection: Equatable, Sendable {
|
|
4
5
|
let transcriptionPrompt: String
|
|
@@ -16,11 +17,17 @@ enum OpenAIAPIKeyStore {
|
|
|
16
17
|
static func load(
|
|
17
18
|
homePath: String,
|
|
18
19
|
environment: [String: String] = ProcessInfo.processInfo.environment,
|
|
19
|
-
userDefaultKey: String? = UserDefaults.standard.string(forKey: "openAIAPIKey")
|
|
20
|
+
userDefaultKey: String? = UserDefaults.standard.string(forKey: "openAIAPIKey"),
|
|
21
|
+
keychainLoader: (() throws -> String?)? = nil
|
|
20
22
|
) -> String {
|
|
21
|
-
if let key = firstNonEmpty(environment["OPENAI_API_KEY"], environment["RECORDINGS_API_KEY"]) {
|
|
23
|
+
if let key = firstNonEmpty(environment["RECORDINGS_OPENAI_API_KEY"], environment["OPENAI_API_KEY"], environment["RECORDINGS_API_KEY"]) {
|
|
22
24
|
return key
|
|
23
25
|
}
|
|
26
|
+
// Test and resolver homes must never read the owner's credentials.
|
|
27
|
+
if let loader = keychainLoader,
|
|
28
|
+
let key = try? loader(), let key = firstNonEmpty(key) { return key }
|
|
29
|
+
if keychainLoader == nil, homePath == FileManager.default.homeDirectoryForCurrentUser.path,
|
|
30
|
+
let key = try? loadKeychain(), let key = firstNonEmpty(key) { return key }
|
|
24
31
|
if let key = firstNonEmpty(userDefaultKey) {
|
|
25
32
|
return key
|
|
26
33
|
}
|
|
@@ -96,30 +103,77 @@ enum OpenAIAPIKeyStore {
|
|
|
96
103
|
)
|
|
97
104
|
}
|
|
98
105
|
|
|
99
|
-
///
|
|
100
|
-
///
|
|
101
|
-
static func save(
|
|
106
|
+
/// The app and its embedded helper share the provider credential through Keychain
|
|
107
|
+
/// and an in-memory child environment. Never write a new key into preferences.
|
|
108
|
+
static func save(
|
|
109
|
+
key: String, homePath: String,
|
|
110
|
+
defaults: UserDefaults = .standard,
|
|
111
|
+
keychainWriter: (String) throws -> Void = saveKeychain
|
|
112
|
+
) throws {
|
|
102
113
|
let trimmed = key.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
var json: [String: Any] = [:]
|
|
109
|
-
if let data = try? Data(contentsOf: url),
|
|
110
|
-
let existing = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
|
|
111
|
-
json = existing
|
|
114
|
+
try keychainWriter(trimmed)
|
|
115
|
+
var json = loadMutableConfig(homePath: homePath)
|
|
116
|
+
if json.removeValue(forKey: "openai_api_key") != nil || json["api_key"] != nil {
|
|
117
|
+
json.removeValue(forKey: "api_key")
|
|
118
|
+
try writeConfig(json, homePath: homePath)
|
|
112
119
|
}
|
|
120
|
+
defaults.removeObject(forKey: "openAIAPIKey")
|
|
121
|
+
}
|
|
113
122
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
123
|
+
private static var keychainQuery: [String: Any] {
|
|
124
|
+
[kSecClass as String: kSecClassGenericPassword,
|
|
125
|
+
kSecAttrService as String: "hasna.credentials.openai.api-key",
|
|
126
|
+
kSecAttrAccount as String: "openai/api_key"]
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
static func loadKeychain(allowInteraction: Bool = false) throws -> String? {
|
|
130
|
+
var query = keychainQuery
|
|
131
|
+
query[kSecReturnData as String] = true
|
|
132
|
+
query[kSecMatchLimit as String] = kSecMatchLimitOne
|
|
133
|
+
if !allowInteraction { query[kSecUseAuthenticationUI as String] = kSecUseAuthenticationUIFail }
|
|
134
|
+
var result: CFTypeRef?
|
|
135
|
+
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
|
136
|
+
if status == errSecItemNotFound { return nil }
|
|
137
|
+
guard status == errSecSuccess else { throw KeychainFailure(status: status) }
|
|
138
|
+
return (result as? Data).flatMap { String(data: $0, encoding: .utf8) }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private static func saveKeychain(_ key: String) throws {
|
|
142
|
+
let query = keychainQuery
|
|
143
|
+
if key.isEmpty {
|
|
144
|
+
let status = SecItemDelete(query as CFDictionary)
|
|
145
|
+
guard status == errSecSuccess || status == errSecItemNotFound else { throw KeychainFailure(status: status) }
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
let value = [kSecValueData as String: Data(key.utf8)]
|
|
149
|
+
var status = SecItemUpdate(query as CFDictionary, value as CFDictionary)
|
|
150
|
+
if status == errSecItemNotFound {
|
|
151
|
+
var item = query.merging(value) { _, new in new }
|
|
152
|
+
item[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
|
|
153
|
+
status = SecItemAdd(item as CFDictionary, nil)
|
|
118
154
|
}
|
|
155
|
+
guard status == errSecSuccess else { throw KeychainFailure(status: status) }
|
|
156
|
+
}
|
|
119
157
|
|
|
120
|
-
|
|
121
|
-
let
|
|
122
|
-
|
|
158
|
+
struct KeychainFailure: Error, LocalizedError {
|
|
159
|
+
let status: OSStatus
|
|
160
|
+
var errorDescription: String? { "Could not access the OpenAI key in Keychain (\(status))." }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
static func childEnvironment(
|
|
164
|
+
base: [String: String], homePath: String,
|
|
165
|
+
keyLoader: (() -> String)? = nil
|
|
166
|
+
) -> [String: String] {
|
|
167
|
+
var result = base
|
|
168
|
+
let key = keyLoader?() ?? load(homePath: homePath, environment: base)
|
|
169
|
+
// RECORDINGS_API_KEY is also an alias in the Hasna service resolver. Keeping
|
|
170
|
+
// it would send a provider key to the service API as its authentication.
|
|
171
|
+
result.removeValue(forKey: "RECORDINGS_API_KEY")
|
|
172
|
+
if !key.isEmpty {
|
|
173
|
+
result["OPENAI_API_KEY"] = key
|
|
174
|
+
result["RECORDINGS_OPENAI_API_KEY"] = key
|
|
175
|
+
}
|
|
176
|
+
return result
|
|
123
177
|
}
|
|
124
178
|
|
|
125
179
|
static func saveLanguage(language: String, homePath: String) throws {
|
|
@@ -100,12 +100,24 @@ public struct Recording: Identifiable, Codable, Sendable, Equatable {
|
|
|
100
100
|
|
|
101
101
|
public static func parseDate(_ value: String) -> Date? {
|
|
102
102
|
guard !value.isEmpty else { return nil }
|
|
103
|
+
// PostgreSQL NOW()::text uses a space and may abbreviate the UTC offset
|
|
104
|
+
// to hours, for example "2026-09-06 10:52:42.523193+00".
|
|
105
|
+
var internetDate = value
|
|
106
|
+
if value.range(of: #"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d{1,6})?[+-]\d{2}(?::?\d{2})?$"#,
|
|
107
|
+
options: .regularExpression) != nil {
|
|
108
|
+
guard value.range(of: #"[+-](?:[01]\d|2[0-3])(?::?[0-5]\d)?$"#,
|
|
109
|
+
options: .regularExpression) != nil else { return nil }
|
|
110
|
+
internetDate = value.replacingOccurrences(of: " ", with: "T")
|
|
111
|
+
if internetDate.range(of: #"[+-]\d{2}$"#, options: .regularExpression) != nil {
|
|
112
|
+
internetDate += ":00"
|
|
113
|
+
}
|
|
114
|
+
}
|
|
103
115
|
let withFrac = ISO8601DateFormatter()
|
|
104
116
|
withFrac.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
|
105
|
-
if let d = withFrac.date(from:
|
|
117
|
+
if let d = withFrac.date(from: internetDate) { return d }
|
|
106
118
|
let plain = ISO8601DateFormatter()
|
|
107
119
|
plain.formatOptions = [.withInternetDateTime]
|
|
108
|
-
if let d = plain.date(from:
|
|
120
|
+
if let d = plain.date(from: internetDate) { return d }
|
|
109
121
|
// SQLite "YYYY-MM-DD HH:MM:SS" fallback.
|
|
110
122
|
let df = DateFormatter()
|
|
111
123
|
df.locale = Locale(identifier: "en_US_POSIX")
|