Skip to content

feat: support Individual API Keys (issuer-less JWT)#4

Open
conversun wants to merge 10 commits intozelentsov-dev:developfrom
conversun:feat/individual-api-key-support
Open

feat: support Individual API Keys (issuer-less JWT)#4
conversun wants to merge 10 commits intozelentsov-dev:developfrom
conversun:feat/individual-api-key-support

Conversation

@conversun
Copy link
Copy Markdown

Summary

Adds support for App Store Connect Individual API Keys, which use a different JWT shape than Team Keys (no issuer ID, sub: "user" instead of iss). Team Key behavior is fully preserved — this is purely additive.

Per Apple's spec:

  • Team Key → JWT payload uses iss claim
  • Individual Key → JWT payload omits iss and uses sub: \"user\"

Changes

Model (Company)

  • issuerID is now String? with a computed isIndividualKey flag
  • Custom encode(to:) omits issuer_id from JSON when absent → backward compatible with existing Team Key configs

JWT (JWTService)

  • Branches payload construction by key type
  • JWTPayload custom encoder skips nil claims so the wire format matches Apple's spec exactly

Config loading

  • Single-company env vars: ASC_ISSUER_ID is now optional
  • Multi-company env vars: ASC_COMPANY_N_ISSUER_ID is now optional
  • Env loading is injectable for testability

UX

  • Company list, current/switch handlers, and startup logs display "Individual Key" vs "Team Key"
  • --test mode guards against missing config (no crash on smoke path)

Docs

  • README.md — Individual API Keys subsection with env + JSON examples and access limitations
  • CLAUDE.md — note that issuerID is optional for Individual Keys
  • companies.example.json — third example without issuer_id

Limitations (documented)

Individual API Keys cannot access:

  • Provisioning endpoints
  • Sales / Finance reports
  • notarytool endpoints

Calls to those will fail with 403 at the Apple API level — no client-side pre-validation.

Test Plan

  • `swift build` — clean
  • `swift test` — 452/452 passed across 32 suites (16 new tests added)
  • New coverage:
    • `CompanyModelTests` — decode / encode / roundtrip with optional `issuerID`
    • `CompaniesConfigModelTests` — mixed Team + Individual config decoding
    • `CompaniesManagerEnvTests` — Team / Individual / mixed env var combinations (single + multi-company)
    • `JWTServiceTests` — Team Key payload uses `iss`; Individual Key payload omits `iss` and uses `sub: "user"`; header and expiration parity verified

Backward Compatibility

  • Existing Team Key JSON configs decode and re-encode identically (no `issuer_id` change on disk)
  • Existing Team Key env vars continue to work unchanged
  • JWT wire format for Team Keys is byte-identical to before

conversun and others added 5 commits April 21, 2026 15:29
… Keys

Company now distinguishes Team and Individual API keys via an optional issuerID and a computed isIndividualKey flag, while encoding omits issuer_id when absent.

Added a dedicated individual-key fixture plus tests covering decode, encode, roundtrip, and mixed config decoding; temporary caller fallbacks keep the tree building until later JWT/config waves.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Apple requires Team Key JWTs to use iss and Individual Key JWTs to omit iss entirely and send sub="user". Team key behavior remains unchanged, and the new tests verify both payload shapes plus header and expiration parity.

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
…Keys

Support loading both single-company and multi-company App Store Connect config from environment variables without requiring issuer IDs, while preserving Team key behavior. Also make environment loading testable via dependency injection and add coverage for Team and Individual key env combinations.
Show Individual Key vs Team Key consistently in company list, switch/current handlers, startup reinitialization logs, and the integration test helper. Also guard test mode against missing configuration so the smoke path exits cleanly without crashing.
- README.md: add Individual API Keys subsection with env var and JSON config examples, plus limitations warning

- CLAUDE.md: note that issuerID is optional for Individual Keys; add Individual API Keys note in Environment Configuration

- companies.example.json: add third company example without issuer_id for Individual Key configuration
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f23e04bf6d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +19 to 21
} catch {
print("\n⚠️ Test mode exited early: \(error.localizedDescription)", to: &standardError)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Re-throw failures in debug test mode

Catching all errors here and then returning makes asc-mcp --test exit successfully even when testAppMetadata() or testCompanySwitching() fails. That regresses test-mode behavior from fail-fast to silent success, which can mask real breakages in local/CI smoke checks that rely on process exit status. Log the error if needed, but propagate it (or call exit(1)) so failed test runs are observable.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant