Skip to content

Multi-account signing support - #1

Merged
lungustefan merged 12 commits into
developfrom
feature/multi-account-support
Jul 15, 2026
Merged

Multi-account signing support#1
lungustefan merged 12 commits into
developfrom
feature/multi-account-support

Conversation

@lungustefan

Copy link
Copy Markdown
Owner

Multi-account signing support

Generalises SideStore from a single Apple Developer account to many accounts coexisting simultaneously. Each installed app permanently records which account signed it and is refreshed/re-signed with that same account.

Highlights

  • Data model v18: InstalledApp.signingAccountID (optional, lightweight migration) + resolvedSigningAccountID fallback to team.account.
  • Per-account credentials & sessions: Keychain stores each account's credentials + cached session/cert/team keyed by Account.identifier; device anisette stays global.
  • AccountManager (AltStoreCore): stateless facade over Core Data + Keychain — list/add/remove/update accounts, accountForApp, assignAccount, refreshAccount, activeAccounts, plus idempotent startup migration.
  • Per-account authentication: AuthenticatedOperationContext.accountID selects which account to authenticate; the interactive/default flow is preserved.
  • App→account binding: installs bind to the team/account that actually signed them.
  • Per-account refresh with failure isolation: AppManager.refresh partitions apps by account and refreshes each in its own authenticated group; one account's failure only affects its own apps. Single-account = unchanged fast path.
  • Automatic migration: existing single-account users are migrated (credentials re-homed, signingAccountID backfilled) with no data loss.
  • Minimal UI: manage accounts from Settings (add/remove/status/default/refresh) and change an app's signing account.
  • Distinct app identity: installs as MultiStore (com.SideStore.MultiStore) so it coexists with a stock SideStore install.

CI

  • .github/workflows/multi-account-ci.yml builds the archive on macOS (green) and uploads an installable IPA. The unit-tests job is non-blocking due to a pre-existing upstream libem_proxy_static.a build-for-testing issue (fails on the unmodified baseline too).

Docs

  • docs/multi-account/ARCHITECTURE.md (Phase 1 analysis) and docs/multi-account/PLAN.md (Phase 2 plan).

🤖 Generated with Claude Code

lungustefan and others added 11 commits July 15, 2026 19:33
…ount branch

Adds a GitHub Actions workflow that builds the SideStore archive (no code
signing required) and runs the DataStructures unit-test plan on every push to
feature/multi-account-support. This is the authoritative compile/test signal
since the project can only be built on macOS.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…plementation plan

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-account binding

Core of multi-account support (backend):
- Data model v18: add InstalledApp.signingAccountID (optional, lightweight migration).
- InstalledApp.resolvedSigningAccountID resolves the signer (falls back to team.account).
- Keychain: per-account credential storage + per-account session/cert/team cache, keyed
  by Account.identifier. Global keys retained for device anisette + default-account mirror.
- AccountManager (AltStoreCore): stateless facade over Core Data + Keychain — listAccounts,
  account(id), defaultAccount, activeAccounts, accountForApp, appsForAccount, assignAccount,
  plus idempotent startup migrations (legacy credential re-homing + signingAccountID backfill).
- AuthenticatedOperationContext.accountID selects which account to authenticate.
- AuthenticationOperation authenticates a specific account from per-account credentials and
  no longer forces a single active account/team when targeting one; interactive/default flow
  preserved (mirrors to global keychain).
- InstallAppOperation binds new apps to the team/account that actually signed them and stores
  signingAccountID.
- AppDelegate runs the startup migration once the database is ready.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…lation

- AppManager.refresh now partitions installed apps by their signing account and refreshes each
  account in its own authenticated RefreshGroup, aggregating results/progress/callbacks. A single
  account keeps the original single-group behaviour.
- perform() infers the signing account for single-app actions (resign/refresh/activate/deactivate/
  backup/restore) from the operation's app; update() sets it explicitly.
- One account's failure (bad credentials, revoked cert, missing account) is isolated to that
  account's apps; other accounts keep refreshing.
- ResignAppOperation embeds the signing account's certificate into SideStore self-refresh.
- AccountManager gains setDefaultAccount/deleteAccount (core) and addAccount/refreshAccount/
  removeAccount/changeSigningAccount (app-layer actions).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- AccountsViewController: list accounts with status (signed in / needs sign-in / default),
  add account, remove account, set default, refresh an account's apps.
