@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.
Files changed (202) hide show
  1. package/dist/client/index.d.ts +96 -33
  2. package/dist/client/index.d.ts.map +1 -1
  3. package/dist/client/index.js +80 -35
  4. package/dist/client/index.js.map +1 -1
  5. package/dist/component/_generated/api.d.ts +18 -0
  6. package/dist/component/_generated/api.d.ts.map +1 -1
  7. package/dist/component/_generated/api.js.map +1 -1
  8. package/dist/component/_generated/component.d.ts +43 -9
  9. package/dist/component/_generated/component.d.ts.map +1 -1
  10. package/dist/component/garmin/auth.d.ts +0 -4
  11. package/dist/component/garmin/auth.d.ts.map +1 -1
  12. package/dist/component/garmin/auth.js +0 -8
  13. package/dist/component/garmin/auth.js.map +1 -1
  14. package/dist/component/garmin/private.d.ts +20 -3
  15. package/dist/component/garmin/private.d.ts.map +1 -1
  16. package/dist/component/garmin/private.js +17 -26
  17. package/dist/component/garmin/private.js.map +1 -1
  18. package/dist/component/garmin/public.d.ts +4 -4
  19. package/dist/component/garmin/public.d.ts.map +1 -1
  20. package/dist/component/garmin/public.js +6 -1
  21. package/dist/component/garmin/public.js.map +1 -1
  22. package/dist/component/garmin/webhooks.d.ts +4 -0
  23. package/dist/component/garmin/webhooks.d.ts.map +1 -1
  24. package/dist/component/garmin/webhooks.js +23 -18
  25. package/dist/component/garmin/webhooks.js.map +1 -1
  26. package/dist/component/schema.d.ts +2 -2
  27. package/dist/component/schema.d.ts.map +1 -1
  28. package/dist/component/schema.js +5 -3
  29. package/dist/component/schema.js.map +1 -1
  30. package/dist/{strava → component/strava}/auth.d.ts +15 -48
  31. package/dist/component/strava/auth.d.ts.map +1 -0
  32. package/dist/{strava → component/strava}/auth.js +4 -39
  33. package/dist/component/strava/auth.js.map +1 -0
  34. package/dist/component/strava/client.d.ts +8 -0
  35. package/dist/component/strava/client.d.ts.map +1 -0
  36. package/dist/component/strava/client.js +18 -0
  37. package/dist/component/strava/client.js.map +1 -0
  38. package/dist/component/strava/private.d.ts +19 -0
  39. package/dist/component/strava/private.d.ts.map +1 -1
  40. package/dist/component/strava/private.js +52 -2
  41. package/dist/component/strava/private.js.map +1 -1
  42. package/dist/component/strava/public.d.ts +87 -12
  43. package/dist/component/strava/public.d.ts.map +1 -1
  44. package/dist/component/strava/public.js +218 -92
  45. package/dist/component/strava/public.js.map +1 -1
  46. package/dist/component/strava/transform/activity.d.ts +19 -0
  47. package/dist/component/strava/transform/activity.d.ts.map +1 -0
  48. package/dist/{strava → component/strava/transform}/activity.js +21 -41
  49. package/dist/component/strava/transform/activity.js.map +1 -0
  50. package/dist/{strava → component/strava/transform}/athlete.d.ts +4 -10
  51. package/dist/component/strava/transform/athlete.d.ts.map +1 -0
  52. package/dist/{strava → component/strava/transform}/athlete.js +2 -8
  53. package/dist/component/strava/transform/athlete.js.map +1 -0
  54. package/dist/component/strava/transform/maps/sportType.d.ts +7 -0
  55. package/dist/component/strava/transform/maps/sportType.d.ts.map +1 -0
  56. package/dist/{strava/maps/sport-type.js → component/strava/transform/maps/sportType.js} +4 -2
  57. package/dist/component/strava/transform/maps/sportType.js.map +1 -0
  58. package/dist/component/strava/types/stravaApi/client/client.gen.d.ts +3 -0
  59. package/dist/component/strava/types/stravaApi/client/client.gen.d.ts.map +1 -0
  60. package/dist/component/strava/types/stravaApi/client/client.gen.js +236 -0
  61. package/dist/component/strava/types/stravaApi/client/client.gen.js.map +1 -0
  62. package/dist/component/strava/types/stravaApi/client/index.d.ts +9 -0
  63. package/dist/component/strava/types/stravaApi/client/index.d.ts.map +1 -0
  64. package/dist/component/strava/types/stravaApi/client/index.js +7 -0
  65. package/dist/component/strava/types/stravaApi/client/index.js.map +1 -0
  66. package/dist/component/strava/types/stravaApi/client/types.gen.d.ts +118 -0
  67. package/dist/component/strava/types/stravaApi/client/types.gen.d.ts.map +1 -0
  68. package/dist/component/strava/types/stravaApi/client/types.gen.js +3 -0
  69. package/dist/component/strava/types/stravaApi/client/types.gen.js.map +1 -0
  70. package/dist/component/strava/types/stravaApi/client/utils.gen.d.ts +34 -0
  71. package/dist/component/strava/types/stravaApi/client/utils.gen.d.ts.map +1 -0
  72. package/dist/component/strava/types/stravaApi/client/utils.gen.js +229 -0
  73. package/dist/component/strava/types/stravaApi/client/utils.gen.js.map +1 -0
  74. package/dist/component/strava/types/stravaApi/client.gen.d.ts +13 -0
  75. package/dist/component/strava/types/stravaApi/client.gen.d.ts.map +1 -0
  76. package/dist/component/strava/types/stravaApi/client.gen.js +4 -0
  77. package/dist/component/strava/types/stravaApi/client.gen.js.map +1 -0
  78. package/dist/component/strava/types/stravaApi/core/auth.gen.d.ts +19 -0
  79. package/dist/component/strava/types/stravaApi/core/auth.gen.d.ts.map +1 -0
  80. package/dist/component/strava/types/stravaApi/core/auth.gen.js +15 -0
  81. package/dist/component/strava/types/stravaApi/core/auth.gen.js.map +1 -0
  82. package/dist/component/strava/types/stravaApi/core/bodySerializer.gen.d.ts +26 -0
  83. package/dist/component/strava/types/stravaApi/core/bodySerializer.gen.d.ts.map +1 -0
  84. package/dist/component/strava/types/stravaApi/core/bodySerializer.gen.js +58 -0
  85. package/dist/component/strava/types/stravaApi/core/bodySerializer.gen.js.map +1 -0
  86. package/dist/component/strava/types/stravaApi/core/params.gen.d.ts +44 -0
  87. package/dist/component/strava/types/stravaApi/core/params.gen.d.ts.map +1 -0
  88. package/dist/component/strava/types/stravaApi/core/params.gen.js +101 -0
  89. package/dist/component/strava/types/stravaApi/core/params.gen.js.map +1 -0
  90. package/dist/component/strava/types/stravaApi/core/pathSerializer.gen.d.ts +34 -0
  91. package/dist/component/strava/types/stravaApi/core/pathSerializer.gen.d.ts.map +1 -0
  92. package/dist/component/strava/types/stravaApi/core/pathSerializer.gen.js +107 -0
  93. package/dist/component/strava/types/stravaApi/core/pathSerializer.gen.js.map +1 -0
  94. package/dist/component/strava/types/stravaApi/core/queryKeySerializer.gen.d.ts +19 -0
  95. package/dist/component/strava/types/stravaApi/core/queryKeySerializer.gen.d.ts.map +1 -0
  96. package/dist/component/strava/types/stravaApi/core/queryKeySerializer.gen.js +93 -0
  97. package/dist/component/strava/types/stravaApi/core/queryKeySerializer.gen.js.map +1 -0
  98. package/dist/component/strava/types/stravaApi/core/serverSentEvents.gen.d.ts +72 -0
  99. package/dist/component/strava/types/stravaApi/core/serverSentEvents.gen.d.ts.map +1 -0
  100. package/dist/component/strava/types/stravaApi/core/serverSentEvents.gen.js +134 -0
  101. package/dist/component/strava/types/stravaApi/core/serverSentEvents.gen.js.map +1 -0
  102. package/dist/component/strava/types/stravaApi/core/types.gen.d.ts +79 -0
  103. package/dist/component/strava/types/stravaApi/core/types.gen.d.ts.map +1 -0
  104. package/dist/component/strava/types/stravaApi/core/types.gen.js +3 -0
  105. package/dist/component/strava/types/stravaApi/core/types.gen.js.map +1 -0
  106. package/dist/component/strava/types/stravaApi/core/utils.gen.d.ts +20 -0
  107. package/dist/component/strava/types/stravaApi/core/utils.gen.d.ts.map +1 -0
  108. package/dist/component/strava/types/stravaApi/core/utils.gen.js +88 -0
  109. package/dist/component/strava/types/stravaApi/core/utils.gen.js.map +1 -0
  110. package/dist/component/strava/types/stravaApi/index.d.ts +3 -0
  111. package/dist/component/strava/types/stravaApi/index.d.ts.map +1 -0
  112. package/dist/component/strava/types/stravaApi/index.js +3 -0
  113. package/dist/component/strava/types/stravaApi/index.js.map +1 -0
  114. package/dist/component/strava/types/stravaApi/sdk.gen.d.ts +224 -0
  115. package/dist/component/strava/types/stravaApi/sdk.gen.d.ts.map +1 -0
  116. package/dist/component/strava/types/stravaApi/sdk.gen.js +361 -0
  117. package/dist/component/strava/types/stravaApi/sdk.gen.js.map +1 -0
  118. package/dist/component/strava/types/stravaApi/types.gen.d.ts +2209 -0
  119. package/dist/component/strava/types/stravaApi/types.gen.d.ts.map +1 -0
  120. package/dist/component/strava/types/stravaApi/types.gen.js +3 -0
  121. package/dist/component/strava/types/stravaApi/types.gen.js.map +1 -0
  122. package/dist/component/strava/types/stravaApi/zod.gen.d.ts +5332 -0
  123. package/dist/component/strava/types/stravaApi/zod.gen.d.ts.map +1 -0
  124. package/dist/component/strava/types/stravaApi/zod.gen.js +1009 -0
  125. package/dist/component/strava/types/stravaApi/zod.gen.js.map +1 -0
  126. package/dist/component/strava/utils.d.ts +15 -0
  127. package/dist/component/strava/utils.d.ts.map +1 -0
  128. package/dist/component/strava/utils.js +36 -0
  129. package/dist/component/strava/utils.js.map +1 -0
  130. package/dist/component/utils.d.ts +5 -0
  131. package/dist/component/utils.d.ts.map +1 -0
  132. package/dist/component/utils.js +11 -0
  133. package/dist/component/utils.js.map +1 -0
  134. package/package.json +131 -130
  135. package/src/client/index.ts +121 -52
  136. package/src/component/_generated/api.ts +18 -0
  137. package/src/component/_generated/component.ts +44 -11
  138. package/src/component/garmin/auth.ts +0 -9
  139. package/src/component/garmin/private.ts +0 -12
  140. package/src/component/garmin/public.ts +8 -1
  141. package/src/component/schema.ts +5 -3
  142. package/src/{strava → component/strava}/auth.ts +143 -185
  143. package/src/component/strava/client.ts +20 -0
  144. package/src/component/strava/private.ts +147 -89
  145. package/src/component/strava/public.ts +268 -110
  146. package/src/{strava → component/strava/transform}/activity.ts +256 -276
  147. package/src/{strava → component/strava/transform}/athlete.ts +41 -47
  148. package/src/{strava/maps/sport-type.ts → component/strava/transform/maps/sportType.ts} +100 -99
  149. package/src/component/strava/types/specs/strava-api.json +4796 -0
  150. package/src/component/strava/types/stravaApi/client/client.gen.ts +290 -0
  151. package/src/component/strava/types/stravaApi/client/index.ts +25 -0
  152. package/src/component/strava/types/stravaApi/client/types.gen.ts +214 -0
  153. package/src/component/strava/types/stravaApi/client/utils.gen.ts +316 -0
  154. package/src/component/strava/types/stravaApi/client.gen.ts +16 -0
  155. package/src/component/strava/types/stravaApi/core/auth.gen.ts +41 -0
  156. package/src/component/strava/types/stravaApi/core/bodySerializer.gen.ts +82 -0
  157. package/src/component/strava/types/stravaApi/core/params.gen.ts +169 -0
  158. package/src/component/strava/types/stravaApi/core/pathSerializer.gen.ts +171 -0
  159. package/src/component/strava/types/stravaApi/core/queryKeySerializer.gen.ts +117 -0
  160. package/src/component/strava/types/stravaApi/core/serverSentEvents.gen.ts +243 -0
  161. package/src/component/strava/types/stravaApi/core/types.gen.ts +104 -0
  162. package/src/component/strava/types/stravaApi/core/utils.gen.ts +140 -0
  163. package/src/component/strava/types/stravaApi/index.ts +4 -0
  164. package/src/component/strava/types/stravaApi/sdk.gen.ts +410 -0
  165. package/src/component/strava/types/stravaApi/types.gen.ts +2435 -0
  166. package/src/component/strava/types/stravaApi/zod.gen.ts +1132 -0
  167. package/src/component/strava/utils.ts +52 -0
  168. package/src/component/utils.ts +11 -0
  169. package/dist/strava/activity.d.ts +0 -121
  170. package/dist/strava/activity.d.ts.map +0 -1
  171. package/dist/strava/activity.js.map +0 -1
  172. package/dist/strava/athlete.d.ts.map +0 -1
  173. package/dist/strava/athlete.js.map +0 -1
  174. package/dist/strava/auth.d.ts.map +0 -1
  175. package/dist/strava/auth.js.map +0 -1
  176. package/dist/strava/client.d.ts +0 -93
  177. package/dist/strava/client.d.ts.map +0 -1
  178. package/dist/strava/client.js +0 -158
  179. package/dist/strava/client.js.map +0 -1
  180. package/dist/strava/index.d.ts +0 -13
  181. package/dist/strava/index.d.ts.map +0 -1
  182. package/dist/strava/index.js +0 -17
  183. package/dist/strava/index.js.map +0 -1
  184. package/dist/strava/maps/sport-type.d.ts +0 -7
  185. package/dist/strava/maps/sport-type.d.ts.map +0 -1
  186. package/dist/strava/maps/sport-type.js.map +0 -1
  187. package/dist/strava/sync.d.ts +0 -104
  188. package/dist/strava/sync.d.ts.map +0 -1
  189. package/dist/strava/sync.js +0 -87
  190. package/dist/strava/sync.js.map +0 -1
  191. package/dist/strava/types.d.ts +0 -266
  192. package/dist/strava/types.d.ts.map +0 -1
  193. package/dist/strava/types.js +0 -8
  194. package/dist/strava/types.js.map +0 -1
  195. package/src/strava/activity.test.ts +0 -415
  196. package/src/strava/athlete.test.ts +0 -139
  197. package/src/strava/auth.test.ts +0 -78
  198. package/src/strava/client.ts +0 -212
  199. package/src/strava/index.ts +0 -54
  200. package/src/strava/maps/sport-type.test.ts +0 -69
  201. package/src/strava/sync.ts +0 -168
  202. package/src/strava/types.ts +0 -361
