Airline integration work: AirlineLoadService updates, docs, JSX scripts
- AirlineLoadService: pass airport DB for timezone-aware date strings, add browser-shaped headers for United, expand JetBlue/Alaska/Emirates signatures to take origin, log/parse fixes for Korean Air. - FlightsApp: build AirlineLoadService with the airport DB and inject it. - JSX: continued WebView-based fetcher work plus updated JSX_NOTES. - Docs: add AIRLINE_INTEGRATION_GUIDE.md, drop the old AIRLINE_API_SPEC.md, add api_docs/ (StaffTraveler reverse-engineering captures + findings). - Scripts: jsx_cdp_probe, jsx_live_monitor, jsx_swift_smoke for JSX protocol exploration. - .gitignore: exclude airlines/ (local-only APK/IPA reverse-engineering). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,291 @@
|
||||
# StaffTraveler API
|
||||
|
||||
Extracted from `com.stafftraveler.webview` v3.12.0 (build 1880000346).
|
||||
Source artifacts:
|
||||
- `apps/com.stafftraveler.webview_3.12.0-1880000346_apkcombo.com.xapk`
|
||||
- `/Users/m4mini/Desktop/code/airlines/extracted/stafftraveler/`
|
||||
- Hermes disassembly: `extracted/stafftraveler/hermes_out/bundle.hasm`
|
||||
|
||||
## Architecture
|
||||
|
||||
React Native + Hermes bytecode. All business logic lives in `assets/index.android.bundle`. Native Android is a thin RN shell (`MainActivity`, `MainApplication`).
|
||||
|
||||
**Stack**
|
||||
- **Firebase Auth** — user accounts, ID token issuance.
|
||||
- **Firebase Remote Config** — runtime-fetched config (API base URL, Typesense host/key, feature flags, etc).
|
||||
- **Firebase Firestore** — realtime store for flights, load requests, load reports, tips, user state.
|
||||
- **Firebase App Check** — registered (`FirebaseAppCheckKtxRegistrar`) and Play Integrity is bundled (`com.google.android.play.core.integrity`) — but the `/v1/commands/*` fetch path does NOT attach `X-Firebase-AppCheck` (see `createFirebaseCommand` disassembly at `bundle.hasm` offset `0x017ed673`). App Check enforcement on the HTTP API is **not observed**. Firestore rules may still consult App Check.
|
||||
- **Typesense** (`enhk2ji1vu6csxzrp-1.a1.typesense.net`) — flight search index. The API key is delivered via Remote Config, not hardcoded.
|
||||
- **Custom "commands" HTTP API** — `POST https://api.stafftraveler.com/v1/commands/<commandName>` — all mutating/reading operations go through this, auth'd with a Firebase ID token.
|
||||
|
||||
## Firebase Project Config (from `res/values/strings.xml`)
|
||||
|
||||
| Key | Value |
|
||||
|-----|-------|
|
||||
| Project ID | `stafftraveler-prod` |
|
||||
| Android App ID | `1:628258099825:android:5fb976ba6ad1bb05` |
|
||||
| Sender ID | `628258099825` |
|
||||
| Android API Key | `AIzaSyC2zG6ArnguzzdWsLYV1qjQznma0zl1Q0s` |
|
||||
| Maps API Key | `AIzaSyD2yg_WuGtrAC_fyyEwvAQgycMoP3Xf9cM` |
|
||||
| Storage Bucket | `stafftraveler-prod.appspot.com` |
|
||||
| Realtime DB | `https://stafftraveler-prod.firebaseio.com` |
|
||||
| Firestore | (default, project `stafftraveler-prod`) |
|
||||
|
||||
These are all public identifiers — Firebase security comes from rules + App Check, not from hiding them.
|
||||
|
||||
## Remote Config Keys
|
||||
|
||||
Fetched at startup; controls every external URL. Full shape (from `bundle.hasm` at line 140129):
|
||||
|
||||
| Key | Purpose |
|
||||
|-----|---------|
|
||||
| `api_commands_url` | Base for the commands API (production: `https://api.stafftraveler.com/v1/commands`) |
|
||||
| `search_typesense_hostname` | Typesense host |
|
||||
| `search_typesense_api_key` | Typesense search-only key |
|
||||
| `image_cdn_hostname` | Aircraft/airline image CDN |
|
||||
| `airline_logos_hostname` | Airline logo CDN |
|
||||
| `google_places_api_key`, `_details_url`, `_search_url`, `_photo_url` | Places integration |
|
||||
| `weather_service_url` (`api.weatherapi.com`), `weather_service_api_key` | Weather |
|
||||
| `location_service_url` (`pro.ip-api.com`), `location_service_api_key` | Geo-IP |
|
||||
| `google_web_client_id` | Google Sign-In |
|
||||
| `loads_jackpot_enabled_airline_ids`, `onboarding_invite_reward`, `notice_*`, `promotion_*` | Feature flags / content |
|
||||
|
||||
To get the real values, call Firebase Remote Config REST with the project config above:
|
||||
```
|
||||
POST https://firebaseremoteconfig.googleapis.com/v1/projects/stafftraveler-prod/namespaces/firebase:fetch?key=AIzaSyC2zG6ArnguzzdWsLYV1qjQznma0zl1Q0s
|
||||
```
|
||||
|
||||
## Auth
|
||||
|
||||
Standard Firebase Auth (email/password, Google, Apple, Facebook providers present in bundle strings). No custom auth flow.
|
||||
|
||||
To authenticate:
|
||||
1. Sign in via Firebase Auth REST with the `stafftraveler-prod` API key above, or via the Firebase iOS SDK configured for the same project.
|
||||
2. `getIdToken()` produces a JWT with ~1h TTL.
|
||||
3. Use the token as `Authorization: Bearer <idToken>` on every commands-API call.
|
||||
|
||||
App logic in `bundle.hasm` (`createFirebaseCommand` generator, line 1827439) checks `expirationTime` and calls `getIdToken()` right before sending, so token refresh is transparent.
|
||||
|
||||
## Commands API
|
||||
|
||||
**Base**: `https://api.stafftraveler.com/v1/commands/user` (from Remote Config `api_commands_url` — confirmed from captured traffic, `2026-04-22`)
|
||||
**Auth**: `Authorization: Bearer <firebase-id-token>`
|
||||
**Method**: `POST`
|
||||
**Headers**: `Content-Type: application/json`, `x-stafftraveler-client: mobile`
|
||||
**Body**: JSON — shape is command-specific
|
||||
**Response**: 200 OK with `{payload: {...}}` on success. Business errors raise `isError`/`isNetworkError` flags in the bundle.
|
||||
|
||||
Fetch options object literally constructed in the bundle (offset `0x017ed673`):
|
||||
```js
|
||||
{ method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + idToken }, body: JSON.stringify(payload) }
|
||||
```
|
||||
|
||||
Full endpoint pattern: `POST https://api.stafftraveler.com/v1/commands/user/<commandName>`
|
||||
|
||||
### Confirmed Commands (built via `createFirebaseCommand(name)`)
|
||||
|
||||
Endpoint = `POST {api_commands_url}/<command>`
|
||||
|
||||
| Command | Purpose |
|
||||
|---------|---------|
|
||||
| `searchFlightsByRoute` | Flight search by origin+destination+date |
|
||||
| `searchFlightsByCode` | Flight search by flight number + date |
|
||||
| `createLoadsRequests` | Submit one or more load requests for selected flights (spends credits) |
|
||||
| `deleteLoadsRequest` | Cancel a pending request |
|
||||
| `reopenLoadsRequest` | Reopen an expired/cancelled request |
|
||||
| `upgradeLoadsRequest` | Promote a request to Priority (uses extra credits) |
|
||||
| `requestLockForLoadsRequest` / `unlockLoadsRequest` | Cooperative lock: while you're typing a load answer, others can't also submit |
|
||||
| `submitLoadsReport` | Answer someone else's request (the reverse direction — you earn credits) |
|
||||
| `reviseLoadsReport` | Edit your answer |
|
||||
| `addComment` / `removeComment` / `reportComment` | Flight comments |
|
||||
| `addAirlineDetails` | Provide airline employee credentials for verification |
|
||||
| `addCreditsForPurchase` | IAP receipt validation → credits |
|
||||
| `setAirlineStCodes` | Set user's airline ("ST code" = StaffTraveler internal airline code) |
|
||||
|
||||
Command names are stored in the bundle as plain string identifiers; the full inventory is in `/tmp/st_fn_names.txt` (extracted function names).
|
||||
|
||||
### `searchFlightsByRoute` — confirmed shape (captured 2026-04-22)
|
||||
|
||||
Request body:
|
||||
```json
|
||||
{
|
||||
"originIata": "DFW",
|
||||
"destinationIata": "LAS",
|
||||
"dates": ["2026-04-22"],
|
||||
"maxConnections": 0,
|
||||
"allowNearbyDepartures": false,
|
||||
"allowNearbyArrivals": false,
|
||||
"payloadType": "passenger",
|
||||
"includeMultipleCarriers": false
|
||||
}
|
||||
```
|
||||
|
||||
Field corrections from the bytecode guess: the key I thought was `stops` is actually `maxConnections`; `payloadType` takes the literal value `"passenger"` (not `"route"`); `dates` is an array of `YYYY-MM-DD` strings.
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"payload": {
|
||||
"directFlights": [ /* array of flight objects */ ],
|
||||
"connectingFlights": [],
|
||||
"numberOfDiscardedFlights": 0,
|
||||
"settingsFilteredCount": 0,
|
||||
"messages": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Each flight object has three IDs (`id`, `id_v2`, `id_v3` — the `id_v3` is human-readable like `AA_2998_DFW_2026_04_22`), airline info (`airlineId`, `airlineStCode`, `flightCode`, `flightNumber`), local/UTC times, equipment (`flightEquipmentId`, `flightEquipmentIata`), `durationMinutes`, `isCargoFlight`, `seatsTotal`, and `seatsByClass: {first, business, premiumEconomy, economy}`.
|
||||
|
||||
**Important:** `seatsByClass` is the **aircraft cabin configuration**, not current availability. Current load data comes from Firestore (see below), not this response.
|
||||
|
||||
See `api_docs/stafftraveler_captures/searchFlightsByRoute_DFW-LAS_*.json` for the full live capture.
|
||||
|
||||
### `searchFlightsByCode` — payload shape
|
||||
|
||||
From caller at `bundle.hasm:1948510` (Function #38848):
|
||||
|
||||
```json
|
||||
{
|
||||
"flightCode": "UA123",
|
||||
"dates": ["<ISO date>", ...]
|
||||
}
|
||||
```
|
||||
|
||||
Returns flights matching the flight code on each listed date.
|
||||
|
||||
### `createLoadsRequests` — confirmed shape (captured 2026-04-22)
|
||||
|
||||
Request body is a **bare JSON array** of flight objects (no outer wrapper). Each element is the full flight object as returned by `searchFlightsByRoute`, with one added field: `isPriorityRequest: bool`.
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "a23d6f8379f5109eddeb5bbf78bdccc1",
|
||||
"id_v2": "0d9d0702b203d224ace3e6eb94531dde",
|
||||
"id_v3": "AA_2178_DFW_2026_04_22",
|
||||
"airlineId": "AA",
|
||||
"flightNumber": 2178,
|
||||
"flightCode": "AA2178",
|
||||
"departureAirportId": "DFW",
|
||||
"arrivalAirportId": "LAS",
|
||||
"departureTimeLocal": "2026-04-22T16:45:00-05:00",
|
||||
"departureTimeUtc": "2026-04-22T21:45:00Z",
|
||||
"arrivalTimeLocal": "2026-04-22T17:46:00-07:00",
|
||||
"arrivalTimeUtc": "2026-04-23T00:46:00Z",
|
||||
"flightEquipmentId": "32Q",
|
||||
"durationMinutes": 181,
|
||||
"isCargoFlight": false,
|
||||
"airlineStCode": "st_AAL",
|
||||
"departureAirportIata": "DFW",
|
||||
"arrivalAirportIata": "LAS",
|
||||
"flightEquipmentIata": "32Q",
|
||||
"seatsTotal": 196,
|
||||
"seatsByClass": {"first": 0, "business": 20, "premiumEconomy": 0, "economy": 176},
|
||||
"isPriorityRequest": false
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"payload": {
|
||||
"numberOfRequests": 0,
|
||||
"numberOfRequestsPaidFor": 0,
|
||||
"numberOfRequestsPaidForPriority": 0,
|
||||
"hasSufficientCredits": true,
|
||||
"verifiedRequests": [
|
||||
{
|
||||
"flightId": "AA_2178_DFW_2026_04_22",
|
||||
"isExisting": true,
|
||||
"isDuplicateForUser": true,
|
||||
"isRequestUpdateForUser": false,
|
||||
"hasDeparted": false,
|
||||
"isCancelled": false,
|
||||
"isPriorityRequest": false,
|
||||
"hasRecentLoads": false,
|
||||
"scheduledFlight": { /* echoed full flight obj */ }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Idempotent per (user, flight):** on duplicate submission, `numberOfRequests: 0`, `isDuplicateForUser: true`, **no credit charged**. This makes the command safe to replay as a "what's the current state of my request for flight X" probe — you just pay for the first request on any given flight.
|
||||
|
||||
**`hasRecentLoads`** is the flag the app uses to decide whether load data is already available. When true, the Firestore listener is expected to deliver load-report documents; when false, there's nothing to show until someone in the community answers.
|
||||
|
||||
See `api_docs/stafftraveler_captures/createLoadsRequests_AA2178_*.json` for the full live capture.
|
||||
|
||||
## Firestore Collections (realtime)
|
||||
|
||||
Once a request is created, load results stream back via Firestore listeners. Listener constructors in the bundle:
|
||||
|
||||
| Listener | What it subscribes to |
|
||||
|----------|----------------------|
|
||||
| `TrackedFlightsStoreListener` (`bundle.hasm:253969`) | The user's currently-tracked flights (including ones with pending requests) |
|
||||
| `OpenRequestsStoreListener` (`bundle.hasm:443724`) | The user's open requests, filtered by `useUserAirlineStCodes()` |
|
||||
| `ConnectingFlightsStoreListener` | Connecting-flight variants |
|
||||
| `PinnedFlightsStoreListener` | User-pinned flights |
|
||||
| `PriorityRequestCounterStoreListener` | Priority request quota counter |
|
||||
| `OpenRequestsCountListener` | Open request count for UI badge |
|
||||
| `DerivedLoadsReportsStoreListener` | Reports on flights the user is answering |
|
||||
| `CreditsListener` / `UserSessionsListener` / `UserStoreListener` | Account state |
|
||||
| `RemoteConfigListener` | Remote Config changes |
|
||||
|
||||
Exact collection paths are not plain strings in the bundle — they're built from user state (e.g. `users/{uid}/flightRequests/{id}` or `users/{uid}/trackedFlights/{id}`). To get them, run Firestore in debug (iOS SDK logs every collection path) or capture with Firebase Local Emulator proxy.
|
||||
|
||||
## Typesense (search infrastructure)
|
||||
|
||||
Not directly hit by the client for flight search — `searchFlightsByRoute` goes through the commands API, which internally queries Typesense. However, the bundle also contains a `typesense-instantsearch-adapter`, and strings like `searchTypesenseAndAdapt`, `searchTypesenseForFacetValuesAndAdapt`, `x-typesense-api-key` (`bundle.hasm:1995183`) suggest **direct Typesense queries are used for some facets** (probably airline filtering, autocomplete on airports/airlines).
|
||||
|
||||
Host: `enhk2ji1vu6csxzrp-1.a1.typesense.net`
|
||||
Key: from Remote Config `search_typesense_api_key` (search-only, scoped, safe to embed).
|
||||
|
||||
## Integration plan for the iOS Flights app
|
||||
|
||||
The cleanest path — do not build a parallel HTTP client, just use Firebase directly:
|
||||
|
||||
1. **Embed the `stafftraveler-prod` Firebase config** in a second `FirebaseApp` instance (iOS supports multiple named apps). Use the API key + app ID + sender ID from above. Name it something like `StaffTravelerFirebaseApp` so it doesn't collide with Flights' own Firebase project.
|
||||
2. **Sign in** with the user's StaffTraveler credentials using `Auth.auth(app: stApp).signIn(withEmail:password:)`. Store the refresh token in Keychain.
|
||||
3. **Fetch Remote Config** once on first use to get `api_commands_url`, `search_typesense_api_key`, etc.
|
||||
4. **Search a route** → `POST https://api.stafftraveler.com/v1/commands/searchFlightsByRoute` with the payload shape documented above and `Authorization: Bearer <idToken>`.
|
||||
5. **Request loads** → `POST .../createLoadsRequests` with the flight IDs from step 4.
|
||||
6. **Listen for answers** → `Firestore.firestore(app: stApp).collection("users/\(uid)/flightRequests").document(requestId).addSnapshotListener { ... }` (exact path TBD — confirm via traffic capture).
|
||||
|
||||
### Firestore is the answer — schema captured (2026-04-22)
|
||||
|
||||
Loads and all real-time state live in Firestore, queried via the `Listen/channel` long-poll transport. Mobile iOS uses the gRPC variant (hard to mitm); the `app.stafftraveler.com` web client uses plain HTTP long-poll, which is fully inspectable. Playwright capture + URL-decoding gave us every `addTarget` body.
|
||||
|
||||
**Full schema and query recipes:** see `api_docs/stafftraveler_captures/firestore_schema.md` and the raw capture at `api_docs/stafftraveler_captures/firestore_listen_targets_and_queries.txt`.
|
||||
|
||||
**The one query that renders loads:**
|
||||
```
|
||||
collection: derivedLoadsReports
|
||||
where: flightId == "{id_v3}" (e.g. "AA_2178_DFW_2026_04_22")
|
||||
orderBy: createdAt DESC
|
||||
```
|
||||
|
||||
Returns docs containing `openSeats.{first,business,premiumEconomy,eco}.count`, `staffListing.count`, `creatorName`, `createdAt`, `seatsAvailabilityScore`, etc. Example at `api_docs/stafftraveler_captures/derivedLoadsReports_AA2178_response.json`.
|
||||
|
||||
**Required REST headers** (earlier runQuery attempts were 403 because the API key wasn't attached):
|
||||
```
|
||||
Authorization: Bearer <firebase id token>
|
||||
X-Firebase-GMPID: 1:628258099825:web:35b20eaab4d441894041d0
|
||||
?key=AIzaSyD82Hiqx-jAbHF8faMGSveqLw8CdX8a-Uc (as URL query, web key)
|
||||
```
|
||||
|
||||
### Remaining nuances
|
||||
|
||||
1. **Account eligibility** — `addAirlineDetails` still required for the account to participate. Your existing account is verified (WN / `st_SWA`), so no blocker.
|
||||
2. **Credits / access model** — `createLoadsRequests` charges for *new* requests only. Idempotent per `(user, flight)` — `isDuplicateForUser: true` returns current state with no charge. **Firestore `derivedLoadsReports` reads are gated by an active `trackedFlights/{flightId}` doc**, which is only created by `createLoadsRequests`. So you must spend 1 credit per *new* flight to unlock load reads for it; after that, reads are free and unlimited. You cannot peek at other flights' loads without requesting.
|
||||
3. **App Check** — not enforced on commands API or Firestore for this project (confirmed: web client makes all these requests with just ID token + API key).
|
||||
|
||||
### Integration for iOS Flights app (final)
|
||||
|
||||
- Secondary `FirebaseApp` configured against `stafftraveler-prod` with the web API key + GMPID above.
|
||||
- `signInWithEmailAndPassword` → persist refresh token.
|
||||
- `Firestore.firestore(app: stApp).collection("derivedLoadsReports").whereField("flightId", isEqualTo: flightIdV3).order(by: "createdAt", descending: true).addSnapshotListener { ... }` — realtime loads for any flight you know the id_v3 of.
|
||||
- Plus `trackedFlights`, `airlinesBySt`, `airportsByIata`, `flightEquipment`, `conversations/{flightId}` subscriptions — full recipes in `firestore_schema.md`.
|
||||
- Writes (request loads, submit report) keep using `POST https://api.stafftraveler.com/v1/commands/user/<cmd>`.
|
||||
@@ -0,0 +1,108 @@
|
||||
# StaffTraveler API captures
|
||||
|
||||
Real request/response pairs captured from the iOS app `com.stafftraveler.webview` v3.12.0 (build 1880000346) against `api.stafftraveler.com` on 2026-04-22. All captures are by user `3NNPesQMiMRNYnPmuQzh6w2YKyh1` (`stafftraveler@treymail.com`, WN/Southwest, airline ST code `st_SWA`).
|
||||
|
||||
## Auth
|
||||
|
||||
All command-API calls are authenticated with:
|
||||
```
|
||||
Authorization: Bearer <Firebase ID token>
|
||||
```
|
||||
Token issued by `securetoken.google.com/stafftraveler-prod`, `aud=stafftraveler-prod`, ~1h TTL. The captured tokens are already expired — do not try to reuse them. To get a new one, sign in via Firebase Auth REST against API key `AIzaSyC2zG6ArnguzzdWsLYV1qjQznma0zl1Q0s` (needs `X-Android-Package` + `X-Android-Cert` headers or iOS SDK).
|
||||
|
||||
Only standard headers are sent. No App Check. No SSL pinning. No device/client signing beyond the ID token.
|
||||
|
||||
## Endpoint path pattern
|
||||
|
||||
```
|
||||
POST https://api.stafftraveler.com/v1/commands/user/<commandName>
|
||||
```
|
||||
|
||||
Note the `user/` segment — the `api_commands_url` Remote Config value is `https://api.stafftraveler.com/v1/commands/user`, and the command name is appended with `/`. I initially documented this as `/v1/commands/<cmd>` which is wrong.
|
||||
|
||||
## Files
|
||||
|
||||
### `searchFlightsByRoute_DFW-LAS_*.json`
|
||||
DFW→LAS direct-only search for 2026-04-22.
|
||||
|
||||
Request body:
|
||||
```json
|
||||
{
|
||||
"originIata": "DFW",
|
||||
"destinationIata": "LAS",
|
||||
"dates": ["2026-04-22"],
|
||||
"maxConnections": 0,
|
||||
"allowNearbyDepartures": false,
|
||||
"allowNearbyArrivals": false,
|
||||
"payloadType": "passenger",
|
||||
"includeMultipleCarriers": false
|
||||
}
|
||||
```
|
||||
|
||||
Response: `{payload: {directFlights: [...], connectingFlights: [...], numberOfDiscardedFlights, messages, settingsFilteredCount}}`.
|
||||
|
||||
Each flight carries three IDs (`id`, `id_v2`, `id_v3`), airline info, local/UTC times, equipment, and a `seatsByClass` **aircraft configuration** (NOT current availability — that's a different read path entirely).
|
||||
|
||||
Corrections vs the api_doc's initial guesses:
|
||||
- `stops` → actually `maxConnections`
|
||||
- `payloadType: "route"` → actually `"passenger"`
|
||||
- No `origin`/`destination`/`date` fields — only the `*Iata` variants + `dates` array.
|
||||
|
||||
### `createLoadsRequests_AA2178_*.json`
|
||||
Request loads for AA2178 DFW→LAS 2026-04-22.
|
||||
|
||||
**Request body** is a bare JSON array of flight objects (no outer wrapper). Each flight is the complete flight object from the search response with one added field: `isPriorityRequest: bool`.
|
||||
|
||||
**Response shape:**
|
||||
```json
|
||||
{
|
||||
"payload": {
|
||||
"numberOfRequests": 0,
|
||||
"numberOfRequestsPaidFor": 0,
|
||||
"numberOfRequestsPaidForPriority": 0,
|
||||
"hasSufficientCredits": true,
|
||||
"verifiedRequests": [
|
||||
{
|
||||
"flightId": "AA_2178_DFW_2026_04_22",
|
||||
"isExisting": true,
|
||||
"isDuplicateForUser": true,
|
||||
"isRequestUpdateForUser": false,
|
||||
"hasDeparted": false,
|
||||
"isCancelled": false,
|
||||
"isPriorityRequest": false,
|
||||
"hasRecentLoads": false,
|
||||
"scheduledFlight": { /* full flight obj */ }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Dedup behavior (important)
|
||||
|
||||
On this capture, `numberOfRequests: 0` and `isDuplicateForUser: true` — the server recognized this user had already requested this flight and **did NOT charge a credit**. Replaying the same `createLoadsRequests` payload is safe (idempotent).
|
||||
|
||||
This means:
|
||||
- `numberOfRequests` is the count of *newly created* requests in this call.
|
||||
- `isDuplicateForUser` tells you whether the flight already had an open request for this user.
|
||||
- **No credit is spent on duplicates.** Useful for polling the current request state without cost.
|
||||
|
||||
### `hasRecentLoads: false` — where loads actually come from
|
||||
|
||||
`hasRecentLoads` is the flag the app uses to decide whether load data is available for display. It's `false` here because nobody has submitted loads for AA2178 yet. The actual load data (when `hasRecentLoads: true`) does NOT come through the commands API — it arrives via a Firestore realtime listener on a doc that the user's account has permission to read.
|
||||
|
||||
To find the exact Firestore path when a flight has loads, one more capture is needed: tap a flight that already has loads populated, and grab the `firestore.googleapis.com/.../Listen/channel` traffic. That will show the watched doc path.
|
||||
|
||||
## Known error shapes
|
||||
|
||||
Not captured yet:
|
||||
- What happens when you send an invalid flight ID
|
||||
- What `createLoadsRequests` returns when credits are insufficient (`hasSufficientCredits: false`)
|
||||
- What `numberOfRequests > 0` responses look like (fresh request, credit actually spent)
|
||||
|
||||
## Replay safety
|
||||
|
||||
To replay any of these:
|
||||
1. Grab a fresh token from the app or by re-signing-in.
|
||||
2. Swap it into the curl.
|
||||
3. For `createLoadsRequests`, same payload = dedup-safe. Different payload = may charge credits.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,31 @@
|
||||
[
|
||||
{
|
||||
"id": "a23d6f8379f5109eddeb5bbf78bdccc1",
|
||||
"id_v2": "0d9d0702b203d224ace3e6eb94531dde",
|
||||
"id_v3": "AA_2178_DFW_2026_04_22",
|
||||
"airlineId": "AA",
|
||||
"flightNumber": 2178,
|
||||
"flightCode": "AA2178",
|
||||
"departureAirportId": "DFW",
|
||||
"arrivalAirportId": "LAS",
|
||||
"departureTimeLocal": "2026-04-22T16:45:00-05:00",
|
||||
"departureTimeUtc": "2026-04-22T21:45:00Z",
|
||||
"arrivalTimeLocal": "2026-04-22T17:46:00-07:00",
|
||||
"arrivalTimeUtc": "2026-04-23T00:46:00Z",
|
||||
"flightEquipmentId": "32Q",
|
||||
"durationMinutes": 181,
|
||||
"isCargoFlight": false,
|
||||
"airlineStCode": "st_AAL",
|
||||
"departureAirportIata": "DFW",
|
||||
"arrivalAirportIata": "LAS",
|
||||
"flightEquipmentIata": "32Q",
|
||||
"seatsTotal": 196,
|
||||
"seatsByClass": {
|
||||
"first": 0,
|
||||
"business": 20,
|
||||
"premiumEconomy": 0,
|
||||
"economy": 176
|
||||
},
|
||||
"isPriorityRequest": false
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1 @@
|
||||
{"payload":{"numberOfRequests":0,"verifiedRequests":[{"flightId":"AA_2178_DFW_2026_04_22","isExisting":true,"isDuplicateForUser":true,"isRequestUpdateForUser":false,"hasDeparted":false,"isCancelled":false,"isPriorityRequest":false,"hasRecentLoads":false,"scheduledFlight":{"id":"a23d6f8379f5109eddeb5bbf78bdccc1","id_v2":"0d9d0702b203d224ace3e6eb94531dde","id_v3":"AA_2178_DFW_2026_04_22","airlineId":"AA","flightCode":"AA2178","flightNumber":2178,"departureAirportId":"DFW","arrivalAirportId":"LAS","departureTimeLocal":"2026-04-22T16:45:00-05:00","arrivalTimeLocal":"2026-04-22T17:46:00-07:00","departureTimeUtc":"2026-04-22T21:45:00Z","arrivalTimeUtc":"2026-04-23T00:46:00Z","flightEquipmentId":"32Q","durationMinutes":181,"isCargoFlight":false,"isPriorityRequest":false,"airlineStCode":"st_AAL","departureAirportIata":"DFW","arrivalAirportIata":"LAS","flightEquipmentIata":"32Q","seatsTotal":196,"seatsByClass":{"first":0,"business":20,"premiumEconomy":0,"economy":176}}}],"hasSufficientCredits":true,"numberOfRequestsPaidFor":0,"numberOfRequestsPaidForPriority":0}}
|
||||
@@ -0,0 +1,12 @@
|
||||
HTTP/2 200
|
||||
access-control-allow-credentials: true
|
||||
cache-control: no-store
|
||||
content-type: application/json
|
||||
vary: Origin
|
||||
x-cloud-trace-context: 7f1fda60d8d56f8106053d5e7df58556
|
||||
date: Wed, 22 Apr 2026 18:40:54 GMT
|
||||
server: Google Frontend
|
||||
content-length: 1070
|
||||
via: 1.1 google
|
||||
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"structuredQuery": {
|
||||
"from": [{"collectionId": "derivedLoadsReports"}],
|
||||
"where": {
|
||||
"fieldFilter": {
|
||||
"field": {"fieldPath": "flightId"},
|
||||
"op": "EQUAL",
|
||||
"value": {"stringValue": "AA_2178_DFW_2026_04_22"}
|
||||
}
|
||||
},
|
||||
"orderBy": [
|
||||
{"field": {"fieldPath": "createdAt"}, "direction": "DESCENDING"},
|
||||
{"field": {"fieldPath": "__name__"}, "direction": "DESCENDING"}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
[{
|
||||
"document": {
|
||||
"name": "projects/stafftraveler-prod/databases/(default)/documents/derivedLoadsReports/xskQc2a2oAVAwZk3Rfhg",
|
||||
"fields": {
|
||||
"flightId": {
|
||||
"stringValue": "AA_2178_DFW_2026_04_22"
|
||||
},
|
||||
"openSeats": {
|
||||
"mapValue": {
|
||||
"fields": {
|
||||
"first": {
|
||||
"mapValue": {
|
||||
"fields": {
|
||||
"count": {
|
||||
"integerValue": "3"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"eco": {
|
||||
"mapValue": {
|
||||
"fields": {
|
||||
"count": {
|
||||
"integerValue": "9"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"staffListing": {
|
||||
"mapValue": {
|
||||
"fields": {
|
||||
"type": {
|
||||
"stringValue": "available"
|
||||
},
|
||||
"count": {
|
||||
"integerValue": "6"
|
||||
},
|
||||
"countByClass": {
|
||||
"mapValue": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responseTimeSeconds": {
|
||||
"integerValue": "196"
|
||||
},
|
||||
"numberOfCreditsRewarded": {
|
||||
"integerValue": "1"
|
||||
},
|
||||
"isFlagged": {
|
||||
"booleanValue": false
|
||||
},
|
||||
"seatsAvailabilityScore": {
|
||||
"doubleValue": 0.79
|
||||
},
|
||||
"hasClosedRequest": {
|
||||
"booleanValue": true
|
||||
},
|
||||
"isJackpotHit": {
|
||||
"booleanValue": false
|
||||
},
|
||||
"isPriorityRequest": {
|
||||
"booleanValue": false
|
||||
},
|
||||
"expireAt": {
|
||||
"timestampValue": "2026-07-21T21:45:00Z"
|
||||
},
|
||||
"createdAt": {
|
||||
"timestampValue": "2026-04-22T18:42:51.313Z"
|
||||
},
|
||||
"creatorName": {
|
||||
"stringValue": "M"
|
||||
},
|
||||
"creatorImageUrl": {
|
||||
"stringValue": "https://images.stafftraveler.com/avatars/happysuitcase.png"
|
||||
},
|
||||
"creatorImagePath": {
|
||||
"stringValue": "avatars/happysuitcase.png"
|
||||
},
|
||||
"isFlaggedConfirmed": {
|
||||
"booleanValue": false
|
||||
}
|
||||
},
|
||||
"createTime": "2026-04-22T18:42:51.422202Z",
|
||||
"updateTime": "2026-04-22T18:42:51.530333Z"
|
||||
},
|
||||
"readTime": "2026-04-22T19:11:47.260445Z"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,90 @@
|
||||
[POST] https://firestore.googleapis.com/google.firestore.v1.Firestore/Listen/channel?VER=8&database=projects%2Fstafftraveler-prod%2Fdatabases%2F(default)&RID=80724&CVER=22&X-HTTP-Session-Id=gsessionid&zx=rtxwctgfqxp6&t=1 => [200]
|
||||
Request body: headers=X-Goog-Api-Client%3Agl-js%2F%20fire%2F12.12.0%0D%0AContent-Type%3Atext%2Fplain%0D%0AX-Firebase-GMPID%3A1%3A628258099825%3Aweb%3A35b20eaab4d441894041d0%0D%0Ax-goog-api-key%3AAIzaSyD82Hiqx-jAbHF8faMGSveqLw8CdX8a-Uc%0D%0A&count=1&ofs=0&req0___data__=%7B%22database%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%22%2C%22addTarget%22%3A%7B%22documents%22%3A%7B%22documents%22%3A%5B%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%2Fdocuments%2F__system%2Fmaintenance%22%5D%7D%2C%22targetId%22%3A2%7D%7D
|
||||
[POST] https://firestore.googleapis.com/google.firestore.v1.Firestore/Listen/channel?VER=8&database=projects%2Fstafftraveler-prod%2Fdatabases%2F(default)&RID=9158&CVER=22&X-HTTP-Session-Id=gsessionid&zx=f83g0aueufis&t=1 => [200]
|
||||
Request body: headers=X-Goog-Api-Client%3Agl-js%2F%20fire%2F12.12.0%0D%0AContent-Type%3Atext%2Fplain%0D%0AX-Firebase-GMPID%3A1%3A628258099825%3Aweb%3A35b20eaab4d441894041d0%0D%0AAuthorization%3ABearer%20eyJhbGciOiJSUzI1NiIsImtpZCI6IjNiMDk1NzQ3YmY4MzMxZWE0YWQ1M2YzNzBjNjMyNjAxNzliMGQyM2EiLCJ0eXAiOiJKV1QifQ.eyJpc19yZWdpc3RlcmVkIjp0cnVlLCJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20vc3RhZmZ0cmF2ZWxlci1wcm9kIiwiYXVkIjoic3RhZmZ0cmF2ZWxlci1wcm9kIiwiYXV0aF90aW1lIjoxNzc2ODg0OTM2LCJ1c2VyX2lkIjoiM05OUGVzUU1pTVJOWW5QbXVRemg2dzJZS3loMSIsInN1YiI6IjNOTlBlc1FNaU1STlluUG11UXpoNncyWUt5aDEiLCJpYXQiOjE3NzY4ODQ5MzYsImV4cCI6MTc3Njg4ODUzNiwiZW1haWwiOiJzdGFmZnRyYXZlbGVyQHRyZXltYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwiZmlyZWJhc2UiOnsiaWRlbnRpdGllcyI6eyJlbWFpbCI6WyJzdGFmZnRyYXZlbGVyQHRyZXltYWlsLmNvbSJdfSwic2lnbl9pbl9wcm92aWRlciI6InBhc3N3b3JkIn19.Oi2cpketCmgJ7ND8R8SY4FNdeySoWOF2Bgzwptb1qopDDa4eEvJrXutcvrIP408MhOedXOVdSSB4J4gLZ3w3nZA939N-3DHj-rREpraF2XtuabOnragy2e9FoSSWJsB1chyvCidBh1b-b53AzdnFV3kspwtr5C7xoxSZOf32p5mwIENdFtBZg30wjRd3ZdNIbSiVbUouUcQIRiIdPdwtYG8wfAYztonWwW4K8hK4hN5LK4v6PitFOpC70Baoh-4LpfCEPGrk5zxrbPMOc95ydgWCL8Av6eFRiHJgMd4juUvzOwOOJ_77z7QCSRLwpu5XKc9Em2D5l0_UiEyn79SQfg%0D%0Ax-goog-api-key%3AAIzaSyD82Hiqx-jAbHF8faMGSveqLw8CdX8a-Uc%0D%0A&count=1&ofs=0&req0___data__=%7B%22database%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%22%2C%22addTarget%22%3A%7B%22documents%22%3A%7B%22documents%22%3A%5B%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%2Fdocuments%2F__system%2Fmaintenance%22%5D%7D%2C%22targetId%22%3A2%2C%22resumeToken%22%3A%22CgkIh8rNkpSClAM%3D%22%2C%22expectedCount%22%3A1%7D%7D
|
||||
[POST] https://firestore.googleapis.com/google.firestore.v1.Firestore/Listen/channel?VER=8&database=projects%2Fstafftraveler-prod%2Fdatabases%2F(default)&gsessionid=HFF5EogcWeK0-zCr8BqQiP1LG6gZwKIpFMpTR4_G01uQgOSO1g6VYA&SID=a-XMXYp-Vwc6W3fSsZRnKA&RID=9159&AID=4&zx=d2hrtufyduql&t=1 => [200]
|
||||
Request body: count=1&ofs=1&req0___data__=%7B%22database%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%22%2C%22addTarget%22%3A%7B%22documents%22%3A%7B%22documents%22%3A%5B%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%2Fdocuments%2Fusers%2F3NNPesQMiMRNYnPmuQzh6w2YKyh1%22%5D%7D%2C%22targetId%22%3A4%7D%7D
|
||||
[POST] https://firestore.googleapis.com/google.firestore.v1.Firestore/Listen/channel?VER=8&database=projects%2Fstafftraveler-prod%2Fdatabases%2F(default)&gsessionid=HFF5EogcWeK0-zCr8BqQiP1LG6gZwKIpFMpTR4_G01uQgOSO1g6VYA&SID=a-XMXYp-Vwc6W3fSsZRnKA&RID=9160&AID=8&zx=tobs2rmzgvt7&t=1 => [200]
|
||||
Request body: count=1&ofs=2&req0___data__=%7B%22database%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%22%2C%22removeTarget%22%3A4%7D
|
||||
[POST] https://firestore.googleapis.com/google.firestore.v1.Firestore/Listen/channel?VER=8&database=projects%2Fstafftraveler-prod%2Fdatabases%2F(default)&gsessionid=HFF5EogcWeK0-zCr8BqQiP1LG6gZwKIpFMpTR4_G01uQgOSO1g6VYA&SID=a-XMXYp-Vwc6W3fSsZRnKA&RID=9161&AID=8&zx=gv4pfepp0iut&t=1 => [200]
|
||||
Request body: count=1&ofs=3&req0___data__=%7B%22database%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%22%2C%22addTarget%22%3A%7B%22documents%22%3A%7B%22documents%22%3A%5B%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%2Fdocuments%2Fusers%2F3NNPesQMiMRNYnPmuQzh6w2YKyh1%22%5D%7D%2C%22targetId%22%3A4%2C%22resumeToken%22%3A%22CgkIoKrGm5SClAM%3D%22%2C%22expectedCount%22%3A1%7D%7D
|
||||
[POST] https://firestore.googleapis.com/google.firestore.v1.Firestore/Listen/channel?VER=8&database=projects%2Fstafftraveler-prod%2Fdatabases%2F(default)&gsessionid=HFF5EogcWeK0-zCr8BqQiP1LG6gZwKIpFMpTR4_G01uQgOSO1g6VYA&SID=a-XMXYp-Vwc6W3fSsZRnKA&RID=9162&AID=8&zx=d0m0hj89m00m&t=1 => [200]
|
||||
Request body: count=1&ofs=4&req0___data__=%7B%22database%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%22%2C%22addTarget%22%3A%7B%22documents%22%3A%7B%22documents%22%3A%5B%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%2Fdocuments%2FuserHints%2F3NNPesQMiMRNYnPmuQzh6w2YKyh1%22%5D%7D%2C%22targetId%22%3A6%7D%7D
|
||||
[POST] https://firestore.googleapis.com/google.firestore.v1.Firestore/Listen/channel?VER=8&database=projects%2Fstafftraveler-prod%2Fdatabases%2F(default)&gsessionid=HFF5EogcWeK0-zCr8BqQiP1LG6gZwKIpFMpTR4_G01uQgOSO1g6VYA&SID=a-XMXYp-Vwc6W3fSsZRnKA&RID=9163&AID=8&zx=ir7thnhgqtrz&t=1 => [200]
|
||||
Request body: count=1&ofs=5&req0___data__=%7B%22database%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%22%2C%22addTarget%22%3A%7B%22documents%22%3A%7B%22documents%22%3A%5B%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%2Fdocuments%2FuserRequestCounters%2F3NNPesQMiMRNYnPmuQzh6w2YKyh1%22%5D%7D%2C%22targetId%22%3A8%7D%7D
|
||||
[POST] https://firestore.googleapis.com/google.firestore.v1.Firestore/Listen/channel?VER=8&database=projects%2Fstafftraveler-prod%2Fdatabases%2F(default)&gsessionid=HFF5EogcWeK0-zCr8BqQiP1LG6gZwKIpFMpTR4_G01uQgOSO1g6VYA&SID=a-XMXYp-Vwc6W3fSsZRnKA&RID=9164&AID=8&zx=w1dq4sb4jwq5&t=1 => [200]
|
||||
Request body: count=1&ofs=6&req0___data__=%7B%22database%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%22%2C%22addTarget%22%3A%7B%22documents%22%3A%7B%22documents%22%3A%5B%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%2Fdocuments%2FuserFlightSearchHistory%2F3NNPesQMiMRNYnPmuQzh6w2YKyh1%22%5D%7D%2C%22targetId%22%3A10%7D%7D
|
||||
[POST] https://firestore.googleapis.com/google.firestore.v1.Firestore/Listen/channel?VER=8&database=projects%2Fstafftraveler-prod%2Fdatabases%2F(default)&gsessionid=HFF5EogcWeK0-zCr8BqQiP1LG6gZwKIpFMpTR4_G01uQgOSO1g6VYA&SID=a-XMXYp-Vwc6W3fSsZRnKA&RID=9165&AID=8&zx=ecl2ritbm1hb&t=1 => [200]
|
||||
Request body: count=1&ofs=7&req0___data__=%7B%22database%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%22%2C%22addTarget%22%3A%7B%22documents%22%3A%7B%22documents%22%3A%5B%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%2Fdocuments%2FuserMetrics%2F3NNPesQMiMRNYnPmuQzh6w2YKyh1%22%5D%7D%2C%22targetId%22%3A12%7D%7D
|
||||
[POST] https://firestore.googleapis.com/google.firestore.v1.Firestore/Listen/channel?VER=8&database=projects%2Fstafftraveler-prod%2Fdatabases%2F(default)&gsessionid=HFF5EogcWeK0-zCr8BqQiP1LG6gZwKIpFMpTR4_G01uQgOSO1g6VYA&SID=a-XMXYp-Vwc6W3fSsZRnKA&RID=9166&AID=8&zx=wvohx872im0n&t=1 => [200]
|
||||
Request body: count=1&ofs=8&req0___data__=%7B%22database%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%22%2C%22addTarget%22%3A%7B%22query%22%3A%7B%22structuredQuery%22%3A%7B%22from%22%3A%5B%7B%22collectionId%22%3A%22autoRequestRecords%22%7D%5D%2C%22where%22%3A%7B%22compositeFilter%22%3A%7B%22op%22%3A%22AND%22%2C%22filters%22%3A%5B%7B%22fieldFilter%22%3A%7B%22field%22%3A%7B%22fieldPath%22%3A%22subscribedUserIds%22%7D%2C%22op%22%3A%22ARRAY_CONTAINS%22%2C%22value%22%3A%7B%22stringValue%22%3A%223NNPesQMiMRNYnPmuQzh6w2YKyh1%22%7D%7D%7D%2C%7B%22fieldFilter%22%3A%7B%22field%22%3A%7B%22fieldPath%22%3A%22isActive%22%7D%2C%22op%22%3A%22EQUAL%22%2C%22value%22%3A%7B%22booleanValue%22%3Atrue%7D%7D%7D%5D%7D%7D%2C%22orderBy%22%3A%5B%7B%22field%22%3A%7B%22fieldPath%22%3A%22__name__%22%7D%2C%22direction%22%3A%22ASCENDING%22%7D%5D%7D%2C%22parent%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%2Fdocuments%22%7D%2C%22targetId%22%3A14%7D%7D
|
||||
[POST] https://firestore.googleapis.com/google.firestore.v1.Firestore/Listen/channel?VER=8&database=projects%2Fstafftraveler-prod%2Fdatabases%2F(default)&gsessionid=HFF5EogcWeK0-zCr8BqQiP1LG6gZwKIpFMpTR4_G01uQgOSO1g6VYA&SID=a-XMXYp-Vwc6W3fSsZRnKA&RID=9167&AID=8&zx=vnoas3e35db&t=1 => [200]
|
||||
Request body: count=1&ofs=9&req0___data__=%7B%22database%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%22%2C%22addTarget%22%3A%7B%22query%22%3A%7B%22structuredQuery%22%3A%7B%22from%22%3A%5B%7B%22collectionId%22%3A%22pinnedFlights%22%7D%5D%2C%22orderBy%22%3A%5B%7B%22field%22%3A%7B%22fieldPath%22%3A%22__name__%22%7D%2C%22direction%22%3A%22ASCENDING%22%7D%5D%7D%2C%22parent%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%2Fdocuments%2Fusers%2F3NNPesQMiMRNYnPmuQzh6w2YKyh1%22%7D%2C%22targetId%22%3A16%7D%7D
|
||||
[POST] https://firestore.googleapis.com/google.firestore.v1.Firestore/Listen/channel?VER=8&database=projects%2Fstafftraveler-prod%2Fdatabases%2F(default)&gsessionid=HFF5EogcWeK0-zCr8BqQiP1LG6gZwKIpFMpTR4_G01uQgOSO1g6VYA&SID=a-XMXYp-Vwc6W3fSsZRnKA&RID=9168&AID=8&zx=laziejjz5fkl&t=1 => [200]
|
||||
Request body: count=1&ofs=10&req0___data__=%7B%22database%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%22%2C%22addTarget%22%3A%7B%22documents%22%3A%7B%22documents%22%3A%5B%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%2Fdocuments%2FairportsByIata%2FDAL%22%5D%7D%2C%22targetId%22%3A18%7D%7D
|
||||
[POST] https://firestore.googleapis.com/google.firestore.v1.Firestore/Listen/channel?VER=8&database=projects%2Fstafftraveler-prod%2Fdatabases%2F(default)&gsessionid=HFF5EogcWeK0-zCr8BqQiP1LG6gZwKIpFMpTR4_G01uQgOSO1g6VYA&SID=a-XMXYp-Vwc6W3fSsZRnKA&RID=9169&AID=9&zx=nicmm3gy44sg&t=1 => [200]
|
||||
Request body: count=1&ofs=11&req0___data__=%7B%22database%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%22%2C%22addTarget%22%3A%7B%22documents%22%3A%7B%22documents%22%3A%5B%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%2Fdocuments%2FairportsByIata%2FDFW%22%5D%7D%2C%22targetId%22%3A20%7D%7D
|
||||
[POST] https://firestore.googleapis.com/google.firestore.v1.Firestore/Listen/channel?VER=8&database=projects%2Fstafftraveler-prod%2Fdatabases%2F(default)&gsessionid=HFF5EogcWeK0-zCr8BqQiP1LG6gZwKIpFMpTR4_G01uQgOSO1g6VYA&SID=a-XMXYp-Vwc6W3fSsZRnKA&RID=9170&AID=9&zx=xxcf384tpe8r&t=1 => [200]
|
||||
Request body: count=1&ofs=12&req0___data__=%7B%22database%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%22%2C%22addTarget%22%3A%7B%22documents%22%3A%7B%22documents%22%3A%5B%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%2Fdocuments%2FairportsByIata%2FLAS%22%5D%7D%2C%22targetId%22%3A22%7D%7D
|
||||
[POST] https://firestore.googleapis.com/google.firestore.v1.Firestore/Listen/channel?VER=8&database=projects%2Fstafftraveler-prod%2Fdatabases%2F(default)&gsessionid=HFF5EogcWeK0-zCr8BqQiP1LG6gZwKIpFMpTR4_G01uQgOSO1g6VYA&SID=a-XMXYp-Vwc6W3fSsZRnKA&RID=9171&AID=45&zx=zaws1snjd3dz&t=1 => [200]
|
||||
Request body: count=1&ofs=13&req0___data__=%7B%22database%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%22%2C%22removeTarget%22%3A18%7D
|
||||
[POST] https://firestore.googleapis.com/google.firestore.v1.Firestore/Listen/channel?VER=8&database=projects%2Fstafftraveler-prod%2Fdatabases%2F(default)&gsessionid=HFF5EogcWeK0-zCr8BqQiP1LG6gZwKIpFMpTR4_G01uQgOSO1g6VYA&SID=a-XMXYp-Vwc6W3fSsZRnKA&RID=9172&AID=45&zx=foe8j91xoirj&t=1 => [200]
|
||||
Request body: count=1&ofs=14&req0___data__=%7B%22database%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%22%2C%22removeTarget%22%3A20%7D
|
||||
[POST] https://firestore.googleapis.com/google.firestore.v1.Firestore/Listen/channel?VER=8&database=projects%2Fstafftraveler-prod%2Fdatabases%2F(default)&gsessionid=HFF5EogcWeK0-zCr8BqQiP1LG6gZwKIpFMpTR4_G01uQgOSO1g6VYA&SID=a-XMXYp-Vwc6W3fSsZRnKA&RID=9173&AID=45&zx=ytkmsor9yf6y&t=1 => [200]
|
||||
Request body: count=1&ofs=15&req0___data__=%7B%22database%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%22%2C%22removeTarget%22%3A22%7D
|
||||
[POST] https://firestore.googleapis.com/google.firestore.v1.Firestore/Listen/channel?VER=8&database=projects%2Fstafftraveler-prod%2Fdatabases%2F(default)&gsessionid=HFF5EogcWeK0-zCr8BqQiP1LG6gZwKIpFMpTR4_G01uQgOSO1g6VYA&SID=a-XMXYp-Vwc6W3fSsZRnKA&RID=9174&AID=45&zx=z4vurp4vvm9d&t=1 => [200]
|
||||
Request body: count=1&ofs=16&req0___data__=%7B%22database%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%22%2C%22addTarget%22%3A%7B%22documents%22%3A%7B%22documents%22%3A%5B%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%2Fdocuments%2FuserPriorityRequestCounters%2F3NNPesQMiMRNYnPmuQzh6w2YKyh1%22%5D%7D%2C%22targetId%22%3A24%7D%7D
|
||||
[POST] https://firestore.googleapis.com/google.firestore.v1.Firestore/Listen/channel?VER=8&database=projects%2Fstafftraveler-prod%2Fdatabases%2F(default)&gsessionid=HFF5EogcWeK0-zCr8BqQiP1LG6gZwKIpFMpTR4_G01uQgOSO1g6VYA&SID=a-XMXYp-Vwc6W3fSsZRnKA&RID=9175&AID=45&zx=m5cg66e9m500&t=1 => [200]
|
||||
Request body: count=1&ofs=17&req0___data__=%7B%22database%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%22%2C%22addTarget%22%3A%7B%22documents%22%3A%7B%22documents%22%3A%5B%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%2Fdocuments%2FuserCredits%2F3NNPesQMiMRNYnPmuQzh6w2YKyh1%22%5D%7D%2C%22targetId%22%3A26%7D%7D
|
||||
[POST] https://firestore.googleapis.com/google.firestore.v1.Firestore/Listen/channel?VER=8&database=projects%2Fstafftraveler-prod%2Fdatabases%2F(default)&gsessionid=HFF5EogcWeK0-zCr8BqQiP1LG6gZwKIpFMpTR4_G01uQgOSO1g6VYA&SID=a-XMXYp-Vwc6W3fSsZRnKA&RID=9176&AID=45&zx=tdpwb8n7t6n5&t=1 => [200]
|
||||
Request body: count=1&ofs=18&req0___data__=%7B%22database%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%22%2C%22addTarget%22%3A%7B%22query%22%3A%7B%22structuredQuery%22%3A%7B%22from%22%3A%5B%7B%22collectionId%22%3A%22trackedFlights%22%7D%5D%2C%22where%22%3A%7B%22compositeFilter%22%3A%7B%22op%22%3A%22AND%22%2C%22filters%22%3A%5B%7B%22fieldFilter%22%3A%7B%22field%22%3A%7B%22fieldPath%22%3A%22status%22%7D%2C%22op%22%3A%22EQUAL%22%2C%22value%22%3A%7B%22stringValue%22%3A%22open%22%7D%7D%7D%2C%7B%22fieldFilter%22%3A%7B%22field%22%3A%7B%22fieldPath%22%3A%22hasDeparted%22%7D%2C%22op%22%3A%22EQUAL%22%2C%22value%22%3A%7B%22booleanValue%22%3Afalse%7D%7D%7D%2C%7B%22fieldFilter%22%3A%7B%22field%22%3A%7B%22fieldPath%22%3A%22scheduledFlight.airlineStCode%22%7D%2C%22op%22%3A%22EQUAL%22%2C%22value%22%3A%7B%22stringValue%22%3A%22st_SWA%22%7D%7D%7D%5D%7D%7D%2C%22orderBy%22%3A%5B%7B%22field%22%3A%7B%22fieldPath%22%3A%22priority%22%7D%2C%22direction%22%3A%22DESCENDING%22%7D%2C%7B%22field%22%3A%7B%22fieldPath%22%3A%22scheduledFlight.departureTimeUtc%22%7D%2C%22direction%22%3A%22ASCENDING%22%7D%2C%7B%22field%22%3A%7B%22fieldPath%22%3A%22__name__%22%7D%2C%22direction%22%3A%22ASCENDING%22%7D%5D%2C%22limit%22%3A25%7D%2C%22parent%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%2Fdocuments%22%7D%2C%22targetId%22%3A28%7D%7D
|
||||
[POST] https://firestore.googleapis.com/google.firestore.v1.Firestore/Listen/channel?VER=8&database=projects%2Fstafftraveler-prod%2Fdatabases%2F(default)&gsessionid=HFF5EogcWeK0-zCr8BqQiP1LG6gZwKIpFMpTR4_G01uQgOSO1g6VYA&SID=a-XMXYp-Vwc6W3fSsZRnKA&RID=9177&AID=45&zx=vjcqutd14qa4&t=1 => [200]
|
||||
Request body: count=1&ofs=19&req0___data__=%7B%22database%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%22%2C%22addTarget%22%3A%7B%22query%22%3A%7B%22structuredQuery%22%3A%7B%22from%22%3A%5B%7B%22collectionId%22%3A%22trackedFlights%22%7D%5D%2C%22where%22%3A%7B%22compositeFilter%22%3A%7B%22op%22%3A%22AND%22%2C%22filters%22%3A%5B%7B%22fieldFilter%22%3A%7B%22field%22%3A%7B%22fieldPath%22%3A%22status%22%7D%2C%22op%22%3A%22EQUAL%22%2C%22value%22%3A%7B%22stringValue%22%3A%22open%22%7D%7D%7D%2C%7B%22fieldFilter%22%3A%7B%22field%22%3A%7B%22fieldPath%22%3A%22hasDeparted%22%7D%2C%22op%22%3A%22EQUAL%22%2C%22value%22%3A%7B%22booleanValue%22%3Afalse%7D%7D%7D%2C%7B%22fieldFilter%22%3A%7B%22field%22%3A%7B%22fieldPath%22%3A%22isPriorityRequest%22%7D%2C%22op%22%3A%22EQUAL%22%2C%22value%22%3A%7B%22booleanValue%22%3Atrue%7D%7D%7D%2C%7B%22fieldFilter%22%3A%7B%22field%22%3A%7B%22fieldPath%22%3A%22scheduledFlight.airlineStCode%22%7D%2C%22op%22%3A%22IN%22%2C%22value%22%3A%7B%22arrayValue%22%3A%7B%22values%22%3A%5B%7B%22stringValue%22%3A%22st_SWA%22%7D%5D%7D%7D%7D%7D%5D%7D%7D%2C%22orderBy%22%3A%5B%7B%22field%22%3A%7B%22fieldPath%22%3A%22__name__%22%7D%2C%22direction%22%3A%22ASCENDING%22%7D%5D%2C%22limit%22%3A25%7D%2C%22parent%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%2Fdocuments%22%7D%2C%22targetId%22%3A30%7D%7D
|
||||
[POST] https://firestore.googleapis.com/google.firestore.v1.Firestore/Listen/channel?VER=8&database=projects%2Fstafftraveler-prod%2Fdatabases%2F(default)&gsessionid=HFF5EogcWeK0-zCr8BqQiP1LG6gZwKIpFMpTR4_G01uQgOSO1g6VYA&SID=a-XMXYp-Vwc6W3fSsZRnKA&RID=9178&AID=45&zx=kyar8tjvbzg0&t=1 => [200]
|
||||
Request body: count=1&ofs=20&req0___data__=%7B%22database%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%22%2C%22addTarget%22%3A%7B%22documents%22%3A%7B%22documents%22%3A%5B%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%2Fdocuments%2FuserAchievements%2F3NNPesQMiMRNYnPmuQzh6w2YKyh1%22%5D%7D%2C%22targetId%22%3A32%7D%7D
|
||||
[POST] https://firestore.googleapis.com/google.firestore.v1.Firestore/Listen/channel?VER=8&database=projects%2Fstafftraveler-prod%2Fdatabases%2F(default)&gsessionid=HFF5EogcWeK0-zCr8BqQiP1LG6gZwKIpFMpTR4_G01uQgOSO1g6VYA&SID=a-XMXYp-Vwc6W3fSsZRnKA&RID=9179&AID=45&zx=xf2m25yhj5d&t=1 => [200]
|
||||
Request body: count=1&ofs=21&req0___data__=%7B%22database%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%22%2C%22addTarget%22%3A%7B%22documents%22%3A%7B%22documents%22%3A%5B%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%2Fdocuments%2FairportsByIata%2FIAH%22%5D%7D%2C%22targetId%22%3A34%7D%7D
|
||||
[POST] https://firestore.googleapis.com/google.firestore.v1.Firestore/Listen/channel?VER=8&database=projects%2Fstafftraveler-prod%2Fdatabases%2F(default)&gsessionid=HFF5EogcWeK0-zCr8BqQiP1LG6gZwKIpFMpTR4_G01uQgOSO1g6VYA&SID=a-XMXYp-Vwc6W3fSsZRnKA&RID=9180&AID=45&zx=8fhokbce9hm7&t=1 => [200]
|
||||
Request body: count=1&ofs=22&req0___data__=%7B%22database%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%22%2C%22addTarget%22%3A%7B%22documents%22%3A%7B%22documents%22%3A%5B%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%2Fdocuments%2FairportsByIata%2FCLL%22%5D%7D%2C%22targetId%22%3A36%7D%7D
|
||||
[POST] https://firestore.googleapis.com/google.firestore.v1.Firestore/Listen/channel?VER=8&database=projects%2Fstafftraveler-prod%2Fdatabases%2F(default)&gsessionid=HFF5EogcWeK0-zCr8BqQiP1LG6gZwKIpFMpTR4_G01uQgOSO1g6VYA&SID=a-XMXYp-Vwc6W3fSsZRnKA&RID=9181&AID=45&zx=41ahzajvy06l&t=1 => [200]
|
||||
Request body: count=6&ofs=23&req0___data__=%7B%22database%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%22%2C%22addTarget%22%3A%7B%22documents%22%3A%7B%22documents%22%3A%5B%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%2Fdocuments%2FairportsByIata%2FSJU%22%5D%7D%2C%22targetId%22%3A38%7D%7D&req1___data__=%7B%22database%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%22%2C%22addTarget%22%3A%7B%22documents%22%3A%7B%22documents%22%3A%5B%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%2Fdocuments%2FairportsByIata%2FBOS%22%5D%7D%2C%22targetId%22%3A40%7D%7D&req2___data__=%7B%22database%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%22%2C%22addTarget%22%3A%7B%22documents%22%3A%7B%22documents%22%3A%5B%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%2Fdocuments%2FairportsByIata%2FCUN%22%5D%7D%2C%22targetId%22%3A42%7D%7D&req3___data__=%7B%22database%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%22%2C%22addTarget%22%3A%7B%22documents%22%3A%7B%22documents%22%3A%5B%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%2Fdocuments%2FairportsByIata%2FSYD%22%5D%7D%2C%22targetId%22%3A44%7D%7D&req4___data__=%7B%22database%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%22%2C%22addTarget%22%3A%7B%22documents%22%3A%7B%22documents%22%3A%5B%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%2Fdocuments%2FairportsByIata%2FALH%22%5D%7D%2C%22targetId%22%3A46%7D%7D&req5___data__=%7B%22database%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%22%2C%22addTarget%22%3A%7B%22documents%22%3A%7B%22documents%22%3A%5B%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%2Fdocuments%2FairportsByIata%2FNTL%22%5D%7D%2C%22targetId%22%3A48%7D%7D
|
||||
[POST] https://firestore.googleapis.com/google.firestore.v1.Firestore/Listen/channel?VER=8&database=projects%2Fstafftraveler-prod%2Fdatabases%2F(default)&gsessionid=HFF5EogcWeK0-zCr8BqQiP1LG6gZwKIpFMpTR4_G01uQgOSO1g6VYA&SID=a-XMXYp-Vwc6W3fSsZRnKA&RID=9182&AID=93&zx=7u11wjuyzy3c&t=1 => [200]
|
||||
Request body: count=1&ofs=29&req0___data__=%7B%22database%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%22%2C%22addTarget%22%3A%7B%22query%22%3A%7B%22structuredQuery%22%3A%7B%22from%22%3A%5B%7B%22collectionId%22%3A%22trackedFlights%22%7D%5D%2C%22where%22%3A%7B%22compositeFilter%22%3A%7B%22op%22%3A%22AND%22%2C%22filters%22%3A%5B%7B%22fieldFilter%22%3A%7B%22field%22%3A%7B%22fieldPath%22%3A%22subscribedUsers.%603NNPesQMiMRNYnPmuQzh6w2YKyh1%60%22%7D%2C%22op%22%3A%22EQUAL%22%2C%22value%22%3A%7B%22booleanValue%22%3Atrue%7D%7D%7D%2C%7B%22fieldFilter%22%3A%7B%22field%22%3A%7B%22fieldPath%22%3A%22isDeleted%22%7D%2C%22op%22%3A%22EQUAL%22%2C%22value%22%3A%7B%22booleanValue%22%3Afalse%7D%7D%7D%5D%7D%7D%2C%22orderBy%22%3A%5B%7B%22field%22%3A%7B%22fieldPath%22%3A%22__name__%22%7D%2C%22direction%22%3A%22ASCENDING%22%7D%5D%7D%2C%22parent%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%2Fdocuments%22%7D%2C%22targetId%22%3A50%7D%7D
|
||||
[POST] https://firestore.googleapis.com/google.firestore.v1.Firestore/Listen/channel?VER=8&database=projects%2Fstafftraveler-prod%2Fdatabases%2F(default)&gsessionid=HFF5EogcWeK0-zCr8BqQiP1LG6gZwKIpFMpTR4_G01uQgOSO1g6VYA&SID=a-XMXYp-Vwc6W3fSsZRnKA&RID=9183&AID=93&zx=16mwafi45zsw&t=1 => [200]
|
||||
Request body: count=1&ofs=30&req0___data__=%7B%22database%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%22%2C%22addTarget%22%3A%7B%22query%22%3A%7B%22structuredQuery%22%3A%7B%22from%22%3A%5B%7B%22collectionId%22%3A%22connectingFlights%22%7D%5D%2C%22orderBy%22%3A%5B%7B%22field%22%3A%7B%22fieldPath%22%3A%22__name__%22%7D%2C%22direction%22%3A%22ASCENDING%22%7D%5D%2C%22limit%22%3A500%7D%2C%22parent%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%2Fdocuments%2Fusers%2F3NNPesQMiMRNYnPmuQzh6w2YKyh1%22%7D%2C%22targetId%22%3A52%7D%7D
|
||||
[POST] https://firestore.googleapis.com/google.firestore.v1.Firestore/Listen/channel?VER=8&database=projects%2Fstafftraveler-prod%2Fdatabases%2F(default)&gsessionid=HFF5EogcWeK0-zCr8BqQiP1LG6gZwKIpFMpTR4_G01uQgOSO1g6VYA&SID=a-XMXYp-Vwc6W3fSsZRnKA&RID=9184&AID=104&zx=lhcjya6q73dp&t=1 => [200]
|
||||
Request body: count=1&ofs=31&req0___data__=%7B%22database%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%22%2C%22removeTarget%22%3A34%7D
|
||||
[POST] https://firestore.googleapis.com/google.firestore.v1.Firestore/Listen/channel?VER=8&database=projects%2Fstafftraveler-prod%2Fdatabases%2F(default)&gsessionid=HFF5EogcWeK0-zCr8BqQiP1LG6gZwKIpFMpTR4_G01uQgOSO1g6VYA&SID=a-XMXYp-Vwc6W3fSsZRnKA&RID=9185&AID=104&zx=6qh74wz9it19&t=1 => [200]
|
||||
Request body: count=1&ofs=32&req0___data__=%7B%22database%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%22%2C%22removeTarget%22%3A36%7D
|
||||
[POST] https://firestore.googleapis.com/google.firestore.v1.Firestore/Listen/channel?VER=8&database=projects%2Fstafftraveler-prod%2Fdatabases%2F(default)&gsessionid=HFF5EogcWeK0-zCr8BqQiP1LG6gZwKIpFMpTR4_G01uQgOSO1g6VYA&SID=a-XMXYp-Vwc6W3fSsZRnKA&RID=9186&AID=104&zx=66tex5sth8cf&t=1 => [200]
|
||||
Request body: count=1&ofs=33&req0___data__=%7B%22database%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%22%2C%22removeTarget%22%3A38%7D
|
||||
[POST] https://firestore.googleapis.com/google.firestore.v1.Firestore/Listen/channel?VER=8&database=projects%2Fstafftraveler-prod%2Fdatabases%2F(default)&gsessionid=HFF5EogcWeK0-zCr8BqQiP1LG6gZwKIpFMpTR4_G01uQgOSO1g6VYA&SID=a-XMXYp-Vwc6W3fSsZRnKA&RID=9187&AID=104&zx=8g9i20onobi5&t=1 => [200]
|
||||
Request body: count=1&ofs=34&req0___data__=%7B%22database%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%22%2C%22removeTarget%22%3A40%7D
|
||||
[POST] https://firestore.googleapis.com/google.firestore.v1.Firestore/Listen/channel?VER=8&database=projects%2Fstafftraveler-prod%2Fdatabases%2F(default)&gsessionid=HFF5EogcWeK0-zCr8BqQiP1LG6gZwKIpFMpTR4_G01uQgOSO1g6VYA&SID=a-XMXYp-Vwc6W3fSsZRnKA&RID=9188&AID=104&zx=flmjlaq6s76r&t=1 => [200]
|
||||
Request body: count=1&ofs=35&req0___data__=%7B%22database%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%22%2C%22removeTarget%22%3A42%7D
|
||||
[POST] https://firestore.googleapis.com/google.firestore.v1.Firestore/Listen/channel?VER=8&database=projects%2Fstafftraveler-prod%2Fdatabases%2F(default)&gsessionid=HFF5EogcWeK0-zCr8BqQiP1LG6gZwKIpFMpTR4_G01uQgOSO1g6VYA&SID=a-XMXYp-Vwc6W3fSsZRnKA&RID=9189&AID=104&zx=x8vcama6wdcw&t=1 => [200]
|
||||
Request body: count=1&ofs=36&req0___data__=%7B%22database%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%22%2C%22removeTarget%22%3A44%7D
|
||||
[POST] https://firestore.googleapis.com/google.firestore.v1.Firestore/Listen/channel?VER=8&database=projects%2Fstafftraveler-prod%2Fdatabases%2F(default)&gsessionid=HFF5EogcWeK0-zCr8BqQiP1LG6gZwKIpFMpTR4_G01uQgOSO1g6VYA&SID=a-XMXYp-Vwc6W3fSsZRnKA&RID=9190&AID=104&zx=cq90glstafxi&t=1 => [200]
|
||||
Request body: count=1&ofs=37&req0___data__=%7B%22database%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%22%2C%22removeTarget%22%3A46%7D
|
||||
[POST] https://firestore.googleapis.com/google.firestore.v1.Firestore/Listen/channel?VER=8&database=projects%2Fstafftraveler-prod%2Fdatabases%2F(default)&gsessionid=HFF5EogcWeK0-zCr8BqQiP1LG6gZwKIpFMpTR4_G01uQgOSO1g6VYA&SID=a-XMXYp-Vwc6W3fSsZRnKA&RID=9191&AID=104&zx=iidxjkascr6p&t=1 => [200]
|
||||
Request body: count=1&ofs=38&req0___data__=%7B%22database%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%22%2C%22removeTarget%22%3A48%7D
|
||||
[POST] https://firestore.googleapis.com/google.firestore.v1.Firestore/Listen/channel?VER=8&database=projects%2Fstafftraveler-prod%2Fdatabases%2F(default)&gsessionid=HFF5EogcWeK0-zCr8BqQiP1LG6gZwKIpFMpTR4_G01uQgOSO1g6VYA&SID=a-XMXYp-Vwc6W3fSsZRnKA&RID=9192&AID=104&zx=ib3004hvhsyb&t=1 => [200]
|
||||
Request body: count=1&ofs=39&req0___data__=%7B%22database%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%22%2C%22addTarget%22%3A%7B%22query%22%3A%7B%22structuredQuery%22%3A%7B%22from%22%3A%5B%7B%22collectionId%22%3A%22derivedLoadsReports%22%7D%5D%2C%22where%22%3A%7B%22fieldFilter%22%3A%7B%22field%22%3A%7B%22fieldPath%22%3A%22flightId%22%7D%2C%22op%22%3A%22EQUAL%22%2C%22value%22%3A%7B%22stringValue%22%3A%22AA_2178_DFW_2026_04_22%22%7D%7D%7D%2C%22orderBy%22%3A%5B%7B%22field%22%3A%7B%22fieldPath%22%3A%22createdAt%22%7D%2C%22direction%22%3A%22DESCENDING%22%7D%2C%7B%22field%22%3A%7B%22fieldPath%22%3A%22__name__%22%7D%2C%22direction%22%3A%22DESCENDING%22%7D%5D%7D%2C%22parent%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%2Fdocuments%22%7D%2C%22targetId%22%3A54%7D%7D
|
||||
[POST] https://firestore.googleapis.com/google.firestore.v1.Firestore/Listen/channel?VER=8&database=projects%2Fstafftraveler-prod%2Fdatabases%2F(default)&gsessionid=HFF5EogcWeK0-zCr8BqQiP1LG6gZwKIpFMpTR4_G01uQgOSO1g6VYA&SID=a-XMXYp-Vwc6W3fSsZRnKA&RID=9193&AID=104&zx=vdy4t3cplgip&t=1 => [200]
|
||||
Request body: count=1&ofs=40&req0___data__=%7B%22database%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%22%2C%22addTarget%22%3A%7B%22documents%22%3A%7B%22documents%22%3A%5B%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%2Fdocuments%2FairlinesBySt%2Fst_AAL%22%5D%7D%2C%22targetId%22%3A56%7D%7D
|
||||
[POST] https://firestore.googleapis.com/google.firestore.v1.Firestore/Listen/channel?VER=8&database=projects%2Fstafftraveler-prod%2Fdatabases%2F(default)&gsessionid=HFF5EogcWeK0-zCr8BqQiP1LG6gZwKIpFMpTR4_G01uQgOSO1g6VYA&SID=a-XMXYp-Vwc6W3fSsZRnKA&RID=9194&AID=106&zx=vm7g1bolx517&t=1 => [200]
|
||||
Request body: count=5&ofs=41&req0___data__=%7B%22database%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%22%2C%22addTarget%22%3A%7B%22documents%22%3A%7B%22documents%22%3A%5B%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%2Fdocuments%2FflightEquipment%2F32Q%22%5D%7D%2C%22targetId%22%3A58%7D%7D&req1___data__=%7B%22database%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%22%2C%22addTarget%22%3A%7B%22documents%22%3A%7B%22documents%22%3A%5B%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%2Fdocuments%2FairlineNotesBySt%2Fst_AAL%22%5D%7D%2C%22targetId%22%3A60%7D%7D&req2___data__=%7B%22database%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%22%2C%22addTarget%22%3A%7B%22documents%22%3A%7B%22documents%22%3A%5B%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%2Fdocuments%2Fconversations%2FAA_2178_DFW_2026_04_22%22%5D%7D%2C%22targetId%22%3A62%7D%7D&req3___data__=%7B%22database%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%22%2C%22addTarget%22%3A%7B%22query%22%3A%7B%22structuredQuery%22%3A%7B%22from%22%3A%5B%7B%22collectionId%22%3A%22statusUpdates%22%7D%5D%2C%22orderBy%22%3A%5B%7B%22field%22%3A%7B%22fieldPath%22%3A%22createdAt%22%7D%2C%22direction%22%3A%22DESCENDING%22%7D%2C%7B%22field%22%3A%7B%22fieldPath%22%3A%22__name__%22%7D%2C%22direction%22%3A%22DESCENDING%22%7D%5D%7D%2C%22parent%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%2Fdocuments%2FtrackedFlights%2FAA_2178_DFW_2026_04_22%22%7D%2C%22targetId%22%3A64%7D%7D&req4___data__=%7B%22database%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%22%2C%22addTarget%22%3A%7B%22documents%22%3A%7B%22documents%22%3A%5B%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%2Fdocuments%2FderivedLoadsReports%2FxskQc2a2oAVAwZk3Rfhg%22%5D%7D%2C%22targetId%22%3A66%7D%7D
|
||||
[POST] https://firestore.googleapis.com/google.firestore.v1.Firestore/Listen/channel?VER=8&database=projects%2Fstafftraveler-prod%2Fdatabases%2F(default)&gsessionid=HFF5EogcWeK0-zCr8BqQiP1LG6gZwKIpFMpTR4_G01uQgOSO1g6VYA&SID=a-XMXYp-Vwc6W3fSsZRnKA&RID=9195&AID=136&zx=4xj7drdge1b4&t=1 => [200]
|
||||
Request body: count=1&ofs=46&req0___data__=%7B%22database%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%22%2C%22removeTarget%22%3A56%7D
|
||||
[POST] https://firestore.googleapis.com/google.firestore.v1.Firestore/Listen/channel?VER=8&database=projects%2Fstafftraveler-prod%2Fdatabases%2F(default)&gsessionid=HFF5EogcWeK0-zCr8BqQiP1LG6gZwKIpFMpTR4_G01uQgOSO1g6VYA&SID=a-XMXYp-Vwc6W3fSsZRnKA&RID=9196&AID=136&zx=u6l12lhidxh7&t=1 => [200]
|
||||
Request body: count=1&ofs=47&req0___data__=%7B%22database%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%22%2C%22removeTarget%22%3A58%7D
|
||||
[POST] https://firestore.googleapis.com/google.firestore.v1.Firestore/Listen/channel?VER=8&database=projects%2Fstafftraveler-prod%2Fdatabases%2F(default)&gsessionid=HFF5EogcWeK0-zCr8BqQiP1LG6gZwKIpFMpTR4_G01uQgOSO1g6VYA&SID=a-XMXYp-Vwc6W3fSsZRnKA&RID=9197&AID=136&zx=v3wmv0ko207f&t=1 => [200]
|
||||
Request body: count=1&ofs=48&req0___data__=%7B%22database%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%22%2C%22removeTarget%22%3A60%7D
|
||||
[POST] https://firestore.googleapis.com/google.firestore.v1.Firestore/Listen/channel?VER=8&database=projects%2Fstafftraveler-prod%2Fdatabases%2F(default)&gsessionid=HFF5EogcWeK0-zCr8BqQiP1LG6gZwKIpFMpTR4_G01uQgOSO1g6VYA&SID=a-XMXYp-Vwc6W3fSsZRnKA&RID=9198&AID=141&zx=2dyg9dv4hlx7&t=1 => [200]
|
||||
Request body: count=1&ofs=49&req0___data__=%7B%22database%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%22%2C%22addTarget%22%3A%7B%22documents%22%3A%7B%22documents%22%3A%5B%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%2Fdocuments%2FtrackedFlights%2FWN_2101_ELP_2026_05_03%22%5D%7D%2C%22targetId%22%3A1%7D%2C%22labels%22%3A%7B%22goog-listen-tags%22%3A%22limbo-document%22%7D%7D
|
||||
[POST] https://firestore.googleapis.com/google.firestore.v1.Firestore/Listen/channel?VER=8&database=projects%2Fstafftraveler-prod%2Fdatabases%2F(default)&gsessionid=HFF5EogcWeK0-zCr8BqQiP1LG6gZwKIpFMpTR4_G01uQgOSO1g6VYA&SID=a-XMXYp-Vwc6W3fSsZRnKA&RID=9199&AID=145&zx=96e5i6boihdb&t=1 => [200]
|
||||
Request body: count=1&ofs=50&req0___data__=%7B%22database%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%22%2C%22removeTarget%22%3A1%7D
|
||||
[POST] https://firestore.googleapis.com/google.firestore.v1.Firestore/Listen/channel?VER=8&database=projects%2Fstafftraveler-prod%2Fdatabases%2F(default)&gsessionid=HFF5EogcWeK0-zCr8BqQiP1LG6gZwKIpFMpTR4_G01uQgOSO1g6VYA&SID=a-XMXYp-Vwc6W3fSsZRnKA&RID=9200&AID=154&zx=mpc9gcjtbo7r&t=1 => [200]
|
||||
Request body: count=1&ofs=51&req0___data__=%7B%22database%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%22%2C%22addTarget%22%3A%7B%22documents%22%3A%7B%22documents%22%3A%5B%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%2Fdocuments%2FtrackedFlights%2FWN_3187_ELP_2026_05_03%22%5D%7D%2C%22targetId%22%3A3%7D%2C%22labels%22%3A%7B%22goog-listen-tags%22%3A%22limbo-document%22%7D%7D
|
||||
[POST] https://firestore.googleapis.com/google.firestore.v1.Firestore/Listen/channel?VER=8&database=projects%2Fstafftraveler-prod%2Fdatabases%2F(default)&gsessionid=HFF5EogcWeK0-zCr8BqQiP1LG6gZwKIpFMpTR4_G01uQgOSO1g6VYA&SID=a-XMXYp-Vwc6W3fSsZRnKA&RID=9201&AID=158&zx=yqxzfkm1k2r3&t=1 => [200]
|
||||
Request body: count=1&ofs=52&req0___data__=%7B%22database%22%3A%22projects%2Fstafftraveler-prod%2Fdatabases%2F(default)%22%2C%22removeTarget%22%3A3%7D
|
||||
@@ -0,0 +1,192 @@
|
||||
# StaffTraveler Firestore schema (live-extracted)
|
||||
|
||||
Captured 2026-04-22 via Playwright MCP on `https://app.stafftraveler.com` while logged in as `stafftraveler@treymail.com` (uid `3NNPesQMiMRNYnPmuQzh6w2YKyh1`, airline `st_SWA`). All queries/docs below are copied from the web client's `firestore.googleapis.com/.../Listen/channel` `addTarget` bodies — not guessed.
|
||||
|
||||
## Why iOS mitm didn't show this
|
||||
|
||||
The iOS Firebase SDK and Android SDK both use the gRPC transport to `firestore.googleapis.com` (HTTP/2 with protobuf frames). Many mitm setups decode HTTPS but not gRPC framing, so the requests appear as opaque binary. **The web client uses the `VER=8 TYPE=xmlhttp` long-poll fallback — plain HTTP POST with URL-encoded JSON bodies** — which is fully readable. That's why we only saw this via the browser.
|
||||
|
||||
## Required headers for REST access
|
||||
|
||||
Direct REST (`/v1/projects/.../documents:runQuery` or `/documents/...`) works with:
|
||||
|
||||
```
|
||||
Authorization: Bearer <firebase id token>
|
||||
X-Firebase-GMPID: 1:628258099825:web:35b20eaab4d441894041d0
|
||||
X-Goog-Api-Client: gl-js/ fire/12.12.0
|
||||
?key=AIzaSyD82Hiqx-jAbHF8faMGSveqLw8CdX8a-Uc (URL query param)
|
||||
```
|
||||
|
||||
Note: **web API key** (`AIzaSyD82...`), not the Android key from strings.xml. Omitting `?key=...` or the GMPID header causes `403 PERMISSION_DENIED` — the rules consult the key for the project consumer context before evaluating `allow read`.
|
||||
|
||||
## Single-document subscriptions
|
||||
|
||||
These are fetched as `addTarget.documents.documents`:
|
||||
|
||||
| Path | What |
|
||||
|------|------|
|
||||
| `__system/maintenance` | Server maintenance flags |
|
||||
| `__system/version` | Required client versions (public) |
|
||||
| `users/{uid}` | Profile: airlineStCode, airlineId, firstName, gender, … |
|
||||
| `userHints/{uid}` | UI hints the user has dismissed |
|
||||
| `userRequestCounters/{uid}` | Regular-request quota state |
|
||||
| `userPriorityRequestCounters/{uid}` | Priority-request quota state |
|
||||
| `userCredits/{uid}` | Credit balance |
|
||||
| `userAchievements/{uid}` | Unlocked achievements |
|
||||
| `userMetrics/{uid}` | Usage telemetry |
|
||||
| `userFlightSearchHistory/{uid}` | Recent searches |
|
||||
| `airportsByIata/{IATA}` | Public airport lookup (DFW, LAS, …) |
|
||||
| `airlinesBySt/{stCode}` | Airline lookup by StaffTraveler code (`st_AAL`, `st_SWA`, …) |
|
||||
| `airlineNotesBySt/{stCode}` | Non-rev agreement notes per airline |
|
||||
| `flightEquipment/{iataOrId}` | Aircraft type info (`32Q`, `321`, `738`, …) |
|
||||
| `conversations/{flightId_v3}` | Flight-level comment thread |
|
||||
| `derivedLoadsReports/{reportId}` | **Individual load report doc — see below** |
|
||||
| `trackedFlights/{flightId_v3}` | User's tracked flight state |
|
||||
|
||||
## Collection queries
|
||||
|
||||
### `derivedLoadsReports` — THE LOADS DATA
|
||||
|
||||
```json
|
||||
{
|
||||
"from": [{"collectionId": "derivedLoadsReports"}],
|
||||
"where": {
|
||||
"fieldFilter": {
|
||||
"field": {"fieldPath": "flightId"},
|
||||
"op": "EQUAL",
|
||||
"value": {"stringValue": "AA_2178_DFW_2026_04_22"}
|
||||
}
|
||||
},
|
||||
"orderBy": [
|
||||
{"field": {"fieldPath": "createdAt"}, "direction": "DESCENDING"},
|
||||
{"field": {"fieldPath": "__name__"}, "direction": "DESCENDING"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**This is the query you want for the Flights integration.** Pass the flight `id_v3` (e.g. `AA_2178_DFW_2026_04_22` — format is `{airlineId}_{flightNumber}_{departureIata}_{YYYY}_{MM}_{DD}`).
|
||||
|
||||
**Access control (verified 2026-04-22):** this query returns `403 PERMISSION_DENIED` unless the user has an active `trackedFlights/{flightId_v3}` doc, which is created only by `POST /v1/commands/user/createLoadsRequests`. That call spends a credit on flights the user has never requested; replay on an already-requested flight is idempotent (no charge). So:
|
||||
|
||||
- Freely re-read `derivedLoadsReports` for any flight you've already requested — no cost, no rate limit observed.
|
||||
- You cannot peek at loads without first requesting — the "instant results" in the app for flights with recent community reports rely on the user having previously paid the credit.
|
||||
- On the 15 DFW-LAS flights tested, only `AA_2178_DFW_2026_04_22` (the one with an open request) returned 200; the other 14 all returned 403. Same for 29 LAX-JFK flights the user hadn't requested — all 403.
|
||||
|
||||
Example doc (captured AA2178 2026-04-22 12:42 PM CT):
|
||||
```json
|
||||
{
|
||||
"flightId": "AA_2178_DFW_2026_04_22",
|
||||
"openSeats": {
|
||||
"first": {"count": 3},
|
||||
"eco": {"count": 9}
|
||||
},
|
||||
"staffListing": {
|
||||
"type": "available",
|
||||
"count": 6,
|
||||
"countByClass": {}
|
||||
},
|
||||
"responseTimeSeconds": 196,
|
||||
"numberOfCreditsRewarded": 1,
|
||||
"isFlagged": false,
|
||||
"isFlaggedConfirmed": false,
|
||||
"seatsAvailabilityScore": 0.79,
|
||||
"hasClosedRequest": true,
|
||||
"isJackpotHit": false,
|
||||
"isPriorityRequest": false,
|
||||
"createdAt": "2026-04-22T18:42:51.313Z",
|
||||
"expireAt": "2026-07-21T21:45:00Z",
|
||||
"creatorName": "M",
|
||||
"creatorImageUrl": "https://images.stafftraveler.com/avatars/happysuitcase.png",
|
||||
"creatorImagePath": "avatars/happysuitcase.png"
|
||||
}
|
||||
```
|
||||
|
||||
Field notes:
|
||||
- `openSeats.<class>.count` — **exactly what the app shows as "OPEN SEATS"**. Classes seen: `first`, `business`, `premiumEconomy`, `eco`. Absent class = 0 or unknown.
|
||||
- `staffListing.count` — **the "LISTED NON-REV PASSENGERS" number**.
|
||||
- `staffListing.type` — `"available"` means loads were given; other types likely include `"upgrade"` variants (see `checkForLoads` heuristic in `bundle.hasm`).
|
||||
- `seatsAvailabilityScore` — 0-1, app uses this to color-code the flight.
|
||||
- `creatorName` is the first letter only (privacy).
|
||||
- `expireAt` — ~90 days out, so load reports stay queryable.
|
||||
|
||||
### `trackedFlights` — your open requests
|
||||
|
||||
```json
|
||||
{
|
||||
"from": [{"collectionId": "trackedFlights"}],
|
||||
"where": {
|
||||
"compositeFilter": {"op": "AND", "filters": [
|
||||
{"fieldFilter": {"field": {"fieldPath": "status"}, "op": "EQUAL", "value": {"stringValue": "open"}}},
|
||||
{"fieldFilter": {"field": {"fieldPath": "hasDeparted"}, "op": "EQUAL", "value": {"booleanValue": false}}},
|
||||
{"fieldFilter": {"field": {"fieldPath": "scheduledFlight.airlineStCode"}, "op": "EQUAL", "value": {"stringValue": "st_SWA"}}}
|
||||
]}
|
||||
},
|
||||
"orderBy": [
|
||||
{"field": {"fieldPath": "priority"}, "direction": "DESCENDING"},
|
||||
{"field": {"fieldPath": "scheduledFlight.departureTimeUtc"}, "direction": "ASCENDING"},
|
||||
{"field": {"fieldPath": "__name__"}, "direction": "ASCENDING"}
|
||||
],
|
||||
"limit": 25
|
||||
}
|
||||
```
|
||||
|
||||
Returns the logged-in user's open requests filtered by their airline. Doc ID = flight id_v3. `scheduledFlight` is the embedded full flight object (matches `searchFlightsByRoute` item shape).
|
||||
|
||||
### `trackedFlights/{flightId}/statusUpdates`
|
||||
|
||||
```json
|
||||
{
|
||||
"from": [{"collectionId": "statusUpdates"}],
|
||||
"orderBy": [
|
||||
{"field": {"fieldPath": "createdAt"}, "direction": "DESCENDING"},
|
||||
{"field": {"fieldPath": "__name__"}, "direction": "DESCENDING"}
|
||||
],
|
||||
"parent": "projects/stafftraveler-prod/databases/(default)/documents/trackedFlights/AA_2178_DFW_2026_04_22"
|
||||
}
|
||||
```
|
||||
|
||||
Delay / gate / equipment changes for that flight over time.
|
||||
|
||||
### `users/{uid}/pinnedFlights`, `users/{uid}/connectingFlights`
|
||||
|
||||
Subcollections under the user doc. Empty for this account.
|
||||
|
||||
### `autoRequestRecords` where `subscribedUserIds ARRAY_CONTAINS {uid}` AND `isActive`
|
||||
|
||||
Auto-request subscriptions — flights the user has set up to auto-request loads for on a recurring schedule.
|
||||
|
||||
### `trackedFlights` where `subscribedUsers.{uid} == true` AND `isDeleted == false`
|
||||
|
||||
Other users' requests that THIS user has subscribed to (so they also see the loads when answered).
|
||||
|
||||
## Idempotent REST access pattern (integration recipe)
|
||||
|
||||
```bash
|
||||
# 1. sign in (you already do this)
|
||||
TOKEN=$(curl -sX POST "https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=$WEB_KEY" \
|
||||
-d "{\"email\":\"$EMAIL\",\"password\":\"$PW\",\"returnSecureToken\":true}" | jq -r .idToken)
|
||||
|
||||
# 2. read loads for any flight
|
||||
BASE='https://firestore.googleapis.com/v1/projects/stafftraveler-prod/databases/(default)/documents'
|
||||
KEY='AIzaSyD82Hiqx-jAbHF8faMGSveqLw8CdX8a-Uc'
|
||||
curl -sX POST "$BASE:runQuery?key=$KEY" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "X-Firebase-GMPID: 1:628258099825:web:35b20eaab4d441894041d0" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"structuredQuery":{"from":[{"collectionId":"derivedLoadsReports"}],"where":{"fieldFilter":{"field":{"fieldPath":"flightId"},"op":"EQUAL","value":{"stringValue":"AA_2178_DFW_2026_04_22"}}},"orderBy":[{"field":{"fieldPath":"createdAt"},"direction":"DESCENDING"}]}}'
|
||||
```
|
||||
|
||||
Returns empty array if no report exists yet (meaning you'd need `createLoadsRequests` first to ask the crew). If a report exists, you get it without spending a credit.
|
||||
|
||||
## Integration plan for the iOS Flights app (final)
|
||||
|
||||
1. Secondary `FirebaseApp` configured against `stafftraveler-prod` with the web API key + GMPID above.
|
||||
2. `Auth.auth(app: stApp).signIn(withEmail:password:)` with user's StaffTraveler credentials, refresh token stored in Keychain.
|
||||
3. Use `Firestore.firestore(app: stApp)` and wire:
|
||||
- `collection("derivedLoadsReports").whereField("flightId", isEqualTo: flightIdV3).addSnapshotListener { ... }` — live load updates per flight
|
||||
- `collection("trackedFlights").whereField("status", isEqualTo: "open")....whereField("scheduledFlight.airlineStCode", isEqualTo: userStCode).limit(to: 25).addSnapshotListener` — user's open requests list
|
||||
- `document("airlinesBySt/\(stCode)")` — airline metadata
|
||||
- `document("airportsByIata/\(iata)")` — airport metadata
|
||||
4. For writes (request loads, submit report, etc.), keep using the HTTP commands API at `https://api.stafftraveler.com/v1/commands/user/<cmd>`.
|
||||
|
||||
No App Check enforcement observed from the web client — plain Firebase ID token + API key is sufficient.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
{"originIata":"DFW","destinationIata":"LAS","dates":["2026-04-22"],"maxConnections":0,"allowNearbyDepartures":false,"allowNearbyArrivals":false,"payloadType":"passenger","includeMultipleCarriers":false}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,100 @@
|
||||
# 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
|
||||
@@ -0,0 +1,368 @@
|
||||
# StaffTraveler — complete findings
|
||||
|
||||
Single source of truth for the StaffTraveler reverse-engineering effort. Verified by HTTP captures, Firestore REST probes, and Hermes bytecode analysis against the live `stafftraveler-prod` Firebase project. Raw artifacts in `stafftraveler_captures/`.
|
||||
|
||||
Source app: `com.stafftraveler.webview` v3.12.0 (build 1880000346), iOS UA `stafftraveler/560 CFNetwork/3860.500.112 Darwin/25.4.0`. Web client at `https://app.stafftraveler.com` (same Firebase project, same Firestore rules — used for inspection because it sends the long-poll fallback transport instead of gRPC).
|
||||
|
||||
## TL;DR for the iOS Flights integration
|
||||
|
||||
To show flight loads inside our app:
|
||||
|
||||
1. Add a secondary `FirebaseApp` named e.g. `StaffTravelerFirebaseApp` configured with the **web** API key + GMPID below.
|
||||
2. Sign in as the user with Firebase Auth (email/password) → store refresh token in Keychain.
|
||||
3. To search routes, `POST https://api.stafftraveler.com/v1/commands/user/searchFlightsByRoute` with the Bearer ID token.
|
||||
4. To **unlock** loads on a flight, `POST .../createLoadsRequests` with the flight object + `isPriorityRequest: false` — costs **1 credit per new flight**, idempotent on replay.
|
||||
5. After unlock, attach a Firestore snapshot listener on `derivedLoadsReports.where("flightId", "==", flightIdV3).order("createdAt", desc)` to receive live load updates.
|
||||
6. Optional surface for "what's happening on my own airline": query `trackedFlights.where("scheduledFlight.airlineStCode", "==", userStCode)` to see flights other employees on the same airline have requested.
|
||||
|
||||
You **cannot** show real load numbers for a flight without first calling `createLoadsRequests` on that specific flight. There is no bulk/peek endpoint — verified extensively (see "Security model").
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
React Native + Hermes bytecode iOS/Android app with a thin native shell. All business logic in `assets/index.android.bundle`. Backend is Firebase:
|
||||
|
||||
| Component | Service |
|
||||
|-----------|---------|
|
||||
| Auth | Firebase Auth (email/password, Google, Apple, Facebook) |
|
||||
| Real-time data | Cloud Firestore (`stafftraveler-prod`) |
|
||||
| Mutations | Custom HTTP commands API at `api.stafftraveler.com/v1/commands/user/<name>` (likely Cloud Functions) |
|
||||
| Config | Firebase Remote Config (controls API base URL, Typesense host/key, feature flags) |
|
||||
| Search index | Typesense (`enhk2ji1vu6csxzrp-1.a1.typesense.net`) — server-side, not hit directly by the client |
|
||||
| Push | FCM / APNs |
|
||||
| Static CDN | `images.stafftraveler.com` |
|
||||
|
||||
No SSL pinning. No App Check enforcement (Play Integrity classes are bundled but the `createFirebaseCommand` fetch doesn't attach an `X-Firebase-AppCheck` header, and the web client makes every request with just the ID token).
|
||||
|
||||
## Firebase project config
|
||||
|
||||
From APK `res/values/strings.xml` and the web client's IndexedDB:
|
||||
|
||||
| Key | Value |
|
||||
|-----|-------|
|
||||
| Project ID | `stafftraveler-prod` |
|
||||
| **Web API key** (the one that works for REST) | `AIzaSyD82Hiqx-jAbHF8faMGSveqLw8CdX8a-Uc` |
|
||||
| Web App ID / GMPID | `1:628258099825:web:35b20eaab4d441894041d0` |
|
||||
| Android API key | `AIzaSyC2zG6ArnguzzdWsLYV1qjQznma0zl1Q0s` (locked to the signed APK) |
|
||||
| Android App ID | `1:628258099825:android:5fb976ba6ad1bb05` |
|
||||
| Sender ID | `628258099825` |
|
||||
| Firestore | `(default)` database |
|
||||
| RTDB | `https://stafftraveler-prod.firebaseio.com` (registered but unused for loads) |
|
||||
| Storage | `stafftraveler-prod.appspot.com` |
|
||||
|
||||
For an iOS integration, register a new web/iOS app in the same Firebase console — but the existing web app credentials above are sufficient for direct REST and the web SDK in our app.
|
||||
|
||||
## Auth
|
||||
|
||||
```
|
||||
POST https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=<web-key>
|
||||
Content-Type: application/json
|
||||
{ "email": "...", "password": "...", "returnSecureToken": true }
|
||||
→ { idToken, refreshToken, expiresIn: "3600", localId: <uid>, ... }
|
||||
```
|
||||
|
||||
The ID token is a 1-hour JWT; refresh via `securetoken.googleapis.com/v1/token?key=<web-key>` with `grant_type=refresh_token&refresh_token=...`. The Firebase SDK does this transparently.
|
||||
|
||||
Token claims include `email`, `user_id`, `is_registered`. Issuer `https://securetoken.google.com/stafftraveler-prod`, audience `stafftraveler-prod`.
|
||||
|
||||
## HTTP commands API
|
||||
|
||||
```
|
||||
POST https://api.stafftraveler.com/v1/commands/user/<commandName>
|
||||
Authorization: Bearer <firebase id token>
|
||||
Content-Type: application/json
|
||||
x-stafftraveler-client: mobile ← the iOS app sends this; web sends "web". Probably not enforced.
|
||||
{ ... command-specific body ... }
|
||||
```
|
||||
|
||||
Response: `{ payload: <result> }` on success, `{ isError: true, payload: <message> }` on validation/business errors. Both come back HTTP 200; check the `isError` flag.
|
||||
|
||||
`api_commands_url` is fetched from Remote Config at startup. The deployed value is `https://api.stafftraveler.com/v1/commands/user`. The bundle disassembly confirms there is no other path prefix — tested `/public/`, `/internal/`, `/admin/`, `/v2/`, `/flights/`, `/loads/`, `/system/`, `/airline/` → all 404.
|
||||
|
||||
Also exposed on the same host: `GET /health` → `{message:"ok", serverTime:...}`.
|
||||
|
||||
### Command inventory (all 34, via `createFirebaseCommand` enumeration)
|
||||
|
||||
**Reads (free, idempotent):**
|
||||
- `searchFlightsByRoute` — flight schedule by origin/destination/date
|
||||
- `searchFlightsByCode` — flight schedule by flight number
|
||||
- `appActive` — heartbeat; needs `{localDateIso, appType}`; returns `{payload: null}`
|
||||
- `mobile` — stub, returns `"not implemented"`
|
||||
|
||||
**Mutations (most of these spend credits or modify account state):**
|
||||
|
||||
```
|
||||
addAirlineDetails addCreditsForPurchase addComment addIdentity
|
||||
appLogout createLoadsRequests createTip deleteLoadsRequest
|
||||
deleteUserAccount disableAllAutoRequestSubscriptions
|
||||
disableFlightStatusUpdates enableFlightStatusUpdates discardHint
|
||||
flagLoadsReport omitIdentity pinFlight unpinFlight
|
||||
registerUser removeComment reportComment
|
||||
reopenLoadsRequest reviseLoadsReport requestLockForLoadsRequest
|
||||
unlockLoadsRequest setAutoRequestSubscription submitFeedback
|
||||
submitLoadsReport unlikeTip updateUserProfile upgradeLoadsRequest
|
||||
```
|
||||
|
||||
### `searchFlightsByRoute` — confirmed payload
|
||||
|
||||
```json
|
||||
{
|
||||
"originIata": "DFW",
|
||||
"destinationIata": "LAS",
|
||||
"dates": ["2026-04-22"],
|
||||
"maxConnections": 0,
|
||||
"allowNearbyDepartures": false,
|
||||
"allowNearbyArrivals": false,
|
||||
"payloadType": "passenger",
|
||||
"includeMultipleCarriers": false
|
||||
}
|
||||
```
|
||||
|
||||
Response: `{ payload: { directFlights: [...], connectingFlights: [...], numberOfDiscardedFlights, settingsFilteredCount, messages } }`.
|
||||
|
||||
Flight object fields:
|
||||
```
|
||||
id, id_v2, id_v3 (= "AA_2178_DFW_2026_04_22"),
|
||||
airlineId, airlineStCode, flightCode, flightNumber,
|
||||
departureAirportId, departureAirportIata, arrivalAirportId, arrivalAirportIata,
|
||||
departureTimeLocal, departureTimeUtc, arrivalTimeLocal, arrivalTimeUtc,
|
||||
durationMinutes, isCargoFlight,
|
||||
flightEquipmentId, flightEquipmentIata,
|
||||
seatsTotal, seatsByClass: { first, business, premiumEconomy, economy }
|
||||
```
|
||||
|
||||
`id_v3` format is `{airlineId}_{flightNumber}_{departureIata}_{YYYY}_{MM}_{DD}`. **This is the key used everywhere downstream** (Firestore docs, command bodies, etc).
|
||||
|
||||
`seatsByClass` is the **aircraft cabin configuration**, NOT current availability — that's a separate Firestore lookup.
|
||||
|
||||
### `createLoadsRequests` — confirmed payload
|
||||
|
||||
Body is a **bare JSON array** (no outer wrapper) of full flight objects, each with one extra field `isPriorityRequest: bool`:
|
||||
|
||||
```json
|
||||
[
|
||||
{ ...full flight from search response..., "isPriorityRequest": false }
|
||||
]
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"payload": {
|
||||
"numberOfRequests": 0,
|
||||
"numberOfRequestsPaidFor": 0,
|
||||
"numberOfRequestsPaidForPriority": 0,
|
||||
"hasSufficientCredits": true,
|
||||
"verifiedRequests": [
|
||||
{
|
||||
"flightId": "AA_2178_DFW_2026_04_22",
|
||||
"isExisting": true,
|
||||
"isDuplicateForUser": true,
|
||||
"isRequestUpdateForUser": false,
|
||||
"hasDeparted": false,
|
||||
"isCancelled": false,
|
||||
"isPriorityRequest": false,
|
||||
"hasRecentLoads": false, ← summary flag; actual loads come from Firestore
|
||||
"scheduledFlight": { ... full flight ... }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Idempotency:** Replaying the same flight returns `numberOfRequests: 0` + `isDuplicateForUser: true` + **no credit charged**. Useful as a "what's the current state" probe — pay for the first call only, then replay free.
|
||||
|
||||
## Firestore — the data plane
|
||||
|
||||
REST endpoint: `https://firestore.googleapis.com/v1/projects/stafftraveler-prod/databases/(default)/documents`
|
||||
|
||||
**Required headers on every read:**
|
||||
```
|
||||
Authorization: Bearer <firebase id token>
|
||||
X-Firebase-GMPID: 1:628258099825:web:35b20eaab4d441894041d0
|
||||
?key=AIzaSyD82Hiqx-jAbHF8faMGSveqLw8CdX8a-Uc (URL query)
|
||||
```
|
||||
|
||||
Omitting `?key=` or the GMPID gives `403 PERMISSION_DENIED` even with a valid token — the project-consumer context is required.
|
||||
|
||||
**Why mobile mitm couldn't see this:** the iOS Firebase SDK uses gRPC over HTTP/2 to `firestore.googleapis.com`. Many proxies show those streams as opaque binary frames. The web client uses the long-poll fallback (`Listen/channel?VER=8&TYPE=xmlhttp`) with URL-encoded JSON bodies — fully decodable. That's why the answer only fell out via the browser.
|
||||
|
||||
### Loads — `derivedLoadsReports`
|
||||
|
||||
The single most important query for the integration:
|
||||
|
||||
```json
|
||||
POST documents:runQuery
|
||||
{
|
||||
"structuredQuery": {
|
||||
"from": [{"collectionId": "derivedLoadsReports"}],
|
||||
"where": {
|
||||
"fieldFilter": {
|
||||
"field": {"fieldPath": "flightId"},
|
||||
"op": "EQUAL",
|
||||
"value": {"stringValue": "AA_2178_DFW_2026_04_22"}
|
||||
}
|
||||
},
|
||||
"orderBy": [
|
||||
{"field": {"fieldPath": "createdAt"}, "direction": "DESCENDING"},
|
||||
{"field": {"fieldPath": "__name__"}, "direction": "DESCENDING"}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Doc shape (verified live capture, AA2178 2026-04-22):
|
||||
|
||||
```json
|
||||
{
|
||||
"flightId": "AA_2178_DFW_2026_04_22",
|
||||
"openSeats": {
|
||||
"first": { "count": 3 },
|
||||
"eco": { "count": 9 }
|
||||
// also: business, premiumEconomy. Absent class = 0/unknown.
|
||||
},
|
||||
"staffListing": {
|
||||
"type": "available", // also: "upgrade" variants per checkForLoads heuristic
|
||||
"count": 6, // # standby/listed non-rev passengers
|
||||
"countByClass": {}
|
||||
},
|
||||
"responseTimeSeconds": 196,
|
||||
"numberOfCreditsRewarded": 1,
|
||||
"isFlagged": false,
|
||||
"isFlaggedConfirmed": false,
|
||||
"seatsAvailabilityScore": 0.79, // 0-1; app uses for color coding
|
||||
"hasClosedRequest": true,
|
||||
"isJackpotHit": false,
|
||||
"isPriorityRequest": false,
|
||||
"createdAt": "2026-04-22T18:42:51.313Z",
|
||||
"expireAt": "2026-07-21T21:45:00Z", // ~90 days
|
||||
"creatorName": "M", // first letter only (privacy)
|
||||
"creatorImageUrl": "https://images.stafftraveler.com/avatars/happysuitcase.png",
|
||||
"creatorImagePath": "avatars/happysuitcase.png"
|
||||
}
|
||||
```
|
||||
|
||||
### `trackedFlights` — request state
|
||||
|
||||
One doc per (flight, anyone-who-requested-it). Doc ID = `id_v3` of the flight. Key fields:
|
||||
|
||||
```
|
||||
scheduledFlight: { full flight object — same shape as searchFlightsByRoute }
|
||||
status: "open" | "closed"
|
||||
createdBy: <uid of original requester>
|
||||
createdAt, statusChangedAt, expireAt
|
||||
priority: integer
|
||||
priorityElevatedAt
|
||||
isPriorityRequest, isAutoRequest, isLocked, isCancelled, isDeleted, isDelayed, isDiverted
|
||||
hasDeparted, hasArrived, hasBeenRefunded
|
||||
lockedBy, lockedAt ← while a reporter is filling it in
|
||||
loadsUpdateRequestedBy
|
||||
currentLoadsReportId ← doc ID into derivedLoadsReports
|
||||
seatsAvailabilityScore ← summary score (also in derivedLoadsReports)
|
||||
reportAuthorList: [<uid>] ← who has reported on this flight
|
||||
reportAuthors: { <uid>: true, ... }
|
||||
subscribedUsers: { <uid>: true, ... } ← all users tracking this flight
|
||||
subscribedUserList: [<uid>]
|
||||
statusUpdatesSubscribedUserIds: [<uid>]
|
||||
usersRequestingLoads: { <uid>: <ts>, ... } ← users currently requesting an update
|
||||
usersRequestingPriority: { ... }
|
||||
usersEligibleForRefund: { <uid>: 1, ... }
|
||||
sita: { ... rich live SITA flight record (gate, registration, flight times, marketing carriers, etc) ... }
|
||||
__checksums: { typesense: "..." }
|
||||
```
|
||||
|
||||
### Other Firestore listeners the web client opens (captured)
|
||||
|
||||
From the live `Listen/channel` body capture in `firestore_listen_targets_and_queries.txt`:
|
||||
|
||||
| Target | Use |
|
||||
|--------|-----|
|
||||
| `users/{uid}` | profile |
|
||||
| `userHints/{uid}`, `userRequestCounters/{uid}`, `userPriorityRequestCounters/{uid}`, `userCredits/{uid}`, `userAchievements/{uid}`, `userMetrics/{uid}`, `userFlightSearchHistory/{uid}` | per-user state |
|
||||
| `airportsByIata/{IATA}` | airport metadata |
|
||||
| `airlinesBySt/{stCode}` | airline metadata by ST code |
|
||||
| `airlineNotesBySt/{stCode}` | non-rev policy notes |
|
||||
| `flightEquipment/{type}` | aircraft type info |
|
||||
| `conversations/{flightId_v3}` | per-flight comment thread |
|
||||
| `users/{uid}/pinnedFlights` (subcoll. query) | pinned flights |
|
||||
| `users/{uid}/connectingFlights` (subcoll. query) | connecting flight definitions |
|
||||
| `trackedFlights/{flightId_v3}/statusUpdates` (subcoll. query) | flight status change feed |
|
||||
| `trackedFlights` query | see "Security model" — airline-scoped |
|
||||
| `autoRequestRecords` query filtered by `subscribedUserIds ARRAY_CONTAINS uid` AND `isActive == true` | user's auto-subscriptions |
|
||||
|
||||
## Security model (the load-data unlock)
|
||||
|
||||
This is the constraint that defines the integration shape. Verified by exhaustive probing.
|
||||
|
||||
**Rules summary** (inferred from observed access pattern):
|
||||
|
||||
| Collection | Rule (effective) |
|
||||
|------------|------------------|
|
||||
| `__system/*`, `__derived/nonRevAgreementsBySt`, `airlines/*`, `airlinesBySt/*`, `airportsByIata/*`, `airlineNotesBySt/*`, `flightEquipment/*`, `tips/*`, `autoRequestRecords/*`, `conversations/*` | Public read for any authenticated user |
|
||||
| `users/{uid}`, `userCredits/{uid}`, all other `user*/{uid}` | Owner-only |
|
||||
| `trackedFlights` query (`list`) | Allowed only when filtered by `scheduledFlight.airlineStCode == <user's own airline>` |
|
||||
| `trackedFlights/{id}` direct read (`get`) | Allowed if same-airline OR createdBy == auth.uid |
|
||||
| `trackedFlights/{id}/statusUpdates` subcollection | Allowed only if you own/created the flight |
|
||||
| `derivedLoadsReports` query and direct-get | Allowed only if you have a matching `trackedFlights/{flightId}` where you're the creator/subscriber |
|
||||
| `scheduledFlights/{id}`, `flightRecord/{id}`, `flights/{id}` | Denied for direct-get on any flight |
|
||||
|
||||
**Verified 2026-04-22:**
|
||||
- AA2178 query (I have a request on it) → 200, returns the load doc.
|
||||
- WN862 query (someone else's request, my airline) → 403.
|
||||
- Direct GET on `derivedLoadsReports/dVb4zStjhITceArRgLBB` (WN862's load doc, ID extracted from the trackedFlight doc which IS readable as same-airline) → still 403. Same token, same headers. So knowing the doc ID is not a bypass.
|
||||
- DL/AA/UA/JBU/etc trackedFlights queries → 403 across all 9 airlines tested.
|
||||
- Same-airline trackedFlights query → 500+ historic WN flights returned (status=closed mostly), confirming the airline-scope leak.
|
||||
|
||||
**Net consequence:** there is no "peek" path. To see load numbers on a flight, you must spend a credit on it via `createLoadsRequests`. After that, both the trackedFlight and derivedLoadsReports become readable indefinitely (or until the flight expires ~90 days later), and snapshot listeners deliver new community-submitted reports for free.
|
||||
|
||||
The same-airline `trackedFlights` listing exposes one useful summary signal without paying: `seatsAvailabilityScore` (0–1) on flights other employees on your own airline have asked about. Coarse go/no-go indicator, no class-level breakdown.
|
||||
|
||||
## Public reference data worth caching
|
||||
|
||||
Pulled and saved to `stafftraveler_captures/`:
|
||||
|
||||
- **`__derived/nonRevAgreementsBySt`** (`nonRevAgreementsBySt.json`) — full interline matrix: 300 airlines, each with an array of airline ST codes they have non-rev agreements with. Useful for filtering search results to "airlines I can actually non-rev on" without us building this ourselves.
|
||||
- **`airlinesBySt/*`** (`airlinesBySt_full.json`) — ST-code → airline directory.
|
||||
|
||||
Both stable, refresh occasionally.
|
||||
|
||||
## What the iOS app actually needs
|
||||
|
||||
Concrete integration plan:
|
||||
|
||||
1. **Secondary FirebaseApp** `StaffTravelerApp` configured with the web key + GMPID above. Initialize on first user-opt-in.
|
||||
2. **Auth screen** — email/password login → store refresh token in Keychain. Surface "no StaffTraveler account? Sign up in the StaffTraveler app first" — `addAirlineDetails` verification has to happen there.
|
||||
3. **Bridge user state** — read `users/{uid}` (gets `airlineStCode`, name) and `userCredits/{uid}` (gets balance). Display credit balance in our UI.
|
||||
4. **Search**: `POST /v1/commands/user/searchFlightsByRoute`. Free.
|
||||
5. **Flight row UI**: for each result, attach a Firestore snapshot listener on `derivedLoadsReports.where("flightId", "==", flightId_v3)`.
|
||||
- If the listener gets a doc → render loads inline.
|
||||
- If 403 → flight isn't unlocked yet; show a "Request loads (1 credit)" CTA.
|
||||
6. **Request CTA** → `POST /v1/commands/user/createLoadsRequests` with the full flight + `isPriorityRequest: false`. Idempotent — surface "you already requested this" if `isDuplicateForUser: true`.
|
||||
7. **Comments / status feed (optional)** — listeners on `conversations/{flightId}` and `trackedFlights/{flightId}/statusUpdates`.
|
||||
8. **Token refresh** — let Firebase iOS SDK handle it. Don't try to do it manually.
|
||||
|
||||
Mutations to wire later if useful: `pinFlight`/`unpinFlight`, `setAutoRequestSubscription` (auto-poll a recurring route), `submitLoadsReport` (let the user contribute back if they're at the gate). Skip everything else.
|
||||
|
||||
## Captures (raw artifacts)
|
||||
|
||||
In `stafftraveler_captures/`:
|
||||
|
||||
- `searchFlightsByRoute_DFW-LAS_request.json` / `_response.json` — schedule API
|
||||
- `createLoadsRequests_AA2178_request.json` / `_response.json` / `_response_headers.txt` — load-request API
|
||||
- `derivedLoadsReports_AA2178_request.json` / `_response.json` — actual loads readout
|
||||
- `firestore_listen_targets_and_queries.txt` — all `addTarget` bodies the web client opens in a session
|
||||
- `nonRevAgreementsBySt.json` — interline matrix
|
||||
- `airlinesBySt_full.json` — airline directory
|
||||
- `firestore_schema.md`, `url_surface_crawl.md`, `README.md` — interim notes
|
||||
|
||||
## Things that are NOT possible
|
||||
|
||||
Documenting the dead ends so we don't relitigate:
|
||||
|
||||
- **Cross-airline load enumeration** — blocked by the `airlineStCode` filter on `trackedFlights` and the createdBy check on `derivedLoadsReports`. Tested across 9 airline codes.
|
||||
- **Bulk peek at multiple flights' loads in one call** — no API. Each flight needs its own credit unlock.
|
||||
- **Reading another user's `derivedLoadsReports` by knowing the doc ID** — rule evaluates the doc's `flightId` against your tracked flights. Direct-GET on a known foreign doc ID returns 403.
|
||||
- **Spoofing airline membership** — token claims are server-issued; `setAirlineStCodes` is a real command but changing your airline would affect the user's actual account.
|
||||
- **Anonymous Firebase Auth on this project** — the Android API key is restricted to the signed APK package + cert SHA1. The web key is unrestricted on `signInWithPassword` but you still need real credentials.
|
||||
- **App Check bypass** — moot; not enforced in practice.
|
||||
Reference in New Issue
Block a user