Files
FTStore/admin
Felitendo 96547ac86f ci(admin): publish a rolling admin-dev prerelease instead of build artifacts
The console now ships the way the app and the Manager do: every push
touching admin/ builds an unsigned IPA and publishes it, together with a
generated AltStore source, to a rolling `admin-dev` prerelease.

- admin/app-repo.json is the source template the workflow rewrites with
  jq, same as manager/app-repo.json.
- The tag is `admin-dev`, distinct from `dev` and `manager-dev` so the
  three builds don't overwrite each other's assets.
- admin-build.yml is gone; the dev workflow covers the compile check it
  existed for, and running both would build every admin/ push twice.

The repo is private, so "published" here means reachable by whoever
already has access — which is the whole distribution model for an
operator console. There is still no web/ proxy and no in-app updater:
this app only ever calls the session-gated /api/* routes.
2026-08-04 09:29:58 +02:00
..

FTStore Admin

The FTStore Admin dashboard as an iOS app — for the person who runs the server, and nobody else.


FTStore Admin signs in against your FTStore Admin server with a normal dashboard account and then drives the same API the web UI does: the worker, the library, the queue, the scraper, Telegram, users, settings, backups.

It used to be a second tab inside FTStore Manager, hidden behind a developer-mode switch. It is its own app now, because "hidden" was the wrong tool: the Manager is what a normal user installs to get FTStore onto their phone, and an operator console has no business being one gesture away from that.

What it is not

  • Not something to hand out. It ships from the rolling admin-dev prerelease of a private repo, so its AltStore source is only reachable by someone who already has access. There is no web/ proxy and no in-app updater — it never touches the /api/ftstore-* endpoints the app and the Manager use.
  • Not an installer. It signs nothing and installs nothing — no Zsign, no Vapor, no certificates, no OTA. Installing FTStore is the Manager's job.
  • Not useful without an account. See below.

Screens

Screen What it does
Root Worker start/stop, headline KPIs, navigation, sign out / lock
Dashboard KPIs, 30-day trends, builds per source, in-flight transfers, recent activity
Library Every build: search, filter, detail (tweaks, entitlements, privacy manifest), rename in the feed, flag corrupted, refresh App Store metadata, delete
Upload IPA Pick an .ipa, attribute it to a source, push it in 8 MiB chunks and run the ingest pipeline
Queue Per-status counts, storage-cap banner, paging, retry / skip / delete, bulk retry-failed and clear-completed
Logs The activity log — filter by type and status, search, paginate
Channels & Sources Telegram channels (add, enable, remove) and GitHub release sources (enable)
Telegram The worker's TDLib login: connect, verification code, 2FA password, disconnect, reset
Known Tweaks Edit known_tweaks: aliases, lock to a Telegram channel or GitHub repo, recommended flag, file-name filters, custom icon (Files, Photos or camera)
Users Admin only — approve, revoke, delete accounts
Scraper Actions Find IPA, scan, scan previous, process queue, publish feed, cleanup, cache, rebuild storage, nuke
Server Settings Source metadata, intervals, limits, feed/storage options; credentials shown read-only and masked
Backup & Restore Export the whole configuration as JSON (with or without secrets) and import one back

Two things stay in the browser, both because they are bulk work on a table rather than a phone task: multi-select delete/rename across the Library, and reordering channels.

Why decrypting the IPA gets you nothing

The security model is that the app is worthless without an account:

  • No secret ships in the app. The only baked-in value is the FTStore Admin base URL, which is the same public domain FTStore already uses as its default source. There is no API key, no token, no privileged URL to extract.
  • Every request is authenticated server-side. web/src/middleware.ts rejects any /api/* call without a session cookie, and each handler re-checks that session against the database through withAuth() / withAdmin(). A forged cookie gets a 401 from the handler even though it passes the middleware's presence check.
  • The password is never stored. It is used for one POST /api/auth/login and discarded. What persists is the server-issued session token.
  • The token lives in the Keychain, as kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly — so it is not in an iTunes/iCloud backup and does not travel to another device. The app's URLSession is ephemeral with cookie storage disabled, so nothing authenticating it is ever written to the app container in the clear.
  • Sessions are revocable. Signing out ends the session on the server too, and an admin can revoke any account from Settings → Users at any time.
  • Nothing sensitive is cached locally. GitHub PATs and the Telegram API hash arrive masked from the server and are read-only here; the Telegram login code and 2FA password are typed here but handled only by your own worker; a settings export is written to the temp directory for the share sheet, never to a browsable one.
  • Optional local gate. Face ID / Touch ID (falling back to the device passcode) can be required before the console opens, and re-arms whenever the app leaves the screen. That protects an unlocked, unattended device; the server credentials are what actually protect the data.

Layout

FTStoreAdmin/
  FTStoreAdminApp.swift        @main — session, tint, the biometric re-lock
  Console/                     One file per screen, plus the transport
    AdminAPI.swift             HTTP, multipart upload, raw payloads, login
    AdminSession.swift         The single door: token, 401 handling, lock
    AdminKeychain.swift        Where the session token lives
    AdminModels.swift          Mirrors web/'s handler responses + request bodies
    AdminRootView.swift        Front door and table of contents
    Admin*View.swift           The screens in the table above
  Support/
    AdminConfig.swift          Base URL, User-Agent — the only baked-in values
    AdminTheme.swift           The brand colour
    AdminUIKit.swift           Share sheet + image resize, inlined from NimbleKit
    AnyCodable.swift           For the open-ended payloads
  Resources/                   Info.plist, app icon

The app links no Swift package at all — only system frameworks (SwiftUI, Charts, Security, LocalAuthentication, PhotosUI). That is deliberate: the few helpers it needed from the FTStore engine are inlined in Support/, which keeps the binary small and keeps "a decrypted IPA is a login form" easy to verify.

Configuration

The server address is injected at build time via FTSTORE_ADMIN_BASE_URL in FTStoreAdmin.xcconfig (it flows into Info.plist as FTStoreAdminBaseURL); leave it blank to use the built-in default, https://ftstore.app. Never commit a token here — there is no field for one, and there should never be.

Building

macOS + Xcode required. No submodules, no packages, no deps step:

cd FTStore/admin
make            # builds packages/FTStore-Admin.ipa

Or open FTStoreAdmin.xcodeproj in Xcode.

CI works like the app's and the Manager's: Admin Dev Repo builds every push that touches admin/ and publishes the IPA plus a generated AltStore source (admin-devrepo.json) to the rolling admin-dev prerelease, so the console updates itself the same way the other two do. Admin Release cuts a draft release from an admin-v* tag when a stable one is wanted.

https://github.com/FTStuff/FTStore/releases/download/admin-dev/admin-devrepo.json

The IPA is unsigned — sign it on-device at install with your own certificate.

License & credits

GPL-3.0, like the rest of the repo — see the root LICENSE. The console was written for FTStore Admin's own API; the two UIKit helpers in Support/AdminUIKit.swift are adapted from NimbleKit (MIT), and AnyCodable.swift comes from FTStore's engine, which descends from Feather.