@taskforcehq/taskforce 0.3.301 → 0.3.303

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/dist/Taskforce.module.css +37 -2
  2. package/dist/TaskforceCore.filter-persistence.test.js +1 -1
  3. package/dist/TaskforceCore.js +72 -33
  4. package/dist/TaskforceCore.test.js +848 -130
  5. package/dist/components/features/TaskSettings.js +14 -82
  6. package/dist/components/features/TaskSettings.test.js +33 -70
  7. package/dist/components/features/settings/TaxonomyLibraryManager.js +8 -2
  8. package/dist/components/features/settings/TaxonomyLibraryManager.test.js +1 -1
  9. package/dist/components/task/TaskActionHeader.js +6 -3
  10. package/dist/components/task/TaskCard.js +6 -3
  11. package/dist/components/task/TaskForm.js +2 -2
  12. package/dist/components/task/TaskForm.test.js +41 -2
  13. package/dist/components/task/TaskKanban.test.js +17 -0
  14. package/dist/components/ui/ReferenceBadgeButton.js +1 -1
  15. package/dist/components/views/PlansPage.d.ts +3 -1
  16. package/dist/components/views/PlansPage.js +109 -49
  17. package/dist/components/views/PlansPage.test.d.ts +1 -0
  18. package/dist/components/views/PlansPage.test.js +100 -0
  19. package/dist/components/views/StandaloneLayout.d.ts +1 -0
  20. package/dist/components/views/StandaloneLayout.js +473 -256
  21. package/dist/components/views/panels/PlanningDrawer.js +18 -1
  22. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +48 -0
  23. package/dist/components/views/standalone/modals/AccountHubModal.js +36 -0
  24. package/dist/components/views/standalone/modals/AccountHubModal.test.d.ts +1 -0
  25. package/dist/components/views/standalone/modals/AccountHubModal.test.js +26 -0
  26. package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.d.ts +9 -0
  27. package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.js +8 -0
  28. package/dist/components/views/standalone/modals/EditProfileModal.d.ts +26 -0
  29. package/dist/components/views/standalone/modals/EditProfileModal.js +10 -0
  30. package/dist/components/views/standalone/modals/HelpModal.d.ts +9 -0
  31. package/dist/components/views/standalone/modals/HelpModal.js +8 -0
  32. package/dist/components/views/standalone/modals/ScheduleWarningModal.d.ts +13 -0
  33. package/dist/components/views/standalone/modals/ScheduleWarningModal.js +6 -0
  34. package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +61 -0
  35. package/dist/components/views/standalone/modals/SyncStatusModal.js +38 -0
  36. package/dist/components/views/standalone/modals/TeamManagementModal.d.ts +64 -0
  37. package/dist/components/views/standalone/modals/TeamManagementModal.js +10 -0
  38. package/dist/components/views/standalone/modals/types.d.ts +2 -0
  39. package/dist/components/views/standalone/modals/types.js +1 -0
  40. package/dist/components/views/teamManagementAccess.d.ts +1 -1
  41. package/dist/components/views/teamManagementAccess.js +2 -2
  42. package/dist/components/views/teamManagementAccess.test.js +3 -3
  43. package/dist/config/deployment.js +2 -2
  44. package/dist/config/deployment.test.js +5 -3
  45. package/dist/core/AiProfileService.js +7 -2
  46. package/dist/core/AiProfiles.test.js +19 -0
  47. package/dist/core/EntitlementsPolicy.test.js +2 -2
  48. package/dist/core/GlobalSettingsService.js +78 -18
  49. package/dist/core/PlanEntitlementService.d.ts +29 -5
  50. package/dist/core/PlanEntitlementService.js +297 -219
  51. package/dist/core/SignupMonetizationSettings.test.js +173 -49
  52. package/dist/core/SyncReconciliationService.d.ts +2 -0
  53. package/dist/core/SyncReconciliationService.js +3 -0
  54. package/dist/core/SystemAdmin.test.js +186 -96
  55. package/dist/core/TaskAuditTimeline.test.js +40 -0
  56. package/dist/core/Taskforce.d.ts +100 -10
  57. package/dist/core/Taskforce.ids.test.js +133 -0
  58. package/dist/core/Taskforce.js +459 -127
  59. package/dist/core/Taskforce.mcpAuth.test.js +0 -2
  60. package/dist/core/UserProfileAvatarDrafts.test.d.ts +1 -0
  61. package/dist/core/UserProfileAvatarDrafts.test.js +111 -0
  62. package/dist/core/taskReferencePolicy.d.ts +28 -0
  63. package/dist/core/taskReferencePolicy.js +189 -0
  64. package/dist/core/types.d.ts +13 -12
  65. package/dist/hooks/tasks/useTaskFormActions.js +3 -0
  66. package/dist/hooks/useSyncOrchestrator.js +17 -3
  67. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +97 -5
  68. package/dist/hooks/useTaskData.js +3 -2
  69. package/dist/hooks/useTaskData.test.js +66 -0
  70. package/dist/hooks/useTaskforce.d.ts +8 -0
  71. package/dist/hooks/useTaskforce.js +72 -19
  72. package/dist/hooks/useTaskforce.sync-behavior.test.js +185 -6
  73. package/dist/mcp/aiProfileBootstrap.d.ts +29 -0
  74. package/dist/mcp/aiProfileBootstrap.js +80 -0
  75. package/dist/mcp/aiProfileBootstrap.test.d.ts +1 -0
  76. package/dist/mcp/aiProfileBootstrap.test.js +68 -0
  77. package/dist/mcp/clientIntegrations.d.ts +2 -8
  78. package/dist/mcp/clientIntegrations.js +21 -40
  79. package/dist/mcp/clientIntegrations.test.js +28 -2
  80. package/dist/mcp/clientProfileDefaults.d.ts +12 -0
  81. package/dist/mcp/clientProfileDefaults.js +37 -0
  82. package/dist/mcp/runtime.d.ts +10 -7
  83. package/dist/mcp/runtime.js +131 -48
  84. package/dist/mcp/runtime.test.js +321 -83
  85. package/dist/migrations/billingSchemaParity.test.js +3 -0
  86. package/dist/migrations/taskSchemaMigrations.js +68 -1
  87. package/dist/qaOpen.test.d.ts +1 -0
  88. package/dist/qaOpen.test.js +56 -0
  89. package/dist/server/index.cookieProxy.test.js +2 -0
  90. package/dist/server/index.d.ts +1 -0
  91. package/dist/server/index.js +17 -1
  92. package/dist/server/index.test.js +15 -1
  93. package/dist/server/mockStripe.d.ts +1 -0
  94. package/dist/server/mockStripe.js +2 -0
  95. package/dist/server/routes/admin.js +105 -45
  96. package/dist/server/routes/auth.d.ts +6 -0
  97. package/dist/server/routes/auth.js +505 -30
  98. package/dist/server/routes/billing.js +517 -24
  99. package/dist/server/routes/billing.test.js +967 -45
  100. package/dist/server/routes/documents.js +6 -1
  101. package/dist/server/routes/shared.d.ts +1 -1
  102. package/dist/server/routes/shared.js +16 -6
  103. package/dist/server/routes/sync.integration.test.js +290 -0
  104. package/dist/server/routes/sync.js +234 -1922
  105. package/dist/server/routes/tasks.js +18 -4
  106. package/dist/server/routes.js +29 -22
  107. package/dist/server/routes.test.js +639 -82
  108. package/dist/shared/runtimeContract.d.ts +1 -1
  109. package/dist/sync/cloudSyncApi.d.ts +1 -0
  110. package/dist/sync/collaborationSyncPayload.d.ts +17 -0
  111. package/dist/sync/collaborationSyncPayload.js +131 -0
  112. package/dist/sync/collaborationSyncPayload.test.d.ts +1 -0
  113. package/dist/sync/collaborationSyncPayload.test.js +104 -0
  114. package/dist/sync/collaborationSyncState.d.ts +15 -0
  115. package/dist/sync/collaborationSyncState.js +60 -0
  116. package/dist/sync/contentSyncPayload.d.ts +45 -0
  117. package/dist/sync/contentSyncPayload.js +104 -0
  118. package/dist/sync/contentSyncPayload.test.d.ts +1 -0
  119. package/dist/sync/contentSyncPayload.test.js +104 -0
  120. package/dist/sync/contentSyncState.d.ts +98 -0
  121. package/dist/sync/contentSyncState.js +1004 -0
  122. package/dist/sync/planningSyncPayload.d.ts +9 -0
  123. package/dist/sync/planningSyncPayload.js +82 -0
  124. package/dist/sync/planningSyncPayload.test.d.ts +1 -0
  125. package/dist/sync/planningSyncPayload.test.js +68 -0
  126. package/dist/sync/syncApplyHandlers.d.ts +83 -0
  127. package/dist/sync/syncApplyHandlers.js +445 -0
  128. package/dist/sync/syncService.d.ts +1 -0
  129. package/dist/sync/syncService.js +21 -8
  130. package/dist/sync/taskSyncPayload.d.ts +33 -0
  131. package/dist/sync/taskSyncPayload.js +126 -0
  132. package/dist/sync/taskSyncPayload.test.d.ts +1 -0
  133. package/dist/sync/taskSyncPayload.test.js +54 -0
  134. package/dist/sync/workspacePullFeed.d.ts +38 -0
  135. package/dist/sync/workspacePullFeed.js +349 -0
  136. package/dist/sync/workspacePullFeed.test.d.ts +1 -0
  137. package/dist/sync/workspacePullFeed.test.js +278 -0
  138. package/dist/sync/workspaceSyncModel.d.ts +4 -1
  139. package/dist/sync/workspaceSyncModel.js +9 -2
  140. package/dist/sync/workspaceSyncModel.test.js +29 -0
  141. package/dist/test/setup.js +1 -1
  142. package/dist/types.d.ts +1 -0
  143. package/dist/ui/assets/{AgentsModule-Bdq3T1Ts.js → AgentsModule-CpsTmrIW.js} +1 -1
  144. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-jKUVo-PJ.js → AnnotatedAttachmentWorkspace-C_TmXZwA.js} +1 -1
  145. package/dist/ui/assets/{DocumentWorkspace-DuQa-uIC.js → DocumentWorkspace-C3GyzrWm.js} +2 -2
  146. package/dist/ui/assets/{InitiativesModule-C8Pi2VTk.js → InitiativesModule-4-Rh2K2n.js} +1 -1
  147. package/dist/ui/assets/PlansPage-BP7AYOqr.js +1 -0
  148. package/dist/ui/assets/TaskSettings-BOC5F6Ag.js +8 -0
  149. package/dist/ui/assets/{TaskSettings-BwB9NtmJ.css → TaskSettings-CnIBL_Eb.css} +1 -1
  150. package/dist/ui/assets/{WorkflowsModule-b6TLuHwJ.js → WorkflowsModule-CGk9s3NJ.js} +1 -1
  151. package/dist/ui/assets/index-CLIMgR6g.js +6 -0
  152. package/dist/ui/assets/index-DneETxcu.css +1 -0
  153. package/dist/ui/assets/{vendor-icons-QZyhEwgT.js → vendor-icons-pWocEipT.js} +1 -1
  154. package/dist/ui/index.html +3 -3
  155. package/dist/utils/accountProfileSummaryCache.d.ts +31 -0
  156. package/dist/utils/accountProfileSummaryCache.js +97 -0
  157. package/dist/utils/accountProfileSummaryCache.test.d.ts +1 -0
  158. package/dist/utils/accountProfileSummaryCache.test.js +63 -0
  159. package/dist/utils/taskActivity.js +17 -4
  160. package/dist/utils/taskActivity.test.js +27 -0
  161. package/dist/utils/taskNormalization.js +10 -2
  162. package/dist/utils/taskReferences.d.ts +11 -1
  163. package/dist/utils/taskReferences.js +33 -1
  164. package/package.json +4 -3
  165. package/scripts/qa-open-lib.mjs +175 -0
  166. package/scripts/qa-open-lib.mjs.d.ts +49 -0
  167. package/scripts/qa-open.mjs +352 -0
  168. package/dist/ui/assets/PlansPage-B6BDECSi.js +0 -1
  169. package/dist/ui/assets/TaskSettings-DUJgRTPU.js +0 -8
  170. package/dist/ui/assets/index-BiiF1cLQ.css +0 -1
  171. package/dist/ui/assets/index-xBWLPOEb.js +0 -6
