rivet-design 0.11.1 → 0.11.2

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 (45) hide show
  1. package/dist/index.d.ts +7 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +25 -9
  4. package/dist/index.js.map +1 -1
  5. package/dist/mcp/auth/httpOAuthProvider.d.ts +12 -12
  6. package/dist/mcp/auth/httpOAuthProvider.d.ts.map +1 -1
  7. package/dist/mcp/auth/httpOAuthProvider.js +348 -67
  8. package/dist/mcp/auth/httpOAuthProvider.js.map +1 -1
  9. package/dist/mcp/auth/tools.d.ts +1 -0
  10. package/dist/mcp/auth/tools.d.ts.map +1 -1
  11. package/dist/mcp/auth/tools.js +31 -10
  12. package/dist/mcp/auth/tools.js.map +1 -1
  13. package/dist/mcp/hostedServer.d.ts +13 -0
  14. package/dist/mcp/hostedServer.d.ts.map +1 -0
  15. package/dist/mcp/hostedServer.js +46 -0
  16. package/dist/mcp/hostedServer.js.map +1 -0
  17. package/dist/mcp/httpServer.d.ts +23 -3
  18. package/dist/mcp/httpServer.d.ts.map +1 -1
  19. package/dist/mcp/httpServer.js +320 -56
  20. package/dist/mcp/httpServer.js.map +1 -1
  21. package/dist/mcp/server.d.ts +4 -8
  22. package/dist/mcp/server.d.ts.map +1 -1
  23. package/dist/mcp/server.js +43 -34
  24. package/dist/mcp/server.js.map +1 -1
  25. package/dist/mcp/types.d.ts +7 -0
  26. package/dist/mcp/types.d.ts.map +1 -0
  27. package/dist/mcp/types.js +3 -0
  28. package/dist/mcp/types.js.map +1 -0
  29. package/dist/services/AuthService.d.ts +15 -1
  30. package/dist/services/AuthService.d.ts.map +1 -1
  31. package/dist/services/AuthService.js +176 -8
  32. package/dist/services/AuthService.js.map +1 -1
  33. package/dist/services/ConfigManager.d.ts +2 -0
  34. package/dist/services/ConfigManager.d.ts.map +1 -1
  35. package/dist/services/ConfigManager.js +21 -7
  36. package/dist/services/ConfigManager.js.map +1 -1
  37. package/dist/services/IntegrationsClient.d.ts.map +1 -1
  38. package/dist/services/IntegrationsClient.js +15 -4
  39. package/dist/services/IntegrationsClient.js.map +1 -1
  40. package/dist/services/RequestAuthContext.d.ts +1 -0
  41. package/dist/services/RequestAuthContext.d.ts.map +1 -1
  42. package/dist/services/RequestAuthContext.js.map +1 -1
  43. package/package.json +1 -1
  44. package/src/ui/dist/assets/{main-n7zyCCGw.js → main-4_VncEaM.js} +2 -2
  45. package/src/ui/dist/index.html +1 -1
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.RivetOAuthServerProvider = void 0;
3
+ exports.RivetOAuthServerProvider = exports.MCP_BROWSER_AUTH_COOKIE_NAME = void 0;
4
4
  const crypto_1 = require("crypto");
5
5
  const errors_js_1 = require("@modelcontextprotocol/sdk/server/auth/errors.js");
6
6
  const accessTokenRefresh_1 = require("../../services/accessTokenRefresh");
@@ -16,6 +16,12 @@ const TOKEN_POLL_INTERVAL_MS = 1000;
16
16
  const PROFILE_FETCH_TIMEOUT_MS = 10_000;
17
17
  const MCP_OAUTH_SCOPE = 'rivet';
18
18
  const HOSTED_DASHBOARD_AUTH_URL = 'https://rivet-proxy.onrender.com/dashboard/';
