@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,48 +7,100 @@ import { v } from "convex/values";
7
7
  import { action } from "../_generated/server";
8
8
  import { api, internal } from "../_generated/api";
9
9
  import type { Doc, Id } from "../_generated/dataModel";
10
- import { StravaClient } from "../../strava/client";
10
+ import { createStravaClient } from "./client.js";
11
+ import { listAllActivities } from "./utils.js";
11
12
  import {
13
+ getLoggedInAthlete,
14
+ getActivityById,
15
+ getActivityStreams,
16
+ } from "./types/stravaApi/sdk.gen.js";
17
+ import { generateState } from "../utils.js";
18
+ import {
19
+ buildAuthUrl,
12
20
  exchangeCode,
13
21
  refreshToken as refreshStravaToken,
14
- } from "../../strava/auth";
15
- import { transformActivity } from "../../strava/activity";
16
- import { transformAthlete } from "../../strava/athlete";
22
+ } from "./auth.js";
23
+ import { transformActivity } from "./transform/activity.js";
24
+ import { transformAthlete } from "./transform/athlete.js";
17
25
 
18
26
  // ─── Public Actions ──────────────────────────────────────────────────────────
19
27
 
20
28
  /**
21
- * Full Strava OAuth callback handler.
29
+ * Generate a Strava OAuth authorization URL.
30
+ *
31
+ * If `userId` is provided, the state parameter is stored in the component's
32
+ * `pendingOAuth` table so that `completeStravaOAuth` can look it up
33
+ * automatically when the callback fires. This is the recommended flow
34
+ * when using `registerRoutes`.
35
+ *
36
+ * If `userId` is omitted, the host app must store the returned `state`
37
+ * itself and pass the userId to `connectStrava` manually.
38
+ */
39
+ export const getStravaAuthUrl = action({
40
+ args: {
41
+ clientId: v.string(),
42
+ redirectUri: v.string(),
43
+ scope: v.optional(v.string()),
44
+ userId: v.optional(v.string()),
45
+ },
46
+ handler: async (ctx, args) => {
47
+ const state = generateState();
48
+
49
+ const authUrl = buildAuthUrl({
50
+ clientId: args.clientId,
51
+ redirectUri: args.redirectUri,
52
+ scope: args.scope,
53
+ state,
54
+ });
55
+
56
+ if (args.userId) {
57
+ await ctx.runMutation(internal.strava.private.storePendingOAuth, {
58
+ provider: "STRAVA",
59
+ state,
60
+ userId: args.userId,
61
+ });
62
+ }
63
+
64
+ return { authUrl, state };
65
+ },
66
+ });
67
+
68
+ /**
69
+ * Full Strava OAuth callback handler (manual flow).
22
70
  *
23
71
  * Exchanges the authorization code for tokens, creates/reactivates the
24
72
  * Soma connection, stores tokens securely, syncs the athlete profile,
25
73
  * and syncs all activities.
26
74
  *
75
+ * Used when the host app handles the callback itself and passes the
76
+ * userId directly (analogous to `connectGarmin`).
77
+ *
27
78
  * Returns `{ connectionId, synced, errors }`.
28
79
  */
