codex-overleaf-link 1.9.5 → 1.9.7

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.
@@ -0,0 +1,334 @@
1
+ (() => {
2
+ 'use strict';
3
+
4
+ const MESSAGE_TYPES = Object.freeze({
5
+ get: 'codex-overleaf/consent-update-get-state',
6
+ check: 'codex-overleaf/consent-update-check',
7
+ install: 'codex-overleaf/consent-update-install',
8
+ later: 'codex-overleaf/consent-update-later',
9
+ retry: 'codex-overleaf/consent-update-check'
10
+ });
11
+ const ACTIVE_PHASES = new Set([
12
+ 'checking',
13
+ 'downloading',
14
+ 'staged',
15
+ 'waiting',
16
+ 'waiting_for_idle',
17
+ 'waiting_for_safe_point',
18
+ 'applying',
19
+ 'awaiting_health',
20
+ 'awaiting_health_check',
21
+ 'health_checking'
22
+ ]);
23
+
24
+ const elements = {
25
+ badge: document.getElementById('status-badge'),
26
+ title: document.getElementById('update-title'),
27
+ route: document.getElementById('version-route'),
28
+ detail: document.getElementById('update-detail'),
29
+ progressTrack: document.getElementById('progress-track'),
30
+ progressFill: document.getElementById('progress-fill'),
31
+ stages: Array.from(document.querySelectorAll('#update-stages > li')),
32
+ note: document.getElementById('persistence-note'),
33
+ details: document.getElementById('technical-details'),
34
+ technicalMessage: document.getElementById('technical-message'),
35
+ actions: document.getElementById('update-actions')
36
+ };
37
+
38
+ let currentView = {};
39
+ let pollTimer = null;
40
+ let refreshTimer = null;
41
+ let actionPending = false;
42
+
43
+ function asObject(value) {
44
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
45
+ }
46
+
47
+ function unwrapResponse(response) {
48
+ const root = asObject(response) || {};
49
+ if (root.ok === false) {
50
+ const error = new Error(root.error?.message || root.message || 'Update action failed.');
51
+ error.code = root.error?.code || root.code || 'update_failed';
52
+ throw error;
53
+ }
54
+ const candidates = [
55
+ root.view,
56
+ root.result?.view,
57
+ root.result,
58
+ root.updateConsent,
59
+ root.updateState,
60
+ root.state
61
+ ];
62
+ return candidates.find(asObject) || root;
63
+ }
64
+
65
+ async function request(action) {
66
+ const type = MESSAGE_TYPES[action];
67
+ if (!type) throw new Error(`Unsupported update action: ${action}`);
68
+ return unwrapResponse(await chrome.runtime.sendMessage({ type }));
69
+ }
70
+
71
+ function phaseOf(view) {
72
+ const value = view.phase || view.status || view.state || view.lifecycle || 'idle';
73
+ return typeof value === 'string' ? value.toLowerCase().replace(/[- ]+/g, '_') : 'idle';
74
+ }
75
+
76
+ function versionOf(view, candidates, fallback = '') {
77
+ for (const key of candidates) {
78
+ const value = view[key];
79
+ if (typeof value === 'string' && value.trim()) return value.trim().replace(/^v/i, '');
80
+ }
81
+ return fallback;
82
+ }
83
+
84
+ function presentationFor(view) {
85
+ const phase = phaseOf(view);
86
+ const presentations = {
87
+ idle: ['Stable updates', 'Automatically checks signed stable releases.', 'Ready', 'neutral'],
88
+ up_to_date: ['Everything is up to date', 'Extension and Native Host are on the latest stable release.', 'Up to date', 'success'],
89
+ checking: ['Checking for updates', 'Reading the latest signed stable release.', 'Checking', 'active'],
90
+ update_available: ['A stable update is ready', 'Review the target version, then update when convenient.', 'Available', 'success'],
91
+ downloading: ['Downloading the signed release', 'The package is being fetched and verified before any files change.', 'Downloading', 'active'],
92
+ staged: ['Update verified', 'The signed package is ready and waiting for a safe installation point.', 'Verified', 'active'],
93
+ waiting: ['Waiting for a safe point', 'Active Overleaf work will finish before the runtime changes.', 'Waiting', 'warning'],
94
+ waiting_for_idle: ['Waiting for a safe point', 'Active Overleaf work will finish before the runtime changes.', 'Waiting', 'warning'],
95
+ waiting_for_safe_point: ['Waiting for a safe point', 'Active Overleaf work will finish before the runtime changes.', 'Waiting', 'warning'],
96
+ applying: ['Installing the update', 'Extension and Native Host are being switched as one managed unit.', 'Installing', 'active'],
97
+ awaiting_health: ['Verifying the installation', 'The updated components are restarting and reconnecting.', 'Verifying', 'active'],
98
+ awaiting_health_check: ['Verifying the installation', 'The updated components are restarting and reconnecting.', 'Verifying', 'active'],
99
+ health_checking: ['Verifying the installation', 'The updated components are restarting and reconnecting.', 'Verifying', 'active'],
100
+ committed: ['Update complete', 'Extension and Native Host passed the post-update health check.', 'Complete', 'success'],
101
+ failed: ['Update could not continue', 'No further update steps will run until the failure is retried.', 'Needs attention', 'error'],
102
+ rolled_back: ['Previous version restored', 'The health check failed, so the managed update was rolled back safely.', 'Rolled back', 'warning'],
103
+ deferred: ['Update paused', 'The available update will remain ready for a later installation.', 'Later', 'neutral']
104
+ };
105
+ const [title, detail, badge, tone] = presentations[phase] || presentations.idle;
106
+ return { phase, title, detail, badge, tone };
107
+ }
108
+
109
+ function progressFor(view, phase) {
110
+ const raw = asObject(view.progress)?.percent ?? view.progressPercent ?? view.progress;
111
+ if (Number.isFinite(Number(raw))) {
112
+ const numeric = Number(raw);
113
+ return Math.max(0, Math.min(100, numeric <= 1 ? numeric * 100 : numeric));
114
+ }
115
+ const fallback = {
116
+ checking: 8,
117
+ update_available: 0,
118
+ downloading: 25,
119
+ staged: 48,
120
+ waiting: 55,
121
+ waiting_for_idle: 55,
122
+ waiting_for_safe_point: 55,
123
+ applying: 75,
124
+ awaiting_health: 90,
125
+ awaiting_health_check: 90,
126
+ health_checking: 90,
127
+ committed: 100,
128
+ rolled_back: 100
129
+ };
130
+ return fallback[phase] ?? 0;
131
+ }
132
+
133
+ function activeStageFor(phase) {
134
+ if (['downloading'].includes(phase)) return 0;
135
+ if (['staged', 'waiting', 'waiting_for_idle', 'waiting_for_safe_point'].includes(phase)) return 1;
136
+ if (['applying', 'awaiting_health', 'awaiting_health_check', 'health_checking'].includes(phase)) return 2;
137
+ if (phase === 'committed') return 3;
138
+ return -1;
139
+ }
140
+
141
+ function errorStageFor(view, phase) {
142
+ if (!['failed', 'rolled_back'].includes(phase)) return -1;
143
+ const stage = String(view.failureStage || view.error?.stage || '').toLowerCase();
144
+ if (/download|verify|signature|manifest/.test(stage)) return 0;
145
+ if (/idle|safe|wait/.test(stage)) return 1;
146
+ return 2;
147
+ }
148
+
149
+ function technicalText(view) {
150
+ const value = view.technicalDetail || view.error?.technicalDetail || view.error?.message || view.error || view.message;
151
+ if (!value) return '';
152
+ if (typeof value === 'string') return value;
153
+ try {
154
+ return JSON.stringify(value, null, 2);
155
+ } catch {
156
+ return String(value);
157
+ }
158
+ }
159
+
160
+ function actionDefinitions(phase) {
161
+ if (phase === 'update_available') {
162
+ return [
163
+ { id: 'later', label: 'Later', kind: 'quiet' },
164
+ { id: 'install', label: 'Update now', kind: 'primary' }
165
+ ];
166
+ }
167
+ if (['failed', 'rolled_back'].includes(phase)) {
168
+ return [
169
+ { id: 'close', label: 'Close', kind: 'quiet' },
170
+ { id: 'retry', label: 'Retry', kind: 'primary' }
171
+ ];
172
+ }
173
+ if (phase === 'committed') {
174
+ return [{ id: 'close', label: 'Done', kind: 'primary' }];
175
+ }
176
+ if (ACTIVE_PHASES.has(phase)) {
177
+ return [{ id: 'close', label: 'Close window', kind: 'quiet' }];
178
+ }
179
+ return [
180
+ { id: 'close', label: 'Close', kind: 'quiet' },
181
+ { id: 'check', label: 'Check latest', kind: 'primary' }
182
+ ];
183
+ }
184
+
185
+ function renderActions(phase) {
186
+ elements.actions.replaceChildren(...actionDefinitions(phase).map(action => {
187
+ const button = document.createElement('button');
188
+ button.type = 'button';
189
+ button.dataset.action = action.id;
190
+ button.dataset.kind = action.kind;
191
+ button.textContent = action.label;
192
+ button.disabled = actionPending;
193
+ return button;
194
+ }));
195
+ }
196
+
197
+ function render(view) {
198
+ currentView = asObject(view) || {};
199
+ const presentation = presentationFor(currentView);
200
+ const installed = versionOf(currentView, ['currentVersion', 'installedVersion', 'extensionVersion'], chrome.runtime.getManifest().version);
201
+ const target = versionOf(currentView, ['latestVersion', 'targetVersion', 'availableVersion'], 'latest stable');
202
+ const hasTarget = target && target !== 'latest stable' && target !== installed;
203
+ elements.route.textContent = hasTarget ? `v${installed} → v${target}` : `Installed v${installed}`;
204
+ elements.title.textContent = presentation.title;
205
+ elements.detail.textContent = typeof currentView.detail === 'string' && currentView.detail.trim()
206
+ ? currentView.detail.trim()
207
+ : presentation.detail;
208
+ elements.badge.textContent = presentation.badge;
209
+ elements.badge.dataset.tone = presentation.tone;
210
+
211
+ const progress = progressFor(currentView, presentation.phase);
212
+ const showProgress = ACTIVE_PHASES.has(presentation.phase) || ['committed', 'rolled_back'].includes(presentation.phase);
213
+ elements.progressTrack.hidden = !showProgress;
214
+ elements.progressTrack.dataset.indeterminate = presentation.phase === 'checking' ? 'true' : 'false';
215
+ elements.progressTrack.setAttribute('aria-valuenow', String(Math.round(progress)));
216
+ elements.progressFill.style.width = `${progress}%`;
217
+
218
+ const activeStage = activeStageFor(presentation.phase);
219
+ const errorStage = errorStageFor(currentView, presentation.phase);
220
+ elements.stages.forEach((stage, index) => {
221
+ let state = 'pending';
222
+ if (index < activeStage || activeStage === 3) state = 'done';
223
+ if (index === activeStage) state = 'active';
224
+ if (index === errorStage) state = 'error';
225
+ stage.dataset.state = state;
226
+ const marker = stage.querySelector('.stage-marker');
227
+ marker.textContent = state === 'done' ? '✓' : String(index + 1);
228
+ });
229
+
230
+ const restarting = ['applying', 'awaiting_health', 'awaiting_health_check', 'health_checking'].includes(presentation.phase);
231
+ elements.note.textContent = restarting
232
+ ? 'The extension may restart briefly. This window will reconnect and restore the saved progress.'
233
+ : 'This window can stay open while work continues elsewhere. Progress is saved automatically.';
234
+
235
+ const technical = technicalText(currentView);
236
+ elements.details.hidden = !technical || !['failed', 'rolled_back'].includes(presentation.phase);
237
+ elements.technicalMessage.textContent = technical;
238
+ renderActions(presentation.phase);
239
+ schedulePolling(presentation.phase);
240
+ }
241
+
242
+ function renderReconnecting() {
243
+ elements.title.textContent = 'Reconnecting after restart';
244
+ elements.detail.textContent = 'The saved update state will return when the extension is available again.';
245
+ elements.badge.textContent = 'Reconnecting';
246
+ elements.badge.dataset.tone = 'active';
247
+ elements.progressTrack.hidden = false;
248
+ elements.progressTrack.dataset.indeterminate = 'true';
249
+ elements.actions.replaceChildren();
250
+ }
251
+
252
+ function renderLocalFailure(error) {
253
+ render({
254
+ ...currentView,
255
+ phase: 'failed',
256
+ detail: error?.message || 'Update action failed.',
257
+ technicalDetail: `${error?.code || 'update_failed'}: ${error?.message || 'Update action failed.'}`
258
+ });
259
+ }
260
+
261
+ function isTransientContextError(error) {
262
+ return /context invalidated|receiving end does not exist|message port closed|could not establish connection/i.test(String(error?.message || error));
263
+ }
264
+
265
+ async function refresh() {
266
+ try {
267
+ render(await request('get'));
268
+ } catch (error) {
269
+ if (isTransientContextError(error)) {
270
+ renderReconnecting();
271
+ window.setTimeout(() => window.location.reload(), 900);
272
+ return;
273
+ }
274
+ renderLocalFailure(error);
275
+ }
276
+ }
277
+
278
+ function scheduleRefresh() {
279
+ window.clearTimeout(refreshTimer);
280
+ refreshTimer = window.setTimeout(refresh, 80);
281
+ }
282
+
283
+ function schedulePolling(phase) {
284
+ window.clearTimeout(pollTimer);
285
+ pollTimer = null;
286
+ if (!ACTIVE_PHASES.has(phase)) return;
287
+ pollTimer = window.setTimeout(async () => {
288
+ await refresh();
289
+ }, 1100);
290
+ }
291
+
292
+ async function runAction(action) {
293
+ if (actionPending) return;
294
+ if (action === 'close') {
295
+ window.close();
296
+ return;
297
+ }
298
+ actionPending = true;
299
+ renderActions(phaseOf(currentView));
300
+ try {
301
+ render(await request(action));
302
+ if (action === 'later') window.close();
303
+ } catch (error) {
304
+ if (isTransientContextError(error)) {
305
+ renderReconnecting();
306
+ window.setTimeout(() => window.location.reload(), 900);
307
+ } else {
308
+ renderLocalFailure(error);
309
+ }
310
+ } finally {
311
+ actionPending = false;
312
+ if (document.visibilityState !== 'unloaded') renderActions(phaseOf(currentView));
313
+ }
314
+ }
315
+
316
+ elements.actions.addEventListener('click', event => {
317
+ const button = event.target.closest('button[data-action]');
318
+ if (button) runAction(button.dataset.action);
319
+ });
320
+
321
+ try {
322
+ chrome.storage.onChanged.addListener((_changes, areaName) => {
323
+ if (areaName === 'local') scheduleRefresh();
324
+ });
325
+ } catch {
326
+ renderReconnecting();
327
+ }
328
+
329
+ const requestedAction = new URLSearchParams(window.location.search).get('action');
330
+ window.history.replaceState(null, '', window.location.pathname);
331
+ refresh().then(() => {
332
+ if (requestedAction && ['install', 'retry'].includes(requestedAction)) runAction(requestedAction);
333
+ });
334
+ })();
@@ -7,6 +7,7 @@
7
7
  const CHECK_INTERVAL_MINUTES = 24 * 60;
