Compare commits

..

4 Commits

Author SHA1 Message Date
Max Goedjen
0d16a6ffb1 Clear needs review. 2025-09-11 18:51:01 -07:00
Max Goedjen
95658072e7 Fixed back sides 2025-09-11 18:43:35 -07:00
Max Goedjen
7eeb09b1ec Few more 2025-09-11 18:31:45 -07:00
Max Goedjen
e6f21c13b0 Fix up strings (hopefully) 2025-09-11 18:27:47 -07:00
64 changed files with 1194 additions and 3028 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 668 KiB

After

Width:  |  Height:  |  Size: 520 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 618 KiB

After

Width:  |  Height:  |  Size: 519 KiB

BIN
.github/readme/localize_add.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

BIN
.github/readme/localize_sidebar.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

BIN
.github/readme/localize_translate.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 230 KiB

After

Width:  |  Height:  |  Size: 259 KiB

View File

@@ -25,8 +25,8 @@ jobs:
- language: actions - language: actions
build-mode: none build-mode: none
# Disable this until CodeQL supports Xcode 26 builds. # Disable this until CodeQL supports Xcode 26 builds.
- language: swift # - language: swift
build-mode: manual # build-mode: manual
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4

View File

@@ -12,7 +12,6 @@ jobs:
id-token: write id-token: write
contents: write contents: write
attestations: write attestations: write
actions: read
timeout-minutes: 10 timeout-minutes: 10
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
@@ -34,30 +33,23 @@ jobs:
DATE=$(date "+%Y-%m-%d") 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_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/Config/Config.xcconfig 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: Move to Artifact Folder - name: Create ZIP
run: mkdir Artifact; cp -r Archive.xcarchive/Products/Applications/Secretive.app Artifact
- name: Upload App to Artifacts
id: upload
uses: actions/upload-artifact@v4
with:
name: Secretive
path: Artifact
- name: Download Zipped Artifact
id: download
env:
ZIP_ID: ${{ steps.upload.outputs.artifact-id }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
curl -L -H "Authorization: Bearer $GITHUB_TOKEN" -L \ ditto -c -k --sequesterRsrc --keepParent Archive.xcarchive/Products/Applications/Secretive.app ./Secretive.zip
https://api.github.com/repos/maxgoedjen/secretive/actions/artifacts/$ZIP_ID/zip > Secretive.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

View File

@@ -32,7 +32,6 @@ jobs:
id-token: write id-token: write
contents: write contents: write
attestations: write attestations: write
actions: read
runs-on: macos-26 runs-on: macos-26
timeout-minutes: 10 timeout-minutes: 10
steps: steps:
@@ -56,43 +55,37 @@ jobs:
export CLEAN_TAG=$(echo $TAG_NAME | sed -e 's/refs\/tags\/v//') export CLEAN_TAG=$(echo $TAG_NAME | sed -e 's/refs\/tags\/v//')
sed -i '' -e "s/GITHUB_CI_VERSION/$CLEAN_TAG/g" Sources/Config/Config.xcconfig sed -i '' -e "s/GITHUB_CI_VERSION/$CLEAN_TAG/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/github.com\/maxgoedjen\/secretive\/actions\/runs\/$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
- 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: Move to Artifact Folder - name: Create ZIP
run: mkdir Artifact; cp -r Archive.xcarchive/Products/Applications/Secretive.app Artifact
- name: Upload App to Artifacts
id: upload
uses: actions/upload-artifact@v4
with:
name: Secretive.zip
path: Artifact
- name: Download Zipped Artifact
id: download
env:
ZIP_ID: ${{ steps.upload.outputs.artifact-id }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
curl -L -H "Authorization: Bearer $GITHUB_TOKEN" -L \ ditto -c -k --sequesterRsrc --keepParent Archive.xcarchive/Products/Applications/Secretive.app ./Secretive.zip
https://api.github.com/repos/maxgoedjen/secretive/actions/artifacts/$ZIP_ID/zip > Secretive.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" subject-name: "Secretive.zip"
subject-digest: ${{ steps.upload.outputs.artifact-digest }}
- name: Create Release - name: Create Release
run: |
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
gh release create $TAG_NAME -d -F .github/templates/release.md
gh release upload Secretive.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 }}
run: |
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
gh release create $TAG_NAME -d -F .github/templates/release.md
gh release upload $TAG_NAME Secretive.zip

View File

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

View File

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 69 KiB

View File

