borgmcp 2.10.0 → 2.11.0
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/dist/assimilate-cmd.d.ts.map +1 -1
- package/dist/assimilate-cmd.js +10 -3
- package/dist/assimilate-cmd.js.map +1 -1
- package/dist/cli-help.d.ts.map +1 -1
- package/dist/cli-help.js +5 -3
- package/dist/cli-help.js.map +1 -1
- package/dist/config.d.ts +34 -9
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +487 -105
- package/dist/config.js.map +1 -1
- package/dist/enrollment-lock.d.ts +10 -0
- package/dist/enrollment-lock.d.ts.map +1 -0
- package/dist/enrollment-lock.js +49 -0
- package/dist/enrollment-lock.js.map +1 -0
- package/dist/enrollment-types.d.ts +47 -0
- package/dist/enrollment-types.d.ts.map +1 -0
- package/dist/enrollment-types.js +2 -0
- package/dist/enrollment-types.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/invitation-artifact.d.ts +3 -1
- package/dist/invitation-artifact.d.ts.map +1 -1
- package/dist/invitation-artifact.js +3 -1
- package/dist/invitation-artifact.js.map +1 -1
- package/dist/recover-enrollment-cmd.d.ts.map +1 -1
- package/dist/recover-enrollment-cmd.js +34 -15
- package/dist/recover-enrollment-cmd.js.map +1 -1
- package/dist/remote-client.d.ts +3 -2
- package/dist/remote-client.d.ts.map +1 -1
- package/dist/remote-client.js +32 -11
- package/dist/remote-client.js.map +1 -1
- package/dist/server-errors.d.ts +4 -0
- package/dist/server-errors.d.ts.map +1 -1
- package/dist/server-errors.js +8 -0
- package/dist/server-errors.js.map +1 -1
- package/dist/server-facade.d.ts +1 -1
- package/dist/server-facade.d.ts.map +1 -1
- package/dist/server-facade.js +13 -2
- package/dist/server-facade.js.map +1 -1
- package/dist/server-handshake.d.ts +11 -3
- package/dist/server-handshake.d.ts.map +1 -1
- package/dist/server-handshake.js +103 -45
- package/dist/server-handshake.js.map +1 -1
- package/dist/server-trust.d.ts +14 -1
- package/dist/server-trust.d.ts.map +1 -1
- package/dist/server-trust.js +363 -66
- package/dist/server-trust.js.map +1 -1
- package/dist/tool-manifest.d.ts.map +1 -1
- package/dist/tool-manifest.js +4 -10
- package/dist/tool-manifest.js.map +1 -1
- package/docs/EXTRACTION_PROVENANCE.md +3 -3
- package/docs/RELEASING.md +40 -1
- package/package.json +1 -1
- package/src/assimilate-cmd.ts +14 -4
- package/src/cli-help.ts +5 -3
- package/src/config.ts +544 -101
- package/src/enrollment-lock.ts +53 -0
- package/src/enrollment-types.ts +51 -0
- package/src/index.ts +1 -1
- package/src/invitation-artifact.ts +7 -1
- package/src/recover-enrollment-cmd.ts +39 -15
- package/src/remote-client.ts +36 -10
- package/src/server-errors.ts +7 -0
- package/src/server-facade.ts +13 -2
- package/src/server-handshake.ts +130 -52
- package/src/server-trust.ts +392 -66
- package/src/tool-manifest.ts +4 -10
package/dist/config.js
CHANGED
|
@@ -10,8 +10,12 @@ import { createHash, randomBytes, randomUUID } from 'crypto';
|
|
|
10
10
|
import { withStoreLock } from './seat-store.js';
|
|
11
11
|
import { makeFileBackend, } from './token-store.js';
|
|
12
12
|
import { BORG_USER_ROOT, SERVER_CREDENTIALS_FILE } from './credential-paths.js';
|
|
13
|
+
import { withEnrollmentOriginLock } from './enrollment-lock.js';
|
|
14
|
+
import { InvitationArtifactRecoveryError, MISKEYED_RECOVERY_ERROR, } from './invitation-artifact.js';
|
|
13
15
|
const SERVER_CREDENTIAL_RECORD_VERSION = 2;
|
|
14
|
-
const SERVER_PENDING_ENROLLMENT_RECORD_VERSION =
|
|
16
|
+
const SERVER_PENDING_ENROLLMENT_RECORD_VERSION = 3;
|
|
17
|
+
const LEGACY_SERVER_PENDING_ENROLLMENT_RECORD_VERSION = 1;
|
|
18
|
+
const SERVER_ACCEPTED_ENROLLMENT_MARKER_VERSION = 1;
|
|
15
19
|
const SERVER_CUBE_RETRY_RECORD_VERSION = 2;
|
|
16
20
|
// The 0600 credential store (Queen rescope: replaces the OS keychain). A single
|
|
17
21
|
// file holds every parent credential/enrollment record; a single flock
|
|
@@ -59,6 +63,51 @@ function serverPendingEnrollmentAccount(origin, trustIdentity) {
|
|
|
59
63
|
.digest('hex');
|
|
60
64
|
return `borg-server-enrollment-pending:${binding}`;
|
|
61
65
|
}
|
|
66
|
+
function serverAcceptedEnrollmentAccount(origin) {
|
|
67
|
+
const binding = createHash('sha256').update(origin).digest('hex');
|
|
68
|
+
return `borg-server-enrollment-accepted:${binding}`;
|
|
69
|
+
}
|
|
70
|
+
function serverEnrollmentRollbackAccount(origin, retryKey) {
|
|
71
|
+
const binding = createHash('sha256').update(origin).update('\0').update(retryKey).digest('hex');
|
|
72
|
+
return `borg-server-enrollment-rollback:${binding}`;
|
|
73
|
+
}
|
|
74
|
+
function digestAccountSnapshot(accounts, origin) {
|
|
75
|
+
const selected = Object.entries(accounts)
|
|
76
|
+
.filter(([, value]) => {
|
|
77
|
+
try {
|
|
78
|
+
const parsed = JSON.parse(value);
|
|
79
|
+
return parsed.version === SERVER_CREDENTIAL_RECORD_VERSION && parsed.origin === origin;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
.sort(([left], [right]) => left.localeCompare(right));
|
|
86
|
+
return createHash('sha256').update(JSON.stringify(selected)).digest('hex');
|
|
87
|
+
}
|
|
88
|
+
function validateArtifactBinding(binding) {
|
|
89
|
+
if (binding === undefined)
|
|
90
|
+
return;
|
|
91
|
+
validateServerCredentialBinding(binding.endpoint, binding.trustIdentity);
|
|
92
|
+
if (!Number.isSafeInteger(binding.artifactFormatVersion) || binding.artifactFormatVersion < 1 ||
|
|
93
|
+
!/^[a-f0-9]{64}$/.test(binding.artifactDigest) ||
|
|
94
|
+
!/^[a-f0-9]{64}$/.test(binding.caSpkiSha256) ||
|
|
95
|
+
!/^[a-f0-9]{64}$/.test(binding.stagedGenerationId) ||
|
|
96
|
+
binding.endpoint.length < 1 ||
|
|
97
|
+
binding.expectedAuthority !== 'owner' && binding.expectedAuthority !== 'client')
|
|
98
|
+
throw new Error('invalid Borg enrollment artifact binding');
|
|
99
|
+
}
|
|
100
|
+
function validateReplacementCapability(capability) {
|
|
101
|
+
if (capability === undefined)
|
|
102
|
+
return;
|
|
103
|
+
validateUuid(capability.token, 'replacement capability');
|
|
104
|
+
validateServerCredentialBinding(capability.endpoint, capability.trustIdentity);
|
|
105
|
+
if (!/^[a-f0-9]{64}$/.test(capability.priorAccountsDigest) ||
|
|
106
|
+
!/^[a-f0-9]{64}$/.test(capability.caSpkiSha256) ||
|
|
107
|
+
!/^[a-f0-9]{64}$/.test(capability.artifactDigest))
|
|
108
|
+
throw new Error('invalid Borg enrollment replacement capability');
|
|
109
|
+
validateUuid(capability.retryKey, 'enrollment retry key');
|
|
110
|
+
}
|
|
62
111
|
function validateUuid(value, label) {
|
|
63
112
|
if (!UUID_RE.test(value))
|
|
64
113
|
throw new Error(`invalid Borg server ${label}`);
|
|
@@ -223,6 +272,102 @@ function encodeServerCredentialRecord(record, serverCapabilities) {
|
|
|
223
272
|
serverCapabilities,
|
|
224
273
|
});
|
|
225
274
|
}
|
|
275
|
+
function decodeActiveServerCredentialRecord(stored, origin, trustIdentity) {
|
|
276
|
+
const record = JSON.parse(stored);
|
|
277
|
+
if (record.version !== SERVER_CREDENTIAL_RECORD_VERSION ||
|
|
278
|
+
record.origin !== origin ||
|
|
279
|
+
record.trustIdentity !== trustIdentity ||
|
|
280
|
+
typeof record.credential !== 'string' ||
|
|
281
|
+
!/^[A-Za-z0-9_-]{43}$/.test(record.credential) ||
|
|
282
|
+
(record.clientId !== null &&
|
|
283
|
+
(typeof record.clientId !== 'string' || !UUID_RE.test(record.clientId))) ||
|
|
284
|
+
!Array.isArray(record.serverCapabilities))
|
|
285
|
+
throw new Error('invalid Borg server credential record');
|
|
286
|
+
const serverCapabilities = validateServerCapabilities(record.serverCapabilities);
|
|
287
|
+
return {
|
|
288
|
+
origin,
|
|
289
|
+
trustIdentity,
|
|
290
|
+
credential: record.credential,
|
|
291
|
+
clientId: record.clientId,
|
|
292
|
+
serverCapabilities,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
function decodeAcceptedEnrollmentMarker(value) {
|
|
296
|
+
const marker = JSON.parse(value);
|
|
297
|
+
if (marker.version !== SERVER_ACCEPTED_ENROLLMENT_MARKER_VERSION ||
|
|
298
|
+
marker.state !== 'accepted' ||
|
|
299
|
+
typeof marker.origin !== 'string' ||
|
|
300
|
+
typeof marker.trustIdentity !== 'string' ||
|
|
301
|
+
typeof marker.generationId !== 'string' || !/^[a-f0-9]{64}$/.test(marker.generationId) ||
|
|
302
|
+
(marker.previousPointer !== null && typeof marker.previousPointer !== 'object') ||
|
|
303
|
+
typeof marker.activeDigest !== 'string' || !/^[a-f0-9]{64}$/.test(marker.activeDigest) ||
|
|
304
|
+
typeof marker.rollbackAccount !== 'string' ||
|
|
305
|
+
typeof marker.rollbackDigest !== 'string' || !/^[a-f0-9]{64}$/.test(marker.rollbackDigest))
|
|
306
|
+
throw new Error('invalid Borg enrollment recovery marker');
|
|
307
|
+
validateServerCredentialBinding(marker.origin, marker.trustIdentity);
|
|
308
|
+
if (marker.previousPointer !== null) {
|
|
309
|
+
const pointer = marker.previousPointer;
|
|
310
|
+
if (pointer.version !== 1 || pointer.origin !== marker.origin ||
|
|
311
|
+
typeof pointer.generationId !== 'string' || !/^[a-f0-9]{64}$/.test(pointer.generationId) ||
|
|
312
|
+
typeof pointer.trustIdentity !== 'string')
|
|
313
|
+
throw new Error('invalid Borg enrollment recovery marker');
|
|
314
|
+
validateServerCredentialBinding(pointer.origin, pointer.trustIdentity);
|
|
315
|
+
}
|
|
316
|
+
return marker;
|
|
317
|
+
}
|
|
318
|
+
function decodeRollbackRecord(value, marker) {
|
|
319
|
+
if (createHash('sha256').update(value).digest('hex') !== marker.rollbackDigest) {
|
|
320
|
+
throw new Error('Borg enrollment rollback snapshot digest is invalid');
|
|
321
|
+
}
|
|
322
|
+
const record = JSON.parse(value);
|
|
323
|
+
if (record.version !== 1 || record.state !== 'rollback-snapshot' || record.origin !== marker.origin ||
|
|
324
|
+
typeof record.snapshot !== 'object' || record.snapshot === null ||
|
|
325
|
+
typeof record.snapshot.pendingAccount !== 'string' ||
|
|
326
|
+
typeof record.snapshot.pendingValue !== 'string' ||
|
|
327
|
+
typeof record.snapshot.activeAccounts !== 'object' || record.snapshot.activeAccounts === null)
|
|
328
|
+
throw new Error('invalid Borg enrollment rollback snapshot');
|
|
329
|
+
for (const [account, stored] of Object.entries(record.snapshot.activeAccounts)) {
|
|
330
|
+
if (typeof account !== 'string' || typeof stored !== 'string') {
|
|
331
|
+
throw new Error('invalid Borg enrollment rollback snapshot');
|
|
332
|
+
}
|
|
333
|
+
let active;
|
|
334
|
+
try {
|
|
335
|
+
active = JSON.parse(stored);
|
|
336
|
+
}
|
|
337
|
+
catch {
|
|
338
|
+
throw new Error('invalid Borg enrollment rollback snapshot');
|
|
339
|
+
}
|
|
340
|
+
if (active.version !== SERVER_CREDENTIAL_RECORD_VERSION || active.origin !== marker.origin ||
|
|
341
|
+
typeof active.trustIdentity !== 'string' ||
|
|
342
|
+
account !== serverCredentialAccount(marker.origin, active.trustIdentity))
|
|
343
|
+
throw new Error('invalid Borg enrollment rollback snapshot');
|
|
344
|
+
}
|
|
345
|
+
let pendingRaw;
|
|
346
|
+
try {
|
|
347
|
+
pendingRaw = JSON.parse(record.snapshot.pendingValue);
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
throw new Error('invalid Borg enrollment rollback snapshot');
|
|
351
|
+
}
|
|
352
|
+
if (typeof pendingRaw.origin !== 'string' || typeof pendingRaw.trustIdentity !== 'string') {
|
|
353
|
+
throw new Error('invalid Borg enrollment rollback snapshot');
|
|
354
|
+
}
|
|
355
|
+
const pending = decodePendingServerEnrollment(record.snapshot.pendingValue, pendingRaw.origin, pendingRaw.trustIdentity);
|
|
356
|
+
if (pending.origin !== marker.origin || pending.trustIdentity !== marker.trustIdentity ||
|
|
357
|
+
record.snapshot.pendingAccount !== serverPendingEnrollmentAccount(marker.origin, marker.trustIdentity) ||
|
|
358
|
+
marker.rollbackAccount !== serverEnrollmentRollbackAccount(marker.origin, pending.retryKey))
|
|
359
|
+
throw new Error('invalid Borg enrollment rollback snapshot');
|
|
360
|
+
return record;
|
|
361
|
+
}
|
|
362
|
+
async function markerForOriginUnlocked(backend, origin) {
|
|
363
|
+
const stored = await backend.get(serverAcceptedEnrollmentAccount(origin));
|
|
364
|
+
if (!stored)
|
|
365
|
+
return null;
|
|
366
|
+
return decodeAcceptedEnrollmentMarker(stored);
|
|
367
|
+
}
|
|
368
|
+
function processSharedEnrollmentLock() {
|
|
369
|
+
return !testBackendInjected;
|
|
370
|
+
}
|
|
226
371
|
/**
|
|
227
372
|
* Persist one self-hosted server credential in the dedicated 0600 credential store.
|
|
228
373
|
*
|
|
@@ -233,46 +378,28 @@ function encodeServerCredentialRecord(record, serverCapabilities) {
|
|
|
233
378
|
* not credential sources. CR3b: the load→set→rename runs inside ONE hold of the
|
|
234
379
|
* single store lock so a concurrent writer cannot lose an unrelated account.
|
|
235
380
|
*/
|
|
236
|
-
export async function storeServerCredential(record
|
|
381
|
+
export async function storeServerCredential(record) {
|
|
237
382
|
const backend = await getServerCredentialBackend();
|
|
238
|
-
await withCredentialStoreLock(() => writeServerCredentialRecord(backend, record,
|
|
383
|
+
await withEnrollmentOriginLock(record.origin, () => withCredentialStoreLock(() => writeServerCredentialRecord(backend, record, false)), {
|
|
384
|
+
processShared: processSharedEnrollmentLock(),
|
|
385
|
+
});
|
|
239
386
|
}
|
|
240
387
|
/** Read an authority-bound active client record, failing closed on corruption. */
|
|
241
388
|
export async function getServerCredentialRecord(origin, trustIdentity) {
|
|
242
389
|
const backend = await getServerCredentialBackend();
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
if (record.version !== SERVER_CREDENTIAL_RECORD_VERSION ||
|
|
249
|
-
record.origin !== origin ||
|
|
250
|
-
record.trustIdentity !== trustIdentity ||
|
|
251
|
-
typeof record.credential !== 'string' ||
|
|
252
|
-
!/^[A-Za-z0-9_-]{43}$/.test(record.credential) ||
|
|
253
|
-
(record.clientId !== null &&
|
|
254
|
-
(typeof record.clientId !== 'string' || !UUID_RE.test(record.clientId))) ||
|
|
255
|
-
!Array.isArray(record.serverCapabilities)) {
|
|
390
|
+
return withEnrollmentOriginLock(origin, () => withCredentialStoreLock(async () => {
|
|
391
|
+
if (await markerForOriginUnlocked(backend, origin))
|
|
392
|
+
throw new InvitationArtifactRecoveryError();
|
|
393
|
+
const stored = await backend.get(serverCredentialAccount(origin, trustIdentity));
|
|
394
|
+
if (!stored)
|
|
256
395
|
return null;
|
|
257
|
-
}
|
|
258
|
-
let serverCapabilities;
|
|
259
396
|
try {
|
|
260
|
-
|
|
397
|
+
return decodeActiveServerCredentialRecord(stored, origin, trustIdentity);
|
|
261
398
|
}
|
|
262
399
|
catch {
|
|
263
400
|
return null;
|
|
264
401
|
}
|
|
265
|
-
|
|
266
|
-
origin,
|
|
267
|
-
trustIdentity,
|
|
268
|
-
credential: record.credential,
|
|
269
|
-
clientId: record.clientId,
|
|
270
|
-
serverCapabilities,
|
|
271
|
-
};
|
|
272
|
-
}
|
|
273
|
-
catch {
|
|
274
|
-
return null;
|
|
275
|
-
}
|
|
402
|
+
}), { processShared: processSharedEnrollmentLock() });
|
|
276
403
|
}
|
|
277
404
|
/** Read only the bearer for existing call sites that do not need capability metadata. */
|
|
278
405
|
export async function getServerCredential(origin, trustIdentity) {
|
|
@@ -282,32 +409,46 @@ export async function hasServerCredentialForOrigin(origin) {
|
|
|
282
409
|
const backend = await getServerCredentialBackend();
|
|
283
410
|
if (!backend.entries)
|
|
284
411
|
return false;
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
412
|
+
return withEnrollmentOriginLock(origin, () => withCredentialStoreLock(async () => {
|
|
413
|
+
if (await markerForOriginUnlocked(backend, origin))
|
|
414
|
+
throw new InvitationArtifactRecoveryError();
|
|
415
|
+
const accounts = await backend.entries();
|
|
416
|
+
return Object.values(accounts).some((value) => {
|
|
417
|
+
try {
|
|
418
|
+
const record = JSON.parse(value);
|
|
419
|
+
return record.version === SERVER_CREDENTIAL_RECORD_VERSION && record.origin === origin;
|
|
420
|
+
}
|
|
421
|
+
catch {
|
|
422
|
+
return false;
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
}), { processShared: processSharedEnrollmentLock() });
|
|
295
426
|
}
|
|
296
427
|
function decodePendingServerEnrollment(stored, origin, trustIdentity) {
|
|
297
428
|
const record = JSON.parse(stored);
|
|
298
|
-
if (record.version !== SERVER_PENDING_ENROLLMENT_RECORD_VERSION
|
|
429
|
+
if (record.version !== SERVER_PENDING_ENROLLMENT_RECORD_VERSION &&
|
|
430
|
+
record.version !== LEGACY_SERVER_PENDING_ENROLLMENT_RECORD_VERSION ||
|
|
299
431
|
record.state !== 'pending' ||
|
|
300
432
|
record.origin !== origin ||
|
|
301
433
|
record.trustIdentity !== trustIdentity ||
|
|
302
434
|
typeof record.invitation !== 'string' ||
|
|
303
435
|
typeof record.retryKey !== 'string' || !UUID_RE.test(record.retryKey) ||
|
|
304
436
|
typeof record.credential !== 'string' || !/^[A-Za-z0-9_-]{43}$/.test(record.credential) ||
|
|
305
|
-
(record.clientName !== undefined && typeof record.clientName !== 'string')
|
|
306
|
-
|| (record.replacementCapability !== undefined && !UUID_RE.test(record.replacementCapability))) {
|
|
437
|
+
(record.clientName !== undefined && typeof record.clientName !== 'string')) {
|
|
307
438
|
throw new Error('invalid');
|
|
308
439
|
}
|
|
309
440
|
validateInvitation(record.invitation);
|
|
310
441
|
validateClientName(record.clientName);
|
|
442
|
+
validateArtifactBinding(record.artifactBinding);
|
|
443
|
+
validateReplacementCapability(record.replacementCapability);
|
|
444
|
+
if (record.artifactBinding && (record.artifactBinding.endpoint !== origin ||
|
|
445
|
+
record.artifactBinding.trustIdentity !== trustIdentity ||
|
|
446
|
+
record.replacementCapability && (record.replacementCapability.endpoint !== origin ||
|
|
447
|
+
record.replacementCapability.trustIdentity !== trustIdentity ||
|
|
448
|
+
record.replacementCapability.retryKey !== record.retryKey ||
|
|
449
|
+
record.replacementCapability.artifactDigest !== record.artifactBinding.artifactDigest ||
|
|
450
|
+
record.replacementCapability.caSpkiSha256 !== record.artifactBinding.caSpkiSha256)))
|
|
451
|
+
throw new Error('invalid');
|
|
311
452
|
return {
|
|
312
453
|
origin,
|
|
313
454
|
trustIdentity,
|
|
@@ -315,6 +456,7 @@ function decodePendingServerEnrollment(stored, origin, trustIdentity) {
|
|
|
315
456
|
retryKey: record.retryKey,
|
|
316
457
|
credential: record.credential,
|
|
317
458
|
...(record.clientName === undefined ? {} : { clientName: record.clientName }),
|
|
459
|
+
...(record.artifactBinding === undefined ? {} : { artifactBinding: record.artifactBinding }),
|
|
318
460
|
...(record.replacementCapability === undefined ? {} : { replacementCapability: record.replacementCapability }),
|
|
319
461
|
};
|
|
320
462
|
}
|
|
@@ -323,7 +465,9 @@ export async function getPendingServerEnrollment(origin, trustIdentity) {
|
|
|
323
465
|
validateServerCredentialBinding(origin, trustIdentity);
|
|
324
466
|
const backend = await getServerCredentialBackend();
|
|
325
467
|
const account = serverPendingEnrollmentAccount(origin, trustIdentity);
|
|
326
|
-
return withCredentialStoreLock(async () => {
|
|
468
|
+
return withEnrollmentOriginLock(origin, () => withCredentialStoreLock(async () => {
|
|
469
|
+
if (await markerForOriginUnlocked(backend, origin))
|
|
470
|
+
throw new InvitationArtifactRecoveryError();
|
|
327
471
|
const stored = await backend.get(account);
|
|
328
472
|
if (!stored)
|
|
329
473
|
return null;
|
|
@@ -333,7 +477,7 @@ export async function getPendingServerEnrollment(origin, trustIdentity) {
|
|
|
333
477
|
catch {
|
|
334
478
|
throw new Error('pending Borg server enrollment is corrupt');
|
|
335
479
|
}
|
|
336
|
-
});
|
|
480
|
+
}), { processShared: processSharedEnrollmentLock() });
|
|
337
481
|
}
|
|
338
482
|
/** Find the sole pending enrollment so artifact-only retries need no invitation. */
|
|
339
483
|
export async function findPendingServerEnrollment() {
|
|
@@ -342,16 +486,33 @@ export async function findPendingServerEnrollment() {
|
|
|
342
486
|
return null;
|
|
343
487
|
return withCredentialStoreLock(async () => {
|
|
344
488
|
const matches = [];
|
|
345
|
-
for (const value of Object.
|
|
489
|
+
for (const [account, value] of Object.entries(await backend.entries())) {
|
|
346
490
|
try {
|
|
347
491
|
const raw = JSON.parse(value);
|
|
348
|
-
if (raw.version
|
|
492
|
+
if (raw.version === SERVER_ACCEPTED_ENROLLMENT_MARKER_VERSION && raw.state === 'accepted') {
|
|
493
|
+
const marker = decodeAcceptedEnrollmentMarker(value);
|
|
494
|
+
if (account !== serverAcceptedEnrollmentAccount(marker.origin)) {
|
|
495
|
+
throw new InvitationArtifactRecoveryError(MISKEYED_RECOVERY_ERROR);
|
|
496
|
+
}
|
|
497
|
+
throw new InvitationArtifactRecoveryError();
|
|
498
|
+
}
|
|
499
|
+
if (raw.version !== SERVER_PENDING_ENROLLMENT_RECORD_VERSION &&
|
|
500
|
+
raw.version !== LEGACY_SERVER_PENDING_ENROLLMENT_RECORD_VERSION ||
|
|
501
|
+
raw.state !== 'pending')
|
|
349
502
|
continue;
|
|
350
503
|
if (typeof raw.origin !== 'string' || typeof raw.trustIdentity !== 'string')
|
|
351
504
|
continue;
|
|
505
|
+
if (account !== serverPendingEnrollmentAccount(raw.origin, raw.trustIdentity)) {
|
|
506
|
+
throw new InvitationArtifactRecoveryError(MISKEYED_RECOVERY_ERROR);
|
|
507
|
+
}
|
|
352
508
|
matches.push(decodePendingServerEnrollment(value, raw.origin, raw.trustIdentity));
|
|
353
509
|
}
|
|
354
|
-
catch {
|
|
510
|
+
catch (error) {
|
|
511
|
+
if (error instanceof InvitationArtifactRecoveryError)
|
|
512
|
+
throw error;
|
|
513
|
+
if (account.startsWith('borg-server-enrollment-pending:') ||
|
|
514
|
+
account.startsWith('borg-server-enrollment-accepted:'))
|
|
515
|
+
throw new InvitationArtifactRecoveryError();
|
|
355
516
|
// Ignore unrelated or corrupt records; the keyed resume path remains fail-closed.
|
|
356
517
|
}
|
|
357
518
|
}
|
|
@@ -366,21 +527,37 @@ export async function findPendingServerEnrollment() {
|
|
|
366
527
|
* this makes response-loss retries exact without minting a second bearer.
|
|
367
528
|
*/
|
|
368
529
|
export async function getOrCreatePendingServerEnrollment(input) {
|
|
530
|
+
return getOrCreatePendingServerEnrollmentInternal(input, false);
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Mint replacement consent only for the fresh interactive confirmation path.
|
|
534
|
+
* Resume can load and consume the resulting capability, but has no mint API.
|
|
535
|
+
*/
|
|
536
|
+
export async function createPendingServerEnrollmentWithReplacementConsent(input) {
|
|
537
|
+
return getOrCreatePendingServerEnrollmentInternal(input, true);
|
|
538
|
+
}
|
|
539
|
+
async function getOrCreatePendingServerEnrollmentInternal(input, mintReplacementConsent) {
|
|
369
540
|
validateServerCredentialBinding(input.origin, input.trustIdentity);
|
|
370
541
|
validateInvitation(input.invitation);
|
|
371
542
|
validateClientName(input.clientName);
|
|
372
|
-
|
|
373
|
-
|
|
543
|
+
validateArtifactBinding(input.artifactBinding);
|
|
544
|
+
if (input.artifactBinding !== undefined &&
|
|
545
|
+
createHash('sha256').update(input.invitation).digest('hex') !== input.artifactBinding.artifactDigest)
|
|
546
|
+
throw new Error('enrollment invitation does not match its verified artifact binding');
|
|
547
|
+
if (mintReplacementConsent && input.artifactBinding === undefined) {
|
|
548
|
+
throw new Error('replacement consent requires a verified invitation artifact binding');
|
|
549
|
+
}
|
|
374
550
|
const backend = await getServerCredentialBackend();
|
|
375
551
|
const account = serverPendingEnrollmentAccount(input.origin, input.trustIdentity);
|
|
376
|
-
return withCredentialStoreLock(async () => {
|
|
552
|
+
return withEnrollmentOriginLock(input.origin, () => withCredentialStoreLock(async () => {
|
|
377
553
|
const stored = await backend.get(account);
|
|
378
554
|
if (stored) {
|
|
379
555
|
try {
|
|
380
556
|
const record = decodePendingServerEnrollment(stored, input.origin, input.trustIdentity);
|
|
381
557
|
if (record.invitation !== input.invitation ||
|
|
382
558
|
record.clientName !== input.clientName
|
|
383
|
-
|| record.
|
|
559
|
+
|| JSON.stringify(record.artifactBinding) !== JSON.stringify(input.artifactBinding)
|
|
560
|
+
|| mintReplacementConsent && record.replacementCapability === undefined) {
|
|
384
561
|
throw new Error('mismatch');
|
|
385
562
|
}
|
|
386
563
|
return record;
|
|
@@ -389,14 +566,26 @@ export async function getOrCreatePendingServerEnrollment(input) {
|
|
|
389
566
|
throw new Error('pending Borg server enrollment does not match this request');
|
|
390
567
|
}
|
|
391
568
|
}
|
|
569
|
+
const retryKey = randomUUID();
|
|
570
|
+
const accounts = backend.entries ? await backend.entries() : {};
|
|
571
|
+
const replacementCapability = mintReplacementConsent ? {
|
|
572
|
+
token: randomUUID(),
|
|
573
|
+
priorAccountsDigest: digestAccountSnapshot(accounts, input.origin),
|
|
574
|
+
endpoint: input.origin,
|
|
575
|
+
caSpkiSha256: input.artifactBinding.caSpkiSha256,
|
|
576
|
+
trustIdentity: input.trustIdentity,
|
|
577
|
+
retryKey,
|
|
578
|
+
artifactDigest: input.artifactBinding.artifactDigest,
|
|
579
|
+
} : undefined;
|
|
392
580
|
const record = {
|
|
393
581
|
origin: input.origin,
|
|
394
582
|
trustIdentity: input.trustIdentity,
|
|
395
583
|
invitation: input.invitation,
|
|
396
|
-
retryKey
|
|
584
|
+
retryKey,
|
|
397
585
|
credential: randomBytes(32).toString('base64url'),
|
|
398
586
|
...(input.clientName === undefined ? {} : { clientName: input.clientName }),
|
|
399
|
-
...(input.
|
|
587
|
+
...(input.artifactBinding === undefined ? {} : { artifactBinding: input.artifactBinding }),
|
|
588
|
+
...(replacementCapability === undefined ? {} : { replacementCapability }),
|
|
400
589
|
};
|
|
401
590
|
validateEnrollmentCredential(record.credential);
|
|
402
591
|
await backend.set(account, JSON.stringify({
|
|
@@ -405,7 +594,7 @@ export async function getOrCreatePendingServerEnrollment(input) {
|
|
|
405
594
|
...record,
|
|
406
595
|
}));
|
|
407
596
|
return record;
|
|
408
|
-
});
|
|
597
|
+
}), { processShared: processSharedEnrollmentLock() });
|
|
409
598
|
}
|
|
410
599
|
/** Activate the exact pending tuple only after a verified server response. */
|
|
411
600
|
export async function activatePendingServerEnrollment(input) {
|
|
@@ -416,34 +605,53 @@ export async function activatePendingServerEnrollment(input) {
|
|
|
416
605
|
const serverCapabilities = validateServerCapabilities(input.serverCapabilities);
|
|
417
606
|
const backend = await getServerCredentialBackend();
|
|
418
607
|
const pendingAccount = serverPendingEnrollmentAccount(input.origin, input.trustIdentity);
|
|
419
|
-
await withCredentialStoreLock(async () => {
|
|
608
|
+
await withEnrollmentOriginLock(input.origin, () => withCredentialStoreLock(async () => {
|
|
609
|
+
if (!backend.entries || !backend.replaceAccounts) {
|
|
610
|
+
throw new Error('Borg credential store does not support atomic enrollment activation');
|
|
611
|
+
}
|
|
420
612
|
const stored = await backend.get(pendingAccount);
|
|
421
613
|
if (!stored)
|
|
422
614
|
throw new Error('pending Borg server enrollment is missing');
|
|
423
615
|
try {
|
|
424
616
|
const pending = decodePendingServerEnrollment(stored, input.origin, input.trustIdentity);
|
|
425
|
-
if (pending.retryKey !== input.retryKey || pending.credential !== input.credential
|
|
426
|
-
pending.replacementCapability !== input.replacementCapability) {
|
|
617
|
+
if (pending.retryKey !== input.retryKey || pending.credential !== input.credential) {
|
|
427
618
|
throw new Error('mismatch');
|
|
428
619
|
}
|
|
429
620
|
}
|
|
430
621
|
catch {
|
|
431
622
|
throw new Error('pending Borg server enrollment does not match the verified response');
|
|
432
623
|
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
624
|
+
const pending = decodePendingServerEnrollment(stored, input.origin, input.trustIdentity);
|
|
625
|
+
const accounts = await backend.entries();
|
|
626
|
+
const activeAccounts = Object.fromEntries(Object.entries(accounts).filter(([, value]) => {
|
|
627
|
+
try {
|
|
628
|
+
const record = JSON.parse(value);
|
|
629
|
+
return record.version === SERVER_CREDENTIAL_RECORD_VERSION && record.origin === input.origin;
|
|
630
|
+
}
|
|
631
|
+
catch {
|
|
632
|
+
return false;
|
|
633
|
+
}
|
|
634
|
+
}));
|
|
635
|
+
if (Object.keys(activeAccounts).length > 0) {
|
|
636
|
+
const capability = pending.replacementCapability;
|
|
637
|
+
if (!capability || capability.token !== input.replacementCapabilityToken) {
|
|
638
|
+
throw new Error('local Borg server enrollment already exists and replacement consent is missing');
|
|
639
|
+
}
|
|
640
|
+
validateReplacementCapability(capability);
|
|
641
|
+
if (capability.priorAccountsDigest !== digestAccountSnapshot(accounts, input.origin) ||
|
|
642
|
+
capability.endpoint !== input.origin ||
|
|
643
|
+
capability.trustIdentity !== input.trustIdentity ||
|
|
644
|
+
capability.retryKey !== input.retryKey ||
|
|
645
|
+
capability.artifactDigest !== pending.artifactBinding?.artifactDigest ||
|
|
646
|
+
capability.caSpkiSha256 !== pending.artifactBinding?.caSpkiSha256)
|
|
647
|
+
throw new Error('Borg enrollment replacement consent does not match this transaction');
|
|
648
|
+
}
|
|
649
|
+
else if (input.replacementCapabilityToken !== undefined) {
|
|
650
|
+
throw new Error('Borg enrollment replacement consent does not match this transaction');
|
|
651
|
+
}
|
|
652
|
+
const generationId = input.generationId ?? pending.artifactBinding?.stagedGenerationId;
|
|
653
|
+
if (pending.artifactBinding && generationId !== pending.artifactBinding.stagedGenerationId) {
|
|
654
|
+
throw new Error('pending Borg server enrollment generation does not match the verified response');
|
|
447
655
|
}
|
|
448
656
|
const activeRecord = {
|
|
449
657
|
origin: input.origin,
|
|
@@ -452,31 +660,180 @@ export async function activatePendingServerEnrollment(input) {
|
|
|
452
660
|
clientId: input.clientId,
|
|
453
661
|
serverCapabilities,
|
|
454
662
|
};
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
663
|
+
const target = serverCredentialAccount(input.origin, input.trustIdentity);
|
|
664
|
+
const next = { ...accounts };
|
|
665
|
+
for (const [account, value] of Object.entries(accounts)) {
|
|
666
|
+
try {
|
|
667
|
+
const parsed = JSON.parse(value);
|
|
668
|
+
if (parsed.version === SERVER_CREDENTIAL_RECORD_VERSION && parsed.origin === input.origin) {
|
|
669
|
+
delete next[account];
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
catch {
|
|
673
|
+
// Preserve unrelated malformed records; the backend's normal validation remains fail-closed.
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
const activeValue = encodeServerCredentialRecord(activeRecord, serverCapabilities);
|
|
677
|
+
next[target] = activeValue;
|
|
678
|
+
delete next[pendingAccount];
|
|
679
|
+
if (generationId !== undefined) {
|
|
680
|
+
const rollbackAccount = serverEnrollmentRollbackAccount(input.origin, input.retryKey);
|
|
681
|
+
const rollbackRecord = {
|
|
682
|
+
version: 1,
|
|
683
|
+
state: 'rollback-snapshot',
|
|
684
|
+
origin: input.origin,
|
|
685
|
+
snapshot: { activeAccounts, pendingAccount, pendingValue: stored },
|
|
686
|
+
};
|
|
687
|
+
const rollbackValue = JSON.stringify(rollbackRecord);
|
|
688
|
+
const marker = {
|
|
689
|
+
version: SERVER_ACCEPTED_ENROLLMENT_MARKER_VERSION,
|
|
690
|
+
state: 'accepted',
|
|
691
|
+
origin: input.origin,
|
|
692
|
+
trustIdentity: input.trustIdentity,
|
|
693
|
+
generationId,
|
|
694
|
+
previousPointer: input.previousPointer ?? null,
|
|
695
|
+
activeDigest: createHash('sha256').update(activeValue).digest('hex'),
|
|
696
|
+
rollbackAccount,
|
|
697
|
+
rollbackDigest: createHash('sha256').update(rollbackValue).digest('hex'),
|
|
698
|
+
};
|
|
699
|
+
next[rollbackAccount] = rollbackValue;
|
|
700
|
+
next[serverAcceptedEnrollmentAccount(input.origin)] = JSON.stringify(marker);
|
|
701
|
+
}
|
|
702
|
+
await backend.replaceAccounts(next);
|
|
703
|
+
}), { processShared: processSharedEnrollmentLock() });
|
|
704
|
+
}
|
|
705
|
+
export async function getAcceptedEnrollmentMarker(origin) {
|
|
706
|
+
const backend = await getServerCredentialBackend();
|
|
707
|
+
return withEnrollmentOriginLock(origin, () => withCredentialStoreLock(() => markerForOriginUnlocked(backend, origin)), { processShared: processSharedEnrollmentLock() });
|
|
708
|
+
}
|
|
709
|
+
/** Remove the gate only after the pointer and active account have been verified together. */
|
|
710
|
+
export async function finalizeAcceptedEnrollment(origin, trustIdentity, generationId) {
|
|
711
|
+
const backend = await getServerCredentialBackend();
|
|
712
|
+
await withEnrollmentOriginLock(origin, () => withCredentialStoreLock(async () => {
|
|
713
|
+
if (!backend.entries || !backend.replaceAccounts) {
|
|
714
|
+
throw new Error('Borg credential store does not support atomic enrollment finalization');
|
|
715
|
+
}
|
|
716
|
+
const accounts = await backend.entries();
|
|
717
|
+
const markerValue = accounts[serverAcceptedEnrollmentAccount(origin)];
|
|
718
|
+
if (!markerValue)
|
|
719
|
+
return;
|
|
720
|
+
const marker = decodeAcceptedEnrollmentMarker(markerValue);
|
|
721
|
+
if (marker.trustIdentity !== trustIdentity || marker.generationId !== generationId) {
|
|
722
|
+
throw new Error('Borg enrollment recovery marker does not match committed trust');
|
|
723
|
+
}
|
|
724
|
+
const active = accounts[serverCredentialAccount(origin, trustIdentity)];
|
|
725
|
+
if (!active)
|
|
726
|
+
throw new Error('Borg enrollment recovery marker has no matching active credential');
|
|
727
|
+
if (createHash('sha256').update(active).digest('hex') !== marker.activeDigest) {
|
|
728
|
+
throw new Error('Borg enrollment recovery marker active credential does not match the committed transaction');
|
|
729
|
+
}
|
|
730
|
+
try {
|
|
731
|
+
decodeActiveServerCredentialRecord(active, origin, trustIdentity);
|
|
732
|
+
}
|
|
733
|
+
catch {
|
|
734
|
+
throw new Error('Borg enrollment recovery marker active credential is invalid');
|
|
735
|
+
}
|
|
736
|
+
const next = { ...accounts };
|
|
737
|
+
delete next[serverAcceptedEnrollmentAccount(origin)];
|
|
738
|
+
delete next[marker.rollbackAccount];
|
|
739
|
+
await backend.replaceAccounts(next);
|
|
740
|
+
}), { processShared: processSharedEnrollmentLock() });
|
|
741
|
+
}
|
|
742
|
+
/** Restore the complete pre-commit account snapshot after pointer restoration. */
|
|
743
|
+
export async function restoreAcceptedEnrollmentAccounts(marker) {
|
|
744
|
+
const backend = await getServerCredentialBackend();
|
|
745
|
+
await withEnrollmentOriginLock(marker.origin, () => withCredentialStoreLock(async () => {
|
|
746
|
+
if (!backend.entries || !backend.replaceAccounts) {
|
|
747
|
+
throw new Error('Borg credential store does not support atomic enrollment rollback');
|
|
748
|
+
}
|
|
749
|
+
const accounts = await backend.entries();
|
|
750
|
+
const current = accounts[serverAcceptedEnrollmentAccount(marker.origin)];
|
|
751
|
+
if (!current || JSON.stringify(decodeAcceptedEnrollmentMarker(current)) !== JSON.stringify(marker)) {
|
|
752
|
+
throw new Error('Borg enrollment recovery marker changed during rollback');
|
|
753
|
+
}
|
|
754
|
+
const rollbackValue = accounts[marker.rollbackAccount];
|
|
755
|
+
if (!rollbackValue)
|
|
756
|
+
throw new Error('Borg enrollment rollback snapshot is missing');
|
|
757
|
+
const rollback = decodeRollbackRecord(rollbackValue, marker).snapshot;
|
|
758
|
+
const next = { ...accounts };
|
|
759
|
+
for (const [account, value] of Object.entries(accounts)) {
|
|
760
|
+
try {
|
|
761
|
+
const parsed = JSON.parse(value);
|
|
762
|
+
if (parsed.version === SERVER_CREDENTIAL_RECORD_VERSION && parsed.origin === marker.origin) {
|
|
763
|
+
delete next[account];
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
catch {
|
|
767
|
+
// Preserve unrelated malformed entries for the backend's fail-closed reader.
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
Object.assign(next, rollback.activeAccounts);
|
|
771
|
+
next[rollback.pendingAccount] = rollback.pendingValue;
|
|
772
|
+
delete next[serverAcceptedEnrollmentAccount(marker.origin)];
|
|
773
|
+
delete next[marker.rollbackAccount];
|
|
774
|
+
await backend.replaceAccounts(next);
|
|
775
|
+
}), { processShared: processSharedEnrollmentLock() });
|
|
776
|
+
}
|
|
777
|
+
export async function findEnrollmentRecoveryTransaction(selectedOrigin) {
|
|
778
|
+
if (selectedOrigin !== undefined) {
|
|
779
|
+
let parsed;
|
|
780
|
+
try {
|
|
781
|
+
parsed = new URL(selectedOrigin);
|
|
782
|
+
}
|
|
783
|
+
catch {
|
|
784
|
+
throw new Error('invalid Borg server credential origin');
|
|
785
|
+
}
|
|
786
|
+
if (parsed.origin !== selectedOrigin || parsed.protocol !== 'https:') {
|
|
787
|
+
throw new Error('Borg server credentials require a canonical HTTPS origin');
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
const backend = await getServerCredentialBackend();
|
|
791
|
+
if (!backend.entries)
|
|
792
|
+
return null;
|
|
793
|
+
return withCredentialStoreLock(async () => {
|
|
794
|
+
const accepted = [];
|
|
795
|
+
const pending = [];
|
|
796
|
+
for (const [account, value] of Object.entries(await backend.entries())) {
|
|
797
|
+
try {
|
|
798
|
+
const raw = JSON.parse(value);
|
|
799
|
+
if (raw.version === SERVER_ACCEPTED_ENROLLMENT_MARKER_VERSION && raw.state === 'accepted') {
|
|
800
|
+
const marker = decodeAcceptedEnrollmentMarker(value);
|
|
801
|
+
if (account !== serverAcceptedEnrollmentAccount(marker.origin)) {
|
|
802
|
+
throw new InvitationArtifactRecoveryError(MISKEYED_RECOVERY_ERROR);
|
|
466
803
|
}
|
|
804
|
+
if (selectedOrigin === undefined || marker.origin === selectedOrigin)
|
|
805
|
+
accepted.push(marker);
|
|
467
806
|
}
|
|
468
|
-
|
|
469
|
-
|
|
807
|
+
else if ((raw.version === SERVER_PENDING_ENROLLMENT_RECORD_VERSION ||
|
|
808
|
+
raw.version === LEGACY_SERVER_PENDING_ENROLLMENT_RECORD_VERSION) &&
|
|
809
|
+
raw.state === 'pending' &&
|
|
810
|
+
typeof raw.origin === 'string' && typeof raw.trustIdentity === 'string') {
|
|
811
|
+
const record = decodePendingServerEnrollment(value, raw.origin, raw.trustIdentity);
|
|
812
|
+
if (account !== serverPendingEnrollmentAccount(raw.origin, raw.trustIdentity)) {
|
|
813
|
+
throw new InvitationArtifactRecoveryError(MISKEYED_RECOVERY_ERROR);
|
|
814
|
+
}
|
|
815
|
+
if (selectedOrigin === undefined || record.origin === selectedOrigin)
|
|
816
|
+
pending.push(record);
|
|
470
817
|
}
|
|
471
818
|
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
819
|
+
catch (error) {
|
|
820
|
+
if (error instanceof InvitationArtifactRecoveryError)
|
|
821
|
+
throw error;
|
|
822
|
+
if (account.startsWith('borg-server-enrollment-accepted:') ||
|
|
823
|
+
account.startsWith('borg-server-enrollment-pending:') ||
|
|
824
|
+
account.startsWith('borg-server-enrollment-rollback:'))
|
|
825
|
+
throw new InvitationArtifactRecoveryError();
|
|
826
|
+
// Unrelated corrupt entries are preserved for their owning reader.
|
|
827
|
+
}
|
|
475
828
|
}
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
await backend.delete(pendingAccount);
|
|
829
|
+
if (accepted.length + pending.length > 1) {
|
|
830
|
+
throw new Error('multiple Borg enrollment transactions require an explicit server endpoint');
|
|
479
831
|
}
|
|
832
|
+
if (accepted[0])
|
|
833
|
+
return { kind: 'accepted', marker: accepted[0] };
|
|
834
|
+
if (pending[0])
|
|
835
|
+
return { kind: 'pending', pending: pending[0] };
|
|
836
|
+
return null;
|
|
480
837
|
});
|
|
481
838
|
}
|
|
482
839
|
/** Delete only the exact definitively rejected pending attempt. */
|
|
@@ -484,7 +841,7 @@ export async function clearPendingServerEnrollment(origin, trustIdentity, retryK
|
|
|
484
841
|
validateUuid(retryKey, 'enrollment retry key');
|
|
485
842
|
const backend = await getServerCredentialBackend();
|
|
486
843
|
const account = serverPendingEnrollmentAccount(origin, trustIdentity);
|
|
487
|
-
await withCredentialStoreLock(async () => {
|
|
844
|
+
await withEnrollmentOriginLock(origin, () => withCredentialStoreLock(async () => {
|
|
488
845
|
const stored = await backend.get(account);
|
|
489
846
|
if (!stored)
|
|
490
847
|
return;
|
|
@@ -497,7 +854,7 @@ export async function clearPendingServerEnrollment(origin, trustIdentity, retryK
|
|
|
497
854
|
return;
|
|
498
855
|
}
|
|
499
856
|
await backend.delete(account);
|
|
500
|
-
});
|
|
857
|
+
}), { processShared: processSharedEnrollmentLock() });
|
|
501
858
|
}
|
|
502
859
|
/** Persist one repository-scoped cube-create idempotency key in the 0600 credential store. */
|
|
503
860
|
export async function getOrCreatePendingServerCubeCreation(input) {
|
|
@@ -598,28 +955,53 @@ export async function clearPendingServerCubeCreation(record) {
|
|
|
598
955
|
export async function clearServerCredential(origin, trustIdentity) {
|
|
599
956
|
const backend = await getServerCredentialBackend();
|
|
600
957
|
const pendingAccount = serverPendingEnrollmentAccount(origin, trustIdentity);
|
|
601
|
-
await withCredentialStoreLock(async () => {
|
|
958
|
+
await withEnrollmentOriginLock(origin, () => withCredentialStoreLock(async () => {
|
|
602
959
|
await backend.delete(serverCredentialAccount(origin, trustIdentity));
|
|
603
960
|
await backend.delete(pendingAccount);
|
|
604
|
-
});
|
|
961
|
+
}), { processShared: processSharedEnrollmentLock() });
|
|
605
962
|
}
|
|
606
|
-
/** Clear only the failed enrollment transaction
|
|
607
|
-
export async function clearEnrollmentTransaction(
|
|
608
|
-
validateServerCredentialBinding(origin, trustIdentity);
|
|
963
|
+
/** Clear only the exact failed enrollment transaction that the operator reviewed. */
|
|
964
|
+
export async function clearEnrollmentTransaction(expected) {
|
|
965
|
+
validateServerCredentialBinding(expected.origin, expected.trustIdentity);
|
|
609
966
|
const backend = await getServerCredentialBackend();
|
|
610
|
-
const pendingAccount = serverPendingEnrollmentAccount(origin, trustIdentity);
|
|
611
|
-
|
|
612
|
-
|
|
967
|
+
const pendingAccount = serverPendingEnrollmentAccount(expected.origin, expected.trustIdentity);
|
|
968
|
+
return withEnrollmentOriginLock(expected.origin, () => withCredentialStoreLock(async () => {
|
|
969
|
+
if (await markerForOriginUnlocked(backend, expected.origin)) {
|
|
970
|
+
throw new InvitationArtifactRecoveryError();
|
|
971
|
+
}
|
|
613
972
|
if (backend.entries && backend.replaceAccounts) {
|
|
614
973
|
const accounts = await backend.entries();
|
|
974
|
+
const stored = accounts[pendingAccount];
|
|
975
|
+
if (!stored)
|
|
976
|
+
return false;
|
|
977
|
+
let current;
|
|
978
|
+
try {
|
|
979
|
+
current = decodePendingServerEnrollment(stored, expected.origin, expected.trustIdentity);
|
|
980
|
+
}
|
|
981
|
+
catch {
|
|
982
|
+
return false;
|
|
983
|
+
}
|
|
984
|
+
if (JSON.stringify(current) !== JSON.stringify(expected))
|
|
985
|
+
return false;
|
|
615
986
|
const next = { ...accounts };
|
|
616
987
|
delete next[pendingAccount];
|
|
617
|
-
delete next[credentialAccount];
|
|
618
988
|
await backend.replaceAccounts(next);
|
|
619
|
-
return;
|
|
989
|
+
return (await backend.get(pendingAccount)) === null;
|
|
990
|
+
}
|
|
991
|
+
const stored = await backend.get(pendingAccount);
|
|
992
|
+
if (!stored)
|
|
993
|
+
return false;
|
|
994
|
+
let current;
|
|
995
|
+
try {
|
|
996
|
+
current = decodePendingServerEnrollment(stored, expected.origin, expected.trustIdentity);
|
|
620
997
|
}
|
|
998
|
+
catch {
|
|
999
|
+
return false;
|
|
1000
|
+
}
|
|
1001
|
+
if (JSON.stringify(current) !== JSON.stringify(expected))
|
|
1002
|
+
return false;
|
|
621
1003
|
await backend.delete(pendingAccount);
|
|
622
|
-
await backend.
|
|
623
|
-
});
|
|
1004
|
+
return (await backend.get(pendingAccount)) === null;
|
|
1005
|
+
}), { processShared: processSharedEnrollmentLock() });
|
|
624
1006
|
}
|
|
625
1007
|
//# sourceMappingURL=config.js.map
|