@nativesquare/soma 0.16.5 → 0.16.7

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 (37) hide show
  1. package/dist/client/garmin.d.ts.map +1 -1
  2. package/dist/client/garmin.js +2 -0
  3. package/dist/client/garmin.js.map +1 -1
  4. package/dist/client/strava.d.ts.map +1 -1
  5. package/dist/client/strava.js +3 -1
  6. package/dist/client/strava.js.map +1 -1
  7. package/dist/client/types.d.ts +3 -3
  8. package/dist/client/types.d.ts.map +1 -1
  9. package/dist/component/_generated/api.d.ts +2 -0
  10. package/dist/component/_generated/api.d.ts.map +1 -1
  11. package/dist/component/_generated/api.js.map +1 -1
  12. package/dist/component/_generated/component.d.ts +4 -0
  13. package/dist/component/_generated/component.d.ts.map +1 -1
  14. package/dist/component/garmin/schemas/deregistration.d.ts +8 -0
  15. package/dist/component/garmin/schemas/deregistration.d.ts.map +1 -0
  16. package/dist/component/garmin/schemas/deregistration.js +12 -0
  17. package/dist/component/garmin/schemas/deregistration.js.map +1 -0
  18. package/dist/component/garmin/webhooks.d.ts +14 -0
  19. package/dist/component/garmin/webhooks.d.ts.map +1 -1
  20. package/dist/component/garmin/webhooks.js +67 -0
  21. package/dist/component/garmin/webhooks.js.map +1 -1
  22. package/dist/component/strava/webhooks.d.ts.map +1 -1
  23. package/dist/component/strava/webhooks.js +13 -0
  24. package/dist/component/strava/webhooks.js.map +1 -1
  25. package/dist/component/validators/shared.d.ts +1 -1
  26. package/dist/component/validators/shared.d.ts.map +1 -1
  27. package/dist/component/validators/shared.js.map +1 -1
  28. package/package.json +1 -1
  29. package/src/client/garmin.ts +3 -1
  30. package/src/client/strava.ts +16 -2
  31. package/src/client/types.ts +4 -3
  32. package/src/component/_generated/api.ts +2 -0
  33. package/src/component/_generated/component.ts +7 -0
  34. package/src/component/garmin/schemas/deregistration.ts +14 -0
  35. package/src/component/garmin/webhooks.ts +90 -0
  36. package/src/component/strava/webhooks.ts +42 -0
  37. package/src/component/validators/shared.ts +1 -0
@@ -6,7 +6,9 @@
6
6
  import { v } from "convex/values";
7
7
  import { action, type ActionCtx } from "../_generated/server";
8
8
  import { api, internal } from "../_generated/api";
9
+ import type { Doc } from "../_generated/dataModel";
9
10
  import type { SomaError, WebhookResult } from "../validators/shared.js";