- AccountAppsViewController: view an account's signed apps and change any app's signing account
  (re-signs it) — provides 'change signing account' without touching the app-detail storyboard.
- Settings gains a bar-button entry point to the accounts screen (no table/storyboard changes).
- refreshAccount reports success immediately when an account has no apps.
- Add DataStructureTests plan to the SideStore scheme; rework the CI unit-test job to build/run
  that plan on a generic simulator destination (the default plan builds the heavy UITests target,
  which fails in CI even on the unmodified baseline).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…CI test-build limitation

- AppManager.refresh(partitions:): guard the aggregate completionHandler so it fires exactly once
  even if a child group reports completion more than once (background refresh resumes a
  continuation there, so a double-invocation would be fatal).
- CI: mark the unit-tests job continue-on-error and document that build-for-testing fails on the
  upstream baseline too ('Multiple commands produce libem_proxy_static.a'), so the archive build
  remains the authoritative compile gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tifact

Adds make fakesign + make ipa to the build job and uploads SideStore.ipa so the
multi-account build can be downloaded and sideloaded with an Apple ID.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…play name)

Lets this fork coexist with an existing SideStore install:
- Build.xcconfig: BASE_BUNDLE_ID -> com.SideStore.MultiStore (bundle id, app group and all
  sub-bundle ids follow). PRODUCT_NAME stays SideStore so Makefile packaging (SideStore.app/ipa)
  is unaffected.
- Bundle+AltStore.swift: appbundleIdentifier -> com.SideStore.MultiStore so keychain namespace,
  app-group lookup and self-refresh detection are isolated from SideStore.
- Info.plist: CFBundleDisplayName = MultiStore (home-screen name).
- AltStore/AltWidget ReleaseEntitlements.plist: app id + app group -> MultiStore (baked into the
  fakesigned CI IPA).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ttings account section

The top-right accounts bar button wasn't reliably visible under the app's custom
ForwardingNavigationController. Make the visible ACCOUNT section rows open the accounts
manager (add/remove/switch Apple accounts) and show a disclosure chevron so it's obviously
tappable. The accounts screen is presented modally, so it's independent of the nav controller.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Free-account sideloading must provision every embedded app extension; the AltWidget appex
fails with 'The app extension is missing a valid provisioning profile.' and consumes the free
10-App-IDs/week limit. Remove PlugIns before packaging so the IPA has no extensions to sign.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The picker built its allowed-types list dynamically from UTType.types(tag:"plist"...), which
on-device could fail to match how iOS tags an exported pairing file, greying out even a valid
.plist. Open with the universal .item supertype (plus propertyList/xml/text/data); the file is
validated when minimuxer starts, so a wrong pick fails gracefully rather than being unselectable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 15, 2026 16:34
@lungustefan lungustefan self-assigned this Jul 15, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR generalizes the signing/refresh pipeline from a single Apple Developer account to multiple coexisting accounts by introducing per-app account binding, per-account credentials/session storage, and account-partitioned refresh execution (with failure isolation). It also re-identifies the app as “MultiStore” to coexist alongside a stock SideStore install, and adds minimal Settings UI to manage accounts.

Changes:

  • Add Core Data v18 field InstalledApp.signingAccountID (+ fallback resolvedSigningAccountID) and startup migrations to backfill existing installs.
  • Introduce AccountManager + per-account Keychain storage and update authentication/refresh/install flows to be account-aware.
  • Add minimal account-management UI in Settings and update bundle identifiers / entitlements / CI to support the fork identity.

Reviewed changes

