@skrr-ai/cli 0.1.123 → 0.1.124

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 (85) hide show
  1. package/dist/commands/agents/delete.d.ts +8 -0
  2. package/dist/commands/agents/delete.js +16 -0
  3. package/dist/commands/context.js +7 -0
  4. package/dist/commands/search/workspace.js +1 -1
  5. package/dist/commands/store/gallery-admissions/approve.js +1 -1
  6. package/dist/commands/store/gallery-admissions/preview.js +1 -1
  7. package/dist/commands/store/gallery-admissions/refuse.js +1 -1
  8. package/dist/commands/store/gallery-admissions/show.js +1 -1
  9. package/dist/lib/auth-core-init.d.ts +15 -2
  10. package/dist/lib/auth-core-init.js +47 -6
  11. package/dist/lib/command-hygiene.js +30 -1
  12. package/dist/lib/daemonBrokerRefusal.js +5 -1
  13. package/dist/lib/keychain.js +41 -4
  14. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.d.ts +5 -1
  15. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.js +1 -1
  16. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.js +13 -28
  17. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.d.ts +133 -4
  18. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +387 -90
  19. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKey.d.ts +106 -7
  20. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKey.js +77 -19
  21. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKeyHandle.d.ts +33 -0
  22. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKeyHandle.js +77 -0
  23. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKeyHardware.d.ts +68 -0
  24. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKeyHardware.js +264 -0
  25. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKeyServerSupport.d.ts +31 -0
  26. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKeyServerSupport.js +90 -0
  27. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/dpop.js +2 -0
  28. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/independentMachineReservation.js +2 -0
  29. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +7 -3
  30. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +102 -0
  31. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/index.d.ts +1 -1
  32. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/index.js +6 -0
  33. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/linux.d.ts +18 -0
  34. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/linux.js +8 -0
  35. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/macos.d.ts +22 -0
  36. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/macos.js +11 -0
  37. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/masterKekService.d.ts +43 -3
  38. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/masterKekService.js +33 -3
  39. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/liveState.d.ts +154 -0
  40. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/liveState.js +281 -0
  41. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineId.js +6 -3
  42. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.js +8 -5
  43. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/profileStateDir.js +7 -27
  44. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.d.ts +1 -1
  45. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.js +12 -10
  46. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/stateFiles.d.ts +31 -0
  47. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/stateFiles.js +89 -0
  48. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.d.ts +5 -1
  49. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.js +4 -1
  50. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.js +15 -37
  51. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.d.ts +133 -4
  52. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +536 -101
  53. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKey.d.ts +106 -7
  54. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKey.js +131 -27
  55. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKeyHandle.d.ts +33 -0
  56. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKeyHandle.js +81 -0
  57. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKeyHardware.d.ts +68 -0
  58. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKeyHardware.js +317 -0
  59. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKeyServerSupport.d.ts +31 -0
  60. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKeyServerSupport.js +86 -0
  61. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/dpop.js +4 -0
  62. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/independentMachineReservation.js +4 -0
  63. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +7 -3
  64. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +13 -3
  65. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/index.d.ts +1 -1
  66. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/index.js +1 -1
  67. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/linux.d.ts +18 -0
  68. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/linux.js +15 -0
  69. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/macos.d.ts +22 -0
  70. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/macos.js +20 -0
  71. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/masterKekService.d.ts +43 -3
  72. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/masterKekService.js +104 -8
  73. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/liveState.d.ts +154 -0
  74. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/liveState.js +386 -0
  75. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineId.js +6 -3
  76. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.js +11 -5
  77. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/profileStateDir.js +6 -35
  78. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.d.ts +1 -1
  79. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.js +14 -10
  80. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/stateFiles.d.ts +31 -0
  81. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/stateFiles.js +73 -0
  82. package/dist/node_modules/@skrr-ai/auth-core/package.json +11 -1
  83. package/dist/node_modules/@skrr-ai/data-provider/index.js +25 -7
  84. package/oclif.manifest.json +1 -1
  85. package/package.json +4 -1
@@ -28,6 +28,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
28
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
29
  var deviceIdentityBridge_exports = {};
