@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.
Files changed (165) hide show
  1. package/dist/Taskforce.module.css +102 -5
  2. package/dist/TaskforceCore.js +514 -212
  3. package/dist/TaskforceCore.test.js +1172 -133
  4. package/dist/annotatedAttachments/service.d.ts +1 -1
  5. package/dist/annotatedAttachments/types.d.ts +1 -1
  6. package/dist/components/features/AgentsModule.js +1 -1
  7. package/dist/components/features/AgentsModule.test.js +1 -1
  8. package/dist/components/features/TaskSettings.js +58 -38
  9. package/dist/components/features/TaskSettings.test.js +139 -40
  10. package/dist/components/task/TaskActionHeader.d.ts +2 -1
  11. package/dist/components/task/TaskActionHeader.js +3 -3
  12. package/dist/components/task/TaskActionHeader.test.js +8 -0
  13. package/dist/components/task/TaskCard.js +16 -1
  14. package/dist/components/task/TaskCard.test.js +31 -0
  15. package/dist/components/task/TaskForm.d.ts +2 -1
  16. package/dist/components/task/TaskForm.js +22 -4
  17. package/dist/components/task/TaskForm.test.js +46 -0
  18. package/dist/components/task/TaskKanban.d.ts +1 -1
  19. package/dist/components/task/TaskKanban.js +31 -160
  20. package/dist/components/task/TaskStatusActions.d.ts +6 -1
  21. package/dist/components/task/TaskStatusActions.js +8 -3
  22. package/dist/components/task/TaskStatusActions.test.js +9 -0
  23. package/dist/components/task/taskKanbanDropRules.d.ts +58 -0
  24. package/dist/components/task/taskKanbanDropRules.js +187 -0
  25. package/dist/components/task/taskKanbanDropRules.test.d.ts +1 -0
  26. package/dist/components/task/taskKanbanDropRules.test.js +114 -0
  27. package/dist/components/ui/Modal.d.ts +1 -1
  28. package/dist/components/ui/Modal.js +1 -0
  29. package/dist/components/views/AppShellHeader.js +5 -1
  30. package/dist/components/views/AppShellHeader.test.js +4 -0
  31. package/dist/components/views/PlanComparisonPage.d.ts +3 -2
  32. package/dist/components/views/PlanComparisonPage.js +4 -2
  33. package/dist/components/views/PlanComparisonPage.test.js +4 -0
  34. package/dist/components/views/PlansPage.d.ts +5 -1
  35. package/dist/components/views/PlansPage.js +39 -12
  36. package/dist/components/views/PlansPage.test.js +59 -2
  37. package/dist/components/views/StandaloneLayout.js +113 -38
  38. package/dist/components/views/WidgetView.js +11 -2
  39. package/dist/components/views/standalone/modals/SyncStatusModal.js +1 -1
  40. package/dist/core/DeletedTaskLifecycleService.d.ts +2 -1
  41. package/dist/core/PlanEntitlementService.d.ts +3 -1
  42. package/dist/core/PlanEntitlementService.js +61 -11
  43. package/dist/core/SignupMonetizationSettings.test.js +37 -2
  44. package/dist/core/TaskActivityService.d.ts +4 -1
  45. package/dist/core/TaskActivityService.js +45 -0
  46. package/dist/core/TaskAuditTimeline.test.js +52 -1
  47. package/dist/core/Taskforce.d.ts +15 -11
  48. package/dist/core/Taskforce.js +43 -2
  49. package/dist/core/WorkspacePlanMode.test.js +65 -0
  50. package/dist/documentReviews/service.d.ts +1 -1
  51. package/dist/documentReviews/types.d.ts +1 -1
  52. package/dist/hooks/auth/useTaskforceAuthBootstrap.d.ts +48 -0
  53. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +219 -0
  54. package/dist/hooks/sync/auth.d.ts +37 -0
  55. package/dist/hooks/sync/auth.js +61 -0
  56. package/dist/hooks/sync/bootstrap.d.ts +34 -0
  57. package/dist/hooks/sync/bootstrap.js +52 -0
  58. package/dist/hooks/sync/orchestratorShared.d.ts +36 -0
  59. package/dist/hooks/sync/orchestratorShared.js +209 -0
  60. package/dist/hooks/sync/orchestratorShared.test.d.ts +1 -0
  61. package/dist/hooks/sync/orchestratorShared.test.js +49 -0
  62. package/dist/hooks/sync/realtime.d.ts +25 -0
  63. package/dist/hooks/sync/realtime.js +60 -0
  64. package/dist/hooks/sync/recovery.d.ts +69 -0
  65. package/dist/hooks/sync/recovery.js +118 -0
  66. package/dist/hooks/sync/transfers.d.ts +122 -0
  67. package/dist/hooks/sync/transfers.js +431 -0
  68. package/dist/hooks/sync/useDataVersionRefresh.d.ts +16 -0
  69. package/dist/hooks/sync/useDataVersionRefresh.js +73 -0
  70. package/dist/hooks/ui/useResourceExport.d.ts +3 -1
  71. package/dist/hooks/ui/useResourceExport.js +5 -2
  72. package/dist/hooks/ui/useWorkflowTemplates.js +11 -6
  73. package/dist/hooks/useRealtimeSync.js +2 -1
  74. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +8 -0
  75. package/dist/hooks/useSyncOrchestrator.context-assets.test.d.ts +1 -0
  76. package/dist/hooks/useSyncOrchestrator.context-assets.test.js +228 -0
  77. package/dist/hooks/useSyncOrchestrator.d.ts +5 -4
  78. package/dist/hooks/useSyncOrchestrator.js +314 -873
  79. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +568 -3
  80. package/dist/hooks/useTaskMutations.js +45 -6
  81. package/dist/hooks/useTaskMutations.test.js +147 -0
  82. package/dist/hooks/useTaskforce.d.ts +8 -2
  83. package/dist/hooks/useTaskforce.js +285 -812
  84. package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
  85. package/dist/hooks/useWorkspaceSyncController.js +22 -10
  86. package/dist/hooks/workspace/useTaskforceApiRouting.test.js +17 -0
  87. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +166 -0
  88. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +434 -0
  89. package/dist/hooks/workspace/workspaceLocalState.d.ts +19 -0
  90. package/dist/hooks/workspace/workspaceLocalState.js +100 -0
  91. package/dist/hooks/workspace/workspaceLocalState.test.d.ts +1 -0
  92. package/dist/hooks/workspace/workspaceLocalState.test.js +59 -0
  93. package/dist/localization/locales/en-US.js +2 -2
  94. package/dist/mcp/canonicalAssetHelpers.d.ts +70 -0
  95. package/dist/mcp/canonicalAssetHelpers.js +259 -0
  96. package/dist/mcp/clientIntegrations.d.ts +12 -1
  97. package/dist/mcp/clientIntegrations.js +161 -6
  98. package/dist/mcp/clientIntegrations.test.js +76 -4
  99. package/dist/mcp/documentHelpers.d.ts +100 -0
  100. package/dist/mcp/documentHelpers.js +161 -0
  101. package/dist/mcp/runtime.js +174 -476
  102. package/dist/mcp/runtime.test.js +81 -8
  103. package/dist/mcp/taskAttachmentHelpers.d.ts +41 -0
  104. package/dist/mcp/taskAttachmentHelpers.js +60 -0
  105. package/dist/mcp/taskAttachmentTypes.d.ts +37 -0
  106. package/dist/mcp/taskAttachmentTypes.js +1 -0
  107. package/dist/plugins/vite.js +1 -1
  108. package/dist/runtime/appGateDefinitions.d.ts +1 -1
  109. package/dist/runtime/appGateDefinitions.js +1 -1
  110. package/dist/server/index.d.ts +1 -0
  111. package/dist/server/index.js +46 -3
  112. package/dist/server/index.test.js +12 -1
  113. package/dist/server/routes/admin.js +13 -19
  114. package/dist/server/routes/auth.js +37 -2
  115. package/dist/server/routes/billing.js +18 -67
  116. package/dist/server/routes/billing.test.js +43 -0
  117. package/dist/server/routes/shared.d.ts +30 -0
  118. package/dist/server/routes/shared.js +26 -0
  119. package/dist/server/routes/sync.integration.test.js +20 -0
  120. package/dist/server/routes/sync.js +110 -237
  121. package/dist/server/routes/tasks.js +49 -7
  122. package/dist/server/routes.js +8 -19
  123. package/dist/server/routes.test.js +243 -5
  124. package/dist/shared/taskActor.d.ts +10 -0
  125. package/dist/shared/taskActor.js +1 -0
  126. package/dist/sync/collaborationSyncPayload.js +4 -0
  127. package/dist/sync/collaborationSyncPayload.test.js +8 -0
  128. package/dist/sync/collaborationSyncState.d.ts +3 -1
  129. package/dist/sync/collaborationSyncState.js +19 -0
  130. package/dist/sync/contentSyncState.js +12 -10
  131. package/dist/sync/syncApplyHandlers.d.ts +87 -1
  132. package/dist/sync/syncApplyHandlers.js +370 -8
  133. package/dist/sync/syncApplyHandlers.test.d.ts +1 -0
  134. package/dist/sync/syncApplyHandlers.test.js +377 -0
  135. package/dist/sync/syncService.d.ts +8 -1
  136. package/dist/sync/syncService.js +36 -2
  137. package/dist/sync/syncService.test.js +48 -0
  138. package/dist/sync/workspacePullFeed.d.ts +1 -0
  139. package/dist/sync/workspacePullFeed.js +66 -59
  140. package/dist/sync/workspacePullFeed.test.js +72 -4
  141. package/dist/sync/workspaceSyncModel.d.ts +22 -1
  142. package/dist/sync/workspaceSyncModel.js +65 -0
  143. package/dist/sync/workspaceSyncModel.test.js +119 -0
  144. package/dist/types.d.ts +2 -10
  145. package/dist/ui/assets/AgentsModule-BLWVbuKP.js +1 -0
  146. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-C_TmXZwA.js → AnnotatedAttachmentWorkspace-BlS-cqnl.js} +1 -1
  147. package/dist/ui/assets/{DocumentWorkspace-C3GyzrWm.js → DocumentWorkspace-BH_6DF6x.js} +1 -1
  148. package/dist/ui/assets/{InitiativesModule-4-Rh2K2n.js → InitiativesModule-BjR6iBf9.js} +1 -1
  149. package/dist/ui/assets/{PlansPage-BlVC_lRq.css → PlansPage-C2dd2n1X.css} +1 -1
  150. package/dist/ui/assets/PlansPage-CSDQ4sLa.js +1 -0
  151. package/dist/ui/assets/TaskSettings-CEDy34Jo.js +9 -0
  152. package/dist/ui/assets/{WorkflowsModule-CGk9s3NJ.js → WorkflowsModule-DnFqdUME.js} +1 -1
  153. package/dist/ui/assets/index-DXUdtH1B.js +6 -0
  154. package/dist/ui/assets/index-Ii0B0rTO.css +1 -0
  155. package/dist/ui/assets/{vendor-icons-pWocEipT.js → vendor-icons-I7ZwG1z_.js} +1 -1
  156. package/dist/ui/index.html +4 -4
  157. package/dist/utils/taskActivity.js +4 -0
  158. package/dist/utils/taskActivity.test.js +25 -1
  159. package/package.json +3 -1
  160. package/scripts/playwright-auth.sh +4 -0
  161. package/dist/ui/assets/AgentsModule-CpsTmrIW.js +0 -1
  162. package/dist/ui/assets/PlansPage-BP7AYOqr.js +0 -1
  163. package/dist/ui/assets/TaskSettings-BOC5F6Ag.js +0 -8
  164. package/dist/ui/assets/index-CLIMgR6g.js +0 -6
  165. package/dist/ui/assets/index-DneETxcu.css +0 -1
