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.
Files changed (168) hide show
  1. package/README.md +38 -26
  2. package/README.zh-CN.md +39 -27
  3. package/extension/bootstrap/background.js +52 -9
  4. package/extension/bootstrap/manifest.template.json +1 -1
  5. package/extension/bootstrap/popup.html +1 -0
  6. package/extension/bootstrap/popupFallback.js +55 -0
  7. package/extension/bootstrap/updateStatus.js +1 -0
  8. package/extension/src/background.js +206 -53
  9. package/extension/src/backgroundBootstrapMigration.js +181 -0
  10. package/extension/src/backgroundSharedSessions.js +74 -14
  11. package/extension/src/backgroundUpdateCoordinator.js +118 -21
  12. package/extension/src/backgroundUpdateSafety.js +173 -0
  13. package/extension/src/content/activeTurnControl.js +8 -2
  14. package/extension/src/content/applyResultFormatters.js +46 -73
  15. package/extension/src/content/assetTransferBroker.js +72 -14
  16. package/extension/src/content/composerAttachments.js +93 -45
  17. package/extension/src/content/composerPanel.js +18 -0
  18. package/extension/src/content/contentRuntime.js +661 -676
  19. package/extension/src/content/diagnosticsController.js +122 -96
  20. package/extension/src/content/diagnosticsExport.js +19 -5
  21. package/extension/src/content/diagnosticsPanel.js +30 -0
  22. package/extension/src/content/directoryCleanupRecovery.js +54 -0
  23. package/extension/src/content/generated/content.bundle.js +6879 -2786
  24. package/extension/src/content/generated/content.bundle.meta.json +11 -3
  25. package/extension/src/content/globalPreferencesController.js +1 -1
  26. package/extension/src/content/legacyGlobalRegistry.js +6 -0
  27. package/extension/src/content/localSkillsPanel.js +102 -12
  28. package/extension/src/content/markdownText.js +5 -6
  29. package/extension/src/content/modelPicker.js +7 -4
  30. package/extension/src/content/nativeCompatibilityController.js +45 -20
  31. package/extension/src/content/otWarmMirror.js +4 -5
  32. package/extension/src/content/otWarmMirrorController.js +17 -20
  33. package/extension/src/content/pageBridgeClient.js +15 -2
  34. package/extension/src/content/panelMaintenance.js +40 -12
  35. package/extension/src/content/panelRenderer.js +6 -0
  36. package/extension/src/content/projectProviderSelection.js +13 -4
  37. package/extension/src/content/projectSessionCleanup.js +109 -62
  38. package/extension/src/content/projectSettingsCoordinator.js +39 -2
  39. package/extension/src/content/providerSettingsCoordinator.js +46 -8
  40. package/extension/src/content/providerSettingsDialog.js +71 -13
  41. package/extension/src/content/recentProjects.js +11 -5
  42. package/extension/src/content/runActivitySummary.js +40 -6
  43. package/extension/src/content/runCompletionMeta.js +344 -0
  44. package/extension/src/content/runController.js +37 -14
  45. package/extension/src/content/runFailureNotice.js +21 -12
  46. package/extension/src/content/runGuidanceController.js +72 -27
  47. package/extension/src/content/runGuidanceView.js +59 -10
  48. package/extension/src/content/runQueueScheduler.js +148 -42
  49. package/extension/src/content/runRecoveryActions.js +146 -0
  50. package/extension/src/content/runResultActions.js +8 -125
  51. package/extension/src/content/runReviewController.js +574 -0
  52. package/extension/src/content/runScrollLayout.js +21 -2
  53. package/extension/src/content/runTimelineView.js +146 -153
  54. package/extension/src/content/scopedPersistencePanelState.js +51 -6
  55. package/extension/src/content/selectionContextView.js +1 -1
  56. package/extension/src/content/sensitivePreflight.js +140 -0
  57. package/extension/src/content/sessionForkController.js +80 -17
  58. package/extension/src/content/sessionManager.js +1 -1
  59. package/extension/src/content/settingsPanel.js +7 -3
  60. package/extension/src/content/settingsWorkbench.js +5 -0
  61. package/extension/src/content/sharedSessionViewSync.js +45 -8
  62. package/extension/src/content/updateIdle.js +4 -1
  63. package/extension/src/content/updateNotice.js +60 -8
  64. package/extension/src/content/updateSafetyReceiver.js +58 -0
  65. package/extension/src/content/writebackController.js +53 -4
  66. package/extension/src/content/writebackOrchestrator.js +82 -73
  67. package/extension/src/content/writingStyleSettings.js +35 -11
  68. package/extension/src/page/binaryAssetUploader.js +136 -14
  69. package/extension/src/page/fileMover.js +256 -0
  70. package/extension/src/page/ownedDirectoryCleanup.js +94 -0
  71. package/extension/src/page/referenceProjects.js +1 -0
  72. package/extension/src/page/reviewContext.js +113 -0
  73. package/extension/src/page/saveState.js +6 -4
  74. package/extension/src/page/snapshotRouter.js +84 -39
  75. package/extension/src/page/textFileCreator.js +105 -25
  76. package/extension/src/page/trackedChangeCapture.js +12 -2
  77. package/extension/src/page/trackedChangeOwnership.js +19 -1
  78. package/extension/src/page/trackedChangeReplay.js +41 -21
  79. package/extension/src/page/trackedChangesLifecycle.js +217 -199
  80. package/extension/src/page/treeOperations.js +7 -10
  81. package/extension/src/page/updateSafetyProbe.js +41 -0
  82. package/extension/src/page/writebackReceiptJournal.js +2 -0
  83. package/extension/src/page/writebackRouter.js +80 -79
  84. package/extension/src/pageBridge.js +19 -3
  85. package/extension/src/popup.js +62 -10
  86. package/extension/src/shared/agentTranscript.js +23 -0
  87. package/extension/src/shared/auditRecords.js +2 -0
  88. package/extension/src/shared/compatibility.js +22 -6
  89. package/extension/src/shared/compileAdapter.js +79 -57
  90. package/extension/src/shared/composerAttachmentDraft.js +151 -0
  91. package/extension/src/shared/directoryCleanupProof.js +43 -0
  92. package/extension/src/shared/failureReasons.js +309 -3
  93. package/extension/src/shared/globalPreferences.js +30 -13
  94. package/extension/src/shared/governanceRules.js +4 -37
  95. package/extension/src/shared/i18n.js +298 -2
  96. package/extension/src/shared/managedUpdateProjection.js +2 -2
  97. package/extension/src/shared/nativeRequestIdentity.js +1 -0
  98. package/extension/src/shared/pageRpcContract.js +5 -4
  99. package/extension/src/shared/projectFiles.js +21 -44
  100. package/extension/src/shared/providerProfiles.js +5 -3
  101. package/extension/src/shared/runInputQueue.js +48 -5
  102. package/extension/src/shared/selectionContext.js +12 -3
  103. package/extension/src/shared/sessionState.js +72 -27
  104. package/extension/src/shared/settlementFacts.js +10 -2
  105. package/extension/src/shared/sharedSessionBridge.js +37 -14
  106. package/extension/src/shared/sharedSessionTransfer.js +140 -0
  107. package/extension/src/shared/staleGuard.js +118 -13
  108. package/extension/src/shared/storageDb.js +58 -26
  109. package/extension/src/shared/storageMigration.js +71 -111
  110. package/extension/src/shared/storageRunActions.js +209 -26
  111. package/extension/src/shared/subagentActivityModel.js +1 -1
  112. package/extension/src/shared/undoOperations.js +136 -13
  113. package/extension/src/shared/updateConsent.js +6 -4
  114. package/extension/src/shared/updateRuntimeIdentity.js +1 -1
  115. package/extension/src/shared/writebackIntent.js +16 -0
  116. package/extension/src/shared/writebackOperationEvidence.js +4 -15
  117. package/extension/src/shared/writebackSettlement.js +38 -38
  118. package/extension/styles/panel.css +19 -5
  119. package/native-host/src/anthropicBridgeRequest.js +4 -2
  120. package/native-host/src/anthropicBridgeResponse.js +6 -2
  121. package/native-host/src/anthropicMessagesBridge.js +24 -16
  122. package/native-host/src/bootstrapMigration.js +296 -0
  123. package/native-host/src/bootstrapMigrationContract.js +101 -0
  124. package/native-host/src/bootstrapUpdate.js +233 -0
  125. package/native-host/src/bootstrapUpdateWatchdog.js +44 -0
  126. package/native-host/src/chatBridgeResponse.js +10 -0
  127. package/native-host/src/chatCompletionsBridge.js +21 -15
  128. package/native-host/src/codexConfigPaths.js +106 -0
  129. package/native-host/src/codexHome.js +66 -73
  130. package/native-host/src/codexHomeScope.js +124 -0
  131. package/native-host/src/codexInheritedAuth.js +90 -0
  132. package/native-host/src/codexLocalSkills.js +301 -0
  133. package/native-host/src/codexPromptAssembly.js +31 -12
  134. package/native-host/src/codexProviderTest.js +24 -26
  135. package/native-host/src/codexSessionRunner.js +110 -183
  136. package/native-host/src/codexSessionTiming.js +18 -8
  137. package/native-host/src/codexSkillIsolation.js +170 -0
  138. package/native-host/src/codexThreadFork.js +19 -3
  139. package/native-host/src/index.js +19 -7
  140. package/native-host/src/managedInstall.js +110 -35
  141. package/native-host/src/managedInstallRecovery.js +140 -0
  142. package/native-host/src/managedRuntimeEvidence.js +57 -0
  143. package/native-host/src/mirrorWorkspace.js +92 -22
  144. package/native-host/src/nativeDoctor.js +15 -0
  145. package/native-host/src/nativeHelperLifecycle.js +21 -0
  146. package/native-host/src/nativeHostRegistration.js +33 -0
  147. package/native-host/src/nativeRunLifecycle.js +71 -0
  148. package/native-host/src/processStopBarrier.js +42 -7
  149. package/native-host/src/projectPathPlatform.js +30 -0
  150. package/native-host/src/providerHttp.js +80 -0
  151. package/native-host/src/providerProfile.js +4 -2
  152. package/native-host/src/providerReasoning.js +2 -7
  153. package/native-host/src/providerRuntime.js +1 -0
  154. package/native-host/src/providerStore.js +72 -14
  155. package/native-host/src/responsesPassthroughBridge.js +266 -52
  156. package/native-host/src/runtimeInstaller.js +21 -1
  157. package/native-host/src/selectionScope.js +36 -8
  158. package/native-host/src/subagentAdmission.js +26 -0
  159. package/native-host/src/subagentBroker.js +79 -66
  160. package/native-host/src/subagentTelemetry.js +19 -10
  161. package/native-host/src/subagentWorkspacePath.js +5 -2
  162. package/native-host/src/taskRunnerRuntime.js +77 -54
  163. package/native-host/src/tomlStatements.js +140 -0
  164. package/native-host/src/updateManager.js +297 -39
  165. package/native-host/src/writingStyleRuntime.js +57 -29
  166. package/native-host/src/writingStyleStore.js +73 -0
  167. package/package.json +1 -1
  168. package/scripts/install-managed.mjs +1 -0
