@taskforcehq/taskforce 0.3.302 → 0.3.304

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 (192) hide show
  1. package/dist/Taskforce.module.css +113 -5
  2. package/dist/TaskforceCore.js +560 -208
  3. package/dist/TaskforceCore.test.js +1833 -91
  4. package/dist/annotatedAttachments/service.d.ts +1 -1
  5. package/dist/annotatedAttachments/types.d.ts +1 -1
  6. package/dist/components/features/AgentsModule.js +1 -1
  7. package/dist/components/features/AgentsModule.test.js +1 -1
  8. package/dist/components/features/TaskSettings.js +58 -38
  9. package/dist/components/features/TaskSettings.test.js +139 -40
  10. package/dist/components/task/TaskActionHeader.d.ts +2 -1
  11. package/dist/components/task/TaskActionHeader.js +3 -3
  12. package/dist/components/task/TaskActionHeader.test.js +8 -0
  13. package/dist/components/task/TaskCard.js +16 -1
  14. package/dist/components/task/TaskCard.test.js +31 -0
  15. package/dist/components/task/TaskForm.d.ts +2 -1
  16. package/dist/components/task/TaskForm.js +22 -4
  17. package/dist/components/task/TaskForm.test.js +46 -0
  18. package/dist/components/task/TaskKanban.d.ts +1 -1
  19. package/dist/components/task/TaskKanban.js +31 -160
  20. package/dist/components/task/TaskKanban.test.js +17 -0
  21. package/dist/components/task/TaskStatusActions.d.ts +6 -1
  22. package/dist/components/task/TaskStatusActions.js +8 -3
  23. package/dist/components/task/TaskStatusActions.test.js +9 -0
  24. package/dist/components/task/taskKanbanDropRules.d.ts +58 -0
  25. package/dist/components/task/taskKanbanDropRules.js +187 -0
  26. package/dist/components/task/taskKanbanDropRules.test.d.ts +1 -0
  27. package/dist/components/task/taskKanbanDropRules.test.js +114 -0
  28. package/dist/components/ui/Modal.d.ts +1 -1
  29. package/dist/components/ui/Modal.js +1 -0
  30. package/dist/components/views/AppShellHeader.js +5 -1
  31. package/dist/components/views/AppShellHeader.test.js +4 -0
  32. package/dist/components/views/PlanComparisonPage.d.ts +3 -2
  33. package/dist/components/views/PlanComparisonPage.js +4 -2
  34. package/dist/components/views/PlanComparisonPage.test.js +4 -0
  35. package/dist/components/views/PlansPage.d.ts +7 -1
  36. package/dist/components/views/PlansPage.js +139 -53
  37. package/dist/components/views/PlansPage.test.d.ts +1 -0
  38. package/dist/components/views/PlansPage.test.js +157 -0
  39. package/dist/components/views/StandaloneLayout.d.ts +1 -0
  40. package/dist/components/views/StandaloneLayout.js +216 -186
  41. package/dist/components/views/WidgetView.js +11 -2
  42. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +8 -3
  43. package/dist/components/views/standalone/modals/AccountHubModal.js +16 -2
  44. package/dist/components/views/standalone/modals/AccountHubModal.test.d.ts +1 -0
  45. package/dist/components/views/standalone/modals/AccountHubModal.test.js +26 -0
  46. package/dist/components/views/standalone/modals/SyncStatusModal.js +1 -1
  47. package/dist/core/DeletedTaskLifecycleService.d.ts +2 -1
  48. package/dist/core/EntitlementsPolicy.test.js +2 -2
  49. package/dist/core/GlobalSettingsService.js +78 -18
  50. package/dist/core/PlanEntitlementService.d.ts +31 -5
  51. package/dist/core/PlanEntitlementService.js +350 -222
  52. package/dist/core/SignupMonetizationSettings.test.js +208 -49
  53. package/dist/core/SystemAdmin.test.js +186 -96
  54. package/dist/core/TaskActivityService.d.ts +4 -1
  55. package/dist/core/TaskActivityService.js +45 -0
  56. package/dist/core/TaskAuditTimeline.test.js +52 -1
  57. package/dist/core/Taskforce.d.ts +65 -20
  58. package/dist/core/Taskforce.js +261 -51
  59. package/dist/core/Taskforce.mcpAuth.test.js +0 -2
  60. package/dist/core/UserProfileAvatarDrafts.test.js +12 -3
  61. package/dist/core/WorkspacePlanMode.test.js +65 -0
  62. package/dist/core/types.d.ts +11 -12
  63. package/dist/documentReviews/service.d.ts +1 -1
  64. package/dist/documentReviews/types.d.ts +1 -1
  65. package/dist/hooks/auth/useTaskforceAuthBootstrap.d.ts +48 -0
  66. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +219 -0
  67. package/dist/hooks/sync/auth.d.ts +37 -0
  68. package/dist/hooks/sync/auth.js +61 -0
  69. package/dist/hooks/sync/bootstrap.d.ts +34 -0
  70. package/dist/hooks/sync/bootstrap.js +52 -0
  71. package/dist/hooks/sync/orchestratorShared.d.ts +36 -0
  72. package/dist/hooks/sync/orchestratorShared.js +209 -0
  73. package/dist/hooks/sync/orchestratorShared.test.d.ts +1 -0
  74. package/dist/hooks/sync/orchestratorShared.test.js +49 -0
  75. package/dist/hooks/sync/realtime.d.ts +25 -0
  76. package/dist/hooks/sync/realtime.js +60 -0
  77. package/dist/hooks/sync/recovery.d.ts +69 -0
  78. package/dist/hooks/sync/recovery.js +118 -0
  79. package/dist/hooks/sync/transfers.d.ts +122 -0
  80. package/dist/hooks/sync/transfers.js +431 -0
  81. package/dist/hooks/sync/useDataVersionRefresh.d.ts +16 -0
  82. package/dist/hooks/sync/useDataVersionRefresh.js +73 -0
  83. package/dist/hooks/ui/useResourceExport.d.ts +3 -1
  84. package/dist/hooks/ui/useResourceExport.js +5 -2
  85. package/dist/hooks/ui/useWorkflowTemplates.js +11 -6
  86. package/dist/hooks/useRealtimeSync.js +2 -1
  87. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +8 -0
  88. package/dist/hooks/useSyncOrchestrator.context-assets.test.d.ts +1 -0
  89. package/dist/hooks/useSyncOrchestrator.context-assets.test.js +228 -0
  90. package/dist/hooks/useSyncOrchestrator.d.ts +5 -4
  91. package/dist/hooks/useSyncOrchestrator.js +315 -860
  92. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +665 -8
  93. package/dist/hooks/useTaskMutations.js +45 -6
  94. package/dist/hooks/useTaskMutations.test.js +147 -0
  95. package/dist/hooks/useTaskforce.d.ts +11 -2
  96. package/dist/hooks/useTaskforce.js +290 -812
  97. package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
  98. package/dist/hooks/useWorkspaceSyncController.js +22 -10
  99. package/dist/hooks/workspace/useTaskforceApiRouting.test.js +17 -0
  100. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +166 -0
  101. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +434 -0
  102. package/dist/hooks/workspace/workspaceLocalState.d.ts +19 -0
  103. package/dist/hooks/workspace/workspaceLocalState.js +100 -0
  104. package/dist/hooks/workspace/workspaceLocalState.test.d.ts +1 -0
  105. package/dist/hooks/workspace/workspaceLocalState.test.js +59 -0
  106. package/dist/localization/locales/en-US.js +2 -2
  107. package/dist/mcp/canonicalAssetHelpers.d.ts +70 -0
  108. package/dist/mcp/canonicalAssetHelpers.js +259 -0
  109. package/dist/mcp/clientIntegrations.d.ts +12 -1
  110. package/dist/mcp/clientIntegrations.js +161 -6
  111. package/dist/mcp/clientIntegrations.test.js +76 -4
  112. package/dist/mcp/documentHelpers.d.ts +100 -0
  113. package/dist/mcp/documentHelpers.js +161 -0
  114. package/dist/mcp/runtime.js +264 -505
  115. package/dist/mcp/runtime.test.js +353 -89
  116. package/dist/mcp/taskAttachmentHelpers.d.ts +41 -0
  117. package/dist/mcp/taskAttachmentHelpers.js +60 -0
  118. package/dist/mcp/taskAttachmentTypes.d.ts +37 -0
  119. package/dist/mcp/taskAttachmentTypes.js +1 -0
  120. package/dist/migrations/billingSchemaParity.test.js +3 -0
  121. package/dist/migrations/taskSchemaMigrations.js +15 -0
  122. package/dist/plugins/vite.js +1 -1
  123. package/dist/runtime/appGateDefinitions.d.ts +1 -1
  124. package/dist/runtime/appGateDefinitions.js +1 -1
  125. package/dist/server/index.cookieProxy.test.js +1 -0
  126. package/dist/server/index.d.ts +1 -0
  127. package/dist/server/index.js +48 -3
  128. package/dist/server/index.test.js +14 -2
  129. package/dist/server/mockStripe.d.ts +1 -0
  130. package/dist/server/mockStripe.js +2 -0
  131. package/dist/server/routes/admin.js +91 -40
  132. package/dist/server/routes/auth.d.ts +1 -0
  133. package/dist/server/routes/auth.js +231 -55
  134. package/dist/server/routes/billing.js +473 -39
  135. package/dist/server/routes/billing.test.js +972 -37
  136. package/dist/server/routes/shared.d.ts +30 -0
  137. package/dist/server/routes/shared.js +31 -3
  138. package/dist/server/routes/sync.integration.test.js +180 -0
  139. package/dist/server/routes/sync.js +125 -242
  140. package/dist/server/routes/tasks.js +49 -7
  141. package/dist/server/routes.js +11 -22
  142. package/dist/server/routes.test.js +530 -81
  143. package/dist/shared/taskActor.d.ts +10 -0
  144. package/dist/shared/taskActor.js +1 -0
  145. package/dist/sync/cloudSyncApi.d.ts +1 -0
  146. package/dist/sync/collaborationSyncPayload.js +4 -0
  147. package/dist/sync/collaborationSyncPayload.test.js +8 -0
  148. package/dist/sync/collaborationSyncState.d.ts +3 -1
  149. package/dist/sync/collaborationSyncState.js +19 -0
  150. package/dist/sync/contentSyncState.d.ts +10 -0
  151. package/dist/sync/contentSyncState.js +277 -14
  152. package/dist/sync/syncApplyHandlers.d.ts +87 -1
  153. package/dist/sync/syncApplyHandlers.js +370 -8
  154. package/dist/sync/syncApplyHandlers.test.d.ts +1 -0
  155. package/dist/sync/syncApplyHandlers.test.js +377 -0
  156. package/dist/sync/syncService.d.ts +9 -1
  157. package/dist/sync/syncService.js +57 -10
  158. package/dist/sync/syncService.test.js +48 -0
  159. package/dist/sync/workspacePullFeed.d.ts +11 -0
  160. package/dist/sync/workspacePullFeed.js +123 -68
  161. package/dist/sync/workspacePullFeed.test.js +180 -0
  162. package/dist/sync/workspaceSyncModel.d.ts +22 -1
  163. package/dist/sync/workspaceSyncModel.js +65 -0
  164. package/dist/sync/workspaceSyncModel.test.js +119 -0
  165. package/dist/types.d.ts +2 -10
  166. package/dist/ui/assets/AgentsModule-BLWVbuKP.js +1 -0
  167. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-Dvi02Y7q.js → AnnotatedAttachmentWorkspace-BlS-cqnl.js} +1 -1
  168. package/dist/ui/assets/{DocumentWorkspace-iys3zfca.js → DocumentWorkspace-BH_6DF6x.js} +1 -1
  169. package/dist/ui/assets/{InitiativesModule-DJvLJI3A.js → InitiativesModule-BjR6iBf9.js} +1 -1
  170. package/dist/ui/assets/{PlansPage-BlVC_lRq.css → PlansPage-C2dd2n1X.css} +1 -1
  171. package/dist/ui/assets/PlansPage-CSDQ4sLa.js +1 -0
  172. package/dist/ui/assets/TaskSettings-CEDy34Jo.js +9 -0
  173. package/dist/ui/assets/{WorkflowsModule-BguHeQaV.js → WorkflowsModule-DnFqdUME.js} +1 -1
  174. package/dist/ui/assets/index-DXUdtH1B.js +6 -0
  175. package/dist/ui/assets/index-Ii0B0rTO.css +1 -0
  176. package/dist/ui/assets/{vendor-icons-C8EED3Oh.js → vendor-icons-I7ZwG1z_.js} +1 -1
  177. package/dist/ui/index.html +4 -4
  178. package/dist/utils/accountProfileSummaryCache.d.ts +31 -0
  179. package/dist/utils/accountProfileSummaryCache.js +97 -0
  180. package/dist/utils/accountProfileSummaryCache.test.d.ts +1 -0
  181. package/dist/utils/accountProfileSummaryCache.test.js +63 -0
  182. package/dist/utils/taskActivity.js +4 -0
  183. package/dist/utils/taskActivity.test.js +25 -1
  184. package/package.json +5 -3
  185. package/scripts/playwright-auth.sh +4 -0
  186. package/dist/ui/assets/AgentsModule-JG5jc3he.js +0 -1
  187. package/dist/ui/assets/PlansPage-BMPlpFpw.js +0 -1
  188. package/dist/ui/assets/TaskSettings-Bqd4cCzW.js +0 -8
  189. package/dist/ui/assets/index-MXeWoebC.css +0 -1
  190. package/dist/ui/assets/index-YJ4k5QNa.js +0 -6
  191. package/dist/utils/billingStatusCache.d.ts +0 -21
  192. package/dist/utils/billingStatusCache.js +0 -70
