Files
FTStore/manager/AGENTS.md
T
Felitendo 86cf31bb2b feat: split the admin console out of the manager into its own app
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.
2026-08-04 08:53:22 +02:00

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-Manager repo; it now lives in manager/ inside FTStuff/FTStore, alongside ios/ (the app it installs), web/ (the FTStore Admin server it talks to) and admin/ (that server's console app). Read the root AGENTS.md first — it owns the tag namespaces (manager-v*, manager-dev) and the rule that scripts/apply-rebrand.sh must never touch this directory. Zsign and IDeviceKitten are submodules here, not vendored copies; clone with --recurse-submodules.

Brand / identity

  • App name FTStore Manager · bundle id app.ftstore.manager (must differ from app.ftstore so 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), so make and 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 Admin repo: 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-dev release with an FTStore*.ipa asset (version = tag); dev = the dev-tagged release, tracked by the asset's updatedAt (the version string is constant). Compare logic lives in FTStore/Manager/FTStoreUpdateManager.swift.
  • Install: OTA only (itms-services via Vapor ServerInstaller). The device-pairing method was removed. InstallPreviewView already 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, its manager.admin.* defaults, the now-dead FTStore.developerMode flag). 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 shows ManagerHomeView directly — 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 under Views/.
  • Config: FTStore.xcconfig (FTSTORE_PRODUCT_BUNDLE_IDENTIFIER, FTSTORE_ADMIN_BASE_URL) → injected into Info.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).
  • FTStoreUpdateManager reads installed state from Storage.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 the Manager Release GitHub Action.
  • Static checks (CI/Linux ok): plistlib/json parse, and grep the repo + built binary for ghp_ / any token — there must be none (the proxy design guarantees it).

GPL-3.0

Keep LICENSE and Feather/FTStore attribution intact; keep source public.