@@ -216,6 +216,7 @@ function appendBlockDelta(state, res, index, delta) {
216
216
  function closeBlock(state, res, index) {
217
217
  const entry = state.blocks.get(index);
218
218
  if (!entry || entry.closed) return;
219
+ const args = entry.type === 'tool_use' ? canonicalArguments(entry.arguments) : '';
219
220
  entry.closed = true;
220
221
  if (entry.type === 'text') {
221
222
  const part = { type: 'output_text', text: entry.text, annotations: [] };
@@ -236,7 +237,6 @@ function closeBlock(state, res, index) {
236
237
  state.assistantBlocks[index] = block;
237
238
  } else {
238
239
  entry.item.status = 'completed';
239
- const args = canonicalArguments(entry.arguments);
240
240
  if (entry.kind === 'custom') {
241
241
  entry.item.input = unwrapCustomInput(args);
242
242
  emit(state, res, 'response.custom_tool_call_input.done', { item_id: entry.item.id, output_index: entry.outputIndex, input: entry.item.input });
@@ -401,7 +401,11 @@ function reserve(state, item) {
401
401
  }
402
402
 
403
403
  function canonicalArguments(value) {
404
- try { return JSON.stringify(JSON.parse(value || '{}')); } catch (_error) { return '{}'; }
404
+ try {
405
+ return JSON.stringify(JSON.parse(value || '{}'));
406
+ } catch (_error) {
407
+ throw responseError('The provider returned incomplete or invalid JSON tool arguments.');
408
+ }
405
409
  }
406
410
 
407
411
  function unwrapCustomInput(value) {
@@ -13,6 +13,7 @@ const {
13
13
  const { writeConvertedResponseAsSse } = require('./chatBridgeResponse');
14
14
  const { classifyResponsesRoute } = require('./providerBridgeRoutes');
15
15
  const { providerError } = require('./providerProfile');
16
+ const { fetchProvider, readProviderText, readProviderJson } = require('./providerHttp');
16
17
  const { sanitizeProviderMessage } = require('./providerRedaction');
17
18
  const { logDebug } = require('./debugLog');
18
19
  const {
@@ -92,9 +93,13 @@ async function handleRequest({ req, res, launch, clientToken, activeRequests, hi
92
93
  const requestBody = await readJsonBody(req);
93
94
  const previousResponseId = String(requestBody.previous_response_id || '');
94
95
  const previous = history.get(previousResponseId);
95
- const fallbackAnchor = previous ? [] : continuationState.anchorMessages;
96
+ const fallbackAllowed = !previousResponseId || previousResponseId === continuationState.responseId;
97
+ const fallbackAnchor = previous || !fallbackAllowed ? [] : continuationState.anchorMessages;
98
+ const continuationAttempts = previous
99
+ ? Number(previous.continuationAttempts) || 0
100
+ : fallbackAllowed ? continuationState.attempts : 0;
96
101
  const continuing = Boolean(previous?.continuation || fallbackAnchor.length);
97
- if (continuing && continuationState.attempts >= MAX_REASONING_CONTINUATIONS) {
102
+ if (continuing && continuationAttempts >= MAX_REASONING_CONTINUATIONS) {
98
103
  throw providerError(
99
104
  'provider_reasoning_continuation_exhausted',
100
105
  'The provider repeatedly interrupted the same reasoning stream. Retry the task or use a different model endpoint.'
@@ -107,12 +112,12 @@ async function handleRequest({ req, res, launch, clientToken, activeRequests, hi
107
112
  });
108
113
  logDebug('provider.anthropic.request', {
109
114
  model: translated.body.model,
110
- stream: requestBody.stream !== false,
115
+ stream: translated.body.stream,
111
116
  maxTokens: translated.body.max_tokens,
112
117
  thinkingType: translated.body.thinking?.type || 'disabled',
113
118
  hasPreviousResponse: Boolean(previousResponseId),
114
119
  continuing,
115
- continuationAttempt: continuationState.attempts
120
+ continuationAttempt: continuationAttempts
116
121
  });
117
122
  const controller = new AbortController();
118
123
  activeRequests.add(controller);
@@ -130,7 +135,7 @@ async function handleRequest({ req, res, launch, clientToken, activeRequests, hi
130
135
  res.on('close', onClientClose);
131
136
  lifecycle.start();
132
137
  try {
133
- const upstream = await fetchWithRectification(launch, translated.body, controller.signal);
138
+ const upstream = await fetchWithRectification(launch, translated.body, controller.signal, lifecycle.touch);
134
139
  lifecycle.touch();
135
140
  if (!upstream.response.ok) {
136
141
  forwardUpstreamError(upstream.response, upstream.errorText, res, launch.apiKey);
@@ -152,14 +157,15 @@ async function handleRequest({ req, res, launch, clientToken, activeRequests, hi
152
157
  const messages = [...baseMessages, ...assistantMessages];
153
158
  if (anchored) {
154
159
  continuationState.anchorMessages = messages;
155
- continuationState.attempts += 1;
160
+ continuationState.attempts = continuationAttempts + 1;
156
161
  continuationState.responseId = converted.response.id;
157
162
  } else {
158
163
  continuationState.anchorMessages = [];
159
164
  continuationState.attempts = 0;
160
165
  continuationState.responseId = '';
161
166
  }
162
- rememberHistory(history, converted.response.id, messages, { continuation: anchored });
167
+ rememberHistory(history, converted.response.id, messages, { continuation: anchored,
168
+ continuationAttempts: anchored ? continuationAttempts + 1 : 0 });
163
169
  const blocks = Array.isArray(converted.assistantBlocks) ? converted.assistantBlocks : [];
164
170
  logDebug('provider.anthropic.response', {
165
171
  responseId: converted.response.id,
@@ -185,7 +191,7 @@ async function handleRequest({ req, res, launch, clientToken, activeRequests, hi
185
191
  recoverInterrupted: lifecycle.recoverInterrupted
186
192
  });
187
193
  } else {
188
- const converted = convertAnthropicResponse(await upstream.response.json(), context);
194
+ const converted = convertAnthropicResponse(await readProviderJson(upstream.response, lifecycle.touch), context);
189
195
  writeConvertedResponseAsSse(res, converted, context, remember);
190
196
  }
191
197
  } else if (contentType.includes('text/event-stream')) {
@@ -202,7 +208,7 @@ async function handleRequest({ req, res, launch, clientToken, activeRequests, hi
202
208
  remember(converted);
203
209
  sendJson(res, 200, converted.response);
204
210
  } else {
205
- const converted = convertAnthropicResponse(await upstream.response.json(), context);
211
+ const converted = convertAnthropicResponse(await readProviderJson(upstream.response, lifecycle.touch), context);
206
212
  remember(converted);
207
213
  sendJson(res, 200, converted.response);
208
214
  }
@@ -224,27 +230,29 @@ async function handleRequest({ req, res, launch, clientToken, activeRequests, hi
224
230
  }
225
231
  }
226
232
 
227
- async function fetchWithRectification(launch, body, signal) {
233
+ async function fetchWithRectification(launch, body, signal, onActivity) {
228
234
  let requestBody = body;
229
- let response = await fetch(buildAnthropicMessagesUrl(launch.baseUrl, launch), {
235
+ let response = await fetchProvider(buildAnthropicMessagesUrl(launch.baseUrl, launch), {
230
236
  method: 'POST',
231
237
  headers: buildAnthropicHeaders(launch),
232
238
  body: JSON.stringify(body),
233
239
  signal
234
- });
240
+ }, launch);
241
+ onActivity();
235
242
  if (response.ok) return { response, errorText: '', requestBody };
236
- let errorText = (await response.text()).slice(0, MAX_ERROR_BYTES);
243
+ let errorText = (await readProviderText(response, onActivity)).slice(0, MAX_ERROR_BYTES);
237
244
  if (response.status === 400) {
238
245
  const retryBody = JSON.parse(JSON.stringify(body));
239
246
  if (rectifyAnthropicRequestForRetry(retryBody, errorText)) {
240
247
  requestBody = retryBody;
241
- response = await fetch(buildAnthropicMessagesUrl(launch.baseUrl, launch), {
248
+ response = await fetchProvider(buildAnthropicMessagesUrl(launch.baseUrl, launch), {
242
249
  method: 'POST',
243
250
  headers: buildAnthropicHeaders(launch),
244
251
  body: JSON.stringify(retryBody),
245
252
  signal
246
- });
247
- errorText = response.ok ? '' : (await response.text()).slice(0, MAX_ERROR_BYTES);
253
+ }, launch);
254
+ onActivity();
255
+ errorText = response.ok ? '' : (await readProviderText(response, onActivity)).slice(0, MAX_ERROR_BYTES);
248
256
  }
249
257
  }
250
258
  return { response, errorText, requestBody };
@@ -0,0 +1,296 @@
1
+ 'use strict';
2
+
3
+ // This module owns only files and their recovery record. The caller must verify
4
+ // the signed release, bind its authorization and hold the update mutation lock.
5
+ const fs = require('node:fs');
6
+ const path = require('node:path');
7
+ const crypto = require('node:crypto');
8
+ const { validateBootstrapMigrationDescriptor } = require('./bootstrapMigrationContract');
9
+
10
+ const RECORD_SCHEMA = 1;
11
+ const TRANSACTION_ID = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/;
12
+ const MAX_FILE_BYTES = 2 * 1024 * 1024;
13
+
14
+ function prepareBootstrapMigration(options, hooks = {}) {
15
+ const descriptor = clone(options.descriptor);
16
+ validateBootstrapMigrationDescriptor(descriptor, { targetVersion: options.targetVersion });
17
+ if (!descriptor.sourceVersions.includes(options.sourceVersion)) fail('source_version_mismatch');
18
+ const transactionId = validTransaction(options.transactionId);
19
+ const backupRoot = absoluteRoot(options.backupRoot);
20
+ const extensionRoot = absoluteRoot(options.extensionRoot);
21
+ const payloadRoot = absoluteRoot(options.payloadRoot);
22
+ if (overlaps(backupRoot, extensionRoot) || overlaps(backupRoot, payloadRoot)
23
+ || overlaps(extensionRoot, payloadRoot)) fail('roots_overlap');
24
+ ensureDirectory(backupRoot);
25
+ const recordRoot = path.join(backupRoot, transactionId);
26
+ ensureChildDirectory(backupRoot, transactionId);
27
+ const journalPath = path.join(recordRoot, 'state.json');
28
+ const identity = { transactionId, sourceVersion: options.sourceVersion,
29
+ targetVersion: options.targetVersion, extensionRoot, payloadRoot,
30
+ descriptorSha256: hash(JSON.stringify(descriptor)) };
31
+ if (readFileInfo(journalPath)) {
32
+ const saved = loadBootstrapMigration({ backupRoot, transactionId });
33
+ if (Object.entries(identity).some(([key, value]) => saved[key] !== value)) fail('transaction_mismatch');
34
+ return saved;
35
+ }
36
+
37
+ // Verify the entire source and target before writing any recovery material.
38
+ const files = descriptor.files.map(file => {
39
+ const source = readRootFile(extensionRoot, file.path);
40
+ if (source?.sha256 !== file.sourceSha256 && !(source === null && file.sourceSha256 === null)) fail('source_mismatch', file.path);
41
+ const target = readRootFile(payloadRoot, file.path);
42
+ if (!target || target.sha256 !== file.targetSha256) fail('payload_mismatch', file.path);
43
+ return { ...file, sourceMode: source?.mode ?? null, targetMode: source?.mode ?? target.mode };
44
+ });
45
+ ensureChildDirectory(recordRoot, 'files');
46
+ for (const file of files) {
47
+ if (file.sourceSha256 === null) continue;
48
+ const source = readRootFile(extensionRoot, file.path);
49
+ if (!source || source.sha256 !== file.sourceSha256 || source.mode !== file.sourceMode) fail('source_mismatch', file.path);
50
+ const target = childFile(recordRoot, 'files/' + file.path, true);
51
+ atomicWrite(target, source.bytes, 0o600, hooks, 'backup');
52
+ }
53
+ const record = { schemaVersion: RECORD_SCHEMA, ...identity, descriptor,
54
+ migrationId: descriptor.id, backupRoot, recordRoot, journalPath, files,
55
+ sourceDigest: digest(files, 'sourceSha256'), targetDigest: digest(files, 'targetSha256'),
56
+ state: 'prepared', pending: null, completed: [], createdAt: new Date().toISOString() };
57
+ writeRecord(record, hooks);
58
+ return publicPlan(record);
59
+ }
60
+
61
+ function loadBootstrapMigration({ backupRoot, transactionId }) {
62
+ backupRoot = absoluteRoot(backupRoot);
63
+ transactionId = validTransaction(transactionId);
64
+ const recordRoot = childFile(backupRoot, transactionId);
65
+ const journalPath = childFile(recordRoot, 'state.json');
66
+ const info = readFileInfo(journalPath);
67
+ if (!info) fail('record_missing');
68
+ let record;
69
+ try { record = JSON.parse(info.bytes.toString('utf8')); } catch (_) { fail('record_invalid'); }
70
+ if (!record || record.schemaVersion !== RECORD_SCHEMA || record.transactionId !== transactionId
71
+ || record.backupRoot !== backupRoot || record.recordRoot !== recordRoot || record.journalPath !== journalPath) fail('record_invalid');
72
+ validateBootstrapMigrationDescriptor(record.descriptor, { targetVersion: record.targetVersion });
73
+ if (!record.descriptor.sourceVersions.includes(record.sourceVersion)
74
+ || record.migrationId !== record.descriptor.id
75
+ || record.descriptorSha256 !== hash(JSON.stringify(record.descriptor))
76
+ || !Array.isArray(record.files) || record.files.length !== record.descriptor.files.length
77
+ || !['prepared', 'applying', 'applied', 'restoring', 'restored'].includes(record.state)
78
+ || !Array.isArray(record.completed)) fail('record_invalid');
79
+ const extensionRoot = absoluteRoot(record.extensionRoot);
80
+ const payloadRoot = absoluteRoot(record.payloadRoot);
81
+ if (overlaps(backupRoot, extensionRoot) || overlaps(backupRoot, payloadRoot)
82
+ || overlaps(extensionRoot, payloadRoot)) fail('roots_overlap');
83
+ for (let index = 0; index < record.files.length; index++) {
84
+ const file = record.files[index];
85
+ const expected = record.descriptor.files[index];
86
+ if (['path', 'sourceSha256', 'targetSha256'].some(key => file[key] !== expected[key])
87
+ || !validMode(file.targetMode) || (file.sourceSha256 === null ? file.sourceMode !== null : !validMode(file.sourceMode))) fail('record_invalid');
88
+ }
89
+ if (record.sourceDigest !== digest(record.files, 'sourceSha256')
90
+ || record.targetDigest !== digest(record.files, 'targetSha256')) fail('record_invalid');
91
+ return publicPlan(record);
92
+ }
93
+
94
+ function applyBootstrapMigration(plan, hooks = {}) {
95
+ const record = reloadPlan(plan);
96
+ if (record.state === 'restoring' || record.state === 'restored') fail('already_restoring');
97
+ verifyKnownFiles(record);
98
+ verifyBackups(record);
99
+ for (const file of record.files) {
100
+ const target = readRootFile(record.payloadRoot, file.path);
101
+ if (!target || target.sha256 !== file.targetSha256) fail('payload_mismatch', file.path);
102
+ }
103
+ if (record.state === 'applied') return verifyBootstrapMigration(record, { expected: 'target' });
104
+ record.state = 'applying';
105
+ writeRecord(record, hooks);
106
+ for (const file of writeOrder(record.files)) {
107
+ record.pending = file.path;
108
+ writeRecord(record, hooks);
109
+ const current = readRootFile(record.extensionRoot, file.path);
110
+ if (!matchesKnown(current, file)) fail('source_mismatch', file.path);
111
+ if (current?.sha256 !== file.targetSha256 || current.mode !== file.targetMode) {
112
+ const target = readRootFile(record.payloadRoot, file.path);
113
+ if (!target || target.sha256 !== file.targetSha256) fail('payload_mismatch', file.path);
114
+ atomicWrite(childFile(record.extensionRoot, file.path, true), target.bytes, file.targetMode, hooks, 'apply');
115
+ }
116
+ verifyFile(record.extensionRoot, file, 'target');
117
+ if (!record.completed.includes(file.path)) record.completed.push(file.path);
118
+ record.pending = null;
119
+ writeRecord(record, hooks);
120
+ }
121
+ record.state = 'applied';
122
+ writeRecord(record, hooks);
123
+ return verifyBootstrapMigration(record, { expected: 'target' });
124
+ }
125
+
126
+ function restoreBootstrapMigration(plan, hooks = {}) {
127
+ const record = reloadPlan(plan);
128
+ verifyKnownFiles(record);
129
+ verifyBackups(record);
130
+ if (record.state === 'restored') return verifyBootstrapMigration(record, { expected: 'source' });
131
+ record.state = 'restoring';
132
+ writeRecord(record, hooks);
133
+ // Reverse dependencies: restore HTML/entry users before removing new files.
134
+ for (const file of writeOrder(record.files).reverse()) {
135
+ record.pending = file.path;
136
+ writeRecord(record, hooks);
137
+ const current = readRootFile(record.extensionRoot, file.path);
138
+ if (!matchesKnown(current, file)) fail('source_mismatch', file.path);
139
+ const target = childFile(record.extensionRoot, file.path);
140
+ if (file.sourceSha256 === null) {
141
+ if (current) {
142
+ fs.unlinkSync(target);
143
+ syncDirectory(path.dirname(target));
144
+ step(hooks, 'restore:removed', target);
145
+ }
146
+ } else if (current?.sha256 !== file.sourceSha256 || current.mode !== file.sourceMode) {
147
+ const backup = readRootFile(record.recordRoot, 'files/' + file.path);
148
+ if (!backup || backup.sha256 !== file.sourceSha256) fail('backup_mismatch', file.path);
149
+ atomicWrite(target, backup.bytes, file.sourceMode, hooks, 'restore');
150
+ }
151
+ verifyFile(record.extensionRoot, file, 'source');
152
+ record.completed = record.completed.filter(value => value !== file.path);
153
+ record.pending = null;
154
+ writeRecord(record, hooks);
155
+ }
156
+ record.state = 'restored';
157
+ writeRecord(record, hooks);
158
+ return verifyBootstrapMigration(record, { expected: 'source' });
159
+ }
160
+
161
+ function verifyBootstrapMigration(plan, { expected = 'target' } = {}) {
162
+ if (!['source', 'target'].includes(expected)) fail('expectation_invalid');
163
+ const record = reloadPlan(plan);
164
+ for (const file of record.files) verifyFile(record.extensionRoot, file, expected);
165
+ return { transactionId: record.transactionId, migrationId: record.migrationId,
166
+ state: record.state, digest: expected === 'target' ? record.targetDigest : record.sourceDigest,
167
+ targetRevision: record.descriptor.targetRevision, expected };
168
+ }
169
+
170
+ function readBootstrapMigrationState(plan) { return reloadPlan(plan); }
171
+
172
+ function reloadPlan(plan) {
173
+ if (!plan || typeof plan !== 'object') fail('record_invalid');
174
+ const saved = loadBootstrapMigration(plan);
175
+ for (const key of ['descriptorSha256', 'extensionRoot', 'payloadRoot', 'sourceVersion', 'targetVersion']) {
176
+ if (plan[key] !== saved[key]) fail('transaction_mismatch');
177
+ }
178
+ return saved;
179
+ }
180
+
181
+ function verifyKnownFiles(record) {
182
+ for (const file of record.files) {
183
+ if (!matchesKnown(readRootFile(record.extensionRoot, file.path), file)) fail('source_mismatch', file.path);
184
+ }
185
+ }
186
+
187
+ function matchesKnown(current, file) {
188
+ if (current === null) return file.sourceSha256 === null;
189
+ return (current.sha256 === file.sourceSha256 && current.mode === file.sourceMode)
190
+ || (current.sha256 === file.targetSha256 && current.mode === file.targetMode);
191
+ }
192
+
193
+ function verifyBackups(record) {
194
+ for (const file of record.files) {
195
+ if (file.sourceSha256 === null) continue;
196
+ const backup = readRootFile(record.recordRoot, 'files/' + file.path);
197
+ if (!backup || backup.sha256 !== file.sourceSha256) fail('backup_mismatch', file.path);
198
+ }
199
+ }
200
+
201
+ function verifyFile(root, file, expected) {
202
+ const current = readRootFile(root, file.path);
203
+ const expectedHash = file[expected + 'Sha256'];
204
+ if (expectedHash === null ? current !== null : !current || current.sha256 !== expectedHash
205
+ || current.mode !== file[expected + 'Mode']) fail(expected + '_mismatch', file.path);
206
+ }
207
+
208
+ function writeOrder(files) {
209
+ const rank = file => file.sourceSha256 === null ? 0 : file.path === 'bootstrap/background.js' ? 3
210
+ : file.path.endsWith('.html') ? 2 : 1;
211
+ return [...files].sort((a, b) => rank(a) - rank(b) || a.path.localeCompare(b.path));
212
+ }
213
+
214
+ function writeRecord(record, hooks) {
215
+ record.updatedAt = new Date().toISOString();
216
+ atomicWrite(record.journalPath, JSON.stringify(record, null, 2) + '\n', 0o600, hooks, 'journal');
217
+ }
218
+
219
+ function atomicWrite(target, bytes, mode, hooks, phase) {
220
+ try { if (!fs.lstatSync(target).isFile()) fail('path_unsafe'); }
221
+ catch (error) { if (error.code !== 'ENOENT') throw error; }
222
+ const temporary = target + '.migration-' + crypto.randomBytes(8).toString('hex') + '.tmp';
223
+ const fd = fs.openSync(temporary, 'wx', mode);
224
+ try { fs.writeFileSync(fd, bytes); fs.fchmodSync(fd, mode); fs.fsyncSync(fd); }
225
+ finally { fs.closeSync(fd); }
226
+ step(hooks, phase + ':temporary', target);
227
+ fs.renameSync(temporary, target);
228
+ syncDirectory(path.dirname(target));
229
+ step(hooks, phase + ':replaced', target);
230
+ }
231
+
232
+ function syncDirectory(directory) {
233
+ let fd;
234
+ try { fd = fs.openSync(directory, 'r'); fs.fsyncSync(fd); }
235
+ catch (error) {
236
+ // Windows does not expose POSIX directory fsync through Node. File contents
237
+ // are flushed before rename on all platforms; directory durability is OS-dependent.
238
+ if (process.platform !== 'win32' || !['EPERM', 'EISDIR', 'EINVAL', 'EBADF', 'EACCES'].includes(error.code)) throw error;
239
+ } finally { if (fd !== undefined) fs.closeSync(fd); }
240
+ }
241
+
242
+ function childFile(root, relative, createParents = false) {
243
+ if (typeof relative !== 'string' || relative.includes('\\') || relative.split('/').some(part => !part || part === '.' || part === '..')
244
+ || path.isAbsolute(relative)) fail('path_unsafe');
245
+ assertDirectory(root);
246
+ const parts = relative.split('/');
247
+ let parent = root;
248
+ for (const part of parts.slice(0, -1)) {
249
+ parent = path.join(parent, part);
250
+ if (createParents && !fs.existsSync(parent)) { fs.mkdirSync(parent, { mode: 0o700 }); syncDirectory(path.dirname(parent)); }
251
+ assertDirectory(parent);
252
+ }
253
+ const target = path.join(root, ...parts);
254
+ try { if (fs.lstatSync(target).isSymbolicLink()) fail('path_unsafe'); }
255
+ catch (error) { if (error.code !== 'ENOENT') throw error; }
256
+ return target;
257
+ }
258
+
259
+ function readRootFile(root, relative) { return readFileInfo(childFile(root, relative)); }
260
+
261
+ function readFileInfo(target) {
262
+ let stat;
263
+ try { stat = fs.lstatSync(target); } catch (error) { if (error.code === 'ENOENT') return null; throw error; }
264
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_FILE_BYTES) fail('path_unsafe', target);
265
+ const bytes = fs.readFileSync(target);
266
+ return { bytes, sha256: hash(bytes), mode: stat.mode & 0o777 };
267
+ }
268
+
269
+ function ensureDirectory(root) {
270
+ if (!fs.existsSync(root)) {
271
+ ensureDirectory(path.dirname(root));
272
+ fs.mkdirSync(root, { mode: 0o700 });
273
+ syncDirectory(path.dirname(root));
274
+ }
275
+ assertDirectory(root);
276
+ }
277
+ function ensureChildDirectory(root, relative) {
278
+ const directory = childFile(root, relative, true);
279
+ if (!fs.existsSync(directory)) { fs.mkdirSync(directory, { mode: 0o700 }); syncDirectory(path.dirname(directory)); }
280
+ assertDirectory(directory);
281
+ return directory;
282
+ }
283
+ function assertDirectory(value) { if (!fs.lstatSync(value).isDirectory() || fs.lstatSync(value).isSymbolicLink()) fail('path_unsafe'); }
284
+ function absoluteRoot(value) { if (typeof value !== 'string' || !path.isAbsolute(value) || path.normalize(value) !== value) fail('path_unsafe'); return value; }
285
+ function validTransaction(value) { if (typeof value !== 'string' || !TRANSACTION_ID.test(value) || value === '.' || value === '..') fail('transaction_invalid'); return value; }
286
+ function overlaps(a, b) { return a === b || a.startsWith(b + path.sep) || b.startsWith(a + path.sep); }
287
+ function validMode(value) { return Number.isInteger(value) && value >= 0 && value <= 0o777; }
288
+ function digest(files, key) { return hash(JSON.stringify([...files].sort((a, b) => a.path.localeCompare(b.path)).map(file => [file.path, file[key]]))); }
289
+ function hash(value) { return crypto.createHash('sha256').update(value).digest('hex'); }
290
+ function clone(value) { return JSON.parse(JSON.stringify(value)); }
291
+ function publicPlan(record) { return clone(record); }
292
+ function step(hooks, phase, target) { if (typeof hooks.onStep === 'function') hooks.onStep({ phase, path: target }); }
293
+ function fail(reason, file) { const error = new Error('Bootstrap migration ' + reason.replaceAll('_', ' ') + (file ? ': ' + file : '.')); error.code = 'update_bootstrap_' + reason; throw error; }
294
+
295
+ module.exports = { prepareBootstrapMigration, loadBootstrapMigration, applyBootstrapMigration,
296
+ restoreBootstrapMigration, verifyBootstrapMigration, readBootstrapMigrationState };
@@ -0,0 +1,101 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('node:crypto');
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+
7
+ // Exact bytes from the published v2.5.0 Bootstrap. Unknown local modifications
8
+ // are never converted into a migration source simply because versions agree.
9
+ const BOOTSTRAP_MIGRATION_BASELINE = Object.freeze({
10
+ "bootstrap/background.js": "5e15d208f07668f96cb94a84642d5fd9ad2dd921991bb3bd5bf67cc43037c87a",
11
+ "bootstrap/popup.css": "0709a4b8aba5e59e95fd07afec4d29ba441bde46fae48863133a170ad12cbe58",
12
+ "bootstrap/popup.html": "7cbcead701cb03a4006bbd682a744aeca30d613eb8315a194f2ba751b6fd9e55",
13
+ "bootstrap/popup.js": "24f7171045ad7de9431d24e0e4a7d15237620f4217dd5e68772dbdf52b8ce4d8",
14
+ "bootstrap/popupFallback.js": null,
15
+ "bootstrap/runtimeContext.js": "ac04d9bb28308015ecfe296f152996521398dc0bb018416e34abe8424734a21b",
16
+ "bootstrap/update.css": "43a0d4345f6f35a30c792a6fd67227776ec85ba710f89cae9c167b453de72342",
17
+ "bootstrap/update.html": "7ce092eabcdf51564f994d8f72c474285f07777bd7a5a205eca8e5a0931964ab",
18
+ "bootstrap/update.js": "ee67a23d89d435f05d3e4c1ec1f9c5a7f0bdcef4449e13be4d7acbbcf182413c",
19
+ "bootstrap/updateStatus.js": "d2ad90e78c50367a5489bb6f5c27c7248112047aca9f05d2ad868ac3b5266b24"
20
+ });
21
+ const BOOTSTRAP_MIGRATION_FILES = Object.freeze(Object.keys(BOOTSTRAP_MIGRATION_BASELINE));
22
+ const BOOTSTRAP_MIGRATION_TARGET_VERSION = '2.5.1';
23
+ const BOOTSTRAP_MIGRATION_REVISION = '3.1';
24
+ const BOOTSTRAP_MIGRATION_IMPLEMENTATION_FILES = Object.freeze([
25
+ 'native-host/src/bootstrapMigrationContract.js',
26
+ 'native-host/src/bootstrapMigration.js',
27
+ 'native-host/src/bootstrapUpdate.js',
28
+ 'native-host/src/bootstrapUpdateWatchdog.js',
29
+ 'extension/src/backgroundBootstrapMigration.js'
30
+ ]);
31
+ const BOOTSTRAP_MANIFEST_SHAPE_SHA256 = '85c2d3875196ff9b334b40bb115a42fe4b8cab4e9c37609fb65e8834284c8fbe';
32
+
33
+ function isBootstrapMigrationRelease(version) {
34
+ return version === BOOTSTRAP_MIGRATION_TARGET_VERSION;
35
+ }
36
+
37
+ function buildBootstrapMigrationDescriptor({ packageRoot, version } = {}) {
38
+ if (!isBootstrapMigrationRelease(version)) return null;
39
+ assertBootstrapManifestShape(JSON.parse(fs.readFileSync(path.join(packageRoot,
40
+ 'extension/bootstrap/manifest.template.json'), 'utf8')), { version });
41
+ const files = BOOTSTRAP_MIGRATION_FILES.map(relativePath => ({
42
+ path: relativePath,
43
+ sourceSha256: BOOTSTRAP_MIGRATION_BASELINE[relativePath],
44
+ targetSha256: sha256(fs.readFileSync(path.join(packageRoot, 'extension', relativePath)))
45
+ }));
46
+ return validateBootstrapMigrationDescriptor({
47
+ schemaVersion: 1,
48
+ id: 'bootstrap-v3.1-from-2.5.0',
49
+ bootstrapProtocol: 3,
50
+ sourceVersions: ['2.5.0'],
51
+ targetVersion: version,
52
+ targetRevision: BOOTSTRAP_MIGRATION_REVISION,
53
+ files
54
+ }, { targetVersion: version });
55
+ }
56
+
57
+ function validateBootstrapMigrationDescriptor(descriptor, { targetVersion } = {}) {
58
+ if (!descriptor || descriptor.schemaVersion !== 1 || descriptor.id !== 'bootstrap-v3.1-from-2.5.0'
59
+ || descriptor.bootstrapProtocol !== 3 || descriptor.targetRevision !== BOOTSTRAP_MIGRATION_REVISION
60
+ || descriptor.targetVersion !== BOOTSTRAP_MIGRATION_TARGET_VERSION
61
+ || (targetVersion && descriptor.targetVersion !== targetVersion)
62
+ || JSON.stringify(descriptor.sourceVersions) !== JSON.stringify(['2.5.0'])
63
+ || !Array.isArray(descriptor.files) || descriptor.files.length !== BOOTSTRAP_MIGRATION_FILES.length) {
64
+ throw new Error('Unsupported Bootstrap migration descriptor.');
65
+ }
66
+ const seen = new Set();
67
+ for (const file of descriptor.files) {
68
+ if (!file || !Object.hasOwn(BOOTSTRAP_MIGRATION_BASELINE, file.path) || seen.has(file.path)
69
+ || file.sourceSha256 !== BOOTSTRAP_MIGRATION_BASELINE[file.path]
70
+ || !/^[0-9a-f]{64}$/.test(file.targetSha256 || '')) {
71
+ throw new Error('Bootstrap migration file identity is invalid.');
72
+ }
73
+ seen.add(file.path);
74
+ }
75
+ return descriptor;
76
+ }
77
+
78
+ function assertBootstrapManifestShape(manifest, { version } = {}) {
79
+ if (!manifest || (version && manifest.version !== version)
80
+ || sha256(Buffer.from(stableJson({ ...manifest, version: '<release-version>' }))) !== BOOTSTRAP_MANIFEST_SHAPE_SHA256) {
81
+ throw new Error('Bootstrap migration cannot change the extension manifest shape or permissions.');
82
+ }
83
+ return manifest;
84
+ }
85
+
86
+ function stableJson(value) {
87
+ if (Array.isArray(value)) return '[' + value.map(stableJson).join(',') + ']';
88
+ if (value && typeof value === 'object') return '{' + Object.keys(value).sort()
89
+ .map(key => JSON.stringify(key) + ':' + stableJson(value[key])).join(',') + '}';
90
+ return JSON.stringify(value);
91
+ }
92
+
93
+ function sha256(bytes) {
94
+ return crypto.createHash('sha256').update(bytes).digest('hex');
95
+ }
96
+
97
+ module.exports = {
98
+ BOOTSTRAP_MIGRATION_BASELINE, BOOTSTRAP_MIGRATION_FILES, BOOTSTRAP_MIGRATION_TARGET_VERSION,
99
+ BOOTSTRAP_MIGRATION_REVISION, isBootstrapMigrationRelease, buildBootstrapMigrationDescriptor,
100
+ BOOTSTRAP_MIGRATION_IMPLEMENTATION_FILES, validateBootstrapMigrationDescriptor, assertBootstrapManifestShape
101
+ };