@nativesquare/soma 0.5.0 → 0.6.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.
@@ -1,5 +1,5 @@
1
1
  // ─── Garmin Component Actions ────────────────────────────────────────────────
2
- // Public actions that handle the full Garmin OAuth + sync lifecycle.
2
+ // Public actions that handle the full Garmin OAuth 2.0 PKCE + sync lifecycle.
3
3
  // The host app calls these through the Soma class, which threads the
4
4
  // credentials automatically from env vars or constructor config.
5
5
  //
@@ -12,7 +12,14 @@ import {
12
12
  internalMutation,
13
13
  internalQuery,
14
14
  } from "./_generated/server.js";
15
- import { getRequestToken, getAccessToken } from "../garmin/auth.js";
15
+ import {
16
+ generateCodeVerifier,
17
+ generateCodeChallenge,
18
+ generateState,
19
+ buildAuthUrl,
20
+ exchangeCode,
21
+ refreshToken,
22
+ } from "../garmin/auth.js";
16
23
  import { GarminClient } from "../garmin/client.js";
17
24
  import { transformActivity } from "../garmin/activity.js";
18
25
  import { transformDaily } from "../garmin/daily.js";
@@ -29,15 +36,18 @@ const internalApi: any = anyApi;
29
36
  // Default sync window: last 30 days
30
37
  const DEFAULT_SYNC_DAYS = 30;
31
38
 
39
+ // Refresh buffer: refresh tokens 10 minutes before expiry
40
+ const REFRESH_BUFFER_SECONDS = 600;
41
+
32
42
  // ─── Internal Pending OAuth CRUD ─────────────────────────────────────────────
33
- // Temporary storage for in-progress Garmin OAuth 1.0a flows.
34
- // Bridges Step 1 (getGarminRequestToken) and Step 3 (completeGarminOAuth).
43
+ // Temporary storage for in-progress Garmin OAuth 2.0 PKCE flows.
44
+ // Bridges getGarminAuthUrl and completeGarminOAuth.
35
45
 
