whoopper 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 +176 -0
- package/dist/auth/auth-server.d.ts +15 -0
- package/dist/auth/auth-server.d.ts.map +1 -0
- package/dist/auth/auth-server.js +127 -0
- package/dist/auth/auth-server.js.map +1 -0
- package/dist/auth/oauth-provider.d.ts +17 -0
- package/dist/auth/oauth-provider.d.ts.map +1 -0
- package/dist/auth/oauth-provider.js +139 -0
- package/dist/auth/oauth-provider.js.map +1 -0
- package/dist/auth/token-info.d.ts +21 -0
- package/dist/auth/token-info.d.ts.map +1 -0
- package/dist/auth/token-info.js +35 -0
- package/dist/auth/token-info.js.map +1 -0
- package/dist/auth/token-store.d.ts +21 -0
- package/dist/auth/token-store.d.ts.map +1 -0
- package/dist/auth/token-store.js +75 -0
- package/dist/auth/token-store.js.map +1 -0
- package/dist/auth/types.d.ts +42 -0
- package/dist/auth/types.d.ts.map +1 -0
- package/dist/auth/types.js +2 -0
- package/dist/auth/types.js.map +1 -0
- package/dist/client.d.ts +27 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +68 -0
- package/dist/client.js.map +1 -0
- package/dist/errors/auth.d.ts +11 -0
- package/dist/errors/auth.d.ts.map +1 -0
- package/dist/errors/auth.js +20 -0
- package/dist/errors/auth.js.map +1 -0
- package/dist/errors/base.d.ts +5 -0
- package/dist/errors/base.d.ts.map +1 -0
- package/dist/errors/base.js +9 -0
- package/dist/errors/base.js.map +1 -0
- package/dist/errors/config.d.ts +5 -0
- package/dist/errors/config.d.ts.map +1 -0
- package/dist/errors/config.js +8 -0
- package/dist/errors/config.js.map +1 -0
- package/dist/errors/http.d.ts +19 -0
- package/dist/errors/http.d.ts.map +1 -0
- package/dist/errors/http.js +36 -0
- package/dist/errors/http.js.map +1 -0
- package/dist/errors/index.d.ts +5 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +5 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/http/errors.d.ts +3 -0
- package/dist/http/errors.d.ts.map +1 -0
- package/dist/http/errors.js +31 -0
- package/dist/http/errors.js.map +1 -0
- package/dist/http/fetch-client.d.ts +31 -0
- package/dist/http/fetch-client.d.ts.map +1 -0
- package/dist/http/fetch-client.js +76 -0
- package/dist/http/fetch-client.js.map +1 -0
- package/dist/http/retry.d.ts +7 -0
- package/dist/http/retry.d.ts.map +1 -0
- package/dist/http/retry.js +36 -0
- package/dist/http/retry.js.map +1 -0
- package/dist/http/throttle.d.ts +16 -0
- package/dist/http/throttle.d.ts.map +1 -0
- package/dist/http/throttle.js +51 -0
- package/dist/http/throttle.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/models/achievements.d.ts +13 -0
- package/dist/models/achievements.d.ts.map +1 -0
- package/dist/models/achievements.js +2 -0
- package/dist/models/achievements.js.map +1 -0
- package/dist/models/common.d.ts +16 -0
- package/dist/models/common.d.ts.map +1 -0
- package/dist/models/common.js +7 -0
- package/dist/models/common.js.map +1 -0
- package/dist/models/cycle.d.ts +19 -0
- package/dist/models/cycle.d.ts.map +1 -0
- package/dist/models/cycle.js +2 -0
- package/dist/models/cycle.js.map +1 -0
- package/dist/models/heart-rate.d.ts +10 -0
- package/dist/models/heart-rate.d.ts.map +1 -0
- package/dist/models/heart-rate.js +2 -0
- package/dist/models/heart-rate.js.map +1 -0
- package/dist/models/index.d.ts +13 -0
- package/dist/models/index.d.ts.map +1 -0
- package/dist/models/index.js +3 -0
- package/dist/models/index.js.map +1 -0
- package/dist/models/internal.d.ts +78 -0
- package/dist/models/internal.d.ts.map +1 -0
- package/dist/models/internal.js +2 -0
- package/dist/models/internal.js.map +1 -0
- package/dist/models/recovery.d.ts +19 -0
- package/dist/models/recovery.d.ts.map +1 -0
- package/dist/models/recovery.js +2 -0
- package/dist/models/recovery.js.map +1 -0
- package/dist/models/sleep.d.ts +38 -0
- package/dist/models/sleep.d.ts.map +1 -0
- package/dist/models/sleep.js +2 -0
- package/dist/models/sleep.js.map +1 -0
- package/dist/models/sports.d.ts +6 -0
- package/dist/models/sports.d.ts.map +1 -0
- package/dist/models/sports.js +190 -0
- package/dist/models/sports.js.map +1 -0
- package/dist/models/stress.d.ts +6 -0
- package/dist/models/stress.d.ts.map +1 -0
- package/dist/models/stress.js +2 -0
- package/dist/models/stress.js.map +1 -0
- package/dist/models/trends.d.ts +10 -0
- package/dist/models/trends.d.ts.map +1 -0
- package/dist/models/trends.js +2 -0
- package/dist/models/trends.js.map +1 -0
- package/dist/models/user.d.ts +12 -0
- package/dist/models/user.d.ts.map +1 -0
- package/dist/models/user.js +2 -0
- package/dist/models/user.js.map +1 -0
- package/dist/models/workout.d.ts +33 -0
- package/dist/models/workout.d.ts.map +1 -0
- package/dist/models/workout.js +2 -0
- package/dist/models/workout.js.map +1 -0
- package/dist/pagination/paginator.d.ts +11 -0
- package/dist/pagination/paginator.d.ts.map +1 -0
- package/dist/pagination/paginator.js +38 -0
- package/dist/pagination/paginator.js.map +1 -0
- package/dist/resources/base.d.ts +17 -0
- package/dist/resources/base.d.ts.map +1 -0
- package/dist/resources/base.js +37 -0
- package/dist/resources/base.js.map +1 -0
- package/dist/resources/official/cycle.d.ts +10 -0
- package/dist/resources/official/cycle.d.ts.map +1 -0
- package/dist/resources/official/cycle.js +13 -0
- package/dist/resources/official/cycle.js.map +1 -0
- package/dist/resources/official/recovery.d.ts +6 -0
- package/dist/resources/official/recovery.d.ts.map +1 -0
- package/dist/resources/official/recovery.js +7 -0
- package/dist/resources/official/recovery.js.map +1 -0
- package/dist/resources/official/sleep.d.ts +6 -0
- package/dist/resources/official/sleep.d.ts.map +1 -0
- package/dist/resources/official/sleep.js +7 -0
- package/dist/resources/official/sleep.js.map +1 -0
- package/dist/resources/official/user.d.ts +8 -0
- package/dist/resources/official/user.d.ts.map +1 -0
- package/dist/resources/official/user.js +13 -0
- package/dist/resources/official/user.js.map +1 -0
- package/dist/resources/official/workout.d.ts +6 -0
- package/dist/resources/official/workout.d.ts.map +1 -0
- package/dist/resources/official/workout.js +7 -0
- package/dist/resources/official/workout.js.map +1 -0
- package/dist/result/result.d.ts +11 -0
- package/dist/result/result.d.ts.map +1 -0
- package/dist/result/result.js +19 -0
- package/dist/result/result.js.map +1 -0
- package/dist/utils/conversions.d.ts +4 -0
- package/dist/utils/conversions.d.ts.map +1 -0
- package/dist/utils/conversions.js +10 -0
- package/dist/utils/conversions.js.map +1 -0
- package/dist/utils/date.d.ts +6 -0
- package/dist/utils/date.d.ts.map +1 -0
- package/dist/utils/date.js +19 -0
- package/dist/utils/date.js.map +1 -0
- package/dist/utils/headers.d.ts +2 -0
- package/dist/utils/headers.d.ts.map +1 -0
- package/dist/utils/headers.js +8 -0
- package/dist/utils/headers.js.map +1 -0
- package/dist/utils/index.d.ts +5 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +5 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/sleep-helpers.d.ts +7 -0
- package/dist/utils/sleep-helpers.d.ts.map +1 -0
- package/dist/utils/sleep-helpers.js +49 -0
- package/dist/utils/sleep-helpers.js.map +1 -0
- package/package.json +61 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 car1os
|
|
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,176 @@
|
|
|
1
|
+
# whoopper
|
|
2
|
+
|
|
3
|
+
The definitive WHOOP API client. Zero runtime dependencies. Full TypeScript types. Every endpoint.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install whoopper
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires Node.js 18+.
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
### With OAuth (Official API)
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { WhooopperClient } from 'whoopper';
|
|
19
|
+
|
|
20
|
+
const client = WhooopperClient.withOAuth({
|
|
21
|
+
clientId: process.env.WHOOP_CLIENT_ID!,
|
|
22
|
+
clientSecret: process.env.WHOOP_CLIENT_SECRET!,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
await client.authenticate(); // opens browser for OAuth
|
|
26
|
+
|
|
27
|
+
const profile = await client.user.getProfile();
|
|
28
|
+
console.log(`Hello, ${profile.first_name}!`);
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### With Pre-existing Tokens
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
const client = WhooopperClient.withTokens({
|
|
35
|
+
official: {
|
|
36
|
+
accessToken: 'your-access-token',
|
|
37
|
+
refreshToken: 'your-refresh-token',
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const cycles = await client.cycle.getAll({ start: '2024-01-01' });
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Resources
|
|
45
|
+
|
|
46
|
+
All official WHOOP API v2 endpoints are supported:
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
// User
|
|
50
|
+
await client.user.getProfile();
|
|
51
|
+
await client.user.getBodyMeasurement();
|
|
52
|
+
|
|
53
|
+
// Cycles
|
|
54
|
+
await client.cycle.list({ start: '2024-01-01', end: '2024-02-01' });
|
|
55
|
+
await client.cycle.getById(12345);
|
|
56
|
+
await client.cycle.getRecovery(12345);
|
|
57
|
+
await client.cycle.getSleep(12345);
|
|
58
|
+
|
|
59
|
+
// Recovery
|
|
60
|
+
await client.recovery.list({ start: '2024-01-01' });
|
|
61
|
+
|
|
62
|
+
// Sleep
|
|
63
|
+
await client.sleep.list({ start: '2024-01-01' });
|
|
64
|
+
await client.sleep.getById(12345);
|
|
65
|
+
|
|
66
|
+
// Workouts
|
|
67
|
+
await client.workout.list({ start: '2024-01-01' });
|
|
68
|
+
await client.workout.getById(12345);
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Pagination
|
|
72
|
+
|
|
73
|
+
Every collection resource supports four pagination strategies:
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
// Get a single page
|
|
77
|
+
const page = await client.cycle.list({ start: '2024-01-01' });
|
|
78
|
+
|
|
79
|
+
// Get all records (auto-paginates)
|
|
80
|
+
const all = await client.cycle.getAll({ start: '2024-01-01' });
|
|
81
|
+
|
|
82
|
+
// Async iterator (memory-efficient)
|
|
83
|
+
for await (const cycle of client.cycle.iterate({ start: '2024-01-01' })) {
|
|
84
|
+
console.log(cycle.id);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Page-level iterator
|
|
88
|
+
for await (const page of client.cycle.paginator().iteratePages()) {
|
|
89
|
+
console.log(`Got ${page.records.length} records`);
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Token Storage
|
|
94
|
+
|
|
95
|
+
By default, tokens are stored in memory. For persistence across sessions:
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
import { WhooopperClient, FileTokenStore } from 'whoopper';
|
|
99
|
+
|
|
100
|
+
const client = WhooopperClient.withOAuth(
|
|
101
|
+
{ clientId: '...', clientSecret: '...' },
|
|
102
|
+
{ tokenStore: new FileTokenStore('./tokens.json') },
|
|
103
|
+
);
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`FileTokenStore` writes with `0600` permissions and warns if they're looser.
|
|
107
|
+
|
|
108
|
+
You can also implement the `TokenStore` interface for custom storage (Redis, database, etc.).
|
|
109
|
+
|
|
110
|
+
## Utilities
|
|
111
|
+
|
|
112
|
+
Standalone helpers that work on plain API responses:
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
import { kJToCalories, msToHours, totalSleepTime, sleepEfficiency } from 'whoopper/utils';
|
|
116
|
+
|
|
117
|
+
kJToCalories(1000); // 239
|
|
118
|
+
msToHours(27_000_000); // 7.5
|
|
119
|
+
|
|
120
|
+
// Pass a SleepScore from the API
|
|
121
|
+
totalSleepTime(sleep.score); // total ms of actual sleep
|
|
122
|
+
sleepEfficiency(sleep.score); // percentage
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Subpath Exports
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
import { ... } from 'whoopper'; // everything
|
|
129
|
+
import { ... } from 'whoopper/models'; // type interfaces only
|
|
130
|
+
import { ... } from 'whoopper/errors'; // error classes only
|
|
131
|
+
import { ... } from 'whoopper/utils'; // utility functions only
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Error Handling
|
|
135
|
+
|
|
136
|
+
All errors extend `WhoopError`:
|
|
137
|
+
|
|
138
|
+
| HTTP Status | Error Class | Notes |
|
|
139
|
+
|-------------|-------------|-------|
|
|
140
|
+
| 400 | `ValidationError` | Bad request parameters |
|
|
141
|
+
| 401 | `TokenExpiredError` | Token needs refresh |
|
|
142
|
+
| 403 | `AuthenticationError` | Insufficient permissions |
|
|
143
|
+
| 404 | `NotFoundError` | Resource doesn't exist |
|
|
144
|
+
| 429 | `RateLimitError` | Has `.retryAfter` (seconds) |
|
|
145
|
+
| 5xx | `ServerError` | Has `.statusCode` |
|
|
146
|
+
|
|
147
|
+
Retries are automatic for 429 and 5xx (exponential backoff, respects `Retry-After` header, max 5 attempts).
|
|
148
|
+
|
|
149
|
+
### Optional Result Type
|
|
150
|
+
|
|
151
|
+
For those who prefer explicit error handling over try/catch:
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
import { tryCatch } from 'whoopper';
|
|
155
|
+
|
|
156
|
+
const result = await tryCatch(() => client.user.getProfile());
|
|
157
|
+
if (result.ok) {
|
|
158
|
+
console.log(result.value.first_name);
|
|
159
|
+
} else {
|
|
160
|
+
console.error(result.error);
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Configuration
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
WhooopperClient.withOAuth(config, {
|
|
168
|
+
tokenStore: new FileTokenStore('./tokens.json'),
|
|
169
|
+
retry: { maxAttempts: 3, baseDelayMs: 2000 },
|
|
170
|
+
throttle: { maxConcurrent: 5, minDelayMs: 100 },
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## License
|
|
175
|
+
|
|
176
|
+
MIT
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
interface AuthServerResult {
|
|
2
|
+
port: number;
|
|
3
|
+
waitForCode: () => Promise<{
|
|
4
|
+
code: string;
|
|
5
|
+
state: string;
|
|
6
|
+
}>;
|
|
7
|
+
waitForTokens: () => Promise<{
|
|
8
|
+
accessToken: string;
|
|
9
|
+
expiresIn: number;
|
|
10
|
+
}>;
|
|
11
|
+
close: () => void;
|
|
12
|
+
}
|
|
13
|
+
export declare function startAuthServer(): Promise<AuthServerResult>;
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=auth-server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-server.d.ts","sourceRoot":"","sources":["../../src/auth/auth-server.ts"],"names":[],"mappings":"AAEA,UAAU,gBAAgB;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC5D,aAAa,EAAE,MAAM,OAAO,CAAC;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACzE,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AA2DD,wBAAgB,eAAe,IAAI,OAAO,CAAC,gBAAgB,CAAC,CA4E3D"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
const SUCCESS_HTML = `<!DOCTYPE html>
|
|
3
|
+
<html><head><title>Authentication Successful</title></head>
|
|
4
|
+
<body style="font-family:system-ui;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#0a0a0a;color:#fff">
|
|
5
|
+
<div style="text-align:center">
|
|
6
|
+
<h1 style="color:#00d1b2">Authentication Successful</h1>
|
|
7
|
+
<p>You can close this tab and return to the terminal.</p>
|
|
8
|
+
</div></body></html>`;
|
|
9
|
+
const LOGIN_HTML = `<!DOCTYPE html>
|
|
10
|
+
<html><head><title>WHOOP Login</title></head>
|
|
11
|
+
<body style="font-family:system-ui;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#0a0a0a;color:#fff">
|
|
12
|
+
<div style="max-width:400px;width:100%;padding:2rem">
|
|
13
|
+
<h1 style="color:#00d1b2;text-align:center">WHOOP Login</h1>
|
|
14
|
+
<p style="color:#999;text-align:center;font-size:0.9rem">Your password stays in this browser — it is never sent to the library.</p>
|
|
15
|
+
<form id="loginForm" style="display:flex;flex-direction:column;gap:1rem">
|
|
16
|
+
<input name="email" type="email" placeholder="Email" required style="padding:0.75rem;border:1px solid #333;border-radius:8px;background:#1a1a1a;color:#fff;font-size:1rem">
|
|
17
|
+
<input name="password" type="password" placeholder="Password" required style="padding:0.75rem;border:1px solid #333;border-radius:8px;background:#1a1a1a;color:#fff;font-size:1rem">
|
|
18
|
+
<button type="submit" style="padding:0.75rem;border:none;border-radius:8px;background:#00d1b2;color:#000;font-size:1rem;font-weight:bold;cursor:pointer">Log In</button>
|
|
19
|
+
<div id="error" style="color:#ff6b6b;text-align:center;display:none"></div>
|
|
20
|
+
</form>
|
|
21
|
+
</div>
|
|
22
|
+
<script>
|
|
23
|
+
const COGNITO_URL = 'https://api.prod.whoop.com/auth-service/v3/whoop';
|
|
24
|
+
document.getElementById('loginForm').addEventListener('submit', async (e) => {
|
|
25
|
+
e.preventDefault();
|
|
26
|
+
const form = e.target;
|
|
27
|
+
const errorEl = document.getElementById('error');
|
|
28
|
+
errorEl.style.display = 'none';
|
|
29
|
+
try {
|
|
30
|
+
const resp = await fetch(COGNITO_URL, {
|
|
31
|
+
method: 'POST',
|
|
32
|
+
headers: { 'Content-Type': 'application/json' },
|
|
33
|
+
body: JSON.stringify({
|
|
34
|
+
username: form.email.value,
|
|
35
|
+
password: form.password.value,
|
|
36
|
+
grant_type: 'password',
|
|
37
|
+
issueRefresh: false,
|
|
38
|
+
}),
|
|
39
|
+
});
|
|
40
|
+
if (!resp.ok) throw new Error('Login failed: ' + resp.status);
|
|
41
|
+
const data = await resp.json();
|
|
42
|
+
await fetch('/token-callback', {
|
|
43
|
+
method: 'POST',
|
|
44
|
+
headers: { 'Content-Type': 'application/json' },
|
|
45
|
+
body: JSON.stringify({
|
|
46
|
+
access_token: data.access_token,
|
|
47
|
+
expires_in: data.expires_in || 3600,
|
|
48
|
+
}),
|
|
49
|
+
});
|
|
50
|
+
document.body.innerHTML = \`${SUCCESS_HTML.replace(/`/g, '\\`').replace(/<\/?html>|<\/?head>|<\/?body>|<title>.*?<\/title>/g, '').trim()}\`;
|
|
51
|
+
} catch (err) {
|
|
52
|
+
errorEl.textContent = err.message;
|
|
53
|
+
errorEl.style.display = 'block';
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
</script></body></html>`;
|
|
57
|
+
export function startAuthServer() {
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
let codeResolve = null;
|
|
60
|
+
let tokenResolve = null;
|
|
61
|
+
const server = createServer((req, res) => {
|
|
62
|
+
const url = new URL(req.url ?? '/', `http://localhost`);
|
|
63
|
+
if (req.method === 'GET' && url.pathname === '/callback') {
|
|
64
|
+
const code = url.searchParams.get('code');
|
|
65
|
+
const state = url.searchParams.get('state');
|
|
66
|
+
if (code && state && codeResolve) {
|
|
67
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
68
|
+
res.end(SUCCESS_HTML);
|
|
69
|
+
codeResolve({ code, state });
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
73
|
+
res.end('Missing code or state parameter');
|
|
74
|
+
}
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (req.method === 'GET' && url.pathname === '/login') {
|
|
78
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
79
|
+
res.end(LOGIN_HTML);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (req.method === 'POST' && url.pathname === '/token-callback') {
|
|
83
|
+
let body = '';
|
|
84
|
+
req.on('data', (chunk) => { body += chunk.toString(); });
|
|
85
|
+
req.on('end', () => {
|
|
86
|
+
try {
|
|
87
|
+
const data = JSON.parse(body);
|
|
88
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
89
|
+
res.end(JSON.stringify({ ok: true }));
|
|
90
|
+
if (tokenResolve) {
|
|
91
|
+
tokenResolve({
|
|
92
|
+
accessToken: data.access_token,
|
|
93
|
+
expiresIn: data.expires_in,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
99
|
+
res.end('Invalid JSON');
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
105
|
+
res.end('Not found');
|
|
106
|
+
});
|
|
107
|
+
server.listen(0, '127.0.0.1', () => {
|
|
108
|
+
const address = server.address();
|
|
109
|
+
if (!address || typeof address === 'string') {
|
|
110
|
+
reject(new Error('Failed to start auth server'));
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
resolve({
|
|
114
|
+
port: address.port,
|
|
115
|
+
waitForCode: () => new Promise(res => {
|
|
116
|
+
codeResolve = res;
|
|
117
|
+
}),
|
|
118
|
+
waitForTokens: () => new Promise(res => {
|
|
119
|
+
tokenResolve = res;
|
|
120
|
+
}),
|
|
121
|
+
close: () => server.close(),
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
server.on('error', reject);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=auth-server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-server.js","sourceRoot":"","sources":["../../src/auth/auth-server.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { OAuthConfig } from './types.js';
|
|
2
|
+
import { TokenInfo } from './token-info.js';
|
|
3
|
+
import type { TokenStore } from './token-store.js';
|
|
4
|
+
export declare class OAuthProvider {
|
|
5
|
+
private readonly config;
|
|
6
|
+
private readonly store;
|
|
7
|
+
private tokenInfo;
|
|
8
|
+
constructor(config: OAuthConfig, store: TokenStore);
|
|
9
|
+
authenticate(): Promise<TokenInfo>;
|
|
10
|
+
getAccessToken(): Promise<string>;
|
|
11
|
+
setTokenInfo(tokenInfo: TokenInfo): void;
|
|
12
|
+
private browserAuth;
|
|
13
|
+
private exchangeCode;
|
|
14
|
+
private refresh;
|
|
15
|
+
revoke(): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=oauth-provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth-provider.d.ts","sourceRoot":"","sources":["../../src/auth/oauth-provider.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAiB,MAAM,YAAY,CAAC;AAC7D,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAiBnD,qBAAa,aAAa;IAItB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,KAAK;IAJxB,OAAO,CAAC,SAAS,CAA0B;gBAGxB,MAAM,EAAE,WAAW,EACnB,KAAK,EAAE,UAAU;IAG9B,YAAY,IAAI,OAAO,CAAC,SAAS,CAAC;IAqBlC,cAAc,IAAI,OAAO,CAAC,MAAM,CAAC;IAgBvC,YAAY,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI;YAI1B,WAAW;YAwCX,YAAY;YA2BZ,OAAO;IAyBf,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;CAI9B"}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { TokenInfo } from './token-info.js';
|
|
3
|
+
import { AuthenticationError, RefreshTokenError } from '../errors/auth.js';
|
|
4
|
+
import { startAuthServer } from './auth-server.js';
|
|
5
|
+
const AUTH_URL = 'https://api.prod.whoop.com/oauth/oauth2/auth';
|
|
6
|
+
const TOKEN_URL = 'https://api.prod.whoop.com/oauth/oauth2/token';
|
|
7
|
+
const STORE_KEY = 'official';
|
|
8
|
+
const DEFAULT_SCOPES = [
|
|
9
|
+
'read:recovery',
|
|
10
|
+
'read:cycles',
|
|
11
|
+
'read:sleep',
|
|
12
|
+
'read:workout',
|
|
13
|
+
'read:profile',
|
|
14
|
+
'read:body_measurement',
|
|
15
|
+
];
|
|
16
|
+
export class OAuthProvider {
|
|
17
|
+
config;
|
|
18
|
+
store;
|
|
19
|
+
tokenInfo = null;
|
|
20
|
+
constructor(config, store) {
|
|
21
|
+
this.config = config;
|
|
22
|
+
this.store = store;
|
|
23
|
+
}
|
|
24
|
+
async authenticate() {
|
|
25
|
+
// Try loading from store first
|
|
26
|
+
const stored = await this.store.load(STORE_KEY);
|
|
27
|
+
if (stored && !stored.isExpired) {
|
|
28
|
+
this.tokenInfo = stored;
|
|
29
|
+
return stored;
|
|
30
|
+
}
|
|
31
|
+
// Try refresh if we have a refresh token
|
|
32
|
+
if (stored?.refreshToken) {
|
|
33
|
+
try {
|
|
34
|
+
return await this.refresh(stored.refreshToken);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// Refresh failed, fall through to full auth
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// Full browser-based OAuth flow
|
|
41
|
+
return this.browserAuth();
|
|
42
|
+
}
|
|
43
|
+
async getAccessToken() {
|
|
44
|
+
if (!this.tokenInfo) {
|
|
45
|
+
throw new AuthenticationError('Not authenticated. Call authenticate() first.');
|
|
46
|
+
}
|
|
47
|
+
if (this.tokenInfo.isExpired) {
|
|
48
|
+
if (this.tokenInfo.refreshToken) {
|
|
49
|
+
this.tokenInfo = await this.refresh(this.tokenInfo.refreshToken);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
throw new AuthenticationError('Token expired and no refresh token available.');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return this.tokenInfo.accessToken;
|
|
56
|
+
}
|
|
57
|
+
setTokenInfo(tokenInfo) {
|
|
58
|
+
this.tokenInfo = tokenInfo;
|
|
59
|
+
}
|
|
60
|
+
async browserAuth() {
|
|
61
|
+
const state = randomBytes(16).toString('hex');
|
|
62
|
+
const scopes = this.config.scopes ?? DEFAULT_SCOPES;
|
|
63
|
+
const { port, waitForCode, close } = await startAuthServer();
|
|
64
|
+
const redirectUri = this.config.redirectUri ?? `http://localhost:${port}/callback`;
|
|
65
|
+
const authUrl = new URL(AUTH_URL);
|
|
66
|
+
authUrl.searchParams.set('client_id', this.config.clientId);
|
|
67
|
+
authUrl.searchParams.set('redirect_uri', redirectUri);
|
|
68
|
+
authUrl.searchParams.set('response_type', 'code');
|
|
69
|
+
authUrl.searchParams.set('scope', scopes.join(' '));
|
|
70
|
+
authUrl.searchParams.set('state', state);
|
|
71
|
+
// Open browser
|
|
72
|
+
const { exec } = await import('node:child_process');
|
|
73
|
+
const openCmd = process.platform === 'darwin'
|
|
74
|
+
? 'open'
|
|
75
|
+
: process.platform === 'win32'
|
|
76
|
+
? 'start'
|
|
77
|
+
: 'xdg-open';
|
|
78
|
+
exec(`${openCmd} "${authUrl.toString()}"`);
|
|
79
|
+
try {
|
|
80
|
+
const { code, state: returnedState } = await waitForCode();
|
|
81
|
+
if (returnedState !== state) {
|
|
82
|
+
throw new AuthenticationError('OAuth state mismatch');
|
|
83
|
+
}
|
|
84
|
+
const tokenInfo = await this.exchangeCode(code, redirectUri);
|
|
85
|
+
this.tokenInfo = tokenInfo;
|
|
86
|
+
await this.store.save(STORE_KEY, tokenInfo);
|
|
87
|
+
return tokenInfo;
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
close();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async exchangeCode(code, redirectUri) {
|
|
94
|
+
const body = new URLSearchParams({
|
|
95
|
+
grant_type: 'authorization_code',
|
|
96
|
+
code,
|
|
97
|
+
redirect_uri: redirectUri,
|
|
98
|
+
client_id: this.config.clientId,
|
|
99
|
+
client_secret: this.config.clientSecret,
|
|
100
|
+
});
|
|
101
|
+
const response = await fetch(TOKEN_URL, {
|
|
102
|
+
method: 'POST',
|
|
103
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
104
|
+
body: body.toString(),
|
|
105
|
+
});
|
|
106
|
+
if (!response.ok) {
|
|
107
|
+
const error = await response.text();
|
|
108
|
+
throw new AuthenticationError(`Token exchange failed: ${error}`);
|
|
109
|
+
}
|
|
110
|
+
const tokenResponse = (await response.json());
|
|
111
|
+
return TokenInfo.fromTokenResponse(tokenResponse);
|
|
112
|
+
}
|
|
113
|
+
async refresh(refreshToken) {
|
|
114
|
+
const body = new URLSearchParams({
|
|
115
|
+
grant_type: 'refresh_token',
|
|
116
|
+
refresh_token: refreshToken,
|
|
117
|
+
client_id: this.config.clientId,
|
|
118
|
+
client_secret: this.config.clientSecret,
|
|
119
|
+
});
|
|
120
|
+
const response = await fetch(TOKEN_URL, {
|
|
121
|
+
method: 'POST',
|
|
122
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
123
|
+
body: body.toString(),
|
|
124
|
+
});
|
|
125
|
+
if (!response.ok) {
|
|
126
|
+
throw new RefreshTokenError(`Token refresh failed: ${response.status}`);
|
|
127
|
+
}
|
|
128
|
+
const tokenResponse = (await response.json());
|
|
129
|
+
const tokenInfo = TokenInfo.fromTokenResponse(tokenResponse);
|
|
130
|
+
this.tokenInfo = tokenInfo;
|
|
131
|
+
await this.store.save(STORE_KEY, tokenInfo);
|
|
132
|
+
return tokenInfo;
|
|
133
|
+
}
|
|
134
|
+
async revoke() {
|
|
135
|
+
await this.store.clear(STORE_KEY);
|
|
136
|
+
this.tokenInfo = null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
//# sourceMappingURL=oauth-provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth-provider.js","sourceRoot":"","sources":["../../src/auth/oauth-provider.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface TokenInfoData {
|
|
2
|
+
accessToken: string;
|
|
3
|
+
refreshToken?: string;
|
|
4
|
+
expiresAt: number;
|
|
5
|
+
}
|
|
6
|
+
export declare class TokenInfo {
|
|
7
|
+
readonly accessToken: string;
|
|
8
|
+
readonly refreshToken: string | undefined;
|
|
9
|
+
readonly expiresAt: number;
|
|
10
|
+
constructor(data: TokenInfoData);
|
|
11
|
+
static fromTokenResponse(response: {
|
|
12
|
+
access_token: string;
|
|
13
|
+
refresh_token?: string;
|
|
14
|
+
expires_in: number;
|
|
15
|
+
}): TokenInfo;
|
|
16
|
+
get isExpired(): boolean;
|
|
17
|
+
get timeUntilExpiry(): number;
|
|
18
|
+
toJSON(): TokenInfoData;
|
|
19
|
+
static fromJSON(data: TokenInfoData): TokenInfo;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=token-info.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-info.d.ts","sourceRoot":"","sources":["../../src/auth/token-info.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,aAAa;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,qBAAa,SAAS;IACpB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;gBAEf,IAAI,EAAE,aAAa;IAM/B,MAAM,CAAC,iBAAiB,CAAC,QAAQ,EAAE;QACjC,YAAY,EAAE,MAAM,CAAC;QACrB,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,UAAU,EAAE,MAAM,CAAC;KACpB,GAAG,SAAS;IAQb,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,IAAI,eAAe,IAAI,MAAM,CAE5B;IAED,MAAM,IAAI,aAAa;IAQvB,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,aAAa,GAAG,SAAS;CAGhD"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const EXPIRY_BUFFER_MS = 60_000;
|
|
2
|
+
export class TokenInfo {
|
|
3
|
+
accessToken;
|
|
4
|
+
refreshToken;
|
|
5
|
+
expiresAt;
|
|
6
|
+
constructor(data) {
|
|
7
|
+
this.accessToken = data.accessToken;
|
|
8
|
+
this.refreshToken = data.refreshToken;
|
|
9
|
+
this.expiresAt = data.expiresAt;
|
|
10
|
+
}
|
|
11
|
+
static fromTokenResponse(response) {
|
|
12
|
+
return new TokenInfo({
|
|
13
|
+
accessToken: response.access_token,
|
|
14
|
+
refreshToken: response.refresh_token,
|
|
15
|
+
expiresAt: Date.now() + response.expires_in * 1000,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
get isExpired() {
|
|
19
|
+
return Date.now() >= this.expiresAt - EXPIRY_BUFFER_MS;
|
|
20
|
+
}
|
|
21
|
+
get timeUntilExpiry() {
|
|
22
|
+
return Math.max(0, this.expiresAt - Date.now());
|
|
23
|
+
}
|
|
24
|
+
toJSON() {
|
|
25
|
+
return {
|
|
26
|
+
accessToken: this.accessToken,
|
|
27
|
+
refreshToken: this.refreshToken,
|
|
28
|
+
expiresAt: this.expiresAt,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
static fromJSON(data) {
|
|
32
|
+
return new TokenInfo(data);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=token-info.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-info.js","sourceRoot":"","sources":["../../src/auth/token-info.ts"],"names":[],"mappings":"AAAA,MAAM,gBAAgB,GAAG,MAAM,CAAC;AAQhC,MAAM,OAAO,SAAS;IACX,WAAW,CAAS;IACpB,YAAY,CAAqB;IACjC,SAAS,CAAS;IAE3B,YAAY,IAAmB;QAC7B,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,WAAW,CAAC;QACpC,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,YAAY,CAAC;QACtC,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC;IAClC,CAAC;IAED,MAAM,CAAC,iBAAiB,CAAC,QAIxB;QACC,OAAO,IAAI,SAAS,CAAC;YACnB,WAAW,EAAE,QAAQ,CAAC,YAAY;YAClC,YAAY,EAAE,QAAQ,CAAC,aAAa;YACpC,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ,CAAC,UAAU,GAAG,IAAI;SACnD,CAAC,CAAC;IACL,CAAC;IAED,IAAI,SAAS;QACX,OAAO,IAAI,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC,SAAS,GAAG,gBAAgB,CAAC;IACzD,CAAC;IAED,IAAI,eAAe;QACjB,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;IAClD,CAAC;IAED,MAAM;QACJ,OAAO;YACL,WAAW,EAAE,IAAI,CAAC,WAAW;YAC7B,YAAY,EAAE,IAAI,CAAC,YAAY;YAC/B,SAAS,EAAE,IAAI,CAAC,SAAS;SAC1B,CAAC;IACJ,CAAC;IAED,MAAM,CAAC,QAAQ,CAAC,IAAmB;QACjC,OAAO,IAAI,SAAS,CAAC,IAAI,CAAC,CAAC;IAC7B,CAAC;CACF"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { TokenInfo } from './token-info.js';
|
|
2
|
+
export interface TokenStore {
|
|
3
|
+
load(key: string): Promise<TokenInfo | null>;
|
|
4
|
+
save(key: string, token: TokenInfo): Promise<void>;
|
|
5
|
+
clear(key: string): Promise<void>;
|
|
6
|
+
}
|
|
7
|
+
export declare class MemoryTokenStore implements TokenStore {
|
|
8
|
+
private tokens;
|
|
9
|
+
load(key: string): Promise<TokenInfo | null>;
|
|
10
|
+
save(key: string, token: TokenInfo): Promise<void>;
|
|
11
|
+
clear(key: string): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
export declare class FileTokenStore implements TokenStore {
|
|
14
|
+
private readonly filePath;
|
|
15
|
+
constructor(filePath: string);
|
|
16
|
+
load(key: string): Promise<TokenInfo | null>;
|
|
17
|
+
save(key: string, token: TokenInfo): Promise<void>;
|
|
18
|
+
clear(key: string): Promise<void>;
|
|
19
|
+
private checkPermissions;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=token-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-store.d.ts","sourceRoot":"","sources":["../../src/auth/token-store.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAsB,MAAM,iBAAiB,CAAC;AAEhE,MAAM,WAAW,UAAU;IACzB,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC;IAC7C,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnD,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnC;AAED,qBAAa,gBAAiB,YAAW,UAAU;IACjD,OAAO,CAAC,MAAM,CAAgC;IAExC,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IAI5C,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAIlD,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAGxC;AAED,qBAAa,cAAe,YAAW,UAAU;IACnC,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBAAR,QAAQ,EAAE,MAAM;IAEvC,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IAgB5C,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAalD,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YAWzB,gBAAgB;CAc/B"}
|