@skrr-ai/cli 0.1.58 → 0.1.59

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/base-command.d.ts +0 -16
  2. package/dist/base-command.js +65 -11
  3. package/dist/commands/logout.d.ts +0 -46
  4. package/dist/commands/logout.js +21 -3
  5. package/dist/lib/agentic-stream.d.ts +42 -0
  6. package/dist/lib/agentic-stream.js +103 -14
  7. package/dist/lib/api-fetch.d.ts +11 -0
  8. package/dist/lib/api-fetch.js +98 -1
  9. package/dist/lib/daemonBrokerRefusal.d.ts +21 -0
  10. package/dist/lib/daemonBrokerRefusal.js +87 -2
  11. package/dist/lib/dedicated-guest.d.ts +13 -0
  12. package/dist/lib/dedicated-guest.js +22 -0
  13. package/dist/lib/dedicated-machines.js +17 -1
  14. package/dist/lib/dedicated-service.d.ts +1 -13
  15. package/dist/lib/dedicated-service.js +7 -19
  16. package/dist/lib/login.js +10 -1
  17. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.d.ts +41 -0
  18. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.js +147 -3
  19. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.d.ts +17 -0
  20. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +38 -0
  21. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +2 -2
  22. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +5 -3
  23. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/profileStateDir.d.ts +21 -1
  24. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/profileStateDir.js +40 -7
  25. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.d.ts +41 -0
  26. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.js +147 -4
  27. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.d.ts +17 -0
  28. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +38 -1
  29. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +2 -2
  30. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +2 -2
  31. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/profileStateDir.d.ts +21 -1
  32. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/profileStateDir.js +39 -6
  33. package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
  34. package/dist/node_modules/@skrr-ai/data-provider/index.js +4391 -4363
  35. package/oclif.manifest.json +33201 -33201
  36. package/package.json +1 -1
@@ -32,7 +32,7 @@
32
32
  import fs from 'node:fs';
33
33
  import path from 'node:path';
34
34
  import { buildProof as buildDeviceProof, generateDeviceKeyPair, jwkThumbprint, } from './deviceKey.js';
35
- import { hasCredEnvelopePathConflict, isCredEnvelopeActive, maybeDecryptOnRead, wrapIfActiveOrPassthrough, } from './credentialEnvelopeBridge.js';
35
+ import { hasCredEnvelopePathConflict, isCredEnvelopeActive, maybeDecryptOnRead, splitKeyConvergencePolicy, wrapIfActiveOrPassthrough, } from './credentialEnvelopeBridge.js';
36
36
  import { consolidateProfileState, profileStateFilePathsToClear, resolveProfileStateDir, } from './profileStateDir.js';
37
37
  import { emitAuthTelemetry, getAuthLogger } from './runtime.js';
38
38
  const PUBLIC_KEY_FILENAME = 'device-public-key.jwk';
@@ -243,6 +243,37 @@ function deviceIdentityOf(dir) {
243
243
  return null;
244
244
  }
245
245
  }
