@taskforcehq/taskforce 0.3.302 → 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 (77) hide show
  1. package/dist/Taskforce.module.css +11 -0
  2. package/dist/TaskforceCore.js +59 -9
  3. package/dist/TaskforceCore.test.js +717 -14
  4. package/dist/components/task/TaskKanban.test.js +17 -0
  5. package/dist/components/views/PlansPage.d.ts +3 -1
  6. package/dist/components/views/PlansPage.js +110 -51
  7. package/dist/components/views/PlansPage.test.d.ts +1 -0
  8. package/dist/components/views/PlansPage.test.js +100 -0
  9. package/dist/components/views/StandaloneLayout.d.ts +1 -0
  10. package/dist/components/views/StandaloneLayout.js +106 -151
  11. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +8 -3
  12. package/dist/components/views/standalone/modals/AccountHubModal.js +16 -2
  13. package/dist/components/views/standalone/modals/AccountHubModal.test.d.ts +1 -0
  14. package/dist/components/views/standalone/modals/AccountHubModal.test.js +26 -0
  15. package/dist/core/EntitlementsPolicy.test.js +2 -2
  16. package/dist/core/GlobalSettingsService.js +78 -18
  17. package/dist/core/PlanEntitlementService.d.ts +29 -5
  18. package/dist/core/PlanEntitlementService.js +297 -219
  19. package/dist/core/SignupMonetizationSettings.test.js +173 -49
  20. package/dist/core/SystemAdmin.test.js +186 -96
  21. package/dist/core/Taskforce.d.ts +51 -10
  22. package/dist/core/Taskforce.js +218 -49
  23. package/dist/core/Taskforce.mcpAuth.test.js +0 -2
  24. package/dist/core/UserProfileAvatarDrafts.test.js +12 -3
  25. package/dist/core/types.d.ts +11 -12
  26. package/dist/hooks/useSyncOrchestrator.js +17 -3
  27. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +97 -5
  28. package/dist/hooks/useTaskforce.d.ts +3 -0
  29. package/dist/hooks/useTaskforce.js +9 -4
  30. package/dist/mcp/runtime.js +90 -29
  31. package/dist/mcp/runtime.test.js +272 -81
  32. package/dist/migrations/billingSchemaParity.test.js +3 -0
  33. package/dist/migrations/taskSchemaMigrations.js +15 -0
  34. package/dist/server/index.cookieProxy.test.js +1 -0
  35. package/dist/server/index.js +2 -0
  36. package/dist/server/index.test.js +2 -1
  37. package/dist/server/mockStripe.d.ts +1 -0
  38. package/dist/server/mockStripe.js +2 -0
  39. package/dist/server/routes/admin.js +80 -23
  40. package/dist/server/routes/auth.d.ts +1 -0
  41. package/dist/server/routes/auth.js +194 -53
  42. package/dist/server/routes/billing.js +506 -23
  43. package/dist/server/routes/billing.test.js +937 -45
  44. package/dist/server/routes/shared.js +5 -3
  45. package/dist/server/routes/sync.integration.test.js +160 -0
  46. package/dist/server/routes/sync.js +15 -5
  47. package/dist/server/routes.js +3 -3
  48. package/dist/server/routes.test.js +287 -76
  49. package/dist/sync/cloudSyncApi.d.ts +1 -0
  50. package/dist/sync/contentSyncState.d.ts +10 -0
  51. package/dist/sync/contentSyncState.js +277 -16
  52. package/dist/sync/syncService.d.ts +1 -0
  53. package/dist/sync/syncService.js +21 -8
  54. package/dist/sync/workspacePullFeed.d.ts +10 -0
  55. package/dist/sync/workspacePullFeed.js +111 -63
  56. package/dist/sync/workspacePullFeed.test.js +112 -0
  57. package/dist/ui/assets/{AgentsModule-JG5jc3he.js → AgentsModule-CpsTmrIW.js} +1 -1
  58. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-Dvi02Y7q.js → AnnotatedAttachmentWorkspace-C_TmXZwA.js} +1 -1
  59. package/dist/ui/assets/{DocumentWorkspace-iys3zfca.js → DocumentWorkspace-C3GyzrWm.js} +1 -1
  60. package/dist/ui/assets/{InitiativesModule-DJvLJI3A.js → InitiativesModule-4-Rh2K2n.js} +1 -1
  61. package/dist/ui/assets/PlansPage-BP7AYOqr.js +1 -0
  62. package/dist/ui/assets/{TaskSettings-Bqd4cCzW.js → TaskSettings-BOC5F6Ag.js} +1 -1
  63. package/dist/ui/assets/{WorkflowsModule-BguHeQaV.js → WorkflowsModule-CGk9s3NJ.js} +1 -1
  64. package/dist/ui/assets/index-CLIMgR6g.js +6 -0
  65. package/dist/ui/assets/index-DneETxcu.css +1 -0
  66. package/dist/ui/assets/{vendor-icons-C8EED3Oh.js → vendor-icons-pWocEipT.js} +1 -1
  67. package/dist/ui/index.html +3 -3
  68. package/dist/utils/accountProfileSummaryCache.d.ts +31 -0
  69. package/dist/utils/accountProfileSummaryCache.js +97 -0
  70. package/dist/utils/accountProfileSummaryCache.test.d.ts +1 -0
  71. package/dist/utils/accountProfileSummaryCache.test.js +63 -0
  72. package/package.json +3 -3
  73. package/dist/ui/assets/PlansPage-BMPlpFpw.js +0 -1
  74. package/dist/ui/assets/index-MXeWoebC.css +0 -1
  75. package/dist/ui/assets/index-YJ4k5QNa.js +0 -6
  76. package/dist/utils/billingStatusCache.d.ts +0 -21
  77. package/dist/utils/billingStatusCache.js +0 -70
