@nativesquare/soma 0.17.0 → 0.18.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/garmin.d.ts.map +1 -1
- package/dist/client/garmin.js +5 -4
- package/dist/client/garmin.js.map +1 -1
- package/dist/client/strava.d.ts.map +1 -1
- package/dist/client/strava.js +20 -4
- package/dist/client/strava.js.map +1 -1
- package/dist/client/types.d.ts +67 -34
- package/dist/client/types.d.ts.map +1 -1
- package/dist/client/types.js +11 -1
- package/dist/client/types.js.map +1 -1
- package/dist/component/_generated/component.d.ts +1 -1
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/strava/webhooks.d.ts +1 -1
- package/dist/component/strava/webhooks.d.ts.map +1 -1
- package/dist/component/strava/webhooks.js +2 -2
- package/dist/component/strava/webhooks.js.map +1 -1
- package/package.json +1 -1
- package/src/client/garmin.ts +6 -5
- package/src/client/strava.ts +23 -4
- package/src/client/types.ts +77 -36
- package/src/component/_generated/component.ts +1 -1
- package/src/component/strava/webhooks.ts +360 -359
|
@@ -1,359 +1,360 @@
|
|
|
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
|
-
|
|
7
|
-
import { v } from "convex/values";
|
|
8
|
-
import { z } from "zod";
|
|
9
|
-
import { action, type ActionCtx } from "../_generated/server";
|
|
10
|
-
import { api, internal } from "../_generated/api";
|
|
11
|
-
import type { Doc } from "../_generated/dataModel";
|
|
12
|
-
import { createStravaClient } from "./client.js";
|
|
13
|
-
import {
|
|
14
|
-
getLoggedInAthlete,
|
|
15
|
-
getActivityById,
|
|
16
|
-
getActivityStreams,
|
|
17
|
-
} from "./types/stravaApi/sdk.gen.js";
|
|
18
|
-
import { transformActivity } from "./transform/activity.js";
|
|
19
|
-
import { transformAthlete } from "./transform/athlete.js";
|
|
20
|
-
import type { SomaError, SomaErrorType, WebhookResult } from "../validators/shared.js";
|
|
21
|
-
|
|
22
|
-
// ─── Schema ─────────────────────────────────────────────────────────────────
|
|
23
|
-
|
|
24
|
-
const stravaWebhookPayloadSchema = z.object({
|
|
25
|
-
object_type: z.string(),
|
|
26
|
-
object_id: z.number(),
|
|
27
|
-
aspect_type: z.string(),
|
|
28
|
-
owner_id: z.number(),
|
|
29
|
-
subscription_id: z.number(),
|
|
30
|
-
event_time: z.number(),
|
|
31
|
-
updates: z.record(z.string(), z.unknown()).optional(),
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
35
|
-
|
|
36
|
-
const VALID_EVENT_NAMES = new Set([
|
|
37
|
-
"activity-create",
|
|
38
|
-
"activity-update",
|
|
39
|
-
"activity-delete",
|
|
40
|
-
"athlete-update",
|
|
41
|
-
"athlete-deauthorize",
|
|
42
|
-
]);
|
|
43
|
-
|
|
44
|
-
// Strava webhooks use WebhookResult with nullable data (for delete/deauthorize events).
|
|
45
|
-
type StravaWebhookResult = WebhookResult<Record<string, unknown> | null>;
|
|
46
|
-
|
|
47
|
-
// ─── Handler ────────────────────────────────────────────────────────────────
|
|
48
|
-
|
|
49
|
-
export const handleStravaWebhook = action({
|
|
50
|
-
args: {
|
|
51
|
-
payload: v.any(),
|
|
52
|
-
clientId: v.string(),
|
|
53
|
-
clientSecret: v.string(),
|
|
54
|
-
|
|
55
|
-
},
|
|
56
|
-
handler: async (ctx, args): Promise<StravaWebhookResult> => {
|
|
57
|
-
const payload = stravaWebhookPayloadSchema.parse(args.payload);
|
|
58
|
-
const { clientId, clientSecret } = args;
|
|
59
|
-
|
|
60
|
-
//
|
|
61
|
-
// `athlete-
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
}
|
|
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
|
+
|
|
7
|
+
import { v } from "convex/values";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { action, type ActionCtx } from "../_generated/server";
|
|
10
|
+
import { api, internal } from "../_generated/api";
|
|
11
|
+
import type { Doc } from "../_generated/dataModel";
|
|
12
|
+
import { createStravaClient } from "./client.js";
|
|
13
|
+
import {
|
|
14
|
+
getLoggedInAthlete,
|
|
15
|
+
getActivityById,
|
|
16
|
+
getActivityStreams,
|
|
17
|
+
} from "./types/stravaApi/sdk.gen.js";
|
|
18
|
+
import { transformActivity } from "./transform/activity.js";
|
|
19
|
+
import { transformAthlete } from "./transform/athlete.js";
|
|
20
|
+
import type { SomaError, SomaErrorType, WebhookResult } from "../validators/shared.js";
|
|
21
|
+
|
|
22
|
+
// ─── Schema ─────────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
const stravaWebhookPayloadSchema = z.object({
|
|
25
|
+
object_type: z.string(),
|
|
26
|
+
object_id: z.number(),
|
|
27
|
+
aspect_type: z.string(),
|
|
28
|
+
owner_id: z.number(),
|
|
29
|
+
subscription_id: z.number(),
|
|
30
|
+
event_time: z.number(),
|
|
31
|
+
updates: z.record(z.string(), z.unknown()).optional(),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
const VALID_EVENT_NAMES = new Set([
|
|
37
|
+
"activity-create",
|
|
38
|
+
"activity-update",
|
|
39
|
+
"activity-delete",
|
|
40
|
+
"athlete-update",
|
|
41
|
+
"athlete-deauthorize",
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
// Strava webhooks use WebhookResult with nullable data (for delete/deauthorize events).
|
|
45
|
+
type StravaWebhookResult = WebhookResult<Record<string, unknown> | null>;
|
|
46
|
+
|
|
47
|
+
// ─── Handler ────────────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
export const handleStravaWebhook = action({
|
|
50
|
+
args: {
|
|
51
|
+
payload: v.any(),
|
|
52
|
+
clientId: v.string(),
|
|
53
|
+
clientSecret: v.string(),
|
|
54
|
+
autoIngestByEvent: v.optional(v.record(v.string(), v.boolean())),
|
|
55
|
+
},
|
|
56
|
+
handler: async (ctx, args): Promise<StravaWebhookResult> => {
|
|
57
|
+
const payload = stravaWebhookPayloadSchema.parse(args.payload);
|
|
58
|
+
const { clientId, clientSecret } = args;
|
|
59
|
+
// Strava has no standalone deauthorize event: revocations arrive as
|
|
60
|
+
// `athlete-update` with `updates.authorized = "false"`. Remap to a
|
|
61
|
+
// synthetic `athlete-deauthorize` so we dispatch to the disconnect path.
|
|
62
|
+
const rawEventName = `${payload.object_type}-${payload.aspect_type}`;
|
|
63
|
+
const isDeauthorize =
|
|
64
|
+
rawEventName === "athlete-update" &&
|
|
65
|
+
String(payload.updates?.authorized) === "false";
|
|
66
|
+
const eventName = isDeauthorize ? "athlete-deauthorize" : rawEventName;
|
|
67
|
+
|
|
68
|
+
if (!VALID_EVENT_NAMES.has(eventName)) {
|
|
69
|
+
console.warn(
|
|
70
|
+
`[strava:webhook] unknown eventName=${eventName} ownerId=${payload.owner_id} objectId=${payload.object_id}`,
|
|
71
|
+
);
|
|
72
|
+
return { errors: [], items: [] };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const shouldIngest = args.autoIngestByEvent?.[eventName] ?? true;
|
|
76
|
+
|
|
77
|
+
// ── Resolve connection from owner_id ──────────────────────────────────
|
|
78
|
+
const connection: Doc<"connections"> | null = await ctx.runQuery(
|
|
79
|
+
internal.private.getConnectionByProviderUserId,
|
|
80
|
+
{ providerUserId: String(payload.owner_id), provider: "STRAVA" },
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
if (!connection) {
|
|
84
|
+
console.warn(
|
|
85
|
+
`[strava:webhook] no connection found eventName=${eventName} ownerId=${payload.owner_id}`,
|
|
86
|
+
);
|
|
87
|
+
return {
|
|
88
|
+
errors: [{
|
|
89
|
+
type: payload.object_type as SomaErrorType,
|
|
90
|
+
id: String(payload.object_id),
|
|
91
|
+
message: `No Soma connection found for Strava owner_id "${payload.owner_id}". ` +
|
|
92
|
+
"The user may need to reconnect to populate the provider user ID.",
|
|
93
|
+
}],
|
|
94
|
+
items: [],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Allow deauthorize to proceed even if connection is inactive
|
|
99
|
+
if (!connection.active && eventName !== "athlete-deauthorize") {
|
|
100
|
+
console.warn(
|
|
101
|
+
`[strava:webhook] inactive connection eventName=${eventName} ownerId=${payload.owner_id} userId=${connection.userId}`,
|
|
102
|
+
);
|
|
103
|
+
return {
|
|
104
|
+
errors: [{
|
|
105
|
+
type: payload.object_type as SomaErrorType,
|
|
106
|
+
id: String(payload.object_id),
|
|
107
|
+
message: `Strava connection for owner_id "${payload.owner_id}" is inactive`,
|
|
108
|
+
}],
|
|
109
|
+
items: [],
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
console.log(
|
|
114
|
+
`[strava:webhook] dispatching eventName=${eventName} ownerId=${payload.owner_id} userId=${connection.userId} objectId=${payload.object_id}`,
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const connectionId = connection._id;
|
|
118
|
+
const userId = connection.userId;
|
|
119
|
+
|
|
120
|
+
// ── Dispatch by event type ───────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
if (eventName === "activity-create" || eventName === "activity-update") {
|
|
123
|
+
return await handleActivityCreateOrUpdate(
|
|
124
|
+
ctx, { connectionId, userId, objectId: payload.object_id, clientId, clientSecret, shouldIngest },
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (eventName === "activity-delete") {
|
|
129
|
+
console.log(
|
|
130
|
+
`[strava:webhook:activity-delete] received userId=${userId} objectId=${payload.object_id}`,
|
|
131
|
+
);
|
|
132
|
+
return {
|
|
133
|
+
errors: [],
|
|
134
|
+
items: [{ connectionId, userId, data: null }],
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (eventName === "athlete-update") {
|
|
139
|
+
return await handleAthleteUpdate(
|
|
140
|
+
ctx, { connectionId, userId, clientId, clientSecret, shouldIngest },
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (eventName === "athlete-deauthorize") {
|
|
145
|
+
return await handleAthleteDeauthorize(
|
|
146
|
+
ctx, { connectionId, userId, shouldIngest },
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return { errors: [], items: [] };
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// ─── Event Handlers ─────────────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
async function handleActivityCreateOrUpdate(
|
|
157
|
+
ctx: ActionCtx,
|
|
158
|
+
args: {
|
|
159
|
+
connectionId: string;
|
|
160
|
+
userId: string;
|
|
161
|
+
objectId: number;
|
|
162
|
+
clientId: string;
|
|
163
|
+
clientSecret: string;
|
|
164
|
+
shouldIngest: boolean;
|
|
165
|
+
},
|
|
166
|
+
): Promise<StravaWebhookResult> {
|
|
167
|
+
const errors: SomaError[] = [];
|
|
168
|
+
|
|
169
|
+
let accessToken: string;
|
|
170
|
+
try {
|
|
171
|
+
const resolved = await ctx.runAction(
|
|
172
|
+
internal.private.resolveConnectionAndAccessToken,
|
|
173
|
+
{ userId: args.userId, provider: "STRAVA", clientId: args.clientId, clientSecret: args.clientSecret },
|
|
174
|
+
);
|
|
175
|
+
accessToken = resolved.accessToken;
|
|
176
|
+
} catch (err) {
|
|
177
|
+
console.error(
|
|
178
|
+
`[strava:webhook:activity] token resolution failed userId=${args.userId} objectId=${args.objectId} error=${err instanceof Error ? err.message : String(err)}`,
|
|
179
|
+
);
|
|
180
|
+
return {
|
|
181
|
+
errors: [{
|
|
182
|
+
type: "activity",
|
|
183
|
+
id: String(args.objectId),
|
|
184
|
+
message: `Token resolution failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
185
|
+
}],
|
|
186
|
+
items: [],
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const client = createStravaClient(accessToken);
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
const { data: detailed, error: detailError } = await getActivityById({
|
|
194
|
+
client,
|
|
195
|
+
path: { id: args.objectId },
|
|
196
|
+
});
|
|
197
|
+
if (detailError || !detailed) {
|
|
198
|
+
throw new Error(detailError ? JSON.stringify(detailError) : "No activity data");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const { data: streams } = await getActivityStreams({
|
|
202
|
+
client,
|
|
203
|
+
path: { id: args.objectId },
|
|
204
|
+
query: {
|
|
205
|
+
keys: ["time", "heartrate", "watts", "cadence", "latlng", "altitude", "velocity_smooth", "grade_smooth", "distance", "temp"],
|
|
206
|
+
key_by_type: true,
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const data = transformActivity(detailed, { streams: streams ?? undefined });
|
|
211
|
+
|
|
212
|
+
if (args.shouldIngest) {
|
|
213
|
+
await ctx.runMutation(api.public.ingestActivity, {
|
|
214
|
+
connectionId: args.connectionId,
|
|
215
|
+
userId: args.userId,
|
|
216
|
+
...data,
|
|
217
|
+
} as never);
|
|
218
|
+
await ctx.runMutation(api.public.updateConnection, {
|
|
219
|
+
connectionId: args.connectionId,
|
|
220
|
+
lastDataUpdate: new Date().toISOString(),
|
|
221
|
+
} as never);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
console.log(
|
|
225
|
+
`[strava:webhook:activity] completed userId=${args.userId} objectId=${args.objectId}`,
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
errors,
|
|
230
|
+
items: [{ connectionId: args.connectionId, userId: args.userId, data }],
|
|
231
|
+
};
|
|
232
|
+
} catch (err) {
|
|
233
|
+
console.error(
|
|
234
|
+
`[strava:webhook:activity] errored userId=${args.userId} objectId=${args.objectId} error=${err instanceof Error ? err.message : String(err)}`,
|
|
235
|
+
);
|
|
236
|
+
errors.push({
|
|
237
|
+
type: "activity",
|
|
238
|
+
id: String(args.objectId),
|
|
239
|
+
message: err instanceof Error ? err.message : String(err),
|
|
240
|
+
});
|
|
241
|
+
return { errors, items: [] };
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function handleAthleteUpdate(
|
|
246
|
+
ctx: ActionCtx,
|
|
247
|
+
args: {
|
|
248
|
+
connectionId: string;
|
|
249
|
+
userId: string;
|
|
250
|
+
clientId: string;
|
|
251
|
+
clientSecret: string;
|
|
252
|
+
shouldIngest: boolean;
|
|
253
|
+
},
|
|
254
|
+
): Promise<StravaWebhookResult> {
|
|
255
|
+
let accessToken: string;
|
|
256
|
+
try {
|
|
257
|
+
const resolved = await ctx.runAction(
|
|
258
|
+
internal.private.resolveConnectionAndAccessToken,
|
|
259
|
+
{ userId: args.userId, provider: "STRAVA", clientId: args.clientId, clientSecret: args.clientSecret },
|
|
260
|
+
);
|
|
261
|
+
accessToken = resolved.accessToken;
|
|
262
|
+
} catch (err) {
|
|
263
|
+
console.error(
|
|
264
|
+
`[strava:webhook:athlete-update] token resolution failed userId=${args.userId} error=${err instanceof Error ? err.message : String(err)}`,
|
|
265
|
+
);
|
|
266
|
+
return {
|
|
267
|
+
errors: [{
|
|
268
|
+
type: "athlete",
|
|
269
|
+
id: "fetch",
|
|
270
|
+
message: `Token resolution failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
271
|
+
}],
|
|
272
|
+
items: [],
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const client = createStravaClient(accessToken);
|
|
277
|
+
|
|
278
|
+
try {
|
|
279
|
+
const { data: athlete, error } = await getLoggedInAthlete({ client });
|
|
280
|
+
if (error || !athlete) {
|
|
281
|
+
throw new Error(error ? JSON.stringify(error) : "No athlete data");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const data = transformAthlete(athlete);
|
|
285
|
+
|
|
286
|
+
if (args.shouldIngest) {
|
|
287
|
+
await ctx.runMutation(api.public.ingestAthlete, {
|
|
288
|
+
connectionId: args.connectionId,
|
|
289
|
+
userId: args.userId,
|
|
290
|
+
...data,
|
|
291
|
+
} as never);
|
|
292
|
+
await ctx.runMutation(api.public.updateConnection, {
|
|
293
|
+
connectionId: args.connectionId,
|
|
294
|
+
lastDataUpdate: new Date().toISOString(),
|
|
295
|
+
} as never);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
console.log(
|
|
299
|
+
`[strava:webhook:athlete-update] completed userId=${args.userId}`,
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
errors: [],
|
|
304
|
+
items: [{ connectionId: args.connectionId, userId: args.userId, data }],
|
|
305
|
+
};
|
|
306
|
+
} catch (err) {
|
|
307
|
+
console.error(
|
|
308
|
+
`[strava:webhook:athlete-update] errored userId=${args.userId} error=${err instanceof Error ? err.message : String(err)}`,
|
|
309
|
+
);
|
|
310
|
+
return {
|
|
311
|
+
errors: [{
|
|
312
|
+
type: "athlete",
|
|
313
|
+
id: "fetch",
|
|
314
|
+
message: err instanceof Error ? err.message : String(err),
|
|
315
|
+
}],
|
|
316
|
+
items: [],
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async function handleAthleteDeauthorize(
|
|
322
|
+
ctx: ActionCtx,
|
|
323
|
+
args: {
|
|
324
|
+
connectionId: string;
|
|
325
|
+
userId: string;
|
|
326
|
+
shouldIngest: boolean;
|
|
327
|
+
},
|
|
328
|
+
): Promise<StravaWebhookResult> {
|
|
329
|
+
if (args.shouldIngest) {
|
|
330
|
+
try {
|
|
331
|
+
await ctx.runMutation(internal.private.deleteTokens, {
|
|
332
|
+
connectionId: args.connectionId,
|
|
333
|
+
} as never);
|
|
334
|
+
await ctx.runMutation(api.public.disconnect, {
|
|
335
|
+
userId: args.userId,
|
|
336
|
+
provider: "STRAVA",
|
|
337
|
+
});
|
|
338
|
+
console.log(
|
|
339
|
+
`[strava:webhook:deauthorize] completed userId=${args.userId}`,
|
|
340
|
+
);
|
|
341
|
+
} catch (err) {
|
|
342
|
+
console.error(
|
|
343
|
+
`[strava:webhook:deauthorize] errored userId=${args.userId} error=${err instanceof Error ? err.message : String(err)}`,
|
|
344
|
+
);
|
|
345
|
+
return {
|
|
346
|
+
errors: [{
|
|
347
|
+
type: "athlete",
|
|
348
|
+
id: "deauthorize",
|
|
349
|
+
message: err instanceof Error ? err.message : String(err),
|
|
350
|
+
}],
|
|
351
|
+
items: [{ connectionId: args.connectionId, userId: args.userId, data: null }],
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return {
|
|
357
|
+
errors: [],
|
|
358
|
+
items: [{ connectionId: args.connectionId, userId: args.userId, data: null }],
|
|
359
|
+
};
|
|
360
|
+
}
|