Merge branch 'main' into extensions

This commit is contained in:
Max Goedjen 2025-09-06 12:43:14 -07:00
commit 55ce4fdbea
No known key found for this signature in database
53 changed files with 2160 additions and 1070 deletions

47
.github/workflows/codeql.yml vendored Normal file
View File

@ -0,0 +1,47 @@
name: "CodeQL Advanced"
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
schedule:
- cron: '26 15 * * 3'
jobs:
analyze:
name: Analyze (${{ matrix.language }})
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
permissions:
security-events: write
packages: read
actions: read
contents: read
strategy:
fail-fast: false
matrix:
include:
- language: actions
build-mode: none
# Disable this until CodeQL supports Xcode 26 builds.
# - language: swift
# build-mode: manual
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
- if: matrix.build-mode == 'manual'
name: "Select Xcode"
run: sudo xcrun xcode-select -s /Applications/Xcode_26.0.app
- if: matrix.build-mode == 'manual'
name: "Build"
run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme Secretive CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"

View File

@ -3,10 +3,15 @@ name: Nightly
on: on:
schedule: schedule:
- cron: "0 8 * * *" - cron: "0 8 * * *"
workflow_dispatch:
jobs: jobs:
build: build:
# runs-on: macOS-latest
runs-on: macos-15 runs-on: macos-15
permissions:
id-token: write
contents: write
attestations: write
timeout-minutes: 10 timeout-minutes: 10
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
@ -25,27 +30,29 @@ jobs:
env: env:
RUN_ID: ${{ github.run_id }} RUN_ID: ${{ github.run_id }}
run: | run: |
sed -i '' -e "s/GITHUB_CI_VERSION/0.0.0/g" Sources/Config/Config.xcconfig DATE=$(date "+%Y-%m-%d")
sed -i '' -e "s/GITHUB_CI_VERSION/0.0.0_nightly-$DATE/g" Sources/Config/Config.xcconfig
sed -i '' -e "s/GITHUB_BUILD_NUMBER/1.$RUN_ID/g" Sources/Config/Config.xcconfig sed -i '' -e "s/GITHUB_BUILD_NUMBER/1.$RUN_ID/g" Sources/Config/Config.xcconfig
sed -i '' -e "s/GITHUB_BUILD_URL/https:\/\/github.com\/maxgoedjen\/secretive\/actions\/runs\/$RUN_ID/g" Sources/Secretive/Credits.rtf sed -i '' -e "s/GITHUB_BUILD_URL/https:\/\/github.com\/maxgoedjen\/secretive\/actions\/runs\/$RUN_ID/g" Sources/Secretive/Credits.rtf
- name: Build - name: Build
run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme Secretive -configuration Release -archivePath Archive.xcarchive archive run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme Secretive -configuration Release -archivePath Archive.xcarchive archive
- name: Create ZIPs - name: Create ZIP
run: | run: |
ditto -c -k --sequesterRsrc --keepParent Archive.xcarchive/Products/Applications/Secretive.app ./Secretive.zip ditto -c -k --sequesterRsrc --keepParent Archive.xcarchive/Products/Applications/Secretive.app ./Secretive.zip
ditto -c -k --sequesterRsrc --keepParent Archive.xcarchive ./Archive.zip
- name: Notarize - name: Notarize
env: env:
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
run: xcrun notarytool submit --key ~/.private_keys/AuthKey_$APPLE_API_KEY_ID.p8 --key-id $APPLE_API_KEY_ID --issuer $APPLE_API_ISSUER Secretive.zip run: xcrun notarytool submit --key ~/.private_keys/AuthKey_$APPLE_API_KEY_ID.p8 --key-id $APPLE_API_KEY_ID --issuer $APPLE_API_ISSUER Secretive.zip
- name: Attest
id: attest
uses: actions/attest-build-provenance@v2
with:
subject-path: 'Secretive.zip'
- name: Upload App to Artifacts - name: Upload App to Artifacts
id: upload
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: Secretive.zip name: Secretive.zip
path: Secretive.zip path: Secretive.zip
- name: Attest
id: attest
uses: actions/attest-build-provenance@v2
with:
subject-name: "Secretive.zip"
subject-digest: sha256:${{ steps.upload.outputs.artifact-digest }}

View File

@ -6,7 +6,8 @@ on:
- '*' - '*'
jobs: jobs:
test: test:
# runs-on: macOS-latest permissions:
contents: read
runs-on: macos-15 runs-on: macos-15
timeout-minutes: 10 timeout-minutes: 10
steps: steps:
@ -25,12 +26,11 @@ jobs:
- name: Test - name: Test
run: swift test --build-system swiftbuild --package-path Sources/Packages run: swift test --build-system swiftbuild --package-path Sources/Packages
build: build:
# runs-on: macOS-latest
runs-on: macos-15
permissions: permissions:
id-token: write id-token: write
contents: write contents: write
attestations: write attestations: write
runs-on: macos-15
timeout-minutes: 10 timeout-minutes: 10
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
@ -56,39 +56,34 @@ jobs:
sed -i '' -e "s/GITHUB_BUILD_URL/https:\/\/github.com\/maxgoedjen\/secretive\/actions\/runs\/$RUN_ID/g" Sources/Secretive/Credits.rtf sed -i '' -e "s/GITHUB_BUILD_URL/https:\/\/github.com\/maxgoedjen\/secretive\/actions\/runs\/$RUN_ID/g" Sources/Secretive/Credits.rtf
- name: Build - name: Build
run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme Secretive -configuration Release -archivePath Archive.xcarchive archive run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme Secretive -configuration Release -archivePath Archive.xcarchive archive
- name: Create ZIPs - name: Create ZIP
run: | run: |
ditto -c -k --sequesterRsrc --keepParent Archive.xcarchive/Products/Applications/Secretive.app ./Secretive.zip ditto -c -k --sequesterRsrc --keepParent Archive.xcarchive/Products/Applications/Secretive.app ./Secretive.zip
ditto -c -k --sequesterRsrc --keepParent Archive.xcarchive ./Xcode_Archive.zip
- name: Notarize - name: Notarize
env: env:
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
run: xcrun notarytool submit --key ~/.private_keys/AuthKey_$APPLE_API_KEY_ID.p8 --key-id $APPLE_API_KEY_ID --issuer $APPLE_API_ISSUER Secretive.zip run: xcrun notarytool submit --key ~/.private_keys/AuthKey_$APPLE_API_KEY_ID.p8 --key-id $APPLE_API_KEY_ID --issuer $APPLE_API_ISSUER Secretive.zip
- name: Upload App to Artifacts
id: upload
uses: actions/upload-artifact@v4
with:
name: Secretive.zip
path: Secretive.zip
- name: Attest - name: Attest
id: attest id: attest
uses: actions/attest-build-provenance@v2 uses: actions/attest-build-provenance@v2
with: with:
subject-path: 'Secretive.zip, Xcode_Archive.zip' subject-name: "Secretive.zip"
subject-digest: ${{ steps.upload.outputs.artifact-digest }}
- name: Create Release - name: Create Release
run: | run: |
sed -i.tmp "s/RUN_ID/$RUN_ID/g" .github/templates/release.md sed -i.tmp "s/RUN_ID/$RUN_ID/g" .github/templates/release.md
sed -i.tmp "s/ATTESTATION_ID/$ATTESTATION_ID/g" .github/templates/release.md sed -i.tmp "s/ATTESTATION_ID/$ATTESTATION_ID/g" .github/templates/release.md
gh release create $TAG_NAME -d -F .github/templates/release.md gh release create $TAG_NAME -d -F .github/templates/release.md
gh release upload Secretive.zip gh release upload Secretive.zip
gh release upload Xcode_Archive.zip
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG_NAME: ${{ github.ref }} TAG_NAME: ${{ github.ref }}
RUN_ID: ${{ github.run_id }} RUN_ID: ${{ github.run_id }}
ATTESTATION_ID: ${{ steps.attest.outputs.attestation-id }} ATTESTATION_ID: ${{ steps.attest.outputs.attestation-id }}
- name: Upload App to Artifacts
uses: actions/upload-artifact@v4
with:
name: Secretive.zip
path: Secretive.zip
- name: Upload Archive to Artifacts
uses: actions/upload-artifact@v4
with:
name: Xcode_Archive.zip
path: Xcode_Archive.zip

View File

@ -3,7 +3,8 @@ name: Test
on: [push, pull_request] on: [push, pull_request]
jobs: jobs:
test: test:
# runs-on: macOS-latest permissions:
contents: read
runs-on: macos-15 runs-on: macos-15
timeout-minutes: 10 timeout-minutes: 10
steps: steps:

View File

