Skip to content
This repository was archived by the owner on Jun 14, 2026. It is now read-only.

Latest commit

 

History

History
406 lines (289 loc) · 19.7 KB

File metadata and controls

406 lines (289 loc) · 19.7 KB

Agent Instructions for AssppWeb

TypeScript Code Style

  • Indentation: 2 spaces
  • Semicolons: Required
  • Quotes: Single quotes for strings
  • Naming: PascalCase for types/interfaces, camelCase for variables/functions

Project Structure

  • backend/ — Node.js/Express backend for Docker-era API compatibility
  • backend-swift/ — production Swift/Vapor backend for personal jailbroken iPhone deployment
  • frontend/ — React SPA (TypeScript, Vite, Tailwind CSS)
  • e2e/ — Playwright E2E tests (pnpm)
  • references/ApplePackage/ — Swift reference implementation (source of truth)
  • Multi-stage Docker build (single container serves both)

Architecture — Personal Backend

┌─ Browser (Client) ─────────────────────────────────────┐
│  Account management UI                                 │
│  Credentials in IndexedDB: email, password, cookies,   │
│    passwordToken, DSID, deviceIdentifier, pod          │
│                                                        │
│  Calls backend-swift APIs:                             │
│    POST /api/apple/authenticate                        │
│    POST /api/apple/purchase                            │
│    POST /api/apple/versions                            │
│    POST /api/apple/version-metadata                    │
│    POST /api/downloads/apple                           │
└──────────────────────┬─────────────────────────────────┘
                       │ Authenticated local HTTP API
┌─ backend-swift ──────┴─────────────────────────────────┐
│  Apple protocol client (AsyncHTTPClient HTTP/1.1):      │
│    1. Bag fetch → resolve authenticateAccount URL       │
│       with default auth endpoint when bag omits it      │
│    2. Authenticate → token, cookies, store front, pod   │
│    3. Purchase → acquire license                       │
│    4. Download info → CDN URL + SINFs + metadata        │
│    5. Version listing/lookup                            │
│                                                        │
│  IPA pipeline:                                          │
│    - Download IPA from Apple CDN                        │
│    - Inject SINFs and iTunesMetadata.plist              │
│    - Decrypt locally through unfaird                    │
│    - Serve completed IPA and install manifest           │
└──────────────────────────────────────────────────────┘

Key invariant: backend-swift owns Apple protocol execution for personal self-hosted use. The browser manages account records and submits the selected account object to Swift APIs for Apple operations. Swift handles Apple TLS, cookie merging, pod routing, license acquisition, download info retrieval, IPA download, SINF injection, and local decryption in one process.

Reference Implementation

The Swift reference at references/ApplePackage/ is the source of truth for Apple protocol behavior:

  • Field mappings (iTunes API → Software type) use Swift CodingKeys
  • Authentication flow, bag endpoint, pod routing, error codes
  • Always consult the reference when making protocol changes

iTunes API Field Mapping

The backend (backend/src/routes/search.ts) maps raw iTunes API fields to our Software type, matching the Swift CodingKeys in references/ApplePackage/Sources/ApplePackage/Models/Software.swift:

iTunes Field Software Field
trackId id
bundleId bundleID
trackName name
artworkUrl512 artworkUrl
currentVersionReleaseDate releaseDate

All other fields (version, price, artistName, sellerName, description, averageUserRating, userRatingCount, screenshotUrls, minimumOsVersion, fileSizeBytes, releaseNotes, formattedPrice, primaryGenreName) keep their original names.

The backend also extracts the results array from the iTunes wrapper { resultCount, results } before sending to the frontend.

Per-Account Device Identifiers

Device identifiers are per-account, not global:

  • Generated as 12 random hex chars (6 bytes) at account creation via generateDeviceId()
  • Editable during login, immutable after authentication
  • Stored in IndexedDB on the Account object as deviceIdentifier
  • Passed to all Apple protocol calls (auth, purchase, download, version listing)

Pod-Based Host Routing

After authentication, Apple returns a pod header:

  • Store API: p{pod}-buy.itunes.apple.com (default: p25-buy.itunes.apple.com)
  • Purchase API: p{pod}-buy.itunes.apple.com (default: buy.itunes.apple.com)
  • Pod is stored on the Account object and used for all subsequent API calls
  • Host selection lives in backend-swift/Sources/UnfairDaemonCore/AppleProtocolService.swift

Apple Protocol Backend

