@taskforcehq/taskforce 0.3.301 → 0.3.302

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 (147) hide show
  1. package/dist/Taskforce.module.css +26 -2
  2. package/dist/TaskforceCore.filter-persistence.test.js +1 -1
  3. package/dist/TaskforceCore.js +13 -24
  4. package/dist/TaskforceCore.test.js +167 -152
  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/ui/ReferenceBadgeButton.js +1 -1
  14. package/dist/components/views/PlansPage.js +9 -8
  15. package/dist/components/views/StandaloneLayout.js +466 -204
  16. package/dist/components/views/panels/PlanningDrawer.js +18 -1
  17. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +43 -0
  18. package/dist/components/views/standalone/modals/AccountHubModal.js +22 -0
  19. package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.d.ts +9 -0
  20. package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.js +8 -0
  21. package/dist/components/views/standalone/modals/EditProfileModal.d.ts +26 -0
  22. package/dist/components/views/standalone/modals/EditProfileModal.js +10 -0
  23. package/dist/components/views/standalone/modals/HelpModal.d.ts +9 -0
  24. package/dist/components/views/standalone/modals/HelpModal.js +8 -0
  25. package/dist/components/views/standalone/modals/ScheduleWarningModal.d.ts +13 -0
  26. package/dist/components/views/standalone/modals/ScheduleWarningModal.js +6 -0
  27. package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +61 -0
  28. package/dist/components/views/standalone/modals/SyncStatusModal.js +38 -0
  29. package/dist/components/views/standalone/modals/TeamManagementModal.d.ts +64 -0
  30. package/dist/components/views/standalone/modals/TeamManagementModal.js +10 -0
  31. package/dist/components/views/standalone/modals/types.d.ts +2 -0
  32. package/dist/components/views/standalone/modals/types.js +1 -0
  33. package/dist/components/views/teamManagementAccess.d.ts +1 -1
  34. package/dist/components/views/teamManagementAccess.js +2 -2
  35. package/dist/components/views/teamManagementAccess.test.js +3 -3
  36. package/dist/config/deployment.js +2 -2
  37. package/dist/config/deployment.test.js +5 -3
  38. package/dist/core/AiProfileService.js +7 -2
  39. package/dist/core/AiProfiles.test.js +19 -0
  40. package/dist/core/SyncReconciliationService.d.ts +2 -0
  41. package/dist/core/SyncReconciliationService.js +3 -0
  42. package/dist/core/TaskAuditTimeline.test.js +40 -0
  43. package/dist/core/Taskforce.d.ts +49 -0
  44. package/dist/core/Taskforce.ids.test.js +133 -0
  45. package/dist/core/Taskforce.js +241 -78
  46. package/dist/core/UserProfileAvatarDrafts.test.d.ts +1 -0
  47. package/dist/core/UserProfileAvatarDrafts.test.js +102 -0
  48. package/dist/core/taskReferencePolicy.d.ts +28 -0
  49. package/dist/core/taskReferencePolicy.js +189 -0
  50. package/dist/core/types.d.ts +2 -0
  51. package/dist/hooks/tasks/useTaskFormActions.js +3 -0
  52. package/dist/hooks/useTaskData.js +3 -2
  53. package/dist/hooks/useTaskData.test.js +66 -0
  54. package/dist/hooks/useTaskforce.d.ts +5 -0
  55. package/dist/hooks/useTaskforce.js +63 -15
  56. package/dist/hooks/useTaskforce.sync-behavior.test.js +185 -6
  57. package/dist/mcp/aiProfileBootstrap.d.ts +29 -0
  58. package/dist/mcp/aiProfileBootstrap.js +80 -0
  59. package/dist/mcp/aiProfileBootstrap.test.d.ts +1 -0
  60. package/dist/mcp/aiProfileBootstrap.test.js +68 -0
  61. package/dist/mcp/clientIntegrations.d.ts +2 -8
  62. package/dist/mcp/clientIntegrations.js +21 -40
  63. package/dist/mcp/clientIntegrations.test.js +28 -2
  64. package/dist/mcp/clientProfileDefaults.d.ts +12 -0
  65. package/dist/mcp/clientProfileDefaults.js +37 -0
  66. package/dist/mcp/runtime.d.ts +10 -7
  67. package/dist/mcp/runtime.js +41 -19
  68. package/dist/mcp/runtime.test.js +49 -2
  69. package/dist/migrations/taskSchemaMigrations.js +53 -1
  70. package/dist/qaOpen.test.d.ts +1 -0
  71. package/dist/qaOpen.test.js +56 -0
  72. package/dist/server/index.cookieProxy.test.js +1 -0
  73. package/dist/server/index.d.ts +1 -0
  74. package/dist/server/index.js +15 -1
  75. package/dist/server/index.test.js +14 -1
  76. package/dist/server/routes/admin.js +25 -22
  77. package/dist/server/routes/auth.d.ts +5 -0
  78. package/dist/server/routes/auth.js +350 -16
  79. package/dist/server/routes/billing.js +11 -1
  80. package/dist/server/routes/billing.test.js +32 -2
  81. package/dist/server/routes/documents.js +6 -1
  82. package/dist/server/routes/shared.d.ts +1 -1
  83. package/dist/server/routes/shared.js +11 -3
  84. package/dist/server/routes/sync.integration.test.js +130 -0
  85. package/dist/server/routes/sync.js +226 -1924
  86. package/dist/server/routes/tasks.js +18 -4
  87. package/dist/server/routes.js +26 -19
  88. package/dist/server/routes.test.js +352 -6
  89. package/dist/shared/runtimeContract.d.ts +1 -1
  90. package/dist/sync/collaborationSyncPayload.d.ts +17 -0
  91. package/dist/sync/collaborationSyncPayload.js +131 -0
  92. package/dist/sync/collaborationSyncPayload.test.d.ts +1 -0
  93. package/dist/sync/collaborationSyncPayload.test.js +104 -0
  94. package/dist/sync/collaborationSyncState.d.ts +15 -0
  95. package/dist/sync/collaborationSyncState.js +60 -0
  96. package/dist/sync/contentSyncPayload.d.ts +45 -0
  97. package/dist/sync/contentSyncPayload.js +104 -0
  98. package/dist/sync/contentSyncPayload.test.d.ts +1 -0
  99. package/dist/sync/contentSyncPayload.test.js +104 -0
  100. package/dist/sync/contentSyncState.d.ts +88 -0
  101. package/dist/sync/contentSyncState.js +743 -0
  102. package/dist/sync/planningSyncPayload.d.ts +9 -0
  103. package/dist/sync/planningSyncPayload.js +82 -0
  104. package/dist/sync/planningSyncPayload.test.d.ts +1 -0
  105. package/dist/sync/planningSyncPayload.test.js +68 -0
  106. package/dist/sync/syncApplyHandlers.d.ts +83 -0
  107. package/dist/sync/syncApplyHandlers.js +445 -0
  108. package/dist/sync/taskSyncPayload.d.ts +33 -0
  109. package/dist/sync/taskSyncPayload.js +126 -0
  110. package/dist/sync/taskSyncPayload.test.d.ts +1 -0
  111. package/dist/sync/taskSyncPayload.test.js +54 -0
  112. package/dist/sync/workspacePullFeed.d.ts +28 -0
  113. package/dist/sync/workspacePullFeed.js +301 -0
  114. package/dist/sync/workspacePullFeed.test.d.ts +1 -0
  115. package/dist/sync/workspacePullFeed.test.js +166 -0
  116. package/dist/sync/workspaceSyncModel.d.ts +4 -1
  117. package/dist/sync/workspaceSyncModel.js +9 -2
  118. package/dist/sync/workspaceSyncModel.test.js +29 -0
  119. package/dist/test/setup.js +1 -1
  120. package/dist/types.d.ts +1 -0
  121. package/dist/ui/assets/{AgentsModule-Bdq3T1Ts.js → AgentsModule-JG5jc3he.js} +1 -1
  122. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-jKUVo-PJ.js → AnnotatedAttachmentWorkspace-Dvi02Y7q.js} +1 -1
  123. package/dist/ui/assets/{DocumentWorkspace-DuQa-uIC.js → DocumentWorkspace-iys3zfca.js} +2 -2
  124. package/dist/ui/assets/{InitiativesModule-C8Pi2VTk.js → InitiativesModule-DJvLJI3A.js} +1 -1
  125. package/dist/ui/assets/PlansPage-BMPlpFpw.js +1 -0
  126. package/dist/ui/assets/TaskSettings-Bqd4cCzW.js +8 -0
  127. package/dist/ui/assets/{TaskSettings-BwB9NtmJ.css → TaskSettings-CnIBL_Eb.css} +1 -1
  128. package/dist/ui/assets/{WorkflowsModule-b6TLuHwJ.js → WorkflowsModule-BguHeQaV.js} +1 -1
  129. package/dist/ui/assets/index-MXeWoebC.css +1 -0
  130. package/dist/ui/assets/index-YJ4k5QNa.js +6 -0
  131. package/dist/ui/assets/{vendor-icons-QZyhEwgT.js → vendor-icons-C8EED3Oh.js} +1 -1
  132. package/dist/ui/index.html +3 -3
  133. package/dist/utils/billingStatusCache.d.ts +21 -0
  134. package/dist/utils/billingStatusCache.js +70 -0
  135. package/dist/utils/taskActivity.js +17 -4
  136. package/dist/utils/taskActivity.test.js +27 -0
  137. package/dist/utils/taskNormalization.js +10 -2
  138. package/dist/utils/taskReferences.d.ts +11 -1
  139. package/dist/utils/taskReferences.js +33 -1
  140. package/package.json +2 -1
  141. package/scripts/qa-open-lib.mjs +175 -0
  142. package/scripts/qa-open-lib.mjs.d.ts +49 -0
  143. package/scripts/qa-open.mjs +352 -0
  144. package/dist/ui/assets/PlansPage-B6BDECSi.js +0 -1
  145. package/dist/ui/assets/TaskSettings-DUJgRTPU.js +0 -8
  146. package/dist/ui/assets/index-BiiF1cLQ.css +0 -1
  147. package/dist/ui/assets/index-xBWLPOEb.js +0 -6