@@ -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: "...", baseUrl: "..." },
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
- * Build the Strava OAuth authorization URL.
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
- * This is a pure computation (no DB or HTTP calls), so it doesn't need
891
- * a Convex context. Redirect the user to this URL to begin the OAuth flow.
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.state - Optional state parameter for CSRF protection
896
- * @returns The authorization URL string
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 url = soma.getStravaAuthUrl({
901
- * redirectUri: "https://your-app.com/api/strava/callback",
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(opts: {
906
- redirectUri: string;
907
- scope?: string;
908
- state?: string;
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 buildAuthUrl({
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
- state: opts.state,
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; includeStreams?: boolean },
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, includeStreams: true });
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; includeStreams?: boolean; after?: number },
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 StravaRouteOptions {
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
- onSuccess?: string;
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?: StravaRouteOptions;
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
- * path: "/oauth/strava/callback",
1373
- * onSuccess: "https://myapp.com/settings",
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 strava = opts?.strava ?? {};
1410
- const path = strava.path ?? STRAVA_CALLBACK_PATH;
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 userId = url.searchParams.get("state");
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 (!userId) {
1470
+ if (!state) {
1424
1471
  return new Response(
1425
- "Missing state parameter (userId). Pass the userId as the state " +
1426
- "parameter when building the Strava auth URL.",
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
- strava.clientId ?? process.env.STRAVA_CLIENT_ID;
1479
+ oauth.clientId ?? process.env.STRAVA_CLIENT_ID;
1433
1480
  const clientSecret =
1434
- strava.clientSecret ?? process.env.STRAVA_CLIENT_SECRET;
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.connectStrava, {
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 (strava.onSuccess) {
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: strava.onSuccess },
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<{ activityId: number; error: string }>;
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
- clientSecret: string;
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
- null,
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<{ activityId: number; error: string }>;
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,
@@ -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 2.0 PKCE flows. Bridges the gap
131
- // between initiating OAuth (auth URL) and the callback (code exchange).
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"]),