@taskforcehq/taskforce 0.3.300 → 0.3.302
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -0
- package/dist/Taskforce.module.css +26 -2
- package/dist/TaskforceCore.filter-persistence.test.js +1 -1
- package/dist/TaskforceCore.js +13 -24
- package/dist/TaskforceCore.test.js +167 -152
- package/dist/components/features/TaskSettings.js +14 -82
- package/dist/components/features/TaskSettings.test.js +33 -70
- package/dist/components/features/settings/TaxonomyLibraryManager.js +8 -2
- package/dist/components/features/settings/TaxonomyLibraryManager.test.js +1 -1
- package/dist/components/task/TaskActionHeader.js +6 -3
- package/dist/components/task/TaskCard.js +6 -3
- package/dist/components/task/TaskForm.js +2 -2
- package/dist/components/task/TaskForm.test.js +41 -2
- package/dist/components/ui/ReferenceBadgeButton.js +1 -1
- package/dist/components/views/PlansPage.js +9 -8
- package/dist/components/views/StandaloneLayout.js +490 -207
- package/dist/components/views/panels/FilterToolbar.d.ts +2 -0
- package/dist/components/views/panels/FilterToolbarControls.d.ts +1 -0
- package/dist/components/views/panels/FilterToolbarControls.js +1 -1
- package/dist/components/views/panels/PlanningDrawer.js +19 -2
- package/dist/components/views/standalone/modals/AccountHubModal.d.ts +43 -0
- package/dist/components/views/standalone/modals/AccountHubModal.js +22 -0
- package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.d.ts +9 -0
- package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.js +8 -0
- package/dist/components/views/standalone/modals/EditProfileModal.d.ts +26 -0
- package/dist/components/views/standalone/modals/EditProfileModal.js +10 -0
- package/dist/components/views/standalone/modals/HelpModal.d.ts +9 -0
- package/dist/components/views/standalone/modals/HelpModal.js +8 -0
- package/dist/components/views/standalone/modals/ScheduleWarningModal.d.ts +13 -0
- package/dist/components/views/standalone/modals/ScheduleWarningModal.js +6 -0
- package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +61 -0
- package/dist/components/views/standalone/modals/SyncStatusModal.js +38 -0
- package/dist/components/views/standalone/modals/TeamManagementModal.d.ts +64 -0
- package/dist/components/views/standalone/modals/TeamManagementModal.js +10 -0
- package/dist/components/views/standalone/modals/types.d.ts +2 -0
- package/dist/components/views/standalone/modals/types.js +1 -0
- package/dist/components/views/teamManagementAccess.d.ts +1 -1
- package/dist/components/views/teamManagementAccess.js +2 -2
- package/dist/components/views/teamManagementAccess.test.js +3 -3
- package/dist/config/deployment.js +2 -2
- package/dist/config/deployment.test.js +5 -3
- package/dist/core/AiProfileService.js +7 -2
- package/dist/core/AiProfiles.test.js +19 -0
- package/dist/core/SyncReconciliationService.d.ts +2 -0
- package/dist/core/SyncReconciliationService.js +3 -0
- package/dist/core/TaskAuditTimeline.test.js +40 -0
- package/dist/core/Taskforce.d.ts +49 -0
- package/dist/core/Taskforce.ids.test.js +133 -0
- package/dist/core/Taskforce.js +241 -78
- package/dist/core/UserProfileAvatarDrafts.test.d.ts +1 -0
- package/dist/core/UserProfileAvatarDrafts.test.js +102 -0
- package/dist/core/taskReferencePolicy.d.ts +28 -0
- package/dist/core/taskReferencePolicy.js +189 -0
- package/dist/core/types.d.ts +2 -0
- package/dist/hooks/tasks/useTaskFormActions.js +3 -0
- package/dist/hooks/useTaskData.js +16 -11
- package/dist/hooks/useTaskData.test.js +127 -0
- package/dist/hooks/useTaskforce.d.ts +5 -0
- package/dist/hooks/useTaskforce.js +63 -15
- package/dist/hooks/useTaskforce.sync-behavior.test.js +185 -6
- package/dist/mcp/aiProfileBootstrap.d.ts +29 -0
- package/dist/mcp/aiProfileBootstrap.js +80 -0
- package/dist/mcp/aiProfileBootstrap.test.d.ts +1 -0
- package/dist/mcp/aiProfileBootstrap.test.js +68 -0
- package/dist/mcp/clientIntegrations.d.ts +2 -8
- package/dist/mcp/clientIntegrations.js +21 -40
- package/dist/mcp/clientIntegrations.test.js +28 -2
- package/dist/mcp/clientProfileDefaults.d.ts +12 -0
- package/dist/mcp/clientProfileDefaults.js +37 -0
- package/dist/mcp/runtime.d.ts +10 -7
- package/dist/mcp/runtime.js +45 -23
- package/dist/mcp/runtime.test.js +56 -3
- package/dist/migrations/taskSchemaMigrations.js +53 -1
- package/dist/qaOpen.test.d.ts +1 -0
- package/dist/qaOpen.test.js +56 -0
- package/dist/server/index.cookieProxy.test.js +1 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +15 -1
- package/dist/server/index.test.js +14 -1
- package/dist/server/routes/admin.js +25 -22
- package/dist/server/routes/auth.d.ts +5 -0
- package/dist/server/routes/auth.js +350 -16
- package/dist/server/routes/billing.js +11 -1
- package/dist/server/routes/billing.test.js +32 -2
- package/dist/server/routes/documents.js +6 -1
- package/dist/server/routes/shared.d.ts +1 -1
- package/dist/server/routes/shared.js +11 -3
- package/dist/server/routes/sync.integration.test.js +130 -0
- package/dist/server/routes/sync.js +226 -1924
- package/dist/server/routes/tasks.js +18 -4
- package/dist/server/routes.js +26 -19
- package/dist/server/routes.test.js +352 -6
- package/dist/shared/runtimeContract.d.ts +1 -1
- package/dist/sync/collaborationSyncPayload.d.ts +17 -0
- package/dist/sync/collaborationSyncPayload.js +131 -0
- package/dist/sync/collaborationSyncPayload.test.d.ts +1 -0
- package/dist/sync/collaborationSyncPayload.test.js +104 -0
- package/dist/sync/collaborationSyncState.d.ts +15 -0
- package/dist/sync/collaborationSyncState.js +60 -0
- package/dist/sync/contentSyncPayload.d.ts +45 -0
- package/dist/sync/contentSyncPayload.js +104 -0
- package/dist/sync/contentSyncPayload.test.d.ts +1 -0
- package/dist/sync/contentSyncPayload.test.js +104 -0
- package/dist/sync/contentSyncState.d.ts +88 -0
- package/dist/sync/contentSyncState.js +743 -0
- package/dist/sync/planningSyncPayload.d.ts +9 -0
- package/dist/sync/planningSyncPayload.js +82 -0
- package/dist/sync/planningSyncPayload.test.d.ts +1 -0
- package/dist/sync/planningSyncPayload.test.js +68 -0
- package/dist/sync/syncApplyHandlers.d.ts +83 -0
- package/dist/sync/syncApplyHandlers.js +445 -0
- package/dist/sync/taskSyncPayload.d.ts +33 -0
- package/dist/sync/taskSyncPayload.js +126 -0
- package/dist/sync/taskSyncPayload.test.d.ts +1 -0
- package/dist/sync/taskSyncPayload.test.js +54 -0
- package/dist/sync/workspacePullFeed.d.ts +28 -0
- package/dist/sync/workspacePullFeed.js +301 -0
- package/dist/sync/workspacePullFeed.test.d.ts +1 -0
- package/dist/sync/workspacePullFeed.test.js +166 -0
- package/dist/sync/workspaceSyncModel.d.ts +4 -1
- package/dist/sync/workspaceSyncModel.js +9 -2
- package/dist/sync/workspaceSyncModel.test.js +29 -0
- package/dist/test/setup.js +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/ui/assets/{AgentsModule-TVmCgizS.js → AgentsModule-JG5jc3he.js} +1 -1
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-CAyhnPeR.js → AnnotatedAttachmentWorkspace-Dvi02Y7q.js} +1 -1
- package/dist/ui/assets/{DocumentWorkspace-BOCw5e4D.js → DocumentWorkspace-iys3zfca.js} +2 -2
- package/dist/ui/assets/{InitiativesModule-CN0r02Oi.js → InitiativesModule-DJvLJI3A.js} +1 -1
- package/dist/ui/assets/PlansPage-BMPlpFpw.js +1 -0
- package/dist/ui/assets/TaskSettings-Bqd4cCzW.js +8 -0
- package/dist/ui/assets/{TaskSettings-BwB9NtmJ.css → TaskSettings-CnIBL_Eb.css} +1 -1
- package/dist/ui/assets/{WorkflowsModule-CY9R2zPB.js → WorkflowsModule-BguHeQaV.js} +1 -1
- package/dist/ui/assets/index-MXeWoebC.css +1 -0
- package/dist/ui/assets/index-YJ4k5QNa.js +6 -0
- package/dist/ui/assets/{vendor-icons-QZyhEwgT.js → vendor-icons-C8EED3Oh.js} +1 -1
- package/dist/ui/index.html +3 -3
- package/dist/utils/billingStatusCache.d.ts +21 -0
- package/dist/utils/billingStatusCache.js +70 -0
- package/dist/utils/taskActivity.js +17 -4
- package/dist/utils/taskActivity.test.js +27 -0
- package/dist/utils/taskNormalization.js +10 -2
- package/dist/utils/taskReferences.d.ts +11 -1
- package/dist/utils/taskReferences.js +33 -1
- package/package.json +4 -1
- package/scripts/deploy-prod.sh +41 -0
- package/scripts/deploy-staging.sh +23 -0
- package/scripts/qa-open-lib.mjs +175 -0
- package/scripts/qa-open-lib.mjs.d.ts +49 -0
- package/scripts/qa-open.mjs +352 -0
- package/dist/ui/assets/PlansPage-FcQSNLZd.js +0 -1
- package/dist/ui/assets/TaskSettings-Cw5zYD1L.js +0 -8
- package/dist/ui/assets/index-Bzqy5QGm.js +0 -6
- package/dist/ui/assets/index-DxRiI3tY.css +0 -1
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* POST /api/taskforce/auth/logout
|
|
22
22
|
* GET /api/taskforce/account/access-status
|
|
23
23
|
* GET /api/taskforce/account/entitlements
|
|
24
|
+
* GET /api/taskforce/account/team-management-access
|
|
24
25
|
* GET /api/taskforce/account/plan-preview
|
|
25
26
|
* GET /api/taskforce/auth/workspace-members
|
|
26
27
|
* GET /api/taskforce/workspace/assignee-options
|
|
@@ -33,15 +34,45 @@
|
|
|
33
34
|
* DELETE /api/taskforce/admin/users/:id
|
|
34
35
|
* GET /api/taskforce/admin/workspace-audit-logs
|
|
35
36
|
*/
|
|
37
|
+
import * as fs from 'fs';
|
|
38
|
+
import * as path from 'path';
|
|
39
|
+
import { randomUUID } from 'crypto';
|
|
36
40
|
import { parseJsonBody } from '../routes.js';
|
|
37
41
|
import { hasValidSession, createClearSessionCookie, createSessionTokenCookie, resolveRequestAccess, resolveSessionCookieAttributesForRequest, } from '../auth.js';
|
|
38
42
|
import { resolveEffectiveEmailConfig, sendEmail } from '../email.js';
|
|
39
43
|
import { resolveDeploymentConfig } from '../../config/deployment.js';
|
|
40
44
|
import { resolveRuntimeModeContract } from '../../shared/runtimeContract.js';
|
|
41
|
-
import { resolveErrorStatusCode } from './shared.js';
|
|
45
|
+
import { requestHeader, resolveErrorStatusCode } from './shared.js';
|
|
46
|
+
import { normalizeTaskDataRelativePath } from '../../storage/objectStorageClient.js';
|
|
42
47
|
// === SECTION: Registration ===
|
|
43
48
|
export function registerAuthRoutes(deps) {
|
|
44
|
-
const { addRoute, core, context, requestWorkspaceId, requestUserId, ensureAppAccess, ensureAuthenticatedUser, ensureWorkspaceAdminRole, ensureCloudPlanData, requireCloudRuntime, auditAuthAction, resolveAdminRequestActor, shouldExposeDevAuthTokens, resolveRuntimeConfigBaseUrl, resolvePublicAuthBaseUrl, } = deps;
|
|
49
|
+
const { addRoute, core, context, requestWorkspaceId, requestUserId, ensureAppAccess, ensureAuthenticatedUser, ensureWorkspaceAdminRole, ensureCloudPlanData, requireCloudRuntime, auditAuthAction, resolveAdminRequestActor, shouldExposeDevAuthTokens, resolveRuntimeConfigBaseUrl, resolvePublicAuthBaseUrl, resolveEffectiveObjectStorageConfig, getObjectStorageClient, } = deps;
|
|
50
|
+
const normalizeTeamWorkspaceRole = (value) => {
|
|
51
|
+
const role = String(value || '').trim().toLowerCase();
|
|
52
|
+
if (role === 'owner' || role === 'admin' || role === 'member' || role === 'read-only')
|
|
53
|
+
return role;
|
|
54
|
+
return null;
|
|
55
|
+
};
|
|
56
|
+
const resolveBooleanSetting = (value, fallback) => {
|
|
57
|
+
if (typeof value === 'boolean')
|
|
58
|
+
return value;
|
|
59
|
+
if (typeof value === 'number')
|
|
60
|
+
return value !== 0;
|
|
61
|
+
const normalized = String(value || '').trim().toLowerCase();
|
|
62
|
+
if (!normalized)
|
|
63
|
+
return fallback;
|
|
64
|
+
if (['1', 'true', 'yes', 'on'].includes(normalized))
|
|
65
|
+
return true;
|
|
66
|
+
if (['0', 'false', 'no', 'off'].includes(normalized))
|
|
67
|
+
return false;
|
|
68
|
+
return fallback;
|
|
69
|
+
};
|
|
70
|
+
const resolveTeamPlanModeFromEntitlements = (payload) => {
|
|
71
|
+
const state = String(payload?.state || '').trim().toLowerCase();
|
|
72
|
+
const teamAllowed = payload?.features?.['collaboration.team_management']?.allowed === true;
|
|
73
|
+
const stateAllows = state === 'active' || state === 'trialing' || state === 'grace';
|
|
74
|
+
return teamAllowed && stateAllows ? 'team' : 'personal';
|
|
75
|
+
};
|
|
45
76
|
// === SECTION: Email delivery helpers ===
|
|
46
77
|
const getEmailConfig = () => {
|
|
47
78
|
if (context.authConfig?.runtimeMode !== 'cloud') {
|
|
@@ -57,10 +88,10 @@ export function registerAuthRoutes(deps) {
|
|
|
57
88
|
const authRateLimitWindowMsRaw = Number(authSettings.authRateLimitWindowMs);
|
|
58
89
|
const signupSettings = core.getSignupMonetizationSettings();
|
|
59
90
|
return {
|
|
60
|
-
allowSelfRegistration: authSettings.allowSelfRegistration
|
|
61
|
-
requireVerifiedEmail: authSettings.requireVerifiedEmail
|
|
62
|
-
defaultNewUserBetaAccess: authSettings.defaultNewUserBetaAccess
|
|
63
|
-
authRateLimitEnabled: authSettings.authRateLimitEnabled
|
|
91
|
+
allowSelfRegistration: resolveBooleanSetting(authSettings.allowSelfRegistration, true),
|
|
92
|
+
requireVerifiedEmail: resolveBooleanSetting(authSettings.requireVerifiedEmail, true),
|
|
93
|
+
defaultNewUserBetaAccess: resolveBooleanSetting(authSettings.defaultNewUserBetaAccess, false),
|
|
94
|
+
authRateLimitEnabled: resolveBooleanSetting(authSettings.authRateLimitEnabled, true),
|
|
64
95
|
authRateLimitMaxRequests: Number.isFinite(authRateLimitMaxRequestsRaw) && authRateLimitMaxRequestsRaw >= 1
|
|
65
96
|
? Math.floor(authRateLimitMaxRequestsRaw)
|
|
66
97
|
: 10,
|
|
@@ -75,10 +106,57 @@ export function registerAuthRoutes(deps) {
|
|
|
75
106
|
const globalSettings = core.getGlobalSettings();
|
|
76
107
|
const siteSettings = globalSettings.site || {};
|
|
77
108
|
return {
|
|
78
|
-
showRunTaskforceLocally: siteSettings.showRunTaskforceLocally
|
|
79
|
-
pricingPageEnabled: siteSettings.pricingPageEnabled
|
|
109
|
+
showRunTaskforceLocally: resolveBooleanSetting(siteSettings.showRunTaskforceLocally, true),
|
|
110
|
+
pricingPageEnabled: resolveBooleanSetting(siteSettings.pricingPageEnabled, true)
|
|
80
111
|
};
|
|
81
112
|
};
|
|
113
|
+
const AVATAR_DRAFT_RETENTION_MS = 24 * 60 * 60 * 1000;
|
|
114
|
+
const MAX_AVATAR_UPLOAD_BYTES = 5 * 1024 * 1024;
|
|
115
|
+
const AVATAR_MIME_EXTENSION = {
|
|
116
|
+
'image/png': '.png',
|
|
117
|
+
'image/jpeg': '.jpg',
|
|
118
|
+
'image/webp': '.webp',
|
|
119
|
+
'image/gif': '.gif'
|
|
120
|
+
};
|
|
121
|
+
const buildUserAvatarStoragePath = (userIdRaw, draftIdRaw, originalNameRaw, mimeTypeRaw) => {
|
|
122
|
+
const userId = String(userIdRaw || '').trim().replace(/[^a-zA-Z0-9._-]/g, '-') || 'user';
|
|
123
|
+
const draftId = String(draftIdRaw || '').trim().replace(/[^a-zA-Z0-9._-]/g, '-') || randomUUID().replace(/-/g, '');
|
|
124
|
+
const originalName = String(originalNameRaw || '').trim();
|
|
125
|
+
const mimeType = String(mimeTypeRaw || '').trim().toLowerCase();
|
|
126
|
+
const originalExt = originalName ? path.extname(originalName).toLowerCase() : '';
|
|
127
|
+
const ext = originalExt || AVATAR_MIME_EXTENSION[mimeType] || '.bin';
|
|
128
|
+
const stem = (originalName ? path.basename(originalName, originalExt) : 'avatar')
|
|
129
|
+
.replace(/[^a-zA-Z0-9._-]/g, '-')
|
|
130
|
+
.replace(/-+/g, '-')
|
|
131
|
+
.replace(/^-+|-+$/g, '')
|
|
132
|
+
.slice(0, 64) || 'avatar';
|
|
133
|
+
return `users/${userId}/profile/avatar/${draftId}-${stem}${ext}`;
|
|
134
|
+
};
|
|
135
|
+
const deleteStoredAvatarObject = async (relativePathRaw) => {
|
|
136
|
+
const relativePath = normalizeTaskDataRelativePath(String(relativePathRaw || '').trim());
|
|
137
|
+
if (!relativePath)
|
|
138
|
+
return;
|
|
139
|
+
const objectStorageClient = getObjectStorageClient();
|
|
140
|
+
const effectiveObjectStorage = resolveEffectiveObjectStorageConfig();
|
|
141
|
+
if (objectStorageClient && effectiveObjectStorage.provider === 'r2') {
|
|
142
|
+
await objectStorageClient.deleteObject(relativePath).catch(() => { });
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const { basePath } = core.getPaths();
|
|
146
|
+
const resolvedPath = path.resolve(basePath, relativePath);
|
|
147
|
+
if (!resolvedPath.startsWith(path.resolve(basePath)))
|
|
148
|
+
return;
|
|
149
|
+
if (fs.existsSync(resolvedPath)) {
|
|
150
|
+
fs.rmSync(resolvedPath, { force: true });
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
const cleanupStaleAvatarDrafts = async (userId) => {
|
|
154
|
+
const cutoff = new Date(Date.now() - AVATAR_DRAFT_RETENTION_MS).toISOString();
|
|
155
|
+
const staleDrafts = core.cleanupStaleUserAvatarUploadDrafts(userId, cutoff);
|
|
156
|
+
for (const draft of staleDrafts) {
|
|
157
|
+
await deleteStoredAvatarObject(draft.storageKey);
|
|
158
|
+
}
|
|
159
|
+
};
|
|
82
160
|
const logAuthLink = (kind, email, token, req) => {
|
|
83
161
|
const base = resolvePublicAuthBaseUrl(req);
|
|
84
162
|
const urlPath = kind === 'verify-email'
|
|
@@ -187,8 +265,10 @@ export function registerAuthRoutes(deps) {
|
|
|
187
265
|
let userId = access?.userId || 'anonymous';
|
|
188
266
|
let email = null;
|
|
189
267
|
let displayName = null;
|
|
268
|
+
let avatarUrl = null;
|
|
190
269
|
let betaAccess = true;
|
|
191
270
|
if (authenticated && access?.userId) {
|
|
271
|
+
await cleanupStaleAvatarDrafts(access.userId);
|
|
192
272
|
betaAccess = core.getUserBetaAccess(access.userId);
|
|
193
273
|
const resolved = core.resolveUserAccess(access.userId, workspaceId, role);
|
|
194
274
|
if (resolved) {
|
|
@@ -204,6 +284,7 @@ export function registerAuthRoutes(deps) {
|
|
|
204
284
|
const identity = core.resolveUserIdentity(userId);
|
|
205
285
|
email = identity?.email || null;
|
|
206
286
|
displayName = identity?.displayName || null;
|
|
287
|
+
avatarUrl = identity?.avatarUrl || null;
|
|
207
288
|
}
|
|
208
289
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
209
290
|
res.end(JSON.stringify({
|
|
@@ -213,6 +294,7 @@ export function registerAuthRoutes(deps) {
|
|
|
213
294
|
role,
|
|
214
295
|
email,
|
|
215
296
|
displayName,
|
|
297
|
+
avatarUrl,
|
|
216
298
|
betaAccess
|
|
217
299
|
}));
|
|
218
300
|
});
|
|
@@ -229,28 +311,67 @@ export function registerAuthRoutes(deps) {
|
|
|
229
311
|
addRoute('POST', '/api/taskforce/auth/profile', async (req, res) => {
|
|
230
312
|
if (!requireCloudRuntime(res, 'Authentication'))
|
|
231
313
|
return;
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
res.
|
|
314
|
+
const authConfig = context.authConfig;
|
|
315
|
+
if (!authConfig) {
|
|
316
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
317
|
+
res.end(JSON.stringify({ success: false, error: 'Auth context unavailable.' }));
|
|
235
318
|
return;
|
|
236
319
|
}
|
|
237
|
-
const
|
|
238
|
-
|
|
320
|
+
const access = resolveRequestAccess(req, authConfig);
|
|
321
|
+
const userId = String(access.userId || '').trim();
|
|
322
|
+
if (!access.authenticated || !userId || userId === 'anonymous') {
|
|
239
323
|
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
240
324
|
res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
|
|
241
325
|
return;
|
|
242
326
|
}
|
|
243
327
|
const body = await parseJsonBody(req);
|
|
244
328
|
try {
|
|
245
|
-
|
|
329
|
+
await cleanupStaleAvatarDrafts(userId);
|
|
330
|
+
const avatarDraftId = typeof body?.avatarDraftId === 'string' ? body.avatarDraftId.trim() : '';
|
|
331
|
+
const clearAvatar = body?.clearAvatar === true;
|
|
332
|
+
let avatarUrl;
|
|
333
|
+
if (avatarDraftId) {
|
|
334
|
+
const draft = core.getUserAvatarUploadDraft(userId, avatarDraftId);
|
|
335
|
+
if (!draft || draft.discardedAt || !draft.finalizedAt) {
|
|
336
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
337
|
+
res.end(JSON.stringify({ success: false, error: 'Avatar draft is invalid or expired.', code: 'AVATAR_DRAFT_INVALID' }));
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
avatarUrl = `/api/taskforce/context/${encodeURIComponent(draft.storageKey)}`;
|
|
341
|
+
}
|
|
342
|
+
else if (clearAvatar) {
|
|
343
|
+
avatarUrl = null;
|
|
344
|
+
}
|
|
345
|
+
const previousIdentity = core.resolveUserIdentity(userId);
|
|
346
|
+
const updated = core.updateUserProfile(userId, {
|
|
347
|
+
displayName: String(body?.displayName || ''),
|
|
348
|
+
...((avatarDraftId || clearAvatar) ? { avatarUrl: avatarUrl || null } : {})
|
|
349
|
+
});
|
|
246
350
|
if (!updated) {
|
|
247
351
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
248
352
|
res.end(JSON.stringify({ success: false, error: 'User not found.', code: 'USER_NOT_FOUND' }));
|
|
249
353
|
return;
|
|
250
354
|
}
|
|
355
|
+
if (avatarDraftId) {
|
|
356
|
+
core.markUserAvatarUploadDraftConsumed(userId, avatarDraftId);
|
|
357
|
+
}
|
|
358
|
+
if (avatarDraftId || clearAvatar) {
|
|
359
|
+
const previousAvatarUrl = String(previousIdentity?.avatarUrl || '').trim();
|
|
360
|
+
const nextAvatarUrl = String(updated.avatarUrl || '').trim();
|
|
361
|
+
const currentDraft = avatarDraftId ? core.getUserAvatarUploadDraft(userId, avatarDraftId) : null;
|
|
362
|
+
const currentStorageKey = String(currentDraft?.storageKey || '').trim();
|
|
363
|
+
if (previousAvatarUrl.startsWith('/api/taskforce/context/')
|
|
364
|
+
&& previousAvatarUrl !== nextAvatarUrl) {
|
|
365
|
+
const previousStorageKey = normalizeTaskDataRelativePath(decodeURIComponent(previousAvatarUrl.slice('/api/taskforce/context/'.length)));
|
|
366
|
+
if (previousStorageKey && previousStorageKey !== currentStorageKey) {
|
|
367
|
+
await deleteStoredAvatarObject(previousStorageKey);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
251
371
|
auditAuthAction(req, 'auth-profile-updated', {
|
|
252
372
|
userId,
|
|
253
|
-
displayName: updated.displayName || null
|
|
373
|
+
displayName: updated.displayName || null,
|
|
374
|
+
avatarUrl: updated.avatarUrl || null
|
|
254
375
|
});
|
|
255
376
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
256
377
|
res.end(JSON.stringify({
|
|
@@ -258,7 +379,8 @@ export function registerAuthRoutes(deps) {
|
|
|
258
379
|
profile: {
|
|
259
380
|
userId: updated.userId,
|
|
260
381
|
email: updated.email,
|
|
261
|
-
displayName: updated.displayName || null
|
|
382
|
+
displayName: updated.displayName || null,
|
|
383
|
+
avatarUrl: updated.avatarUrl || null
|
|
262
384
|
}
|
|
263
385
|
}));
|
|
264
386
|
}
|
|
@@ -268,6 +390,161 @@ export function registerAuthRoutes(deps) {
|
|
|
268
390
|
res.end(JSON.stringify({ success: false, error: String(error?.message || 'Unable to update profile.'), code: error?.code }));
|
|
269
391
|
}
|
|
270
392
|
});
|
|
393
|
+
addRoute('POST', '/api/taskforce/auth/profile/avatar/init', async (req, res) => {
|
|
394
|
+
if (!requireCloudRuntime(res, 'Authentication'))
|
|
395
|
+
return;
|
|
396
|
+
const authConfig = context.authConfig;
|
|
397
|
+
if (!authConfig) {
|
|
398
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
399
|
+
res.end(JSON.stringify({ success: false, error: 'Auth context unavailable.' }));
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
const access = resolveRequestAccess(req, authConfig);
|
|
403
|
+
const userId = String(access.userId || '').trim();
|
|
404
|
+
if (!access.authenticated || !userId || userId === 'anonymous') {
|
|
405
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
406
|
+
res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
const objectStorageClient = getObjectStorageClient();
|
|
410
|
+
const effectiveObjectStorage = resolveEffectiveObjectStorageConfig();
|
|
411
|
+
if (!objectStorageClient || effectiveObjectStorage.provider !== 'r2' || !effectiveObjectStorage.r2) {
|
|
412
|
+
res.writeHead(501, { 'Content-Type': 'application/json' });
|
|
413
|
+
res.end(JSON.stringify({ success: false, error: 'Avatar uploads require object storage.' }));
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
const body = await parseJsonBody(req);
|
|
417
|
+
const originalName = String(body?.originalName || '').trim();
|
|
418
|
+
const mimeType = String(body?.mimeType || '').trim().toLowerCase();
|
|
419
|
+
const size = Number(body?.size || 0);
|
|
420
|
+
if (!originalName || !mimeType.startsWith('image/') || !(mimeType in AVATAR_MIME_EXTENSION)) {
|
|
421
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
422
|
+
res.end(JSON.stringify({ success: false, error: 'A supported image file is required.', code: 'AVATAR_FILE_INVALID' }));
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
if (!Number.isFinite(size) || size <= 0 || size > MAX_AVATAR_UPLOAD_BYTES) {
|
|
426
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
427
|
+
res.end(JSON.stringify({ success: false, error: 'Avatar must be 5 MB or smaller.', code: 'AVATAR_SIZE_INVALID' }));
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
await cleanupStaleAvatarDrafts(userId);
|
|
431
|
+
const draftId = `avatar-draft-${randomUUID().replace(/-/g, '')}`;
|
|
432
|
+
const relativePath = buildUserAvatarStoragePath(userId, draftId, originalName, mimeType);
|
|
433
|
+
core.createUserAvatarUploadDraft({
|
|
434
|
+
userId,
|
|
435
|
+
draftId,
|
|
436
|
+
storageKey: relativePath,
|
|
437
|
+
mimeType,
|
|
438
|
+
originalFilename: originalName,
|
|
439
|
+
sizeBytes: size,
|
|
440
|
+
storageProvider: 'r2'
|
|
441
|
+
});
|
|
442
|
+
const uploadUrl = objectStorageClient.createSignedPutUrl(relativePath, effectiveObjectStorage.r2.signedUploadTtlSeconds, mimeType);
|
|
443
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
444
|
+
res.end(JSON.stringify({
|
|
445
|
+
success: true,
|
|
446
|
+
mode: 'direct',
|
|
447
|
+
draftId,
|
|
448
|
+
uploadUrl,
|
|
449
|
+
method: 'PUT',
|
|
450
|
+
headers: { 'Content-Type': mimeType },
|
|
451
|
+
relativePath,
|
|
452
|
+
path: `/api/taskforce/context/${encodeURIComponent(relativePath)}`
|
|
453
|
+
}));
|
|
454
|
+
});
|
|
455
|
+
addRoute('POST', '/api/taskforce/auth/profile/avatar/finalize', async (req, res) => {
|
|
456
|
+
if (!requireCloudRuntime(res, 'Authentication'))
|
|
457
|
+
return;
|
|
458
|
+
const authConfig = context.authConfig;
|
|
459
|
+
if (!authConfig) {
|
|
460
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
461
|
+
res.end(JSON.stringify({ success: false, error: 'Auth context unavailable.' }));
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
const access = resolveRequestAccess(req, authConfig);
|
|
465
|
+
const userId = String(access.userId || '').trim();
|
|
466
|
+
if (!access.authenticated || !userId || userId === 'anonymous') {
|
|
467
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
468
|
+
res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
const body = await parseJsonBody(req);
|
|
472
|
+
const draftId = String(body?.draftId || '').trim();
|
|
473
|
+
const relativePath = normalizeTaskDataRelativePath(String(body?.relativePath || '').trim());
|
|
474
|
+
if (!draftId || !relativePath) {
|
|
475
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
476
|
+
res.end(JSON.stringify({ success: false, error: 'draftId and relativePath are required.' }));
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
await cleanupStaleAvatarDrafts(userId);
|
|
480
|
+
const draft = core.getUserAvatarUploadDraft(userId, draftId);
|
|
481
|
+
if (!draft || draft.discardedAt || draft.storageKey !== relativePath) {
|
|
482
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
483
|
+
res.end(JSON.stringify({ success: false, error: 'Avatar draft is invalid or expired.', code: 'AVATAR_DRAFT_INVALID' }));
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
const objectStorageClient = getObjectStorageClient();
|
|
487
|
+
if (objectStorageClient) {
|
|
488
|
+
const uploaded = await objectStorageClient.getObject(relativePath);
|
|
489
|
+
if (!uploaded) {
|
|
490
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
491
|
+
res.end(JSON.stringify({ success: false, error: 'Uploaded avatar not found.', code: 'AVATAR_UPLOAD_MISSING' }));
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
const { basePath } = core.getPaths();
|
|
497
|
+
const resolvedPath = path.resolve(basePath, relativePath);
|
|
498
|
+
if (!resolvedPath.startsWith(path.resolve(basePath)) || !fs.existsSync(resolvedPath)) {
|
|
499
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
500
|
+
res.end(JSON.stringify({ success: false, error: 'Uploaded avatar not found.', code: 'AVATAR_UPLOAD_MISSING' }));
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
const finalized = core.markUserAvatarUploadDraftFinalized(userId, draftId);
|
|
505
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
506
|
+
res.end(JSON.stringify({
|
|
507
|
+
success: true,
|
|
508
|
+
draftId,
|
|
509
|
+
avatarUrl: `/api/taskforce/context/${encodeURIComponent(relativePath)}`,
|
|
510
|
+
profile: finalized
|
|
511
|
+
}));
|
|
512
|
+
});
|
|
513
|
+
addRoute('POST', '/api/taskforce/auth/profile/avatar/discard', async (req, res) => {
|
|
514
|
+
if (!requireCloudRuntime(res, 'Authentication'))
|
|
515
|
+
return;
|
|
516
|
+
const authConfig = context.authConfig;
|
|
517
|
+
if (!authConfig) {
|
|
518
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
519
|
+
res.end(JSON.stringify({ success: false, error: 'Auth context unavailable.' }));
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
const access = resolveRequestAccess(req, authConfig);
|
|
523
|
+
const userId = String(access.userId || '').trim();
|
|
524
|
+
if (!access.authenticated || !userId || userId === 'anonymous') {
|
|
525
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
526
|
+
res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
const body = await parseJsonBody(req);
|
|
530
|
+
const draftId = String(body?.draftId || '').trim();
|
|
531
|
+
if (!draftId) {
|
|
532
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
533
|
+
res.end(JSON.stringify({ success: false, error: 'draftId is required.' }));
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
await cleanupStaleAvatarDrafts(userId);
|
|
537
|
+
const draft = core.getUserAvatarUploadDraft(userId, draftId);
|
|
538
|
+
if (!draft) {
|
|
539
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
540
|
+
res.end(JSON.stringify({ success: true, discarded: false }));
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
core.discardUserAvatarUploadDraft(userId, draftId);
|
|
544
|
+
await deleteStoredAvatarObject(draft.storageKey);
|
|
545
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
546
|
+
res.end(JSON.stringify({ success: true, discarded: true }));
|
|
547
|
+
});
|
|
271
548
|
// === SECTION: Login and register ===
|
|
272
549
|
// POST /api/taskforce/auth/login - Exchange email/password for secure session cookie
|
|
273
550
|
addRoute('POST', '/api/taskforce/auth/login', async (req, res) => {
|
|
@@ -857,6 +1134,63 @@ export function registerAuthRoutes(deps) {
|
|
|
857
1134
|
res.end(JSON.stringify({ error: String(error?.message || 'Unable to resolve entitlements'), code: error?.code, details: error?.details }));
|
|
858
1135
|
}
|
|
859
1136
|
});
|
|
1137
|
+
// GET /api/taskforce/account/team-management-access - Resolve authoritative team-management access for the active workspace.
|
|
1138
|
+
addRoute('GET', '/api/taskforce/account/team-management-access', async (req, res) => {
|
|
1139
|
+
if (!ensureAuthenticatedUser(req)) {
|
|
1140
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
1141
|
+
res.end(JSON.stringify({ error: 'Authentication required.' }));
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
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
|
+
try {
|
|
1162
|
+
const entitlements = core.getAccountEntitlementsPayload({ userId, workspaceId: wid });
|
|
1163
|
+
const teamPlanMode = resolveTeamPlanModeFromEntitlements(entitlements);
|
|
1164
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1165
|
+
res.end(JSON.stringify({
|
|
1166
|
+
workspaceId: wid,
|
|
1167
|
+
role,
|
|
1168
|
+
canManageWorkspace: true,
|
|
1169
|
+
teamPlanMode,
|
|
1170
|
+
allowed: teamPlanMode === 'team'
|
|
1171
|
+
}));
|
|
1172
|
+
}
|
|
1173
|
+
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
|
+
const statusCode = resolveErrorStatusCode(error, 400);
|
|
1190
|
+
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
|
1191
|
+
res.end(JSON.stringify({ error: String(error?.message || 'Unable to resolve team management access'), code: error?.code, details: error?.details }));
|
|
1192
|
+
}
|
|
1193
|
+
});
|
|
860
1194
|
// GET /api/taskforce/account/plan-preview - Read-only payload for upgrade/support CTAs.
|
|
861
1195
|
addRoute('GET', '/api/taskforce/account/plan-preview', async (req, res) => {
|
|
862
1196
|
if (!ensureCloudPlanData(req, res))
|
|
@@ -6,6 +6,7 @@ const BILLING_CONFIG_KEY_PLAN = 'billing.config.v1';
|
|
|
6
6
|
const PRICE_METADATA_PLAN_VERSION = 'taskforce_plan_version_id';
|
|
7
7
|
const BILLING_DIAGNOSTIC_RETENTION_DAYS = 30;
|
|
8
8
|
const BILLING_DIAGNOSTIC_MAX_PAYLOAD_BYTES = 8 * 1024;
|
|
9
|
+
const BILLING_STATUS_RECONCILIATION_TTL_MS = 30_000;
|
|
9
10
|
const TASKFORCE_PROXY_ORIGIN_HOST_HEADER = 'x-taskforce-origin-host';
|
|
10
11
|
const TASKFORCE_PROXY_ORIGIN_PROTO_HEADER = 'x-taskforce-origin-proto';
|
|
11
12
|
const TASKFORCE_PROXY_ORIGIN_BASE_URL_HEADER = 'x-taskforce-origin-base-url';
|
|
@@ -623,12 +624,18 @@ function getBillingMirrorRow(core, userIdRaw) {
|
|
|
623
624
|
const db = core.getDatabaseAdapter();
|
|
624
625
|
const tenantId = core.getTenantId();
|
|
625
626
|
return db.prepare(`
|
|
626
|
-
SELECT stripe_status, stripe_customer_id, stripe_subscription_id, stripe_price_id, billing_interval, trial_end, current_period_end, effective_until, entitlement_state, tier
|
|
627
|
+
SELECT stripe_status, stripe_customer_id, stripe_subscription_id, stripe_price_id, billing_interval, trial_end, current_period_end, effective_until, entitlement_state, tier, updated_at
|
|
627
628
|
FROM user_billing
|
|
628
629
|
WHERE tenant_id = ? AND user_id = ?
|
|
629
630
|
LIMIT 1
|
|
630
631
|
`).get(tenantId, userId);
|
|
631
632
|
}
|
|
633
|
+
function isBillingMirrorFresh(row) {
|
|
634
|
+
const updatedAt = Date.parse(String(row?.updated_at || ''));
|
|
635
|
+
if (!Number.isFinite(updatedAt))
|
|
636
|
+
return false;
|
|
637
|
+
return (Date.now() - updatedAt) < BILLING_STATUS_RECONCILIATION_TTL_MS;
|
|
638
|
+
}
|
|
632
639
|
function isStripeManagedSubscriptionStatus(statusRaw) {
|
|
633
640
|
const status = String(statusRaw || '').trim().toLowerCase();
|
|
634
641
|
return status === 'trialing'
|
|
@@ -708,6 +715,9 @@ async function reconcileBillingMirrorIfNeeded(core, userIdRaw) {
|
|
|
708
715
|
if (!userId)
|
|
709
716
|
return { row: null, conflictCode: null };
|
|
710
717
|
let row = getBillingMirrorRow(core, userId);
|
|
718
|
+
if (isBillingMirrorFresh(row)) {
|
|
719
|
+
return { row, conflictCode: null };
|
|
720
|
+
}
|
|
711
721
|
if (String(row?.stripe_subscription_id || '').trim()) {
|
|
712
722
|
try {
|
|
713
723
|
await reconcileBillingFromStripe(core, userId, row?.stripe_subscription_id);
|
|
@@ -1435,6 +1435,7 @@ describe('Billing Webhook Routes', () => {
|
|
|
1435
1435
|
});
|
|
1436
1436
|
it('reconciles stale entitlement from Stripe when billing status is requested', async () => {
|
|
1437
1437
|
const now = new Date().toISOString();
|
|
1438
|
+
const stale = new Date(Date.now() - 60_000).toISOString();
|
|
1438
1439
|
db.prepare(`
|
|
1439
1440
|
INSERT INTO plan_catalog (plan_id, tenant_id, display_name, status, created_at, updated_at)
|
|
1440
1441
|
VALUES ('pro', 'default', 'Pro Plan', 'active', ?, ?)
|
|
@@ -1451,12 +1452,12 @@ describe('Billing Webhook Routes', () => {
|
|
|
1451
1452
|
INSERT INTO user_billing (
|
|
1452
1453
|
user_id, tenant_id, stripe_customer_id, stripe_subscription_id, stripe_price_id, stripe_status, tier, billing_interval, entitlement_state, created_at, updated_at
|
|
1453
1454
|
) VALUES ('u1', 'default', 'cus_123', 'sub_123', 'price_old', 'active', 'starter', 'month', 'active', ?, ?)
|
|
1454
|
-
`).run(
|
|
1455
|
+
`).run(stale, stale);
|
|
1455
1456
|
db.prepare(`
|
|
1456
1457
|
INSERT INTO account_entitlements (
|
|
1457
1458
|
account_id, tenant_id, plan_version_id, entitlement_state, effective_at, created_at, updated_at
|
|
1458
1459
|
) VALUES ('u1', 'default', 'starter-v1', 'active', ?, ?, ?)
|
|
1459
|
-
`).run(
|
|
1460
|
+
`).run(stale, stale, stale);
|
|
1460
1461
|
const statusRoute = routeHandlers.get('GET /api/taskforce/billing/status');
|
|
1461
1462
|
const req = createMockReq('', { 'x-taskforce-runtime-mode': 'cloud' });
|
|
1462
1463
|
const res = createMockRes();
|
|
@@ -1471,6 +1472,35 @@ describe('Billing Webhook Routes', () => {
|
|
|
1471
1472
|
`).get();
|
|
1472
1473
|
expect(entitlement?.plan_version_id).toBe('pro-v1');
|
|
1473
1474
|
});
|
|
1475
|
+
it('skips Stripe reconciliation for fresh billing mirror rows when billing status is requested', async () => {
|
|
1476
|
+
const now = new Date().toISOString();
|
|
1477
|
+
db.prepare(`
|
|
1478
|
+
INSERT INTO plan_catalog (plan_id, tenant_id, display_name, status, created_at, updated_at)
|
|
1479
|
+
VALUES ('pro', 'default', 'Pro Plan', 'active', ?, ?)
|
|
1480
|
+
`).run(now, now);
|
|
1481
|
+
db.prepare(`
|
|
1482
|
+
INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
|
|
1483
|
+
VALUES ('pro-v1', 'default', 'pro', 1, 1, '{}', ?, ?)
|
|
1484
|
+
`).run(now, now);
|
|
1485
|
+
db.prepare(`
|
|
1486
|
+
INSERT INTO user_billing (
|
|
1487
|
+
user_id, tenant_id, stripe_customer_id, stripe_subscription_id, stripe_price_id, stripe_status, tier, billing_interval, entitlement_state, created_at, updated_at
|
|
1488
|
+
) VALUES ('u1', 'default', 'cus_123', 'sub_123', 'price_pro_month', 'active', 'pro', 'month', 'active', ?, ?)
|
|
1489
|
+
`).run(now, now);
|
|
1490
|
+
db.prepare(`
|
|
1491
|
+
INSERT INTO account_entitlements (
|
|
1492
|
+
account_id, tenant_id, plan_version_id, entitlement_state, effective_at, created_at, updated_at
|
|
1493
|
+
) VALUES ('u1', 'default', 'pro-v1', 'active', ?, ?, ?)
|
|
1494
|
+
`).run(now, now, now);
|
|
1495
|
+
const statusRoute = routeHandlers.get('GET /api/taskforce/billing/status');
|
|
1496
|
+
const req = createMockReq('', { 'x-taskforce-runtime-mode': 'cloud' });
|
|
1497
|
+
const res = createMockRes();
|
|
1498
|
+
await statusRoute(req, res, {});
|
|
1499
|
+
expect(res.statusCode).toBe(200);
|
|
1500
|
+
expect(mockSubscriptionRetrieve).not.toHaveBeenCalled();
|
|
1501
|
+
expect(mockCustomersList).not.toHaveBeenCalled();
|
|
1502
|
+
expect(mockSubscriptionList).not.toHaveBeenCalled();
|
|
1503
|
+
});
|
|
1474
1504
|
it('repairs billing status from an active Stripe subscription discovered by account email', async () => {
|
|
1475
1505
|
const now = new Date().toISOString();
|
|
1476
1506
|
db.prepare(`
|
|
@@ -24,9 +24,11 @@
|
|
|
24
24
|
import * as fs from 'fs';
|
|
25
25
|
import * as path from 'path';
|
|
26
26
|
import { normalizeTaskDataRelativePath } from '../../storage/objectStorageClient.js';
|
|
27
|
+
import { shouldUseProvisionalTaskReferences } from '../../utils/taskReferences.js';
|
|
27
28
|
import { purgeExpiredDocuments } from '../../storage/documentPurge.js';
|
|
28
29
|
import { parsePlan } from '../../utils/planParser.js';
|
|
29
30
|
import { getCanonicalDocumentName, getReadableDocumentName, isGeneratedDocumentName } from '../../utils/documentNames.js';
|
|
31
|
+
import { requestRuntimeMode } from './shared.js';
|
|
30
32
|
import { parseJsonBody } from '../routes.js';
|
|
31
33
|
export function registerDocumentRoutes(deps) {
|
|
32
34
|
const { addRoute, core, context, requestWorkspaceId, ensureAdminRole, resolveEffectiveObjectStorageConfig, getObjectStorageClient, resolveStorageWorkspaceId, ensureContextUploadAllowed, parseStorageFolderSegments, buildCanonicalWorkspaceAssetRelativePath, inferAssetKindFromPath, appendStorageTelemetry, scanAndRecordDocument, buildTaskAttachmentPayload, workspaceAssetStore, taskAssetStore, normalizeDocumentPathInput, getAssetMetadata, getCanonicalAssetMetadata, documentRetentionDays, appendDocumentPurgeHistory, markAssetDeleted, isAssetDeleted, clearAssetDeleted, ensureWithinDir, sanitizeDownloadFilename, contentTypeForExt, inferInlineDocumentFromPath, resolveCanonicalDocumentRead, inferWorkspaceIdFromStoragePath, inferTaskIdFromStoragePath, } = deps;
|
|
@@ -1097,7 +1099,10 @@ export function registerDocumentRoutes(deps) {
|
|
|
1097
1099
|
type: node.type || 'feature',
|
|
1098
1100
|
priority: typeof node.priority === 'number' ? node.priority : 2,
|
|
1099
1101
|
status: 'task',
|
|
1100
|
-
}, {
|
|
1102
|
+
}, {
|
|
1103
|
+
workspaceId,
|
|
1104
|
+
provisionalLocalReference: shouldUseProvisionalTaskReferences(requestRuntimeMode(req))
|
|
1105
|
+
});
|
|
1101
1106
|
created.push(taskItem.id);
|
|
1102
1107
|
totalCreated++;
|
|
1103
1108
|
}
|
|
@@ -30,6 +30,6 @@ export declare function makeResolveAdminRequestActor(core: TaskforceCore, contex
|
|
|
30
30
|
systemRole: "system_admin" | "user";
|
|
31
31
|
};
|
|
32
32
|
export declare function makeAuditAdminAction(core: TaskforceCore, context: RouteContext): (req: IncomingMessage, action: string, details: Record<string, unknown>) => void;
|
|
33
|
-
export declare function makeAuditAuthAction(core: TaskforceCore, requestWorkspaceId: (req: IncomingMessage) => string): (req: IncomingMessage, action: string, details: Record<string, unknown>) => void;
|
|
33
|
+
export declare function makeAuditAuthAction(core: TaskforceCore, requestWorkspaceId: (req: IncomingMessage) => string, context: RouteContext): (req: IncomingMessage, action: string, details: Record<string, unknown>) => void;
|
|
34
34
|
export declare function makeRequireCloudRuntime(context: RouteContext): (res: ServerResponse, feature: string) => boolean;
|
|
35
35
|
export declare function makeEnsureCloudPlanData(context: RouteContext): (req: IncomingMessage, res: ServerResponse) => boolean;
|
|
@@ -257,13 +257,21 @@ export function makeAuditAdminAction(core, context) {
|
|
|
257
257
|
console.info(`[Taskforce Admin Audit] ${JSON.stringify(payload)}`);
|
|
258
258
|
};
|
|
259
259
|
}
|
|
260
|
-
export function makeAuditAuthAction(core, requestWorkspaceId) {
|
|
260
|
+
export function makeAuditAuthAction(core, requestWorkspaceId, context) {
|
|
261
261
|
return function auditAuthAction(req, action, details) {
|
|
262
|
+
const authConfig = context.authConfig;
|
|
263
|
+
const resolvedAccess = authConfig?.enabled ? resolveRequestAccess(req, authConfig) : null;
|
|
264
|
+
const actorUserId = resolvedAccess?.authenticated && resolvedAccess.userId && resolvedAccess.userId !== 'anonymous'
|
|
265
|
+
? resolvedAccess.userId
|
|
266
|
+
: requestUserId(req);
|
|
267
|
+
const actorRole = resolvedAccess?.authenticated
|
|
268
|
+
? String(resolvedAccess.role || '').trim().toLowerCase() || requestRole(req)
|
|
269
|
+
: requestRole(req);
|
|
262
270
|
const payload = {
|
|
263
271
|
ts: new Date().toISOString(),
|
|
264
272
|
action,
|
|
265
|
-
actorUserId
|
|
266
|
-
actorRole
|
|
273
|
+
actorUserId,
|
|
274
|
+
actorRole,
|
|
267
275
|
workspaceId: requestWorkspaceId(req),
|
|
268
276
|
details
|
|
269
277
|
};
|