29
80
  export const connectStrava = action({
30
81
  args: {
31
82
  userId: v.string(),
32
- code: v.string(),
33
83
  clientId: v.string(),
34
84
  clientSecret: v.string(),
35
- baseUrl: v.optional(v.string()),
36
- includeStreams: v.optional(v.boolean()),
85
+ code: v.string(),
37
86
  },
38
87
  returns: v.object({
39
88
  connectionId: v.string(),
40
- synced: v.number(),
89
+ synced: v.object({ athletes: v.number(), activities: v.number() }),
41
90
  errors: v.array(
42
- v.object({ activityId: v.number(), error: v.string() }),
91
+ v.object({ type: v.string(), id: v.string(), error: v.string() }),
43
92
  ),
44
93
  }),
45
- handler: async (ctx, args) => {
94
+ handler: async (ctx, args): Promise<{
95
+ connectionId: Id<"connections">;
96
+ synced: { athletes: number; activities: number };
97
+ errors: Array<{ type: string; id: string; error: string }>;
98
+ }> => {
46
99
  // 1. Exchange authorization code for tokens
47
100
  const tokens = await exchangeCode({
48
101
  clientId: args.clientId,
49
102
  clientSecret: args.clientSecret,
50
103
  code: args.code,
51
- baseUrl: args.baseUrl,
52
104
  });
53
105
 
54
106
  // 2. Create/reactivate the Soma connection
@@ -61,7 +113,7 @@ export const connectStrava = action({
61
113
  );
62
114
 
63
115
  // 3. Store OAuth tokens in providerTokens table
64
- const _stored: null = await ctx.runMutation(
116
+ await ctx.runMutation(
65
117
  internal.strava.private.storeTokens,
66
118
  {
67
119
  connectionId,
@@ -71,54 +123,109 @@ export const connectStrava = action({
71
123
  },
72
124
  );
73
125
 
74
- // 4. Sync athlete profile
75
- const client = new StravaClient({
126
+ // 4. Sync all data types
127
+ const result = await ctx.runAction(api.strava.public.syncAllTypes, {
76
128
  accessToken: tokens.access_token,
77
- baseUrl: args.baseUrl,
129
+ connectionId,
130
+ userId: args.userId,
78
131
  });
79
132
 
80
- const athlete = await client.getAthlete();
81
- const athleteData = transformAthlete(athlete);
82
- const _athleteId: Id<"athletes"> = await ctx.runMutation(
83
- api.public.ingestAthlete,
133
+ // 5. Update lastDataUpdate timestamp
134
+ await ctx.runMutation(
135
+ api.public.updateConnection,
84
136
  {
85
137
  connectionId,
86
- userId: args.userId,
87
- ...athleteData,
88
- } as never,
138
+ lastDataUpdate: new Date().toISOString(),
139
+ },
89
140
  );
90
141
 
91
- // 5. Sync all activities
92
- const summaries = await client.listAllActivities();
93
- let synced = 0;
94
- const errors: Array<{ activityId: number; error: string }> = [];
142
+ return { connectionId, synced: result.synced, errors: result.errors };
143
+ },
144
+ });
95
145
 
96
- for (const summary of summaries) {
97
- try {
98
- const detailed = await client.getActivity(summary.id);
99
- const streams = args.includeStreams
100
- ? await client.getActivityStreams(summary.id)
101
- : undefined;
102
- const data = transformActivity(detailed, { streams });
103
- const _activityId: Id<"activities"> = await ctx.runMutation(
104
- api.public.ingestActivity,
105
- {
106
- connectionId,
107
- userId: args.userId,
108
- ...data,
109
- } as never,
110
- );
111
- synced++;
112
- } catch (err) {
113
- errors.push({
114
- activityId: summary.id,
115
- error: err instanceof Error ? err.message : String(err),
116
- });
117
- }
146
+ /**
147
+ * Complete a Strava OAuth flow using stored pending state.
148
+ *
149
+ * Used by `registerRoutes` — the callback handler calls this with the
150
+ * `code` and `state` from the redirect. The action looks up the pending
151
+ * state (userId) stored during `getStravaAuthUrl`, exchanges for tokens,
152
+ * creates the connection, syncs data, and cleans up the pending entry.
153
+ */
154
+ export const completeStravaOAuth = action({
155
+ args: {
156
+ code: v.string(),
157
+ state: v.string(),
158
+ clientId: v.string(),
159
+ clientSecret: v.string(),
160
+ },
161
+ returns: v.object({
162
+ connectionId: v.string(),
163
+ userId: v.string(),
164
+ synced: v.object({ athletes: v.number(), activities: v.number() }),
165
+ errors: v.array(
166
+ v.object({ type: v.string(), id: v.string(), error: v.string() }),
167
+ ),
168
+ }),
169
+ handler: async (ctx, args): Promise<{
170
+ connectionId: Id<"connections">;
171
+ userId: string;
172
+ synced: { athletes: number; activities: number };
173
+ errors: Array<{ type: string; id: string; error: string }>;
174
+ }> => {
175
+ // 1. Look up pending state
176
+ const pending: Doc<"pendingOAuth"> | null = await ctx.runQuery(
177
+ internal.strava.private.getPendingOAuth,
178
+ { state: args.state },
179
+ );
180
+ if (!pending) {
181
+ throw new Error(
182
+ "No pending Strava OAuth state found for this state parameter. " +
183
+ "The authorization may have expired or was already used.",
184
+ );
118
185
  }
119
186
 
120
- // 6. Update lastDataUpdate timestamp
121
- const _updated: null = await ctx.runMutation(
187
+ // 2. Exchange authorization code for tokens
188
+ const tokens = await exchangeCode({
189
+ clientId: args.clientId,
190
+ clientSecret: args.clientSecret,
191
+ code: args.code,
192
+ });
193
+
194
+ // 3. Clean up pending entry
195
+ await ctx.runMutation(
196
+ internal.strava.private.deletePendingOAuth,
197
+ { state: args.state },
198
+ );
199
+
200
+ // 4. Create/reactivate the Soma connection
201
+ const connectionId: Id<"connections"> = await ctx.runMutation(
202
+ api.public.connect,
203
+ {
204
+ userId: pending.userId,
205
+ provider: "STRAVA",
206
+ },
207
+ );
208
+
209
+ // 5. Store OAuth tokens
210
+ await ctx.runMutation(
211
+ internal.strava.private.storeTokens,
212
+ {
213
+ connectionId,
214
+ accessToken: tokens.access_token,
215
+ refreshToken: tokens.refresh_token,
216
+ expiresAt: tokens.expires_at,
217
+ },
218
+ );
219
+
220
+ // 6. Sync all data types
221
+ const result = await ctx.runAction(api.strava.public.syncAllTypes, {
222
+ accessToken: tokens.access_token,
223
+ connectionId,
224
+ userId: pending.userId,
225
+ });
226
+
227
+ // 7. Update lastDataUpdate timestamp
228
+ await ctx.runMutation(
122
229
  api.public.updateConnection,
123
230
  {
124
231
  connectionId,
@@ -126,7 +233,12 @@ export const connectStrava = action({
126
233
  },
127
234
  );
128
235
 
129
- return { connectionId, synced, errors };
236
+ return {
237
+ connectionId,
238
+ userId: pending.userId,
239
+ synced: result.synced,
240
+ errors: result.errors,
241
+ };
130
242
  },
131
243
  });
132
244
 
@@ -143,17 +255,18 @@ export const syncStrava = action({
143
255
  userId: v.string(),
144
256
  clientId: v.string(),
145
257
  clientSecret: v.string(),
146
- baseUrl: v.optional(v.string()),
147
- includeStreams: v.optional(v.boolean()),
148
258
  after: v.optional(v.number()),
149
259
  },
150
260
  returns: v.object({
151
- synced: v.number(),
261
+ synced: v.object({ athletes: v.number(), activities: v.number() }),
152
262
  errors: v.array(
153
- v.object({ activityId: v.number(), error: v.string() }),
263
+ v.object({ type: v.string(), id: v.string(), error: v.string() }),
154
264
  ),
155
265
  }),
156
- handler: async (ctx, args) => {
266
+ handler: async (ctx, args): Promise<{
267
+ synced: { athletes: number; activities: number };
268
+ errors: Array<{ type: string; id: string; error: string }>;
269
+ }> => {
157
270
  // 1. Look up connection
158
271
  const connection: Doc<"connections"> | null = await ctx.runQuery(
159
272
  internal.private.getConnectionByProvider,
@@ -199,10 +312,9 @@ export const syncStrava = action({
199
312
  clientId: args.clientId,
200
313
  clientSecret: args.clientSecret,
201
314
  refreshToken: tokenDoc.refreshToken,
202
- baseUrl: args.baseUrl,
203
315
  });
204
316
  accessToken = refreshed.access_token;
205
- const _refreshed: null = await ctx.runMutation(
317
+ await ctx.runMutation(
206
318
  internal.strava.private.storeTokens,
207
319
  {
208
320
  connectionId,
@@ -213,68 +325,119 @@ export const syncStrava = action({
213
325
  );
214
326
  }
215
327
 
216
- // 4. Create client and sync
217
- const client = new StravaClient({
328
+ // 4. Sync all data types
329
+ const result = await ctx.runAction(api.strava.public.syncAllTypes, {
218
330
  accessToken,
219
- baseUrl: args.baseUrl,
331
+ connectionId,
332
+ userId: args.userId,
333
+ after: args.after,
220
334
  });
221
335
 
222
- // Sync athlete profile
223
- const athlete = await client.getAthlete();
224
- const athleteData = transformAthlete(athlete);
225
- const _athleteId: Id<"athletes"> = await ctx.runMutation(
226
- api.public.ingestAthlete,
336
+ // 5. Update lastDataUpdate timestamp
337
+ await ctx.runMutation(
338
+ api.public.updateConnection,
227
339
  {
228
340
  connectionId,
229
- userId: args.userId,
230
- ...athleteData,
231
- } as never,
341
+ lastDataUpdate: new Date().toISOString(),
342
+ },
232
343
  );
233
344
 
234
- // Sync activities (optionally incremental via `after`)
235
- const summaries = await client.listAllActivities({
236
- after: args.after,
237
- });
238
- let synced = 0;
239
- const errors: Array<{ activityId: number; error: string }> = [];
345
+ return { synced: result.synced, errors: result.errors };
346
+ },
347
+ });
240
348
 
241
- for (const summary of summaries) {
242
- try {
243
- const detailed = await client.getActivity(summary.id);
244
- const streams = args.includeStreams
245
- ? await client.getActivityStreams(summary.id)
246
- : undefined;
247
- const data = transformActivity(detailed, { streams });
248
- const _activityId: Id<"activities"> = await ctx.runMutation(
249
- api.public.ingestActivity,
250
- {
349
+ // ─── Sync Engine ────────────────────────────────────────────────────────────
350
+
351
+ /**
352
+ * Fetch and ingest all Strava data types for a connected user.
353
+ *
354
+ * Called by the public actions (connectStrava, completeStravaOAuth, syncStrava)
355
+ * after obtaining a valid access token.
356
+ */
357
+ export const syncAllTypes = action({
358
+ args: {
359
+ accessToken: v.string(),
360
+ connectionId: v.id("connections"),
361
+ userId: v.string(),
362
+ after: v.optional(v.number()),
363
+ before: v.optional(v.number()),
364
+ },
365
+ handler: async (ctx, args) => {
366
+ const { accessToken, connectionId, userId } = args;
367
+ const client = createStravaClient(accessToken);
368
+
369
+ const synced = { athletes: 0, activities: 0 };
370
+ const errors: Array<{ type: string; id: string; error: string }> = [];
371
+
372
+ // ── Athlete ──────────────────────────────────────────────────────────
373
+ try {
374
+ const { data: athlete, error } = await getLoggedInAthlete({ client });
375
+ if (error || !athlete) throw new Error(error ? JSON.stringify(error) : "No athlete data");
376
+ const data = transformAthlete(athlete);
377
+ await ctx.runMutation(api.public.ingestAthlete, {
378
+ connectionId,
379
+ userId,
380
+ ...data,
381
+ } as never);
382
+ synced.athletes++;
383
+ } catch (err) {
384
+ errors.push({
385
+ type: "athlete",
386
+ id: "fetch",
387
+ error: err instanceof Error ? err.message : String(err),
388
+ });
389
+ }
390
+
391
+ // ── Activities ───────────────────────────────────────────────────────
392
+ try {
393
+ const summaries = await listAllActivities(client, {
394
+ after: args.after,
395
+ before: args.before,
396
+ });
397
+ for (const summary of summaries) {
398
+ if (summary.id == null) continue;
399
+ try {
400
+ const { data: detailed, error: detailError } = await getActivityById({ client, path: { id: summary.id } });
401
+ if (detailError || !detailed) throw new Error(detailError ? JSON.stringify(detailError) : "No activity data");
402
+
403
+ const { data: streams } = await getActivityStreams({
404
+ client,
405
+ path: { id: summary.id },
406
+ query: {
407
+ keys: ["time", "heartrate", "watts", "cadence", "latlng", "altitude", "velocity_smooth", "grade_smooth", "distance", "temp"],
408
+ key_by_type: true,
409
+ },
410
+ });
411
+
412
+ const data = transformActivity(detailed, { streams: streams ?? undefined });
413
+ await ctx.runMutation(api.public.ingestActivity, {
251
414
  connectionId,
252
- userId: args.userId,
415
+ userId,
253
416
  ...data,
254
- } as never,
255
- );
256
- synced++;
257
- } catch (err) {
258
- errors.push({
259
- activityId: summary.id,
260
- error: err instanceof Error ? err.message : String(err),
261
- });
417
+ } as never);
418
+ synced.activities++;
419
+ } catch (err) {
420
+ errors.push({
421
+ type: "activity",
422
+ id: String(summary.id),
423
+ error: err instanceof Error ? err.message : String(err),
424
+ });
425
+ }
262
426
  }
427
+ } catch (err) {
428
+ errors.push({
429
+ type: "activity",
430
+ id: "fetch",
431
+ error: err instanceof Error ? err.message : String(err),
432
+ });
263
433
  }
264
434
 
265
- // 5. Update lastDataUpdate timestamp
266
- const _updated: null = await ctx.runMutation(
267
- api.public.updateConnection,
268
- {
269
- connectionId,
270
- lastDataUpdate: new Date().toISOString(),
271
- },
272
- );
273
-
274
435
  return { synced, errors };
275
436
  },
276
437
  });
277
438
 
439
+ // ─── Disconnect ─────────────────────────────────────────────────────────────
440
+
278
441
  /**
279
442
  * Disconnect a user from Strava.
280
443
  *
@@ -286,7 +449,6 @@ export const disconnectStrava = action({
286
449
  userId: v.string(),
287
450
  clientId: v.string(),
288
451
  clientSecret: v.string(),
289
- baseUrl: v.optional(v.string()),
290
452
  },
291
453
  returns: v.null(),
292
454
  handler: async (ctx, args) => {
@@ -310,11 +472,7 @@ export const disconnectStrava = action({
310
472
  );
311
473
  if (tokenDoc) {
312
474
  try {
313
- const base = (args.baseUrl ?? "https://www.strava.com").replace(
314
- /\/+$/,
315
- "",
316
- );
317
- await fetch(`${base}/oauth/deauthorize`, {
475
+ await fetch("https://www.strava.com/oauth/deauthorize", {
318
476
  method: "POST",
319
477
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
320
478
  body: `access_token=${tokenDoc.accessToken}`,