backend-swift/Sources/UnfairDaemonCore/AppleProtocolService.swift mirrors the necessary ApplePackage flows:

  • authenticate fetches the bag, sets the guid query once, follows Apple auth redirects, detects 2FA, stores storefront and pod.
  • purchase sends buyProduct, uses STDQ, retries with GAME for failure 2059, and returns updated cookies.
  • downloadInfo sends volumeStoreDownloadProduct, returns CDN URL, base64 SINFs, bundle versions, and binary iTunes metadata.
  • listVersions and versionMetadata use the same Apple download-product response shape as ApplePackage.
  • AsyncHTTPClient is configured for HTTP/1.1 and manual redirects for Apple protocol requests.

Backend

  • Express backend for Docker-era API compatibility
  • ESM modules ("type": "module" in package.json)
  • tsx for development, tsc for production build
  • SINF injector also handles optional iTunesMetadata.plist injection at IPA root
  • Bag proxy for init.itunes.apple.com

Swift Backend

  • backend-swift/ is the production jailbroken iPhone backend.
  • It embeds AssppWeb API routes, Apple protocol execution, static frontend serving, and unfaird IPA processing into one Swift/Vapor launchd service.
  • Package ID is wiki.qaq.unfaird; launchd label is wiki.qaq.unfaird; default port is 8080.
  • backend-swift/Package.swift depends on sibling ../../unfair when built from this repository.
  • Root make build is the single production packaging entry: it builds the frontend, builds the Swift iOS backend, and emits the rootless deb.
  • Root make install depends on make build and installs the generated deb on DEVICE_HOST or Theos device variables (THEOS_DEVICE_IP, THEOS_DEVICE_USER, THEOS_DEVICE_PORT).

Backend Shared Utilities

  • backend/src/utils/route.ts — shared Express route helpers (getIdParam, requireAccountHash, verifyTaskOwnership)
  • backend/src/config.ts — centralized constants (MAX_DOWNLOAD_SIZE, DOWNLOAD_TIMEOUT_MS, BAG_TIMEOUT_MS, BAG_MAX_BYTES, MIN_ACCOUNT_HASH_LENGTH) and env-var config (disableHttpsRedirect via UNSAFE_DANGEROUSLY_DISABLE_HTTPS_REDIRECT)

Frontend

  • React 19, React Router 7, Zustand for state
  • Tailwind CSS 4 for styling
  • Vite for build tooling
  • IndexedDB for credential storage (via idb)
  • frontend/src/api/apple.ts wraps Swift Apple protocol APIs for auth, purchase, backend download creation, version listing, and version metadata.
  • frontend/src/api/client.ts preserves structured API error payloads through ApiError.
  • frontend/src/apple/config.ts keeps storefront constants and per-account device ID generation.

Frontend Shared Components (components/common/)

  • Alert<Alert type="error|success|warning"> for status messages (replaces inline alert divs)
  • Modal<Modal open={bool} onClose={fn} title={string}> for dialog overlays
  • Spinner — inline SVG loading spinner for buttons
  • CountrySelect — optgroup-based country dropdown with "Available Regions" + "All Regions"
  • AppIcon — 3 sizes (40/56/80px), rounded corners, letter fallback
  • Badge — color-coded status pill
  • ProgressBar — gray track, blue fill, percentage label
  • icons — shared SVG icon components (HomeIcon, AccountsIcon, SearchIcon, DownloadsIcon, SettingsIcon, SunIcon, MoonIcon, SystemIcon) used by Sidebar, MobileNav, and MobileHeader

Frontend Shared Utilities (utils/)

  • utils/error.tsgetErrorMessage(e, fallback) for standardized catch-block error extraction
  • utils/crypto.ts — AES-GCM encrypt/decrypt for account export/import
  • utils/account.tsaccountHash(), accountStoreCountry(), firstAccountCountry()

Import Ordering Convention

  1. React / library imports (useState, useNavigate, useTranslation)
  2. Layout components (PageContainer)
  3. Common components (AppIcon, Alert, Spinner, Modal, CountrySelect)
  4. Sibling components within the same feature folder (e.g., DownloadItem inside Download/)
  5. Hooks / stores (useAccounts, useSettingsStore)
  6. Apple API modules (authenticate, purchaseApp, apiPost)
  7. Utilities (accountHash, getErrorMessage)
  8. Config (countryCodeMap, storeIdToCountry)
  9. Types (type Software)

Enforcement: Every PR must verify import ordering. Common mistakes:

  • Putting hooks/stores before layout/common components
  • Putting config before utilities
  • Putting type imports in the middle instead of last

Security Model

Account Hash Is Public

accountHash is a SHA-256 of the account email. It is treated as a local owner label for downloads and packages. Access control comes from the optional ACCESS_PASSWORD gate and the browser's selected account set.

