@capgo/cli 7.100.8 → 7.102.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,351 @@
1
+ // keychain-export.swift
2
+ //
3
+ // Capgo helper: export ONE iOS signing identity from the user's Keychain as a
4
+ // PKCS#12 blob. Always emits a single line of JSON on stdout describing the
5
+ // outcome — successful or otherwise — so the Node caller never has to parse
6
+ // stderr or guess from exit codes.
7
+ //
8
+ // Usage:
9
+ // keychain-export --sha1 <40-hex-char-cert-sha1>
10
+ // --output <path-to-output.p12>
11
+ // --passphrase <wrap-passphrase-for-p12>
12
+ //
13
+ // JSON output (single line on stdout, ALWAYS emitted before exit):
14
+ //
15
+ // Success:
16
+ // {"ok":true,"p12Path":"/tmp/x.p12","p12SizeBytes":4096,"identityName":"Apple Distribution: …"}
17
+ //
18
+ // Failure:
19
+ // {"ok":false,"errorCode":"USER_DENIED","message":"…","osStatus":-128}
20
+ // {"ok":false,"errorCode":"NO_IDENTITY","message":"…"}
21
+ // {"ok":false,"errorCode":"INVALID_ARGS","message":"…"}
22
+ // {"ok":false,"errorCode":"EXPORT_FAILED","message":"…","osStatus":-12345}
23
+ // {"ok":false,"errorCode":"WRITE_FAILED","message":"…"}
24
+ // {"ok":false,"errorCode":"INTERNAL","message":"…"}
25
+ //
26
+ // Exit codes (still emitted for shell-style consumers):
27
+ // 0 — success
28
+ // 1 — generic / internal error
29
+ // 2 — argument parsing error (INVALID_ARGS)
30
+ // 3 — no identity matching the given SHA1 (NO_IDENTITY)
31
+ // 4 — user denied macOS Keychain access (USER_DENIED)
32
+ //
33
+ // Why we use SecItemExport(.formatPKCS12) and accept the 2 prompts:
34
+ // Xcode-imported signing keys are non-extractable (kSecKeyExtractable=false).
35
+ // `SecKeyCopyExternalRepresentation` rejects them with
36
+ // CSSMERR_CSP_INVALID_KEYATTR_MASK. PKCS#12 wrapped export is the only
37
+ // non-GUI path that works on these keys. macOS asks the user twice on first
38
+ // run — once for "access" ACL, once for "export" ACL — but caches both
39
+ // "Always Allow" decisions, so subsequent runs are silent.
40
+ //
41
+ // Build:
42
+ // swiftc keychain-export.swift -framework Security -o keychain-export
43
+ //
44
+ // Tested on macOS 11+ (Swift 5.5+, CryptoKit available).
45
+
46
+ import CryptoKit
47
+ import Foundation
48
+ import Security
49
+
50
+ // MARK: - Output (always JSON on stdout, always before exit)
51
+
52
+ /// JSON-escape a string for embedding in our hand-rolled JSON output. We
53
+ /// avoid Foundation's JSONSerialization for output to keep the line shape
54
+ /// fully predictable (one line, no spaces, ASCII only when possible).
55
+ func jsonEscape(_ s: String) -> String {
56
+ var out = ""
57
+ out.reserveCapacity(s.count)
58
+ for scalar in s.unicodeScalars {
59
+ switch scalar {
60
+ case "\"": out += "\\\""
61
+ case "\\": out += "\\\\"
62
+ case "\n": out += "\\n"
63
+ case "\r": out += "\\r"
64
+ case "\t": out += "\\t"
65
+ case "\u{08}": out += "\\b"
66
+ case "\u{0C}": out += "\\f"
67
+ default:
68
+ if scalar.value < 0x20 {
69
+ out += String(format: "\\u%04x", scalar.value)
70
+ } else {
71
+ out.unicodeScalars.append(scalar)
72
+ }
73
+ }
74
+ }
75
+ return out
76
+ }
77
+
78
+ /// Emit a JSON line to stdout and exit. NEVER call exit() any other way.
79
+ func emitSuccessAndExit(p12Path: String, p12SizeBytes: Int, identityName: String) -> Never {
80
+ let json = "{\"ok\":true,"
81
+ + "\"p12Path\":\"\(jsonEscape(p12Path))\","
82
+ + "\"p12SizeBytes\":\(p12SizeBytes),"
83
+ + "\"identityName\":\"\(jsonEscape(identityName))\""
84
+ + "}"
85
+ print(json)
86
+ exit(0)
87
+ }
88
+
89
+ func emitFailureAndExit(
90
+ code: Int32,
91
+ errorCode: String,
92
+ message: String,
93
+ osStatus: OSStatus? = nil
94
+ ) -> Never {
95
+ var json = "{\"ok\":false,"
96
+ + "\"errorCode\":\"\(jsonEscape(errorCode))\","
97
+ + "\"message\":\"\(jsonEscape(message))\""
98
+ if let s = osStatus {
99
+ json += ",\"osStatus\":\(s)"
100
+ }
101
+ json += "}"
102
+ print(json)
103
+ exit(code)
104
+ }
105
+
106
+ // MARK: - Top-level fatal handler
107
+ //
108
+ // If anything in main throws, traps, or hits an uncaught issue, we want to at
109
+ // least emit a JSON line. Swift doesn't have an easy uncaught-exception hook,
110
+ // so the pattern is: wrap all real work in do/catch + use guard everywhere
111
+ // instead of force-unwrap. There are still ways to crash Swift (e.g. real
112
+ // SIGSEGV from a corrupted heap), but in practice anything reachable from our
113
+ // code is recoverable into a JSON failure line.
114
+
115
+ enum KeychainExportError: Error {
116
+ case invalidArgs(String)
117
+ case noIdentity(String)
118
+ case userDenied(OSStatus, String)
119
+ case exportFailed(OSStatus, String)
120
+ case writeFailed(String)
121
+ case copyFailed(OSStatus, String)
122
+ }
123
+
124
+ extension KeychainExportError {
125
+ var errorCode: String {
126
+ switch self {
127
+ case .invalidArgs: return "INVALID_ARGS"
128
+ case .noIdentity: return "NO_IDENTITY"
129
+ case .userDenied: return "USER_DENIED"
130
+ case .exportFailed: return "EXPORT_FAILED"
131
+ case .writeFailed: return "WRITE_FAILED"
132
+ case .copyFailed: return "EXPORT_FAILED"
133
+ }
134
+ }
135
+ var exitCode: Int32 {
136
+ switch self {
137
+ case .invalidArgs: return 2
138
+ case .noIdentity: return 3
139
+ case .userDenied: return 4
140
+ default: return 1
141
+ }
142
+ }
143
+ var message: String {
144
+ switch self {
145
+ case let .invalidArgs(m), let .noIdentity(m), let .writeFailed(m): return m
146
+ case let .userDenied(_, m), let .exportFailed(_, m), let .copyFailed(_, m): return m
147
+ }
148
+ }
149
+ var osStatus: OSStatus? {
150
+ switch self {
151
+ case let .userDenied(s, _), let .exportFailed(s, _), let .copyFailed(s, _): return s
152
+ default: return nil
153
+ }
154
+ }
155
+ }
156
+
157
+ func emitFailureAndExit(_ error: KeychainExportError) -> Never {
158
+ emitFailureAndExit(
159
+ code: error.exitCode,
160
+ errorCode: error.errorCode,
161
+ message: error.message,
162
+ osStatus: error.osStatus
163
+ )
164
+ }
165
+
166
+ func describeStatus(_ status: OSStatus) -> String {
167
+ let secMessage = SecCopyErrorMessageString(status, nil) as String? ?? "(no description)"
168
+ return "\(secMessage) [OSStatus \(status)]"
169
+ }
170
+
171
+ // MARK: - Args
172
+
173
+ struct Args {
174
+ var sha1Hex: String = ""
175
+ var outputPath: String = ""
176
+ var passphrase: String = ""
177
+ }
178
+
179
+ func parseArgs() throws -> Args {
180
+ var args = Args()
181
+ let cli = CommandLine.arguments
182
+ var i = 1
183
+ while i < cli.count {
184
+ let flag = cli[i]
185
+ i += 1
186
+ guard i < cli.count else {
187
+ throw KeychainExportError.invalidArgs("Missing value for \(flag)")
188
+ }
189
+ let value = cli[i]
190
+ i += 1
191
+ switch flag {
192
+ case "--sha1": args.sha1Hex = value.lowercased()
193
+ case "--output": args.outputPath = value
194
+ case "--passphrase": args.passphrase = value
195
+ default: throw KeychainExportError.invalidArgs("Unknown argument: \(flag)")
196
+ }
197
+ }
198
+ if args.sha1Hex.isEmpty {
199
+ throw KeychainExportError.invalidArgs("Required: --sha1 <40-hex-char-cert-sha1>")
200
+ }
201
+ if args.outputPath.isEmpty {
202
+ throw KeychainExportError.invalidArgs("Required: --output <path>")
203
+ }
204
+ if args.passphrase.isEmpty {
205
+ throw KeychainExportError.invalidArgs("Required: --passphrase <wrap-passphrase>")
206
+ }
207
+ if args.sha1Hex.count != 40 || args.sha1Hex.range(of: "^[0-9a-f]{40}$", options: .regularExpression) == nil {
208
+ throw KeychainExportError.invalidArgs("--sha1 must be 40 lowercase hex chars (got \"\(args.sha1Hex)\")")
209
+ }
210
+ return args
211
+ }
212
+
213
+ // MARK: - SHA1 of cert DER (matches `security find-identity` output)
214
+
215
+ func sha1OfCertDer(_ cert: SecCertificate) -> String {
216
+ let derData = SecCertificateCopyData(cert) as Data
217
+ let hash = Insecure.SHA1.hash(data: derData)
218
+ return hash.map { String(format: "%02x", $0) }.joined()
219
+ }
220
+
221
+ func subjectName(of cert: SecCertificate) -> String {
222
+ var commonName: CFString?
223
+ let status = SecCertificateCopyCommonName(cert, &commonName)
224
+ if status == errSecSuccess, let cn = commonName as String? { return cn }
225
+ return SecCertificateCopySubjectSummary(cert) as String? ?? "(unknown)"
226
+ }
227
+
228
+ // MARK: - Find identity by cert SHA1
229
+
230
+ func findIdentityBySha1(_ targetSha1: String) throws -> (SecIdentity, String) {
231
+ let query: [String: Any] = [
232
+ kSecClass as String: kSecClassIdentity,
233
+ kSecReturnRef as String: true,
234
+ kSecMatchLimit as String: kSecMatchLimitAll,
235
+ ]
236
+ var result: CFTypeRef?
237
+ let status = SecItemCopyMatching(query as CFDictionary, &result)
238
+ if status == errSecItemNotFound {
239
+ throw KeychainExportError.noIdentity(
240
+ "No identity with cert SHA1 \(targetSha1) found (keychain has no identities at all)."
241
+ )
242
+ }
243
+ if status != errSecSuccess {
244
+ throw KeychainExportError.copyFailed(status, "SecItemCopyMatching(identities) failed: \(describeStatus(status))")
245
+ }
246
+ guard let identities = result as? [SecIdentity] else {
247
+ throw KeychainExportError.copyFailed(0, "SecItemCopyMatching returned an unexpected type")
248
+ }
249
+
250
+ for identity in identities {
251
+ var maybeCert: SecCertificate?
252
+ let copyStatus = SecIdentityCopyCertificate(identity, &maybeCert)
253
+ if copyStatus != errSecSuccess { continue }
254
+ guard let cert = maybeCert else { continue }
255
+ if sha1OfCertDer(cert) == targetSha1 {
256
+ return (identity, subjectName(of: cert))
257
+ }
258
+ }
259
+ throw KeychainExportError.noIdentity(
260
+ "No identity with cert SHA1 \(targetSha1) found in any keychain in your default search list."
261
+ )
262
+ }
263
+
264
+ // MARK: - Export to PKCS#12
265
+
266
+ func exportIdentityAsPkcs12(_ identity: SecIdentity, passphrase: String) throws -> Data {
267
+ // CFString must outlive the SecItemExport call. Holding `cfPass` in a
268
+ // local keeps it alive for the duration of this function.
269
+ let cfPass: CFString = passphrase as CFString
270
+ var keyParams = SecItemImportExportKeyParameters()
271
+ keyParams.version = UInt32(SEC_KEY_IMPORT_EXPORT_PARAMS_VERSION)
272
+ keyParams.passphrase = Unmanaged.passUnretained(cfPass)
273
+
274
+ var exportedData: CFData?
275
+ let status = withUnsafePointer(to: &keyParams) { paramsPtr in
276
+ SecItemExport(
277
+ identity,
278
+ .formatPKCS12,
279
+ SecItemImportExportFlags(rawValue: 0),
280
+ paramsPtr,
281
+ &exportedData
282
+ )
283
+ }
284
+
285
+ // Treat user-denied / canceled distinctly so the caller can offer retry
286
+ // vs. fall back to a different path. -128 is errSecUserCanceled (raw
287
+ // value not always present in Swift's enum on older SDKs, hence direct
288
+ // comparison).
289
+ if status == errSecAuthFailed || status == errSecUserCanceled || status == -128 {
290
+ throw KeychainExportError.userDenied(
291
+ status,
292
+ "macOS Keychain access was denied by the user. \(describeStatus(status))"
293
+ )
294
+ }
295
+ if status != errSecSuccess {
296
+ throw KeychainExportError.exportFailed(
297
+ status,
298
+ "SecItemExport failed: \(describeStatus(status))"
299
+ )
300
+ }
301
+ guard let data = exportedData else {
302
+ throw KeychainExportError.exportFailed(0, "SecItemExport returned nil data with success status")
303
+ }
304
+
305
+ // Keep cfPass alive past the call — Unmanaged.passUnretained doesn't
306
+ // bump the retain count; the Security framework relies on us holding it.
307
+ _ = cfPass
308
+ return data as Data
309
+ }
310
+
311
+ // MARK: - Disk write
312
+
313
+ func writeP12(_ data: Data, to path: String) throws {
314
+ do {
315
+ try data.write(to: URL(fileURLWithPath: path), options: .atomic)
316
+ } catch {
317
+ throw KeychainExportError.writeFailed(
318
+ "Failed to write P12 to \(path): \(error.localizedDescription)"
319
+ )
320
+ }
321
+ // Best-effort 0600 chmod. Non-fatal if it fails.
322
+ do {
323
+ try FileManager.default.setAttributes(
324
+ [.posixPermissions: NSNumber(value: Int16(0o600))],
325
+ ofItemAtPath: path
326
+ )
327
+ } catch {
328
+ FileHandle.standardError.write(
329
+ Data("warning: could not chmod 0600 on \(path): \(error.localizedDescription)\n".utf8)
330
+ )
331
+ }
332
+ }
333
+
334
+ // MARK: - Main
335
+
336
+ do {
337
+ let args = try parseArgs()
338
+ let (identity, identityName) = try findIdentityBySha1(args.sha1Hex)
339
+ let p12 = try exportIdentityAsPkcs12(identity, passphrase: args.passphrase)
340
+ try writeP12(p12, to: args.outputPath)
341
+ emitSuccessAndExit(p12Path: args.outputPath, p12SizeBytes: p12.count, identityName: identityName)
342
+ } catch let error as KeychainExportError {
343
+ emitFailureAndExit(error)
344
+ } catch {
345
+ // Any other Swift error (Foundation throw, etc.) lands here.
346
+ emitFailureAndExit(
347
+ code: 1,
348
+ errorCode: "INTERNAL",
349
+ message: "Unhandled error: \(error.localizedDescription)"
350
+ )
351
+ }
package/dist/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@capgo/cli",
3
3
  "type": "module",
