codex-overleaf-link 2.5.0 → 2.6.0
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 +155 -392
- package/README.zh-CN.md +154 -393
- package/extension/bootstrap/background.js +52 -9
- package/extension/bootstrap/manifest.template.json +1 -1
- package/extension/bootstrap/popup.html +1 -0
- package/extension/bootstrap/popupFallback.js +55 -0
- package/extension/bootstrap/updateStatus.js +1 -0
- package/extension/src/background.js +283 -55
- package/extension/src/backgroundBootstrapMigration.js +181 -0
- package/extension/src/backgroundSharedSessions.js +129 -16
- package/extension/src/backgroundUpdateCoordinator.js +242 -29
- package/extension/src/backgroundUpdateSafety.js +173 -0
- package/extension/src/content/activeTurnControl.js +171 -8
- package/extension/src/content/applyResultFormatters.js +46 -73
- package/extension/src/content/assetTransferBroker.js +72 -14
- package/extension/src/content/composerAttachments.js +104 -47
- package/extension/src/content/composerPanel.js +110 -12
- package/extension/src/content/contentRuntime.js +1555 -926
- package/extension/src/content/contextTray.js +8 -2
- package/extension/src/content/contextUsageView.js +267 -0
- package/extension/src/content/diagnosticsController.js +122 -96
- package/extension/src/content/diagnosticsExport.js +19 -5
- package/extension/src/content/diagnosticsPanel.js +48 -0
- package/extension/src/content/directoryCleanupRecovery.js +54 -0
- package/extension/src/content/generated/content.bundle.js +11077 -3412
- package/extension/src/content/generated/content.bundle.meta.json +14 -3
- package/extension/src/content/globalPreferencesController.js +1 -1
- package/extension/src/content/legacyGlobalRegistry.js +10 -0
- package/extension/src/content/localSkillsPanel.js +104 -14
- package/extension/src/content/markdownDomRenderer.js +5 -3
- package/extension/src/content/markdownText.js +47 -11
- package/extension/src/content/modelPicker.js +8 -4
- package/extension/src/content/nativeCompatibilityController.js +74 -30
- package/extension/src/content/otWarmMirror.js +5 -6
- package/extension/src/content/otWarmMirrorController.js +17 -20
- package/extension/src/content/pageBridgeClient.js +17 -4
- package/extension/src/content/panelMaintenance.js +46 -14
- package/extension/src/content/panelRenderer.js +57 -27
- package/extension/src/content/pendingInputView.js +1 -1
- package/extension/src/content/projectProviderSelection.js +13 -4
- package/extension/src/content/projectSessionCleanup.js +109 -62
- package/extension/src/content/projectSettingsCoordinator.js +67 -6
- package/extension/src/content/providerSettingsCoordinator.js +46 -8
- package/extension/src/content/providerSettingsDialog.js +73 -14
- package/extension/src/content/recentProjects.js +15 -8
- package/extension/src/content/runActivitySummary.js +160 -18
- package/extension/src/content/runCompletionMeta.js +532 -0
- package/extension/src/content/runController.js +39 -15
- package/extension/src/content/runFailureNotice.js +29 -13
- package/extension/src/content/runGuidanceController.js +72 -27
- package/extension/src/content/runGuidanceView.js +59 -10
- package/extension/src/content/runPresence.js +18 -8
- package/extension/src/content/runQueueScheduler.js +157 -47
- package/extension/src/content/runRecoveryActions.js +147 -0
- package/extension/src/content/runResultActions.js +28 -143
- package/extension/src/content/runReviewController.js +633 -0
- package/extension/src/content/runScrollLayout.js +21 -2
- package/extension/src/content/runTimelineView.js +367 -175
- package/extension/src/content/scopedPersistencePanelState.js +134 -20
- package/extension/src/content/selectionContextView.js +1 -1
- package/extension/src/content/sensitivePreflight.js +153 -0
- package/extension/src/content/sessionForkController.js +80 -17
- package/extension/src/content/sessionManager.js +50 -4
- package/extension/src/content/sessionMenuView.js +54 -18
- package/extension/src/content/sessionPersistence.js +25 -5
- package/extension/src/content/settingsPanel.js +22 -5
- package/extension/src/content/settingsWorkbench.js +132 -89
- package/extension/src/content/sharedSessionViewSync.js +49 -11
- package/extension/src/content/subagentActivityView.js +23 -7
- package/extension/src/content/updateIdle.js +4 -1
- package/extension/src/content/updateNotice.js +118 -12
- package/extension/src/content/updateSafetyReceiver.js +58 -0
- package/extension/src/content/writebackController.js +53 -4
- package/extension/src/content/writebackOrchestrator.js +116 -94
- package/extension/src/content/writingStyleSettings.js +35 -11
- package/extension/src/page/binaryAssetUploader.js +136 -14
- package/extension/src/page/compileBridge.js +193 -180
- package/extension/src/page/fileMover.js +256 -0
- package/extension/src/page/overleafCapabilities.js +4 -1
- package/extension/src/page/ownedDirectoryCleanup.js +94 -0
- package/extension/src/page/referenceProjects.js +1 -0
- package/extension/src/page/reviewContext.js +113 -0
- package/extension/src/page/saveState.js +6 -4
- package/extension/src/page/snapshotRouter.js +84 -39
- package/extension/src/page/textCoordinates.js +17 -1
- package/extension/src/page/textFileCreator.js +114 -26
- package/extension/src/page/trackedChangeCapture.js +12 -2
- package/extension/src/page/trackedChangeOwnership.js +19 -1
- package/extension/src/page/trackedChangeReplay.js +41 -21
- package/extension/src/page/trackedChangesLifecycle.js +217 -199
- package/extension/src/page/treeOperations.js +7 -10
- package/extension/src/page/updateSafetyProbe.js +41 -0
- package/extension/src/page/writebackReceiptJournal.js +2 -0
- package/extension/src/page/writebackRouter.js +103 -155
- package/extension/src/pageBridge.js +126 -69
- package/extension/src/popup.js +62 -10
- package/extension/src/shared/agentTranscript.js +373 -17
- package/extension/src/shared/auditRecords.js +4 -0
- package/extension/src/shared/compatibility.js +22 -6
- package/extension/src/shared/compileAdapter.js +129 -66
- package/extension/src/shared/composerAttachmentDraft.js +156 -0
- package/extension/src/shared/contextUsage.js +125 -0
- package/extension/src/shared/directoryCleanupProof.js +43 -0
- package/extension/src/shared/failureReasons.js +349 -4
- package/extension/src/shared/globalPreferences.js +30 -13
- package/extension/src/shared/governanceRules.js +4 -37
- package/extension/src/shared/i18n.js +590 -24
- package/extension/src/shared/lineReferences.js +38 -31
- package/extension/src/shared/managedUpdateProjection.js +2 -2
- package/extension/src/shared/nativeRequestIdentity.js +1 -0
- package/extension/src/shared/pageRpcContract.js +5 -4
- package/extension/src/shared/panelIcons.js +39 -0
- package/extension/src/shared/projectFiles.js +21 -44
- package/extension/src/shared/providerProfiles.js +5 -3
- package/extension/src/shared/reviewing.js +130 -1
- package/extension/src/shared/runActivityModel.js +70 -4
- package/extension/src/shared/runExecutionSnapshot.js +13 -0
- package/extension/src/shared/runInputQueue.js +48 -5
- package/extension/src/shared/selectionContext.js +12 -3
- package/extension/src/shared/sessionState.js +132 -28
- package/extension/src/shared/sessionTitle.js +14 -1
- package/extension/src/shared/settlementFacts.js +10 -2
- package/extension/src/shared/sharedSessionBridge.js +118 -27
- package/extension/src/shared/sharedSessionTransfer.js +140 -0
- package/extension/src/shared/staleGuard.js +166 -52
- package/extension/src/shared/storageDb.js +90 -26
- package/extension/src/shared/storageMigration.js +78 -112
- package/extension/src/shared/storageRunActions.js +267 -30
- package/extension/src/shared/subagentActivityModel.js +1 -1
- package/extension/src/shared/undoOperations.js +253 -32
- package/extension/src/shared/updateConsent.js +6 -4
- package/extension/src/shared/updateRuntimeIdentity.js +17 -3
- package/extension/src/shared/writebackIntent.js +16 -0
- package/extension/src/shared/writebackOperationEvidence.js +4 -15
- package/extension/src/shared/writebackSettlement.js +58 -40
- package/extension/styles/panel.css +1123 -493
- package/native-host/src/anthropicBridgeRequest.js +4 -2
- package/native-host/src/anthropicBridgeResponse.js +6 -2
- package/native-host/src/anthropicMessagesBridge.js +24 -16
- package/native-host/src/bootstrapMigration.js +296 -0
- package/native-host/src/bootstrapMigrationContract.js +101 -0
- package/native-host/src/bootstrapUpdate.js +269 -0
- package/native-host/src/bootstrapUpdateWatchdog.js +44 -0
- package/native-host/src/chatBridgeRequest.js +60 -6
- package/native-host/src/chatBridgeResponse.js +20 -1
- package/native-host/src/chatCompletionsBridge.js +48 -15
- package/native-host/src/codexConfigPaths.js +106 -0
- package/native-host/src/codexHome.js +66 -73
- package/native-host/src/codexHomeScope.js +124 -0
- package/native-host/src/codexInheritedAuth.js +90 -0
- package/native-host/src/codexLocalSkills.js +301 -0
- package/native-host/src/codexPromptAssembly.js +114 -127
- package/native-host/src/codexProviderTest.js +24 -26
- package/native-host/src/codexSessionRunner.js +234 -198
- package/native-host/src/codexSessionTiming.js +18 -8
- package/native-host/src/codexSkillIsolation.js +170 -0
- package/native-host/src/codexThreadFork.js +19 -3
- package/native-host/src/index.js +19 -7
- package/native-host/src/managedInstall.js +110 -35
- package/native-host/src/managedInstallRecovery.js +140 -0
- package/native-host/src/managedRuntimeEvidence.js +57 -0
- package/native-host/src/mirrorWorkspace.js +92 -22
- package/native-host/src/nativeDoctor.js +15 -0
- package/native-host/src/nativeHelperLifecycle.js +21 -0
- package/native-host/src/nativeHostRegistration.js +33 -0
- package/native-host/src/nativeRunLifecycle.js +71 -0
- package/native-host/src/processStopBarrier.js +42 -7
- package/native-host/src/projectPathPlatform.js +30 -0
- package/native-host/src/providerContextError.js +20 -0
- package/native-host/src/providerHttp.js +80 -0
- package/native-host/src/providerProfile.js +4 -2
- package/native-host/src/providerReasoning.js +2 -7
- package/native-host/src/providerRuntime.js +1 -0
- package/native-host/src/providerStore.js +72 -14
- package/native-host/src/responsesPassthroughBridge.js +266 -52
- package/native-host/src/runtimeInstaller.js +21 -1
- package/native-host/src/selectionScope.js +36 -8
- package/native-host/src/subagentAdmission.js +26 -0
- package/native-host/src/subagentBroker.js +79 -66
- package/native-host/src/subagentTelemetry.js +19 -10
- package/native-host/src/subagentWorkspacePath.js +5 -2
- package/native-host/src/taskRunnerRuntime.js +92 -57
- package/native-host/src/textPatch.js +473 -377
- package/native-host/src/tomlStatements.js +140 -0
- package/native-host/src/updateManager.js +409 -46
- package/native-host/src/writingStyleRuntime.js +57 -29
- package/native-host/src/writingStyleStore.js +73 -0
- package/package.json +5 -8
- package/scripts/install-managed.mjs +1 -0
|
@@ -37,8 +37,11 @@
|
|
|
37
37
|
'codex-overleaf/consent-update-cancel',
|
|
38
38
|
'codex-overleaf/consent-update-recover',
|
|
39
39
|
'codex-overleaf/consent-update-dismiss',
|
|
40
|
-
'codex-overleaf/consent-update-reload'
|
|
40
|
+
'codex-overleaf/consent-update-reload',
|
|
41
|
+
'codex-overleaf/consent-update-rollback'
|
|
41
42
|
]);
|
|
43
|
+
const USER_ROLLBACK_REASON = 'update_user_rollback';
|
|
44
|
+
const RECOVERABLE_TRANSACTION_STATES = new Set(['awaiting_health', 'bootstrap_pending', 'bootstrap_rolling_back']);
|
|
42
45
|
|
|
43
46
|
const policy = root.CodexOverleafUpdateConsent;
|
|
44
47
|
const revocation = root.CodexOverleafUpdateRevocation;
|
|
@@ -98,6 +101,7 @@
|
|
|
98
101
|
}
|
|
99
102
|
|
|
100
103
|
async function startup() {
|
|
104
|
+
if (!await waitForBootstrapOwnership()) return;
|
|
101
105
|
chrome.alarms?.create?.(CHECK_ALARM, {
|
|
102
106
|
delayInMinutes: 0.5,
|
|
103
107
|
periodInMinutes: CHECK_INTERVAL_MINUTES
|
|
@@ -155,6 +159,8 @@
|
|
|
155
159
|
return dismissCompletedUpdate();
|
|
156
160
|
case 'codex-overleaf/consent-update-reload':
|
|
157
161
|
return reloadInstalledRuntime();
|
|
162
|
+
case 'codex-overleaf/consent-update-rollback':
|
|
163
|
+
return restorePreviousVersion();
|
|
158
164
|
default:
|
|
159
165
|
throw codedError('unknown_update_action', 'Unknown update action.');
|
|
160
166
|
}
|
|
@@ -170,40 +176,50 @@
|
|
|
170
176
|
operation.phase = 'checking';
|
|
171
177
|
await setUpdateState({ ...currentState, state: 'checking', operationId: operation.id,
|
|
172
178
|
cancelRequested: false, initiatedBy: manual ? 'manual' : 'automatic',
|
|
173
|
-
code: '', message: '', blocker: '', blockers: [], postponeUntil: LEGACY_GUARD }, { operation });
|
|
179
|
+
code: '', message: '', blocker: '', blockers: [], cooldownUntil: 0, postponeUntil: LEGACY_GUARD }, { operation });
|
|
174
180
|
try {
|
|
175
181
|
const result = await withTimeout(requestNative('update.check', {
|
|
176
|
-
operationId: operation.id, currentVersion: currentVersion(), etag: currentState.etag || ''
|
|
182
|
+
operationId: operation.id, currentVersion: currentVersion(), etag: currentState.etag || '', manual: manual === true
|
|
177
183
|
}), CHECK_REQUEST_TIMEOUT_MS, codedError('update_check_timeout', 'The managed update check did not complete in time.'));
|
|
178
184
|
assertOperation(operation);
|
|
179
185
|
const checkedAt = Date.now();
|
|
180
|
-
if (result.available
|
|
181
|
-
&& policy.compareStableVersions(currentState.latestVersion, currentVersion()) > 0)) {
|
|
186
|
+
if (result.available) {
|
|
182
187
|
const latestVersion = result.latestVersion || currentState.latestVersion;
|
|
183
188
|
await setConsentState({ ...consent,
|
|
184
|
-
...(latestVersion !== consent.snoozedVersion ? { snoozedVersion: '', snoozedUntil: 0 } : {}),
|
|
189
|
+
...(manual || latestVersion !== consent.snoozedVersion ? { snoozedVersion: '', snoozedUntil: 0 } : {}),
|
|
185
190
|
lastPromptedVersion: latestVersion,
|
|
186
191
|
lastPromptedAt: manual ? checkedAt : consent.lastPromptedAt
|
|
187
192
|
}, { operation });
|
|
188
193
|
await setUpdateState({ ...currentState, state: 'update_available', operationId: operation.id,
|
|
189
194
|
currentVersion: result.currentVersion || currentVersion(), latestVersion,
|
|
190
195
|
managed: true, etag: result.etag || currentState.etag || '', lastCheckedAt: checkedAt,
|
|
191
|
-
postponeUntil: LEGACY_GUARD, code: '', message: '', cancelRequested: false
|
|
196
|
+
postponeUntil: LEGACY_GUARD, code: '', message: '', cooldownUntil: 0, manualReason: {}, cancelRequested: false
|
|
192
197
|
}, { operation });
|
|
193
|
-
} else if (result.reason === 'manual_install_required'
|
|
194
|
-
&& policy.compareStableVersions(result.latestVersion, currentVersion()) > 0)
|
|
198
|
+
} else if ((result.reason === 'manual_install_required'
|
|
199
|
+
&& policy.compareStableVersions(result.latestVersion, currentVersion()) > 0)
|
|
200
|
+
|| (result.reason === 'prerelease_promotion_requires_manual_update'
|
|
201
|
+
&& result.latestVersion === (result.currentVersion || currentVersion()))) {
|
|
195
202
|
await setUpdateState({ ...currentState, state: 'manual_install_required', operationId: operation.id,
|
|
196
203
|
currentVersion: result.currentVersion || currentVersion(), latestVersion: result.latestVersion,
|
|
197
204
|
managed: true, etag: result.etag || currentState.etag || '', lastCheckedAt: checkedAt,
|
|
198
|
-
postponeUntil: LEGACY_GUARD, code:
|
|
205
|
+
postponeUntil: LEGACY_GUARD, code: result.reason === 'prerelease_promotion_requires_manual_update'
|
|
206
|
+
? result.reason : result.code === 'update_bootstrap_source_mismatch'
|
|
207
|
+
? result.code : 'update_bootstrap_upgrade_required', message: '', cooldownUntil: 0,
|
|
199
208
|
manualReason: result.manualInstall?.reason || {}, cancelRequested: false
|
|
200
209
|
}, { operation });
|
|
201
210
|
} else {
|
|
202
211
|
await setUpdateState({ ...currentState, state: 'idle', operationId: operation.id,
|
|
203
212
|
currentVersion: result.currentVersion || currentVersion(),
|
|
204
213
|
latestVersion: result.latestVersion || result.currentVersion || currentVersion(),
|
|
205
|
-
|
|
206
|
-
|
|
214
|
+
// A 304 without a verified Native candidate is a cache miss. Drop
|
|
215
|
+
// its validator so the next normal check can obtain fresh bytes.
|
|
216
|
+
// A skipped version also needs fresh bytes when checked explicitly.
|
|
217
|
+
etag: ['not_modified', 'cooldown'].includes(result.reason) ? '' : result.etag || currentState.etag || '', lastCheckedAt: checkedAt,
|
|
218
|
+
postponeUntil: LEGACY_GUARD, transactionId: '',
|
|
219
|
+
code: result.reason === 'cooldown'
|
|
220
|
+
? (result.cooldownReason === USER_ROLLBACK_REASON ? 'update_skipped_after_rollback' : 'update_cooldown') : '',
|
|
221
|
+
cooldownUntil: result.reason === 'cooldown' ? Number(result.cooldownUntil || 0) : 0,
|
|
222
|
+
manualReason: {}, message: '', cancelRequested: false
|
|
207
223
|
}, { operation });
|
|
208
224
|
}
|
|
209
225
|
return getView();
|
|
@@ -500,7 +516,7 @@
|
|
|
500
516
|
async function cancelUpdateCore() {
|
|
501
517
|
const [state, consent] = await Promise.all([getUpdateState(), getConsentState()]);
|
|
502
518
|
if (['applying', 'awaiting_health', 'rolling_back'].includes(state.state)
|
|
503
|
-
|| activeOperation?.phase
|
|
519
|
+
|| ['applying', 'rolling_back'].includes(activeOperation?.phase) || state.recoveryPending) {
|
|
504
520
|
throw codedError('update_revoke_too_late', 'The update is being installed or recovered.');
|
|
505
521
|
}
|
|
506
522
|
const operation = activeOperation;
|
|
@@ -555,8 +571,10 @@
|
|
|
555
571
|
|
|
556
572
|
|
|
557
573
|
function reconcileRecoveryState() {
|
|
574
|
+
if (activeOperation?.phase === 'rolling_back') return getUpdateState();
|
|
558
575
|
if (recoveryPromise) return recoveryPromise;
|
|
559
576
|
recoveryPromise = (async () => {
|
|
577
|
+
if (!await waitForBootstrapOwnership()) return getUpdateState();
|
|
560
578
|
const state = await getUpdateState();
|
|
561
579
|
if (state.recoveryPending) return reconcileExpiredPhase();
|
|
562
580
|
if (state.cancelRequested) return postponeUpdate();
|
|
@@ -584,7 +602,9 @@
|
|
|
584
602
|
|
|
585
603
|
|
|
586
604
|
async function reconcileExpiredPhase() {
|
|
587
|
-
const
|
|
605
|
+
const recoveryOwner = await captureRecoveryOwner();
|
|
606
|
+
const state = recoveryOwner.state;
|
|
607
|
+
const recoveryOptions = { observed: true, recoveryOwner };
|
|
588
608
|
if (!state.recoveryPending && (!state.deadlineAt || Date.now() <= state.deadlineAt
|
|
589
609
|
|| policy.isTerminalState(state.state))) return state;
|
|
590
610
|
if (['checking', 'downloading'].includes(state.state)) {
|
|
@@ -597,32 +617,38 @@
|
|
|
597
617
|
try {
|
|
598
618
|
let status = await withTimeout(requestNative('update.status'), 10000,
|
|
599
619
|
codedError('update_status_timeout', 'Update recovery status is unavailable.'));
|
|
620
|
+
await assertRecoveryOwner(recoveryOwner);
|
|
600
621
|
let transaction = status.transaction;
|
|
601
622
|
if (state.transactionId && transaction?.id !== state.transactionId) {
|
|
602
623
|
throw codedError('update_transaction_mismatch', 'The update recovery transaction could not be matched.');
|
|
603
624
|
}
|
|
625
|
+
if (transaction?.state === 'superseded') {
|
|
626
|
+
return settleSupersededTransaction(status, recoveryOwner);
|
|
627
|
+
}
|
|
604
628
|
if (transaction?.state === 'staged' || (!transaction && status.installedAligned
|
|
605
629
|
&& status.activeVersion === state.currentVersion)) {
|
|
606
630
|
if (state.operationId) await requestNative('update.cancel', { operationId: state.operationId });
|
|
631
|
+
await assertRecoveryOwner(recoveryOwner);
|
|
607
632
|
const consent = await getConsentState();
|
|
608
633
|
const authorizationId = consent.authorizationId || consent.revokingAuthorizationId;
|
|
609
634
|
const targetVersion = consent.authorizedVersion || consent.revokingVersion || state.latestVersion;
|
|
635
|
+
await assertRecoveryOwner(recoveryOwner);
|
|
610
636
|
if (authorizationId) await requestNative('update.revoke', { authorizationId, targetVersion });
|
|
611
|
-
await
|
|
612
|
-
authorizedVersion: '', authorizationId: '', authorizedAt: 0
|
|
613
|
-
}));
|
|
614
|
-
return setUpdateState({ state: 'failed', recoveryPending: false, cancelRequested: false,
|
|
637
|
+
const completed = await setUpdateAndConsentState({ state: 'failed', recoveryPending: false, cancelRequested: false,
|
|
615
638
|
operationId: '', code: 'update_not_applied', message: 'The update was stopped before installation. Retry when ready.'
|
|
616
|
-
}, {
|
|
639
|
+
}, revocation.clear({ ...consent, authorizedVersion: '', authorizationId: '', authorizedAt: 0 }), recoveryOptions);
|
|
640
|
+
return completed.updateState;
|
|
617
641
|
}
|
|
618
642
|
if (transaction?.state === 'awaiting_health') {
|
|
619
643
|
const rolledBack = await requestNative('update.rollback', {
|
|
620
644
|
transactionId: transaction.id, expectedState: 'awaiting_health', reasonCode: 'update_health_timeout'
|
|
621
645
|
});
|
|
646
|
+
await assertRecoveryOwner(recoveryOwner);
|
|
622
647
|
if (rolledBack.state !== 'rolled_back' || rolledBack.version !== transaction.sourceVersion) {
|
|
623
648
|
throw codedError('update_rollback_unconfirmed', 'Rollback returned incomplete evidence.');
|
|
624
649
|
}
|
|
625
650
|
status = await requestNative('update.status');
|
|
651
|
+
await assertRecoveryOwner(recoveryOwner);
|
|
626
652
|
transaction = status.transaction;
|
|
627
653
|
}
|
|
628
654
|
if (transaction?.state === 'committed' && status.installedAligned
|
|
@@ -630,30 +656,121 @@
|
|
|
630
656
|
return setUpdateState({ state: 'committed', currentVersion: transaction.targetVersion,
|
|
631
657
|
latestVersion: transaction.targetVersion, transactionId: transaction.id,
|
|
632
658
|
recoveryPending: false, cancelRequested: false, code: '', message: ''
|
|
633
|
-
},
|
|
659
|
+
}, recoveryOptions);
|
|
634
660
|
}
|
|
635
661
|
if (transaction?.state === 'rolled_back' && status.installedAligned
|
|
636
662
|
&& status.activeVersion === transaction.sourceVersion
|
|
637
663
|
&& (!state.transactionId || transaction.id === state.transactionId)) {
|
|
638
|
-
const
|
|
664
|
+
const userRestore = transaction.reasonCode === USER_ROLLBACK_REASON;
|
|
665
|
+
if ((userRestore || state.code === 'update_rollback_unconfirmed')
|
|
666
|
+
&& !root.CodexOverleafUpdateRuntimeIdentity?.verifiedRollback(status)) {
|
|
667
|
+
throw codedError('update_rollback_unconfirmed', 'Rollback returned incomplete evidence.');
|
|
668
|
+
}
|
|
669
|
+
if (userRestore) {
|
|
670
|
+
const pending = (await chrome.storage.local.get(MANUAL_RELOAD_TABS_KEY))?.[MANUAL_RELOAD_TABS_KEY];
|
|
671
|
+
const tabs = await chrome.tabs.query({ url: OVERLEAF_MATCHES });
|
|
672
|
+
await assertRecoveryOwner(recoveryOwner);
|
|
673
|
+
await chrome.storage.local.set({ [UPDATE_RELOAD_TABS_KEY]: [],
|
|
674
|
+
[MANUAL_RELOAD_TABS_KEY]: pending?.targetVersion === transaction.sourceVersion ? pending : {
|
|
675
|
+
targetVersion: transaction.sourceVersion, tabIds: tabs.filter(isUsableEditorTab).map(tab => tab.id)
|
|
676
|
+
} });
|
|
677
|
+
}
|
|
678
|
+
const value = { state: 'rolled_back', currentVersion: transaction.sourceVersion,
|
|
639
679
|
transactionId: transaction.id, recoveryPending: false, cancelRequested: false,
|
|
640
680
|
code: transaction.reasonCode || 'update_rolled_back', message: 'The previous version has been restored.'
|
|
641
|
-
}
|
|
681
|
+
};
|
|
682
|
+
const restored = userRestore
|
|
683
|
+
? (await setUpdateAndConsentState({ ...value, latestVersion: transaction.targetVersion,
|
|
684
|
+
postponeUntil: LEGACY_GUARD, etag: '', cooldownUntil: 0
|
|
685
|
+
}, policy.normalizeConsentState({ snoozedVersion: transaction.targetVersion,
|
|
686
|
+
snoozedUntil: Date.now() + SNOOZE_MS }), recoveryOptions)).updateState
|
|
687
|
+
: await setUpdateState(value, recoveryOptions);
|
|
688
|
+
runtimeStatus = null;
|
|
689
|
+
if (userRestore) {
|
|
690
|
+
try { await requireReloadSafePoint(); } catch (_error) { return restored; }
|
|
691
|
+
}
|
|
692
|
+
if (recoveryOwner.generation !== operationGeneration) return getUpdateState();
|
|
642
693
|
setTimeout(() => chrome.runtime.reload(), 0);
|
|
643
694
|
return restored;
|
|
644
695
|
}
|
|
645
696
|
throw codedError('update_recovery_pending', 'The installed update result has not been confirmed.');
|
|
646
697
|
} catch (error) {
|
|
698
|
+
if (error.code === 'update_recovery_superseded') return getUpdateState();
|
|
647
699
|
return setUpdateState({ state: 'failed', recoveryPending: true, cancelRequested: false,
|
|
648
700
|
code: safeCode(error), message: safeMessage(error)
|
|
649
|
-
},
|
|
701
|
+
}, recoveryOptions).catch(writeError => {
|
|
702
|
+
if (writeError.code === 'update_recovery_superseded') return getUpdateState();
|
|
703
|
+
throw writeError;
|
|
704
|
+
});
|
|
650
705
|
}
|
|
651
706
|
}
|
|
652
707
|
return state;
|
|
653
708
|
}
|
|
654
709
|
|
|
710
|
+
async function captureRecoveryOwner() {
|
|
711
|
+
const generation = operationGeneration;
|
|
712
|
+
const [state, consent] = await Promise.all([getUpdateState(), getConsentState()]);
|
|
713
|
+
return { generation, state, consent };
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function matchesRecoveryOwner(owner, state, consent) {
|
|
717
|
+
return owner && owner.generation === operationGeneration
|
|
718
|
+
&& ['state', 'transactionId', 'operationId', 'currentVersion', 'latestVersion',
|
|
719
|
+
'recoveryPending', 'cancelRequested', 'lastCheckedAt', 'phaseStartedAt']
|
|
720
|
+
.every(key => state[key] === owner.state[key])
|
|
721
|
+
&& ['authorizationId', 'authorizedVersion', 'revokingAuthorizationId', 'revokingVersion']
|
|
722
|
+
.every(key => consent[key] === owner.consent[key]);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
async function assertRecoveryOwner(owner) {
|
|
726
|
+
const [state, consent] = await Promise.all([getUpdateState(), getConsentState()]);
|
|
727
|
+
assertRecoverySnapshot(owner, state, consent);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function assertRecoverySnapshot(owner, state, consent) {
|
|
731
|
+
if (owner && !matchesRecoveryOwner(owner, state, consent)) {
|
|
732
|
+
throw codedError('update_recovery_superseded', 'A newer update state replaced this recovery request.');
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// Both startup recovery and the coordinator retire the same Native journal
|
|
737
|
+
// through this queue. The snapshot was captured before requesting status.
|
|
738
|
+
function settleSupersededTransaction(status, owner) {
|
|
739
|
+
return enqueueStateWrite(async () => {
|
|
740
|
+
const [previous, consent] = await Promise.all([getUpdateState(), getConsentState()]);
|
|
741
|
+
if (!matchesRecoveryOwner(owner, previous, consent)) return previous;
|
|
742
|
+
const transaction = status?.transaction;
|
|
743
|
+
const sameTargetReinstall = transaction?.reasonCode === 'update_reinstalled'
|
|
744
|
+
&& status.activeVersion === transaction.targetVersion
|
|
745
|
+
&& status.reinstalledPair?.extensionVersion === status.activeVersion
|
|
746
|
+
&& status.reinstalledPair?.nativeVersion === status.activeVersion
|
|
747
|
+
&& /^[0-9a-f]{64}$/.test(status.reinstalledPair.extensionSha256 || '')
|
|
748
|
+
&& /^[0-9a-f]{64}$/.test(status.reinstalledPair.nativeSha256 || '');
|
|
749
|
+
if (activeOperation || transaction?.state !== 'superseded' || !transaction.id
|
|
750
|
+
|| (previous.transactionId && previous.transactionId !== transaction.id)
|
|
751
|
+
|| status.installedAligned !== true || status.activeVersion !== transaction.supersededByVersion
|
|
752
|
+
|| (!sameTargetReinstall && policy.compareStableVersions(status.activeVersion, transaction.targetVersion) <= 0)
|
|
753
|
+
|| policy.compareStableVersions(status.activeVersion, transaction.sourceVersion) <= 0
|
|
754
|
+
|| ['authorized', 'bound'].includes(status.authorization?.state)
|
|
755
|
+
|| (consent.authorizedVersion && consent.authorizedVersion !== transaction.targetVersion)
|
|
756
|
+
|| (consent.revokingVersion && consent.revokingVersion !== transaction.targetVersion)) return previous;
|
|
757
|
+
const next = projectUpdateState(previous, { state: 'idle', currentVersion: status.activeVersion,
|
|
758
|
+
latestVersion: status.activeVersion, transactionId: '', operationId: '', recoveryPending: false,
|
|
759
|
+
cancelRequested: false, blocker: '', blockers: [], code: '', message: '', manualReason: {}, cooldownUntil: 0
|
|
760
|
+
}, { observed: true });
|
|
761
|
+
// Invalidate older awaits before yielding to storage, including recovery
|
|
762
|
+
// responses that describe this same retired transaction.
|
|
763
|
+
operationGeneration += 1;
|
|
764
|
+
await chrome.storage.local.set({ [UPDATE_STATE_KEY]: next,
|
|
765
|
+
[CONSENT_STATE_KEY]: policy.normalizeConsentState({}), [UPDATE_RELOAD_TABS_KEY]: [] });
|
|
766
|
+
runtimeStatus = null;
|
|
767
|
+
return next;
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
|
|
655
771
|
|
|
656
772
|
async function handleObservedStateChange() {
|
|
773
|
+
if (!await waitForBootstrapOwnership()) return;
|
|
657
774
|
await settleTerminalConsent();
|
|
658
775
|
const [state, consent] = await Promise.all([getUpdateState(), getConsentState()]);
|
|
659
776
|
if (!consent.authorizationId && !policy.isExecutionState(state.state)) {
|
|
@@ -682,9 +799,15 @@
|
|
|
682
799
|
now: Date.now()
|
|
683
800
|
});
|
|
684
801
|
view.runtime = runtime;
|
|
802
|
+
// Native decides availability: a committed pair with its source still on
|
|
803
|
+
// disk, or any unsettled installation that recovery must finish anyway.
|
|
804
|
+
view.restorePrevious = !state.cancelRequested && !activeOperation
|
|
805
|
+
&& (runtime.rollbackAvailable?.sourceVersion || (state.recoveryPending && state.transactionId))
|
|
806
|
+
? { version: runtime.rollbackAvailable?.sourceVersion || '' } : null;
|
|
807
|
+
view.actions = { ...view.actions, restorePrevious: Boolean(view.restorePrevious) };
|
|
685
808
|
const pending = (await chrome.storage.local.get(MANUAL_RELOAD_TABS_KEY))?.[MANUAL_RELOAD_TABS_KEY];
|
|
686
809
|
const pagesPending = runtime.state === 'aligned' && pending?.targetVersion === runtime.installedVersion && pending.tabIds?.length;
|
|
687
|
-
if (runtime.state === 'reload_required' || pagesPending) {
|
|
810
|
+
if (!state.recoveryPending && (runtime.state === 'reload_required' || pagesPending)) {
|
|
688
811
|
view.showPanel = true;
|
|
689
812
|
view.state = { ...state, state: pagesPending ? 'reload_tabs_required' : 'reload_required',
|
|
690
813
|
currentVersion: runtime.extensionVersion, latestVersion: runtime.installedVersion,
|
|
@@ -701,9 +824,9 @@
|
|
|
701
824
|
runtimeStatusRead = withTimeout(requestNative('update.status'), 4000,
|
|
702
825
|
codedError('update_status_timeout', 'Installed version inspection timed out.')).then(status => {
|
|
703
826
|
runtimeStatusAt = Date.now();
|
|
704
|
-
runtimeStatus = root.CodexOverleafUpdateRuntimeIdentity.inspectInstalledRuntime(status, {
|
|
827
|
+
runtimeStatus = { ...root.CodexOverleafUpdateRuntimeIdentity.inspectInstalledRuntime(status, {
|
|
705
828
|
extensionVersion: chrome.runtime.getManifest().version, runtimeVersion: currentVersion()
|
|
706
|
-
});
|
|
829
|
+
}), rollbackAvailable: status.rollbackAvailable || null };
|
|
707
830
|
return runtimeStatus;
|
|
708
831
|
}).catch(() => ({ state: 'unknown' })).finally(() => { runtimeStatusRead = null; });
|
|
709
832
|
return runtimeStatusRead;
|
|
@@ -725,6 +848,9 @@
|
|
|
725
848
|
}
|
|
726
849
|
|
|
727
850
|
async function reloadInstalledRuntime() {
|
|
851
|
+
if ((await getUpdateState()).recoveryPending) {
|
|
852
|
+
throw codedError('update_rollback_unconfirmed', 'Confirm update recovery before reloading.');
|
|
853
|
+
}
|
|
728
854
|
const runtime = await readRuntimeStatus(true);
|
|
729
855
|
if (runtime.state === 'aligned') { await finishManualRuntimeReload(); return getView(); }
|
|
730
856
|
if (runtime.state !== 'reload_required') throw codedError('update_reload_unavailable', 'No verified installed update is awaiting reload.');
|
|
@@ -736,7 +862,80 @@
|
|
|
736
862
|
return getView();
|
|
737
863
|
}
|
|
738
864
|
|
|
865
|
+
// Returns a committed update, or an update whose installation or recovery is
|
|
866
|
+
// unsettled, to the source pair Native still holds. Native owns the decision;
|
|
867
|
+
// the browser only supplies the transaction it observed and reloads after an
|
|
868
|
+
// exact readback.
|
|
869
|
+
async function restorePreviousVersion() {
|
|
870
|
+
if (activeOperation || cancellationPromise) {
|
|
871
|
+
throw codedError('update_transaction_in_progress', 'Another update operation is in progress.');
|
|
872
|
+
}
|
|
873
|
+
const operation = beginOperation();
|
|
874
|
+
operation.phase = 'rolling_back';
|
|
875
|
+
try {
|
|
876
|
+
const status = await requestNative('update.status');
|
|
877
|
+
assertOperation(operation);
|
|
878
|
+
const transaction = status.transaction;
|
|
879
|
+
const committed = transaction?.state === 'committed' && status.rollbackAvailable?.transactionId === transaction.id;
|
|
880
|
+
if (!transaction?.id || (!committed && !RECOVERABLE_TRANSACTION_STATES.has(transaction.state))) {
|
|
881
|
+
throw codedError('update_rollback_unavailable', 'No installed update can be restored to its previous version.');
|
|
882
|
+
}
|
|
883
|
+
// Page refresh happens after restoration, so it waits for the same saved
|
|
884
|
+
// and idle point as a reload of an installed update.
|
|
885
|
+
const tabs = await requireReloadSafePoint();
|
|
886
|
+
assertOperation(operation);
|
|
887
|
+
const generation = operation.generation;
|
|
888
|
+
clearIdleRetryTimer();
|
|
889
|
+
await setUpdateState({ state: 'rolling_back', transactionId: transaction.id,
|
|
890
|
+
currentVersion: transaction.targetVersion, latestVersion: transaction.targetVersion,
|
|
891
|
+
recoveryPending: false, cancelRequested: false, operationId: '', code: '', message: ''
|
|
892
|
+
}, { observed: true, generation });
|
|
893
|
+
try {
|
|
894
|
+
// Old source runtimes consume this key through their saved/idle check.
|
|
895
|
+
// The fixed Bootstrap's legacy list would refresh tabs unconditionally.
|
|
896
|
+
await chrome.storage.local.set({ [UPDATE_RELOAD_TABS_KEY]: [],
|
|
897
|
+
[MANUAL_RELOAD_TABS_KEY]: { targetVersion: transaction.sourceVersion, tabIds: tabs.map(tab => tab.id) } });
|
|
898
|
+
assertOperation(operation);
|
|
899
|
+
const rolledBack = await requestNative('update.rollback', { transactionId: transaction.id,
|
|
900
|
+
expectedState: transaction.state, reasonCode: USER_ROLLBACK_REASON });
|
|
901
|
+
assertOperation(operation);
|
|
902
|
+
const readback = await requestNative('update.status');
|
|
903
|
+
assertOperation(operation);
|
|
904
|
+
if (rolledBack.transactionId !== transaction.id || rolledBack.state !== 'rolled_back'
|
|
905
|
+
|| rolledBack.version !== transaction.sourceVersion || readback.transaction?.id !== transaction.id
|
|
906
|
+
|| readback.transaction.sourceVersion !== transaction.sourceVersion
|
|
907
|
+
|| readback.transaction.targetVersion !== transaction.targetVersion
|
|
908
|
+
|| !root.CodexOverleafUpdateRuntimeIdentity?.verifiedRollback({ ...readback,
|
|
909
|
+
transaction: { ...readback.transaction, bootstrapRevision: transaction.bootstrapRevision } })
|
|
910
|
+
|| !['extensionVersion', 'nativeVersion', 'extensionSha256', 'nativeSha256', 'bootstrapSha256']
|
|
911
|
+
.every(key => readback.restoredPair[key] === rolledBack.restoredPair?.[key])) {
|
|
912
|
+
throw codedError('update_rollback_unconfirmed', 'Rollback returned incomplete evidence.');
|
|
913
|
+
}
|
|
914
|
+
} catch (error) {
|
|
915
|
+
await setUpdateState({ state: 'failed', transactionId: transaction.id, recoveryPending: true,
|
|
916
|
+
cancelRequested: false, code: safeCode(error), message: safeMessage(error)
|
|
917
|
+
}, { observed: true, generation }).catch(() => {});
|
|
918
|
+
throw error;
|
|
919
|
+
}
|
|
920
|
+
await setUpdateAndConsentState({ state: 'rolled_back', transactionId: transaction.id,
|
|
921
|
+
currentVersion: transaction.sourceVersion, latestVersion: transaction.targetVersion,
|
|
922
|
+
recoveryPending: false, cancelRequested: false, blocker: '', blockers: [],
|
|
923
|
+
postponeUntil: LEGACY_GUARD, etag: '', cooldownUntil: 0,
|
|
924
|
+
code: USER_ROLLBACK_REASON, message: 'You restored the previous version.'
|
|
925
|
+
}, policy.normalizeConsentState({ snoozedVersion: transaction.targetVersion,
|
|
926
|
+
snoozedUntil: Date.now() + SNOOZE_MS }), { observed: true, generation });
|
|
927
|
+
runtimeStatus = null;
|
|
928
|
+
// The disk restore is complete even if new work now prevents a reload.
|
|
929
|
+
// Keep the existing Reload action available without attempting rollback again.
|
|
930
|
+
try { await requireReloadSafePoint(); } catch (_error) { return getView(); }
|
|
931
|
+
assertOperation(operation);
|
|
932
|
+
setTimeout(() => chrome.runtime.reload(), 0);
|
|
933
|
+
return getView();
|
|
934
|
+
} finally { finishOperation(operation); }
|
|
935
|
+
}
|
|
936
|
+
|
|
739
937
|
async function finishManualRuntimeReload() {
|
|
938
|
+
if ((await getUpdateState()).recoveryPending) return;
|
|
740
939
|
const pending = (await chrome.storage.local.get(MANUAL_RELOAD_TABS_KEY))?.[MANUAL_RELOAD_TABS_KEY];
|
|
741
940
|
if (!pending || !Array.isArray(pending.tabIds)) return;
|
|
742
941
|
const runtime = await readRuntimeStatus(true);
|
|
@@ -784,9 +983,11 @@
|
|
|
784
983
|
function setUpdateState(value, options = {}) {
|
|
785
984
|
return enqueueStateWrite(async () => {
|
|
786
985
|
assertWriteOwner(options);
|
|
787
|
-
const stored = await chrome.storage.local.get(
|
|
986
|
+
const stored = await chrome.storage.local.get(options.recoveryOwner
|
|
987
|
+
? [UPDATE_STATE_KEY, CONSENT_STATE_KEY] : UPDATE_STATE_KEY);
|
|
788
988
|
assertWriteOwner(options);
|
|
789
989
|
const previous = policy.normalizeUpdateState(stored?.[UPDATE_STATE_KEY], currentVersion());
|
|
990
|
+
assertRecoverySnapshot(options.recoveryOwner, previous, policy.normalizeConsentState(stored?.[CONSENT_STATE_KEY]));
|
|
790
991
|
const next = projectUpdateState(previous, value, options);
|
|
791
992
|
await chrome.storage.local.set({ [UPDATE_STATE_KEY]: next });
|
|
792
993
|
return next;
|
|
@@ -844,6 +1045,7 @@
|
|
|
844
1045
|
const stored = await chrome.storage.local.get([UPDATE_STATE_KEY, CONSENT_STATE_KEY]);
|
|
845
1046
|
assertWriteOwner(options);
|
|
846
1047
|
const previous = policy.normalizeUpdateState(stored?.[UPDATE_STATE_KEY], currentVersion());
|
|
1048
|
+
assertRecoverySnapshot(options.recoveryOwner, previous, policy.normalizeConsentState(stored?.[CONSENT_STATE_KEY]));
|
|
847
1049
|
const updateState = projectUpdateState(previous, updateValue, options);
|
|
848
1050
|
const consentState = policy.normalizeConsentState(consentValue);
|
|
849
1051
|
await chrome.storage.local.set({ [UPDATE_STATE_KEY]: updateState, [CONSENT_STATE_KEY]: consentState });
|
|
@@ -869,12 +1071,20 @@
|
|
|
869
1071
|
|
|
870
1072
|
function enqueuePolicyAction(action) {
|
|
871
1073
|
const generation = operationGeneration;
|
|
872
|
-
const guarded = () =>
|
|
1074
|
+
const guarded = async () => !await waitForBootstrapOwnership()
|
|
1075
|
+
|| generation !== operationGeneration || cancellationPromise ? getView() : action();
|
|
873
1076
|
const result = policyTail.then(guarded, guarded);
|
|
874
1077
|
policyTail = result.catch(() => {});
|
|
875
1078
|
return result;
|
|
876
1079
|
}
|
|
877
1080
|
|
|
1081
|
+
async function waitForBootstrapOwnership() {
|
|
1082
|
+
const migration = await root.CodexOverleafBootstrapMigration?.ready?.();
|
|
1083
|
+
if (migration?.continueInitialization === false) return false;
|
|
1084
|
+
await root.__CODEX_OVERLEAF_BOOTSTRAP_READY__;
|
|
1085
|
+
return true;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
878
1088
|
function beginOperation(id) {
|
|
879
1089
|
const operation = { id: id || crypto.randomUUID(), generation: ++operationGeneration, phase: 'checking' };
|
|
880
1090
|
activeOperation = operation;
|
|
@@ -894,6 +1104,9 @@
|
|
|
894
1104
|
}
|
|
895
1105
|
|
|
896
1106
|
function assertWriteOwner(options) {
|
|
1107
|
+
if (options.recoveryOwner && options.recoveryOwner.generation !== operationGeneration) {
|
|
1108
|
+
throw codedError('update_recovery_superseded', 'A newer update state replaced this recovery request.');
|
|
1109
|
+
}
|
|
897
1110
|
if (options.operation) assertOperation(options.operation);
|
|
898
1111
|
if (options.generation !== undefined && options.generation !== operationGeneration) {
|
|
899
1112
|
throw codedError('update_network_cancelled', 'The update request was superseded.');
|
|
@@ -970,5 +1183,5 @@
|
|
|
970
1183
|
.slice(0, 300);
|
|
971
1184
|
}
|
|
972
1185
|
|
|
973
|
-
root.CodexOverleafUpdateCoordinator = Object.freeze({ init });
|
|
1186
|
+
root.CodexOverleafUpdateCoordinator = Object.freeze({ init, captureRecoveryOwner, settleSupersededTransaction });
|
|
974
1187
|
})(globalThis);
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
(function installBackgroundUpdateSafety(root) {
|
|
2
|
+
'use strict';
|
|
3
|
+
const blocked = () => ({ idle: false, saved: false, blockers: ['tab_probe_unavailable'] });
|
|
4
|
+
// Chrome storage serializes object keys independently of insertion order.
|
|
5
|
+
// Preserve every key/value and array position when comparing readback.
|
|
6
|
+
function sameStored(left, right) {
|
|
7
|
+
if (left === right) return true;
|
|
8
|
+
if (!left || !right || typeof left !== 'object' || typeof right !== 'object') return false;
|
|
9
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
10
|
+
return Array.isArray(left) && Array.isArray(right) && left.length === right.length
|
|
11
|
+
&& left.every((value, index) => sameStored(value, right[index]));
|
|
12
|
+
}
|
|
13
|
+
const leftKeys = Object.keys(left), rightKeys = Object.keys(right);
|
|
14
|
+
return leftKeys.length === rightKeys.length
|
|
15
|
+
&& leftKeys.every(key => Object.prototype.hasOwnProperty.call(right, key) && sameStored(left[key], right[key]));
|
|
16
|
+
}
|
|
17
|
+
function create(options) {
|
|
18
|
+
const bindings = new Map();
|
|
19
|
+
const tabsKey = 'codex-overleaf-managed-update-tabs-v1';
|
|
20
|
+
const guardKey = 'codex-overleaf-managed-update-guarded-tabs-v1';
|
|
21
|
+
async function ownerMatches(binding) {
|
|
22
|
+
if (!options.isExecutionLocked()) return false;
|
|
23
|
+
const response = await options.request({ id: crypto.randomUUID(), method: 'update.status',
|
|
24
|
+
params: { bootstrapRevision: String(root.__CODEX_OVERLEAF_BOOTSTRAP_REVISION__ || '') } });
|
|
25
|
+
const tx = response?.ok && response.result?.transaction;
|
|
26
|
+
return tx?.id === binding.transactionId && tx.targetVersion === binding.targetVersion
|
|
27
|
+
&& tx.sourceVersion === binding.sourceVersion && tx.state === binding.expectedState;
|
|
28
|
+
}
|
|
29
|
+
async function snapshot(tabId, documentId) {
|
|
30
|
+
const results = await chrome.scripting.executeScript({ target: documentId ? { tabId, documentIds: [documentId] }
|
|
31
|
+
: { tabId, frameIds: [0] }, world: 'ISOLATED', func: () => ({ url: location.href }) });
|
|
32
|
+
const item = results?.length === 1 && results[0];
|
|
33
|
+
return item?.frameId === 0 && item.documentId && typeof item.result?.url === 'string'
|
|
34
|
+
? { documentId: item.documentId, url: item.result.url } : null;
|
|
35
|
+
}
|
|
36
|
+
async function saveProbe(message, sender) {
|
|
37
|
+
const binding = bindings.get(sender?.tab?.id);
|
|
38
|
+
if (!binding || sender.id !== chrome.runtime.id || sender.frameId !== 0 || sender.documentId !== binding.documentId
|
|
39
|
+
|| sender.url !== binding.url || message.token !== binding.token || message.transactionId !== binding.transactionId
|
|
40
|
+
|| message.nonce !== binding.nonce || !await ownerMatches(binding)) return { ok: false };
|
|
41
|
+
const before = await snapshot(binding.tabId, binding.documentId);
|
|
42
|
+
if (before?.url !== binding.url) return { ok: false };
|
|
43
|
+
const results = await chrome.scripting.executeScript({ target: { tabId: binding.tabId, documentIds: [binding.documentId] },
|
|
44
|
+
world: 'MAIN', func: url => globalThis.CodexOverleafUpdateSafetyProbe?.probe(url), args: [binding.url] });
|
|
45
|
+
const after = await snapshot(binding.tabId, binding.documentId);
|
|
46
|
+
if (after?.url !== binding.url || message.nonce !== binding.nonce || !await ownerMatches(binding)
|
|
47
|
+
|| results?.length !== 1 || results[0].documentId !== binding.documentId || results[0].frameId !== 0) return { ok: false };
|
|
48
|
+
return { ok: true, nonce: message.nonce, result: results[0].result };
|
|
49
|
+
}
|
|
50
|
+
chrome.runtime.onMessage.addListener((message, sender, respond) => {
|
|
51
|
+
if (message?.type !== 'codex-overleaf/migration-save-probe') return undefined;
|
|
52
|
+
saveProbe(message, sender).then(respond).catch(() => respond({ ok: false }));
|
|
53
|
+
return true;
|
|
54
|
+
});
|
|
55
|
+
async function probe(tab, owner) {
|
|
56
|
+
try {
|
|
57
|
+
const current = await snapshot(tab.id);
|
|
58
|
+
if (!current || current.url !== tab.url) return blocked();
|
|
59
|
+
let binding = bindings.get(tab.id);
|
|
60
|
+
if (binding && (binding.documentId !== current.documentId || binding.url !== current.url
|
|
61
|
+
|| binding.transactionId !== owner.transactionId)) return blocked();
|
|
62
|
+
if (!binding) {
|
|
63
|
+
binding = { ...owner, ...current, tabId: tab.id, token: crypto.randomUUID(), nonce: '' };
|
|
64
|
+
if (!await ownerMatches(binding)) return blocked();
|
|
65
|
+
bindings.set(tab.id, binding);
|
|
66
|
+
}
|
|
67
|
+
binding.expectedState = owner.expectedState;
|
|
68
|
+
if (!binding.installed) {
|
|
69
|
+
const target = { tabId: tab.id, documentIds: [binding.documentId] };
|
|
70
|
+
await chrome.scripting.executeScript({ target, world: 'MAIN',
|
|
71
|
+
files: ['runtime/src/page/saveState.js', 'runtime/src/page/updateSafetyProbe.js'] });
|
|
72
|
+
await chrome.scripting.executeScript({ target, world: 'ISOLATED',
|
|
73
|
+
files: ['runtime/src/shared/pageRpcContract.js', 'runtime/src/content/updateIdle.js', 'runtime/src/content/updateSafetyReceiver.js'] });
|
|
74
|
+
const installed = await chrome.scripting.executeScript({ target, world: 'ISOLATED',
|
|
75
|
+
func: value => globalThis.CodexOverleafUpdateSafetyReceiver?.install(value, {
|
|
76
|
+
UpdateIdle: globalThis.CodexOverleafUpdateIdle, PageRpcContract: globalThis.CodexOverleafPageRpcContract
|
|
77
|
+
}), args: [binding] });
|
|
78
|
+
if (installed?.length !== 1 || installed[0].documentId !== binding.documentId
|
|
79
|
+
|| installed[0].result?.ok !== true || installed[0].result.url !== binding.url) return blocked();
|
|
80
|
+
binding.installed = true;
|
|
81
|
+
}
|
|
82
|
+
if (!await ownerMatches(binding)) return blocked();
|
|
83
|
+
binding.nonce = crypto.randomUUID();
|
|
84
|
+
const nonce = binding.nonce;
|
|
85
|
+
let timer;
|
|
86
|
+
const response = await Promise.race([
|
|
87
|
+
chrome.tabs.sendMessage(tab.id, { type: 'codex-overleaf/migration-idle-probe', ...binding },
|
|
88
|
+
{ documentId: binding.documentId }),
|
|
89
|
+
new Promise(resolve => { timer = setTimeout(() => resolve(null), 3500); })
|
|
90
|
+
]).finally(() => clearTimeout(timer));
|
|
91
|
+
const after = await snapshot(tab.id, binding.documentId);
|
|
92
|
+
if (!response?.safetyOnly || response.token !== binding.token || response.transactionId !== binding.transactionId
|
|
93
|
+
|| response.targetVersion !== binding.targetVersion || response.nonce !== nonce || response.url !== binding.url
|
|
94
|
+
|| after?.url !== binding.url || !await ownerMatches(binding)) return blocked();
|
|
95
|
+
return response;
|
|
96
|
+
} catch (_) { return blocked(); }
|
|
97
|
+
}
|
|
98
|
+
async function protectReloadTabs(transaction) {
|
|
99
|
+
const owner = { transactionId: transaction.id, sourceVersion: transaction.sourceVersion,
|
|
100
|
+
targetVersion: transaction.targetVersion, expectedState: 'bootstrap_pending' };
|
|
101
|
+
if (!await ownerMatches(owner)) return false;
|
|
102
|
+
const stored = await chrome.storage.local.get([tabsKey, guardKey]);
|
|
103
|
+
if (stored[guardKey]?.transactionId === transaction.id && !stored[tabsKey]?.length) return true;
|
|
104
|
+
if (stored[guardKey]?.state === 'guarded' && stored[guardKey].transactionId !== transaction.id) {
|
|
105
|
+
const open = new Set((await chrome.tabs.query({})).map(tab => tab.id));
|
|
106
|
+
for (const entry of stored[guardKey].entries || []) {
|
|
107
|
+
if (!open.has(entry.tabId)) continue;
|
|
108
|
+
const current = await snapshot(entry.tabId).catch(() => null);
|
|
109
|
+
if (!entry.documentId || !current || current.documentId === entry.documentId) return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const entries = await Promise.all((stored[tabsKey] || []).map(async tabId => ({ tabId,
|
|
113
|
+
...await snapshot(tabId).catch(() => null) })));
|
|
114
|
+
const guard = { transactionId: transaction.id, sourceVersion: transaction.sourceVersion,
|
|
115
|
+
targetVersion: transaction.targetVersion, state: 'guarded', entries };
|
|
116
|
+
const current = await chrome.storage.local.get([tabsKey, guardKey]);
|
|
117
|
+
if (!sameStored(current, stored) || !await ownerMatches(owner)) return false;
|
|
118
|
+
await chrome.storage.local.set({ [guardKey]: guard, [tabsKey]: [] });
|
|
119
|
+
const readback = await chrome.storage.local.get([tabsKey, guardKey]);
|
|
120
|
+
return sameStored(readback[guardKey], guard) && readback[tabsKey]?.length === 0;
|
|
121
|
+
}
|
|
122
|
+
async function releaseReloadTabs(transaction, expectedState = 'bootstrap_pending') {
|
|
123
|
+
const owner = { transactionId: transaction.id, sourceVersion: transaction.sourceVersion,
|
|
124
|
+
targetVersion: transaction.targetVersion, expectedState };
|
|
125
|
+
if (!await ownerMatches(owner)) return false;
|
|
126
|
+
const stored = await chrome.storage.local.get([tabsKey, guardKey]);
|
|
127
|
+
const guard = stored[guardKey];
|
|
128
|
+
if (guard?.transactionId !== transaction.id || guard.state !== 'guarded' || stored[tabsKey]?.length) return false;
|
|
129
|
+
const open = new Set((await chrome.tabs.query({})).map(tab => tab.id));
|
|
130
|
+
const ids = [];
|
|
131
|
+
for (const entry of guard.entries) {
|
|
132
|
+
if (!open.has(entry.tabId)) continue;
|
|
133
|
+
if (!entry.documentId || !entry.url) return false;
|
|
134
|
+
const current = await snapshot(entry.tabId, entry.documentId).catch(() => null);
|
|
135
|
+
if (current?.documentId !== entry.documentId || current?.url !== entry.url) return false;
|
|
136
|
+
ids.push(entry.tabId);
|
|
137
|
+
}
|
|
138
|
+
const again = await chrome.storage.local.get([tabsKey, guardKey]);
|
|
139
|
+
if (!sameStored(again[guardKey], guard) || again[tabsKey]?.length || !await ownerMatches(owner)) return false;
|
|
140
|
+
await chrome.storage.local.set({ [tabsKey]: ids, [guardKey]: { ...guard, state: 'ready' } });
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
async function revalidate(tabs, owner) {
|
|
144
|
+
if (!await ownerMatches(owner)) return false;
|
|
145
|
+
for (const tab of tabs) {
|
|
146
|
+
const binding = bindings.get(tab.id);
|
|
147
|
+
if (!binding || binding.transactionId !== owner.transactionId) continue;
|
|
148
|
+
if ((await probe(tab, owner)).idle !== true) return false;
|
|
149
|
+
}
|
|
150
|
+
return ownerMatches(owner);
|
|
151
|
+
}
|
|
152
|
+
async function hasReplacedReloadDocument(transaction) {
|
|
153
|
+
const owner = { transactionId: transaction.id, sourceVersion: transaction.sourceVersion,
|
|
154
|
+
targetVersion: transaction.targetVersion, expectedState: 'rolled_back' };
|
|
155
|
+
if (!await ownerMatches(owner)) return false;
|
|
156
|
+
const stored = await chrome.storage.local.get([tabsKey, guardKey]);
|
|
157
|
+
const guard = stored[guardKey];
|
|
158
|
+
if (guard?.transactionId !== transaction.id || guard.state !== 'guarded' || stored[tabsKey]?.length) return false;
|
|
159
|
+
const open = new Set((await chrome.tabs.query({})).map(tab => tab.id));
|
|
160
|
+
for (const entry of guard.entries) {
|
|
161
|
+
if (!open.has(entry.tabId) || !entry.documentId) continue;
|
|
162
|
+
const current = await snapshot(entry.tabId).catch(() => null);
|
|
163
|
+
if (current && current.documentId !== entry.documentId) {
|
|
164
|
+
const again = await chrome.storage.local.get([tabsKey, guardKey]);
|
|
165
|
+
return sameStored(again, stored) && await ownerMatches(owner);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
return Object.freeze({ probe, protectReloadTabs, releaseReloadTabs, revalidate, hasReplacedReloadDocument });
|
|
171
|
+
}
|
|
172
|
+
root.CodexOverleafBackgroundUpdateSafety = Object.freeze({ create });
|
|
173
|
+
})(globalThis);
|