@nativesquare/soma 0.11.0 → 0.13.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.
- package/dist/client/garmin.d.ts +291 -0
- package/dist/client/garmin.d.ts.map +1 -0
- package/dist/client/garmin.js +493 -0
- package/dist/client/garmin.js.map +1 -0
- package/dist/client/index.d.ts +29 -394
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +30 -520
- package/dist/client/index.js.map +1 -1
- package/dist/client/strava.d.ts +97 -0
- package/dist/client/strava.d.ts.map +1 -0
- package/dist/client/strava.js +160 -0
- package/dist/client/strava.js.map +1 -0
- package/dist/client/types.d.ts +238 -0
- package/dist/client/types.d.ts.map +1 -1
- package/dist/component/_generated/component.d.ts +24 -12
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/garmin/private.d.ts +53 -68
- package/dist/component/garmin/private.d.ts.map +1 -1
- package/dist/component/garmin/private.js +87 -85
- package/dist/component/garmin/private.js.map +1 -1
- package/dist/component/garmin/public.d.ts +97 -53
- package/dist/component/garmin/public.d.ts.map +1 -1
- package/dist/component/garmin/public.js +75 -148
- package/dist/component/garmin/public.js.map +1 -1
- package/dist/component/garmin/webhooks.d.ts +22 -20
- package/dist/component/garmin/webhooks.d.ts.map +1 -1
- package/dist/component/garmin/webhooks.js +115 -76
- package/dist/component/garmin/webhooks.js.map +1 -1
- package/dist/component/public.d.ts +15 -15
- package/dist/component/schema.d.ts +25 -25
- package/dist/component/strava/public.d.ts +12 -8
- package/dist/component/strava/public.d.ts.map +1 -1
- package/dist/component/strava/public.js +7 -7
- package/dist/component/strava/public.js.map +1 -1
- package/dist/component/validators/activity.d.ts +4 -4
- package/dist/component/validators/body.d.ts +4 -4
- package/dist/component/validators/daily.d.ts +4 -4
- package/dist/component/validators/nutrition.d.ts +3 -3
- package/dist/component/validators/samples.d.ts +4 -4
- package/dist/component/validators/shared.d.ts +13 -4
- package/dist/component/validators/shared.d.ts.map +1 -1
- package/dist/component/validators/shared.js +7 -0
- package/dist/component/validators/shared.js.map +1 -1
- package/dist/component/validators/sleep.d.ts +5 -5
- package/dist/validators.d.ts +41 -40
- package/dist/validators.d.ts.map +1 -1
- package/dist/validators.js +1 -0
- package/dist/validators.js.map +1 -1
- package/package.json +1 -1
- package/src/client/garmin.ts +692 -0
- package/src/client/index.ts +68 -933
- package/src/client/strava.ts +199 -0
- package/src/client/types.ts +285 -0
- package/src/component/_generated/component.ts +19 -32
- package/src/component/garmin/private.ts +1872 -1870
- package/src/component/garmin/public.ts +1073 -1184
- package/src/component/garmin/webhooks.ts +898 -857
- package/src/component/strava/public.ts +393 -393
- package/src/component/validators/shared.ts +9 -0
- package/src/validators.ts +1 -0
package/src/client/index.ts
CHANGED
|
@@ -1,50 +1,47 @@
|
|
|
1
1
|
import type { ComponentApi } from "../component/_generated/component.js";
|
|
2
|
-
import type {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
2
|
+
import type {
|
|
3
|
+
MutationCtx,
|
|
4
|
+
QueryCtx,
|
|
5
|
+
SomaStravaConfig,
|
|
6
|
+
SomaGarminConfig,
|
|
7
|
+
IngestArgs,
|
|
8
|
+
ListTimeRangeArgs,
|
|
9
|
+
PaginateTimeRangeArgs,
|
|
10
|
+
RegisterRoutesOptions,
|
|
11
|
+
} from "./types.js";
|
|
12
|
+
export type {
|
|
13
|
+
ActionCtx,
|
|
14
|
+
SomaError,
|
|
15
|
+
SomaResult,
|
|
16
|
+
SomaStravaConfig,
|
|
17
|
+
SomaGarminConfig,
|
|
18
|
+
IngestArgs,
|
|
19
|
+
TimeRangeArgs,
|
|
20
|
+
ListTimeRangeArgs,
|
|
21
|
+
PaginateTimeRangeArgs,
|
|
22
|
+
StravaOAuthOptions,
|
|
23
|
+
StravaConnectEvent,
|
|
24
|
+
GarminConnectEvent,
|
|
25
|
+
GarminWebhookActionArgs,
|
|
26
|
+
GarminWebhookActionResult,
|
|
27
|
+
GarminWebhookEvent,
|
|
28
|
+
GarminOAuthOptions,
|
|
29
|
+
GarminWebhookEventName,
|
|
30
|
+
GarminWebhookHandler,
|
|
31
|
+
GarminWebhookOptions,
|
|
32
|
+
RegisterRoutesOptions,
|
|
33
|
+
} from "./types.js";
|
|
34
|
+
import type { HttpRouter } from "convex/server";
|
|
35
|
+
import { SomaGarmin } from "./garmin.js";
|
|
36
|
+
import { SomaStrava } from "./strava.js";
|
|
37
|
+
|
|
38
|
+
export { SomaGarmin } from "./garmin.js";
|
|
39
|
+
export { SomaStrava } from "./strava.js";
|
|
40
|
+
export { STRAVA_CALLBACK_PATH } from "./strava.js";
|
|
41
|
+
export { GARMIN_OAUTH_CALLBACK_PATH, GARMIN_WEBHOOK_BASE_PATH } from "./garmin.js";
|
|
10
42
|
|
|
11
43
|
export type SomaComponent = ComponentApi;
|
|
12
44
|
|
|
13
|
-
// ─── Default OAuth Callback Paths ────────────────────────────────────────────
|
|
14
|
-
// Used by `registerRoutes` as defaults. Override per-provider in the opts.
|
|
15
|
-
|
|
16
|
-
export const STRAVA_CALLBACK_PATH = "/api/strava/callback";
|
|
17
|
-
export const GARMIN_OAUTH_CALLBACK_PATH = "/api/garmin/callback";
|
|
18
|
-
export const GARMIN_WEBHOOK_BASE_PATH = "/api/garmin/webhook";
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Configuration for the Strava integration.
|
|
22
|
-
*
|
|
23
|
-
* If not provided to the constructor, the Soma class will attempt to
|
|
24
|
-
* read `STRAVA_CLIENT_ID`, `STRAVA_CLIENT_SECRET`, and `STRAVA_BASE_URL`
|
|
25
|
-
* from environment variables automatically.
|
|
26
|
-
*/
|
|
27
|
-
export interface SomaStravaConfig {
|
|
28
|
-
/** Your Strava application's Client ID. */
|
|
29
|
-
clientId: string;
|
|
30
|
-
/** Your Strava application's Client Secret. */
|
|
31
|
-
clientSecret: string;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Configuration for the Garmin integration.
|
|
36
|
-
*
|
|
37
|
-
* If not provided to the constructor, the Soma class will attempt to
|
|
38
|
-
* read `GARMIN_CLIENT_ID` and `GARMIN_CLIENT_SECRET` from
|
|
39
|
-
* environment variables automatically.
|
|
40
|
-
*/
|
|
41
|
-
export interface SomaGarminConfig {
|
|
42
|
-
/** Your Garmin application's Client ID. */
|
|
43
|
-
clientId: string;
|
|
44
|
-
/** Your Garmin application's Client Secret. */
|
|
45
|
-
clientSecret: string;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
45
|
/**
|
|
49
46
|
* Client class for the @nativesquare/soma Convex component.
|
|
50
47
|
*
|
|
@@ -52,7 +49,7 @@ export interface SomaGarminConfig {
|
|
|
52
49
|
* and querying normalized health & fitness data.
|
|
53
50
|
*
|
|
54
51
|
* All capabilities are also accessible via direct component function calls:
|
|
55
|
-
* `ctx.
|
|
52
|
+
* `ctx.runQuery(components.soma.public.listActivities, { userId: "user_123" })`
|
|
56
53
|
*
|
|
57
54
|
* @example
|
|
58
55
|
* ```ts
|
|
@@ -61,37 +58,42 @@ export interface SomaGarminConfig {
|
|
|
61
58
|
* import { components } from "./_generated/api";
|
|
62
59
|
*
|
|
63
60
|
* // Zero config if env vars are set in Convex dashboard:
|
|
64
|
-
* // STRAVA_CLIENT_ID, STRAVA_CLIENT_SECRET
|
|
61
|
+
* // STRAVA_CLIENT_ID, STRAVA_CLIENT_SECRET
|
|
62
|
+
* // GARMIN_CLIENT_ID, GARMIN_CLIENT_SECRET
|
|
65
63
|
* const soma = new Soma(components.soma);
|
|
66
64
|
*
|
|
67
|
-
* // Or with explicit
|
|
65
|
+
* // Or with explicit config:
|
|
68
66
|
* // const soma = new Soma(components.soma, {
|
|
69
67
|
* // strava: { clientId: "...", clientSecret: "..." },
|
|
68
|
+
* // garmin: { clientId: "...", clientSecret: "..." },
|
|
70
69
|
* // });
|
|
71
70
|
*
|
|
72
|
-
* // Connect a user to a provider:
|
|
73
|
-
* const connectionId = await soma.connect(ctx, {
|
|
74
|
-
* userId: "user_123",
|
|
75
|
-
* provider: "GARMIN",
|
|
76
|
-
* });
|
|
77
|
-
*
|
|
78
71
|
* // Start Strava OAuth (redirects user, callback handled by registerRoutes):
|
|
79
|
-
* const { authUrl } = await soma.
|
|
72
|
+
* const { authUrl } = await soma.strava.getAuthUrl(ctx, {
|
|
80
73
|
* userId: "user_123",
|
|
81
74
|
* redirectUri: "https://your-app.convex.site/api/strava/callback",
|
|
82
75
|
* });
|
|
76
|
+
*
|
|
77
|
+
* // Pull all Garmin data:
|
|
78
|
+
* await soma.garmin.pullAll(ctx, { userId: "user_123" });
|
|
83
79
|
* ```
|
|
84
80
|
*/
|
|
85
81
|
export class Soma {
|
|
86
82
|
private stravaConfig?: SomaStravaConfig;
|
|
87
83
|
private garminConfig?: SomaGarminConfig;
|
|
88
84
|
|
|
85
|
+
public readonly garmin: SomaGarmin;
|
|
86
|
+
public readonly strava: SomaStrava;
|
|
87
|
+
|
|
89
88
|
constructor(
|
|
90
89
|
public component: SomaComponent,
|
|
91
90
|
options?: { strava?: SomaStravaConfig; garmin?: SomaGarminConfig },
|
|
92
91
|
) {
|
|
93
92
|
this.stravaConfig = options?.strava ?? this.readStravaEnv();
|
|
94
93
|
this.garminConfig = options?.garmin ?? this.readGarminEnv();
|
|
94
|
+
|
|
95
|
+
this.garmin = new SomaGarmin(this.component, () => this.requireGarminConfig());
|
|
96
|
+
this.strava = new SomaStrava(this.component, () => this.requireStravaConfig());
|
|
95
97
|
}
|
|
96
98
|
|
|
97
99
|
/**
|
|
@@ -147,66 +149,6 @@ export class Soma {
|
|
|
147
149
|
return this.garminConfig;
|
|
148
150
|
}
|
|
149
151
|
|
|
150
|
-
// ─── Connect / Disconnect ───────────────────────────────────────────────────
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Connect a user to a wearable provider.
|
|
154
|
-
*
|
|
155
|
-
* Creates the connection if it doesn't exist, or re-activates it if it was
|
|
156
|
-
* previously disconnected. Idempotent — calling twice is a no-op.
|
|
157
|
-
*
|
|
158
|
-
* Use this when the host app has completed the provider's auth flow and
|
|
159
|
-
* wants to register the connection in Soma.
|
|
160
|
-
*
|
|
161
|
-
* @param ctx - Mutation context from the host app
|
|
162
|
-
* @param args.userId - The host app's user identifier (Clerk ID, etc.)
|
|
163
|
-
* @param args.provider - The wearable provider name ("GARMIN", "FITBIT", "OURA", etc.)
|
|
164
|
-
* @returns The connection document ID
|
|
165
|
-
*
|
|
166
|
-
* @example
|
|
167
|
-
* ```ts
|
|
168
|
-
* // "Connect to Garmin" button handler
|
|
169
|
-
* const connectionId = await soma.connect(ctx, {
|
|
170
|
-
* userId: "user_123",
|
|
171
|
-
* provider: "GARMIN",
|
|
172
|
-
* });
|
|
173
|
-
* ```
|
|
174
|
-
*/
|
|
175
|
-
async connect(
|
|
176
|
-
ctx: MutationCtx,
|
|
177
|
-
args: { userId: string; provider: string },
|
|
178
|
-
): Promise<string> {
|
|
179
|
-
return await ctx.runMutation(this.component.public.connect, args);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Disconnect a user from a wearable provider.
|
|
184
|
-
*
|
|
185
|
-
* Sets the connection to inactive. Does not delete any synced data,
|
|
186
|
-
* so re-connecting later preserves historical records.
|
|
187
|
-
*
|
|
188
|
-
* @param ctx - Mutation context from the host app
|
|
189
|
-
* @param args.userId - The host app's user identifier
|
|
190
|
-
* @param args.provider - The wearable provider name
|
|
191
|
-
*
|
|
192
|
-
* @throws Error if no connection exists for the given user–provider pair
|
|
193
|
-
*
|
|
194
|
-
* @example
|
|
195
|
-
* ```ts
|
|
196
|
-
* // "Disconnect Garmin" button handler
|
|
197
|
-
* await soma.disconnect(ctx, {
|
|
198
|
-
* userId: "user_123",
|
|
199
|
-
* provider: "GARMIN",
|
|
200
|
-
* });
|
|
201
|
-
* ```
|
|
202
|
-
*/
|
|
203
|
-
async disconnect(
|
|
204
|
-
ctx: MutationCtx,
|
|
205
|
-
args: { userId: string; provider: string },
|
|
206
|
-
): Promise<null> {
|
|
207
|
-
return await ctx.runMutation(this.component.public.disconnect, args);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
152
|
// ─── Connection Queries ─────────────────────────────────────────────────────
|
|
211
153
|
|
|
212
154
|
/**
|
|
@@ -812,7 +754,7 @@ export class Soma {
|
|
|
812
754
|
) {
|
|
813
755
|
return await ctx.runMutation(
|
|
814
756
|
this.component.public.deletePlannedWorkout,
|
|
815
|
-
args
|
|
757
|
+
args,
|
|
816
758
|
);
|
|
817
759
|
}
|
|
818
760
|
|
|
@@ -828,563 +770,10 @@ export class Soma {
|
|
|
828
770
|
) {
|
|
829
771
|
return await ctx.runQuery(
|
|
830
772
|
this.component.public.getPlannedWorkout,
|
|
831
|
-
args
|
|
773
|
+
args,
|
|
832
774
|
);
|
|
833
775
|
}
|
|
834
776
|
|
|
835
|
-
// ─── Strava Integration ──────────────────────────────────────────────────────
|
|
836
|
-
// High-level methods that handle OAuth, token storage, and data syncing
|
|
837
|
-
// for Strava. Requires Strava credentials to be configured either via
|
|
838
|
-
// environment variables or the constructor.
|
|
839
|
-
|
|
840
|
-
/**
|
|
841
|
-
* Generate a Strava OAuth authorization URL.
|
|
842
|
-
*
|
|
843
|
-
* The state parameter is stored inside the component automatically,
|
|
844
|
-
* and the callback handler registered by `registerRoutes` will
|
|
845
|
-
* complete the flow without further host-app intervention.
|
|
846
|
-
*
|
|
847
|
-
* @param ctx - Action context from the host app
|
|
848
|
-
* @param opts.userId - The host app's user identifier
|
|
849
|
-
* @param opts.redirectUri - The URL Strava will redirect to after authorization
|
|
850
|
-
* @param opts.scope - Comma-separated Strava OAuth scopes (default: "read,activity:read_all,profile:read_all")
|
|
851
|
-
* @returns `{ authUrl, state }`
|
|
852
|
-
*
|
|
853
|
-
* @example
|
|
854
|
-
* ```ts
|
|
855
|
-
* const { authUrl } = await soma.getStravaAuthUrl(ctx, {
|
|
856
|
-
* userId: "user_123",
|
|
857
|
-
* redirectUri: "https://your-app.convex.site/api/strava/callback",
|
|
858
|
-
* });
|
|
859
|
-
* // Redirect user to authUrl — the callback is handled automatically
|
|
860
|
-
* ```
|
|
861
|
-
*/
|
|
862
|
-
async getStravaAuthUrl(
|
|
863
|
-
ctx: ActionCtx,
|
|
864
|
-
opts: { userId: string; redirectUri: string; scope?: string },
|
|
865
|
-
) {
|
|
866
|
-
const config = this.requireStravaConfig();
|
|
867
|
-
return await ctx.runAction(this.component.strava.public.getStravaAuthUrl, {
|
|
868
|
-
clientId: config.clientId,
|
|
869
|
-
redirectUri: opts.redirectUri,
|
|
870
|
-
scope: opts.scope,
|
|
871
|
-
userId: opts.userId,
|
|
872
|
-
});
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
/**
|
|
877
|
-
* Sync activities from Strava for an already-connected user.
|
|
878
|
-
*
|
|
879
|
-
* Automatically refreshes the access token if expired. Fetches the
|
|
880
|
-
* athlete profile and activities, transforms them, and ingests into Soma.
|
|
881
|
-
*
|
|
882
|
-
* @param ctx - Action context from the host app
|
|
883
|
-
* @param args.userId - The host app's user identifier
|
|
884
|
-
* @param args.after - Only sync activities after this Unix epoch timestamp (for incremental sync)
|
|
885
|
-
* @returns `{ synced, errors }`
|
|
886
|
-
*
|
|
887
|
-
* @example
|
|
888
|
-
* ```ts
|
|
889
|
-
* export const syncStrava = action({
|
|
890
|
-
* args: { userId: v.string() },
|
|
891
|
-
* handler: async (ctx, { userId }) => {
|
|
892
|
-
* return await soma.syncStrava(ctx, { userId });
|
|
893
|
-
* },
|
|
894
|
-
* });
|
|
895
|
-
* ```
|
|
896
|
-
*/
|
|
897
|
-
async syncStrava(
|
|
898
|
-
ctx: ActionCtx,
|
|
899
|
-
args: { userId: string; after?: number },
|
|
900
|
-
) {
|
|
901
|
-
const config = this.requireStravaConfig();
|
|
902
|
-
return await ctx.runAction(this.component.strava.public.syncStrava, {
|
|
903
|
-
...args,
|
|
904
|
-
clientId: config.clientId,
|
|
905
|
-
clientSecret: config.clientSecret,
|
|
906
|
-
});
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
/**
|
|
910
|
-
* Disconnect a user from Strava.
|
|
911
|
-
*
|
|
912
|
-
* Revokes the token at Strava (best-effort), deletes stored tokens,
|
|
913
|
-
* and sets the connection to inactive.
|
|
914
|
-
*
|
|
915
|
-
* @param ctx - Action context from the host app
|
|
916
|
-
* @param args.userId - The host app's user identifier
|
|
917
|
-
*
|
|
918
|
-
* @example
|
|
919
|
-
* ```ts
|
|
920
|
-
* export const disconnectStrava = action({
|
|
921
|
-
* args: { userId: v.string() },
|
|
922
|
-
* handler: async (ctx, { userId }) => {
|
|
923
|
-
* await soma.disconnectStrava(ctx, { userId });
|
|
924
|
-
* },
|
|
925
|
-
* });
|
|
926
|
-
* ```
|
|
927
|
-
*/
|
|
928
|
-
async disconnectStrava(
|
|
929
|
-
ctx: ActionCtx,
|
|
930
|
-
args: { userId: string },
|
|
931
|
-
) {
|
|
932
|
-
const config = this.requireStravaConfig();
|
|
933
|
-
return await ctx.runAction(this.component.strava.public.disconnectStrava, {
|
|
934
|
-
...args,
|
|
935
|
-
clientId: config.clientId,
|
|
936
|
-
clientSecret: config.clientSecret,
|
|
937
|
-
});
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
// ─── Garmin ──────────────────────────────────────────────────────
|
|
941
|
-
|
|
942
|
-
async getGarminAuthUrl(
|
|
943
|
-
ctx: ActionCtx,
|
|
944
|
-
opts: { userId: string; redirectUri?: string },
|
|
945
|
-
) {
|
|
946
|
-
const config = this.requireGarminConfig();
|
|
947
|
-
return await ctx.runAction(this.component.garmin.public.getGarminAuthUrl, {
|
|
948
|
-
clientId: config.clientId,
|
|
949
|
-
redirectUri: opts.redirectUri,
|
|
950
|
-
userId: opts.userId,
|
|
951
|
-
});
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
async disconnectGarmin(
|
|
955
|
-
ctx: ActionCtx,
|
|
956
|
-
args: { userId: string },
|
|
957
|
-
) {
|
|
958
|
-
return await ctx.runAction(this.component.garmin.public.disconnectGarmin, args);
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
async pullGarminActivities(
|
|
962
|
-
ctx: ActionCtx,
|
|
963
|
-
args: {
|
|
964
|
-
userId: string;
|
|
965
|
-
startTimeInSeconds?: number;
|
|
966
|
-
endTimeInSeconds?: number;
|
|
967
|
-
},
|
|
968
|
-
) {
|
|
969
|
-
const config = this.requireGarminConfig();
|
|
970
|
-
return await ctx.runAction(this.component.garmin.public.pullActivities, {
|
|
971
|
-
...args,
|
|
972
|
-
clientId: config.clientId,
|
|
973
|
-
clientSecret: config.clientSecret,
|
|
974
|
-
});
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
async pullGarminDailies(
|
|
978
|
-
ctx: ActionCtx,
|
|
979
|
-
args: {
|
|
980
|
-
userId: string;
|
|
981
|
-
startTimeInSeconds?: number;
|
|
982
|
-
endTimeInSeconds?: number;
|
|
983
|
-
},
|
|
984
|
-
) {
|
|
985
|
-
const config = this.requireGarminConfig();
|
|
986
|
-
return await ctx.runAction(this.component.garmin.public.pullDailies, {
|
|
987
|
-
...args,
|
|
988
|
-
clientId: config.clientId,
|
|
989
|
-
clientSecret: config.clientSecret,
|
|
990
|
-
});
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
async pullGarminSleep(
|
|
994
|
-
ctx: ActionCtx,
|
|
995
|
-
args: {
|
|
996
|
-
userId: string;
|
|
997
|
-
startTimeInSeconds?: number;
|
|
998
|
-
endTimeInSeconds?: number;
|
|
999
|
-
},
|
|
1000
|
-
) {
|
|
1001
|
-
const config = this.requireGarminConfig();
|
|
1002
|
-
return await ctx.runAction(this.component.garmin.public.pullSleep, {
|
|
1003
|
-
...args,
|
|
1004
|
-
clientId: config.clientId,
|
|
1005
|
-
clientSecret: config.clientSecret,
|
|
1006
|
-
});
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
async pullGarminBody(
|
|
1010
|
-
ctx: ActionCtx,
|
|
1011
|
-
args: {
|
|
1012
|
-
userId: string;
|
|
1013
|
-
startTimeInSeconds?: number;
|
|
1014
|
-
endTimeInSeconds?: number;
|
|
1015
|
-
},
|
|
1016
|
-
) {
|
|
1017
|
-
const config = this.requireGarminConfig();
|
|
1018
|
-
return await ctx.runAction(this.component.garmin.public.pullBody, {
|
|
1019
|
-
...args,
|
|
1020
|
-
clientId: config.clientId,
|
|
1021
|
-
clientSecret: config.clientSecret,
|
|
1022
|
-
});
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
async pullGarminMenstruation(
|
|
1026
|
-
ctx: ActionCtx,
|
|
1027
|
-
args: {
|
|
1028
|
-
userId: string;
|
|
1029
|
-
startTimeInSeconds?: number;
|
|
1030
|
-
endTimeInSeconds?: number;
|
|
1031
|
-
},
|
|
1032
|
-
) {
|
|
1033
|
-
const config = this.requireGarminConfig();
|
|
1034
|
-
return await ctx.runAction(this.component.garmin.public.pullMenstruation, {
|
|
1035
|
-
...args,
|
|
1036
|
-
clientId: config.clientId,
|
|
1037
|
-
clientSecret: config.clientSecret,
|
|
1038
|
-
});
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
async pullGarminBloodPressures(
|
|
1042
|
-
ctx: ActionCtx,
|
|
1043
|
-
args: {
|
|
1044
|
-
userId: string;
|
|
1045
|
-
startTimeInSeconds?: number;
|
|
1046
|
-
endTimeInSeconds?: number;
|
|
1047
|
-
},
|
|
1048
|
-
) {
|
|
1049
|
-
const config = this.requireGarminConfig();
|
|
1050
|
-
return await ctx.runAction(this.component.garmin.public.pullBloodPressures, {
|
|
1051
|
-
...args,
|
|
1052
|
-
clientId: config.clientId,
|
|
1053
|
-
clientSecret: config.clientSecret,
|
|
1054
|
-
});
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
async pullGarminSkinTemperature(
|
|
1058
|
-
ctx: ActionCtx,
|
|
1059
|
-
args: {
|
|
1060
|
-
userId: string;
|
|
1061
|
-
startTimeInSeconds?: number;
|
|
1062
|
-
endTimeInSeconds?: number;
|
|
1063
|
-
},
|
|
1064
|
-
) {
|
|
1065
|
-
const config = this.requireGarminConfig();
|
|
1066
|
-
return await ctx.runAction(this.component.garmin.public.pullSkinTemperature, {
|
|
1067
|
-
...args,
|
|
1068
|
-
clientId: config.clientId,
|
|
1069
|
-
clientSecret: config.clientSecret,
|
|
1070
|
-
});
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
|
-
async pullGarminUserMetrics(
|
|
1074
|
-
ctx: ActionCtx,
|
|
1075
|
-
args: {
|
|
1076
|
-
userId: string;
|
|
1077
|
-
startTimeInSeconds?: number;
|
|
1078
|
-
endTimeInSeconds?: number;
|
|
1079
|
-
},
|
|
1080
|
-
) {
|
|
1081
|
-
const config = this.requireGarminConfig();
|
|
1082
|
-
return await ctx.runAction(this.component.garmin.public.pullUserMetrics, {
|
|
1083
|
-
...args,
|
|
1084
|
-
clientId: config.clientId,
|
|
1085
|
-
clientSecret: config.clientSecret,
|
|
1086
|
-
});
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
async pullGarminHRV(
|
|
1090
|
-
ctx: ActionCtx,
|
|
1091
|
-
args: {
|
|
1092
|
-
userId: string;
|
|
1093
|
-
startTimeInSeconds?: number;
|
|
1094
|
-
endTimeInSeconds?: number;
|
|
1095
|
-
},
|
|
1096
|
-
) {
|
|
1097
|
-
const config = this.requireGarminConfig();
|
|
1098
|
-
return await ctx.runAction(this.component.garmin.public.pullHRV, {
|
|
1099
|
-
...args,
|
|
1100
|
-
clientId: config.clientId,
|
|
1101
|
-
clientSecret: config.clientSecret,
|
|
1102
|
-
});
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
async pullGarminStressDetails(
|
|
1106
|
-
ctx: ActionCtx,
|
|
1107
|
-
args: {
|
|
1108
|
-
userId: string;
|
|
1109
|
-
startTimeInSeconds?: number;
|
|
1110
|
-
endTimeInSeconds?: number;
|
|
1111
|
-
},
|
|
1112
|
-
) {
|
|
1113
|
-
const config = this.requireGarminConfig();
|
|
1114
|
-
return await ctx.runAction(this.component.garmin.public.pullStressDetails, {
|
|
1115
|
-
...args,
|
|
1116
|
-
clientId: config.clientId,
|
|
1117
|
-
clientSecret: config.clientSecret,
|
|
1118
|
-
});
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
async pullGarminPulseOx(
|
|
1122
|
-
ctx: ActionCtx,
|
|
1123
|
-
args: {
|
|
1124
|
-
userId: string;
|
|
1125
|
-
startTimeInSeconds?: number;
|
|
1126
|
-
endTimeInSeconds?: number;
|
|
1127
|
-
},
|
|
1128
|
-
) {
|
|
1129
|
-
const config = this.requireGarminConfig();
|
|
1130
|
-
return await ctx.runAction(this.component.garmin.public.pullPulseOx, {
|
|
1131
|
-
...args,
|
|
1132
|
-
clientId: config.clientId,
|
|
1133
|
-
clientSecret: config.clientSecret,
|
|
1134
|
-
});
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
async pullGarminRespiration(
|
|
1138
|
-
ctx: ActionCtx,
|
|
1139
|
-
args: {
|
|
1140
|
-
userId: string;
|
|
1141
|
-
startTimeInSeconds?: number;
|
|
1142
|
-
endTimeInSeconds?: number;
|
|
1143
|
-
},
|
|
1144
|
-
) {
|
|
1145
|
-
const config = this.requireGarminConfig();
|
|
1146
|
-
return await ctx.runAction(this.component.garmin.public.pullRespiration, {
|
|
1147
|
-
...args,
|
|
1148
|
-
clientId: config.clientId,
|
|
1149
|
-
clientSecret: config.clientSecret,
|
|
1150
|
-
});
|
|
1151
|
-
}
|
|
1152
|
-
|
|
1153
|
-
async pullGarminAll(
|
|
1154
|
-
ctx: ActionCtx,
|
|
1155
|
-
args: {
|
|
1156
|
-
userId: string;
|
|
1157
|
-
startTimeInSeconds?: number;
|
|
1158
|
-
endTimeInSeconds?: number;
|
|
1159
|
-
},
|
|
1160
|
-
) {
|
|
1161
|
-
const config = this.requireGarminConfig();
|
|
1162
|
-
return await ctx.runAction(this.component.garmin.public.pullAll, {
|
|
1163
|
-
...args,
|
|
1164
|
-
clientId: config.clientId,
|
|
1165
|
-
clientSecret: config.clientSecret,
|
|
1166
|
-
});
|
|
1167
|
-
}
|
|
1168
|
-
|
|
1169
|
-
async pushPlannedWorkoutToGarmin(
|
|
1170
|
-
ctx: ActionCtx,
|
|
1171
|
-
args: {
|
|
1172
|
-
userId: string;
|
|
1173
|
-
plannedWorkoutId: string;
|
|
1174
|
-
workoutProvider?: string;
|
|
1175
|
-
},
|
|
1176
|
-
) {
|
|
1177
|
-
const config = this.requireGarminConfig();
|
|
1178
|
-
return await ctx.runAction(this.component.garmin.public.pushPlannedWorkout, {
|
|
1179
|
-
...args,
|
|
1180
|
-
clientId: config.clientId,
|
|
1181
|
-
clientSecret: config.clientSecret,
|
|
1182
|
-
});
|
|
1183
|
-
}
|
|
1184
|
-
|
|
1185
|
-
async pushWorkoutToGarmin(
|
|
1186
|
-
ctx: ActionCtx,
|
|
1187
|
-
args: {
|
|
1188
|
-
userId: string;
|
|
1189
|
-
plannedWorkoutId: string;
|
|
1190
|
-
workoutProvider?: string;
|
|
1191
|
-
},
|
|
1192
|
-
) {
|
|
1193
|
-
const config = this.requireGarminConfig();
|
|
1194
|
-
return await ctx.runAction(this.component.garmin.public.pushWorkout, {
|
|
1195
|
-
...args,
|
|
1196
|
-
clientId: config.clientId,
|
|
1197
|
-
clientSecret: config.clientSecret,
|
|
1198
|
-
});
|
|
1199
|
-
}
|
|
1200
|
-
|
|
1201
|
-
async pushScheduleToGarmin(
|
|
1202
|
-
ctx: ActionCtx,
|
|
1203
|
-
args: {
|
|
1204
|
-
userId: string;
|
|
1205
|
-
plannedWorkoutId: string;
|
|
1206
|
-
date?: string;
|
|
1207
|
-
},
|
|
1208
|
-
) {
|
|
1209
|
-
const config = this.requireGarminConfig();
|
|
1210
|
-
return await ctx.runAction(this.component.garmin.public.pushSchedule, {
|
|
1211
|
-
...args,
|
|
1212
|
-
clientId: config.clientId,
|
|
1213
|
-
clientSecret: config.clientSecret,
|
|
1214
|
-
});
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
async deleteWorkoutFromGarmin(
|
|
1218
|
-
ctx: ActionCtx,
|
|
1219
|
-
args: {
|
|
1220
|
-
userId: string;
|
|
1221
|
-
plannedWorkoutId: string;
|
|
1222
|
-
},
|
|
1223
|
-
) {
|
|
1224
|
-
const config = this.requireGarminConfig();
|
|
1225
|
-
return await ctx.runAction(this.component.garmin.public.deleteWorkout, {
|
|
1226
|
-
...args,
|
|
1227
|
-
clientId: config.clientId,
|
|
1228
|
-
clientSecret: config.clientSecret,
|
|
1229
|
-
});
|
|
1230
|
-
}
|
|
1231
|
-
|
|
1232
|
-
async deleteScheduleFromGarmin(
|
|
1233
|
-
ctx: ActionCtx,
|
|
1234
|
-
args: {
|
|
1235
|
-
userId: string;
|
|
1236
|
-
plannedWorkoutId: string;
|
|
1237
|
-
},
|
|
1238
|
-
) {
|
|
1239
|
-
const config = this.requireGarminConfig();
|
|
1240
|
-
return await ctx.runAction(this.component.garmin.public.deleteSchedule, {
|
|
1241
|
-
...args,
|
|
1242
|
-
clientId: config.clientId,
|
|
1243
|
-
clientSecret: config.clientSecret,
|
|
1244
|
-
});
|
|
1245
|
-
}
|
|
1246
|
-
}
|
|
1247
|
-
|
|
1248
|
-
// ─── Shared Types ────────────────────────────────────────────────────────────
|
|
1249
|
-
|
|
1250
|
-
/**
|
|
1251
|
-
* Common args shape for all ingestion methods.
|
|
1252
|
-
*
|
|
1253
|
-
* Requires `connectionId` and `userId` at minimum — additional fields
|
|
1254
|
-
* come from the transformer output (e.g., `metadata`, `calories_data`, etc.)
|
|
1255
|
-
* and are validated server-side by Convex validators.
|
|
1256
|
-
*/
|
|
1257
|
-
type IngestArgs = {
|
|
1258
|
-
connectionId: string;
|
|
1259
|
-
userId: string;
|
|
1260
|
-
} & Record<string, unknown>;
|
|
1261
|
-
|
|
1262
|
-
/**
|
|
1263
|
-
* Base args for time-range filtered queries.
|
|
1264
|
-
*
|
|
1265
|
-
* - `userId` is required for all health data queries.
|
|
1266
|
-
* - `startTime` / `endTime` are optional ISO-8601 bounds on `metadata.start_time`.
|
|
1267
|
-
*/
|
|
1268
|
-
type TimeRangeArgs = {
|
|
1269
|
-
userId: string;
|
|
1270
|
-
startTime?: string;
|
|
1271
|
-
endTime?: string;
|
|
1272
|
-
};
|
|
1273
|
-
|
|
1274
|
-
/**
|
|
1275
|
-
* Args for list (collect-all) queries with optional ordering and limit.
|
|
1276
|
-
*/
|
|
1277
|
-
type ListTimeRangeArgs = TimeRangeArgs & {
|
|
1278
|
-
order?: "asc" | "desc";
|
|
1279
|
-
limit?: number;
|
|
1280
|
-
};
|
|
1281
|
-
|
|
1282
|
-
/**
|
|
1283
|
-
* Args for paginated queries with Convex pagination options.
|
|
1284
|
-
*/
|
|
1285
|
-
type PaginateTimeRangeArgs = TimeRangeArgs & {
|
|
1286
|
-
paginationOpts: { numItems: number; cursor: string | null };
|
|
1287
|
-
};
|
|
1288
|
-
|
|
1289
|
-
// ─── Route Registration ──────────────────────────────────────────────────────
|
|
1290
|
-
|
|
1291
|
-
/**
|
|
1292
|
-
* Per-provider options for `registerRoutes`.
|
|
1293
|
-
*/
|
|
1294
|
-
export interface StravaOAuthOptions {
|
|
1295
|
-
/** HTTP path for the OAuth callback. @default "/api/strava/callback" */
|
|
1296
|
-
path?: string;
|
|
1297
|
-
/** Override STRAVA_CLIENT_ID env var. */
|
|
1298
|
-
clientId?: string;
|
|
1299
|
-
/** Override STRAVA_CLIENT_SECRET env var. */
|
|
1300
|
-
clientSecret?: string;
|
|
1301
|
-
/** URL to redirect the user to after a successful connection. */
|
|
1302
|
-
redirectTo?: string;
|
|
1303
|
-
/** Called after Strava OAuth completes and the connection is established. */
|
|
1304
|
-
onComplete?: (
|
|
1305
|
-
ctx: GenericActionCtx<GenericDataModel>,
|
|
1306
|
-
event: StravaConnectEvent,
|
|
1307
|
-
) => Promise<void>;
|
|
1308
|
-
}
|
|
1309
|
-
|
|
1310
|
-
// ─── Strava Callback Event Types ────────────────────────────────────────────
|
|
1311
|
-
|
|
1312
|
-
/** Data passed to `onComplete` after Strava OAuth completes. */
|
|
1313
|
-
export interface StravaConnectEvent {
|
|
1314
|
-
provider: "STRAVA";
|
|
1315
|
-
userId: string;
|
|
1316
|
-
connectionId: string;
|
|
1317
|
-
}
|
|
1318
|
-
|
|
1319
|
-
// ─── Garmin Callback Event Types ─────────────────────────────────────────────
|
|
1320
|
-
|
|
1321
|
-
/** Data passed to `oauth.onComplete` after Garmin OAuth completes. */
|
|
1322
|
-
export interface GarminConnectEvent {
|
|
1323
|
-
provider: "GARMIN";
|
|
1324
|
-
userId: string;
|
|
1325
|
-
connectionId: string;
|
|
1326
|
-
}
|
|
1327
|
-
|
|
1328
|
-
/** Data passed to webhook `events` handlers and `onEvent` after data ingestion. */
|
|
1329
|
-
export interface GarminWebhookEvent {
|
|
1330
|
-
dataType: string;
|
|
1331
|
-
processed: number;
|
|
1332
|
-
errors: Array<{ type: string; id: string; error: string }>;
|
|
1333
|
-
/** Users whose data was affected by this webhook. */
|
|
1334
|
-
affectedUsers: Array<{ userId: string; connectionId: string }>;
|
|
1335
|
-
}
|
|
1336
|
-
|
|
1337
|
-
// ─── Garmin Route Options ───────────────────────────────────────────────────
|
|
1338
|
-
|
|
1339
|
-
export interface GarminOAuthOptions {
|
|
1340
|
-
/** HTTP path for the OAuth callback. @default "/api/garmin/callback" */
|
|
1341
|
-
path?: string;
|
|
1342
|
-
/** Override GARMIN_CLIENT_ID env var. */
|
|
1343
|
-
clientId?: string;
|
|
1344
|
-
/** Override GARMIN_CLIENT_SECRET env var. */
|
|
1345
|
-
clientSecret?: string;
|
|
1346
|
-
/** URL to redirect the user to after a successful connection. */
|
|
1347
|
-
redirectTo?: string;
|
|
1348
|
-
/** Called after Garmin OAuth completes and the connection is established. */
|
|
1349
|
-
onComplete?: (
|
|
1350
|
-
ctx: GenericActionCtx<GenericDataModel>,
|
|
1351
|
-
event: GarminConnectEvent,
|
|
1352
|
-
) => Promise<void>;
|
|
1353
|
-
}
|
|
1354
|
-
|
|
1355
|
-
/** Webhook endpoint names matching the Garmin API data types. */
|
|
1356
|
-
export type GarminWebhookEventName =
|
|
1357
|
-
| "activities" | "activity-details" | "manually-updated-activities" | "move-iq"
|
|
1358
|
-
| "blood-pressures" | "body-compositions" | "dailies" | "epochs"
|
|
1359
|
-
| "health-snapshot" | "sleeps" | "hrv" | "stress" | "pulse-ox"
|
|
1360
|
-
| "respiration" | "skin-temp" | "user-metrics" | "menstrual-cycle-tracking";
|
|
1361
|
-
|
|
1362
|
-
/** Handler for a specific webhook event or the catch-all `onEvent`. */
|
|
1363
|
-
export type GarminWebhookHandler = (
|
|
1364
|
-
ctx: GenericActionCtx<GenericDataModel>,
|
|
1365
|
-
event: GarminWebhookEvent,
|
|
1366
|
-
) => Promise<void>;
|
|
1367
|
-
|
|
1368
|
-
export interface GarminWebhookOptions {
|
|
1369
|
-
/** Base path prefix for all webhook routes. @default "/api/garmin/webhook" */
|
|
1370
|
-
basePath?: string;
|
|
1371
|
-
/** Called after every webhook payload is processed, regardless of data type. */
|
|
1372
|
-
onEvent?: GarminWebhookHandler;
|
|
1373
|
-
/** Per-data-type handlers. Only subscribe to the types you care about. */
|
|
1374
|
-
events?: Partial<Record<GarminWebhookEventName, GarminWebhookHandler>>;
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
export interface RegisterRoutesOptions {
|
|
1378
|
-
strava?: {
|
|
1379
|
-
/** OAuth callback configuration. */
|
|
1380
|
-
oauth?: StravaOAuthOptions;
|
|
1381
|
-
};
|
|
1382
|
-
garmin?: {
|
|
1383
|
-
/** OAuth callback configuration. */
|
|
1384
|
-
oauth?: GarminOAuthOptions;
|
|
1385
|
-
/** Webhook route configuration. Set to `false` to skip webhook registration. */
|
|
1386
|
-
webhook?: GarminWebhookOptions | false;
|
|
1387
|
-
};
|
|
1388
777
|
}
|
|
1389
778
|
|
|
1390
779
|
/**
|
|
@@ -1444,14 +833,18 @@ export interface RegisterRoutesOptions {
|
|
|
1444
833
|
* },
|
|
1445
834
|
* },
|
|
1446
835
|
* webhook: {
|
|
1447
|
-
*
|
|
1448
|
-
*
|
|
1449
|
-
* console.log(`Garmin ${event.dataType}: ${event.processed} processed`);
|
|
1450
|
-
* },
|
|
836
|
+
* // Only listed data types get an HTTP route registered.
|
|
837
|
+
* // Pass a handler for custom logic, or `true` for default processing.
|
|
1451
838
|
* events: {
|
|
1452
839
|
* "activities": async (ctx, event) => {
|
|
1453
|
-
* //
|
|
840
|
+
* // Custom side-effect after activity ingestion
|
|
1454
841
|
* },
|
|
842
|
+
* "sleeps": true, // register route, default processing only
|
|
843
|
+
* "dailies": true,
|
|
844
|
+
* },
|
|
845
|
+
* // Optional catch-all, runs for every registered event
|
|
846
|
+
* onEvent: async (ctx, event) => {
|
|
847
|
+
* console.log(`Garmin ${event.dataType}: ${event.items.length} items`);
|
|
1455
848
|
* },
|
|
1456
849
|
* },
|
|
1457
850
|
* },
|
|
@@ -1466,268 +859,10 @@ export function registerRoutes(
|
|
|
1466
859
|
const registerAll = opts === undefined;
|
|
1467
860
|
|
|
1468
861
|
if (registerAll || opts?.strava) {
|
|
1469
|
-
|
|
1470
|
-
const oauth = stravaOpts.oauth ?? {};
|
|
1471
|
-
const path = oauth.path ?? STRAVA_CALLBACK_PATH;
|
|
1472
|
-
|
|
1473
|
-
http.route({
|
|
1474
|
-
path,
|
|
1475
|
-
method: "GET",
|
|
1476
|
-
handler: httpActionGeneric(async (ctx, request) => {
|
|
1477
|
-
const url = new URL(request.url);
|
|
1478
|
-
const code = url.searchParams.get("code");
|
|
1479
|
-
const state = url.searchParams.get("state");
|
|
1480
|
-
|
|
1481
|
-
if (!code) {
|
|
1482
|
-
return new Response("Missing authorization code", { status: 400 });
|
|
1483
|
-
}
|
|
1484
|
-
if (!state) {
|
|
1485
|
-
return new Response(
|
|
1486
|
-
"Missing state parameter. Ensure the state was included " +
|
|
1487
|
-
"when building the Strava auth URL via getStravaAuthUrl.",
|
|
1488
|
-
{ status: 400 },
|
|
1489
|
-
);
|
|
1490
|
-
}
|
|
1491
|
-
|
|
1492
|
-
const clientId =
|
|
1493
|
-
oauth.clientId ?? process.env.STRAVA_CLIENT_ID;
|
|
1494
|
-
const clientSecret =
|
|
1495
|
-
oauth.clientSecret ?? process.env.STRAVA_CLIENT_SECRET;
|
|
1496
|
-
|
|
1497
|
-
if (!clientId || !clientSecret) {
|
|
1498
|
-
return new Response(
|
|
1499
|
-
"Strava credentials not configured. Set STRAVA_CLIENT_ID and " +
|
|
1500
|
-
"STRAVA_CLIENT_SECRET environment variables, or pass them to registerRoutes.",
|
|
1501
|
-
{ status: 500 },
|
|
1502
|
-
);
|
|
1503
|
-
}
|
|
1504
|
-
|
|
1505
|
-
let result: {
|
|
1506
|
-
connectionId: string;
|
|
1507
|
-
userId: string;
|
|
1508
|
-
};
|
|
1509
|
-
try {
|
|
1510
|
-
result = await ctx.runAction(component.strava.public.completeStravaOAuth, {
|
|
1511
|
-
code,
|
|
1512
|
-
state,
|
|
1513
|
-
clientId,
|
|
1514
|
-
clientSecret,
|
|
1515
|
-
});
|
|
1516
|
-
} catch (error) {
|
|
1517
|
-
const message =
|
|
1518
|
-
error instanceof Error ? error.message : "Unknown error";
|
|
1519
|
-
return new Response(`Strava OAuth callback failed: ${message}`, {
|
|
1520
|
-
status: 500,
|
|
1521
|
-
});
|
|
1522
|
-
}
|
|
1523
|
-
|
|
1524
|
-
if (oauth.onComplete) {
|
|
1525
|
-
try {
|
|
1526
|
-
await oauth.onComplete(ctx, {
|
|
1527
|
-
provider: "STRAVA",
|
|
1528
|
-
userId: result.userId,
|
|
1529
|
-
connectionId: result.connectionId,
|
|
1530
|
-
});
|
|
1531
|
-
} catch (callbackError) {
|
|
1532
|
-
console.error(
|
|
1533
|
-
"[soma] strava onComplete callback error:",
|
|
1534
|
-
callbackError instanceof Error ? callbackError.message : callbackError,
|
|
1535
|
-
);
|
|
1536
|
-
}
|
|
1537
|
-
}
|
|
1538
|
-
|
|
1539
|
-
if (oauth.redirectTo) {
|
|
1540
|
-
return new Response(null, {
|
|
1541
|
-
status: 302,
|
|
1542
|
-
headers: { Location: oauth.redirectTo },
|
|
1543
|
-
});
|
|
1544
|
-
}
|
|
1545
|
-
|
|
1546
|
-
return new Response("Successfully connected to Strava!", {
|
|
1547
|
-
status: 200,
|
|
1548
|
-
});
|
|
1549
|
-
}),
|
|
1550
|
-
});
|
|
862
|
+
SomaStrava.registerRoutes(http, component, opts?.strava);
|
|
1551
863
|
}
|
|
1552
864
|
|
|
1553
865
|
if (registerAll || opts?.garmin) {
|
|
1554
|
-
|
|
1555
|
-
const oauth = garminOpts.oauth ?? {};
|
|
1556
|
-
const path = oauth.path ?? GARMIN_OAUTH_CALLBACK_PATH;
|
|
1557
|
-
|
|
1558
|
-
http.route({
|
|
1559
|
-
path,
|
|
1560
|
-
method: "GET",
|
|
1561
|
-
handler: httpActionGeneric(async (ctx, request) => {
|
|
1562
|
-
const url = new URL(request.url);
|
|
1563
|
-
const code = url.searchParams.get("code");
|
|
1564
|
-
const state = url.searchParams.get("state");
|
|
1565
|
-
|
|
1566
|
-
if (!code) {
|
|
1567
|
-
return new Response("Missing authorization code", {
|
|
1568
|
-
status: 400,
|
|
1569
|
-
});
|
|
1570
|
-
}
|
|
1571
|
-
if (!state) {
|
|
1572
|
-
return new Response(
|
|
1573
|
-
"Missing state parameter. Ensure the state was included " +
|
|
1574
|
-
"when building the Garmin auth URL.",
|
|
1575
|
-
{ status: 400 },
|
|
1576
|
-
);
|
|
1577
|
-
}
|
|
1578
|
-
|
|
1579
|
-
const clientId =
|
|
1580
|
-
oauth.clientId ?? process.env.GARMIN_CLIENT_ID;
|
|
1581
|
-
const clientSecret =
|
|
1582
|
-
oauth.clientSecret ?? process.env.GARMIN_CLIENT_SECRET;
|
|
1583
|
-
|
|
1584
|
-
if (!clientId || !clientSecret) {
|
|
1585
|
-
return new Response(
|
|
1586
|
-
"Garmin credentials not configured. Set GARMIN_CLIENT_ID and " +
|
|
1587
|
-
"GARMIN_CLIENT_SECRET environment variables, or pass them to registerRoutes.",
|
|
1588
|
-
{ status: 500 },
|
|
1589
|
-
);
|
|
1590
|
-
}
|
|
1591
|
-
|
|
1592
|
-
let result: {
|
|
1593
|
-
connectionId: string;
|
|
1594
|
-
userId: string;
|
|
1595
|
-
};
|
|
1596
|
-
try {
|
|
1597
|
-
result = await ctx.runAction(component.garmin.public.completeGarminOAuth, {
|
|
1598
|
-
code,
|
|
1599
|
-
state,
|
|
1600
|
-
clientId,
|
|
1601
|
-
clientSecret,
|
|
1602
|
-
}) as typeof result;
|
|
1603
|
-
} catch (error) {
|
|
1604
|
-
const message =
|
|
1605
|
-
error instanceof Error ? error.message : "Unknown error";
|
|
1606
|
-
return new Response(`Garmin OAuth callback failed: ${message}`, {
|
|
1607
|
-
status: 500,
|
|
1608
|
-
});
|
|
1609
|
-
}
|
|
1610
|
-
|
|
1611
|
-
if (oauth.onComplete) {
|
|
1612
|
-
try {
|
|
1613
|
-
await oauth.onComplete(ctx, {
|
|
1614
|
-
provider: "GARMIN",
|
|
1615
|
-
userId: result.userId,
|
|
1616
|
-
connectionId: result.connectionId,
|
|
1617
|
-
});
|
|
1618
|
-
} catch (callbackError) {
|
|
1619
|
-
console.error(
|
|
1620
|
-
"[soma] garmin oauth.onComplete callback error:",
|
|
1621
|
-
callbackError instanceof Error ? callbackError.message : callbackError,
|
|
1622
|
-
);
|
|
1623
|
-
}
|
|
1624
|
-
}
|
|
1625
|
-
|
|
1626
|
-
if (oauth.redirectTo) {
|
|
1627
|
-
return new Response(null, {
|
|
1628
|
-
status: 302,
|
|
1629
|
-
headers: { Location: oauth.redirectTo },
|
|
1630
|
-
});
|
|
1631
|
-
}
|
|
1632
|
-
|
|
1633
|
-
return new Response("Successfully connected to Garmin!", {
|
|
1634
|
-
status: 200,
|
|
1635
|
-
});
|
|
1636
|
-
}),
|
|
1637
|
-
});
|
|
1638
|
-
|
|
1639
|
-
// ── Garmin Webhook Routes ──────────────────────────────────
|
|
1640
|
-
if (garminOpts.webhook !== false) {
|
|
1641
|
-
const webhookCfg = typeof garminOpts.webhook === "object" ? garminOpts.webhook : {};
|
|
1642
|
-
const webhookBase = webhookCfg.basePath ?? GARMIN_WEBHOOK_BASE_PATH;
|
|
1643
|
-
|
|
1644
|
-
const webhookRoutes: Array<{
|
|
1645
|
-
suffix: string;
|
|
1646
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1647
|
-
action: FunctionReference<"action", "internal", { payload: any }>;
|
|
1648
|
-
}> = [
|
|
1649
|
-
// ACTIVITY category
|
|
1650
|
-
{ suffix: "/activities", action: component.garmin.webhooks.handleGarminWebhookActivities },
|
|
1651
|
-
{ suffix: "/activity-details", action: component.garmin.webhooks.handleGarminWebhookActivityDetails },
|
|
1652
|
-
{ suffix: "/manually-updated-activities", action: component.garmin.webhooks.handleGarminWebhookManuallyUpdatedActivities },
|
|
1653
|
-
{ suffix: "/move-iq", action: component.garmin.webhooks.handleGarminWebhookMoveIQ },
|
|
1654
|
-
// HEALTH category
|
|
1655
|
-
{ suffix: "/blood-pressures", action: component.garmin.webhooks.handleGarminWebhookBloodPressures },
|
|
1656
|
-
{ suffix: "/body-compositions", action: component.garmin.webhooks.handleGarminWebhookBodyCompositions },
|
|
1657
|
-
{ suffix: "/dailies", action: component.garmin.webhooks.handleGarminWebhookDailies },
|
|
1658
|
-
{ suffix: "/epochs", action: component.garmin.webhooks.handleGarminWebhookEpochs },
|
|
1659
|
-
{ suffix: "/health-snapshot", action: component.garmin.webhooks.handleGarminWebhookHealthSnapshot },
|
|
1660
|
-
{ suffix: "/sleeps", action: component.garmin.webhooks.handleGarminWebhookSleeps },
|
|
1661
|
-
{ suffix: "/hrv", action: component.garmin.webhooks.handleGarminWebhookHRVSummary },
|
|
1662
|
-
{ suffix: "/stress", action: component.garmin.webhooks.handleGarminWebhookStress },
|
|
1663
|
-
{ suffix: "/pulse-ox", action: component.garmin.webhooks.handleGarminWebhookPulseOx },
|
|
1664
|
-
{ suffix: "/respiration", action: component.garmin.webhooks.handleGarminWebhookRespiration },
|
|
1665
|
-
{ suffix: "/skin-temp", action: component.garmin.webhooks.handleGarminWebhookSkinTemp },
|
|
1666
|
-
{ suffix: "/user-metrics", action: component.garmin.webhooks.handleGarminWebhookUserMetrics },
|
|
1667
|
-
// WOMEN_HEALTH category
|
|
1668
|
-
{ suffix: "/menstrual-cycle-tracking", action: component.garmin.webhooks.handleGarminWebhookMenstrualCycleTracking },
|
|
1669
|
-
];
|
|
1670
|
-
|
|
1671
|
-
for (const route of webhookRoutes) {
|
|
1672
|
-
http.route({
|
|
1673
|
-
path: `${webhookBase}${route.suffix}`,
|
|
1674
|
-
method: "POST",
|
|
1675
|
-
handler: httpActionGeneric(async (ctx, request) => {
|
|
1676
|
-
let payload: unknown;
|
|
1677
|
-
try {
|
|
1678
|
-
payload = await request.json();
|
|
1679
|
-
} catch {
|
|
1680
|
-
return new Response("Invalid JSON body", { status: 400 });
|
|
1681
|
-
}
|
|
1682
|
-
|
|
1683
|
-
let result: { processed: number; errors: Array<{ type: string; id: string; error: string }>; affectedUsers: Array<{ userId: string; connectionId: string }> } | undefined;
|
|
1684
|
-
try {
|
|
1685
|
-
result = await ctx.runAction(route.action, { payload }) as typeof result;
|
|
1686
|
-
} catch (error) {
|
|
1687
|
-
// Log but return 200 to prevent Garmin from retrying
|
|
1688
|
-
console.error(
|
|
1689
|
-
`Garmin webhook error (${route.suffix}):`,
|
|
1690
|
-
error instanceof Error ? error.message : error,
|
|
1691
|
-
);
|
|
1692
|
-
}
|
|
1693
|
-
|
|
1694
|
-
if (result) {
|
|
1695
|
-
const dataType = route.suffix.slice(1) as GarminWebhookEventName;
|
|
1696
|
-
const event: GarminWebhookEvent = {
|
|
1697
|
-
dataType,
|
|
1698
|
-
processed: result.processed,
|
|
1699
|
-
errors: result.errors,
|
|
1700
|
-
affectedUsers: result.affectedUsers,
|
|
1701
|
-
};
|
|
1702
|
-
|
|
1703
|
-
const specificHandler = webhookCfg.events?.[dataType];
|
|
1704
|
-
if (specificHandler) {
|
|
1705
|
-
try {
|
|
1706
|
-
await specificHandler(ctx, event);
|
|
1707
|
-
} catch (callbackError) {
|
|
1708
|
-
console.error(
|
|
1709
|
-
`[soma] garmin webhook events["${dataType}"] callback error:`,
|
|
1710
|
-
callbackError instanceof Error ? callbackError.message : callbackError,
|
|
1711
|
-
);
|
|
1712
|
-
}
|
|
1713
|
-
}
|
|
1714
|
-
|
|
1715
|
-
if (webhookCfg.onEvent) {
|
|
1716
|
-
try {
|
|
1717
|
-
await webhookCfg.onEvent(ctx, event);
|
|
1718
|
-
} catch (callbackError) {
|
|
1719
|
-
console.error(
|
|
1720
|
-
`[soma] garmin webhook onEvent callback error:`,
|
|
1721
|
-
callbackError instanceof Error ? callbackError.message : callbackError,
|
|
1722
|
-
);
|
|
1723
|
-
}
|
|
1724
|
-
}
|
|
1725
|
-
}
|
|
1726
|
-
|
|
1727
|
-
return new Response("OK", { status: 200 });
|
|
1728
|
-
}),
|
|
1729
|
-
});
|
|
1730
|
-
}
|
|
1731
|
-
}
|
|
866
|
+
SomaGarmin.registerRoutes(http, component, opts?.garmin);
|
|
1732
867
|
}
|
|
1733
868
|
}
|