matrix-workouts-core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +70 -0
- package/LICENSE +21 -0
- package/MATRIX_API.md +245 -0
- package/README.md +123 -0
- package/dist/api/client.d.ts +63 -0
- package/dist/api/client.d.ts.map +1 -0
- package/dist/api/client.js +112 -0
- package/dist/api/client.js.map +1 -0
- package/dist/api/credentials.d.ts +24 -0
- package/dist/api/credentials.d.ts.map +1 -0
- package/dist/api/credentials.js +32 -0
- package/dist/api/credentials.js.map +1 -0
- package/dist/api/index.d.ts +4 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +4 -0
- package/dist/api/index.js.map +1 -0
- package/dist/api/login.d.ts +43 -0
- package/dist/api/login.d.ts.map +1 -0
- package/dist/api/login.js +97 -0
- package/dist/api/login.js.map +1 -0
- package/dist/export/document.d.ts +108 -0
- package/dist/export/document.d.ts.map +1 -0
- package/dist/export/document.js +97 -0
- package/dist/export/document.js.map +1 -0
- package/dist/export/index.d.ts +2 -0
- package/dist/export/index.d.ts.map +1 -0
- package/dist/export/index.js +2 -0
- package/dist/export/index.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/node/bin/history.d.ts +3 -0
- package/dist/node/bin/history.d.ts.map +1 -0
- package/dist/node/bin/history.js +80 -0
- package/dist/node/bin/history.js.map +1 -0
- package/dist/node/history.d.ts +60 -0
- package/dist/node/history.d.ts.map +1 -0
- package/dist/node/history.js +126 -0
- package/dist/node/history.js.map +1 -0
- package/dist/node/index.d.ts +2 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +2 -0
- package/dist/node/index.js.map +1 -0
- package/dist/parse/controlSignature.d.ts +38 -0
- package/dist/parse/controlSignature.d.ts.map +1 -0
- package/dist/parse/controlSignature.js +42 -0
- package/dist/parse/controlSignature.js.map +1 -0
- package/dist/parse/heartRate.d.ts +78 -0
- package/dist/parse/heartRate.d.ts.map +1 -0
- package/dist/parse/heartRate.js +57 -0
- package/dist/parse/heartRate.js.map +1 -0
- package/dist/parse/index.d.ts +30 -0
- package/dist/parse/index.d.ts.map +1 -0
- package/dist/parse/index.js +33 -0
- package/dist/parse/index.js.map +1 -0
- package/dist/parse/machine.d.ts +42 -0
- package/dist/parse/machine.d.ts.map +1 -0
- package/dist/parse/machine.js +65 -0
- package/dist/parse/machine.js.map +1 -0
- package/dist/parse/persist.d.ts +13 -0
- package/dist/parse/persist.d.ts.map +1 -0
- package/dist/parse/persist.js +40 -0
- package/dist/parse/persist.js.map +1 -0
- package/dist/parse/program.d.ts +58 -0
- package/dist/parse/program.d.ts.map +1 -0
- package/dist/parse/program.js +51 -0
- package/dist/parse/program.js.map +1 -0
- package/dist/parse/types.d.ts +127 -0
- package/dist/parse/types.d.ts.map +1 -0
- package/dist/parse/types.js +5 -0
- package/dist/parse/types.js.map +1 -0
- package/dist/parse/workout.d.ts +22 -0
- package/dist/parse/workout.d.ts.map +1 -0
- package/dist/parse/workout.js +130 -0
- package/dist/parse/workout.js.map +1 -0
- package/package.json +68 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The version history source of truth is git tags in the format `vMAJOR.MINOR.PATCH`.
|
|
6
|
+
|
|
7
|
+
## [0.1.0] - 2026-09-14
|
|
8
|
+
|
|
9
|
+
First public release. Node 20+, ESM only, **zero runtime dependencies**.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **`matrix-workouts-core`** — the platform-free entry point. Nothing reachable from
|
|
14
|
+
it touches a DOM, a `chrome.*`, a `node:` module or `fetch`, which is what lets one
|
|
15
|
+
parser serve a browser extension, a CLI and an MCP server without a fork. A test
|
|
16
|
+
asserts it per identifier; `tsconfig` omits the DOM lib so the compiler agrees.
|
|
17
|
+
- `toWorkout` normalizes either upstream shape — camelCase from the site's
|
|
18
|
+
`localStorage`, snake_case from the API — into a `Workout` whose field names carry
|
|
19
|
+
units. `loadCachedWorkouts` reads the browser's persisted blob through an injected
|
|
20
|
+
`ReadableStorage`.
|
|
21
|
+
- `findWorkout` accepts **either** id a record has. Rides recorded before
|
|
22
|
+
13 Aug 2026 carry a record id that differs from the id their own URL uses, so a
|
|
23
|
+
lookup that knows only one of them fails on half a history.
|
|
24
|
+
- `heartRateStats` and `flagHeartRateDropouts` filter chest-strap dropouts. The mask
|
|
25
|
+
is *validity*: `true` means the reading is real.
|
|
26
|
+
- `programMode` names the console program behind a numeric `programType`;
|
|
27
|
+
`controlSignature` derives what the console was actually holding constant from the
|
|
28
|
+
series, which is the more trustworthy of the two.
|
|
29
|
+
- `fetchWorkoutHistory` over an injected `FetchLike` — the client never calls
|
|
30
|
+
`fetch` itself. `loginWithXid` exchanges an xid and passcode for credentials and
|
|
31
|
+
returns nothing else; `readCredentials` borrows a session the browser already has.
|
|
32
|
+
- `workoutExport` builds the export document, carrying the upstream record verbatim
|
|
33
|
+
alongside the normalized telemetry so the file is never a worse record of the ride
|
|
34
|
+
than the browser already had.
|
|
35
|
+
- **`matrix-workouts-core/node`** — the one place real `fetch` and real files are
|
|
36
|
+
used, behind its own entry point so a browser bundle cannot reach `node:fs` through
|
|
37
|
+
the root import. `httpFetch`, `downloadHistory`, `readRawHistoryFile`, `loadEnvFile`.
|
|
38
|
+
Downloads are written `0600`; they are one person's heart rate.
|
|
39
|
+
- **`matrix-workouts-history`** — a CLI that signs in and downloads an entire history,
|
|
40
|
+
optionally one export document per ride. The only thing here that handles a passcode.
|
|
41
|
+
- `parseHistoryResponse`, so a response read back from a cache file gets the same
|
|
42
|
+
tolerance as one read off the wire: the same accepted shapes, the same per-record
|
|
43
|
+
failure isolation, the same truncation check.
|
|
44
|
+
|
|
45
|
+
### Notes
|
|
46
|
+
|
|
47
|
+
- **Scope is the indoor bike**, upright and recumbent, which is what the fixtures
|
|
48
|
+
cover and what this is verified against. Treadmill and rower records parse, but
|
|
49
|
+
nothing here is tuned for them and no fixture backs them.
|
|
50
|
+
- **Both heart-rate averages are preserved**, never reconciled. The platform's summary
|
|
51
|
+
is not derived from the sample series and does not always agree with it; on a ride
|
|
52
|
+
with independent Apple Watch ground truth the console's 153 bpm was exactly right
|
|
53
|
+
while the filtered series gave 151. On a glitching strap *theirs* is the better
|
|
54
|
+
number, so both are carried and a consumer decides.
|
|
55
|
+
- **Paging is deliberately not followed.** The workouts endpoint has returned every
|
|
56
|
+
record in one response on every account seen; inventing page parameters against an
|
|
57
|
+
undocumented API is a good way to silently truncate someone's history. A
|
|
58
|
+
`paging.total` larger than what arrived surfaces as `truncated` instead.
|
|
59
|
+
- **The export format identifier stays `full-matrix-workouts/workout`.** It names the
|
|
60
|
+
repository this code came from, which no longer exists under that name — and it is
|
|
61
|
+
not going to be renamed. It is an opaque identifier in files already written to
|
|
62
|
+
people's disks, and changing it would orphan every one of them for the sake of
|
|
63
|
+
tidiness. `src/export/contract.test.ts` pins it.
|
|
64
|
+
|
|
65
|
+
### Provenance
|
|
66
|
+
|
|
67
|
+
Extracted from the `full-matrix-workouts` Chrome extension, now
|
|
68
|
+
[matrix-workouts-chrome](https://github.com/bestimmaa/matrix-workouts-chrome), which
|
|
69
|
+
consumes this package instead of its own copy. The extension's rendering is unchanged
|
|
70
|
+
across the move: every fixture's preview output is byte-identical.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Christoph Halang
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/MATRIX_API.md
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# MATRIX_API.md — the jfit HTTP API
|
|
2
|
+
|
|
3
|
+
Reference for the undocumented API behind `matrixworkouts.jfit.co` (Matrix / Johnson
|
|
4
|
+
Fitness). Written while building the standalone client in `src/cli/history.ts`.
|
|
5
|
+
|
|
6
|
+
**Everything here was read out of the site's own JavaScript bundle or confirmed
|
|
7
|
+
against the live API on 14 Sep 2026.** Each endpoint below is marked *verified* (a
|
|
8
|
+
request was actually made and the response observed) or *from the bundle* (the call
|
|
9
|
+
site was read, but never exercised). Do not promote the second kind to the first by
|
|
10
|
+
assuming — this API is undocumented, unversioned, and **its record shape has already
|
|
11
|
+
changed at least once** (see AGENTS.md, "The upstream shape has changed").
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Hosts
|
|
16
|
+
|
|
17
|
+
| Host | Use |
|
|
18
|
+
|---|---|
|
|
19
|
+
| `https://apollo.jfit.co` | **The one that works.** Sign-in and workout history. |
|
|
20
|
+
| `https://orion.jfit.co` | Configured in the bundle as `exerciserApi`, answers **403**. Do not use. |
|
|
21
|
+
|
|
22
|
+
The bundle sets both up in one function:
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
e.apolloBackend.setup({ baseUrl: `https://apollo.jfit.co` });
|
|
26
|
+
e.exerciserApi.setup({ baseUrl: `https://orion.jfit.co` });
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The Apollo client is created with `headers: { Accept: "application/json" }` and no
|
|
30
|
+
authentication of its own; the bearer token is attached per request.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Authentication
|
|
35
|
+
|
|
36
|
+
Two independent token systems appear in the bundle. Only the first matters here.
|
|
37
|
+
|
|
38
|
+
**Apollo (what we use).** `POST /exerciser/login` returns a profile with a `token`
|
|
39
|
+
field. Send it as `Authorization: Bearer <token>` on subsequent requests.
|
|
40
|
+
|
|
41
|
+
**Orion `jumpToken`.** A separate async request transform attaches
|
|
42
|
+
`Bearer ${token.accessToken}` from `authStore.jumpToken`. Irrelevant while Orion
|
|
43
|
+
answers 403.
|
|
44
|
+
|
|
45
|
+
Inside the browser there is no need to sign in at all: the site leaves the token in
|
|
46
|
+
the redux-persist blob at `userStore.exerciserProfile.token`, which is what
|
|
47
|
+
`readCredentials` (`src/api/credentials.ts`) reads. **Prefer that.** It handles no
|
|
48
|
+
passcode, needs no network, and cannot be rate-limited. Signing in with an xid is
|
|
49
|
+
only for the standalone client, which has no browser session to borrow.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## `POST /exerciser/login` — *verified*
|
|
54
|
+
|
|
55
|
+
Exchange a member number (xid) and numeric passcode for a bearer token.
|
|
56
|
+
|
|
57
|
+
The request shape is not guessed; it is the site's own, from the bundle:
|
|
58
|
+
|
|
59
|
+
```js
|
|
60
|
+
async loginWithXid(e, t, n = `xid`, r = 0) {
|
|
61
|
+
let i = { username: e, password: t, type: n, club_id: r };
|
|
62
|
+
return apisauce.post(`/exerciser/login`, i);
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Request** — `Content-Type: application/json`
|
|
67
|
+
|
|
68
|
+
| Field | Value |
|
|
69
|
+
|---|---|
|
|
70
|
+
| `username` | the xid (member number). **Not** an email. |
|
|
71
|
+
| `password` | the numeric passcode |
|
|
72
|
+
| `type` | `"xid"`. The bundle's other value is `"apollo"`, used by `loginWithNpUUID` for SSO with an empty password. |
|
|
73
|
+
| `club_id` | `0` in every call the site makes |
|
|
74
|
+
|
|
75
|
+
**Response** `200` — a flat profile object:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
first_name last_name email units gender height weight
|
|
79
|
+
birthday age pictureUrl aliases identities id token
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Only two fields matter: **`id`** is the exerciser id for the workouts path, and
|
|
83
|
+
**`token`** is the bearer token. `src/api/login.ts` deliberately returns only those
|
|
84
|
+
two — the rest is PII that no later call needs.
|
|
85
|
+
|
|
86
|
+
A wrong xid or passcode answers `401`.
|
|
87
|
+
|
|
88
|
+
> **Rate limiting is unmeasured.** Nothing in the bundle suggests a lockout, and none
|
|
89
|
+
> was hit in testing, but that is not evidence of absence. Do not loop this endpoint.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## `GET /exerciser/{id}/workouts` — *verified*
|
|
94
|
+
|
|
95
|
+
**The whole point.** Returns complete workout records *including every interval* —
|
|
96
|
+
one request gets the entire history.
|
|
97
|
+
|
|
98
|
+
**Request** — `Authorization: Bearer <token>`, `Accept: application/json`
|
|
99
|
+
|
|
100
|
+
The site always sends a date window:
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
apisauce.get(`/exerciser/${e}/workouts`, {
|
|
104
|
+
startdate: Math.floor(t.getTime() / 1e3), // unix seconds
|
|
105
|
+
enddate: Math.floor(n.getTime() / 1e3),
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**Omitting both returns the full history**, which is what this project does. Confirmed
|
|
110
|
+
live: 47 records spanning 2026-01-10 to 2026-09-13 in a single 1.8 MB response.
|
|
111
|
+
|
|
112
|
+
**Response** `200` — `{ workouts, messages, paging }`
|
|
113
|
+
|
|
114
|
+
```json
|
|
115
|
+
{ "paging": { "returned": 47, "total": 47, "page": 1 } }
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`paging` has come back complete (`returned === total`) on every account seen.
|
|
119
|
+
**Paging is deliberately not followed** — inventing page parameters against an
|
|
120
|
+
undocumented API is a good way to silently truncate someone's history — so a `total`
|
|
121
|
+
larger than what arrived surfaces as `truncated` instead. See `src/api/client.ts`.
|
|
122
|
+
|
|
123
|
+
### Record fields
|
|
124
|
+
|
|
125
|
+
Both spellings, because both occur: the API sends the left column, the browser's
|
|
126
|
+
persisted blob sends the right. This table is the single reference for the upstream
|
|
127
|
+
shape — AGENTS.md carries what the fields *mean* and which of them lie, not what they
|
|
128
|
+
are called.
|
|
129
|
+
|
|
130
|
+
| API (snake_case) | Cache (camelCase) | Notes |
|
|
131
|
+
|---|---|---|
|
|
132
|
+
| `workout_id` | `workoutId` | names the ride. **Not** the URL segment |
|
|
133
|
+
| `id` | `id` | document id, and the `/workouts/:id` segment the site links to |
|
|
134
|
+
| `model_id` | `modelId` | the machine **model** — not the individual unit |
|
|
135
|
+
| `machine_id` | `machineId` | UUID of the physical machine |
|
|
136
|
+
| `machine_type` / `exercise_title` | `machineType` / `exerciseTitle` | `upright_bike`, `treadmill`, `rower`, … |
|
|
137
|
+
| `workout_type` | `workoutType` | `cardio` |
|
|
138
|
+
| `workout_source` | `workoutSource` | `connected` = machine-recorded |
|
|
139
|
+
| `workout_time` | `workoutTime` | ISO 8601, **UTC** |
|
|
140
|
+
| `duration` | `duration` | **seconds** |
|
|
141
|
+
| `distance` | `distance` | **meters** (the UI renders km) |
|
|
142
|
+
| `calories` | `calories` | kcal |
|
|
143
|
+
| `min/max/average_heart_rate` | `min/max/averageHeartRate` | bpm |
|
|
144
|
+
| `program_type` | `programType` | integer console program id (e.g. `46`) |
|
|
145
|
+
| `program_id` | — | API only; all-zero UUID on every ride seen |
|
|
146
|
+
| `program_level` | — | API only |
|
|
147
|
+
| `watts_kg` | `wattsKg` | often `0` |
|
|
148
|
+
| `workout_originator` | — | API only; empty string on every ride seen |
|
|
149
|
+
| `integration_metadata` | — | API only; `{}` on every ride seen |
|
|
150
|
+
| `archived` | `archived` | `0` / `1` |
|
|
151
|
+
| `intervals` | `intervals` | the sample array — see below |
|
|
152
|
+
|
|
153
|
+
Cache-only extras, absent from the API: `functionThresholdPower`, `peakRpm`,
|
|
154
|
+
`averageRpm`, `peakSpm`, `totalStrokes` (often `0`, several machine-type specific),
|
|
155
|
+
and `totalSweatScore`, `sprintScores`, `sprint8ProgramLevel` on **Sprint 8 rides only**.
|
|
156
|
+
|
|
157
|
+
**Field presence is not a promise.** `program_level` and `sprint8ProgramLevel` have
|
|
158
|
+
already disappeared from records that still carry the Sprint 8 scores. Never assume a
|
|
159
|
+
field is meaningful just because it is present and zero.
|
|
160
|
+
|
|
161
|
+
### Interval fields — the reason this project exists
|
|
162
|
+
|
|
163
|
+
Each entry in `intervals` is one ~10-second sample:
|
|
164
|
+
|
|
165
|
+
| API | Cache | Unit | Notes |
|
|
166
|
+
|---|---|---|---|
|
|
167
|
+
| `power` | `power` | watts | **not in the stock UI** |
|
|
168
|
+
| `resistance` | `resistance` | console level (1–30 observed) | **not in the stock UI** — discrete, moves in steps |
|
|
169
|
+
| `rpm` | `rpm` | cadence | **not in the stock UI** |
|
|
170
|
+
| `speed` | `speed` | km/h | |
|
|
171
|
+
| `heart_rate` | `heartRate` | bpm | `0` on a chest-strap dropout |
|
|
172
|
+
| `incline` | `incline` | % | treadmill-relevant; `0` on a bike |
|
|
173
|
+
| `average_distance` | `averageDistance` | meters | **cumulative** distance, despite the name |
|
|
174
|
+
| `distance` | `distance` | meters | per-sample delta |
|
|
175
|
+
| `duration` | `duration` | seconds | `10` for every sample but the last, a partial (0–11) |
|
|
176
|
+
| `total_steps` | `totalSteps` | count | populated on bikes too |
|
|
177
|
+
|
|
178
|
+
Sample count × 10 s ≈ the record's `duration`.
|
|
179
|
+
|
|
180
|
+
**`rpm`, `power` and `resistance` appear nowhere in the stock UI.** The stock detail
|
|
181
|
+
page shows six tiles: distance, avg incline, avg heart rate, calories, duration, avg
|
|
182
|
+
speed. That is the entire gap this project closes.
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Two shapes for the same data
|
|
187
|
+
|
|
188
|
+
**The API returns `snake_case`; the browser's persisted blob returns `camelCase`** —
|
|
189
|
+
including inside `intervals` (`average_distance` vs `averageDistance`).
|
|
190
|
+
|
|
191
|
+
The API also carries four fields the cached blob does not: `program_id`,
|
|
192
|
+
`program_level`, `workout_originator`, `integration_metadata`.
|
|
193
|
+
|
|
194
|
+
That is the wire fact. The rule it implies for code in this repo — always go through
|
|
195
|
+
`camelizeWorkout()`, never read raw keys — lives in AGENTS.md, which is where anyone
|
|
196
|
+
writing against the parse layer will be looking.
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Endpoints that do not work
|
|
201
|
+
|
|
202
|
+
| Endpoint | Result |
|
|
203
|
+
|---|---|
|
|
204
|
+
| `GET /workouts/{id}` | **404.** In the bundle, but dead. Fetch the list and filter. |
|
|
205
|
+
| anything on `orion.jfit.co` | **403** |
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## The rest of the surface — *from the bundle, unverified*
|
|
210
|
+
|
|
211
|
+
Present in the JavaScript, never exercised. Listed so nobody has to re-read a 1.8 MB
|
|
212
|
+
bundle to find them; **treat every shape here as unconfirmed.**
|
|
213
|
+
|
|
214
|
+
| Endpoint | Bundle payload |
|
|
215
|
+
|---|---|
|
|
216
|
+
| `POST /exerciser/reset_password` | `{ xid }` |
|
|
217
|
+
| `POST /exerciser/register` | the profile, `decamelizeKeys`'d |
|
|
218
|
+
| `POST /exerciser/validate` | — |
|
|
219
|
+
| `POST /exerciser/exchange_token_for_exerciser` | `{ token, vendor: "upace" }` |
|
|
220
|
+
| `GET /exerciser` | — |
|
|
221
|
+
| `PUT /exerciser/{id}` | profile update |
|
|
222
|
+
| `POST /dapi/login`, `POST /dapi/addUser`, `POST /dapi/unlinkUser` | the "dapi" account system |
|
|
223
|
+
| `POST /dapi/dapi-exchange` | `{ token }` |
|
|
224
|
+
| `GET /dapi/user` | `{ user_id }`, headers `session` and `user-uuid` |
|
|
225
|
+
| `GET /dapi/machine`, `GET /dapi/usermachine` | — |
|
|
226
|
+
| `POST /brand/graphql` | branding/CMS |
|
|
227
|
+
| `POST /wallet/pass` | Apple/Google wallet pass |
|
|
228
|
+
|
|
229
|
+
`reset_password` and the `dapi` write endpoints change account state. **Do not probe
|
|
230
|
+
them.**
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## Using it
|
|
235
|
+
|
|
236
|
+
From this repo:
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
cp .env.example .env # fill in MATRIX_XID and MATRIX_PIN
|
|
240
|
+
npm run history # full history -> history/
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
See `src/cli/history.ts`. Credentials live in `.env` (gitignored), are used for the
|
|
244
|
+
single sign-in request, and are never written to output or logged. The `history/`
|
|
245
|
+
directory is gitignored: those files are the rider's resting heart rate.
|
package/README.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# matrix-workouts-core
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/matrix-workouts-core)
|
|
4
|
+
[](https://github.com/bestimmaa/matrix-workouts-core/actions/workflows/ci.yml)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
Read what a Matrix / Johnson Fitness exercise bike actually recorded.
|
|
8
|
+
|
|
9
|
+
The dashboard at `matrixworkouts.jfit.co` shows six tiles per ride: distance, average
|
|
10
|
+
incline, average heart rate, calories, duration, average speed. The record behind that
|
|
11
|
+
page also carries **a sample every 10 seconds with power (watts), console resistance
|
|
12
|
+
level and cadence (rpm)** — none of which appears anywhere in the site's UI. This
|
|
13
|
+
package parses those records, talks to the API that holds the full history, and
|
|
14
|
+
writes a lossless export document. Those three fields are why it exists.
|
|
15
|
+
|
|
16
|
+
It has **no runtime dependencies** and touches no platform: no DOM, no `chrome.*`, no
|
|
17
|
+
`node:`, no `fetch`. A test asserts that on every run. Node-only helpers live behind a
|
|
18
|
+
separate entry point, so a browser bundle cannot pull them in by accident.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install matrix-workouts-core
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Node 20 or newer. ESM only.
|
|
27
|
+
|
|
28
|
+
## Quickstart
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { toWorkout, heartRateStats, fetchWorkoutHistory } from "matrix-workouts-core";
|
|
32
|
+
|
|
33
|
+
const workout = toWorkout(record); // one raw record -> normalized model
|
|
34
|
+
workout.samples[0]?.powerWatts; // the thing the site never shows
|
|
35
|
+
heartRateStats(workout.samples); // dropout-filtered, with the count
|
|
36
|
+
|
|
37
|
+
// The API needs a fetch; it never calls one itself.
|
|
38
|
+
const { workouts, truncated, skipped } = await fetchWorkoutHistory(credentials, myFetch);
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Under Node, the fetch adapter is already written:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { httpFetch, downloadHistory } from "matrix-workouts-core/node";
|
|
45
|
+
import { loginWithXid } from "matrix-workouts-core";
|
|
46
|
+
|
|
47
|
+
const credentials = await loginWithXid({ xid, pin }, httpFetch);
|
|
48
|
+
const result = await downloadHistory(credentials, { outDir: "history", split: true });
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Download a whole history
|
|
52
|
+
|
|
53
|
+
The site's own list page holds roughly the current week and errors on anything older.
|
|
54
|
+
The API has everything, so the package ships a CLI that signs in and takes the lot:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
cp .env.example .env # your xid and passcode
|
|
58
|
+
npx matrix-workouts-history # -> history/raw-history-<timestamp>.json
|
|
59
|
+
npx matrix-workouts-history --split # plus one export document per ride
|
|
60
|
+
npx matrix-workouts-history --out data # somewhere other than history/
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
| Variable | Required | Meaning |
|
|
64
|
+
|---|---|---|
|
|
65
|
+
| `MATRIX_XID` | yes | the member number printed on the gym tag |
|
|
66
|
+
| `MATRIX_PIN` | yes | the numeric passcode |
|
|
67
|
+
|
|
68
|
+
Read from `.env` in the working directory or from the environment; the environment
|
|
69
|
+
wins. Used for one sign-in request, never written to the output and never printed.
|
|
70
|
+
|
|
71
|
+
## What it exports
|
|
72
|
+
|
|
73
|
+
| Entry point | Holds |
|
|
74
|
+
|---|---|
|
|
75
|
+
| `matrix-workouts-core` | `toWorkout`, `loadCachedWorkouts`, `findWorkout`, `heartRateStats`, `flagHeartRateDropouts`, `controlSignature`, `programMode`, `fetchWorkoutHistory`, `loginWithXid`, `readCredentials`, `workoutExport`, and the `Workout` / `Sample` types |
|
|
76
|
+
| `matrix-workouts-core/node` | `httpFetch`, `downloadHistory`, `readRawHistoryFile`, `loadEnvFile` — real fetch, real files |
|
|
77
|
+
|
|
78
|
+
Units are in the names (`powerWatts`, `cadenceRpm`, `distanceMeters`), because the
|
|
79
|
+
upstream names are ambiguous and one of them — `averageDistance`, which is cumulative
|
|
80
|
+
— is actively misleading. Every record keeps its untouched original under `raw`, so an
|
|
81
|
+
export is lossless even where this model has no name for a field.
|
|
82
|
+
|
|
83
|
+
[MATRIX_API.md](MATRIX_API.md) documents the upstream API: endpoints, wire shapes,
|
|
84
|
+
units, and which parts are verified rather than read out of the site's bundle.
|
|
85
|
+
|
|
86
|
+
## Privacy and scope
|
|
87
|
+
|
|
88
|
+
**Scope: indoor bikes** — upright and recumbent. Treadmill and rower records parse,
|
|
89
|
+
but nothing here is tuned for them.
|
|
90
|
+
|
|
91
|
+
This library reads one account's own data and sends it nowhere except the API it came
|
|
92
|
+
from. The CLI handles a passcode for exactly one sign-in request and never stores,
|
|
93
|
+
logs or prints it; downloads are written `0600` because they are the rider's heart
|
|
94
|
+
rate. Where you point `--out` is where that data lives — choose accordingly.
|
|
95
|
+
|
|
96
|
+
## Development
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
npm test # vitest
|
|
100
|
+
npm run typecheck
|
|
101
|
+
npm run build # -> dist/
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
All three must pass before committing. See [AGENTS.md](AGENTS.md) for the decisions
|
|
105
|
+
and rules behind the code, and [CHANGELOG.md](CHANGELOG.md) for the release history.
|
|
106
|
+
|
|
107
|
+
Cutting a release:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
npm run release -- patch # bumps, tags, packs; refuses without a CHANGELOG entry
|
|
111
|
+
git push origin main:main --follow-tags
|
|
112
|
+
npm publish --access public
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Part of matrix-workouts
|
|
116
|
+
|
|
117
|
+
| | |
|
|
118
|
+
|---|---|
|
|
119
|
+
| [matrix-workouts-core](https://github.com/bestimmaa/matrix-workouts-core) | this package: parser, API client, export format |
|
|
120
|
+
| [matrix-workouts-chrome](https://github.com/bestimmaa/matrix-workouts-chrome) | the Chrome extension that puts it on screen |
|
|
121
|
+
| [matrix-workouts-mcp](https://github.com/bestimmaa/matrix-workouts-mcp) | an MCP server, so an AI agent can ask about your rides |
|
|
122
|
+
|
|
123
|
+
MIT licensed. Unaffiliated with Matrix Fitness or Johnson Health Tech.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { Workout } from "../parse/types.js";
|
|
2
|
+
import { type Credentials } from "./credentials.js";
|
|
3
|
+
/**
|
|
4
|
+
* Client for the jfit HTTP API — the only route to history deeper than the week
|
|
5
|
+
* the site caches in localStorage.
|
|
6
|
+
*
|
|
7
|
+
* Host is `apollo.jfit.co`. `orion.jfit.co` appears in the site's bundle but answers
|
|
8
|
+
* 403; do not use it. `GET /workouts/{id}` is likewise in the bundle and answers
|
|
9
|
+
* 404 — the list endpoint is the only one that works, and it returns complete
|
|
10
|
+
* records with every interval, so one request gets everything.
|
|
11
|
+
*/
|
|
12
|
+
export declare const API_ORIGIN = "https://apollo.jfit.co";
|
|
13
|
+
export declare class ApiError extends Error {
|
|
14
|
+
/** HTTP status, or 0 for a transport failure. */
|
|
15
|
+
readonly status: number;
|
|
16
|
+
name: string;
|
|
17
|
+
constructor(message: string,
|
|
18
|
+
/** HTTP status, or 0 for a transport failure. */
|
|
19
|
+
status: number);
|
|
20
|
+
}
|
|
21
|
+
/** Just the part of `fetch` we use, so tests need no network and no globals. */
|
|
22
|
+
export interface FetchInit {
|
|
23
|
+
headers: Record<string, string>;
|
|
24
|
+
/** Absent means GET. Only the sign-in request sets this. */
|
|
25
|
+
method?: string;
|
|
26
|
+
/** Already-serialized JSON, for the one request that has a body. */
|
|
27
|
+
body?: string;
|
|
28
|
+
}
|
|
29
|
+
export type FetchLike = (url: string, init: FetchInit) => Promise<{
|
|
30
|
+
ok: boolean;
|
|
31
|
+
status: number;
|
|
32
|
+
json(): Promise<unknown>;
|
|
33
|
+
}>;
|
|
34
|
+
export declare function workoutsUrl(exerciserId: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* Fetch the exerciser's full workout history.
|
|
37
|
+
*
|
|
38
|
+
* The response is `{ workouts, messages, paging }`. Paging is deliberately not
|
|
39
|
+
* followed: the endpoint has returned every record in one response on the accounts
|
|
40
|
+
* seen so far, and inventing page parameters against an undocumented API is a good
|
|
41
|
+
* way to silently truncate someone's history. If `paging` ever indicates more than
|
|
42
|
+
* arrived, that surfaces as `truncated` rather than being quietly dropped.
|
|
43
|
+
*/
|
|
44
|
+
export interface HistoryResult {
|
|
45
|
+
workouts: Workout[];
|
|
46
|
+
/** True when the response's own paging says there is more than we received. */
|
|
47
|
+
truncated: boolean;
|
|
48
|
+
/** Records the API returned that this parser could not read. */
|
|
49
|
+
skipped: number;
|
|
50
|
+
}
|
|
51
|
+
export declare function fetchWorkoutHistory(credentials: Credentials, fetchImpl: FetchLike): Promise<HistoryResult>;
|
|
52
|
+
/**
|
|
53
|
+
* Turn a history response body into the normalized result.
|
|
54
|
+
*
|
|
55
|
+
* Split out of `fetchWorkoutHistory` so that a response saved to disk and read back
|
|
56
|
+
* later — which is how the standalone client and the MCP server cache a history —
|
|
57
|
+
* goes through exactly the same tolerance: the same accepted shapes, the same
|
|
58
|
+
* per-record failure isolation, the same truncation check. A second reader that
|
|
59
|
+
* reimplemented any of that would drift from this one on the day the upstream shape
|
|
60
|
+
* changes again.
|
|
61
|
+
*/
|
|
62
|
+
export declare function parseHistoryResponse(body: unknown): HistoryResult;
|
|
63
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAU,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAE5D;;;;;;;;GAQG;AACH,eAAO,MAAM,UAAU,2BAA2B,CAAC;AAEnD,qBAAa,QAAS,SAAQ,KAAK;IAI/B,iDAAiD;IACjD,QAAQ,CAAC,MAAM,EAAE,MAAM;IAJhB,IAAI,SAAc;gBAEzB,OAAO,EAAE,MAAM;IACf,iDAAiD;IACxC,MAAM,EAAE,MAAM;CAI1B;AAED,gFAAgF;AAChF,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,4DAA4D;IAC5D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oEAAoE;IACpE,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,MAAM,SAAS,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,KAAK,OAAO,CAAC;IAChE,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;CAC1B,CAAC,CAAC;AAEH,wBAAgB,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,+EAA+E;IAC/E,SAAS,EAAE,OAAO,CAAC;IACnB,gEAAgE;IAChE,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,wBAAsB,mBAAmB,CACvC,WAAW,EAAE,WAAW,EACxB,SAAS,EAAE,SAAS,GACnB,OAAO,CAAC,aAAa,CAAC,CA8BxB;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,OAAO,GAAG,aAAa,CAejE"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { toWorkout } from "../parse/workout.js";
|
|
2
|
+
import { redact } from "./credentials.js";
|
|
3
|
+
/**
|
|
4
|
+
* Client for the jfit HTTP API — the only route to history deeper than the week
|
|
5
|
+
* the site caches in localStorage.
|
|
6
|
+
*
|
|
7
|
+
* Host is `apollo.jfit.co`. `orion.jfit.co` appears in the site's bundle but answers
|
|
8
|
+
* 403; do not use it. `GET /workouts/{id}` is likewise in the bundle and answers
|
|
9
|
+
* 404 — the list endpoint is the only one that works, and it returns complete
|
|
10
|
+
* records with every interval, so one request gets everything.
|
|
11
|
+
*/
|
|
12
|
+
export const API_ORIGIN = "https://apollo.jfit.co";
|
|
13
|
+
export class ApiError extends Error {
|
|
14
|
+
status;
|
|
15
|
+
name = "ApiError";
|
|
16
|
+
constructor(message,
|
|
17
|
+
/** HTTP status, or 0 for a transport failure. */
|
|
18
|
+
status) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.status = status;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function workoutsUrl(exerciserId) {
|
|
24
|
+
return `${API_ORIGIN}/exerciser/${encodeURIComponent(exerciserId)}/workouts`;
|
|
25
|
+
}
|
|
26
|
+
export async function fetchWorkoutHistory(credentials, fetchImpl) {
|
|
27
|
+
const url = workoutsUrl(credentials.exerciserId);
|
|
28
|
+
let response;
|
|
29
|
+
try {
|
|
30
|
+
response = await fetchImpl(url, {
|
|
31
|
+
headers: {
|
|
32
|
+
Authorization: `Bearer ${credentials.token}`,
|
|
33
|
+
Accept: "application/json",
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
// A transport failure's message can carry the request, so redact before it
|
|
39
|
+
// escapes into a UI string or a log.
|
|
40
|
+
const detail = error instanceof Error ? redact(error.message, credentials.token) : "";
|
|
41
|
+
throw new ApiError(`Could not reach the workout API. ${detail}`.trim(), 0);
|
|
42
|
+
}
|
|
43
|
+
if (!response.ok) {
|
|
44
|
+
throw new ApiError(describeStatus(response.status), response.status);
|
|
45
|
+
}
|
|
46
|
+
let body;
|
|
47
|
+
try {
|
|
48
|
+
body = await response.json();
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
throw new ApiError("The workout API returned something that is not JSON.", response.status);
|
|
52
|
+
}
|
|
53
|
+
return parseHistoryResponse(body);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Turn a history response body into the normalized result.
|
|
57
|
+
*
|
|
58
|
+
* Split out of `fetchWorkoutHistory` so that a response saved to disk and read back
|
|
59
|
+
* later — which is how the standalone client and the MCP server cache a history —
|
|
60
|
+
* goes through exactly the same tolerance: the same accepted shapes, the same
|
|
61
|
+
* per-record failure isolation, the same truncation check. A second reader that
|
|
62
|
+
* reimplemented any of that would drift from this one on the day the upstream shape
|
|
63
|
+
* changes again.
|
|
64
|
+
*/
|
|
65
|
+
export function parseHistoryResponse(body) {
|
|
66
|
+
const records = extractWorkouts(body);
|
|
67
|
+
const workouts = [];
|
|
68
|
+
let skipped = 0;
|
|
69
|
+
for (const record of records) {
|
|
70
|
+
// One malformed record must not cost the user their whole history.
|
|
71
|
+
try {
|
|
72
|
+
workouts.push(toWorkout(record));
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
skipped += 1;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
workouts.sort((a, b) => b.startedAt.getTime() - a.startedAt.getTime());
|
|
79
|
+
return { workouts, truncated: isTruncated(body, records.length), skipped };
|
|
80
|
+
}
|
|
81
|
+
function describeStatus(status) {
|
|
82
|
+
if (status === 401 || status === 403) {
|
|
83
|
+
return "The workout API rejected the stored sign-in. Reload the site to refresh it, then try again.";
|
|
84
|
+
}
|
|
85
|
+
if (status === 404)
|
|
86
|
+
return "The workout API has no history for this account.";
|
|
87
|
+
if (status >= 500)
|
|
88
|
+
return `The workout API is failing (HTTP ${status}). Try again later.`;
|
|
89
|
+
return `The workout API refused the request (HTTP ${status}).`;
|
|
90
|
+
}
|
|
91
|
+
function extractWorkouts(body) {
|
|
92
|
+
// The documented shape is { workouts, messages, paging }; a bare array is
|
|
93
|
+
// accepted too, since the upstream shape is undocumented and can change.
|
|
94
|
+
const list = Array.isArray(body)
|
|
95
|
+
? body
|
|
96
|
+
: typeof body === "object" && body !== null
|
|
97
|
+
? body["workouts"]
|
|
98
|
+
: null;
|
|
99
|
+
if (!Array.isArray(list))
|
|
100
|
+
return [];
|
|
101
|
+
return list.filter((item) => typeof item === "object" && item !== null && !Array.isArray(item));
|
|
102
|
+
}
|
|
103
|
+
function isTruncated(body, received) {
|
|
104
|
+
if (typeof body !== "object" || body === null)
|
|
105
|
+
return false;
|
|
106
|
+
const paging = body["paging"];
|
|
107
|
+
if (typeof paging !== "object" || paging === null)
|
|
108
|
+
return false;
|
|
109
|
+
const total = paging["total"];
|
|
110
|
+
return typeof total === "number" && total > received;
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ReadableStorage } from "../parse/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* The bearer token and exerciser id the site has already stored in this browser.
|
|
4
|
+
*
|
|
5
|
+
* PRIVACY: the token is read into memory on demand and passed straight to the one
|
|
6
|
+
* request that needs it. It is never logged, never written to `chrome.storage`, and
|
|
7
|
+
* never included in an error message — `redact()` below exists so a stray token in
|
|
8
|
+
* a URL cannot reach a thrown string either.
|
|
9
|
+
*/
|
|
10
|
+
export interface Credentials {
|
|
11
|
+
exerciserId: string;
|
|
12
|
+
token: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Pull credentials out of the persisted blob.
|
|
16
|
+
*
|
|
17
|
+
* The documented location is `userStore.exerciserProfile.token`, but the id is
|
|
18
|
+
* mirrored in several places depending on how the session was established, so the
|
|
19
|
+
* lookup is tolerant in the same spirit as the rest of the parse layer.
|
|
20
|
+
*/
|
|
21
|
+
export declare function readCredentials(storage: ReadableStorage): Credentials;
|
|
22
|
+
/** Strip anything token-shaped from a string before it can reach a message or log. */
|
|
23
|
+
export declare function redact(text: string, token: string): string;
|
|
24
|
+
//# sourceMappingURL=credentials.d.ts.map
|