@nativesquare/soma 0.2.0 → 0.4.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/dist/client/index.d.ts +167 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +150 -0
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/api.d.ts +2 -0
- package/dist/component/_generated/api.d.ts.map +1 -1
- package/dist/component/_generated/api.js.map +1 -1
- package/dist/component/_generated/component.d.ts +56 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/garmin.d.ts +110 -0
- package/dist/component/garmin.d.ts.map +1 -0
- package/dist/component/garmin.js +454 -0
- package/dist/component/garmin.js.map +1 -0
- package/dist/component/public.d.ts +761 -761
- package/dist/component/schema.d.ts +390 -388
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +3 -2
- package/dist/component/schema.js.map +1 -1
- package/dist/component/strava.d.ts +5 -4
- package/dist/component/strava.d.ts.map +1 -1
- package/dist/component/strava.js +18 -1
- package/dist/component/strava.js.map +1 -1
- package/dist/component/validators/activity.d.ts +42 -42
- package/dist/component/validators/body.d.ts +47 -47
- package/dist/component/validators/daily.d.ts +17 -17
- package/dist/component/validators/plannedWorkout.d.ts +5 -5
- package/dist/component/validators/samples.d.ts +2 -2
- package/dist/component/validators/shared.d.ts +17 -17
- package/dist/component/validators/sleep.d.ts +17 -17
- package/dist/garmin/activity.d.ts +101 -0
- package/dist/garmin/activity.d.ts.map +1 -0
- package/dist/garmin/activity.js +207 -0
- package/dist/garmin/activity.js.map +1 -0
- package/dist/garmin/auth.d.ts +65 -0
- package/dist/garmin/auth.d.ts.map +1 -0
- package/dist/garmin/auth.js +155 -0
- package/dist/garmin/auth.js.map +1 -0
- package/dist/garmin/body.d.ts +26 -0
- package/dist/garmin/body.d.ts.map +1 -0
- package/dist/garmin/body.js +44 -0
- package/dist/garmin/body.js.map +1 -0
- package/dist/garmin/client.d.ts +99 -0
- package/dist/garmin/client.d.ts.map +1 -0
- package/dist/garmin/client.js +153 -0
- package/dist/garmin/client.js.map +1 -0
- package/dist/garmin/daily.d.ts +74 -0
- package/dist/garmin/daily.d.ts.map +1 -0
- package/dist/garmin/daily.js +143 -0
- package/dist/garmin/daily.js.map +1 -0
- package/dist/garmin/index.d.ts +20 -0
- package/dist/garmin/index.d.ts.map +1 -0
- package/dist/garmin/index.js +21 -0
- package/dist/garmin/index.js.map +1 -0
- package/dist/garmin/maps/activity-type.d.ts +7 -0
- package/dist/garmin/maps/activity-type.d.ts.map +1 -0
- package/dist/garmin/maps/activity-type.js +98 -0
- package/dist/garmin/maps/activity-type.js.map +1 -0
- package/dist/garmin/maps/sleep-level.d.ts +6 -0
- package/dist/garmin/maps/sleep-level.d.ts.map +1 -0
- package/dist/garmin/maps/sleep-level.js +21 -0
- package/dist/garmin/maps/sleep-level.js.map +1 -0
- package/dist/garmin/menstruation.d.ts +23 -0
- package/dist/garmin/menstruation.d.ts.map +1 -0
- package/dist/garmin/menstruation.js +34 -0
- package/dist/garmin/menstruation.js.map +1 -0
- package/dist/garmin/sleep.d.ts +62 -0
- package/dist/garmin/sleep.d.ts.map +1 -0
- package/dist/garmin/sleep.js +125 -0
- package/dist/garmin/sleep.js.map +1 -0
- package/dist/garmin/sync.d.ts +39 -0
- package/dist/garmin/sync.d.ts.map +1 -0
- package/dist/garmin/sync.js +175 -0
- package/dist/garmin/sync.js.map +1 -0
- package/dist/garmin/types.d.ts +212 -0
- package/dist/garmin/types.d.ts.map +1 -0
- package/dist/garmin/types.js +8 -0
- package/dist/garmin/types.js.map +1 -0
- package/dist/validators.d.ts +6617 -0
- package/dist/validators.d.ts.map +1 -0
- package/dist/validators.js +78 -0
- package/dist/validators.js.map +1 -0
- package/package.json +9 -1
- package/src/client/index.ts +194 -1
- package/src/component/_generated/api.ts +2 -0
- package/src/component/_generated/component.ts +62 -0
- package/src/component/garmin.ts +534 -0
- package/src/component/schema.ts +3 -2
- package/src/component/strava.ts +23 -1
- package/src/garmin/activity.test.ts +178 -0
- package/src/garmin/activity.ts +272 -0
- package/src/garmin/auth.test.ts +128 -0
- package/src/garmin/auth.ts +249 -0
- package/src/garmin/body.ts +59 -0
- package/src/garmin/client.ts +254 -0
- package/src/garmin/daily.ts +211 -0
- package/src/garmin/index.ts +76 -0
- package/src/garmin/maps/activity-type.test.ts +78 -0
- package/src/garmin/maps/activity-type.ts +116 -0
- package/src/garmin/maps/sleep-level.ts +22 -0
- package/src/garmin/menstruation.ts +42 -0
- package/src/garmin/sleep.test.ts +110 -0
- package/src/garmin/sleep.ts +170 -0
- package/src/garmin/sync.ts +223 -0
- package/src/garmin/types.ts +338 -0
- package/src/validators.ts +89 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// ─── Garmin OAuth 1.0a Helpers ───────────────────────────────────────────────
|
|
2
|
+
// Pure helper functions for the Garmin OAuth 1.0a three-legged flow.
|
|
3
|
+
// Uses the Web Crypto API for HMAC-SHA1 signing and global `fetch`.
|
|
4
|
+
const OAUTH_BASE_URL = "https://connectapi.garmin.com";
|
|
5
|
+
const AUTH_CONFIRM_URL = "https://connect.garmin.com/oauthConfirm";
|
|
6
|
+
// ─── OAuth 1.0a Signature ───────────────────────────────────────────────────
|
|
7
|
+
/**
|
|
8
|
+
* Generate a random nonce for OAuth 1.0a requests.
|
|
9
|
+
*/
|
|
10
|
+
export function generateNonce() {
|
|
11
|
+
const bytes = new Uint8Array(16);
|
|
12
|
+
crypto.getRandomValues(bytes);
|
|
13
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Get the current Unix timestamp in seconds.
|
|
17
|
+
*/
|
|
18
|
+
export function getTimestamp() {
|
|
19
|
+
return String(Math.floor(Date.now() / 1000));
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Percent-encode a string per RFC 3986 (used by OAuth 1.0a).
|
|
23
|
+
* Unlike encodeURIComponent, this also encodes `!`, `*`, `'`, `(`, `)`.
|
|
24
|
+
*/
|
|
25
|
+
export function percentEncode(str) {
|
|
26
|
+
return encodeURIComponent(str).replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Build the OAuth 1.0a signature base string and compute the HMAC-SHA1 signature.
|
|
30
|
+
*
|
|
31
|
+
* @param method - HTTP method (e.g., "POST", "GET")
|
|
32
|
+
* @param url - The base URL (without query string)
|
|
33
|
+
* @param params - All OAuth + request parameters sorted alphabetically
|
|
34
|
+
* @param consumerSecret - The application's consumer secret
|
|
35
|
+
* @param tokenSecret - The token secret (empty string for request token step)
|
|
36
|
+
*/
|
|
37
|
+
export async function buildOAuthSignature(method, url, params, consumerSecret, tokenSecret = "") {
|
|
38
|
+
const sortedKeys = Object.keys(params).sort();
|
|
39
|
+
const paramString = sortedKeys
|
|
40
|
+
.map((key) => `${percentEncode(key)}=${percentEncode(params[key])}`)
|
|
41
|
+
.join("&");
|
|
42
|
+
const signatureBaseString = [
|
|
43
|
+
method.toUpperCase(),
|
|
44
|
+
percentEncode(url),
|
|
45
|
+
percentEncode(paramString),
|
|
46
|
+
].join("&");
|
|
47
|
+
const signingKey = `${percentEncode(consumerSecret)}&${percentEncode(tokenSecret)}`;
|
|
48
|
+
const encoder = new TextEncoder();
|
|
49
|
+
const key = await crypto.subtle.importKey("raw", encoder.encode(signingKey), { name: "HMAC", hash: "SHA-1" }, false, ["sign"]);
|
|
50
|
+
const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(signatureBaseString));
|
|
51
|
+
return btoa(String.fromCharCode(...new Uint8Array(signature)));
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Build the `Authorization: OAuth ...` header value from OAuth parameters.
|
|
55
|
+
*/
|
|
56
|
+
export function buildOAuthHeader(params) {
|
|
57
|
+
const entries = Object.entries(params)
|
|
58
|
+
.map(([key, value]) => `${percentEncode(key)}="${percentEncode(value)}"`)
|
|
59
|
+
.join(", ");
|
|
60
|
+
return `OAuth ${entries}`;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Obtain an unauthorized request token from Garmin.
|
|
64
|
+
*
|
|
65
|
+
* This is Step 1 of the OAuth 1.0a three-legged flow:
|
|
66
|
+
* 1. Your server calls this function to get a temporary request token
|
|
67
|
+
* 2. Redirect the user to the returned `authUrl`
|
|
68
|
+
* 3. After the user authorizes, exchange the verifier for an access token
|
|
69
|
+
*
|
|
70
|
+
* @returns The request token, token secret, and the authorization URL
|
|
71
|
+
*/
|
|
72
|
+
export async function getRequestToken(opts) {
|
|
73
|
+
const url = `${OAUTH_BASE_URL}/oauth-service/oauth/request_token`;
|
|
74
|
+
const nonce = generateNonce();
|
|
75
|
+
const timestamp = getTimestamp();
|
|
76
|
+
const oauthParams = {
|
|
77
|
+
oauth_consumer_key: opts.consumerKey,
|
|
78
|
+
oauth_nonce: nonce,
|
|
79
|
+
oauth_signature_method: "HMAC-SHA1",
|
|
80
|
+
oauth_timestamp: timestamp,
|
|
81
|
+
oauth_version: "1.0",
|
|
82
|
+
};
|
|
83
|
+
if (opts.callbackUrl) {
|
|
84
|
+
oauthParams.oauth_callback = opts.callbackUrl;
|
|
85
|
+
}
|
|
86
|
+
const signature = await buildOAuthSignature("POST", url, oauthParams, opts.consumerSecret);
|
|
87
|
+
oauthParams.oauth_signature = signature;
|
|
88
|
+
const response = await fetch(url, {
|
|
89
|
+
method: "POST",
|
|
90
|
+
headers: {
|
|
91
|
+
Authorization: buildOAuthHeader(oauthParams),
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
if (!response.ok) {
|
|
95
|
+
const body = await response.text().catch(() => "");
|
|
96
|
+
throw new Error(`Garmin OAuth error (getRequestToken): ${response.status} ${response.statusText} — ${body}`);
|
|
97
|
+
}
|
|
98
|
+
const responseText = await response.text();
|
|
99
|
+
const parsed = new URLSearchParams(responseText);
|
|
100
|
+
const oauthToken = parsed.get("oauth_token");
|
|
101
|
+
const oauthTokenSecret = parsed.get("oauth_token_secret");
|
|
102
|
+
if (!oauthToken || !oauthTokenSecret) {
|
|
103
|
+
throw new Error(`Garmin OAuth error: unexpected response format — ${responseText}`);
|
|
104
|
+
}
|
|
105
|
+
const authUrl = opts.callbackUrl
|
|
106
|
+
? `${AUTH_CONFIRM_URL}?oauth_token=${encodeURIComponent(oauthToken)}&oauth_callback=${encodeURIComponent(opts.callbackUrl)}`
|
|
107
|
+
: `${AUTH_CONFIRM_URL}?oauth_token=${encodeURIComponent(oauthToken)}`;
|
|
108
|
+
return {
|
|
109
|
+
oauthToken,
|
|
110
|
+
oauthTokenSecret,
|
|
111
|
+
authUrl,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Exchange a request token + verifier for a permanent access token.
|
|
116
|
+
*
|
|
117
|
+
* This is Step 3 of the OAuth 1.0a three-legged flow.
|
|
118
|
+
* The returned access token and secret are permanent — Garmin tokens
|
|
119
|
+
* do not expire and there is no refresh flow.
|
|
120
|
+
*/
|
|
121
|
+
export async function getAccessToken(opts) {
|
|
122
|
+
const url = `${OAUTH_BASE_URL}/oauth-service/oauth/access_token`;
|
|
123
|
+
const nonce = generateNonce();
|
|
124
|
+
const timestamp = getTimestamp();
|
|
125
|
+
const oauthParams = {
|
|
126
|
+
oauth_consumer_key: opts.consumerKey,
|
|
127
|
+
oauth_nonce: nonce,
|
|
128
|
+
oauth_signature_method: "HMAC-SHA1",
|
|
129
|
+
oauth_timestamp: timestamp,
|
|
130
|
+
oauth_token: opts.token,
|
|
131
|
+
oauth_verifier: opts.verifier,
|
|
132
|
+
oauth_version: "1.0",
|
|
133
|
+
};
|
|
134
|
+
const signature = await buildOAuthSignature("POST", url, oauthParams, opts.consumerSecret, opts.tokenSecret);
|
|
135
|
+
oauthParams.oauth_signature = signature;
|
|
136
|
+
const response = await fetch(url, {
|
|
137
|
+
method: "POST",
|
|
138
|
+
headers: {
|
|
139
|
+
Authorization: buildOAuthHeader(oauthParams),
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
if (!response.ok) {
|
|
143
|
+
const body = await response.text().catch(() => "");
|
|
144
|
+
throw new Error(`Garmin OAuth error (getAccessToken): ${response.status} ${response.statusText} — ${body}`);
|
|
145
|
+
}
|
|
146
|
+
const responseText = await response.text();
|
|
147
|
+
const parsed = new URLSearchParams(responseText);
|
|
148
|
+
const oauthToken = parsed.get("oauth_token");
|
|
149
|
+
const oauthTokenSecret = parsed.get("oauth_token_secret");
|
|
150
|
+
if (!oauthToken || !oauthTokenSecret) {
|
|
151
|
+
throw new Error(`Garmin OAuth error: unexpected access token response — ${responseText}`);
|
|
152
|
+
}
|
|
153
|
+
return { oauthToken, oauthTokenSecret };
|
|
154
|
+
}
|
|
155
|
+
//# sourceMappingURL=auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/garmin/auth.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { GarminBodyComposition } from "./types.js";
|
|
2
|
+
export type BodyData = ReturnType<typeof transformBody>;
|
|
3
|
+
/**
|
|
4
|
+
* Transform a Garmin body composition record into a Soma Body document shape.
|
|
5
|
+
*
|
|
6
|
+
* @param body - The Garmin body composition data from the Health API
|
|
7
|
+
* @returns Soma Body fields (without connectionId/userId)
|
|
8
|
+
*/
|
|
9
|
+
export declare function transformBody(body: GarminBodyComposition): {
|
|
10
|
+
metadata: {
|
|
11
|
+
start_time: string;
|
|
12
|
+
end_time: string;
|
|
13
|
+
};
|
|
14
|
+
measurements_data: {
|
|
15
|
+
measurements: {
|
|
16
|
+
measurement_time: string;
|
|
17
|
+
weight_kg: number | undefined;
|
|
18
|
+
BMI: number | undefined;
|
|
19
|
+
bodyfat_percentage: number | undefined;
|
|
20
|
+
muscle_mass_g: number | undefined;
|
|
21
|
+
bone_mass_g: number | undefined;
|
|
22
|
+
water_percentage: number | undefined;
|
|
23
|
+
}[];
|
|
24
|
+
} | undefined;
|
|
25
|
+
};
|
|
26
|
+
//# sourceMappingURL=body.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"body.d.ts","sourceRoot":"","sources":["../../src/garmin/body.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAExD,MAAM,MAAM,QAAQ,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AAExD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,qBAAqB;;;;;;;;;;;;;;;;EAYxD"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// ─── Body Transformer ────────────────────────────────────────────────────────
|
|
2
|
+
// Transforms Garmin body composition data into the Soma Body schema shape.
|
|
3
|
+
/**
|
|
4
|
+
* Transform a Garmin body composition record into a Soma Body document shape.
|
|
5
|
+
*
|
|
6
|
+
* @param body - The Garmin body composition data from the Health API
|
|
7
|
+
* @returns Soma Body fields (without connectionId/userId)
|
|
8
|
+
*/
|
|
9
|
+
export function transformBody(body) {
|
|
10
|
+
const measurementMs = body.measurementTimeInSeconds * 1000;
|
|
11
|
+
const timestamp = new Date(measurementMs).toISOString();
|
|
12
|
+
return {
|
|
13
|
+
metadata: {
|
|
14
|
+
start_time: timestamp,
|
|
15
|
+
end_time: timestamp,
|
|
16
|
+
},
|
|
17
|
+
measurements_data: buildMeasurementsData(body, timestamp),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
21
|
+
function buildMeasurementsData(body, timestamp) {
|
|
22
|
+
if (body.weightInGrams == null &&
|
|
23
|
+
body.bodyFatInPercent == null &&
|
|
24
|
+
body.bodyMassIndex == null &&
|
|
25
|
+
body.muscleMassInGrams == null &&
|
|
26
|
+
body.boneMassInGrams == null &&
|
|
27
|
+
body.bodyWaterInPercent == null) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
measurements: [
|
|
32
|
+
{
|
|
33
|
+
measurement_time: timestamp,
|
|
34
|
+
weight_kg: body.weightInGrams != null ? body.weightInGrams / 1000 : undefined,
|
|
35
|
+
BMI: body.bodyMassIndex,
|
|
36
|
+
bodyfat_percentage: body.bodyFatInPercent,
|
|
37
|
+
muscle_mass_g: body.muscleMassInGrams,
|
|
38
|
+
bone_mass_g: body.boneMassInGrams,
|
|
39
|
+
water_percentage: body.bodyWaterInPercent,
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=body.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"body.js","sourceRoot":"","sources":["../../src/garmin/body.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,2EAA2E;AAM3E;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAAC,IAA2B;IACvD,MAAM,aAAa,GAAG,IAAI,CAAC,wBAAwB,GAAG,IAAI,CAAC;IAC3D,MAAM,SAAS,GAAG,IAAI,IAAI,CAAC,aAAa,CAAC,CAAC,WAAW,EAAE,CAAC;IAExD,OAAO;QACL,QAAQ,EAAE;YACR,UAAU,EAAE,SAAS;YACrB,QAAQ,EAAE,SAAS;SACpB;QAED,iBAAiB,EAAE,qBAAqB,CAAC,IAAI,EAAE,SAAS,CAAC;KAC1D,CAAC;AACJ,CAAC;AAED,iFAAiF;AAEjF,SAAS,qBAAqB,CAC5B,IAA2B,EAC3B,SAAiB;IAEjB,IACE,IAAI,CAAC,aAAa,IAAI,IAAI;QAC1B,IAAI,CAAC,gBAAgB,IAAI,IAAI;QAC7B,IAAI,CAAC,aAAa,IAAI,IAAI;QAC1B,IAAI,CAAC,iBAAiB,IAAI,IAAI;QAC9B,IAAI,CAAC,eAAe,IAAI,IAAI;QAC5B,IAAI,CAAC,kBAAkB,IAAI,IAAI,EAC/B,CAAC;QACD,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,OAAO;QACL,YAAY,EAAE;YACZ;gBACE,gBAAgB,EAAE,SAAS;gBAC3B,SAAS,EACP,IAAI,CAAC,aAAa,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC,CAAC,CAAC,SAAS;gBACpE,GAAG,EAAE,IAAI,CAAC,aAAa;gBACvB,kBAAkB,EAAE,IAAI,CAAC,gBAAgB;gBACzC,aAAa,EAAE,IAAI,CAAC,iBAAiB;gBACrC,WAAW,EAAE,IAAI,CAAC,eAAe;gBACjC,gBAAgB,EAAE,IAAI,CAAC,kBAAkB;aAC1C;SACF;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { GarminActivity, GarminDailySummary, GarminSleep, GarminBodyComposition, GarminMenstrualCycleData } from "./types.js";
|
|
2
|
+
export interface GarminClientOptions {
|
|
3
|
+
/** Your application's consumer key (from Garmin Developer Portal). */
|
|
4
|
+
consumerKey: string;
|
|
5
|
+
/** Your application's consumer secret. */
|
|
6
|
+
consumerSecret: string;
|
|
7
|
+
/** The user's permanent OAuth access token. */
|
|
8
|
+
accessToken: string;
|
|
9
|
+
/** The user's permanent OAuth token secret. */
|
|
10
|
+
tokenSecret: string;
|
|
11
|
+
/**
|
|
12
|
+
* Base URL of the Garmin Health API.
|
|
13
|
+
* Defaults to `https://apis.garmin.com`.
|
|
14
|
+
*/
|
|
15
|
+
baseUrl?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* A lightweight client for the Garmin Health API.
|
|
19
|
+
*
|
|
20
|
+
* All requests are signed with OAuth 1.0a. Time-range parameters
|
|
21
|
+
* use Unix epoch seconds for `uploadStartTimeInSeconds` and
|
|
22
|
+
* `uploadEndTimeInSeconds`.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* const client = new GarminClient({
|
|
27
|
+
* consumerKey: "your_key",
|
|
28
|
+
* consumerSecret: "your_secret",
|
|
29
|
+
* accessToken: "user_token",
|
|
30
|
+
* tokenSecret: "user_secret",
|
|
31
|
+
* });
|
|
32
|
+
*
|
|
33
|
+
* const dailies = await client.getDailies({
|
|
34
|
+
* uploadStartTimeInSeconds: startEpoch,
|
|
35
|
+
* uploadEndTimeInSeconds: endEpoch,
|
|
36
|
+
* });
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export declare class GarminClient {
|
|
40
|
+
private readonly consumerKey;
|
|
41
|
+
private readonly consumerSecret;
|
|
42
|
+
private readonly accessToken;
|
|
43
|
+
private readonly tokenSecret;
|
|
44
|
+
private readonly baseUrl;
|
|
45
|
+
constructor(opts: GarminClientOptions);
|
|
46
|
+
/**
|
|
47
|
+
* Get daily wellness summaries.
|
|
48
|
+
*
|
|
49
|
+
* Garmin API: `GET /wellness-api/rest/dailies`
|
|
50
|
+
*/
|
|
51
|
+
getDailies(params: TimeRangeParams): Promise<GarminDailySummary[]>;
|
|
52
|
+
/**
|
|
53
|
+
* Get activity summaries.
|
|
54
|
+
*
|
|
55
|
+
* Garmin API: `GET /wellness-api/rest/activities`
|
|
56
|
+
*/
|
|
57
|
+
getActivities(params: TimeRangeParams): Promise<GarminActivity[]>;
|
|
58
|
+
/**
|
|
59
|
+
* Get sleep summaries.
|
|
60
|
+
*
|
|
61
|
+
* Garmin API: `GET /wellness-api/rest/sleeps`
|
|
62
|
+
*/
|
|
63
|
+
getSleeps(params: TimeRangeParams): Promise<GarminSleep[]>;
|
|
64
|
+
/**
|
|
65
|
+
* Get body composition summaries.
|
|
66
|
+
*
|
|
67
|
+
* Garmin API: `GET /wellness-api/rest/bodyComps`
|
|
68
|
+
*/
|
|
69
|
+
getBodyCompositions(params: TimeRangeParams): Promise<GarminBodyComposition[]>;
|
|
70
|
+
/**
|
|
71
|
+
* Get menstrual cycle data.
|
|
72
|
+
*
|
|
73
|
+
* Garmin API: `GET /wellness-api/rest/menstrualCycleData`
|
|
74
|
+
*/
|
|
75
|
+
getMenstrualCycleData(params: TimeRangeParams): Promise<GarminMenstrualCycleData[]>;
|
|
76
|
+
/**
|
|
77
|
+
* Request historical data backfill from Garmin.
|
|
78
|
+
*
|
|
79
|
+
* Garmin processes backfill requests asynchronously and delivers data
|
|
80
|
+
* via the configured webhook endpoint. Maximum range: 90 days per request.
|
|
81
|
+
*
|
|
82
|
+
* @param summaryType - The data type to backfill (e.g., "dailies", "activities", "sleeps", "bodyComps")
|
|
83
|
+
* @param params - Time range for the backfill
|
|
84
|
+
*/
|
|
85
|
+
requestBackfill(summaryType: string, params: TimeRangeParams): Promise<void>;
|
|
86
|
+
private get;
|
|
87
|
+
}
|
|
88
|
+
export interface TimeRangeParams {
|
|
89
|
+
/** Start of the time range as Unix epoch seconds. */
|
|
90
|
+
uploadStartTimeInSeconds: number;
|
|
91
|
+
/** End of the time range as Unix epoch seconds. */
|
|
92
|
+
uploadEndTimeInSeconds: number;
|
|
93
|
+
}
|
|
94
|
+
export declare class GarminApiError extends Error {
|
|
95
|
+
readonly status: number;
|
|
96
|
+
readonly body: string;
|
|
97
|
+
constructor(message: string, status: number, body: string);
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/garmin/client.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,cAAc,EACd,kBAAkB,EAClB,WAAW,EACX,qBAAqB,EACrB,wBAAwB,EACzB,MAAM,YAAY,CAAC;AAUpB,MAAM,WAAW,mBAAmB;IAClC,sEAAsE;IACtE,WAAW,EAAE,MAAM,CAAC;IACpB,0CAA0C;IAC1C,cAAc,EAAE,MAAM,CAAC;IACvB,+CAA+C;IAC/C,WAAW,EAAE,MAAM,CAAC;IACpB,+CAA+C;IAC/C,WAAW,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;gBAErB,IAAI,EAAE,mBAAmB;IAUrC;;;;OAIG;IACG,UAAU,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,kBAAkB,EAAE,CAAC;IASxE;;;;OAIG;IACG,aAAa,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IASvE;;;;OAIG;IACG,SAAS,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IAShE;;;;OAIG;IACG,mBAAmB,CACvB,MAAM,EAAE,eAAe,GACtB,OAAO,CAAC,qBAAqB,EAAE,CAAC;IASnC;;;;OAIG;IACG,qBAAqB,CACzB,MAAM,EAAE,eAAe,GACtB,OAAO,CAAC,wBAAwB,EAAE,CAAC;IAStC;;;;;;;;OAQG;IACG,eAAe,CACnB,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,eAAe,GACtB,OAAO,CAAC,IAAI,CAAC;YAUF,GAAG;CAoDlB;AAID,MAAM,WAAW,eAAe;IAC9B,qDAAqD;IACrD,wBAAwB,EAAE,MAAM,CAAC;IACjC,mDAAmD;IACnD,sBAAsB,EAAE,MAAM,CAAC;CAChC;AAWD,qBAAa,cAAe,SAAQ,KAAK;aAGrB,MAAM,EAAE,MAAM;aACd,IAAI,EAAE,MAAM;gBAF5B,OAAO,EAAE,MAAM,EACC,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM;CAK/B"}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// ─── Garmin Health API Client ────────────────────────────────────────────────
|
|
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`.
|
|
5
|
+
import { generateNonce, getTimestamp, buildOAuthSignature, buildOAuthHeader, } from "./auth.js";
|
|
6
|
+
const DEFAULT_BASE_URL = "https://apis.garmin.com";
|
|
7
|
+
/**
|
|
8
|
+
* A lightweight client for the Garmin Health API.
|
|
9
|
+
*
|
|
10
|
+
* All requests are signed with OAuth 1.0a. Time-range parameters
|
|
11
|
+
* use Unix epoch seconds for `uploadStartTimeInSeconds` and
|
|
12
|
+
* `uploadEndTimeInSeconds`.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* const client = new GarminClient({
|
|
17
|
+
* consumerKey: "your_key",
|
|
18
|
+
* consumerSecret: "your_secret",
|
|
19
|
+
* accessToken: "user_token",
|
|
20
|
+
* tokenSecret: "user_secret",
|
|
21
|
+
* });
|
|
22
|
+
*
|
|
23
|
+
* const dailies = await client.getDailies({
|
|
24
|
+
* uploadStartTimeInSeconds: startEpoch,
|
|
25
|
+
* uploadEndTimeInSeconds: endEpoch,
|
|
26
|
+
* });
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export class GarminClient {
|
|
30
|
+
consumerKey;
|
|
31
|
+
consumerSecret;
|
|
32
|
+
accessToken;
|
|
33
|
+
tokenSecret;
|
|
34
|
+
baseUrl;
|
|
35
|
+
constructor(opts) {
|
|
36
|
+
this.consumerKey = opts.consumerKey;
|
|
37
|
+
this.consumerSecret = opts.consumerSecret;
|
|
38
|
+
this.accessToken = opts.accessToken;
|
|
39
|
+
this.tokenSecret = opts.tokenSecret;
|
|
40
|
+
this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
41
|
+
}
|
|
42
|
+
// ─── Daily Summaries ────────────────────────────────────────────────────
|
|
43
|
+
/**
|
|
44
|
+
* Get daily wellness summaries.
|
|
45
|
+
*
|
|
46
|
+
* Garmin API: `GET /wellness-api/rest/dailies`
|
|
47
|
+
*/
|
|
48
|
+
async getDailies(params) {
|
|
49
|
+
return this.get("/wellness-api/rest/dailies", timeRangeQuery(params));
|
|
50
|
+
}
|
|
51
|
+
// ─── Activities ─────────────────────────────────────────────────────────
|
|
52
|
+
/**
|
|
53
|
+
* Get activity summaries.
|
|
54
|
+
*
|
|
55
|
+
* Garmin API: `GET /wellness-api/rest/activities`
|
|
56
|
+
*/
|
|
57
|
+
async getActivities(params) {
|
|
58
|
+
return this.get("/wellness-api/rest/activities", timeRangeQuery(params));
|
|
59
|
+
}
|
|
60
|
+
// ─── Sleep ──────────────────────────────────────────────────────────────
|
|
61
|
+
/**
|
|
62
|
+
* Get sleep summaries.
|
|
63
|
+
*
|
|
64
|
+
* Garmin API: `GET /wellness-api/rest/sleeps`
|
|
65
|
+
*/
|
|
66
|
+
async getSleeps(params) {
|
|
67
|
+
return this.get("/wellness-api/rest/sleeps", timeRangeQuery(params));
|
|
68
|
+
}
|
|
69
|
+
// ─── Body Composition ─────────────────────────────────────────────────
|
|
70
|
+
/**
|
|
71
|
+
* Get body composition summaries.
|
|
72
|
+
*
|
|
73
|
+
* Garmin API: `GET /wellness-api/rest/bodyComps`
|
|
74
|
+
*/
|
|
75
|
+
async getBodyCompositions(params) {
|
|
76
|
+
return this.get("/wellness-api/rest/bodyComps", timeRangeQuery(params));
|
|
77
|
+
}
|
|
78
|
+
// ─── Menstrual Cycle ──────────────────────────────────────────────────
|
|
79
|
+
/**
|
|
80
|
+
* Get menstrual cycle data.
|
|
81
|
+
*
|
|
82
|
+
* Garmin API: `GET /wellness-api/rest/menstrualCycleData`
|
|
83
|
+
*/
|
|
84
|
+
async getMenstrualCycleData(params) {
|
|
85
|
+
return this.get("/wellness-api/rest/menstrualCycleData", timeRangeQuery(params));
|
|
86
|
+
}
|
|
87
|
+
// ─── Backfill ─────────────────────────────────────────────────────────
|
|
88
|
+
/**
|
|
89
|
+
* Request historical data backfill from Garmin.
|
|
90
|
+
*
|
|
91
|
+
* Garmin processes backfill requests asynchronously and delivers data
|
|
92
|
+
* via the configured webhook endpoint. Maximum range: 90 days per request.
|
|
93
|
+
*
|
|
94
|
+
* @param summaryType - The data type to backfill (e.g., "dailies", "activities", "sleeps", "bodyComps")
|
|
95
|
+
* @param params - Time range for the backfill
|
|
96
|
+
*/
|
|
97
|
+
async requestBackfill(summaryType, params) {
|
|
98
|
+
const query = timeRangeQuery(params);
|
|
99
|
+
await this.get(`/wellness-api/rest/backfill/${summaryType}`, query);
|
|
100
|
+
}
|
|
101
|
+
// ─── Internal ─────────────────────────────────────────────────────────
|
|
102
|
+
async get(path, queryParams) {
|
|
103
|
+
const fullUrl = `${this.baseUrl}${path}`;
|
|
104
|
+
const qs = queryParams
|
|
105
|
+
? `?${new URLSearchParams(queryParams).toString()}`
|
|
106
|
+
: "";
|
|
107
|
+
const requestUrl = `${fullUrl}${qs}`;
|
|
108
|
+
const nonce = generateNonce();
|
|
109
|
+
const timestamp = getTimestamp();
|
|
110
|
+
const oauthParams = {
|
|
111
|
+
oauth_consumer_key: this.consumerKey,
|
|
112
|
+
oauth_nonce: nonce,
|
|
113
|
+
oauth_signature_method: "HMAC-SHA1",
|
|
114
|
+
oauth_timestamp: timestamp,
|
|
115
|
+
oauth_token: this.accessToken,
|
|
116
|
+
oauth_version: "1.0",
|
|
117
|
+
};
|
|
118
|
+
// OAuth signature must include both OAuth params and query params
|
|
119
|
+
const allParams = { ...oauthParams, ...(queryParams ?? {}) };
|
|
120
|
+
const signature = await buildOAuthSignature("GET", fullUrl, allParams, this.consumerSecret, this.tokenSecret);
|
|
121
|
+
oauthParams.oauth_signature = signature;
|
|
122
|
+
const response = await fetch(requestUrl, {
|
|
123
|
+
method: "GET",
|
|
124
|
+
headers: {
|
|
125
|
+
Authorization: buildOAuthHeader(oauthParams),
|
|
126
|
+
Accept: "application/json",
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
if (!response.ok) {
|
|
130
|
+
const body = await response.text().catch(() => "");
|
|
131
|
+
throw new GarminApiError(`Garmin API error: ${response.status} ${response.statusText}`, response.status, body);
|
|
132
|
+
}
|
|
133
|
+
return (await response.json());
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function timeRangeQuery(params) {
|
|
137
|
+
return {
|
|
138
|
+
uploadStartTimeInSeconds: String(params.uploadStartTimeInSeconds),
|
|
139
|
+
uploadEndTimeInSeconds: String(params.uploadEndTimeInSeconds),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
// ─── Error ────────────────────────────────────────────────────────────────────
|
|
143
|
+
export class GarminApiError extends Error {
|
|
144
|
+
status;
|
|
145
|
+
body;
|
|
146
|
+
constructor(message, status, body) {
|
|
147
|
+
super(message);
|
|
148
|
+
this.status = status;
|
|
149
|
+
this.body = body;
|
|
150
|
+
this.name = "GarminApiError";
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/garmin/client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { GarminDailySummary } from "./types.js";
|
|
2
|
+
export type DailyData = ReturnType<typeof transformDaily>;
|
|
3
|
+
/**
|
|
4
|
+
* Transform a Garmin daily summary into a Soma Daily document shape.
|
|
5
|
+
*
|
|
6
|
+
* @param daily - The Garmin daily summary from the Health API
|
|
7
|
+
* @returns Soma Daily fields (without connectionId/userId)
|
|
8
|
+
*/
|
|
9
|
+
export declare function transformDaily(daily: GarminDailySummary): {
|
|
10
|
+
metadata: {
|
|
11
|
+
start_time: string;
|
|
12
|
+
end_time: string;
|
|
13
|
+
upload_type: number;
|
|
14
|
+
};
|
|
15
|
+
active_durations_data: {
|
|
16
|
+
activity_seconds: number | undefined;
|
|
17
|
+
moderate_intensity_seconds: number | undefined;
|
|
18
|
+
vigorous_intensity_seconds: number | undefined;
|
|
19
|
+
} | undefined;
|
|
20
|
+
calories_data: {
|
|
21
|
+
net_activity_calories: number | undefined;
|
|
22
|
+
BMR_calories: number | undefined;
|
|
23
|
+
total_burned_calories: number | undefined;
|
|
24
|
+
} | undefined;
|
|
25
|
+
distance_data: {
|
|
26
|
+
distance_meters: number | undefined;
|
|
27
|
+
steps: number | undefined;
|
|
28
|
+
floors_climbed: number | undefined;
|
|
29
|
+
} | undefined;
|
|
30
|
+
heart_rate_data: {
|
|
31
|
+
summary: {
|
|
32
|
+
avg_hr_bpm: number | undefined;
|
|
33
|
+
max_hr_bpm: number | undefined;
|
|
34
|
+
min_hr_bpm: number | undefined;
|
|
35
|
+
resting_hr_bpm: number | undefined;
|
|
36
|
+
} | undefined;
|
|
37
|
+
detailed: {
|
|
38
|
+
hr_samples: ({
|
|
39
|
+
timestamp: string;
|
|
40
|
+
} & {
|
|
41
|
+
bpm: number;
|
|
42
|
+
})[];
|
|
43
|
+
} | undefined;
|
|
44
|
+
} | undefined;
|
|
45
|
+
oxygen_data: {
|
|
46
|
+
avg_saturation_percentage: number | undefined;
|
|
47
|
+
saturation_samples: ({
|
|
48
|
+
timestamp: string;
|
|
49
|
+
} & {
|
|
50
|
+
percentage: number;
|
|
51
|
+
})[] | undefined;
|
|
52
|
+
} | undefined;
|
|
53
|
+
stress_data: {
|
|
54
|
+
avg_stress_level: number | undefined;
|
|
55
|
+
max_stress_level: number | undefined;
|
|
56
|
+
stress_duration_seconds: number | undefined;
|
|
57
|
+
rest_stress_duration_seconds: number | undefined;
|
|
58
|
+
activity_stress_duration_seconds: number | undefined;
|
|
59
|
+
low_stress_duration_seconds: number | undefined;
|
|
60
|
+
medium_stress_duration_seconds: number | undefined;
|
|
61
|
+
high_stress_duration_seconds: number | undefined;
|
|
62
|
+
samples: ({
|
|
63
|
+
timestamp: string;
|
|
64
|
+
} & {
|
|
65
|
+
level: number;
|
|
66
|
+
})[] | undefined;
|
|
67
|
+
body_battery_samples: ({
|
|
68
|
+
timestamp: string;
|
|
69
|
+
} & {
|
|
70
|
+
level: number;
|
|
71
|
+
})[] | undefined;
|
|
72
|
+
} | undefined;
|
|
73
|
+
};
|
|
74
|
+
//# sourceMappingURL=daily.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"daily.d.ts","sourceRoot":"","sources":["../../src/garmin/daily.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAErD,MAAM,MAAM,SAAS,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AAE1D;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2BA8LlC,MAAM;;;;;;;;;uBAAN,MAAM;;;;;;;;;;;;;;;uBAAN,MAAM;;;;;uBAAN,MAAM;;;;;EAvK3B"}
|