@nativesquare/soma 0.9.4 → 0.10.1

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 (203) hide show
  1. package/dist/client/index.d.ts +124 -144
  2. package/dist/client/index.d.ts.map +1 -1
  3. package/dist/client/index.js +157 -134
  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 +113 -22
  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 +10 -1
  15. package/dist/component/garmin/private.d.ts.map +1 -1
  16. package/dist/component/garmin/private.js +49 -9
  17. package/dist/component/garmin/private.js.map +1 -1
  18. package/dist/component/garmin/public.d.ts +237 -62
  19. package/dist/component/garmin/public.d.ts.map +1 -1
  20. package/dist/component/garmin/public.js +689 -254
  21. package/dist/component/garmin/public.js.map +1 -1
  22. package/dist/component/garmin/utils.d.ts +8 -0
  23. package/dist/component/garmin/utils.d.ts.map +1 -1
  24. package/dist/component/garmin/utils.js +9 -0
  25. package/dist/component/garmin/utils.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 +54 -19
  43. package/dist/component/strava/public.d.ts.map +1 -1
  44. package/dist/component/strava/public.js +159 -109
  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 +31 -45
  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 +285 -164
  136. package/src/component/_generated/api.ts +18 -0
  137. package/src/component/_generated/component.ts +191 -24
  138. package/src/component/garmin/auth.ts +0 -9
  139. package/src/component/garmin/private.ts +84 -12
  140. package/src/component/garmin/public.ts +812 -348
  141. package/src/component/garmin/utils.ts +17 -0
  142. package/src/component/schema.ts +5 -3
  143. package/src/{strava → component/strava}/auth.ts +143 -185
  144. package/src/component/strava/client.ts +20 -0
  145. package/src/component/strava/private.ts +147 -89
  146. package/src/component/strava/public.ts +191 -139
  147. package/src/{strava → component/strava/transform}/activity.ts +256 -276
  148. package/src/{strava → component/strava/transform}/athlete.ts +41 -47
  149. package/src/{strava/maps/sport-type.ts → component/strava/transform/maps/sportType.ts} +100 -99
  150. package/src/component/strava/types/specs/strava-api.json +4796 -0
  151. package/src/component/strava/types/stravaApi/client/client.gen.ts +290 -0
  152. package/src/component/strava/types/stravaApi/client/index.ts +25 -0
  153. package/src/component/strava/types/stravaApi/client/types.gen.ts +214 -0
  154. package/src/component/strava/types/stravaApi/client/utils.gen.ts +316 -0
  155. package/src/component/strava/types/stravaApi/client.gen.ts +16 -0
  156. package/src/component/strava/types/stravaApi/core/auth.gen.ts +41 -0
  157. package/src/component/strava/types/stravaApi/core/bodySerializer.gen.ts +82 -0
  158. package/src/component/strava/types/stravaApi/core/params.gen.ts +169 -0
  159. package/src/component/strava/types/stravaApi/core/pathSerializer.gen.ts +171 -0
  160. package/src/component/strava/types/stravaApi/core/queryKeySerializer.gen.ts +117 -0
  161. package/src/component/strava/types/stravaApi/core/serverSentEvents.gen.ts +243 -0
  162. package/src/component/strava/types/stravaApi/core/types.gen.ts +104 -0
  163. package/src/component/strava/types/stravaApi/core/utils.gen.ts +140 -0
  164. package/src/component/strava/types/stravaApi/index.ts +4 -0
  165. package/src/component/strava/types/stravaApi/sdk.gen.ts +410 -0
  166. package/src/component/strava/types/stravaApi/types.gen.ts +2435 -0
  167. package/src/component/strava/types/stravaApi/zod.gen.ts +1132 -0
  168. package/src/component/strava/utils.ts +52 -0
  169. package/src/component/utils.ts +11 -0
  170. package/dist/strava/activity.d.ts +0 -121
  171. package/dist/strava/activity.d.ts.map +0 -1
  172. package/dist/strava/activity.js.map +0 -1
  173. package/dist/strava/athlete.d.ts.map +0 -1
  174. package/dist/strava/athlete.js.map +0 -1
  175. package/dist/strava/auth.d.ts.map +0 -1
  176. package/dist/strava/auth.js.map +0 -1
  177. package/dist/strava/client.d.ts +0 -93
  178. package/dist/strava/client.d.ts.map +0 -1
  179. package/dist/strava/client.js +0 -158
  180. package/dist/strava/client.js.map +0 -1
  181. package/dist/strava/index.d.ts +0 -13
  182. package/dist/strava/index.d.ts.map +0 -1
  183. package/dist/strava/index.js +0 -17
  184. package/dist/strava/index.js.map +0 -1
  185. package/dist/strava/maps/sport-type.d.ts +0 -7
  186. package/dist/strava/maps/sport-type.d.ts.map +0 -1
  187. package/dist/strava/maps/sport-type.js.map +0 -1
  188. package/dist/strava/sync.d.ts +0 -104
  189. package/dist/strava/sync.d.ts.map +0 -1
  190. package/dist/strava/sync.js +0 -87
  191. package/dist/strava/sync.js.map +0 -1
  192. package/dist/strava/types.d.ts +0 -266
  193. package/dist/strava/types.d.ts.map +0 -1
  194. package/dist/strava/types.js +0 -8
  195. package/dist/strava/types.js.map +0 -1
  196. package/src/strava/activity.test.ts +0 -415
  197. package/src/strava/athlete.test.ts +0 -139
  198. package/src/strava/auth.test.ts +0 -78
  199. package/src/strava/client.ts +0 -212
  200. package/src/strava/index.ts +0 -54
  201. package/src/strava/maps/sport-type.test.ts +0 -69
  202. package/src/strava/sync.ts +0 -168
  203. 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:
