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

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,37 @@ 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
+ });
19179
19237
  var SqliteReplicaStore = class _SqliteReplicaStore {
19180
19238
  dir;
19181
19239
  #db;
19240
+ #pinnedDir;
19241
+ #directoryFd;
19182
19242
  #holder = `${process.pid}-${randomBytes2(6).toString("hex")}`;
19183
19243
  #now;
19184
19244
  #faults;
19185
19245
  #guard;
19186
19246
  /** Identity of the database file this store opened: a deleted-and-recreated replica is a different file. */
19187
19247
  #ino;
19188
- constructor(dir, db, ino, options) {
19248
+ #dev;
19249
+ #namedAncestors;
19250
+ constructor(dir, db, identity, options, directoryFd, namedAncestors = []) {
19189
19251
  this.dir = dir;
19252
+ this.#pinnedDir = directoryFd !== void 0 && process.platform === "linux" ? `/proc/self/fd/${directoryFd}` : dir;
19253
+ this.#directoryFd = directoryFd;
19254
+ if (directoryFd !== void 0) pinnedDirectories.register(this, directoryFd, this);
19190
19255
  this.#db = db;
19191
- this.#ino = ino;
19256
+ this.#ino = identity.ino;
19257
+ this.#dev = identity.dev;
19192
19258
  this.#now = options.now ?? Date.now;
19193
19259
  this.#faults = options[FAULTS] ?? {};
19194
19260
  this.#guard = options.guard ?? ((section) => section());
19261
+ this.#namedAncestors = namedAncestors;
19195
19262
  }
19196
19263
  /**
19197
19264
  * Read-only status snapshot; does not create a replica, migrate, recover
@@ -19204,9 +19271,10 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
19204
19271
  };
19205
19272
  checkAbort();
19206
19273
  const dbPath = join7(dir, "replica.db");
19207
- let ino;
19274
+ let identity;
19208
19275
  try {
19209
- ino = (await stat2(dbPath)).ino;
19276
+ identity = await lstat2(dbPath);
19277
+ if (!identity.isFile()) return null;
19210
19278
  } catch (error) {
19211
19279
  if (errnoOf(error) === "ENOENT") return null;
19212
19280
  throw storageError(error, "Inspecting the replica");
@@ -19219,14 +19287,14 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
19219
19287
  db = opener(dbPath, { readonly: true });
19220
19288
  db.exec("PRAGMA busy_timeout = 0");
19221
19289
  checkAbort();
19222
- const store = new _SqliteReplicaStore(dir, db, ino, { create: false, ...options.now ? { now: options.now } : {} });
19290
+ const store = new _SqliteReplicaStore(dir, db, identity, { create: false, ...options.now ? { now: options.now } : {} });
19223
19291
  for (let attempt = 0; attempt < 2; attempt++) {
19224
19292
  const before = db.get("PRAGMA data_version").data_version;
19225
19293
  const state2 = await store.open();
19226
19294
  checkAbort();
19227
19295
  if (state2 === null) return null;
19228
19296
  const status = await store.status();
19229
- await new Promise((resolve5) => setImmediate(resolve5));
19297
+ await new Promise((resolve22) => setImmediate(resolve22));
19230
19298
  checkAbort();
19231
19299
  const after = db.get("PRAGMA data_version").data_version;
19232
19300
  if (before === after) return { state: state2, status };
@@ -19246,65 +19314,145 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
19246
19314
  * REPLICA_NOT_FOUND and nothing is written to disk.
19247
19315
  */
19248
19316
  static async open(dir, options) {
19249
- const dbPath = join7(dir, "replica.db");
19317
+ const namedDbPath = join7(dir, "replica.db");
19250
19318
  if (!options.create) {
19251
- const exists = await stat2(dbPath).then(
19252
- () => true,
19319
+ const exists = await lstat2(namedDbPath).then(
19320
+ (stats) => stats.isFile(),
19253
19321
  () => false
19254
19322
  );
19255
- if (!exists) throw new ReplicaError2(ReplicaErrorCode2.NOT_FOUND, `No replica at ${dir}.`);
19323
+ if (!exists) throw new ReplicaError2(ReplicaErrorCode2.NOT_FOUND, `No regular replica database at ${dir}.`);
19256
19324
  }
19257
19325
  const opener = options.sqlite ?? await loadSqlite();
19326
+ const releaseOpen = await acquireSqliteOpen();
19258
19327
  let store;
19328
+ let directoryFd;
19259
19329
  try {
19330
+ if (options.create) await mkdir2(dir, { recursive: true, mode: 448 });
19331
+ if (process.platform === "linux") {
19332
+ directoryFd = openSync(dir, fsConstants.O_RDONLY | fsConstants.O_DIRECTORY | fsConstants.O_NOFOLLOW);
19333
+ }
19334
+ const opened = directoryFd === void 0 ? lstatSync(dir, { bigint: true }) : fstatSync(directoryFd, { bigint: true });
19335
+ if (!opened.isDirectory() || opened.isSymbolicLink() || options.expectedDirectory && (opened.dev !== BigInt(options.expectedDirectory.dev) || opened.ino !== BigInt(options.expectedDirectory.ino))) {
19336
+ throw new Error("The replica directory changed before SQLite opened it.");
19337
+ }
19338
+ const namedAncestors = process.platform === "linux" ? [] : await captureNamedPath(dir);
19339
+ if (namedAncestors.length && (namedAncestors[namedAncestors.length - 1].dev !== opened.dev || namedAncestors[namedAncestors.length - 1].ino !== opened.ino)) {
19340
+ throw new Error("The replica directory changed while checking its ancestry.");
19341
+ }
19342
+ const pinnedDir = directoryFd === void 0 ? dir : `/proc/self/fd/${directoryFd}`;
19343
+ const dbPath = join7(pinnedDir, "replica.db");
19260
19344
  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
- );
19345
+ await mkdir2(join7(pinnedDir, "blobs", ".tmp"), { recursive: true, mode: 448 });
19346
+ await Promise.all([chmod3(pinnedDir, 448), chmod3(join7(pinnedDir, "blobs"), 448), chmod3(join7(pinnedDir, "blobs", ".tmp"), 448)]);
19347
+ const handle = await open2(dbPath, fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_RDWR | fsConstants.O_NOFOLLOW, 384).catch((error) => {
19348
+ if (errnoOf(error) === "EEXIST") return null;
19349
+ throw error;
19350
+ });
19351
+ if (handle) {
19352
+ try {
19353
+ await handle.chmod(384);
19354
+ } finally {
19355
+ await handle.close();
19356
+ }
19357
+ }
19281
19358
  }
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");
19359
+ const leaf = await open2(dbPath, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
19360
+ let db;
19361
+ try {
19362
+ const identity = await leaf.stat();
19363
+ const named = await lstat2(dbPath);
19364
+ if (!identity.isFile() || !named.isFile() || named.dev !== identity.dev || named.ino !== identity.ino) {
19365
+ throw new Error("The replica database changed before SQLite opened it.");
19366
+ }
19367
+ const previousFds = process.platform === "linux" && !options.sqlite ? new Map(readdirSync2("/proc/self/fd").map((fd) => {
19368
+ try {
19369
+ return [fd, readlinkSync(`/proc/self/fd/${fd}`)];
19370
+ } catch {
19371
+ return [fd, ""];
19372
+ }
19373
+ })) : null;
19374
+ db = opener(dbPath);
19375
+ const afterOpen = lstatSync(dbPath);
19376
+ if (!afterOpen.isFile() || afterOpen.dev !== identity.dev || afterOpen.ino !== identity.ino) {
19377
+ throw new Error("The replica database changed while SQLite opened it.");
19378
+ }
19379
+ if (previousFds) db.get("SELECT count(*) FROM sqlite_master");
19380
+ if (previousFds) {
19381
+ const databases = readdirSync2("/proc/self/fd").filter((fd) => {
19382
+ try {
19383
+ const target = readlinkSync(`/proc/self/fd/${fd}`);
19384
+ return target.endsWith("/replica.db") && previousFds.get(fd) !== target;
19385
+ } catch {
19386
+ return false;
19387
+ }
19388
+ });
19389
+ if (databases.length === 0 || databases.some((fd) => {
19390
+ try {
19391
+ const actual = fstatSync(Number(fd));
19392
+ return actual.dev !== identity.dev || actual.ino !== identity.ino;
19393
+ } catch {
19394
+ return true;
19395
+ }
19396
+ })) {
19397
+ throw new Error("SQLite opened a different replica database.");
19398
+ }
19399
+ }
19400
+ const after = lstatSync(dbPath);
19401
+ if (!after.isFile() || after.dev !== identity.dev || after.ino !== identity.ino) {
19402
+ throw new Error("The replica database changed while SQLite opened it.");
19403
+ }
19404
+ db.exec("PRAGMA busy_timeout = 5000");
19405
+ db.exec("PRAGMA journal_mode = WAL");
19406
+ db.exec("PRAGMA synchronous = FULL");
19407
+ db.exec("PRAGMA secure_delete = ON");
19408
+ db.exec("PRAGMA foreign_keys = ON");
19409
+ const version = db.get("PRAGMA user_version")?.user_version ?? 0;
19410
+ if (version > SCHEMA_VERSION) {
19411
+ throw new ReplicaError2(
19412
+ ReplicaErrorCode2.STORAGE_ERROR,
19413
+ `The replica at ${dir} was written by a newer tc (schema ${version}); upgrade tc to read it.`
19414
+ );
19415
+ }
19416
+ if (version < SCHEMA_VERSION) {
19417
+ db.exec("BEGIN IMMEDIATE");
19418
+ db.exec(SCHEMA);
19419
+ db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
19420
+ db.exec("COMMIT");
19421
+ }
19422
+ store = new _SqliteReplicaStore(dir, db, identity, options, directoryFd, namedAncestors);
19423
+ db = void 0;
19424
+ } finally {
19425
+ try {
19426
+ db?.close();
19427
+ } finally {
19428
+ await leaf.close();
19429
+ }
19287
19430
  }
19288
- store = new _SqliteReplicaStore(dir, db, ino, options);
19289
19431
  } catch (error) {
19432
+ if (store) await store.close().catch(() => void 0);
19433
+ else if (directoryFd !== void 0) closeSync(directoryFd);
19290
19434
  throw storageError(error, `Opening the replica at ${dir}`);
19435
+ } finally {
19436
+ releaseOpen();
19291
19437
  }
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);
19438
+ try {
19439
+ const row = store.#row();
19440
+ if (row?.revoked_detail != null && (row.purge_pending === 1 || await store.#holdsContentFiles().catch(() => true))) {
19441
+ await store.#finishRevocationPurge().catch(() => void 0);
19442
+ }
19443
+ return store;
19444
+ } catch (error) {
19445
+ await store.close().catch(() => void 0);
19446
+ throw error;
19295
19447
  }
19296
- return store;
19297
19448
  }
19298
19449
  #row() {
19299
19450
  return this.#db.get("SELECT * FROM replica WHERE id = 1");
19300
19451
  }
19301
19452
  /** Refuse to touch the filesystem for a replica that was deleted (or replaced) under us. */
19302
19453
  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) {
19454
+ const identity = await lstat2(join7(this.#pinnedDir, "replica.db")).catch(() => null);
19455
+ if (!identity?.isFile() || identity.dev !== this.#dev || identity.ino !== this.#ino) {
19308
19456
  throw new ReplicaError2(
19309
19457
  ReplicaErrorCode2.NOT_FOUND,
19310
19458
  `The replica at ${this.dir} was deleted while this command ran (its profile was deleted or the replica purged); nothing more was written.`
@@ -19534,7 +19682,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
19534
19682
  return held;
19535
19683
  }
19536
19684
  #blobPath(hash) {
19537
- return join7(this.dir, "blobs", hash.slice(0, 2), hash);
19685
+ return join7(this.#pinnedDir, "blobs", hash.slice(0, 2), hash);
19538
19686
  }
19539
19687
  /**
19540
19688
  * Make every blob durable before any row references it: write to a fresh
@@ -19551,11 +19699,11 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
19551
19699
  () => false
19552
19700
  );
19553
19701
  if (exists) continue;
19554
- const shard = join7(this.dir, "blobs", hash.slice(0, 2));
19702
+ const shard = join7(this.#pinnedDir, "blobs", hash.slice(0, 2));
19555
19703
  await mkdir2(shard, { mode: 448 }).catch((error) => {
19556
19704
  if (errnoOf(error) !== "EEXIST") throw error;
19557
19705
  });
19558
- const temp = join7(this.dir, "blobs", ".tmp", `${hash}.${randomBytes2(6).toString("hex")}`);
19706
+ const temp = join7(this.#pinnedDir, "blobs", ".tmp", `${hash}.${randomBytes2(6).toString("hex")}`);
19559
19707
  const handle = await open2(temp, fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_WRONLY, 384);
19560
19708
  try {
19561
19709
  await handle.writeFile(bytes);
@@ -19571,7 +19719,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
19571
19719
  await unlink(temp);
19572
19720
  }
19573
19721
  dirs.add(shard);
19574
- dirs.add(join7(this.dir, "blobs"));
19722
+ dirs.add(join7(this.#pinnedDir, "blobs"));
19575
19723
  }
19576
19724
  for (const dir of dirs) {
19577
19725
  const handle = await open2(dir, fsConstants.O_RDONLY);
@@ -19679,7 +19827,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
19679
19827
  /** Unlink every blob not in `keep`, and every temp file. Only a missing directory counts as empty. */
19680
19828
  async #unlinkBlobs(keep) {
19681
19829
  let removed = 0;
19682
- const root = join7(this.dir, "blobs");
19830
+ const root = join7(this.#pinnedDir, "blobs");
19683
19831
  for (const shard of await entriesOf(root)) {
19684
19832
  const shardPath = join7(root, shard);
19685
19833
  if (shard === ".tmp") {
@@ -19698,13 +19846,13 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
19698
19846
  await rm3(join7(shardPath, name), { force: true });
19699
19847
  removed += 1;
19700
19848
  }
19701
- if (kept === 0) await rmdir2(shardPath).catch(() => void 0);
19849
+ if (kept === 0) await rmdir(shardPath).catch(() => void 0);
19702
19850
  }
19703
19851
  return removed;
19704
19852
  }
19705
19853
  /** Whether any content file (a blob or a temp file) is on disk. */
19706
19854
  async #holdsContentFiles() {
19707
- const root = join7(this.dir, "blobs");
19855
+ const root = join7(this.#pinnedDir, "blobs");
19708
19856
  for (const shard of await entriesOf(root)) {
19709
19857
  if ((await entriesOf(join7(root, shard))).length > 0) return true;
19710
19858
  }
@@ -19770,8 +19918,93 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
19770
19918
  }
19771
19919
  try {
19772
19920
  this.#checkLease(t, { allowRevoked: true });
19921
+ const expected = this.#directoryFd === void 0 ? this.#namedAncestors.at(-1) : fstatSync(this.#directoryFd, { bigint: true });
19922
+ if (!expected) {
19923
+ throw new ReplicaError2(ReplicaErrorCode2.RUNTIME_UNSUPPORTED, "Replica removal requires an opened or identity-checked directory.");
19924
+ }
19925
+ const assertDirectory2 = async () => {
19926
+ const current = await lstat2(this.dir, { bigint: true });
19927
+ if (!current.isDirectory() || current.dev !== expected.dev || current.ino !== expected.ino) {
19928
+ throw new ReplicaError2(ReplicaErrorCode2.NOT_FOUND, "The replica directory changed during removal.");
19929
+ }
19930
+ return current;
19931
+ };
19932
+ const pinned = await assertDirectory2();
19773
19933
  await this.#assertPresent();
19774
- await rm3(this.dir, { recursive: true, force: true });
19934
+ if (process.platform === "linux") {
19935
+ for (const entry of await readdir3(this.#pinnedDir)) {
19936
+ await rm3(join7(this.#pinnedDir, entry), { recursive: true, force: true });
19937
+ }
19938
+ await assertDirectory2();
19939
+ await rmdir(this.dir);
19940
+ } else {
19941
+ const ancestors = this.#namedAncestors;
19942
+ const checkAncestors = async () => {
19943
+ for (const expected2 of ancestors) {
19944
+ const current = await lstat2(expected2.path, { bigint: true });
19945
+ if (current.dev !== expected2.dev || current.ino !== expected2.ino) {
19946
+ throw new ReplicaError2(ReplicaErrorCode2.NOT_FOUND, "The replica ancestry changed during removal.");
19947
+ }
19948
+ }
19949
+ };
19950
+ const directories = /* @__PURE__ */ new Map([[this.dir, pinned]]);
19951
+ const files = /* @__PURE__ */ new Map();
19952
+ const check = async (path, expected2, directory) => {
19953
+ if (path !== this.dir) {
19954
+ const parent = dirname3(path);
19955
+ await check(parent, directories.get(parent), true);
19956
+ }
19957
+ await checkAncestors();
19958
+ const current = await lstat2(path, { bigint: true });
19959
+ if ((directory ? !current.isDirectory() : !current.isFile()) || current.dev !== expected2.dev || current.ino !== expected2.ino) {
19960
+ throw new ReplicaError2(ReplicaErrorCode2.NOT_FOUND, "The replica layout changed during removal.");
19961
+ }
19962
+ };
19963
+ const record2 = async (path, directory) => {
19964
+ const parent = dirname3(path);
19965
+ await check(parent, directories.get(parent), true);
19966
+ const current = await lstat2(path, { bigint: true });
19967
+ if (!(directory ? current.isDirectory() : current.isFile())) {
19968
+ throw new ReplicaError2(ReplicaErrorCode2.STORAGE_ERROR, "Unexpected replica layout during removal.");
19969
+ }
19970
+ (directory ? directories : files).set(path, current);
19971
+ };
19972
+ const blobs = join7(this.dir, "blobs");
19973
+ for (const name of await readdir3(this.dir)) {
19974
+ if (name === "blobs") await record2(blobs, true);
19975
+ else if (["replica.db", "replica.db-wal", "replica.db-shm"].includes(name)) await record2(join7(this.dir, name), false);
19976
+ else throw new ReplicaError2(ReplicaErrorCode2.STORAGE_ERROR, "Unexpected replica layout during removal.");
19977
+ }
19978
+ if (directories.has(blobs)) {
19979
+ await check(blobs, directories.get(blobs), true);
19980
+ for (const shard of await readdir3(blobs)) {
19981
+ if (shard !== ".tmp" && !/^[0-9a-f]{2}$/.test(shard)) {
19982
+ throw new ReplicaError2(ReplicaErrorCode2.STORAGE_ERROR, "Unexpected replica blob shard during removal.");
19983
+ }
19984
+ const shardPath = join7(blobs, shard);
19985
+ await record2(shardPath, true);
19986
+ await check(shardPath, directories.get(shardPath), true);
19987
+ for (const name of await readdir3(shardPath)) {
19988
+ if (shard === ".tmp" ? !/^[0-9a-f]{64}\.[0-9a-f]{12}$/.test(name) : !isHash(name) || !name.startsWith(shard)) {
19989
+ throw new ReplicaError2(ReplicaErrorCode2.STORAGE_ERROR, "Unexpected replica blob file during removal.");
19990
+ }
19991
+ await record2(join7(shardPath, name), false);
19992
+ }
19993
+ }
19994
+ }
19995
+ for (const [path, identity] of files) {
19996
+ await check(path, identity, false);
19997
+ await unlink(path);
19998
+ }
19999
+ for (const [path, identity] of [...directories].reverse()) {
20000
+ if (path === this.dir) continue;
20001
+ await check(path, identity, true);
20002
+ await rmdir(path);
20003
+ }
20004
+ await assertDirectory2();
20005
+ await checkAncestors();
20006
+ await rmdir(this.dir);
20007
+ }
19775
20008
  } catch (error) {
19776
20009
  throw storageError(error, "Removing the replica");
19777
20010
  } finally {
@@ -19937,7 +20170,14 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
19937
20170
  };
19938
20171
  }
19939
20172
  async close() {
19940
- this.#db.close();
20173
+ const fd = this.#directoryFd;
20174
+ this.#directoryFd = void 0;
20175
+ if (fd !== void 0) pinnedDirectories.unregister(this);
20176
+ try {
20177
+ this.#db.close();
20178
+ } finally {
20179
+ if (fd !== void 0) closeSync(fd);
20180
+ }
19941
20181
  }
19942
20182
  };
19943
20183
 
@@ -19945,229 +20185,388 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
19945
20185
  init_errors();
19946
20186
  init_constants();
19947
20187
  var SYNC_LEASE_MS = 6e4;
19948
- function refuseUnsafeReplicaPath(profile, detail) {
20188
+ function refuseUnsafeReplicaPath(profile, detail, operation = "logout") {
19949
20189
  return new CLIError(
19950
20190
  "REPLICA_PURGE_FAILED",
19951
- `Logout for profile "${profile}" cleared the session, but ${detail}; the replicas were left untouched.`,
20191
+ 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
20192
  ExitCode.ERROR
19953
20193
  );
19954
20194
  }
19955
- async function safeReplicaRoot(profile, root) {
20195
+ async function safeReplicaRoot(profile, root, operation = "logout") {
19956
20196
  let rootStats;
19957
20197
  try {
19958
- rootStats = await lstat2(root);
20198
+ rootStats = root.startsWith("/proc/self/fd/") ? await stat3(root) : await lstat3(root);
19959
20199
  } catch (error) {
19960
20200
  if (error?.code === "ENOENT") return null;
19961
20201
  throw error;
19962
20202
  }
19963
20203
  if (rootStats.isSymbolicLink()) {
19964
- throw refuseUnsafeReplicaPath(profile, `the replicas directory "${root}" is a symlink`);
20204
+ throw refuseUnsafeReplicaPath(profile, `the replicas directory "${root}" is a symlink`, operation);
19965
20205
  }
19966
20206
  if (!rootStats.isDirectory()) {
19967
- throw refuseUnsafeReplicaPath(profile, `the replicas path "${root}" is not a real directory`);
20207
+ throw refuseUnsafeReplicaPath(profile, `the replicas path "${root}" is not a real directory`, operation);
19968
20208
  }
19969
- const [profileRealPath, rootRealPath] = await Promise.all([realpath(profilePath3(profile)), realpath(root)]);
20209
+ const [profileRealPath, rootRealPath] = await Promise.all([realpath(profilePath4(profile)), realpath(root)]);
19970
20210
  if (!rootRealPath.startsWith(profileRealPath + sep)) {
19971
- throw refuseUnsafeReplicaPath(profile, `the replicas directory "${root}" resolves outside the selected profile`);
20211
+ throw refuseUnsafeReplicaPath(profile, `the replicas directory "${root}" resolves outside the selected profile`, operation);
19972
20212
  }
19973
20213
  return rootRealPath;
19974
20214
  }
19975
- async function safeReplicaEntry(profile, root, entry) {
20215
+ async function safeReplicaEntry(profile, root, entry, operation = "logout") {
19976
20216
  const entryPath = join8(root, entry.name);
19977
- const rootRealPath = await safeReplicaRoot(profile, root);
20217
+ const rootRealPath = await safeReplicaRoot(profile, root, operation);
19978
20218
  if (rootRealPath === null) {
19979
- throw refuseUnsafeReplicaPath(profile, `the replicas directory "${root}" disappeared`);
20219
+ throw refuseUnsafeReplicaPath(profile, `the replicas directory "${root}" disappeared`, operation);
19980
20220
  }
19981
- const entryStats = await lstat2(entryPath);
20221
+ const entryStats = await lstat3(entryPath);
19982
20222
  if (entry.isSymbolicLink() || entryStats.isSymbolicLink()) {
19983
- throw refuseUnsafeReplicaPath(profile, `replica entry "${entry.name}" is a symlink`);
20223
+ throw refuseUnsafeReplicaPath(profile, `replica entry "${entry.name}" is a symlink`, operation);
19984
20224
  }
19985
20225
  if (!entry.isDirectory() || !entryStats.isDirectory()) {
19986
- throw refuseUnsafeReplicaPath(profile, `replica entry "${entry.name}" is not a real directory`);
20226
+ throw refuseUnsafeReplicaPath(profile, `replica entry "${entry.name}" is not a real directory`, operation);
19987
20227
  }
19988
20228
  const entryRealPath = await realpath(entryPath);
19989
20229
  if (!entryRealPath.startsWith(rootRealPath + sep)) {
19990
- throw refuseUnsafeReplicaPath(profile, `replica entry "${entry.name}" resolves outside the replicas directory`);
20230
+ throw refuseUnsafeReplicaPath(profile, `replica entry "${entry.name}" resolves outside the replicas directory`, operation);
20231
+ }
20232
+ return { path: entryPath, identity: { dev: entryStats.dev, ino: entryStats.ino } };
20233
+ }
20234
+ async function inspectLegacyReplica(profile, root, entry) {
20235
+ const { path, identity } = await safeReplicaEntry(profile, root, entry);
20236
+ const handle = await open3(path, fsConstants2.O_RDONLY | fsConstants2.O_DIRECTORY | fsConstants2.O_NOFOLLOW);
20237
+ try {
20238
+ const pinnedEntry = await handle.stat();
20239
+ if (pinnedEntry.dev !== identity.dev || pinnedEntry.ino !== identity.ino) {
20240
+ throw refuseUnsafeReplicaPath(profile, `replica entry "${entry.name}" changed during inspection`);
20241
+ }
20242
+ const children = await readdir4(`/proc/self/fd/${handle.fd}`, { withFileTypes: true });
20243
+ if (children.length > 0 && !children.some((child) => child.name === "replica.db" && child.isFile())) {
20244
+ throw refuseUnsafeReplicaPath(profile, `replica entry "${entry.name}" has files but no regular replica database`);
20245
+ }
20246
+ return { path, identity, empty: children.length === 0 };
20247
+ } finally {
20248
+ await handle.close();
20249
+ }
20250
+ }
20251
+ async function pinReplicaRoot(profile, named, operation) {
20252
+ const initial = await lstat3(named).catch((error) => {
20253
+ if (error.code === "ENOENT") return null;
20254
+ throw error;
20255
+ });
20256
+ if (await safeReplicaRoot(profile, named, operation) === null) return null;
20257
+ if (process.platform !== "linux") {
20258
+ throw refuseUnsafeReplicaPath(profile, "pinned replica removal is unavailable on this platform", operation);
20259
+ }
20260
+ const handle = await open3(named, fsConstants2.O_RDONLY | fsConstants2.O_DIRECTORY | fsConstants2.O_NOFOLLOW);
20261
+ try {
20262
+ const pinned = await handle.stat();
20263
+ const assertNamed = async () => {
20264
+ const current = await lstat3(named);
20265
+ if (!current.isDirectory() || current.dev !== initial?.dev || current.ino !== initial?.ino || current.dev !== pinned.dev || current.ino !== pinned.ino) {
20266
+ throw refuseUnsafeReplicaPath(profile, "the replicas directory changed during cleanup", operation);
20267
+ }
20268
+ };
20269
+ await assertNamed();
20270
+ return { path: `/proc/self/fd/${handle.fd}`, assertNamed, close: () => handle.close() };
20271
+ } catch (error) {
20272
+ await handle.close();
20273
+ throw error;
19991
20274
  }
19992
- return entryPath;
19993
20275
  }
19994
- async function removeProfileReplicas(profile, heldLeases) {
20276
+ async function withPinnedReplicaForReset(profile, name, action) {
19995
20277
  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 = [];
20278
+ const root = join8(profilePath4(profile), "replicas");
20279
+ const pinned = await pinReplicaRoot(profile, root, "reset");
20280
+ if (!pinned) throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, `Profile "${profile}" has no replicas.`);
20003
20281
  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.`);
20282
+ const fdRoot = pinned.path;
20283
+ const entry = (await readdir4(fdRoot, { withFileTypes: true })).find((item) => item.name === name);
20284
+ if (!entry) throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, `Replica "${name}" was not found.`);
20285
+ const { path, identity: initial } = await safeReplicaEntry(profile, fdRoot, entry, "reset");
20286
+ const recheck = async () => {
20287
+ const current = await lstat3(path);
20288
+ if (!current.isDirectory() || current.isSymbolicLink() || current.dev !== initial.dev || current.ino !== initial.ino) {
20289
+ throw refuseUnsafeReplicaPath(profile, `replica entry "${name}" changed during removal`, "reset");
20290
+ }
20291
+ };
20292
+ await recheck();
20293
+ return await action(path, recheck, { dev: initial.dev, ino: initial.ino });
20294
+ } finally {
20295
+ await pinned.close();
20296
+ }
20297
+ });
20298
+ }
20299
+ async function removeProfileReplicas(profile, heldLeases, preflightRoot) {
20300
+ return withProfileLock4(profile, async () => {
20301
+ const namedRoot = join8(profilePath4(profile), "replicas");
20302
+ const pinned = preflightRoot ?? await pinReplicaRoot(profile, namedRoot, "logout");
20303
+ if (!pinned) return [];
20304
+ if (preflightRoot) await pinned.assertNamed();
20305
+ try {
20306
+ const root = pinned.path;
20307
+ const replicas = (await readdir4(root, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
20308
+ if (replicas.length === 0) return [];
20309
+ const checked = await Promise.all(replicas.map(async (entry) => ({
20310
+ entry,
20311
+ ...await inspectLegacyReplica(profile, root, entry)
20312
+ })));
20313
+ const removed = [];
20314
+ const opened = [];
20315
+ try {
20316
+ for (const { entry, identity, empty: empty4 } of checked) {
20317
+ if (!empty4) continue;
20318
+ await pinned.assertNamed();
20319
+ const current = await safeReplicaEntry(profile, root, entry);
20320
+ if (current.identity.dev !== identity.dev || current.identity.ino !== identity.ino) {
20321
+ throw refuseUnsafeReplicaPath(profile, `empty replica entry "${entry.name}" changed during cleanup`);
20322
+ }
20323
+ const handle = await open3(current.path, fsConstants2.O_RDONLY | fsConstants2.O_DIRECTORY | fsConstants2.O_NOFOLLOW);
20324
+ try {
20325
+ const pinnedEntry = await handle.stat();
20326
+ if (pinnedEntry.dev !== identity.dev || pinnedEntry.ino !== identity.ino || (await readdir4(`/proc/self/fd/${handle.fd}`)).length !== 0) {
20327
+ throw refuseUnsafeReplicaPath(profile, `empty replica entry "${entry.name}" changed during cleanup`);
20328
+ }
20329
+ await pinned.assertNamed();
20330
+ const namedEntry = await lstat3(current.path);
20331
+ if (!namedEntry.isDirectory() || namedEntry.dev !== pinnedEntry.dev || namedEntry.ino !== pinnedEntry.ino) {
20332
+ throw refuseUnsafeReplicaPath(profile, `empty replica entry "${entry.name}" changed at removal`);
20333
+ }
20334
+ await rmdir2(current.path);
20335
+ } finally {
20336
+ await handle.close();
20337
+ }
20338
+ removed.push(entry.name);
20339
+ }
20340
+ if (removed.length === replicas.length) return removed;
20341
+ await loadSqlite();
20342
+ for (const { entry, empty: empty4 } of checked) {
20343
+ if (empty4) continue;
20344
+ const { path: entryPath, identity: expectedDirectory } = await safeReplicaEntry(profile, root, entry);
20345
+ const store = await SqliteReplicaStore.open(entryPath, {
20346
+ create: false,
20347
+ expectedDirectory,
20348
+ guard: async (action) => {
20349
+ try {
20350
+ return await withProfileLock4(profile, async () => {
20351
+ await safeReplicaEntry(profile, root, entry);
20352
+ return action();
20353
+ }, { requireProfile: true });
20354
+ } catch (error) {
20355
+ if (error instanceof ProfileDeletedError2) {
20356
+ throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, `Profile "${profile}" was deleted; its replicas are gone and nothing was written.`);
20357
+ }
20358
+ throw error;
20014
20359
  }
20015
- throw error;
20016
20360
  }
20361
+ });
20362
+ opened.push({ name: entry.name, store, lease: null });
20363
+ if (await store.open() === null) {
20364
+ throw new CLIError("REPLICA_PURGE_FAILED", `Replica "${entry.name}" has no initialized store; logout cleared the session but removed no replicas.`, ExitCode.ERROR);
20017
20365
  }
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);
20366
+ const lease = heldLeases?.get(join8(namedRoot, entry.name)) ?? await store.acquireSyncLease(SYNC_LEASE_MS);
20367
+ if (lease === null) {
20368
+ throw new CLIError("REPLICA_BUSY", `Cannot log out: replica "${entry.name}" is syncing. The session was cleared and no replicas were removed.`, ExitCode.ERROR);
20369
+ }
20370
+ opened[opened.length - 1].lease = lease;
20026
20371
  }
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
- );
20372
+ for (const replica of opened) {
20373
+ try {
20374
+ await replica.store.destroy(replica.lease);
20375
+ removed.push(replica.name);
20376
+ } catch (error) {
20377
+ const replicaStillExists = await lstat3(join8(root, replica.name)).then(
20378
+ () => true,
20379
+ (statError) => statError.code !== "ENOENT"
20380
+ );
20381
+ if (!replicaStillExists) removed.push(replica.name);
20382
+ const remaining = opened.filter(({ name }) => !removed.includes(name)).map(({ name }) => name);
20383
+ throw new CLIError(
20384
+ "REPLICA_PURGE_FAILED",
20385
+ `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)}`,
20386
+ ExitCode.ERROR,
20387
+ { replicasRemoved: removed, replicasRemaining: remaining }
20388
+ );
20389
+ }
20047
20390
  }
20391
+ return removed;
20392
+ } catch (error) {
20393
+ if (error instanceof CLIError) throw error;
20394
+ throw new CLIError(
20395
+ "REPLICA_PURGE_FAILED",
20396
+ `Logout cleared the session, but replica cleanup could not finish. Removed: ${removed.length ? removed.join(", ") : "none"}. ${error instanceof Error ? error.message : String(error)}`,
20397
+ ExitCode.ERROR
20398
+ );
20399
+ } finally {
20400
+ await Promise.all(opened.map(async ({ name, store, lease }) => {
20401
+ if (lease !== null && heldLeases?.has(join8(namedRoot, name)) !== true) await store.releaseLease(lease).catch(() => void 0);
20402
+ await store.close().catch(() => void 0);
20403
+ }));
20048
20404
  }
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
20405
  } 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
- }));
20406
+ if (!preflightRoot) await pinned.close();
20062
20407
  }
20063
20408
  });
20064
20409
  }
20065
- async function removeProfileReplication(profile, heldLeases) {
20410
+ async function assertSafeFlagReplica(profile, root, partition, name) {
20411
+ const rootReal = await safeReplicaRoot(profile, root);
20412
+ if (rootReal === null) throw refuseUnsafeReplicaPath(profile, `the replication root "${root}" disappeared`);
20413
+ const partitionPath = join8(root, partition);
20414
+ const partitionStats = await lstat3(partitionPath);
20415
+ if (partitionStats.isSymbolicLink() || !partitionStats.isDirectory()) {
20416
+ throw refuseUnsafeReplicaPath(profile, `replication entry "${partition}" is not a real directory`);
20417
+ }
20418
+ const partitionReal = await realpath(partitionPath);
20419
+ if (!partitionReal.startsWith(rootReal + sep)) {
20420
+ throw refuseUnsafeReplicaPath(profile, `replication entry "${partition}" resolves outside the replication root`);
20421
+ }
20422
+ const replicasRoot = join8(partitionPath, "replicas");
20423
+ const replicasStats = await lstat3(replicasRoot);
20424
+ if (replicasStats.isSymbolicLink() || !replicasStats.isDirectory()) {
20425
+ throw refuseUnsafeReplicaPath(profile, `replication replicas path "${replicasRoot}" is not a real directory`);
20426
+ }
20427
+ const replicaPath = join8(replicasRoot, name);
20428
+ const replicaStats = await lstat3(replicaPath);
20429
+ if (replicaStats.isSymbolicLink() || !replicaStats.isDirectory()) {
20430
+ throw refuseUnsafeReplicaPath(profile, `replication replica "${name}" is not a real directory`);
20431
+ }
20432
+ const replicaReal = await realpath(replicaPath);
20433
+ if (!replicaReal.startsWith(partitionReal + sep)) {
20434
+ throw refuseUnsafeReplicaPath(profile, `replication replica "${name}" resolves outside the partition`);
20435
+ }
20436
+ const database = await lstat3(join8(replicaPath, "replica.db"));
20437
+ if (database.isSymbolicLink() || !database.isFile()) {
20438
+ throw refuseUnsafeReplicaPath(profile, `replication database "${name}" is not a regular file`);
20439
+ }
20440
+ return { dev: replicaStats.dev, ino: replicaStats.ino };
20441
+ }
20442
+ async function removeProfileReplication(profile, heldLeases, preflightRoot) {
20066
20443
  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;
20444
+ const namedRoot = join8(profilePath4(profile), "replication");
20445
+ const pinned = preflightRoot ?? await pinReplicaRoot(profile, namedRoot, "logout");
20446
+ if (!pinned) return [];
20447
+ if (preflightRoot) await pinned.assertNamed();
20075
20448
  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`);
20449
+ const root = pinned.path;
20450
+ const rootReal = await safeReplicaRoot(profile, root);
20451
+ const profileReal = await realpath(profilePath4(profile));
20452
+ const partitions = await readdir4(root, { withFileTypes: true });
20453
+ const opened = [];
20454
+ const removed = [];
20455
+ let deleting = false;
20456
+ try {
20457
+ for (const partition of partitions) {
20458
+ const partitionPath = join8(root, partition.name);
20459
+ const partitionStats = await lstat3(partitionPath);
20460
+ if (partition.isSymbolicLink() || partitionStats.isSymbolicLink()) {
20461
+ throw refuseUnsafeReplicaPath(profile, `replication entry "${partition.name}" is a symlink`);
20095
20462
  }
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`);
20463
+ if (partitionStats.isDirectory()) {
20464
+ const partitionReal = await realpath(partitionPath);
20465
+ if (!partitionReal.startsWith(rootReal + sep) || !partitionReal.startsWith(profileReal + sep)) {
20466
+ throw refuseUnsafeReplicaPath(profile, `replication entry "${partition.name}" resolves outside the selected profile`);
20102
20467
  }
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`);
20468
+ const replicasRoot = join8(partitionPath, "replicas");
20469
+ const replicasStats = await lstat3(replicasRoot).catch((error) => {
20470
+ if (error.code === "ENOENT") return null;
20471
+ throw error;
20472
+ });
20473
+ if (!replicasStats) continue;
20474
+ if (replicasStats.isSymbolicLink() || !replicasStats.isDirectory()) {
20475
+ throw refuseUnsafeReplicaPath(profile, `replication replicas path "${replicasRoot}" is not a real directory`);
20106
20476
  }
20107
- const database = await lstat2(join8(replicaPath, "replica.db")).catch((error) => {
20477
+ const replicas = await readdir4(replicasRoot, { withFileTypes: true });
20478
+ for (const replica of replicas) {
20479
+ const replicaPath = join8(replicasRoot, replica.name);
20480
+ const replicaStats = await lstat3(replicaPath);
20481
+ if (replica.isSymbolicLink() || replicaStats.isSymbolicLink() || !replicaStats.isDirectory()) {
20482
+ throw refuseUnsafeReplicaPath(profile, `replication replica "${replica.name}" is not a real directory`);
20483
+ }
20484
+ const replicaReal = await realpath(replicaPath);
20485
+ if (!replicaReal.startsWith(rootReal + sep)) {
20486
+ throw refuseUnsafeReplicaPath(profile, `replication replica "${replica.name}" resolves outside the replication root`);
20487
+ }
20488
+ const database = await lstat3(join8(replicaPath, "replica.db")).catch((error) => {
20489
+ if (error.code === "ENOENT") return null;
20490
+ throw error;
20491
+ });
20492
+ if (!database) continue;
20493
+ if (database.isSymbolicLink() || !database.isFile()) {
20494
+ throw refuseUnsafeReplicaPath(profile, `replication database "${replica.name}" is not a regular file`);
20495
+ }
20496
+ }
20497
+ } else if (!partitionStats.isFile()) {
20498
+ throw refuseUnsafeReplicaPath(profile, `replication path "${partition.name}" is not a regular file or directory`);
20499
+ }
20500
+ }
20501
+ const hasReplicaDatabase = await Promise.all(partitions.filter((entry) => entry.isDirectory()).map(async (partition) => {
20502
+ 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)));
20503
+ })).then((values) => values.some((partition) => partition.some(Boolean)));
20504
+ if (hasReplicaDatabase) await loadSqlite();
20505
+ for (const partition of partitions.filter((entry) => entry.isDirectory())) {
20506
+ const replicasRoot = join8(root, partition.name, "replicas");
20507
+ for (const entry of await readdir4(replicasRoot, { withFileTypes: true }).catch(() => [])) {
20508
+ if (!entry.isDirectory()) continue;
20509
+ const database = await lstat3(join8(replicasRoot, entry.name, "replica.db")).catch((error) => {
20108
20510
  if (error.code === "ENOENT") return null;
20109
20511
  throw error;
20110
20512
  });
20111
20513
  if (!database) continue;
20112
- if (database.isSymbolicLink() || !database.isFile()) {
20113
- throw refuseUnsafeReplicaPath(profile, `replication database "${replica.name}" is not a regular file`);
20514
+ const name = `replication/${partition.name}/${entry.name}`;
20515
+ const expectedDirectory = await assertSafeFlagReplica(profile, root, partition.name, entry.name);
20516
+ const store = await SqliteReplicaStore.open(join8(replicasRoot, entry.name), {
20517
+ create: false,
20518
+ expectedDirectory,
20519
+ guard: async (action) => withProfileLock4(profile, async () => {
20520
+ await assertSafeFlagReplica(profile, root, partition.name, entry.name);
20521
+ return action();
20522
+ }, { requireProfile: true })
20523
+ });
20524
+ try {
20525
+ if (await store.open() === null) {
20526
+ throw new CLIError("REPLICA_PURGE_FAILED", `Logout cleared the session, but flag replica "${name}" has no initialized store.`, ExitCode.ERROR);
20527
+ }
20528
+ const leasePath = join8(namedRoot, partition.name, "replicas", entry.name);
20529
+ const lease = heldLeases?.get(leasePath) ?? await store.acquireSyncLease(SYNC_LEASE_MS);
20530
+ if (lease === null) {
20531
+ throw new CLIError("REPLICA_BUSY", `Cannot log out: flag replica "${name}" is syncing. The session was cleared and no replicas were removed.`, ExitCode.ERROR);
20532
+ }
20533
+ opened.push({ name, path: leasePath, store, lease });
20534
+ } catch (error) {
20535
+ await store.close().catch(() => void 0);
20536
+ throw error;
20114
20537
  }
20115
20538
  }
20116
- } else if (!partitionStats.isFile()) {
20117
- throw refuseUnsafeReplicaPath(profile, `replication path "${partition.name}" is not a regular file or directory`);
20118
20539
  }
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 });
20540
+ deleting = true;
20541
+ for (const item of opened) {
20542
+ await item.store.destroy(item.lease);
20543
+ removed.push(item.name);
20145
20544
  }
20545
+ await pinned.assertNamed();
20546
+ for (const entry of await readdir4(root)) await rm4(join8(root, entry), { recursive: true, force: true });
20547
+ await pinned.assertNamed();
20548
+ await rmdir2(namedRoot);
20549
+ return removed;
20550
+ } catch (error) {
20551
+ if (deleting) {
20552
+ const remaining = opened.filter(({ name }) => !removed.includes(name)).map(({ name }) => name);
20553
+ throw new CLIError(
20554
+ "REPLICA_PURGE_FAILED",
20555
+ `Logout cleared the session, but replication cleanup failed. Removed: ${removed.join(", ") || "none"}. Not removed: ${remaining.join(", ") || "none"}. ${error instanceof Error ? error.message : String(error)}`,
20556
+ ExitCode.ERROR,
20557
+ { replicasRemoved: removed, replicasRemaining: remaining }
20558
+ );
20559
+ }
20560
+ if (error instanceof CLIError) throw error;
20561
+ 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);
20562
+ } finally {
20563
+ await Promise.all(opened.map(async ({ path, store, lease }) => {
20564
+ if (heldLeases?.has(path) !== true) await store.releaseLease(lease).catch(() => void 0);
20565
+ await store.close().catch(() => void 0);
20566
+ }));
20146
20567
  }
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
20568
  } 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