@@ -0,0 +1,37 @@
1
+ import type { WorkspaceAssetKind } from '../storage/workspaceAssetStore.js';
2
+ export type TaskAttachmentRecord = {
3
+ path: string;
4
+ fsPath?: string;
5
+ caption?: string;
6
+ displayName?: string;
7
+ originalFilename?: string;
8
+ timestamp?: string;
9
+ assetId?: string;
10
+ referenceNumber?: number | null;
11
+ referenceLabel?: string | null;
12
+ taskId?: string | null;
13
+ };
14
+ export type McpTaskAttachmentDescriptor = {
15
+ attachmentId: string;
16
+ assetId?: string;
17
+ referenceNumber?: number | null;
18
+ referenceLabel?: string | null;
19
+ documentId?: string | null;
20
+ path: string;
21
+ fsPath?: string;
22
+ caption?: string;
23
+ displayName?: string;
24
+ originalFilename?: string;
25
+ kind: WorkspaceAssetKind;
26
+ mimeType: string;
27
+ sizeBytes: number | null;
28
+ timestamp?: string;
29
+ source: 'canonical' | 'external';
30
+ readableAsText: boolean;
31
+ preferredNextTool?: string;
32
+ preferredWorkflow?: string;
33
+ suggestedArgs?: {
34
+ documentId: string;
35
+ };
36
+ guidance?: string;
37
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -25,6 +25,7 @@ describe('billing schema parity', () => {
25
25
  expect(names.has('user_billing')).toBe(true);
26
26
  expect(names.has('stripe_webhook_events')).toBe(true);
27
27
  expect(names.has('stripe_webhook_failures')).toBe(true);
28
+ expect(names.has('billing_checkout_attempts')).toBe(true);
28
29
  expect(names.has('billing_config')).toBe(true);
29
30
  expect(names.has('billing_price_mappings')).toBe(true);
30
31
  }
@@ -101,9 +102,11 @@ describe('billing schema parity', () => {
101
102
  expect(sql).toContain('CREATE TABLE IF NOT EXISTS user_billing');
102
103
  expect(sql).toContain('CREATE TABLE IF NOT EXISTS stripe_webhook_events');
103
104
  expect(sql).toContain('CREATE TABLE IF NOT EXISTS stripe_webhook_failures');
105
+ expect(sql).toContain('CREATE TABLE IF NOT EXISTS billing_checkout_attempts');
104
106
  expect(sql).toContain('CREATE TABLE IF NOT EXISTS billing_config');
105
107
  expect(sql).toContain('CREATE TABLE IF NOT EXISTS billing_price_mappings');
106
108
  expect(sql).toContain('CREATE INDEX IF NOT EXISTS idx_stripe_webhook_failures_purge_after');
109
+ expect(sql).toContain('CREATE INDEX IF NOT EXISTS idx_billing_checkout_attempts_user_status');
107
110
  expect(sql).toContain('CREATE INDEX IF NOT EXISTS idx_billing_price_mappings_plan');
108
111
  expect(sql).toContain('CREATE UNIQUE INDEX IF NOT EXISTS idx_billing_price_mappings_active_interval');
109
112
  });
@@ -493,6 +493,18 @@ export function ensureBillingSchema(db) {
493
493
  created_at TEXT NOT NULL
494
494
  );
495
495
 
496
+ CREATE TABLE IF NOT EXISTS billing_checkout_attempts (
497
+ session_id TEXT PRIMARY KEY,
498
+ tenant_id TEXT NOT NULL DEFAULT 'default',
499
+ user_id TEXT NOT NULL,
500
+ plan_version_id TEXT NOT NULL,
501
+ billing_interval TEXT,
502
+ status TEXT NOT NULL DEFAULT 'open',
503
+ expires_at TEXT,
504
+ created_at TEXT NOT NULL,
505
+ updated_at TEXT NOT NULL
506
+ );
507
+
496
508
  CREATE TABLE IF NOT EXISTS billing_config (
497
509
  tenant_id TEXT PRIMARY KEY,
498
510
  config_json TEXT NOT NULL DEFAULT '{}',
@@ -520,6 +532,8 @@ export function ensureBillingSchema(db) {
520
532
  CREATE INDEX IF NOT EXISTS idx_stripe_webhook_failures_tenant ON stripe_webhook_failures(tenant_id);
521
533
  CREATE INDEX IF NOT EXISTS idx_stripe_webhook_failures_event ON stripe_webhook_failures(event_id);
522
534
  CREATE INDEX IF NOT EXISTS idx_stripe_webhook_failures_purge_after ON stripe_webhook_failures(purge_after);
535
+ CREATE INDEX IF NOT EXISTS idx_billing_checkout_attempts_user_status ON billing_checkout_attempts(tenant_id, user_id, status, created_at);
536
+ CREATE INDEX IF NOT EXISTS idx_billing_checkout_attempts_plan_status ON billing_checkout_attempts(tenant_id, plan_version_id, status, created_at);
523
537
  CREATE INDEX IF NOT EXISTS idx_billing_price_mappings_plan ON billing_price_mappings(tenant_id, plan_version_id);
524
538
  CREATE INDEX IF NOT EXISTS idx_billing_price_mappings_active ON billing_price_mappings(tenant_id, active);
525
539
  `);
@@ -723,6 +737,7 @@ export function ensureEntitlementsSchema(db) {
723
737
  updated_by TEXT,
724
738
  created_at TEXT NOT NULL,
725
739
  updated_at TEXT NOT NULL,
740
+ FOREIGN KEY (account_id) REFERENCES users(id) ON DELETE CASCADE,
726
741
  FOREIGN KEY (plan_version_id) REFERENCES plan_versions(plan_version_id)
727
742
  );
728
743
 
@@ -190,7 +190,7 @@ export function taskforcePlugin(options) {
190
190
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
191
191
  <link rel="icon" type="image/svg+xml" href="/src/assets/favicon/favicon.svg" />
192
192
  <link rel="icon" type="image/x-icon" href="/src/assets/favicon/favicon.ico" />
193
- <title>Taskforce Dashboard</title>
193
+ <title>Taskforce</title>
194
194
  <style>
195
195
  body { margin: 0; background: #0f0f1a; color: white; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; }
196
196
  #root { height: 100vh; overflow: hidden; }
@@ -12,7 +12,7 @@ export declare const APP_GATE_DEFINITIONS: {
12
12
  readonly removalMilestone: "Document Manager GA";
13
13
  };
14
14
  readonly 'workflows.workspace_access': {
15
- readonly enabled: true;
15
+ readonly enabled: false;
16
16
  readonly description: "Workflow workspace readiness gate.";
17
17
  readonly owner: "taskforce";
18
18
  readonly removalMilestone: "Workflow workspace GA";
@@ -12,7 +12,7 @@ export const APP_GATE_DEFINITIONS = {
12
12
  removalMilestone: 'Document Manager GA'
13
13
  },
14
14
  'workflows.workspace_access': {
15
- enabled: true,
15
+ enabled: false,
16
16
  description: 'Workflow workspace readiness gate.',
17
17
  owner: 'taskforce',
18
18
  removalMilestone: 'Workflow workspace GA'
@@ -22,6 +22,7 @@ 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);
25
26
  expect(shouldProxyCloudPath('/api/taskforce/account/team-management-access')).toBe(true);
26
27
  expect(shouldProxyCloudPath('/api/taskforce/admin/mcp-audit-logs')).toBe(true);
27
28
  expect(shouldProxyCloudPath('/api/taskforce/billing/status')).toBe(true);
@@ -28,6 +28,7 @@ export declare function shouldRewriteSetCookieForLocalDevHost(hostHeader: string
28
28
  export declare function writeJsonErrorIfPossible(res: http.ServerResponse, statusCode: number, payload: Record<string, unknown>): boolean;
29
29
  export declare function applyCloudProxyForwardedHeaders(headers: Headers, req: http.IncomingMessage): void;
30
30
  export declare function rewriteProxySetCookieForLocalHost(cookie: string): string;
31
+ export declare function shouldRewriteProxySetCookieDomain(cookie: string, hostHeader: string | undefined): boolean;
31
32
  export declare function shouldApplyAuthRateLimitForPath(pathname: string): boolean;
32
33
  export declare function resolveCloudProxyBaseUrl(env?: NodeJS.ProcessEnv): string;
33
34
  export declare function ensureBillingWebhookAuthExclusion(excludedPaths: string[]): string[];
@@ -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/')
@@ -203,6 +205,43 @@ export function rewriteProxySetCookieForLocalHost(cookie) {
203
205
  const filteredAttrs = attrs.filter((attr) => !attr.toLowerCase().startsWith('domain='));
204
206
  return [nameValue, ...filteredAttrs].join('; ');
205
207
  }
208
+ function normalizeHostHeaderHost(hostHeader) {
209
+ const raw = String(hostHeader || '').trim().toLowerCase();
210
+ if (!raw)
211
+ return '';
212
+ if (raw.startsWith('[')) {
213
+ const closing = raw.indexOf(']');
214
+ if (closing >= 0)
215
+ return raw.slice(1, closing);
216
+ }
217
+ const portSeparator = raw.lastIndexOf(':');
218
+ if (portSeparator <= 0)
219
+ return raw;
220
+ const maybePort = raw.slice(portSeparator + 1);
221
+ if (/^\d+$/.test(maybePort)) {
222
+ return raw.slice(0, portSeparator);
223
+ }
224
+ return raw;
225
+ }
226
+ function parseSetCookieDomain(cookie) {
227
+ const parts = String(cookie || '').split(';').map((part) => part.trim()).filter(Boolean);
228
+ for (const part of parts.slice(1)) {
229
+ const lower = part.toLowerCase();
230
+ if (!lower.startsWith('domain='))
231
+ continue;
232
+ return part.slice('domain='.length).trim().replace(/^\.+/, '').toLowerCase();
233
+ }
234
+ return '';
235
+ }
236
+ export function shouldRewriteProxySetCookieDomain(cookie, hostHeader) {
237
+ const requestHost = normalizeHostHeaderHost(hostHeader);
238
+ const cookieDomain = parseSetCookieDomain(cookie);
239
+ if (!requestHost || !cookieDomain)
240
+ return false;
241
+ if (requestHost === cookieDomain)
242
+ return false;
243
+ return !requestHost.endsWith(`.${cookieDomain}`);
244
+ }
206
245
  async function readRequestBody(req, maxBytes) {
207
246
  const chunks = [];
208
247
  let total = 0;
@@ -548,9 +587,15 @@ export function createStandaloneServer(options) {
548
587
  }
549
588
  const setCookies = upstream.headers.getSetCookie?.();
550
589
  if (Array.isArray(setCookies) && setCookies.length > 0) {
551
- const normalizedSetCookies = shouldRewriteSetCookieForLocalDevHost(String(req.headers.host || '').trim())
552
- ? setCookies.map((cookie) => rewriteProxySetCookieForLocalHost(cookie))
553
- : setCookies;
590
+ const requestHost = String(req.headers.host || '').trim();
591
+ const normalizedSetCookies = setCookies.map((cookie) => {
592
+ if (shouldRewriteSetCookieForLocalDevHost(requestHost)) {
593
+ return rewriteProxySetCookieForLocalHost(cookie);
594
+ }
595
+ return shouldRewriteProxySetCookieDomain(cookie, requestHost)
596
+ ? rewriteProxySetCookieForLocalHost(cookie)
597
+ : cookie;
598
+ });
554
599
  res.setHeader('Set-Cookie', normalizedSetCookies);
555
600
  }
556
601
  res.writeHead(upstream.status, outgoingHeaders);
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it, vi } from 'vitest';
2
- import { ensureBillingWebhookAuthExclusion, ensurePublicSiteConfigAuthExclusion, isBillingWebhookPath, resolveEffectiveRequestUserId, resolveAuthorizedWorkspaceAccess, shouldApplyAuthRateLimitForPath, shouldProxyCloudPath, writeJsonErrorIfPossible } from './index.js';
2
+ import { ensureBillingWebhookAuthExclusion, ensurePublicSiteConfigAuthExclusion, isBillingWebhookPath, rewriteProxySetCookieForLocalHost, resolveEffectiveRequestUserId, resolveAuthorizedWorkspaceAccess, shouldRewriteProxySetCookieDomain, 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,11 +86,23 @@ 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/team-management-access')).toBe(false);
89
+ expect(shouldProxyCloudPath('/api/taskforce/account/profile-summary')).toBe(true);
90
+ expect(shouldProxyCloudPath('/api/taskforce/account/team-management-access')).toBe(true);
90
91
  expect(shouldProxyCloudPath('/api/taskforce/public/site-config')).toBe(true);
91
92
  expect(shouldProxyCloudPath('/api/taskforce/public/site-config-script')).toBe(true);
92
93
  });
93
94
  });
95
+ describe('proxy cookie helpers', () => {
96
+ it('rewrites mismatched cookie domains for non-loopback local hosts', () => {
97
+ const cookie = 'taskforce_session=abc; Path=/; HttpOnly; SameSite=Lax; Domain=taskforcehq.ai';
98
+ expect(shouldRewriteProxySetCookieDomain(cookie, 'taskforce.localtest.me:3847')).toBe(true);
99
+ expect(rewriteProxySetCookieForLocalHost(cookie)).not.toContain('Domain=');
100
+ });
101
+ it('preserves cookie domains that already match the request host', () => {
102
+ const cookie = 'taskforce_session=abc; Path=/; HttpOnly; SameSite=Lax; Domain=taskforcehq.ai';
103
+ expect(shouldRewriteProxySetCookieDomain(cookie, 'app.taskforcehq.ai')).toBe(false);
104
+ });
105
+ });
94
106
  describe('resolveAuthorizedWorkspaceAccess', () => {
95
107
  it('prefers an explicitly requested workspace when the user is authorized for it', () => {
96
108
  const core = {
@@ -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
  };
@@ -65,7 +65,7 @@ import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
65
65
  import { randomUUID } from 'crypto';
66
66
  import { resolveDeploymentConfig } from '../../config/deployment.js';
67
67
  import { getTaskforceCloudEnvironmentConfig, resolveTaskforceCloudEnvironmentFromBaseUrl } from '../../config/cloudEnvironment.js';
68
- import { resolveErrorStatusCode } from './shared.js';
68
+ import { buildCloudMcpRuntimeAuth, resolveErrorStatusCode } from './shared.js';
69
69
  import { applyWorkspaceRepair, scanWorkspaceRepair } from '../../sync/workspaceRepair.js';
70
70
  // === SECTION: Registration ===
71
71
  export function registerAdminRoutes(deps) {
@@ -101,7 +101,7 @@ export function registerAdminRoutes(deps) {
101
101
  const authSettings = globalSettings.auth || {};
102
102
  const authRateLimitMaxRequestsRaw = Number(authSettings.authRateLimitMaxRequests);
103
103
  const authRateLimitWindowMsRaw = Number(authSettings.authRateLimitWindowMs);
104
- const signupSettings = core.getSignupMonetizationSettings();
104
+ const onboardingPolicy = core.getOnboardingPolicy();
105
105
  return {
106
106
  allowSelfRegistration: resolveBooleanSetting(authSettings.allowSelfRegistration, true),
107
107
  requireVerifiedEmail: resolveBooleanSetting(authSettings.requireVerifiedEmail, true),
@@ -113,8 +113,8 @@ export function registerAdminRoutes(deps) {
113
113
  authRateLimitWindowMs: Number.isFinite(authRateLimitWindowMsRaw) && authRateLimitWindowMsRaw >= 1000
114
114
  ? Math.floor(authRateLimitWindowMsRaw)
115
115
  : 15 * 60_000,
116
- signupMonetizationStrategy: signupSettings.strategy,
117
- defaultSignupPlanVersionId: signupSettings.defaultSignupPlanVersionId
116
+ onboardingPolicyMode: onboardingPolicy.mode,
117
+ onboardingPlanVersionId: onboardingPolicy.planVersionId
118
118
  };
119
119
  };
120
120
  const getSiteBehavior = () => {
@@ -163,6 +163,10 @@ export function registerAdminRoutes(deps) {
163
163
  }
164
164
  return String(configured || resolvePublicAuthBaseUrl(req)).replace(/\/+$/, '');
165
165
  };
166
+ const resolveAdminCloudEnvironment = (req) => {
167
+ return resolveTaskforceCloudEnvironmentFromBaseUrl(resolveRuntimeConfigBaseUrl(req))
168
+ || resolveTaskforceCloudEnvironmentFromBaseUrl(resolveCloudLoginBaseUrl(req));
169
+ };
166
170
  const resolveMcpOauthAuthorizeUrl = (req) => `${resolveMcpOauthIssuer(req)}/oauth/mcp/authorize`;
167
171
  const resolveMcpOauthTokenUrl = (req) => `${resolveMcpOauthIssuer(req)}/oauth/mcp/token`;
168
172
  const resolveMcpOauthRegisterUrl = (req) => `${resolveMcpOauthIssuer(req)}/oauth/mcp/register`;
@@ -470,22 +474,13 @@ export function registerAdminRoutes(deps) {
470
474
  serverName: 'taskforce-cloud',
471
475
  logStartup: false,
472
476
  runtimeMode: 'cloud',
473
- auth: {
474
- scopes: authScopes,
475
- userId: auth.authenticatedViaToken?.userId || auth.authenticatedViaOAuth?.userId || auth.userId || null,
476
- tokenId: auth.authenticatedViaToken?.tokenId || auth.authenticatedViaOAuth?.grantId || null,
477
- aiProfileToken: auth.aiProfileToken,
478
- aiProfileName: auth.aiProfileName,
479
- aiProfileDescription: auth.aiProfileDescription,
480
- aiProfileRole: auth.aiProfileRole,
481
- aiProfileProvider: auth.aiProfileProvider,
482
- aiProfileModel: auth.aiProfileModel,
483
- aiProfileSurfaceType: auth.aiProfileSurfaceType,
484
- actorType: auth.authenticatedViaToken ? 'token' : auth.authenticatedViaOAuth ? 'oauth' : 'user',
477
+ auth: buildCloudMcpRuntimeAuth({
478
+ req,
479
+ auth,
480
+ requestHeader,
485
481
  requestId: String(req.headers['x-request-id'] || req.headers['x-railway-request-id'] || '').trim() || randomUUID(),
486
- ip: req.socket?.remoteAddress || null,
487
- userAgent: requestHeader(req, 'user-agent', '') || null,
488
- },
482
+ authScopes,
483
+ }),
489
484
  });
490
485
  const transport = new StreamableHTTPServerTransport({
491
486
  sessionIdGenerator: undefined,
@@ -964,6 +959,51 @@ export function registerAdminRoutes(deps) {
964
959
  res.writeHead(200, { 'Content-Type': 'application/json' });
965
960
  res.end(JSON.stringify({ success: true }));
966
961
  });
962
+ // DELETE /api/taskforce/admin/system-users/:id - Purge a test user in non-production cloud environments
963
+ addRoute('DELETE', '/api/taskforce/admin/system-users/:id', async (req, res, params) => {
964
+ if (!requireCloudRuntime(res, 'System user purge'))
965
+ return;
966
+ if (!ensureAdminRole(req)) {
967
+ res.writeHead(403, { 'Content-Type': 'application/json' });
968
+ res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
969
+ return;
970
+ }
971
+ const environment = resolveAdminCloudEnvironment(req);
972
+ if (environment !== 'staging' && environment !== 'performance') {
973
+ res.writeHead(403, { 'Content-Type': 'application/json' });
974
+ res.end(JSON.stringify({
975
+ error: 'System user purge is only available in staging and performance.',
976
+ code: 'SYSTEM_USER_PURGE_ENVIRONMENT_FORBIDDEN'
977
+ }));
978
+ return;
979
+ }
980
+ const body = await parseJsonBody(req);
981
+ const actor = resolveAdminRequestActor(req);
982
+ try {
983
+ const summary = core.purgeSystemUser({
984
+ targetUserId: params.id,
985
+ actorUserId: actor.userId,
986
+ emailConfirmation: String(body?.emailConfirmation || '')
987
+ });
988
+ auditAdminAction(req, 'purge-system-user', {
989
+ targetUserId: params.id,
990
+ targetEmail: summary.email,
991
+ environment,
992
+ summary
993
+ });
994
+ res.writeHead(200, { 'Content-Type': 'application/json' });
995
+ res.end(JSON.stringify({ success: true, environment, summary }));
996
+ }
997
+ catch (error) {
998
+ const statusCode = resolveErrorStatusCode(error, 400);
999
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
1000
+ res.end(JSON.stringify({
1001
+ error: String(error?.message || 'Unable to purge system user'),
1002
+ code: error?.code,
1003
+ details: error?.details || null
1004
+ }));
1005
+ }
1006
+ });
967
1007
  // === SECTION: Admin email settings ===
968
1008
  // GET /api/taskforce/admin/email-settings - Read email provider settings
969
1009
  addRoute('GET', '/api/taskforce/admin/email-settings', async (req, res) => {
@@ -1036,7 +1076,9 @@ export function registerAdminRoutes(deps) {
1036
1076
  res.end(JSON.stringify({ error: 'Forbidden: admin role required.' }));
1037
1077
  return;
1038
1078
  }
1039
- const authSettings = core.getGlobalSettings().auth || {};
1079
+ const globalSettings = core.getGlobalSettings();
1080
+ const authSettings = globalSettings.auth || {};
1081
+ const billingSettings = globalSettings.billing || {};
1040
1082
  const effective = getAuthBehavior();
1041
1083
  res.writeHead(200, { 'Content-Type': 'application/json' });
1042
1084
  res.end(JSON.stringify({
@@ -1047,11 +1089,13 @@ export function registerAdminRoutes(deps) {
1047
1089
  ...(typeof authSettings.authRateLimitEnabled === 'boolean' ? { authRateLimitEnabled: authSettings.authRateLimitEnabled } : {}),
1048
1090
  ...(typeof authSettings.authRateLimitMaxRequests === 'number' ? { authRateLimitMaxRequests: authSettings.authRateLimitMaxRequests } : {}),
1049
1091
  ...(typeof authSettings.authRateLimitWindowMs === 'number' ? { authRateLimitWindowMs: authSettings.authRateLimitWindowMs } : {}),
1050
- ...((authSettings.signupMonetizationStrategy === 'auto_assign_default' || authSettings.signupMonetizationStrategy === 'plan_selection_required')
1051
- ? { signupMonetizationStrategy: authSettings.signupMonetizationStrategy }
1092
+ ...((billingSettings.onboardingPolicy?.mode === 'require_plan_selection'
1093
+ || billingSettings.onboardingPolicy?.mode === 'auto_grant_free'
1094
+ || billingSettings.onboardingPolicy?.mode === 'auto_start_trial')
1095
+ ? { onboardingPolicyMode: billingSettings.onboardingPolicy.mode }
1052
1096
  : {}),
1053
- ...(typeof authSettings.defaultSignupPlanVersionId === 'string' || authSettings.defaultSignupPlanVersionId === null
1054
- ? { defaultSignupPlanVersionId: authSettings.defaultSignupPlanVersionId ?? null }
1097
+ ...(typeof billingSettings.onboardingPolicy?.planVersionId === 'string' || billingSettings.onboardingPolicy?.planVersionId === null
1098
+ ? { onboardingPlanVersionId: billingSettings.onboardingPolicy?.planVersionId ?? null }
1055
1099
  : {})
1056
1100
  },
1057
1101
  status: {
@@ -1061,8 +1105,8 @@ export function registerAdminRoutes(deps) {
1061
1105
  authRateLimitEnabled: effective.authRateLimitEnabled,
1062
1106
  authRateLimitMaxRequests: effective.authRateLimitMaxRequests,
1063
1107
  authRateLimitWindowMs: effective.authRateLimitWindowMs,
1064
- signupMonetizationStrategy: effective.signupMonetizationStrategy,
1065
- defaultSignupPlanVersionId: effective.defaultSignupPlanVersionId
1108
+ onboardingPolicyMode: effective.onboardingPolicyMode,
1109
+ onboardingPlanVersionId: effective.onboardingPlanVersionId
1066
1110
  }
1067
1111
  }));
1068
1112
  });
@@ -1688,21 +1732,24 @@ export function registerAdminRoutes(deps) {
1688
1732
  return;
1689
1733
  }
1690
1734
  const body = await parseJsonBody(req);
1691
- const signupMonetizationStrategy = body?.signupMonetizationStrategy === 'plan_selection_required'
1692
- ? 'plan_selection_required'
1693
- : 'auto_assign_default';
1694
- const defaultSignupPlanVersionId = typeof body?.defaultSignupPlanVersionId === 'string'
1695
- ? body.defaultSignupPlanVersionId.trim() || null
1696
- : body?.defaultSignupPlanVersionId === null
1735
+ const currentOnboardingPolicy = core.getOnboardingPolicy();
1736
+ const onboardingPolicyMode = body?.onboardingPolicyMode === 'auto_grant_free' || body?.onboardingPolicyMode === 'auto_start_trial'
1737
+ ? body.onboardingPolicyMode
1738
+ : body?.onboardingPolicyMode === 'require_plan_selection'
1739
+ ? 'require_plan_selection'
1740
+ : currentOnboardingPolicy.mode;
1741
+ const onboardingPlanVersionId = typeof body?.onboardingPlanVersionId === 'string'
1742
+ ? body.onboardingPlanVersionId.trim() || null
1743
+ : body?.onboardingPlanVersionId === null
1697
1744
  ? null
1698
- : undefined;
1699
- const validation = core.validateSignupMonetizationSettings({
1700
- strategy: signupMonetizationStrategy,
1701
- ...(defaultSignupPlanVersionId !== undefined ? { defaultSignupPlanVersionId } : {})
1745
+ : currentOnboardingPolicy.planVersionId;
1746
+ const validation = core.validateOnboardingPolicy({
1747
+ mode: onboardingPolicyMode,
1748
+ planVersionId: onboardingPlanVersionId
1702
1749
  });
1703
1750
  if (!validation.valid) {
1704
1751
  res.writeHead(400, { 'Content-Type': 'application/json' });
1705
- res.end(JSON.stringify({ error: validation.message || 'Invalid signup monetization settings.', code: validation.code, details: validation.details }));
1752
+ res.end(JSON.stringify({ error: validation.message || 'Invalid onboarding policy.', code: validation.code, details: validation.details }));
1706
1753
  return;
1707
1754
  }
1708
1755
  core.updateGlobalSettings({
@@ -1714,9 +1761,13 @@ export function registerAdminRoutes(deps) {
1714
1761
  ...(typeof body?.authRateLimitMaxRequests === 'number' && Number.isFinite(body.authRateLimitMaxRequests)
1715
1762
  ? { authRateLimitMaxRequests: Math.max(1, Math.floor(body.authRateLimitMaxRequests)) } : {}),
1716
1763
  ...(typeof body?.authRateLimitWindowMs === 'number' && Number.isFinite(body.authRateLimitWindowMs)
1717
- ? { authRateLimitWindowMs: Math.max(1000, Math.floor(body.authRateLimitWindowMs)) } : {}),
1718
- signupMonetizationStrategy,
1719
- ...(defaultSignupPlanVersionId !== undefined ? { defaultSignupPlanVersionId } : {})
1764
+ ? { authRateLimitWindowMs: Math.max(1000, Math.floor(body.authRateLimitWindowMs)) } : {})
1765
+ },
1766
+ billing: {
1767
+ onboardingPolicy: {
1768
+ mode: onboardingPolicyMode,
1769
+ planVersionId: onboardingPlanVersionId ?? null
1770
+ }
1720
1771
  }
1721
1772
  });
1722
1773
  res.writeHead(200, { 'Content-Type': 'application/json' });
@@ -20,6 +20,7 @@
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
24
25
  * GET /api/taskforce/account/team-management-access
25
26
  * GET /api/taskforce/account/plan-preview