246
+ /**
247
+ * The device-key split, read from disk: the daemon (legacy directory, not yet
248
+ * consolidated) and the CLI (canonical directory) holding DIFFERENT keypairs
249
+ * for one profile. `null` when this process has no legacy location, when the
250
+ * legacy copy is consolidated, or when either side has no readable public key.
251
+ *
252
+ * This is the fact the server acts on (OSK-12462): a CLI credential brokered by
253
+ * the daemon is checked against the key pinned on the daemon's machine row, so
254
+ * while the thumbprints differ the CLI's refresh is refused. Reading it from
255
+ * the public JWKs, which are plaintext, needs no DEK and changes nothing.
256
+ */
257
+ export function describeDeviceKeySplit(profile) {
258
+ try {
259
+ const resolved = resolveProfileStateDir(profile, PRIVATE_KEY_FILENAME);
260
+ if (!resolved.usingLegacy || resolved.legacyDir === null)
261
+ return null;
262
+ const daemonThumbprint = deviceIdentityOf(resolved.legacyDir);
263
+ const cliThumbprint = deviceIdentityOf(resolved.canonicalDir);
264
+ if (!daemonThumbprint || !cliThumbprint || daemonThumbprint === cliThumbprint)
265
+ return null;
266
+ return {
267
+ daemonThumbprint,
268
+ cliThumbprint,
269
+ daemonDir: resolved.legacyDir,
270
+ cliDir: resolved.canonicalDir,
271
+ };
272
+ }
273
+ catch {
274
+ return null;
275
+ }
276
+ }
246
277
  /**
247
278
  * Run the OSK-10314 consolidation for this profile's device identity.
248
279
  *
@@ -257,11 +288,17 @@ async function consolidateDeviceIdentity(profile) {
257
288
  emit('path_consolidation.deferred', true, { profile, reason: 'cred_envelope_path_conflict' }, { logLevel: 'info' });
258
289
  return;
259
290
  }
291
+ // Two different keypairs under one DEK are converged the way two DEKs are:
292
+ // onto the daemon's, unless the daemon itself was refused over it. The CLI's
293
+ // keypair is set aside as `.retired`, not deleted (OSK-12462). Leaving them
294
+ // split is what had the CLI refresh refused as a device-key mismatch.
295
+ const policy = splitKeyConvergencePolicy();
260
296
  try {
261
297
  await consolidateProfileState(profile, {
262
298
  files: DEVICE_IDENTITY_FILES,
263
299
  primary: PRIVATE_KEY_FILENAME,
264
300
  identify: deviceIdentityOf,
301
+ onConflict: policy.converge ? 'adopt_legacy' : 'refuse',
265
302
  onEvent: (name, success, meta) => emit(name, success, meta),
266
303
  });
267
304
  }
@@ -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';
@@ -108,14 +108,14 @@ export { buildProof as buildDeviceProof, generateDeviceKeyPair, jwkThumbprint, n
108
108
  // Daemon and CLI consume these; the underlying primitives live above.
109
109
  export {
110
110
  // credEnvelope bridge
111
- initCredEnvelope, resetCredEnvelope, shutdownCredEnvelope, isCredEnvelopeActive, hasCredEnvelopePathConflict, describeCredEnvelopeState, maybeEncryptForWrite, maybeDecryptOnRead, DEFAULT_UNREADABLE_CREDENTIAL_HINT,
111
+ initCredEnvelope, resetCredEnvelope, shutdownCredEnvelope, isCredEnvelopeActive, hasCredEnvelopePathConflict, describeCredEnvelopeState, splitKeyConvergencePolicy, maybeEncryptForWrite, maybeDecryptOnRead, DEFAULT_UNREADABLE_CREDENTIAL_HINT,
112
112
  // Test seams — `__`-prefixed so consumers don't import them in
113
113
  // production code paths. Re-exported here so CLI/daemon spec files
114
114
  // can resolve the wrapped-DEK path against the same module instance.
115
115
  __wrappedDekFilePathForTest, __setStateForTest as __setCredEnvelopeStateForTest, __getStateForTest as __getCredEnvelopeStateForTest, __resetShutdownGuardForTest, } from './credentialEnvelopeBridge.js';
116
116
  export {
117
117
  // deviceIdentity bridge
118
- initDeviceIdentity, resetDeviceIdentity, signServerRequest, enrollWithServer, clearEnrollmentMarker, isDeviceIdentityActive, getDevicePublicKey, describeDeviceIdentityState,
118
+ initDeviceIdentity, resetDeviceIdentity, signServerRequest, enrollWithServer, clearEnrollmentMarker, isDeviceIdentityActive, getDevicePublicKey, describeDeviceIdentityState, describeDeviceKeySplit,
119
119
  // Test seams — file path resolvers + state setters for spec files.
120
120
  __publicKeyPathForTest, __privateKeyPathForTest, __enrollmentMarkerPathForTest, __enrollmentBackoffPathForTest, __setStateForTest as __setDeviceIdentityStateForTest, __getStateForTest as __getDeviceIdentityStateForTest, } from './deviceIdentityBridge.js';
121
121
  // Hardware machine identity sent as `machineUuid` (IOPlatformUUID /
@@ -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
  /**
@@ -56,10 +56,14 @@
56
56
  * 4. no canonical copy of the file → legacy (not yet consolidated)
57
57
  * 5. both copies, no marker → legacy (unresolved: stay put)
58
58
  *
59
- * The migration writes the marker, never deletes, and refuses case 5 when both
60
- * sides hold a usable and DIFFERENT key — because merging those would require
61
- * re-sealing content that lives in the OS keychain and is not auth-core's to
62
- * re-seal. A human resolves that one, and the loud signal is the deliverable.
59
+ * The migration writes the marker and never deletes. Case 5 with a usable and
60
+ * DIFFERENT key on each side is refused by default. A caller that knows which
61
+ * key must win can pass `onConflict: 'adopt_legacy'`, which moves the canonical
62
+ * copy aside as `<file>.retired.<stamp>` (kept, never deleted) and adopts the
63
+ * legacy one. The credential envelope does that for the daemon's key, and keeps
64
+ * the retired DEK readable so nothing sealed under it is lost (OSK-12462): the
65
+ * refusal alone left the CLI signing with a key the server does not pin, which
66
+ * gets its refresh refused as a device-key mismatch.
63
67
  *
64
68
  * ROLLBACK. The legacy file is left exactly where it was, under its own name.
65
69
  * Older code looks there, finds it, and works — no operator step. The marker is
@@ -82,6 +86,13 @@ export const SUPERSEDED_MARKER_SUFFIX = '.superseded.json';
82
86
  * canonical copy existed and was NOT usable — a usable one is never replaced.
83
87
  */
