@mnemonik/cli 7.112.1 → 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 (47) 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 +308 -96
  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 +69 -13
  7. package/dist/auth/index.js.map +1 -1
  8. package/dist/diagnostics.js +312 -100
  9. package/dist/digests.json +45 -45
  10. package/dist/identity/migrate.js +37 -0
  11. package/dist/install/journal.js +37 -0
  12. package/dist/install/journey.js +37 -0
  13. package/dist/install/ownership.js +37 -0
  14. package/dist/install/transaction.d.ts +1 -3
  15. package/dist/install/transaction.d.ts.map +1 -1
  16. package/dist/install/transaction.js +64 -14
  17. package/dist/install/transaction.js.map +1 -1
  18. package/dist/install/ui.js +37 -0
  19. package/dist/installation.js +37 -0
  20. package/dist/launcher.js +37 -0
  21. package/dist/preflight.js +1 -1
  22. package/dist/project/eligibility.js +37 -0
  23. package/dist/project/records.js +37 -0
  24. package/dist/project.d.ts +11 -2
  25. package/dist/project.d.ts.map +1 -1
  26. package/dist/project.js +43 -2
  27. package/dist/project.js.map +1 -1
  28. package/dist/router.js +38 -1
  29. package/dist/runtime/releaseSource.js +37 -0
  30. package/dist/runtime/selfUpdate.js +37 -0
  31. package/dist/runtime/signers.js +242 -50
  32. package/dist/runtime/store.js +34 -0
  33. package/dist/runtime/updateCheck.js +37 -0
  34. package/dist/sbom.json +21 -21
  35. package/dist/scanner/control.js +37 -0
  36. package/dist/scanner/discover.js +1 -1
  37. package/dist/scanner/enable.js +37 -0
  38. package/dist/scanner/service.js +37 -0
  39. package/dist/scanner/update.js +37 -0
  40. package/dist/scanner-release.json +29 -29
  41. package/dist/status.js +41 -4
  42. package/dist/transport/server.d.ts +21 -2
  43. package/dist/transport/server.d.ts.map +1 -1
  44. package/dist/transport/server.js +15 -1
  45. package/dist/transport/server.js.map +1 -1
  46. package/dist/vendor/local-setup.js +37 -0
  47. 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,14 +2379,15 @@ var require_proper_lockfile = __commonJS({
2345
2379
  });
2346
2380
 
2347
2381
  // packages/cli/src/diagnostics.ts
2348
- import { createHash as createHash4 } from "node:crypto";
2382
+ import { createHash as createHash5 } from "node:crypto";
2349
2383
  import { execFile as nodeExecFile2 } from "node:child_process";
2350
2384
  import { lstat as lstat2, mkdir as mkdir3, readFile as readFile2 } from "node:fs/promises";
2351
- import { dirname as dirname5, join as join7 } from "node:path";
2385
+ import { dirname as dirname6, join as join7 } from "node:path";
2352
2386
 
2353
2387
  // packages/credentials/dist/index.js
2354
- import { createHmac, randomBytes } from "node:crypto";
2355
- import { join as join6 } from "node:path";
2388
+ import { createHash as createHash4, createHmac, randomBytes } from "node:crypto";
2389
+ import { realpath } from "node:fs/promises";
2390
+ import { basename as basename3, dirname as dirname5, join as join6 } from "node:path";
2356
2391
 
2357
2392
  // packages/shared/dist/instructions.js
2358
2393
  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.
@@ -6537,6 +6572,9 @@ var DEFAULT_OPTIONS = {
6537
6572
  };
6538
6573
  var BUILT_IN_IGNORE_DIR_SET = new Set(BUILT_IN_IGNORE_DIRS);
6539
6574
 
6575
+ // packages/shared/dist/index.js
6576
+ init_hookTimeouts();
6577
+
6540
6578
  // packages/shared/dist/jitDeliveryAck.js
6541
6579
  init_runtimeSigners();
6542
6580
 