@@ -82,8 +75,11 @@ export interface SomaGarminConfig {
82
75
  * provider: "GARMIN",
83
76
  * });
84
77
  *
85
- * // Connect via Strava (handles OAuth, token storage, and initial sync):
86
- * const result = await soma.connectStrava(ctx, { userId: "user_123", code: "..." });
78
+ * // Start Strava OAuth (redirects user, callback handled by registerRoutes):
79
+ * const { authUrl } = await soma.getStravaAuthUrl(ctx, {
80
+ * userId: "user_123",
81
+ * redirectUri: "https://your-app.convex.site/api/strava/callback",
82
+ * });
87
83
  * ```
88
84
  */
89
85
  export class Soma {
@@ -109,7 +105,6 @@ export class Soma {
109
105
  return {
110
106
  clientId,
111
107
  clientSecret,
112
- baseUrl: process.env.STRAVA_BASE_URL,
113
108
  };
114
109
  }
115
110
 
@@ -885,76 +880,40 @@ export class Soma {
885
880
  // environment variables or the constructor.
886
881
 
887
882
  /**
888
- * Build the Strava OAuth authorization URL.
883
+ * Generate a Strava OAuth authorization URL.
889
884
  *
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.
885
+ * The state parameter is stored inside the component automatically,
886
+ * and the callback handler registered by `registerRoutes` will
887
+ * complete the flow without further host-app intervention.
892
888
  *
889
+ * @param ctx - Action context from the host app
890
+ * @param opts.userId - The host app's user identifier
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
+ * @returns `{ authUrl, state }`
897
894
  *
898
895
  * @example
899
896
  * ```ts