@@ -58,6 +58,7 @@ import { parseJsonBody } from '../routes.js';
58
58
  import { resolveErrorStatusCode, requestHeader, requestRuntimeMode } from './shared.js';
59
59
  import { listBuiltInCustomTaxonomyLibraryPacks } from '../../shared/customTaxonomyLibrary.js';
60
60
  import { listBuiltInTaxonomyLibraryPacks } from '../../shared/taxonomyLibrary.js';
61
+ import { shouldUseProvisionalTaskReferences } from '../../utils/taskReferences.js';
61
62
  // === SECTION: Registration ===
62
63
  export function registerTaskRoutes(deps) {
63
64
  const { addRoute, core, context, requestWorkspaceId, ensureAppAccess, resolveEffectiveObjectStorageConfig, buildBatchSummary, CLOUD_SAFE_CONFIG_PATHS, } = deps;
@@ -343,7 +344,8 @@ export function registerTaskRoutes(deps) {
343
344
  const result = core.bulkCreateTasks([taskInput], {
344
345
  atomic: true,
345
346
  validateOnly: true,
346
- workspaceId
347
+ workspaceId,
348
+ provisionalLocalReference: shouldUseProvisionalTaskReferences(requestRuntimeMode(req))
347
349
  });
348
350
  const hasErrors = result.errors.length > 0;
349
351
  const failureStatus = hasErrors
@@ -361,7 +363,11 @@ export function registerTaskRoutes(deps) {
361
363
  }
362
364
  try {
363
365
  const workspaceId = requestWorkspaceId(req);
364
- const task = core.createTask(taskInput, { workspaceId, actorRef: 'user' });
366
+ const task = core.createTask(taskInput, {
367
+ workspaceId,
368
+ actorRef: 'user',
369
+ provisionalLocalReference: shouldUseProvisionalTaskReferences(requestRuntimeMode(req))
370
+ });
365
371
  res.writeHead(201, { 'Content-Type': 'application/json' });
366
372
  res.end(JSON.stringify(task));
367
373
  }
@@ -386,7 +392,14 @@ export function registerTaskRoutes(deps) {
386
392
  const body = await parseJsonBody(req);
387
393
  try {
388
394
  const workspaceId = requestWorkspaceId(req);
389
- const updated = core.updateTask(params.id, body, workspaceId, { actorRef: 'user' });
395
+ const saveSource = body?.saveSource === 'autosave' ? 'autosave' : 'manual';
396
+ const updates = body && typeof body === 'object'
397
+ ? Object.fromEntries(Object.entries(body).filter(([key]) => key !== 'saveSource'))
398
+ : body;
399
+ const updated = core.updateTask(params.id, updates, workspaceId, {
400
+ actorRef: 'user',
401
+ skipTaskEvents: saveSource === 'autosave'
402
+ });
390
403
  if (!updated) {
391
404
  res.writeHead(404, { 'Content-Type': 'application/json' });
392
405
  res.end(JSON.stringify({ error: 'Task not found' }));
@@ -869,7 +882,8 @@ export function registerTaskRoutes(deps) {
869
882
  const result = core.bulkCreateTasks(tasks, {
870
883
  atomic: atomic !== false,
871
884
  validateOnly: validate_only === true,
872
- workspaceId
885
+ workspaceId,
886
+ provisionalLocalReference: shouldUseProvisionalTaskReferences(requestRuntimeMode(req))
873
887
  });
874
888
  const hasErrors = result.errors.length > 0;
875
889
  const succeeded = validate_only ? (hasErrors ? 0 : result.total) : result.created.length;
@@ -10,6 +10,7 @@ import { createHash, randomUUID } from 'crypto';
10
10
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
11
11
  import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
12
12
  import { TaskforceRuleError } from '../core/Taskforce.js';
13
+ import { parseAiProfileBootstrapHeaders } from '../mcp/aiProfileBootstrap.js';
13
14
  import { createTaskforceMcpServer, isMcpWriteToolName } from '../mcp/runtime.js';
14
15
  import { applyPrefixedRateLimitHeaders } from './rateLimit.js';
15
16
  import { createSessionTokenCookie, resolveRequestAccess } from './auth.js';
@@ -69,6 +70,20 @@ function readPositiveInteger(value, fallback) {
69
70
  return fallback;
70
71
  return parsed;
71
72
  }
73
+ function resolveBooleanSetting(value, fallback) {
74
+ if (typeof value === 'boolean')
75
+ return value;
76
+ if (typeof value === 'number')
77
+ return value !== 0;
78
+ const normalized = String(value || '').trim().toLowerCase();
79
+ if (!normalized)
80
+ return fallback;
81
+ if (['1', 'true', 'yes', 'on'].includes(normalized))
82
+ return true;
83
+ if (['0', 'false', 'no', 'off'].includes(normalized))
84
+ return false;
85
+ return fallback;
86
+ }
72
87
  // Convert route pattern like '/api/taskforce/tasks/:id' to regex
73
88
  function createRoutePattern(path) {
74
89
  const keys = [];
@@ -332,10 +347,10 @@ export function createRoutes(core, context = {}) {
332
347
  const authRateLimitWindowMsRaw = Number(authSettings.authRateLimitWindowMs);
333
348
  const signupSettings = core.getSignupMonetizationSettings();
334
349
  return {
335
- allowSelfRegistration: authSettings.allowSelfRegistration !== false,
336
- requireVerifiedEmail: authSettings.requireVerifiedEmail !== false,
337
- defaultNewUserBetaAccess: authSettings.defaultNewUserBetaAccess === true,
338
- authRateLimitEnabled: authSettings.authRateLimitEnabled !== false,
350
+ allowSelfRegistration: resolveBooleanSetting(authSettings.allowSelfRegistration, true),
351
+ requireVerifiedEmail: resolveBooleanSetting(authSettings.requireVerifiedEmail, true),
352
+ defaultNewUserBetaAccess: resolveBooleanSetting(authSettings.defaultNewUserBetaAccess, false),
353
+ authRateLimitEnabled: resolveBooleanSetting(authSettings.authRateLimitEnabled, true),
339
354
  authRateLimitMaxRequests: Number.isFinite(authRateLimitMaxRequestsRaw) && authRateLimitMaxRequestsRaw >= 1
340
355
  ? Math.floor(authRateLimitMaxRequestsRaw)
341
356
  : 10,
@@ -1124,13 +1139,7 @@ export function createRoutes(core, context = {}) {
1124
1139
  ? authorization.slice(7).trim()
1125
1140
  : '';
1126
1141
  const requestedWorkspaceId = requestHeader(req, 'x-taskforce-workspace-id', '').trim();
1127
- const aiProfileToken = requestHeader(req, 'x-taskforce-ai-profile-token', '').trim() || null;
1128
- const aiProfileName = requestHeader(req, 'x-taskforce-ai-name', '').trim() || null;
1129
- const aiProfileDescription = requestHeader(req, 'x-taskforce-ai-description', '').trim() || null;
1130
- const aiProfileRole = requestHeader(req, 'x-taskforce-ai-role', '').trim() || null;
1131
- const aiProfileProvider = requestHeader(req, 'x-taskforce-ai-provider', '').trim() || null;
1132
- const aiProfileModel = requestHeader(req, 'x-taskforce-ai-model', '').trim() || null;
1133
- const aiProfileSurfaceType = requestHeader(req, 'x-taskforce-ai-surface-type', '').trim() || null;
1142
+ const bootstrap = parseAiProfileBootstrapHeaders(req.headers);
1134
1143
  if (bearerToken) {
1135
1144
  const tokenAuth = core.authenticateMcpAccessToken(bearerToken);
1136
1145
  const oauthAuth = tokenAuth ? null : core.authenticateMcpOAuthAccessToken(bearerToken);
@@ -1178,13 +1187,7 @@ export function createRoutes(core, context = {}) {
1178
1187
  authenticatedViaToken: tokenAuth || null,
1179
1188
  authenticatedViaOAuth: oauthAuth || null,
1180
1189
  userId: null,
1181
- aiProfileToken,
1182
- aiProfileName,
1183
- aiProfileDescription,
1184
- aiProfileRole,
1185
- aiProfileProvider,
1186
- aiProfileModel,
1187
- aiProfileSurfaceType,
1190
+ ...bootstrap,
1188
1191
  };
1189
1192
  }
1190
1193
  core.addMcpAuditLog({
@@ -1461,6 +1464,8 @@ export function createRoutes(core, context = {}) {
1461
1464
  tokenId: auth.authenticatedViaToken?.tokenId || auth.authenticatedViaOAuth?.grantId || null,
1462
1465
  aiProfileToken: auth.aiProfileToken,
1463
1466
  aiProfileName: auth.aiProfileName,
1467
+ aiProfileIcon: auth.aiProfileIcon,
1468
+ aiProfileColor: auth.aiProfileColor,
1464
1469
  aiProfileDescription: auth.aiProfileDescription,
1465
1470
  aiProfileRole: auth.aiProfileRole,
1466
1471
  aiProfileProvider: auth.aiProfileProvider,
@@ -1495,7 +1500,7 @@ export function createRoutes(core, context = {}) {
1495
1500
  };
1496
1501
  const resolveAdminRequestActor = makeResolveAdminRequestActor(core, context);
1497
1502
  const auditAdminAction = makeAuditAdminAction(core, context);
1498
- const auditAuthAction = makeAuditAuthAction(core, requestWorkspaceId);
1503
+ const auditAuthAction = makeAuditAuthAction(core, requestWorkspaceId, context);
1499
1504
  const ensureAdminRole = makeEnsureAdminRole(core, context);
1500
1505
  const ensureAuthenticatedUser = makeEnsureAuthenticatedUser();
1501
1506
  const isCollaborationPolicyEnabled = () => {
@@ -1560,6 +1565,8 @@ export function createRoutes(core, context = {}) {
1560
1565
  shouldExposeDevAuthTokens,
1561
1566
  resolveRuntimeConfigBaseUrl,
1562
1567
  resolvePublicAuthBaseUrl,
1568
+ resolveEffectiveObjectStorageConfig,
1569
+ getObjectStorageClient,
1563
1570
  });
1564
1571
  registerWorkspaceRoutes({
1565
1572
  addRoute,
@@ -462,10 +462,22 @@ vi.mock('../core/Taskforce', () => {
462
462
  }));
463
463
  resolveUserIdentity = vi.fn().mockImplementation((userId) => {
464
464
  if (userId === 'admin-user') {
465
- return { userId, email: 'admin@example.com' };
465
+ return { userId, email: 'admin@example.com', avatarUrl: null };
466
466
  }
467
467
  return null;
468
468
  });
469
+ updateUserProfile = vi.fn().mockImplementation((userId, input) => ({
470
+ userId,
471
+ email: `${userId}@example.com`,
472
+ displayName: input.displayName,
473
+ avatarUrl: input.avatarUrl === undefined ? null : input.avatarUrl
474
+ }));
475
+ createUserAvatarUploadDraft = vi.fn();
476
+ getUserAvatarUploadDraft = vi.fn().mockReturnValue(null);
477
+ markUserAvatarUploadDraftFinalized = vi.fn().mockReturnValue(null);
478
+ markUserAvatarUploadDraftConsumed = vi.fn().mockReturnValue(null);
479
+ discardUserAvatarUploadDraft = vi.fn().mockReturnValue(null);
480
+ cleanupStaleUserAvatarUploadDrafts = vi.fn().mockReturnValue([]);
469
481
  resolvePreferredWorkspaceId = vi.fn().mockImplementation((options) => (options?.allowDefault ? 'default' : null));
470
482
  resolvePreferredUserWorkspaceId = vi.fn().mockImplementation((_userId, options) => {
471
483
  const preferred = String(options?.preferredWorkspaceId || '').trim();
@@ -863,6 +875,8 @@ describe('Server Routes', () => {
863
875
  authorization: 'Bearer tfmcp_ai_header_token',
864
876
  'x-taskforce-ai-name': 'Automation Codex',
865
877
  'x-taskforce-ai-profile-token': 'tfp_automation_codex',
878
+ 'x-taskforce-ai-icon': 'Code2',
879
+ 'x-taskforce-ai-color': '#10a37f',
866
880
  'x-taskforce-ai-description': 'OpenAI Codex coding agent connected through MCP.',
867
881
  'x-taskforce-ai-role': 'Coding Agent',
868
882
  'x-taskforce-ai-provider': 'OpenAI',
@@ -3511,7 +3525,11 @@ describe('Server Routes', () => {
3511
3525
  const req = createMockReq('POST', '/api/taskforce/task', newTask);
3512
3526
  const res = createMockRes();
3513
3527
  await match?.handler(req, res, {});
3514
- expect(core.createTask).toHaveBeenCalledWith(newTask, { workspaceId: 'default', actorRef: 'user' });
3528
+ expect(core.createTask).toHaveBeenCalledWith(newTask, {
3529
+ workspaceId: 'default',
3530
+ actorRef: 'user',
3531
+ provisionalLocalReference: true
3532
+ });
3515
3533
  expect(res.writeHead).toHaveBeenCalledWith(201, expect.any(Object));
3516
3534
  expect(res.end).toHaveBeenCalledWith(JSON.stringify(createdTask));
3517
3535
  });
@@ -3591,7 +3609,7 @@ describe('Server Routes', () => {
3591
3609
  const payload = JSON.parse(res.end.mock.calls[0][0]);
3592
3610
  expect(payload?.config?.baseUrl).toBe('http://127.0.0.1:4271');
3593
3611
  expect(payload?.config?.apiBaseUrl).toBe('http://127.0.0.1:4271');
3594
- expect(payload?.config?.cloudAuthBaseUrl).toBe('http://127.0.0.1:4271');
3612
+ expect(payload?.config?.cloudAuthBaseUrl).toBe('https://app.taskforcehq.ai');
3595
3613
  expect(payload?.config?.wsBaseUrl).toBe('http://127.0.0.1:4271');
3596
3614
  expect(payload?.config?.cloudMcpBaseUrl).toBe('https://mcp.taskforcehq.ai');
3597
3615
  }
@@ -4172,6 +4190,44 @@ describe('Server Routes', () => {
4172
4190
  expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
4173
4191
  expect(res.end).toHaveBeenCalledWith(expect.stringContaining('"success":true'));
4174
4192
  });
4193
+ it('POST /api/taskforce/auth/login should treat string false as disabled verified-email enforcement', async () => {
4194
+ const authConfig = {
4195
+ runtimeMode: 'cloud',
4196
+ enabled: true,
4197
+ requiredForApi: true,
4198
+ excludedPaths: ['/api/taskforce/health', '/api/taskforce/auth'],
4199
+ defaultRole: 'member',
4200
+ defaultWorkspaceId: 'default',
4201
+ sessionCookieName: 'taskforce_session',
4202
+ sessionSecret: 'test-secret',
4203
+ sessionTtlSeconds: 3600
4204
+ };
4205
+ core.getGlobalSettings.mockReturnValue({
4206
+ auth: {
4207
+ allowSelfRegistration: true,
4208
+ requireVerifiedEmail: 'false'
4209
+ }
4210
+ });
4211
+ routes = createRoutes(core, { authConfig });
4212
+ core.resolvePreferredUserWorkspaceId.mockReturnValueOnce(null);
4213
+ core.authenticatePasswordUserDetailed.mockReturnValueOnce({
4214
+ ok: true,
4215
+ access: { userId: 'u2', workspaceId: 'default', role: 'member' }
4216
+ });
4217
+ const match = matchRoute('POST', '/api/taskforce/auth/login', routes);
4218
+ const req = createMockReq('POST', '/api/taskforce/auth/login', {
4219
+ email: 'user@example.com',
4220
+ password: 'password123'
4221
+ });
4222
+ const res = createMockRes();
4223
+ res.setHeader = vi.fn();
4224
+ await match?.handler(req, res, {});
4225
+ expect(core.authenticatePasswordUserDetailed).toHaveBeenCalledWith(expect.objectContaining({
4226
+ email: 'user@example.com',
4227
+ requireVerifiedEmail: false
4228
+ }));
4229
+ expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
4230
+ });
4175
4231
  it('POST /api/taskforce/auth/login should re-scope default session workspace to accessible membership', async () => {
4176
4232
  const authConfig = {
4177
4233
  runtimeMode: 'cloud',
@@ -8673,6 +8729,215 @@ describe('Server Routes', () => {
8673
8729
  expect(payload.betaAccess).toBe(false);
8674
8730
  expect(core.getUserBetaAccess).toHaveBeenCalledWith('u-no-workspace');
8675
8731
  });
8732
+ it('GET /api/taskforce/auth/session should expose avatarUrl from the resolved profile', async () => {
8733
+ const authConfig = {
8734
+ runtimeMode: 'cloud',
8735
+ enabled: true,
8736
+ requiredForApi: true,
8737
+ excludedPaths: ['/api/taskforce/health', '/api/taskforce/auth'],
8738
+ defaultRole: 'member',
8739
+ defaultWorkspaceId: 'default',
8740
+ sessionCookieName: 'taskforce_session',
8741
+ sessionSecret: 'test-secret',
8742
+ sessionTtlSeconds: 3600
8743
+ };
8744
+ routes = createRoutes(core, { authConfig });
8745
+ core.resolveUserIdentity.mockReturnValueOnce({
8746
+ userId: 'u-avatar',
8747
+ email: 'avatar@example.com',
8748
+ displayName: 'Avatar User',
8749
+ avatarUrl: '/api/taskforce/context/users%2Fu-avatar%2Fprofile%2Favatar%2Fdraft.png'
8750
+ });
8751
+ const cookie = createSessionTokenCookie('session-avatar-auth', authConfig, {
8752
+ userId: 'u-avatar',
8753
+ role: 'member',
8754
+ workspaceId: 'workspace-1'
8755
+ });
8756
+ const match = matchRoute('GET', '/api/taskforce/auth/session', routes);
8757
+ const req = createMockReq('GET', '/api/taskforce/auth/session', undefined, { cookie });
8758
+ const res = createMockRes();
8759
+ await match?.handler(req, res, {});
8760
+ const payload = JSON.parse(res.end.mock.calls[0][0]);
8761
+ expect(payload.authenticated).toBe(true);
8762
+ expect(payload.avatarUrl).toBe('/api/taskforce/context/users%2Fu-avatar%2Fprofile%2Favatar%2Fdraft.png');
8763
+ });
8764
+ it('POST /api/taskforce/auth/profile should save avatar by validated draft id', async () => {
8765
+ const authConfig = {
8766
+ runtimeMode: 'cloud',
8767
+ enabled: true,
8768
+ requiredForApi: true,
8769
+ excludedPaths: ['/api/taskforce/health', '/api/taskforce/auth'],
8770
+ defaultRole: 'member',
8771
+ defaultWorkspaceId: 'default',
8772
+ sessionCookieName: 'taskforce_session',
8773
+ sessionSecret: 'test-secret',
8774
+ sessionTtlSeconds: 3600
8775
+ };
8776
+ routes = createRoutes(core, { authConfig });
8777
+ core.getUserAvatarUploadDraft.mockReturnValueOnce({
8778
+ draftId: 'avatar-draft-1',
8779
+ userId: 'u1',
8780
+ storageKey: 'users/u1/profile/avatar/avatar-draft-1-headshot.png',
8781
+ finalizedAt: '2026-04-09T00:00:00.000Z',
8782
+ discardedAt: null
8783
+ });
8784
+ core.resolveUserIdentity.mockReturnValueOnce({
8785
+ userId: 'u1',
8786
+ email: 'u1@example.com',
8787
+ displayName: 'Before',
8788
+ avatarUrl: null
8789
+ });
8790
+ core.updateUserProfile.mockReturnValueOnce({
8791
+ userId: 'u1',
8792
+ email: 'u1@example.com',
8793
+ displayName: 'Updated Name',
8794
+ avatarUrl: '/api/taskforce/context/users%2Fu1%2Fprofile%2Favatar%2Favatar-draft-1-headshot.png'
8795
+ });
8796
+ const cookie = createSessionTokenCookie('session-profile-auth', authConfig, {
8797
+ userId: 'u1',
8798
+ role: 'member',
8799
+ workspaceId: 'workspace-1'
8800
+ });
8801
+ const match = matchRoute('POST', '/api/taskforce/auth/profile', routes);
8802
+ const req = createMockReq('POST', '/api/taskforce/auth/profile', {
8803
+ displayName: 'Updated Name',
8804
+ avatarDraftId: 'avatar-draft-1'
8805
+ }, { cookie });
8806
+ const res = createMockRes();
8807
+ await match?.handler(req, res, {});
8808
+ expect(core.updateUserProfile).toHaveBeenCalledWith('u1', {
8809
+ displayName: 'Updated Name',
8810
+ avatarUrl: '/api/taskforce/context/users%2Fu1%2Fprofile%2Favatar%2Favatar-draft-1-headshot.png'
8811
+ });
8812
+ expect(core.addAdminAuditLog).toHaveBeenCalledWith(expect.objectContaining({
8813
+ action: 'auth-profile-updated',
8814
+ actorUserId: 'u1',
8815
+ actorRole: 'member'
8816
+ }));
8817
+ const payload = JSON.parse(res.end.mock.calls[0][0]);
8818
+ expect(payload.profile.avatarUrl).toBe('/api/taskforce/context/users%2Fu1%2Fprofile%2Favatar%2Favatar-draft-1-headshot.png');
8819
+ });
8820
+ it('POST /api/taskforce/auth/profile/avatar/init should create a signed avatar upload draft', async () => {
8821
+ const authConfig = {
8822
+ runtimeMode: 'cloud',
8823
+ enabled: true,
8824
+ requiredForApi: true,
8825
+ excludedPaths: ['/api/taskforce/health', '/api/taskforce/auth'],
8826
+ defaultRole: 'member',
8827
+ defaultWorkspaceId: 'default',
8828
+ sessionCookieName: 'taskforce_session',
8829
+ sessionSecret: 'test-secret',
8830
+ sessionTtlSeconds: 3600
8831
+ };
8832
+ const routesWithStorage = createRoutes(core, {
8833
+ authConfig,
8834
+ objectStorage: {
8835
+ provider: 'r2',
8836
+ r2: {
8837
+ accountId: 'acct-1',
8838
+ bucket: 'taskforce-docs',
8839
+ endpoint: 'https://example.r2.cloudflarestorage.com',
8840
+ accessKeyId: 'access-key-1',
8841
+ secretAccessKey: 'secret-key-1',
8842
+ keyPrefix: 'taskforce',
8843
+ signedUploadTtlSeconds: 600,
8844
+ signedDownloadTtlSeconds: 300
8845
+ }
8846
+ }
8847
+ });
8848
+ const cookie = createSessionTokenCookie('session-avatar-init', authConfig, {
8849
+ userId: 'u1',
8850
+ role: 'member',
8851
+ workspaceId: 'workspace-1'
8852
+ });
8853
+ const match = matchRoute('POST', '/api/taskforce/auth/profile/avatar/init', routesWithStorage);
8854
+ const req = createMockReq('POST', '/api/taskforce/auth/profile/avatar/init', {
8855
+ originalName: 'headshot.png',
8856
+ mimeType: 'image/png',
8857
+ size: 1024
8858
+ }, { cookie });
8859
+ const res = createMockRes();
8860
+ await match?.handler(req, res, {});
8861
+ const payload = JSON.parse(res.end.mock.calls[0][0]);
8862
+ expect(payload.success).toBe(true);
8863
+ expect(payload.draftId).toMatch(/^avatar-draft-/);
8864
+ expect(payload.relativePath).toMatch(/^users\/u1\/profile\/avatar\/avatar-draft-[a-f0-9]{32}-headshot\.png$/);
8865
+ expect(payload.uploadUrl).toContain('X-Amz-Signature=');
8866
+ expect(core.createUserAvatarUploadDraft).toHaveBeenCalledWith(expect.objectContaining({
8867
+ userId: 'u1',
8868
+ draftId: payload.draftId,
8869
+ storageKey: payload.relativePath,
8870
+ mimeType: 'image/png'
8871
+ }));
8872
+ });
8873
+ it('POST /api/taskforce/auth/profile/avatar/finalize should validate and finalize the uploaded draft', async () => {
8874
+ const authConfig = {
8875
+ runtimeMode: 'cloud',
8876
+ enabled: true,
8877
+ requiredForApi: true,
8878
+ excludedPaths: ['/api/taskforce/health', '/api/taskforce/auth'],
8879
+ defaultRole: 'member',
8880
+ defaultWorkspaceId: 'default',
8881
+ sessionCookieName: 'taskforce_session',
8882
+ sessionSecret: 'test-secret',
8883
+ sessionTtlSeconds: 3600
8884
+ };
8885
+ const fetchMock = vi.fn().mockResolvedValue({
8886
+ ok: true,
8887
+ status: 200,
8888
+ statusText: 'OK',
8889
+ headers: { get: () => 'image/png' },
8890
+ arrayBuffer: async () => new Uint8Array([1, 2, 3]).buffer
8891
+ });
8892
+ vi.stubGlobal('fetch', fetchMock);
8893
+ const routesWithStorage = createRoutes(core, {
8894
+ authConfig,
8895
+ objectStorage: {
8896
+ provider: 'r2',
8897
+ r2: {
8898
+ accountId: 'acct-1',
8899
+ bucket: 'taskforce-docs',
8900
+ endpoint: 'https://example.r2.cloudflarestorage.com',
8901
+ accessKeyId: 'access-key-1',
8902
+ secretAccessKey: 'secret-key-1',
8903
+ keyPrefix: 'taskforce',
8904
+ signedUploadTtlSeconds: 600,
8905
+ signedDownloadTtlSeconds: 300
8906
+ }
8907
+ }
8908
+ });
8909
+ core.getUserAvatarUploadDraft.mockReturnValueOnce({
8910
+ draftId: 'avatar-draft-1',
8911
+ userId: 'u1',
8912
+ storageKey: 'users/u1/profile/avatar/avatar-draft-1-headshot.png',
8913
+ finalizedAt: null,
8914
+ discardedAt: null
8915
+ });
8916
+ core.markUserAvatarUploadDraftFinalized.mockReturnValueOnce({
8917
+ draftId: 'avatar-draft-1',
8918
+ userId: 'u1',
8919
+ storageKey: 'users/u1/profile/avatar/avatar-draft-1-headshot.png',
8920
+ finalizedAt: '2026-04-09T00:00:00.000Z',
8921
+ discardedAt: null
8922
+ });
8923
+ const cookie = createSessionTokenCookie('session-avatar-finalize', authConfig, {
8924
+ userId: 'u1',
8925
+ role: 'member',
8926
+ workspaceId: 'workspace-1'
8927
+ });
8928
+ const match = matchRoute('POST', '/api/taskforce/auth/profile/avatar/finalize', routesWithStorage);
8929
+ const req = createMockReq('POST', '/api/taskforce/auth/profile/avatar/finalize', {
8930
+ draftId: 'avatar-draft-1',
8931
+ relativePath: 'users/u1/profile/avatar/avatar-draft-1-headshot.png'
8932
+ }, { cookie });
8933
+ const res = createMockRes();
8934
+ await match?.handler(req, res, {});
8935
+ const payload = JSON.parse(res.end.mock.calls[0][0]);
8936
+ expect(payload.success).toBe(true);
8937
+ expect(payload.avatarUrl).toBe('/api/taskforce/context/users%2Fu1%2Fprofile%2Favatar%2Favatar-draft-1-headshot.png');
8938
+ expect(core.markUserAvatarUploadDraftFinalized).toHaveBeenCalledWith('u1', 'avatar-draft-1');
8939
+ vi.unstubAllGlobals();
8940
+ });
8676
8941
  it('GET /api/taskforce/admin/system-users should require system admin role', async () => {
8677
8942
  const match = matchRoute('GET', '/api/taskforce/admin/system-users', routes);
8678
8943
  const req = createMockReq('GET', '/api/taskforce/admin/system-users', undefined, { 'x-taskforce-system-role': 'user' });
@@ -8701,6 +8966,54 @@ describe('Server Routes', () => {
8701
8966
  expect(payload.planId).toBe('collab');
8702
8967
  expect(payload.features['collaboration.invites'].allowed).toBe(true);
8703
8968
  });
8969
+ it('GET /api/taskforce/account/team-management-access should return unified role and team plan access', async () => {
8970
+ core.resolveUserAccess.mockReturnValueOnce({
8971
+ userId: 'u1',
8972
+ workspaceId: 'default',
8973
+ role: 'owner',
8974
+ disabled: false,
8975
+ betaAccess: true
8976
+ });
8977
+ const match = matchRoute('GET', '/api/taskforce/account/team-management-access', routes);
8978
+ const req = createMockReq('GET', '/api/taskforce/account/team-management-access', undefined, {
8979
+ 'x-taskforce-user-id': 'u1',
8980
+ 'x-taskforce-workspace-id': 'workspace-1',
8981
+ 'x-taskforce-role': 'owner'
8982
+ });
8983
+ const res = createMockRes();
8984
+ await match?.handler(req, res, {});
8985
+ expect(core.resolveUserAccess).toHaveBeenCalledWith('u1', 'default', 'member');
8986
+ expect(core.getAccountEntitlementsPayload).toHaveBeenCalledWith({ userId: 'u1', workspaceId: 'default' });
8987
+ expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
8988
+ const payload = JSON.parse(res.end.mock.calls[0][0]);
8989
+ expect(payload).toMatchObject({
8990
+ workspaceId: 'default',
8991
+ role: 'owner',
8992
+ canManageWorkspace: true,
8993
+ teamPlanMode: 'team',
8994
+ allowed: true
8995
+ });
8996
+ });
8997
+ it('GET /api/taskforce/account/team-management-access should fall back to the hinted workspace role when local access cannot resolve the cloud user id', async () => {
8998
+ core.resolveUserAccess.mockReturnValueOnce(null);
8999
+ const match = matchRoute('GET', '/api/taskforce/account/team-management-access', routes);
9000
+ const req = createMockReq('GET', '/api/taskforce/account/team-management-access?workspaceId=workspace-1', undefined, {
9001
+ 'x-taskforce-user-id': 'u-cloud',
9002
+ 'x-taskforce-workspace-role': 'owner'
9003
+ });
9004
+ const res = createMockRes();
9005
+ await match?.handler(req, res, {});
9006
+ expect(core.resolveUserAccess).toHaveBeenCalledWith('u-cloud', 'workspace-1', 'member');
9007
+ expect(core.getAccountEntitlementsPayload).toHaveBeenCalledWith({ userId: 'u-cloud', workspaceId: 'workspace-1' });
9008
+ const payload = JSON.parse(res.end.mock.calls[0][0]);
9009
+ expect(payload).toMatchObject({
9010
+ workspaceId: 'workspace-1',
9011
+ role: 'owner',
9012
+ canManageWorkspace: true,
9013
+ teamPlanMode: 'team',
9014
+ allowed: true
9015
+ });
9016
+ });
8704
9017
  it('GET /api/taskforce/account/access-status should return pending plan-selection gate', async () => {
8705
9018
  core.getAccountAccessStatus.mockReturnValueOnce({
8706
9019
  gate: 'plan_selection_required',
@@ -10894,7 +11207,8 @@ describe('Server Routes', () => {
10894
11207
  expect(core.bulkCreateTasks).toHaveBeenCalledWith([newTask], {
10895
11208
  atomic: true,
10896
11209
  validateOnly: true,
10897
- workspaceId: 'default'
11210
+ workspaceId: 'default',
11211
+ provisionalLocalReference: true
10898
11212
  });
10899
11213
  expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
10900
11214
  expect(res.end).toHaveBeenCalledWith(JSON.stringify({
@@ -10949,7 +11263,8 @@ describe('Server Routes', () => {
10949
11263
  expect(core.bulkCreateTasks).toHaveBeenCalledWith(inputs, {
10950
11264
  atomic: true,
10951
11265
  validateOnly: false,
10952
- workspaceId: 'default'
11266
+ workspaceId: 'default',
11267
+ provisionalLocalReference: true
10953
11268
  });
10954
11269
  expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
10955
11270
  expect(res.end).toHaveBeenCalledWith(JSON.stringify({
@@ -11023,7 +11338,8 @@ describe('Server Routes', () => {
11023
11338
  expect(core.bulkCreateTasks).toHaveBeenCalledWith(inputs, {
11024
11339
  atomic: true,
11025
11340
  validateOnly: true,
11026
- workspaceId: 'default'
11341
+ workspaceId: 'default',
11342
+ provisionalLocalReference: true
11027
11343
  });
11028
11344
  expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
11029
11345
  expect(res.end).toHaveBeenCalledWith(JSON.stringify({
@@ -11044,6 +11360,36 @@ describe('Server Routes', () => {
11044
11360
  errors: []
11045
11361
  }));
11046
11362
  });
11363
+ it('PATCH /api/taskforce/task/:id should pass autosave updates through as non-audited route metadata', async () => {
11364
+ const updatedTask = { id: '123', description: 'Draft body' };
11365
+ core.updateTask.mockReturnValue(updatedTask);
11366
+ const url = '/api/taskforce/task/123';
11367
+ const match = matchRoute('PATCH', url, routes);
11368
+ const req = createMockReq('PATCH', url, { description: 'Draft body', saveSource: 'autosave' });
11369
+ const res = createMockRes();
11370
+ await match?.handler(req, res, { id: '123' });
11371
+ expect(core.updateTask).toHaveBeenCalledWith('123', { description: 'Draft body' }, 'default', {
11372
+ actorRef: 'user',
11373
+ skipTaskEvents: true
11374
+ });
11375
+ expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
11376
+ expect(res.end).toHaveBeenCalledWith(JSON.stringify(updatedTask));
11377
+ });
11378
+ it('PATCH /api/taskforce/task/:id should keep manual updates on the audited path', async () => {
11379
+ const updatedTask = { id: '123', description: 'Manual body' };
11380
+ core.updateTask.mockReturnValue(updatedTask);
11381
+ const url = '/api/taskforce/task/123';
11382
+ const match = matchRoute('PATCH', url, routes);
11383
+ const req = createMockReq('PATCH', url, { description: 'Manual body', saveSource: 'manual' });
11384
+ const res = createMockRes();
11385
+ await match?.handler(req, res, { id: '123' });
11386
+ expect(core.updateTask).toHaveBeenCalledWith('123', { description: 'Manual body' }, 'default', {
11387
+ actorRef: 'user',
11388
+ skipTaskEvents: false
11389
+ });
11390
+ expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
11391
+ expect(res.end).toHaveBeenCalledWith(JSON.stringify(updatedTask));
11392
+ });
11047
11393
  it('PATCH /api/taskforce/task/:id should return rule-error status/message', async () => {
11048
11394
  core.updateTask.mockImplementation(() => {
11049
11395
  const err = new Error('Task update blocked by rule check');
@@ -1,5 +1,5 @@
1
1
  export type RuntimeMode = 'local' | 'cloud';
2
- export type AuthSource = 'cloud' | 'local';
2
+ export type AuthSource = 'cloud';
3
3
  export type WorkspaceMode = 'single-local' | 'multi-cloud';
4
4
  export interface RuntimeModeContract {
5
5
  runtimeMode: RuntimeMode;
@@ -0,0 +1,17 @@
1
+ import type { WorkspaceSyncAiProfileSnapshot, WorkspaceSyncAnnotatedAttachmentSessionSnapshot, WorkspaceSyncDocumentReviewCommentSnapshot, WorkspaceSyncDocumentReviewSessionSnapshot } from './workspaceSyncModel.js';
2
+ import type { AiProfileSurfaceType } from '../shared/aiProfileSurfaceType.js';
3
+ export declare function normalizeAiProfileSyncPayload(raw: unknown, workspaceId: string, normalizeSurfaceType: (value: unknown, context: {
4
+ source: string;
5
+ profileId?: string;
6
+ workspaceId?: string;
7
+ }) => AiProfileSurfaceType | null, source: string): WorkspaceSyncAiProfileSnapshot | null;
8
+ export declare function normalizeDocumentReviewSessionSyncPayload(raw: unknown, sourceWatermark?: string): WorkspaceSyncDocumentReviewSessionSnapshot | null;
9
+ export declare function normalizeDocumentReviewCommentSyncPayload(raw: unknown, options?: {
10
+ sessionId?: string;
11
+ sourceWatermark?: string;
12
+ }): WorkspaceSyncDocumentReviewCommentSnapshot | null;
13
+ export declare function normalizeAnnotatedAttachmentSessionSyncPayload(raw: unknown, expectedWorkspaceId?: string, sourceWatermark?: string): WorkspaceSyncAnnotatedAttachmentSessionSnapshot | null;
14
+ export declare function normalizeDeleteSyncPayload(raw: unknown, idKey: 'sessionId' | 'commentId'): {
15
+ id: string;
16
+ deletedAt: string;
17
+ } | null;