- }));
20569
+ if (!preflightRoot) await pinned.close();
20171
20570
  }
20172
20571
  });
20173
20572
  }
@@ -20175,36 +20574,54 @@ async function acquireLogoutLeases(profile) {
20175
20574
  return withProfileLock4(profile, async () => {
20176
20575
  let sqliteLoaded = false;
20177
20576
  const opened = [];
20577
+ const roots = /* @__PURE__ */ new Map();
20178
20578
  try {
20179
- const legacyRoot = join8(profilePath3(profile), "replicas");
20180
- if (await safeReplicaRoot(profile, legacyRoot) !== null) {
20579
+ const legacyNamed = join8(profilePath4(profile), "replicas");
20580
+ const legacyPin = await pinReplicaRoot(profile, legacyNamed, "logout");
20581
+ if (legacyPin) {
20582
+ roots.set("replicas", legacyPin);
20583
+ const legacyRoot = legacyPin.path;
20181
20584
  const entries = (await readdir4(legacyRoot, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
20182
20585
  for (const entry of entries) {
20183
- const path = await safeReplicaEntry(profile, legacyRoot, entry);
20586
+ const { path, identity: expectedDirectory, empty: empty4 } = await inspectLegacyReplica(profile, legacyRoot, entry);
20587
+ if (empty4) continue;
20184
20588
  if (!sqliteLoaded) {
20185
20589
  await loadSqlite();
20186
20590
  sqliteLoaded = true;
20187
20591
  }
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);
20592
+ const store = await SqliteReplicaStore.open(path, {
20593
+ create: false,
20594
+ expectedDirectory,
20595
+ guard: async (action) => withProfileLock4(profile, async () => {
20596
+ await safeReplicaEntry(profile, legacyRoot, entry);
20597
+ return action();
20598
+ }, { requireProfile: true })
20599
+ });
20600
+ try {
20601
+ if (await store.open() === null) {
20602
+ throw new CLIError("REPLICA_PURGE_FAILED", `Replica "${entry.name}" has no initialized store; logout cleared the session but removed no replicas.`, ExitCode.ERROR);
20603
+ }
20604
+ const lease = await store.acquireSyncLease(SYNC_LEASE_MS);
20605
+ if (lease === null) {
20606
+ throw new CLIError("REPLICA_BUSY", `Cannot log out: replica "${entry.name}" is syncing. The session was cleared and no replicas were removed.`, ExitCode.ERROR);
20607
+ }
20608
+ opened.push({ path: join8(legacyNamed, entry.name), store, lease });
20609
+ } catch (error) {
20610
+ await store.close().catch(() => void 0);
20611
+ throw error;
20197
20612
  }
20198
- opened.push({ path, store, lease });
20199
20613
  }
20200
20614
  }
20201
- const replicationRoot = join8(profilePath3(profile), "replication");
20202
- const replicationReal = await safeReplicaRoot(profile, replicationRoot);
20203
- if (replicationReal !== null) {
20615
+ const replicationNamed = join8(profilePath4(profile), "replication");
20616
+ const replicationPin = await pinReplicaRoot(profile, replicationNamed, "logout");
20617
+ if (replicationPin) {
20618
+ roots.set("replication", replicationPin);
20619
+ const replicationRoot = replicationPin.path;
20620
+ const replicationReal = await safeReplicaRoot(profile, replicationRoot);
20204
20621
  const partitions = await readdir4(replicationRoot, { withFileTypes: true });
20205
20622
  for (const partition of partitions) {
20206
20623
  const partitionPath = join8(replicationRoot, partition.name);
20207
- const partitionStats = await lstat2(partitionPath);
20624
+ const partitionStats = await lstat3(partitionPath);
20208
20625
  if (partition.isSymbolicLink() || partitionStats.isSymbolicLink()) {
20209
20626
  throw refuseUnsafeReplicaPath(profile, `replication entry "${partition.name}" is a symlink`);
20210
20627
  }
@@ -20216,22 +20633,22 @@ async function acquireLogoutLeases(profile) {
20216
20633
  if (!partitionReal.startsWith(replicationReal + sep)) {
20217
20634
  throw refuseUnsafeReplicaPath(profile, `replication entry "${partition.name}" resolves outside the replication root`);
20218
20635
  }
20219
- const replicasRoot2 = join8(partitionPath, "replicas");
20220
- const replicasStats = await lstat2(replicasRoot2).catch((error) => {
20636
+ const replicasRoot = join8(partitionPath, "replicas");
20637
+ const replicasStats = await lstat3(replicasRoot).catch((error) => {
20221
20638
  if (error.code === "ENOENT") return null;
20222
20639
  throw error;
20223
20640
  });
20224
20641
  if (!replicasStats) continue;
20225
20642
  if (replicasStats.isSymbolicLink() || !replicasStats.isDirectory()) {
20226
- throw refuseUnsafeReplicaPath(profile, `replication replicas path "${replicasRoot2}" is not a real directory`);
20643
+ throw refuseUnsafeReplicaPath(profile, `replication replicas path "${replicasRoot}" is not a real directory`);
20227
20644
  }
20228
- for (const replica of await readdir4(replicasRoot2, { withFileTypes: true })) {
20229
- const path = join8(replicasRoot2, replica.name);
20230
- const stats = await lstat2(path);
20645
+ for (const replica of await readdir4(replicasRoot, { withFileTypes: true })) {
20646
+ const path = join8(replicasRoot, replica.name);
20647
+ const stats = await lstat3(path);
20231
20648
  if (!replica.isDirectory() || replica.isSymbolicLink() || stats.isSymbolicLink() || !stats.isDirectory()) {
20232
20649
  throw refuseUnsafeReplicaPath(profile, `replication replica "${replica.name}" is not a real directory`);
20233
20650
  }
20234
- const database = await lstat2(join8(path, "replica.db")).catch((error) => {
20651
+ const database = await lstat3(join8(path, "replica.db")).catch((error) => {
20235
20652
  if (error.code === "ENOENT") return null;
20236
20653
  throw error;
20237
20654
  });
@@ -20247,26 +20664,38 @@ async function acquireLogoutLeases(profile) {
20247
20664
  await loadSqlite();
20248
20665
  sqliteLoaded = true;
20249
20666
  }
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);
20667
+ const expectedDirectory = await assertSafeFlagReplica(profile, replicationRoot, partition.name, replica.name);
20668
+ const store = await SqliteReplicaStore.open(path, {
20669
+ create: false,
20670
+ expectedDirectory,
20671
+ guard: async (action) => withProfileLock4(profile, async () => {
20672
+ await assertSafeFlagReplica(profile, replicationRoot, partition.name, replica.name);
20673
+ return action();
20674
+ }, { requireProfile: true })
20675
+ });
20676
+ try {
20677
+ if (await store.open() === null) {
20678
+ throw new CLIError("REPLICA_PURGE_FAILED", `Logout cleared the session, but flag replica "replication/${partition.name}/${replica.name}" has no initialized store.`, ExitCode.ERROR);
20679
+ }
20680
+ const lease = await store.acquireSyncLease(SYNC_LEASE_MS);
20681
+ if (lease === null) {
20682
+ 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);
20683
+ }
20684
+ opened.push({ path: join8(replicationNamed, partition.name, "replicas", replica.name), store, lease });
20685
+ } catch (error) {
20686
+ await store.close().catch(() => void 0);
20687
+ throw error;
20259
20688
  }
20260
- opened.push({ path, store, lease });
20261
20689
  }
20262
20690
  }
20263
20691
  }
20264
- return opened;
20692
+ return { stores: opened, roots };
20265
20693
  } catch (error) {
20266
20694
  await Promise.all(opened.map(async ({ store, lease }) => {
20267
20695
  await store.releaseLease(lease).catch(() => void 0);
20268
20696
  await store.close().catch(() => void 0);
20269
20697
  }));
20698
+ await Promise.all([...roots.values()].map((root) => root.close()));
20270
20699
  throw error;
20271
20700
  }
20272
20701
  });
@@ -20274,31 +20703,32 @@ async function acquireLogoutLeases(profile) {
20274
20703
  async function removeProfileReplicasAndReplication(profile) {
20275
20704
  return withProfileLock4(profile, async () => {
20276
20705
  const acquired = await acquireLogoutLeases(profile);
20277
- const heldLeases = new Map(acquired.map(({ path, lease }) => [path, lease]));
20706
+ const heldLeases = new Map(acquired.stores.map(({ path, lease }) => [path, lease]));
20278
20707
  let legacyRemoved = [];
20279
20708
  try {
20280
- legacyRemoved = await removeProfileReplicas(profile, heldLeases);
20281
- const replication = await removeProfileReplication(profile, heldLeases);
20709
+ legacyRemoved = await removeProfileReplicas(profile, heldLeases, acquired.roots.get("replicas"));
20710
+ const replication = await removeProfileReplication(profile, heldLeases, acquired.roots.get("replication"));
20282
20711
  return [...legacyRemoved, ...replication];
20283
20712
  } catch (error) {
20284
20713
  const partialRemoved = error instanceof CLIError && Array.isArray(error.metadata?.replicasRemoved) ? error.metadata.replicasRemoved.filter((name) => typeof name === "string") : [];
20285
20714
  const removed = [.../* @__PURE__ */ new Set([...legacyRemoved, ...partialRemoved])];
20286
20715
  const removedSet = new Set(removed);
20287
- const remaining = acquired.map(({ path }) => {
20288
- const name = relative(profilePath3(profile), path).split(sep).join("/");
20716
+ const remaining = acquired.stores.map(({ path }) => {
20717
+ const name = relative(profilePath4(profile), path).split(sep).join("/");
20289
20718
  return name.startsWith("replication/") ? name.replace("/replicas/", "/") : name.split("/").at(-1);
20290
20719
  }).filter((name) => !removedSet.has(name));
20291
20720
  throw new CLIError(
20292
20721
  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"}.`,
20722
+ `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
20723
  error instanceof CLIError ? error.exitCode : ExitCode.ERROR,
20295
20724
  { replicasRemoved: removed, replicasRemaining: remaining }
20296
20725
  );
20297
20726
  } finally {
20298
- await Promise.all(acquired.map(async ({ store, lease }) => {
20727
+ await Promise.all(acquired.stores.map(async ({ store, lease }) => {
20299
20728
  await store.releaseLease(lease).catch(() => void 0);
20300
20729
  await store.close().catch(() => void 0);
20301
20730
  }));
20731
+ await Promise.all([...acquired.roots.values()].map((root) => root.close()));
20302
20732
  }
20303
20733
  });
20304
20734
  }
@@ -20469,7 +20899,7 @@ function shouldOpenBrowser(options) {
20469
20899
  return env !== "1" && env !== "true";
20470
20900
  }
20471
20901
  async function callbackFlow(did, options = {}) {
20472
- return new Promise((resolve5, reject) => {
20902
+ return new Promise((resolve6, reject) => {
20473
20903
  let timeout;
20474
20904
  let settled = false;
20475
20905
  let rl;
@@ -20483,7 +20913,7 @@ async function callbackFlow(did, options = {}) {
20483
20913
  rl.close();
20484
20914
  }
20485
20915
  if (result.data) {
20486
- resolve5(result.data);
20916
+ resolve6(result.data);
20487
20917
  } else {
20488
20918
  reject(result.error);
20489
20919
  }
@@ -20562,8 +20992,8 @@ async function callbackFlow(did, options = {}) {
20562
20992
  }
20563
20993
  if (openBrowser) {
20564
20994
  try {
20565
- const open8 = (await import("open")).default;
20566
- await open8(authUrl);
20995
+ const open9 = (await import("open")).default;
20996
+ await open9(authUrl);
20567
20997
  } catch {
20568
20998
  settle({ error: new Error("Failed to open browser") });
20569
20999
  return;
@@ -20607,13 +21037,13 @@ Open this URL in a browser to authenticate:
20607
21037
  input: process.stdin,
20608
21038
  output: process.stderr
20609
21039
  });
20610
- return new Promise((resolve5, reject) => {
21040
+ return new Promise((resolve6, reject) => {
20611
21041
  let answered = false;
20612
21042
  rl.on("line", (input) => {
20613
21043
  if (answered || input.trim() === "") return;
20614
21044
  answered = true;
20615
21045
  rl.close();
20616
- void delegationFromInput(input, did, options).then(resolve5, reject);
21046
+ void delegationFromInput(input, did, options).then(resolve6, reject);
20617
21047
  });
20618
21048
  rl.on("close", () => {
20619
21049
  if (answered) return;
@@ -21514,7 +21944,7 @@ async function acquireDeviceDelegation(input) {
21514
21944
  expiresAt: new Date(deadline).toISOString().replace(/\.\d{3}Z$/, "Z")
21515
21945
  });
21516
21946
  let interval = started.interval;
21517
- const wait = input.wait ?? ((milliseconds) => new Promise((resolve5) => setTimeout(resolve5, milliseconds)));
21947
+ const wait = input.wait ?? ((milliseconds) => new Promise((resolve6) => setTimeout(resolve6, milliseconds)));
21518
21948
  while (Date.now() < deadline) {
21519
21949
  await wait(interval * 1e3);
21520
21950
  let response;
@@ -22150,7 +22580,7 @@ async function promptAuthMethod() {
22150
22580
  input: process.stdin,
22151
22581
  output: process.stderr
22152
22582
  });
22153
- return new Promise((resolve5) => {
22583
+ return new Promise((resolve6) => {
22154
22584
  process.stderr.write("\n" + theme.heading("Choose authentication method:") + "\n");
22155
22585
  process.stderr.write(` ${theme.accent("1)")} OpenKey ${theme.muted("(browser-based, for interactive use)")}
22156
22586
  `);
@@ -22161,9 +22591,9 @@ async function promptAuthMethod() {
22161
22591
  rl.close();
22162
22592
  const trimmed = answer.trim();
22163
22593
  if (trimmed === "2" || trimmed.toLowerCase() === "local") {
22164
- resolve5("local");
22594
+ resolve6("local");
22165
22595
  } else {
22166
- resolve5("openkey");
22596
+ resolve6("openkey");
22167
22597
  }
22168
22598
  });
22169
22599
  });
@@ -22865,7 +23295,7 @@ function describeSession(state2, expiresAt) {
22865
23295
  }
22866
23296
  async function emitPermissionRequestArtifact(artifact, emitOption) {
22867
23297
  if (typeof emitOption === "string" && emitOption.length > 0) {
22868
- await mkdir3(dirname3(emitOption), { recursive: true });
23298
+ await mkdir3(dirname4(emitOption), { recursive: true });
22869
23299
  await writeFile2(emitOption, JSON.stringify(artifact, null, 2) + "\n", "utf8");
22870
23300
  outputJson({
22871
23301
  emitted: true,
@@ -22901,13 +23331,13 @@ async function readStdin() {
22901
23331
  return Buffer.concat(chunks).toString("utf8");
22902
23332
  }
22903
23333
  function readUrl(source) {
22904
- return new Promise((resolve5, reject) => {
23334
+ return new Promise((resolve6, reject) => {
22905
23335
  const getter = source.startsWith("https://") ? httpsGet : httpGet;
22906
23336
  const request = getter(source, (response) => {
22907
23337
  const status = response.statusCode ?? 0;
22908
23338
  if (status >= 300 && status < 400 && response.headers.location) {
22909
23339
  response.resume();
22910
- readUrl(new URL(response.headers.location, source).toString()).then(resolve5, reject);
23340
+ readUrl(new URL(response.headers.location, source).toString()).then(resolve6, reject);
22911
23341
  return;
22912
23342
  }
22913
23343
  if (status < 200 || status >= 300) {
@@ -22923,7 +23353,7 @@ function readUrl(source) {
22923
23353
  response.on("data", (chunk) => {
22924
23354
  chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
22925
23355
  });
22926
- response.on("end", () => resolve5(Buffer.concat(chunks).toString("utf8")));
23356
+ response.on("end", () => resolve6(Buffer.concat(chunks).toString("utf8")));
22927
23357
  });
22928
23358
  request.on("error", reject);
22929
23359
  });
@@ -23163,7 +23593,7 @@ function permissionGrantReason(context, permissions) {
23163
23593
  return `${context} Requested: ${summary}${more}.`;
23164
23594
  }
23165
23595
  function execCapturedCommand(command) {
23166
- return new Promise((resolve5, reject) => {
23596
+ return new Promise((resolve6, reject) => {
23167
23597
  const child = spawn(process.execPath, [process.argv[1], ...command.argv], {
23168
23598
  cwd: command.cwd,
23169
23599
  env: process.env,
@@ -23182,7 +23612,7 @@ function execCapturedCommand(command) {
23182
23612
  if (code3 && code3 !== 0) {
23183
23613
  process.exitCode = code3;
23184
23614
  }
23185
- resolve5();
23615
+ resolve6();
23186
23616
  });
23187
23617
  });
23188
23618
  }
@@ -23211,8 +23641,8 @@ async function confirmPermissionRequest(permissions) {
23211
23641
  input: process.stdin,
23212
23642
  output: process.stderr
23213
23643
  });
23214
- const answer = await new Promise((resolve5) => {
23215
- rl.question("Approve local-key delegation? [y/N] ", resolve5);
23644
+ const answer = await new Promise((resolve6) => {
23645
+ rl.question("Approve local-key delegation? [y/N] ", resolve6);
23216
23646
  });
23217
23647
  rl.close();
23218
23648
  if (!/^y(es)?$/i.test(answer.trim())) {
@@ -24057,7 +24487,7 @@ init_errors();
24057
24487
  init_sdk();
24058
24488
  init_theme();
24059
24489
  import { readFile as readFile6, writeFile as writeFile3 } from "fs/promises";
24060
- import { resolve } from "path";
24490
+ import { resolve as resolve2 } from "path";
24061
24491
  function registerDuckdbCommand(program) {
24062
24492
  const duckdb = program.command("duckdb").description("DuckDB database operations");
24063
24493
  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 +24599,7 @@ ${rowCount} row${rowCount === 1 ? "" : "s"} returned`) + "\n");
24169
24599
  }
24170
24600
  const blob = result.data;
24171
24601
  const buffer = Buffer.from(await blob.arrayBuffer());
24172
- const outputPath = resolve(options.output);
24602
+ const outputPath = resolve2(options.output);
24173
24603
  await writeFile3(outputPath, buffer);
24174
24604
  outputJson({
24175
24605
  file: outputPath,
@@ -24185,7 +24615,7 @@ ${rowCount} row${rowCount === 1 ? "" : "s"} returned`) + "\n");
24185
24615
  const globalOpts = cmd.optsWithGlobals();
24186
24616
  const ctx = await ProfileManager.resolveContext(globalOpts);
24187
24617
  const node = await ensureAuthenticated(ctx);
24188
- const filePath = resolve(file);
24618
+ const filePath = resolve2(file);
24189
24619
  const bytes = new Uint8Array(await readFile6(filePath));
24190
24620
  const result = await withSpinner(
24191
24621
  "Importing database...",
@@ -24327,11 +24757,11 @@ init_storage();
24327
24757
  init_errors();
24328
24758
  init_constants();
24329
24759
  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";
24760
+ import { lstat as lstat4, open as open4, rename as rename2, rm as rm5 } from "fs/promises";
24761
+ import { basename as basename2, dirname as dirname5, join as join9 } from "path";
24332
24762
  async function validatePrivateOutput(path, label = "output") {
24333
24763
  try {
24334
- const destination = await lstat3(path);
24764
+ const destination = await lstat4(path);
24335
24765
  if (!destination.isFile()) {
24336
24766
  throw new CLIError("INVALID_ARGUMENT", `${label} "${path}" must be a regular file, not a symlink, directory, or device.`, ExitCode.USAGE_ERROR);
24337
24767
  }
@@ -24339,7 +24769,7 @@ async function validatePrivateOutput(path, label = "output") {
24339
24769
  if (error?.code !== "ENOENT") throw error;
24340
24770
  }
24341
24771
  try {
24342
- const parent = await lstat3(dirname4(path));
24772
+ const parent = await lstat4(dirname5(path));
24343
24773
  if (!parent.isDirectory()) {
24344
24774
  throw new CLIError("INVALID_ARGUMENT", `${label} "${path}" requires an existing directory parent.`, ExitCode.USAGE_ERROR);
24345
24775
  }
@@ -24350,11 +24780,11 @@ async function validatePrivateOutput(path, label = "output") {
24350
24780
  }
24351
24781
  async function writePrivateOutput(path, value, label = "output") {
24352
24782
  await validatePrivateOutput(path, label);
24353
- const parentPath = dirname4(path);
24783
+ const parentPath = dirname5(path);
24354
24784
  const temp = join9(parentPath, `.${basename2(path)}.${randomUUID2()}.tmp`);
24355
24785
  let created = false;
24356
24786
  try {
24357
- const handle = await open3(temp, "wx", PRIVATE_FILE_MODE);
24787
+ const handle = await open4(temp, "wx", PRIVATE_FILE_MODE);
24358
24788
  created = true;
24359
24789
  try {
24360
24790
  await handle.writeFile(value);
@@ -24369,7 +24799,7 @@ async function writePrivateOutput(path, value, label = "output") {
24369
24799
  throw new CLIError("ERROR", `Could not write ${label.toLowerCase()} "${path}"${code3}.`, ExitCode.ERROR);
24370
24800
  }
24371
24801
  try {
24372
- const parent = await open3(parentPath, "r");
24802
+ const parent = await open4(parentPath, "r");
24373
24803
  try {
24374
24804
  await parent.sync();
24375
24805
  } finally {
@@ -25009,8 +25439,8 @@ Examples:
25009
25439
  try {
25010
25440
  if (isInteractive()) {
25011
25441
  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);
25442
+ const answer = await new Promise((resolve6) => {
25443
+ rl.question(`Delete profile "${name}"? This cannot be undone. [y/N] `, resolve6);
25014
25444
  });
25015
25445
  rl.close();
25016
25446
  if (answer.toLowerCase() !== "y") {
@@ -25045,22 +25475,19 @@ function parseOperatorType(raw) {
25045
25475
  }
25046
25476
 
25047
25477
  // src/commands/replica.ts
25048
- init_sdk();
25049
- init_replication_registry();
25050
25478
  import { CLI_NAME as CLI_NAME22 } from "@tinycloud/operations/cli-name";
25051
25479
  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";
25480
+ import { constants as fsConstants3, lstatSync as lstatSync2 } from "fs";
25481
+ import { mkdir as mkdir4, open as open5, readdir as readdir5, rename as rename3, unlink as unlink2, lstat as lstat5 } from "fs/promises";
25482
+ import { basename as basename3, dirname as dirname6, join as join10, resolve as resolve3 } from "path";
25055
25483
 
25056
25484
  // 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
25485
  function percentile(values, fraction) {
25059
25486
  if (values.length === 0) return null;
25060
25487
  values.sort((left, right) => left - right);
25061
25488
  return values[Math.ceil(fraction * values.length) - 1] ?? values[values.length - 1];
25062
25489
  }
25063
- function createReplicationReport(since, events, replicas, partitions, warning) {
25490
+ function createReplicationReport(since, events, partitions) {
25064
25491
  const reads = events.filter((event) => event.type === "replication.read");
25065
25492
  const replicaReads = reads.filter((event) => event.source === "replica");
25066
25493
  const sourceLatency = /* @__PURE__ */ new Map();
@@ -25103,9 +25530,9 @@ function createReplicationReport(since, events, replicas, partitions, warning) {
25103
25530
  syncs,
25104
25531
  writes,
25105
25532
  divergences,
25106
- replicas,
25533
+ replicas: [],
25107
25534
  partitions,
25108
- ...warning === void 0 ? {} : { warning }
25535
+ replicasUnavailable: true
25109
25536
  };
25110
25537
  }
25111
25538
  function renderReplicationReport(report) {
@@ -25118,34 +25545,30 @@ function renderReplicationReport(report) {
25118
25545
  `Writes: ${Object.entries(report.writes).map(([key, count]) => `${key}=${count}`).join(", ") || "none"}`,
25119
25546
  `Read source/reason: ${Object.entries(report.readsBySourceReason).map(([key, count]) => `${key}=${count}`).join(", ") || "none"}`,
25120
25547
  `Divergences: ${report.totals.divergences}; showing ${report.divergences.length} of ${report.totals.divergences}`,
25121
- `Replicas: ${report.replicas.length}`
25548
+ "Replicas: unavailable (read-only report)"
25122
25549
  ];
25123
25550
  for (const divergence of report.divergences) {
25124
25551
  if (divergence.type === "replication.divergence") {
25125
25552
  lines.push(` ${divergence.space} ${divergence.key}: ${divergence.kind} stalenessMs=${divergence.stalenessMs}`);
25126
25553
  }
25127
25554
  }
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
25555
  for (const partition of report.partitions) {
25134
25556
  lines.push(`Partition ${partition.idHash}: host=${partition.host ?? "unknown"} space=${partition.space ?? "unknown"} pinned=${partition.pinned}`);
25135
25557
  }
25136
25558
  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
25559
  return lines.join("\n");
25139
25560
  }
25140
25561
 
25141
25562
  // src/commands/replica.ts
25142
25563
  init_dist3();
25143
25564
  import {
25565
+ ProfileSymlinkError as ProfileSymlinkError3,
25144
25566
  ProfileDeletedError as ProfileDeletedError3,
25145
- profilePath as profilePath4,
25567
+ profilePath as profilePath5,
25146
25568
  readAdditionalDelegations as readAdditionalDelegations2,
25147
25569
  readSession as readSession2,
25148
- withProfileLock as withProfileLock5
25570
+ withProfileLock as withProfileLock5,
25571
+ withPinnedProfileDirectory as withPinnedProfileDirectory3
25149
25572
  } from "@tinycloud/operations/state";
25150
25573
  import { ownerSessionLacksProof as ownerSessionLacksProof4 } from "@tinycloud/operations/profile";
25151
25574
  init_profiles();
@@ -25201,9 +25624,9 @@ function toCliError(error) {
25201
25624
  const hint = error.code === ReplicaErrorCode.GRANT_MISSING && typeof error.detail?.hint === "string" ? error.detail.hint : void 0;
25202
25625
  return new CLIError(error.code, error.message, EXIT_BY_CODE[error.code], hint === void 0 ? void 0 : { hint });
25203
25626
  }
25204
- async function run(action) {
25627
+ async function run(action, needsSqlite = true) {
25205
25628
  try {
25206
- await loadSqlite();
25629
+ if (needsSqlite) await loadSqlite();
25207
25630
  await action();
25208
25631
  } catch (error) {
25209
25632
  return handleError(toCliError(error));
@@ -25216,20 +25639,25 @@ async function profileName(cmd) {
25216
25639
  const config = await ProfileManager.getConfig().catch(() => void 0);
25217
25640
  return config?.defaultProfile ?? DEFAULT_PROFILE;
25218
25641
  }
25219
- function replicasRoot(profile) {
25220
- return join10(profilePath4(profile), "replicas");
25221
- }
25222
25642
  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();
25643
+ return await withReplicaRoot(profile, async (root) => {
25644
+ const names = [];
25645
+ for (const entry of await readdir5(root, { withFileTypes: true })) {
25646
+ if (entry.isSymbolicLink()) throw localPathChanged(profile);
25647
+ if (!NAME.test(entry.name) || !entry.isDirectory()) continue;
25648
+ const present = await withPinnedLocalDirectory(profile, root, entry.name, async (directory) => {
25649
+ const database = await lstat5(join10(directory, "replica.db"), { bigint: true }).catch((error) => {
25650
+ if (error.code === "ENOENT") return null;
25651
+ throw error;
25652
+ });
25653
+ if (database && (database.isSymbolicLink() || !database.isFile())) throw localPathChanged(profile);
25654
+ return database !== null;
25655
+ });
25656
+ if (present === null) throw localPathChanged(profile);
25657
+ if (present) names.push(entry.name);
25658
+ }
25659
+ return names.sort();
25660
+ }) ?? [];
25233
25661
  }
25234
25662
  function assertName(name) {
25235
25663
  if (!NAME.test(name)) {
@@ -25237,6 +25665,67 @@ function assertName(name) {
25237
25665
  }
25238
25666
  return name;
25239
25667
  }
25668
+ async function withReplicaRoot(profile, action, create4 = false) {
25669
+ const profileDirectory = profilePath5(profile);
25670
+ const readRoot = async (directory, recheckProfile) => {
25671
+ if (create4) {
25672
+ if (recheckProfile) await recheckProfile();
25673
+ await mkdir4(join10(directory, "replicas"), { mode: 448 }).catch((error) => {
25674
+ if (error.code !== "EEXIST") throw error;
25675
+ });
25676
+ }
25677
+ return withPinnedLocalDirectory(
25678
+ profile,
25679
+ directory,
25680
+ "replicas",
25681
+ (root, _named, _identity, recheckRoot) => action(root, recheckRoot, recheckProfile)
25682
+ );
25683
+ };
25684
+ if (process.platform === "linux") return readRoot(profileDirectory);
25685
+ const checked = await withPinnedLocalDirectory(
25686
+ profile,
25687
+ dirname6(profileDirectory),
25688
+ basename3(profileDirectory),
25689
+ (directory, _named, _identity, recheckProfile) => readRoot(directory, recheckProfile)
25690
+ );
25691
+ if (checked === null) throw localPathChanged(profile);
25692
+ return checked;
25693
+ }
25694
+ async function withReplicaDirectory(profile, name, action, create4 = false) {
25695
+ assertName(name);
25696
+ const result = await withReplicaRoot(profile, async (root, recheckRoot, recheckProfile) => {
25697
+ if (create4) {
25698
+ await recheckRoot();
25699
+ await mkdir4(join10(root, name), { mode: 448 }).catch((error) => {
25700
+ if (error.code !== "EEXIST") throw error;
25701
+ });
25702
+ }
25703
+ const opened = await withPinnedLocalDirectory(
25704
+ profile,
25705
+ root,
25706
+ name,
25707
+ async (_directory, path, expectedDirectory, recheckChild) => {
25708
+ await recheckRoot();
25709
+ await recheckChild();
25710
+ const database = await lstat5(join10(_directory, "replica.db"), { bigint: true }).catch((error) => {
25711
+ if (error.code === "ENOENT") return null;
25712
+ throw error;
25713
+ });
25714
+ if (database && (database.isSymbolicLink() || !database.isFile())) throw localPathChanged(profile);
25715
+ if (recheckProfile) {
25716
+ await recheckProfile();
25717
+ await recheckRoot();
25718
+ await recheckChild();
25719
+ }
25720
+ return action(path, expectedDirectory);
25721
+ }
25722
+ );
25723
+ if (opened === null) throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, `Replica "${name}" was not found.`);
25724
+ return opened;
25725
+ }, create4);
25726
+ if (result === null) throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, `Profile "${profile}" has no replicas.`);
25727
+ return result;
25728
+ }
25240
25729
  async function existingReplicaName(profile, option) {
25241
25730
  if (option !== void 0) return assertName(option);
25242
25731
  const names = await replicaNames(profile);
@@ -25258,17 +25747,37 @@ function profileGuard(profile) {
25258
25747
  }
25259
25748
  };
25260
25749
  }
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;
25750
+ async function withOpenReplica(profile, name, action, guard = profileGuard(profile)) {
25751
+ return withReplicaDirectory(profile, name, async (path, expectedDirectory) => {
25752
+ const store = await SqliteReplicaStore.open(path, { create: false, guard, expectedDirectory });
25753
+ try {
25754
+ return await action(store);
25755
+ } finally {
25756
+ await store.close();
25757
+ }
25270
25758
  });
25271
25759
  }
25760
+ function assertLegacyLogoutClear(profile) {
25761
+ try {
25762
+ lstatSync2(join10(profilePath5(profile), "replica-logout-pending.json"));
25763
+ } catch (error) {
25764
+ if (error?.code === "ENOENT") return;
25765
+ }
25766
+ throw new CLIError(
25767
+ "REPLICA_GATED",
25768
+ "A logout is pending or its state cannot be checked for this profile.",
25769
+ ExitCode.PERMISSION_DENIED
25770
+ );
25771
+ }
25772
+ async function withLegacyReplicaRead(profile, action) {
25773
+ await withProfileLock5(profile, async () => {
25774
+ const checkDelivery = async () => {
25775
+ assertLegacyLogoutClear(profile);
25776
+ };
25777
+ await checkDelivery();
25778
+ await action(checkDelivery);
25779
+ }, { requireProfile: true });
25780
+ }
25272
25781
  async function storedGrants(profile) {
25273
25782
  const authorizations = [];
25274
25783
  const session = await readSession2(profile).catch(() => null);
@@ -25398,9 +25907,9 @@ function describeStatus(status) {
25398
25907
  ].join("\n");
25399
25908
  }
25400
25909
  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);
25910
+ const target = resolve3(path);
25911
+ const temp = join10(dirname6(target), `.${basename3(target)}.${randomBytes6(6).toString("hex")}.tmp`);
25912
+ const handle = await open5(temp, fsConstants3.O_CREAT | fsConstants3.O_EXCL | fsConstants3.O_WRONLY, 384);
25404
25913
  try {
25405
25914
  await handle.writeFile(bytes);
25406
25915
  await handle.sync();
@@ -25425,14 +25934,86 @@ function replicaSecretsWarning(space, prefix, secretsAllowed) {
25425
25934
  if (!secretsAllowed || !requiresSecretsOptIn(space, prefix)) return void 0;
25426
25935
  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
25936
  }
25428
- async function readReplicationEvents(profile, sinceMs) {
25429
- const root = join10(profilePath4(profile), "replication");
25937
+ function localFileId(value) {
25938
+ if (typeof value === "bigint") return value >= 0n ? value : null;
25939
+ return Number.isSafeInteger(value) && value >= 0 ? BigInt(value) : null;
25940
+ }
25941
+ function sameLocalEntry(left, right) {
25942
+ const dev = localFileId(left.dev);
25943
+ const ino = localFileId(left.ino);
25944
+ return dev !== null && ino !== null && dev === localFileId(right.dev) && ino === localFileId(right.ino);
25945
+ }
25946
+ function localPathChanged(profile) {
25947
+ return new ProfileSymlinkError3(profile, "its local replica files contain a symlink or changed while reading");
25948
+ }
25949
+ async function assertNamedLocalEntry(profile, path, expected, kind) {
25950
+ const current = await lstat5(path, { bigint: true }).catch(() => null);
25951
+ if (!current || current.isSymbolicLink() || (kind === "directory" ? !current.isDirectory() : !current.isFile()) || !sameLocalEntry(expected, current)) throw localPathChanged(profile);
25952
+ }
25953
+ async function withPinnedLocalDirectory(profile, parent, name, action) {
25954
+ const path = join10(parent, name);
25955
+ const before = await lstat5(path, { bigint: true }).catch((error) => {
25956
+ if (error.code === "ENOENT") return null;
25957
+ throw error;
25958
+ });
25959
+ if (!before) return null;
25960
+ if (before.isSymbolicLink() || !before.isDirectory()) throw localPathChanged(profile);
25961
+ const handle = await open5(path, fsConstants3.O_RDONLY | fsConstants3.O_DIRECTORY | fsConstants3.O_NOFOLLOW).catch((error) => {
25962
+ if (error.code === "ELOOP" || error.code === "ENOTDIR" || error.code === "ENOENT") throw localPathChanged(profile);
25963
+ throw error;
25964
+ });
25965
+ try {
25966
+ const opened = await handle.stat({ bigint: true });
25967
+ if (!opened.isDirectory() || !sameLocalEntry(before, opened)) throw localPathChanged(profile);
25968
+ const dev = localFileId(opened.dev);
25969
+ const ino = localFileId(opened.ino);
25970
+ if (dev === null || ino === null || dev > BigInt(Number.MAX_SAFE_INTEGER) || ino > BigInt(Number.MAX_SAFE_INTEGER)) {
25971
+ throw localPathChanged(profile);
25972
+ }
25973
+ const pinned = process.platform === "linux" ? `/proc/self/fd/${handle.fd}` : path;
25974
+ const recheck = () => assertNamedLocalEntry(profile, path, opened, "directory");
25975
+ const result = await action(pinned, path, { dev: Number(dev), ino: Number(ino) }, recheck);
25976
+ await recheck();
25977
+ return result;
25978
+ } finally {
25979
+ await handle.close();
25980
+ }
25981
+ }
25982
+ async function readReportFile(profile, parent, name) {
25983
+ const path = join10(parent, name);
25984
+ const before = await lstat5(path, { bigint: true }).catch((error) => {
25985
+ if (error.code === "ENOENT") return null;
25986
+ throw error;
25987
+ });
25988
+ if (!before) return null;
25989
+ if (before.isSymbolicLink() || !before.isFile()) throw localPathChanged(profile);
25990
+ const handle = await open5(path, fsConstants3.O_RDONLY | fsConstants3.O_NOFOLLOW).catch((error) => {
25991
+ if (error.code === "ELOOP" || error.code === "ENOTDIR" || error.code === "ENOENT") throw localPathChanged(profile);
25992
+ throw error;
25993
+ });
25994
+ try {
25995
+ const opened = await handle.stat({ bigint: true });
25996
+ if (!opened.isFile() || !sameLocalEntry(before, opened)) throw localPathChanged(profile);
25997
+ const data = await handle.readFile("utf8");
25998
+ await assertNamedLocalEntry(profile, path, opened, "file");
25999
+ return data;
26000
+ } finally {
26001
+ await handle.close();
26002
+ }
26003
+ }
26004
+ async function readReportJson(profile, parent, name) {
26005
+ const data = await readReportFile(profile, parent, name);
26006
+ if (data === null) return null;
26007
+ try {
26008
+ return JSON.parse(data);
26009
+ } catch {
26010
+ return null;
26011
+ }
26012
+ }
26013
+ async function readReplicationEvents(profile, root, sinceMs) {
25430
26014
  const events = [];
25431
26015
  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
- });
26016
+ const data = await readReportFile(profile, root, name) ?? "";
25436
26017
  for (const line of data.split("\n")) {
25437
26018
  if (!line) continue;
25438
26019
  try {
@@ -25444,27 +26025,26 @@ async function readReplicationEvents(profile, sinceMs) {
25444
26025
  }
25445
26026
  return events;
25446
26027
  }
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
- });
26028
+ async function replicationPartitions(profile, root) {
26029
+ const entries = await readdir5(root, { withFileTypes: true });
25453
26030
  const partitions = [];
25454
26031
  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
26032
+ if (!/^[a-z2-7]{26}$/.test(entry.name)) continue;
26033
+ if (entry.isSymbolicLink()) throw localPathChanged(profile);
26034
+ if (!entry.isDirectory()) continue;
26035
+ const partition = await withPinnedLocalDirectory(profile, root, entry.name, async (pinned) => {
26036
+ const identity = await readReportJson(profile, pinned, "identity.json");
26037
+ const pending = await readReportJson(profile, pinned, "pending.json");
26038
+ const pinnedCount = pending?.records?.filter((record2) => record2.state === "in_flight" || record2.state === "ambiguous").length ?? 0;
26039
+ return {
26040
+ idHash: entry.name,
26041
+ host: typeof identity?.host === "string" ? identity.host : null,
26042
+ space: typeof identity?.space === "string" ? identity.space : null,
26043
+ pinned: pinnedCount
26044
+ };
25467
26045
  });
26046
+ if (!partition) throw localPathChanged(profile);
26047
+ partitions.push(partition);
25468
26048
  }
25469
26049
  return partitions.sort((left, right) => left.idHash.localeCompare(right.idHash));
25470
26050
  }
@@ -25483,52 +26063,41 @@ SECRETS_OPT_IN_REQUIRED; 3 grant missing; 4 key absent, deleted, content missing
25483
26063
  or coverage incomplete; 5 grant expired, revoked or not yet valid; 6 network;
25484
26064
  7 node, protocol or integrity error; 10 storage full.`
25485
26065
  );
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(
26066
+ 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
26067
  (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
26068
  const profile = await profileName(cmd);
26069
+ if (options.purge) {
26070
+ 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);
26071
+ }
26072
+ if (options.clearPending) {
26073
+ 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);
26074
+ }
25492
26075
  let durationMs;
25493
26076
  try {
25494
26077
  durationMs = parseDuration(options.since);
25495
26078
  } catch (error) {
25496
26079
  throw new CLIError("USAGE_ERROR", error instanceof Error ? error.message : String(error), ExitCode.USAGE_ERROR);
25497
26080
  }
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)}
26081
+ const report = await withPinnedProfileDirectory3(profile, async (directory) => {
26082
+ if (directory === null) throw new CLIError("PROFILE_NOT_FOUND", `Profile "${profile}" does not exist.`, ExitCode.ERROR);
26083
+ const readPersistedReport = async (root) => {
26084
+ await replicaNames(profile);
26085
+ const sinceMs = Date.now() - durationMs;
26086
+ const persisted = await withPinnedLocalDirectory(profile, root, "replication", async (directory2) => ({
26087
+ events: await readReplicationEvents(profile, directory2, sinceMs),
26088
+ partitions: await replicationPartitions(profile, directory2)
26089
+ }));
26090
+ return createReplicationReport(options.since, persisted?.events ?? [], persisted?.partitions ?? []);
26091
+ };
26092
+ if (process.platform === "linux") return readPersistedReport(directory);
26093
+ const checked = await withPinnedLocalDirectory(profile, dirname6(directory), basename3(directory), readPersistedReport);
26094
+ if (checked === null) throw localPathChanged(profile);
26095
+ return checked;
26096
+ });
26097
+ if (shouldOutputJson()) outputJson(report);
26098
+ else process.stdout.write(`${renderReplicationReport(report)}
25529
26099
  `);
25530
- }
25531
- })
26100
+ }, false)
25532
26101
  );
25533
26102
  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
26103
  (options, cmd) => run(async () => {
@@ -25540,87 +26109,98 @@ or coverage incomplete; 5 grant expired, revoked or not yet valid; 6 network;
25540
26109
  throw new CLIError("INVALID_REPLICATION_PREFIX", error instanceof Error ? error.message : String(error), ExitCode.USAGE_ERROR);
25541
26110
  }
25542
26111
  }
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);
26112
+ await withPinnedProfileDirectory3(profile, async (directory) => {
26113
+ if (directory === null) throw new CLIError("PROFILE_NOT_FOUND", `Profile "${profile}" does not exist.`, ExitCode.ERROR);
26114
+ const globals = cmd.optsWithGlobals();
26115
+ const config = await ProfileManager.getProfile(profile);
26116
+ const deviceDid = config.sessionDid ?? config.did;
26117
+ const jwk = await ProfileManager.getKey(profile);
26118
+ if (jwk === null) {
26119
+ throw new CLIError("AUTH_REQUIRED", `Profile "${profile}" has no device key.`, ExitCode.AUTH_REQUIRED);
26120
+ }
26121
+ if (options.limit !== void 0 && (!Number.isInteger(options.limit) || options.limit < 1 || options.limit > 1e3)) {
26122
+ throw new CLIError("USAGE_ERROR", "--limit must be an integer from 1 through 1000.", ExitCode.USAGE_ERROR);
26123
+ }
26124
+ const existing = await replicaNames(profile);
26125
+ 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);
26126
+ const grants = await storedGrants(profile);
26127
+ const initialConfig = existing.includes(name) ? null : (() => {
26128
+ const prefix = options.prefix;
26129
+ if (prefix === void 0) {
26130
+ throw new CLIError("USAGE_ERROR", "The first sync of a replica needs --prefix (and a device grant covering it).", ExitCode.USAGE_ERROR);
26131
+ }
26132
+ const space = resolveSpace(options.space, prefix, grants);
26133
+ if (requiresSecretsOptIn(space, prefix) && options.allowSecrets !== true) {
26134
+ throw new ReplicaError(
26135
+ ReplicaErrorCode.SECRETS_OPT_IN_REQUIRED,
26136
+ `Replicating ${space}/kv/${prefix} copies encrypted secret material to this disk; pass --allow-secrets to opt in.`
26137
+ );
26138
+ }
26139
+ const host = (globals.host ?? process.env.TC_HOST ?? config.host)?.replace(/\/+$/, "");
26140
+ if (!host) throw new CLIError("USAGE_ERROR", "Pass --host: the profile has no host to pin.", ExitCode.USAGE_ERROR);
26141
+ chooseGrant(grants, deviceDid, space, prefix);
26142
+ return {
26143
+ name,
26144
+ replicaId: randomBytes6(16).toString("hex"),
26145
+ host,
26146
+ space,
26147
+ prefix,
26148
+ deviceDid,
26149
+ allowSecrets: options.allowSecrets === true,
26150
+ localReadPolicy: "whileGrantValid",
26151
+ retentionGrantCid: null
26152
+ };
26153
+ })();
26154
+ const { replica: replica2, report, status, warning } = await withReplicaDirectory(profile, name, async (path, expectedDirectory) => {
26155
+ const store = await SqliteReplicaStore.open(path, {
26156
+ create: initialConfig !== null,
26157
+ guard: profileGuard(profile),
26158
+ expectedDirectory
26159
+ });
26160
+ try {
26161
+ if (initialConfig !== null && await store.open() === null) await store.init(initialConfig);
26162
+ const state2 = await store.open();
26163
+ if (initialConfig === null) {
26164
+ const differs = syncOptionsDiffer(state2.config, {
26165
+ ...options.space === void 0 ? {} : { space: options.space },
26166
+ ...options.prefix === void 0 ? {} : { prefix: options.prefix },
26167
+ ...globals.host === void 0 ? {} : { host: globals.host }
26168
+ });
26169
+ if (differs !== void 0) {
26170
+ throw new ReplicaError(
26171
+ ReplicaErrorCode.CONFIG_MISMATCH,
26172
+ `Replica ${name} already follows ${differs}. Use another --replica name, or ${CLI_NAME22} replica reset --replica ${name} --purge first.`
26173
+ );
26174
+ }
26175
+ }
26176
+ const { space, prefix, host } = state2.config;
26177
+ if (requiresSecretsOptIn(space, prefix) && !state2.config.allowSecrets && options.allowSecrets !== true) {
26178
+ throw new ReplicaError(ReplicaErrorCode.SECRETS_OPT_IN_REQUIRED, `Replicating ${space}/kv/${prefix} needs --allow-secrets.`);
26179
+ }
26180
+ let grant;
26181
+ try {
26182
+ grant = chooseGrant(grants, state2.config.deviceDid, space, prefix);
26183
+ } catch (error) {
26184
+ if (!isReplicaError(error, ReplicaErrorCode.GRANT_MISSING) || (state2.grant ?? state2.pendingGrant) === null) throw error;
26185
+ }
26186
+ if (grant !== void 0) await store.installGrant(grant);
26187
+ if (options.retentionGrant !== void 0 && options.retentionGrant !== state2.config.retentionGrantCid) {
26188
+ await store.setRetentionGrant(options.retentionGrant === "" ? null : options.retentionGrant);
26189
+ }
26190
+ const current = await store.open();
26191
+ const transportFor = await grantTransports({ host, space, deviceDid: current.config.deviceDid, jwk });
26192
+ const warning2 = replicaSecretsWarning(space, prefix, options.allowSecrets === true || state2.config.allowSecrets);
26193
+ const report2 = await new Replica({ store, transportFor }).sync(options.limit === void 0 ? {} : { limit: options.limit });
26194
+ const status2 = await store.status();
26195
+ return { replica: current.config.name, report: report2, status: status2, warning: warning2 };
26196
+ } finally {
26197
+ await store.close();
26198
+ }
26199
+ }, initialConfig !== null);
25618
26200
  if (warning !== void 0) process.stderr.write(`Warning: ${warning}
25619
26201
  `);
25620
- const report = await new Replica({ store, transportFor }).sync(options.limit === void 0 ? {} : { limit: options.limit });
25621
- const status = await store.status();
25622
26202
  if (shouldOutputJson()) {
25623
- outputJson({ replica: current.config.name, sync: report, status, ...warning === void 0 ? {} : { warning } });
26203
+ outputJson({ replica: replica2, sync: report, status, ...warning === void 0 ? {} : { warning } });
25624
26204
  } else {
25625
26205
  process.stdout.write(
25626
26206
  `${theme.success("\u2713")} Synced ${report.changes} change(s) in ${report.pages} page(s), fetched ${report.fetched} value(s).
@@ -25628,40 +26208,39 @@ ${describeStatus(status)}
25628
26208
  `
25629
26209
  );
25630
26210
  }
25631
- } finally {
25632
- await store.close();
25633
- }
26211
+ });
25634
26212
  })
25635
26213
  );
