@taskforcehq/taskforce 0.3.302 → 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 (77) hide show
  1. package/dist/Taskforce.module.css +11 -0
  2. package/dist/TaskforceCore.js +59 -9
  3. package/dist/TaskforceCore.test.js +717 -14
  4. package/dist/components/task/TaskKanban.test.js +17 -0
  5. package/dist/components/views/PlansPage.d.ts +3 -1
  6. package/dist/components/views/PlansPage.js +110 -51
  7. package/dist/components/views/PlansPage.test.d.ts +1 -0
  8. package/dist/components/views/PlansPage.test.js +100 -0
  9. package/dist/components/views/StandaloneLayout.d.ts +1 -0
  10. package/dist/components/views/StandaloneLayout.js +106 -151
  11. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +8 -3
  12. package/dist/components/views/standalone/modals/AccountHubModal.js +16 -2
  13. package/dist/components/views/standalone/modals/AccountHubModal.test.d.ts +1 -0
  14. package/dist/components/views/standalone/modals/AccountHubModal.test.js +26 -0
  15. package/dist/core/EntitlementsPolicy.test.js +2 -2
  16. package/dist/core/GlobalSettingsService.js +78 -18
  17. package/dist/core/PlanEntitlementService.d.ts +29 -5
  18. package/dist/core/PlanEntitlementService.js +297 -219
  19. package/dist/core/SignupMonetizationSettings.test.js +173 -49
  20. package/dist/core/SystemAdmin.test.js +186 -96
  21. package/dist/core/Taskforce.d.ts +51 -10
  22. package/dist/core/Taskforce.js +218 -49
  23. package/dist/core/Taskforce.mcpAuth.test.js +0 -2
  24. package/dist/core/UserProfileAvatarDrafts.test.js +12 -3
  25. package/dist/core/types.d.ts +11 -12
  26. package/dist/hooks/useSyncOrchestrator.js +17 -3
  27. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +97 -5
  28. package/dist/hooks/useTaskforce.d.ts +3 -0
  29. package/dist/hooks/useTaskforce.js +9 -4
  30. package/dist/mcp/runtime.js +90 -29
  31. package/dist/mcp/runtime.test.js +272 -81
  32. package/dist/migrations/billingSchemaParity.test.js +3 -0
  33. package/dist/migrations/taskSchemaMigrations.js +15 -0
  34. package/dist/server/index.cookieProxy.test.js +1 -0
  35. package/dist/server/index.js +2 -0
  36. package/dist/server/index.test.js +2 -1
  37. package/dist/server/mockStripe.d.ts +1 -0
  38. package/dist/server/mockStripe.js +2 -0
  39. package/dist/server/routes/admin.js +80 -23
  40. package/dist/server/routes/auth.d.ts +1 -0
  41. package/dist/server/routes/auth.js +194 -53
  42. package/dist/server/routes/billing.js +506 -23
  43. package/dist/server/routes/billing.test.js +937 -45
  44. package/dist/server/routes/shared.js +5 -3
  45. package/dist/server/routes/sync.integration.test.js +160 -0
  46. package/dist/server/routes/sync.js +15 -5
  47. package/dist/server/routes.js +3 -3
  48. package/dist/server/routes.test.js +287 -76
  49. package/dist/sync/cloudSyncApi.d.ts +1 -0
  50. package/dist/sync/contentSyncState.d.ts +10 -0
  51. package/dist/sync/contentSyncState.js +277 -16
  52. package/dist/sync/syncService.d.ts +1 -0
  53. package/dist/sync/syncService.js +21 -8
  54. package/dist/sync/workspacePullFeed.d.ts +10 -0
  55. package/dist/sync/workspacePullFeed.js +111 -63
  56. package/dist/sync/workspacePullFeed.test.js +112 -0
  57. package/dist/ui/assets/{AgentsModule-JG5jc3he.js → AgentsModule-CpsTmrIW.js} +1 -1
  58. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-Dvi02Y7q.js → AnnotatedAttachmentWorkspace-C_TmXZwA.js} +1 -1
  59. package/dist/ui/assets/{DocumentWorkspace-iys3zfca.js → DocumentWorkspace-C3GyzrWm.js} +1 -1
  60. package/dist/ui/assets/{InitiativesModule-DJvLJI3A.js → InitiativesModule-4-Rh2K2n.js} +1 -1
  61. package/dist/ui/assets/PlansPage-BP7AYOqr.js +1 -0
  62. package/dist/ui/assets/{TaskSettings-Bqd4cCzW.js → TaskSettings-BOC5F6Ag.js} +1 -1
  63. package/dist/ui/assets/{WorkflowsModule-BguHeQaV.js → WorkflowsModule-CGk9s3NJ.js} +1 -1
  64. package/dist/ui/assets/index-CLIMgR6g.js +6 -0
  65. package/dist/ui/assets/index-DneETxcu.css +1 -0
  66. package/dist/ui/assets/{vendor-icons-C8EED3Oh.js → vendor-icons-pWocEipT.js} +1 -1
  67. package/dist/ui/index.html +3 -3
  68. package/dist/utils/accountProfileSummaryCache.d.ts +31 -0
  69. package/dist/utils/accountProfileSummaryCache.js +97 -0
  70. package/dist/utils/accountProfileSummaryCache.test.d.ts +1 -0
  71. package/dist/utils/accountProfileSummaryCache.test.js +63 -0
  72. package/package.json +3 -3
  73. package/dist/ui/assets/PlansPage-BMPlpFpw.js +0 -1
  74. package/dist/ui/assets/index-MXeWoebC.css +0 -1
  75. package/dist/ui/assets/index-YJ4k5QNa.js +0 -6
  76. package/dist/utils/billingStatusCache.d.ts +0 -21
  77. package/dist/utils/billingStatusCache.js +0 -70