Trusted Sources

  • Apple API responses (bag XML, iTunes search results, customerMessage fields) are treated as trusted content. No additional sanitization is applied beyond what React's text rendering provides (no dangerouslySetInnerHTML).
  • Apple CDN redirects during IPA download are trusted. The initial URL is validated against *.apple.com, and redirect targets from Apple's CDN infrastructure (e.g., Akamai) are followed. The response body is saved to disk — it is never reflected back to the requester.

Personal Browser and Backend Boundary

Credentials (passwords, passwordToken, cookies) are stored in IndexedDB for local account management. When the user authenticates, purchases, lists versions, or starts a download, the frontend sends the selected account object to backend-swift, and the Swift process performs the Apple network operation.

Backend Does Not Reflect Request Headers

The settings endpoint (/api/settings) must never reflect request headers (x-forwarded-host, host, etc.) in its response body. Use server-side values only (config.*, process.uptime()).

Error Handling

  • Early returns to reduce nesting
  • try/catch for async operations
  • Express error middleware for centralized handling
  • Type-safe error responses

Apple Protocol Error Codes

  • 2034 / 2042: Token expired — re-authentication required
  • customerMessage === 'Your password has changed.': Password token invalid
  • action.url ending in termsPage: Terms acceptance required (throw with URL)

Testing

Unit Tests

cd backend && npx vitest run    # Node environment
cd frontend && npx vitest run   # jsdom environment with fake-indexeddb

E2E Tests (Playwright)

cd e2e && pnpm test                            # Local (requires Docker on port 8080)
docker compose --profile test run --rm playwright  # Docker-based
bash e2e/docker-test.sh                        # Full Docker build + test flow

E2E tests import from ./fixtures instead of @playwright/test.

E2E tests cover:

  • Add account flow (device ID field, randomize button, auth)
  • Account detail (device ID, pod display)
  • Settings page (no global device ID section)
  • Search/lookup by bundle ID (verifies iTunes field mapping)
  • Downloads API (backend Apple protocol execution, iTunesMetadata support, package lifecycle)

Test Account

Test credentials are stored in environment variables (TEST_EMAIL, TEST_PASSWORD, TEST_DEVICE_ID, TEST_BUNDLE_ID) and must never be committed to the repository.

Deployment

docker compose up --build -d   # Builds and runs on port 8080

Single container serves both the Express backend and the Vite-built React SPA. SPA routes are handled by serving index.html for all non-API paths.

Docker E2E Testing

The compose.yml includes a playwright service under the test profile:

docker compose --profile test run --rm playwright