@ -57,7 +57,7 @@ let package = Package(
) )
var localization: Resource { var localization: Resource {
.process("../../Localizable.xcstrings") .process("../../Resources/Localizable.xcstrings")
} }
var swiftSettings: [PackageDescription.SwiftSetting] { var swiftSettings: [PackageDescription.SwiftSetting] {

View File

@ -61,4 +61,4 @@ Because secrets in the Secure Enclave are not exportable, they are not able to b
## Security ## Security
If you discover any vulnerabilities in this project, please notify [max.goedjen@gmail.com](mailto:max.goedjen@gmail.com) with the subject containing "SECRETIVE SECURITY." Secretive's security policy is detailed in [SECURITY.md](SECURITY.md). To report security issues, please use [GitHub's private reporting feature.](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability)

View File

@ -24,4 +24,4 @@ The latest version on the [Releases page](https://github.com/maxgoedjen/secretiv
## Reporting a Vulnerability ## Reporting a Vulnerability
If you discover any vulnerabilities in this project, please notify max.goedjen@gmail.com with the subject containing "SECRETIVE SECURITY." To report security issues, please use [GitHub's private reporting feature.](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability)

View File

@ -82,7 +82,7 @@ let package = Package(
) )
var localization: Resource { var localization: Resource {
.process("../../Localizable.xcstrings") .process("../../Resources/Localizable.xcstrings")
} }
var swiftSettings: [PackageDescription.SwiftSetting] { var swiftSettings: [PackageDescription.SwiftSetting] {

View File

@ -5,12 +5,20 @@ public struct SemVer: Sendable {
/// The SemVer broken into an array of integers. /// The SemVer broken into an array of integers.
let versionNumbers: [Int] let versionNumbers: [Int]
public let previewDescription: String?
public var isTestBuild: Bool {
versionNumbers == [0, 0, 0]
}
/// Initializes a SemVer from a string representation. /// Initializes a SemVer from a string representation.
/// - Parameter version: A string representation of the SemVer, formatted as "major.minor.patch". /// - Parameter version: A string representation of the SemVer, formatted as "major.minor.patch".
public init(_ version: String) { public init(_ version: String) {
// Betas have the format 1.2.3_beta1 // Betas have the format 1.2.3_beta1
let strippedBeta = version.split(separator: "_").first! // Nightlies have the format 0.0.0_nightly-2025-09-03
let splitFull = version.split(separator: "_")
let strippedBeta = splitFull.first!
previewDescription = splitFull.count > 1 ? String(splitFull[1]) : nil
var split = strippedBeta.split(separator: ".").compactMap { Int($0) } var split = strippedBeta.split(separator: ".").compactMap { Int($0) }
while split.count < 3 { while split.count < 3 {
split.append(0) split.append(0)
@ -22,6 +30,7 @@ public struct SemVer: Sendable {
/// - Parameter version: An `OperatingSystemVersion` representation of the SemVer. /// - Parameter version: An `OperatingSystemVersion` representation of the SemVer.
public init(_ version: OperatingSystemVersion) { public init(_ version: OperatingSystemVersion) {
versionNumbers = [version.majorVersion, version.minorVersion, version.patchVersion] versionNumbers = [version.majorVersion, version.minorVersion, version.patchVersion]
previewDescription = nil
} }
} }

View File

@ -13,12 +13,11 @@ import Observation
state.update state.update
} }
public let testBuild: Bool /// The current version of the app that is running.
public let currentVersion: SemVer
/// The current OS version. /// The current OS version.
private let osVersion: SemVer private let osVersion: SemVer
/// The current version of the app that is running.
private let currentVersion: SemVer
/// Initializes an Updater. /// Initializes an Updater.
/// - Parameters: /// - Parameters:
@ -34,7 +33,6 @@ import Observation
) { ) {
self.osVersion = osVersion self.osVersion = osVersion
self.currentVersion = currentVersion self.currentVersion = currentVersion
testBuild = currentVersion == SemVer("0.0.0")
if checkOnLaunch { if checkOnLaunch {
// Don't do a launch check if the user hasn't seen the setup prompt explaining updater yet. // Don't do a launch check if the user hasn't seen the setup prompt explaining updater yet.
Task { Task {

View File

@ -5,8 +5,8 @@ public protocol UpdaterProtocol: Observable, Sendable {
/// The latest update /// The latest update
@MainActor var update: Release? { get } @MainActor var update: Release? { get }
/// A boolean describing whether or not the current build of the app is a "test" build (ie, a debug build or otherwise special build)
var testBuild: Bool { get } var currentVersion: SemVer { get }
func ignore(release: Release) async func ignore(release: Release) async
} }

View File

@ -133,20 +133,22 @@ private extension SocketPort {
convenience init(path: String) { convenience init(path: String) {
var addr = sockaddr_un() var addr = sockaddr_un()
addr.sun_family = sa_family_t(AF_UNIX)
var len: Int = 0 let length = withUnsafeMutablePointer(to: &addr.sun_path.0) { pointer in
withUnsafeMutablePointer(to: &addr.sun_path.0) { pointer in
path.withCString { cstring in path.withCString { cstring in
len = strlen(cstring) let len = strlen(cstring)
strncpy(pointer, cstring, len) strncpy(pointer, cstring, len)
return len
} }
} }
addr.sun_len = UInt8(len+2) // This doesn't seem to be _strictly_ neccessary with SocketPort.
// but just for good form.
addr.sun_family = sa_family_t(AF_UNIX)
// This mirrors the SUN_LEN macro format.
addr.sun_len = UInt8(MemoryLayout<sockaddr_un>.size - MemoryLayout.size(ofValue: addr.sun_path) + length)
var data: Data! let data = withUnsafePointer(to: &addr) { pointer in
withUnsafePointer(to: &addr) { pointer in Data(bytes: pointer, count: MemoryLayout<sockaddr_un>.size)
data = Data(bytes: pointer, count: MemoryLayout<sockaddr_un>.size)
} }
self.init(protocolFamily: AF_UNIX, socketType: SOCK_STREAM, protocol: 0, address: data)! self.init(protocolFamily: AF_UNIX, socketType: SOCK_STREAM, protocol: 0, address: data)!

View File

@ -4,7 +4,7 @@ import OSLog
/// Manages storage and lookup for OpenSSH certificates. /// Manages storage and lookup for OpenSSH certificates.
public actor OpenSSHCertificateHandler: Sendable { public actor OpenSSHCertificateHandler: Sendable {
private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: NSHomeDirectory()) private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: URL.homeDirectory)
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "OpenSSHCertificateHandler") private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "OpenSSHCertificateHandler")
private let writer = OpenSSHPublicKeyWriter() private let writer = OpenSSHPublicKeyWriter()
private var keyBlobsAndNames: [AnySecret: (Data, Data)] = [:] private var keyBlobsAndNames: [AnySecret: (Data, Data)] = [:]

View File

@ -5,12 +5,12 @@ import OSLog
public final class PublicKeyFileStoreController: Sendable { public final class PublicKeyFileStoreController: Sendable {
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "PublicKeyFileStoreController") private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "PublicKeyFileStoreController")
private let directory: String private let directory: URL
private let keyWriter = OpenSSHPublicKeyWriter() private let keyWriter = OpenSSHPublicKeyWriter()
/// Initializes a PublicKeyFileStoreController. /// Initializes a PublicKeyFileStoreController.
public init(homeDirectory: String) { public init(homeDirectory: URL) {
directory = homeDirectory.appending("/PublicKeys") directory = homeDirectory.appending(component: "PublicKeys")
} }
/// Writes out the keys specified to disk. /// Writes out the keys specified to disk.
@ -20,16 +20,17 @@ public final class PublicKeyFileStoreController: Sendable {
logger.log("Writing public keys to disk") logger.log("Writing public keys to disk")
if clear { if clear {
let validPaths = Set(secrets.map { publicKeyPath(for: $0) }).union(Set(secrets.map { sshCertificatePath(for: $0) })) let validPaths = Set(secrets.map { publicKeyPath(for: $0) }).union(Set(secrets.map { sshCertificatePath(for: $0) }))
let contentsOfDirectory = (try? FileManager.default.contentsOfDirectory(atPath: directory)) ?? [] let contentsOfDirectory = (try? FileManager.default.contentsOfDirectory(atPath: directory.path())) ?? []
let fullPathContents = contentsOfDirectory.map { "\(directory)/\($0)" } let fullPathContents = contentsOfDirectory.map { "\(directory)/\($0)" }
let untracked = Set(fullPathContents) let untracked = Set(fullPathContents)
.subtracting(validPaths) .subtracting(validPaths)
for path in untracked { for path in untracked {
try? FileManager.default.removeItem(at: URL(fileURLWithPath: path)) // string instead of fileURLWithPath since we're already using fileURL format.
try? FileManager.default.removeItem(at: URL(string: path)!)
} }
} }
try? FileManager.default.createDirectory(at: URL(fileURLWithPath: directory), withIntermediateDirectories: false, attributes: nil) try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: false, attributes: nil)
for secret in secrets { for secret in secrets {
let path = publicKeyPath(for: secret) let path = publicKeyPath(for: secret)
let data = Data(keyWriter.openSSHString(secret: secret).utf8) let data = Data(keyWriter.openSSHString(secret: secret).utf8)
@ -44,14 +45,14 @@ public final class PublicKeyFileStoreController: Sendable {
/// - Warning: This method returning a path does not imply that a key has been written to disk already. This method only describes where it will be written to. /// - Warning: This method returning a path does not imply that a key has been written to disk already. This method only describes where it will be written to.
public func publicKeyPath<SecretType: Secret>(for secret: SecretType) -> String { public func publicKeyPath<SecretType: Secret>(for secret: SecretType) -> String {
let minimalHex = keyWriter.openSSHMD5Fingerprint(secret: secret).replacingOccurrences(of: ":", with: "") let minimalHex = keyWriter.openSSHMD5Fingerprint(secret: secret).replacingOccurrences(of: ":", with: "")
return directory.appending("/").appending("\(minimalHex).pub") return directory.appending(component: "\(minimalHex).pub").path()
} }
/// Short-circuit check to ship enumerating a bunch of paths if there's nothing in the cert directory. /// Short-circuit check to ship enumerating a bunch of paths if there's nothing in the cert directory.
public var hasAnyCertificates: Bool { public var hasAnyCertificates: Bool {
do { do {
return try FileManager.default return try FileManager.default
.contentsOfDirectory(atPath: directory) .contentsOfDirectory(atPath: directory.path())
.filter { $0.hasSuffix("-cert.pub") } .filter { $0.hasSuffix("-cert.pub") }
.isEmpty == false .isEmpty == false
} catch { } catch {
@ -65,7 +66,7 @@ public final class PublicKeyFileStoreController: Sendable {
/// - Warning: This method returning a path does not imply that a key has a SSH certificates. This method only describes where it will be. /// - Warning: This method returning a path does not imply that a key has a SSH certificates. This method only describes where it will be.
public func sshCertificatePath<SecretType: Secret>(for secret: SecretType) -> String { public func sshCertificatePath<SecretType: Secret>(for secret: SecretType) -> String {
let minimalHex = keyWriter.openSSHMD5Fingerprint(secret: secret).replacingOccurrences(of: ":", with: "") let minimalHex = keyWriter.openSSHMD5Fingerprint(secret: secret).replacingOccurrences(of: ":", with: "")
return directory.appending("/").appending("\(minimalHex)-cert.pub") return directory.appending(component: "\(minimalHex)-cert.pub").path()
} }
} }

View File

@ -30,7 +30,7 @@ extension SecureEnclave {
SecItemCopyMatching(privateAttributes, &privateUntyped) SecItemCopyMatching(privateAttributes, &privateUntyped)
guard let privateTyped = privateUntyped as? [[CFString: Any]] else { return } guard let privateTyped = privateUntyped as? [[CFString: Any]] else { return }
let migratedPublicKeys = Set(store.secrets.map(\.publicKey)) let migratedPublicKeys = Set(store.secrets.map(\.publicKey))
var migrated = false var migratedAny = false
for key in privateTyped { for key in privateTyped {
let name = key[kSecAttrLabel] as? String ?? String(localized: .unnamedSecret) let name = key[kSecAttrLabel] as? String ?? String(localized: .unnamedSecret)
let id = key[kSecAttrApplicationLabel] as! Data let id = key[kSecAttrApplicationLabel] as! Data
@ -45,20 +45,24 @@ extension SecureEnclave {
// Best guess. // Best guess.
let auth: AuthenticationRequirement = String(describing: accessControl) let auth: AuthenticationRequirement = String(describing: accessControl)
.contains("DeviceOwnerAuthentication") ? .presenceRequired : .unknown .contains("DeviceOwnerAuthentication") ? .presenceRequired : .unknown
let parsed = try CryptoKit.SecureEnclave.P256.Signing.PrivateKey(dataRepresentation: tokenObjectID) do {
let secret = Secret(id: UUID().uuidString, name: name, publicKey: parsed.publicKey.x963Representation, attributes: Attributes(keyType: .init(algorithm: .ecdsa, size: 256), authentication: auth)) let parsed = try CryptoKit.SecureEnclave.P256.Signing.PrivateKey(dataRepresentation: tokenObjectID)
guard !migratedPublicKeys.contains(parsed.publicKey.x963Representation) else { let secret = Secret(id: UUID().uuidString, name: name, publicKey: parsed.publicKey.x963Representation, attributes: Attributes(keyType: .init(algorithm: .ecdsa, size: 256), authentication: auth))
logger.log("Skipping \(name), public key already present. Marking as migrated.") guard !migratedPublicKeys.contains(parsed.publicKey.x963Representation) else {
logger.log("Skipping \(name), public key already present. Marking as migrated.")
try markMigrated(secret: secret, oldID: id)
continue
}
logger.log("Migrating \(name).")
try store.saveKey(tokenObjectID, name: name, attributes: secret.attributes)
logger.log("Migrated \(name).")
try markMigrated(secret: secret, oldID: id) try markMigrated(secret: secret, oldID: id)
continue migratedAny = true
} catch {
logger.error("Failed to migrate \(name): \(error).")
} }
logger.log("Migrating \(name).")
try store.saveKey(tokenObjectID, name: name, attributes: secret.attributes)
logger.log("Migrated \(name).")
try markMigrated(secret: secret, oldID: id)
migrated = true
} }
if migrated { if migratedAny {
store.reloadSecrets() store.reloadSecrets()
} }
} }

View File

@ -26,7 +26,7 @@ extension SecureEnclave {
for await note in DistributedNotificationCenter.default().notifications(named: .secretStoreUpdated) { for await note in DistributedNotificationCenter.default().notifications(named: .secretStoreUpdated) {
guard Constants.notificationToken != (note.object as? String) else { guard Constants.notificationToken != (note.object as? String) else {
// Don't reload if we're the ones triggering this by reloading. // Don't reload if we're the ones triggering this by reloading.
return continue
} }
reloadSecrets() reloadSecrets()
} }
@ -112,7 +112,7 @@ extension SecureEnclave {
var accessError: SecurityError? var accessError: SecurityError?
let flags: SecAccessControlCreateFlags = switch attributes.authentication { let flags: SecAccessControlCreateFlags = switch attributes.authentication {
case .notRequired: case .notRequired:
[] [.privateKeyUsage]
case .presenceRequired: case .presenceRequired:
[.userPresence, .privateKeyUsage] [.userPresence, .privateKeyUsage]
case .biometryCurrent: case .biometryCurrent:

View File

@ -21,7 +21,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
}() }()
private let updater = Updater(checkOnLaunch: true) private let updater = Updater(checkOnLaunch: true)
private let notifier = Notifier() private let notifier = Notifier()
private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: NSHomeDirectory()) private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: URL.homeDirectory)
private lazy var agent: Agent = { private lazy var agent: Agent = {
Agent(storeList: storeList, witness: notifier) Agent(storeList: storeList, witness: notifier)
}() }()
@ -58,7 +58,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
updater.update updater.update
} onChange: { [updater, notifier] in } onChange: { [updater, notifier] in
Task { Task {
guard !updater.testBuild else { return } guard !updater.currentVersion.isTestBuild else { return }
await notifier.notify(update: updater.update!) { release in await notifier.notify(update: updater.update!) { release in
await updater.ignore(release: release) await updater.ignore(release: release)
} }

View File

@ -26,6 +26,11 @@
50153E20250AFCB200525160 /* UpdateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E1F250AFCB200525160 /* UpdateView.swift */; }; 50153E20250AFCB200525160 /* UpdateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E1F250AFCB200525160 /* UpdateView.swift */; };
50153E22250DECA300525160 /* SecretListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E21250DECA300525160 /* SecretListItemView.swift */; }; 50153E22250DECA300525160 /* SecretListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E21250DECA300525160 /* SecretListItemView.swift */; };
5018F54F24064786002EB505 /* Notifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5018F54E24064786002EB505 /* Notifier.swift */; }; 5018F54F24064786002EB505 /* Notifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5018F54E24064786002EB505 /* Notifier.swift */; };
504788EC2E680DC800B4556F /* URLs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788EB2E680DC400B4556F /* URLs.swift */; };
504788F22E681F3A00B4556F /* Instructions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F12E681F3A00B4556F /* Instructions.swift */; };
504788F42E681F6900B4556F /* ToolConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F32E681F6900B4556F /* ToolConfigurationView.swift */; };
504788F62E68206F00B4556F /* GettingStartedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F52E68206F00B4556F /* GettingStartedView.swift */; };
504789232E697DD300B4556F /* BoxBackgroundStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504789222E697DD300B4556F /* BoxBackgroundStyle.swift */; };
50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */; }; 50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */; };
50571E0524393D1500F76F6C /* LaunchAgentController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0424393D1500F76F6C /* LaunchAgentController.swift */; }; 50571E0524393D1500F76F6C /* LaunchAgentController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0424393D1500F76F6C /* LaunchAgentController.swift */; };
50617D8323FCE48E0099B055 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D8223FCE48E0099B055 /* App.swift */; }; 50617D8323FCE48E0099B055 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D8223FCE48E0099B055 /* App.swift */; };
@ -36,7 +41,6 @@
5065E313295517C500E16645 /* ToolbarButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5065E312295517C500E16645 /* ToolbarButtonStyle.swift */; }; 5065E313295517C500E16645 /* ToolbarButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5065E312295517C500E16645 /* ToolbarButtonStyle.swift */; };
5066A6C22516F303004B5A36 /* SetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6C12516F303004B5A36 /* SetupView.swift */; }; 5066A6C22516F303004B5A36 /* SetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6C12516F303004B5A36 /* SetupView.swift */; };
5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6C72516FE6E004B5A36 /* CopyableView.swift */; }; 5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6C72516FE6E004B5A36 /* CopyableView.swift */; };
5066A6F7251829B1004B5A36 /* ShellConfigurationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6F6251829B1004B5A36 /* ShellConfigurationController.swift */; };
506772C72424784600034DED /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 506772C62424784600034DED /* Credits.rtf */; }; 506772C72424784600034DED /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 506772C62424784600034DED /* Credits.rtf */; };
506772C92425BB8500034DED /* NoStoresView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506772C82425BB8500034DED /* NoStoresView.swift */; }; 506772C92425BB8500034DED /* NoStoresView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506772C82425BB8500034DED /* NoStoresView.swift */; };
5079BA0F250F29BF00EA86F4 /* StoreListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5079BA0E250F29BF00EA86F4 /* StoreListView.swift */; }; 5079BA0F250F29BF00EA86F4 /* StoreListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5079BA0E250F29BF00EA86F4 /* StoreListView.swift */; };
@ -49,8 +53,12 @@
5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */; }; 5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */; };
50A3B79424026B7600D209EA /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50A3B79324026B7600D209EA /* Preview Assets.xcassets */; }; 50A3B79424026B7600D209EA /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50A3B79324026B7600D209EA /* Preview Assets.xcassets */; };
50A3B79724026B7600D209EA /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 50A3B79524026B7600D209EA /* Main.storyboard */; }; 50A3B79724026B7600D209EA /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 50A3B79524026B7600D209EA /* Main.storyboard */; };
50AE97002E5C1A420018C710 /* IntegrationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50AE96FF2E5C1A420018C710 /* IntegrationsView.swift */; };
50B8550D24138C4F009958AC /* DeleteSecretView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B8550C24138C4F009958AC /* DeleteSecretView.swift */; }; 50B8550D24138C4F009958AC /* DeleteSecretView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B8550C24138C4F009958AC /* DeleteSecretView.swift */; };
50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */; }; 50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */; };
50BDCB722E63BAF20072D2E7 /* AgentStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BDCB712E63BAF20072D2E7 /* AgentStatusView.swift */; };
50BDCB742E6436CA0072D2E7 /* ErrorStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BDCB732E6436C60072D2E7 /* ErrorStyle.swift */; };
50BDCB762E6450950072D2E7 /* ConfigurationItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BDCB752E6450950072D2E7 /* ConfigurationItemView.swift */; };
50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C385A42407A76D00AF2719 /* SecretDetailView.swift */; }; 50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C385A42407A76D00AF2719 /* SecretDetailView.swift */; };
50CF4ABC2E601B0F005588DC /* ActionButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */; }; 50CF4ABC2E601B0F005588DC /* ActionButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
@ -103,10 +111,15 @@
50020BAF24064869003D4025 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; 50020BAF24064869003D4025 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
50033AC227813F1700253856 /* BundleIDs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleIDs.swift; sourceTree = "<group>"; }; 50033AC227813F1700253856 /* BundleIDs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleIDs.swift; sourceTree = "<group>"; };
5003EF39278005C800DF2006 /* Packages */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Packages; sourceTree = "<group>"; }; 5003EF39278005C800DF2006 /* Packages */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Packages; sourceTree = "<group>"; };
5008C23D2E525D8200507AC2 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = Localizable.xcstrings; path = Packages/Localizable.xcstrings; sourceTree = SOURCE_ROOT; }; 5008C23D2E525D8200507AC2 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = Localizable.xcstrings; path = Packages/Resources/Localizable.xcstrings; sourceTree = SOURCE_ROOT; };
50153E1F250AFCB200525160 /* UpdateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateView.swift; sourceTree = "<group>"; }; 50153E1F250AFCB200525160 /* UpdateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateView.swift; sourceTree = "<group>"; };
50153E21250DECA300525160 /* SecretListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretListItemView.swift; sourceTree = "<group>"; }; 50153E21250DECA300525160 /* SecretListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretListItemView.swift; sourceTree = "<group>"; };
5018F54E24064786002EB505 /* Notifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifier.swift; sourceTree = "<group>"; }; 5018F54E24064786002EB505 /* Notifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifier.swift; sourceTree = "<group>"; };
504788EB2E680DC400B4556F /* URLs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLs.swift; sourceTree = "<group>"; };
504788F12E681F3A00B4556F /* Instructions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Instructions.swift; sourceTree = "<group>"; };
504788F32E681F6900B4556F /* ToolConfigurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolConfigurationView.swift; sourceTree = "<group>"; };
504788F52E68206F00B4556F /* GettingStartedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GettingStartedView.swift; sourceTree = "<group>"; };
504789222E697DD300B4556F /* BoxBackgroundStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxBackgroundStyle.swift; sourceTree = "<group>"; };
50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JustUpdatedChecker.swift; sourceTree = "<group>"; }; 50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JustUpdatedChecker.swift; sourceTree = "<group>"; };
50571E0424393D1500F76F6C /* LaunchAgentController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchAgentController.swift; sourceTree = "<group>"; }; 50571E0424393D1500F76F6C /* LaunchAgentController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchAgentController.swift; sourceTree = "<group>"; };
50617D7F23FCE48E0099B055 /* Secretive.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Secretive.app; sourceTree = BUILT_PRODUCTS_DIR; }; 50617D7F23FCE48E0099B055 /* Secretive.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Secretive.app; sourceTree = BUILT_PRODUCTS_DIR; };
@ -120,7 +133,6 @@
5065E312295517C500E16645 /* ToolbarButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarButtonStyle.swift; sourceTree = "<group>"; }; 5065E312295517C500E16645 /* ToolbarButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarButtonStyle.swift; sourceTree = "<group>"; };
5066A6C12516F303004B5A36 /* SetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupView.swift; sourceTree = "<group>"; }; 5066A6C12516F303004B5A36 /* SetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupView.swift; sourceTree = "<group>"; };
5066A6C72516FE6E004B5A36 /* CopyableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyableView.swift; sourceTree = "<group>"; }; 5066A6C72516FE6E004B5A36 /* CopyableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyableView.swift; sourceTree = "<group>"; };
5066A6F6251829B1004B5A36 /* ShellConfigurationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShellConfigurationController.swift; sourceTree = "<group>"; };
506772C62424784600034DED /* Credits.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = Credits.rtf; sourceTree = "<group>"; }; 506772C62424784600034DED /* Credits.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = Credits.rtf; sourceTree = "<group>"; };
506772C82425BB8500034DED /* NoStoresView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoStoresView.swift; sourceTree = "<group>"; }; 506772C82425BB8500034DED /* NoStoresView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoStoresView.swift; sourceTree = "<group>"; };
5079BA0E250F29BF00EA86F4 /* StoreListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreListView.swift; sourceTree = "<group>"; }; 5079BA0E250F29BF00EA86F4 /* StoreListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreListView.swift; sourceTree = "<group>"; };
@ -138,8 +150,12 @@
50A3B79624026B7600D209EA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; }; 50A3B79624026B7600D209EA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
50A3B79824026B7600D209EA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 50A3B79824026B7600D209EA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
50A3B79924026B7600D209EA /* SecretAgent.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SecretAgent.entitlements; sourceTree = "<group>"; }; 50A3B79924026B7600D209EA /* SecretAgent.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SecretAgent.entitlements; sourceTree = "<group>"; };
50AE96FF2E5C1A420018C710 /* IntegrationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegrationsView.swift; sourceTree = "<group>"; };
50B8550C24138C4F009958AC /* DeleteSecretView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeleteSecretView.swift; sourceTree = "<group>"; }; 50B8550C24138C4F009958AC /* DeleteSecretView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeleteSecretView.swift; sourceTree = "<group>"; };
50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyStoreView.swift; sourceTree = "<group>"; }; 50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyStoreView.swift; sourceTree = "<group>"; };
50BDCB712E63BAF20072D2E7 /* AgentStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgentStatusView.swift; sourceTree = "<group>"; };
50BDCB732E6436C60072D2E7 /* ErrorStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorStyle.swift; sourceTree = "<group>"; };
50BDCB752E6450950072D2E7 /* ConfigurationItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationItemView.swift; sourceTree = "<group>"; };
50C385A42407A76D00AF2719 /* SecretDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretDetailView.swift; sourceTree = "<group>"; }; 50C385A42407A76D00AF2719 /* SecretDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretDetailView.swift; sourceTree = "<group>"; };
50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionButtonStyle.swift; sourceTree = "<group>"; }; 50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionButtonStyle.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
@ -179,6 +195,56 @@
path = Helpers; path = Helpers;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
504788ED2E681EB200B4556F /* Styles */ = {
isa = PBXGroup;
children = (
50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */,
50BDCB732E6436C60072D2E7 /* ErrorStyle.swift */,
504789222E697DD300B4556F /* BoxBackgroundStyle.swift */,
5065E312295517C500E16645 /* ToolbarButtonStyle.swift */,
);
path = Styles;
sourceTree = "<group>";
};
504788EE2E681EC300B4556F /* Secrets */ = {
isa = PBXGroup;
children = (
5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */,
50B8550C24138C4F009958AC /* DeleteSecretView.swift */,
2C4A9D2E2636FFD3008CC8E2 /* EditSecretView.swift */,
50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */,
506772C82425BB8500034DED /* NoStoresView.swift */,
50C385A42407A76D00AF2719 /* SecretDetailView.swift */,
50153E21250DECA300525160 /* SecretListItemView.swift */,
5079BA0E250F29BF00EA86F4 /* StoreListView.swift */,
);
path = Secrets;
sourceTree = "<group>";
};
504788EF2E681ED700B4556F /* Configuration */ = {
isa = PBXGroup;
children = (
50BDCB752E6450950072D2E7 /* ConfigurationItemView.swift */,
50AE96FF2E5C1A420018C710 /* IntegrationsView.swift */,
504788F12E681F3A00B4556F /* Instructions.swift */,
504788F32E681F6900B4556F /* ToolConfigurationView.swift */,
5066A6C12516F303004B5A36 /* SetupView.swift */,
504788F52E68206F00B4556F /* GettingStartedView.swift */,
);
path = Configuration;
sourceTree = "<group>";
};
504788F02E681F0100B4556F /* Views */ = {
isa = PBXGroup;
children = (
50BDCB712E63BAF20072D2E7 /* AgentStatusView.swift */,
50617D8423FCE48E0099B055 /* ContentView.swift */,
5066A6C72516FE6E004B5A36 /* CopyableView.swift */,
50153E1F250AFCB200525160 /* UpdateView.swift */,
);
path = Views;
sourceTree = "<group>";
};
50617D7623FCE48D0099B055 = { 50617D7623FCE48D0099B055 = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -241,20 +307,10 @@
508A58B0241ED1C40069DC07 /* Views */ = { 508A58B0241ED1C40069DC07 /* Views */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
50617D8423FCE48E0099B055 /* ContentView.swift */, 504788EF2E681ED700B4556F /* Configuration */,
5065E312295517C500E16645 /* ToolbarButtonStyle.swift */, 504788EE2E681EC300B4556F /* Secrets */,
50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */, 504788ED2E681EB200B4556F /* Styles */,
5079BA0E250F29BF00EA86F4 /* StoreListView.swift */, 504788F02E681F0100B4556F /* Views */,
50153E21250DECA300525160 /* SecretListItemView.swift */,
50C385A42407A76D00AF2719 /* SecretDetailView.swift */,
5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */,
50B8550C24138C4F009958AC /* DeleteSecretView.swift */,
2C4A9D2E2636FFD3008CC8E2 /* EditSecretView.swift */,
50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */,
506772C82425BB8500034DED /* NoStoresView.swift */,
50153E1F250AFCB200525160 /* UpdateView.swift */,
5066A6C12516F303004B5A36 /* SetupView.swift */,
5066A6C72516FE6E004B5A36 /* CopyableView.swift */,
); );
path = Views; path = Views;
sourceTree = "<group>"; sourceTree = "<group>";
@ -262,11 +318,11 @@
508A58B1241ED1EA0069DC07 /* Controllers */ = { 508A58B1241ED1EA0069DC07 /* Controllers */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
504788EB2E680DC400B4556F /* URLs.swift */,
508A58B2241ED2180069DC07 /* AgentStatusChecker.swift */, 508A58B2241ED2180069DC07 /* AgentStatusChecker.swift */,
5091D2BB25183B830049FD9B /* ApplicationDirectoryController.swift */, 5091D2BB25183B830049FD9B /* ApplicationDirectoryController.swift */,
50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */, 50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */,
50571E0424393D1500F76F6C /* LaunchAgentController.swift */, 50571E0424393D1500F76F6C /* LaunchAgentController.swift */,
5066A6F6251829B1004B5A36 /* ShellConfigurationController.swift */,
); );
path = Controllers; path = Controllers;
sourceTree = "<group>"; sourceTree = "<group>";
@ -433,26 +489,34 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
504788F22E681F3A00B4556F /* Instructions.swift in Sources */,
50BDCB742E6436CA0072D2E7 /* ErrorStyle.swift in Sources */,
2C4A9D2F2636FFD3008CC8E2 /* EditSecretView.swift in Sources */, 2C4A9D2F2636FFD3008CC8E2 /* EditSecretView.swift in Sources */,
5091D2BC25183B830049FD9B /* ApplicationDirectoryController.swift in Sources */, 5091D2BC25183B830049FD9B /* ApplicationDirectoryController.swift in Sources */,
504788EC2E680DC800B4556F /* URLs.swift in Sources */,
504789232E697DD300B4556F /* BoxBackgroundStyle.swift in Sources */,
5066A6C22516F303004B5A36 /* SetupView.swift in Sources */, 5066A6C22516F303004B5A36 /* SetupView.swift in Sources */,
5065E313295517C500E16645 /* ToolbarButtonStyle.swift in Sources */, 5065E313295517C500E16645 /* ToolbarButtonStyle.swift in Sources */,
50617D8523FCE48E0099B055 /* ContentView.swift in Sources */, 50617D8523FCE48E0099B055 /* ContentView.swift in Sources */,
504788F62E68206F00B4556F /* GettingStartedView.swift in Sources */,
50CF4ABC2E601B0F005588DC /* ActionButtonStyle.swift in Sources */, 50CF4ABC2E601B0F005588DC /* ActionButtonStyle.swift in Sources */,
50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */, 50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */,
5079BA0F250F29BF00EA86F4 /* StoreListView.swift in Sources */, 5079BA0F250F29BF00EA86F4 /* StoreListView.swift in Sources */,
50617DD223FCEFA90099B055 /* PreviewStore.swift in Sources */, 50617DD223FCEFA90099B055 /* PreviewStore.swift in Sources */,
5066A6F7251829B1004B5A36 /* ShellConfigurationController.swift in Sources */,
50033AC327813F1700253856 /* BundleIDs.swift in Sources */, 50033AC327813F1700253856 /* BundleIDs.swift in Sources */,
50BDCB722E63BAF20072D2E7 /* AgentStatusView.swift in Sources */,
508A58B3241ED2180069DC07 /* AgentStatusChecker.swift in Sources */, 508A58B3241ED2180069DC07 /* AgentStatusChecker.swift in Sources */,
50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */, 50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */,
5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */, 5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */,
50AE97002E5C1A420018C710 /* IntegrationsView.swift in Sources */,
50153E20250AFCB200525160 /* UpdateView.swift in Sources */, 50153E20250AFCB200525160 /* UpdateView.swift in Sources */,
50571E0524393D1500F76F6C /* LaunchAgentController.swift in Sources */, 50571E0524393D1500F76F6C /* LaunchAgentController.swift in Sources */,
5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */, 5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */,
50B8550D24138C4F009958AC /* DeleteSecretView.swift in Sources */, 50B8550D24138C4F009958AC /* DeleteSecretView.swift in Sources */,
50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */, 50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */,
50BDCB762E6450950072D2E7 /* ConfigurationItemView.swift in Sources */,
50617D8323FCE48E0099B055 /* App.swift in Sources */, 50617D8323FCE48E0099B055 /* App.swift in Sources */,
504788F42E681F6900B4556F /* ToolConfigurationView.swift in Sources */,
506772C92425BB8500034DED /* NoStoresView.swift in Sources */, 506772C92425BB8500034DED /* NoStoresView.swift in Sources */,
50153E22250DECA300525160 /* SecretListItemView.swift in Sources */, 50153E22250DECA300525160 /* SecretListItemView.swift in Sources */,
508A58B5241ED48F0069DC07 /* PreviewAgentStatusChecker.swift in Sources */, 508A58B5241ED48F0069DC07 /* PreviewAgentStatusChecker.swift in Sources */,
@ -647,10 +711,18 @@
ENABLE_APP_SANDBOX = YES; ENABLE_APP_SANDBOX = YES;
ENABLE_ENHANCED_SECURITY = YES; ENABLE_ENHANCED_SECURITY = YES;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
ENABLE_POINTER_AUTHENTICATION = YES; ENABLE_POINTER_AUTHENTICATION = YES;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
ENABLE_USER_SELECTED_FILES = readwrite; ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
ENABLE_RESOURCE_ACCESS_LOCATION = NO;
ENABLE_RESOURCE_ACCESS_PRINTING = NO;
ENABLE_RESOURCE_ACCESS_USB = NO;
INFOPLIST_FILE = Secretive/Info.plist; INFOPLIST_FILE = Secretive/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@ -679,10 +751,18 @@
ENABLE_APP_SANDBOX = YES; ENABLE_APP_SANDBOX = YES;
ENABLE_ENHANCED_SECURITY = YES; ENABLE_ENHANCED_SECURITY = YES;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
ENABLE_POINTER_AUTHENTICATION = YES; ENABLE_POINTER_AUTHENTICATION = YES;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
ENABLE_USER_SELECTED_FILES = readwrite; ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
ENABLE_RESOURCE_ACCESS_LOCATION = NO;
ENABLE_RESOURCE_ACCESS_PRINTING = NO;
ENABLE_RESOURCE_ACCESS_USB = NO;
INFOPLIST_FILE = Secretive/Info.plist; INFOPLIST_FILE = Secretive/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@ -783,10 +863,18 @@
ENABLE_APP_SANDBOX = YES; ENABLE_APP_SANDBOX = YES;
ENABLE_ENHANCED_SECURITY = YES; ENABLE_ENHANCED_SECURITY = YES;
ENABLE_HARDENED_RUNTIME = NO; ENABLE_HARDENED_RUNTIME = NO;
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
ENABLE_POINTER_AUTHENTICATION = YES; ENABLE_POINTER_AUTHENTICATION = YES;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
ENABLE_USER_SELECTED_FILES = readwrite; ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
ENABLE_RESOURCE_ACCESS_LOCATION = NO;
ENABLE_RESOURCE_ACCESS_PRINTING = NO;
ENABLE_RESOURCE_ACCESS_USB = NO;
INFOPLIST_FILE = Secretive/Info.plist; INFOPLIST_FILE = Secretive/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@ -809,8 +897,17 @@
DEVELOPMENT_ASSET_PATHS = "\"SecretAgent/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"SecretAgent/Preview Content\"";
ENABLE_APP_SANDBOX = YES; ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
ENABLE_RESOURCE_ACCESS_LOCATION = NO;
ENABLE_RESOURCE_ACCESS_PRINTING = NO;
ENABLE_RESOURCE_ACCESS_USB = NO;
INFOPLIST_FILE = SecretAgent/Info.plist; INFOPLIST_FILE = SecretAgent/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@ -835,8 +932,17 @@
DEVELOPMENT_TEAM = Z72PRUAWF6; DEVELOPMENT_TEAM = Z72PRUAWF6;
ENABLE_APP_SANDBOX = YES; ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
ENABLE_RESOURCE_ACCESS_LOCATION = NO;
ENABLE_RESOURCE_ACCESS_PRINTING = NO;
ENABLE_RESOURCE_ACCESS_USB = NO;
INFOPLIST_FILE = SecretAgent/Info.plist; INFOPLIST_FILE = SecretAgent/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@ -862,8 +968,17 @@
DEVELOPMENT_TEAM = Z72PRUAWF6; DEVELOPMENT_TEAM = Z72PRUAWF6;
ENABLE_APP_SANDBOX = YES; ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
ENABLE_RESOURCE_ACCESS_LOCATION = NO;
ENABLE_RESOURCE_ACCESS_PRINTING = NO;
ENABLE_RESOURCE_ACCESS_USB = NO;
INFOPLIST_FILE = SecretAgent/Info.plist; INFOPLIST_FILE = SecretAgent/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",

