@nativesquare/soma 0.13.1 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/dist/client/index.d.ts +2 -2
  2. package/dist/client/index.d.ts.map +1 -1
  3. package/dist/client/index.js +1 -1
  4. package/dist/client/index.js.map +1 -1
  5. package/dist/client/strava.d.ts +45 -33
  6. package/dist/client/strava.d.ts.map +1 -1
  7. package/dist/client/strava.js +138 -22
  8. package/dist/client/strava.js.map +1 -1
  9. package/dist/client/types.d.ts +108 -0
  10. package/dist/client/types.d.ts.map +1 -1
  11. package/dist/component/_generated/api.d.ts +2 -2
  12. package/dist/component/_generated/api.d.ts.map +1 -1
  13. package/dist/component/_generated/component.d.ts +19 -17
  14. package/dist/component/_generated/component.d.ts.map +1 -1
  15. package/dist/component/garmin/auth.d.ts +2 -1
  16. package/dist/component/garmin/auth.d.ts.map +1 -1
  17. package/dist/component/garmin/auth.js +6 -1
  18. package/dist/component/garmin/auth.js.map +1 -1
  19. package/dist/component/garmin/private.d.ts +17 -75
  20. package/dist/component/garmin/private.d.ts.map +1 -1
  21. package/dist/component/garmin/private.js +4 -167
  22. package/dist/component/garmin/private.js.map +1 -1
  23. package/dist/component/garmin/public.d.ts +18 -33
  24. package/dist/component/garmin/public.d.ts.map +1 -1
  25. package/dist/component/garmin/public.js +23 -22
  26. package/dist/component/garmin/public.js.map +1 -1
  27. package/dist/component/garmin/webhooks.d.ts +3 -6
  28. package/dist/component/garmin/webhooks.d.ts.map +1 -1
  29. package/dist/component/garmin/webhooks.js +17 -28
  30. package/dist/component/garmin/webhooks.js.map +1 -1
  31. package/dist/component/private.d.ts +59 -0
  32. package/dist/component/private.d.ts.map +1 -1
  33. package/dist/component/private.js +182 -1
  34. package/dist/component/private.js.map +1 -1
  35. package/dist/component/strava/auth.d.ts +2 -1
  36. package/dist/component/strava/auth.d.ts.map +1 -1
  37. package/dist/component/strava/auth.js +6 -1
  38. package/dist/component/strava/auth.js.map +1 -1
  39. package/dist/component/strava/public.d.ts +26 -50
  40. package/dist/component/strava/public.d.ts.map +1 -1
  41. package/dist/component/strava/public.js +88 -132
  42. package/dist/component/strava/public.js.map +1 -1
  43. package/dist/component/strava/webhooks.d.ts +17 -0
  44. package/dist/component/strava/webhooks.d.ts.map +1 -0
  45. package/dist/component/strava/webhooks.js +231 -0
  46. package/dist/component/strava/webhooks.js.map +1 -0
  47. package/dist/component/utils.d.ts +10 -0
  48. package/dist/component/utils.d.ts.map +1 -1
  49. package/dist/component/utils.js.map +1 -1
  50. package/dist/component/validators/shared.d.ts +3 -0
  51. package/dist/component/validators/shared.d.ts.map +1 -1
  52. package/dist/component/validators/shared.js +1 -1
  53. package/dist/component/validators/shared.js.map +1 -1
  54. package/package.json +1 -1
  55. package/src/client/index.ts +8 -1
  56. package/src/client/strava.ts +190 -26
  57. package/src/client/types.ts +125 -0
  58. package/src/component/_generated/api.ts +2 -2
  59. package/src/component/_generated/component.ts +25 -6
  60. package/src/component/garmin/auth.ts +9 -2
  61. package/src/component/garmin/private.ts +22 -243
  62. package/src/component/garmin/public.ts +56 -54
  63. package/src/component/garmin/webhooks.ts +38 -55
  64. package/src/component/private.ts +245 -1
  65. package/src/component/strava/auth.ts +9 -2
  66. package/src/component/strava/public.ts +105 -171
  67. package/src/component/strava/webhooks.ts +312 -0
  68. package/src/component/utils.ts +11 -0
  69. package/src/component/validators/shared.ts +5 -2
  70. package/dist/component/strava/private.d.ts +0 -49
  71. package/dist/component/strava/private.d.ts.map +0 -1
  72. package/dist/component/strava/private.js +0 -121
  73. package/dist/component/strava/private.js.map +0 -1
  74. package/src/component/strava/private.ts +0 -147