25636
26214
  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
26215
  (key, options, cmd) => run(async () => {
25638
26216
  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) {
26217
+ await withLegacyReplicaRead(profile, async (checkDelivery) => {
26218
+ const result = await withOpenReplica(
26219
+ profile,
26220
+ await existingReplicaName(profile, options.replica),
26221
+ (store) => new Replica({ store }).get(key, { verify: options.verify !== false })
26222
+ );
26223
+ await checkDelivery();
26224
+ if (result.status !== "present") {
26225
+ const failure = READ_FAILURE[result.status];
26226
+ throw new CLIError(failure.code, `Key ${JSON.stringify(key)} ${failure.what}.`, failure.exit);
26227
+ }
26228
+ if (options.output) {
26229
+ await writeFileAtomic(options.output, result.value);
26230
+ outputJson({ key, written: options.output, etag: result.etag, meta: result.meta });
26231
+ return;
26232
+ }
26233
+ if (options.raw) {
26234
+ process.stdout.write(result.value);
26235
+ return;
26236
+ }
26237
+ if (shouldOutputJson()) {
26238
+ outputJson({ key, status: result.status, ...jsonValue(result.value), etag: result.etag, metadata: result.metadata, meta: result.meta });
26239
+ return;
26240
+ }
25656
26241
  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");
26242
+ process.stdout.write("\n");
26243
+ });
25665
26244
  })
