@taskforcehq/taskforce 0.3.318 → 0.3.320
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/TaskforceCore.js +57 -16
- package/dist/server/routes/admin.js +146 -28
- package/dist/server/routes/auth.js +24 -0
- package/dist/server/routes.js +48 -25
- package/dist/ui/assets/{AgentsModule-DxsN8GQI.js → AgentsModule-BZG0WHDN.js} +1 -1
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-DUkRcfo7.js → AnnotatedAttachmentWorkspace-EQEzsETm.js} +1 -1
- package/dist/ui/assets/{ContextAttachmentManager-D_uqJ94r.js → ContextAttachmentManager-B3hESx6y.js} +1 -1
- package/dist/ui/assets/{DocumentWorkspace-DAsuvSWR.js → DocumentWorkspace-Z7bykYtA.js} +1 -1
- package/dist/ui/assets/{EntityActivityTimeline-QLUNczJS.js → EntityActivityTimeline-B2aXkxiE.js} +1 -1
- package/dist/ui/assets/{InitiativesModule-D23morXe.js → InitiativesModule-CSzXelzW.js} +1 -1
- package/dist/ui/assets/{PlansPage-X6c0yJ1a.js → PlansPage-2vebpTBs.js} +1 -1
- package/dist/ui/assets/{TaskContextUpload-BERMUT_S.js → TaskContextUpload-Bz06La5A.js} +1 -1
- package/dist/ui/assets/{TaskSettings-DlX_5CSg.js → TaskSettings-4PbrcOqD.js} +1 -1
- package/dist/ui/assets/{WorkflowsModule-CVu4lhC1.js → WorkflowsModule-AyOzrP3y.js} +1 -1
- package/dist/ui/assets/documentReferences-CKk8ylqA.js +1 -0
- package/dist/ui/assets/index-rB9GGUCz.js +5 -0
- package/dist/ui/index.html +1 -1
- package/package.json +1 -1
- package/dist/ui/assets/documentReferences-BCXQQ60P.js +0 -1
- package/dist/ui/assets/index-CdlMnBxP.js +0 -5
package/dist/TaskforceCore.js
CHANGED
|
@@ -579,14 +579,44 @@ function TaskforceCoreWithRouter({ config = {}, initialTaskId, onTaskCountChange
|
|
|
579
579
|
persistWorkspaceSyncStatePatch,
|
|
580
580
|
refreshSetupContext
|
|
581
581
|
]);
|
|
582
|
-
const
|
|
582
|
+
const nextTarget = useMemo(() => {
|
|
583
583
|
const raw = new URLSearchParams(location.search).get('next') || '/';
|
|
584
|
-
if (
|
|
585
|
-
|
|
586
|
-
|
|
584
|
+
if (raw.startsWith('/')) {
|
|
585
|
+
if (raw === '/login')
|
|
586
|
+
return '/';
|
|
587
|
+
return raw;
|
|
588
|
+
}
|
|
589
|
+
try {
|
|
590
|
+
const parsed = new URL(raw);
|
|
591
|
+
const host = parsed.hostname.toLowerCase();
|
|
592
|
+
const isLocal = host === 'localhost' || host === '127.0.0.1' || host === '::1';
|
|
593
|
+
const isTaskforceHost = host === 'taskforcehq.ai' || host.endsWith('.taskforcehq.ai');
|
|
594
|
+
if ((parsed.protocol === 'http:' || parsed.protocol === 'https:') && (isLocal || isTaskforceHost)) {
|
|
595
|
+
return parsed.toString();
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
catch {
|
|
587
599
|
return '/';
|
|
588
|
-
|
|
600
|
+
}
|
|
601
|
+
return '/';
|
|
589
602
|
}, [location.search]);
|
|
603
|
+
const nextPath = useMemo(() => nextTarget.startsWith('/') ? nextTarget : '/', [nextTarget]);
|
|
604
|
+
const navigateToPostAuthTarget = useCallback((target, options) => {
|
|
605
|
+
if (!target)
|
|
606
|
+
return;
|
|
607
|
+
if (target.startsWith('/')) {
|
|
608
|
+
navigate(target, { replace: options?.replace === true });
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
if (typeof window !== 'undefined') {
|
|
612
|
+
if (options?.replace === true) {
|
|
613
|
+
window.location.replace(target);
|
|
614
|
+
}
|
|
615
|
+
else {
|
|
616
|
+
window.location.assign(target);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}, [navigate]);
|
|
590
620
|
const shouldReturnToCloudWorkspaceSelection = useMemo(() => {
|
|
591
621
|
try {
|
|
592
622
|
const parsed = new URL(nextPath, 'https://taskforce.local');
|
|
@@ -622,9 +652,9 @@ function TaskforceCoreWithRouter({ config = {}, initialTaskId, onTaskCountChange
|
|
|
622
652
|
interval: selectedSignupPlanVersionId ? resolvedSignupInterval : null
|
|
623
653
|
});
|
|
624
654
|
}
|
|
625
|
-
return
|
|
655
|
+
return nextTarget;
|
|
626
656
|
}, [
|
|
627
|
-
|
|
657
|
+
nextTarget,
|
|
628
658
|
planSelectionPath,
|
|
629
659
|
resolvedSignupInterval,
|
|
630
660
|
selectedSignupPlanId,
|
|
@@ -688,9 +718,13 @@ function TaskforceCoreWithRouter({ config = {}, initialTaskId, onTaskCountChange
|
|
|
688
718
|
? resolveWorkspaceSetupPath()
|
|
689
719
|
: pendingPostAuthRedirect.path;
|
|
690
720
|
const currentRoute = `${location.pathname}${location.search}${location.hash || ''}`;
|
|
691
|
-
if (
|
|
692
|
-
|
|
721
|
+
if (destination.startsWith('/')) {
|
|
722
|
+
if (currentRoute !== destination) {
|
|
723
|
+
navigate(destination, { replace: true });
|
|
724
|
+
}
|
|
725
|
+
return;
|
|
693
726
|
}
|
|
727
|
+
navigateToPostAuthTarget(destination, { replace: true });
|
|
694
728
|
}, [
|
|
695
729
|
authSessionResolved,
|
|
696
730
|
cloudAccountAccessPending,
|
|
@@ -699,6 +733,7 @@ function TaskforceCoreWithRouter({ config = {}, initialTaskId, onTaskCountChange
|
|
|
699
733
|
location.pathname,
|
|
700
734
|
location.search,
|
|
701
735
|
navigate,
|
|
736
|
+
navigateToPostAuthTarget,
|
|
702
737
|
pendingPostAuthRedirect,
|
|
703
738
|
resolveWorkspaceSetupPath
|
|
704
739
|
]);
|
|
@@ -726,9 +761,13 @@ function TaskforceCoreWithRouter({ config = {}, initialTaskId, onTaskCountChange
|
|
|
726
761
|
? pendingPostAuthRedirect.path
|
|
727
762
|
: planSelectionPath;
|
|
728
763
|
const currentRoute = `${location.pathname}${location.search}${location.hash || ''}`;
|
|
729
|
-
if (
|
|
730
|
-
|
|
764
|
+
if (destination.startsWith('/')) {
|
|
765
|
+
if (currentRoute !== destination) {
|
|
766
|
+
navigate(destination, { replace: true });
|
|
767
|
+
}
|
|
768
|
+
return;
|
|
731
769
|
}
|
|
770
|
+
navigateToPostAuthTarget(destination, { replace: true });
|
|
732
771
|
}, [
|
|
733
772
|
accountAccessGate?.canAccessApp,
|
|
734
773
|
accountAccessGateResolved,
|
|
@@ -739,6 +778,7 @@ function TaskforceCoreWithRouter({ config = {}, initialTaskId, onTaskCountChange
|
|
|
739
778
|
location.pathname,
|
|
740
779
|
location.search,
|
|
741
780
|
navigate,
|
|
781
|
+
navigateToPostAuthTarget,
|
|
742
782
|
pendingPostAuthRedirect,
|
|
743
783
|
planSelectionPath
|
|
744
784
|
]);
|
|
@@ -901,8 +941,9 @@ function TaskforceCoreWithRouter({ config = {}, initialTaskId, onTaskCountChange
|
|
|
901
941
|
if (shouldDelayAuthenticatedLoginRedirect) {
|
|
902
942
|
return;
|
|
903
943
|
}
|
|
904
|
-
if (isLoginRoute && isAuthenticated && !shouldStayOnInviteRoute)
|
|
905
|
-
|
|
944
|
+
if (isLoginRoute && isAuthenticated && !shouldStayOnInviteRoute) {
|
|
945
|
+
navigateToPostAuthTarget(nextTarget, { replace: true });
|
|
946
|
+
}
|
|
906
947
|
}
|
|
907
948
|
else {
|
|
908
949
|
if (!isLoginRoute) {
|
|
@@ -1299,7 +1340,7 @@ function TaskforceCoreWithRouter({ config = {}, initialTaskId, onTaskCountChange
|
|
|
1299
1340
|
setLoginBusy(false);
|
|
1300
1341
|
return;
|
|
1301
1342
|
}
|
|
1302
|
-
|
|
1343
|
+
navigateToPostAuthTarget(nextTarget, { replace: true });
|
|
1303
1344
|
}
|
|
1304
1345
|
else if (authMode === 'login') {
|
|
1305
1346
|
if (inviteToken.trim()) {
|
|
@@ -1319,14 +1360,14 @@ function TaskforceCoreWithRouter({ config = {}, initialTaskId, onTaskCountChange
|
|
|
1319
1360
|
setLoginDisplayName('');
|
|
1320
1361
|
setLoginPassword('');
|
|
1321
1362
|
setLoginPasswordConfirm('');
|
|
1322
|
-
|
|
1363
|
+
navigateToPostAuthTarget(nextTarget, { replace: true });
|
|
1323
1364
|
}
|
|
1324
1365
|
else {
|
|
1325
1366
|
setLoginEmail('');
|
|
1326
1367
|
setLoginDisplayName('');
|
|
1327
1368
|
setLoginPassword('');
|
|
1328
1369
|
setLoginPasswordConfirm('');
|
|
1329
|
-
|
|
1370
|
+
navigateToPostAuthTarget(nextTarget, { replace: true });
|
|
1330
1371
|
}
|
|
1331
1372
|
setLoginBusy(false);
|
|
1332
1373
|
}, children: loginBusy ? 'Working...' : (authMode === 'login' ? 'Sign In'
|
|
@@ -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'
|
|
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) =>
|
|
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,47 +280,62 @@ 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:
|
|
278
|
-
.wrap { min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 24px; }
|
|
279
|
-
.card { width: min(
|
|
280
|
-
h1 { margin: 0
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
.
|
|
285
|
-
.
|
|
286
|
-
.
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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(720px, 100%); background: rgba(22,22,44,0.95); border: 1px solid rgba(255,255,255,0.08); border-radius: 32px; box-shadow: 0 24px 80px rgba(0,0,0,0.35); padding: 40px 48px; }
|
|
289
|
+
h1 { margin: 0; font-size: clamp(1.95rem, 4.2vw, 2.6rem); line-height: 1.02; letter-spacing: -0.035em; color: #f8f7ff; }
|
|
290
|
+
.lede { margin: 16px 0 8px; font-size: 1.1rem; line-height: 1.45; color: rgba(255,255,255,0.78); max-width: 48ch; text-wrap: pretty; }
|
|
291
|
+
.lede strong { color: #fff; }
|
|
292
|
+
.session-row { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin: 0 0 28px; }
|
|
293
|
+
.user-meta { margin: 0; color: rgba(255,255,255,0.5); font-size: 0.9rem; }
|
|
294
|
+
.account-switch { padding: 0; border: 0; min-width: 0; background: transparent; color: #cbd8ff; font-size: 0.9rem; font-weight: 600; box-shadow: none; }
|
|
295
|
+
.account-switch:hover { transform: none; color: #ffffff; text-decoration: underline; }
|
|
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
|
+
.legal { margin: 28px 2px 0; color: rgba(255,255,255,0.58); font-size: 0.9rem; line-height: 1.55; max-width: 58ch; }
|
|
301
|
+
.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); }
|
|
302
|
+
.actions { display:flex; justify-content:flex-end; gap:14px; margin-top: 36px; }
|
|
303
|
+
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; }
|
|
304
|
+
button:hover { transform: translateY(-1px); }
|
|
305
|
+
button[name="decision"][value="deny"] { background: transparent; color: rgba(255,255,255,0.88); border-color: rgba(255,255,255,0.12); }
|
|
306
|
+
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); }
|
|
307
|
+
a { color: #cbd8ff; }
|
|
308
|
+
@media (max-width: 720px) {
|
|
309
|
+
.card { padding: 30px 22px; border-radius: 28px; }
|
|
310
|
+
h1 { font-size: 1.85rem; }
|
|
311
|
+
.lede { max-width: none; }
|
|
312
|
+
.actions { flex-direction: column-reverse; }
|
|
313
|
+
button { width: 100%; }
|
|
314
|
+
}
|
|
293
315
|
</style>
|
|
294
316
|
</head>
|
|
295
317
|
<body>
|
|
296
318
|
<div class="wrap">
|
|
297
319
|
<form class="card" method="post" action="/oauth/mcp/authorize">
|
|
298
320
|
<h1>Authorize Taskforce Connector</h1>
|
|
299
|
-
<p>${escapeHtml(input.clientName)} wants
|
|
321
|
+
<p class="lede"><strong>${escapeHtml(input.clientName)}</strong> wants to connect to Taskforce for <strong>${escapeHtml(input.userName)}</strong>.</p>
|
|
322
|
+
<div class="session-row">
|
|
323
|
+
${input.userMeta ? `<p class="user-meta">Signed in as ${escapeHtml(input.userMeta)}</p>` : ''}
|
|
324
|
+
<button class="account-switch" type="submit" formaction="/api/taskforce/auth/logout" formmethod="post" formnovalidate>Use different account</button>
|
|
325
|
+
</div>
|
|
300
326
|
${input.error ? `<div class="error">${escapeHtml(input.error)}</div>` : ''}
|
|
301
|
-
<label for="workspaceId">Workspace</label>
|
|
327
|
+
<label for="workspaceId">Select Workspace</label>
|
|
302
328
|
<select id="workspaceId" name="workspaceId" required>${options}</select>
|
|
303
|
-
<label>Requested
|
|
329
|
+
<label>Requested Permissions</label>
|
|
304
330
|
<div class="scopes">${scopeBadges}</div>
|
|
305
|
-
<
|
|
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'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>
|
|
331
|
+
<p class="legal">By authorizing this connector, you agree to Taskforce'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
332
|
<input type="hidden" name="client_id" value="${escapeHtml(input.clientId)}" />
|
|
311
333
|
<input type="hidden" name="redirect_uri" value="${escapeHtml(input.redirectUri)}" />
|
|
312
334
|
<input type="hidden" name="scope" value="${escapeHtml(input.scopes.join(' '))}" />
|
|
313
335
|
<input type="hidden" name="state" value="${escapeHtml(input.state)}" />
|
|
314
336
|
<input type="hidden" name="code_challenge" value="${escapeHtml(input.codeChallenge)}" />
|
|
315
337
|
<input type="hidden" name="code_challenge_method" value="${escapeHtml(input.codeChallengeMethod)}" />
|
|
338
|
+
<input type="hidden" name="next" value="${escapeHtml(input.authorizeUrl)}" />
|
|
316
339
|
<div class="actions">
|
|
317
340
|
<button type="submit" name="decision" value="deny">Cancel</button>
|
|
318
341
|
<button type="submit" name="decision" value="approve">Authorize</button>
|
|
@@ -2481,13 +2504,15 @@ export function registerAdminRoutes(deps) {
|
|
|
2481
2504
|
addRoute('POST', '/mcp/', async (req, res) => { await handleCloudMcpRequest(req, res); });
|
|
2482
2505
|
addRoute('GET', '/mcp', async (req, res) => { await handleCloudMcpRequest(req, res); });
|
|
2483
2506
|
addRoute('GET', '/mcp/', async (req, res) => { await handleCloudMcpRequest(req, res); });
|
|
2507
|
+
addRoute('HEAD', '/mcp', async (req, res) => { await handleCloudMcpRequest(req, res); });
|
|
2508
|
+
addRoute('HEAD', '/mcp/', async (req, res) => { await handleCloudMcpRequest(req, res); });
|
|
2484
2509
|
addRoute('DELETE', '/mcp', async (req, res) => { await handleCloudMcpRequest(req, res); });
|
|
2485
2510
|
addRoute('DELETE', '/mcp/', async (req, res) => { await handleCloudMcpRequest(req, res); });
|
|
2486
2511
|
addRoute('OPTIONS', '/mcp', async (_req, res) => {
|
|
2487
2512
|
res.writeHead(204, {
|
|
2488
2513
|
'Access-Control-Allow-Origin': '*',
|
|
2489
2514
|
'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'
|
|
2515
|
+
'Access-Control-Allow-Methods': 'GET, HEAD, POST, DELETE, OPTIONS'
|
|
2491
2516
|
});
|
|
2492
2517
|
res.end();
|
|
2493
2518
|
});
|
|
@@ -2495,7 +2520,7 @@ export function registerAdminRoutes(deps) {
|
|
|
2495
2520
|
res.writeHead(204, {
|
|
2496
2521
|
'Access-Control-Allow-Origin': '*',
|
|
2497
2522
|
'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'
|
|
2523
|
+
'Access-Control-Allow-Methods': 'GET, HEAD, POST, DELETE, OPTIONS'
|
|
2499
2524
|
});
|
|
2500
2525
|
res.end();
|
|
2501
2526
|
});
|
|
@@ -2515,6 +2540,21 @@ export function registerAdminRoutes(deps) {
|
|
|
2515
2540
|
scopes_supported: ['tasks:read', 'tasks:write', 'workspace:read'],
|
|
2516
2541
|
}));
|
|
2517
2542
|
});
|
|
2543
|
+
addRoute('HEAD', '/.well-known/oauth-authorization-server', async (req, res) => {
|
|
2544
|
+
const issuer = resolveMcpOauthIssuer(req);
|
|
2545
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2546
|
+
res.end(JSON.stringify({
|
|
2547
|
+
issuer,
|
|
2548
|
+
authorization_endpoint: resolveMcpOauthAuthorizeUrl(req),
|
|
2549
|
+
token_endpoint: resolveMcpOauthTokenUrl(req),
|
|
2550
|
+
registration_endpoint: resolveMcpOauthRegisterUrl(req),
|
|
2551
|
+
response_types_supported: ['code'],
|
|
2552
|
+
grant_types_supported: ['authorization_code'],
|
|
2553
|
+
token_endpoint_auth_methods_supported: ['none'],
|
|
2554
|
+
code_challenge_methods_supported: ['S256'],
|
|
2555
|
+
scopes_supported: ['tasks:read', 'tasks:write', 'workspace:read'],
|
|
2556
|
+
}));
|
|
2557
|
+
});
|
|
2518
2558
|
addRoute('GET', '/.well-known/oauth-protected-resource', async (req, res) => {
|
|
2519
2559
|
const issuer = resolveMcpOauthIssuer(req);
|
|
2520
2560
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
@@ -2524,6 +2564,15 @@ export function registerAdminRoutes(deps) {
|
|
|
2524
2564
|
bearer_methods_supported: ['header'],
|
|
2525
2565
|
}));
|
|
2526
2566
|
});
|
|
2567
|
+
addRoute('HEAD', '/.well-known/oauth-protected-resource', async (req, res) => {
|
|
2568
|
+
const issuer = resolveMcpOauthIssuer(req);
|
|
2569
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2570
|
+
res.end(JSON.stringify({
|
|
2571
|
+
resource: resolveMcpOauthResourceUrl(req),
|
|
2572
|
+
authorization_servers: [issuer],
|
|
2573
|
+
bearer_methods_supported: ['header'],
|
|
2574
|
+
}));
|
|
2575
|
+
});
|
|
2527
2576
|
addRoute('POST', '/oauth/mcp/register', async (req, res) => {
|
|
2528
2577
|
if (!ensureAppAccess(req, res))
|
|
2529
2578
|
return;
|
|
@@ -2606,8 +2655,77 @@ export function registerAdminRoutes(deps) {
|
|
|
2606
2655
|
if (workspaces.length === 0)
|
|
2607
2656
|
throw new TaskforceRuleError('No accessible workspaces found for this user', 403, 'MCP_OAUTH_WORKSPACE_REQUIRED');
|
|
2608
2657
|
const preferredWorkspaceId = workspaces.find((workspace) => workspace.id === String(access.workspaceId || '').trim())?.id || workspaces[0].id;
|
|
2658
|
+
const userIdentity = core.resolveUserIdentity(access.userId);
|
|
2659
|
+
const userName = String(userIdentity?.displayName || userIdentity?.email || access.userId).trim() || String(access.userId);
|
|
2660
|
+
const userMeta = userIdentity?.email && userIdentity.email !== userName ? String(userIdentity.email).trim() : null;
|
|
2661
|
+
const authorizeUrl = new URL('/oauth/mcp/authorize', resolveMcpOauthIssuer(req));
|
|
2662
|
+
authorizeUrl.searchParams.set('client_id', clientId);
|
|
2663
|
+
authorizeUrl.searchParams.set('redirect_uri', redirectUri);
|
|
2664
|
+
authorizeUrl.searchParams.set('response_type', responseType);
|
|
2665
|
+
authorizeUrl.searchParams.set('code_challenge', codeChallenge);
|
|
2666
|
+
authorizeUrl.searchParams.set('code_challenge_method', codeChallengeMethod);
|
|
2667
|
+
if (state)
|
|
2668
|
+
authorizeUrl.searchParams.set('state', state);
|
|
2669
|
+
if (scopes.length > 0)
|
|
2670
|
+
authorizeUrl.searchParams.set('scope', scopes.join(' '));
|
|
2671
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
2672
|
+
res.end(renderMcpOauthAuthorizePage({ userName, userMeta, authorizeUrl: authorizeUrl.toString(), workspaces, preselectedWorkspaceId: preferredWorkspaceId, clientName: clientRecord.clientName, clientId: clientRecord.id, redirectUri, scopes, state, codeChallenge, codeChallengeMethod }));
|
|
2673
|
+
}
|
|
2674
|
+
catch (error) {
|
|
2675
|
+
const statusCode = resolveErrorStatusCode(error);
|
|
2676
|
+
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
|
2677
|
+
res.end(JSON.stringify({ error: 'invalid_request', error_description: error.message, code: error?.code || 'MCP_OAUTH_AUTHORIZE_FAILED' }));
|
|
2678
|
+
}
|
|
2679
|
+
});
|
|
2680
|
+
addRoute('HEAD', '/oauth/mcp/authorize', async (req, res) => {
|
|
2681
|
+
if (!ensureAppAccess(req, res))
|
|
2682
|
+
return;
|
|
2683
|
+
const reqUrl = new URL(req.url || '/oauth/mcp/authorize', resolveMcpOauthIssuer(req));
|
|
2684
|
+
const clientId = String(reqUrl.searchParams.get('client_id') || '').trim();
|
|
2685
|
+
const redirectUri = String(reqUrl.searchParams.get('redirect_uri') || '').trim();
|
|
2686
|
+
const responseType = String(reqUrl.searchParams.get('response_type') || '').trim();
|
|
2687
|
+
const state = String(reqUrl.searchParams.get('state') || '').trim();
|
|
2688
|
+
const codeChallenge = String(reqUrl.searchParams.get('code_challenge') || '').trim();
|
|
2689
|
+
const codeChallengeMethod = String(reqUrl.searchParams.get('code_challenge_method') || 'S256').trim() || 'S256';
|
|
2690
|
+
const scopes = normalizeMcpOauthScopeString(reqUrl.searchParams.get('scope'));
|
|
2691
|
+
if (responseType !== 'code' || !clientId || !redirectUri || !codeChallenge) {
|
|
2692
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
2693
|
+
res.end(JSON.stringify({ error: 'invalid_request', error_description: 'client_id, redirect_uri, response_type=code, and code_challenge are required.' }));
|
|
2694
|
+
return;
|
|
2695
|
+
}
|
|
2696
|
+
const access = context.authConfig ? resolveRequestAccess(req, context.authConfig) : null;
|
|
2697
|
+
if (!access?.authenticated || !access.userId || access.userId === 'anonymous') {
|
|
2698
|
+
res.writeHead(302, { Location: `${resolveCloudLoginBaseUrl(req)}/login?next=${encodeURIComponent(reqUrl.toString())}` });
|
|
2699
|
+
res.end();
|
|
2700
|
+
return;
|
|
2701
|
+
}
|
|
2702
|
+
try {
|
|
2703
|
+
const clientRecord = core.getMcpOAuthClient(clientId);
|
|
2704
|
+
if (!clientRecord)
|
|
2705
|
+
throw new TaskforceRuleError('OAuth client not found', 404, 'MCP_OAUTH_CLIENT_NOT_FOUND');
|
|
2706
|
+
if (!clientRecord.redirectUris.includes(redirectUri))
|
|
2707
|
+
throw new TaskforceRuleError('Redirect URI is not registered for this OAuth client', 400, 'MCP_OAUTH_REDIRECT_URI_INVALID');
|
|
2708
|
+
const workspaces = core.listUserWorkspaces(access.userId)
|
|
2709
|
+
.map((workspace) => ({ id: String(workspace.id || '').trim(), name: String(workspace.name || workspace.slug || workspace.id || 'Workspace'), role: String(workspace.role || 'member') }))
|
|
2710
|
+
.filter((workspace) => Boolean(workspace.id));
|
|
2711
|
+
if (workspaces.length === 0)
|
|
2712
|
+
throw new TaskforceRuleError('No accessible workspaces found for this user', 403, 'MCP_OAUTH_WORKSPACE_REQUIRED');
|
|
2713
|
+
const preferredWorkspaceId = workspaces.find((workspace) => workspace.id === String(access.workspaceId || '').trim())?.id || workspaces[0].id;
|
|
2714
|
+
const userIdentity = core.resolveUserIdentity(access.userId);
|
|
2715
|
+
const userName = String(userIdentity?.displayName || userIdentity?.email || access.userId).trim() || String(access.userId);
|
|
2716
|
+
const userMeta = userIdentity?.email && userIdentity.email !== userName ? String(userIdentity.email).trim() : null;
|
|
2717
|
+
const authorizeUrl = new URL('/oauth/mcp/authorize', resolveMcpOauthIssuer(req));
|
|
2718
|
+
authorizeUrl.searchParams.set('client_id', clientId);
|
|
2719
|
+
authorizeUrl.searchParams.set('redirect_uri', redirectUri);
|
|
2720
|
+
authorizeUrl.searchParams.set('response_type', responseType);
|
|
2721
|
+
authorizeUrl.searchParams.set('code_challenge', codeChallenge);
|
|
2722
|
+
authorizeUrl.searchParams.set('code_challenge_method', codeChallengeMethod);
|
|
2723
|
+
if (state)
|
|
2724
|
+
authorizeUrl.searchParams.set('state', state);
|
|
2725
|
+
if (scopes.length > 0)
|
|
2726
|
+
authorizeUrl.searchParams.set('scope', scopes.join(' '));
|
|
2609
2727
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
2610
|
-
res.end(renderMcpOauthAuthorizePage({ userName:
|
|
2728
|
+
res.end(renderMcpOauthAuthorizePage({ userName, userMeta, authorizeUrl: authorizeUrl.toString(), workspaces, preselectedWorkspaceId: preferredWorkspaceId, clientName: clientRecord.clientName, clientId: clientRecord.id, redirectUri, scopes, state, codeChallenge, codeChallengeMethod }));
|
|
2611
2729
|
}
|
|
2612
2730
|
catch (error) {
|
|
2613
2731
|
const statusCode = resolveErrorStatusCode(error);
|
|
@@ -2068,7 +2068,31 @@ export function registerAuthRoutes(deps) {
|
|
|
2068
2068
|
for (const domain of domains) {
|
|
2069
2069
|
clearCookies.push(`${clearBase}; Domain=${domain}`);
|
|
2070
2070
|
}
|
|
2071
|
+
const body = await parseFormBody(req);
|
|
2072
|
+
const requestedNext = String(body.next || '').trim();
|
|
2073
|
+
const next = (() => {
|
|
2074
|
+
if (!requestedNext)
|
|
2075
|
+
return '';
|
|
2076
|
+
try {
|
|
2077
|
+
const parsed = new URL(requestedNext);
|
|
2078
|
+
const host = parsed.hostname.toLowerCase();
|
|
2079
|
+
const isLocal = host === 'localhost' || host === '127.0.0.1' || host === '::1';
|
|
2080
|
+
const isTaskforceHost = host === 'taskforcehq.ai' || host.endsWith('.taskforcehq.ai');
|
|
2081
|
+
if ((parsed.protocol === 'http:' || parsed.protocol === 'https:') && (isLocal || isTaskforceHost)) {
|
|
2082
|
+
return parsed.toString();
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
catch {
|
|
2086
|
+
return '';
|
|
2087
|
+
}
|
|
2088
|
+
return '';
|
|
2089
|
+
})();
|
|
2071
2090
|
res.setHeader('Set-Cookie', [...new Set(clearCookies)]);
|
|
2091
|
+
if (next) {
|
|
2092
|
+
res.writeHead(303, { Location: next });
|
|
2093
|
+
res.end();
|
|
2094
|
+
return;
|
|
2095
|
+
}
|
|
2072
2096
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2073
2097
|
res.end(JSON.stringify({ success: true }));
|
|
2074
2098
|
});
|
package/dist/server/routes.js
CHANGED
|
@@ -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'
|
|
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) =>
|
|
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,47 +1283,62 @@ 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:
|
|
1281
|
-
.wrap { min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 24px; }
|
|
1282
|
-
.card { width: min(
|
|
1283
|
-
h1 { margin: 0
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
.
|
|
1288
|
-
.
|
|
1289
|
-
.
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
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(720px, 100%); background: rgba(22,22,44,0.95); border: 1px solid rgba(255,255,255,0.08); border-radius: 32px; box-shadow: 0 24px 80px rgba(0,0,0,0.35); padding: 40px 48px; }
|
|
1292
|
+
h1 { margin: 0; font-size: clamp(1.95rem, 4.2vw, 2.6rem); line-height: 1.02; letter-spacing: -0.035em; color: #f8f7ff; }
|
|
1293
|
+
.lede { margin: 16px 0 8px; font-size: 1.1rem; line-height: 1.45; color: rgba(255,255,255,0.78); max-width: 48ch; text-wrap: pretty; }
|
|
1294
|
+
.lede strong { color: #fff; }
|
|
1295
|
+
.session-row { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin: 0 0 28px; }
|
|
1296
|
+
.user-meta { margin: 0; color: rgba(255,255,255,0.5); font-size: 0.9rem; }
|
|
1297
|
+
.account-switch { padding: 0; border: 0; min-width: 0; background: transparent; color: #cbd8ff; font-size: 0.9rem; font-weight: 600; box-shadow: none; }
|
|
1298
|
+
.account-switch:hover { transform: none; color: #ffffff; text-decoration: underline; }
|
|
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
|
+
.legal { margin: 28px 2px 0; color: rgba(255,255,255,0.58); font-size: 0.9rem; line-height: 1.55; max-width: 58ch; }
|
|
1304
|
+
.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); }
|
|
1305
|
+
.actions { display:flex; justify-content:flex-end; gap:14px; margin-top: 36px; }
|
|
1306
|
+
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; }
|
|
1307
|
+
button:hover { transform: translateY(-1px); }
|
|
1308
|
+
button[name="decision"][value="deny"] { background: transparent; color: rgba(255,255,255,0.88); border-color: rgba(255,255,255,0.12); }
|
|
1309
|
+
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); }
|
|
1310
|
+
a { color: #cbd8ff; }
|
|
1311
|
+
@media (max-width: 720px) {
|
|
1312
|
+
.card { padding: 30px 22px; border-radius: 28px; }
|
|
1313
|
+
h1 { font-size: 1.85rem; }
|
|
1314
|
+
.lede { max-width: none; }
|
|
1315
|
+
.actions { flex-direction: column-reverse; }
|
|
1316
|
+
button { width: 100%; }
|
|
1317
|
+
}
|
|
1296
1318
|
</style>
|
|
1297
1319
|
</head>
|
|
1298
1320
|
<body>
|
|
1299
1321
|
<div class="wrap">
|
|
1300
1322
|
<form class="card" method="post" action="/oauth/mcp/authorize">
|
|
1301
1323
|
<h1>Authorize Taskforce Connector</h1>
|
|
1302
|
-
<p>${escapeHtml(input.clientName)} wants
|
|
1324
|
+
<p class="lede"><strong>${escapeHtml(input.clientName)}</strong> wants to connect to Taskforce for <strong>${escapeHtml(input.userName)}</strong>.</p>
|
|
1325
|
+
<div class="session-row">
|
|
1326
|
+
${input.userMeta ? `<p class="user-meta">Signed in as ${escapeHtml(input.userMeta)}</p>` : ''}
|
|
1327
|
+
<button class="account-switch" type="submit" formaction="/api/taskforce/auth/logout" formmethod="post" formnovalidate>Use different account</button>
|
|
1328
|
+
</div>
|
|
1303
1329
|
${input.error ? `<div class="error">${escapeHtml(input.error)}</div>` : ''}
|
|
1304
|
-
<label for="workspaceId">Workspace</label>
|
|
1330
|
+
<label for="workspaceId">Select Workspace</label>
|
|
1305
1331
|
<select id="workspaceId" name="workspaceId" required>${options}</select>
|
|
1306
|
-
<label>Requested
|
|
1332
|
+
<label>Requested Permissions</label>
|
|
1307
1333
|
<div class="scopes">${scopeBadges}</div>
|
|
1308
|
-
<
|
|
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'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>
|
|
1334
|
+
<p class="legal">By authorizing this connector, you agree to Taskforce'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
1335
|
<input type="hidden" name="client_id" value="${escapeHtml(input.clientId)}" />
|
|
1314
1336
|
<input type="hidden" name="redirect_uri" value="${escapeHtml(input.redirectUri)}" />
|
|
1315
1337
|
<input type="hidden" name="scope" value="${escapeHtml(input.scopes.join(' '))}" />
|
|
1316
1338
|
<input type="hidden" name="state" value="${escapeHtml(input.state)}" />
|
|
1317
1339
|
<input type="hidden" name="code_challenge" value="${escapeHtml(input.codeChallenge)}" />
|
|
1318
1340
|
<input type="hidden" name="code_challenge_method" value="${escapeHtml(input.codeChallengeMethod)}" />
|
|
1341
|
+
<input type="hidden" name="next" value="${escapeHtml(input.authorizeUrl)}" />
|
|
1319
1342
|
<div class="actions">
|
|
1320
1343
|
<button type="submit" name="decision" value="deny">Cancel</button>
|
|
1321
1344
|
<button type="submit" name="decision" value="approve">Authorize</button>
|