84
88
  export const SUPERSEDED_COPY_SUFFIX = '.superseded';
89
+ /**
90
+ * Suffix for a USABLE canonical file set aside by a converging consolidation
91
+ * (`onConflict: 'adopt_legacy'`). Distinct from `SUPERSEDED_COPY_SUFFIX`, which
92
+ * names an unusable copy: a retired key still opens what was sealed under it,
93
+ * and the credential envelope reads these as its keyring (OSK-12462).
94
+ */
95
+ export const RETIRED_COPY_SUFFIX = '.retired';
85
96
  /** The canonical directory for a profile's auth state. */
86
97
  export function profileStateDir(profile) {
87
98
  assertProfile(profile);
@@ -213,6 +224,28 @@ export async function consolidateProfileState(profile, spec) {
213
224
  identities,
214
225
  };
215
226
  }
227
+ if (spec.onConflict === 'adopt_legacy') {
228
+ const aside = dryRun
229
+ ? plannedAside(canonicalDir, spec.files)
230
+ : moveAside(canonicalDir, spec.files, RETIRED_COPY_SUFFIX);
231
+ const moved = adopt(legacyDir, canonicalDir, spec.files, dryRun);
232
+ if (!dryRun)
233
+ writeMarker(markerPath, canonicalPrimary, 'converged');
234
+ emitOutcome(spec, 'converged', {
235
+ profile,
236
+ identities,
237
+ from: legacyDir,
238
+ to: canonicalDir,
239
+ retired: aside,
240
+ });
241
+ return {
242
+ ...base,
243
+ status: 'converged',
244
+ written: moved.written,
245
+ supersededAside: aside,
246
+ identities,
247
+ };
248
+ }
216
249
  emitOutcome(spec, 'conflict', { profile, identities, canonicalDir, legacyDir });
217
250
  return { ...base, status: 'conflict', identities };
218
251
  }
@@ -297,14 +330,14 @@ function adopt(fromDir, toDir, files, dryRun) {
297
330
  }
298
331
  return { written, supersededAside: [] };
299
332
  }
300
- function moveAside(dir, files) {
333
+ function moveAside(dir, files, suffix = SUPERSEDED_COPY_SUFFIX) {
301
334
  const stamp = new Date().toISOString().replace(/[:.]/g, '-');
302
335
  const aside = [];
303
336
  for (const name of files) {
304
337
  const target = path.join(dir, name);
305
338
  if (!existsFile(target))
306
339
  continue;
307
- fs.renameSync(target, `${target}${SUPERSEDED_COPY_SUFFIX}.${stamp}`);
340
+ fs.renameSync(target, `${target}${suffix}.${stamp}`);
308
341
  aside.push(target);
309
342
  }
310
343
  return aside;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skrr-ai/auth-core",
3
- "version": "0.4.7",
3
+ "version": "0.4.8",
4
4
  "main": "dist/cjs/index.js",
5
5
  "types": "dist/esm/index.d.ts",
6
6
  "exports": {