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.
Files changed (77) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/LICENSE +21 -0
  3. package/MATRIX_API.md +245 -0
  4. package/README.md +123 -0
  5. package/dist/api/client.d.ts +63 -0
  6. package/dist/api/client.d.ts.map +1 -0
  7. package/dist/api/client.js +112 -0
  8. package/dist/api/client.js.map +1 -0
  9. package/dist/api/credentials.d.ts +24 -0
  10. package/dist/api/credentials.d.ts.map +1 -0
  11. package/dist/api/credentials.js +32 -0
  12. package/dist/api/credentials.js.map +1 -0
  13. package/dist/api/index.d.ts +4 -0
  14. package/dist/api/index.d.ts.map +1 -0
  15. package/dist/api/index.js +4 -0
  16. package/dist/api/index.js.map +1 -0
  17. package/dist/api/login.d.ts +43 -0
  18. package/dist/api/login.d.ts.map +1 -0
  19. package/dist/api/login.js +97 -0
  20. package/dist/api/login.js.map +1 -0
  21. package/dist/export/document.d.ts +108 -0
  22. package/dist/export/document.d.ts.map +1 -0
  23. package/dist/export/document.js +97 -0
  24. package/dist/export/document.js.map +1 -0
  25. package/dist/export/index.d.ts +2 -0
  26. package/dist/export/index.d.ts.map +1 -0
  27. package/dist/export/index.js +2 -0
  28. package/dist/export/index.js.map +1 -0
  29. package/dist/index.d.ts +16 -0
  30. package/dist/index.d.ts.map +1 -0
  31. package/dist/index.js +16 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/node/bin/history.d.ts +3 -0
  34. package/dist/node/bin/history.d.ts.map +1 -0
  35. package/dist/node/bin/history.js +80 -0
  36. package/dist/node/bin/history.js.map +1 -0
  37. package/dist/node/history.d.ts +60 -0
  38. package/dist/node/history.d.ts.map +1 -0
  39. package/dist/node/history.js +126 -0
  40. package/dist/node/history.js.map +1 -0
  41. package/dist/node/index.d.ts +2 -0
  42. package/dist/node/index.d.ts.map +1 -0
  43. package/dist/node/index.js +2 -0
  44. package/dist/node/index.js.map +1 -0
  45. package/dist/parse/controlSignature.d.ts +38 -0
  46. package/dist/parse/controlSignature.d.ts.map +1 -0
  47. package/dist/parse/controlSignature.js +42 -0
  48. package/dist/parse/controlSignature.js.map +1 -0
  49. package/dist/parse/heartRate.d.ts +78 -0
  50. package/dist/parse/heartRate.d.ts.map +1 -0
  51. package/dist/parse/heartRate.js +57 -0
  52. package/dist/parse/heartRate.js.map +1 -0
  53. package/dist/parse/index.d.ts +30 -0
  54. package/dist/parse/index.d.ts.map +1 -0
  55. package/dist/parse/index.js +33 -0
  56. package/dist/parse/index.js.map +1 -0
  57. package/dist/parse/machine.d.ts +42 -0
  58. package/dist/parse/machine.d.ts.map +1 -0
  59. package/dist/parse/machine.js +65 -0
  60. package/dist/parse/machine.js.map +1 -0
  61. package/dist/parse/persist.d.ts +13 -0
  62. package/dist/parse/persist.d.ts.map +1 -0
  63. package/dist/parse/persist.js +40 -0
  64. package/dist/parse/persist.js.map +1 -0
  65. package/dist/parse/program.d.ts +58 -0
  66. package/dist/parse/program.d.ts.map +1 -0
  67. package/dist/parse/program.js +51 -0
  68. package/dist/parse/program.js.map +1 -0
  69. package/dist/parse/types.d.ts +127 -0
  70. package/dist/parse/types.d.ts.map +1 -0
  71. package/dist/parse/types.js +5 -0
  72. package/dist/parse/types.js.map +1 -0
  73. package/dist/parse/workout.d.ts +22 -0
  74. package/dist/parse/workout.d.ts.map +1 -0
  75. package/dist/parse/workout.js +130 -0
  76. package/dist/parse/workout.js.map +1 -0
  77. package/package.json +68 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credentials.d.ts","sourceRoot":"","sources":["../../src/api/credentials.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEzD;;;;;;;GAOG;AACH,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;CACf;AAMD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,eAAe,GAAG,WAAW,CAkBrE;AAED,sFAAsF;AACtF,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAG1D"}