View File

@ -25,6 +25,9 @@ extension EnvironmentValues {
}() }()
@Entry var updater: any UpdaterProtocol = _updater @Entry var updater: any UpdaterProtocol = _updater
private static let _justUpdatedChecker = JustUpdatedChecker()
@Entry var justUpdatedChecker: any JustUpdatedCheckerProtocol = _justUpdatedChecker
@MainActor var secretStoreList: SecretStoreList { @MainActor var secretStoreList: SecretStoreList {
EnvironmentValues._secretStoreList EnvironmentValues._secretStoreList
} }
@ -33,10 +36,11 @@ extension EnvironmentValues {
@main @main
struct Secretive: App { struct Secretive: App {
private let justUpdatedChecker = JustUpdatedChecker()
@Environment(\.agentStatusChecker) var agentStatusChecker @Environment(\.agentStatusChecker) var agentStatusChecker
@Environment(\.justUpdatedChecker) var justUpdatedChecker
@AppStorage("defaultsHasRunSetup") var hasRunSetup = false @AppStorage("defaultsHasRunSetup") var hasRunSetup = false
@State private var showingSetup = false @State private var showingSetup = false
@State private var showingIntegrations = false
@State private var showingCreation = false @State private var showingCreation = false
@SceneBuilder var body: some Scene { @SceneBuilder var body: some Scene {
@ -51,15 +55,23 @@ struct Secretive: App {
.onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in .onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in
guard hasRunSetup else { return } guard hasRunSetup else { return }
agentStatusChecker.check() agentStatusChecker.check()
if agentStatusChecker.running && justUpdatedChecker.justUpdated { if agentStatusChecker.running && justUpdatedChecker.justUpdatedBuild {
// Relaunch the agent, since it'll be running from earlier update still // Relaunch the agent, since it'll be running from earlier update still
reinstallAgent() reinstallAgent()
} else if !agentStatusChecker.running && !agentStatusChecker.developmentBuild { } else if !agentStatusChecker.running && !agentStatusChecker.developmentBuild {
forceLaunchAgent() forceLaunchAgent()
} }
} }
.sheet(isPresented: $showingIntegrations) {
IntegrationsView()
}
} }
.commands { .commands {
CommandGroup(before: CommandGroupPlacement.appSettings) {
Button(.integrationsMenuBarTitle, systemImage: "app.connected.to.app.below.fill") {
showingIntegrations = true
}
}
CommandGroup(after: CommandGroupPlacement.newItem) { CommandGroup(after: CommandGroupPlacement.newItem) {
Button(.appMenuNewSecretButton) { Button(.appMenuNewSecretButton) {
showingCreation = true showingCreation = true
@ -71,11 +83,6 @@ struct Secretive: App {
NSWorkspace.shared.open(Constants.helpURL) NSWorkspace.shared.open(Constants.helpURL)
} }
} }
CommandGroup(after: .help) {
Button(.appMenuSetupButton) {
showingSetup = true
}
}
SidebarCommands() SidebarCommands()
} }
} }
@ -85,9 +92,8 @@ struct Secretive: App {
extension Secretive { extension Secretive {
private func reinstallAgent() { private func reinstallAgent() {
justUpdatedChecker.check()
Task { Task {
await LaunchAgentController().install() _ = await LaunchAgentController().install()
try? await Task.sleep(for: .seconds(1)) try? await Task.sleep(for: .seconds(1))
agentStatusChecker.check() agentStatusChecker.check()
if !agentStatusChecker.running { if !agentStatusChecker.running {

View File

@ -6,12 +6,14 @@ import Observation
@MainActor protocol AgentStatusCheckerProtocol: Observable, Sendable { @MainActor protocol AgentStatusCheckerProtocol: Observable, Sendable {
var running: Bool { get } var running: Bool { get }
var developmentBuild: Bool { get } var developmentBuild: Bool { get }
var process: NSRunningApplication? { get }
func check() func check()
} }
@Observable @MainActor final class AgentStatusChecker: AgentStatusCheckerProtocol { @Observable @MainActor final class AgentStatusChecker: AgentStatusCheckerProtocol {
var running: Bool = false var running: Bool = false
var process: NSRunningApplication? = nil
nonisolated init() { nonisolated init() {
Task { @MainActor in Task { @MainActor in
@ -20,32 +22,39 @@ import Observation
} }
func check() { func check() {
running = instanceSecretAgentProcess != nil process = instanceSecretAgentProcess
running = process != nil
} }
// All processes, including ones from older versions, etc // All processes, including ones from older versions, etc
var secretAgentProcesses: [NSRunningApplication] { var allSecretAgentProcesses: [NSRunningApplication] {
NSRunningApplication.runningApplications(withBundleIdentifier: Bundle.main.agentBundleID) NSRunningApplication.runningApplications(withBundleIdentifier: Bundle.agentBundleID)
} }
// The process corresponding to this instance of Secretive // The process corresponding to this instance of Secretive
var instanceSecretAgentProcess: NSRunningApplication? { var instanceSecretAgentProcess: NSRunningApplication? {
let agents = secretAgentProcesses // FIXME: CHECK VERSION
let agents = allSecretAgentProcesses
for agent in agents { for agent in agents {
guard let url = agent.bundleURL else { continue } guard let url = agent.bundleURL else { continue }
if url.absoluteString.hasPrefix(Bundle.main.bundleURL.absoluteString) { if url.absoluteString.hasPrefix(Bundle.main.bundleURL.absoluteString) || (url.isXcodeURL && developmentBuild) {
return agent return agent
} }
} }
return nil return nil
} }
// Whether Secretive is being run in an Xcode environment. // Whether Secretive is being run in an Xcode environment.
var developmentBuild: Bool { var developmentBuild: Bool {
Bundle.main.bundleURL.absoluteString.contains("/Library/Developer/Xcode") Bundle.main.bundleURL.isXcodeURL
} }
} }
extension URL {
var isXcodeURL: Bool {
absoluteString.contains("/Library/Developer/Xcode")
}
}

View File

@ -1,23 +1,33 @@
import Foundation import Foundation
import AppKit import AppKit
protocol JustUpdatedCheckerProtocol: Observable { @MainActor protocol JustUpdatedCheckerProtocol: Observable {
var justUpdated: Bool { get } var justUpdatedBuild: Bool { get }
var justUpdatedOS: Bool { get }
} }
@Observable class JustUpdatedChecker: JustUpdatedCheckerProtocol { @Observable @MainActor class JustUpdatedChecker: JustUpdatedCheckerProtocol {
var justUpdated: Bool = false var justUpdatedBuild: Bool = false
var justUpdatedOS: Bool = false
init() { nonisolated init() {
check() Task { @MainActor in
check()
}
} }
func check() { private func check() {
let lastBuild = UserDefaults.standard.object(forKey: Constants.previousVersionUserDefaultsKey) as? String ?? "None" let lastBuild = UserDefaults.standard.object(forKey: Constants.previousVersionUserDefaultsKey) as? String
let lastOS = UserDefaults.standard.object(forKey: Constants.previousOSVersionUserDefaultsKey) as? String
let currentBuild = Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String let currentBuild = Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String
let osRaw = ProcessInfo.processInfo.operatingSystemVersion
let currentOS = "\(osRaw.majorVersion).\(osRaw.minorVersion).\(osRaw.patchVersion)"
UserDefaults.standard.set(currentBuild, forKey: Constants.previousVersionUserDefaultsKey) UserDefaults.standard.set(currentBuild, forKey: Constants.previousVersionUserDefaultsKey)
justUpdated = lastBuild != currentBuild UserDefaults.standard.set(currentOS, forKey: Constants.previousOSVersionUserDefaultsKey)
justUpdatedBuild = lastBuild != currentBuild
// To prevent this showing on first lauch for every user, only show if lastBuild is non-nil.
justUpdatedOS = lastBuild != nil && lastOS != currentOS
} }
@ -28,6 +38,7 @@ extension JustUpdatedChecker {
enum Constants { enum Constants {
static let previousVersionUserDefaultsKey = "com.maxgoedjen.Secretive.lastBuild" static let previousVersionUserDefaultsKey = "com.maxgoedjen.Secretive.lastBuild"
static let previousOSVersionUserDefaultsKey = "com.maxgoedjen.Secretive.lastOS"
} }
} }

View File

@ -8,16 +8,28 @@ struct LaunchAgentController {
private let logger = Logger(subsystem: "com.maxgoedjen.secretive", category: "LaunchAgentController") private let logger = Logger(subsystem: "com.maxgoedjen.secretive", category: "LaunchAgentController")
func install() async { func install() async -> Bool {
logger.debug("Installing agent") logger.debug("Installing agent")
_ = setEnabled(false) _ = setEnabled(false)
// This is definitely a bit of a "seems to work better" thing but: // This is definitely a bit of a "seems to work better" thing but:
// Seems to more reliably hit if these are on separate runloops, otherwise it seems like it sometimes doesn't kill old // Seems to more reliably hit if these are on separate runloops, otherwise it seems like it sometimes doesn't kill old
// and start new? // and start new?
try? await Task.sleep(for: .seconds(1)) try? await Task.sleep(for: .seconds(1))
await MainActor.run { let result = await MainActor.run {
_ = setEnabled(true) setEnabled(true)
} }
try? await Task.sleep(for: .seconds(1))
return result
}
func uninstall() async -> Bool {
logger.debug("Uninstalling agent")
try? await Task.sleep(for: .seconds(1))
let result = await MainActor.run {
setEnabled(false)
}
try? await Task.sleep(for: .seconds(1))
return result
} }
func forceLaunch() async -> Bool { func forceLaunch() async -> Bool {
@ -28,6 +40,7 @@ struct LaunchAgentController {
do { do {
try await NSWorkspace.shared.openApplication(at: url, configuration: config) try await NSWorkspace.shared.openApplication(at: url, configuration: config)
logger.debug("Agent force launched") logger.debug("Agent force launched")
try? await Task.sleep(for: .seconds(1))
return true return true
} catch { } catch {
logger.error("Error force launching \(error.localizedDescription)") logger.error("Error force launching \(error.localizedDescription)")
@ -36,7 +49,7 @@ struct LaunchAgentController {
} }
private func setEnabled(_ enabled: Bool) -> Bool { private func setEnabled(_ enabled: Bool) -> Bool {
let service = SMAppService.loginItem(identifier: Bundle.main.agentBundleID) let service = SMAppService.loginItem(identifier: Bundle.agentBundleID)
do { do {
if enabled { if enabled {
try service.register() try service.register()

View File

@ -1,63 +0,0 @@
import Foundation
import Cocoa
import SecretKit
struct ShellConfigurationController {
let socketPath = (NSHomeDirectory().replacingOccurrences(of: Bundle.main.hostBundleID, with: Bundle.main.agentBundleID) as NSString).appendingPathComponent("socket.ssh") as String
var shellInstructions: [ShellConfigInstruction] {
[
ShellConfigInstruction(shell: "global",
shellConfigDirectory: "~/.ssh/",
shellConfigFilename: "config",
text: "Host *\n\tIdentityAgent \(socketPath)"),
ShellConfigInstruction(shell: "zsh",
shellConfigDirectory: "~/",
shellConfigFilename: ".zshrc",
text: "export SSH_AUTH_SOCK=\(socketPath)"),
ShellConfigInstruction(shell: "bash",
shellConfigDirectory: "~/",
shellConfigFilename: ".bashrc",
text: "export SSH_AUTH_SOCK=\(socketPath)"),
ShellConfigInstruction(shell: "fish",
shellConfigDirectory: "~/.config/fish",
shellConfigFilename: "config.fish",
text: "set -x SSH_AUTH_SOCK \(socketPath)"),
]
}
@MainActor func addToShell(shellInstructions: ShellConfigInstruction) -> Bool {
let openPanel = NSOpenPanel()
// This is sync, so no need to strongly retain
let delegate = Delegate(name: shellInstructions.shellConfigFilename)
openPanel.delegate = delegate
openPanel.message = "Select \(shellInstructions.shellConfigFilename) to let Secretive configure your shell automatically."
openPanel.prompt = "Add to \(shellInstructions.shellConfigFilename)"
openPanel.canChooseFiles = true
openPanel.canChooseDirectories = false
openPanel.showsHiddenFiles = true
openPanel.directoryURL = URL(fileURLWithPath: shellInstructions.shellConfigDirectory)
openPanel.nameFieldStringValue = shellInstructions.shellConfigFilename
openPanel.allowedContentTypes = [.symbolicLink, .data, .plainText]
openPanel.runModal()
guard let fileURL = openPanel.urls.first else { return false }
let handle: FileHandle
do {
handle = try FileHandle(forUpdating: fileURL)
guard let existing = try handle.readToEnd(),
let existingString = String(data: existing, encoding: .utf8) else { return false }
guard !existingString.contains(shellInstructions.text) else {
return true
}
try handle.seekToEnd()
} catch {
return false
}
handle.write(Data("\n# Secretive Config\n\(shellInstructions.text)\n".utf8))
return true
}
}

View File

@ -0,0 +1,25 @@
import Foundation
extension URL {
static var agentHomeURL: URL {
URL(fileURLWithPath: URL.homeDirectory.path().replacingOccurrences(of: Bundle.hostBundleID, with: Bundle.agentBundleID))
}
static var socketPath: String {
URL.agentHomeURL.appendingPathComponent("socket.ssh").path()
}
}
extension String {
var normalizedPathAndFolder: (String, String) {
// All foundation-based normalization methods replace this with the container directly.
let processedPath = replacingOccurrences(of: "~", with: "/Users/\(NSUserName())")
let url = URL(filePath: processedPath)
let folder = url.deletingLastPathComponent().path()
return (processedPath, folder)
}
}

View File

@ -1,7 +1,11 @@
import Foundation import Foundation
extension Bundle { extension Bundle {
public var agentBundleID: String {(self.bundleIdentifier?.replacingOccurrences(of: "Host", with: "SecretAgent"))!} public static var agentBundleID: String {
public var hostBundleID: String {(self.bundleIdentifier?.replacingOccurrences(of: "SecretAgent", with: "Host"))!} Bundle.main.bundleIdentifier!.replacingOccurrences(of: "Host", with: "SecretAgent")
}
public static var hostBundleID: String {
Bundle.main.bundleIdentifier!.replacingOccurrences(of: "SecretAgent", with: "Host")
}
} }

View File

@ -1,12 +1,15 @@
import Foundation import Foundation
import AppKit
class PreviewAgentStatusChecker: AgentStatusCheckerProtocol { class PreviewAgentStatusChecker: AgentStatusCheckerProtocol {
let running: Bool let running: Bool
let process: NSRunningApplication?
let developmentBuild = false let developmentBuild = false
init(running: Bool = true) { init(running: Bool = true, process: NSRunningApplication? = nil) {
self.running = running self.running = running
self.process = process
} }
func check() { func check() {

View File

@ -6,7 +6,7 @@ import Brief
var update: Release? = nil var update: Release? = nil
let testBuild = false let currentVersion = SemVer("0.0.0_preview")
init(update: Update = .none) { init(update: Update = .none) {
switch update { switch update {

View File

@ -1,24 +0,0 @@
import SwiftUI
struct PrimaryButtonModifier: ViewModifier {
@Environment(\.colorScheme) var colorScheme
func body(content: Content) -> some View {
// Tinted glass prominent is really hard to read on 26.0.
if #available(macOS 26.0, *), colorScheme == .dark {
content.buttonStyle(.glassProminent)
} else {
content.buttonStyle(.borderedProminent)
}
}
}
extension View {
func primary() -> some View {
modifier(PrimaryButtonModifier())
}
}

View File

@ -0,0 +1,56 @@
import SwiftUI
struct ConfigurationItemView<Content: View>: View {
enum Action: Hashable {
case copy(String)
case revealInFinder(String)
}
let title: LocalizedStringResource
let content: Content
let action: Action?
init(title: LocalizedStringResource, value: String, action: Action? = nil) where Content == Text {
self.title = title
self.content = Text(value)
.font(.subheadline)
.foregroundStyle(.secondary)
self.action = action
}
init(title: LocalizedStringResource, action: Action? = nil, content: () -> Content) {
self.title = title
self.content = content()
self.action = action
}
var body: some View {
VStack(alignment: .leading) {
HStack {
Text(title)
Spacer()
switch action {
case .copy(let string):
Button(.copyableClickToCopyButton, systemImage: "document.on.document") {
NSPasteboard.general.declareTypes([.string], owner: nil)
NSPasteboard.general.setString(string, forType: .string)
}
.labelStyle(.iconOnly)
.buttonStyle(.borderless)
case .revealInFinder(let rawPath):
Button(.revealInFinderButton, systemImage: "folder") {
let (processedPath, folder) = rawPath.normalizedPathAndFolder
NSWorkspace.shared.selectFile(processedPath, inFileViewerRootedAtPath: folder)
}
.labelStyle(.iconOnly)
.buttonStyle(.borderless)
case nil:
EmptyView()
}
}
content
}
}
}

View File

@ -0,0 +1,49 @@
import SwiftUI
struct GettingStartedView: View {
private let instructions = Instructions()
@Binding var selectedInstruction: ConfigurationFileInstructions?
init(selectedInstruction: Binding<ConfigurationFileInstructions?>) {
_selectedInstruction = selectedInstruction
}
var body: some View {
Form {
Section(.integrationsGettingStartedTitle) {
Text(.integrationsGettingStartedTitleDescription)
}
Section {
Group {
Text(.integrationsGettingStartedSuggestionSsh)
.onTapGesture {
self.selectedInstruction = instructions.ssh
}
VStack(alignment: .leading, spacing: 5) {
Text(.integrationsGettingStartedSuggestionShell)
Text(.integrationsGettingStartedSuggestionShellDefault(shellName: String(localized: instructions.defaultShell.tool)))
.font(.caption2)
}
.onTapGesture {
self.selectedInstruction = instructions.defaultShell
}
Text(.integrationsGettingStartedSuggestionGit)
.onTapGesture {
self.selectedInstruction = instructions.git
}
}
.foregroundStyle(.link)
} header: {
Text(.integrationsGettingStartedWhatShouldIConfigureTitle)
}
footer: {
Text(.integrationsGettingStartedMultipleConfig)
}
}
.formStyle(.grouped)
}
}

View File

@ -0,0 +1,179 @@
import Foundation
struct Instructions {
enum Constants {
static let publicKeyPathPlaceholder = "_PUBLIC_KEY_PATH_PLACEHOLDER_"
static let publicKeyPlaceholder = "_PUBLIC_KEY_PLACEHOLDER_"
}
var defaultShell: ConfigurationFileInstructions {
zsh
}
var gettingStarted: ConfigurationFileInstructions = ConfigurationFileInstructions(.integrationsGettingStartedRowTitle, id: .gettingStarted)
var ssh: ConfigurationFileInstructions {
ConfigurationFileInstructions(
tool: LocalizedStringResource.integrationsToolNameSsh,
configPath: "~/.ssh/config",
configText: "Host *\n\tIdentityAgent \(URL.socketPath)",
website: URL(string: "https://man.openbsd.org/ssh_config.5")!,
note: .integrationsSshSpecificKeyNote,
)
}
var git: ConfigurationFileInstructions {
ConfigurationFileInstructions(
tool: .integrationsToolNameGitSigning,
steps: [
.init(path: "~/.gitconfig", steps: [
.integrationsGitStepGitconfigDescription(publicKeyPathPlaceholder: Constants.publicKeyPathPlaceholder)
],
note: .integrationsGitStepGitconfigSectionNote
),
.init(
path: "~/.gitallowedsigners",
steps: [
LocalizedStringResource(stringLiteral: Constants.publicKeyPlaceholder)
],
note: .integrationsGitStepGitallowedsignersDescription
),
],
website: URL(string: "https://git-scm.com/docs/git-config")!,
)
}
var zsh: ConfigurationFileInstructions {
ConfigurationFileInstructions(
tool: .integrationsToolNameZsh,
configPath: "~/.zshrc",
configText: "export SSH_AUTH_SOCK=\(URL.socketPath)"
)
}
var instructions: [ConfigurationGroup] {
[
ConfigurationGroup(name: .integrationsGettingStartedSectionTitle, instructions: [
gettingStarted
]),
ConfigurationGroup(
name: .integrationsSystemSectionTitle,
instructions: [
ssh,
git,
]
),
ConfigurationGroup(name: .integrationsShellSectionTitle, instructions: [
zsh,
ConfigurationFileInstructions(
tool: .integrationsToolNameBash,
configPath: "~/.bashrc",
configText: "export SSH_AUTH_SOCK=\(URL.socketPath)"
),
ConfigurationFileInstructions(
tool: .integrationsToolNameFish,
configPath: "~/.config/fish/config.fish",
configText: "set -x SSH_AUTH_SOCK \(URL.socketPath)"
),
ConfigurationFileInstructions(.integrationsOtherShellRowTitle, id: .otherShell),
]),
ConfigurationGroup(name: .integrationsOtherSectionTitle, instructions: [
ConfigurationFileInstructions(.integrationsAppsRowTitle, id: .otherApp),
]),
]
}
}
struct ConfigurationGroup: Identifiable {
let id = UUID()
var name: LocalizedStringResource
var instructions: [ConfigurationFileInstructions] = []
}
struct ConfigurationFileInstructions: Hashable, Identifiable {
struct StepGroup: Hashable, Identifiable {
let path: String
let steps: [LocalizedStringResource]
let note: LocalizedStringResource?
var id: String { path }
init(path: String, steps: [LocalizedStringResource], note: LocalizedStringResource? = nil) {
self.path = path
self.steps = steps
self.note = note
}
func hash(into hasher: inout Hasher) {
id.hash(into: &hasher)
}
}
var id: ID
var tool: LocalizedStringResource
var steps: [StepGroup]
var requiresSecret: Bool
var website: URL?
init(
tool: LocalizedStringResource,
configPath: String,
configText: LocalizedStringResource,
requiresSecret: Bool = false,
website: URL? = nil,
note: LocalizedStringResource? = nil
) {
self.id = .tool(String(localized: tool))
self.tool = tool
self.steps = [StepGroup(path: configPath, steps: [configText], note: note)]
self.requiresSecret = requiresSecret
self.website = website
}
init(
tool: LocalizedStringResource,
steps: [StepGroup],
requiresSecret: Bool = false,
website: URL? = nil
) {
self.id = .tool(String(localized: tool))
self.tool = tool
self.steps = steps
self.requiresSecret = true
self.website = website
}
init(_ name: LocalizedStringResource, id: ID) {
self.id = id
tool = name
steps = []
requiresSecret = false
}
func hash(into hasher: inout Hasher) {
id.hash(into: &hasher)
}
enum ID: Identifiable, Hashable {
case gettingStarted
case tool(String)
case otherShell
case otherApp
var id: String {
switch self {
case .gettingStarted:
"getting_started"
case .tool(let name):
name
case .otherShell:
"other_shell"
case .otherApp:
"other_app"
}
}
}
}

View File

@ -0,0 +1,115 @@
import SwiftUI
struct IntegrationsView: View {
@Environment(\.dismiss) private var dismiss
@State private var selectedInstruction: ConfigurationFileInstructions?
private let instructions = Instructions()
var body: some View {
NavigationSplitView {
List(selection: $selectedInstruction) {
ForEach(instructions.instructions) { group in
Section(group.name) {
ForEach(group.instructions) { instruction in
Text(instruction.tool)
.padding(.vertical, 8)
.tag(instruction)
}
}
}
}
} detail: {
IntegrationsDetailView(selectedInstruction: $selectedInstruction)
.fauxToolbar {
Button(.setupDoneButton) {
dismiss()
}
.normalButton()
}
}
.onAppear {
selectedInstruction = instructions.gettingStarted
}
.frame(minHeight: 500)
}
}
extension View {
func fauxToolbar<Content: View>(content: () -> Content) -> some View {
modifier(FauxToolbarModifier(toolbarContent: content()))
}
}
struct FauxToolbarModifier<ToolbarContent: View>: ViewModifier {
var toolbarContent: ToolbarContent
func body(content: Content) -> some View {
VStack(alignment: .leading, spacing: 0) {
content
Divider()
HStack {
Spacer()
toolbarContent
.padding(.top, 8)
.padding(.trailing, 16)
.padding(.bottom, 16)
}
}
}
}
struct IntegrationsDetailView: View {
@Binding private var selectedInstruction: ConfigurationFileInstructions?
init(selectedInstruction: Binding<ConfigurationFileInstructions?>) {
_selectedInstruction = selectedInstruction
}
var body: some View {
if let selectedInstruction {
switch selectedInstruction.id {
case .gettingStarted:
GettingStartedView(selectedInstruction: $selectedInstruction)
case .tool:
ToolConfigurationView(selectedInstruction: selectedInstruction)
case .otherShell:
Form {
Section {
Link(.integrationsViewOtherGithubLink, destination: URL(string: "https://github.com/maxgoedjen/secretive-config-instructions/tree/main/shells")!)
} header: {
Text(.integrationsCommunityShellListDescription)
.font(.body)
}
}
.formStyle(.grouped)
case .otherApp:
Form {
Section {
Link(.integrationsViewOtherGithubLink, destination: URL(string: "https://github.com/maxgoedjen/secretive-config-instructions/tree/main/apps")!)
} header: {
Text(.integrationsCommunityAppsListDescription)
.font(.body)
}
}
.formStyle(.grouped)
}
}
}
}
#Preview {
IntegrationsView()
.frame(height: 500)
}

View File

@ -0,0 +1,187 @@
import SwiftUI
struct SetupView: View {
@Environment(\.dismiss) private var dismiss
@Binding var setupComplete: Bool
@State var showingIntegrations = false
@State var buttonWidth: CGFloat?
@State var installed = false
@State var updates = false
@State var integrations = false
var allDone: Bool {
installed && updates && integrations
}
var body: some View {
VStack {
VStack(alignment: .leading, spacing: 0) {
StepView(
title: .setupAgentTitle,
description: .setupAgentDescription,
systemImage: "lock.laptopcomputer",
) {
setupButton(
.setupAgentInstallButton,
complete: installed,
width: buttonWidth
) {
installed = true
Task {
await LaunchAgentController().install()
}
}
}
Divider()
StepView(
title: .setupUpdatesTitle,
description: .setupUpdatesDescription,
systemImage: "network.badge.shield.half.filled",
) {
setupButton(
.setupUpdatesOkButton,
complete: updates,
width: buttonWidth
) {
updates = true
}
}
Divider()
StepView(
title: .setupIntegrationsTitle,
description: .setupIntegrationsDescription,
systemImage: "firewall",
) {
setupButton(
.setupIntegrationsButton,
complete: integrations,
width: buttonWidth
) {
showingIntegrations = true
}
}
}
.onPreferenceChange(setupButton.WidthKey.self) { width in
buttonWidth = width
}
.background(.white.opacity(0.1), in: RoundedRectangle(cornerRadius: 10))
.frame(minWidth: 600, maxWidth: .infinity)
HStack {
Spacer()
Button(.setupDoneButton) {
setupComplete = true
dismiss()
}
.disabled(!allDone)
.primaryButton()
}
}
.interactiveDismissDisabled()
.padding()
.sheet(isPresented: $showingIntegrations, onDismiss: {
integrations = true
}, content: {
IntegrationsView()
})
}
}
struct setupButton: View {
struct WidthKey: @MainActor PreferenceKey {
@MainActor static var defaultValue: CGFloat? = nil
static func reduce(value: inout CGFloat?, nextValue: () -> CGFloat?) {
if let next = nextValue(), next > (value ?? -1) {
value = next
}
}
}
let label: LocalizedStringResource
let complete: Bool
let action: () -> Void
let width: CGFloat?
@State var currentWidth: CGFloat?
init(_ label: LocalizedStringResource, complete: Bool, width: CGFloat? = nil, action: @escaping () -> Void) {
self.label = label
self.complete = complete
self.action = action
self.width = width
}
var body: some View {
Button(action: action) {
HStack(spacing: 6) {
if complete {
Text(.setupStepCompleteButton)
Image(systemName: "checkmark.circle.fill")
} else {
Text(label)
}
}
.frame(width: width)
.padding(.vertical, 2)
.onGeometryChange(for: CGFloat.self) { proxy in
proxy.size.width
} action: { newValue in
currentWidth = newValue
}
}
.preference(key: WidthKey.self, value: currentWidth)
.primaryButton()
.disabled(complete)
.tint(complete ? .green : nil)
}
}
struct StepView<Content: View>: View {
let title: LocalizedStringResource
let icon: Image
let description: LocalizedStringResource
let actions: Content
init(title: LocalizedStringResource, description: LocalizedStringResource, systemImage: String, actions: () -> Content) {
self.title = title
self.icon = Image(systemName: systemImage)
self.description = description
self.actions = actions()
}
var body: some View {
HStack(spacing: 0) {
icon
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 24)
Spacer()
.frame(width: 20)
VStack(alignment: .leading, spacing: 4) {
Text(title)
.bold()
Text(description)
}
Spacer(minLength: 20)
actions
}
.padding(20)
}
}
extension SetupView {
enum Constants {
static let updaterFAQURL = URL(string: "https://github.com/maxgoedjen/secretive/blob/main/FAQ.md#whats-this-network-request-to-github")!
}
}
#Preview {
SetupView(setupComplete: .constant(false))
}

View File

@ -0,0 +1,110 @@
import SwiftUI
import SecretKit
struct ToolConfigurationView: View {
private let instructions = Instructions()
let selectedInstruction: ConfigurationFileInstructions
@Environment(\.secretStoreList) private var secretStoreList
@State var creating = false
@State var selectedSecret: AnySecret?
init(selectedInstruction: ConfigurationFileInstructions) {
self.selectedInstruction = selectedInstruction
}
var body: some View {
Form {
if selectedInstruction.requiresSecret {
if secretStoreList.allSecrets.isEmpty {
Section {
Text(.integrationsConfigureUsingSecretEmptyCreate)
if let store = secretStoreList.modifiableStore {
HStack {
Spacer()
Button(.createSecretTitle) {
creating = true
}
.sheet(isPresented: $creating) {
CreateSecretView(store: store) { created in
selectedSecret = created
}
}
}
}
}
} else {
Section {
Picker(.integrationsConfigureUsingSecretSecretTitle, selection: $selectedSecret) {
if selectedSecret == nil {
Text(.integrationsConfigureUsingSecretNoSecret)
.tag(nil as (AnySecret?))
}
ForEach(secretStoreList.allSecrets) { secret in
Text(secret.name)
.tag(secret)
}
}
} header: {
Text(.integrationsConfigureUsingSecretHeader)
}
.onAppear {
selectedSecret = secretStoreList.allSecrets.first
}
}
}
ForEach(selectedInstruction.steps) { stepGroup in
Section {
ConfigurationItemView(title: .integrationsPathTitle, value: stepGroup.path, action: .revealInFinder(stepGroup.path))
ForEach(stepGroup.steps, id: \.self.key) { step in
ConfigurationItemView(title: .integrationsAddThisTitle, action: .copy(String(localized: step))) {
HStack {
Text(placeholdersReplaced(text: String(localized: step)))
.padding(8)
.font(.system(.subheadline, design: .monospaced))
Spacer()
}
.frame(maxWidth: .infinity)
.background {
RoundedRectangle(cornerRadius: 6)
.fill(.black.opacity(0.05))
.stroke(.separator, lineWidth: 1)
}
}
}
} footer: {
if let note = stepGroup.note {
Text(note)
.font(.caption)
}
}
}
if let url = selectedInstruction.website {
Section {
Link(destination: url) {
VStack(alignment: .leading, spacing: 5) {
Text(.integrationsWebLink)
.font(.headline)
Text(url.absoluteString)
.font(.caption2)
}
}
}
}
}
.formStyle(.grouped)
}
func placeholdersReplaced(text: String) -> String {
guard let selectedSecret else { return text }
let writer = OpenSSHPublicKeyWriter()
let fileController = PublicKeyFileStoreController(homeDirectory: URL.agentHomeURL)
return text
.replacingOccurrences(of: Instructions.Constants.publicKeyPlaceholder, with: writer.openSSHString(secret: selectedSecret))
.replacingOccurrences(of: Instructions.Constants.publicKeyPathPlaceholder, with: fileController.publicKeyPath(for: selectedSecret))
}
}

View File

@ -4,13 +4,15 @@ import SecretKit
struct CreateSecretView<StoreType: SecretStoreModifiable>: View { struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
@State var store: StoreType @State var store: StoreType
@Binding var showing: Bool @Environment(\.dismiss) private var dismiss
var createdSecret: (AnySecret?) -> Void
@State private var name = "" @State private var name = ""
@State private var keyAttribution = "" @State private var keyAttribution = ""
@State private var authenticationRequirement: AuthenticationRequirement = .presenceRequired @State private var authenticationRequirement: AuthenticationRequirement = .presenceRequired
@State private var keyType: KeyType? @State private var keyType: KeyType?
@State var advanced = false @State var advanced = false
@State var errorText: String?
private var authenticationOptions: [AuthenticationRequirement] { private var authenticationOptions: [AuthenticationRequirement] {
if advanced || authenticationRequirement == .biometryCurrent { if advanced || authenticationRequirement == .biometryCurrent {
@ -64,7 +66,7 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
Text(.createSecretBiometryCurrentWarning) Text(.createSecretBiometryCurrentWarning)
.padding(.horizontal, 10) .padding(.horizontal, 10)
.padding(.vertical, 3) .padding(.vertical, 3)
.background(.red.opacity(0.5), in: RoundedRectangle(cornerRadius: 5)) .boxBackground(color: .red)
} }
} }
@ -83,7 +85,7 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
Text(.createSecretMldsaWarning) Text(.createSecretMldsaWarning)
.padding(.horizontal, 10) .padding(.horizontal, 10)
.padding(.vertical, 3) .padding(.vertical, 3)
.background(.red.opacity(0.5), in: RoundedRectangle(cornerRadius: 5)) .boxBackground(color: .orange)
} }
} }
VStack(alignment: .leading) { VStack(alignment: .leading) {
@ -94,16 +96,24 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
} }
} }
} }
if let errorText {
Section {
} footer: {
Text(verbatim: errorText)
.errorStyle()
}
}
} }
HStack { HStack {
Toggle(.createSecretAdvancedLabel, isOn: $advanced) Toggle(.createSecretAdvancedLabel, isOn: $advanced)
.toggleStyle(.button) .toggleStyle(.button)
Spacer() Spacer()
Button(.createSecretCancelButton, role: .cancel) { Button(.createSecretCancelButton, role: .cancel) {
showing = false dismiss()
} }
Button(.createSecretCreateButton, action: save) Button(.createSecretCreateButton, action: save)
.primary() .keyboardShortcut(.return)
.primaryButton()
.disabled(name.isEmpty) .disabled(name.isEmpty)
} }
.padding() .padding()
@ -117,20 +127,25 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
func save() { func save() {
let attribution = keyAttribution.isEmpty ? nil : keyAttribution let attribution = keyAttribution.isEmpty ? nil : keyAttribution
Task { Task {
try! await store.create( do {
name: name, let new = try await store.create(
attributes: .init( name: name,
keyType: keyType!, attributes: .init(
authentication: authenticationRequirement, keyType: keyType!,
publicKeyAttribution: attribution authentication: authenticationRequirement,
publicKeyAttribution: attribution
)
) )
) createdSecret(AnySecret(new))
showing = false dismiss()
} catch {
errorText = error.localizedDescription
}
} }
} }
} }
#Preview { //#Preview {
CreateSecretView(store: Preview.StoreModifiable(), showing: .constant(true)) // CreateSecretView(store: Preview.StoreModifiable()) { _ in }
} //}

View File

@ -28,8 +28,7 @@ struct DeleteSecretConfirmationModifier: ViewModifier {
TextField(secret.name, text: $confirmedSecretName) TextField(secret.name, text: $confirmedSecretName)
if let errorText { if let errorText {
Text(verbatim: errorText) Text(verbatim: errorText)
.foregroundStyle(.red) .errorStyle()
.font(.callout)
} }
Button(.deleteConfirmationDeleteButton, action: delete) Button(.deleteConfirmationDeleteButton, action: delete)
.disabled(confirmedSecretName != secret.name) .disabled(confirmedSecretName != secret.name)

View File

@ -30,21 +30,22 @@ struct EditSecretView<StoreType: SecretStoreModifiable>: View {
.font(.subheadline) .font(.subheadline)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
} } footer: {
if let errorText { if let errorText {
Text(verbatim: errorText) Text(verbatim: errorText)
.foregroundStyle(.red) .errorStyle()
.font(.callout) }
} }
} }
HStack { HStack {
Button(.editSaveButton, action: rename)
.disabled(name.isEmpty)
.keyboardShortcut(.return)
Button(.editCancelButton) { Button(.editCancelButton) {
dismissalBlock(false) dismissalBlock(false)
} }
.keyboardShortcut(.cancelAction) .keyboardShortcut(.cancelAction)
Button(.editSaveButton, action: rename)
.disabled(name.isEmpty)
.keyboardShortcut(.return)
.primaryButton()
} }
.padding() .padding()
} }

View File

@ -28,6 +28,8 @@ struct EmptyStoreImmutableView: View {
struct EmptyStoreModifiableView: View { struct EmptyStoreModifiableView: View {
@Environment(\.justUpdatedChecker) var justUpdatedChecker
var body: some View { var body: some View {
GeometryReader { windowGeometry in GeometryReader { windowGeometry in
VStack { VStack {
@ -51,21 +53,35 @@ struct EmptyStoreModifiableView: View {
}.frame(height: (windowGeometry.size.height/2) - 20).padding() }.frame(height: (windowGeometry.size.height/2) - 20).padding()
Text(.emptyStoreModifiableClickHereTitle).bold() Text(.emptyStoreModifiableClickHereTitle).bold()
Text(.emptyStoreModifiableClickHereDescription) Text(.emptyStoreModifiableClickHereDescription)
if justUpdatedChecker.justUpdatedOS {
Spacer()
.frame(height: 20)
VStack(spacing: 10) {
Text(.emptyStoreModifiableEmptyOsWarningTitle)
.font(.title2)
.bold()
Text(.emptyStoreModifiableEmptyOsWarningDescription)
.fixedSize(horizontal: false, vertical: true)
.bold()
}
.padding()
.boxBackground(color: .orange)
.padding()
}
Spacer() Spacer()
}.frame(maxWidth: .infinity, maxHeight: .infinity) }.frame(maxWidth: .infinity, maxHeight: .infinity)
} }
} }
} }
#if DEBUG
struct EmptyStoreModifiableView_Previews: PreviewProvider { #Preview {
static var previews: some View { EmptyStoreImmutableView()
Group { }
EmptyStoreImmutableView() #Preview {
EmptyStoreModifiableView() EmptyStoreImmutableView()
} // .environment(\.justUpdatedChecker, <#T##value: V##V#>)
} }
#Preview {
EmptyStoreModifiableView()
} }
#endif

View File

@ -13,12 +13,7 @@ struct NoStoresView: View {
} }
#if DEBUG #Preview {
NoStoresView()
struct NoStoresView_Previews: PreviewProvider {
static var previews: some View {
NoStoresView()
}
} }
#endif

View File

@ -6,7 +6,7 @@ struct SecretDetailView<SecretType: Secret>: View {
let secret: SecretType let secret: SecretType
private let keyWriter = OpenSSHPublicKeyWriter() private let keyWriter = OpenSSHPublicKeyWriter()
private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: NSHomeDirectory().replacingOccurrences(of: Bundle.main.hostBundleID, with: Bundle.main.agentBundleID)) private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: URL.agentHomeURL)
var body: some View { var body: some View {
ScrollView { ScrollView {
@ -21,7 +21,7 @@ struct SecretDetailView<SecretType: Secret>: View {
CopyableView(title: .secretDetailPublicKeyLabel, image: Image(systemName: "key"), text: keyString) CopyableView(title: .secretDetailPublicKeyLabel, image: Image(systemName: "key"), text: keyString)
Spacer() Spacer()
.frame(height: 20) .frame(height: 20)
CopyableView(title: .secretDetailPublicKeyPathLabel, image: Image(systemName: "lock.doc"), text: publicKeyFileStoreController.publicKeyPath(for: secret)) CopyableView(title: .secretDetailPublicKeyPathLabel, image: Image(systemName: "lock.doc"), text: publicKeyFileStoreController.publicKeyPath(for: secret), showRevealInFinder: true)
Spacer() Spacer()
} }
} }
@ -37,12 +37,6 @@ struct SecretDetailView<SecretType: Secret>: View {
} }
#if DEBUG //#Preview {
// SecretDetailView(secret: Preview.Secret(name: "Demonstration Secret"))
struct SecretDetailView_Previews: PreviewProvider { //}
static var previews: some View {
SecretDetailView(secret: Preview.Store(numberOfRandomSecrets: 1).secrets[0])
}
}
#endif

View File

@ -1,297 +0,0 @@
import SwiftUI
struct SetupView: View {
@State var stepIndex = 0
@Binding var visible: Bool
@Binding var setupComplete: Bool
var body: some View {
GeometryReader { proxy in
VStack {
StepView(numberOfSteps: 3, currentStep: stepIndex, width: proxy.size.width)
GeometryReader { _ in
HStack(spacing: 0) {
SecretAgentSetupView(buttonAction: advance)
.frame(width: proxy.size.width)
SSHAgentSetupView(buttonAction: advance)
.frame(width: proxy.size.width)
UpdaterExplainerView {
visible = false
setupComplete = true
}
.frame(width: proxy.size.width)
}
.offset(x: -proxy.size.width * Double(stepIndex), y: 0)
}
}
}
.frame(minWidth: 500, idealWidth: 500, minHeight: 500, idealHeight: 500)
}
func advance() {
withAnimation(.spring()) {
stepIndex += 1
}
}
}
struct StepView: View {
let numberOfSteps: Int
let currentStep: Int
// Ideally we'd have a geometry reader inside this view doing this for us, but that crashes on 11.0b7
let width: Double
var body: some View {
ZStack(alignment: .leading) {
Rectangle()
.foregroundColor(.blue)
.frame(height: 5)
Rectangle()
.foregroundColor(.green)
.frame(width: max(0, ((width - (Constants.padding * 2)) / Double(numberOfSteps - 1)) * Double(currentStep) - (Constants.circleWidth / 2)), height: 5)
HStack {
ForEach(Array(0..<numberOfSteps), id: \.self) { index in
ZStack {
if currentStep > index {
Circle()
.foregroundColor(.green)
.frame(width: Constants.circleWidth, height: Constants.circleWidth)
Text(.setupStepCompleteSymbol)
.foregroundColor(.white)
.bold()
} else {
Circle()
.foregroundColor(.blue)
.frame(width: Constants.circleWidth, height: Constants.circleWidth)
if currentStep == index {
Circle()
.strokeBorder(Color.white, lineWidth: 3)
.frame(width: Constants.circleWidth, height: Constants.circleWidth)
}
Text(String(describing: index + 1))
.foregroundColor(.white)
.bold()
}
}
if index < numberOfSteps - 1 {
Spacer(minLength: 30)
}
}
}
}.padding(Constants.padding)
}
}
extension StepView {
enum Constants {
static let padding: Double = 15
static let circleWidth: Double = 30
}
}
struct SetupStepView<Content> : View where Content : View {
let title: LocalizedStringResource
let image: Image
let bodyText: LocalizedStringResource
let buttonTitle: LocalizedStringResource
let buttonAction: () -> Void
let content: Content
init(title: LocalizedStringResource, image: Image, bodyText: LocalizedStringResource, buttonTitle: LocalizedStringResource, buttonAction: @escaping () -> Void = {}, @ViewBuilder content: () -> Content) {
self.title = title
self.image = image
self.bodyText = bodyText
self.buttonTitle = buttonTitle
self.buttonAction = buttonAction
self.content = content()
}
var body: some View {
VStack {
Text(title)
.font(.title)
Spacer()
image
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 64)
Spacer()
Text(bodyText)
.multilineTextAlignment(.center)
Spacer()
content
Spacer()
Button(buttonTitle) {
buttonAction()
}
}.padding()
}
}
struct SecretAgentSetupView: View {
let buttonAction: () -> Void
var body: some View {
SetupStepView(title: .setupAgentTitle,
image: Image(nsImage: NSApplication.shared.applicationIconImage),
bodyText: .setupAgentDescription,
buttonTitle: .setupAgentInstallButton,
buttonAction: install) {
Text(.setupAgentActivityMonitorDescription)
.multilineTextAlignment(.center)
}
}
func install() {
Task {
await LaunchAgentController().install()
buttonAction()
}
}
}
struct SSHAgentSetupView: View {
let buttonAction: () -> Void
private static let controller = ShellConfigurationController()
@State private var selectedShellInstruction: ShellConfigInstruction = controller.shellInstructions.first!
var body: some View {
SetupStepView(title: .setupSshTitle,
image: Image(systemName: "terminal"),
bodyText: .setupSshDescription,
buttonTitle: .setupSshAddedManuallyButton,
buttonAction: buttonAction) {
Link(.setupThirdPartyFaqLink, destination: URL(string: "https://github.com/maxgoedjen/secretive/blob/main/APP_CONFIG.md")!)
Picker(selection: $selectedShellInstruction, label: EmptyView()) {
ForEach(SSHAgentSetupView.controller.shellInstructions) { instruction in
Text(instruction.shell)
.tag(instruction)
.padding()
}
}.pickerStyle(SegmentedPickerStyle())
CopyableView(title: .setupSshAddToConfigButton(configPath: selectedShellInstruction.shellConfigPath), image: Image(systemName: "greaterthan.square"), text: selectedShellInstruction.text)
Button(.setupSshAddForMeButton) {
let controller = ShellConfigurationController()
if controller.addToShell(shellInstructions: selectedShellInstruction) {
buttonAction()
}
}
}
}
}
class Delegate: NSObject, NSOpenSavePanelDelegate {
private let name: String
init(name: String) {
self.name = name
}
func panel(_ sender: Any, shouldEnable url: URL) -> Bool {
return url.lastPathComponent == name
}
}
struct UpdaterExplainerView: View {
let buttonAction: () -> Void
var body: some View {
SetupStepView(title: .setupUpdatesTitle,
image: Image(systemName: "dot.radiowaves.left.and.right"),
bodyText: .setupUpdatesDescription,
buttonTitle: .setupUpdatesOk,
buttonAction: buttonAction) {
Link(.setupUpdatesReadmore, destination: SetupView.Constants.updaterFAQURL)
}
}
}
extension SetupView {
enum Constants {
static let updaterFAQURL = URL(string: "https://github.com/maxgoedjen/secretive/blob/main/FAQ.md#whats-this-network-request-to-github")!
}
}
struct ShellConfigInstruction: Identifiable, Hashable {
var shell: String
var shellConfigDirectory: String
var shellConfigFilename: String
var text: String
var id: String {
shell
}
var shellConfigPath: String {
return (shellConfigDirectory as NSString).appendingPathComponent(shellConfigFilename)
}
}
#if DEBUG
struct SetupView_Previews: PreviewProvider {
static var previews: some View {
Group {
SetupView(visible: .constant(true), setupComplete: .constant(false))
}
}
}
struct SecretAgentSetupView_Previews: PreviewProvider {
static var previews: some View {
Group {
SecretAgentSetupView(buttonAction: {})
}
}
}
struct SSHAgentSetupView_Previews: PreviewProvider {
static var previews: some View {
Group {
SSHAgentSetupView(buttonAction: {})
}
}
}
struct UpdaterExplainerView_Previews: PreviewProvider {
static var previews: some View {
Group {
UpdaterExplainerView(buttonAction: {})
}
}
}
#endif

View File

@ -0,0 +1,94 @@
import SwiftUI
struct PrimaryButtonModifier: ViewModifier {
@Environment(\.colorScheme) var colorScheme
@Environment(\.isEnabled) var isEnabled
func body(content: Content) -> some View {
// Tinted glass prominent is really hard to read on 26.0.
if #available(macOS 26.0, *), colorScheme == .dark, isEnabled {
content.buttonStyle(.glassProminent)
} else {
content.buttonStyle(.borderedProminent)
}
}
}
extension View {
func primaryButton() -> some View {
modifier(PrimaryButtonModifier())
}
}
struct MenuButtonModifier: ViewModifier {
func body(content: Content) -> some View {
if #available(macOS 26.0, *) {
content
.glassEffect(.regular.tint(.white.opacity(0.1)), in: .circle)
} else {
content
.buttonStyle(.borderless)
}
}
}
extension View {
func menuButton() -> some View {
modifier(MenuButtonModifier())
}
}
struct NormalButtonModifier: ViewModifier {
func body(content: Content) -> some View {
if #available(macOS 26.0, *) {
content.buttonStyle(.glass)
} else {
content.buttonStyle(.bordered)
}
}
}
extension View {
func normalButton() -> some View {
modifier(NormalButtonModifier())
}
}
struct DangerButtonModifier: ViewModifier {
@Environment(\.colorScheme) var colorScheme
func body(content: Content) -> some View {
// Tinted glass prominent is really hard to read on 26.0.
if #available(macOS 26.0, *), colorScheme == .dark {
content.buttonStyle(.glassProminent)
.tint(.red)
.foregroundStyle(.white)
} else {
content.buttonStyle(.borderedProminent)
.tint(.red)
.foregroundStyle(.white)
}
}
}
extension View {
func danger() -> some View {
modifier(DangerButtonModifier())
}
}

View File

@ -0,0 +1,32 @@
import SwiftUI
struct BoxBackgroundModifier: ViewModifier {
let color: Color
func body(content: Content) -> some View {
content
.background {
RoundedRectangle(cornerRadius: 5)
.fill(color.opacity(0.3))
.stroke(color, lineWidth: 1)
}
}
}
extension View {
func boxBackground(color: Color) -> some View {
modifier(BoxBackgroundModifier(color: color))
}
}
#Preview {
Text("Hello")
.boxBackground(color: .red)
.padding()
Text("Hello")
.boxBackground(color: .orange)
.padding()
}

