Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
e28991b
feat(cli): guided App Store Connect API key via precompiled macOS helper
WcaleNieWolny Jun 12, 2026
7341f59
feat(cli): move ASC key helper into repo + wire it into iOS onboarding
WcaleNieWolny Jun 12, 2026
ef606c9
test(cli): journey tests for the ASC key helper integration
WcaleNieWolny Jun 12, 2026
2748e28
test(cli): move ASC key helper journey tests to the private suite
WcaleNieWolny Jun 12, 2026
c1e2899
fix(cli): register new p8 fork steps in STEP_PROGRESS + getPhaseLabel
WcaleNieWolny Jun 12, 2026
a4d576f
feat(cli): expand ASC helper protocol with diagnostic log lines → int…
WcaleNieWolny Jun 13, 2026
9f88156
refactor(cli): don't render ASC helper log lines — route them, redact…
WcaleNieWolny Jun 13, 2026
2e7713f
feat(cli): only show the .p8 source fork when the guided helper is av…
WcaleNieWolny Jun 13, 2026
9593a08
fix(helper): harden highlight overlay loop + probe the generate-butto…
WcaleNieWolny Jun 13, 2026
0186a3b
fix(cli): resume the automated p8 path on the helper, not the manual …
WcaleNieWolny Jun 13, 2026
e640c34
fix(helper): attach the '+' highlight directly to the button, not an …
WcaleNieWolny Jun 13, 2026
607c3e2
fix(cli): kill the guided helper on quit so the CLI doesn't hang
WcaleNieWolny Jun 13, 2026
762f6fc
fix(cli): scope helper-kill to unmount + surface crash signal (NO_RES…
WcaleNieWolny Jun 13, 2026
801e104
fix(helper): bigger, rounder ring on the '+' highlight
WcaleNieWolny Jun 13, 2026
d12bc4a
feat(helper): persist Apple sign-in across runs (no re-login each time)
WcaleNieWolny Jun 13, 2026
2e985c5
fix(helper): persist Apple sign-in via native cookies, not forIdentif…
WcaleNieWolny Jun 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions cli/native/asc-key-helper/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.build/
dist-helper/
*.xcodeproj
.DS_Store
20 changes: 20 additions & 0 deletions cli/native/asc-key-helper/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// swift-tools-version: 5.9
import PackageDescription

// Capgo App Store Connect API-key helper — a native macOS app that guides the
// user through creating an App Store Connect *team* API key in an embedded
// browser and emits the result + a stats protocol on stdout (see
// Sources/Models/StatsProtocol.swift). The Capgo CLI spawns the precompiled
// binary; build it with cli/scripts/build-asc-key-helper.sh.
let package = Package(
name: "P8Extract",
platforms: [
.macOS(.v14),
],
targets: [
.executableTarget(
name: "P8Extract",
path: "Sources"
),
]
)
29 changes: 29 additions & 0 deletions cli/native/asc-key-helper/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Capgo App Store Connect API-key helper (macOS)

A native macOS SwiftUI app that guides a user through creating an App Store
Connect **team** API key inside an embedded browser — auto-capturing the Issuer
ID + Key ID, intercepting the one-time `.p8`, validating it against Apple, and
saving it to `~/.appstoreconnect/private_keys/`.

The Capgo CLI spawns the **precompiled** binary from the iOS build-credentials
onboarding (`build init`) when the user has no `.p8` yet, and from
`build credentials apple-key`. The helper streams a stdout **stats protocol**
(`Sources/Models/StatsProtocol.swift`) that the CLI forwards to PostHog; see
`../../src/build/onboarding/asc-key/PROTOCOL.md`.

## Build

```bash
# Universal (arm64 + x86_64) release binary:
../../scripts/build-asc-key-helper.sh

# Or directly with SwiftPM (single arch, for dev):
swift build -c release
# → .build/release/P8Extract
```

The binary is **not** shipped in the npm tarball (macOS-only). The CLI locates
it via `CAPGO_ASC_KEY_HELPER_PATH`, the `~/.capgo/asc-key-helper/` cache, or a
local `.build/` product during development.

Portions adapted from AppStoreConnectKit (MIT) — see `THIRD-PARTY-LICENSES.md`.
92 changes: 92 additions & 0 deletions cli/native/asc-key-helper/Sources/Models/ASCSession.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import Foundation

/// A team (provider) the signed-in user belongs to.
struct ASCTeam: Identifiable, Equatable {
/// Numeric providerId — stable identity, used to compare/verify.
let id: String
/// publicProviderId (UUID) — required by the providerSwitchRequests API.
let publicId: String?
let name: String
}

/// The signed-in App Store Connect session, as reported by the same
/// `/olympus/v1/session` endpoint the ASC web app uses.
struct ASCSession: Equatable {
let currentTeam: ASCTeam
let teams: [ASCTeam]
let email: String
let role: String?
/// Raw role strings for the current team (e.g. ["DEVELOPER", "ADMIN"]).
let roles: [String]
/// Per-team feature flags (e.g. contains "apiKeys" when the team has the
/// App Store Connect API feature enabled).
let featureFlags: [String]

var otherTeams: [ASCTeam] {
teams.filter { $0.id != currentTeam.id }
}

/// The team has turned on the App Store Connect API (the Account Holder
/// did the one-time "Request Access" under Integrations).
var teamHasApiEnabled: Bool { featureFlags.contains("apiKeys") }

/// The user holds a role allowed to generate team keys.
var userCanGenerateKeys: Bool {
roles.contains { $0 == "ADMIN" || $0 == "ACCOUNT_HOLDER" }
}

/// Both gates must pass to create a team API key here.
var canCreateKeys: Bool { teamHasApiEnabled && userCanGenerateKeys }

/// Defensive parse — Apple doesn't document this payload, so missing
/// fields degrade to a smaller session instead of a failure.
static func parse(jsonText: String) -> ASCSession? {
guard let data = jsonText.data(using: .utf8),
let root = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let provider = root["provider"] as? [String: Any],
let current = team(from: provider) else {
return nil
}
let available = (root["availableProviders"] as? [[String: Any]] ?? [])
.compactMap { team(from: $0) }
let user = root["user"] as? [String: Any]
let email = (user?["emailAddress"] as? String) ?? ""
// Roles live at the root of the session payload (e.g. ["DEVELOPER",
// "CIPS", "ADMIN"]) and mix real roles with plumbing flags.

Check warning on line 55 in cli/native/asc-key-helper/Sources/Models/ASCSession.swift

View workflow job for this annotation

GitHub Actions / Check for typos (CLI)

"CIPS" should be "CHIPS".
let roles = (root["roles"] as? [String]) ?? (user?["roles"] as? [String]) ?? []
let featureFlags = (root["featureFlags"] as? [String]) ?? []
return ASCSession(
currentTeam: current,
teams: available.isEmpty ? [current] : available,
email: email,
role: primaryRole(from: roles),
roles: roles,
featureFlags: featureFlags
)
}

/// Pick the most privileged human-meaningful role; ignore plumbing
/// entries like CIPS or CLOUD_MANAGED_APP_DISTRIBUTION.

Check warning on line 69 in cli/native/asc-key-helper/Sources/Models/ASCSession.swift

View workflow job for this annotation

GitHub Actions / Check for typos (CLI)

"CIPS" should be "CHIPS".
private static func primaryRole(from roles: [String]) -> String? {
let ranking = [
"ACCOUNT_HOLDER", "ADMIN", "APP_MANAGER", "DEVELOPER",
"MARKETING", "FINANCE", "SALES", "CUSTOMER_SUPPORT",
]
return ranking.first(where: roles.contains).map(prettyRole)
}

private static func team(from object: [String: Any]) -> ASCTeam? {
guard let name = object["name"] as? String else { return nil }
let numericId = (object["providerId"] as? Int).map(String.init)
?? (object["providerId"] as? String)
let publicId = object["publicProviderId"] as? String
guard let id = numericId ?? publicId else { return nil }
return ASCTeam(id: id, publicId: publicId, name: name)
}

private static func prettyRole(_ raw: String) -> String {
raw.split(separator: "_")
.map { $0.capitalized }
.joined(separator: " ")
}
}
Loading
Loading