codex-overleaf-link 2.4.1 → 2.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (397) hide show
  1. package/README.md +275 -305
  2. package/README.zh-CN.md +290 -320
  3. package/extension/bootstrap/background.js +99 -19
  4. package/extension/bootstrap/manifest.template.json +9 -4
  5. package/extension/bootstrap/popup.html +11 -16
  6. package/extension/bootstrap/popupFallback.js +55 -0
  7. package/extension/bootstrap/update.js +25 -2
  8. package/extension/bootstrap/updateStatus.js +1 -0
  9. package/extension/runtime-manifest.json +3 -1
  10. package/extension/src/background.js +234 -56
  11. package/extension/src/backgroundBootstrapMigration.js +181 -0
  12. package/extension/src/backgroundSharedSessions.js +440 -0
  13. package/extension/src/backgroundUpdateCoordinator.js +541 -381
  14. package/extension/src/backgroundUpdateSafety.js +173 -0
  15. package/extension/src/content/activeTurnControl.js +16 -2
  16. package/extension/src/content/applyResultFormatters.js +46 -73
  17. package/extension/src/content/assetTransferBroker.js +244 -12
  18. package/extension/src/content/changeHistoryView.js +175 -0
  19. package/extension/src/content/composerAttachments.js +117 -67
  20. package/extension/src/content/composerPanel.js +41 -8
  21. package/extension/src/content/contentRuntime.js +1084 -1099
  22. package/extension/src/content/diagnosticsController.js +122 -97
  23. package/extension/src/content/diagnosticsExport.js +236 -0
  24. package/extension/src/content/diagnosticsPanel.js +78 -19
  25. package/extension/src/content/diffReviewPanel.js +8 -5
  26. package/extension/src/content/directoryCleanupRecovery.js +54 -0
  27. package/extension/src/content/generated/content.bundle.js +14680 -3606
  28. package/extension/src/content/generated/content.bundle.meta.json +30 -3
  29. package/extension/src/content/globalPreferencesController.js +1 -1
  30. package/extension/src/content/legacyGlobalRegistry.js +25 -1
  31. package/extension/src/content/localSkillsPanel.js +141 -16
  32. package/extension/src/content/markdownDomRenderer.js +9 -4
  33. package/extension/src/content/markdownText.js +5 -6
  34. package/extension/src/content/modelPicker.js +8 -5
  35. package/extension/src/content/nativeCompatibilityController.js +45 -20
  36. package/extension/src/content/otWarmMirror.js +4 -5
  37. package/extension/src/content/otWarmMirrorController.js +17 -20
  38. package/extension/src/content/pageBridgeClient.js +33 -5
  39. package/extension/src/content/panelMaintenance.js +87 -132
  40. package/extension/src/content/panelRenderer.js +10 -0
  41. package/extension/src/content/pendingInputView.js +109 -76
  42. package/extension/src/content/projectProviderSelection.js +13 -4
  43. package/extension/src/content/projectSessionCleanup.js +109 -62
  44. package/extension/src/content/projectSettingsCoordinator.js +39 -2
  45. package/extension/src/content/providerSettingsCoordinator.js +63 -14
  46. package/extension/src/content/providerSettingsDialog.js +724 -113
  47. package/extension/src/content/recentProjects.js +21 -6
  48. package/extension/src/content/runActivitySummary.js +665 -0
  49. package/extension/src/content/runCompletionMeta.js +344 -0
  50. package/extension/src/content/runController.js +40 -15
  51. package/extension/src/content/runFailureNotice.js +34 -12
  52. package/extension/src/content/runGuidanceController.js +76 -30
  53. package/extension/src/content/runGuidanceView.js +59 -10
  54. package/extension/src/content/runPresence.js +306 -0
  55. package/extension/src/content/runQueueScheduler.js +150 -43
  56. package/extension/src/content/runRecoveryActions.js +146 -0
  57. package/extension/src/content/runResultActions.js +11 -8
  58. package/extension/src/content/runReviewController.js +574 -0
  59. package/extension/src/content/runScrollLayout.js +21 -2
  60. package/extension/src/content/runTimelineView.js +315 -290
  61. package/extension/src/content/scopedPersistencePanelState.js +51 -6
  62. package/extension/src/content/selectionContextView.js +300 -0
  63. package/extension/src/content/sensitivePreflight.js +140 -0
  64. package/extension/src/content/sessionForkController.js +80 -17
  65. package/extension/src/content/sessionManager.js +1 -1
  66. package/extension/src/content/sessionPersistence.js +6 -1
  67. package/extension/src/content/settingsPanel.js +84 -55
  68. package/extension/src/content/settingsWorkbench.js +379 -0
  69. package/extension/src/content/sharedSessionViewSync.js +109 -0
  70. package/extension/src/content/subagentActivityView.js +195 -0
  71. package/extension/src/content/trackedChangeCaptureController.js +7 -3
  72. package/extension/src/content/updateIdle.js +4 -1
  73. package/extension/src/content/updateNotice.js +179 -56
  74. package/extension/src/content/updateSafetyReceiver.js +58 -0
  75. package/extension/src/content/writebackController.js +308 -2
  76. package/extension/src/content/writebackOrchestrator.js +213 -199
  77. package/extension/src/content/writebackPlan.js +231 -0
  78. package/extension/src/content/writebackRecovery.js +122 -0
  79. package/extension/src/content/writingStyleSettings.js +600 -0
  80. package/extension/src/content/writtenChangesView.js +152 -0
  81. package/extension/src/page/binaryAssetUploader.js +223 -23
  82. package/extension/src/page/fileMover.js +256 -0
  83. package/extension/src/page/overleafProjectSnapshot.js +1 -0
  84. package/extension/src/page/ownedDirectoryCleanup.js +94 -0
  85. package/extension/src/page/referenceProjects.js +149 -0
  86. package/extension/src/page/reviewContext.js +113 -0
  87. package/extension/src/page/saveState.js +20 -6
  88. package/extension/src/page/snapshotRouter.js +124 -48
  89. package/extension/src/page/textCoordinates.js +102 -0
  90. package/extension/src/page/textFileCreator.js +442 -71
  91. package/extension/src/page/trackedChangeCapture.js +53 -57
  92. package/extension/src/page/trackedChangeOwnership.js +216 -0
  93. package/extension/src/page/trackedChangeReplay.js +184 -0
  94. package/extension/src/page/trackedChangesLifecycle.js +343 -339
  95. package/extension/src/page/treeOperations.js +33 -12
  96. package/extension/src/page/updateSafetyProbe.js +41 -0
  97. package/extension/src/page/writebackReceiptJournal.js +114 -0
  98. package/extension/src/page/writebackRouter.js +242 -201
  99. package/extension/src/pageBridge.js +73 -102
  100. package/extension/src/popup.js +261 -65
  101. package/extension/src/shared/agentTranscript.js +41 -4
  102. package/extension/src/shared/auditRecords.js +73 -0
  103. package/extension/src/shared/changeHistoryModel.js +94 -0
  104. package/extension/src/shared/compatibility.js +22 -6
  105. package/extension/src/shared/compileAdapter.js +101 -37
  106. package/extension/src/shared/composerAttachmentDraft.js +151 -0
  107. package/extension/src/shared/directoryCleanupProof.js +43 -0
  108. package/extension/src/shared/failureReasons.js +329 -3
  109. package/extension/src/shared/globalPreferences.js +31 -14
  110. package/extension/src/shared/governanceRules.js +4 -37
  111. package/extension/src/shared/i18n.js +389 -25
  112. package/extension/src/shared/managedUpdateProjection.js +8 -5
  113. package/extension/src/shared/nativeRequestIdentity.js +1 -0
  114. package/extension/src/shared/pageRpcContract.js +9 -2
  115. package/extension/src/shared/projectFiles.js +21 -44
  116. package/extension/src/shared/providerProfiles.js +114 -3
  117. package/extension/src/shared/runActivityModel.js +361 -0
  118. package/extension/src/shared/runExecutionSnapshot.js +8 -2
  119. package/extension/src/shared/runExecutionSnapshotCodec.js +16 -4
  120. package/extension/src/shared/runInputQueue.js +48 -5
  121. package/extension/src/shared/scopedPersistenceBrowserAdapter.js +6 -3
  122. package/extension/src/shared/selectionContext.js +37 -0
  123. package/extension/src/shared/sessionState.js +166 -30
  124. package/extension/src/shared/settlementFacts.js +10 -2
  125. package/extension/src/shared/sharedSessionBridge.js +311 -0
  126. package/extension/src/shared/sharedSessionTransfer.js +140 -0
  127. package/extension/src/shared/staleGuard.js +118 -13
  128. package/extension/src/shared/storageDb.js +77 -78
  129. package/extension/src/shared/storageMigration.js +74 -111
  130. package/extension/src/shared/storageRunActions.js +211 -22
  131. package/extension/src/shared/storageValuePruning.js +40 -0
  132. package/extension/src/shared/subagentActivityModel.js +174 -0
  133. package/extension/src/shared/undoOperations.js +216 -6
  134. package/extension/src/shared/updateConsent.js +42 -11
  135. package/extension/src/shared/updateRuntimeIdentity.js +1 -1
  136. package/extension/src/shared/writebackIntent.js +78 -0
  137. package/extension/src/shared/writebackOperationEvidence.js +4 -15
  138. package/extension/src/shared/writebackSettlement.js +67 -48
  139. package/extension/src/shared/writingStyle.js +33 -0
  140. package/extension/styles/panel.css +1987 -385
  141. package/extension/styles/popup.css +110 -0
  142. package/native-host/src/anthropicBridgeRequest.js +6 -3
  143. package/native-host/src/anthropicBridgeResponse.js +6 -2
  144. package/native-host/src/anthropicMessagesBridge.js +24 -16
  145. package/native-host/src/bootstrapMigration.js +296 -0
  146. package/native-host/src/bootstrapMigrationContract.js +101 -0
  147. package/native-host/src/bootstrapUpdate.js +233 -0
  148. package/native-host/src/bootstrapUpdateWatchdog.js +44 -0
  149. package/native-host/src/chatBridgeResponse.js +10 -0
  150. package/native-host/src/chatCompletionsBridge.js +21 -15
  151. package/native-host/src/codexConfigPaths.js +106 -0
  152. package/native-host/src/codexHome.js +66 -73
  153. package/native-host/src/codexHomeScope.js +124 -0
  154. package/native-host/src/codexInheritedAuth.js +90 -0
  155. package/native-host/src/codexLocalSkills.js +301 -0
  156. package/native-host/src/codexModelRecovery.js +107 -0
  157. package/native-host/src/codexPromptAssembly.js +33 -12
  158. package/native-host/src/codexProviderLaunch.js +3 -3
  159. package/native-host/src/codexProviderTest.js +24 -26
  160. package/native-host/src/codexRuntimeIdentity.js +1 -0
  161. package/native-host/src/codexSessionRunner.js +211 -318
  162. package/native-host/src/codexSessionTiming.js +21 -8
  163. package/native-host/src/codexSkillIsolation.js +170 -0
  164. package/native-host/src/codexThreadFork.js +19 -3
  165. package/native-host/src/index.js +19 -7
  166. package/native-host/src/managedInstall.js +115 -39
  167. package/native-host/src/managedInstallRecovery.js +140 -0
  168. package/native-host/src/managedRuntimeEvidence.js +57 -0
  169. package/native-host/src/mirrorWorkspace.js +92 -22
  170. package/native-host/src/nativeDoctor.js +15 -0
  171. package/native-host/src/nativeEnvironment.js +70 -0
  172. package/native-host/src/nativeHelperLifecycle.js +21 -0
  173. package/native-host/src/nativeHostRegistration.js +33 -0
  174. package/native-host/src/nativeQuotas.js +15 -8
  175. package/native-host/src/nativeRunLifecycle.js +71 -0
  176. package/native-host/src/overleafWorkspaceRules.js +16 -0
  177. package/native-host/src/processStopBarrier.js +115 -0
  178. package/native-host/src/projectPathPlatform.js +30 -0
  179. package/native-host/src/providerHttp.js +80 -0
  180. package/native-host/src/providerModelCatalog.js +4 -3
  181. package/native-host/src/providerProfile.js +17 -3
  182. package/native-host/src/providerReasoning.js +10 -11
  183. package/native-host/src/providerRuntime.js +1 -0
  184. package/native-host/src/providerStore.js +76 -14
  185. package/native-host/src/releaseTransport.js +14 -5
  186. package/native-host/src/responsesPassthroughBridge.js +266 -52
  187. package/native-host/src/runtimeInstaller.js +21 -1
  188. package/native-host/src/selectionScope.js +86 -0
  189. package/native-host/src/skills/parallel-subagents/SKILL.md +31 -3
  190. package/native-host/src/skills/writing-style-builder/SKILL.md +146 -0
  191. package/native-host/src/subagentAdmission.js +26 -0
  192. package/native-host/src/subagentBroker.js +176 -159
  193. package/native-host/src/subagentTelemetry.js +325 -0
  194. package/native-host/src/subagentWorkspace.js +103 -0
  195. package/native-host/src/subagentWorkspacePath.js +34 -0
  196. package/native-host/src/taskRunnerRuntime.js +130 -57
  197. package/native-host/src/tomlStatements.js +140 -0
  198. package/native-host/src/turnAttachments.js +113 -0
  199. package/native-host/src/updateManager.js +406 -38
  200. package/native-host/src/updateNetworkControl.js +75 -0
  201. package/native-host/src/updateTrust.js +21 -2
  202. package/native-host/src/vendor/pdfjs/LICENSE +177 -0
  203. package/native-host/src/vendor/pdfjs/SOURCE.txt +3 -0
  204. package/native-host/src/vendor/pdfjs/cmaps/78-EUC-H.bcmap +0 -0
  205. package/native-host/src/vendor/pdfjs/cmaps/78-EUC-V.bcmap +0 -0
  206. package/native-host/src/vendor/pdfjs/cmaps/78-H.bcmap +0 -0
  207. package/native-host/src/vendor/pdfjs/cmaps/78-RKSJ-H.bcmap +0 -0
  208. package/native-host/src/vendor/pdfjs/cmaps/78-RKSJ-V.bcmap +0 -0
  209. package/native-host/src/vendor/pdfjs/cmaps/78-V.bcmap +0 -0
  210. package/native-host/src/vendor/pdfjs/cmaps/78ms-RKSJ-H.bcmap +0 -0
  211. package/native-host/src/vendor/pdfjs/cmaps/78ms-RKSJ-V.bcmap +0 -0
  212. package/native-host/src/vendor/pdfjs/cmaps/83pv-RKSJ-H.bcmap +0 -0
  213. package/native-host/src/vendor/pdfjs/cmaps/90ms-RKSJ-H.bcmap +0 -0
  214. package/native-host/src/vendor/pdfjs/cmaps/90ms-RKSJ-V.bcmap +0 -0
  215. package/native-host/src/vendor/pdfjs/cmaps/90msp-RKSJ-H.bcmap +0 -0
  216. package/native-host/src/vendor/pdfjs/cmaps/90msp-RKSJ-V.bcmap +0 -0
  217. package/native-host/src/vendor/pdfjs/cmaps/90pv-RKSJ-H.bcmap +0 -0
  218. package/native-host/src/vendor/pdfjs/cmaps/90pv-RKSJ-V.bcmap +0 -0
  219. package/native-host/src/vendor/pdfjs/cmaps/Add-H.bcmap +0 -0
  220. package/native-host/src/vendor/pdfjs/cmaps/Add-RKSJ-H.bcmap +0 -0
  221. package/native-host/src/vendor/pdfjs/cmaps/Add-RKSJ-V.bcmap +0 -0
  222. package/native-host/src/vendor/pdfjs/cmaps/Add-V.bcmap +0 -0
  223. package/native-host/src/vendor/pdfjs/cmaps/Adobe-CNS1-0.bcmap +0 -0
  224. package/native-host/src/vendor/pdfjs/cmaps/Adobe-CNS1-1.bcmap +0 -0
  225. package/native-host/src/vendor/pdfjs/cmaps/Adobe-CNS1-2.bcmap +0 -0
  226. package/native-host/src/vendor/pdfjs/cmaps/Adobe-CNS1-3.bcmap +0 -0
  227. package/native-host/src/vendor/pdfjs/cmaps/Adobe-CNS1-4.bcmap +0 -0
  228. package/native-host/src/vendor/pdfjs/cmaps/Adobe-CNS1-5.bcmap +0 -0
  229. package/native-host/src/vendor/pdfjs/cmaps/Adobe-CNS1-6.bcmap +0 -0
  230. package/native-host/src/vendor/pdfjs/cmaps/Adobe-CNS1-UCS2.bcmap +0 -0
  231. package/native-host/src/vendor/pdfjs/cmaps/Adobe-GB1-0.bcmap +0 -0
  232. package/native-host/src/vendor/pdfjs/cmaps/Adobe-GB1-1.bcmap +0 -0
  233. package/native-host/src/vendor/pdfjs/cmaps/Adobe-GB1-2.bcmap +0 -0
  234. package/native-host/src/vendor/pdfjs/cmaps/Adobe-GB1-3.bcmap +0 -0
  235. package/native-host/src/vendor/pdfjs/cmaps/Adobe-GB1-4.bcmap +0 -0
  236. package/native-host/src/vendor/pdfjs/cmaps/Adobe-GB1-5.bcmap +0 -0
  237. package/native-host/src/vendor/pdfjs/cmaps/Adobe-GB1-UCS2.bcmap +0 -0
  238. package/native-host/src/vendor/pdfjs/cmaps/Adobe-Japan1-0.bcmap +0 -0
  239. package/native-host/src/vendor/pdfjs/cmaps/Adobe-Japan1-1.bcmap +0 -0
  240. package/native-host/src/vendor/pdfjs/cmaps/Adobe-Japan1-2.bcmap +0 -0
  241. package/native-host/src/vendor/pdfjs/cmaps/Adobe-Japan1-3.bcmap +0 -0
  242. package/native-host/src/vendor/pdfjs/cmaps/Adobe-Japan1-4.bcmap +0 -0
  243. package/native-host/src/vendor/pdfjs/cmaps/Adobe-Japan1-5.bcmap +0 -0
  244. package/native-host/src/vendor/pdfjs/cmaps/Adobe-Japan1-6.bcmap +0 -0
  245. package/native-host/src/vendor/pdfjs/cmaps/Adobe-Japan1-UCS2.bcmap +0 -0
  246. package/native-host/src/vendor/pdfjs/cmaps/Adobe-Korea1-0.bcmap +0 -0
  247. package/native-host/src/vendor/pdfjs/cmaps/Adobe-Korea1-1.bcmap +0 -0
  248. package/native-host/src/vendor/pdfjs/cmaps/Adobe-Korea1-2.bcmap +0 -0
  249. package/native-host/src/vendor/pdfjs/cmaps/Adobe-Korea1-UCS2.bcmap +0 -0
  250. package/native-host/src/vendor/pdfjs/cmaps/B5-H.bcmap +0 -0
  251. package/native-host/src/vendor/pdfjs/cmaps/B5-V.bcmap +0 -0
  252. package/native-host/src/vendor/pdfjs/cmaps/B5pc-H.bcmap +0 -0
  253. package/native-host/src/vendor/pdfjs/cmaps/B5pc-V.bcmap +0 -0
  254. package/native-host/src/vendor/pdfjs/cmaps/CNS-EUC-H.bcmap +0 -0
  255. package/native-host/src/vendor/pdfjs/cmaps/CNS-EUC-V.bcmap +0 -0
  256. package/native-host/src/vendor/pdfjs/cmaps/CNS1-H.bcmap +0 -0
  257. package/native-host/src/vendor/pdfjs/cmaps/CNS1-V.bcmap +0 -0
  258. package/native-host/src/vendor/pdfjs/cmaps/CNS2-H.bcmap +0 -0
  259. package/native-host/src/vendor/pdfjs/cmaps/CNS2-V.bcmap +3 -0
  260. package/native-host/src/vendor/pdfjs/cmaps/ETHK-B5-H.bcmap +0 -0
  261. package/native-host/src/vendor/pdfjs/cmaps/ETHK-B5-V.bcmap +0 -0
  262. package/native-host/src/vendor/pdfjs/cmaps/ETen-B5-H.bcmap +0 -0
  263. package/native-host/src/vendor/pdfjs/cmaps/ETen-B5-V.bcmap +0 -0
  264. package/native-host/src/vendor/pdfjs/cmaps/ETenms-B5-H.bcmap +3 -0
  265. package/native-host/src/vendor/pdfjs/cmaps/ETenms-B5-V.bcmap +0 -0
  266. package/native-host/src/vendor/pdfjs/cmaps/EUC-H.bcmap +0 -0
  267. package/native-host/src/vendor/pdfjs/cmaps/EUC-V.bcmap +0 -0
  268. package/native-host/src/vendor/pdfjs/cmaps/Ext-H.bcmap +0 -0
  269. package/native-host/src/vendor/pdfjs/cmaps/Ext-RKSJ-H.bcmap +0 -0
  270. package/native-host/src/vendor/pdfjs/cmaps/Ext-RKSJ-V.bcmap +0 -0
  271. package/native-host/src/vendor/pdfjs/cmaps/Ext-V.bcmap +0 -0
  272. package/native-host/src/vendor/pdfjs/cmaps/GB-EUC-H.bcmap +0 -0
  273. package/native-host/src/vendor/pdfjs/cmaps/GB-EUC-V.bcmap +0 -0
  274. package/native-host/src/vendor/pdfjs/cmaps/GB-H.bcmap +4 -0
  275. package/native-host/src/vendor/pdfjs/cmaps/GB-V.bcmap +0 -0
  276. package/native-host/src/vendor/pdfjs/cmaps/GBK-EUC-H.bcmap +0 -0
  277. package/native-host/src/vendor/pdfjs/cmaps/GBK-EUC-V.bcmap +0 -0
  278. package/native-host/src/vendor/pdfjs/cmaps/GBK2K-H.bcmap +0 -0
  279. package/native-host/src/vendor/pdfjs/cmaps/GBK2K-V.bcmap +0 -0
  280. package/native-host/src/vendor/pdfjs/cmaps/GBKp-EUC-H.bcmap +0 -0
  281. package/native-host/src/vendor/pdfjs/cmaps/GBKp-EUC-V.bcmap +0 -0
  282. package/native-host/src/vendor/pdfjs/cmaps/GBT-EUC-H.bcmap +0 -0
  283. package/native-host/src/vendor/pdfjs/cmaps/GBT-EUC-V.bcmap +0 -0
  284. package/native-host/src/vendor/pdfjs/cmaps/GBT-H.bcmap +0 -0
  285. package/native-host/src/vendor/pdfjs/cmaps/GBT-V.bcmap +0 -0
  286. package/native-host/src/vendor/pdfjs/cmaps/GBTpc-EUC-H.bcmap +0 -0
  287. package/native-host/src/vendor/pdfjs/cmaps/GBTpc-EUC-V.bcmap +0 -0
  288. package/native-host/src/vendor/pdfjs/cmaps/GBpc-EUC-H.bcmap +0 -0
  289. package/native-host/src/vendor/pdfjs/cmaps/GBpc-EUC-V.bcmap +0 -0
  290. package/native-host/src/vendor/pdfjs/cmaps/H.bcmap +0 -0
  291. package/native-host/src/vendor/pdfjs/cmaps/HKdla-B5-H.bcmap +0 -0
  292. package/native-host/src/vendor/pdfjs/cmaps/HKdla-B5-V.bcmap +0 -0
  293. package/native-host/src/vendor/pdfjs/cmaps/HKdlb-B5-H.bcmap +0 -0
  294. package/native-host/src/vendor/pdfjs/cmaps/HKdlb-B5-V.bcmap +0 -0
  295. package/native-host/src/vendor/pdfjs/cmaps/HKgccs-B5-H.bcmap +0 -0
  296. package/native-host/src/vendor/pdfjs/cmaps/HKgccs-B5-V.bcmap +0 -0
  297. package/native-host/src/vendor/pdfjs/cmaps/HKm314-B5-H.bcmap +0 -0
  298. package/native-host/src/vendor/pdfjs/cmaps/HKm314-B5-V.bcmap +0 -0
  299. package/native-host/src/vendor/pdfjs/cmaps/HKm471-B5-H.bcmap +0 -0
  300. package/native-host/src/vendor/pdfjs/cmaps/HKm471-B5-V.bcmap +0 -0
  301. package/native-host/src/vendor/pdfjs/cmaps/HKscs-B5-H.bcmap +0 -0
  302. package/native-host/src/vendor/pdfjs/cmaps/HKscs-B5-V.bcmap +0 -0
  303. package/native-host/src/vendor/pdfjs/cmaps/Hankaku.bcmap +0 -0
  304. package/native-host/src/vendor/pdfjs/cmaps/Hiragana.bcmap +0 -0
  305. package/native-host/src/vendor/pdfjs/cmaps/KSC-EUC-H.bcmap +0 -0
  306. package/native-host/src/vendor/pdfjs/cmaps/KSC-EUC-V.bcmap +0 -0
  307. package/native-host/src/vendor/pdfjs/cmaps/KSC-H.bcmap +0 -0
  308. package/native-host/src/vendor/pdfjs/cmaps/KSC-Johab-H.bcmap +0 -0
  309. package/native-host/src/vendor/pdfjs/cmaps/KSC-Johab-V.bcmap +0 -0
  310. package/native-host/src/vendor/pdfjs/cmaps/KSC-V.bcmap +0 -0
  311. package/native-host/src/vendor/pdfjs/cmaps/KSCms-UHC-H.bcmap +0 -0
  312. package/native-host/src/vendor/pdfjs/cmaps/KSCms-UHC-HW-H.bcmap +0 -0
  313. package/native-host/src/vendor/pdfjs/cmaps/KSCms-UHC-HW-V.bcmap +0 -0
  314. package/native-host/src/vendor/pdfjs/cmaps/KSCms-UHC-V.bcmap +0 -0
  315. package/native-host/src/vendor/pdfjs/cmaps/KSCpc-EUC-H.bcmap +0 -0
  316. package/native-host/src/vendor/pdfjs/cmaps/KSCpc-EUC-V.bcmap +0 -0
  317. package/native-host/src/vendor/pdfjs/cmaps/Katakana.bcmap +0 -0
  318. package/native-host/src/vendor/pdfjs/cmaps/LICENSE +36 -0
  319. package/native-host/src/vendor/pdfjs/cmaps/NWP-H.bcmap +0 -0
  320. package/native-host/src/vendor/pdfjs/cmaps/NWP-V.bcmap +0 -0
  321. package/native-host/src/vendor/pdfjs/cmaps/RKSJ-H.bcmap +0 -0
  322. package/native-host/src/vendor/pdfjs/cmaps/RKSJ-V.bcmap +0 -0
  323. package/native-host/src/vendor/pdfjs/cmaps/Roman.bcmap +0 -0
  324. package/native-host/src/vendor/pdfjs/cmaps/UniCNS-UCS2-H.bcmap +0 -0
  325. package/native-host/src/vendor/pdfjs/cmaps/UniCNS-UCS2-V.bcmap +0 -0
  326. package/native-host/src/vendor/pdfjs/cmaps/UniCNS-UTF16-H.bcmap +0 -0
  327. package/native-host/src/vendor/pdfjs/cmaps/UniCNS-UTF16-V.bcmap +0 -0
  328. package/native-host/src/vendor/pdfjs/cmaps/UniCNS-UTF32-H.bcmap +0 -0
  329. package/native-host/src/vendor/pdfjs/cmaps/UniCNS-UTF32-V.bcmap +0 -0
  330. package/native-host/src/vendor/pdfjs/cmaps/UniCNS-UTF8-H.bcmap +0 -0
  331. package/native-host/src/vendor/pdfjs/cmaps/UniCNS-UTF8-V.bcmap +0 -0
  332. package/native-host/src/vendor/pdfjs/cmaps/UniGB-UCS2-H.bcmap +0 -0
  333. package/native-host/src/vendor/pdfjs/cmaps/UniGB-UCS2-V.bcmap +0 -0
  334. package/native-host/src/vendor/pdfjs/cmaps/UniGB-UTF16-H.bcmap +0 -0
  335. package/native-host/src/vendor/pdfjs/cmaps/UniGB-UTF16-V.bcmap +0 -0
  336. package/native-host/src/vendor/pdfjs/cmaps/UniGB-UTF32-H.bcmap +0 -0
  337. package/native-host/src/vendor/pdfjs/cmaps/UniGB-UTF32-V.bcmap +0 -0
  338. package/native-host/src/vendor/pdfjs/cmaps/UniGB-UTF8-H.bcmap +0 -0
  339. package/native-host/src/vendor/pdfjs/cmaps/UniGB-UTF8-V.bcmap +0 -0
  340. package/native-host/src/vendor/pdfjs/cmaps/UniJIS-UCS2-H.bcmap +0 -0
  341. package/native-host/src/vendor/pdfjs/cmaps/UniJIS-UCS2-HW-H.bcmap +0 -0
  342. package/native-host/src/vendor/pdfjs/cmaps/UniJIS-UCS2-HW-V.bcmap +0 -0
  343. package/native-host/src/vendor/pdfjs/cmaps/UniJIS-UCS2-V.bcmap +0 -0
  344. package/native-host/src/vendor/pdfjs/cmaps/UniJIS-UTF16-H.bcmap +0 -0
  345. package/native-host/src/vendor/pdfjs/cmaps/UniJIS-UTF16-V.bcmap +0 -0
  346. package/native-host/src/vendor/pdfjs/cmaps/UniJIS-UTF32-H.bcmap +0 -0
  347. package/native-host/src/vendor/pdfjs/cmaps/UniJIS-UTF32-V.bcmap +0 -0
  348. package/native-host/src/vendor/pdfjs/cmaps/UniJIS-UTF8-H.bcmap +0 -0
  349. package/native-host/src/vendor/pdfjs/cmaps/UniJIS-UTF8-V.bcmap +0 -0
  350. package/native-host/src/vendor/pdfjs/cmaps/UniJIS2004-UTF16-H.bcmap +0 -0
  351. package/native-host/src/vendor/pdfjs/cmaps/UniJIS2004-UTF16-V.bcmap +0 -0
  352. package/native-host/src/vendor/pdfjs/cmaps/UniJIS2004-UTF32-H.bcmap +0 -0
  353. package/native-host/src/vendor/pdfjs/cmaps/UniJIS2004-UTF32-V.bcmap +0 -0
  354. package/native-host/src/vendor/pdfjs/cmaps/UniJIS2004-UTF8-H.bcmap +0 -0
  355. package/native-host/src/vendor/pdfjs/cmaps/UniJIS2004-UTF8-V.bcmap +0 -0
  356. package/native-host/src/vendor/pdfjs/cmaps/UniJISPro-UCS2-HW-V.bcmap +0 -0
  357. package/native-host/src/vendor/pdfjs/cmaps/UniJISPro-UCS2-V.bcmap +0 -0
  358. package/native-host/src/vendor/pdfjs/cmaps/UniJISPro-UTF8-V.bcmap +0 -0
  359. package/native-host/src/vendor/pdfjs/cmaps/UniJISX0213-UTF32-H.bcmap +0 -0
  360. package/native-host/src/vendor/pdfjs/cmaps/UniJISX0213-UTF32-V.bcmap +0 -0
  361. package/native-host/src/vendor/pdfjs/cmaps/UniJISX02132004-UTF32-H.bcmap +0 -0
  362. package/native-host/src/vendor/pdfjs/cmaps/UniJISX02132004-UTF32-V.bcmap +0 -0
  363. package/native-host/src/vendor/pdfjs/cmaps/UniKS-UCS2-H.bcmap +0 -0
  364. package/native-host/src/vendor/pdfjs/cmaps/UniKS-UCS2-V.bcmap +0 -0
  365. package/native-host/src/vendor/pdfjs/cmaps/UniKS-UTF16-H.bcmap +0 -0
  366. package/native-host/src/vendor/pdfjs/cmaps/UniKS-UTF16-V.bcmap +0 -0
  367. package/native-host/src/vendor/pdfjs/cmaps/UniKS-UTF32-H.bcmap +0 -0
  368. package/native-host/src/vendor/pdfjs/cmaps/UniKS-UTF32-V.bcmap +0 -0
  369. package/native-host/src/vendor/pdfjs/cmaps/UniKS-UTF8-H.bcmap +0 -0
  370. package/native-host/src/vendor/pdfjs/cmaps/UniKS-UTF8-V.bcmap +0 -0
  371. package/native-host/src/vendor/pdfjs/cmaps/V.bcmap +0 -0
  372. package/native-host/src/vendor/pdfjs/cmaps/WP-Symbol.bcmap +0 -0
  373. package/native-host/src/vendor/pdfjs/pdf.mjs +26740 -0
  374. package/native-host/src/vendor/pdfjs/pdf.worker.mjs +62201 -0
  375. package/native-host/src/vendor/pdfjs/standard_fonts/FoxitDingbats.pfb +0 -0
  376. package/native-host/src/vendor/pdfjs/standard_fonts/FoxitFixed.pfb +0 -0
  377. package/native-host/src/vendor/pdfjs/standard_fonts/FoxitFixedBold.pfb +0 -0
  378. package/native-host/src/vendor/pdfjs/standard_fonts/FoxitFixedBoldItalic.pfb +0 -0
  379. package/native-host/src/vendor/pdfjs/standard_fonts/FoxitFixedItalic.pfb +0 -0
  380. package/native-host/src/vendor/pdfjs/standard_fonts/FoxitSerif.pfb +0 -0
  381. package/native-host/src/vendor/pdfjs/standard_fonts/FoxitSerifBold.pfb +0 -0
  382. package/native-host/src/vendor/pdfjs/standard_fonts/FoxitSerifBoldItalic.pfb +0 -0
  383. package/native-host/src/vendor/pdfjs/standard_fonts/FoxitSerifItalic.pfb +0 -0
  384. package/native-host/src/vendor/pdfjs/standard_fonts/FoxitSymbol.pfb +0 -0
  385. package/native-host/src/vendor/pdfjs/standard_fonts/LICENSE_FOXIT +27 -0
  386. package/native-host/src/vendor/pdfjs/standard_fonts/LICENSE_LIBERATION +102 -0
  387. package/native-host/src/vendor/pdfjs/standard_fonts/LiberationSans-Bold.ttf +0 -0
  388. package/native-host/src/vendor/pdfjs/standard_fonts/LiberationSans-BoldItalic.ttf +0 -0
  389. package/native-host/src/vendor/pdfjs/standard_fonts/LiberationSans-Italic.ttf +0 -0
  390. package/native-host/src/vendor/pdfjs/standard_fonts/LiberationSans-Regular.ttf +0 -0
  391. package/native-host/src/writingStyleEvidence.js +102 -0
  392. package/native-host/src/writingStylePdfWorker.js +35 -0
  393. package/native-host/src/writingStyleProfile.js +165 -0
  394. package/native-host/src/writingStyleRuntime.js +475 -0
  395. package/native-host/src/writingStyleStore.js +73 -0
  396. package/package.json +1 -1
  397. package/scripts/install-managed.mjs +1 -0