@@ -20,7 +20,9 @@
20
20
  * POST /api/taskforce/auth/invite/join
21
21
  * POST /api/taskforce/auth/logout
22
22
  * GET /api/taskforce/account/access-status
23
+ * GET /api/taskforce/account/profile-summary
23
24
  * GET /api/taskforce/account/entitlements
25
+ * GET /api/taskforce/account/team-management-access
24
26
  * GET /api/taskforce/account/plan-preview
25
27
  * GET /api/taskforce/auth/workspace-members
26
28
  * GET /api/taskforce/workspace/assignee-options
@@ -33,15 +35,160 @@
33
35
  * DELETE /api/taskforce/admin/users/:id
34
36
  * GET /api/taskforce/admin/workspace-audit-logs
35
37
  */
38
+ import * as fs from 'fs';
39
+ import * as path from 'path';
40
+ import { randomUUID } from 'crypto';
36
41
  import { parseJsonBody } from '../routes.js';
37
42
  import { hasValidSession, createClearSessionCookie, createSessionTokenCookie, resolveRequestAccess, resolveSessionCookieAttributesForRequest, } from '../auth.js';
38
43
  import { resolveEffectiveEmailConfig, sendEmail } from '../email.js';
39
44
  import { resolveDeploymentConfig } from '../../config/deployment.js';
