rivet-design 0.11.0 → 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.
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +25 -9
- package/dist/index.js.map +1 -1
- package/dist/mcp/agent-variants/areNaSourceContext.d.ts +21 -0
- package/dist/mcp/agent-variants/areNaSourceContext.d.ts.map +1 -0
- package/dist/mcp/agent-variants/areNaSourceContext.js +127 -0
- package/dist/mcp/agent-variants/areNaSourceContext.js.map +1 -0
- package/dist/mcp/agent-variants/createZeroToOneTool.d.ts.map +1 -1
- package/dist/mcp/agent-variants/createZeroToOneTool.js +43 -4
- package/dist/mcp/agent-variants/createZeroToOneTool.js.map +1 -1
- package/dist/mcp/agent-variants/pinterestSourceContext.d.ts +4 -0
- package/dist/mcp/agent-variants/pinterestSourceContext.d.ts.map +1 -1
- package/dist/mcp/agent-variants/pinterestSourceContext.js +7 -0
- package/dist/mcp/agent-variants/pinterestSourceContext.js.map +1 -1
- package/dist/mcp/agent-variants/tools.d.ts +6 -1
- package/dist/mcp/agent-variants/tools.d.ts.map +1 -1
- package/dist/mcp/agent-variants/tools.js.map +1 -1
- package/dist/mcp/auth/httpOAuthProvider.d.ts +12 -12
- package/dist/mcp/auth/httpOAuthProvider.d.ts.map +1 -1
- package/dist/mcp/auth/httpOAuthProvider.js +348 -67
- package/dist/mcp/auth/httpOAuthProvider.js.map +1 -1
- package/dist/mcp/auth/tools.d.ts +1 -0
- package/dist/mcp/auth/tools.d.ts.map +1 -1
- package/dist/mcp/auth/tools.js +31 -10
- package/dist/mcp/auth/tools.js.map +1 -1
- package/dist/mcp/hostedServer.d.ts +13 -0
- package/dist/mcp/hostedServer.d.ts.map +1 -0
- package/dist/mcp/hostedServer.js +46 -0
- package/dist/mcp/hostedServer.js.map +1 -0
- package/dist/mcp/httpServer.d.ts +23 -3
- package/dist/mcp/httpServer.d.ts.map +1 -1
- package/dist/mcp/httpServer.js +320 -56
- package/dist/mcp/httpServer.js.map +1 -1
- package/dist/mcp/integrations/tools.d.ts +6 -5
- package/dist/mcp/integrations/tools.d.ts.map +1 -1
- package/dist/mcp/integrations/tools.js +39 -5
- package/dist/mcp/integrations/tools.js.map +1 -1
- package/dist/mcp/server.d.ts +4 -8
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +56 -35
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/types.d.ts +7 -0
- package/dist/mcp/types.d.ts.map +1 -0
- package/dist/mcp/types.js +3 -0
- package/dist/mcp/types.js.map +1 -0
- package/dist/services/AuthService.d.ts +15 -1
- package/dist/services/AuthService.d.ts.map +1 -1
- package/dist/services/AuthService.js +176 -8
- package/dist/services/AuthService.js.map +1 -1
- package/dist/services/ConfigManager.d.ts +2 -0
- package/dist/services/ConfigManager.d.ts.map +1 -1
- package/dist/services/ConfigManager.js +21 -7
- package/dist/services/ConfigManager.js.map +1 -1
- package/dist/services/IntegrationsClient.d.ts +53 -0
- package/dist/services/IntegrationsClient.d.ts.map +1 -1
- package/dist/services/IntegrationsClient.js +51 -4
- package/dist/services/IntegrationsClient.js.map +1 -1
- package/dist/services/RequestAuthContext.d.ts +1 -0
- package/dist/services/RequestAuthContext.d.ts.map +1 -1
- package/dist/services/RequestAuthContext.js.map +1 -1
- package/package.json +1 -1
- package/src/ui/dist/assets/{main-Cw6Pd8ye.js → main-4_VncEaM.js} +2 -2
- 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(
|
|
33
|
-
throw new errors_js_1.InvalidClientMetadataError('Rivet MCP OAuth clients must
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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
|
-
|
|
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
|
-
|
|
90
|
-
display:
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
|
134
|
-
*
|
|
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
|
-
|
|
140
|
-
|
|
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
|
|
363
|
+
activeRefreshTokens = new Map();
|
|
153
364
|
constructor(options) {
|
|
154
|
-
|
|
155
|
-
this.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
264
|
-
|
|
499
|
+
const issuedTokens = this.rememberIssuedBearerToken({
|
|
500
|
+
proxyAccessToken: entry.accessToken,
|
|
501
|
+
proxyRefreshToken: entry.refreshToken,
|
|
265
502
|
user: entry.user,
|
|
266
503
|
});
|
|
267
|
-
return this.toOAuthTokens(
|
|
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
|
-
!
|
|
272
|
-
this.isRefreshTokenCurrent?.(
|
|
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.
|
|
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
|
|
521
|
+
const nextProxyRefreshToken = refreshed.refreshToken ?? activeRefreshToken.proxyRefreshToken;
|
|
284
522
|
try {
|
|
285
523
|
this.persistTokens?.({
|
|
286
524
|
token: refreshed.token,
|
|
287
|
-
refreshToken:
|
|
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.
|
|
295
|
-
|
|
296
|
-
|
|
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(
|
|
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.
|
|
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 ${
|
|
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)(
|
|
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
|
-
|
|
340
|
-
|
|
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
|
-
|
|
370
|
-
|
|
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
|
|
646
|
+
expiresAt,
|
|
373
647
|
});
|
|
374
|
-
if (tokens.
|
|
375
|
-
this.activeRefreshTokens.
|
|
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(
|
|
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.
|
|
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 }),
|