@@ -24,14 +24,18 @@
24
24
  const OVERLEAF_MATCHES = [
25
25
  'https://www.overleaf.com/project',
26
26
  'https://overleaf.com/project',
27
+ 'https://cn.overleaf.com/project',
27
28
  'https://www.overleaf.com/project/*',
28
- 'https://overleaf.com/project/*'
29
+ 'https://overleaf.com/project/*',
30
+ 'https://cn.overleaf.com/project/*'
29
31
  ];
30
32
  const MESSAGE_TYPES = new Set([
31
33
  'codex-overleaf/consent-update-get-state',
32
34
  'codex-overleaf/consent-update-check',
33
35
  'codex-overleaf/consent-update-install',
34
36
  'codex-overleaf/consent-update-later',
37
+ 'codex-overleaf/consent-update-cancel',
38
+ 'codex-overleaf/consent-update-recover',
35
39
  'codex-overleaf/consent-update-dismiss',
36
40
  'codex-overleaf/consent-update-reload'
37
41
  ]);
@@ -47,6 +51,11 @@
47
51
  let runtimeStatusAt = 0;
48
52
  let runtimeStatusRead = null;
49
53
  let runtimeReloadMessage = '';
54
+ let operationGeneration = 0;
55
+ let activeOperation = null;
56
+ let cancellationPromise = null;
57
+ let recoveryPromise = null;
58
+ let stateWriteTail = Promise.resolve();
50
59
 