36
46
  export const storePendingOAuth = internalMutation({
37
47
  args: {
38
48
  provider: v.string(),
39
- oauthToken: v.string(),
40
- tokenSecret: v.string(),
49
+ state: v.string(),
50
+ codeVerifier: v.string(),
41
51
  userId: v.string(),
42
52
  },
43
53
  returns: v.null(),
@@ -51,14 +61,14 @@ export const storePendingOAuth = internalMutation({
51
61
  });
52
62
 
53
63
  export const getPendingOAuth = internalQuery({
54
- args: { oauthToken: v.string() },
64
+ args: { state: v.string() },
55
65
  returns: v.union(
56
66
  v.object({
57
67
  _id: v.id("pendingOAuth"),
58
68
  _creationTime: v.number(),
59
69
  provider: v.string(),
60
- oauthToken: v.string(),
61
- tokenSecret: v.string(),
70
+ state: v.string(),
71
+ codeVerifier: v.string(),
62
72
  userId: v.string(),
63
73
  createdAt: v.number(),
64
74
  }),
@@ -67,18 +77,18 @@ export const getPendingOAuth = internalQuery({
67
77
  handler: async (ctx, args) => {
68
78
  return await ctx.db
69
79
  .query("pendingOAuth")
70
- .withIndex("by_oauthToken", (q) => q.eq("oauthToken", args.oauthToken))
80
+ .withIndex("by_state", (q) => q.eq("state", args.state))
71
81
  .first();
72
82
  },
73
83
  });
74
84
 
75
85
  export const deletePendingOAuth = internalMutation({
76
- args: { oauthToken: v.string() },
86
+ args: { state: v.string() },
77
87
  returns: v.null(),
78
88
  handler: async (ctx, args) => {
79
89
  const pending = await ctx.db
80
90
  .query("pendingOAuth")
81
- .withIndex("by_oauthToken", (q) => q.eq("oauthToken", args.oauthToken))
91
+ .withIndex("by_state", (q) => q.eq("state", args.state))
82
92
  .first();
83
93
  if (pending) {
84
94
  await ctx.db.delete(pending._id);
@@ -90,14 +100,15 @@ export const deletePendingOAuth = internalMutation({
90
100
  // ─── Internal Token CRUD ─────────────────────────────────────────────────────
91
101
 
92
102
  /**
93
- * Store OAuth 1.0a tokens for a Garmin connection.
103
+ * Store OAuth 2.0 tokens for a Garmin connection.
94
104
  * Upserts by connectionId — one token record per connection.
95
105
  */
96
106
  export const storeTokens = internalMutation({
97
107
  args: {
98
108
  connectionId: v.id("connections"),
99
109
  accessToken: v.string(),
100
- tokenSecret: v.string(),
110
+ refreshToken: v.string(),
111
+ expiresAt: v.number(),
101
112
  },
102
113
  returns: v.null(),
103
114
  handler: async (ctx, args) => {
@@ -111,7 +122,8 @@ export const storeTokens = internalMutation({
111
122
  if (existing) {
112
123
  await ctx.db.patch(existing._id, {
113
124
  accessToken: args.accessToken,
114
- tokenSecret: args.tokenSecret,
125
+ refreshToken: args.refreshToken,
126
+ expiresAt: args.expiresAt,
115
127
  });
116
128
  return null;
117
129
  }
@@ -119,7 +131,8 @@ export const storeTokens = internalMutation({
119
131
  await ctx.db.insert("providerTokens", {
120
132
  connectionId: args.connectionId,
121
133
  accessToken: args.accessToken,
122
- tokenSecret: args.tokenSecret,
134
+ refreshToken: args.refreshToken,
135
+ expiresAt: args.expiresAt,
123
136
  });
124
137
  return null;
125
138
  },
@@ -137,7 +150,6 @@ export const getTokens = internalQuery({
137
150
  connectionId: v.id("connections"),
138
151
  accessToken: v.string(),
139
152
  refreshToken: v.optional(v.string()),
140
- tokenSecret: v.optional(v.string()),
141
153
  expiresAt: v.optional(v.number()),
142
154
  }),
143
155
  v.null(),
@@ -176,67 +188,66 @@ export const deleteTokens = internalMutation({
176
188
  // ─── Public Actions ──────────────────────────────────────────────────────────
177
189
 
178
190
  /**
179
- * Step 1 of OAuth: obtain a request token and return the authorization URL.
191
+ * Generate a Garmin OAuth 2.0 authorization URL with PKCE.
180
192
  *
181
- * If `userId` is provided, the request token and secret are stored in the
193
+ * If `userId` is provided, the PKCE code verifier and state are stored in the
182
194
  * component's `pendingOAuth` table so that `completeGarminOAuth` can look
183
195
  * them up automatically when the callback fires. This is the recommended
184
196
  * flow when using `registerRoutes`.
185
197
  *
186
- * If `userId` is omitted, the host app must store the returned `token`
187
- * and `tokenSecret` itself and pass them to `connectGarmin` manually.
198
+ * If `userId` is omitted, the host app must store the returned `codeVerifier`
199
+ * itself and pass it to `connectGarmin` manually.
188
200
  */
189
- export const getGarminRequestToken = action({
201
+ export const getGarminAuthUrl = action({
190
202
  args: {
191
- consumerKey: v.string(),
192
- consumerSecret: v.string(),
193
- callbackUrl: v.optional(v.string()),
203
+ clientId: v.string(),
204
+ redirectUri: v.optional(v.string()),
194
205
  userId: v.optional(v.string()),
195
206
  },
196
207
  returns: v.object({
197
- token: v.string(),
198
- tokenSecret: v.string(),
199
208
  authUrl: v.string(),
209
+ state: v.string(),
210
+ codeVerifier: v.string(),
200
211
  }),
201
212
  handler: async (ctx, args) => {
202
- const result = await getRequestToken({
203
- consumerKey: args.consumerKey,
204
- consumerSecret: args.consumerSecret,
205
- callbackUrl: args.callbackUrl,
213
+ const codeVerifier = generateCodeVerifier();
214
+ const codeChallenge = await generateCodeChallenge(codeVerifier);
215
+ const state = generateState();
216
+
217
+ const authUrl = buildAuthUrl({
218
+ clientId: args.clientId,
219
+ codeChallenge,
220
+ redirectUri: args.redirectUri,
221
+ state,
206
222
  });
207
223
 
208
224
  if (args.userId) {
209
225
  await ctx.runMutation(internalApi.garmin.storePendingOAuth, {
210
226
  provider: "GARMIN",
211
- oauthToken: result.oauthToken,
212
- tokenSecret: result.oauthTokenSecret,
227
+ state,
228
+ codeVerifier,
213
229
  userId: args.userId,
214
230
  });
215
231
  }
216
232
 
217
- return {
218
- token: result.oauthToken,
219
- tokenSecret: result.oauthTokenSecret,
220
- authUrl: result.authUrl,
221
- };
233
+ return { authUrl, state, codeVerifier };
222
234
  },
223
235
  });
224
236
 
225
237
  /**
226
- * Step 3 of OAuth + initial sync.
238
+ * Exchange an authorization code for tokens + initial sync.
227
239
  *
228
- * Exchanges the request token + verifier for permanent access tokens,
229
- * creates/reactivates the Soma connection, stores tokens, and syncs
230
- * the last 30 days of all data types.
240
+ * Used in the manual flow where the host app stores the code verifier
241
+ * and handles the callback itself.
231
242
  */
232
243
  export const connectGarmin = action({
233
244
  args: {
234
245
  userId: v.string(),
235
- consumerKey: v.string(),
236
- consumerSecret: v.string(),
237
- token: v.string(),
238
- tokenSecret: v.string(),
239
- verifier: v.string(),
246
+ clientId: v.string(),
247
+ clientSecret: v.string(),
248
+ code: v.string(),
249
+ codeVerifier: v.string(),
250
+ redirectUri: v.optional(v.string()),
240
251
  },
241
252
  returns: v.object({
242
253
  connectionId: v.string(),
@@ -252,34 +263,29 @@ export const connectGarmin = action({
252
263
  ),
253
264
  }),
254
265
  handler: async (ctx, args) => {
255
- // 1. Exchange request token for permanent access token
256
- const accessTokenResult = await getAccessToken({
257
- consumerKey: args.consumerKey,
258
- consumerSecret: args.consumerSecret,
259
- token: args.token,
260
- tokenSecret: args.tokenSecret,
261
- verifier: args.verifier,
266
+ const tokenResult = await exchangeCode({
267
+ clientId: args.clientId,
268
+ clientSecret: args.clientSecret,
269
+ code: args.code,
270
+ codeVerifier: args.codeVerifier,
271
+ redirectUri: args.redirectUri,
262
272
  });
263
273
 
264
- // 2. Create/reactivate the Soma connection
265
274
  const connectionId = await ctx.runMutation(publicApi.public.connect, {
266
275
  userId: args.userId,
267
276
  provider: "GARMIN",
268
277
  });
269
278
 
270
- // 3. Store permanent OAuth tokens
279
+ const expiresAt = Math.floor(Date.now() / 1000) + tokenResult.expires_in;
271
280
  await ctx.runMutation(internalApi.garmin.storeTokens, {
272
281
  connectionId,
273
- accessToken: accessTokenResult.oauthToken,
274
- tokenSecret: accessTokenResult.oauthTokenSecret,
282
+ accessToken: tokenResult.access_token,
283
+ refreshToken: tokenResult.refresh_token,
284
+ expiresAt,
275
285
  });
276
286
 
277
- // 4. Sync last 30 days of all data types
278
287
  const client = new GarminClient({
279
- consumerKey: args.consumerKey,
280
- consumerSecret: args.consumerSecret,
281
- accessToken: accessTokenResult.oauthToken,
282
- tokenSecret: accessTokenResult.oauthTokenSecret,
288
+ accessToken: tokenResult.access_token,
283
289
  });
284
290
 
285
291
  const now = Math.floor(Date.now() / 1000);
@@ -292,12 +298,9 @@ export const connectGarmin = action({
292
298
  const result = await syncAllTypes(ctx, client, {
293
299
  connectionId,
294
300
  userId: args.userId,
295
- consumerKey: args.consumerKey,
296
- consumerSecret: args.consumerSecret,
297
301
  timeRange,
298
302
  });
299
303
 
300
- // 5. Update lastDataUpdate timestamp
301
304
  await ctx.runMutation(publicApi.public.updateConnection, {
302
305
  connectionId,
303
306
  lastDataUpdate: new Date().toISOString(),
@@ -312,20 +315,21 @@ export const connectGarmin = action({
312
315
  });
313
316
 
314
317
  /**
315
- * Complete a Garmin OAuth flow using stored pending state.
318
+ * Complete a Garmin OAuth 2.0 flow using stored pending state.
316
319
  *
317
320
  * Used by `registerRoutes` — the callback handler calls this with the
318
- * `oauth_token` and `oauth_verifier` from the redirect. The action looks
319
- * up the pending state (tokenSecret, userId) stored during Step 1,
320
- * exchanges for permanent tokens, creates the connection, syncs data,
321
- * and cleans up the pending entry.
321
+ * `code` and `state` from the redirect. The action looks up the pending
322
+ * state (codeVerifier, userId) stored during `getGarminAuthUrl`,
323
+ * exchanges for tokens, creates the connection, syncs data, and
324
+ * cleans up the pending entry.
322
325
  */
323
326
  export const completeGarminOAuth = action({
324
327
  args: {
325
- oauthToken: v.string(),
326
- oauthVerifier: v.string(),
327
- consumerKey: v.string(),
328
- consumerSecret: v.string(),
328
+ code: v.string(),
329
+ state: v.string(),
330
+ clientId: v.string(),
331
+ clientSecret: v.string(),
332
+ redirectUri: v.optional(v.string()),
329
333
  },
330
334
  returns: v.object({
331
335
  connectionId: v.string(),
@@ -341,50 +345,43 @@ export const completeGarminOAuth = action({
341
345
  ),
342
346
  }),
343
347
  handler: async (ctx, args) => {
344
- // 1. Look up pending state
345
348
  const pending = await ctx.runQuery(internalApi.garmin.getPendingOAuth, {
346
- oauthToken: args.oauthToken,
349
+ state: args.state,
347
350
  });
348
351
  if (!pending) {
349
352
  throw new Error(
350
- "No pending Garmin OAuth state found for this token. " +
351
- "The request token may have expired or was already used.",
353
+ "No pending Garmin OAuth state found for this state parameter. " +
354
+ "The authorization may have expired or was already used.",
352
355
  );
353
356
  }
354
357
 
355
- // 2. Exchange request token for permanent access token
356
- const accessTokenResult = await getAccessToken({
357
- consumerKey: args.consumerKey,
358
- consumerSecret: args.consumerSecret,
359
- token: args.oauthToken,
360
- tokenSecret: pending.tokenSecret,
361
- verifier: args.oauthVerifier,
358
+ const tokenResult = await exchangeCode({
359
+ clientId: args.clientId,
360
+ clientSecret: args.clientSecret,
361
+ code: args.code,
362
+ codeVerifier: pending.codeVerifier,
363
+ redirectUri: args.redirectUri,
362
364
  });
363
365
 
364
- // 3. Delete pending state (no longer needed)
365
366
  await ctx.runMutation(internalApi.garmin.deletePendingOAuth, {
366
- oauthToken: args.oauthToken,
367
+ state: args.state,
367
368
  });
368
369
 
369
- // 4. Create/reactivate the Soma connection
370
370
  const connectionId = await ctx.runMutation(publicApi.public.connect, {
371
371
  userId: pending.userId,
372
372
  provider: "GARMIN",
373
373
  });
374
374
 
375
- // 5. Store permanent OAuth tokens
375
+ const expiresAt = Math.floor(Date.now() / 1000) + tokenResult.expires_in;
376
376
  await ctx.runMutation(internalApi.garmin.storeTokens, {
377
377
  connectionId,
378
- accessToken: accessTokenResult.oauthToken,
379
- tokenSecret: accessTokenResult.oauthTokenSecret,
378
+ accessToken: tokenResult.access_token,
379
+ refreshToken: tokenResult.refresh_token,
380
+ expiresAt,
380
381
  });
381
382
 
382
- // 6. Sync last 30 days of all data types
383
383
  const client = new GarminClient({
384
- consumerKey: args.consumerKey,
385
- consumerSecret: args.consumerSecret,
386
- accessToken: accessTokenResult.oauthToken,
387
- tokenSecret: accessTokenResult.oauthTokenSecret,
384
+ accessToken: tokenResult.access_token,
388
385
  });
389
386
 
390
387
  const now = Math.floor(Date.now() / 1000);
@@ -397,12 +394,9 @@ export const completeGarminOAuth = action({
397
394
  const result = await syncAllTypes(ctx, client, {
398
395
  connectionId,
399
396
  userId: pending.userId,
400
- consumerKey: args.consumerKey,
401
- consumerSecret: args.consumerSecret,
402
397
  timeRange,
403
398
  });
404
399
 
405
- // 7. Update lastDataUpdate timestamp
406
400
  await ctx.runMutation(publicApi.public.updateConnection, {
407
401
  connectionId,
408
402
  lastDataUpdate: new Date().toISOString(),
@@ -419,14 +413,14 @@ export const completeGarminOAuth = action({
419
413
  /**
420
414
  * Incremental Garmin sync for an already-connected user.
421
415
  *
422
- * Looks up the stored tokens and syncs all data types for the specified
423
- * time range (defaults to last 30 days).
416
+ * Looks up the stored tokens, refreshes if expired, and syncs all data
417
+ * types for the specified time range (defaults to last 30 days).
424
418
  */
425
419
  export const syncGarmin = action({
426
420
  args: {
427
421
  userId: v.string(),
428
- consumerKey: v.string(),
429
- consumerSecret: v.string(),
422
+ clientId: v.string(),
423
+ clientSecret: v.string(),
430
424
  startTimeInSeconds: v.optional(v.number()),
431
425
  endTimeInSeconds: v.optional(v.number()),
432
426
  },
@@ -443,7 +437,6 @@ export const syncGarmin = action({
443
437
  ),
444
438
  }),
445
439
  handler: async (ctx, args) => {
446
- // 1. Look up connection
447
440
  const connection = await ctx.runQuery(
448
441
  internalApi.private.getConnectionByProvider,
449
442
  { userId: args.userId, provider: "GARMIN" },
@@ -462,24 +455,42 @@ export const syncGarmin = action({
462
455
 
463
456
  const connectionId = connection._id;
464
457
 
465
- // 2. Get stored tokens
466
458
  const tokenDoc = await ctx.runQuery(internalApi.garmin.getTokens, {
467
459
  connectionId,
468
460
  });
469
- if (!tokenDoc || !tokenDoc.tokenSecret) {
461
+ if (!tokenDoc) {
470
462
  throw new Error(
471
463
  "No Garmin tokens found for this connection. " +
472
464
  "The connection may have been created before token storage was available.",
473
465
  );
474
466
  }
475
467
 
476
- // 3. Create client and sync
477
- const client = new GarminClient({
478
- consumerKey: args.consumerKey,
479
- consumerSecret: args.consumerSecret,
480
- accessToken: tokenDoc.accessToken,
481
- tokenSecret: tokenDoc.tokenSecret,
482
- });
468
+ let accessToken = tokenDoc.accessToken;
469
+
470
+ // Refresh the token if it's expired or about to expire
471
+ const nowSeconds = Math.floor(Date.now() / 1000);
472
+ if (
473
+ tokenDoc.expiresAt &&
474
+ tokenDoc.refreshToken &&
475
+ nowSeconds >= tokenDoc.expiresAt - REFRESH_BUFFER_SECONDS
476
+ ) {
477
+ const refreshed = await refreshToken({
478
+ clientId: args.clientId,
479
+ clientSecret: args.clientSecret,
480
+ refreshToken: tokenDoc.refreshToken,
481
+ });
482
+
483
+ accessToken = refreshed.access_token;
484
+ const newExpiresAt = nowSeconds + refreshed.expires_in;
485
+ await ctx.runMutation(internalApi.garmin.storeTokens, {
486
+ connectionId,
487
+ accessToken: refreshed.access_token,
488
+ refreshToken: refreshed.refresh_token,
489
+ expiresAt: newExpiresAt,
490
+ });
491
+ }
492
+
493
+ const client = new GarminClient({ accessToken });
483
494
 
484
495
  const now = Math.floor(Date.now() / 1000);
485
496
  const timeRange = {
@@ -491,12 +502,9 @@ export const syncGarmin = action({
491
502
  const result = await syncAllTypes(ctx, client, {
492
503
  connectionId,
493
504
  userId: args.userId,
494
- consumerKey: args.consumerKey,
495
- consumerSecret: args.consumerSecret,
496
505
  timeRange,
497
506
  });
498
507
 
499
- // 4. Update lastDataUpdate timestamp
500
508
  await ctx.runMutation(publicApi.public.updateConnection, {
501
509
  connectionId,
502
510
  lastDataUpdate: new Date().toISOString(),
@@ -509,8 +517,8 @@ export const syncGarmin = action({
509
517
  /**
510
518
  * Disconnect a user from Garmin.
511
519
  *
512
- * Deletes stored tokens and sets the connection to inactive.
513
- * Garmin OAuth 1.0a tokens can't be revoked via API, so we just clean up locally.
520
+ * Deregisters the user via the Garmin API (best-effort), deletes stored
521
+ * tokens, and sets the connection to inactive.
514
522
  */
515
523
  export const disconnectGarmin = action({
516
524
  args: {
@@ -518,7 +526,6 @@ export const disconnectGarmin = action({
518
526
  },
519
527
  returns: v.null(),
520
528
  handler: async (ctx, args) => {
521
- // 1. Look up connection
522
529
  const connection = await ctx.runQuery(
523
530
  internalApi.private.getConnectionByProvider,
524
531
  { userId: args.userId, provider: "GARMIN" },
@@ -531,10 +538,21 @@ export const disconnectGarmin = action({
531
538
 
532
539
  const connectionId = connection._id;
533
540
 
534
- // 2. Delete stored tokens
541
+ // Best-effort: deregister user at Garmin
542
+ const tokenDoc = await ctx.runQuery(internalApi.garmin.getTokens, {
543
+ connectionId,
544
+ });
545
+ if (tokenDoc) {
546
+ try {
547
+ const client = new GarminClient({ accessToken: tokenDoc.accessToken });
548
+ await client.deleteUserRegistration();
549
+ } catch {
550
+ // Deregistration is best-effort; proceed with local cleanup
551
+ }
552
+ }
553
+
535
554
  await ctx.runMutation(internalApi.garmin.deleteTokens, { connectionId });
536
555
 
537
- // 3. Set connection inactive
538
556
  await ctx.runMutation(publicApi.public.disconnect, {
539
557
  userId: args.userId,
540
558
  provider: "GARMIN",
@@ -549,8 +567,6 @@ export const disconnectGarmin = action({
549
567
  interface SyncAllConfig {
550
568
  connectionId: string;
551
569
  userId: string;
552
- consumerKey: string;
553
- consumerSecret: string;
554
570
  timeRange: { uploadStartTimeInSeconds: number; uploadEndTimeInSeconds: number };
555
571
  }
556
572
 
@@ -115,26 +115,25 @@ export default defineSchema({
115
115
  .index("by_userId_plannedDate", ["userId", "metadata.planned_date"]),
116
116
 
117
117
  // ── Provider Tokens ────────────────────────────────────────────────────────
118
- // OAuth tokens for cloud-based providers (Strava, Garmin, etc.).
118
+ // OAuth 2.0 tokens for cloud-based providers (Strava, Garmin, etc.).
119
119
  // Stored separately from connections to keep the connection table
120
120
  // provider-agnostic. One token record per connection.
121
121
  providerTokens: defineTable({
122
122
  connectionId: v.id("connections"),
123
123
  accessToken: v.string(),
124
- refreshToken: v.optional(v.string()), // OAuth 2.0 providers (Strava)
125
- tokenSecret: v.optional(v.string()), // OAuth 1.0a providers (Garmin)
126
- expiresAt: v.optional(v.number()), // Unix epoch seconds; absent for permanent tokens
124
+ refreshToken: v.optional(v.string()),
125
+ expiresAt: v.optional(v.number()),
127
126
  }).index("by_connectionId", ["connectionId"]),
128
127
 
129
128
  // ── Pending OAuth ─────────────────────────────────────────────────────────
130
- // Temporary storage for in-progress OAuth flows. Bridges the gap between
131
- // initiating OAuth (Step 1) and the callback (Step 3) for providers like
132
- // Garmin that use OAuth 1.0a and don't have a `state` parameter.
129
+ // Temporary storage for in-progress OAuth 2.0 PKCE flows. Bridges the gap
130
+ // between initiating OAuth (auth URL) and the callback (code exchange).
131
+ // The `state` parameter links the callback back to the pending entry.
133
132
  pendingOAuth: defineTable({
134
133
  provider: v.string(),
135
- oauthToken: v.string(),
136
- tokenSecret: v.string(),
134
+ state: v.string(),
135
+ codeVerifier: v.string(),
137
136
  userId: v.string(),
138
137
  createdAt: v.number(),
139
- }).index("by_oauthToken", ["oauthToken"]),
138
+ }).index("by_state", ["state"]),
140
139
  });
@@ -75,7 +75,6 @@ export const getTokens = internalQuery({
75
75
  connectionId: v.id("connections"),
76
76
  accessToken: v.string(),
77
77
  refreshToken: v.optional(v.string()),
78
- tokenSecret: v.optional(v.string()),
79
78
  expiresAt: v.optional(v.number()),
80
79
  }),
81
80
  v.null(),