25666
26245
  );
25667
26246
  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 +26249,17 @@ ${describeStatus(status)}
25670
26249
  throw new CLIError("USAGE_ERROR", "--limit must be a positive integer.", ExitCode.USAGE_ERROR);
25671
26250
  }
25672
26251
  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
- });
26252
+ await withLegacyReplicaRead(profile, async (checkDelivery) => {
26253
+ const { entries, meta } = await withOpenReplica(
26254
+ profile,
26255
+ await existingReplicaName(profile, options.replica),
26256
+ (store) => new Replica({ store }).list({
26257
+ ...prefix === void 0 ? {} : { prefix },
26258
+ ...options.after === void 0 ? {} : { after: options.after },
26259
+ ...options.limit === void 0 ? {} : { limit: options.limit }
26260
+ })
26261
+ );
26262
+ await checkDelivery();
25680
26263
  if (shouldOutputJson()) {
25681
26264
  outputJson({ keys: entries, count: entries.length, prefix: prefix ?? null, meta });
25682
26265
  } else if (entries.length === 0) {
@@ -25689,50 +26272,57 @@ ${describeStatus(status)}
25689
26272
  ) + "\n"
25690
26273
  );
25691
26274
  }
25692
- } finally {
25693
- await store.close();
25694
- }
26275
+ });
25695
26276
  })
