@mnemonik/cli 7.111.10 → 7.112.3

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 (55) hide show
  1. package/dist/auth/credentials.d.ts +7 -2
  2. package/dist/auth/credentials.d.ts.map +1 -1
  3. package/dist/auth/credentials.js +311 -98
  4. package/dist/auth/index.d.ts +15 -0
  5. package/dist/auth/index.d.ts.map +1 -1
  6. package/dist/auth/index.js +72 -15
  7. package/dist/auth/index.js.map +1 -1
  8. package/dist/auth/installSession.js +3 -2
  9. package/dist/auth/status.js +3 -2
  10. package/dist/diagnostics.js +315 -102
  11. package/dist/digests.json +53 -53
  12. package/dist/identity/migrate.js +40 -2
  13. package/dist/install/hosts.js +3 -2
  14. package/dist/install/journal.js +40 -2
  15. package/dist/install/journey.js +40 -2
  16. package/dist/install/ownership.js +40 -2
  17. package/dist/install/transaction.d.ts +1 -3
  18. package/dist/install/transaction.d.ts.map +1 -1
  19. package/dist/install/transaction.js +67 -16
  20. package/dist/install/transaction.js.map +1 -1
  21. package/dist/install/ui.d.ts.map +1 -1
  22. package/dist/install/ui.js +42 -2
  23. package/dist/install/ui.js.map +1 -1
  24. package/dist/installSession.js +3 -2
  25. package/dist/installation.js +40 -2
  26. package/dist/launcher.js +40 -2
  27. package/dist/preflight.js +4 -3
  28. package/dist/project/eligibility.js +40 -2
  29. package/dist/project/records.js +40 -2
  30. package/dist/project.d.ts +11 -2
  31. package/dist/project.d.ts.map +1 -1
  32. package/dist/project.js +46 -4
  33. package/dist/project.js.map +1 -1
  34. package/dist/router.js +41 -3
  35. package/dist/runtime/releaseSource.js +40 -2
  36. package/dist/runtime/selfUpdate.js +40 -2
  37. package/dist/runtime/signers.js +242 -50
  38. package/dist/runtime/store.js +34 -0
  39. package/dist/runtime/updateCheck.js +40 -2
  40. package/dist/sbom.json +21 -21
  41. package/dist/scanner/control.js +40 -2
  42. package/dist/scanner/data.js +3 -2
  43. package/dist/scanner/discover.js +4 -3
  44. package/dist/scanner/enable.js +40 -2
  45. package/dist/scanner/picker.js +3 -2
  46. package/dist/scanner/service.js +40 -2
  47. package/dist/scanner/update.js +40 -2
  48. package/dist/scanner-release.json +29 -29
  49. package/dist/status.js +44 -6
  50. package/dist/transport/server.d.ts +21 -2
  51. package/dist/transport/server.d.ts.map +1 -1
  52. package/dist/transport/server.js +15 -1
  53. package/dist/transport/server.js.map +1 -1
  54. package/dist/vendor/local-setup.js +40 -2
  55. package/package.json +31 -31
@@ -501,6 +501,13 @@ var require_ignore = __commonJS({
501
501
  }
502
502
  });
503
503
 
504
+ // packages/shared/dist/hookTimeouts.js
505
+ var init_hookTimeouts = __esm({
506
+ "packages/shared/dist/hookTimeouts.js"() {
507
+ "use strict";
508
+ }
509
+ });
510
+
504
511
  // packages/shared/dist/runtimeSigners.js
505
512
  import { lstatSync, mkdirSync, readFileSync, writeFileSync, renameSync, unlinkSync } from "node:fs";
506
513
  import { basename, dirname as dirname2, join as join2, resolve, sep, win32 } from "node:path";
@@ -747,11 +754,37 @@ var init_runtimeReader = __esm({
747
754
  });
748
755
 
749
756
  // packages/shared/dist/projectSetupHandoff.js
757
+ var PROJECT_ENSURE_TIMEOUT_MS, SUPERVISOR, RUN_STALE_MS;
750
758
  var init_projectSetupHandoff = __esm({
751
759
  "packages/shared/dist/projectSetupHandoff.js"() {
752
760
  "use strict";
753
761
  init_runtimeSigners();
762
+ init_hookTimeouts();
754
763
  init_runtimeReader();
764
+ PROJECT_ENSURE_TIMEOUT_MS = 3e4;
765
+ SUPERVISOR = `
766
+ const { execFile } = require('node:child_process');
767
+ const { writeFileSync, renameSync } = require('node:fs');
768
+ const [out, bin, ...args] = process.argv.slice(1);
769
+ let input = '';
770
+ process.stdin.setEncoding('utf8');
771
+ process.stdin.on('data', (chunk) => (input += chunk));
772
+ process.stdin.on('end', () => {
773
+ const child = execFile(
774
+ process.execPath,
775
+ [bin, ...args],
776
+ { shell: false, timeout: ${PROJECT_ENSURE_TIMEOUT_MS}, killSignal: 'SIGKILL', maxBuffer: 64 * 1024, env: process.env },
777
+ (error, stdout) => {
778
+ const result = { code: error ? (typeof error.code === 'number' ? error.code : null) : 0, killed: !!(error && error.killed), stdout: String(stdout) };
779
+ writeFileSync(out + '.tmp', JSON.stringify(result), { mode: 0o600 });
780
+ renameSync(out + '.tmp', out);
781
+ }
782
+ );
783
+ child.stdin.on('error', () => {});
784
+ child.stdin.end(input);
785
+ });
786
+ `;
787
+ RUN_STALE_MS = PROJECT_ENSURE_TIMEOUT_MS + 1e4;
755
788
  }
756
789
  });