51
60
  function init(options = {}) {
52
61
  if (initialized || !policy || !revocation) return;
@@ -59,7 +68,10 @@
59
68
  sendResponse({ ok: false, error: { code: 'forbidden_sender', message: 'Update actions are limited to this extension and Overleaf project tabs.' } });
60
69
  return false;
61
70
  }
62
- enqueuePolicyAction(() => handleMessage(message))
71
+ const direct = ['codex-overleaf/consent-update-get-state',
72
+ 'codex-overleaf/consent-update-cancel', 'codex-overleaf/consent-update-later',
73
+ 'codex-overleaf/consent-update-recover'].includes(message.type);
74
+ (direct ? handleMessage(message) : enqueuePolicyAction(() => handleMessage(message)))
63
75
  .then(result => sendResponse({ ok: true, result }))
64
76
  .catch(error => sendResponse({ ok: false, error: safeError(error) }));
65
77
  return true;
@@ -70,7 +82,7 @@
70
82
  void enqueuePolicyAction(() => checkOnly({ manual: false })).catch(() => {});
71
83
  }
72
84
  if (alarm?.name === WATCHDOG_ALARM) {
73
- void enqueuePolicyAction(() => reconcileRecoveryState()).catch(() => {});
85
+ void reconcileRecoveryState().catch(() => {});
74
86
  }