25696
26277
  );
25697
26278
  replica.command("status").description("Show one replica's sync state, authority and counts, or all of them").option("--replica <name>", "Replica name").action(
25698
26279
  (options, cmd) => run(async () => {
25699
26280
  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();
26281
+ await withLegacyReplicaRead(profile, async (checkDelivery) => {
26282
+ const names = options.replica === void 0 ? await replicaNames(profile) : [assertName(options.replica)];
26283
+ const statuses = [];
26284
+ for (const name of names) {
26285
+ statuses.push(await withOpenReplica(profile, name, (store) => new Replica({ store }).status()));
25708
26286
  }
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
- }
26287
+ await checkDelivery();
26288
+ if (shouldOutputJson()) {
26289
+ outputJson(options.replica === void 0 ? { replicas: statuses } : statuses[0]);
26290
+ } else if (statuses.length === 0) {
26291
+ process.stdout.write(theme.muted(`Profile "${profile}" has no replicas.`) + "\n");
26292
+ } else {
26293
+ process.stdout.write(statuses.map(describeStatus).join("\n\n") + "\n");
26294
+ }
26295
+ });
25717
26296
  })
25718
26297
  );
25719
26298
  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
26299
  (options, cmd) => run(async () => {
25721
26300
  const profile = await profileName(cmd);
25722
- const name = await existingReplicaName(profile, options.replica);
25723
- const store = await openReplica(profile, name);
25724
- try {
26301
+ await withProfileLock5(profile, async () => {
26302
+ const name = await existingReplicaName(profile, options.replica);
25725
26303
  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);
26304
+ await withPinnedReplicaForReset(profile, name, async (path, recheck, expectedDirectory) => {
26305
+ const profileLock = profileGuard(profile);
26306
+ const safeGuard = (section) => profileLock(async () => {
26307
+ await recheck();
26308
+ return section();
26309
+ });
26310
+ const store = await SqliteReplicaStore.open(path, { create: false, guard: safeGuard, expectedDirectory });
26311
+ try {
26312
+ await recheck();
26313
+ const lease = await store.acquireSyncLease(6e4);
26314
+ if (lease === null) throw new ReplicaError(ReplicaErrorCode.BUSY, "Another process is syncing this replica.");
26315
+ await recheck();
26316
+ await store.destroy(lease);
26317
+ } finally {
26318
+ await store.close();
26319
+ }
26320
+ });
25729
26321
  } else {
25730
- await new Replica({ store }).reset("manual");
26322
+ await withOpenReplica(profile, name, (store) => new Replica({ store }).reset("manual"));
25731
26323
  }
25732
- } finally {
25733
- await store.close();
25734
- }
25735
- outputJson({ replica: name, reset: true, purged: options.purge === true });
26324
+ outputJson({ replica: name, reset: true, purged: options.purge === true });
26325
+ }, { requireProfile: true });
25736
26326
  })
