The web/ dashboard used to ride along inside FTStore Manager as a second tab, hidden behind a developer-mode switch. Hiding was the wrong tool: the Manager is what a normal user installs to get FTStore onto a phone, and the worker controls were one gesture away from it. FTStore Admin (admin/, app.ftstore.admin) is now its own app: - admin/FTStoreAdmin/Console/* is the moved console, unchanged in behaviour — ManagerConfig became AdminConfig, ManagerHomeView.brand became AdminTheme.brand. - It links no Swift package at all, only system frameworks. The three helpers it borrowed from NimbleKit and the engine (User-Agent, share sheet, image resize, AnyCodable) are inlined in Support/, so the IPA stays what it claims to be: a login form and nothing else. - Its Keychain service is app.ftstore.admin and its User-Agent is FTStore-Admin/…, deliberately outside web/'s APP_UA gate — the console only calls session-authenticated routes. - The optional biometric gate now re-arms whenever the app leaves the screen, not just at launch. - Sideloaded on purpose: admin-release.yml cuts a draft release from an admin-v* tag and admin-build.yml compiles every push, but there is no AltStore source and no self-update channel. The Manager loses the console, the developer-mode gate and therefore the tab bar — FTStoreManagerApp shows ManagerHomeView directly. Its camera and Face ID usage strings go with it, and ManagerLegacyConsole.purge() wipes the session an updated install would otherwise still be holding.
4.5 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.
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.