900
- * const url = soma.getStravaAuthUrl({
901
- * redirectUri: "https://your-app.com/api/strava/callback",
897
+ * const { authUrl } = await soma.getStravaAuthUrl(ctx, {
898
+ * userId: "user_123",
899
+ * redirectUri: "https://your-app.convex.site/api/strava/callback",
902
900
  * });
901
+ * // Redirect user to authUrl — the callback is handled automatically
903
902
  * ```
904
903
  */
905
- getStravaAuthUrl(opts: {
906
- redirectUri: string;
907
- scope?: string;
908
- state?: string;
909
- }): string {
904
+ async getStravaAuthUrl(
905
+ ctx: ActionCtx,
906
+ opts: { userId: string; redirectUri: string; scope?: string },
907
+ ) {
910
908
  const config = this.requireStravaConfig();
911
- return buildAuthUrl({
909
+ return await ctx.runAction(this.component.strava.public.getStravaAuthUrl, {
912
910
  clientId: config.clientId,
913
911
  redirectUri: opts.redirectUri,
914
912
  scope: opts.scope,
915
- state: opts.state,
916
- baseUrl: config.baseUrl,
913
+ userId: opts.userId,
917
914
  });
918
915
  }
919
916
 
920
- /**
921
- * Handle the Strava OAuth callback.
922
- *
923
- * Exchanges the authorization code for tokens, creates/reactivates the
924
- * Soma connection, stores tokens securely in the component, syncs the
925
- * athlete profile, and syncs all activities.
926
- *
927
- * Call this from your OAuth callback endpoint after receiving the `code`
928
- * query parameter from Strava.
929
- *
930
- * @param ctx - Action context from the host app
931
- * @param args.userId - The host app's user identifier
932
- * @param args.code - The authorization code from the OAuth callback
933
- * @param args.includeStreams - Fetch detailed streams per activity (default: false)
934
- * @returns `{ connectionId, synced, errors }`
935
- *
936
- * @example
937
- * ```ts
938
- * export const handleStravaCallback = action({
939
- * args: { userId: v.string(), code: v.string() },
940
- * handler: async (ctx, { userId, code }) => {
941
- * return await soma.connectStrava(ctx, { userId, code });
942
- * },
943
- * });
944
- * ```
945
- */
946
- async connectStrava(
947
- ctx: ActionCtx,
948
- args: { userId: string; code: string; includeStreams?: boolean },
949
- ) {
950
- const config = this.requireStravaConfig();
951
- return await ctx.runAction(this.component.strava.public.connectStrava, {
952
- ...args,
953
- clientId: config.clientId,
954
- clientSecret: config.clientSecret,
955
- baseUrl: config.baseUrl,
956
- });
957
- }
958
917
 
959
918
  /**
960
919
  * Sync activities from Strava for an already-connected user.
@@ -964,7 +923,6 @@ export class Soma {
964
923
  *
965
924
  * @param ctx - Action context from the host app
966
925
  * @param args.userId - The host app's user identifier
967
- * @param args.includeStreams - Fetch detailed streams per activity (default: false)
968
926
  * @param args.after - Only sync activities after this Unix epoch timestamp (for incremental sync)
969
927
  * @returns `{ synced, errors }`
970
928
  *
@@ -973,21 +931,20 @@ export class Soma {
973
931
  * export const syncStrava = action({
974
932
  * args: { userId: v.string() },
975
933
  * handler: async (ctx, { userId }) => {
976
- * return await soma.syncStrava(ctx, { userId, includeStreams: true });
934
+ * return await soma.syncStrava(ctx, { userId });
977
935
  * },
978
936
  * });
979
937
  * ```
980
938
  */
981
939
  async syncStrava(
982
940
  ctx: ActionCtx,
983
- args: { userId: string; includeStreams?: boolean; after?: number },
941
+ args: { userId: string; after?: number },
984
942
  ) {
985
943
  const config = this.requireStravaConfig();
986
944
  return await ctx.runAction(this.component.strava.public.syncStrava, {
987
945
  ...args,
988
946
  clientId: config.clientId,
989
947
  clientSecret: config.clientSecret,
990
- baseUrl: config.baseUrl,
991
948
  });
992
949
  }
993
950
 
@@ -1019,7 +976,6 @@ export class Soma {
1019
976
  ...args,
1020
977
  clientId: config.clientId,
1021
978
  clientSecret: config.clientSecret,
1022
- baseUrl: config.baseUrl,
1023
979
  });
1024
980
  }
1025
981
 