@@ -1,11 +1,11 @@
# Secretive [![Test](https://github.com/maxgoedjen/secretive/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/maxgoedjen/secretive/actions/workflows/test.yml) ![Release](https://github.com/maxgoedjen/secretive/workflows/Release/badge.svg) # Secretive [![Test](https://github.com/maxgoedjen/secretive/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/maxgoedjen/secretive/actions/workflows/test.yml) ![Release](https://github.com/maxgoedjen/secretive/workflows/Release/badge.svg)
Secretive is an app for protecting and managing SSH keys with the Secure Enclave. Secretive is an app for storing and managing SSH keys in the Secure Enclave. It is inspired by the [sekey project](https://github.com/sekey/sekey), but rewritten in Swift with no external dependencies and with a handy native management app.
<picture> <picture>
<source media="(prefers-color-scheme: dark)" srcset="/.github/readme/app-dark.png"> <source media="(prefers-color-scheme: dark)" srcset="/.github/readme/app-dark.png">
<source media="(prefers-color-scheme: light)" srcset="/.github/readme/app-light.png"> <img src="/.github/readme/app-light.png" alt="Screenshot of Secretive" width="600">
<img src="/.github/readme/app-dark.png" alt="Screenshot of Secretive" width="600">
</picture> </picture>
@@ -13,7 +13,7 @@ Secretive is an app for protecting and managing SSH keys with the Secure Enclave
### Safer Storage ### Safer Storage
The most common setup for SSH keys is just keeping them on disk, guarded by proper permissions. This is fine in most cases, but it's not super hard for malicious users or malware to copy your private key. If you protect your keys with the Secure Enclave, it's impossible to export them, by design. The most common setup for SSH keys is just keeping them on disk, guarded by proper permissions. This is fine in most cases, but it's not super hard for malicious users or malware to copy your private key. If you store your keys in the Secure Enclave, it's impossible to export them, by design.
### Access Control ### Access Control
@@ -53,7 +53,7 @@ Builds are produced by GitHub Actions with an auditable build and release genera
### A Note Around Code Signing and Keychains ### A Note Around Code Signing and Keychains
While Secretive uses the Secure Enclave to protect keys, it still relies on Keychain APIs to store and access them. Keychain restricts reads of keys to the app (and specifically, the bundle ID) that created them. If you build Secretive from source, make sure you are consistent in which bundle ID you use so that the Keychain is able to locate your keys. While Secretive uses the Secure Enclave for key storage, it still relies on Keychain APIs to access them. Keychain restricts reads of keys to the app (and specifically, the bundle ID) that created them. If you build Secretive from source, make sure you are consistent in which bundle ID you use so that the Keychain is able to locate your keys.
### Backups and Transfers to New Machines ### Backups and Transfers to New Machines
@@ -62,11 +62,3 @@ Because secrets in the Secure Enclave are not exportable, they are not able to b
## Security ## 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) 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)
## Acknowledgements
### sekey
Secretive was inspired by the [sekey project](https://github.com/sekey/sekey).
### Localization
Secretive is localized to many languages by a generous team of volunteers. To learn more, see [LOCALIZING.md](LOCALIZING.md). Secretive's localization workflow is generously provided by [Crowdin](https://crowdin.com).

View File

@@ -1,3 +1,2 @@
CI_VERSION = GITHUB_CI_VERSION CI_VERSION = GITHUB_CI_VERSION
CI_BUILD_NUMBER = GITHUB_BUILD_NUMBER CI_BUILD_NUMBER = GITHUB_BUILD_NUMBER
CI_BUILD_LINK = GITHUB_BUILD_URL

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,7 @@
import Foundation import Foundation
import SwiftUI
/// A release is a representation of a downloadable update. /// A release is a representation of a downloadable update.
public struct Release: Codable, Sendable, Hashable { public struct Release: Codable, Sendable {
/// The user-facing name of the release. Typically "Secretive 1.2.3" /// The user-facing name of the release. Typically "Secretive 1.2.3"
public let name: String public let name: String
@@ -16,8 +15,6 @@ public struct Release: Codable, Sendable, Hashable {
/// A user-facing description of the contents of the update. /// A user-facing description of the contents of the update.
public let body: String public let body: String
public let attributedBody: AttributedString
/// Initializes a Release. /// Initializes a Release.
/// - Parameters: /// - Parameters:
/// - name: The user-facing name of the release. /// - name: The user-facing name of the release.
@@ -29,56 +26,6 @@ public struct Release: Codable, Sendable, Hashable {
self.prerelease = prerelease self.prerelease = prerelease
self.html_url = html_url self.html_url = html_url
self.body = body self.body = body
self.attributedBody = AttributedString(_markdown: body)
}
public init(_ release: GitHubRelease) {
self.name = release.name
self.prerelease = release.prerelease
self.html_url = release.html_url
self.body = release.body
self.attributedBody = AttributedString(_markdown: release.body)
}
}
public struct GitHubRelease: Codable, Sendable {
let name: String
let prerelease: Bool
let html_url: URL
let body: String
}
fileprivate extension AttributedString {
init(_markdown markdown: String) {
let split = markdown.split(whereSeparator: \.isNewline)
let lines = split
.compactMap {
try? AttributedString(markdown: String($0), options: .init(allowsExtendedAttributes: true, interpretedSyntax: .full))
}
.map { (string: AttributedString) in
guard case let .header(level) = string.runs.first?.presentationIntent?.components.first?.kind else { return string }
return AttributedString("\n") + string
.transformingAttributes(\.font) { font in
font.value = switch level {
case 2: .headline.bold()
case 3: .headline
default: .subheadline
}
}
.transformingAttributes(\.underlineStyle) { underline in
underline.value = switch level {
case 2: .single
default: .none
}
}
+ AttributedString("\n")
}
self = lines.reduce(into: AttributedString()) { partialResult, next in
partialResult.append(next)
partialResult.append(AttributedString("\n"))
}
} }
} }

View File

@@ -47,7 +47,6 @@ extension Agent {
logger.debug("Agent returned \(SSHAgent.Response.agentSignResponse.debugDescription)") logger.debug("Agent returned \(SSHAgent.Response.agentSignResponse.debugDescription)")
case .unknown(let value): case .unknown(let value):
logger.error("Agent received unknown request of type \(value).") logger.error("Agent received unknown request of type \(value).")
throw UnhandledRequestError()
default: default:
logger.debug("Agent received valid request of type \(request.debugDescription), but not currently supported.") logger.debug("Agent received valid request of type \(request.debugDescription), but not currently supported.")
throw UnhandledRequestError() throw UnhandledRequestError()

View File

@@ -64,7 +64,7 @@ public final class AnySecretStoreModifiable: AnySecretStore, SecretStoreModifiab
private let _create: @Sendable (String, Attributes) async throws -> AnySecret private let _create: @Sendable (String, Attributes) async throws -> AnySecret
private let _delete: @Sendable (AnySecret) async throws -> Void private let _delete: @Sendable (AnySecret) async throws -> Void
private let _update: @Sendable (AnySecret, String, Attributes) async throws -> Void private let _update: @Sendable (AnySecret, String, Attributes) async throws -> Void
private let _supportedKeyTypes: @Sendable () -> KeyAvailability private let _supportedKeyTypes: @Sendable () -> [KeyType]
public init<SecretStoreType>(_ secretStore: SecretStoreType) where SecretStoreType: SecretStoreModifiable { public init<SecretStoreType>(_ secretStore: SecretStoreType) where SecretStoreType: SecretStoreModifiable {
_create = { AnySecret(try await secretStore.create(name: $0, attributes: $1)) } _create = { AnySecret(try await secretStore.create(name: $0, attributes: $1)) }
@@ -87,7 +87,7 @@ public final class AnySecretStoreModifiable: AnySecretStore, SecretStoreModifiab
try await _update(secret, name, attributes) try await _update(secret, name, attributes)
} }
public var supportedKeyTypes: KeyAvailability { public var supportedKeyTypes: [KeyType] {
_supportedKeyTypes() _supportedKeyTypes()
} }

View File

@@ -19,10 +19,9 @@ public final class PublicKeyFileStoreController: Sendable {
public func generatePublicKeys(for secrets: [AnySecret], clear: Bool = false) throws { public func generatePublicKeys(for secrets: [AnySecret], clear: Bool = false) throws {
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) }) let validPaths = Set(secrets.map { publicKeyPath(for: $0) }).union(Set(secrets.map { sshCertificatePath(for: $0) }))
.union(Set(secrets.map { sshCertificatePath(for: $0) }))
let contentsOfDirectory = (try? FileManager.default.contentsOfDirectory(atPath: directory.path())) ?? [] let contentsOfDirectory = (try? FileManager.default.contentsOfDirectory(atPath: directory.path())) ?? []
let fullPathContents = contentsOfDirectory.map { directory.appending(path: $0).path() } let fullPathContents = contentsOfDirectory.map { "\(directory)/\($0)" }
let untracked = Set(fullPathContents) let untracked = Set(fullPathContents)
.subtracting(validPaths) .subtracting(validPaths)

View File

@@ -62,37 +62,10 @@ public protocol SecretStoreModifiable<SecretType>: SecretStore {
/// - attributes: The new attributes for the secret. /// - attributes: The new attributes for the secret.
func update(secret: SecretType, name: String, attributes: Attributes) async throws func update(secret: SecretType, name: String, attributes: Attributes) async throws
var supportedKeyTypes: KeyAvailability { get } var supportedKeyTypes: [KeyType] { get }
} }
public struct KeyAvailability: Sendable {
public let available: [KeyType]
public let unavailable: [UnavailableKeyType]
public init(available: [KeyType], unavailable: [UnavailableKeyType]) {
self.available = available
self.unavailable = unavailable
}
public struct UnavailableKeyType: Sendable {
public let keyType: KeyType
public let reason: Reason
public init(keyType: KeyType, reason: Reason) {
self.keyType = keyType
self.reason = reason
}
public enum Reason: Sendable {
case macOSUpdateRequired
}
}
}
extension NSNotification.Name { extension NSNotification.Name {
// Distributed notification that keys were modified out of process (ie, that the management tool added/removed secrets) // Distributed notification that keys were modified out of process (ie, that the management tool added/removed secrets)

View File

@@ -50,16 +50,16 @@ extension SecureEnclave {
let secret = Secret(id: UUID().uuidString, name: name, publicKey: parsed.publicKey.x963Representation, attributes: Attributes(keyType: .init(algorithm: .ecdsa, size: 256), authentication: auth)) let secret = Secret(id: UUID().uuidString, name: name, publicKey: parsed.publicKey.x963Representation, attributes: Attributes(keyType: .init(algorithm: .ecdsa, size: 256), authentication: auth))
guard !migratedPublicKeys.contains(parsed.publicKey.x963Representation) else { guard !migratedPublicKeys.contains(parsed.publicKey.x963Representation) else {
logger.log("Skipping \(name), public key already present. Marking as migrated.") logger.log("Skipping \(name), public key already present. Marking as migrated.")
markMigrated(secret: secret, oldID: id) try markMigrated(secret: secret, oldID: id)
continue continue
} }
logger.log("Migrating \(name).") logger.log("Migrating \(name).")
try store.saveKey(tokenObjectID, name: name, attributes: secret.attributes) try store.saveKey(tokenObjectID, name: name, attributes: secret.attributes)
logger.log("Migrated \(name).") logger.log("Migrated \(name).")
markMigrated(secret: secret, oldID: id) try markMigrated(secret: secret, oldID: id)
migratedAny = true migratedAny = true
} catch { } catch {
logger.error("Failed to migrate \(name): \(error.localizedDescription).") logger.error("Failed to migrate \(name): \(error).")
} }
} }
if migratedAny { if migratedAny {
@@ -69,10 +69,10 @@ extension SecureEnclave {
public func markMigrated(secret: Secret, oldID: Data) { public func markMigrated(secret: Secret, oldID: Data) throws {
let updateQuery = KeychainDictionary([ let updateQuery = KeychainDictionary([
kSecClass: kSecClassKey, kSecClass: kSecClassKey,
kSecAttrApplicationLabel: oldID kSecAttrApplicationLabel: secret.id
]) ])
let newID = oldID + Constants.migrationMagicNumber let newID = oldID + Constants.migrationMagicNumber
@@ -82,7 +82,7 @@ extension SecureEnclave {
let status = SecItemUpdate(updateQuery, updatedAttributes) let status = SecItemUpdate(updateQuery, updatedAttributes)
if status != errSecSuccess { if status != errSecSuccess {
logger.warning("Failed to mark \(secret.name) as migrated: \(status).") throw KeychainError(statusCode: status)
} }
} }

View File

@@ -186,22 +186,17 @@ extension SecureEnclave {
await reloadSecrets() await reloadSecrets()
} }
public let supportedKeyTypes: KeyAvailability = { public var supportedKeyTypes: [KeyType] {
let macOS26Keys: [KeyType] = [.mldsa65, .mldsa87] if #available(macOS 26, *) {
let isAtLeastMacOS26 = if #available(macOS 26, *) { [
true
} else {
false
}
return KeyAvailability(
available: [
.ecdsa256, .ecdsa256,
] + (isAtLeastMacOS26 ? macOS26Keys : []), .mldsa65,
unavailable: (isAtLeastMacOS26 ? [] : macOS26Keys).map { .mldsa87,
KeyAvailability.UnavailableKeyType(keyType: $0, reason: .macOSUpdateRequired) ]
} else {
[.ecdsa256]
}
} }
)
}()
} }
} }

View File

@@ -34,9 +34,7 @@ public final class XPCServiceDelegate: NSObject, NSXPCListenerDelegate {
if let error = error as? Codable & Error { if let error = error as? Codable & Error {
reply(nil, NSError(error)) reply(nil, NSError(error))
} else { } else {
// Sending cast directly tries to serialize it and crashes XPCEncoder. reply(nil, error)
let cast = error as NSError
reply(nil, NSError(domain: cast.domain, code: cast.code, userInfo: [NSLocalizedDescriptionKey: error.localizedDescription]))
} }
} }
} }

View File