11
+ import { garminDeregistrationPayloadSchema } from "./schemas/deregistration.js";
10
12
  import {
11
13
  garminSkinTemperaturePingPayloadSchema,
12
14
  garminSkinTemperaturePushPayloadSchema,
@@ -873,4 +875,92 @@ export const handleGarminWebhookRespiration = action({
873
875
  );
874
876
  return { errors: [], items: [] };
875
877
  },
878
+ });
879
+
880
+ // Deregistration uses WebhookResult with nullable data (terminal event with no
881
+ // payload to ingest), mirroring Strava's athlete-deauthorize convention.
882
+ type GarminDeregistrationResult = WebhookResult<Record<string, unknown> | null>;
883
+
884
+ /**
885
+ * Handle a Garmin deregistration notification.
886
+ *
887
+ * Garmin POSTs this when a user revokes access at connect.garmin.com. We mirror
888
+ * Strava's `athlete-deauthorize`: clear stored tokens and mark the connection
889
+ * inactive. Proceeds even when the connection is already inactive (e.g. a user
890
+ * who deregisters after a prior in-app disconnect).
891
+ */
892
+ export const handleGarminWebhookDeregistration = action({
893
+ args: { payload: v.any(), autoIngest: v.optional(v.boolean()) },
894
+ handler: async (ctx, args): Promise<GarminDeregistrationResult> => {
895
+ const shouldIngest = args.autoIngest !== false;
896
+
897
+ const parsed = garminDeregistrationPayloadSchema.safeParse(args.payload);
898
+ if (!parsed.success) {
899
+ console.warn(
900
+ `[garmin:webhook:deregistration] Payload did not match schema`,
901
+ );
902
+ return { errors: [], items: [] };
903
+ }
904
+
905
+ const errors: SomaError[] = [];
906
+ const items: GarminDeregistrationResult["items"] = [];
907
+
908
+ for (const entry of parsed.data.deregistrations) {
909
+ const garminUserId = entry.userId;
910
+
911
+ const connection: Doc<"connections"> | null = await ctx.runQuery(
912
+ internal.private.getConnectionByProviderUserId,
913
+ { providerUserId: garminUserId, provider: "GARMIN" },
914
+ );
915
+
916
+ if (!connection) {
917
+ errors.push({
918
+ type: "deregistration",
919
+ id: garminUserId,
920
+ message:
921
+ `No Soma connection found for Garmin userId "${garminUserId}". ` +
922
+ "The user may have been disconnected already, or never finished OAuth.",
923
+ });
924
+ continue;
925
+ }
926
+
927
+ if (shouldIngest) {
928
+ try {
929
+ await ctx.runMutation(internal.private.deleteTokens, {
930
+ connectionId: connection._id,
931
+ });
932
+ await ctx.runMutation(api.public.disconnect, {
933
+ userId: connection.userId,
934
+ provider: "GARMIN",
935
+ });
936
+ console.log(
937
+ `[garmin:webhook:deregistration] completed garminUserId=${garminUserId} userId=${connection.userId}`,
938
+ );
939
+ } catch (err) {
940
+ console.error(
941
+ `[garmin:webhook:deregistration] errored garminUserId=${garminUserId} userId=${connection.userId} error=${err instanceof Error ? err.message : String(err)}`,
942
+ );
943
+ errors.push({
944
+ type: "deregistration",
945
+ id: garminUserId,
946
+ message: err instanceof Error ? err.message : String(err),
947
+ });
948
+ items.push({
949
+ connectionId: connection._id,
950
+ userId: connection.userId,
951
+ data: null,
952
+ });
953
+ continue;
954
+ }
955
+ }
956
+
957
+ items.push({
958
+ connectionId: connection._id,
959
+ userId: connection.userId,
960
+ data: null,
961
+ });
962
+ }
963
+
964
+ return { errors, items };
965
+ },
876
966
  });