@@ -0,0 +1,32 @@
1
+ import { parsePersistSlice } from "../parse/persist.js";
2
+ import { WorkoutParseError } from "../parse/types.js";
3
+ function str(value) {
4
+ return typeof value === "string" && value.length > 0 ? value : null;
5
+ }
6
+ /**
7
+ * Pull credentials out of the persisted blob.
8
+ *
9
+ * The documented location is `userStore.exerciserProfile.token`, but the id is
10
+ * mirrored in several places depending on how the session was established, so the
11
+ * lookup is tolerant in the same spirit as the rest of the parse layer.
12
+ */
13
+ export function readCredentials(storage) {
14
+ const userStore = parsePersistSlice(storage.getItem("root"), "userStore");
15
+ const profile = (userStore["exerciserProfile"] ?? {});
16
+ const token = str(profile["token"]);
17
+ if (!token) {
18
+ throw new WorkoutParseError("No sign-in token found in this browser. Open the site and sign in, then try again.");
19
+ }
20
+ const exerciserId = str(profile["id"]) ?? str(userStore["userId"]) ?? str(userStore["id"]) ?? str(profile["exerciserId"]);
21
+ if (!exerciserId) {
22
+ throw new WorkoutParseError("Signed in, but no exerciser id is stored in this browser.");
23
+ }
24
+ return { exerciserId, token };
25
+ }
26
+ /** Strip anything token-shaped from a string before it can reach a message or log. */
27
+ export function redact(text, token) {
28
+ if (!token)
29
+ return text;
30
+ return text.split(token).join("[token]");
31
+ }
32
+ //# sourceMappingURL=credentials.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credentials.js","sourceRoot":"","sources":["../../src/api/credentials.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAgBtD,SAAS,GAAG,CAAC,KAAc;IACzB,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;AACtE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAAC,OAAwB;IACtD,MAAM,SAAS,GAAG,iBAAiB,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC,CAAC;IAC1E,MAAM,OAAO,GAAG,CAAC,SAAS,CAAC,kBAAkB,CAAC,IAAI,EAAE,CAA4B,CAAC;IAEjF,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC;IACpC,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,iBAAiB,CACzB,oFAAoF,CACrF,CAAC;IACJ,CAAC;IAED,MAAM,WAAW,GACf,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,GAAG,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,IAAI,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC;IACxG,IAAI,CAAC,WAAW,EAAE,CAAC;QACjB,MAAM,IAAI,iBAAiB,CAAC,2DAA2D,CAAC,CAAC;IAC3F,CAAC;IAED,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC;AAChC,CAAC;AAED,sFAAsF;AACtF,MAAM,UAAU,MAAM,CAAC,IAAY,EAAE,KAAa;IAChD,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;AAC3C,CAAC"}
@@ -0,0 +1,4 @@
1
+ export * from "./credentials.js";
2
+ export * from "./client.js";
3
+ export * from "./login.js";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC;AACjC,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC"}
@@ -0,0 +1,4 @@
1
+ export * from "./credentials.js";
2
+ export * from "./client.js";
3
+ export * from "./login.js";
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC;AACjC,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC"}
@@ -0,0 +1,43 @@
1
+ import { type FetchLike } from "./client.js";
2
+ import { type Credentials } from "./credentials.js";
3
+ /**
4
+ * Exchange an xid and passcode for a bearer token.
5
+ *
6
+ * The extension does not use this — inside the browser the site has already signed
7
+ * in and `readCredentials` reads the token it left behind, which is strictly better:
8
+ * no passcode is handled at all. This exists for the standalone client, which has no
9
+ * browser to borrow a session from. See MATRIX_API.md.
10
+ *
11
+ * The request shape is the site's own, read out of its bundle rather than guessed:
12
+ *
13
+ * loginWithXid(e, t, n = `xid`, r = 0) {
14
+ * let i = { username: e, password: t, type: n, club_id: r };
15
+ * return apisauce.post(`/exerciser/login`, i);
16
+ * }
17
+ *
18
+ * PRIVACY: the passcode is used for exactly this one request and is never returned,
19
+ * stored or logged. `redact()` guards the one path — a thrown transport error — that
20
+ * could otherwise carry the request body out with it.
21
+ */
22
+ export declare const LOGIN_PATH = "/exerciser/login";
23
+ /** The site sends `xid`; `apollo` is the other value its bundle uses, for SSO. */
24
+ export declare const LOGIN_TYPE_XID = "xid";
25
+ export interface XidLogin {
26
+ /** The member number printed on the gym tag. Sent as `username`. */
27
+ xid: string;
28
+ /** The numeric passcode. Sent as `password`. */
29
+ pin: string;
30
+ /** The site defaults this to 0 and so do we. */
31
+ clubId?: number;
32
+ }
33
+ export declare function loginUrl(): string;
34
+ /**
35
+ * Sign in and return just enough to make the history request.
36
+ *
37
+ * Deliberately returns `Credentials` and nothing else. The login response is a full
38
+ * profile — name, email, birthday, height, weight — and none of it is needed to
39
+ * fetch workouts, so none of it is carried out of this function where a caller could
40
+ * print it by accident.
41
+ */
42
+ export declare function loginWithXid(login: XidLogin, fetchImpl: FetchLike): Promise<Credentials>;
43
+ //# sourceMappingURL=login.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../../src/api/login.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwB,KAAK,SAAS,EAAE,MAAM,aAAa,CAAC;AACnE,OAAO,EAAU,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAE5D;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,UAAU,qBAAqB,CAAC;AAE7C,kFAAkF;AAClF,eAAO,MAAM,cAAc,QAAQ,CAAC;AAEpC,MAAM,WAAW,QAAQ;IACvB,oEAAoE;IACpE,GAAG,EAAE,MAAM,CAAC;IACZ,gDAAgD;IAChD,GAAG,EAAE,MAAM,CAAC;IACZ,gDAAgD;IAChD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,QAAQ,IAAI,MAAM,CAEjC;AAMD;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAAC,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC,CAkD9F"}
@@ -0,0 +1,97 @@
1
+ import { API_ORIGIN, ApiError } from "./client.js";
2
+ import { redact } from "./credentials.js";
3
+ /**
4
+ * Exchange an xid and passcode for a bearer token.
5
+ *
6
+ * The extension does not use this — inside the browser the site has already signed
7
+ * in and `readCredentials` reads the token it left behind, which is strictly better:
8
+ * no passcode is handled at all. This exists for the standalone client, which has no
9
+ * browser to borrow a session from. See MATRIX_API.md.
10
+ *
11
+ * The request shape is the site's own, read out of its bundle rather than guessed:
12
+ *
13
+ * loginWithXid(e, t, n = `xid`, r = 0) {
14
+ * let i = { username: e, password: t, type: n, club_id: r };
15
+ * return apisauce.post(`/exerciser/login`, i);
16
+ * }
17
+ *
18
+ * PRIVACY: the passcode is used for exactly this one request and is never returned,
19
+ * stored or logged. `redact()` guards the one path — a thrown transport error — that
20
+ * could otherwise carry the request body out with it.
21
+ */
22
+ export const LOGIN_PATH = "/exerciser/login";
23
+ /** The site sends `xid`; `apollo` is the other value its bundle uses, for SSO. */
24
+ export const LOGIN_TYPE_XID = "xid";
25
+ export function loginUrl() {
26
+ return `${API_ORIGIN}${LOGIN_PATH}`;
27
+ }
28
+ function str(value) {
29
+ return typeof value === "string" && value.length > 0 ? value : null;
30
+ }
31
+ /**
32
+ * Sign in and return just enough to make the history request.
33
+ *
34
+ * Deliberately returns `Credentials` and nothing else. The login response is a full
35
+ * profile — name, email, birthday, height, weight — and none of it is needed to
36
+ * fetch workouts, so none of it is carried out of this function where a caller could
37
+ * print it by accident.
38
+ */
39
+ export async function loginWithXid(login, fetchImpl) {
40
+ const body = JSON.stringify({
41
+ username: login.xid,
42
+ password: login.pin,
43
+ type: LOGIN_TYPE_XID,
44
+ club_id: login.clubId ?? 0,
45
+ });
46
+ let response;
47
+ try {
48
+ response = await fetchImpl(loginUrl(), {
49
+ method: "POST",
50
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
51
+ body,
52
+ });
53
+ }
54
+ catch (error) {
55
+ // The message can quote the request, and the request is the passcode.
56
+ const detail = error instanceof Error ? redact(error.message, login.pin) : "";
57
+ throw new ApiError(`Could not reach the sign-in API. ${detail}`.trim(), 0);
58
+ }
59
+ if (!response.ok) {
60
+ throw new ApiError(describeLoginStatus(response.status), response.status);
61
+ }
62
+ let payload;
63
+ try {
64
+ payload = await response.json();
65
+ }
66
+ catch {
67
+ throw new ApiError("The sign-in API returned something that is not JSON.", response.status);
68
+ }
69
+ if (typeof payload !== "object" || payload === null) {
70
+ throw new ApiError("The sign-in API returned no profile.", response.status);
71
+ }
72
+ // Observed live: `id` and `token` sit at the top level of the login response,
73
+ // which is the same pair `readCredentials` digs out of the persisted blob.
74
+ const record = payload;
75
+ const token = str(record["token"]);
76
+ const exerciserId = str(record["id"]);
77
+ if (!token) {
78
+ throw new ApiError("Signed in, but the API returned no token.", response.status);
79
+ }
80
+ if (!exerciserId) {
81
+ throw new ApiError("Signed in, but the API returned no exerciser id.", response.status);
82
+ }
83
+ return { exerciserId, token };
84
+ }
85
+ function describeLoginStatus(status) {
86
+ if (status === 400 || status === 401 || status === 403) {
87
+ return "The API rejected that xid and passcode.";
88
+ }
89
+ if (status === 404)
90
+ return "The sign-in endpoint is gone — the API has moved.";
91
+ if (status === 429)
92
+ return "Too many sign-in attempts. Wait before trying again.";
93
+ if (status >= 500)
94
+ return `The sign-in API is failing (HTTP ${status}). Try again later.`;
95
+ return `The sign-in API refused the request (HTTP ${status}).`;
96
+ }
97
+ //# sourceMappingURL=login.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"login.js","sourceRoot":"","sources":["../../src/api/login.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,108 @@
1
+ import { type ControlSignature } from "../parse/controlSignature.js";
2
+ import { type HeartRateStats } from "../parse/heartRate.js";
3
+ import type { Sample, Workout } from "../parse/types.js";
4
+ /**
5
+ * The JSON a user takes out of here and into something else.
6
+ *
7
+ * Pure and DOM-free, for the same reason `parse/` and `charts/` are: the shape of
8
+ * what leaves this extension is worth asserting against every fixture, and a test
9
+ * should not need a browser to do it.
10
+ *
11
+ * Three things it is deliberately not:
12
+ *
13
+ * - **Not a summary.** The whole argument for this project is that the platform's
14
+ * six tiles throw away power, resistance and cadence. An export that shipped
15
+ * only what the tiles show would repeat the mistake.
16
+ * - **Not lossy.** `source.record` carries every field of the upstream record
17
+ * unaltered, so a field this model has never heard of still arrives at the other
18
+ * end. The upstream shape is undocumented and can change without notice; a
19
+ * normalized-only export would quietly become the smaller of the two records over
20
+ * time. (Unaltered, not byte-for-byte: `JSON.stringify` renders a `28.0` as `28`
21
+ * and does not promise the server's key order. Same JSON, different bytes.)
22
+ * - **Not a re-interpretation.** Where the platform's reported figures disagree
23
+ * with the series — which they do, see AGENTS.md — both are carried, labelled,
24
+ * rather than one being picked on the reader's behalf.
25
+ */
26
+ /**
27
+ * The format identifier, and it does not change.
28
+ *
29
+ * It names `full-matrix-workouts`, the repository this code was extracted from in
30
+ * Sep 2026 and which is now `matrix-workouts-chrome`. That looks like an oversight and
31
+ * is not one: this string is already in files on people's disks, and every decoder
32
+ * that reads them compares against it. Renaming it to match the new repository would
33
+ * orphan all of them and buy nothing — an identifier's job is to be stable, not
34
+ * accurate about where the code lives today. `contract.test.ts` pins it.
35
+ */
36
+ export declare const EXPORT_FORMAT = "full-matrix-workouts/workout";
37
+ /** Bump only for a breaking change; new optional fields do not need one. */
38
+ export declare const EXPORT_FORMAT_VERSION = 1;
39
+ /** One sample, plus the one thing we know about it that the console does not. */
40
+ export interface ExportSample extends Sample {
41
+ /**
42
+ * False where the heart-rate reading was rejected as a chest-strap dropout.
43
+ * `heartRateBpm` still carries the value the console recorded — filtering is the
44
+ * consumer's decision to make, and hiding the raw reading would make this export
45
+ * less honest than the record it came from. See `filter` on the stats block for
46
+ * what was applied.
47
+ */
48
+ heartRateValid: boolean;
49
+ }
50
+ export interface WorkoutExport {
51
+ format: typeof EXPORT_FORMAT;
52
+ formatVersion: number;
53
+ /** When the file was written, ISO 8601 UTC. */
54
+ exportedAt: string;
55
+ workout: {
56
+ id: string;
57
+ /** ISO 8601 UTC, as the platform stores it. */
58
+ startedAt: string;
59
+ machineType: string;
60
+ machineId: string | null;
61
+ /** Raw console program id. Always present even when unmapped. */
62
+ programType: number | null;
63
+ /** Our name for that id, or `"unknown"` — never a guess. */
64
+ mode: string;
65
+ durationSeconds: number;
66
+ distanceMeters: number;
67
+ calories: number | null;
68
+ archived: boolean;
69
+ /** Sprint 8 rides only. */
70
+ sprint8: Workout["sprint8"];
71
+ /**
72
+ * The platform's own summary. NOT derived from `samples` and does not always
73
+ * agree with them — on a badly glitching strap it can be the better figure,
74
+ * because the console averaged in real time.
75
+ */
76
+ reported: Workout["reported"];
77
+ /** Computed here, from the series. Shown beside `reported`, never instead of it. */
78
+ derived: {
79
+ heartRate: HeartRateStats & {
80
+ filter: string;
81
+ };
82
+ control: ControlSignature;
83
+ };
84
+ /** Nominal seconds per sample. The final sample is usually shorter. */
85
+ sampleIntervalSeconds: number;
86
+ samples: ExportSample[];
87
+ };
88
+ source: {
89
+ /** Where the record came from, and therefore which key style it uses. */
90
+ shape: "camelCase" | "snake_case";
91
+ /**
92
+ * The upstream record, every field unaltered. Extracting this alone gives you a
93
+ * fixture in the shape `fixtures/raw-<id>.json` expects.
94
+ */
95
+ record: Record<string, unknown>;
96
+ };
97
+ }
98
+ /** Build the export document for one workout. `now` is injectable so tests can pin it. */
99
+ export declare function workoutExport(workout: Workout, now?: Date): WorkoutExport;
100
+ /**
101
+ * A filename that sorts by date and still names the ride uniquely. The date is the
102
+ * UTC one from `workoutTime` rather than a localized rendering, so two exports of
103
+ * the same ride from two machines agree.
104
+ */
105
+ export declare function exportFilename(workout: Workout): string;
106
+ /** The document as the text that gets written. Indented — this is meant to be read. */
107
+ export declare function workoutExportJson(workout: Workout, now?: Date): string;
108
+ //# sourceMappingURL=document.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"document.d.ts","sourceRoot":"","sources":["../../src/export/document.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoB,KAAK,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AACvF,OAAO,EAAyC,KAAK,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAEnG,OAAO,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAEzD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH;;;;;;;;;GASG;AACH,eAAO,MAAM,aAAa,iCAAiC,CAAC;AAE5D,4EAA4E;AAC5E,eAAO,MAAM,qBAAqB,IAAI,CAAC;AAEvC,iFAAiF;AACjF,MAAM,WAAW,YAAa,SAAQ,MAAM;IAC1C;;;;;;OAMG;IACH,cAAc,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,OAAO,aAAa,CAAC;IAC7B,aAAa,EAAE,MAAM,CAAC;IACtB,+CAA+C;IAC/C,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE;QACP,EAAE,EAAE,MAAM,CAAC;QACX,+CAA+C;QAC/C,SAAS,EAAE,MAAM,CAAC;QAClB,WAAW,EAAE,MAAM,CAAC;QACpB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;QACzB,iEAAiE;QACjE,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;QAC3B,4DAA4D;QAC5D,IAAI,EAAE,MAAM,CAAC;QACb,eAAe,EAAE,MAAM,CAAC;QACxB,cAAc,EAAE,MAAM,CAAC;QACvB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;QACxB,QAAQ,EAAE,OAAO,CAAC;QAClB,2BAA2B;QAC3B,OAAO,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;QAC5B;;;;WAIG;QACH,QAAQ,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC;QAC9B,oFAAoF;QACpF,OAAO,EAAE;YACP,SAAS,EAAE,cAAc,GAAG;gBAAE,MAAM,EAAE,MAAM,CAAA;aAAE,CAAC;YAC/C,OAAO,EAAE,gBAAgB,CAAC;SAC3B,CAAC;QACF,uEAAuE;QACvE,qBAAqB,EAAE,MAAM,CAAC;QAC9B,OAAO,EAAE,YAAY,EAAE,CAAC;KACzB,CAAC;IACF,MAAM,EAAE;QACN,yEAAyE;QACzE,KAAK,EAAE,WAAW,GAAG,YAAY,CAAC;QAClC;;;WAGG;QACH,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACjC,CAAC;CACH;AAeD,0FAA0F;AAC1F,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,GAAE,IAAiB,GAAG,aAAa,CAmCrF;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAEvD;AAED,uFAAuF;AACvF,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,IAAI,GAAG,MAAM,CAEtE"}
@@ -0,0 +1,97 @@
1
+ import { controlSignature } from "../parse/controlSignature.js";
2
+ import { flagHeartRateDropouts, heartRateStats } from "../parse/heartRate.js";
3
+ import { SAMPLE_INTERVAL_SECONDS } from "../parse/workout.js";
4
+ /**
5
+ * The JSON a user takes out of here and into something else.
6
+ *
7
+ * Pure and DOM-free, for the same reason `parse/` and `charts/` are: the shape of
8
+ * what leaves this extension is worth asserting against every fixture, and a test
9
+ * should not need a browser to do it.
10
+ *
11
+ * Three things it is deliberately not:
12
+ *
13
+ * - **Not a summary.** The whole argument for this project is that the platform's
14
+ * six tiles throw away power, resistance and cadence. An export that shipped
15
+ * only what the tiles show would repeat the mistake.
16
+ * - **Not lossy.** `source.record` carries every field of the upstream record
17
+ * unaltered, so a field this model has never heard of still arrives at the other
18
+ * end. The upstream shape is undocumented and can change without notice; a
19
+ * normalized-only export would quietly become the smaller of the two records over
20
+ * time. (Unaltered, not byte-for-byte: `JSON.stringify` renders a `28.0` as `28`
21
+ * and does not promise the server's key order. Same JSON, different bytes.)
22
+ * - **Not a re-interpretation.** Where the platform's reported figures disagree
23
+ * with the series — which they do, see AGENTS.md — both are carried, labelled,
24
+ * rather than one being picked on the reader's behalf.
25
+ */
26
+ /**
27
+ * The format identifier, and it does not change.
28
+ *
29
+ * It names `full-matrix-workouts`, the repository this code was extracted from in
30
+ * Sep 2026 and which is now `matrix-workouts-chrome`. That looks like an oversight and
31
+ * is not one: this string is already in files on people's disks, and every decoder
32
+ * that reads them compares against it. Renaming it to match the new repository would
33
+ * orphan all of them and buy nothing — an identifier's job is to be stable, not
34
+ * accurate about where the code lives today. `contract.test.ts` pins it.
35
+ */
36
+ export const EXPORT_FORMAT = "full-matrix-workouts/workout";
37
+ /** Bump only for a breaking change; new optional fields do not need one. */
38
+ export const EXPORT_FORMAT_VERSION = 1;
39
+ /** How the dropout filter was configured, in one line the file can carry. */
40
+ const HEART_RATE_FILTER = "samples at or below 60 bpm, or moving faster than a heart rate plausibly can " +
41
+ "since the last accepted reading, are flagged as chest-strap dropouts";
42
+ /**
43
+ * The API returns snake_case and the persisted blob camelCase. `workoutId` is
44
+ * mandatory in both, so its spelling identifies the source without guessing.
45
+ */
46
+ function sourceShape(record) {
47
+ return "workout_id" in record ? "snake_case" : "camelCase";
48
+ }
49
+ /** Build the export document for one workout. `now` is injectable so tests can pin it. */
50
+ export function workoutExport(workout, now = new Date()) {
51
+ const valid = flagHeartRateDropouts(workout.samples);
52
+ return {
53
+ format: EXPORT_FORMAT,
54
+ formatVersion: EXPORT_FORMAT_VERSION,
55
+ exportedAt: now.toISOString(),
56
+ workout: {
57
+ id: workout.id,
58
+ startedAt: workout.startedAt.toISOString(),
59
+ machineType: workout.machineType,
60
+ machineId: workout.machineId,
61
+ programType: workout.programType,
62
+ mode: workout.mode,
63
+ durationSeconds: workout.durationSeconds,
64
+ distanceMeters: workout.distanceMeters,
65
+ calories: workout.calories,
66
+ archived: workout.archived,
67
+ sprint8: workout.sprint8,
68
+ reported: workout.reported,
69
+ derived: {
70
+ heartRate: { ...heartRateStats(workout.samples), filter: HEART_RATE_FILTER },
71
+ control: controlSignature(workout.samples),
72
+ },
73
+ sampleIntervalSeconds: SAMPLE_INTERVAL_SECONDS,
74
+ samples: workout.samples.map((sample, index) => ({
75
+ ...sample,
76
+ heartRateValid: valid[index] ?? false,
77
+ })),
78
+ },
79
+ source: {
80
+ shape: sourceShape(workout.raw),
81
+ record: workout.raw,
82
+ },
83
+ };
84
+ }
85
+ /**
86
+ * A filename that sorts by date and still names the ride uniquely. The date is the
87
+ * UTC one from `workoutTime` rather than a localized rendering, so two exports of
88
+ * the same ride from two machines agree.
89
+ */
90
+ export function exportFilename(workout) {
91
+ return `matrix-workout-${workout.startedAt.toISOString().slice(0, 10)}-${workout.id}.json`;
92
+ }
93
+ /** The document as the text that gets written. Indented — this is meant to be read. */
94
+ export function workoutExportJson(workout, now) {
95
+ return `${JSON.stringify(workoutExport(workout, now), null, 2)}\n`;
96
+ }
97
+ //# sourceMappingURL=document.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"document.js","sourceRoot":"","sources":["../../src/export/document.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAyB,MAAM,8BAA8B,CAAC;AACvF,OAAO,EAAE,qBAAqB,EAAE,cAAc,EAAuB,MAAM,uBAAuB,CAAC;AACnG,OAAO,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AAG9D;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,8BAA8B,CAAC;AAE5D,4EAA4E;AAC5E,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC;AA6DvC,6EAA6E;AAC7E,MAAM,iBAAiB,GACrB,+EAA+E;IAC/E,sEAAsE,CAAC;AAEzE;;;GAGG;AACH,SAAS,WAAW,CAAC,MAA+B;IAClD,OAAO,YAAY,IAAI,MAAM,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,WAAW,CAAC;AAC7D,CAAC;AAED,0FAA0F;AAC1F,MAAM,UAAU,aAAa,CAAC,OAAgB,EAAE,MAAY,IAAI,IAAI,EAAE;IACpE,MAAM,KAAK,GAAG,qBAAqB,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IAErD,OAAO;QACL,MAAM,EAAE,aAAa;QACrB,aAAa,EAAE,qBAAqB;QACpC,UAAU,EAAE,GAAG,CAAC,WAAW,EAAE;QAC7B,OAAO,EAAE;YACP,EAAE,EAAE,OAAO,CAAC,EAAE;YACd,SAAS,EAAE,OAAO,CAAC,SAAS,CAAC,WAAW,EAAE;YAC1C,WAAW,EAAE,OAAO,CAAC,WAAW;YAChC,SAAS,EAAE,OAAO,CAAC,SAAS;YAC5B,WAAW,EAAE,OAAO,CAAC,WAAW;YAChC,IAAI,EAAE,OAAO,CAAC,IAAI;YAClB,eAAe,EAAE,OAAO,CAAC,eAAe;YACxC,cAAc,EAAE,OAAO,CAAC,cAAc;YACtC,QAAQ,EAAE,OAAO,CAAC,QAAQ;YAC1B,QAAQ,EAAE,OAAO,CAAC,QAAQ;YAC1B,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,QAAQ,EAAE,OAAO,CAAC,QAAQ;YAC1B,OAAO,EAAE;gBACP,SAAS,EAAE,EAAE,GAAG,cAAc,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,iBAAiB,EAAE;gBAC5E,OAAO,EAAE,gBAAgB,CAAC,OAAO,CAAC,OAAO,CAAC;aAC3C;YACD,qBAAqB,EAAE,uBAAuB;YAC9C,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC;gBAC/C,GAAG,MAAM;gBACT,cAAc,EAAE,KAAK,CAAC,KAAK,CAAC,IAAI,KAAK;aACtC,CAAC,CAAC;SACJ;QACD,MAAM,EAAE;YACN,KAAK,EAAE,WAAW,CAAC,OAAO,CAAC,GAAG,CAAC;YAC/B,MAAM,EAAE,OAAO,CAAC,GAAG;SACpB;KACF,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,OAAgB;IAC7C,OAAO,kBAAkB,OAAO,CAAC,SAAS,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,OAAO,CAAC,EAAE,OAAO,CAAC;AAC7F,CAAC;AAED,uFAAuF;AACvF,MAAM,UAAU,iBAAiB,CAAC,OAAgB,EAAE,GAAU;IAC5D,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,aAAa,CAAC,OAAO,EAAE,GAAG,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC;AACrE,CAAC"}
@@ -0,0 +1,2 @@
1
+ export * from "./document.js";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/export/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC"}
@@ -0,0 +1,2 @@
1
+ export * from "./document.js";
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/export/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The platform-free core: read a Matrix / jfit workout record, normalize it, talk to
3
+ * the API, write the export document.
4
+ *
5
+ * Nothing reachable from here touches a DOM, a `chrome.*`, a `node:` module or
6
+ * `fetch` — `layers.test.ts` asserts it on every run, and `tsconfig` omits the DOM
7
+ * lib so the compiler agrees. That is what lets one parser serve a Chrome extension,
8
+ * a CLI and an MCP server without a fork.
9
+ *
10
+ * Node-side helpers — real `fetch`, files, `.env` — are a separate entry point:
11
+ * `matrix-workouts-core/node`.
12
+ */
13
+ export * from "./parse/index.js";
14
+ export * from "./api/index.js";
15
+ export * from "./export/index.js";
16
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,cAAc,kBAAkB,CAAC;AACjC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The platform-free core: read a Matrix / jfit workout record, normalize it, talk to
3
+ * the API, write the export document.
4
+ *
5
+ * Nothing reachable from here touches a DOM, a `chrome.*`, a `node:` module or
6
+ * `fetch` — `layers.test.ts` asserts it on every run, and `tsconfig` omits the DOM
7
+ * lib so the compiler agrees. That is what lets one parser serve a Chrome extension,
8
+ * a CLI and an MCP server without a fork.
9
+ *
10
+ * Node-side helpers — real `fetch`, files, `.env` — are a separate entry point:
11
+ * `matrix-workouts-core/node`.
12
+ */
13
+ export * from "./parse/index.js";
14
+ export * from "./api/index.js";
15
+ export * from "./export/index.js";
16
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,cAAc,kBAAkB,CAAC;AACjC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC"}
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=history.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"history.d.ts","sourceRoot":"","sources":["../../../src/node/bin/history.ts"],"names":[],"mappings":""}
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Standalone workout client — sign in with an xid and passcode, download the full
4
+ * history, write it to disk.
5
+ *
6
+ * matrix-workouts-history # -> history/raw-history-<timestamp>.json
7
+ * matrix-workouts-history --split # also one export document per ride
8
+ * matrix-workouts-history --out data # somewhere other than history/
9
+ *
10
+ * This is the one part of the project that handles a passcode; the extension borrows
11
+ * the session the site already established. See MATRIX_API.md for the endpoints.
12
+ *
13
+ * PRIVACY: credentials come from `.env` (gitignored) or the environment, are used for
14
+ * the single sign-in request, and are never written to the output or the console. The
15
+ * token is likewise never printed.
16
+ *
17
+ * Everything here is argv, stdout and exit codes. The work itself is in
18
+ * `../history.ts`, so the MCP server gets the same download without the CLI.
19
+ */
20
+ import { resolve } from "node:path";
21
+ import { loginWithXid } from "../../api/index.js";
22
+ import { downloadHistory, httpFetch, loadEnvFile } from "../history.js";
23
+ const args = process.argv.slice(2);
24
+ const flag = (name) => args.includes(`--${name}`);
25
+ const option = (name, fallback) => {
26
+ const at = args.indexOf(`--${name}`);
27
+ return at >= 0 ? (args[at + 1] ?? fallback) : fallback;
28
+ };
29
+ function required(name) {
30
+ const value = process.env[name];
31
+ if (!value) {
32
+ console.error(`Missing ${name}. Copy .env.example to .env and fill in your xid and passcode,\n` +
33
+ `or pass it in the environment: ${name}=... matrix-workouts-history`);
34
+ process.exit(1);
35
+ }
36
+ return value;
37
+ }
38
+ const km = (meters) => (meters / 1000).toFixed(2).padStart(6);
39
+ const hms = (seconds) => `${String(Math.floor(seconds / 60)).padStart(3)}:${String(Math.round(seconds % 60)).padStart(2, "0")}`;
40
+ function printTable(workouts) {
41
+ console.log("");
42
+ console.log(" date mode dur km avg HR samples");
43
+ console.log(" " + "─".repeat(66));
44
+ for (const workout of workouts) {
45
+ const hr = workout.reported.averageHeartRateBpm;
46
+ console.log(` ${workout.startedAt.toISOString().slice(0, 10)} ${workout.mode.padEnd(22)}` +
47
+ `${hms(workout.durationSeconds)} ${km(workout.distanceMeters)} ` +
48
+ `${String(hr ?? "—").padStart(5)} ${String(workout.samples.length).padStart(7)}`);
49
+ }
50
+ }
51
+ loadEnvFile(resolve(process.cwd(), ".env"));
52
+ const xid = required("MATRIX_XID");
53
+ const pin = required("MATRIX_PIN");
54
+ const outDir = resolve(process.cwd(), option("out", "history"));
55
+ try {
56
+ process.stderr.write(`Signing in as ${xid}…\n`);
57
+ const credentials = await loginWithXid({ xid, pin }, httpFetch);
58
+ process.stderr.write("Downloading full history…\n");
59
+ const result = await downloadHistory(credentials, { outDir, split: flag("split") });
60
+ printTable(result.workouts);
61
+ console.log("");
62
+ console.log(` ${result.workouts.length} workouts`);
63
+ if (result.skipped > 0)
64
+ console.log(` ${result.skipped} record(s) this parser could not read`);
65
+ if (result.truncated) {
66
+ console.log(" WARNING: the API's paging says there is more history than arrived.");
67
+ }
68
+ console.log(` raw response -> ${result.rawPath}`);
69
+ if (result.exportPaths.length > 0) {
70
+ console.log(` ${result.exportPaths.length} export document(s) -> ${outDir}/`);
71
+ }
72
+ console.log("");
73
+ }
74
+ catch (error) {
75
+ // ApiError messages are written to be safe to print; anything else gets its
76
+ // message only, never the object, which could carry the request.
77
+ console.error(`\n ${error instanceof Error ? error.message : "Unknown failure."}\n`);
78
+ process.exit(1);
79
+ }
80
+ //# sourceMappingURL=history.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"history.js","sourceRoot":"","sources":["../../../src/node/bin/history.ts"],"names":[],"mappings":"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"}