@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
@@ -4,7 +4,17 @@ import os from 'os';
4
4
  import path from 'path';
5
5
  import { TaskforceCore } from './Taskforce';
6
6
  function makeProjectRoot() {
7
- return fs.mkdtempSync(path.join(os.tmpdir(), 'taskforce-signup-monetization-'));
7
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'taskforce-onboarding-policy-'));
8
+ }
9
+ function insertFreePriceMapping(core, planVersionId) {
10
+ const db = core.db;
11
+ const tenantId = core.getTenantId();
12
+ const now = new Date().toISOString();
13
+ db.prepare(`
14
+ INSERT OR IGNORE INTO billing_price_mappings (
15
+ tenant_id, plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency, created_at, updated_at
16
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
17
+ `).run(tenantId, planVersionId, 'month', `free:${planVersionId}:month`, 'free', 1, 0, null, now, now);
8
18
  }
9
19
  function openCore(projectRoot) {
10
20
  const core = new TaskforceCore({
@@ -12,31 +22,24 @@ function openCore(projectRoot) {
12
22
  storagePath: path.join(projectRoot, '.taskforce'),
13
23
  tenantId: path.basename(projectRoot)
14
24
  });
15
- const db = core.db;
16
- const now = new Date().toISOString();
17
25
  core.upsertPlanCatalog({ planId: 'starter', displayName: 'Starter' });
18
- db.prepare(`
19
- INSERT OR IGNORE INTO billing_price_mappings (
20
- tenant_id, plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency, created_at, updated_at
21
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
22
- `).run(path.basename(projectRoot), 'starter-v1', 'month', 'free:starter-v1:month', 'free', 1, 0, null, now, now);
23
- core.upsertPlanCatalog({ planId: 'starter', displayName: 'Starter', status: 'active' });
24
26
  core.updatePlanVersionFeatures({
25
27
  planVersionId: 'starter-v1',
26
28
  features: [
27
29
  { featureKey: 'workspace.switching', access: 'enabled' }
28
30
  ]
29
31
  });
32
+ insertFreePriceMapping(core, 'starter-v1');
30
33
  return core;
31
34
  }
32
- describe('TaskforceCore signup monetization settings', () => {
33
- it('normalizes defaults to auto_assign_default with no version id', () => {
35
+ describe('TaskforceCore onboarding policy', () => {
36
+ it('defaults to require_plan_selection with no plan version', () => {
34
37
  const projectRoot = makeProjectRoot();
35
38
  const core = openCore(projectRoot);
36
39
  try {
37
- expect(core.getSignupMonetizationSettings()).toEqual({
38
- strategy: 'auto_assign_default',
39
- defaultSignupPlanVersionId: null
40
+ expect(core.getOnboardingPolicy()).toEqual({
41
+ mode: 'require_plan_selection',
42
+ planVersionId: null
40
43
  });
41
44
  }
42
45
  finally {
@@ -44,16 +47,16 @@ describe('TaskforceCore signup monetization settings', () => {
44
47
  fs.rmSync(projectRoot, { recursive: true, force: true });
45
48
  }
46
49
  });
47
- it('fails validation for auto_assign_default without a default version id', () => {
50
+ it('fails validation for auto_grant_free without a plan version id', () => {
48
51
  const projectRoot = makeProjectRoot();
49
52
  const core = openCore(projectRoot);
50
53
  try {
51
- expect(core.validateSignupMonetizationSettings({
52
- strategy: 'auto_assign_default',
53
- defaultSignupPlanVersionId: null
54
+ expect(core.validateOnboardingPolicy({
55
+ mode: 'auto_grant_free',
56
+ planVersionId: null
54
57
  })).toEqual(expect.objectContaining({
55
58
  valid: false,
56
- code: 'SIGNUP_PLAN_VERSION_REQUIRED'
59
+ code: 'ONBOARDING_PLAN_VERSION_REQUIRED'
57
60
  }));
58
61
  }
59
62
  finally {
@@ -65,12 +68,12 @@ describe('TaskforceCore signup monetization settings', () => {
65
68
  const projectRoot = makeProjectRoot();
66
69
  const core = openCore(projectRoot);
67
70
  try {
68
- expect(core.validateSignupMonetizationSettings({
69
- strategy: 'auto_assign_default',
70
- defaultSignupPlanVersionId: 'missing-v9'
71
+ expect(core.validateOnboardingPolicy({
72
+ mode: 'auto_grant_free',
73
+ planVersionId: 'missing-v9'
71
74
  })).toEqual(expect.objectContaining({
72
75
  valid: false,
73
- code: 'SIGNUP_PLAN_VERSION_NOT_FOUND'
76
+ code: 'ONBOARDING_PLAN_VERSION_NOT_FOUND'
74
77
  }));
75
78
  }
76
79
  finally {
@@ -83,12 +86,12 @@ describe('TaskforceCore signup monetization settings', () => {
83
86
  const core = openCore(projectRoot);
84
87
  try {
85
88
  core.upsertPlanCatalog({ planId: 'starter', displayName: 'Starter', status: 'archived' });
86
- expect(core.validateSignupMonetizationSettings({
87
- strategy: 'auto_assign_default',
88
- defaultSignupPlanVersionId: 'starter-v1'
89
+ expect(core.validateOnboardingPolicy({
90
+ mode: 'auto_grant_free',
91
+ planVersionId: 'starter-v1'
89
92
  })).toEqual(expect.objectContaining({
90
93
  valid: false,
91
- code: 'SIGNUP_PLAN_NOT_ENABLED'
94
+ code: 'ONBOARDING_PLAN_NOT_ENABLED'
92
95
  }));
93
96
  }
94
97
  finally {
@@ -96,13 +99,31 @@ describe('TaskforceCore signup monetization settings', () => {
96
99
  fs.rmSync(projectRoot, { recursive: true, force: true });
97
100
  }
98
101
  });
99
- it('passes validation for plan_selection_required without a default version id', () => {
102
+ it('fails validation when auto_grant_free points at a version without free pricing', () => {
100
103
  const projectRoot = makeProjectRoot();
101
104
  const core = openCore(projectRoot);
102
105
  try {
103
- expect(core.validateSignupMonetizationSettings({
104
- strategy: 'plan_selection_required',
105
- defaultSignupPlanVersionId: null
106
+ core.upsertPlanCatalog({ planId: 'collab', displayName: 'Collab', status: 'active' });
107
+ expect(core.validateOnboardingPolicy({
108
+ mode: 'auto_grant_free',
109
+ planVersionId: 'collab-v1'
110
+ })).toEqual(expect.objectContaining({
111
+ valid: false,
112
+ code: 'ONBOARDING_PLAN_MUST_BE_FREE'
113
+ }));
114
+ }
115
+ finally {
116
+ core.close();
117
+ fs.rmSync(projectRoot, { recursive: true, force: true });
118
+ }
119
+ });
120
+ it('passes validation for require_plan_selection without a plan version id', () => {
121
+ const projectRoot = makeProjectRoot();
122
+ const core = openCore(projectRoot);
123
+ try {
124
+ expect(core.validateOnboardingPolicy({
125
+ mode: 'require_plan_selection',
126
+ planVersionId: null
106
127
  })).toEqual({ valid: true });
107
128
  }
108
129
  finally {
@@ -110,19 +131,21 @@ describe('TaskforceCore signup monetization settings', () => {
110
131
  fs.rmSync(projectRoot, { recursive: true, force: true });
111
132
  }
112
133
  });
113
- it('does not backfill canonical entitlements for pending plan selection accounts', () => {
134
+ it('allows backfill transitions to create pending plan selection entitlements', () => {
114
135
  const projectRoot = makeProjectRoot();
115
136
  const tenantId = path.basename(projectRoot);
116
- const initial = openCore(projectRoot);
137
+ const core = openCore(projectRoot);
117
138
  try {
118
139
  const now = new Date().toISOString();
119
- initial.updateGlobalSettings({
120
- auth: {
121
- signupMonetizationStrategy: 'plan_selection_required',
122
- defaultSignupPlanVersionId: null
140
+ core.updateGlobalSettings({
141
+ billing: {
142
+ onboardingPolicy: {
143
+ mode: 'require_plan_selection',
144
+ planVersionId: null
145
+ }
123
146
  }
124
147
  });
125
- const db = initial.getDatabaseAdapter();
148
+ const db = core.getDatabaseAdapter();
126
149
  db.prepare(`
127
150
  INSERT INTO users (id, tenant_id, email, display_name, auth_provider, external_auth_id, is_disabled, created_at, updated_at)
128
151
  VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?)
@@ -131,28 +154,129 @@ describe('TaskforceCore signup monetization settings', () => {
131
154
  INSERT INTO user_billing (user_id, tenant_id, entitlement_state, created_at, updated_at)
132
155
  VALUES (?, ?, ?, ?, ?)
133
156
  `).run('pending-u1', tenantId, 'pending_plan_selection', now, now);
157
+ core.transitionCanonicalCommercialState({
158
+ accountId: 'pending-u1',
159
+ planVersionId: 'starter-v1',
160
+ state: 'pending_plan_selection',
161
+ source: 'backfill',
162
+ updatedBy: 'backfill',
163
+ billingMirrorPlanId: null,
164
+ effectiveAt: now,
165
+ createdAt: now,
166
+ updatedAt: now
167
+ });
168
+ expect(core.hasAccountEntitlement('pending-u1')).toBe(true);
169
+ expect(core.getAccountAccessStatus('pending-u1')).toEqual(expect.objectContaining({
170
+ gate: 'plan_selection_required',
171
+ canAccessApp: false,
172
+ canAccessPlans: true,
173
+ planSelectionRequired: true
174
+ }));
175
+ }
176
+ finally {
177
+ core.close();
178
+ fs.rmSync(projectRoot, { recursive: true, force: true });
179
+ }
180
+ });
181
+ it('treats a live checkout_pending entitlement as checkout recovery state', () => {
182
+ const projectRoot = makeProjectRoot();
183
+ const tenantId = path.basename(projectRoot);
184
+ const core = openCore(projectRoot);
185
+ try {
186
+ const now = new Date().toISOString();
187
+ const checkoutExpiresAt = new Date(Date.now() + (60 * 60 * 1000)).toISOString();
188
+ core.upsertPlanCatalog({ planId: 'pro', displayName: 'Pro', status: 'active' });
189
+ const db = core.getDatabaseAdapter();
190
+ db.prepare(`
191
+ INSERT INTO users (id, tenant_id, email, display_name, auth_provider, external_auth_id, is_disabled, created_at, updated_at)
192
+ VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?)
193
+ `).run('checkout-u1', tenantId, 'checkout@example.com', 'Checkout', 'token', 'checkout-u1', now, now);
194
+ core.transitionCanonicalCommercialState({
195
+ accountId: 'checkout-u1',
196
+ planVersionId: 'pro-v1',
197
+ state: 'checkout_pending',
198
+ source: 'checkout-start',
199
+ updatedBy: 'checkout-start',
200
+ billingMirrorPlanId: 'pro',
201
+ effectiveAt: now,
202
+ effectiveUntil: checkoutExpiresAt,
203
+ createdAt: now,
204
+ updatedAt: now
205
+ });
206
+ expect(core.getAccountAccessStatus('checkout-u1')).toEqual(expect.objectContaining({
207
+ gate: 'checkout_pending',
208
+ canAccessApp: false,
209
+ canAccessPlans: true,
210
+ planSelectionRequired: false,
211
+ message: 'Complete checkout to continue.'
212
+ }));
134
213
  }
135
214
  finally {
136
- initial.close();
215
+ core.close();
216
+ fs.rmSync(projectRoot, { recursive: true, force: true });
137
217
  }
138
- const reopened = openCore(projectRoot);
218
+ });
219
+ it('treats an expired checkout_pending entitlement as requiring plan reselection', () => {
220
+ const projectRoot = makeProjectRoot();
221
+ const tenantId = path.basename(projectRoot);
222
+ const core = openCore(projectRoot);
139
223
  try {
140
- reopened.updateGlobalSettings({
141
- auth: {
142
- signupMonetizationStrategy: 'plan_selection_required',
143
- defaultSignupPlanVersionId: null
144
- }
224
+ const now = new Date().toISOString();
225
+ const expiredAt = new Date(Date.now() - (60 * 1000)).toISOString();
226
+ core.upsertPlanCatalog({ planId: 'pro', displayName: 'Pro', status: 'active' });
227
+ const db = core.getDatabaseAdapter();
228
+ db.prepare(`
229
+ INSERT INTO users (id, tenant_id, email, display_name, auth_provider, external_auth_id, is_disabled, created_at, updated_at)
230
+ VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?)
231
+ `).run('expired-checkout-u1', tenantId, 'expired@example.com', 'Expired Checkout', 'token', 'expired-checkout-u1', now, now);
232
+ core.transitionCanonicalCommercialState({
233
+ accountId: 'expired-checkout-u1',
234
+ planVersionId: 'pro-v1',
235
+ state: 'checkout_pending',
236
+ source: 'checkout-start',
237
+ updatedBy: 'checkout-start',
238
+ billingMirrorPlanId: 'pro',
239
+ effectiveAt: now,
240
+ effectiveUntil: expiredAt,
241
+ createdAt: now,
242
+ updatedAt: now
145
243
  });
146
- expect(reopened.hasAccountEntitlement('pending-u1')).toBe(false);
147
- expect(reopened.getAccountAccessStatus('pending-u1')).toEqual(expect.objectContaining({
244
+ expect(core.getAccountAccessStatus('expired-checkout-u1')).toEqual(expect.objectContaining({
148
245
  gate: 'plan_selection_required',
149
246
  canAccessApp: false,
150
247
  canAccessPlans: true,
151
- planSelectionRequired: true
248
+ planSelectionRequired: true,
249
+ message: 'Choose a plan to continue.'
250
+ }));
251
+ expect(core.getAccountEntitlement('expired-checkout-u1')).toEqual(expect.objectContaining({
252
+ state: 'pending_plan_selection',
253
+ effectiveUntil: null
254
+ }));
255
+ const entitlementRow = db.prepare(`
256
+ SELECT entitlement_state, effective_until, updated_by
257
+ FROM account_entitlements
258
+ WHERE tenant_id = ? AND account_id = ?
259
+ LIMIT 1
260
+ `).get(tenantId, 'expired-checkout-u1');
261
+ expect(entitlementRow).toEqual(expect.objectContaining({
262
+ entitlement_state: 'pending_plan_selection',
263
+ effective_until: null,
264
+ updated_by: 'checkout-expired'
265
+ }));
266
+ const billingRow = db.prepare(`
267
+ SELECT entitlement_state, tier, effective_until
268
+ FROM user_billing
269
+ WHERE tenant_id = ? AND user_id = ?
270
+ LIMIT 1
271
+ `).get(tenantId, 'expired-checkout-u1');
272
+ expect(billingRow).toEqual(expect.objectContaining({
273
+ entitlement_state: 'pending_plan_selection',
274
+ tier: '',
275
+ effective_until: null
152
276
  }));
153
277
  }
154
278
  finally {
155
- reopened.close();
279
+ core.close();
156
280
  fs.rmSync(projectRoot, { recursive: true, force: true });
157
281
  }
158
282
  });