8
8
  const SNOOZE_MS = 24 * 60 * 60 * 1000;
9
9
  const CANDIDATE_MAX_AGE_MS = 5 * 60 * 1000;
10
+ const CHECK_REQUEST_TIMEOUT_MS = 15 * 1000;
10
11
  const LEGACY_GUARD = Number.MAX_SAFE_INTEGER;
11
12
  const OVERLEAF_MATCHES = [
12
13
  'https://www.overleaf.com/project/*',
@@ -16,7 +17,8 @@
16
17
  'codex-overleaf/consent-update-get-state',
17
18
  'codex-overleaf/consent-update-check',
18
19
  'codex-overleaf/consent-update-install',
19
- 'codex-overleaf/consent-update-later'
20
+ 'codex-overleaf/consent-update-later',
21
+ 'codex-overleaf/consent-update-dismiss'
20
22
  ]);
21
23
 
22
24
  const policy = root.CodexOverleafUpdateConsent;
@@ -60,6 +62,7 @@
60
62
  delayInMinutes: 0.5,
61
63
  periodInMinutes: CHECK_INTERVAL_MINUTES
62
64
  });
65
+ await recoverInterruptedCheck();
63
66
  await settleTerminalConsent();
64
67
  await armLegacyGuard();
65
68
  await publishView();
