@nativesquare/soma 0.1.2 → 0.3.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/README.md +260 -19
- package/dist/client/index.d.ts +158 -4
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +165 -3
- 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 +37 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/public.d.ts +3 -3
- package/dist/component/schema.d.ts +18 -5
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +10 -0
- package/dist/component/schema.js.map +1 -1
- package/dist/component/strava.d.ts +88 -0
- package/dist/component/strava.d.ts.map +1 -0
- package/dist/component/strava.js +318 -0
- package/dist/component/strava.js.map +1 -0
- package/dist/component/validators/activity.d.ts +4 -4
- package/dist/component/validators/samples.d.ts +2 -2
- package/dist/strava/activity.d.ts +121 -0
- package/dist/strava/activity.d.ts.map +1 -0
- package/dist/strava/activity.js +201 -0
- package/dist/strava/activity.js.map +1 -0
- package/dist/strava/athlete.d.ts +34 -0
- package/dist/strava/athlete.d.ts.map +1 -0
- package/dist/strava/athlete.js +39 -0
- package/dist/strava/athlete.js.map +1 -0
- package/dist/strava/auth.d.ts +103 -0
- package/dist/strava/auth.d.ts.map +1 -0
- package/dist/strava/auth.js +111 -0
- package/dist/strava/auth.js.map +1 -0
- package/dist/strava/client.d.ts +93 -0
- package/dist/strava/client.d.ts.map +1 -0
- package/dist/strava/client.js +158 -0
- package/dist/strava/client.js.map +1 -0
- package/dist/strava/index.d.ts +13 -0
- package/dist/strava/index.d.ts.map +1 -0
- package/dist/strava/index.js +17 -0
- package/dist/strava/index.js.map +1 -0
- package/dist/strava/maps/sport-type.d.ts +7 -0
- package/dist/strava/maps/sport-type.d.ts.map +1 -0
- package/dist/strava/maps/sport-type.js +84 -0
- package/dist/strava/maps/sport-type.js.map +1 -0
- package/dist/strava/sync.d.ts +104 -0
- package/dist/strava/sync.d.ts.map +1 -0
- package/dist/strava/sync.js +87 -0
- package/dist/strava/sync.js.map +1 -0
- package/dist/strava/types.d.ts +266 -0
- package/dist/strava/types.d.ts.map +1 -0
- package/dist/strava/types.js +8 -0
- package/dist/strava/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 +212 -4
- package/src/component/_generated/api.ts +2 -0
- package/src/component/_generated/component.ts +49 -0
- package/src/component/schema.ts +11 -0
- package/src/component/strava.ts +383 -0
- package/src/strava/activity.test.ts +415 -0
- package/src/strava/activity.ts +276 -0
- package/src/strava/athlete.test.ts +139 -0
- package/src/strava/athlete.ts +47 -0
- package/src/strava/auth.test.ts +78 -0
- package/src/strava/auth.ts +185 -0
- package/src/strava/client.ts +212 -0
- package/src/strava/index.ts +54 -0
- package/src/strava/maps/sport-type.test.ts +69 -0
- package/src/strava/maps/sport-type.ts +99 -0
- package/src/strava/sync.ts +168 -0
- package/src/strava/types.ts +361 -0
- package/src/validators.ts +89 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// ─── Activity Transformer ────────────────────────────────────────────────────
|
|
2
|
+
// Transforms a Strava DetailedActivity (+ optional streams/laps) into the
|
|
3
|
+
// Soma Activity schema shape.
|
|
4
|
+
import { mapSportType } from "./maps/sport-type.js";
|
|
5
|
+
/**
|
|
6
|
+
* Transform a Strava activity into a Soma Activity document shape.
|
|
7
|
+
*
|
|
8
|
+
* The returned object is ready to be spread into an `ingestActivity` call
|
|
9
|
+
* alongside `connectionId` and `userId`.
|
|
10
|
+
*
|
|
11
|
+
* Accepts either a DetailedActivity (from `GET /activities/{id}`) or a
|
|
12
|
+
* SummaryActivity (from `GET /athlete/activities`). When a DetailedActivity
|
|
13
|
+
* is provided, additional fields like `calories`, `segment_efforts`, and
|
|
14
|
+
* embedded `laps` are mapped. Optional streams and laps can also be supplied
|
|
15
|
+
* for time-series data (heart rate, power, position, etc.).
|
|
16
|
+
*
|
|
17
|
+
* @param activity - The Strava activity (summary or detailed)
|
|
18
|
+
* @param opts - Optional streams and laps data
|
|
19
|
+
* @returns Soma Activity fields (without connectionId/userId)
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* const data = transformActivity(stravaActivity, { streams, laps });
|
|
24
|
+
* await soma.ingestActivity(ctx, { connectionId, userId, ...data });
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export function transformActivity(activity, opts) {
|
|
28
|
+
const streams = opts?.streams;
|
|
29
|
+
const laps = opts?.laps ?? (isDetailed(activity) ? activity.laps : undefined);
|
|
30
|
+
const timeStream = streams?.time?.data;
|
|
31
|
+
const startDate = activity.start_date;
|
|
32
|
+
return {
|
|
33
|
+
metadata: {
|
|
34
|
+
summary_id: String(activity.id),
|
|
35
|
+
start_time: activity.start_date,
|
|
36
|
+
end_time: computeEndTime(activity.start_date, activity.elapsed_time),
|
|
37
|
+
type: mapSportType(activity.sport_type),
|
|
38
|
+
upload_type: activity.manual ? 2 : 1, // 2=Manual, 1=Automatic
|
|
39
|
+
name: activity.name,
|
|
40
|
+
city: activity.location_city ?? undefined,
|
|
41
|
+
state: activity.location_state ?? undefined,
|
|
42
|
+
country: activity.location_country ?? undefined,
|
|
43
|
+
},
|
|
44
|
+
active_durations_data: {
|
|
45
|
+
activity_seconds: activity.moving_time,
|
|
46
|
+
},
|
|
47
|
+
calories_data: isDetailed(activity) && activity.calories != null
|
|
48
|
+
? { total_burned_calories: activity.calories }
|
|
49
|
+
: undefined,
|
|
50
|
+
device_data: activity.device_name
|
|
51
|
+
? { name: activity.device_name }
|
|
52
|
+
: undefined,
|
|
53
|
+
distance_data: buildDistanceData(activity),
|
|
54
|
+
energy_data: activity.kilojoules != null
|
|
55
|
+
? { energy_kilojoules: activity.kilojoules }
|
|
56
|
+
: undefined,
|
|
57
|
+
heart_rate_data: buildHeartRateData(activity, streams, timeStream, startDate),
|
|
58
|
+
lap_data: buildLapData(laps),
|
|
59
|
+
movement_data: buildMovementData(activity, streams, timeStream, startDate),
|
|
60
|
+
polyline_map_data: activity.map?.summary_polyline
|
|
61
|
+
? { summary_polyline: activity.map.summary_polyline }
|
|
62
|
+
: undefined,
|
|
63
|
+
position_data: buildPositionData(activity, streams, timeStream, startDate),
|
|
64
|
+
power_data: buildPowerData(activity, streams, timeStream, startDate),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
68
|
+
function isDetailed(activity) {
|
|
69
|
+
return activity.resource_state === 3;
|
|
70
|
+
}
|
|
71
|
+
function computeEndTime(startDate, elapsedTimeSeconds) {
|
|
72
|
+
const start = new Date(startDate);
|
|
73
|
+
return new Date(start.getTime() + elapsedTimeSeconds * 1000).toISOString();
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Compute an ISO-8601 timestamp for a stream data point, given the
|
|
77
|
+
* activity start time and the time stream offset in seconds.
|
|
78
|
+
*/
|
|
79
|
+
function streamTimestamp(startDate, timeOffsetSeconds) {
|
|
80
|
+
const start = new Date(startDate);
|
|
81
|
+
return new Date(start.getTime() + timeOffsetSeconds * 1000).toISOString();
|
|
82
|
+
}
|
|
83
|
+
function buildDistanceData(activity) {
|
|
84
|
+
if (activity.distance == null && activity.total_elevation_gain == null) {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
const detailed = isDetailed(activity) ? activity : undefined;
|
|
88
|
+
return {
|
|
89
|
+
summary: {
|
|
90
|
+
distance_meters: activity.distance ?? undefined,
|
|
91
|
+
elevation: activity.total_elevation_gain != null
|
|
92
|
+
? {
|
|
93
|
+
gain_actual_meters: activity.total_elevation_gain,
|
|
94
|
+
max_meters: detailed?.elev_high ?? undefined,
|
|
95
|
+
min_meters: detailed?.elev_low ?? undefined,
|
|
96
|
+
}
|
|
97
|
+
: undefined,
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function buildHeartRateData(activity, streams, timeStream, startDate) {
|
|
102
|
+
const hasHrSummary = activity.average_heartrate != null || activity.max_heartrate != null;
|
|
103
|
+
const hrStream = streams?.heartrate?.data;
|
|
104
|
+
const hasHrStream = hrStream && hrStream.length > 0 && timeStream;
|
|
105
|
+
if (!hasHrSummary && !hasHrStream)
|
|
106
|
+
return undefined;
|
|
107
|
+
return {
|
|
108
|
+
summary: hasHrSummary
|
|
109
|
+
? {
|
|
110
|
+
avg_hr_bpm: activity.average_heartrate,
|
|
111
|
+
max_hr_bpm: activity.max_heartrate,
|
|
112
|
+
}
|
|
113
|
+
: undefined,
|
|
114
|
+
detailed: hasHrStream && timeStream
|
|
115
|
+
? {
|
|
116
|
+
hr_samples: hrStream.map((bpm, i) => ({
|
|
117
|
+
timestamp: streamTimestamp(startDate, timeStream[i]),
|
|
118
|
+
bpm,
|
|
119
|
+
})),
|
|
120
|
+
}
|
|
121
|
+
: undefined,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function buildLapData(laps) {
|
|
125
|
+
if (!laps || laps.length === 0)
|
|
126
|
+
return undefined;
|
|
127
|
+
return {
|
|
128
|
+
laps: laps.map((lap) => ({
|
|
129
|
+
start_time: lap.start_date,
|
|
130
|
+
end_time: computeEndTime(lap.start_date, lap.elapsed_time),
|
|
131
|
+
distance_meters: lap.distance,
|
|
132
|
+
calories: undefined,
|
|
133
|
+
avg_speed_meters_per_second: lap.average_speed,
|
|
134
|
+
avg_hr_bpm: lap.average_heartrate,
|
|
135
|
+
})),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function buildMovementData(activity, streams, timeStream, startDate) {
|
|
139
|
+
const hasMovement = activity.average_speed != null ||
|
|
140
|
+
activity.max_speed != null ||
|
|
141
|
+
activity.average_cadence != null;
|
|
142
|
+
const speedStream = streams?.velocity_smooth?.data;
|
|
143
|
+
const cadenceStream = streams?.cadence?.data;
|
|
144
|
+
const hasStreams = ((speedStream && speedStream.length > 0) ||
|
|
145
|
+
(cadenceStream && cadenceStream.length > 0)) &&
|
|
146
|
+
timeStream;
|
|
147
|
+
if (!hasMovement && !hasStreams)
|
|
148
|
+
return undefined;
|
|
149
|
+
return {
|
|
150
|
+
avg_speed_meters_per_second: activity.average_speed ?? undefined,
|
|
151
|
+
max_speed_meters_per_second: activity.max_speed ?? undefined,
|
|
152
|
+
avg_cadence_rpm: activity.average_cadence ?? undefined,
|
|
153
|
+
speed_samples: speedStream && timeStream
|
|
154
|
+
? speedStream.map((speed, i) => ({
|
|
155
|
+
timestamp: streamTimestamp(startDate, timeStream[i]),
|
|
156
|
+
speed_meters_per_second: speed,
|
|
157
|
+
}))
|
|
158
|
+
: undefined,
|
|
159
|
+
cadence_samples: cadenceStream && timeStream
|
|
160
|
+
? cadenceStream.map((cadence, i) => ({
|
|
161
|
+
timestamp: streamTimestamp(startDate, timeStream[i]),
|
|
162
|
+
cadence_rpm: cadence,
|
|
163
|
+
}))
|
|
164
|
+
: undefined,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function buildPositionData(activity, streams, timeStream, startDate) {
|
|
168
|
+
const latlngStream = streams?.latlng?.data;
|
|
169
|
+
const hasPositionStream = latlngStream && latlngStream.length > 0 && timeStream;
|
|
170
|
+
const hasStartEnd = activity.start_latlng != null || activity.end_latlng != null;
|
|
171
|
+
if (!hasPositionStream && !hasStartEnd)
|
|
172
|
+
return undefined;
|
|
173
|
+
return {
|
|
174
|
+
start_pos_lat_lng_deg: activity.start_latlng ?? undefined,
|
|
175
|
+
end_pos_lat_lng_deg: activity.end_latlng ?? undefined,
|
|
176
|
+
position_samples: hasPositionStream && timeStream
|
|
177
|
+
? latlngStream.map((coords, i) => ({
|
|
178
|
+
timestamp: streamTimestamp(startDate, timeStream[i]),
|
|
179
|
+
coords_lat_lng_deg: coords,
|
|
180
|
+
}))
|
|
181
|
+
: undefined,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function buildPowerData(activity, streams, timeStream, startDate) {
|
|
185
|
+
const hasPowerSummary = activity.average_watts != null || activity.max_watts != null;
|
|
186
|
+
const wattsStream = streams?.watts?.data;
|
|
187
|
+
const hasWattsStream = wattsStream && wattsStream.length > 0 && timeStream;
|
|
188
|
+
if (!hasPowerSummary && !hasWattsStream)
|
|
189
|
+
return undefined;
|
|
190
|
+
return {
|
|
191
|
+
avg_watts: activity.average_watts ?? undefined,
|
|
192
|
+
max_watts: activity.max_watts ?? undefined,
|
|
193
|
+
power_samples: hasWattsStream && timeStream
|
|
194
|
+
? wattsStream.map((watts, i) => ({
|
|
195
|
+
timestamp: streamTimestamp(startDate, timeStream[i]),
|
|
196
|
+
watts,
|
|
197
|
+
}))
|
|
198
|
+
: undefined,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
//# sourceMappingURL=activity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"activity.js","sourceRoot":"","sources":["../../src/strava/activity.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { DetailedAthlete } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* The output shape of {@link transformAthlete}.
|
|
4
|
+
*/
|
|
5
|
+
export type AthleteData = ReturnType<typeof transformAthlete>;
|
|
6
|
+
/**
|
|
7
|
+
* Transform a Strava athlete profile into a Soma Athlete document shape.
|
|
8
|
+
*
|
|
9
|
+
* Strava provides a relatively rich profile compared to HealthKit, including
|
|
10
|
+
* name, location, sex, and the date the athlete joined Strava.
|
|
11
|
+
*
|
|
12
|
+
* @param athlete - The Strava DetailedAthlete from `GET /athlete`
|
|
13
|
+
* @returns Soma Athlete fields (without connectionId/userId)
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* const data = transformAthlete(stravaAthlete);
|
|
18
|
+
* await soma.ingestAthlete(ctx, { connectionId, userId, ...data });
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export declare function transformAthlete(athlete: DetailedAthlete): {
|
|
22
|
+
first_name: string;
|
|
23
|
+
last_name: string;
|
|
24
|
+
city: string | undefined;
|
|
25
|
+
state: string | undefined;
|
|
26
|
+
country: string | undefined;
|
|
27
|
+
sex: string | undefined;
|
|
28
|
+
joined_provider: string;
|
|
29
|
+
devices: {
|
|
30
|
+
name: string;
|
|
31
|
+
id: string;
|
|
32
|
+
}[] | undefined;
|
|
33
|
+
};
|
|
34
|
+
//# sourceMappingURL=athlete.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"athlete.d.ts","sourceRoot":"","sources":["../../src/strava/athlete.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD;;GAEG;AACH,MAAM,MAAM,WAAW,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE9D;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,eAAe;;;;;;;;;;;;EAqBxD"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// ─── Athlete Transformer ─────────────────────────────────────────────────────
|
|
2
|
+
// Transforms a Strava DetailedAthlete into the Soma Athlete schema shape.
|
|
3
|
+
/**
|
|
4
|
+
* Transform a Strava athlete profile into a Soma Athlete document shape.
|
|
5
|
+
*
|
|
6
|
+
* Strava provides a relatively rich profile compared to HealthKit, including
|
|
7
|
+
* name, location, sex, and the date the athlete joined Strava.
|
|
8
|
+
*
|
|
9
|
+
* @param athlete - The Strava DetailedAthlete from `GET /athlete`
|
|
10
|
+
* @returns Soma Athlete fields (without connectionId/userId)
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* const data = transformAthlete(stravaAthlete);
|
|
15
|
+
* await soma.ingestAthlete(ctx, { connectionId, userId, ...data });
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export function transformAthlete(athlete) {
|
|
19
|
+
const sexMap = {
|
|
20
|
+
M: "male",
|
|
21
|
+
F: "female",
|
|
22
|
+
};
|
|
23
|
+
return {
|
|
24
|
+
first_name: athlete.firstname ?? undefined,
|
|
25
|
+
last_name: athlete.lastname ?? undefined,
|
|
26
|
+
city: athlete.city ?? undefined,
|
|
27
|
+
state: athlete.state ?? undefined,
|
|
28
|
+
country: athlete.country ?? undefined,
|
|
29
|
+
sex: athlete.sex ? sexMap[athlete.sex] : undefined,
|
|
30
|
+
joined_provider: athlete.created_at ?? undefined,
|
|
31
|
+
devices: athlete.bikes && athlete.shoes
|
|
32
|
+
? [
|
|
33
|
+
...athlete.bikes.map((b) => ({ name: b.name, id: b.id })),
|
|
34
|
+
...athlete.shoes.map((s) => ({ name: s.name, id: s.id })),
|
|
35
|
+
]
|
|
36
|
+
: undefined,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=athlete.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"athlete.js","sourceRoot":"","sources":["../../src/strava/athlete.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,0EAA0E;AAS1E;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAAwB;IACvD,MAAM,MAAM,GAA2B;QACrC,CAAC,EAAE,MAAM;QACT,CAAC,EAAE,QAAQ;KACZ,CAAC;IAEF,OAAO;QACL,UAAU,EAAE,OAAO,CAAC,SAAS,IAAI,SAAS;QAC1C,SAAS,EAAE,OAAO,CAAC,QAAQ,IAAI,SAAS;QACxC,IAAI,EAAE,OAAO,CAAC,IAAI,IAAI,SAAS;QAC/B,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,SAAS;QACjC,OAAO,EAAE,OAAO,CAAC,OAAO,IAAI,SAAS;QACrC,GAAG,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS;QAClD,eAAe,EAAE,OAAO,CAAC,UAAU,IAAI,SAAS;QAChD,OAAO,EAAE,OAAO,CAAC,KAAK,IAAI,OAAO,CAAC,KAAK;YACrC,CAAC,CAAC;gBACA,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;gBACzD,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;aAC1D;YACD,CAAC,CAAC,SAAS;KACd,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { OAuthTokenResponse } from "./types.js";
|
|
2
|
+
export interface BuildAuthUrlOptions {
|
|
3
|
+
/** Your Strava application's Client ID. */
|
|
4
|
+
clientId: string;
|
|
5
|
+
/** The URL Strava will redirect to after authorization. */
|
|
6
|
+
redirectUri: string;
|
|
7
|
+
/**
|
|
8
|
+
* Comma-separated Strava OAuth scopes.
|
|
9
|
+
* @default "read,activity:read_all,profile:read_all"
|
|
10
|
+
*/
|
|
11
|
+
scope?: string;
|
|
12
|
+
/** Optional state parameter for CSRF protection. */
|
|
13
|
+
state?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Base URL of the Strava site.
|
|
16
|
+
* @default "https://www.strava.com"
|
|
17
|
+
*/
|
|
18
|
+
baseUrl?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Build the Strava OAuth authorization URL.
|
|
22
|
+
*
|
|
23
|
+
* Redirect the user to this URL to begin the OAuth flow. After the user
|
|
24
|
+
* grants access, Strava will redirect back to `redirectUri` with a `code`
|
|
25
|
+
* query parameter.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* const url = buildAuthUrl({
|
|
30
|
+
* clientId: process.env.STRAVA_CLIENT_ID!,
|
|
31
|
+
* redirectUri: "https://your-app.com/api/strava/callback",
|
|
32
|
+
* });
|
|
33
|
+
* // Redirect user to `url`
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export declare function buildAuthUrl(opts: BuildAuthUrlOptions): string;
|
|
37
|
+
export interface ExchangeCodeOptions {
|
|
38
|
+
/** Your Strava application's Client ID. */
|
|
39
|
+
clientId: string;
|
|
40
|
+
/** Your Strava application's Client Secret. */
|
|
41
|
+
clientSecret: string;
|
|
42
|
+
/** The authorization code from the OAuth callback. */
|
|
43
|
+
code: string;
|
|
44
|
+
/**
|
|
45
|
+
* Base URL of the Strava site.
|
|
46
|
+
* @default "https://www.strava.com"
|
|
47
|
+
*/
|
|
48
|
+
baseUrl?: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Exchange an authorization code for access and refresh tokens.
|
|
52
|
+
*
|
|
53
|
+
* Call this from your OAuth callback endpoint after receiving the `code`
|
|
54
|
+
* query parameter from Strava.
|
|
55
|
+
*
|
|
56
|
+
* @returns The token response including `access_token`, `refresh_token`,
|
|
57
|
+
* `expires_at`, and the authenticated `athlete` profile.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```ts
|
|
61
|
+
* const tokens = await exchangeCode({
|
|
62
|
+
* clientId: process.env.STRAVA_CLIENT_ID!,
|
|
63
|
+
* clientSecret: process.env.STRAVA_CLIENT_SECRET!,
|
|
64
|
+
* code: request.query.code,
|
|
65
|
+
* });
|
|
66
|
+
* // Store tokens.access_token, tokens.refresh_token, tokens.expires_at
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
export declare function exchangeCode(opts: ExchangeCodeOptions): Promise<OAuthTokenResponse>;
|
|
70
|
+
export interface RefreshTokenOptions {
|
|
71
|
+
/** Your Strava application's Client ID. */
|
|
72
|
+
clientId: string;
|
|
73
|
+
/** Your Strava application's Client Secret. */
|
|
74
|
+
clientSecret: string;
|
|
75
|
+
/** The refresh token from a previous token exchange or refresh. */
|
|
76
|
+
refreshToken: string;
|
|
77
|
+
/**
|
|
78
|
+
* Base URL of the Strava site.
|
|
79
|
+
* @default "https://www.strava.com"
|
|
80
|
+
*/
|
|
81
|
+
baseUrl?: string;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Refresh an expired access token using a refresh token.
|
|
85
|
+
*
|
|
86
|
+
* Strava access tokens expire after ~6 hours. Call this when the
|
|
87
|
+
* `expires_at` timestamp has passed to obtain a fresh access token.
|
|
88
|
+
*
|
|
89
|
+
* @returns A new token response with a fresh `access_token` and
|
|
90
|
+
* possibly a new `refresh_token`.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```ts
|
|
94
|
+
* const tokens = await refreshToken({
|
|
95
|
+
* clientId: process.env.STRAVA_CLIENT_ID!,
|
|
96
|
+
* clientSecret: process.env.STRAVA_CLIENT_SECRET!,
|
|
97
|
+
* refreshToken: storedRefreshToken,
|
|
98
|
+
* });
|
|
99
|
+
* // Update stored tokens
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
export declare function refreshToken(opts: RefreshTokenOptions): Promise<OAuthTokenResponse>;
|
|
103
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/strava/auth.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAMrD,MAAM,WAAW,mBAAmB;IAClC,2CAA2C;IAC3C,QAAQ,EAAE,MAAM,CAAC;IACjB,2DAA2D;IAC3D,WAAW,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,oDAAoD;IACpD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,mBAAmB,GAAG,MAAM,CAe9D;AAID,MAAM,WAAW,mBAAmB;IAClC,2CAA2C;IAC3C,QAAQ,EAAE,MAAM,CAAC;IACjB,+CAA+C;IAC/C,YAAY,EAAE,MAAM,CAAC;IACrB,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,YAAY,CAChC,IAAI,EAAE,mBAAmB,GACxB,OAAO,CAAC,kBAAkB,CAAC,CAuB7B;AAID,MAAM,WAAW,mBAAmB;IAClC,2CAA2C;IAC3C,QAAQ,EAAE,MAAM,CAAC;IACjB,+CAA+C;IAC/C,YAAY,EAAE,MAAM,CAAC;IACrB,mEAAmE;IACnE,YAAY,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,YAAY,CAChC,IAAI,EAAE,mBAAmB,GACxB,OAAO,CAAC,kBAAkB,CAAC,CAuB7B"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// ─── Strava OAuth Helpers ────────────────────────────────────────────────────
|
|
2
|
+
// Pure helper functions for the Strava OAuth 2.0 Authorization Code flow.
|
|
3
|
+
// No external dependencies — uses the global `fetch`.
|
|
4
|
+
const DEFAULT_BASE_URL = "https://www.strava.com";
|
|
5
|
+
/**
|
|
6
|
+
* Build the Strava OAuth authorization URL.
|
|
7
|
+
*
|
|
8
|
+
* Redirect the user to this URL to begin the OAuth flow. After the user
|
|
9
|
+
* grants access, Strava will redirect back to `redirectUri` with a `code`
|
|
10
|
+
* query parameter.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* const url = buildAuthUrl({
|
|
15
|
+
* clientId: process.env.STRAVA_CLIENT_ID!,
|
|
16
|
+
* redirectUri: "https://your-app.com/api/strava/callback",
|
|
17
|
+
* });
|
|
18
|
+
* // Redirect user to `url`
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export function buildAuthUrl(opts) {
|
|
22
|
+
const base = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
23
|
+
const params = new URLSearchParams({
|
|
24
|
+
client_id: opts.clientId,
|
|
25
|
+
redirect_uri: opts.redirectUri,
|
|
26
|
+
response_type: "code",
|
|
27
|
+
approval_prompt: "auto",
|
|
28
|
+
scope: opts.scope ?? "read,activity:read_all,profile:read_all",
|
|
29
|
+
});
|
|
30
|
+
if (opts.state) {
|
|
31
|
+
params.set("state", opts.state);
|
|
32
|
+
}
|
|
33
|
+
return `${base}/oauth/authorize?${params.toString()}`;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Exchange an authorization code for access and refresh tokens.
|
|
37
|
+
*
|
|
38
|
+
* Call this from your OAuth callback endpoint after receiving the `code`
|
|
39
|
+
* query parameter from Strava.
|
|
40
|
+
*
|
|
41
|
+
* @returns The token response including `access_token`, `refresh_token`,
|
|
42
|
+
* `expires_at`, and the authenticated `athlete` profile.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* const tokens = await exchangeCode({
|
|
47
|
+
* clientId: process.env.STRAVA_CLIENT_ID!,
|
|
48
|
+
* clientSecret: process.env.STRAVA_CLIENT_SECRET!,
|
|
49
|
+
* code: request.query.code,
|
|
50
|
+
* });
|
|
51
|
+
* // Store tokens.access_token, tokens.refresh_token, tokens.expires_at
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export async function exchangeCode(opts) {
|
|
55
|
+
const base = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
56
|
+
const url = `${base}/oauth/token`;
|
|
57
|
+
const response = await fetch(url, {
|
|
58
|
+
method: "POST",
|
|
59
|
+
headers: { "Content-Type": "application/json" },
|
|
60
|
+
body: JSON.stringify({
|
|
61
|
+
client_id: opts.clientId,
|
|
62
|
+
client_secret: opts.clientSecret,
|
|
63
|
+
code: opts.code,
|
|
64
|
+
grant_type: "authorization_code",
|
|
65
|
+
}),
|
|
66
|
+
});
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
const body = await response.text().catch(() => "");
|
|
69
|
+
throw new Error(`Strava OAuth error (exchangeCode): ${response.status} ${response.statusText} — ${body}`);
|
|
70
|
+
}
|
|
71
|
+
return (await response.json());
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Refresh an expired access token using a refresh token.
|
|
75
|
+
*
|
|
76
|
+
* Strava access tokens expire after ~6 hours. Call this when the
|
|
77
|
+
* `expires_at` timestamp has passed to obtain a fresh access token.
|
|
78
|
+
*
|
|
79
|
+
* @returns A new token response with a fresh `access_token` and
|
|
80
|
+
* possibly a new `refresh_token`.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```ts
|
|
84
|
+
* const tokens = await refreshToken({
|
|
85
|
+
* clientId: process.env.STRAVA_CLIENT_ID!,
|
|
86
|
+
* clientSecret: process.env.STRAVA_CLIENT_SECRET!,
|
|
87
|
+
* refreshToken: storedRefreshToken,
|
|
88
|
+
* });
|
|
89
|
+
* // Update stored tokens
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
export async function refreshToken(opts) {
|
|
93
|
+
const base = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
94
|
+
const url = `${base}/oauth/token`;
|
|
95
|
+
const response = await fetch(url, {
|
|
96
|
+
method: "POST",
|
|
97
|
+
headers: { "Content-Type": "application/json" },
|
|
98
|
+
body: JSON.stringify({
|
|
99
|
+
client_id: opts.clientId,
|
|
100
|
+
client_secret: opts.clientSecret,
|
|
101
|
+
refresh_token: opts.refreshToken,
|
|
102
|
+
grant_type: "refresh_token",
|
|
103
|
+
}),
|
|
104
|
+
});
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
const body = await response.text().catch(() => "");
|
|
107
|
+
throw new Error(`Strava OAuth error (refreshToken): ${response.status} ${response.statusText} — ${body}`);
|
|
108
|
+
}
|
|
109
|
+
return (await response.json());
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/strava/auth.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,0EAA0E;AAC1E,sDAAsD;AAItD,MAAM,gBAAgB,GAAG,wBAAwB,CAAC;AAuBlD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,YAAY,CAAC,IAAyB;IACpD,MAAM,IAAI,GAAG,CAAC,IAAI,CAAC,OAAO,IAAI,gBAAgB,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IACpE,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC;QACjC,SAAS,EAAE,IAAI,CAAC,QAAQ;QACxB,YAAY,EAAE,IAAI,CAAC,WAAW;QAC9B,aAAa,EAAE,MAAM;QACrB,eAAe,EAAE,MAAM;QACvB,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,yCAAyC;KAC/D,CAAC,CAAC;IAEH,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;QACf,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;IAClC,CAAC;IAED,OAAO,GAAG,IAAI,oBAAoB,MAAM,CAAC,QAAQ,EAAE,EAAE,CAAC;AACxD,CAAC;AAkBD;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,IAAyB;IAEzB,MAAM,IAAI,GAAG,CAAC,IAAI,CAAC,OAAO,IAAI,gBAAgB,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IACpE,MAAM,GAAG,GAAG,GAAG,IAAI,cAAc,CAAC;IAElC,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;QAChC,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;QAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;YACnB,SAAS,EAAE,IAAI,CAAC,QAAQ;YACxB,aAAa,EAAE,IAAI,CAAC,YAAY;YAChC,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,UAAU,EAAE,oBAAoB;SACjC,CAAC;KACH,CAAC,CAAC;IAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;QACnD,MAAM,IAAI,KAAK,CACb,sCAAsC,QAAQ,CAAC,MAAM,IAAI,QAAQ,CAAC,UAAU,MAAM,IAAI,EAAE,CACzF,CAAC;IACJ,CAAC;IAED,OAAO,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAuB,CAAC;AACvD,CAAC;AAkBD;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,IAAyB;IAEzB,MAAM,IAAI,GAAG,CAAC,IAAI,CAAC,OAAO,IAAI,gBAAgB,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IACpE,MAAM,GAAG,GAAG,GAAG,IAAI,cAAc,CAAC;IAElC,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;QAChC,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;QAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;YACnB,SAAS,EAAE,IAAI,CAAC,QAAQ;YACxB,aAAa,EAAE,IAAI,CAAC,YAAY;YAChC,aAAa,EAAE,IAAI,CAAC,YAAY;YAChC,UAAU,EAAE,eAAe;SAC5B,CAAC;KACH,CAAC,CAAC;IAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;QACnD,MAAM,IAAI,KAAK,CACb,sCAAsC,QAAQ,CAAC,MAAM,IAAI,QAAQ,CAAC,UAAU,MAAM,IAAI,EAAE,CACzF,CAAC;IACJ,CAAC;IAED,OAAO,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAuB,CAAC;AACvD,CAAC"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { DetailedActivity, DetailedAthlete, Lap, StreamSet, SummaryActivity } from "./types.js";
|
|
2
|
+
export interface StravaClientOptions {
|
|
3
|
+
/** OAuth access token for the authenticated athlete. */
|
|
4
|
+
accessToken: string;
|
|
5
|
+
/**
|
|
6
|
+
* Base URL of the Strava API (without `/api/v3` suffix).
|
|
7
|
+
* Defaults to `https://www.strava.com`.
|
|
8
|
+
* Override to point at a mock server during development.
|
|
9
|
+
*/
|
|
10
|
+
baseUrl?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* A lightweight client for the Strava API v3.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* const client = new StravaClient({
|
|
18
|
+
* accessToken: "tok_xxx",
|
|
19
|
+
* baseUrl: "https://strava-mock-server.onrender.com", // optional
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* const athlete = await client.getAthlete();
|
|
23
|
+
* const activities = await client.listActivities({ per_page: 50 });
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export declare class StravaClient {
|
|
27
|
+
private readonly accessToken;
|
|
28
|
+
private readonly baseUrl;
|
|
29
|
+
constructor(opts: StravaClientOptions);
|
|
30
|
+
/**
|
|
31
|
+
* Get the authenticated athlete's profile.
|
|
32
|
+
*
|
|
33
|
+
* Strava API: `GET /athlete`
|
|
34
|
+
*/
|
|
35
|
+
getAthlete(): Promise<DetailedAthlete>;
|
|
36
|
+
/**
|
|
37
|
+
* List the authenticated athlete's activities.
|
|
38
|
+
*
|
|
39
|
+
* Strava API: `GET /athlete/activities`
|
|
40
|
+
*
|
|
41
|
+
* @param params.before - Only return activities before this Unix epoch timestamp
|
|
42
|
+
* @param params.after - Only return activities after this Unix epoch timestamp
|
|
43
|
+
* @param params.page - Page number (defaults to 1)
|
|
44
|
+
* @param params.per_page - Items per page (defaults to 30, max 200)
|
|
45
|
+
*/
|
|
46
|
+
listActivities(params?: {
|
|
47
|
+
before?: number;
|
|
48
|
+
after?: number;
|
|
49
|
+
page?: number;
|
|
50
|
+
per_page?: number;
|
|
51
|
+
}): Promise<SummaryActivity[]>;
|
|
52
|
+
/**
|
|
53
|
+
* List ALL activities for the authenticated athlete, automatically
|
|
54
|
+
* paginating through all pages.
|
|
55
|
+
*
|
|
56
|
+
* @param params.after - Only return activities after this Unix epoch timestamp
|
|
57
|
+
* @param params.before - Only return activities before this Unix epoch timestamp
|
|
58
|
+
* @param params.per_page - Items per page (defaults to 200)
|
|
59
|
+
*/
|
|
60
|
+
listAllActivities(params?: {
|
|
61
|
+
after?: number;
|
|
62
|
+
before?: number;
|
|
63
|
+
per_page?: number;
|
|
64
|
+
}): Promise<SummaryActivity[]>;
|
|
65
|
+
/**
|
|
66
|
+
* Get a detailed activity by ID.
|
|
67
|
+
*
|
|
68
|
+
* Strava API: `GET /activities/{id}`
|
|
69
|
+
*/
|
|
70
|
+
getActivity(id: number): Promise<DetailedActivity>;
|
|
71
|
+
/**
|
|
72
|
+
* Get time-series streams for an activity.
|
|
73
|
+
*
|
|
74
|
+
* Strava API: `GET /activities/{id}/streams`
|
|
75
|
+
*
|
|
76
|
+
* @param id - Activity ID
|
|
77
|
+
* @param keys - Stream types to request (e.g. `["heartrate", "watts", "latlng", "altitude", "time"]`)
|
|
78
|
+
*/
|
|
79
|
+
getActivityStreams(id: number, keys?: string[]): Promise<StreamSet>;
|
|
80
|
+
/**
|
|
81
|
+
* Get laps for an activity.
|
|
82
|
+
*
|
|
83
|
+
* Strava API: `GET /activities/{id}/laps`
|
|
84
|
+
*/
|
|
85
|
+
getActivityLaps(id: number): Promise<Lap[]>;
|
|
86
|
+
private get;
|
|
87
|
+
}
|
|
88
|
+
export declare class StravaApiError extends Error {
|
|
89
|
+
readonly status: number;
|
|
90
|
+
readonly body: string;
|
|
91
|
+
constructor(message: string, status: number, body: string);
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/strava/client.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,gBAAgB,EAChB,eAAe,EACf,GAAG,EACH,SAAS,EAET,eAAe,EAChB,MAAM,YAAY,CAAC;AAKpB,MAAM,WAAW,mBAAmB;IAClC,wDAAwD;IACxD,WAAW,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;;;GAaG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;gBAErB,IAAI,EAAE,mBAAmB;IAOrC;;;;OAIG;IACG,UAAU,IAAI,OAAO,CAAC,eAAe,CAAC;IAM5C;;;;;;;;;OASG;IACG,cAAc,CAAC,MAAM,CAAC,EAAE;QAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;IAa9B;;;;;;;OAOG;IACG,iBAAiB,CAAC,MAAM,CAAC,EAAE;QAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;IAoB9B;;;;OAIG;IACG,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAIxD;;;;;;;OAOG;IACG,kBAAkB,CACtB,EAAE,EAAE,MAAM,EACV,IAAI,GAAE,MAAM,EAWX,GACA,OAAO,CAAC,SAAS,CAAC;IAWrB;;;;OAIG;IACG,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;YAMnC,GAAG;CAqBlB;AAID,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"}
|