@taskforcehq/taskforce 0.3.303 → 0.3.304
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 +102 -5
- package/dist/TaskforceCore.js +514 -212
- package/dist/TaskforceCore.test.js +1172 -133
- package/dist/annotatedAttachments/service.d.ts +1 -1
- package/dist/annotatedAttachments/types.d.ts +1 -1
- package/dist/components/features/AgentsModule.js +1 -1
- package/dist/components/features/AgentsModule.test.js +1 -1
- package/dist/components/features/TaskSettings.js +58 -38
- package/dist/components/features/TaskSettings.test.js +139 -40
- package/dist/components/task/TaskActionHeader.d.ts +2 -1
- package/dist/components/task/TaskActionHeader.js +3 -3
- package/dist/components/task/TaskActionHeader.test.js +8 -0
- package/dist/components/task/TaskCard.js +16 -1
- package/dist/components/task/TaskCard.test.js +31 -0
- package/dist/components/task/TaskForm.d.ts +2 -1
- package/dist/components/task/TaskForm.js +22 -4
- package/dist/components/task/TaskForm.test.js +46 -0
- package/dist/components/task/TaskKanban.d.ts +1 -1
- package/dist/components/task/TaskKanban.js +31 -160
- package/dist/components/task/TaskStatusActions.d.ts +6 -1
- package/dist/components/task/TaskStatusActions.js +8 -3
- package/dist/components/task/TaskStatusActions.test.js +9 -0
- package/dist/components/task/taskKanbanDropRules.d.ts +58 -0
- package/dist/components/task/taskKanbanDropRules.js +187 -0
- package/dist/components/task/taskKanbanDropRules.test.d.ts +1 -0
- package/dist/components/task/taskKanbanDropRules.test.js +114 -0
- package/dist/components/ui/Modal.d.ts +1 -1
- package/dist/components/ui/Modal.js +1 -0
- package/dist/components/views/AppShellHeader.js +5 -1
- package/dist/components/views/AppShellHeader.test.js +4 -0
- package/dist/components/views/PlanComparisonPage.d.ts +3 -2
- package/dist/components/views/PlanComparisonPage.js +4 -2
- package/dist/components/views/PlanComparisonPage.test.js +4 -0
- package/dist/components/views/PlansPage.d.ts +5 -1
- package/dist/components/views/PlansPage.js +39 -12
- package/dist/components/views/PlansPage.test.js +59 -2
- package/dist/components/views/StandaloneLayout.js +113 -38
- package/dist/components/views/WidgetView.js +11 -2
- package/dist/components/views/standalone/modals/SyncStatusModal.js +1 -1
- package/dist/core/DeletedTaskLifecycleService.d.ts +2 -1
- package/dist/core/PlanEntitlementService.d.ts +3 -1
- package/dist/core/PlanEntitlementService.js +61 -11
- package/dist/core/SignupMonetizationSettings.test.js +37 -2
- package/dist/core/TaskActivityService.d.ts +4 -1
- package/dist/core/TaskActivityService.js +45 -0
- package/dist/core/TaskAuditTimeline.test.js +52 -1
- package/dist/core/Taskforce.d.ts +15 -11
- package/dist/core/Taskforce.js +43 -2
- package/dist/core/WorkspacePlanMode.test.js +65 -0
- package/dist/documentReviews/service.d.ts +1 -1
- package/dist/documentReviews/types.d.ts +1 -1
- package/dist/hooks/auth/useTaskforceAuthBootstrap.d.ts +48 -0
- package/dist/hooks/auth/useTaskforceAuthBootstrap.js +219 -0
- package/dist/hooks/sync/auth.d.ts +37 -0
- package/dist/hooks/sync/auth.js +61 -0
- package/dist/hooks/sync/bootstrap.d.ts +34 -0
- package/dist/hooks/sync/bootstrap.js +52 -0
- package/dist/hooks/sync/orchestratorShared.d.ts +36 -0
- package/dist/hooks/sync/orchestratorShared.js +209 -0
- package/dist/hooks/sync/orchestratorShared.test.d.ts +1 -0
- package/dist/hooks/sync/orchestratorShared.test.js +49 -0
- package/dist/hooks/sync/realtime.d.ts +25 -0
- package/dist/hooks/sync/realtime.js +60 -0
- package/dist/hooks/sync/recovery.d.ts +69 -0
- package/dist/hooks/sync/recovery.js +118 -0
- package/dist/hooks/sync/transfers.d.ts +122 -0
- package/dist/hooks/sync/transfers.js +431 -0
- package/dist/hooks/sync/useDataVersionRefresh.d.ts +16 -0
- package/dist/hooks/sync/useDataVersionRefresh.js +73 -0
- package/dist/hooks/ui/useResourceExport.d.ts +3 -1
- package/dist/hooks/ui/useResourceExport.js +5 -2
- package/dist/hooks/ui/useWorkflowTemplates.js +11 -6
- package/dist/hooks/useRealtimeSync.js +2 -1
- package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +8 -0
- package/dist/hooks/useSyncOrchestrator.context-assets.test.d.ts +1 -0
- package/dist/hooks/useSyncOrchestrator.context-assets.test.js +228 -0
- package/dist/hooks/useSyncOrchestrator.d.ts +5 -4
- package/dist/hooks/useSyncOrchestrator.js +314 -873
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +568 -3
- package/dist/hooks/useTaskMutations.js +45 -6
- package/dist/hooks/useTaskMutations.test.js +147 -0
- package/dist/hooks/useTaskforce.d.ts +8 -2
- package/dist/hooks/useTaskforce.js +285 -812
- package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
- package/dist/hooks/useWorkspaceSyncController.js +22 -10
- package/dist/hooks/workspace/useTaskforceApiRouting.test.js +17 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +166 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +434 -0
- package/dist/hooks/workspace/workspaceLocalState.d.ts +19 -0
- package/dist/hooks/workspace/workspaceLocalState.js +100 -0
- package/dist/hooks/workspace/workspaceLocalState.test.d.ts +1 -0
- package/dist/hooks/workspace/workspaceLocalState.test.js +59 -0
- package/dist/localization/locales/en-US.js +2 -2
- package/dist/mcp/canonicalAssetHelpers.d.ts +70 -0
- package/dist/mcp/canonicalAssetHelpers.js +259 -0
- package/dist/mcp/clientIntegrations.d.ts +12 -1
- package/dist/mcp/clientIntegrations.js +161 -6
- package/dist/mcp/clientIntegrations.test.js +76 -4
- package/dist/mcp/documentHelpers.d.ts +100 -0
- package/dist/mcp/documentHelpers.js +161 -0
- package/dist/mcp/runtime.js +174 -476
- package/dist/mcp/runtime.test.js +81 -8
- package/dist/mcp/taskAttachmentHelpers.d.ts +41 -0
- package/dist/mcp/taskAttachmentHelpers.js +60 -0
- package/dist/mcp/taskAttachmentTypes.d.ts +37 -0
- package/dist/mcp/taskAttachmentTypes.js +1 -0
- package/dist/plugins/vite.js +1 -1
- package/dist/runtime/appGateDefinitions.d.ts +1 -1
- package/dist/runtime/appGateDefinitions.js +1 -1
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +46 -3
- package/dist/server/index.test.js +12 -1
- package/dist/server/routes/admin.js +13 -19
- package/dist/server/routes/auth.js +37 -2
- package/dist/server/routes/billing.js +18 -67
- package/dist/server/routes/billing.test.js +43 -0
- package/dist/server/routes/shared.d.ts +30 -0
- package/dist/server/routes/shared.js +26 -0
- package/dist/server/routes/sync.integration.test.js +20 -0
- package/dist/server/routes/sync.js +110 -237
- package/dist/server/routes/tasks.js +49 -7
- package/dist/server/routes.js +8 -19
- package/dist/server/routes.test.js +243 -5
- package/dist/shared/taskActor.d.ts +10 -0
- package/dist/shared/taskActor.js +1 -0
- package/dist/sync/collaborationSyncPayload.js +4 -0
- package/dist/sync/collaborationSyncPayload.test.js +8 -0
- package/dist/sync/collaborationSyncState.d.ts +3 -1
- package/dist/sync/collaborationSyncState.js +19 -0
- package/dist/sync/contentSyncState.js +12 -10
- package/dist/sync/syncApplyHandlers.d.ts +87 -1
- package/dist/sync/syncApplyHandlers.js +370 -8
- package/dist/sync/syncApplyHandlers.test.d.ts +1 -0
- package/dist/sync/syncApplyHandlers.test.js +377 -0
- package/dist/sync/syncService.d.ts +8 -1
- package/dist/sync/syncService.js +36 -2
- package/dist/sync/syncService.test.js +48 -0
- package/dist/sync/workspacePullFeed.d.ts +1 -0
- package/dist/sync/workspacePullFeed.js +66 -59
- package/dist/sync/workspacePullFeed.test.js +72 -4
- package/dist/sync/workspaceSyncModel.d.ts +22 -1
- package/dist/sync/workspaceSyncModel.js +65 -0
- package/dist/sync/workspaceSyncModel.test.js +119 -0
- package/dist/types.d.ts +2 -10
- package/dist/ui/assets/AgentsModule-BLWVbuKP.js +1 -0
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-C_TmXZwA.js → AnnotatedAttachmentWorkspace-BlS-cqnl.js} +1 -1
- package/dist/ui/assets/{DocumentWorkspace-C3GyzrWm.js → DocumentWorkspace-BH_6DF6x.js} +1 -1
- package/dist/ui/assets/{InitiativesModule-4-Rh2K2n.js → InitiativesModule-BjR6iBf9.js} +1 -1
- package/dist/ui/assets/{PlansPage-BlVC_lRq.css → PlansPage-C2dd2n1X.css} +1 -1
- package/dist/ui/assets/PlansPage-CSDQ4sLa.js +1 -0
- package/dist/ui/assets/TaskSettings-CEDy34Jo.js +9 -0
- package/dist/ui/assets/{WorkflowsModule-CGk9s3NJ.js → WorkflowsModule-DnFqdUME.js} +1 -1
- package/dist/ui/assets/index-DXUdtH1B.js +6 -0
- package/dist/ui/assets/index-Ii0B0rTO.css +1 -0
- package/dist/ui/assets/{vendor-icons-pWocEipT.js → vendor-icons-I7ZwG1z_.js} +1 -1
- package/dist/ui/index.html +4 -4
- package/dist/utils/taskActivity.js +4 -0
- package/dist/utils/taskActivity.test.js +25 -1
- package/package.json +3 -1
- package/scripts/playwright-auth.sh +4 -0
- package/dist/ui/assets/AgentsModule-CpsTmrIW.js +0 -1
- package/dist/ui/assets/PlansPage-BP7AYOqr.js +0 -1
- package/dist/ui/assets/TaskSettings-BOC5F6Ag.js +0 -8
- package/dist/ui/assets/index-CLIMgR6g.js +0 -6
- package/dist/ui/assets/index-DneETxcu.css +0 -1
package/dist/mcp/runtime.test.js
CHANGED
|
@@ -436,11 +436,11 @@ describe('Taskforce MCP tool descriptions', () => {
|
|
|
436
436
|
expect(linkTaskContext.description).toContain('external http(s) URL');
|
|
437
437
|
expect(saveTaskAttachment.description).toContain('generated content');
|
|
438
438
|
expect(saveTaskAttachment.description).toContain('overwrite=true');
|
|
439
|
-
expect(saveTaskAttachment.description).toContain('AI-authored markdown/text
|
|
439
|
+
expect(saveTaskAttachment.description).toContain('use inline content for AI-authored markdown/text');
|
|
440
440
|
expect(saveTaskAttachment.inputSchema).toBeDefined();
|
|
441
441
|
const saveTaskAttachmentSchema = saveTaskAttachment.inputSchema;
|
|
442
442
|
expect(saveTaskAttachmentSchema.properties.content.description).toContain('use this for AI-authored markdown/text attachments');
|
|
443
|
-
expect(saveTaskAttachmentSchema.properties.sourcePath.description).toContain('
|
|
443
|
+
expect(saveTaskAttachmentSchema.properties.sourcePath.description).toContain('approved agent workspace root');
|
|
444
444
|
expect(resolveProfile.description).toContain('AI collaborator profile');
|
|
445
445
|
expect(resolveProfile.description).toContain('Update semantics');
|
|
446
446
|
expect(resolveProfile.description).toContain('empty strings are ignored rather than clearing');
|
|
@@ -1667,6 +1667,40 @@ describe('Taskforce MCP tool descriptions', () => {
|
|
|
1667
1667
|
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1668
1668
|
}
|
|
1669
1669
|
});
|
|
1670
|
+
it('returns summary-only rows from list_tasks json output', async () => {
|
|
1671
|
+
const projectRoot = createProjectRoot();
|
|
1672
|
+
try {
|
|
1673
|
+
const created = parseToolJson(await callTool(projectRoot, 'local', 'create_task', {
|
|
1674
|
+
title: 'List summary task',
|
|
1675
|
+
description: 'Verbose description that should not appear in list rows'
|
|
1676
|
+
}));
|
|
1677
|
+
await callTool(projectRoot, 'local', 'replace_task_checklist', {
|
|
1678
|
+
taskId: created.referenceLabel,
|
|
1679
|
+
items: [
|
|
1680
|
+
{ title: 'Checklist item one', isCompleted: false }
|
|
1681
|
+
]
|
|
1682
|
+
});
|
|
1683
|
+
await callTool(projectRoot, 'local', 'add_comment', {
|
|
1684
|
+
id: created.referenceLabel,
|
|
1685
|
+
text: 'Verbose comment that should not appear in list rows',
|
|
1686
|
+
author: 'ai-profile-019d15c5-6ad3-7bf0-9b9e-fa9c366c3ff4'
|
|
1687
|
+
});
|
|
1688
|
+
const listed = parseToolJson(await callTool(projectRoot, 'local', 'list_tasks', {
|
|
1689
|
+
format: 'json'
|
|
1690
|
+
}));
|
|
1691
|
+
const row = listed.find((task) => task.referenceLabel === created.referenceLabel);
|
|
1692
|
+
expect(row).toBeTruthy();
|
|
1693
|
+
expect(row.description).toBeUndefined();
|
|
1694
|
+
expect(row.comments).toBeUndefined();
|
|
1695
|
+
expect(row.taskEvents).toBeUndefined();
|
|
1696
|
+
expect(row.activity).toBeUndefined();
|
|
1697
|
+
expect(row.attachments).toBeUndefined();
|
|
1698
|
+
expect(row.checklistItems).toBeUndefined();
|
|
1699
|
+
}
|
|
1700
|
+
finally {
|
|
1701
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1702
|
+
}
|
|
1703
|
+
});
|
|
1670
1704
|
it('reads task resources by T-XXX reference and exposes version/scope in config resource', async () => {
|
|
1671
1705
|
const projectRoot = createProjectRoot();
|
|
1672
1706
|
try {
|
|
@@ -1961,8 +1995,45 @@ describe('Taskforce MCP tool descriptions', () => {
|
|
|
1961
1995
|
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1962
1996
|
}
|
|
1963
1997
|
});
|
|
1964
|
-
it('
|
|
1998
|
+
it('allows save_task_attachment sourcePath in cloud runtime for approved agent workspace roots', async () => {
|
|
1999
|
+
const projectRoot = createProjectRoot();
|
|
2000
|
+
const agentWorkspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'taskforce-mcp-agent-workspace-'));
|
|
2001
|
+
const previousRoots = process.env.TASKFORCE_ALLOWED_ATTACHMENT_SOURCE_ROOTS;
|
|
2002
|
+
try {
|
|
2003
|
+
const core = new TaskforceCore({
|
|
2004
|
+
projectRoot,
|
|
2005
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
2006
|
+
});
|
|
2007
|
+
const task = core.createTask({ title: 'Cloud sourcePath allow test' }, { workspaceId: 'workspace-1' });
|
|
2008
|
+
core.close();
|
|
2009
|
+
const sourcePath = path.join(agentWorkspaceRoot, 'implementation-plan.md');
|
|
2010
|
+
fs.writeFileSync(sourcePath, '# Recovered implementation plan', 'utf8');
|
|
2011
|
+
process.env.TASKFORCE_ALLOWED_ATTACHMENT_SOURCE_ROOTS = agentWorkspaceRoot;
|
|
2012
|
+
await callTool(projectRoot, 'cloud', 'save_task_attachment', {
|
|
2013
|
+
id: task.id,
|
|
2014
|
+
filename: 'implementation-plan.md',
|
|
2015
|
+
sourcePath
|
|
2016
|
+
});
|
|
2017
|
+
const readBack = parseToolJson(await callTool(projectRoot, 'cloud', 'read_task_attachment', {
|
|
2018
|
+
taskId: task.id,
|
|
2019
|
+
attachmentId: parseToolJson(await callTool(projectRoot, 'cloud', 'get_task_attachments', { id: task.id })).attachments[0]?.attachmentId
|
|
2020
|
+
}));
|
|
2021
|
+
expect(readBack.content).toContain('# Recovered implementation plan');
|
|
2022
|
+
}
|
|
2023
|
+
finally {
|
|
2024
|
+
if (previousRoots === undefined) {
|
|
2025
|
+
delete process.env.TASKFORCE_ALLOWED_ATTACHMENT_SOURCE_ROOTS;
|
|
2026
|
+
}
|
|
2027
|
+
else {
|
|
2028
|
+
process.env.TASKFORCE_ALLOWED_ATTACHMENT_SOURCE_ROOTS = previousRoots;
|
|
2029
|
+
}
|
|
2030
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
2031
|
+
fs.rmSync(agentWorkspaceRoot, { recursive: true, force: true });
|
|
2032
|
+
}
|
|
2033
|
+
});
|
|
2034
|
+
it('rejects save_task_attachment sourcePath in cloud runtime for unapproved roots', async () => {
|
|
1965
2035
|
const projectRoot = createProjectRoot();
|
|
2036
|
+
const externalRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'taskforce-mcp-cloud-unapproved-'));
|
|
1966
2037
|
try {
|
|
1967
2038
|
const core = new TaskforceCore({
|
|
1968
2039
|
projectRoot,
|
|
@@ -1970,16 +2041,18 @@ describe('Taskforce MCP tool descriptions', () => {
|
|
|
1970
2041
|
});
|
|
1971
2042
|
const task = core.createTask({ title: 'Cloud sourcePath reject test' }, { workspaceId: 'workspace-1' });
|
|
1972
2043
|
core.close();
|
|
2044
|
+
const sourcePath = path.join(externalRoot, 'outside.md');
|
|
2045
|
+
fs.writeFileSync(sourcePath, '# Outside approved roots', 'utf8');
|
|
1973
2046
|
const result = await callTool(projectRoot, 'cloud', 'save_task_attachment', {
|
|
1974
2047
|
id: task.id,
|
|
1975
|
-
filename: '
|
|
1976
|
-
sourcePath
|
|
2048
|
+
filename: 'outside.md',
|
|
2049
|
+
sourcePath
|
|
1977
2050
|
});
|
|
1978
|
-
expect(result.content?.[0]?.text).toContain('
|
|
1979
|
-
expect(result.content?.[0]?.text).toContain('resend the attachment using inline content instead');
|
|
2051
|
+
expect(result.content?.[0]?.text).toContain('sourcePath must be within project root, a Taskforce-managed attachment source, or an approved agent workspace root');
|
|
1980
2052
|
}
|
|
1981
2053
|
finally {
|
|
1982
2054
|
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
2055
|
+
fs.rmSync(externalRoot, { recursive: true, force: true });
|
|
1983
2056
|
}
|
|
1984
2057
|
});
|
|
1985
2058
|
it('accepts task references for save_task_attachment', async () => {
|
|
@@ -2331,7 +2404,7 @@ describe('Taskforce MCP tool descriptions', () => {
|
|
|
2331
2404
|
filename: 'outside.md',
|
|
2332
2405
|
sourcePath: externalSourcePath
|
|
2333
2406
|
});
|
|
2334
|
-
expect(result.content?.[0]?.text).toContain('sourcePath must be within project root
|
|
2407
|
+
expect(result.content?.[0]?.text).toContain('sourcePath must be within project root, a Taskforce-managed attachment source, or an approved agent workspace root');
|
|
2335
2408
|
}
|
|
2336
2409
|
finally {
|
|
2337
2410
|
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { WorkspaceAssetEntry, WorkspaceAssetKind, WorkspaceAssetStore } from '../storage/workspaceAssetStore.js';
|
|
2
|
+
import type { McpTaskAttachmentDescriptor, TaskAttachmentRecord } from './taskAttachmentTypes.js';
|
|
3
|
+
export interface TaskAttachmentHelpersDeps {
|
|
4
|
+
activeWorkspaceId: string;
|
|
5
|
+
workspaceAssetStore: WorkspaceAssetStore | null;
|
|
6
|
+
getImageReferenceLabel: (asset: WorkspaceAssetEntry | null | undefined) => string | null;
|
|
7
|
+
getDocumentReferenceLabel: (asset: WorkspaceAssetEntry | null | undefined) => string | null;
|
|
8
|
+
inferMimeTypeFromPath: (filePath: string) => string;
|
|
9
|
+
inferAssetKind: (filePath: string, mimeType: string) => WorkspaceAssetKind;
|
|
10
|
+
isTextReadableMimeType: (mimeTypeRaw: string) => boolean;
|
|
11
|
+
isValidCanonicalDocumentAsset: (params: Pick<WorkspaceAssetEntry, 'kind' | 'storageKey' | 'originalFilename' | 'mimeType'>) => boolean;
|
|
12
|
+
canonicalAssetHelpers: {
|
|
13
|
+
getAttachmentAssetRecord: (attachment: TaskAttachmentRecord) => WorkspaceAssetEntry | null;
|
|
14
|
+
normalizeAttachmentStorageKey: (pathValue: unknown, fsPathValue: unknown) => string | null;
|
|
15
|
+
buildSyntheticAttachmentId: (taskId: string, attachment: TaskAttachmentRecord) => string;
|
|
16
|
+
toTaskAttachmentRecord: (attachment: string | TaskAttachmentRecord) => TaskAttachmentRecord;
|
|
17
|
+
};
|
|
18
|
+
documentHelpers: {
|
|
19
|
+
buildCanonicalDocumentAttachmentHint: (asset: {
|
|
20
|
+
assetId: string;
|
|
21
|
+
documentId: string | null;
|
|
22
|
+
referenceNumber?: number | null;
|
|
23
|
+
mimeType?: string | null;
|
|
24
|
+
}) => {
|
|
25
|
+
documentId: string | null;
|
|
26
|
+
preferredNextTool: string;
|
|
27
|
+
preferredWorkflow: string;
|
|
28
|
+
suggestedArgs: {
|
|
29
|
+
documentId: string;
|
|
30
|
+
};
|
|
31
|
+
guidance: string;
|
|
32
|
+
} | null;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export declare function createTaskAttachmentHelpers(deps: TaskAttachmentHelpersDeps): {
|
|
36
|
+
describeTaskAttachment: (taskId: string, attachmentInput: string | TaskAttachmentRecord) => McpTaskAttachmentDescriptor;
|
|
37
|
+
listTaskAttachmentDescriptors: (task: {
|
|
38
|
+
id: string;
|
|
39
|
+
attachments?: Array<string | TaskAttachmentRecord> | null;
|
|
40
|
+
}) => McpTaskAttachmentDescriptor[];
|
|
41
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export function createTaskAttachmentHelpers(deps) {
|
|
2
|
+
const isCanonicalDocumentAssetRecord = (asset) => deps.isValidCanonicalDocumentAsset(asset);
|
|
3
|
+
const describeTaskAttachment = (taskId, attachmentInput) => {
|
|
4
|
+
const attachment = deps.canonicalAssetHelpers.toTaskAttachmentRecord(attachmentInput);
|
|
5
|
+
const asset = deps.canonicalAssetHelpers.getAttachmentAssetRecord(attachment);
|
|
6
|
+
const storageKey = deps.canonicalAssetHelpers.normalizeAttachmentStorageKey(attachment.path, attachment.fsPath);
|
|
7
|
+
const assetId = String(attachment.assetId || asset?.assetId || '').trim() || undefined;
|
|
8
|
+
const source = (asset?.storageProvider === 'external' || /^https?:\/\//i.test(String(attachment.path || '').trim()))
|
|
9
|
+
? 'external'
|
|
10
|
+
: 'canonical';
|
|
11
|
+
const mimeType = String(asset?.mimeType
|
|
12
|
+
|| (storageKey && !/^https?:\/\//i.test(storageKey) ? deps.inferMimeTypeFromPath(storageKey) : '')
|
|
13
|
+
|| deps.inferMimeTypeFromPath(String(attachment.originalFilename || attachment.displayName || attachment.caption || attachment.path || ''))).trim() || 'application/octet-stream';
|
|
14
|
+
const kind = asset?.kind || deps.inferAssetKind(storageKey || String(attachment.path || ''), mimeType);
|
|
15
|
+
const isCanonicalDocument = Boolean(asset && isCanonicalDocumentAssetRecord(asset)) || (!asset && kind === 'document');
|
|
16
|
+
const referenceNumber = typeof attachment.referenceNumber === 'number'
|
|
17
|
+
? attachment.referenceNumber
|
|
18
|
+
: (typeof asset?.referenceNumber === 'number' ? asset.referenceNumber : null);
|
|
19
|
+
const referenceLabel = String(attachment.referenceLabel
|
|
20
|
+
|| (kind === 'image' ? deps.getImageReferenceLabel(asset) : '')
|
|
21
|
+
|| (isCanonicalDocument ? deps.getDocumentReferenceLabel(asset) : '')).trim() || null;
|
|
22
|
+
const readableAsText = source === 'canonical' && deps.isTextReadableMimeType(mimeType);
|
|
23
|
+
const canonicalDocumentHint = isCanonicalDocument && source === 'canonical' && asset
|
|
24
|
+
? deps.documentHelpers.buildCanonicalDocumentAttachmentHint(asset)
|
|
25
|
+
: null;
|
|
26
|
+
return {
|
|
27
|
+
attachmentId: assetId || deps.canonicalAssetHelpers.buildSyntheticAttachmentId(taskId, attachment),
|
|
28
|
+
...(assetId ? { assetId } : {}),
|
|
29
|
+
...(referenceNumber ? { referenceNumber } : {}),
|
|
30
|
+
...(referenceLabel ? { referenceLabel } : {}),
|
|
31
|
+
...(canonicalDocumentHint?.documentId ? { documentId: canonicalDocumentHint.documentId } : {}),
|
|
32
|
+
path: String(attachment.path || '').trim(),
|
|
33
|
+
...(storageKey && !/^https?:\/\//i.test(storageKey) ? { fsPath: storageKey } : (attachment.fsPath ? { fsPath: String(attachment.fsPath).trim() } : {})),
|
|
34
|
+
...(attachment.caption ? { caption: String(attachment.caption).trim() } : {}),
|
|
35
|
+
...(attachment.displayName ? { displayName: String(attachment.displayName).trim() } : {}),
|
|
36
|
+
...(attachment.originalFilename ? { originalFilename: String(attachment.originalFilename).trim() } : {}),
|
|
37
|
+
kind: isCanonicalDocument ? 'document' : kind,
|
|
38
|
+
mimeType,
|
|
39
|
+
sizeBytes: asset ? Number(asset.sizeBytes || 0) : null,
|
|
40
|
+
...(attachment.timestamp ? { timestamp: String(attachment.timestamp).trim() } : {}),
|
|
41
|
+
source,
|
|
42
|
+
readableAsText,
|
|
43
|
+
...(canonicalDocumentHint ? {
|
|
44
|
+
preferredNextTool: canonicalDocumentHint.preferredNextTool,
|
|
45
|
+
preferredWorkflow: canonicalDocumentHint.preferredWorkflow,
|
|
46
|
+
suggestedArgs: canonicalDocumentHint.suggestedArgs,
|
|
47
|
+
guidance: canonicalDocumentHint.guidance,
|
|
48
|
+
} : {}),
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
const listTaskAttachmentDescriptors = (task) => {
|
|
52
|
+
if (!task || !Array.isArray(task.attachments))
|
|
53
|
+
return [];
|
|
54
|
+
return task.attachments.map((attachment) => describeTaskAttachment(task.id, attachment));
|
|
55
|
+
};
|
|
56
|
+
return {
|
|
57
|
+
describeTaskAttachment,
|
|
58
|
+
listTaskAttachmentDescriptors,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { WorkspaceAssetKind } from '../storage/workspaceAssetStore.js';
|
|
2
|
+
export type TaskAttachmentRecord = {
|
|
3
|
+
path: string;
|
|
4
|
+
fsPath?: string;
|
|
5
|
+
caption?: string;
|
|
6
|
+
displayName?: string;
|
|
7
|
+
originalFilename?: string;
|
|
8
|
+
timestamp?: string;
|
|
9
|
+
assetId?: string;
|
|
10
|
+
referenceNumber?: number | null;
|
|
11
|
+
referenceLabel?: string | null;
|
|
12
|
+
taskId?: string | null;
|
|
13
|
+
};
|
|
14
|
+
export type McpTaskAttachmentDescriptor = {
|
|
15
|
+
attachmentId: string;
|
|
16
|
+
assetId?: string;
|
|
17
|
+
referenceNumber?: number | null;
|
|
18
|
+
referenceLabel?: string | null;
|
|
19
|
+
documentId?: string | null;
|
|
20
|
+
path: string;
|
|
21
|
+
fsPath?: string;
|
|
22
|
+
caption?: string;
|
|
23
|
+
displayName?: string;
|
|
24
|
+
originalFilename?: string;
|
|
25
|
+
kind: WorkspaceAssetKind;
|
|
26
|
+
mimeType: string;
|
|
27
|
+
sizeBytes: number | null;
|
|
28
|
+
timestamp?: string;
|
|
29
|
+
source: 'canonical' | 'external';
|
|
30
|
+
readableAsText: boolean;
|
|
31
|
+
preferredNextTool?: string;
|
|
32
|
+
preferredWorkflow?: string;
|
|
33
|
+
suggestedArgs?: {
|
|
34
|
+
documentId: string;
|
|
35
|
+
};
|
|
36
|
+
guidance?: string;
|
|
37
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/plugins/vite.js
CHANGED
|
@@ -190,7 +190,7 @@ export function taskforcePlugin(options) {
|
|
|
190
190
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
191
191
|
<link rel="icon" type="image/svg+xml" href="/src/assets/favicon/favicon.svg" />
|
|
192
192
|
<link rel="icon" type="image/x-icon" href="/src/assets/favicon/favicon.ico" />
|
|
193
|
-
<title>Taskforce
|
|
193
|
+
<title>Taskforce</title>
|
|
194
194
|
<style>
|
|
195
195
|
body { margin: 0; background: #0f0f1a; color: white; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; }
|
|
196
196
|
#root { height: 100vh; overflow: hidden; }
|
|
@@ -12,7 +12,7 @@ export declare const APP_GATE_DEFINITIONS: {
|
|
|
12
12
|
readonly removalMilestone: "Document Manager GA";
|
|
13
13
|
};
|
|
14
14
|
readonly 'workflows.workspace_access': {
|
|
15
|
-
readonly enabled:
|
|
15
|
+
readonly enabled: false;
|
|
16
16
|
readonly description: "Workflow workspace readiness gate.";
|
|
17
17
|
readonly owner: "taskforce";
|
|
18
18
|
readonly removalMilestone: "Workflow workspace GA";
|
|
@@ -12,7 +12,7 @@ export const APP_GATE_DEFINITIONS = {
|
|
|
12
12
|
removalMilestone: 'Document Manager GA'
|
|
13
13
|
},
|
|
14
14
|
'workflows.workspace_access': {
|
|
15
|
-
enabled:
|
|
15
|
+
enabled: false,
|
|
16
16
|
description: 'Workflow workspace readiness gate.',
|
|
17
17
|
owner: 'taskforce',
|
|
18
18
|
removalMilestone: 'Workflow workspace GA'
|
package/dist/server/index.d.ts
CHANGED
|
@@ -28,6 +28,7 @@ export declare function shouldRewriteSetCookieForLocalDevHost(hostHeader: string
|
|
|
28
28
|
export declare function writeJsonErrorIfPossible(res: http.ServerResponse, statusCode: number, payload: Record<string, unknown>): boolean;
|
|
29
29
|
export declare function applyCloudProxyForwardedHeaders(headers: Headers, req: http.IncomingMessage): void;
|
|
30
30
|
export declare function rewriteProxySetCookieForLocalHost(cookie: string): string;
|
|
31
|
+
export declare function shouldRewriteProxySetCookieDomain(cookie: string, hostHeader: string | undefined): boolean;
|
|
31
32
|
export declare function shouldApplyAuthRateLimitForPath(pathname: string): boolean;
|
|
32
33
|
export declare function resolveCloudProxyBaseUrl(env?: NodeJS.ProcessEnv): string;
|
|
33
34
|
export declare function ensureBillingWebhookAuthExclusion(excludedPaths: string[]): string[];
|
package/dist/server/index.js
CHANGED
|
@@ -205,6 +205,43 @@ export function rewriteProxySetCookieForLocalHost(cookie) {
|
|
|
205
205
|
const filteredAttrs = attrs.filter((attr) => !attr.toLowerCase().startsWith('domain='));
|
|
206
206
|
return [nameValue, ...filteredAttrs].join('; ');
|
|
207
207
|
}
|
|
208
|
+
function normalizeHostHeaderHost(hostHeader) {
|
|
209
|
+
const raw = String(hostHeader || '').trim().toLowerCase();
|
|
210
|
+
if (!raw)
|
|
211
|
+
return '';
|
|
212
|
+
if (raw.startsWith('[')) {
|
|
213
|
+
const closing = raw.indexOf(']');
|
|
214
|
+
if (closing >= 0)
|
|
215
|
+
return raw.slice(1, closing);
|
|
216
|
+
}
|
|
217
|
+
const portSeparator = raw.lastIndexOf(':');
|
|
218
|
+
if (portSeparator <= 0)
|
|
219
|
+
return raw;
|
|
220
|
+
const maybePort = raw.slice(portSeparator + 1);
|
|
221
|
+
if (/^\d+$/.test(maybePort)) {
|
|
222
|
+
return raw.slice(0, portSeparator);
|
|
223
|
+
}
|
|
224
|
+
return raw;
|
|
225
|
+
}
|
|
226
|
+
function parseSetCookieDomain(cookie) {
|
|
227
|
+
const parts = String(cookie || '').split(';').map((part) => part.trim()).filter(Boolean);
|
|
228
|
+
for (const part of parts.slice(1)) {
|
|
229
|
+
const lower = part.toLowerCase();
|
|
230
|
+
if (!lower.startsWith('domain='))
|
|
231
|
+
continue;
|
|
232
|
+
return part.slice('domain='.length).trim().replace(/^\.+/, '').toLowerCase();
|
|
233
|
+
}
|
|
234
|
+
return '';
|
|
235
|
+
}
|
|
236
|
+
export function shouldRewriteProxySetCookieDomain(cookie, hostHeader) {
|
|
237
|
+
const requestHost = normalizeHostHeaderHost(hostHeader);
|
|
238
|
+
const cookieDomain = parseSetCookieDomain(cookie);
|
|
239
|
+
if (!requestHost || !cookieDomain)
|
|
240
|
+
return false;
|
|
241
|
+
if (requestHost === cookieDomain)
|
|
242
|
+
return false;
|
|
243
|
+
return !requestHost.endsWith(`.${cookieDomain}`);
|
|
244
|
+
}
|
|
208
245
|
async function readRequestBody(req, maxBytes) {
|
|
209
246
|
const chunks = [];
|
|
210
247
|
let total = 0;
|
|
@@ -550,9 +587,15 @@ export function createStandaloneServer(options) {
|
|
|
550
587
|
}
|
|
551
588
|
const setCookies = upstream.headers.getSetCookie?.();
|
|
552
589
|
if (Array.isArray(setCookies) && setCookies.length > 0) {
|
|
553
|
-
const
|
|
554
|
-
|
|
555
|
-
|
|
590
|
+
const requestHost = String(req.headers.host || '').trim();
|
|
591
|
+
const normalizedSetCookies = setCookies.map((cookie) => {
|
|
592
|
+
if (shouldRewriteSetCookieForLocalDevHost(requestHost)) {
|
|
593
|
+
return rewriteProxySetCookieForLocalHost(cookie);
|
|
594
|
+
}
|
|
595
|
+
return shouldRewriteProxySetCookieDomain(cookie, requestHost)
|
|
596
|
+
? rewriteProxySetCookieForLocalHost(cookie)
|
|
597
|
+
: cookie;
|
|
598
|
+
});
|
|
556
599
|
res.setHeader('Set-Cookie', normalizedSetCookies);
|
|
557
600
|
}
|
|
558
601
|
res.writeHead(upstream.status, outgoingHeaders);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { ensureBillingWebhookAuthExclusion, ensurePublicSiteConfigAuthExclusion, isBillingWebhookPath, resolveEffectiveRequestUserId, resolveAuthorizedWorkspaceAccess, shouldApplyAuthRateLimitForPath, shouldProxyCloudPath, writeJsonErrorIfPossible } from './index.js';
|
|
2
|
+
import { ensureBillingWebhookAuthExclusion, ensurePublicSiteConfigAuthExclusion, isBillingWebhookPath, rewriteProxySetCookieForLocalHost, resolveEffectiveRequestUserId, resolveAuthorizedWorkspaceAccess, shouldRewriteProxySetCookieDomain, shouldApplyAuthRateLimitForPath, shouldProxyCloudPath, writeJsonErrorIfPossible } from './index.js';
|
|
3
3
|
describe('server index helper policies', () => {
|
|
4
4
|
it('adds webhook path to auth exclusions without duplicates', () => {
|
|
5
5
|
const base = ['/api/taskforce/health', '/api/taskforce/auth'];
|
|
@@ -92,6 +92,17 @@ describe('shouldProxyCloudPath', () => {
|
|
|
92
92
|
expect(shouldProxyCloudPath('/api/taskforce/public/site-config-script')).toBe(true);
|
|
93
93
|
});
|
|
94
94
|
});
|
|
95
|
+
describe('proxy cookie helpers', () => {
|
|
96
|
+
it('rewrites mismatched cookie domains for non-loopback local hosts', () => {
|
|
97
|
+
const cookie = 'taskforce_session=abc; Path=/; HttpOnly; SameSite=Lax; Domain=taskforcehq.ai';
|
|
98
|
+
expect(shouldRewriteProxySetCookieDomain(cookie, 'taskforce.localtest.me:3847')).toBe(true);
|
|
99
|
+
expect(rewriteProxySetCookieForLocalHost(cookie)).not.toContain('Domain=');
|
|
100
|
+
});
|
|
101
|
+
it('preserves cookie domains that already match the request host', () => {
|
|
102
|
+
const cookie = 'taskforce_session=abc; Path=/; HttpOnly; SameSite=Lax; Domain=taskforcehq.ai';
|
|
103
|
+
expect(shouldRewriteProxySetCookieDomain(cookie, 'app.taskforcehq.ai')).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
95
106
|
describe('resolveAuthorizedWorkspaceAccess', () => {
|
|
96
107
|
it('prefers an explicitly requested workspace when the user is authorized for it', () => {
|
|
97
108
|
const core = {
|
|
@@ -65,7 +65,7 @@ import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
|
|
|
65
65
|
import { randomUUID } from 'crypto';
|
|
66
66
|
import { resolveDeploymentConfig } from '../../config/deployment.js';
|
|
67
67
|
import { getTaskforceCloudEnvironmentConfig, resolveTaskforceCloudEnvironmentFromBaseUrl } from '../../config/cloudEnvironment.js';
|
|
68
|
-
import { resolveErrorStatusCode } from './shared.js';
|
|
68
|
+
import { buildCloudMcpRuntimeAuth, resolveErrorStatusCode } from './shared.js';
|
|
69
69
|
import { applyWorkspaceRepair, scanWorkspaceRepair } from '../../sync/workspaceRepair.js';
|
|
70
70
|
// === SECTION: Registration ===
|
|
71
71
|
export function registerAdminRoutes(deps) {
|
|
@@ -474,22 +474,13 @@ export function registerAdminRoutes(deps) {
|
|
|
474
474
|
serverName: 'taskforce-cloud',
|
|
475
475
|
logStartup: false,
|
|
476
476
|
runtimeMode: 'cloud',
|
|
477
|
-
auth: {
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
aiProfileToken: auth.aiProfileToken,
|
|
482
|
-
aiProfileName: auth.aiProfileName,
|
|
483
|
-
aiProfileDescription: auth.aiProfileDescription,
|
|
484
|
-
aiProfileRole: auth.aiProfileRole,
|
|
485
|
-
aiProfileProvider: auth.aiProfileProvider,
|
|
486
|
-
aiProfileModel: auth.aiProfileModel,
|
|
487
|
-
aiProfileSurfaceType: auth.aiProfileSurfaceType,
|
|
488
|
-
actorType: auth.authenticatedViaToken ? 'token' : auth.authenticatedViaOAuth ? 'oauth' : 'user',
|
|
477
|
+
auth: buildCloudMcpRuntimeAuth({
|
|
478
|
+
req,
|
|
479
|
+
auth,
|
|
480
|
+
requestHeader,
|
|
489
481
|
requestId: String(req.headers['x-request-id'] || req.headers['x-railway-request-id'] || '').trim() || randomUUID(),
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
},
|
|
482
|
+
authScopes,
|
|
483
|
+
}),
|
|
493
484
|
});
|
|
494
485
|
const transport = new StreamableHTTPServerTransport({
|
|
495
486
|
sessionIdGenerator: undefined,
|
|
@@ -1741,17 +1732,20 @@ export function registerAdminRoutes(deps) {
|
|
|
1741
1732
|
return;
|
|
1742
1733
|
}
|
|
1743
1734
|
const body = await parseJsonBody(req);
|
|
1735
|
+
const currentOnboardingPolicy = core.getOnboardingPolicy();
|
|
1744
1736
|
const onboardingPolicyMode = body?.onboardingPolicyMode === 'auto_grant_free' || body?.onboardingPolicyMode === 'auto_start_trial'
|
|
1745
1737
|
? body.onboardingPolicyMode
|
|
1746
|
-
: 'require_plan_selection'
|
|
1738
|
+
: body?.onboardingPolicyMode === 'require_plan_selection'
|
|
1739
|
+
? 'require_plan_selection'
|
|
1740
|
+
: currentOnboardingPolicy.mode;
|
|
1747
1741
|
const onboardingPlanVersionId = typeof body?.onboardingPlanVersionId === 'string'
|
|
1748
1742
|
? body.onboardingPlanVersionId.trim() || null
|
|
1749
1743
|
: body?.onboardingPlanVersionId === null
|
|
1750
1744
|
? null
|
|
1751
|
-
:
|
|
1745
|
+
: currentOnboardingPolicy.planVersionId;
|
|
1752
1746
|
const validation = core.validateOnboardingPolicy({
|
|
1753
1747
|
mode: onboardingPolicyMode,
|
|
1754
|
-
|
|
1748
|
+
planVersionId: onboardingPlanVersionId
|
|
1755
1749
|
});
|
|
1756
1750
|
if (!validation.valid) {
|
|
1757
1751
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
@@ -308,7 +308,7 @@ export function registerAuthRoutes(deps) {
|
|
|
308
308
|
? 'Verify your Taskforce account'
|
|
309
309
|
: 'Reset your Taskforce password';
|
|
310
310
|
const text = kind === 'verify-email'
|
|
311
|
-
? `Welcome to Taskforce.\n\
|
|
311
|
+
? `Welcome to Taskforce.\n\nYou're almost in. Enter this verification token on the verification screen to activate your account:\n\n${token}\n\nPrefer the faster route? Open the link below to verify your email automatically:\n${link}\n\nIf you didn't create a Taskforce account, you can safely ignore this email.`
|
|
312
312
|
: `Reset your Taskforce password using this link:\n${link}\n\nIf you did not request this reset, you can ignore this message.`;
|
|
313
313
|
const result = await sendEmail({
|
|
314
314
|
to: email,
|
|
@@ -401,7 +401,8 @@ export function registerAuthRoutes(deps) {
|
|
|
401
401
|
let avatarUrl = null;
|
|
402
402
|
let betaAccess = true;
|
|
403
403
|
if (authenticated && access?.userId) {
|
|
404
|
-
|
|
404
|
+
// Keep the session endpoint fast on reload; avatar draft cleanup is best-effort housekeeping.
|
|
405
|
+
void cleanupStaleAvatarDrafts(access.userId).catch(() => { });
|
|
405
406
|
betaAccess = core.getUserBetaAccess(access.userId);
|
|
406
407
|
const resolved = core.resolveUserAccess(access.userId, workspaceId, role);
|
|
407
408
|
if (resolved) {
|
|
@@ -870,6 +871,40 @@ export function registerAuthRoutes(deps) {
|
|
|
870
871
|
}));
|
|
871
872
|
}
|
|
872
873
|
catch (error) {
|
|
874
|
+
if (error?.code === 'EMAIL_ALREADY_EXISTS'
|
|
875
|
+
&& authBehavior.requireVerifiedEmail
|
|
876
|
+
&& email
|
|
877
|
+
&& !core.isEmailVerified(email)) {
|
|
878
|
+
let emailSent = true;
|
|
879
|
+
let emailError;
|
|
880
|
+
let verificationToken;
|
|
881
|
+
const verification = core.requestEmailVerification({ email });
|
|
882
|
+
verificationToken = verification.token || null;
|
|
883
|
+
if (verification.token) {
|
|
884
|
+
const delivery = await deliverAuthEmail('verify-email', email, verification.token, req);
|
|
885
|
+
emailSent = delivery.ok;
|
|
886
|
+
emailError = delivery.error;
|
|
887
|
+
}
|
|
888
|
+
auditAuthAction(req, 'auth-register-existing-unverified', {
|
|
889
|
+
email,
|
|
890
|
+
workspaceId: workspaceIdInput || null,
|
|
891
|
+
code: 'EMAIL_ALREADY_EXISTS_UNVERIFIED',
|
|
892
|
+
emailSent
|
|
893
|
+
});
|
|
894
|
+
res.writeHead(409, { 'Content-Type': 'application/json' });
|
|
895
|
+
res.end(JSON.stringify({
|
|
896
|
+
success: false,
|
|
897
|
+
error: 'This email already has an unverified account. Continue with email verification instead.',
|
|
898
|
+
code: 'EMAIL_ALREADY_EXISTS_UNVERIFIED',
|
|
899
|
+
verificationRequired: true,
|
|
900
|
+
emailSent,
|
|
901
|
+
emailError,
|
|
902
|
+
verificationToken: shouldExposeDevAuthTokens()
|
|
903
|
+
? (verificationToken || null)
|
|
904
|
+
: undefined
|
|
905
|
+
}));
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
873
908
|
const statusCode = Number(error?.statusCode || 400);
|
|
874
909
|
const message = String(error?.message || 'Unable to register');
|
|
875
910
|
auditAuthAction(req, 'auth-register-failed', {
|
|
@@ -733,72 +733,18 @@ function shouldRecordCheckoutPendingState(entitlement) {
|
|
|
733
733
|
|| currentState === 'canceled';
|
|
734
734
|
}
|
|
735
735
|
function beginCheckoutPending(core, input) {
|
|
736
|
-
const planId = resolvePlanIdForVersion(core, input.planVersionId);
|
|
737
736
|
const updatedBy = input.updatedBy || 'checkout-start';
|
|
738
737
|
const source = input.source || 'checkout-start';
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
updatedBy
|
|
748
|
-
});
|
|
749
|
-
return;
|
|
750
|
-
}
|
|
751
|
-
const db = core.getDatabaseAdapter();
|
|
752
|
-
const tenantId = core.getTenantId();
|
|
753
|
-
const now = new Date().toISOString();
|
|
754
|
-
db.prepare(`
|
|
755
|
-
INSERT INTO account_entitlements (account_id, tenant_id, plan_version_id, entitlement_state, effective_at, effective_until, updated_by, created_at, updated_at)
|
|
756
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
757
|
-
ON CONFLICT (account_id) DO UPDATE SET
|
|
758
|
-
plan_version_id = excluded.plan_version_id,
|
|
759
|
-
entitlement_state = excluded.entitlement_state,
|
|
760
|
-
effective_at = excluded.effective_at,
|
|
761
|
-
effective_until = excluded.effective_until,
|
|
762
|
-
updated_by = excluded.updated_by,
|
|
763
|
-
updated_at = excluded.updated_at
|
|
764
|
-
`).run(input.userId, tenantId, input.planVersionId, 'checkout_pending', now, input.effectiveUntil || null, updatedBy, now, now);
|
|
765
|
-
upsertBillingMirror(core, {
|
|
766
|
-
userId: input.userId,
|
|
767
|
-
planId,
|
|
768
|
-
canonicalState: 'checkout_pending',
|
|
769
|
-
effectiveUntil: input.effectiveUntil || null
|
|
738
|
+
core.transitionCanonicalCommercialState({
|
|
739
|
+
accountId: input.userId,
|
|
740
|
+
planVersionId: input.planVersionId,
|
|
741
|
+
state: 'checkout_pending',
|
|
742
|
+
effectiveUntil: input.effectiveUntil || null,
|
|
743
|
+
billingMirrorPlanId: resolvePlanIdForVersion(core, input.planVersionId),
|
|
744
|
+
source,
|
|
745
|
+
updatedBy
|
|
770
746
|
});
|
|
771
747
|
}
|
|
772
|
-
function upsertCanonicalEntitlement(core, input) {
|
|
773
|
-
const updatedBy = input.updatedBy || 'stripe-webhook';
|
|
774
|
-
if (typeof core.transitionCanonicalCommercialState === 'function') {
|
|
775
|
-
core.transitionCanonicalCommercialState({
|
|
776
|
-
accountId: input.userId,
|
|
777
|
-
planVersionId: input.planVersionId,
|
|
778
|
-
state: input.state,
|
|
779
|
-
effectiveAt: input.effectiveAt,
|
|
780
|
-
effectiveUntil: input.effectiveUntil,
|
|
781
|
-
billingMirrorPlanId: resolvePlanIdForVersion(core, input.planVersionId),
|
|
782
|
-
source: updatedBy === 'checkout-confirm' ? 'checkout-confirm' : 'stripe-webhook',
|
|
783
|
-
updatedBy
|
|
784
|
-
});
|
|
785
|
-
return;
|
|
786
|
-
}
|
|
787
|
-
const db = core.getDatabaseAdapter();
|
|
788
|
-
const tenantId = core.getTenantId();
|
|
789
|
-
const now = new Date().toISOString();
|
|
790
|
-
db.prepare(`
|
|
791
|
-
INSERT INTO account_entitlements (account_id, tenant_id, plan_version_id, entitlement_state, effective_at, effective_until, updated_by, created_at, updated_at)
|
|
792
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
793
|
-
ON CONFLICT (account_id) DO UPDATE SET
|
|
794
|
-
plan_version_id = excluded.plan_version_id,
|
|
795
|
-
entitlement_state = excluded.entitlement_state,
|
|
796
|
-
effective_at = excluded.effective_at,
|
|
797
|
-
effective_until = excluded.effective_until,
|
|
798
|
-
updated_by = excluded.updated_by,
|
|
799
|
-
updated_at = excluded.updated_at
|
|
800
|
-
`).run(input.userId, tenantId, input.planVersionId, input.state, input.effectiveAt || now, input.effectiveUntil || null, updatedBy, now, now);
|
|
801
|
-
}
|
|
802
748
|
function upsertBillingMirror(core, input) {
|
|
803
749
|
const db = core.getDatabaseAdapter();
|
|
804
750
|
const tenantId = core.getTenantId();
|
|
@@ -881,12 +827,14 @@ function applySubscriptionSnapshot(core, input) {
|
|
|
881
827
|
}
|
|
882
828
|
}
|
|
883
829
|
else if (planVersionId) {
|
|
884
|
-
|
|
885
|
-
userId,
|
|
830
|
+
core.transitionCanonicalCommercialState({
|
|
831
|
+
accountId: userId,
|
|
886
832
|
planVersionId: planVersionId || existingEntitlement?.plan_version_id || core.getDefaultPlanVersionIdByMode('personal', { allowFallback: true }),
|
|
887
833
|
state: canonicalState,
|
|
888
834
|
effectiveAt: trialEnd || nowIso,
|
|
889
835
|
effectiveUntil,
|
|
836
|
+
billingMirrorPlanId: resolvePlanIdForVersion(core, planVersionId),
|
|
837
|
+
source,
|
|
890
838
|
updatedBy: source
|
|
891
839
|
});
|
|
892
840
|
}
|
|
@@ -2385,12 +2333,15 @@ export function registerBillingRoutes(deps) {
|
|
|
2385
2333
|
});
|
|
2386
2334
|
return;
|
|
2387
2335
|
}
|
|
2388
|
-
|
|
2389
|
-
userId,
|
|
2336
|
+
core.transitionCanonicalCommercialState({
|
|
2337
|
+
accountId: userId,
|
|
2390
2338
|
planVersionId: planVersionId || existingEntitlement?.plan_version_id || core.getDefaultPlanVersionIdByMode('personal', { allowFallback: true }),
|
|
2391
2339
|
state: canonicalState,
|
|
2392
2340
|
effectiveAt: new Date().toISOString(),
|
|
2393
|
-
effectiveUntil: currentPeriodEnd
|
|
2341
|
+
effectiveUntil: currentPeriodEnd,
|
|
2342
|
+
billingMirrorPlanId: resolvePlanIdForVersion(core, planVersionId),
|
|
2343
|
+
source: 'stripe-webhook',
|
|
2344
|
+
updatedBy: 'stripe-webhook'
|
|
2394
2345
|
});
|
|
2395
2346
|
markLatestOpenCheckoutAttemptConfirmed(core, {
|
|
2396
2347
|
userId,
|