@insuline/whoop-cli 0.1.1
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/README.md +183 -0
- package/dist/api/client.d.ts +18 -0
- package/dist/api/client.d.ts.map +1 -0
- package/dist/api/client.js +96 -0
- package/dist/api/client.js.map +1 -0
- package/dist/api/endpoints.d.ts +10 -0
- package/dist/api/endpoints.d.ts.map +1 -0
- package/dist/api/endpoints.js +10 -0
- package/dist/api/endpoints.js.map +1 -0
- package/dist/auth/oauth.d.ts +9 -0
- package/dist/auth/oauth.d.ts.map +1 -0
- package/dist/auth/oauth.js +122 -0
- package/dist/auth/oauth.js.map +1 -0
- package/dist/auth/server.d.ts +7 -0
- package/dist/auth/server.d.ts.map +1 -0
- package/dist/auth/server.js +53 -0
- package/dist/auth/server.js.map +1 -0
- package/dist/auth/tokens.d.ts +12 -0
- package/dist/auth/tokens.d.ts.map +1 -0
- package/dist/auth/tokens.js +102 -0
- package/dist/auth/tokens.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +191 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/types/whoop.d.ts +159 -0
- package/dist/types/whoop.d.ts.map +1 -0
- package/dist/types/whoop.js +2 -0
- package/dist/types/whoop.js.map +1 -0
- package/dist/utils/analysis.d.ts +30 -0
- package/dist/utils/analysis.d.ts.map +1 -0
- package/dist/utils/analysis.js +231 -0
- package/dist/utils/analysis.js.map +1 -0
- package/dist/utils/date.d.ts +10 -0
- package/dist/utils/date.d.ts.map +1 -0
- package/dist/utils/date.js +38 -0
- package/dist/utils/date.js.map +1 -0
- package/dist/utils/errors.d.ts +14 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +36 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/format.d.ts +5 -0
- package/dist/utils/format.d.ts.map +1 -0
- package/dist/utils/format.js +89 -0
- package/dist/utils/format.js.map +1 -0
- package/package.json +55 -0
package/README.md
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# WHOOP Skill
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/whoop-cli)
|
|
4
|
+
|
|
5
|
+
CLI for fetching WHOOP health data via the WHOOP API v2.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g whoop-cli
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# One-liner health snapshot
|
|
17
|
+
whoop-cli summary
|
|
18
|
+
# Output: 2026-01-05 | Recovery: 52% | HRV: 39ms | RHR: 60 | Sleep: 40% | Strain: 6.7
|
|
19
|
+
|
|
20
|
+
# Human-readable output
|
|
21
|
+
whoop-cli --pretty
|
|
22
|
+
|
|
23
|
+
# JSON output (default)
|
|
24
|
+
whoop-cli
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Setup
|
|
28
|
+
|
|
29
|
+
Before using, you need to configure WHOOP API credentials:
|
|
30
|
+
|
|
31
|
+
1. Register a WHOOP application at [developer.whoop.com](https://developer.whoop.com)
|
|
32
|
+
- Apps with <10 users don't need WHOOP review (immediate use)
|
|
33
|
+
|
|
34
|
+
2. Set environment variables:
|
|
35
|
+
```bash
|
|
36
|
+
export WHOOP_CLIENT_ID=your_client_id
|
|
37
|
+
export WHOOP_CLIENT_SECRET=your_client_secret
|
|
38
|
+
export WHOOP_REDIRECT_URI=https://your-redirect-uri.com/callback
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Or create a `.env` file in your working directory.
|
|
42
|
+
|
|
43
|
+
3. Authenticate:
|
|
44
|
+
```bash
|
|
45
|
+
whoop-cli auth login
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Tokens are stored in `~/.whoop-cli/tokens.json` and auto-refresh when expired.
|
|
49
|
+
|
|
50
|
+
## Usage
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# Fetch all today's data
|
|
54
|
+
whoop-cli
|
|
55
|
+
|
|
56
|
+
# One-liner health snapshot
|
|
57
|
+
whoop-cli summary
|
|
58
|
+
|
|
59
|
+
# Human-readable output
|
|
60
|
+
whoop-cli --pretty
|
|
61
|
+
|
|
62
|
+
# Specific data type
|
|
63
|
+
whoop-cli profile
|
|
64
|
+
whoop-cli body
|
|
65
|
+
whoop-cli sleep
|
|
66
|
+
whoop-cli recovery
|
|
67
|
+
whoop-cli workout
|
|
68
|
+
whoop-cli cycle
|
|
69
|
+
|
|
70
|
+
# Multiple types
|
|
71
|
+
whoop-cli --sleep --recovery --body
|
|
72
|
+
|
|
73
|
+
# Specific date (ISO format)
|
|
74
|
+
whoop-cli --date 2025-01-03
|
|
75
|
+
|
|
76
|
+
# Pagination
|
|
77
|
+
whoop-cli workout --limit 50
|
|
78
|
+
whoop-cli workout --all
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Auth Commands
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
whoop-cli auth login # OAuth flow (opens browser)
|
|
85
|
+
whoop-cli auth status # Check token status
|
|
86
|
+
whoop-cli auth refresh # Refresh access token using refresh token
|
|
87
|
+
whoop-cli auth logout # Clear tokens
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Keeping tokens fresh (recommended for cron/servers)
|
|
91
|
+
|
|
92
|
+
If you run `whoop-cli` from cron/systemd, you may occasionally see authentication failures if a token refresh is missed or the token file becomes stale.
|
|
93
|
+
|
|
94
|
+
Important:
|
|
95
|
+
- `whoop-cli auth status` **does not refresh tokens** — it only reports whether they’re expired.
|
|
96
|
+
- For automation, you must call `whoop-cli auth refresh` periodically.
|
|
97
|
+
|
|
98
|
+
Recommended pattern:
|
|
99
|
+
- Run `whoop-cli auth login` once interactively (creates `~/.whoop-cli/tokens.json`).
|
|
100
|
+
- Run a small periodic monitor that calls `whoop-cli auth refresh` and performs a lightweight fetch.
|
|
101
|
+
|
|
102
|
+
An example monitor script + systemd timer/cron examples are included here:
|
|
103
|
+
- `examples/monitor/whoop-refresh-monitor.sh`
|
|
104
|
+
- `examples/monitor/systemd/*`
|
|
105
|
+
- `examples/monitor/cron/README-cron.txt`
|
|
106
|
+
|
|
107
|
+
If refresh fails with an expired refresh token, you must re-authenticate:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
whoop-cli auth login
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Data Types
|
|
114
|
+
|
|
115
|
+
| Type | Description |
|
|
116
|
+
|------|-------------|
|
|
117
|
+
| `profile` | User info (name, email) |
|
|
118
|
+
| `body` | Body measurements (height, weight, max HR) |
|
|
119
|
+
| `sleep` | Sleep records with stages, efficiency, respiratory rate |
|
|
120
|
+
| `recovery` | Recovery score, HRV, RHR, SpO2, skin temp |
|
|
121
|
+
| `workout` | Workouts with strain, HR zones, calories |
|
|
122
|
+
| `cycle` | Daily physiological cycle (strain, calories) |
|
|
123
|
+
|
|
124
|
+
## Options
|
|
125
|
+
|
|
126
|
+
| Flag | Description |
|
|
127
|
+
|------|-------------|
|
|
128
|
+
| `-d, --date <date>` | Date in ISO format (YYYY-MM-DD) |
|
|
129
|
+
| `-l, --limit <n>` | Max results per page (default: 25) |
|
|
130
|
+
| `-a, --all` | Fetch all pages |
|
|
131
|
+
| `-p, --pretty` | Human-readable output |
|
|
132
|
+
| `--profile` | Include profile |
|
|
133
|
+
| `--body` | Include body measurements |
|
|
134
|
+
| `--sleep` | Include sleep |
|
|
135
|
+
| `--recovery` | Include recovery |
|
|
136
|
+
| `--workout` | Include workouts |
|
|
137
|
+
| `--cycle` | Include cycle |
|
|
138
|
+
|
|
139
|
+
## Output
|
|
140
|
+
|
|
141
|
+
JSON to stdout by default. Use `--pretty` for human-readable format.
|
|
142
|
+
|
|
143
|
+
```json
|
|
144
|
+
{
|
|
145
|
+
"date": "2025-01-05",
|
|
146
|
+
"fetched_at": "2025-01-05T12:00:00.000Z",
|
|
147
|
+
"profile": { "user_id": 123, "first_name": "John" },
|
|
148
|
+
"body": { "height_meter": 1.83, "weight_kilogram": 82.5, "max_heart_rate": 182 },
|
|
149
|
+
"recovery": [{ "score": { "recovery_score": 52, "hrv_rmssd_milli": 38.9 }}],
|
|
150
|
+
"sleep": [{ "score": { "sleep_performance_percentage": 40 }}],
|
|
151
|
+
"workout": [{ "sport_name": "hiit", "score": { "strain": 6.2 }}],
|
|
152
|
+
"cycle": [{ "score": { "strain": 6.7 }}]
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Exit Codes
|
|
157
|
+
|
|
158
|
+
| Code | Meaning |
|
|
159
|
+
|------|---------|
|
|
160
|
+
| 0 | Success |
|
|
161
|
+
| 1 | General error |
|
|
162
|
+
| 2 | Authentication error |
|
|
163
|
+
| 3 | Rate limit exceeded |
|
|
164
|
+
| 4 | Network error |
|
|
165
|
+
|
|
166
|
+
## Requirements
|
|
167
|
+
|
|
168
|
+
- Node.js 22+
|
|
169
|
+
- WHOOP membership with API access
|
|
170
|
+
|
|
171
|
+
## Development
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
git clone https://github.com/insulineru/whoop-cli.git
|
|
175
|
+
cd whoop-cli
|
|
176
|
+
npm install
|
|
177
|
+
npm run dev # Run with tsx
|
|
178
|
+
npm run build # Compile TypeScript
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## License
|
|
182
|
+
|
|
183
|
+
MIT
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { WhoopProfile, WhoopBody, WhoopSleep, WhoopRecovery, WhoopWorkout, WhoopCycle, QueryParams, CombinedOutput, DataType } from '../types/whoop.js';
|
|
2
|
+
export declare function getProfile(): Promise<WhoopProfile>;
|
|
3
|
+
export declare function getBody(): Promise<WhoopBody>;
|
|
4
|
+
export declare function getSleep(params?: QueryParams, all?: boolean): Promise<WhoopSleep[]>;
|
|
5
|
+
export declare function getRecovery(params?: QueryParams, all?: boolean): Promise<WhoopRecovery[]>;
|
|
6
|
+
export declare function getWorkout(params?: QueryParams, all?: boolean): Promise<WhoopWorkout[]>;
|
|
7
|
+
export declare function getCycle(params?: QueryParams, all?: boolean): Promise<WhoopCycle[]>;
|
|
8
|
+
export declare function fetchData(types: DataType[], date: string, options?: {
|
|
9
|
+
limit?: number;
|
|
10
|
+
all?: boolean;
|
|
11
|
+
start?: string;
|
|
12
|
+
end?: string;
|
|
13
|
+
}): Promise<CombinedOutput>;
|
|
14
|
+
export declare function fetchAllTypes(date: string, options?: {
|
|
15
|
+
limit?: number;
|
|
16
|
+
all?: boolean;
|
|
17
|
+
}): Promise<CombinedOutput>;
|
|
18
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,YAAY,EACZ,SAAS,EACT,UAAU,EACV,aAAa,EACb,YAAY,EACZ,UAAU,EAEV,WAAW,EACX,cAAc,EACd,QAAQ,EACT,MAAM,mBAAmB,CAAC;AAiD3B,wBAAsB,UAAU,IAAI,OAAO,CAAC,YAAY,CAAC,CAExD;AAED,wBAAsB,OAAO,IAAI,OAAO,CAAC,SAAS,CAAC,CAElD;AAED,wBAAsB,QAAQ,CAAC,MAAM,GAAE,WAAgB,EAAE,GAAG,UAAQ,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAE3F;AAED,wBAAsB,WAAW,CAAC,MAAM,GAAE,WAAgB,EAAE,GAAG,UAAQ,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CAEjG;AAED,wBAAsB,UAAU,CAAC,MAAM,GAAE,WAAgB,EAAE,GAAG,UAAQ,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAE/F;AAED,wBAAsB,QAAQ,CAAC,MAAM,GAAE,WAAgB,EAAE,GAAG,UAAQ,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAE3F;AAED,wBAAsB,SAAS,CAC7B,KAAK,EAAE,QAAQ,EAAE,EACjB,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAO,GAC5E,OAAO,CAAC,cAAc,CAAC,CAmCzB;AAED,wBAAsB,aAAa,CACjC,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,OAAO,CAAA;CAAO,GAC9C,OAAO,CAAC,cAAc,CAAC,CAEzB"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { getValidTokens } from '../auth/tokens.js';
|
|
2
|
+
import { BASE_URL, ENDPOINTS } from './endpoints.js';
|
|
3
|
+
import { WhoopError, ExitCode } from '../utils/errors.js';
|
|
4
|
+
import { getDateRange, nowISO } from '../utils/date.js';
|
|
5
|
+
async function request(endpoint, params) {
|
|
6
|
+
const tokens = await getValidTokens();
|
|
7
|
+
const url = new URL(BASE_URL + endpoint);
|
|
8
|
+
if (params?.start)
|
|
9
|
+
url.searchParams.set('start', params.start);
|
|
10
|
+
if (params?.end)
|
|
11
|
+
url.searchParams.set('end', params.end);
|
|
12
|
+
if (params?.limit)
|
|
13
|
+
url.searchParams.set('limit', String(params.limit));
|
|
14
|
+
if (params?.nextToken)
|
|
15
|
+
url.searchParams.set('nextToken', params.nextToken);
|
|
16
|
+
const response = await fetch(url.toString(), {
|
|
17
|
+
headers: {
|
|
18
|
+
Authorization: `Bearer ${tokens.access_token}`,
|
|
19
|
+
'Content-Type': 'application/json',
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
if (!response.ok) {
|
|
23
|
+
if (response.status === 401) {
|
|
24
|
+
throw new WhoopError('Authentication failed', ExitCode.AUTH_ERROR, 401);
|
|
25
|
+
}
|
|
26
|
+
if (response.status === 429) {
|
|
27
|
+
throw new WhoopError('Rate limit exceeded', ExitCode.RATE_LIMIT, 429);
|
|
28
|
+
}
|
|
29
|
+
throw new WhoopError(`API request failed`, ExitCode.GENERAL_ERROR, response.status);
|
|
30
|
+
}
|
|
31
|
+
return response.json();
|
|
32
|
+
}
|
|
33
|
+
async function fetchAll(endpoint, params, fetchAllPages) {
|
|
34
|
+
const results = [];
|
|
35
|
+
let nextToken;
|
|
36
|
+
do {
|
|
37
|
+
const response = await request(endpoint, { ...params, nextToken });
|
|
38
|
+
results.push(...response.records);
|
|
39
|
+
nextToken = fetchAllPages ? response.next_token : undefined;
|
|
40
|
+
} while (nextToken);
|
|
41
|
+
return results;
|
|
42
|
+
}
|
|
43
|
+
export async function getProfile() {
|
|
44
|
+
return request(ENDPOINTS.profile);
|
|
45
|
+
}
|
|
46
|
+
export async function getBody() {
|
|
47
|
+
return request(ENDPOINTS.body);
|
|
48
|
+
}
|
|
49
|
+
export async function getSleep(params = {}, all = false) {
|
|
50
|
+
return fetchAll(ENDPOINTS.sleep, { limit: 25, ...params }, all);
|
|
51
|
+
}
|
|
52
|
+
export async function getRecovery(params = {}, all = false) {
|
|
53
|
+
return fetchAll(ENDPOINTS.recovery, { limit: 25, ...params }, all);
|
|
54
|
+
}
|
|
55
|
+
export async function getWorkout(params = {}, all = false) {
|
|
56
|
+
return fetchAll(ENDPOINTS.workout, { limit: 25, ...params }, all);
|
|
57
|
+
}
|
|
58
|
+
export async function getCycle(params = {}, all = false) {
|
|
59
|
+
return fetchAll(ENDPOINTS.cycle, { limit: 25, ...params }, all);
|
|
60
|
+
}
|
|
61
|
+
export async function fetchData(types, date, options = {}) {
|
|
62
|
+
const { start, end } = options.start && options.end
|
|
63
|
+
? { start: options.start, end: options.end }
|
|
64
|
+
: getDateRange(date);
|
|
65
|
+
const params = { start, end, limit: options.limit };
|
|
66
|
+
const output = {
|
|
67
|
+
date,
|
|
68
|
+
fetched_at: nowISO(),
|
|
69
|
+
};
|
|
70
|
+
const fetchers = {
|
|
71
|
+
profile: async () => {
|
|
72
|
+
output.profile = await getProfile();
|
|
73
|
+
},
|
|
74
|
+
body: async () => {
|
|
75
|
+
output.body = await getBody();
|
|
76
|
+
},
|
|
77
|
+
sleep: async () => {
|
|
78
|
+
output.sleep = await getSleep(params, options.all);
|
|
79
|
+
},
|
|
80
|
+
recovery: async () => {
|
|
81
|
+
output.recovery = await getRecovery(params, options.all);
|
|
82
|
+
},
|
|
83
|
+
workout: async () => {
|
|
84
|
+
output.workout = await getWorkout(params, options.all);
|
|
85
|
+
},
|
|
86
|
+
cycle: async () => {
|
|
87
|
+
output.cycle = await getCycle(params, options.all);
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
await Promise.all(types.map((type) => fetchers[type]()));
|
|
91
|
+
return output;
|
|
92
|
+
}
|
|
93
|
+
export async function fetchAllTypes(date, options = {}) {
|
|
94
|
+
return fetchData(['profile', 'body', 'sleep', 'recovery', 'workout', 'cycle'], date, options);
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const BASE_URL = "https://api.prod.whoop.com/developer/v2";
|
|
2
|
+
export declare const ENDPOINTS: {
|
|
3
|
+
readonly profile: "/user/profile/basic";
|
|
4
|
+
readonly body: "/user/measurement/body";
|
|
5
|
+
readonly workout: "/activity/workout";
|
|
6
|
+
readonly sleep: "/activity/sleep";
|
|
7
|
+
readonly recovery: "/recovery";
|
|
8
|
+
readonly cycle: "/cycle";
|
|
9
|
+
};
|
|
10
|
+
//# sourceMappingURL=endpoints.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"endpoints.d.ts","sourceRoot":"","sources":["../../src/api/endpoints.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,QAAQ,4CAA4C,CAAC;AAElE,eAAO,MAAM,SAAS;;;;;;;CAOZ,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const BASE_URL = 'https://api.prod.whoop.com/developer/v2';
|
|
2
|
+
export const ENDPOINTS = {
|
|
3
|
+
profile: '/user/profile/basic',
|
|
4
|
+
body: '/user/measurement/body',
|
|
5
|
+
workout: '/activity/workout',
|
|
6
|
+
sleep: '/activity/sleep',
|
|
7
|
+
recovery: '/recovery',
|
|
8
|
+
cycle: '/cycle',
|
|
9
|
+
};
|
|
10
|
+
//# sourceMappingURL=endpoints.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"endpoints.js","sourceRoot":"","sources":["../../src/api/endpoints.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,QAAQ,GAAG,yCAAyC,CAAC;AAElE,MAAM,CAAC,MAAM,SAAS,GAAG;IACvB,OAAO,EAAE,qBAAqB;IAC9B,IAAI,EAAE,wBAAwB;IAC9B,OAAO,EAAE,mBAAmB;IAC5B,KAAK,EAAE,iBAAiB;IACxB,QAAQ,EAAE,WAAW;IACrB,KAAK,EAAE,QAAQ;CACP,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare function login(): Promise<void>;
|
|
2
|
+
export declare function logout(): void;
|
|
3
|
+
export declare function status(): void;
|
|
4
|
+
/**
|
|
5
|
+
* Proactively refresh the access token.
|
|
6
|
+
* Use this in cron jobs to keep tokens fresh.
|
|
7
|
+
*/
|
|
8
|
+
export declare function refresh(): Promise<void>;
|
|
9
|
+
//# sourceMappingURL=oauth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../../src/auth/oauth.ts"],"names":[],"mappings":"AAoCA,wBAAsB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAsD3C;AAED,wBAAgB,MAAM,IAAI,IAAI,CAG7B;AAED,wBAAgB,MAAM,IAAI,IAAI,CAoB7B;AAED;;;GAGG;AACH,wBAAsB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CA6B7C"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { createInterface } from 'node:readline';
|
|
3
|
+
import open from 'open';
|
|
4
|
+
import { saveTokens, clearTokens, getTokenStatus, getValidTokens, isTokenExpired, loadTokens } from './tokens.js';
|
|
5
|
+
import { WhoopError, ExitCode } from '../utils/errors.js';
|
|
6
|
+
const WHOOP_AUTH_URL = 'https://api.prod.whoop.com/oauth/oauth2/auth';
|
|
7
|
+
const WHOOP_TOKEN_URL = 'https://api.prod.whoop.com/oauth/oauth2/token';
|
|
8
|
+
const SCOPES = 'read:profile read:body_measurement read:workout read:recovery read:sleep read:cycles offline';
|
|
9
|
+
function getCredentials() {
|
|
10
|
+
const clientId = process.env.WHOOP_CLIENT_ID;
|
|
11
|
+
const clientSecret = process.env.WHOOP_CLIENT_SECRET;
|
|
12
|
+
const redirectUri = process.env.WHOOP_REDIRECT_URI;
|
|
13
|
+
if (!clientId || !clientSecret || !redirectUri) {
|
|
14
|
+
throw new WhoopError('Missing WHOOP_CLIENT_ID, WHOOP_CLIENT_SECRET, or WHOOP_REDIRECT_URI in environment', ExitCode.AUTH_ERROR);
|
|
15
|
+
}
|
|
16
|
+
return { clientId, clientSecret, redirectUri };
|
|
17
|
+
}
|
|
18
|
+
function prompt(question) {
|
|
19
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
20
|
+
return new Promise((resolve) => {
|
|
21
|
+
rl.question(question, (answer) => {
|
|
22
|
+
rl.close();
|
|
23
|
+
resolve(answer.trim());
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
export async function login() {
|
|
28
|
+
const { clientId, clientSecret, redirectUri } = getCredentials();
|
|
29
|
+
const state = randomBytes(16).toString('hex');
|
|
30
|
+
const authUrl = new URL(WHOOP_AUTH_URL);
|
|
31
|
+
authUrl.searchParams.set('client_id', clientId);
|
|
32
|
+
authUrl.searchParams.set('redirect_uri', redirectUri);
|
|
33
|
+
authUrl.searchParams.set('response_type', 'code');
|
|
34
|
+
authUrl.searchParams.set('scope', SCOPES);
|
|
35
|
+
authUrl.searchParams.set('state', state);
|
|
36
|
+
console.log('Opening browser for authorization...');
|
|
37
|
+
console.log('\nIf browser does not open, visit this URL:\n');
|
|
38
|
+
console.log(authUrl.toString());
|
|
39
|
+
console.log('');
|
|
40
|
+
await open(authUrl.toString()).catch(() => { });
|
|
41
|
+
const callbackUrl = await prompt('Paste the callback URL here: ');
|
|
42
|
+
const url = new URL(callbackUrl);
|
|
43
|
+
const code = url.searchParams.get('code');
|
|
44
|
+
const returnedState = url.searchParams.get('state');
|
|
45
|
+
if (!code) {
|
|
46
|
+
throw new WhoopError('No authorization code in callback URL', ExitCode.AUTH_ERROR);
|
|
47
|
+
}
|
|
48
|
+
if (returnedState !== state) {
|
|
49
|
+
throw new WhoopError('OAuth state mismatch', ExitCode.AUTH_ERROR);
|
|
50
|
+
}
|
|
51
|
+
const tokenResponse = await fetch(WHOOP_TOKEN_URL, {
|
|
52
|
+
method: 'POST',
|
|
53
|
+
headers: {
|
|
54
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
55
|
+
},
|
|
56
|
+
body: new URLSearchParams({
|
|
57
|
+
grant_type: 'authorization_code',
|
|
58
|
+
code,
|
|
59
|
+
redirect_uri: redirectUri,
|
|
60
|
+
client_id: clientId,
|
|
61
|
+
client_secret: clientSecret,
|
|
62
|
+
}),
|
|
63
|
+
});
|
|
64
|
+
if (!tokenResponse.ok) {
|
|
65
|
+
const text = await tokenResponse.text();
|
|
66
|
+
throw new WhoopError(`Token exchange failed: ${text}`, ExitCode.AUTH_ERROR, tokenResponse.status);
|
|
67
|
+
}
|
|
68
|
+
const tokens = (await tokenResponse.json());
|
|
69
|
+
saveTokens(tokens);
|
|
70
|
+
console.log('Authentication successful');
|
|
71
|
+
}
|
|
72
|
+
export function logout() {
|
|
73
|
+
clearTokens();
|
|
74
|
+
console.log('Logged out');
|
|
75
|
+
}
|
|
76
|
+
export function status() {
|
|
77
|
+
const tokenStatus = getTokenStatus();
|
|
78
|
+
const tokens = loadTokens();
|
|
79
|
+
if (!tokenStatus.authenticated) {
|
|
80
|
+
console.log(JSON.stringify({ authenticated: false, message: 'Not logged in. Run: whoop-cli auth login' }, null, 2));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const now = Math.floor(Date.now() / 1000);
|
|
84
|
+
const expiresIn = tokenStatus.expires_at - now;
|
|
85
|
+
const needsRefresh = isTokenExpired(tokens);
|
|
86
|
+
console.log(JSON.stringify({
|
|
87
|
+
authenticated: true,
|
|
88
|
+
expires_at: tokenStatus.expires_at,
|
|
89
|
+
expires_in_seconds: expiresIn,
|
|
90
|
+
expires_in_human: expiresIn > 0 ? `${Math.floor(expiresIn / 60)} minutes` : 'EXPIRED',
|
|
91
|
+
needs_refresh: needsRefresh,
|
|
92
|
+
}, null, 2));
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Proactively refresh the access token.
|
|
96
|
+
* Use this in cron jobs to keep tokens fresh.
|
|
97
|
+
*/
|
|
98
|
+
export async function refresh() {
|
|
99
|
+
const tokens = loadTokens();
|
|
100
|
+
if (!tokens) {
|
|
101
|
+
throw new WhoopError('Not authenticated. Run: whoop-cli auth login', ExitCode.AUTH_ERROR);
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
const newTokens = await getValidTokens();
|
|
105
|
+
const now = Math.floor(Date.now() / 1000);
|
|
106
|
+
const expiresIn = newTokens.expires_at - now;
|
|
107
|
+
console.log(JSON.stringify({
|
|
108
|
+
success: true,
|
|
109
|
+
message: 'Token refreshed successfully',
|
|
110
|
+
expires_at: newTokens.expires_at,
|
|
111
|
+
expires_in_seconds: expiresIn,
|
|
112
|
+
expires_in_human: `${Math.floor(expiresIn / 60)} minutes`,
|
|
113
|
+
}, null, 2));
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
if (error instanceof WhoopError && error.message.includes('refresh')) {
|
|
117
|
+
throw new WhoopError('Refresh token expired. Please re-authenticate with: whoop-cli auth login', ExitCode.AUTH_ERROR);
|
|
118
|
+
}
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=oauth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth.js","sourceRoot":"","sources":["../../src/auth/oauth.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface CallbackResult {
|
|
2
|
+
code: string;
|
|
3
|
+
state: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function findAvailablePort(startPort?: number): Promise<number>;
|
|
6
|
+
export declare function startCallbackServer(port: number): Promise<CallbackResult>;
|
|
7
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/auth/server.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED,wBAAsB,iBAAiB,CAAC,SAAS,GAAE,MAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAUjF;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CA8CzE"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
import { URL } from 'node:url';
|
|
3
|
+
export async function findAvailablePort(startPort = 3000) {
|
|
4
|
+
return new Promise((resolve) => {
|
|
5
|
+
const server = createServer();
|
|
6
|
+
server.listen(startPort, () => {
|
|
7
|
+
server.close(() => resolve(startPort));
|
|
8
|
+
});
|
|
9
|
+
server.on('error', () => {
|
|
10
|
+
resolve(findAvailablePort(startPort + 1));
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
export function startCallbackServer(port) {
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
let server;
|
|
17
|
+
const timeout = setTimeout(() => {
|
|
18
|
+
server?.close();
|
|
19
|
+
reject(new Error('OAuth callback timeout'));
|
|
20
|
+
}, 120000);
|
|
21
|
+
server = createServer((req, res) => {
|
|
22
|
+
const url = new URL(req.url || '/', `http://localhost:${port}`);
|
|
23
|
+
if (url.pathname === '/callback') {
|
|
24
|
+
const code = url.searchParams.get('code');
|
|
25
|
+
const state = url.searchParams.get('state');
|
|
26
|
+
const error = url.searchParams.get('error');
|
|
27
|
+
if (error) {
|
|
28
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
29
|
+
res.end('<h1>Authorization Failed</h1><p>You can close this window.</p>');
|
|
30
|
+
clearTimeout(timeout);
|
|
31
|
+
server.close();
|
|
32
|
+
reject(new Error(`OAuth error: ${error}`));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (code && state) {
|
|
36
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
37
|
+
res.end('<h1>Authorization Successful</h1><p>You can close this window.</p>');
|
|
38
|
+
clearTimeout(timeout);
|
|
39
|
+
server.close();
|
|
40
|
+
resolve({ code, state });
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
44
|
+
res.end('<h1>Missing Parameters</h1>');
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
res.writeHead(404);
|
|
48
|
+
res.end('Not Found');
|
|
49
|
+
});
|
|
50
|
+
server.listen(port);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/auth/server.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { TokenData, OAuthTokenResponse } from '../types/whoop.js';
|
|
2
|
+
export declare function saveTokens(response: OAuthTokenResponse): void;
|
|
3
|
+
export declare function loadTokens(): TokenData | null;
|
|
4
|
+
export declare function clearTokens(): void;
|
|
5
|
+
export declare function isTokenExpired(tokens: TokenData): boolean;
|
|
6
|
+
export declare function refreshAccessToken(tokens: TokenData): Promise<TokenData>;
|
|
7
|
+
export declare function getValidTokens(): Promise<TokenData>;
|
|
8
|
+
export declare function getTokenStatus(): {
|
|
9
|
+
authenticated: boolean;
|
|
10
|
+
expires_at?: number;
|
|
11
|
+
};
|
|
12
|
+
//# sourceMappingURL=tokens.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../../src/auth/tokens.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAevE,wBAAgB,UAAU,CAAC,QAAQ,EAAE,kBAAkB,GAAG,IAAI,CAa7D;AAED,wBAAgB,UAAU,IAAI,SAAS,GAAG,IAAI,CAW7C;AAED,wBAAgB,WAAW,IAAI,IAAI,CAIlC;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAGzD;AAED,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC,CAqC9E;AAED,wBAAsB,cAAc,IAAI,OAAO,CAAC,SAAS,CAAC,CAYzD;AAED,wBAAgB,cAAc,IAAI;IAAE,aAAa,EAAE,OAAO,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,CAShF"}
|