@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 +21 -0
- package/README.md +130 -0
- package/dist/client.d.ts +12 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +66 -0
- package/dist/client.js.map +1 -0
- package/dist/parsing.d.ts +16 -0
- package/dist/parsing.d.ts.map +1 -0
- package/dist/parsing.js +61 -0
- package/dist/parsing.js.map +1 -0
- package/dist/sports.d.ts +4 -0
- package/dist/sports.d.ts.map +1 -0
- package/dist/sports.js +19 -0
- package/dist/sports.js.map +1 -0
- package/dist/types.d.ts +28 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +60 -0
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)
|
package/dist/client.d.ts
ADDED
|
@@ -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"}
|
package/dist/parsing.js
ADDED
|
@@ -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":"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"}
|
package/dist/sports.d.ts
ADDED
|
@@ -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"}
|
package/dist/types.d.ts
ADDED
|
@@ -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 @@
|
|
|
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
|
+
}
|