@@ -2,24 +2,18 @@ import Foundation
import SecretAgentKit import SecretAgentKit
import Brief import Brief
import XPCWrappers import XPCWrappers
import OSLog
/// Delegates all agent input parsing to an XPC service which wraps OpenSSH /// Delegates all agent input parsing to an XPC service which wraps OpenSSH
public final class XPCAgentInputParser: SSHAgentInputParserProtocol { public final class XPCAgentInputParser: SSHAgentInputParserProtocol {
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "XPCAgentInputParser")
private let session: XPCTypedSession<SSHAgent.Request, SSHAgentInputParser.AgentParsingError> private let session: XPCTypedSession<SSHAgent.Request, SSHAgentInputParser.AgentParsingError>
public init() async throws { public init() async throws {
logger.debug("Creating XPCAgentInputParser")
session = try await XPCTypedSession(serviceName: "com.maxgoedjen.Secretive.SecretAgentInputParser", warmup: true) session = try await XPCTypedSession(serviceName: "com.maxgoedjen.Secretive.SecretAgentInputParser", warmup: true)
logger.debug("XPCAgentInputParser is warmed up.")
} }
public func parse(data: Data) async throws -> SSHAgent.Request { public func parse(data: Data) async throws -> SSHAgent.Request {
logger.debug("Parsing input") try await session.send(data)
defer { logger.debug("Parsed input") }
return try await session.send(data)
} }
deinit { deinit {

View File

@@ -19,6 +19,7 @@
5003EF632780081B00DF2006 /* SecureEnclaveSecretKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5003EF622780081B00DF2006 /* SecureEnclaveSecretKit */; }; 5003EF632780081B00DF2006 /* SecureEnclaveSecretKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5003EF622780081B00DF2006 /* SecureEnclaveSecretKit */; };
5003EF652780081B00DF2006 /* SmartCardSecretKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5003EF642780081B00DF2006 /* SmartCardSecretKit */; }; 5003EF652780081B00DF2006 /* SmartCardSecretKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5003EF642780081B00DF2006 /* SmartCardSecretKit */; };
5008C23E2E525D8900507AC2 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 5008C23D2E525D8200507AC2 /* Localizable.xcstrings */; }; 5008C23E2E525D8900507AC2 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 5008C23D2E525D8200507AC2 /* Localizable.xcstrings */; };
5008C2402E52792400507AC2 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50617D8623FCE48E0099B055 /* Assets.xcassets */; };
5008C2412E52D18700507AC2 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 5008C23D2E525D8200507AC2 /* Localizable.xcstrings */; }; 5008C2412E52D18700507AC2 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 5008C23D2E525D8200507AC2 /* Localizable.xcstrings */; };
501421622781262300BBAA70 /* Brief in Frameworks */ = {isa = PBXBuildFile; productRef = 501421612781262300BBAA70 /* Brief */; }; 501421622781262300BBAA70 /* Brief in Frameworks */ = {isa = PBXBuildFile; productRef = 501421612781262300BBAA70 /* Brief */; };
501421652781268000BBAA70 /* SecretAgent.app in CopyFiles */ = {isa = PBXBuildFile; fileRef = 50A3B78A24026B7500D209EA /* SecretAgent.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 501421652781268000BBAA70 /* SecretAgent.app in CopyFiles */ = {isa = PBXBuildFile; fileRef = 50A3B78A24026B7500D209EA /* SecretAgent.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
@@ -32,13 +33,16 @@
504788F62E68206F00B4556F /* GettingStartedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F52E68206F00B4556F /* GettingStartedView.swift */; }; 504788F62E68206F00B4556F /* GettingStartedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F52E68206F00B4556F /* GettingStartedView.swift */; };
504789232E697DD300B4556F /* BoxBackgroundStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504789222E697DD300B4556F /* BoxBackgroundStyle.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 */; };
50617D8323FCE48E0099B055 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D8223FCE48E0099B055 /* App.swift */; }; 50617D8323FCE48E0099B055 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D8223FCE48E0099B055 /* App.swift */; };
50617D8523FCE48E0099B055 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D8423FCE48E0099B055 /* ContentView.swift */; }; 50617D8523FCE48E0099B055 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D8423FCE48E0099B055 /* ContentView.swift */; };
50617D8723FCE48E0099B055 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50617D8623FCE48E0099B055 /* Assets.xcassets */; };
50617D8A23FCE48E0099B055 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50617D8923FCE48E0099B055 /* Preview Assets.xcassets */; }; 50617D8A23FCE48E0099B055 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50617D8923FCE48E0099B055 /* Preview Assets.xcassets */; };
50617DD223FCEFA90099B055 /* PreviewStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617DD123FCEFA90099B055 /* PreviewStore.swift */; }; 50617DD223FCEFA90099B055 /* PreviewStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617DD123FCEFA90099B055 /* PreviewStore.swift */; };
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 */; };
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 */; };
50692D1D2E6FDB880043C7BB /* SecretiveUpdater.xpc in Embed XPC Services */ = {isa = PBXBuildFile; fileRef = 50692D122E6FDB880043C7BB /* SecretiveUpdater.xpc */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 50692D1D2E6FDB880043C7BB /* SecretiveUpdater.xpc in Embed XPC Services */ = {isa = PBXBuildFile; fileRef = 50692D122E6FDB880043C7BB /* SecretiveUpdater.xpc */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
50692D282E6FDB8D0043C7BB /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50692D242E6FDB8D0043C7BB /* main.swift */; }; 50692D282E6FDB8D0043C7BB /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50692D242E6FDB8D0043C7BB /* main.swift */; };
@@ -69,10 +73,6 @@
50BDCB762E6450950072D2E7 /* ConfigurationItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BDCB752E6450950072D2E7 /* ConfigurationItemView.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 */; };
50E4C4532E73C78C00C73783 /* WindowBackgroundStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E4C4522E73C78900C73783 /* WindowBackgroundStyle.swift */; };
50E4C4C32E7765DF00C73783 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E4C4C22E7765DF00C73783 /* AboutView.swift */; };
50E4C4C82E777E4200C73783 /* AppIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = 50E4C4C72E777E4200C73783 /* AppIcon.icon */; };
50E4C4C92E777E4200C73783 /* AppIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = 50E4C4C72E777E4200C73783 /* AppIcon.icon */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@@ -193,10 +193,11 @@
504788F52E68206F00B4556F /* GettingStartedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GettingStartedView.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>"; }; 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>"; };
5059933F2E7A3B5B0092CFFA /* en */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = en; path = en.lproj/Main.storyboard; 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; };
50617D8223FCE48E0099B055 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = "<group>"; }; 50617D8223FCE48E0099B055 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = "<group>"; };
50617D8423FCE48E0099B055 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; }; 50617D8423FCE48E0099B055 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
50617D8623FCE48E0099B055 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
50617D8923FCE48E0099B055 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; }; 50617D8923FCE48E0099B055 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
50617D8E23FCE48E0099B055 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 50617D8E23FCE48E0099B055 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
50617D8F23FCE48E0099B055 /* Secretive.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Secretive.entitlements; sourceTree = "<group>"; }; 50617D8F23FCE48E0099B055 /* Secretive.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Secretive.entitlements; sourceTree = "<group>"; };
@@ -204,6 +205,7 @@
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>"; };
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>"; };
50692BA52E6D5CC90043C7BB /* InternetAccessPolicy.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = InternetAccessPolicy.plist; sourceTree = "<group>"; }; 50692BA52E6D5CC90043C7BB /* InternetAccessPolicy.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = InternetAccessPolicy.plist; sourceTree = "<group>"; };
50692D122E6FDB880043C7BB /* SecretiveUpdater.xpc */ = {isa = PBXFileReference; explicitFileType = "wrapper.xpc-service"; includeInIndex = 0; path = SecretiveUpdater.xpc; sourceTree = BUILT_PRODUCTS_DIR; }; 50692D122E6FDB880043C7BB /* SecretiveUpdater.xpc */ = {isa = PBXFileReference; explicitFileType = "wrapper.xpc-service"; includeInIndex = 0; path = SecretiveUpdater.xpc; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -226,6 +228,7 @@
5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateSecretView.swift; sourceTree = "<group>"; }; 5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateSecretView.swift; sourceTree = "<group>"; };
50A3B78A24026B7500D209EA /* SecretAgent.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SecretAgent.app; sourceTree = BUILT_PRODUCTS_DIR; }; 50A3B78A24026B7500D209EA /* SecretAgent.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SecretAgent.app; sourceTree = BUILT_PRODUCTS_DIR; };
50A3B79324026B7600D209EA /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; }; 50A3B79324026B7600D209EA /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; 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>"; }; 50AE96FF2E5C1A420018C710 /* IntegrationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegrationsView.swift; sourceTree = "<group>"; };
@@ -236,9 +239,6 @@
50BDCB752E6450950072D2E7 /* ConfigurationItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationItemView.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>"; };
50E4C4522E73C78900C73783 /* WindowBackgroundStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowBackgroundStyle.swift; sourceTree = "<group>"; };
50E4C4C22E7765DF00C73783 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
50E4C4C72E777E4200C73783 /* AppIcon.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = AppIcon.icon; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@@ -293,16 +293,15 @@
path = Helpers; path = Helpers;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
504788ED2E681EB200B4556F /* Modifiers */ = { 504788ED2E681EB200B4556F /* Styles */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
50E4C4522E73C78900C73783 /* WindowBackgroundStyle.swift */,
50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */, 50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */,
50BDCB732E6436C60072D2E7 /* ErrorStyle.swift */, 50BDCB732E6436C60072D2E7 /* ErrorStyle.swift */,
504789222E697DD300B4556F /* BoxBackgroundStyle.swift */, 504789222E697DD300B4556F /* BoxBackgroundStyle.swift */,
5065E312295517C500E16645 /* ToolbarButtonStyle.swift */, 5065E312295517C500E16645 /* ToolbarButtonStyle.swift */,
); );
path = Modifiers; path = Styles;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
504788EE2E681EC300B4556F /* Secrets */ = { 504788EE2E681EC300B4556F /* Secrets */ = {
@@ -336,7 +335,6 @@
504788F02E681F0100B4556F /* Views */ = { 504788F02E681F0100B4556F /* Views */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
50E4C4C22E7765DF00C73783 /* AboutView.swift */,
50BDCB712E63BAF20072D2E7 /* AgentStatusView.swift */, 50BDCB712E63BAF20072D2E7 /* AgentStatusView.swift */,
50617D8423FCE48E0099B055 /* ContentView.swift */, 50617D8423FCE48E0099B055 /* ContentView.swift */,
5066A6C72516FE6E004B5A36 /* CopyableView.swift */, 5066A6C72516FE6E004B5A36 /* CopyableView.swift */,
@@ -377,10 +375,11 @@
508A58B0241ED1C40069DC07 /* Views */, 508A58B0241ED1C40069DC07 /* Views */,
508A58B1241ED1EA0069DC07 /* Controllers */, 508A58B1241ED1EA0069DC07 /* Controllers */,
50033AC427813F1C00253856 /* Helpers */, 50033AC427813F1C00253856 /* Helpers */,
50617D8623FCE48E0099B055 /* Assets.xcassets */,
50617D8E23FCE48E0099B055 /* Info.plist */, 50617D8E23FCE48E0099B055 /* Info.plist */,
508BF28D25B4F005009EFB7E /* InternetAccessPolicy.plist */, 508BF28D25B4F005009EFB7E /* InternetAccessPolicy.plist */,
50E4C4C72E777E4200C73783 /* AppIcon.icon */,
50617D8F23FCE48E0099B055 /* Secretive.entitlements */, 50617D8F23FCE48E0099B055 /* Secretive.entitlements */,
506772C62424784600034DED /* Credits.rtf */,
5008C23D2E525D8200507AC2 /* Localizable.xcstrings */, 5008C23D2E525D8200507AC2 /* Localizable.xcstrings */,
50617D8823FCE48E0099B055 /* Preview Content */, 50617D8823FCE48E0099B055 /* Preview Content */,
); );
@@ -433,7 +432,7 @@
children = ( children = (
504788EF2E681ED700B4556F /* Configuration */, 504788EF2E681ED700B4556F /* Configuration */,
504788EE2E681EC300B4556F /* Secrets */, 504788EE2E681EC300B4556F /* Secrets */,
504788ED2E681EB200B4556F /* Modifiers */, 504788ED2E681EB200B4556F /* Styles */,
504788F02E681F0100B4556F /* Views */, 504788F02E681F0100B4556F /* Views */,
); );
path = Views; path = Views;
@@ -446,6 +445,7 @@
508A58B2241ED2180069DC07 /* AgentStatusChecker.swift */, 508A58B2241ED2180069DC07 /* AgentStatusChecker.swift */,
5091D2BB25183B830049FD9B /* ApplicationDirectoryController.swift */, 5091D2BB25183B830049FD9B /* ApplicationDirectoryController.swift */,
50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */, 50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */,
50571E0424393D1500F76F6C /* LaunchAgentController.swift */,
); );
path = Controllers; path = Controllers;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -613,6 +613,7 @@
hasScannedForEncodings = 0; hasScannedForEncodings = 0;
knownRegions = ( knownRegions = (
en, en,
Base,
it, it,
fr, fr,
de, de,
@@ -642,8 +643,9 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
50617D8A23FCE48E0099B055 /* Preview Assets.xcassets in Resources */, 50617D8A23FCE48E0099B055 /* Preview Assets.xcassets in Resources */,
50E4C4C82E777E4200C73783 /* AppIcon.icon in Resources */,
5008C23E2E525D8900507AC2 /* Localizable.xcstrings in Resources */, 5008C23E2E525D8900507AC2 /* Localizable.xcstrings in Resources */,
50617D8723FCE48E0099B055 /* Assets.xcassets in Resources */,
506772C72424784600034DED /* Credits.rtf in Resources */,
508BF28E25B4F005009EFB7E /* InternetAccessPolicy.plist in Resources */, 508BF28E25B4F005009EFB7E /* InternetAccessPolicy.plist in Resources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@@ -669,8 +671,8 @@
50A3B79724026B7600D209EA /* Main.storyboard in Resources */, 50A3B79724026B7600D209EA /* Main.storyboard in Resources */,
5008C2412E52D18700507AC2 /* Localizable.xcstrings in Resources */, 5008C2412E52D18700507AC2 /* Localizable.xcstrings in Resources */,
50A3B79424026B7600D209EA /* Preview Assets.xcassets in Resources */, 50A3B79424026B7600D209EA /* Preview Assets.xcassets in Resources */,
50E4C4C92E777E4200C73783 /* AppIcon.icon in Resources */,
508BF2AA25B4F1CB009EFB7E /* InternetAccessPolicy.plist in Resources */, 508BF2AA25B4F1CB009EFB7E /* InternetAccessPolicy.plist in Resources */,
5008C2402E52792400507AC2 /* Assets.xcassets in Resources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -683,9 +685,7 @@
files = ( files = (
504788F22E681F3A00B4556F /* Instructions.swift in Sources */, 504788F22E681F3A00B4556F /* Instructions.swift in Sources */,
50BDCB742E6436CA0072D2E7 /* ErrorStyle.swift in Sources */, 50BDCB742E6436CA0072D2E7 /* ErrorStyle.swift in Sources */,
50E4C4C32E7765DF00C73783 /* AboutView.swift in Sources */,
2C4A9D2F2636FFD3008CC8E2 /* EditSecretView.swift in Sources */, 2C4A9D2F2636FFD3008CC8E2 /* EditSecretView.swift in Sources */,
50E4C4532E73C78C00C73783 /* WindowBackgroundStyle.swift in Sources */,
5091D2BC25183B830049FD9B /* ApplicationDirectoryController.swift in Sources */, 5091D2BC25183B830049FD9B /* ApplicationDirectoryController.swift in Sources */,
504788EC2E680DC800B4556F /* URLs.swift in Sources */, 504788EC2E680DC800B4556F /* URLs.swift in Sources */,
504789232E697DD300B4556F /* BoxBackgroundStyle.swift in Sources */, 504789232E697DD300B4556F /* BoxBackgroundStyle.swift in Sources */,
@@ -704,6 +704,7 @@
5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */, 5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */,
50AE97002E5C1A420018C710 /* IntegrationsView.swift in Sources */, 50AE97002E5C1A420018C710 /* IntegrationsView.swift in Sources */,
50153E20250AFCB200525160 /* UpdateView.swift in Sources */, 50153E20250AFCB200525160 /* UpdateView.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 */,
@@ -783,7 +784,7 @@
50A3B79524026B7600D209EA /* Main.storyboard */ = { 50A3B79524026B7600D209EA /* Main.storyboard */ = {
isa = PBXVariantGroup; isa = PBXVariantGroup;
children = ( children = (
5059933F2E7A3B5B0092CFFA /* en */, 50A3B79624026B7600D209EA /* Base */,
); );
name = Main.storyboard; name = Main.storyboard;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -830,6 +831,8 @@
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf; DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_ENHANCED_SECURITY = YES;
ENABLE_POINTER_AUTHENTICATION = YES;
ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES; ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -904,7 +907,9 @@
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_ENHANCED_SECURITY = YES;
ENABLE_NS_ASSERTIONS = NO; ENABLE_NS_ASSERTIONS = NO;
ENABLE_POINTER_AUTHENTICATION = YES;
ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
@@ -945,9 +950,11 @@
DEVELOPMENT_ASSET_PATHS = "\"Secretive/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"Secretive/Preview Content\"";
DEVELOPMENT_TEAM = Z72PRUAWF6; DEVELOPMENT_TEAM = Z72PRUAWF6;
ENABLE_APP_SANDBOX = YES; ENABLE_APP_SANDBOX = YES;
ENABLE_ENHANCED_SECURITY = YES;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO; ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = NO; ENABLE_OUTGOING_NETWORK_CONNECTIONS = NO;
ENABLE_POINTER_AUTHENTICATION = YES;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO; ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
@@ -983,9 +990,11 @@
DEVELOPMENT_ASSET_PATHS = "\"Secretive/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"Secretive/Preview Content\"";
DEVELOPMENT_TEAM = Z72PRUAWF6; DEVELOPMENT_TEAM = Z72PRUAWF6;
ENABLE_APP_SANDBOX = YES; ENABLE_APP_SANDBOX = YES;
ENABLE_ENHANCED_SECURITY = YES;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO; ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = NO; ENABLE_OUTGOING_NETWORK_CONNECTIONS = NO;
ENABLE_POINTER_AUTHENTICATION = YES;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO; ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
@@ -1036,7 +1045,7 @@
INFOPLIST_KEY_CFBundleDisplayName = SecretiveUpdater; INFOPLIST_KEY_CFBundleDisplayName = SecretiveUpdater;
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 Max Goedjen. All rights reserved."; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 Max Goedjen. All rights reserved.";
LOCALIZATION_PREFERS_STRING_CATALOGS = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MACOSX_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 26.0;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretiveUpdater; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretiveUpdater;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@@ -1076,7 +1085,7 @@
INFOPLIST_KEY_CFBundleDisplayName = SecretiveUpdater; INFOPLIST_KEY_CFBundleDisplayName = SecretiveUpdater;
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 Max Goedjen. All rights reserved."; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 Max Goedjen. All rights reserved.";
LOCALIZATION_PREFERS_STRING_CATALOGS = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MACOSX_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 26.0;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretiveUpdater; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretiveUpdater;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@@ -1118,7 +1127,7 @@
INFOPLIST_KEY_CFBundleDisplayName = SecretiveUpdater; INFOPLIST_KEY_CFBundleDisplayName = SecretiveUpdater;
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 Max Goedjen. All rights reserved."; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 Max Goedjen. All rights reserved.";
LOCALIZATION_PREFERS_STRING_CATALOGS = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MACOSX_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 26.0;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretiveUpdater; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretiveUpdater;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@@ -1150,7 +1159,7 @@
INFOPLIST_KEY_CFBundleDisplayName = SecretAgentInputParser; INFOPLIST_KEY_CFBundleDisplayName = SecretAgentInputParser;
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 Max Goedjen. All rights reserved."; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 Max Goedjen. All rights reserved.";
LOCALIZATION_PREFERS_STRING_CATALOGS = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MACOSX_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 26.0;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretAgentInputParser; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretAgentInputParser;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@@ -1180,7 +1189,7 @@
INFOPLIST_KEY_CFBundleDisplayName = SecretAgentInputParser; INFOPLIST_KEY_CFBundleDisplayName = SecretAgentInputParser;
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 Max Goedjen. All rights reserved."; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 Max Goedjen. All rights reserved.";
LOCALIZATION_PREFERS_STRING_CATALOGS = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MACOSX_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 26.0;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretAgentInputParser; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretAgentInputParser;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@@ -1212,7 +1221,7 @@
INFOPLIST_KEY_CFBundleDisplayName = SecretAgentInputParser; INFOPLIST_KEY_CFBundleDisplayName = SecretAgentInputParser;
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 Max Goedjen. All rights reserved."; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 Max Goedjen. All rights reserved.";
LOCALIZATION_PREFERS_STRING_CATALOGS = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MACOSX_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 26.0;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretAgentInputParser; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretAgentInputParser;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@@ -1266,6 +1275,8 @@
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf; DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_ENHANCED_SECURITY = YES;
ENABLE_POINTER_AUTHENTICATION = YES;
ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES; ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -1312,9 +1323,11 @@
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_ASSET_PATHS = "\"Secretive/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"Secretive/Preview Content\"";
ENABLE_APP_SANDBOX = YES; ENABLE_APP_SANDBOX = YES;
ENABLE_ENHANCED_SECURITY = YES;
ENABLE_HARDENED_RUNTIME = NO; ENABLE_HARDENED_RUNTIME = NO;
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO; ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = NO; ENABLE_OUTGOING_NETWORK_CONNECTIONS = NO;
ENABLE_POINTER_AUTHENTICATION = YES;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO; ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;

View File

@@ -4,88 +4,6 @@ import SecureEnclaveSecretKit
import SmartCardSecretKit import SmartCardSecretKit
import Brief import Brief
@main
struct Secretive: App {
@Environment(\.agentLaunchController) var agentLaunchController
@Environment(\.justUpdatedChecker) var justUpdatedChecker
@SceneBuilder var body: some Scene {
WindowGroup {
ContentView()
.environment(EnvironmentValues._secretStoreList)
.onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in
Task {
@AppStorage("defaultsHasRunSetup") var hasRunSetup = false
@AppStorage("explicitlyDisabled") var explicitlyDisabled = false
guard hasRunSetup && !explicitlyDisabled else { return }
agentLaunchController.check()
guard !agentLaunchController.developmentBuild else { return }
if justUpdatedChecker.justUpdatedBuild || !agentLaunchController.running {
// Relaunch the agent, since it'll be running from earlier update still
try await agentLaunchController.forceLaunch()
}
}
}
}
.commands {
AppCommands()
}
WindowGroup(id: String(describing: IntegrationsView.self)) {
IntegrationsView()
}
.windowResizability(.contentMinSize)
WindowGroup(id: String(describing: AboutView.self)) {
AboutView()
}
.windowStyle(.hiddenTitleBar)
.windowResizability(.contentSize)
}
}
extension Secretive {
struct AppCommands: Commands {
@Environment(\.openWindow) var openWindow
@Environment(\.openURL) var openURL
@FocusedValue(\.showCreateSecret) var showCreateSecret
var body: some Commands {
CommandGroup(replacing: .appInfo) {
Button(.aboutMenuBarTitle, systemImage: "info.circle") {
openWindow(id: String(describing: AboutView.self))
}
}
CommandGroup(before: CommandGroupPlacement.appSettings) {
Button(.integrationsMenuBarTitle, systemImage: "app.connected.to.app.below.fill") {
openWindow(id: String(describing: IntegrationsView.self))
}
}
CommandGroup(after: CommandGroupPlacement.newItem) {
Button(.appMenuNewSecretButton, systemImage: "plus") {
showCreateSecret?()
}
.keyboardShortcut(KeyboardShortcut(KeyEquivalent("N"), modifiers: [.command, .shift]))
.disabled(showCreateSecret?.isEnabled == false)
}
CommandGroup(replacing: .help) {
Button(.appMenuHelpButton) {
openURL(Constants.helpURL)
}
}
SidebarCommands()
}
}
}
private enum Constants {
static let helpURL = URL(string: "https://github.com/maxgoedjen/secretive/blob/main/FAQ.md")!
}
extension EnvironmentValues { extension EnvironmentValues {
// This is injected through .environment modifier below instead of @Entry for performance reasons (basially, restrictions around init/mainactor causing delay in loading secrets/"empty screen" blip). // This is injected through .environment modifier below instead of @Entry for performance reasons (basially, restrictions around init/mainactor causing delay in loading secrets/"empty screen" blip).
@@ -99,8 +17,8 @@ extension EnvironmentValues {
return list return list
}() }()
private static let _agentLaunchController = AgentLaunchController() private static let _agentStatusChecker = AgentStatusChecker()
@Entry var agentLaunchController: any AgentLaunchControllerProtocol = _agentLaunchController @Entry var agentStatusChecker: any AgentStatusCheckerProtocol = _agentStatusChecker
private static let _updater: any UpdaterProtocol = { private static let _updater: any UpdaterProtocol = {
@AppStorage("defaultsHasRunSetup") var hasRunSetup = false @AppStorage("defaultsHasRunSetup") var hasRunSetup = false
return Updater(checkOnLaunch: hasRunSetup) return Updater(checkOnLaunch: hasRunSetup)
@@ -115,22 +33,88 @@ extension EnvironmentValues {
} }
} }
extension FocusedValues { @main
@Entry var showCreateSecret: OpenSheet? struct Secretive: App {
@Environment(\.agentStatusChecker) var agentStatusChecker
@Environment(\.justUpdatedChecker) var justUpdatedChecker
@AppStorage("defaultsHasRunSetup") var hasRunSetup = false
@State private var showingSetup = false
@State private var showingIntegrations = false
@State private var showingCreation = false
@SceneBuilder var body: some Scene {
WindowGroup {
ContentView(showingCreation: $showingCreation, runningSetup: $showingSetup, hasRunSetup: $hasRunSetup)
.environment(EnvironmentValues._secretStoreList)
.onAppear {
if !hasRunSetup {
showingSetup = true
} }
final class OpenSheet {
let closure: () -> Void
let isEnabled: Bool
init(isEnabled: Bool = true, closure: @escaping () -> Void) {
self.isEnabled = isEnabled
self.closure = closure
} }
.onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in
func callAsFunction() { guard hasRunSetup else { return }
closure() agentStatusChecker.check()
if agentStatusChecker.running && justUpdatedChecker.justUpdatedBuild {
// Relaunch the agent, since it'll be running from earlier update still
reinstallAgent()
} else if !agentStatusChecker.running && !agentStatusChecker.developmentBuild {
forceLaunchAgent()
}
}
.sheet(isPresented: $showingIntegrations) {
IntegrationsView()
}
}
.commands {
CommandGroup(before: CommandGroupPlacement.appSettings) {
Button(.integrationsMenuBarTitle, systemImage: "app.connected.to.app.below.fill") {
showingIntegrations = true
}
}
CommandGroup(after: CommandGroupPlacement.newItem) {
Button(.appMenuNewSecretButton) {
showingCreation = true
}
.keyboardShortcut(KeyboardShortcut(KeyEquivalent("N"), modifiers: [.command, .shift]))
}
CommandGroup(replacing: .help) {
Button(.appMenuHelpButton) {
NSWorkspace.shared.open(Constants.helpURL)
}
}
SidebarCommands()
}
} }
} }
extension Secretive {
private func reinstallAgent() {
Task {
_ = await LaunchAgentController().install()
try? await Task.sleep(for: .seconds(1))
agentStatusChecker.check()
if !agentStatusChecker.running {
forceLaunchAgent()
}
}
}
private func forceLaunchAgent() {
// We've run setup, we didn't just update, launchd is just not doing it's thing.
// Force a launch directly.
Task {
_ = await LaunchAgentController().forceLaunch()
agentStatusChecker.check()
}
}
}
private enum Constants {
static let helpURL = URL(string: "https://github.com/maxgoedjen/secretive/blob/main/FAQ.md")!
}

View File

@@ -0,0 +1,68 @@
{
"images" : [
{
"filename" : "Icon-macOS-ClearDark-16x16@1x.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
"filename" : "Icon-macOS-ClearDark-16x16@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
"filename" : "Icon-macOS-ClearDark-32x32@1x.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
"filename" : "Icon-macOS-ClearDark-32x32@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
"filename" : "Icon-macOS-ClearDark-128x128@1x.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
"filename" : "Icon-macOS-ClearDark-128x128@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
"filename" : "Icon-macOS-ClearDark-256x256@1x.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
"filename" : "Icon-macOS-ClearDark-256x256@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
"filename" : "Icon-macOS-ClearDark-512x512@1x.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
"filename" : "Icon-macOS-ClearDark-1024x1024@1x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 856 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 356 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 356 KiB

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -2,25 +2,18 @@ import Foundation
import AppKit import AppKit
import SecretKit import SecretKit
import Observation import Observation
import OSLog
import ServiceManagement
@MainActor protocol AgentLaunchControllerProtocol: 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 } var process: NSRunningApplication? { get }
func check() func check()
func install() async throws
func uninstall() async throws
func forceLaunch() async throws
} }
@Observable @MainActor final class AgentLaunchController: AgentLaunchControllerProtocol { @Observable @MainActor final class AgentStatusChecker: AgentStatusCheckerProtocol {
var running: Bool = false var running: Bool = false
var process: NSRunningApplication? = nil var process: NSRunningApplication? = nil
private let logger = Logger(subsystem: "com.maxgoedjen.secretive", category: "LaunchAgentController")
private let service = SMAppService.loginItem(identifier: Bundle.agentBundleID)
nonisolated init() { nonisolated init() {
Task { @MainActor in Task { @MainActor in
@@ -40,7 +33,7 @@ import ServiceManagement
// The process corresponding to this instance of Secretive // The process corresponding to this instance of Secretive
var instanceSecretAgentProcess: NSRunningApplication? { var instanceSecretAgentProcess: NSRunningApplication? {
// TODO: CHECK VERSION // FIXME: CHECK VERSION
let agents = allSecretAgentProcesses 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 }
@@ -56,47 +49,6 @@ import ServiceManagement
Bundle.main.bundleURL.isXcodeURL Bundle.main.bundleURL.isXcodeURL
} }
func install() async throws {
logger.debug("Installing agent")
try? await service.unregister()
// 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
// and start new?
try await Task.sleep(for: .seconds(1))
try service.register()
try await Task.sleep(for: .seconds(1))
check()
}
func uninstall() async throws {
logger.debug("Uninstalling agent")
try await Task.sleep(for: .seconds(1))
try await service.unregister()
try await Task.sleep(for: .seconds(1))
check()
}
func forceLaunch() async throws {
logger.debug("Agent is not running, attempting to force launch by reinstalling")
try await install()
if running {
logger.debug("Agent successfully force launched by reinstalling")
return
}
logger.debug("Agent is not running, attempting to force launch by launching directly")
let url = Bundle.main.bundleURL.appendingPathComponent("Contents/Library/LoginItems/SecretAgent.app")
let config = NSWorkspace.OpenConfiguration()
config.activates = false
do {
try await NSWorkspace.shared.openApplication(at: url, configuration: config)
logger.debug("Agent force launched")
try await Task.sleep(for: .seconds(1))
} catch {
logger.error("Error force launching \(error.localizedDescription)")
}
check()
}
} }
extension URL { extension URL {

View File

@@ -0,0 +1,65 @@
import Foundation
import ServiceManagement
import AppKit
import OSLog
import SecretKit
struct LaunchAgentController {
private let logger = Logger(subsystem: "com.maxgoedjen.secretive", category: "LaunchAgentController")
func install() async -> Bool {
logger.debug("Installing agent")
_ = setEnabled(false)
// 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
// and start new?
try? await Task.sleep(for: .seconds(1))
let result = await MainActor.run {
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 {
logger.debug("Agent is not running, attempting to force launch")
let url = Bundle.main.bundleURL.appendingPathComponent("Contents/Library/LoginItems/SecretAgent.app")
let config = NSWorkspace.OpenConfiguration()
config.activates = false
do {
try await NSWorkspace.shared.openApplication(at: url, configuration: config)
logger.debug("Agent force launched")
try? await Task.sleep(for: .seconds(1))
return true
} catch {
logger.error("Error force launching \(error.localizedDescription)")
return false
}
}
private func setEnabled(_ enabled: Bool) -> Bool {
let service = SMAppService.loginItem(identifier: Bundle.agentBundleID)
do {
if enabled {
try service.register()
} else {
try service.unregister()
}
return true
} catch {
return false
}
}
}

View File

@@ -0,0 +1,36 @@
{\rtf1\ansi\ansicpg1252\cocoartf2580
\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
{\colortbl;\red255\green255\blue255;}
{\*\expandedcolortbl;;}
\margl1440\margr1440\vieww9000\viewh8400\viewkind0
\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6119\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0
{\field{\*\fldinst{HYPERLINK "https://github.com/maxgoedjen/secretive"}}{\fldrslt
\f0\fs24 \cf0 GitHub Repository}}
\f0\fs24 \
\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0
\cf0 \
{\field{\*\fldinst{HYPERLINK "GITHUB_BUILD_URL"}}{\fldrslt Build Log}}\
\
Special Thanks To:\
\
{\field{\*\fldinst{HYPERLINK "https://github.com/maxgoedjen/secretive/graphs/contributors"}}{\fldrslt Contributors}}:\
{\field{\*\fldinst{HYPERLINK "https://github.com/0xflotus"}}{\fldrslt 0xflotus}}\
{\field{\*\fldinst{HYPERLINK "https://github.com/aaron-trout"}}{\fldrslt Aaron Trout}}\
\pard\pardeftab720\partightenfactor0
{\field{\*\fldinst{HYPERLINK "https://github.com/EppO"}}{\fldrslt \cf0 Florent Monbillard}}\
{\field{\*\fldinst{HYPERLINK "https://github.com/vladimyr"}}{\fldrslt Dario Vladovi\uc0\u263 }}\
{\field{\*\fldinst{HYPERLINK "https://github.com/lavalleeale"}}{\fldrslt Alex Lavallee}}\
{\field{\*\fldinst{HYPERLINK "https://github.com/joshheyse"}}{\fldrslt Josh}}\
{\field{\*\fldinst{HYPERLINK "https://github.com/diesal11"}}{\fldrslt Dylan Lundy}}\
\
\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0
\cf0 Testers:\
{\field{\*\fldinst{HYPERLINK "https://github.com/bdash"}}{\fldrslt Mark Rowe}}\
{\field{\*\fldinst{HYPERLINK "https://github.com/danielctull"}}{\fldrslt Daniel Tull}}\
{\field{\*\fldinst{HYPERLINK "https://github.com/davedelong"}}{\fldrslt Dave DeLong}}\
{\field{\*\fldinst{HYPERLINK "https://github.com/esttorhe"}}{\fldrslt Esteban Torres}}\
{\field{\*\fldinst{HYPERLINK "https://github.com/joeblau"}}{\fldrslt Joe Blau}}\
{\field{\*\fldinst{HYPERLINK "https://github.com/marksands"}}{\fldrslt Mark Sands}}\
{\field{\*\fldinst{HYPERLINK "https://github.com/mergesort"}}{\fldrslt Joe Fabisevich}}\
{\field{\*\fldinst{HYPERLINK "https://github.com/phillco"}}{\fldrslt Phil Cohen}}\
{\field{\*\fldinst{HYPERLINK "https://github.com/zackdotcomputer"}}{\fldrslt Zack Sheppard}}}

View File

@@ -20,8 +20,6 @@
<string>$(CI_VERSION)</string> <string>$(CI_VERSION)</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>$(CI_BUILD_NUMBER)</string> <string>$(CI_BUILD_NUMBER)</string>
<key>GitHubBuildLog</key>
<string>https://$(CI_BUILD_LINK)</string>
<key>LSMinimumSystemVersion</key> <key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string> <string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>NSHumanReadableCopyright</key> <key>NSHumanReadableCopyright</key>

View File

@@ -1,7 +1,7 @@
import Foundation import Foundation
import AppKit import AppKit
class PreviewAgentLaunchController: AgentLaunchControllerProtocol { class PreviewAgentStatusChecker: AgentStatusCheckerProtocol {
let running: Bool let running: Bool
let process: NSRunningApplication? let process: NSRunningApplication?
@@ -15,13 +15,4 @@ class PreviewAgentLaunchController: AgentLaunchControllerProtocol {
func check() { func check() {
} }
func install() async throws {
}
func uninstall() async throws {
}
func forceLaunch() async throws {
}
} }

View File

@@ -60,17 +60,16 @@ extension Preview {
let id = UUID() let id = UUID()
var name: String { "Modifiable Preview Store" } var name: String { "Modifiable Preview Store" }
let secrets: [Secret] let secrets: [Secret]
var supportedKeyTypes: KeyAvailability { var supportedKeyTypes: [KeyType] {
return KeyAvailability( if #available(macOS 26, *) {
available: [ [
.ecdsa256, .ecdsa256,
.mldsa65, .mldsa65,
.mldsa87 .mldsa87,
],
unavailable: [
.init(keyType: .ecdsa384, reason: .macOSUpdateRequired)
] ]
) } else {
[.ecdsa256]
}
} }
init(secrets: [Secret]) { init(secrets: [Secret]) {

View File

@@ -2,6 +2,16 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>com.apple.security.hardened-process</key>
<true/>
<key>com.apple.security.hardened-process.dyld-ro</key>
<true/>
<key>com.apple.security.hardened-process.enhanced-security-version</key>
<integer>1</integer>
<key>com.apple.security.hardened-process.hardened-heap</key>
<true/>
<key>com.apple.security.hardened-process.platform-restrictions</key>
<integer>2</integer>
<key>com.apple.security.smartcard</key> <key>com.apple.security.smartcard</key>
<true/> <true/>
<key>keychain-access-groups</key> <key>keychain-access-groups</key>

View File

@@ -32,7 +32,7 @@ struct ConfigurationItemView<Content: View>: View {
Spacer() Spacer()
switch action { switch action {
case .copy(let string): case .copy(let string):
Button(.copyableClickToCopyButton, systemImage: "doc.on.doc") { Button(.copyableClickToCopyButton, systemImage: "document.on.document") {
NSPasteboard.general.declareTypes([.string], owner: nil) NSPasteboard.general.declareTypes([.string], owner: nil)
NSPasteboard.general.setString(string, forType: .string) NSPasteboard.general.setString(string, forType: .string)
} }

View File

@@ -22,18 +22,46 @@ struct IntegrationsView: View {
} }
} detail: { } detail: {
IntegrationsDetailView(selectedInstruction: $selectedInstruction) IntegrationsDetailView(selectedInstruction: $selectedInstruction)
} .fauxToolbar {
.toolbar {
Button(.setupDoneButton) { Button(.setupDoneButton) {
dismiss() dismiss()
} }
.normalButton()
}
} }
.hiddenToolbar()
.windowBackgroundStyle(.thinMaterial)
.onAppear { .onAppear {
selectedInstruction = instructions.gettingStarted selectedInstruction = instructions.gettingStarted
} }
.frame(minWidth: 400, minHeight: 400) .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)
}
}
} }
} }

View File

@@ -3,7 +3,6 @@ import SwiftUI
struct SetupView: View { struct SetupView: View {
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
@Environment(\.agentLaunchController) private var agentLaunchController
@Binding var setupComplete: Bool @Binding var setupComplete: Bool
@State var showingIntegrations = false @State var showingIntegrations = false
@@ -32,7 +31,7 @@ struct SetupView: View {
) { ) {
installed = true installed = true
Task { Task {
try? await agentLaunchController.install() await LaunchAgentController().install()
} }
} }
} }
@@ -86,10 +85,7 @@ struct SetupView: View {
integrations = true integrations = true
}, content: { }, content: {
IntegrationsView() IntegrationsView()
.frame(minWidth: 500, minHeight: 400)
}) })
.frame(idealWidth: 600)
.fixedSize(horizontal: false, vertical: true)
} }
} }
@@ -176,13 +172,10 @@ struct StepView<Content: View>: View {
.frame(width: 20) .frame(width: 20)
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
Text(title) Text(title)
.fixedSize(horizontal: false, vertical: true)
.bold() .bold()
Text(description) Text(description)
.fixedSize(horizontal: false, vertical: true)
if let detail { if let detail {
Text(detail) Text(detail)
.fixedSize(horizontal: false, vertical: true)
.font(.callout) .font(.callout)
.italic() .italic()
} }

View File

@@ -10,7 +10,6 @@ struct ToolConfigurationView: View {
@State var creating = false @State var creating = false
@State var selectedSecret: AnySecret? @State var selectedSecret: AnySecret?
@State var email = ""
init(selectedInstruction: ConfigurationFileInstructions) { init(selectedInstruction: ConfigurationFileInstructions) {
self.selectedInstruction = selectedInstruction self.selectedInstruction = selectedInstruction
@@ -33,7 +32,6 @@ struct ToolConfigurationView: View {
selectedSecret = created selectedSecret = created
} }
} }
.fixedSize()
} }
} }
} }
@@ -49,12 +47,6 @@ struct ToolConfigurationView: View {
.tag(secret) .tag(secret)
} }
} }
TextField(text: $email, prompt: Text(.integrationsConfigureUsingEmailPlaceholder)) {
Text(.integrationsConfigureUsingEmailTitle)
Text(.integrationsConfigureUsingEmailSubtitle)
.font(.subheadline)
.foregroundStyle(.secondary)
}
} header: { } header: {
Text(.integrationsConfigureUsingSecretHeader) Text(.integrationsConfigureUsingSecretHeader)
} }
@@ -67,7 +59,7 @@ struct ToolConfigurationView: View {
Section { Section {
ConfigurationItemView(title: .integrationsPathTitle, value: stepGroup.path, action: .revealInFinder(stepGroup.path)) ConfigurationItemView(title: .integrationsPathTitle, value: stepGroup.path, action: .revealInFinder(stepGroup.path))
ForEach(stepGroup.steps, id: \.self.key) { step in ForEach(stepGroup.steps, id: \.self.key) { step in
ConfigurationItemView(title: .integrationsAddThisTitle, action: .copy(placeholdersReplaced(text: String(localized: step)))) { ConfigurationItemView(title: .integrationsAddThisTitle, action: .copy(String(localized: step))) {
HStack { HStack {
Text(placeholdersReplaced(text: String(localized: step))) Text(placeholdersReplaced(text: String(localized: step)))
.padding(8) .padding(8)
@@ -109,11 +101,9 @@ struct ToolConfigurationView: View {
func placeholdersReplaced(text: String) -> String { func placeholdersReplaced(text: String) -> String {
guard let selectedSecret else { return text } guard let selectedSecret else { return text }
let writer = OpenSSHPublicKeyWriter() let writer = OpenSSHPublicKeyWriter()
let gitAllowedSignersString = [email.isEmpty ? String(localized: .integrationsConfigureUsingEmailPlaceholder) : email, writer.openSSHString(secret: selectedSecret)]
.joined(separator: " ")
let fileController = PublicKeyFileStoreController(homeDirectory: URL.agentHomeURL) let fileController = PublicKeyFileStoreController(homeDirectory: URL.agentHomeURL)
return text return text
.replacingOccurrences(of: Instructions.Constants.publicKeyPlaceholder, with: gitAllowedSignersString) .replacingOccurrences(of: Instructions.Constants.publicKeyPlaceholder, with: writer.openSSHString(secret: selectedSecret))
.replacingOccurrences(of: Instructions.Constants.publicKeyPathPlaceholder, with: fileController.publicKeyPath(for: selectedSecret)) .replacingOccurrences(of: Instructions.Constants.publicKeyPathPlaceholder, with: fileController.publicKeyPath(for: selectedSecret))
} }

View File

@@ -1,47 +0,0 @@
import SwiftUI
struct WindowBackgroundStyleModifier: ViewModifier {
let shapeStyle: any ShapeStyle
func body(content: Content) -> some View {
if #available(macOS 15.0, *) {
content
.containerBackground(
shapeStyle, for: .window
)
} else {
content
}
}
}
extension View {
func windowBackgroundStyle(_ style: some ShapeStyle) -> some View {
modifier(WindowBackgroundStyleModifier(shapeStyle: style))
}
}
struct HiddenToolbarModifier: ViewModifier {
func body(content: Content) -> some View {
if #available(macOS 15.0, *) {
content
.toolbarBackgroundVisibility(.hidden, for: .automatic)
} else {
content
}
}
}
extension View {
func hiddenToolbar() -> some View {
modifier(HiddenToolbarModifier())
}
}

View File

@@ -75,23 +75,10 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
Section { Section {
VStack { VStack {
Picker(.createSecretKeyTypeLabel, selection: $keyType) { Picker(.createSecretKeyTypeLabel, selection: $keyType) {
ForEach(store.supportedKeyTypes.available, id: \.self) { option in ForEach(store.supportedKeyTypes, id: \.self) { option in
Text(String(describing: option)) Text(String(describing: option))
.tag(option) .tag(option)
} .font(.caption)
Divider()
ForEach(store.supportedKeyTypes.unavailable, id: \.keyType) { option in
VStack {
Button {
} label: {
Text(String(describing: option.keyType))
switch option.reason {
case .macOSUpdateRequired:
Text(.createSecretKeyTypeMacOSUpdateRequiredLabel)
}
}
}
.selectionDisabled()
} }
} }
if keyType?.algorithm == .mldsa { if keyType?.algorithm == .mldsa {
@@ -132,7 +119,7 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
.padding() .padding()
} }
.onAppear { .onAppear {
keyType = store.supportedKeyTypes.available.first keyType = store.supportedKeyTypes.first
} }
.formStyle(.grouped) .formStyle(.grouped)
} }

View File

@@ -24,7 +24,7 @@ extension View {
} }
struct ToolbarCircleButtonModifier: ViewModifier { struct MenuButtonModifier: ViewModifier {
func body(content: Content) -> some View { func body(content: Content) -> some View {
if #available(macOS 26.0, *) { if #available(macOS 26.0, *) {
@@ -40,8 +40,8 @@ struct ToolbarCircleButtonModifier: ViewModifier {
extension View { extension View {
func toolbarCircleButton() -> some View { func menuButton() -> some View {
modifier(ToolbarCircleButtonModifier()) modifier(MenuButtonModifier())
} }
} }

View File

@@ -1,6 +1,6 @@
import SwiftUI import SwiftUI
struct ToolbarStatusButtonStyle: ButtonStyle { struct ToolbarButtonStyle: ButtonStyle {
private let lightColor: Color private let lightColor: Color
private let darkColor: Color private let darkColor: Color
@@ -39,7 +39,6 @@ struct ToolbarStatusButtonStyle: ButtonStyle {
} else { } else {
configuration configuration
.label .label
.padding(EdgeInsets(top: 6, leading: 8, bottom: 6, trailing: 8))
.background(colorScheme == .light ? lightColor : darkColor) .background(colorScheme == .light ? lightColor : darkColor)
.foregroundColor(.white) .foregroundColor(.white)
.clipShape(RoundedRectangle(cornerRadius: 5)) .clipShape(RoundedRectangle(cornerRadius: 5))
@@ -56,27 +55,3 @@ struct ToolbarStatusButtonStyle: ButtonStyle {
} }
} }
} }
struct ToolbarButtonStyle: PrimitiveButtonStyle {
var tint: Color = .white.opacity(0.1)
func makeBody(configuration: Configuration) -> some View {
if #available(macOS 26.0, *) {
configuration
.label
.padding(.vertical, 10)
.padding(.horizontal, 12)
.glassEffect(.regular.interactive().tint(tint))
.onTapGesture {
configuration.trigger()
}
} else {
BorderedButtonStyle().makeBody(configuration: configuration)
.padding(EdgeInsets(top: 6, leading: 8, bottom: 6, trailing: 8))
.foregroundColor(.white)
.clipShape(RoundedRectangle(cornerRadius: 5))
}
}
}

View File

@@ -1,69 +0,0 @@
import SwiftUI
struct AboutView: View {
var body: some View {
if #available(macOS 15.0, *) {
AboutViewContent()
.containerBackground(
.thinMaterial, for: .window
)
} else {
AboutViewContent()
}
}
}
struct AboutViewContent: View {
@Environment(\.openURL) var openURL
var body: some View {
VStack(spacing: 10) {
HStack {
Image(nsImage: NSApplication.shared.applicationIconImage)
VStack(alignment: .leading) {
Text(verbatim: "Secretive")
.font(.system(.largeTitle, weight: .bold))
Text("**\(Bundle.main.versionNumber)** (\(Bundle.main.buildNumber))")
.fixedSize(horizontal: true, vertical: false)
HStack {
Button(.aboutViewOnGithubButton) {
openURL(URL(string: "https://github.com/maxgoedjen/secretive")!)
}
.normalButton()
Button(.aboutBuildLogButton) {
openURL(Bundle.main.buildLog)
}
.normalButton()
}
}
}
Text(.aboutThanks(contributorsLink: "https://github.com/maxgoedjen/secretive/graphs/contributors", sponsorsLink: "https://github.com/sponsors/maxgoedjen"))
.font(.headline)
Text(.aboutOpenSourceNotice)
.font(.subheadline)
}
.padding(EdgeInsets(top: 10, leading: 30, bottom: 30, trailing: 30))
}
}
private extension Bundle {
var buildLog: URL {
URL(string: infoDictionary!["GitHubBuildLog"] as! String)!
}
var versionNumber: String {
infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0"
}
var buildNumber: String {
infoDictionary?["CFBundleVersion"] as? String ?? "0.0"
}
}
#Preview {
AboutView()
.frame(width: 500, height: 250)
}

View File

@@ -2,10 +2,10 @@ import SwiftUI
struct AgentStatusView: View { struct AgentStatusView: View {
@Environment(\.agentLaunchController) private var agentLaunchController: any AgentLaunchControllerProtocol @Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol
var body: some View { var body: some View {
if agentLaunchController.running { if agentStatusChecker.running {
AgentRunningView() AgentRunningView()
} else { } else {
AgentNotRunningView() AgentNotRunningView()
@@ -14,13 +14,12 @@ struct AgentStatusView: View {
} }
struct AgentRunningView: View { struct AgentRunningView: View {
@Environment(\.agentLaunchController) private var agentLaunchController: any AgentLaunchControllerProtocol @Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol
@AppStorage("explicitlyDisabled") var explicitlyDisabled = false
var body: some View { var body: some View {
Form { Form {
Section { Section {
if let process = agentLaunchController.process { if let process = agentStatusChecker.process {
ConfigurationItemView( ConfigurationItemView(
title: .agentDetailsLocationTitle, title: .agentDetailsLocationTitle,
value: process.bundleURL!.path(), value: process.bundleURL!.path(),
@@ -54,14 +53,19 @@ struct AgentRunningView: View {
Menu(.agentDetailsRestartAgentButton) { Menu(.agentDetailsRestartAgentButton) {
Button(.agentDetailsDisableAgentButton) { Button(.agentDetailsDisableAgentButton) {
Task { Task {
explicitlyDisabled = true _ = await LaunchAgentController()
try? await agentLaunchController
.uninstall() .uninstall()
agentStatusChecker.check()
} }
} }
} primaryAction: { } primaryAction: {
Task { Task {
try? await agentLaunchController.forceLaunch() let controller = LaunchAgentController()
let installed = await controller.install()
if !installed {
_ = await controller.forceLaunch()
}
agentStatusChecker.check()
} }
} }
} }
@@ -78,10 +82,9 @@ struct AgentRunningView: View {
struct AgentNotRunningView: View { struct AgentNotRunningView: View {
@Environment(\.agentLaunchController) private var agentLaunchController @Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol
@State var triedRestart = false @State var triedRestart = false
@State var loading = false @State var loading = false
@AppStorage("explicitlyDisabled") var explicitlyDisabled = false
var body: some View { var body: some View {
Form { Form {
@@ -97,14 +100,18 @@ struct AgentNotRunningView: View {
if !triedRestart { if !triedRestart {
Spacer() Spacer()
Button { Button {
explicitlyDisabled = false
guard !loading else { return } guard !loading else { return }
loading = true loading = true
Task { Task {
try await agentLaunchController.forceLaunch() let controller = LaunchAgentController()
let installed = await controller.install()
if !installed {
_ = await controller.forceLaunch()
}
agentStatusChecker.check()
loading = false loading = false
if !agentLaunchController.running { if !agentStatusChecker.running {
triedRestart = true triedRestart = true
} }
} }
@@ -138,9 +145,9 @@ struct AgentNotRunningView: View {
//#Preview { //#Preview {
// AgentStatusView() // AgentStatusView()
// .environment(\.agentLaunchController, PreviewAgentLaunchController(running: false)) // .environment(\.agentStatusChecker, PreviewAgentStatusChecker(running: false))
//} //}
//#Preview { //#Preview {
// AgentStatusView() // AgentStatusView()
// .environment(\.agentLaunchController, PreviewAgentLaunchController(running: true, process: .current)) // .environment(\.agentStatusChecker, PreviewAgentStatusChecker(running: true, process: .current))
//} //}

View File

@@ -6,21 +6,19 @@ import Brief
struct ContentView: View { struct ContentView: View {
@Binding var showingCreation: Bool
@Binding var runningSetup: Bool
@Binding var hasRunSetup: Bool
@State var showingAgentInfo = false
@State var activeSecret: AnySecret? @State var activeSecret: AnySecret?
@Environment(\.colorScheme) var colorScheme
@Environment(\.secretStoreList) private var storeList
@Environment(\.updater) private var updater: any UpdaterProtocol
@Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol
@State private var selectedUpdate: Release? @State private var selectedUpdate: Release?
@Environment(\.colorScheme) private var colorScheme
@Environment(\.openWindow) private var openWindow
@Environment(\.secretStoreList) private var storeList
@Environment(\.updater) private var updater
@Environment(\.agentLaunchController) private var agentLaunchController
@AppStorage("defaultsHasRunSetup") private var hasRunSetup = false
@State private var showingCreation = false
@State private var showingAppPathNotice = false @State private var showingAppPathNotice = false
@State private var runningSetup = false
@State private var showingAgentInfo = false
var body: some View { var body: some View {
VStack { VStack {
@@ -37,23 +35,6 @@ struct ContentView: View {
toolbarItem(appPathNoticeView, id: "appPath") toolbarItem(appPathNoticeView, id: "appPath")
toolbarItem(newItemView, id: "new") toolbarItem(newItemView, id: "new")
} }
.onAppear {
if !hasRunSetup {
runningSetup = true
}
}
.focusedSceneValue(\.showCreateSecret, .init(isEnabled: !runningSetup) {
showingCreation = true
})
.sheet(isPresented: $showingCreation) {
if let modifiable = storeList.modifiableStore {
CreateSecretView(store: modifiable) { created in
if let created {
activeSecret = created
}
}
}
}
.sheet(isPresented: $runningSetup) { .sheet(isPresented: $runningSetup) {
SetupView(setupComplete: $hasRunSetup) SetupView(setupComplete: $hasRunSetup)
} }
@@ -104,12 +85,27 @@ extension ContentView {
.font(.headline) .font(.headline)
.foregroundColor(.white) .foregroundColor(.white)
}) })
.buttonStyle(ToolbarStatusButtonStyle(color: color)) .buttonStyle(ToolbarButtonStyle(color: color))
.sheet(item: $selectedUpdate) { update in .sheet(item: $selectedUpdate) { update in
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) UpdateDetailView(update: update)
} }
} }
} }
}
@ViewBuilder @ViewBuilder
var newItemView: some View { var newItemView: some View {
@@ -117,7 +113,16 @@ extension ContentView {
Button(.appMenuNewSecretButton, systemImage: "plus") { Button(.appMenuNewSecretButton, systemImage: "plus") {
showingCreation = true showingCreation = true
} }
.toolbarCircleButton() .menuButton()
.sheet(isPresented: $showingCreation) {
if let modifiable = storeList.modifiableStore {
CreateSecretView(store: modifiable) { created in
if let created {
activeSecret = created
}
}
}
}
} }
} }
@@ -127,7 +132,7 @@ extension ContentView {
showingAgentInfo = true showingAgentInfo = true
}, label: { }, label: {
HStack { HStack {
if agentLaunchController.running { if agentStatusChecker.running {
Text(.agentRunningNoticeTitle) Text(.agentRunningNoticeTitle)
.font(.headline) .font(.headline)
.foregroundColor(colorScheme == .light ? Color(white: 0.3) : .white) .foregroundColor(colorScheme == .light ? Color(white: 0.3) : .white)
@@ -144,9 +149,9 @@ extension ContentView {
} }
}) })
.buttonStyle( .buttonStyle(
ToolbarStatusButtonStyle( ToolbarButtonStyle(
lightColor: agentLaunchController.running ? .black.opacity(0.05) : .red.opacity(0.75), lightColor: agentStatusChecker.running ? .black.opacity(0.05) : .red.opacity(0.75),
darkColor: agentLaunchController.running ? .white.opacity(0.05) : .red.opacity(0.5), 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) {
@@ -166,18 +171,18 @@ extension ContentView {
.font(.headline) .font(.headline)
.foregroundColor(.white) .foregroundColor(.white)
}) })
.buttonStyle(ToolbarStatusButtonStyle(color: .orange)) .buttonStyle(ToolbarButtonStyle(color: .orange))
.confirmationDialog(.appNotInApplicationsNoticeTitle, isPresented: $showingAppPathNotice) { .popover(isPresented: $showingAppPathNotice, attachmentAnchor: attachmentAnchor, arrowEdge: .bottom) {
Button(.appNotInApplicationsNoticeCancelButton, role: .cancel) { VStack {
} Image(systemName: "exclamationmark.triangle")
Button(.appNotInApplicationsNoticeQuitButton) { .resizable()
NSWorkspace.shared.selectFile(Bundle.main.bundlePath, inFileViewerRootedAtPath: Bundle.main.bundlePath) .aspectRatio(contentMode: .fit)
NSApplication.shared.terminate(nil) .frame(width: 64)
}
} message: {
Text(.appNotInApplicationsNoticeDetailDescription) Text(.appNotInApplicationsNoticeDetailDescription)
.frame(maxWidth: 300)
}
.padding()
} }
.dialogIcon(Image(systemName: "folder.fill.badge.questionmark"))
} }
} }

View File

@@ -11,7 +11,7 @@ struct CopyableView: View {
@State private var interactionState: InteractionState = .normal @State private var interactionState: InteractionState = .normal
var content: some View { var content: some View {
VStack(alignment: .leading, spacing: 15) { VStack(alignment: .leading) {
HStack { HStack {
image image
.renderingMode(.template) .renderingMode(.template)
@@ -31,16 +31,17 @@ struct CopyableView: View {
.foregroundColor(secondaryTextColor) .foregroundColor(secondaryTextColor)
.transition(.opacity) .transition(.opacity)
} }
} }
.padding(EdgeInsets(top: 20, leading: 20, bottom: 10, trailing: 20))
Divider() Divider()
.ignoresSafeArea()
Text(text) Text(text)
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)
.foregroundColor(primaryTextColor) .foregroundColor(primaryTextColor)
.padding(EdgeInsets(top: 10, leading: 20, bottom: 20, trailing: 20))
.multilineTextAlignment(.leading) .multilineTextAlignment(.leading)
.font(.system(.body, design: .monospaced)) .font(.system(.body, design: .monospaced))
} }
.safeAreaPadding(20)
._background(interactionState: interactionState) ._background(interactionState: interactionState)
.frame(minWidth: 150, maxWidth: .infinity) .frame(minWidth: 150, maxWidth: .infinity)
} }
@@ -52,12 +53,12 @@ struct CopyableView: View {
interactionState = hovering ? .hovering : .normal interactionState = hovering ? .hovering : .normal
} }
} }
.draggable(text) { .onDrag({
NSItemProvider(item: NSData(data: text.data(using: .utf8)!), typeIdentifier: UTType.utf8PlainText.identifier)
}, preview: {
content content
.lineLimit(3)
.frame(maxWidth: 300)
._background(interactionState: .dragging) ._background(interactionState: .dragging)
} })
.onTapGesture { .onTapGesture {
copy() copy()
withAnimation { withAnimation {
@@ -78,7 +79,7 @@ struct CopyableView: View {
var copyButton: some View { var copyButton: some View {
switch interactionState { switch interactionState {
case .hovering: case .hovering:
Button(.copyableClickToCopyButton, systemImage: "doc.on.doc") { Button(.copyableClickToCopyButton, systemImage: "document.on.document") {
withAnimation { withAnimation {
// Button will eat the click, so we set interaction state manually. // Button will eat the click, so we set interaction state manually.
interactionState = .clicking interactionState = .clicking
@@ -158,8 +159,7 @@ fileprivate struct BackgroundViewModifier: ViewModifier {
// Very thin opacity lets user hover anywhere over the view, glassEffect doesn't allow. // Very thin opacity lets user hover anywhere over the view, glassEffect doesn't allow.
.background(.white.opacity(0.01), in: RoundedRectangle(cornerRadius: 15)) .background(.white.opacity(0.01), in: RoundedRectangle(cornerRadius: 15))
.glassEffect(.regular.tint(backgroundColor(interactionState: interactionState)), in: RoundedRectangle(cornerRadius: 15)) .glassEffect(.regular.tint(backgroundColor(interactionState: interactionState)), in: RoundedRectangle(cornerRadius: 15))
.mask(RoundedRectangle(cornerRadius: 15))
.shadow(color: .black.opacity(0.1), radius: 5)
} else { } else {
content content
.background(backgroundColor(interactionState: interactionState)) .background(backgroundColor(interactionState: interactionState))
@@ -170,36 +170,21 @@ fileprivate struct BackgroundViewModifier: ViewModifier {
func backgroundColor(interactionState: InteractionState) -> Color { func backgroundColor(interactionState: InteractionState) -> Color {
guard appearsActive else { return Color.clear } guard appearsActive else { return Color.clear }
if #available(macOS 26.0, *) {
let base = colorScheme == .dark ? Color(white: 0.2) : Color(white: 1)
switch interactionState {
case .normal:
return base
case .hovering:
return base.mix(with: .accentColor, by: colorScheme == .dark ? 0.2 : 0.1)
case .clicking, .dragging:
return base.mix(with: .accentColor, by: 0.8)
}
} else {
switch interactionState { switch interactionState {
case .normal: case .normal:
return colorScheme == .dark ? Color(white: 0.2) : Color(white: 0.885) return colorScheme == .dark ? Color(white: 0.2) : Color(white: 0.885)
case .hovering: case .hovering, .dragging:
return colorScheme == .dark ? Color(white: 0.275) : Color(white: 0.82) return colorScheme == .dark ? Color(white: 0.275) : Color(white: 0.82)
case .clicking, .dragging: case .clicking:
return .accentColor return .accentColor
} }
} }
}
} }
#Preview { #Preview {
VStack {
CopyableView(title: .secretDetailSha256FingerprintLabel, image: Image(systemName: "figure.wave"), text: "Hello world.") CopyableView(title: .secretDetailSha256FingerprintLabel, image: Image(systemName: "figure.wave"), text: "Hello world.")
CopyableView(title: .secretDetailSha256FingerprintLabel, image: Image(systemName: "figure.wave"), text: "Hello world.")
}
.padding() .padding()
} }

View File

@@ -3,13 +3,18 @@ import Brief
struct UpdateDetailView: View { struct UpdateDetailView: View {
@Environment(\.updater) var updater @Environment(\.updater) var updater: any UpdaterProtocol
@Environment(\.openURL) var openURL
let update: Release let update: Release
var body: some View { var body: some View {
VStack(spacing: 0) { VStack {
Text(.updateVersionName(updateName: update.name)).font(.title)
GroupBox(label: Text(.updateReleaseNotesTitle)) {
ScrollView {
attributedBody
}
}
HStack { HStack {
if !update.critical { if !update.critical {
Button(.updateIgnoreButton) { Button(.updateIgnoreButton) {
@@ -17,36 +22,42 @@ struct UpdateDetailView: View {
await updater.ignore(release: update) await updater.ignore(release: update)
} }
} }
.buttonStyle(ToolbarButtonStyle())
}
Spacer() Spacer()
if updater.currentVersion.isTestBuild {
Button(.updaterDownloadLatestNightlyButton) {
openURL(URL(string: "https://github.com/maxgoedjen/secretive/actions/workflows/nightly.yml")!)
}
.buttonStyle(ToolbarButtonStyle(tint: .accentColor))
} }
Button(.updateUpdateButton) { Button(.updateUpdateButton) {
openURL(update.html_url) NSWorkspace.shared.open(update.html_url)
} }
.buttonStyle(ToolbarButtonStyle(tint: .accentColor))
.keyboardShortcut(.defaultAction) .keyboardShortcut(.defaultAction)
} }
}
.padding() .padding()
Divider() .frame(maxWidth: 500)
Form {
Section {
Text(update.attributedBody)
} header: {
Text(.updateVersionName(updateName: update.name)) .headerProminence(.increased)
} }
var attributedBody: Text {
var text = Text(verbatim: "")
for line in update.body.split(whereSeparator: \.isNewline) {
let attributed: Text
let split = line.split(separator: " ")
let unprefixed = split.dropFirst().joined(separator: " ")
if let prefix = split.first {
switch prefix {
case "#":
attributed = Text(unprefixed).font(.title) + Text(verbatim: "\n")
case "##":
attributed = Text(unprefixed).font(.title2) + Text(verbatim: "\n")
case "###":
attributed = Text(unprefixed).font(.title3) + Text(verbatim: "\n")
default:
attributed = Text(line) + Text(verbatim: "\n\n")
} }
.formStyle(.grouped) } else {
attributed = Text(line) + Text(verbatim: "\n\n")
} }
text = text + attributed
}
return text
} }
} }
#Preview {
UpdateDetailView(update: .init(name: "3.0.0", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Hello"))
}

View File

@@ -11,9 +11,7 @@ final class SecretiveUpdater: NSObject, XPCProtocol {
func process(_: Data) async throws -> [Release] { func process(_: Data) async throws -> [Release] {
let (data, _) = try await URLSession.shared.data(from: Constants.updateURL) let (data, _) = try await URLSession.shared.data(from: Constants.updateURL)
return try JSONDecoder() return try JSONDecoder().decode([Release].self, from: data)
.decode([GitHubRelease].self, from: data)
.map(Release.init)
} }
} }