@@ -1031,20 +987,13 @@ export class Soma {
1031
987
  /**
1032
988
  * Generate a Garmin OAuth 2.0 authorization URL with PKCE.
1033
989
  *
1034
- * Returns the `authUrl` to redirect the user to, along with the `state`
1035
- * and `codeVerifier` used for the PKCE flow.
1036
- *
1037
- * If `userId` is provided, the PKCE state is stored inside the component
1038
- * automatically, and the callback handler registered by `registerRoutes`
1039
- * will complete the flow without further host-app intervention. This is
1040
- * the recommended approach.
1041
- *
1042
- * If `userId` is omitted, the host app must store the returned
1043
- * `codeVerifier` itself and pass it to `connectGarmin` manually.
990
+ * The PKCE state is stored inside the component automatically,
991
+ * and the callback handler registered by `registerRoutes` will
992
+ * complete the flow without further host-app intervention.
1044
993
  *
1045
994
  * @param ctx - Action context from the host app
995
+ * @param opts.userId - The host app's user identifier
1046
996
  * @param opts.redirectUri - The URL Garmin will redirect to after authorization
1047
- * @param opts.userId - The host app's user identifier (required for `registerRoutes` flow)
1048
997
  * @returns `{ authUrl, state, codeVerifier }`
1049
998
  *
1050
999
  * @example
@@ -1058,7 +1007,7 @@ export class Soma {
1058
1007
  */
1059
1008
  async getGarminAuthUrl(
1060
1009
  ctx: ActionCtx,
1061
- opts: { redirectUri?: string; userId?: string },
1010
+ opts: { userId: string; redirectUri?: string },
1062
1011
  ) {
1063
1012
  const config = this.requireGarminConfig();
1064
1013
  return await ctx.runAction(this.component.garmin.public.getGarminAuthUrl, {
@@ -1068,73 +1017,208 @@ export class Soma {
1068
1017
  });
1069
1018
  }
1070
1019
 
1071
- /**
1072
- * Handle the Garmin OAuth 2.0 callback (manual flow).
1073
- *
1074
- * Exchanges the authorization code for tokens, creates/reactivates the
1075
- * Soma connection, stores tokens securely, and syncs the last 30 days
1076
- * of all data types.
1077
- *
1078
- * Call this from your OAuth callback endpoint after receiving the `code`
1079
- * query parameter from Garmin.
1080
- *
1081
- * @param ctx - Action context from the host app
1082
- * @param args.userId - The host app's user identifier
1083
- * @param args.code - The authorization code from the callback
1084
- * @param args.codeVerifier - The PKCE code verifier from Step 1
1085
- * @param args.redirectUri - The redirect URI used in the authorization request
1086
- * @returns `{ connectionId, synced, errors }`
1087
- *
1088
- * @example
1089
- * ```ts
1090
- * export const handleGarminCallback = action({
1091
- * args: {
1092
- * userId: v.string(),
1093
- * code: v.string(),
1094
- * codeVerifier: v.string(),
1095
- * },
1096
- * handler: async (ctx, args) => {
1097
- * return await soma.connectGarmin(ctx, args);
1098
- * },
1099
- * });
1100
- * ```
1101
- */
1102
- async connectGarmin(
1020
+ async pullGarminActivities(
1103
1021
  ctx: ActionCtx,
1104
1022
  args: {
1105
1023
  userId: string;
1106
- code: string;
1107
- codeVerifier: string;
1108
- redirectUri?: string;
1024
+ startTimeInSeconds?: number;
1025
+ endTimeInSeconds?: number;
1109
1026
  },
1110
1027
  ) {
1111
1028
  const config = this.requireGarminConfig();
1112
- return await ctx.runAction(this.component.garmin.public.connectGarmin, {
1029
+ return await ctx.runAction(this.component.garmin.public.pullActivities, {
1113
1030
  ...args,
1114
1031
  clientId: config.clientId,
1115
1032
  clientSecret: config.clientSecret,
1116
1033
  });
1117
1034
  }
1118
1035
 
1119
- /**
1120
- * Complete a Garmin OAuth 2.0 flow using stored pending state.
1121
- *
1122
- * This is called automatically by the `registerRoutes` callback handler.
1123
- * It looks up the pending OAuth state stored during `getGarminAuthUrl`,
1124
- * exchanges for tokens, creates the connection, and syncs data.
1125
- *
1126
- * @param ctx - Action context from the host app
1127
- * @param args.code - The authorization code from the callback query params
1128
- * @param args.state - The state parameter from the callback query params
1129
- * @param args.redirectUri - The redirect URI used in the authorization request
1130
- * @returns `{ connectionId, synced, errors }`
1131
- */
1132
- async completeGarminOAuth(
1036
+ async pullGarminDailies(
1133
1037
  ctx: ActionCtx,
1134
- args: { code: string; state: string; redirectUri?: string },
1038
+ args: {
1039
+ userId: string;
1040
+ startTimeInSeconds?: number;
1041
+ endTimeInSeconds?: number;
1042
+ },
1043
+ ) {
1044
+ const config = this.requireGarminConfig();
1045
+ return await ctx.runAction(this.component.garmin.public.pullDailies, {
1046
+ ...args,
1047
+ clientId: config.clientId,
1048
+ clientSecret: config.clientSecret,
1049
+ });
1050
+ }
1051
+
1052
+ async pullGarminSleep(
1053
+ ctx: ActionCtx,
1054
+ args: {
1055
+ userId: string;
1056
+ startTimeInSeconds?: number;
1057
+ endTimeInSeconds?: number;
1058
+ },
1059
+ ) {
1060
+ const config = this.requireGarminConfig();
1061
+ return await ctx.runAction(this.component.garmin.public.pullSleep, {
1062
+ ...args,
1063
+ clientId: config.clientId,
1064
+ clientSecret: config.clientSecret,
1065
+ });
1066
+ }
1067
+
1068
+ async pullGarminBody(
1069
+ ctx: ActionCtx,
1070
+ args: {
1071
+ userId: string;
1072
+ startTimeInSeconds?: number;
1073
+ endTimeInSeconds?: number;
1074
+ },
1075
+ ) {
1076
+ const config = this.requireGarminConfig();
1077
+ return await ctx.runAction(this.component.garmin.public.pullBody, {
1078
+ ...args,
1079
+ clientId: config.clientId,
1080
+ clientSecret: config.clientSecret,
1081
+ });
1082
+ }
1083
+
1084
+ async pullGarminMenstruation(
1085
+ ctx: ActionCtx,
1086
+ args: {
1087
+ userId: string;
1088
+ startTimeInSeconds?: number;
1089
+ endTimeInSeconds?: number;
1090
+ },
1091
+ ) {
1092
+ const config = this.requireGarminConfig();
1093
+ return await ctx.runAction(this.component.garmin.public.pullMenstruation, {
1094
+ ...args,
1095
+ clientId: config.clientId,
1096
+ clientSecret: config.clientSecret,
1097
+ });
1098
+ }
1099
+
1100
+ async pullGarminBloodPressures(
1101
+ ctx: ActionCtx,
1102
+ args: {
1103
+ userId: string;
1104
+ startTimeInSeconds?: number;
1105
+ endTimeInSeconds?: number;
1106
+ },
1107
+ ) {
1108
+ const config = this.requireGarminConfig();
1109
+ return await ctx.runAction(this.component.garmin.public.pullBloodPressures, {
1110
+ ...args,
1111
+ clientId: config.clientId,
1112
+ clientSecret: config.clientSecret,
1113
+ });
1114
+ }
1115
+
1116
+ async pullGarminSkinTemperature(
1117
+ ctx: ActionCtx,
1118
+ args: {
1119
+ userId: string;
1120
+ startTimeInSeconds?: number;
1121
+ endTimeInSeconds?: number;
1122
+ },
1135
1123
  ) {
1136
1124
  const config = this.requireGarminConfig();
1137
- return await ctx.runAction(this.component.garmin.public.completeGarminOAuth, {
1125
+ return await ctx.runAction(this.component.garmin.public.pullSkinTemperature, {
1126
+ ...args,
1127
+ clientId: config.clientId,
1128
+ clientSecret: config.clientSecret,
1129
+ });
1130
+ }
1131
+
1132
+ async pullGarminUserMetrics(
1133
+ ctx: ActionCtx,
1134
+ args: {
1135
+ userId: string;
1136
+ startTimeInSeconds?: number;
1137
+ endTimeInSeconds?: number;
1138
+ },
1139
+ ) {
1140
+ const config = this.requireGarminConfig();
1141
+ return await ctx.runAction(this.component.garmin.public.pullUserMetrics, {
1142
+ ...args,
1143
+ clientId: config.clientId,
1144
+ clientSecret: config.clientSecret,
1145
+ });
1146
+ }
1147
+
1148
+ async pullGarminHRV(
1149
+ ctx: ActionCtx,
1150
+ args: {
1151
+ userId: string;
1152
+ startTimeInSeconds?: number;
1153
+ endTimeInSeconds?: number;
1154
+ },
1155
+ ) {
1156
+ const config = this.requireGarminConfig();
1157
+ return await ctx.runAction(this.component.garmin.public.pullHRV, {
1158
+ ...args,
1159
+ clientId: config.clientId,
1160
+ clientSecret: config.clientSecret,
1161
+ });
1162
+ }
1163
+
1164
+ async pullGarminStressDetails(
1165
+ ctx: ActionCtx,
1166
+ args: {
1167
+ userId: string;
1168
+ startTimeInSeconds?: number;
1169
+ endTimeInSeconds?: number;
1170
+ },
1171
+ ) {
1172
+ const config = this.requireGarminConfig();
1173
+ return await ctx.runAction(this.component.garmin.public.pullStressDetails, {
1174
+ ...args,
1175
+ clientId: config.clientId,
1176
+ clientSecret: config.clientSecret,
1177
+ });
1178
+ }
1179
+
1180
+ async pullGarminPulseOx(
1181
+ ctx: ActionCtx,
1182
+ args: {
1183
+ userId: string;
1184
+ startTimeInSeconds?: number;
1185
+ endTimeInSeconds?: number;
1186
+ },
1187
+ ) {
1188
+ const config = this.requireGarminConfig();
1189
+ return await ctx.runAction(this.component.garmin.public.pullPulseOx, {
1190
+ ...args,
1191
+ clientId: config.clientId,
1192
+ clientSecret: config.clientSecret,
1193
+ });
1194
+ }
1195
+
1196
+ async pullGarminRespiration(
1197
+ ctx: ActionCtx,
1198
+ args: {
1199
+ userId: string;
1200
+ startTimeInSeconds?: number;
1201
+ endTimeInSeconds?: number;
1202
+ },
1203
+ ) {
1204
+ const config = this.requireGarminConfig();
1205
+ return await ctx.runAction(this.component.garmin.public.pullRespiration, {
1206
+ ...args,
1207
+ clientId: config.clientId,
1208
+ clientSecret: config.clientSecret,
1209
+ });
1210
+ }
1211
+
1212
+ async pullGarminAll(
1213
+ ctx: ActionCtx,
1214
+ args: {
1215
+ userId: string;
1216
+ startTimeInSeconds?: number;
1217
+ endTimeInSeconds?: number;
1218
+ },
1219
+ ) {
1220
+ const config = this.requireGarminConfig();
1221
+ return await ctx.runAction(this.component.garmin.public.pullAll, {
1138
1222
  ...args,
1139
1223
  clientId: config.clientId,
1140
1224
  clientSecret: config.clientSecret,
@@ -1253,28 +1337,38 @@ type PaginateTimeRangeArgs = TimeRangeArgs & {
1253
1337
  /**
1254
1338
  * Per-provider options for `registerRoutes`.
1255
1339
  */
1256
- export interface StravaRouteOptions {
1340
+ export interface StravaOAuthOptions {
1257
1341
  /** HTTP path for the OAuth callback. @default "/api/strava/callback" */
1258
1342
  path?: string;
1259
1343
  /** Override STRAVA_CLIENT_ID env var. */
1260
1344
  clientId?: string;
1261
1345
  /** Override STRAVA_CLIENT_SECRET env var. */
1262
1346
  clientSecret?: string;
1263
- /** Override STRAVA_BASE_URL env var. */
1264
- baseUrl?: string;
1265
1347
  /** URL to redirect the user to after a successful connection. */
1266
- onSuccess?: string;
1348
+ redirectTo?: string;
1349
+ /** Called after Strava OAuth completes and the connection is established. */
1350
+ onComplete?: (
1351
+ ctx: GenericActionCtx<GenericDataModel>,
1352
+ event: StravaConnectEvent,
1353
+ ) => Promise<void>;
1354
+ }
1355
+
1356
+ // ─── Strava Callback Event Types ────────────────────────────────────────────
1357
+
1358
+ /** Data passed to `onComplete` after Strava OAuth completes. */
1359
+ export interface StravaConnectEvent {
1360
+ provider: "STRAVA";
1361
+ userId: string;
1362
+ connectionId: string;
1267
1363
  }
1268
1364
 
1269
1365
  // ─── Garmin Callback Event Types ─────────────────────────────────────────────
1270
1366
 
1271
- /** Data passed to `oauth.onComplete` after Garmin OAuth + initial sync. */
1367
+ /** Data passed to `oauth.onComplete` after Garmin OAuth completes. */
1272
1368
  export interface GarminConnectEvent {
1273
1369
  provider: "GARMIN";
1274
1370
  userId: string;
1275
1371
  connectionId: string;
1276
- synced: Record<string, number>;
1277
- errors: Array<{ type: string; id: string; error: string }>;
1278
1372
  }
1279
1373
 
1280
1374
  /** Data passed to webhook `events` handlers and `onEvent` after data ingestion. */
@@ -1297,7 +1391,7 @@ export interface GarminOAuthOptions {
1297
1391
  clientSecret?: string;
1298
1392
  /** URL to redirect the user to after a successful connection. */
1299
1393
  redirectTo?: string;
1300
- /** Called after Garmin OAuth completes and initial data sync finishes. */
1394
+ /** Called after Garmin OAuth completes and the connection is established. */
1301
1395
  onComplete?: (
1302
1396
  ctx: GenericActionCtx<GenericDataModel>,
1303
1397
  event: GarminConnectEvent,
@@ -1327,7 +1421,10 @@ export interface GarminWebhookOptions {
1327
1421
  }
1328
1422
 
1329
1423
  export interface RegisterRoutesOptions {
1330
- strava?: StravaRouteOptions;
1424
+ strava?: {
1425
+ /** OAuth callback configuration. */
1426
+ oauth?: StravaOAuthOptions;
1427
+ };
1331
1428
  garmin?: {
1332
1429
  /** OAuth callback configuration. */
1333
1430
  oauth?: GarminOAuthOptions;
@@ -1341,8 +1438,8 @@ export interface RegisterRoutesOptions {
1341
1438
  *
1342
1439
  * Call this from your `convex/http.ts` to set up the callback endpoints
1343
1440
  * that Strava and Garmin redirect to after user authorization. The handlers
1344
- * complete the OAuth exchange, create the connection, and sync data
1345
- * automatically.
1441
+ * complete the OAuth exchange, create the connection, and store tokens.
1442
+ * The host app is responsible for calling sync separately.
1346
1443
  *
1347
1444
  * When called with no `opts`, registers both Strava and Garmin routes with
1348
1445
  * default paths and credentials from environment variables. When `opts` is
@@ -1369,14 +1466,23 @@ export interface RegisterRoutesOptions {
1369
1466
  * // With Garmin OAuth callbacks and per-type webhook handlers
1370
1467
  * registerRoutes(http, components.soma, {
1371
1468
  * strava: {
1372
- * path: "/oauth/strava/callback",
1373
- * onSuccess: "https://myapp.com/settings",
1469
+ * oauth: {
1470
+ * path: "/oauth/strava/callback",
1471
+ * redirectTo: "https://myapp.com/settings",
1472
+ * onComplete: async (ctx, event) => {
1473
+ * // Runs after OAuth completes and connection is established
1474
+ * await ctx.runMutation(internal.users.markConnected, {
1475
+ * userId: event.userId,
1476
+ * provider: event.provider,
1477
+ * });
1478
+ * },
1479
+ * },
1374
1480
  * },
1375
1481
  * garmin: {
1376
1482
  * oauth: {
1377
1483
  * redirectTo: "https://myapp.com/settings",
1378
1484
  * onComplete: async (ctx, event) => {
1379
- * // Runs after OAuth + initial sync completes
1485
+ * // Runs after OAuth completes and connection is established
1380
1486
  * await ctx.runMutation(internal.users.markConnected, {
1381
1487
  * userId: event.userId,
1382
1488
  * provider: event.provider,
@@ -1406,8 +1512,9 @@ export function registerRoutes(
1406
1512
  const registerAll = opts === undefined;
1407
1513
 
1408
1514
  if (registerAll || opts?.strava) {
1409
- const strava = opts?.strava ?? {};
1410
- const path = strava.path ?? STRAVA_CALLBACK_PATH;
1515
+ const stravaOpts = opts?.strava ?? {};
1516
+ const oauth = stravaOpts.oauth ?? {};
1517
+ const path = oauth.path ?? STRAVA_CALLBACK_PATH;
1411
1518
 
1412
1519
  http.route({
1413
1520
  path,
@@ -1415,23 +1522,23 @@ export function registerRoutes(
1415
1522
  handler: httpActionGeneric(async (ctx, request) => {
1416
1523
  const url = new URL(request.url);
1417
1524
  const code = url.searchParams.get("code");
1418
- const userId = url.searchParams.get("state");
1525
+ const state = url.searchParams.get("state");
1419
1526
 
1420
1527
  if (!code) {
1421
1528
  return new Response("Missing authorization code", { status: 400 });
1422
1529
  }
1423
- if (!userId) {
1530
+ if (!state) {
1424
1531
  return new Response(
1425
- "Missing state parameter (userId). Pass the userId as the state " +
1426
- "parameter when building the Strava auth URL.",
1532
+ "Missing state parameter. Ensure the state was included " +
1533
+ "when building the Strava auth URL via getStravaAuthUrl.",
1427
1534
  { status: 400 },
1428
1535
  );
1429
1536
  }
1430
1537
 
1431
1538
  const clientId =
1432
- strava.clientId ?? process.env.STRAVA_CLIENT_ID;
1539
+ oauth.clientId ?? process.env.STRAVA_CLIENT_ID;
1433
1540
  const clientSecret =
1434
- strava.clientSecret ?? process.env.STRAVA_CLIENT_SECRET;
1541
+ oauth.clientSecret ?? process.env.STRAVA_CLIENT_SECRET;
1435
1542
 
1436
1543
  if (!clientId || !clientSecret) {
1437
1544
  return new Response(
@@ -1441,13 +1548,16 @@ export function registerRoutes(
1441
1548
  );
1442
1549
  }
1443
1550
 
1551
+ let result: {
1552
+ connectionId: string;
1553
+ userId: string;
1554
+ };
1444
1555
  try {
1445
- await ctx.runAction(component.strava.public.connectStrava, {
1446
- userId,
1556
+ result = await ctx.runAction(component.strava.public.completeStravaOAuth, {
1447
1557
  code,
1558
+ state,
1448
1559
  clientId,
1449
1560
  clientSecret,
1450
- baseUrl: strava.baseUrl ?? process.env.STRAVA_BASE_URL,
1451
1561
  });
1452
1562
  } catch (error) {
1453
1563
  const message =
@@ -1457,10 +1567,25 @@ export function registerRoutes(
1457
1567
  });
1458
1568
  }
1459
1569
 
1460
- if (strava.onSuccess) {
1570
+ if (oauth.onComplete) {
1571
+ try {
1572
+ await oauth.onComplete(ctx, {
1573
+ provider: "STRAVA",
1574
+ userId: result.userId,
1575
+ connectionId: result.connectionId,
1576
+ });
1577
+ } catch (callbackError) {
1578
+ console.error(
1579
+ "[soma] strava onComplete callback error:",
1580
+ callbackError instanceof Error ? callbackError.message : callbackError,
1581
+ );
1582
+ }
1583
+ }
1584
+
1585
+ if (oauth.redirectTo) {
1461
1586
  return new Response(null, {
1462
1587
  status: 302,
1463
- headers: { Location: strava.onSuccess },
1588
+ headers: { Location: oauth.redirectTo },
1464
1589
  });
1465
1590
  }
1466
1591
 
@@ -1513,8 +1638,6 @@ export function registerRoutes(
1513
1638
  let result: {
1514
1639
  connectionId: string;
1515
1640
  userId: string;
1516
- synced: Record<string, number>;
1517
- errors: Array<{ type: string; id: string; error: string }>;
1518
1641
  };
1519
1642
  try {
1520
1643
  result = await ctx.runAction(component.garmin.public.completeGarminOAuth, {
@@ -1537,8 +1660,6 @@ export function registerRoutes(
1537
1660
  provider: "GARMIN",
1538
1661
  userId: result.userId,
1539
1662
  connectionId: result.connectionId,
1540
- synced: result.synced,
1541
- errors: result.errors,
1542
1663
  });
1543
1664
  } catch (callbackError) {
1544
1665
  console.error(