@nativesquare/soma 0.5.0 → 0.6.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/package.json +1 -1
- package/src/client/index.ts +89 -85
- package/src/component/_generated/component.ts +15 -19
- package/src/component/garmin.ts +140 -124
- package/src/component/schema.ts +9 -10
- package/src/component/strava.ts +0 -1
- package/src/garmin/auth.test.ts +71 -96
- package/src/garmin/auth.ts +129 -193
- package/src/garmin/client.ts +33 -51
- package/src/garmin/index.ts +13 -14
- package/src/garmin/types.ts +9 -10
package/src/garmin/client.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// ─── Garmin Health API Client ────────────────────────────────────────────────
|
|
2
2
|
// Lightweight, fetch-based client for the Garmin Health API.
|
|
3
|
-
//
|
|
4
|
-
// Uses the Web Crypto API for HMAC-SHA1 signing and global `fetch`.
|
|
3
|
+
// Authenticates requests with an OAuth 2.0 Bearer token.
|
|
5
4
|
|
|
6
5
|
import type {
|
|
7
6
|
GarminActivity,
|
|
@@ -10,27 +9,15 @@ import type {
|
|
|
10
9
|
GarminBodyComposition,
|
|
11
10
|
GarminMenstrualCycleData,
|
|
12
11
|
} from "./types.js";
|
|
13
|
-
import {
|
|
14
|
-
generateNonce,
|
|
15
|
-
getTimestamp,
|
|
16
|
-
buildOAuthSignature,
|
|
17
|
-
buildOAuthHeader,
|
|
18
|
-
} from "./auth.js";
|
|
19
12
|
|
|
20
13
|
const DEFAULT_BASE_URL = "https://apis.garmin.com";
|
|
21
14
|
|
|
22
15
|
export interface GarminClientOptions {
|
|
23
|
-
/**
|
|
24
|
-
consumerKey: string;
|
|
25
|
-
/** Your application's consumer secret. */
|
|
26
|
-
consumerSecret: string;
|
|
27
|
-
/** The user's permanent OAuth access token. */
|
|
16
|
+
/** The user's OAuth 2.0 access token. */
|
|
28
17
|
accessToken: string;
|
|
29
|
-
/** The user's permanent OAuth token secret. */
|
|
30
|
-
tokenSecret: string;
|
|
31
18
|
/**
|
|
32
19
|
* Base URL of the Garmin Health API.
|
|
33
|
-
*
|
|
20
|
+
* @default "https://apis.garmin.com"
|
|
34
21
|
*/
|
|
35
22
|
baseUrl?: string;
|
|
36
23
|
}
|
|
@@ -38,17 +25,14 @@ export interface GarminClientOptions {
|
|
|
38
25
|
/**
|
|
39
26
|
* A lightweight client for the Garmin Health API.
|
|
40
27
|
*
|
|
41
|
-
* All requests are
|
|
28
|
+
* All requests are authenticated with a Bearer token. Time-range parameters
|
|
42
29
|
* use Unix epoch seconds for `uploadStartTimeInSeconds` and
|
|
43
30
|
* `uploadEndTimeInSeconds`.
|
|
44
31
|
*
|
|
45
32
|
* @example
|
|
46
33
|
* ```ts
|
|
47
34
|
* const client = new GarminClient({
|
|
48
|
-
*
|
|
49
|
-
* consumerSecret: "your_secret",
|
|
50
|
-
* accessToken: "user_token",
|
|
51
|
-
* tokenSecret: "user_secret",
|
|
35
|
+
* accessToken: "user_access_token",
|
|
52
36
|
* });
|
|
53
37
|
*
|
|
54
38
|
* const dailies = await client.getDailies({
|
|
@@ -58,17 +42,11 @@ export interface GarminClientOptions {
|
|
|
58
42
|
* ```
|
|
59
43
|
*/
|
|
60
44
|
export class GarminClient {
|
|
61
|
-
private readonly consumerKey: string;
|
|
62
|
-
private readonly consumerSecret: string;
|
|
63
45
|
private readonly accessToken: string;
|
|
64
|
-
private readonly tokenSecret: string;
|
|
65
46
|
private readonly baseUrl: string;
|
|
66
47
|
|
|
67
48
|
constructor(opts: GarminClientOptions) {
|
|
68
|
-
this.consumerKey = opts.consumerKey;
|
|
69
|
-
this.consumerSecret = opts.consumerSecret;
|
|
70
49
|
this.accessToken = opts.accessToken;
|
|
71
|
-
this.tokenSecret = opts.tokenSecret;
|
|
72
50
|
this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
73
51
|
}
|
|
74
52
|
|
|
@@ -168,6 +146,33 @@ export class GarminClient {
|
|
|
168
146
|
);
|
|
169
147
|
}
|
|
170
148
|
|
|
149
|
+
// ─── User Deregistration ──────────────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Delete the user's registration with Garmin.
|
|
153
|
+
*
|
|
154
|
+
* Must be called when the user disconnects or deletes their account
|
|
155
|
+
* to comply with Garmin's API requirements.
|
|
156
|
+
*/
|
|
157
|
+
async deleteUserRegistration(): Promise<void> {
|
|
158
|
+
const url = `${this.baseUrl}/wellness-api/rest/user/registration`;
|
|
159
|
+
const response = await fetch(url, {
|
|
160
|
+
method: "DELETE",
|
|
161
|
+
headers: {
|
|
162
|
+
Authorization: `Bearer ${this.accessToken}`,
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
if (!response.ok) {
|
|
167
|
+
const body = await response.text().catch(() => "");
|
|
168
|
+
throw new GarminApiError(
|
|
169
|
+
`Garmin API error: ${response.status} ${response.statusText}`,
|
|
170
|
+
response.status,
|
|
171
|
+
body,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
171
176
|
// ─── Internal ─────────────────────────────────────────────────────────
|
|
172
177
|
|
|
173
178
|
private async get<T>(
|
|
@@ -180,33 +185,10 @@ export class GarminClient {
|
|
|
180
185
|
: "";
|
|
181
186
|
const requestUrl = `${fullUrl}${qs}`;
|
|
182
187
|
|
|
183
|
-
const nonce = generateNonce();
|
|
184
|
-
const timestamp = getTimestamp();
|
|
185
|
-
|
|
186
|
-
const oauthParams: Record<string, string> = {
|
|
187
|
-
oauth_consumer_key: this.consumerKey,
|
|
188
|
-
oauth_nonce: nonce,
|
|
189
|
-
oauth_signature_method: "HMAC-SHA1",
|
|
190
|
-
oauth_timestamp: timestamp,
|
|
191
|
-
oauth_token: this.accessToken,
|
|
192
|
-
oauth_version: "1.0",
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
// OAuth signature must include both OAuth params and query params
|
|
196
|
-
const allParams = { ...oauthParams, ...(queryParams ?? {}) };
|
|
197
|
-
const signature = await buildOAuthSignature(
|
|
198
|
-
"GET",
|
|
199
|
-
fullUrl,
|
|
200
|
-
allParams,
|
|
201
|
-
this.consumerSecret,
|
|
202
|
-
this.tokenSecret,
|
|
203
|
-
);
|
|
204
|
-
oauthParams.oauth_signature = signature;
|
|
205
|
-
|
|
206
188
|
const response = await fetch(requestUrl, {
|
|
207
189
|
method: "GET",
|
|
208
190
|
headers: {
|
|
209
|
-
Authorization:
|
|
191
|
+
Authorization: `Bearer ${this.accessToken}`,
|
|
210
192
|
Accept: "application/json",
|
|
211
193
|
},
|
|
212
194
|
});
|
package/src/garmin/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// ─── @nativesquare/soma/garmin ───────────────────────────────────────────────
|
|
2
|
-
// Garmin Health API → Soma schema transformers, API client, OAuth helpers, and sync.
|
|
2
|
+
// Garmin Health API → Soma schema transformers, API client, OAuth 2.0 PKCE helpers, and sync.
|
|
3
3
|
//
|
|
4
|
-
// Uses the Web Crypto API for
|
|
4
|
+
// Uses the Web Crypto API for PKCE code challenge generation.
|
|
5
5
|
// Compatible with both the Convex V8 runtime and Node.js environments.
|
|
6
6
|
|
|
7
7
|
// ── Transformers ─────────────────────────────────────────────────────────────
|
|
@@ -28,19 +28,19 @@ export { mapSleepLevel } from "./maps/sleep-level.js";
|
|
|
28
28
|
export { GarminClient, GarminApiError } from "./client.js";
|
|
29
29
|
export type { GarminClientOptions, TimeRangeParams } from "./client.js";
|
|
30
30
|
|
|
31
|
-
// ── OAuth Helpers
|
|
31
|
+
// ── OAuth 2.0 PKCE Helpers ───────────────────────────────────────────────────
|
|
32
32
|
export {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
getTimestamp,
|
|
33
|
+
generateCodeVerifier,
|
|
34
|
+
generateCodeChallenge,
|
|
35
|
+
generateState,
|
|
36
|
+
buildAuthUrl,
|
|
37
|
+
exchangeCode,
|
|
38
|
+
refreshToken,
|
|
40
39
|
} from "./auth.js";
|
|
41
40
|
export type {
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
BuildAuthUrlOptions,
|
|
42
|
+
ExchangeCodeOptions,
|
|
43
|
+
RefreshTokenOptions,
|
|
44
44
|
} from "./auth.js";
|
|
45
45
|
|
|
46
46
|
// ── Sync Helpers ─────────────────────────────────────────────────────────────
|
|
@@ -70,7 +70,6 @@ export type {
|
|
|
70
70
|
GarminMenstrualCycleData,
|
|
71
71
|
GarminUserProfile,
|
|
72
72
|
GarminActivityType,
|
|
73
|
-
|
|
74
|
-
GarminOAuthAccessTokenResponse,
|
|
73
|
+
GarminOAuth2TokenResponse,
|
|
75
74
|
GarminWebhookPayload,
|
|
76
75
|
} from "./types.js";
|
package/src/garmin/types.ts
CHANGED
|
@@ -314,16 +314,15 @@ export type GarminActivityType =
|
|
|
314
314
|
| "FITNESS_EQUIPMENT"
|
|
315
315
|
| (string & {});
|
|
316
316
|
|
|
317
|
-
// ─── OAuth
|
|
318
|
-
|
|
319
|
-
export interface
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
oauthTokenSecret: string;
|
|
317
|
+
// ─── OAuth 2.0 ──────────────────────────────────────────────────────────────
|
|
318
|
+
|
|
319
|
+
export interface GarminOAuth2TokenResponse {
|
|
320
|
+
access_token: string;
|
|
321
|
+
refresh_token: string;
|
|
322
|
+
expires_in: number;
|
|
323
|
+
token_type: string;
|
|
324
|
+
scope: string;
|
|
325
|
+
refresh_token_expires_in: number;
|
|
327
326
|
}
|
|
328
327
|
|
|
329
328
|
// ─── API Response Wrappers ──────────────────────────────────────────────────
|