@nativesquare/soma 0.13.1 → 0.14.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 +2 -2
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/strava.d.ts +45 -33
- package/dist/client/strava.d.ts.map +1 -1
- package/dist/client/strava.js +138 -22
- package/dist/client/strava.js.map +1 -1
- package/dist/client/types.d.ts +108 -0
- package/dist/client/types.d.ts.map +1 -1
- package/dist/component/_generated/api.d.ts +2 -2
- package/dist/component/_generated/api.d.ts.map +1 -1
- package/dist/component/_generated/component.d.ts +19 -17
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/garmin/auth.d.ts +2 -1
- package/dist/component/garmin/auth.d.ts.map +1 -1
- package/dist/component/garmin/auth.js +6 -1
- package/dist/component/garmin/auth.js.map +1 -1
- package/dist/component/garmin/private.d.ts +17 -75
- package/dist/component/garmin/private.d.ts.map +1 -1
- package/dist/component/garmin/private.js +4 -167
- package/dist/component/garmin/private.js.map +1 -1
- package/dist/component/garmin/public.d.ts +18 -33
- package/dist/component/garmin/public.d.ts.map +1 -1
- package/dist/component/garmin/public.js +23 -22
- package/dist/component/garmin/public.js.map +1 -1
- package/dist/component/garmin/webhooks.d.ts +3 -6
- package/dist/component/garmin/webhooks.d.ts.map +1 -1
- package/dist/component/garmin/webhooks.js +17 -28
- package/dist/component/garmin/webhooks.js.map +1 -1
- package/dist/component/private.d.ts +59 -0
- package/dist/component/private.d.ts.map +1 -1
- package/dist/component/private.js +182 -1
- package/dist/component/private.js.map +1 -1
- package/dist/component/strava/auth.d.ts +2 -1
- package/dist/component/strava/auth.d.ts.map +1 -1
- package/dist/component/strava/auth.js +6 -1
- package/dist/component/strava/auth.js.map +1 -1
- package/dist/component/strava/public.d.ts +26 -50
- package/dist/component/strava/public.d.ts.map +1 -1
- package/dist/component/strava/public.js +88 -132
- package/dist/component/strava/public.js.map +1 -1
- package/dist/component/strava/webhooks.d.ts +17 -0
- package/dist/component/strava/webhooks.d.ts.map +1 -0
- package/dist/component/strava/webhooks.js +231 -0
- package/dist/component/strava/webhooks.js.map +1 -0
- package/dist/component/utils.d.ts +10 -0
- package/dist/component/utils.d.ts.map +1 -1
- package/dist/component/utils.js.map +1 -1
- package/dist/component/validators/shared.d.ts +3 -0
- package/dist/component/validators/shared.d.ts.map +1 -1
- package/dist/component/validators/shared.js +1 -1
- package/dist/component/validators/shared.js.map +1 -1
- package/package.json +1 -1
- package/src/client/index.ts +8 -1
- package/src/client/strava.ts +190 -26
- package/src/client/types.ts +125 -0
- package/src/component/_generated/api.ts +2 -2
- package/src/component/_generated/component.ts +25 -6
- package/src/component/garmin/auth.ts +9 -2
- package/src/component/garmin/private.ts +22 -243
- package/src/component/garmin/public.ts +56 -54
- package/src/component/garmin/webhooks.ts +38 -55
- package/src/component/private.ts +245 -1
- package/src/component/strava/auth.ts +9 -2
- package/src/component/strava/public.ts +105 -171
- package/src/component/strava/webhooks.ts +312 -0
- package/src/component/utils.ts +11 -0
- package/src/component/validators/shared.ts +5 -2
- package/dist/component/strava/private.d.ts +0 -49
- package/dist/component/strava/private.d.ts.map +0 -1
- package/dist/component/strava/private.js +0 -121
- package/dist/component/strava/private.js.map +0 -1
- package/src/component/strava/private.ts +0 -147
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// ─── Strava
|
|
2
|
-
// Public actions that handle the full Strava OAuth +
|
|
1
|
+
// ─── Strava Public Actions ──────────────────────────────────────────────────
|
|
2
|
+
// Public actions that handle the full Strava OAuth + pull lifecycle.
|
|
3
3
|
// The host app calls these through the Soma class, which threads the
|
|
4
4
|
// credentials automatically from env vars or constructor config.
|
|
5
5
|
import { v } from "convex/values";
|
|
@@ -9,17 +9,10 @@ import { createStravaClient } from "./client.js";
|
|
|
9
9
|
import { listAllActivities } from "./utils.js";
|
|
10
10
|
import { getLoggedInAthlete, getActivityById, getActivityStreams, } from "./types/stravaApi/sdk.gen.js";
|
|
11
11
|
import { generateState } from "../utils.js";
|
|
12
|
-
import { buildAuthUrl, exchangeCode,
|
|
12
|
+
import { buildAuthUrl, exchangeCode, } from "./auth.js";
|
|
13
13
|
import { transformActivity } from "./transform/activity.js";
|
|
14
14
|
import { transformAthlete } from "./transform/athlete.js";
|
|
15
|
-
// ───
|
|
16
|
-
/**
|
|
17
|
-
* Generate a Strava OAuth authorization URL.
|
|
18
|
-
*
|
|
19
|
-
* The state parameter is stored in the component's `pendingOAuth` table
|
|
20
|
-
* so that `completeStravaOAuth` can look it up automatically when the
|
|
21
|
-
* callback fires via `registerRoutes`.
|
|
22
|
-
*/
|
|
15
|
+
// ─── OAuth ──────────────────────────────────────────────────────────────────
|
|
23
16
|
export const getStravaAuthUrl = action({
|
|
24
17
|
args: {
|
|
25
18
|
clientId: v.string(),
|
|
@@ -35,7 +28,7 @@ export const getStravaAuthUrl = action({
|
|
|
35
28
|
scope: args.scope,
|
|
36
29
|
state,
|
|
37
30
|
});
|
|
38
|
-
await ctx.runMutation(internal.
|
|
31
|
+
await ctx.runMutation(internal.private.storePendingOAuth, {
|
|
39
32
|
provider: "STRAVA",
|
|
40
33
|
state,
|
|
41
34
|
userId: args.userId,
|
|
@@ -43,17 +36,6 @@ export const getStravaAuthUrl = action({
|
|
|
43
36
|
return { authUrl, state };
|
|
44
37
|
},
|
|
45
38
|
});
|
|
46
|
-
/**
|
|
47
|
-
* Complete a Strava OAuth flow using stored pending state.
|
|
48
|
-
*
|
|
49
|
-
* Called internally by `registerRoutes` — the callback handler calls
|
|
50
|
-
* this with the `code` and `state` from the redirect. The action looks
|
|
51
|
-
* up the pending state (userId) stored during `getStravaAuthUrl`,
|
|
52
|
-
* exchanges for tokens, creates the connection, stores tokens, and
|
|
53
|
-
* cleans up the pending entry.
|
|
54
|
-
*
|
|
55
|
-
* The host app is responsible for calling `syncStrava` afterwards.
|
|
56
|
-
*/
|
|
57
39
|
export const completeStravaOAuth = action({
|
|
58
40
|
args: {
|
|
59
41
|
code: v.string(),
|
|
@@ -67,7 +49,7 @@ export const completeStravaOAuth = action({
|
|
|
67
49
|
}),
|
|
68
50
|
handler: async (ctx, args) => {
|
|
69
51
|
// 1. Look up pending state
|
|
70
|
-
const pending = await ctx.runQuery(internal.
|
|
52
|
+
const pending = await ctx.runQuery(internal.private.getPendingOAuth, { state: args.state });
|
|
71
53
|
if (!pending) {
|
|
72
54
|
throw new Error("No pending Strava OAuth state found for this state parameter. " +
|
|
73
55
|
"The authorization may have expired or was already used.");
|
|
@@ -79,14 +61,15 @@ export const completeStravaOAuth = action({
|
|
|
79
61
|
code: args.code,
|
|
80
62
|
});
|
|
81
63
|
// 3. Clean up pending entry
|
|
82
|
-
await ctx.runMutation(internal.
|
|
64
|
+
await ctx.runMutation(internal.private.deletePendingOAuth, { state: args.state });
|
|
83
65
|
// 4. Create/reactivate the Soma connection
|
|
84
66
|
const connectionId = await ctx.runMutation(api.public.connect, {
|
|
85
67
|
userId: pending.userId,
|
|
86
68
|
provider: "STRAVA",
|
|
69
|
+
providerUserId: String(tokens.athlete.id),
|
|
87
70
|
});
|
|
88
71
|
// 5. Store OAuth tokens
|
|
89
|
-
await ctx.runMutation(internal.
|
|
72
|
+
await ctx.runMutation(internal.private.storeTokens, {
|
|
90
73
|
connectionId,
|
|
91
74
|
accessToken: tokens.access_token,
|
|
92
75
|
refreshToken: tokens.refresh_token,
|
|
@@ -98,98 +81,56 @@ export const completeStravaOAuth = action({
|
|
|
98
81
|
};
|
|
99
82
|
},
|
|
100
83
|
});
|
|
101
|
-
|
|
102
|
-
* Incremental Strava sync for an already-connected user.
|
|
103
|
-
*
|
|
104
|
-
* Looks up the stored tokens, auto-refreshes if expired, then syncs
|
|
105
|
-
* the athlete profile and activities.
|
|
106
|
-
*
|
|
107
|
-
* Returns `{ synced, errors }`.
|
|
108
|
-
*/
|
|
109
|
-
export const syncStrava = action({
|
|
84
|
+
export const disconnectStrava = action({
|
|
110
85
|
args: {
|
|
111
86
|
userId: v.string(),
|
|
112
87
|
clientId: v.string(),
|
|
113
88
|
clientSecret: v.string(),
|
|
114
|
-
after: v.optional(v.number()),
|
|
115
89
|
},
|
|
116
|
-
returns: v.
|
|
117
|
-
data: v.object({ synced: v.object({ athletes: v.number(), activities: v.number() }) }),
|
|
118
|
-
errors: v.array(v.object({ type: v.string(), id: v.string(), message: v.string() })),
|
|
119
|
-
}),
|
|
90
|
+
returns: v.null(),
|
|
120
91
|
handler: async (ctx, args) => {
|
|
121
92
|
// 1. Look up connection
|
|
122
93
|
const connection = await ctx.runQuery(internal.private.getConnectionByProvider, { userId: args.userId, provider: "STRAVA" });
|
|
123
94
|
if (!connection) {
|
|
124
|
-
throw new Error(`No Strava connection found for user "${args.userId}"
|
|
125
|
-
"Connect to Strava first via getStravaAuthUrl.");
|
|
126
|
-
}
|
|
127
|
-
if (!connection.active) {
|
|
128
|
-
throw new Error(`Strava connection for user "${args.userId}" is inactive. Reconnect first.`);
|
|
95
|
+
throw new Error(`No Strava connection found for user "${args.userId}".`);
|
|
129
96
|
}
|
|
130
97
|
const connectionId = connection._id;
|
|
131
|
-
// 2.
|
|
132
|
-
const tokenDoc = await ctx.runQuery(internal.
|
|
133
|
-
if (
|
|
134
|
-
|
|
135
|
-
"
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const
|
|
146
|
-
clientId: args.clientId,
|
|
147
|
-
clientSecret: args.clientSecret,
|
|
148
|
-
refreshToken: tokenDoc.refreshToken,
|
|
149
|
-
});
|
|
150
|
-
accessToken = refreshed.access_token;
|
|
151
|
-
await ctx.runMutation(internal.strava.private.storeTokens, {
|
|
152
|
-
connectionId,
|
|
153
|
-
accessToken: refreshed.access_token,
|
|
154
|
-
refreshToken: refreshed.refresh_token,
|
|
155
|
-
expiresAt: refreshed.expires_at,
|
|
156
|
-
});
|
|
98
|
+
// 2. Revoke token at Strava (best-effort, don't fail if it errors)
|
|
99
|
+
const tokenDoc = await ctx.runQuery(internal.private.getTokens, { connectionId });
|
|
100
|
+
if (tokenDoc) {
|
|
101
|
+
try {
|
|
102
|
+
await fetch("https://www.strava.com/oauth/deauthorize", {
|
|
103
|
+
method: "POST",
|
|
104
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
105
|
+
body: `access_token=${tokenDoc.accessToken}`,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// Deauthorization is best-effort — clean up locally regardless
|
|
110
|
+
}
|
|
111
|
+
// 3. Delete stored tokens
|
|
112
|
+
const _deleted = await ctx.runMutation(internal.private.deleteTokens, { connectionId });
|
|
157
113
|
}
|
|
158
|
-
// 4.
|
|
159
|
-
const
|
|
160
|
-
accessToken,
|
|
161
|
-
connectionId,
|
|
114
|
+
// 4. Set connection inactive
|
|
115
|
+
const _disconnected = await ctx.runMutation(api.public.disconnect, {
|
|
162
116
|
userId: args.userId,
|
|
163
|
-
|
|
164
|
-
});
|
|
165
|
-
// 5. Update lastDataUpdate timestamp
|
|
166
|
-
await ctx.runMutation(api.public.updateConnection, {
|
|
167
|
-
connectionId,
|
|
168
|
-
lastDataUpdate: new Date().toISOString(),
|
|
117
|
+
provider: "STRAVA",
|
|
169
118
|
});
|
|
170
|
-
return
|
|
119
|
+
return null;
|
|
171
120
|
},
|
|
172
121
|
});
|
|
173
|
-
// ───
|
|
174
|
-
|
|
175
|
-
* Fetch and ingest all Strava data types for a connected user.
|
|
176
|
-
*
|
|
177
|
-
* Called by syncStrava after obtaining a valid access token.
|
|
178
|
-
*/
|
|
179
|
-
export const syncAllTypes = action({
|
|
122
|
+
// ─── Pull ───────────────────────────────────────────────────────────────────
|
|
123
|
+
export const pullAthlete = action({
|
|
180
124
|
args: {
|
|
181
|
-
accessToken: v.string(),
|
|
182
|
-
connectionId: v.id("connections"),
|
|
183
125
|
userId: v.string(),
|
|
184
|
-
|
|
185
|
-
|
|
126
|
+
clientId: v.string(),
|
|
127
|
+
clientSecret: v.string(),
|
|
186
128
|
},
|
|
187
129
|
handler: async (ctx, args) => {
|
|
188
|
-
const {
|
|
130
|
+
const { connectionId, accessToken } = await ctx.runAction(internal.private.resolveConnectionAndAccessToken, { userId: args.userId, provider: "STRAVA", clientId: args.clientId, clientSecret: args.clientSecret });
|
|
189
131
|
const client = createStravaClient(accessToken);
|
|
190
|
-
const synced = { athletes: 0
|
|
132
|
+
const synced = { athletes: 0 };
|
|
191
133
|
const errors = [];
|
|
192
|
-
// ── Athlete ──────────────────────────────────────────────────────────
|
|
193
134
|
try {
|
|
194
135
|
const { data: athlete, error } = await getLoggedInAthlete({ client });
|
|
195
136
|
if (error || !athlete)
|
|
@@ -197,7 +138,7 @@ export const syncAllTypes = action({
|
|
|
197
138
|
const data = transformAthlete(athlete);
|
|
198
139
|
await ctx.runMutation(api.public.ingestAthlete, {
|
|
199
140
|
connectionId,
|
|
200
|
-
userId,
|
|
141
|
+
userId: args.userId,
|
|
201
142
|
...data,
|
|
202
143
|
});
|
|
203
144
|
synced.athletes++;
|
|
@@ -209,7 +150,26 @@ export const syncAllTypes = action({
|
|
|
209
150
|
message: err instanceof Error ? err.message : String(err),
|
|
210
151
|
});
|
|
211
152
|
}
|
|
212
|
-
|
|
153
|
+
await ctx.runMutation(api.public.updateConnection, {
|
|
154
|
+
connectionId,
|
|
155
|
+
lastDataUpdate: new Date().toISOString(),
|
|
156
|
+
});
|
|
157
|
+
return { data: { synced }, errors };
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
export const pullActivities = action({
|
|
161
|
+
args: {
|
|
162
|
+
userId: v.string(),
|
|
163
|
+
clientId: v.string(),
|
|
164
|
+
clientSecret: v.string(),
|
|
165
|
+
after: v.optional(v.number()),
|
|
166
|
+
before: v.optional(v.number()),
|
|
167
|
+
},
|
|
168
|
+
handler: async (ctx, args) => {
|
|
169
|
+
const { connectionId, accessToken } = await ctx.runAction(internal.private.resolveConnectionAndAccessToken, { userId: args.userId, provider: "STRAVA", clientId: args.clientId, clientSecret: args.clientSecret });
|
|
170
|
+
const client = createStravaClient(accessToken);
|
|
171
|
+
const synced = { activities: 0 };
|
|
172
|
+
const errors = [];
|
|
213
173
|
try {
|
|
214
174
|
const summaries = await listAllActivities(client, {
|
|
215
175
|
after: args.after,
|
|
@@ -233,7 +193,7 @@ export const syncAllTypes = action({
|
|
|
233
193
|
const data = transformActivity(detailed, { streams: streams ?? undefined });
|
|
234
194
|
await ctx.runMutation(api.public.ingestActivity, {
|
|
235
195
|
connectionId,
|
|
236
|
-
userId,
|
|
196
|
+
userId: args.userId,
|
|
237
197
|
...data,
|
|
238
198
|
});
|
|
239
199
|
synced.activities++;
|
|
@@ -254,52 +214,48 @@ export const syncAllTypes = action({
|
|
|
254
214
|
message: err instanceof Error ? err.message : String(err),
|
|
255
215
|
});
|
|
256
216
|
}
|
|
217
|
+
await ctx.runMutation(api.public.updateConnection, {
|
|
218
|
+
connectionId,
|
|
219
|
+
lastDataUpdate: new Date().toISOString(),
|
|
220
|
+
});
|
|
257
221
|
return { data: { synced }, errors };
|
|
258
222
|
},
|
|
259
223
|
});
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* Disconnect a user from Strava.
|
|
263
|
-
*
|
|
264
|
-
* Revokes the token at Strava (best-effort), deletes stored tokens,
|
|
265
|
-
* and sets the connection to inactive.
|
|
266
|
-
*/
|
|
267
|
-
export const disconnectStrava = action({
|
|
224
|
+
export const pullAll = action({
|
|
268
225
|
args: {
|
|
269
226
|
userId: v.string(),
|
|
270
227
|
clientId: v.string(),
|
|
271
228
|
clientSecret: v.string(),
|
|
229
|
+
after: v.optional(v.number()),
|
|
230
|
+
before: v.optional(v.number()),
|
|
272
231
|
},
|
|
273
|
-
returns: v.null(),
|
|
274
232
|
handler: async (ctx, args) => {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
}
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
233
|
+
const sharedArgs = {
|
|
234
|
+
userId: args.userId,
|
|
235
|
+
clientId: args.clientId,
|
|
236
|
+
clientSecret: args.clientSecret,
|
|
237
|
+
};
|
|
238
|
+
const pullFns = [
|
|
239
|
+
{ ref: api.strava.public.pullAthlete, name: "athlete", args: sharedArgs },
|
|
240
|
+
{ ref: api.strava.public.pullActivities, name: "activities", args: { ...sharedArgs, after: args.after, before: args.before } },
|
|
241
|
+
];
|
|
242
|
+
const synced = {};
|
|
243
|
+
const errors = [];
|
|
244
|
+
for (const { ref, name, args: fnArgs } of pullFns) {
|
|
284
245
|
try {
|
|
285
|
-
await
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
body: `access_token=${tokenDoc.accessToken}`,
|
|
289
|
-
});
|
|
246
|
+
const result = await ctx.runAction(ref, fnArgs);
|
|
247
|
+
Object.assign(synced, result.data.synced);
|
|
248
|
+
errors.push(...result.errors);
|
|
290
249
|
}
|
|
291
|
-
catch {
|
|
292
|
-
|
|
250
|
+
catch (err) {
|
|
251
|
+
errors.push({
|
|
252
|
+
type: name,
|
|
253
|
+
id: "pull",
|
|
254
|
+
message: err instanceof Error ? err.message : String(err),
|
|
255
|
+
});
|
|
293
256
|
}
|
|
294
|
-
// 3. Delete stored tokens
|
|
295
|
-
const _deleted = await ctx.runMutation(internal.strava.private.deleteTokens, { connectionId });
|
|
296
257
|
}
|
|
297
|
-
|
|
298
|
-
const _disconnected = await ctx.runMutation(api.public.disconnect, {
|
|
299
|
-
userId: args.userId,
|
|
300
|
-
provider: "STRAVA",
|
|
301
|
-
});
|
|
302
|
-
return null;
|
|
258
|
+
return { data: { synced }, errors };
|
|
303
259
|
},
|
|
304
260
|
});
|
|
305
261
|
//# sourceMappingURL=public.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"public.js","sourceRoot":"","sources":["../../../src/component/strava/public.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"public.js","sourceRoot":"","sources":["../../../src/component/strava/public.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,qEAAqE;AACrE,qEAAqE;AACrE,iEAAiE;AAEjE,OAAO,EAAE,CAAC,EAAE,MAAM,eAAe,CAAC;AAClC,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAC9C,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAElD,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,kBAAkB,GACnB,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EACL,YAAY,EACZ,YAAY,GACb,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAG1D,+EAA+E;AAE/E,MAAM,CAAC,MAAM,gBAAgB,GAAG,MAAM,CAAC;IACrC,IAAI,EAAE;QACJ,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE;QACpB,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE;QACvB,KAAK,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;QAC7B,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE;KACnB;IACD,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;QAC3B,MAAM,KAAK,GAAG,aAAa,EAAE,CAAC;QAE9B,MAAM,OAAO,GAAG,YAAY,CAAC;YAC3B,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,WAAW,EAAE,IAAI,CAAC,WAAW;YAC7B,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,KAAK;SACN,CAAC,CAAC;QAEH,MAAM,GAAG,CAAC,WAAW,CAAC,QAAQ,CAAC,OAAO,CAAC,iBAAiB,EAAE;YACxD,QAAQ,EAAE,QAAQ;YAClB,KAAK;YACL,MAAM,EAAE,IAAI,CAAC,MAAM;SACpB,CAAC,CAAC;QAEH,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAC5B,CAAC;CACF,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,mBAAmB,GAAG,MAAM,CAAC;IACxC,IAAI,EAAE;QACJ,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;QAChB,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;QACjB,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE;QACpB,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE;KACzB;IACD,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC;QAChB,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE;QACxB,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE;KACnB,CAAC;IACF,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,EAGtB,EAAE;QACH,2BAA2B;QAC3B,MAAM,OAAO,GAA+B,MAAM,GAAG,CAAC,QAAQ,CAC5D,QAAQ,CAAC,OAAO,CAAC,eAAe,EAChC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CACtB,CAAC;QACF,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,MAAM,IAAI,KAAK,CACb,gEAAgE;gBAChE,yDAAyD,CAC1D,CAAC;QACJ,CAAC;QAED,4CAA4C;QAC5C,MAAM,MAAM,GAAG,MAAM,YAAY,CAAC;YAChC,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,YAAY,EAAE,IAAI,CAAC,YAAY;YAC/B,IAAI,EAAE,IAAI,CAAC,IAAI;SAChB,CAAC,CAAC;QAEH,4BAA4B;QAC5B,MAAM,GAAG,CAAC,WAAW,CACnB,QAAQ,CAAC,OAAO,CAAC,kBAAkB,EACnC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CACtB,CAAC;QAEF,2CAA2C;QAC3C,MAAM,YAAY,GAAsB,MAAM,GAAG,CAAC,WAAW,CAC3D,GAAG,CAAC,MAAM,CAAC,OAAO,EAClB;YACE,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,QAAQ,EAAE,QAAQ;YAClB,cAAc,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;SAC1C,CACF,CAAC;QAEF,wBAAwB;QACxB,MAAM,GAAG,CAAC,WAAW,CACnB,QAAQ,CAAC,OAAO,CAAC,WAAW,EAC5B;YACE,YAAY;YACZ,WAAW,EAAE,MAAM,CAAC,YAAY;YAChC,YAAY,EAAE,MAAM,CAAC,aAAa;YAClC,SAAS,EAAE,MAAM,CAAC,UAAU;SAC7B,CACF,CAAC;QAEF,OAAO;YACL,YAAY;YACZ,MAAM,EAAE,OAAO,CAAC,MAAM;SACvB,CAAC;IACJ,CAAC;CACF,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,gBAAgB,GAAG,MAAM,CAAC;IACrC,IAAI,EAAE;QACJ,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE;QAClB,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE;QACpB,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE;KACzB;IACD,OAAO,EAAE,CAAC,CAAC,IAAI,EAAE;IACjB,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;QAC3B,wBAAwB;QACxB,MAAM,UAAU,GAA8B,MAAM,GAAG,CAAC,QAAQ,CAC9D,QAAQ,CAAC,OAAO,CAAC,uBAAuB,EACxC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAC5C,CAAC;QACF,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,MAAM,IAAI,KAAK,CACb,wCAAwC,IAAI,CAAC,MAAM,IAAI,CACxD,CAAC;QACJ,CAAC;QAED,MAAM,YAAY,GAAG,UAAU,CAAC,GAAG,CAAC;QAEpC,mEAAmE;QACnE,MAAM,QAAQ,GAAiC,MAAM,GAAG,CAAC,QAAQ,CAC/D,QAAQ,CAAC,OAAO,CAAC,SAAS,EAC1B,EAAE,YAAY,EAAE,CACjB,CAAC;QACF,IAAI,QAAQ,EAAE,CAAC;YACb,IAAI,CAAC;gBACH,MAAM,KAAK,CAAC,0CAA0C,EAAE;oBACtD,MAAM,EAAE,MAAM;oBACd,OAAO,EAAE,EAAE,cAAc,EAAE,mCAAmC,EAAE;oBAChE,IAAI,EAAE,gBAAgB,QAAQ,CAAC,WAAW,EAAE;iBAC7C,CAAC,CAAC;YACL,CAAC;YAAC,MAAM,CAAC;gBACP,+DAA+D;YACjE,CAAC;YAED,0BAA0B;YAC1B,MAAM,QAAQ,GAAS,MAAM,GAAG,CAAC,WAAW,CAC1C,QAAQ,CAAC,OAAO,CAAC,YAAY,EAC7B,EAAE,YAAY,EAAE,CACjB,CAAC;QACJ,CAAC;QAED,6BAA6B;QAC7B,MAAM,aAAa,GAAS,MAAM,GAAG,CAAC,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC,UAAU,EAAE;YACvE,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,QAAQ,EAAE,QAAQ;SACnB,CAAC,CAAC;QAEH,OAAO,IAAI,CAAC;IACd,CAAC;CACF,CAAC,CAAC;AAEH,+EAA+E;AAE/E,MAAM,CAAC,MAAM,WAAW,GAAG,MAAM,CAAC;IAChC,IAAI,EAAE;QACJ,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE;QAClB,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE;QACpB,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE;KACzB;IACD,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;QAC3B,MAAM,EAAE,YAAY,EAAE,WAAW,EAAE,GAAG,MAAM,GAAG,CAAC,SAAS,CACvD,QAAQ,CAAC,OAAO,CAAC,+BAA+B,EAChD,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,YAAY,EAAE,IAAI,CAAC,YAAY,EAAE,CACtG,CAAC;QAEF,MAAM,MAAM,GAAG,kBAAkB,CAAC,WAAW,CAAC,CAAC;QAC/C,MAAM,MAAM,GAAG,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC;QAC/B,MAAM,MAAM,GAAgB,EAAE,CAAC;QAE/B,IAAI,CAAC;YACH,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,MAAM,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC;YACtE,IAAI,KAAK,IAAI,CAAC,OAAO;gBAAE,MAAM,IAAI,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,iBAAiB,CAAC,CAAC;YAC1F,MAAM,IAAI,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;YACvC,MAAM,GAAG,CAAC,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC,aAAa,EAAE;gBAC9C,YAAY;gBACZ,MAAM,EAAE,IAAI,CAAC,MAAM;gBACnB,GAAG,IAAI;aACR,CAAC,CAAC;YACH,MAAM,CAAC,QAAQ,EAAE,CAAC;QACpB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,CAAC,IAAI,CAAC;gBACV,IAAI,EAAE,SAAS;gBACf,EAAE,EAAE,OAAO;gBACX,OAAO,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;aAC1D,CAAC,CAAC;QACL,CAAC;QAED,MAAM,GAAG,CAAC,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC,gBAAgB,EAAE;YACjD,YAAY;YACZ,cAAc,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;SACzC,CAAC,CAAC;QAEH,OAAO,EAAE,IAAI,EAAE,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,CAAC;IACtC,CAAC;CACF,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,cAAc,GAAG,MAAM,CAAC;IACnC,IAAI,EAAE;QACJ,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE;QAClB,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE;QACpB,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE;QACxB,KAAK,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;QAC7B,MAAM,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;KAC/B;IACD,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;QAC3B,MAAM,EAAE,YAAY,EAAE,WAAW,EAAE,GAAG,MAAM,GAAG,CAAC,SAAS,CACvD,QAAQ,CAAC,OAAO,CAAC,+BAA+B,EAChD,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,YAAY,EAAE,IAAI,CAAC,YAAY,EAAE,CACtG,CAAC;QAEF,MAAM,MAAM,GAAG,kBAAkB,CAAC,WAAW,CAAC,CAAC;QAC/C,MAAM,MAAM,GAAG,EAAE,UAAU,EAAE,CAAC,EAAE,CAAC;QACjC,MAAM,MAAM,GAAgB,EAAE,CAAC;QAE/B,IAAI,CAAC;YACH,MAAM,SAAS,GAAG,MAAM,iBAAiB,CAAC,MAAM,EAAE;gBAChD,KAAK,EAAE,IAAI,CAAC,KAAK;gBACjB,MAAM,EAAE,IAAI,CAAC,MAAM;aACpB,CAAC,CAAC;YACH,KAAK,MAAM,OAAO,IAAI,SAAS,EAAE,CAAC;gBAChC,IAAI,OAAO,CAAC,EAAE,IAAI,IAAI;oBAAE,SAAS;gBACjC,IAAI,CAAC;oBACH,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,WAAW,EAAE,GAAG,MAAM,eAAe,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,OAAO,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;oBAC3G,IAAI,WAAW,IAAI,CAAC,QAAQ;wBAAE,MAAM,IAAI,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC;oBAE9G,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,MAAM,kBAAkB,CAAC;wBACjD,MAAM;wBACN,IAAI,EAAE,EAAE,EAAE,EAAE,OAAO,CAAC,EAAE,EAAE;wBACxB,KAAK,EAAE;4BACL,IAAI,EAAE,CAAC,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,UAAU,EAAE,iBAAiB,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,CAAC;4BAC5H,WAAW,EAAE,IAAI;yBAClB;qBACF,CAAC,CAAC;oBAEH,MAAM,IAAI,GAAG,iBAAiB,CAAC,QAAQ,EAAE,EAAE,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,CAAC,CAAC;oBAC5E,MAAM,GAAG,CAAC,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC,cAAc,EAAE;wBAC/C,YAAY;wBACZ,MAAM,EAAE,IAAI,CAAC,MAAM;wBACnB,GAAG,IAAI;qBACR,CAAC,CAAC;oBACH,MAAM,CAAC,UAAU,EAAE,CAAC;gBACtB,CAAC;gBAAC,OAAO,GAAG,EAAE,CAAC;oBACb,MAAM,CAAC,IAAI,CAAC;wBACV,IAAI,EAAE,UAAU;wBAChB,EAAE,EAAE,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;wBACtB,OAAO,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;qBAC1D,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;QACH,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,CAAC,IAAI,CAAC;gBACV,IAAI,EAAE,UAAU;gBAChB,EAAE,EAAE,OAAO;gBACX,OAAO,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;aAC1D,CAAC,CAAC;QACL,CAAC;QAED,MAAM,GAAG,CAAC,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC,gBAAgB,EAAE;YACjD,YAAY;YACZ,cAAc,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;SACzC,CAAC,CAAC;QAEH,OAAO,EAAE,IAAI,EAAE,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,CAAC;IACtC,CAAC;CACF,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,OAAO,GAAG,MAAM,CAAC;IAC5B,IAAI,EAAE;QACJ,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE;QAClB,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE;QACpB,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE;QACxB,KAAK,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;QAC7B,MAAM,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;KAC/B;IACD,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;QAC3B,MAAM,UAAU,GAAG;YACjB,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,YAAY,EAAE,IAAI,CAAC,YAAY;SAChC,CAAC;QACF,MAAM,OAAO,GAAG;YACd,EAAE,GAAG,EAAE,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,WAAW,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,UAAU,EAAE;YACzE,EAAE,GAAG,EAAE,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,cAAc,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,EAAE,GAAG,UAAU,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,EAAE;SAC/H,CAAC;QACF,MAAM,MAAM,GAA2B,EAAE,CAAC;QAC1C,MAAM,MAAM,GAAgB,EAAE,CAAC;QAC/B,KAAK,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,OAAO,EAAE,CAAC;YAClD,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;gBAChD,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;gBAC1C,MAAM,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;YAChC,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,MAAM,CAAC,IAAI,CAAC;oBACV,IAAI,EAAE,IAAI;oBACV,EAAE,EAAE,MAAM;oBACV,OAAO,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;iBAC1D,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,CAAC;IACtC,CAAC;CACF,CAAC,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { SomaError } from "../validators/shared.js";
|
|
2
|
+
type WebhookResult = {
|
|
3
|
+
errors: SomaError[];
|
|
4
|
+
items: Array<{
|
|
5
|
+
connectionId: string;
|
|
6
|
+
userId: string;
|
|
7
|
+
data: Record<string, unknown> | null;
|
|
8
|
+
}>;
|
|
9
|
+
};
|
|
10
|
+
export declare const handleStravaWebhook: import("convex/server").RegisteredAction<"public", {
|
|
11
|
+
autoIngest?: boolean | undefined;
|
|
12
|
+
clientId: string;
|
|
13
|
+
clientSecret: string;
|
|
14
|
+
payload: any;
|
|
15
|
+
}, Promise<WebhookResult>>;
|
|
16
|
+
export {};
|
|
17
|
+
//# sourceMappingURL=webhooks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhooks.d.ts","sourceRoot":"","sources":["../../../src/component/strava/webhooks.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAwBzD,KAAK,aAAa,GAAG;IACnB,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,KAAK,EAAE,KAAK,CAAC;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;CAC9F,CAAC;AAIF,eAAO,MAAM,mBAAmB;;;;;0BA+E9B,CAAC"}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
// ─── Strava Webhook Handler ─────────────────────────────────────────────────
|
|
2
|
+
// Single action that handles all Strava webhook event types.
|
|
3
|
+
// Strava sends notification-only payloads to a single endpoint — the handler
|
|
4
|
+
// fetches the actual data from the Strava API, transforms it, and optionally
|
|
5
|
+
// ingests it into the database.
|
|
6
|
+
import { v } from "convex/values";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { action } from "../_generated/server";
|
|
9
|
+
import { api, internal } from "../_generated/api";
|
|
10
|
+
import { createStravaClient } from "./client.js";
|
|
11
|
+
import { getLoggedInAthlete, getActivityById, getActivityStreams, } from "./types/stravaApi/sdk.gen.js";
|
|
12
|
+
import { transformActivity } from "./transform/activity.js";
|
|
13
|
+
import { transformAthlete } from "./transform/athlete.js";
|
|
14
|
+
// ─── Schema ─────────────────────────────────────────────────────────────────
|
|
15
|
+
const stravaWebhookPayloadSchema = z.object({
|
|
16
|
+
object_type: z.string(),
|
|
17
|
+
object_id: z.number(),
|
|
18
|
+
aspect_type: z.string(),
|
|
19
|
+
owner_id: z.number(),
|
|
20
|
+
subscription_id: z.number(),
|
|
21
|
+
event_time: z.number(),
|
|
22
|
+
updates: z.record(z.string(), z.unknown()).optional(),
|
|
23
|
+
});
|
|
24
|
+
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
25
|
+
const VALID_EVENT_NAMES = new Set([
|
|
26
|
+
"activity-create",
|
|
27
|
+
"activity-update",
|
|
28
|
+
"activity-delete",
|
|
29
|
+
"athlete-update",
|
|
30
|
+
"athlete-deauthorize",
|
|
31
|
+
]);
|
|
32
|
+
// ─── Handler ────────────────────────────────────────────────────────────────
|
|
33
|
+
export const handleStravaWebhook = action({
|
|
34
|
+
args: {
|
|
35
|
+
payload: v.any(),
|
|
36
|
+
clientId: v.string(),
|
|
37
|
+
clientSecret: v.string(),
|
|
38
|
+
autoIngest: v.optional(v.boolean()),
|
|
39
|
+
},
|
|
40
|
+
handler: async (ctx, args) => {
|
|
41
|
+
const payload = stravaWebhookPayloadSchema.parse(args.payload);
|
|
42
|
+
const { clientId, clientSecret } = args;
|
|
43
|
+
const shouldIngest = args.autoIngest !== false;
|
|
44
|
+
const eventName = `${payload.object_type}-${payload.aspect_type}`;
|
|
45
|
+
if (!VALID_EVENT_NAMES.has(eventName)) {
|
|
46
|
+
return { errors: [], items: [] };
|
|
47
|
+
}
|
|
48
|
+
// ── Resolve connection from owner_id ──────────────────────────────────
|
|
49
|
+
const connection = await ctx.runQuery(internal.private.getConnectionByProviderUserId, { providerUserId: String(payload.owner_id), provider: "STRAVA" });
|
|
50
|
+
if (!connection) {
|
|
51
|
+
return {
|
|
52
|
+
errors: [{
|
|
53
|
+
type: payload.object_type,
|
|
54
|
+
id: String(payload.object_id),
|
|
55
|
+
message: `No Soma connection found for Strava owner_id "${payload.owner_id}". ` +
|
|
56
|
+
"The user may need to reconnect to populate the provider user ID.",
|
|
57
|
+
}],
|
|
58
|
+
items: [],
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
// Allow deauthorize to proceed even if connection is inactive
|
|
62
|
+
if (!connection.active && eventName !== "athlete-deauthorize") {
|
|
63
|
+
return {
|
|
64
|
+
errors: [{
|
|
65
|
+
type: payload.object_type,
|
|
66
|
+
id: String(payload.object_id),
|
|
67
|
+
message: `Strava connection for owner_id "${payload.owner_id}" is inactive`,
|
|
68
|
+
}],
|
|
69
|
+
items: [],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
const connectionId = connection._id;
|
|
73
|
+
const userId = connection.userId;
|
|
74
|
+
// ── Dispatch by event type ───────────────────────────────────────────
|
|
75
|
+
if (eventName === "activity-create" || eventName === "activity-update") {
|
|
76
|
+
return await handleActivityCreateOrUpdate(ctx, { connectionId, userId, objectId: payload.object_id, clientId, clientSecret, shouldIngest });
|
|
77
|
+
}
|
|
78
|
+
if (eventName === "activity-delete") {
|
|
79
|
+
return {
|
|
80
|
+
errors: [],
|
|
81
|
+
items: [{ connectionId, userId, data: null }],
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
if (eventName === "athlete-update") {
|
|
85
|
+
return await handleAthleteUpdate(ctx, { connectionId, userId, clientId, clientSecret, shouldIngest });
|
|
86
|
+
}
|
|
87
|
+
if (eventName === "athlete-deauthorize") {
|
|
88
|
+
return await handleAthleteDeauthorize(ctx, { connectionId, userId, shouldIngest });
|
|
89
|
+
}
|
|
90
|
+
return { errors: [], items: [] };
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
// ─── Event Handlers ─────────────────────────────────────────────────────────
|
|
94
|
+
async function handleActivityCreateOrUpdate(ctx, args) {
|
|
95
|
+
const errors = [];
|
|
96
|
+
let accessToken;
|
|
97
|
+
try {
|
|
98
|
+
const resolved = await ctx.runAction(internal.private.resolveConnectionAndAccessToken, { userId: args.userId, provider: "STRAVA", clientId: args.clientId, clientSecret: args.clientSecret });
|
|
99
|
+
accessToken = resolved.accessToken;
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
return {
|
|
103
|
+
errors: [{
|
|
104
|
+
type: "activity",
|
|
105
|
+
id: String(args.objectId),
|
|
106
|
+
message: `Token resolution failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
107
|
+
}],
|
|
108
|
+
items: [],
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
const client = createStravaClient(accessToken);
|
|
112
|
+
try {
|
|
113
|
+
const { data: detailed, error: detailError } = await getActivityById({
|
|
114
|
+
client,
|
|
115
|
+
path: { id: args.objectId },
|
|
116
|
+
});
|
|
117
|
+
if (detailError || !detailed) {
|
|
118
|
+
throw new Error(detailError ? JSON.stringify(detailError) : "No activity data");
|
|
119
|
+
}
|
|
120
|
+
const { data: streams } = await getActivityStreams({
|
|
121
|
+
client,
|
|
122
|
+
path: { id: args.objectId },
|
|
123
|
+
query: {
|
|
124
|
+
keys: ["time", "heartrate", "watts", "cadence", "latlng", "altitude", "velocity_smooth", "grade_smooth", "distance", "temp"],
|
|
125
|
+
key_by_type: true,
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
const data = transformActivity(detailed, { streams: streams ?? undefined });
|
|
129
|
+
if (args.shouldIngest) {
|
|
130
|
+
await ctx.runMutation(api.public.ingestActivity, {
|
|
131
|
+
connectionId: args.connectionId,
|
|
132
|
+
userId: args.userId,
|
|
133
|
+
...data,
|
|
134
|
+
});
|
|
135
|
+
await ctx.runMutation(api.public.updateConnection, {
|
|
136
|
+
connectionId: args.connectionId,
|
|
137
|
+
lastDataUpdate: new Date().toISOString(),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
errors,
|
|
142
|
+
items: [{ connectionId: args.connectionId, userId: args.userId, data }],
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
errors.push({
|
|
147
|
+
type: "activity",
|
|
148
|
+
id: String(args.objectId),
|
|
149
|
+
message: err instanceof Error ? err.message : String(err),
|
|
150
|
+
});
|
|
151
|
+
return { errors, items: [] };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
async function handleAthleteUpdate(ctx, args) {
|
|
155
|
+
let accessToken;
|
|
156
|
+
try {
|
|
157
|
+
const resolved = await ctx.runAction(internal.private.resolveConnectionAndAccessToken, { userId: args.userId, provider: "STRAVA", clientId: args.clientId, clientSecret: args.clientSecret });
|
|
158
|
+
accessToken = resolved.accessToken;
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
return {
|
|
162
|
+
errors: [{
|
|
163
|
+
type: "athlete",
|
|
164
|
+
id: "fetch",
|
|
165
|
+
message: `Token resolution failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
166
|
+
}],
|
|
167
|
+
items: [],
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
const client = createStravaClient(accessToken);
|
|
171
|
+
try {
|
|
172
|
+
const { data: athlete, error } = await getLoggedInAthlete({ client });
|
|
173
|
+
if (error || !athlete) {
|
|
174
|
+
throw new Error(error ? JSON.stringify(error) : "No athlete data");
|
|
175
|
+
}
|
|
176
|
+
const data = transformAthlete(athlete);
|
|
177
|
+
if (args.shouldIngest) {
|
|
178
|
+
await ctx.runMutation(api.public.ingestAthlete, {
|
|
179
|
+
connectionId: args.connectionId,
|
|
180
|
+
userId: args.userId,
|
|
181
|
+
...data,
|
|
182
|
+
});
|
|
183
|
+
await ctx.runMutation(api.public.updateConnection, {
|
|
184
|
+
connectionId: args.connectionId,
|
|
185
|
+
lastDataUpdate: new Date().toISOString(),
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
errors: [],
|
|
190
|
+
items: [{ connectionId: args.connectionId, userId: args.userId, data }],
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
catch (err) {
|
|
194
|
+
return {
|
|
195
|
+
errors: [{
|
|
196
|
+
type: "athlete",
|
|
197
|
+
id: "fetch",
|
|
198
|
+
message: err instanceof Error ? err.message : String(err),
|
|
199
|
+
}],
|
|
200
|
+
items: [],
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
async function handleAthleteDeauthorize(ctx, args) {
|
|
205
|
+
if (args.shouldIngest) {
|
|
206
|
+
try {
|
|
207
|
+
await ctx.runMutation(internal.private.deleteTokens, {
|
|
208
|
+
connectionId: args.connectionId,
|
|
209
|
+
});
|
|
210
|
+
await ctx.runMutation(api.public.disconnect, {
|
|
211
|
+
userId: args.userId,
|
|
212
|
+
provider: "STRAVA",
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
catch (err) {
|
|
216
|
+
return {
|
|
217
|
+
errors: [{
|
|
218
|
+
type: "athlete",
|
|
219
|
+
id: "deauthorize",
|
|
220
|
+
message: err instanceof Error ? err.message : String(err),
|
|
221
|
+
}],
|
|
222
|
+
items: [{ connectionId: args.connectionId, userId: args.userId, data: null }],
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
errors: [],
|
|
228
|
+
items: [{ connectionId: args.connectionId, userId: args.userId, data: null }],
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
//# sourceMappingURL=webhooks.js.map
|