@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.
- package/dist/Taskforce.module.css +11 -0
- package/dist/TaskforceCore.js +59 -9
- package/dist/TaskforceCore.test.js +717 -14
- package/dist/components/task/TaskKanban.test.js +17 -0
- package/dist/components/views/PlansPage.d.ts +3 -1
- package/dist/components/views/PlansPage.js +110 -51
- package/dist/components/views/PlansPage.test.d.ts +1 -0
- package/dist/components/views/PlansPage.test.js +100 -0
- package/dist/components/views/StandaloneLayout.d.ts +1 -0
- package/dist/components/views/StandaloneLayout.js +106 -151
- package/dist/components/views/standalone/modals/AccountHubModal.d.ts +8 -3
- package/dist/components/views/standalone/modals/AccountHubModal.js +16 -2
- package/dist/components/views/standalone/modals/AccountHubModal.test.d.ts +1 -0
- package/dist/components/views/standalone/modals/AccountHubModal.test.js +26 -0
- package/dist/core/EntitlementsPolicy.test.js +2 -2
- package/dist/core/GlobalSettingsService.js +78 -18
- package/dist/core/PlanEntitlementService.d.ts +29 -5
- package/dist/core/PlanEntitlementService.js +297 -219
- package/dist/core/SignupMonetizationSettings.test.js +173 -49
- package/dist/core/SystemAdmin.test.js +186 -96
- package/dist/core/Taskforce.d.ts +51 -10
- package/dist/core/Taskforce.js +218 -49
- package/dist/core/Taskforce.mcpAuth.test.js +0 -2
- package/dist/core/UserProfileAvatarDrafts.test.js +12 -3
- package/dist/core/types.d.ts +11 -12
- package/dist/hooks/useSyncOrchestrator.js +17 -3
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +97 -5
- package/dist/hooks/useTaskforce.d.ts +3 -0
- package/dist/hooks/useTaskforce.js +9 -4
- package/dist/mcp/runtime.js +90 -29
- package/dist/mcp/runtime.test.js +272 -81
- package/dist/migrations/billingSchemaParity.test.js +3 -0
- package/dist/migrations/taskSchemaMigrations.js +15 -0
- package/dist/server/index.cookieProxy.test.js +1 -0
- package/dist/server/index.js +2 -0
- package/dist/server/index.test.js +2 -1
- package/dist/server/mockStripe.d.ts +1 -0
- package/dist/server/mockStripe.js +2 -0
- package/dist/server/routes/admin.js +80 -23
- package/dist/server/routes/auth.d.ts +1 -0
- package/dist/server/routes/auth.js +194 -53
- package/dist/server/routes/billing.js +506 -23
- package/dist/server/routes/billing.test.js +937 -45
- package/dist/server/routes/shared.js +5 -3
- package/dist/server/routes/sync.integration.test.js +160 -0
- package/dist/server/routes/sync.js +15 -5
- package/dist/server/routes.js +3 -3
- package/dist/server/routes.test.js +287 -76
- package/dist/sync/cloudSyncApi.d.ts +1 -0
- package/dist/sync/contentSyncState.d.ts +10 -0
- package/dist/sync/contentSyncState.js +277 -16
- package/dist/sync/syncService.d.ts +1 -0
- package/dist/sync/syncService.js +21 -8
- package/dist/sync/workspacePullFeed.d.ts +10 -0
- package/dist/sync/workspacePullFeed.js +111 -63
- package/dist/sync/workspacePullFeed.test.js +112 -0
- package/dist/ui/assets/{AgentsModule-JG5jc3he.js → AgentsModule-CpsTmrIW.js} +1 -1
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-Dvi02Y7q.js → AnnotatedAttachmentWorkspace-C_TmXZwA.js} +1 -1
- package/dist/ui/assets/{DocumentWorkspace-iys3zfca.js → DocumentWorkspace-C3GyzrWm.js} +1 -1
- package/dist/ui/assets/{InitiativesModule-DJvLJI3A.js → InitiativesModule-4-Rh2K2n.js} +1 -1
- package/dist/ui/assets/PlansPage-BP7AYOqr.js +1 -0
- package/dist/ui/assets/{TaskSettings-Bqd4cCzW.js → TaskSettings-BOC5F6Ag.js} +1 -1
- package/dist/ui/assets/{WorkflowsModule-BguHeQaV.js → WorkflowsModule-CGk9s3NJ.js} +1 -1
- package/dist/ui/assets/index-CLIMgR6g.js +6 -0
- package/dist/ui/assets/index-DneETxcu.css +1 -0
- package/dist/ui/assets/{vendor-icons-C8EED3Oh.js → vendor-icons-pWocEipT.js} +1 -1
- package/dist/ui/index.html +3 -3
- package/dist/utils/accountProfileSummaryCache.d.ts +31 -0
- package/dist/utils/accountProfileSummaryCache.js +97 -0
- package/dist/utils/accountProfileSummaryCache.test.d.ts +1 -0
- package/dist/utils/accountProfileSummaryCache.test.js +63 -0
- package/package.json +3 -3
- package/dist/ui/assets/PlansPage-BMPlpFpw.js +0 -1
- package/dist/ui/assets/index-MXeWoebC.css +0 -1
- package/dist/ui/assets/index-YJ4k5QNa.js +0 -6
- package/dist/utils/billingStatusCache.d.ts +0 -21
- package/dist/utils/billingStatusCache.js +0 -70
package/dist/core/Taskforce.js
CHANGED
|
@@ -1175,7 +1175,6 @@ export class TaskforceCore {
|
|
|
1175
1175
|
writeGlobalConfig: (config) => this.writeGlobalConfig(config),
|
|
1176
1176
|
});
|
|
1177
1177
|
this.planEntitlementService.repairPlanCatalogDefaultsIfNeeded();
|
|
1178
|
-
this.planEntitlementService.repairSignupMonetizationSettingsIfNeeded();
|
|
1179
1178
|
this.planEntitlementService.normalizeLegacyPlanDataIfNeeded();
|
|
1180
1179
|
this.planEntitlementService.backfillAccountEntitlements();
|
|
1181
1180
|
this.repairDetachedAnnotatedAttachmentSessions();
|
|
@@ -1209,11 +1208,14 @@ export class TaskforceCore {
|
|
|
1209
1208
|
upsertCanonicalEntitlementRecord(input) {
|
|
1210
1209
|
return this.planEntitlementService.upsertCanonicalEntitlementRecord(input);
|
|
1211
1210
|
}
|
|
1211
|
+
transitionCanonicalCommercialState(input) {
|
|
1212
|
+
return this.planEntitlementService.transitionCanonicalCommercialState(input);
|
|
1213
|
+
}
|
|
1212
1214
|
resolveWorkspaceLimitForUser(userId) {
|
|
1213
1215
|
return this.planEntitlementService.resolveWorkspaceLimitForUser(userId);
|
|
1214
1216
|
}
|
|
1215
|
-
|
|
1216
|
-
return this.planEntitlementService.
|
|
1217
|
+
resolveRegistrationOnboardingOutcome(selectedPlanVersionIdRaw) {
|
|
1218
|
+
return this.planEntitlementService.resolveRegistrationOnboardingOutcome(selectedPlanVersionIdRaw);
|
|
1217
1219
|
}
|
|
1218
1220
|
createUserBillingRecord(userId, options) {
|
|
1219
1221
|
return this.planEntitlementService.createUserBillingRecord(userId, options);
|
|
@@ -2598,11 +2600,11 @@ export class TaskforceCore {
|
|
|
2598
2600
|
const config = this.readConfig(workspaceId, userId) || {};
|
|
2599
2601
|
return typeof config.jsonBackupEnabled === 'boolean';
|
|
2600
2602
|
}
|
|
2601
|
-
|
|
2602
|
-
return this.planEntitlementService.
|
|
2603
|
+
getOnboardingPolicy() {
|
|
2604
|
+
return this.planEntitlementService.getOnboardingPolicy();
|
|
2603
2605
|
}
|
|
2604
|
-
|
|
2605
|
-
return this.planEntitlementService.
|
|
2606
|
+
validateOnboardingPolicy(input) {
|
|
2607
|
+
return this.planEntitlementService.validateOnboardingPolicy(input);
|
|
2606
2608
|
}
|
|
2607
2609
|
updateGlobalSettings(settings) {
|
|
2608
2610
|
updateGlobalSettingsService(this.getGlobalSettingsServiceDeps(), settings);
|
|
@@ -4375,6 +4377,193 @@ export class TaskforceCore {
|
|
|
4375
4377
|
`).run(enabled ? 1 : 0, new Date().toISOString(), this.tenantId, normalizedUserId);
|
|
4376
4378
|
return Number(result?.changes || 0) > 0;
|
|
4377
4379
|
}
|
|
4380
|
+
purgeSystemUser(input) {
|
|
4381
|
+
const targetUserId = String(input.targetUserId || '').trim();
|
|
4382
|
+
const actorUserId = String(input.actorUserId || '').trim();
|
|
4383
|
+
const emailConfirmation = String(input.emailConfirmation || '').trim().toLowerCase();
|
|
4384
|
+
if (!targetUserId) {
|
|
4385
|
+
throw new TaskforceRuleError('targetUserId is required', 400, 'TARGET_USER_ID_REQUIRED');
|
|
4386
|
+
}
|
|
4387
|
+
if (!actorUserId || actorUserId === 'anonymous') {
|
|
4388
|
+
throw new TaskforceRuleError('actorUserId is required', 400, 'ACTOR_USER_ID_REQUIRED');
|
|
4389
|
+
}
|
|
4390
|
+
if (!emailConfirmation) {
|
|
4391
|
+
throw new TaskforceRuleError('Email confirmation is required', 400, 'EMAIL_CONFIRMATION_REQUIRED');
|
|
4392
|
+
}
|
|
4393
|
+
if (targetUserId === actorUserId) {
|
|
4394
|
+
throw new TaskforceRuleError('Cannot purge the current system admin session user', 400, 'CURRENT_SYSTEM_USER_PURGE_FORBIDDEN');
|
|
4395
|
+
}
|
|
4396
|
+
return this.db.transaction(() => {
|
|
4397
|
+
const user = this.db.prepare(`
|
|
4398
|
+
SELECT
|
|
4399
|
+
id,
|
|
4400
|
+
email,
|
|
4401
|
+
COALESCE(system_role, 'user') AS system_role,
|
|
4402
|
+
COALESCE(is_disabled, 0) AS is_disabled
|
|
4403
|
+
FROM users
|
|
4404
|
+
WHERE tenant_id = ? AND id = ?
|
|
4405
|
+
LIMIT 1
|
|
4406
|
+
`).get(this.tenantId, targetUserId);
|
|
4407
|
+
if (!user?.id || !user.email) {
|
|
4408
|
+
throw new TaskforceRuleError('User not found', 404, 'USER_NOT_FOUND');
|
|
4409
|
+
}
|
|
4410
|
+
const normalizedEmail = String(user.email).trim().toLowerCase();
|
|
4411
|
+
if (normalizedEmail !== emailConfirmation) {
|
|
4412
|
+
throw new TaskforceRuleError('Email confirmation did not match the target user', 400, 'EMAIL_CONFIRMATION_MISMATCH');
|
|
4413
|
+
}
|
|
4414
|
+
const currentRole = this.normalizeSystemRole(user.system_role, 'user');
|
|
4415
|
+
const isDisabled = Boolean(user.is_disabled);
|
|
4416
|
+
if (currentRole === 'system_admin' && !isDisabled && this.countSystemAdmins() <= 1) {
|
|
4417
|
+
throw new TaskforceRuleError('Cannot purge the last enabled system admin', 400, 'LAST_SYSTEM_ADMIN_REQUIRED');
|
|
4418
|
+
}
|
|
4419
|
+
const elevatedWorkspaceMemberships = this.db.prepare(`
|
|
4420
|
+
SELECT
|
|
4421
|
+
m.workspace_id AS workspace_id,
|
|
4422
|
+
COALESCE(m.role, 'member') AS role,
|
|
4423
|
+
w.name AS workspace_name
|
|
4424
|
+
FROM workspace_memberships m
|
|
4425
|
+
LEFT JOIN workspaces w
|
|
4426
|
+
ON w.tenant_id = m.tenant_id
|
|
4427
|
+
AND w.id = m.workspace_id
|
|
4428
|
+
WHERE m.tenant_id = ?
|
|
4429
|
+
AND m.user_id = ?
|
|
4430
|
+
AND COALESCE(m.status, 'active') = 'active'
|
|
4431
|
+
AND COALESCE(m.role, 'member') IN ('owner', 'admin')
|
|
4432
|
+
ORDER BY COALESCE(w.updated_at, w.created_at) DESC, w.name ASC
|
|
4433
|
+
LIMIT 10
|
|
4434
|
+
`).all(this.tenantId, targetUserId);
|
|
4435
|
+
if (elevatedWorkspaceMemberships.length > 0) {
|
|
4436
|
+
throw new TaskforceRuleError('Cannot purge a user with active owner/admin workspace memberships', 409, 'SYSTEM_USER_PURGE_BLOCKED_ACTIVE_WORKSPACE_ROLE', {
|
|
4437
|
+
memberships: elevatedWorkspaceMemberships.map((membership) => ({
|
|
4438
|
+
workspaceId: membership.workspace_id ? String(membership.workspace_id) : null,
|
|
4439
|
+
workspaceName: membership.workspace_name ? String(membership.workspace_name) : null,
|
|
4440
|
+
role: membership.role ? String(membership.role) : null
|
|
4441
|
+
}))
|
|
4442
|
+
});
|
|
4443
|
+
}
|
|
4444
|
+
const billingRow = this.db.prepare(`
|
|
4445
|
+
SELECT
|
|
4446
|
+
stripe_customer_id,
|
|
4447
|
+
stripe_subscription_id,
|
|
4448
|
+
stripe_price_id,
|
|
4449
|
+
stripe_status,
|
|
4450
|
+
billing_interval
|
|
4451
|
+
FROM user_billing
|
|
4452
|
+
WHERE tenant_id = ? AND user_id = ?
|
|
4453
|
+
LIMIT 1
|
|
4454
|
+
`).get(this.tenantId, targetUserId);
|
|
4455
|
+
const hasCommercialBillingState = [
|
|
4456
|
+
billingRow?.stripe_customer_id,
|
|
4457
|
+
billingRow?.stripe_subscription_id,
|
|
4458
|
+
billingRow?.stripe_price_id,
|
|
4459
|
+
billingRow?.stripe_status,
|
|
4460
|
+
billingRow?.billing_interval
|
|
4461
|
+
].some((value) => String(value || '').trim().length > 0);
|
|
4462
|
+
if (hasCommercialBillingState) {
|
|
4463
|
+
throw new TaskforceRuleError('Cannot purge a user with active commercial billing state', 409, 'SYSTEM_USER_PURGE_BLOCKED_BILLING_STATE');
|
|
4464
|
+
}
|
|
4465
|
+
const now = new Date().toISOString();
|
|
4466
|
+
const runCount = (sql, ...params) => Number(this.db.prepare(sql).run(...params)?.changes || 0);
|
|
4467
|
+
const unassignedTaskCount = runCount(`
|
|
4468
|
+
UPDATE tasks
|
|
4469
|
+
SET assignee = 'unassigned',
|
|
4470
|
+
updated_at = ?
|
|
4471
|
+
WHERE tenant_id = ?
|
|
4472
|
+
AND assignee = ?
|
|
4473
|
+
AND is_archived = 0
|
|
4474
|
+
`, now, this.tenantId, targetUserId);
|
|
4475
|
+
const clearedInitiativeOwnerCount = runCount(`
|
|
4476
|
+
UPDATE initiatives
|
|
4477
|
+
SET owner_id = NULL,
|
|
4478
|
+
updated_at = ?
|
|
4479
|
+
WHERE tenant_id = ?
|
|
4480
|
+
AND owner_id = ?
|
|
4481
|
+
`, now, this.tenantId, targetUserId);
|
|
4482
|
+
const clearedWorkstreamOwnerCount = runCount(`
|
|
4483
|
+
UPDATE workstreams
|
|
4484
|
+
SET owner_id = NULL,
|
|
4485
|
+
updated_at = ?
|
|
4486
|
+
WHERE tenant_id = ?
|
|
4487
|
+
AND owner_id = ?
|
|
4488
|
+
`, now, this.tenantId, targetUserId);
|
|
4489
|
+
const deletedEntitlementCount = runCount(`
|
|
4490
|
+
DELETE FROM account_entitlements
|
|
4491
|
+
WHERE tenant_id = ?
|
|
4492
|
+
AND account_id = ?
|
|
4493
|
+
`, this.tenantId, targetUserId);
|
|
4494
|
+
const deletedWorkspaceMembershipCount = runCount(`
|
|
4495
|
+
DELETE FROM workspace_memberships
|
|
4496
|
+
WHERE tenant_id = ?
|
|
4497
|
+
AND user_id = ?
|
|
4498
|
+
`, this.tenantId, targetUserId);
|
|
4499
|
+
const deletedAuthTokenCount = runCount(`
|
|
4500
|
+
DELETE FROM auth_tokens
|
|
4501
|
+
WHERE tenant_id = ?
|
|
4502
|
+
AND user_id = ?
|
|
4503
|
+
`, this.tenantId, targetUserId);
|
|
4504
|
+
const deletedAvatarDraftCount = runCount(`
|
|
4505
|
+
DELETE FROM user_avatar_upload_drafts
|
|
4506
|
+
WHERE tenant_id = ?
|
|
4507
|
+
AND user_id = ?
|
|
4508
|
+
`, this.tenantId, targetUserId);
|
|
4509
|
+
const deletedMcpAccessTokenCount = runCount(`
|
|
4510
|
+
DELETE FROM mcp_access_tokens
|
|
4511
|
+
WHERE tenant_id = ?
|
|
4512
|
+
AND user_id = ?
|
|
4513
|
+
`, this.tenantId, targetUserId);
|
|
4514
|
+
const deletedMcpOauthAuthCodeCount = runCount(`
|
|
4515
|
+
DELETE FROM mcp_oauth_auth_codes
|
|
4516
|
+
WHERE tenant_id = ?
|
|
4517
|
+
AND user_id = ?
|
|
4518
|
+
`, this.tenantId, targetUserId);
|
|
4519
|
+
const deletedMcpOauthConnectorGrantCount = runCount(`
|
|
4520
|
+
DELETE FROM mcp_oauth_connector_grants
|
|
4521
|
+
WHERE tenant_id = ?
|
|
4522
|
+
AND user_id = ?
|
|
4523
|
+
`, this.tenantId, targetUserId);
|
|
4524
|
+
const deletedUiStateCount = runCount(`
|
|
4525
|
+
DELETE FROM ui_state
|
|
4526
|
+
WHERE tenant_id = ?
|
|
4527
|
+
AND user_id = ?
|
|
4528
|
+
`, this.tenantId, targetUserId);
|
|
4529
|
+
const deletedUserPreferenceCount = runCount(`
|
|
4530
|
+
DELETE FROM user_preferences
|
|
4531
|
+
WHERE tenant_id = ?
|
|
4532
|
+
AND user_id = ?
|
|
4533
|
+
`, this.tenantId, targetUserId);
|
|
4534
|
+
const deletedUserBillingCount = runCount(`
|
|
4535
|
+
DELETE FROM user_billing
|
|
4536
|
+
WHERE tenant_id = ?
|
|
4537
|
+
AND user_id = ?
|
|
4538
|
+
`, this.tenantId, targetUserId);
|
|
4539
|
+
const deletedUserCount = runCount(`
|
|
4540
|
+
DELETE FROM users
|
|
4541
|
+
WHERE tenant_id = ?
|
|
4542
|
+
AND id = ?
|
|
4543
|
+
`, this.tenantId, targetUserId);
|
|
4544
|
+
if (deletedUserCount !== 1) {
|
|
4545
|
+
throw new TaskforceRuleError('User not found', 404, 'USER_NOT_FOUND');
|
|
4546
|
+
}
|
|
4547
|
+
return {
|
|
4548
|
+
targetUserId,
|
|
4549
|
+
email: String(user.email),
|
|
4550
|
+
unassignedTaskCount,
|
|
4551
|
+
clearedInitiativeOwnerCount,
|
|
4552
|
+
clearedWorkstreamOwnerCount,
|
|
4553
|
+
deletedWorkspaceMembershipCount,
|
|
4554
|
+
deletedAuthTokenCount,
|
|
4555
|
+
deletedAvatarDraftCount,
|
|
4556
|
+
deletedMcpAccessTokenCount,
|
|
4557
|
+
deletedMcpOauthAuthCodeCount,
|
|
4558
|
+
deletedMcpOauthConnectorGrantCount,
|
|
4559
|
+
deletedUiStateCount,
|
|
4560
|
+
deletedUserPreferenceCount,
|
|
4561
|
+
deletedEntitlementCount,
|
|
4562
|
+
deletedUserBillingCount,
|
|
4563
|
+
deletedUserCount
|
|
4564
|
+
};
|
|
4565
|
+
})();
|
|
4566
|
+
}
|
|
4378
4567
|
clearActiveAssignmentsForUnavailableActor(actorId, workspaceId, updatedAt) {
|
|
4379
4568
|
const normalizedActorId = String(actorId || '').trim();
|
|
4380
4569
|
const normalizedWorkspaceId = String(workspaceId || 'default').trim() || 'default';
|
|
@@ -5329,7 +5518,20 @@ export class TaskforceCore {
|
|
|
5329
5518
|
const assignFirstSystemAdmin = !this.hasAnySystemAdmin();
|
|
5330
5519
|
const betaAccess = input.betaAccess !== false;
|
|
5331
5520
|
const requestedWorkspaceId = String(input.workspaceId || '').trim();
|
|
5332
|
-
const
|
|
5521
|
+
const onboardingOutcome = this.resolveRegistrationOnboardingOutcome(input.planVersionId);
|
|
5522
|
+
const applyRegistrationCommercialState = (accountId, now) => {
|
|
5523
|
+
this.transitionCanonicalCommercialState({
|
|
5524
|
+
accountId,
|
|
5525
|
+
planVersionId: onboardingOutcome.planVersionId,
|
|
5526
|
+
state: onboardingOutcome.commercialState,
|
|
5527
|
+
effectiveAt: now,
|
|
5528
|
+
billingMirrorPlanId: onboardingOutcome.planId,
|
|
5529
|
+
source: 'registration',
|
|
5530
|
+
updatedBy: 'registration',
|
|
5531
|
+
createdAt: now,
|
|
5532
|
+
updatedAt: now
|
|
5533
|
+
});
|
|
5534
|
+
};
|
|
5333
5535
|
if (!requestedWorkspaceId || requestedWorkspaceId.toLowerCase() === 'default') {
|
|
5334
5536
|
const now = new Date().toISOString();
|
|
5335
5537
|
const userId = this.generateEntityId('user');
|
|
@@ -5338,32 +5540,14 @@ export class TaskforceCore {
|
|
|
5338
5540
|
INSERT INTO users (id, tenant_id, email, display_name, auth_provider, external_auth_id, password_hash, system_role, beta_access, is_disabled, created_at, updated_at)
|
|
5339
5541
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?)
|
|
5340
5542
|
`).run(userId, this.tenantId, email, displayName, 'password', email, hashPassword(password), assignFirstSystemAdmin ? 'system_admin' : 'user', betaAccess ? 1 : 0, now, now);
|
|
5341
|
-
|
|
5342
|
-
this.createUserBillingRecord(userId, {
|
|
5343
|
-
entitlementState: 'pending_plan_selection',
|
|
5344
|
-
planId: null
|
|
5345
|
-
});
|
|
5346
|
-
}
|
|
5347
|
-
if (!registrationPlanAssignment.requiresPlanSelection && registrationPlanAssignment.planVersionId && registrationPlanAssignment.planId) {
|
|
5348
|
-
this.createUserBillingRecord(userId, {
|
|
5349
|
-
entitlementState: 'active',
|
|
5350
|
-
planId: registrationPlanAssignment.planId
|
|
5351
|
-
});
|
|
5352
|
-
this.upsertCanonicalEntitlementRecord({
|
|
5353
|
-
accountId: userId,
|
|
5354
|
-
planVersionId: registrationPlanAssignment.planVersionId,
|
|
5355
|
-
state: 'active',
|
|
5356
|
-
effectiveAt: now,
|
|
5357
|
-
updatedBy: 'registration',
|
|
5358
|
-
createdAt: now,
|
|
5359
|
-
updatedAt: now
|
|
5360
|
-
});
|
|
5361
|
-
}
|
|
5543
|
+
applyRegistrationCommercialState(userId, now);
|
|
5362
5544
|
return {
|
|
5363
5545
|
userId,
|
|
5364
5546
|
workspaceId: null,
|
|
5365
5547
|
role: 'member',
|
|
5366
|
-
planSelectionRequired:
|
|
5548
|
+
planSelectionRequired: onboardingOutcome.requiresPlanSelection,
|
|
5549
|
+
checkoutPending: onboardingOutcome.checkoutPending,
|
|
5550
|
+
commercialState: onboardingOutcome.commercialState
|
|
5367
5551
|
};
|
|
5368
5552
|
}
|
|
5369
5553
|
const workspaceId = requestedWorkspaceId;
|
|
@@ -5381,27 +5565,12 @@ export class TaskforceCore {
|
|
|
5381
5565
|
WHERE tenant_id = ? AND id = ?
|
|
5382
5566
|
`).run(new Date().toISOString(), this.tenantId, created.userId);
|
|
5383
5567
|
}
|
|
5384
|
-
|
|
5385
|
-
this.createUserBillingRecord(created.userId, {
|
|
5386
|
-
entitlementState: 'pending_plan_selection',
|
|
5387
|
-
planId: null
|
|
5388
|
-
});
|
|
5389
|
-
}
|
|
5390
|
-
if (!registrationPlanAssignment.requiresPlanSelection && registrationPlanAssignment.planVersionId && registrationPlanAssignment.planId) {
|
|
5391
|
-
this.createUserBillingRecord(created.userId, {
|
|
5392
|
-
entitlementState: 'active',
|
|
5393
|
-
planId: registrationPlanAssignment.planId
|
|
5394
|
-
});
|
|
5395
|
-
this.upsertCanonicalEntitlementRecord({
|
|
5396
|
-
accountId: created.userId,
|
|
5397
|
-
planVersionId: registrationPlanAssignment.planVersionId,
|
|
5398
|
-
state: 'active',
|
|
5399
|
-
updatedBy: 'registration'
|
|
5400
|
-
});
|
|
5401
|
-
}
|
|
5568
|
+
applyRegistrationCommercialState(created.userId);
|
|
5402
5569
|
return {
|
|
5403
5570
|
...created,
|
|
5404
|
-
planSelectionRequired:
|
|
5571
|
+
planSelectionRequired: onboardingOutcome.requiresPlanSelection,
|
|
5572
|
+
checkoutPending: onboardingOutcome.checkoutPending,
|
|
5573
|
+
commercialState: onboardingOutcome.commercialState
|
|
5405
5574
|
};
|
|
5406
5575
|
}
|
|
5407
5576
|
createOneTimeAuthToken(userId, purpose, ttlSeconds, workspaceId) {
|
|
@@ -27,7 +27,6 @@ function setupWorkspace(core, userId = 'owner-1', workspaceId = 'workspace-1') {
|
|
|
27
27
|
hasEntitlement: true,
|
|
28
28
|
canAccessApp: true,
|
|
29
29
|
canAccessPlans: true,
|
|
30
|
-
signupMonetizationStrategy: 'none',
|
|
31
30
|
planSelectionRequired: false,
|
|
32
31
|
message: null,
|
|
33
32
|
entitlement: null
|
|
@@ -328,7 +327,6 @@ describe('TaskforceCore MCP auth invalidation', () => {
|
|
|
328
327
|
hasEntitlement: false,
|
|
329
328
|
canAccessApp: false,
|
|
330
329
|
canAccessPlans: true,
|
|
331
|
-
signupMonetizationStrategy: 'none',
|
|
332
330
|
planSelectionRequired: false,
|
|
333
331
|
message: 'Suspended for test',
|
|
334
332
|
entitlement: null
|
|
@@ -13,10 +13,19 @@ function seedPlans(core) {
|
|
|
13
13
|
versionNumber: 1,
|
|
14
14
|
seatLimit: 1
|
|
15
15
|
});
|
|
16
|
+
const db = core.db;
|
|
17
|
+
const now = new Date().toISOString();
|
|
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(core.getTenantId(), 'starter-v1', 'month', 'free:starter-v1:month', 'free', 1, 0, null, now, now);
|
|
16
23
|
core.updateGlobalSettings({
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
24
|
+
billing: {
|
|
25
|
+
onboardingPolicy: {
|
|
26
|
+
mode: 'auto_grant_free',
|
|
27
|
+
planVersionId: 'starter-v1'
|
|
28
|
+
}
|
|
20
29
|
}
|
|
21
30
|
});
|
|
22
31
|
}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -268,8 +268,9 @@ export interface GlobalTaskforceSettings {
|
|
|
268
268
|
authRateLimitEnabled?: boolean;
|
|
269
269
|
authRateLimitMaxRequests?: number;
|
|
270
270
|
authRateLimitWindowMs?: number;
|
|
271
|
-
|
|
272
|
-
|
|
271
|
+
};
|
|
272
|
+
billing?: {
|
|
273
|
+
onboardingPolicy?: OnboardingPolicy;
|
|
273
274
|
};
|
|
274
275
|
site?: {
|
|
275
276
|
showRunTaskforceLocally?: boolean;
|
|
@@ -299,14 +300,14 @@ export interface UserGlobalSyncSettings {
|
|
|
299
300
|
weekStartsOn?: 'sunday' | 'monday';
|
|
300
301
|
};
|
|
301
302
|
}
|
|
302
|
-
export type
|
|
303
|
-
export interface
|
|
304
|
-
|
|
305
|
-
|
|
303
|
+
export type OnboardingPolicyMode = 'require_plan_selection' | 'auto_grant_free' | 'auto_start_trial';
|
|
304
|
+
export interface OnboardingPolicy {
|
|
305
|
+
mode: OnboardingPolicyMode;
|
|
306
|
+
planVersionId: string | null;
|
|
306
307
|
}
|
|
307
|
-
export interface
|
|
308
|
+
export interface OnboardingPolicyValidationResult {
|
|
308
309
|
valid: boolean;
|
|
309
|
-
code?: '
|
|
310
|
+
code?: 'ONBOARDING_PLAN_VERSION_REQUIRED' | 'ONBOARDING_PLAN_VERSION_NOT_FOUND' | 'ONBOARDING_PLAN_NOT_ENABLED' | 'ONBOARDING_PLAN_MUST_BE_FREE' | 'ONBOARDING_PLAN_TRIAL_REQUIRED';
|
|
310
311
|
message?: string;
|
|
311
312
|
details?: Record<string, unknown>;
|
|
312
313
|
}
|
|
@@ -441,7 +442,7 @@ export interface McpAuditLogRecord {
|
|
|
441
442
|
reasonCode: string | null;
|
|
442
443
|
details: Record<string, unknown>;
|
|
443
444
|
}
|
|
444
|
-
export type EntitlementState = 'active' | 'trialing' | 'grace' | 'suspended' | 'canceled';
|
|
445
|
+
export type EntitlementState = 'pending_plan_selection' | 'checkout_pending' | 'active' | 'trialing' | 'grace' | 'suspended' | 'canceled';
|
|
445
446
|
export type PlanFeatureAccess = 'enabled' | 'disabled' | 'limited';
|
|
446
447
|
export interface PlanFeatureSnapshot {
|
|
447
448
|
featureKey: string;
|
|
@@ -499,17 +500,15 @@ export interface AccountAccessStatus_Ok {
|
|
|
499
500
|
hasEntitlement: true;
|
|
500
501
|
canAccessApp: true;
|
|
501
502
|
canAccessPlans: true;
|
|
502
|
-
signupMonetizationStrategy: SignupMonetizationStrategy;
|
|
503
503
|
planSelectionRequired: false;
|
|
504
504
|
message: null;
|
|
505
505
|
entitlement: AccountEntitlementSnapshot;
|
|
506
506
|
}
|
|
507
507
|
export interface AccountAccessStatus_Blocked {
|
|
508
|
-
gate: 'plan_selection_required' | 'misconfigured' | 'missing_entitlement';
|
|
508
|
+
gate: 'plan_selection_required' | 'checkout_pending' | 'misconfigured' | 'missing_entitlement';
|
|
509
509
|
hasEntitlement: boolean;
|
|
510
510
|
canAccessApp: false;
|
|
511
511
|
canAccessPlans: true;
|
|
512
|
-
signupMonetizationStrategy: SignupMonetizationStrategy;
|
|
513
512
|
planSelectionRequired: boolean;
|
|
514
513
|
message: string;
|
|
515
514
|
entitlement: AccountEntitlementSnapshot | null;
|
|
@@ -1571,7 +1571,12 @@ export function useSyncOrchestrator(input) {
|
|
|
1571
1571
|
status: 'error',
|
|
1572
1572
|
statusCode: pullResult.status,
|
|
1573
1573
|
errorMessage: message,
|
|
1574
|
-
requestMs: pullResult.pullRequestMs
|
|
1574
|
+
requestMs: pullResult.pullRequestMs,
|
|
1575
|
+
details: {
|
|
1576
|
+
pages: pullResult.pages,
|
|
1577
|
+
applyMs: pullResult.applyMs,
|
|
1578
|
+
...(pullResult.serverDiagnostics ? { serverPullDiagnostics: pullResult.serverDiagnostics } : {})
|
|
1579
|
+
}
|
|
1575
1580
|
});
|
|
1576
1581
|
if (pullResult.transient || pullResult.hasTransientApplyFailure || isTransientWorkspaceSyncStatus(pullResult.status)) {
|
|
1577
1582
|
scheduleWorkspaceSyncRetry(pullResult.retryAfterMs);
|
|
@@ -1638,7 +1643,12 @@ export function useSyncOrchestrator(input) {
|
|
|
1638
1643
|
eventType: workspaceSyncPhase === 'attach-cloud' ? 'bootstrap' : 'pull',
|
|
1639
1644
|
status: 'success',
|
|
1640
1645
|
changeCount: pullResult.appliedChanges,
|
|
1641
|
-
requestMs: pullResult.pullRequestMs
|
|
1646
|
+
requestMs: pullResult.pullRequestMs,
|
|
1647
|
+
details: {
|
|
1648
|
+
pages: pullResult.pages,
|
|
1649
|
+
applyMs: pullResult.applyMs,
|
|
1650
|
+
...(pullResult.serverDiagnostics ? { serverPullDiagnostics: pullResult.serverDiagnostics } : {})
|
|
1651
|
+
}
|
|
1642
1652
|
});
|
|
1643
1653
|
if (workspaceSyncPhase === 'attach-cloud') {
|
|
1644
1654
|
await refreshWorkspaceSyncMarkdownDocumentsSnapshot();
|
|
@@ -1727,8 +1737,11 @@ export function useSyncOrchestrator(input) {
|
|
|
1727
1737
|
const eventId = String(signal?.eventId || '').trim();
|
|
1728
1738
|
if (eventId && realtimeSuppressedEventIdsRef.current.delete(eventId))
|
|
1729
1739
|
return;
|
|
1730
|
-
if (
|
|
1740
|
+
if (workspacePullInFlightRef.current)
|
|
1731
1741
|
return;
|
|
1742
|
+
if (isWorkspaceRetryPending()) {
|
|
1743
|
+
clearWorkspaceRetry();
|
|
1744
|
+
}
|
|
1732
1745
|
if (realtimePullDebounceRef.current !== null) {
|
|
1733
1746
|
window.clearTimeout(realtimePullDebounceRef.current);
|
|
1734
1747
|
}
|
|
@@ -1742,6 +1755,7 @@ export function useSyncOrchestrator(input) {
|
|
|
1742
1755
|
workspaceCloudSyncEnabled,
|
|
1743
1756
|
workspaceSyncPhase,
|
|
1744
1757
|
isWorkspaceRetryPending,
|
|
1758
|
+
clearWorkspaceRetry,
|
|
1745
1759
|
workspacePullInFlightRef,
|
|
1746
1760
|
pullWorkspaceChangesFromCloud
|
|
1747
1761
|
]);
|
|
@@ -3,19 +3,44 @@ import { act, renderHook, waitFor } from '@testing-library/react';
|
|
|
3
3
|
import { useSyncOrchestrator } from './useSyncOrchestrator';
|
|
4
4
|
class MockWebSocket {
|
|
5
5
|
static instances = [];
|
|
6
|
+
static OPEN = 1;
|
|
7
|
+
static CLOSED = 3;
|
|
6
8
|
url;
|
|
9
|
+
readyState = 0;
|
|
10
|
+
sent = [];
|
|
11
|
+
listeners = new Map();
|
|
7
12
|
constructor(url) {
|
|
8
13
|
this.url = url;
|
|
9
14
|
MockWebSocket.instances.push(this);
|
|
10
15
|
}
|
|
11
16
|
close() {
|
|
12
|
-
|
|
17
|
+
this.readyState = MockWebSocket.CLOSED;
|
|
18
|
+
this.emit('close', {});
|
|
13
19
|
}
|
|
14
|
-
addEventListener() {
|
|
15
|
-
|
|
20
|
+
addEventListener(type, listener) {
|
|
21
|
+
const bucket = this.listeners.get(type) || new Set();
|
|
22
|
+
bucket.add(listener);
|
|
23
|
+
this.listeners.set(type, bucket);
|
|
16
24
|
}
|
|
17
|
-
send() {
|
|
18
|
-
|
|
25
|
+
send(payload) {
|
|
26
|
+
if (typeof payload === 'string') {
|
|
27
|
+
this.sent.push(payload);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
emit(type, event) {
|
|
31
|
+
const bucket = this.listeners.get(type);
|
|
32
|
+
if (!bucket)
|
|
33
|
+
return;
|
|
34
|
+
for (const listener of bucket) {
|
|
35
|
+
listener(event);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
open() {
|
|
39
|
+
this.readyState = MockWebSocket.OPEN;
|
|
40
|
+
this.emit('open', {});
|
|
41
|
+
}
|
|
42
|
+
message(payload) {
|
|
43
|
+
this.emit('message', { data: JSON.stringify(payload) });
|
|
19
44
|
}
|
|
20
45
|
}
|
|
21
46
|
const { ensureCloudWorkspaceReadyForSyncServiceMock, runWorkspacePullSyncServiceMock, runWorkspacePushSyncServiceMock, syncUserGlobalSettingsServiceMock } = vi.hoisted(() => ({
|
|
@@ -206,6 +231,73 @@ describe('useSyncOrchestrator', () => {
|
|
|
206
231
|
expect(MockWebSocket.instances[0]?.url).toBe('wss://api.example.com/taskforce-ws');
|
|
207
232
|
unmount();
|
|
208
233
|
});
|
|
234
|
+
it('lets a realtime signal break transient retry backoff and pull immediately', async () => {
|
|
235
|
+
vi.useFakeTimers();
|
|
236
|
+
vi.stubGlobal('WebSocket', MockWebSocket);
|
|
237
|
+
runWorkspacePullSyncServiceMock
|
|
238
|
+
.mockResolvedValueOnce({
|
|
239
|
+
success: false,
|
|
240
|
+
kind: 'pull_http',
|
|
241
|
+
status: 502,
|
|
242
|
+
transient: true,
|
|
243
|
+
retryAfterMs: 60_000,
|
|
244
|
+
cursor: null,
|
|
245
|
+
pages: 0,
|
|
246
|
+
appliedChanges: 0,
|
|
247
|
+
pullRequestMs: 1,
|
|
248
|
+
applyMs: 0,
|
|
249
|
+
pulledDeleteTaskIds: new Set(),
|
|
250
|
+
pulledActiveUpserts: false,
|
|
251
|
+
pulledArchivedUpserts: false,
|
|
252
|
+
documentDecryptFailures: []
|
|
253
|
+
})
|
|
254
|
+
.mockResolvedValueOnce({
|
|
255
|
+
success: true,
|
|
256
|
+
syncedAt: '2026-03-15T18:05:00.000Z',
|
|
257
|
+
cursor: null,
|
|
258
|
+
pages: 1,
|
|
259
|
+
appliedChanges: 1,
|
|
260
|
+
pullRequestMs: 1,
|
|
261
|
+
applyMs: 0,
|
|
262
|
+
pulledDeleteTaskIds: new Set(),
|
|
263
|
+
pulledActiveUpserts: true,
|
|
264
|
+
pulledArchivedUpserts: false,
|
|
265
|
+
documentDecryptFailures: []
|
|
266
|
+
});
|
|
267
|
+
const { result, unmount } = renderHook((props) => useSyncOrchestrator(props), {
|
|
268
|
+
initialProps: buildInput({ realtimeSyncEnabled: true })
|
|
269
|
+
});
|
|
270
|
+
act(() => {
|
|
271
|
+
result.current.applyWorkspaceSyncStateSnapshot({
|
|
272
|
+
enabled: true,
|
|
273
|
+
phase: 'active',
|
|
274
|
+
pullCursor: null
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
await act(async () => {
|
|
278
|
+
await Promise.resolve();
|
|
279
|
+
await Promise.resolve();
|
|
280
|
+
});
|
|
281
|
+
expect(result.current.workspaceRetryAt).not.toBeNull();
|
|
282
|
+
expect(MockWebSocket.instances).toHaveLength(1);
|
|
283
|
+
expect(runWorkspacePullSyncServiceMock).toHaveBeenCalledTimes(1);
|
|
284
|
+
act(() => {
|
|
285
|
+
MockWebSocket.instances[0]?.open();
|
|
286
|
+
MockWebSocket.instances[0]?.message({
|
|
287
|
+
type: 'taskforce:update',
|
|
288
|
+
workspaceId: 'workspace-local-active'
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
await act(async () => {
|
|
292
|
+
await vi.advanceTimersByTimeAsync(300);
|
|
293
|
+
await Promise.resolve();
|
|
294
|
+
await Promise.resolve();
|
|
295
|
+
});
|
|
296
|
+
expect(runWorkspacePullSyncServiceMock).toHaveBeenCalledTimes(2);
|
|
297
|
+
expect(result.current.workspaceRetryAt).toBeNull();
|
|
298
|
+
expect(result.current.workspaceLastPullAt).toBe('2026-03-15T18:05:00.000Z');
|
|
299
|
+
unmount();
|
|
300
|
+
});
|
|
209
301
|
it('retries transient push failures automatically', async () => {
|
|
210
302
|
runWorkspacePushSyncServiceMock
|
|
211
303
|
.mockResolvedValueOnce({
|
|
@@ -249,6 +249,9 @@ export declare function useTaskforce({ config, initialTaskId, onTaskCountChange,
|
|
|
249
249
|
emailSent?: boolean;
|
|
250
250
|
emailError?: string;
|
|
251
251
|
workspaceSetupRequired?: boolean;
|
|
252
|
+
planSelectionRequired?: boolean;
|
|
253
|
+
checkoutPending?: boolean;
|
|
254
|
+
commercialState?: string | null;
|
|
252
255
|
}>;
|
|
253
256
|
updateCurrentUserProfile: (input: {
|
|
254
257
|
displayName: string;
|
|
@@ -229,6 +229,8 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
229
229
|
if (!path.startsWith('/'))
|
|
230
230
|
return path;
|
|
231
231
|
const isCloudScopedPath = path.startsWith('/api/taskforce/auth/')
|
|
232
|
+
|| path.startsWith('/api/taskforce/account/')
|
|
233
|
+
|| path.startsWith('/api/taskforce/billing/')
|
|
232
234
|
|| path.startsWith('/api/taskforce/sync/')
|
|
233
235
|
|| path.startsWith('/api/taskforce/settings/mcp/');
|
|
234
236
|
if (!isCloudHost && isCloudScopedPath && (!runtimeConfigReady || runtimeConfigOverride.cloudAuthViaLocalProxy)) {
|
|
@@ -1999,7 +2001,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
1999
2001
|
const retryBootstrapChecks = useCallback(async () => {
|
|
2000
2002
|
setBootstrapError(null);
|
|
2001
2003
|
markBootstrapPhase('auth');
|
|
2002
|
-
const authenticated = await checkAuthSession();
|
|
2004
|
+
const authenticated = await checkAuthSession({ force: true });
|
|
2003
2005
|
if (workspaceSwitchingEnabled && authenticated) {
|
|
2004
2006
|
const workspaceResolution = await resolveWorkspaceAfterAuth();
|
|
2005
2007
|
if (!workspaceResolution.success || workspaceResolution.workspaceSetupRequired) {
|
|
@@ -2066,7 +2068,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2066
2068
|
if (!res.ok || data?.success === false) {
|
|
2067
2069
|
return { success: false, error: data?.error || `Failed to delete workspace (${res.status})`, code: data?.code };
|
|
2068
2070
|
}
|
|
2069
|
-
await checkAuthSession();
|
|
2071
|
+
await checkAuthSession({ force: true });
|
|
2070
2072
|
await Promise.all([fetchConfig(), fetchWorkspaces()]);
|
|
2071
2073
|
const nextWorkspaceId = typeof data?.nextWorkspaceId === 'string' ? data.nextWorkspaceId.trim() : '';
|
|
2072
2074
|
if (nextWorkspaceId) {
|
|
@@ -2168,7 +2170,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2168
2170
|
return { success: false, error: data?.error || 'Sign in failed.', code: data?.code };
|
|
2169
2171
|
}
|
|
2170
2172
|
setError('');
|
|
2171
|
-
await checkAuthSession();
|
|
2173
|
+
await checkAuthSession({ force: true });
|
|
2172
2174
|
const workspaceResolution = await resolveWorkspaceAfterAuth();
|
|
2173
2175
|
if (!workspaceResolution.success) {
|
|
2174
2176
|
return {
|
|
@@ -2229,6 +2231,9 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2229
2231
|
return {
|
|
2230
2232
|
success: true,
|
|
2231
2233
|
workspaceSetupRequired: Boolean(data?.workspaceSetupRequired),
|
|
2234
|
+
planSelectionRequired: data?.planSelectionRequired === true,
|
|
2235
|
+
checkoutPending: data?.checkoutPending === true,
|
|
2236
|
+
commercialState: typeof data?.commercialState === 'string' ? data.commercialState : null,
|
|
2232
2237
|
verificationRequired: Boolean(data?.verificationRequired),
|
|
2233
2238
|
verificationToken: typeof data?.verificationToken === 'string' ? data.verificationToken : undefined,
|
|
2234
2239
|
emailSent: data?.emailSent !== false,
|
|
@@ -2443,7 +2448,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2443
2448
|
return { success: false, error: data?.error || 'Invite acceptance failed.', code: data?.code };
|
|
2444
2449
|
}
|
|
2445
2450
|
setError('');
|
|
2446
|
-
await checkAuthSession();
|
|
2451
|
+
await checkAuthSession({ force: true });
|
|
2447
2452
|
const preferredWorkspaceId = typeof data?.workspaceId === 'string' ? data.workspaceId : null;
|
|
2448
2453
|
const workspaceResolution = await resolveWorkspaceAfterAuth({ preferredWorkspaceId });
|
|
2449
2454
|
if (!workspaceResolution.success) {
|