@dofek/velohero 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-2026 Asher Cohen
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/README.md ADDED
@@ -0,0 +1,130 @@
1
+ # @dofek/velohero
2
+
3
+ Unofficial TypeScript client for VeloHero's private workout-export API.
4
+
5
+ This package is not affiliated with, endorsed by, or supported by VeloHero.
6
+ The endpoints and response shapes were observed from VeloHero's web
7
+ application, are not a supported public API contract, and may change without
8
+ notice. Use only with an account and data you are authorized to access.
9
+
10
+ ## Requirements
11
+
12
+ - Node.js 22.14 or newer
13
+
14
+ ## Install
15
+
16
+ ```sh
17
+ npm install @dofek/velohero
18
+ ```
19
+
20
+ ## Quick start
21
+
22
+ ```ts
23
+ import { VeloHeroClient } from "@dofek/velohero";
24
+
25
+ const username = process.env.VELOHERO_USERNAME;
26
+ const password = process.env.VELOHERO_PASSWORD;
27
+ if (!username || !password) {
28
+ throw new Error("Set VELOHERO_USERNAME and VELOHERO_PASSWORD");
29
+ }
30
+
31
+ const { sessionCookie, userId } = await VeloHeroClient.signIn(
32
+ username,
33
+ password,
34
+ );
35
+ const client = new VeloHeroClient(sessionCookie);
36
+
37
+ const workouts = await client.getWorkouts("2026-01-01", "2026-01-31");
38
+ const firstWorkout = workouts[0]
39
+ ? await client.getWorkout(workouts[0].id)
40
+ : null;
41
+
42
+ console.log({ userId, workoutCount: workouts.length, firstWorkout });
43
+ ```
44
+
45
+ Dates passed to `getWorkouts` use `YYYY-MM-DD`.
46
+
47
+ ## Authentication lifecycle
48
+
49
+ `VeloHeroClient.signIn(username, password)` posts form data to the observed
50
+ `/sso` endpoint and returns:
51
+
52
+ ```ts
53
+ {
54
+ sessionCookie: string; // "VeloHero_session=<session token>"
55
+ userId: string;
56
+ }
57
+ ```
58
+
59
+ Pass `sessionCookie` to the constructor. Treat it like a password: do not log
60
+ it or expose it to a browser. The private response does not provide expiry
61
+ metadata, and this package has no session-refresh method. When VeloHero rejects
62
+ an expired session, sign in again and construct a new client.
63
+
64
+ Both the constructor and `signIn` accept an optional `fetch` implementation as
65
+ their final argument for compatible runtimes and network-level tests.
66
+
67
+ ## Public API
68
+
69
+ The package root exports `VeloHeroClient`:
70
+
71
+ - `VeloHeroClient.signIn(username, password, fetch?)`
72
+ - `new VeloHeroClient(sessionCookie, fetch?)`
73
+ - `client.getWorkouts(dateFrom, dateTo)`
74
+ - `client.getWorkout(id)`
75
+
76
+ Additional modules are available through documented deep imports:
77
+
78
+ ```ts
79
+ import {
80
+ parseDurationToSeconds,
81
+ parseVeloHeroWorkout,
82
+ type ParsedVeloHeroWorkout,
83
+ } from "@dofek/velohero/parsing";
84
+ import {
85
+ mapVeloHeroSport,
86
+ VELOHERO_SPORT_MAP,
87
+ } from "@dofek/velohero/sports";
88
+ import type {
89
+ VeloHeroSsoResponse,
90
+ VeloHeroWorkout,
91
+ VeloHeroWorkoutsResponse,
92
+ } from "@dofek/velohero/types";
93
+ ```
94
+
95
+ `parseVeloHeroWorkout` converts VeloHero's string-valued export record to a
96
+ provider-neutral activity summary while retaining observed metrics in `raw`.
97
+
98
+ ## Rate limits and errors
99
+
100
+ Observed client behavior:
101
+
102
+ - HTTP `429` throws `ProviderRateLimitError` from
103
+ `@dofek/provider-http/rate-limit`. Its `retryAfterSeconds` property is parsed
104
+ from `Retry-After` when present.
105
+ - HTTP `502`, `503`, and `504` throw `ProviderServiceUnavailableError` from the
106
+ same module.
107
+ - Other unsuccessful sign-in and API responses throw `Error` containing the
108
+ HTTP status and response body.
109
+ - The client does not automatically sleep or retry. Callers decide whether and
110
+ when an operation is safe to repeat.
111
+ - Successful private responses are represented by TypeScript interfaces, not
112
+ runtime-validated schemas. Be prepared for upstream shape changes.
113
+
114
+ ## Observed private protocol
115
+
116
+ - Base URL: `https://app.velohero.com`
117
+ - Sign-in: `POST /sso` using form fields `user`, `pass`, and `view=json`
118
+ - Authentication: `VeloHero_session` cookie
119
+ - Workout list: `GET /export/workouts/json`
120
+ - Workout detail: `GET /export/workouts/json/{id}`
121
+
122
+ These details document observed behavior; they are not promises made by
123
+ VeloHero.
124
+
125
+ ## Project
126
+
127
+ - [Source](https://github.com/Asherlc/dofek/tree/main/packages/velohero-client)
128
+ - [Issues](https://github.com/Asherlc/dofek/issues)
129
+ - [Pull requests](https://github.com/Asherlc/dofek/pulls)
130
+ - [MIT License](./LICENSE)
@@ -0,0 +1,12 @@
1
+ import type { VeloHeroWorkout } from "./types.ts";
2
+ export declare class VeloHeroClient {
3
+ #private;
4
+ constructor(sessionCookie: string, fetchFn?: typeof globalThis.fetch);
5
+ getWorkouts(dateFrom: string, dateTo: string): Promise<VeloHeroWorkout[]>;
6
+ getWorkout(id: string): Promise<VeloHeroWorkout>;
7
+ static signIn(username: string, password: string, fetchFn?: typeof globalThis.fetch): Promise<{
8
+ sessionCookie: string;
9
+ userId: string;
10
+ }>;
11
+ }
12
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAuB,eAAe,EAA4B,MAAM,YAAY,CAAC;AAIjG,qBAAa,cAAc;;gBAIb,aAAa,EAAE,MAAM,EAAE,OAAO,GAAE,OAAO,UAAU,CAAC,KAAwB;IAsBhF,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;IASzE,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;WAIzC,MAAM,CACjB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,OAAO,UAAU,CAAC,KAAwB,GAClD,OAAO,CAAC;QAAE,aAAa,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CAmCtD"}
package/dist/client.js ADDED
@@ -0,0 +1,66 @@
1
+ import { createRateLimitAwareFetch } from "@dofek/provider-http/rate-limit";
2
+ const VELOHERO_BASE_URL = "https://app.velohero.com";
3
+ export class VeloHeroClient {
4
+ #sessionCookie;
5
+ #fetchFn;
6
+ constructor(sessionCookie, fetchFn = globalThis.fetch) {
7
+ this.#sessionCookie = sessionCookie;
8
+ this.#fetchFn = createRateLimitAwareFetch(fetchFn, { providerId: "velohero" });
9
+ }
10
+ async #get(path, params) {
11
+ const url = params ? `${VELOHERO_BASE_URL}${path}?${params}` : `${VELOHERO_BASE_URL}${path}`;
12
+ const response = await this.#fetchFn(url, {
13
+ headers: {
14
+ Cookie: this.#sessionCookie,
15
+ Accept: "application/json",
16
+ },
17
+ });
18
+ if (!response.ok) {
19
+ const text = await response.text();
20
+ throw new Error(`VeloHero API error (${response.status}): ${text}`);
21
+ }
22
+ return response.json();
23
+ }
24
+ async getWorkouts(dateFrom, dateTo) {
25
+ const params = new URLSearchParams({
26
+ date_from: dateFrom,
27
+ date_to: dateTo,
28
+ });
29
+ const data = await this.#get("/export/workouts/json", params);
30
+ return data.workouts ?? [];
31
+ }
32
+ async getWorkout(id) {
33
+ return this.#get(`/export/workouts/json/${id}`);
34
+ }
35
+ static async signIn(username, password, fetchFn = globalThis.fetch) {
36
+ const body = new URLSearchParams({
37
+ user: username,
38
+ pass: password,
39
+ view: "json",
40
+ });
41
+ const rateLimitFetchFn = createRateLimitAwareFetch(fetchFn, { providerId: "velohero" });
42
+ const response = await rateLimitFetchFn(`${VELOHERO_BASE_URL}/sso`, {
43
+ method: "POST",
44
+ headers: {
45
+ "Content-Type": "application/x-www-form-urlencoded",
46
+ },
47
+ body: body.toString(),
48
+ redirect: "manual",
49
+ });
50
+ if (!response.ok) {
51
+ const text = await response.text();
52
+ throw new Error(`VeloHero sign-in failed (${response.status}): ${text}`);
53
+ }
54
+ const data = await response.json();
55
+ if (!data.session) {
56
+ throw new Error("VeloHero sign-in did not return a session token");
57
+ }
58
+ // The session token is used as a cookie value
59
+ const sessionCookie = `VeloHero_session=${data.session}`;
60
+ return {
61
+ sessionCookie,
62
+ userId: data["user-id"],
63
+ };
64
+ }
65
+ }
66
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import type { CanonicalActivityType } from "@dofek/training/training";
2
+ import type { VeloHeroWorkout } from "./types.ts";
3
+ export interface ParsedVeloHeroWorkout {
4
+ externalId: string;
5
+ activityType: CanonicalActivityType;
6
+ name: string;
7
+ startedAt: Date;
8
+ endedAt: Date;
9
+ raw: Record<string, unknown>;
10
+ }
11
+ /**
12
+ * Parse a duration string in HH:MM:SS format to total seconds.
13
+ */
14
+ export declare function parseDurationToSeconds(durTime: string): number;
15
+ export declare function parseVeloHeroWorkout(workout: VeloHeroWorkout): ParsedVeloHeroWorkout;
16
+ //# sourceMappingURL=parsing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parsing.d.ts","sourceRoot":"","sources":["../src/parsing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AAEtE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,MAAM,WAAW,qBAAqB;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,qBAAqB,CAAC;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,IAAI,CAAC;IAChB,OAAO,EAAE,IAAI,CAAC;IACd,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC9B;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAO9D;AAWD,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,eAAe,GAAG,qBAAqB,CAwCpF"}
@@ -0,0 +1,61 @@
1
+ import { mapVeloHeroSport } from "./sports.js";
2
+ /**
3
+ * Parse a duration string in HH:MM:SS format to total seconds.
4
+ */
5
+ export function parseDurationToSeconds(durTime) {
6
+ const parts = durTime.split(":");
7
+ if (parts.length !== 3)
8
+ return 0;
9
+ const hours = Number.parseInt(parts[0] ?? "0", 10);
10
+ const minutes = Number.parseInt(parts[1] ?? "0", 10);
11
+ const seconds = Number.parseInt(parts[2] ?? "0", 10);
12
+ return hours * 3600 + minutes * 60 + seconds;
13
+ }
14
+ /**
15
+ * Parse a numeric string, returning undefined if empty/invalid.
16
+ */
17
+ function parseOptionalNumber(value) {
18
+ if (!value || value.trim() === "")
19
+ return undefined;
20
+ const num = Number.parseFloat(value);
21
+ return Number.isNaN(num) ? undefined : num;
22
+ }
23
+ export function parseVeloHeroWorkout(workout) {
24
+ const durationSeconds = parseDurationToSeconds(workout.dur_time);
25
+ // Build startedAt from date_ymd + start_time
26
+ const dateStr = workout.date_ymd;
27
+ const timeStr = workout.start_time || "00:00:00";
28
+ const startedAt = new Date(`${dateStr}T${timeStr}`);
29
+ const endedAt = new Date(startedAt.getTime() + durationSeconds * 1000);
30
+ const distanceKm = parseOptionalNumber(workout.dist_km);
31
+ const distanceMeters = distanceKm !== undefined ? Math.round(distanceKm * 1000) : undefined;
32
+ const avgHeartRate = parseOptionalNumber(workout.avg_hr);
33
+ const maxHeartRate = parseOptionalNumber(workout.max_hr);
34
+ const avgPower = parseOptionalNumber(workout.avg_power);
35
+ const maxPower = parseOptionalNumber(workout.max_power);
36
+ const avgCadence = parseOptionalNumber(workout.avg_cadence);
37
+ const maxCadence = parseOptionalNumber(workout.max_cadence);
38
+ const ascent = parseOptionalNumber(workout.ascent);
39
+ const descent = parseOptionalNumber(workout.descent);
40
+ return {
41
+ externalId: String(workout.id),
42
+ activityType: mapVeloHeroSport(workout.sport_id),
43
+ name: workout.title || `${mapVeloHeroSport(workout.sport_id)} workout`,
44
+ startedAt,
45
+ endedAt,
46
+ raw: {
47
+ sportId: workout.sport_id,
48
+ durationSeconds,
49
+ distanceMeters,
50
+ avgHeartRate,
51
+ maxHeartRate,
52
+ avgPower,
53
+ maxPower,
54
+ avgCadence,
55
+ maxCadence,
56
+ ascent,
57
+ descent,
58
+ },
59
+ };
60
+ }
61
+ //# sourceMappingURL=parsing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parsing.js","sourceRoot":"","sources":["../src/parsing.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAY/C;;GAEG;AACH,MAAM,UAAU,sBAAsB,CAAC,OAAe;IACpD,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACjC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC;IACjC,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,GAAG,EAAE,EAAE,CAAC,CAAC;IACnD,MAAM,OAAO,GAAG,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,GAAG,EAAE,EAAE,CAAC,CAAC;IACrD,MAAM,OAAO,GAAG,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,GAAG,EAAE,EAAE,CAAC,CAAC;IACrD,OAAO,KAAK,GAAG,IAAI,GAAG,OAAO,GAAG,EAAE,GAAG,OAAO,CAAC;AAC/C,CAAC;AAED;;GAEG;AACH,SAAS,mBAAmB,CAAC,KAAyB;IACpD,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE;QAAE,OAAO,SAAS,CAAC;IACpD,MAAM,GAAG,GAAG,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;IACrC,OAAO,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC;AAC7C,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,OAAwB;IAC3D,MAAM,eAAe,GAAG,sBAAsB,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAEjE,6CAA6C;IAC7C,MAAM,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IACjC,MAAM,OAAO,GAAG,OAAO,CAAC,UAAU,IAAI,UAAU,CAAC;IACjD,MAAM,SAAS,GAAG,IAAI,IAAI,CAAC,GAAG,OAAO,IAAI,OAAO,EAAE,CAAC,CAAC;IACpD,MAAM,OAAO,GAAG,IAAI,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,GAAG,eAAe,GAAG,IAAI,CAAC,CAAC;IAEvE,MAAM,UAAU,GAAG,mBAAmB,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IACxD,MAAM,cAAc,GAAG,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAC5F,MAAM,YAAY,GAAG,mBAAmB,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IACzD,MAAM,YAAY,GAAG,mBAAmB,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IACzD,MAAM,QAAQ,GAAG,mBAAmB,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IACxD,MAAM,QAAQ,GAAG,mBAAmB,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IACxD,MAAM,UAAU,GAAG,mBAAmB,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;IAC5D,MAAM,UAAU,GAAG,mBAAmB,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;IAC5D,MAAM,MAAM,GAAG,mBAAmB,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IACnD,MAAM,OAAO,GAAG,mBAAmB,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IAErD,OAAO;QACL,UAAU,EAAE,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;QAC9B,YAAY,EAAE,gBAAgB,CAAC,OAAO,CAAC,QAAQ,CAAC;QAChD,IAAI,EAAE,OAAO,CAAC,KAAK,IAAI,GAAG,gBAAgB,CAAC,OAAO,CAAC,QAAQ,CAAC,UAAU;QACtE,SAAS;QACT,OAAO;QACP,GAAG,EAAE;YACH,OAAO,EAAE,OAAO,CAAC,QAAQ;YACzB,eAAe;YACf,cAAc;YACd,YAAY;YACZ,YAAY;YACZ,QAAQ;YACR,QAAQ;YACR,UAAU;YACV,UAAU;YACV,MAAM;YACN,OAAO;SACR;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { CanonicalActivityType } from "@dofek/training/training";
2
+ export declare const VELOHERO_SPORT_MAP: Record<string, CanonicalActivityType>;
3
+ export declare function mapVeloHeroSport(sportId: string): CanonicalActivityType;
4
+ //# sourceMappingURL=sports.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sports.d.ts","sourceRoot":"","sources":["../src/sports.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AAEtE,eAAO,MAAM,kBAAkB,EAAE,MAAM,CAAC,MAAM,EAAE,qBAAqB,CAcpE,CAAC;AAEF,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,qBAAqB,CAEvE"}
package/dist/sports.js ADDED
@@ -0,0 +1,19 @@
1
+ export const VELOHERO_SPORT_MAP = {
2
+ "0": "other",
3
+ "1": "cycling",
4
+ "2": "running",
5
+ "3": "swimming",
6
+ "4": "gym",
7
+ "5": "strength",
8
+ "6": "mountain_biking",
9
+ "7": "hiking",
10
+ "8": "cross_country_skiing",
11
+ "9": "cycling", // velomobil / HPV
12
+ "10": "other",
13
+ "11": "rowing",
14
+ "12": "e_bike_cycling", // pedelec / e-bike
15
+ };
16
+ export function mapVeloHeroSport(sportId) {
17
+ return VELOHERO_SPORT_MAP[sportId] ?? "other";
18
+ }
19
+ //# sourceMappingURL=sports.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sports.js","sourceRoot":"","sources":["../src/sports.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,kBAAkB,GAA0C;IACvE,GAAG,EAAE,OAAO;IACZ,GAAG,EAAE,SAAS;IACd,GAAG,EAAE,SAAS;IACd,GAAG,EAAE,UAAU;IACf,GAAG,EAAE,KAAK;IACV,GAAG,EAAE,UAAU;IACf,GAAG,EAAE,iBAAiB;IACtB,GAAG,EAAE,QAAQ;IACb,GAAG,EAAE,sBAAsB;IAC3B,GAAG,EAAE,SAAS,EAAE,kBAAkB;IAClC,IAAI,EAAE,OAAO;IACb,IAAI,EAAE,QAAQ;IACd,IAAI,EAAE,gBAAgB,EAAE,mBAAmB;CAC5C,CAAC;AAEF,MAAM,UAAU,gBAAgB,CAAC,OAAe;IAC9C,OAAO,kBAAkB,CAAC,OAAO,CAAC,IAAI,OAAO,CAAC;AAChD,CAAC"}
@@ -0,0 +1,28 @@
1
+ export interface VeloHeroWorkout {
2
+ id: string;
3
+ date_ymd: string;
4
+ start_time: string;
5
+ dur_time: string;
6
+ sport_id: string;
7
+ dist_km: string;
8
+ title?: string;
9
+ ascent?: string;
10
+ descent?: string;
11
+ avg_hr?: string;
12
+ max_hr?: string;
13
+ avg_power?: string;
14
+ max_power?: string;
15
+ avg_cadence?: string;
16
+ max_cadence?: string;
17
+ calories?: string;
18
+ file?: string;
19
+ hide?: string;
20
+ }
21
+ export interface VeloHeroWorkoutsResponse {
22
+ workouts: VeloHeroWorkout[];
23
+ }
24
+ export interface VeloHeroSsoResponse {
25
+ session: string;
26
+ "user-id": string;
27
+ }
28
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,EAAE,eAAe,EAAE,CAAC;CAC7B;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@dofek/velohero",
3
+ "version": "0.1.0",
4
+ "description": "Unofficial VeloHero API client using reverse-engineered session authentication",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/Asherlc/dofek.git",
10
+ "directory": "packages/velohero-client"
11
+ },
12
+ "homepage": "https://github.com/Asherlc/dofek/tree/main/packages/velohero-client#readme",
13
+ "bugs": "https://github.com/Asherlc/dofek/issues",
14
+ "keywords": [
15
+ "velohero",
16
+ "cycling",
17
+ "api-client",
18
+ "unofficial",
19
+ "typescript"
20
+ ],
21
+ "engines": {
22
+ "node": ">=22.14.0"
23
+ },
24
+ "sideEffects": false,
25
+ "files": [
26
+ "dist",
27
+ "README.md",
28
+ "LICENSE"
29
+ ],
30
+ "exports": {
31
+ ".": {
32
+ "types": "./dist/client.d.ts",
33
+ "import": "./dist/client.js"
34
+ },
35
+ "./*": {
36
+ "types": "./dist/*.d.ts",
37
+ "import": "./dist/*.js"
38
+ }
39
+ },
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "scripts": {
44
+ "test": "vitest run",
45
+ "test:watch": "vitest --watch",
46
+ "build": "tsc",
47
+ "prepack": "pnpm run build",
48
+ "lint": "biome check .",
49
+ "typecheck": "tsc --noEmit"
50
+ },
51
+ "dependencies": {
52
+ "@dofek/provider-http": "0.1.0",
53
+ "@dofek/training": "0.1.0"
54
+ },
55
+ "devDependencies": {
56
+ "typescript": "6.0.3",
57
+ "vitest": "3.2.6"
58
+ },
59
+ "gitHead": "ac95258c753c7e75b57da7ae47426d22b78e8c01"
60
+ }