@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
|
@@ -1415,7 +1415,7 @@ describe('Billing Webhook Routes', () => {
|
|
|
1415
1415
|
stripe_subscription_id: 'sub_email_match'
|
|
1416
1416
|
}));
|
|
1417
1417
|
});
|
|
1418
|
-
it('updates the existing paid subscription
|
|
1418
|
+
it('updates the existing paid subscription and keeps canonical entitlement aligned with the new plan', async () => {
|
|
1419
1419
|
const now = new Date().toISOString();
|
|
1420
1420
|
db.prepare(`
|
|
1421
1421
|
INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
|
|
@@ -1476,12 +1476,26 @@ describe('Billing Webhook Routes', () => {
|
|
|
1476
1476
|
planVersionId: 'team-v1'
|
|
1477
1477
|
}));
|
|
1478
1478
|
const entitlement = db.prepare(`
|
|
1479
|
-
SELECT plan_version_id
|
|
1479
|
+
SELECT plan_version_id, entitlement_state, updated_by
|
|
1480
1480
|
FROM account_entitlements
|
|
1481
1481
|
WHERE tenant_id = 'default' AND account_id = 'u1'
|
|
1482
1482
|
LIMIT 1
|
|
1483
1483
|
`).get();
|
|
1484
|
-
expect(entitlement
|
|
1484
|
+
expect(entitlement).toEqual(expect.objectContaining({
|
|
1485
|
+
plan_version_id: 'team-v1',
|
|
1486
|
+
entitlement_state: 'active',
|
|
1487
|
+
updated_by: 'checkout-confirm'
|
|
1488
|
+
}));
|
|
1489
|
+
const billingRow = db.prepare(`
|
|
1490
|
+
SELECT stripe_price_id, billing_interval
|
|
1491
|
+
FROM user_billing
|
|
1492
|
+
WHERE tenant_id = 'default' AND user_id = 'u1'
|
|
1493
|
+
LIMIT 1
|
|
1494
|
+
`).get();
|
|
1495
|
+
expect(billingRow).toEqual(expect.objectContaining({
|
|
1496
|
+
stripe_price_id: 'price_team_month',
|
|
1497
|
+
billing_interval: 'month'
|
|
1498
|
+
}));
|
|
1485
1499
|
});
|
|
1486
1500
|
it('updates the existing subscription when only the interval changes on the same plan', async () => {
|
|
1487
1501
|
const now = new Date().toISOString();
|
|
@@ -1544,7 +1558,7 @@ describe('Billing Webhook Routes', () => {
|
|
|
1544
1558
|
planVersionId: 'pro-v1'
|
|
1545
1559
|
}));
|
|
1546
1560
|
});
|
|
1547
|
-
it('confirms a successful checkout session and
|
|
1561
|
+
it('confirms a successful checkout session, activates canonical entitlement, and closes the checkout attempt', async () => {
|
|
1548
1562
|
const now = new Date().toISOString();
|
|
1549
1563
|
db.prepare(`
|
|
1550
1564
|
INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
|
|
@@ -1554,6 +1568,11 @@ describe('Billing Webhook Routes', () => {
|
|
|
1554
1568
|
INSERT INTO billing_price_mappings (tenant_id, plan_version_id, billing_interval, stripe_price_id, active, created_at, updated_at)
|
|
1555
1569
|
VALUES ('default', 'pro-v1', 'month', 'price_pro_month', 1, ?, ?)
|
|
1556
1570
|
`).run(now, now);
|
|
1571
|
+
db.prepare(`
|
|
1572
|
+
INSERT INTO billing_checkout_attempts (
|
|
1573
|
+
session_id, tenant_id, user_id, plan_version_id, billing_interval, status, expires_at, created_at, updated_at
|
|
1574
|
+
) VALUES ('cs_success_123', 'default', 'u1', 'pro-v1', 'month', 'open', ?, ?, ?)
|
|
1575
|
+
`).run(new Date(Date.now() + 60 * 60 * 1000).toISOString(), now, now);
|
|
1557
1576
|
const confirmHandler = routeHandlers.get('POST /api/taskforce/billing/checkout-session/confirm');
|
|
1558
1577
|
expect(confirmHandler).toBeTypeOf('function');
|
|
1559
1578
|
const req = createMockReq(JSON.stringify({
|
|
@@ -1570,14 +1589,36 @@ describe('Billing Webhook Routes', () => {
|
|
|
1570
1589
|
}));
|
|
1571
1590
|
expect(mockSubscriptionRetrieve).toHaveBeenCalledWith('sub_123');
|
|
1572
1591
|
const entitlement = db.prepare(`
|
|
1573
|
-
SELECT plan_version_id, entitlement_state
|
|
1592
|
+
SELECT plan_version_id, entitlement_state, updated_by
|
|
1574
1593
|
FROM account_entitlements
|
|
1575
1594
|
WHERE tenant_id = 'default' AND account_id = 'u1'
|
|
1576
1595
|
LIMIT 1
|
|
1577
1596
|
`).get();
|
|
1578
1597
|
expect(entitlement).toEqual(expect.objectContaining({
|
|
1579
1598
|
plan_version_id: 'pro-v1',
|
|
1580
|
-
entitlement_state: 'active'
|
|
1599
|
+
entitlement_state: 'active',
|
|
1600
|
+
updated_by: 'checkout-confirm'
|
|
1601
|
+
}));
|
|
1602
|
+
const billingRow = db.prepare(`
|
|
1603
|
+
SELECT stripe_customer_id, stripe_subscription_id, stripe_price_id, stripe_status
|
|
1604
|
+
FROM user_billing
|
|
1605
|
+
WHERE tenant_id = 'default' AND user_id = 'u1'
|
|
1606
|
+
LIMIT 1
|
|
1607
|
+
`).get();
|
|
1608
|
+
expect(billingRow).toEqual(expect.objectContaining({
|
|
1609
|
+
stripe_customer_id: 'cus_123',
|
|
1610
|
+
stripe_subscription_id: 'sub_123',
|
|
1611
|
+
stripe_price_id: 'price_pro_month',
|
|
1612
|
+
stripe_status: 'active'
|
|
1613
|
+
}));
|
|
1614
|
+
const attempt = db.prepare(`
|
|
1615
|
+
SELECT status
|
|
1616
|
+
FROM billing_checkout_attempts
|
|
1617
|
+
WHERE tenant_id = 'default' AND session_id = 'cs_success_123'
|
|
1618
|
+
LIMIT 1
|
|
1619
|
+
`).get();
|
|
1620
|
+
expect(attempt).toEqual(expect.objectContaining({
|
|
1621
|
+
status: 'confirmed'
|
|
1581
1622
|
}));
|
|
1582
1623
|
});
|
|
1583
1624
|
it('does not activate entitlement when the checkout session is still incomplete', async () => {
|
|
@@ -1631,6 +1672,156 @@ describe('Billing Webhook Routes', () => {
|
|
|
1631
1672
|
updated_by: 'checkout-start'
|
|
1632
1673
|
}));
|
|
1633
1674
|
});
|
|
1675
|
+
it('keeps checkout confirmation retryable until Stripe attaches the subscription to the completed session', async () => {
|
|
1676
|
+
const now = new Date().toISOString();
|
|
1677
|
+
db.prepare(`
|
|
1678
|
+
INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
|
|
1679
|
+
VALUES ('free-v1', 'default', 'free', 1, 1, '{}', ?, ?)
|
|
1680
|
+
`).run(now, now);
|
|
1681
|
+
db.prepare(`
|
|
1682
|
+
INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
|
|
1683
|
+
VALUES ('pro-v1', 'default', 'pro', 1, 0, '{}', ?, ?)
|
|
1684
|
+
`).run(now, now);
|
|
1685
|
+
db.prepare(`
|
|
1686
|
+
INSERT INTO account_entitlements (
|
|
1687
|
+
account_id, tenant_id, plan_version_id, entitlement_state, effective_at, updated_by, created_at, updated_at
|
|
1688
|
+
) VALUES ('u1', 'default', 'free-v1', 'active', ?, 'registration', ?, ?)
|
|
1689
|
+
`).run(now, now, now);
|
|
1690
|
+
db.prepare(`
|
|
1691
|
+
INSERT INTO billing_checkout_attempts (
|
|
1692
|
+
session_id, tenant_id, user_id, plan_version_id, billing_interval, status, expires_at, created_at, updated_at
|
|
1693
|
+
) VALUES ('cs_pending_subscription', 'default', 'u1', 'pro-v1', 'month', 'open', ?, ?, ?)
|
|
1694
|
+
`).run(new Date(Date.now() + 60 * 60 * 1000).toISOString(), now, now);
|
|
1695
|
+
mockCheckoutRetrieve.mockResolvedValueOnce({
|
|
1696
|
+
id: 'cs_pending_subscription',
|
|
1697
|
+
mode: 'subscription',
|
|
1698
|
+
status: 'complete',
|
|
1699
|
+
payment_status: 'paid',
|
|
1700
|
+
customer: 'cus_123',
|
|
1701
|
+
subscription: null,
|
|
1702
|
+
metadata: { userId: 'u1', planVersionId: 'pro-v1' }
|
|
1703
|
+
});
|
|
1704
|
+
const confirmHandler = routeHandlers.get('POST /api/taskforce/billing/checkout-session/confirm');
|
|
1705
|
+
const req = createMockReq(JSON.stringify({
|
|
1706
|
+
sessionId: 'cs_pending_subscription'
|
|
1707
|
+
}), {
|
|
1708
|
+
'x-taskforce-runtime-mode': 'cloud',
|
|
1709
|
+
host: 'app.taskforcehq.ai'
|
|
1710
|
+
});
|
|
1711
|
+
const res = createMockRes();
|
|
1712
|
+
await confirmHandler(req, res, {});
|
|
1713
|
+
expect(res.statusCode).toBe(409);
|
|
1714
|
+
expect(JSON.parse(res.body)).toEqual({
|
|
1715
|
+
error: 'Checkout session is still finalizing your subscription.',
|
|
1716
|
+
code: 'CHECKOUT_SESSION_INCOMPLETE'
|
|
1717
|
+
});
|
|
1718
|
+
expect(mockSubscriptionRetrieve).not.toHaveBeenCalled();
|
|
1719
|
+
const entitlement = db.prepare(`
|
|
1720
|
+
SELECT plan_version_id, entitlement_state
|
|
1721
|
+
FROM account_entitlements
|
|
1722
|
+
WHERE tenant_id = 'default' AND account_id = 'u1'
|
|
1723
|
+
LIMIT 1
|
|
1724
|
+
`).get();
|
|
1725
|
+
expect(entitlement).toEqual(expect.objectContaining({
|
|
1726
|
+
plan_version_id: 'free-v1',
|
|
1727
|
+
entitlement_state: 'active'
|
|
1728
|
+
}));
|
|
1729
|
+
const attempt = db.prepare(`
|
|
1730
|
+
SELECT status
|
|
1731
|
+
FROM billing_checkout_attempts
|
|
1732
|
+
WHERE tenant_id = 'default' AND session_id = 'cs_pending_subscription'
|
|
1733
|
+
LIMIT 1
|
|
1734
|
+
`).get();
|
|
1735
|
+
expect(attempt).toEqual(expect.objectContaining({
|
|
1736
|
+
status: 'open'
|
|
1737
|
+
}));
|
|
1738
|
+
});
|
|
1739
|
+
it('keeps checkout confirmation retryable until the subscription settles into an active commercial state', async () => {
|
|
1740
|
+
const now = new Date().toISOString();
|
|
1741
|
+
db.prepare(`
|
|
1742
|
+
INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
|
|
1743
|
+
VALUES ('free-v1', 'default', 'free', 1, 1, '{}', ?, ?)
|
|
1744
|
+
`).run(now, now);
|
|
1745
|
+
db.prepare(`
|
|
1746
|
+
INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
|
|
1747
|
+
VALUES ('pro-v1', 'default', 'pro', 1, 0, '{}', ?, ?)
|
|
1748
|
+
`).run(now, now);
|
|
1749
|
+
db.prepare(`
|
|
1750
|
+
INSERT INTO billing_price_mappings (tenant_id, plan_version_id, billing_interval, stripe_price_id, active, created_at, updated_at)
|
|
1751
|
+
VALUES ('default', 'pro-v1', 'month', 'price_pro_month', 1, ?, ?)
|
|
1752
|
+
`).run(now, now);
|
|
1753
|
+
db.prepare(`
|
|
1754
|
+
INSERT INTO user_billing (
|
|
1755
|
+
user_id, tenant_id, tier, entitlement_state, created_at, updated_at
|
|
1756
|
+
) VALUES ('u1', 'default', 'free', 'active', ?, ?)
|
|
1757
|
+
`).run(now, now);
|
|
1758
|
+
db.prepare(`
|
|
1759
|
+
INSERT INTO account_entitlements (
|
|
1760
|
+
account_id, tenant_id, plan_version_id, entitlement_state, effective_at, updated_by, created_at, updated_at
|
|
1761
|
+
) VALUES ('u1', 'default', 'free-v1', 'active', ?, 'registration', ?, ?)
|
|
1762
|
+
`).run(now, now, now);
|
|
1763
|
+
db.prepare(`
|
|
1764
|
+
INSERT INTO billing_checkout_attempts (
|
|
1765
|
+
session_id, tenant_id, user_id, plan_version_id, billing_interval, status, expires_at, created_at, updated_at
|
|
1766
|
+
) VALUES ('cs_wait_for_active', 'default', 'u1', 'pro-v1', 'month', 'open', ?, ?, ?)
|
|
1767
|
+
`).run(new Date(Date.now() + 60 * 60 * 1000).toISOString(), now, now);
|
|
1768
|
+
mockCheckoutRetrieve.mockResolvedValueOnce({
|
|
1769
|
+
id: 'cs_wait_for_active',
|
|
1770
|
+
mode: 'subscription',
|
|
1771
|
+
status: 'complete',
|
|
1772
|
+
payment_status: 'paid',
|
|
1773
|
+
customer: 'cus_123',
|
|
1774
|
+
subscription: 'sub_123',
|
|
1775
|
+
metadata: { userId: 'u1', planVersionId: 'pro-v1' }
|
|
1776
|
+
});
|
|
1777
|
+
mockSubscriptionRetrieve.mockResolvedValueOnce({
|
|
1778
|
+
id: 'sub_123',
|
|
1779
|
+
customer: 'cus_123',
|
|
1780
|
+
status: 'incomplete',
|
|
1781
|
+
trial_end: null,
|
|
1782
|
+
metadata: { userId: 'u1', planVersionId: 'pro-v1' },
|
|
1783
|
+
items: {
|
|
1784
|
+
data: [{
|
|
1785
|
+
id: 'si_123',
|
|
1786
|
+
current_period_end: Math.floor(Date.now() / 1000) + 86400,
|
|
1787
|
+
price: { id: 'price_pro_month', recurring: { interval: 'month' } }
|
|
1788
|
+
}]
|
|
1789
|
+
}
|
|
1790
|
+
});
|
|
1791
|
+
const confirmHandler = routeHandlers.get('POST /api/taskforce/billing/checkout-session/confirm');
|
|
1792
|
+
const req = createMockReq(JSON.stringify({
|
|
1793
|
+
sessionId: 'cs_wait_for_active'
|
|
1794
|
+
}), {
|
|
1795
|
+
'x-taskforce-runtime-mode': 'cloud',
|
|
1796
|
+
host: 'app.taskforcehq.ai'
|
|
1797
|
+
});
|
|
1798
|
+
const res = createMockRes();
|
|
1799
|
+
await confirmHandler(req, res, {});
|
|
1800
|
+
expect(res.statusCode).toBe(409);
|
|
1801
|
+
expect(JSON.parse(res.body)).toEqual({
|
|
1802
|
+
error: 'Checkout session is still finalizing your plan.',
|
|
1803
|
+
code: 'CHECKOUT_SESSION_INCOMPLETE'
|
|
1804
|
+
});
|
|
1805
|
+
const entitlement = db.prepare(`
|
|
1806
|
+
SELECT plan_version_id, entitlement_state
|
|
1807
|
+
FROM account_entitlements
|
|
1808
|
+
WHERE tenant_id = 'default' AND account_id = 'u1'
|
|
1809
|
+
LIMIT 1
|
|
1810
|
+
`).get();
|
|
1811
|
+
expect(entitlement).toEqual(expect.objectContaining({
|
|
1812
|
+
plan_version_id: 'free-v1',
|
|
1813
|
+
entitlement_state: 'active'
|
|
1814
|
+
}));
|
|
1815
|
+
const attempt = db.prepare(`
|
|
1816
|
+
SELECT status
|
|
1817
|
+
FROM billing_checkout_attempts
|
|
1818
|
+
WHERE tenant_id = 'default' AND session_id = 'cs_wait_for_active'
|
|
1819
|
+
LIMIT 1
|
|
1820
|
+
`).get();
|
|
1821
|
+
expect(attempt).toEqual(expect.objectContaining({
|
|
1822
|
+
status: 'open'
|
|
1823
|
+
}));
|
|
1824
|
+
});
|
|
1634
1825
|
it('rejects checkout confirmation when a newer pending checkout superseded the session plan', async () => {
|
|
1635
1826
|
const now = new Date().toISOString();
|
|
1636
1827
|
db.prepare(`
|
|
@@ -1928,14 +2119,7 @@ describe('Billing Webhook Routes', () => {
|
|
|
1928
2119
|
`).get();
|
|
1929
2120
|
expect(purged).toBeUndefined();
|
|
1930
2121
|
});
|
|
1931
|
-
it('
|
|
1932
|
-
mockPricesList.mockResolvedValueOnce({
|
|
1933
|
-
data: [
|
|
1934
|
-
{ id: 'price_month_1', active: true, currency: 'usd', unit_amount: 1200, recurring: { interval: 'month' }, nickname: 'Monthly', product: 'prod_1' },
|
|
1935
|
-
{ id: 'price_year_1', active: true, currency: 'usd', unit_amount: 12000, recurring: { interval: 'year' }, nickname: 'Yearly', product: 'prod_1' }
|
|
1936
|
-
],
|
|
1937
|
-
has_more: false
|
|
1938
|
-
});
|
|
2122
|
+
it('rejects manual stripe price option listing now that pricing is import-only', async () => {
|
|
1939
2123
|
const route = routeHandlers.get('GET /api/taskforce/admin/billing/stripe-price-options');
|
|
1940
2124
|
expect(route).toBeTypeOf('function');
|
|
1941
2125
|
const req = createMockReq('', {
|
|
@@ -1944,16 +2128,11 @@ describe('Billing Webhook Routes', () => {
|
|
|
1944
2128
|
req.url = '/api/taskforce/admin/billing/stripe-price-options?interval=month';
|
|
1945
2129
|
const res = createMockRes();
|
|
1946
2130
|
await route(req, res, {});
|
|
1947
|
-
expect(res.statusCode).toBe(
|
|
2131
|
+
expect(res.statusCode).toBe(410);
|
|
1948
2132
|
const body = JSON.parse(res.body);
|
|
1949
|
-
expect(
|
|
1950
|
-
expect(body.options.length).toBe(1);
|
|
1951
|
-
expect(body.options[0]).toEqual(expect.objectContaining({
|
|
1952
|
-
stripePriceId: 'price_month_1',
|
|
1953
|
-
interval: 'month'
|
|
1954
|
-
}));
|
|
2133
|
+
expect(body.code).toBe('BILLING_PRICE_MANUAL_MAPPING_REMOVED');
|
|
1955
2134
|
});
|
|
1956
|
-
it('keeps active
|
|
2135
|
+
it('keeps active imported price mappings during Stripe import when the Stripe price is still active but lacks plan metadata', async () => {
|
|
1957
2136
|
const now = new Date().toISOString();
|
|
1958
2137
|
db.prepare(`
|
|
1959
2138
|
INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
|
|
@@ -1992,23 +2171,12 @@ describe('Billing Webhook Routes', () => {
|
|
|
1992
2171
|
`).get();
|
|
1993
2172
|
expect(row.active).toBe(1);
|
|
1994
2173
|
});
|
|
1995
|
-
it('
|
|
2174
|
+
it('rejects manual price mapping upserts now that pricing is import-only', async () => {
|
|
1996
2175
|
const now = new Date().toISOString();
|
|
1997
2176
|
db.prepare(`
|
|
1998
2177
|
INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
|
|
1999
2178
|
VALUES ('collab-v1', 'default', 'collab', 1, 1, '{}', ?, ?)
|
|
2000
2179
|
`).run(now, now);
|
|
2001
|
-
db.prepare(`
|
|
2002
|
-
INSERT INTO billing_price_mappings (tenant_id, plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency, created_at, updated_at)
|
|
2003
|
-
VALUES ('default', 'collab-v1', 'month', 'price_old', 'stripe', 1, 1100, 'usd', ?, ?)
|
|
2004
|
-
`).run(now, now);
|
|
2005
|
-
mockPriceRetrieve.mockResolvedValueOnce({
|
|
2006
|
-
id: 'price_new',
|
|
2007
|
-
active: true,
|
|
2008
|
-
currency: 'usd',
|
|
2009
|
-
unit_amount: 1500,
|
|
2010
|
-
recurring: { interval: 'month' }
|
|
2011
|
-
});
|
|
2012
2180
|
const upsertRoute = routeHandlers.get('PUT /api/taskforce/admin/billing/price-mappings');
|
|
2013
2181
|
expect(upsertRoute).toBeTypeOf('function');
|
|
2014
2182
|
const upsertReq = createMockReq(JSON.stringify({
|
|
@@ -2019,19 +2187,12 @@ describe('Billing Webhook Routes', () => {
|
|
|
2019
2187
|
}), { 'x-taskforce-runtime-mode': 'cloud' });
|
|
2020
2188
|
const upsertRes = createMockRes();
|
|
2021
2189
|
await upsertRoute(upsertReq, upsertRes, {});
|
|
2022
|
-
expect(upsertRes.statusCode).toBe(
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
WHERE tenant_id = 'default' AND plan_version_id = 'collab-v1' AND billing_interval = 'month'
|
|
2027
|
-
ORDER BY stripe_price_id ASC
|
|
2028
|
-
`).all();
|
|
2029
|
-
expect(rows).toEqual([
|
|
2030
|
-
{ stripe_price_id: 'price_new', active: 1 },
|
|
2031
|
-
{ stripe_price_id: 'price_old', active: 0 }
|
|
2032
|
-
]);
|
|
2190
|
+
expect(upsertRes.statusCode).toBe(410);
|
|
2191
|
+
expect(JSON.parse(upsertRes.body)).toEqual(expect.objectContaining({
|
|
2192
|
+
code: 'BILLING_PRICE_MANUAL_MAPPING_REMOVED'
|
|
2193
|
+
}));
|
|
2033
2194
|
});
|
|
2034
|
-
it('
|
|
2195
|
+
it('rejects manual reuse of a Stripe price id across multiple plan versions', async () => {
|
|
2035
2196
|
const now = new Date().toISOString();
|
|
2036
2197
|
db.prepare(`
|
|
2037
2198
|
INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
|
|
@@ -2041,13 +2202,6 @@ describe('Billing Webhook Routes', () => {
|
|
|
2041
2202
|
INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
|
|
2042
2203
|
VALUES ('team-v1', 'default', 'team', 1, 1, '{}', ?, ?)
|
|
2043
2204
|
`).run(now, now);
|
|
2044
|
-
mockPriceRetrieve.mockResolvedValue({
|
|
2045
|
-
id: 'price_shared',
|
|
2046
|
-
active: true,
|
|
2047
|
-
currency: 'usd',
|
|
2048
|
-
unit_amount: 1900,
|
|
2049
|
-
recurring: { interval: 'month' }
|
|
2050
|
-
});
|
|
2051
2205
|
const upsertRoute = routeHandlers.get('PUT /api/taskforce/admin/billing/price-mappings');
|
|
2052
2206
|
const firstReq = createMockReq(JSON.stringify({
|
|
2053
2207
|
planVersionId: 'collab-v1',
|
|
@@ -2057,28 +2211,9 @@ describe('Billing Webhook Routes', () => {
|
|
|
2057
2211
|
}), { 'x-taskforce-runtime-mode': 'cloud' });
|
|
2058
2212
|
const firstRes = createMockRes();
|
|
2059
2213
|
await upsertRoute(firstReq, firstRes, {});
|
|
2060
|
-
expect(firstRes.statusCode).toBe(
|
|
2061
|
-
const secondReq = createMockReq(JSON.stringify({
|
|
2062
|
-
planVersionId: 'team-v1',
|
|
2063
|
-
interval: 'month',
|
|
2064
|
-
stripePriceId: 'price_shared',
|
|
2065
|
-
active: true
|
|
2066
|
-
}), { 'x-taskforce-runtime-mode': 'cloud' });
|
|
2067
|
-
const secondRes = createMockRes();
|
|
2068
|
-
await upsertRoute(secondReq, secondRes, {});
|
|
2069
|
-
expect(secondRes.statusCode).toBe(200);
|
|
2070
|
-
const rows = db.prepare(`
|
|
2071
|
-
SELECT plan_version_id, stripe_price_id, active
|
|
2072
|
-
FROM billing_price_mappings
|
|
2073
|
-
WHERE tenant_id = 'default' AND stripe_price_id = 'price_shared'
|
|
2074
|
-
ORDER BY plan_version_id ASC
|
|
2075
|
-
`).all();
|
|
2076
|
-
expect(rows).toEqual([
|
|
2077
|
-
{ plan_version_id: 'collab-v1', stripe_price_id: 'price_shared', active: 1 },
|
|
2078
|
-
{ plan_version_id: 'team-v1', stripe_price_id: 'price_shared', active: 1 }
|
|
2079
|
-
]);
|
|
2214
|
+
expect(firstRes.statusCode).toBe(410);
|
|
2080
2215
|
});
|
|
2081
|
-
it('keeps reused mappings
|
|
2216
|
+
it('keeps reused active mappings during Stripe import when the shared Stripe price is still active', async () => {
|
|
2082
2217
|
const now = new Date().toISOString();
|
|
2083
2218
|
db.prepare(`
|
|
2084
2219
|
INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
|
|
@@ -2125,7 +2260,7 @@ describe('Billing Webhook Routes', () => {
|
|
|
2125
2260
|
{ plan_version_id: 'team-v1', stripe_price_id: 'price_shared', active: 1 }
|
|
2126
2261
|
]);
|
|
2127
2262
|
});
|
|
2128
|
-
it('
|
|
2263
|
+
it('rejects manual free price mapping upserts now that pricing is import-only', async () => {
|
|
2129
2264
|
const now = new Date().toISOString();
|
|
2130
2265
|
db.prepare(`
|
|
2131
2266
|
INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
|
|
@@ -2141,17 +2276,10 @@ describe('Billing Webhook Routes', () => {
|
|
|
2141
2276
|
}), { 'x-taskforce-runtime-mode': 'cloud' });
|
|
2142
2277
|
const upsertRes = createMockRes();
|
|
2143
2278
|
await upsertRoute(upsertReq, upsertRes, {});
|
|
2144
|
-
expect(upsertRes.statusCode).toBe(
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
WHERE tenant_id = 'default' AND plan_version_id = 'starter-v1' AND billing_interval = 'month'
|
|
2149
|
-
LIMIT 1
|
|
2150
|
-
`).get();
|
|
2151
|
-
expect(row.stripe_price_id).toBe('free:starter-v1:month');
|
|
2152
|
-
expect(row.pricing_type).toBe('free');
|
|
2153
|
-
expect(row.active).toBe(1);
|
|
2154
|
-
expect(row.unit_amount).toBe(0);
|
|
2279
|
+
expect(upsertRes.statusCode).toBe(410);
|
|
2280
|
+
expect(JSON.parse(upsertRes.body)).toEqual(expect.objectContaining({
|
|
2281
|
+
code: 'BILLING_PRICE_MANUAL_MAPPING_REMOVED'
|
|
2282
|
+
}));
|
|
2155
2283
|
});
|
|
2156
2284
|
it('rejects webhook failure diagnostics for non-admin', async () => {
|
|
2157
2285
|
const nonAdminHandlers = buildBillingRouteHandlers(db, { isAdmin: false });
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from 'http';
|
|
2
2
|
import type { TaskforceCore } from '../../core/Taskforce.js';
|
|
3
|
-
import type
|
|
3
|
+
import { type RouteHandler } from './primitives.js';
|
|
4
4
|
export interface DocumentReviewRouteDependencies {
|
|
5
5
|
addRoute: (method: string, path: string, handler: RouteHandler) => void;
|
|
6
6
|
core: TaskforceCore;
|
|
@@ -28,7 +28,7 @@ import { ObjectStorageClient } from '../../storage/objectStorageClient.js';
|
|
|
28
28
|
import type { DocumentRegistryEntry } from '../../storage/documentRegistry.js';
|
|
29
29
|
import { TaskAssetStore } from '../../storage/taskAssetStore.js';
|
|
30
30
|
import { WorkspaceAssetStore, type WorkspaceAssetEntry, type WorkspaceAssetKind } from '../../storage/workspaceAssetStore.js';
|
|
31
|
-
import type
|
|
31
|
+
import { type RouteContext, type RouteHandler } from './primitives.js';
|
|
32
32
|
type AssetMetadata = DocumentRegistryEntry & {
|
|
33
33
|
quarantineStatus?: 'none' | 'quarantined' | 'released';
|
|
34
34
|
workspaceId?: string;
|
|
@@ -51,7 +51,9 @@ export interface DocumentRouteDependencies {
|
|
|
51
51
|
resolveEffectiveObjectStorageConfig: () => ResolvedObjectStorageConfig;
|
|
52
52
|
getObjectStorageClient: () => ObjectStorageClient | null;
|
|
53
53
|
resolveStorageWorkspaceId: (req: IncomingMessage, body?: any) => string | null;
|
|
54
|
-
ensureContextUploadAllowed: (req: IncomingMessage, res: ServerResponse, workspaceId: string
|
|
54
|
+
ensureContextUploadAllowed: (req: IncomingMessage, res: ServerResponse, workspaceId: string, options?: {
|
|
55
|
+
requestedBytes?: number | null;
|
|
56
|
+
}) => boolean;
|
|
55
57
|
parseStorageFolderSegments: (folderPathRaw: unknown) => string[] | null;
|
|
56
58
|
buildCanonicalWorkspaceAssetRelativePath: (params: {
|
|
57
59
|
workspaceIdRaw: unknown;
|
|
@@ -29,21 +29,53 @@ import { purgeExpiredDocuments } from '../../storage/documentPurge.js';
|
|
|
29
29
|
import { parsePlan } from '../../utils/planParser.js';
|
|
30
30
|
import { getCanonicalDocumentName, getReadableDocumentName, isGeneratedDocumentName } from '../../utils/documentNames.js';
|
|
31
31
|
import { requestRuntimeMode } from './shared.js';
|
|
32
|
-
import { parseJsonBody } from '
|
|
32
|
+
import { parseJsonBody } from './primitives.js';
|
|
33
33
|
export function registerDocumentRoutes(deps) {
|
|
34
34
|
const { addRoute, core, context, requestWorkspaceId, ensureAdminRole, resolveEffectiveObjectStorageConfig, getObjectStorageClient, resolveStorageWorkspaceId, ensureContextUploadAllowed, parseStorageFolderSegments, buildCanonicalWorkspaceAssetRelativePath, inferAssetKindFromPath, appendStorageTelemetry, scanAndRecordDocument, buildTaskAttachmentPayload, workspaceAssetStore, taskAssetStore, normalizeDocumentPathInput, getAssetMetadata, getCanonicalAssetMetadata, documentRetentionDays, appendDocumentPurgeHistory, markAssetDeleted, isAssetDeleted, clearAssetDeleted, ensureWithinDir, sanitizeDownloadFilename, contentTypeForExt, inferInlineDocumentFromPath, resolveCanonicalDocumentRead, inferWorkspaceIdFromStoragePath, inferTaskIdFromStoragePath, } = deps;
|
|
35
|
+
const removeDocumentAttachmentFromTaskAttachments = (attachmentsRaw, asset) => {
|
|
36
|
+
const canonicalDocumentPath = `/api/taskforce/documents/${encodeURIComponent(asset.assetId)}/content`;
|
|
37
|
+
const legacyContextPath = `/api/taskforce/context/${encodeURIComponent(asset.storageKey)}`;
|
|
38
|
+
if (!Array.isArray(attachmentsRaw))
|
|
39
|
+
return [];
|
|
40
|
+
return attachmentsRaw.filter((attachment) => {
|
|
41
|
+
if (typeof attachment === 'string') {
|
|
42
|
+
const normalized = attachment.trim();
|
|
43
|
+
if (!normalized)
|
|
44
|
+
return false;
|
|
45
|
+
return normalized !== canonicalDocumentPath
|
|
46
|
+
&& normalized !== legacyContextPath
|
|
47
|
+
&& normalized !== asset.storageKey;
|
|
48
|
+
}
|
|
49
|
+
if (!attachment || typeof attachment !== 'object')
|
|
50
|
+
return false;
|
|
51
|
+
const candidate = attachment;
|
|
52
|
+
const candidatePath = String(candidate.path || '').trim();
|
|
53
|
+
const candidateFsPath = String(candidate.fsPath || '').trim();
|
|
54
|
+
const candidateAssetId = String(candidate.assetId || '').trim();
|
|
55
|
+
if (candidateAssetId && candidateAssetId === asset.assetId)
|
|
56
|
+
return false;
|
|
57
|
+
if (candidatePath === canonicalDocumentPath || candidatePath === legacyContextPath)
|
|
58
|
+
return false;
|
|
59
|
+
if (candidateFsPath && candidateFsPath === asset.storageKey)
|
|
60
|
+
return false;
|
|
61
|
+
return !!candidatePath;
|
|
62
|
+
});
|
|
63
|
+
};
|
|
35
64
|
addRoute('POST', '/api/taskforce/context-upload/init', async (req, res) => {
|
|
36
65
|
const resolvedObjectStorage = resolveEffectiveObjectStorageConfig();
|
|
37
66
|
const objectStorageClient = getObjectStorageClient();
|
|
38
67
|
const body = await parseJsonBody(req);
|
|
39
68
|
const { originalName, mimeType, taskId, folderPath } = body || {};
|
|
40
69
|
const workspaceId = resolveStorageWorkspaceId(req, body);
|
|
70
|
+
const requestedBytes = Number(body?.size);
|
|
41
71
|
if (!workspaceId) {
|
|
42
72
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
43
73
|
res.end(JSON.stringify({ error: 'workspaceId is required and cannot be default for uploads.' }));
|
|
44
74
|
return;
|
|
45
75
|
}
|
|
46
|
-
if (!ensureContextUploadAllowed(req, res, workspaceId
|
|
76
|
+
if (!ensureContextUploadAllowed(req, res, workspaceId, {
|
|
77
|
+
requestedBytes: Number.isFinite(requestedBytes) ? requestedBytes : null
|
|
78
|
+
}))
|
|
47
79
|
return;
|
|
48
80
|
const folderSegments = parseStorageFolderSegments(folderPath);
|
|
49
81
|
if (folderSegments === null) {
|
|
@@ -144,6 +176,10 @@ export function registerDocumentRoutes(deps) {
|
|
|
144
176
|
res.end(JSON.stringify({ error: 'Uploaded file not found.' }));
|
|
145
177
|
return;
|
|
146
178
|
}
|
|
179
|
+
if (!ensureContextUploadAllowed(req, res, workspaceId, {
|
|
180
|
+
requestedBytes: object.body.length
|
|
181
|
+
}))
|
|
182
|
+
return;
|
|
147
183
|
try {
|
|
148
184
|
const taskId = typeof body?.taskId === 'string' && body.taskId.trim().length > 0
|
|
149
185
|
? body.taskId.trim()
|
|
@@ -191,8 +227,6 @@ export function registerDocumentRoutes(deps) {
|
|
|
191
227
|
res.end(JSON.stringify({ error: 'workspaceId is required and cannot be default for uploads.' }));
|
|
192
228
|
return;
|
|
193
229
|
}
|
|
194
|
-
if (!ensureContextUploadAllowed(req, res, workspaceId))
|
|
195
|
-
return;
|
|
196
230
|
const folderSegments = parseStorageFolderSegments(folderPath);
|
|
197
231
|
if (folderSegments === null) {
|
|
198
232
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
@@ -233,6 +267,10 @@ export function registerDocumentRoutes(deps) {
|
|
|
233
267
|
const actualFilename = `${safeBase}-${Date.now()}${extension}`;
|
|
234
268
|
const { basePath, projectRoot } = core.getPaths();
|
|
235
269
|
const buffer = Buffer.from(base64Data, 'base64');
|
|
270
|
+
if (!ensureContextUploadAllowed(req, res, workspaceId, {
|
|
271
|
+
requestedBytes: buffer.length
|
|
272
|
+
}))
|
|
273
|
+
return;
|
|
236
274
|
const relativeToStorage = buildCanonicalWorkspaceAssetRelativePath({
|
|
237
275
|
workspaceIdRaw: workspaceId,
|
|
238
276
|
kind: mimeType.startsWith('image/') ? 'image' : inferAssetKindFromPath(actualFilename),
|
|
@@ -771,6 +809,8 @@ export function registerDocumentRoutes(deps) {
|
|
|
771
809
|
addRoute('DELETE', '/api/taskforce/documents/:assetId', async (req, res, params) => {
|
|
772
810
|
const workspaceId = requestWorkspaceId(req);
|
|
773
811
|
const assetId = String(params.assetId || '').trim();
|
|
812
|
+
const body = await parseJsonBody(req);
|
|
813
|
+
const forceDetach = body?.forceDetach === true;
|
|
774
814
|
if (!workspaceAssetStore || !assetId) {
|
|
775
815
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
776
816
|
res.end(JSON.stringify({ error: 'Document not found' }));
|
|
@@ -786,14 +826,39 @@ export function registerDocumentRoutes(deps) {
|
|
|
786
826
|
.filter((link) => !link.deletedAt)
|
|
787
827
|
.filter((link) => !!core.getTask(link.taskId, workspaceId));
|
|
788
828
|
if (activeLinks.length > 0) {
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
829
|
+
if (!forceDetach) {
|
|
830
|
+
res.writeHead(409, { 'Content-Type': 'application/json' });
|
|
831
|
+
res.end(JSON.stringify({
|
|
832
|
+
error: 'Document is still attached to one or more tasks.',
|
|
833
|
+
activeAttachmentCount: activeLinks.length,
|
|
834
|
+
requiresForceDetach: true
|
|
835
|
+
}));
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
for (const link of activeLinks) {
|
|
839
|
+
const task = core.getTask(link.taskId, workspaceId);
|
|
840
|
+
if (!task)
|
|
841
|
+
continue;
|
|
842
|
+
const updatedAttachments = removeDocumentAttachmentFromTaskAttachments(task.attachments, asset);
|
|
843
|
+
core.updateTask(link.taskId, { attachments: updatedAttachments }, workspaceId, {
|
|
844
|
+
actorRef: 'user',
|
|
845
|
+
saveSource: 'manual'
|
|
846
|
+
});
|
|
847
|
+
}
|
|
792
848
|
}
|
|
793
849
|
const nowIso = new Date().toISOString();
|
|
794
850
|
const purgeAfter = new Date(Date.now() + documentRetentionDays * 24 * 60 * 60 * 1000).toISOString();
|
|
795
851
|
const metadata = markAssetDeleted(asset.storageKey);
|
|
796
852
|
workspaceAssetStore.markDeleted(asset.assetId, workspaceId, nowIso, purgeAfter);
|
|
853
|
+
const links = workspaceAssetStore.listLinksForAsset(asset.assetId, workspaceId, { includeDeleted: true });
|
|
854
|
+
for (const link of links) {
|
|
855
|
+
workspaceAssetStore.deleteLink({
|
|
856
|
+
workspaceId,
|
|
857
|
+
assetId: asset.assetId,
|
|
858
|
+
taskId: link.taskId,
|
|
859
|
+
role: link.role
|
|
860
|
+
});
|
|
861
|
+
}
|
|
797
862
|
appendStorageTelemetry('delete.soft', {
|
|
798
863
|
path: asset.storageKey,
|
|
799
864
|
status: 'soft-deleted',
|
|
@@ -803,6 +868,7 @@ export function registerDocumentRoutes(deps) {
|
|
|
803
868
|
res.end(JSON.stringify({
|
|
804
869
|
success: true,
|
|
805
870
|
softDeleted: true,
|
|
871
|
+
detachedTaskCount: activeLinks.length,
|
|
806
872
|
retentionDays: documentRetentionDays,
|
|
807
873
|
purgeAfter: metadata?.purgeAfter || purgeAfter
|
|
808
874
|
}));
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { IncomingMessage, ServerResponse } from 'http';
|
|
2
|
+
import type { TaskforceAuthConfig } from '../auth.js';
|
|
3
|
+
import type { ResolvedObjectStorageConfig } from '../../storage/objectStorage.js';
|
|
4
|
+
export type RouteHandler = (req: IncomingMessage, res: ServerResponse, params: Record<string, string>) => Promise<void>;
|
|
5
|
+
export interface RouteContext {
|
|
6
|
+
authConfig?: TaskforceAuthConfig;
|
|
7
|
+
objectStorage?: ResolvedObjectStorageConfig;
|
|
8
|
+
}
|
|
9
|
+
export declare function parseJsonBody(req: IncomingMessage, maxBytes?: number): Promise<any>;
|
|
10
|
+
export declare function parseFormBody(req: IncomingMessage, maxBytes?: number): Promise<Record<string, string>>;
|
|
11
|
+
export declare function escapeHtml(value: unknown): string;
|