cli-helper/README.md
Rama main · réplica de lectura del repositorio original.
# Capgo CLI keychain helper
Small Swift program (Security framework only), shipped inside a hidden macOS
app bundle, **`CapgoKeychainHelper.app`**. The single binary uses subcommand dispatch — today
just `keychain-export`:
CapgoKeychainHelper.app/Contents/MacOS/capgo keychain-export \
--sha1 <40-hex> --output <path.p12> --passphrase <wrap-pass> --invoked-by capgo-cli
It exports one code-signing identity from the macOS Keychain as a
passphrase-wrapped PKCS#12 and always emits one line of JSON on stdout
(`{"ok":true,...}` or `{"ok":false,"errorCode":...}`). Future helpers are new
subcommands of the same signed binary.
## Why a bundle (not a bare binary)
`CapgoKeychainHelper.app` is an **`LSUIElement` agent** — no Dock icon, no Cmd-Tab entry, no
window; it runs headlessly and exits. The bundle gives two things a bare binary
can't:
- **Branded Keychain prompts.** Because the export runs from inside a signed
`CapgoKeychainHelper.app`, the macOS Keychain "Allow / Always Allow" prompts show the
**Capgo name + icon** instead of a generic process name. (This requires the
bundle to be signed — see the dev note below.)
- **Stable ACL identity.** `CFBundleIdentifier = app.capgo.cli.helper` keys the
Keychain "Always Allow" grant, and it never changes across releases, so the
grant persists across CLI upgrades. The CLI also verifies the bundle's
Developer ID + Capgo Team ID code signature before running it.
The CLI execs `CapgoKeychainHelper.app/Contents/MacOS/capgo` **directly** (never `open
CapgoKeychainHelper.app`), so there is no Dock flash and no Gatekeeper "downloaded from the
internet" prompt (npm doesn't set the quarantine xattr; direct exec isn't a
LaunchServices launch).
Shipped as two precompiled, Developer-ID-signed, notarized, **stapled** npm
packages:
- `@capgo/cli-helper-darwin-arm64` (Apple Silicon, macOS 11+)
- `@capgo/cli-helper-darwin-x64` (Intel, macOS 10.15+)
Both are `optionalDependencies` of `@capgo/cli`; npm installs at most one. Each
ships TWO signed bundles: `CapgoKeychainHelper.app` (the keychain helper
described here, per-arch) and `CapgoAscKeyHelper.app` (the App Store Connect API
key helper, a universal bundle — see `cli/native/asc-key-helper`). See
SECURITY.md for the threat model.
## Dev bootstrap (working on the Swift source)
The published CLI has no compile fallback. To test local Swift changes quickly:
swiftc cli-helper/src/helper.swift -framework Security -O -o /tmp/helper-dev
cd cli && NODE_ENV=development bun run build
CAPGO_KEYCHAIN_HELPER_PATH=/tmp/helper-dev node dist/index.js ...
`CAPGO_KEYCHAIN_HELPER_PATH` only exists in dev builds — it is dead-code-
eliminated from npm release builds (asserted in CI). The env-override path skips
both the signature check and the bundle, so point it at a binary you built and
trust. Note: a bare dev binary is **not** signed, so the Keychain prompt shows
the process name, not "Capgo" — to see the branded prompt, build + sign the
bundle (`bash cli-helper/scripts/build.sh` then `codesign … CapgoKeychainHelper.app`).
## Release
The workflow has two triggers:
- **Normal release** — the deliberate button: GitHub Actions UI → "Run
workflow" → enter the version, or `gh workflow run publish_cli_helper.yml -f
version=X.Y.Z`. (`workflow_dispatch` requires the workflow to be on the
default branch.)
- **Test from a branch / manual release** — push a `cli-helper-X.Y.Z` tag.
The workflow runs the version of itself *at that tagged commit*, so you can
validate the whole pipeline from a PR branch without merging:
git tag cli-helper-1.0.0-rc.1 # tags your current HEAD
git push origin cli-helper-1.0.0-rc.1
(Use a `-rc.N` prerelease for tests — npm versions are immutable. The Apple
secrets below must exist for the signing steps to pass.)
Either way it builds, signs, notarizes, staples, smoke-tests, publishes both
packages with npm provenance, and creates the `cli-helper-X.Y.Z` tag + GitHub
release. Release only when `src/helper.swift` actually changed.
**npm dist-tag:** while the packages are new/unproven they publish under the
**`rc`** dist-tag, not `latest`, so they never become the default bare install.
This doesn't affect `@capgo/cli`, whose `optionalDependencies` (`^1.0.0`)
resolve against published *versions*, not dist-tags. Promote to stable later by
dropping `--tag rc` in the workflow or `npm dist-tag add <pkg>@<version> latest`.
Required GitHub secrets: `DEVELOPER_ID_CERT_BASE64`, `DEVELOPER_ID_CERT_PASSWORD`
(Developer ID Application cert as base64 .p12), `APPLE_TEAM_ID`, plus existing
`APPLE_KEY_ID`, `APPLE_ISSUER_ID`, `APPLE_KEY_CONTENT` (App Store Connect API
key, used by notarytool) and `NPM_TOKEN`.