@@ -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 should usually be passed inline via content');
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('local runtime only today');
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('rejects sourcePath for save_task_attachment in cloud runtime', async () => {
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: 'copy.txt',
1976
- sourcePath: 'docs/source.txt'
2048
+ filename: 'outside.md',
2049
+ sourcePath
1977
2050
  });
1978
- expect(result.content?.[0]?.text).toContain('does not support sourcePath for save_task_attachment yet');
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 or a Taskforce-managed attachment source');
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 {};
@@ -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 Dashboard</title>
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: true;
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: true,
15
+ enabled: false,
16
16
  description: 'Workflow workspace readiness gate.',
17
17
  owner: 'taskforce',
18
18
  removalMilestone: 'Workflow workspace GA'
@@ -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[];
@@ -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 normalizedSetCookies = shouldRewriteSetCookieForLocalDevHost(String(req.headers.host || '').trim())
554
- ? setCookies.map((cookie) => rewriteProxySetCookieForLocalHost(cookie))
555
- : setCookies;
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
- scopes: authScopes,
479
- userId: auth.authenticatedViaToken?.userId || auth.authenticatedViaOAuth?.userId || auth.userId || null,
480
- tokenId: auth.authenticatedViaToken?.tokenId || auth.authenticatedViaOAuth?.grantId || null,
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
- ip: req.socket?.remoteAddress || null,
491
- userAgent: requestHeader(req, 'user-agent', '') || null,
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
- : undefined;
1745
+ : currentOnboardingPolicy.planVersionId;
1752
1746
  const validation = core.validateOnboardingPolicy({
1753
1747
  mode: onboardingPolicyMode,
1754
- ...(onboardingPlanVersionId !== undefined ? { planVersionId: onboardingPlanVersionId } : {})
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\nVerify your email by opening this link:\n${link}\n\nIf you did not create an account, you can ignore this message.`
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
- await cleanupStaleAvatarDrafts(access.userId);
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
- if (typeof core.transitionCanonicalCommercialState === 'function') {
740
- core.transitionCanonicalCommercialState({
741
- accountId: input.userId,
742
- planVersionId: input.planVersionId,
743
- state: 'checkout_pending',
744
- effectiveUntil: input.effectiveUntil || null,
745
- billingMirrorPlanId: planId,
746
- source,
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
- upsertCanonicalEntitlement(core, {
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
- upsertCanonicalEntitlement(core, {
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,