Files
FTStore/manager/AGENTS.md
T
Felitendo e3e87b03a9 feat(manager): import a certificate baked into the downloaded ipa
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.
2026-08-04 13:30:57 +02:00

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-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.
  • 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 appends FTStoreSeed/ to the IPA's ZIP before saving it. ManagerSeedImporter reads 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 no FTStoreSeed/ 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.
    • _supportedVersion here and SEED_FORMAT_VERSION in web/src/lib/landing/ipa-seed.ts are 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.

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.