@taskforcehq/taskforce 0.3.302 → 0.3.304
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Taskforce.module.css +113 -5
- package/dist/TaskforceCore.js +560 -208
- package/dist/TaskforceCore.test.js +1833 -91
- package/dist/annotatedAttachments/service.d.ts +1 -1
- package/dist/annotatedAttachments/types.d.ts +1 -1
- package/dist/components/features/AgentsModule.js +1 -1
- package/dist/components/features/AgentsModule.test.js +1 -1
- package/dist/components/features/TaskSettings.js +58 -38
- package/dist/components/features/TaskSettings.test.js +139 -40
- package/dist/components/task/TaskActionHeader.d.ts +2 -1
- package/dist/components/task/TaskActionHeader.js +3 -3
- package/dist/components/task/TaskActionHeader.test.js +8 -0
- package/dist/components/task/TaskCard.js +16 -1
- package/dist/components/task/TaskCard.test.js +31 -0
- package/dist/components/task/TaskForm.d.ts +2 -1
- package/dist/components/task/TaskForm.js +22 -4
- package/dist/components/task/TaskForm.test.js +46 -0
- package/dist/components/task/TaskKanban.d.ts +1 -1
- package/dist/components/task/TaskKanban.js +31 -160
- package/dist/components/task/TaskKanban.test.js +17 -0
- package/dist/components/task/TaskStatusActions.d.ts +6 -1
- package/dist/components/task/TaskStatusActions.js +8 -3
- package/dist/components/task/TaskStatusActions.test.js +9 -0
- package/dist/components/task/taskKanbanDropRules.d.ts +58 -0
- package/dist/components/task/taskKanbanDropRules.js +187 -0
- package/dist/components/task/taskKanbanDropRules.test.d.ts +1 -0
- package/dist/components/task/taskKanbanDropRules.test.js +114 -0
- package/dist/components/ui/Modal.d.ts +1 -1
- package/dist/components/ui/Modal.js +1 -0
- package/dist/components/views/AppShellHeader.js +5 -1
- package/dist/components/views/AppShellHeader.test.js +4 -0
- package/dist/components/views/PlanComparisonPage.d.ts +3 -2
- package/dist/components/views/PlanComparisonPage.js +4 -2
- package/dist/components/views/PlanComparisonPage.test.js +4 -0
- package/dist/components/views/PlansPage.d.ts +7 -1
- package/dist/components/views/PlansPage.js +139 -53
- package/dist/components/views/PlansPage.test.d.ts +1 -0
- package/dist/components/views/PlansPage.test.js +157 -0
- package/dist/components/views/StandaloneLayout.d.ts +1 -0
- package/dist/components/views/StandaloneLayout.js +216 -186
- package/dist/components/views/WidgetView.js +11 -2
- package/dist/components/views/standalone/modals/AccountHubModal.d.ts +8 -3
- package/dist/components/views/standalone/modals/AccountHubModal.js +16 -2
- package/dist/components/views/standalone/modals/AccountHubModal.test.d.ts +1 -0
- package/dist/components/views/standalone/modals/AccountHubModal.test.js +26 -0
- package/dist/components/views/standalone/modals/SyncStatusModal.js +1 -1
- package/dist/core/DeletedTaskLifecycleService.d.ts +2 -1
- package/dist/core/EntitlementsPolicy.test.js +2 -2
- package/dist/core/GlobalSettingsService.js +78 -18
- package/dist/core/PlanEntitlementService.d.ts +31 -5
- package/dist/core/PlanEntitlementService.js +350 -222
- package/dist/core/SignupMonetizationSettings.test.js +208 -49
- package/dist/core/SystemAdmin.test.js +186 -96
- package/dist/core/TaskActivityService.d.ts +4 -1
- package/dist/core/TaskActivityService.js +45 -0
- package/dist/core/TaskAuditTimeline.test.js +52 -1
- package/dist/core/Taskforce.d.ts +65 -20
- package/dist/core/Taskforce.js +261 -51
- package/dist/core/Taskforce.mcpAuth.test.js +0 -2
- package/dist/core/UserProfileAvatarDrafts.test.js +12 -3
- package/dist/core/WorkspacePlanMode.test.js +65 -0
- package/dist/core/types.d.ts +11 -12
- package/dist/documentReviews/service.d.ts +1 -1
- package/dist/documentReviews/types.d.ts +1 -1
- package/dist/hooks/auth/useTaskforceAuthBootstrap.d.ts +48 -0
- package/dist/hooks/auth/useTaskforceAuthBootstrap.js +219 -0
- package/dist/hooks/sync/auth.d.ts +37 -0
- package/dist/hooks/sync/auth.js +61 -0
- package/dist/hooks/sync/bootstrap.d.ts +34 -0
- package/dist/hooks/sync/bootstrap.js +52 -0
- package/dist/hooks/sync/orchestratorShared.d.ts +36 -0
- package/dist/hooks/sync/orchestratorShared.js +209 -0
- package/dist/hooks/sync/orchestratorShared.test.d.ts +1 -0
- package/dist/hooks/sync/orchestratorShared.test.js +49 -0
- package/dist/hooks/sync/realtime.d.ts +25 -0
- package/dist/hooks/sync/realtime.js +60 -0
- package/dist/hooks/sync/recovery.d.ts +69 -0
- package/dist/hooks/sync/recovery.js +118 -0
- package/dist/hooks/sync/transfers.d.ts +122 -0
- package/dist/hooks/sync/transfers.js +431 -0
- package/dist/hooks/sync/useDataVersionRefresh.d.ts +16 -0
- package/dist/hooks/sync/useDataVersionRefresh.js +73 -0
- package/dist/hooks/ui/useResourceExport.d.ts +3 -1
- package/dist/hooks/ui/useResourceExport.js +5 -2
- package/dist/hooks/ui/useWorkflowTemplates.js +11 -6
- package/dist/hooks/useRealtimeSync.js +2 -1
- package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +8 -0
- package/dist/hooks/useSyncOrchestrator.context-assets.test.d.ts +1 -0
- package/dist/hooks/useSyncOrchestrator.context-assets.test.js +228 -0
- package/dist/hooks/useSyncOrchestrator.d.ts +5 -4
- package/dist/hooks/useSyncOrchestrator.js +315 -860
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +665 -8
- package/dist/hooks/useTaskMutations.js +45 -6
- package/dist/hooks/useTaskMutations.test.js +147 -0
- package/dist/hooks/useTaskforce.d.ts +11 -2
- package/dist/hooks/useTaskforce.js +290 -812
- package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
- package/dist/hooks/useWorkspaceSyncController.js +22 -10
- package/dist/hooks/workspace/useTaskforceApiRouting.test.js +17 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +166 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +434 -0
- package/dist/hooks/workspace/workspaceLocalState.d.ts +19 -0
- package/dist/hooks/workspace/workspaceLocalState.js +100 -0
- package/dist/hooks/workspace/workspaceLocalState.test.d.ts +1 -0
- package/dist/hooks/workspace/workspaceLocalState.test.js +59 -0
- package/dist/localization/locales/en-US.js +2 -2
- package/dist/mcp/canonicalAssetHelpers.d.ts +70 -0
- package/dist/mcp/canonicalAssetHelpers.js +259 -0
- package/dist/mcp/clientIntegrations.d.ts +12 -1
- package/dist/mcp/clientIntegrations.js +161 -6
- package/dist/mcp/clientIntegrations.test.js +76 -4
- package/dist/mcp/documentHelpers.d.ts +100 -0
- package/dist/mcp/documentHelpers.js +161 -0
- package/dist/mcp/runtime.js +264 -505
- package/dist/mcp/runtime.test.js +353 -89
- package/dist/mcp/taskAttachmentHelpers.d.ts +41 -0
- package/dist/mcp/taskAttachmentHelpers.js +60 -0
- package/dist/mcp/taskAttachmentTypes.d.ts +37 -0
- package/dist/mcp/taskAttachmentTypes.js +1 -0
- package/dist/migrations/billingSchemaParity.test.js +3 -0
- package/dist/migrations/taskSchemaMigrations.js +15 -0
- package/dist/plugins/vite.js +1 -1
- package/dist/runtime/appGateDefinitions.d.ts +1 -1
- package/dist/runtime/appGateDefinitions.js +1 -1
- package/dist/server/index.cookieProxy.test.js +1 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +48 -3
- package/dist/server/index.test.js +14 -2
- package/dist/server/mockStripe.d.ts +1 -0
- package/dist/server/mockStripe.js +2 -0
- package/dist/server/routes/admin.js +91 -40
- package/dist/server/routes/auth.d.ts +1 -0
- package/dist/server/routes/auth.js +231 -55
- package/dist/server/routes/billing.js +473 -39
- package/dist/server/routes/billing.test.js +972 -37
- package/dist/server/routes/shared.d.ts +30 -0
- package/dist/server/routes/shared.js +31 -3
- package/dist/server/routes/sync.integration.test.js +180 -0
- package/dist/server/routes/sync.js +125 -242
- package/dist/server/routes/tasks.js +49 -7
- package/dist/server/routes.js +11 -22
- package/dist/server/routes.test.js +530 -81
- package/dist/shared/taskActor.d.ts +10 -0
- package/dist/shared/taskActor.js +1 -0
- package/dist/sync/cloudSyncApi.d.ts +1 -0
- package/dist/sync/collaborationSyncPayload.js +4 -0
- package/dist/sync/collaborationSyncPayload.test.js +8 -0
- package/dist/sync/collaborationSyncState.d.ts +3 -1
- package/dist/sync/collaborationSyncState.js +19 -0
- package/dist/sync/contentSyncState.d.ts +10 -0
- package/dist/sync/contentSyncState.js +277 -14
- package/dist/sync/syncApplyHandlers.d.ts +87 -1
- package/dist/sync/syncApplyHandlers.js +370 -8
- package/dist/sync/syncApplyHandlers.test.d.ts +1 -0
- package/dist/sync/syncApplyHandlers.test.js +377 -0
- package/dist/sync/syncService.d.ts +9 -1
- package/dist/sync/syncService.js +57 -10
- package/dist/sync/syncService.test.js +48 -0
- package/dist/sync/workspacePullFeed.d.ts +11 -0
- package/dist/sync/workspacePullFeed.js +123 -68
- package/dist/sync/workspacePullFeed.test.js +180 -0
- package/dist/sync/workspaceSyncModel.d.ts +22 -1
- package/dist/sync/workspaceSyncModel.js +65 -0
- package/dist/sync/workspaceSyncModel.test.js +119 -0
- package/dist/types.d.ts +2 -10
- package/dist/ui/assets/AgentsModule-BLWVbuKP.js +1 -0
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-Dvi02Y7q.js → AnnotatedAttachmentWorkspace-BlS-cqnl.js} +1 -1
- package/dist/ui/assets/{DocumentWorkspace-iys3zfca.js → DocumentWorkspace-BH_6DF6x.js} +1 -1
- package/dist/ui/assets/{InitiativesModule-DJvLJI3A.js → InitiativesModule-BjR6iBf9.js} +1 -1
- package/dist/ui/assets/{PlansPage-BlVC_lRq.css → PlansPage-C2dd2n1X.css} +1 -1
- package/dist/ui/assets/PlansPage-CSDQ4sLa.js +1 -0
- package/dist/ui/assets/TaskSettings-CEDy34Jo.js +9 -0
- package/dist/ui/assets/{WorkflowsModule-BguHeQaV.js → WorkflowsModule-DnFqdUME.js} +1 -1
- package/dist/ui/assets/index-DXUdtH1B.js +6 -0
- package/dist/ui/assets/index-Ii0B0rTO.css +1 -0
- package/dist/ui/assets/{vendor-icons-C8EED3Oh.js → vendor-icons-I7ZwG1z_.js} +1 -1
- package/dist/ui/index.html +4 -4
- package/dist/utils/accountProfileSummaryCache.d.ts +31 -0
- package/dist/utils/accountProfileSummaryCache.js +97 -0
- package/dist/utils/accountProfileSummaryCache.test.d.ts +1 -0
- package/dist/utils/accountProfileSummaryCache.test.js +63 -0
- package/dist/utils/taskActivity.js +4 -0
- package/dist/utils/taskActivity.test.js +25 -1
- package/package.json +5 -3
- package/scripts/playwright-auth.sh +4 -0
- package/dist/ui/assets/AgentsModule-JG5jc3he.js +0 -1
- package/dist/ui/assets/PlansPage-BMPlpFpw.js +0 -1
- package/dist/ui/assets/TaskSettings-Bqd4cCzW.js +0 -8
- package/dist/ui/assets/index-MXeWoebC.css +0 -1
- package/dist/ui/assets/index-YJ4k5QNa.js +0 -6
- package/dist/utils/billingStatusCache.d.ts +0 -21
- package/dist/utils/billingStatusCache.js +0 -70
|
@@ -23,6 +23,17 @@ function openCore(projectRoot) {
|
|
|
23
23
|
core.upsertPlanCatalog({ planId: 'starter', displayName: 'Starter' });
|
|
24
24
|
addFreeMapping('starter-v1');
|
|
25
25
|
core.upsertPlanCatalog({ planId: 'starter', displayName: 'Starter', status: 'active' });
|
|
26
|
+
core.updatePlanVersion({
|
|
27
|
+
planId: 'starter',
|
|
28
|
+
versionNumber: 1,
|
|
29
|
+
seatLimit: 1
|
|
30
|
+
});
|
|
31
|
+
core.updatePlanVersionFeatures({
|
|
32
|
+
planVersionId: 'starter-v1',
|
|
33
|
+
features: [
|
|
34
|
+
{ featureKey: 'workspace.switching', access: 'enabled' }
|
|
35
|
+
]
|
|
36
|
+
});
|
|
26
37
|
core.upsertPlanCatalog({ planId: 'collab', displayName: 'Collab' });
|
|
27
38
|
addFreeMapping('collab-v1');
|
|
28
39
|
core.upsertPlanCatalog({ planId: 'collab', displayName: 'Collab', status: 'active' });
|
|
@@ -214,4 +225,58 @@ describe('TaskforceCore account-level plan mode and invite revoke', () => {
|
|
|
214
225
|
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
215
226
|
}
|
|
216
227
|
});
|
|
228
|
+
it('grants the default onboarding entitlement to newly accepted invited accounts', () => {
|
|
229
|
+
const projectRoot = makeProjectRoot();
|
|
230
|
+
const core = openCore(projectRoot);
|
|
231
|
+
try {
|
|
232
|
+
core.updateGlobalSettings({
|
|
233
|
+
billing: {
|
|
234
|
+
onboardingPolicy: {
|
|
235
|
+
mode: 'auto_grant_free',
|
|
236
|
+
planVersionId: 'starter-v1'
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
core.createWorkspace({ workspaceId: 'workspace-invite', name: 'Invite Workspace' });
|
|
241
|
+
core.ensureBootstrapUserAccess({
|
|
242
|
+
userId: 'owner-user',
|
|
243
|
+
workspaceId: 'workspace-invite',
|
|
244
|
+
role: 'owner',
|
|
245
|
+
email: 'owner@example.com'
|
|
246
|
+
});
|
|
247
|
+
core.setUserAccountPlanMode('owner-user', 'team');
|
|
248
|
+
core.ensureBootstrapUserAccess({
|
|
249
|
+
userId: 'invited-user',
|
|
250
|
+
workspaceId: 'workspace-invite',
|
|
251
|
+
role: 'member',
|
|
252
|
+
email: 'invited@example.com'
|
|
253
|
+
});
|
|
254
|
+
core.setWorkspaceUserDisabled('invited-user', 'workspace-invite', true);
|
|
255
|
+
const db = core.db;
|
|
256
|
+
db.prepare(`
|
|
257
|
+
UPDATE workspace_memberships
|
|
258
|
+
SET status = 'invited'
|
|
259
|
+
WHERE tenant_id = ? AND user_id = ? AND workspace_id = ?
|
|
260
|
+
`).run('default', 'invited-user', 'workspace-invite');
|
|
261
|
+
const invite = core.requestInviteAcceptance({ userId: 'invited-user', workspaceId: 'workspace-invite' });
|
|
262
|
+
const accepted = core.acceptInviteWithToken({
|
|
263
|
+
token: String(invite.token),
|
|
264
|
+
password: 'Password123!'
|
|
265
|
+
});
|
|
266
|
+
expect(accepted.workspaceId).toBe('workspace-invite');
|
|
267
|
+
expect(core.hasAccountEntitlement('invited-user')).toBe(true);
|
|
268
|
+
expect(core.getAccountEntitlement('invited-user')).toEqual(expect.objectContaining({
|
|
269
|
+
planVersionId: 'starter-v1',
|
|
270
|
+
state: 'active'
|
|
271
|
+
}));
|
|
272
|
+
expect(core.getAccountAccessStatus('invited-user')).toEqual(expect.objectContaining({
|
|
273
|
+
gate: 'ok',
|
|
274
|
+
canAccessApp: true
|
|
275
|
+
}));
|
|
276
|
+
}
|
|
277
|
+
finally {
|
|
278
|
+
core.close();
|
|
279
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
280
|
+
}
|
|
281
|
+
});
|
|
217
282
|
});
|
package/dist/core/types.d.ts
CHANGED
|
@@ -268,8 +268,9 @@ export interface GlobalTaskforceSettings {
|
|
|
268
268
|
authRateLimitEnabled?: boolean;
|
|
269
269
|
authRateLimitMaxRequests?: number;
|
|
270
270
|
authRateLimitWindowMs?: number;
|
|
271
|
-
|
|
272
|
-
|
|
271
|
+
};
|
|
272
|
+
billing?: {
|
|
273
|
+
onboardingPolicy?: OnboardingPolicy;
|
|
273
274
|
};
|
|
274
275
|
site?: {
|
|
275
276
|
showRunTaskforceLocally?: boolean;
|
|
@@ -299,14 +300,14 @@ export interface UserGlobalSyncSettings {
|
|
|
299
300
|
weekStartsOn?: 'sunday' | 'monday';
|
|
300
301
|
};
|
|
301
302
|
}
|
|
302
|
-
export type
|
|
303
|
-
export interface
|
|
304
|
-
|
|
305
|
-
|
|
303
|
+
export type OnboardingPolicyMode = 'require_plan_selection' | 'auto_grant_free' | 'auto_start_trial';
|
|
304
|
+
export interface OnboardingPolicy {
|
|
305
|
+
mode: OnboardingPolicyMode;
|
|
306
|
+
planVersionId: string | null;
|
|
306
307
|
}
|
|
307
|
-
export interface
|
|
308
|
+
export interface OnboardingPolicyValidationResult {
|
|
308
309
|
valid: boolean;
|
|
309
|
-
code?: '
|
|
310
|
+
code?: 'ONBOARDING_PLAN_VERSION_REQUIRED' | 'ONBOARDING_PLAN_VERSION_NOT_FOUND' | 'ONBOARDING_PLAN_NOT_ENABLED' | 'ONBOARDING_PLAN_MUST_BE_FREE' | 'ONBOARDING_PLAN_TRIAL_REQUIRED';
|
|
310
311
|
message?: string;
|
|
311
312
|
details?: Record<string, unknown>;
|
|
312
313
|
}
|
|
@@ -441,7 +442,7 @@ export interface McpAuditLogRecord {
|
|
|
441
442
|
reasonCode: string | null;
|
|
442
443
|
details: Record<string, unknown>;
|
|
443
444
|
}
|
|
444
|
-
export type EntitlementState = 'active' | 'trialing' | 'grace' | 'suspended' | 'canceled';
|
|
445
|
+
export type EntitlementState = 'pending_plan_selection' | 'checkout_pending' | 'active' | 'trialing' | 'grace' | 'suspended' | 'canceled';
|
|
445
446
|
export type PlanFeatureAccess = 'enabled' | 'disabled' | 'limited';
|
|
446
447
|
export interface PlanFeatureSnapshot {
|
|
447
448
|
featureKey: string;
|
|
@@ -499,17 +500,15 @@ export interface AccountAccessStatus_Ok {
|
|
|
499
500
|
hasEntitlement: true;
|
|
500
501
|
canAccessApp: true;
|
|
501
502
|
canAccessPlans: true;
|
|
502
|
-
signupMonetizationStrategy: SignupMonetizationStrategy;
|
|
503
503
|
planSelectionRequired: false;
|
|
504
504
|
message: null;
|
|
505
505
|
entitlement: AccountEntitlementSnapshot;
|
|
506
506
|
}
|
|
507
507
|
export interface AccountAccessStatus_Blocked {
|
|
508
|
-
gate: 'plan_selection_required' | 'misconfigured' | 'missing_entitlement';
|
|
508
|
+
gate: 'plan_selection_required' | 'checkout_pending' | 'misconfigured' | 'missing_entitlement';
|
|
509
509
|
hasEntitlement: boolean;
|
|
510
510
|
canAccessApp: false;
|
|
511
511
|
canAccessPlans: true;
|
|
512
|
-
signupMonetizationStrategy: SignupMonetizationStrategy;
|
|
513
512
|
planSelectionRequired: boolean;
|
|
514
513
|
message: string;
|
|
515
514
|
entitlement: AccountEntitlementSnapshot | null;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { TaskActor } from '../
|
|
1
|
+
import type { TaskActor } from '../shared/taskActor.js';
|
|
2
2
|
import type { WorkspaceAssetStore } from '../storage/workspaceAssetStore.js';
|
|
3
3
|
import { DocumentReviewCommentStore } from './DocumentReviewCommentStore.js';
|
|
4
4
|
import { DocumentReviewSessionStore } from './DocumentReviewSessionStore.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { TaskActor } from '../
|
|
1
|
+
import type { TaskActor } from '../shared/taskActor.js';
|
|
2
2
|
export type DocumentReviewSessionStatus = 'open' | 'resolved';
|
|
3
3
|
export type DocumentReviewAnchorKind = 'document' | 'quote' | 'line-range';
|
|
4
4
|
export interface DocumentReviewDocumentAnchor {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type MutableRefObject } from 'react';
|
|
2
|
+
import { createDefaultAssigneeOptions } from '../../utils/assignees';
|
|
3
|
+
export type BootstrapPhase = 'idle' | 'auth' | 'workspace' | 'config' | 'ready' | 'stalled';
|
|
4
|
+
interface UseTaskforceAuthBootstrapArgs {
|
|
5
|
+
runtimeConfigReady: boolean;
|
|
6
|
+
shouldProbeCloudAuth: boolean;
|
|
7
|
+
authSessionResolved: boolean;
|
|
8
|
+
resolveCloudAuthUrl: (path: string) => string;
|
|
9
|
+
authOnlyCloudMode: boolean;
|
|
10
|
+
authRequiredError: string;
|
|
11
|
+
runtimeMode: 'local' | 'cloud';
|
|
12
|
+
workspaceSelectionScope: string;
|
|
13
|
+
markBootstrapPhase: (phase: Exclude<BootstrapPhase, 'stalled'>) => void;
|
|
14
|
+
markBootstrapStalled: (message: string) => void;
|
|
15
|
+
setRuntimeMode: (mode: 'local' | 'cloud') => void;
|
|
16
|
+
setAuthRequiredForApi: (value: boolean) => void;
|
|
17
|
+
setIsAuthenticated: (value: boolean) => void;
|
|
18
|
+
setAuthUserId: (value: string) => void;
|
|
19
|
+
setAuthWorkspaceId: (value: string) => void;
|
|
20
|
+
setAuthUserEmail: (value: string) => void;
|
|
21
|
+
setAuthUserDisplayName: (value: string) => void;
|
|
22
|
+
setAuthUserAvatarUrl: (value: string) => void;
|
|
23
|
+
setUserGlobalSyncStatus: (value: 'idle' | 'disconnected') => void;
|
|
24
|
+
setUserGlobalSyncError: (value: string | null) => void;
|
|
25
|
+
setHasBetaAccess: (value: boolean) => void;
|
|
26
|
+
setAvailableWorkspaces: (value: any[]) => void;
|
|
27
|
+
setHydratedWorkspaceRole: (value: any) => void;
|
|
28
|
+
setAssigneeOptions: (value: ReturnType<typeof createDefaultAssigneeOptions>) => void;
|
|
29
|
+
setAuthBlocked: (value: boolean) => void;
|
|
30
|
+
setAuthSessionResolved: (value: boolean) => void;
|
|
31
|
+
setError: (value: string | ((prev: string) => string)) => void;
|
|
32
|
+
applyResolvedWorkspaceId: (workspaceId: string, options?: {
|
|
33
|
+
preserveNonDefaultDefault?: boolean;
|
|
34
|
+
}) => void;
|
|
35
|
+
readPersistedWorkspaceId: (scope: string) => string;
|
|
36
|
+
authSessionRequestRef: MutableRefObject<Promise<boolean> | null>;
|
|
37
|
+
authSessionEpochRef: MutableRefObject<number>;
|
|
38
|
+
authSessionLastCheckedAtRef: MutableRefObject<number>;
|
|
39
|
+
authSessionLastResultRef: MutableRefObject<boolean>;
|
|
40
|
+
isLoopbackHost: boolean;
|
|
41
|
+
setAuthStateReadyRefs: (authenticated: boolean, authSessionResolved: boolean, authRequiredForApi: boolean) => void;
|
|
42
|
+
}
|
|
43
|
+
export declare function useTaskforceAuthBootstrap({ runtimeConfigReady, shouldProbeCloudAuth, authSessionResolved, resolveCloudAuthUrl, authOnlyCloudMode, authRequiredError, runtimeMode, workspaceSelectionScope, markBootstrapPhase, markBootstrapStalled, setRuntimeMode, setAuthRequiredForApi, setIsAuthenticated, setAuthUserId, setAuthWorkspaceId, setAuthUserEmail, setAuthUserDisplayName, setAuthUserAvatarUrl, setUserGlobalSyncStatus, setUserGlobalSyncError, setHasBetaAccess, setAvailableWorkspaces, setHydratedWorkspaceRole, setAssigneeOptions, setAuthBlocked, setAuthSessionResolved, setError, applyResolvedWorkspaceId, readPersistedWorkspaceId, authSessionRequestRef, authSessionEpochRef, authSessionLastCheckedAtRef, authSessionLastResultRef, isLoopbackHost, setAuthStateReadyRefs }: UseTaskforceAuthBootstrapArgs): {
|
|
44
|
+
checkAuthSession: (options?: {
|
|
45
|
+
force?: boolean;
|
|
46
|
+
}) => Promise<boolean>;
|
|
47
|
+
};
|
|
48
|
+
export {};
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { useCallback, useEffect } from 'react';
|
|
2
|
+
import { createDefaultAssigneeOptions } from '../../utils/assignees';
|
|
3
|
+
import { persistLocalMutationActorUserId, readLocalMutationActorUserId } from '../../utils/taskforceApiClient';
|
|
4
|
+
export function useTaskforceAuthBootstrap({ runtimeConfigReady, shouldProbeCloudAuth, authSessionResolved, resolveCloudAuthUrl, authOnlyCloudMode, authRequiredError, runtimeMode, workspaceSelectionScope, markBootstrapPhase, markBootstrapStalled, setRuntimeMode, setAuthRequiredForApi, setIsAuthenticated, setAuthUserId, setAuthWorkspaceId, setAuthUserEmail, setAuthUserDisplayName, setAuthUserAvatarUrl, setUserGlobalSyncStatus, setUserGlobalSyncError, setHasBetaAccess, setAvailableWorkspaces, setHydratedWorkspaceRole, setAssigneeOptions, setAuthBlocked, setAuthSessionResolved, setError, applyResolvedWorkspaceId, readPersistedWorkspaceId, authSessionRequestRef, authSessionEpochRef, authSessionLastCheckedAtRef, authSessionLastResultRef, isLoopbackHost, setAuthStateReadyRefs }) {
|
|
5
|
+
const checkAuthSession = useCallback(async (options) => {
|
|
6
|
+
const force = options?.force === true;
|
|
7
|
+
markBootstrapPhase('auth');
|
|
8
|
+
if (!runtimeConfigReady) {
|
|
9
|
+
return authSessionLastResultRef.current;
|
|
10
|
+
}
|
|
11
|
+
if (!shouldProbeCloudAuth) {
|
|
12
|
+
setRuntimeMode('local');
|
|
13
|
+
setAuthRequiredForApi(false);
|
|
14
|
+
setIsAuthenticated(false);
|
|
15
|
+
setAuthUserId('anonymous');
|
|
16
|
+
setAuthWorkspaceId('');
|
|
17
|
+
persistLocalMutationActorUserId(null);
|
|
18
|
+
setAuthUserEmail('');
|
|
19
|
+
setAuthUserDisplayName('');
|
|
20
|
+
setAuthUserAvatarUrl('');
|
|
21
|
+
setUserGlobalSyncStatus('disconnected');
|
|
22
|
+
setUserGlobalSyncError(null);
|
|
23
|
+
setHasBetaAccess(true);
|
|
24
|
+
setAvailableWorkspaces([]);
|
|
25
|
+
setHydratedWorkspaceRole(null);
|
|
26
|
+
setAssigneeOptions(createDefaultAssigneeOptions());
|
|
27
|
+
setAuthBlocked(false);
|
|
28
|
+
setAuthSessionResolved(true);
|
|
29
|
+
setAuthStateReadyRefs(false, true, false);
|
|
30
|
+
authSessionLastResultRef.current = false;
|
|
31
|
+
authSessionLastCheckedAtRef.current = Date.now();
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
const sessionUrl = resolveCloudAuthUrl('/api/taskforce/auth/session');
|
|
35
|
+
const now = Date.now();
|
|
36
|
+
if (!force && authSessionRequestRef.current)
|
|
37
|
+
return authSessionRequestRef.current;
|
|
38
|
+
if (!force && authSessionResolved && (now - authSessionLastCheckedAtRef.current) < 1500) {
|
|
39
|
+
return authSessionLastResultRef.current;
|
|
40
|
+
}
|
|
41
|
+
const requestEpoch = ++authSessionEpochRef.current;
|
|
42
|
+
let request = null;
|
|
43
|
+
request = (async () => {
|
|
44
|
+
const abortController = new AbortController();
|
|
45
|
+
const timeoutId = typeof window !== 'undefined'
|
|
46
|
+
? window.setTimeout(() => abortController.abort(), 8000)
|
|
47
|
+
: null;
|
|
48
|
+
const isStaleRequest = () => authSessionEpochRef.current !== requestEpoch;
|
|
49
|
+
try {
|
|
50
|
+
const res = await fetch(sessionUrl, {
|
|
51
|
+
method: 'GET',
|
|
52
|
+
credentials: 'include',
|
|
53
|
+
mode: 'cors',
|
|
54
|
+
signal: abortController.signal
|
|
55
|
+
});
|
|
56
|
+
if (isStaleRequest()) {
|
|
57
|
+
return authSessionLastResultRef.current;
|
|
58
|
+
}
|
|
59
|
+
if (res.status === 429) {
|
|
60
|
+
setAuthSessionResolved(true);
|
|
61
|
+
setAuthStateReadyRefs(authSessionLastResultRef.current, true, false);
|
|
62
|
+
return authSessionLastResultRef.current;
|
|
63
|
+
}
|
|
64
|
+
if (!res.ok) {
|
|
65
|
+
if (isStaleRequest()) {
|
|
66
|
+
return authSessionLastResultRef.current;
|
|
67
|
+
}
|
|
68
|
+
if (isLoopbackHost)
|
|
69
|
+
persistLocalMutationActorUserId(null);
|
|
70
|
+
setAuthSessionResolved(true);
|
|
71
|
+
setAuthStateReadyRefs(false, true, false);
|
|
72
|
+
authSessionLastResultRef.current = false;
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
const data = await res.json();
|
|
76
|
+
if (isStaleRequest()) {
|
|
77
|
+
return authSessionLastResultRef.current;
|
|
78
|
+
}
|
|
79
|
+
const required = Boolean(data.authRequiredForApi);
|
|
80
|
+
const authenticated = Boolean(data.authenticated);
|
|
81
|
+
const betaAccess = authenticated ? (data.betaAccess !== false) : true;
|
|
82
|
+
const workspaceId = typeof data.workspaceId === 'string' && data.workspaceId.trim().length > 0
|
|
83
|
+
? data.workspaceId.trim()
|
|
84
|
+
: '';
|
|
85
|
+
const userId = typeof data.userId === 'string' && data.userId.trim().length > 0
|
|
86
|
+
? data.userId.trim()
|
|
87
|
+
: 'anonymous';
|
|
88
|
+
const userEmail = typeof data.email === 'string' && data.email.trim().length > 0
|
|
89
|
+
? data.email.trim().toLowerCase()
|
|
90
|
+
: '';
|
|
91
|
+
const userDisplayName = typeof data.displayName === 'string' ? data.displayName.trim() : '';
|
|
92
|
+
const userAvatarUrl = typeof data.avatarUrl === 'string' ? data.avatarUrl.trim() : '';
|
|
93
|
+
const isLocalRuntime = runtimeMode === 'local';
|
|
94
|
+
const keepLocalWorkspaceContext = runtimeMode === 'local' || authOnlyCloudMode;
|
|
95
|
+
const nextAuthRequiredForApi = authOnlyCloudMode ? false : (isLocalRuntime ? false : required);
|
|
96
|
+
setAuthRequiredForApi(nextAuthRequiredForApi);
|
|
97
|
+
setIsAuthenticated(authenticated);
|
|
98
|
+
setAuthUserId(authenticated ? userId : 'anonymous');
|
|
99
|
+
setAuthWorkspaceId(authenticated ? workspaceId : '');
|
|
100
|
+
if (isLoopbackHost)
|
|
101
|
+
persistLocalMutationActorUserId(authenticated ? userId : null);
|
|
102
|
+
setAuthUserEmail(authenticated ? userEmail : '');
|
|
103
|
+
setAuthUserDisplayName(authenticated ? userDisplayName : '');
|
|
104
|
+
setAuthUserAvatarUrl(authenticated ? userAvatarUrl : '');
|
|
105
|
+
if (authenticated) {
|
|
106
|
+
setError((prev) => prev === authRequiredError ? '' : prev);
|
|
107
|
+
}
|
|
108
|
+
setUserGlobalSyncStatus(authenticated ? 'idle' : 'disconnected');
|
|
109
|
+
setUserGlobalSyncError(null);
|
|
110
|
+
setHasBetaAccess(betaAccess);
|
|
111
|
+
if (!keepLocalWorkspaceContext) {
|
|
112
|
+
applyResolvedWorkspaceId(workspaceId || readPersistedWorkspaceId(workspaceSelectionScope) || 'default', { preserveNonDefaultDefault: true });
|
|
113
|
+
}
|
|
114
|
+
setAuthBlocked(authOnlyCloudMode ? false : (isLocalRuntime ? false : (required && !authenticated)));
|
|
115
|
+
setAuthSessionResolved(true);
|
|
116
|
+
setAuthStateReadyRefs(authenticated, true, nextAuthRequiredForApi);
|
|
117
|
+
authSessionLastResultRef.current = authenticated;
|
|
118
|
+
if (authenticated)
|
|
119
|
+
markBootstrapPhase('ready');
|
|
120
|
+
return authenticated;
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
if (isStaleRequest()) {
|
|
124
|
+
return authSessionLastResultRef.current;
|
|
125
|
+
}
|
|
126
|
+
if (isLoopbackHost)
|
|
127
|
+
persistLocalMutationActorUserId(null);
|
|
128
|
+
setAuthSessionResolved(true);
|
|
129
|
+
setAuthStateReadyRefs(false, true, false);
|
|
130
|
+
authSessionLastResultRef.current = false;
|
|
131
|
+
markBootstrapStalled('Unable to verify your session.');
|
|
132
|
+
return authSessionLastResultRef.current;
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
if (timeoutId !== null && typeof window !== 'undefined') {
|
|
136
|
+
window.clearTimeout(timeoutId);
|
|
137
|
+
}
|
|
138
|
+
if (!isStaleRequest()) {
|
|
139
|
+
authSessionLastCheckedAtRef.current = Date.now();
|
|
140
|
+
}
|
|
141
|
+
if (request && authSessionRequestRef.current === request) {
|
|
142
|
+
authSessionRequestRef.current = null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
})();
|
|
146
|
+
authSessionRequestRef.current = request;
|
|
147
|
+
return request;
|
|
148
|
+
}, [
|
|
149
|
+
runtimeConfigReady,
|
|
150
|
+
shouldProbeCloudAuth,
|
|
151
|
+
authSessionResolved,
|
|
152
|
+
resolveCloudAuthUrl,
|
|
153
|
+
authOnlyCloudMode,
|
|
154
|
+
authRequiredError,
|
|
155
|
+
runtimeMode,
|
|
156
|
+
workspaceSelectionScope,
|
|
157
|
+
markBootstrapPhase,
|
|
158
|
+
markBootstrapStalled,
|
|
159
|
+
setRuntimeMode,
|
|
160
|
+
setAuthRequiredForApi,
|
|
161
|
+
setIsAuthenticated,
|
|
162
|
+
setAuthUserId,
|
|
163
|
+
setAuthWorkspaceId,
|
|
164
|
+
setAuthUserEmail,
|
|
165
|
+
setAuthUserDisplayName,
|
|
166
|
+
setAuthUserAvatarUrl,
|
|
167
|
+
setUserGlobalSyncStatus,
|
|
168
|
+
setUserGlobalSyncError,
|
|
169
|
+
setHasBetaAccess,
|
|
170
|
+
setAvailableWorkspaces,
|
|
171
|
+
setHydratedWorkspaceRole,
|
|
172
|
+
setAssigneeOptions,
|
|
173
|
+
setAuthBlocked,
|
|
174
|
+
setAuthSessionResolved,
|
|
175
|
+
setError,
|
|
176
|
+
applyResolvedWorkspaceId,
|
|
177
|
+
readPersistedWorkspaceId,
|
|
178
|
+
authSessionRequestRef,
|
|
179
|
+
authSessionEpochRef,
|
|
180
|
+
authSessionLastCheckedAtRef,
|
|
181
|
+
authSessionLastResultRef,
|
|
182
|
+
isLoopbackHost,
|
|
183
|
+
setAuthStateReadyRefs
|
|
184
|
+
]);
|
|
185
|
+
const optimisticLocalAuthUserId = runtimeMode === 'local' && isLoopbackHost && shouldProbeCloudAuth
|
|
186
|
+
? readLocalMutationActorUserId()
|
|
187
|
+
: '';
|
|
188
|
+
useEffect(() => {
|
|
189
|
+
if (optimisticLocalAuthUserId) {
|
|
190
|
+
setIsAuthenticated(true);
|
|
191
|
+
setAuthSessionResolved(true);
|
|
192
|
+
setAuthUserId(optimisticLocalAuthUserId);
|
|
193
|
+
setAuthStateReadyRefs(true, true, false);
|
|
194
|
+
authSessionLastResultRef.current = true;
|
|
195
|
+
}
|
|
196
|
+
}, [optimisticLocalAuthUserId, setAuthSessionResolved, setAuthStateReadyRefs, setAuthUserId, setIsAuthenticated, authSessionLastResultRef]);
|
|
197
|
+
useEffect(() => {
|
|
198
|
+
if (runtimeMode !== 'local' || !shouldProbeCloudAuth)
|
|
199
|
+
return;
|
|
200
|
+
if (typeof window === 'undefined')
|
|
201
|
+
return;
|
|
202
|
+
const recheck = () => { void checkAuthSession(); };
|
|
203
|
+
const onVisibilityChange = () => {
|
|
204
|
+
if (document.visibilityState === 'visible')
|
|
205
|
+
recheck();
|
|
206
|
+
};
|
|
207
|
+
window.addEventListener('focus', recheck);
|
|
208
|
+
window.addEventListener('online', recheck);
|
|
209
|
+
document.addEventListener('visibilitychange', onVisibilityChange);
|
|
210
|
+
const intervalId = window.setInterval(recheck, 30_000);
|
|
211
|
+
return () => {
|
|
212
|
+
window.removeEventListener('focus', recheck);
|
|
213
|
+
window.removeEventListener('online', recheck);
|
|
214
|
+
document.removeEventListener('visibilitychange', onVisibilityChange);
|
|
215
|
+
window.clearInterval(intervalId);
|
|
216
|
+
};
|
|
217
|
+
}, [runtimeMode, shouldProbeCloudAuth, checkAuthSession]);
|
|
218
|
+
return { checkAuthSession };
|
|
219
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { WorkspaceSyncPhase } from '../../sync/workspaceSyncState';
|
|
2
|
+
interface CreateSyncAuthCoordinatorArgs {
|
|
3
|
+
currentWorkspaceId: string;
|
|
4
|
+
workspaceSyncPhase: WorkspaceSyncPhase;
|
|
5
|
+
clearWorkspaceRetry: () => void;
|
|
6
|
+
setWorkspaceSyncBusy: (value: boolean) => void;
|
|
7
|
+
setUserGlobalSyncStatus: (value: 'error') => void;
|
|
8
|
+
setUserGlobalSyncError: (value: string) => void;
|
|
9
|
+
setWorkspaceLastErrorMessage: (value: string) => void;
|
|
10
|
+
setWorkspaceLastErrorAt: (value: string) => void;
|
|
11
|
+
reportSyncEvent: (workspaceId: string, event: {
|
|
12
|
+
eventType: 'pull' | 'handshake' | 'bootstrap' | 'apply' | 'push';
|
|
13
|
+
status: 'success' | 'error';
|
|
14
|
+
statusCode?: number;
|
|
15
|
+
errorMessage?: string;
|
|
16
|
+
}) => void;
|
|
17
|
+
setAuthBlocked: (value: boolean) => void;
|
|
18
|
+
setIsAuthenticated: (value: boolean) => void;
|
|
19
|
+
checkAuthSession: () => Promise<boolean>;
|
|
20
|
+
ensureCloudWorkspaceReadyForSync: () => Promise<{
|
|
21
|
+
success: boolean;
|
|
22
|
+
statusCode?: number;
|
|
23
|
+
error?: string;
|
|
24
|
+
transient?: boolean;
|
|
25
|
+
retryAfterMs?: number;
|
|
26
|
+
}>;
|
|
27
|
+
scheduleWorkspaceSyncRetry: (minDelayMs?: number) => void;
|
|
28
|
+
persistWorkspaceSyncPatchSafely: (patch: {
|
|
29
|
+
phase: 'error';
|
|
30
|
+
lastErrorMessage: string;
|
|
31
|
+
}) => void;
|
|
32
|
+
}
|
|
33
|
+
export declare function createSyncAuthCoordinator({ currentWorkspaceId, workspaceSyncPhase, clearWorkspaceRetry, setWorkspaceSyncBusy, setUserGlobalSyncStatus, setUserGlobalSyncError, setWorkspaceLastErrorMessage, setWorkspaceLastErrorAt, reportSyncEvent, setAuthBlocked, setIsAuthenticated, checkAuthSession, ensureCloudWorkspaceReadyForSync, scheduleWorkspaceSyncRetry, persistWorkspaceSyncPatchSafely }: CreateSyncAuthCoordinatorArgs): {
|
|
34
|
+
handleSyncAuthFailure: (source: "push" | "pull" | "apply" | "handshake", statusCode: number) => Promise<void>;
|
|
35
|
+
ensureWorkspaceSyncReady: () => Promise<boolean>;
|
|
36
|
+
};
|
|
37
|
+
export {};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { isTransientWorkspaceSyncStatus } from '../../sync/syncService';
|
|
2
|
+
import { isValidWorkspaceKey } from './orchestratorShared';
|
|
3
|
+
export function createSyncAuthCoordinator({ currentWorkspaceId, workspaceSyncPhase, clearWorkspaceRetry, setWorkspaceSyncBusy, setUserGlobalSyncStatus, setUserGlobalSyncError, setWorkspaceLastErrorMessage, setWorkspaceLastErrorAt, reportSyncEvent, setAuthBlocked, setIsAuthenticated, checkAuthSession, ensureCloudWorkspaceReadyForSync, scheduleWorkspaceSyncRetry, persistWorkspaceSyncPatchSafely }) {
|
|
4
|
+
const handleSyncAuthFailure = async (source, statusCode) => {
|
|
5
|
+
clearWorkspaceRetry();
|
|
6
|
+
setWorkspaceSyncBusy(false);
|
|
7
|
+
setUserGlobalSyncStatus('error');
|
|
8
|
+
const message = 'Session expired. Sign in again to resume cloud sync.';
|
|
9
|
+
setUserGlobalSyncError(message);
|
|
10
|
+
setWorkspaceLastErrorMessage(message);
|
|
11
|
+
setWorkspaceLastErrorAt(new Date().toISOString());
|
|
12
|
+
reportSyncEvent(currentWorkspaceId, {
|
|
13
|
+
eventType: source === 'handshake' ? 'handshake' : source,
|
|
14
|
+
status: 'error',
|
|
15
|
+
statusCode,
|
|
16
|
+
errorMessage: message
|
|
17
|
+
});
|
|
18
|
+
setAuthBlocked(true);
|
|
19
|
+
setIsAuthenticated(false);
|
|
20
|
+
await checkAuthSession();
|
|
21
|
+
};
|
|
22
|
+
const ensureWorkspaceSyncReady = async () => {
|
|
23
|
+
if (!isValidWorkspaceKey(currentWorkspaceId)) {
|
|
24
|
+
const message = 'Workspace sync blocked: local workspace ID is unresolved.';
|
|
25
|
+
setWorkspaceLastErrorMessage(message);
|
|
26
|
+
setWorkspaceLastErrorAt(new Date().toISOString());
|
|
27
|
+
setUserGlobalSyncError(message);
|
|
28
|
+
setUserGlobalSyncStatus('error');
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
const ensured = await ensureCloudWorkspaceReadyForSync();
|
|
32
|
+
if (ensured.success)
|
|
33
|
+
return true;
|
|
34
|
+
if (ensured.statusCode === 401) {
|
|
35
|
+
await handleSyncAuthFailure('handshake', 401);
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
const message = ensured.error || 'Workspace sync handshake failed.';
|
|
39
|
+
setWorkspaceLastErrorMessage(message);
|
|
40
|
+
setWorkspaceLastErrorAt(new Date().toISOString());
|
|
41
|
+
setUserGlobalSyncError(message);
|
|
42
|
+
setUserGlobalSyncStatus('error');
|
|
43
|
+
reportSyncEvent(currentWorkspaceId, {
|
|
44
|
+
eventType: 'handshake',
|
|
45
|
+
status: 'error',
|
|
46
|
+
statusCode: ensured.statusCode,
|
|
47
|
+
errorMessage: message
|
|
48
|
+
});
|
|
49
|
+
if (ensured.transient || isTransientWorkspaceSyncStatus(ensured.statusCode)) {
|
|
50
|
+
scheduleWorkspaceSyncRetry(ensured.retryAfterMs);
|
|
51
|
+
}
|
|
52
|
+
else if (workspaceSyncPhase === 'active') {
|
|
53
|
+
persistWorkspaceSyncPatchSafely({ phase: 'error', lastErrorMessage: message });
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
};
|
|
57
|
+
return {
|
|
58
|
+
handleSyncAuthFailure,
|
|
59
|
+
ensureWorkspaceSyncReady
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { MutableRefObject } from 'react';
|
|
2
|
+
import type { WorkspaceSyncPhase } from '../../sync/workspaceSyncState';
|
|
3
|
+
interface BootstrapPhaseTrackingArgs {
|
|
4
|
+
workspaceSyncPhase: WorkspaceSyncPhase;
|
|
5
|
+
lastBootstrapPhaseRef: MutableRefObject<WorkspaceSyncPhase>;
|
|
6
|
+
bootstrapPhaseStartedAtRef: MutableRefObject<number | null>;
|
|
7
|
+
bootstrapLastProgressAtRef: MutableRefObject<number | null>;
|
|
8
|
+
}
|
|
9
|
+
export declare function syncBootstrapPhaseTracking({ workspaceSyncPhase, lastBootstrapPhaseRef, bootstrapPhaseStartedAtRef, bootstrapLastProgressAtRef }: BootstrapPhaseTrackingArgs): void;
|
|
10
|
+
interface BootstrapTimeoutMonitorArgs {
|
|
11
|
+
currentWorkspaceId: string;
|
|
12
|
+
workspaceCloudSyncEnabled: boolean;
|
|
13
|
+
workspaceSyncPhase: WorkspaceSyncPhase;
|
|
14
|
+
workspacePullInFlightRef: MutableRefObject<boolean>;
|
|
15
|
+
workspacePushInFlightRef: MutableRefObject<boolean>;
|
|
16
|
+
bootstrapPhaseStartedAtRef: MutableRefObject<number | null>;
|
|
17
|
+
bootstrapLastProgressAtRef: MutableRefObject<number | null>;
|
|
18
|
+
setWorkspaceLastErrorMessage: (value: string) => void;
|
|
19
|
+
setWorkspaceLastErrorAt: (value: string) => void;
|
|
20
|
+
setUserGlobalSyncStatus: (value: 'error') => void;
|
|
21
|
+
setUserGlobalSyncError: (value: string) => void;
|
|
22
|
+
setWorkspaceSyncBusy: (value: boolean) => void;
|
|
23
|
+
persistWorkspaceSyncPatchSafely: (patch: {
|
|
24
|
+
phase: 'error';
|
|
25
|
+
lastErrorMessage: string;
|
|
26
|
+
}) => void;
|
|
27
|
+
}
|
|
28
|
+
export declare function createBootstrapTimeoutMonitor({ currentWorkspaceId, workspaceCloudSyncEnabled, workspaceSyncPhase, workspacePullInFlightRef, workspacePushInFlightRef, bootstrapPhaseStartedAtRef, bootstrapLastProgressAtRef, setWorkspaceLastErrorMessage, setWorkspaceLastErrorAt, setUserGlobalSyncStatus, setUserGlobalSyncError, setWorkspaceSyncBusy, persistWorkspaceSyncPatchSafely }: BootstrapTimeoutMonitorArgs): (() => void) | undefined;
|
|
29
|
+
export declare function resolveRepairPhase(args: {
|
|
30
|
+
workspaceSyncPhase: WorkspaceSyncPhase;
|
|
31
|
+
lastBootstrapPhase: WorkspaceSyncPhase;
|
|
32
|
+
workspaceLastPullAt: string | null;
|
|
33
|
+
}): WorkspaceSyncPhase;
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { WORKSPACE_BOOTSTRAP_TIMEOUT_MS, logSyncDebug } from './orchestratorShared';
|
|
2
|
+
export function syncBootstrapPhaseTracking({ workspaceSyncPhase, lastBootstrapPhaseRef, bootstrapPhaseStartedAtRef, bootstrapLastProgressAtRef }) {
|
|
3
|
+
if (workspaceSyncPhase === 'attach-cloud' || workspaceSyncPhase === 'provision-local') {
|
|
4
|
+
lastBootstrapPhaseRef.current = workspaceSyncPhase;
|
|
5
|
+
if (bootstrapPhaseStartedAtRef.current === null) {
|
|
6
|
+
bootstrapPhaseStartedAtRef.current = Date.now();
|
|
7
|
+
}
|
|
8
|
+
bootstrapLastProgressAtRef.current = null;
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
bootstrapPhaseStartedAtRef.current = null;
|
|
12
|
+
bootstrapLastProgressAtRef.current = null;
|
|
13
|
+
}
|
|
14
|
+
export function createBootstrapTimeoutMonitor({ currentWorkspaceId, workspaceCloudSyncEnabled, workspaceSyncPhase, workspacePullInFlightRef, workspacePushInFlightRef, bootstrapPhaseStartedAtRef, bootstrapLastProgressAtRef, setWorkspaceLastErrorMessage, setWorkspaceLastErrorAt, setUserGlobalSyncStatus, setUserGlobalSyncError, setWorkspaceSyncBusy, persistWorkspaceSyncPatchSafely }) {
|
|
15
|
+
if (!workspaceCloudSyncEnabled)
|
|
16
|
+
return undefined;
|
|
17
|
+
if (workspaceSyncPhase !== 'attach-cloud' && workspaceSyncPhase !== 'provision-local')
|
|
18
|
+
return undefined;
|
|
19
|
+
const intervalId = window.setInterval(() => {
|
|
20
|
+
if (workspaceSyncPhase !== 'attach-cloud' && workspaceSyncPhase !== 'provision-local')
|
|
21
|
+
return;
|
|
22
|
+
if (workspacePullInFlightRef.current || workspacePushInFlightRef.current)
|
|
23
|
+
return;
|
|
24
|
+
const lastActivity = Math.max(bootstrapPhaseStartedAtRef.current ?? 0, bootstrapLastProgressAtRef.current ?? 0);
|
|
25
|
+
if (lastActivity > 0 && (Date.now() - lastActivity) < WORKSPACE_BOOTSTRAP_TIMEOUT_MS)
|
|
26
|
+
return;
|
|
27
|
+
const phaseLabel = workspaceSyncPhase === 'attach-cloud' ? 'initial cloud pull' : 'initial cloud upload';
|
|
28
|
+
const message = `Workspace sync ${phaseLabel} stalled. Press Repair to restart sync for this workspace.`;
|
|
29
|
+
setWorkspaceLastErrorMessage(message);
|
|
30
|
+
setWorkspaceLastErrorAt(new Date().toISOString());
|
|
31
|
+
setUserGlobalSyncStatus('error');
|
|
32
|
+
setUserGlobalSyncError(message);
|
|
33
|
+
setWorkspaceSyncBusy(false);
|
|
34
|
+
persistWorkspaceSyncPatchSafely({ phase: 'error', lastErrorMessage: message });
|
|
35
|
+
logSyncDebug('workspace_sync_bootstrap_timeout', {
|
|
36
|
+
workspaceId: currentWorkspaceId,
|
|
37
|
+
phase: workspaceSyncPhase
|
|
38
|
+
});
|
|
39
|
+
}, 10_000);
|
|
40
|
+
return () => window.clearInterval(intervalId);
|
|
41
|
+
}
|
|
42
|
+
export function resolveRepairPhase(args) {
|
|
43
|
+
const { workspaceSyncPhase, lastBootstrapPhase, workspaceLastPullAt } = args;
|
|
44
|
+
if (workspaceSyncPhase === 'provision-local')
|
|
45
|
+
return 'provision-local';
|
|
46
|
+
if (workspaceSyncPhase === 'attach-cloud')
|
|
47
|
+
return 'attach-cloud';
|
|
48
|
+
if (lastBootstrapPhase === 'provision-local' || lastBootstrapPhase === 'attach-cloud') {
|
|
49
|
+
return lastBootstrapPhase;
|
|
50
|
+
}
|
|
51
|
+
return workspaceLastPullAt ? 'attach-cloud' : 'provision-local';
|
|
52
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { WorkspaceTaxonomyState } from '../../shared/taxonomyState.js';
|
|
2
|
+
import type { WorkspaceSyncAiProfileSnapshot, WorkspaceSyncDocumentReviewCommentSnapshot, WorkspaceSyncDocumentReviewSessionSnapshot, WorkspaceSyncTaskEventSnapshot } from '../../sync/workspaceSyncModel';
|
|
3
|
+
import type { TaskItem } from '../../types';
|
|
4
|
+
export declare const USER_GLOBAL_SYNC_META_KEY = "taskforce.userGlobalSyncMeta.v1";
|
|
5
|
+
export declare const SYNC_DEBUG_LOG_KEY = "taskforce.syncDebug.v1";
|
|
6
|
+
export declare const WORKSPACE_BOOTSTRAP_TIMEOUT_MS = 120000;
|
|
7
|
+
export declare const WORKSPACE_SYNC_WATERMARKS_KEY_PREFIX = "taskforce.syncWatermarks.v3";
|
|
8
|
+
export declare const WORKSPACE_FULL_RECONCILE_INTERVAL_MS: number;
|
|
9
|
+
export declare const WORKSPACE_CONCURRENT_WINDOW_MESSAGE = "Another local window is already syncing this workspace. Wait a few seconds, or press Repair in that window.";
|
|
10
|
+
export declare function isValidWorkspaceKey(workspaceId: string): boolean;
|
|
11
|
+
export declare function logSyncDebug(event: string, details?: Record<string, unknown>): void;
|
|
12
|
+
export declare function normalizeWorkspaceSyncAiProfileSnapshotEntry(entry: any): WorkspaceSyncAiProfileSnapshot;
|
|
13
|
+
export declare function readUserGlobalSyncMeta(): Record<string, {
|
|
14
|
+
updatedAt: string;
|
|
15
|
+
}>;
|
|
16
|
+
export declare function writeUserGlobalSyncMeta(next: Record<string, {
|
|
17
|
+
updatedAt: string;
|
|
18
|
+
}>): void;
|
|
19
|
+
export declare function buildTaxonomyStateFingerprint(taxonomyState?: WorkspaceTaxonomyState): string;
|
|
20
|
+
export declare function flattenDocumentReviewComments(sessions: WorkspaceSyncDocumentReviewSessionSnapshot[]): WorkspaceSyncDocumentReviewCommentSnapshot[];
|
|
21
|
+
export declare function flattenTaskEvents(tasks: TaskItem[], archivedTasks: TaskItem[]): WorkspaceSyncTaskEventSnapshot[];
|
|
22
|
+
export interface PersistedSyncWatermarks {
|
|
23
|
+
taskWatermarks: Map<string, string>;
|
|
24
|
+
initiativeWatermarks: Map<string, string>;
|
|
25
|
+
workstreamWatermarks: Map<string, string>;
|
|
26
|
+
documentWatermarks: Map<string, string>;
|
|
27
|
+
assetWatermarks: Map<string, string>;
|
|
28
|
+
documentReviewSessionWatermarks: Map<string, string>;
|
|
29
|
+
documentReviewCommentWatermarks: Map<string, string>;
|
|
30
|
+
annotatedAttachmentSessionWatermarks: Map<string, string>;
|
|
31
|
+
taskEventWatermarks: Map<string, string>;
|
|
32
|
+
}
|
|
33
|
+
export declare function readPersistedSyncWatermarks(workspaceId: string, taxonomyStateFingerprint?: string): PersistedSyncWatermarks | null;
|
|
34
|
+
export declare function savePersistedSyncWatermarks(workspaceId: string, data: PersistedSyncWatermarks & {
|
|
35
|
+
taxonomyStateFingerprint?: string;
|
|
36
|
+
}): void;
|