Multi-account signing support - #1
Merged
Merged
Conversation
…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>
There was a problem hiding this comment.
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(+ fallbackresolvedSigningAccountID) 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 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>
Owner
Author
|
Thanks @copilot — addressed in 38b14ce:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
InstalledApp.signingAccountID(optional, lightweight migration) +resolvedSigningAccountIDfallback toteam.account.Keychainstores each account's credentials + cached session/cert/team keyed byAccount.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.AuthenticatedOperationContext.accountIDselects which account to authenticate; the interactive/default flow is preserved.AppManager.refreshpartitions 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.signingAccountIDbackfilled) with no data loss.com.SideStore.MultiStore) so it coexists with a stock SideStore install.CI
.github/workflows/multi-account-ci.ymlbuilds the archive on macOS (green) and uploads an installable IPA. Theunit-testsjob is non-blocking due to a pre-existing upstreamlibem_proxy_static.abuild-for-testing issue (fails on the unmodified baseline too).Docs
docs/multi-account/ARCHITECTURE.md(Phase 1 analysis) anddocs/multi-account/PLAN.md(Phase 2 plan).🤖 Generated with Claude Code