40
45
  import { resolveRuntimeModeContract } from '../../shared/runtimeContract.js';
41
- import { resolveErrorStatusCode } from './shared.js';
46
+ import { requestHeader, resolveErrorStatusCode } from './shared.js';
47
+ import { normalizeTaskDataRelativePath } from '../../storage/objectStorageClient.js';
42
48
  // === SECTION: Registration ===
43
49
  export function registerAuthRoutes(deps) {
44
- const { addRoute, core, context, requestWorkspaceId, requestUserId, ensureAppAccess, ensureAuthenticatedUser, ensureWorkspaceAdminRole, ensureCloudPlanData, requireCloudRuntime, auditAuthAction, resolveAdminRequestActor, shouldExposeDevAuthTokens, resolveRuntimeConfigBaseUrl, resolvePublicAuthBaseUrl, } = deps;
50
+ const { addRoute, core, context, requestWorkspaceId, requestUserId, ensureAppAccess, ensureAuthenticatedUser, ensureWorkspaceAdminRole, ensureCloudPlanData, requireCloudRuntime, auditAuthAction, resolveAdminRequestActor, shouldExposeDevAuthTokens, resolveRuntimeConfigBaseUrl, resolvePublicAuthBaseUrl, resolveEffectiveObjectStorageConfig, getObjectStorageClient, } = deps;
51
+ const normalizeTeamWorkspaceRole = (value) => {
52
+ const role = String(value || '').trim().toLowerCase();
53
+ if (role === 'owner' || role === 'admin' || role === 'member' || role === 'read-only')
54
+ return role;
55
+ return null;
56
+ };
57
+ const resolveBooleanSetting = (value, fallback) => {
58
+ if (typeof value === 'boolean')
59
+ return value;
60
+ if (typeof value === 'number')
61
+ return value !== 0;
62
+ const normalized = String(value || '').trim().toLowerCase();
63
+ if (!normalized)
64
+ return fallback;
65
+ if (['1', 'true', 'yes', 'on'].includes(normalized))
66
+ return true;
67
+ if (['0', 'false', 'no', 'off'].includes(normalized))
68
+ return false;
69
+ return fallback;
70
+ };
71
+ const resolveTeamPlanModeFromEntitlements = (payload) => {
72
+ const state = String(payload?.state || '').trim().toLowerCase();
73
+ const teamAllowed = payload?.features?.['collaboration.team_management']?.allowed === true;
74
+ const stateAllows = state === 'active' || state === 'trialing' || state === 'grace';
75
+ return teamAllowed && stateAllows ? 'team' : 'personal';
76
+ };
77
+ const resolveBillingConflictCodeForUser = (userIdRaw, gateRaw) => {
78
+ const userId = String(userIdRaw || '').trim();
79
+ const gate = String(gateRaw || '').trim().toLowerCase();
80
+ if (!userId || !gate || gate === 'ok')
81
+ return null;
82
+ const db = core.getDatabaseAdapter();
83
+ const tenantId = core.getTenantId();
84
+ try {
85
+ const row = db.prepare(`
86
+ SELECT session_id, status, expires_at
87
+ FROM billing_checkout_attempts
88
+ WHERE tenant_id = ? AND user_id = ?
89
+ ORDER BY created_at DESC, session_id DESC
90
+ LIMIT 1
91
+ `).get(tenantId, userId);
92
+ const sessionId = String(row?.session_id || '').trim();
93
+ const status = String(row?.status || '').trim().toLowerCase();
94
+ const expiresAt = String(row?.expires_at || '').trim();
95
+ if (!sessionId)
96
+ return null;
97
+ if (status === 'open' && expiresAt) {
98
+ const expiresAtMs = Date.parse(expiresAt);
99
+ if (Number.isFinite(expiresAtMs) && expiresAtMs <= Date.now()) {
100
+ db.prepare(`
101
+ UPDATE billing_checkout_attempts
102
+ SET status = 'expired', updated_at = ?
103
+ WHERE tenant_id = ? AND session_id = ?
104
+ `).run(new Date().toISOString(), tenantId, sessionId);
105
+ return 'CHECKOUT_SESSION_EXPIRED';
106
+ }
107
+ }
108
+ if (status === 'expired')
109
+ return 'CHECKOUT_SESSION_EXPIRED';
110
+ if (status === 'superseded')
111
+ return 'CHECKOUT_SESSION_SUPERSEDED';
112
+ }
113
+ catch {
114
+ return null;
115
+ }
116
+ return null;
117
+ };
118
+ const buildAccountProfileSummary = (req) => {
119
+ const userId = requestUserId(req);
120
+ const workspaceId = requestWorkspaceId(req);
121
+ const access = core.getAccountAccessStatus(userId);
122
+ const resolvedAccess = core.resolveUserAccess(userId, workspaceId, 'member');
123
+ const hintedRole = normalizeTeamWorkspaceRole(requestHeader(req, 'x-taskforce-workspace-role', ''));
124
+ const workspaceRole = normalizeTeamWorkspaceRole(resolvedAccess?.role || hintedRole || null);
125
+ const canManageWorkspace = workspaceRole === 'owner' || workspaceRole === 'admin';
126
+ const db = core.getDatabaseAdapter();
127
+ const tenantId = core.getTenantId();
128
+ let billingRow;
129
+ try {
130
+ billingRow = db.prepare(`
131
+ SELECT stripe_status, stripe_customer_id, stripe_subscription_id, stripe_price_id, billing_interval,
132
+ trial_end, current_period_end, effective_until, entitlement_state, tier
133
+ FROM user_billing
134
+ WHERE tenant_id = ? AND user_id = ?
135
+ LIMIT 1
136
+ `).get(tenantId, userId);
137
+ }
138
+ catch {
139
+ billingRow = undefined;
140
+ }
141
+ const planId = String(access.entitlement?.planId || billingRow?.tier || '').trim() || null;
142
+ const planVersionId = String(access.entitlement?.planVersionId || '').trim() || null;
143
+ let planRow;
144
+ if (planId) {
145
+ try {
146
+ planRow = db.prepare(`
147
+ SELECT display_name
148
+ FROM plan_catalog
149
+ WHERE tenant_id = ? AND plan_id = ?
150
+ LIMIT 1
151
+ `).get(tenantId, planId);
152
+ }
153
+ catch {
154
+ planRow = undefined;
155
+ }
156
+ }
157
+ let teamPlanMode = 'unknown';
158
+ try {
159
+ const entitlements = core.getAccountEntitlementsPayload({ userId, workspaceId });
160
+ teamPlanMode = resolveTeamPlanModeFromEntitlements(entitlements);
161
+ }
162
+ catch (error) {
163
+ if (error?.code === 'ENTITLEMENT_NOT_FOUND' && (access.gate === 'plan_selection_required' || access.gate === 'checkout_pending')) {
164
+ teamPlanMode = 'personal';
165
+ }
166
+ }
167
+ const billingConflictCode = resolveBillingConflictCodeForUser(userId, access.gate);
168
+ return {
169
+ stripeStatus: billingRow?.stripe_status || null,
170
+ stripeCustomerId: billingRow?.stripe_customer_id || null,
171
+ stripeSubscriptionId: billingRow?.stripe_subscription_id || null,
172
+ stripePriceId: billingRow?.stripe_price_id || null,
173
+ billingInterval: billingRow?.billing_interval || null,
174
+ trialEnd: billingRow?.trial_end || null,
175
+ currentPeriodEnd: billingRow?.current_period_end || null,
176
+ effectiveUntil: billingRow?.effective_until || null,
177
+ entitlementState: access.entitlement?.state || billingRow?.entitlement_state || null,
178
+ planVersionId,
179
+ planId,
180
+ planName: planRow?.display_name ? String(planRow.display_name) : planId,
181
+ gate: access.gate,
182
+ message: access.message,
183
+ billingConflictCode,
184
+ planSelectionRequired: access.planSelectionRequired,
185
+ workspaceId,
186
+ workspaceRole,
187
+ canManageWorkspace,
188
+ teamPlanMode,
189
+ teamManagementAllowed: canManageWorkspace && teamPlanMode === 'team'
190
+ };
191
+ };
45
192
  // === SECTION: Email delivery helpers ===
46
193
  const getEmailConfig = () => {
47
194
  if (context.authConfig?.runtimeMode !== 'cloud') {
@@ -55,30 +202,94 @@ export function registerAuthRoutes(deps) {
55
202
  const authSettings = globalSettings.auth || {};
56
203
  const authRateLimitMaxRequestsRaw = Number(authSettings.authRateLimitMaxRequests);
57
204
  const authRateLimitWindowMsRaw = Number(authSettings.authRateLimitWindowMs);
58
- const signupSettings = core.getSignupMonetizationSettings();
205
+ const onboardingPolicy = core.getOnboardingPolicy();
59
206
  return {
60
- allowSelfRegistration: authSettings.allowSelfRegistration !== false,
61
- requireVerifiedEmail: authSettings.requireVerifiedEmail !== false,
62
- defaultNewUserBetaAccess: authSettings.defaultNewUserBetaAccess === true,
63
- authRateLimitEnabled: authSettings.authRateLimitEnabled !== false,
207
+ allowSelfRegistration: resolveBooleanSetting(authSettings.allowSelfRegistration, true),
208
+ requireVerifiedEmail: resolveBooleanSetting(authSettings.requireVerifiedEmail, true),
209
+ defaultNewUserBetaAccess: resolveBooleanSetting(authSettings.defaultNewUserBetaAccess, false),
210
+ authRateLimitEnabled: resolveBooleanSetting(authSettings.authRateLimitEnabled, true),
64
211
  authRateLimitMaxRequests: Number.isFinite(authRateLimitMaxRequestsRaw) && authRateLimitMaxRequestsRaw >= 1
65
212
  ? Math.floor(authRateLimitMaxRequestsRaw)
66
213
  : 10,
67
214
  authRateLimitWindowMs: Number.isFinite(authRateLimitWindowMsRaw) && authRateLimitWindowMsRaw >= 1000
68
215
  ? Math.floor(authRateLimitWindowMsRaw)
69
216
  : 15 * 60_000,
70
- signupMonetizationStrategy: signupSettings.strategy,
71
- defaultSignupPlanVersionId: signupSettings.defaultSignupPlanVersionId
217
+ onboardingPolicyMode: onboardingPolicy.mode,
218
+ onboardingPlanVersionId: onboardingPolicy.planVersionId
72
219
  };
73
220
  };
74
221
  const getSiteBehavior = () => {
75
222
  const globalSettings = core.getGlobalSettings();
76
223
  const siteSettings = globalSettings.site || {};
77
224
  return {
78
- showRunTaskforceLocally: siteSettings.showRunTaskforceLocally !== false,
79
- pricingPageEnabled: siteSettings.pricingPageEnabled !== false
225
+ showRunTaskforceLocally: resolveBooleanSetting(siteSettings.showRunTaskforceLocally, true),
226
+ pricingPageEnabled: resolveBooleanSetting(siteSettings.pricingPageEnabled, true)
80
227
  };
81
228
  };
229
+ const buildPasswordSessionCookieForUser = (req, authConfig, userIdRaw, options) => {
230
+ const userId = String(userIdRaw || '').trim();
231
+ const fallbackRole = String(options?.fallbackRole || authConfig.defaultRole || 'member').trim() || 'member';
232
+ const resolvedWorkspaceId = core.resolvePreferredUserWorkspaceId(userId, {
233
+ preferredWorkspaceId: options?.preferredWorkspaceId || null,
234
+ persistedWorkspaceId: options?.persistedWorkspaceId || null
235
+ });
236
+ const access = resolvedWorkspaceId
237
+ ? core.resolveUserAccess(userId, resolvedWorkspaceId, fallbackRole)
238
+ : null;
239
+ return createSessionTokenCookie(`pwd:${userId}`, authConfig, {
240
+ role: (access?.role || fallbackRole),
241
+ workspaceId: access?.workspaceId || '',
242
+ userId,
243
+ ...resolveSessionCookieAttributesForRequest(req, authConfig)
244
+ });
245
+ };
246
+ const AVATAR_DRAFT_RETENTION_MS = 24 * 60 * 60 * 1000;
247
+ const MAX_AVATAR_UPLOAD_BYTES = 5 * 1024 * 1024;
248
+ const AVATAR_MIME_EXTENSION = {
249
+ 'image/png': '.png',
250
+ 'image/jpeg': '.jpg',
251
+ 'image/webp': '.webp',
252
+ 'image/gif': '.gif'
253
+ };
254
+ const buildUserAvatarStoragePath = (userIdRaw, draftIdRaw, originalNameRaw, mimeTypeRaw) => {
255
+ const userId = String(userIdRaw || '').trim().replace(/[^a-zA-Z0-9._-]/g, '-') || 'user';
256
+ const draftId = String(draftIdRaw || '').trim().replace(/[^a-zA-Z0-9._-]/g, '-') || randomUUID().replace(/-/g, '');
257
+ const originalName = String(originalNameRaw || '').trim();
258
+ const mimeType = String(mimeTypeRaw || '').trim().toLowerCase();
259
+ const originalExt = originalName ? path.extname(originalName).toLowerCase() : '';
260
+ const ext = originalExt || AVATAR_MIME_EXTENSION[mimeType] || '.bin';
261
+ const stem = (originalName ? path.basename(originalName, originalExt) : 'avatar')
262
+ .replace(/[^a-zA-Z0-9._-]/g, '-')
263
+ .replace(/-+/g, '-')
264
+ .replace(/^-+|-+$/g, '')
265
+ .slice(0, 64) || 'avatar';
266
+ return `users/${userId}/profile/avatar/${draftId}-${stem}${ext}`;
267
+ };
268
+ const deleteStoredAvatarObject = async (relativePathRaw) => {
269
+ const relativePath = normalizeTaskDataRelativePath(String(relativePathRaw || '').trim());
270
+ if (!relativePath)
271
+ return;
272
+ const objectStorageClient = getObjectStorageClient();
273
+ const effectiveObjectStorage = resolveEffectiveObjectStorageConfig();
274
+ if (objectStorageClient && effectiveObjectStorage.provider === 'r2') {
275
+ await objectStorageClient.deleteObject(relativePath).catch(() => { });
276
+ return;
277
+ }
278
+ const { basePath } = core.getPaths();
279
+ const resolvedPath = path.resolve(basePath, relativePath);
280
+ if (!resolvedPath.startsWith(path.resolve(basePath)))
281
+ return;
282
+ if (fs.existsSync(resolvedPath)) {
283
+ fs.rmSync(resolvedPath, { force: true });
284
+ }
285
+ };
286
+ const cleanupStaleAvatarDrafts = async (userId) => {
287
+ const cutoff = new Date(Date.now() - AVATAR_DRAFT_RETENTION_MS).toISOString();
288
+ const staleDrafts = core.cleanupStaleUserAvatarUploadDrafts(userId, cutoff);
289
+ for (const draft of staleDrafts) {
290
+ await deleteStoredAvatarObject(draft.storageKey);
291
+ }
292
+ };
82
293
  const logAuthLink = (kind, email, token, req) => {
83
294
  const base = resolvePublicAuthBaseUrl(req);
84
295
  const urlPath = kind === 'verify-email'
@@ -187,8 +398,10 @@ export function registerAuthRoutes(deps) {
187
398
  let userId = access?.userId || 'anonymous';
188
399
  let email = null;
189
400
  let displayName = null;
401
+ let avatarUrl = null;
190
402
  let betaAccess = true;
191
403
  if (authenticated && access?.userId) {
404
+ await cleanupStaleAvatarDrafts(access.userId);
192
405
  betaAccess = core.getUserBetaAccess(access.userId);
193
406
  const resolved = core.resolveUserAccess(access.userId, workspaceId, role);
194
407
  if (resolved) {
@@ -204,6 +417,7 @@ export function registerAuthRoutes(deps) {
204
417
  const identity = core.resolveUserIdentity(userId);
205
418
  email = identity?.email || null;
206
419
  displayName = identity?.displayName || null;
420
+ avatarUrl = identity?.avatarUrl || null;
207
421
  }
208
422
  res.writeHead(200, { 'Content-Type': 'application/json' });
209
423
  res.end(JSON.stringify({
@@ -213,6 +427,7 @@ export function registerAuthRoutes(deps) {
213
427
  role,
214
428
  email,
215
429
  displayName,
430
+ avatarUrl,
216
431
  betaAccess
217
432
  }));
218
433
  });
@@ -229,28 +444,67 @@ export function registerAuthRoutes(deps) {
229
444
  addRoute('POST', '/api/taskforce/auth/profile', async (req, res) => {
230
445
  if (!requireCloudRuntime(res, 'Authentication'))
231
446
  return;
232
- if (!ensureAuthenticatedUser(req)) {
233
- res.writeHead(401, { 'Content-Type': 'application/json' });
234
- res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
447
+ const authConfig = context.authConfig;
448
+ if (!authConfig) {
449
+ res.writeHead(500, { 'Content-Type': 'application/json' });
450
+ res.end(JSON.stringify({ success: false, error: 'Auth context unavailable.' }));
235
451
  return;
236
452
  }
237
- const userId = String(requestUserId(req) || '').trim();
238
- if (!userId || userId === 'anonymous') {
453
+ const access = resolveRequestAccess(req, authConfig);
454
+ const userId = String(access.userId || '').trim();
455
+ if (!access.authenticated || !userId || userId === 'anonymous') {
239
456
  res.writeHead(401, { 'Content-Type': 'application/json' });
240
457
  res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
241
458
  return;
242
459
  }
243
460
  const body = await parseJsonBody(req);
244
461
  try {
245
- const updated = core.updateUserDisplayName(userId, String(body?.displayName || ''));
462
+ await cleanupStaleAvatarDrafts(userId);
463
+ const avatarDraftId = typeof body?.avatarDraftId === 'string' ? body.avatarDraftId.trim() : '';
464
+ const clearAvatar = body?.clearAvatar === true;
465
+ let avatarUrl;
466
+ if (avatarDraftId) {
467
+ const draft = core.getUserAvatarUploadDraft(userId, avatarDraftId);
468
+ if (!draft || draft.discardedAt || !draft.finalizedAt) {
469
+ res.writeHead(400, { 'Content-Type': 'application/json' });
470
+ res.end(JSON.stringify({ success: false, error: 'Avatar draft is invalid or expired.', code: 'AVATAR_DRAFT_INVALID' }));
471
+ return;
472
+ }
473
+ avatarUrl = `/api/taskforce/context/${encodeURIComponent(draft.storageKey)}`;
474
+ }
475
+ else if (clearAvatar) {
476
+ avatarUrl = null;
477
+ }
478
+ const previousIdentity = core.resolveUserIdentity(userId);
479
+ const updated = core.updateUserProfile(userId, {
480
+ displayName: String(body?.displayName || ''),
481
+ ...((avatarDraftId || clearAvatar) ? { avatarUrl: avatarUrl || null } : {})
482
+ });
246
483
  if (!updated) {
247
484
  res.writeHead(404, { 'Content-Type': 'application/json' });
248
485
  res.end(JSON.stringify({ success: false, error: 'User not found.', code: 'USER_NOT_FOUND' }));
249
486
  return;
250
487
  }
488
+ if (avatarDraftId) {
489
+ core.markUserAvatarUploadDraftConsumed(userId, avatarDraftId);
490
+ }
491
+ if (avatarDraftId || clearAvatar) {
492
+ const previousAvatarUrl = String(previousIdentity?.avatarUrl || '').trim();
493
+ const nextAvatarUrl = String(updated.avatarUrl || '').trim();
494
+ const currentDraft = avatarDraftId ? core.getUserAvatarUploadDraft(userId, avatarDraftId) : null;
495
+ const currentStorageKey = String(currentDraft?.storageKey || '').trim();
496
+ if (previousAvatarUrl.startsWith('/api/taskforce/context/')
497
+ && previousAvatarUrl !== nextAvatarUrl) {
498
+ const previousStorageKey = normalizeTaskDataRelativePath(decodeURIComponent(previousAvatarUrl.slice('/api/taskforce/context/'.length)));
499
+ if (previousStorageKey && previousStorageKey !== currentStorageKey) {
500
+ await deleteStoredAvatarObject(previousStorageKey);
501
+ }
502
+ }
503
+ }
251
504
  auditAuthAction(req, 'auth-profile-updated', {
252
505
  userId,
253
- displayName: updated.displayName || null
506
+ displayName: updated.displayName || null,
507
+ avatarUrl: updated.avatarUrl || null
254
508
  });
255
509
  res.writeHead(200, { 'Content-Type': 'application/json' });
256
510
  res.end(JSON.stringify({
@@ -258,7 +512,8 @@ export function registerAuthRoutes(deps) {
258
512
  profile: {
259
513
  userId: updated.userId,
260
514
  email: updated.email,
261
- displayName: updated.displayName || null
515
+ displayName: updated.displayName || null,
516
+ avatarUrl: updated.avatarUrl || null
262
517
  }
263
518
  }));
264
519
  }
@@ -268,6 +523,161 @@ export function registerAuthRoutes(deps) {
268
523
  res.end(JSON.stringify({ success: false, error: String(error?.message || 'Unable to update profile.'), code: error?.code }));
269
524
  }
270
525
  });
526
+ addRoute('POST', '/api/taskforce/auth/profile/avatar/init', async (req, res) => {
527
+ if (!requireCloudRuntime(res, 'Authentication'))
528
+ return;
529
+ const authConfig = context.authConfig;
530
+ if (!authConfig) {
531
+ res.writeHead(500, { 'Content-Type': 'application/json' });
532
+ res.end(JSON.stringify({ success: false, error: 'Auth context unavailable.' }));
533
+ return;
534
+ }
535
+ const access = resolveRequestAccess(req, authConfig);
536
+ const userId = String(access.userId || '').trim();
537
+ if (!access.authenticated || !userId || userId === 'anonymous') {
538
+ res.writeHead(401, { 'Content-Type': 'application/json' });
539
+ res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
540
+ return;
541
+ }
542
+ const objectStorageClient = getObjectStorageClient();
543
+ const effectiveObjectStorage = resolveEffectiveObjectStorageConfig();
544
+ if (!objectStorageClient || effectiveObjectStorage.provider !== 'r2' || !effectiveObjectStorage.r2) {
545
+ res.writeHead(501, { 'Content-Type': 'application/json' });
546
+ res.end(JSON.stringify({ success: false, error: 'Avatar uploads require object storage.' }));
547
+ return;
548
+ }
549
+ const body = await parseJsonBody(req);
550
+ const originalName = String(body?.originalName || '').trim();
551
+ const mimeType = String(body?.mimeType || '').trim().toLowerCase();
552
+ const size = Number(body?.size || 0);
553
+ if (!originalName || !mimeType.startsWith('image/') || !(mimeType in AVATAR_MIME_EXTENSION)) {
554
+ res.writeHead(400, { 'Content-Type': 'application/json' });
555
+ res.end(JSON.stringify({ success: false, error: 'A supported image file is required.', code: 'AVATAR_FILE_INVALID' }));
556
+ return;
557
+ }
558
+ if (!Number.isFinite(size) || size <= 0 || size > MAX_AVATAR_UPLOAD_BYTES) {
559
+ res.writeHead(400, { 'Content-Type': 'application/json' });
560
+ res.end(JSON.stringify({ success: false, error: 'Avatar must be 5 MB or smaller.', code: 'AVATAR_SIZE_INVALID' }));
561
+ return;
562
+ }
563
+ await cleanupStaleAvatarDrafts(userId);
564
+ const draftId = `avatar-draft-${randomUUID().replace(/-/g, '')}`;
565
+ const relativePath = buildUserAvatarStoragePath(userId, draftId, originalName, mimeType);
566
+ core.createUserAvatarUploadDraft({
567
+ userId,
568
+ draftId,
569
+ storageKey: relativePath,
570
+ mimeType,
571
+ originalFilename: originalName,
572
+ sizeBytes: size,
573
+ storageProvider: 'r2'
574
+ });
575
+ const uploadUrl = objectStorageClient.createSignedPutUrl(relativePath, effectiveObjectStorage.r2.signedUploadTtlSeconds, mimeType);
576
+ res.writeHead(200, { 'Content-Type': 'application/json' });
577
+ res.end(JSON.stringify({
578
+ success: true,
579
+ mode: 'direct',
580
+ draftId,
581
+ uploadUrl,
582
+ method: 'PUT',
583
+ headers: { 'Content-Type': mimeType },
584
+ relativePath,
585
+ path: `/api/taskforce/context/${encodeURIComponent(relativePath)}`
586
+ }));
587
+ });
588
+ addRoute('POST', '/api/taskforce/auth/profile/avatar/finalize', async (req, res) => {
589
+ if (!requireCloudRuntime(res, 'Authentication'))
590
+ return;
591
+ const authConfig = context.authConfig;
592
+ if (!authConfig) {
593
+ res.writeHead(500, { 'Content-Type': 'application/json' });
594
+ res.end(JSON.stringify({ success: false, error: 'Auth context unavailable.' }));
595
+ return;
596
+ }
597
+ const access = resolveRequestAccess(req, authConfig);
598
+ const userId = String(access.userId || '').trim();
599
+ if (!access.authenticated || !userId || userId === 'anonymous') {
600
+ res.writeHead(401, { 'Content-Type': 'application/json' });
601
+ res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
602
+ return;
603
+ }
604
+ const body = await parseJsonBody(req);
605
+ const draftId = String(body?.draftId || '').trim();
606
+ const relativePath = normalizeTaskDataRelativePath(String(body?.relativePath || '').trim());
607
+ if (!draftId || !relativePath) {
608
+ res.writeHead(400, { 'Content-Type': 'application/json' });
609
+ res.end(JSON.stringify({ success: false, error: 'draftId and relativePath are required.' }));
610
+ return;
611
+ }
612
+ await cleanupStaleAvatarDrafts(userId);
613
+ const draft = core.getUserAvatarUploadDraft(userId, draftId);
614
+ if (!draft || draft.discardedAt || draft.storageKey !== relativePath) {
615
+ res.writeHead(400, { 'Content-Type': 'application/json' });
616
+ res.end(JSON.stringify({ success: false, error: 'Avatar draft is invalid or expired.', code: 'AVATAR_DRAFT_INVALID' }));
617
+ return;
618
+ }
619
+ const objectStorageClient = getObjectStorageClient();
620
+ if (objectStorageClient) {
621
+ const uploaded = await objectStorageClient.getObject(relativePath);
622
+ if (!uploaded) {
623
+ res.writeHead(404, { 'Content-Type': 'application/json' });
624
+ res.end(JSON.stringify({ success: false, error: 'Uploaded avatar not found.', code: 'AVATAR_UPLOAD_MISSING' }));
625
+ return;
626
+ }
627
+ }
628
+ else {
629
+ const { basePath } = core.getPaths();
630
+ const resolvedPath = path.resolve(basePath, relativePath);
631
+ if (!resolvedPath.startsWith(path.resolve(basePath)) || !fs.existsSync(resolvedPath)) {
632
+ res.writeHead(404, { 'Content-Type': 'application/json' });
633
+ res.end(JSON.stringify({ success: false, error: 'Uploaded avatar not found.', code: 'AVATAR_UPLOAD_MISSING' }));
634
+ return;
635
+ }
636
+ }
637
+ const finalized = core.markUserAvatarUploadDraftFinalized(userId, draftId);
638
+ res.writeHead(200, { 'Content-Type': 'application/json' });
639
+ res.end(JSON.stringify({
640
+ success: true,
641
+ draftId,
642
+ avatarUrl: `/api/taskforce/context/${encodeURIComponent(relativePath)}`,
643
+ profile: finalized
644
+ }));
645
+ });
646
+ addRoute('POST', '/api/taskforce/auth/profile/avatar/discard', async (req, res) => {
647
+ if (!requireCloudRuntime(res, 'Authentication'))
648
+ return;
649
+ const authConfig = context.authConfig;
650
+ if (!authConfig) {
651
+ res.writeHead(500, { 'Content-Type': 'application/json' });
652
+ res.end(JSON.stringify({ success: false, error: 'Auth context unavailable.' }));
653
+ return;
654
+ }
655
+ const access = resolveRequestAccess(req, authConfig);
656
+ const userId = String(access.userId || '').trim();
657
+ if (!access.authenticated || !userId || userId === 'anonymous') {
658
+ res.writeHead(401, { 'Content-Type': 'application/json' });
659
+ res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
660
+ return;
661
+ }
662
+ const body = await parseJsonBody(req);
663
+ const draftId = String(body?.draftId || '').trim();
664
+ if (!draftId) {
665
+ res.writeHead(400, { 'Content-Type': 'application/json' });
666
+ res.end(JSON.stringify({ success: false, error: 'draftId is required.' }));
667
+ return;
668
+ }
669
+ await cleanupStaleAvatarDrafts(userId);
670
+ const draft = core.getUserAvatarUploadDraft(userId, draftId);
671
+ if (!draft) {
672
+ res.writeHead(200, { 'Content-Type': 'application/json' });
673
+ res.end(JSON.stringify({ success: true, discarded: false }));
674
+ return;
675
+ }
676
+ core.discardUserAvatarUploadDraft(userId, draftId);
677
+ await deleteStoredAvatarObject(draft.storageKey);
678
+ res.writeHead(200, { 'Content-Type': 'application/json' });
679
+ res.end(JSON.stringify({ success: true, discarded: true }));
680
+ });
271
681
  // === SECTION: Login and register ===
272
682
  // POST /api/taskforce/auth/login - Exchange email/password for secure session cookie
273
683
  addRoute('POST', '/api/taskforce/auth/login', async (req, res) => {
@@ -366,7 +776,7 @@ export function registerAuthRoutes(deps) {
366
776
  res.writeHead(400, { 'Content-Type': 'application/json' });
367
777
  res.end(JSON.stringify({ success: false, error: 'Email and password are required.' }));
368
778
  });
369
- // POST /api/taskforce/auth/register - Create account and exchange for secure session cookie
779
+ // POST /api/taskforce/auth/register - Create account and optionally start a session
370
780
  addRoute('POST', '/api/taskforce/auth/register', async (req, res) => {
371
781
  if (!requireCloudRuntime(res, 'Authentication'))
372
782
  return;
@@ -416,13 +826,6 @@ export function registerAuthRoutes(deps) {
416
826
  role,
417
827
  betaAccess: authBehavior.defaultNewUserBetaAccess
418
828
  });
419
- const sessionWorkspaceId = created.workspaceId || '';
420
- const cookie = createSessionTokenCookie(`pwd:${created.userId}`, authConfig, {
421
- role: created.role,
422
- workspaceId: sessionWorkspaceId,
423
- userId: created.userId,
424
- ...resolveSessionCookieAttributesForRequest(req, authConfig)
425
- });
426
829
  let emailSent = true;
427
830
  let emailError;
428
831
  let verificationToken;
@@ -442,7 +845,13 @@ export function registerAuthRoutes(deps) {
442
845
  role: created.role,
443
846
  emailSent
444
847
  });
445
- res.setHeader('Set-Cookie', cookie);
848
+ if (!authBehavior.requireVerifiedEmail) {
849
+ res.setHeader('Set-Cookie', buildPasswordSessionCookieForUser(req, authConfig, created.userId, {
850
+ preferredWorkspaceId: created.workspaceId || null,
851
+ persistedWorkspaceId: created.workspaceId || null,
852
+ fallbackRole: created.role
853
+ }));
854
+ }
446
855
  res.writeHead(201, { 'Content-Type': 'application/json' });
447
856
  res.end(JSON.stringify({
448
857
  success: true,
@@ -450,6 +859,8 @@ export function registerAuthRoutes(deps) {
450
859
  role: created.role,
451
860
  workspaceSetupRequired: !created.workspaceId,
452
861
  planSelectionRequired: created.planSelectionRequired === true,
862
+ checkoutPending: created.checkoutPending === true,
863
+ commercialState: created.commercialState,
453
864
  verificationRequired: authBehavior.requireVerifiedEmail,
454
865
  emailSent,
455
866
  emailError,
@@ -517,6 +928,12 @@ export function registerAuthRoutes(deps) {
517
928
  addRoute('POST', '/api/taskforce/auth/verify-email/confirm', async (req, res) => {
518
929
  if (!requireCloudRuntime(res, 'Authentication'))
519
930
  return;
931
+ const authConfig = context.authConfig;
932
+ if (!authConfig?.enabled) {
933
+ res.writeHead(400, { 'Content-Type': 'application/json' });
934
+ res.end(JSON.stringify({ success: false, error: 'Authentication is not configured.' }));
935
+ return;
936
+ }
520
937
  const body = await parseJsonBody(req);
521
938
  const token = String(body?.token || '').trim();
522
939
  if (!token) {
@@ -524,6 +941,15 @@ export function registerAuthRoutes(deps) {
524
941
  res.end(JSON.stringify({ success: false, error: 'Token is required.' }));
525
942
  return;
526
943
  }
944
+ const inspection = core.inspectOneTimeAuthToken(token, 'email_verification');
945
+ if (!inspection.userId) {
946
+ auditAuthAction(req, 'auth-verify-confirm-failed', {
947
+ tokenProvided: true
948
+ });
949
+ res.writeHead(400, { 'Content-Type': 'application/json' });
950
+ res.end(JSON.stringify({ success: false, error: 'Invalid or expired token.' }));
951
+ return;
952
+ }
527
953
  const verified = core.verifyEmailWithToken(token);
528
954
  if (!verified) {
529
955
  auditAuthAction(req, 'auth-verify-confirm-failed', {
@@ -536,6 +962,11 @@ export function registerAuthRoutes(deps) {
536
962
  auditAuthAction(req, 'auth-verify-confirm-success', {
537
963
  tokenProvided: true
538
964
  });
965
+ const cookie = buildPasswordSessionCookieForUser(req, authConfig, inspection.userId, {
966
+ preferredWorkspaceId: inspection.workspaceId || null,
967
+ persistedWorkspaceId: inspection.workspaceId || null
968
+ });
969
+ res.setHeader('Set-Cookie', cookie);
539
970
  res.writeHead(200, { 'Content-Type': 'application/json' });
540
971
  res.end(JSON.stringify({ success: true }));
541
972
  });
@@ -821,6 +1252,24 @@ export function registerAuthRoutes(deps) {
821
1252
  res.end(JSON.stringify({ error: String(error?.message || 'Unable to resolve account access status'), code: error?.code, details: error?.details }));
822
1253
  }
823
1254
  });
1255
+ // GET /api/taskforce/account/profile-summary - Fast authoritative summary for the visible account surface.
1256
+ addRoute('GET', '/api/taskforce/account/profile-summary', async (req, res) => {
1257
+ if (!ensureAuthenticatedUser(req)) {
1258
+ res.writeHead(401, { 'Content-Type': 'application/json' });
1259
+ res.end(JSON.stringify({ error: 'Authentication required.' }));
1260
+ return;
1261
+ }
1262
+ try {
1263
+ const payload = buildAccountProfileSummary(req);
1264
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1265
+ res.end(JSON.stringify(payload));
1266
+ }
1267
+ catch (error) {
1268
+ const statusCode = resolveErrorStatusCode(error, 400);
1269
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
1270
+ res.end(JSON.stringify({ error: String(error?.message || 'Unable to resolve account profile summary'), code: error?.code, details: error?.details }));
1271
+ }
1272
+ });
824
1273
  // GET /api/taskforce/account/entitlements - Resolve effective plan/features for the active workspace.
825
1274
  addRoute('GET', '/api/taskforce/account/entitlements', async (req, res) => {
826
1275
  if (!ensureAuthenticatedUser(req)) {
@@ -838,11 +1287,11 @@ export function registerAuthRoutes(deps) {
838
1287
  catch (error) {
839
1288
  if (error?.code === 'ENTITLEMENT_NOT_FOUND') {
840
1289
  const access = core.getAccountAccessStatus(requestUserId(req));
841
- if (access.gate === 'plan_selection_required') {
1290
+ if (access.gate === 'plan_selection_required' || access.gate === 'checkout_pending') {
842
1291
  res.writeHead(403, { 'Content-Type': 'application/json' });
843
1292
  res.end(JSON.stringify({
844
1293
  error: access.message,
845
- code: 'PLAN_SELECTION_REQUIRED',
1294
+ code: access.gate === 'checkout_pending' ? 'CHECKOUT_PENDING' : 'PLAN_SELECTION_REQUIRED',
846
1295
  details: {
847
1296
  gate: access.gate,
848
1297
  planSelectionRequired: access.planSelectionRequired,
@@ -857,6 +1306,32 @@ export function registerAuthRoutes(deps) {
857
1306
  res.end(JSON.stringify({ error: String(error?.message || 'Unable to resolve entitlements'), code: error?.code, details: error?.details }));
858
1307
  }
859
1308
  });
1309
+ // GET /api/taskforce/account/team-management-access - Resolve authoritative team-management access for the active workspace.
1310
+ addRoute('GET', '/api/taskforce/account/team-management-access', async (req, res) => {
1311
+ if (!ensureAuthenticatedUser(req)) {
1312
+ res.writeHead(401, { 'Content-Type': 'application/json' });
1313
+ res.end(JSON.stringify({ error: 'Authentication required.' }));
1314
+ return;
1315
+ }
1316
+ try {
1317
+ const payload = buildAccountProfileSummary(req);
1318
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1319
+ res.end(JSON.stringify({
1320
+ workspaceId: payload.workspaceId,
1321
+ role: payload.workspaceRole,
1322
+ canManageWorkspace: payload.canManageWorkspace,
1323
+ teamPlanMode: payload.teamPlanMode,
1324
+ allowed: payload.teamManagementAllowed,
1325
+ gate: payload.gate,
1326
+ planSelectionRequired: payload.planSelectionRequired
1327
+ }));
1328
+ }
1329
+ catch (error) {
1330
+ const statusCode = resolveErrorStatusCode(error, 400);
1331
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
1332
+ res.end(JSON.stringify({ error: String(error?.message || 'Unable to resolve team management access'), code: error?.code, details: error?.details }));
1333
+ }
1334
+ });
860
1335
  // GET /api/taskforce/account/plan-preview - Read-only payload for upgrade/support CTAs.
861
1336
  addRoute('GET', '/api/taskforce/account/plan-preview', async (req, res) => {
862
1337
  if (!ensureCloudPlanData(req, res))