@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.
Files changed (171) hide show
  1. package/dist/Taskforce.module.css +37 -2
  2. package/dist/TaskforceCore.filter-persistence.test.js +1 -1
  3. package/dist/TaskforceCore.js +72 -33
  4. package/dist/TaskforceCore.test.js +848 -130
  5. package/dist/components/features/TaskSettings.js +14 -82
  6. package/dist/components/features/TaskSettings.test.js +33 -70
  7. package/dist/components/features/settings/TaxonomyLibraryManager.js +8 -2
  8. package/dist/components/features/settings/TaxonomyLibraryManager.test.js +1 -1
  9. package/dist/components/task/TaskActionHeader.js +6 -3
  10. package/dist/components/task/TaskCard.js +6 -3
  11. package/dist/components/task/TaskForm.js +2 -2
  12. package/dist/components/task/TaskForm.test.js +41 -2
  13. package/dist/components/task/TaskKanban.test.js +17 -0
  14. package/dist/components/ui/ReferenceBadgeButton.js +1 -1
  15. package/dist/components/views/PlansPage.d.ts +3 -1
  16. package/dist/components/views/PlansPage.js +109 -49
  17. package/dist/components/views/PlansPage.test.d.ts +1 -0
  18. package/dist/components/views/PlansPage.test.js +100 -0
  19. package/dist/components/views/StandaloneLayout.d.ts +1 -0
  20. package/dist/components/views/StandaloneLayout.js +473 -256
  21. package/dist/components/views/panels/PlanningDrawer.js +18 -1
  22. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +48 -0
  23. package/dist/components/views/standalone/modals/AccountHubModal.js +36 -0
  24. package/dist/components/views/standalone/modals/AccountHubModal.test.d.ts +1 -0
  25. package/dist/components/views/standalone/modals/AccountHubModal.test.js +26 -0
  26. package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.d.ts +9 -0
  27. package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.js +8 -0
  28. package/dist/components/views/standalone/modals/EditProfileModal.d.ts +26 -0
  29. package/dist/components/views/standalone/modals/EditProfileModal.js +10 -0
  30. package/dist/components/views/standalone/modals/HelpModal.d.ts +9 -0
  31. package/dist/components/views/standalone/modals/HelpModal.js +8 -0
  32. package/dist/components/views/standalone/modals/ScheduleWarningModal.d.ts +13 -0
  33. package/dist/components/views/standalone/modals/ScheduleWarningModal.js +6 -0
  34. package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +61 -0
  35. package/dist/components/views/standalone/modals/SyncStatusModal.js +38 -0
  36. package/dist/components/views/standalone/modals/TeamManagementModal.d.ts +64 -0
  37. package/dist/components/views/standalone/modals/TeamManagementModal.js +10 -0
  38. package/dist/components/views/standalone/modals/types.d.ts +2 -0
  39. package/dist/components/views/standalone/modals/types.js +1 -0
  40. package/dist/components/views/teamManagementAccess.d.ts +1 -1
  41. package/dist/components/views/teamManagementAccess.js +2 -2
  42. package/dist/components/views/teamManagementAccess.test.js +3 -3
  43. package/dist/config/deployment.js +2 -2
  44. package/dist/config/deployment.test.js +5 -3
  45. package/dist/core/AiProfileService.js +7 -2
  46. package/dist/core/AiProfiles.test.js +19 -0
  47. package/dist/core/EntitlementsPolicy.test.js +2 -2
  48. package/dist/core/GlobalSettingsService.js +78 -18
  49. package/dist/core/PlanEntitlementService.d.ts +29 -5
  50. package/dist/core/PlanEntitlementService.js +297 -219
  51. package/dist/core/SignupMonetizationSettings.test.js +173 -49
  52. package/dist/core/SyncReconciliationService.d.ts +2 -0
  53. package/dist/core/SyncReconciliationService.js +3 -0
  54. package/dist/core/SystemAdmin.test.js +186 -96
  55. package/dist/core/TaskAuditTimeline.test.js +40 -0
  56. package/dist/core/Taskforce.d.ts +100 -10
  57. package/dist/core/Taskforce.ids.test.js +133 -0
  58. package/dist/core/Taskforce.js +459 -127
  59. package/dist/core/Taskforce.mcpAuth.test.js +0 -2
  60. package/dist/core/UserProfileAvatarDrafts.test.d.ts +1 -0
  61. package/dist/core/UserProfileAvatarDrafts.test.js +111 -0
  62. package/dist/core/taskReferencePolicy.d.ts +28 -0
  63. package/dist/core/taskReferencePolicy.js +189 -0
  64. package/dist/core/types.d.ts +13 -12
  65. package/dist/hooks/tasks/useTaskFormActions.js +3 -0
  66. package/dist/hooks/useSyncOrchestrator.js +17 -3
  67. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +97 -5
  68. package/dist/hooks/useTaskData.js +3 -2
  69. package/dist/hooks/useTaskData.test.js +66 -0
  70. package/dist/hooks/useTaskforce.d.ts +8 -0
  71. package/dist/hooks/useTaskforce.js +72 -19
  72. package/dist/hooks/useTaskforce.sync-behavior.test.js +185 -6
  73. package/dist/mcp/aiProfileBootstrap.d.ts +29 -0
  74. package/dist/mcp/aiProfileBootstrap.js +80 -0
  75. package/dist/mcp/aiProfileBootstrap.test.d.ts +1 -0
  76. package/dist/mcp/aiProfileBootstrap.test.js +68 -0
  77. package/dist/mcp/clientIntegrations.d.ts +2 -8
  78. package/dist/mcp/clientIntegrations.js +21 -40
  79. package/dist/mcp/clientIntegrations.test.js +28 -2
  80. package/dist/mcp/clientProfileDefaults.d.ts +12 -0
  81. package/dist/mcp/clientProfileDefaults.js +37 -0
  82. package/dist/mcp/runtime.d.ts +10 -7
  83. package/dist/mcp/runtime.js +131 -48
  84. package/dist/mcp/runtime.test.js +321 -83
  85. package/dist/migrations/billingSchemaParity.test.js +3 -0
  86. package/dist/migrations/taskSchemaMigrations.js +68 -1
  87. package/dist/qaOpen.test.d.ts +1 -0
  88. package/dist/qaOpen.test.js +56 -0
  89. package/dist/server/index.cookieProxy.test.js +2 -0
  90. package/dist/server/index.d.ts +1 -0
  91. package/dist/server/index.js +17 -1
  92. package/dist/server/index.test.js +15 -1
  93. package/dist/server/mockStripe.d.ts +1 -0
  94. package/dist/server/mockStripe.js +2 -0
  95. package/dist/server/routes/admin.js +105 -45
  96. package/dist/server/routes/auth.d.ts +6 -0
  97. package/dist/server/routes/auth.js +505 -30
  98. package/dist/server/routes/billing.js +517 -24
  99. package/dist/server/routes/billing.test.js +967 -45
  100. package/dist/server/routes/documents.js +6 -1
  101. package/dist/server/routes/shared.d.ts +1 -1
  102. package/dist/server/routes/shared.js +16 -6
  103. package/dist/server/routes/sync.integration.test.js +290 -0
  104. package/dist/server/routes/sync.js +234 -1922
  105. package/dist/server/routes/tasks.js +18 -4
  106. package/dist/server/routes.js +29 -22
  107. package/dist/server/routes.test.js +639 -82
  108. package/dist/shared/runtimeContract.d.ts +1 -1
  109. package/dist/sync/cloudSyncApi.d.ts +1 -0
  110. package/dist/sync/collaborationSyncPayload.d.ts +17 -0
  111. package/dist/sync/collaborationSyncPayload.js +131 -0
  112. package/dist/sync/collaborationSyncPayload.test.d.ts +1 -0
  113. package/dist/sync/collaborationSyncPayload.test.js +104 -0
  114. package/dist/sync/collaborationSyncState.d.ts +15 -0
  115. package/dist/sync/collaborationSyncState.js +60 -0
  116. package/dist/sync/contentSyncPayload.d.ts +45 -0
  117. package/dist/sync/contentSyncPayload.js +104 -0
  118. package/dist/sync/contentSyncPayload.test.d.ts +1 -0
  119. package/dist/sync/contentSyncPayload.test.js +104 -0
  120. package/dist/sync/contentSyncState.d.ts +98 -0
  121. package/dist/sync/contentSyncState.js +1004 -0
  122. package/dist/sync/planningSyncPayload.d.ts +9 -0
  123. package/dist/sync/planningSyncPayload.js +82 -0
  124. package/dist/sync/planningSyncPayload.test.d.ts +1 -0
  125. package/dist/sync/planningSyncPayload.test.js +68 -0
  126. package/dist/sync/syncApplyHandlers.d.ts +83 -0
  127. package/dist/sync/syncApplyHandlers.js +445 -0
  128. package/dist/sync/syncService.d.ts +1 -0
  129. package/dist/sync/syncService.js +21 -8
  130. package/dist/sync/taskSyncPayload.d.ts +33 -0
  131. package/dist/sync/taskSyncPayload.js +126 -0
  132. package/dist/sync/taskSyncPayload.test.d.ts +1 -0
  133. package/dist/sync/taskSyncPayload.test.js +54 -0
  134. package/dist/sync/workspacePullFeed.d.ts +38 -0
  135. package/dist/sync/workspacePullFeed.js +349 -0
  136. package/dist/sync/workspacePullFeed.test.d.ts +1 -0
  137. package/dist/sync/workspacePullFeed.test.js +278 -0
  138. package/dist/sync/workspaceSyncModel.d.ts +4 -1
  139. package/dist/sync/workspaceSyncModel.js +9 -2
  140. package/dist/sync/workspaceSyncModel.test.js +29 -0
  141. package/dist/test/setup.js +1 -1
  142. package/dist/types.d.ts +1 -0
  143. package/dist/ui/assets/{AgentsModule-Bdq3T1Ts.js → AgentsModule-CpsTmrIW.js} +1 -1
  144. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-jKUVo-PJ.js → AnnotatedAttachmentWorkspace-C_TmXZwA.js} +1 -1
  145. package/dist/ui/assets/{DocumentWorkspace-DuQa-uIC.js → DocumentWorkspace-C3GyzrWm.js} +2 -2
  146. package/dist/ui/assets/{InitiativesModule-C8Pi2VTk.js → InitiativesModule-4-Rh2K2n.js} +1 -1
  147. package/dist/ui/assets/PlansPage-BP7AYOqr.js +1 -0
  148. package/dist/ui/assets/TaskSettings-BOC5F6Ag.js +8 -0
  149. package/dist/ui/assets/{TaskSettings-BwB9NtmJ.css → TaskSettings-CnIBL_Eb.css} +1 -1
  150. package/dist/ui/assets/{WorkflowsModule-b6TLuHwJ.js → WorkflowsModule-CGk9s3NJ.js} +1 -1
  151. package/dist/ui/assets/index-CLIMgR6g.js +6 -0
  152. package/dist/ui/assets/index-DneETxcu.css +1 -0
  153. package/dist/ui/assets/{vendor-icons-QZyhEwgT.js → vendor-icons-pWocEipT.js} +1 -1
  154. package/dist/ui/index.html +3 -3
  155. package/dist/utils/accountProfileSummaryCache.d.ts +31 -0
  156. package/dist/utils/accountProfileSummaryCache.js +97 -0
  157. package/dist/utils/accountProfileSummaryCache.test.d.ts +1 -0
  158. package/dist/utils/accountProfileSummaryCache.test.js +63 -0
  159. package/dist/utils/taskActivity.js +17 -4
  160. package/dist/utils/taskActivity.test.js +27 -0
  161. package/dist/utils/taskNormalization.js +10 -2
  162. package/dist/utils/taskReferences.d.ts +11 -1
  163. package/dist/utils/taskReferences.js +33 -1
  164. package/package.json +4 -3
  165. package/scripts/qa-open-lib.mjs +175 -0
  166. package/scripts/qa-open-lib.mjs.d.ts +49 -0
  167. package/scripts/qa-open.mjs +352 -0
  168. package/dist/ui/assets/PlansPage-B6BDECSi.js +0 -1
  169. package/dist/ui/assets/TaskSettings-DUJgRTPU.js +0 -8
  170. package/dist/ui/assets/index-BiiF1cLQ.css +0 -1
  171. 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);