757
790
 
@@ -760,6 +793,7 @@ var init_hookRuntime = __esm({
760
793
  "packages/shared/dist/hookRuntime.js"() {
761
794
  "use strict";
762
795
  init_runtimeSigners();
796
+ init_hookTimeouts();
763
797
  init_repositoryRoot();
764
798
  init_runtimeReader();
765
799
  init_runtimeSigners();
@@ -2345,8 +2379,9 @@ var require_proper_lockfile = __commonJS({
2345
2379
  });
2346
2380
 
2347
2381
  // packages/credentials/dist/index.js
2348
- import { createHmac, randomBytes } from "node:crypto";
2349
- import { join as join6 } from "node:path";
2382
+ import { createHash as createHash4, createHmac, randomBytes } from "node:crypto";
2383
+ import { realpath } from "node:fs/promises";
2384
+ import { basename as basename3, dirname as dirname5, join as join6 } from "node:path";
2350
2385
 
2351
2386
  // packages/shared/dist/instructions.js
2352
2387
  var INSTRUCTIONS_CONTENT = `Mnemonik provides persistent project memory: decisions, rationale, tasks, and policies across sessions. If no block begins "PROJECT_CONTEXT schemaVersion=", session_bootstrap({ cwd }) loads project context when cwd is the real absolute path of the project root; a placeholder or relative path fails. With that block, context is loaded and no session_bootstrap call is needed. Code and docs show current artifacts, git shows changes, and memory carries the why; memory_search recalls rationale or intent, then Grep or Read verifies current source.
@@ -2359,7 +2394,7 @@ Project work items live in tasks. A pending list requires action:"list" and stat
2359
2394
 
2360
2395
  Which tool for what:
2361
2396
  - memory_get: hydrates exact originals by id.
2362
- - memory_add: saves a discrete decision or root cause.
2397
+ - memory_add: saves a decision, a user's preference, or a root cause.
2363
2398
  - memory_state: corrects wrong, outdated, or conflicting memory.
2364
2399
  - memory_info: explains confidence or origin when memory looks suspect.
2365
2400
  - memory_links: connects related decisions so they surface together.
@@ -2368,7 +2403,8 @@ Which tool for what:
2368
2403
  - policy: stores durable enforced rules or preferences, not memories.
2369
2404
  - tasks: creates follow-up work and closes completed work.
2370
2405
  - code_search: finds conceptual code when source wording differs.
2371
- - memory_search: finds rationale, intent, decisions, and prior work.`;
2406
+ - memory_search: finds rationale, intent, decisions, and prior work.
2407
+ - memory_ingest: brings the project's docs and code comments into memory when asked.`;
2372
2408
  function getMcpInstructions() {
2373
2409
  const process2 = globalThis.process;
2374
2410
  if (!process2?.env) {
@@ -6530,6 +6566,9 @@ var DEFAULT_OPTIONS = {
6530
6566
  };
6531
6567
  var BUILT_IN_IGNORE_DIR_SET = new Set(BUILT_IN_IGNORE_DIRS);
6532
6568
 
6569
+ // packages/shared/dist/index.js
6570
+ init_hookTimeouts();
6571
+
6533
6572
  // packages/shared/dist/jitDeliveryAck.js
6534
6573
  init_runtimeSigners();
6535
6574
 
@@ -6817,16 +6856,17 @@ function isCredentialSessionUnavailableError(error) {
6817
6856
  // packages/credentials/dist/storage.js
6818
6857
  import { createHash as createHash3 } from "node:crypto";
6819
6858
  import { lstat as nodeLstat, mkdir as mkdir2, open as open2, readdir, rmdir as rmdir2, unlink as unlink2 } from "node:fs/promises";
6820
- import { constants } from "node:fs";
6859
+ import { constants, realpathSync as realpathSync2 } from "node:fs";
6821
6860
  import { basename as basename2, dirname as dirname4, isAbsolute, join as join5, parse, relative, resolve as resolve2, sep as sep2 } from "node:path";
6822
6861
  var CredentialError = class extends Error {
6823
6862
  reason;
6824
- constructor(reason) {
6825
- super(reason);
6863
+ constructor(reason, message = reason) {
6864
+ super(message);
6826
6865
  this.reason = reason;
6827
6866
  this.name = "CredentialError";
6828
6867
  }
6829
6868
  };
6869
+ var STATE_ROOT_LINK_MESSAGE = "The Mnemonik state directory is a symbolic link. Replace it with a real directory, or set MNEMONIK_STATE_DIR to one.";
6830
6870
  var digest = (value) => createHash3("sha256").update(value).digest("hex");
6831
6871
  function credentialPaths(stateDir = stateDirectory(), familyId) {
6832
6872
  const root = join5(stateDir, "credentials");
@@ -6846,8 +6886,25 @@ function credentialPaths(stateDir = stateDirectory(), familyId) {
6846
6886
  };
6847
6887
  }
6848
6888
  var codeIs2 = (error, code) => error.code === code;
6889
+ function canonicalDirectory(path) {
6890
+ const missing = [];
6891
+ let current2 = resolve2(path);
6892
+ for (; ; ) {
6893
+ try {
6894
+ return join5(realpathSync2.native(current2), ...missing.reverse());
6895
+ } catch (error) {
6896
+ const parent = dirname4(current2);
6897
+ if (!codeIs2(error, "ENOENT") || parent === current2)
6898
+ return resolve2(path);
6899
+ missing.push(basename2(current2));
6900
+ current2 = parent;
6901
+ }
6902
+ }
6903
+ }
6849
6904
  var SecureFiles = class {
6850
6905
  stateDir;
6906
+ /** The root as configured; paths spelled through it are rebased onto `stateDir`. */
6907
+ configuredStateDir;
6851
6908
  platform;
6852
6909
  lstat;
6853
6910
  uid;
@@ -6856,7 +6913,8 @@ var SecureFiles = class {
6856
6913
  username;
6857
6914
  aclRun;
6858
6915
  constructor(options = {}) {
6859
- this.stateDir = resolve2(options.stateDir ?? stateDirectory());
6916
+ this.configuredStateDir = resolve2(options.stateDir ?? stateDirectory());
6917
+ this.stateDir = join5(canonicalDirectory(dirname4(this.configuredStateDir)), basename2(this.configuredStateDir));
6860
6918
  this.platform = options.platform ?? process.platform;
6861
6919
  this.lstat = options.lstat ?? nodeLstat;
6862
6920
  this.uid = options.uid ?? process.getuid?.();
@@ -6866,7 +6924,10 @@ var SecureFiles = class {
6866
6924
  this.aclRun = options.aclRun;
6867
6925
  }
6868
6926
  assertInsideState(path) {
6869
- const absolute = resolve2(path);
6927
+ let absolute = resolve2(path);
6928
+ const fromConfigured = relative(this.configuredStateDir, absolute);
6929
+ if (this.configuredStateDir !== this.stateDir && fromConfigured !== ".." && !fromConfigured.startsWith(`..${sep2}`) && !isAbsolute(fromConfigured))
6930
+ absolute = join5(this.stateDir, fromConfigured);
6870
6931
  const fromState = relative(this.stateDir, absolute);
6871
6932
  if (fromState === ".." || fromState.startsWith(`..${sep2}`) || isAbsolute(fromState))
6872
6933
  throw new CredentialError("path_outside_state");
@@ -6887,6 +6948,8 @@ var SecureFiles = class {
6887
6948
  async inspect(path, expectFile, allowMissing) {
6888
6949
  const absolute = this.assertInsideState(path);
6889
6950
  for (const component of this.components(absolute)) {
6951
+ if (component !== this.stateDir && !component.startsWith(`${this.stateDir}${sep2}`))
6952
+ continue;
6890
6953
  let value;
6891
6954
  try {
6892
6955
  value = await this.lstat(component);
@@ -6896,10 +6959,7 @@ var SecureFiles = class {
6896
6959
  throw error;
6897
6960
  }
6898
6961
  if (value.isSymbolicLink())
6899
- throw new CredentialError("symlink_rejected");
6900
- const protectedComponent = component === this.stateDir || component.startsWith(`${this.stateDir}${sep2}`);
6901
- if (!protectedComponent)
6902
- continue;
6962
+ throw new CredentialError("symlink_rejected", component === this.stateDir ? STATE_ROOT_LINK_MESSAGE : void 0);
6903
6963
  const final = component === absolute;
6904
6964
  if (this.platform === "win32") {
6905
6965
  if (final)
@@ -7237,6 +7297,7 @@ var action = (familyId, reason) => ({
7237
7297
  reason,
7238
7298
  familyId
7239
7299
  });
7300
+ var isSecretMissing = (error) => error instanceof Error && error.message === "credential_secret_missing";
7240
7301
  var retryLater = (familyId, reason) => ({
7241
7302
  status: "RETRY_LATER",
7242
7303
  reason,
@@ -7244,12 +7305,15 @@ var retryLater = (familyId, reason) => ({
7244
7305
  });
7245
7306
  var isFailure = (result) => "status" in result && (result.status === "ACTION_REQUIRED" || result.status === "RETRY_LATER");
7246
7307
  var diagnosedStores = /* @__PURE__ */ new WeakSet();
7308
+ var familyFileName = /^([0-9a-f]{64})\.json(?:\.[0-9a-f-]{36}\.tmp)?$/;
7309
+ var temporaryOf = (base, name2) => name2.startsWith(`${base}.`) && /^\.[0-9a-f-]{36}\.tmp$/.test(name2.slice(base.length));
7247
7310
  function createCredentialAdapter(options = {}) {
7248
7311
  const files = new SecureFiles({ ...options, stateDir: options.stateDir ?? stateDirectory() });
7249
7312
  const paths = credentialPaths(files.stateDir);
7250
7313
  const now = options.now ?? Date.now;
7251
7314
  const sleep = options.sleep ?? wait;
7252
7315
  const retryLimit = options.retryLimit ?? 2;
7316
+ const lockWaitMs = options.lockWaitMs ?? 6e4;
7253
7317
  const store = options.secretStore;
7254
7318
  const rotations = /* @__PURE__ */ new Map();
7255
7319
  let osAvailable;
@@ -7265,7 +7329,10 @@ function createCredentialAdapter(options = {}) {
7265
7329
  disable();
7266
7330
  return ok;
7267
7331
  }) : Promise.resolve(false);
7268
- const reference = (name2) => `mnemonik.credentials.v1:${name2}`;
7332
+ const reference = async (name2) => {
7333
+ const directory = createHash4("sha256").update(await realpath(files.stateDir));
7334
+ return `mnemonik.credentials.v1:${name2}:${directory.digest("hex").slice(0, 32)}`;
7335
+ };
7269
7336
  const relativeSecret = (path) => path.slice(files.stateDir.length + 1);
7270
7337
  const fileBackend = {
7271
7338
  store: "file",
@@ -7297,7 +7364,7 @@ function createCredentialAdapter(options = {}) {
7297
7364
  async function locator(name2, file, existing) {
7298
7365
  if (existing?.store === "os")
7299
7366
  return existing;
7300
- return name2 === "cli-oauth" && await osBackend.isAvailable() ? { store: "os", secretRef: reference(name2) } : { store: "file", secretFile: relativeSecret(file) };
7367
+ return name2 === "cli-oauth" && await osBackend.isAvailable() ? { store: "os", secretRef: await reference(name2) } : { store: "file", secretFile: relativeSecret(file) };
7301
7368
  }
7302
7369
  const useFile = (location, file) => {
7303
7370
  Object.assign(location, { store: "file", secretFile: relativeSecret(file) });
@@ -7351,6 +7418,14 @@ function createCredentialAdapter(options = {}) {
7351
7418
  return bytes ? parse2(bytes, kind) : null;
7352
7419
  }
7353
7420
  async function putCliOAuth(metadata2, tokens) {
7421
+ await files.ensureParent(paths.cliRecord);
7422
+ return leased(paths.cliRecord, () => writeCliOAuth(metadata2, tokens), () => putCliOAuth(metadata2, tokens));
7423
+ }
7424
+ async function waitForCliLease() {
7425
+ await files.ensureParent(paths.cliRecord);
7426
+ await leased(paths.cliRecord, async () => void 0, () => void 0);
7427
+ }
7428
+ async function writeCliOAuth(metadata2, tokens) {
7354
7429
  const current2 = await readRecord(paths.cliRecord, "cli-oauth");
7355
7430
  const location = await locator("cli-oauth", paths.cliSecret, current2 ?? void 0);
7356
7431
  const secret = typeof tokens === "string" ? { accessToken: "", refreshToken: tokens, accessExpiresAt: (/* @__PURE__ */ new Date(0)).toISOString() } : tokens;
@@ -7417,8 +7492,11 @@ function createCredentialAdapter(options = {}) {
7417
7492
  }
7418
7493
  async function putFamily(componentKind, response) {
7419
7494
  const familyPaths = credentialPaths(files.stateDir, response.id);
7420
- const current2 = await readRecord(familyPaths.record, "component");
7421
- return persistFamily(componentKind, response, current2 ?? void 0);
7495
+ await files.ensureParent(familyPaths.record);
7496
+ return leased(familyPaths.record, async () => {
7497
+ const current2 = await readRecord(familyPaths.record, "component");
7498
+ return persistFamily(componentKind, response, current2 ?? void 0);
7499
+ }, () => putFamily(componentKind, response));
7422
7500
  }
7423
7501
  async function readFamily(familyId) {
7424
7502
  const familyPaths = credentialPaths(files.stateDir, familyId);
@@ -7438,49 +7516,73 @@ function createCredentialAdapter(options = {}) {
7438
7516
  void _file;
7439
7517
  return { ...publicMetadata, ...secret };
7440
7518
  }
7441
- async function requestRotation(familyId, request) {
7442
- const started = now();
7443
- let lostResponseRetried = false;
7444
- let retryableAttempt = 0;
7519
+ async function requestRotation(familyId, request, state) {
7445
7520
  while (true) {
7446
7521
  let response;
7447
7522
  try {
7448
7523
  response = await request();
7449
7524
  } catch {
7450
- if (lostResponseRetried)
7525
+ if (state.lostResponseRetried)
7451
7526
  return action(familyId, "rotation_response_lost");
7452
- lostResponseRetried = true;
7527
+ state.lostResponseRetried = true;
7453
7528
  await sleep(250);
7454
- if (now() > started + 6e4)
7529
+ if (now() > state.started + 6e4)
7455
7530
  return action(familyId, "rotation_response_lost");
7456
7531
  continue;
7457
7532
  }
7458
7533
  if (response.status === 400 && response.body.error === "invalid_grant")
7459
7534
  return action(familyId, "invalid_grant");
7460
7535
  if (response.status === 429 || response.status >= 500) {
7461
- if (lostResponseRetried)
7536
+ if (state.lostResponseRetried)
7462
7537
  return action(familyId, "rotation_response_lost");
7463
- if (retryableAttempt >= retryLimit)
7538
+ if (state.retryableAttempt >= retryLimit)
7464
7539
  return retryLater(familyId, response.status === 429 ? "rate_limited" : "server_error");
7465
- const delay2 = response.retryAfterMs ?? 250 * 2 ** retryableAttempt;
7466
- retryableAttempt += 1;
7467
- await sleep(delay2);
7468
- continue;
7540
+ const delay2 = response.retryAfterMs ?? 250 * 2 ** state.retryableAttempt;
7541
+ state.retryableAttempt += 1;
7542
+ return { backoffMs: delay2 };
7469
7543
  }
7470
7544
  return response;
7471
7545
  }
7472
7546
  }
7547
+ async function leased(recordPath2, work, vanished) {
7548
+ let entered = false;
7549
+ try {
7550
+ return await withLock(recordPath2, lockWaitMs, () => {
7551
+ entered = true;
7552
+ return work();
7553
+ });
7554
+ } catch (error) {
7555
+ if (entered || error.code !== "ENOENT")
7556
+ throw error;
7557
+ return vanished();
7558
+ }
7559
+ }
7473
7560
  async function rotateStored(lockPath, missingId, read, request, decode, persist) {
7474
7561
  const predecessor = await read();
7475
7562
  if (!predecessor)
7476
7563
  return action(missingId, "family_missing");
7477
- return withLock(lockPath, options.lockWaitMs ?? 6e4, async () => {
7564
+ const attempts = {
7565
+ started: now(),
7566
+ lostResponseRetried: false,
7567
+ retryableAttempt: 0
7568
+ };
7569
+ for (; ; ) {
7570
+ const outcome = await rotateStoredOnce(lockPath, predecessor, read, request, decode, persist, attempts);
7571
+ if (!("backoffMs" in outcome))
7572
+ return outcome;
7573
+ await sleep(outcome.backoffMs);
7574
+ }
7575
+ }
7576
+ async function rotateStoredOnce(lockPath, predecessor, read, request, decode, persist, attempts) {
7577
+ return leased(lockPath, async () => {
7478
7578
  const current2 = await read();
7479
7579
  if (!current2)
7480
7580
  return action(predecessor.familyId, "family_missing");
7481
7581
  if (current2.refreshToken !== predecessor.refreshToken)
7482
7582
  return current2;
7483
- const response = await requestRotation(current2.familyId, () => request(current2));
7583
+ const response = await requestRotation(current2.familyId, () => request(current2), attempts);
7584
+ if ("backoffMs" in response)
7585
+ return response;
7484
7586
  if ("status" in response && (response.status === "ACTION_REQUIRED" || response.status === "RETRY_LATER"))
7485
7587
  return response;
7486
7588
  if (response.status !== 200)
@@ -7493,21 +7595,19 @@ function createCredentialAdapter(options = {}) {
7493
7595
  } catch {
7494
7596
  return action(current2.familyId, "persistence_failed");
7495
7597
  }
7496
- });
7598
+ }, () => action(predecessor.familyId, "family_missing"));
7497
7599
  }
7498
7600
  function rotateLocked(familyId, transport) {
7499
7601
  const familyPaths = credentialPaths(files.stateDir, familyId);
7500
- return rotateStored(familyPaths.record, familyId, () => readFamily(familyId), (current2) => transport.rotateFamily(familyId, current2.refreshToken), (body2) => "access_token" in body2 ? body2 : null, (current2, body2) => persistFamily(current2.componentKind, body2, {
7501
- schemaVersion: 1,
7502
- kind: "component",
7503
- familyId: current2.familyId,
7504
- componentKind: current2.componentKind,
7505
- scopes: current2.scopes,
7506
- accessExpiresAt: current2.accessExpiresAt,
7507
- refreshExpiresAt: current2.refreshExpiresAt,
7508
- lastRotationTime: current2.lastRotationTime,
7509
- ...current2.store === "os" ? { store: "os", secretRef: reference(`component:${familyId}`) } : { store: "file", secretFile: relativeSecret(familyPaths.secret) }
7510
- }));
7602
+ return rotateStored(
7603
+ familyPaths.record,
7604
+ familyId,
7605
+ () => readFamily(familyId),
7606
+ (current2) => transport.rotateFamily(familyId, current2.refreshToken),
7607
+ (body2) => "access_token" in body2 ? body2 : null,
7608
+ // The stored record's own locator: a reference rebuilt from the name could differ.
7609
+ async (current2, body2) => persistFamily(current2.componentKind, body2, await readRecord(familyPaths.record, "component") ?? void 0)
7610
+ );
7511
7611
  }
7512
7612
  function rotateFamily(familyId, transport) {
7513
7613
  const active = rotations.get(familyId);
@@ -7525,7 +7625,7 @@ function createCredentialAdapter(options = {}) {
7525
7625
  function rotateCliLocked(transport) {
7526
7626
  return rotateStored(paths.cliRecord, "cli", readCliOAuth, (current2) => transport.rotateCli(current2), (body2) => "access_token" in body2 && typeof body2.access_token === "string" && "refresh_token" in body2 && typeof body2.refresh_token === "string" && "expires_in" in body2 && typeof body2.expires_in === "number" && "scope" in body2 && typeof body2.scope === "string" ? body2 : null, async (current2, body2) => {
7527
7627
  const rotatedAt = now();
7528
- await putCliOAuth({
7628
+ await writeCliOAuth({
7529
7629
  issuer: current2.issuer,
7530
7630
  clientId: current2.clientId,
7531
7631
  familyId: current2.familyId,
@@ -7576,11 +7676,33 @@ function createCredentialAdapter(options = {}) {
7576
7676
  return second.status === 401 ? action(rotated.familyId, "protected_unauthorized") : second;
7577
7677
  }
7578
7678
  async function removeCliOAuth() {
7579
- const record = await readRecord(paths.cliRecord, "cli-oauth");
7580
- if (!record)
7581
- return;
7582
- await deleteSecret(record, paths.cliSecret);
7583
- await files.remove(paths.cliRecord);
7679
+ await removeLeased(paths.cliRecord, "cli-oauth", paths.cliSecret);
7680
+ }
7681
+ async function revokeCli(transport) {
7682
+ if (!await readRecord(paths.cliRecord, "cli-oauth"))
7683
+ return action("cli", "family_missing");
7684
+ return leased(paths.cliRecord, async () => {
7685
+ let current2;
7686
+ try {
7687
+ current2 = await readCliOAuth();
7688
+ } catch (error) {
7689
+ if (!isSecretMissing(error))
7690
+ throw error;
7691
+ await removeStored(paths.cliRecord, "cli-oauth", paths.cliSecret);
7692
+ return action("cli", "credential_secret_missing");
7693
+ }
7694
+ if (!current2)
7695
+ return action("cli", "family_missing");
7696
+ const response = await transport.revokeCli(current2);
7697
+ if (response.status === 429)
7698
+ return retryLater(current2.familyId, "rate_limited");
7699
+ if (response.status >= 500)
7700
+ return retryLater(current2.familyId, "server_error");
7701
+ if (response.status !== 200)
7702
+ return action(current2.familyId, "error" in response.body ? response.body.error : "revoke_refused");
7703
+ await removeStored(paths.cliRecord, "cli-oauth", paths.cliSecret);
7704
+ return { status: "revoked", familyId: current2.familyId };
7705
+ }, () => action("cli", "family_missing"));
7584
7706
  }
7585
7707
  async function withCredential(familyId, transport, work) {
7586
7708
  let current2 = await readFamily(familyId);
@@ -7601,43 +7723,98 @@ function createCredentialAdapter(options = {}) {
7601
7723
  const second = await work(rotated.accessToken);
7602
7724
  return second.status === 401 ? action(familyId, "protected_unauthorized") : second;
7603
7725
  }
7726
+ async function removeStored(recordPath2, kind, secretPath) {
7727
+ const record = await readRecord(recordPath2, kind);
7728
+ if (!record)
7729
+ return null;
7730
+ await files.remove(recordPath2);
7731
+ await deleteSecret(record, secretPath);
7732
+ return record;
7733
+ }
7734
+ async function removeLeased(recordPath2, kind, secretPath) {
7735
+ if (!await readRecord(recordPath2, kind))
7736
+ return null;
7737
+ return leased(recordPath2, () => removeStored(recordPath2, kind, secretPath), () => null);
7738
+ }
7739
+ async function orphanRefreshToken(secretPath) {
7740
+ const bytes = await files.read(secretPath);
7741
+ if (!bytes)
7742
+ return null;
7743
+ const secret = JSON.parse(bytes.toString());
7744
+ if (typeof secret.refreshToken !== "string")
7745
+ throw new Error("credential_record_invalid");
7746
+ return secret.refreshToken;
7747
+ }
7604
7748
  async function removeFamily(familyId) {
7605
7749
  const familyPaths = credentialPaths(files.stateDir, familyId);
7606
- const record = await readRecord(familyPaths.record, "component");
7607
- if (!record)
7608
- return;
7609
- await deleteSecret(record, familyPaths.secret);
7610
- await files.remove(familyPaths.record);
7750
+ await removeStored(familyPaths.record, "component", familyPaths.secret);
7611
7751
  }
7612
7752
  async function revokeFamily(familyId, transport) {
7613
- const current2 = await readFamily(familyId);
7614
- if (!current2)
7753
+ const familyPaths = credentialPaths(files.stateDir, familyId);
7754
+ if (!await readRecord(familyPaths.record, "component") && !await files.read(familyPaths.secret))
7615
7755
  return action(familyId, "family_missing");
7616
- const response = await transport.revokeFamily(familyId, current2.refreshToken);
7617
- if (response.status === 429)
7618
- return retryLater(familyId, "rate_limited");
7619
- if (response.status >= 500)
7620
- return retryLater(familyId, "server_error");
7621
- if (response.status !== 200)
7622
- return action(familyId, "error" in response.body ? response.body.error : "revoke_refused");
7623
- await removeFamily(familyId);
7624
- return { status: "revoked", familyId };
7756
+ await files.ensureParent(familyPaths.record);
7757
+ return leased(familyPaths.record, async () => {
7758
+ let current2;
7759
+ try {
7760
+ current2 = await readFamily(familyId);
7761
+ } catch (error) {
7762
+ if (!isSecretMissing(error))
7763
+ throw error;
7764
+ await removeFamily(familyId);
7765
+ return action(familyId, "credential_secret_missing");
7766
+ }
7767
+ const refreshToken = current2?.refreshToken ?? await orphanRefreshToken(familyPaths.secret);
7768
+ if (!refreshToken)
7769
+ return action(familyId, "family_missing");
7770
+ const response = await transport.revokeFamily(familyId, refreshToken);
7771
+ if (response.status === 429)
7772
+ return retryLater(familyId, "rate_limited");
7773
+ if (response.status >= 500)
7774
+ return retryLater(familyId, "server_error");
7775
+ const refusal = response.status === 200 ? null : "error" in response.body ? response.body.error : "revoke_refused";
7776
+ if (refusal)
7777
+ return action(familyId, refusal);
7778
+ if (current2)
7779
+ await removeFamily(familyId);
7780
+ else
7781
+ await files.remove(familyPaths.secret);
7782
+ return { status: "revoked", familyId };
7783
+ }, () => action(familyId, "family_missing"));
7625
7784
  }
7626
- async function rootKey() {
7627
- return withLock(paths.rootRecord, options.lockWaitMs ?? 6e4, async () => {
7628
- const record = await readRecord(paths.rootRecord, "root-binding");
7629
- if (record) {
7630
- const key2 = await readSecret(record, paths.rootSecret);
7631
- if (!key2 || key2.length !== 32)
7632
- throw new Error("root_binding_key_invalid");
7633
- return key2;
7634
- }
7635
- const location = await locator("root-binding", paths.rootSecret);
7636
- const key = randomBytes(32);
7637
- await writeSecret(location, paths.rootSecret, key);
7638
- await files.write(paths.rootRecord, json({ schemaVersion: 1, kind: "root-binding", ...location }), "record");
7639
- return key;
7785
+ async function storedRootKey() {
7786
+ const record = await readRecord(paths.rootRecord, "root-binding");
7787
+ if (!record)
7788
+ return null;
7789
+ const key = await readSecret(record, paths.rootSecret).catch((error) => {
7790
+ if (isSecretMissing(error))
7791
+ return null;
7792
+ throw error;
7640
7793
  });
7794
+ if (key && key.length !== 32)
7795
+ throw new Error("root_binding_key_invalid");
7796
+ return key;
7797
+ }
7798
+ async function rootKey() {
7799
+ const existing = await storedRootKey();
7800
+ if (existing)
7801
+ return existing;
7802
+ await files.ensureParent(paths.rootRecord);
7803
+ return leased(
7804
+ paths.rootRecord,
7805
+ async () => {
7806
+ const stored = await storedRootKey();
7807
+ if (stored)
7808
+ return stored;
7809
+ const location = await locator("root-binding", paths.rootSecret);
7810
+ const key = randomBytes(32);
7811
+ await writeSecret(location, paths.rootSecret, key);
7812
+ await files.write(paths.rootRecord, json({ schemaVersion: 1, kind: "root-binding", ...location }), "record");
7813
+ return key;
7814
+ },
7815
+ // forget erased the binding and its directory while this call waited: create afresh.
7816
+ rootKey
7817
+ );
7641
7818
  }
7642
7819
  async function hmacRootBinding(version, input) {
7643
7820
  if (version !== 1)
@@ -7645,26 +7822,60 @@ function createCredentialAdapter(options = {}) {
7645
7822
  const key = await rootKey();
7646
7823
  return { version, hmac: createHmac("sha256", key).update(input).digest("base64url") };
7647
7824
  }
7825
+ async function eraseEntry(recordPath2, kind, secretPath) {
7826
+ const record = await removeStored(recordPath2, kind, secretPath);
7827
+ if (!record)
7828
+ await files.remove(secretPath);
7829
+ for (const path of [recordPath2, secretPath])
7830
+ for (const entry of await files.list(dirname5(path)))
7831
+ if (temporaryOf(basename3(path), entry))
7832
+ await files.remove(join6(dirname5(path), entry));
7833
+ return record;
7834
+ }
7835
+ async function forgetCliOsOrphan() {
7836
+ if (!store)
7837
+ return;
7838
+ const recordPath2 = paths.cliRecord;
7839
+ const ref = await reference("cli-oauth").catch(() => null);
7840
+ if (!ref || !await available())
7841
+ return;
7842
+ if (!await osBackend.read(ref, "").catch(() => null))
7843
+ return;
7844
+ await files.ensureParent(recordPath2);
7845
+ await leased(recordPath2, async () => {
7846
+ const record = await readRecord(recordPath2, "cli-oauth");
7847
+ if (record?.store === "os" && record.secretRef === ref)
7848
+ return;
7849
+ await osBackend.delete(ref, "");
7850
+ }, () => void 0);
7851
+ }
7852
+ async function forgetEntry(recordPath2, kind, secretPath) {
7853
+ let present = false;
7854
+ for (const path of [recordPath2, secretPath])
7855
+ for (const entry of await files.list(dirname5(path)))
7856
+ present ||= entry === basename3(path) || temporaryOf(basename3(path), entry);
7857
+ if (!present)
7858
+ return null;
7859
+ await files.ensureParent(recordPath2);
7860
+ return leased(recordPath2, () => eraseEntry(recordPath2, kind, secretPath), () => null);
7861
+ }
7648
7862
  async function forget() {
7649
- const cli = await readRecord(paths.cliRecord, "cli-oauth");
7650
7863
  const componentFamilyIds = [];
7651
- for (const name2 of await files.list(paths.familyRecords)) {
7652
- const recordPath2 = join6(paths.familyRecords, name2);
7653
- const record = await readRecord(recordPath2, "component");
7654
- if (!record)
7655
- continue;
7656
- componentFamilyIds.push(record.familyId);
7657
- await removeFamily(record.familyId);
7658
- }
7659
- if (cli) {
7660
- await deleteSecret(cli, paths.cliSecret);
7661
- await files.remove(paths.cliRecord);
7662
- }
7663
- const root = await readRecord(paths.rootRecord, "root-binding");
7664
- if (root) {
7665
- await deleteSecret(root, paths.rootSecret);
7666
- await files.remove(paths.rootRecord);
7667
- }
7864
+ const digests = /* @__PURE__ */ new Set();
7865
+ for (const directory of [paths.familyRecords, paths.familySecrets])
7866
+ for (const name2 of await files.list(directory)) {
7867
+ const digest2 = familyFileName.exec(name2)?.[1];
7868
+ if (digest2)
7869
+ digests.add(digest2);
7870
+ }
7871
+ for (const digest2 of [...digests].sort()) {
7872
+ const record = await forgetEntry(join6(paths.familyRecords, `${digest2}.json`), "component", join6(paths.familySecrets, `${digest2}.json`));
7873
+ if (record)
7874
+ componentFamilyIds.push(record.familyId);
7875
+ }
7876
+ const cli = await forgetEntry(paths.cliRecord, "cli-oauth", paths.cliSecret);
7877
+ await forgetCliOsOrphan();
7878
+ await forgetEntry(paths.rootRecord, "root-binding", paths.rootSecret);
7668
7879
  await files.removeEmptyDirectories([
7669
7880
  paths.familySecrets,
7670
7881
  paths.familyRecords,
@@ -7686,6 +7897,8 @@ function createCredentialAdapter(options = {}) {
7686
7897
  rotateCli,
7687
7898
  withCliCredential,
7688
7899
  removeCliOAuth,
7900
+ revokeCli,
7901
+ waitForCliLease,
7689
7902
  putFamily,
7690
7903
  readFamily,
7691
7904
  rotateFamily,
@@ -1,5 +1,16 @@
1
1
  import { type createCredentialAdapter, type CliOAuthCredential, type CredentialAdapterOptions } from '@mnemonik/credentials';
2
2
  export declare const CLI_SCOPES: readonly ['account:read', 'install:manage', 'components:manage', 'projects:manage', 'offline_access'];
3
+ /**
4
+ * Sign-out holds the CLI credential lease across its /oauth/revoke call, so the call is
5
+ * bounded: a hung issuer must not keep the lease (and a sign-in waiting for it) indefinitely.
6
+ */
7
+ export declare const CLI_REVOKE_TIMEOUT_MS = 10000;
8
+ /**
9
+ * Bound on one token refresh. The credential adapter retries a lost response
10
+ * once, so a refresh the server never answers ends as rotation_response_lost
11
+ * (retry) after about twice this.
12
+ */
13
+ export declare const CLI_REFRESH_TIMEOUT_MS = 5000;
3
14
  export interface CliAuthOptions {
4
15
  stateDir?: string;
5
16
  scannerRoots?: string;
@@ -21,6 +32,10 @@ export interface CliAuthOptions {
21
32
  now?: () => number;
22
33
  credentials?: ReturnType<typeof createCredentialAdapter>;
23
34
  credentialOptions?: CredentialAdapterOptions;
35
+ /** Bound on the sign-out revoke call; CLI_REVOKE_TIMEOUT_MS unless a test replaces it. */
36
+ revokeTimeoutMs?: number;
37
+ /** Bound on one token refresh; CLI_REFRESH_TIMEOUT_MS unless a test replaces it. */
38
+ refreshTimeoutMs?: number;
24
39
  }
25
40
  export declare function noBrowserAvailable(platform?: NodeJS.Platform, env?: NodeJS.ProcessEnv): boolean;
26
41
  export declare function createCliAuth(options?: CliAuthOptions): {