25737
26327
  );
25738
26328
  }
@@ -25745,7 +26335,7 @@ init_errors();
25745
26335
  init_warnings();
25746
26336
  init_constants();
25747
26337
  import { CLI_NAME as CLI_NAME24 } from "@tinycloud/operations/cli-name";
25748
- import { readFile as readFile10 } from "fs/promises";
26338
+ import { readFile as readFile9 } from "fs/promises";
25749
26339
  import { join as join11 } from "path";
25750
26340
  import { homedir, hostname, userInfo } from "os";
25751
26341
  import { appSecretScope } from "@tinycloud/sdk-core";
@@ -25774,24 +26364,24 @@ init_space();
25774
26364
  // src/lib/browser.ts
25775
26365
  init_formatter();
25776
26366
  init_theme();
25777
- import open5 from "open";
26367
+ import open6 from "open";
25778
26368
  var LAUNCH_CHECK_MS = 1e3;
25779
26369
  async function tryOpenBrowser(url) {
25780
26370
  let launcher;
25781
26371
  try {
25782
- launcher = await open5(url);
26372
+ launcher = await open6(url);
25783
26373
  } catch {
25784
26374
  return false;
25785
26375
  }
25786
- return new Promise((resolve5) => {
25787
- const timer = setTimeout(() => resolve5(true), LAUNCH_CHECK_MS);
26376
+ return new Promise((resolve6) => {
26377
+ const timer = setTimeout(() => resolve6(true), LAUNCH_CHECK_MS);
25788
26378
  launcher.once("error", () => {
25789
26379
  clearTimeout(timer);
25790
- resolve5(false);
26380
+ resolve6(false);
25791
26381
  });
25792
26382
  launcher.once("exit", (code3) => {
25793
26383
  clearTimeout(timer);
25794
- resolve5(code3 === 0);
26384
+ resolve6(code3 === 0);
25795
26385
  });
25796
26386
  });
25797
26387
  }
@@ -25835,9 +26425,9 @@ function runProbeChild(request, options = {}) {
25835
26425
  const killGraceMs = options.killGraceMs ?? KILL_GRACE_MS;
25836
26426
  const spawn2 = options.spawn ?? nodeSpawn;
25837
26427
  const signal = options.signal;
25838
- return new Promise((resolve5) => {
26428
+ return new Promise((resolve6) => {
25839
26429
  if (signal?.aborted) {
25840
- resolve5({ kind: "cancelled" });
26430
+ resolve6({ kind: "cancelled" });
25841
26431
  return;
25842
26432
  }
25843
26433
  const child = spawn2(command, args, { env: process.env, stdio: ["ignore", "pipe", "pipe"] });
@@ -25873,7 +26463,7 @@ function runProbeChild(request, options = {}) {
25873
26463
  const envelope = stdout.length > 0 && stdout.length <= MAX_PROBE_LINE_BYTES ? parseStatusEnvelope(stdout.toString("utf8")) : void 0;
25874
26464
  outcome = envelope !== void 0 ? { kind: "ok", envelope } : probeFailed(stderr.toString("utf8"));
25875
26465
  }
25876
- resolve5(outcome);
26466
+ resolve6(outcome);
25877
26467
  };
25878
26468
  if (child.pid !== void 0) options.onSpawn?.(child.pid);
25879
26469
  child.stdout?.on("data", (chunk) => {
@@ -25965,15 +26555,15 @@ var TIMEOUT_RE = /^(0|\d+s|\d+m)$/;
25965
26555
  var RETRYABLE_STATUS_ERRORS = { NODE_UNREACHABLE: true, NODE_ERROR: true };
25966
26556
  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
26557
  function abortableSleep(ms2, signal) {
25968
- return new Promise((resolve5) => {
26558
+ return new Promise((resolve6) => {
25969
26559
  if (signal.aborted) {
25970
- resolve5();
26560
+ resolve6();
25971
26561
  return;
25972
26562
  }
25973
26563
  const done = () => {
25974
26564
  clearTimeout(timer);
25975
26565
  signal.removeEventListener("abort", done);
25976
- resolve5();
26566
+ resolve6();
25977
26567
  };
25978
26568
  const timer = setTimeout(done, ms2);
25979
26569
  signal.addEventListener("abort", done, { once: true });
@@ -26639,7 +27229,7 @@ function permissionsFromDelegation2(delegation) {
26639
27229
  }
26640
27230
  async function loadDelegationCandidates(source) {
26641
27231
  try {
26642
- const raw = JSON.parse(await readFile10(source, "utf8"));
27232
+ const raw = JSON.parse(await readFile9(source, "utf8"));
26643
27233
  return normalizeDelegationCandidates(raw, source);
26644
27234
  } catch (error) {
26645
27235
  if (error?.code !== "ENOENT") {
@@ -26659,7 +27249,7 @@ async function loadDelegationCandidates(source) {
26659
27249
  }
26660
27250
  try {
26661
27251
  const importedPath = join11(resolveProfilesDir(), source, "additional-delegations.json");
26662
- const raw = JSON.parse(await readFile10(importedPath, "utf8"));
27252
+ const raw = JSON.parse(await readFile9(importedPath, "utf8"));
26663
27253
  return normalizeDelegationCandidates(raw, source);
26664
27254
  } catch (error) {
26665
27255
  if (error?.code === "ENOENT") {
@@ -27166,7 +27756,7 @@ function registerSecretsCommand(program, openKeyAcquisition) {
27166
27756
  throw new CLIError("USAGE_ERROR", "Provide only one of: value argument, --file, or --stdin", ExitCode.USAGE_ERROR);
27167
27757
  }
27168
27758
  if (options.file) {
27169
- secretValue = await readFile10(options.file, "utf-8");
27759
+ secretValue = await readFile9(options.file, "utf-8");
27170
27760
  } else if (options.stdin) {
27171
27761
  secretValue = (await readStdin3()).toString("utf-8");
27172
27762
  } else {
@@ -32753,7 +33343,7 @@ async function importAesKey(key32, usage) {
32753
33343
  [usage]
32754
33344
  );
32755
33345
  }
32756
- async function open6(blob, key32) {
33346
+ async function open7(blob, key32) {
32757
33347
  if (blob.length < HEADER_LENGTH + NONCE_LENGTH + TAG_LENGTH) {
32758
33348
  throw new TypeError(`sealed blob too short: ${blob.length} bytes`);
32759
33349
  }
@@ -33091,7 +33681,7 @@ async function resolvePolicyShare(link3, options) {
33091
33681
  options.signal?.throwIfAborted();
33092
33682
  let envelope;
33093
33683
  try {
33094
- const encoded = new TextDecoder("utf-8", { fatal: true }).decode(await open6(parsed.ciphertext, parsed.key32));
33684
+ const encoded = new TextDecoder("utf-8", { fatal: true }).decode(await open7(parsed.ciphertext, parsed.key32));
33095
33685
  const value = JSON.parse(encoded);
33096
33686
  if (canonicalize2(value) !== encoded) throw new Error("non-canonical envelope");
33097
33687
  envelope = shareEnvelopeV3Schema.parse(value);
@@ -35683,9 +36273,9 @@ function receiveJson(result, path) {
35683
36273
 
35684
36274
  // src/share/io.ts
35685
36275
  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";
36276
+ 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
36277
  import { randomBytes as randomBytes7 } from "crypto";
35688
- import { basename as basename4, join as join12, resolve as resolve3, sep as sep2 } from "path";
36278
+ import { basename as basename4, join as join12, resolve as resolve4, sep as sep2 } from "path";
35689
36279
  init_errors();
35690
36280
  var MAX_SHARE_STDIN_BYTES = 100 * 1024 * 1024;
35691
36281
  var MAX_SHARE_URL_BYTES = 64 * 1024;
@@ -35722,26 +36312,26 @@ function shareFilename(value) {
35722
36312
  }
35723
36313
  }
35724
36314
  function shareInputFilename(input, name) {
35725
- return shareFilename(name ?? (input === "-" ? "stdin.md" : basename4(resolve3(input))));
36315
+ return shareFilename(name ?? (input === "-" ? "stdin.md" : basename4(resolve4(input))));
35726
36316
  }
35727
36317
  async function readShareInput(input, name, limit = MAX_SHARE_STDIN_BYTES) {
35728
36318
  const filename = shareInputFilename(input, name);
35729
36319
  if (input === "-") return { bytes: await readBoundedStdin(limit), filename };
35730
- const path = resolve3(input);
35731
- const info = await stat4(path);
36320
+ const path = resolve4(input);
36321
+ const info = await stat5(path);
35732
36322
  if (!info.isFile() || info.size > limit) throw new Error("MAX_BYTES_EXCEEDED");
35733
- const bytes = new Uint8Array(await readFile11(path));
36323
+ const bytes = new Uint8Array(await readFile10(path));
35734
36324
  if (bytes.byteLength > limit) throw new Error("MAX_BYTES_EXCEEDED");
35735
36325
  return { bytes, filename };
35736
36326
  }
35737
36327
  async function assertDirectory(path) {
35738
- const absolute = resolve3(path);
36328
+ const absolute = resolve4(path);
35739
36329
  const segments = absolute.split(sep2).filter(Boolean);
35740
36330
  let current = absolute.startsWith(sep2) ? sep2 : "";
35741
36331
  for (const segment of segments) {
35742
36332
  current = current === sep2 ? join12(current, segment) : join12(current, segment);
35743
36333
  try {
35744
- const info = await lstat5(current);
36334
+ const info = await lstat6(current);
35745
36335
  if (info.isSymbolicLink()) {
35746
36336
  const canonical = await realpath2(current);
35747
36337
  if (current !== "/tmp" && current !== "/var") throw new Error("OUTPUT_EXISTS");
@@ -35749,27 +36339,27 @@ async function assertDirectory(path) {
35749
36339
  } else if (!info.isDirectory()) throw new Error("OUTPUT_EXISTS");
35750
36340
  } catch (error) {
35751
36341
  if (error.code !== "ENOENT") throw error;
35752
- await mkdir4(current, { mode: 448 });
35753
- const created = await lstat5(current);
36342
+ await mkdir5(current, { mode: 448 });
36343
+ const created = await lstat6(current);
35754
36344
  if (created.isSymbolicLink() || !created.isDirectory()) throw new Error("OUTPUT_EXISTS");
35755
36345
  }
35756
36346
  }
35757
36347
  }
35758
36348
  async function writeShareOutput(directory, filename, bytes, force) {
35759
- const outputDirectory = resolve3(directory);
36349
+ const outputDirectory = resolve4(directory);
35760
36350
  await assertDirectory(outputDirectory);
35761
36351
  const safeName = safeFilename(filename);
35762
- const directoryHandle = await open7(outputDirectory, constants.O_RDONLY | (constants.O_DIRECTORY ?? 0) | (constants.O_NOFOLLOW ?? 0));
36352
+ const directoryHandle = await open8(outputDirectory, constants.O_RDONLY | (constants.O_DIRECTORY ?? 0) | (constants.O_NOFOLLOW ?? 0));
35763
36353
  const stableDirectory = await realpath2(outputDirectory);
35764
36354
  const outputPath = join12(stableDirectory, safeName);
35765
36355
  const directoryIdentity = await directoryHandle.stat();
35766
36356
  const assertStableDirectory = async () => {
35767
- const current = await stat4(stableDirectory);
36357
+ const current = await stat5(stableDirectory);
35768
36358
  if (current.dev !== directoryIdentity.dev || current.ino !== directoryIdentity.ino) throw new Error("OUTPUT_EXISTS");
35769
36359
  };
35770
36360
  await assertStableDirectory();
35771
36361
  const stagingDirectory = await mkdtemp(join12(stableDirectory, ".tinycloud-share-stage-"));
35772
- const stagingInfo = await lstat5(stagingDirectory);
36362
+ const stagingInfo = await lstat6(stagingDirectory);
35773
36363
  if (!stagingInfo.isDirectory() || (stagingInfo.mode & 511) !== 448) throw new Error("OUTPUT_EXISTS");
35774
36364
  const stagingPath = join12(stagingDirectory, `.tinycloud-share-${randomBytes7(16).toString("hex")}.tmp`);
35775
36365
  let temporaryPath;
@@ -35777,13 +36367,13 @@ async function writeShareOutput(directory, filename, bytes, force) {
35777
36367
  try {
35778
36368
  await assertStableDirectory();
35779
36369
  try {
35780
- const existing = await lstat5(outputPath);
36370
+ const existing = await lstat6(outputPath);
35781
36371
  if (existing.isSymbolicLink()) throw new Error("UNSAFE_FILENAME");
35782
36372
  } catch (error) {
35783
36373
  if (error.code !== "ENOENT") throw error;
35784
36374
  }
35785
36375
  temporaryPath = stagingPath;
35786
- handle = await open7(temporaryPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0), 384);
36376
+ handle = await open8(temporaryPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0), 384);
35787
36377
  await handle.writeFile(bytes);
35788
36378
  await handle.close();
35789
36379
  handle = void 0;
@@ -36233,8 +36823,8 @@ init_theme();
36233
36823
  import { CLI_NAME as CLI_NAME26 } from "@tinycloud/operations/cli-name";
36234
36824
  import { ACCOUNT_REGISTRY_SPACE as ACCOUNT_REGISTRY_SPACE2 } from "@tinycloud/node-sdk";
36235
36825
  import { randomBytes as randomBytes8 } from "crypto";
36236
- import { mkdir as mkdir5, writeFile as writeFile4 } from "fs/promises";
36237
- import { dirname as dirname6 } from "path";
36826
+ import { mkdir as mkdir6, writeFile as writeFile4 } from "fs/promises";
36827
+ import { dirname as dirname7 } from "path";
36238
36828
  function didWithoutFragment2(did) {
36239
36829
  const fragment = did.indexOf("#");
36240
36830
  return fragment === -1 ? did : did.slice(0, fragment);
@@ -36390,7 +36980,7 @@ it directly with \`${CLI_NAME26} space host <name>\` (no request needed).
36390
36980
  }
36391
36981
  async function emitHostRequestArtifact(artifact, emitOption) {
36392
36982
  if (typeof emitOption === "string" && emitOption.length > 0) {
36393
- await mkdir5(dirname6(emitOption), { recursive: true });
36983
+ await mkdir6(dirname7(emitOption), { recursive: true });
36394
36984
  await writeFile4(emitOption, JSON.stringify(artifact, null, 2) + "\n", "utf8");
36395
36985
  outputJson({
36396
36986
  emitted: true,
@@ -36416,7 +37006,7 @@ init_host();
36416
37006
  init_theme();
36417
37007
  import { CLI_NAME as CLI_NAME27 } from "@tinycloud/operations/cli-name";
36418
37008
  import { writeFile as writeFile5 } from "fs/promises";
36419
- import { resolve as resolve4 } from "path";
37009
+ import { resolve as resolve5 } from "path";
36420
37010
  async function dbHandle(node, dbName, spaceInput, profileName2) {
36421
37011
  const spaceUri = await resolveSpaceUri(spaceInput, profileName2);
36422
37012
  const sql = spaceUri ? node.sqlForSpace(spaceUri) : node.sql;
@@ -36558,7 +37148,7 @@ Output:
36558
37148
  }
36559
37149
  const blob = result.data;
36560
37150
  const buffer = Buffer.from(await blob.arrayBuffer());
36561
- const outputPath = resolve4(options.output);
37151
+ const outputPath = resolve5(options.output);
36562
37152
  await writeFile5(outputPath, buffer);
36563
37153
  outputJson({
36564
37154
  file: outputPath,
@@ -37114,7 +37704,7 @@ function registerUpgradeCommand(program) {
37114
37704
  }
37115
37705
 
37116
37706
  // src/commands/vault.ts
37117
- import { readFile as readFile12 } from "fs/promises";
37707
+ import { readFile as readFile11 } from "fs/promises";
37118
37708
  init_profiles();
37119
37709
  init_formatter();
37120
37710
  init_errors();
@@ -37176,7 +37766,7 @@ function registerVaultCommand(program) {
37176
37766
  throw new CLIError("USAGE_ERROR", "Provide only one of: value argument, --file, or --stdin", ExitCode.USAGE_ERROR);
37177
37767
  }
37178
37768
  if (options.file) {
37179
- putValue = new Uint8Array(await readFile12(options.file));
37769
+ putValue = new Uint8Array(await readFile11(options.file));
37180
37770
  } else if (options.stdin) {
37181
37771
  putValue = new Uint8Array(await readStdin4());
37182
37772
  } else {
@@ -37291,7 +37881,7 @@ function registerVaultCommand(program) {
37291
37881
  }
37292
37882
 
37293
37883
  // src/commands/vars.ts
37294
- import { readFile as readFile13 } from "fs/promises";
37884
+ import { readFile as readFile12 } from "fs/promises";
37295
37885
  init_profiles();
37296
37886
  init_formatter();
37297
37887
  init_errors();
@@ -37396,7 +37986,7 @@ function registerVarsCommand(program) {
37396
37986
  throw new CLIError("USAGE_ERROR", "Provide only one of: value argument, --file, or --stdin", ExitCode.USAGE_ERROR);
37397
37987
  }
37398
37988
  if (options.file) {
37399
- varValue = await readFile13(options.file, "utf-8");
37989
+ varValue = await readFile12(options.file, "utf-8");
37400
37990
  } else if (options.stdin) {
37401
37991
  varValue = (await readStdin5()).toString("utf-8");
37402
37992
  } else {