# StaffTraveler — URL surface crawl (2026-04-22)

Full enumeration of what is and isn't accessible with a valid user Firebase ID token from the `stafftraveler-prod` project. Tested as user `3NNPesQMiMRNYnPmuQzh6w2YKyh1` (WN/`st_SWA`) using fresh tokens from `app.stafftraveler.com`.

## HTTP commands (`api.stafftraveler.com/v1/commands/user/<name>`)

All 34 commands confirmed via `createFirebaseCommand` enumeration in the Hermes bundle. Only the `user/` prefix exists — tried `public/`, `internal/`, `admin/`, `v2/`, `flights/`, `loads/`, `system/`, `airline/` → all 404.

**Read-ish (test with safe payloads):**
- `appActive` — heartbeat. Requires `{localDateIso: ISO8601, appType: "mobile"|"web"}`. Returns `{payload: null}`.
- `mobile` — stub, returns `"Command mobile is not implemented"`.
- `searchFlightsByCode`, `searchFlightsByRoute` — flight search (free, any route).

**Everything else is a mutation:** `createLoadsRequests`, `deleteLoadsRequest`, `reopenLoadsRequest`, `upgradeLoadsRequest`, `requestLockForLoadsRequest`, `unlockLoadsRequest`, `submitLoadsReport`, `reviseLoadsReport`, `flagLoadsReport`, `addAirlineDetails`, `addComment`, `addIdentity`, `addCreditsForPurchase`, `createTip`, `deleteUserAccount`, `disableAllAutoRequestSubscriptions`, `disableFlightStatusUpdates`, `enableFlightStatusUpdates`, `discardHint`, `omitIdentity`, `pinFlight`, `unpinFlight`, `registerUser`, `removeComment`, `reportComment`, `setAutoRequestSubscription`, `submitFeedback`, `unlikeTip`, `updateUserProfile`, `appLogout`.

**`api.stafftraveler.com/health`** returns `{message:"ok", serverTime:...}` — open.

## Firestore collections (`firestore.googleapis.com/v1/projects/stafftraveler-prod/databases/(default)/documents`)

All reads require `Authorization: Bearer <idToken>` + `X-Firebase-GMPID: 1:628258099825:web:35b20eaab4d441894041d0` + `?key=AIzaSyD82Hiqx-jAbHF8faMGSveqLw8CdX8a-Uc`.

### Publicly readable, cross-airline (globally listable or get-by-id)

| Path | Content | Size |
|------|---------|------|
| `__system/maintenance` | service maintenance flags | 1 doc |
| `__system/version` | required client versions (android/ios blacklists) | 1 doc |
| `__system/supportedCurrencies` | currency support map | 1 doc |
| `__derived/nonRevAgreementsBySt` | **Full interline non-rev agreement matrix**: `agreements: { st_XXX: [st_YYY, ...], ... }` | 300 airline keys — pulled and saved to `nonRevAgreementsBySt.json` |
| `airlinesBySt/*` | Airline ST-code directory | 300+ docs — pulled to `airlinesBySt_full.json` |
| `airlines/*` | Airline IATA directory | publicly readable per-doc (tested `AA`) |
| `airportsByIata/*` | Airport IATA directory | publicly readable per-doc |
| `airlineNotesBySt/*` | Per-airline non-rev notes | publicly readable per-doc |
| `flightEquipment/*` | Aircraft type directory | publicly readable per-doc |
| `conversations/*` | Flight comment threads | 404 for empty (rule allows, doc absent); readable where they exist |
| `tips/*` | Travel recommendations (restaurants, lounges, etc.) | 5+ docs listable (pagination not tested to completion) |
| `autoRequestRecords/*` | **Global** auto-request subscription configs — user uids, trigger schedules, blocked users | 500+ docs listable; flight metadata is NOT in the doc itself (likely derivable from the doc ID hash), but user-activity is |

### Airline-scoped (only MY airline == `st_SWA`)

