@nativesquare/soma 0.9.3 → 0.10.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 +96 -33
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +80 -35
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/api.d.ts +18 -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 +43 -9
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/garmin/auth.d.ts +0 -4
- package/dist/component/garmin/auth.d.ts.map +1 -1
- package/dist/component/garmin/auth.js +0 -8
- package/dist/component/garmin/auth.js.map +1 -1
- package/dist/component/garmin/private.d.ts +20 -3
- package/dist/component/garmin/private.d.ts.map +1 -1
- package/dist/component/garmin/private.js +17 -26
- package/dist/component/garmin/private.js.map +1 -1
- package/dist/component/garmin/public.d.ts +4 -4
- package/dist/component/garmin/public.d.ts.map +1 -1
- package/dist/component/garmin/public.js +6 -1
- package/dist/component/garmin/public.js.map +1 -1
- package/dist/component/garmin/webhooks.d.ts +4 -0
- package/dist/component/garmin/webhooks.d.ts.map +1 -1
- package/dist/component/garmin/webhooks.js +23 -18
- package/dist/component/garmin/webhooks.js.map +1 -1
- package/dist/component/schema.d.ts +2 -2
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +5 -3
- package/dist/component/schema.js.map +1 -1
- package/dist/{strava → component/strava}/auth.d.ts +15 -48
- package/dist/component/strava/auth.d.ts.map +1 -0
- package/dist/{strava → component/strava}/auth.js +4 -39
- package/dist/component/strava/auth.js.map +1 -0
- package/dist/component/strava/client.d.ts +8 -0
- package/dist/component/strava/client.d.ts.map +1 -0
- package/dist/component/strava/client.js +18 -0
- package/dist/component/strava/client.js.map +1 -0
- package/dist/component/strava/private.d.ts +19 -0
- package/dist/component/strava/private.d.ts.map +1 -1
- package/dist/component/strava/private.js +52 -2
- package/dist/component/strava/private.js.map +1 -1
- package/dist/component/strava/public.d.ts +87 -12
- package/dist/component/strava/public.d.ts.map +1 -1
- package/dist/component/strava/public.js +218 -92
- package/dist/component/strava/public.js.map +1 -1
- package/dist/component/strava/transform/activity.d.ts +19 -0
- package/dist/component/strava/transform/activity.d.ts.map +1 -0
- package/dist/{strava → component/strava/transform}/activity.js +21 -41
- package/dist/component/strava/transform/activity.js.map +1 -0
- package/dist/{strava → component/strava/transform}/athlete.d.ts +4 -10
- package/dist/component/strava/transform/athlete.d.ts.map +1 -0
- package/dist/{strava → component/strava/transform}/athlete.js +2 -8
- package/dist/component/strava/transform/athlete.js.map +1 -0
- package/dist/component/strava/transform/maps/sportType.d.ts +7 -0
- package/dist/component/strava/transform/maps/sportType.d.ts.map +1 -0
- package/dist/{strava/maps/sport-type.js → component/strava/transform/maps/sportType.js} +4 -2
- package/dist/component/strava/transform/maps/sportType.js.map +1 -0
- package/dist/component/strava/types/stravaApi/client/client.gen.d.ts +3 -0
- package/dist/component/strava/types/stravaApi/client/client.gen.d.ts.map +1 -0
- package/dist/component/strava/types/stravaApi/client/client.gen.js +236 -0
- package/dist/component/strava/types/stravaApi/client/client.gen.js.map +1 -0
- package/dist/component/strava/types/stravaApi/client/index.d.ts +9 -0
- package/dist/component/strava/types/stravaApi/client/index.d.ts.map +1 -0
- package/dist/component/strava/types/stravaApi/client/index.js +7 -0
- package/dist/component/strava/types/stravaApi/client/index.js.map +1 -0
- package/dist/component/strava/types/stravaApi/client/types.gen.d.ts +118 -0
- package/dist/component/strava/types/stravaApi/client/types.gen.d.ts.map +1 -0
- package/dist/component/strava/types/stravaApi/client/types.gen.js +3 -0
- package/dist/component/strava/types/stravaApi/client/types.gen.js.map +1 -0
- package/dist/component/strava/types/stravaApi/client/utils.gen.d.ts +34 -0
- package/dist/component/strava/types/stravaApi/client/utils.gen.d.ts.map +1 -0
- package/dist/component/strava/types/stravaApi/client/utils.gen.js +229 -0
- package/dist/component/strava/types/stravaApi/client/utils.gen.js.map +1 -0
- package/dist/component/strava/types/stravaApi/client.gen.d.ts +13 -0
- package/dist/component/strava/types/stravaApi/client.gen.d.ts.map +1 -0
- package/dist/component/strava/types/stravaApi/client.gen.js +4 -0
- package/dist/component/strava/types/stravaApi/client.gen.js.map +1 -0
- package/dist/component/strava/types/stravaApi/core/auth.gen.d.ts +19 -0
- package/dist/component/strava/types/stravaApi/core/auth.gen.d.ts.map +1 -0
- package/dist/component/strava/types/stravaApi/core/auth.gen.js +15 -0
- package/dist/component/strava/types/stravaApi/core/auth.gen.js.map +1 -0
- package/dist/component/strava/types/stravaApi/core/bodySerializer.gen.d.ts +26 -0
- package/dist/component/strava/types/stravaApi/core/bodySerializer.gen.d.ts.map +1 -0
- package/dist/component/strava/types/stravaApi/core/bodySerializer.gen.js +58 -0
- package/dist/component/strava/types/stravaApi/core/bodySerializer.gen.js.map +1 -0
- package/dist/component/strava/types/stravaApi/core/params.gen.d.ts +44 -0
- package/dist/component/strava/types/stravaApi/core/params.gen.d.ts.map +1 -0
- package/dist/component/strava/types/stravaApi/core/params.gen.js +101 -0
- package/dist/component/strava/types/stravaApi/core/params.gen.js.map +1 -0
- package/dist/component/strava/types/stravaApi/core/pathSerializer.gen.d.ts +34 -0
- package/dist/component/strava/types/stravaApi/core/pathSerializer.gen.d.ts.map +1 -0
- package/dist/component/strava/types/stravaApi/core/pathSerializer.gen.js +107 -0
- package/dist/component/strava/types/stravaApi/core/pathSerializer.gen.js.map +1 -0
- package/dist/component/strava/types/stravaApi/core/queryKeySerializer.gen.d.ts +19 -0
- package/dist/component/strava/types/stravaApi/core/queryKeySerializer.gen.d.ts.map +1 -0
- package/dist/component/strava/types/stravaApi/core/queryKeySerializer.gen.js +93 -0
- package/dist/component/strava/types/stravaApi/core/queryKeySerializer.gen.js.map +1 -0
- package/dist/component/strava/types/stravaApi/core/serverSentEvents.gen.d.ts +72 -0
- package/dist/component/strava/types/stravaApi/core/serverSentEvents.gen.d.ts.map +1 -0
- package/dist/component/strava/types/stravaApi/core/serverSentEvents.gen.js +134 -0
- package/dist/component/strava/types/stravaApi/core/serverSentEvents.gen.js.map +1 -0
- package/dist/component/strava/types/stravaApi/core/types.gen.d.ts +79 -0
- package/dist/component/strava/types/stravaApi/core/types.gen.d.ts.map +1 -0
- package/dist/component/strava/types/stravaApi/core/types.gen.js +3 -0
- package/dist/component/strava/types/stravaApi/core/types.gen.js.map +1 -0
- package/dist/component/strava/types/stravaApi/core/utils.gen.d.ts +20 -0
- package/dist/component/strava/types/stravaApi/core/utils.gen.d.ts.map +1 -0
- package/dist/component/strava/types/stravaApi/core/utils.gen.js +88 -0
- package/dist/component/strava/types/stravaApi/core/utils.gen.js.map +1 -0
- package/dist/component/strava/types/stravaApi/index.d.ts +3 -0
- package/dist/component/strava/types/stravaApi/index.d.ts.map +1 -0
- package/dist/component/strava/types/stravaApi/index.js +3 -0
- package/dist/component/strava/types/stravaApi/index.js.map +1 -0
- package/dist/component/strava/types/stravaApi/sdk.gen.d.ts +224 -0
- package/dist/component/strava/types/stravaApi/sdk.gen.d.ts.map +1 -0
- package/dist/component/strava/types/stravaApi/sdk.gen.js +361 -0
- package/dist/component/strava/types/stravaApi/sdk.gen.js.map +1 -0
- package/dist/component/strava/types/stravaApi/types.gen.d.ts +2209 -0
- package/dist/component/strava/types/stravaApi/types.gen.d.ts.map +1 -0
- package/dist/component/strava/types/stravaApi/types.gen.js +3 -0
- package/dist/component/strava/types/stravaApi/types.gen.js.map +1 -0
- package/dist/component/strava/types/stravaApi/zod.gen.d.ts +5332 -0
- package/dist/component/strava/types/stravaApi/zod.gen.d.ts.map +1 -0
- package/dist/component/strava/types/stravaApi/zod.gen.js +1009 -0
- package/dist/component/strava/types/stravaApi/zod.gen.js.map +1 -0
- package/dist/component/strava/utils.d.ts +15 -0
- package/dist/component/strava/utils.d.ts.map +1 -0
- package/dist/component/strava/utils.js +36 -0
- package/dist/component/strava/utils.js.map +1 -0
- package/dist/component/utils.d.ts +5 -0
- package/dist/component/utils.d.ts.map +1 -0
- package/dist/component/utils.js +11 -0
- package/dist/component/utils.js.map +1 -0
- package/package.json +131 -130
- package/src/client/index.ts +121 -52
- package/src/component/_generated/api.ts +18 -0
- package/src/component/_generated/component.ts +44 -11
- package/src/component/garmin/auth.ts +0 -9
- package/src/component/garmin/private.ts +0 -12
- package/src/component/garmin/public.ts +8 -1
- package/src/component/schema.ts +5 -3
- package/src/{strava → component/strava}/auth.ts +143 -185
- package/src/component/strava/client.ts +20 -0
- package/src/component/strava/private.ts +147 -89
- package/src/component/strava/public.ts +268 -110
- package/src/{strava → component/strava/transform}/activity.ts +256 -276
- package/src/{strava → component/strava/transform}/athlete.ts +41 -47
- package/src/{strava/maps/sport-type.ts → component/strava/transform/maps/sportType.ts} +100 -99
- package/src/component/strava/types/specs/strava-api.json +4796 -0
- package/src/component/strava/types/stravaApi/client/client.gen.ts +290 -0
- package/src/component/strava/types/stravaApi/client/index.ts +25 -0
- package/src/component/strava/types/stravaApi/client/types.gen.ts +214 -0
- package/src/component/strava/types/stravaApi/client/utils.gen.ts +316 -0
- package/src/component/strava/types/stravaApi/client.gen.ts +16 -0
- package/src/component/strava/types/stravaApi/core/auth.gen.ts +41 -0
- package/src/component/strava/types/stravaApi/core/bodySerializer.gen.ts +82 -0
- package/src/component/strava/types/stravaApi/core/params.gen.ts +169 -0
- package/src/component/strava/types/stravaApi/core/pathSerializer.gen.ts +171 -0
- package/src/component/strava/types/stravaApi/core/queryKeySerializer.gen.ts +117 -0
- package/src/component/strava/types/stravaApi/core/serverSentEvents.gen.ts +243 -0
- package/src/component/strava/types/stravaApi/core/types.gen.ts +104 -0
- package/src/component/strava/types/stravaApi/core/utils.gen.ts +140 -0
- package/src/component/strava/types/stravaApi/index.ts +4 -0
- package/src/component/strava/types/stravaApi/sdk.gen.ts +410 -0
- package/src/component/strava/types/stravaApi/types.gen.ts +2435 -0
- package/src/component/strava/types/stravaApi/zod.gen.ts +1132 -0
- package/src/component/strava/utils.ts +52 -0
- package/src/component/utils.ts +11 -0
- package/dist/strava/activity.d.ts +0 -121
- package/dist/strava/activity.d.ts.map +0 -1
- package/dist/strava/activity.js.map +0 -1
- package/dist/strava/athlete.d.ts.map +0 -1
- package/dist/strava/athlete.js.map +0 -1
- package/dist/strava/auth.d.ts.map +0 -1
- package/dist/strava/auth.js.map +0 -1
- package/dist/strava/client.d.ts +0 -93
- package/dist/strava/client.d.ts.map +0 -1
- package/dist/strava/client.js +0 -158
- package/dist/strava/client.js.map +0 -1
- package/dist/strava/index.d.ts +0 -13
- package/dist/strava/index.d.ts.map +0 -1
- package/dist/strava/index.js +0 -17
- package/dist/strava/index.js.map +0 -1
- package/dist/strava/maps/sport-type.d.ts +0 -7
- package/dist/strava/maps/sport-type.d.ts.map +0 -1
- package/dist/strava/maps/sport-type.js.map +0 -1
- package/dist/strava/sync.d.ts +0 -104
- package/dist/strava/sync.d.ts.map +0 -1
- package/dist/strava/sync.js +0 -87
- package/dist/strava/sync.js.map +0 -1
- package/dist/strava/types.d.ts +0 -266
- package/dist/strava/types.d.ts.map +0 -1
- package/dist/strava/types.js +0 -8
- package/dist/strava/types.js.map +0 -1
- package/src/strava/activity.test.ts +0 -415
- package/src/strava/athlete.test.ts +0 -139
- package/src/strava/auth.test.ts +0 -78
- package/src/strava/client.ts +0 -212
- package/src/strava/index.ts +0 -54
- package/src/strava/maps/sport-type.test.ts +0 -69
- package/src/strava/sync.ts +0 -168
- package/src/strava/types.ts +0 -361
package/src/client/index.ts
CHANGED
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
type GenericDataModel,
|
|
8
8
|
type HttpRouter,
|
|
9
9
|
} from "convex/server";
|
|
10
|
-
import { buildAuthUrl } from "../strava/auth.js";
|
|
11
10
|
|
|
12
11
|
export type SomaComponent = ComponentApi;
|
|
13
12
|
|
|
@@ -30,12 +29,6 @@ export interface SomaStravaConfig {
|
|
|
30
29
|
clientId: string;
|
|
31
30
|
/** Your Strava application's Client Secret. */
|
|
32
31
|
clientSecret: string;
|
|
33
|
-
/**
|
|
34
|
-
* Base URL of the Strava API (without `/api/v3` suffix).
|
|
35
|
-
* Defaults to `https://www.strava.com`.
|
|
36
|
-
* Override to point at a mock server during development.
|
|
37
|
-
*/
|
|
38
|
-
baseUrl?: string;
|
|
39
32
|
}
|
|
40
33
|
|
|
41
34
|
/**
|
|
@@ -73,7 +66,7 @@ export interface SomaGarminConfig {
|
|
|
73
66
|
*
|
|
74
67
|
* // Or with explicit Strava config:
|
|
75
68
|
* // const soma = new Soma(components.soma, {
|
|
76
|
-
* // strava: { clientId: "...", clientSecret: "..."
|
|
69
|
+
* // strava: { clientId: "...", clientSecret: "..." },
|
|
77
70
|
* // });
|
|
78
71
|
*
|
|
79
72
|
* // Connect a user to a provider:
|
|
@@ -109,7 +102,6 @@ export class Soma {
|
|
|
109
102
|
return {
|
|
110
103
|
clientId,
|
|
111
104
|
clientSecret,
|
|
112
|
-
baseUrl: process.env.STRAVA_BASE_URL,
|
|
113
105
|
};
|
|
114
106
|
}
|
|
115
107
|
|
|
@@ -885,35 +877,41 @@ export class Soma {
|
|
|
885
877
|
// environment variables or the constructor.
|
|
886
878
|
|
|
887
879
|
/**
|
|
888
|
-
*
|
|
880
|
+
* Generate a Strava OAuth authorization URL.
|
|
881
|
+
*
|
|
882
|
+
* If `userId` is provided, the state parameter is stored inside the
|
|
883
|
+
* component automatically, and the callback handler registered by
|
|
884
|
+
* `registerRoutes` will complete the flow without further host-app
|
|
885
|
+
* intervention. This is the recommended approach.
|
|
889
886
|
*
|
|
890
|
-
*
|
|
891
|
-
*
|
|
887
|
+
* If `userId` is omitted, the host app must store the returned `state`
|
|
888
|
+
* itself and handle the callback via `connectStrava`.
|
|
892
889
|
*
|
|
890
|
+
* @param ctx - Action context from the host app
|
|
893
891
|
* @param opts.redirectUri - The URL Strava will redirect to after authorization
|
|
894
892
|
* @param opts.scope - Comma-separated Strava OAuth scopes (default: "read,activity:read_all,profile:read_all")
|
|
895
|
-
* @param opts.
|
|
896
|
-
* @returns
|
|
893
|
+
* @param opts.userId - The host app's user identifier (required for `registerRoutes` flow)
|
|
894
|
+
* @returns `{ authUrl, state }`
|
|
897
895
|
*
|
|
898
896
|
* @example
|
|
899
897
|
* ```ts
|
|
900
|
-
* const
|
|
901
|
-
*
|
|
898
|
+
* const { authUrl } = await soma.getStravaAuthUrl(ctx, {
|
|
899
|
+
* userId: "user_123",
|
|
900
|
+
* redirectUri: "https://your-app.convex.site/api/strava/callback",
|
|
902
901
|
* });
|
|
902
|
+
* // Redirect user to authUrl — the callback is handled automatically
|
|
903
903
|
* ```
|
|
904
904
|
*/
|
|
905
|
-
getStravaAuthUrl(
|
|
906
|
-
|
|
907
|
-
scope?: string;
|
|
908
|
-
|
|
909
|
-
}): string {
|
|
905
|
+
async getStravaAuthUrl(
|
|
906
|
+
ctx: ActionCtx,
|
|
907
|
+
opts: { redirectUri: string; scope?: string; userId?: string },
|
|
908
|
+
) {
|
|
910
909
|
const config = this.requireStravaConfig();
|
|
911
|
-
return
|
|
910
|
+
return await ctx.runAction(this.component.strava.public.getStravaAuthUrl, {
|
|
912
911
|
clientId: config.clientId,
|
|
913
912
|
redirectUri: opts.redirectUri,
|
|
914
913
|
scope: opts.scope,
|
|
915
|
-
|
|
916
|
-
baseUrl: config.baseUrl,
|
|
914
|
+
userId: opts.userId,
|
|
917
915
|
});
|
|
918
916
|
}
|
|
919
917
|
|
|
@@ -930,7 +928,6 @@ export class Soma {
|
|
|
930
928
|
* @param ctx - Action context from the host app
|
|
931
929
|
* @param args.userId - The host app's user identifier
|
|
932
930
|
* @param args.code - The authorization code from the OAuth callback
|
|
933
|
-
* @param args.includeStreams - Fetch detailed streams per activity (default: false)
|
|
934
931
|
* @returns `{ connectionId, synced, errors }`
|
|
935
932
|
*
|
|
936
933
|
* @example
|
|
@@ -945,17 +942,43 @@ export class Soma {
|
|
|
945
942
|
*/
|
|
946
943
|
async connectStrava(
|
|
947
944
|
ctx: ActionCtx,
|
|
948
|
-
args: { userId: string; code: string
|
|
945
|
+
args: { userId: string; code: string },
|
|
949
946
|
) {
|
|
950
947
|
const config = this.requireStravaConfig();
|
|
951
948
|
return await ctx.runAction(this.component.strava.public.connectStrava, {
|
|
952
949
|
...args,
|
|
953
950
|
clientId: config.clientId,
|
|
954
951
|
clientSecret: config.clientSecret,
|
|
955
|
-
baseUrl: config.baseUrl,
|
|
956
952
|
});
|
|
957
953
|
}
|
|
958
954
|
|
|
955
|
+
/**
|
|
956
|
+
* Complete a Strava OAuth flow using stored pending state.
|
|
957
|
+
*
|
|
958
|
+
* This is called automatically by the `registerRoutes` callback handler.
|
|
959
|
+
* It looks up the pending OAuth state stored during `getStravaAuthUrl`,
|
|
960
|
+
* exchanges for tokens, creates the connection, and syncs data.
|
|
961
|
+
*
|
|
962
|
+
* @param ctx - Action context from the host app
|
|
963
|
+
* @param args.code - The authorization code from the callback query params
|
|
964
|
+
* @param args.state - The state parameter from the callback query params
|
|
965
|
+
* @returns `{ connectionId, userId, synced, errors }`
|
|
966
|
+
*/
|
|
967
|
+
async completeStravaOAuth(
|
|
968
|
+
ctx: ActionCtx,
|
|
969
|
+
args: { code: string; state: string },
|
|
970
|
+
) {
|
|
971
|
+
const config = this.requireStravaConfig();
|
|
972
|
+
return await ctx.runAction(
|
|
973
|
+
this.component.strava.public.completeStravaOAuth,
|
|
974
|
+
{
|
|
975
|
+
...args,
|
|
976
|
+
clientId: config.clientId,
|
|
977
|
+
clientSecret: config.clientSecret,
|
|
978
|
+
},
|
|
979
|
+
);
|
|
980
|
+
}
|
|
981
|
+
|
|
959
982
|
/**
|
|
960
983
|
* Sync activities from Strava for an already-connected user.
|
|
961
984
|
*
|
|
@@ -964,7 +987,6 @@ export class Soma {
|
|
|
964
987
|
*
|
|
965
988
|
* @param ctx - Action context from the host app
|
|
966
989
|
* @param args.userId - The host app's user identifier
|
|
967
|
-
* @param args.includeStreams - Fetch detailed streams per activity (default: false)
|
|
968
990
|
* @param args.after - Only sync activities after this Unix epoch timestamp (for incremental sync)
|
|
969
991
|
* @returns `{ synced, errors }`
|
|
970
992
|
*
|
|
@@ -973,21 +995,20 @@ export class Soma {
|
|
|
973
995
|
* export const syncStrava = action({
|
|
974
996
|
* args: { userId: v.string() },
|
|
975
997
|
* handler: async (ctx, { userId }) => {
|
|
976
|
-
* return await soma.syncStrava(ctx, { userId
|
|
998
|
+
* return await soma.syncStrava(ctx, { userId });
|
|
977
999
|
* },
|
|
978
1000
|
* });
|
|
979
1001
|
* ```
|
|
980
1002
|
*/
|
|
981
1003
|
async syncStrava(
|
|
982
1004
|
ctx: ActionCtx,
|
|
983
|
-
args: { userId: string;
|
|
1005
|
+
args: { userId: string; after?: number },
|
|
984
1006
|
) {
|
|
985
1007
|
const config = this.requireStravaConfig();
|
|
986
1008
|
return await ctx.runAction(this.component.strava.public.syncStrava, {
|
|
987
1009
|
...args,
|
|
988
1010
|
clientId: config.clientId,
|
|
989
1011
|
clientSecret: config.clientSecret,
|
|
990
|
-
baseUrl: config.baseUrl,
|
|
991
1012
|
});
|
|
992
1013
|
}
|
|
993
1014
|
|
|
@@ -1019,7 +1040,6 @@ export class Soma {
|
|
|
1019
1040
|
...args,
|
|
1020
1041
|
clientId: config.clientId,
|
|
1021
1042
|
clientSecret: config.clientSecret,
|
|
1022
|
-
baseUrl: config.baseUrl,
|
|
1023
1043
|
});
|
|
1024
1044
|
}
|
|
1025
1045
|
|
|
@@ -1253,17 +1273,31 @@ type PaginateTimeRangeArgs = TimeRangeArgs & {
|
|
|
1253
1273
|
/**
|
|
1254
1274
|
* Per-provider options for `registerRoutes`.
|
|
1255
1275
|
*/
|
|
1256
|
-
export interface
|
|
1276
|
+
export interface StravaOAuthOptions {
|
|
1257
1277
|
/** HTTP path for the OAuth callback. @default "/api/strava/callback" */
|
|
1258
1278
|
path?: string;
|
|
1259
1279
|
/** Override STRAVA_CLIENT_ID env var. */
|
|
1260
1280
|
clientId?: string;
|
|
1261
1281
|
/** Override STRAVA_CLIENT_SECRET env var. */
|
|
1262
1282
|
clientSecret?: string;
|
|
1263
|
-
/** Override STRAVA_BASE_URL env var. */
|
|
1264
|
-
baseUrl?: string;
|
|
1265
1283
|
/** URL to redirect the user to after a successful connection. */
|
|
1266
|
-
|
|
1284
|
+
redirectTo?: string;
|
|
1285
|
+
/** Called after Strava OAuth completes and initial data sync finishes. */
|
|
1286
|
+
onComplete?: (
|
|
1287
|
+
ctx: GenericActionCtx<GenericDataModel>,
|
|
1288
|
+
event: StravaConnectEvent,
|
|
1289
|
+
) => Promise<void>;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
// ─── Strava Callback Event Types ────────────────────────────────────────────
|
|
1293
|
+
|
|
1294
|
+
/** Data passed to `onComplete` after Strava OAuth + initial sync. */
|
|
1295
|
+
export interface StravaConnectEvent {
|
|
1296
|
+
provider: "STRAVA";
|
|
1297
|
+
userId: string;
|
|
1298
|
+
connectionId: string;
|
|
1299
|
+
synced: Record<string, number>;
|
|
1300
|
+
errors: Array<{ type: string; id: string; error: string }>;
|
|
1267
1301
|
}
|
|
1268
1302
|
|
|
1269
1303
|
// ─── Garmin Callback Event Types ─────────────────────────────────────────────
|
|
@@ -1327,7 +1361,10 @@ export interface GarminWebhookOptions {
|
|
|
1327
1361
|
}
|
|
1328
1362
|
|
|
1329
1363
|
export interface RegisterRoutesOptions {
|
|
1330
|
-
strava?:
|
|
1364
|
+
strava?: {
|
|
1365
|
+
/** OAuth callback configuration. */
|
|
1366
|
+
oauth?: StravaOAuthOptions;
|
|
1367
|
+
};
|
|
1331
1368
|
garmin?: {
|
|
1332
1369
|
/** OAuth callback configuration. */
|
|
1333
1370
|
oauth?: GarminOAuthOptions;
|
|
@@ -1369,8 +1406,17 @@ export interface RegisterRoutesOptions {
|
|
|
1369
1406
|
* // With Garmin OAuth callbacks and per-type webhook handlers
|
|
1370
1407
|
* registerRoutes(http, components.soma, {
|
|
1371
1408
|
* strava: {
|
|
1372
|
-
*
|
|
1373
|
-
*
|
|
1409
|
+
* oauth: {
|
|
1410
|
+
* path: "/oauth/strava/callback",
|
|
1411
|
+
* redirectTo: "https://myapp.com/settings",
|
|
1412
|
+
* onComplete: async (ctx, event) => {
|
|
1413
|
+
* // Runs after OAuth + initial sync completes
|
|
1414
|
+
* await ctx.runMutation(internal.users.markConnected, {
|
|
1415
|
+
* userId: event.userId,
|
|
1416
|
+
* provider: event.provider,
|
|
1417
|
+
* });
|
|
1418
|
+
* },
|
|
1419
|
+
* },
|
|
1374
1420
|
* },
|
|
1375
1421
|
* garmin: {
|
|
1376
1422
|
* oauth: {
|
|
@@ -1406,8 +1452,9 @@ export function registerRoutes(
|
|
|
1406
1452
|
const registerAll = opts === undefined;
|
|
1407
1453
|
|
|
1408
1454
|
if (registerAll || opts?.strava) {
|
|
1409
|
-
const
|
|
1410
|
-
const
|
|
1455
|
+
const stravaOpts = opts?.strava ?? {};
|
|
1456
|
+
const oauth = stravaOpts.oauth ?? {};
|
|
1457
|
+
const path = oauth.path ?? STRAVA_CALLBACK_PATH;
|
|
1411
1458
|
|
|
1412
1459
|
http.route({
|
|
1413
1460
|
path,
|
|
@@ -1415,23 +1462,23 @@ export function registerRoutes(
|
|
|
1415
1462
|
handler: httpActionGeneric(async (ctx, request) => {
|
|
1416
1463
|
const url = new URL(request.url);
|
|
1417
1464
|
const code = url.searchParams.get("code");
|
|
1418
|
-
const
|
|
1465
|
+
const state = url.searchParams.get("state");
|
|
1419
1466
|
|
|
1420
1467
|
if (!code) {
|
|
1421
1468
|
return new Response("Missing authorization code", { status: 400 });
|
|
1422
1469
|
}
|
|
1423
|
-
if (!
|
|
1470
|
+
if (!state) {
|
|
1424
1471
|
return new Response(
|
|
1425
|
-
"Missing state parameter
|
|
1426
|
-
"
|
|
1472
|
+
"Missing state parameter. Ensure the state was included " +
|
|
1473
|
+
"when building the Strava auth URL via getStravaAuthUrl.",
|
|
1427
1474
|
{ status: 400 },
|
|
1428
1475
|
);
|
|
1429
1476
|
}
|
|
1430
1477
|
|
|
1431
1478
|
const clientId =
|
|
1432
|
-
|
|
1479
|
+
oauth.clientId ?? process.env.STRAVA_CLIENT_ID;
|
|
1433
1480
|
const clientSecret =
|
|
1434
|
-
|
|
1481
|
+
oauth.clientSecret ?? process.env.STRAVA_CLIENT_SECRET;
|
|
1435
1482
|
|
|
1436
1483
|
if (!clientId || !clientSecret) {
|
|
1437
1484
|
return new Response(
|
|
@@ -1441,13 +1488,18 @@ export function registerRoutes(
|
|
|
1441
1488
|
);
|
|
1442
1489
|
}
|
|
1443
1490
|
|
|
1491
|
+
let result: {
|
|
1492
|
+
connectionId: string;
|
|
1493
|
+
userId: string;
|
|
1494
|
+
synced: Record<string, number>;
|
|
1495
|
+
errors: Array<{ type: string; id: string; error: string }>;
|
|
1496
|
+
};
|
|
1444
1497
|
try {
|
|
1445
|
-
await ctx.runAction(component.strava.public.
|
|
1446
|
-
userId,
|
|
1498
|
+
result = await ctx.runAction(component.strava.public.completeStravaOAuth, {
|
|
1447
1499
|
code,
|
|
1500
|
+
state,
|
|
1448
1501
|
clientId,
|
|
1449
1502
|
clientSecret,
|
|
1450
|
-
baseUrl: strava.baseUrl ?? process.env.STRAVA_BASE_URL,
|
|
1451
1503
|
});
|
|
1452
1504
|
} catch (error) {
|
|
1453
1505
|
const message =
|
|
@@ -1457,10 +1509,27 @@ export function registerRoutes(
|
|
|
1457
1509
|
});
|
|
1458
1510
|
}
|
|
1459
1511
|
|
|
1460
|
-
if (
|
|
1512
|
+
if (oauth.onComplete) {
|
|
1513
|
+
try {
|
|
1514
|
+
await oauth.onComplete(ctx, {
|
|
1515
|
+
provider: "STRAVA",
|
|
1516
|
+
userId: result.userId,
|
|
1517
|
+
connectionId: result.connectionId,
|
|
1518
|
+
synced: result.synced,
|
|
1519
|
+
errors: result.errors,
|
|
1520
|
+
});
|
|
1521
|
+
} catch (callbackError) {
|
|
1522
|
+
console.error(
|
|
1523
|
+
"[soma] strava onComplete callback error:",
|
|
1524
|
+
callbackError instanceof Error ? callbackError.message : callbackError,
|
|
1525
|
+
);
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
if (oauth.redirectTo) {
|
|
1461
1530
|
return new Response(null, {
|
|
1462
1531
|
status: 302,
|
|
1463
|
-
headers: { Location:
|
|
1532
|
+
headers: { Location: oauth.redirectTo },
|
|
1464
1533
|
});
|
|
1465
1534
|
}
|
|
1466
1535
|
|
|
@@ -57,8 +57,17 @@ import type * as garmin_utils from "../garmin/utils.js";
|
|
|
57
57
|
import type * as garmin_webhooks from "../garmin/webhooks.js";
|
|
58
58
|
import type * as private_ from "../private.js";
|
|
59
59
|
import type * as public_ from "../public.js";
|
|
60
|
+
import type * as strava_auth from "../strava/auth.js";
|
|
61
|
+
import type * as strava_client from "../strava/client.js";
|
|
60
62
|
import type * as strava_private from "../strava/private.js";
|
|
61
63
|
import type * as strava_public from "../strava/public.js";
|
|
64
|
+
import type * as strava_transform_activity from "../strava/transform/activity.js";
|
|
65
|
+
import type * as strava_transform_athlete from "../strava/transform/athlete.js";
|
|
66
|
+
import type * as strava_transform_maps_sportType from "../strava/transform/maps/sportType.js";
|
|
67
|
+
import type * as strava_types_stravaApi_client_index from "../strava/types/stravaApi/client/index.js";
|
|
68
|
+
import type * as strava_types_stravaApi_index from "../strava/types/stravaApi/index.js";
|
|
69
|
+
import type * as strava_utils from "../strava/utils.js";
|
|
70
|
+
import type * as utils from "../utils.js";
|
|
62
71
|
import type * as validators_activity from "../validators/activity.js";
|
|
63
72
|
import type * as validators_athlete from "../validators/athlete.js";
|
|
64
73
|
import type * as validators_body from "../validators/body.js";
|
|
@@ -130,8 +139,17 @@ const fullApi: ApiFromModules<{
|
|
|
130
139
|
"garmin/webhooks": typeof garmin_webhooks;
|
|
131
140
|
private: typeof private_;
|
|
132
141
|
public: typeof public_;
|
|
142
|
+
"strava/auth": typeof strava_auth;
|
|
143
|
+
"strava/client": typeof strava_client;
|
|
133
144
|
"strava/private": typeof strava_private;
|
|
134
145
|
"strava/public": typeof strava_public;
|
|
146
|
+
"strava/transform/activity": typeof strava_transform_activity;
|
|
147
|
+
"strava/transform/athlete": typeof strava_transform_athlete;
|
|
148
|
+
"strava/transform/maps/sportType": typeof strava_transform_maps_sportType;
|
|
149
|
+
"strava/types/stravaApi/client/index": typeof strava_types_stravaApi_client_index;
|
|
150
|
+
"strava/types/stravaApi/index": typeof strava_types_stravaApi_index;
|
|
151
|
+
"strava/utils": typeof strava_utils;
|
|
152
|
+
utils: typeof utils;
|
|
135
153
|
"validators/activity": typeof validators_activity;
|
|
136
154
|
"validators/athlete": typeof validators_athlete;
|
|
137
155
|
"validators/body": typeof validators_body;
|
|
@@ -1647,34 +1647,69 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
1647
1647
|
};
|
|
1648
1648
|
strava: {
|
|
1649
1649
|
public: {
|
|
1650
|
+
completeStravaOAuth: FunctionReference<
|
|
1651
|
+
"action",
|
|
1652
|
+
"internal",
|
|
1653
|
+
{
|
|
1654
|
+
clientId: string;
|
|
1655
|
+
clientSecret: string;
|
|
1656
|
+
code: string;
|
|
1657
|
+
state: string;
|
|
1658
|
+
},
|
|
1659
|
+
{
|
|
1660
|
+
connectionId: string;
|
|
1661
|
+
errors: Array<{ error: string; id: string; type: string }>;
|
|
1662
|
+
synced: { activities: number; athletes: number };
|
|
1663
|
+
userId: string;
|
|
1664
|
+
},
|
|
1665
|
+
Name
|
|
1666
|
+
>;
|
|
1650
1667
|
connectStrava: FunctionReference<
|
|
1651
1668
|
"action",
|
|
1652
1669
|
"internal",
|
|
1653
1670
|
{
|
|
1654
|
-
baseUrl?: string;
|
|
1655
1671
|
clientId: string;
|
|
1656
1672
|
clientSecret: string;
|
|
1657
1673
|
code: string;
|
|
1658
|
-
includeStreams?: boolean;
|
|
1659
1674
|
userId: string;
|
|
1660
1675
|
},
|
|
1661
1676
|
{
|
|
1662
1677
|
connectionId: string;
|
|
1663
|
-
errors: Array<{
|
|
1664
|
-
synced: number;
|
|
1678
|
+
errors: Array<{ error: string; id: string; type: string }>;
|
|
1679
|
+
synced: { activities: number; athletes: number };
|
|
1665
1680
|
},
|
|
1666
1681
|
Name
|
|
1667
1682
|
>;
|
|
1668
1683
|
disconnectStrava: FunctionReference<
|
|
1684
|
+
"action",
|
|
1685
|
+
"internal",
|
|
1686
|
+
{ clientId: string; clientSecret: string; userId: string },
|
|
1687
|
+
null,
|
|
1688
|
+
Name
|
|
1689
|
+
>;
|
|
1690
|
+
getStravaAuthUrl: FunctionReference<
|
|
1669
1691
|
"action",
|
|
1670
1692
|
"internal",
|
|
1671
1693
|
{
|
|
1672
|
-
baseUrl?: string;
|
|
1673
1694
|
clientId: string;
|
|
1674
|
-
|
|
1695
|
+
redirectUri: string;
|
|
1696
|
+
scope?: string;
|
|
1697
|
+
userId?: string;
|
|
1698
|
+
},
|
|
1699
|
+
any,
|
|
1700
|
+
Name
|
|
1701
|
+
>;
|
|
1702
|
+
syncAllTypes: FunctionReference<
|
|
1703
|
+
"action",
|
|
1704
|
+
"internal",
|
|
1705
|
+
{
|
|
1706
|
+
accessToken: string;
|
|
1707
|
+
after?: number;
|
|
1708
|
+
before?: number;
|
|
1709
|
+
connectionId: string;
|
|
1675
1710
|
userId: string;
|
|
1676
1711
|
},
|
|
1677
|
-
|
|
1712
|
+
any,
|
|
1678
1713
|
Name
|
|
1679
1714
|
>;
|
|
1680
1715
|
syncStrava: FunctionReference<
|
|
@@ -1682,15 +1717,13 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
1682
1717
|
"internal",
|
|
1683
1718
|
{
|
|
1684
1719
|
after?: number;
|
|
1685
|
-
baseUrl?: string;
|
|
1686
1720
|
clientId: string;
|
|
1687
1721
|
clientSecret: string;
|
|
1688
|
-
includeStreams?: boolean;
|
|
1689
1722
|
userId: string;
|
|
1690
1723
|
},
|
|
1691
1724
|
{
|
|
1692
|
-
errors: Array<{
|
|
1693
|
-
synced: number;
|
|
1725
|
+
errors: Array<{ error: string; id: string; type: string }>;
|
|
1726
|
+
synced: { activities: number; athletes: number };
|
|
1694
1727
|
},
|
|
1695
1728
|
Name
|
|
1696
1729
|
>;
|
|
@@ -32,15 +32,6 @@ export function generateCodeVerifier(length = 64): string {
|
|
|
32
32
|
);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
/**
|
|
36
|
-
* Generate a random state parameter for CSRF protection.
|
|
37
|
-
*/
|
|
38
|
-
export function generateState(): string {
|
|
39
|
-
const bytes = new Uint8Array(32);
|
|
40
|
-
crypto.getRandomValues(bytes);
|
|
41
|
-
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
42
|
-
}
|
|
43
|
-
|
|
44
35
|
/**
|
|
45
36
|
* Compute the S256 code challenge from a code verifier.
|
|
46
37
|
* Returns `base64url(sha256(verifier))`.
|
|
@@ -117,18 +117,6 @@ export const storePendingOAuth = internalMutation({
|
|
|
117
117
|
|
|
118
118
|
export const getPendingOAuth = internalQuery({
|
|
119
119
|
args: { state: v.string() },
|
|
120
|
-
returns: v.union(
|
|
121
|
-
v.object({
|
|
122
|
-
_id: v.id("pendingOAuth"),
|
|
123
|
-
_creationTime: v.number(),
|
|
124
|
-
provider: v.string(),
|
|
125
|
-
state: v.string(),
|
|
126
|
-
codeVerifier: v.string(),
|
|
127
|
-
userId: v.string(),
|
|
128
|
-
createdAt: v.number(),
|
|
129
|
-
}),
|
|
130
|
-
v.null(),
|
|
131
|
-
),
|
|
132
120
|
handler: async (ctx, args) => {
|
|
133
121
|
return await ctx.db
|
|
134
122
|
.query("pendingOAuth")
|
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
import { v } from "convex/values";
|
|
7
7
|
import { action } from "../_generated/server";
|
|
8
8
|
import type { Doc, Id } from "../_generated/dataModel";
|
|
9
|
+
import { generateState } from "../utils.js";
|
|
9
10
|
import {
|
|
10
11
|
generateCodeVerifier,
|
|
11
12
|
generateCodeChallenge,
|
|
12
|
-
generateState,
|
|
13
13
|
buildAuthUrl,
|
|
14
14
|
exchangeCode,
|
|
15
15
|
refreshToken,
|
|
@@ -219,6 +219,13 @@ export const completeGarminOAuth = action({
|
|
|
219
219
|
);
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
+
if (!pending.codeVerifier) {
|
|
223
|
+
throw new Error(
|
|
224
|
+
"No code verifier found for this state parameter. " +
|
|
225
|
+
"The authorization may have expired or was already used.",
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
222
229
|
const tokenResult = await exchangeCode({
|
|
223
230
|
clientId: args.clientId,
|
|
224
231
|
clientSecret: args.clientSecret,
|
package/src/component/schema.ts
CHANGED
|
@@ -127,13 +127,15 @@ export default defineSchema({
|
|
|
127
127
|
}).index("by_connectionId", ["connectionId"]),
|
|
128
128
|
|
|
129
129
|
// ── Pending OAuth ─────────────────────────────────────────────────────────
|
|
130
|
-
// Temporary storage for in-progress OAuth
|
|
131
|
-
//
|
|
130
|
+
// Temporary storage for in-progress OAuth flows. Bridges the gap between
|
|
131
|
+
// initiating OAuth (auth URL) and the callback (code exchange).
|
|
132
132
|
// The `state` parameter links the callback back to the pending entry.
|
|
133
|
+
// `codeVerifier` is required for PKCE providers (Garmin) but absent for
|
|
134
|
+
// providers that don't use PKCE (Strava).
|
|
133
135
|
pendingOAuth: defineTable({
|
|
134
136
|
provider: v.string(),
|
|
135
137
|
state: v.string(),
|
|
136
|
-
codeVerifier: v.string(),
|
|
138
|
+
codeVerifier: v.optional(v.string()),
|
|
137
139
|
userId: v.string(),
|
|
138
140
|
createdAt: v.number(),
|
|
139
141
|
}).index("by_state", ["state"]),
|