4
- "version": "7.100.8",
4
+ "version": "7.102.0",
5
5
  "description": "A CLI to upload to capgo servers",
6
6
  "author": "Martin martin@capgo.app",
7
7
  "license": "Apache 2.0",
@@ -88,8 +88,10 @@
88
88
  "test:version-detection:setup": "./test/fixtures/setup-test-projects.sh",
89
89
  "test:platform-paths": "bun test/test-platform-paths.mjs",
90
90
  "test:payload-split": "bun test/test-payload-split.mjs",
91
+ "test:macos-signing": "bun test/test-macos-signing.mjs",
92
+ "test:apple-api-import-helpers": "bun test/test-apple-api-import-helpers.mjs",
91
93
  "test:manifest-path-encoding": "bun test/test-manifest-path-encoding.mjs",
92
- "test": "bun run build && bun run test:version-detection:setup && bun run test:bundle && bun run test:functional && bun run test:semver && bun run test:version-edge-cases && bun run test:regex && bun run test:upload && bun run test:credentials && bun run test:credentials-validation && bun run test:build-zip-filter && bun run test:checksum && bun run test:build-needed && bun run test:ci-prompts && bun run test:posthog-exception && bun run test:build-platform-selection && bun run test:onboarding-recovery && bun run test:onboarding-run-targets && bun run test:run-device-command && bun run test:init-app-conflict && bun run test:init-guardrails && bun run test:prompt-preferences && bun run test:esm-sdk && bun run test:mcp && bun run test:version-detection && bun run test:platform-paths && bun run test:payload-split && bun run test:manifest-path-encoding",
94
+ "test": "bun run build && bun run test:version-detection:setup && bun run test:bundle && bun run test:functional && bun run test:semver && bun run test:version-edge-cases && bun run test:regex && bun run test:upload && bun run test:credentials && bun run test:credentials-validation && bun run test:build-zip-filter && bun run test:checksum && bun run test:build-needed && bun run test:ci-prompts && bun run test:posthog-exception && bun run test:build-platform-selection && bun run test:onboarding-recovery && bun run test:onboarding-run-targets && bun run test:run-device-command && bun run test:init-app-conflict && bun run test:init-guardrails && bun run test:prompt-preferences && bun run test:esm-sdk && bun run test:mcp && bun run test:version-detection && bun run test:platform-paths && bun run test:payload-split && bun run test:manifest-path-encoding && bun run test:macos-signing && bun run test:apple-api-import-helpers",
93
95
  "test:build-platform-selection": "bun test/test-build-platform-selection.mjs"