@@ -75,6 +78,8 @@
75
78
  return installUpdate();
76
79
  case 'codex-overleaf/consent-update-later':
77
80
  return postponeUpdate();
81
+ case 'codex-overleaf/consent-update-dismiss':
82
+ return dismissCompletedUpdate();
78
83
  default:
79
84
  throw codedError('unknown_update_action', 'Unknown update action.');
80
85
  }
@@ -95,10 +100,14 @@
95
100
  });
96
101
 
97
102
  try {
98
- const result = await requestNative('update.check', {
99
- currentVersion: currentVersion(),
100
- etag: currentState.etag || ''
101
- });
103
+ const result = await withTimeout(
104
+ requestNative('update.check', {
105
+ currentVersion: currentVersion(),
106
+ etag: currentState.etag || ''
107
+ }),
108
+ CHECK_REQUEST_TIMEOUT_MS,
109
+ codedError('update_check_timeout', 'The managed update check did not complete in time.')
110
+ );
102
111
  const checkedAt = Date.now();
103
112
  if (result.available) {
104
113
  const nextConsent = result.latestVersion !== consent.snoozedVersion
@@ -158,6 +167,41 @@
158
167
  }
159
168
  }
160
169
 
170
+ async function recoverInterruptedCheck() {
171
+ const state = await getUpdateState();
172
+ if (state.state !== 'checking') return;
173
+ const nextState = policy.compareStableVersions(state.latestVersion, currentVersion()) > 0
174
+ ? 'update_available'
175
+ : 'idle';
176
+ await setUpdateState({
177
+ ...state,
178
+ state: nextState,
179
+ blocker: '',
180
+ blockers: [],
181
+ code: '',
182
+ message: ''
183
+ });
184
+ }
185
+
186
+ async function dismissCompletedUpdate() {
187
+ const state = await getUpdateState();
188
+ if (state.state !== 'committed') return getView();
189
+ const version = state.latestVersion || state.currentVersion || currentVersion();
190
+ await setUpdateState({
191
+ ...state,
192
+ state: 'idle',
193
+ currentVersion: version,
194
+ latestVersion: version,
195
+ transactionId: '',
196
+ stagedAt: 0,
197
+ blocker: '',
198
+ blockers: [],
199
+ code: '',
200
+ message: ''
201
+ });
202
+ return getView();
203
+ }
204
+
161
205
  async function installUpdate() {
162
206
  let state = await getUpdateState();
163
207
  let consent = await getConsentState();
@@ -189,10 +233,11 @@
189
233
  await setUpdateState({ ...state, postponeUntil: 0, code: '', message: '' });
190
234
 
191
235
  try {
192
- const result = await chrome.runtime.sendMessage({ type: 'codex-overleaf/update-check-now' });
193
- if (result === undefined) {
236
+ const executor = root.CodexOverleafManagedUpdateExecutor;
237
+ if (typeof executor?.installAuthorizedUpdate !== 'function') {
194
238
  throw codedError('update_executor_unavailable', 'The managed update executor is unavailable.');
195
239
  }
240
+ await executor.installAuthorizedUpdate();
196
241
  return getView();
197
242
  } catch (error) {
198
243
  await bestEffortRevoke({
@@ -393,6 +438,22 @@
393
438
  return error;
394
439
  }
395
440
 
441
+ function withTimeout(promise, timeoutMs, timeoutError) {
442
+ return new Promise((resolve, reject) => {
443
+ const timer = setTimeout(() => reject(timeoutError), timeoutMs);
444
+ Promise.resolve(promise).then(
445
+ value => {
446
+ clearTimeout(timer);
447
+ resolve(value);
448
+ },
449
+ error => {
450
+ clearTimeout(timer);
451
+ reject(error);
452
+ }
453
+ );
454
+ });
455
+ }
456
+
396
457
  function safeError(error) {
397
458
  return { code: safeCode(error), message: safeMessage(error) };
398
459
  }