@capgo/cli 7.100.7 → 7.101.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.
- package/dist/index.js +445 -443
- package/dist/keychain-export.swift +351 -0
- package/dist/package.json +4 -2
- package/dist/src/api/channels.d.ts +3 -3
- package/dist/src/build/mobileprovision-parser.d.ts +27 -0
- package/dist/src/build/onboarding/apple-api.d.ts +50 -15
- package/dist/src/build/onboarding/macos-signing.d.ts +159 -0
- package/dist/src/build/onboarding/progress.d.ts +4 -0
- package/dist/src/build/onboarding/types.d.ts +23 -1
- package/dist/src/sdk.js +178 -178
- package/dist/src/types/supabase.types.d.ts +3 -3
- package/dist/src/utils.d.ts +3 -3
- package/package.json +4 -2
|
@@ -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.
|
|
4
|
+
"version": "7.101.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": {
|
|
@@ -2708,7 +2708,7 @@ export declare function delChannelDevices(supabase: SupabaseClient<Database>, ap
|
|
|
2708
2708
|
webhooks: {
|
|
2709
2709
|
Row: {
|
|
2710
2710
|
created_at: string;
|
|
2711
|
-
created_by: string
|
|
2711
|
+
created_by: string;
|
|
2712
2712
|
enabled: boolean;
|
|
2713
2713
|
events: string[];
|
|
2714
2714
|
id: string;
|
|
@@ -2720,7 +2720,7 @@ export declare function delChannelDevices(supabase: SupabaseClient<Database>, ap
|
|
|
2720
2720
|
};
|
|
2721
2721
|
Insert: {
|
|
2722
2722
|
created_at?: string;
|
|
2723
|
-
created_by
|
|
2723
|
+
created_by: string;
|
|
2724
2724
|
enabled?: boolean;
|
|
2725
2725
|
events: string[];
|
|
2726
2726
|
id?: string;
|
|
@@ -2732,7 +2732,7 @@ export declare function delChannelDevices(supabase: SupabaseClient<Database>, ap
|
|
|
2732
2732
|
};
|
|
2733
2733
|
Update: {
|
|
2734
2734
|
created_at?: string;
|
|
2735
|
-
created_by?: string
|
|
2735
|
+
created_by?: string;
|
|
2736
2736
|
enabled?: boolean;
|
|
2737
2737
|
events?: string[];
|
|
2738
2738
|
id?: string;
|
|
@@ -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
|
|
59
|
+
export interface AscProfileSummary {
|
|
18
60
|
id: string;
|
|
19
61
|
name: string;
|
|
20
|
-
|
|
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:
|
|
33
|
-
|
|
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 {};
|