ftstore.app can hand out a personalised Manager build with a FTStoreSeed folder appended to its zip. When one is present, ManagerSeedImporter reads it once at launch and imports the certificate as default, so a first install has no setup step. A plain ipa has no seed folder and the importer no-ops. The once-only guard is a fingerprint of the key material rather than a bool: the bundle is read-only so the seed cannot be consumed, and reinstalling with a different certificate must still seed. It is recorded only once the import has actually reached the database, so a failure retries on the next launch.
5.6 KiB
FTStore Manager — project guide for AI agents
FTStore Manager is a stripped-down fork of FTStore (itself a fork of Feather, GPL-3.0). Its only job: install and update FTStore from a private FTStore Admin, after the user imports a signing certificate.
Monorepo. This used to be the separate
FTStuff/FTStore-Managerrepo; it now lives inmanager/insideFTStuff/FTStore, alongsideios/(the app it installs),web/(the FTStore Admin server it talks to) andadmin/(that server's console app). Read the root AGENTS.md first — it owns the tag namespaces (manager-v*,manager-dev) and the rule thatscripts/apply-rebrand.shmust never touch this directory.ZsignandIDeviceKittenare submodules here, not vendored copies; clone with--recurse-submodules.
Brand / identity
- App name
FTStore Manager· bundle idapp.ftstore.manager(must differ fromapp.ftstoreso installing FTStore is never a self-install → OTA works without pairing). - Internal Xcode target/scheme/product name is still
FTStore(only the display name and bundle id changed), somakeand the project file work unchanged.
How it works (the important part)
- The GitHub PAT is never in the app. Downloads go through the FTStore Admin proxy
(
FTStore Adminrepo:src/app/api/ftstore-app/manifest+/download), which holds the token server-side. The app only knows the FTStore Admin base URL. - Channels:
stable= newest non-devrelease with anFTStore*.ipaasset (version = tag);dev= thedev-tagged release, tracked by the asset'supdatedAt(the version string is constant). Compare logic lives inFTStore/Manager/FTStoreUpdateManager.swift. - Install: OTA only (itms-services via Vapor
ServerInstaller). The device-pairing method was removed.InstallPreviewViewalready uses OTA for a non-self-install. - A build can arrive with its certificate already in it. ftstore.app's landing page
lets someone hand over a
.p12+.mobileprovision+ password in their browser, and appendsFTStoreSeed/to the IPA's ZIP before saving it.ManagerSeedImporterreads that folder once at launch and imports the certificate as default, so a first install has no setup step at all. A plain IPA has noFTStoreSeed/and the importer no-ops.- The once-only guard is a fingerprint of the key material in
UserDefaults, not a bool: the bundle is read-only so the seed cannot be consumed, and reinstalling with a different certificate must still seed. It is only recorded once the import actually reached the database, so a failure retries next launch. _supportedVersionhere andSEED_FORMAT_VERSIONinweb/src/lib/landing/ipa-seed.tsare one contract — bump both or neither.- The seed holds the password in the clear. That is deliberate and the website says so; do not "fix" it with obfuscation, which would only make the honesty harder.
- The once-only guard is a fingerprint of the key material in
No operator surface lives here
The web/ dashboard used to ride along as a second tab (FTStore/Admin/), hidden behind
a developer-mode switch. It is its own app now — admin/ — and it must stay
there:
- This app is for normal users. One screen, no account, no tabs. Anything that needs a
dashboard login belongs in
admin/, never behind a hidden gate here. - The install path must stay account-free —
/api/ftstore-app/*is UA-gated only, on purpose, so a fresh device can always get FTStore onto it. ManagerLegacyConsole.purge()runs at launch and wipes what the old in-app console left behind (its Keychain session, itsmanager.admin.*defaults, the now-deadFTStore.developerModeflag). Keep it: an install that updated across the split is otherwise still holding a live server session nothing can reach.
Where things live
- New Manager code:
FTStore/Manager/*+FTStore/FTStoreManagerApp.swift(the@main, which showsManagerHomeViewdirectly — there is no root/tab container). - Reused engine (unchanged):
Backend/Storage,Backend/Server,Backend/Observable(DownloadManager,AppInstallManager,OptionsManager,UpdateManager[inert]),Utilities/Handlers(sign/archive/cert/zsign/tweak),Utilities/{FR,CertificateReader,MachO,ARDecompression},Extensions, and the certificate/install/signing-setup views underViews/. - Config:
FTStore.xcconfig(FTSTORE_PRODUCT_BUNDLE_IDENTIFIER,FTSTORE_ADMIN_BASE_URL) → injected intoInfo.plist(FTStoreAdminBaseURL). Never commit a real PAT.
Integration seams (when changing the engine)
AppInstallManager.install(InstallRequest)is the Manager's entry into the existing download → cert-gate → sign → install pipeline (reuses the private_startStoreDownload).FTStoreUpdateManagerreads installed state fromStorage.newestSignedApp(identifier: "app.ftstore").
Dependencies
Vendored (no submodules): Zsign, NimbleKit, AltSourceKit, IDeviceKitten. Remote
SPM: Vapor, Zip, SWCompression, Nuke, LicensePlist. AltSourceKit/IDeviceKitten/
LicensePlist stay linked because the engine still references a few helpers from them; they
can be pruned in Xcode if desired.
Build / validate
- macOS + Xcode only.
make→packages/FTStore-Manager.ipa, or theManager ReleaseGitHub Action. - Static checks (CI/Linux ok):
plistlib/jsonparse, and grep the repo + built binary forghp_/ any token — there must be none (the proxy design guarantees it).
GPL-3.0
Keep LICENSE and Feather/FTStore attribution intact; keep source public.