@nativesquare/soma 0.5.0 → 0.7.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/index.d.ts +151 -53
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +162 -69
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/component.d.ts +130 -17
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/garmin.d.ts +61 -43
- package/dist/component/garmin.d.ts.map +1 -1
- package/dist/component/garmin.js +208 -122
- package/dist/component/garmin.js.map +1 -1
- package/dist/component/public.d.ts +363 -0
- package/dist/component/public.d.ts.map +1 -1
- package/dist/component/public.js +124 -0
- package/dist/component/public.js.map +1 -1
- package/dist/component/schema.d.ts +7 -9
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +9 -10
- package/dist/component/schema.js.map +1 -1
- package/dist/component/strava.d.ts +0 -1
- package/dist/component/strava.d.ts.map +1 -1
- package/dist/component/strava.js +0 -1
- package/dist/component/strava.js.map +1 -1
- package/dist/component/validators/enums.d.ts +1 -1
- package/dist/garmin/auth.d.ts +55 -46
- package/dist/garmin/auth.d.ts.map +1 -1
- package/dist/garmin/auth.js +82 -122
- package/dist/garmin/auth.js.map +1 -1
- package/dist/garmin/client.d.ts +64 -17
- package/dist/garmin/client.d.ts.map +1 -1
- package/dist/garmin/client.js +143 -29
- package/dist/garmin/client.js.map +1 -1
- package/dist/garmin/index.d.ts +3 -3
- package/dist/garmin/index.d.ts.map +1 -1
- package/dist/garmin/index.js +4 -4
- package/dist/garmin/index.js.map +1 -1
- package/dist/garmin/plannedWorkout.d.ts +12 -0
- package/dist/garmin/plannedWorkout.d.ts.map +1 -0
- package/dist/garmin/plannedWorkout.js +267 -0
- package/dist/garmin/plannedWorkout.js.map +1 -0
- package/dist/garmin/types.d.ts +78 -6
- package/dist/garmin/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/client/index.ts +236 -85
- package/src/component/_generated/component.ts +155 -17
- package/src/component/garmin.ts +258 -124
- package/src/component/public.ts +135 -0
- package/src/component/schema.ts +9 -10
- package/src/component/strava.ts +0 -1
- package/src/garmin/auth.test.ts +71 -96
- package/src/garmin/auth.ts +129 -193
- package/src/garmin/client.ts +197 -51
- package/src/garmin/index.ts +13 -14
- package/src/garmin/plannedWorkout.ts +333 -0
- package/src/garmin/types.ts +149 -7
package/src/client/index.ts
CHANGED
|
@@ -35,14 +35,14 @@ export interface SomaStravaConfig {
|
|
|
35
35
|
* Configuration for the Garmin integration.
|
|
36
36
|
*
|
|
37
37
|
* If not provided to the constructor, the Soma class will attempt to
|
|
38
|
-
* read `
|
|
38
|
+
* read `GARMIN_CLIENT_ID` and `GARMIN_CLIENT_SECRET` from
|
|
39
39
|
* environment variables automatically.
|
|
40
40
|
*/
|
|
41
41
|
export interface SomaGarminConfig {
|
|
42
|
-
/** Your Garmin application's
|
|
43
|
-
|
|
44
|
-
/** Your Garmin application's
|
|
45
|
-
|
|
42
|
+
/** Your Garmin application's Client ID. */
|
|
43
|
+
clientId: string;
|
|
44
|
+
/** Your Garmin application's Client Secret. */
|
|
45
|
+
clientSecret: string;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
/**
|
|
@@ -125,10 +125,10 @@ export class Soma {
|
|
|
125
125
|
* Returns undefined if the required vars are not set.
|
|
126
126
|
*/
|
|
127
127
|
private readGarminEnv(): SomaGarminConfig | undefined {
|
|
128
|
-
const
|
|
129
|
-
const
|
|
130
|
-
if (!
|
|
131
|
-
return {
|
|
128
|
+
const clientId = process.env.GARMIN_CLIENT_ID;
|
|
129
|
+
const clientSecret = process.env.GARMIN_CLIENT_SECRET;
|
|
130
|
+
if (!clientId || !clientSecret) return undefined;
|
|
131
|
+
return { clientId, clientSecret };
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
/**
|
|
@@ -137,9 +137,9 @@ export class Soma {
|
|
|
137
137
|
private requireGarminConfig(): SomaGarminConfig {
|
|
138
138
|
if (!this.garminConfig) {
|
|
139
139
|
throw new Error(
|
|
140
|
-
"Garmin is not configured. Either set
|
|
141
|
-
"
|
|
142
|
-
"or pass { garmin: {
|
|
140
|
+
"Garmin is not configured. Either set GARMIN_CLIENT_ID and " +
|
|
141
|
+
"GARMIN_CLIENT_SECRET environment variables in the Convex dashboard, " +
|
|
142
|
+
"or pass { garmin: { clientId, clientSecret } } to the Soma constructor.",
|
|
143
143
|
);
|
|
144
144
|
}
|
|
145
145
|
return this.garminConfig;
|
|
@@ -725,6 +725,153 @@ export class Soma {
|
|
|
725
725
|
return await ctx.runQuery(this.component.public.getAthlete, args);
|
|
726
726
|
}
|
|
727
727
|
|
|
728
|
+
// ── Planned Workouts ────────────────────────────────────────────────────────
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* Ingest a planned workout record.
|
|
732
|
+
*
|
|
733
|
+
* Upserts by `connectionId + metadata.id` when an id is present.
|
|
734
|
+
*
|
|
735
|
+
* @param ctx - Mutation context from the host app
|
|
736
|
+
* @param args - Planned workout data including connectionId, userId, metadata, and steps
|
|
737
|
+
* @returns The planned workout document ID
|
|
738
|
+
*/
|
|
739
|
+
async ingestPlannedWorkout(
|
|
740
|
+
ctx: MutationCtx,
|
|
741
|
+
args: IngestArgs,
|
|
742
|
+
): Promise<string> {
|
|
743
|
+
return await ctx.runMutation(
|
|
744
|
+
this.component.public.ingestPlannedWorkout,
|
|
745
|
+
args as never,
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* List planned workout records for a user, optionally filtered by planned date range.
|
|
751
|
+
*
|
|
752
|
+
* @param ctx - Query context from the host app
|
|
753
|
+
* @param args.userId - The host app's user identifier
|
|
754
|
+
* @param args.startDate - Optional lower bound (inclusive) on metadata.planned_date (YYYY-MM-DD)
|
|
755
|
+
* @param args.endDate - Optional upper bound (inclusive) on metadata.planned_date (YYYY-MM-DD)
|
|
756
|
+
* @param args.order - Sort order: "asc" or "desc" (default: "desc")
|
|
757
|
+
* @param args.limit - Optional max number of results to return
|
|
758
|
+
*/
|
|
759
|
+
async listPlannedWorkouts(
|
|
760
|
+
ctx: QueryCtx,
|
|
761
|
+
args: {
|
|
762
|
+
userId: string;
|
|
763
|
+
startDate?: string;
|
|
764
|
+
endDate?: string;
|
|
765
|
+
order?: "asc" | "desc";
|
|
766
|
+
limit?: number;
|
|
767
|
+
},
|
|
768
|
+
) {
|
|
769
|
+
return await ctx.runQuery(
|
|
770
|
+
this.component.public.listPlannedWorkouts,
|
|
771
|
+
args,
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* Paginate planned workout records for a user, optionally filtered by planned date range.
|
|
777
|
+
*
|
|
778
|
+
* Returns `{ page, isDone, continueCursor }` for cursor-based pagination.
|
|
779
|
+
*
|
|
780
|
+
* @param ctx - Query context from the host app
|
|
781
|
+
* @param args.userId - The host app's user identifier
|
|
782
|
+
* @param args.startDate - Optional lower bound (inclusive) on metadata.planned_date
|
|
783
|
+
* @param args.endDate - Optional upper bound (inclusive) on metadata.planned_date
|
|
784
|
+
* @param args.paginationOpts - Convex pagination options `{ numItems, cursor }`
|
|
785
|
+
*/
|
|
786
|
+
async paginatePlannedWorkouts(
|
|
787
|
+
ctx: QueryCtx,
|
|
788
|
+
args: {
|
|
789
|
+
userId: string;
|
|
790
|
+
startDate?: string;
|
|
791
|
+
endDate?: string;
|
|
792
|
+
paginationOpts: { numItems: number; cursor: string | null };
|
|
793
|
+
},
|
|
794
|
+
) {
|
|
795
|
+
return await ctx.runQuery(
|
|
796
|
+
this.component.public.paginatePlannedWorkouts,
|
|
797
|
+
args,
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* Delete a planned workout by document ID.
|
|
803
|
+
*
|
|
804
|
+
* @param ctx - Mutation context from the host app
|
|
805
|
+
* @param args.plannedWorkoutId - The planned workout document ID
|
|
806
|
+
*/
|
|
807
|
+
async deletePlannedWorkout(
|
|
808
|
+
ctx: MutationCtx,
|
|
809
|
+
args: { plannedWorkoutId: string },
|
|
810
|
+
) {
|
|
811
|
+
return await ctx.runMutation(
|
|
812
|
+
this.component.public.deletePlannedWorkout,
|
|
813
|
+
args as never,
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* Get a single planned workout by document ID.
|
|
819
|
+
*
|
|
820
|
+
* @param ctx - Query context from the host app
|
|
821
|
+
* @param args.plannedWorkoutId - The planned workout document ID
|
|
822
|
+
*/
|
|
823
|
+
async getPlannedWorkout(
|
|
824
|
+
ctx: QueryCtx,
|
|
825
|
+
args: { plannedWorkoutId: string },
|
|
826
|
+
) {
|
|
827
|
+
return await ctx.runQuery(
|
|
828
|
+
this.component.public.getPlannedWorkout,
|
|
829
|
+
args as never,
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* Push a planned workout to Garmin Connect.
|
|
835
|
+
*
|
|
836
|
+
* Creates the workout on Garmin's Training API and optionally schedules it
|
|
837
|
+
* to the user's calendar if `metadata.planned_date` is set. The workout
|
|
838
|
+
* will appear on the user's Garmin device after they sync.
|
|
839
|
+
*
|
|
840
|
+
* @param ctx - Action context from the host app
|
|
841
|
+
* @param args.userId - The host app's user identifier
|
|
842
|
+
* @param args.plannedWorkoutId - The Soma planned workout document ID
|
|
843
|
+
* @param args.workoutProvider - Name shown to user in Garmin Connect (default: "Soma", 20 chars max)
|
|
844
|
+
* @returns `{ garminWorkoutId, garminScheduleId }`
|
|
845
|
+
*
|
|
846
|
+
* @example
|
|
847
|
+
* ```ts
|
|
848
|
+
* export const pushWorkout = action({
|
|
849
|
+
* args: { userId: v.string(), workoutId: v.string() },
|
|
850
|
+
* handler: async (ctx, { userId, workoutId }) => {
|
|
851
|
+
* return await soma.pushPlannedWorkoutToGarmin(ctx, {
|
|
852
|
+
* userId,
|
|
853
|
+
* plannedWorkoutId: workoutId,
|
|
854
|
+
* });
|
|
855
|
+
* },
|
|
856
|
+
* });
|
|
857
|
+
* ```
|
|
858
|
+
*/
|
|
859
|
+
async pushPlannedWorkoutToGarmin(
|
|
860
|
+
ctx: ActionCtx,
|
|
861
|
+
args: {
|
|
862
|
+
userId: string;
|
|
863
|
+
plannedWorkoutId: string;
|
|
864
|
+
workoutProvider?: string;
|
|
865
|
+
},
|
|
866
|
+
) {
|
|
867
|
+
const config = this.requireGarminConfig();
|
|
868
|
+
return await ctx.runAction(this.component.garmin.pushPlannedWorkout, {
|
|
869
|
+
...args,
|
|
870
|
+
clientId: config.clientId,
|
|
871
|
+
clientSecret: config.clientSecret,
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
|
|
728
875
|
// ─── Strava Integration ──────────────────────────────────────────────────────
|
|
729
876
|
// High-level methods that handle OAuth, token storage, and data syncing
|
|
730
877
|
// for Strava. Requires Strava credentials to be configured either via
|
|
@@ -870,68 +1017,65 @@ export class Soma {
|
|
|
870
1017
|
}
|
|
871
1018
|
|
|
872
1019
|
// ─── Garmin Integration ──────────────────────────────────────────────────────
|
|
873
|
-
// High-level methods that handle OAuth
|
|
874
|
-
// for Garmin. Requires Garmin credentials to be configured either
|
|
875
|
-
// environment variables or the constructor.
|
|
1020
|
+
// High-level methods that handle OAuth 2.0 PKCE, token storage, and data
|
|
1021
|
+
// syncing for Garmin. Requires Garmin credentials to be configured either
|
|
1022
|
+
// via environment variables or the constructor.
|
|
876
1023
|
|
|
877
1024
|
/**
|
|
878
|
-
*
|
|
1025
|
+
* Generate a Garmin OAuth 2.0 authorization URL with PKCE.
|
|
879
1026
|
*
|
|
880
|
-
* Returns the
|
|
881
|
-
*
|
|
1027
|
+
* Returns the `authUrl` to redirect the user to, along with the `state`
|
|
1028
|
+
* and `codeVerifier` used for the PKCE flow.
|
|
882
1029
|
*
|
|
883
|
-
* If `userId` is provided, the
|
|
884
|
-
*
|
|
885
|
-
*
|
|
886
|
-
*
|
|
1030
|
+
* If `userId` is provided, the PKCE state is stored inside the component
|
|
1031
|
+
* automatically, and the callback handler registered by `registerRoutes`
|
|
1032
|
+
* will complete the flow without further host-app intervention. This is
|
|
1033
|
+
* the recommended approach.
|
|
887
1034
|
*
|
|
888
|
-
* If `userId` is omitted, the host app must store
|
|
889
|
-
* itself and pass
|
|
1035
|
+
* If `userId` is omitted, the host app must store the returned
|
|
1036
|
+
* `codeVerifier` itself and pass it to `connectGarmin` manually.
|
|
890
1037
|
*
|
|
891
1038
|
* @param ctx - Action context from the host app
|
|
892
|
-
* @param opts.
|
|
1039
|
+
* @param opts.redirectUri - The URL Garmin will redirect to after authorization
|
|
893
1040
|
* @param opts.userId - The host app's user identifier (required for `registerRoutes` flow)
|
|
894
|
-
* @returns `{
|
|
1041
|
+
* @returns `{ authUrl, state, codeVerifier }`
|
|
895
1042
|
*
|
|
896
1043
|
* @example
|
|
897
1044
|
* ```ts
|
|
898
|
-
*
|
|
899
|
-
* const { authUrl } = await soma.getGarminRequestToken(ctx, {
|
|
1045
|
+
* const { authUrl } = await soma.getGarminAuthUrl(ctx, {
|
|
900
1046
|
* userId: "user_123",
|
|
901
|
-
*
|
|
1047
|
+
* redirectUri: "https://your-app.convex.site/api/garmin/callback",
|
|
902
1048
|
* });
|
|
903
1049
|
* // Redirect user to authUrl — the callback is handled automatically
|
|
904
1050
|
* ```
|
|
905
1051
|
*/
|
|
906
|
-
async
|
|
1052
|
+
async getGarminAuthUrl(
|
|
907
1053
|
ctx: ActionCtx,
|
|
908
|
-
opts: {
|
|
1054
|
+
opts: { redirectUri?: string; userId?: string },
|
|
909
1055
|
) {
|
|
910
1056
|
const config = this.requireGarminConfig();
|
|
911
|
-
return await ctx.runAction(this.component.garmin.
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
callbackUrl: opts.callbackUrl,
|
|
1057
|
+
return await ctx.runAction(this.component.garmin.getGarminAuthUrl, {
|
|
1058
|
+
clientId: config.clientId,
|
|
1059
|
+
redirectUri: opts.redirectUri,
|
|
915
1060
|
userId: opts.userId,
|
|
916
1061
|
});
|
|
917
1062
|
}
|
|
918
1063
|
|
|
919
1064
|
/**
|
|
920
|
-
*
|
|
1065
|
+
* Handle the Garmin OAuth 2.0 callback (manual flow).
|
|
921
1066
|
*
|
|
922
|
-
* Exchanges the
|
|
923
|
-
*
|
|
924
|
-
*
|
|
925
|
-
* sleep, body composition, menstruation).
|
|
1067
|
+
* Exchanges the authorization code for tokens, creates/reactivates the
|
|
1068
|
+
* Soma connection, stores tokens securely, and syncs the last 30 days
|
|
1069
|
+
* of all data types.
|
|
926
1070
|
*
|
|
927
|
-
* Call this from your OAuth callback endpoint after receiving the
|
|
928
|
-
*
|
|
1071
|
+
* Call this from your OAuth callback endpoint after receiving the `code`
|
|
1072
|
+
* query parameter from Garmin.
|
|
929
1073
|
*
|
|
930
1074
|
* @param ctx - Action context from the host app
|
|
931
1075
|
* @param args.userId - The host app's user identifier
|
|
932
|
-
* @param args.
|
|
933
|
-
* @param args.
|
|
934
|
-
* @param args.
|
|
1076
|
+
* @param args.code - The authorization code from the callback
|
|
1077
|
+
* @param args.codeVerifier - The PKCE code verifier from Step 1
|
|
1078
|
+
* @param args.redirectUri - The redirect URI used in the authorization request
|
|
935
1079
|
* @returns `{ connectionId, synced, errors }`
|
|
936
1080
|
*
|
|
937
1081
|
* @example
|
|
@@ -939,9 +1083,8 @@ export class Soma {
|
|
|
939
1083
|
* export const handleGarminCallback = action({
|
|
940
1084
|
* args: {
|
|
941
1085
|
* userId: v.string(),
|
|
942
|
-
*
|
|
943
|
-
*
|
|
944
|
-
* verifier: v.string(),
|
|
1086
|
+
* code: v.string(),
|
|
1087
|
+
* codeVerifier: v.string(),
|
|
945
1088
|
* },
|
|
946
1089
|
* handler: async (ctx, args) => {
|
|
947
1090
|
* return await soma.connectGarmin(ctx, args);
|
|
@@ -953,40 +1096,41 @@ export class Soma {
|
|
|
953
1096
|
ctx: ActionCtx,
|
|
954
1097
|
args: {
|
|
955
1098
|
userId: string;
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
1099
|
+
code: string;
|
|
1100
|
+
codeVerifier: string;
|
|
1101
|
+
redirectUri?: string;
|
|
959
1102
|
},
|
|
960
1103
|
) {
|
|
961
1104
|
const config = this.requireGarminConfig();
|
|
962
1105
|
return await ctx.runAction(this.component.garmin.connectGarmin, {
|
|
963
1106
|
...args,
|
|
964
|
-
|
|
965
|
-
|
|
1107
|
+
clientId: config.clientId,
|
|
1108
|
+
clientSecret: config.clientSecret,
|
|
966
1109
|
});
|
|
967
1110
|
}
|
|
968
1111
|
|
|
969
1112
|
/**
|
|
970
|
-
* Complete a Garmin OAuth flow using stored pending state.
|
|
1113
|
+
* Complete a Garmin OAuth 2.0 flow using stored pending state.
|
|
971
1114
|
*
|
|
972
1115
|
* This is called automatically by the `registerRoutes` callback handler.
|
|
973
|
-
* It looks up the pending OAuth state stored during `
|
|
974
|
-
* exchanges for
|
|
1116
|
+
* It looks up the pending OAuth state stored during `getGarminAuthUrl`,
|
|
1117
|
+
* exchanges for tokens, creates the connection, and syncs data.
|
|
975
1118
|
*
|
|
976
1119
|
* @param ctx - Action context from the host app
|
|
977
|
-
* @param args.
|
|
978
|
-
* @param args.
|
|
1120
|
+
* @param args.code - The authorization code from the callback query params
|
|
1121
|
+
* @param args.state - The state parameter from the callback query params
|
|
1122
|
+
* @param args.redirectUri - The redirect URI used in the authorization request
|
|
979
1123
|
* @returns `{ connectionId, synced, errors }`
|
|
980
1124
|
*/
|
|
981
1125
|
async completeGarminOAuth(
|
|
982
1126
|
ctx: ActionCtx,
|
|
983
|
-
args: {
|
|
1127
|
+
args: { code: string; state: string; redirectUri?: string },
|
|
984
1128
|
) {
|
|
985
1129
|
const config = this.requireGarminConfig();
|
|
986
1130
|
return await ctx.runAction(this.component.garmin.completeGarminOAuth, {
|
|
987
1131
|
...args,
|
|
988
|
-
|
|
989
|
-
|
|
1132
|
+
clientId: config.clientId,
|
|
1133
|
+
clientSecret: config.clientSecret,
|
|
990
1134
|
});
|
|
991
1135
|
}
|
|
992
1136
|
|
|
@@ -995,6 +1139,7 @@ export class Soma {
|
|
|
995
1139
|
*
|
|
996
1140
|
* Fetches activities, dailies, sleep, body composition, and menstruation
|
|
997
1141
|
* data for the specified time range (defaults to last 30 days).
|
|
1142
|
+
* Automatically refreshes expired tokens.
|
|
998
1143
|
*
|
|
999
1144
|
* @param ctx - Action context from the host app
|
|
1000
1145
|
* @param args.userId - The host app's user identifier
|
|
@@ -1023,17 +1168,16 @@ export class Soma {
|
|
|
1023
1168
|
const config = this.requireGarminConfig();
|
|
1024
1169
|
return await ctx.runAction(this.component.garmin.syncGarmin, {
|
|
1025
1170
|
...args,
|
|
1026
|
-
|
|
1027
|
-
|
|
1171
|
+
clientId: config.clientId,
|
|
1172
|
+
clientSecret: config.clientSecret,
|
|
1028
1173
|
});
|
|
1029
1174
|
}
|
|
1030
1175
|
|
|
1031
1176
|
/**
|
|
1032
1177
|
* Disconnect a user from Garmin.
|
|
1033
1178
|
*
|
|
1034
|
-
*
|
|
1035
|
-
*
|
|
1036
|
-
* is local only.
|
|
1179
|
+
* Deregisters the user at Garmin (best-effort), deletes stored tokens,
|
|
1180
|
+
* and sets the connection to inactive.
|
|
1037
1181
|
*
|
|
1038
1182
|
* @param ctx - Action context from the host app
|
|
1039
1183
|
* @param args.userId - The host app's user identifier
|
|
@@ -1118,10 +1262,10 @@ export interface StravaRouteOptions {
|
|
|
1118
1262
|
export interface GarminRouteOptions {
|
|
1119
1263
|
/** HTTP path for the OAuth callback. @default "/api/garmin/callback" */
|
|
1120
1264
|
path?: string;
|
|
1121
|
-
/** Override
|
|
1122
|
-
|
|
1123
|
-
/** Override
|
|
1124
|
-
|
|
1265
|
+
/** Override GARMIN_CLIENT_ID env var. */
|
|
1266
|
+
clientId?: string;
|
|
1267
|
+
/** Override GARMIN_CLIENT_SECRET env var. */
|
|
1268
|
+
clientSecret?: string;
|
|
1125
1269
|
/** URL to redirect the user to after a successful connection. */
|
|
1126
1270
|
onSuccess?: string;
|
|
1127
1271
|
}
|
|
@@ -1256,34 +1400,41 @@ export function registerRoutes(
|
|
|
1256
1400
|
method: "GET",
|
|
1257
1401
|
handler: httpActionGeneric(async (ctx, request) => {
|
|
1258
1402
|
const url = new URL(request.url);
|
|
1259
|
-
const
|
|
1260
|
-
const
|
|
1403
|
+
const code = url.searchParams.get("code");
|
|
1404
|
+
const state = url.searchParams.get("state");
|
|
1261
1405
|
|
|
1262
|
-
if (!
|
|
1263
|
-
return new Response("Missing
|
|
1406
|
+
if (!code) {
|
|
1407
|
+
return new Response("Missing authorization code", {
|
|
1264
1408
|
status: 400,
|
|
1265
1409
|
});
|
|
1266
1410
|
}
|
|
1411
|
+
if (!state) {
|
|
1412
|
+
return new Response(
|
|
1413
|
+
"Missing state parameter. Ensure the state was included " +
|
|
1414
|
+
"when building the Garmin auth URL.",
|
|
1415
|
+
{ status: 400 },
|
|
1416
|
+
);
|
|
1417
|
+
}
|
|
1267
1418
|
|
|
1268
|
-
const
|
|
1269
|
-
garmin.
|
|
1270
|
-
const
|
|
1271
|
-
garmin.
|
|
1419
|
+
const clientId =
|
|
1420
|
+
garmin.clientId ?? process.env.GARMIN_CLIENT_ID;
|
|
1421
|
+
const clientSecret =
|
|
1422
|
+
garmin.clientSecret ?? process.env.GARMIN_CLIENT_SECRET;
|
|
1272
1423
|
|
|
1273
|
-
if (!
|
|
1424
|
+
if (!clientId || !clientSecret) {
|
|
1274
1425
|
return new Response(
|
|
1275
|
-
"Garmin credentials not configured. Set
|
|
1276
|
-
"
|
|
1426
|
+
"Garmin credentials not configured. Set GARMIN_CLIENT_ID and " +
|
|
1427
|
+
"GARMIN_CLIENT_SECRET environment variables, or pass them to registerRoutes.",
|
|
1277
1428
|
{ status: 500 },
|
|
1278
1429
|
);
|
|
1279
1430
|
}
|
|
1280
1431
|
|
|
1281
1432
|
try {
|
|
1282
1433
|
await ctx.runAction(component.garmin.completeGarminOAuth, {
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1434
|
+
code,
|
|
1435
|
+
state,
|
|
1436
|
+
clientId,
|
|
1437
|
+
clientSecret,
|
|
1287
1438
|
});
|
|
1288
1439
|
} catch (error) {
|
|
1289
1440
|
const message =
|