@@ -2,6 +2,8 @@
2
2
  // Pure helper functions for the Garmin OAuth 2.0 PKCE flow.
3
3
  // Uses the Web Crypto API for SHA-256 challenge generation and global `fetch`.
4
4
 
5
+ import type { OAuthRefreshResult } from "../utils.js";
6
+
5
7
  export interface GarminOAuth2TokenResponse {
6
8
  access_token: string;
7
9
  refresh_token: string;
@@ -158,7 +160,7 @@ export interface RefreshTokenOptions {
158
160
  */
159
161
  export async function refreshToken(
160
162
  opts: RefreshTokenOptions,
161
- ): Promise<GarminOAuth2TokenResponse> {
163
+ ): Promise<OAuthRefreshResult> {
162
164
  const body = new URLSearchParams({
163
165
  grant_type: "refresh_token",
164
166
  client_id: opts.clientId,
@@ -179,5 +181,10 @@ export async function refreshToken(
179
181
  );
180
182
  }
181
183
 
182
- return (await response.json()) as GarminOAuth2TokenResponse;
184
+ const raw = (await response.json()) as GarminOAuth2TokenResponse;
185
+ return {
186
+ access_token: raw.access_token,
187
+ refresh_token: raw.refresh_token,
188
+ expiresAt: Math.floor(Date.now() / 1000) + raw.expires_in,
189
+ };
183
190
  }
@@ -1,14 +1,11 @@
1
- // ─── Garmin Internal Mutations ───────────────────────────────────────────────
2
- // Token CRUD and pending OAuth state management.
3
- // Called only from garmin/public.ts and garmin/webhooks.ts.
1
+ // ─── Garmin Internal Functions ───────────────────────────────────────────────
2
+ // Garmin-specific internal actions (token refresh, webhook payload processing).
3
+ // Shared CRUD (tokens, pending OAuth) lives in the root private.ts.
4
4
 
5
5
  import { v } from "convex/values";
6
- import {
7
- internalAction,
8
- internalMutation,
9
- internalQuery,
10
- } from "../_generated/server";
6
+ import { internalAction } from "../_generated/server";
11
7
  import { internal } from "../_generated/api";
8
+ import type { SomaError } from "../validators/shared.js";
12
9
  import {
13
10
  garminActivityPingPayloadSchema,
14
11
  garminActivityPushPayloadSchema,
@@ -94,224 +91,6 @@ import {
94
91
  garminMenstrualCycleTrackingPushPayloadSchema,
95
92
  } from "./schemas/menstrualCycleTracking.js";
96
93
  import { transformMenstrualCycleTracking } from "./transform/menstrualCycleTracking.js";
97
- import { refreshToken } from "./auth";
98
- import type { Doc, Id } from "../_generated/dataModel";
99
-
100
- const REFRESH_BUFFER_SECONDS = 600;
101
- // ─── Internal Pending OAuth CRUD ─────────────────────────────────────────────
102
- // Temporary storage for in-progress Garmin OAuth 2.0 PKCE flows.
103
- // Bridges getGarminAuthUrl and completeGarminOAuth.
104
-
105
- export const storePendingOAuth = internalMutation({
106
- args: {
107
- provider: v.string(),
108
- state: v.string(),
109
- codeVerifier: v.string(),
110
- userId: v.string(),
111
- },
112
- returns: v.null(),
113
- handler: async (ctx, args) => {
114
- await ctx.db.insert("pendingOAuth", {
115
- ...args,
116
- createdAt: Date.now(),
117
- });
118
- return null;
119
- },
120
- });
121
-
122
- export const getPendingOAuth = internalQuery({
123
- args: { state: v.string() },
124
- handler: async (ctx, args) => {
125
- return await ctx.db
126
- .query("pendingOAuth")
127
- .withIndex("by_state", (q) => q.eq("state", args.state))
128
- .first();
129
- },
130
- });
131
-
132
- export const deletePendingOAuth = internalMutation({
133
- args: { state: v.string() },
134
- returns: v.null(),
135
- handler: async (ctx, args) => {
136
- const pending = await ctx.db
137
- .query("pendingOAuth")
138
- .withIndex("by_state", (q) => q.eq("state", args.state))
139
- .first();
140
- if (pending) {
141
- await ctx.db.delete(pending._id);
142
- }
143
- return null;
144
- },
145
- });
146
-
147
- export const resolveConnectionAndAccessToken = internalAction({
148
- args: {
149
- userId: v.string(),
150
- clientId: v.string(),
151
- clientSecret: v.string(),
152
- },
153
- handler: async (ctx, args): Promise<{
154
- connectionId: Id<"connections">;
155
- accessToken: string;
156
- }> => {
157
- const connection: Doc<"connections"> | null = await ctx.runQuery(
158
- internal.private.getConnectionByProvider,
159
- { userId: args.userId, provider: "GARMIN" },
160
- );
161
-
162
- if (!connection) {
163
- throw new Error(
164
- `No Garmin connection found for user "${args.userId}". ` +
165
- "Connect to Garmin first via getGarminAuthUrl.",
166
- );
167
- }
168
-
169
- if (!connection.active) {
170
- throw new Error(
171
- `Garmin connection for user "${args.userId}" is inactive. Reconnect first.`,
172
- );
173
- }
174
-
175
- const connectionId = connection._id;
176
-
177
- const tokenDoc: Doc<"providerTokens"> | null = await ctx.runQuery(
178
- internal.garmin.private.getTokens,
179
- { connectionId },
180
- );
181
-
182
- if (!tokenDoc) {
183
- throw new Error(
184
- "No Garmin tokens found for this connection. " +
185
- "The connection may have been created before token storage was available.",
186
- );
187
- }
188
-
189
- let accessToken = tokenDoc.accessToken;
190
-
191
- // Refresh the token if it's expired or about to expire
192
- const nowSeconds = Math.floor(Date.now() / 1000);
193
- if (
194
- tokenDoc.expiresAt &&
195
- tokenDoc.refreshToken &&
196
- nowSeconds >= tokenDoc.expiresAt - REFRESH_BUFFER_SECONDS
197
- ) {
198
- const refreshed = await refreshToken({
199
- clientId: args.clientId,
200
- clientSecret: args.clientSecret,
201
- refreshToken: tokenDoc.refreshToken,
202
- });
203
-
204
- accessToken = refreshed.access_token;
205
- const newExpiresAt = nowSeconds + refreshed.expires_in;
206
-
207
- const _refreshed: null = await ctx.runMutation(
208
- internal.garmin.private.storeTokens,
209
- {
210
- connectionId,
211
- accessToken: refreshed.access_token,
212
- refreshToken: refreshed.refresh_token,
213
- expiresAt: newExpiresAt,
214
- },
215
- );
216
- }
217
-
218
- return {
219
- connectionId,
220
- accessToken,
221
- };
222
- },
223
- });
224
-
225
- // ─── Internal Token CRUD ─────────────────────────────────────────────────────
226
-
227
- /**
228
- * Store OAuth 2.0 tokens for a Garmin connection.
229
- * Upserts by connectionId — one token record per connection.
230
- */
231
- export const storeTokens = internalMutation({
232
- args: {
233
- connectionId: v.id("connections"),
234
- accessToken: v.string(),
235
- refreshToken: v.string(),
236
- expiresAt: v.number(),
237
- },
238
- returns: v.null(),
239
- handler: async (ctx, args) => {
240
- const existing = await ctx.db
241
- .query("providerTokens")
242
- .withIndex("by_connectionId", (q) =>
243
- q.eq("connectionId", args.connectionId),
244
- )
245
- .first();
246
-
247
- if (existing) {
248
- await ctx.db.patch(existing._id, {
249
- accessToken: args.accessToken,
250
- refreshToken: args.refreshToken,
251
- expiresAt: args.expiresAt,
252
- });
253
- return null;
254
- }
255
-
256
- await ctx.db.insert("providerTokens", {
257
- connectionId: args.connectionId,
258
- accessToken: args.accessToken,
259
- refreshToken: args.refreshToken,
260
- expiresAt: args.expiresAt,
261
- });
262
- return null;
263
- },
264
- });
265
-
266
- /**
267
- * Get stored tokens for a connection.
268
- */
269
- export const getTokens = internalQuery({
270
- args: { connectionId: v.id("connections") },
271
- returns: v.union(
272
- v.object({
273
- _id: v.id("providerTokens"),
274
- _creationTime: v.number(),
275
- connectionId: v.id("connections"),
276
- accessToken: v.string(),
277
- refreshToken: v.optional(v.string()),
278
- expiresAt: v.optional(v.number()),
279
- }),
280
- v.null(),
281
- ),
282
- handler: async (ctx, args) => {
283
- return await ctx.db
284
- .query("providerTokens")
285
- .withIndex("by_connectionId", (q) =>
286
- q.eq("connectionId", args.connectionId),
287
- )
288
- .first();
289
- },
290
- });
291
-
292
- /**
293
- * Delete stored tokens for a connection.
294
- */
295
- export const deleteTokens = internalMutation({
296
- args: { connectionId: v.id("connections") },
297
- returns: v.null(),
298
- handler: async (ctx, args) => {
299
- const existing = await ctx.db
300
- .query("providerTokens")
301
- .withIndex("by_connectionId", (q) =>
302
- q.eq("connectionId", args.connectionId),
303
- )
304
- .first();
305
-
306
- if (existing) {
307
- await ctx.db.delete(existing._id);
308
- }
309
- return null;
310
- },
311
- });
312
-
313
-
314
-
315
94
  // ─── Activity Push Processing ───────────────────────────────────────────────
316
95
 
317
96
  /**
@@ -325,7 +104,7 @@ export const processActivityPushPayload = internalAction({
325
104
  const { activities } = garminActivityPushPayloadSchema.parse(args.payload);
326
105
 
327
106
  const items: Array<{ connectionId: string; userId: string; data: Record<string, unknown> }> = [];
328
- const errors: Array<{ type: string; id: string; message: string }> = [];
107
+ const errors: SomaError[] = [];
329
108
 
330
109
  // Group items by Garmin userId
331
110
  const byUser = new Map<string, typeof activities>();
@@ -416,7 +195,7 @@ export const processActivityDetailsPushPayload = internalAction({
416
195
  garminActivityDetailsPushPayloadSchema.parse(args.payload);
417
196
 
418
197
  const items: Array<{ connectionId: string; userId: string; data: Record<string, unknown> }> = [];
419
- const errors: Array<{ type: string; id: string; message: string }> = [];
198
+ const errors: SomaError[] = [];
420
199
 
421
200
  // Group items by Garmin userId
422
201
  const byUser = new Map<string, typeof activityDetails>();
@@ -507,7 +286,7 @@ export const processManuallyUpdatedActivitiesPushPayload = internalAction({
507
286
  garminManuallyUpdatedActivitiesPushPayloadSchema.parse(args.payload);
508
287
 
509
288
  const items: Array<{ connectionId: string; userId: string; data: Record<string, unknown> }> = [];
510
- const errors: Array<{ type: string; id: string; message: string }> = [];
289
+ const errors: SomaError[] = [];
511
290
 
512
291
  // Group items by Garmin userId
513
292
  const byUser = new Map<string, typeof manuallyUpdatedActivities>();
@@ -600,7 +379,7 @@ export const processMoveIQPushPayload = internalAction({
600
379
  );
601
380
 
602
381
  const items: Array<{ connectionId: string; userId: string; data: Record<string, unknown> }> = [];
603
- const errors: Array<{ type: string; id: string; message: string }> = [];
382
+ const errors: SomaError[] = [];
604
383
 
605
384
  // Group items by Garmin userId
606
385
  const byUser = new Map<string, typeof moveIQActivities>();
@@ -692,7 +471,7 @@ export const processBloodPressurePushPayload = internalAction({
692
471
  garminBloodPressurePushPayloadSchema.parse(args.payload);
693
472
 
694
473
  const items: Array<{ connectionId: string; userId: string; data: Record<string, unknown> }> = [];
695
- const errors: Array<{ type: string; id: string; message: string }> = [];
474
+ const errors: SomaError[] = [];
696
475
 
697
476
  // Group items by Garmin userId
698
477
  const byUser = new Map<string, typeof bloodPressures>();
@@ -784,7 +563,7 @@ export const processBodyCompositionsPushPayload = internalAction({
784
563
  garminBodyCompositionsPushPayloadSchema.parse(args.payload);
785
564
 
786
565
  const items: Array<{ connectionId: string; userId: string; data: Record<string, unknown> }> = [];
787
- const errors: Array<{ type: string; id: string; message: string }> = [];
566
+ const errors: SomaError[] = [];
788
567
 
789
568
  // Group items by Garmin userId
790
569
  const byUser = new Map<string, typeof bodyComps>();
@@ -876,7 +655,7 @@ export const processDailiesPushPayload = internalAction({
876
655
  garminDailiesPushPayloadSchema.parse(args.payload);
877
656
 
878
657
  const items: Array<{ connectionId: string; userId: string; data: Record<string, unknown> }> = [];
879
- const errors: Array<{ type: string; id: string; message: string }> = [];
658
+ const errors: SomaError[] = [];
880
659
 
881
660
  // Group items by Garmin userId
882
661
  const byUser = new Map<string, typeof dailies>();
@@ -968,7 +747,7 @@ export const processHealthSnapshotPushPayload = internalAction({
968
747
  garminHealthSnapshotPushPayloadSchema.parse(args.payload);
969
748
 
970
749
  const items: Array<{ connectionId: string; userId: string; data: Record<string, unknown> }> = [];
971
- const errors: Array<{ type: string; id: string; message: string }> = [];
750
+ const errors: SomaError[] = [];
972
751
 
973
752
  // Group items by Garmin userId
974
753
  const byUser = new Map<string, typeof healthSnapshot>();
@@ -1060,7 +839,7 @@ export const processHRVSummaryPushPayload = internalAction({
1060
839
  garminHRVSummaryPushPayloadSchema.parse(args.payload);
1061
840
 
1062
841
  const items: Array<{ connectionId: string; userId: string; data: Record<string, unknown> }> = [];
1063
- const errors: Array<{ type: string; id: string; message: string }> = [];
842
+ const errors: SomaError[] = [];
1064
843
 
1065
844
  // Group items by Garmin userId
1066
845
  const byUser = new Map<string, typeof hrv>();
@@ -1152,7 +931,7 @@ export const processEpochPushPayload = internalAction({
1152
931
  garminEpochPushPayloadSchema.parse(args.payload);
1153
932
 
1154
933
  const items: Array<{ connectionId: string; userId: string; data: Record<string, unknown> }> = [];
1155
- const errors: Array<{ type: string; id: string; message: string }> = [];
934
+ const errors: SomaError[] = [];
1156
935
 
1157
936
  // Group items by Garmin userId
1158
937
  const byUser = new Map<string, typeof epochs>();
@@ -1244,7 +1023,7 @@ export const processPulseOxPushPayload = internalAction({
1244
1023
  garminPulseOxPushPayloadSchema.parse(args.payload);
1245
1024
 
1246
1025
  const items: Array<{ connectionId: string; userId: string; data: Record<string, unknown> }> = [];
1247
- const errors: Array<{ type: string; id: string; message: string }> = [];
1026
+ const errors: SomaError[] = [];
1248
1027
 
1249
1028
  // Group items by Garmin userId
1250
1029
  const byUser = new Map<string, typeof pulseox>();
@@ -1336,7 +1115,7 @@ export const processRespirationPushPayload = internalAction({
1336
1115
  garminRespirationPushPayloadSchema.parse(args.payload);
1337
1116
 
1338
1117
  const items: Array<{ connectionId: string; userId: string; data: Record<string, unknown> }> = [];
1339
- const errors: Array<{ type: string; id: string; message: string }> = [];
1118
+ const errors: SomaError[] = [];
1340
1119
 
1341
1120
  // Group items by Garmin userId
1342
1121
  const byUser = new Map<string, typeof allDayRespiration>();
@@ -1428,7 +1207,7 @@ export const processStressPushPayload = internalAction({
1428
1207
  garminStressPushPayloadSchema.parse(args.payload);
1429
1208
 
1430
1209
  const items: Array<{ connectionId: string; userId: string; data: Record<string, unknown> }> = [];
1431
- const errors: Array<{ type: string; id: string; message: string }> = [];
1210
+ const errors: SomaError[] = [];
1432
1211
 
1433
1212
  // Group items by Garmin userId
1434
1213
  const byUser = new Map<string, typeof stressDetails>();
@@ -1520,7 +1299,7 @@ export const processSkinTemperaturePushPayload = internalAction({
1520
1299
  garminSkinTemperaturePushPayloadSchema.parse(args.payload);
1521
1300
 
1522
1301
  const items: Array<{ connectionId: string; userId: string; data: Record<string, unknown> }> = [];
1523
- const errors: Array<{ type: string; id: string; message: string }> = [];
1302
+ const errors: SomaError[] = [];
1524
1303
 
1525
1304
  // Group items by Garmin userId
1526
1305
  const byUser = new Map<string, typeof skinTemp>();
@@ -1611,7 +1390,7 @@ export const processSleepsPushPayload = internalAction({
1611
1390
  const { sleeps } = garminSleepsPushPayloadSchema.parse(args.payload);
1612
1391
 
1613
1392
  const items: Array<{ connectionId: string; userId: string; data: Record<string, unknown> }> = [];
1614
- const errors: Array<{ type: string; id: string; message: string }> = [];
1393
+ const errors: SomaError[] = [];
1615
1394
 
1616
1395
  // Group items by Garmin userId
1617
1396
  const byUser = new Map<string, typeof sleeps>();
@@ -1701,7 +1480,7 @@ export const processUserMetricsPushPayload = internalAction({
1701
1480
  garminUserMetricsPushPayloadSchema.parse(args.payload);
1702
1481
 
1703
1482
  const items: Array<{ connectionId: string; userId: string; data: Record<string, unknown> }> = [];
1704
- const errors: Array<{ type: string; id: string; message: string }> = [];
1483
+ const errors: SomaError[] = [];
1705
1484
 
1706
1485
  // Group items by Garmin userId
1707
1486
  const byUser = new Map<string, typeof userMetrics>();
@@ -1793,7 +1572,7 @@ export const processMenstrualCycleTrackingPushPayload = internalAction({
1793
1572
  garminMenstrualCycleTrackingPushPayloadSchema.parse(args.payload);
1794
1573
 
1795
1574
  const items: Array<{ connectionId: string; userId: string; data: Record<string, unknown> }> = [];
1796
- const errors: Array<{ type: string; id: string; message: string }> = [];
1575
+ const errors: SomaError[] = [];
1797
1576
 
1798
1577
  // Group items by Garmin userId
1799
1578
  const byUser = new Map<string, typeof mct>();