@skrr-ai/cli 0.1.58 → 0.1.60
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/base-command.d.ts +0 -16
- package/dist/base-command.js +65 -11
- package/dist/commands/followups/checkin.js +16 -3
- package/dist/commands/logout.d.ts +0 -46
- package/dist/commands/logout.js +21 -3
- package/dist/lib/agentic-stream.d.ts +49 -0
- package/dist/lib/agentic-stream.js +140 -15
- package/dist/lib/api-fetch.d.ts +11 -0
- package/dist/lib/api-fetch.js +98 -1
- package/dist/lib/daemonBrokerRefusal.d.ts +21 -0
- package/dist/lib/daemonBrokerRefusal.js +87 -2
- package/dist/lib/dedicated-guest.d.ts +13 -0
- package/dist/lib/dedicated-guest.js +22 -0
- package/dist/lib/dedicated-machines.js +17 -1
- package/dist/lib/dedicated-service.d.ts +1 -13
- package/dist/lib/dedicated-service.js +7 -19
- package/dist/lib/followups.d.ts +7 -0
- package/dist/lib/followups.js +17 -4
- package/dist/lib/login.js +10 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.d.ts +41 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.js +147 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.d.ts +17 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +38 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +2 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +5 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/profileStateDir.d.ts +21 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/profileStateDir.js +40 -7
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.d.ts +41 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.js +147 -4
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.d.ts +17 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +38 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +2 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +2 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/profileStateDir.d.ts +21 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/profileStateDir.js +39 -6
- package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +4397 -4363
- package/oclif.manifest.json +17475 -17475
- package/package.json +1 -1
|
@@ -7,6 +7,7 @@ exports.DEFAULT_UNREADABLE_CREDENTIAL_HINT = void 0;
|
|
|
7
7
|
exports.__setStateForTest = __setStateForTest;
|
|
8
8
|
exports.__getStateForTest = __getStateForTest;
|
|
9
9
|
exports.__clearWarnDedupeForTest = __clearWarnDedupeForTest;
|
|
10
|
+
exports.splitKeyConvergencePolicy = splitKeyConvergencePolicy;
|
|
10
11
|
exports.initCredEnvelope = initCredEnvelope;
|
|
11
12
|
exports.resetCredEnvelope = resetCredEnvelope;
|
|
12
13
|
exports.shutdownCredEnvelope = shutdownCredEnvelope;
|
|
@@ -67,6 +68,7 @@ const proper_lockfile_1 = __importDefault(require("proper-lockfile"));
|
|
|
67
68
|
const credentialEnvelope_js_1 = require("./credentialEnvelope.js");
|
|
68
69
|
const index_js_1 = require("./kek/index.js");
|
|
69
70
|
const profileStateDir_js_1 = require("./profileStateDir.js");
|
|
71
|
+
const refresh_js_1 = require("./refresh.js");
|
|
70
72
|
const runtime_js_1 = require("./runtime.js");
|
|
71
73
|
/**
|
|
72
74
|
* Account name the wrapped DEK lives under (file fallback). Keeping the
|
|
@@ -259,6 +261,20 @@ async function withCredEnvelopeLock(profile, body) {
|
|
|
259
261
|
* auth-core's to do), so being loud about it IS the remedy.
|
|
260
262
|
*/
|
|
261
263
|
let _pathConflict = null;
|
|
264
|
+
/**
|
|
265
|
+
* DEKs this profile used before a converging consolidation retired them
|
|
266
|
+
* (OSK-12462). Read-only: nothing is ever sealed under one again. A value that
|
|
267
|
+
* only a retired DEK opens is returned with `needsMigration: true`, the same
|
|
268
|
+
* signal a legacy plaintext value carries, so every consumer that already
|
|
269
|
+
* upgrades plaintext in place re-seals it under the active DEK on its own
|
|
270
|
+
* schedule. auth-core does not need to know where the consumer keeps it.
|
|
271
|
+
*/
|
|
272
|
+
let _retiredDeks = [];
|
|
273
|
+
/**
|
|
274
|
+
* Set when THIS process converged a split profile onto the legacy key. For the
|
|
275
|
+
* status surface and tests; the on-disk marker is the durable record.
|
|
276
|
+
*/
|
|
277
|
+
let _convergedFrom = null;
|
|
262
278
|
/**
|
|
263
279
|
* Identity of a wrapped DEK: the SHA-256 of the key it unwraps to.
|
|
264
280
|
*
|
|
@@ -297,18 +313,48 @@ async function wrappedDekIdentity(target, kek, aad) {
|
|
|
297
313
|
* Never fatal. A migration that cannot run leaves the process exactly where it
|
|
298
314
|
* was, which is the state it has been operating in all along.
|
|
299
315
|
*/
|
|
316
|
+
/**
|
|
317
|
+
* Whether a split profile may be converged onto the legacy (daemon) key.
|
|
318
|
+
*
|
|
319
|
+
* Only the daemon ever has a legacy location, so only the daemon runs this, and
|
|
320
|
+
* the legacy key is the one it signs with. Since OSK-12063 the CLI enrolls no
|
|
321
|
+
* device key and only an interactive daemon login re-pins, so the key the server
|
|
322
|
+
* pins for this machine is the daemon's, UNLESS the daemon has itself been
|
|
323
|
+
* refused over its device key. That is the one fact that proves otherwise, and
|
|
324
|
+
* then nothing here knows which key the server holds, so it refuses as before.
|
|
325
|
+
*
|
|
326
|
+
* Why converging matters (OSK-12462): a CLI credential brokered by the daemon
|
|
327
|
+
* is tied to the daemon's machine row, and the server checks its refresh proof
|
|
328
|
+
* against the key pinned there. A CLI signing with a different key has that
|
|
329
|
+
* refresh refused as DEVICE_PROOF_THUMBPRINT_MISMATCH at its first renewal.
|
|
330
|
+
*/
|
|
331
|
+
function splitKeyConvergencePolicy() {
|
|
332
|
+
let reauthReason;
|
|
333
|
+
try {
|
|
334
|
+
const state = (0, refresh_js_1.needsReauthState)();
|
|
335
|
+
reauthReason = state.needed ? state.reason : undefined;
|
|
336
|
+
}
|
|
337
|
+
catch {
|
|
338
|
+
reauthReason = undefined;
|
|
339
|
+
}
|
|
340
|
+
return reauthReason === 'DEVICE_KEY_MISMATCH'
|
|
341
|
+
? { converge: false, reason: 'daemon_key_refused' }
|
|
342
|
+
: { converge: true, reason: 'daemon_key_pinned' };
|
|
343
|
+
}
|
|
300
344
|
async function consolidateWrappedDek(profile, kek, aad) {
|
|
301
345
|
const resolved = (0, profileStateDir_js_1.resolveProfileStateFile)(profile, ACCOUNT_CRED_DEK_WRAPPED);
|
|
302
346
|
if (!resolved.usingLegacy) {
|
|
303
347
|
_pathConflict = null;
|
|
304
348
|
return;
|
|
305
349
|
}
|
|
350
|
+
const policy = splitKeyConvergencePolicy();
|
|
306
351
|
let outcome;
|
|
307
352
|
try {
|
|
308
353
|
outcome = await withCredEnvelopeLock(profile, () => (0, profileStateDir_js_1.consolidateProfileState)(profile, {
|
|
309
354
|
files: [ACCOUNT_CRED_DEK_WRAPPED],
|
|
310
355
|
primary: ACCOUNT_CRED_DEK_WRAPPED,
|
|
311
356
|
identify: (dir) => wrappedDekIdentity(node_path_1.default.join(dir, ACCOUNT_CRED_DEK_WRAPPED), kek, aad),
|
|
357
|
+
onConflict: policy.converge ? 'adopt_legacy' : 'refuse',
|
|
312
358
|
onEvent: (name, success, meta) => emit(`cred_envelope.${name}`, success, meta),
|
|
313
359
|
}));
|
|
314
360
|
}
|
|
@@ -319,6 +365,19 @@ async function consolidateWrappedDek(profile, kek, aad) {
|
|
|
319
365
|
});
|
|
320
366
|
return;
|
|
321
367
|
}
|
|
368
|
+
if (outcome.status === 'converged') {
|
|
369
|
+
_pathConflict = null;
|
|
370
|
+
_convergedFrom = {
|
|
371
|
+
canonicalPath: node_path_1.default.join(outcome.canonicalDir, ACCOUNT_CRED_DEK_WRAPPED),
|
|
372
|
+
legacyPath: node_path_1.default.join(outcome.legacyDir ?? '', ACCOUNT_CRED_DEK_WRAPPED),
|
|
373
|
+
};
|
|
374
|
+
(0, runtime_js_1.getAuthLogger)().info(`[credEnvelope] profile "${profile}" had two different usable wrapped DEKs; ` +
|
|
375
|
+
`converged on the daemon's (${_convergedFrom.legacyPath}), which is the key the ` +
|
|
376
|
+
"server pins for this machine. The CLI's former key is kept beside it as " +
|
|
377
|
+
`${ACCOUNT_CRED_DEK_WRAPPED}${profileStateDir_js_1.RETIRED_COPY_SUFFIX}.* and still opens what was ` +
|
|
378
|
+
'sealed under it; each value is re-sealed under the shared key on its next write.');
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
322
381
|
if (outcome.status !== 'conflict') {
|
|
323
382
|
_pathConflict = null;
|
|
324
383
|
return;
|
|
@@ -335,13 +394,62 @@ async function consolidateWrappedDek(profile, kek, aad) {
|
|
|
335
394
|
// BINARY restarts and adopts its legacy key into the empty canonical slot,
|
|
336
395
|
// and the CLI logs back in on it. Not `skrr daemon restart`: any `skrr`
|
|
337
396
|
// command mints a fresh key into an empty slot before doing anything else.
|
|
338
|
-
|
|
397
|
+
//
|
|
398
|
+
// Reached only when the daemon itself was refused over its device key
|
|
399
|
+
// (`splitKeyConvergencePolicy`); an ordinary split converges above.
|
|
400
|
+
(0, runtime_js_1.getAuthLogger)().warn(`[credEnvelope] profile "${profile}" has two different usable wrapped DEKs, and ` +
|
|
401
|
+
"the server refused the daemon's key, so neither is known to be the pinned one; " +
|
|
339
402
|
`refusing to choose. Keeping ${_pathConflict.legacyPath} and leaving ` +
|
|
340
403
|
`${_pathConflict.canonicalPath} untouched. Each side can still read its own ` +
|
|
341
404
|
"credentials, and neither can read the other's. Do not delete " +
|
|
342
405
|
`${_pathConflict.legacyPath}: it is the key the daemon reads. To converge on it, ` +
|
|
343
406
|
'run `skrr logout && skrrd restart && skrr login`.');
|
|
344
407
|
}
|
|
408
|
+
/**
|
|
409
|
+
* Load every retired DEK beside the canonical one (OSK-12462). Only files a
|
|
410
|
+
* converging consolidation set aside are read; the `.superseded.*` copy an
|
|
411
|
+
* UNUSABLE key leaves is not a key anything can open. Read-only unwrap, never
|
|
412
|
+
* fatal: a retired key that no longer opens is simply not in the ring.
|
|
413
|
+
*/
|
|
414
|
+
async function loadRetiredDeks(profile, kek, aad, active) {
|
|
415
|
+
for (const old of _retiredDeks)
|
|
416
|
+
old.fill(0);
|
|
417
|
+
_retiredDeks = [];
|
|
418
|
+
for (const target of retiredDekFiles(profile)) {
|
|
419
|
+
try {
|
|
420
|
+
if (!kek.unwrapExisting)
|
|
421
|
+
return;
|
|
422
|
+
const dek = await kek.unwrapExisting(node_fs_1.default.readFileSync(target), aad);
|
|
423
|
+
if (dek.length === 32 && !dek.equals(active)) {
|
|
424
|
+
_retiredDeks.push(dek);
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
dek.fill(0);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
catch {
|
|
431
|
+
emit('cred_envelope.retired_dek.unreadable', false, {
|
|
432
|
+
profile,
|
|
433
|
+
file: node_path_1.default.basename(target),
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
/** Retired wrapped DEKs for `profile`, oldest first. */
|
|
439
|
+
function retiredDekFiles(profile) {
|
|
440
|
+
const dir = (0, profileStateDir_js_1.profileStateDir)(profile);
|
|
441
|
+
const prefix = `${ACCOUNT_CRED_DEK_WRAPPED}${profileStateDir_js_1.RETIRED_COPY_SUFFIX}.`;
|
|
442
|
+
try {
|
|
443
|
+
return node_fs_1.default
|
|
444
|
+
.readdirSync(dir)
|
|
445
|
+
.filter((name) => name.startsWith(prefix))
|
|
446
|
+
.sort()
|
|
447
|
+
.map((name) => node_path_1.default.join(dir, name));
|
|
448
|
+
}
|
|
449
|
+
catch {
|
|
450
|
+
return [];
|
|
451
|
+
}
|
|
452
|
+
}
|
|
345
453
|
/**
|
|
346
454
|
* Initialize the envelope path. Idempotent — second call when state is
|
|
347
455
|
* non-uninit returns immediately.
|
|
@@ -416,6 +524,7 @@ async function initCredEnvelope(profile) {
|
|
|
416
524
|
const wrappedBytes = node_fs_1.default.readFileSync(targetPath);
|
|
417
525
|
const candidate = await kek.unwrapExisting(wrappedBytes, aad);
|
|
418
526
|
if (candidate.length === 32) {
|
|
527
|
+
await loadRetiredDeks(profile, kek, aad, candidate);
|
|
419
528
|
_state = {
|
|
420
529
|
kind: 'active',
|
|
421
530
|
dek: candidate,
|
|
@@ -523,6 +632,7 @@ async function initCredEnvelope(profile) {
|
|
|
523
632
|
return;
|
|
524
633
|
}
|
|
525
634
|
}
|
|
635
|
+
await loadRetiredDeks(profile, kek, aad, dek);
|
|
526
636
|
_state = {
|
|
527
637
|
kind: 'active',
|
|
528
638
|
dek: dek,
|
|
@@ -553,7 +663,12 @@ async function initCredEnvelope(profile) {
|
|
|
553
663
|
*/
|
|
554
664
|
async function resetCredEnvelope(opts) {
|
|
555
665
|
if (opts?.clearOnDisk && opts.profile) {
|
|
556
|
-
|
|
666
|
+
// A logout ends every key this profile held, the retired ones included: a
|
|
667
|
+
// retired DEK exists only to open values the logout is deleting anyway.
|
|
668
|
+
for (const target of [
|
|
669
|
+
...(0, profileStateDir_js_1.profileStateFilePathsToClear)(opts.profile, ACCOUNT_CRED_DEK_WRAPPED),
|
|
670
|
+
...retiredDekFiles(opts.profile),
|
|
671
|
+
]) {
|
|
557
672
|
try {
|
|
558
673
|
node_fs_1.default.unlinkSync(target);
|
|
559
674
|
}
|
|
@@ -574,6 +689,10 @@ async function resetCredEnvelope(opts) {
|
|
|
574
689
|
_state = { kind: 'uninit' };
|
|
575
690
|
_kekRotationDetected = false;
|
|
576
691
|
_pathConflict = null;
|
|
692
|
+
_convergedFrom = null;
|
|
693
|
+
for (const old of _retiredDeks)
|
|
694
|
+
old.fill(0);
|
|
695
|
+
_retiredDeks = [];
|
|
577
696
|
}
|
|
578
697
|
/**
|
|
579
698
|
* Idempotent shutdown — must be called from the daemon's graceful-exit
|
|
@@ -612,6 +731,10 @@ function shutdownCredEnvelope() {
|
|
|
612
731
|
_state = { kind: 'uninit' };
|
|
613
732
|
_kekRotationDetected = false;
|
|
614
733
|
_pathConflict = null;
|
|
734
|
+
_convergedFrom = null;
|
|
735
|
+
for (const old of _retiredDeks)
|
|
736
|
+
old.fill(0);
|
|
737
|
+
_retiredDeks = [];
|
|
615
738
|
}
|
|
616
739
|
/** @internal Test seam — re-arm `shutdownCredEnvelope` so it can run again. */
|
|
617
740
|
function __resetShutdownGuardForTest() {
|
|
@@ -647,6 +770,10 @@ function hasCredEnvelopePathConflict() {
|
|
|
647
770
|
* `oversky status`; clears on the next clean `resetCredEnvelope`.
|
|
648
771
|
*/
|
|
649
772
|
function describeCredEnvelopeState() {
|
|
773
|
+
const extras = {
|
|
774
|
+
...(_convergedFrom ? { convergedFrom: _convergedFrom } : {}),
|
|
775
|
+
...(_retiredDeks.length > 0 ? { retiredKeys: _retiredDeks.length } : {}),
|
|
776
|
+
};
|
|
650
777
|
if (_state.kind === 'disabled') {
|
|
651
778
|
return {
|
|
652
779
|
kind: 'disabled',
|
|
@@ -663,6 +790,7 @@ function describeCredEnvelopeState() {
|
|
|
663
790
|
profile: _state.profile,
|
|
664
791
|
...(_kekRotationDetected ? { kekRotated: true } : {}),
|
|
665
792
|
...(_pathConflict ? { pathConflict: _pathConflict } : {}),
|
|
793
|
+
...extras,
|
|
666
794
|
};
|
|
667
795
|
}
|
|
668
796
|
return { kind: 'uninit', ...(_pathConflict ? { pathConflict: _pathConflict } : {}) };
|
|
@@ -784,12 +912,28 @@ function maybeDecryptOnRead(stored, options = {}) {
|
|
|
784
912
|
});
|
|
785
913
|
return { plaintext: null, needsMigration: false };
|
|
786
914
|
}
|
|
915
|
+
const aad = (0, credentialEnvelope_js_1.buildCredentialAad)(_state.profile);
|
|
787
916
|
try {
|
|
788
|
-
const aad = (0, credentialEnvelope_js_1.buildCredentialAad)(_state.profile);
|
|
789
917
|
const pt = (0, credentialEnvelope_js_1.decrypt)(sealed, _state.dek, aad).toString('utf-8');
|
|
790
918
|
return { plaintext: pt, needsMigration: false };
|
|
791
919
|
}
|
|
792
920
|
catch (err) {
|
|
921
|
+
// Sealed before a converging consolidation retired its key (OSK-12462).
|
|
922
|
+
// Readable, and flagged for the in-place upgrade every consumer already
|
|
923
|
+
// performs for legacy plaintext, which re-seals it under the active DEK.
|
|
924
|
+
for (const retired of _retiredDeks) {
|
|
925
|
+
try {
|
|
926
|
+
const pt = (0, credentialEnvelope_js_1.decrypt)(sealed, retired, aad).toString('utf-8');
|
|
927
|
+
emit('cred_envelope.read.retired_key', true, {
|
|
928
|
+
kekId: _state.kekId,
|
|
929
|
+
kekKind: _state.kekKind,
|
|
930
|
+
});
|
|
931
|
+
return { plaintext: pt, needsMigration: true };
|
|
932
|
+
}
|
|
933
|
+
catch {
|
|
934
|
+
/* not this one */
|
|
935
|
+
}
|
|
936
|
+
}
|
|
793
937
|
emit('cred_envelope.read.decrypt_failed', false, {
|
|
794
938
|
kekId: _state.kekId,
|
|
795
939
|
kekKind: _state.kekKind,
|
|
@@ -15,6 +15,23 @@ type DisabledReason = 'init_failed' | 'load_failed' | 'persist_failed';
|
|
|
15
15
|
export declare function __setStateForTest(next: DeviceIdentityState): DeviceIdentityState;
|
|
16
16
|
/** @internal Test seam — read state. */
|
|
17
17
|
export declare function __getStateForTest(): DeviceIdentityState;
|
|
18
|
+
/**
|
|
19
|
+
* The device-key split, read from disk: the daemon (legacy directory, not yet
|
|
20
|
+
* consolidated) and the CLI (canonical directory) holding DIFFERENT keypairs
|
|
21
|
+
* for one profile. `null` when this process has no legacy location, when the
|
|
22
|
+
* legacy copy is consolidated, or when either side has no readable public key.
|
|
23
|
+
*
|
|
24
|
+
* This is the fact the server acts on (OSK-12462): a CLI credential brokered by
|
|
25
|
+
* the daemon is checked against the key pinned on the daemon's machine row, so
|
|
26
|
+
* while the thumbprints differ the CLI's refresh is refused. Reading it from
|
|
27
|
+
* the public JWKs, which are plaintext, needs no DEK and changes nothing.
|
|
28
|
+
*/
|
|
29
|
+
export declare function describeDeviceKeySplit(profile: string): {
|
|
30
|
+
daemonThumbprint: string;
|
|
31
|
+
cliThumbprint: string;
|
|
32
|
+
daemonDir: string;
|
|
33
|
+
cliDir: string;
|
|
34
|
+
} | null;
|
|
18
35
|
/**
|
|
19
36
|
* Initialize device identity. Idempotent.
|
|
20
37
|
*
|
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.__setStateForTest = __setStateForTest;
|
|
7
7
|
exports.__getStateForTest = __getStateForTest;
|
|
8
|
+
exports.describeDeviceKeySplit = describeDeviceKeySplit;
|
|
8
9
|
exports.initDeviceIdentity = initDeviceIdentity;
|
|
9
10
|
exports.resetDeviceIdentity = resetDeviceIdentity;
|
|
10
11
|
exports.isDeviceIdentityActive = isDeviceIdentityActive;
|
|
@@ -262,6 +263,37 @@ function deviceIdentityOf(dir) {
|
|
|
262
263
|
return null;
|
|
263
264
|
}
|
|
264
265
|
}
|
|
266
|
+
/**
|
|
267
|
+
* The device-key split, read from disk: the daemon (legacy directory, not yet
|
|
268
|
+
* consolidated) and the CLI (canonical directory) holding DIFFERENT keypairs
|
|
269
|
+
* for one profile. `null` when this process has no legacy location, when the
|
|
270
|
+
* legacy copy is consolidated, or when either side has no readable public key.
|
|
271
|
+
*
|
|
272
|
+
* This is the fact the server acts on (OSK-12462): a CLI credential brokered by
|
|
273
|
+
* the daemon is checked against the key pinned on the daemon's machine row, so
|
|
274
|
+
* while the thumbprints differ the CLI's refresh is refused. Reading it from
|
|
275
|
+
* the public JWKs, which are plaintext, needs no DEK and changes nothing.
|
|
276
|
+
*/
|
|
277
|
+
function describeDeviceKeySplit(profile) {
|
|
278
|
+
try {
|
|
279
|
+
const resolved = (0, profileStateDir_js_1.resolveProfileStateDir)(profile, PRIVATE_KEY_FILENAME);
|
|
280
|
+
if (!resolved.usingLegacy || resolved.legacyDir === null)
|
|
281
|
+
return null;
|
|
282
|
+
const daemonThumbprint = deviceIdentityOf(resolved.legacyDir);
|
|
283
|
+
const cliThumbprint = deviceIdentityOf(resolved.canonicalDir);
|
|
284
|
+
if (!daemonThumbprint || !cliThumbprint || daemonThumbprint === cliThumbprint)
|
|
285
|
+
return null;
|
|
286
|
+
return {
|
|
287
|
+
daemonThumbprint,
|
|
288
|
+
cliThumbprint,
|
|
289
|
+
daemonDir: resolved.legacyDir,
|
|
290
|
+
cliDir: resolved.canonicalDir,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
catch {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
265
297
|
/**
|
|
266
298
|
* Run the OSK-10314 consolidation for this profile's device identity.
|
|
267
299
|
*
|
|
@@ -276,11 +308,17 @@ async function consolidateDeviceIdentity(profile) {
|
|
|
276
308
|
emit('path_consolidation.deferred', true, { profile, reason: 'cred_envelope_path_conflict' }, { logLevel: 'info' });
|
|
277
309
|
return;
|
|
278
310
|
}
|
|
311
|
+
// Two different keypairs under one DEK are converged the way two DEKs are:
|
|
312
|
+
// onto the daemon's, unless the daemon itself was refused over it. The CLI's
|
|
313
|
+
// keypair is set aside as `.retired`, not deleted (OSK-12462). Leaving them
|
|
314
|
+
// split is what had the CLI refresh refused as a device-key mismatch.
|
|
315
|
+
const policy = (0, credentialEnvelopeBridge_js_1.splitKeyConvergencePolicy)();
|
|
279
316
|
try {
|
|
280
317
|
await (0, profileStateDir_js_1.consolidateProfileState)(profile, {
|
|
281
318
|
files: DEVICE_IDENTITY_FILES,
|
|
282
319
|
primary: PRIVATE_KEY_FILENAME,
|
|
283
320
|
identify: deviceIdentityOf,
|
|
321
|
+
onConflict: policy.converge ? 'adopt_legacy' : 'refuse',
|
|
284
322
|
onEvent: (name, success, meta) => emit(name, success, meta),
|
|
285
323
|
});
|
|
286
324
|
}
|
|
@@ -27,8 +27,8 @@ export { formatRecoveryCode, parseRecoveryCode, normalizeRecoveryCode, isValidRe
|
|
|
27
27
|
export { buildCredentialAad, decrypt as credEnvelopeDecrypt, deserialize as credEnvelopeDeserialize, encrypt as credEnvelopeEncrypt, ENVELOPE_MAGIC as CRED_ENVELOPE_MAGIC, generateDek, isEnvelopeString as isCredEnvelopeString, KEY_LEN as CRED_KEY_LEN, openBuffer as credOpenBuffer, sealBuffer as credSealBuffer, serialize as credEnvelopeSerialize, unwrap as credEnvelopeUnwrap, wrap as credEnvelopeWrap, type SealedBlob as CredSealedBlob, } from './credentialEnvelope.js';
|
|
28
28
|
export { __setKekStrategyForTest, getKekStrategy, InMemoryKek, UnavailableKek, KekUnavailableError, LinuxKek, MacosKek, registerZeroizeHook, WindowsKek, zeroizeKekCaches, type KekStrategy, } from './kek/index.js';
|
|
29
29
|
export { buildProof as buildDeviceProof, generateDeviceKeyPair, jwkThumbprint, normalizeHtu, publicJwkFromX, tokenHash, verifyProof as verifyDeviceProof, type DeviceKeyPair, type DeviceProofClaims, type DeviceProofHeader, type Ed25519PrivateJwk, type Ed25519PublicJwk, type VerifyFailureReason as DeviceProofVerifyFailureReason, type VerifyFailure as DeviceProofVerifyFailure, type VerifyOptions as DeviceProofVerifyOptions, type VerifySuccess as DeviceProofVerifySuccess, } from './deviceKey.js';
|
|
30
|
-
export { initCredEnvelope, resetCredEnvelope, shutdownCredEnvelope, isCredEnvelopeActive, hasCredEnvelopePathConflict, describeCredEnvelopeState, maybeEncryptForWrite, maybeDecryptOnRead, DEFAULT_UNREADABLE_CREDENTIAL_HINT, type DecryptOnReadOptions, __wrappedDekFilePathForTest, __setStateForTest as __setCredEnvelopeStateForTest, __getStateForTest as __getCredEnvelopeStateForTest, __resetShutdownGuardForTest, type ReadTransformResult, } from './credentialEnvelopeBridge.js';
|
|
31
|
-
export { initDeviceIdentity, resetDeviceIdentity, signServerRequest, enrollWithServer, clearEnrollmentMarker, isDeviceIdentityActive, getDevicePublicKey, describeDeviceIdentityState, __publicKeyPathForTest, __privateKeyPathForTest, __enrollmentMarkerPathForTest, __enrollmentBackoffPathForTest, __setStateForTest as __setDeviceIdentityStateForTest, __getStateForTest as __getDeviceIdentityStateForTest, type EnrollmentResult, } from './deviceIdentityBridge.js';
|
|
30
|
+
export { initCredEnvelope, resetCredEnvelope, shutdownCredEnvelope, isCredEnvelopeActive, hasCredEnvelopePathConflict, describeCredEnvelopeState, splitKeyConvergencePolicy, maybeEncryptForWrite, maybeDecryptOnRead, DEFAULT_UNREADABLE_CREDENTIAL_HINT, type DecryptOnReadOptions, __wrappedDekFilePathForTest, __setStateForTest as __setCredEnvelopeStateForTest, __getStateForTest as __getCredEnvelopeStateForTest, __resetShutdownGuardForTest, type ReadTransformResult, } from './credentialEnvelopeBridge.js';
|
|
31
|
+
export { initDeviceIdentity, resetDeviceIdentity, signServerRequest, enrollWithServer, clearEnrollmentMarker, isDeviceIdentityActive, getDevicePublicKey, describeDeviceIdentityState, describeDeviceKeySplit, __publicKeyPathForTest, __privateKeyPathForTest, __enrollmentMarkerPathForTest, __enrollmentBackoffPathForTest, __setStateForTest as __setDeviceIdentityStateForTest, __getStateForTest as __getDeviceIdentityStateForTest, type EnrollmentResult, } from './deviceIdentityBridge.js';
|
|
32
32
|
export { getMachineUuid, hostedRuntimeIdOverride, HOSTED_RUNTIME_MACHINE_SOURCES, __resetMachineUuidCacheForTest, } from './machineUuid.js';
|
|
33
33
|
export { ensureMachineDaemonId, legacyDaemonUuids, __resetCachedDaemonIdForTest, } from './machineId.js';
|
|
34
34
|
export { PAIR_AAD_PREFIX, PAIR_AAD_MAX_LEN, PAIR_BUNDLE_MAX_BASE64_LEN, PAIR_NONCE_LEN, PAIR_PUBKEY_LEN, PAIR_SECRET_LEN, PairBundleError, assemblePairBundleBase64, base64ToUint8Array as pairBase64ToUint8Array, base64UrlToUint8Array as pairBase64UrlToUint8Array, buildPairAad, buildPairUrl, generatePairKeyPair, openPairBundleBase64, parsePairBundle, parsePairPlaintext, parsePairUrl, uint8ArrayToBase64 as pairUint8ArrayToBase64, uint8ArrayToBase64Url as pairUint8ArrayToBase64Url, type AssembleBundleOptions as PairAssembleBundleOptions, type OpenBundleOptions as PairOpenBundleOptions, type PairBundle, type PairBundleErrorCode, type PairKeyPair, type PairPlaintextV1, type PairUrlFields, } from './daemonPair.js';
|
|
@@ -24,9 +24,9 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
24
24
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
25
|
exports.generateVerifier = exports.invalidateHeadersHelperCache = exports.runHeadersHelper = exports.SENTINEL_TRIGGERING_FAILURES = exports.__resetAuthHelperFailureReasonForTest = exports.getLastAuthHelperFailureReason = exports.getDefaultHelperTtlMs = exports.invokeAuthHelper = exports.AuthHelperUnavailableError = exports.AuthHelperUntrustedError = exports.CI_TOKEN_PREFIX = exports.classifyTokenKind = exports.invalidateAuthHelperCache = exports.__resetCredentialResolverForTest = exports.CREDENTIAL_PRECEDENCE = exports.CredentialResolver = exports.__setFdReaderForTest = exports.readAuthFromFd = exports.createRefreshScheduler = exports.UNKNOWN_401_BUDGET = exports.__resetUnknown401StreaksForTest = exports.__refreshDiagnostics = exports.installLockExitHandlers = exports.clearReauthState = exports.needsReauthState = exports.persistReauthState = exports.withAuthLock = exports.revokeDaemonRefreshSession = exports.refreshDaemonToken = exports.classifyRefreshResponse = exports.EXIT_NEEDS_REAUTH = exports.reauthCommand = exports.formatReauthMessage = exports.handleMidStreamAuthExpiry = exports.isAuthExpiredSseEvent = exports.decodeJwtPayload = exports.decodeJwtExpiry = exports.TransientAuthFailure = exports.PermanentAuthFailure = exports.emitAuthTelemetry = exports.getAuthBinaryName = exports.getAuthKillSwitches = exports.getAuthConfigDir = exports.getAuthLogger = exports.configureAuthCore = exports.OAUTH_CLI_SCOPE = exports.isDaemonScope = exports.VALID_DAEMON_SCOPE_SET = exports.ALL_DAEMON_SCOPES = exports.SCOPES = void 0;
|
|
26
26
|
exports.publicJwkFromX = exports.normalizeHtu = exports.jwkThumbprint = exports.generateDeviceKeyPair = exports.buildDeviceProof = exports.zeroizeKekCaches = exports.WindowsKek = exports.registerZeroizeHook = exports.MacosKek = exports.LinuxKek = exports.KekUnavailableError = exports.UnavailableKek = exports.InMemoryKek = exports.getKekStrategy = exports.__setKekStrategyForTest = exports.credEnvelopeWrap = exports.credEnvelopeUnwrap = exports.credEnvelopeSerialize = exports.credSealBuffer = exports.credOpenBuffer = exports.CRED_KEY_LEN = exports.isCredEnvelopeString = exports.generateDek = exports.CRED_ENVELOPE_MAGIC = exports.credEnvelopeEncrypt = exports.credEnvelopeDeserialize = exports.credEnvelopeDecrypt = exports.buildCredentialAad = exports.isValidRecoveryCode = exports.normalizeRecoveryCode = exports.parseRecoveryCode = exports.formatRecoveryCode = exports.MAX_TTL_DAYS = exports.MIN_TTL_DAYS = exports.parseTtlDays = exports.LoginExchangeError = exports.LoginInitError = exports.loginWithLocalhost = exports.LocalCallbackMalformedError = exports.LocalCallbackDeniedError = exports.LocalCallbackStateMismatchError = exports.LocalCallbackTimeoutError = exports.LocalCallbackBindError = exports.LocalCallbackError = exports.DEFAULT_LOGIN_TIMEOUT_MS = exports.startLocalCallback = exports.verifyChallenge = exports.safeEqual = exports.generateState = exports.challenge = void 0;
|
|
27
|
-
exports.
|
|
28
|
-
exports.
|
|
29
|
-
exports.consolidateProfileState = exports.profileStateFilePathsToClear = exports.profileStateFileLocations = exports.resolveProfileStateFile = exports.resolveProfileStateDir = exports.legacyProfileStateDir = exports.profileStateDir = exports.SUPERSEDED_COPY_SUFFIX = exports.SUPERSEDED_MARKER_SUFFIX = exports.PROFILES_SEGMENT = exports.isConfigRootOverridden = exports.configRootPath = exports.resolveConfigRoot = exports.verifyManifest = exports.verifyArtifact = exports.isTrustedArtifactOrigin = exports.getDeltaFor = exports.buildManifestMessage = exports.buildArtifactMessage = exports.TRUSTED_FEED_ORIGINS = exports.MAX_SIGS = void 0;
|
|
27
|
+
exports.buildPairUrl = exports.buildPairAad = exports.pairBase64UrlToUint8Array = exports.pairBase64ToUint8Array = exports.assemblePairBundleBase64 = exports.PairBundleError = exports.PAIR_SECRET_LEN = exports.PAIR_PUBKEY_LEN = exports.PAIR_NONCE_LEN = exports.PAIR_BUNDLE_MAX_BASE64_LEN = exports.PAIR_AAD_MAX_LEN = exports.PAIR_AAD_PREFIX = exports.__resetCachedDaemonIdForTest = exports.legacyDaemonUuids = exports.ensureMachineDaemonId = exports.__resetMachineUuidCacheForTest = exports.HOSTED_RUNTIME_MACHINE_SOURCES = exports.hostedRuntimeIdOverride = exports.getMachineUuid = exports.__getDeviceIdentityStateForTest = exports.__setDeviceIdentityStateForTest = exports.__enrollmentBackoffPathForTest = exports.__enrollmentMarkerPathForTest = exports.__privateKeyPathForTest = exports.__publicKeyPathForTest = exports.describeDeviceKeySplit = exports.describeDeviceIdentityState = exports.getDevicePublicKey = exports.isDeviceIdentityActive = exports.clearEnrollmentMarker = exports.enrollWithServer = exports.signServerRequest = exports.resetDeviceIdentity = exports.initDeviceIdentity = exports.__resetShutdownGuardForTest = exports.__getCredEnvelopeStateForTest = exports.__setCredEnvelopeStateForTest = exports.__wrappedDekFilePathForTest = exports.DEFAULT_UNREADABLE_CREDENTIAL_HINT = exports.maybeDecryptOnRead = exports.maybeEncryptForWrite = exports.splitKeyConvergencePolicy = exports.describeCredEnvelopeState = exports.hasCredEnvelopePathConflict = exports.isCredEnvelopeActive = exports.shutdownCredEnvelope = exports.resetCredEnvelope = exports.initCredEnvelope = exports.verifyDeviceProof = exports.tokenHash = void 0;
|
|
28
|
+
exports.resolveChannelManifestUrl = exports.isFirstPartyHarnessChannel = exports.firstPartyHarnessFeedCandidates = exports.firstPartyHarnessFeedBase = exports.firstPartyHarnessChannelPrefix = exports.FIRST_PARTY_HARNESS_CHANNELS = exports.DEFAULT_FIRST_PARTY_HARNESS_CHANNEL = exports.migrateFirstPartyHarnessHome = exports.linkFirstPartyHarnessAliases = exports.firstPartyHarnessInstalledBinaryPath = exports.firstPartyHarnessHomeDir = exports.firstPartyHarnessBinDir = exports.FIRST_PARTY_HARNESS_PROVENANCE_SUFFIX = exports.FIRST_PARTY_HARNESS_PREVIOUS_SUFFIX = exports.describeLegacyState = exports.findLegacyLocalState = exports.DEFAULT_BINARY_NAME = exports.LEGACY_NATIVE_ID_PREFIXES = exports.NATIVE_ID_PREFIX = exports.LEGACY_CONFIG_DIR_NAMES = exports.CONFIG_DIR_NAME = exports.LOOPBACK_DEFAULT_MAX_BODY_BYTES = exports.LoopbackTargetRefusedError = exports.isLoopbackAddress = exports.isLoopbackHostname = exports.assertLoopbackHttpUrl = exports.loopbackFetch = exports.withLoopbackNoProxy = exports.envHasProxy = exports.LOOPBACK_NO_PROXY_HOSTS = exports.SENSITIVE_ENV_PREFIX_EXCEPTIONS = exports.SENSITIVE_ENV_NAME_TOKENS = exports.SENSITIVE_ENV_SUFFIXES = exports.SENSITIVE_ENV_PREFIXES = exports.SENSITIVE_ENV_VARS = exports.sanitizeSpawnEnv = exports.harnessLoadsClaudeSkillsLayout = exports.claudeSkillsLayoutEvidence = exports.HARNESSES_LOADING_CLAUDE_SKILLS_LAYOUT = exports.tierPermitsConfiguredAutoMode = exports.credentialPolicyForTier = exports.getHarnessTier = exports.HARNESS_TIERS = exports.pairUint8ArrayToBase64Url = exports.pairUint8ArrayToBase64 = exports.parsePairUrl = exports.parsePairPlaintext = exports.parsePairBundle = exports.openPairBundleBase64 = exports.generatePairKeyPair = void 0;
|
|
29
|
+
exports.consolidateProfileState = exports.profileStateFilePathsToClear = exports.profileStateFileLocations = exports.resolveProfileStateFile = exports.resolveProfileStateDir = exports.legacyProfileStateDir = exports.profileStateDir = exports.SUPERSEDED_COPY_SUFFIX = exports.SUPERSEDED_MARKER_SUFFIX = exports.PROFILES_SEGMENT = exports.isConfigRootOverridden = exports.configRootPath = exports.resolveConfigRoot = exports.verifyManifest = exports.verifyArtifact = exports.isTrustedArtifactOrigin = exports.getDeltaFor = exports.buildManifestMessage = exports.buildArtifactMessage = exports.TRUSTED_FEED_ORIGINS = exports.MAX_SIGS = exports.OVERSKY_RELEASE_PUBLIC_KEYS = exports.resolveFirstPartyHarnessChannel = void 0;
|
|
30
30
|
// Daemon scope vocabulary — single source of truth for non-Mongoose tiers
|
|
31
31
|
// (daemon, desktop, cli). Schema-bound canonical lives in
|
|
32
32
|
// `@skrr-ai/data-schemas/common/daemonScopes`; the two are kept byte-equal
|
|
@@ -232,6 +232,7 @@ Object.defineProperty(exports, "shutdownCredEnvelope", { enumerable: true, get:
|
|
|
232
232
|
Object.defineProperty(exports, "isCredEnvelopeActive", { enumerable: true, get: function () { return credentialEnvelopeBridge_js_1.isCredEnvelopeActive; } });
|
|
233
233
|
Object.defineProperty(exports, "hasCredEnvelopePathConflict", { enumerable: true, get: function () { return credentialEnvelopeBridge_js_1.hasCredEnvelopePathConflict; } });
|
|
234
234
|
Object.defineProperty(exports, "describeCredEnvelopeState", { enumerable: true, get: function () { return credentialEnvelopeBridge_js_1.describeCredEnvelopeState; } });
|
|
235
|
+
Object.defineProperty(exports, "splitKeyConvergencePolicy", { enumerable: true, get: function () { return credentialEnvelopeBridge_js_1.splitKeyConvergencePolicy; } });
|
|
235
236
|
Object.defineProperty(exports, "maybeEncryptForWrite", { enumerable: true, get: function () { return credentialEnvelopeBridge_js_1.maybeEncryptForWrite; } });
|
|
236
237
|
Object.defineProperty(exports, "maybeDecryptOnRead", { enumerable: true, get: function () { return credentialEnvelopeBridge_js_1.maybeDecryptOnRead; } });
|
|
237
238
|
Object.defineProperty(exports, "DEFAULT_UNREADABLE_CREDENTIAL_HINT", { enumerable: true, get: function () { return credentialEnvelopeBridge_js_1.DEFAULT_UNREADABLE_CREDENTIAL_HINT; } });
|
|
@@ -252,6 +253,7 @@ Object.defineProperty(exports, "clearEnrollmentMarker", { enumerable: true, get:
|
|
|
252
253
|
Object.defineProperty(exports, "isDeviceIdentityActive", { enumerable: true, get: function () { return deviceIdentityBridge_js_1.isDeviceIdentityActive; } });
|
|
253
254
|
Object.defineProperty(exports, "getDevicePublicKey", { enumerable: true, get: function () { return deviceIdentityBridge_js_1.getDevicePublicKey; } });
|
|
254
255
|
Object.defineProperty(exports, "describeDeviceIdentityState", { enumerable: true, get: function () { return deviceIdentityBridge_js_1.describeDeviceIdentityState; } });
|
|
256
|
+
Object.defineProperty(exports, "describeDeviceKeySplit", { enumerable: true, get: function () { return deviceIdentityBridge_js_1.describeDeviceKeySplit; } });
|
|
255
257
|
// Test seams — file path resolvers + state setters for spec files.
|
|
256
258
|
Object.defineProperty(exports, "__publicKeyPathForTest", { enumerable: true, get: function () { return deviceIdentityBridge_js_1.__publicKeyPathForTest; } });
|
|
257
259
|
Object.defineProperty(exports, "__privateKeyPathForTest", { enumerable: true, get: function () { return deviceIdentityBridge_js_1.__privateKeyPathForTest; } });
|
|
@@ -11,6 +11,13 @@ export declare const SUPERSEDED_MARKER_SUFFIX = ".superseded.json";
|
|
|
11
11
|
* canonical copy existed and was NOT usable — a usable one is never replaced.
|
|
12
12
|
*/
|
|
13
13
|
export declare const SUPERSEDED_COPY_SUFFIX = ".superseded";
|
|
14
|
+
/**
|
|
15
|
+
* Suffix for a USABLE canonical file set aside by a converging consolidation
|
|
16
|
+
* (`onConflict: 'adopt_legacy'`). Distinct from `SUPERSEDED_COPY_SUFFIX`, which
|
|
17
|
+
* names an unusable copy: a retired key still opens what was sealed under it,
|
|
18
|
+
* and the credential envelope reads these as its keyring (OSK-12462).
|
|
19
|
+
*/
|
|
20
|
+
export declare const RETIRED_COPY_SUFFIX = ".retired";
|
|
14
21
|
/** The canonical directory for a profile's auth state. */
|
|
15
22
|
export declare function profileStateDir(profile: string): string;
|
|
16
23
|
/**
|
|
@@ -59,7 +66,12 @@ export type ConsolidationStatus =
|
|
|
59
66
|
/** Canonical was present but unusable; replaced, and kept under `.superseded`. */
|
|
60
67
|
| 'superseded'
|
|
61
68
|
/** Both copies are usable and DIFFERENT. Refused — a human decides. */
|
|
62
|
-
| 'conflict'
|
|
69
|
+
| 'conflict'
|
|
70
|
+
/**
|
|
71
|
+
* Both copies were usable and DIFFERENT, and the caller chose the legacy one
|
|
72
|
+
* (`onConflict: 'adopt_legacy'`). The canonical copy is kept as `.retired`.
|
|
73
|
+
*/
|
|
74
|
+
| 'converged';
|
|
63
75
|
export interface ConsolidationOutcome {
|
|
64
76
|
status: ConsolidationStatus;
|
|
65
77
|
profile: string;
|
|
@@ -98,6 +110,14 @@ export interface ConsolidationSpec {
|
|
|
98
110
|
identify?: (dir: string) => Promise<string | null> | string | null;
|
|
99
111
|
/** Report what would happen and write nothing. */
|
|
100
112
|
dryRun?: boolean;
|
|
113
|
+
/**
|
|
114
|
+
* What to do when both copies are usable and DIFFERENT. `refuse` (default)
|
|
115
|
+
* leaves both untouched and reports `conflict`. `adopt_legacy` sets the
|
|
116
|
+
* canonical copy aside as `<file>.retired.<stamp>` and adopts the legacy one,
|
|
117
|
+
* reporting `converged`. Only a caller that has decided which key must win
|
|
118
|
+
* may pass it — the files are kept, but nothing here can re-seal content.
|
|
119
|
+
*/
|
|
120
|
+
onConflict?: 'refuse' | 'adopt_legacy';
|
|
101
121
|
onEvent?: (name: string, success: boolean, meta: Record<string, unknown>) => void;
|
|
102
122
|
}
|
|
103
123
|
/**
|
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.SUPERSEDED_COPY_SUFFIX = exports.SUPERSEDED_MARKER_SUFFIX = exports.PROFILES_SEGMENT = void 0;
|
|
6
|
+
exports.RETIRED_COPY_SUFFIX = exports.SUPERSEDED_COPY_SUFFIX = exports.SUPERSEDED_MARKER_SUFFIX = exports.PROFILES_SEGMENT = void 0;
|
|
7
7
|
exports.profileStateDir = profileStateDir;
|
|
8
8
|
exports.legacyProfileStateDir = legacyProfileStateDir;
|
|
9
9
|
exports.resolveProfileStateDir = resolveProfileStateDir;
|
|
@@ -69,10 +69,14 @@ exports.profileStateFilePathsToClear = profileStateFilePathsToClear;
|
|
|
69
69
|
* 4. no canonical copy of the file → legacy (not yet consolidated)
|
|
70
70
|
* 5. both copies, no marker → legacy (unresolved: stay put)
|
|
71
71
|
*
|
|
72
|
-
* The migration writes the marker
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
72
|
+
* The migration writes the marker and never deletes. Case 5 with a usable and
|
|
73
|
+
* DIFFERENT key on each side is refused by default. A caller that knows which
|
|
74
|
+
* key must win can pass `onConflict: 'adopt_legacy'`, which moves the canonical
|
|
75
|
+
* copy aside as `<file>.retired.<stamp>` (kept, never deleted) and adopts the
|
|
76
|
+
* legacy one. The credential envelope does that for the daemon's key, and keeps
|
|
77
|
+
* the retired DEK readable so nothing sealed under it is lost (OSK-12462): the
|
|
78
|
+
* refusal alone left the CLI signing with a key the server does not pin, which
|
|
79
|
+
* gets its refresh refused as a device-key mismatch.
|
|
76
80
|
*
|
|
77
81
|
* ROLLBACK. The legacy file is left exactly where it was, under its own name.
|
|
78
82
|
* Older code looks there, finds it, and works — no operator step. The marker is
|
|
@@ -95,6 +99,13 @@ exports.SUPERSEDED_MARKER_SUFFIX = '.superseded.json';
|
|
|
95
99
|
* canonical copy existed and was NOT usable — a usable one is never replaced.
|
|
96
100
|
*/
|
|
97
101
|
exports.SUPERSEDED_COPY_SUFFIX = '.superseded';
|
|
102
|
+
/**
|
|
103
|
+
* Suffix for a USABLE canonical file set aside by a converging consolidation
|
|
104
|
+
* (`onConflict: 'adopt_legacy'`). Distinct from `SUPERSEDED_COPY_SUFFIX`, which
|
|
105
|
+
* names an unusable copy: a retired key still opens what was sealed under it,
|
|
106
|
+
* and the credential envelope reads these as its keyring (OSK-12462).
|
|
107
|
+
*/
|
|
108
|
+
exports.RETIRED_COPY_SUFFIX = '.retired';
|
|
98
109
|
/** The canonical directory for a profile's auth state. */
|
|
99
110
|
function profileStateDir(profile) {
|
|
100
111
|
assertProfile(profile);
|
|
@@ -226,6 +237,28 @@ async function consolidateProfileState(profile, spec) {
|
|
|
226
237
|
identities,
|
|
227
238
|
};
|
|
228
239
|
}
|
|
240
|
+
if (spec.onConflict === 'adopt_legacy') {
|
|
241
|
+
const aside = dryRun
|
|
242
|
+
? plannedAside(canonicalDir, spec.files)
|
|
243
|
+
: moveAside(canonicalDir, spec.files, exports.RETIRED_COPY_SUFFIX);
|
|
244
|
+
const moved = adopt(legacyDir, canonicalDir, spec.files, dryRun);
|
|
245
|
+
if (!dryRun)
|
|
246
|
+
writeMarker(markerPath, canonicalPrimary, 'converged');
|
|
247
|
+
emitOutcome(spec, 'converged', {
|
|
248
|
+
profile,
|
|
249
|
+
identities,
|
|
250
|
+
from: legacyDir,
|
|
251
|
+
to: canonicalDir,
|
|
252
|
+
retired: aside,
|
|
253
|
+
});
|
|
254
|
+
return {
|
|
255
|
+
...base,
|
|
256
|
+
status: 'converged',
|
|
257
|
+
written: moved.written,
|
|
258
|
+
supersededAside: aside,
|
|
259
|
+
identities,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
229
262
|
emitOutcome(spec, 'conflict', { profile, identities, canonicalDir, legacyDir });
|
|
230
263
|
return { ...base, status: 'conflict', identities };
|
|
231
264
|
}
|
|
@@ -310,14 +343,14 @@ function adopt(fromDir, toDir, files, dryRun) {
|
|
|
310
343
|
}
|
|
311
344
|
return { written, supersededAside: [] };
|
|
312
345
|
}
|
|
313
|
-
function moveAside(dir, files) {
|
|
346
|
+
function moveAside(dir, files, suffix = exports.SUPERSEDED_COPY_SUFFIX) {
|
|
314
347
|
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
315
348
|
const aside = [];
|
|
316
349
|
for (const name of files) {
|
|
317
350
|
const target = node_path_1.default.join(dir, name);
|
|
318
351
|
if (!existsFile(target))
|
|
319
352
|
continue;
|
|
320
|
-
node_fs_1.default.renameSync(target, `${target}${
|
|
353
|
+
node_fs_1.default.renameSync(target, `${target}${suffix}.${stamp}`);
|
|
321
354
|
aside.push(target);
|
|
322
355
|
}
|
|
323
356
|
return aside;
|
|
@@ -23,6 +23,37 @@ export declare function __setStateForTest(next: CredEnvelopeState): CredEnvelope
|
|
|
23
23
|
export declare function __getStateForTest(): CredEnvelopeState;
|
|
24
24
|
/** @internal Test seam — each spec owns its warn-dedupe horizon. */
|
|
25
25
|
export declare function __clearWarnDedupeForTest(): void;
|
|
26
|
+
/**
|
|
27
|
+
* Run the OSK-10314 consolidation for this profile's wrapped DEK.
|
|
28
|
+
*
|
|
29
|
+
* Under the canonical lock, because the window it closes is real: a daemon
|
|
30
|
+
* copying its legacy DEK into an empty canonical slot must not race a CLI
|
|
31
|
+
* enrolling a fresh one there — the loser's key would be overwritten and
|
|
32
|
+
* anything already sealed under it lost. That is the same race the lock was
|
|
33
|
+
* introduced for, one layer up.
|
|
34
|
+
*
|
|
35
|
+
* Never fatal. A migration that cannot run leaves the process exactly where it
|
|
36
|
+
* was, which is the state it has been operating in all along.
|
|
37
|
+
*/
|
|
38
|
+
/**
|
|
39
|
+
* Whether a split profile may be converged onto the legacy (daemon) key.
|
|
40
|
+
*
|
|
41
|
+
* Only the daemon ever has a legacy location, so only the daemon runs this, and
|
|
42
|
+
* the legacy key is the one it signs with. Since OSK-12063 the CLI enrolls no
|
|
43
|
+
* device key and only an interactive daemon login re-pins, so the key the server
|
|
44
|
+
* pins for this machine is the daemon's, UNLESS the daemon has itself been
|
|
45
|
+
* refused over its device key. That is the one fact that proves otherwise, and
|
|
46
|
+
* then nothing here knows which key the server holds, so it refuses as before.
|
|
47
|
+
*
|
|
48
|
+
* Why converging matters (OSK-12462): a CLI credential brokered by the daemon
|
|
49
|
+
* is tied to the daemon's machine row, and the server checks its refresh proof
|
|
50
|
+
* against the key pinned there. A CLI signing with a different key has that
|
|
51
|
+
* refresh refused as DEVICE_PROOF_THUMBPRINT_MISMATCH at its first renewal.
|
|
52
|
+
*/
|
|
53
|
+
export declare function splitKeyConvergencePolicy(): {
|
|
54
|
+
converge: boolean;
|
|
55
|
+
reason: 'daemon_key_pinned' | 'daemon_key_refused';
|
|
56
|
+
};
|
|
26
57
|
/**
|
|
27
58
|
* Initialize the envelope path. Idempotent — second call when state is
|
|
28
59
|
* non-uninit returns immediately.
|
|
@@ -96,6 +127,16 @@ export declare function describeCredEnvelopeState(): {
|
|
|
96
127
|
canonicalPath: string;
|
|
97
128
|
legacyPath: string;
|
|
98
129
|
};
|
|
130
|
+
/**
|
|
131
|
+
* Set when THIS process converged a split profile onto the legacy (daemon)
|
|
132
|
+
* key (OSK-12462). `canonicalPath` held the key that is now retired.
|
|
133
|
+
*/
|
|
134
|
+
convergedFrom?: {
|
|
135
|
+
canonicalPath: string;
|
|
136
|
+
legacyPath: string;
|
|
137
|
+
};
|
|
138
|
+
/** How many retired DEKs this process can still open old values with. */
|
|
139
|
+
retiredKeys?: number;
|
|
99
140
|
};
|
|
100
141
|
/**
|
|
101
142
|
* Sync write transform. Returns the wrapped serialized form when active.
|