This runs Playwright inside the official mcr.microsoft.com/playwright image, connecting to the app container via Docker internal DNS (http://asspp:8080). The asspp service has a healthcheck so the test container waits until the app is ready.

The e2e/docker-test.sh script automates the Docker build and Playwright test flow.

Interface Design System

Intent

Who: Developers and power users managing Apple app downloads outside the App Store — sideloading IPAs, managing multiple Apple IDs, tracking licenses. Technical audience, likely running this alongside terminals or Xcode.

Task: Authenticate Apple accounts → search apps → acquire licenses → download/compile IPAs → install.

Feel: A sharp utility. Precise like a package manager, clear like Apple's developer tools. Confident, quiet, functional. Not playful, not corporate.

Design Tokens

  • Primary accent: blue-600 / blue-700 (hover) — trust + system authority, echoes Apple dev tooling
  • Backgrounds: gray-50 (app), white (cards/surfaces)
  • Text: gray-900 (primary), gray-600 (secondary), gray-400 (tertiary)
  • Borders: gray-200 (default), gray-300 (hover) — use sparingly, prefer background tinting for containment
  • Status badges: Muted tones — green (completed), blue (downloading), yellow (paused), purple (injecting), red (failed), gray (pending)
  • Alerts: red-50/red-700 (error), amber-50/amber-700 (warning), green-50/green-700 (success)

Typography

  • System font stack (Inter / SF Pro fallback)
  • Weight scale: 500 (medium, workhorse), 600 (semibold, page titles and key labels only). Avoid 700 in body.
  • Size scale: xs (12px), sm (14px), base (16px), lg (18px), xl (20px), 2xl (24px)

Spacing

  • Base unit: 4px
  • Consistent vertical rhythm: space-y-4 within sections, space-y-6 between sections
  • Page padding: px-4 sm:px-6, py-6
  • Container: max-w-5xl (1024px)

Depth & Surfaces

  • Single elevation: white cards on gray-50 background
  • No shadows. Borders only where they serve function (form inputs, dividers, interactive boundaries)
  • Rounded corners: rounded-lg (8px) for cards, rounded-md (6px) for inputs/buttons, rounded-full for badges
  • Prefer background tinting (gray-50gray-100) over borders for visual containment

Layout

  • Desktop: fixed sidebar (240px / w-60) + scrollable main content
  • Mobile: bottom tab bar with safe-area padding
  • Breakpoint: md: (768px) for sidebar ↔ bottom nav switch
  • Page structure: PageContainer with title + optional action button, then content

Component Patterns

  • Buttons: Primary (bg-blue-600 text-white), Secondary (border border-gray-300 text-gray-700), Danger (text-red-600 border-red-300)
  • Inputs: rounded-md border-gray-300 focus:border-blue-500 focus:ring-1 focus:ring-blue-500
  • Cards: White background, border border-gray-200 rounded-lg, no shadow
  • Badge: Color-coded pill (rounded-full px-2 py-0.5 text-xs font-medium)
  • ProgressBar: Gray track, blue fill, percentage label
  • AppIcon: 3 sizes (40/56/80px), rounded corners, letter fallback
  • Nav active state: bg-blue-50 text-blue-700 (sidebar), text-blue-600 (mobile)

Frontend Cleanup Rules

These rules prevent the codebase from becoming messy after merging PRs. Enforce them on every change.

transition-colors Usage Policy

Problem: transition-colors on static containers (cards, sections, alerts, badges) causes visible color flashing when the page loads in dark mode — the element briefly renders in light colors then transitions to dark.

Rule: Only use transition-colors on interactive elements that change color on user interaction:

  • Buttons (hover state)
  • Links (hover state)
  • Form inputs and selects (focus state)
  • Nav items (hover/active state)

Never use transition-colors on:

  • Card containers (bg-white dark:bg-gray-900 rounded-lg border ...)
  • Section wrappers (<section> with background)
  • Alert/warning banners (use the <Alert> component)
  • Badge pills
  • ProgressBar tracks
  • Modal containers
  • AppIcon fallback containers
  • Empty state placeholder containers

Exception: Layout chrome (Sidebar, MobileNav, MobileHeader, PageContainer) may keep transition-colors duration-200 for smooth theme toggle animation, since these persist across navigations.

Shared Icons

All navigation and theme icons live in components/common/icons.tsx. When Sidebar, MobileNav, or MobileHeader need icons, import from there. Never duplicate icon SVG components inline.

Import Ordering Verification

Before merging any frontend PR, verify imports follow the convention in every changed file:

1. React / library imports
2. Layout components
3. Common components
4. Sibling components (same feature folder)
5. Hooks / stores
6. Apple protocol / API modules
7. Utilities
8. Config
9. Types (always last)

Empty State Containers

Empty states (shown when a list has no items) use a consistent pattern:

  • border-2 border-dashed (not solid border)
  • bg-gray-50 dark:bg-gray-900/30 background
  • No transition-colors (removed to prevent dark mode flashing)
  • Centered icon in a white circle, title, description, optional CTA button

Dark Mode Color Pairings

Always pair light and dark variants consistently:

  • Primary text: text-gray-900 dark:text-white
  • Secondary text: text-gray-600 dark:text-gray-400 or text-gray-500 dark:text-gray-400
  • Tertiary text: text-gray-400 dark:text-gray-500
  • Card background: bg-white dark:bg-gray-900
  • Page background: bg-gray-50 dark:bg-gray-950
  • Card border: border-gray-200 dark:border-gray-800
  • Input border: border-gray-300 dark:border-gray-700

Code Duplication Prevention

When the same UI pattern appears in 3+ components, extract it to components/common/. Current shared components:

  • Alert, Modal, Spinner, CountrySelect, AppIcon, Badge, ProgressBar, icons

When adding new common components, update this AGENTS.md file accordingly.

Authenticated API Downloads

Problem: Plain <a href="/api/..."> tags and window.open("/api/...") make regular browser navigations that cannot carry custom HTTP headers. When ACCESS_PASSWORD is set, the accessAuth middleware requires an X-Access-Token header, so these requests fail with 401.

Rule: Never use <a href> or window.open for /api/ endpoints that require authentication. Instead, use fetch() with authHeaders() from api/client.ts, then trigger a download via blob URL:

const res = await fetch(url, { headers: authHeaders() });
const blob = await res.blob();
const blobUrl = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = blobUrl;
a.download = filename;
a.click();
URL.revokeObjectURL(blobUrl);

Exceptions: Routes that the backend explicitly skips auth for (/auth/*, /install/*) may use plain links — e.g., itms-services:// install URLs are fine since /install/* is public.