@@ -11,6 +11,7 @@ type MockStripeCheckoutSessionRecord = {
11
11
  successUrl: string;
12
12
  cancelUrl: string;
13
13
  subscriptionId: string | null;
14
+ expiresAt: number;
14
15
  paymentStatus: 'unpaid' | 'paid';
15
16
  status: 'open' | 'complete' | 'expired';
16
17
  };
@@ -122,6 +122,7 @@ function formatMockCheckoutSession(record, expandSubscription = false) {
122
122
  metadata: { ...record.metadata },
123
123
  payment_status: record.paymentStatus,
124
124
  status: record.status,
125
+ expires_at: record.expiresAt,
125
126
  subscription: record.subscriptionId
126
127
  ? (expandSubscription ? formatMockSubscription(getMockStripeSubscription(record.subscriptionId)) : record.subscriptionId)
127
128
  : null
@@ -243,6 +244,7 @@ export function createMockStripeClient() {
243
244
  successUrl: String(params.success_url || '').trim(),
244
245
  cancelUrl: String(params.cancel_url || '').trim(),
245
246
  subscriptionId: null,
247
+ expiresAt: Math.floor(Date.now() / 1000) + (24 * 60 * 60),
246
248
  paymentStatus: 'unpaid',
247
249
  status: 'open'
248
250
  };
@@ -101,7 +101,7 @@ export function registerAdminRoutes(deps) {
101
101
  const authSettings = globalSettings.auth || {};
102
102
  const authRateLimitMaxRequestsRaw = Number(authSettings.authRateLimitMaxRequests);
103
103
  const authRateLimitWindowMsRaw = Number(authSettings.authRateLimitWindowMs);
104
- const signupSettings = core.getSignupMonetizationSettings();
104
+ const onboardingPolicy = core.getOnboardingPolicy();
105
105
  return {
106
106
  allowSelfRegistration: resolveBooleanSetting(authSettings.allowSelfRegistration, true),
107
107
  requireVerifiedEmail: resolveBooleanSetting(authSettings.requireVerifiedEmail, true),
@@ -113,8 +113,8 @@ export function registerAdminRoutes(deps) {
113
113
  authRateLimitWindowMs: Number.isFinite(authRateLimitWindowMsRaw) && authRateLimitWindowMsRaw >= 1000
114
114
  ? Math.floor(authRateLimitWindowMsRaw)
115
115
  : 15 * 60_000,
116
- signupMonetizationStrategy: signupSettings.strategy,
117
- defaultSignupPlanVersionId: signupSettings.defaultSignupPlanVersionId
116
+ onboardingPolicyMode: onboardingPolicy.mode,
117
+ onboardingPlanVersionId: onboardingPolicy.planVersionId
118
118
  };
119
119
  };
120
120
  const getSiteBehavior = () => {
@@ -163,6 +163,10 @@ export function registerAdminRoutes(deps) {
163
163
  }
164
164
  return String(configured || resolvePublicAuthBaseUrl(req)).replace(/\/+$/, '');
165
165
  };
166
+ const resolveAdminCloudEnvironment = (req) => {
167
+ return resolveTaskforceCloudEnvironmentFromBaseUrl(resolveRuntimeConfigBaseUrl(req))
168
+ || resolveTaskforceCloudEnvironmentFromBaseUrl(resolveCloudLoginBaseUrl(req));
169
+ };
166
170
  const resolveMcpOauthAuthorizeUrl = (req) => `${resolveMcpOauthIssuer(req)}/oauth/mcp/authorize`;
167
171
  const resolveMcpOauthTokenUrl = (req) => `${resolveMcpOauthIssuer(req)}/oauth/mcp/token`;
168
172
  const resolveMcpOauthRegisterUrl = (req) => `${resolveMcpOauthIssuer(req)}/oauth/mcp/register`;
@@ -964,6 +968,51 @@ export function registerAdminRoutes(deps) {
964
968
  res.writeHead(200, { 'Content-Type': 'application/json' });
965
969
  res.end(JSON.stringify({ success: true }));
966
970
  });
971
+ // DELETE /api/taskforce/admin/system-users/:id - Purge a test user in non-production cloud environments
972
+ addRoute('DELETE', '/api/taskforce/admin/system-users/:id', async (req, res, params) => {
973
+ if (!requireCloudRuntime(res, 'System user purge'))
974
+ return;
975
+ if (!ensureAdminRole(req)) {
976
+ res.writeHead(403, { 'Content-Type': 'application/json' });
977
+ res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
978
+ return;
979
+ }
980
+ const environment = resolveAdminCloudEnvironment(req);
981
+ if (environment !== 'staging' && environment !== 'performance') {
982
+ res.writeHead(403, { 'Content-Type': 'application/json' });
983
+ res.end(JSON.stringify({
984
+ error: 'System user purge is only available in staging and performance.',
985
+ code: 'SYSTEM_USER_PURGE_ENVIRONMENT_FORBIDDEN'
986
+ }));
987
+ return;
988
+ }
989
+ const body = await parseJsonBody(req);
990
+ const actor = resolveAdminRequestActor(req);
991
+ try {
992
+ const summary = core.purgeSystemUser({
993
+ targetUserId: params.id,
994
+ actorUserId: actor.userId,
995
+ emailConfirmation: String(body?.emailConfirmation || '')
996
+ });
997
+ auditAdminAction(req, 'purge-system-user', {
998
+ targetUserId: params.id,
999
+ targetEmail: summary.email,
1000
+ environment,
1001
+ summary
1002
+ });
1003
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1004
+ res.end(JSON.stringify({ success: true, environment, summary }));
1005
+ }
1006
+ catch (error) {
1007
+ const statusCode = resolveErrorStatusCode(error, 400);
1008
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
1009
+ res.end(JSON.stringify({
1010
+ error: String(error?.message || 'Unable to purge system user'),
1011
+ code: error?.code,
1012
+ details: error?.details || null
1013
+ }));
1014
+ }
1015
+ });
967
1016
  // === SECTION: Admin email settings ===
968
1017
  // GET /api/taskforce/admin/email-settings - Read email provider settings
969
1018
  addRoute('GET', '/api/taskforce/admin/email-settings', async (req, res) => {
@@ -1036,7 +1085,9 @@ export function registerAdminRoutes(deps) {
1036
1085
  res.end(JSON.stringify({ error: 'Forbidden: admin role required.' }));
1037
1086
  return;
1038
1087
  }
1039
- const authSettings = core.getGlobalSettings().auth || {};
1088
+ const globalSettings = core.getGlobalSettings();
1089
+ const authSettings = globalSettings.auth || {};
1090
+ const billingSettings = globalSettings.billing || {};
1040
1091
  const effective = getAuthBehavior();
1041
1092
  res.writeHead(200, { 'Content-Type': 'application/json' });
1042
1093
  res.end(JSON.stringify({
@@ -1047,11 +1098,13 @@ export function registerAdminRoutes(deps) {
1047
1098
  ...(typeof authSettings.authRateLimitEnabled === 'boolean' ? { authRateLimitEnabled: authSettings.authRateLimitEnabled } : {}),
1048
1099
  ...(typeof authSettings.authRateLimitMaxRequests === 'number' ? { authRateLimitMaxRequests: authSettings.authRateLimitMaxRequests } : {}),
1049
1100
  ...(typeof authSettings.authRateLimitWindowMs === 'number' ? { authRateLimitWindowMs: authSettings.authRateLimitWindowMs } : {}),
1050
- ...((authSettings.signupMonetizationStrategy === 'auto_assign_default' || authSettings.signupMonetizationStrategy === 'plan_selection_required')
1051
- ? { signupMonetizationStrategy: authSettings.signupMonetizationStrategy }
1101
+ ...((billingSettings.onboardingPolicy?.mode === 'require_plan_selection'
1102
+ || billingSettings.onboardingPolicy?.mode === 'auto_grant_free'
1103
+ || billingSettings.onboardingPolicy?.mode === 'auto_start_trial')
1104
+ ? { onboardingPolicyMode: billingSettings.onboardingPolicy.mode }
1052
1105
  : {}),
1053
- ...(typeof authSettings.defaultSignupPlanVersionId === 'string' || authSettings.defaultSignupPlanVersionId === null
1054
- ? { defaultSignupPlanVersionId: authSettings.defaultSignupPlanVersionId ?? null }
1106
+ ...(typeof billingSettings.onboardingPolicy?.planVersionId === 'string' || billingSettings.onboardingPolicy?.planVersionId === null
1107
+ ? { onboardingPlanVersionId: billingSettings.onboardingPolicy?.planVersionId ?? null }
1055
1108
  : {})
1056
1109
  },
1057
1110
  status: {
@@ -1061,8 +1114,8 @@ export function registerAdminRoutes(deps) {
1061
1114
  authRateLimitEnabled: effective.authRateLimitEnabled,
1062
1115
  authRateLimitMaxRequests: effective.authRateLimitMaxRequests,
1063
1116
  authRateLimitWindowMs: effective.authRateLimitWindowMs,
1064
- signupMonetizationStrategy: effective.signupMonetizationStrategy,
1065
- defaultSignupPlanVersionId: effective.defaultSignupPlanVersionId
1117
+ onboardingPolicyMode: effective.onboardingPolicyMode,
1118
+ onboardingPlanVersionId: effective.onboardingPlanVersionId
1066
1119
  }
1067
1120
  }));
1068
1121
  });
@@ -1688,21 +1741,21 @@ export function registerAdminRoutes(deps) {
1688
1741
  return;
1689
1742
  }
1690
1743
  const body = await parseJsonBody(req);
1691
- const signupMonetizationStrategy = body?.signupMonetizationStrategy === 'plan_selection_required'
1692
- ? 'plan_selection_required'
1693
- : 'auto_assign_default';
1694
- const defaultSignupPlanVersionId = typeof body?.defaultSignupPlanVersionId === 'string'
1695
- ? body.defaultSignupPlanVersionId.trim() || null
1696
- : body?.defaultSignupPlanVersionId === null
1744
+ const onboardingPolicyMode = body?.onboardingPolicyMode === 'auto_grant_free' || body?.onboardingPolicyMode === 'auto_start_trial'
1745
+ ? body.onboardingPolicyMode
1746
+ : 'require_plan_selection';
1747
+ const onboardingPlanVersionId = typeof body?.onboardingPlanVersionId === 'string'
1748
+ ? body.onboardingPlanVersionId.trim() || null
1749
+ : body?.onboardingPlanVersionId === null
1697
1750
  ? null
1698
1751
  : undefined;
1699
- const validation = core.validateSignupMonetizationSettings({
1700
- strategy: signupMonetizationStrategy,
1701
- ...(defaultSignupPlanVersionId !== undefined ? { defaultSignupPlanVersionId } : {})
1752
+ const validation = core.validateOnboardingPolicy({
1753
+ mode: onboardingPolicyMode,
1754
+ ...(onboardingPlanVersionId !== undefined ? { planVersionId: onboardingPlanVersionId } : {})
1702
1755
  });
1703
1756
  if (!validation.valid) {
1704
1757
  res.writeHead(400, { 'Content-Type': 'application/json' });
1705
- res.end(JSON.stringify({ error: validation.message || 'Invalid signup monetization settings.', code: validation.code, details: validation.details }));
1758
+ res.end(JSON.stringify({ error: validation.message || 'Invalid onboarding policy.', code: validation.code, details: validation.details }));
1706
1759
  return;
1707
1760
  }
1708
1761
  core.updateGlobalSettings({
@@ -1714,9 +1767,13 @@ export function registerAdminRoutes(deps) {
1714
1767
  ...(typeof body?.authRateLimitMaxRequests === 'number' && Number.isFinite(body.authRateLimitMaxRequests)
1715
1768
  ? { authRateLimitMaxRequests: Math.max(1, Math.floor(body.authRateLimitMaxRequests)) } : {}),
1716
1769
  ...(typeof body?.authRateLimitWindowMs === 'number' && Number.isFinite(body.authRateLimitWindowMs)
1717
- ? { authRateLimitWindowMs: Math.max(1000, Math.floor(body.authRateLimitWindowMs)) } : {}),
1718
- signupMonetizationStrategy,
1719
- ...(defaultSignupPlanVersionId !== undefined ? { defaultSignupPlanVersionId } : {})
1770
+ ? { authRateLimitWindowMs: Math.max(1000, Math.floor(body.authRateLimitWindowMs)) } : {})
1771
+ },
1772
+ billing: {
1773
+ onboardingPolicy: {
1774
+ mode: onboardingPolicyMode,
1775
+ planVersionId: onboardingPlanVersionId ?? null
1776
+ }
1720
1777
  }
1721
1778
  });
1722
1779
  res.writeHead(200, { 'Content-Type': 'application/json' });
@@ -20,6 +20,7 @@
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
24
25
  * GET /api/taskforce/account/team-management-access
25
26
  * GET /api/taskforce/account/plan-preview
@@ -20,6 +20,7 @@
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
24
25
  * GET /api/taskforce/account/team-management-access
25
26
  * GET /api/taskforce/account/plan-preview
@@ -73,6 +74,121 @@ export function registerAuthRoutes(deps) {
73
74
  const stateAllows = state === 'active' || state === 'trialing' || state === 'grace';
74
75
  return teamAllowed && stateAllows ? 'team' : 'personal';
75
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
+ };
76
192
  // === SECTION: Email delivery helpers ===
77
193
  const getEmailConfig = () => {
78
194
  if (context.authConfig?.runtimeMode !== 'cloud') {
@@ -86,7 +202,7 @@ export function registerAuthRoutes(deps) {
86
202
  const authSettings = globalSettings.auth || {};
87
203
  const authRateLimitMaxRequestsRaw = Number(authSettings.authRateLimitMaxRequests);
88
204
  const authRateLimitWindowMsRaw = Number(authSettings.authRateLimitWindowMs);
89
- const signupSettings = core.getSignupMonetizationSettings();
205
+ const onboardingPolicy = core.getOnboardingPolicy();
90
206
  return {
91
207
  allowSelfRegistration: resolveBooleanSetting(authSettings.allowSelfRegistration, true),
92
208
  requireVerifiedEmail: resolveBooleanSetting(authSettings.requireVerifiedEmail, true),
@@ -98,8 +214,8 @@ export function registerAuthRoutes(deps) {
98
214
  authRateLimitWindowMs: Number.isFinite(authRateLimitWindowMsRaw) && authRateLimitWindowMsRaw >= 1000
99
215
  ? Math.floor(authRateLimitWindowMsRaw)
100
216
  : 15 * 60_000,
101
- signupMonetizationStrategy: signupSettings.strategy,
102
- defaultSignupPlanVersionId: signupSettings.defaultSignupPlanVersionId
217
+ onboardingPolicyMode: onboardingPolicy.mode,
218
+ onboardingPlanVersionId: onboardingPolicy.planVersionId
103
219
  };
104
220
  };
105
221
  const getSiteBehavior = () => {
@@ -110,6 +226,23 @@ export function registerAuthRoutes(deps) {
110
226
  pricingPageEnabled: resolveBooleanSetting(siteSettings.pricingPageEnabled, true)
111
227
  };
112
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
+ };
113
246
  const AVATAR_DRAFT_RETENTION_MS = 24 * 60 * 60 * 1000;
114
247
  const MAX_AVATAR_UPLOAD_BYTES = 5 * 1024 * 1024;
115
248
  const AVATAR_MIME_EXTENSION = {
@@ -643,7 +776,7 @@ export function registerAuthRoutes(deps) {
643
776
  res.writeHead(400, { 'Content-Type': 'application/json' });
644
777
  res.end(JSON.stringify({ success: false, error: 'Email and password are required.' }));
645
778
  });
646
- // 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
647
780
  addRoute('POST', '/api/taskforce/auth/register', async (req, res) => {
648
781
  if (!requireCloudRuntime(res, 'Authentication'))
649
782
  return;
@@ -693,13 +826,6 @@ export function registerAuthRoutes(deps) {
693
826
  role,
694
827
  betaAccess: authBehavior.defaultNewUserBetaAccess
695
828
  });
696
- const sessionWorkspaceId = created.workspaceId || '';
697
- const cookie = createSessionTokenCookie(`pwd:${created.userId}`, authConfig, {
698
- role: created.role,
699
- workspaceId: sessionWorkspaceId,
700
- userId: created.userId,
701
- ...resolveSessionCookieAttributesForRequest(req, authConfig)
702
- });
703
829
  let emailSent = true;
704
830
  let emailError;
705
831
  let verificationToken;
@@ -719,7 +845,13 @@ export function registerAuthRoutes(deps) {
719
845
  role: created.role,
720
846
  emailSent
721
847
  });
722
- 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
+ }
723
855
  res.writeHead(201, { 'Content-Type': 'application/json' });
724
856
  res.end(JSON.stringify({
725
857
  success: true,
@@ -727,6 +859,8 @@ export function registerAuthRoutes(deps) {
727
859
  role: created.role,
728
860
  workspaceSetupRequired: !created.workspaceId,
729
861
  planSelectionRequired: created.planSelectionRequired === true,
862
+ checkoutPending: created.checkoutPending === true,
863
+ commercialState: created.commercialState,
730
864
  verificationRequired: authBehavior.requireVerifiedEmail,
731
865
  emailSent,
732
866
  emailError,
@@ -794,6 +928,12 @@ export function registerAuthRoutes(deps) {
794
928
  addRoute('POST', '/api/taskforce/auth/verify-email/confirm', async (req, res) => {
795
929
  if (!requireCloudRuntime(res, 'Authentication'))
796
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
+ }
797
937
  const body = await parseJsonBody(req);
798
938
  const token = String(body?.token || '').trim();
799
939
  if (!token) {
@@ -801,6 +941,15 @@ export function registerAuthRoutes(deps) {
801
941
  res.end(JSON.stringify({ success: false, error: 'Token is required.' }));
802
942
  return;
803
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
+ }
804
953
  const verified = core.verifyEmailWithToken(token);
805
954
  if (!verified) {
806
955
  auditAuthAction(req, 'auth-verify-confirm-failed', {
@@ -813,6 +962,11 @@ export function registerAuthRoutes(deps) {
813
962
  auditAuthAction(req, 'auth-verify-confirm-success', {
814
963
  tokenProvided: true
815
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);
816
970
  res.writeHead(200, { 'Content-Type': 'application/json' });
817
971
  res.end(JSON.stringify({ success: true }));
818
972
  });
@@ -1098,6 +1252,24 @@ export function registerAuthRoutes(deps) {
1098
1252
  res.end(JSON.stringify({ error: String(error?.message || 'Unable to resolve account access status'), code: error?.code, details: error?.details }));
1099
1253
  }
1100
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
+ });
1101
1273
  // GET /api/taskforce/account/entitlements - Resolve effective plan/features for the active workspace.
1102
1274
  addRoute('GET', '/api/taskforce/account/entitlements', async (req, res) => {
1103
1275
  if (!ensureAuthenticatedUser(req)) {
@@ -1115,11 +1287,11 @@ export function registerAuthRoutes(deps) {
1115
1287
  catch (error) {
1116
1288
  if (error?.code === 'ENTITLEMENT_NOT_FOUND') {
1117
1289
  const access = core.getAccountAccessStatus(requestUserId(req));
1118
- if (access.gate === 'plan_selection_required') {
1290
+ if (access.gate === 'plan_selection_required' || access.gate === 'checkout_pending') {
1119
1291
  res.writeHead(403, { 'Content-Type': 'application/json' });
1120
1292
  res.end(JSON.stringify({
1121
1293
  error: access.message,
1122
- code: 'PLAN_SELECTION_REQUIRED',
1294
+ code: access.gate === 'checkout_pending' ? 'CHECKOUT_PENDING' : 'PLAN_SELECTION_REQUIRED',
1123
1295
  details: {
1124
1296
  gate: access.gate,
1125
1297
  planSelectionRequired: access.planSelectionRequired,
@@ -1141,51 +1313,20 @@ export function registerAuthRoutes(deps) {
1141
1313
  res.end(JSON.stringify({ error: 'Authentication required.' }));
1142
1314
  return;
1143
1315
  }
1144
- const wid = requestWorkspaceId(req);
1145
- const userId = requestUserId(req);
1146
- const hintedRole = normalizeTeamWorkspaceRole(requestHeader(req, 'x-taskforce-workspace-role', ''));
1147
- const resolvedAccess = core.resolveUserAccess(userId, wid, 'member');
1148
- const role = normalizeTeamWorkspaceRole(resolvedAccess?.role || hintedRole || null);
1149
- const canManageWorkspace = role === 'owner' || role === 'admin';
1150
- if (!canManageWorkspace) {
1151
- res.writeHead(200, { 'Content-Type': 'application/json' });
1152
- res.end(JSON.stringify({
1153
- workspaceId: wid,
1154
- role,
1155
- canManageWorkspace: false,
1156
- teamPlanMode: 'unknown',
1157
- allowed: false
1158
- }));
1159
- return;
1160
- }
1161
1316
  try {
1162
- const entitlements = core.getAccountEntitlementsPayload({ userId, workspaceId: wid });
1163
- const teamPlanMode = resolveTeamPlanModeFromEntitlements(entitlements);
1317
+ const payload = buildAccountProfileSummary(req);
1164
1318
  res.writeHead(200, { 'Content-Type': 'application/json' });
1165
1319
  res.end(JSON.stringify({
1166
- workspaceId: wid,
1167
- role,
1168
- canManageWorkspace: true,
1169
- teamPlanMode,
1170
- allowed: teamPlanMode === 'team'
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
1171
1327
  }));
1172
1328
  }
1173
1329
  catch (error) {
1174
- if (error?.code === 'ENTITLEMENT_NOT_FOUND') {
1175
- const access = core.getAccountAccessStatus(userId);
1176
- const teamPlanMode = access.gate === 'plan_selection_required' ? 'personal' : 'unknown';
1177
- res.writeHead(200, { 'Content-Type': 'application/json' });
1178
- res.end(JSON.stringify({
1179
- workspaceId: wid,
1180
- role,
1181
- canManageWorkspace: true,
1182
- teamPlanMode,
1183
- allowed: false,
1184
- gate: access.gate,
1185
- planSelectionRequired: access.planSelectionRequired
1186
- }));
1187
- return;
1188
- }
1189
1330
  const statusCode = resolveErrorStatusCode(error, 400);
1190
1331
  res.writeHead(statusCode, { 'Content-Type': 'application/json' });
1191
1332
  res.end(JSON.stringify({ error: String(error?.message || 'Unable to resolve team management access'), code: error?.code, details: error?.details }));