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
|
@@ -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 {
|
|
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
|
|
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 &&
|
|
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:
|
|
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:
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
+
};
|