codex-overleaf-link 1.9.3 → 1.9.5
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 +23 -23
- package/extension/bootstrap/manifest.template.json +1 -1
- package/extension/runtime-manifest.json +1 -0
- package/extension/src/background.js +20 -0
- package/extension/src/backgroundUpdateCoordinator.js +414 -0
- package/extension/src/content/contentRuntime.js +3 -0
- package/extension/src/content/updateNotice.js +248 -0
- package/extension/src/popup.js +268 -3
- package/extension/src/shared/compatibility.js +1 -1
- package/extension/src/shared/updateConsent.js +201 -0
- package/extension/styles/panel.css +146 -0
- package/native-host/src/updateManager.js +257 -37
- package/package.json +1 -1
|
@@ -19,20 +19,26 @@ const NATIVE_MARKER = '.codex-overleaf-managed-native.json';
|
|
|
19
19
|
const JOURNAL_FILE = 'transaction.json';
|
|
20
20
|
const CANDIDATE_FILE = 'candidate.json';
|
|
21
21
|
const COOLDOWN_FILE = 'cooldowns.json';
|
|
22
|
+
const AUTHORIZATION_FILE = 'authorization.json';
|
|
23
|
+
const AUTHORIZATION_TTL_MS = 2 * 60 * 60 * 1000;
|
|
24
|
+
const CONSENT_MIGRATION_TARGET_VERSION = '1.9.4';
|
|
22
25
|
const UPDATE_METHODS = new Set([
|
|
23
26
|
'update.status',
|
|
24
27
|
'update.check',
|
|
28
|
+
'update.authorize',
|
|
25
29
|
'update.stage',
|
|
26
30
|
'update.canApply',
|
|
27
31
|
'update.apply',
|
|
28
32
|
'update.confirm',
|
|
29
|
-
'update.rollback'
|
|
33
|
+
'update.rollback',
|
|
34
|
+
'update.revoke'
|
|
30
35
|
]);
|
|
31
36
|
const RELEASE_ASSET_HOSTS = new Set([
|
|
32
37
|
'github.com',
|
|
33
38
|
'objects.githubusercontent.com',
|
|
34
39
|
'release-assets.githubusercontent.com'
|
|
35
40
|
]);
|
|
41
|
+
let updateMutationTail = Promise.resolve();
|
|
36
42
|
|
|
37
43
|
function isUpdateMethod(method) {
|
|
38
44
|
return UPDATE_METHODS.has(method);
|
|
@@ -49,19 +55,23 @@ async function handleUpdateRequest(request, options = {}) {
|
|
|
49
55
|
}
|
|
50
56
|
switch (request.method) {
|
|
51
57
|
case 'update.status':
|
|
52
|
-
return okResponse(request.id, recoverAndReadStatus(context));
|
|
58
|
+
return okResponse(request.id, await withUpdateMutex(() => recoverAndReadStatus(context)));
|
|
53
59
|
case 'update.check':
|
|
54
|
-
return okResponse(request.id, await checkForUpdate(context, request.params || {}, options));
|
|
60
|
+
return okResponse(request.id, await withUpdateMutex(() => checkForUpdate(context, request.params || {}, options)));
|
|
61
|
+
case 'update.authorize':
|
|
62
|
+
return okResponse(request.id, await withUpdateMutex(() => authorizeUpdate(context, request.params || {})));
|
|
55
63
|
case 'update.stage':
|
|
56
|
-
return okResponse(request.id, await stageCandidate(context, options));
|
|
64
|
+
return okResponse(request.id, await withUpdateMutex(() => stageCandidate(context, options)));
|
|
57
65
|
case 'update.canApply':
|
|
58
66
|
return okResponse(request.id, getApplyGate(options));
|
|
59
67
|
case 'update.apply':
|
|
60
|
-
return okResponse(request.id, applyStagedUpdate(context, request.params || {}, options));
|
|
68
|
+
return okResponse(request.id, await withUpdateMutex(() => applyStagedUpdate(context, request.params || {}, options)));
|
|
61
69
|
case 'update.confirm':
|
|
62
|
-
return okResponse(request.id, confirmUpdate(context, request.params || {}));
|
|
70
|
+
return okResponse(request.id, await withUpdateMutex(() => confirmUpdate(context, request.params || {})));
|
|
63
71
|
case 'update.rollback':
|
|
64
|
-
return okResponse(request.id, rollbackUpdate(context, request.params || {}));
|
|
72
|
+
return okResponse(request.id, await withUpdateMutex(() => rollbackUpdate(context, request.params || {})));
|
|
73
|
+
case 'update.revoke':
|
|
74
|
+
return okResponse(request.id, await withUpdateMutex(() => revokeUpdate(context, request.params || {})));
|
|
65
75
|
default:
|
|
66
76
|
throw updateError('unknown_update_method', 'Unknown managed update method.');
|
|
67
77
|
}
|
|
@@ -70,6 +80,12 @@ async function handleUpdateRequest(request, options = {}) {
|
|
|
70
80
|
}
|
|
71
81
|
}
|
|
72
82
|
|
|
83
|
+
function withUpdateMutex(action) {
|
|
84
|
+
const result = updateMutationTail.then(action, action);
|
|
85
|
+
updateMutationTail = result.catch(() => {});
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
|
|
73
89
|
async function checkForUpdate(context, params = {}, options = {}) {
|
|
74
90
|
const fetchImpl = options.fetch || globalThis.fetch;
|
|
75
91
|
if (typeof fetchImpl !== 'function') {
|
|
@@ -160,6 +176,53 @@ function buildReleaseAssetUrl(tag, assetName) {
|
|
|
160
176
|
return GITHUB_RELEASE_DOWNLOAD_ROOT + '/' + tag + '/' + encodeURIComponent(assetName);
|
|
161
177
|
}
|
|
162
178
|
|
|
179
|
+
function authorizeUpdate(context, params = {}) {
|
|
180
|
+
const authorizationId = String(params.authorizationId || '');
|
|
181
|
+
const targetVersion = String(params.targetVersion || '');
|
|
182
|
+
const requestedCurrentVersion = String(params.currentVersion || '');
|
|
183
|
+
if (!isUuid(authorizationId)) {
|
|
184
|
+
throw updateError('update_authorization_invalid', 'Update authorization id is invalid.');
|
|
185
|
+
}
|
|
186
|
+
const candidate = readJsonSafe(path.join(context.updatesRoot, CANDIDATE_FILE), null);
|
|
187
|
+
if (!candidate) {
|
|
188
|
+
throw updateError('update_candidate_missing', 'Check for an update before authorizing it.');
|
|
189
|
+
}
|
|
190
|
+
const manifest = verifySignedReleaseManifest(
|
|
191
|
+
Buffer.from(candidate.manifestBase64 || '', 'base64'),
|
|
192
|
+
Buffer.from(candidate.signatureBase64 || '', 'base64')
|
|
193
|
+
);
|
|
194
|
+
const activeVersion = readVersionPointer(context.nativeRoot, 'active-version');
|
|
195
|
+
if (requestedCurrentVersion !== activeVersion ||
|
|
196
|
+
targetVersion !== candidate.latestVersion ||
|
|
197
|
+
targetVersion !== manifest.version ||
|
|
198
|
+
!isNewerStableVersion(targetVersion, activeVersion)) {
|
|
199
|
+
throw updateError('update_consent_mismatch', 'Update authorization does not match the active signed candidate.');
|
|
200
|
+
}
|
|
201
|
+
const existing = readAuthorization(context);
|
|
202
|
+
if (existing?.id === authorizationId &&
|
|
203
|
+
existing.targetVersion === targetVersion &&
|
|
204
|
+
['authorized', 'bound'].includes(existing.state) &&
|
|
205
|
+
(!existing.expiresAt || Date.parse(existing.expiresAt) > Date.now())) {
|
|
206
|
+
return publicAuthorization(existing);
|
|
207
|
+
}
|
|
208
|
+
if (existing && ['authorized', 'bound'].includes(existing.state)) {
|
|
209
|
+
throw updateError('update_authorization_in_progress', 'Another update authorization is already active.');
|
|
210
|
+
}
|
|
211
|
+
const grantedAt = new Date();
|
|
212
|
+
const authorization = {
|
|
213
|
+
schemaVersion: 1,
|
|
214
|
+
id: authorizationId,
|
|
215
|
+
targetVersion,
|
|
216
|
+
sourceVersion: activeVersion,
|
|
217
|
+
state: 'authorized',
|
|
218
|
+
grantedAt: grantedAt.toISOString(),
|
|
219
|
+
expiresAt: new Date(grantedAt.getTime() + AUTHORIZATION_TTL_MS).toISOString(),
|
|
220
|
+
transactionId: ''
|
|
221
|
+
};
|
|
222
|
+
writeAuthorization(context, authorization);
|
|
223
|
+
return publicAuthorization(authorization);
|
|
224
|
+
}
|
|
225
|
+
|
|
163
226
|
async function stageCandidate(context, options = {}) {
|
|
164
227
|
const candidate = readJsonSafe(path.join(context.updatesRoot, CANDIDATE_FILE), null);
|
|
165
228
|
if (!candidate) {
|
|
@@ -172,10 +235,16 @@ async function stageCandidate(context, options = {}) {
|
|
|
172
235
|
if (!isNewerStableVersion(manifest.version, activeVersion)) {
|
|
173
236
|
throw updateError('update_candidate_stale', 'The staged update is no longer newer than the active version.');
|
|
174
237
|
}
|
|
238
|
+
const authorization = requireAuthorization(context, manifest.version, ['authorized', 'bound']);
|
|
175
239
|
const existingJournal = readJournal(context);
|
|
176
240
|
if (existingJournal?.state === 'staged' && existingJournal.targetVersion === manifest.version) {
|
|
241
|
+
if (existingJournal.authorizationId !== authorization.id ||
|
|
242
|
+
(authorization.transactionId && authorization.transactionId !== existingJournal.id)) {
|
|
243
|
+
throw updateError('update_consent_mismatch', 'Staged transaction does not match the active authorization.');
|
|
244
|
+
}
|
|
177
245
|
try {
|
|
178
246
|
verifyStagedPair(existingJournal.payloadRoot, manifest.version);
|
|
247
|
+
bindAuthorization(context, authorization, existingJournal.id);
|
|
179
248
|
return {
|
|
180
249
|
transactionId: existingJournal.id,
|
|
181
250
|
targetVersion: existingJournal.targetVersion,
|
|
@@ -189,38 +258,51 @@ async function stageCandidate(context, options = {}) {
|
|
|
189
258
|
if (existingJournal && ['applying', 'awaiting_health'].includes(existingJournal.state)) {
|
|
190
259
|
throw updateError('update_transaction_in_progress', 'A managed update transaction is already being applied.');
|
|
191
260
|
}
|
|
261
|
+
if (authorization.state === 'bound') {
|
|
262
|
+
throw updateError('update_consent_mismatch', 'Bound authorization has no reusable staged transaction.');
|
|
263
|
+
}
|
|
192
264
|
const transactionId = crypto.randomUUID();
|
|
193
265
|
const stageRoot = path.join(context.updatesRoot, 'staging-' + transactionId);
|
|
194
266
|
fs.mkdirSync(stageRoot, { recursive: true, mode: 0o700 });
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
267
|
+
try {
|
|
268
|
+
const archivePath = path.join(stageRoot, manifest.updateBundle.name);
|
|
269
|
+
const fetchImpl = options.fetch || globalThis.fetch;
|
|
270
|
+
const bundleBytes = await fetchReleaseAsset(fetchImpl, candidate.bundleUrl, manifest.updateBundle.size + 1);
|
|
271
|
+
if (bundleBytes.length !== manifest.updateBundle.size) {
|
|
272
|
+
throw updateError('update_bundle_size_mismatch', 'Downloaded update bundle size does not match the signed manifest.');
|
|
273
|
+
}
|
|
274
|
+
const hash = crypto.createHash('sha256').update(bundleBytes).digest('hex');
|
|
275
|
+
if (hash !== manifest.updateBundle.sha256) {
|
|
276
|
+
throw updateError('update_bundle_hash_mismatch', 'Downloaded update bundle hash does not match the signed manifest.');
|
|
277
|
+
}
|
|
278
|
+
fs.writeFileSync(archivePath, bundleBytes, { mode: 0o600 });
|
|
279
|
+
const payloadRoot = path.join(stageRoot, 'payload');
|
|
280
|
+
extractVerifiedUpdateBundle({ archivePath, destinationRoot: payloadRoot });
|
|
281
|
+
verifyStagedPair(payloadRoot, manifest.version);
|
|
282
|
+
const journal = {
|
|
283
|
+
id: transactionId,
|
|
284
|
+
authorizationId: authorization.id,
|
|
285
|
+
state: 'staged',
|
|
286
|
+
sourceVersion: activeVersion,
|
|
287
|
+
sourcePreviousVersion: readVersionPointer(context.nativeRoot, 'previous-version'),
|
|
288
|
+
targetVersion: manifest.version,
|
|
289
|
+
stageRoot,
|
|
290
|
+
payloadRoot,
|
|
291
|
+
manifestSha256: crypto.createHash('sha256').update(manifestBytes).digest('hex'),
|
|
292
|
+
createdAt: new Date().toISOString(),
|
|
293
|
+
updatedAt: new Date().toISOString()
|
|
294
|
+
};
|
|
295
|
+
writeJournal(context, journal);
|
|
296
|
+
bindAuthorization(context, authorization, transactionId);
|
|
297
|
+
cleanupOrphanStageRoots(context.updatesRoot, [stageRoot]);
|
|
298
|
+
return { transactionId, targetVersion: manifest.version, state: journal.state };
|
|
299
|
+
} catch (error) {
|
|
300
|
+
cleanupStage(stageRoot);
|
|
301
|
+
const journal = readJournal(context);
|
|
302
|
+
if (journal?.id === transactionId) removeJournal(context);
|
|
303
|
+
settleAuthorization(context, authorization.id, 'revoked');
|
|
304
|
+
throw error;
|
|
305
|
+
}
|
|
224
306
|
}
|
|
225
307
|
|
|
226
308
|
function getApplyGate(options = {}) {
|
|
@@ -245,6 +327,10 @@ function applyStagedUpdate(context, params = {}, options = {}) {
|
|
|
245
327
|
if (params.transactionId && params.transactionId !== journal.id) {
|
|
246
328
|
throw updateError('update_transaction_mismatch', 'Update transaction id does not match the staged update.');
|
|
247
329
|
}
|
|
330
|
+
const authorization = requireAuthorization(context, journal.targetVersion, ['bound']);
|
|
331
|
+
if (journal.authorizationId !== authorization.id || authorization.transactionId !== journal.id) {
|
|
332
|
+
throw updateError('update_consent_mismatch', 'Apply transaction does not match the bound authorization.');
|
|
333
|
+
}
|
|
248
334
|
assertManagedMarkers(context);
|
|
249
335
|
const extensionRuntime = path.join(context.extensionRoot, 'runtime');
|
|
250
336
|
const previousRuntime = path.join(context.extensionRoot, 'slots', 'previous', 'runtime');
|
|
@@ -268,6 +354,7 @@ function applyStagedUpdate(context, params = {}, options = {}) {
|
|
|
268
354
|
syncManagedMarkerVersions(context, journal.targetVersion);
|
|
269
355
|
} catch (error) {
|
|
270
356
|
rollbackFiles(context, { ...journal, previousManifest });
|
|
357
|
+
settleAuthorization(context, journal.authorizationId, 'revoked');
|
|
271
358
|
throw updateError('update_apply_failed', 'Managed update could not be applied atomically.', { cause: error });
|
|
272
359
|
}
|
|
273
360
|
const awaiting = {
|
|
@@ -289,6 +376,14 @@ function confirmUpdate(context, params = {}) {
|
|
|
289
376
|
if (params.transactionId !== journal.id || params.extensionVersion !== journal.targetVersion || params.nativeVersion !== journal.targetVersion) {
|
|
290
377
|
throw updateError('update_health_version_mismatch', 'Extension and native host did not confirm the same target version.');
|
|
291
378
|
}
|
|
379
|
+
if (!isLegacyInboundConsentMigration(journal)) {
|
|
380
|
+
const authorization = requireAuthorization(context, journal.targetVersion, ['bound'], {
|
|
381
|
+
activeVersion: journal.sourceVersion
|
|
382
|
+
});
|
|
383
|
+
if (journal.authorizationId !== authorization.id || authorization.transactionId !== journal.id) {
|
|
384
|
+
throw updateError('update_consent_mismatch', 'Health confirmation does not match the bound authorization.');
|
|
385
|
+
}
|
|
386
|
+
}
|
|
292
387
|
const manifest = readJsonSafe(path.join(context.extensionRoot, 'manifest.json'), null);
|
|
293
388
|
if (readVersionPointer(context.nativeRoot, 'active-version') !== journal.targetVersion || manifest?.version !== journal.targetVersion) {
|
|
294
389
|
throw updateError('update_health_version_mismatch', 'Managed files do not match the confirmed target version.');
|
|
@@ -299,6 +394,7 @@ function confirmUpdate(context, params = {}) {
|
|
|
299
394
|
confirmedAt: new Date().toISOString(),
|
|
300
395
|
updatedAt: new Date().toISOString()
|
|
301
396
|
});
|
|
397
|
+
settleAuthorization(context, journal.authorizationId, 'consumed');
|
|
302
398
|
pruneNativeVersions(context.nativeRoot, new Set([journal.targetVersion, journal.sourceVersion]));
|
|
303
399
|
cleanupStage(journal.stageRoot);
|
|
304
400
|
cleanupOrphanStageRoots(context.updatesRoot);
|
|
@@ -325,11 +421,50 @@ function rollbackUpdate(context, params = {}) {
|
|
|
325
421
|
updatedAt: new Date().toISOString(),
|
|
326
422
|
previousManifest: undefined
|
|
327
423
|
});
|
|
424
|
+
settleAuthorization(context, journal.authorizationId, 'revoked');
|
|
328
425
|
cleanupStage(journal.stageRoot);
|
|
329
426
|
cleanupOrphanStageRoots(context.updatesRoot);
|
|
330
427
|
return { version: journal.sourceVersion, state: 'rolled_back' };
|
|
331
428
|
}
|
|
332
429
|
|
|
430
|
+
function revokeUpdate(context, params = {}) {
|
|
431
|
+
const authorizationId = String(params.authorizationId || '');
|
|
432
|
+
const targetVersion = String(params.targetVersion || '');
|
|
433
|
+
const transactionId = String(params.transactionId || '');
|
|
434
|
+
if (!isUuid(authorizationId) || !parseSemver(targetVersion)) {
|
|
435
|
+
throw updateError('update_authorization_invalid', 'Update revocation parameters are invalid.');
|
|
436
|
+
}
|
|
437
|
+
const journal = readJournal(context);
|
|
438
|
+
if (journal?.targetVersion === targetVersion &&
|
|
439
|
+
['applying', 'awaiting_health', 'committed'].includes(journal.state)) {
|
|
440
|
+
throw updateError('update_revoke_too_late', 'The update is already being applied.');
|
|
441
|
+
}
|
|
442
|
+
const authorization = readAuthorization(context);
|
|
443
|
+
if (!authorization) {
|
|
444
|
+
if (!journal || journal.targetVersion !== targetVersion) {
|
|
445
|
+
return { state: 'already_revoked', targetVersion };
|
|
446
|
+
}
|
|
447
|
+
if (journal.authorizationId !== authorizationId) {
|
|
448
|
+
throw updateError('update_consent_mismatch', 'Revocation does not match the staged transaction.');
|
|
449
|
+
}
|
|
450
|
+
} else if (authorization.id !== authorizationId || authorization.targetVersion !== targetVersion) {
|
|
451
|
+
throw updateError('update_consent_mismatch', 'Revocation does not match the active authorization.');
|
|
452
|
+
}
|
|
453
|
+
if (transactionId && journal?.id !== transactionId) {
|
|
454
|
+
throw updateError('update_transaction_mismatch', 'Revocation transaction id does not match the staged update.');
|
|
455
|
+
}
|
|
456
|
+
if (authorization) {
|
|
457
|
+
writeAuthorization(context, { ...authorization, state: 'revoked', revokedAt: new Date().toISOString() });
|
|
458
|
+
}
|
|
459
|
+
if (journal?.state === 'staged') {
|
|
460
|
+
cleanupStage(journal.stageRoot);
|
|
461
|
+
removeJournal(context);
|
|
462
|
+
}
|
|
463
|
+
cleanupOrphanStageRoots(context.updatesRoot);
|
|
464
|
+
fs.rmSync(path.join(context.updatesRoot, AUTHORIZATION_FILE), { force: true });
|
|
465
|
+
return { state: 'revoked', targetVersion };
|
|
466
|
+
}
|
|
467
|
+
|
|
333
468
|
function rollbackFiles(context, journal) {
|
|
334
469
|
const runtime = path.join(context.extensionRoot, 'runtime');
|
|
335
470
|
const previousRuntime = path.join(context.extensionRoot, 'slots', 'previous', 'runtime');
|
|
@@ -397,6 +532,7 @@ function recoverAndReadStatus(context) {
|
|
|
397
532
|
rollbackFiles(context, journal);
|
|
398
533
|
journal = { ...journal, state: 'rolled_back', reasonCode: 'update_interrupted', updatedAt: new Date().toISOString() };
|
|
399
534
|
writeJournal(context, journal);
|
|
535
|
+
settleAuthorization(context, journal.authorizationId, 'revoked');
|
|
400
536
|
}
|
|
401
537
|
if (journal?.state === 'rolled_back') {
|
|
402
538
|
pruneNativeVersions(context.nativeRoot, new Set([
|
|
@@ -408,7 +544,8 @@ function recoverAndReadStatus(context) {
|
|
|
408
544
|
managed: true,
|
|
409
545
|
activeVersion: readVersionPointer(context.nativeRoot, 'active-version'),
|
|
410
546
|
previousVersion: readVersionPointer(context.nativeRoot, 'previous-version'),
|
|
411
|
-
transaction: journal ? publicTransaction(journal) : null
|
|
547
|
+
transaction: journal ? publicTransaction(journal) : null,
|
|
548
|
+
authorization: publicAuthorization(readAuthorization(context))
|
|
412
549
|
};
|
|
413
550
|
}
|
|
414
551
|
|
|
@@ -542,6 +679,18 @@ function publicTransaction(journal) {
|
|
|
542
679
|
};
|
|
543
680
|
}
|
|
544
681
|
|
|
682
|
+
function publicAuthorization(authorization) {
|
|
683
|
+
if (!authorization) return null;
|
|
684
|
+
return {
|
|
685
|
+
state: authorization.state,
|
|
686
|
+
targetVersion: authorization.targetVersion,
|
|
687
|
+
sourceVersion: authorization.sourceVersion,
|
|
688
|
+
transactionId: authorization.transactionId || '',
|
|
689
|
+
grantedAt: authorization.grantedAt,
|
|
690
|
+
expiresAt: authorization.expiresAt
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
|
|
545
694
|
function readJournal(context) {
|
|
546
695
|
return readJsonSafe(path.join(context.updatesRoot, JOURNAL_FILE), null);
|
|
547
696
|
}
|
|
@@ -550,6 +699,60 @@ function writeJournal(context, journal) {
|
|
|
550
699
|
atomicWriteJson(path.join(context.updatesRoot, JOURNAL_FILE), journal);
|
|
551
700
|
}
|
|
552
701
|
|
|
702
|
+
function removeJournal(context) {
|
|
703
|
+
fs.rmSync(path.join(context.updatesRoot, JOURNAL_FILE), { force: true });
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function readAuthorization(context) {
|
|
707
|
+
return readJsonSafe(path.join(context.updatesRoot, AUTHORIZATION_FILE), null);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function writeAuthorization(context, authorization) {
|
|
711
|
+
atomicWriteJson(path.join(context.updatesRoot, AUTHORIZATION_FILE), authorization);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function requireAuthorization(context, targetVersion, allowedStates, options = {}) {
|
|
715
|
+
const authorization = readAuthorization(context);
|
|
716
|
+
if (!authorization) {
|
|
717
|
+
throw updateError('update_consent_required', 'Choose Update now before downloading or installing this update.');
|
|
718
|
+
}
|
|
719
|
+
const expectedActiveVersion = options.activeVersion || readVersionPointer(context.nativeRoot, 'active-version');
|
|
720
|
+
if (authorization.targetVersion !== targetVersion || authorization.sourceVersion !== expectedActiveVersion) {
|
|
721
|
+
throw updateError('update_consent_mismatch', 'Update authorization does not match this source and target version.');
|
|
722
|
+
}
|
|
723
|
+
if (!allowedStates.includes(authorization.state)) {
|
|
724
|
+
throw updateError('update_consent_required', 'Update authorization is not active.');
|
|
725
|
+
}
|
|
726
|
+
if (authorization.state === 'authorized' &&
|
|
727
|
+
(!authorization.expiresAt || Date.parse(authorization.expiresAt) <= Date.now())) {
|
|
728
|
+
settleAuthorization(context, authorization.id, 'revoked');
|
|
729
|
+
throw updateError('update_consent_required', 'Update authorization expired. Choose Update now again.');
|
|
730
|
+
}
|
|
731
|
+
return authorization;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function bindAuthorization(context, authorization, transactionId) {
|
|
735
|
+
const bound = {
|
|
736
|
+
...authorization,
|
|
737
|
+
state: 'bound',
|
|
738
|
+
transactionId,
|
|
739
|
+
boundAt: authorization.boundAt || new Date().toISOString()
|
|
740
|
+
};
|
|
741
|
+
writeAuthorization(context, bound);
|
|
742
|
+
return bound;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function settleAuthorization(context, authorizationId, state) {
|
|
746
|
+
const authorization = readAuthorization(context);
|
|
747
|
+
if (!authorization || (authorizationId && authorization.id !== authorizationId)) return;
|
|
748
|
+
writeAuthorization(context, {
|
|
749
|
+
...authorization,
|
|
750
|
+
state,
|
|
751
|
+
settledAt: new Date().toISOString()
|
|
752
|
+
});
|
|
753
|
+
fs.rmSync(path.join(context.updatesRoot, AUTHORIZATION_FILE), { force: true });
|
|
754
|
+
}
|
|
755
|
+
|
|
553
756
|
function atomicWriteJson(target, value) {
|
|
554
757
|
atomicWriteText(target, JSON.stringify(value, null, 2) + '\n');
|
|
555
758
|
}
|
|
@@ -579,6 +782,21 @@ function normalizeReasonCode(value) {
|
|
|
579
782
|
return /^[a-z0-9_]{1,80}$/.test(String(value || '')) ? String(value) : 'update_health_failed';
|
|
580
783
|
}
|
|
581
784
|
|
|
785
|
+
function isUuid(value) {
|
|
786
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(String(value || ''));
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function isLegacyInboundConsentMigration(journal) {
|
|
790
|
+
return Boolean(
|
|
791
|
+
journal &&
|
|
792
|
+
!journal.authorizationId &&
|
|
793
|
+
journal.state === 'awaiting_health' &&
|
|
794
|
+
journal.targetVersion === CONSENT_MIGRATION_TARGET_VERSION &&
|
|
795
|
+
parseSemver(journal.sourceVersion) &&
|
|
796
|
+
compareSemver(journal.sourceVersion, CONSENT_MIGRATION_TARGET_VERSION) < 0
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
|
|
582
800
|
function safeErrorCode(error) {
|
|
583
801
|
return /^[a-z0-9_]{1,80}$/.test(String(error?.code || '')) ? error.code : 'update_internal_error';
|
|
584
802
|
}
|
|
@@ -598,6 +816,7 @@ function errorResponse(id, code, message) {
|
|
|
598
816
|
module.exports = {
|
|
599
817
|
GITHUB_LATEST_URL,
|
|
600
818
|
applyStagedUpdate,
|
|
819
|
+
authorizeUpdate,
|
|
601
820
|
checkForUpdate,
|
|
602
821
|
cleanupOrphanStageRoots,
|
|
603
822
|
confirmUpdate,
|
|
@@ -605,6 +824,7 @@ module.exports = {
|
|
|
605
824
|
getManagedContext,
|
|
606
825
|
handleUpdateRequest,
|
|
607
826
|
isUpdateMethod,
|
|
827
|
+
revokeUpdate,
|
|
608
828
|
rollbackUpdate,
|
|
609
829
|
stageCandidate,
|
|
610
830
|
syncManagedMarkerVersions
|