View File

@ -0,0 +1,19 @@
import SwiftUI
struct ErrorStyleModifier: ViewModifier {
func body(content: Content) -> some View {
content
.foregroundStyle(.red)
.font(.callout)
}
}
extension View {
func errorStyle() -> some View {
modifier(ErrorStyleModifier())
}
}

View File

@ -0,0 +1,153 @@
import SwiftUI
struct AgentStatusView: View {
@Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol
var body: some View {
if agentStatusChecker.running {
AgentRunningView()
} else {
AgentNotRunningView()
}
}
}
struct AgentRunningView: View {
@Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol
var body: some View {
Form {
Section {
if let process = agentStatusChecker.process {
ConfigurationItemView(
title: .agentDetailsLocationTitle,
value: process.bundleURL!.path(),
action: .revealInFinder(process.bundleURL!.path()),
)
ConfigurationItemView(
title: .agentDetailsSocketPathTitle,
value: URL.socketPath,
action: .copy(URL.socketPath),
)
ConfigurationItemView(
title: .agentDetailsVersionTitle,
value: Bundle(url: process.bundleURL!)!.infoDictionary!["CFBundleShortVersionString"] as! String
)
if let launchDate = process.launchDate {
ConfigurationItemView(
title: .agentDetailsRunningSinceTitle,
value: launchDate.formatted()
)
}
}
} header: {
Text(.agentRunningNoticeDetailTitle)
.font(.headline)
.padding(.top)
} footer: {
VStack(alignment: .leading, spacing: 10) {
Text(.agentRunningNoticeDetailDescription)
HStack {
Spacer()
Menu(.agentDetailsRestartAgentButton) {
Button(.agentDetailsDisableAgentButton) {
Task {
_ = await LaunchAgentController()
.uninstall()
agentStatusChecker.check()
}
}
} primaryAction: {
Task {
let controller = LaunchAgentController()
let installed = await controller.install()
if !installed {
_ = await controller.forceLaunch()
}
agentStatusChecker.check()
}
}
}
}
.padding(.vertical)
}
}
.formStyle(.grouped)
.frame(width: 400)
}
}
struct AgentNotRunningView: View {
@Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol
@State var triedRestart = false
@State var loading = false
var body: some View {
Form {
Section {
} header: {
Text(.agentNotRunningNoticeTitle)
.font(.headline)
.padding(.top)
} footer: {
VStack(alignment: .leading, spacing: 10) {
Text(.agentNotRunningNoticeDetailDescription)
HStack {
if !triedRestart {
Spacer()
Button {
guard !loading else { return }
loading = true
Task {
let controller = LaunchAgentController()
let installed = await controller.install()
if !installed {
_ = await controller.forceLaunch()
}
agentStatusChecker.check()
loading = false
if !agentStatusChecker.running {
triedRestart = true
}
}
} label: {
if !loading {
Text(.agentDetailsStartAgentButton)
} else {
HStack {
Text(.agentDetailsStartAgentButtonStarting)
ProgressView()
.controlSize(.mini)
}
}
}
.primaryButton()
} else {
Text(.agentDetailsCouldNotStartError)
.bold()
.foregroundStyle(.red)
}
}
}
.padding(.bottom)
}
}
.formStyle(.grouped)
.frame(width: 400)
}
}
//#Preview {
// AgentStatusView()
// .environment(\.agentStatusChecker, PreviewAgentStatusChecker(running: false))
//}
//#Preview {
// AgentStatusView()
// .environment(\.agentStatusChecker, PreviewAgentStatusChecker(running: true, process: .current))
//}