Copilot reviewed 25 out of 25 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
Shared/Extensions/Bundle+AltStore.swift Updates fork identity constants used for keychain/app-group isolation.
Build.xcconfig Changes base bundle id to …MultiStore and documents packaging constraints.
AltStore/Info.plist Sets on-device display name to “MultiStore”.
AltStore/Resources/ReleaseEntitlements.plist Updates release entitlements bundle/app-group identifiers for MultiStore.
AltWidget/Resources/ReleaseEntitlements.plist Updates widget entitlements identifiers/app-group for MultiStore.
AltStoreCore/Model/InstalledApp.swift Adds signingAccountID and resolvedSigningAccountID helper.
AltStoreCore/Model/AltStore.xcdatamodeld/AltStore 18.xcdatamodel/contents Adds Core Data model v18 with InstalledApp.signingAccountID.
AltStoreCore/Model/AltStore.xcdatamodeld/.xccurrentversion Points current model version to AltStore 18.
AltStoreCore/Managers/AccountManager.swift Adds multi-account facade, app↔account mapping, migrations, default account behavior.
AltStoreCore/Components/Keychain.swift Adds per-account credential storage + per-account in-memory auth-state cache.
AltStore/Operations/Common/OperationContexts.swift Adds AuthenticatedOperationContext.accountID + ignoresCachedCredentials.
AltStore/Operations/AuthenticationOperation.swift Implements per-account auth + storage, avoids deactivating other accounts for targeted auth.
AltStore/Operations/InstallAppOperation.swift Binds installs to the team/account that actually signed the app + stamps signingAccountID.
AltStore/Operations/ResignAppOperation.swift Embeds the correct (per-account) certificate for self-refresh.
AltStore/Managing Apps/AppManager.swift Partitions refreshes by account and infers account for single-app actions.
AltStore/Managing Apps/AccountManager+Actions.swift Adds app-layer actions: add/remove/refresh account and change app signing account.
AltStore/Settings/SettingsViewController.swift Adds entry points to Accounts UI and updates account-section interaction affordance.
AltStore/Settings/Accounts/AccountsViewController.swift New minimal accounts management UI (list/add/remove/default/refresh/manage apps).
AltStore/Settings/Accounts/AccountAppsViewController.swift New UI to view apps signed by an account and reassign signing account.
AltStore/LaunchViewController.swift Broadens accepted file types in document picker for pairing/account flows.
AltStore/AppDelegate.swift Runs idempotent startup migrations after DB startup.
AltStore.xcodeproj/xcshareddata/xcschemes/SideStore.xcscheme Adds DataStructureTests plan reference.
.github/workflows/multi-account-ci.yml Adds branch CI to build/archive and package an IPA; unit-tests job is non-blocking.
docs/multi-account/ARCHITECTURE.md Adds phase-1 architecture analysis documenting single-account assumptions.
docs/multi-account/PLAN.md Adds phase-2 implementation plan and migration/testing strategy.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 19 to 20
public static let altBundleID = "ALTBundleIdentifier"
public static let storeAppBundleIdentifier = "com.SideStore.SideStore"
Comment thread AltStore/Managing Apps/AccountManager+Actions.swift Outdated
Comment on lines +903 to +906
var credentials = Keychain.shared.credentials(forAccount: accountID)
credentials.emailAddress = emailAddress
if let password = password { credentials.password = password }
Keychain.shared.setCredentials(credentials, forAccount: accountID)
…resh completion)

- refreshAccount: complete via group.completionHandler when an account has no apps, so
  group-based observers/progress can't wait indefinitely.
- AuthenticationOperation: persist the session dsid/authToken into the per-account credential
  record (not just the global slots), so an account signed in via the global/interactive flow
  can still re-authenticate silently by token.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lungustefan

Copy link
Copy Markdown
Owner Author

Thanks @copilot — addressed in 38b14ce:

  • refreshAccount empty-apps path: now completes through group.completionHandler?([:]) instead of calling the external handler directly, so group-based observers/progress can't wait indefinitely. ✅
  • Per-account tokens in the global login flow: persistLoginCredentials now also stores the session dsid/authToken into the per-account keychain record (previously only email+password were persisted per-account, with tokens going to the global slots), so an account signed in via the global/interactive flow can re-authenticate silently by token. ✅
  • storeAppBundleIdentifier pointing at the old id: intentionally left unchanged — it's currently dead code (only referenced in a comment), so there's no collision. The active identity constant (appbundleIdentifier) and the xcconfig BASE_BUNDLE_ID are both com.SideStore.MultiStore. Noting it here rather than churning an unused constant.

@lungustefan
lungustefan merged commit 1ab68c4 into develop Jul 15, 2026
4 of 6 checks passed
@lungustefan
lungustefan deleted the feature/multi-account-support branch July 15, 2026 17:07
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.

2 participants