@dofek/peloton 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 +170 -0
- package/dist/auth.d.ts +15 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +252 -0
- package/dist/auth.js.map +1 -0
- package/dist/client.d.ts +10 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +90 -0
- package/dist/client.js.map +1 -0
- package/dist/errors.d.ts +23 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +37 -0
- package/dist/errors.js.map +1 -0
- package/dist/parsing.d.ts +24 -0
- package/dist/parsing.d.ts.map +1 -0
- package/dist/parsing.js +62 -0
- package/dist/parsing.js.map +1 -0
- package/dist/types.d.ts +154 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +72 -0
- package/dist/types.js.map +1 -0
- package/package.json +64 -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,170 @@
|
|
|
1
|
+
# @dofek/peloton
|
|
2
|
+
|
|
3
|
+
Unofficial TypeScript client for Peloton's private member API. It includes
|
|
4
|
+
workout and performance-graph access, runtime response validation, PKCE token
|
|
5
|
+
exchange and refresh, and the observed Auth0 username/password browser flow.
|
|
6
|
+
|
|
7
|
+
This package is not affiliated with, endorsed by, or supported by Peloton.
|
|
8
|
+
The endpoints and automated login flow are undocumented implementation
|
|
9
|
+
details and may change without notice. Review the
|
|
10
|
+
[implementation](https://github.com/Asherlc/dofek/tree/main/packages/peloton-client/src)
|
|
11
|
+
before using it with an account you care about.
|
|
12
|
+
|
|
13
|
+
## Requirements and installation
|
|
14
|
+
|
|
15
|
+
Requires Node.js 22.14 or newer and its built-in
|
|
16
|
+
[`fetch`](https://nodejs.org/api/globals.html#fetch).
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
npm install @dofek/peloton
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quick start: automated sign-in
|
|
23
|
+
|
|
24
|
+
Set `PELOTON_EMAIL` and `PELOTON_PASSWORD`, save this as `example.mjs`, and run
|
|
25
|
+
`node example.mjs`:
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
import { PelotonClient } from "@dofek/peloton";
|
|
29
|
+
import { pelotonAutomatedLogin } from "@dofek/peloton/auth";
|
|
30
|
+
|
|
31
|
+
const email = process.env.PELOTON_EMAIL;
|
|
32
|
+
const password = process.env.PELOTON_PASSWORD;
|
|
33
|
+
if (!email || !password) {
|
|
34
|
+
throw new Error("Set PELOTON_EMAIL and PELOTON_PASSWORD");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const tokens = await pelotonAutomatedLogin(email, password);
|
|
38
|
+
const client = new PelotonClient(tokens.accessToken);
|
|
39
|
+
const workouts = await client.getWorkouts(0, 20);
|
|
40
|
+
|
|
41
|
+
console.log(workouts.data);
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`pelotonAutomatedLogin` drives the currently observed Auth0 Universal Login
|
|
45
|
+
HTML flow. Auth0 documents Universal Login as a browser authentication
|
|
46
|
+
experience, so automation is inherently more fragile than opening the
|
|
47
|
+
authorization URL in a user-controlled browser.
|
|
48
|
+
[Auth0 Universal Login documentation](https://auth0.com/docs/authenticate/login/auth0-universal-login)
|
|
49
|
+
|
|
50
|
+
Treat the password, access token, and refresh token as secrets. Do not log or
|
|
51
|
+
commit them. Multi-factor authentication, bot checks, account policy, or an
|
|
52
|
+
upstream page change can prevent automated login.
|
|
53
|
+
|
|
54
|
+
## Browser authorization with PKCE
|
|
55
|
+
|
|
56
|
+
For applications that can receive Peloton's configured callback, create a
|
|
57
|
+
PKCE authorization request, retain the verifier, and exchange the callback
|
|
58
|
+
code:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import {
|
|
62
|
+
createPelotonAuthorization,
|
|
63
|
+
exchangePelotonAuthorizationCode,
|
|
64
|
+
} from "@dofek/peloton/auth";
|
|
65
|
+
|
|
66
|
+
const authorization = createPelotonAuthorization();
|
|
67
|
+
console.log("Open this URL:", authorization.url);
|
|
68
|
+
|
|
69
|
+
// Obtain `code` from the callback URL in your application.
|
|
70
|
+
const tokens = await exchangePelotonAuthorizationCode(
|
|
71
|
+
code,
|
|
72
|
+
authorization.codeVerifier,
|
|
73
|
+
);
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The verifier must remain private and be paired with the same authorization
|
|
77
|
+
request. PKCE is standardized by
|
|
78
|
+
[RFC 7636](https://www.rfc-editor.org/rfc/rfc7636), and Auth0 describes the
|
|
79
|
+
[authorization-code flow with PKCE](https://auth0.com/docs/get-started/authentication-and-authorization-flow/authorization-code-flow-with-pkce).
|
|
80
|
+
|
|
81
|
+
## Refreshing tokens
|
|
82
|
+
|
|
83
|
+
Persist the complete token set in encrypted storage. Before API use, compare
|
|
84
|
+
`expiresAt` with the current time. Refresh an expired token and atomically
|
|
85
|
+
replace the stored set:
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
import { refreshPelotonAccessToken } from "@dofek/peloton/auth";
|
|
89
|
+
|
|
90
|
+
if (!tokens.refreshToken) {
|
|
91
|
+
throw new Error("Peloton did not return a refresh token; sign in again");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const refreshed = await refreshPelotonAccessToken(tokens.refreshToken);
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
If Peloton does not rotate the refresh token, the returned token set retains
|
|
98
|
+
the one supplied to `refreshPelotonAccessToken`.
|
|
99
|
+
|
|
100
|
+
## API client
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
import { PelotonClient } from "@dofek/peloton/client";
|
|
104
|
+
|
|
105
|
+
const client = new PelotonClient(accessToken);
|
|
106
|
+
|
|
107
|
+
const userId = await client.getUserId();
|
|
108
|
+
const page = await client.getWorkouts(0, 20);
|
|
109
|
+
const graph = await client.getPerformanceGraph(page.data[0].id, 5);
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
- `getUserId()` retrieves and caches the current Peloton member ID.
|
|
113
|
+
- `getWorkouts(page?, limit?)` returns a validated workout page joined with
|
|
114
|
+
ride and instructor details.
|
|
115
|
+
- `getPerformanceGraph(workoutId, everyN?)` returns validated metric samples;
|
|
116
|
+
`everyN` is the requested sampling interval in seconds.
|
|
117
|
+
|
|
118
|
+
All remote JSON crosses a
|
|
119
|
+
[`zod`](https://zod.dev/) schema boundary. A successful HTTP response whose
|
|
120
|
+
shape does not match the currently observed API throws
|
|
121
|
+
`PelotonResponseError`.
|
|
122
|
+
|
|
123
|
+
## Parsing
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
import {
|
|
127
|
+
parsePerformanceGraph,
|
|
128
|
+
parseWorkout,
|
|
129
|
+
} from "@dofek/peloton/parsing";
|
|
130
|
+
|
|
131
|
+
const workout = parseWorkout(page.data[0]);
|
|
132
|
+
const series = parsePerformanceGraph(graph, 5);
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
`parseWorkout` maps Peloton disciplines to the canonical activity types from
|
|
136
|
+
`@dofek/training`, converts Unix timestamps to `Date`, and retains useful
|
|
137
|
+
source metadata. `parsePerformanceGraph` adds a seconds offset to every metric
|
|
138
|
+
sample. It does not aggregate, merge, or discard samples.
|
|
139
|
+
|
|
140
|
+
## Errors and rate limits
|
|
141
|
+
|
|
142
|
+
- `PelotonAuthenticationError` indicates an API `401`.
|
|
143
|
+
- `PelotonServiceError` represents other unsuccessful Peloton API responses
|
|
144
|
+
and exposes `statusCode` and `responseBody`.
|
|
145
|
+
- `PelotonAuthFlowError` identifies the failed authentication stage and may
|
|
146
|
+
include HTTP diagnostics.
|
|
147
|
+
- `PelotonResponseError` identifies an invalid successful response.
|
|
148
|
+
- `ProviderRateLimitError` and `ProviderServiceUnavailableError` come from
|
|
149
|
+
`@dofek/provider-http` for `429` and transient `502`/`503`/`504` responses.
|
|
150
|
+
|
|
151
|
+
Declare `@dofek/provider-http` directly if your application imports its error
|
|
152
|
+
classes. Its retry-delay parsing follows the HTTP `Retry-After` semantics in
|
|
153
|
+
[RFC 9110](https://www.rfc-editor.org/rfc/rfc9110.html#name-retry-after).
|
|
154
|
+
|
|
155
|
+
## Public modules
|
|
156
|
+
|
|
157
|
+
- `@dofek/peloton` — API client and base URL.
|
|
158
|
+
- `@dofek/peloton/auth` — login, authorization-code exchange, refresh, and
|
|
159
|
+
token types.
|
|
160
|
+
- `@dofek/peloton/client` — API client and base URL.
|
|
161
|
+
- `@dofek/peloton/errors` — typed package errors.
|
|
162
|
+
- `@dofek/peloton/parsing` — pure workout and graph parsers.
|
|
163
|
+
- `@dofek/peloton/types` — Zod schemas and inferred raw API types.
|
|
164
|
+
|
|
165
|
+
## Project
|
|
166
|
+
|
|
167
|
+
- [Source](https://github.com/Asherlc/dofek/tree/main/packages/peloton-client)
|
|
168
|
+
- [Report an issue](https://github.com/Asherlc/dofek/issues)
|
|
169
|
+
- [Contribute a pull request](https://github.com/Asherlc/dofek/pulls)
|
|
170
|
+
- License: [MIT](./LICENSE)
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { PelotonOAuthConfig, PelotonTokenSet } from "./types.ts";
|
|
2
|
+
export declare function pelotonOAuthConfig(_host?: string): PelotonOAuthConfig;
|
|
3
|
+
export interface PelotonAuthorization {
|
|
4
|
+
url: string;
|
|
5
|
+
codeVerifier: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function createPelotonAuthorization(): PelotonAuthorization;
|
|
8
|
+
export declare function exchangePelotonAuthorizationCode(code: string, codeVerifier: string, fetchFn?: typeof globalThis.fetch): Promise<PelotonTokenSet>;
|
|
9
|
+
export declare function refreshPelotonAccessToken(refreshToken: string, fetchFn?: typeof globalThis.fetch): Promise<PelotonTokenSet>;
|
|
10
|
+
export declare function parseAuth0FormHtml(html: string): {
|
|
11
|
+
action: string;
|
|
12
|
+
fields: Record<string, string>;
|
|
13
|
+
};
|
|
14
|
+
export declare function pelotonAutomatedLogin(email: string, password: string, fetchFn?: typeof globalThis.fetch): Promise<PelotonTokenSet>;
|
|
15
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAqBtE,wBAAgB,kBAAkB,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,kBAAkB,CAUrE;AAyBD,MAAM,WAAW,oBAAoB;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,wBAAgB,0BAA0B,IAAI,oBAAoB,CAMjE;AAmCD,wBAAgB,gCAAgC,CAC9C,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,MAAM,EACpB,OAAO,GAAE,OAAO,UAAU,CAAC,KAAwB,GAClD,OAAO,CAAC,eAAe,CAAC,CAY1B;AAED,wBAAgB,yBAAyB,CACvC,YAAY,EAAE,MAAM,EACpB,OAAO,GAAE,OAAO,UAAU,CAAC,KAAwB,GAClD,OAAO,CAAC,eAAe,CAAC,CAM1B;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG;IAChD,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAChC,CAaA;AA8DD,wBAAsB,qBAAqB,CACzC,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,OAAO,UAAU,CAAC,KAAwB,GAClD,OAAO,CAAC,eAAe,CAAC,CA+G1B"}
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { createRateLimitAwareFetch } from "@dofek/provider-http/rate-limit";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { PelotonAuthFlowError, PelotonResponseError } from "./errors.js";
|
|
5
|
+
const PELOTON_AUTH_DOMAIN = "https://auth.onepeloton.com";
|
|
6
|
+
const PELOTON_CLIENT_ID = "WVoJxVDdPoFx4RNewvvg6ch2mZ7bwnsM";
|
|
7
|
+
const PELOTON_REDIRECT_URI = "https://members.onepeloton.com/callback";
|
|
8
|
+
const PELOTON_API_BASE = "https://api.onepeloton.com";
|
|
9
|
+
const AUTH0_CLIENT = btoa(JSON.stringify({ name: "auth0.js-ulp", version: "9.14.3" }));
|
|
10
|
+
const tokenResponseSchema = z.object({
|
|
11
|
+
access_token: z.string().min(1),
|
|
12
|
+
refresh_token: z.string().min(1).optional(),
|
|
13
|
+
expires_in: z.number().positive(),
|
|
14
|
+
scope: z.string().optional(),
|
|
15
|
+
});
|
|
16
|
+
const injectedConfigSchema = z.object({
|
|
17
|
+
extraParams: z.object({
|
|
18
|
+
state: z.string().min(1),
|
|
19
|
+
_csrf: z.string().min(1),
|
|
20
|
+
nonce: z.string().optional(),
|
|
21
|
+
}),
|
|
22
|
+
});
|
|
23
|
+
export function pelotonOAuthConfig(_host) {
|
|
24
|
+
return {
|
|
25
|
+
clientId: PELOTON_CLIENT_ID,
|
|
26
|
+
authorizeUrl: `${PELOTON_AUTH_DOMAIN}/authorize`,
|
|
27
|
+
tokenUrl: `${PELOTON_AUTH_DOMAIN}/oauth/token`,
|
|
28
|
+
redirectUri: PELOTON_REDIRECT_URI,
|
|
29
|
+
scopes: ["offline_access", "openid", "peloton-api.members:default"],
|
|
30
|
+
usePkce: true,
|
|
31
|
+
audience: `${PELOTON_API_BASE}/`,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function generateCodeVerifier() {
|
|
35
|
+
return randomBytes(32).toString("base64url");
|
|
36
|
+
}
|
|
37
|
+
function generateCodeChallenge(verifier) {
|
|
38
|
+
return createHash("sha256").update(verifier).digest("base64url");
|
|
39
|
+
}
|
|
40
|
+
function buildAuthorizationUrl(codeChallenge, state, nonce) {
|
|
41
|
+
const config = pelotonOAuthConfig();
|
|
42
|
+
const url = new URL(config.authorizeUrl);
|
|
43
|
+
url.searchParams.set("client_id", config.clientId);
|
|
44
|
+
url.searchParams.set("redirect_uri", config.redirectUri);
|
|
45
|
+
url.searchParams.set("response_type", "code");
|
|
46
|
+
url.searchParams.set("scope", config.scopes.join(" "));
|
|
47
|
+
url.searchParams.set("audience", config.audience);
|
|
48
|
+
url.searchParams.set("code_challenge", codeChallenge);
|
|
49
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
50
|
+
if (state)
|
|
51
|
+
url.searchParams.set("state", state);
|
|
52
|
+
if (nonce)
|
|
53
|
+
url.searchParams.set("nonce", nonce);
|
|
54
|
+
return url.toString();
|
|
55
|
+
}
|
|
56
|
+
export function createPelotonAuthorization() {
|
|
57
|
+
const codeVerifier = generateCodeVerifier();
|
|
58
|
+
return {
|
|
59
|
+
url: buildAuthorizationUrl(generateCodeChallenge(codeVerifier)),
|
|
60
|
+
codeVerifier,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
async function requestTokens(params, fallbackRefreshToken, fetchFn) {
|
|
64
|
+
const config = pelotonOAuthConfig();
|
|
65
|
+
const rateLimitFetch = createRateLimitAwareFetch(fetchFn, { providerId: "peloton" });
|
|
66
|
+
const response = await rateLimitFetch(config.tokenUrl, {
|
|
67
|
+
method: "POST",
|
|
68
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
69
|
+
body: new URLSearchParams({ ...params, client_id: config.clientId }).toString(),
|
|
70
|
+
});
|
|
71
|
+
if (!response.ok) {
|
|
72
|
+
const responseBody = await response.text();
|
|
73
|
+
throw new PelotonAuthFlowError("token", `Peloton token request failed (${response.status})`, {
|
|
74
|
+
statusCode: response.status,
|
|
75
|
+
responseBody,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
const body = tokenResponseSchema.parse(await response.json());
|
|
80
|
+
return {
|
|
81
|
+
accessToken: body.access_token,
|
|
82
|
+
refreshToken: body.refresh_token ?? fallbackRefreshToken,
|
|
83
|
+
expiresAt: new Date(Date.now() + body.expires_in * 1000),
|
|
84
|
+
scopes: body.scope ?? null,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
throw new PelotonResponseError("token", error);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
export function exchangePelotonAuthorizationCode(code, codeVerifier, fetchFn = globalThis.fetch) {
|
|
92
|
+
const config = pelotonOAuthConfig();
|
|
93
|
+
return requestTokens({
|
|
94
|
+
grant_type: "authorization_code",
|
|
95
|
+
code,
|
|
96
|
+
redirect_uri: config.redirectUri,
|
|
97
|
+
code_verifier: codeVerifier,
|
|
98
|
+
}, null, fetchFn);
|
|
99
|
+
}
|
|
100
|
+
export function refreshPelotonAccessToken(refreshToken, fetchFn = globalThis.fetch) {
|
|
101
|
+
return requestTokens({ grant_type: "refresh_token", refresh_token: refreshToken }, refreshToken, fetchFn);
|
|
102
|
+
}
|
|
103
|
+
export function parseAuth0FormHtml(html) {
|
|
104
|
+
const action = html.match(/<form[^>]+action="([^"]+)"/)?.[1];
|
|
105
|
+
if (!action) {
|
|
106
|
+
throw new PelotonAuthFlowError("login-form", "Could not find form action in Auth0 response");
|
|
107
|
+
}
|
|
108
|
+
const fields = {};
|
|
109
|
+
for (const match of html.matchAll(/<input[^>]+type="hidden"[^>]*>/gi)) {
|
|
110
|
+
const tag = match[0];
|
|
111
|
+
const name = tag.match(/name="([^"]+)"/)?.[1];
|
|
112
|
+
if (name)
|
|
113
|
+
fields[name] = tag.match(/value="([^"]*)"/)?.[1] ?? "";
|
|
114
|
+
}
|
|
115
|
+
return { action, fields };
|
|
116
|
+
}
|
|
117
|
+
function getSetCookieHeaders(headers) {
|
|
118
|
+
if (typeof headers.getSetCookie === "function")
|
|
119
|
+
return headers.getSetCookie();
|
|
120
|
+
const combined = headers.get("set-cookie");
|
|
121
|
+
return combined ? combined.split(/,\s*(?=[^;,=\s]+=[^;,]*)/) : [];
|
|
122
|
+
}
|
|
123
|
+
class CookieJar {
|
|
124
|
+
#cookies = new Map();
|
|
125
|
+
addFromResponse(url, headers) {
|
|
126
|
+
const domain = new URL(url).hostname;
|
|
127
|
+
const cookies = this.#cookies.get(domain) ?? new Map();
|
|
128
|
+
for (const header of getSetCookieHeaders(headers)) {
|
|
129
|
+
const match = header.match(/^([^=]+)=([^;]*)/);
|
|
130
|
+
const name = match?.[1];
|
|
131
|
+
const value = match?.[2];
|
|
132
|
+
if (name !== undefined && value !== undefined)
|
|
133
|
+
cookies.set(name, value);
|
|
134
|
+
}
|
|
135
|
+
this.#cookies.set(domain, cookies);
|
|
136
|
+
}
|
|
137
|
+
getForUrl(url) {
|
|
138
|
+
const hostname = new URL(url).hostname;
|
|
139
|
+
const values = [];
|
|
140
|
+
for (const [domain, cookies] of this.#cookies) {
|
|
141
|
+
if (hostname === domain || hostname.endsWith(`.${domain}`)) {
|
|
142
|
+
for (const [name, value] of cookies)
|
|
143
|
+
values.push(`${name}=${value}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return values.join("; ");
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
async function followRedirect(url, jar, fetchFn, init) {
|
|
150
|
+
const fullUrl = new URL(url, PELOTON_AUTH_DOMAIN).toString();
|
|
151
|
+
const response = await fetchFn(fullUrl, {
|
|
152
|
+
...init,
|
|
153
|
+
redirect: "manual",
|
|
154
|
+
headers: { ...(init?.headers ?? {}), Cookie: jar.getForUrl(fullUrl) },
|
|
155
|
+
});
|
|
156
|
+
jar.addFromResponse(fullUrl, response.headers);
|
|
157
|
+
return { response, location: response.headers.get("location") };
|
|
158
|
+
}
|
|
159
|
+
function decodeHtmlEntities(value) {
|
|
160
|
+
return value
|
|
161
|
+
.replace(/"/g, '"')
|
|
162
|
+
.replace(/'/g, "'")
|
|
163
|
+
.replace(/"/g, '"')
|
|
164
|
+
.replace(/'/g, "'")
|
|
165
|
+
.replace(/</g, "<")
|
|
166
|
+
.replace(/>/g, ">")
|
|
167
|
+
.replace(/&/g, "&");
|
|
168
|
+
}
|
|
169
|
+
export async function pelotonAutomatedLogin(email, password, fetchFn = globalThis.fetch) {
|
|
170
|
+
const rateLimitFetch = createRateLimitAwareFetch(fetchFn, { providerId: "peloton" });
|
|
171
|
+
const config = pelotonOAuthConfig();
|
|
172
|
+
const codeVerifier = generateCodeVerifier();
|
|
173
|
+
const codeChallenge = generateCodeChallenge(codeVerifier);
|
|
174
|
+
const state = generateCodeVerifier();
|
|
175
|
+
const nonce = generateCodeVerifier();
|
|
176
|
+
const jar = new CookieJar();
|
|
177
|
+
let { response, location } = await followRedirect(buildAuthorizationUrl(codeChallenge, state, nonce), jar, rateLimitFetch);
|
|
178
|
+
let redirectsRemaining = 15;
|
|
179
|
+
while (location && redirectsRemaining > 0) {
|
|
180
|
+
({ response, location } = await followRedirect(location, jar, rateLimitFetch));
|
|
181
|
+
redirectsRemaining--;
|
|
182
|
+
}
|
|
183
|
+
if (location) {
|
|
184
|
+
throw new PelotonAuthFlowError("authorize", "Auth0 authorization redirect limit exceeded");
|
|
185
|
+
}
|
|
186
|
+
const loginPageHtml = await response.text();
|
|
187
|
+
const encodedConfig = loginPageHtml.match(/window\.injectedConfig\s*=\s*[^"]*"([^"]+)"/)?.[1];
|
|
188
|
+
if (!encodedConfig) {
|
|
189
|
+
throw new PelotonAuthFlowError("authorize", "Could not find injectedConfig in Auth0 login page");
|
|
190
|
+
}
|
|
191
|
+
let extraParams;
|
|
192
|
+
try {
|
|
193
|
+
extraParams = injectedConfigSchema.parse(JSON.parse(Buffer.from(encodedConfig, "base64").toString("utf8"))).extraParams;
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
throw new PelotonResponseError("Auth0 injected configuration", error);
|
|
197
|
+
}
|
|
198
|
+
const loginUrl = `${PELOTON_AUTH_DOMAIN}/usernamepassword/login`;
|
|
199
|
+
const { response: loginResponse } = await followRedirect(loginUrl, jar, rateLimitFetch, {
|
|
200
|
+
method: "POST",
|
|
201
|
+
headers: { "Content-Type": "application/json", "Auth0-Client": AUTH0_CLIENT },
|
|
202
|
+
body: JSON.stringify({
|
|
203
|
+
client_id: config.clientId,
|
|
204
|
+
redirect_uri: config.redirectUri,
|
|
205
|
+
tenant: "peloton-prod",
|
|
206
|
+
response_type: "code",
|
|
207
|
+
scope: config.scopes.join(" "),
|
|
208
|
+
audience: config.audience,
|
|
209
|
+
_csrf: extraParams._csrf,
|
|
210
|
+
state: extraParams.state,
|
|
211
|
+
nonce: extraParams.nonce ?? nonce,
|
|
212
|
+
connection: "pelo-user-password",
|
|
213
|
+
username: email,
|
|
214
|
+
password,
|
|
215
|
+
code_challenge: codeChallenge,
|
|
216
|
+
code_challenge_method: "S256",
|
|
217
|
+
}),
|
|
218
|
+
});
|
|
219
|
+
if (!loginResponse.ok) {
|
|
220
|
+
const responseBody = await loginResponse.text();
|
|
221
|
+
throw new PelotonAuthFlowError("credentials", `Auth0 login failed (${loginResponse.status}): ${responseBody}`, { statusCode: loginResponse.status, responseBody });
|
|
222
|
+
}
|
|
223
|
+
const { action, fields: encodedFields } = parseAuth0FormHtml(await loginResponse.text());
|
|
224
|
+
const fields = Object.fromEntries(Object.entries(encodedFields).map(([key, value]) => [key, decodeHtmlEntities(value)]));
|
|
225
|
+
let { location: callbackLocation } = await followRedirect(action, jar, rateLimitFetch, {
|
|
226
|
+
method: "POST",
|
|
227
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
228
|
+
body: new URLSearchParams(fields).toString(),
|
|
229
|
+
});
|
|
230
|
+
redirectsRemaining = 15;
|
|
231
|
+
while (callbackLocation &&
|
|
232
|
+
!callbackLocation.includes("code=") &&
|
|
233
|
+
!callbackLocation.includes("error=") &&
|
|
234
|
+
redirectsRemaining > 0) {
|
|
235
|
+
({ location: callbackLocation } = await followRedirect(callbackLocation, jar, rateLimitFetch));
|
|
236
|
+
redirectsRemaining--;
|
|
237
|
+
}
|
|
238
|
+
if (!callbackLocation) {
|
|
239
|
+
throw new PelotonAuthFlowError("callback", "Auth0 redirect chain ended without a Location header");
|
|
240
|
+
}
|
|
241
|
+
const callbackUrl = new URL(callbackLocation, PELOTON_REDIRECT_URI);
|
|
242
|
+
const authError = callbackUrl.searchParams.get("error");
|
|
243
|
+
if (authError) {
|
|
244
|
+
throw new PelotonAuthFlowError("callback", `Auth0 returned error: ${callbackUrl.searchParams.get("error_description") ?? authError}`);
|
|
245
|
+
}
|
|
246
|
+
const code = callbackUrl.searchParams.get("code");
|
|
247
|
+
if (!code) {
|
|
248
|
+
throw new PelotonAuthFlowError("callback", "Authorization code not found in callback URL");
|
|
249
|
+
}
|
|
250
|
+
return exchangePelotonAuthorizationCode(code, codeVerifier, rateLimitFetch);
|
|
251
|
+
}
|
|
252
|
+
//# sourceMappingURL=auth.js.map
|
package/dist/auth.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAAE,yBAAyB,EAAE,MAAM,iCAAiC,CAAC;AAC5E,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAGzE,MAAM,mBAAmB,GAAG,6BAA6B,CAAC;AAC1D,MAAM,iBAAiB,GAAG,kCAAkC,CAAC;AAC7D,MAAM,oBAAoB,GAAG,yCAAyC,CAAC;AACvE,MAAM,gBAAgB,GAAG,4BAA4B,CAAC;AACtD,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC;AACvF,MAAM,mBAAmB,GAAG,CAAC,CAAC,MAAM,CAAC;IACnC,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC/B,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;IAC3C,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACjC,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAC7B,CAAC,CAAC;AACH,MAAM,oBAAoB,GAAG,CAAC,CAAC,MAAM,CAAC;IACpC,WAAW,EAAE,CAAC,CAAC,MAAM,CAAC;QACpB,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;QACxB,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;QACxB,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;KAC7B,CAAC;CACH,CAAC,CAAC;AAEH,MAAM,UAAU,kBAAkB,CAAC,KAAc;IAC/C,OAAO;QACL,QAAQ,EAAE,iBAAiB;QAC3B,YAAY,EAAE,GAAG,mBAAmB,YAAY;QAChD,QAAQ,EAAE,GAAG,mBAAmB,cAAc;QAC9C,WAAW,EAAE,oBAAoB;QACjC,MAAM,EAAE,CAAC,gBAAgB,EAAE,QAAQ,EAAE,6BAA6B,CAAC;QACnE,OAAO,EAAE,IAAI;QACb,QAAQ,EAAE,GAAG,gBAAgB,GAAG;KACjC,CAAC;AACJ,CAAC;AAED,SAAS,oBAAoB;IAC3B,OAAO,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AAC/C,CAAC;AAED,SAAS,qBAAqB,CAAC,QAAgB;IAC7C,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;AACnE,CAAC;AAED,SAAS,qBAAqB,CAAC,aAAqB,EAAE,KAAc,EAAE,KAAc;IAClF,MAAM,MAAM,GAAG,kBAAkB,EAAE,CAAC;IACpC,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;IACzC,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,WAAW,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC;IACnD,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,cAAc,EAAE,MAAM,CAAC,WAAW,CAAC,CAAC;IACzD,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,eAAe,EAAE,MAAM,CAAC,CAAC;IAC9C,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IACvD,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,UAAU,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC;IAClD,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,gBAAgB,EAAE,aAAa,CAAC,CAAC;IACtD,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,uBAAuB,EAAE,MAAM,CAAC,CAAC;IACtD,IAAI,KAAK;QAAE,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IAChD,IAAI,KAAK;QAAE,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IAChD,OAAO,GAAG,CAAC,QAAQ,EAAE,CAAC;AACxB,CAAC;AAOD,MAAM,UAAU,0BAA0B;IACxC,MAAM,YAAY,GAAG,oBAAoB,EAAE,CAAC;IAC5C,OAAO;QACL,GAAG,EAAE,qBAAqB,CAAC,qBAAqB,CAAC,YAAY,CAAC,CAAC;QAC/D,YAAY;KACb,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,aAAa,CAC1B,MAA8B,EAC9B,oBAAmC,EACnC,OAAgC;IAEhC,MAAM,MAAM,GAAG,kBAAkB,EAAE,CAAC;IACpC,MAAM,cAAc,GAAG,yBAAyB,CAAC,OAAO,EAAE,EAAE,UAAU,EAAE,SAAS,EAAE,CAAC,CAAC;IACrF,MAAM,QAAQ,GAAG,MAAM,cAAc,CAAC,MAAM,CAAC,QAAQ,EAAE;QACrD,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE,cAAc,EAAE,mCAAmC,EAAE;QAChE,IAAI,EAAE,IAAI,eAAe,CAAC,EAAE,GAAG,MAAM,EAAE,SAAS,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE;KAChF,CAAC,CAAC;IACH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,YAAY,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;QAC3C,MAAM,IAAI,oBAAoB,CAAC,OAAO,EAAE,iCAAiC,QAAQ,CAAC,MAAM,GAAG,EAAE;YAC3F,UAAU,EAAE,QAAQ,CAAC,MAAM;YAC3B,YAAY;SACb,CAAC,CAAC;IACL,CAAC;IAED,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,mBAAmB,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;QAC9D,OAAO;YACL,WAAW,EAAE,IAAI,CAAC,YAAY;YAC9B,YAAY,EAAE,IAAI,CAAC,aAAa,IAAI,oBAAoB;YACxD,SAAS,EAAE,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;YACxD,MAAM,EAAE,IAAI,CAAC,KAAK,IAAI,IAAI;SAC3B,CAAC;IACJ,CAAC;IAAC,OAAO,KAAc,EAAE,CAAC;QACxB,MAAM,IAAI,oBAAoB,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IACjD,CAAC;AACH,CAAC;AAED,MAAM,UAAU,gCAAgC,CAC9C,IAAY,EACZ,YAAoB,EACpB,UAAmC,UAAU,CAAC,KAAK;IAEnD,MAAM,MAAM,GAAG,kBAAkB,EAAE,CAAC;IACpC,OAAO,aAAa,CAClB;QACE,UAAU,EAAE,oBAAoB;QAChC,IAAI;QACJ,YAAY,EAAE,MAAM,CAAC,WAAW;QAChC,aAAa,EAAE,YAAY;KAC5B,EACD,IAAI,EACJ,OAAO,CACR,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,yBAAyB,CACvC,YAAoB,EACpB,UAAmC,UAAU,CAAC,KAAK;IAEnD,OAAO,aAAa,CAClB,EAAE,UAAU,EAAE,eAAe,EAAE,aAAa,EAAE,YAAY,EAAE,EAC5D,YAAY,EACZ,OAAO,CACR,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAI7C,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,4BAA4B,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAC7D,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,IAAI,oBAAoB,CAAC,YAAY,EAAE,8CAA8C,CAAC,CAAC;IAC/F,CAAC;IAED,MAAM,MAAM,GAA2B,EAAE,CAAC;IAC1C,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ,CAAC,kCAAkC,CAAC,EAAE,CAAC;QACtE,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACrB,MAAM,IAAI,GAAG,GAAG,CAAC,KAAK,CAAC,gBAAgB,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAC9C,IAAI,IAAI;YAAE,MAAM,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,iBAAiB,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IACnE,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;AAC5B,CAAC;AAED,SAAS,mBAAmB,CAAC,OAAgB;IAC3C,IAAI,OAAO,OAAO,CAAC,YAAY,KAAK,UAAU;QAAE,OAAO,OAAO,CAAC,YAAY,EAAE,CAAC;IAC9E,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;IAC3C,OAAO,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,0BAA0B,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AACpE,CAAC;AAED,MAAM,SAAS;IACb,QAAQ,GAAG,IAAI,GAAG,EAA+B,CAAC;IAElD,eAAe,CAAC,GAAW,EAAE,OAAgB;QAC3C,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC;QACrC,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,IAAI,GAAG,EAAkB,CAAC;QACvE,KAAK,MAAM,MAAM,IAAI,mBAAmB,CAAC,OAAO,CAAC,EAAE,CAAC;YAClD,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,kBAAkB,CAAC,CAAC;YAC/C,MAAM,IAAI,GAAG,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;YACxB,MAAM,KAAK,GAAG,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;YACzB,IAAI,IAAI,KAAK,SAAS,IAAI,KAAK,KAAK,SAAS;gBAAE,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QAC1E,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,CAAC;IAED,SAAS,CAAC,GAAW;QACnB,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC;QACvC,MAAM,MAAM,GAAa,EAAE,CAAC;QAC5B,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAC9C,IAAI,QAAQ,KAAK,MAAM,IAAI,QAAQ,CAAC,QAAQ,CAAC,IAAI,MAAM,EAAE,CAAC,EAAE,CAAC;gBAC3D,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,OAAO;oBAAE,MAAM,CAAC,IAAI,CAAC,GAAG,IAAI,IAAI,KAAK,EAAE,CAAC,CAAC;YACvE,CAAC;QACH,CAAC;QACD,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC3B,CAAC;CACF;AAED,KAAK,UAAU,cAAc,CAC3B,GAAW,EACX,GAAc,EACd,OAAgC,EAChC,IAAkB;IAElB,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,GAAG,EAAE,mBAAmB,CAAC,CAAC,QAAQ,EAAE,CAAC;IAC7D,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,OAAO,EAAE;QACtC,GAAG,IAAI;QACP,QAAQ,EAAE,QAAQ;QAClB,OAAO,EAAE,EAAE,GAAG,CAAC,IAAI,EAAE,OAAO,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE;KACtE,CAAC,CAAC;IACH,GAAG,CAAC,eAAe,CAAC,OAAO,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;IAC/C,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC;AAClE,CAAC;AAED,SAAS,kBAAkB,CAAC,KAAa;IACvC,OAAO,KAAK;SACT,OAAO,CAAC,QAAQ,EAAE,GAAG,CAAC;SACtB,OAAO,CAAC,QAAQ,EAAE,GAAG,CAAC;SACtB,OAAO,CAAC,SAAS,EAAE,GAAG,CAAC;SACvB,OAAO,CAAC,SAAS,EAAE,GAAG,CAAC;SACvB,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC;SACrB,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC;SACrB,OAAO,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;AAC5B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACzC,KAAa,EACb,QAAgB,EAChB,UAAmC,UAAU,CAAC,KAAK;IAEnD,MAAM,cAAc,GAAG,yBAAyB,CAAC,OAAO,EAAE,EAAE,UAAU,EAAE,SAAS,EAAE,CAAC,CAAC;IACrF,MAAM,MAAM,GAAG,kBAAkB,EAAE,CAAC;IACpC,MAAM,YAAY,GAAG,oBAAoB,EAAE,CAAC;IAC5C,MAAM,aAAa,GAAG,qBAAqB,CAAC,YAAY,CAAC,CAAC;IAC1D,MAAM,KAAK,GAAG,oBAAoB,EAAE,CAAC;IACrC,MAAM,KAAK,GAAG,oBAAoB,EAAE,CAAC;IACrC,MAAM,GAAG,GAAG,IAAI,SAAS,EAAE,CAAC;IAE5B,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,MAAM,cAAc,CAC/C,qBAAqB,CAAC,aAAa,EAAE,KAAK,EAAE,KAAK,CAAC,EAClD,GAAG,EACH,cAAc,CACf,CAAC;IACF,IAAI,kBAAkB,GAAG,EAAE,CAAC;IAC5B,OAAO,QAAQ,IAAI,kBAAkB,GAAG,CAAC,EAAE,CAAC;QAC1C,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,MAAM,cAAc,CAAC,QAAQ,EAAE,GAAG,EAAE,cAAc,CAAC,CAAC,CAAC;QAC/E,kBAAkB,EAAE,CAAC;IACvB,CAAC;IACD,IAAI,QAAQ,EAAE,CAAC;QACb,MAAM,IAAI,oBAAoB,CAAC,WAAW,EAAE,6CAA6C,CAAC,CAAC;IAC7F,CAAC;IAED,MAAM,aAAa,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;IAC5C,MAAM,aAAa,GAAG,aAAa,CAAC,KAAK,CAAC,6CAA6C,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAC9F,IAAI,CAAC,aAAa,EAAE,CAAC;QACnB,MAAM,IAAI,oBAAoB,CAC5B,WAAW,EACX,mDAAmD,CACpD,CAAC;IACJ,CAAC;IAED,IAAI,WAAgE,CAAC;IACrE,IAAI,CAAC;QACH,WAAW,GAAG,oBAAoB,CAAC,KAAK,CACtC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,aAAa,EAAE,QAAQ,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAClE,CAAC,WAAW,CAAC;IAChB,CAAC;IAAC,OAAO,KAAc,EAAE,CAAC;QACxB,MAAM,IAAI,oBAAoB,CAAC,8BAA8B,EAAE,KAAK,CAAC,CAAC;IACxE,CAAC;IAED,MAAM,QAAQ,GAAG,GAAG,mBAAmB,yBAAyB,CAAC;IACjE,MAAM,EAAE,QAAQ,EAAE,aAAa,EAAE,GAAG,MAAM,cAAc,CAAC,QAAQ,EAAE,GAAG,EAAE,cAAc,EAAE;QACtF,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,cAAc,EAAE,YAAY,EAAE;QAC7E,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;YACnB,SAAS,EAAE,MAAM,CAAC,QAAQ;YAC1B,YAAY,EAAE,MAAM,CAAC,WAAW;YAChC,MAAM,EAAE,cAAc;YACtB,aAAa,EAAE,MAAM;YACrB,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC;YAC9B,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,KAAK,EAAE,WAAW,CAAC,KAAK;YACxB,KAAK,EAAE,WAAW,CAAC,KAAK;YACxB,KAAK,EAAE,WAAW,CAAC,KAAK,IAAI,KAAK;YACjC,UAAU,EAAE,oBAAoB;YAChC,QAAQ,EAAE,KAAK;YACf,QAAQ;YACR,cAAc,EAAE,aAAa;YAC7B,qBAAqB,EAAE,MAAM;SAC9B,CAAC;KACH,CAAC,CAAC;IACH,IAAI,CAAC,aAAa,CAAC,EAAE,EAAE,CAAC;QACtB,MAAM,YAAY,GAAG,MAAM,aAAa,CAAC,IAAI,EAAE,CAAC;QAChD,MAAM,IAAI,oBAAoB,CAC5B,aAAa,EACb,uBAAuB,aAAa,CAAC,MAAM,MAAM,YAAY,EAAE,EAC/D,EAAE,UAAU,EAAE,aAAa,CAAC,MAAM,EAAE,YAAY,EAAE,CACnD,CAAC;IACJ,CAAC;IAED,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,GAAG,kBAAkB,CAAC,MAAM,aAAa,CAAC,IAAI,EAAE,CAAC,CAAC;IACzF,MAAM,MAAM,GAAG,MAAM,CAAC,WAAW,CAC/B,MAAM,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,EAAE,kBAAkB,CAAC,KAAK,CAAC,CAAC,CAAC,CACtF,CAAC;IACF,IAAI,EAAE,QAAQ,EAAE,gBAAgB,EAAE,GAAG,MAAM,cAAc,CAAC,MAAM,EAAE,GAAG,EAAE,cAAc,EAAE;QACrF,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE,cAAc,EAAE,mCAAmC,EAAE;QAChE,IAAI,EAAE,IAAI,eAAe,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE;KAC7C,CAAC,CAAC;IAEH,kBAAkB,GAAG,EAAE,CAAC;IACxB,OACE,gBAAgB;QAChB,CAAC,gBAAgB,CAAC,QAAQ,CAAC,OAAO,CAAC;QACnC,CAAC,gBAAgB,CAAC,QAAQ,CAAC,QAAQ,CAAC;QACpC,kBAAkB,GAAG,CAAC,EACtB,CAAC;QACD,CAAC,EAAE,QAAQ,EAAE,gBAAgB,EAAE,GAAG,MAAM,cAAc,CAAC,gBAAgB,EAAE,GAAG,EAAE,cAAc,CAAC,CAAC,CAAC;QAC/F,kBAAkB,EAAE,CAAC;IACvB,CAAC;IACD,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACtB,MAAM,IAAI,oBAAoB,CAC5B,UAAU,EACV,sDAAsD,CACvD,CAAC;IACJ,CAAC;IAED,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,gBAAgB,EAAE,oBAAoB,CAAC,CAAC;IACpE,MAAM,SAAS,GAAG,WAAW,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IACxD,IAAI,SAAS,EAAE,CAAC;QACd,MAAM,IAAI,oBAAoB,CAC5B,UAAU,EACV,yBAAyB,WAAW,CAAC,YAAY,CAAC,GAAG,CAAC,mBAAmB,CAAC,IAAI,SAAS,EAAE,CAC1F,CAAC;IACJ,CAAC;IACD,MAAM,IAAI,GAAG,WAAW,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IAClD,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,MAAM,IAAI,oBAAoB,CAAC,UAAU,EAAE,8CAA8C,CAAC,CAAC;IAC7F,CAAC;IACD,OAAO,gCAAgC,CAAC,IAAI,EAAE,YAAY,EAAE,cAAc,CAAC,CAAC;AAC9E,CAAC"}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type PelotonPerformanceGraph, type PelotonWorkoutListResponse } from "./types.ts";
|
|
2
|
+
export declare const PELOTON_API_BASE = "https://api.onepeloton.com";
|
|
3
|
+
export declare class PelotonClient {
|
|
4
|
+
#private;
|
|
5
|
+
constructor(accessToken: string, fetchFn?: typeof globalThis.fetch);
|
|
6
|
+
getUserId(): Promise<string>;
|
|
7
|
+
getWorkouts(page?: number, limit?: number): Promise<PelotonWorkoutListResponse>;
|
|
8
|
+
getPerformanceGraph(workoutId: string, everyN?: number): Promise<PelotonPerformanceGraph>;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAOA,OAAO,EACL,KAAK,uBAAuB,EAC5B,KAAK,0BAA0B,EAGhC,MAAM,YAAY,CAAC;AAEpB,eAAO,MAAM,gBAAgB,+BAA+B,CAAC;AAI7D,qBAAa,aAAa;;gBAKZ,WAAW,EAAE,MAAM,EAAE,OAAO,GAAE,OAAO,UAAU,CAAC,KAAwB;IA2B9E,SAAS,IAAI,OAAO,CAAC,MAAM,CAAC;IAmB5B,WAAW,CAAC,IAAI,SAAI,EAAE,KAAK,SAAK,GAAG,OAAO,CAAC,0BAA0B,CAAC;IAwBtE,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,SAAI,GAAG,OAAO,CAAC,uBAAuB,CAAC;CAmB3F"}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { createRateLimitAwareFetch, ProviderRateLimitError, ProviderServiceUnavailableError, } from "@dofek/provider-http/rate-limit";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { PelotonAuthenticationError, PelotonResponseError, PelotonServiceError } from "./errors.js";
|
|
4
|
+
import { pelotonPerformanceGraphSchema, pelotonWorkoutListResponseSchema, } from "./types.js";
|
|
5
|
+
export const PELOTON_API_BASE = "https://api.onepeloton.com";
|
|
6
|
+
const pelotonUserSchema = z.object({ id: z.string().min(1) });
|
|
7
|
+
export class PelotonClient {
|
|
8
|
+
#accessToken;
|
|
9
|
+
#userId = null;
|
|
10
|
+
#fetchFn;
|
|
11
|
+
constructor(accessToken, fetchFn = globalThis.fetch) {
|
|
12
|
+
this.#accessToken = accessToken;
|
|
13
|
+
this.#fetchFn = createRateLimitAwareFetch(fetchFn, { providerId: "peloton" });
|
|
14
|
+
}
|
|
15
|
+
async #get(path, params) {
|
|
16
|
+
const url = new URL(path, PELOTON_API_BASE);
|
|
17
|
+
for (const [key, value] of Object.entries(params ?? {})) {
|
|
18
|
+
url.searchParams.set(key, value);
|
|
19
|
+
}
|
|
20
|
+
const response = await this.#fetchFn(url.toString(), {
|
|
21
|
+
headers: {
|
|
22
|
+
Authorization: `Bearer ${this.#accessToken}`,
|
|
23
|
+
"peloton-platform": "web",
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
const responseBody = await response.text();
|
|
28
|
+
if (response.status === 401) {
|
|
29
|
+
throw new PelotonAuthenticationError(response.status, responseBody);
|
|
30
|
+
}
|
|
31
|
+
throw new PelotonServiceError(response.status, responseBody);
|
|
32
|
+
}
|
|
33
|
+
return response.json();
|
|
34
|
+
}
|
|
35
|
+
async getUserId() {
|
|
36
|
+
if (this.#userId)
|
|
37
|
+
return this.#userId;
|
|
38
|
+
try {
|
|
39
|
+
const parsed = pelotonUserSchema.parse(await this.#get("/api/me"));
|
|
40
|
+
this.#userId = parsed.id;
|
|
41
|
+
return parsed.id;
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
if (error instanceof PelotonAuthenticationError ||
|
|
45
|
+
error instanceof PelotonServiceError ||
|
|
46
|
+
error instanceof ProviderRateLimitError ||
|
|
47
|
+
error instanceof ProviderServiceUnavailableError) {
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
throw new PelotonResponseError("current-user", error);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async getWorkouts(page = 0, limit = 20) {
|
|
54
|
+
const userId = await this.getUserId();
|
|
55
|
+
try {
|
|
56
|
+
return pelotonWorkoutListResponseSchema.parse(await this.#get(`/api/user/${userId}/workouts`, {
|
|
57
|
+
page: String(page),
|
|
58
|
+
limit: String(limit),
|
|
59
|
+
sort_by: "-created_at",
|
|
60
|
+
joins: "ride",
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
if (error instanceof PelotonAuthenticationError ||
|
|
65
|
+
error instanceof PelotonServiceError ||
|
|
66
|
+
error instanceof ProviderRateLimitError ||
|
|
67
|
+
error instanceof ProviderServiceUnavailableError) {
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
throw new PelotonResponseError("workouts", error);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async getPerformanceGraph(workoutId, everyN = 5) {
|
|
74
|
+
try {
|
|
75
|
+
return pelotonPerformanceGraphSchema.parse(await this.#get(`/api/workout/${workoutId}/performance_graph`, {
|
|
76
|
+
every_n: String(everyN),
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
if (error instanceof PelotonAuthenticationError ||
|
|
81
|
+
error instanceof PelotonServiceError ||
|
|
82
|
+
error instanceof ProviderRateLimitError ||
|
|
83
|
+
error instanceof ProviderServiceUnavailableError) {
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
throw new PelotonResponseError("performance-graph", error);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export declare class PelotonServiceError extends Error {
|
|
2
|
+
readonly statusCode: number;
|
|
3
|
+
readonly responseBody: string;
|
|
4
|
+
constructor(statusCode: number, responseBody: string, options?: ErrorOptions);
|
|
5
|
+
}
|
|
6
|
+
export declare class PelotonAuthenticationError extends PelotonServiceError {
|
|
7
|
+
constructor(statusCode: number, responseBody: string, options?: ErrorOptions);
|
|
8
|
+
}
|
|
9
|
+
export declare class PelotonAuthFlowError extends Error {
|
|
10
|
+
readonly stage: string;
|
|
11
|
+
readonly statusCode?: number;
|
|
12
|
+
readonly responseBody?: string;
|
|
13
|
+
constructor(stage: string, message: string, details?: {
|
|
14
|
+
statusCode?: number;
|
|
15
|
+
responseBody?: string;
|
|
16
|
+
cause?: unknown;
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
export declare class PelotonResponseError extends Error {
|
|
20
|
+
readonly source: string;
|
|
21
|
+
constructor(source: string, cause: unknown);
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;gBAElB,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY;CAM7E;AAED,qBAAa,0BAA2B,SAAQ,mBAAmB;gBACrD,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY;CAI7E;AAED,qBAAa,oBAAqB,SAAQ,KAAK;IAC7C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;gBAG7B,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE;QAAE,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAQ5E;AAED,qBAAa,oBAAqB,SAAQ,KAAK;IAC7C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;gBAEZ,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO;CAK3C"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export class PelotonServiceError extends Error {
|
|
2
|
+
statusCode;
|
|
3
|
+
responseBody;
|
|
4
|
+
constructor(statusCode, responseBody, options) {
|
|
5
|
+
super(`Peloton API error (${statusCode}): ${responseBody}`, options);
|
|
6
|
+
this.name = "PelotonServiceError";
|
|
7
|
+
this.statusCode = statusCode;
|
|
8
|
+
this.responseBody = responseBody;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export class PelotonAuthenticationError extends PelotonServiceError {
|
|
12
|
+
constructor(statusCode, responseBody, options) {
|
|
13
|
+
super(statusCode, responseBody, options);
|
|
14
|
+
this.name = "PelotonAuthenticationError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export class PelotonAuthFlowError extends Error {
|
|
18
|
+
stage;
|
|
19
|
+
statusCode;
|
|
20
|
+
responseBody;
|
|
21
|
+
constructor(stage, message, details) {
|
|
22
|
+
super(message, { cause: details?.cause });
|
|
23
|
+
this.name = "PelotonAuthFlowError";
|
|
24
|
+
this.stage = stage;
|
|
25
|
+
this.statusCode = details?.statusCode;
|
|
26
|
+
this.responseBody = details?.responseBody;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export class PelotonResponseError extends Error {
|
|
30
|
+
source;
|
|
31
|
+
constructor(source, cause) {
|
|
32
|
+
super(`Peloton returned an invalid ${source} response`, { cause });
|
|
33
|
+
this.name = "PelotonResponseError";
|
|
34
|
+
this.source = source;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,mBAAoB,SAAQ,KAAK;IACnC,UAAU,CAAS;IACnB,YAAY,CAAS;IAE9B,YAAY,UAAkB,EAAE,YAAoB,EAAE,OAAsB;QAC1E,KAAK,CAAC,sBAAsB,UAAU,MAAM,YAAY,EAAE,EAAE,OAAO,CAAC,CAAC;QACrE,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAC;QAClC,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;IACnC,CAAC;CACF;AAED,MAAM,OAAO,0BAA2B,SAAQ,mBAAmB;IACjE,YAAY,UAAkB,EAAE,YAAoB,EAAE,OAAsB;QAC1E,KAAK,CAAC,UAAU,EAAE,YAAY,EAAE,OAAO,CAAC,CAAC;QACzC,IAAI,CAAC,IAAI,GAAG,4BAA4B,CAAC;IAC3C,CAAC;CACF;AAED,MAAM,OAAO,oBAAqB,SAAQ,KAAK;IACpC,KAAK,CAAS;IACd,UAAU,CAAU;IACpB,YAAY,CAAU;IAE/B,YACE,KAAa,EACb,OAAe,EACf,OAAyE;QAEzE,KAAK,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;QAC1C,IAAI,CAAC,IAAI,GAAG,sBAAsB,CAAC;QACnC,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,UAAU,GAAG,OAAO,EAAE,UAAU,CAAC;QACtC,IAAI,CAAC,YAAY,GAAG,OAAO,EAAE,YAAY,CAAC;IAC5C,CAAC;CACF;AAED,MAAM,OAAO,oBAAqB,SAAQ,KAAK;IACpC,MAAM,CAAS;IAExB,YAAY,MAAc,EAAE,KAAc;QACxC,KAAK,CAAC,+BAA+B,MAAM,WAAW,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;QACnE,IAAI,CAAC,IAAI,GAAG,sBAAsB,CAAC;QACnC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;CACF"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { CanonicalActivityType } from "@dofek/training/training";
|
|
2
|
+
import type { PelotonPerformanceGraph, PelotonWorkout } from "./types.ts";
|
|
3
|
+
export declare function mapFitnessDiscipline(discipline: string): CanonicalActivityType;
|
|
4
|
+
export interface ParsedPelotonWorkout {
|
|
5
|
+
externalId: string;
|
|
6
|
+
activityType: CanonicalActivityType;
|
|
7
|
+
name?: string;
|
|
8
|
+
timezone?: string;
|
|
9
|
+
stravaId?: string;
|
|
10
|
+
startedAt: Date;
|
|
11
|
+
endedAt?: Date;
|
|
12
|
+
raw: Record<string, unknown>;
|
|
13
|
+
}
|
|
14
|
+
export declare function parseWorkout(workout: PelotonWorkout): ParsedPelotonWorkout;
|
|
15
|
+
export interface ParsedMetricSeries {
|
|
16
|
+
slug: string;
|
|
17
|
+
displayName: string;
|
|
18
|
+
values: number[];
|
|
19
|
+
offsetsSeconds: number[];
|
|
20
|
+
averageValue: number;
|
|
21
|
+
maxValue: number;
|
|
22
|
+
}
|
|
23
|
+
export declare function parsePerformanceGraph(graph: PelotonPerformanceGraph, everyN: number): ParsedMetricSeries[];
|
|
24
|
+
//# 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;AACtE,OAAO,KAAK,EAAE,uBAAuB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAkB1E,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,qBAAqB,CAE9E;AAED,MAAM,WAAW,oBAAoB;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,qBAAqB,CAAC;IACpC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,IAAI,CAAC;IAChB,OAAO,CAAC,EAAE,IAAI,CAAC;IACf,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC9B;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,cAAc,GAAG,oBAAoB,CAiC1E;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,uBAAuB,EAC9B,MAAM,EAAE,MAAM,GACb,kBAAkB,EAAE,CAStB"}
|
package/dist/parsing.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const DISCIPLINE_MAP = {
|
|
2
|
+
cycling: "indoor_cycling",
|
|
3
|
+
running: "running",
|
|
4
|
+
walking: "walking",
|
|
5
|
+
rowing: "rowing",
|
|
6
|
+
caesar: "rowing",
|
|
7
|
+
strength: "strength",
|
|
8
|
+
yoga: "yoga",
|
|
9
|
+
meditation: "meditation",
|
|
10
|
+
stretching: "stretching",
|
|
11
|
+
cardio: "cardio",
|
|
12
|
+
bike_bootcamp: "bootcamp",
|
|
13
|
+
tread_bootcamp: "bootcamp",
|
|
14
|
+
outdoor: "running",
|
|
15
|
+
};
|
|
16
|
+
export function mapFitnessDiscipline(discipline) {
|
|
17
|
+
return DISCIPLINE_MAP[discipline] ?? "other";
|
|
18
|
+
}
|
|
19
|
+
export function parseWorkout(workout) {
|
|
20
|
+
const startedAt = new Date(workout.start_time * 1000);
|
|
21
|
+
const endedAt = workout.end_time > 0 ? new Date(workout.end_time * 1000) : undefined;
|
|
22
|
+
const stravaId = workout.strava_id && workout.strava_id !== "-1" ? workout.strava_id : undefined;
|
|
23
|
+
return {
|
|
24
|
+
externalId: workout.id,
|
|
25
|
+
activityType: mapFitnessDiscipline(workout.fitness_discipline),
|
|
26
|
+
name: workout.ride?.title,
|
|
27
|
+
timezone: workout.timezone || undefined,
|
|
28
|
+
stravaId,
|
|
29
|
+
startedAt,
|
|
30
|
+
endedAt,
|
|
31
|
+
raw: {
|
|
32
|
+
instructor: workout.ride?.instructor?.name,
|
|
33
|
+
classTitle: workout.ride?.title,
|
|
34
|
+
difficultyRating: workout.ride?.difficulty_rating_avg,
|
|
35
|
+
overallRating: workout.ride?.overall_rating_avg,
|
|
36
|
+
rideDescription: workout.ride?.description,
|
|
37
|
+
leaderboardRank: workout.leaderboard_rank,
|
|
38
|
+
totalLeaderboardUsers: workout.total_leaderboard_users,
|
|
39
|
+
totalWorkJoules: workout.total_work || undefined,
|
|
40
|
+
isPersonalRecord: workout.is_total_work_personal_record || undefined,
|
|
41
|
+
fitnessDiscipline: workout.fitness_discipline,
|
|
42
|
+
pelotonRideId: workout.ride?.id,
|
|
43
|
+
deviceType: workout.device_type || undefined,
|
|
44
|
+
platform: workout.platform || undefined,
|
|
45
|
+
pelotonClassId: workout.peloton_id || undefined,
|
|
46
|
+
workoutType: workout.workout_type || undefined,
|
|
47
|
+
hasPedalingMetrics: workout.has_pedaling_metrics,
|
|
48
|
+
timezone: workout.timezone || undefined,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export function parsePerformanceGraph(graph, everyN) {
|
|
53
|
+
return graph.metrics.map((metric) => ({
|
|
54
|
+
slug: metric.slug,
|
|
55
|
+
displayName: metric.display_name,
|
|
56
|
+
values: metric.values,
|
|
57
|
+
offsetsSeconds: metric.values.map((_, index) => index * everyN),
|
|
58
|
+
averageValue: metric.average_value,
|
|
59
|
+
maxValue: metric.max_value,
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
//# 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/types.d.ts
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const pelotonInstructorSchema: z.ZodObject<{
|
|
3
|
+
id: z.ZodString;
|
|
4
|
+
name: z.ZodString;
|
|
5
|
+
image_url: z.ZodOptional<z.ZodString>;
|
|
6
|
+
}, z.core.$strip>;
|
|
7
|
+
export declare const pelotonRideSchema: z.ZodObject<{
|
|
8
|
+
id: z.ZodString;
|
|
9
|
+
title: z.ZodString;
|
|
10
|
+
description: z.ZodOptional<z.ZodString>;
|
|
11
|
+
duration: z.ZodNumber;
|
|
12
|
+
difficulty_rating_avg: z.ZodOptional<z.ZodNumber>;
|
|
13
|
+
overall_rating_avg: z.ZodOptional<z.ZodNumber>;
|
|
14
|
+
instructor: z.ZodOptional<z.ZodObject<{
|
|
15
|
+
id: z.ZodString;
|
|
16
|
+
name: z.ZodString;
|
|
17
|
+
image_url: z.ZodOptional<z.ZodString>;
|
|
18
|
+
}, z.core.$strip>>;
|
|
19
|
+
}, z.core.$strip>;
|
|
20
|
+
export declare const pelotonWorkoutSchema: z.ZodObject<{
|
|
21
|
+
id: z.ZodString;
|
|
22
|
+
status: z.ZodString;
|
|
23
|
+
fitness_discipline: z.ZodString;
|
|
24
|
+
name: z.ZodOptional<z.ZodString>;
|
|
25
|
+
title: z.ZodOptional<z.ZodString>;
|
|
26
|
+
created_at: z.ZodNumber;
|
|
27
|
+
start_time: z.ZodNumber;
|
|
28
|
+
end_time: z.ZodNumber;
|
|
29
|
+
total_work: z.ZodNumber;
|
|
30
|
+
is_total_work_personal_record: z.ZodBoolean;
|
|
31
|
+
metrics_type: z.ZodOptional<z.ZodString>;
|
|
32
|
+
device_type: z.ZodOptional<z.ZodString>;
|
|
33
|
+
platform: z.ZodOptional<z.ZodString>;
|
|
34
|
+
peloton_id: z.ZodOptional<z.ZodString>;
|
|
35
|
+
workout_type: z.ZodOptional<z.ZodString>;
|
|
36
|
+
has_pedaling_metrics: z.ZodOptional<z.ZodBoolean>;
|
|
37
|
+
has_leaderboard_metrics: z.ZodOptional<z.ZodBoolean>;
|
|
38
|
+
timezone: z.ZodOptional<z.ZodString>;
|
|
39
|
+
strava_id: z.ZodOptional<z.ZodString>;
|
|
40
|
+
ride: z.ZodOptional<z.ZodObject<{
|
|
41
|
+
id: z.ZodString;
|
|
42
|
+
title: z.ZodString;
|
|
43
|
+
description: z.ZodOptional<z.ZodString>;
|
|
44
|
+
duration: z.ZodNumber;
|
|
45
|
+
difficulty_rating_avg: z.ZodOptional<z.ZodNumber>;
|
|
46
|
+
overall_rating_avg: z.ZodOptional<z.ZodNumber>;
|
|
47
|
+
instructor: z.ZodOptional<z.ZodObject<{
|
|
48
|
+
id: z.ZodString;
|
|
49
|
+
name: z.ZodString;
|
|
50
|
+
image_url: z.ZodOptional<z.ZodString>;
|
|
51
|
+
}, z.core.$strip>>;
|
|
52
|
+
}, z.core.$strip>>;
|
|
53
|
+
total_leaderboard_users: z.ZodOptional<z.ZodNumber>;
|
|
54
|
+
leaderboard_rank: z.ZodOptional<z.ZodNumber>;
|
|
55
|
+
average_effort_score: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
56
|
+
}, z.core.$strip>;
|
|
57
|
+
export declare const pelotonWorkoutListResponseSchema: z.ZodObject<{
|
|
58
|
+
data: z.ZodArray<z.ZodObject<{
|
|
59
|
+
id: z.ZodString;
|
|
60
|
+
status: z.ZodString;
|
|
61
|
+
fitness_discipline: z.ZodString;
|
|
62
|
+
name: z.ZodOptional<z.ZodString>;
|
|
63
|
+
title: z.ZodOptional<z.ZodString>;
|
|
64
|
+
created_at: z.ZodNumber;
|
|
65
|
+
start_time: z.ZodNumber;
|
|
66
|
+
end_time: z.ZodNumber;
|
|
67
|
+
total_work: z.ZodNumber;
|
|
68
|
+
is_total_work_personal_record: z.ZodBoolean;
|
|
69
|
+
metrics_type: z.ZodOptional<z.ZodString>;
|
|
70
|
+
device_type: z.ZodOptional<z.ZodString>;
|
|
71
|
+
platform: z.ZodOptional<z.ZodString>;
|
|
72
|
+
peloton_id: z.ZodOptional<z.ZodString>;
|
|
73
|
+
workout_type: z.ZodOptional<z.ZodString>;
|
|
74
|
+
has_pedaling_metrics: z.ZodOptional<z.ZodBoolean>;
|
|
75
|
+
has_leaderboard_metrics: z.ZodOptional<z.ZodBoolean>;
|
|
76
|
+
timezone: z.ZodOptional<z.ZodString>;
|
|
77
|
+
strava_id: z.ZodOptional<z.ZodString>;
|
|
78
|
+
ride: z.ZodOptional<z.ZodObject<{
|
|
79
|
+
id: z.ZodString;
|
|
80
|
+
title: z.ZodString;
|
|
81
|
+
description: z.ZodOptional<z.ZodString>;
|
|
82
|
+
duration: z.ZodNumber;
|
|
83
|
+
difficulty_rating_avg: z.ZodOptional<z.ZodNumber>;
|
|
84
|
+
overall_rating_avg: z.ZodOptional<z.ZodNumber>;
|
|
85
|
+
instructor: z.ZodOptional<z.ZodObject<{
|
|
86
|
+
id: z.ZodString;
|
|
87
|
+
name: z.ZodString;
|
|
88
|
+
image_url: z.ZodOptional<z.ZodString>;
|
|
89
|
+
}, z.core.$strip>>;
|
|
90
|
+
}, z.core.$strip>>;
|
|
91
|
+
total_leaderboard_users: z.ZodOptional<z.ZodNumber>;
|
|
92
|
+
leaderboard_rank: z.ZodOptional<z.ZodNumber>;
|
|
93
|
+
average_effort_score: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
94
|
+
}, z.core.$strip>>;
|
|
95
|
+
total: z.ZodNumber;
|
|
96
|
+
count: z.ZodNumber;
|
|
97
|
+
page: z.ZodNumber;
|
|
98
|
+
limit: z.ZodNumber;
|
|
99
|
+
page_count: z.ZodNumber;
|
|
100
|
+
sort_by: z.ZodString;
|
|
101
|
+
show_next: z.ZodBoolean;
|
|
102
|
+
show_previous: z.ZodBoolean;
|
|
103
|
+
}, z.core.$strip>;
|
|
104
|
+
export declare const pelotonMetricSchema: z.ZodObject<{
|
|
105
|
+
display_name: z.ZodString;
|
|
106
|
+
slug: z.ZodString;
|
|
107
|
+
values: z.ZodArray<z.ZodNumber>;
|
|
108
|
+
average_value: z.ZodNumber;
|
|
109
|
+
max_value: z.ZodNumber;
|
|
110
|
+
}, z.core.$strip>;
|
|
111
|
+
export declare const pelotonPerformanceGraphSchema: z.ZodObject<{
|
|
112
|
+
duration: z.ZodNumber;
|
|
113
|
+
is_class_plan_shown: z.ZodBoolean;
|
|
114
|
+
segment_list: z.ZodArray<z.ZodUnknown>;
|
|
115
|
+
average_summaries: z.ZodArray<z.ZodObject<{
|
|
116
|
+
display_name: z.ZodString;
|
|
117
|
+
value: z.ZodString;
|
|
118
|
+
slug: z.ZodString;
|
|
119
|
+
}, z.core.$strip>>;
|
|
120
|
+
summaries: z.ZodArray<z.ZodObject<{
|
|
121
|
+
display_name: z.ZodString;
|
|
122
|
+
value: z.ZodString;
|
|
123
|
+
slug: z.ZodString;
|
|
124
|
+
}, z.core.$strip>>;
|
|
125
|
+
metrics: z.ZodArray<z.ZodObject<{
|
|
126
|
+
display_name: z.ZodString;
|
|
127
|
+
slug: z.ZodString;
|
|
128
|
+
values: z.ZodArray<z.ZodNumber>;
|
|
129
|
+
average_value: z.ZodNumber;
|
|
130
|
+
max_value: z.ZodNumber;
|
|
131
|
+
}, z.core.$strip>>;
|
|
132
|
+
}, z.core.$strip>;
|
|
133
|
+
export type PelotonInstructor = z.infer<typeof pelotonInstructorSchema>;
|
|
134
|
+
export type PelotonRide = z.infer<typeof pelotonRideSchema>;
|
|
135
|
+
export type PelotonWorkout = z.infer<typeof pelotonWorkoutSchema>;
|
|
136
|
+
export type PelotonWorkoutListResponse = z.infer<typeof pelotonWorkoutListResponseSchema>;
|
|
137
|
+
export type PelotonMetric = z.infer<typeof pelotonMetricSchema>;
|
|
138
|
+
export type PelotonPerformanceGraph = z.infer<typeof pelotonPerformanceGraphSchema>;
|
|
139
|
+
export interface PelotonOAuthConfig {
|
|
140
|
+
clientId: string;
|
|
141
|
+
authorizeUrl: string;
|
|
142
|
+
tokenUrl: string;
|
|
143
|
+
redirectUri: string;
|
|
144
|
+
scopes: string[];
|
|
145
|
+
usePkce: true;
|
|
146
|
+
audience: string;
|
|
147
|
+
}
|
|
148
|
+
export interface PelotonTokenSet {
|
|
149
|
+
accessToken: string;
|
|
150
|
+
refreshToken: string | null;
|
|
151
|
+
expiresAt: Date;
|
|
152
|
+
scopes: string | null;
|
|
153
|
+
}
|
|
154
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,uBAAuB;;;;iBAIlC,CAAC;AAEH,eAAO,MAAM,iBAAiB;;;;;;;;;;;;iBAQ5B,CAAC;AAEH,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAwB/B,CAAC;AAEH,eAAO,MAAM,gCAAgC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAU3C,CAAC;AAEH,eAAO,MAAM,mBAAmB;;;;;;iBAM9B,CAAC;AAQH,eAAO,MAAM,6BAA6B;;;;;;;;;;;;;;;;;;;;;iBAOxC,CAAC;AAEH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AACxE,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAC5D,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAClE,MAAM,MAAM,0BAA0B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gCAAgC,CAAC,CAAC;AAC1F,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAChE,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,6BAA6B,CAAC,CAAC;AAEpF,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,OAAO,EAAE,IAAI,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,SAAS,EAAE,IAAI,CAAC;IAChB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const pelotonInstructorSchema = z.object({
|
|
3
|
+
id: z.string(),
|
|
4
|
+
name: z.string(),
|
|
5
|
+
image_url: z.string().optional(),
|
|
6
|
+
});
|
|
7
|
+
export const pelotonRideSchema = z.object({
|
|
8
|
+
id: z.string(),
|
|
9
|
+
title: z.string(),
|
|
10
|
+
description: z.string().optional(),
|
|
11
|
+
duration: z.number(),
|
|
12
|
+
difficulty_rating_avg: z.number().optional(),
|
|
13
|
+
overall_rating_avg: z.number().optional(),
|
|
14
|
+
instructor: pelotonInstructorSchema.optional(),
|
|
15
|
+
});
|
|
16
|
+
export const pelotonWorkoutSchema = z.object({
|
|
17
|
+
id: z.string(),
|
|
18
|
+
status: z.string(),
|
|
19
|
+
fitness_discipline: z.string(),
|
|
20
|
+
name: z.string().optional(),
|
|
21
|
+
title: z.string().optional(),
|
|
22
|
+
created_at: z.number(),
|
|
23
|
+
start_time: z.number(),
|
|
24
|
+
end_time: z.number(),
|
|
25
|
+
total_work: z.number(),
|
|
26
|
+
is_total_work_personal_record: z.boolean(),
|
|
27
|
+
metrics_type: z.string().optional(),
|
|
28
|
+
device_type: z.string().optional(),
|
|
29
|
+
platform: z.string().optional(),
|
|
30
|
+
peloton_id: z.string().optional(),
|
|
31
|
+
workout_type: z.string().optional(),
|
|
32
|
+
has_pedaling_metrics: z.boolean().optional(),
|
|
33
|
+
has_leaderboard_metrics: z.boolean().optional(),
|
|
34
|
+
timezone: z.string().optional(),
|
|
35
|
+
strava_id: z.string().optional(),
|
|
36
|
+
ride: pelotonRideSchema.optional(),
|
|
37
|
+
total_leaderboard_users: z.number().optional(),
|
|
38
|
+
leaderboard_rank: z.number().optional(),
|
|
39
|
+
average_effort_score: z.number().nullable().optional(),
|
|
40
|
+
});
|
|
41
|
+
export const pelotonWorkoutListResponseSchema = z.object({
|
|
42
|
+
data: z.array(pelotonWorkoutSchema),
|
|
43
|
+
total: z.number(),
|
|
44
|
+
count: z.number(),
|
|
45
|
+
page: z.number(),
|
|
46
|
+
limit: z.number(),
|
|
47
|
+
page_count: z.number(),
|
|
48
|
+
sort_by: z.string(),
|
|
49
|
+
show_next: z.boolean(),
|
|
50
|
+
show_previous: z.boolean(),
|
|
51
|
+
});
|
|
52
|
+
export const pelotonMetricSchema = z.object({
|
|
53
|
+
display_name: z.string(),
|
|
54
|
+
slug: z.string(),
|
|
55
|
+
values: z.array(z.number()),
|
|
56
|
+
average_value: z.number(),
|
|
57
|
+
max_value: z.number(),
|
|
58
|
+
});
|
|
59
|
+
const pelotonSummarySchema = z.object({
|
|
60
|
+
display_name: z.string(),
|
|
61
|
+
value: z.string(),
|
|
62
|
+
slug: z.string(),
|
|
63
|
+
});
|
|
64
|
+
export const pelotonPerformanceGraphSchema = z.object({
|
|
65
|
+
duration: z.number(),
|
|
66
|
+
is_class_plan_shown: z.boolean(),
|
|
67
|
+
segment_list: z.array(z.unknown()),
|
|
68
|
+
average_summaries: z.array(pelotonSummarySchema),
|
|
69
|
+
summaries: z.array(pelotonSummarySchema),
|
|
70
|
+
metrics: z.array(pelotonMetricSchema),
|
|
71
|
+
});
|
|
72
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dofek/peloton",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Unofficial Peloton API client with reverse-engineered Auth0 authentication",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/Asherlc/dofek.git",
|
|
10
|
+
"directory": "packages/peloton-client"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/Asherlc/dofek/tree/main/packages/peloton-client#readme",
|
|
13
|
+
"bugs": "https://github.com/Asherlc/dofek/issues",
|
|
14
|
+
"keywords": [
|
|
15
|
+
"peloton",
|
|
16
|
+
"fitness",
|
|
17
|
+
"api-client",
|
|
18
|
+
"auth0",
|
|
19
|
+
"oauth",
|
|
20
|
+
"unofficial",
|
|
21
|
+
"typescript"
|
|
22
|
+
],
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=22.14.0"
|
|
25
|
+
},
|
|
26
|
+
"sideEffects": false,
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"README.md",
|
|
30
|
+
"LICENSE"
|
|
31
|
+
],
|
|
32
|
+
"exports": {
|
|
33
|
+
".": {
|
|
34
|
+
"types": "./dist/client.d.ts",
|
|
35
|
+
"import": "./dist/client.js"
|
|
36
|
+
},
|
|
37
|
+
"./*": {
|
|
38
|
+
"types": "./dist/*.d.ts",
|
|
39
|
+
"import": "./dist/*.js"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"test": "vitest run src",
|
|
47
|
+
"test:watch": "vitest --watch src",
|
|
48
|
+
"build": "tsc",
|
|
49
|
+
"prepack": "pnpm run build",
|
|
50
|
+
"lint": "biome check .",
|
|
51
|
+
"typecheck": "tsc --noEmit"
|
|
52
|
+
},
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"@dofek/provider-http": "0.1.0",
|
|
55
|
+
"@dofek/training": "0.1.0",
|
|
56
|
+
"zod": "4.4.3"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@types/node": "22.19.15",
|
|
60
|
+
"typescript": "6.0.3",
|
|
61
|
+
"vitest": "3.2.6"
|
|
62
|
+
},
|
|
63
|
+
"gitHead": "ac95258c753c7e75b57da7ae47426d22b78e8c01"
|
|
64
|
+
}
|