@@ -60,6 +60,9 @@ export const handleStravaWebhook = action({
60
60
  const eventName = `${payload.object_type}-${payload.aspect_type}`;
61
61
 
62
62
  if (!VALID_EVENT_NAMES.has(eventName)) {
63
+ console.warn(
64
+ `[strava:webhook] unknown eventName=${eventName} ownerId=${payload.owner_id} objectId=${payload.object_id}`,
65
+ );
63
66
  return { errors: [], items: [] };
64
67
  }
65
68
 
@@ -70,6 +73,9 @@ export const handleStravaWebhook = action({
70
73
  );
71
74
 
72
75
  if (!connection) {
76
+ console.warn(
77
+ `[strava:webhook] no connection found eventName=${eventName} ownerId=${payload.owner_id}`,
78
+ );
73
79
  return {
74
80
  errors: [{
75
81
  type: payload.object_type as SomaErrorType,
@@ -83,6 +89,9 @@ export const handleStravaWebhook = action({
83
89
 
84
90
  // Allow deauthorize to proceed even if connection is inactive
85
91
  if (!connection.active && eventName !== "athlete-deauthorize") {
92
+ console.warn(
93
+ `[strava:webhook] inactive connection eventName=${eventName} ownerId=${payload.owner_id} userId=${connection.userId}`,
94
+ );
86
95
  return {
87
96
  errors: [{
88
97
  type: payload.object_type as SomaErrorType,
@@ -93,6 +102,10 @@ export const handleStravaWebhook = action({
93
102
  };
94
103
  }
95
104
 
105
+ console.log(
106
+ `[strava:webhook] dispatching eventName=${eventName} ownerId=${payload.owner_id} userId=${connection.userId} objectId=${payload.object_id}`,
107
+ );
108
+
96
109
  const connectionId = connection._id;
97
110
  const userId = connection.userId;
98
111
 
@@ -105,6 +118,9 @@ export const handleStravaWebhook = action({
105
118
  }
106
119
 
107
120
  if (eventName === "activity-delete") {
121
+ console.log(
122
+ `[strava:webhook:activity-delete] received userId=${userId} objectId=${payload.object_id}`,
123
+ );
108
124
  return {
109
125
  errors: [],
110
126
  items: [{ connectionId, userId, data: null }],
@@ -150,6 +166,9 @@ async function handleActivityCreateOrUpdate(
150
166
  );
151
167
  accessToken = resolved.accessToken;
152
168
  } catch (err) {
169
+ console.error(
170
+ `[strava:webhook:activity] token resolution failed userId=${args.userId} objectId=${args.objectId} error=${err instanceof Error ? err.message : String(err)}`,
171
+ );
153
172
  return {
154
173
  errors: [{
155
174
  type: "activity",
@@ -194,11 +213,18 @@ async function handleActivityCreateOrUpdate(
194
213
  } as never);
195
214
  }
196
215
 
216
+ console.log(
217
+ `[strava:webhook:activity] completed userId=${args.userId} objectId=${args.objectId}`,
218
+ );
219
+
197
220
  return {
198
221
  errors,
199
222
  items: [{ connectionId: args.connectionId, userId: args.userId, data }],
200
223
  };
201
224
  } catch (err) {
225
+ console.error(
226
+ `[strava:webhook:activity] errored userId=${args.userId} objectId=${args.objectId} error=${err instanceof Error ? err.message : String(err)}`,
227
+ );
202
228
  errors.push({
203
229
  type: "activity",
204
230
  id: String(args.objectId),
@@ -226,6 +252,9 @@ async function handleAthleteUpdate(
226
252
  );
227
253
  accessToken = resolved.accessToken;
228
254
  } catch (err) {
255
+ console.error(
256
+ `[strava:webhook:athlete-update] token resolution failed userId=${args.userId} error=${err instanceof Error ? err.message : String(err)}`,
257
+ );
229
258
  return {
230
259
  errors: [{
231
260
  type: "athlete",
@@ -258,11 +287,18 @@ async function handleAthleteUpdate(
258
287
  } as never);
259
288
  }
260
289
 
290
+ console.log(
291
+ `[strava:webhook:athlete-update] completed userId=${args.userId}`,
292
+ );
293
+
261
294
  return {
262
295
  errors: [],
263
296
  items: [{ connectionId: args.connectionId, userId: args.userId, data }],
264
297
  };
265
298
  } catch (err) {
299
+ console.error(
300
+ `[strava:webhook:athlete-update] errored userId=${args.userId} error=${err instanceof Error ? err.message : String(err)}`,
301
+ );
266
302
  return {
267
303
  errors: [{
268
304
  type: "athlete",
@@ -291,7 +327,13 @@ async function handleAthleteDeauthorize(
291
327
  userId: args.userId,
292
328
  provider: "STRAVA",
293
329
  });
330
+ console.log(
331
+ `[strava:webhook:deauthorize] completed userId=${args.userId}`,
332
+ );
294
333
  } catch (err) {
334
+ console.error(
335
+ `[strava:webhook:deauthorize] errored userId=${args.userId} error=${err instanceof Error ? err.message : String(err)}`,
336
+ );
295
337
  return {
296
338
  errors: [{
297
339
  type: "athlete",
@@ -26,6 +26,7 @@ export type SomaErrorType =
26
26
  // Operation types
27
27
  | "deleteSchedule"
28
28
  | "deleteWorkout"
29
+ | "deregistration"
29
30
  | "ingest"
30
31
  | "pushSchedule"
31
32
  | "pushWorkout";