@tinycloud/cli 1.2.6-beta.4 → 1.3.0-beta.6

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.
@@ -170,15 +170,15 @@ function closeReplication(schedule = scheduleTimeout) {
170
170
  removeSignalHandlers();
171
171
  let resolveClosing;
172
172
  let rejectClosing;
173
- const closing = new Promise((resolve5, reject) => {
174
- resolveClosing = resolve5;
173
+ const closing = new Promise((resolve6, reject) => {
174
+ resolveClosing = resolve6;
175
175
  rejectClosing = reject;
176
176
  });
177
177
  state.closing = closing;
178
178
  void (async () => {
179
179
  let timeout;
180
- const deadline = new Promise((resolve5) => {
181
- timeout = schedule(() => resolve5(true), CLOSE_TIMEOUT_MS);
180
+ const deadline = new Promise((resolve6) => {
181
+ timeout = schedule(() => resolve6(true), CLOSE_TIMEOUT_MS);
182
182
  });
183
183
  let timedOut = false;
184
184
  const abandonQueuedControls = () => {
@@ -204,7 +204,7 @@ function closeReplication(schedule = scheduleTimeout) {
204
204
  if (state.generation === generation && state.allControls.size === 0) break;
205
205
  }
206
206
  if (timedOut) break;
207
- const turn = new Promise((resolve5) => setImmediate(() => resolve5(false)));
207
+ const turn = new Promise((resolve6) => setImmediate(() => resolve6(false)));
208
208
  timedOut ||= await Promise.race([turn, deadline]);
209
209
  if (timedOut) abandonQueuedControls();
210
210
  else if (state.allControls.size === 0) break;
@@ -529,7 +529,7 @@ var init_storage2 = __esm({
529
529
  // src/output/errors.ts
530
530
  import { readFileSync, readdirSync } from "fs";
531
531
  import { join as join2 } from "path";
532
- import { ProfileDeletedError, ProfileLockTimeoutError } from "@tinycloud/operations/state";
532
+ import { ProfileDeletedError, ProfileLockTimeoutError, ProfileSymlinkError } from "@tinycloud/operations/state";
533
533
  import { isSafeRecovery } from "@tinycloud/operations/recovery";
534
534
  import { CLI_NAME as CLI_NAME2 } from "@tinycloud/operations/cli-name";
535
535
  import {
@@ -590,6 +590,9 @@ function wrapError(error) {
590
590
  if (error instanceof ProfileDeletedError) {
591
591
  return new CLIError("PROFILE_NOT_FOUND", message);
592
592
  }
593
+ if (error instanceof ProfileSymlinkError) {
594
+ return new CLIError("PROFILE_SYMLINK", message, ExitCode.ERROR);
595
+ }
593
596
  if (verdict === void 0 && (message.includes("Not signed in") || message.includes("AUTH_EXPIRED") || message.includes("Session expired"))) {
594
597
  return new CLIError("AUTH_REQUIRED", message, ExitCode.AUTH_REQUIRED);
595
598
  }
@@ -7094,7 +7097,7 @@ async function awaitOwned(signal, operation) {
7094
7097
  throw SUPERSEDED;
7095
7098
  }
7096
7099
  const promise = typeof operation === "function" ? operation() : operation;
7097
- return new Promise((resolve5, reject) => {
7100
+ return new Promise((resolve6, reject) => {
7098
7101
  let settled = false;
7099
7102
  const cleanup = () => signal.removeEventListener("abort", onAbort);
7100
7103
  const onAbort = () => {
@@ -7112,7 +7115,7 @@ async function awaitOwned(signal, operation) {
7112
7115
  }
7113
7116
  settled = true;
7114
7117
  cleanup();
7115
- resolve5(value);
7118
+ resolve6(value);
7116
7119
  },
7117
7120
  (error) => {
7118
7121
  if (settled) return;
@@ -7165,11 +7168,11 @@ function readProperty(value, key) {
7165
7168
  }
7166
7169
  }
7167
7170
  function sleep(delayMs, _attempt, signal) {
7168
- return new Promise((resolve5) => {
7171
+ return new Promise((resolve6) => {
7169
7172
  const finish = () => {
7170
7173
  clearTimeout(timer);
7171
7174
  signal.removeEventListener("abort", stop);
7172
- resolve5();
7175
+ resolve6();
7173
7176
  };
7174
7177
  const timer = setTimeout(finish, delayMs);
7175
7178
  timer.unref?.();
@@ -7182,13 +7185,13 @@ function sleep(delayMs, _attempt, signal) {
7182
7185
  });
7183
7186
  }
7184
7187
  function abortPromise(signal) {
7185
- let resolve5;
7188
+ let resolve6;
7186
7189
  const promise = new Promise((done) => {
7187
- resolve5 = done;
7190
+ resolve6 = done;
7188
7191
  });
7189
- const onAbort = () => resolve5();
7192
+ const onAbort = () => resolve6();
7190
7193
  if (signal.aborted) {
7191
- resolve5();
7194
+ resolve6();
7192
7195
  } else {
7193
7196
  signal.addEventListener("abort", onAbort, { once: true });
7194
7197
  }
@@ -7491,7 +7494,7 @@ function isBrowser() {
7491
7494
  }
7492
7495
  }
7493
7496
  function openDB() {
7494
- return new Promise((resolve5, reject) => {
7497
+ return new Promise((resolve6, reject) => {
7495
7498
  const request = indexedDB.open(DB_NAME, DB_VERSION);
7496
7499
  request.onupgradeneeded = () => {
7497
7500
  const db = request.result;
@@ -7499,43 +7502,43 @@ function openDB() {
7499
7502
  db.createObjectStore(STORE_NAME);
7500
7503
  }
7501
7504
  };
7502
- request.onsuccess = () => resolve5(request.result);
7505
+ request.onsuccess = () => resolve6(request.result);
7503
7506
  request.onerror = () => reject(request.error);
7504
7507
  });
7505
7508
  }
7506
7509
  function idbGet(db, key) {
7507
- return new Promise((resolve5, reject) => {
7510
+ return new Promise((resolve6, reject) => {
7508
7511
  const tx = db.transaction(STORE_NAME, "readonly");
7509
7512
  const store = tx.objectStore(STORE_NAME);
7510
7513
  const req = store.get(key);
7511
- req.onsuccess = () => resolve5(req.result);
7514
+ req.onsuccess = () => resolve6(req.result);
7512
7515
  req.onerror = () => reject(req.error);
7513
7516
  });
7514
7517
  }
7515
7518
  function idbPut(db, key, value) {
7516
- return new Promise((resolve5, reject) => {
7519
+ return new Promise((resolve6, reject) => {
7517
7520
  const tx = db.transaction(STORE_NAME, "readwrite");
7518
7521
  const store = tx.objectStore(STORE_NAME);
7519
7522
  const req = store.put(value, key);
7520
- req.onsuccess = () => resolve5();
7523
+ req.onsuccess = () => resolve6();
7521
7524
  req.onerror = () => reject(req.error);
7522
7525
  });
7523
7526
  }
7524
7527
  function idbDelete(db, key) {
7525
- return new Promise((resolve5, reject) => {
7528
+ return new Promise((resolve6, reject) => {
7526
7529
  const tx = db.transaction(STORE_NAME, "readwrite");
7527
7530
  const store = tx.objectStore(STORE_NAME);
7528
7531
  const req = store.delete(key);
7529
- req.onsuccess = () => resolve5();
7532
+ req.onsuccess = () => resolve6();
7530
7533
  req.onerror = () => reject(req.error);
7531
7534
  });
7532
7535
  }
7533
7536
  function idbKeys(db) {
7534
- return new Promise((resolve5, reject) => {
7537
+ return new Promise((resolve6, reject) => {
7535
7538
  const tx = db.transaction(STORE_NAME, "readonly");
7536
7539
  const store = tx.objectStore(STORE_NAME);
7537
7540
  const req = store.getAllKeys();
7538
- req.onsuccess = () => resolve5(req.result.filter((k) => typeof k === "string"));
7541
+ req.onsuccess = () => resolve6(req.result.filter((k) => typeof k === "string"));
7539
7542
  req.onerror = () => reject(req.error);
7540
7543
  });
7541
7544
  }
@@ -11594,8 +11597,8 @@ var init_dist2 = __esm({
11594
11597
  if (this.closed) {
11595
11598
  return Promise.resolve({ value: void 0, done: true });
11596
11599
  }
11597
- return new Promise((resolve5, reject) => {
11598
- this.waiters.push({ resolve: resolve5, reject });
11600
+ return new Promise((resolve6, reject) => {
11601
+ this.waiters.push({ resolve: resolve6, reject });
11599
11602
  });
11600
11603
  }
11601
11604
  };
@@ -11991,7 +11994,7 @@ var init_dist2 = __esm({
11991
11994
  } finally {
11992
11995
  for (const abort of aborts) abort.dispose();
11993
11996
  }
11994
- await new Promise((resolve5) => setTimeout(resolve5, 0));
11997
+ await new Promise((resolve6) => setTimeout(resolve6, 0));
11995
11998
  }
11996
11999
  /** The stream the current subscribers need, or undefined when none remain. */
11997
12000
  plan() {
@@ -14362,12 +14365,14 @@ import { resolveSecretsOwner } from "@tinycloud/operations/secret-request";
14362
14365
  import {
14363
14366
  additionalDelegationsPath as sharedAdditionalDelegationsPath,
14364
14367
  authRequestsPath as sharedAuthRequestsPath,
14368
+ profilePath,
14365
14369
  profileStoreMetadataPath,
14366
14370
  readAdditionalDelegations,
14367
14371
  readAuthRequests,
14368
14372
  refuseWriteToDeletedProfile,
14369
14373
  updateProfileStore,
14370
14374
  withProfileLock,
14375
+ withPinnedProfileDirectory,
14371
14376
  writeJsonAtomic
14372
14377
  } from "@tinycloud/operations/state";
14373
14378
  function isCompatiblePermissionRequestArtifact(value) {
@@ -14385,10 +14390,10 @@ function permissionRequestsPath(profile) {
14385
14390
  return sharedAuthRequestsPath(profile);
14386
14391
  }
14387
14392
  function grantHistoryPath(profile) {
14388
- return join3(PROFILES_DIR, profile, "auth-grants.jsonl");
14393
+ return join3(profilePath(profile), "auth-grants.jsonl");
14389
14394
  }
14390
14395
  function localGrantArtifactsPath(profile) {
14391
- return join3(PROFILES_DIR, profile, "auth-grant-artifacts.json");
14396
+ return join3(profilePath(profile), "auth-grant-artifacts.json");
14392
14397
  }
14393
14398
  function createPermissionRequestArtifact(params) {
14394
14399
  return buildPermissionRequestArtifact({
@@ -14415,22 +14420,30 @@ async function loadAdditionalDelegations(profile) {
14415
14420
  return readAdditionalDelegations(profile);
14416
14421
  }
14417
14422
  async function loadLocalGrantArtifacts(profile) {
14418
- const path = localGrantArtifactsPath(profile);
14419
- if (!await fileExists(path)) return [];
14420
- let parsed;
14421
- try {
14422
- parsed = JSON.parse(await readFile2(path, "utf8"));
14423
- } catch {
14424
- return [];
14425
- }
14426
- if (!Array.isArray(parsed)) return [];
14427
- return parsed.filter(
14428
- (value) => value !== null && typeof value === "object" && typeof value.delegationCid === "string" && value.delegation !== null && typeof value.delegation === "object" && value.delegation.cid === value.delegationCid && typeof value.delegation.delegationHeader?.Authorization === "string"
14429
- );
14423
+ return withPinnedProfileDirectory(profile, async (directory) => {
14424
+ if (!directory) return [];
14425
+ const path = localGrantArtifactsPath(profile);
14426
+ if (!await fileExists(path)) return [];
14427
+ let parsed;
14428
+ try {
14429
+ parsed = JSON.parse(await readFile2(path, "utf8"));
14430
+ } catch {
14431
+ return [];
14432
+ }
14433
+ if (!Array.isArray(parsed)) return [];
14434
+ const records = parsed;
14435
+ return records.filter((value) => {
14436
+ if (value === null || typeof value !== "object" || !("delegationCid" in value) || typeof value.delegationCid !== "string" || !("delegation" in value) || value.delegation === null || typeof value.delegation !== "object") return false;
14437
+ const delegation = value.delegation;
14438
+ if (!("cid" in delegation) || delegation.cid !== value.delegationCid || !("delegationHeader" in delegation) || delegation.delegationHeader === null || typeof delegation.delegationHeader !== "object") return false;
14439
+ const header = delegation.delegationHeader;
14440
+ return "Authorization" in header && typeof header.Authorization === "string";
14441
+ });
14442
+ });
14430
14443
  }
14431
14444
  async function appendLocalGrantArtifact(profile, delegation) {
14432
- await ProfileManager.ensureProfileDir(profile);
14433
14445
  await withProfileLock(profile, async () => {
14446
+ await ProfileManager.ensureProfileDir(profile);
14434
14447
  await refuseWriteToDeletedProfile(profile);
14435
14448
  const records = await loadLocalGrantArtifacts(profile);
14436
14449
  if (records.some((record2) => record2.delegationCid === delegation.cid)) return;
@@ -14523,21 +14536,27 @@ async function cliGrantRecord(node, delegation, permissions, host) {
14523
14536
  );
14524
14537
  }
14525
14538
  async function appendGrantHistory(profile, entry) {
14526
- await ProfileManager.ensureProfileDir(profile);
14527
- const line = JSON.stringify({
14528
- ts: (/* @__PURE__ */ new Date()).toISOString(),
14529
- profile,
14530
- ...entry
14531
- }) + "\n";
14532
- const path = grantHistoryPath(profile);
14533
- await appendFile(path, line, { encoding: "utf8", mode: PRIVATE_FILE_MODE });
14534
- await chmod(path, PRIVATE_FILE_MODE);
14539
+ await withProfileLock(profile, async () => {
14540
+ await ProfileManager.ensureProfileDir(profile);
14541
+ await refuseWriteToDeletedProfile(profile);
14542
+ const line = JSON.stringify({
14543
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
14544
+ profile,
14545
+ ...entry
14546
+ }) + "\n";
14547
+ const path = grantHistoryPath(profile);
14548
+ await appendFile(path, line, { encoding: "utf8", mode: PRIVATE_FILE_MODE });
14549
+ await chmod(path, PRIVATE_FILE_MODE);
14550
+ });
14535
14551
  }
14536
14552
  async function readGrantHistory(profile) {
14537
- const path = grantHistoryPath(profile);
14538
- if (!await fileExists(path)) return [];
14539
- const raw = await readFile2(path, "utf8");
14540
- return raw.split("\n").map((line) => line.trim()).filter(Boolean).map((line) => JSON.parse(line));
14553
+ return withPinnedProfileDirectory(profile, async (directory) => {
14554
+ if (!directory) return [];
14555
+ const path = grantHistoryPath(profile);
14556
+ if (!await fileExists(path)) return [];
14557
+ const raw = await readFile2(path, "utf8");
14558
+ return raw.split("\n").map((line) => line.trim()).filter(Boolean).map((line) => JSON.parse(line));
14559
+ });
14541
14560
  }
14542
14561
  async function parseCapSpec(spec, profile) {
14543
14562
  const firstColon = spec.indexOf(":");
@@ -14786,7 +14805,6 @@ var init_permissions = __esm({
14786
14805
  "src/lib/permissions.ts"() {
14787
14806
  "use strict";
14788
14807
  init_manifest();
14789
- init_constants();
14790
14808
  init_storage();
14791
14809
  init_profiles();
14792
14810
  init_errors();
@@ -17765,7 +17783,7 @@ var init_dist3 = __esm({
17765
17783
  // src/lib/replication-config.ts
17766
17784
  import { join as join5 } from "path";
17767
17785
  import { CLI_NAME as CLI_NAME6 } from "@tinycloud/operations/cli-name";
17768
- import { profilePath, withProfileLock as withProfileLock2 } from "@tinycloud/operations/state";
17786
+ import { profilePath as profilePath2, withProfileLock as withProfileLock2 } from "@tinycloud/operations/state";
17769
17787
  import { sqliteReplicaStorage } from "@tinycloud/node-sdk";
17770
17788
  function createCliReplicationConfig(profile, settings, output2, enabled) {
17771
17789
  if (!enabled || !settings || settings.prefixes.length === 0) return void 0;
@@ -17783,7 +17801,7 @@ function createCliReplicationConfig(profile, settings, output2, enabled) {
17783
17801
  );
17784
17802
  }
17785
17803
  }
17786
- const root = profilePath(profile);
17804
+ const root = profilePath2(profile);
17787
17805
  return {
17788
17806
  enabled: true,
17789
17807
  prefixes: settings.prefixes,
@@ -18269,17 +18287,21 @@ var init_host = __esm({
18269
18287
  });
18270
18288
 
18271
18289
  // src/config/profiles.ts
18272
- import { chmod as chmod2, lstat, readdir as readdir2, rm as rm2, rmdir } from "fs/promises";
18290
+ import { chmod as chmod2, lstat, readdir as readdir2, rm as rm2 } from "fs/promises";
18273
18291
  import { CLI_NAME as CLI_NAME9 } from "@tinycloud/operations/cli-name";
18274
18292
  import { join as join6 } from "path";
18275
18293
  import {
18276
- profilePath as profilePath2,
18294
+ assertRealProfileDirectory,
18295
+ ProfileSymlinkError as ProfileSymlinkError2,
18296
+ namedProfilePath,
18297
+ profilePath as profilePath3,
18277
18298
  readSession,
18278
18299
  recordProfileDeletion,
18279
18300
  refuseWriteToDeletedProfile as refuseWriteToDeletedProfile2,
18280
18301
  removeSession,
18281
18302
  withProfileLock as withProfileLock3,
18282
- writeSession
18303
+ writeSession,
18304
+ withPinnedProfileDirectory as withPinnedProfileDirectory2
18283
18305
  } from "@tinycloud/operations/state";
18284
18306
  var ProfileManager;
18285
18307
  var init_profiles = __esm({
@@ -18321,8 +18343,13 @@ var init_profiles = __esm({
18321
18343
  }
18322
18344
  /** Owner-only profile directory (0700), created or tightened before any write into it. */
18323
18345
  static async ensureProfileDir(name) {
18346
+ const profileDir = profilePath3(name);
18347
+ if (profileDir !== join6(PROFILES_DIR, name)) {
18348
+ await chmod2(profileDir, PRIVATE_DIR_MODE);
18349
+ return profileDir;
18350
+ }
18324
18351
  await _ProfileManager.ensureConfigDir();
18325
- const profileDir = join6(PROFILES_DIR, name);
18352
+ await assertRealProfileDirectory(name);
18326
18353
  await ensureDir(profileDir);
18327
18354
  await chmod2(profileDir, PRIVATE_DIR_MODE);
18328
18355
  return profileDir;
@@ -18351,8 +18378,7 @@ var init_profiles = __esm({
18351
18378
  * Throws CLIError if the profile doesn't exist.
18352
18379
  */
18353
18380
  static async getProfile(name) {
18354
- const profilePath5 = join6(PROFILES_DIR, name, "profile.json");
18355
- const profile = await readJson(profilePath5);
18381
+ const profile = await withPinnedProfileDirectory2(name, (directory) => directory ? readJson(join6(directory, "profile.json")) : Promise.resolve(null));
18356
18382
  if (!profile) {
18357
18383
  throw new CLIError(
18358
18384
  "PROFILE_NOT_FOUND",
@@ -18372,13 +18398,13 @@ var init_profiles = __esm({
18372
18398
  }
18373
18399
  /** Removes profile.json under the profile lock (rollback of a profile a failed login created). */
18374
18400
  static async removeProfileConfig(name) {
18375
- await _ProfileManager.withLock(name, () => rm2(join6(PROFILES_DIR, name, "profile.json"), { force: true }));
18401
+ await _ProfileManager.withLock(name, () => rm2(join6(profilePath3(name), "profile.json"), { force: true }));
18376
18402
  }
18377
18403
  /**
18378
18404
  * Returns true if a profile directory exists.
18379
18405
  */
18380
18406
  static async profileExists(name) {
18381
- return fileExists(join6(PROFILES_DIR, name, "profile.json"));
18407
+ return withPinnedProfileDirectory2(name, (directory) => directory ? fileExists(join6(directory, "profile.json")) : Promise.resolve(false));
18382
18408
  }
18383
18409
  /**
18384
18410
  * Returns an array of profile directory names.
@@ -18402,7 +18428,7 @@ var init_profiles = __esm({
18402
18428
  static async deleteProfile(name) {
18403
18429
  let profileDir;
18404
18430
  try {
18405
- profileDir = profilePath2(name);
18431
+ profileDir = namedProfilePath(name);
18406
18432
  } catch {
18407
18433
  throw new CLIError(
18408
18434
  "INVALID_PROFILE_NAME",
@@ -18427,23 +18453,21 @@ var init_profiles = __esm({
18427
18453
  return;
18428
18454
  }
18429
18455
  await _ProfileManager.withLock(name, async () => {
18430
- for (const file of ["session.json", "key.json"]) await rm2(join6(profileDir, file), { force: true });
18431
- for (const entry of await readdir2(profileDir)) {
18432
- if (entry !== ".lock" && entry !== "profile.json") await rm2(join6(profileDir, entry), { recursive: true, force: true });
18456
+ const directory = profilePath3(name);
18457
+ for (const file of ["session.json", "key.json"]) await rm2(join6(directory, file), { force: true });
18458
+ for (const entry of await readdir2(directory)) {
18459
+ if (entry !== ".lock" && entry !== "profile.json") await rm2(join6(directory, entry), { recursive: true, force: true });
18433
18460
  }
18434
- await rm2(join6(profileDir, "profile.json"), { force: true });
18461
+ await rm2(join6(directory, "profile.json"), { force: true });
18435
18462
  await recordProfileDeletion(name);
18436
18463
  });
18437
- await rmdir(profileDir).catch((error) => {
18438
- if (error.code !== "ENOENT" && error.code !== "ENOTEMPTY" && error.code !== "EEXIST") throw error;
18439
- });
18440
18464
  }
18441
18465
  // ── Key management ──────────────────────────────────────────────────
18442
18466
  /**
18443
18467
  * Returns the parsed JWK for a profile, or null if no key exists.
18444
18468
  */
18445
18469
  static async getKey(name) {
18446
- return readJson(join6(PROFILES_DIR, name, "key.json"));
18470
+ return withPinnedProfileDirectory2(name, (directory) => directory ? readJson(join6(directory, "key.json")) : Promise.resolve(null));
18447
18471
  }
18448
18472
  /**
18449
18473
  * Saves a JWK key under the profile lock (0600, in an owner-only profile
@@ -18458,7 +18482,7 @@ var init_profiles = __esm({
18458
18482
  }
18459
18483
  /** Removes key.json under the profile lock (rollback of a key a failed login created). */
18460
18484
  static async removeKey(name) {
18461
- await _ProfileManager.withLock(name, () => rm2(join6(PROFILES_DIR, name, "key.json"), { force: true }));
18485
+ await _ProfileManager.withLock(name, () => rm2(join6(profilePath3(name), "key.json"), { force: true }));
18462
18486
  }
18463
18487
  // ── Session management ──────────────────────────────────────────────
18464
18488
  /**
@@ -18485,10 +18509,13 @@ var init_profiles = __esm({
18485
18509
  * created or tightened to 0700.
18486
18510
  */
18487
18511
  static async getCacheDir(name) {
18488
- const cacheDir = join6(await _ProfileManager.ensureProfileDir(name), "cache");
18489
- await ensureDir(cacheDir);
18490
- await chmod2(cacheDir, PRIVATE_DIR_MODE);
18491
- return cacheDir;
18512
+ const alreadyPinned = profilePath3(name) !== namedProfilePath(name);
18513
+ return _ProfileManager.withLock(name, async () => {
18514
+ const cacheDir = join6(await _ProfileManager.ensureProfileDir(name), "cache");
18515
+ await ensureDir(cacheDir);
18516
+ await chmod2(cacheDir, PRIVATE_DIR_MODE);
18517
+ return alreadyPinned ? cacheDir : join6(namedProfilePath(name), "cache");
18518
+ });
18492
18519
  }
18493
18520
  // ── Resolution helpers ──────────────────────────────────────────────
18494
18521
  /**
@@ -18512,7 +18539,8 @@ var init_profiles = __esm({
18512
18539
  try {
18513
18540
  profileConfig = await _ProfileManager.getProfile(profile);
18514
18541
  profileHost = profileConfig.host;
18515
- } catch {
18542
+ } catch (error) {
18543
+ if (error instanceof ProfileSymlinkError2) throw error;
18516
18544
  }
18517
18545
  const envReplication = process.env.TC_REPLICATION?.toLowerCase();
18518
18546
  const envEnabled = envReplication === "1" || envReplication === "true" || envReplication === "on";
@@ -18876,22 +18904,23 @@ import { get as httpGet } from "http";
18876
18904
  import { get as httpsGet } from "https";
18877
18905
  import { spawn } from "child_process";
18878
18906
  import { mkdir as mkdir3, readFile as readFile5, writeFile as writeFile2 } from "fs/promises";
18879
- import { dirname as dirname3 } from "path";
18907
+ import { dirname as dirname4 } from "path";
18880
18908
  import { createInterface as createInterface2 } from "readline";
18881
18909
  import { grantAuthRequest, principalDidEquals, signedCompactUcanExpiry as signedCompactUcanExpiry3 } from "@tinycloud/node-sdk";
18882
18910
  import { invokeOperation } from "@tinycloud/operations";
18883
18911
 
18884
18912
  // src/lib/profile-replicas.ts
18885
18913
  init_dist3();
18886
- import { lstat as lstat2, readdir as readdir4, realpath, rm as rm4 } from "fs/promises";
18914
+ import { lstat as lstat3, open as open3, readdir as readdir4, realpath, rm as rm4, rmdir as rmdir2, stat as stat3 } from "fs/promises";
18915
+ import { constants as fsConstants2 } from "fs";
18887
18916
  import { join as join8, relative, sep } from "path";
18888
- import { ProfileDeletedError as ProfileDeletedError2, profilePath as profilePath3, withProfileLock as withProfileLock4 } from "@tinycloud/operations/state";
18917
+ import { ProfileDeletedError as ProfileDeletedError2, profilePath as profilePath4, withProfileLock as withProfileLock4 } from "@tinycloud/operations/state";
18889
18918
 
18890
18919
  // ../replica/dist/sqlite.js
18891
18920
  import { randomBytes as randomBytes2 } from "crypto";
18892
- import { constants as fsConstants } from "fs";
18893
- import { chmod as chmod3, link, mkdir as mkdir2, open as open2, readFile as readFile4, readdir as readdir3, rm as rm3, rmdir as rmdir2, stat as stat2, unlink } from "fs/promises";
18894
- import { join as join7 } from "path";
18921
+ import { closeSync, constants as fsConstants, fstatSync, lstatSync, openSync, readdirSync as readdirSync2, readlinkSync } from "fs";
18922
+ import { chmod as chmod3, link, lstat as lstat2, mkdir as mkdir2, open as open2, readFile as readFile4, readdir as readdir3, rm as rm3, rmdir, stat as stat2, unlink } from "fs/promises";
18923
+ import { dirname as dirname3, join as join7, resolve } from "path";
18895
18924
  var ReplicaErrorCode2 = {
18896
18925
  /** Another process holds the sync lease. */
18897
18926
  BUSY: "REPLICA_BUSY",
@@ -19078,6 +19107,29 @@ async function loadSqlite() {
19078
19107
  return (path, options) => wrap(options?.readonly ? new DatabaseSync(path, { readOnly: true }) : new DatabaseSync(path));
19079
19108
  }
19080
19109
  var SCHEMA_VERSION = 1;
19110
+ var sqliteOpenQueue = Promise.resolve();
19111
+ async function acquireSqliteOpen() {
19112
+ const previous = sqliteOpenQueue;
19113
+ let release;
19114
+ sqliteOpenQueue = new Promise((resolve22) => {
19115
+ release = resolve22;
19116
+ });
19117
+ await previous;
19118
+ return release;
19119
+ }
19120
+ async function captureNamedPath(path) {
19121
+ const names = [];
19122
+ for (let current = resolve(path); ; current = dirname3(current)) {
19123
+ names.push(current);
19124
+ if (dirname3(current) === current) break;
19125
+ }
19126
+ const identities = [];
19127
+ for (const name of names.reverse()) {
19128
+ const stats = await lstat2(name, { bigint: true });
19129
+ identities.push({ path: name, dev: stats.dev, ino: stats.ino });
19130
+ }
19131
+ return identities;
19132
+ }
19081
19133
  var DURABILITY = "sqlite-wal-synchronous-full+fsynced-blobs";
19082
19134
  var SCHEMA = `
19083
19135
  CREATE TABLE IF NOT EXISTS replica (
@@ -19176,22 +19228,78 @@ async function entriesOf(path) {
19176
19228
  }
19177
19229
  }
19178
19230
  var FAULTS = /* @__PURE__ */ Symbol("tinycloud.replica.faults");
19231
+ var pinnedDirectories = new FinalizationRegistry((fd) => {
19232
+ try {
19233
+ closeSync(fd);
19234
+ } catch {
19235
+ }
19236
+ });
19237
+ var writableStores = /* @__PURE__ */ new Map();
19238
+ function releaseWritable(shared) {
19239
+ if (shared.references === 0 || --shared.references !== 0) return;
19240
+ if (writableStores.get(shared.key) === shared) writableStores.delete(shared.key);
19241
+ try {
19242
+ shared.db.close();
19243
+ } finally {
19244
+ if (shared.directoryFd !== void 0) closeSync(shared.directoryFd);
19245
+ }
19246
+ }
19247
+ var sharedStores = new FinalizationRegistry((shared) => {
19248
+ try {
19249
+ releaseWritable(shared);
19250
+ } catch {
19251
+ }
19252
+ });
19253
+ async function acquireWritableMutation(shared) {
19254
+ const previous = shared.mutationTail;
19255
+ let release;
19256
+ shared.mutationTail = new Promise((resolve22) => {
19257
+ release = resolve22;
19258
+ });
19259
+ await previous;
19260
+ return release;
19261
+ }
19179
19262
  var SqliteReplicaStore = class _SqliteReplicaStore {
19180
19263
  dir;
19181
- #db;
19264
+ #database;
19265
+ get #db() {
19266
+ if (this.#closed) throw new ReplicaError2(ReplicaErrorCode2.CLOSED, "The replica store is closed.");
19267
+ return this.#database;
19268
+ }
19269
+ #closed = false;
19270
+ #closing = false;
19271
+ #closePromise;
19272
+ #activeMutations = 0;
19273
+ #drained;
19274
+ #shared;
19275
+ #pinnedDir;
19276
+ #directoryFd;
19182
19277
  #holder = `${process.pid}-${randomBytes2(6).toString("hex")}`;
19183
19278
  #now;
19184
19279
  #faults;
19185
19280
  #guard;
19186
19281
  /** Identity of the database file this store opened: a deleted-and-recreated replica is a different file. */
19187
19282
  #ino;
19188
- constructor(dir, db, ino, options) {
19283
+ #dev;
19284
+ #namedAncestors;
19285
+ constructor(dir, db, identity, options, directoryFd, namedAncestors = [], shared) {
19189
19286
  this.dir = dir;
19190
- this.#db = db;
19191
- this.#ino = ino;
19287
+ this.#pinnedDir = shared?.pinnedDir ?? (directoryFd !== void 0 && process.platform === "linux" ? `/proc/self/fd/${directoryFd}` : dir);
19288
+ this.#directoryFd = directoryFd;
19289
+ this.#shared = shared;
19290
+ if (shared) {
19291
+ shared.references++;
19292
+ sharedStores.register(this, shared, this);
19293
+ } else if (directoryFd !== void 0) {
19294
+ pinnedDirectories.register(this, directoryFd, this);
19295
+ }
19296
+ this.#database = db;
19297
+ this.#ino = identity.ino;
19298
+ this.#dev = identity.dev;
19192
19299
  this.#now = options.now ?? Date.now;
19193
19300
  this.#faults = options[FAULTS] ?? {};
19194
19301
  this.#guard = options.guard ?? ((section) => section());
19302
+ this.#namedAncestors = namedAncestors;
19195
19303
  }
19196
19304
  /**
19197
19305
  * Read-only status snapshot; does not create a replica, migrate, recover
@@ -19204,9 +19312,10 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
19204
19312
  };
19205
19313
  checkAbort();
19206
19314
  const dbPath = join7(dir, "replica.db");
19207
- let ino;
19315
+ let identity;
19208
19316
  try {
19209
- ino = (await stat2(dbPath)).ino;
19317
+ identity = await lstat2(dbPath);
19318
+ if (!identity.isFile()) return null;
19210
19319
  } catch (error) {
19211
19320
  if (errnoOf(error) === "ENOENT") return null;
19212
19321
  throw storageError(error, "Inspecting the replica");
@@ -19219,14 +19328,14 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
19219
19328
  db = opener(dbPath, { readonly: true });
19220
19329
  db.exec("PRAGMA busy_timeout = 0");
19221
19330
  checkAbort();
19222
- const store = new _SqliteReplicaStore(dir, db, ino, { create: false, ...options.now ? { now: options.now } : {} });
19331
+ const store = new _SqliteReplicaStore(dir, db, identity, { create: false, ...options.now ? { now: options.now } : {} });
19223
19332
  for (let attempt = 0; attempt < 2; attempt++) {
19224
19333
  const before = db.get("PRAGMA data_version").data_version;
19225
19334
  const state2 = await store.open();
19226
19335
  checkAbort();
19227
19336
  if (state2 === null) return null;
19228
19337
  const status = await store.status();
19229
- await new Promise((resolve5) => setImmediate(resolve5));
19338
+ await new Promise((resolve22) => setImmediate(resolve22));
19230
19339
  checkAbort();
19231
19340
  const after = db.get("PRAGMA data_version").data_version;
19232
19341
  if (before === after) return { state: state2, status };
@@ -19246,95 +19355,233 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
19246
19355
  * REPLICA_NOT_FOUND and nothing is written to disk.
19247
19356
  */
19248
19357
  static async open(dir, options) {
19249
- const dbPath = join7(dir, "replica.db");
19358
+ const namedDbPath = join7(dir, "replica.db");
19250
19359
  if (!options.create) {
19251
- const exists = await stat2(dbPath).then(
19252
- () => true,
19360
+ const exists = await lstat2(namedDbPath).then(
19361
+ (stats) => stats.isFile(),
19253
19362
  () => false
19254
19363
  );
19255
- if (!exists) throw new ReplicaError2(ReplicaErrorCode2.NOT_FOUND, `No replica at ${dir}.`);
19364
+ if (!exists) throw new ReplicaError2(ReplicaErrorCode2.NOT_FOUND, `No regular replica database at ${dir}.`);
19256
19365
  }
19257
- const opener = options.sqlite ?? await loadSqlite();
19366
+ const releaseOpen = await acquireSqliteOpen();
19258
19367
  let store;
19368
+ let directoryFd;
19259
19369
  try {
19370
+ if (options.create) await mkdir2(dir, { recursive: true, mode: 448 });
19371
+ if (process.platform === "linux") {
19372
+ directoryFd = openSync(dir, fsConstants.O_RDONLY | fsConstants.O_DIRECTORY | fsConstants.O_NOFOLLOW);
19373
+ }
19374
+ const opened = directoryFd === void 0 ? lstatSync(dir, { bigint: true }) : fstatSync(directoryFd, { bigint: true });
19375
+ if (!opened.isDirectory() || opened.isSymbolicLink() || options.expectedDirectory && (opened.dev !== BigInt(options.expectedDirectory.dev) || opened.ino !== BigInt(options.expectedDirectory.ino))) {
19376
+ throw new Error("The replica directory changed before SQLite opened it.");
19377
+ }
19378
+ const namedAncestors = process.platform === "linux" ? [] : await captureNamedPath(dir);
19379
+ if (namedAncestors.length && (namedAncestors[namedAncestors.length - 1].dev !== opened.dev || namedAncestors[namedAncestors.length - 1].ino !== opened.ino)) {
19380
+ throw new Error("The replica directory changed while checking its ancestry.");
19381
+ }
19382
+ const pinnedDir = directoryFd === void 0 ? dir : `/proc/self/fd/${directoryFd}`;
19383
+ const dbPath = join7(pinnedDir, "replica.db");
19260
19384
  if (options.create) {
19261
- await mkdir2(join7(dir, "blobs", ".tmp"), { recursive: true, mode: 448 });
19262
- await Promise.all([chmod3(dir, 448), chmod3(join7(dir, "blobs"), 448), chmod3(join7(dir, "blobs", ".tmp"), 448)]);
19263
- const handle = await open2(dbPath, fsConstants.O_CREAT | fsConstants.O_RDWR, 384);
19264
- await handle.close();
19265
- await chmod3(dbPath, 384);
19266
- }
19267
- const ino = (await stat2(dbPath)).ino;
19268
- const db = opener(dbPath);
19269
- db.exec("PRAGMA busy_timeout = 5000");
19270
- db.exec("PRAGMA journal_mode = WAL");
19271
- db.exec("PRAGMA synchronous = FULL");
19272
- db.exec("PRAGMA secure_delete = ON");
19273
- db.exec("PRAGMA foreign_keys = ON");
19274
- const version = db.get("PRAGMA user_version")?.user_version ?? 0;
19275
- if (version > SCHEMA_VERSION) {
19276
- db.close();
19277
- throw new ReplicaError2(
19278
- ReplicaErrorCode2.STORAGE_ERROR,
19279
- `The replica at ${dir} was written by a newer tc (schema ${version}); upgrade tc to read it.`
19280
- );
19385
+ await mkdir2(join7(pinnedDir, "blobs", ".tmp"), { recursive: true, mode: 448 });
19386
+ await Promise.all([chmod3(pinnedDir, 448), chmod3(join7(pinnedDir, "blobs"), 448), chmod3(join7(pinnedDir, "blobs", ".tmp"), 448)]);
19387
+ const handle = await open2(dbPath, fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_RDWR | fsConstants.O_NOFOLLOW, 384).catch((error) => {
19388
+ if (errnoOf(error) === "EEXIST") return null;
19389
+ throw error;
19390
+ });
19391
+ if (handle) {
19392
+ try {
19393
+ await handle.chmod(384);
19394
+ } finally {
19395
+ await handle.close();
19396
+ }
19397
+ }
19281
19398
  }
19282
- if (version < SCHEMA_VERSION) {
19283
- db.exec("BEGIN IMMEDIATE");
19284
- db.exec(SCHEMA);
19285
- db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
19286
- db.exec("COMMIT");
19399
+ const leaf = await open2(dbPath, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
19400
+ let db;
19401
+ try {
19402
+ const identity = await leaf.stat();
19403
+ const named = await lstat2(dbPath);
19404
+ if (!identity.isFile() || !named.isFile() || named.dev !== identity.dev || named.ino !== identity.ino) {
19405
+ throw new Error("The replica database changed before SQLite opened it.");
19406
+ }
19407
+ const key = `${opened.dev}:${opened.ino}`;
19408
+ const active = writableStores.get(key);
19409
+ if (active && (active.dev !== identity.dev || active.ino !== identity.ino)) {
19410
+ throw new Error("The replica database changed while another controller holds it open.");
19411
+ }
19412
+ if (active && !options.sqlite) {
19413
+ const namedDirectory = lstatSync(dir, { bigint: true });
19414
+ if (!namedDirectory.isDirectory() || namedDirectory.dev !== opened.dev || namedDirectory.ino !== opened.ino) {
19415
+ throw new Error("The replica directory changed before its checked SQLite handle could be shared.");
19416
+ }
19417
+ const pinned = active.directoryFd === void 0 ? lstatSync(active.pinnedDir, { bigint: true }) : fstatSync(active.directoryFd, { bigint: true });
19418
+ const activeLeaf = lstatSync(join7(active.pinnedDir, "replica.db"));
19419
+ if (!pinned.isDirectory() || pinned.dev !== opened.dev || pinned.ino !== opened.ino || !activeLeaf.isFile() || activeLeaf.dev !== identity.dev || activeLeaf.ino !== identity.ino || active.directoryFd === void 0 && active.namedPath !== dir) {
19420
+ throw new Error("The live replica changed before its checked SQLite handle could be shared.");
19421
+ }
19422
+ if (directoryFd !== void 0) {
19423
+ closeSync(directoryFd);
19424
+ directoryFd = void 0;
19425
+ }
19426
+ store = new _SqliteReplicaStore(dir, active.db, identity, options, active.directoryFd, namedAncestors, active);
19427
+ }
19428
+ if (!store) {
19429
+ const opener = options.sqlite ?? await loadSqlite();
19430
+ const previousFds = process.platform === "linux" && !options.sqlite ? new Map(readdirSync2("/proc/self/fd").map((fd) => {
19431
+ try {
19432
+ return [fd, readlinkSync(`/proc/self/fd/${fd}`)];
19433
+ } catch {
19434
+ return [fd, ""];
19435
+ }
19436
+ })) : null;
19437
+ db = opener(dbPath);
19438
+ const afterOpen = lstatSync(dbPath);
19439
+ if (!afterOpen.isFile() || afterOpen.dev !== identity.dev || afterOpen.ino !== identity.ino) {
19440
+ throw new Error("The replica database changed while SQLite opened it.");
19441
+ }
19442
+ if (previousFds) db.get("SELECT count(*) FROM sqlite_master");
19443
+ if (previousFds) {
19444
+ const databases = readdirSync2("/proc/self/fd").filter((fd) => {
19445
+ try {
19446
+ const target = readlinkSync(`/proc/self/fd/${fd}`);
19447
+ return target.endsWith("/replica.db") && previousFds.get(fd) !== target;
19448
+ } catch {
19449
+ return false;
19450
+ }
19451
+ });
19452
+ if (databases.length === 0 || databases.some((fd) => {
19453
+ try {
19454
+ const actual = fstatSync(Number(fd));
19455
+ return actual.dev !== identity.dev || actual.ino !== identity.ino;
19456
+ } catch {
19457
+ return true;
19458
+ }
19459
+ })) {
19460
+ throw new Error("SQLite opened a different replica database.");
19461
+ }
19462
+ }
19463
+ const after = lstatSync(dbPath);
19464
+ if (!after.isFile() || after.dev !== identity.dev || after.ino !== identity.ino) {
19465
+ throw new Error("The replica database changed while SQLite opened it.");
19466
+ }
19467
+ db.exec("PRAGMA busy_timeout = 5000");
19468
+ db.exec("PRAGMA journal_mode = WAL");
19469
+ db.exec("PRAGMA synchronous = FULL");
19470
+ db.exec("PRAGMA secure_delete = ON");
19471
+ db.exec("PRAGMA foreign_keys = ON");
19472
+ const version = db.get("PRAGMA user_version")?.user_version ?? 0;
19473
+ if (version > SCHEMA_VERSION) {
19474
+ throw new ReplicaError2(
19475
+ ReplicaErrorCode2.STORAGE_ERROR,
19476
+ `The replica at ${dir} was written by a newer tc (schema ${version}); upgrade tc to read it.`
19477
+ );
19478
+ }
19479
+ if (version < SCHEMA_VERSION) {
19480
+ db.exec("BEGIN IMMEDIATE");
19481
+ db.exec(SCHEMA);
19482
+ db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
19483
+ db.exec("COMMIT");
19484
+ }
19485
+ const shared = options.sqlite ? void 0 : {
19486
+ key,
19487
+ db,
19488
+ directoryFd,
19489
+ pinnedDir,
19490
+ namedPath: dir,
19491
+ dev: identity.dev,
19492
+ ino: identity.ino,
19493
+ references: 0,
19494
+ mutationTail: Promise.resolve()
19495
+ };
19496
+ store = new _SqliteReplicaStore(dir, db, identity, options, directoryFd, namedAncestors, shared);
19497
+ if (shared) writableStores.set(key, shared);
19498
+ db = void 0;
19499
+ }
19500
+ } finally {
19501
+ try {
19502
+ db?.close();
19503
+ } finally {
19504
+ await leaf.close();
19505
+ }
19287
19506
  }
19288
- store = new _SqliteReplicaStore(dir, db, ino, options);
19289
19507
  } catch (error) {
19508
+ if (store) await store.close().catch(() => void 0);
19509
+ else if (directoryFd !== void 0) closeSync(directoryFd);
19290
19510
  throw storageError(error, `Opening the replica at ${dir}`);
19511
+ } finally {
19512
+ releaseOpen();
19291
19513
  }
19292
- const row = store.#row();
19293
- if (row?.revoked_detail != null && (row.purge_pending === 1 || await store.#holdsContentFiles().catch(() => true))) {
19294
- await store.#finishRevocationPurge().catch(() => void 0);
19514
+ try {
19515
+ const row = store.#row();
19516
+ if (row?.revoked_detail != null && (row.purge_pending === 1 || await store.#holdsContentFiles().catch(() => true))) {
19517
+ await store.#finishRevocationPurge().catch(() => void 0);
19518
+ }
19519
+ return store;
19520
+ } catch (error) {
19521
+ await store.close().catch(() => void 0);
19522
+ throw error;
19295
19523
  }
19296
- return store;
19297
19524
  }
19298
19525
  #row() {
19299
19526
  return this.#db.get("SELECT * FROM replica WHERE id = 1");
19300
19527
  }
19301
19528
  /** Refuse to touch the filesystem for a replica that was deleted (or replaced) under us. */
19302
19529
  async #assertPresent() {
19303
- const ino = await stat2(join7(this.dir, "replica.db")).then(
19304
- (stats) => stats.ino,
19305
- () => void 0
19306
- );
19307
- if (ino !== this.#ino) {
19530
+ const identity = await lstat2(join7(this.#pinnedDir, "replica.db")).catch(() => null);
19531
+ if (!identity?.isFile() || identity.dev !== this.#dev || identity.ino !== this.#ino) {
19308
19532
  throw new ReplicaError2(
19309
19533
  ReplicaErrorCode2.NOT_FOUND,
19310
19534
  `The replica at ${this.dir} was deleted while this command ran (its profile was deleted or the replica purged); nothing more was written.`
19311
19535
  );
19312
19536
  }
19313
19537
  }
19538
+ /** New calls stop at close; accepted mutation sections may still finish their transactions. */
19539
+ #assertAccepting() {
19540
+ if (this.#closing || this.#closed) {
19541
+ throw new ReplicaError2(ReplicaErrorCode2.CLOSED, "The replica store is closed.");
19542
+ }
19543
+ }
19314
19544
  /** A short mutation section: guarded (profile lock) and checked against the database this store opened. */
19315
19545
  #mutate(section) {
19316
- return this.#guard(async () => {
19317
- await this.#assertPresent();
19318
- return section();
19319
- });
19546
+ this.#assertAccepting();
19547
+ this.#activeMutations++;
19548
+ return (async () => {
19549
+ try {
19550
+ return await this.#guard(async () => {
19551
+ await this.#assertPresent();
19552
+ return section();
19553
+ });
19554
+ } finally {
19555
+ if (--this.#activeMutations === 0) {
19556
+ const drained = this.#drained;
19557
+ this.#drained = void 0;
19558
+ drained?.();
19559
+ }
19560
+ }
19561
+ })();
19320
19562
  }
19321
19563
  /** Run `body` in a write transaction (BEGIN IMMEDIATE). Callers outside a mutation section use `#guardedWrite`. */
19322
- #write(action, body) {
19323
- try {
19324
- this.#db.exec("BEGIN IMMEDIATE");
19325
- } catch (error) {
19326
- throw storageError(error, action);
19327
- }
19564
+ async #write(action, body) {
19565
+ const release = this.#shared ? await acquireWritableMutation(this.#shared) : void 0;
19328
19566
  try {
19329
- const result = body();
19330
- this.#db.exec("COMMIT");
19331
- return result;
19332
- } catch (error) {
19333
19567
  try {
19334
- this.#db.exec("ROLLBACK");
19335
- } catch {
19568
+ this.#db.exec("BEGIN IMMEDIATE");
19569
+ } catch (error) {
19570
+ throw storageError(error, action);
19571
+ }
19572
+ try {
19573
+ const result = body();
19574
+ this.#db.exec("COMMIT");
19575
+ return result;
19576
+ } catch (error) {
19577
+ try {
19578
+ this.#db.exec("ROLLBACK");
19579
+ } catch {
19580
+ }
19581
+ throw storageError(error, action);
19336
19582
  }
19337
- throw storageError(error, action);
19583
+ } finally {
19584
+ release?.();
19338
19585
  }
19339
19586
  }
19340
19587
  /** One write transaction as its own mutation section. */
@@ -19361,6 +19608,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
19361
19608
  }
19362
19609
  }
19363
19610
  async open() {
19611
+ this.#assertAccepting();
19364
19612
  let row;
19365
19613
  try {
19366
19614
  row = this.#row();
@@ -19522,6 +19770,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
19522
19770
  });
19523
19771
  }
19524
19772
  async hasContent(hashes) {
19773
+ this.#assertAccepting();
19525
19774
  const held = /* @__PURE__ */ new Set();
19526
19775
  for (let start = 0; start < hashes.length; start += 500) {
19527
19776
  const chunk = hashes.slice(start, start + 500);
@@ -19534,7 +19783,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
19534
19783
  return held;
19535
19784
  }
19536
19785
  #blobPath(hash) {
19537
- return join7(this.dir, "blobs", hash.slice(0, 2), hash);
19786
+ return join7(this.#pinnedDir, "blobs", hash.slice(0, 2), hash);
19538
19787
  }
19539
19788
  /**
19540
19789
  * Make every blob durable before any row references it: write to a fresh
@@ -19551,11 +19800,11 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
19551
19800
  () => false
19552
19801
  );
19553
19802
  if (exists) continue;
19554
- const shard = join7(this.dir, "blobs", hash.slice(0, 2));
19803
+ const shard = join7(this.#pinnedDir, "blobs", hash.slice(0, 2));
19555
19804
  await mkdir2(shard, { mode: 448 }).catch((error) => {
19556
19805
  if (errnoOf(error) !== "EEXIST") throw error;
19557
19806
  });
19558
- const temp = join7(this.dir, "blobs", ".tmp", `${hash}.${randomBytes2(6).toString("hex")}`);
19807
+ const temp = join7(this.#pinnedDir, "blobs", ".tmp", `${hash}.${randomBytes2(6).toString("hex")}`);
19559
19808
  const handle = await open2(temp, fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_WRONLY, 384);
19560
19809
  try {
19561
19810
  await handle.writeFile(bytes);
@@ -19571,7 +19820,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
19571
19820
  await unlink(temp);
19572
19821
  }
19573
19822
  dirs.add(shard);
19574
- dirs.add(join7(this.dir, "blobs"));
19823
+ dirs.add(join7(this.#pinnedDir, "blobs"));
19575
19824
  }
19576
19825
  for (const dir of dirs) {
19577
19826
  const handle = await open2(dir, fsConstants.O_RDONLY);
@@ -19604,7 +19853,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
19604
19853
  }
19605
19854
  await this.#faults.afterBlobs?.();
19606
19855
  try {
19607
- this.#write("Committing a sync page", () => {
19856
+ await this.#write("Committing a sync page", () => {
19608
19857
  this.#checkLease(t);
19609
19858
  const now = this.#now();
19610
19859
  if (p.window.notBefore !== null && now < Date.parse(p.window.notBefore)) {
@@ -19679,7 +19928,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
19679
19928
  /** Unlink every blob not in `keep`, and every temp file. Only a missing directory counts as empty. */
19680
19929
  async #unlinkBlobs(keep) {
19681
19930
  let removed = 0;
19682
- const root = join7(this.dir, "blobs");
19931
+ const root = join7(this.#pinnedDir, "blobs");
19683
19932
  for (const shard of await entriesOf(root)) {
19684
19933
  const shardPath = join7(root, shard);
19685
19934
  if (shard === ".tmp") {
@@ -19698,13 +19947,13 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
19698
19947
  await rm3(join7(shardPath, name), { force: true });
19699
19948
  removed += 1;
19700
19949
  }
19701
- if (kept === 0) await rmdir2(shardPath).catch(() => void 0);
19950
+ if (kept === 0) await rmdir(shardPath).catch(() => void 0);
19702
19951
  }
19703
19952
  return removed;
19704
19953
  }
19705
19954
  /** Whether any content file (a blob or a temp file) is on disk. */
19706
19955
  async #holdsContentFiles() {
19707
- const root = join7(this.dir, "blobs");
19956
+ const root = join7(this.#pinnedDir, "blobs");
19708
19957
  for (const shard of await entriesOf(root)) {
19709
19958
  if ((await entriesOf(join7(root, shard))).length > 0) return true;
19710
19959
  }
@@ -19712,13 +19961,16 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
19712
19961
  }
19713
19962
  async collectGarbage(t) {
19714
19963
  return this.#mutate(async () => {
19964
+ const release = this.#shared ? await acquireWritableMutation(this.#shared) : void 0;
19715
19965
  try {
19716
19966
  this.#db.exec("BEGIN IMMEDIATE");
19717
19967
  } catch (error) {
19968
+ release?.();
19718
19969
  throw storageError(error, "Collecting unused content");
19719
19970
  }
19720
19971
  try {
19721
19972
  this.#checkLease(t);
19973
+ await this.#faults.afterGarbageCollectionBegin?.();
19722
19974
  const keep = new Set(
19723
19975
  this.#db.all("SELECT DISTINCT hash FROM entry WHERE content = 1").map((row) => row.hash)
19724
19976
  );
@@ -19731,12 +19983,14 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
19731
19983
  } catch {
19732
19984
  }
19733
19985
  throw storageError(error, "Collecting unused content");
19986
+ } finally {
19987
+ release?.();
19734
19988
  }
19735
19989
  });
19736
19990
  }
19737
19991
  async reset(t, reason, options = {}) {
19738
19992
  await this.#mutate(async () => {
19739
- this.#write("Resetting the replica", () => {
19993
+ await this.#write("Resetting the replica", () => {
19740
19994
  this.#checkLease(t);
19741
19995
  this.#db.run("DELETE FROM entry");
19742
19996
  this.#db.run(
@@ -19763,15 +20017,102 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
19763
20017
  */
19764
20018
  async destroy(t) {
19765
20019
  await this.#mutate(async () => {
20020
+ const release = this.#shared ? await acquireWritableMutation(this.#shared) : void 0;
19766
20021
  try {
19767
20022
  this.#db.exec("BEGIN IMMEDIATE");
19768
20023
  } catch (error) {
20024
+ release?.();
19769
20025
  throw storageError(error, "Removing the replica");
19770
20026
  }
19771
20027
  try {
19772
20028
  this.#checkLease(t, { allowRevoked: true });
20029
+ const expected = this.#directoryFd === void 0 ? this.#namedAncestors.at(-1) : fstatSync(this.#directoryFd, { bigint: true });
20030
+ if (!expected) {
20031
+ throw new ReplicaError2(ReplicaErrorCode2.RUNTIME_UNSUPPORTED, "Replica removal requires an opened or identity-checked directory.");
20032
+ }
20033
+ const assertDirectory2 = async () => {
20034
+ const current = await lstat2(this.dir, { bigint: true });
20035
+ if (!current.isDirectory() || current.dev !== expected.dev || current.ino !== expected.ino) {
20036
+ throw new ReplicaError2(ReplicaErrorCode2.NOT_FOUND, "The replica directory changed during removal.");
20037
+ }
20038
+ return current;
20039
+ };
20040
+ const pinned = await assertDirectory2();
19773
20041
  await this.#assertPresent();
19774
- await rm3(this.dir, { recursive: true, force: true });
20042
+ if (process.platform === "linux") {
20043
+ for (const entry of await readdir3(this.#pinnedDir)) {
20044
+ await rm3(join7(this.#pinnedDir, entry), { recursive: true, force: true });
20045
+ }
20046
+ await assertDirectory2();
20047
+ await rmdir(this.dir);
20048
+ } else {
20049
+ const ancestors = this.#namedAncestors;
20050
+ const checkAncestors = async () => {
20051
+ for (const expected2 of ancestors) {
20052
+ const current = await lstat2(expected2.path, { bigint: true });
20053
+ if (current.dev !== expected2.dev || current.ino !== expected2.ino) {
20054
+ throw new ReplicaError2(ReplicaErrorCode2.NOT_FOUND, "The replica ancestry changed during removal.");
20055
+ }
20056
+ }
20057
+ };
20058
+ const directories = /* @__PURE__ */ new Map([[this.dir, pinned]]);
20059
+ const files = /* @__PURE__ */ new Map();
20060
+ const check = async (path, expected2, directory) => {
20061
+ if (path !== this.dir) {
20062
+ const parent = dirname3(path);
20063
+ await check(parent, directories.get(parent), true);
20064
+ }
20065
+ await checkAncestors();
20066
+ const current = await lstat2(path, { bigint: true });
20067
+ if ((directory ? !current.isDirectory() : !current.isFile()) || current.dev !== expected2.dev || current.ino !== expected2.ino) {
20068
+ throw new ReplicaError2(ReplicaErrorCode2.NOT_FOUND, "The replica layout changed during removal.");
20069
+ }
20070
+ };
20071
+ const record2 = async (path, directory) => {
20072
+ const parent = dirname3(path);
20073
+ await check(parent, directories.get(parent), true);
20074
+ const current = await lstat2(path, { bigint: true });
20075
+ if (!(directory ? current.isDirectory() : current.isFile())) {
20076
+ throw new ReplicaError2(ReplicaErrorCode2.STORAGE_ERROR, "Unexpected replica layout during removal.");
20077
+ }
20078
+ (directory ? directories : files).set(path, current);
20079
+ };
20080
+ const blobs = join7(this.dir, "blobs");
20081
+ for (const name of await readdir3(this.dir)) {
20082
+ if (name === "blobs") await record2(blobs, true);
20083
+ else if (["replica.db", "replica.db-wal", "replica.db-shm"].includes(name)) await record2(join7(this.dir, name), false);
20084
+ else throw new ReplicaError2(ReplicaErrorCode2.STORAGE_ERROR, "Unexpected replica layout during removal.");
20085
+ }
20086
+ if (directories.has(blobs)) {
20087
+ await check(blobs, directories.get(blobs), true);
20088
+ for (const shard of await readdir3(blobs)) {
20089
+ if (shard !== ".tmp" && !/^[0-9a-f]{2}$/.test(shard)) {
20090
+ throw new ReplicaError2(ReplicaErrorCode2.STORAGE_ERROR, "Unexpected replica blob shard during removal.");
20091
+ }
20092
+ const shardPath = join7(blobs, shard);
20093
+ await record2(shardPath, true);
20094
+ await check(shardPath, directories.get(shardPath), true);
20095
+ for (const name of await readdir3(shardPath)) {
20096
+ if (shard === ".tmp" ? !/^[0-9a-f]{64}\.[0-9a-f]{12}$/.test(name) : !isHash(name) || !name.startsWith(shard)) {
20097
+ throw new ReplicaError2(ReplicaErrorCode2.STORAGE_ERROR, "Unexpected replica blob file during removal.");
20098
+ }
20099
+ await record2(join7(shardPath, name), false);
20100
+ }
20101
+ }
20102
+ }
20103
+ for (const [path, identity] of files) {
20104
+ await check(path, identity, false);
20105
+ await unlink(path);
20106
+ }
20107
+ for (const [path, identity] of [...directories].reverse()) {
20108
+ if (path === this.dir) continue;
20109
+ await check(path, identity, true);
20110
+ await rmdir(path);
20111
+ }
20112
+ await assertDirectory2();
20113
+ await checkAncestors();
20114
+ await rmdir(this.dir);
20115
+ }
19775
20116
  } catch (error) {
19776
20117
  throw storageError(error, "Removing the replica");
19777
20118
  } finally {
@@ -19779,6 +20120,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
19779
20120
  this.#db.exec("ROLLBACK");
19780
20121
  } catch {
19781
20122
  }
20123
+ release?.();
19782
20124
  }
19783
20125
  });
19784
20126
  }
@@ -19822,7 +20164,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
19822
20164
  * when both are done.
19823
20165
  */
19824
20166
  async #purgeContent() {
19825
- this.#write("Starting the revocation purge", () => {
20167
+ await this.#write("Starting the revocation purge", () => {
19826
20168
  this.#db.run("UPDATE replica SET purge_pending = 1 WHERE id = 1 AND revoked_detail IS NOT NULL");
19827
20169
  });
19828
20170
  try {
@@ -19833,7 +20175,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
19833
20175
  if (!this.#checkpoint()) {
19834
20176
  throw new ReplicaError2(ReplicaErrorCode2.BUSY, "A reader holds the replica's write-ahead log; the purge finishes on the next open.");
19835
20177
  }
19836
- this.#write("Finishing the revocation purge", () => {
20178
+ await this.#write("Finishing the revocation purge", () => {
19837
20179
  this.#db.run("UPDATE replica SET purge_pending = 0 WHERE id = 1 AND revoked_detail IS NOT NULL");
19838
20180
  });
19839
20181
  }
@@ -19843,6 +20185,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
19843
20185
  });
19844
20186
  }
19845
20187
  async get(key) {
20188
+ this.#assertAccepting();
19846
20189
  try {
19847
20190
  const row = this.#db.get("SELECT key, deleted, etag, hash, metadata, content FROM entry WHERE key = ?", key);
19848
20191
  return row === void 0 ? void 0 : entryOf(row);
@@ -19851,6 +20194,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
19851
20194
  }
19852
20195
  }
19853
20196
  async list(o) {
20197
+ this.#assertAccepting();
19854
20198
  const where = ["deleted = 0"];
19855
20199
  const params = [];
19856
20200
  if (o.prefix !== void 0 && o.prefix !== "") {
@@ -19936,8 +20280,31 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
19936
20280
  lastReset: state2.lastReset
19937
20281
  };
19938
20282
  }
19939
- async close() {
19940
- this.#db.close();
20283
+ close() {
20284
+ if (this.#closePromise) return this.#closePromise;
20285
+ this.#closing = true;
20286
+ this.#closePromise = (async () => {
20287
+ if (this.#activeMutations !== 0) {
20288
+ await new Promise((resolve22) => {
20289
+ this.#drained = resolve22;
20290
+ });
20291
+ }
20292
+ this.#closed = true;
20293
+ const fd = this.#directoryFd;
20294
+ this.#directoryFd = void 0;
20295
+ if (this.#shared) {
20296
+ sharedStores.unregister(this);
20297
+ releaseWritable(this.#shared);
20298
+ } else {
20299
+ if (fd !== void 0) pinnedDirectories.unregister(this);
20300
+ try {
20301
+ this.#database.close();
20302
+ } finally {
20303
+ if (fd !== void 0) closeSync(fd);
20304
+ }
20305
+ }
20306
+ })();
20307
+ return this.#closePromise;
19941
20308
  }
19942
20309
  };
19943
20310
 
@@ -19945,229 +20312,388 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
19945
20312
  init_errors();
19946
20313
  init_constants();
19947
20314
  var SYNC_LEASE_MS = 6e4;
19948
- function refuseUnsafeReplicaPath(profile, detail) {
20315
+ function refuseUnsafeReplicaPath(profile, detail, operation = "logout") {
19949
20316
  return new CLIError(
19950
20317
  "REPLICA_PURGE_FAILED",
19951
- `Logout for profile "${profile}" cleared the session, but ${detail}; the replicas were left untouched.`,
20318
+ operation === "logout" ? `Logout for profile "${profile}" cleared the session, but ${detail}; the replicas were left untouched.` : `Cannot reset a replica in profile "${profile}": ${detail}; no replica was removed.`,
19952
20319
  ExitCode.ERROR
19953
20320
  );
19954
20321
  }
19955
- async function safeReplicaRoot(profile, root) {
20322
+ async function safeReplicaRoot(profile, root, operation = "logout") {
19956
20323
  let rootStats;
19957
20324
  try {
19958
- rootStats = await lstat2(root);
20325
+ rootStats = root.startsWith("/proc/self/fd/") ? await stat3(root) : await lstat3(root);
19959
20326
  } catch (error) {
19960
20327
  if (error?.code === "ENOENT") return null;
19961
20328
  throw error;
19962
20329
  }
19963
20330
  if (rootStats.isSymbolicLink()) {
19964
- throw refuseUnsafeReplicaPath(profile, `the replicas directory "${root}" is a symlink`);
20331
+ throw refuseUnsafeReplicaPath(profile, `the replicas directory "${root}" is a symlink`, operation);
19965
20332
  }
19966
20333
  if (!rootStats.isDirectory()) {
19967
- throw refuseUnsafeReplicaPath(profile, `the replicas path "${root}" is not a real directory`);
20334
+ throw refuseUnsafeReplicaPath(profile, `the replicas path "${root}" is not a real directory`, operation);
19968
20335
  }
19969
- const [profileRealPath, rootRealPath] = await Promise.all([realpath(profilePath3(profile)), realpath(root)]);
20336
+ const [profileRealPath, rootRealPath] = await Promise.all([realpath(profilePath4(profile)), realpath(root)]);
19970
20337
  if (!rootRealPath.startsWith(profileRealPath + sep)) {
19971
- throw refuseUnsafeReplicaPath(profile, `the replicas directory "${root}" resolves outside the selected profile`);
20338
+ throw refuseUnsafeReplicaPath(profile, `the replicas directory "${root}" resolves outside the selected profile`, operation);
19972
20339
  }
19973
20340
  return rootRealPath;
19974
20341
  }
19975
- async function safeReplicaEntry(profile, root, entry) {
20342
+ async function safeReplicaEntry(profile, root, entry, operation = "logout") {
19976
20343
  const entryPath = join8(root, entry.name);
19977
- const rootRealPath = await safeReplicaRoot(profile, root);
20344
+ const rootRealPath = await safeReplicaRoot(profile, root, operation);
19978
20345
  if (rootRealPath === null) {
19979
- throw refuseUnsafeReplicaPath(profile, `the replicas directory "${root}" disappeared`);
20346
+ throw refuseUnsafeReplicaPath(profile, `the replicas directory "${root}" disappeared`, operation);
19980
20347
  }
19981
- const entryStats = await lstat2(entryPath);
20348
+ const entryStats = await lstat3(entryPath);
19982
20349
  if (entry.isSymbolicLink() || entryStats.isSymbolicLink()) {
19983
- throw refuseUnsafeReplicaPath(profile, `replica entry "${entry.name}" is a symlink`);
20350
+ throw refuseUnsafeReplicaPath(profile, `replica entry "${entry.name}" is a symlink`, operation);
19984
20351
  }
19985
20352
  if (!entry.isDirectory() || !entryStats.isDirectory()) {
19986
- throw refuseUnsafeReplicaPath(profile, `replica entry "${entry.name}" is not a real directory`);
20353
+ throw refuseUnsafeReplicaPath(profile, `replica entry "${entry.name}" is not a real directory`, operation);
19987
20354
  }
19988
20355
  const entryRealPath = await realpath(entryPath);
19989
20356
  if (!entryRealPath.startsWith(rootRealPath + sep)) {
19990
- throw refuseUnsafeReplicaPath(profile, `replica entry "${entry.name}" resolves outside the replicas directory`);
20357
+ throw refuseUnsafeReplicaPath(profile, `replica entry "${entry.name}" resolves outside the replicas directory`, operation);
19991
20358
  }
19992
- return entryPath;
20359
+ return { path: entryPath, identity: { dev: entryStats.dev, ino: entryStats.ino } };
19993
20360
  }
19994
- async function removeProfileReplicas(profile, heldLeases) {
20361
+ async function inspectLegacyReplica(profile, root, entry) {
20362
+ const { path, identity } = await safeReplicaEntry(profile, root, entry);
20363
+ const handle = await open3(path, fsConstants2.O_RDONLY | fsConstants2.O_DIRECTORY | fsConstants2.O_NOFOLLOW);
20364
+ try {
20365
+ const pinnedEntry = await handle.stat();
20366
+ if (pinnedEntry.dev !== identity.dev || pinnedEntry.ino !== identity.ino) {
20367
+ throw refuseUnsafeReplicaPath(profile, `replica entry "${entry.name}" changed during inspection`);
20368
+ }
20369
+ const children = await readdir4(`/proc/self/fd/${handle.fd}`, { withFileTypes: true });
20370
+ if (children.length > 0 && !children.some((child) => child.name === "replica.db" && child.isFile())) {
20371
+ throw refuseUnsafeReplicaPath(profile, `replica entry "${entry.name}" has files but no regular replica database`);
20372
+ }
20373
+ return { path, identity, empty: children.length === 0 };
20374
+ } finally {
20375
+ await handle.close();
20376
+ }
20377
+ }
20378
+ async function pinReplicaRoot(profile, named, operation) {
20379
+ const initial = await lstat3(named).catch((error) => {
20380
+ if (error.code === "ENOENT") return null;
20381
+ throw error;
20382
+ });
20383
+ if (await safeReplicaRoot(profile, named, operation) === null) return null;
20384
+ if (process.platform !== "linux") {
20385
+ throw refuseUnsafeReplicaPath(profile, "pinned replica removal is unavailable on this platform", operation);
20386
+ }
20387
+ const handle = await open3(named, fsConstants2.O_RDONLY | fsConstants2.O_DIRECTORY | fsConstants2.O_NOFOLLOW);
20388
+ try {
20389
+ const pinned = await handle.stat();
20390
+ const assertNamed = async () => {
20391
+ const current = await lstat3(named);
20392
+ if (!current.isDirectory() || current.dev !== initial?.dev || current.ino !== initial?.ino || current.dev !== pinned.dev || current.ino !== pinned.ino) {
20393
+ throw refuseUnsafeReplicaPath(profile, "the replicas directory changed during cleanup", operation);
20394
+ }
20395
+ };
20396
+ await assertNamed();
20397
+ return { path: `/proc/self/fd/${handle.fd}`, assertNamed, close: () => handle.close() };
20398
+ } catch (error) {
20399
+ await handle.close();
20400
+ throw error;
20401
+ }
20402
+ }
20403
+ async function withPinnedReplicaForReset(profile, name, action) {
19995
20404
  return withProfileLock4(profile, async () => {
19996
- const root = join8(profilePath3(profile), "replicas");
19997
- if (await safeReplicaRoot(profile, root) === null) return [];
19998
- const replicas = (await readdir4(root, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
19999
- if (replicas.length === 0) return [];
20000
- for (const entry of replicas) await safeReplicaEntry(profile, root, entry);
20001
- await loadSqlite();
20002
- const opened = [];
20405
+ const root = join8(profilePath4(profile), "replicas");
20406
+ const pinned = await pinReplicaRoot(profile, root, "reset");
20407
+ if (!pinned) throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, `Profile "${profile}" has no replicas.`);
20003
20408
  try {
20004
- for (const entry of replicas) {
20005
- const entryPath = await safeReplicaEntry(profile, root, entry);
20006
- const store = await SqliteReplicaStore.open(entryPath, {
20007
- create: false,
20008
- guard: async (action) => {
20009
- try {
20010
- return await withProfileLock4(profile, action, { requireProfile: true });
20011
- } catch (error) {
20012
- if (error instanceof ProfileDeletedError2) {
20013
- throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, `Profile "${profile}" was deleted; its replicas are gone and nothing was written.`);
20409
+ const fdRoot = pinned.path;
20410
+ const entry = (await readdir4(fdRoot, { withFileTypes: true })).find((item) => item.name === name);
20411
+ if (!entry) throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, `Replica "${name}" was not found.`);
20412
+ const { path, identity: initial } = await safeReplicaEntry(profile, fdRoot, entry, "reset");
20413
+ const recheck = async () => {
20414
+ const current = await lstat3(path);
20415
+ if (!current.isDirectory() || current.isSymbolicLink() || current.dev !== initial.dev || current.ino !== initial.ino) {
20416
+ throw refuseUnsafeReplicaPath(profile, `replica entry "${name}" changed during removal`, "reset");
20417
+ }
20418
+ };
20419
+ await recheck();
20420
+ return await action(path, recheck, { dev: initial.dev, ino: initial.ino });
20421
+ } finally {
20422
+ await pinned.close();
20423
+ }
20424
+ });
20425
+ }
20426
+ async function removeProfileReplicas(profile, heldLeases, preflightRoot) {
20427
+ return withProfileLock4(profile, async () => {
20428
+ const namedRoot = join8(profilePath4(profile), "replicas");
20429
+ const pinned = preflightRoot ?? await pinReplicaRoot(profile, namedRoot, "logout");
20430
+ if (!pinned) return [];
20431
+ if (preflightRoot) await pinned.assertNamed();
20432
+ try {
20433
+ const root = pinned.path;
20434
+ const replicas = (await readdir4(root, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
20435
+ if (replicas.length === 0) return [];
20436
+ const checked = await Promise.all(replicas.map(async (entry) => ({
20437
+ entry,
20438
+ ...await inspectLegacyReplica(profile, root, entry)
20439
+ })));
20440
+ const removed = [];
20441
+ const opened = [];
20442
+ try {
20443
+ for (const { entry, identity, empty: empty4 } of checked) {
20444
+ if (!empty4) continue;
20445
+ await pinned.assertNamed();
20446
+ const current = await safeReplicaEntry(profile, root, entry);
20447
+ if (current.identity.dev !== identity.dev || current.identity.ino !== identity.ino) {
20448
+ throw refuseUnsafeReplicaPath(profile, `empty replica entry "${entry.name}" changed during cleanup`);
20449
+ }
20450
+ const handle = await open3(current.path, fsConstants2.O_RDONLY | fsConstants2.O_DIRECTORY | fsConstants2.O_NOFOLLOW);
20451
+ try {
20452
+ const pinnedEntry = await handle.stat();
20453
+ if (pinnedEntry.dev !== identity.dev || pinnedEntry.ino !== identity.ino || (await readdir4(`/proc/self/fd/${handle.fd}`)).length !== 0) {
20454
+ throw refuseUnsafeReplicaPath(profile, `empty replica entry "${entry.name}" changed during cleanup`);
20455
+ }
20456
+ await pinned.assertNamed();
20457
+ const namedEntry = await lstat3(current.path);
20458
+ if (!namedEntry.isDirectory() || namedEntry.dev !== pinnedEntry.dev || namedEntry.ino !== pinnedEntry.ino) {
20459
+ throw refuseUnsafeReplicaPath(profile, `empty replica entry "${entry.name}" changed at removal`);
20460
+ }
20461
+ await rmdir2(current.path);
20462
+ } finally {
20463
+ await handle.close();
20464
+ }
20465
+ removed.push(entry.name);
20466
+ }
20467
+ if (removed.length === replicas.length) return removed;
20468
+ await loadSqlite();
20469
+ for (const { entry, empty: empty4 } of checked) {
20470
+ if (empty4) continue;
20471
+ const { path: entryPath, identity: expectedDirectory } = await safeReplicaEntry(profile, root, entry);
20472
+ const store = await SqliteReplicaStore.open(entryPath, {
20473
+ create: false,
20474
+ expectedDirectory,
20475
+ guard: async (action) => {
20476
+ try {
20477
+ return await withProfileLock4(profile, async () => {
20478
+ await safeReplicaEntry(profile, root, entry);
20479
+ return action();
20480
+ }, { requireProfile: true });
20481
+ } catch (error) {
20482
+ if (error instanceof ProfileDeletedError2) {
20483
+ throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, `Profile "${profile}" was deleted; its replicas are gone and nothing was written.`);
20484
+ }
20485
+ throw error;
20014
20486
  }
20015
- throw error;
20016
20487
  }
20488
+ });
20489
+ opened.push({ name: entry.name, store, lease: null });
20490
+ if (await store.open() === null) {
20491
+ throw new CLIError("REPLICA_PURGE_FAILED", `Replica "${entry.name}" has no initialized store; logout cleared the session but removed no replicas.`, ExitCode.ERROR);
20017
20492
  }
20018
- });
20019
- opened.push({ name: entry.name, store, lease: null });
20020
- if (await store.open() === null) {
20021
- throw new CLIError("REPLICA_PURGE_FAILED", `Replica "${entry.name}" has no initialized store; logout cleared the session but removed no replicas.`, ExitCode.ERROR);
20022
- }
20023
- const lease = heldLeases?.get(entryPath) ?? await store.acquireSyncLease(SYNC_LEASE_MS);
20024
- if (lease === null) {
20025
- throw new CLIError("REPLICA_BUSY", `Cannot log out: replica "${entry.name}" is syncing. The session was cleared and no replicas were removed.`, ExitCode.ERROR);
20493
+ const lease = heldLeases?.get(join8(namedRoot, entry.name)) ?? await store.acquireSyncLease(SYNC_LEASE_MS);
20494
+ if (lease === null) {
20495
+ throw new CLIError("REPLICA_BUSY", `Cannot log out: replica "${entry.name}" is syncing. The session was cleared and no replicas were removed.`, ExitCode.ERROR);
20496
+ }
20497
+ opened[opened.length - 1].lease = lease;
20026
20498
  }
20027
- opened[opened.length - 1].lease = lease;
20028
- }
20029
- const removed = [];
20030
- for (const replica of opened) {
20031
- try {
20032
- await replica.store.destroy(replica.lease);
20033
- removed.push(replica.name);
20034
- } catch (error) {
20035
- const replicaStillExists = await lstat2(join8(root, replica.name)).then(
20036
- () => true,
20037
- (statError) => statError.code !== "ENOENT"
20038
- );
20039
- if (!replicaStillExists) removed.push(replica.name);
20040
- const remaining = opened.filter(({ name }) => !removed.includes(name)).map(({ name }) => name);
20041
- throw new CLIError(
20042
- "REPLICA_PURGE_FAILED",
20043
- `Logout cleared the session, but replica removal failed. Removed: ${removed.length ? removed.join(", ") : "none"}. Not removed: ${remaining.join(", ") || "none"}. ${error instanceof Error ? error.message : String(error)}`,
20044
- ExitCode.ERROR,
20045
- { replicasRemoved: removed, replicasRemaining: remaining }
20046
- );
20499
+ for (const replica of opened) {
20500
+ try {
20501
+ await replica.store.destroy(replica.lease);
20502
+ removed.push(replica.name);
20503
+ } catch (error) {
20504
+ const replicaStillExists = await lstat3(join8(root, replica.name)).then(
20505
+ () => true,
20506
+ (statError) => statError.code !== "ENOENT"
20507
+ );
20508
+ if (!replicaStillExists) removed.push(replica.name);
20509
+ const remaining = opened.filter(({ name }) => !removed.includes(name)).map(({ name }) => name);
20510
+ throw new CLIError(
20511
+ "REPLICA_PURGE_FAILED",
20512
+ `Logout cleared the session, but replica removal failed. Removed: ${removed.length ? removed.join(", ") : "none"}. Not removed: ${remaining.join(", ") || "none"}. ${error instanceof Error ? error.message : String(error)}`,
20513
+ ExitCode.ERROR,
20514
+ { replicasRemoved: removed, replicasRemaining: remaining }
20515
+ );
20516
+ }
20047
20517
  }
20518
+ return removed;
20519
+ } catch (error) {
20520
+ if (error instanceof CLIError) throw error;
20521
+ throw new CLIError(
20522
+ "REPLICA_PURGE_FAILED",
20523
+ `Logout cleared the session, but replica cleanup could not finish. Removed: ${removed.length ? removed.join(", ") : "none"}. ${error instanceof Error ? error.message : String(error)}`,
20524
+ ExitCode.ERROR
20525
+ );
20526
+ } finally {
20527
+ await Promise.all(opened.map(async ({ name, store, lease }) => {
20528
+ if (lease !== null && heldLeases?.has(join8(namedRoot, name)) !== true) await store.releaseLease(lease).catch(() => void 0);
20529
+ await store.close().catch(() => void 0);
20530
+ }));
20048
20531
  }
20049
- return removed;
20050
- } catch (error) {
20051
- if (error instanceof CLIError) throw error;
20052
- throw new CLIError(
20053
- "REPLICA_PURGE_FAILED",
20054
- `Logout cleared the session, but replica cleanup could not start; no replicas were removed. ${error instanceof Error ? error.message : String(error)}`,
20055
- ExitCode.ERROR
20056
- );
20057
20532
  } finally {
20058
- await Promise.all(opened.map(async ({ name, store, lease }) => {
20059
- if (lease !== null && heldLeases?.has(join8(root, name)) !== true) await store.releaseLease(lease).catch(() => void 0);
20060
- await store.close().catch(() => void 0);
20061
- }));
20533
+ if (!preflightRoot) await pinned.close();
20062
20534
  }
20063
20535
  });
20064
20536
  }
20065
- async function removeProfileReplication(profile, heldLeases) {
20537
+ async function assertSafeFlagReplica(profile, root, partition, name) {
20538
+ const rootReal = await safeReplicaRoot(profile, root);
20539
+ if (rootReal === null) throw refuseUnsafeReplicaPath(profile, `the replication root "${root}" disappeared`);
20540
+ const partitionPath = join8(root, partition);
20541
+ const partitionStats = await lstat3(partitionPath);
20542
+ if (partitionStats.isSymbolicLink() || !partitionStats.isDirectory()) {
20543
+ throw refuseUnsafeReplicaPath(profile, `replication entry "${partition}" is not a real directory`);
20544
+ }
20545
+ const partitionReal = await realpath(partitionPath);
20546
+ if (!partitionReal.startsWith(rootReal + sep)) {
20547
+ throw refuseUnsafeReplicaPath(profile, `replication entry "${partition}" resolves outside the replication root`);
20548
+ }
20549
+ const replicasRoot = join8(partitionPath, "replicas");
20550
+ const replicasStats = await lstat3(replicasRoot);
20551
+ if (replicasStats.isSymbolicLink() || !replicasStats.isDirectory()) {
20552
+ throw refuseUnsafeReplicaPath(profile, `replication replicas path "${replicasRoot}" is not a real directory`);
20553
+ }
20554
+ const replicaPath = join8(replicasRoot, name);
20555
+ const replicaStats = await lstat3(replicaPath);
20556
+ if (replicaStats.isSymbolicLink() || !replicaStats.isDirectory()) {
20557
+ throw refuseUnsafeReplicaPath(profile, `replication replica "${name}" is not a real directory`);
20558
+ }
20559
+ const replicaReal = await realpath(replicaPath);
20560
+ if (!replicaReal.startsWith(partitionReal + sep)) {
20561
+ throw refuseUnsafeReplicaPath(profile, `replication replica "${name}" resolves outside the partition`);
20562
+ }
20563
+ const database = await lstat3(join8(replicaPath, "replica.db"));
20564
+ if (database.isSymbolicLink() || !database.isFile()) {
20565
+ throw refuseUnsafeReplicaPath(profile, `replication database "${name}" is not a regular file`);
20566
+ }
20567
+ return { dev: replicaStats.dev, ino: replicaStats.ino };
20568
+ }
20569
+ async function removeProfileReplication(profile, heldLeases, preflightRoot) {
20066
20570
  return withProfileLock4(profile, async () => {
20067
- const root = join8(profilePath3(profile), "replication");
20068
- const rootReal = await safeReplicaRoot(profile, root);
20069
- if (rootReal === null) return [];
20070
- const profileReal = await realpath(profilePath3(profile));
20071
- const partitions = await readdir4(root, { withFileTypes: true });
20072
- const opened = [];
20073
- const removed = [];
20074
- let deleting = false;
20571
+ const namedRoot = join8(profilePath4(profile), "replication");
20572
+ const pinned = preflightRoot ?? await pinReplicaRoot(profile, namedRoot, "logout");
20573
+ if (!pinned) return [];
20574
+ if (preflightRoot) await pinned.assertNamed();
20075
20575
  try {
20076
- for (const partition of partitions) {
20077
- const partitionPath = join8(root, partition.name);
20078
- const partitionStats = await lstat2(partitionPath);
20079
- if (partition.isSymbolicLink() || partitionStats.isSymbolicLink()) {
20080
- throw refuseUnsafeReplicaPath(profile, `replication entry "${partition.name}" is a symlink`);
20081
- }
20082
- if (partitionStats.isDirectory()) {
20083
- const partitionReal = await realpath(partitionPath);
20084
- if (!partitionReal.startsWith(rootReal + sep) || !partitionReal.startsWith(profileReal + sep)) {
20085
- throw refuseUnsafeReplicaPath(profile, `replication entry "${partition.name}" resolves outside the selected profile`);
20086
- }
20087
- const replicasRoot2 = join8(partitionPath, "replicas");
20088
- const replicasStats = await lstat2(replicasRoot2).catch((error) => {
20089
- if (error.code === "ENOENT") return null;
20090
- throw error;
20091
- });
20092
- if (!replicasStats) continue;
20093
- if (replicasStats.isSymbolicLink() || !replicasStats.isDirectory()) {
20094
- throw refuseUnsafeReplicaPath(profile, `replication replicas path "${replicasRoot2}" is not a real directory`);
20576
+ const root = pinned.path;
20577
+ const rootReal = await safeReplicaRoot(profile, root);
20578
+ const profileReal = await realpath(profilePath4(profile));
20579
+ const partitions = await readdir4(root, { withFileTypes: true });
20580
+ const opened = [];
20581
+ const removed = [];
20582
+ let deleting = false;
20583
+ try {
20584
+ for (const partition of partitions) {
20585
+ const partitionPath = join8(root, partition.name);
20586
+ const partitionStats = await lstat3(partitionPath);
20587
+ if (partition.isSymbolicLink() || partitionStats.isSymbolicLink()) {
20588
+ throw refuseUnsafeReplicaPath(profile, `replication entry "${partition.name}" is a symlink`);
20095
20589
  }
20096
- const replicas = await readdir4(replicasRoot2, { withFileTypes: true });
20097
- for (const replica of replicas) {
20098
- const replicaPath = join8(replicasRoot2, replica.name);
20099
- const replicaStats = await lstat2(replicaPath);
20100
- if (replica.isSymbolicLink() || replicaStats.isSymbolicLink() || !replicaStats.isDirectory()) {
20101
- throw refuseUnsafeReplicaPath(profile, `replication replica "${replica.name}" is not a real directory`);
20590
+ if (partitionStats.isDirectory()) {
20591
+ const partitionReal = await realpath(partitionPath);
20592
+ if (!partitionReal.startsWith(rootReal + sep) || !partitionReal.startsWith(profileReal + sep)) {
20593
+ throw refuseUnsafeReplicaPath(profile, `replication entry "${partition.name}" resolves outside the selected profile`);
20102
20594
  }
20103
- const replicaReal = await realpath(replicaPath);
20104
- if (!replicaReal.startsWith(rootReal + sep)) {
20105
- throw refuseUnsafeReplicaPath(profile, `replication replica "${replica.name}" resolves outside the replication root`);
20595
+ const replicasRoot = join8(partitionPath, "replicas");
20596
+ const replicasStats = await lstat3(replicasRoot).catch((error) => {
20597
+ if (error.code === "ENOENT") return null;
20598
+ throw error;
20599
+ });
20600
+ if (!replicasStats) continue;
20601
+ if (replicasStats.isSymbolicLink() || !replicasStats.isDirectory()) {
20602
+ throw refuseUnsafeReplicaPath(profile, `replication replicas path "${replicasRoot}" is not a real directory`);
20603
+ }
20604
+ const replicas = await readdir4(replicasRoot, { withFileTypes: true });
20605
+ for (const replica of replicas) {
20606
+ const replicaPath = join8(replicasRoot, replica.name);
20607
+ const replicaStats = await lstat3(replicaPath);
20608
+ if (replica.isSymbolicLink() || replicaStats.isSymbolicLink() || !replicaStats.isDirectory()) {
20609
+ throw refuseUnsafeReplicaPath(profile, `replication replica "${replica.name}" is not a real directory`);
20610
+ }
20611
+ const replicaReal = await realpath(replicaPath);
20612
+ if (!replicaReal.startsWith(rootReal + sep)) {
20613
+ throw refuseUnsafeReplicaPath(profile, `replication replica "${replica.name}" resolves outside the replication root`);
20614
+ }
20615
+ const database = await lstat3(join8(replicaPath, "replica.db")).catch((error) => {
20616
+ if (error.code === "ENOENT") return null;
20617
+ throw error;
20618
+ });
20619
+ if (!database) continue;
20620
+ if (database.isSymbolicLink() || !database.isFile()) {
20621
+ throw refuseUnsafeReplicaPath(profile, `replication database "${replica.name}" is not a regular file`);
20622
+ }
20106
20623
  }
20107
- const database = await lstat2(join8(replicaPath, "replica.db")).catch((error) => {
20624
+ } else if (!partitionStats.isFile()) {
20625
+ throw refuseUnsafeReplicaPath(profile, `replication path "${partition.name}" is not a regular file or directory`);
20626
+ }
20627
+ }
20628
+ const hasReplicaDatabase = await Promise.all(partitions.filter((entry) => entry.isDirectory()).map(async (partition) => {
20629
+ return Promise.all((await readdir4(join8(root, partition.name, "replicas"), { withFileTypes: true }).catch(() => [])).filter((entry) => entry.isDirectory()).map((entry) => lstat3(join8(root, partition.name, "replicas", entry.name, "replica.db")).then(() => true, () => false)));
20630
+ })).then((values) => values.some((partition) => partition.some(Boolean)));
20631
+ if (hasReplicaDatabase) await loadSqlite();
20632
+ for (const partition of partitions.filter((entry) => entry.isDirectory())) {
20633
+ const replicasRoot = join8(root, partition.name, "replicas");
20634
+ for (const entry of await readdir4(replicasRoot, { withFileTypes: true }).catch(() => [])) {
20635
+ if (!entry.isDirectory()) continue;
20636
+ const database = await lstat3(join8(replicasRoot, entry.name, "replica.db")).catch((error) => {
20108
20637
  if (error.code === "ENOENT") return null;
20109
20638
  throw error;
20110
20639
  });
20111
20640
  if (!database) continue;
20112
- if (database.isSymbolicLink() || !database.isFile()) {
20113
- throw refuseUnsafeReplicaPath(profile, `replication database "${replica.name}" is not a regular file`);
20641
+ const name = `replication/${partition.name}/${entry.name}`;
20642
+ const expectedDirectory = await assertSafeFlagReplica(profile, root, partition.name, entry.name);
20643
+ const store = await SqliteReplicaStore.open(join8(replicasRoot, entry.name), {
20644
+ create: false,
20645
+ expectedDirectory,
20646
+ guard: async (action) => withProfileLock4(profile, async () => {
20647
+ await assertSafeFlagReplica(profile, root, partition.name, entry.name);
20648
+ return action();
20649
+ }, { requireProfile: true })
20650
+ });
20651
+ try {
20652
+ if (await store.open() === null) {
20653
+ throw new CLIError("REPLICA_PURGE_FAILED", `Logout cleared the session, but flag replica "${name}" has no initialized store.`, ExitCode.ERROR);
20654
+ }
20655
+ const leasePath = join8(namedRoot, partition.name, "replicas", entry.name);
20656
+ const lease = heldLeases?.get(leasePath) ?? await store.acquireSyncLease(SYNC_LEASE_MS);
20657
+ if (lease === null) {
20658
+ throw new CLIError("REPLICA_BUSY", `Cannot log out: flag replica "${name}" is syncing. The session was cleared and no replicas were removed.`, ExitCode.ERROR);
20659
+ }
20660
+ opened.push({ name, path: leasePath, store, lease });
20661
+ } catch (error) {
20662
+ await store.close().catch(() => void 0);
20663
+ throw error;
20114
20664
  }
20115
20665
  }
20116
- } else if (!partitionStats.isFile()) {
20117
- throw refuseUnsafeReplicaPath(profile, `replication path "${partition.name}" is not a regular file or directory`);
20118
20666
  }
20119
- }
20120
- const hasReplicaDatabase = await Promise.all(partitions.filter((entry) => entry.isDirectory()).map(async (partition) => {
20121
- return Promise.all((await readdir4(join8(root, partition.name, "replicas"), { withFileTypes: true }).catch(() => [])).filter((entry) => entry.isDirectory()).map((entry) => lstat2(join8(root, partition.name, "replicas", entry.name, "replica.db")).then(() => true, () => false)));
20122
- })).then((values) => values.some((partition) => partition.some(Boolean)));
20123
- if (hasReplicaDatabase) await loadSqlite();
20124
- for (const partition of partitions.filter((entry) => entry.isDirectory())) {
20125
- const replicasRoot2 = join8(root, partition.name, "replicas");
20126
- for (const entry of await readdir4(replicasRoot2, { withFileTypes: true }).catch(() => [])) {
20127
- if (!entry.isDirectory()) continue;
20128
- const database = await lstat2(join8(replicasRoot2, entry.name, "replica.db")).catch((error) => {
20129
- if (error.code === "ENOENT") return null;
20130
- throw error;
20131
- });
20132
- if (!database) continue;
20133
- const name = `replication/${partition.name}/${entry.name}`;
20134
- const store = await SqliteReplicaStore.open(join8(replicasRoot2, entry.name), { create: false, guard: async (action) => withProfileLock4(profile, action, { requireProfile: true }) });
20135
- if (await store.open() === null) {
20136
- await store.close();
20137
- throw new CLIError("REPLICA_PURGE_FAILED", `Logout cleared the session, but flag replica "${name}" has no initialized store.`, ExitCode.ERROR);
20138
- }
20139
- const lease = heldLeases?.get(join8(replicasRoot2, entry.name)) ?? await store.acquireSyncLease(SYNC_LEASE_MS);
20140
- if (lease === null) {
20141
- await store.close();
20142
- throw new CLIError("REPLICA_BUSY", `Cannot log out: flag replica "${name}" is syncing. The session was cleared and no replicas were removed.`, ExitCode.ERROR);
20143
- }
20144
- opened.push({ name, path: join8(replicasRoot2, entry.name), store, lease });
20667
+ deleting = true;
20668
+ for (const item of opened) {
20669
+ await item.store.destroy(item.lease);
20670
+ removed.push(item.name);
20145
20671
  }
20672
+ await pinned.assertNamed();
20673
+ for (const entry of await readdir4(root)) await rm4(join8(root, entry), { recursive: true, force: true });
20674
+ await pinned.assertNamed();
20675
+ await rmdir2(namedRoot);
20676
+ return removed;
20677
+ } catch (error) {
20678
+ if (deleting) {
20679
+ const remaining = opened.filter(({ name }) => !removed.includes(name)).map(({ name }) => name);
20680
+ throw new CLIError(
20681
+ "REPLICA_PURGE_FAILED",
20682
+ `Logout cleared the session, but replication cleanup failed. Removed: ${removed.join(", ") || "none"}. Not removed: ${remaining.join(", ") || "none"}. ${error instanceof Error ? error.message : String(error)}`,
20683
+ ExitCode.ERROR,
20684
+ { replicasRemoved: removed, replicasRemaining: remaining }
20685
+ );
20686
+ }
20687
+ if (error instanceof CLIError) throw error;
20688
+ throw new CLIError("REPLICA_PURGE_FAILED", `Logout cleared the session, but replication cleanup failed; no flag replicas were removed. ${error instanceof Error ? error.message : String(error)}`, ExitCode.ERROR);
20689
+ } finally {
20690
+ await Promise.all(opened.map(async ({ path, store, lease }) => {
20691
+ if (heldLeases?.has(path) !== true) await store.releaseLease(lease).catch(() => void 0);
20692
+ await store.close().catch(() => void 0);
20693
+ }));
20146
20694
  }
20147
- deleting = true;
20148
- for (const item of opened) {
20149
- await item.store.destroy(item.lease);
20150
- removed.push(item.name);
20151
- }
20152
- await rm4(root, { recursive: true });
20153
- return removed;
20154
- } catch (error) {
20155
- if (deleting) {
20156
- const remaining = opened.filter(({ name }) => !removed.includes(name)).map(({ name }) => name);
20157
- throw new CLIError(
20158
- "REPLICA_PURGE_FAILED",
20159
- `Logout cleared the session, but replication cleanup failed. Removed: ${removed.join(", ") || "none"}. Not removed: ${remaining.join(", ") || "none"}. ${error instanceof Error ? error.message : String(error)}`,
20160
- ExitCode.ERROR,
20161
- { replicasRemoved: removed, replicasRemaining: remaining }
20162
- );
20163
- }
20164
- if (error instanceof CLIError) throw error;
20165
- throw new CLIError("REPLICA_PURGE_FAILED", `Logout cleared the session, but replication cleanup failed; no flag replicas were removed. ${error instanceof Error ? error.message : String(error)}`, ExitCode.ERROR);
20166
20695
  } finally {
20167
- await Promise.all(opened.map(async ({ path, store, lease }) => {
20168
- if (heldLeases?.has(path) !== true) await store.releaseLease(lease).catch(() => void 0);
20169
- await store.close().catch(() => void 0);
20170
- }));
20696
+ if (!preflightRoot) await pinned.close();
20171
20697
  }
20172
20698
  });
20173
20699
  }
@@ -20175,36 +20701,54 @@ async function acquireLogoutLeases(profile) {
20175
20701
  return withProfileLock4(profile, async () => {
20176
20702
  let sqliteLoaded = false;
20177
20703
  const opened = [];
20704
+ const roots = /* @__PURE__ */ new Map();
20178
20705
  try {
20179
- const legacyRoot = join8(profilePath3(profile), "replicas");
20180
- if (await safeReplicaRoot(profile, legacyRoot) !== null) {
20706
+ const legacyNamed = join8(profilePath4(profile), "replicas");
20707
+ const legacyPin = await pinReplicaRoot(profile, legacyNamed, "logout");
20708
+ if (legacyPin) {
20709
+ roots.set("replicas", legacyPin);
20710
+ const legacyRoot = legacyPin.path;
20181
20711
  const entries = (await readdir4(legacyRoot, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
20182
20712
  for (const entry of entries) {
20183
- const path = await safeReplicaEntry(profile, legacyRoot, entry);
20713
+ const { path, identity: expectedDirectory, empty: empty4 } = await inspectLegacyReplica(profile, legacyRoot, entry);
20714
+ if (empty4) continue;
20184
20715
  if (!sqliteLoaded) {
20185
20716
  await loadSqlite();
20186
20717
  sqliteLoaded = true;
20187
20718
  }
20188
- const store = await SqliteReplicaStore.open(path, { create: false, guard: async (action) => withProfileLock4(profile, action, { requireProfile: true }) });
20189
- if (await store.open() === null) {
20190
- await store.close();
20191
- throw new CLIError("REPLICA_PURGE_FAILED", `Replica "${entry.name}" has no initialized store; logout cleared the session but removed no replicas.`, ExitCode.ERROR);
20192
- }
20193
- const lease = await store.acquireSyncLease(SYNC_LEASE_MS);
20194
- if (lease === null) {
20195
- await store.close();
20196
- throw new CLIError("REPLICA_BUSY", `Cannot log out: replica "${entry.name}" is syncing. The session was cleared and no replicas were removed.`, ExitCode.ERROR);
20719
+ const store = await SqliteReplicaStore.open(path, {
20720
+ create: false,
20721
+ expectedDirectory,
20722
+ guard: async (action) => withProfileLock4(profile, async () => {
20723
+ await safeReplicaEntry(profile, legacyRoot, entry);
20724
+ return action();
20725
+ }, { requireProfile: true })
20726
+ });
20727
+ try {
20728
+ if (await store.open() === null) {
20729
+ throw new CLIError("REPLICA_PURGE_FAILED", `Replica "${entry.name}" has no initialized store; logout cleared the session but removed no replicas.`, ExitCode.ERROR);
20730
+ }
20731
+ const lease = await store.acquireSyncLease(SYNC_LEASE_MS);
20732
+ if (lease === null) {
20733
+ throw new CLIError("REPLICA_BUSY", `Cannot log out: replica "${entry.name}" is syncing. The session was cleared and no replicas were removed.`, ExitCode.ERROR);
20734
+ }
20735
+ opened.push({ path: join8(legacyNamed, entry.name), store, lease });
20736
+ } catch (error) {
20737
+ await store.close().catch(() => void 0);
20738
+ throw error;
20197
20739
  }
20198
- opened.push({ path, store, lease });
20199
20740
  }
20200
20741
  }
20201
- const replicationRoot = join8(profilePath3(profile), "replication");
20202
- const replicationReal = await safeReplicaRoot(profile, replicationRoot);
20203
- if (replicationReal !== null) {
20742
+ const replicationNamed = join8(profilePath4(profile), "replication");
20743
+ const replicationPin = await pinReplicaRoot(profile, replicationNamed, "logout");
20744
+ if (replicationPin) {
20745
+ roots.set("replication", replicationPin);
20746
+ const replicationRoot = replicationPin.path;
20747
+ const replicationReal = await safeReplicaRoot(profile, replicationRoot);
20204
20748
  const partitions = await readdir4(replicationRoot, { withFileTypes: true });
20205
20749
  for (const partition of partitions) {
20206
20750
  const partitionPath = join8(replicationRoot, partition.name);
20207
- const partitionStats = await lstat2(partitionPath);
20751
+ const partitionStats = await lstat3(partitionPath);
20208
20752
  if (partition.isSymbolicLink() || partitionStats.isSymbolicLink()) {
20209
20753
  throw refuseUnsafeReplicaPath(profile, `replication entry "${partition.name}" is a symlink`);
20210
20754
  }
@@ -20216,22 +20760,22 @@ async function acquireLogoutLeases(profile) {
20216
20760
  if (!partitionReal.startsWith(replicationReal + sep)) {
20217
20761
  throw refuseUnsafeReplicaPath(profile, `replication entry "${partition.name}" resolves outside the replication root`);
20218
20762
  }
20219
- const replicasRoot2 = join8(partitionPath, "replicas");
20220
- const replicasStats = await lstat2(replicasRoot2).catch((error) => {
20763
+ const replicasRoot = join8(partitionPath, "replicas");
20764
+ const replicasStats = await lstat3(replicasRoot).catch((error) => {
20221
20765
  if (error.code === "ENOENT") return null;
20222
20766
  throw error;
20223
20767
  });
20224
20768
  if (!replicasStats) continue;
20225
20769
  if (replicasStats.isSymbolicLink() || !replicasStats.isDirectory()) {
20226
- throw refuseUnsafeReplicaPath(profile, `replication replicas path "${replicasRoot2}" is not a real directory`);
20770
+ throw refuseUnsafeReplicaPath(profile, `replication replicas path "${replicasRoot}" is not a real directory`);
20227
20771
  }
20228
- for (const replica of await readdir4(replicasRoot2, { withFileTypes: true })) {
20229
- const path = join8(replicasRoot2, replica.name);
20230
- const stats = await lstat2(path);
20772
+ for (const replica of await readdir4(replicasRoot, { withFileTypes: true })) {
20773
+ const path = join8(replicasRoot, replica.name);
20774
+ const stats = await lstat3(path);
20231
20775
  if (!replica.isDirectory() || replica.isSymbolicLink() || stats.isSymbolicLink() || !stats.isDirectory()) {
20232
20776
  throw refuseUnsafeReplicaPath(profile, `replication replica "${replica.name}" is not a real directory`);
20233
20777
  }
20234
- const database = await lstat2(join8(path, "replica.db")).catch((error) => {
20778
+ const database = await lstat3(join8(path, "replica.db")).catch((error) => {
20235
20779
  if (error.code === "ENOENT") return null;
20236
20780
  throw error;
20237
20781
  });
@@ -20247,26 +20791,38 @@ async function acquireLogoutLeases(profile) {
20247
20791
  await loadSqlite();
20248
20792
  sqliteLoaded = true;
20249
20793
  }
20250
- const store = await SqliteReplicaStore.open(path, { create: false, guard: async (action) => withProfileLock4(profile, action, { requireProfile: true }) });
20251
- if (await store.open() === null) {
20252
- await store.close();
20253
- throw new CLIError("REPLICA_PURGE_FAILED", `Logout cleared the session, but flag replica "replication/${partition.name}/${replica.name}" has no initialized store.`, ExitCode.ERROR);
20254
- }
20255
- const lease = await store.acquireSyncLease(SYNC_LEASE_MS);
20256
- if (lease === null) {
20257
- await store.close();
20258
- throw new CLIError("REPLICA_BUSY", `Cannot log out: flag replica "replication/${partition.name}/${replica.name}" is syncing. The session was cleared and no replicas were removed.`, ExitCode.ERROR);
20794
+ const expectedDirectory = await assertSafeFlagReplica(profile, replicationRoot, partition.name, replica.name);
20795
+ const store = await SqliteReplicaStore.open(path, {
20796
+ create: false,
20797
+ expectedDirectory,
20798
+ guard: async (action) => withProfileLock4(profile, async () => {
20799
+ await assertSafeFlagReplica(profile, replicationRoot, partition.name, replica.name);
20800
+ return action();
20801
+ }, { requireProfile: true })
20802
+ });
20803
+ try {
20804
+ if (await store.open() === null) {
20805
+ throw new CLIError("REPLICA_PURGE_FAILED", `Logout cleared the session, but flag replica "replication/${partition.name}/${replica.name}" has no initialized store.`, ExitCode.ERROR);
20806
+ }
20807
+ const lease = await store.acquireSyncLease(SYNC_LEASE_MS);
20808
+ if (lease === null) {
20809
+ throw new CLIError("REPLICA_BUSY", `Cannot log out: flag replica "replication/${partition.name}/${replica.name}" is syncing. The session was cleared and no replicas were removed.`, ExitCode.ERROR);
20810
+ }
20811
+ opened.push({ path: join8(replicationNamed, partition.name, "replicas", replica.name), store, lease });
20812
+ } catch (error) {
20813
+ await store.close().catch(() => void 0);
20814
+ throw error;
20259
20815
  }
20260
- opened.push({ path, store, lease });
20261
20816
  }
20262
20817
  }
20263
20818
  }
20264
- return opened;
20819
+ return { stores: opened, roots };
20265
20820
  } catch (error) {
20266
20821
  await Promise.all(opened.map(async ({ store, lease }) => {
20267
20822
  await store.releaseLease(lease).catch(() => void 0);
20268
20823
  await store.close().catch(() => void 0);
20269
20824
  }));
20825
+ await Promise.all([...roots.values()].map((root) => root.close()));
20270
20826
  throw error;
20271
20827
  }
20272
20828
  });
@@ -20274,31 +20830,32 @@ async function acquireLogoutLeases(profile) {
20274
20830
  async function removeProfileReplicasAndReplication(profile) {
20275
20831
  return withProfileLock4(profile, async () => {
20276
20832
  const acquired = await acquireLogoutLeases(profile);
20277
- const heldLeases = new Map(acquired.map(({ path, lease }) => [path, lease]));
20833
+ const heldLeases = new Map(acquired.stores.map(({ path, lease }) => [path, lease]));
20278
20834
  let legacyRemoved = [];
20279
20835
  try {
20280
- legacyRemoved = await removeProfileReplicas(profile, heldLeases);
20281
- const replication = await removeProfileReplication(profile, heldLeases);
20836
+ legacyRemoved = await removeProfileReplicas(profile, heldLeases, acquired.roots.get("replicas"));
20837
+ const replication = await removeProfileReplication(profile, heldLeases, acquired.roots.get("replication"));
20282
20838
  return [...legacyRemoved, ...replication];
20283
20839
  } catch (error) {
20284
20840
  const partialRemoved = error instanceof CLIError && Array.isArray(error.metadata?.replicasRemoved) ? error.metadata.replicasRemoved.filter((name) => typeof name === "string") : [];
20285
20841
  const removed = [.../* @__PURE__ */ new Set([...legacyRemoved, ...partialRemoved])];
20286
20842
  const removedSet = new Set(removed);
20287
- const remaining = acquired.map(({ path }) => {
20288
- const name = relative(profilePath3(profile), path).split(sep).join("/");
20843
+ const remaining = acquired.stores.map(({ path }) => {
20844
+ const name = relative(profilePath4(profile), path).split(sep).join("/");
20289
20845
  return name.startsWith("replication/") ? name.replace("/replicas/", "/") : name.split("/").at(-1);
20290
20846
  }).filter((name) => !removedSet.has(name));
20291
20847
  throw new CLIError(
20292
20848
  error instanceof CLIError ? error.code : "REPLICA_PURGE_FAILED",
20293
- `Logout cleared the session, but replica cleanup failed. Removed: ${removed.join(", ") || "none"}. Not removed: ${remaining.join(", ") || "none"}.`,
20849
+ `Logout cleared the session, but replica cleanup failed. Removed: ${removed.join(", ") || "none"}. Not removed: ${remaining.join(", ") || "none"}. ${error instanceof Error ? error.message : String(error)}`,
20294
20850
  error instanceof CLIError ? error.exitCode : ExitCode.ERROR,
20295
20851
  { replicasRemoved: removed, replicasRemaining: remaining }
20296
20852
  );
20297
20853
  } finally {
20298
- await Promise.all(acquired.map(async ({ store, lease }) => {
20854
+ await Promise.all(acquired.stores.map(async ({ store, lease }) => {
20299
20855
  await store.releaseLease(lease).catch(() => void 0);
20300
20856
  await store.close().catch(() => void 0);
20301
20857
  }));
20858
+ await Promise.all([...acquired.roots.values()].map((root) => root.close()));
20302
20859
  }
20303
20860
  });
20304
20861
  }
@@ -20469,7 +21026,7 @@ function shouldOpenBrowser(options) {
20469
21026
  return env !== "1" && env !== "true";
20470
21027
  }
20471
21028
  async function callbackFlow(did, options = {}) {
20472
- return new Promise((resolve5, reject) => {
21029
+ return new Promise((resolve6, reject) => {
20473
21030
  let timeout;
20474
21031
  let settled = false;
20475
21032
  let rl;
@@ -20483,7 +21040,7 @@ async function callbackFlow(did, options = {}) {
20483
21040
  rl.close();
20484
21041
  }
20485
21042
  if (result.data) {
20486
- resolve5(result.data);
21043
+ resolve6(result.data);
20487
21044
  } else {
20488
21045
  reject(result.error);
20489
21046
  }
@@ -20562,8 +21119,8 @@ async function callbackFlow(did, options = {}) {
20562
21119
  }
20563
21120
  if (openBrowser) {
20564
21121
  try {
20565
- const open8 = (await import("open")).default;
20566
- await open8(authUrl);
21122
+ const open9 = (await import("open")).default;
21123
+ await open9(authUrl);
20567
21124
  } catch {
20568
21125
  settle({ error: new Error("Failed to open browser") });
20569
21126
  return;
@@ -20607,13 +21164,13 @@ Open this URL in a browser to authenticate:
20607
21164
  input: process.stdin,
20608
21165
  output: process.stderr
20609
21166
  });
20610
- return new Promise((resolve5, reject) => {
21167
+ return new Promise((resolve6, reject) => {
20611
21168
  let answered = false;
20612
21169
  rl.on("line", (input) => {
20613
21170
  if (answered || input.trim() === "") return;
20614
21171
  answered = true;
20615
21172
  rl.close();
20616
- void delegationFromInput(input, did, options).then(resolve5, reject);
21173
+ void delegationFromInput(input, did, options).then(resolve6, reject);
20617
21174
  });
20618
21175
  rl.on("close", () => {
20619
21176
  if (answered) return;
@@ -21514,7 +22071,7 @@ async function acquireDeviceDelegation(input) {
21514
22071
  expiresAt: new Date(deadline).toISOString().replace(/\.\d{3}Z$/, "Z")
21515
22072
  });
21516
22073
  let interval = started.interval;
21517
- const wait = input.wait ?? ((milliseconds) => new Promise((resolve5) => setTimeout(resolve5, milliseconds)));
22074
+ const wait = input.wait ?? ((milliseconds) => new Promise((resolve6) => setTimeout(resolve6, milliseconds)));
21518
22075
  while (Date.now() < deadline) {
21519
22076
  await wait(interval * 1e3);
21520
22077
  let response;
@@ -22150,7 +22707,7 @@ async function promptAuthMethod() {
22150
22707
  input: process.stdin,
22151
22708
  output: process.stderr
22152
22709
  });
22153
- return new Promise((resolve5) => {
22710
+ return new Promise((resolve6) => {
22154
22711
  process.stderr.write("\n" + theme.heading("Choose authentication method:") + "\n");
22155
22712
  process.stderr.write(` ${theme.accent("1)")} OpenKey ${theme.muted("(browser-based, for interactive use)")}
22156
22713
  `);
@@ -22161,9 +22718,9 @@ async function promptAuthMethod() {
22161
22718
  rl.close();
22162
22719
  const trimmed = answer.trim();
22163
22720
  if (trimmed === "2" || trimmed.toLowerCase() === "local") {
22164
- resolve5("local");
22721
+ resolve6("local");
22165
22722
  } else {
22166
- resolve5("openkey");
22723
+ resolve6("openkey");
22167
22724
  }
22168
22725
  });
22169
22726
  });
@@ -22865,7 +23422,7 @@ function describeSession(state2, expiresAt) {
22865
23422
  }
22866
23423
  async function emitPermissionRequestArtifact(artifact, emitOption) {
22867
23424
  if (typeof emitOption === "string" && emitOption.length > 0) {
22868
- await mkdir3(dirname3(emitOption), { recursive: true });
23425
+ await mkdir3(dirname4(emitOption), { recursive: true });
22869
23426
  await writeFile2(emitOption, JSON.stringify(artifact, null, 2) + "\n", "utf8");
22870
23427
  outputJson({
22871
23428
  emitted: true,
@@ -22901,13 +23458,13 @@ async function readStdin() {
22901
23458
  return Buffer.concat(chunks).toString("utf8");
22902
23459
  }
22903
23460
  function readUrl(source) {
22904
- return new Promise((resolve5, reject) => {
23461
+ return new Promise((resolve6, reject) => {
22905
23462
  const getter = source.startsWith("https://") ? httpsGet : httpGet;
22906
23463
  const request = getter(source, (response) => {
22907
23464
  const status = response.statusCode ?? 0;
22908
23465
  if (status >= 300 && status < 400 && response.headers.location) {
22909
23466
  response.resume();
22910
- readUrl(new URL(response.headers.location, source).toString()).then(resolve5, reject);
23467
+ readUrl(new URL(response.headers.location, source).toString()).then(resolve6, reject);
22911
23468
  return;
22912
23469
  }
22913
23470
  if (status < 200 || status >= 300) {
@@ -22923,7 +23480,7 @@ function readUrl(source) {
22923
23480
  response.on("data", (chunk) => {
22924
23481
  chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
22925
23482
  });
22926
- response.on("end", () => resolve5(Buffer.concat(chunks).toString("utf8")));
23483
+ response.on("end", () => resolve6(Buffer.concat(chunks).toString("utf8")));
22927
23484
  });
22928
23485
  request.on("error", reject);
22929
23486
  });
@@ -23163,7 +23720,7 @@ function permissionGrantReason(context, permissions) {
23163
23720
  return `${context} Requested: ${summary}${more}.`;
23164
23721
  }
23165
23722
  function execCapturedCommand(command) {
23166
- return new Promise((resolve5, reject) => {
23723
+ return new Promise((resolve6, reject) => {
23167
23724
  const child = spawn(process.execPath, [process.argv[1], ...command.argv], {
23168
23725
  cwd: command.cwd,
23169
23726
  env: process.env,
@@ -23182,7 +23739,7 @@ function execCapturedCommand(command) {
23182
23739
  if (code3 && code3 !== 0) {
23183
23740
  process.exitCode = code3;
23184
23741
  }
23185
- resolve5();
23742
+ resolve6();
23186
23743
  });
23187
23744
  });
23188
23745
  }
@@ -23211,8 +23768,8 @@ async function confirmPermissionRequest(permissions) {
23211
23768
  input: process.stdin,
23212
23769
  output: process.stderr
23213
23770
  });
23214
- const answer = await new Promise((resolve5) => {
23215
- rl.question("Approve local-key delegation? [y/N] ", resolve5);
23771
+ const answer = await new Promise((resolve6) => {
23772
+ rl.question("Approve local-key delegation? [y/N] ", resolve6);
23216
23773
  });
23217
23774
  rl.close();
23218
23775
  if (!/^y(es)?$/i.test(answer.trim())) {
@@ -24057,7 +24614,7 @@ init_errors();
24057
24614
  init_sdk();
24058
24615
  init_theme();
24059
24616
  import { readFile as readFile6, writeFile as writeFile3 } from "fs/promises";
24060
- import { resolve } from "path";
24617
+ import { resolve as resolve2 } from "path";
24061
24618
  function registerDuckdbCommand(program) {
24062
24619
  const duckdb = program.command("duckdb").description("DuckDB database operations");
24063
24620
  duckdb.command("query <sql>").description("Run a SELECT query").option("--db <name>", "Database name", "default").option("--params <json>", "Bind parameters as JSON array").action(async (sqlStr, options, cmd) => {
@@ -24169,7 +24726,7 @@ ${rowCount} row${rowCount === 1 ? "" : "s"} returned`) + "\n");
24169
24726
  }
24170
24727
  const blob = result.data;
24171
24728
  const buffer = Buffer.from(await blob.arrayBuffer());
24172
- const outputPath = resolve(options.output);
24729
+ const outputPath = resolve2(options.output);
24173
24730
  await writeFile3(outputPath, buffer);
24174
24731
  outputJson({
24175
24732
  file: outputPath,
@@ -24185,7 +24742,7 @@ ${rowCount} row${rowCount === 1 ? "" : "s"} returned`) + "\n");
24185
24742
  const globalOpts = cmd.optsWithGlobals();
24186
24743
  const ctx = await ProfileManager.resolveContext(globalOpts);
24187
24744
  const node = await ensureAuthenticated(ctx);
24188
- const filePath = resolve(file);
24745
+ const filePath = resolve2(file);
24189
24746
  const bytes = new Uint8Array(await readFile6(filePath));
24190
24747
  const result = await withSpinner(
24191
24748
  "Importing database...",
@@ -24327,11 +24884,11 @@ init_storage();
24327
24884
  init_errors();
24328
24885
  init_constants();
24329
24886
  import { randomUUID as randomUUID2 } from "crypto";
24330
- import { lstat as lstat3, open as open3, rename as rename2, rm as rm5 } from "fs/promises";
24331
- import { basename as basename2, dirname as dirname4, join as join9 } from "path";
24887
+ import { lstat as lstat4, open as open4, rename as rename2, rm as rm5 } from "fs/promises";
24888
+ import { basename as basename2, dirname as dirname5, join as join9 } from "path";
24332
24889
  async function validatePrivateOutput(path, label = "output") {
24333
24890
  try {
24334
- const destination = await lstat3(path);
24891
+ const destination = await lstat4(path);
24335
24892
  if (!destination.isFile()) {
24336
24893
  throw new CLIError("INVALID_ARGUMENT", `${label} "${path}" must be a regular file, not a symlink, directory, or device.`, ExitCode.USAGE_ERROR);
24337
24894
  }
@@ -24339,7 +24896,7 @@ async function validatePrivateOutput(path, label = "output") {
24339
24896
  if (error?.code !== "ENOENT") throw error;
24340
24897
  }
24341
24898
  try {
24342
- const parent = await lstat3(dirname4(path));
24899
+ const parent = await lstat4(dirname5(path));
24343
24900
  if (!parent.isDirectory()) {
24344
24901
  throw new CLIError("INVALID_ARGUMENT", `${label} "${path}" requires an existing directory parent.`, ExitCode.USAGE_ERROR);
24345
24902
  }
@@ -24350,11 +24907,11 @@ async function validatePrivateOutput(path, label = "output") {
24350
24907
  }
24351
24908
  async function writePrivateOutput(path, value, label = "output") {
24352
24909
  await validatePrivateOutput(path, label);
24353
- const parentPath = dirname4(path);
24910
+ const parentPath = dirname5(path);
24354
24911
  const temp = join9(parentPath, `.${basename2(path)}.${randomUUID2()}.tmp`);
24355
24912
  let created = false;
24356
24913
  try {
24357
- const handle = await open3(temp, "wx", PRIVATE_FILE_MODE);
24914
+ const handle = await open4(temp, "wx", PRIVATE_FILE_MODE);
24358
24915
  created = true;
24359
24916
  try {
24360
24917
  await handle.writeFile(value);
@@ -24369,7 +24926,7 @@ async function writePrivateOutput(path, value, label = "output") {
24369
24926
  throw new CLIError("ERROR", `Could not write ${label.toLowerCase()} "${path}"${code3}.`, ExitCode.ERROR);
24370
24927
  }
24371
24928
  try {
24372
- const parent = await open3(parentPath, "r");
24929
+ const parent = await open4(parentPath, "r");
24373
24930
  try {
24374
24931
  await parent.sync();
24375
24932
  } finally {
@@ -25009,8 +25566,8 @@ Examples:
25009
25566
  try {
25010
25567
  if (isInteractive()) {
25011
25568
  const rl = createInterface3({ input: process.stdin, output: process.stderr });
25012
- const answer = await new Promise((resolve5) => {
25013
- rl.question(`Delete profile "${name}"? This cannot be undone. [y/N] `, resolve5);
25569
+ const answer = await new Promise((resolve6) => {
25570
+ rl.question(`Delete profile "${name}"? This cannot be undone. [y/N] `, resolve6);
25014
25571
  });
25015
25572
  rl.close();
25016
25573
  if (answer.toLowerCase() !== "y") {
@@ -25045,22 +25602,19 @@ function parseOperatorType(raw) {
25045
25602
  }
25046
25603
 
25047
25604
  // src/commands/replica.ts
25048
- init_sdk();
25049
- init_replication_registry();
25050
25605
  import { CLI_NAME as CLI_NAME22 } from "@tinycloud/operations/cli-name";
25051
25606
  import { randomBytes as randomBytes6 } from "crypto";
25052
- import { constants as fsConstants2 } from "fs";
25053
- import { open as open4, readdir as readdir5, rename as rename3, stat as stat3, unlink as unlink2, readFile as readFile9, lstat as lstat4 } from "fs/promises";
25054
- import { basename as basename3, dirname as dirname5, join as join10, resolve as resolve2 } from "path";
25607
+ import { constants as fsConstants3, lstatSync as lstatSync2 } from "fs";
25608
+ import { mkdir as mkdir4, open as open5, readdir as readdir5, rename as rename3, unlink as unlink2, lstat as lstat5 } from "fs/promises";
25609
+ import { basename as basename3, dirname as dirname6, join as join10, resolve as resolve3 } from "path";
25055
25610
 
25056
25611
  // src/lib/replication-report.ts
25057
- var CLEAR_PENDING_WARNING = "Clearing pending writes stops pinning keys whose last write had an unknown outcome or never finished. If such a write did commit, or commits later, this device can serve the older value for that key until the next sync after the commit, as it would for a write from another device.";
25058
25612
  function percentile(values, fraction) {
25059
25613
  if (values.length === 0) return null;
25060
25614
  values.sort((left, right) => left - right);
25061
25615
  return values[Math.ceil(fraction * values.length) - 1] ?? values[values.length - 1];
25062
25616
  }
25063
- function createReplicationReport(since, events, replicas, partitions, warning) {
25617
+ function createReplicationReport(since, events, partitions) {
25064
25618
  const reads = events.filter((event) => event.type === "replication.read");
25065
25619
  const replicaReads = reads.filter((event) => event.source === "replica");
25066
25620
  const sourceLatency = /* @__PURE__ */ new Map();
@@ -25103,9 +25657,9 @@ function createReplicationReport(since, events, replicas, partitions, warning) {
25103
25657
  syncs,
25104
25658
  writes,
25105
25659
  divergences,
25106
- replicas,
25660
+ replicas: [],
25107
25661
  partitions,
25108
- ...warning === void 0 ? {} : { warning }
25662
+ replicasUnavailable: true
25109
25663
  };
25110
25664
  }
25111
25665
  function renderReplicationReport(report) {
@@ -25118,34 +25672,30 @@ function renderReplicationReport(report) {
25118
25672
  `Writes: ${Object.entries(report.writes).map(([key, count]) => `${key}=${count}`).join(", ") || "none"}`,
25119
25673
  `Read source/reason: ${Object.entries(report.readsBySourceReason).map(([key, count]) => `${key}=${count}`).join(", ") || "none"}`,
25120
25674
  `Divergences: ${report.totals.divergences}; showing ${report.divergences.length} of ${report.totals.divergences}`,
25121
- `Replicas: ${report.replicas.length}`
25675
+ "Replicas: unavailable (read-only report)"
25122
25676
  ];
25123
25677
  for (const divergence of report.divergences) {
25124
25678
  if (divergence.type === "replication.divergence") {
25125
25679
  lines.push(` ${divergence.space} ${divergence.key}: ${divergence.kind} stalenessMs=${divergence.stalenessMs}`);
25126
25680
  }
25127
25681
  }
25128
- for (const replica of report.replicas) {
25129
- const grant = replica.grant ? ` grant=${replica.grant.cid} parent=${replica.grant.parentCid ?? "\u2014"} expires=${replica.grant.expiresAt ?? "\u2014"}` : " grant=missing";
25130
- lines.push(` ${replica.prefix}: ${replica.state}; pending in_flight=${replica.pending.inFlight} committed=${replica.pending.committed} ambiguous=${replica.pending.ambiguous}${grant}`);
25131
- for (const pinned of replica.pinned) lines.push(` pinned ${pinned.key} ${pinned.state} ${pinned.op} since=${pinned.since} code=${pinned.code ?? "\u2014"} likelyOrphaned=${pinned.likelyOrphaned}`);
25132
- }
25133
25682
  for (const partition of report.partitions) {
25134
25683
  lines.push(`Partition ${partition.idHash}: host=${partition.host ?? "unknown"} space=${partition.space ?? "unknown"} pinned=${partition.pinned}`);
25135
25684
  }
25136
25685
  if (report.partitions.some((partition) => partition.pinned > 100)) lines.push("Warning: a partition holds more than 100 pinned records.");
25137
- if (report.warning) lines.push(`Warning: ${report.warning}`);
25138
25686
  return lines.join("\n");
25139
25687
  }
25140
25688
 
25141
25689
  // src/commands/replica.ts
25142
25690
  init_dist3();
25143
25691
  import {
25692
+ ProfileSymlinkError as ProfileSymlinkError3,
25144
25693
  ProfileDeletedError as ProfileDeletedError3,
25145
- profilePath as profilePath4,
25694
+ profilePath as profilePath5,
25146
25695
  readAdditionalDelegations as readAdditionalDelegations2,
25147
25696
  readSession as readSession2,
25148
- withProfileLock as withProfileLock5
25697
+ withProfileLock as withProfileLock5,
25698
+ withPinnedProfileDirectory as withPinnedProfileDirectory3
25149
25699
  } from "@tinycloud/operations/state";
25150
25700
  import { ownerSessionLacksProof as ownerSessionLacksProof4 } from "@tinycloud/operations/profile";
25151
25701
  init_profiles();
@@ -25201,9 +25751,9 @@ function toCliError(error) {
25201
25751
  const hint = error.code === ReplicaErrorCode.GRANT_MISSING && typeof error.detail?.hint === "string" ? error.detail.hint : void 0;
25202
25752
  return new CLIError(error.code, error.message, EXIT_BY_CODE[error.code], hint === void 0 ? void 0 : { hint });
25203
25753
  }
25204
- async function run(action) {
25754
+ async function run(action, needsSqlite = true) {
25205
25755
  try {
25206
- await loadSqlite();
25756
+ if (needsSqlite) await loadSqlite();
25207
25757
  await action();
25208
25758
  } catch (error) {
25209
25759
  return handleError(toCliError(error));
@@ -25216,20 +25766,25 @@ async function profileName(cmd) {
25216
25766
  const config = await ProfileManager.getConfig().catch(() => void 0);
25217
25767
  return config?.defaultProfile ?? DEFAULT_PROFILE;
25218
25768
  }
25219
- function replicasRoot(profile) {
25220
- return join10(profilePath4(profile), "replicas");
25221
- }
25222
25769
  async function replicaNames(profile) {
25223
- const names = [];
25224
- for (const name of await readdir5(replicasRoot(profile)).catch(() => [])) {
25225
- if (!NAME.test(name)) continue;
25226
- const exists = await stat3(join10(replicasRoot(profile), name, "replica.db")).then(
25227
- () => true,
25228
- () => false
25229
- );
25230
- if (exists) names.push(name);
25231
- }
25232
- return names.sort();
25770
+ return await withReplicaRoot(profile, async (root) => {
25771
+ const names = [];
25772
+ for (const entry of await readdir5(root, { withFileTypes: true })) {
25773
+ if (entry.isSymbolicLink()) throw localPathChanged(profile);
25774
+ if (!NAME.test(entry.name) || !entry.isDirectory()) continue;
25775
+ const present = await withPinnedLocalDirectory(profile, root, entry.name, async (directory) => {
25776
+ const database = await lstat5(join10(directory, "replica.db"), { bigint: true }).catch((error) => {
25777
+ if (error.code === "ENOENT") return null;
25778
+ throw error;
25779
+ });
25780
+ if (database && (database.isSymbolicLink() || !database.isFile())) throw localPathChanged(profile);
25781
+ return database !== null;
25782
+ });
25783
+ if (present === null) throw localPathChanged(profile);
25784
+ if (present) names.push(entry.name);
25785
+ }
25786
+ return names.sort();
25787
+ }) ?? [];
25233
25788
  }
25234
25789
  function assertName(name) {
25235
25790
  if (!NAME.test(name)) {
@@ -25237,6 +25792,67 @@ function assertName(name) {
25237
25792
  }
25238
25793
  return name;
25239
25794
  }
25795
+ async function withReplicaRoot(profile, action, create4 = false) {
25796
+ const profileDirectory = profilePath5(profile);
25797
+ const readRoot = async (directory, recheckProfile) => {
25798
+ if (create4) {
25799
+ if (recheckProfile) await recheckProfile();
25800
+ await mkdir4(join10(directory, "replicas"), { mode: 448 }).catch((error) => {
25801
+ if (error.code !== "EEXIST") throw error;
25802
+ });
25803
+ }
25804
+ return withPinnedLocalDirectory(
25805
+ profile,
25806
+ directory,
25807
+ "replicas",
25808
+ (root, _named, _identity, recheckRoot) => action(root, recheckRoot, recheckProfile)
25809
+ );
25810
+ };
25811
+ if (process.platform === "linux") return readRoot(profileDirectory);
25812
+ const checked = await withPinnedLocalDirectory(
25813
+ profile,
25814
+ dirname6(profileDirectory),
25815
+ basename3(profileDirectory),
25816
+ (directory, _named, _identity, recheckProfile) => readRoot(directory, recheckProfile)
25817
+ );
25818
+ if (checked === null) throw localPathChanged(profile);
25819
+ return checked;
25820
+ }
25821
+ async function withReplicaDirectory(profile, name, action, create4 = false) {
25822
+ assertName(name);
25823
+ const result = await withReplicaRoot(profile, async (root, recheckRoot, recheckProfile) => {
25824
+ if (create4) {
25825
+ await recheckRoot();
25826
+ await mkdir4(join10(root, name), { mode: 448 }).catch((error) => {
25827
+ if (error.code !== "EEXIST") throw error;
25828
+ });
25829
+ }
25830
+ const opened = await withPinnedLocalDirectory(
25831
+ profile,
25832
+ root,
25833
+ name,
25834
+ async (_directory, path, expectedDirectory, recheckChild) => {
25835
+ await recheckRoot();
25836
+ await recheckChild();
25837
+ const database = await lstat5(join10(_directory, "replica.db"), { bigint: true }).catch((error) => {
25838
+ if (error.code === "ENOENT") return null;
25839
+ throw error;
25840
+ });
25841
+ if (database && (database.isSymbolicLink() || !database.isFile())) throw localPathChanged(profile);
25842
+ if (recheckProfile) {
25843
+ await recheckProfile();
25844
+ await recheckRoot();
25845
+ await recheckChild();
25846
+ }
25847
+ return action(path, expectedDirectory);
25848
+ }
25849
+ );
25850
+ if (opened === null) throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, `Replica "${name}" was not found.`);
25851
+ return opened;
25852
+ }, create4);
25853
+ if (result === null) throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, `Profile "${profile}" has no replicas.`);
25854
+ return result;
25855
+ }
25240
25856
  async function existingReplicaName(profile, option) {
25241
25857
  if (option !== void 0) return assertName(option);
25242
25858
  const names = await replicaNames(profile);
@@ -25258,17 +25874,37 @@ function profileGuard(profile) {
25258
25874
  }
25259
25875
  };
25260
25876
  }
25261
- async function openReplica(profile, name) {
25262
- return SqliteReplicaStore.open(join10(replicasRoot(profile), name), { create: false, guard: profileGuard(profile) });
25263
- }
25264
- async function createReplica(profile, config) {
25265
- const guard = profileGuard(profile);
25266
- return guard(async () => {
25267
- const store = await SqliteReplicaStore.open(join10(replicasRoot(profile), config.name), { create: true, guard });
25268
- if (await store.open() === null) await store.init(config);
25269
- return store;
25877
+ async function withOpenReplica(profile, name, action, guard = profileGuard(profile)) {
25878
+ return withReplicaDirectory(profile, name, async (path, expectedDirectory) => {
25879
+ const store = await SqliteReplicaStore.open(path, { create: false, guard, expectedDirectory });
25880
+ try {
25881
+ return await action(store);
25882
+ } finally {
25883
+ await store.close();
25884
+ }
25270
25885
  });
25271
25886
  }
25887
+ function assertLegacyLogoutClear(profile) {
25888
+ try {
25889
+ lstatSync2(join10(profilePath5(profile), "replica-logout-pending.json"));
25890
+ } catch (error) {
25891
+ if (error?.code === "ENOENT") return;
25892
+ }
25893
+ throw new CLIError(
25894
+ "REPLICA_GATED",
25895
+ "A logout is pending or its state cannot be checked for this profile.",
25896
+ ExitCode.PERMISSION_DENIED
25897
+ );
25898
+ }
25899
+ async function withLegacyReplicaRead(profile, action) {
25900
+ await withProfileLock5(profile, async () => {
25901
+ const checkDelivery = async () => {
25902
+ assertLegacyLogoutClear(profile);
25903
+ };
25904
+ await checkDelivery();
25905
+ await action(checkDelivery);
25906
+ }, { requireProfile: true });
25907
+ }
25272
25908
  async function storedGrants(profile) {
25273
25909
  const authorizations = [];
25274
25910
  const session = await readSession2(profile).catch(() => null);
@@ -25398,9 +26034,9 @@ function describeStatus(status) {
25398
26034
  ].join("\n");
25399
26035
  }
25400
26036
  async function writeFileAtomic(path, bytes) {
25401
- const target = resolve2(path);
25402
- const temp = join10(dirname5(target), `.${basename3(target)}.${randomBytes6(6).toString("hex")}.tmp`);
25403
- const handle = await open4(temp, fsConstants2.O_CREAT | fsConstants2.O_EXCL | fsConstants2.O_WRONLY, 384);
26037
+ const target = resolve3(path);
26038
+ const temp = join10(dirname6(target), `.${basename3(target)}.${randomBytes6(6).toString("hex")}.tmp`);
26039
+ const handle = await open5(temp, fsConstants3.O_CREAT | fsConstants3.O_EXCL | fsConstants3.O_WRONLY, 384);
25404
26040
  try {
25405
26041
  await handle.writeFile(bytes);
25406
26042
  await handle.sync();
@@ -25425,14 +26061,86 @@ function replicaSecretsWarning(space, prefix, secretsAllowed) {
25425
26061
  if (!secretsAllowed || !requiresSecretsOptIn(space, prefix)) return void 0;
25426
26062
  return `This replica stores the ciphertext of every secret under "${prefix}". A tinycloud.encryption/decrypt grant covers the whole encryption network, not one secret (TC-755), so whoever holds decrypt can open every replicated secret.`;
25427
26063
  }
25428
- async function readReplicationEvents(profile, sinceMs) {
25429
- const root = join10(profilePath4(profile), "replication");
26064
+ function localFileId(value) {
26065
+ if (typeof value === "bigint") return value >= 0n ? value : null;
26066
+ return Number.isSafeInteger(value) && value >= 0 ? BigInt(value) : null;
26067
+ }
26068
+ function sameLocalEntry(left, right) {
26069
+ const dev = localFileId(left.dev);
26070
+ const ino = localFileId(left.ino);
26071
+ return dev !== null && ino !== null && dev === localFileId(right.dev) && ino === localFileId(right.ino);
26072
+ }
26073
+ function localPathChanged(profile) {
26074
+ return new ProfileSymlinkError3(profile, "its local replica files contain a symlink or changed while reading");
26075
+ }
26076
+ async function assertNamedLocalEntry(profile, path, expected, kind) {
26077
+ const current = await lstat5(path, { bigint: true }).catch(() => null);
26078
+ if (!current || current.isSymbolicLink() || (kind === "directory" ? !current.isDirectory() : !current.isFile()) || !sameLocalEntry(expected, current)) throw localPathChanged(profile);
26079
+ }
26080
+ async function withPinnedLocalDirectory(profile, parent, name, action) {
26081
+ const path = join10(parent, name);
26082
+ const before = await lstat5(path, { bigint: true }).catch((error) => {
26083
+ if (error.code === "ENOENT") return null;
26084
+ throw error;
26085
+ });
26086
+ if (!before) return null;
26087
+ if (before.isSymbolicLink() || !before.isDirectory()) throw localPathChanged(profile);
26088
+ const handle = await open5(path, fsConstants3.O_RDONLY | fsConstants3.O_DIRECTORY | fsConstants3.O_NOFOLLOW).catch((error) => {
26089
+ if (error.code === "ELOOP" || error.code === "ENOTDIR" || error.code === "ENOENT") throw localPathChanged(profile);
26090
+ throw error;
26091
+ });
26092
+ try {
26093
+ const opened = await handle.stat({ bigint: true });
26094
+ if (!opened.isDirectory() || !sameLocalEntry(before, opened)) throw localPathChanged(profile);
26095
+ const dev = localFileId(opened.dev);
26096
+ const ino = localFileId(opened.ino);
26097
+ if (dev === null || ino === null || dev > BigInt(Number.MAX_SAFE_INTEGER) || ino > BigInt(Number.MAX_SAFE_INTEGER)) {
26098
+ throw localPathChanged(profile);
26099
+ }
26100
+ const pinned = process.platform === "linux" ? `/proc/self/fd/${handle.fd}` : path;
26101
+ const recheck = () => assertNamedLocalEntry(profile, path, opened, "directory");
26102
+ const result = await action(pinned, path, { dev: Number(dev), ino: Number(ino) }, recheck);
26103
+ await recheck();
26104
+ return result;
26105
+ } finally {
26106
+ await handle.close();
26107
+ }
26108
+ }
26109
+ async function readReportFile(profile, parent, name) {
26110
+ const path = join10(parent, name);
26111
+ const before = await lstat5(path, { bigint: true }).catch((error) => {
26112
+ if (error.code === "ENOENT") return null;
26113
+ throw error;
26114
+ });
26115
+ if (!before) return null;
26116
+ if (before.isSymbolicLink() || !before.isFile()) throw localPathChanged(profile);
26117
+ const handle = await open5(path, fsConstants3.O_RDONLY | fsConstants3.O_NOFOLLOW).catch((error) => {
26118
+ if (error.code === "ELOOP" || error.code === "ENOTDIR" || error.code === "ENOENT") throw localPathChanged(profile);
26119
+ throw error;
26120
+ });
26121
+ try {
26122
+ const opened = await handle.stat({ bigint: true });
26123
+ if (!opened.isFile() || !sameLocalEntry(before, opened)) throw localPathChanged(profile);
26124
+ const data = await handle.readFile("utf8");
26125
+ await assertNamedLocalEntry(profile, path, opened, "file");
26126
+ return data;
26127
+ } finally {
26128
+ await handle.close();
26129
+ }
26130
+ }
26131
+ async function readReportJson(profile, parent, name) {
26132
+ const data = await readReportFile(profile, parent, name);
26133
+ if (data === null) return null;
26134
+ try {
26135
+ return JSON.parse(data);
26136
+ } catch {
26137
+ return null;
26138
+ }
26139
+ }
26140
+ async function readReplicationEvents(profile, root, sinceMs) {
25430
26141
  const events = [];
25431
26142
  for (const name of ["events.jsonl.1", "events.jsonl"]) {
25432
- const data = await readFile9(join10(root, name), "utf8").catch((error) => {
25433
- if (error.code === "ENOENT") return "";
25434
- throw error;
25435
- });
26143
+ const data = await readReportFile(profile, root, name) ?? "";
25436
26144
  for (const line of data.split("\n")) {
25437
26145
  if (!line) continue;
25438
26146
  try {
@@ -25444,27 +26152,26 @@ async function readReplicationEvents(profile, sinceMs) {
25444
26152
  }
25445
26153
  return events;
25446
26154
  }
25447
- async function replicationPartitions(profile) {
25448
- const root = join10(profilePath4(profile), "replication");
25449
- const entries = await readdir5(root, { withFileTypes: true }).catch((error) => {
25450
- if (error.code === "ENOENT") return [];
25451
- throw error;
25452
- });
26155
+ async function replicationPartitions(profile, root) {
26156
+ const entries = await readdir5(root, { withFileTypes: true });
25453
26157
  const partitions = [];
25454
26158
  for (const entry of entries) {
25455
- if (!entry.isDirectory() || !/^[a-z2-7]{26}$/.test(entry.name)) continue;
25456
- const directory = join10(root, entry.name);
25457
- const info = await lstat4(directory);
25458
- if (info.isSymbolicLink() || !info.isDirectory()) continue;
25459
- const identity = await readFile9(join10(directory, "identity.json"), "utf8").then((value) => JSON.parse(value)).catch(() => null);
25460
- const pending = await readFile9(join10(directory, "pending.json"), "utf8").then((value) => JSON.parse(value)).catch(() => null);
25461
- const pinned = pending?.records?.filter((record2) => record2.state === "in_flight" || record2.state === "ambiguous").length ?? 0;
25462
- partitions.push({
25463
- idHash: entry.name,
25464
- host: typeof identity?.host === "string" ? identity.host : null,
25465
- space: typeof identity?.space === "string" ? identity.space : null,
25466
- pinned
26159
+ if (!/^[a-z2-7]{26}$/.test(entry.name)) continue;
26160
+ if (entry.isSymbolicLink()) throw localPathChanged(profile);
26161
+ if (!entry.isDirectory()) continue;
26162
+ const partition = await withPinnedLocalDirectory(profile, root, entry.name, async (pinned) => {
26163
+ const identity = await readReportJson(profile, pinned, "identity.json");
26164
+ const pending = await readReportJson(profile, pinned, "pending.json");
26165
+ const pinnedCount = pending?.records?.filter((record2) => record2.state === "in_flight" || record2.state === "ambiguous").length ?? 0;
26166
+ return {
26167
+ idHash: entry.name,
26168
+ host: typeof identity?.host === "string" ? identity.host : null,
26169
+ space: typeof identity?.space === "string" ? identity.space : null,
26170
+ pinned: pinnedCount
26171
+ };
25467
26172
  });
26173
+ if (!partition) throw localPathChanged(profile);
26174
+ partitions.push(partition);
25468
26175
  }
25469
26176
  return partitions.sort((left, right) => left.idHash.localeCompare(right.idHash));
25470
26177
  }
@@ -25483,52 +26190,41 @@ SECRETS_OPT_IN_REQUIRED; 3 grant missing; 4 key absent, deleted, content missing
25483
26190
  or coverage incomplete; 5 grant expired, revoked or not yet valid; 6 network;
25484
26191
  7 node, protocol or integrity error; 10 storage full.`
25485
26192
  );
25486
- replica.command("report").description("Report replication reads, syncs, pending writes and local identity partitions").option("--since <duration>", "Include events from the last duration (default: 24h)", "24h").option("--purge", "Purge configured replicas for the current identity").option("--clear-pending", "Clear ambiguous and likely orphaned in-flight writes").action(
26193
+ replica.command("report").description("Read-only report of persisted replication events and identity partitions").option("--since <duration>", "Include events from the last duration (default: 24h)", "24h").option("--purge", "Refused: named replica reset --replica <name> --purge only; flag-owned purge unavailable (TC-1309)").option("--clear-pending", "Refused: no equivalent pending-clear command yet (TC-1309)").action(
25487
26194
  (options, cmd) => run(async () => {
25488
- const globals = cmd.optsWithGlobals();
25489
- const context = await ProfileManager.resolveContext(globals);
25490
- const node = context.replication ? await ensureAuthenticated(context) : void 0;
25491
26195
  const profile = await profileName(cmd);
26196
+ if (options.purge) {
26197
+ throw new CLIError("REPORT_READ_ONLY", `Replica report is read-only. To remove a named local replica, run ${CLI_NAME22} replica reset --replica <name> --purge; flag-owned purge has no equivalent yet (TC-1309).`, ExitCode.USAGE_ERROR);
26198
+ }
26199
+ if (options.clearPending) {
26200
+ throw new CLIError("REPORT_READ_ONLY", "Clearing pending replica writes has no equivalent yet; see TC-1309 for a safe replacement command.", ExitCode.USAGE_ERROR);
26201
+ }
25492
26202
  let durationMs;
25493
26203
  try {
25494
26204
  durationMs = parseDuration(options.since);
25495
26205
  } catch (error) {
25496
26206
  throw new CLIError("USAGE_ERROR", error instanceof Error ? error.message : String(error), ExitCode.USAGE_ERROR);
25497
26207
  }
25498
- const control = node?.replication ?? replicationForProfile(profile);
25499
- let cleared;
25500
- let purged;
25501
- let warning;
25502
- if (options.clearPending || options.purge) {
25503
- if (!control) throw new CLIError("REPLICATION_UNAVAILABLE", "No replication runtime is registered for this profile.", ExitCode.ERROR);
25504
- warning = CLEAR_PENDING_WARNING;
25505
- process.stderr.write(`Warning: ${warning}
25506
- `);
25507
- if (options.clearPending) cleared = await control.clearPending();
25508
- if (options.purge) purged = await control.purge();
25509
- }
25510
- const sinceMs = Date.now() - durationMs;
25511
- const [events, partitions, replicas] = await Promise.all([
25512
- readReplicationEvents(profile, sinceMs),
25513
- replicationPartitions(profile),
25514
- node?.replication ? node.replication.status() : control ? control.status() : Promise.resolve([])
25515
- ]);
25516
- const report = createReplicationReport(options.since, events, replicas, partitions, warning);
25517
- if (shouldOutputJson()) {
25518
- outputJson({
25519
- ...report,
25520
- ...cleared === void 0 ? {} : { cleared },
25521
- ...purged === void 0 ? {} : { purged }
25522
- });
25523
- } else {
25524
- process.stdout.write(`${renderReplicationReport(report)}
25525
- `);
25526
- if (cleared !== void 0) process.stdout.write(`Cleared ${cleared} pending write record(s).
25527
- `);
25528
- if (purged !== void 0) process.stdout.write(`Purge: ${JSON.stringify(purged)}
26208
+ const report = await withPinnedProfileDirectory3(profile, async (directory) => {
26209
+ if (directory === null) throw new CLIError("PROFILE_NOT_FOUND", `Profile "${profile}" does not exist.`, ExitCode.ERROR);
26210
+ const readPersistedReport = async (root) => {
26211
+ await replicaNames(profile);
26212
+ const sinceMs = Date.now() - durationMs;
26213
+ const persisted = await withPinnedLocalDirectory(profile, root, "replication", async (directory2) => ({
26214
+ events: await readReplicationEvents(profile, directory2, sinceMs),
26215
+ partitions: await replicationPartitions(profile, directory2)
26216
+ }));
26217
+ return createReplicationReport(options.since, persisted?.events ?? [], persisted?.partitions ?? []);
26218
+ };
26219
+ if (process.platform === "linux") return readPersistedReport(directory);
26220
+ const checked = await withPinnedLocalDirectory(profile, dirname6(directory), basename3(directory), readPersistedReport);
26221
+ if (checked === null) throw localPathChanged(profile);
26222
+ return checked;
26223
+ });
26224
+ if (shouldOutputJson()) outputJson(report);
26225
+ else process.stdout.write(`${renderReplicationReport(report)}
25529
26226
  `);
25530
- }
25531
- })
26227
+ }, false)
25532
26228
  );
25533
26229
  replica.command("sync").description("Create or update a replica from its source host").option("--replica <name>", "Replica name (default: the profile's only replica, or one named after the prefix)").option("--space <id|name>", "Space to replicate (first sync; default: the space of the sync grant)").option("--prefix <prefix>", "KV prefix to replicate, e.g. notes/ (first sync)").option("--retention-grant <cid>", "CID of a tinycloud.kv/retain grant: keep local reads after the sync grant expires").option("--limit <n>", "Feed page size (1-1000)", (value) => Number.parseInt(value, 10)).option("--allow-secrets", "Opt in to copying ciphertext from a secrets space, vault prefix, or whole space").action(
25534
26230
  (options, cmd) => run(async () => {
@@ -25540,87 +26236,98 @@ or coverage incomplete; 5 grant expired, revoked or not yet valid; 6 network;
25540
26236
  throw new CLIError("INVALID_REPLICATION_PREFIX", error instanceof Error ? error.message : String(error), ExitCode.USAGE_ERROR);
25541
26237
  }
25542
26238
  }
25543
- const globals = cmd.optsWithGlobals();
25544
- const config = await ProfileManager.getProfile(profile);
25545
- const deviceDid = config.sessionDid ?? config.did;
25546
- const jwk = await ProfileManager.getKey(profile);
25547
- if (jwk === null) {
25548
- throw new CLIError("AUTH_REQUIRED", `Profile "${profile}" has no device key.`, ExitCode.AUTH_REQUIRED);
25549
- }
25550
- if (options.limit !== void 0 && (!Number.isInteger(options.limit) || options.limit < 1 || options.limit > 1e3)) {
25551
- throw new CLIError("USAGE_ERROR", "--limit must be an integer from 1 through 1000.", ExitCode.USAGE_ERROR);
25552
- }
25553
- const existing = await replicaNames(profile);
25554
- const name = options.replica !== void 0 ? assertName(options.replica) : options.prefix !== void 0 ? assertName(options.prefix.replace(/\/+$/, "").replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^[^A-Za-z0-9]+/, "") || "default") : await existingReplicaName(profile, void 0);
25555
- const grants = await storedGrants(profile);
25556
- let store;
25557
- if (existing.includes(name)) {
25558
- store = await openReplica(profile, name);
25559
- const state2 = await store.open();
25560
- const differs = syncOptionsDiffer(state2.config, {
25561
- ...options.space === void 0 ? {} : { space: options.space },
25562
- ...options.prefix === void 0 ? {} : { prefix: options.prefix },
25563
- ...globals.host === void 0 ? {} : { host: globals.host }
25564
- });
25565
- if (differs !== void 0) {
25566
- await store.close();
25567
- throw new ReplicaError(
25568
- ReplicaErrorCode.CONFIG_MISMATCH,
25569
- `Replica ${name} already follows ${differs}. Use another --replica name, or ${CLI_NAME22} replica reset --replica ${name} --purge first.`
25570
- );
25571
- }
25572
- } else {
25573
- const prefix = options.prefix;
25574
- if (prefix === void 0) {
25575
- throw new CLIError("USAGE_ERROR", "The first sync of a replica needs --prefix (and a device grant covering it).", ExitCode.USAGE_ERROR);
25576
- }
25577
- const space = resolveSpace(options.space, prefix, grants);
25578
- if (requiresSecretsOptIn(space, prefix) && options.allowSecrets !== true) {
25579
- throw new ReplicaError(
25580
- ReplicaErrorCode.SECRETS_OPT_IN_REQUIRED,
25581
- `Replicating ${space}/kv/${prefix} copies encrypted secret material to this disk; pass --allow-secrets to opt in.`
25582
- );
25583
- }
25584
- const host = (globals.host ?? process.env.TC_HOST ?? config.host)?.replace(/\/+$/, "");
25585
- if (!host) throw new CLIError("USAGE_ERROR", "Pass --host: the profile has no host to pin.", ExitCode.USAGE_ERROR);
25586
- chooseGrant(grants, deviceDid, space, prefix);
25587
- store = await createReplica(profile, {
25588
- name,
25589
- replicaId: randomBytes6(16).toString("hex"),
25590
- host,
25591
- space,
25592
- prefix,
25593
- deviceDid,
25594
- allowSecrets: options.allowSecrets === true,
25595
- localReadPolicy: "whileGrantValid",
25596
- retentionGrantCid: null
25597
- });
25598
- }
25599
- try {
25600
- const state2 = await store.open();
25601
- const { space, prefix, host } = state2.config;
25602
- if (requiresSecretsOptIn(space, prefix) && !state2.config.allowSecrets && options.allowSecrets !== true) {
25603
- throw new ReplicaError(ReplicaErrorCode.SECRETS_OPT_IN_REQUIRED, `Replicating ${space}/kv/${prefix} needs --allow-secrets.`);
25604
- }
25605
- let grant;
25606
- try {
25607
- grant = chooseGrant(grants, state2.config.deviceDid, space, prefix);
25608
- } catch (error) {
25609
- if (!isReplicaError(error, ReplicaErrorCode.GRANT_MISSING) || (state2.grant ?? state2.pendingGrant) === null) throw error;
25610
- }
25611
- if (grant !== void 0) await store.installGrant(grant);
25612
- if (options.retentionGrant !== void 0 && options.retentionGrant !== state2.config.retentionGrantCid) {
25613
- await store.setRetentionGrant(options.retentionGrant === "" ? null : options.retentionGrant);
25614
- }
25615
- const current = await store.open();
25616
- const transportFor = await grantTransports({ host, space, deviceDid: current.config.deviceDid, jwk });
25617
- const warning = replicaSecretsWarning(space, prefix, options.allowSecrets === true || state2.config.allowSecrets);
26239
+ await withPinnedProfileDirectory3(profile, async (directory) => {
26240
+ if (directory === null) throw new CLIError("PROFILE_NOT_FOUND", `Profile "${profile}" does not exist.`, ExitCode.ERROR);
26241
+ const globals = cmd.optsWithGlobals();
26242
+ const config = await ProfileManager.getProfile(profile);
26243
+ const deviceDid = config.sessionDid ?? config.did;
26244
+ const jwk = await ProfileManager.getKey(profile);
26245
+ if (jwk === null) {
26246
+ throw new CLIError("AUTH_REQUIRED", `Profile "${profile}" has no device key.`, ExitCode.AUTH_REQUIRED);
26247
+ }
26248
+ if (options.limit !== void 0 && (!Number.isInteger(options.limit) || options.limit < 1 || options.limit > 1e3)) {
26249
+ throw new CLIError("USAGE_ERROR", "--limit must be an integer from 1 through 1000.", ExitCode.USAGE_ERROR);
26250
+ }
26251
+ const existing = await replicaNames(profile);
26252
+ const name = options.replica !== void 0 ? assertName(options.replica) : options.prefix !== void 0 ? assertName(options.prefix.replace(/\/+$/, "").replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^[^A-Za-z0-9]+/, "") || "default") : await existingReplicaName(profile, void 0);
26253
+ const grants = await storedGrants(profile);
26254
+ const initialConfig = existing.includes(name) ? null : (() => {
26255
+ const prefix = options.prefix;
26256
+ if (prefix === void 0) {
26257
+ throw new CLIError("USAGE_ERROR", "The first sync of a replica needs --prefix (and a device grant covering it).", ExitCode.USAGE_ERROR);
26258
+ }
26259
+ const space = resolveSpace(options.space, prefix, grants);
26260
+ if (requiresSecretsOptIn(space, prefix) && options.allowSecrets !== true) {
26261
+ throw new ReplicaError(
26262
+ ReplicaErrorCode.SECRETS_OPT_IN_REQUIRED,
26263
+ `Replicating ${space}/kv/${prefix} copies encrypted secret material to this disk; pass --allow-secrets to opt in.`
26264
+ );
26265
+ }
26266
+ const host = (globals.host ?? process.env.TC_HOST ?? config.host)?.replace(/\/+$/, "");
26267
+ if (!host) throw new CLIError("USAGE_ERROR", "Pass --host: the profile has no host to pin.", ExitCode.USAGE_ERROR);
26268
+ chooseGrant(grants, deviceDid, space, prefix);
26269
+ return {
26270
+ name,
26271
+ replicaId: randomBytes6(16).toString("hex"),
26272
+ host,
26273
+ space,
26274
+ prefix,
26275
+ deviceDid,
26276
+ allowSecrets: options.allowSecrets === true,
26277
+ localReadPolicy: "whileGrantValid",
26278
+ retentionGrantCid: null
26279
+ };
26280
+ })();
26281
+ const { replica: replica2, report, status, warning } = await withReplicaDirectory(profile, name, async (path, expectedDirectory) => {
26282
+ const store = await SqliteReplicaStore.open(path, {
26283
+ create: initialConfig !== null,
26284
+ guard: profileGuard(profile),
26285
+ expectedDirectory
26286
+ });
26287
+ try {
26288
+ if (initialConfig !== null && await store.open() === null) await store.init(initialConfig);
26289
+ const state2 = await store.open();
26290
+ if (initialConfig === null) {
26291
+ const differs = syncOptionsDiffer(state2.config, {
26292
+ ...options.space === void 0 ? {} : { space: options.space },
26293
+ ...options.prefix === void 0 ? {} : { prefix: options.prefix },
26294
+ ...globals.host === void 0 ? {} : { host: globals.host }
26295
+ });
26296
+ if (differs !== void 0) {
26297
+ throw new ReplicaError(
26298
+ ReplicaErrorCode.CONFIG_MISMATCH,
26299
+ `Replica ${name} already follows ${differs}. Use another --replica name, or ${CLI_NAME22} replica reset --replica ${name} --purge first.`
26300
+ );
26301
+ }
26302
+ }
26303
+ const { space, prefix, host } = state2.config;
26304
+ if (requiresSecretsOptIn(space, prefix) && !state2.config.allowSecrets && options.allowSecrets !== true) {
26305
+ throw new ReplicaError(ReplicaErrorCode.SECRETS_OPT_IN_REQUIRED, `Replicating ${space}/kv/${prefix} needs --allow-secrets.`);
26306
+ }
26307
+ let grant;
26308
+ try {
26309
+ grant = chooseGrant(grants, state2.config.deviceDid, space, prefix);
26310
+ } catch (error) {
26311
+ if (!isReplicaError(error, ReplicaErrorCode.GRANT_MISSING) || (state2.grant ?? state2.pendingGrant) === null) throw error;
26312
+ }
26313
+ if (grant !== void 0) await store.installGrant(grant);
26314
+ if (options.retentionGrant !== void 0 && options.retentionGrant !== state2.config.retentionGrantCid) {
26315
+ await store.setRetentionGrant(options.retentionGrant === "" ? null : options.retentionGrant);
26316
+ }
26317
+ const current = await store.open();
26318
+ const transportFor = await grantTransports({ host, space, deviceDid: current.config.deviceDid, jwk });
26319
+ const warning2 = replicaSecretsWarning(space, prefix, options.allowSecrets === true || state2.config.allowSecrets);
26320
+ const report2 = await new Replica({ store, transportFor }).sync(options.limit === void 0 ? {} : { limit: options.limit });
26321
+ const status2 = await store.status();
26322
+ return { replica: current.config.name, report: report2, status: status2, warning: warning2 };
26323
+ } finally {
26324
+ await store.close();
26325
+ }
26326
+ }, initialConfig !== null);
25618
26327
  if (warning !== void 0) process.stderr.write(`Warning: ${warning}
25619
26328
  `);
25620
- const report = await new Replica({ store, transportFor }).sync(options.limit === void 0 ? {} : { limit: options.limit });
25621
- const status = await store.status();
25622
26329
  if (shouldOutputJson()) {
25623
- outputJson({ replica: current.config.name, sync: report, status, ...warning === void 0 ? {} : { warning } });
26330
+ outputJson({ replica: replica2, sync: report, status, ...warning === void 0 ? {} : { warning } });
25624
26331
  } else {
25625
26332
  process.stdout.write(
25626
26333
  `${theme.success("\u2713")} Synced ${report.changes} change(s) in ${report.pages} page(s), fetched ${report.fetched} value(s).
@@ -25628,40 +26335,39 @@ ${describeStatus(status)}
25628
26335
  `
25629
26336
  );
25630
26337
  }
25631
- } finally {
25632
- await store.close();
25633
- }
26338
+ });
25634
26339
  })
25635
26340
  );
25636
26341
  replica.command("get <key>").description("Read a key from the local replica (never touches the network)").option("--replica <name>", "Replica name").option("--raw", "Write the value's bytes to stdout").option("-o, --output <file>", "Write the value to a file (atomically, mode 0600)").option("--no-verify", "Skip re-hashing the stored content").action(
25637
26342
  (key, options, cmd) => run(async () => {
25638
26343
  const profile = await profileName(cmd);
25639
- const store = await openReplica(profile, await existingReplicaName(profile, options.replica));
25640
- let result;
25641
- try {
25642
- result = await new Replica({ store }).get(key, { verify: options.verify !== false });
25643
- } finally {
25644
- await store.close();
25645
- }
25646
- if (result.status !== "present") {
25647
- const failure = READ_FAILURE[result.status];
25648
- throw new CLIError(failure.code, `Key ${JSON.stringify(key)} ${failure.what}.`, failure.exit);
25649
- }
25650
- if (options.output) {
25651
- await writeFileAtomic(options.output, result.value);
25652
- outputJson({ key, written: options.output, etag: result.etag, meta: result.meta });
25653
- return;
25654
- }
25655
- if (options.raw) {
26344
+ await withLegacyReplicaRead(profile, async (checkDelivery) => {
26345
+ const result = await withOpenReplica(
26346
+ profile,
26347
+ await existingReplicaName(profile, options.replica),
26348
+ (store) => new Replica({ store }).get(key, { verify: options.verify !== false })
26349
+ );
26350
+ await checkDelivery();
26351
+ if (result.status !== "present") {
26352
+ const failure = READ_FAILURE[result.status];
26353
+ throw new CLIError(failure.code, `Key ${JSON.stringify(key)} ${failure.what}.`, failure.exit);
26354
+ }
26355
+ if (options.output) {
26356
+ await writeFileAtomic(options.output, result.value);
26357
+ outputJson({ key, written: options.output, etag: result.etag, meta: result.meta });
26358
+ return;
26359
+ }
26360
+ if (options.raw) {
26361
+ process.stdout.write(result.value);
26362
+ return;
26363
+ }
26364
+ if (shouldOutputJson()) {
26365
+ outputJson({ key, status: result.status, ...jsonValue(result.value), etag: result.etag, metadata: result.metadata, meta: result.meta });
26366
+ return;
26367
+ }
25656
26368
  process.stdout.write(result.value);
25657
- return;
25658
- }
25659
- if (shouldOutputJson()) {
25660
- outputJson({ key, status: result.status, ...jsonValue(result.value), etag: result.etag, metadata: result.metadata, meta: result.meta });
25661
- return;
25662
- }
25663
- process.stdout.write(result.value);
25664
- process.stdout.write("\n");
26369
+ process.stdout.write("\n");
26370
+ });
25665
26371
  })
25666
26372
  );
25667
26373
  replica.command("list [prefix]").description("List live keys in the local replica (never touches the network)").option("--replica <name>", "Replica name").option("--after <key>", "Start after this key").option("--limit <n>", "Maximum keys", (value) => Number.parseInt(value, 10)).action(
@@ -25670,13 +26376,17 @@ ${describeStatus(status)}
25670
26376
  throw new CLIError("USAGE_ERROR", "--limit must be a positive integer.", ExitCode.USAGE_ERROR);
25671
26377
  }
25672
26378
  const profile = await profileName(cmd);
25673
- const store = await openReplica(profile, await existingReplicaName(profile, options.replica));
25674
- try {
25675
- const { entries, meta } = await new Replica({ store }).list({
25676
- ...prefix === void 0 ? {} : { prefix },
25677
- ...options.after === void 0 ? {} : { after: options.after },
25678
- ...options.limit === void 0 ? {} : { limit: options.limit }
25679
- });
26379
+ await withLegacyReplicaRead(profile, async (checkDelivery) => {
26380
+ const { entries, meta } = await withOpenReplica(
26381
+ profile,
26382
+ await existingReplicaName(profile, options.replica),
26383
+ (store) => new Replica({ store }).list({
26384
+ ...prefix === void 0 ? {} : { prefix },
26385
+ ...options.after === void 0 ? {} : { after: options.after },
26386
+ ...options.limit === void 0 ? {} : { limit: options.limit }
26387
+ })
26388
+ );
26389
+ await checkDelivery();
25680
26390
  if (shouldOutputJson()) {
25681
26391
  outputJson({ keys: entries, count: entries.length, prefix: prefix ?? null, meta });
25682
26392
  } else if (entries.length === 0) {
@@ -25689,50 +26399,57 @@ ${describeStatus(status)}
25689
26399
  ) + "\n"
25690
26400
  );
25691
26401
  }
25692
- } finally {
25693
- await store.close();
25694
- }
26402
+ });
25695
26403
  })
25696
26404
  );
25697
26405
  replica.command("status").description("Show one replica's sync state, authority and counts, or all of them").option("--replica <name>", "Replica name").action(
25698
26406
  (options, cmd) => run(async () => {
25699
26407
  const profile = await profileName(cmd);
25700
- const names = options.replica === void 0 ? await replicaNames(profile) : [assertName(options.replica)];
25701
- const statuses = [];
25702
- for (const name of names) {
25703
- const store = await openReplica(profile, name);
25704
- try {
25705
- statuses.push(await new Replica({ store }).status());
25706
- } finally {
25707
- await store.close();
26408
+ await withLegacyReplicaRead(profile, async (checkDelivery) => {
26409
+ const names = options.replica === void 0 ? await replicaNames(profile) : [assertName(options.replica)];
26410
+ const statuses = [];
26411
+ for (const name of names) {
26412
+ statuses.push(await withOpenReplica(profile, name, (store) => new Replica({ store }).status()));
25708
26413
  }
25709
- }
25710
- if (shouldOutputJson()) {
25711
- outputJson(options.replica === void 0 ? { replicas: statuses } : statuses[0]);
25712
- } else if (statuses.length === 0) {
25713
- process.stdout.write(theme.muted(`Profile "${profile}" has no replicas.`) + "\n");
25714
- } else {
25715
- process.stdout.write(statuses.map(describeStatus).join("\n\n") + "\n");
25716
- }
26414
+ await checkDelivery();
26415
+ if (shouldOutputJson()) {
26416
+ outputJson(options.replica === void 0 ? { replicas: statuses } : statuses[0]);
26417
+ } else if (statuses.length === 0) {
26418
+ process.stdout.write(theme.muted(`Profile "${profile}" has no replicas.`) + "\n");
26419
+ } else {
26420
+ process.stdout.write(statuses.map(describeStatus).join("\n\n") + "\n");
26421
+ }
26422
+ });
25717
26423
  })
25718
26424
  );
25719
26425
  replica.command("reset").description("Clear a replica's entries, content and cursor (keeps its configuration and grant)").option("--replica <name>", "Replica name").option("--purge", "Remove the replica entirely").action(
25720
26426
  (options, cmd) => run(async () => {
25721
26427
  const profile = await profileName(cmd);
25722
- const name = await existingReplicaName(profile, options.replica);
25723
- const store = await openReplica(profile, name);
25724
- try {
26428
+ await withProfileLock5(profile, async () => {
26429
+ const name = await existingReplicaName(profile, options.replica);
25725
26430
  if (options.purge) {
25726
- const lease = await store.acquireSyncLease(6e4);
25727
- if (lease === null) throw new ReplicaError(ReplicaErrorCode.BUSY, "Another process is syncing this replica.");
25728
- await store.destroy(lease);
26431
+ await withPinnedReplicaForReset(profile, name, async (path, recheck, expectedDirectory) => {
26432
+ const profileLock = profileGuard(profile);
26433
+ const safeGuard = (section) => profileLock(async () => {
26434
+ await recheck();
26435
+ return section();
26436
+ });
26437
+ const store = await SqliteReplicaStore.open(path, { create: false, guard: safeGuard, expectedDirectory });
26438
+ try {
26439
+ await recheck();
26440
+ const lease = await store.acquireSyncLease(6e4);
26441
+ if (lease === null) throw new ReplicaError(ReplicaErrorCode.BUSY, "Another process is syncing this replica.");
26442
+ await recheck();
26443
+ await store.destroy(lease);
26444
+ } finally {
26445
+ await store.close();
26446
+ }
26447
+ });
25729
26448
  } else {
25730
- await new Replica({ store }).reset("manual");
26449
+ await withOpenReplica(profile, name, (store) => new Replica({ store }).reset("manual"));
25731
26450
  }
25732
- } finally {
25733
- await store.close();
25734
- }
25735
- outputJson({ replica: name, reset: true, purged: options.purge === true });
26451
+ outputJson({ replica: name, reset: true, purged: options.purge === true });
26452
+ }, { requireProfile: true });
25736
26453
  })
25737
26454
  );
25738
26455
  }
@@ -25745,7 +26462,7 @@ init_errors();
25745
26462
  init_warnings();
25746
26463
  init_constants();
25747
26464
  import { CLI_NAME as CLI_NAME24 } from "@tinycloud/operations/cli-name";
25748
- import { readFile as readFile10 } from "fs/promises";
26465
+ import { readFile as readFile9 } from "fs/promises";
25749
26466
  import { join as join11 } from "path";
25750
26467
  import { homedir, hostname, userInfo } from "os";
25751
26468
  import { appSecretScope } from "@tinycloud/sdk-core";
@@ -25774,24 +26491,24 @@ init_space();
25774
26491
  // src/lib/browser.ts
25775
26492
  init_formatter();
25776
26493
  init_theme();
25777
- import open5 from "open";
26494
+ import open6 from "open";
25778
26495
  var LAUNCH_CHECK_MS = 1e3;
25779
26496
  async function tryOpenBrowser(url) {
25780
26497
  let launcher;
25781
26498
  try {
25782
- launcher = await open5(url);
26499
+ launcher = await open6(url);
25783
26500
  } catch {
25784
26501
  return false;
25785
26502
  }
25786
- return new Promise((resolve5) => {
25787
- const timer = setTimeout(() => resolve5(true), LAUNCH_CHECK_MS);
26503
+ return new Promise((resolve6) => {
26504
+ const timer = setTimeout(() => resolve6(true), LAUNCH_CHECK_MS);
25788
26505
  launcher.once("error", () => {
25789
26506
  clearTimeout(timer);
25790
- resolve5(false);
26507
+ resolve6(false);
25791
26508
  });
25792
26509
  launcher.once("exit", (code3) => {
25793
26510
  clearTimeout(timer);
25794
- resolve5(code3 === 0);
26511
+ resolve6(code3 === 0);
25795
26512
  });
25796
26513
  });
25797
26514
  }
@@ -25835,9 +26552,9 @@ function runProbeChild(request, options = {}) {
25835
26552
  const killGraceMs = options.killGraceMs ?? KILL_GRACE_MS;
25836
26553
  const spawn2 = options.spawn ?? nodeSpawn;
25837
26554
  const signal = options.signal;
25838
- return new Promise((resolve5) => {
26555
+ return new Promise((resolve6) => {
25839
26556
  if (signal?.aborted) {
25840
- resolve5({ kind: "cancelled" });
26557
+ resolve6({ kind: "cancelled" });
25841
26558
  return;
25842
26559
  }
25843
26560
  const child = spawn2(command, args, { env: process.env, stdio: ["ignore", "pipe", "pipe"] });
@@ -25873,7 +26590,7 @@ function runProbeChild(request, options = {}) {
25873
26590
  const envelope = stdout.length > 0 && stdout.length <= MAX_PROBE_LINE_BYTES ? parseStatusEnvelope(stdout.toString("utf8")) : void 0;
25874
26591
  outcome = envelope !== void 0 ? { kind: "ok", envelope } : probeFailed(stderr.toString("utf8"));
25875
26592
  }
25876
- resolve5(outcome);
26593
+ resolve6(outcome);
25877
26594
  };
25878
26595
  if (child.pid !== void 0) options.onSpawn?.(child.pid);
25879
26596
  child.stdout?.on("data", (chunk) => {
@@ -25965,15 +26682,15 @@ var TIMEOUT_RE = /^(0|\d+s|\d+m)$/;
25965
26682
  var RETRYABLE_STATUS_ERRORS = { NODE_UNREACHABLE: true, NODE_ERROR: true };
25966
26683
  var EXPLICIT_KEY_HINT = `${CLI_NAME23} secrets wait never signs in, and an explicit private key has no stored session. Use a local profile signed in once with ${CLI_NAME23} --profile <profile> auth login --method local, without TC_PRIVATE_KEY.`;
25967
26684
  function abortableSleep(ms2, signal) {
25968
- return new Promise((resolve5) => {
26685
+ return new Promise((resolve6) => {
25969
26686
  if (signal.aborted) {
25970
- resolve5();
26687
+ resolve6();
25971
26688
  return;
25972
26689
  }
25973
26690
  const done = () => {
25974
26691
  clearTimeout(timer);
25975
26692
  signal.removeEventListener("abort", done);
25976
- resolve5();
26693
+ resolve6();
25977
26694
  };
25978
26695
  const timer = setTimeout(done, ms2);
25979
26696
  signal.addEventListener("abort", done, { once: true });
@@ -26639,7 +27356,7 @@ function permissionsFromDelegation2(delegation) {
26639
27356
  }
26640
27357
  async function loadDelegationCandidates(source) {
26641
27358
  try {
26642
- const raw = JSON.parse(await readFile10(source, "utf8"));
27359
+ const raw = JSON.parse(await readFile9(source, "utf8"));
26643
27360
  return normalizeDelegationCandidates(raw, source);
26644
27361
  } catch (error) {
26645
27362
  if (error?.code !== "ENOENT") {
@@ -26659,7 +27376,7 @@ async function loadDelegationCandidates(source) {
26659
27376
  }
26660
27377
  try {
26661
27378
  const importedPath = join11(resolveProfilesDir(), source, "additional-delegations.json");
26662
- const raw = JSON.parse(await readFile10(importedPath, "utf8"));
27379
+ const raw = JSON.parse(await readFile9(importedPath, "utf8"));
26663
27380
  return normalizeDelegationCandidates(raw, source);
26664
27381
  } catch (error) {
26665
27382
  if (error?.code === "ENOENT") {
@@ -27166,7 +27883,7 @@ function registerSecretsCommand(program, openKeyAcquisition) {
27166
27883
  throw new CLIError("USAGE_ERROR", "Provide only one of: value argument, --file, or --stdin", ExitCode.USAGE_ERROR);
27167
27884
  }
27168
27885
  if (options.file) {
27169
- secretValue = await readFile10(options.file, "utf-8");
27886
+ secretValue = await readFile9(options.file, "utf-8");
27170
27887
  } else if (options.stdin) {
27171
27888
  secretValue = (await readStdin3()).toString("utf-8");
27172
27889
  } else {
@@ -32753,7 +33470,7 @@ async function importAesKey(key32, usage) {
32753
33470
  [usage]
32754
33471
  );
32755
33472
  }
32756
- async function open6(blob, key32) {
33473
+ async function open7(blob, key32) {
32757
33474
  if (blob.length < HEADER_LENGTH + NONCE_LENGTH + TAG_LENGTH) {
32758
33475
  throw new TypeError(`sealed blob too short: ${blob.length} bytes`);
32759
33476
  }
@@ -33091,7 +33808,7 @@ async function resolvePolicyShare(link3, options) {
33091
33808
  options.signal?.throwIfAborted();
33092
33809
  let envelope;
33093
33810
  try {
33094
- const encoded = new TextDecoder("utf-8", { fatal: true }).decode(await open6(parsed.ciphertext, parsed.key32));
33811
+ const encoded = new TextDecoder("utf-8", { fatal: true }).decode(await open7(parsed.ciphertext, parsed.key32));
33095
33812
  const value = JSON.parse(encoded);
33096
33813
  if (canonicalize2(value) !== encoded) throw new Error("non-canonical envelope");
33097
33814
  envelope = shareEnvelopeV3Schema.parse(value);
@@ -35683,9 +36400,9 @@ function receiveJson(result, path) {
35683
36400
 
35684
36401
  // src/share/io.ts
35685
36402
  import { constants } from "fs";
35686
- import { lstat as lstat5, mkdir as mkdir4, mkdtemp, open as open7, readFile as readFile11, realpath as realpath2, stat as stat4, link as link2, rename as rename4, rm as rm6, unlink as unlink3 } from "fs/promises";
36403
+ import { lstat as lstat6, mkdir as mkdir5, mkdtemp, open as open8, readFile as readFile10, realpath as realpath2, stat as stat5, link as link2, rename as rename4, rm as rm6, unlink as unlink3 } from "fs/promises";
35687
36404
  import { randomBytes as randomBytes7 } from "crypto";
35688
- import { basename as basename4, join as join12, resolve as resolve3, sep as sep2 } from "path";
36405
+ import { basename as basename4, join as join12, resolve as resolve4, sep as sep2 } from "path";
35689
36406
  init_errors();
35690
36407
  var MAX_SHARE_STDIN_BYTES = 100 * 1024 * 1024;
35691
36408
  var MAX_SHARE_URL_BYTES = 64 * 1024;
@@ -35722,26 +36439,26 @@ function shareFilename(value) {
35722
36439
  }
35723
36440
  }
35724
36441
  function shareInputFilename(input, name) {
35725
- return shareFilename(name ?? (input === "-" ? "stdin.md" : basename4(resolve3(input))));
36442
+ return shareFilename(name ?? (input === "-" ? "stdin.md" : basename4(resolve4(input))));
35726
36443
  }
35727
36444
  async function readShareInput(input, name, limit = MAX_SHARE_STDIN_BYTES) {
35728
36445
  const filename = shareInputFilename(input, name);
35729
36446
  if (input === "-") return { bytes: await readBoundedStdin(limit), filename };
35730
- const path = resolve3(input);
35731
- const info = await stat4(path);
36447
+ const path = resolve4(input);
36448
+ const info = await stat5(path);
35732
36449
  if (!info.isFile() || info.size > limit) throw new Error("MAX_BYTES_EXCEEDED");
35733
- const bytes = new Uint8Array(await readFile11(path));
36450
+ const bytes = new Uint8Array(await readFile10(path));
35734
36451
  if (bytes.byteLength > limit) throw new Error("MAX_BYTES_EXCEEDED");
35735
36452
  return { bytes, filename };
35736
36453
  }
35737
36454
  async function assertDirectory(path) {
35738
- const absolute = resolve3(path);
36455
+ const absolute = resolve4(path);
35739
36456
  const segments = absolute.split(sep2).filter(Boolean);
35740
36457
  let current = absolute.startsWith(sep2) ? sep2 : "";
35741
36458
  for (const segment of segments) {
35742
36459
  current = current === sep2 ? join12(current, segment) : join12(current, segment);
35743
36460
  try {
35744
- const info = await lstat5(current);
36461
+ const info = await lstat6(current);
35745
36462
  if (info.isSymbolicLink()) {
35746
36463
  const canonical = await realpath2(current);
35747
36464
  if (current !== "/tmp" && current !== "/var") throw new Error("OUTPUT_EXISTS");
@@ -35749,27 +36466,27 @@ async function assertDirectory(path) {
35749
36466
  } else if (!info.isDirectory()) throw new Error("OUTPUT_EXISTS");
35750
36467
  } catch (error) {
35751
36468
  if (error.code !== "ENOENT") throw error;
35752
- await mkdir4(current, { mode: 448 });
35753
- const created = await lstat5(current);
36469
+ await mkdir5(current, { mode: 448 });
36470
+ const created = await lstat6(current);
35754
36471
  if (created.isSymbolicLink() || !created.isDirectory()) throw new Error("OUTPUT_EXISTS");
35755
36472
  }
35756
36473
  }
35757
36474
  }
35758
36475
  async function writeShareOutput(directory, filename, bytes, force) {
35759
- const outputDirectory = resolve3(directory);
36476
+ const outputDirectory = resolve4(directory);
35760
36477
  await assertDirectory(outputDirectory);
35761
36478
  const safeName = safeFilename(filename);
35762
- const directoryHandle = await open7(outputDirectory, constants.O_RDONLY | (constants.O_DIRECTORY ?? 0) | (constants.O_NOFOLLOW ?? 0));
36479
+ const directoryHandle = await open8(outputDirectory, constants.O_RDONLY | (constants.O_DIRECTORY ?? 0) | (constants.O_NOFOLLOW ?? 0));
35763
36480
  const stableDirectory = await realpath2(outputDirectory);
35764
36481
  const outputPath = join12(stableDirectory, safeName);
35765
36482
  const directoryIdentity = await directoryHandle.stat();
35766
36483
  const assertStableDirectory = async () => {
35767
- const current = await stat4(stableDirectory);
36484
+ const current = await stat5(stableDirectory);
35768
36485
  if (current.dev !== directoryIdentity.dev || current.ino !== directoryIdentity.ino) throw new Error("OUTPUT_EXISTS");
35769
36486
  };
35770
36487
  await assertStableDirectory();
35771
36488
  const stagingDirectory = await mkdtemp(join12(stableDirectory, ".tinycloud-share-stage-"));
35772
- const stagingInfo = await lstat5(stagingDirectory);
36489
+ const stagingInfo = await lstat6(stagingDirectory);
35773
36490
  if (!stagingInfo.isDirectory() || (stagingInfo.mode & 511) !== 448) throw new Error("OUTPUT_EXISTS");
35774
36491
  const stagingPath = join12(stagingDirectory, `.tinycloud-share-${randomBytes7(16).toString("hex")}.tmp`);
35775
36492
  let temporaryPath;
@@ -35777,13 +36494,13 @@ async function writeShareOutput(directory, filename, bytes, force) {
35777
36494
  try {
35778
36495
  await assertStableDirectory();
35779
36496
  try {
35780
- const existing = await lstat5(outputPath);
36497
+ const existing = await lstat6(outputPath);
35781
36498
  if (existing.isSymbolicLink()) throw new Error("UNSAFE_FILENAME");
35782
36499
  } catch (error) {
35783
36500
  if (error.code !== "ENOENT") throw error;
35784
36501
  }
35785
36502
  temporaryPath = stagingPath;
35786
- handle = await open7(temporaryPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0), 384);
36503
+ handle = await open8(temporaryPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0), 384);
35787
36504
  await handle.writeFile(bytes);
35788
36505
  await handle.close();
35789
36506
  handle = void 0;
@@ -36233,8 +36950,8 @@ init_theme();
36233
36950
  import { CLI_NAME as CLI_NAME26 } from "@tinycloud/operations/cli-name";
36234
36951
  import { ACCOUNT_REGISTRY_SPACE as ACCOUNT_REGISTRY_SPACE2 } from "@tinycloud/node-sdk";
36235
36952
  import { randomBytes as randomBytes8 } from "crypto";
36236
- import { mkdir as mkdir5, writeFile as writeFile4 } from "fs/promises";
36237
- import { dirname as dirname6 } from "path";
36953
+ import { mkdir as mkdir6, writeFile as writeFile4 } from "fs/promises";
36954
+ import { dirname as dirname7 } from "path";
36238
36955
  function didWithoutFragment2(did) {
36239
36956
  const fragment = did.indexOf("#");
36240
36957
  return fragment === -1 ? did : did.slice(0, fragment);
@@ -36390,7 +37107,7 @@ it directly with \`${CLI_NAME26} space host <name>\` (no request needed).
36390
37107
  }
36391
37108
  async function emitHostRequestArtifact(artifact, emitOption) {
36392
37109
  if (typeof emitOption === "string" && emitOption.length > 0) {
36393
- await mkdir5(dirname6(emitOption), { recursive: true });
37110
+ await mkdir6(dirname7(emitOption), { recursive: true });
36394
37111
  await writeFile4(emitOption, JSON.stringify(artifact, null, 2) + "\n", "utf8");
36395
37112
  outputJson({
36396
37113
  emitted: true,
@@ -36416,7 +37133,7 @@ init_host();
36416
37133
  init_theme();
36417
37134
  import { CLI_NAME as CLI_NAME27 } from "@tinycloud/operations/cli-name";
36418
37135
  import { writeFile as writeFile5 } from "fs/promises";
36419
- import { resolve as resolve4 } from "path";
37136
+ import { resolve as resolve5 } from "path";
36420
37137
  async function dbHandle(node, dbName, spaceInput, profileName2) {
36421
37138
  const spaceUri = await resolveSpaceUri(spaceInput, profileName2);
36422
37139
  const sql = spaceUri ? node.sqlForSpace(spaceUri) : node.sql;
@@ -36558,7 +37275,7 @@ Output:
36558
37275
  }
36559
37276
  const blob = result.data;
36560
37277
  const buffer = Buffer.from(await blob.arrayBuffer());
36561
- const outputPath = resolve4(options.output);
37278
+ const outputPath = resolve5(options.output);
36562
37279
  await writeFile5(outputPath, buffer);
36563
37280
  outputJson({
36564
37281
  file: outputPath,
@@ -37114,7 +37831,7 @@ function registerUpgradeCommand(program) {
37114
37831
  }
37115
37832
 
37116
37833
  // src/commands/vault.ts
37117
- import { readFile as readFile12 } from "fs/promises";
37834
+ import { readFile as readFile11 } from "fs/promises";
37118
37835
  init_profiles();
37119
37836
  init_formatter();
37120
37837
  init_errors();
@@ -37176,7 +37893,7 @@ function registerVaultCommand(program) {
37176
37893
  throw new CLIError("USAGE_ERROR", "Provide only one of: value argument, --file, or --stdin", ExitCode.USAGE_ERROR);
37177
37894
  }
37178
37895
  if (options.file) {
37179
- putValue = new Uint8Array(await readFile12(options.file));
37896
+ putValue = new Uint8Array(await readFile11(options.file));
37180
37897
  } else if (options.stdin) {
37181
37898
  putValue = new Uint8Array(await readStdin4());
37182
37899
  } else {
@@ -37291,7 +38008,7 @@ function registerVaultCommand(program) {
37291
38008
  }
37292
38009
 
37293
38010
  // src/commands/vars.ts
37294
- import { readFile as readFile13 } from "fs/promises";
38011
+ import { readFile as readFile12 } from "fs/promises";
37295
38012
  init_profiles();
37296
38013
  init_formatter();
37297
38014
  init_errors();
@@ -37396,7 +38113,7 @@ function registerVarsCommand(program) {
37396
38113
  throw new CLIError("USAGE_ERROR", "Provide only one of: value argument, --file, or --stdin", ExitCode.USAGE_ERROR);
37397
38114
  }
37398
38115
  if (options.file) {
37399
- varValue = await readFile13(options.file, "utf-8");
38116
+ varValue = await readFile12(options.file, "utf-8");
37400
38117
  } else if (options.stdin) {
37401
38118
  varValue = (await readStdin5()).toString("utf-8");
37402
38119
  } else {