19
+ exports.MCP_BROWSER_AUTH_COOKIE_NAME = 'rivet_mcp_oauth';
20
+ const CURSOR_DESKTOP_REDIRECT_PROTOCOL = 'cursor:';
21
+ const CURSOR_DESKTOP_REDIRECT_HOST = 'anysphere.cursor-mcp';
22
+ const CURSOR_DESKTOP_REDIRECT_PATH = '/oauth/callback';
23
+ const CURSOR_CLOUD_REDIRECT_HOST = 'www.cursor.com';
24
+ const CURSOR_CLOUD_REDIRECT_PATH = '/agents/mcp/oauth/callback';
19
25
  const LOOPBACK_REDIRECT_HOSTS = new Set([
20
26
  'localhost',
21
27
  '127.0.0.1',
@@ -29,8 +35,8 @@ class InMemoryClientsStore {
29
35
  return this.clients.get(clientId);
30
36
  }
31
37
  registerClient(client) {
32
- if (!client.redirect_uris.every(isLoopbackRedirectUri)) {
33
- throw new errors_js_1.InvalidClientMetadataError('Rivet MCP OAuth clients must use loopback HTTP redirect URIs.');
38
+ if (!client.redirect_uris.every(isAllowedRedirectUri)) {
39
+ throw new errors_js_1.InvalidClientMetadataError('Rivet MCP OAuth clients must declare valid loopback or Cursor redirect URIs.');
34
40
  }
35
41
  const registered = {
36
42
  ...client,
@@ -50,6 +56,31 @@ const isLoopbackRedirectUri = (redirectUri) => {
50
56
  return false;
51
57
  }
52
58
  };
59
+ const isAllowedRedirectUri = (redirectUri) => {
60
+ try {
61
+ const url = new URL(redirectUri);
62
+ if (url.hash) {
63
+ return false;
64
+ }
65
+ if (isLoopbackRedirectUri(redirectUri)) {
66
+ return true;
67
+ }
68
+ if (url.protocol === CURSOR_DESKTOP_REDIRECT_PROTOCOL &&
69
+ url.hostname === CURSOR_DESKTOP_REDIRECT_HOST &&
70
+ url.pathname === CURSOR_DESKTOP_REDIRECT_PATH &&
71
+ !url.search) {
72
+ return true;
73
+ }
74
+ return (url.protocol === 'https:' &&
75
+ url.hostname.toLowerCase() === CURSOR_CLOUD_REDIRECT_HOST &&
76
+ url.pathname === CURSOR_CLOUD_REDIRECT_PATH &&
77
+ !url.search &&
78
+ !url.port);
79
+ }
80
+ catch {
81
+ return false;
82
+ }
83
+ };
53
84
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
54
85
  const escapeHtml = (value) => value
55
86
  .replace(/&/g, '&')
@@ -57,12 +88,17 @@ const escapeHtml = (value) => value
57
88
  .replace(/>/g, '>')
58
89
  .replace(/"/g, '"')
59
90
  .replace(/'/g, ''');
91
+ const DASHBOARD_AUTH_STYLESHEET_PATH = '/dashboard/auth.css';
92
+ const GOOGLE_ICON_URL = 'https://www.gstatic.com/firebasejs/ui/2.0.0/images/auth/google.svg';
93
+ const getBrowserAuthCookieValue = (sessionId, nonce) => `${sessionId}:${nonce}`;
60
94
  /**
61
95
  * Shows the browser user which local OAuth client will receive the code before
62
96
  * starting Google sign-in.
63
97
  */
64
98
  const getClientApprovalHtml = (input) => {
65
99
  const clientName = input.clientName?.trim() || 'Unknown MCP client';
100
+ const escapedAuthUrl = escapeHtml(input.authUrl);
101
+ const escapedRedirectUri = escapeHtml(input.redirectUri);
66
102
  return `<!doctype html>
67
103
  <html>
68
104
  <head>
@@ -70,41 +106,217 @@ const getClientApprovalHtml = (input) => {
70
106
  <meta name="viewport" content="width=device-width, initial-scale=1" />
71
107
  <title>Approve Rivet MCP sign in</title>
72
108
  <style>
109
+ @font-face {
110
+ font-family: 'Satoshi';
111
+ src: url('/fonts/Satoshi-Variable.woff2') format('woff2-variations');
112
+ font-weight: 300 900;
113
+ font-style: normal;
114
+ font-display: swap;
115
+ }
116
+ :root {
117
+ color-scheme: dark;
118
+ --bg: #1c1c20;
119
+ --text: #ffffff;
120
+ --text-muted: #d1d5db;
121
+ --muted: #9ca3af;
122
+ --border: #232328;
123
+ --surface-hover: #404040;
124
+ --primary: #ff3300;
125
+ --primary-hover: #c2410c;
126
+ --ease: cubic-bezier(0.4, 0, 0.2, 1);
127
+ --shadow-panel:
128
+ inset 0 1px 0 rgba(255, 255, 255, 0.04),
129
+ inset 0 0 0 1px rgba(255, 255, 255, 0.06),
130
+ 0 24px 24px -12px rgba(0, 0, 0, 0.35),
131
+ 0 48px 48px -24px rgba(0, 0, 0, 0.35);
132
+ }
133
+ * {
134
+ box-sizing: border-box;
135
+ }
73
136
  body {
74
137
  margin: 0;
75
138
  min-height: 100vh;
76
- display: grid;
77
- place-items: center;
78
- font-family: system-ui, sans-serif;
79
- background: #0b0b0f;
80
- color: #fff;
139
+ font-family: 'Satoshi', 'Inter', system-ui, sans-serif;
140
+ background:
141
+ linear-gradient(
142
+ 180deg,
143
+ rgba(255, 255, 255, 0.018),
144
+ transparent 240px
145
+ ),
146
+ radial-gradient(
147
+ circle at 18% 0%,
148
+ rgba(255, 51, 0, 0.05),
149
+ transparent 32%
150
+ ),
151
+ var(--bg);
152
+ color: var(--text);
153
+ display: flex;
154
+ flex-direction: column;
155
+ align-items: center;
156
+ padding: 0;
157
+ -webkit-font-smoothing: antialiased;
158
+ -moz-osx-font-smoothing: grayscale;
81
159
  }
82
160
  main {
83
- max-width: 480px;
84
- padding: 32px;
161
+ width: 100%;
162
+ max-width: 920px;
163
+ padding: 28px 20px 96px;
164
+ }
165
+ .auth-shell {
166
+ display: flex;
167
+ flex-direction: column;
168
+ align-items: center;
169
+ justify-content: center;
170
+ min-height: 60vh;
171
+ width: 100%;
172
+ }
173
+ .auth-card {
174
+ background:
175
+ linear-gradient(
176
+ 180deg,
177
+ rgba(255, 255, 255, 0.025),
178
+ rgba(255, 255, 255, 0.01)
179
+ ),
180
+ var(--bg);
181
+ border: 1px solid rgba(255, 255, 255, 0.06);
182
+ border-radius: 24px;
183
+ padding: 40px;
184
+ width: 100%;
185
+ max-width: 400px;
186
+ display: flex;
187
+ flex-direction: column;
188
+ align-items: center;
189
+ gap: 32px;
190
+ backdrop-filter: blur(20px);
191
+ -webkit-backdrop-filter: blur(20px);
192
+ box-shadow: var(--shadow-panel);
193
+ }
194
+ .wordmark {
195
+ display: block;
196
+ width: auto;
197
+ height: 32px;
198
+ object-fit: contain;
199
+ }
200
+ .auth-copy {
201
+ text-align: center;
202
+ }
203
+ .auth-heading {
204
+ margin: 0 0 8px;
205
+ font-size: 20px;
206
+ font-weight: 500;
207
+ line-height: 1.2;
208
+ letter-spacing: 0;
209
+ color: var(--text);
85
210
  }
86
- code {
211
+ .auth-subtitle {
212
+ margin: 0;
213
+ font-size: 14px;
214
+ font-weight: 400;
215
+ line-height: 1.5;
216
+ color: var(--text-muted);
217
+ }
218
+ .auth-code {
219
+ display: block;
220
+ margin-top: 12px;
221
+ border-radius: 12px;
222
+ background: #17171b;
223
+ padding: 10px 12px;
224
+ color: var(--text);
225
+ font-size: 12px;
226
+ line-height: 1.5;
87
227
  word-break: break-all;
88
228
  }
89
- a {
90
- display: inline-block;
91
- margin-top: 20px;
92
- padding: 10px 14px;
93
- border-radius: 8px;
94
- background: #fff;
95
- color: #0b0b0f;
229
+ .auth-actions {
230
+ display: flex;
231
+ flex-direction: column;
232
+ align-items: stretch;
233
+ gap: 12px;
234
+ width: 100%;
235
+ }
236
+ .button-link {
237
+ display: inline-flex;
238
+ align-items: center;
239
+ justify-content: center;
240
+ gap: 8px;
241
+ min-height: 40px;
242
+ padding: 10px 18px;
243
+ border-radius: 10px;
244
+ font-family: inherit;
245
+ font-size: 14px;
246
+ font-weight: 500;
247
+ cursor: pointer;
248
+ transition:
249
+ background-color 160ms var(--ease),
250
+ border-color 160ms var(--ease),
251
+ color 160ms var(--ease),
252
+ transform 100ms ease;
253
+ border: 1px solid transparent;
96
254
  text-decoration: none;
97
- font-weight: 600;
255
+ color: var(--text);
256
+ background: rgba(255, 255, 255, 0.015);
257
+ backdrop-filter: blur(20px);
258
+ -webkit-backdrop-filter: blur(20px);
259
+ }
260
+ .button-link:hover {
261
+ background: rgba(255, 255, 255, 0.1);
262
+ }
263
+ .button-link:active {
264
+ transform: scale(0.98);
265
+ }
266
+ .auth-google-button {
267
+ min-height: 48px;
268
+ padding: 14px 32px;
269
+ gap: 12px;
270
+ border-radius: 12px;
271
+ background: rgba(255, 255, 255, 0.015);
272
+ border-color: rgba(255, 255, 255, 0.03);
273
+ color: var(--text);
274
+ font-size: 14px;
275
+ font-weight: 500;
276
+ }
277
+ .auth-google-button:hover {
278
+ background: rgba(255, 255, 255, 0.1);
279
+ }
280
+ .auth-google-icon {
281
+ width: 20px;
282
+ height: 20px;
283
+ filter: brightness(1.1);
284
+ flex: 0 0 20px;
98
285
  }
99
286
  </style>
287
+ <link rel="stylesheet" href="${DASHBOARD_AUTH_STYLESHEET_PATH}" />
100
288
  </head>
101
289
  <body>
102
290
  <main>
103
- <h1>Approve Rivet MCP sign in</h1>
104
- <p><strong>${escapeHtml(clientName)}</strong> wants to connect to Rivet.</p>
105
- <p>After sign-in, Rivet will return an OAuth code to:</p>
106
- <p><code>${escapeHtml(input.redirectUri)}</code></p>
107
- <a href="${escapeHtml(input.authUrl)}">Continue to Rivet sign in</a>
291
+ <section class="auth-shell">
292
+ <div class="auth-card" aria-labelledby="approval-title">
293
+ <img class="wordmark" src="/dashboard/assets/logo.png" alt="Rivet" />
294
+ <div class="auth-copy">
295
+ <h1 id="approval-title" class="auth-heading">Approve Rivet MCP sign in</h1>
296
+ <p class="auth-subtitle">
297
+ <strong>${escapeHtml(clientName)}</strong> wants to connect to Rivet.
298
+ </p>
299
+ <p class="auth-subtitle">After sign-in, Rivet will return an OAuth code to:</p>
300
+ <p class="auth-subtitle">
301
+ <code class="auth-code">${escapedRedirectUri}</code>
302
+ </p>
303
+ </div>
304
+ <div class="auth-actions">
305
+ <a
306
+ href="${escapedAuthUrl}"
307
+ class="button-link auth-google-button"
308
+ aria-label="Sign in with Google to approve Rivet MCP"
309
+ >
310
+ <img
311
+ class="auth-google-icon"
312
+ src="${GOOGLE_ICON_URL}"
313
+ alt="Google"
314
+ />
315
+ Sign in with Google
316
+ </a>
317
+ </div>
318
+ </div>
319
+ </section>
108
320
  </main>
109
321
  </body>
110
322
  </html>`;
@@ -130,14 +342,13 @@ const normalizeProfileUser = (user) => {
130
342
  * Supabase hash tokens to `completeBrowserCallback`. That completes the
131
343
  * proxy login, obtains the Rivet access token, and mints a single-use
132
344
  * authorization code bound to the client's PKCE challenge.
133
- * 3. `exchangeAuthorizationCode` returns the Rivet access token as the OAuth
134
- * access token; `verifyAccessToken` validates bearer tokens against the
135
- * proxy profile endpoint.
345
+ * 3. `exchangeAuthorizationCode` returns opaque MCP OAuth tokens; the Rivet
346
+ * proxy tokens stay server-side and are used only after provider validation.
136
347
  */
137
348
  class RivetOAuthServerProvider {
138
349
  clientsStore = new InMemoryClientsStore();
139
- proxyUrl;
140
- callbackUrl;
350
+ getProxyUrl;
351
+ getCallbackUrl;
141
352
  mcpEditor;
142
353
  persistTokens;
143
354
  isRefreshTokenCurrent;
@@ -149,22 +360,41 @@ class RivetOAuthServerProvider {
149
360
  verifiedTokens = new Map();
150
361
  revokedAccessTokens = new Set();
151
362
  revokedRefreshTokens = new Set();
152
- activeRefreshTokens = new Set();
363
+ activeRefreshTokens = new Map();
153
364
  constructor(options) {
154
- this.proxyUrl = options.proxyUrl.replace(/\/$/, '');
155
- this.callbackUrl = options.callbackUrl;
365
+ const { proxyUrl, callbackUrl } = options;
366
+ this.getProxyUrl =
367
+ typeof proxyUrl === 'function'
368
+ ? () => proxyUrl().replace(/\/$/, '')
369
+ : () => proxyUrl.replace(/\/$/, '');
370
+ this.getCallbackUrl =
371
+ typeof callbackUrl === 'function' ? callbackUrl : () => callbackUrl;
156
372
  this.mcpEditor = options.mcpEditor;
157
373
  this.persistTokens = options.persistTokens;
158
374
  this.isRefreshTokenCurrent = options.isRefreshTokenCurrent;
159
375
  this.fetchFn = options.fetchImpl ?? fetch;
160
376
  this.pollIntervalMs = options.pollIntervalMs ?? TOKEN_POLL_INTERVAL_MS;
161
377
  }
378
+ getBrowserAuthCookie(sessionId, nonce) {
379
+ const callbackUrl = new URL(this.getCallbackUrl());
380
+ const attributes = [
381
+ `${exports.MCP_BROWSER_AUTH_COOKIE_NAME}=${encodeURIComponent(getBrowserAuthCookieValue(sessionId, nonce))}`,
382
+ 'Path=/',
383
+ 'HttpOnly',
384
+ 'SameSite=Lax',
385
+ `Max-Age=${Math.ceil(PENDING_AUTH_TTL_MS / 1000)}`,
386
+ ];
387
+ if (callbackUrl.protocol === 'https:') {
388
+ attributes.push('Secure');
389
+ }
390
+ return attributes.join('; ');
391
+ }
162
392
  async authorize(client, params, res) {
163
- const response = await this.fetchFn(`${this.proxyUrl}/api/auth/google/start`, {
393
+ const response = await this.fetchFn(`${this.getProxyUrl()}/api/auth/google/start`, {
164
394
  method: 'POST',
165
395
  headers: { 'Content-Type': 'application/json' },
166
396
  body: JSON.stringify({
167
- redirectOrigin: this.callbackUrl,
397
+ redirectOrigin: this.getCallbackUrl(),
168
398
  authDestination: HOSTED_DASHBOARD_AUTH_URL,
169
399
  source: 'mcp',
170
400
  editor: this.mcpEditor,
@@ -182,13 +412,16 @@ class RivetOAuthServerProvider {
182
412
  throw new errors_js_1.ServerError('Failed to start Rivet sign-in');
183
413
  }
184
414
  this.prunePendingAuthorizations();
415
+ const browserNonce = (0, crypto_1.randomBytes)(32).toString('base64url');
185
416
  this.pendingAuthorizations.set(result.sessionId, {
186
417
  client,
187
418
  params,
188
419
  pollSecret: result.pollSecret,
189
420
  completionSecret: result.completionSecret,
421
+ browserNonce,
190
422
  expiresAt: Date.now() + PENDING_AUTH_TTL_MS,
191
423
  });
424
+ res.setHeader('Set-Cookie', this.getBrowserAuthCookie(result.sessionId, browserNonce));
192
425
  res.status(200).type('html').send(getClientApprovalHtml({
193
426
  authUrl: result.authUrl,
194
427
  clientName: client.client_name,
@@ -201,7 +434,7 @@ class RivetOAuthServerProvider {
201
434
  * single-use authorization code, and returns the MCP client redirect URL.
202
435
  */
203
436
  async completeBrowserCallback(input) {
204
- const { sessionId, accessToken, refreshToken } = input;
437
+ const { sessionId, accessToken, refreshToken, browserAuthCookie } = input;
205
438
  if (!sessionId || !accessToken) {
206
439
  throw new Error('OAuth callback missing session or token');
207
440
  }
@@ -210,7 +443,11 @@ class RivetOAuthServerProvider {
210
443
  if (!pending || pending.expiresAt < Date.now()) {
211
444
  throw new Error('Unknown or expired sign-in session');
212
445
  }
213
- const completeResponse = await this.fetchFn(`${this.proxyUrl}/api/auth/google/complete`, {
446
+ if (browserAuthCookie !==
447
+ getBrowserAuthCookieValue(sessionId, pending.browserNonce)) {
448
+ throw new Error('OAuth callback missing browser session');
449
+ }
450
+ const completeResponse = await this.fetchFn(`${this.getProxyUrl()}/api/auth/google/complete`, {
214
451
  method: 'POST',
215
452
  headers: { 'Content-Type': 'application/json' },
216
453
  body: JSON.stringify({
@@ -259,60 +496,90 @@ class RivetOAuthServerProvider {
259
496
  throw new errors_js_1.InvalidGrantError('redirect_uri does not match');
260
497
  }
261
498
  this.authorizationCodes.delete(authorizationCode);
262
- this.rememberIssuedBearerToken({
263
- token: entry.accessToken,
264
- refreshToken: entry.refreshToken,
499
+ const issuedTokens = this.rememberIssuedBearerToken({
500
+ proxyAccessToken: entry.accessToken,
501
+ proxyRefreshToken: entry.refreshToken,
265
502
  user: entry.user,
266
503
  });
267
- return this.toOAuthTokens(entry.accessToken, entry.refreshToken);
504
+ return this.toOAuthTokens(issuedTokens.accessToken, issuedTokens.refreshToken, issuedTokens.expiresIn);
268
505
  }
269
506
  async exchangeRefreshToken(_client, refreshToken) {
507
+ const activeRefreshToken = this.activeRefreshTokens.get(refreshToken);
270
508
  if (this.revokedRefreshTokens.has(refreshToken) ||
271
- !this.activeRefreshTokens.has(refreshToken) ||
272
- this.isRefreshTokenCurrent?.(refreshToken) === false) {
509
+ !activeRefreshToken ||
510
+ this.isRefreshTokenCurrent?.(activeRefreshToken.proxyRefreshToken) === false) {
273
511
  throw new errors_js_1.InvalidGrantError('Refresh token was revoked');
274
512
  }
275
513
  const refreshed = await (0, accessTokenRefresh_1.refreshAccessTokenViaProxy)({
276
- refreshToken,
277
- proxyUrl: this.proxyUrl,
514
+ refreshToken: activeRefreshToken.proxyRefreshToken,
515
+ proxyUrl: this.getProxyUrl(),
278
516
  fetchImpl: this.fetchFn,
279
517
  });
280
518
  if (!refreshed) {
281
519
  throw new errors_js_1.InvalidGrantError('Refresh token was rejected');
282
520
  }
283
- const nextRefreshToken = refreshed.refreshToken ?? refreshToken;
521
+ const nextProxyRefreshToken = refreshed.refreshToken ?? activeRefreshToken.proxyRefreshToken;
284
522
  try {
285
523
  this.persistTokens?.({
286
524
  token: refreshed.token,
287
- refreshToken: nextRefreshToken,
525
+ refreshToken: nextProxyRefreshToken,
288
526
  });
289
527
  }
290
528
  catch (error) {
291
529
  log.warn('Failed to persist refreshed Rivet tokens:', error);
292
530
  }
293
531
  this.activeRefreshTokens.delete(refreshToken);
294
- this.rememberIssuedBearerToken({
295
- token: refreshed.token,
296
- refreshToken: nextRefreshToken,
532
+ this.revokedRefreshTokens.add(refreshToken);
533
+ const issuedTokens = this.rememberIssuedBearerToken({
534
+ proxyAccessToken: refreshed.token,
535
+ proxyRefreshToken: nextProxyRefreshToken,
536
+ user: activeRefreshToken.user,
297
537
  });
298
- return this.toOAuthTokens(refreshed.token, nextRefreshToken);
538
+ return this.toOAuthTokens(issuedTokens.accessToken, issuedTokens.refreshToken, issuedTokens.expiresIn);
539
+ }
540
+ async revokeToken(_client, request) {
541
+ const { token } = request;
542
+ const knownAccessToken = this.issuedBearerTokens.get(token);
543
+ const knownRefreshToken = this.activeRefreshTokens.has(token);
544
+ if (knownAccessToken) {
545
+ this.revokeAccessToken(token);
546
+ return;
547
+ }
548
+ if (!knownRefreshToken) {
549
+ return;
550
+ }
551
+ this.revokedRefreshTokens.add(token);
552
+ this.activeRefreshTokens.delete(token);
553
+ for (const [accessToken, issuedToken] of this.issuedBearerTokens) {
554
+ if (issuedToken.refreshToken === token) {
555
+ this.revokedAccessTokens.add(accessToken);
556
+ this.issuedBearerTokens.delete(accessToken);
557
+ this.verifiedTokens.delete(accessToken);
558
+ }
559
+ }
299
560
  }
300
561
  async verifyAccessToken(token) {
301
562
  if (this.revokedAccessTokens.has(token)) {
302
563
  this.verifiedTokens.delete(token);
303
564
  throw new errors_js_1.InvalidTokenError('Access token was revoked');
304
565
  }
566
+ this.pruneIssuedBearerTokens();
567
+ const issuedToken = this.issuedBearerTokens.get(token);
568
+ if (!issuedToken) {
569
+ this.verifiedTokens.delete(token);
570
+ throw new errors_js_1.InvalidTokenError('Access token was not issued by this OAuth provider');
571
+ }
305
572
  const cached = this.verifiedTokens.get(token);
306
573
  if (cached && cached.cachedUntil > Date.now()) {
307
574
  return cached.authInfo;
308
575
  }
309
576
  let response;
310
577
  try {
311
- response = await this.fetchFn(`${this.proxyUrl}/api/user/profile`, {
578
+ response = await this.fetchFn(`${this.getProxyUrl()}/api/user/profile`, {
312
579
  method: 'GET',
313
580
  headers: {
314
581
  'Content-Type': 'application/json',
315
- Authorization: `Bearer ${token}`,
582
+ Authorization: `Bearer ${issuedToken.proxyAccessToken}`,
316
583
  },
317
584
  signal: AbortSignal.timeout(PROFILE_FETCH_TIMEOUT_MS),
318
585
  });
@@ -326,18 +593,18 @@ class RivetOAuthServerProvider {
326
593
  this.verifiedTokens.delete(token);
327
594
  throw new errors_js_1.InvalidTokenError('Rivet rejected the access token');
328
595
  }
329
- this.pruneIssuedBearerTokens();
330
- const issuedToken = this.issuedBearerTokens.get(token);
331
596
  const user = normalizeProfileUser(result.user) ?? issuedToken?.user;
332
- const secondsUntilExpiry = (0, accessTokenRefresh_1.getJwtSecondsUntilExpiry)(token) ?? DEFAULT_TOKEN_TTL_SECONDS;
597
+ const secondsUntilExpiry = (0, accessTokenRefresh_1.getJwtSecondsUntilExpiry)(issuedToken.proxyAccessToken) ??
598
+ DEFAULT_TOKEN_TTL_SECONDS;
333
599
  const authInfo = {
334
- token,
600
+ token: issuedToken.proxyAccessToken,
335
601
  clientId: 'rivet-mcp',
336
602
  scopes: [MCP_OAUTH_SCOPE],
337
603
  expiresAt: Math.floor(Date.now() / 1000) + Math.max(secondsUntilExpiry, 1),
338
604
  extra: {
339
- ...(issuedToken?.refreshToken
340
- ? { refreshToken: issuedToken.refreshToken }
605
+ mcpAccessToken: token,
606
+ ...(issuedToken.proxyRefreshToken
607
+ ? { refreshToken: issuedToken.proxyRefreshToken }
341
608
  : {}),
342
609
  ...(user ? { email: user.email, userId: user.id } : {}),
343
610
  },
@@ -366,14 +633,29 @@ class RivetOAuthServerProvider {
366
633
  }
367
634
  rememberIssuedBearerToken(tokens) {
368
635
  this.pruneIssuedBearerTokens();
369
- this.issuedBearerTokens.set(tokens.token, {
370
- refreshToken: tokens.refreshToken,
636
+ const accessToken = `rivet_mcp_at_${(0, crypto_1.randomBytes)(32).toString('base64url')}`;
637
+ const refreshToken = tokens.proxyRefreshToken
638
+ ? `rivet_mcp_rt_${(0, crypto_1.randomBytes)(32).toString('base64url')}`
639
+ : undefined;
640
+ const expiresAt = getTokenExpiryMs(tokens.proxyAccessToken);
641
+ this.issuedBearerTokens.set(accessToken, {
642
+ proxyAccessToken: tokens.proxyAccessToken,
643
+ proxyRefreshToken: tokens.proxyRefreshToken,
644
+ refreshToken,
371
645
  user: tokens.user,
372
- expiresAt: getTokenExpiryMs(tokens.token),
646
+ expiresAt,
373
647
  });
374
- if (tokens.refreshToken) {
375
- this.activeRefreshTokens.add(tokens.refreshToken);
648
+ if (refreshToken && tokens.proxyRefreshToken) {
649
+ this.activeRefreshTokens.set(refreshToken, {
650
+ proxyRefreshToken: tokens.proxyRefreshToken,
651
+ user: tokens.user,
652
+ });
376
653
  }
654
+ return {
655
+ accessToken,
656
+ refreshToken,
657
+ expiresIn: Math.max(Math.floor((expiresAt - Date.now()) / 1000), 1),
658
+ };
377
659
  }
378
660
  getValidCodeEntry(client, authorizationCode) {
379
661
  const entry = this.authorizationCodes.get(authorizationCode);
@@ -385,19 +667,18 @@ class RivetOAuthServerProvider {
385
667
  }
386
668
  return entry;
387
669
  }
388
- toOAuthTokens(accessToken, refreshToken) {
389
- const secondsUntilExpiry = (0, accessTokenRefresh_1.getJwtSecondsUntilExpiry)(accessToken) ?? DEFAULT_TOKEN_TTL_SECONDS;
670
+ toOAuthTokens(accessToken, refreshToken, expiresIn = DEFAULT_TOKEN_TTL_SECONDS) {
390
671
  return {
391
672
  access_token: accessToken,
392
673
  token_type: 'bearer',
393
- expires_in: Math.max(secondsUntilExpiry, 1),
674
+ expires_in: Math.max(expiresIn, 1),
394
675
  scope: MCP_OAUTH_SCOPE,
395
676
  ...(refreshToken ? { refresh_token: refreshToken } : {}),
396
677
  };
397
678
  }
398
679
  async pollForRivetTokens(sessionId, pollSecret) {
399
680
  for (let attempt = 0; attempt < TOKEN_POLL_MAX_ATTEMPTS; attempt++) {
400
- const response = await this.fetchFn(`${this.proxyUrl}/api/auth/google/token`, {
681
+ const response = await this.fetchFn(`${this.getProxyUrl()}/api/auth/google/token`, {
401
682
  method: 'POST',
402
683
  headers: { 'Content-Type': 'application/json' },
403
684
  body: JSON.stringify({ session: sessionId, pollSecret }),