@torqlab/strava-api 1.0.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.
Files changed (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +167 -0
  3. package/dist/client/client.d.ts +34 -0
  4. package/dist/client/client.d.ts.map +1 -0
  5. package/dist/client/create-error/create-error.d.ts +13 -0
  6. package/dist/client/create-error/create-error.d.ts.map +1 -0
  7. package/dist/client/create-error/index.d.ts +2 -0
  8. package/dist/client/create-error/index.d.ts.map +1 -0
  9. package/dist/client/get-auth-headers/get-auth-headers.d.ts +22 -0
  10. package/dist/client/get-auth-headers/get-auth-headers.d.ts.map +1 -0
  11. package/dist/client/get-auth-headers/index.d.ts +2 -0
  12. package/dist/client/get-auth-headers/index.d.ts.map +1 -0
  13. package/dist/client/handle-fetch-error/handle-fetch-error.d.ts +19 -0
  14. package/dist/client/handle-fetch-error/handle-fetch-error.d.ts.map +1 -0
  15. package/dist/client/handle-fetch-error/index.d.ts +2 -0
  16. package/dist/client/handle-fetch-error/index.d.ts.map +1 -0
  17. package/dist/client/handle-rate-limit/constants.d.ts +3 -0
  18. package/dist/client/handle-rate-limit/constants.d.ts.map +1 -0
  19. package/dist/client/handle-rate-limit/handle-rate-limit.d.ts +32 -0
  20. package/dist/client/handle-rate-limit/handle-rate-limit.d.ts.map +1 -0
  21. package/dist/client/handle-rate-limit/index.d.ts +2 -0
  22. package/dist/client/handle-rate-limit/index.d.ts.map +1 -0
  23. package/dist/client/handle-retry/handle-retry.d.ts +30 -0
  24. package/dist/client/handle-retry/handle-retry.d.ts.map +1 -0
  25. package/dist/client/handle-retry/index.d.ts +2 -0
  26. package/dist/client/handle-retry/index.d.ts.map +1 -0
  27. package/dist/client/handle-retry/types.d.ts +7 -0
  28. package/dist/client/handle-retry/types.d.ts.map +1 -0
  29. package/dist/client/index.d.ts +4 -0
  30. package/dist/client/index.d.ts.map +1 -0
  31. package/dist/client/parse-error/index.d.ts +2 -0
  32. package/dist/client/parse-error/index.d.ts.map +1 -0
  33. package/dist/client/parse-error/parse-error.d.ts +14 -0
  34. package/dist/client/parse-error/parse-error.d.ts.map +1 -0
  35. package/dist/client/parse-error-code/index.d.ts +2 -0
  36. package/dist/client/parse-error-code/index.d.ts.map +1 -0
  37. package/dist/client/parse-error-code/parse-error-code.d.ts +12 -0
  38. package/dist/client/parse-error-code/parse-error-code.d.ts.map +1 -0
  39. package/dist/client/parse-response/index.d.ts +2 -0
  40. package/dist/client/parse-response/index.d.ts.map +1 -0
  41. package/dist/client/parse-response/parse-response.d.ts +30 -0
  42. package/dist/client/parse-response/parse-response.d.ts.map +1 -0
  43. package/dist/constants.d.ts +35 -0
  44. package/dist/constants.d.ts.map +1 -0
  45. package/dist/exchange-token/exchange-token.d.ts +36 -0
  46. package/dist/exchange-token/exchange-token.d.ts.map +1 -0
  47. package/dist/exchange-token/index.d.ts +2 -0
  48. package/dist/exchange-token/index.d.ts.map +1 -0
  49. package/dist/fetch-activities/fetch-activities.d.ts +39 -0
  50. package/dist/fetch-activities/fetch-activities.d.ts.map +1 -0
  51. package/dist/fetch-activities/index.d.ts +2 -0
  52. package/dist/fetch-activities/index.d.ts.map +1 -0
  53. package/dist/fetch-activity/fetch-activity.d.ts +49 -0
  54. package/dist/fetch-activity/fetch-activity.d.ts.map +1 -0
  55. package/dist/fetch-activity/index.d.ts +2 -0
  56. package/dist/fetch-activity/index.d.ts.map +1 -0
  57. package/dist/get-auth-url/get-auth-url.d.ts +30 -0
  58. package/dist/get-auth-url/get-auth-url.d.ts.map +1 -0
  59. package/dist/get-auth-url/index.d.ts +2 -0
  60. package/dist/get-auth-url/index.d.ts.map +1 -0
  61. package/dist/index.cjs +473 -0
  62. package/dist/index.d.ts +6 -0
  63. package/dist/index.d.ts.map +1 -0
  64. package/dist/index.mjs +431 -0
  65. package/dist/refresh-token/index.d.ts +2 -0
  66. package/dist/refresh-token/index.d.ts.map +1 -0
  67. package/dist/refresh-token/refresh-token.d.ts +34 -0
  68. package/dist/refresh-token/refresh-token.d.ts.map +1 -0
  69. package/dist/refresh-token/types.d.ts +5 -0
  70. package/dist/refresh-token/types.d.ts.map +1 -0
  71. package/dist/types.d.ts +192 -0
  72. package/dist/types.d.ts.map +1 -0
  73. package/dist/validators/index.d.ts +2 -0
  74. package/dist/validators/index.d.ts.map +1 -0
  75. package/dist/validators/validate-activity-id/index.d.ts +2 -0
  76. package/dist/validators/validate-activity-id/index.d.ts.map +1 -0
  77. package/dist/validators/validate-activity-id/validate-activity-id.d.ts +22 -0
  78. package/dist/validators/validate-activity-id/validate-activity-id.d.ts.map +1 -0
  79. package/package.json +69 -0
@@ -0,0 +1,39 @@
1
+ import type { StravaApiConfig, StravaActivity } from '../types';
2
+ /**
3
+ * Fetches a list of activities from Strava API for the authenticated athlete.
4
+ *
5
+ * Main entry point for fetching activities list. Orchestrates the complete flow:
6
+ * fetches data from Strava API with retry logic, handles rate limiting and token
7
+ * refresh, and returns the raw Strava API response format.
8
+ *
9
+ * This function is typically called to retrieve a list of activities for display
10
+ * or processing purposes.
11
+ *
12
+ * The function implements the following flow:
13
+ * 1. Fetches from API with automatic retry on retryable errors
14
+ * 2. Handles rate limiting by waiting before retry
15
+ * 3. Attempts token refresh on 401 errors (if refresh token available)
16
+ * 4. Returns raw API response array
17
+ *
18
+ * @param {StravaApiConfig} config - Strava API configuration including OAuth tokens.
19
+ * @returns {Promise<StravaActivityApiResponse[]>} Promise resolving to array of activities in raw Strava API format.
20
+ * @throws {Error} Throws an error with StravaActivityError structure for various failure scenarios:
21
+ * - 'UNAUTHORIZED' (not retryable): Authentication failed (after refresh attempt if applicable)
22
+ * - 'FORBIDDEN' (not retryable): Insufficient permissions
23
+ * - 'RATE_LIMITED' (retryable): Rate limit exceeded (handled with retry)
24
+ * - 'SERVER_ERROR' (retryable): Strava API server error (handled with retry)
25
+ * - 'NETWORK_ERROR' (retryable): Network connection failure (handled with retry)
26
+ * - 'MALFORMED_RESPONSE' (not retryable): Invalid API response format
27
+ *
28
+ * @see {@link https://developers.strava.com/docs/reference/#api-Activities-getLoggedInAthleteActivities | Strava Get Activities API}
29
+ *
30
+ * @example
31
+ * ```typescript
32
+ * const activities = await fetchActivities({
33
+ * accessToken: 'abc123',
34
+ * });
35
+ * ```
36
+ */
37
+ declare const fetchActivities: (config: StravaApiConfig) => Promise<StravaActivity[]>;
38
+ export default fetchActivities;
39
+ //# sourceMappingURL=fetch-activities.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetch-activities.d.ts","sourceRoot":"","sources":["../../fetch-activities/fetch-activities.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAEhE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,QAAA,MAAM,eAAe,GAAU,QAAQ,eAAe,KAAG,OAAO,CAAC,cAAc,EAAE,CAa7E,CAAC;AAEL,eAAe,eAAe,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { default } from './fetch-activities';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../fetch-activities/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC"}
@@ -0,0 +1,49 @@
1
+ import type { StravaApiConfig, StravaActivity } from '../types';
2
+ /**
3
+ * Fetches and validates a Strava activity by ID.
4
+ *
5
+ * Main entry point for the Activity module. Orchestrates the complete flow:
6
+ * validates activity ID, fetches data from Strava API with retry logic,
7
+ * handles rate limiting and token refresh, transforms the response to internal
8
+ * format, and validates through Activity Guardrails if provided.
9
+ *
10
+ * This function is typically called when a Strava webhook notification is
11
+ * received containing an activity ID, initiating the activity processing pipeline.
12
+ *
13
+ * The function implements the following flow:
14
+ * 1. Validates activity ID format
15
+ * 2. Fetches from API with automatic retry on retryable errors
16
+ * 3. Handles rate limiting by waiting before retry
17
+ * 4. Attempts token refresh on 401 errors (if refresh token available)
18
+ * 5. Transforms API response to internal format
19
+ * 6. Validates through Activity Guardrails (if provided)
20
+ * 7. Returns validated activity
21
+ *
22
+ * @param {string} activityId - Activity ID from Strava (typically received via webhook)
23
+ * @param {ActivityConfig} config - Activity module configuration including OAuth tokens and optional guardrails
24
+ * @returns {Promise<Activity>} Promise resolving to validated Activity object in internal format
25
+ * @throws {Error} Throws an error with ActivityError structure for various failure scenarios:
26
+ * - 'INVALID_ID' (not retryable): Invalid activity ID format
27
+ * - 'NOT_FOUND' (not retryable): Activity doesn't exist
28
+ * - 'UNAUTHORIZED' (not retryable): Authentication failed (after refresh attempt if applicable)
29
+ * - 'FORBIDDEN' (not retryable): Insufficient permissions
30
+ * - 'RATE_LIMITED' (retryable): Rate limit exceeded (handled with retry)
31
+ * - 'SERVER_ERROR' (retryable): Strava API server error (handled with retry)
32
+ * - 'NETWORK_ERROR' (retryable): Network connection failure (handled with retry)
33
+ * - 'VALIDATION_FAILED' (not retryable): Activity Guardrails validation failed
34
+ * - 'MALFORMED_RESPONSE' (not retryable): Invalid API response format
35
+ *
36
+ * @see {@link https://developers.strava.com/docs/reference/#api-Activities-getActivityById | Strava Get Activity API}
37
+ * @see {@link https://developers.strava.com/docs/webhooks/ | Strava Webhooks}
38
+ *
39
+ * @example
40
+ * ```typescript
41
+ * const activity = await fetchActivity('123456789', {
42
+ * accessToken: 'abc123',
43
+ * guardrails: activityGuardrailsInstance
44
+ * });
45
+ * ```
46
+ */
47
+ declare const fetchActivity: (activityId: string, config: StravaApiConfig) => Promise<StravaActivity | null>;
48
+ export default fetchActivity;
49
+ //# sourceMappingURL=fetch-activity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetch-activity.d.ts","sourceRoot":"","sources":["../../fetch-activity/fetch-activity.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAsChE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,QAAA,MAAM,aAAa,GACjB,YAAY,MAAM,EAClB,QAAQ,eAAe,KACtB,OAAO,CAAC,cAAc,GAAG,IAAI,CAa/B,CAAC;AAEF,eAAe,aAAa,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { default } from './fetch-activity';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../fetch-activity/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC"}
@@ -0,0 +1,30 @@
1
+ import type { StravaAuthConfig } from '../types';
2
+ /**
3
+ * Creates the Strava OAuth2 authorization URL.
4
+ *
5
+ * Creates a URL that users can visit to authorize the application and grant
6
+ * access to their Strava data. After authorization, Strava redirects to the
7
+ * configured redirect URI with an authorization code that can be exchanged for tokens.
8
+ *
9
+ * @param {StravaAuthConfig} config - OAuth2 configuration including client ID, redirect URI, and optional scope.
10
+ * @returns {string} Complete authorization URL ready to be opened in a browser.
11
+ *
12
+ * @throws {Error} Throws an error if configuration is invalid:
13
+ * - 'INVALID_CONFIG': Missing required configuration (`clientId` or `redirectUri`).
14
+ *
15
+ * @see {@link https://developers.strava.com/docs/authentication/ | Strava API Authentication}
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * const url = getAuthUrl({
20
+ * clientId: '12345',
21
+ * clientSecret: 'secret',
22
+ * redirectUri: 'http://localhost',
23
+ * scope: 'activity:read'
24
+ * });
25
+ * // Returns: https://www.strava.com/oauth/authorize?client_id=12345&...
26
+ * ```
27
+ */
28
+ declare const getAuthUrl: (config: StravaAuthConfig) => string;
29
+ export default getAuthUrl;
30
+ //# sourceMappingURL=get-auth-url.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"get-auth-url.d.ts","sourceRoot":"","sources":["../../get-auth-url/get-auth-url.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAIjD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,QAAA,MAAM,UAAU,GAAI,QAAQ,gBAAgB,KAAG,MAiB9C,CAAC;AAEF,eAAe,UAAU,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { default } from './get-auth-url';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../get-auth-url/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC"}
package/dist/index.cjs ADDED
@@ -0,0 +1,473 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ function __accessProp(key) {
6
+ return this[key];
7
+ }
8
+ var __toCommonJS = (from) => {
9
+ var entry = (__moduleCache ??= new WeakMap).get(from), desc;
10
+ if (entry)
11
+ return entry;
12
+ entry = __defProp({}, "__esModule", { value: true });
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (var key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(entry, key))
16
+ __defProp(entry, key, {
17
+ get: __accessProp.bind(from, key),
18
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
19
+ });
20
+ }
21
+ __moduleCache.set(from, entry);
22
+ return entry;
23
+ };
24
+ var __moduleCache;
25
+ var __returnValue = (v) => v;
26
+ function __exportSetter(name, newValue) {
27
+ this[name] = __returnValue.bind(null, newValue);
28
+ }
29
+ var __export = (target, all) => {
30
+ for (var name in all)
31
+ __defProp(target, name, {
32
+ get: all[name],
33
+ enumerable: true,
34
+ configurable: true,
35
+ set: __exportSetter.bind(all, name)
36
+ });
37
+ };
38
+
39
+ // index.ts
40
+ var exports_strava_api = {};
41
+ __export(exports_strava_api, {
42
+ refreshStravaAuthToken: () => refresh_token_default,
43
+ getStravaAuthUrl: () => get_auth_url_default,
44
+ fetchStravaActivity: () => fetch_activity_default,
45
+ fetchStravaActivities: () => fetch_activities_default,
46
+ exchangeStravaAuthToken: () => exchange_token_default
47
+ });
48
+ module.exports = __toCommonJS(exports_strava_api);
49
+
50
+ // constants.ts
51
+ var STRAVA_API_INITIAL_BACKOFF_MS = 1000;
52
+ var STRAVA_API_MAX_BACKOFF_MS = 16000;
53
+ var STRAVA_API_MAX_RETRIES = 3;
54
+ var STRAVA_AUTH_DEFAULT_SCOPE = "activity:read";
55
+ var STRAVA_OAUTH_BASE_URL = "https://www.strava.com/oauth";
56
+ var STRAVA_API_BASE_URL = "https://www.strava.com/api/v3";
57
+ var STRAVA_API_ENDPOINTS = {
58
+ TOKEN: `${STRAVA_OAUTH_BASE_URL}/token`,
59
+ AUTH: `${STRAVA_OAUTH_BASE_URL}/authorize`,
60
+ ACTIVITIES: `${STRAVA_API_BASE_URL}/athlete/activities`,
61
+ ACTIVITY: `${STRAVA_API_BASE_URL}/activities`
62
+ };
63
+ var STRAVA_API_ERROR_CODES = {
64
+ UNAUTHORIZED: "UNAUTHORIZED",
65
+ FORBIDDEN: "FORBIDDEN",
66
+ RATE_LIMITED: "RATE_LIMITED",
67
+ SERVER_ERROR: "SERVER_ERROR",
68
+ MALFORMED_RESPONSE: "MALFORMED_RESPONSE",
69
+ INVALID_ID: "INVALID_ID",
70
+ NOT_FOUND: "NOT_FOUND",
71
+ NETWORK_ERROR: "NETWORK_ERROR",
72
+ VALIDATION_FAILED: "VALIDATION_FAILED",
73
+ INVALID_CONFIG: "INVALID_CONFIG",
74
+ INVALID_CODE: "INVALID_CODE"
75
+ };
76
+ var STRAVA_API_STATUS_CODES = {
77
+ UNAUTHORIZED: 401,
78
+ FORBIDDEN: 403,
79
+ RATE_LIMITED: 429,
80
+ SERVER_ERROR: 500
81
+ };
82
+
83
+ // validators/validate-activity-id/validate-activity-id.ts
84
+ var validateActivityId = (activityId) => {
85
+ const trimmedActivityId = String(activityId).trim();
86
+ const numericActivityId = Number(trimmedActivityId);
87
+ const isValidNumericActivityId = !Number.isNaN(numericActivityId) && Number.isFinite(numericActivityId) && numericActivityId > 0;
88
+ if (!trimmedActivityId) {
89
+ const error = {
90
+ code: "INVALID_ID",
91
+ message: "Activity ID is required and cannot be empty",
92
+ retryable: false
93
+ };
94
+ throw new Error(JSON.stringify(error));
95
+ } else if (!isValidNumericActivityId) {
96
+ const error = {
97
+ code: "INVALID_ID",
98
+ message: "Activity ID must be a valid positive number",
99
+ retryable: false
100
+ };
101
+ throw new Error(JSON.stringify(error));
102
+ }
103
+ };
104
+ var validate_activity_id_default = validateActivityId;
105
+ // client/get-auth-headers/get-auth-headers.ts
106
+ var getAuthHeaders = (config) => {
107
+ const headers = {
108
+ Authorization: `Bearer ${config.accessToken}`
109
+ };
110
+ return headers;
111
+ };
112
+ var get_auth_headers_default = getAuthHeaders;
113
+ // client/create-error/create-error.ts
114
+ var createError = (code, message, retryable = false, response) => {
115
+ if (response) {
116
+ const error = {
117
+ code,
118
+ message,
119
+ retryable
120
+ };
121
+ return new Error(JSON.stringify({ ...error, response }));
122
+ } else {
123
+ const error = {
124
+ code,
125
+ message,
126
+ retryable
127
+ };
128
+ return new Error(JSON.stringify(error));
129
+ }
130
+ };
131
+ var create_error_default = createError;
132
+ // client/handle-rate-limit/constants.ts
133
+ var DEFAULT_WAIT_MS = 60 * 1000;
134
+ var WINDOW_MS = 15 * 60 * 1000;
135
+
136
+ // client/handle-rate-limit/handle-rate-limit.ts
137
+ var handleRateLimit = async (response) => {
138
+ const retryAfterHeader = response.headers.get("Retry-After");
139
+ const retryAfterSeconds = retryAfterHeader ? Number.parseFloat(retryAfterHeader) : NaN;
140
+ const shouldWaitAndRetry = !Number.isNaN(retryAfterSeconds) && Number.isFinite(retryAfterSeconds) && retryAfterSeconds > 0;
141
+ if (shouldWaitAndRetry) {
142
+ const waitMs = Math.ceil(retryAfterSeconds * 1000);
143
+ await new Promise((resolve) => {
144
+ setTimeout(resolve, waitMs);
145
+ });
146
+ } else {
147
+ const rateLimitLimit = response.headers.get("X-RateLimit-Limit");
148
+ const rateLimitUsage = response.headers.get("X-RateLimit-Usage");
149
+ const limit = rateLimitLimit ? Number.parseInt(rateLimitLimit, 10) : NaN;
150
+ const usage = rateLimitUsage ? Number.parseInt(rateLimitUsage, 10) : NaN;
151
+ const shouldWaitInWindow = !Number.isNaN(limit) && !Number.isNaN(usage) && Number.isFinite(limit) && Number.isFinite(usage) && usage >= limit;
152
+ if (shouldWaitInWindow) {
153
+ await new Promise((resolve) => {
154
+ setTimeout(resolve, WINDOW_MS);
155
+ });
156
+ } else {
157
+ await new Promise((resolve) => {
158
+ setTimeout(resolve, DEFAULT_WAIT_MS);
159
+ });
160
+ }
161
+ }
162
+ };
163
+ var handle_rate_limit_default = handleRateLimit;
164
+ // client/parse-error/parse-error.ts
165
+ var parseError = (error) => {
166
+ try {
167
+ return JSON.parse(error.message);
168
+ } catch {
169
+ return null;
170
+ }
171
+ };
172
+ var parse_error_default = parseError;
173
+
174
+ // client/parse-error-code/parse-error-code.ts
175
+ var parseErrorCode = (error) => {
176
+ try {
177
+ const errorParsed = parse_error_default(error);
178
+ return errorParsed?.code || null;
179
+ } catch {
180
+ return null;
181
+ }
182
+ };
183
+ var parse_error_code_default = parseErrorCode;
184
+ // refresh-token/refresh-token.ts
185
+ var createError2 = (code, message) => {
186
+ const error = {
187
+ retryable: false,
188
+ code,
189
+ message
190
+ };
191
+ return new Error(JSON.stringify(error));
192
+ };
193
+ var doRefreshToken = async (body) => {
194
+ try {
195
+ return await fetch(STRAVA_API_ENDPOINTS.TOKEN, {
196
+ method: "POST",
197
+ body: body.toString(),
198
+ headers: {
199
+ "Content-Type": "application/x-www-form-urlencoded"
200
+ }
201
+ });
202
+ } catch {
203
+ throw createError2("NETWORK_ERROR", "Failed to connect to Strava OAuth endpoint");
204
+ }
205
+ };
206
+ var parseApiResponse = async (response) => {
207
+ try {
208
+ return await response.json();
209
+ } catch {
210
+ throw createError2("MALFORMED_RESPONSE", "Invalid response format from token refresh endpoint");
211
+ }
212
+ };
213
+ var refreshToken = async (config) => {
214
+ if (!config.refreshToken) {
215
+ throw createError2("UNAUTHORIZED", "Refresh token is not available");
216
+ } else if (!config.clientId || !config.clientSecret) {
217
+ throw createError2("UNAUTHORIZED", "Client ID and client secret are required for token refresh");
218
+ } else {
219
+ const body = new URLSearchParams({
220
+ client_id: config.clientId,
221
+ client_secret: config.clientSecret,
222
+ grant_type: "refresh_token",
223
+ refresh_token: config.refreshToken
224
+ });
225
+ const response = await doRefreshToken(body);
226
+ if (!response.ok) {
227
+ throw createError2("UNAUTHORIZED", "Token refresh failed");
228
+ } else {
229
+ const jsonData = await parseApiResponse(response.clone());
230
+ if (jsonData.access_token && jsonData.refresh_token) {
231
+ return {
232
+ access_token: jsonData.access_token,
233
+ refresh_token: jsonData.refresh_token
234
+ };
235
+ } else {
236
+ throw createError2("MALFORMED_RESPONSE", "Access token not found in refresh response");
237
+ }
238
+ }
239
+ }
240
+ };
241
+ var refresh_token_default = refreshToken;
242
+ // client/handle-fetch-error/handle-fetch-error.ts
243
+ var handleFetchError = async (fn, config, error) => {
244
+ const errorCode = parse_error_code_default(error);
245
+ const isRateLimitedError = errorCode === STRAVA_API_ERROR_CODES.RATE_LIMITED;
246
+ const isUnauthorizedError = errorCode === STRAVA_API_ERROR_CODES.UNAUTHORIZED;
247
+ if (isRateLimitedError) {
248
+ const errorWithResponse = error;
249
+ const rateLimitedResponse = errorWithResponse.response ?? new Response("Rate Limited", {
250
+ status: STRAVA_API_STATUS_CODES.RATE_LIMITED,
251
+ headers: { "Retry-After": "60" }
252
+ });
253
+ await handle_rate_limit_default(rateLimitedResponse);
254
+ throw error;
255
+ } else if (isUnauthorizedError) {
256
+ const canRefreshToken = Boolean(config.refreshToken) && Boolean(config.clientId) && Boolean(config.clientSecret);
257
+ if (canRefreshToken) {
258
+ try {
259
+ const { access_token } = await refresh_token_default(config);
260
+ const refreshedConfig = {
261
+ ...config,
262
+ accessToken: access_token
263
+ };
264
+ return await fn(refreshedConfig);
265
+ } catch {
266
+ throw error;
267
+ }
268
+ } else {
269
+ throw error;
270
+ }
271
+ } else {
272
+ throw error;
273
+ }
274
+ };
275
+ var handle_fetch_error_default = handleFetchError;
276
+ // client/parse-response/parse-response.ts
277
+ var parseApiResponse2 = async (response) => {
278
+ try {
279
+ const data = await response.json();
280
+ if (!data) {
281
+ throw create_error_default("MALFORMED_RESPONSE", "Expected non-empty response from Strava API", false);
282
+ } else {
283
+ return data;
284
+ }
285
+ } catch (error) {
286
+ const parsedError = parse_error_default(error);
287
+ if (parsedError) {
288
+ throw create_error_default("MALFORMED_RESPONSE", "Invalid response format from Strava API", false);
289
+ } else {
290
+ throw error;
291
+ }
292
+ }
293
+ };
294
+ var parseResponse = async (response) => {
295
+ const isUnauthorized = response.status === STRAVA_API_STATUS_CODES.UNAUTHORIZED;
296
+ const isForbidden = response.status === STRAVA_API_STATUS_CODES.FORBIDDEN;
297
+ const isRateLimited = response.status === STRAVA_API_STATUS_CODES.RATE_LIMITED;
298
+ const isServerError = response.status >= STRAVA_API_STATUS_CODES.SERVER_ERROR;
299
+ if (isUnauthorized) {
300
+ throw create_error_default("UNAUTHORIZED", "Authentication failed. Token may be expired or invalid.", false);
301
+ } else if (isForbidden) {
302
+ throw create_error_default("FORBIDDEN", "Insufficient permissions to access Strava API", false);
303
+ } else if (isRateLimited) {
304
+ throw create_error_default("RATE_LIMITED", "Rate limit exceeded. Please try again later.", true, response);
305
+ } else if (isServerError) {
306
+ throw create_error_default("SERVER_ERROR", "Strava API server error", true);
307
+ } else if (!response.ok) {
308
+ throw create_error_default("SERVER_ERROR", `Unexpected API error: ${response.status}`, false);
309
+ } else {
310
+ return parseApiResponse2(response);
311
+ }
312
+ };
313
+ var parse_response_default = parseResponse;
314
+ // client/client.ts
315
+ var doFetch = async (url, config) => {
316
+ try {
317
+ const response = await fetch(url, {
318
+ method: "GET",
319
+ headers: get_auth_headers_default(config)
320
+ });
321
+ return parse_response_default(response);
322
+ } catch {
323
+ throw create_error_default("NETWORK_ERROR", "Failed to connect to Strava API", true);
324
+ }
325
+ };
326
+ var client = async (url, config) => {
327
+ try {
328
+ return await doFetch(url, config);
329
+ } catch (error) {
330
+ return await handle_fetch_error_default((newConfig) => doFetch(url, newConfig), config, error);
331
+ }
332
+ };
333
+ var client_default = client;
334
+ // client/handle-retry/handle-retry.ts
335
+ var parseError2 = (error) => {
336
+ try {
337
+ return JSON.parse(error.message);
338
+ } catch {
339
+ return null;
340
+ }
341
+ };
342
+ var attemptWithBackoff = async (fn, attemptIndex, maxRetries, currentBackoffMs, _previousError) => {
343
+ try {
344
+ return await fn();
345
+ } catch (error) {
346
+ const currentError = error;
347
+ const activityError = parseError2(currentError);
348
+ if (activityError !== null && activityError.retryable === false) {
349
+ throw currentError;
350
+ } else {
351
+ const isLastAttempt = attemptIndex >= maxRetries;
352
+ if (isLastAttempt) {
353
+ throw currentError;
354
+ } else {
355
+ const delayMs = Math.min(currentBackoffMs, STRAVA_API_MAX_BACKOFF_MS);
356
+ await new Promise((resolve) => {
357
+ setTimeout(resolve, delayMs);
358
+ });
359
+ const nextBackoffMs = currentBackoffMs * 2;
360
+ const nextAttemptIndex = attemptIndex + 1;
361
+ return attemptWithBackoff(fn, nextAttemptIndex, maxRetries, nextBackoffMs, currentError);
362
+ }
363
+ }
364
+ }
365
+ };
366
+ var handleRetry = async ({
367
+ fn,
368
+ maxRetries,
369
+ initialBackoffMs = STRAVA_API_INITIAL_BACKOFF_MS
370
+ }) => attemptWithBackoff(fn, 0, maxRetries, initialBackoffMs, null);
371
+ var handle_retry_default = handleRetry;
372
+ // fetch-activity/fetch-activity.ts
373
+ var fetchActivityWithValidation = async (config, activityId) => {
374
+ const activity = await client_default(`/activities/${activityId}`, config);
375
+ if (config.guardrails) {
376
+ const validationResult = activity ? config.guardrails.validate(activity) : { valid: false, errors: ["Activity is empty"] };
377
+ if (!validationResult.valid) {
378
+ const error = create_error_default("VALIDATION_FAILED", validationResult.errors?.join(", ") ?? "Activity validation failed", false);
379
+ throw new Error(JSON.stringify(error));
380
+ }
381
+ }
382
+ return activity;
383
+ };
384
+ var fetchActivity = async (activityId, config) => {
385
+ validate_activity_id_default(activityId);
386
+ return handle_retry_default({
387
+ maxRetries: STRAVA_API_MAX_RETRIES,
388
+ initialBackoffMs: STRAVA_API_INITIAL_BACKOFF_MS,
389
+ fn: async () => fetchActivityWithValidation(config, activityId)
390
+ });
391
+ };
392
+ var fetch_activity_default = fetchActivity;
393
+ // fetch-activities/fetch-activities.ts
394
+ var fetchActivities = async (config) => handle_retry_default({
395
+ maxRetries: STRAVA_API_MAX_RETRIES,
396
+ initialBackoffMs: STRAVA_API_INITIAL_BACKOFF_MS,
397
+ fn: () => client_default(STRAVA_API_ENDPOINTS.ACTIVITIES, config)
398
+ });
399
+ var fetch_activities_default = fetchActivities;
400
+ // get-auth-url/get-auth-url.ts
401
+ var getAuthUrl = (config) => {
402
+ if (!config.clientId) {
403
+ throw create_error_default("INVALID_CONFIG", "Client ID is required");
404
+ } else if (!config.redirectUri) {
405
+ throw create_error_default("INVALID_CONFIG", "Redirect URI is required");
406
+ } else {
407
+ const scope = config.scope ?? STRAVA_AUTH_DEFAULT_SCOPE;
408
+ const params = new URLSearchParams({
409
+ client_id: config.clientId,
410
+ response_type: "code",
411
+ redirect_uri: config.redirectUri,
412
+ scope,
413
+ approval_prompt: "force"
414
+ });
415
+ return `${STRAVA_API_ENDPOINTS.AUTH}?${params.toString()}`;
416
+ }
417
+ };
418
+ var get_auth_url_default = getAuthUrl;
419
+ // exchange-token/exchange-token.ts
420
+ var fetchTokenResponse = async (body) => {
421
+ try {
422
+ return await fetch(STRAVA_API_ENDPOINTS.TOKEN, {
423
+ method: "POST",
424
+ body: body.toString(),
425
+ headers: {
426
+ "Content-Type": "application/x-www-form-urlencoded"
427
+ }
428
+ });
429
+ } catch {
430
+ throw create_error_default("NETWORK_ERROR", "Failed to connect to Strava OAuth endpoint");
431
+ }
432
+ };
433
+ var parseTokenResponse = async (response) => {
434
+ try {
435
+ return await response.json();
436
+ } catch {
437
+ throw create_error_default("MALFORMED_RESPONSE", "Invalid response format from token endpoint");
438
+ }
439
+ };
440
+ var exchangeToken = async (authorizationCode, config) => {
441
+ if (!config.clientId || !config.clientSecret) {
442
+ throw create_error_default("INVALID_CONFIG", "Client ID and client secret are required");
443
+ } else if (!config.redirectUri) {
444
+ throw create_error_default("INVALID_CONFIG", "Redirect URI is required");
445
+ } else if (!authorizationCode) {
446
+ throw create_error_default("INVALID_CODE", "Authorization code is required");
447
+ } else {
448
+ const body = new URLSearchParams({
449
+ client_id: config.clientId,
450
+ client_secret: config.clientSecret,
451
+ code: authorizationCode,
452
+ grant_type: "authorization_code"
453
+ });
454
+ const response = await fetchTokenResponse(body);
455
+ if (response.ok) {
456
+ const tokenData = await parseTokenResponse(response.clone());
457
+ if (!tokenData.access_token) {
458
+ throw create_error_default("MALFORMED_RESPONSE", "Access token not found in response");
459
+ } else if (!tokenData.refresh_token) {
460
+ throw create_error_default("MALFORMED_RESPONSE", "Refresh token not found in response");
461
+ } else {
462
+ return tokenData;
463
+ }
464
+ } else {
465
+ if (response.status === 401 || response.status === 403) {
466
+ throw create_error_default("UNAUTHORIZED", "Invalid client credentials or authorization code");
467
+ } else {
468
+ throw create_error_default("UNAUTHORIZED", `Token exchange failed with status ${response.status}`);
469
+ }
470
+ }
471
+ }
472
+ };
473
+ var exchange_token_default = exchangeToken;
@@ -0,0 +1,6 @@
1
+ export { default as fetchStravaActivity } from './fetch-activity';
2
+ export { default as fetchStravaActivities } from './fetch-activities';
3
+ export { default as getStravaAuthUrl } from './get-auth-url';
4
+ export { default as exchangeStravaAuthToken } from './exchange-token';
5
+ export { default as refreshStravaAuthToken } from './refresh-token';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAClE,OAAO,EAAE,OAAO,IAAI,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AACtE,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAC7D,OAAO,EAAE,OAAO,IAAI,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AACtE,OAAO,EAAE,OAAO,IAAI,sBAAsB,EAAE,MAAM,iBAAiB,CAAC"}