| 1 | # smbcloud-ascapi — upload + signing-identity hardening: plan (closed) |
| 2 | |
| 3 | Findings from the **Siti AI** release session (macOS App Store upload of |
| 4 | `Siti AI.pkg` through App Store Connect). Repo: |
| 5 | `github.com:smbcloudXYZ/smbcloud-ascapi`, base branch `development`, remote |
| 6 | `setoelkahfi` (there is no `origin`, so use `git`/`gh`, not the sigit MCP |
| 7 | tools). |
| 8 | |
| 9 | Companion plan: [`feature-plan-from-klepon-xcode-release-session.md`](./feature-plan-from-klepon-xcode-release-session.md) |
| 10 | — that session shipped the builds API + token-source + altool-upload stack |
| 11 | (gh stack #6→#7→#8→#10). Both have now closed out. |
| 12 | |
| 13 | **Closed-out progress (2025-10-11, this session):** gh stack #27 |
| 14 | (bottom→top) #24 `docs/list-builds-pagination` → #25 `feat/signing-doctor` |
| 15 | → #26 `feat/apps-await-processing`, rebased onto `development`, all |
| 16 | CI-clean (fmt / clippy `-D warnings` / test / `RUSTDOCFLAGS=-D warnings` |
| 17 | doc / lower-crate solo builds), signed+verified, auto-merge enabled. |
| 18 | This landed finding **#2** (signing doctor), finding **#5** |
| 19 | (await-processing), and finding **#6** (list_builds pagination doc). |
| 20 | |
| 21 | **Closed-out progress (later):** the `upload.rs` cluster |
| 22 | (findings #1, #3, #4 — pre-flight `--validate-app`, body-aware |
| 23 | `success`, `.app`-vs-`.pkg` distinction) shipped together on the same |
| 24 | file, sharing the test scaffolding that already covered `altool_args`. |
| 25 | Every finding from this plan is now in `development`. |
| 26 | |
| 27 | - Both sessions hit the same root pain: a **stale private key / signing |
| 28 | identity with the right id but wrong contents**, which surfaces as an |
| 29 | indistinguishable 401 (klepon plan, "Separate / unaddressed bug") or, in |
| 30 | this session, ~20 min lost to a stale `SIGNING_IDENTITY` in |
| 31 | `.env.appstore`. `ascapi apps upload` already dodges the *key* half by |
| 32 | passing `--p8-file-path` (klepon #10); the *local signing identity* half |
| 33 | (finding #2 below) is now covered by `asc signing doctor`. |
| 34 | - Klepon's open "Live smoke test against ASC" item is the natural place to |
| 35 | also exercise findings #1 and #3 once implemented. **Still blocked on |
| 36 | credentials.** |
| 37 | |
| 38 | ## Session context |
| 39 | |
| 40 | - Uploaded a macOS build (`Siti AI.pkg`) to ASC successfully. The time sink |
| 41 | was a stale `SIGNING_IDENTITY` in `.env.appstore` — nothing in ascapi |
| 42 | inspects the local keychain, so the tool gave no signal it was stale. |
| 43 | - `ascapi` *does* own upload + TestFlight/review orchestration (`asc apps |
| 44 | upload`, `xcrun altool`). It cannot build or sign — out of scope by |
| 45 | design. The findings below stay inside what ascapi already owns. |
| 46 | |
| 47 | ## Findings |
| 48 | |
| 49 | ### 1. No pre-upload `altool --validate-app` pass — HIGH ✅ SHIPPED |
| 50 | `frontend::upload::validate_app` (added in |
| 51 | `crates/frontend/src/upload.rs`) is now a *server-side* check: it runs |
| 52 | `xcrun altool --validate-app` with the same `--p8-file-path` auth parity |
| 53 | as the upload path, surfacing Apple's JSON verdict. Reachable from the CLI |
| 54 | as `asc apps validate <pkg>`. `--wait` is deliberately not passed on the |
| 55 | validation path — validation is already synchronous. |
| 56 | - **Value:** turns a ~minutes-late failure into a seconds-early one; catches |
| 57 | the common "wrong version / bad entitlement" class before upload. |
| 58 | |
| 59 | ### 2. No local-keychain `signing doctor` — HIGH ✅ SHIPPED (PR #25) |
| 60 | The `signing` crate (`certificate.rs`, `profile.rs`, `csr.rs`) talks **only |
| 61 | to the ASC REST API**. `security find-identity` is called nowhere in the |
| 62 | repo; there is no local-keychain code at all. So the exact stale-identity |
| 63 | problem this session hit (and the klepon 401 key-drift bug) has **zero |
| 64 | coverage**: ascapi can tell you what certs/profiles *Apple* has, but not |
| 65 | what's installed and valid locally, nor whether they line up. |
| 66 | - **Fix:** add `asc signing doctor` that runs `security find-identity -v |
| 67 | -p codesigning`, lists local valid signing identities, and cross-checks |
| 68 | them against the certificates the ASC API reports |
| 69 | (`CertificateApi::list_certificates`) — flagging identities that are |
| 70 | expired, missing locally, or present locally but revoked/absent on Apple. |
| 71 | Optionally resolve the `.p8` the CLI would use and confirm it exists and |
| 72 | is readable. |
| 73 | - **Value:** directly closes the gap that cost ~20 min this session and is |
| 74 | the local half of klepon's key-drift bug. Bigger surface (new keychain |
| 75 | interaction, macOS-only) → its own branch. |
| 76 | |
| 77 | ### 3. `success` trusts the exit code, not the JSON body — HIGH ✅ SHIPPED |
| 78 | `outcome()` (`crates/frontend/src/upload.rs`) now derives `success` from |
| 79 | exit code **and** the parsed body — a non-empty `product-errors` / |
| 80 | `product_errors` / `errors` array under any of altool's key spellings |
| 81 | counts as failure even on exit 0. Exit code stays in `UploadOutcome` for |
| 82 | debugging. |
| 83 | - **Value:** small, same-file change; removes false "success" reports. |
| 84 | |
| 85 | ### 4. `.app`-vs-`.pkg` gives a generic "does not exist" — MEDIUM ✅ SHIPPED |
| 86 | In `validate_package`, a `.app` **bundle** (a directory) now hits a |
| 87 | precise message: *"this is an .app bundle, not an uploadable package; |
| 88 | export the archive first (`xcodebuild -exportArchive`) to get an |
| 89 | .ipa/.pkg"*. A bare directory still reports "is a directory, not a |
| 90 | package file" so "path missing" and "wrong kind" stay distinct. |
| 91 | - **Value:** tiny, same-file, removes a recurring confusing error. |
| 92 | |
| 93 | ### 5. No non-blocking `await-processing` loop — MEDIUM ✅ SHIPPED (PR #26) |
| 94 | The only way to wait for ASC to finish processing a build is altool's |
| 95 | `--wait`, which blocks the altool process for the whole duration. There's |
| 96 | no poll-based path that uses the builds API (`BuildsApi::list_builds`, |
| 97 | shipped in klepon #8) to report processing state incrementally or let the |
| 98 | caller poll on its own cadence. |
| 99 | - **Fix:** add `asc apps await-processing <build-id|--build-number>` that |
| 100 | polls `/v1/builds` for `processingState` until `VALID`/`FAILED`/timeout, |
| 101 | with backoff. Complements klepon's builds API; an MCP-friendly |
| 102 | alternative to a blocking `--wait`. |
| 103 | - **Value:** better UX for CI/agents; builds on existing klepon surface. |
| 104 | - **Shipped (PR #26):** `asc apps await-processing <APP_ID>` with |
| 105 | `--version`, `--pre-release-version`, `--interval` (30s), |
| 106 | `--timeout` (1800s). Shared poll loop + pure `classify()` / |
| 107 | `AwaitConfig` / `AwaitOutcome` in `crates/frontend/src/await_processing.rs` |
| 108 | (serializes without key material; 7 in-module tests). Terminal map: |
| 109 | VALID → success, INVALID/FAILED → failure, else timeout → non-zero exit. |
| 110 | CLI-only for now; the pure core is MCP-ready if a tool is added later. |
| 111 | |
| 112 | ### 6. `list_builds` limit=200 pagination — LOW (doc-only) ✅ SHIPPED (PR #24) |
| 113 | `list_builds` caps at limit=200 with no cursor follow-through. Fine for |
| 114 | normal apps; note it as a known limit. Overlaps klepon's open "MCP |
| 115 | `list_builds` doesn't expose `BuildFilter`" item — fold into that rather |
| 116 | than tracking separately. |
| 117 | |
| 118 | ## Status |
| 119 | Closed. All six findings merged. The only remaining item is the live ASC |
| 120 | smoke test, shared with the klepon plan and credential-blocked. |