@thoughtspot/mcp-auth 1.0.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.
@@ -0,0 +1,287 @@
1
+ import { encodeBase64Url } from "hono/utils/encode";
2
+ const DEFAULT_MCP_LOGO_URL = "https://raw.githubusercontent.com/thoughtspot/mcp-server/refs/heads/main/static/MCP%20Server%20Logo.svg";
3
+ const DEFAULT_INSTANCE_LOGO_URL = "https://avatars.githubusercontent.com/u/8906680?s=200&v=4";
4
+ const DEFAULT_TERMS_URL = "https://www.thoughtspot.com/legal/thoughtspot-for-apps";
5
+ const DEFAULT_PRIVACY_URL = "https://www.thoughtspot.com/privacy-statement";
6
+ const DEFAULT_SIGNUP_URL = "https://www.thoughtspot.com/trial";
7
+ export function renderApprovalDialog(request, options) {
8
+ const { serverInfo, state, client } = options;
9
+ const encodedState = btoa(JSON.stringify(state));
10
+ const serverName = sanitizeHtml(serverInfo.name);
11
+ const mcpLogoUrl = serverInfo.mcpLogoUrl ?? DEFAULT_MCP_LOGO_URL;
12
+ const instanceLogoUrl = serverInfo.instanceLogoUrl ?? DEFAULT_INSTANCE_LOGO_URL;
13
+ const termsUrl = serverInfo.termsUrl ?? DEFAULT_TERMS_URL;
14
+ const privacyUrl = serverInfo.privacyUrl ?? DEFAULT_PRIVACY_URL;
15
+ const signupUrl = serverInfo.signupUrl ?? DEFAULT_SIGNUP_URL;
16
+ const approvalTitle = serverInfo.approvalTitle ??
17
+ `${serverName} wants access<br>to your ThoughtSpot instance`;
18
+ const clientUrl = client?.clientUri;
19
+ const tsInstanceUrlMatch = clientUrl?.match(/x-ts-host:(.*)/);
20
+ const tsInstanceUrl = tsInstanceUrlMatch ? tsInstanceUrlMatch[1].trim() : "";
21
+ const htmlContent = `
22
+ <!DOCTYPE html>
23
+ <html lang="en">
24
+ <head>
25
+ <meta charset="UTF-8">
26
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
27
+ <title>${serverName} | Authorization Request</title>
28
+ <style>
29
+ html, body {
30
+ height: 100%;
31
+ margin: 0;
32
+ padding: 0;
33
+ background: #f6f7fa;
34
+ }
35
+ body {
36
+ min-height: 100vh;
37
+ display: flex;
38
+ align-items: center;
39
+ justify-content: center;
40
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
41
+ color: #111827;
42
+ }
43
+ .approval-card {
44
+ background: #fff;
45
+ border-radius: 18px;
46
+ box-shadow: 0 2px 16px 0 rgba(16,30,54,0.10), 0 1.5px 4px 0 rgba(16,30,54,0.06);
47
+ max-width: 520px;
48
+ width: 100%;
49
+ padding: 40px 32px 32px 32px;
50
+ box-sizing: border-box;
51
+ display: flex;
52
+ flex-direction: column;
53
+ align-items: center;
54
+ }
55
+ .approval-logos {
56
+ display: flex;
57
+ align-items: center;
58
+ justify-content: center;
59
+ margin-bottom: 32px;
60
+ gap: 40px;
61
+ }
62
+ .approval-logo {
63
+ width: 64px;
64
+ height: 64px;
65
+ object-fit: contain;
66
+ background: #fff;
67
+ border-radius: 12px;
68
+ box-shadow: 0 2px 8px rgba(0,0,0,0.04);
69
+ }
70
+ .approval-arrow {
71
+ width: 56px;
72
+ height: 32px;
73
+ display: flex;
74
+ align-items: center;
75
+ justify-content: center;
76
+ }
77
+ .approval-title {
78
+ font-size: 1.25rem;
79
+ font-weight: 700;
80
+ text-align: center;
81
+ margin: 0 0 32px 0;
82
+ line-height: 1.3;
83
+ }
84
+ .approval-form { width: 100%; display: flex; flex-direction: column; align-items: stretch; }
85
+ .form-group { margin-bottom: 28px; }
86
+ .form-group label { display: block; margin-bottom: 8px; font-weight: 400; font-size: 0.94rem; color: #111827; transition: color 0.2s; }
87
+ .form-group label.label-blue { color: #2563eb; }
88
+ .form-group label.label-red { color: #dc2626; }
89
+ .form-group input { width: 100%; padding: 14px 16px; border: 1.5px solid #d1d5db; border-radius: 8px; font-size: 0.94rem; background: #fff; box-sizing: border-box; transition: border-color 0.2s; }
90
+ .form-group input.input-blue { border-color: #2563eb; }
91
+ .form-group input.input-red { border-color: #dc2626; }
92
+ .approval-subtitle { font-weight: 600; font-size: 0.94rem; margin-bottom: 8px; margin-top: 0; }
93
+ .approval-permissions { margin: 0 0 32px 0; padding: 0 0 0 18px; list-style: disc; color: #111827; font-size: 0.94rem; }
94
+ .approval-permissions li { margin-bottom: 8px; line-height: 1.6; }
95
+ .approval-actions { display: flex; justify-content: space-between; gap: 16px; margin-bottom: 18px; }
96
+ .terms-checkbox { margin-bottom: 24px; display: flex; align-items: flex-start; gap: 8px; }
97
+ .terms-checkbox input[type="checkbox"] { margin-top: 3px; }
98
+ .terms-checkbox label { font-size: 0.94rem; line-height: 1.4; color: #111827; }
99
+ .terms-checkbox a { color: #2563eb; text-decoration: none; }
100
+ .terms-checkbox a:hover { text-decoration: underline; }
101
+ .button { flex: 1 1 0; padding: 12px 0; border-radius: 8px; font-weight: 500; font-size: 0.94rem; border: none; cursor: pointer; transition: background 0.2s, color 0.2s; }
102
+ .button-cancel { background: #f3f4f6; color: #6b7280; border: none; }
103
+ .button-cancel:hover { background: #e5e7eb; }
104
+ .button-allow { background: #2563eb; color: #fff; border: none; }
105
+ .button-allow:hover { background: #1a56db; }
106
+ .button-allow:disabled { background: #93c5fd; cursor: not-allowed; }
107
+ .approval-footer { text-align: center; font-size: 0.88rem; color: #111827; margin-top: 8px; }
108
+ .approval-footer a { color: #2563eb; text-decoration: none; margin-left: 0.25em; font-weight: 500; }
109
+ .approval-footer a:hover { text-decoration: underline; }
110
+ @media (max-width: 600px) {
111
+ .approval-card { padding: 18px 4vw 18px 4vw; max-width: 98vw; }
112
+ .approval-logos { gap: 18px; margin-bottom: 18px; }
113
+ .approval-title { font-size: 1.1rem; margin-bottom: 18px; }
114
+ }
115
+ </style>
116
+ </head>
117
+ <body>
118
+ <div class="approval-card">
119
+ <div class="approval-logos">
120
+ <img src="${mcpLogoUrl}" alt="MCP Server Logo" class="approval-logo">
121
+ <span class="approval-arrow">
122
+ <svg width="56" height="32" viewBox="0 0 56 32" fill="none" xmlns="http://www.w3.org/2000/svg">
123
+ <g opacity="0.25">
124
+ <line x1="8" y1="10" x2="48" y2="10" stroke="#6B7280" stroke-width="2.5" stroke-linecap="round"/>
125
+ <polyline points="44,6 48,10 44,14" fill="none" stroke="#6B7280" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
126
+ <line x1="48" y1="22" x2="8" y2="22" stroke="#6B7280" stroke-width="2.5" stroke-linecap="round"/>
127
+ <polyline points="12,18 8,22 12,26" fill="none" stroke="#6B7280" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
128
+ </g>
129
+ </svg>
130
+ </span>
131
+ <img src="${instanceLogoUrl}" alt="Instance Logo" class="approval-logo">
132
+ </div>
133
+ <div class="approval-title">${approvalTitle}</div>
134
+ <form class="approval-form" method="post" action="${new URL(request.url).pathname}" id="approvalForm" autocomplete="off" novalidate>
135
+ <div class="form-group">
136
+ <label for="instanceUrl" id="instanceUrlLabel">ThoughtSpot instance URL</label>
137
+ <input type="text" id="instanceUrl" name="instanceUrl" value="${tsInstanceUrl}" placeholder="https://your-instance.thoughtspot.cloud" autocomplete="off">
138
+ <input type="hidden" name="state" value="${encodedState}">
139
+ </div>
140
+ <div class="approval-subtitle">${serverName} will be able to:</div>
141
+ <ul class="approval-permissions">
142
+ <li>Read all ThoughtSpot data and content you have access to</li>
143
+ <li>Send data to the client you are connecting to</li>
144
+ </ul>
145
+ <div class="terms-checkbox">
146
+ <input type="checkbox" id="termsCheckbox" name="termsCheckbox" required>
147
+ <label for="termsCheckbox">
148
+ By checking this box, I acknowledge and agree that my use of this application is subject to the ThoughtSpot
149
+ <a href="${termsUrl}" target="_blank" rel="noopener noreferrer">Terms of Use</a>
150
+ and <a href="${privacyUrl}" target="_blank" rel="noopener noreferrer">Privacy Statement</a>.
151
+ </label>
152
+ </div>
153
+ <div class="approval-actions">
154
+ <button type="button" class="button button-cancel" onclick="window.history.back()">Cancel</button>
155
+ <button type="submit" class="button button-allow" id="allowButton" disabled>Allow</button>
156
+ </div>
157
+ </form>
158
+ <div class="approval-footer">
159
+ Don't have an account?
160
+ <a href="${signupUrl}" target="_blank" rel="noopener noreferrer">Sign up</a>
161
+ </div>
162
+ </div>
163
+ <script>
164
+ const input = document.getElementById('instanceUrl');
165
+ const label = document.getElementById('instanceUrlLabel');
166
+ const form = document.getElementById('approvalForm');
167
+ const termsCheckbox = document.getElementById('termsCheckbox');
168
+ const allowButton = document.getElementById('allowButton');
169
+
170
+ function setBlue() {
171
+ input.classList.add('input-blue');
172
+ input.classList.remove('input-red');
173
+ label.classList.add('label-blue');
174
+ label.classList.remove('label-red');
175
+ }
176
+ function setRed() {
177
+ input.classList.add('input-red');
178
+ input.classList.remove('input-blue');
179
+ label.classList.add('label-red');
180
+ label.classList.remove('label-blue');
181
+ }
182
+ function clearColors() {
183
+ input.classList.remove('input-blue', 'input-red');
184
+ label.classList.remove('label-blue', 'label-red');
185
+ }
186
+ function updateAllowButton() {
187
+ allowButton.disabled = !(input.value.trim() && termsCheckbox.checked);
188
+ }
189
+ input.addEventListener('input', function() {
190
+ if (input.value.trim()) { setBlue(); } else { clearColors(); }
191
+ updateAllowButton();
192
+ });
193
+ termsCheckbox.addEventListener('change', updateAllowButton);
194
+ form.addEventListener('submit', function(e) {
195
+ if (!input.value.trim()) {
196
+ e.preventDefault(); setRed(); input.focus();
197
+ } else if (!termsCheckbox.checked) {
198
+ e.preventDefault(); termsCheckbox.focus();
199
+ } else {
200
+ setBlue();
201
+ }
202
+ });
203
+ </script>
204
+ </body>
205
+ </html>
206
+ `;
207
+ return new Response(htmlContent, {
208
+ headers: { "Content-Type": "text/html; charset=utf-8" },
209
+ });
210
+ }
211
+ function decodeState(encodedState) {
212
+ try {
213
+ const decoded = atob(encodedState);
214
+ return JSON.parse(decoded);
215
+ }
216
+ catch (e) {
217
+ console.error("Error decoding state:", e);
218
+ throw new Error("Invalid state format");
219
+ }
220
+ }
221
+ export function validateAndSanitizeUrl(url) {
222
+ try {
223
+ const trimmedUrl = url.trim();
224
+ const urlWithProtocol = trimmedUrl.startsWith("http://") || trimmedUrl.startsWith("https://")
225
+ ? trimmedUrl
226
+ : `https://${trimmedUrl}`;
227
+ const parsedUrl = new URL(urlWithProtocol);
228
+ if (parsedUrl.protocol !== "https:") {
229
+ throw new Error(`Only HTTPS URLs are allowed, received: ${parsedUrl.protocol}`);
230
+ }
231
+ return parsedUrl.origin;
232
+ }
233
+ catch (e) {
234
+ if (e instanceof Error) {
235
+ throw new Error(`Invalid URL: ${e.message}`);
236
+ }
237
+ throw new Error("Invalid URL format");
238
+ }
239
+ }
240
+ export async function parseRedirectApproval(request) {
241
+ if (request.method !== "POST") {
242
+ throw new Error("Invalid request method. Expected POST.");
243
+ }
244
+ let state;
245
+ let clientId;
246
+ let instanceUrl;
247
+ try {
248
+ const formData = await request.formData();
249
+ const encodedState = formData.get("state");
250
+ const rawInstanceUrl = formData.get("instanceUrl");
251
+ if (typeof encodedState !== "string" || !encodedState) {
252
+ throw new Error("Missing or invalid 'state' in form data.");
253
+ }
254
+ state = decodeState(encodedState);
255
+ clientId = state?.oauthReqInfo?.clientId;
256
+ if (!clientId) {
257
+ throw new Error("Could not extract clientId from state object.");
258
+ }
259
+ if (!rawInstanceUrl) {
260
+ throw new Error("Missing instance URL");
261
+ }
262
+ instanceUrl = validateAndSanitizeUrl(rawInstanceUrl);
263
+ }
264
+ catch (e) {
265
+ console.error("Error processing form submission:", e);
266
+ throw new Error(`Failed to parse approval form: ${e instanceof Error ? e.message : String(e)}`);
267
+ }
268
+ return { state, instanceUrl };
269
+ }
270
+ function sanitizeHtml(unsafe) {
271
+ return unsafe
272
+ .replace(/&/g, "&amp;")
273
+ .replace(/</g, "&lt;")
274
+ .replace(/>/g, "&gt;")
275
+ .replace(/"/g, "&quot;")
276
+ .replace(/'/g, "&#039;");
277
+ }
278
+ export function buildSamlRedirectUrl(instanceUrl, oauthReqInfo, callbackOrigin) {
279
+ const redirectUrl = new URL("callosum/v1/saml/login", instanceUrl);
280
+ const targetURLPath = new URL("/callback", callbackOrigin);
281
+ targetURLPath.searchParams.append("instanceUrl", instanceUrl);
282
+ const encodedState = encodeBase64Url(new TextEncoder().encode(JSON.stringify(oauthReqInfo)).buffer);
283
+ targetURLPath.searchParams.append("oauthReqInfo", encodedState);
284
+ redirectUrl.searchParams.append("targetURLPath", targetURLPath.href);
285
+ return redirectUrl.toString();
286
+ }
287
+ //# sourceMappingURL=oauth-utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth-utils.js","sourceRoot":"","sources":["../src/oauth-utils.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ export declare const PUBLIC_ROUTES: {
2
+ readonly root: "/";
3
+ readonly authorize: "/authorize";
4
+ readonly callback: "/callback";
5
+ readonly storeToken: "/store-token";
6
+ readonly oauthToken: "/token";
7
+ readonly register: "/register";
8
+ readonly mcp: "/mcp";
9
+ readonly sse: "/sse";
10
+ readonly bearerMcp: "/bearer/mcp";
11
+ readonly bearerSse: "/bearer/sse";
12
+ readonly tokenMcp: "/token/mcp";
13
+ readonly tokenSse: "/token/sse";
14
+ };
15
+ export type PublicRoutes = typeof PUBLIC_ROUTES;
16
+ export declare const PUBLIC_ROUTE_PREFIXES: {
17
+ readonly bearer: "/bearer";
18
+ readonly token: "/token";
19
+ };
20
+ export type PublicRoutePrefixes = typeof PUBLIC_ROUTE_PREFIXES;
21
+ //# sourceMappingURL=routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,aAAa;;;;;;;;;;;;;CAahB,CAAC;AAEX,MAAM,MAAM,YAAY,GAAG,OAAO,aAAa,CAAC;AAEhD,eAAO,MAAM,qBAAqB;;;CAGxB,CAAC;AAEX,MAAM,MAAM,mBAAmB,GAAG,OAAO,qBAAqB,CAAC"}
package/dist/routes.js ADDED
@@ -0,0 +1,19 @@
1
+ export const PUBLIC_ROUTES = {
2
+ root: "/",
3
+ authorize: "/authorize",
4
+ callback: "/callback",
5
+ storeToken: "/store-token",
6
+ oauthToken: "/token",
7
+ register: "/register",
8
+ mcp: "/mcp",
9
+ sse: "/sse",
10
+ bearerMcp: "/bearer/mcp",
11
+ bearerSse: "/bearer/sse",
12
+ tokenMcp: "/token/mcp",
13
+ tokenSse: "/token/sse",
14
+ };
15
+ export const PUBLIC_ROUTE_PREFIXES = {
16
+ bearer: "/bearer",
17
+ token: "/token",
18
+ };
19
+ //# sourceMappingURL=routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.js","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,aAAa,GAAG;IAC5B,IAAI,EAAE,GAAG;IACT,SAAS,EAAE,YAAY;IACvB,QAAQ,EAAE,WAAW;IACrB,UAAU,EAAE,cAAc;IAC1B,UAAU,EAAE,QAAQ;IACpB,QAAQ,EAAE,WAAW;IACrB,GAAG,EAAE,MAAM;IACX,GAAG,EAAE,MAAM;IACX,SAAS,EAAE,aAAa;IACxB,SAAS,EAAE,aAAa;IACxB,QAAQ,EAAE,YAAY;IACtB,QAAQ,EAAE,YAAY;CACb,CAAC;AAIX,MAAM,CAAC,MAAM,qBAAqB,GAAG;IACpC,MAAM,EAAE,SAAS;IACjB,KAAK,EAAE,QAAQ;CACN,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { AssetsFetcher } from "./types";
2
+ export declare function renderTokenCallback(instanceUrl: string, oauthReqInfo: unknown, assets: AssetsFetcher, origin: string): Promise<string>;
3
+ //# sourceMappingURL=token-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token-utils.d.ts","sourceRoot":"","sources":["../src/token-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAE7C,wBAAsB,mBAAmB,CACxC,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,OAAO,EACrB,MAAM,EAAE,aAAa,EACrB,MAAM,EAAE,MAAM,GACZ,OAAO,CAAC,MAAM,CAAC,CAoEjB"}
@@ -0,0 +1,57 @@
1
+ export async function renderTokenCallback(instanceUrl, oauthReqInfo, assets, origin) {
2
+ const parsedOAuthReqInfo = typeof oauthReqInfo === "string" ? JSON.parse(oauthReqInfo) : oauthReqInfo;
3
+ const oauthReqInfoJson = JSON.stringify(parsedOAuthReqInfo);
4
+ try {
5
+ const htmlResponse = await assets.fetch(`${origin}/oauth-callback.html`);
6
+ if (!htmlResponse.ok) {
7
+ throw new Error("Failed to load oauth-callback.html");
8
+ }
9
+ let htmlContent = await htmlResponse.text();
10
+ const cssResponse = await assets.fetch(`${origin}/oauth-callback.css`);
11
+ if (!cssResponse.ok) {
12
+ throw new Error("Failed to load oauth-callback.css");
13
+ }
14
+ const css = await cssResponse.text();
15
+ const jsResponse = await assets.fetch(`${origin}/oauth-callback.js`);
16
+ if (!jsResponse.ok) {
17
+ throw new Error("Failed to load oauth-callback.js");
18
+ }
19
+ const js = await jsResponse.text();
20
+ htmlContent = htmlContent.replace("{{OAUTH_REQ_INFO}}", oauthReqInfoJson);
21
+ htmlContent = htmlContent.replace('<link rel="stylesheet" href="oauth-callback.css">', `<style>${css}</style>`);
22
+ htmlContent = htmlContent.replace('<script src="oauth-callback.js"></script>', `<script>window.INSTANCE_URL = '${instanceUrl}';</script>\n <script>${js}</script>`);
23
+ return htmlContent;
24
+ }
25
+ catch (error) {
26
+ console.error("Error loading static files:", error);
27
+ return `
28
+ <!DOCTYPE html>
29
+ <html>
30
+ <head>
31
+ <title>Error - Authorization</title>
32
+ <style>
33
+ body {
34
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
35
+ display: flex; justify-content: center; align-items: center;
36
+ height: 100vh; margin: 0; background-color: #f8f9fa; color: #2c3e50;
37
+ }
38
+ .container {
39
+ text-align: center; padding: 3rem; background: white;
40
+ border-radius: 12px; box-shadow: 0 4px 6px rgba(0,0,0,0.1);
41
+ max-width: 480px; width: 90%;
42
+ }
43
+ h2 { color: #dc3545; margin-bottom: 1rem; }
44
+ </style>
45
+ </head>
46
+ <body>
47
+ <div class="container">
48
+ <h2>Authorization Error</h2>
49
+ <p>Failed to load authorization page. Please try again or contact support.</p>
50
+ <p>Error: ${error instanceof Error ? error.message : "Unknown error"}</p>
51
+ </div>
52
+ </body>
53
+ </html>
54
+ `;
55
+ }
56
+ }
57
+ //# sourceMappingURL=token-utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token-utils.js","sourceRoot":"","sources":["../src/token-utils.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACxC,WAAmB,EACnB,YAAqB,EACrB,MAAqB,EACrB,MAAc;IAEd,MAAM,kBAAkB,GACvB,OAAO,YAAY,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC;IAC5E,MAAM,gBAAgB,GAAG,IAAI,CAAC,SAAS,CAAC,kBAAkB,CAAC,CAAC;IAE5D,IAAI,CAAC;QACJ,MAAM,YAAY,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC,GAAG,MAAM,sBAAsB,CAAC,CAAC;QACzE,IAAI,CAAC,YAAY,CAAC,EAAE,EAAE,CAAC;YACtB,MAAM,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAC;QACvD,CAAC;QACD,IAAI,WAAW,GAAG,MAAM,YAAY,CAAC,IAAI,EAAE,CAAC;QAE5C,MAAM,WAAW,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC,GAAG,MAAM,qBAAqB,CAAC,CAAC;QACvE,IAAI,CAAC,WAAW,CAAC,EAAE,EAAE,CAAC;YACrB,MAAM,IAAI,KAAK,CAAC,mCAAmC,CAAC,CAAC;QACtD,CAAC;QACD,MAAM,GAAG,GAAG,MAAM,WAAW,CAAC,IAAI,EAAE,CAAC;QAErC,MAAM,UAAU,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC,GAAG,MAAM,oBAAoB,CAAC,CAAC;QACrE,IAAI,CAAC,UAAU,CAAC,EAAE,EAAE,CAAC;YACpB,MAAM,IAAI,KAAK,CAAC,kCAAkC,CAAC,CAAC;QACrD,CAAC;QACD,MAAM,EAAE,GAAG,MAAM,UAAU,CAAC,IAAI,EAAE,CAAC;QAEnC,WAAW,GAAG,WAAW,CAAC,OAAO,CAAC,oBAAoB,EAAE,gBAAgB,CAAC,CAAC;QAE1E,WAAW,GAAG,WAAW,CAAC,OAAO,CAChC,mDAAmD,EACnD,UAAU,GAAG,UAAU,CACvB,CAAC;QAEF,WAAW,GAAG,WAAW,CAAC,OAAO,CAChC,2CAA2C,EAC3C,kCAAkC,WAAW,4BAA4B,EAAE,WAAW,CACtF,CAAC;QAEF,OAAO,WAAW,CAAC;IACpB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,OAAO,CAAC,KAAK,CAAC,6BAA6B,EAAE,KAAK,CAAC,CAAC;QACpD,OAAO;;;;;;;;;;;;;;;;;;;;;;;4BAuBmB,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,eAAe;;;;SAI3E,CAAC;IACT,CAAC;AACF,CAAC"}
@@ -0,0 +1,56 @@
1
+ import type { ClientInfo } from "@cloudflare/workers-oauth-provider";
2
+ /**
3
+ * Minimum props injected into the MCP server context after successful auth.
4
+ * Consumers can extend via `extendProps` hook.
5
+ */
6
+ export interface BaseProps {
7
+ accessToken: string;
8
+ instanceUrl: string;
9
+ clientName: ClientInfo | {
10
+ clientId: string;
11
+ clientName: string;
12
+ registrationDate: number;
13
+ } | null;
14
+ }
15
+ /**
16
+ * Branding for approval dialog + token callback pages.
17
+ */
18
+ export interface ServerInfo {
19
+ name: string;
20
+ logo?: string;
21
+ description?: string;
22
+ /** Logo for the approval dialog (left side). Defaults to MCP logo. */
23
+ mcpLogoUrl?: string;
24
+ /** Logo for the right side (target service). Defaults to ThoughtSpot logo. */
25
+ instanceLogoUrl?: string;
26
+ termsUrl?: string;
27
+ privacyUrl?: string;
28
+ signupUrl?: string;
29
+ /** Approval dialog title. Defaults to "<name> wants access to your ThoughtSpot instance". */
30
+ approvalTitle?: string;
31
+ }
32
+ export type AuthMetricName = "oauth_authorize_requests_total" | "oauth_authorize_submit_total" | "oauth_callback_total" | "oauth_store_token_total";
33
+ export type BearerAuthRouteGroup = "bearer_mcp" | "bearer_sse" | "token_mcp" | "token_sse";
34
+ export interface AuthHooks<P extends BaseProps = BaseProps> {
35
+ /** Called on each OAuth flow request with the response status. Use to record metrics. */
36
+ onAuthMetric?: (name: AuthMetricName, status: number, ctx: ExecutionContext, req: Request) => void;
37
+ /** Called on each bearer/token MCP request. */
38
+ onBearerMetric?: (status: number, ctx: ExecutionContext, req: Request, group: BearerAuthRouteGroup) => void;
39
+ /**
40
+ * Extend the props object set onto the execution context before MCP handler runs.
41
+ * Use to stamp custom fields (e.g. api version) from request headers/query params.
42
+ */
43
+ extendProps?: (req: Request, base: BaseProps) => P;
44
+ /**
45
+ * Optional span wrapper for tracing each auth handler invocation.
46
+ * Defaults to identity (no tracing).
47
+ */
48
+ wrapSpan?: <T>(name: string, fn: () => Promise<T>) => Promise<T>;
49
+ }
50
+ /**
51
+ * Pluggable static asset fetcher. Cloudflare Workers `env.ASSETS` satisfies this shape.
52
+ */
53
+ export interface AssetsFetcher {
54
+ fetch(input: string | URL | Request): Promise<Response>;
55
+ }
56
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oCAAoC,CAAC;AAErE;;;GAGG;AACH,MAAM,WAAW,SAAS;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EACP,UAAU,GACV;QACA,QAAQ,EAAE,MAAM,CAAC;QACjB,UAAU,EAAE,MAAM,CAAC;QACnB,gBAAgB,EAAE,MAAM,CAAC;KACxB,GACD,IAAI,CAAC;CACR;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sEAAsE;IACtE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,8EAA8E;IAC9E,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,6FAA6F;IAC7F,aAAa,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,MAAM,cAAc,GACvB,gCAAgC,GAChC,8BAA8B,GAC9B,sBAAsB,GACtB,yBAAyB,CAAC;AAE7B,MAAM,MAAM,oBAAoB,GAC7B,YAAY,GACZ,YAAY,GACZ,WAAW,GACX,WAAW,CAAC;AAEf,MAAM,WAAW,SAAS,CAAC,CAAC,SAAS,SAAS,GAAG,SAAS;IACzD,yFAAyF;IACzF,YAAY,CAAC,EAAE,CACd,IAAI,EAAE,cAAc,EACpB,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,gBAAgB,EACrB,GAAG,EAAE,OAAO,KACR,IAAI,CAAC;IACV,+CAA+C;IAC/C,cAAc,CAAC,EAAE,CAChB,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,gBAAgB,EACrB,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,oBAAoB,KACvB,IAAI,CAAC;IACV;;;OAGG;IACH,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,KAAK,CAAC,CAAC;IACnD;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;CACjE;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC7B,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,GAAG,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CACxD"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@thoughtspot/mcp-auth",
3
+ "version": "1.0.0",
4
+ "description": "Reusable ThoughtSpot OAuth + bearer auth layer for Cloudflare Workers MCP servers",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "./assets/*": "./assets/*"
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "assets",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsc",
23
+ "clean": "rm -rf dist",
24
+ "test": "vitest run",
25
+ "format": "biome format --write",
26
+ "lint": "biome lint",
27
+ "prepare": "npm run build"
28
+ },
29
+ "keywords": [
30
+ "thoughtspot",
31
+ "mcp",
32
+ "oauth",
33
+ "cloudflare",
34
+ "workers"
35
+ ],
36
+ "author": "ThoughtSpot",
37
+ "license": "ThoughtSpot End user license agreement",
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "git+https://github.com/thoughtspot/ts-mcp-auth.git"
41
+ },
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "peerDependencies": {
46
+ "@cloudflare/workers-oauth-provider": "^0.0.5",
47
+ "hono": "^4.12.21"
48
+ },
49
+ "devDependencies": {
50
+ "@biomejs/biome": "^1.9.4",
51
+ "@cloudflare/workers-oauth-provider": "^0.0.5",
52
+ "@cloudflare/workers-types": "^4.20250612.0",
53
+ "@types/node": "^22.15.3",
54
+ "hono": "^4.12.21",
55
+ "typescript": "^5.8.3",
56
+ "vitest": "^3.1.2"
57
+ }
58
+ }