@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.
@@ -1,7 +1,6 @@
1
1
  // ─── Garmin Health API Client ────────────────────────────────────────────────
2
2
  // Lightweight, fetch-based client for the Garmin Health API.
3
- // Every request is signed with OAuth 1.0a using the consumer and user tokens.
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
- /** Your application's consumer key (from Garmin Developer Portal). */
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
- * Defaults to `https://apis.garmin.com`.
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 signed with OAuth 1.0a. Time-range parameters
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
- * consumerKey: "your_key",
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: buildOAuthHeader(oauthParams),
191
+ Authorization: `Bearer ${this.accessToken}`,
210
192
  Accept: "application/json",
211
193
  },
212
194
  });
@@ -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 OAuth 1.0a HMAC-SHA1 signing.
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
- getRequestToken,
34
- getAccessToken,
35
- buildOAuthSignature,
36
- buildOAuthHeader,
37
- percentEncode,
38
- generateNonce,
39
- getTimestamp,
33
+ generateCodeVerifier,
34
+ generateCodeChallenge,
35
+ generateState,
36
+ buildAuthUrl,
37
+ exchangeCode,
38
+ refreshToken,
40
39
  } from "./auth.js";
41
40
  export type {
42
- GetRequestTokenOptions,
43
- GetAccessTokenOptions,
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
- GarminOAuthRequestTokenResponse,
74
- GarminOAuthAccessTokenResponse,
73
+ GarminOAuth2TokenResponse,
75
74
  GarminWebhookPayload,
76
75
  } from "./types.js";
@@ -314,16 +314,15 @@ export type GarminActivityType =
314
314
  | "FITNESS_EQUIPMENT"
315
315
  | (string & {});
316
316
 
317
- // ─── OAuth ──────────────────────────────────────────────────────────────────
318
-
319
- export interface GarminOAuthRequestTokenResponse {
320
- oauthToken: string;
321
- oauthTokenSecret: string;
322
- }
323
-
324
- export interface GarminOAuthAccessTokenResponse {
325
- oauthToken: string;
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 ──────────────────────────────────────────────────