View File

@ -36,7 +36,7 @@ struct ContentView: View {
toolbarItem(newItemView, id: "new") toolbarItem(newItemView, id: "new")
} }
.sheet(isPresented: $runningSetup) { .sheet(isPresented: $runningSetup) {
SetupView(visible: $runningSetup, setupComplete: $hasRunSetup) SetupView(setupComplete: $hasRunSetup)
} }
} }
@ -56,7 +56,7 @@ extension ContentView {
} }
var needsSetup: Bool { var needsSetup: Bool {
(runningSetup || !hasRunSetup || !agentStatusChecker.running) && !agentStatusChecker.developmentBuild runningSetup || !hasRunSetup
} }
/// Item either showing a "everything's good, here's more info" or "something's wrong, re-run setup" message /// Item either showing a "everything's good, here's more info" or "something's wrong, re-run setup" message
@ -66,7 +66,7 @@ extension ContentView {
if needsSetup { if needsSetup {
setupNoticeView setupNoticeView
} else { } else {
runningNoticeView agentStatusToolbarView
} }
} }
@ -75,7 +75,7 @@ extension ContentView {
if update.critical { if update.critical {
return (.updateCriticalNoticeTitle, .red) return (.updateCriticalNoticeTitle, .red)
} else { } else {
if updater.testBuild { if updater.currentVersion.isTestBuild {
return (.updateTestNoticeTitle, .blue) return (.updateTestNoticeTitle, .blue)
} else { } else {
return (.updateNormalNoticeTitle, .orange) return (.updateNormalNoticeTitle, .orange)
@ -94,8 +94,23 @@ extension ContentView {
.foregroundColor(.white) .foregroundColor(.white)
}) })
.buttonStyle(ToolbarButtonStyle(color: color)) .buttonStyle(ToolbarButtonStyle(color: color))
.popover(item: $selectedUpdate, attachmentAnchor: attachmentAnchor, arrowEdge: .bottom) { update in .sheet(item: $selectedUpdate) { update in
UpdateDetailView(update: update) VStack {
if updater.currentVersion.isTestBuild {
VStack {
if let description = updater.currentVersion.previewDescription {
Text(description)
}
Link(destination: URL(string: "https://github.com/maxgoedjen/secretive/actions/workflows/nightly.yml")!) {
Button(.updaterDownloadLatestNightlyButton) {}
.frame(maxWidth: .infinity)
.primaryButton()
}
}
.padding()
}
UpdateDetailView(update: update)
}
} }
} }
} }
@ -103,18 +118,17 @@ extension ContentView {
@ViewBuilder @ViewBuilder
var newItemView: some View { var newItemView: some View {
if storeList.modifiableStore?.isAvailable ?? false { if storeList.modifiableStore?.isAvailable ?? false {
Button(action: { Button(.appMenuNewSecretButton, systemImage: "plus") {
showingCreation = true showingCreation = true
}, label: { }
Image(systemName: "plus") .menuButton()
})
.sheet(isPresented: $showingCreation) { .sheet(isPresented: $showingCreation) {
if let modifiable = storeList.modifiableStore { if let modifiable = storeList.modifiableStore {
CreateSecretView(store: modifiable, showing: $showingCreation) CreateSecretView(store: modifiable) { created in
.onDisappear { if let created {
guard let newest = modifiable.secrets.last else { return } activeSecret = created
activeSecret = newest
} }
}
} }
} }
} }
@ -125,43 +139,44 @@ extension ContentView {
Button(action: { Button(action: {
runningSetup = true runningSetup = true
}, label: { }, label: {
Group { if !hasRunSetup {
if hasRunSetup && !agentStatusChecker.running { Text(.agentSetupNoticeTitle)
Text(.agentNotRunningNoticeTitle) .font(.headline)
} else {
Text(.agentSetupNoticeTitle)
}
} }
.font(.headline)
}) })
.buttonStyle(ToolbarButtonStyle(color: .orange)) .buttonStyle(ToolbarButtonStyle(color: .orange))
} }
@ViewBuilder @ViewBuilder
var runningNoticeView: some View { var agentStatusToolbarView: some View {
Button(action: { Button(action: {
showingAgentInfo = true showingAgentInfo = true
}, label: { }, label: {
HStack { HStack {
Text(.agentRunningNoticeTitle) if agentStatusChecker.running {
.font(.headline) Text(.agentRunningNoticeTitle)
.foregroundColor(colorScheme == .light ? Color(white: 0.3) : .white) .font(.headline)
Circle() .foregroundColor(colorScheme == .light ? Color(white: 0.3) : .white)
.frame(width: 10, height: 10) Circle()
.foregroundColor(Color.green) .frame(width: 10, height: 10)
.foregroundColor(Color.green)
} else {
Text(.agentNotRunningNoticeTitle)
.font(.headline)
Circle()
.frame(width: 10, height: 10)
.foregroundColor(Color.red)
}
} }
}) })
.buttonStyle(ToolbarButtonStyle(lightColor: .black.opacity(0.05), darkColor: .white.opacity(0.05))) .buttonStyle(
ToolbarButtonStyle(
lightColor: agentStatusChecker.running ? .black.opacity(0.05) : .red.opacity(0.75),
darkColor: agentStatusChecker.running ? .white.opacity(0.05) : .red.opacity(0.5),
)
)
.popover(isPresented: $showingAgentInfo, attachmentAnchor: attachmentAnchor, arrowEdge: .bottom) { .popover(isPresented: $showingAgentInfo, attachmentAnchor: attachmentAnchor, arrowEdge: .bottom) {
VStack { AgentStatusView()
Text(.agentRunningNoticeDetailTitle)
.font(.title)
.padding(5)
Text(.agentRunningNoticeDetailDescription)
.frame(width: 300)
}
.padding()
} }
} }
@ -193,31 +208,22 @@ extension ContentView {
} }
var attachmentAnchor: PopoverAttachmentAnchor { var attachmentAnchor: PopoverAttachmentAnchor {
// Ideally .point(.bottom), but broken on Sonoma (FB12726503)
.rect(.bounds) .rect(.bounds)
} }
} }
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
Group {
// Empty on modifiable and nonmodifiable
ContentView(showingCreation: .constant(false), runningSetup: .constant(false), hasRunSetup: .constant(true))
.environment(Preview.storeList(stores: [Preview.Store(numberOfRandomSecrets: 0)], modifiableStores: [Preview.StoreModifiable(numberOfRandomSecrets: 0)]))
.environment(PreviewUpdater())
// 5 items on modifiable and nonmodifiable
ContentView(showingCreation: .constant(false), runningSetup: .constant(false), hasRunSetup: .constant(true))
.environment(Preview.storeList(stores: [Preview.Store()], modifiableStores: [Preview.StoreModifiable()]))
.environment(PreviewUpdater())
}
}
}
#endif
//#Preview {
// // Empty on modifiable and nonmodifiable
// ContentView(showingCreation: .constant(false), runningSetup: .constant(false), hasRunSetup: .constant(true))
// .environment(Preview.storeList(stores: [Preview.Store(numberOfRandomSecrets: 0)], modifiableStores: [Preview.StoreModifiable(numberOfRandomSecrets: 0)]))
// .environment(PreviewUpdater())
//}
//
//#Preview {
// // 5 items on modifiable and nonmodifiable
// ContentView(showingCreation: .constant(false), runningSetup: .constant(false), hasRunSetup: .constant(true))
// .environment(Preview.storeList(stores: [Preview.Store()], modifiableStores: [Preview.StoreModifiable()]))
// .environment(PreviewUpdater())
//}

