diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..54f2c65 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,79 @@ +# iOS App Store + +Self-hosted iOS OTA distribution server. Node.js/Express + SQLite + Docker, deployed on unraid behind Nginx Proxy Manager. + +## Live deployment + +- **URL**: https://appstore.treytartt.com +- **Container**: `ios-appstore` on unraid (port `3080` internally, proxied via NPM at host `10.3.3.11:3080`) +- **App code**: `/mnt/user/appdata/ios-appstore/` on unraid (Dockerfile, source, compose, `.env`) +- **Data volume**: `/mnt/user/downloads/ios-appstore/` mounted as `/data` (SQLite DB, IPAs, icons) + +This split is intentional — app code in appdata, persistent data in downloads. Don't put data volumes in appdata or app source in downloads. + +The `.env` on the server holds `ADMIN_PASSWORD`, `API_TOKEN`, and `SESSION_SECRET`. Read it from `/mnt/user/appdata/ios-appstore/.env` when you need them. + +## Deploy flow + +```bash +# 1. Sync local changes to unraid (excludes node_modules, data, .env) +rsync -avz --exclude node_modules --exclude data --exclude .env \ + /Users/m4mini/Desktop/code/ios-appstore/ \ + unraid:/mnt/user/appdata/ios-appstore/ + +# 2. Rebuild and restart +ssh unraid "cd /mnt/user/appdata/ios-appstore && docker compose up -d --build" + +# 3. Verify +ssh unraid "docker logs ios-appstore --tail 20 && curl -s http://localhost:3080/api/health" +``` + +`docker-compose.yml` builds the image from local source — no registry. The data volume persists across rebuilds. + +## Architecture + +- `src/server.js` — Express app, all routes, multer upload handling +- `src/db.js` — SQLite schema (apps, builds, devices) +- `src/ipa-parser.js` — Unzips IPA, extracts `Info.plist` and app icon +- `src/manifest.js` — Generates the OTA manifest plist iOS fetches +- `src/auth.js` — Session middleware (web UI) + token middleware (API) +- `views/` — Login, app listing, upload pages (vanilla HTML) +- `public/` — CSS + client-side JS + +Apps are keyed by bundle ID. Uploading the same bundle ID adds a new build to the existing app instead of duplicating it. + +## Auth model + +Two parallel auth schemes: +- **Session cookies** for browser users (login at `/login` with `ADMIN_PASSWORD`) +- **`X-Api-Token` header** for CLI/automation (`API_TOKEN`) + +`requireAuth` accepts either. The manifest and IPA download endpoints (`/api/manifest/:id`, `/api/download/:id`) are intentionally **public** — iOS fetches them unauthenticated during the OTA install. + +## OTA gotchas + +- **Development-signed IPAs cannot be installed OTA.** iOS only allows OTA installs of `ad-hoc` or `enterprise`-signed builds. If a user reports "integrity could not be verified", first check the export method in their `ExportOptions.plist`. +- Ad-hoc requires a **distribution certificate** (not a development cert) and an **ad-hoc provisioning profile** with the target device UDIDs registered. +- HTTPS is mandatory and must use a trusted CA. Self-signed certs don't work on iOS 12+. NPM's Let's Encrypt cert handles this. +- The manifest's `bundle-identifier` must exactly match the IPA's bundle ID. + +## Testing changes + +For backend changes, after deploying: +```bash +# Health +curl -s https://appstore.treytartt.com/api/health + +# List apps (needs token) +curl -s -H "X-Api-Token: $TOKEN" https://appstore.treytartt.com/api/apps + +# Upload an IPA +curl -X POST https://appstore.treytartt.com/api/upload \ + -H "X-Api-Token: $TOKEN" \ + -F "ipa=@/path/to/App.ipa" \ + -F "notes=test" +``` + +Get `$TOKEN` from `/mnt/user/appdata/ios-appstore/.env` on unraid. + +For frontend changes, just rsync + restart and refresh the browser.