75
87
  if (alarm?.name === IDLE_ALARM) {
76
88
  void enqueuePolicyAction(() => tryActivateStagedUpdate()).catch(() => {});
@@ -86,6 +98,7 @@
86
98
  }
87
99
 
88
100
  async function startup() {
101
+ if (!await waitForBootstrapOwnership()) return;
89
102
  chrome.alarms?.create?.(CHECK_ALARM, {
90
103
  delayInMinutes: 0.5,
91
104
  periodInMinutes: CHECK_INTERVAL_MINUTES
@@ -128,11 +141,16 @@
128
141
  switch (message.type) {
129
142
  case 'codex-overleaf/consent-update-get-state':
130
143
  return getView();
144
+ case 'codex-overleaf/consent-update-recover':
145
+ await reconcileRecoveryState();
146
+ return getView();
131
147
  case 'codex-overleaf/consent-update-check':
132
148
  return checkOnly({ manual: true });
133
149
  case 'codex-overleaf/consent-update-install':
134
150
  return installUpdate();
135
151
  case 'codex-overleaf/consent-update-later':
152
+ return snoozeManualInstall().then(view => view || postponeUpdate());
153
+ case 'codex-overleaf/consent-update-cancel':
136
154
  return postponeUpdate();
137
155
  case 'codex-overleaf/consent-update-dismiss':
138
156
  return dismissCompletedUpdate();
@@ -143,106 +161,90 @@
143
161
  }
144
162
  }
145
163
 
146
- async function checkOnly({ manual }) {
147
- const [currentState, consent] = await Promise.all([getUpdateState(), getConsentState()]);
148
- if (consent.authorizedVersion || policy.isExecutionState(currentState.state)) {
149
- return getView();
150
- }
151
- await setUpdateState({
152
- ...currentState,
153
- state: 'checking',
154
- initiatedBy: manual ? 'manual' : 'automatic',
155
- code: '',
156
- message: '',
157
- blocker: '',
158
- blockers: []
159
- });
160
-
164
+ async function checkOnly({ manual }, inheritedOperation) {
165
+ const operation = inheritedOperation || beginOperation();
161
166
  try {
162
- const result = await withTimeout(
163
- requestNative('update.check', {
164
- currentVersion: currentVersion(),
165
- etag: currentState.etag || ''
166
- }),
167
- CHECK_REQUEST_TIMEOUT_MS,
168
- codedError('update_check_timeout', 'The managed update check did not complete in time.')
169
- );
170
- const checkedAt = Date.now();
171
- if (result.available) {
172
- const nextConsent = result.latestVersion !== consent.snoozedVersion
173
- ? { ...consent, snoozedVersion: '', snoozedUntil: 0 }
174
- : consent;
175
- await setConsentState({
176
- ...nextConsent,
177
- lastPromptedVersion: result.latestVersion,
178
- lastPromptedAt: manual ? checkedAt : nextConsent.lastPromptedAt
179
- });
180
- await setUpdateState({
181
- ...currentState,
182
- state: 'update_available',
183
- managed: true,
184
- currentVersion: result.currentVersion || currentVersion(),
185
- latestVersion: result.latestVersion,
186
- etag: result.etag || currentState.etag || '',
187
- lastCheckedAt: checkedAt,
188
- postponeUntil: LEGACY_GUARD,
189
- code: '',
190
- message: ''
191
- });
192
- } else if (result.reason === 'not_modified' &&
193
- policy.compareStableVersions(currentState.latestVersion, currentVersion()) > 0) {
194
- await setUpdateState({
195
- ...currentState,
196
- state: 'update_available',
197
- lastCheckedAt: checkedAt,
198
- postponeUntil: LEGACY_GUARD,
199
- code: '',
200
- message: ''
201
- });
202
- } else {
203
- await setUpdateState({
204
- ...currentState,
205
- state: 'idle',
206
- currentVersion: result.currentVersion || currentVersion(),
207
- latestVersion: result.latestVersion || result.currentVersion || currentVersion(),
208
- etag: result.etag || currentState.etag || '',
209
- lastCheckedAt: checkedAt,
210
- postponeUntil: LEGACY_GUARD,
211
- transactionId: '',
212
- code: '',
213
- message: ''
214
- });
167
+ const [currentState, consent] = await Promise.all([getUpdateState(), getConsentState()]);
168
+ assertOperation(operation);
169
+ if (currentState.cancelRequested || currentState.recoveryPending
170
+ || consent.authorizedVersion || policy.isExecutionState(currentState.state)) return getView();
171
+ operation.phase = 'checking';
172
+ await setUpdateState({ ...currentState, state: 'checking', operationId: operation.id,
173
+ cancelRequested: false, initiatedBy: manual ? 'manual' : 'automatic',
174
+ code: '', message: '', blocker: '', blockers: [], cooldownUntil: 0, postponeUntil: LEGACY_GUARD }, { operation });
175
+ try {
176
+ const result = await withTimeout(requestNative('update.check', {
177
+ operationId: operation.id, currentVersion: currentVersion(), etag: currentState.etag || ''
178
+ }), CHECK_REQUEST_TIMEOUT_MS, codedError('update_check_timeout', 'The managed update check did not complete in time.'));
179
+ assertOperation(operation);
180
+ const checkedAt = Date.now();
181
+ if (result.available) {
182
+ const latestVersion = result.latestVersion || currentState.latestVersion;
183
+ await setConsentState({ ...consent,
184
+ ...(latestVersion !== consent.snoozedVersion ? { snoozedVersion: '', snoozedUntil: 0 } : {}),
185
+ lastPromptedVersion: latestVersion,
186
+ lastPromptedAt: manual ? checkedAt : consent.lastPromptedAt
187
+ }, { operation });
188
+ await setUpdateState({ ...currentState, state: 'update_available', operationId: operation.id,
189
+ currentVersion: result.currentVersion || currentVersion(), latestVersion,
190
+ managed: true, etag: result.etag || currentState.etag || '', lastCheckedAt: checkedAt,
191
+ postponeUntil: LEGACY_GUARD, code: '', message: '', cooldownUntil: 0, manualReason: {}, cancelRequested: false
192
+ }, { operation });
193
+ } else if ((result.reason === 'manual_install_required'
194
+ && policy.compareStableVersions(result.latestVersion, currentVersion()) > 0)
195
+ || (result.reason === 'prerelease_promotion_requires_manual_update'
196
+ && result.latestVersion === (result.currentVersion || currentVersion()))) {
197
+ await setUpdateState({ ...currentState, state: 'manual_install_required', operationId: operation.id,
198
+ currentVersion: result.currentVersion || currentVersion(), latestVersion: result.latestVersion,
199
+ managed: true, etag: result.etag || currentState.etag || '', lastCheckedAt: checkedAt,
200
+ postponeUntil: LEGACY_GUARD, code: result.reason === 'prerelease_promotion_requires_manual_update'
201
+ ? result.reason : 'update_bootstrap_upgrade_required', message: '', cooldownUntil: 0,
202
+ manualReason: result.manualInstall?.reason || {}, cancelRequested: false
203
+ }, { operation });
204
+ } else {
205
+ await setUpdateState({ ...currentState, state: 'idle', operationId: operation.id,
206
+ currentVersion: result.currentVersion || currentVersion(),
207
+ latestVersion: result.latestVersion || result.currentVersion || currentVersion(),
208
+ // A 304 without a verified Native candidate is a cache miss. Drop
209
+ // its validator so the next normal check can obtain fresh bytes.
210
+ etag: result.reason === 'not_modified' ? '' : result.etag || currentState.etag || '', lastCheckedAt: checkedAt,
211
+ postponeUntil: LEGACY_GUARD, transactionId: '',
212
+ code: result.reason === 'cooldown' ? 'update_cooldown' : '',
213
+ cooldownUntil: result.reason === 'cooldown' ? Number(result.cooldownUntil || 0) : 0,
214
+ manualReason: {}, message: '', cancelRequested: false
215
+ }, { operation });
216
+ }
217
+ return getView();
218
+ } catch (error) {
219
+ if (!isCurrentOperation(operation)) return getView();
220
+ await requestNative('update.cancel', { operationId: operation.id }).catch(() => {});
221
+ assertOperation(operation);
222
+ await setUpdateState({ ...currentState, state: 'failed', operationId: operation.id,
223
+ initiatedBy: manual ? 'manual' : 'automatic', postponeUntil: LEGACY_GUARD,
224
+ code: safeCode(error), message: safeMessage(error)
225
+ }, { operation });
226
+ throw error;
215
227
  }
216
- return getView();
217
- } catch (error) {
218
- await setUpdateState({
219
- ...currentState,
220
- state: 'failed',
221
- initiatedBy: manual ? 'manual' : 'automatic',
222
- postponeUntil: LEGACY_GUARD,
223
- code: safeCode(error),
224
- message: safeMessage(error)
225
- });
226
- throw error;
228
+ } finally {
229
+ if (!inheritedOperation) finishOperation(operation);
227
230
  }
228
231
  }
229
232
 
233
+
230
234
  async function recoverInterruptedCheck() {
231
235
  const state = await getUpdateState();
232
- if (state.state !== 'checking') return;
233
- const nextState = policy.compareStableVersions(state.latestVersion, currentVersion()) > 0
234
- ? 'update_available'
235
- : 'idle';
236
- await setUpdateState({
237
- ...state,
238
- state: nextState,
239
- blocker: '',
240
- blockers: [],
241
- code: '',
242
- message: ''
236
+ if (state.state !== 'checking' || activeOperation || state.cancelRequested) return;
237
+ if (state.operationId) {
238
+ await postponeUpdate().catch(() => {});
239
+ return;
240
+ }
241
+ await setUpdateState({ ...state,
242
+ state: policy.compareStableVersions(state.latestVersion, currentVersion()) > 0 ? 'update_available' : 'idle',
243
+ blocker: '', blockers: [], code: '', message: ''
243
244
  });
244
245
  }
245
246
 
247
+
246
248
  async function dismissCompletedUpdate() {
247
249
  const state = await getUpdateState();
248
250
  if (state.state !== 'committed') return getView();
@@ -263,194 +265,177 @@
263
265
  }
264
266
 
265
267
  async function installUpdate() {
266
- let state = await getUpdateState();
267
- let consent = await getConsentState();
268
- const candidateStale = !state.lastCheckedAt || Date.now() - state.lastCheckedAt > CANDIDATE_MAX_AGE_MS;
269
- if (state.state !== 'update_available' || candidateStale) {
270
- await checkOnly({ manual: true });
268
+ const operation = beginOperation();
269
+ let state, consent, authorizationId = '';
270
+ try {
271
271
  state = await getUpdateState();
272
272
  consent = await getConsentState();
273
- }
274
- if (state.state !== 'update_available' ||
275
- policy.compareStableVersions(state.latestVersion, currentVersion()) <= 0) {
276
- throw codedError('update_candidate_missing', 'No newer signed stable update is available.');
277
- }
278
-
279
- const authorizationId = crypto.randomUUID();
280
- let authorizationIntent = revocation.prepareAuthorization(
281
- consent,
282
- authorizationId,
283
- state.latestVersion,
284
- Date.now()
285
- );
286
- try {
287
- authorizationIntent = await setConsentState(authorizationIntent);
273
+ assertOperation(operation);
274
+ if (state.cancelRequested || state.recoveryPending || policy.isExecutionState(state.state)) return getView();
275
+ const candidateStale = !state.lastCheckedAt || Date.now() - state.lastCheckedAt > CANDIDATE_MAX_AGE_MS;
276
+ if (state.state !== 'update_available' || candidateStale) {
277
+ await checkOnly({ manual: true }, operation);
278
+ assertOperation(operation);
279
+ state = await getUpdateState();
280
+ consent = await getConsentState();
281
+ assertOperation(operation);
282
+ }
283
+ // The fresh check found a release only a reinstall can apply; show that notice.
284
+ if (state.state === 'manual_install_required') return getView();
285
+ if (state.state !== 'update_available'
286
+ || policy.compareStableVersions(state.latestVersion, currentVersion()) <= 0) {
287
+ throw codedError('update_candidate_missing', 'No newer signed stable update is available.');
288
+ }
289
+ authorizationId = crypto.randomUUID();
290
+ operation.authorizationId = authorizationId;
291
+ operation.targetVersion = state.latestVersion;
292
+ const intent = revocation.prepareAuthorization(consent, authorizationId, state.latestVersion, Date.now());
293
+ await setConsentState(intent, { operation });
294
+ assertOperation(operation);
288
295
  await requestNative('update.authorize', {
289
- authorizationId,
290
- targetVersion: state.latestVersion,
291
- currentVersion: currentVersion()
296
+ operationId: operation.id, authorizationId, targetVersion: state.latestVersion, currentVersion: currentVersion()
292
297
  });
293
- await setConsentState(revocation.clear({
294
- ...authorizationIntent,
295
- snoozedVersion: '',
296
- snoozedUntil: 0,
297
- authorizedVersion: state.latestVersion,
298
- authorizationId,
299
- authorizedAt: Date.now()
300
- }));
301
- await setUpdateState({ ...state, postponeUntil: 0, code: '', message: '' });
302
- const executor = globalThis.CodexOverleafManagedUpdateExecutor;
303
- if (!executor || typeof executor.installAuthorizedUpdate !== 'function') {
304
- throw codedError(
305
- 'update_executor_unavailable',
306
- 'The managed update executor is unavailable.'
307
- );
308
- }
309
- await executor.installAuthorizedUpdate();
298
+ assertOperation(operation);
299
+ await setConsentState(revocation.clear({ ...intent, snoozedVersion: '', snoozedUntil: 0,
300
+ authorizedVersion: state.latestVersion, authorizationId, authorizedAt: Date.now()
301
+ }), { operation });
302
+ await setUpdateState({ ...state, operationId: operation.id, postponeUntil: LEGACY_GUARD,
303
+ cancelRequested: false, recoveryPending: false, code: '', message: ''
304
+ }, { operation });
305
+ // Replaceable runtime owns this transfer. The immutable Bootstrap keeps
306
+ // its health-confirmation role and cannot resume a postponed operation.
307
+ await stageAuthorizedUpdate(operation);
310
308
  return getView();
311
309
  } catch (error) {
312
- const observedConsent = await getConsentState().catch(() => consent);
313
- const pendingConsent = revocation.prepareAuthorization({
314
- ...observedConsent,
315
- authorizedVersion: state.latestVersion,
316
- authorizationId,
317
- authorizedAt: observedConsent.authorizedAt || Date.now()
318
- }, authorizationId, state.latestVersion, Date.now());
319
- await setConsentState(pendingConsent);
320
- try {
321
- await requestNative('update.revoke', {
322
- authorizationId: pendingConsent.revokingAuthorizationId,
323
- targetVersion: pendingConsent.revokingVersion,
324
- transactionId: pendingConsent.revokingTransactionId
325
- });
326
- await setConsentState(revocation.clear({
327
- ...pendingConsent,
328
- authorizedVersion: '',
329
- authorizationId: '',
330
- authorizedAt: 0
331
- }));
332
- } catch (revokeError) {
333
- if (error && typeof error === 'object') {
310
+ if (!isCurrentOperation(operation)) return getView();
311
+ if (authorizationId) {
312
+ const observed = await getConsentState();
313
+ assertOperation(operation);
314
+ const pending = revocation.prepareAuthorization({ ...observed,
315
+ authorizedVersion: state.latestVersion, authorizationId,
316
+ authorizedAt: observed.authorizedAt || Date.now()
317
+ }, authorizationId, state.latestVersion, Date.now());
318
+ await setConsentState(pending, { operation });
319
+ try {
320
+ await requestNative('update.revoke', {
321
+ operationId: operation.id, authorizationId, targetVersion: state.latestVersion
322
+ });
323
+ assertOperation(operation);
324
+ await setConsentState(revocation.clear({ ...pending,
325
+ authorizedVersion: '', authorizationId: '', authorizedAt: 0
326
+ }), { operation });
327
+ } catch (revokeError) {
328
+ if (!isCurrentOperation(operation)) return getView();
329
+ operation.revocationPending = true;
334
330
  error.revocationError = safeMessage(revokeError);
335
331
  }
336
332
  }
337
- const observed = await getUpdateState().catch(() => state);
338
- if (!['awaiting_health', 'committed', 'rolled_back'].includes(observed.state)) {
339
- await setUpdateState({
340
- ...observed,
341
- state: 'failed',
342
- initiatedBy: 'manual',
343
- blocker: '',
344
- blockers: [],
345
- code: safeCode(error),
346
- message: safeMessage(error)
347
- });
333
+ const observedState = await getUpdateState();
334
+ assertOperation(operation);
335
+ if (!['awaiting_health', 'committed', 'rolled_back'].includes(observedState.state)) {
336
+ await setUpdateState({ ...observedState, state: 'failed', initiatedBy: 'manual',
337
+ blocker: '', blockers: [], code: safeCode(error), message: safeMessage(error),
338
+ operationId: operation.id,
339
+ cancelRequested: operation.phase !== 'applying' && operation.revocationPending === true,
340
+ recoveryPending: operation.phase === 'applying'
341
+ }, { operation });
348
342
  }
349
- await armLegacyGuard();
350
343
  throw error;
351
- }
344
+ } finally { finishOperation(operation); }
352
345
  }
353
346
 
354
- async function stageAuthorizedUpdate() {
355
- const current = await getUpdateState();
356
- await setUpdateState({
357
- ...current,
358
- state: 'downloading',
359
- initiatedBy: 'manual',
360
- blocker: '',
361
- blockers: [],
362
- code: '',
363
- message: ''
364
- });
365
- const staged = await requestNative('update.stage');
366
- await setUpdateState({
367
- ...current,
368
- state: 'staged',
369
- initiatedBy: 'manual',
370
- latestVersion: staged.targetVersion,
371
- transactionId: staged.transactionId,
372
- stagedAt: Date.now(),
373
- blocker: '',
374
- blockers: [],
375
- code: '',
376
- message: ''
377
- });
378
- return tryActivateStagedUpdate();
347
+
348
+ async function stageAuthorizedUpdate(operation) {
349
+ operation.phase = 'downloading';
350
+ await setUpdateState({ state: 'downloading', operationId: operation.id,
351
+ initiatedBy: 'manual', blocker: '', blockers: [], code: '', message: '',
352
+ postponeUntil: LEGACY_GUARD
353
+ }, { operation });
354
+ assertOperation(operation);
355
+ const staged = await requestNative('update.stage', { operationId: operation.id });
356
+ assertOperation(operation);
357
+ await setUpdateState({ state: 'staged', operationId: operation.id, initiatedBy: 'manual',
358
+ latestVersion: staged.targetVersion, transactionId: staged.transactionId, stagedAt: Date.now(),
359
+ blocker: '', blockers: [], code: '', message: '', postponeUntil: LEGACY_GUARD
360
+ }, { operation });
361
+ return tryActivateStagedUpdate(operation);
379
362
  }
380
363
 
381
- function tryActivateStagedUpdate() {
382
- const executor = root.CodexOverleafManagedUpdateExecutor;
383
- if (typeof executor?.installAuthorizedUpdate === 'function') {
384
- if (activationPromise) return activationPromise;
385
- activationPromise = Promise.resolve(executor.installAuthorizedUpdate()).finally(() => {
386
- activationPromise = null;
387
- });
388
- return activationPromise;
389
- }
364
+
365
+ async function tryActivateStagedUpdate(inheritedOperation) {
390
366
  if (activationPromise) return activationPromise;
391
- activationPromise = tryActivateStagedUpdateCore().finally(() => {
367
+ const state = await getUpdateState();
368
+ if (state.cancelRequested || state.recoveryPending || cancellationPromise
369
+ || !['staged', 'waiting_for_idle'].includes(state.state)) return state;
370
+ const operation = inheritedOperation || beginOperation(state.operationId);
371
+ assertOperation(operation);
372
+ activationPromise = tryActivateStagedUpdateCore(operation).finally(() => {
392
373
  activationPromise = null;
374
+ if (!inheritedOperation) finishOperation(operation);
393
375
  });
394
376
  return activationPromise;
395
377
  }
396
378
 
397
- async function tryActivateStagedUpdateCore() {
379
+
380
+ async function tryActivateStagedUpdateCore(operation) {
398
381
  clearIdleRetryTimer();
399
382
  const state = await getUpdateState();
400
- if (!['staged', 'waiting_for_idle'].includes(state.state)) {
401
- await chrome.alarms?.clear?.(IDLE_ALARM).catch(() => {});
402
- return state;
403
- }
404
- const surfaceTabs = await chrome.tabs.query({ url: OVERLEAF_MATCHES }).catch(() => []);
383
+ assertOperation(operation);
384
+ if (state.cancelRequested || !['staged', 'waiting_for_idle'].includes(state.state)) return state;
385
+ operation.phase = 'waiting_for_idle';
386
+ const surfaceTabs = await chrome.tabs.query({ url: OVERLEAF_MATCHES });
405
387
  const editorTabs = surfaceTabs.filter(isUsableEditorTab);
406
- const reloadTabs = surfaceTabs
407
- .filter(tab => Number.isInteger(tab?.id) && !tab.discarded && tab.status !== 'unloaded')
408
- .map(tab => tab.id);
388
+ const reloadTabs = surfaceTabs.filter(tab => Number.isInteger(tab?.id) && !tab.discarded && tab.status !== 'unloaded').map(tab => tab.id);
409
389
  const probes = await Promise.all(editorTabs.map(tab => probeTabIdle(tab.id)));
410
390
  const nativeGate = await requestNative('update.canApply')
411
- .then(result => ({ ok: true, result }))
412
- .catch(error => ({ ok: false, error: safeError(error) }));
391
+ .then(result => ({ ok: true, result })).catch(error => ({ ok: false, error: safeError(error) }));
392
+ assertOperation(operation);
413
393
  const blockers = root.CodexOverleafUpdateStatus?.collectBlockers(probes, nativeGate) || ['busy'];
394
+ if (nativeBridge?.getPendingState?.().executionRequests > 0) blockers.push('background_execution_pending');
414
395
  if (blockers.length) {
415
- const waiting = await setUpdateState({
416
- ...state,
417
- state: 'waiting_for_idle',
418
- blocker: blockers[0],
419
- blockers
420
- });
421
- scheduleIdleRetry(blockers);
396
+ const waiting = await setUpdateState({ state: 'waiting_for_idle',
397
+ operationId: operation.id, blocker: blockers[0], blockers
398
+ }, { operation });
399
+ if (isCurrentOperation(operation)) scheduleIdleRetry(blockers);
422
400
  return waiting;
423
401
  }
424
-
402
+ operation.phase = 'applying';
425
403
  await chrome.alarms?.clear?.(IDLE_ALARM).catch(() => {});
404
+ assertOperation(operation);
426
405
  await chrome.storage.local.set({ [UPDATE_RELOAD_TABS_KEY]: reloadTabs });
427
- await setUpdateState({
428
- ...state,
429
- state: 'applying',
430
- blocker: '',
431
- blockers: [],
432
- code: '',
433
- message: ''
434
- });
435
- const activation = await requestNative('update.activate', {
436
- transactionId: state.transactionId
437
- });
438
- const transaction = await waitForActivatedTransaction(activation.transactionId || state.transactionId);
439
- if (transaction.state === 'rolled_back') {
440
- throw codedError(transaction.reasonCode || 'update_apply_failed', 'The managed update was rolled back during activation.');
406
+ await setUpdateState({ state: 'applying', operationId: operation.id,
407
+ blocker: '', blockers: [], code: '', message: ''
408
+ }, { operation });
409
+ assertOperation(operation);
410
+ // requestInternal rechecks the safe point and blocks new native work.
411
+ let applied;
412
+ try {
413
+ applied = await requestNative('update.apply', {
414
+ operationId: operation.id, transactionId: state.transactionId
415
+ });
416
+ } catch (error) {
417
+ assertOperation(operation);
418
+ if (!['update_not_idle', 'update_native_busy'].includes(error.code)) throw error;
419
+ operation.phase = 'waiting_for_idle';
420
+ const waiting = await setUpdateState({ state: 'waiting_for_idle',
421
+ blocker: 'busy', blockers: ['busy'], recoveryPending: false
422
+ }, { operation, observed: true });
423
+ scheduleIdleRetry(waiting.blockers);
424
+ return waiting;
441
425
  }
442
- await setUpdateState({
443
- ...state,
444
- state: 'awaiting_health',
445
- latestVersion: transaction.targetVersion,
446
- transactionId: transaction.id,
447
- code: '',
448
- message: ''
449
- });
426
+ assertOperation(operation);
427
+ if (applied.state !== 'awaiting_health' || applied.transactionId !== state.transactionId) {
428
+ throw codedError('update_apply_unconfirmed', 'Update activation did not return matching transaction evidence.');
429
+ }
430
+ await setUpdateState({ state: 'awaiting_health', operationId: operation.id,
431
+ latestVersion: applied.targetVersion, transactionId: applied.transactionId,
432
+ recoveryPending: false, code: '', message: ''
433
+ }, { operation });
450
434
  chrome.runtime.reload();
451
435
  return { state: 'awaiting_health' };
452
436
  }
453
437
 
438
+
454
439
  async function waitForActivatedTransaction(transactionId) {
455
440
  const deadline = Date.now() + ACTIVATION_TIMEOUT_MS;
456
441
  while (Date.now() <= deadline) {
@@ -486,7 +471,7 @@
486
471
  try {
487
472
  const url = new URL(tab.url || '');
488
473
  return url.protocol === 'https:' &&
489
- (url.hostname === 'www.overleaf.com' || url.hostname === 'overleaf.com') &&
474
+ (['overleaf.com', 'www.overleaf.com', 'cn.overleaf.com'].includes(url.hostname)) &&
490
475
  /^\/project\/[^/]+(?:\/|$)/.test(url.pathname);
491
476
  } catch (_error) {
492
477
  return false;
@@ -505,143 +490,253 @@
505
490
  }
506
491
  }
507
492
 
508
- async function postponeUpdate() {
493
+ // A reinstall notice has no download or authorization to cancel; Later only
494
+ // hides it for a day.
495
+ async function snoozeManualInstall() {
509
496
  const [state, consent] = await Promise.all([getUpdateState(), getConsentState()]);
510
- if (['applying', 'awaiting_health'].includes(state.state)) {
511
- throw codedError('update_revoke_too_late', 'The update is already being installed.');
512
- }
513
- let pendingConsent = consent;
514
- if (consent.authorizationId) {
515
- pendingConsent = revocation.begin(consent, state, Date.now());
516
- await setConsentState(pendingConsent);
517
- try {
518
- await requestNative('update.revoke', {
519
- authorizationId: pendingConsent.revokingAuthorizationId,
520
- targetVersion: pendingConsent.revokingVersion,
521
- transactionId: pendingConsent.revokingTransactionId
522
- });
523
- } catch (error) {
524
- if (error?.code === 'update_revoke_too_late') {
525
- await setConsentState(revocation.clear(pendingConsent));
526
- return getView();
527
- }
528
- throw error;
529
- }
530
- } else if (['staged', 'waiting_for_idle'].includes(state.state)) {
531
- throw codedError('update_consent_mismatch', 'The staged update has no matching runtime authorization.');
532
- }
533
-
534
- clearIdleRetryTimer();
535
- await chrome.alarms?.clear?.(IDLE_ALARM).catch(() => {});
536
- const completed = revocation.complete(state, pendingConsent, {
537
- now: Date.now(),
538
- snoozeMs: SNOOZE_MS,
539
- postponeUntil: LEGACY_GUARD
540
- });
541
- await setUpdateAndConsentState(completed.updateState, completed.consentState);
497
+ if (state.state !== 'manual_install_required') return null;
498
+ await setConsentState({ ...consent, snoozedVersion: state.latestVersion, snoozedUntil: Date.now() + SNOOZE_MS });
542
499
  return getView();
543
500
  }
544
501
 
545
- async function reconcileRecoveryState() {
546
- await reconcilePendingRevocation();
547
- return reconcileExpiredPhase();
502
+ function postponeUpdate() {
503
+ if (cancellationPromise) return cancellationPromise;
504
+ cancellationPromise = cancelUpdateCore().finally(() => { cancellationPromise = null; });
505
+ return cancellationPromise;
548
506
  }
549
507
 
550
- async function reconcilePendingRevocation() {
508
+ async function cancelUpdateCore() {
551
509
  const [state, consent] = await Promise.all([getUpdateState(), getConsentState()]);
552
- if (!revocation.hasPending(consent)) return state;
553
-
510
+ if (['applying', 'awaiting_health', 'rolling_back'].includes(state.state)
511
+ || activeOperation?.phase === 'applying' || state.recoveryPending) {
512
+ throw codedError('update_revoke_too_late', 'The update is being installed or recovered.');
513
+ }
514
+ const operation = activeOperation;
515
+ const generation = ++operationGeneration;
516
+ activeOperation = null;
517
+ clearIdleRetryTimer();
518
+ const operationId = operation?.id || state.operationId || '';
519
+ const authorizationId = consent.authorizationId || consent.revokingAuthorizationId || operation?.authorizationId || '';
520
+ const targetVersion = consent.authorizedVersion || consent.revokingVersion || operation?.targetVersion || state.latestVersion;
521
+ const pending = authorizationId ? revocation.begin({ ...consent, authorizationId,
522
+ authorizedVersion: targetVersion }, state, Date.now()) : consent;
523
+ await setUpdateAndConsentState({ ...state, operationId, cancelRequested: true,
524
+ postponeUntil: LEGACY_GUARD, code: '', message: ''
525
+ }, pending, { generation });
526
+ await chrome.alarms?.clear?.(IDLE_ALARM).catch(() => {});
554
527
  try {
555
- await requestNative('update.revoke', {
556
- authorizationId: consent.revokingAuthorizationId,
557
- targetVersion: consent.revokingVersion,
558
- transactionId: consent.revokingTransactionId
559
- });
560
- } catch (error) {
561
- if (error?.code === 'update_revoke_too_late') {
562
- await setConsentState(revocation.clear(consent));
563
- return state;
528
+ if (operationId) {
529
+ const result = await withTimeout(requestNative('update.cancel', { operationId }), 15000,
530
+ codedError('update_cancel_unconfirmed', 'Update cancellation has not been acknowledged yet.'));
531
+ if (result.state !== 'cancelled' || result.operationId !== operationId) {
532
+ throw codedError('update_cancel_unconfirmed', 'Update cancellation returned incomplete evidence.');
533
+ }
534
+ } else if (state.state === 'downloading') {
535
+ throw codedError('update_cancel_unconfirmed', 'This older download has no operation identity. Reload the extension to recover it.');
536
+ }
537
+ if (authorizationId) {
538
+ await withTimeout(requestNative('update.revoke', {
539
+ operationId, authorizationId, targetVersion
540
+ }), 15000, codedError('update_cancel_unconfirmed', 'Update authorization cleanup has not finished yet.'));
564
541
  }
565
- if (!['update_already_revoked', 'update_authorization_revoked'].includes(error?.code)) {
566
- return state;
542
+ const completed = revocation.complete(state, pending, { now: Date.now(),
543
+ snoozeMs: SNOOZE_MS, postponeUntil: LEGACY_GUARD });
544
+ await setUpdateAndConsentState({ ...completed.updateState,
545
+ state: policy.compareStableVersions(state.latestVersion, currentVersion()) > 0 ? 'update_available' : 'idle',
546
+ operationId: '', cancelRequested: false, recoveryPending: false,
547
+ code: 'update_cancelled', message: ''
548
+ }, completed.consentState, { generation, observed: true });
549
+ return getView();
550
+ } catch (error) {
551
+ if (error.code === 'update_revoke_too_late') {
552
+ await setUpdateAndConsentState({ cancelRequested: false, recoveryPending: true,
553
+ state: 'failed', code: 'update_recovery_pending', message: 'The update reached installation before cancellation. Checking its result.'
554
+ }, revocation.clear(pending), { generation, observed: true });
555
+ } else {
556
+ await setUpdateState({ cancelRequested: true, code: 'update_cancel_unconfirmed',
557
+ message: safeMessage(error)
558
+ }, { generation });
567
559
  }
560
+ throw error;
568
561
  }
562
+ }
569
563
 
570
- clearIdleRetryTimer();
571
- await chrome.alarms?.clear?.(IDLE_ALARM).catch(() => {});
572
- const completed = revocation.complete(state, consent, {
573
- now: Date.now(),
574
- snoozeMs: SNOOZE_MS,
575
- postponeUntil: LEGACY_GUARD
576
- });
577
- const settled = await setUpdateAndConsentState(
578
- completed.updateState,
579
- completed.consentState
580
- );
581
- return settled.updateState;
564
+
565
+ function reconcileRecoveryState() {
566
+ if (recoveryPromise) return recoveryPromise;
567
+ recoveryPromise = (async () => {
568
+ if (!await waitForBootstrapOwnership()) return getUpdateState();
569
+ const state = await getUpdateState();
570
+ if (state.recoveryPending) return reconcileExpiredPhase();
571
+ if (state.cancelRequested) return postponeUpdate();
572
+ if (!activeOperation) await reconcilePendingRevocation();
573
+ return reconcileExpiredPhase();
574
+ })().finally(() => { recoveryPromise = null; });
575
+ return recoveryPromise;
582
576
  }
583
577
 
578
+
579
+ async function reconcilePendingRevocation() {
580
+ const consent = await getConsentState();
581
+ if (!revocation.hasPending(consent) || activeOperation) return getUpdateState();
582
+ return postponeUpdate().then(view => view.state);
583
+ }
584
+
585
+
584
586
  async function settleTerminalConsent() {
585
587
  const [state, consent] = await Promise.all([getUpdateState(), getConsentState()]);
586
- if (!policy.isTerminalState(state.state) || !consent.authorizationId) return;
587
- await setConsentState({
588
- ...consent,
589
- authorizedVersion: '',
590
- authorizationId: '',
591
- authorizedAt: 0
592
- });
588
+ if (!policy.isTerminalState(state.state) || state.cancelRequested || state.recoveryPending
589
+ || revocation.hasPending(consent) || !consent.authorizationId) return;
590
+ await setConsentState({ ...consent, authorizedVersion: '', authorizationId: '', authorizedAt: 0 },
591
+ { expectedAuthorizationId: consent.authorizationId });
593
592
  }
594
593
 
594
+
595
595
  async function reconcileExpiredPhase() {
596
- const state = await getUpdateState();
597
- if (!state.deadlineAt || Date.now() <= state.deadlineAt || policy.isTerminalState(state.state)) {
598
- return state;
596
+ const recoveryOwner = await captureRecoveryOwner();
597
+ const state = recoveryOwner.state;
598
+ const recoveryOptions = { observed: true, recoveryOwner };
599
+ if (!state.recoveryPending && (!state.deadlineAt || Date.now() <= state.deadlineAt
600
+ || policy.isTerminalState(state.state))) return state;
601
+ if (['checking', 'downloading'].includes(state.state)) {
602
+ await postponeUpdate();
603
+ return setUpdateState({ state: 'failed', cancelRequested: false,
604
+ code: 'update_phase_timeout', message: 'The update timed out and its download was stopped. Retry when the connection is available.'
605
+ }, { observed: true });
599
606
  }
600
- if (['applying', 'awaiting_health', 'rolling_back'].includes(state.state)) {
601
- const status = await requestNative('update.status').catch(() => null);
602
- const transaction = status?.transaction;
603
- if (transaction?.state === 'awaiting_health') {
604
- await requestNative('update.rollback', {
605
- transactionId: transaction.id,
606
- reasonCode: 'update_health_timeout'
607
- }).catch(() => null);
608
- return setUpdateState({
609
- ...state,
610
- state: 'failed',
611
- currentVersion: transaction.sourceVersion || state.currentVersion,
612
- code: 'update_health_timeout',
613
- message: 'The updated runtime did not complete its health check and was rolled back. Retry, or use the manual update command.'
614
- }, { observed: true });
615
- }
616
- if (transaction?.state === 'committed') {
617
- return setUpdateState({
618
- ...state,
619
- state: 'committed',
620
- currentVersion: transaction.targetVersion,
621
- latestVersion: transaction.targetVersion,
622
- code: '',
623
- message: ''
624
- }, { observed: true });
625
- }
626
- if (transaction?.state === 'rolled_back') {
627
- return setUpdateState({
628
- ...state,
629
- state: 'rolled_back',
630
- currentVersion: transaction.sourceVersion || state.currentVersion,
631
- code: transaction.reasonCode || 'update_rolled_back',
632
- message: 'The managed update was rolled back after activation did not complete.'
633
- }, { observed: true });
607
+ if (state.recoveryPending || ['applying', 'awaiting_health', 'rolling_back'].includes(state.state)) {
608
+ try {
609
+ let status = await withTimeout(requestNative('update.status'), 10000,
610
+ codedError('update_status_timeout', 'Update recovery status is unavailable.'));
611
+ await assertRecoveryOwner(recoveryOwner);
612
+ let transaction = status.transaction;
613
+ if (state.transactionId && transaction?.id !== state.transactionId) {
614
+ throw codedError('update_transaction_mismatch', 'The update recovery transaction could not be matched.');
615
+ }
616
+ if (transaction?.state === 'superseded') {
617
+ return settleSupersededTransaction(status, recoveryOwner);
618
+ }
619
+ if (transaction?.state === 'staged' || (!transaction && status.installedAligned
620
+ && status.activeVersion === state.currentVersion)) {
621
+ if (state.operationId) await requestNative('update.cancel', { operationId: state.operationId });
622
+ await assertRecoveryOwner(recoveryOwner);
623
+ const consent = await getConsentState();
624
+ const authorizationId = consent.authorizationId || consent.revokingAuthorizationId;
625
+ const targetVersion = consent.authorizedVersion || consent.revokingVersion || state.latestVersion;
626
+ await assertRecoveryOwner(recoveryOwner);
627
+ if (authorizationId) await requestNative('update.revoke', { authorizationId, targetVersion });
628
+ const completed = await setUpdateAndConsentState({ state: 'failed', recoveryPending: false, cancelRequested: false,
629
+ operationId: '', code: 'update_not_applied', message: 'The update was stopped before installation. Retry when ready.'
630
+ }, revocation.clear({ ...consent, authorizedVersion: '', authorizationId: '', authorizedAt: 0 }), recoveryOptions);
631
+ return completed.updateState;
632
+ }
633
+ if (transaction?.state === 'awaiting_health') {
634
+ const rolledBack = await requestNative('update.rollback', {
635
+ transactionId: transaction.id, expectedState: 'awaiting_health', reasonCode: 'update_health_timeout'
636
+ });
637
+ await assertRecoveryOwner(recoveryOwner);
638
+ if (rolledBack.state !== 'rolled_back' || rolledBack.version !== transaction.sourceVersion) {
639
+ throw codedError('update_rollback_unconfirmed', 'Rollback returned incomplete evidence.');
640
+ }
641
+ status = await requestNative('update.status');
642
+ await assertRecoveryOwner(recoveryOwner);
643
+ transaction = status.transaction;
644
+ }
645
+ if (transaction?.state === 'committed' && status.installedAligned
646
+ && status.activeVersion === transaction.targetVersion) {
647
+ return setUpdateState({ state: 'committed', currentVersion: transaction.targetVersion,
648
+ latestVersion: transaction.targetVersion, transactionId: transaction.id,
649
+ recoveryPending: false, cancelRequested: false, code: '', message: ''
650
+ }, recoveryOptions);
651
+ }
652
+ if (transaction?.state === 'rolled_back' && status.installedAligned
653
+ && status.activeVersion === transaction.sourceVersion
654
+ && (!state.transactionId || transaction.id === state.transactionId)) {
655
+ const restored = await setUpdateState({ state: 'rolled_back', currentVersion: transaction.sourceVersion,
656
+ transactionId: transaction.id, recoveryPending: false, cancelRequested: false,
657
+ code: transaction.reasonCode || 'update_rolled_back', message: 'The previous version has been restored.'
658
+ }, recoveryOptions);
659
+ setTimeout(() => chrome.runtime.reload(), 0);
660
+ return restored;
661
+ }
662
+ throw codedError('update_recovery_pending', 'The installed update result has not been confirmed.');
663
+ } catch (error) {
664
+ if (error.code === 'update_recovery_superseded') return getUpdateState();
665
+ return setUpdateState({ state: 'failed', recoveryPending: true, cancelRequested: false,
666
+ code: safeCode(error), message: safeMessage(error)
667
+ }, recoveryOptions).catch(writeError => {
668
+ if (writeError.code === 'update_recovery_superseded') return getUpdateState();
669
+ throw writeError;
670
+ });
634
671
  }
635
672
  }
636
- return setUpdateState({
637
- ...state,
638
- state: 'failed',
639
- code: 'update_phase_timeout',
640
- message: 'The managed update phase did not complete in time. Retry, or use the manual update command.'
673
+ return state;
674
+ }
675
+
676
+ async function captureRecoveryOwner() {
677
+ const generation = operationGeneration;
678
+ const [state, consent] = await Promise.all([getUpdateState(), getConsentState()]);
679
+ return { generation, state, consent };
680
+ }
681
+
682
+ function matchesRecoveryOwner(owner, state, consent) {
683
+ return owner && owner.generation === operationGeneration
684
+ && ['state', 'transactionId', 'operationId', 'currentVersion', 'latestVersion',
685
+ 'recoveryPending', 'cancelRequested', 'lastCheckedAt', 'phaseStartedAt']
686
+ .every(key => state[key] === owner.state[key])
687
+ && ['authorizationId', 'authorizedVersion', 'revokingAuthorizationId', 'revokingVersion']
688
+ .every(key => consent[key] === owner.consent[key]);
689
+ }
690
+
691
+ async function assertRecoveryOwner(owner) {
692
+ const [state, consent] = await Promise.all([getUpdateState(), getConsentState()]);
693
+ assertRecoverySnapshot(owner, state, consent);
694
+ }
695
+
696
+ function assertRecoverySnapshot(owner, state, consent) {
697
+ if (owner && !matchesRecoveryOwner(owner, state, consent)) {
698
+ throw codedError('update_recovery_superseded', 'A newer update state replaced this recovery request.');
699
+ }
700
+ }
701
+
702
+ // Both startup recovery and the coordinator retire the same Native journal
703
+ // through this queue. The snapshot was captured before requesting status.
704
+ function settleSupersededTransaction(status, owner) {
705
+ return enqueueStateWrite(async () => {
706
+ const [previous, consent] = await Promise.all([getUpdateState(), getConsentState()]);
707
+ if (!matchesRecoveryOwner(owner, previous, consent)) return previous;
708
+ const transaction = status?.transaction;
709
+ const sameTargetReinstall = transaction?.reasonCode === 'update_reinstalled'
710
+ && status.activeVersion === transaction.targetVersion
711
+ && status.reinstalledPair?.extensionVersion === status.activeVersion
712
+ && status.reinstalledPair?.nativeVersion === status.activeVersion
713
+ && /^[0-9a-f]{64}$/.test(status.reinstalledPair.extensionSha256 || '')
714
+ && /^[0-9a-f]{64}$/.test(status.reinstalledPair.nativeSha256 || '');
715
+ if (activeOperation || transaction?.state !== 'superseded' || !transaction.id
716
+ || (previous.transactionId && previous.transactionId !== transaction.id)
717
+ || status.installedAligned !== true || status.activeVersion !== transaction.supersededByVersion
718
+ || (!sameTargetReinstall && policy.compareStableVersions(status.activeVersion, transaction.targetVersion) <= 0)
719
+ || policy.compareStableVersions(status.activeVersion, transaction.sourceVersion) <= 0
720
+ || ['authorized', 'bound'].includes(status.authorization?.state)
721
+ || (consent.authorizedVersion && consent.authorizedVersion !== transaction.targetVersion)
722
+ || (consent.revokingVersion && consent.revokingVersion !== transaction.targetVersion)) return previous;
723
+ const next = projectUpdateState(previous, { state: 'idle', currentVersion: status.activeVersion,
724
+ latestVersion: status.activeVersion, transactionId: '', operationId: '', recoveryPending: false,
725
+ cancelRequested: false, blocker: '', blockers: [], code: '', message: '', manualReason: {}, cooldownUntil: 0
726
+ }, { observed: true });
727
+ // Invalidate older awaits before yielding to storage, including recovery
728
+ // responses that describe this same retired transaction.
729
+ operationGeneration += 1;
730
+ await chrome.storage.local.set({ [UPDATE_STATE_KEY]: next,
731
+ [CONSENT_STATE_KEY]: policy.normalizeConsentState({}), [UPDATE_RELOAD_TABS_KEY]: [] });
732
+ runtimeStatus = null;
733
+ return next;
641
734
  });
642
735
  }
643
736
 
737
+
644
738
  async function handleObservedStateChange() {
739
+ if (!await waitForBootstrapOwnership()) return;
645
740
  await settleTerminalConsent();
646
741
  const [state, consent] = await Promise.all([getUpdateState(), getConsentState()]);
647
742
  if (!consent.authorizationId && !policy.isExecutionState(state.state)) {
@@ -653,14 +748,14 @@
653
748
  async function armLegacyGuard() {
654
749
  const [state, consent] = await Promise.all([getUpdateState(), getConsentState()]);
655
750
  if (consent.authorizationId || state.postponeUntil === LEGACY_GUARD) return state;
656
- return setUpdateState({ ...state, postponeUntil: LEGACY_GUARD });
751
+ return setUpdateState({ postponeUntil: LEGACY_GUARD });
657
752
  }
658
753
 
659
754
  async function getView() {
660
755
  let [state, consent] = await Promise.all([getUpdateState(), getConsentState()]);
661
- const busy = state.state === 'checking' || policy.isExecutionState(state.state) || consent.authorizationId;
756
+ const busy = state.state === 'checking' || policy.isExecutionState(state.state) || consent.authorizationId || state.cancelRequested;
662
757
  const runtime = busy ? { state: 'transaction_active' } : await readRuntimeStatus();
663
- if (runtime.state === 'aligned' && ['failed', 'rolled_back'].includes(state.state) &&
758
+ if (!state.recoveryPending && !state.cancelRequested && runtime.state === 'aligned' && ['failed', 'rolled_back'].includes(state.state) &&
664
759
  policy.compareStableVersions(state.latestVersion || state.currentVersion, runtime.installedVersion) <= 0) {
665
760
  state = await setUpdateState({ ...state, state: 'idle', currentVersion: runtime.installedVersion,
666
761
  latestVersion: runtime.installedVersion, code: '', message: '', transactionId: '', blocker: '', blockers: [] });
@@ -769,17 +864,25 @@
769
864
  return policy.normalizeUpdateState(stored?.[UPDATE_STATE_KEY], currentVersion());
770
865
  }
771
866
 
772
- async function setUpdateState(value, options = {}) {
773
- const stored = await chrome.storage.local.get(UPDATE_STATE_KEY);
774
- const previous = policy.normalizeUpdateState(stored?.[UPDATE_STATE_KEY], currentVersion());
775
- const next = projectUpdateState(previous, value, options);
776
- await chrome.storage.local.set({ [UPDATE_STATE_KEY]: next });
777
- return next;
867
+ function setUpdateState(value, options = {}) {
868
+ return enqueueStateWrite(async () => {
869
+ assertWriteOwner(options);
870
+ const stored = await chrome.storage.local.get(options.recoveryOwner
871
+ ? [UPDATE_STATE_KEY, CONSENT_STATE_KEY] : UPDATE_STATE_KEY);
872
+ assertWriteOwner(options);
873
+ const previous = policy.normalizeUpdateState(stored?.[UPDATE_STATE_KEY], currentVersion());
874
+ assertRecoverySnapshot(options.recoveryOwner, previous, policy.normalizeConsentState(stored?.[CONSENT_STATE_KEY]));
875
+ const next = projectUpdateState(previous, value, options);
876
+ await chrome.storage.local.set({ [UPDATE_STATE_KEY]: next });
877
+ return next;
878
+ });
778
879
  }
779
880
 
881
+
780
882
  function projectUpdateState(previous, value, options = {}) {
781
883
  const now = Date.now();
782
884
  const candidate = {
885
+ ...previous,
783
886
  ...value,
784
887
  initiatedBy: value.initiatedBy || previous.initiatedBy
785
888
  };
@@ -805,24 +908,36 @@
805
908
  return policy.normalizeConsentState(stored?.[CONSENT_STATE_KEY]);
806
909
  }
807
910
 
808
- async function setConsentState(value) {
809
- const next = policy.normalizeConsentState(value);
810
- await chrome.storage.local.set({ [CONSENT_STATE_KEY]: next });
811
- return next;
911
+ function setConsentState(value, options = {}) {
912
+ return enqueueStateWrite(async () => {
913
+ assertWriteOwner(options);
914
+ if (options.expectedAuthorizationId) {
915
+ const current = await getConsentState();
916
+ if (current.authorizationId !== options.expectedAuthorizationId) return current;
917
+ }
918
+ assertWriteOwner(options);
919
+ const next = policy.normalizeConsentState(value);
920
+ await chrome.storage.local.set({ [CONSENT_STATE_KEY]: next });
921
+ return next;
922
+ });
812
923
  }
813
924
 
814
- async function setUpdateAndConsentState(updateValue, consentValue, options = {}) {
815
- const stored = await chrome.storage.local.get([UPDATE_STATE_KEY, CONSENT_STATE_KEY]);
816
- const previous = policy.normalizeUpdateState(stored?.[UPDATE_STATE_KEY], currentVersion());
817
- const updateState = projectUpdateState(previous, updateValue, options);
818
- const consentState = policy.normalizeConsentState(consentValue);
819
- await chrome.storage.local.set({
820
- [UPDATE_STATE_KEY]: updateState,
821
- [CONSENT_STATE_KEY]: consentState
925
+
926
+ function setUpdateAndConsentState(updateValue, consentValue, options = {}) {
927
+ return enqueueStateWrite(async () => {
928
+ assertWriteOwner(options);
929
+ const stored = await chrome.storage.local.get([UPDATE_STATE_KEY, CONSENT_STATE_KEY]);
930
+ assertWriteOwner(options);
931
+ const previous = policy.normalizeUpdateState(stored?.[UPDATE_STATE_KEY], currentVersion());
932
+ assertRecoverySnapshot(options.recoveryOwner, previous, policy.normalizeConsentState(stored?.[CONSENT_STATE_KEY]));
933
+ const updateState = projectUpdateState(previous, updateValue, options);
934
+ const consentState = policy.normalizeConsentState(consentValue);
935
+ await chrome.storage.local.set({ [UPDATE_STATE_KEY]: updateState, [CONSENT_STATE_KEY]: consentState });
936
+ return { updateState, consentState };
822
937
  });
823
- return { updateState, consentState };
824
938
  }
825
939
 
940
+
826
941
  async function requestNative(method, params = {}) {
827
942
  const response = await nativeBridge?.requestInternal?.({
828
943
  id: crypto.randomUUID(),
@@ -839,11 +954,56 @@
839
954
  }
840
955
 
841
956
  function enqueuePolicyAction(action) {
842
- const result = policyTail.then(action, action);
957
+ const generation = operationGeneration;
958
+ const guarded = async () => !await waitForBootstrapOwnership()
959
+ || generation !== operationGeneration || cancellationPromise ? getView() : action();
960
+ const result = policyTail.then(guarded, guarded);
843
961
  policyTail = result.catch(() => {});
844
962
  return result;
845
963
  }
846
964
 
965
+ async function waitForBootstrapOwnership() {
966
+ const migration = await root.CodexOverleafBootstrapMigration?.ready?.();
967
+ if (migration?.continueInitialization === false) return false;
968
+ await root.__CODEX_OVERLEAF_BOOTSTRAP_READY__;
969
+ return true;
970
+ }
971
+
972
+ function beginOperation(id) {
973
+ const operation = { id: id || crypto.randomUUID(), generation: ++operationGeneration, phase: 'checking' };
974
+ activeOperation = operation;
975
+ return operation;
976
+ }
977
+
978
+ function isCurrentOperation(operation) {
979
+ return activeOperation === operation && operation.generation === operationGeneration;
980
+ }
981
+
982
+ function assertOperation(operation) {
983
+ if (!isCurrentOperation(operation)) throw codedError('update_network_cancelled', 'The update request was cancelled.');
984
+ }
985
+
986
+ function finishOperation(operation) {
987
+ if (activeOperation === operation) activeOperation = null;
988
+ }
989
+
990
+ function assertWriteOwner(options) {
991
+ if (options.recoveryOwner && options.recoveryOwner.generation !== operationGeneration) {
992
+ throw codedError('update_recovery_superseded', 'A newer update state replaced this recovery request.');
993
+ }
994
+ if (options.operation) assertOperation(options.operation);
995
+ if (options.generation !== undefined && options.generation !== operationGeneration) {
996
+ throw codedError('update_network_cancelled', 'The update request was superseded.');
997
+ }
998
+ }
999
+
1000
+ function enqueueStateWrite(action) {
1001
+ const result = stateWriteTail.then(action, action);
1002
+ stateWriteTail = result.catch(() => {});
1003
+ return result;
1004
+ }
1005
+
1006
+
847
1007
  function isAllowedSender(sender) {
848
1008
  if (sender?.id !== chrome.runtime.id) return false;
849
1009
  try {
@@ -853,7 +1013,7 @@
853
1013
  return url.pathname === '/bootstrap/popup.html';
854
1014
  }
855
1015
  return url.protocol === 'https:' &&
856
- (url.hostname === 'www.overleaf.com' || url.hostname === 'overleaf.com') &&
1016
+ (['overleaf.com', 'www.overleaf.com', 'cn.overleaf.com'].includes(url.hostname)) &&
857
1017
  (url.pathname === '/project' || url.pathname.startsWith('/project/'));
858
1018
  } catch (_error) {
859
1019
  return false;
@@ -907,5 +1067,5 @@
907
1067
  .slice(0, 300);
908
1068
  }
909
1069
 
910
- root.CodexOverleafUpdateCoordinator = Object.freeze({ init });
1070
+ root.CodexOverleafUpdateCoordinator = Object.freeze({ init, captureRecoveryOwner, settleSupersededTransaction });
911
1071
  })(globalThis);