@@ -6821,16 +6859,17 @@ var CredentialSessionUnavailableError = class extends Error {
6821
6859
  // packages/credentials/dist/storage.js
6822
6860
  import { createHash as createHash3 } from "node:crypto";
6823
6861
  import { lstat as nodeLstat, mkdir as mkdir2, open as open2, readdir, rmdir as rmdir2, unlink as unlink2 } from "node:fs/promises";
6824
- import { constants } from "node:fs";
6862
+ import { constants, realpathSync as realpathSync2 } from "node:fs";
6825
6863
  import { basename as basename2, dirname as dirname4, isAbsolute, join as join5, parse, relative, resolve as resolve2, sep as sep2 } from "node:path";
6826
6864
  var CredentialError = class extends Error {
6827
6865
  reason;
6828
- constructor(reason) {
6829
- super(reason);
6866
+ constructor(reason, message = reason) {
6867
+ super(message);
6830
6868
  this.reason = reason;
6831
6869
  this.name = "CredentialError";
6832
6870
  }
6833
6871
  };
6872
+ 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.";
6834
6873
  var digest = (value) => createHash3("sha256").update(value).digest("hex");
6835
6874
  function credentialPaths(stateDir = stateDirectory(), familyId) {
6836
6875
  const root = join5(stateDir, "credentials");
@@ -6850,8 +6889,25 @@ function credentialPaths(stateDir = stateDirectory(), familyId) {
6850
6889
  };
6851
6890
  }
6852
6891
  var codeIs2 = (error, code) => error.code === code;
6892
+ function canonicalDirectory(path) {
6893
+ const missing = [];
6894
+ let current = resolve2(path);
6895
+ for (; ; ) {
6896
+ try {
6897
+ return join5(realpathSync2.native(current), ...missing.reverse());
6898
+ } catch (error) {
6899
+ const parent = dirname4(current);
6900
+ if (!codeIs2(error, "ENOENT") || parent === current)
6901
+ return resolve2(path);
6902
+ missing.push(basename2(current));
6903
+ current = parent;
6904
+ }
6905
+ }
6906
+ }
6853
6907
  var SecureFiles = class {
6854
6908
  stateDir;
6909
+ /** The root as configured; paths spelled through it are rebased onto `stateDir`. */
6910
+ configuredStateDir;
6855
6911
  platform;
6856
6912
  lstat;
6857
6913
  uid;
@@ -6860,7 +6916,8 @@ var SecureFiles = class {
6860
6916
  username;
6861
6917
  aclRun;
6862
6918
  constructor(options = {}) {
6863
- this.stateDir = resolve2(options.stateDir ?? stateDirectory());
6919
+ this.configuredStateDir = resolve2(options.stateDir ?? stateDirectory());
6920
+ this.stateDir = join5(canonicalDirectory(dirname4(this.configuredStateDir)), basename2(this.configuredStateDir));
6864
6921
  this.platform = options.platform ?? process.platform;
6865
6922
  this.lstat = options.lstat ?? nodeLstat;
6866
6923
  this.uid = options.uid ?? process.getuid?.();
@@ -6870,7 +6927,10 @@ var SecureFiles = class {
6870
6927
  this.aclRun = options.aclRun;
6871
6928
  }
6872
6929
  assertInsideState(path) {
6873
- const absolute = resolve2(path);
6930
+ let absolute = resolve2(path);
6931
+ const fromConfigured = relative(this.configuredStateDir, absolute);
6932
+ if (this.configuredStateDir !== this.stateDir && fromConfigured !== ".." && !fromConfigured.startsWith(`..${sep2}`) && !isAbsolute(fromConfigured))
6933
+ absolute = join5(this.stateDir, fromConfigured);
6874
6934
  const fromState = relative(this.stateDir, absolute);
6875
6935
  if (fromState === ".." || fromState.startsWith(`..${sep2}`) || isAbsolute(fromState))
6876
6936
  throw new CredentialError("path_outside_state");
@@ -6891,6 +6951,8 @@ var SecureFiles = class {
6891
6951
  async inspect(path, expectFile, allowMissing) {
6892
6952
  const absolute = this.assertInsideState(path);
6893
6953
  for (const component of this.components(absolute)) {
6954
+ if (component !== this.stateDir && !component.startsWith(`${this.stateDir}${sep2}`))
6955
+ continue;
6894
6956
  let value;
6895
6957
  try {
6896
6958
  value = await this.lstat(component);
@@ -6900,10 +6962,7 @@ var SecureFiles = class {
6900
6962
  throw error;
6901
6963
  }
6902
6964
  if (value.isSymbolicLink())
6903
- throw new CredentialError("symlink_rejected");
6904
- const protectedComponent = component === this.stateDir || component.startsWith(`${this.stateDir}${sep2}`);
6905
- if (!protectedComponent)
6906
- continue;
6965
+ throw new CredentialError("symlink_rejected", component === this.stateDir ? STATE_ROOT_LINK_MESSAGE : void 0);
6907
6966
  const final = component === absolute;
6908
6967
  if (this.platform === "win32") {
6909
6968
  if (final)
@@ -7106,6 +7165,7 @@ var action = (familyId, reason) => ({
7106
7165
  reason,
7107
7166
  familyId
7108
7167
  });
7168
+ var isSecretMissing = (error) => error instanceof Error && error.message === "credential_secret_missing";
7109
7169
  var retryLater = (familyId, reason) => ({
7110
7170
  status: "RETRY_LATER",
7111
7171
  reason,
@@ -7113,12 +7173,15 @@ var retryLater = (familyId, reason) => ({
7113
7173
  });
7114
7174
  var isFailure = (result) => "status" in result && (result.status === "ACTION_REQUIRED" || result.status === "RETRY_LATER");
7115
7175
  var diagnosedStores = /* @__PURE__ */ new WeakSet();
7176
+ var familyFileName = /^([0-9a-f]{64})\.json(?:\.[0-9a-f-]{36}\.tmp)?$/;
7177
+ var temporaryOf = (base, name2) => name2.startsWith(`${base}.`) && /^\.[0-9a-f-]{36}\.tmp$/.test(name2.slice(base.length));
7116
7178
  function createCredentialAdapter(options = {}) {
7117
7179
  const files = new SecureFiles({ ...options, stateDir: options.stateDir ?? stateDirectory() });
7118
7180
  const paths = credentialPaths(files.stateDir);
7119
7181
  const now = options.now ?? Date.now;
7120
7182
  const sleep = options.sleep ?? wait;
7121
7183
  const retryLimit = options.retryLimit ?? 2;
7184
+ const lockWaitMs = options.lockWaitMs ?? 6e4;
7122
7185
  const store = options.secretStore;
7123
7186
  const rotations = /* @__PURE__ */ new Map();
7124
7187
  let osAvailable;
@@ -7134,7 +7197,10 @@ function createCredentialAdapter(options = {}) {
7134
7197
  disable();
7135
7198
  return ok;
7136
7199
  }) : Promise.resolve(false);
7137
- const reference = (name2) => `mnemonik.credentials.v1:${name2}`;
7200
+ const reference = async (name2) => {
7201
+ const directory = createHash4("sha256").update(await realpath(files.stateDir));
7202
+ return `mnemonik.credentials.v1:${name2}:${directory.digest("hex").slice(0, 32)}`;
7203
+ };
7138
7204
  const relativeSecret = (path) => path.slice(files.stateDir.length + 1);
7139
7205
  const fileBackend = {
7140
7206
  store: "file",
@@ -7166,7 +7232,7 @@ function createCredentialAdapter(options = {}) {
7166
7232
  async function locator(name2, file, existing) {
7167
7233
  if (existing?.store === "os")
7168
7234
  return existing;
7169
- return name2 === "cli-oauth" && await osBackend.isAvailable() ? { store: "os", secretRef: reference(name2) } : { store: "file", secretFile: relativeSecret(file) };
7235
+ return name2 === "cli-oauth" && await osBackend.isAvailable() ? { store: "os", secretRef: await reference(name2) } : { store: "file", secretFile: relativeSecret(file) };
7170
7236
  }
7171
7237
  const useFile = (location, file) => {
7172
7238
  Object.assign(location, { store: "file", secretFile: relativeSecret(file) });
@@ -7220,6 +7286,14 @@ function createCredentialAdapter(options = {}) {
7220
7286
  return bytes ? parse2(bytes, kind) : null;
7221
7287
  }
7222
7288
  async function putCliOAuth(metadata2, tokens) {
7289
+ await files.ensureParent(paths.cliRecord);
7290
+ return leased(paths.cliRecord, () => writeCliOAuth(metadata2, tokens), () => putCliOAuth(metadata2, tokens));
7291
+ }
7292
+ async function waitForCliLease() {
7293
+ await files.ensureParent(paths.cliRecord);
7294
+ await leased(paths.cliRecord, async () => void 0, () => void 0);
7295
+ }
7296
+ async function writeCliOAuth(metadata2, tokens) {
7223
7297
  const current = await readRecord(paths.cliRecord, "cli-oauth");
7224
7298
  const location = await locator("cli-oauth", paths.cliSecret, current ?? void 0);
7225
7299
  const secret = typeof tokens === "string" ? { accessToken: "", refreshToken: tokens, accessExpiresAt: (/* @__PURE__ */ new Date(0)).toISOString() } : tokens;
@@ -7286,8 +7360,11 @@ function createCredentialAdapter(options = {}) {
7286
7360
  }
7287
7361
  async function putFamily(componentKind, response) {
7288
7362
  const familyPaths = credentialPaths(files.stateDir, response.id);
7289
- const current = await readRecord(familyPaths.record, "component");
7290
- return persistFamily(componentKind, response, current ?? void 0);
7363
+ await files.ensureParent(familyPaths.record);
7364
+ return leased(familyPaths.record, async () => {
7365
+ const current = await readRecord(familyPaths.record, "component");
7366
+ return persistFamily(componentKind, response, current ?? void 0);
7367
+ }, () => putFamily(componentKind, response));
7291
7368
  }
7292
7369
  async function readFamily(familyId) {
7293
7370
  const familyPaths = credentialPaths(files.stateDir, familyId);
@@ -7307,49 +7384,73 @@ function createCredentialAdapter(options = {}) {
7307
7384
  void _file;
7308
7385
  return { ...publicMetadata, ...secret };
7309
7386
  }
7310
- async function requestRotation(familyId, request) {
7311
- const started = now();
7312
- let lostResponseRetried = false;
7313
- let retryableAttempt = 0;
7387
+ async function requestRotation(familyId, request, state) {
7314
7388
  while (true) {
7315
7389
  let response;
7316
7390
  try {
7317
7391
  response = await request();
7318
7392
  } catch {
7319
- if (lostResponseRetried)
7393
+ if (state.lostResponseRetried)
7320
7394
  return action(familyId, "rotation_response_lost");
7321
- lostResponseRetried = true;
7395
+ state.lostResponseRetried = true;
7322
7396
  await sleep(250);
7323
- if (now() > started + 6e4)
7397
+ if (now() > state.started + 6e4)
7324
7398
  return action(familyId, "rotation_response_lost");
7325
7399
  continue;
7326
7400
  }
7327
7401
  if (response.status === 400 && response.body.error === "invalid_grant")
7328
7402
  return action(familyId, "invalid_grant");
7329
7403
  if (response.status === 429 || response.status >= 500) {
7330
- if (lostResponseRetried)
7404
+ if (state.lostResponseRetried)
7331
7405
  return action(familyId, "rotation_response_lost");
7332
- if (retryableAttempt >= retryLimit)
7406
+ if (state.retryableAttempt >= retryLimit)
7333
7407
  return retryLater(familyId, response.status === 429 ? "rate_limited" : "server_error");
7334
- const delay2 = response.retryAfterMs ?? 250 * 2 ** retryableAttempt;
7335
- retryableAttempt += 1;
7336
- await sleep(delay2);
7337
- continue;
7408
+ const delay2 = response.retryAfterMs ?? 250 * 2 ** state.retryableAttempt;
7409
+ state.retryableAttempt += 1;
7410
+ return { backoffMs: delay2 };
7338
7411
  }
7339
7412
  return response;
7340
7413
  }
7341
7414
  }
7415
+ async function leased(recordPath2, work, vanished) {
7416
+ let entered = false;
7417
+ try {
7418
+ return await withLock(recordPath2, lockWaitMs, () => {
7419
+ entered = true;
7420
+ return work();
7421
+ });
7422
+ } catch (error) {
7423
+ if (entered || error.code !== "ENOENT")
7424
+ throw error;
7425
+ return vanished();
7426
+ }
7427
+ }
7342
7428
  async function rotateStored(lockPath, missingId, read, request, decode, persist) {
7343
7429
  const predecessor = await read();
7344
7430
  if (!predecessor)
7345
7431
  return action(missingId, "family_missing");
7346
- return withLock(lockPath, options.lockWaitMs ?? 6e4, async () => {
7432
+ const attempts = {
7433
+ started: now(),
7434
+ lostResponseRetried: false,
7435
+ retryableAttempt: 0
7436
+ };
7437
+ for (; ; ) {
7438
+ const outcome = await rotateStoredOnce(lockPath, predecessor, read, request, decode, persist, attempts);
7439
+ if (!("backoffMs" in outcome))
7440
+ return outcome;
7441
+ await sleep(outcome.backoffMs);
7442
+ }
7443
+ }
7444
+ async function rotateStoredOnce(lockPath, predecessor, read, request, decode, persist, attempts) {
7445
+ return leased(lockPath, async () => {
7347
7446
  const current = await read();
7348
7447
  if (!current)
7349
7448
  return action(predecessor.familyId, "family_missing");
7350
7449
  if (current.refreshToken !== predecessor.refreshToken)
7351
7450
  return current;
7352
- const response = await requestRotation(current.familyId, () => request(current));
7451
+ const response = await requestRotation(current.familyId, () => request(current), attempts);
7452
+ if ("backoffMs" in response)
7453
+ return response;
7353
7454
  if ("status" in response && (response.status === "ACTION_REQUIRED" || response.status === "RETRY_LATER"))
7354
7455
  return response;
7355
7456
  if (response.status !== 200)
@@ -7362,21 +7463,19 @@ function createCredentialAdapter(options = {}) {
7362
7463
  } catch {
7363
7464
  return action(current.familyId, "persistence_failed");
7364
7465
  }
7365
- });
7466
+ }, () => action(predecessor.familyId, "family_missing"));
7366
7467
  }
7367
7468
  function rotateLocked(familyId, transport) {
7368
7469
  const familyPaths = credentialPaths(files.stateDir, familyId);
7369
- return rotateStored(familyPaths.record, familyId, () => readFamily(familyId), (current) => transport.rotateFamily(familyId, current.refreshToken), (body2) => "access_token" in body2 ? body2 : null, (current, body2) => persistFamily(current.componentKind, body2, {
7370
- schemaVersion: 1,
7371
- kind: "component",
7372
- familyId: current.familyId,
7373
- componentKind: current.componentKind,
7374
- scopes: current.scopes,
7375
- accessExpiresAt: current.accessExpiresAt,
7376
- refreshExpiresAt: current.refreshExpiresAt,
7377
- lastRotationTime: current.lastRotationTime,
7378
- ...current.store === "os" ? { store: "os", secretRef: reference(`component:${familyId}`) } : { store: "file", secretFile: relativeSecret(familyPaths.secret) }
7379
- }));
7470
+ return rotateStored(
7471
+ familyPaths.record,
7472
+ familyId,
7473
+ () => readFamily(familyId),
7474
+ (current) => transport.rotateFamily(familyId, current.refreshToken),
7475
+ (body2) => "access_token" in body2 ? body2 : null,
7476
+ // The stored record's own locator: a reference rebuilt from the name could differ.
7477
+ async (current, body2) => persistFamily(current.componentKind, body2, await readRecord(familyPaths.record, "component") ?? void 0)
7478
+ );
7380
7479
  }
7381
7480
  function rotateFamily(familyId, transport) {
7382
7481
  const active = rotations.get(familyId);
@@ -7394,7 +7493,7 @@ function createCredentialAdapter(options = {}) {
7394
7493
  function rotateCliLocked(transport) {
7395
7494
  return rotateStored(paths.cliRecord, "cli", readCliOAuth, (current) => transport.rotateCli(current), (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 (current, body2) => {
7396
7495
  const rotatedAt = now();
7397
- await putCliOAuth({
7496
+ await writeCliOAuth({
7398
7497
  issuer: current.issuer,
7399
7498
  clientId: current.clientId,
7400
7499
  familyId: current.familyId,
@@ -7445,11 +7544,33 @@ function createCredentialAdapter(options = {}) {
7445
7544
  return second.status === 401 ? action(rotated.familyId, "protected_unauthorized") : second;
7446
7545
  }
7447
7546
  async function removeCliOAuth() {
7448
- const record = await readRecord(paths.cliRecord, "cli-oauth");
7449
- if (!record)
7450
- return;
7451
- await deleteSecret(record, paths.cliSecret);
7452
- await files.remove(paths.cliRecord);
7547
+ await removeLeased(paths.cliRecord, "cli-oauth", paths.cliSecret);
7548
+ }
7549
+ async function revokeCli(transport) {
7550
+ if (!await readRecord(paths.cliRecord, "cli-oauth"))
7551
+ return action("cli", "family_missing");
7552
+ return leased(paths.cliRecord, async () => {
7553
+ let current;
7554
+ try {
7555
+ current = await readCliOAuth();
7556
+ } catch (error) {
7557
+ if (!isSecretMissing(error))
7558
+ throw error;
7559
+ await removeStored(paths.cliRecord, "cli-oauth", paths.cliSecret);
7560
+ return action("cli", "credential_secret_missing");
7561
+ }
7562
+ if (!current)
7563
+ return action("cli", "family_missing");
7564
+ const response = await transport.revokeCli(current);
7565
+ if (response.status === 429)
7566
+ return retryLater(current.familyId, "rate_limited");
7567
+ if (response.status >= 500)
7568
+ return retryLater(current.familyId, "server_error");
7569
+ if (response.status !== 200)
7570
+ return action(current.familyId, "error" in response.body ? response.body.error : "revoke_refused");
7571
+ await removeStored(paths.cliRecord, "cli-oauth", paths.cliSecret);
7572
+ return { status: "revoked", familyId: current.familyId };
7573
+ }, () => action("cli", "family_missing"));
7453
7574
  }
7454
7575
  async function withCredential(familyId, transport, work) {
7455
7576
  let current = await readFamily(familyId);
@@ -7470,43 +7591,98 @@ function createCredentialAdapter(options = {}) {
7470
7591
  const second = await work(rotated.accessToken);
7471
7592
  return second.status === 401 ? action(familyId, "protected_unauthorized") : second;
7472
7593
  }
7594
+ async function removeStored(recordPath2, kind, secretPath) {
7595
+ const record = await readRecord(recordPath2, kind);
7596
+ if (!record)
7597
+ return null;
7598
+ await files.remove(recordPath2);
7599
+ await deleteSecret(record, secretPath);
7600
+ return record;
7601
+ }
7602
+ async function removeLeased(recordPath2, kind, secretPath) {
7603
+ if (!await readRecord(recordPath2, kind))
7604
+ return null;
7605
+ return leased(recordPath2, () => removeStored(recordPath2, kind, secretPath), () => null);
7606
+ }
7607
+ async function orphanRefreshToken(secretPath) {
7608
+ const bytes = await files.read(secretPath);
7609
+ if (!bytes)
7610
+ return null;
7611
+ const secret = JSON.parse(bytes.toString());
7612
+ if (typeof secret.refreshToken !== "string")
7613
+ throw new Error("credential_record_invalid");
7614
+ return secret.refreshToken;
7615
+ }
7473
7616
  async function removeFamily(familyId) {
7474
7617
  const familyPaths = credentialPaths(files.stateDir, familyId);
7475
- const record = await readRecord(familyPaths.record, "component");
7476
- if (!record)
7477
- return;
7478
- await deleteSecret(record, familyPaths.secret);
7479
- await files.remove(familyPaths.record);
7618
+ await removeStored(familyPaths.record, "component", familyPaths.secret);
7480
7619
  }
7481
7620
  async function revokeFamily(familyId, transport) {
7482
- const current = await readFamily(familyId);
7483
- if (!current)
7621
+ const familyPaths = credentialPaths(files.stateDir, familyId);
7622
+ if (!await readRecord(familyPaths.record, "component") && !await files.read(familyPaths.secret))
7484
7623
  return action(familyId, "family_missing");
7485
- const response = await transport.revokeFamily(familyId, current.refreshToken);
7486
- if (response.status === 429)
7487
- return retryLater(familyId, "rate_limited");
7488
- if (response.status >= 500)
7489
- return retryLater(familyId, "server_error");
7490
- if (response.status !== 200)
7491
- return action(familyId, "error" in response.body ? response.body.error : "revoke_refused");
7492
- await removeFamily(familyId);
7493
- return { status: "revoked", familyId };
7624
+ await files.ensureParent(familyPaths.record);
7625
+ return leased(familyPaths.record, async () => {
7626
+ let current;
7627
+ try {
7628
+ current = await readFamily(familyId);
7629
+ } catch (error) {
7630
+ if (!isSecretMissing(error))
7631
+ throw error;
7632
+ await removeFamily(familyId);
7633
+ return action(familyId, "credential_secret_missing");
7634
+ }
7635
+ const refreshToken = current?.refreshToken ?? await orphanRefreshToken(familyPaths.secret);
7636
+ if (!refreshToken)
7637
+ return action(familyId, "family_missing");
7638
+ const response = await transport.revokeFamily(familyId, refreshToken);
7639
+ if (response.status === 429)
7640
+ return retryLater(familyId, "rate_limited");
7641
+ if (response.status >= 500)
7642
+ return retryLater(familyId, "server_error");
7643
+ const refusal = response.status === 200 ? null : "error" in response.body ? response.body.error : "revoke_refused";
7644
+ if (refusal)
7645
+ return action(familyId, refusal);
7646
+ if (current)
7647
+ await removeFamily(familyId);
7648
+ else
7649
+ await files.remove(familyPaths.secret);
7650
+ return { status: "revoked", familyId };
7651
+ }, () => action(familyId, "family_missing"));
7494
7652
  }
7495
- async function rootKey() {
7496
- return withLock(paths.rootRecord, options.lockWaitMs ?? 6e4, async () => {
7497
- const record = await readRecord(paths.rootRecord, "root-binding");
7498
- if (record) {
7499
- const key2 = await readSecret(record, paths.rootSecret);
7500
- if (!key2 || key2.length !== 32)
7501
- throw new Error("root_binding_key_invalid");
7502
- return key2;
7503
- }
7504
- const location = await locator("root-binding", paths.rootSecret);
7505
- const key = randomBytes(32);
7506
- await writeSecret(location, paths.rootSecret, key);
7507
- await files.write(paths.rootRecord, json({ schemaVersion: 1, kind: "root-binding", ...location }), "record");
7508
- return key;
7653
+ async function storedRootKey() {
7654
+ const record = await readRecord(paths.rootRecord, "root-binding");
7655
+ if (!record)
7656
+ return null;
7657
+ const key = await readSecret(record, paths.rootSecret).catch((error) => {
7658
+ if (isSecretMissing(error))
7659
+ return null;
7660
+ throw error;
7509
7661
  });
7662
+ if (key && key.length !== 32)
7663
+ throw new Error("root_binding_key_invalid");
7664
+ return key;
7665
+ }
7666
+ async function rootKey() {
7667
+ const existing = await storedRootKey();
7668
+ if (existing)
7669
+ return existing;
7670
+ await files.ensureParent(paths.rootRecord);
7671
+ return leased(
7672
+ paths.rootRecord,
7673
+ async () => {
7674
+ const stored = await storedRootKey();
7675
+ if (stored)
7676
+ return stored;
7677
+ const location = await locator("root-binding", paths.rootSecret);
7678
+ const key = randomBytes(32);
7679
+ await writeSecret(location, paths.rootSecret, key);
7680
+ await files.write(paths.rootRecord, json({ schemaVersion: 1, kind: "root-binding", ...location }), "record");
7681
+ return key;
7682
+ },
7683
+ // forget erased the binding and its directory while this call waited: create afresh.
7684
+ rootKey
7685
+ );
7510
7686
  }
7511
7687
  async function hmacRootBinding(version, input) {
7512
7688
  if (version !== 1)
@@ -7514,26 +7690,60 @@ function createCredentialAdapter(options = {}) {
7514
7690
  const key = await rootKey();
7515
7691
  return { version, hmac: createHmac("sha256", key).update(input).digest("base64url") };
7516
7692
  }
7693
+ async function eraseEntry(recordPath2, kind, secretPath) {
7694
+ const record = await removeStored(recordPath2, kind, secretPath);
7695
+ if (!record)
7696
+ await files.remove(secretPath);
7697
+ for (const path of [recordPath2, secretPath])
7698
+ for (const entry of await files.list(dirname5(path)))
7699
+ if (temporaryOf(basename3(path), entry))
7700
+ await files.remove(join6(dirname5(path), entry));
7701
+ return record;
7702
+ }
7703
+ async function forgetCliOsOrphan() {
7704
+ if (!store)
7705
+ return;
7706
+ const recordPath2 = paths.cliRecord;
7707
+ const ref = await reference("cli-oauth").catch(() => null);
7708
+ if (!ref || !await available())
7709
+ return;
7710
+ if (!await osBackend.read(ref, "").catch(() => null))
7711
+ return;
7712
+ await files.ensureParent(recordPath2);
7713
+ await leased(recordPath2, async () => {
7714
+ const record = await readRecord(recordPath2, "cli-oauth");
7715
+ if (record?.store === "os" && record.secretRef === ref)
7716
+ return;
7717
+ await osBackend.delete(ref, "");
7718
+ }, () => void 0);
7719
+ }
7720
+ async function forgetEntry(recordPath2, kind, secretPath) {
7721
+ let present = false;
7722
+ for (const path of [recordPath2, secretPath])
7723
+ for (const entry of await files.list(dirname5(path)))
7724
+ present ||= entry === basename3(path) || temporaryOf(basename3(path), entry);
7725
+ if (!present)
7726
+ return null;
7727
+ await files.ensureParent(recordPath2);
7728
+ return leased(recordPath2, () => eraseEntry(recordPath2, kind, secretPath), () => null);
7729
+ }
7517
7730
  async function forget() {
7518
- const cli = await readRecord(paths.cliRecord, "cli-oauth");
7519
7731
  const componentFamilyIds = [];
7520
- for (const name2 of await files.list(paths.familyRecords)) {
7521
- const recordPath2 = join6(paths.familyRecords, name2);
7522
- const record = await readRecord(recordPath2, "component");
7523
- if (!record)
7524
- continue;
7525
- componentFamilyIds.push(record.familyId);
7526
- await removeFamily(record.familyId);
7527
- }
7528
- if (cli) {
7529
- await deleteSecret(cli, paths.cliSecret);
7530
- await files.remove(paths.cliRecord);
7531
- }
7532
- const root = await readRecord(paths.rootRecord, "root-binding");
7533
- if (root) {
7534
- await deleteSecret(root, paths.rootSecret);
7535
- await files.remove(paths.rootRecord);
7536
- }
7732
+ const digests = /* @__PURE__ */ new Set();
7733
+ for (const directory of [paths.familyRecords, paths.familySecrets])
7734
+ for (const name2 of await files.list(directory)) {
7735
+ const digest3 = familyFileName.exec(name2)?.[1];
7736
+ if (digest3)
7737
+ digests.add(digest3);
7738
+ }
7739
+ for (const digest3 of [...digests].sort()) {
7740
+ const record = await forgetEntry(join6(paths.familyRecords, `${digest3}.json`), "component", join6(paths.familySecrets, `${digest3}.json`));
7741
+ if (record)
7742
+ componentFamilyIds.push(record.familyId);
7743
+ }
7744
+ const cli = await forgetEntry(paths.cliRecord, "cli-oauth", paths.cliSecret);
7745
+ await forgetCliOsOrphan();
7746
+ await forgetEntry(paths.rootRecord, "root-binding", paths.rootSecret);
7537
7747
  await files.removeEmptyDirectories([
7538
7748
  paths.familySecrets,
7539
7749
  paths.familyRecords,
@@ -7555,6 +7765,8 @@ function createCredentialAdapter(options = {}) {
7555
7765
  rotateCli,
7556
7766
  withCliCredential,
7557
7767
  removeCliOAuth,
7768
+ revokeCli,
7769
+ waitForCliLease,
7558
7770
  putFamily,
7559
7771
  readFamily,
7560
7772
  rotateFamily,
@@ -7575,7 +7787,7 @@ var DiagnosticsError = class extends Error {
7575
7787
  }
7576
7788
  code;
7577
7789
  };
7578
- var digest2 = (bytes) => createHash4("sha256").update(bytes).digest("hex");
7790
+ var digest2 = (bytes) => createHash5("sha256").update(bytes).digest("hex");
7579
7791
  var validId = (id) => /^diag-[A-Za-z0-9._-]{1,120}$/u.test(id);
7580
7792
  async function scannerConfig(stateDir) {
7581
7793
  const state = JSON.parse(await readFile2(join7(stateDir, "scanner", "state.json"), "utf8"));
@@ -7628,7 +7840,7 @@ async function previewDiagnostics(out2, dependencies = {}) {
7628
7840
  throw new DiagnosticsError("diagnostics_preview_invalid");
7629
7841
  const receipt = { schemaVersion: 1, ...result };
7630
7842
  const receiptPath = join7(stateDir, "diagnostics", `${result.manifest.bundleId}.json`);
7631
- await mkdir3(dirname5(receiptPath), { recursive: true, mode: 448 });
7843
+ await mkdir3(dirname6(receiptPath), { recursive: true, mode: 448 });
7632
7844
  await atomicWrite(receiptPath, Buffer.from(`${JSON.stringify(receipt)}
7633
7845
  `));
7634
7846
  return result;