@taskforcehq/taskforce 0.3.317 → 0.3.319

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.
@@ -41,6 +41,7 @@ export declare function writeJsonErrorIfPossible(res: http.ServerResponse, statu
41
41
  export declare function applyCloudProxyForwardedHeaders(headers: Headers, req: http.IncomingMessage): void;
42
42
  export declare function rewriteProxySetCookieForLocalHost(cookie: string): string;
43
43
  export declare function shouldRewriteProxySetCookieDomain(cookie: string, hostHeader: string | undefined): boolean;
44
+ export declare function shouldForwardCloudProxyResponseHeader(name: string): boolean;
44
45
  export declare function shouldApplyAuthRateLimitForPath(pathname: string): boolean;
45
46
  export declare function resolveCloudProxyBaseUrl(env?: NodeJS.ProcessEnv): string;
46
47
  export declare function ensureBillingWebhookAuthExclusion(excludedPaths: string[]): string[];
@@ -84,6 +84,10 @@ const CORS_RESPONSE_HEADERS = new Set([
84
84
  'access-control-max-age',
85
85
  'vary'
86
86
  ]);
87
+ const CLOUD_PROXY_BODY_HEADERS = new Set([
88
+ 'content-encoding',
89
+ 'content-length'
90
+ ]);
87
91
  const REFRESH_DIAGNOSTIC_PATH_PREFIXES = [
88
92
  '/api/taskforce/auth/session',
89
93
  '/api/taskforce/config',
@@ -337,6 +341,18 @@ export function shouldRewriteProxySetCookieDomain(cookie, hostHeader) {
337
341
  return false;
338
342
  return !requestHost.endsWith(`.${cookieDomain}`);
339
343
  }
344
+ export function shouldForwardCloudProxyResponseHeader(name) {
345
+ const normalizedName = String(name || '').trim().toLowerCase();
346
+ if (!normalizedName)
347
+ return false;
348
+ if (HOP_BY_HOP_HEADERS.has(normalizedName))
349
+ return false;
350
+ if (CORS_RESPONSE_HEADERS.has(normalizedName))
351
+ return false;
352
+ if (CLOUD_PROXY_BODY_HEADERS.has(normalizedName))
353
+ return false;
354
+ return normalizedName !== 'set-cookie';
355
+ }
340
356
  async function readRequestBody(req, maxBytes) {
341
357
  const chunks = [];
342
358
  let total = 0;
@@ -685,12 +701,7 @@ export function createStandaloneServer(options) {
685
701
  });
686
702
  const outgoingHeaders = {};
687
703
  for (const [key, value] of upstream.headers.entries()) {
688
- const name = key.toLowerCase();
689
- if (HOP_BY_HOP_HEADERS.has(name))
690
- continue;
691
- if (CORS_RESPONSE_HEADERS.has(name))
692
- continue;
693
- if (name === 'set-cookie')
704
+ if (!shouldForwardCloudProxyResponseHeader(key))
694
705
  continue;
695
706
  outgoingHeaders[key] = value;
696
707
  }
@@ -259,11 +259,19 @@ export function registerAdminRoutes(deps) {
259
259
  }
260
260
  };
261
261
  const renderMcpOauthAuthorizePage = (input) => {
262
+ const scopeLabels = {
263
+ 'tasks:read': 'Read tasks',
264
+ 'tasks:write': 'Update tasks',
265
+ 'workspace:read': 'Read workspace details'
266
+ };
262
267
  const options = input.workspaces.map((workspace) => {
263
268
  const selected = workspace.id === input.preselectedWorkspaceId ? ' selected' : '';
264
269
  return `<option value="${escapeHtml(workspace.id)}"${selected}>${escapeHtml(workspace.name)} (${escapeHtml(workspace.role)})</option>`;
265
270
  }).join('');
266
- const scopeBadges = input.scopes.map((scope) => `<span style="display:inline-flex;padding:6px 10px;border-radius:999px;background:#272742;border:1px solid rgba(255,255,255,0.09);font-size:12px;">${escapeHtml(scope)}</span>`).join('');
271
+ const scopeBadges = input.scopes.map((scope) => {
272
+ const label = scopeLabels[scope] || scope;
273
+ return `<span class="scope-chip">${escapeHtml(label)}</span>`;
274
+ }).join('');
267
275
  const termsUrl = 'https://taskforcehq.ai/legal/terms/';
268
276
  const privacyUrl = 'https://taskforcehq.ai/legal/privacy/';
269
277
  return `<!doctype html>
@@ -272,41 +280,65 @@ export function registerAdminRoutes(deps) {
272
280
  <meta charset="utf-8" />
273
281
  <meta name="viewport" content="width=device-width, initial-scale=1" />
274
282
  <title>Authorize Taskforce MCP Connector</title>
283
+ <link rel="stylesheet" href="/styles/fonts.css" />
275
284
  <style>
276
285
  :root { color-scheme: dark; }
277
- body { margin: 0; font-family: Inter, system-ui, sans-serif; background: #111126; color: #f8f7ff; }
278
- .wrap { min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 24px; }
279
- .card { width: min(560px, 100%); background: rgba(22,22,44,0.95); border: 1px solid rgba(255,255,255,0.08); border-radius: 20px; box-shadow: 0 20px 80px rgba(0,0,0,0.35); padding: 28px; }
280
- h1 { margin: 0 0 8px; font-size: 24px; }
281
- p { margin: 0 0 16px; color: rgba(255,255,255,0.72); line-height: 1.45; }
282
- label { display: block; font-size: 13px; font-weight: 600; margin: 18px 0 8px; color: rgba(255,255,255,0.9); }
283
- select { width: 100%; border-radius: 14px; border: 1px solid rgba(255,255,255,0.1); background: #1a1a34; color: #fff; padding: 12px 14px; font-size: 14px; }
284
- .scopes { display:flex; flex-wrap:wrap; gap:8px; margin-top:8px; }
285
- .meta { margin-top: 18px; padding: 14px 16px; border-radius: 14px; background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); }
286
- .meta p { margin: 10px 0 0; font-size: 13px; color: rgba(255,255,255,0.68); }
287
- .error { margin-bottom: 16px; padding: 12px 14px; border-radius: 12px; background: rgba(255,99,132,0.12); color: #ffb6c6; border: 1px solid rgba(255,99,132,0.25); }
288
- .actions { display:flex; justify-content:flex-end; gap:12px; margin-top: 24px; }
289
- button { border-radius: 12px; padding: 11px 16px; font-size: 14px; font-weight: 600; cursor: pointer; border: 1px solid rgba(255,255,255,0.08); }
290
- button[name="decision"][value="deny"] { background: transparent; color: rgba(255,255,255,0.85); }
291
- button[name="decision"][value="approve"] { background: #8d6bff; color: white; border-color: rgba(141,107,255,0.6); }
292
- a { color: #b9d6ff; }
286
+ body { margin: 0; font-family: "Outfit", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: radial-gradient(circle at top, rgba(255,138,0,0.08), transparent 30%), #111126; color: #f8f7ff; }
287
+ .wrap { min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 32px 24px; }
288
+ .card { width: min(680px, 100%); background: rgba(22,22,44,0.95); border: 1px solid rgba(255,255,255,0.08); border-radius: 36px; box-shadow: 0 24px 80px rgba(0,0,0,0.35); padding: 44px 56px; }
289
+ .eyebrow { display: inline-flex; align-items: center; gap: 10px; margin-bottom: 18px; color: rgba(255,255,255,0.55); font-size: 12px; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; }
290
+ .eyebrow-mark { width: 10px; height: 10px; border-radius: 999px; background: linear-gradient(135deg, #ff8a00 0%, #8d6bff 100%); box-shadow: 0 0 18px rgba(255,138,0,0.35); }
291
+ h1 { margin: 0; font-size: clamp(2.25rem, 5vw, 3rem); line-height: 0.96; letter-spacing: -0.04em; color: #f8f7ff; }
292
+ .brand-word { font-family: "Russo One", sans-serif; font-size: 0.94em; letter-spacing: -0.03em; }
293
+ .lede { margin: 18px 0 8px; font-size: 1.15rem; line-height: 1.5; color: rgba(255,255,255,0.78); max-width: 34ch; }
294
+ .lede strong { color: #fff; }
295
+ .user-meta { margin: 0 0 28px; color: rgba(255,255,255,0.5); font-size: 0.9rem; }
296
+ label { display: block; font-size: 13px; font-weight: 700; margin: 24px 0 10px; color: rgba(255,255,255,0.92); letter-spacing: 0.01em; }
297
+ select { width: 100%; border-radius: 18px; border: 1px solid rgba(255,255,255,0.1); background: #1f2039; color: #fff; padding: 16px 18px; font-size: 15px; font-weight: 600; box-shadow: inset 0 1px 0 rgba(255,255,255,0.02); }
298
+ .scopes { display:flex; flex-wrap:wrap; gap:10px; margin-top:8px; }
299
+ .scope-chip { display:inline-flex; padding:8px 14px; border-radius:999px; background:#2a2947; border:1px solid rgba(255,255,255,0.09); font-size:13px; font-weight:600; color:rgba(255,255,255,0.92); }
300
+ .details { margin-top: 24px; padding: 0; border: 1px solid rgba(255,255,255,0.08); border-radius: 18px; background: rgba(255,255,255,0.04); overflow: hidden; }
301
+ .details summary { list-style: none; cursor: pointer; padding: 16px 18px; font-size: 0.92rem; font-weight: 700; color: rgba(255,255,255,0.86); }
302
+ .details summary::-webkit-details-marker { display: none; }
303
+ .details-body { padding: 0 18px 18px; color: rgba(255,255,255,0.68); font-size: 0.9rem; line-height: 1.5; }
304
+ .details-row { margin: 0 0 8px; }
305
+ .details-row strong { color: rgba(255,255,255,0.92); }
306
+ .legal { margin: 24px 2px 0; color: rgba(255,255,255,0.58); font-size: 0.9rem; line-height: 1.55; max-width: 56ch; }
307
+ .error { margin-bottom: 18px; padding: 12px 14px; border-radius: 14px; background: rgba(255,99,132,0.12); color: #ffb6c6; border: 1px solid rgba(255,99,132,0.25); }
308
+ .actions { display:flex; justify-content:flex-end; gap:14px; margin-top: 36px; }
309
+ button { border-radius: 18px; padding: 15px 22px; font-size: 15px; font-weight: 700; cursor: pointer; border: 1px solid rgba(255,255,255,0.08); min-width: 156px; transition: transform 160ms ease, box-shadow 160ms ease, border-color 160ms ease, background-color 160ms ease; }
310
+ button:hover { transform: translateY(-1px); }
311
+ button[name="decision"][value="deny"] { background: transparent; color: rgba(255,255,255,0.88); border-color: rgba(255,255,255,0.12); }
312
+ button[name="decision"][value="approve"] { background: linear-gradient(135deg, #8d6bff 0%, #7c59f1 100%); color: white; border-color: rgba(141,107,255,0.6); box-shadow: 0 14px 28px rgba(141,107,255,0.26); }
313
+ a { color: #cbd8ff; }
314
+ @media (max-width: 720px) {
315
+ .card { padding: 30px 22px; border-radius: 28px; }
316
+ h1 { font-size: 2rem; }
317
+ .actions { flex-direction: column-reverse; }
318
+ button { width: 100%; }
319
+ }
293
320
  </style>
294
321
  </head>
295
322
  <body>
296
323
  <div class="wrap">
297
324
  <form class="card" method="post" action="/oauth/mcp/authorize">
298
- <h1>Authorize Taskforce Connector</h1>
299
- <p>${escapeHtml(input.clientName)} wants access to Taskforce MCP for ${escapeHtml(input.userName)}.</p>
325
+ <div class="eyebrow"><span class="eyebrow-mark"></span>Taskforce MCP</div>
326
+ <h1>Authorize <span class="brand-word">Taskforce</span> Connector</h1>
327
+ <p class="lede"><strong>${escapeHtml(input.clientName)}</strong> wants to connect to Taskforce for <strong>${escapeHtml(input.userName)}</strong>.</p>
328
+ ${input.userMeta ? `<p class="user-meta">Signed in as ${escapeHtml(input.userMeta)}</p>` : ''}
300
329
  ${input.error ? `<div class="error">${escapeHtml(input.error)}</div>` : ''}
301
330
  <label for="workspaceId">Workspace</label>
302
331
  <select id="workspaceId" name="workspaceId" required>${options}</select>
303
- <label>Requested Scopes</label>
332
+ <label>Requested Permissions</label>
304
333
  <div class="scopes">${scopeBadges}</div>
305
- <div class="meta">
306
- <div><strong>Client:</strong> ${escapeHtml(input.clientName)}</div>
307
- <div><strong>Redirect URI:</strong> ${escapeHtml(input.redirectUri)}</div>
308
- <p>By authorizing this connector, you agree to Taskforce&apos;s <a href="${termsUrl}" target="_blank" rel="noopener noreferrer">Terms of Service</a> and acknowledge the <a href="${privacyUrl}" target="_blank" rel="noopener noreferrer">Privacy Policy</a>.</p>
309
- </div>
334
+ <details class="details">
335
+ <summary>Technical details</summary>
336
+ <div class="details-body">
337
+ <div class="details-row"><strong>Client:</strong> ${escapeHtml(input.clientName)}</div>
338
+ <div class="details-row"><strong>Redirect URI:</strong> ${escapeHtml(input.redirectUri)}</div>
339
+ </div>
340
+ </details>
341
+ <p class="legal">By authorizing this connector, you agree to Taskforce&apos;s <a href="${termsUrl}" target="_blank" rel="noopener noreferrer">Terms of Service</a> and acknowledge the <a href="${privacyUrl}" target="_blank" rel="noopener noreferrer">Privacy Policy</a>.</p>
310
342
  <input type="hidden" name="client_id" value="${escapeHtml(input.clientId)}" />
311
343
  <input type="hidden" name="redirect_uri" value="${escapeHtml(input.redirectUri)}" />
312
344
  <input type="hidden" name="scope" value="${escapeHtml(input.scopes.join(' '))}" />
@@ -2481,13 +2513,15 @@ export function registerAdminRoutes(deps) {
2481
2513
  addRoute('POST', '/mcp/', async (req, res) => { await handleCloudMcpRequest(req, res); });
2482
2514
  addRoute('GET', '/mcp', async (req, res) => { await handleCloudMcpRequest(req, res); });
2483
2515
  addRoute('GET', '/mcp/', async (req, res) => { await handleCloudMcpRequest(req, res); });
2516
+ addRoute('HEAD', '/mcp', async (req, res) => { await handleCloudMcpRequest(req, res); });
2517
+ addRoute('HEAD', '/mcp/', async (req, res) => { await handleCloudMcpRequest(req, res); });
2484
2518
  addRoute('DELETE', '/mcp', async (req, res) => { await handleCloudMcpRequest(req, res); });
2485
2519
  addRoute('DELETE', '/mcp/', async (req, res) => { await handleCloudMcpRequest(req, res); });
2486
2520
  addRoute('OPTIONS', '/mcp', async (_req, res) => {
2487
2521
  res.writeHead(204, {
2488
2522
  'Access-Control-Allow-Origin': '*',
2489
2523
  'Access-Control-Allow-Headers': 'authorization, content-type, accept, last-event-id, mcp-protocol-version, mcp-session-id, x-taskforce-workspace-id, x-taskforce-ai-profile-token, x-taskforce-ai-name, x-taskforce-ai-icon, x-taskforce-ai-color, x-taskforce-ai-description, x-taskforce-ai-role, x-taskforce-ai-provider, x-taskforce-ai-model, x-taskforce-ai-surface-type',
2490
- 'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS'
2524
+ 'Access-Control-Allow-Methods': 'GET, HEAD, POST, DELETE, OPTIONS'
2491
2525
  });
2492
2526
  res.end();
2493
2527
  });
@@ -2495,7 +2529,7 @@ export function registerAdminRoutes(deps) {
2495
2529
  res.writeHead(204, {
2496
2530
  'Access-Control-Allow-Origin': '*',
2497
2531
  'Access-Control-Allow-Headers': 'authorization, content-type, accept, last-event-id, mcp-protocol-version, mcp-session-id, x-taskforce-workspace-id, x-taskforce-ai-profile-token, x-taskforce-ai-name, x-taskforce-ai-icon, x-taskforce-ai-color, x-taskforce-ai-description, x-taskforce-ai-role, x-taskforce-ai-provider, x-taskforce-ai-model, x-taskforce-ai-surface-type',
2498
- 'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS'
2532
+ 'Access-Control-Allow-Methods': 'GET, HEAD, POST, DELETE, OPTIONS'
2499
2533
  });
2500
2534
  res.end();
2501
2535
  });
@@ -2515,6 +2549,21 @@ export function registerAdminRoutes(deps) {
2515
2549
  scopes_supported: ['tasks:read', 'tasks:write', 'workspace:read'],
2516
2550
  }));
2517
2551
  });
2552
+ addRoute('HEAD', '/.well-known/oauth-authorization-server', async (req, res) => {
2553
+ const issuer = resolveMcpOauthIssuer(req);
2554
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2555
+ res.end(JSON.stringify({
2556
+ issuer,
2557
+ authorization_endpoint: resolveMcpOauthAuthorizeUrl(req),
2558
+ token_endpoint: resolveMcpOauthTokenUrl(req),
2559
+ registration_endpoint: resolveMcpOauthRegisterUrl(req),
2560
+ response_types_supported: ['code'],
2561
+ grant_types_supported: ['authorization_code'],
2562
+ token_endpoint_auth_methods_supported: ['none'],
2563
+ code_challenge_methods_supported: ['S256'],
2564
+ scopes_supported: ['tasks:read', 'tasks:write', 'workspace:read'],
2565
+ }));
2566
+ });
2518
2567
  addRoute('GET', '/.well-known/oauth-protected-resource', async (req, res) => {
2519
2568
  const issuer = resolveMcpOauthIssuer(req);
2520
2569
  res.writeHead(200, { 'Content-Type': 'application/json' });
@@ -2524,6 +2573,15 @@ export function registerAdminRoutes(deps) {
2524
2573
  bearer_methods_supported: ['header'],
2525
2574
  }));
2526
2575
  });
2576
+ addRoute('HEAD', '/.well-known/oauth-protected-resource', async (req, res) => {
2577
+ const issuer = resolveMcpOauthIssuer(req);
2578
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2579
+ res.end(JSON.stringify({
2580
+ resource: resolveMcpOauthResourceUrl(req),
2581
+ authorization_servers: [issuer],
2582
+ bearer_methods_supported: ['header'],
2583
+ }));
2584
+ });
2527
2585
  addRoute('POST', '/oauth/mcp/register', async (req, res) => {
2528
2586
  if (!ensureAppAccess(req, res))
2529
2587
  return;
@@ -2606,8 +2664,57 @@ export function registerAdminRoutes(deps) {
2606
2664
  if (workspaces.length === 0)
2607
2665
  throw new TaskforceRuleError('No accessible workspaces found for this user', 403, 'MCP_OAUTH_WORKSPACE_REQUIRED');
2608
2666
  const preferredWorkspaceId = workspaces.find((workspace) => workspace.id === String(access.workspaceId || '').trim())?.id || workspaces[0].id;
2667
+ const userIdentity = core.resolveUserIdentity(access.userId);
2668
+ const userName = String(userIdentity?.displayName || userIdentity?.email || access.userId).trim() || String(access.userId);
2669
+ const userMeta = userIdentity?.email && userIdentity.email !== userName ? String(userIdentity.email).trim() : null;
2670
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
2671
+ res.end(renderMcpOauthAuthorizePage({ userName, userMeta, workspaces, preselectedWorkspaceId: preferredWorkspaceId, clientName: clientRecord.clientName, clientId: clientRecord.id, redirectUri, scopes, state, codeChallenge, codeChallengeMethod }));
2672
+ }
2673
+ catch (error) {
2674
+ const statusCode = resolveErrorStatusCode(error);
2675
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
2676
+ res.end(JSON.stringify({ error: 'invalid_request', error_description: error.message, code: error?.code || 'MCP_OAUTH_AUTHORIZE_FAILED' }));
2677
+ }
2678
+ });
2679
+ addRoute('HEAD', '/oauth/mcp/authorize', async (req, res) => {
2680
+ if (!ensureAppAccess(req, res))
2681
+ return;
2682
+ const reqUrl = new URL(req.url || '/oauth/mcp/authorize', resolveMcpOauthIssuer(req));
2683
+ const clientId = String(reqUrl.searchParams.get('client_id') || '').trim();
2684
+ const redirectUri = String(reqUrl.searchParams.get('redirect_uri') || '').trim();
2685
+ const responseType = String(reqUrl.searchParams.get('response_type') || '').trim();
2686
+ const state = String(reqUrl.searchParams.get('state') || '').trim();
2687
+ const codeChallenge = String(reqUrl.searchParams.get('code_challenge') || '').trim();
2688
+ const codeChallengeMethod = String(reqUrl.searchParams.get('code_challenge_method') || 'S256').trim() || 'S256';
2689
+ const scopes = normalizeMcpOauthScopeString(reqUrl.searchParams.get('scope'));
2690
+ if (responseType !== 'code' || !clientId || !redirectUri || !codeChallenge) {
2691
+ res.writeHead(400, { 'Content-Type': 'application/json' });
2692
+ res.end(JSON.stringify({ error: 'invalid_request', error_description: 'client_id, redirect_uri, response_type=code, and code_challenge are required.' }));
2693
+ return;
2694
+ }
2695
+ const access = context.authConfig ? resolveRequestAccess(req, context.authConfig) : null;
2696
+ if (!access?.authenticated || !access.userId || access.userId === 'anonymous') {
2697
+ res.writeHead(302, { Location: `${resolveCloudLoginBaseUrl(req)}/login?next=${encodeURIComponent(reqUrl.toString())}` });
2698
+ res.end();
2699
+ return;
2700
+ }
2701
+ try {
2702
+ const clientRecord = core.getMcpOAuthClient(clientId);
2703
+ if (!clientRecord)
2704
+ throw new TaskforceRuleError('OAuth client not found', 404, 'MCP_OAUTH_CLIENT_NOT_FOUND');
2705
+ if (!clientRecord.redirectUris.includes(redirectUri))
2706
+ throw new TaskforceRuleError('Redirect URI is not registered for this OAuth client', 400, 'MCP_OAUTH_REDIRECT_URI_INVALID');
2707
+ const workspaces = core.listUserWorkspaces(access.userId)
2708
+ .map((workspace) => ({ id: String(workspace.id || '').trim(), name: String(workspace.name || workspace.slug || workspace.id || 'Workspace'), role: String(workspace.role || 'member') }))
2709
+ .filter((workspace) => Boolean(workspace.id));
2710
+ if (workspaces.length === 0)
2711
+ throw new TaskforceRuleError('No accessible workspaces found for this user', 403, 'MCP_OAUTH_WORKSPACE_REQUIRED');
2712
+ const preferredWorkspaceId = workspaces.find((workspace) => workspace.id === String(access.workspaceId || '').trim())?.id || workspaces[0].id;
2713
+ const userIdentity = core.resolveUserIdentity(access.userId);
2714
+ const userName = String(userIdentity?.displayName || userIdentity?.email || access.userId).trim() || String(access.userId);
2715
+ const userMeta = userIdentity?.email && userIdentity.email !== userName ? String(userIdentity.email).trim() : null;
2609
2716
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
2610
- res.end(renderMcpOauthAuthorizePage({ userName: String(access.userId), workspaces, preselectedWorkspaceId: preferredWorkspaceId, clientName: clientRecord.clientName, clientId: clientRecord.id, redirectUri, scopes, state, codeChallenge, codeChallengeMethod }));
2717
+ res.end(renderMcpOauthAuthorizePage({ userName, userMeta, workspaces, preselectedWorkspaceId: preferredWorkspaceId, clientName: clientRecord.clientName, clientId: clientRecord.id, redirectUri, scopes, state, codeChallenge, codeChallengeMethod }));
2611
2718
  }
2612
2719
  catch (error) {
2613
2720
  const statusCode = resolveErrorStatusCode(error);
@@ -1262,11 +1262,19 @@ export function createRoutes(core, context = {}) {
1262
1262
  const resolveMcpOauthRegisterUrl = (req) => `${resolveMcpOauthIssuer(req)}/oauth/mcp/register`;
1263
1263
  const resolveMcpOauthResourceUrl = (req) => `${resolveMcpOauthIssuer(req)}/mcp`;
1264
1264
  const renderMcpOauthAuthorizePage = (input) => {
1265
+ const scopeLabels = {
1266
+ 'tasks:read': 'Read tasks',
1267
+ 'tasks:write': 'Update tasks',
1268
+ 'workspace:read': 'Read workspace details'
1269
+ };
1265
1270
  const options = input.workspaces.map((workspace) => {
1266
1271
  const selected = workspace.id === input.preselectedWorkspaceId ? ' selected' : '';
1267
1272
  return `<option value="${escapeHtml(workspace.id)}"${selected}>${escapeHtml(workspace.name)} (${escapeHtml(workspace.role)})</option>`;
1268
1273
  }).join('');
1269
- const scopeBadges = input.scopes.map((scope) => `<span style="display:inline-flex;padding:6px 10px;border-radius:999px;background:#272742;border:1px solid rgba(255,255,255,0.09);font-size:12px;">${escapeHtml(scope)}</span>`).join('');
1274
+ const scopeBadges = input.scopes.map((scope) => {
1275
+ const label = scopeLabels[scope] || scope;
1276
+ return `<span class="scope-chip">${escapeHtml(label)}</span>`;
1277
+ }).join('');
1270
1278
  const termsUrl = 'https://taskforcehq.ai/legal/terms/';
1271
1279
  const privacyUrl = 'https://taskforcehq.ai/legal/privacy/';
1272
1280
  return `<!doctype html>
@@ -1275,41 +1283,65 @@ export function createRoutes(core, context = {}) {
1275
1283
  <meta charset="utf-8" />
1276
1284
  <meta name="viewport" content="width=device-width, initial-scale=1" />
1277
1285
  <title>Authorize Taskforce MCP Connector</title>
1286
+ <link rel="stylesheet" href="/styles/fonts.css" />
1278
1287
  <style>
1279
1288
  :root { color-scheme: dark; }
1280
- body { margin: 0; font-family: Inter, system-ui, sans-serif; background: #111126; color: #f8f7ff; }
1281
- .wrap { min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 24px; }
1282
- .card { width: min(560px, 100%); background: rgba(22,22,44,0.95); border: 1px solid rgba(255,255,255,0.08); border-radius: 20px; box-shadow: 0 20px 80px rgba(0,0,0,0.35); padding: 28px; }
1283
- h1 { margin: 0 0 8px; font-size: 24px; }
1284
- p { margin: 0 0 16px; color: rgba(255,255,255,0.72); line-height: 1.45; }
1285
- label { display: block; font-size: 13px; font-weight: 600; margin: 18px 0 8px; color: rgba(255,255,255,0.9); }
1286
- select { width: 100%; border-radius: 14px; border: 1px solid rgba(255,255,255,0.1); background: #1a1a34; color: #fff; padding: 12px 14px; font-size: 14px; }
1287
- .scopes { display:flex; flex-wrap:wrap; gap:8px; margin-top:8px; }
1288
- .meta { margin-top: 18px; padding: 14px 16px; border-radius: 14px; background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); }
1289
- .meta p { margin: 10px 0 0; font-size: 13px; color: rgba(255,255,255,0.68); }
1290
- .error { margin-bottom: 16px; padding: 12px 14px; border-radius: 12px; background: rgba(255,99,132,0.12); color: #ffb6c6; border: 1px solid rgba(255,99,132,0.25); }
1291
- .actions { display:flex; justify-content:flex-end; gap:12px; margin-top: 24px; }
1292
- button { border-radius: 12px; padding: 11px 16px; font-size: 14px; font-weight: 600; cursor: pointer; border: 1px solid rgba(255,255,255,0.08); }
1293
- button[name="decision"][value="deny"] { background: transparent; color: rgba(255,255,255,0.85); }
1294
- button[name="decision"][value="approve"] { background: #8d6bff; color: white; border-color: rgba(141,107,255,0.6); }
1295
- a { color: #b9d6ff; }
1289
+ body { margin: 0; font-family: "Outfit", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: radial-gradient(circle at top, rgba(255,138,0,0.08), transparent 30%), #111126; color: #f8f7ff; }
1290
+ .wrap { min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 32px 24px; }
1291
+ .card { width: min(680px, 100%); background: rgba(22,22,44,0.95); border: 1px solid rgba(255,255,255,0.08); border-radius: 36px; box-shadow: 0 24px 80px rgba(0,0,0,0.35); padding: 44px 56px; }
1292
+ .eyebrow { display: inline-flex; align-items: center; gap: 10px; margin-bottom: 18px; color: rgba(255,255,255,0.55); font-size: 12px; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; }
1293
+ .eyebrow-mark { width: 10px; height: 10px; border-radius: 999px; background: linear-gradient(135deg, #ff8a00 0%, #8d6bff 100%); box-shadow: 0 0 18px rgba(255,138,0,0.35); }
1294
+ h1 { margin: 0; font-size: clamp(2.25rem, 5vw, 3rem); line-height: 0.96; letter-spacing: -0.04em; color: #f8f7ff; }
1295
+ .brand-word { font-family: "Russo One", sans-serif; font-size: 0.94em; letter-spacing: -0.03em; }
1296
+ .lede { margin: 18px 0 8px; font-size: 1.15rem; line-height: 1.5; color: rgba(255,255,255,0.78); max-width: 34ch; }
1297
+ .lede strong { color: #fff; }
1298
+ .user-meta { margin: 0 0 28px; color: rgba(255,255,255,0.5); font-size: 0.9rem; }
1299
+ label { display: block; font-size: 13px; font-weight: 700; margin: 24px 0 10px; color: rgba(255,255,255,0.92); letter-spacing: 0.01em; }
1300
+ select { width: 100%; border-radius: 18px; border: 1px solid rgba(255,255,255,0.1); background: #1f2039; color: #fff; padding: 16px 18px; font-size: 15px; font-weight: 600; box-shadow: inset 0 1px 0 rgba(255,255,255,0.02); }
1301
+ .scopes { display:flex; flex-wrap:wrap; gap:10px; margin-top:8px; }
1302
+ .scope-chip { display:inline-flex; padding:8px 14px; border-radius:999px; background:#2a2947; border:1px solid rgba(255,255,255,0.09); font-size:13px; font-weight:600; color:rgba(255,255,255,0.92); }
1303
+ .details { margin-top: 24px; padding: 0; border: 1px solid rgba(255,255,255,0.08); border-radius: 18px; background: rgba(255,255,255,0.04); overflow: hidden; }
1304
+ .details summary { list-style: none; cursor: pointer; padding: 16px 18px; font-size: 0.92rem; font-weight: 700; color: rgba(255,255,255,0.86); }
1305
+ .details summary::-webkit-details-marker { display: none; }
1306
+ .details-body { padding: 0 18px 18px; color: rgba(255,255,255,0.68); font-size: 0.9rem; line-height: 1.5; }
1307
+ .details-row { margin: 0 0 8px; }
1308
+ .details-row strong { color: rgba(255,255,255,0.92); }
1309
+ .legal { margin: 24px 2px 0; color: rgba(255,255,255,0.58); font-size: 0.9rem; line-height: 1.55; max-width: 56ch; }
1310
+ .error { margin-bottom: 18px; padding: 12px 14px; border-radius: 14px; background: rgba(255,99,132,0.12); color: #ffb6c6; border: 1px solid rgba(255,99,132,0.25); }
1311
+ .actions { display:flex; justify-content:flex-end; gap:14px; margin-top: 36px; }
1312
+ button { border-radius: 18px; padding: 15px 22px; font-size: 15px; font-weight: 700; cursor: pointer; border: 1px solid rgba(255,255,255,0.08); min-width: 156px; transition: transform 160ms ease, box-shadow 160ms ease, border-color 160ms ease, background-color 160ms ease; }
1313
+ button:hover { transform: translateY(-1px); }
1314
+ button[name="decision"][value="deny"] { background: transparent; color: rgba(255,255,255,0.88); border-color: rgba(255,255,255,0.12); }
1315
+ button[name="decision"][value="approve"] { background: linear-gradient(135deg, #8d6bff 0%, #7c59f1 100%); color: white; border-color: rgba(141,107,255,0.6); box-shadow: 0 14px 28px rgba(141,107,255,0.26); }
1316
+ a { color: #cbd8ff; }
1317
+ @media (max-width: 720px) {
1318
+ .card { padding: 30px 22px; border-radius: 28px; }
1319
+ h1 { font-size: 2rem; }
1320
+ .actions { flex-direction: column-reverse; }
1321
+ button { width: 100%; }
1322
+ }
1296
1323
  </style>
1297
1324
  </head>
1298
1325
  <body>
1299
1326
  <div class="wrap">
1300
1327
  <form class="card" method="post" action="/oauth/mcp/authorize">
1301
- <h1>Authorize Taskforce Connector</h1>
1302
- <p>${escapeHtml(input.clientName)} wants access to Taskforce MCP for ${escapeHtml(input.userName)}.</p>
1328
+ <div class="eyebrow"><span class="eyebrow-mark"></span>Taskforce MCP</div>
1329
+ <h1>Authorize <span class="brand-word">Taskforce</span> Connector</h1>
1330
+ <p class="lede"><strong>${escapeHtml(input.clientName)}</strong> wants to connect to Taskforce for <strong>${escapeHtml(input.userName)}</strong>.</p>
1331
+ ${input.userMeta ? `<p class="user-meta">Signed in as ${escapeHtml(input.userMeta)}</p>` : ''}
1303
1332
  ${input.error ? `<div class="error">${escapeHtml(input.error)}</div>` : ''}
1304
1333
  <label for="workspaceId">Workspace</label>
1305
1334
  <select id="workspaceId" name="workspaceId" required>${options}</select>
1306
- <label>Requested Scopes</label>
1335
+ <label>Requested Permissions</label>
1307
1336
  <div class="scopes">${scopeBadges}</div>
1308
- <div class="meta">
1309
- <div><strong>Client:</strong> ${escapeHtml(input.clientName)}</div>
1310
- <div><strong>Redirect URI:</strong> ${escapeHtml(input.redirectUri)}</div>
1311
- <p>By authorizing this connector, you agree to Taskforce&apos;s <a href="${termsUrl}" target="_blank" rel="noopener noreferrer">Terms of Service</a> and acknowledge the <a href="${privacyUrl}" target="_blank" rel="noopener noreferrer">Privacy Policy</a>.</p>
1312
- </div>
1337
+ <details class="details">
1338
+ <summary>Technical details</summary>
1339
+ <div class="details-body">
1340
+ <div class="details-row"><strong>Client:</strong> ${escapeHtml(input.clientName)}</div>
1341
+ <div class="details-row"><strong>Redirect URI:</strong> ${escapeHtml(input.redirectUri)}</div>
1342
+ </div>
1343
+ </details>
1344
+ <p class="legal">By authorizing this connector, you agree to Taskforce&apos;s <a href="${termsUrl}" target="_blank" rel="noopener noreferrer">Terms of Service</a> and acknowledge the <a href="${privacyUrl}" target="_blank" rel="noopener noreferrer">Privacy Policy</a>.</p>
1313
1345
  <input type="hidden" name="client_id" value="${escapeHtml(input.clientId)}" />
1314
1346
  <input type="hidden" name="redirect_uri" value="${escapeHtml(input.redirectUri)}" />
1315
1347
  <input type="hidden" name="scope" value="${escapeHtml(input.scopes.join(' '))}" />
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taskforcehq/taskforce",
3
- "version": "0.3.317",
3
+ "version": "0.3.319",
4
4
  "description": "Shared task and planning workspace for humans collaborating with AI (public beta)",
5
5
  "author": "Taskforce HQ <hello@taskforcehq.ai> (https://taskforcehq.ai)",
6
6
  "license": "MIT",