| Query | Behavior |
|-------|----------|
| `trackedFlights where scheduledFlight.airlineStCode == "st_SWA"` | ✅ 500+ docs (historic WN load requests, all status=closed) |
| `trackedFlights where scheduledFlight.airlineStCode == "st_AAL/DAL/UAL/JBU/..."` | ❌ 403 — rules scope to own airline |
| `trackedFlights/{flightId_v3}` direct get, same-airline | ✅ readable (even if I'm not the creator) — exposes `currentLoadsReportId`, `reportAuthorList`, `seatsAvailabilityScore` |
| `trackedFlights/{flightId_v3}` direct get, other airline | ❌ 403 |

### Strictly user-scoped (me only)

| Path | Notes |
|------|-------|
| `users/{MY uid}` | My profile. Other uids → 403. |
| `userCredits/{MY uid}`, `userHints/{MY uid}`, `userRequestCounters/{MY uid}`, `userPriorityRequestCounters/{MY uid}`, `userMetrics/{MY uid}`, `userAchievements/{MY uid}`, `userFlightSearchHistory/{MY uid}` | my state only |
| `users/{MY uid}/pinnedFlights`, `users/{MY uid}/connectingFlights` | my subcollections only |
| `derivedLoadsReports where flightId == "<flight I created request on>"` | ✅ readable |
| `derivedLoadsReports/{docId}` direct get, my flight | ✅ readable |
| `derivedLoadsReports where flightId == "<other flight>"` | ❌ 403 (cross-checked with direct-get of `dVb4zStjhITceArRgLBB` — a WN862 load doc I don't own) |
| `trackedFlights/{flight_I_created}/statusUpdates` subcollection | readable only for flights I own |

### Explicitly denied for listing / querying

| Path | Error |
|------|-------|
| `LIST derivedLoadsReports` without a matching where-filter | 403 |
| `LIST trackedFlights` without airlineStCode filter | 403 |
| `runQuery` on `derivedLoadsReports`, `trackedFlights`, `scheduledFlights`, `flights`, `flightRecord`, `userRegistrations`, `userEmailVerifications`, `loadsReports` (as singular), `conversations` (list-only, 403) without proper predicates | 403 |
| Collection-group queries on sensitive collections | 403 |
| `scheduledFlights/{id}`, `flights/{id}`, `flightRecord/{id}` direct get for ANY flight | 403 |

## Other domains on stafftraveler.com (tested unauthenticated GET)

| URL | Response |
|-----|----------|
| `app.stafftraveler.com/` | web app (Next.js); redirects to `/login` if unauth |
| `share.stafftraveler.com/` | 200 HTML — share landing page (Next.js). Tried `/request/<id>`, `/flight/<id>`, `/loads/<id>`, `/r/<code>` → all 404 |
| `blog.stafftraveler.com/` | blog (not tested deeply) |
| `hotels.stafftraveler.com/` | hotels deals (WebView target) |
| `carrental.stafftraveler.com/` | car rentals (WebView target, URL pattern `<base>/mobile.html?...`) |
| `support.stafftraveler.com/` | support/help |
| `shop.stafftraveler.com/` | merch |
| `links.stafftraveler.com/` | 404 not-found page (linktree-style, but empty) |
| `webhooks.stafftraveler.com/` | Fastify API; `{error:"Not Found"}` on `/`; `/request-password-reset` returns `{error:"Invalid arguments"}` on empty body |
| `stafftraveler.com/r/<code>` | Returns generic HTML (any path) |
| `images.stafftraveler.com/avatars/*` | Public avatar CDN |

## Goal analysis: "find all filled requests"

- **For my own airline (WN/`st_SWA`):** ✅ possible — enumerate all `trackedFlights` with `airlineStCode == "st_SWA"` and `status == "closed"`. 500+ historic records. But actually reading the per-request **load data** (`openSeats.{first,business,economy}.count`, `staffListing.count`) still requires having personally called `createLoadsRequests` on each flight.
- **For other airlines:** ❌ blocked. The rule `scheduledFlight.airlineStCode == <your airline>` is the hard wall. Verified negatively across 9 airline codes (AAL/DAL/UAL/JBU/ACA/KLM/AFR/BAW/DLH).
- **Community-aggregated filled-request data, cross-airline:** not exposed anywhere I found. `autoRequestRecords` comes closest — it shows *who's auto-subscribing*, not what loads were reported.

The design is intentional: StaffTraveler's value proposition is a credit-gated access to crowd-sourced data on your own airline's routes. The rules don't let you enumerate outside that scope.

## Useful artifacts saved

- `nonRevAgreementsBySt.json` — complete 300-airline interline matrix
- `airlinesBySt_full.json` — ST code → airline directory (partial if stopped paging)
- `firestore_listen_targets_and_queries.txt` — all Listen-channel queries captured from the web client
- `derivedLoadsReports_AA2178_*.json` — single known-working load response capture
- `searchFlightsByRoute_DFW-LAS_*.json` / `createLoadsRequests_AA2178_*.json` — HTTP command captures
