Compare commits
44 Commits
fixquote
...
maxg/disab
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e11458fc3 | ||
|
|
595de41f03 | ||
|
|
d82f404166 | ||
|
|
a3bfcb316c | ||
|
|
bba4fb9e7c | ||
|
|
32a1a0bca9 | ||
|
|
bb0b6d8dc3 | ||
|
|
c63d87cbec | ||
|
|
65bc6c1a69 | ||
|
|
275b6ef9bb | ||
|
|
f13bc23991 | ||
|
|
3a67d59519 | ||
|
|
d9a3f0c813 | ||
|
|
84d5a56fb0 | ||
|
|
3bb0cc4a0e | ||
|
|
516e37fdde | ||
|
|
f9dc947b59 | ||
|
|
9c042d1956 | ||
|
|
08e0c4b63b | ||
|
|
f80cbdaf04 | ||
|
|
7a53a85615 | ||
|
|
1f74bd814f | ||
|
|
d9d93574f2 | ||
|
|
15e8ed1ec2 | ||
|
|
1df0c8e96b | ||
|
|
8213a8b451 | ||
|
|
af77fd4a21 | ||
|
|
85d0cab0f5 | ||
|
|
e8cdcdfb7f | ||
|
|
d7f8d5e56b | ||
|
|
3f247d628f | ||
|
|
dae9cead4e | ||
|
|
fe9f8613fa | ||
|
|
5d5ae5bab4 | ||
|
|
f76766a9d5 | ||
|
|
b308b10716 | ||
|
|
0e1e6813a1 | ||
|
|
27bf7c29e4 | ||
|
|
36b6c52979 | ||
|
|
67ec4fee12 | ||
|
|
21fc834fd9 | ||
|
|
726d0580d0 | ||
|
|
4f608ebbc6 | ||
|
|
6e7cf82618 |
BIN
.github/readme/app-dark.png
vendored
|
Before Width: | Height: | Size: 520 KiB After Width: | Height: | Size: 668 KiB |
BIN
.github/readme/app-light.png
vendored
|
Before Width: | Height: | Size: 519 KiB After Width: | Height: | Size: 618 KiB |
BIN
.github/readme/localize_add.png
vendored
|
Before Width: | Height: | Size: 1.3 MiB |
BIN
.github/readme/localize_sidebar.png
vendored
|
Before Width: | Height: | Size: 162 KiB |
BIN
.github/readme/localize_translate.png
vendored
|
Before Width: | Height: | Size: 1.7 MiB |
BIN
.github/readme/notification.png
vendored
|
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 47 KiB |
BIN
.github/readme/touchid.png
vendored
|
Before Width: | Height: | Size: 259 KiB After Width: | Height: | Size: 230 KiB |
2
.github/workflows/codeql.yml
vendored
@@ -37,7 +37,7 @@ jobs:
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
- if: matrix.build-mode == 'manual'
|
||||
name: "Select Xcode"
|
||||
run: sudo xcrun xcode-select -s /Applications/Xcode_26.0.app
|
||||
run: sudo xcrun xcode-select -s /Applications/Xcode_26.2.app
|
||||
- if: matrix.build-mode == 'manual'
|
||||
name: "Build"
|
||||
run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme Secretive CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO
|
||||
|
||||
29
.github/workflows/nightly.yml
vendored
@@ -3,7 +3,6 @@ name: Nightly
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 8 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -12,6 +11,7 @@ jobs:
|
||||
id-token: write
|
||||
contents: write
|
||||
attestations: write
|
||||
actions: read
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
||||
run: ./.github/scripts/signing.sh
|
||||
- name: Set Environment
|
||||
run: sudo xcrun xcode-select -s /Applications/Xcode_26.0.app
|
||||
run: sudo xcrun xcode-select -s /Applications/Xcode_26.2.app
|
||||
- name: Update Build Number
|
||||
env:
|
||||
RUN_ID: ${{ github.run_id }}
|
||||
@@ -33,23 +33,30 @@ jobs:
|
||||
DATE=$(date "+%Y-%m-%d")
|
||||
sed -i '' -e "s/GITHUB_CI_VERSION/0.0.0_nightly-$DATE/g" Sources/Config/Config.xcconfig
|
||||
sed -i '' -e "s/GITHUB_BUILD_NUMBER/1.$RUN_ID/g" Sources/Config/Config.xcconfig
|
||||
sed -i '' -e "s/GITHUB_BUILD_URL/https:\/\/github.com\/maxgoedjen\/secretive\/actions\/runs\/$RUN_ID/g" Sources/Secretive/Credits.rtf
|
||||
sed -i '' -e "s/GITHUB_BUILD_URL/https:\/\/github.com\/maxgoedjen\/secretive\/actions\/runs\/$RUN_ID/g" Sources/Config/Config.xcconfig
|
||||
- name: Build
|
||||
run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme Secretive -configuration Release -archivePath Archive.xcarchive archive
|
||||
- name: Create ZIP
|
||||
- name: Move to Artifact Folder
|
||||
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: |
|
||||
ditto -c -k --sequesterRsrc --keepParent Archive.xcarchive/Products/Applications/Secretive.app ./Secretive.zip
|
||||
curl -L -H "Authorization: Bearer $GITHUB_TOKEN" -L \
|
||||
https://api.github.com/repos/maxgoedjen/secretive/actions/artifacts/$ZIP_ID/zip > Secretive.zip
|
||||
- name: Notarize
|
||||
env:
|
||||
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
||||
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
|
||||
- name: Upload App to Artifacts
|
||||
id: upload
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Secretive.zip
|
||||
path: Secretive.zip
|
||||
- name: Attest
|
||||
id: attest
|
||||
uses: actions/attest-build-provenance@v2
|
||||
|
||||
64
.github/workflows/oneoff.yml
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
name: One-Off Build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: macos-26
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
attestations: write
|
||||
actions: read
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: Setup Signing
|
||||
env:
|
||||
SIGNING_DATA: ${{ secrets.SIGNING_DATA }}
|
||||
SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
|
||||
HOST_PROFILE_DATA: ${{ secrets.HOST_PROFILE_DATA }}
|
||||
AGENT_PROFILE_DATA: ${{ secrets.AGENT_PROFILE_DATA }}
|
||||
APPLE_API_KEY_DATA: ${{ secrets.APPLE_API_KEY_DATA }}
|
||||
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
||||
run: ./.github/scripts/signing.sh
|
||||
- name: Set Environment
|
||||
run: sudo xcrun xcode-select -s /Applications/Xcode_26.2.app
|
||||
- name: Update Build Number
|
||||
env:
|
||||
RUN_ID: ${{ github.run_id }}
|
||||
run: |
|
||||
DATE=$(date "+%Y-%m-%d")
|
||||
sed -i '' -e "s/GITHUB_CI_VERSION/0.0.0_oneoff-$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_URL/https:\/\/github.com\/maxgoedjen\/secretive\/actions\/runs\/$RUN_ID/g" Sources/Config/Config.xcconfig
|
||||
- name: Build
|
||||
run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme Secretive -configuration Release -archivePath Archive.xcarchive archive
|
||||
- name: Move to Artifact Folder
|
||||
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: |
|
||||
curl -L -H "Authorization: Bearer $GITHUB_TOKEN" -L \
|
||||
https://api.github.com/repos/maxgoedjen/secretive/actions/artifacts/$ZIP_ID/zip > Secretive.zip
|
||||
- name: Notarize
|
||||
env:
|
||||
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
||||
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
|
||||
- name: Attest
|
||||
id: attest
|
||||
uses: actions/attest-build-provenance@v2
|
||||
with:
|
||||
subject-name: "Secretive.zip"
|
||||
subject-digest: sha256:${{ steps.upload.outputs.artifact-digest }}
|
||||
45
.github/workflows/release.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
||||
run: ./.github/scripts/signing.sh
|
||||
- name: Set Environment
|
||||
run: sudo xcrun xcode-select -s /Applications/Xcode_26.0.app
|
||||
run: sudo xcrun xcode-select -s /Applications/Xcode_26.2.app
|
||||
- name: Test
|
||||
run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme PackageTests test
|
||||
# SPM doesn't seem to pick up on the tests currently?
|
||||
@@ -32,6 +32,7 @@ jobs:
|
||||
id-token: write
|
||||
contents: write
|
||||
attestations: write
|
||||
actions: read
|
||||
runs-on: macos-26
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
@@ -46,7 +47,7 @@ jobs:
|
||||
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
||||
run: ./.github/scripts/signing.sh
|
||||
- name: Set Environment
|
||||
run: sudo xcrun xcode-select -s /Applications/Xcode_26.0.app
|
||||
run: sudo xcrun xcode-select -s /Applications/Xcode_26.2.app
|
||||
- name: Update Build Number
|
||||
env:
|
||||
TAG_NAME: ${{ github.ref }}
|
||||
@@ -55,37 +56,43 @@ jobs:
|
||||
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_BUILD_NUMBER/1.$RUN_ID/g" Sources/Config/Config.xcconfig
|
||||
sed -i '' -e "s/GITHUB_BUILD_URL/https:\/\/github.com\/maxgoedjen\/secretive\/actions\/runs\/$RUN_ID/g" Sources/Secretive/Credits.rtf
|
||||
sed -i '' -e "s/GITHUB_BUILD_URL/github.com\/maxgoedjen\/secretive\/actions\/runs\/$RUN_ID/g" Sources/Config/Config.xcconfig
|
||||
- name: Build
|
||||
run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme Secretive -configuration Release -archivePath Archive.xcarchive archive
|
||||
- name: Create ZIP
|
||||
run: |
|
||||
ditto -c -k --sequesterRsrc --keepParent Archive.xcarchive/Products/Applications/Secretive.app ./Secretive.zip
|
||||
- name: Notarize
|
||||
env:
|
||||
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
||||
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
|
||||
- name: Move to Artifact Folder
|
||||
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: Secretive.zip
|
||||
path: Artifact
|
||||
- name: Download Zipped Artifact
|
||||
id: download
|
||||
env:
|
||||
ZIP_ID: ${{ steps.upload.outputs.artifact-id }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
curl -L -H "Authorization: Bearer $GITHUB_TOKEN" -L \
|
||||
https://api.github.com/repos/maxgoedjen/secretive/actions/artifacts/$ZIP_ID/zip > Secretive.zip
|
||||
- name: Notarize
|
||||
env:
|
||||
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
||||
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
|
||||
- name: Attest
|
||||
id: attest
|
||||
uses: actions/attest-build-provenance@v2
|
||||
with:
|
||||
subject-name: "Secretive.zip"
|
||||
subject-digest: ${{ steps.upload.outputs.artifact-digest }}
|
||||
subject-path: "Secretive.zip"
|
||||
- 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:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAG_NAME: ${{ github.ref }}
|
||||
RUN_ID: ${{ github.run_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
|
||||
|
||||
2
.github/workflows/test.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: Set Environment
|
||||
run: sudo xcrun xcode-select -s /Applications/Xcode_26.0.app
|
||||
run: sudo xcrun xcode-select -s /Applications/Xcode_26.2.app
|
||||
- name: Test Main Packages
|
||||
run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme PackageTests test
|
||||
# SPM doesn't seem to pick up on the tests currently?
|
||||
|
||||
18
README.md
@@ -1,11 +1,11 @@
|
||||
# Secretive [](https://github.com/maxgoedjen/secretive/actions/workflows/test.yml) 
|
||||
|
||||
|
||||
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.
|
||||
|
||||
Secretive is an app for protecting and managing SSH keys with the Secure Enclave.
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="/.github/readme/app-dark.png">
|
||||
<img src="/.github/readme/app-light.png" alt="Screenshot of Secretive" width="600">
|
||||
<source media="(prefers-color-scheme: light)" srcset="/.github/readme/app-light.png">
|
||||
<img src="/.github/readme/app-dark.png" alt="Screenshot of Secretive" width="600">
|
||||
</picture>
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ Secretive is an app for storing and managing SSH keys in the Secure Enclave. It
|
||||
|
||||
### 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 store your keys in 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 protect your keys with the Secure Enclave, it's impossible to export them, by design.
|
||||
|
||||
### 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
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
### Backups and Transfers to New Machines
|
||||
|
||||
@@ -62,3 +62,11 @@ Because secrets in the Secure Enclave are not exportable, they are not able to b
|
||||
## 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)
|
||||
|
||||
## 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).
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
CI_VERSION = GITHUB_CI_VERSION
|
||||
CI_BUILD_NUMBER = GITHUB_BUILD_NUMBER
|
||||
CI_BUILD_LINK = GITHUB_BUILD_URL
|
||||
|
||||
@@ -22,6 +22,9 @@ let package = Package(
|
||||
.library(
|
||||
name: "SecretAgentKit",
|
||||
targets: ["SecretAgentKit", "XPCWrappers"]),
|
||||
.library(
|
||||
name: "Common",
|
||||
targets: ["Common"]),
|
||||
.library(
|
||||
name: "Brief",
|
||||
targets: ["Brief"]),
|
||||
@@ -65,6 +68,12 @@ let package = Package(
|
||||
name: "SecretAgentKitTests",
|
||||
dependencies: ["SecretAgentKit"],
|
||||
),
|
||||
.target(
|
||||
name: "Common",
|
||||
dependencies: [],
|
||||
resources: [localization],
|
||||
swiftSettings: swiftSettings,
|
||||
),
|
||||
.target(
|
||||
name: "Brief",
|
||||
dependencies: ["XPCWrappers"],
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
/// A release is a representation of a downloadable update.
|
||||
public struct Release: Codable, Sendable {
|
||||
public struct Release: Codable, Sendable, Hashable {
|
||||
|
||||
/// The user-facing name of the release. Typically "Secretive 1.2.3"
|
||||
public let name: String
|
||||
@@ -15,6 +16,8 @@ public struct Release: Codable, Sendable {
|
||||
/// A user-facing description of the contents of the update.
|
||||
public let body: String
|
||||
|
||||
public let attributedBody: AttributedString
|
||||
|
||||
/// Initializes a Release.
|
||||
/// - Parameters:
|
||||
/// - name: The user-facing name of the release.
|
||||
@@ -26,6 +29,56 @@ public struct Release: Codable, Sendable {
|
||||
self.prerelease = prerelease
|
||||
self.html_url = html_url
|
||||
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"))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,19 +2,23 @@ import Foundation
|
||||
|
||||
extension URL {
|
||||
|
||||
static var agentHomeURL: URL {
|
||||
public static var agentHomeURL: URL {
|
||||
URL(fileURLWithPath: URL.homeDirectory.path().replacingOccurrences(of: Bundle.hostBundleID, with: Bundle.agentBundleID))
|
||||
}
|
||||
|
||||
static var socketPath: String {
|
||||
public static var socketPath: String {
|
||||
#if DEBUG
|
||||
URL.agentHomeURL.appendingPathComponent("socket-debug.ssh").path()
|
||||
#else
|
||||
URL.agentHomeURL.appendingPathComponent("socket.ssh").path()
|
||||
#endif
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension String {
|
||||
|
||||
var normalizedPathAndFolder: (String, String) {
|
||||
public var normalizedPathAndFolder: (String, String) {
|
||||
// All foundation-based normalization methods replace this with the container directly.
|
||||
let processedPath = replacingOccurrences(of: "~", with: "/Users/\(NSUserName())")
|
||||
let url = URL(filePath: processedPath)
|
||||
@@ -47,6 +47,7 @@ extension Agent {
|
||||
logger.debug("Agent returned \(SSHAgent.Response.agentSignResponse.debugDescription)")
|
||||
case .unknown(let value):
|
||||
logger.error("Agent received unknown request of type \(value).")
|
||||
throw UnhandledRequestError()
|
||||
default:
|
||||
logger.debug("Agent received valid request of type \(request.debugDescription), but not currently supported.")
|
||||
throw UnhandledRequestError()
|
||||
|
||||
@@ -36,7 +36,7 @@ extension SigningRequestTracer {
|
||||
/// - Parameter pid: The process ID to look up.
|
||||
/// - Returns: A ``SecretKit.SigningRequestProvenance.Process`` describing the process.
|
||||
func process(from pid: Int32) -> SigningRequestProvenance.Process {
|
||||
var pidAndNameInfo = self.pidAndNameInfo(from: pid)
|
||||
var pidAndNameInfo = unsafe self.pidAndNameInfo(from: pid)
|
||||
let ppid = unsafe pidAndNameInfo.kp_eproc.e_ppid != 0 ? pidAndNameInfo.kp_eproc.e_ppid : nil
|
||||
let procName = unsafe withUnsafeMutablePointer(to: &pidAndNameInfo.kp_proc.p_comm.0) { pointer in
|
||||
unsafe String(cString: pointer)
|
||||
|
||||
@@ -36,16 +36,21 @@ public struct SocketController {
|
||||
logger.debug("Socket controller path is clear")
|
||||
port = SocketPort(path: path)
|
||||
fileHandle = FileHandle(fileDescriptor: port.socket, closeOnDealloc: true)
|
||||
Task { [fileHandle, sessionsContinuation, logger] in
|
||||
for await notification in NotificationCenter.default.notifications(named: .NSFileHandleConnectionAccepted) {
|
||||
Task { @MainActor [fileHandle, sessionsContinuation, logger] in
|
||||
// Create the sequence before triggering the notification to
|
||||
// ensure it will not be missed.
|
||||
let connectionAcceptedNotifications = NotificationCenter.default.notifications(named: .NSFileHandleConnectionAccepted)
|
||||
|
||||
fileHandle.acceptConnectionInBackgroundAndNotify()
|
||||
|
||||
for await notification in connectionAcceptedNotifications {
|
||||
logger.debug("Socket controller accepted connection")
|
||||
guard let new = notification.userInfo?[NSFileHandleNotificationFileHandleItem] as? FileHandle else { continue }
|
||||
let session = Session(fileHandle: new)
|
||||
sessionsContinuation.yield(session)
|
||||
await fileHandle.acceptConnectionInBackgroundAndNotifyOnMainActor()
|
||||
fileHandle.acceptConnectionInBackgroundAndNotify()
|
||||
}
|
||||
}
|
||||
fileHandle.acceptConnectionInBackgroundAndNotify(forModes: [RunLoop.Mode.common])
|
||||
logger.debug("Socket listening at \(path)")
|
||||
}
|
||||
|
||||
@@ -77,8 +82,14 @@ extension SocketController {
|
||||
self.fileHandle = fileHandle
|
||||
provenance = SigningRequestTracer().provenance(from: fileHandle)
|
||||
(messages, messagesContinuation) = AsyncStream.makeStream()
|
||||
Task { [messagesContinuation, logger] in
|
||||
for await _ in NotificationCenter.default.notifications(named: .NSFileHandleDataAvailable, object: fileHandle) {
|
||||
Task { @MainActor [messagesContinuation, logger] in
|
||||
// Create the sequence before triggering the notification to
|
||||
// ensure it will not be missed.
|
||||
let dataAvailableNotifications = NotificationCenter.default.notifications(named: .NSFileHandleDataAvailable, object: fileHandle)
|
||||
|
||||
fileHandle.waitForDataInBackgroundAndNotify()
|
||||
|
||||
for await _ in dataAvailableNotifications {
|
||||
let data = fileHandle.availableData
|
||||
guard !data.isEmpty else {
|
||||
logger.debug("Socket controller received empty data, ending continuation.")
|
||||
@@ -90,16 +101,13 @@ extension SocketController {
|
||||
logger.debug("Socket controller yielded data.")
|
||||
}
|
||||
}
|
||||
Task {
|
||||
await fileHandle.waitForDataInBackgroundAndNotifyOnMainActor()
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes new data to the socket.
|
||||
/// - Parameter data: The data to write.
|
||||
public func write(_ data: Data) async throws {
|
||||
try fileHandle.write(contentsOf: data)
|
||||
await fileHandle.waitForDataInBackgroundAndNotifyOnMainActor()
|
||||
@MainActor public func write(_ data: Data) throws {
|
||||
try fileHandle.write(contentsOf: data)
|
||||
fileHandle.waitForDataInBackgroundAndNotify()
|
||||
}
|
||||
|
||||
/// Closes the socket and cleans up resources.
|
||||
@@ -113,22 +121,6 @@ extension SocketController {
|
||||
|
||||
}
|
||||
|
||||
private extension FileHandle {
|
||||
|
||||
/// Ensures waitForDataInBackgroundAndNotify will be called on the main actor.
|
||||
@MainActor func waitForDataInBackgroundAndNotifyOnMainActor() {
|
||||
waitForDataInBackgroundAndNotify()
|
||||
}
|
||||
|
||||
|
||||
/// Ensures acceptConnectionInBackgroundAndNotify will be called on the main actor.
|
||||
/// - Parameter modes: the runloop modes to use.
|
||||
@MainActor func acceptConnectionInBackgroundAndNotifyOnMainActor(forModes modes: [RunLoop.Mode]? = [RunLoop.Mode.common]) {
|
||||
acceptConnectionInBackgroundAndNotify(forModes: modes)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private extension SocketPort {
|
||||
|
||||
convenience init(path: String) {
|
||||
|
||||
@@ -64,7 +64,7 @@ public final class AnySecretStoreModifiable: AnySecretStore, SecretStoreModifiab
|
||||
private let _create: @Sendable (String, Attributes) async throws -> AnySecret
|
||||
private let _delete: @Sendable (AnySecret) async throws -> Void
|
||||
private let _update: @Sendable (AnySecret, String, Attributes) async throws -> Void
|
||||
private let _supportedKeyTypes: @Sendable () -> [KeyType]
|
||||
private let _supportedKeyTypes: @Sendable () -> KeyAvailability
|
||||
|
||||
public init<SecretStoreType>(_ secretStore: SecretStoreType) where SecretStoreType: SecretStoreModifiable {
|
||||
_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)
|
||||
}
|
||||
|
||||
public var supportedKeyTypes: [KeyType] {
|
||||
public var supportedKeyTypes: KeyAvailability {
|
||||
_supportedKeyTypes()
|
||||
}
|
||||
|
||||
|
||||
@@ -19,9 +19,10 @@ public final class PublicKeyFileStoreController: Sendable {
|
||||
public func generatePublicKeys(for secrets: [AnySecret], clear: Bool = false) throws {
|
||||
logger.log("Writing public keys to disk")
|
||||
if clear {
|
||||
let validPaths = Set(secrets.map { publicKeyPath(for: $0) }).union(Set(secrets.map { sshCertificatePath(for: $0) }))
|
||||
let validPaths = Set(secrets.map { publicKeyPath(for: $0) })
|
||||
.union(Set(secrets.map { sshCertificatePath(for: $0) }))
|
||||
let contentsOfDirectory = (try? FileManager.default.contentsOfDirectory(atPath: directory.path())) ?? []
|
||||
let fullPathContents = contentsOfDirectory.map { "\(directory)/\($0)" }
|
||||
let fullPathContents = contentsOfDirectory.map { directory.appending(path: $0).path() }
|
||||
|
||||
let untracked = Set(fullPathContents)
|
||||
.subtracting(validPaths)
|
||||
|
||||
@@ -62,10 +62,37 @@ public protocol SecretStoreModifiable<SecretType>: SecretStore {
|
||||
/// - attributes: The new attributes for the secret.
|
||||
func update(secret: SecretType, name: String, attributes: Attributes) async throws
|
||||
|
||||
var supportedKeyTypes: [KeyType] { get }
|
||||
var supportedKeyTypes: KeyAvailability { 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 {
|
||||
|
||||
// Distributed notification that keys were modified out of process (ie, that the management tool added/removed secrets)
|
||||
|
||||
@@ -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))
|
||||
guard !migratedPublicKeys.contains(parsed.publicKey.x963Representation) else {
|
||||
logger.log("Skipping \(name), public key already present. Marking as migrated.")
|
||||
try markMigrated(secret: secret, oldID: id)
|
||||
markMigrated(secret: secret, oldID: id)
|
||||
continue
|
||||
}
|
||||
logger.log("Migrating \(name).")
|
||||
try store.saveKey(tokenObjectID, name: name, attributes: secret.attributes)
|
||||
logger.log("Migrated \(name).")
|
||||
try markMigrated(secret: secret, oldID: id)
|
||||
markMigrated(secret: secret, oldID: id)
|
||||
migratedAny = true
|
||||
} catch {
|
||||
logger.error("Failed to migrate \(name): \(error).")
|
||||
logger.error("Failed to migrate \(name): \(error.localizedDescription).")
|
||||
}
|
||||
}
|
||||
if migratedAny {
|
||||
@@ -69,10 +69,10 @@ extension SecureEnclave {
|
||||
|
||||
|
||||
|
||||
public func markMigrated(secret: Secret, oldID: Data) throws {
|
||||
public func markMigrated(secret: Secret, oldID: Data) {
|
||||
let updateQuery = KeychainDictionary([
|
||||
kSecClass: kSecClassKey,
|
||||
kSecAttrApplicationLabel: secret.id
|
||||
kSecAttrApplicationLabel: oldID
|
||||
])
|
||||
|
||||
let newID = oldID + Constants.migrationMagicNumber
|
||||
@@ -82,7 +82,7 @@ extension SecureEnclave {
|
||||
|
||||
let status = SecItemUpdate(updateQuery, updatedAttributes)
|
||||
if status != errSecSuccess {
|
||||
throw KeychainError(statusCode: status)
|
||||
logger.warning("Failed to mark \(secret.name) as migrated: \(status).")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -186,17 +186,22 @@ extension SecureEnclave {
|
||||
await reloadSecrets()
|
||||
}
|
||||
|
||||
public var supportedKeyTypes: [KeyType] {
|
||||
if #available(macOS 26, *) {
|
||||
[
|
||||
.ecdsa256,
|
||||
.mldsa65,
|
||||
.mldsa87,
|
||||
]
|
||||
public let supportedKeyTypes: KeyAvailability = {
|
||||
let macOS26Keys: [KeyType] = [.mldsa65, .mldsa87]
|
||||
let isAtLeastMacOS26 = if #available(macOS 26, *) {
|
||||
true
|
||||
} else {
|
||||
[.ecdsa256]
|
||||
false
|
||||
}
|
||||
}
|
||||
return KeyAvailability(
|
||||
available: [
|
||||
.ecdsa256,
|
||||
] + (isAtLeastMacOS26 ? macOS26Keys : []),
|
||||
unavailable: (isAtLeastMacOS26 ? [] : macOS26Keys).map {
|
||||
KeyAvailability.UnavailableKeyType(keyType: $0, reason: .macOSUpdateRequired)
|
||||
}
|
||||
)
|
||||
}()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -34,7 +34,9 @@ public final class XPCServiceDelegate: NSObject, NSXPCListenerDelegate {
|
||||
if let error = error as? Codable & Error {
|
||||
reply(nil, NSError(error))
|
||||
} else {
|
||||
reply(nil, error)
|
||||
// Sending cast directly tries to serialize it and crashes XPCEncoder.
|
||||
let cast = error as NSError
|
||||
reply(nil, NSError(domain: cast.domain, code: cast.code, userInfo: [NSLocalizedDescriptionKey: error.localizedDescription]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import SmartCardSecretKit
|
||||
import SecretAgentKit
|
||||
import Brief
|
||||
import Observation
|
||||
import Common
|
||||
|
||||
@main
|
||||
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
@@ -26,7 +27,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
Agent(storeList: storeList, witness: notifier)
|
||||
}()
|
||||
private lazy var socketController: SocketController = {
|
||||
let path = (NSHomeDirectory() as NSString).appendingPathComponent("socket.ssh") as String
|
||||
let path = URL.socketPath as String
|
||||
return SocketController(path: path)
|
||||
}()
|
||||
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "AppDelegate")
|
||||
@@ -41,7 +42,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
for await message in session.messages {
|
||||
let request = try await inputParser.parse(data: message)
|
||||
let agentResponse = await agent.handle(request: request, provenance: session.provenance)
|
||||
try await session.write(agentResponse)
|
||||
try session.write(agentResponse)
|
||||
}
|
||||
} catch {
|
||||
try session.close()
|
||||
|
||||
@@ -2,18 +2,24 @@ import Foundation
|
||||
import SecretAgentKit
|
||||
import Brief
|
||||
import XPCWrappers
|
||||
import OSLog
|
||||
|
||||
/// Delegates all agent input parsing to an XPC service which wraps OpenSSH
|
||||
public final class XPCAgentInputParser: SSHAgentInputParserProtocol {
|
||||
|
||||
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "XPCAgentInputParser")
|
||||
private let session: XPCTypedSession<SSHAgent.Request, SSHAgentInputParser.AgentParsingError>
|
||||
|
||||
public init() async throws {
|
||||
logger.debug("Creating XPCAgentInputParser")
|
||||
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 {
|
||||
try await session.send(data)
|
||||
logger.debug("Parsing input")
|
||||
defer { logger.debug("Parsed input") }
|
||||
return try await session.send(data)
|
||||
}
|
||||
|
||||
deinit {
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
/* Begin PBXBuildFile section */
|
||||
2C4A9D2F2636FFD3008CC8E2 /* EditSecretView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C4A9D2E2636FFD3008CC8E2 /* EditSecretView.swift */; };
|
||||
50020BB024064869003D4025 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50020BAF24064869003D4025 /* AppDelegate.swift */; };
|
||||
50033AC327813F1700253856 /* BundleIDs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50033AC227813F1700253856 /* BundleIDs.swift */; };
|
||||
5003EF3B278005E800DF2006 /* SecretKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5003EF3A278005E800DF2006 /* SecretKit */; };
|
||||
5003EF3D278005F300DF2006 /* Brief in Frameworks */ = {isa = PBXBuildFile; productRef = 5003EF3C278005F300DF2006 /* Brief */; };
|
||||
5003EF3F278005F300DF2006 /* SecretAgentKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5003EF3E278005F300DF2006 /* SecretAgentKit */; };
|
||||
@@ -19,7 +18,6 @@
|
||||
5003EF632780081B00DF2006 /* SecureEnclaveSecretKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5003EF622780081B00DF2006 /* SecureEnclaveSecretKit */; };
|
||||
5003EF652780081B00DF2006 /* SmartCardSecretKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5003EF642780081B00DF2006 /* SmartCardSecretKit */; };
|
||||
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 */; };
|
||||
501421622781262300BBAA70 /* Brief in Frameworks */ = {isa = PBXBuildFile; productRef = 501421612781262300BBAA70 /* Brief */; };
|
||||
501421652781268000BBAA70 /* SecretAgent.app in CopyFiles */ = {isa = PBXBuildFile; fileRef = 50A3B78A24026B7500D209EA /* SecretAgent.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
@@ -27,22 +25,18 @@
|
||||
50153E22250DECA300525160 /* SecretListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E21250DECA300525160 /* SecretListItemView.swift */; };
|
||||
501578132E6C0479004A37D0 /* XPCInputParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501578122E6C0479004A37D0 /* XPCInputParser.swift */; };
|
||||
5018F54F24064786002EB505 /* Notifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5018F54E24064786002EB505 /* Notifier.swift */; };
|
||||
504788EC2E680DC800B4556F /* URLs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788EB2E680DC400B4556F /* URLs.swift */; };
|
||||
504788F22E681F3A00B4556F /* Instructions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F12E681F3A00B4556F /* Instructions.swift */; };
|
||||
504788F42E681F6900B4556F /* ToolConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F32E681F6900B4556F /* ToolConfigurationView.swift */; };
|
||||
504788F62E68206F00B4556F /* GettingStartedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F52E68206F00B4556F /* GettingStartedView.swift */; };
|
||||
504789232E697DD300B4556F /* BoxBackgroundStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504789222E697DD300B4556F /* BoxBackgroundStyle.swift */; };
|
||||
50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */; };
|
||||
50571E0524393D1500F76F6C /* LaunchAgentController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0424393D1500F76F6C /* LaunchAgentController.swift */; };
|
||||
50617D8323FCE48E0099B055 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D8223FCE48E0099B055 /* App.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 */; };
|
||||
50617DD223FCEFA90099B055 /* PreviewStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617DD123FCEFA90099B055 /* PreviewStore.swift */; };
|
||||
5065E313295517C500E16645 /* ToolbarButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5065E312295517C500E16645 /* ToolbarButtonStyle.swift */; };
|
||||
5066A6C22516F303004B5A36 /* SetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6C12516F303004B5A36 /* SetupView.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 */; };
|
||||
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 */; };
|
||||
@@ -73,6 +67,12 @@
|
||||
50BDCB762E6450950072D2E7 /* ConfigurationItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BDCB752E6450950072D2E7 /* ConfigurationItemView.swift */; };
|
||||
50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C385A42407A76D00AF2719 /* SecretDetailView.swift */; };
|
||||
50CF4ABC2E601B0F005588DC /* ActionButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */; };
|
||||
50E0145C2EDB9CDF00B121F1 /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 50E0145B2EDB9CDF00B121F1 /* Common */; };
|
||||
50E0145E2EDB9CE400B121F1 /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 50E0145D2EDB9CE400B121F1 /* Common */; };
|
||||
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 */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -180,24 +180,21 @@
|
||||
/* Begin PBXFileReference section */
|
||||
2C4A9D2E2636FFD3008CC8E2 /* EditSecretView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditSecretView.swift; sourceTree = "<group>"; };
|
||||
50020BAF24064869003D4025 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
50033AC227813F1700253856 /* BundleIDs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleIDs.swift; sourceTree = "<group>"; };
|
||||
5003EF39278005C800DF2006 /* Packages */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Packages; sourceTree = "<group>"; };
|
||||
5008C23D2E525D8200507AC2 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = Localizable.xcstrings; path = Packages/Resources/Localizable.xcstrings; sourceTree = SOURCE_ROOT; };
|
||||
50153E1F250AFCB200525160 /* UpdateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateView.swift; sourceTree = "<group>"; };
|
||||
50153E21250DECA300525160 /* SecretListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretListItemView.swift; sourceTree = "<group>"; };
|
||||
501578122E6C0479004A37D0 /* XPCInputParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XPCInputParser.swift; sourceTree = "<group>"; };
|
||||
5018F54E24064786002EB505 /* Notifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifier.swift; sourceTree = "<group>"; };
|
||||
504788EB2E680DC400B4556F /* URLs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLs.swift; sourceTree = "<group>"; };
|
||||
504788F12E681F3A00B4556F /* Instructions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Instructions.swift; sourceTree = "<group>"; };
|
||||
504788F32E681F6900B4556F /* ToolConfigurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolConfigurationView.swift; sourceTree = "<group>"; };
|
||||
504788F52E68206F00B4556F /* GettingStartedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GettingStartedView.swift; sourceTree = "<group>"; };
|
||||
504789222E697DD300B4556F /* BoxBackgroundStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxBackgroundStyle.swift; sourceTree = "<group>"; };
|
||||
50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JustUpdatedChecker.swift; sourceTree = "<group>"; };
|
||||
50571E0424393D1500F76F6C /* LaunchAgentController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchAgentController.swift; sourceTree = "<group>"; };
|
||||
5059933F2E7A3B5B0092CFFA /* en */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = en; path = en.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@@ -205,7 +202,6 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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; };
|
||||
@@ -228,7 +224,6 @@
|
||||
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; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@@ -239,6 +234,9 @@
|
||||
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>"; };
|
||||
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 */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -246,6 +244,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
50E0145C2EDB9CDF00B121F1 /* Common in Frameworks */,
|
||||
5003EF3B278005E800DF2006 /* SecretKit in Frameworks */,
|
||||
501421622781262300BBAA70 /* Brief in Frameworks */,
|
||||
5003EF5F2780081600DF2006 /* SecureEnclaveSecretKit in Frameworks */,
|
||||
@@ -279,29 +278,23 @@
|
||||
5003EF652780081B00DF2006 /* SmartCardSecretKit in Frameworks */,
|
||||
5003EF3F278005F300DF2006 /* SecretAgentKit in Frameworks */,
|
||||
5003EF41278005FA00DF2006 /* SecretKit in Frameworks */,
|
||||
50E0145E2EDB9CE400B121F1 /* Common in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
50033AC427813F1C00253856 /* Helpers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
50033AC227813F1700253856 /* BundleIDs.swift */,
|
||||
);
|
||||
path = Helpers;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
504788ED2E681EB200B4556F /* Styles */ = {
|
||||
504788ED2E681EB200B4556F /* Modifiers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
50E4C4522E73C78900C73783 /* WindowBackgroundStyle.swift */,
|
||||
50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */,
|
||||
50BDCB732E6436C60072D2E7 /* ErrorStyle.swift */,
|
||||
504789222E697DD300B4556F /* BoxBackgroundStyle.swift */,
|
||||
5065E312295517C500E16645 /* ToolbarButtonStyle.swift */,
|
||||
);
|
||||
path = Styles;
|
||||
path = Modifiers;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
504788EE2E681EC300B4556F /* Secrets */ = {
|
||||
@@ -335,6 +328,7 @@
|
||||
504788F02E681F0100B4556F /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
50E4C4C22E7765DF00C73783 /* AboutView.swift */,
|
||||
50BDCB712E63BAF20072D2E7 /* AgentStatusView.swift */,
|
||||
50617D8423FCE48E0099B055 /* ContentView.swift */,
|
||||
5066A6C72516FE6E004B5A36 /* CopyableView.swift */,
|
||||
@@ -374,12 +368,10 @@
|
||||
50617D8223FCE48E0099B055 /* App.swift */,
|
||||
508A58B0241ED1C40069DC07 /* Views */,
|
||||
508A58B1241ED1EA0069DC07 /* Controllers */,
|
||||
50033AC427813F1C00253856 /* Helpers */,
|
||||
50617D8623FCE48E0099B055 /* Assets.xcassets */,
|
||||
50617D8E23FCE48E0099B055 /* Info.plist */,
|
||||
508BF28D25B4F005009EFB7E /* InternetAccessPolicy.plist */,
|
||||
50E4C4C72E777E4200C73783 /* AppIcon.icon */,
|
||||
50617D8F23FCE48E0099B055 /* Secretive.entitlements */,
|
||||
506772C62424784600034DED /* Credits.rtf */,
|
||||
5008C23D2E525D8200507AC2 /* Localizable.xcstrings */,
|
||||
50617D8823FCE48E0099B055 /* Preview Content */,
|
||||
);
|
||||
@@ -432,7 +424,7 @@
|
||||
children = (
|
||||
504788EF2E681ED700B4556F /* Configuration */,
|
||||
504788EE2E681EC300B4556F /* Secrets */,
|
||||
504788ED2E681EB200B4556F /* Styles */,
|
||||
504788ED2E681EB200B4556F /* Modifiers */,
|
||||
504788F02E681F0100B4556F /* Views */,
|
||||
);
|
||||
path = Views;
|
||||
@@ -441,11 +433,9 @@
|
||||
508A58B1241ED1EA0069DC07 /* Controllers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
504788EB2E680DC400B4556F /* URLs.swift */,
|
||||
508A58B2241ED2180069DC07 /* AgentStatusChecker.swift */,
|
||||
5091D2BB25183B830049FD9B /* ApplicationDirectoryController.swift */,
|
||||
50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */,
|
||||
50571E0424393D1500F76F6C /* LaunchAgentController.swift */,
|
||||
);
|
||||
path = Controllers;
|
||||
sourceTree = "<group>";
|
||||
@@ -507,6 +497,7 @@
|
||||
5003EF5E2780081600DF2006 /* SecureEnclaveSecretKit */,
|
||||
5003EF602780081600DF2006 /* SmartCardSecretKit */,
|
||||
501421612781262300BBAA70 /* Brief */,
|
||||
50E0145B2EDB9CDF00B121F1 /* Common */,
|
||||
);
|
||||
productName = Secretive;
|
||||
productReference = 50617D7F23FCE48E0099B055 /* Secretive.app */;
|
||||
@@ -577,6 +568,7 @@
|
||||
5003EF40278005FA00DF2006 /* SecretKit */,
|
||||
5003EF622780081B00DF2006 /* SecureEnclaveSecretKit */,
|
||||
5003EF642780081B00DF2006 /* SmartCardSecretKit */,
|
||||
50E0145D2EDB9CE400B121F1 /* Common */,
|
||||
);
|
||||
productName = SecretAgent;
|
||||
productReference = 50A3B78A24026B7500D209EA /* SecretAgent.app */;
|
||||
@@ -613,7 +605,6 @@
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
it,
|
||||
fr,
|
||||
de,
|
||||
@@ -643,9 +634,8 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
50617D8A23FCE48E0099B055 /* Preview Assets.xcassets in Resources */,
|
||||
50E4C4C82E777E4200C73783 /* AppIcon.icon in Resources */,
|
||||
5008C23E2E525D8900507AC2 /* Localizable.xcstrings in Resources */,
|
||||
50617D8723FCE48E0099B055 /* Assets.xcassets in Resources */,
|
||||
506772C72424784600034DED /* Credits.rtf in Resources */,
|
||||
508BF28E25B4F005009EFB7E /* InternetAccessPolicy.plist in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -671,8 +661,8 @@
|
||||
50A3B79724026B7600D209EA /* Main.storyboard in Resources */,
|
||||
5008C2412E52D18700507AC2 /* Localizable.xcstrings in Resources */,
|
||||
50A3B79424026B7600D209EA /* Preview Assets.xcassets in Resources */,
|
||||
50E4C4C92E777E4200C73783 /* AppIcon.icon in Resources */,
|
||||
508BF2AA25B4F1CB009EFB7E /* InternetAccessPolicy.plist in Resources */,
|
||||
5008C2402E52792400507AC2 /* Assets.xcassets in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -685,9 +675,10 @@
|
||||
files = (
|
||||
504788F22E681F3A00B4556F /* Instructions.swift in Sources */,
|
||||
50BDCB742E6436CA0072D2E7 /* ErrorStyle.swift in Sources */,
|
||||
50E4C4C32E7765DF00C73783 /* AboutView.swift in Sources */,
|
||||
2C4A9D2F2636FFD3008CC8E2 /* EditSecretView.swift in Sources */,
|
||||
50E4C4532E73C78C00C73783 /* WindowBackgroundStyle.swift in Sources */,
|
||||
5091D2BC25183B830049FD9B /* ApplicationDirectoryController.swift in Sources */,
|
||||
504788EC2E680DC800B4556F /* URLs.swift in Sources */,
|
||||
504789232E697DD300B4556F /* BoxBackgroundStyle.swift in Sources */,
|
||||
5066A6C22516F303004B5A36 /* SetupView.swift in Sources */,
|
||||
5065E313295517C500E16645 /* ToolbarButtonStyle.swift in Sources */,
|
||||
@@ -697,14 +688,12 @@
|
||||
50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */,
|
||||
5079BA0F250F29BF00EA86F4 /* StoreListView.swift in Sources */,
|
||||
50617DD223FCEFA90099B055 /* PreviewStore.swift in Sources */,
|
||||
50033AC327813F1700253856 /* BundleIDs.swift in Sources */,
|
||||
50BDCB722E63BAF20072D2E7 /* AgentStatusView.swift in Sources */,
|
||||
508A58B3241ED2180069DC07 /* AgentStatusChecker.swift in Sources */,
|
||||
50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */,
|
||||
5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */,
|
||||
50AE97002E5C1A420018C710 /* IntegrationsView.swift in Sources */,
|
||||
50153E20250AFCB200525160 /* UpdateView.swift in Sources */,
|
||||
50571E0524393D1500F76F6C /* LaunchAgentController.swift in Sources */,
|
||||
5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */,
|
||||
50B8550D24138C4F009958AC /* DeleteSecretView.swift in Sources */,
|
||||
50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */,
|
||||
@@ -784,7 +773,7 @@
|
||||
50A3B79524026B7600D209EA /* Main.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
50A3B79624026B7600D209EA /* Base */,
|
||||
5059933F2E7A3B5B0092CFFA /* en */,
|
||||
);
|
||||
name = Main.storyboard;
|
||||
sourceTree = "<group>";
|
||||
@@ -1045,7 +1034,7 @@
|
||||
INFOPLIST_KEY_CFBundleDisplayName = SecretiveUpdater;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 Max Goedjen. All rights reserved.";
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 26.0;
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretiveUpdater;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -1085,7 +1074,7 @@
|
||||
INFOPLIST_KEY_CFBundleDisplayName = SecretiveUpdater;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 Max Goedjen. All rights reserved.";
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 26.0;
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretiveUpdater;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -1127,7 +1116,7 @@
|
||||
INFOPLIST_KEY_CFBundleDisplayName = SecretiveUpdater;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 Max Goedjen. All rights reserved.";
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 26.0;
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretiveUpdater;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -1159,7 +1148,7 @@
|
||||
INFOPLIST_KEY_CFBundleDisplayName = SecretAgentInputParser;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 Max Goedjen. All rights reserved.";
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 26.0;
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretAgentInputParser;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -1189,7 +1178,7 @@
|
||||
INFOPLIST_KEY_CFBundleDisplayName = SecretAgentInputParser;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 Max Goedjen. All rights reserved.";
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 26.0;
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretAgentInputParser;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -1221,7 +1210,7 @@
|
||||
INFOPLIST_KEY_CFBundleDisplayName = SecretAgentInputParser;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 Max Goedjen. All rights reserved.";
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 26.0;
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretAgentInputParser;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -1558,6 +1547,14 @@
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = SecretAgentKit;
|
||||
};
|
||||
50E0145B2EDB9CDF00B121F1 /* Common */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = Common;
|
||||
};
|
||||
50E0145D2EDB9CE400B121F1 /* Common */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = Common;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = 50617D7723FCE48D0099B055 /* Project object */;
|
||||
|
||||
@@ -4,6 +4,88 @@ import SecureEnclaveSecretKit
|
||||
import SmartCardSecretKit
|
||||
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 {
|
||||
|
||||
// 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).
|
||||
@@ -17,8 +99,8 @@ extension EnvironmentValues {
|
||||
return list
|
||||
}()
|
||||
|
||||
private static let _agentStatusChecker = AgentStatusChecker()
|
||||
@Entry var agentStatusChecker: any AgentStatusCheckerProtocol = _agentStatusChecker
|
||||
private static let _agentLaunchController = AgentLaunchController()
|
||||
@Entry var agentLaunchController: any AgentLaunchControllerProtocol = _agentLaunchController
|
||||
private static let _updater: any UpdaterProtocol = {
|
||||
@AppStorage("defaultsHasRunSetup") var hasRunSetup = false
|
||||
return Updater(checkOnLaunch: hasRunSetup)
|
||||
@@ -33,88 +115,22 @@ extension EnvironmentValues {
|
||||
}
|
||||
}
|
||||
|
||||
@main
|
||||
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
|
||||
extension FocusedValues {
|
||||
@Entry var showCreateSecret: OpenSheet?
|
||||
}
|
||||
|
||||
@SceneBuilder var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView(showingCreation: $showingCreation, runningSetup: $showingSetup, hasRunSetup: $hasRunSetup)
|
||||
.environment(EnvironmentValues._secretStoreList)
|
||||
.onAppear {
|
||||
if !hasRunSetup {
|
||||
showingSetup = true
|
||||
}
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in
|
||||
guard hasRunSetup else { return }
|
||||
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()
|
||||
}
|
||||
final class OpenSheet {
|
||||
|
||||
let closure: () -> Void
|
||||
let isEnabled: Bool
|
||||
|
||||
init(isEnabled: Bool = true, closure: @escaping () -> Void) {
|
||||
self.isEnabled = isEnabled
|
||||
self.closure = closure
|
||||
}
|
||||
|
||||
func callAsFunction() {
|
||||
closure()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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")!
|
||||
}
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 69 KiB |
@@ -1,68 +0,0 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 856 B |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 356 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 356 KiB |
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -2,18 +2,26 @@ import Foundation
|
||||
import AppKit
|
||||
import SecretKit
|
||||
import Observation
|
||||
import OSLog
|
||||
import ServiceManagement
|
||||
import Common
|
||||
|
||||
@MainActor protocol AgentStatusCheckerProtocol: Observable, Sendable {
|
||||
@MainActor protocol AgentLaunchControllerProtocol: Observable, Sendable {
|
||||
var running: Bool { get }
|
||||
var developmentBuild: Bool { get }
|
||||
var process: NSRunningApplication? { get }
|
||||
func check()
|
||||
func install() async throws
|
||||
func uninstall() async throws
|
||||
func forceLaunch() async throws
|
||||
}
|
||||
|
||||
@Observable @MainActor final class AgentStatusChecker: AgentStatusCheckerProtocol {
|
||||
@Observable @MainActor final class AgentLaunchController: AgentLaunchControllerProtocol {
|
||||
|
||||
var running: Bool = false
|
||||
var process: NSRunningApplication? = nil
|
||||
private let logger = Logger(subsystem: "com.maxgoedjen.secretive", category: "LaunchAgentController")
|
||||
private let service = SMAppService.loginItem(identifier: Bundle.agentBundleID)
|
||||
|
||||
nonisolated init() {
|
||||
Task { @MainActor in
|
||||
@@ -33,7 +41,7 @@ import Observation
|
||||
|
||||
// The process corresponding to this instance of Secretive
|
||||
var instanceSecretAgentProcess: NSRunningApplication? {
|
||||
// FIXME: CHECK VERSION
|
||||
// TODO: CHECK VERSION
|
||||
let agents = allSecretAgentProcesses
|
||||
for agent in agents {
|
||||
guard let url = agent.bundleURL else { continue }
|
||||
@@ -49,6 +57,47 @@ import Observation
|
||||
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 {
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
{\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}}}
|
||||
@@ -20,6 +20,8 @@
|
||||
<string>$(CI_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CI_BUILD_NUMBER)</string>
|
||||
<key>GitHubBuildLog</key>
|
||||
<string>https://$(CI_BUILD_LINK)</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Foundation
|
||||
import AppKit
|
||||
|
||||
class PreviewAgentStatusChecker: AgentStatusCheckerProtocol {
|
||||
class PreviewAgentLaunchController: AgentLaunchControllerProtocol {
|
||||
|
||||
let running: Bool
|
||||
let process: NSRunningApplication?
|
||||
@@ -15,4 +15,13 @@ class PreviewAgentStatusChecker: AgentStatusCheckerProtocol {
|
||||
func check() {
|
||||
}
|
||||
|
||||
func install() async throws {
|
||||
}
|
||||
|
||||
func uninstall() async throws {
|
||||
}
|
||||
|
||||
func forceLaunch() async throws {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -60,16 +60,17 @@ extension Preview {
|
||||
let id = UUID()
|
||||
var name: String { "Modifiable Preview Store" }
|
||||
let secrets: [Secret]
|
||||
var supportedKeyTypes: [KeyType] {
|
||||
if #available(macOS 26, *) {
|
||||
[
|
||||
var supportedKeyTypes: KeyAvailability {
|
||||
return KeyAvailability(
|
||||
available: [
|
||||
.ecdsa256,
|
||||
.mldsa65,
|
||||
.mldsa87,
|
||||
.mldsa87
|
||||
],
|
||||
unavailable: [
|
||||
.init(keyType: .ecdsa384, reason: .macOSUpdateRequired)
|
||||
]
|
||||
} else {
|
||||
[.ecdsa256]
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
init(secrets: [Secret]) {
|
||||
|
||||
@@ -32,7 +32,7 @@ struct ConfigurationItemView<Content: View>: View {
|
||||
Spacer()
|
||||
switch action {
|
||||
case .copy(let string):
|
||||
Button(.copyableClickToCopyButton, systemImage: "document.on.document") {
|
||||
Button(.copyableClickToCopyButton, systemImage: "doc.on.doc") {
|
||||
NSPasteboard.general.declareTypes([.string], owner: nil)
|
||||
NSPasteboard.general.setString(string, forType: .string)
|
||||
}
|
||||
|
||||
@@ -21,47 +21,19 @@ struct IntegrationsView: View {
|
||||
}
|
||||
}
|
||||
} detail: {
|
||||
IntegrationsDetailView(selectedInstruction: $selectedInstruction)
|
||||
.fauxToolbar {
|
||||
Button(.setupDoneButton) {
|
||||
dismiss()
|
||||
}
|
||||
.normalButton()
|
||||
}
|
||||
IntegrationsDetailView(selectedInstruction: $selectedInstruction)
|
||||
}
|
||||
.toolbar {
|
||||
Button(.setupDoneButton) {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
.hiddenToolbar()
|
||||
.windowBackgroundStyle(.thinMaterial)
|
||||
.onAppear {
|
||||
selectedInstruction = instructions.gettingStarted
|
||||
}
|
||||
.frame(minHeight: 500)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension View {
|
||||
|
||||
func fauxToolbar<Content: View>(content: () -> Content) -> some View {
|
||||
modifier(FauxToolbarModifier(toolbarContent: content()))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct FauxToolbarModifier<ToolbarContent: View>: ViewModifier {
|
||||
|
||||
var toolbarContent: ToolbarContent
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
content
|
||||
Divider()
|
||||
HStack {
|
||||
Spacer()
|
||||
toolbarContent
|
||||
.padding(.top, 8)
|
||||
.padding(.trailing, 16)
|
||||
.padding(.bottom, 16)
|
||||
}
|
||||
}
|
||||
|
||||
.frame(minWidth: 400, minHeight: 400)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import SwiftUI
|
||||
struct SetupView: View {
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Environment(\.agentLaunchController) private var agentLaunchController
|
||||
@Binding var setupComplete: Bool
|
||||
|
||||
@State var showingIntegrations = false
|
||||
@@ -31,7 +32,7 @@ struct SetupView: View {
|
||||
) {
|
||||
installed = true
|
||||
Task {
|
||||
await LaunchAgentController().install()
|
||||
try? await agentLaunchController.install()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,7 +86,10 @@ struct SetupView: View {
|
||||
integrations = true
|
||||
}, content: {
|
||||
IntegrationsView()
|
||||
.frame(minWidth: 500, minHeight: 400)
|
||||
})
|
||||
.frame(idealWidth: 600)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,10 +176,13 @@ struct StepView<Content: View>: View {
|
||||
.frame(width: 20)
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(title)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.bold()
|
||||
Text(description)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
if let detail {
|
||||
Text(detail)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.font(.callout)
|
||||
.italic()
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ struct ToolConfigurationView: View {
|
||||
|
||||
@State var creating = false
|
||||
@State var selectedSecret: AnySecret?
|
||||
@State var email = ""
|
||||
|
||||
init(selectedInstruction: ConfigurationFileInstructions) {
|
||||
self.selectedInstruction = selectedInstruction
|
||||
@@ -32,6 +33,7 @@ struct ToolConfigurationView: View {
|
||||
selectedSecret = created
|
||||
}
|
||||
}
|
||||
.fixedSize()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,6 +49,12 @@ struct ToolConfigurationView: View {
|
||||
.tag(secret)
|
||||
}
|
||||
}
|
||||
TextField(text: $email, prompt: Text(.integrationsConfigureUsingEmailPlaceholder)) {
|
||||
Text(.integrationsConfigureUsingEmailTitle)
|
||||
Text(.integrationsConfigureUsingEmailSubtitle)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
} header: {
|
||||
Text(.integrationsConfigureUsingSecretHeader)
|
||||
}
|
||||
@@ -59,7 +67,7 @@ struct ToolConfigurationView: View {
|
||||
Section {
|
||||
ConfigurationItemView(title: .integrationsPathTitle, value: stepGroup.path, action: .revealInFinder(stepGroup.path))
|
||||
ForEach(stepGroup.steps, id: \.self.key) { step in
|
||||
ConfigurationItemView(title: .integrationsAddThisTitle, action: .copy(String(localized: step))) {
|
||||
ConfigurationItemView(title: .integrationsAddThisTitle, action: .copy(placeholdersReplaced(text: String(localized: step)))) {
|
||||
HStack {
|
||||
Text(placeholdersReplaced(text: String(localized: step)))
|
||||
.padding(8)
|
||||
@@ -101,9 +109,11 @@ struct ToolConfigurationView: View {
|
||||
func placeholdersReplaced(text: String) -> String {
|
||||
guard let selectedSecret else { return text }
|
||||
let writer = OpenSSHPublicKeyWriter()
|
||||
let gitAllowedSignersString = [email.isEmpty ? String(localized: .integrationsConfigureUsingEmailPlaceholder) : email, writer.openSSHString(secret: selectedSecret)]
|
||||
.joined(separator: " ")
|
||||
let fileController = PublicKeyFileStoreController(homeDirectory: URL.agentHomeURL)
|
||||
return text
|
||||
.replacingOccurrences(of: Instructions.Constants.publicKeyPlaceholder, with: writer.openSSHString(secret: selectedSecret))
|
||||
.replacingOccurrences(of: Instructions.Constants.publicKeyPlaceholder, with: gitAllowedSignersString)
|
||||
.replacingOccurrences(of: Instructions.Constants.publicKeyPathPlaceholder, with: fileController.publicKeyPath(for: selectedSecret))
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ extension View {
|
||||
|
||||
}
|
||||
|
||||
struct MenuButtonModifier: ViewModifier {
|
||||
struct ToolbarCircleButtonModifier: ViewModifier {
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
if #available(macOS 26.0, *) {
|
||||
@@ -40,8 +40,8 @@ struct MenuButtonModifier: ViewModifier {
|
||||
|
||||
extension View {
|
||||
|
||||
func menuButton() -> some View {
|
||||
modifier(MenuButtonModifier())
|
||||
func toolbarCircleButton() -> some View {
|
||||
modifier(ToolbarCircleButtonModifier())
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import SwiftUI
|
||||
|
||||
struct ToolbarButtonStyle: ButtonStyle {
|
||||
struct ToolbarStatusButtonStyle: ButtonStyle {
|
||||
|
||||
private let lightColor: Color
|
||||
private let darkColor: Color
|
||||
@@ -39,6 +39,7 @@ struct ToolbarButtonStyle: ButtonStyle {
|
||||
} else {
|
||||
configuration
|
||||
.label
|
||||
.padding(EdgeInsets(top: 6, leading: 8, bottom: 6, trailing: 8))
|
||||
.background(colorScheme == .light ? lightColor : darkColor)
|
||||
.foregroundColor(.white)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 5))
|
||||
@@ -55,3 +56,27 @@ struct ToolbarButtonStyle: 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
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())
|
||||
}
|
||||
|
||||
}
|
||||
@@ -75,10 +75,23 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
|
||||
Section {
|
||||
VStack {
|
||||
Picker(.createSecretKeyTypeLabel, selection: $keyType) {
|
||||
ForEach(store.supportedKeyTypes, id: \.self) { option in
|
||||
ForEach(store.supportedKeyTypes.available, id: \.self) { option in
|
||||
Text(String(describing: 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 {
|
||||
@@ -119,7 +132,7 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
|
||||
.padding()
|
||||
}
|
||||
.onAppear {
|
||||
keyType = store.supportedKeyTypes.first
|
||||
keyType = store.supportedKeyTypes.available.first
|
||||
}
|
||||
.formStyle(.grouped)
|
||||
}
|
||||
|
||||
@@ -27,11 +27,11 @@ struct SecretListItemView: View {
|
||||
.contextMenu {
|
||||
if store is AnySecretStoreModifiable {
|
||||
Button(action: { isRenaming = true }) {
|
||||
Image(systemName: "pencil")
|
||||
// Image(systemName: "pencil")
|
||||
Text(.secretListEditButton)
|
||||
}
|
||||
Button(action: { isDeleting = true }) {
|
||||
Image(systemName: "trash")
|
||||
// Image(systemName: "trash")
|
||||
Text(.secretListDeleteButton)
|
||||
}
|
||||
}
|
||||
|
||||
69
Sources/Secretive/Views/Views/AboutView.swift
Normal file
@@ -0,0 +1,69 @@
|
||||
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)
|
||||
}
|
||||
@@ -2,10 +2,10 @@ import SwiftUI
|
||||
|
||||
struct AgentStatusView: View {
|
||||
|
||||
@Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol
|
||||
@Environment(\.agentLaunchController) private var agentLaunchController: any AgentLaunchControllerProtocol
|
||||
|
||||
var body: some View {
|
||||
if agentStatusChecker.running {
|
||||
if agentLaunchController.running {
|
||||
AgentRunningView()
|
||||
} else {
|
||||
AgentNotRunningView()
|
||||
@@ -14,12 +14,13 @@ struct AgentStatusView: View {
|
||||
}
|
||||
struct AgentRunningView: View {
|
||||
|
||||
@Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol
|
||||
@Environment(\.agentLaunchController) private var agentLaunchController: any AgentLaunchControllerProtocol
|
||||
@AppStorage("explicitlyDisabled") var explicitlyDisabled = false
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section {
|
||||
if let process = agentStatusChecker.process {
|
||||
if let process = agentLaunchController.process {
|
||||
ConfigurationItemView(
|
||||
title: .agentDetailsLocationTitle,
|
||||
value: process.bundleURL!.path(),
|
||||
@@ -53,19 +54,14 @@ struct AgentRunningView: View {
|
||||
Menu(.agentDetailsRestartAgentButton) {
|
||||
Button(.agentDetailsDisableAgentButton) {
|
||||
Task {
|
||||
_ = await LaunchAgentController()
|
||||
explicitlyDisabled = true
|
||||
try? await agentLaunchController
|
||||
.uninstall()
|
||||
agentStatusChecker.check()
|
||||
}
|
||||
}
|
||||
} primaryAction: {
|
||||
Task {
|
||||
let controller = LaunchAgentController()
|
||||
let installed = await controller.install()
|
||||
if !installed {
|
||||
_ = await controller.forceLaunch()
|
||||
}
|
||||
agentStatusChecker.check()
|
||||
try? await agentLaunchController.forceLaunch()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -82,9 +78,10 @@ struct AgentRunningView: View {
|
||||
|
||||
struct AgentNotRunningView: View {
|
||||
|
||||
@Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol
|
||||
@Environment(\.agentLaunchController) private var agentLaunchController
|
||||
@State var triedRestart = false
|
||||
@State var loading = false
|
||||
@AppStorage("explicitlyDisabled") var explicitlyDisabled = false
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
@@ -100,18 +97,14 @@ struct AgentNotRunningView: View {
|
||||
if !triedRestart {
|
||||
Spacer()
|
||||
Button {
|
||||
explicitlyDisabled = false
|
||||
guard !loading else { return }
|
||||
loading = true
|
||||
Task {
|
||||
let controller = LaunchAgentController()
|
||||
let installed = await controller.install()
|
||||
if !installed {
|
||||
_ = await controller.forceLaunch()
|
||||
}
|
||||
agentStatusChecker.check()
|
||||
try await agentLaunchController.forceLaunch()
|
||||
loading = false
|
||||
|
||||
if !agentStatusChecker.running {
|
||||
if !agentLaunchController.running {
|
||||
triedRestart = true
|
||||
}
|
||||
}
|
||||
@@ -145,9 +138,9 @@ struct AgentNotRunningView: View {
|
||||
|
||||
//#Preview {
|
||||
// AgentStatusView()
|
||||
// .environment(\.agentStatusChecker, PreviewAgentStatusChecker(running: false))
|
||||
// .environment(\.agentLaunchController, PreviewAgentLaunchController(running: false))
|
||||
//}
|
||||
//#Preview {
|
||||
// AgentStatusView()
|
||||
// .environment(\.agentStatusChecker, PreviewAgentStatusChecker(running: true, process: .current))
|
||||
// .environment(\.agentLaunchController, PreviewAgentLaunchController(running: true, process: .current))
|
||||
//}
|
||||
|
||||
@@ -6,19 +6,21 @@ import Brief
|
||||
|
||||
struct ContentView: View {
|
||||
|
||||
@Binding var showingCreation: Bool
|
||||
@Binding var runningSetup: Bool
|
||||
@Binding var hasRunSetup: Bool
|
||||
@State var showingAgentInfo = false
|
||||
@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?
|
||||
|
||||
@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 runningSetup = false
|
||||
@State private var showingAgentInfo = false
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
@@ -35,6 +37,23 @@ struct ContentView: View {
|
||||
toolbarItem(appPathNoticeView, id: "appPath")
|
||||
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) {
|
||||
SetupView(setupComplete: $hasRunSetup)
|
||||
}
|
||||
@@ -85,24 +104,9 @@ extension ContentView {
|
||||
.font(.headline)
|
||||
.foregroundColor(.white)
|
||||
})
|
||||
.buttonStyle(ToolbarButtonStyle(color: color))
|
||||
.buttonStyle(ToolbarStatusButtonStyle(color: color))
|
||||
.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,16 +117,7 @@ extension ContentView {
|
||||
Button(.appMenuNewSecretButton, systemImage: "plus") {
|
||||
showingCreation = true
|
||||
}
|
||||
.menuButton()
|
||||
.sheet(isPresented: $showingCreation) {
|
||||
if let modifiable = storeList.modifiableStore {
|
||||
CreateSecretView(store: modifiable) { created in
|
||||
if let created {
|
||||
activeSecret = created
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.toolbarCircleButton()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,7 +127,7 @@ extension ContentView {
|
||||
showingAgentInfo = true
|
||||
}, label: {
|
||||
HStack {
|
||||
if agentStatusChecker.running {
|
||||
if agentLaunchController.running {
|
||||
Text(.agentRunningNoticeTitle)
|
||||
.font(.headline)
|
||||
.foregroundColor(colorScheme == .light ? Color(white: 0.3) : .white)
|
||||
@@ -149,9 +144,9 @@ extension ContentView {
|
||||
}
|
||||
})
|
||||
.buttonStyle(
|
||||
ToolbarButtonStyle(
|
||||
lightColor: agentStatusChecker.running ? .black.opacity(0.05) : .red.opacity(0.75),
|
||||
darkColor: agentStatusChecker.running ? .white.opacity(0.05) : .red.opacity(0.5),
|
||||
ToolbarStatusButtonStyle(
|
||||
lightColor: agentLaunchController.running ? .black.opacity(0.05) : .red.opacity(0.75),
|
||||
darkColor: agentLaunchController.running ? .white.opacity(0.05) : .red.opacity(0.5),
|
||||
)
|
||||
)
|
||||
.popover(isPresented: $showingAgentInfo, attachmentAnchor: attachmentAnchor, arrowEdge: .bottom) {
|
||||
@@ -171,18 +166,18 @@ extension ContentView {
|
||||
.font(.headline)
|
||||
.foregroundColor(.white)
|
||||
})
|
||||
.buttonStyle(ToolbarButtonStyle(color: .orange))
|
||||
.popover(isPresented: $showingAppPathNotice, attachmentAnchor: attachmentAnchor, arrowEdge: .bottom) {
|
||||
VStack {
|
||||
Image(systemName: "exclamationmark.triangle")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 64)
|
||||
Text(.appNotInApplicationsNoticeDetailDescription)
|
||||
.frame(maxWidth: 300)
|
||||
.buttonStyle(ToolbarStatusButtonStyle(color: .orange))
|
||||
.confirmationDialog(.appNotInApplicationsNoticeTitle, isPresented: $showingAppPathNotice) {
|
||||
Button(.appNotInApplicationsNoticeCancelButton, role: .cancel) {
|
||||
}
|
||||
.padding()
|
||||
Button(.appNotInApplicationsNoticeQuitButton) {
|
||||
NSWorkspace.shared.selectFile(Bundle.main.bundlePath, inFileViewerRootedAtPath: Bundle.main.bundlePath)
|
||||
NSApplication.shared.terminate(nil)
|
||||
}
|
||||
} message: {
|
||||
Text(.appNotInApplicationsNoticeDetailDescription)
|
||||
}
|
||||
.dialogIcon(Image(systemName: "folder.fill.badge.questionmark"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ struct CopyableView: View {
|
||||
@State private var interactionState: InteractionState = .normal
|
||||
|
||||
var content: some View {
|
||||
VStack(alignment: .leading) {
|
||||
VStack(alignment: .leading, spacing: 15) {
|
||||
HStack {
|
||||
image
|
||||
.renderingMode(.template)
|
||||
@@ -28,20 +28,19 @@ struct CopyableView: View {
|
||||
}
|
||||
copyButton
|
||||
}
|
||||
.foregroundColor(secondaryTextColor)
|
||||
.transition(.opacity)
|
||||
.foregroundColor(secondaryTextColor)
|
||||
.transition(.opacity)
|
||||
}
|
||||
|
||||
}
|
||||
.padding(EdgeInsets(top: 20, leading: 20, bottom: 10, trailing: 20))
|
||||
Divider()
|
||||
.ignoresSafeArea()
|
||||
Text(text)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.foregroundColor(primaryTextColor)
|
||||
.padding(EdgeInsets(top: 10, leading: 20, bottom: 20, trailing: 20))
|
||||
.multilineTextAlignment(.leading)
|
||||
.font(.system(.body, design: .monospaced))
|
||||
}
|
||||
.safeAreaPadding(20)
|
||||
._background(interactionState: interactionState)
|
||||
.frame(minWidth: 150, maxWidth: .infinity)
|
||||
}
|
||||
@@ -53,12 +52,12 @@ struct CopyableView: View {
|
||||
interactionState = hovering ? .hovering : .normal
|
||||
}
|
||||
}
|
||||
.onDrag({
|
||||
NSItemProvider(item: NSData(data: text.data(using: .utf8)!), typeIdentifier: UTType.utf8PlainText.identifier)
|
||||
}, preview: {
|
||||
content
|
||||
.draggable(text) {
|
||||
content
|
||||
.lineLimit(3)
|
||||
.frame(maxWidth: 300)
|
||||
._background(interactionState: .dragging)
|
||||
})
|
||||
}
|
||||
.onTapGesture {
|
||||
copy()
|
||||
withAnimation {
|
||||
@@ -79,7 +78,7 @@ struct CopyableView: View {
|
||||
var copyButton: some View {
|
||||
switch interactionState {
|
||||
case .hovering:
|
||||
Button(.copyableClickToCopyButton, systemImage: "document.on.document") {
|
||||
Button(.copyableClickToCopyButton, systemImage: "doc.on.doc") {
|
||||
withAnimation {
|
||||
// Button will eat the click, so we set interaction state manually.
|
||||
interactionState = .clicking
|
||||
@@ -159,7 +158,8 @@ fileprivate struct BackgroundViewModifier: ViewModifier {
|
||||
// Very thin opacity lets user hover anywhere over the view, glassEffect doesn't allow.
|
||||
.background(.white.opacity(0.01), 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 {
|
||||
content
|
||||
.background(backgroundColor(interactionState: interactionState))
|
||||
@@ -170,13 +170,25 @@ fileprivate struct BackgroundViewModifier: ViewModifier {
|
||||
|
||||
func backgroundColor(interactionState: InteractionState) -> Color {
|
||||
guard appearsActive else { return Color.clear }
|
||||
switch interactionState {
|
||||
case .normal:
|
||||
return colorScheme == .dark ? Color(white: 0.2) : Color(white: 0.885)
|
||||
case .hovering, .dragging:
|
||||
return colorScheme == .dark ? Color(white: 0.275) : Color(white: 0.82)
|
||||
case .clicking:
|
||||
return .accentColor
|
||||
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 {
|
||||
case .normal:
|
||||
return colorScheme == .dark ? Color(white: 0.2) : Color(white: 0.885)
|
||||
case .hovering:
|
||||
return colorScheme == .dark ? Color(white: 0.275) : Color(white: 0.82)
|
||||
case .clicking, .dragging:
|
||||
return .accentColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,7 +196,10 @@ fileprivate struct BackgroundViewModifier: ViewModifier {
|
||||
}
|
||||
|
||||
#Preview {
|
||||
CopyableView(title: .secretDetailSha256FingerprintLabel, image: Image(systemName: "figure.wave"), text: "Hello world.")
|
||||
VStack {
|
||||
CopyableView(title: .secretDetailSha256FingerprintLabel, image: Image(systemName: "figure.wave"), text: "Hello world.")
|
||||
CopyableView(title: .secretDetailSha256FingerprintLabel, image: Image(systemName: "figure.wave"), text: "Hello world.")
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
|
||||
@@ -3,61 +3,50 @@ import Brief
|
||||
|
||||
struct UpdateDetailView: View {
|
||||
|
||||
@Environment(\.updater) var updater: any UpdaterProtocol
|
||||
@Environment(\.updater) var updater
|
||||
@Environment(\.openURL) var openURL
|
||||
|
||||
let update: Release
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text(.updateVersionName(updateName: update.name)).font(.title)
|
||||
GroupBox(label: Text(.updateReleaseNotesTitle)) {
|
||||
ScrollView {
|
||||
attributedBody
|
||||
}
|
||||
}
|
||||
HStack {
|
||||
if !update.critical {
|
||||
Button(.updateIgnoreButton) {
|
||||
Task {
|
||||
await updater.ignore(release: update)
|
||||
VStack(spacing: 0) {
|
||||
HStack {
|
||||
if !update.critical {
|
||||
Button(.updateIgnoreButton) {
|
||||
Task {
|
||||
await updater.ignore(release: update)
|
||||
}
|
||||
}
|
||||
.buttonStyle(ToolbarButtonStyle())
|
||||
}
|
||||
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) {
|
||||
openURL(update.html_url)
|
||||
}
|
||||
.buttonStyle(ToolbarButtonStyle(tint: .accentColor))
|
||||
.keyboardShortcut(.defaultAction)
|
||||
}
|
||||
Button(.updateUpdateButton) {
|
||||
NSWorkspace.shared.open(update.html_url)
|
||||
.padding()
|
||||
Divider()
|
||||
Form {
|
||||
Section {
|
||||
Text(update.attributedBody)
|
||||
} header: {
|
||||
Text(.updateVersionName(updateName: update.name)) .headerProminence(.increased)
|
||||
}
|
||||
}
|
||||
.keyboardShortcut(.defaultAction)
|
||||
}
|
||||
|
||||
.formStyle(.grouped)
|
||||
}
|
||||
.padding()
|
||||
.frame(maxWidth: 500)
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
} 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"))
|
||||
}
|
||||
|
||||
@@ -11,7 +11,9 @@ final class SecretiveUpdater: NSObject, XPCProtocol {
|
||||
|
||||
func process(_: Data) async throws -> [Release] {
|
||||
let (data, _) = try await URLSession.shared.data(from: Constants.updateURL)
|
||||
return try JSONDecoder().decode([Release].self, from: data)
|
||||
return try JSONDecoder()
|
||||
.decode([GitHubRelease].self, from: data)
|
||||
.map(Release.init)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||