@@ -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;
@@ -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'] = access.userId;
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
+ });
@@ -11,6 +11,7 @@ type MockStripeCheckoutSessionRecord = {
11
11
  successUrl: string;
12
12
  cancelUrl: string;
13
13
  subscriptionId: string | null;
14
+ expiresAt: number;
14
15
  paymentStatus: 'unpaid' | 'paid';
15
16
  status: 'open' | 'complete' | 'expired';
16
17
  };
@@ -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 signupSettings = core.getSignupMonetizationSettings();
104
+ const onboardingPolicy = core.getOnboardingPolicy();
90
105
  return {
91
- allowSelfRegistration: authSettings.allowSelfRegistration !== false,
92
- requireVerifiedEmail: authSettings.requireVerifiedEmail !== false,
93
- defaultNewUserBetaAccess: authSettings.defaultNewUserBetaAccess === true,
94
- authRateLimitEnabled: authSettings.authRateLimitEnabled !== false,
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
- signupMonetizationStrategy: signupSettings.strategy,
102
- defaultSignupPlanVersionId: signupSettings.defaultSignupPlanVersionId
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 !== false,
110
- pricingPageEnabled: siteSettings.pricingPageEnabled !== false
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 aiProfileToken = requestHeader(req, 'x-taskforce-ai-profile-token', '').trim() || null;
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
- aiProfileToken,
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 authSettings = core.getGlobalSettings().auth || {};
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
- ...((authSettings.signupMonetizationStrategy === 'auto_assign_default' || authSettings.signupMonetizationStrategy === 'plan_selection_required')
1048
- ? { signupMonetizationStrategy: authSettings.signupMonetizationStrategy }
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 authSettings.defaultSignupPlanVersionId === 'string' || authSettings.defaultSignupPlanVersionId === null
1051
- ? { defaultSignupPlanVersionId: authSettings.defaultSignupPlanVersionId ?? null }
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
- signupMonetizationStrategy: effective.signupMonetizationStrategy,
1062
- defaultSignupPlanVersionId: effective.defaultSignupPlanVersionId
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 signupMonetizationStrategy = body?.signupMonetizationStrategy === 'plan_selection_required'
1689
- ? 'plan_selection_required'
1690
- : 'auto_assign_default';
1691
- const defaultSignupPlanVersionId = typeof body?.defaultSignupPlanVersionId === 'string'
1692
- ? body.defaultSignupPlanVersionId.trim() || null
1693
- : body?.defaultSignupPlanVersionId === null
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.validateSignupMonetizationSettings({
1697
- strategy: signupMonetizationStrategy,
1698
- ...(defaultSignupPlanVersionId !== undefined ? { defaultSignupPlanVersionId } : {})
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 signup monetization settings.', code: validation.code, details: validation.details }));
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
- signupMonetizationStrategy,
1716
- ...(defaultSignupPlanVersionId !== undefined ? { defaultSignupPlanVersionId } : {})
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;