Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
34 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
14795cf
feat(helper): persist Apple sign-in via WKWebsiteDataStore(forIdentif…
WcaleNieWolny Jun 13, 2026
0b6742e
chore(helper): dev script to locally sign the helper as a .app for pe…
WcaleNieWolny Jun 13, 2026
98c1829
feat(helper): auto-detect persistence via bundle id, drop CAPGO_ASC_K…
WcaleNieWolny Jun 13, 2026
0fe1f87
fix(helper): use a fixed store identifier — file-backed UUID never pe…
WcaleNieWolny Jun 13, 2026
e97d7e1
fix(helper): stop wiping the persisted session on transient auth-redi…
WcaleNieWolny Jun 13, 2026
e7aac52
fix(helper): detect sign-in on any ASC page so a restored session adv…
WcaleNieWolny Jun 13, 2026
35c451d
fix(cli): address hostile-review findings for the ASC key helper
WcaleNieWolny Jun 13, 2026
cdf760a
fix(helper): cleaner '+' highlight — framed ring with a gap, not a so…
WcaleNieWolny Jun 13, 2026
443bdde
fix(helper): force the '+' element round so its highlight ring is a c…
WcaleNieWolny Jun 13, 2026
bd74b78
fix(helper): highlight the REAL '+' button, not a 5x13 sliver
WcaleNieWolny Jun 13, 2026
90a9ae2
fix(helper): match the 16x18 SVG '+' button (probe-confirmed), restor…
WcaleNieWolny Jun 13, 2026
6ee0453
fix(helper): use the unclipped overlay for the '+' (direct ring was c…
WcaleNieWolny Jun 13, 2026
cb9e3af
fix(helper): highlight the VISIBLE '+' SVG, not the hidden duplicate …
WcaleNieWolny Jun 13, 2026
588e467
fix(helper): attach the '+' ring directly to the element, not a float…
WcaleNieWolny Jun 13, 2026
fd3f35f
fix(helper): tighten the '+' ring — drop the fat glow blob
WcaleNieWolny Jun 13, 2026
2ca4163
feat(helper): in-app intro/consent screen + launch helper at 'I don't…
WcaleNieWolny Jun 13, 2026
d32ec2f
feat(helper): success screen that sends the user back to the terminal
WcaleNieWolny Jun 13, 2026
9a5719f
fix(helper): center the consent screen buttons
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".

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".

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