@taskforcehq/taskforce 0.3.304 → 0.3.305
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/README.md +140 -247
- package/dist/TaskforceCore.js +165 -70
- package/dist/TaskforceCore.test.js +799 -90
- package/dist/compat/workspaceSyncCompat.js +6 -0
- package/dist/components/features/DocumentIndex.d.ts +1 -1
- package/dist/components/features/DocumentIndex.js +1 -1
- package/dist/components/features/DocumentViewer.d.ts +1 -1
- package/dist/components/features/DocumentViewer.js +32 -9
- package/dist/components/features/DocumentWorkspace.d.ts +5 -22
- package/dist/components/features/DocumentWorkspace.js +12 -155
- package/dist/components/features/DocumentWorkspace.test.js +193 -6
- package/dist/components/features/TaskSettings.js +29 -3
- package/dist/components/features/TaskSettings.test.js +42 -1
- package/dist/components/features/documentWorkspaceModel.d.ts +24 -0
- package/dist/components/features/documentWorkspaceModel.js +57 -0
- package/dist/components/features/useDocumentWorkspaceController.d.ts +34 -0
- package/dist/components/features/useDocumentWorkspaceController.js +113 -0
- package/dist/components/task/TaskForm.d.ts +8 -8
- package/dist/components/task/TaskForm.js +358 -221
- package/dist/components/task/TaskForm.test.js +456 -381
- package/dist/components/task/TaskKanban.js +2 -1
- package/dist/components/views/PlanComparisonPage.js +5 -8
- package/dist/components/views/PlanComparisonPage.test.js +51 -41
- package/dist/components/views/PlansPage.js +126 -84
- package/dist/components/views/PlansPage.test.js +308 -39
- package/dist/components/views/StandaloneLayout.js +28 -22
- package/dist/components/views/panels/FilterToolbar.test.js +6 -4
- package/dist/components/views/standalone/modals/AccountHubModal.d.ts +2 -14
- package/dist/components/views/standalone/modals/AccountHubModal.js +12 -14
- package/dist/components/views/standalone/modals/AccountHubModal.test.js +24 -1
- package/dist/config/envSchema.js +1 -1
- package/dist/core/AiProfileService.d.ts +6 -1
- package/dist/core/AiProfileService.js +161 -16
- package/dist/core/AiProfileService.test.js +3 -1
- package/dist/core/AiProfiles.test.js +200 -0
- package/dist/core/AuthTokenService.test.d.ts +1 -0
- package/dist/core/AuthTokenService.test.js +78 -0
- package/dist/core/EntitlementsPolicy.test.js +75 -13
- package/dist/core/PlanEntitlementService.d.ts +7 -1
- package/dist/core/PlanEntitlementService.js +117 -42
- package/dist/core/PlanVersionPolicy.test.js +19 -26
- package/dist/core/SignupMonetizationSettings.test.js +46 -21
- package/dist/core/SystemAdmin.test.js +212 -56
- package/dist/core/TaskTaxonomyValidation.test.js +53 -0
- package/dist/core/Taskforce.d.ts +3 -0
- package/dist/core/Taskforce.js +50 -10
- package/dist/core/WorkspacePlanMode.test.js +12 -12
- package/dist/core/shared.d.ts +2 -0
- package/dist/core/shared.js +11 -0
- package/dist/hooks/sync/auth.js +3 -1
- package/dist/hooks/sync/transfers.d.ts +2 -1
- package/dist/hooks/sync/transfers.js +8 -4
- package/dist/hooks/useSyncOrchestrator.d.ts +7 -4
- package/dist/hooks/useSyncOrchestrator.js +75 -7
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +80 -10
- package/dist/hooks/useTaskMutations.d.ts +2 -1
- package/dist/hooks/useTaskMutations.js +12 -1
- package/dist/hooks/useTaskMutations.test.js +30 -1
- package/dist/hooks/useTaskforce.d.ts +5 -2
- package/dist/hooks/useTaskforce.js +17 -5
- package/dist/hooks/useTaskforce.sync-behavior.test.js +78 -0
- package/dist/hooks/useWorkspaceSyncController.d.ts +3 -1
- package/dist/hooks/useWorkspaceSyncController.js +11 -1
- package/dist/hooks/useWorkspaceSyncController.test.js +1 -0
- package/dist/mcp/adminWorkspaceRegistrar.d.ts +2 -0
- package/dist/mcp/adminWorkspaceRegistrar.js +154 -0
- package/dist/mcp/collaborationRegistrar.d.ts +2 -0
- package/dist/mcp/collaborationRegistrar.js +71 -0
- package/dist/mcp/documentAssetRegistrar.d.ts +2 -0
- package/dist/mcp/documentAssetRegistrar.js +346 -0
- package/dist/mcp/runtime.js +2362 -3530
- package/dist/mcp/runtime.test.js +159 -0
- package/dist/mcp/taskPlanningRegistrar.d.ts +2 -0
- package/dist/mcp/taskPlanningRegistrar.js +418 -0
- package/dist/mcp/toolCatalog.d.ts +35 -0
- package/dist/mcp/toolCatalog.js +3 -0
- package/dist/server/routes/admin.d.ts +1 -5
- package/dist/server/routes/admin.js +138 -61
- package/dist/server/routes/annotatedAttachments.d.ts +1 -1
- package/dist/server/routes/annotatedAttachments.js +1 -1
- package/dist/server/routes/auth.d.ts +1 -4
- package/dist/server/routes/auth.js +23 -63
- package/dist/server/routes/authSupport.d.ts +30 -0
- package/dist/server/routes/authSupport.js +81 -0
- package/dist/server/routes/billing.d.ts +1 -1
- package/dist/server/routes/billing.js +104 -188
- package/dist/server/routes/billing.test.js +214 -86
- package/dist/server/routes/documentReviews.d.ts +1 -1
- package/dist/server/routes/documentReviews.js +1 -1
- package/dist/server/routes/documents.d.ts +4 -2
- package/dist/server/routes/documents.js +73 -7
- package/dist/server/routes/primitives.d.ts +11 -0
- package/dist/server/routes/primitives.js +73 -0
- package/dist/server/routes/resources.d.ts +1 -1
- package/dist/server/routes/resources.js +1 -1
- package/dist/server/routes/shared.d.ts +1 -1
- package/dist/server/routes/sync.d.ts +1 -1
- package/dist/server/routes/sync.js +1 -1
- package/dist/server/routes/tasks.d.ts +1 -1
- package/dist/server/routes/tasks.js +1 -1
- package/dist/server/routes/workspaces.d.ts +1 -1
- package/dist/server/routes/workspaces.js +1 -1
- package/dist/server/routes.d.ts +2 -9
- package/dist/server/routes.js +76 -162
- package/dist/server/routes.test.js +672 -30
- package/dist/sync/contentSyncState.js +16 -7
- package/dist/sync/syncApplyHandlers.test.js +2 -0
- package/dist/sync/syncService.d.ts +1 -0
- package/dist/sync/syncService.js +8 -0
- package/dist/sync/syncService.test.js +14 -0
- package/dist/sync/workspaceSyncModel.d.ts +3 -0
- package/dist/sync/workspaceSyncModel.js +12 -3
- package/dist/sync/workspaceSyncModel.test.js +34 -0
- package/dist/sync/workspaceSyncState.d.ts +2 -0
- package/dist/sync/workspaceSyncState.js +1 -0
- package/dist/ui/assets/{AgentsModule-BLWVbuKP.js → AgentsModule-N2MnQBZk.js} +1 -1
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-BlS-cqnl.js → AnnotatedAttachmentWorkspace-BG6P_GVW.js} +1 -1
- package/dist/ui/assets/DocumentWorkspace-B3nV_Gc5.css +1 -0
- package/dist/ui/assets/DocumentWorkspace-DRbDMwh8.js +252 -0
- package/dist/ui/assets/{InitiativesModule-BjR6iBf9.js → InitiativesModule-Dhm7M8e7.js} +1 -1
- package/dist/ui/assets/PlansPage-Cq0zXj3I.js +1 -0
- package/dist/ui/assets/TaskSettings-ln0aF7xc.js +9 -0
- package/dist/ui/assets/{WorkflowsModule-DnFqdUME.js → WorkflowsModule-BcS4afRn.js} +1 -1
- package/dist/ui/assets/{index-Ii0B0rTO.css → index-C2-OXZV7.css} +1 -1
- package/dist/ui/assets/index-DcSI5F86.js +6 -0
- package/dist/ui/assets/{vendor-icons-I7ZwG1z_.js → vendor-icons-BSUaRwF8.js} +1 -1
- package/dist/ui/index.html +3 -3
- package/dist/utils/commercialLifecyclePresentation.d.ts +19 -0
- package/dist/utils/commercialLifecyclePresentation.js +196 -0
- package/package.json +8 -1
- package/scripts/check-cloud-mcp.mjs +83 -0
- package/scripts/playwright-auth.sh +5 -1
- package/scripts/run-billing-performance-smoke.sh +24 -0
- package/dist/ui/assets/DocumentWorkspace-BH_6DF6x.js +0 -250
- package/dist/ui/assets/DocumentWorkspace-C_8T8oz-.css +0 -1
- package/dist/ui/assets/PlansPage-CSDQ4sLa.js +0 -1
- package/dist/ui/assets/TaskSettings-CEDy34Jo.js +0 -9
- package/dist/ui/assets/index-DXUdtH1B.js +0 -6
|
@@ -3,6 +3,7 @@ import fs from 'fs';
|
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import { TaskforceCore, TaskforceRuleError } from './Taskforce';
|
|
6
|
+
import { WorkspaceAssetStore } from '../storage/workspaceAssetStore.js';
|
|
6
7
|
function makeProjectRoot() {
|
|
7
8
|
return fs.mkdtempSync(path.join(os.tmpdir(), 'taskforce-entitlements-'));
|
|
8
9
|
}
|
|
@@ -20,20 +21,20 @@ function openCore(projectRoot) {
|
|
|
20
21
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
21
22
|
`).run('default', planVersionId, 'month', `free:${planVersionId}:month`, 'free', 1, 0, null, now, now);
|
|
22
23
|
};
|
|
23
|
-
core.upsertPlanCatalog({ planId: '
|
|
24
|
-
addFreeMapping('
|
|
25
|
-
core.upsertPlanCatalog({ planId: '
|
|
26
|
-
core.upsertPlanCatalog({ planId: '
|
|
27
|
-
addFreeMapping('
|
|
28
|
-
core.upsertPlanCatalog({ planId: '
|
|
24
|
+
core.upsertPlanCatalog({ planId: 'free', displayName: 'Free' });
|
|
25
|
+
addFreeMapping('free-v1');
|
|
26
|
+
core.upsertPlanCatalog({ planId: 'free', displayName: 'Free', status: 'active' });
|
|
27
|
+
core.upsertPlanCatalog({ planId: 'team-5', displayName: 'Team 5' });
|
|
28
|
+
addFreeMapping('team-5-v1');
|
|
29
|
+
core.upsertPlanCatalog({ planId: 'team-5', displayName: 'Team 5', status: 'active' });
|
|
29
30
|
core.createPlanVersion({
|
|
30
|
-
planId: '
|
|
31
|
+
planId: 'team-5',
|
|
31
32
|
versionNumber: 2,
|
|
32
33
|
isDefault: true,
|
|
33
34
|
seatLimit: 5
|
|
34
35
|
});
|
|
35
36
|
core.updatePlanVersionFeatures({
|
|
36
|
-
planVersionId: '
|
|
37
|
+
planVersionId: 'team-5-v2',
|
|
37
38
|
features: [
|
|
38
39
|
{ featureKey: 'collaboration.invites', access: 'enabled' },
|
|
39
40
|
{ featureKey: 'collaboration.team_management', access: 'enabled' },
|
|
@@ -139,13 +140,13 @@ describe('Taskforce entitlements policy engine', () => {
|
|
|
139
140
|
email: 'owner@example.com'
|
|
140
141
|
});
|
|
141
142
|
core.createPlanVersion({
|
|
142
|
-
planId: '
|
|
143
|
+
planId: 'team-5',
|
|
143
144
|
versionNumber: 3,
|
|
144
145
|
isDefault: false,
|
|
145
146
|
seatLimit: 2
|
|
146
147
|
});
|
|
147
148
|
core.updatePlanVersionFeatures({
|
|
148
|
-
planVersionId: '
|
|
149
|
+
planVersionId: 'team-5-v3',
|
|
149
150
|
features: [
|
|
150
151
|
{ featureKey: 'collaboration.invites', access: 'enabled' },
|
|
151
152
|
{ featureKey: 'collaboration.team_management', access: 'enabled' },
|
|
@@ -154,7 +155,7 @@ describe('Taskforce entitlements policy engine', () => {
|
|
|
154
155
|
});
|
|
155
156
|
core.updateAccountEntitlement({
|
|
156
157
|
accountId: 'owner-u1',
|
|
157
|
-
planVersionId: '
|
|
158
|
+
planVersionId: 'team-5-v3',
|
|
158
159
|
entitlementState: 'active',
|
|
159
160
|
actorUserId: 'system-admin',
|
|
160
161
|
actorRole: 'system_admin'
|
|
@@ -184,7 +185,7 @@ describe('Taskforce entitlements policy engine', () => {
|
|
|
184
185
|
const core = openCore(projectRoot);
|
|
185
186
|
try {
|
|
186
187
|
core.updatePlanVersionFeatures({
|
|
187
|
-
planVersionId: '
|
|
188
|
+
planVersionId: 'free-v1',
|
|
188
189
|
features: [
|
|
189
190
|
{ featureKey: 'workspace.switching', access: 'limited', config: { maxWorkspaces: 1 } }
|
|
190
191
|
]
|
|
@@ -198,7 +199,7 @@ describe('Taskforce entitlements policy engine', () => {
|
|
|
198
199
|
});
|
|
199
200
|
core.updateAccountEntitlement({
|
|
200
201
|
accountId: 'owner-u1',
|
|
201
|
-
planVersionId: '
|
|
202
|
+
planVersionId: 'free-v1',
|
|
202
203
|
entitlementState: 'active',
|
|
203
204
|
actorUserId: 'system-admin',
|
|
204
205
|
actorRole: 'system_admin'
|
|
@@ -215,4 +216,65 @@ describe('Taskforce entitlements policy engine', () => {
|
|
|
215
216
|
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
216
217
|
}
|
|
217
218
|
});
|
|
219
|
+
it('enforces limited document cloud storage allowances by current usage plus requested upload size', () => {
|
|
220
|
+
const projectRoot = makeProjectRoot();
|
|
221
|
+
const core = openCore(projectRoot);
|
|
222
|
+
try {
|
|
223
|
+
core.updatePlanVersionFeatures({
|
|
224
|
+
planVersionId: 'free-v1',
|
|
225
|
+
features: [
|
|
226
|
+
{ featureKey: 'context.uploads', access: 'limited', config: { storageLimitMb: 1 } }
|
|
227
|
+
]
|
|
228
|
+
});
|
|
229
|
+
core.createWorkspace({ workspaceId: 'workspace-storage', name: 'Storage Workspace' });
|
|
230
|
+
core.ensureBootstrapUserAccess({
|
|
231
|
+
userId: 'owner-u1',
|
|
232
|
+
workspaceId: 'workspace-storage',
|
|
233
|
+
role: 'owner',
|
|
234
|
+
email: 'owner@example.com'
|
|
235
|
+
});
|
|
236
|
+
core.updateAccountEntitlement({
|
|
237
|
+
accountId: 'owner-u1',
|
|
238
|
+
planVersionId: 'free-v1',
|
|
239
|
+
entitlementState: 'active',
|
|
240
|
+
actorUserId: 'system-admin',
|
|
241
|
+
actorRole: 'system_admin'
|
|
242
|
+
});
|
|
243
|
+
const store = new WorkspaceAssetStore(core.getDatabaseAdapter(), core.getTenantId());
|
|
244
|
+
store.upsertAsset({
|
|
245
|
+
assetId: 'asset-storage-1',
|
|
246
|
+
workspaceId: 'workspace-storage',
|
|
247
|
+
kind: 'document',
|
|
248
|
+
originalFilename: 'existing.pdf',
|
|
249
|
+
mimeType: 'application/pdf',
|
|
250
|
+
storageProvider: 'local',
|
|
251
|
+
storageKey: 'workspaces/workspace-storage/assets/documents/asset-storage-1-existing.pdf',
|
|
252
|
+
contentSha256: 'existing-hash',
|
|
253
|
+
sizeBytes: 900_000,
|
|
254
|
+
documentId: 'document-storage-1',
|
|
255
|
+
version: 1,
|
|
256
|
+
isLatest: true
|
|
257
|
+
});
|
|
258
|
+
const withinLimit = core.evaluateFeatureAccess({
|
|
259
|
+
userId: 'owner-u1',
|
|
260
|
+
workspaceId: 'workspace-storage',
|
|
261
|
+
featureKey: 'context.uploads',
|
|
262
|
+
context: { requestedBytes: 100_000 }
|
|
263
|
+
});
|
|
264
|
+
expect(withinLimit.allowed).toBe(true);
|
|
265
|
+
expect(withinLimit.reasonCode).toBe('OK');
|
|
266
|
+
const overLimit = core.evaluateFeatureAccess({
|
|
267
|
+
userId: 'owner-u1',
|
|
268
|
+
workspaceId: 'workspace-storage',
|
|
269
|
+
featureKey: 'context.uploads',
|
|
270
|
+
context: { requestedBytes: 200_000 }
|
|
271
|
+
});
|
|
272
|
+
expect(overLimit.allowed).toBe(false);
|
|
273
|
+
expect(overLimit.reasonCode).toBe('UPLOAD_STORAGE_LIMIT_REACHED');
|
|
274
|
+
}
|
|
275
|
+
finally {
|
|
276
|
+
core.close();
|
|
277
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
278
|
+
}
|
|
279
|
+
});
|
|
218
280
|
});
|
|
@@ -29,6 +29,7 @@ export declare class PlanEntitlementService {
|
|
|
29
29
|
private readAccountEntitlementRow;
|
|
30
30
|
private normalizeExpiredCheckoutPendingRow;
|
|
31
31
|
private isBillingConfirmedTransitionSource;
|
|
32
|
+
private countWorkspaceStorageUsageBytes;
|
|
32
33
|
private resolveCanonicalTransitionSource;
|
|
33
34
|
private resolvePendingPlanVersionId;
|
|
34
35
|
private buildBlockedAccessStatus;
|
|
@@ -73,6 +74,10 @@ export declare class PlanEntitlementService {
|
|
|
73
74
|
limit: number | null;
|
|
74
75
|
feature: PlanFeatureSnapshot | null;
|
|
75
76
|
};
|
|
77
|
+
resolveAiProfileLimitForWorkspace(workspaceId: string): {
|
|
78
|
+
limit: number | null;
|
|
79
|
+
feature: PlanFeatureSnapshot | null;
|
|
80
|
+
};
|
|
76
81
|
private planHasAnyActivePricing;
|
|
77
82
|
private planVersionHasActiveFreePricing;
|
|
78
83
|
private planVersionIsTrialEligible;
|
|
@@ -80,7 +85,6 @@ export declare class PlanEntitlementService {
|
|
|
80
85
|
private getActiveDefaultPlanVersions;
|
|
81
86
|
private getDefaultPlanVersionByMode;
|
|
82
87
|
private tryGetDefaultPlanVersionByMode;
|
|
83
|
-
private tryGetImplicitFreeOnboardingPlanVersion;
|
|
84
88
|
getOnboardingPolicy(): OnboardingPolicy;
|
|
85
89
|
validateOnboardingPolicy(input?: Partial<OnboardingPolicy>): OnboardingPolicyValidationResult;
|
|
86
90
|
resolveRegistrationOnboardingOutcome(selectedPlanVersionIdRaw?: string): {
|
|
@@ -101,12 +105,14 @@ export declare class PlanEntitlementService {
|
|
|
101
105
|
};
|
|
102
106
|
hasAccountEntitlement(accountId: string): boolean;
|
|
103
107
|
private tryGetAccountEntitlement;
|
|
108
|
+
private tryAutoHealLegacyPersonalFreeEntitlement;
|
|
104
109
|
getAccountAccessStatus(accountId: string): AccountAccessStatus;
|
|
105
110
|
getEffectivePlanVersion(accountId: string): PlanVersionSnapshot;
|
|
106
111
|
getAccountEntitlement(accountId: string): AccountEntitlementSnapshot;
|
|
107
112
|
countWorkspaceSeatUsage(workspaceId: string, options?: {
|
|
108
113
|
includeInvited?: boolean;
|
|
109
114
|
}): SeatUsageSnapshot;
|
|
115
|
+
countAiProfileUsage(workspaceId: string): SeatUsageSnapshot;
|
|
110
116
|
assertSeatAvailable(workspaceId: string): void;
|
|
111
117
|
evaluateFeatureAccess(input: {
|
|
112
118
|
userId: string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { TaskforceRuleError } from './types.js';
|
|
2
|
-
import { DOCUMENT_CLOUD_STORAGE_FEATURE_KEY, WORKSPACES_FEATURE_KEY, DOCUMENT_CLOUD_STORAGE_LIMIT_CONFIG_KEY, WORKSPACES_LIMIT_CONFIG_KEY, DEFAULT_PLAN_FEATURE_CATALOG, PLAN_FEATURE_DEPENDENCIES, generateUuidV7, } from './shared.js';
|
|
2
|
+
import { DOCUMENT_CLOUD_STORAGE_FEATURE_KEY, WORKSPACES_FEATURE_KEY, AI_PROFILES_FEATURE_KEY, DOCUMENT_CLOUD_STORAGE_LIMIT_CONFIG_KEY, WORKSPACES_LIMIT_CONFIG_KEY, AI_PROFILES_LIMIT_CONFIG_KEY, DEFAULT_PLAN_FEATURE_CATALOG, PLAN_FEATURE_DEPENDENCIES, generateUuidV7, } from './shared.js';
|
|
3
3
|
const LEGACY_REMOVED_PLAN_FEATURE_KEYS = Object.freeze([
|
|
4
4
|
'documents.workspace_access'
|
|
5
5
|
]);
|
|
@@ -113,6 +113,15 @@ export class PlanEntitlementService {
|
|
|
113
113
|
[WORKSPACES_LIMIT_CONFIG_KEY]: limit ?? 1
|
|
114
114
|
};
|
|
115
115
|
}
|
|
116
|
+
if (featureKey === AI_PROFILES_FEATURE_KEY) {
|
|
117
|
+
if (access === 'limited') {
|
|
118
|
+
const limit = this.readPositiveIntegerConfig(config[AI_PROFILES_LIMIT_CONFIG_KEY], 1);
|
|
119
|
+
return {
|
|
120
|
+
[AI_PROFILES_LIMIT_CONFIG_KEY]: limit ?? 1
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
return {};
|
|
124
|
+
}
|
|
116
125
|
return config && typeof config === 'object' ? { ...config } : {};
|
|
117
126
|
}
|
|
118
127
|
isEntitlementStateAllowed(state) {
|
|
@@ -160,6 +169,20 @@ export class PlanEntitlementService {
|
|
|
160
169
|
isBillingConfirmedTransitionSource(source) {
|
|
161
170
|
return source === 'checkout-confirm' || source === 'stripe-webhook';
|
|
162
171
|
}
|
|
172
|
+
countWorkspaceStorageUsageBytes(workspaceIdRaw) {
|
|
173
|
+
const workspaceId = String(workspaceIdRaw || '').trim();
|
|
174
|
+
if (!workspaceId)
|
|
175
|
+
return 0;
|
|
176
|
+
const row = this.db.prepare(`
|
|
177
|
+
SELECT COALESCE(SUM(size_bytes), 0) AS total_bytes
|
|
178
|
+
FROM workspace_assets
|
|
179
|
+
WHERE tenant_id = ?
|
|
180
|
+
AND workspace_id = ?
|
|
181
|
+
AND deleted_at IS NULL
|
|
182
|
+
`).get(this.tenantId, workspaceId);
|
|
183
|
+
const total = Number(row?.total_bytes || 0);
|
|
184
|
+
return Number.isFinite(total) ? Math.max(0, Math.floor(total)) : 0;
|
|
185
|
+
}
|
|
163
186
|
resolveCanonicalTransitionSource(updatedBy) {
|
|
164
187
|
if (updatedBy === 'registration'
|
|
165
188
|
|| updatedBy === 'checkout-start'
|
|
@@ -534,6 +557,32 @@ export class PlanEntitlementService {
|
|
|
534
557
|
}
|
|
535
558
|
return { limit: null, feature };
|
|
536
559
|
}
|
|
560
|
+
resolveAiProfileLimitForWorkspace(workspaceId) {
|
|
561
|
+
const normalizedWorkspaceId = String(workspaceId || '').trim();
|
|
562
|
+
if (!normalizedWorkspaceId) {
|
|
563
|
+
return { limit: null, feature: null };
|
|
564
|
+
}
|
|
565
|
+
const accountId = this.deps.getWorkspaceOwnerUserId(normalizedWorkspaceId);
|
|
566
|
+
if (!accountId) {
|
|
567
|
+
return { limit: null, feature: null };
|
|
568
|
+
}
|
|
569
|
+
let entitlement;
|
|
570
|
+
try {
|
|
571
|
+
entitlement = this.getAccountEntitlement(accountId);
|
|
572
|
+
}
|
|
573
|
+
catch (error) {
|
|
574
|
+
if (error instanceof TaskforceRuleError && error.code === 'ENTITLEMENT_NOT_FOUND') {
|
|
575
|
+
return { limit: null, feature: null };
|
|
576
|
+
}
|
|
577
|
+
throw error;
|
|
578
|
+
}
|
|
579
|
+
const feature = entitlement.version.features[AI_PROFILES_FEATURE_KEY] || null;
|
|
580
|
+
if (!feature || feature.access !== 'limited') {
|
|
581
|
+
return { limit: null, feature };
|
|
582
|
+
}
|
|
583
|
+
const configuredLimit = this.readPositiveIntegerConfig(feature.config?.[AI_PROFILES_LIMIT_CONFIG_KEY], 1);
|
|
584
|
+
return { limit: configuredLimit ?? 1, feature };
|
|
585
|
+
}
|
|
537
586
|
planHasAnyActivePricing(planIdRaw) {
|
|
538
587
|
const planId = String(planIdRaw || '').trim().toLowerCase();
|
|
539
588
|
if (!planId)
|
|
@@ -629,33 +678,6 @@ export class PlanEntitlementService {
|
|
|
629
678
|
throw error;
|
|
630
679
|
}
|
|
631
680
|
}
|
|
632
|
-
tryGetImplicitFreeOnboardingPlanVersion() {
|
|
633
|
-
const rows = this.db.prepare(`
|
|
634
|
-
SELECT DISTINCT v.plan_version_id
|
|
635
|
-
FROM plan_versions v
|
|
636
|
-
JOIN plan_catalog p
|
|
637
|
-
ON p.tenant_id = v.tenant_id
|
|
638
|
-
AND p.plan_id = v.plan_id
|
|
639
|
-
JOIN billing_price_mappings bpm
|
|
640
|
-
ON bpm.tenant_id = v.tenant_id
|
|
641
|
-
AND bpm.plan_version_id = v.plan_version_id
|
|
642
|
-
WHERE v.tenant_id = ?
|
|
643
|
-
AND COALESCE(p.status, 'active') = 'active'
|
|
644
|
-
AND bpm.active = 1
|
|
645
|
-
AND bpm.pricing_type = 'free'
|
|
646
|
-
ORDER BY v.is_default DESC, p.updated_at DESC, p.created_at DESC, v.version_number DESC, v.created_at DESC
|
|
647
|
-
`).all(this.tenantId);
|
|
648
|
-
for (const row of rows) {
|
|
649
|
-
const planVersionId = String(row.plan_version_id || '').trim();
|
|
650
|
-
if (!planVersionId)
|
|
651
|
-
continue;
|
|
652
|
-
const version = this.getPlanVersionSnapshot(planVersionId);
|
|
653
|
-
if (!this.isTeamPlanVersion(version)) {
|
|
654
|
-
return version;
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
return null;
|
|
658
|
-
}
|
|
659
681
|
getOnboardingPolicy() {
|
|
660
682
|
const billing = this.deps.readGlobalConfig().billing || {};
|
|
661
683
|
const policy = billing.onboardingPolicy && typeof billing.onboardingPolicy === 'object'
|
|
@@ -671,13 +693,6 @@ export class PlanEntitlementService {
|
|
|
671
693
|
: null
|
|
672
694
|
};
|
|
673
695
|
}
|
|
674
|
-
const implicitFreeVersion = this.tryGetImplicitFreeOnboardingPlanVersion();
|
|
675
|
-
if (implicitFreeVersion) {
|
|
676
|
-
return {
|
|
677
|
-
mode: 'auto_grant_free',
|
|
678
|
-
planVersionId: implicitFreeVersion.planVersionId
|
|
679
|
-
};
|
|
680
|
-
}
|
|
681
696
|
return {
|
|
682
697
|
mode: 'require_plan_selection',
|
|
683
698
|
planVersionId: null
|
|
@@ -755,7 +770,12 @@ export class PlanEntitlementService {
|
|
|
755
770
|
}
|
|
756
771
|
resolveRegistrationOnboardingOutcome(selectedPlanVersionIdRaw) {
|
|
757
772
|
const selectedPlanVersionId = String(selectedPlanVersionIdRaw || '').trim();
|
|
758
|
-
|
|
773
|
+
const onboardingPolicy = this.getOnboardingPolicy();
|
|
774
|
+
const validation = this.validateOnboardingPolicy(onboardingPolicy);
|
|
775
|
+
if (!validation.valid) {
|
|
776
|
+
throw new TaskforceRuleError(validation.message || 'Onboarding policy is misconfigured.', 500, validation.code || 'ONBOARDING_POLICY_INVALID', validation.details);
|
|
777
|
+
}
|
|
778
|
+
if (onboardingPolicy.mode === 'require_plan_selection' && selectedPlanVersionId) {
|
|
759
779
|
const planVersion = this.getPlanVersionSnapshot(selectedPlanVersionId);
|
|
760
780
|
const commercialState = this.planVersionHasActiveFreePricing(planVersion.planVersionId)
|
|
761
781
|
? 'active'
|
|
@@ -768,11 +788,6 @@ export class PlanEntitlementService {
|
|
|
768
788
|
planId: planVersion.planId
|
|
769
789
|
};
|
|
770
790
|
}
|
|
771
|
-
const onboardingPolicy = this.getOnboardingPolicy();
|
|
772
|
-
const validation = this.validateOnboardingPolicy(onboardingPolicy);
|
|
773
|
-
if (!validation.valid) {
|
|
774
|
-
throw new TaskforceRuleError(validation.message || 'Onboarding policy is misconfigured.', 500, validation.code || 'ONBOARDING_POLICY_INVALID', validation.details);
|
|
775
|
-
}
|
|
776
791
|
if (onboardingPolicy.mode === 'require_plan_selection') {
|
|
777
792
|
return {
|
|
778
793
|
requiresPlanSelection: true,
|
|
@@ -844,6 +859,34 @@ export class PlanEntitlementService {
|
|
|
844
859
|
throw error;
|
|
845
860
|
}
|
|
846
861
|
}
|
|
862
|
+
tryAutoHealLegacyPersonalFreeEntitlement(accountId, entitlement) {
|
|
863
|
+
if (entitlement.state !== 'canceled')
|
|
864
|
+
return null;
|
|
865
|
+
if (this.isTeamPlanVersion(entitlement.version))
|
|
866
|
+
return null;
|
|
867
|
+
const defaultPersonalVersion = this.tryGetDefaultPlanVersionByMode('personal');
|
|
868
|
+
if (!defaultPersonalVersion)
|
|
869
|
+
return null;
|
|
870
|
+
if (!this.planVersionHasActiveFreePricing(defaultPersonalVersion.planVersionId))
|
|
871
|
+
return null;
|
|
872
|
+
if (String(entitlement.planId || '').trim().toLowerCase() !== defaultPersonalVersion.planId)
|
|
873
|
+
return null;
|
|
874
|
+
if (entitlement.planVersionId === defaultPersonalVersion.planVersionId)
|
|
875
|
+
return null;
|
|
876
|
+
if (entitlement.version.versionNumber >= defaultPersonalVersion.versionNumber)
|
|
877
|
+
return null;
|
|
878
|
+
return this.transitionCanonicalCommercialState({
|
|
879
|
+
accountId,
|
|
880
|
+
planVersionId: defaultPersonalVersion.planVersionId,
|
|
881
|
+
state: 'active',
|
|
882
|
+
effectiveAt: entitlement.effectiveAt,
|
|
883
|
+
effectiveUntil: null,
|
|
884
|
+
billingMirrorPlanId: defaultPersonalVersion.planId,
|
|
885
|
+
source: 'system',
|
|
886
|
+
updatedBy: 'legacy-free-auto-heal',
|
|
887
|
+
createdAt: entitlement.effectiveAt
|
|
888
|
+
});
|
|
889
|
+
}
|
|
847
890
|
getAccountAccessStatus(accountId) {
|
|
848
891
|
const normalizedAccountId = String(accountId || '').trim();
|
|
849
892
|
if (!normalizedAccountId) {
|
|
@@ -851,7 +894,10 @@ export class PlanEntitlementService {
|
|
|
851
894
|
}
|
|
852
895
|
const onboardingPolicy = this.getOnboardingPolicy();
|
|
853
896
|
const validation = this.validateOnboardingPolicy(onboardingPolicy);
|
|
854
|
-
const
|
|
897
|
+
const currentEntitlement = this.tryGetAccountEntitlement(normalizedAccountId);
|
|
898
|
+
const entitlement = currentEntitlement
|
|
899
|
+
? (this.tryAutoHealLegacyPersonalFreeEntitlement(normalizedAccountId, currentEntitlement) || currentEntitlement)
|
|
900
|
+
: null;
|
|
855
901
|
if (!entitlement) {
|
|
856
902
|
return !validation.valid
|
|
857
903
|
? this.buildBlockedAccessStatus({
|
|
@@ -989,6 +1035,22 @@ export class PlanEntitlementService {
|
|
|
989
1035
|
const remaining = limit === null ? null : Math.max(0, limit - used);
|
|
990
1036
|
return { used, limit, remaining };
|
|
991
1037
|
}
|
|
1038
|
+
countAiProfileUsage(workspaceId) {
|
|
1039
|
+
const normalizedWorkspaceId = String(workspaceId || '').trim();
|
|
1040
|
+
if (!normalizedWorkspaceId) {
|
|
1041
|
+
throw new TaskforceRuleError('workspaceId is required', 400, 'WORKSPACE_ID_REQUIRED');
|
|
1042
|
+
}
|
|
1043
|
+
const row = this.db.prepare(`
|
|
1044
|
+
SELECT COUNT(*) AS c
|
|
1045
|
+
FROM ai_profiles
|
|
1046
|
+
WHERE tenant_id = ?
|
|
1047
|
+
AND workspace_id = ?
|
|
1048
|
+
`).get(this.tenantId, normalizedWorkspaceId);
|
|
1049
|
+
const used = Math.max(0, Number(row?.c || 0));
|
|
1050
|
+
const { limit } = this.resolveAiProfileLimitForWorkspace(normalizedWorkspaceId);
|
|
1051
|
+
const remaining = limit === null ? null : Math.max(0, limit - used);
|
|
1052
|
+
return { used, limit, remaining };
|
|
1053
|
+
}
|
|
992
1054
|
assertSeatAvailable(workspaceId) {
|
|
993
1055
|
const seatUsage = this.countWorkspaceSeatUsage(workspaceId, { includeInvited: true });
|
|
994
1056
|
if (seatUsage.limit !== null && seatUsage.used >= seatUsage.limit) {
|
|
@@ -1026,6 +1088,19 @@ export class PlanEntitlementService {
|
|
|
1026
1088
|
}
|
|
1027
1089
|
return { allowed: true, reasonCode: 'OK', state: entitlement.state, seatUsage, feature };
|
|
1028
1090
|
}
|
|
1091
|
+
if (featureKey === DOCUMENT_CLOUD_STORAGE_FEATURE_KEY && feature.access === 'limited') {
|
|
1092
|
+
const configuredLimitMb = this.readPositiveIntegerConfig(feature.config?.[DOCUMENT_CLOUD_STORAGE_LIMIT_CONFIG_KEY], 1) ?? 1;
|
|
1093
|
+
const storageLimitBytes = configuredLimitMb * 1024 * 1024;
|
|
1094
|
+
const rawRequestedBytes = input.context?.requestedBytes ?? input.context?.sizeBytes ?? input.context?.pendingSizeBytes;
|
|
1095
|
+
const requestedBytesNumber = Number(rawRequestedBytes);
|
|
1096
|
+
const requestedBytes = Number.isFinite(requestedBytesNumber)
|
|
1097
|
+
? Math.max(0, Math.floor(requestedBytesNumber))
|
|
1098
|
+
: 0;
|
|
1099
|
+
const usedBytes = this.countWorkspaceStorageUsageBytes(workspaceId);
|
|
1100
|
+
if ((usedBytes + requestedBytes) > storageLimitBytes) {
|
|
1101
|
+
return { allowed: false, reasonCode: 'UPLOAD_STORAGE_LIMIT_REACHED', state: entitlement.state, feature };
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1029
1104
|
return { allowed: true, reasonCode: 'OK', state: entitlement.state, feature };
|
|
1030
1105
|
}
|
|
1031
1106
|
getUserAccountPlanMode(userId) {
|
|
@@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest';
|
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import path from 'path';
|
|
5
|
-
import { TaskforceCore
|
|
5
|
+
import { TaskforceCore } from './Taskforce';
|
|
6
6
|
function makeProjectRoot() {
|
|
7
7
|
return fs.mkdtempSync(path.join(os.tmpdir(), 'taskforce-plan-version-policy-'));
|
|
8
8
|
}
|
|
@@ -11,8 +11,8 @@ function openCore(projectRoot) {
|
|
|
11
11
|
projectRoot,
|
|
12
12
|
storagePath: path.join(projectRoot, '.taskforce')
|
|
13
13
|
});
|
|
14
|
-
core.upsertPlanCatalog({ planId: '
|
|
15
|
-
core.upsertPlanCatalog({ planId: '
|
|
14
|
+
core.upsertPlanCatalog({ planId: 'free', displayName: 'Free', status: 'archived' });
|
|
15
|
+
core.upsertPlanCatalog({ planId: 'team-5', displayName: 'Team 5', status: 'archived' });
|
|
16
16
|
return core;
|
|
17
17
|
}
|
|
18
18
|
describe('TaskforceCore plan version policy', () => {
|
|
@@ -20,17 +20,17 @@ describe('TaskforceCore plan version policy', () => {
|
|
|
20
20
|
const projectRoot = makeProjectRoot();
|
|
21
21
|
const core = openCore(projectRoot);
|
|
22
22
|
try {
|
|
23
|
-
core.createPlanVersion({ planId: '
|
|
23
|
+
core.createPlanVersion({ planId: 'free', versionNumber: 2, isDefault: false });
|
|
24
24
|
const db = core.db;
|
|
25
25
|
db.prepare(`
|
|
26
26
|
UPDATE plan_versions
|
|
27
27
|
SET is_default = 0
|
|
28
28
|
WHERE tenant_id = ? AND plan_id = ?
|
|
29
|
-
`).run('default', '
|
|
30
|
-
const versions = core.listPlanVersions('
|
|
29
|
+
`).run('default', 'free');
|
|
30
|
+
const versions = core.listPlanVersions('free');
|
|
31
31
|
const defaults = versions.filter((version) => version.isDefault);
|
|
32
32
|
expect(defaults).toHaveLength(1);
|
|
33
|
-
expect(defaults[0].planVersionId).toBe('
|
|
33
|
+
expect(defaults[0].planVersionId).toBe('free-v2');
|
|
34
34
|
}
|
|
35
35
|
finally {
|
|
36
36
|
core.close();
|
|
@@ -43,11 +43,11 @@ describe('TaskforceCore plan version policy', () => {
|
|
|
43
43
|
try {
|
|
44
44
|
expect(() => {
|
|
45
45
|
core.updatePlanVersion({
|
|
46
|
-
planId: '
|
|
46
|
+
planId: 'free',
|
|
47
47
|
versionNumber: 1,
|
|
48
48
|
isDefault: false
|
|
49
49
|
});
|
|
50
|
-
}).toThrowError(
|
|
50
|
+
}).toThrowError(/exactly one default version/i);
|
|
51
51
|
}
|
|
52
52
|
finally {
|
|
53
53
|
core.close();
|
|
@@ -59,13 +59,13 @@ describe('TaskforceCore plan version policy', () => {
|
|
|
59
59
|
const core = openCore(projectRoot);
|
|
60
60
|
try {
|
|
61
61
|
core.createPlanVersion({
|
|
62
|
-
planId: '
|
|
62
|
+
planId: 'team-5',
|
|
63
63
|
versionNumber: 2,
|
|
64
64
|
isDefault: true,
|
|
65
65
|
seatLimit: 3
|
|
66
66
|
});
|
|
67
67
|
core.updatePlanVersionFeatures({
|
|
68
|
-
planVersionId: '
|
|
68
|
+
planVersionId: 'team-5-v2',
|
|
69
69
|
features: [
|
|
70
70
|
{
|
|
71
71
|
featureKey: 'collaboration.seat_limit',
|
|
@@ -74,7 +74,7 @@ describe('TaskforceCore plan version policy', () => {
|
|
|
74
74
|
}
|
|
75
75
|
]
|
|
76
76
|
});
|
|
77
|
-
const version = core.getPlanVersionFeatures('
|
|
77
|
+
const version = core.getPlanVersionFeatures('team-5', 'team-5-v2');
|
|
78
78
|
expect(version.seatLimit).toBe(3);
|
|
79
79
|
expect(version.features['collaboration.seat_limit']?.config || {}).toEqual({});
|
|
80
80
|
}
|
|
@@ -109,19 +109,19 @@ describe('TaskforceCore plan version policy', () => {
|
|
|
109
109
|
const core = openCore(projectRoot);
|
|
110
110
|
try {
|
|
111
111
|
core.updatePlanVersion({
|
|
112
|
-
planId: '
|
|
112
|
+
planId: 'team-5',
|
|
113
113
|
versionNumber: 1,
|
|
114
114
|
seatLimit: 5
|
|
115
115
|
});
|
|
116
116
|
core.updatePlanVersionFeatures({
|
|
117
|
-
planVersionId: '
|
|
117
|
+
planVersionId: 'team-5-v1',
|
|
118
118
|
features: [
|
|
119
119
|
{ featureKey: 'collaboration.invites', access: 'enabled', config: {} },
|
|
120
120
|
{ featureKey: 'collaboration.team_management', access: 'enabled', config: {} }
|
|
121
121
|
]
|
|
122
122
|
});
|
|
123
123
|
const version = core.updatePlanVersionFeatures({
|
|
124
|
-
planVersionId: '
|
|
124
|
+
planVersionId: 'team-5-v1',
|
|
125
125
|
features: [
|
|
126
126
|
{ featureKey: 'collaboration.team_management', access: 'disabled', config: {} }
|
|
127
127
|
]
|
|
@@ -139,7 +139,7 @@ describe('TaskforceCore plan version policy', () => {
|
|
|
139
139
|
const core = openCore(projectRoot);
|
|
140
140
|
try {
|
|
141
141
|
const version = core.updatePlanVersionFeatures({
|
|
142
|
-
planVersionId: '
|
|
142
|
+
planVersionId: 'free-v1',
|
|
143
143
|
features: [
|
|
144
144
|
{ featureKey: 'workspace.switching', access: 'limited', config: { maxWorkspaces: 0 } },
|
|
145
145
|
{ featureKey: 'context.uploads', access: 'limited', config: { storageLimitMb: -50 } }
|
|
@@ -153,22 +153,15 @@ describe('TaskforceCore plan version policy', () => {
|
|
|
153
153
|
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
154
154
|
}
|
|
155
155
|
});
|
|
156
|
-
it('starts new plans disabled
|
|
156
|
+
it('starts new plans disabled before pricing is configured', () => {
|
|
157
157
|
const projectRoot = makeProjectRoot();
|
|
158
158
|
const core = openCore(projectRoot);
|
|
159
159
|
try {
|
|
160
160
|
const created = core.upsertPlanCatalog({
|
|
161
|
-
planId: '
|
|
162
|
-
displayName: '
|
|
161
|
+
planId: 'agency',
|
|
162
|
+
displayName: 'Agency'
|
|
163
163
|
});
|
|
164
164
|
expect(created.status).toBe('archived');
|
|
165
|
-
expect(() => {
|
|
166
|
-
core.upsertPlanCatalog({
|
|
167
|
-
planId: 'enterprise',
|
|
168
|
-
displayName: 'Enterprise',
|
|
169
|
-
status: 'active'
|
|
170
|
-
});
|
|
171
|
-
}).toThrowError(TaskforceRuleError);
|
|
172
165
|
}
|
|
173
166
|
finally {
|
|
174
167
|
core.close();
|