View File

@ -6,6 +6,7 @@ struct CopyableView: View {
var title: LocalizedStringResource var title: LocalizedStringResource
var image: Image var image: Image
var text: String var text: String
var showRevealInFinder = false
@State private var interactionState: InteractionState = .normal @State private var interactionState: InteractionState = .normal
@ -21,9 +22,12 @@ struct CopyableView: View {
.foregroundColor(primaryTextColor) .foregroundColor(primaryTextColor)
Spacer() Spacer()
if interactionState != .normal { if interactionState != .normal {
hoverIcon HStack {
.bold() if showRevealInFinder {
.textCase(.uppercase) revealInFinderButton
}
copyButton
}
.foregroundColor(secondaryTextColor) .foregroundColor(secondaryTextColor)
.transition(.opacity) .transition(.opacity)
} }
@ -72,19 +76,35 @@ struct CopyableView: View {
} }
@ViewBuilder @ViewBuilder
var hoverIcon: some View { var copyButton: some View {
switch interactionState { switch interactionState {
case .hovering: case .hovering:
Image(systemName: "document.on.document") Button(.copyableClickToCopyButton, systemImage: "document.on.document") {
.accessibilityLabel(String(localized: "copyable_click_to_copy_button")) withAnimation {
// Button will eat the click, so we set interaction state manually.
interactionState = .clicking
}
copy()
}
.labelStyle(.iconOnly)
.buttonStyle(.borderless)
case .clicking: case .clicking:
Image(systemName: "checkmark.circle.fill") Image(systemName: "checkmark.circle.fill")
.accessibilityLabel(String(localized: "copyable_copied")) .accessibilityLabel(String(localized: .copyableCopied))
case .normal, .dragging: case .normal, .dragging:
EmptyView() EmptyView()
} }
} }
var revealInFinderButton: some View {
Button(.revealInFinderButton, systemImage: "folder") {
let (processedPath, folder) = text.normalizedPathAndFolder
NSWorkspace.shared.selectFile(processedPath, inFileViewerRootedAtPath: folder)
}
.labelStyle(.iconOnly)
.buttonStyle(.borderless)
}
var primaryTextColor: Color { var primaryTextColor: Color {
switch interactionState { switch interactionState {
case .normal, .hovering, .dragging: case .normal, .hovering, .dragging:
@ -163,17 +183,12 @@ fileprivate struct BackgroundViewModifier: ViewModifier {
} }
#if DEBUG #Preview {
CopyableView(title: .secretDetailSha256FingerprintLabel, image: Image(systemName: "figure.wave"), text: "Hello world.")
struct CopyableView_Previews: PreviewProvider { .padding()
static var previews: some View {
Group {
CopyableView(title: "secret_detail_sha256_fingerprint_label", image: Image(systemName: "figure.wave"), text: "Hello world.")
.padding()
CopyableView(title: "secret_detail_sha256_fingerprint_label", image: Image(systemName: "figure.wave"), text: "Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. ")
.padding()
}
}
} }
#endif #Preview {
CopyableView(title: .secretDetailSha256FingerprintLabel, image: Image(systemName: "figure.wave"), text: "Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. ")
.padding()
}