@capgo/cli
Version:
A CLI to upload to capgo servers
268 lines (267 loc) • 12.8 kB
TypeScript
import type { MobileprovisionDetail } from '../mobileprovision-parser.js';
/** Standard locations Xcode writes provisioning profiles into. */
export declare const PROVISIONING_PROFILE_DIRS: readonly ["Library/Developer/Xcode/UserData/Provisioning Profiles", "Library/MobileDevice/Provisioning Profiles"];
export type IdentityType = 'distribution' | 'development' | 'unknown';
export interface SigningIdentity {
/** SHA1 hash of the certificate, lowercase 40-char hex */
sha1: string;
/** Full identity string from `security find-identity` (e.g. "Apple Distribution: Acme Corp (XYZ123ABCD)") */
name: string;
/** Best-effort classification from the name prefix */
type: IdentityType;
/** Human-readable team name extracted from the identity string */
teamName: string;
/** Apple Team ID (10-char alphanumeric) extracted from the identity string */
teamId: string;
}
export interface DiscoveredProfile extends MobileprovisionDetail {
/** Absolute path to the .mobileprovision file */
path: string;
}
export interface IdentityProfileMatch {
identity: SigningIdentity;
/** Profiles whose embedded developer certs include this identity's SHA1 */
profiles: DiscoveredProfile[];
}
export interface ExportedP12 {
/** Base64-encoded PKCS#12 blob containing the chosen identity's cert + private key */
base64: string;
/** Auto-generated passphrase used to wrap the export */
passphrase: string;
}
export declare class MacOSSigningError extends Error {
readonly cause?: unknown | undefined;
constructor(message: string, cause?: unknown | undefined);
}
export declare class NotMacOSError extends MacOSSigningError {
constructor();
}
/** Returns `true` when running on macOS (Darwin). */
export declare function isMacOS(): boolean;
/**
* Run a subprocess and capture stdout/stderr/exit-code.
*
* Public so tests can inject a fake runner via the optional argument on
* higher-level functions. Not intended for downstream callers.
*/
export interface SecurityRunResult {
stdout: string;
stderr: string;
code: number | null;
}
export type SecurityRunner = (args: readonly string[]) => Promise<SecurityRunResult>;
/**
* Parse the human-readable output of `security find-identity -v -p codesigning`.
* Each line looks like:
* ` 1) <SHA1> "Apple Distribution: Acme Corp (XYZ123ABCD)"`
*
* Exported so unit tests can verify parsing without spawning a subprocess.
*/
export declare function parseFindIdentityOutput(stdout: string): SigningIdentity[];
/**
* List all code-signing identities visible in the user's default Keychain.
* Read-only — does NOT trigger any Keychain access prompt.
*
* @param runner Optional injection point for testing. Pass a fake to avoid
* spawning the real `/usr/bin/security` binary.
*/
export declare function listSigningIdentities(runner?: SecurityRunner): Promise<SigningIdentity[]>;
/**
* Scan all standard Xcode provisioning-profile directories under the user's
* home and return parsed metadata for every readable `.mobileprovision`.
*
* Read-only — pure filesystem reads, no Keychain interaction.
*
* Files that fail to parse are silently skipped (a teammate's malformed
* profile shouldn't break the whole listing).
*
* @param homeDirOverride Optional override for HOME, used in tests.
*/
export declare function scanProvisioningProfiles(homeDirOverride?: string): Promise<DiscoveredProfile[]>;
/**
* Given a list of identities and profiles, return one match entry per
* identity, populated with profiles whose embedded developer certs include
* that identity's SHA1.
*
* Pure function — no I/O.
*/
export declare function matchIdentitiesToProfiles(identities: readonly SigningIdentity[], profiles: readonly DiscoveredProfile[]): IdentityProfileMatch[];
/**
* Compare a provisioning profile's bundle id against the app's concrete bundle
* id, honoring Apple's wildcard syntax. The mobileprovision parser leaves the
* asterisk in place after stripping the team-id prefix, so a wildcard profile
* arrives here as either the bare `*` (matches everything the team owns) or a
* suffix wildcard like `com.example.*` (matches `com.example.<anything>`).
*
* Exported so the file-picker validation in the Ink UI can reuse the same
* matching rule as `filterProfilesForApp` — otherwise a wildcard
* `.mobileprovision` picked manually would be hard-rejected even though the
* underlying profile is valid for the current app.
*/
export declare function bundleIdMatches(profileBundleId: string, appId: string): boolean;
/**
* Filter profiles that are actually usable for a given Capacitor app + iOS
* distribution mode. Used by the import-existing flow to detect dead-end
* situations where an identity has profiles for a different app or the wrong
* distribution mode — in which case the no-match-recovery menu can offer
* "fetch / create via Apple" instead of dropping the user at an empty picker.
*
* `importDistribution` is null/undefined when the user hasn't picked yet —
* in that case any profileType is accepted.
*
* Bundle-id comparison goes through {@link bundleIdMatches} so wildcard
* profiles (the norm for ad_hoc/enterprise teams that share one profile
* across many apps) are accepted alongside literal-equality matches. Apple
* never issues wildcard `app_store` profiles in practice, so when the caller
* pins `importDistribution = 'app_store'` the conjunction naturally drops
* any ad_hoc/enterprise wildcards that happen to be installed.
*/
export declare function filterProfilesForApp(profiles: readonly DiscoveredProfile[], appId: string, importDistribution: 'app_store' | 'ad_hoc' | null | undefined): DiscoveredProfile[];
/**
* Generate a cryptographically random passphrase suitable for wrapping the
* exported PKCS#12. 32 bytes of entropy → 64-char hex string.
*/
export declare function generateP12Passphrase(): string;
/**
* Bundle identifier of the App Store Connect key helper's CapgoAscKeyHelper.app.
* Pinned in its designated requirement exactly like the keychain helper above —
* same team, same Developer ID, different bundle id. Must match
* cli/scripts/package-asc-key-helper-app.sh and publish_cli_helper.yml's sign step.
*/
export declare const ASC_KEY_HELPER_BUNDLE_IDENTIFIER = "app.capgo.asc-key-helper";
/**
* Map a Node `process.arch` value to the matching helper package name, or
* null when no precompiled helper exists for that architecture.
*/
export declare function helperPackageName(arch: string): string | null;
/**
* codesign designated requirement asserting: the exact helper bundle identifier
* (defaults to the keychain helper's app.capgo.cli.helper), an Apple-rooted
* chain, a Developer ID Application leaf cert (OID 1.2.840.113635.100.6.1.13),
* and the given Apple Team ID as the signing team. The identifier clause is what
* scopes the requirement to THIS binary — without it, any other binary signed
* with Capgo's Developer ID cert (a future tool, a leaked artifact) would also
* satisfy the check. Pass `bundleIdentifier` to scope it to a different Capgo
* helper (e.g. the ASC key helper) signed with the same Developer ID + team.
*/
export declare function helperSignatureRequirement(teamId?: string, bundleIdentifier?: string): string;
export interface CodesignRunner {
(args: readonly string[]): Promise<SpawnResult>;
}
export interface ResolveHelperBinaryOptions {
/** Override `process.arch` (tests). */
arch?: string;
/**
* Override module resolution (tests). Each resolver receives the package's
* `package.json` specifier and must return its absolute path or throw. Pass an
* ARRAY to test the fallback chain (each base is tried in order until one
* resolves); a single function is treated as a one-element chain.
*/
resolve?: ((specifier: string) => string) | Array<(specifier: string) => string>;
/** Override the codesign spawn (tests). */
codesignRunner?: CodesignRunner;
/** Force the dev env-override gate (tests). Defaults to the build-time flag. */
allowEnvOverride?: boolean;
/**
* Project directory to ALSO resolve the helper package from, in addition to the
* CLI's own node_modules. Lets a project-local `npm i @capgo/cli-helper-darwin-*`
* be picked up even when the CLI runs from a global install or the MCP server
* (which doesn't resolve from the user's project). Defaults to `process.cwd()`.
* Ignored when `resolve` is provided (tests).
*/
cwd?: string;
}
/**
* Locate the precompiled `helper` binary for this machine and verify its code
* signature chains to Capgo's Developer ID before returning it.
*
* Resolution order:
* 1. CAPGO_KEYCHAIN_HELPER_PATH (dev builds only — see the build-time flag)
* 2. The arch-matching @capgo/cli-helper-darwin-* optional dependency
* 3. Hard error with install guidance. There is no compile fallback.
*/
export declare function resolveHelperBinary(options?: ResolveHelperBinaryOptions): Promise<string>;
export interface VerifyAppBundleSignatureOptions {
/**
* Bundle identifier to pin in the designated requirement (e.g.
* app.capgo.cli.helper for the keychain helper, app.capgo.asc-key-helper for
* the ASC key helper). Both are signed with the same Developer ID + team.
*/
bundleIdentifier: string;
/** Human-readable name for the helper, used in the thrown error message. */
label?: string;
/** Extra guidance appended to the error message (e.g. a reinstall hint). */
reinstallHint?: string;
/** Override the codesign spawn (tests). */
codesignRunner?: CodesignRunner;
}
/**
* Verify an app bundle's (or binary's) code signature against Capgo's designated
* requirement (Apple-rooted chain + Developer ID Application leaf + Capgo Team ID
* + the given bundle identifier). macOS validates the certificate chain and the
* seal, so this also detects post-install tampering. Throws — never returns —
* on any failure, so callers can verify-then-spawn safely.
*
* Shared by the keychain helper and the App Store Connect key helper: both ship
* inside the same npm package, signed with the same Developer ID, distinguished
* only by their bundle identifiers.
*/
export declare function verifyAppBundleSignature(bundlePath: string, options: VerifyAppBundleSignatureOptions): Promise<void>;
/**
* Output shape from the Swift helper's stdout — always emitted as one line of
* JSON regardless of success or failure. See cli-helper/src/helper.swift for
* the source of truth.
*/
interface SwiftHelperResult {
ok: boolean;
p12Path?: string;
p12SizeBytes?: number;
identityName?: string;
errorCode?: 'INVALID_ARGS' | 'NO_IDENTITY' | 'USER_DENIED' | 'EXPORT_FAILED' | 'WRITE_FAILED' | 'FORBIDDEN_CALLER' | 'INTERNAL';
message?: string;
osStatus?: number;
}
/**
* Spawn an arbitrary command, capturing stdout/stderr/exit-code. Used for
* `/usr/bin/codesign` and the precompiled Swift helper itself.
*/
interface SpawnResult {
stdout: string;
stderr: string;
code: number | null;
}
export interface ExportP12Options {
/**
* Pre-resolved helper binary path. Used in tests to inject a fake binary;
* in production this is computed automatically. Bypasses the signature
* check — not reachable from user input.
*/
helperPathOverride?: string;
/** Injection points for {@link resolveHelperBinary} (tests). */
resolveOptions?: ResolveHelperBinaryOptions;
}
/**
* Export the chosen identity from the user's Keychain as a base64'd PKCS#12.
*
* Triggers exactly TWO macOS Keychain prompts on the user's first run for
* a given identity (one for "access" ACL, one for "export" ACL). Both
* decisions are cached when the user clicks "Always Allow", so subsequent
* runs against the same identity from the same binary are silent.
*
* Internally runs the precompiled, signature-verified `helper keychain-export`
* subcommand from the arch-matching `@capgo/cli-helper-darwin-*` package.
*
* @param targetSha1 SHA1 of the identity to export (from {@link listSigningIdentities})
* @param options See {@link ExportP12Options}
*/
export declare function exportP12FromKeychain(targetSha1: string, options?: ExportP12Options): Promise<ExportedP12>;
/**
* Parse the helper's JSON output. Tolerates: extra whitespace, trailing
* newline, BOM. Throws a clear error if the output is unparsable — that
* indicates the helper crashed without emitting JSON, which our Swift code
* tries hard to never do (see cli-helper/src/helper.swift's top-level catch).
*
* Exported for tests.
*/
export declare function parseHelperJson(stdout: string, stderr: string, exitCode: number | null): SwiftHelperResult;
export {};