# App Store Connect Onboarding (setup Step 3b)

<!-- toc -->
- [API key](#api-key)
- [Apple ID + app-specific password](#apple-id-app-specific-password)
- [Multi-provider accounts](#multi-provider-accounts)
- [Verify + expiry](#verify-expiry)
<!-- /toc -->

Loaded on demand by `/multi-agent:setup` Step 3b (iOS only, optional). The SKILL.md carries the step intro; this file is the full flow.

Runs inside Step 3 alongside the other missing credentials, not as a late add-on:
a user who already has an App Store Connect credential in their keychain gets it
mapped by Step 1 discovery like any other token, and only the genuinely missing
pieces reach this flow.

None of the five entries goes through the clipboard Token Save Flow: four hold
something that is not a pasteable secret, and the one secret arrives as a file.

| Entry | Holds | Flow |
|---|---|---|
| `appstore_connect_key_id` | an identifier | plain value, not a secret; still mapped so it is read through the mapping layer |
| `appstore_connect_issuer_id` | an identifier | same |
| `appstore_connect_private_key` | the full PEM text of `AuthKey_<keyId>.p8` | probed from the file, then stored from the file through stdin; never pasted, never on argv |
| `appstore_connect_apple_id` | an email address | plain value, not a secret |
| `appstore_connect_password_item` | a keychain ITEM NAME | the password lives in Apple's own keychain item, referenced as `-p @keychain:<item>` and never read by the pipeline |

All five entries are **iOS-only and optional**: skip them all and the pipeline
still works, it just reports Gate 2 of `/multi-agent:store-ready` as `SKIPPED`,
never as a pass.

The tiers mirror the Figma 3-tier shape: Tier 1 = API key (`appstore_connect_key_id`
+ `appstore_connect_issuer_id` + `appstore_connect_private_key`), Tier 2 = Apple ID
+ app-specific password (`appstore_connect_apple_id` + `appstore_connect_password_item`),
Tier 3 = nothing configured.

Ask which tier to configure (picker): **API key** / **Apple ID + app-specific
password** / **Skip**. Lead with the second when the user says they cannot create
an API key: creating one needs an Admin or App Manager role in App Store Connect,
while an app-specific password is generated by the account holder at
`appleid.apple.com` with no team permission at all.

## API key

Every part of the key lives in the Keychain, the `.p8` included. The toolkit's
`store_asc_*` tools and `ios_testflight_validate` read all three items through the
store reference file (`~/.config/multi-agent-toolkit/store.json`). Called with
neither `api_key_id` nor `apple_id`, `ios_testflight_validate` uses that same key
and hands `altool` a private temporary copy that exists only for the length of the
call, so no copy under `~/.appstoreconnect/private_keys/` is needed.

Map before storing. The three entries in `prefs.global.keychainMapping`
(`appstore_connect_key_id`, `appstore_connect_issuer_id`,
`appstore_connect_private_key`) point at their standard names from
`refs/keychain.md`, or keep an existing mapping when re-onboarding, as Token Save
Flow Step D does for any key. `set` resolves a logical key through the mapping,
so every write below goes through the logical key and lands in the item the
mapping names; an unmapped key would be stored under the bare logical name.

1. **Key id and issuer id.** App Store Connect -> Users and Access -> Integrations
   -> App Store Connect API. Both are identifiers; store them as plain values:

   ```bash
   ~/.claude/lib/credential-store.sh set appstore_connect_key_id "<key id>"
   ~/.claude/lib/credential-store.sh set appstore_connect_issuer_id "<issuer id>"
   ```

2. **Locate the `.p8`.** Ask for the path to `AuthKey_<keyId>.p8` as a path, never
   its contents. Apple lets the file be downloaded once, so it is usually in
   `~/Downloads` or `~/.appstoreconnect/private_keys/`:

   ```bash
   ls ~/Downloads/AuthKey_*.p8 ~/.appstoreconnect/private_keys/AuthKey_*.p8 2>/dev/null
   ```

3. **Validate before saving.** A key that is saved first and tried later fails at
   the first store call, long after setup. Probe it against the file:

   ```bash
   node "$HOME/.claude/scripts/probe-store-key.mjs" --asc \
     --key-id "<key id>" --issuer-id "<issuer id>" --p8 "<path to AuthKey_<keyId>.p8>"
   ```

   It signs a 10-minute ES256 token, reads `GET /v1/apps?limit=1` and prints one
   line, `{ok, store, status, hint}`, with no key material in it. Exit 0 = accepted.
   On exit 1 show the `hint` and stop this tier: `401` means the key id or issuer
   id does not belong to this `.p8`, the key was revoked, or the clock is off;
   `403` means the key's role cannot read apps. Exit 2 is a usage problem (wrong
   path, not a private key). Nothing is saved on a failed probe.

4. **Store the key from the file.** A file does not go through the clipboard;
   stdin keeps it off argv and out of shell history:

   ```bash
   ~/.claude/lib/credential-store.sh set appstore_connect_private_key - < "<path to AuthKey_<keyId>.p8>"
   ```

5. **Write the store reference file.**

   ```bash
   node "$HOME/.claude/scripts/write-store-config.mjs"
   ```

   It names the three Keychain items in `~/.config/multi-agent-toolkit/store.json`
   (0600, directory 0700), after checking each item exists without reading it.
   `store_status` in the toolkit then reports App Store Connect as configured from
   the Keychain.

6. **Offer to delete the `.p8` from disk** (picker, default keep). The key is in
   the Keychain now, and a plain file in `~/Downloads` is the copy most likely to
   leak. Apple does not let it be downloaded again, so the user decides:

   - `question` / `description` in `outputLanguage`, for example "The key is now in
     the Keychain. Delete the .p8 file from disk?"
   - `header`: `P8 file`
   - options: `{label: "Keep file", description: "<outputLanguage: leave the file where it is>"}`
     (first, default) / `{label: "Delete file", description: "<outputLanguage: remove it; the Keychain copy is the only one left>"}`

   On **Delete file**, `rm "<path>"` that one file and nothing else. On **Keep
   file**, say where it is.

## Apple ID + app-specific password

Use Apple's own keychain helper. The secret never enters chat and never becomes a
shell argument, per the Token Save Flow rule:

```bash
# the user exports AC_PASSWORD_ONCE in their own shell, for this one command
xcrun altool --store-password-in-keychain-item "<item-name>" \
  -u "<apple-id>" -p @env:AC_PASSWORD_ONCE
```

Then map only `<item-name>` as `appstore_connect_password_item`.

## Multi-provider accounts

A corporate Apple ID often belongs to several
providers, and `altool` fails opaquely without one. Resolve it once with
`ios_testflight_validate({list_providers: true, <credentials just configured>})`
and store the answer under
`prefs.projects[<key>].appStoreConnect.providerPublicId`  -  per-project, since a
user can ship for more than one team.

## Verify + expiry

For the API key, `store_status({probe: true})` makes one read-only call and says
whether it worked; for either tier, re-run the `list_providers` probe and report
the resolved tier. A credential that resolves but is rejected (401/403) follows the
Expired-token decision in `refs/keychain.md` Rule 1  -  Regenerate / Use a
different token / Skip and continue  -  never a silent drop. Regenerating the API
key repeats steps 2 to 5 with the new file.