30
30
  __export(deviceIdentityBridge_exports, {
31
+ DEVICE_KEY_UNREADABLE_CREDENTIAL_HINT: () => DEVICE_KEY_UNREADABLE_CREDENTIAL_HINT,
32
+ PENDING_HARDWARE_KEY_MAX_AGE_MS: () => PENDING_HARDWARE_KEY_MAX_AGE_MS,
31
33
  __enrollmentBackoffPathForTest: () => __enrollmentBackoffPathForTest,
32
34
  __enrollmentMarkerPathForTest: () => __enrollmentMarkerPathForTest,
33
35
  __getStateForTest: () => __getStateForTest,
@@ -36,23 +38,35 @@ __export(deviceIdentityBridge_exports, {
36
38
  __setStateForTest: () => __setStateForTest,
37
39
  __setUnreadableReloadIntervalForTest: () => __setUnreadableReloadIntervalForTest,
38
40
  clearEnrollmentMarker: () => clearEnrollmentMarker,
41
+ commitDeviceKeyCandidate: () => commitDeviceKeyCandidate,
42
+ commitPendingHardwareKey: () => commitPendingHardwareKey,
39
43
  describeDeviceIdentityState: () => describeDeviceIdentityState,
40
44
  describeDeviceKeySplit: () => describeDeviceKeySplit,
45
+ describeStoredDeviceKey: () => describeStoredDeviceKey,
46
+ discardPendingHardwareKey: () => discardPendingHardwareKey,
41
47
  enrollWithServer: () => enrollWithServer,
42
48
  getDevicePublicKey: () => getDevicePublicKey,
43
49
  initDeviceIdentity: () => initDeviceIdentity,
44
50
  isDeviceIdentityActive: () => isDeviceIdentityActive,
51
+ persistPendingHardwareKey: () => persistPendingHardwareKey,
52
+ prepareHardwareKeyCandidate: () => prepareHardwareKeyCandidate,
45
53
  resetDeviceIdentity: () => resetDeviceIdentity,
46
- signServerRequest: () => signServerRequest
54
+ signServerRequest: () => signServerRequest,
55
+ signWithCandidate: () => signWithCandidate
47
56
  });
48
57
  module.exports = __toCommonJS(deviceIdentityBridge_exports);
49
58
  var import_node_fs = __toESM(require("node:fs"), 1);
50
59
  var import_node_path = __toESM(require("node:path"), 1);
51
60
  var import_proper_lockfile = __toESM(require("proper-lockfile"), 1);
52
61
  var import_deviceKey = require("./deviceKey.js");
62
+ var import_deviceKeyHandle = require("./deviceKeyHandle.js");
63
+ var import_deviceKeyHardware = require("./deviceKeyHardware.js");
53
64
  var import_credentialEnvelopeBridge = require("./credentialEnvelopeBridge.js");
54
65
  var import_profileStateDir = require("./profileStateDir.js");
66
+ var import_liveState = require("./liveState.js");
55
67
  var import_runtime = require("./runtime.js");
68
+ var import_stateFiles = require("./stateFiles.js");
69
+ const DEVICE_KEY_UNREADABLE_CREDENTIAL_HINT = "stored device key is unreadable on this machine \u2014 only an interactive daemon sign-in replaces it";
56
70
  const PUBLIC_KEY_FILENAME = "device-public-key.jwk";
57
71
  const PRIVATE_KEY_FILENAME = "device-private-key";
58
72
  const ENROLLMENT_MARKER_FILENAME = "device-enrollment.json";
@@ -82,7 +96,16 @@ function keyFilesStamp(profile) {
82
96
  };
83
97
  return `${part(publicKeyPath(profile))}|${part(privateKeyPath(profile))}`;
84
98
  }
99
+ let _activeSeen = null;
100
+ function trackActiveKeyFiles(profile) {
101
+ try {
102
+ _activeSeen = { profile, stamp: keyFilesStamp(profile), checkedAt: Date.now() };
103
+ } catch {
104
+ _activeSeen = null;
105
+ }
106
+ }
85
107
  function reloadIfRecovered() {
108
+ reloadIfActiveKeyChanged();
86
109
  if (_state.kind !== "disabled" || _state.reason !== "unreadable_key")
87
110
  return;
88
111
  const seen = _unreadableSeen;
@@ -101,26 +124,70 @@ function reloadIfRecovered() {
101
124
  if (stamp === seen.stamp)
102
125
  return;
103
126
  seen.stamp = stamp;
104
- let keypair = null;
127
+ let key = null;
105
128
  try {
106
- keypair = loadKeypair(seen.profile);
129
+ key = loadStoredKey(seen.profile);
107
130
  } catch {
108
- keypair = null;
131
+ key = null;
109
132
  }
110
- if (!keypair)
133
+ if (!key)
111
134
  return;
112
135
  _state = {
113
136
  kind: "active",
114
- keypair,
137
+ key,
115
138
  profile: seen.profile,
116
- envelopeProtected: (0, import_credentialEnvelopeBridge.isCredEnvelopeActive)()
139
+ envelopeProtected: key.custody === "software" && (0, import_credentialEnvelopeBridge.isCredEnvelopeActive)()
117
140
  };
118
141
  _unreadableSeen = null;
119
- emit("reloaded_after_recovery", true, { profile: seen.profile, thumbprint: keypair.thumbprint });
142
+ trackActiveKeyFiles(seen.profile);
143
+ emit("reloaded_after_recovery", true, {
144
+ profile: seen.profile,
145
+ thumbprint: key.thumbprint,
146
+ custody: key.custody
147
+ });
148
+ }
149
+ function reloadIfActiveKeyChanged() {
150
+ const state = _state;
151
+ const seen = _activeSeen;
152
+ if (state.kind !== "active" || !seen || seen.profile !== state.profile || _initializing)
153
+ return;
154
+ const now = Date.now();
155
+ if (now - seen.checkedAt < _unreadableReloadIntervalMs)
156
+ return;
157
+ seen.checkedAt = now;
158
+ let stamp;
159
+ try {
160
+ stamp = keyFilesStamp(seen.profile);
161
+ } catch {
162
+ return;
163
+ }
164
+ if (stamp === seen.stamp)
165
+ return;
166
+ seen.stamp = stamp;
167
+ let key = null;
168
+ try {
169
+ key = loadStoredKey(seen.profile);
170
+ } catch {
171
+ key = null;
172
+ }
173
+ if (!key || key.thumbprint === state.key.thumbprint)
174
+ return;
175
+ _state = {
176
+ kind: "active",
177
+ key,
178
+ profile: seen.profile,
179
+ envelopeProtected: key.custody === "software" && (0, import_credentialEnvelopeBridge.isCredEnvelopeActive)()
180
+ };
181
+ emit("reloaded_after_key_change", true, {
182
+ profile: seen.profile,
183
+ from: state.key.thumbprint,
184
+ thumbprint: key.thumbprint,
185
+ custody: key.custody
186
+ });
120
187
  }
121
188
  async function withDeviceIdentityLock(profile, body) {
122
189
  const dir = (0, import_profileStateDir.profileStateDir)(profile);
123
- import_node_fs.default.mkdirSync(dir, { recursive: true, mode: 448 });
190
+ (0, import_stateFiles.ensureStateDir)(dir, `take the device identity lock for profile ${profile}`);
124
191
  const release = await import_proper_lockfile.default.lock(dir, {
125
192
  realpath: false,
126
193
  lockfilePath: import_node_path.default.join(dir, ".device-identity.lock"),
@@ -176,29 +243,6 @@ function enrollmentMarkerPath(profile) {
176
243
  function enrollmentBackoffPath(profile) {
177
244
  return import_node_path.default.join(profileConfigDir(profile), ENROLLMENT_BACKOFF_FILENAME);
178
245
  }
179
- function atomicWrite0600(target, value) {
180
- const dir = import_node_path.default.dirname(target);
181
- import_node_fs.default.mkdirSync(dir, { recursive: true, mode: 448 });
182
- const tmp = `${target}.tmp.${process.pid}.${Date.now()}`;
183
- const buf = typeof value === "string" ? Buffer.from(value, "utf-8") : value;
184
- const fd = import_node_fs.default.openSync(tmp, "w", 384);
185
- try {
186
- import_node_fs.default.writeSync(fd, buf);
187
- import_node_fs.default.fsyncSync(fd);
188
- } finally {
189
- import_node_fs.default.closeSync(fd);
190
- }
191
- import_node_fs.default.renameSync(tmp, target);
192
- try {
193
- const dirFd = import_node_fs.default.openSync(dir, "r");
194
- try {
195
- import_node_fs.default.fsyncSync(dirFd);
196
- } finally {
197
- import_node_fs.default.closeSync(dirFd);
198
- }
199
- } catch {
200
- }
201
- }
202
246
  function emit(eventName, success, metadata, opts = {}) {
203
247
  (0, import_runtime.emitAuthTelemetry)({
204
248
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -216,17 +260,31 @@ function emit(eventName, success, metadata, opts = {}) {
216
260
  }
217
261
  }
218
262
  }
219
- function persistKeypair(profile, kp) {
220
- const publicPayload = JSON.stringify({
221
- publicJwk: kp.publicJwk,
222
- thumbprint: kp.thumbprint,
263
+ function writePublicKeyRecord(profile, publicJwk) {
264
+ (0, import_stateFiles.atomicWrite0600)(publicKeyPath(profile), JSON.stringify({
265
+ publicJwk,
266
+ thumbprint: (0, import_deviceKey.jwkThumbprint)(publicJwk),
267
+ alg: publicJwk.kty === "EC" ? "ES256" : "EdDSA",
268
+ custody: publicJwk.kty === "EC" ? "hardware" : "software",
223
269
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
270
+ }), `write the device public key for profile ${profile}`);
271
+ }
272
+ function persistHardwareKey(profile, record) {
273
+ (0, import_stateFiles.atomicWrite0600)(privateKeyPath(profile), (0, import_deviceKeyHardware.serializeHardwareKeyRecord)(record), `write the hardware device key record for profile ${profile}`);
274
+ writePublicKeyRecord(profile, record.publicJwk);
275
+ emit("keypair_persisted", true, {
276
+ profile,
277
+ thumbprint: (0, import_deviceKey.jwkThumbprint)(record.publicJwk),
278
+ custody: "hardware",
279
+ envelopeProtected: false
224
280
  });
225
- atomicWrite0600(publicKeyPath(profile), publicPayload);
281
+ }
282
+ function persistKeypair(profile, kp) {
283
+ writePublicKeyRecord(profile, kp.publicJwk);
226
284
  const privatePlaintext = JSON.stringify(kp.privateJwk);
227
285
  const wireForm = (0, import_credentialEnvelopeBridge.wrapIfActiveOrPassthrough)(privatePlaintext);
228
286
  const envelopeProtected = wireForm !== privatePlaintext;
229
- atomicWrite0600(privateKeyPath(profile), wireForm);
287
+ (0, import_stateFiles.atomicWrite0600)(privateKeyPath(profile), wireForm, `write the device private key for profile ${profile}`);
230
288
  emit("keypair_persisted", true, {
231
289
  profile,
232
290
  thumbprint: kp.thumbprint,
@@ -267,7 +325,7 @@ function readIndependentIdentityFile(file, sealed) {
267
325
  import_node_fs.default.closeSync(fd);
268
326
  }
269
327
  }
270
- function loadKeypair(profile) {
328
+ function loadStoredKey(profile) {
271
329
  const pubPath = publicKeyPath(profile);
272
330
  const privPath = privateKeyPath(profile);
273
331
  const independent = /^desktop-bg-[a-f0-9]{32}$/i.test(profile);
@@ -281,6 +339,8 @@ function loadKeypair(profile) {
281
339
  let storedThumbprint;
282
340
  try {
283
341
  const parsed = JSON.parse(pubRaw);
342
+ if (!(0, import_deviceKey.isDevicePublicJwk)(parsed.publicJwk))
343
+ throw new Error("public key is not a device key");
284
344
  publicJwk = parsed.publicJwk;
285
345
  storedThumbprint = parsed.thumbprint;
286
346
  } catch (err) {
@@ -290,7 +350,22 @@ function loadKeypair(profile) {
290
350
  });
291
351
  return null;
292
352
  }
293
- const decryptResult = (0, import_credentialEnvelopeBridge.maybeDecryptOnRead)(privRaw);
353
+ const hardware = independent ? null : (0, import_deviceKeyHardware.parseHardwareKeyRecord)(privRaw);
354
+ if (hardware) {
355
+ if ((0, import_deviceKey.jwkThumbprint)(hardware.publicJwk) !== (0, import_deviceKey.jwkThumbprint)(publicJwk)) {
356
+ try {
357
+ writePublicKeyRecord(profile, hardware.publicJwk);
358
+ emit("public_key_repaired_from_hardware_record", true, { profile }, { logLevel: "info" });
359
+ } catch (err) {
360
+ emit("public_key_unreadable", false, { profile, message: err?.message ?? "unknown" });
361
+ return null;
362
+ }
363
+ }
364
+ return (0, import_deviceKeyHardware.hardwareDeviceKeyHandle)(hardware);
365
+ }
366
+ const decryptResult = (0, import_credentialEnvelopeBridge.maybeDecryptOnRead)(privRaw, {
367
+ unreadableHint: DEVICE_KEY_UNREADABLE_CREDENTIAL_HINT
368
+ });
294
369
  if (decryptResult.plaintext === null) {
295
370
  emit("private_key_decrypt_failed", false, { profile });
296
371
  return null;
@@ -306,7 +381,7 @@ function loadKeypair(profile) {
306
381
  });
307
382
  return null;
308
383
  }
309
- if (publicJwk.x !== privateJwk.x) {
384
+ if (publicJwk.kty !== "OKP" || publicJwk.x !== privateJwk.x) {
310
385
  emit("keypair_inconsistent", false, { profile });
311
386
  return null;
312
387
  }
@@ -318,16 +393,20 @@ function loadKeypair(profile) {
318
393
  recomputed
319
394
  });
320
395
  }
321
- return {
322
- publicJwk,
323
- privateJwk,
324
- thumbprint: recomputed
325
- };
396
+ try {
397
+ return (0, import_deviceKeyHandle.softwareEd25519Handle)({ publicJwk, privateJwk, thumbprint: recomputed });
398
+ } catch (err) {
399
+ emit("private_key_malformed", false, {
400
+ profile,
401
+ message: err?.message ?? "unknown"
402
+ });
403
+ return null;
404
+ }
326
405
  }
327
406
  function deviceIdentityOf(dir) {
328
407
  try {
329
408
  const parsed = JSON.parse(import_node_fs.default.readFileSync(import_node_path.default.join(dir, PUBLIC_KEY_FILENAME), "utf-8"));
330
- if (!parsed.publicJwk || typeof parsed.publicJwk.x !== "string")
409
+ if (!(0, import_deviceKey.isDevicePublicJwk)(parsed.publicJwk))
331
410
  return null;
332
411
  return (0, import_deviceKey.jwkThumbprint)(parsed.publicJwk);
333
412
  } catch {
@@ -370,6 +449,7 @@ async function consolidateDeviceIdentity(profile) {
370
449
  onEvent: (name, success, meta) => emit(name, success, meta)
371
450
  });
372
451
  } catch (err) {
452
+ (0, import_liveState.rethrowIfLiveStateRefusal)(err);
373
453
  emit("path_consolidation.failed", false, {
374
454
  profile,
375
455
  message: err?.message ?? "unknown"
@@ -390,6 +470,7 @@ async function initDeviceIdentity(profile, options = {}) {
390
470
  if (_state.kind === "uninit")
391
471
  await initDeviceIdentityLocked(profile, options);
392
472
  }).catch((err) => {
473
+ (0, import_liveState.rethrowIfLiveStateRefusal)(err);
393
474
  _state = { kind: "disabled", reason: "init_failed" };
394
475
  emit("init_failed", false, {
395
476
  profile,
@@ -409,22 +490,24 @@ async function initDeviceIdentityLocked(profile, options = {}) {
409
490
  const independent = /^desktop-bg-[a-f0-9]{32}$/i.test(profile);
410
491
  if (!independent)
411
492
  await consolidateDeviceIdentity(profile);
412
- let keypair = null;
493
+ let stored = null;
413
494
  try {
414
- keypair = loadKeypair(profile);
495
+ stored = loadStoredKey(profile);
415
496
  } catch (err) {
416
497
  emit("load_failed", false, {
417
498
  profile,
418
499
  message: err?.message ?? "unknown"
419
500
  });
420
- keypair = null;
501
+ stored = null;
421
502
  }
422
- if (keypair) {
423
- const envelopeProtected = (0, import_credentialEnvelopeBridge.isCredEnvelopeActive)();
424
- _state = { kind: "active", keypair, profile, envelopeProtected };
503
+ if (stored) {
504
+ const envelopeProtected = stored.custody === "software" && (0, import_credentialEnvelopeBridge.isCredEnvelopeActive)();
505
+ _state = { kind: "active", key: stored, profile, envelopeProtected };
506
+ trackActiveKeyFiles(profile);
425
507
  emit("active", true, {
426
508
  profile,
427
- thumbprint: keypair.thumbprint,
509
+ thumbprint: stored.thumbprint,
510
+ custody: stored.custody,
428
511
  envelopeProtected,
429
512
  regenerated: false
430
513
  });
@@ -458,6 +541,7 @@ async function initDeviceIdentityLocked(profile, options = {}) {
458
541
  try {
459
542
  persistResult = persistKeypair(profile, fresh);
460
543
  } catch (err) {
544
+ (0, import_liveState.rethrowIfLiveStateRefusal)(err);
461
545
  _state = { kind: "disabled", reason: "persist_failed" };
462
546
  emit("persist_failed", false, {
463
547
  profile,
@@ -465,15 +549,29 @@ async function initDeviceIdentityLocked(profile, options = {}) {
465
549
  });
466
550
  return;
467
551
  }
552
+ let freshKey;
553
+ try {
554
+ freshKey = (0, import_deviceKeyHandle.softwareEd25519Handle)(fresh);
555
+ } catch (err) {
556
+ _state = { kind: "disabled", reason: "init_failed" };
557
+ emit("init_failed", false, {
558
+ profile,
559
+ reason: "keygen_threw",
560
+ message: err?.message ?? "unknown"
561
+ });
562
+ return;
563
+ }
468
564
  _state = {
469
565
  kind: "active",
470
- keypair: fresh,
566
+ key: freshKey,
471
567
  profile,
472
568
  envelopeProtected: persistResult.envelopeProtected
473
569
  };
570
+ trackActiveKeyFiles(profile);
474
571
  emit("active", true, {
475
572
  profile,
476
573
  thumbprint: fresh.thumbprint,
574
+ custody: "software",
477
575
  envelopeProtected: persistResult.envelopeProtected,
478
576
  regenerated: true
479
577
  });
@@ -482,12 +580,18 @@ async function resetDeviceIdentity(opts) {
482
580
  if (_initializing)
483
581
  await _initializing;
484
582
  if (opts?.clearOnDisk && opts.profile) {
485
- const targets = [PUBLIC_KEY_FILENAME, PRIVATE_KEY_FILENAME].flatMap((name) => (0, import_profileStateDir.profileStateFilePathsToClear)(opts.profile, name));
583
+ const targets = [
584
+ ...[PUBLIC_KEY_FILENAME, PRIVATE_KEY_FILENAME].flatMap((name) => (0, import_profileStateDir.profileStateFilePathsToClear)(opts.profile, name)),
585
+ ...retiredKeyCopies(opts.profile, PUBLIC_KEY_FILENAME),
586
+ ...retiredKeyCopies(opts.profile, PRIVATE_KEY_FILENAME),
587
+ pendingHardwareKeyPath(opts.profile)
588
+ ];
486
589
  await withDeviceIdentityLock(opts.profile, async () => {
487
590
  for (const p of targets) {
488
591
  try {
489
- import_node_fs.default.unlinkSync(p);
592
+ (0, import_stateFiles.removeStateFile)(p, `delete the device key file ${import_node_path.default.basename(p)}`);
490
593
  } catch (err) {
594
+ (0, import_liveState.rethrowIfLiveStateRefusal)(err);
491
595
  if (err.code !== "ENOENT") {
492
596
  emit("clear_failed", false, {
493
597
  profile: opts.profile,
@@ -501,6 +605,150 @@ async function resetDeviceIdentity(opts) {
501
605
  }
502
606
  _state = { kind: "uninit" };
503
607
  _unreadableSeen = null;
608
+ _activeSeen = null;
609
+ }
610
+ async function prepareHardwareKeyCandidate(profile) {
611
+ if (/^desktop-bg-[a-f0-9]{32}$/i.test(profile))
612
+ return { ok: false, reason: "independent_profile" };
613
+ if (_state.kind === "active" && _state.key.custody === "hardware") {
614
+ return { ok: false, reason: "already_hardware" };
615
+ }
616
+ const capability = await (0, import_deviceKeyHardware.probeHardwareDeviceKey)();
617
+ if (!capability.available)
618
+ return { ok: false, reason: "unavailable", capability };
619
+ try {
620
+ const { record, handle } = await (0, import_deviceKeyHardware.createHardwareDeviceKey)();
621
+ return { ok: true, candidate: { key: handle, record } };
622
+ } catch (err) {
623
+ emit("hardware_key_create_failed", false, {
624
+ profile,
625
+ ...err instanceof import_deviceKeyHardware.HardwareDeviceKeyError ? { hardwareFailure: err.code } : {},
626
+ message: err?.message ?? "unknown"
627
+ });
628
+ return { ok: false, reason: "create_failed", message: err?.message ?? "unknown" };
629
+ }
630
+ }
631
+ async function signWithCandidate(candidate, opts) {
632
+ try {
633
+ return await (0, import_deviceKey.signProofWith)(candidate.key, opts);
634
+ } catch (err) {
635
+ emit("sign_failed", false, {
636
+ thumbprint: candidate.key.thumbprint,
637
+ custody: "hardware",
638
+ candidate: true,
639
+ method: opts.method,
640
+ ...err instanceof import_deviceKeyHardware.HardwareDeviceKeyError ? { hardwareFailure: err.code } : {},
641
+ message: err?.message ?? "unknown"
642
+ });
643
+ return null;
644
+ }
645
+ }
646
+ function retiredKeyCopies(profile, filename) {
647
+ const dir = import_node_path.default.dirname(filename === PUBLIC_KEY_FILENAME ? publicKeyPath(profile) : privateKeyPath(profile));
648
+ const prefix = `${filename}${import_profileStateDir.RETIRED_COPY_SUFFIX}.`;
649
+ try {
650
+ return import_node_fs.default.readdirSync(dir).filter((name) => name.startsWith(prefix)).sort().map((name) => import_node_path.default.join(dir, name));
651
+ } catch {
652
+ return [];
653
+ }
654
+ }
655
+ function retireSoftwareKey(profile) {
656
+ try {
657
+ const priv = privateKeyPath(profile);
658
+ const raw = import_node_fs.default.readFileSync(priv, "utf-8");
659
+ if ((0, import_deviceKeyHardware.parseHardwareKeyRecord)(raw))
660
+ return;
661
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
662
+ (0, import_stateFiles.atomicWrite0600)(`${priv}${import_profileStateDir.RETIRED_COPY_SUFFIX}.${stamp}`, raw, `keep the replaced software device key for profile ${profile}`);
663
+ try {
664
+ (0, import_stateFiles.atomicWrite0600)(`${publicKeyPath(profile)}${import_profileStateDir.RETIRED_COPY_SUFFIX}.${stamp}`, import_node_fs.default.readFileSync(publicKeyPath(profile), "utf-8"), `keep the replaced software device public key for profile ${profile}`);
665
+ } catch (err) {
666
+ (0, import_liveState.rethrowIfLiveStateRefusal)(err);
667
+ }
668
+ for (const filename of [PRIVATE_KEY_FILENAME, PUBLIC_KEY_FILENAME]) {
669
+ for (const stale of retiredKeyCopies(profile, filename).slice(0, -1)) {
670
+ try {
671
+ (0, import_stateFiles.removeStateFile)(stale, `prune a retired device key copy for profile ${profile}`);
672
+ } catch (err) {
673
+ (0, import_liveState.rethrowIfLiveStateRefusal)(err);
674
+ }
675
+ }
676
+ }
677
+ } catch (err) {
678
+ emit("retire_software_key_failed", false, { profile, message: err?.message ?? "unknown" });
679
+ }
680
+ }
681
+ async function commitDeviceKeyCandidate(profile, candidate) {
682
+ if (_initializing)
683
+ await _initializing;
684
+ await withDeviceIdentityLock(profile, async () => {
685
+ commitRecordLocked(profile, candidate.record, candidate.key);
686
+ });
687
+ }
688
+ function commitRecordLocked(profile, record, key) {
689
+ retireSoftwareKey(profile);
690
+ persistHardwareKey(profile, record);
691
+ clearEnrollmentMarker(profile);
692
+ removePendingHardwareKey(profile);
693
+ _state = { kind: "active", key, profile, envelopeProtected: false };
694
+ _unreadableSeen = null;
695
+ trackActiveKeyFiles(profile);
696
+ emit("upgraded_to_hardware", true, {
697
+ profile,
698
+ thumbprint: key.thumbprint,
699
+ custody: "hardware"
700
+ });
701
+ }
702
+ const PENDING_HARDWARE_FILENAME = "device-key.pending";
703
+ const PENDING_HARDWARE_KEY_MAX_AGE_MS = 10 * 60 * 1e3;
704
+ function pendingHardwareKeyPath(profile) {
705
+ return import_node_path.default.join(profileConfigDir(profile), PENDING_HARDWARE_FILENAME);
706
+ }
707
+ function removePendingHardwareKey(profile) {
708
+ try {
709
+ (0, import_stateFiles.removeStateFile)(pendingHardwareKeyPath(profile), `remove the pending hardware key for profile ${profile}`);
710
+ } catch (err) {
711
+ (0, import_liveState.rethrowIfLiveStateRefusal)(err);
712
+ if (err.code !== "ENOENT") {
713
+ emit("pending_hardware_key_clear_failed", false, { profile, message: err?.message ?? "unknown" });
714
+ }
715
+ }
716
+ }
717
+ async function persistPendingHardwareKey(profile, candidate) {
718
+ await withDeviceIdentityLock(profile, async () => {
719
+ (0, import_stateFiles.atomicWrite0600)(pendingHardwareKeyPath(profile), (0, import_deviceKeyHardware.serializeHardwareKeyRecord)(candidate.record), `park a pending hardware key for profile ${profile}`);
720
+ });
721
+ }
722
+ async function commitPendingHardwareKey(profile, thumbprint) {
723
+ if (_initializing)
724
+ await _initializing;
725
+ return withDeviceIdentityLock(profile, async () => {
726
+ const file = pendingHardwareKeyPath(profile);
727
+ let raw;
728
+ let modifiedAt;
729
+ try {
730
+ raw = import_node_fs.default.readFileSync(file, "utf-8");
731
+ modifiedAt = import_node_fs.default.statSync(file).mtimeMs;
732
+ } catch {
733
+ return "no_pending";
734
+ }
735
+ const record = (0, import_deviceKeyHardware.parseHardwareKeyRecord)(raw);
736
+ if (!record)
737
+ return "unreadable";
738
+ if (Date.now() - modifiedAt > PENDING_HARDWARE_KEY_MAX_AGE_MS) {
739
+ removePendingHardwareKey(profile);
740
+ return "expired";
741
+ }
742
+ if ((0, import_deviceKey.jwkThumbprint)(record.publicJwk) !== thumbprint)
743
+ return "mismatch";
744
+ commitRecordLocked(profile, record, (0, import_deviceKeyHardware.hardwareDeviceKeyHandle)(record));
745
+ return "committed";
746
+ });
747
+ }
748
+ async function discardPendingHardwareKey(profile) {
749
+ await withDeviceIdentityLock(profile, async () => {
750
+ removePendingHardwareKey(profile);
751
+ });
504
752
  }
505
753
  function isDeviceIdentityActive() {
506
754
  reloadIfRecovered();
@@ -510,7 +758,12 @@ function getDevicePublicKey() {
510
758
  reloadIfRecovered();
511
759
  if (_state.kind !== "active")
512
760
  return null;
513
- return { jwk: _state.keypair.publicJwk, thumbprint: _state.keypair.thumbprint };
761
+ return {
762
+ jwk: _state.key.publicJwk,
763
+ thumbprint: _state.key.thumbprint,
764
+ alg: _state.key.alg,
765
+ custody: _state.key.custody
766
+ };
514
767
  }
515
768
  function describeDeviceIdentityState() {
516
769
  reloadIfRecovered();
@@ -520,19 +773,41 @@ function describeDeviceIdentityState() {
520
773
  if (_state.kind === "active") {
521
774
  return {
522
775
  kind: "active",
523
- thumbprint: _state.keypair.thumbprint,
776
+ thumbprint: _state.key.thumbprint,
524
777
  profile: _state.profile,
525
- envelopeProtected: _state.envelopeProtected
778
+ envelopeProtected: _state.envelopeProtected,
779
+ alg: _state.key.alg,
780
+ custody: _state.key.custody,
781
+ canSign: _state.key.custody === "software" ? true : (0, import_deviceKeyHardware.resolveHardwareDeviceKeyHelper)() !== null
526
782
  };
527
783
  }
528
784
  return { kind: "uninit" };
529
785
  }
530
- function signServerRequest(opts) {
786
+ function describeStoredDeviceKey(profile) {
787
+ try {
788
+ const parsed = JSON.parse(import_node_fs.default.readFileSync(publicKeyPath(profile), "utf-8"));
789
+ if (!(0, import_deviceKey.isDevicePublicJwk)(parsed.publicJwk))
790
+ return { stored: false };
791
+ const alg = parsed.publicJwk.kty === "EC" ? "ES256" : "EdDSA";
792
+ const custody = alg === "ES256" ? "hardware" : "software";
793
+ return {
794
+ stored: true,
795
+ alg,
796
+ custody,
797
+ thumbprint: (0, import_deviceKey.jwkThumbprint)(parsed.publicJwk),
798
+ canSign: custody === "software" ? true : (0, import_deviceKeyHardware.resolveHardwareDeviceKeyHelper)() !== null
799
+ };
800
+ } catch {
801
+ return { stored: false };
802
+ }
803
+ }
804
+ async function signServerRequest(opts) {
531
805
  reloadIfRecovered();
532
- if (_state.kind !== "active")
806
+ const state = _state;
807
+ if (state.kind !== "active")
533
808
  return null;
534
809
  try {
535
- return (0, import_deviceKey.buildProof)(_state.keypair.privateJwk, _state.keypair.publicJwk, {
810
+ return await (0, import_deviceKey.signProofWith)(state.key, {
536
811
  method: opts.method,
537
812
  url: opts.url,
538
813
  refreshToken: opts.refreshToken,
@@ -540,9 +815,13 @@ function signServerRequest(opts) {
540
815
  });
541
816
  } catch (err) {
542
817
  emit("sign_failed", false, {
543
- profile: _state.profile,
544
- thumbprint: _state.keypair.thumbprint,
818
+ profile: state.profile,
819
+ thumbprint: state.key.thumbprint,
820
+ custody: state.key.custody,
545
821
  method: opts.method,
822
+ // A hardware failure names its cause (helper missing, timed out, key
823
+ // mismatch) so a status line can say what to do about it.
824
+ ...err instanceof import_deviceKeyHardware.HardwareDeviceKeyError ? { hardwareFailure: err.code } : {},
546
825
  message: err?.message ?? "unknown"
547
826
  });
548
827
  return null;
@@ -563,8 +842,9 @@ function __enrollmentBackoffPathForTest(profile) {
563
842
  function clearEnrollmentMarker(profile) {
564
843
  for (const markerPath of [enrollmentMarkerPath(profile), enrollmentBackoffPath(profile)]) {
565
844
  try {
566
- import_node_fs.default.unlinkSync(markerPath);
845
+ (0, import_stateFiles.removeStateFile)(markerPath, `delete the enrollment file ${import_node_path.default.basename(markerPath)}`);
567
846
  } catch (err) {
847
+ (0, import_liveState.rethrowIfLiveStateRefusal)(err);
568
848
  if (err.code !== "ENOENT") {
569
849
  emit("enrollment_marker_clear_failed", false, {
570
850
  profile,
@@ -581,7 +861,7 @@ async function enrollWithServer(opts) {
581
861
  return { enrolled: false, reason: "not_active" };
582
862
  }
583
863
  const profile = _state.profile;
584
- const keypair = _state.keypair;
864
+ const key = _state.key;
585
865
  const markerPath = enrollmentMarkerPath(profile);
586
866
  const backoffPath = enrollmentBackoffPath(profile);
587
867
  try {
@@ -593,27 +873,33 @@ async function enrollWithServer(opts) {
593
873
  try {
594
874
  const raw = JSON.parse(import_node_fs.default.readFileSync(backoffPath, "utf-8"));
595
875
  const retryAfter = Date.parse(raw.retryAfter || "");
596
- if (raw.serverUrl === serverUrl && raw.machineUuid === machineUuid && raw.thumbprint === keypair.thumbprint && Number.isFinite(retryAfter) && retryAfter > Date.now()) {
876
+ if (raw.serverUrl === serverUrl && raw.machineUuid === machineUuid && raw.thumbprint === key.thumbprint && Number.isFinite(retryAfter) && retryAfter > Date.now()) {
597
877
  return { enrolled: false, reason: "backoff_active" };
598
878
  }
599
- import_node_fs.default.unlinkSync(backoffPath);
879
+ (0, import_stateFiles.removeStateFile)(backoffPath, "delete the stale enrollment backoff");
600
880
  } catch (err) {
601
881
  if (err.code !== "ENOENT") {
602
882
  try {
603
- import_node_fs.default.unlinkSync(backoffPath);
604
- } catch {
883
+ (0, import_stateFiles.removeStateFile)(backoffPath, "delete the corrupt enrollment backoff");
884
+ } catch (clearErr) {
885
+ (0, import_liveState.rethrowIfLiveStateRefusal)(clearErr);
605
886
  }
606
887
  }
607
888
  }
608
889
  const base = serverUrl.replace(/\/+$/, "");
609
890
  const htu = `${base}/api/daemon-resources/enroll-device-key`;
610
- const proofJws = (0, import_deviceKey.buildProof)(keypair.privateJwk, keypair.publicJwk, {
611
- method: "POST",
612
- url: htu
613
- });
891
+ let proofJws;
892
+ try {
893
+ proofJws = await (0, import_deviceKey.signProofWith)(key, { method: "POST", url: htu });
894
+ } catch (err) {
895
+ const message = err?.message ?? "unknown";
896
+ emit("enrollment_sign_failed", false, { profile, thumbprint: key.thumbprint, custody: key.custody, message });
897
+ return { enrolled: false, reason: "not_active", message };
898
+ }
614
899
  const body = JSON.stringify({
615
900
  machineUuid,
616
- publicJwkX: keypair.publicJwk.x,
901
+ ...key.publicJwk.kty === "OKP" ? { publicJwkX: key.publicJwk.x } : {},
902
+ publicJwk: key.publicJwk,
617
903
  proofJws
618
904
  });
619
905
  const ownAbort = new AbortController();
@@ -634,7 +920,7 @@ async function enrollWithServer(opts) {
634
920
  const message = err?.message ?? "unknown";
635
921
  emit("enrollment_network_error", false, {
636
922
  profile,
637
- thumbprint: keypair.thumbprint,
923
+ thumbprint: key.thumbprint,
638
924
  serverUrl,
639
925
  message
640
926
  });
@@ -654,13 +940,13 @@ async function enrollWithServer(opts) {
654
940
  } catch {
655
941
  }
656
942
  }
657
- const pinnedToOtherKey = response.status === 409 && enrolledThumbprint !== null && enrolledThumbprint !== keypair.thumbprint;
943
+ const pinnedToOtherKey = response.status === 409 && enrolledThumbprint !== null && enrolledThumbprint !== key.thumbprint;
658
944
  const keyPinnedElsewhere = response.status === 409 && conflictCode === "ENROLLMENT_DUPLICATE_KEY";
659
945
  const isBenignRejection = response.status === 404 && responseText.includes("DAEMON_NOT_FOUND") || response.status === 403 && responseText.includes("OPAQUE_AUTH_REFUSED") || response.status === 409 && !pinnedToOtherKey && !keyPinnedElsewhere;
660
946
  if (pinnedToOtherKey) {
661
947
  emit("enrollment_key_mismatch", false, {
662
948
  profile,
663
- thumbprint: keypair.thumbprint,
949
+ thumbprint: key.thumbprint,
664
950
  enrolledThumbprint,
665
951
  serverUrl,
666
952
  message: "this machine is registered with a different device key than this profile signs with; its refreshes will be refused. An interactive daemon login (skrrd login) re-registers this key."
@@ -668,13 +954,14 @@ async function enrollWithServer(opts) {
668
954
  }
669
955
  if (response.status === 404 && responseText.includes("DAEMON_NOT_FOUND")) {
670
956
  try {
671
- atomicWrite0600(backoffPath, JSON.stringify({
957
+ (0, import_stateFiles.atomicWrite0600)(backoffPath, JSON.stringify({
672
958
  serverUrl,
673
959
  machineUuid,
674
- thumbprint: keypair.thumbprint,
960
+ thumbprint: key.thumbprint,
675
961
  retryAfter: new Date(Date.now() + DAEMON_NOT_FOUND_BACKOFF_MS).toISOString()
676
- }));
962
+ }), "write the enrollment backoff");
677
963
  } catch (err) {
964
+ (0, import_liveState.rethrowIfLiveStateRefusal)(err);
678
965
  emit("enrollment_backoff_write_failed", false, {
679
966
  profile,
680
967
  message: err?.message ?? "unknown"
@@ -683,7 +970,7 @@ async function enrollWithServer(opts) {
683
970
  }
684
971
  emit("enrollment_rejected", false, {
685
972
  profile,
686
- thumbprint: keypair.thumbprint,
973
+ thumbprint: key.thumbprint,
687
974
  serverUrl,
688
975
  status: response.status,
689
976
  body: responseText
@@ -698,20 +985,21 @@ async function enrollWithServer(opts) {
698
985
  const marker = JSON.stringify({
699
986
  enrolledAt: (/* @__PURE__ */ new Date()).toISOString(),
700
987
  serverUrl,
701
- thumbprint: keypair.thumbprint,
988
+ thumbprint: key.thumbprint,
702
989
  machineUuid
703
990
  });
704
991
  try {
705
- atomicWrite0600(markerPath, marker);
992
+ (0, import_stateFiles.atomicWrite0600)(markerPath, marker, "write the enrollment marker");
706
993
  } catch (err) {
994
+ (0, import_liveState.rethrowIfLiveStateRefusal)(err);
707
995
  emit("enrollment_marker_write_failed", false, {
708
996
  profile,
709
- thumbprint: keypair.thumbprint,
997
+ thumbprint: key.thumbprint,
710
998
  message: err?.message ?? "unknown"
711
999
  });
712
1000
  }
713
1001
  try {
714
- import_node_fs.default.unlinkSync(backoffPath);
1002
+ (0, import_stateFiles.removeStateFile)(backoffPath, "delete the enrollment backoff");
715
1003
  } catch (err) {
716
1004
  if (err.code !== "ENOENT") {
717
1005
  emit("enrollment_backoff_clear_failed", false, {
@@ -722,13 +1010,15 @@ async function enrollWithServer(opts) {
722
1010
  }
723
1011
  emit("enrolled_with_server", true, {
724
1012
  profile,
725
- thumbprint: keypair.thumbprint,
1013
+ thumbprint: key.thumbprint,
726
1014
  serverUrl
727
1015
  });
728
- return { enrolled: true, thumbprint: keypair.thumbprint };
1016
+ return { enrolled: true, thumbprint: key.thumbprint };
729
1017
  }
730
1018
  // Annotate the CommonJS export names for ESM import in node:
731
1019
  0 && (module.exports = {
1020
+ DEVICE_KEY_UNREADABLE_CREDENTIAL_HINT,
1021
+ PENDING_HARDWARE_KEY_MAX_AGE_MS,
732
1022
  __enrollmentBackoffPathForTest,
733
1023
  __enrollmentMarkerPathForTest,
734
1024
  __getStateForTest,
@@ -737,12 +1027,19 @@ async function enrollWithServer(opts) {
737
1027
  __setStateForTest,
738
1028
  __setUnreadableReloadIntervalForTest,
739
1029
  clearEnrollmentMarker,
1030
+ commitDeviceKeyCandidate,
1031
+ commitPendingHardwareKey,
740
1032
  describeDeviceIdentityState,
741
1033
  describeDeviceKeySplit,
1034
+ describeStoredDeviceKey,
1035
+ discardPendingHardwareKey,
742
1036
  enrollWithServer,
743
1037
  getDevicePublicKey,
744
1038
  initDeviceIdentity,
745
1039
  isDeviceIdentityActive,
1040
+ persistPendingHardwareKey,
1041
+ prepareHardwareKeyCandidate,
746
1042
  resetDeviceIdentity,
747
- signServerRequest
1043
+ signServerRequest,
1044
+ signWithCandidate
748
1045
  });