codex-overleaf-link 2.5.0 → 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.
- package/README.md +38 -26
- package/README.zh-CN.md +39 -27
- 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 +206 -53
- package/extension/src/backgroundBootstrapMigration.js +181 -0
- package/extension/src/backgroundSharedSessions.js +74 -14
- package/extension/src/backgroundUpdateCoordinator.js +118 -21
- package/extension/src/backgroundUpdateSafety.js +173 -0
- package/extension/src/content/activeTurnControl.js +8 -2
- package/extension/src/content/applyResultFormatters.js +46 -73
- package/extension/src/content/assetTransferBroker.js +72 -14
- package/extension/src/content/composerAttachments.js +93 -45
- package/extension/src/content/composerPanel.js +18 -0
- package/extension/src/content/contentRuntime.js +661 -676
- package/extension/src/content/diagnosticsController.js +122 -96
- package/extension/src/content/diagnosticsExport.js +19 -5
- package/extension/src/content/diagnosticsPanel.js +30 -0
- package/extension/src/content/directoryCleanupRecovery.js +54 -0
- package/extension/src/content/generated/content.bundle.js +6879 -2786
- package/extension/src/content/generated/content.bundle.meta.json +11 -3
- package/extension/src/content/globalPreferencesController.js +1 -1
- package/extension/src/content/legacyGlobalRegistry.js +6 -0
- package/extension/src/content/localSkillsPanel.js +102 -12
- package/extension/src/content/markdownText.js +5 -6
- package/extension/src/content/modelPicker.js +7 -4
- package/extension/src/content/nativeCompatibilityController.js +45 -20
- package/extension/src/content/otWarmMirror.js +4 -5
- package/extension/src/content/otWarmMirrorController.js +17 -20
- package/extension/src/content/pageBridgeClient.js +15 -2
- package/extension/src/content/panelMaintenance.js +40 -12
- package/extension/src/content/panelRenderer.js +6 -0
- package/extension/src/content/projectProviderSelection.js +13 -4
- package/extension/src/content/projectSessionCleanup.js +109 -62
- package/extension/src/content/projectSettingsCoordinator.js +39 -2
- package/extension/src/content/providerSettingsCoordinator.js +46 -8
- package/extension/src/content/providerSettingsDialog.js +71 -13
- package/extension/src/content/recentProjects.js +11 -5
- package/extension/src/content/runActivitySummary.js +40 -6
- package/extension/src/content/runCompletionMeta.js +344 -0
- package/extension/src/content/runController.js +37 -14
- package/extension/src/content/runFailureNotice.js +21 -12
- package/extension/src/content/runGuidanceController.js +72 -27
- package/extension/src/content/runGuidanceView.js +59 -10
- package/extension/src/content/runQueueScheduler.js +148 -42
- package/extension/src/content/runRecoveryActions.js +146 -0
- package/extension/src/content/runResultActions.js +8 -125
- package/extension/src/content/runReviewController.js +574 -0
- package/extension/src/content/runScrollLayout.js +21 -2
- package/extension/src/content/runTimelineView.js +146 -153
- package/extension/src/content/scopedPersistencePanelState.js +51 -6
- package/extension/src/content/selectionContextView.js +1 -1
- package/extension/src/content/sensitivePreflight.js +140 -0
- package/extension/src/content/sessionForkController.js +80 -17
- package/extension/src/content/sessionManager.js +1 -1
- package/extension/src/content/settingsPanel.js +7 -3
- package/extension/src/content/settingsWorkbench.js +5 -0
- package/extension/src/content/sharedSessionViewSync.js +45 -8
- package/extension/src/content/updateIdle.js +4 -1
- package/extension/src/content/updateNotice.js +60 -8
- package/extension/src/content/updateSafetyReceiver.js +58 -0
- package/extension/src/content/writebackController.js +53 -4
- package/extension/src/content/writebackOrchestrator.js +82 -73
- package/extension/src/content/writingStyleSettings.js +35 -11
- package/extension/src/page/binaryAssetUploader.js +136 -14
- package/extension/src/page/fileMover.js +256 -0
- 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/textFileCreator.js +105 -25
- 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 +80 -79
- package/extension/src/pageBridge.js +19 -3
- package/extension/src/popup.js +62 -10
- package/extension/src/shared/agentTranscript.js +23 -0
- package/extension/src/shared/auditRecords.js +2 -0
- package/extension/src/shared/compatibility.js +22 -6
- package/extension/src/shared/compileAdapter.js +79 -57
- package/extension/src/shared/composerAttachmentDraft.js +151 -0
- package/extension/src/shared/directoryCleanupProof.js +43 -0
- package/extension/src/shared/failureReasons.js +309 -3
- package/extension/src/shared/globalPreferences.js +30 -13
- package/extension/src/shared/governanceRules.js +4 -37
- package/extension/src/shared/i18n.js +298 -2
- 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/projectFiles.js +21 -44
- package/extension/src/shared/providerProfiles.js +5 -3
- package/extension/src/shared/runInputQueue.js +48 -5
- package/extension/src/shared/selectionContext.js +12 -3
- package/extension/src/shared/sessionState.js +72 -27
- package/extension/src/shared/settlementFacts.js +10 -2
- package/extension/src/shared/sharedSessionBridge.js +37 -14
- package/extension/src/shared/sharedSessionTransfer.js +140 -0
- package/extension/src/shared/staleGuard.js +118 -13
- package/extension/src/shared/storageDb.js +58 -26
- package/extension/src/shared/storageMigration.js +71 -111
- package/extension/src/shared/storageRunActions.js +209 -26
- package/extension/src/shared/subagentActivityModel.js +1 -1
- package/extension/src/shared/undoOperations.js +136 -13
- package/extension/src/shared/updateConsent.js +6 -4
- package/extension/src/shared/updateRuntimeIdentity.js +1 -1
- package/extension/src/shared/writebackIntent.js +16 -0
- package/extension/src/shared/writebackOperationEvidence.js +4 -15
- package/extension/src/shared/writebackSettlement.js +38 -38
- package/extension/styles/panel.css +19 -5
- 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 +233 -0
- package/native-host/src/bootstrapUpdateWatchdog.js +44 -0
- package/native-host/src/chatBridgeResponse.js +10 -0
- package/native-host/src/chatCompletionsBridge.js +21 -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 +31 -12
- package/native-host/src/codexProviderTest.js +24 -26
- package/native-host/src/codexSessionRunner.js +110 -183
- 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/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 +77 -54
- package/native-host/src/tomlStatements.js +140 -0
- package/native-host/src/updateManager.js +297 -39
- package/native-host/src/writingStyleRuntime.js +57 -29
- package/native-host/src/writingStyleStore.js +73 -0
- package/package.json +1 -1
- package/scripts/install-managed.mjs +1 -0
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
(function (root, factory) {
|
|
2
|
-
const api = factory(
|
|
2
|
+
const api = factory(typeof module === 'object' && module.exports
|
|
3
|
+
? require('./shared/sharedSessionTransfer') : root.CodexOverleafSharedSessionTransfer);
|
|
3
4
|
if (typeof module === 'object' && module.exports) module.exports = api;
|
|
4
5
|
if (root) root.CodexOverleafSharedSessionsBackground = api;
|
|
5
|
-
})(typeof globalThis !== 'undefined' ? globalThis : this, function () {
|
|
6
|
+
})(typeof globalThis !== 'undefined' ? globalThis : this, function (Transfer) {
|
|
6
7
|
'use strict';
|
|
7
8
|
const MESSAGE = 'codex-overleaf/shared-sessions-v1';
|
|
8
9
|
const LOCK_PORT = 'codex-overleaf/shared-session-lock-v1';
|
|
10
|
+
const REVIEW_LEASE_PREFIX = 'codex-overleaf-review-lease-v1:';
|
|
9
11
|
const CHANGE_KEY = 'codex-overleaf-shared-sessions-changed-v1';
|
|
10
12
|
const HOSTS = ['overleaf.com', 'www.overleaf.com', 'cn.overleaf.com'];
|
|
11
13
|
const META_PREFIX = 'codex-overleaf-scoped-persistence-v1:';
|
|
@@ -20,10 +22,11 @@
|
|
|
20
22
|
return value;
|
|
21
23
|
}
|
|
22
24
|
function scope(input) {
|
|
23
|
-
return { accountScopeId: part(input?.accountScopeId), projectId: typeof input?.projectId === 'string' ? input.projectId : '' };
|
|
25
|
+
return { accountScopeId: part(input?.accountScopeId), projectId: typeof input?.projectId === 'string' ? input.projectId : '', ...(input?.runId ? { runId: part(input.runId) } : {}) };
|
|
24
26
|
}
|
|
25
27
|
function keyFor(kind, value) {
|
|
26
|
-
return kind === '
|
|
28
|
+
return kind === 'review' ? 'review:' + JSON.stringify([value.accountScopeId, part(value.projectId), part(value.runId)])
|
|
29
|
+
: kind === 'preferences' ? 'preferences'
|
|
27
30
|
: 'state:' + encodeURIComponent(value.accountScopeId);
|
|
28
31
|
}
|
|
29
32
|
function metaKey(value) {
|
|
@@ -137,7 +140,7 @@
|
|
|
137
140
|
const stored = item.value;
|
|
138
141
|
if (value.projectId && stored.projectId !== value.projectId) { item.continue(); return; }
|
|
139
142
|
const record = { ...stored.record, sharedRevision: stored.revision };
|
|
140
|
-
const size = JSON.stringify(record)
|
|
143
|
+
const size = Transfer.byteLength(JSON.stringify(record));
|
|
141
144
|
if (result.length && (result.length >= 8 || bytes + size > MAX_BYTES)) {
|
|
142
145
|
resolve({ records: result, next: last }); return;
|
|
143
146
|
}
|
|
@@ -162,10 +165,9 @@
|
|
|
162
165
|
appliedOperations: [], trackedChangeCaptures: [], undoStatus: '' })) };
|
|
163
166
|
}
|
|
164
167
|
async function writeMany(message, value, importing, origin, sender) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
throw failure('shared_session_too_large', 'The session batch exceeds the shared storage transfer limit.');
|
|
168
|
+
// Reject a lost owner before doing any bounded decompression work.
|
|
169
|
+
ensureLock(message, value, sender);
|
|
170
|
+
const entries = (await Transfer.decode(message)).map(entry => ({ record: clone(entry.record), base: clone(entry.base) }));
|
|
169
171
|
for (const { record, base } of entries) {
|
|
170
172
|
if (!record || record.accountScopeId !== value.accountScopeId || record.projectId !== value.projectId)
|
|
171
173
|
throw failure('shared_store_scope_mismatch', 'The session does not belong to the captured account and project.');
|
|
@@ -197,6 +199,14 @@
|
|
|
197
199
|
putAt(entry.record, entry.base);
|
|
198
200
|
}
|
|
199
201
|
};
|
|
202
|
+
function appendResult(record, revision) {
|
|
203
|
+
// Merge may grow beyond either input. Reject inside this transaction,
|
|
204
|
+
// before returning success or committing any sibling record.
|
|
205
|
+
Transfer.validateRecord(record);
|
|
206
|
+
const result = { ...record, sharedRevision: revision };
|
|
207
|
+
Transfer.validateResponse([...results, result]);
|
|
208
|
+
results.push(result);
|
|
209
|
+
}
|
|
200
210
|
function putAt(candidate, base, copy = false) {
|
|
201
211
|
const key = [value.accountScopeId, candidate.id];
|
|
202
212
|
const tombstone = deleted.get(key);
|
|
@@ -214,7 +224,7 @@
|
|
|
214
224
|
let next = candidate;
|
|
215
225
|
if (importing && existing) {
|
|
216
226
|
if (equal(existing.record, candidate) || copy) {
|
|
217
|
-
|
|
227
|
+
appendResult(existing.record, existing.revision); return;
|
|
218
228
|
}
|
|
219
229
|
putAt(recoveryCopy(candidate, origin), null, true); return;
|
|
220
230
|
}
|
|
@@ -227,9 +237,9 @@
|
|
|
227
237
|
throw failure('shared_session_deleted', 'The session no longer exists.');
|
|
228
238
|
}
|
|
229
239
|
const revision = (existing?.revision || 0) + 1;
|
|
240
|
+
appendResult(next, revision);
|
|
230
241
|
store.put({ accountScopeId: value.accountScopeId, projectId: value.projectId,
|
|
231
242
|
id: next.id, record: next, revision });
|
|
232
|
-
results.push({ ...next, sharedRevision: revision });
|
|
233
243
|
} catch (error) { abort(error); }
|
|
234
244
|
};
|
|
235
245
|
};
|
|
@@ -264,9 +274,41 @@
|
|
|
264
274
|
}
|
|
265
275
|
await done;
|
|
266
276
|
}
|
|
277
|
+
const reviewLeaseKey = (key, token) => REVIEW_LEASE_PREFIX + key + ':' + token;
|
|
278
|
+
async function activeReviewLease(key) {
|
|
279
|
+
const prefix = REVIEW_LEASE_PREFIX + key + ':', records = await chromeApi.storage.local.get(null);
|
|
280
|
+
const active = [], expired = [];
|
|
281
|
+
for (const [id, lease] of Object.entries(records)) {
|
|
282
|
+
if (!id.startsWith(prefix)) continue;
|
|
283
|
+
const valid = lease && typeof lease.token === 'string' && lease.token.length > 0 && lease.token.length <= 256
|
|
284
|
+
&& id === reviewLeaseKey(key, lease.token) && Number.isSafeInteger(lease.deadlineAt) && lease.deadlineAt > 0;
|
|
285
|
+
if (!valid) throw failure('review_lease_invalid', 'Undo ownership storage could not be verified.');
|
|
286
|
+
if (lease.deadlineAt > Date.now()) active.push(lease); else expired.push(id);
|
|
287
|
+
}
|
|
288
|
+
// Immutable token rows can only be removed by their exact expired key.
|
|
289
|
+
await Promise.all(expired.map(id => chromeApi.storage.local.remove(id).catch(() => {})));
|
|
290
|
+
return active.sort((a, b) => b.deadlineAt - a.deadlineAt)[0];
|
|
291
|
+
}
|
|
267
292
|
async function handle(message, sender) {
|
|
268
293
|
const origin = senderOrigin(sender), value = scope(message.scope);
|
|
269
294
|
const method = message.method;
|
|
295
|
+
if (method === 'review-dispatched') {
|
|
296
|
+
const key = keyFor('review', { ...value, runId: part(message.runId) }), held = locks.get(key);
|
|
297
|
+
if (!held || held.disconnected || held.token !== message.reviewToken || held.tabId !== sender.tab.id)
|
|
298
|
+
throw failure('storage_lock_lost', 'The shared Undo owner was lost before dispatch.');
|
|
299
|
+
const deadlineAt = held.deadlineAt || (held.deadlineAt = Date.now() + 120000);
|
|
300
|
+
held.inFlight += 1;
|
|
301
|
+
try {
|
|
302
|
+
await chromeApi.storage.local.set({ [reviewLeaseKey(key, held.token)]: { token: held.token, deadlineAt } });
|
|
303
|
+
if (held.disconnected) throw failure('storage_lock_lost', 'The shared Undo owner disconnected before dispatch.');
|
|
304
|
+
return { deadlineAt };
|
|
305
|
+
} finally { held.inFlight -= 1; if (held.disconnected && !held.inFlight) held.release(); }
|
|
306
|
+
}
|
|
307
|
+
if (method === 'review-owner') {
|
|
308
|
+
const key = keyFor('review', { ...value, runId: part(message.runId) });
|
|
309
|
+
const lease = await activeReviewLease(key);
|
|
310
|
+
return { busy: locks.has(key) || lease?.deadlineAt > Date.now(), deadlineAt: lease?.deadlineAt || 0 };
|
|
311
|
+
}
|
|
270
312
|
if (method === 'list') return list(value, typeof message.after === 'string' ? message.after : '');
|
|
271
313
|
if (method === 'get') {
|
|
272
314
|
const record = await readOne('sessions', [value.accountScopeId, part(message.id)]);
|
|
@@ -284,7 +326,7 @@
|
|
|
284
326
|
tx.objectStore('migrations').put({ ...value, origin, complete: true, at: Date.now() });
|
|
285
327
|
await done; return { complete: true };
|
|
286
328
|
}
|
|
287
|
-
if (method === 'put' || method === 'import') {
|
|
329
|
+
if (method === 'put' || method === 'put-compressed' || method === 'import') {
|
|
288
330
|
const result = await writeMany(message, value, method === 'import', origin, sender);
|
|
289
331
|
if (result.length) await notify(value, message.clientId);
|
|
290
332
|
return result;
|
|
@@ -327,12 +369,30 @@
|
|
|
327
369
|
queues.delete(key);
|
|
328
370
|
}
|
|
329
371
|
port.onMessage.addListener(message => {
|
|
372
|
+
if (message?.type === 'release-review') {
|
|
373
|
+
const held = locks.get(heldKey);
|
|
374
|
+
if (held?.port !== port || held.token !== message.token || !heldKey.startsWith('review:')) return;
|
|
375
|
+
held.inFlight += 1;
|
|
376
|
+
void chromeApi.storage.local.remove(reviewLeaseKey(heldKey, held.token)).then(() => {
|
|
377
|
+
held.inFlight -= 1; held.release(); if (!closed) port.postMessage({ type: 'released' });
|
|
378
|
+
}, () => { held.inFlight -= 1; port.disconnect(); if (held.disconnected && !held.inFlight) held.release(); });
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
330
381
|
if (message?.type === 'ping') { port.postMessage({ type: 'pong' }); return; }
|
|
331
382
|
if (message?.type !== 'acquire' || job) return;
|
|
332
383
|
try {
|
|
333
|
-
const value = scope(message.scope), kind = message.kind === 'preferences' ? 'preferences' : 'state';
|
|
384
|
+
const value = scope(message.scope), kind = message.kind === 'review' ? 'review' : message.kind === 'preferences' ? 'preferences' : 'state';
|
|
334
385
|
heldKey = keyFor(kind, value);
|
|
335
|
-
|
|
386
|
+
if (kind === 'review' && locks.has(heldKey)) { port.postMessage({ type: 'busy' }); return; }
|
|
387
|
+
job = { port, tabId: port.sender.tab.id, closed: () => closed, grant: token => {
|
|
388
|
+
if (kind !== 'review') { port.postMessage({ type: 'granted', token }); return; }
|
|
389
|
+
const held = locks.get(heldKey); held.inFlight += 1;
|
|
390
|
+
void (async () => {
|
|
391
|
+
const existing = await activeReviewLease(heldKey);
|
|
392
|
+
if (existing?.deadlineAt > Date.now()) { locks.get(heldKey)?.release(); if (!closed) port.postMessage({ type: 'busy' }); return; }
|
|
393
|
+
if (!closed) port.postMessage({ type: 'granted', token });
|
|
394
|
+
})().catch(() => port.disconnect()).finally(() => { held.inFlight -= 1; if (held.disconnected && !held.inFlight) held.release(); });
|
|
395
|
+
} };
|
|
336
396
|
if (!queues.has(heldKey)) queues.set(heldKey, []);
|
|
337
397
|
queues.get(heldKey).push(job);
|
|
338
398
|
pump(heldKey);
|
|
@@ -98,6 +98,7 @@
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
async function startup() {
|
|
101
|
+
if (!await waitForBootstrapOwnership()) return;
|
|
101
102
|
chrome.alarms?.create?.(CHECK_ALARM, {
|
|
102
103
|
delayInMinutes: 0.5,
|
|
103
104
|
periodInMinutes: CHECK_INTERVAL_MINUTES
|
|
@@ -170,15 +171,14 @@
|
|
|
170
171
|
operation.phase = 'checking';
|
|
171
172
|
await setUpdateState({ ...currentState, state: 'checking', operationId: operation.id,
|
|
172
173
|
cancelRequested: false, initiatedBy: manual ? 'manual' : 'automatic',
|
|
173
|
-
code: '', message: '', blocker: '', blockers: [], postponeUntil: LEGACY_GUARD }, { operation });
|
|
174
|
+
code: '', message: '', blocker: '', blockers: [], cooldownUntil: 0, postponeUntil: LEGACY_GUARD }, { operation });
|
|
174
175
|
try {
|
|
175
176
|
const result = await withTimeout(requestNative('update.check', {
|
|
176
177
|
operationId: operation.id, currentVersion: currentVersion(), etag: currentState.etag || ''
|
|
177
178
|
}), CHECK_REQUEST_TIMEOUT_MS, codedError('update_check_timeout', 'The managed update check did not complete in time.'));
|
|
178
179
|
assertOperation(operation);
|
|
179
180
|
const checkedAt = Date.now();
|
|
180
|
-
if (result.available
|
|
181
|
-
&& policy.compareStableVersions(currentState.latestVersion, currentVersion()) > 0)) {
|
|
181
|
+
if (result.available) {
|
|
182
182
|
const latestVersion = result.latestVersion || currentState.latestVersion;
|
|
183
183
|
await setConsentState({ ...consent,
|
|
184
184
|
...(latestVersion !== consent.snoozedVersion ? { snoozedVersion: '', snoozedUntil: 0 } : {}),
|
|
@@ -188,22 +188,30 @@
|
|
|
188
188
|
await setUpdateState({ ...currentState, state: 'update_available', operationId: operation.id,
|
|
189
189
|
currentVersion: result.currentVersion || currentVersion(), latestVersion,
|
|
190
190
|
managed: true, etag: result.etag || currentState.etag || '', lastCheckedAt: checkedAt,
|
|
191
|
-
postponeUntil: LEGACY_GUARD, code: '', message: '', cancelRequested: false
|
|
191
|
+
postponeUntil: LEGACY_GUARD, code: '', message: '', cooldownUntil: 0, manualReason: {}, cancelRequested: false
|
|
192
192
|
}, { operation });
|
|
193
|
-
} else if (result.reason === 'manual_install_required'
|
|
194
|
-
&& policy.compareStableVersions(result.latestVersion, currentVersion()) > 0)
|
|
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()))) {
|
|
195
197
|
await setUpdateState({ ...currentState, state: 'manual_install_required', operationId: operation.id,
|
|
196
198
|
currentVersion: result.currentVersion || currentVersion(), latestVersion: result.latestVersion,
|
|
197
199
|
managed: true, etag: result.etag || currentState.etag || '', lastCheckedAt: checkedAt,
|
|
198
|
-
postponeUntil: LEGACY_GUARD, code:
|
|
200
|
+
postponeUntil: LEGACY_GUARD, code: result.reason === 'prerelease_promotion_requires_manual_update'
|
|
201
|
+
? result.reason : 'update_bootstrap_upgrade_required', message: '', cooldownUntil: 0,
|
|
199
202
|
manualReason: result.manualInstall?.reason || {}, cancelRequested: false
|
|
200
203
|
}, { operation });
|
|
201
204
|
} else {
|
|
202
205
|
await setUpdateState({ ...currentState, state: 'idle', operationId: operation.id,
|
|
203
206
|
currentVersion: result.currentVersion || currentVersion(),
|
|
204
207
|
latestVersion: result.latestVersion || result.currentVersion || currentVersion(),
|
|
205
|
-
|
|
206
|
-
|
|
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
|
|
207
215
|
}, { operation });
|
|
208
216
|
}
|
|
209
217
|
return getView();
|
|
@@ -557,6 +565,7 @@
|
|
|
557
565
|
function reconcileRecoveryState() {
|
|
558
566
|
if (recoveryPromise) return recoveryPromise;
|
|
559
567
|
recoveryPromise = (async () => {
|
|
568
|
+
if (!await waitForBootstrapOwnership()) return getUpdateState();
|
|
560
569
|
const state = await getUpdateState();
|
|
561
570
|
if (state.recoveryPending) return reconcileExpiredPhase();
|
|
562
571
|
if (state.cancelRequested) return postponeUpdate();
|
|
@@ -584,7 +593,9 @@
|
|
|
584
593
|
|
|
585
594
|
|
|
586
595
|
async function reconcileExpiredPhase() {
|
|
587
|
-
const
|
|
596
|
+
const recoveryOwner = await captureRecoveryOwner();
|
|
597
|
+
const state = recoveryOwner.state;
|
|
598
|
+
const recoveryOptions = { observed: true, recoveryOwner };
|
|
588
599
|
if (!state.recoveryPending && (!state.deadlineAt || Date.now() <= state.deadlineAt
|
|
589
600
|
|| policy.isTerminalState(state.state))) return state;
|
|
590
601
|
if (['checking', 'downloading'].includes(state.state)) {
|
|
@@ -597,32 +608,38 @@
|
|
|
597
608
|
try {
|
|
598
609
|
let status = await withTimeout(requestNative('update.status'), 10000,
|
|
599
610
|
codedError('update_status_timeout', 'Update recovery status is unavailable.'));
|
|
611
|
+
await assertRecoveryOwner(recoveryOwner);
|
|
600
612
|
let transaction = status.transaction;
|
|
601
613
|
if (state.transactionId && transaction?.id !== state.transactionId) {
|
|
602
614
|
throw codedError('update_transaction_mismatch', 'The update recovery transaction could not be matched.');
|
|
603
615
|
}
|
|
616
|
+
if (transaction?.state === 'superseded') {
|
|
617
|
+
return settleSupersededTransaction(status, recoveryOwner);
|
|
618
|
+
}
|
|
604
619
|
if (transaction?.state === 'staged' || (!transaction && status.installedAligned
|
|
605
620
|
&& status.activeVersion === state.currentVersion)) {
|
|
606
621
|
if (state.operationId) await requestNative('update.cancel', { operationId: state.operationId });
|
|
622
|
+
await assertRecoveryOwner(recoveryOwner);
|
|
607
623
|
const consent = await getConsentState();
|
|
608
624
|
const authorizationId = consent.authorizationId || consent.revokingAuthorizationId;
|
|
609
625
|
const targetVersion = consent.authorizedVersion || consent.revokingVersion || state.latestVersion;
|
|
626
|
+
await assertRecoveryOwner(recoveryOwner);
|
|
610
627
|
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,
|
|
628
|
+
const completed = await setUpdateAndConsentState({ state: 'failed', recoveryPending: false, cancelRequested: false,
|
|
615
629
|
operationId: '', code: 'update_not_applied', message: 'The update was stopped before installation. Retry when ready.'
|
|
616
|
-
}, {
|
|
630
|
+
}, revocation.clear({ ...consent, authorizedVersion: '', authorizationId: '', authorizedAt: 0 }), recoveryOptions);
|
|
631
|
+
return completed.updateState;
|
|
617
632
|
}
|
|
618
633
|
if (transaction?.state === 'awaiting_health') {
|
|
619
634
|
const rolledBack = await requestNative('update.rollback', {
|
|
620
635
|
transactionId: transaction.id, expectedState: 'awaiting_health', reasonCode: 'update_health_timeout'
|
|
621
636
|
});
|
|
637
|
+
await assertRecoveryOwner(recoveryOwner);
|
|
622
638
|
if (rolledBack.state !== 'rolled_back' || rolledBack.version !== transaction.sourceVersion) {
|
|
623
639
|
throw codedError('update_rollback_unconfirmed', 'Rollback returned incomplete evidence.');
|
|
624
640
|
}
|
|
625
641
|
status = await requestNative('update.status');
|
|
642
|
+
await assertRecoveryOwner(recoveryOwner);
|
|
626
643
|
transaction = status.transaction;
|
|
627
644
|
}
|
|
628
645
|
if (transaction?.state === 'committed' && status.installedAligned
|
|
@@ -630,7 +647,7 @@
|
|
|
630
647
|
return setUpdateState({ state: 'committed', currentVersion: transaction.targetVersion,
|
|
631
648
|
latestVersion: transaction.targetVersion, transactionId: transaction.id,
|
|
632
649
|
recoveryPending: false, cancelRequested: false, code: '', message: ''
|
|
633
|
-
},
|
|
650
|
+
}, recoveryOptions);
|
|
634
651
|
}
|
|
635
652
|
if (transaction?.state === 'rolled_back' && status.installedAligned
|
|
636
653
|
&& status.activeVersion === transaction.sourceVersion
|
|
@@ -638,22 +655,88 @@
|
|
|
638
655
|
const restored = await setUpdateState({ state: 'rolled_back', currentVersion: transaction.sourceVersion,
|
|
639
656
|
transactionId: transaction.id, recoveryPending: false, cancelRequested: false,
|
|
640
657
|
code: transaction.reasonCode || 'update_rolled_back', message: 'The previous version has been restored.'
|
|
641
|
-
},
|
|
658
|
+
}, recoveryOptions);
|
|
642
659
|
setTimeout(() => chrome.runtime.reload(), 0);
|
|
643
660
|
return restored;
|
|
644
661
|
}
|
|
645
662
|
throw codedError('update_recovery_pending', 'The installed update result has not been confirmed.');
|
|
646
663
|
} catch (error) {
|
|
664
|
+
if (error.code === 'update_recovery_superseded') return getUpdateState();
|
|
647
665
|
return setUpdateState({ state: 'failed', recoveryPending: true, cancelRequested: false,
|
|
648
666
|
code: safeCode(error), message: safeMessage(error)
|
|
649
|
-
},
|
|
667
|
+
}, recoveryOptions).catch(writeError => {
|
|
668
|
+
if (writeError.code === 'update_recovery_superseded') return getUpdateState();
|
|
669
|
+
throw writeError;
|
|
670
|
+
});
|
|
650
671
|
}
|
|
651
672
|
}
|
|
652
673
|
return state;
|
|
653
674
|
}
|
|
654
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;
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
|
|
655
737
|
|
|
656
738
|
async function handleObservedStateChange() {
|
|
739
|
+
if (!await waitForBootstrapOwnership()) return;
|
|
657
740
|
await settleTerminalConsent();
|
|
658
741
|
const [state, consent] = await Promise.all([getUpdateState(), getConsentState()]);
|
|
659
742
|
if (!consent.authorizationId && !policy.isExecutionState(state.state)) {
|
|
@@ -784,9 +867,11 @@
|
|
|
784
867
|
function setUpdateState(value, options = {}) {
|
|
785
868
|
return enqueueStateWrite(async () => {
|
|
786
869
|
assertWriteOwner(options);
|
|
787
|
-
const stored = await chrome.storage.local.get(
|
|
870
|
+
const stored = await chrome.storage.local.get(options.recoveryOwner
|
|
871
|
+
? [UPDATE_STATE_KEY, CONSENT_STATE_KEY] : UPDATE_STATE_KEY);
|
|
788
872
|
assertWriteOwner(options);
|
|
789
873
|
const previous = policy.normalizeUpdateState(stored?.[UPDATE_STATE_KEY], currentVersion());
|
|
874
|
+
assertRecoverySnapshot(options.recoveryOwner, previous, policy.normalizeConsentState(stored?.[CONSENT_STATE_KEY]));
|
|
790
875
|
const next = projectUpdateState(previous, value, options);
|
|
791
876
|
await chrome.storage.local.set({ [UPDATE_STATE_KEY]: next });
|
|
792
877
|
return next;
|
|
@@ -844,6 +929,7 @@
|
|
|
844
929
|
const stored = await chrome.storage.local.get([UPDATE_STATE_KEY, CONSENT_STATE_KEY]);
|
|
845
930
|
assertWriteOwner(options);
|
|
846
931
|
const previous = policy.normalizeUpdateState(stored?.[UPDATE_STATE_KEY], currentVersion());
|
|
932
|
+
assertRecoverySnapshot(options.recoveryOwner, previous, policy.normalizeConsentState(stored?.[CONSENT_STATE_KEY]));
|
|
847
933
|
const updateState = projectUpdateState(previous, updateValue, options);
|
|
848
934
|
const consentState = policy.normalizeConsentState(consentValue);
|
|
849
935
|
await chrome.storage.local.set({ [UPDATE_STATE_KEY]: updateState, [CONSENT_STATE_KEY]: consentState });
|
|
@@ -869,12 +955,20 @@
|
|
|
869
955
|
|
|
870
956
|
function enqueuePolicyAction(action) {
|
|
871
957
|
const generation = operationGeneration;
|
|
872
|
-
const guarded = () =>
|
|
958
|
+
const guarded = async () => !await waitForBootstrapOwnership()
|
|
959
|
+
|| generation !== operationGeneration || cancellationPromise ? getView() : action();
|
|
873
960
|
const result = policyTail.then(guarded, guarded);
|
|
874
961
|
policyTail = result.catch(() => {});
|
|
875
962
|
return result;
|
|
876
963
|
}
|
|
877
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
|
+
|
|
878
972
|
function beginOperation(id) {
|
|
879
973
|
const operation = { id: id || crypto.randomUUID(), generation: ++operationGeneration, phase: 'checking' };
|
|
880
974
|
activeOperation = operation;
|
|
@@ -894,6 +988,9 @@
|
|
|
894
988
|
}
|
|
895
989
|
|
|
896
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
|
+
}
|
|
897
994
|
if (options.operation) assertOperation(options.operation);
|
|
898
995
|
if (options.generation !== undefined && options.generation !== operationGeneration) {
|
|
899
996
|
throw codedError('update_network_cancelled', 'The update request was superseded.');
|
|
@@ -970,5 +1067,5 @@
|
|
|
970
1067
|
.slice(0, 300);
|
|
971
1068
|
}
|
|
972
1069
|
|
|
973
|
-
root.CodexOverleafUpdateCoordinator = Object.freeze({ init });
|
|
1070
|
+
root.CodexOverleafUpdateCoordinator = Object.freeze({ init, captureRecoveryOwner, settleSupersededTransaction });
|
|
974
1071
|
})(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);
|