@taskforcehq/taskforce 0.3.301 → 0.3.303
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/Taskforce.module.css +37 -2
- package/dist/TaskforceCore.filter-persistence.test.js +1 -1
- package/dist/TaskforceCore.js +72 -33
- package/dist/TaskforceCore.test.js +848 -130
- package/dist/components/features/TaskSettings.js +14 -82
- package/dist/components/features/TaskSettings.test.js +33 -70
- package/dist/components/features/settings/TaxonomyLibraryManager.js +8 -2
- package/dist/components/features/settings/TaxonomyLibraryManager.test.js +1 -1
- package/dist/components/task/TaskActionHeader.js +6 -3
- package/dist/components/task/TaskCard.js +6 -3
- package/dist/components/task/TaskForm.js +2 -2
- package/dist/components/task/TaskForm.test.js +41 -2
- package/dist/components/task/TaskKanban.test.js +17 -0
- package/dist/components/ui/ReferenceBadgeButton.js +1 -1
- package/dist/components/views/PlansPage.d.ts +3 -1
- package/dist/components/views/PlansPage.js +109 -49
- package/dist/components/views/PlansPage.test.d.ts +1 -0
- package/dist/components/views/PlansPage.test.js +100 -0
- package/dist/components/views/StandaloneLayout.d.ts +1 -0
- package/dist/components/views/StandaloneLayout.js +473 -256
- package/dist/components/views/panels/PlanningDrawer.js +18 -1
- package/dist/components/views/standalone/modals/AccountHubModal.d.ts +48 -0
- package/dist/components/views/standalone/modals/AccountHubModal.js +36 -0
- package/dist/components/views/standalone/modals/AccountHubModal.test.d.ts +1 -0
- package/dist/components/views/standalone/modals/AccountHubModal.test.js +26 -0
- package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.d.ts +9 -0
- package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.js +8 -0
- package/dist/components/views/standalone/modals/EditProfileModal.d.ts +26 -0
- package/dist/components/views/standalone/modals/EditProfileModal.js +10 -0
- package/dist/components/views/standalone/modals/HelpModal.d.ts +9 -0
- package/dist/components/views/standalone/modals/HelpModal.js +8 -0
- package/dist/components/views/standalone/modals/ScheduleWarningModal.d.ts +13 -0
- package/dist/components/views/standalone/modals/ScheduleWarningModal.js +6 -0
- package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +61 -0
- package/dist/components/views/standalone/modals/SyncStatusModal.js +38 -0
- package/dist/components/views/standalone/modals/TeamManagementModal.d.ts +64 -0
- package/dist/components/views/standalone/modals/TeamManagementModal.js +10 -0
- package/dist/components/views/standalone/modals/types.d.ts +2 -0
- package/dist/components/views/standalone/modals/types.js +1 -0
- package/dist/components/views/teamManagementAccess.d.ts +1 -1
- package/dist/components/views/teamManagementAccess.js +2 -2
- package/dist/components/views/teamManagementAccess.test.js +3 -3
- package/dist/config/deployment.js +2 -2
- package/dist/config/deployment.test.js +5 -3
- package/dist/core/AiProfileService.js +7 -2
- package/dist/core/AiProfiles.test.js +19 -0
- package/dist/core/EntitlementsPolicy.test.js +2 -2
- package/dist/core/GlobalSettingsService.js +78 -18
- package/dist/core/PlanEntitlementService.d.ts +29 -5
- package/dist/core/PlanEntitlementService.js +297 -219
- package/dist/core/SignupMonetizationSettings.test.js +173 -49
- package/dist/core/SyncReconciliationService.d.ts +2 -0
- package/dist/core/SyncReconciliationService.js +3 -0
- package/dist/core/SystemAdmin.test.js +186 -96
- package/dist/core/TaskAuditTimeline.test.js +40 -0
- package/dist/core/Taskforce.d.ts +100 -10
- package/dist/core/Taskforce.ids.test.js +133 -0
- package/dist/core/Taskforce.js +459 -127
- package/dist/core/Taskforce.mcpAuth.test.js +0 -2
- package/dist/core/UserProfileAvatarDrafts.test.d.ts +1 -0
- package/dist/core/UserProfileAvatarDrafts.test.js +111 -0
- package/dist/core/taskReferencePolicy.d.ts +28 -0
- package/dist/core/taskReferencePolicy.js +189 -0
- package/dist/core/types.d.ts +13 -12
- package/dist/hooks/tasks/useTaskFormActions.js +3 -0
- package/dist/hooks/useSyncOrchestrator.js +17 -3
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +97 -5
- package/dist/hooks/useTaskData.js +3 -2
- package/dist/hooks/useTaskData.test.js +66 -0
- package/dist/hooks/useTaskforce.d.ts +8 -0
- package/dist/hooks/useTaskforce.js +72 -19
- package/dist/hooks/useTaskforce.sync-behavior.test.js +185 -6
- package/dist/mcp/aiProfileBootstrap.d.ts +29 -0
- package/dist/mcp/aiProfileBootstrap.js +80 -0
- package/dist/mcp/aiProfileBootstrap.test.d.ts +1 -0
- package/dist/mcp/aiProfileBootstrap.test.js +68 -0
- package/dist/mcp/clientIntegrations.d.ts +2 -8
- package/dist/mcp/clientIntegrations.js +21 -40
- package/dist/mcp/clientIntegrations.test.js +28 -2
- package/dist/mcp/clientProfileDefaults.d.ts +12 -0
- package/dist/mcp/clientProfileDefaults.js +37 -0
- package/dist/mcp/runtime.d.ts +10 -7
- package/dist/mcp/runtime.js +131 -48
- package/dist/mcp/runtime.test.js +321 -83
- package/dist/migrations/billingSchemaParity.test.js +3 -0
- package/dist/migrations/taskSchemaMigrations.js +68 -1
- package/dist/qaOpen.test.d.ts +1 -0
- package/dist/qaOpen.test.js +56 -0
- package/dist/server/index.cookieProxy.test.js +2 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +17 -1
- package/dist/server/index.test.js +15 -1
- package/dist/server/mockStripe.d.ts +1 -0
- package/dist/server/mockStripe.js +2 -0
- package/dist/server/routes/admin.js +105 -45
- package/dist/server/routes/auth.d.ts +6 -0
- package/dist/server/routes/auth.js +505 -30
- package/dist/server/routes/billing.js +517 -24
- package/dist/server/routes/billing.test.js +967 -45
- package/dist/server/routes/documents.js +6 -1
- package/dist/server/routes/shared.d.ts +1 -1
- package/dist/server/routes/shared.js +16 -6
- package/dist/server/routes/sync.integration.test.js +290 -0
- package/dist/server/routes/sync.js +234 -1922
- package/dist/server/routes/tasks.js +18 -4
- package/dist/server/routes.js +29 -22
- package/dist/server/routes.test.js +639 -82
- package/dist/shared/runtimeContract.d.ts +1 -1
- package/dist/sync/cloudSyncApi.d.ts +1 -0
- package/dist/sync/collaborationSyncPayload.d.ts +17 -0
- package/dist/sync/collaborationSyncPayload.js +131 -0
- package/dist/sync/collaborationSyncPayload.test.d.ts +1 -0
- package/dist/sync/collaborationSyncPayload.test.js +104 -0
- package/dist/sync/collaborationSyncState.d.ts +15 -0
- package/dist/sync/collaborationSyncState.js +60 -0
- package/dist/sync/contentSyncPayload.d.ts +45 -0
- package/dist/sync/contentSyncPayload.js +104 -0
- package/dist/sync/contentSyncPayload.test.d.ts +1 -0
- package/dist/sync/contentSyncPayload.test.js +104 -0
- package/dist/sync/contentSyncState.d.ts +98 -0
- package/dist/sync/contentSyncState.js +1004 -0
- package/dist/sync/planningSyncPayload.d.ts +9 -0
- package/dist/sync/planningSyncPayload.js +82 -0
- package/dist/sync/planningSyncPayload.test.d.ts +1 -0
- package/dist/sync/planningSyncPayload.test.js +68 -0
- package/dist/sync/syncApplyHandlers.d.ts +83 -0
- package/dist/sync/syncApplyHandlers.js +445 -0
- package/dist/sync/syncService.d.ts +1 -0
- package/dist/sync/syncService.js +21 -8
- package/dist/sync/taskSyncPayload.d.ts +33 -0
- package/dist/sync/taskSyncPayload.js +126 -0
- package/dist/sync/taskSyncPayload.test.d.ts +1 -0
- package/dist/sync/taskSyncPayload.test.js +54 -0
- package/dist/sync/workspacePullFeed.d.ts +38 -0
- package/dist/sync/workspacePullFeed.js +349 -0
- package/dist/sync/workspacePullFeed.test.d.ts +1 -0
- package/dist/sync/workspacePullFeed.test.js +278 -0
- package/dist/sync/workspaceSyncModel.d.ts +4 -1
- package/dist/sync/workspaceSyncModel.js +9 -2
- package/dist/sync/workspaceSyncModel.test.js +29 -0
- package/dist/test/setup.js +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/ui/assets/{AgentsModule-Bdq3T1Ts.js → AgentsModule-CpsTmrIW.js} +1 -1
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-jKUVo-PJ.js → AnnotatedAttachmentWorkspace-C_TmXZwA.js} +1 -1
- package/dist/ui/assets/{DocumentWorkspace-DuQa-uIC.js → DocumentWorkspace-C3GyzrWm.js} +2 -2
- package/dist/ui/assets/{InitiativesModule-C8Pi2VTk.js → InitiativesModule-4-Rh2K2n.js} +1 -1
- package/dist/ui/assets/PlansPage-BP7AYOqr.js +1 -0
- package/dist/ui/assets/TaskSettings-BOC5F6Ag.js +8 -0
- package/dist/ui/assets/{TaskSettings-BwB9NtmJ.css → TaskSettings-CnIBL_Eb.css} +1 -1
- package/dist/ui/assets/{WorkflowsModule-b6TLuHwJ.js → WorkflowsModule-CGk9s3NJ.js} +1 -1
- package/dist/ui/assets/index-CLIMgR6g.js +6 -0
- package/dist/ui/assets/index-DneETxcu.css +1 -0
- package/dist/ui/assets/{vendor-icons-QZyhEwgT.js → vendor-icons-pWocEipT.js} +1 -1
- package/dist/ui/index.html +3 -3
- package/dist/utils/accountProfileSummaryCache.d.ts +31 -0
- package/dist/utils/accountProfileSummaryCache.js +97 -0
- package/dist/utils/accountProfileSummaryCache.test.d.ts +1 -0
- package/dist/utils/accountProfileSummaryCache.test.js +63 -0
- package/dist/utils/taskActivity.js +17 -4
- package/dist/utils/taskActivity.test.js +27 -0
- package/dist/utils/taskNormalization.js +10 -2
- package/dist/utils/taskReferences.d.ts +11 -1
- package/dist/utils/taskReferences.js +33 -1
- package/package.json +4 -3
- package/scripts/qa-open-lib.mjs +175 -0
- package/scripts/qa-open-lib.mjs.d.ts +49 -0
- package/scripts/qa-open.mjs +352 -0
- package/dist/ui/assets/PlansPage-B6BDECSi.js +0 -1
- package/dist/ui/assets/TaskSettings-DUJgRTPU.js +0 -8
- package/dist/ui/assets/index-BiiF1cLQ.css +0 -1
- package/dist/ui/assets/index-xBWLPOEb.js +0 -6
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
// @ts-ignore - JS helper module without generated .d.ts; covered by focused tests.
|
|
5
|
+
import { assertQaProjectRootSafe, matchExpectedWorkspace, parseEnvFileContents, parseQaArgs, resolveExpectedWorkspace, resolveQaPaths } from '../scripts/qa-open-lib.mjs';
|
|
6
|
+
describe('qa open helper library', () => {
|
|
7
|
+
it('derives a stable QA root layout for an agent', () => {
|
|
8
|
+
const homeDir = path.join(os.tmpdir(), 'qa-open-test-home');
|
|
9
|
+
const paths = resolveQaPaths({ agentId: 'Codex Main', homeDir });
|
|
10
|
+
expect(paths.agentId).toBe('codex-main');
|
|
11
|
+
expect(paths.root).toContain(path.join('.taskforce', 'qa', 'codex-main'));
|
|
12
|
+
expect(paths.projectRoot).toContain(path.join('.taskforce', 'qa', 'codex-main', 'project'));
|
|
13
|
+
expect(paths.playwrightStorageState).toContain(path.join('.taskforce', 'qa', 'codex-main', 'playwright-auth.json'));
|
|
14
|
+
});
|
|
15
|
+
it('parses env files with comments, export statements, and quoted values', () => {
|
|
16
|
+
const env = parseEnvFileContents(`
|
|
17
|
+
# comment
|
|
18
|
+
export TASKFORCE_E2E_EMAIL="codex@example.com"
|
|
19
|
+
TASKFORCE_E2E_PASSWORD='secret'
|
|
20
|
+
TASKFORCE_QA_EXPECTED_WORKSPACE_SLUG=codex-qa
|
|
21
|
+
`);
|
|
22
|
+
expect(env.TASKFORCE_E2E_EMAIL).toBe('codex@example.com');
|
|
23
|
+
expect(env.TASKFORCE_E2E_PASSWORD).toBe('secret');
|
|
24
|
+
expect(env.TASKFORCE_QA_EXPECTED_WORKSPACE_SLUG).toBe('codex-qa');
|
|
25
|
+
});
|
|
26
|
+
it('rejects QA roots inside the repository', () => {
|
|
27
|
+
const repoRoot = '/repo/taskforce';
|
|
28
|
+
expect(() => assertQaProjectRootSafe('/repo/taskforce', repoRoot)).toThrow(/inside the repository/i);
|
|
29
|
+
expect(() => assertQaProjectRootSafe('/repo/taskforce/tmp/qa', repoRoot)).toThrow(/inside the repository/i);
|
|
30
|
+
expect(() => assertQaProjectRootSafe('/tmp/taskforce-qa', repoRoot)).not.toThrow();
|
|
31
|
+
});
|
|
32
|
+
it('matches expected workspace by slug against the current workspace id', () => {
|
|
33
|
+
const match = matchExpectedWorkspace({
|
|
34
|
+
currentWorkspaceId: 'workspace-2',
|
|
35
|
+
workspaces: [
|
|
36
|
+
{ id: 'workspace-1', slug: 'alpha' },
|
|
37
|
+
{ id: 'workspace-2', slug: 'beta' }
|
|
38
|
+
]
|
|
39
|
+
}, { workspaceId: null, workspaceSlug: 'beta' });
|
|
40
|
+
expect(match.ok).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
it('builds args and expected workspace from env', () => {
|
|
43
|
+
const parsed = parseQaArgs([], {
|
|
44
|
+
TASKFORCE_QA_AGENT: 'Claude QA',
|
|
45
|
+
TASKFORCE_QA_MODE: 'staging',
|
|
46
|
+
TASKFORCE_QA_PORT: '5199'
|
|
47
|
+
});
|
|
48
|
+
expect(parsed.agentId).toBe('claude-qa');
|
|
49
|
+
expect(parsed.mode).toBe('staging');
|
|
50
|
+
expect(parsed.preferredPort).toBe(5199);
|
|
51
|
+
const expected = resolveExpectedWorkspace({
|
|
52
|
+
TASKFORCE_QA_EXPECTED_WORKSPACE_ID: 'workspace-123'
|
|
53
|
+
});
|
|
54
|
+
expect(expected).toEqual({ workspaceId: 'workspace-123', workspaceSlug: null });
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -22,6 +22,8 @@ describe('proxy cookie rewrite helpers', () => {
|
|
|
22
22
|
expect(shouldProxyCloudPath('/api/taskforce/public/site-config-script')).toBe(true);
|
|
23
23
|
expect(shouldProxyCloudPath('/api/taskforce/version')).toBe(true);
|
|
24
24
|
expect(shouldProxyCloudPath('/api/taskforce/account/entitlements')).toBe(true);
|
|
25
|
+
expect(shouldProxyCloudPath('/api/taskforce/account/profile-summary')).toBe(true);
|
|
26
|
+
expect(shouldProxyCloudPath('/api/taskforce/account/team-management-access')).toBe(true);
|
|
25
27
|
expect(shouldProxyCloudPath('/api/taskforce/admin/mcp-audit-logs')).toBe(true);
|
|
26
28
|
expect(shouldProxyCloudPath('/api/taskforce/billing/status')).toBe(true);
|
|
27
29
|
expect(shouldProxyCloudPath('/api/taskforce/billing/checkout-session')).toBe(true);
|
package/dist/server/index.d.ts
CHANGED
|
@@ -22,6 +22,7 @@ interface AuthorizedRequestAccess extends RequestAccessContext {
|
|
|
22
22
|
export declare function shouldProxyCloudPath(pathname: string): boolean;
|
|
23
23
|
export declare function resolveCloudProxyUpstreamBaseUrl(req: http.IncomingMessage, fallbackBaseUrl: string): string;
|
|
24
24
|
export declare function resolveAuthorizedWorkspaceAccess(core: Pick<TaskforceCore, 'resolveUserAccess' | 'getSystemRole'>, access: AuthorizedRequestAccess, requestedWorkspaceId: string): AuthorizedRequestAccess;
|
|
25
|
+
export declare function resolveEffectiveRequestUserId(accessUserId: string, explicitUserId: string): string;
|
|
25
26
|
export declare function resolveMaxPayloadBytesForPath(pathname: string, defaultMaxBytes: number): number;
|
|
26
27
|
export declare function shouldRewriteSetCookieForLocalDevHost(hostHeader: string | undefined): boolean;
|
|
27
28
|
export declare function writeJsonErrorIfPossible(res: http.ServerResponse, statusCode: number, payload: Record<string, unknown>): boolean;
|
package/dist/server/index.js
CHANGED
|
@@ -109,6 +109,8 @@ export function shouldProxyCloudPath(pathname) {
|
|
|
109
109
|
|| pathname === PUBLIC_SITE_CONFIG_SCRIPT_PATH
|
|
110
110
|
|| pathname === '/api/taskforce/version'
|
|
111
111
|
|| pathname === '/api/taskforce/account/entitlements'
|
|
112
|
+
|| pathname === '/api/taskforce/account/profile-summary'
|
|
113
|
+
|| pathname === '/api/taskforce/account/team-management-access'
|
|
112
114
|
|| pathname.startsWith('/api/taskforce/admin/')
|
|
113
115
|
|| pathname.startsWith('/api/taskforce/billing/')
|
|
114
116
|
|| pathname.startsWith('/api/taskforce/auth/')
|
|
@@ -145,6 +147,17 @@ export function resolveAuthorizedWorkspaceAccess(core, access, requestedWorkspac
|
|
|
145
147
|
}
|
|
146
148
|
return access;
|
|
147
149
|
}
|
|
150
|
+
export function resolveEffectiveRequestUserId(accessUserId, explicitUserId) {
|
|
151
|
+
const normalizedAccessUserId = String(accessUserId || '').trim();
|
|
152
|
+
if (normalizedAccessUserId && normalizedAccessUserId !== 'anonymous') {
|
|
153
|
+
return normalizedAccessUserId;
|
|
154
|
+
}
|
|
155
|
+
const normalizedExplicitUserId = String(explicitUserId || '').trim();
|
|
156
|
+
if (normalizedExplicitUserId && normalizedExplicitUserId !== 'anonymous') {
|
|
157
|
+
return normalizedExplicitUserId;
|
|
158
|
+
}
|
|
159
|
+
return 'anonymous';
|
|
160
|
+
}
|
|
148
161
|
export function resolveMaxPayloadBytesForPath(pathname, defaultMaxBytes) {
|
|
149
162
|
if (pathname.startsWith('/api/taskforce/sync/')) {
|
|
150
163
|
// Sync routes (especially applying local changes) often include large binary attachments
|
|
@@ -677,7 +690,10 @@ export function createStandaloneServer(options) {
|
|
|
677
690
|
req.headers['x-taskforce-role'] = access.role;
|
|
678
691
|
req.headers['x-taskforce-system-role'] = access.systemRole;
|
|
679
692
|
req.headers['x-taskforce-workspace-id'] = access.workspaceId;
|
|
680
|
-
req.headers['x-taskforce-user-id']
|
|
693
|
+
const explicitRequestUserId = Array.isArray(req.headers['x-taskforce-user-id'])
|
|
694
|
+
? String(req.headers['x-taskforce-user-id'][0] || '')
|
|
695
|
+
: String(req.headers['x-taskforce-user-id'] || '');
|
|
696
|
+
req.headers['x-taskforce-user-id'] = resolveEffectiveRequestUserId(access.userId, explicitRequestUserId);
|
|
681
697
|
if (isCollaborationPolicyEnabled() && isMutation && access.role === 'read-only') {
|
|
682
698
|
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
683
699
|
res.end(JSON.stringify({ error: 'Forbidden: read-only role cannot mutate data.' }));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { ensureBillingWebhookAuthExclusion, ensurePublicSiteConfigAuthExclusion, isBillingWebhookPath, resolveAuthorizedWorkspaceAccess, shouldApplyAuthRateLimitForPath, shouldProxyCloudPath, writeJsonErrorIfPossible } from './index.js';
|
|
2
|
+
import { ensureBillingWebhookAuthExclusion, ensurePublicSiteConfigAuthExclusion, isBillingWebhookPath, resolveEffectiveRequestUserId, resolveAuthorizedWorkspaceAccess, shouldApplyAuthRateLimitForPath, shouldProxyCloudPath, writeJsonErrorIfPossible } from './index.js';
|
|
3
3
|
describe('server index helper policies', () => {
|
|
4
4
|
it('adds webhook path to auth exclusions without duplicates', () => {
|
|
5
5
|
const base = ['/api/taskforce/health', '/api/taskforce/auth'];
|
|
@@ -86,6 +86,8 @@ describe('shouldProxyCloudPath', () => {
|
|
|
86
86
|
expect(shouldProxyCloudPath('/api/taskforce/admin/users')).toBe(true);
|
|
87
87
|
expect(shouldProxyCloudPath('/api/taskforce/billing/plans')).toBe(true);
|
|
88
88
|
expect(shouldProxyCloudPath('/api/taskforce/auth/login')).toBe(true);
|
|
89
|
+
expect(shouldProxyCloudPath('/api/taskforce/account/profile-summary')).toBe(true);
|
|
90
|
+
expect(shouldProxyCloudPath('/api/taskforce/account/team-management-access')).toBe(true);
|
|
89
91
|
expect(shouldProxyCloudPath('/api/taskforce/public/site-config')).toBe(true);
|
|
90
92
|
expect(shouldProxyCloudPath('/api/taskforce/public/site-config-script')).toBe(true);
|
|
91
93
|
});
|
|
@@ -148,3 +150,15 @@ describe('resolveAuthorizedWorkspaceAccess', () => {
|
|
|
148
150
|
expect(next.workspaceId).toBe('workspace-personal');
|
|
149
151
|
});
|
|
150
152
|
});
|
|
153
|
+
describe('resolveEffectiveRequestUserId', () => {
|
|
154
|
+
it('prefers the authenticated access user when present', () => {
|
|
155
|
+
expect(resolveEffectiveRequestUserId('user-authenticated', 'user-explicit')).toBe('user-authenticated');
|
|
156
|
+
});
|
|
157
|
+
it('preserves an explicit local actor header when auth access is anonymous', () => {
|
|
158
|
+
expect(resolveEffectiveRequestUserId('anonymous', 'user-local-actor')).toBe('user-local-actor');
|
|
159
|
+
expect(resolveEffectiveRequestUserId('', 'user-local-actor')).toBe('user-local-actor');
|
|
160
|
+
});
|
|
161
|
+
it('falls back to anonymous when neither source has a user id', () => {
|
|
162
|
+
expect(resolveEffectiveRequestUserId('anonymous', '')).toBe('anonymous');
|
|
163
|
+
});
|
|
164
|
+
});
|
|
@@ -122,6 +122,7 @@ function formatMockCheckoutSession(record, expandSubscription = false) {
|
|
|
122
122
|
metadata: { ...record.metadata },
|
|
123
123
|
payment_status: record.paymentStatus,
|
|
124
124
|
status: record.status,
|
|
125
|
+
expires_at: record.expiresAt,
|
|
125
126
|
subscription: record.subscriptionId
|
|
126
127
|
? (expandSubscription ? formatMockSubscription(getMockStripeSubscription(record.subscriptionId)) : record.subscriptionId)
|
|
127
128
|
: null
|
|
@@ -243,6 +244,7 @@ export function createMockStripeClient() {
|
|
|
243
244
|
successUrl: String(params.success_url || '').trim(),
|
|
244
245
|
cancelUrl: String(params.cancel_url || '').trim(),
|
|
245
246
|
subscriptionId: null,
|
|
247
|
+
expiresAt: Math.floor(Date.now() / 1000) + (24 * 60 * 60),
|
|
246
248
|
paymentStatus: 'unpaid',
|
|
247
249
|
status: 'open'
|
|
248
250
|
};
|
|
@@ -59,6 +59,7 @@ import { normalizeStorageIntegrityScheduleSettings, resolveStorageIntegritySched
|
|
|
59
59
|
import { resolveVirusScanConfigFromEnv } from '../../security/virusScan.js';
|
|
60
60
|
import { applyPrefixedRateLimitHeaders } from '../rateLimit.js';
|
|
61
61
|
import { createTaskforceMcpServer, isMcpWriteToolName } from '../../mcp/runtime.js';
|
|
62
|
+
import { parseAiProfileBootstrapHeaders } from '../../mcp/aiProfileBootstrap.js';
|
|
62
63
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
63
64
|
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
|
|
64
65
|
import { randomUUID } from 'crypto';
|
|
@@ -81,33 +82,47 @@ export function registerAdminRoutes(deps) {
|
|
|
81
82
|
const globalSettings = core.getGlobalSettings();
|
|
82
83
|
return resolveEffectiveEmailConfig(process.env, globalSettings.email || null);
|
|
83
84
|
};
|
|
85
|
+
const resolveBooleanSetting = (value, fallback) => {
|
|
86
|
+
if (typeof value === 'boolean')
|
|
87
|
+
return value;
|
|
88
|
+
if (typeof value === 'number')
|
|
89
|
+
return value !== 0;
|
|
90
|
+
const normalized = String(value || '').trim().toLowerCase();
|
|
91
|
+
if (!normalized)
|
|
92
|
+
return fallback;
|
|
93
|
+
if (['1', 'true', 'yes', 'on'].includes(normalized))
|
|
94
|
+
return true;
|
|
95
|
+
if (['0', 'false', 'no', 'off'].includes(normalized))
|
|
96
|
+
return false;
|
|
97
|
+
return fallback;
|
|
98
|
+
};
|
|
84
99
|
const getAuthBehavior = () => {
|
|
85
100
|
const globalSettings = core.getGlobalSettings();
|
|
86
101
|
const authSettings = globalSettings.auth || {};
|
|
87
102
|
const authRateLimitMaxRequestsRaw = Number(authSettings.authRateLimitMaxRequests);
|
|
88
103
|
const authRateLimitWindowMsRaw = Number(authSettings.authRateLimitWindowMs);
|
|
89
|
-
const
|
|
104
|
+
const onboardingPolicy = core.getOnboardingPolicy();
|
|
90
105
|
return {
|
|
91
|
-
allowSelfRegistration: authSettings.allowSelfRegistration
|
|
92
|
-
requireVerifiedEmail: authSettings.requireVerifiedEmail
|
|
93
|
-
defaultNewUserBetaAccess: authSettings.defaultNewUserBetaAccess
|
|
94
|
-
authRateLimitEnabled: authSettings.authRateLimitEnabled
|
|
106
|
+
allowSelfRegistration: resolveBooleanSetting(authSettings.allowSelfRegistration, true),
|
|
107
|
+
requireVerifiedEmail: resolveBooleanSetting(authSettings.requireVerifiedEmail, true),
|
|
108
|
+
defaultNewUserBetaAccess: resolveBooleanSetting(authSettings.defaultNewUserBetaAccess, false),
|
|
109
|
+
authRateLimitEnabled: resolveBooleanSetting(authSettings.authRateLimitEnabled, true),
|
|
95
110
|
authRateLimitMaxRequests: Number.isFinite(authRateLimitMaxRequestsRaw) && authRateLimitMaxRequestsRaw >= 1
|
|
96
111
|
? Math.floor(authRateLimitMaxRequestsRaw)
|
|
97
112
|
: 10,
|
|
98
113
|
authRateLimitWindowMs: Number.isFinite(authRateLimitWindowMsRaw) && authRateLimitWindowMsRaw >= 1000
|
|
99
114
|
? Math.floor(authRateLimitWindowMsRaw)
|
|
100
115
|
: 15 * 60_000,
|
|
101
|
-
|
|
102
|
-
|
|
116
|
+
onboardingPolicyMode: onboardingPolicy.mode,
|
|
117
|
+
onboardingPlanVersionId: onboardingPolicy.planVersionId
|
|
103
118
|
};
|
|
104
119
|
};
|
|
105
120
|
const getSiteBehavior = () => {
|
|
106
121
|
const globalSettings = core.getGlobalSettings();
|
|
107
122
|
const siteSettings = globalSettings.site || {};
|
|
108
123
|
return {
|
|
109
|
-
showRunTaskforceLocally: siteSettings.showRunTaskforceLocally
|
|
110
|
-
pricingPageEnabled: siteSettings.pricingPageEnabled
|
|
124
|
+
showRunTaskforceLocally: resolveBooleanSetting(siteSettings.showRunTaskforceLocally, true),
|
|
125
|
+
pricingPageEnabled: resolveBooleanSetting(siteSettings.pricingPageEnabled, true)
|
|
111
126
|
};
|
|
112
127
|
};
|
|
113
128
|
const resolveEffectiveVirusScanSettings = () => {
|
|
@@ -148,6 +163,10 @@ export function registerAdminRoutes(deps) {
|
|
|
148
163
|
}
|
|
149
164
|
return String(configured || resolvePublicAuthBaseUrl(req)).replace(/\/+$/, '');
|
|
150
165
|
};
|
|
166
|
+
const resolveAdminCloudEnvironment = (req) => {
|
|
167
|
+
return resolveTaskforceCloudEnvironmentFromBaseUrl(resolveRuntimeConfigBaseUrl(req))
|
|
168
|
+
|| resolveTaskforceCloudEnvironmentFromBaseUrl(resolveCloudLoginBaseUrl(req));
|
|
169
|
+
};
|
|
151
170
|
const resolveMcpOauthAuthorizeUrl = (req) => `${resolveMcpOauthIssuer(req)}/oauth/mcp/authorize`;
|
|
152
171
|
const resolveMcpOauthTokenUrl = (req) => `${resolveMcpOauthIssuer(req)}/oauth/mcp/token`;
|
|
153
172
|
const resolveMcpOauthRegisterUrl = (req) => `${resolveMcpOauthIssuer(req)}/oauth/mcp/register`;
|
|
@@ -264,13 +283,7 @@ export function registerAdminRoutes(deps) {
|
|
|
264
283
|
? authorization.slice(7).trim()
|
|
265
284
|
: '';
|
|
266
285
|
const requestedWorkspaceId = requestHeader(req, 'x-taskforce-workspace-id', '').trim();
|
|
267
|
-
const
|
|
268
|
-
const aiProfileName = requestHeader(req, 'x-taskforce-ai-name', '').trim() || null;
|
|
269
|
-
const aiProfileDescription = requestHeader(req, 'x-taskforce-ai-description', '').trim() || null;
|
|
270
|
-
const aiProfileRole = requestHeader(req, 'x-taskforce-ai-role', '').trim() || null;
|
|
271
|
-
const aiProfileProvider = requestHeader(req, 'x-taskforce-ai-provider', '').trim() || null;
|
|
272
|
-
const aiProfileModel = requestHeader(req, 'x-taskforce-ai-model', '').trim() || null;
|
|
273
|
-
const aiProfileSurfaceType = requestHeader(req, 'x-taskforce-ai-surface-type', '').trim() || null;
|
|
286
|
+
const bootstrap = parseAiProfileBootstrapHeaders(req.headers);
|
|
274
287
|
if (bearerToken) {
|
|
275
288
|
const tokenAuth = core.authenticateMcpAccessToken(bearerToken);
|
|
276
289
|
const oauthAuth = tokenAuth ? null : core.authenticateMcpOAuthAccessToken(bearerToken);
|
|
@@ -318,13 +331,7 @@ export function registerAdminRoutes(deps) {
|
|
|
318
331
|
authenticatedViaToken: tokenAuth || null,
|
|
319
332
|
authenticatedViaOAuth: oauthAuth || null,
|
|
320
333
|
userId: null,
|
|
321
|
-
|
|
322
|
-
aiProfileName,
|
|
323
|
-
aiProfileDescription,
|
|
324
|
-
aiProfileRole,
|
|
325
|
-
aiProfileProvider,
|
|
326
|
-
aiProfileModel,
|
|
327
|
-
aiProfileSurfaceType,
|
|
334
|
+
...bootstrap,
|
|
328
335
|
};
|
|
329
336
|
}
|
|
330
337
|
core.addMcpAuditLog({
|
|
@@ -961,6 +968,51 @@ export function registerAdminRoutes(deps) {
|
|
|
961
968
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
962
969
|
res.end(JSON.stringify({ success: true }));
|
|
963
970
|
});
|
|
971
|
+
// DELETE /api/taskforce/admin/system-users/:id - Purge a test user in non-production cloud environments
|
|
972
|
+
addRoute('DELETE', '/api/taskforce/admin/system-users/:id', async (req, res, params) => {
|
|
973
|
+
if (!requireCloudRuntime(res, 'System user purge'))
|
|
974
|
+
return;
|
|
975
|
+
if (!ensureAdminRole(req)) {
|
|
976
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
977
|
+
res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
const environment = resolveAdminCloudEnvironment(req);
|
|
981
|
+
if (environment !== 'staging' && environment !== 'performance') {
|
|
982
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
983
|
+
res.end(JSON.stringify({
|
|
984
|
+
error: 'System user purge is only available in staging and performance.',
|
|
985
|
+
code: 'SYSTEM_USER_PURGE_ENVIRONMENT_FORBIDDEN'
|
|
986
|
+
}));
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
const body = await parseJsonBody(req);
|
|
990
|
+
const actor = resolveAdminRequestActor(req);
|
|
991
|
+
try {
|
|
992
|
+
const summary = core.purgeSystemUser({
|
|
993
|
+
targetUserId: params.id,
|
|
994
|
+
actorUserId: actor.userId,
|
|
995
|
+
emailConfirmation: String(body?.emailConfirmation || '')
|
|
996
|
+
});
|
|
997
|
+
auditAdminAction(req, 'purge-system-user', {
|
|
998
|
+
targetUserId: params.id,
|
|
999
|
+
targetEmail: summary.email,
|
|
1000
|
+
environment,
|
|
1001
|
+
summary
|
|
1002
|
+
});
|
|
1003
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1004
|
+
res.end(JSON.stringify({ success: true, environment, summary }));
|
|
1005
|
+
}
|
|
1006
|
+
catch (error) {
|
|
1007
|
+
const statusCode = resolveErrorStatusCode(error, 400);
|
|
1008
|
+
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
|
1009
|
+
res.end(JSON.stringify({
|
|
1010
|
+
error: String(error?.message || 'Unable to purge system user'),
|
|
1011
|
+
code: error?.code,
|
|
1012
|
+
details: error?.details || null
|
|
1013
|
+
}));
|
|
1014
|
+
}
|
|
1015
|
+
});
|
|
964
1016
|
// === SECTION: Admin email settings ===
|
|
965
1017
|
// GET /api/taskforce/admin/email-settings - Read email provider settings
|
|
966
1018
|
addRoute('GET', '/api/taskforce/admin/email-settings', async (req, res) => {
|
|
@@ -1033,7 +1085,9 @@ export function registerAdminRoutes(deps) {
|
|
|
1033
1085
|
res.end(JSON.stringify({ error: 'Forbidden: admin role required.' }));
|
|
1034
1086
|
return;
|
|
1035
1087
|
}
|
|
1036
|
-
const
|
|
1088
|
+
const globalSettings = core.getGlobalSettings();
|
|
1089
|
+
const authSettings = globalSettings.auth || {};
|
|
1090
|
+
const billingSettings = globalSettings.billing || {};
|
|
1037
1091
|
const effective = getAuthBehavior();
|
|
1038
1092
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1039
1093
|
res.end(JSON.stringify({
|
|
@@ -1044,11 +1098,13 @@ export function registerAdminRoutes(deps) {
|
|
|
1044
1098
|
...(typeof authSettings.authRateLimitEnabled === 'boolean' ? { authRateLimitEnabled: authSettings.authRateLimitEnabled } : {}),
|
|
1045
1099
|
...(typeof authSettings.authRateLimitMaxRequests === 'number' ? { authRateLimitMaxRequests: authSettings.authRateLimitMaxRequests } : {}),
|
|
1046
1100
|
...(typeof authSettings.authRateLimitWindowMs === 'number' ? { authRateLimitWindowMs: authSettings.authRateLimitWindowMs } : {}),
|
|
1047
|
-
...((
|
|
1048
|
-
|
|
1101
|
+
...((billingSettings.onboardingPolicy?.mode === 'require_plan_selection'
|
|
1102
|
+
|| billingSettings.onboardingPolicy?.mode === 'auto_grant_free'
|
|
1103
|
+
|| billingSettings.onboardingPolicy?.mode === 'auto_start_trial')
|
|
1104
|
+
? { onboardingPolicyMode: billingSettings.onboardingPolicy.mode }
|
|
1049
1105
|
: {}),
|
|
1050
|
-
...(typeof
|
|
1051
|
-
? {
|
|
1106
|
+
...(typeof billingSettings.onboardingPolicy?.planVersionId === 'string' || billingSettings.onboardingPolicy?.planVersionId === null
|
|
1107
|
+
? { onboardingPlanVersionId: billingSettings.onboardingPolicy?.planVersionId ?? null }
|
|
1052
1108
|
: {})
|
|
1053
1109
|
},
|
|
1054
1110
|
status: {
|
|
@@ -1058,8 +1114,8 @@ export function registerAdminRoutes(deps) {
|
|
|
1058
1114
|
authRateLimitEnabled: effective.authRateLimitEnabled,
|
|
1059
1115
|
authRateLimitMaxRequests: effective.authRateLimitMaxRequests,
|
|
1060
1116
|
authRateLimitWindowMs: effective.authRateLimitWindowMs,
|
|
1061
|
-
|
|
1062
|
-
|
|
1117
|
+
onboardingPolicyMode: effective.onboardingPolicyMode,
|
|
1118
|
+
onboardingPlanVersionId: effective.onboardingPlanVersionId
|
|
1063
1119
|
}
|
|
1064
1120
|
}));
|
|
1065
1121
|
});
|
|
@@ -1685,21 +1741,21 @@ export function registerAdminRoutes(deps) {
|
|
|
1685
1741
|
return;
|
|
1686
1742
|
}
|
|
1687
1743
|
const body = await parseJsonBody(req);
|
|
1688
|
-
const
|
|
1689
|
-
?
|
|
1690
|
-
: '
|
|
1691
|
-
const
|
|
1692
|
-
? body.
|
|
1693
|
-
: body?.
|
|
1744
|
+
const onboardingPolicyMode = body?.onboardingPolicyMode === 'auto_grant_free' || body?.onboardingPolicyMode === 'auto_start_trial'
|
|
1745
|
+
? body.onboardingPolicyMode
|
|
1746
|
+
: 'require_plan_selection';
|
|
1747
|
+
const onboardingPlanVersionId = typeof body?.onboardingPlanVersionId === 'string'
|
|
1748
|
+
? body.onboardingPlanVersionId.trim() || null
|
|
1749
|
+
: body?.onboardingPlanVersionId === null
|
|
1694
1750
|
? null
|
|
1695
1751
|
: undefined;
|
|
1696
|
-
const validation = core.
|
|
1697
|
-
|
|
1698
|
-
...(
|
|
1752
|
+
const validation = core.validateOnboardingPolicy({
|
|
1753
|
+
mode: onboardingPolicyMode,
|
|
1754
|
+
...(onboardingPlanVersionId !== undefined ? { planVersionId: onboardingPlanVersionId } : {})
|
|
1699
1755
|
});
|
|
1700
1756
|
if (!validation.valid) {
|
|
1701
1757
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1702
|
-
res.end(JSON.stringify({ error: validation.message || 'Invalid
|
|
1758
|
+
res.end(JSON.stringify({ error: validation.message || 'Invalid onboarding policy.', code: validation.code, details: validation.details }));
|
|
1703
1759
|
return;
|
|
1704
1760
|
}
|
|
1705
1761
|
core.updateGlobalSettings({
|
|
@@ -1711,9 +1767,13 @@ export function registerAdminRoutes(deps) {
|
|
|
1711
1767
|
...(typeof body?.authRateLimitMaxRequests === 'number' && Number.isFinite(body.authRateLimitMaxRequests)
|
|
1712
1768
|
? { authRateLimitMaxRequests: Math.max(1, Math.floor(body.authRateLimitMaxRequests)) } : {}),
|
|
1713
1769
|
...(typeof body?.authRateLimitWindowMs === 'number' && Number.isFinite(body.authRateLimitWindowMs)
|
|
1714
|
-
? { authRateLimitWindowMs: Math.max(1000, Math.floor(body.authRateLimitWindowMs)) } : {})
|
|
1715
|
-
|
|
1716
|
-
|
|
1770
|
+
? { authRateLimitWindowMs: Math.max(1000, Math.floor(body.authRateLimitWindowMs)) } : {})
|
|
1771
|
+
},
|
|
1772
|
+
billing: {
|
|
1773
|
+
onboardingPolicy: {
|
|
1774
|
+
mode: onboardingPolicyMode,
|
|
1775
|
+
planVersionId: onboardingPlanVersionId ?? null
|
|
1776
|
+
}
|
|
1717
1777
|
}
|
|
1718
1778
|
});
|
|
1719
1779
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
@@ -1812,7 +1872,7 @@ export function registerAdminRoutes(deps) {
|
|
|
1812
1872
|
addRoute('OPTIONS', '/mcp', async (_req, res) => {
|
|
1813
1873
|
res.writeHead(204, {
|
|
1814
1874
|
'Access-Control-Allow-Origin': '*',
|
|
1815
|
-
'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-description, x-taskforce-ai-role, x-taskforce-ai-provider, x-taskforce-ai-model, x-taskforce-ai-surface-type',
|
|
1875
|
+
'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',
|
|
1816
1876
|
'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS'
|
|
1817
1877
|
});
|
|
1818
1878
|
res.end();
|
|
@@ -1820,7 +1880,7 @@ export function registerAdminRoutes(deps) {
|
|
|
1820
1880
|
addRoute('OPTIONS', '/mcp/', async (_req, res) => {
|
|
1821
1881
|
res.writeHead(204, {
|
|
1822
1882
|
'Access-Control-Allow-Origin': '*',
|
|
1823
|
-
'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-description, x-taskforce-ai-role, x-taskforce-ai-provider, x-taskforce-ai-model, x-taskforce-ai-surface-type',
|
|
1883
|
+
'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',
|
|
1824
1884
|
'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS'
|
|
1825
1885
|
});
|
|
1826
1886
|
res.end();
|
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
* POST /api/taskforce/auth/invite/join
|
|
21
21
|
* POST /api/taskforce/auth/logout
|
|
22
22
|
* GET /api/taskforce/account/access-status
|
|
23
|
+
* GET /api/taskforce/account/profile-summary
|
|
23
24
|
* GET /api/taskforce/account/entitlements
|
|
25
|
+
* GET /api/taskforce/account/team-management-access
|
|
24
26
|
* GET /api/taskforce/account/plan-preview
|
|
25
27
|
* GET /api/taskforce/auth/workspace-members
|
|
26
28
|
* GET /api/taskforce/workspace/assignee-options
|
|
@@ -36,6 +38,8 @@
|
|
|
36
38
|
import type { IncomingMessage, ServerResponse } from 'http';
|
|
37
39
|
import type { TaskforceCore } from '../../core/Taskforce.js';
|
|
38
40
|
import type { RouteHandler, RouteContext } from '../routes.js';
|
|
41
|
+
import type { ResolvedObjectStorageConfig } from '../../storage/objectStorage.js';
|
|
42
|
+
import { ObjectStorageClient } from '../../storage/objectStorageClient.js';
|
|
39
43
|
export interface AuthRouteDependencies {
|
|
40
44
|
addRoute: (method: string, path: string, handler: RouteHandler) => void;
|
|
41
45
|
core: TaskforceCore;
|
|
@@ -57,5 +61,7 @@ export interface AuthRouteDependencies {
|
|
|
57
61
|
shouldExposeDevAuthTokens: () => boolean;
|
|
58
62
|
resolveRuntimeConfigBaseUrl: (req: IncomingMessage) => string;
|
|
59
63
|
resolvePublicAuthBaseUrl: (req: IncomingMessage) => string;
|
|
64
|
+
resolveEffectiveObjectStorageConfig: () => ResolvedObjectStorageConfig;
|
|
65
|
+
getObjectStorageClient: () => ObjectStorageClient | null;
|
|
60
66
|
}
|
|
61
67
|
export declare function registerAuthRoutes(deps: AuthRouteDependencies): void;
|