94
96
  },
95
97
  "dependencies": {
@@ -4,5 +4,32 @@ export interface MobileprovisionInfo {
4
4
  applicationIdentifier: string;
5
5
  bundleId: string;
6
6
  }
7
+ /**
8
+ * Detail returned by {@link parseMobileprovisionDetailed} — extends
9
+ * {@link MobileprovisionInfo} with team/expiry/profile-type metadata and the
10
+ * SHA1 of each developer certificate embedded in the profile.
11
+ *
12
+ * The SHA1 list enables matching a profile against a Keychain identity
13
+ * returned by `security find-identity` (which reports identities by the same
14
+ * SHA1 hash).
15
+ */
16
+ export interface MobileprovisionDetail extends MobileprovisionInfo {
17
+ /** Apple Team ID (10-char alphanumeric) — empty string if not present */
18
+ teamId: string;
19
+ /** ISO timestamp string from the profile's ExpirationDate, or empty string */
20
+ expirationDate: string;
21
+ /** High-level profile type derived from the profile's flags */
22
+ profileType: 'app_store' | 'ad_hoc' | 'development' | 'enterprise' | 'unknown';
23
+ /** SHA1 (40-char lowercase hex) of each DeveloperCertificate embedded in the profile */
24
+ certificateSha1s: string[];
25
+ }
7
26
  export declare function parseMobileprovision(filePath: string): MobileprovisionInfo;
8
27
  export declare function parseMobileprovisionFromBase64(base64Content: string): MobileprovisionInfo;
28
+ /**
29
+ * Parse a mobileprovision file and return enriched metadata including:
30
+ * - team ID
31
+ * - expiration date
32
+ * - profile type (app_store / ad_hoc / development / enterprise)
33
+ * - SHA1 of each embedded developer certificate (used for cert↔profile matching)
34
+ */
35
+ export declare function parseMobileprovisionDetailed(filePath: string): MobileprovisionDetail;
@@ -11,15 +11,60 @@ export declare function verifyApiKey(token: string): Promise<{
11
11
  valid: true;
12
12
  teamId: string;
13
13
  }>;
14
+ export interface AscDistributionCert {
15
+ id: string;
16
+ name: string;
17
+ serialNumber: string;
18
+ expirationDate: string;
19
+ /**
20
+ * Base64-encoded DER of the certificate. Populated when {@link listDistributionCerts}
21
+ * is called with `includeContent: true` — kept optional so existing callers don't pay
22
+ * the larger payload when they don't need it.
23
+ */
24
+ certificateContent?: string;
25
+ }
14
26
  /**
15
27
  * List all iOS distribution certificates.
28
+ *
29
+ * Set `includeContent: true` when you need to compute the cert's SHA1 for
30
+ * matching against a local Keychain identity ({@link findCertIdBySha1}).
31
+ */
32
+ export declare function listDistributionCerts(token: string, options?: {
33
+ includeContent?: boolean;
34
+ }): Promise<AscDistributionCert[]>;
35
+ /**
36
+ * Compute the SHA1 hash of an ASC certificate's base64-DER content. Returns
37
+ * the lowercase 40-char hex string used elsewhere as the canonical identity
38
+ * key — matches the SHA1 reported by `security find-identity` on macOS.
39
+ *
40
+ * SECURITY NOTE on SHA1: this is NOT a security primitive. macOS itself
41
+ * reports code-signing identities as cert-DER SHA1 (via `security
42
+ * find-identity`), and we have to use the same hash to look up an Apple-side
43
+ * cert by its on-Mac counterpart. SHA1 here is a non-secret identifier, not
44
+ * a message digest protecting any data. CodeQL's "weak cryptographic
45
+ * algorithm" rule is suppressed for this reason.
46
+ */
47
+ export declare function computeCertSha1(certificateContentBase64: string): string;
48
+ /**
49
+ * Match a local Keychain identity (by its SHA1) against an Apple-side
50
+ * certificate and return the Apple certificate ID needed for profile
51
+ * creation. Returns null if no Apple-side cert matches the SHA1.
52
+ */
53
+ export declare function findCertIdBySha1(token: string, sha1: string): Promise<string | null>;
54
+ /**
55
+ * List all provisioning profiles linked to a specific Apple-side certificate.
56
+ * Used by the import-flow no-match-recovery menu to surface profiles that
57
+ * exist on Apple but haven't been downloaded to the user's Mac.
16
58
  */
17
- export declare function listDistributionCerts(token: string): Promise<Array<{
59
+ export interface AscProfileSummary {
18
60
  id: string;
19
61
  name: string;
20
- serialNumber: string;
62
+ profileType: string;
63
+ profileContent: string;
21
64
  expirationDate: string;
22
- }>>;
65
+ bundleIdentifier: string;
66
+ }
67
+ export declare function listProfilesForCert(token: string, certificateId: string): Promise<AscProfileSummary[]>;
23
68
  /**
24
69
  * Revoke (delete) a certificate by ID.
25
70
  */
@@ -29,18 +74,8 @@ export declare function revokeCertificate(token: string, certId: string): Promis
29
74
  * Contains the existing certificates so the UI can ask the user which to revoke.
30
75
  */
31
76
  export declare class CertificateLimitError extends Error {
32
- readonly certificates: Array<{
33
- id: string;
34
- name: string;
35
- serialNumber: string;
36
- expirationDate: string;
37
- }>;
38
- constructor(certificates: Array<{
39
- id: string;
40
- name: string;
41
- serialNumber: string;
42
- expirationDate: string;
43
- }>);
77
+ readonly certificates: AscDistributionCert[];
78
+ constructor(certificates: AscDistributionCert[]);
44
79
  }
45
80
  /**
46
81
  * Create a distribution certificate using a CSR.
@@ -0,0 +1,159 @@
1
+ import type { MobileprovisionDetail } from '../mobileprovision-parser.js';
2
+ /** Standard locations Xcode writes provisioning profiles into. */
3
+ export declare const PROVISIONING_PROFILE_DIRS: readonly ["Library/Developer/Xcode/UserData/Provisioning Profiles", "Library/MobileDevice/Provisioning Profiles"];
4
+ export type IdentityType = 'distribution' | 'development' | 'unknown';
5
+ export interface SigningIdentity {
6
+ /** SHA1 hash of the certificate, lowercase 40-char hex */
7
+ sha1: string;
8
+ /** Full identity string from `security find-identity` (e.g. "Apple Distribution: Acme Corp (XYZ123ABCD)") */
9
+ name: string;
10
+ /** Best-effort classification from the name prefix */
11
+ type: IdentityType;
12
+ /** Human-readable team name extracted from the identity string */
13
+ teamName: string;
14
+ /** Apple Team ID (10-char alphanumeric) extracted from the identity string */
15
+ teamId: string;
16
+ }
17
+ export interface DiscoveredProfile extends MobileprovisionDetail {
18
+ /** Absolute path to the .mobileprovision file */
19
+ path: string;
20
+ }
21
+ export interface IdentityProfileMatch {
22
+ identity: SigningIdentity;
23
+ /** Profiles whose embedded developer certs include this identity's SHA1 */
24
+ profiles: DiscoveredProfile[];
25
+ }
26
+ export interface ExportedP12 {
27
+ /** Base64-encoded PKCS#12 blob containing the chosen identity's cert + private key */
28
+ base64: string;
29
+ /** Auto-generated passphrase used to wrap the export */
30
+ passphrase: string;
31
+ }
32
+ export declare class MacOSSigningError extends Error {
33
+ readonly cause?: unknown | undefined;
34
+ constructor(message: string, cause?: unknown | undefined);
35
+ }
36
+ export declare class NotMacOSError extends MacOSSigningError {
37
+ constructor();
38
+ }
39
+ /** Returns `true` when running on macOS (Darwin). */
40
+ export declare function isMacOS(): boolean;
41
+ /**
42
+ * Run a subprocess and capture stdout/stderr/exit-code.
43
+ *
44
+ * Public so tests can inject a fake runner via the optional argument on
45
+ * higher-level functions. Not intended for downstream callers.
46
+ */
47
+ export interface SecurityRunResult {
48
+ stdout: string;
49
+ stderr: string;
50
+ code: number | null;
51
+ }
52
+ export type SecurityRunner = (args: readonly string[]) => Promise<SecurityRunResult>;
53
+ /**
54
+ * Parse the human-readable output of `security find-identity -v -p codesigning`.
55
+ * Each line looks like:
56
+ * ` 1) <SHA1> "Apple Distribution: Acme Corp (XYZ123ABCD)"`
57
+ *
58
+ * Exported so unit tests can verify parsing without spawning a subprocess.
59
+ */
60
+ export declare function parseFindIdentityOutput(stdout: string): SigningIdentity[];
61
+ /**
62
+ * List all code-signing identities visible in the user's default Keychain.
63
+ * Read-only — does NOT trigger any Keychain access prompt.
64
+ *
65
+ * @param runner Optional injection point for testing. Pass a fake to avoid
66
+ * spawning the real `/usr/bin/security` binary.
67
+ */
68
+ export declare function listSigningIdentities(runner?: SecurityRunner): Promise<SigningIdentity[]>;
69
+ /**
70
+ * Scan all standard Xcode provisioning-profile directories under the user's
71
+ * home and return parsed metadata for every readable `.mobileprovision`.
72
+ *
73
+ * Read-only — pure filesystem reads, no Keychain interaction.
74
+ *
75
+ * Files that fail to parse are silently skipped (a teammate's malformed
76
+ * profile shouldn't break the whole listing).
77
+ *
78
+ * @param homeDirOverride Optional override for HOME, used in tests.
79
+ */
80
+ export declare function scanProvisioningProfiles(homeDirOverride?: string): Promise<DiscoveredProfile[]>;
81
+ /**
82
+ * Given a list of identities and profiles, return one match entry per
83
+ * identity, populated with profiles whose embedded developer certs include
84
+ * that identity's SHA1.
85
+ *
86
+ * Pure function — no I/O.
87
+ */
88
+ export declare function matchIdentitiesToProfiles(identities: readonly SigningIdentity[], profiles: readonly DiscoveredProfile[]): IdentityProfileMatch[];
89
+ /**
90
+ * Generate a cryptographically random passphrase suitable for wrapping the
91
+ * exported PKCS#12. 32 bytes of entropy → 64-char hex string.
92
+ */
93
+ export declare function generateP12Passphrase(): string;
94
+ /**
95
+ * Output shape from the Swift helper's stdout — always emitted as one line of
96
+ * JSON regardless of success or failure. See keychain-export.swift for the
97
+ * source of truth.
98
+ */
99
+ interface SwiftHelperResult {
100
+ ok: boolean;
101
+ p12Path?: string;
102
+ p12SizeBytes?: number;
103
+ identityName?: string;
104
+ errorCode?: 'INVALID_ARGS' | 'NO_IDENTITY' | 'USER_DENIED' | 'EXPORT_FAILED' | 'WRITE_FAILED' | 'INTERNAL';
105
+ message?: string;
106
+ osStatus?: number;
107
+ }
108
+ /**
109
+ * Returns true if the Swift helper is already cached at the version-keyed
110
+ * tmp path. Lets the UI decide whether to show a "compiling…" step or skip
111
+ * straight to the export step (the cached case is effectively instant).
112
+ *
113
+ * Sync + cheap (single existsSync). Safe to call from a React onChange
114
+ * handler.
115
+ */
116
+ export declare function isHelperCached(): boolean;
117
+ /**
118
+ * Pre-compile the Swift helper without doing anything else. Used by the UI
119
+ * to show an explicit "compiling helper" step before the export, so the user
120
+ * isn't left staring at a spinner that says "look for the macOS dialog"
121
+ * while we silently build a binary.
122
+ *
123
+ * Returns the path to the compiled binary (same as `ensureSwiftHelper`).
124
+ */
125
+ export declare function precompileSwiftHelper(): Promise<string>;
126
+ export interface ExportP12Options {
127
+ /**
128
+ * Pre-resolved Swift helper binary path. Used in tests to inject a fake
129
+ * binary; in production this is computed automatically.
130
+ */
131
+ helperPathOverride?: string;
132
+ }
133
+ /**
134
+ * Export the chosen identity from the user's Keychain as a base64'd PKCS#12.
135
+ *
136
+ * Triggers exactly TWO macOS Keychain prompts on the user's first run for
137
+ * a given identity (one for "access" ACL, one for "export" ACL). Both
138
+ * decisions are cached when the user clicks "Always Allow", so subsequent
139
+ * runs against the same identity from the same binary are silent.
140
+ *
141
+ * Internally calls the bundled Swift helper (compiled on first use to the
142
+ * OS temp folder via `swiftc`). The helper uses Security framework's
143
+ * `SecItemExport(.formatPKCS12)` — the only Apple-supported path that works
144
+ * on Xcode-imported (non-extractable) signing keys.
145
+ *
146
+ * @param targetSha1 SHA1 of the identity to export (from {@link listSigningIdentities})
147
+ * @param options See {@link ExportP12Options}
148
+ */
149
+ export declare function exportP12FromKeychain(targetSha1: string, options?: ExportP12Options): Promise<ExportedP12>;
150
+ /**
151
+ * Parse the helper's JSON output. Tolerates: extra whitespace, trailing
152
+ * newline, BOM. Throws a clear error if the output is unparsable — that
153
+ * indicates the helper crashed without emitting JSON, which our Swift code
154
+ * tries hard to never do (see keychain-export.swift's top-level catch).
155
+ *
156
+ * Exported for tests.
157
+ */
158
+ export declare function parseHelperJson(stdout: string, stderr: string, exitCode: number | null): SwiftHelperResult;
159
+ export {};
@@ -15,5 +15,9 @@ export declare function deleteProgress(appId: string, baseDir?: string): Promise
15
15
  /**
16
16
  * Determine the first incomplete step based on saved progress.
17
17
  * Returns the step to resume from.
18
+ *
19
+ * Branches on `setupMethod` so the import flow doesn't accidentally resume
20
+ * into the create-new path's `creating-certificate` step (which would trigger
21
+ * the Apple 3-cert-limit error for users at the limit).
18
22
  */
19
23
  export declare function getResumeStep(progress: OnboardingProgress | null): OnboardingStep;