@nativesquare/soma 0.13.0 → 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/garmin.d.ts +4 -1
- package/dist/client/garmin.d.ts.map +1 -1
- package/dist/client/garmin.js +4 -1
- package/dist/client/garmin.js.map +1 -1
- 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 +48 -34
- package/dist/client/strava.d.ts.map +1 -1
- package/dist/client/strava.js +141 -23
- 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/athlete.d.ts +6 -0
- package/dist/component/validators/athlete.d.ts.map +1 -1
- package/dist/component/validators/athlete.js.map +1 -1
- package/dist/component/validators/nutrition.d.ts +6 -0
- package/dist/component/validators/nutrition.d.ts.map +1 -1
- package/dist/component/validators/nutrition.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/dist/component/validators/sleep.d.ts +6 -0
- package/dist/component/validators/sleep.d.ts.map +1 -1
- package/dist/component/validators/sleep.js.map +1 -1
- package/dist/validators.d.ts +7 -1
- package/dist/validators.d.ts.map +1 -1
- package/dist/validators.js +6 -6
- package/dist/validators.js.map +1 -1
- package/package.json +1 -1
- package/src/client/garmin.ts +4 -1
- package/src/client/index.ts +8 -1
- package/src/client/strava.ts +193 -27
- 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/athlete.ts +6 -0
- package/src/component/validators/nutrition.ts +6 -0
- package/src/component/validators/shared.ts +5 -2
- package/src/component/validators/sleep.ts +6 -0
- package/src/validators.ts +34 -7
- 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
package/src/client/strava.ts
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
import type { SomaComponent } from "./index.js";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
ActionCtx,
|
|
4
|
+
SomaStravaConfig,
|
|
5
|
+
RegisterRoutesOptions,
|
|
6
|
+
StravaWebhookActionResult,
|
|
7
|
+
StravaWebhookEvent,
|
|
8
|
+
StravaWebhookEventName,
|
|
9
|
+
} from "./types.js";
|
|
3
10
|
import { httpActionGeneric, type HttpRouter } from "convex/server";
|
|
4
11
|
|
|
5
|
-
export const
|
|
12
|
+
export const STRAVA_OAUTH_CALLBACK_PATH = "/api/strava/callback";
|
|
13
|
+
export const STRAVA_WEBHOOK_BASE_PATH = "/api/strava/webhook";
|
|
6
14
|
|
|
7
15
|
export class SomaStrava {
|
|
8
16
|
constructor(
|
|
9
17
|
private component: SomaComponent,
|
|
10
18
|
private requireConfig: () => SomaStravaConfig,
|
|
11
|
-
) {}
|
|
19
|
+
) { }
|
|
12
20
|
|
|
13
21
|
/**
|
|
14
22
|
* Generate a Strava OAuth authorization URL.
|
|
@@ -19,7 +27,9 @@ export class SomaStrava {
|
|
|
19
27
|
*
|
|
20
28
|
* @param ctx - Action context from the host app
|
|
21
29
|
* @param opts.userId - The host app's user identifier
|
|
22
|
-
* @param opts.redirectUri - The URL Strava will redirect to after authorization
|
|
30
|
+
* @param opts.redirectUri - The URL Strava will redirect to after authorization.
|
|
31
|
+
* This should match the `registerRoutes` callback path
|
|
32
|
+
* (default: `${CONVEX_SITE_URL}/api/strava/callback`).
|
|
23
33
|
* @param opts.scope - Comma-separated Strava OAuth scopes (default: "read,activity:read_all,profile:read_all")
|
|
24
34
|
* @returns `{ authUrl, state }`
|
|
25
35
|
*
|
|
@@ -46,32 +56,30 @@ export class SomaStrava {
|
|
|
46
56
|
}
|
|
47
57
|
|
|
48
58
|
/**
|
|
49
|
-
*
|
|
59
|
+
* Disconnect a user from Strava.
|
|
50
60
|
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
61
|
+
* Revokes the token at Strava (best-effort), deletes stored tokens,
|
|
62
|
+
* and sets the connection to inactive.
|
|
53
63
|
*
|
|
54
64
|
* @param ctx - Action context from the host app
|
|
55
65
|
* @param args.userId - The host app's user identifier
|
|
56
|
-
* @param args.after - Only sync activities after this Unix epoch timestamp (for incremental sync)
|
|
57
|
-
* @returns `{ synced, errors }`
|
|
58
66
|
*
|
|
59
67
|
* @example
|
|
60
68
|
* ```ts
|
|
61
|
-
* export const
|
|
69
|
+
* export const disconnectStrava = action({
|
|
62
70
|
* args: { userId: v.string() },
|
|
63
71
|
* handler: async (ctx, { userId }) => {
|
|
64
|
-
*
|
|
72
|
+
* await soma.strava.disconnect(ctx, { userId });
|
|
65
73
|
* },
|
|
66
74
|
* });
|
|
67
75
|
* ```
|
|
68
76
|
*/
|
|
69
|
-
async
|
|
77
|
+
async disconnect(
|
|
70
78
|
ctx: ActionCtx,
|
|
71
|
-
args: { userId: string
|
|
79
|
+
args: { userId: string },
|
|
72
80
|
) {
|
|
73
81
|
const config = this.requireConfig();
|
|
74
|
-
return await ctx.runAction(this.component.strava.public.
|
|
82
|
+
return await ctx.runAction(this.component.strava.public.disconnectStrava, {
|
|
75
83
|
...args,
|
|
76
84
|
clientId: config.clientId,
|
|
77
85
|
clientSecret: config.clientSecret,
|
|
@@ -79,30 +87,78 @@ export class SomaStrava {
|
|
|
79
87
|
}
|
|
80
88
|
|
|
81
89
|
/**
|
|
82
|
-
*
|
|
90
|
+
* Pull the authenticated athlete profile from Strava.
|
|
83
91
|
*
|
|
84
|
-
*
|
|
85
|
-
* and sets the connection to inactive.
|
|
92
|
+
* Automatically refreshes the access token if expired.
|
|
86
93
|
*
|
|
87
94
|
* @param ctx - Action context from the host app
|
|
88
95
|
* @param args.userId - The host app's user identifier
|
|
96
|
+
*/
|
|
97
|
+
async pullAthlete(
|
|
98
|
+
ctx: ActionCtx,
|
|
99
|
+
args: { userId: string },
|
|
100
|
+
) {
|
|
101
|
+
const config = this.requireConfig();
|
|
102
|
+
return await ctx.runAction(this.component.strava.public.pullAthlete, {
|
|
103
|
+
...args,
|
|
104
|
+
clientId: config.clientId,
|
|
105
|
+
clientSecret: config.clientSecret,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Pull activities from Strava.
|
|
111
|
+
*
|
|
112
|
+
* Fetches activities, transforms them into the normalized Soma schema,
|
|
113
|
+
* and ingests them with automatic deduplication.
|
|
114
|
+
*
|
|
115
|
+
* @param ctx - Action context from the host app
|
|
116
|
+
* @param args.userId - The host app's user identifier
|
|
117
|
+
* @param args.after - Only pull activities after this Unix epoch timestamp
|
|
118
|
+
* @param args.before - Only pull activities before this Unix epoch timestamp
|
|
119
|
+
*/
|
|
120
|
+
async pullActivities(
|
|
121
|
+
ctx: ActionCtx,
|
|
122
|
+
args: {
|
|
123
|
+
userId: string;
|
|
124
|
+
after?: number;
|
|
125
|
+
before?: number;
|
|
126
|
+
},
|
|
127
|
+
) {
|
|
128
|
+
const config = this.requireConfig();
|
|
129
|
+
return await ctx.runAction(this.component.strava.public.pullActivities, {
|
|
130
|
+
...args,
|
|
131
|
+
clientId: config.clientId,
|
|
132
|
+
clientSecret: config.clientSecret,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Pull all supported data types from Strava in a single call.
|
|
138
|
+
*
|
|
139
|
+
* Equivalent to calling `pullAthlete` and `pullActivities`. Automatically
|
|
140
|
+
* refreshes the access token if expired.
|
|
141
|
+
*
|
|
142
|
+
* @param ctx - Action context from the host app
|
|
143
|
+
* @param args.userId - The host app's user identifier
|
|
144
|
+
* @param args.after - Only pull activities after this Unix epoch timestamp
|
|
145
|
+
* @param args.before - Only pull activities before this Unix epoch timestamp
|
|
89
146
|
*
|
|
90
147
|
* @example
|
|
91
148
|
* ```ts
|
|
92
|
-
*
|
|
93
|
-
* args: { userId: v.string() },
|
|
94
|
-
* handler: async (ctx, { userId }) => {
|
|
95
|
-
* await soma.strava.disconnect(ctx, { userId });
|
|
96
|
-
* },
|
|
97
|
-
* });
|
|
149
|
+
* await soma.strava.pullAll(ctx, { userId: "user_123" });
|
|
98
150
|
* ```
|
|
99
151
|
*/
|
|
100
|
-
async
|
|
152
|
+
async pullAll(
|
|
101
153
|
ctx: ActionCtx,
|
|
102
|
-
args: {
|
|
154
|
+
args: {
|
|
155
|
+
userId: string;
|
|
156
|
+
after?: number;
|
|
157
|
+
before?: number;
|
|
158
|
+
},
|
|
103
159
|
) {
|
|
104
160
|
const config = this.requireConfig();
|
|
105
|
-
return await ctx.runAction(this.component.strava.public.
|
|
161
|
+
return await ctx.runAction(this.component.strava.public.pullAll, {
|
|
106
162
|
...args,
|
|
107
163
|
clientId: config.clientId,
|
|
108
164
|
clientSecret: config.clientSecret,
|
|
@@ -115,7 +171,7 @@ export class SomaStrava {
|
|
|
115
171
|
opts?: RegisterRoutesOptions["strava"],
|
|
116
172
|
) {
|
|
117
173
|
const oauth = opts?.oauth ?? {};
|
|
118
|
-
const path = oauth.path ??
|
|
174
|
+
const path = oauth.path ?? STRAVA_OAUTH_CALLBACK_PATH;
|
|
119
175
|
|
|
120
176
|
http.route({
|
|
121
177
|
path,
|
|
@@ -195,5 +251,115 @@ export class SomaStrava {
|
|
|
195
251
|
});
|
|
196
252
|
}),
|
|
197
253
|
});
|
|
254
|
+
|
|
255
|
+
// ── Strava Webhook Routes ──────────────────────────────────────
|
|
256
|
+
const webhookCfg = typeof opts?.webhook === "object" ? opts.webhook : undefined;
|
|
257
|
+
if (webhookCfg?.events) {
|
|
258
|
+
const webhookPath = webhookCfg.path ?? STRAVA_WEBHOOK_BASE_PATH;
|
|
259
|
+
const verifyToken = webhookCfg.verifyToken ?? process.env.STRAVA_WEBHOOK_VERIFY_TOKEN;
|
|
260
|
+
const autoIngest = webhookCfg.autoIngest ?? true;
|
|
261
|
+
|
|
262
|
+
// GET: Strava subscription verification challenge
|
|
263
|
+
http.route({
|
|
264
|
+
path: webhookPath,
|
|
265
|
+
method: "GET",
|
|
266
|
+
handler: httpActionGeneric(async (_ctx, request) => {
|
|
267
|
+
const url = new URL(request.url);
|
|
268
|
+
const mode = url.searchParams.get("hub.mode");
|
|
269
|
+
const challenge = url.searchParams.get("hub.challenge");
|
|
270
|
+
const token = url.searchParams.get("hub.verify_token");
|
|
271
|
+
|
|
272
|
+
if (mode === "subscribe" && token === verifyToken && challenge) {
|
|
273
|
+
return new Response(
|
|
274
|
+
JSON.stringify({ "hub.challenge": challenge }),
|
|
275
|
+
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return new Response("Forbidden", { status: 403 });
|
|
280
|
+
}),
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// POST: Strava webhook event
|
|
284
|
+
http.route({
|
|
285
|
+
path: webhookPath,
|
|
286
|
+
method: "POST",
|
|
287
|
+
handler: httpActionGeneric(async (ctx, request) => {
|
|
288
|
+
let payload: unknown;
|
|
289
|
+
try {
|
|
290
|
+
payload = await request.json();
|
|
291
|
+
} catch {
|
|
292
|
+
return new Response("Invalid JSON body", { status: 400 });
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Determine event name and check if it's registered
|
|
296
|
+
const p = payload as { object_type?: string; aspect_type?: string };
|
|
297
|
+
const eventName = `${p.object_type}-${p.aspect_type}` as StravaWebhookEventName;
|
|
298
|
+
|
|
299
|
+
if (!webhookCfg.events?.[eventName]) {
|
|
300
|
+
// Event type not registered — silently ignore
|
|
301
|
+
return new Response("OK", { status: 200 });
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const webhookClientId = opts?.clientId ?? process.env.STRAVA_CLIENT_ID;
|
|
305
|
+
const webhookClientSecret = opts?.clientSecret ?? process.env.STRAVA_CLIENT_SECRET;
|
|
306
|
+
|
|
307
|
+
if (!webhookClientId || !webhookClientSecret) {
|
|
308
|
+
console.error("[soma] Strava webhook: missing client credentials");
|
|
309
|
+
return new Response("OK", { status: 200 });
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
let result: StravaWebhookActionResult | undefined;
|
|
313
|
+
try {
|
|
314
|
+
result = await ctx.runAction(
|
|
315
|
+
component.strava.webhooks.handleStravaWebhook,
|
|
316
|
+
{ payload, clientId: webhookClientId, clientSecret: webhookClientSecret, autoIngest },
|
|
317
|
+
);
|
|
318
|
+
} catch (error) {
|
|
319
|
+
console.error(
|
|
320
|
+
"[soma] Strava webhook error:",
|
|
321
|
+
error instanceof Error ? error.message : error,
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (result) {
|
|
326
|
+
const event: StravaWebhookEvent = {
|
|
327
|
+
eventName,
|
|
328
|
+
errors: result.errors,
|
|
329
|
+
rawPayload: payload,
|
|
330
|
+
items: result.items,
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
// Fire per-event handler
|
|
334
|
+
const specificHandler = webhookCfg.events?.[eventName];
|
|
335
|
+
if (typeof specificHandler === "function") {
|
|
336
|
+
try {
|
|
337
|
+
await specificHandler(ctx, event);
|
|
338
|
+
} catch (callbackError) {
|
|
339
|
+
console.error(
|
|
340
|
+
`[soma] strava webhook events["${eventName}"] callback error:`,
|
|
341
|
+
callbackError instanceof Error ? callbackError.message : callbackError,
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Fire catch-all handler
|
|
347
|
+
if (webhookCfg.onEvent) {
|
|
348
|
+
try {
|
|
349
|
+
await webhookCfg.onEvent(ctx, event);
|
|
350
|
+
} catch (callbackError) {
|
|
351
|
+
console.error(
|
|
352
|
+
"[soma] strava webhook onEvent callback error:",
|
|
353
|
+
callbackError instanceof Error ? callbackError.message : callbackError,
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Always return 200 to prevent Strava retries
|
|
360
|
+
return new Response("OK", { status: 200 });
|
|
361
|
+
}),
|
|
362
|
+
});
|
|
363
|
+
}
|
|
198
364
|
}
|
|
199
365
|
}
|
package/src/client/types.ts
CHANGED
|
@@ -208,6 +208,79 @@ export type GarminWebhookHandler = (
|
|
|
208
208
|
event: GarminWebhookEvent,
|
|
209
209
|
) => Promise<void>;
|
|
210
210
|
|
|
211
|
+
// ─── Strava Webhook Types ───────────────────────────────────────────────────
|
|
212
|
+
|
|
213
|
+
/** Strava webhook event type names (object_type + aspect_type combined). */
|
|
214
|
+
export type StravaWebhookEventName =
|
|
215
|
+
| "activity-create"
|
|
216
|
+
| "activity-update"
|
|
217
|
+
| "activity-delete"
|
|
218
|
+
| "athlete-update"
|
|
219
|
+
| "athlete-deauthorize";
|
|
220
|
+
|
|
221
|
+
/** Args accepted by the Strava webhook handler action inside the component. */
|
|
222
|
+
export type StravaWebhookActionArgs = {
|
|
223
|
+
payload: {
|
|
224
|
+
object_type: string;
|
|
225
|
+
object_id: number;
|
|
226
|
+
aspect_type: string;
|
|
227
|
+
owner_id: number;
|
|
228
|
+
subscription_id: number;
|
|
229
|
+
event_time: number;
|
|
230
|
+
updates?: Record<string, unknown>;
|
|
231
|
+
};
|
|
232
|
+
clientId: string;
|
|
233
|
+
clientSecret: string;
|
|
234
|
+
autoIngest?: boolean;
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
/** Result returned by the Strava webhook handler action inside the component. */
|
|
238
|
+
export interface StravaWebhookActionResult {
|
|
239
|
+
errors: SomaError[];
|
|
240
|
+
items: StravaWebhookItem[];
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** A single transformed item from a Strava webhook, with user/connection mapping. */
|
|
244
|
+
export interface StravaWebhookItem {
|
|
245
|
+
/** The Soma connection ID that this data belongs to. */
|
|
246
|
+
connectionId: string;
|
|
247
|
+
/** The host app's user ID. */
|
|
248
|
+
userId: string;
|
|
249
|
+
/** The transformed data in Soma's normalized format, or null for delete/deauthorize events. */
|
|
250
|
+
data: Record<string, unknown> | null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Event data passed to Strava webhook handlers after processing.
|
|
255
|
+
*
|
|
256
|
+
* The host app receives the full set of transformed items plus the raw Strava
|
|
257
|
+
* notification payload, regardless of the `autoIngest` setting.
|
|
258
|
+
*/
|
|
259
|
+
export interface StravaWebhookEvent {
|
|
260
|
+
/** The combined event name (e.g. `"activity-create"`, `"athlete-deauthorize"`). */
|
|
261
|
+
eventName: StravaWebhookEventName;
|
|
262
|
+
/** Errors encountered during connection resolution, data fetching, or transformation. */
|
|
263
|
+
errors: SomaError[];
|
|
264
|
+
/** The raw Strava webhook notification payload. */
|
|
265
|
+
rawPayload: unknown;
|
|
266
|
+
/**
|
|
267
|
+
* Transformed items in Soma's normalized format.
|
|
268
|
+
*
|
|
269
|
+
* For `activity-create` / `activity-update` / `athlete-update`, contains the
|
|
270
|
+
* fetched and transformed data. For `activity-delete` / `athlete-deauthorize`,
|
|
271
|
+
* items have `data: null`.
|
|
272
|
+
*
|
|
273
|
+
* When `autoIngest` is `true`, these are the same items written to the database.
|
|
274
|
+
*/
|
|
275
|
+
items: StravaWebhookItem[];
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Handler for a specific Strava webhook event or the catch-all `onEvent`. */
|
|
279
|
+
export type StravaWebhookHandler = (
|
|
280
|
+
ctx: GenericActionCtx<GenericDataModel>,
|
|
281
|
+
event: StravaWebhookEvent,
|
|
282
|
+
) => Promise<void>;
|
|
283
|
+
|
|
211
284
|
// ─── Route Registration Options ─────────────────────────────────────────────
|
|
212
285
|
|
|
213
286
|
/**
|
|
@@ -275,6 +348,51 @@ export interface GarminWebhookOptions {
|
|
|
275
348
|
events?: Partial<Record<GarminWebhookEventName, GarminWebhookHandler | true>>;
|
|
276
349
|
}
|
|
277
350
|
|
|
351
|
+
export interface StravaWebhookOptions {
|
|
352
|
+
/** HTTP path for the webhook endpoint. @default "/api/strava/webhook" */
|
|
353
|
+
path?: string;
|
|
354
|
+
/**
|
|
355
|
+
* Strava webhook subscription verify token.
|
|
356
|
+
* Must match the token used when creating the subscription via Strava's API.
|
|
357
|
+
* Falls back to `STRAVA_WEBHOOK_VERIFY_TOKEN` env var.
|
|
358
|
+
*/
|
|
359
|
+
verifyToken?: string;
|
|
360
|
+
/**
|
|
361
|
+
* Whether to automatically ingest transformed data into the Soma database.
|
|
362
|
+
*
|
|
363
|
+
* When `true` (default), fetched data is transformed and written to the
|
|
364
|
+
* database automatically. When `false`, the webhook still fetches and
|
|
365
|
+
* transforms the data, but skips the database write — useful when you want
|
|
366
|
+
* to handle ingestion yourself via the `onEvent` / per-event callbacks.
|
|
367
|
+
*
|
|
368
|
+
* @default true
|
|
369
|
+
*/
|
|
370
|
+
autoIngest?: boolean;
|
|
371
|
+
/** Called after every webhook event is processed, regardless of event type. */
|
|
372
|
+
onEvent?: StravaWebhookHandler;
|
|
373
|
+
/**
|
|
374
|
+
* Per-event-type webhook handlers.
|
|
375
|
+
*
|
|
376
|
+
* Unlike Garmin (which registers per-type HTTP routes), Strava sends all
|
|
377
|
+
* events to a single endpoint. **Only event types listed here are processed.**
|
|
378
|
+
* Unlisted event types are silently ignored (200 returned, no processing).
|
|
379
|
+
*
|
|
380
|
+
* Pass a handler function for custom logic after processing,
|
|
381
|
+
* or `true` to enable default processing for that event type.
|
|
382
|
+
*
|
|
383
|
+
* @example
|
|
384
|
+
* ```ts
|
|
385
|
+
* events: {
|
|
386
|
+
* "activity-create": async (ctx, event) => { // custom side-effect },
|
|
387
|
+
* "activity-update": true, // default processing only
|
|
388
|
+
* "athlete-update": true,
|
|
389
|
+
* "athlete-deauthorize": true,
|
|
390
|
+
* }
|
|
391
|
+
* ```
|
|
392
|
+
*/
|
|
393
|
+
events?: Partial<Record<StravaWebhookEventName, StravaWebhookHandler | true>>;
|
|
394
|
+
}
|
|
395
|
+
|
|
278
396
|
export interface RegisterRoutesOptions {
|
|
279
397
|
strava?: {
|
|
280
398
|
/** Override STRAVA_CLIENT_ID env var. */
|
|
@@ -283,6 +401,13 @@ export interface RegisterRoutesOptions {
|
|
|
283
401
|
clientSecret?: string;
|
|
284
402
|
/** OAuth callback configuration. */
|
|
285
403
|
oauth?: StravaOAuthOptions;
|
|
404
|
+
/**
|
|
405
|
+
* Webhook configuration.
|
|
406
|
+
*
|
|
407
|
+
* Disabled by default. Only event types listed in `events` are processed.
|
|
408
|
+
* Omit entirely or set to `false` to skip webhook routes.
|
|
409
|
+
*/
|
|
410
|
+
webhook?: StravaWebhookOptions | false;
|
|
286
411
|
};
|
|
287
412
|
garmin?: {
|
|
288
413
|
/** Override GARMIN_CLIENT_ID env var. */
|
|
@@ -59,7 +59,6 @@ import type * as private_ from "../private.js";
|
|
|
59
59
|
import type * as public_ from "../public.js";
|
|
60
60
|
import type * as strava_auth from "../strava/auth.js";
|
|
61
61
|
import type * as strava_client from "../strava/client.js";
|
|
62
|
-
import type * as strava_private from "../strava/private.js";
|
|
63
62
|
import type * as strava_public from "../strava/public.js";
|
|
64
63
|
import type * as strava_transform_activity from "../strava/transform/activity.js";
|
|
65
64
|
import type * as strava_transform_athlete from "../strava/transform/athlete.js";
|
|
@@ -67,6 +66,7 @@ import type * as strava_transform_maps_sportType from "../strava/transform/maps/
|
|
|
67
66
|
import type * as strava_types_stravaApi_client_index from "../strava/types/stravaApi/client/index.js";
|
|
68
67
|
import type * as strava_types_stravaApi_index from "../strava/types/stravaApi/index.js";
|
|
69
68
|
import type * as strava_utils from "../strava/utils.js";
|
|
69
|
+
import type * as strava_webhooks from "../strava/webhooks.js";
|
|
70
70
|
import type * as utils from "../utils.js";
|
|
71
71
|
import type * as validators_activity from "../validators/activity.js";
|
|
72
72
|
import type * as validators_athlete from "../validators/athlete.js";
|
|
@@ -141,7 +141,6 @@ const fullApi: ApiFromModules<{
|
|
|
141
141
|
public: typeof public_;
|
|
142
142
|
"strava/auth": typeof strava_auth;
|
|
143
143
|
"strava/client": typeof strava_client;
|
|
144
|
-
"strava/private": typeof strava_private;
|
|
145
144
|
"strava/public": typeof strava_public;
|
|
146
145
|
"strava/transform/activity": typeof strava_transform_activity;
|
|
147
146
|
"strava/transform/athlete": typeof strava_transform_athlete;
|
|
@@ -149,6 +148,7 @@ const fullApi: ApiFromModules<{
|
|
|
149
148
|
"strava/types/stravaApi/client/index": typeof strava_types_stravaApi_client_index;
|
|
150
149
|
"strava/types/stravaApi/index": typeof strava_types_stravaApi_index;
|
|
151
150
|
"strava/utils": typeof strava_utils;
|
|
151
|
+
"strava/webhooks": typeof strava_webhooks;
|
|
152
152
|
utils: typeof utils;
|
|
153
153
|
"validators/activity": typeof validators_activity;
|
|
154
154
|
"validators/athlete": typeof validators_athlete;
|
|
@@ -1844,32 +1844,51 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
1844
1844
|
any,
|
|
1845
1845
|
Name
|
|
1846
1846
|
>;
|
|
1847
|
-
|
|
1847
|
+
pullActivities: FunctionReference<
|
|
1848
1848
|
"action",
|
|
1849
1849
|
"internal",
|
|
1850
1850
|
{
|
|
1851
|
-
accessToken: string;
|
|
1852
1851
|
after?: number;
|
|
1853
1852
|
before?: number;
|
|
1854
|
-
|
|
1853
|
+
clientId: string;
|
|
1854
|
+
clientSecret: string;
|
|
1855
1855
|
userId: string;
|
|
1856
1856
|
},
|
|
1857
1857
|
any,
|
|
1858
1858
|
Name
|
|
1859
1859
|
>;
|
|
1860
|
-
|
|
1860
|
+
pullAll: FunctionReference<
|
|
1861
1861
|
"action",
|
|
1862
1862
|
"internal",
|
|
1863
1863
|
{
|
|
1864
1864
|
after?: number;
|
|
1865
|
+
before?: number;
|
|
1865
1866
|
clientId: string;
|
|
1866
1867
|
clientSecret: string;
|
|
1867
1868
|
userId: string;
|
|
1868
1869
|
},
|
|
1870
|
+
any,
|
|
1871
|
+
Name
|
|
1872
|
+
>;
|
|
1873
|
+
pullAthlete: FunctionReference<
|
|
1874
|
+
"action",
|
|
1875
|
+
"internal",
|
|
1876
|
+
{ clientId: string; clientSecret: string; userId: string },
|
|
1877
|
+
any,
|
|
1878
|
+
Name
|
|
1879
|
+
>;
|
|
1880
|
+
};
|
|
1881
|
+
webhooks: {
|
|
1882
|
+
handleStravaWebhook: FunctionReference<
|
|
1883
|
+
"action",
|
|
1884
|
+
"internal",
|
|
1869
1885
|
{
|
|
1870
|
-
|
|
1871
|
-
|
|
1886
|
+
autoIngest?: boolean;
|
|
1887
|
+
clientId: string;
|
|
1888
|
+
clientSecret: string;
|
|
1889
|
+
payload: any;
|
|
1872
1890
|
},
|
|
1891
|
+
any,
|
|
1873
1892
|
Name
|
|
1874
1893
|
>;
|
|
1875
1894
|
};
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
// Pure helper functions for the Garmin OAuth 2.0 PKCE flow.
|
|
3
3
|
// Uses the Web Crypto API for SHA-256 challenge generation and global `fetch`.
|
|
4
4
|
|
|
5
|
+
import type { OAuthRefreshResult } from "../utils.js";
|
|
6
|
+
|
|
5
7
|
export interface GarminOAuth2TokenResponse {
|
|
6
8
|
access_token: string;
|
|
7
9
|
refresh_token: string;
|
|
@@ -158,7 +160,7 @@ export interface RefreshTokenOptions {
|
|
|
158
160
|
*/
|
|
159
161
|
export async function refreshToken(
|
|
160
162
|
opts: RefreshTokenOptions,
|
|
161
|
-
): Promise<
|
|
163
|
+
): Promise<OAuthRefreshResult> {
|
|
162
164
|
const body = new URLSearchParams({
|
|
163
165
|
grant_type: "refresh_token",
|
|
164
166
|
client_id: opts.clientId,
|
|
@@ -179,5 +181,10 @@ export async function refreshToken(
|
|
|
179
181
|
);
|
|
180
182
|
}
|
|
181
183
|
|
|
182
|
-
|
|
184
|
+
const raw = (await response.json()) as GarminOAuth2TokenResponse;
|
|
185
|
+
return {
|
|
186
|
+
access_token: raw.access_token,
|
|
187
|
+
refresh_token: raw.refresh_token,
|
|
188
|
+
expiresAt: Math.floor(Date.now() / 1000) + raw.expires_in,
|
|
189
|
+
};
|
|
183
190
|
}
|