Compare commits

..

1 Commits

Author SHA1 Message Date
jackos1998 717ebf0e82 Expand AGENTS.md: checks, brand assets, decompiled-APK note
Add a Checks section (py_compile + nix build), note the brand/ assets and
their source, broaden the backtick-quoting rule, refine the token-expiry
guidance, and explain using decompiled APK output if provided.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 02:42:03 +01:00
2 changed files with 16 additions and 85 deletions
+5 -37
View File
@@ -76,39 +76,8 @@ When dumping flows, redact `Authorization` / `Cookie` headers and the login body
Some questions the capture can't answer (token lifecycle, the white-label ID, Some questions the capture can't answer (token lifecycle, the white-label ID,
error codes) need the app itself. If you're pointed at **decompiled APK output** error codes) need the app itself. If you're pointed at **decompiled APK output**
(e.g. apktool `smali/`), grep it there — but it's R8-obfuscated: class names are (e.g. apktool `smali/`), grep it there — but it's R8-obfuscated: class names are
mangled and library types (e.g. OkHttp, `androidx.security.crypto`) may be mangled and library types (e.g. OkHttp) may be shrunk/repackaged, so a missing
shrunk/repackaged, so a missing grep hit is not proof of absence. No such grep hit is not proof of absence. No such decompilation lives in this repo.
decompilation lives in this repo (the user can supply a local apktool dump on
request; app package `com.perfectgym.perfectgymgo2.westwoodclub`). The original
ELPassion source package names survive obfuscation under
`smali/com/elpassion/perfectgym/`, which is the useful entry point.
### How the app stores the bearer token
Confirmed from the decompilation (relevant because it answers the token-lifecycle
question and shows there's no second auth secret to capture):
- The login response DTO (`AccountAuthorizationGoApiDto`) carries `token`,
`tokenType`, `authorizationHeader`, and a **nullable `expireTime`**.
`DtoMapperKt.asAuthorizeResponse` keeps **only the bare `token`** string;
`tokenType`/`authorizationHeader`/`expireTime` are discarded. The app
reconstructs `Authorization: bearer <token>` itself per request.
- The token is **persisted in `EncryptedSharedPreferences`** (androidx
security-crypto, R8-repackaged to `l3.*`; AES-256-GCM master key in the Android
Keystore). The store class is `f6/o` (interface `f6/p`); the backing file is
named `wevgebvre` and values are Moshi-JSON-encoded. Token key: **`"token"`**
(writer `f6/o.h(String)`, reader `f6/o.r()`).
- **Load path:** at DI-graph construction the provider (`androidx/room/v0`) calls
`f6/p.r()` and passes the stored token into the `appmodel/s0` AppModel
constructor as its initial value, which seeds the reactive `tokenS`
(`Optional<String>`) stream — so the app comes up already authenticated.
Login writes the new token back via `f6/p.h(...)` (dispatcher `z4/c`).
- **Legacy + migration:** older builds kept the same `"token"` key in the
*plaintext* default `SharedPreferences` (`f6/b0`, via
`PreferenceManager.getDefaultSharedPreferences`). `PerfectGymApplication` runs a
one-time migration gated by an `isMigrated` flag: copy from `f6/b0` into the
encrypted `f6/o`, then `clear()` + `deleteSharedPreferences()` the plaintext
file. So the token is no longer recoverable in cleartext on current installs.
## API essentials ## API essentials
@@ -116,10 +85,9 @@ Full detail in `api.md`. Quick reference:
- Responses are wrapped `{ "data": ..., "errors": ... }`; `errors` is `null` on success. - Responses are wrapped `{ "data": ..., "errors": ... }`; `errors` is `null` on success.
- **Auth:** `POST /v1/Authorize/LogInWithEmail` (white-label ID goes in the body) - **Auth:** `POST /v1/Authorize/LogInWithEmail` (white-label ID goes in the body)
→ reuse the returned `bearer <token>` as the `Authorization` header. Treat the → reuse the returned `bearer <token>` as the `Authorization` header. The token
token as long-lived: there's no refresh token, the app persists only the token most likely doesn't expire (no refresh token in the response, app appears to
(not the email/password) and discards the response's `expireTime`, so refetch store no credentials); on `401`/`403` get a fresh one. See `api.md`.
reactively on `401`/`403`. See `api.md` and the token-storage notes above.
- Authenticated endpoints need **only** the `Authorization` header — the `X-Go-*` - Authenticated endpoints need **only** the `Authorization` header — the `X-Go-*`
headers and app `User-Agent` the app sends are not required (verified against headers and app `User-Agent` the app sends are not required (verified against
the clubs endpoint). the clubs endpoint).
+11 -48
View File
@@ -228,19 +228,17 @@ overrides and was empty in the capture.
`GET /v1/PersonalTrainings/Bookings?timestamp=0` `GET /v1/PersonalTrainings/Bookings?timestamp=0`
The account's own PT sessions**past and future in one list**, newest-booked The account's PT sessions. `Classes/BookingsV2` (same shape, class bookings) was
last. The endpoint does not filter by date; to surface *upcoming* bookings, empty in the capture. `instructorId` maps to `Instructors/Instructors`;
filter client-side on `startDate > now` (and typically `not isCanceled` / `clubId` to the club list.
`not isCompleted`). `Classes/BookingsV2` (same shape, for class bookings) was
empty in the capture.
```json ```json
{ {
"name": "New - 4th Program- Review", "name": "1st Consultation",
"startDate": "2026-05-26T08:30:00+01:00", "startDate": "2026-04-20T08:45:00+01:00",
"endDate": "2026-05-26T09:00:00+01:00", "endDate": "2026-04-20T09:15:00+01:00",
"isCanceled": false, "isCanceled": false,
"isCompleted": true, "isCompleted": false,
"instructorId": 0, "instructorId": 0,
"clubId": 962, "clubId": 962,
"personalTrainingTypeId": 0, "personalTrainingTypeId": 0,
@@ -250,23 +248,6 @@ empty in the capture.
} }
``` ```
Fields:
- `name` — already human-readable and self-contained (e.g.
`"New - 1st Consultation"`, `"New - 4th Program- Review"`). A "next PT booking"
sensor needs **only this endpoint** — the lookups below are enrichment.
- `startDate` / `endDate` — ISO-8601 **with** offset (`+01:00`); the duration is
implied (no separate field on the booking).
- `isCanceled` / `isCompleted` — booleans. A future session has both `false`.
- `instructorId``Instructors/Instructors`; `personalTrainingTypeId`
`PersonalTrainings/PersonalTrainingsTypes`; `clubId` → the club list. Note the
booking's own `name` does **not** match the type's `name`.
**Delta sync:** like other catalogue endpoints, passing the largest `timestamp`
seen in a previous response (instead of `0`) returns only rows changed since —
`data: []` when nothing changed. A simple poller can ignore this and always send
`timestamp=0` to get the full list each time.
### Membership contract ### Membership contract
`GET /v1/RemoteAccounts/Contracts?timestamp=0` `GET /v1/RemoteAccounts/Contracts?timestamp=0`
@@ -434,10 +415,7 @@ classes. All empty in capture.
### Instructors ### Instructors
`GET /v1/Instructors/Instructors?timestamp=0` — instructor directory. Large list `GET /v1/Instructors/Instructors?timestamp=0` — instructor directory:
(hundreds of rows), mostly `isActive: false` and/or `isDeleted: true` legacy
staff; `position` is a department label (`Swim`, `Tennis`, `Sales`, …). Only
needed to resolve a booking's `instructorId` to a name.
```json ```json
{ {
@@ -450,8 +428,7 @@ needed to resolve a booking's `instructorId` to a name.
"photoUrl": null, "photoUrl": null,
"description": null, "description": null,
"companyId": 251, "companyId": 251,
"id": 0, "id": 0
"isDeleted": false
} }
``` ```
@@ -460,22 +437,8 @@ needed to resolve a booking's `instructorId` to a name.
capture). capture).
`GET /v1/PersonalTrainings/PersonalTrainingsTypes?timestamp=0` — PT session-type `GET /v1/PersonalTrainings/PersonalTrainingsTypes?timestamp=0` — PT session-type
catalogue; `personalTrainingTypeId` on a PT booking points here. This is where the catalogue (`name`, `duration`, `productId`). `personalTrainingTypeId` on a PT
session `duration` lives (the booking itself carries only start/end). `name` booking points here.
ranges over paid sessions (`"60 min PT session"`, `"45 min PT session FREE"`),
consultations/reviews, and non-session blocks (`"Lunch 30 min"`, `"Shower 15
min"`). Many rows are `isDeleted: true` legacy types.
```json
{
"name": "60 min PT session",
"duration": "01:00",
"productId": 105674,
"companyId": 251,
"id": 0,
"isDeleted": false
}
```
### Products & pricing ### Products & pricing