@@ -236,6 +236,7 @@ vi.mock('../core/Taskforce', () => {
236
236
  setSystemRole = vi.fn().mockReturnValue(true);
237
237
  setUserDisabledGlobal = vi.fn().mockReturnValue(true);
238
238
  setUserBetaAccessGlobal = vi.fn().mockReturnValue(true);
239
+ purgeSystemUser = vi.fn();
239
240
  updateWorkspaceUserRole = vi.fn();
240
241
  inviteWorkspaceUser = vi.fn();
241
242
  updateWorkspaceProfile = vi.fn().mockImplementation((workspaceId, input) => ({
@@ -262,17 +263,16 @@ vi.mock('../core/Taskforce', () => {
262
263
  revokeInviteAcceptanceForUser = vi.fn().mockReturnValue({ revoked: 1 });
263
264
  getWorkspacePlanMode = vi.fn().mockReturnValue('team');
264
265
  setWorkspacePlanMode = vi.fn().mockImplementation((_workspaceId, mode) => mode);
265
- getSignupMonetizationSettings = vi.fn().mockReturnValue({
266
- strategy: 'auto_assign_default',
267
- defaultSignupPlanVersionId: null
266
+ getOnboardingPolicy = vi.fn().mockReturnValue({
267
+ mode: 'require_plan_selection',
268
+ planVersionId: null
268
269
  });
269
- validateSignupMonetizationSettings = vi.fn().mockReturnValue({ valid: true });
270
+ validateOnboardingPolicy = vi.fn().mockReturnValue({ valid: true });
270
271
  getAccountAccessStatus = vi.fn().mockReturnValue({
271
272
  gate: 'ok',
272
273
  hasEntitlement: true,
273
274
  canAccessApp: true,
274
275
  canAccessPlans: true,
275
- signupMonetizationStrategy: 'auto_assign_default',
276
276
  planSelectionRequired: false,
277
277
  message: null,
278
278
  entitlement: {
@@ -3673,7 +3673,7 @@ describe('Server Routes', () => {
3673
3673
  process.env.VITE_TASKFORCE_API_BASE_URL = previousApiBaseUrl;
3674
3674
  }
3675
3675
  });
3676
- it('POST /api/taskforce/auth/register should create account and set session cookie', async () => {
3676
+ it('POST /api/taskforce/auth/register should create account without a session cookie when verification is required', async () => {
3677
3677
  const authConfig = {
3678
3678
  runtimeMode: 'cloud',
3679
3679
  enabled: true,
@@ -3706,6 +3706,7 @@ describe('Server Routes', () => {
3706
3706
  email: 'owner@example.com',
3707
3707
  role: 'owner'
3708
3708
  }));
3709
+ expect(res.setHeader).not.toHaveBeenCalledWith('Set-Cookie', expect.any(String));
3709
3710
  expect(res.writeHead).toHaveBeenCalledWith(201, expect.any(Object));
3710
3711
  expect(res.end).toHaveBeenCalledWith(expect.stringContaining('"verificationRequired":true'));
3711
3712
  });
@@ -3741,7 +3742,7 @@ describe('Server Routes', () => {
3741
3742
  workspaceId: undefined,
3742
3743
  role: 'member'
3743
3744
  }));
3744
- expect(res.setHeader).toHaveBeenCalledWith('Set-Cookie', expect.stringContaining('taskforce_session='));
3745
+ expect(res.setHeader).not.toHaveBeenCalledWith('Set-Cookie', expect.any(String));
3745
3746
  expect(res.writeHead).toHaveBeenCalledWith(201, expect.any(Object));
3746
3747
  expect(res.end).toHaveBeenCalledWith(expect.stringContaining('"workspaceSetupRequired":true'));
3747
3748
  });
@@ -3780,6 +3781,7 @@ describe('Server Routes', () => {
3780
3781
  const res = createMockRes();
3781
3782
  await match?.handler(req, res, {});
3782
3783
  expect(core.requestEmailVerification).not.toHaveBeenCalled();
3784
+ expect(res.setHeader).toHaveBeenCalledWith('Set-Cookie', expect.stringContaining('taskforce_session='));
3783
3785
  expect(res.writeHead).toHaveBeenCalledWith(201, expect.any(Object));
3784
3786
  expect(res.end).toHaveBeenCalledWith(expect.stringContaining('"verificationRequired":false'));
3785
3787
  });
@@ -3854,7 +3856,7 @@ describe('Server Routes', () => {
3854
3856
  code: 'EMAIL_ALREADY_EXISTS'
3855
3857
  }));
3856
3858
  });
3857
- it('POST /api/taskforce/auth/register should fail when signup monetization is misconfigured', async () => {
3859
+ it('POST /api/taskforce/auth/register should fail when the onboarding policy is misconfigured', async () => {
3858
3860
  const authConfig = {
3859
3861
  runtimeMode: 'cloud',
3860
3862
  enabled: true,
@@ -3867,7 +3869,7 @@ describe('Server Routes', () => {
3867
3869
  sessionTtlSeconds: 3600
3868
3870
  };
3869
3871
  routes = createRoutes(core, { authConfig });
3870
- const error = new Error('Configured default signup plan version "missing-v9" was not found.');
3872
+ const error = new Error('Configured onboarding plan version "missing-v9" was not found.');
3871
3873
  error.statusCode = 500;
3872
3874
  error.code = 'SIGNUP_PLAN_VERSION_NOT_FOUND';
3873
3875
  core.registerPasswordUser.mockImplementationOnce(() => {
@@ -3883,7 +3885,7 @@ describe('Server Routes', () => {
3883
3885
  expect(res.writeHead).toHaveBeenCalledWith(500, expect.any(Object));
3884
3886
  expect(res.end).toHaveBeenCalledWith(JSON.stringify({
3885
3887
  success: false,
3886
- error: 'Configured default signup plan version "missing-v9" was not found.',
3888
+ error: 'Configured onboarding plan version "missing-v9" was not found.',
3887
3889
  code: 'SIGNUP_PLAN_VERSION_NOT_FOUND'
3888
3890
  }));
3889
3891
  });
@@ -3900,12 +3902,19 @@ describe('Server Routes', () => {
3900
3902
  sessionTtlSeconds: 3600
3901
3903
  };
3902
3904
  routes = createRoutes(core, { authConfig });
3905
+ core.inspectOneTimeAuthToken = vi.fn().mockReturnValueOnce({
3906
+ state: 'valid',
3907
+ userId: 'u-verify',
3908
+ workspaceId: 'workspace-1'
3909
+ });
3903
3910
  core.verifyEmailWithToken.mockReturnValueOnce(true);
3904
3911
  const match = matchRoute('POST', '/api/taskforce/auth/verify-email/confirm', routes);
3905
3912
  const req = createMockReq('POST', '/api/taskforce/auth/verify-email/confirm', { token: 'tok1' });
3906
3913
  const res = createMockRes();
3907
3914
  await match?.handler(req, res, {});
3915
+ expect(core.inspectOneTimeAuthToken).toHaveBeenCalledWith('tok1', 'email_verification');
3908
3916
  expect(core.verifyEmailWithToken).toHaveBeenCalledWith('tok1');
3917
+ expect(res.setHeader).toHaveBeenCalledWith('Set-Cookie', expect.stringContaining('taskforce_session='));
3909
3918
  expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
3910
3919
  });
3911
3920
  it('POST /api/taskforce/auth/password-reset/confirm should set new password', async () => {
@@ -4779,10 +4788,10 @@ describe('Server Routes', () => {
4779
4788
  }, { cookie });
4780
4789
  const res = createMockRes();
4781
4790
  await match?.handler(req, res, {});
4782
- expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith({
4783
- tasks: [{ id: 'task-1', title: 'Task 1' }],
4784
- archived: [{ id: 'task-2', title: 'Task 2', isArchived: true }]
4785
- }, 'workspace-1');
4791
+ expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith(expect.objectContaining({
4792
+ tasks: [expect.objectContaining({ id: 'task-1', title: 'Task 1' })],
4793
+ archived: [expect.objectContaining({ id: 'task-2', title: 'Task 2', isArchived: true })]
4794
+ }), 'workspace-1');
4786
4795
  expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
4787
4796
  expect(res.end).toHaveBeenCalledWith(expect.stringContaining('"success":true'));
4788
4797
  expect(core.addAdminAuditLog).toHaveBeenCalledWith(expect.objectContaining({
@@ -6427,10 +6436,10 @@ describe('Server Routes', () => {
6427
6436
  const res = createMockRes();
6428
6437
  await match?.handler(req, res, {});
6429
6438
  expect(core.deleteTaskForSync).toHaveBeenCalledWith('task-2', 'workspace-local', { persistBackup: false });
6430
- expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith({
6431
- tasks: [{ id: 'task-1', title: 'Task 1' }],
6439
+ expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith(expect.objectContaining({
6440
+ tasks: [expect.objectContaining({ id: 'task-1', title: 'Task 1' })],
6432
6441
  archived: []
6433
- }, 'workspace-local');
6442
+ }), 'workspace-local');
6434
6443
  expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
6435
6444
  });
6436
6445
  it('POST /api/taskforce/sync/workspace/apply-local should apply document review session upserts and deletes', async () => {
@@ -6868,10 +6877,10 @@ describe('Server Routes', () => {
6868
6877
  });
6869
6878
  const res = createMockRes();
6870
6879
  await match?.handler(req, res, {});
6871
- expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith({
6872
- tasks: [{ id: 'task-shared-1', title: 'Shared task' }],
6880
+ expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith(expect.objectContaining({
6881
+ tasks: [expect.objectContaining({ id: 'task-shared-1', title: 'Shared task' })],
6873
6882
  archived: []
6874
- }, 'workspace-shared-local');
6883
+ }), 'workspace-shared-local');
6875
6884
  expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
6876
6885
  });
6877
6886
  it('POST /api/taskforce/sync/workspace/apply-local should route archived upserts and preserve canceledReason', async () => {
@@ -6898,17 +6907,17 @@ describe('Server Routes', () => {
6898
6907
  });
6899
6908
  const res = createMockRes();
6900
6909
  await match?.handler(req, res, {});
6901
- expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith({
6910
+ expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith(expect.objectContaining({
6902
6911
  tasks: [],
6903
- archived: [{
6912
+ archived: [expect.objectContaining({
6904
6913
  id: 'task-archived-cancelled',
6905
6914
  title: 'Cancelled + Archived',
6906
6915
  status: 'cancelled',
6907
6916
  canceledReason: 'Scope changed',
6908
6917
  workstreamId: 'workstream-archived-1',
6909
6918
  isArchived: true
6910
- }]
6911
- }, 'workspace-local');
6919
+ })]
6920
+ }), 'workspace-local');
6912
6921
  expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
6913
6922
  });
6914
6923
  it('POST /api/taskforce/sync/workspace/apply-local should prioritize delete over same-task upsert', async () => {
@@ -7575,10 +7584,10 @@ describe('Server Routes', () => {
7575
7584
  }, { cookie });
7576
7585
  const res1 = createMockRes();
7577
7586
  await match?.handler(req1, res1, {});
7578
- expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith({
7579
- tasks: [{ id: 'task-1', title: 'Task 1' }],
7587
+ expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith(expect.objectContaining({
7588
+ tasks: [expect.objectContaining({ id: 'task-1', title: 'Task 1' })],
7580
7589
  archived: []
7581
- }, 'workspace-1');
7590
+ }), 'workspace-1');
7582
7591
  expect(res1.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
7583
7592
  const firstPayload = JSON.parse(String(res1.end.mock.calls.at(-1)?.[0] || '{}'));
7584
7593
  expect(Array.isArray(firstPayload.emittedEventIds)).toBe(true);
@@ -7780,10 +7789,10 @@ describe('Server Routes', () => {
7780
7789
  }, { cookie });
7781
7790
  const res = createMockRes();
7782
7791
  await match?.handler(req, res, {});
7783
- expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith({
7784
- tasks: [{ id: 'task-1', title: 'Task 1' }],
7792
+ expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith(expect.objectContaining({
7793
+ tasks: [expect.objectContaining({ id: 'task-1', title: 'Task 1' })],
7785
7794
  archived: []
7786
- }, 'workspace-local-active');
7795
+ }), 'workspace-local-active');
7787
7796
  expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
7788
7797
  expect(core.listUserWorkspaces).toHaveBeenCalledWith('user-1');
7789
7798
  });
@@ -8060,15 +8069,15 @@ describe('Server Routes', () => {
8060
8069
  }, { cookie });
8061
8070
  const res = createMockRes();
8062
8071
  await match?.handler(req, res, {});
8063
- expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith({
8064
- tasks: [{
8072
+ expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith(expect.objectContaining({
8073
+ tasks: [expect.objectContaining({
8065
8074
  id: 'task-cancelled-1',
8066
8075
  title: 'Cancelled Task',
8067
8076
  status: 'cancelled',
8068
8077
  canceledReason: 'No longer needed'
8069
- }],
8078
+ })],
8070
8079
  archived: []
8071
- }, 'workspace-1');
8080
+ }), 'workspace-1');
8072
8081
  expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
8073
8082
  });
8074
8083
  it('POST /api/taskforce/sync/workspace/push should preserve workstreamId in upsert payload', async () => {
@@ -8109,15 +8118,15 @@ describe('Server Routes', () => {
8109
8118
  }, { cookie });
8110
8119
  const res = createMockRes();
8111
8120
  await match?.handler(req, res, {});
8112
- expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith({
8113
- tasks: [{
8121
+ expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith(expect.objectContaining({
8122
+ tasks: [expect.objectContaining({
8114
8123
  id: 'task-workstream-1',
8115
8124
  title: 'Linked task',
8116
8125
  status: 'task',
8117
8126
  workstreamId: 'workstream-1'
8118
- }],
8127
+ })],
8119
8128
  archived: []
8120
- }, 'workspace-1');
8129
+ }), 'workspace-1');
8121
8130
  expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
8122
8131
  });
8123
8132
  it('POST /api/taskforce/sync/workspace/push should route archived upserts into archived snapshot list', async () => {
@@ -8158,15 +8167,15 @@ describe('Server Routes', () => {
8158
8167
  }, { cookie });
8159
8168
  const res = createMockRes();
8160
8169
  await match?.handler(req, res, {});
8161
- expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith({
8170
+ expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith(expect.objectContaining({
8162
8171
  tasks: [],
8163
- archived: [{
8172
+ archived: [expect.objectContaining({
8164
8173
  id: 'task-archived-1',
8165
8174
  title: 'Archived Task',
8166
8175
  workstreamId: 'workstream-archived-1',
8167
8176
  isArchived: true
8168
- }]
8169
- }, 'workspace-1');
8177
+ })]
8178
+ }), 'workspace-1');
8170
8179
  expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
8171
8180
  });
8172
8181
  it('GET /api/taskforce/sync/workspace/pull should include canceledReason and archived marker in response payload', async () => {
@@ -8292,7 +8301,7 @@ describe('Server Routes', () => {
8292
8301
  expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
8293
8302
  const response = JSON.parse(String(res.end.mock.calls[0]?.[0] || '{}'));
8294
8303
  const changes = Array.isArray(response?.changes) ? response.changes : [];
8295
- expect(changes).toContainEqual({
8304
+ expect(changes).toContainEqual(expect.objectContaining({
8296
8305
  op: 'taxonomy-upsert',
8297
8306
  taxonomyState: {
8298
8307
  categories: [{ value: 'default', label: 'General' }],
@@ -8328,8 +8337,9 @@ describe('Server Routes', () => {
8328
8337
  filterEnabled: true,
8329
8338
  sortEnabled: true
8330
8339
  }],
8340
+ updatedAt: '2026-02-22T10:00:00.000Z',
8331
8341
  watermark: '2026-02-22T10:00:00.000Z'
8332
- });
8342
+ }));
8333
8343
  });
8334
8344
  it('POST /api/taskforce/workspaces should create workspace for authenticated user', async () => {
8335
8345
  const authConfig = {
@@ -9014,13 +9024,68 @@ describe('Server Routes', () => {
9014
9024
  allowed: true
9015
9025
  });
9016
9026
  });
9027
+ it('GET /api/taskforce/account/team-management-access should still resolve access when billing tables are unavailable', async () => {
9028
+ core.resolveUserAccess.mockReturnValueOnce({
9029
+ userId: 'u1',
9030
+ workspaceId: 'workspace-1',
9031
+ role: 'owner',
9032
+ disabled: false,
9033
+ betaAccess: true
9034
+ });
9035
+ core.getAccountAccessStatus.mockReturnValueOnce({
9036
+ gate: 'ok',
9037
+ hasEntitlement: true,
9038
+ canAccessApp: true,
9039
+ canAccessPlans: true,
9040
+ planSelectionRequired: false,
9041
+ message: null,
9042
+ entitlement: {
9043
+ state: 'active',
9044
+ planId: 'collab',
9045
+ planVersionId: 'collab-v1'
9046
+ }
9047
+ });
9048
+ core.getAccountEntitlementsPayload.mockReturnValueOnce({
9049
+ state: 'active',
9050
+ planId: 'collab',
9051
+ features: {
9052
+ 'collaboration.team_management': { allowed: true }
9053
+ }
9054
+ });
9055
+ const baseDb = core.getDatabaseAdapter();
9056
+ core.getDatabaseAdapter = vi.fn().mockReturnValue({
9057
+ ...baseDb,
9058
+ prepare: vi.fn((sql) => {
9059
+ if (sql.includes('FROM user_billing') || sql.includes('FROM plan_catalog')) {
9060
+ throw new Error('no such table');
9061
+ }
9062
+ return baseDb.prepare(sql);
9063
+ })
9064
+ });
9065
+ const match = matchRoute('GET', '/api/taskforce/account/team-management-access', routes);
9066
+ const req = createMockReq('GET', '/api/taskforce/account/team-management-access?workspaceId=workspace-1', undefined, {
9067
+ 'x-taskforce-user-id': 'u1'
9068
+ });
9069
+ const res = createMockRes();
9070
+ await match?.handler(req, res, {});
9071
+ expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
9072
+ const payload = JSON.parse(res.end.mock.calls[0][0]);
9073
+ expect(payload).toMatchObject({
9074
+ workspaceId: 'workspace-1',
9075
+ role: 'owner',
9076
+ canManageWorkspace: true,
9077
+ teamPlanMode: 'team',
9078
+ allowed: true,
9079
+ gate: 'ok',
9080
+ planSelectionRequired: false
9081
+ });
9082
+ });
9017
9083
  it('GET /api/taskforce/account/access-status should return pending plan-selection gate', async () => {
9018
9084
  core.getAccountAccessStatus.mockReturnValueOnce({
9019
9085
  gate: 'plan_selection_required',
9020
9086
  hasEntitlement: false,
9021
9087
  canAccessApp: false,
9022
9088
  canAccessPlans: true,
9023
- signupMonetizationStrategy: 'plan_selection_required',
9024
9089
  planSelectionRequired: true,
9025
9090
  message: 'Choose a plan to continue.',
9026
9091
  entitlement: null
@@ -9046,7 +9111,6 @@ describe('Server Routes', () => {
9046
9111
  hasEntitlement: false,
9047
9112
  canAccessApp: false,
9048
9113
  canAccessPlans: true,
9049
- signupMonetizationStrategy: 'plan_selection_required',
9050
9114
  planSelectionRequired: true,
9051
9115
  message: 'Choose a plan to continue.',
9052
9116
  entitlement: null
@@ -9061,6 +9125,87 @@ describe('Server Routes', () => {
9061
9125
  expect(res.writeHead).toHaveBeenCalledWith(403, expect.any(Object));
9062
9126
  expect(res.end).toHaveBeenCalledWith(expect.stringContaining('"code":"PLAN_SELECTION_REQUIRED"'));
9063
9127
  });
9128
+ it('GET /api/taskforce/account/profile-summary should return checkout-pending recovery context', async () => {
9129
+ core.resolveUserAccess.mockReturnValueOnce({
9130
+ userId: 'u1',
9131
+ workspaceId: 'workspace-1',
9132
+ role: 'owner',
9133
+ disabled: false,
9134
+ betaAccess: true
9135
+ });
9136
+ core.getAccountAccessStatus.mockReturnValueOnce({
9137
+ gate: 'checkout_pending',
9138
+ hasEntitlement: true,
9139
+ canAccessApp: false,
9140
+ canAccessPlans: true,
9141
+ planSelectionRequired: false,
9142
+ message: 'Complete checkout to continue.',
9143
+ entitlement: {
9144
+ accountId: 'u1',
9145
+ planId: 'pro',
9146
+ planVersionId: 'pro-v1',
9147
+ state: 'checkout_pending'
9148
+ }
9149
+ });
9150
+ core.getAccountEntitlementsPayload.mockImplementationOnce(() => {
9151
+ const error = new Error('Entitlement not found');
9152
+ error.code = 'ENTITLEMENT_NOT_FOUND';
9153
+ throw error;
9154
+ });
9155
+ const match = matchRoute('GET', '/api/taskforce/account/profile-summary', routes);
9156
+ const req = createMockReq('GET', '/api/taskforce/account/profile-summary?workspaceId=workspace-1', undefined, {
9157
+ 'x-taskforce-user-id': 'u1'
9158
+ });
9159
+ const res = createMockRes();
9160
+ await match?.handler(req, res, {});
9161
+ expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
9162
+ const payload = JSON.parse(res.end.mock.calls[0][0]);
9163
+ expect(payload).toMatchObject({
9164
+ planId: 'pro',
9165
+ planVersionId: 'pro-v1',
9166
+ entitlementState: 'checkout_pending',
9167
+ gate: 'checkout_pending',
9168
+ message: 'Complete checkout to continue.',
9169
+ billingConflictCode: null,
9170
+ planSelectionRequired: false,
9171
+ workspaceId: 'workspace-1',
9172
+ workspaceRole: 'owner',
9173
+ canManageWorkspace: true,
9174
+ teamPlanMode: 'personal',
9175
+ teamManagementAllowed: false
9176
+ });
9177
+ });
9178
+ it('GET /api/taskforce/account/entitlements should surface checkout-pending gate when billing is not complete', async () => {
9179
+ core.getAccountEntitlementsPayload.mockImplementationOnce(() => {
9180
+ const error = new Error('Entitlement not found');
9181
+ error.code = 'ENTITLEMENT_NOT_FOUND';
9182
+ throw error;
9183
+ });
9184
+ core.getAccountAccessStatus.mockReturnValueOnce({
9185
+ gate: 'checkout_pending',
9186
+ hasEntitlement: true,
9187
+ canAccessApp: false,
9188
+ canAccessPlans: true,
9189
+ planSelectionRequired: false,
9190
+ message: 'Complete checkout to continue.',
9191
+ entitlement: {
9192
+ accountId: 'u1',
9193
+ planId: 'pro',
9194
+ planVersionId: 'pro-v1',
9195
+ state: 'checkout_pending'
9196
+ }
9197
+ });
9198
+ const match = matchRoute('GET', '/api/taskforce/account/entitlements', routes);
9199
+ const req = createMockReq('GET', '/api/taskforce/account/entitlements', undefined, {
9200
+ 'x-taskforce-user-id': 'u1',
9201
+ 'x-taskforce-workspace-id': 'workspace-1'
9202
+ });
9203
+ const res = createMockRes();
9204
+ await match?.handler(req, res, {});
9205
+ expect(res.writeHead).toHaveBeenCalledWith(403, expect.any(Object));
9206
+ expect(res.end).toHaveBeenCalledWith(expect.stringContaining('"code":"CHECKOUT_PENDING"'));
9207
+ expect(res.end).toHaveBeenCalledWith(expect.stringContaining('"gate":"checkout_pending"'));
9208
+ });
9064
9209
  it('GET /api/taskforce/config should block authenticated users who must select a plan first', async () => {
9065
9210
  const authConfig = {
9066
9211
  runtimeMode: 'cloud',
@@ -9084,7 +9229,6 @@ describe('Server Routes', () => {
9084
9229
  hasEntitlement: false,
9085
9230
  canAccessApp: false,
9086
9231
  canAccessPlans: true,
9087
- signupMonetizationStrategy: 'plan_selection_required',
9088
9232
  planSelectionRequired: true,
9089
9233
  message: 'Choose a plan to continue.',
9090
9234
  entitlement: null
@@ -9123,7 +9267,6 @@ describe('Server Routes', () => {
9123
9267
  hasEntitlement: false,
9124
9268
  canAccessApp: false,
9125
9269
  canAccessPlans: true,
9126
- signupMonetizationStrategy: 'auto_assign_default',
9127
9270
  planSelectionRequired: false,
9128
9271
  message: 'No entitlement is linked to this account.',
9129
9272
  entitlement: null
@@ -9716,6 +9859,64 @@ describe('Server Routes', () => {
9716
9859
  }));
9717
9860
  expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
9718
9861
  });
9862
+ it('DELETE /api/taskforce/admin/system-users/:id should purge a user in allowed cloud environments', async () => {
9863
+ const authConfig = { runtimeMode: 'cloud', enabled: false };
9864
+ routes = createRoutes(core, { authConfig });
9865
+ core.purgeSystemUser.mockReturnValue({
9866
+ targetUserId: 'u1',
9867
+ email: 'u1@example.com',
9868
+ unassignedTaskCount: 2,
9869
+ clearedInitiativeOwnerCount: 0,
9870
+ clearedWorkstreamOwnerCount: 0,
9871
+ deletedWorkspaceMembershipCount: 1,
9872
+ deletedAuthTokenCount: 1,
9873
+ deletedAvatarDraftCount: 0,
9874
+ deletedMcpAccessTokenCount: 1,
9875
+ deletedMcpOauthAuthCodeCount: 0,
9876
+ deletedMcpOauthConnectorGrantCount: 0,
9877
+ deletedUiStateCount: 1,
9878
+ deletedUserPreferenceCount: 1,
9879
+ deletedEntitlementCount: 1,
9880
+ deletedUserBillingCount: 1,
9881
+ deletedUserCount: 1
9882
+ });
9883
+ const match = matchRoute('DELETE', '/api/taskforce/admin/system-users/u1', routes);
9884
+ const req = createMockReq('DELETE', '/api/taskforce/admin/system-users/u1', { emailConfirmation: 'u1@example.com' }, {
9885
+ 'x-taskforce-system-role': 'system_admin',
9886
+ 'x-taskforce-user-id': 'admin-user',
9887
+ host: 'performance-app.taskforcehq.ai',
9888
+ 'x-forwarded-proto': 'https'
9889
+ });
9890
+ const res = createMockRes();
9891
+ await match?.handler(req, res, { id: 'u1' });
9892
+ expect(core.purgeSystemUser).toHaveBeenCalledWith({
9893
+ targetUserId: 'u1',
9894
+ actorUserId: 'admin-user',
9895
+ emailConfirmation: 'u1@example.com'
9896
+ });
9897
+ expect(core.addAdminAuditLog).toHaveBeenCalledWith(expect.objectContaining({
9898
+ action: 'purge-system-user',
9899
+ actorRole: 'system_admin',
9900
+ workspaceId: 'system'
9901
+ }));
9902
+ expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
9903
+ });
9904
+ it('DELETE /api/taskforce/admin/system-users/:id should reject production environment purges', async () => {
9905
+ const authConfig = { runtimeMode: 'cloud', enabled: false };
9906
+ routes = createRoutes(core, { authConfig });
9907
+ const match = matchRoute('DELETE', '/api/taskforce/admin/system-users/u1', routes);
9908
+ const req = createMockReq('DELETE', '/api/taskforce/admin/system-users/u1', { emailConfirmation: 'u1@example.com' }, {
9909
+ 'x-taskforce-system-role': 'system_admin',
9910
+ 'x-taskforce-user-id': 'admin-user',
9911
+ host: 'app.taskforcehq.ai',
9912
+ 'x-forwarded-proto': 'https'
9913
+ });
9914
+ const res = createMockRes();
9915
+ await match?.handler(req, res, { id: 'u1' });
9916
+ expect(core.purgeSystemUser).not.toHaveBeenCalled();
9917
+ expect(res.writeHead).toHaveBeenCalledWith(403, expect.any(Object));
9918
+ expect(res.end).toHaveBeenCalledWith(expect.stringContaining('"code":"SYSTEM_USER_PURGE_ENVIRONMENT_FORBIDDEN"'));
9919
+ });
9719
9920
  it('removed workspace-based plan route should not match', () => {
9720
9921
  expect(matchRoute('PATCH', '/api/taskforce/admin/workspace/plan', routes)).toBeNull();
9721
9922
  expect(matchRoute('GET', '/api/taskforce/workspace/assignee-options', routes)).not.toBeNull();
@@ -10033,7 +10234,7 @@ describe('Server Routes', () => {
10033
10234
  await match?.handler(req, res, {});
10034
10235
  expect(res.writeHead).toHaveBeenCalledWith(403, expect.any(Object));
10035
10236
  });
10036
- it('GET /api/taskforce/admin/system-settings should include signup monetization fields', async () => {
10237
+ it('GET /api/taskforce/admin/system-settings should include onboarding policy fields', async () => {
10037
10238
  core.getGlobalSettings.mockReturnValue({
10038
10239
  auth: {
10039
10240
  allowSelfRegistration: true,
@@ -10041,26 +10242,30 @@ describe('Server Routes', () => {
10041
10242
  defaultNewUserBetaAccess: false,
10042
10243
  authRateLimitEnabled: true,
10043
10244
  authRateLimitMaxRequests: 10,
10044
- authRateLimitWindowMs: 900000,
10045
- signupMonetizationStrategy: 'auto_assign_default',
10046
- defaultSignupPlanVersionId: 'starter-v1'
10245
+ authRateLimitWindowMs: 900000
10246
+ },
10247
+ billing: {
10248
+ onboardingPolicy: {
10249
+ mode: 'auto_grant_free',
10250
+ planVersionId: 'starter-v1'
10251
+ }
10047
10252
  }
10048
10253
  });
10049
- core.getSignupMonetizationSettings.mockReturnValue({
10050
- strategy: 'auto_assign_default',
10051
- defaultSignupPlanVersionId: 'starter-v1'
10254
+ core.getOnboardingPolicy.mockReturnValue({
10255
+ mode: 'auto_grant_free',
10256
+ planVersionId: 'starter-v1'
10052
10257
  });
10053
10258
  const match = matchRoute('GET', '/api/taskforce/admin/system-settings', routes);
10054
10259
  const req = createMockReq('GET', '/api/taskforce/admin/system-settings', undefined, { 'x-taskforce-role': 'admin' });
10055
10260
  const res = createMockRes();
10056
10261
  await match?.handler(req, res, {});
10057
10262
  const payload = JSON.parse(res.end.mock.calls[0][0]);
10058
- expect(payload.status.signupMonetizationStrategy).toBe('auto_assign_default');
10059
- expect(payload.status.defaultSignupPlanVersionId).toBe('starter-v1');
10060
- expect(payload.settings.signupMonetizationStrategy).toBe('auto_assign_default');
10061
- expect(payload.settings.defaultSignupPlanVersionId).toBe('starter-v1');
10263
+ expect(payload.status.onboardingPolicyMode).toBe('auto_grant_free');
10264
+ expect(payload.status.onboardingPlanVersionId).toBe('starter-v1');
10265
+ expect(payload.settings.onboardingPolicyMode).toBe('auto_grant_free');
10266
+ expect(payload.settings.onboardingPlanVersionId).toBe('starter-v1');
10062
10267
  });
10063
- it('POST /api/taskforce/admin/system-settings should persist signup monetization fields', async () => {
10268
+ it('POST /api/taskforce/admin/system-settings should persist onboarding policy fields', async () => {
10064
10269
  const match = matchRoute('POST', '/api/taskforce/admin/system-settings', routes);
10065
10270
  const req = createMockReq('POST', '/api/taskforce/admin/system-settings', {
10066
10271
  allowSelfRegistration: true,
@@ -10069,14 +10274,14 @@ describe('Server Routes', () => {
10069
10274
  authRateLimitEnabled: true,
10070
10275
  authRateLimitMaxRequests: 10,
10071
10276
  authRateLimitWindowMs: 900000,
10072
- signupMonetizationStrategy: 'auto_assign_default',
10073
- defaultSignupPlanVersionId: 'starter-v1'
10277
+ onboardingPolicyMode: 'auto_grant_free',
10278
+ onboardingPlanVersionId: 'starter-v1'
10074
10279
  }, { 'x-taskforce-role': 'admin' });
10075
10280
  const res = createMockRes();
10076
10281
  await match?.handler(req, res, {});
10077
- expect(core.validateSignupMonetizationSettings).toHaveBeenCalledWith({
10078
- strategy: 'auto_assign_default',
10079
- defaultSignupPlanVersionId: 'starter-v1'
10282
+ expect(core.validateOnboardingPolicy).toHaveBeenCalledWith({
10283
+ mode: 'auto_grant_free',
10284
+ planVersionId: 'starter-v1'
10080
10285
  });
10081
10286
  expect(core.updateGlobalSettings).toHaveBeenCalledWith({
10082
10287
  auth: {
@@ -10085,9 +10290,13 @@ describe('Server Routes', () => {
10085
10290
  defaultNewUserBetaAccess: false,
10086
10291
  authRateLimitEnabled: true,
10087
10292
  authRateLimitMaxRequests: 10,
10088
- authRateLimitWindowMs: 900000,
10089
- signupMonetizationStrategy: 'auto_assign_default',
10090
- defaultSignupPlanVersionId: 'starter-v1'
10293
+ authRateLimitWindowMs: 900000
10294
+ },
10295
+ billing: {
10296
+ onboardingPolicy: {
10297
+ mode: 'auto_grant_free',
10298
+ planVersionId: 'starter-v1'
10299
+ }
10091
10300
  }
10092
10301
  });
10093
10302
  });
@@ -10196,23 +10405,25 @@ describe('Server Routes', () => {
10196
10405
  expect(res.end.mock.calls[0][0]).toContain('window.__TASKFORCE_SITE_CONFIG__ = ');
10197
10406
  expect(res.end.mock.calls[0][0]).toContain('"pricingPageEnabled":false');
10198
10407
  });
10199
- it('POST /api/taskforce/admin/system-settings should reject invalid signup monetization settings', async () => {
10200
- core.validateSignupMonetizationSettings.mockReturnValueOnce({
10408
+ it('POST /api/taskforce/admin/system-settings should reject invalid onboarding settings', async () => {
10409
+ core.validateOnboardingPolicy.mockReturnValueOnce({
10201
10410
  valid: false,
10202
- code: 'SIGNUP_PLAN_VERSION_REQUIRED',
10203
- message: 'Default signup plan version is required when signup strategy is auto-assign default.'
10411
+ code: 'ONBOARDING_PLAN_VERSION_REQUIRED',
10412
+ message: 'An onboarding plan version is required for the selected onboarding mode.'
10204
10413
  });
10205
10414
  const match = matchRoute('POST', '/api/taskforce/admin/system-settings', routes);
10206
10415
  const req = createMockReq('POST', '/api/taskforce/admin/system-settings', {
10207
- signupMonetizationStrategy: 'auto_assign_default',
10208
- defaultSignupPlanVersionId: null
10416
+ onboardingPolicyMode: 'auto_grant_free',
10417
+ onboardingPlanVersionId: null
10209
10418
  }, { 'x-taskforce-role': 'admin' });
10210
10419
  const res = createMockRes();
10211
10420
  await match?.handler(req, res, {});
10212
10421
  expect(res.writeHead).toHaveBeenCalledWith(400, expect.any(Object));
10213
10422
  expect(core.updateGlobalSettings).not.toHaveBeenCalledWith(expect.objectContaining({
10214
- auth: expect.objectContaining({
10215
- signupMonetizationStrategy: 'auto_assign_default'
10423
+ billing: expect.objectContaining({
10424
+ onboardingPolicy: expect.objectContaining({
10425
+ mode: 'auto_grant_free'
10426
+ })
10216
10427
  })
10217
10428
  }));
10218
10429
  });
@@ -25,6 +25,7 @@ export declare function pullWorkspaceChanges(resolveCloudAuthUrl: (path: string)
25
25
  nextCursor?: string | null;
26
26
  hasMore?: boolean;
27
27
  workspaceMembers?: WorkspaceSyncWorkspaceMemberSnapshot[];
28
+ diagnostics?: Record<string, unknown>;
28
29
  }>>;
29
30
  export declare function pushWorkspaceChanges(resolveCloudAuthUrl: (path: string) => string, input: {
30
31
  workspaceId: string;