@tinycloud/cli 1.2.6-beta.3 → 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
  }
@@ -8540,6 +8543,8 @@ var init_dist2 = __esm({
8540
8543
  REPLICATION_SYNC: "replication.sync",
8541
8544
  REPLICATION_STATE: "replication.state",
8542
8545
  REPLICATION_DIVERGENCE: "replication.divergence",
8546
+ REPLICATION_GRANT: "replication.grant",
8547
+ REPLICATION_LIFECYCLE: "replication.lifecycle",
8543
8548
  /** Emitted once, on the first write rejected because storage is full. */
8544
8549
  STORAGE_FULL: "storage.full"
8545
8550
  };
@@ -8559,21 +8564,27 @@ var init_dist2 = __esm({
8559
8564
  "fetched",
8560
8565
  "pages",
8561
8566
  "lagMs",
8562
- "pendingCleared"
8567
+ "pendingCleared",
8568
+ "partitions",
8569
+ "remaining",
8570
+ "expiryMs"
8563
8571
  ]);
8564
- SAFE_BOOLEAN_FIELDS = /* @__PURE__ */ new Set(["authenticated", "ok", "persisted"]);
8572
+ SAFE_BOOLEAN_FIELDS = /* @__PURE__ */ new Set(["authenticated", "ok", "persisted", "purgePending"]);
8565
8573
  SAFE_REPLICATION_FIELDS = {
8566
8574
  source: ["replica", "network", "none"],
8567
- reason: ["hit", "absent", "deleted", "content_missing", "coverage_incomplete", "not_covered", "pending_write", "grant_missing", "grant_expired", "grant_revoked", "grant_not_yet_valid", "stale", "replica_error", "runtime_unsupported", "replica_unavailable", "unsupported_option", "network_cursor", "cursor_restart", "aborted", "REPLICA_BEHIND_OWN_WRITES", "REPLICA_UNPROVEN_SINCE_START", "NETWORK_REQUESTED"],
8575
+ reason: ["hit", "absent", "deleted", "content_missing", "coverage_incomplete", "not_covered", "pending_write", "grant_missing", "grant_expired", "grant_revoked", "grant_not_yet_valid", "stale", "replica_error", "runtime_unsupported", "replica_unavailable", "unsupported_option", "network_cursor", "cursor_restart", "aborted", "REPLICA_BEHIND_OWN_WRITES", "REPLICA_UNPROVEN_SINCE_START", "NETWORK_REQUESTED", "replica_gated", "sign_out", "account_switch", "consent_expired", "startup"],
8568
8576
  op: ["get", "list", "put", "delete", "batchPut"],
8569
- trigger: ["start", "interval", "stale_read", "manual"],
8577
+ trigger: ["start", "interval", "stale_read", "manual", "peer"],
8578
+ origin: ["peer"],
8570
8579
  outcome: ["found", "not_found", "error", "committed", "failed", "ambiguous", "ok", "busy", "aborted"],
8571
- class: ["offline", "authority", "storage", "node"],
8572
- state: ["opened", "grant_installed", "grant_missing", "runtime_unsupported", "unavailable", "revoked", "recreated", "purged", "purge_failed", "pending_store_error", "pinned", "pending_cleared", "closed", "idle", "ready"],
8580
+ class: ["offline", "authority", "storage", "node", "lifecycle"],
8581
+ state: ["opened", "grant_installed", "grant_missing", "runtime_unsupported", "unavailable", "revoked", "recreated", "purged", "purge_failed", "pending_store_error", "pinned", "pending_cleared", "closed", "idle", "ready", "gated"],
8582
+ action: ["minted", "renewed", "renewal_refused", "renewal_deferred", "capped", "expired", "consent_deadline_reached", "denied", "deny_stale", "deny_failed", "database_deleted", "marker_set", "marker_cleared", "unguarded", "drained", "erased", "erase_failed", "activation_blocked", "activated", "recovered"],
8583
+ gate: ["not_established", "stamp_stale", "principal_mismatch", "purge_pending", "logout_pending", "gate_unavailable"],
8573
8584
  kind: ["value", "missing_local", "extra_local", "keys", "order"],
8574
8585
  coverage: ["empty", "bootstrapping", "complete"],
8575
8586
  authority: ["valid", "expired", "revoked", "not-yet-valid"],
8576
- code: ["NETWORK_ERROR", "TIMEOUT", "ABORTED", "REPLICA_BUSY", "REPLICA_BEHIND_OWN_WRITES", "REPLICA_UNPROVEN_SINCE_START", "NETWORK_REQUESTED", "INTEGRITY_ERROR", "STORAGE_ERROR", "REPLICA_CLOSED", "RUNTIME_UNSUPPORTED", "GRANT_EXPIRED", "GRANT_REVOKED", "GRANT_NOT_YET_VALID", "GRANT_MISSING", "GRANT_INVALID", "SOURCE_CHANGED", "PROTOCOL_ERROR", "SCOPE_VIOLATION", "NODE_ERROR", "CONTENT_MISMATCH", "STORAGE_FULL"],
8587
+ code: ["NETWORK_ERROR", "TIMEOUT", "ABORTED", "REPLICA_BUSY", "REPLICA_BEHIND_OWN_WRITES", "REPLICA_UNPROVEN_SINCE_START", "NETWORK_REQUESTED", "INTEGRITY_ERROR", "STORAGE_ERROR", "REPLICA_CLOSED", "RUNTIME_UNSUPPORTED", "GRANT_EXPIRED", "GRANT_REVOKED", "GRANT_NOT_YET_VALID", "GRANT_MISSING", "GRANT_INVALID", "SOURCE_CHANGED", "PROTOCOL_ERROR", "SCOPE_VIOLATION", "NODE_ERROR", "CONTENT_MISMATCH", "STORAGE_FULL", "REPLICA_GATED", "RESET_REQUIRED", "STORAGE_BUDGET", "CONSENT_EXPIRED"],
8577
8588
  pendingState: ["in_flight", "committed", "ambiguous"],
8578
8589
  strategy: ["session", "minted", "installed"],
8579
8590
  verify: ["match", "diverged", "error", "aborted"]
@@ -11586,8 +11597,8 @@ var init_dist2 = __esm({
11586
11597
  if (this.closed) {
11587
11598
  return Promise.resolve({ value: void 0, done: true });
11588
11599
  }
11589
- return new Promise((resolve5, reject) => {
11590
- this.waiters.push({ resolve: resolve5, reject });
11600
+ return new Promise((resolve6, reject) => {
11601
+ this.waiters.push({ resolve: resolve6, reject });
11591
11602
  });
11592
11603
  }
11593
11604
  };
@@ -11983,7 +11994,7 @@ var init_dist2 = __esm({
11983
11994
  } finally {
11984
11995
  for (const abort of aborts) abort.dispose();
11985
11996
  }
11986
- await new Promise((resolve5) => setTimeout(resolve5, 0));
11997
+ await new Promise((resolve6) => setTimeout(resolve6, 0));
11987
11998
  }
11988
11999
  /** The stream the current subscribers need, or undefined when none remain. */
11989
12000
  plan() {
@@ -14354,12 +14365,14 @@ import { resolveSecretsOwner } from "@tinycloud/operations/secret-request";
14354
14365
  import {
14355
14366
  additionalDelegationsPath as sharedAdditionalDelegationsPath,
14356
14367
  authRequestsPath as sharedAuthRequestsPath,
14368
+ profilePath,
14357
14369
  profileStoreMetadataPath,
14358
14370
  readAdditionalDelegations,
14359
14371
  readAuthRequests,
14360
14372
  refuseWriteToDeletedProfile,
14361
14373
  updateProfileStore,
14362
14374
  withProfileLock,
14375
+ withPinnedProfileDirectory,
14363
14376
  writeJsonAtomic
14364
14377
  } from "@tinycloud/operations/state";
14365
14378
  function isCompatiblePermissionRequestArtifact(value) {
@@ -14377,10 +14390,10 @@ function permissionRequestsPath(profile) {
14377
14390
  return sharedAuthRequestsPath(profile);
14378
14391
  }
14379
14392
  function grantHistoryPath(profile) {
14380
- return join3(PROFILES_DIR, profile, "auth-grants.jsonl");
14393
+ return join3(profilePath(profile), "auth-grants.jsonl");
14381
14394
  }
14382
14395
  function localGrantArtifactsPath(profile) {
14383
- return join3(PROFILES_DIR, profile, "auth-grant-artifacts.json");
14396
+ return join3(profilePath(profile), "auth-grant-artifacts.json");
14384
14397
  }
14385
14398
  function createPermissionRequestArtifact(params) {
14386
14399
  return buildPermissionRequestArtifact({
@@ -14407,22 +14420,30 @@ async function loadAdditionalDelegations(profile) {
14407
14420
  return readAdditionalDelegations(profile);
14408
14421
  }
14409
14422
  async function loadLocalGrantArtifacts(profile) {
14410
- const path = localGrantArtifactsPath(profile);
14411
- if (!await fileExists(path)) return [];
14412
- let parsed;
14413
- try {
14414
- parsed = JSON.parse(await readFile2(path, "utf8"));
14415
- } catch {
14416
- return [];
14417
- }
14418
- if (!Array.isArray(parsed)) return [];
14419
- return parsed.filter(
14420
- (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"
14421
- );
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
+ });
14422
14443
  }
14423
14444
  async function appendLocalGrantArtifact(profile, delegation) {
14424
- await ProfileManager.ensureProfileDir(profile);
14425
14445
  await withProfileLock(profile, async () => {
14446
+ await ProfileManager.ensureProfileDir(profile);
14426
14447
  await refuseWriteToDeletedProfile(profile);
14427
14448
  const records = await loadLocalGrantArtifacts(profile);
14428
14449
  if (records.some((record2) => record2.delegationCid === delegation.cid)) return;
@@ -14515,21 +14536,27 @@ async function cliGrantRecord(node, delegation, permissions, host) {
14515
14536
  );
14516
14537
  }
14517
14538
  async function appendGrantHistory(profile, entry) {
14518
- await ProfileManager.ensureProfileDir(profile);
14519
- const line = JSON.stringify({
14520
- ts: (/* @__PURE__ */ new Date()).toISOString(),
14521
- profile,
14522
- ...entry
14523
- }) + "\n";
14524
- const path = grantHistoryPath(profile);
14525
- await appendFile(path, line, { encoding: "utf8", mode: PRIVATE_FILE_MODE });
14526
- 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
+ });
14527
14551
  }
14528
14552
  async function readGrantHistory(profile) {
14529
- const path = grantHistoryPath(profile);
14530
- if (!await fileExists(path)) return [];
14531
- const raw = await readFile2(path, "utf8");
14532
- 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
+ });
14533
14560
  }
14534
14561
  async function parseCapSpec(spec, profile) {
14535
14562
  const firstColon = spec.indexOf(":");
@@ -14778,7 +14805,6 @@ var init_permissions = __esm({
14778
14805
  "src/lib/permissions.ts"() {
14779
14806
  "use strict";
14780
14807
  init_manifest();
14781
- init_constants();
14782
14808
  init_storage();
14783
14809
  init_profiles();
14784
14810
  init_errors();
@@ -14903,7 +14929,14 @@ function createReplicationEventSink(profileRoot, options) {
14903
14929
  `);
14904
14930
  } else if (event.type === "replication.sync") {
14905
14931
  const pending = event.pendingCleared === void 0 ? "" : ` pendingCleared:${event.pendingCleared}`;
14906
- process.stderr.write(`[replication] sync ${event.replica} ${event.outcome}${event.code ? ` ${event.code}` : ""}${pending}
14932
+ const origin = event.origin === void 0 ? "" : ` origin:${event.origin}`;
14933
+ process.stderr.write(`[replication] sync ${event.replica} ${event.outcome}${event.code ? ` ${event.code}` : ""}${pending}${origin}
14934
+ `);
14935
+ } else if (event.type === "replication.grant") {
14936
+ process.stderr.write(`[replication] grant ${event.replicaId} ${event.action}${event.code ? ` ${event.code}` : ""}${event.effectiveExpiresAt ? ` effectiveExpiresAt:${event.effectiveExpiresAt}` : ""}
14937
+ `);
14938
+ } else if (event.type === "replication.lifecycle") {
14939
+ process.stderr.write(`[replication] lifecycle ${event.action}${event.reason ? ` ${event.reason}` : ""}${event.gate ? ` gate:${event.gate}` : ""}${event.code ? ` ${event.code}` : ""}${event.remaining === void 0 ? "" : ` remaining:${event.remaining}`}
14907
14940
  `);
14908
14941
  }
14909
14942
  }
@@ -17750,7 +17783,7 @@ var init_dist3 = __esm({
17750
17783
  // src/lib/replication-config.ts
17751
17784
  import { join as join5 } from "path";
17752
17785
  import { CLI_NAME as CLI_NAME6 } from "@tinycloud/operations/cli-name";
17753
- import { profilePath, withProfileLock as withProfileLock2 } from "@tinycloud/operations/state";
17786
+ import { profilePath as profilePath2, withProfileLock as withProfileLock2 } from "@tinycloud/operations/state";
17754
17787
  import { sqliteReplicaStorage } from "@tinycloud/node-sdk";
17755
17788
  function createCliReplicationConfig(profile, settings, output2, enabled) {
17756
17789
  if (!enabled || !settings || settings.prefixes.length === 0) return void 0;
@@ -17768,7 +17801,7 @@ function createCliReplicationConfig(profile, settings, output2, enabled) {
17768
17801
  );
17769
17802
  }
17770
17803
  }
17771
- const root = profilePath(profile);
17804
+ const root = profilePath2(profile);
17772
17805
  return {
17773
17806
  enabled: true,
17774
17807
  prefixes: settings.prefixes,
@@ -18254,17 +18287,21 @@ var init_host = __esm({
18254
18287
  });
18255
18288
 
18256
18289
  // src/config/profiles.ts
18257
- 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";
18258
18291
  import { CLI_NAME as CLI_NAME9 } from "@tinycloud/operations/cli-name";
18259
18292
  import { join as join6 } from "path";
18260
18293
  import {
18261
- profilePath as profilePath2,
18294
+ assertRealProfileDirectory,
18295
+ ProfileSymlinkError as ProfileSymlinkError2,
18296
+ namedProfilePath,
18297
+ profilePath as profilePath3,
18262
18298
  readSession,
18263
18299
  recordProfileDeletion,
18264
18300
  refuseWriteToDeletedProfile as refuseWriteToDeletedProfile2,
18265
18301
  removeSession,
18266
18302
  withProfileLock as withProfileLock3,
18267
- writeSession
18303
+ writeSession,
18304
+ withPinnedProfileDirectory as withPinnedProfileDirectory2
18268
18305
  } from "@tinycloud/operations/state";
18269
18306
  var ProfileManager;
18270
18307
  var init_profiles = __esm({
@@ -18306,8 +18343,13 @@ var init_profiles = __esm({
18306
18343
  }
18307
18344
  /** Owner-only profile directory (0700), created or tightened before any write into it. */
18308
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
+ }
18309
18351
  await _ProfileManager.ensureConfigDir();
18310
- const profileDir = join6(PROFILES_DIR, name);
18352
+ await assertRealProfileDirectory(name);
18311
18353
  await ensureDir(profileDir);
18312
18354
  await chmod2(profileDir, PRIVATE_DIR_MODE);
18313
18355
  return profileDir;
@@ -18336,8 +18378,7 @@ var init_profiles = __esm({
18336
18378
  * Throws CLIError if the profile doesn't exist.
18337
18379
  */
18338
18380
  static async getProfile(name) {
18339
- const profilePath5 = join6(PROFILES_DIR, name, "profile.json");
18340
- const profile = await readJson(profilePath5);
18381
+ const profile = await withPinnedProfileDirectory2(name, (directory) => directory ? readJson(join6(directory, "profile.json")) : Promise.resolve(null));
18341
18382
  if (!profile) {
18342
18383
  throw new CLIError(
18343
18384
  "PROFILE_NOT_FOUND",
@@ -18357,13 +18398,13 @@ var init_profiles = __esm({
18357
18398
  }
18358
18399
  /** Removes profile.json under the profile lock (rollback of a profile a failed login created). */
18359
18400
  static async removeProfileConfig(name) {
18360
- 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 }));
18361
18402
  }
18362
18403
  /**
18363
18404
  * Returns true if a profile directory exists.
18364
18405
  */
18365
18406
  static async profileExists(name) {
18366
- return fileExists(join6(PROFILES_DIR, name, "profile.json"));
18407
+ return withPinnedProfileDirectory2(name, (directory) => directory ? fileExists(join6(directory, "profile.json")) : Promise.resolve(false));
18367
18408
  }
18368
18409
  /**
18369
18410
  * Returns an array of profile directory names.
@@ -18387,7 +18428,7 @@ var init_profiles = __esm({
18387
18428
  static async deleteProfile(name) {
18388
18429
  let profileDir;
18389
18430
  try {
18390
- profileDir = profilePath2(name);
18431
+ profileDir = namedProfilePath(name);
18391
18432
  } catch {
18392
18433
  throw new CLIError(
18393
18434
  "INVALID_PROFILE_NAME",
@@ -18412,23 +18453,21 @@ var init_profiles = __esm({
18412
18453
  return;
18413
18454
  }
18414
18455
  await _ProfileManager.withLock(name, async () => {
18415
- for (const file of ["session.json", "key.json"]) await rm2(join6(profileDir, file), { force: true });
18416
- for (const entry of await readdir2(profileDir)) {
18417
- 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 });
18418
18460
  }
18419
- await rm2(join6(profileDir, "profile.json"), { force: true });
18461
+ await rm2(join6(directory, "profile.json"), { force: true });
18420
18462
  await recordProfileDeletion(name);
18421
18463
  });
18422
- await rmdir(profileDir).catch((error) => {
18423
- if (error.code !== "ENOENT" && error.code !== "ENOTEMPTY" && error.code !== "EEXIST") throw error;
18424
- });
18425
18464
  }
18426
18465
  // ── Key management ──────────────────────────────────────────────────
18427
18466
  /**
18428
18467
  * Returns the parsed JWK for a profile, or null if no key exists.
18429
18468
  */
18430
18469
  static async getKey(name) {
18431
- return readJson(join6(PROFILES_DIR, name, "key.json"));
18470
+ return withPinnedProfileDirectory2(name, (directory) => directory ? readJson(join6(directory, "key.json")) : Promise.resolve(null));
18432
18471
  }
18433
18472
  /**
18434
18473
  * Saves a JWK key under the profile lock (0600, in an owner-only profile
@@ -18443,7 +18482,7 @@ var init_profiles = __esm({
18443
18482
  }
18444
18483
  /** Removes key.json under the profile lock (rollback of a key a failed login created). */
18445
18484
  static async removeKey(name) {
18446
- 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 }));
18447
18486
  }
18448
18487
  // ── Session management ──────────────────────────────────────────────
18449
18488
  /**
@@ -18470,10 +18509,13 @@ var init_profiles = __esm({
18470
18509
  * created or tightened to 0700.
18471
18510
  */
18472
18511
  static async getCacheDir(name) {
18473
- const cacheDir = join6(await _ProfileManager.ensureProfileDir(name), "cache");
18474
- await ensureDir(cacheDir);
18475
- await chmod2(cacheDir, PRIVATE_DIR_MODE);
18476
- 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
+ });
18477
18519
  }
18478
18520
  // ── Resolution helpers ──────────────────────────────────────────────
18479
18521
  /**
@@ -18497,7 +18539,8 @@ var init_profiles = __esm({
18497
18539
  try {
18498
18540
  profileConfig = await _ProfileManager.getProfile(profile);
18499
18541
  profileHost = profileConfig.host;
18500
- } catch {
18542
+ } catch (error) {
18543
+ if (error instanceof ProfileSymlinkError2) throw error;
18501
18544
  }
18502
18545
  const envReplication = process.env.TC_REPLICATION?.toLowerCase();
18503
18546
  const envEnabled = envReplication === "1" || envReplication === "true" || envReplication === "on";
@@ -18861,22 +18904,23 @@ import { get as httpGet } from "http";
18861
18904
  import { get as httpsGet } from "https";
18862
18905
  import { spawn } from "child_process";
18863
18906
  import { mkdir as mkdir3, readFile as readFile5, writeFile as writeFile2 } from "fs/promises";
18864
- import { dirname as dirname3 } from "path";
18907
+ import { dirname as dirname4 } from "path";
18865
18908
  import { createInterface as createInterface2 } from "readline";
18866
18909
  import { grantAuthRequest, principalDidEquals, signedCompactUcanExpiry as signedCompactUcanExpiry3 } from "@tinycloud/node-sdk";
18867
18910
  import { invokeOperation } from "@tinycloud/operations";
18868
18911
 
18869
18912
  // src/lib/profile-replicas.ts
18870
18913
  init_dist3();
18871
- 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";
18872
18916
  import { join as join8, relative, sep } from "path";
18873
- 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";
18874
18918
 
18875
18919
  // ../replica/dist/sqlite.js
18876
18920
  import { randomBytes as randomBytes2 } from "crypto";
18877
- import { constants as fsConstants } from "fs";
18878
- 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";
18879
- 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";
18880
18924
  var ReplicaErrorCode2 = {
18881
18925
  /** Another process holds the sync lease. */
18882
18926
  BUSY: "REPLICA_BUSY",
@@ -19063,6 +19107,29 @@ async function loadSqlite() {
19063
19107
  return (path, options) => wrap(options?.readonly ? new DatabaseSync(path, { readOnly: true }) : new DatabaseSync(path));
19064
19108
  }
19065
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
+ }
19066
19133
  var DURABILITY = "sqlite-wal-synchronous-full+fsynced-blobs";
19067
19134
  var SCHEMA = `
19068
19135
  CREATE TABLE IF NOT EXISTS replica (
@@ -19161,22 +19228,37 @@ async function entriesOf(path) {
19161
19228
  }
19162
19229
  }
19163
19230
  var FAULTS = /* @__PURE__ */ Symbol("tinycloud.replica.faults");
19231
+ var pinnedDirectories = new FinalizationRegistry((fd) => {
19232
+ try {
19233
+ closeSync(fd);
19234
+ } catch {
19235
+ }
19236
+ });
19164
19237
  var SqliteReplicaStore = class _SqliteReplicaStore {
19165
19238
  dir;
19166
19239
  #db;
19240
+ #pinnedDir;
19241
+ #directoryFd;
19167
19242
  #holder = `${process.pid}-${randomBytes2(6).toString("hex")}`;
19168
19243
  #now;
19169
19244
  #faults;
19170
19245
  #guard;
19171
19246
  /** Identity of the database file this store opened: a deleted-and-recreated replica is a different file. */
19172
19247
  #ino;
19173
- constructor(dir, db, ino, options) {
19248
+ #dev;
19249
+ #namedAncestors;
19250
+ constructor(dir, db, identity, options, directoryFd, namedAncestors = []) {
19174
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);
19175
19255
  this.#db = db;
19176
- this.#ino = ino;
19256
+ this.#ino = identity.ino;
19257
+ this.#dev = identity.dev;
19177
19258
  this.#now = options.now ?? Date.now;
19178
19259
  this.#faults = options[FAULTS] ?? {};
19179
19260
  this.#guard = options.guard ?? ((section) => section());
19261
+ this.#namedAncestors = namedAncestors;
19180
19262
  }
19181
19263
  /**
19182
19264
  * Read-only status snapshot; does not create a replica, migrate, recover
@@ -19189,9 +19271,10 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
19189
19271
  };
19190
19272
  checkAbort();
19191
19273
  const dbPath = join7(dir, "replica.db");
19192
- let ino;
19274
+ let identity;
19193
19275
  try {
19194
- ino = (await stat2(dbPath)).ino;
19276
+ identity = await lstat2(dbPath);
19277
+ if (!identity.isFile()) return null;
19195
19278
  } catch (error) {
19196
19279
  if (errnoOf(error) === "ENOENT") return null;
19197
19280
  throw storageError(error, "Inspecting the replica");
@@ -19204,14 +19287,14 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
19204
19287
  db = opener(dbPath, { readonly: true });
19205
19288
  db.exec("PRAGMA busy_timeout = 0");
19206
19289
  checkAbort();
19207
- 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 } : {} });
19208
19291
  for (let attempt = 0; attempt < 2; attempt++) {
19209
19292
  const before = db.get("PRAGMA data_version").data_version;
19210
19293
  const state2 = await store.open();
19211
19294
  checkAbort();
19212
19295
  if (state2 === null) return null;
19213
19296
  const status = await store.status();
19214
- await new Promise((resolve5) => setImmediate(resolve5));
19297
+ await new Promise((resolve22) => setImmediate(resolve22));
19215
19298
  checkAbort();
19216
19299
  const after = db.get("PRAGMA data_version").data_version;
19217
19300
  if (before === after) return { state: state2, status };
@@ -19231,65 +19314,145 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
19231
19314
  * REPLICA_NOT_FOUND and nothing is written to disk.
19232
19315
  */
19233
19316
  static async open(dir, options) {
19234
- const dbPath = join7(dir, "replica.db");
19317
+ const namedDbPath = join7(dir, "replica.db");
19235
19318
  if (!options.create) {
19236
- const exists = await stat2(dbPath).then(
19237
- () => true,
19319
+ const exists = await lstat2(namedDbPath).then(
19320
+ (stats) => stats.isFile(),
19238
19321
  () => false
19239
19322
  );
19240
- 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}.`);
19241
19324
  }
19242
19325
  const opener = options.sqlite ?? await loadSqlite();
19326
+ const releaseOpen = await acquireSqliteOpen();
19243
19327
  let store;
19328
+ let directoryFd;
19244
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");
19245
19344
  if (options.create) {
19246
- await mkdir2(join7(dir, "blobs", ".tmp"), { recursive: true, mode: 448 });
19247
- await Promise.all([chmod3(dir, 448), chmod3(join7(dir, "blobs"), 448), chmod3(join7(dir, "blobs", ".tmp"), 448)]);
19248
- const handle = await open2(dbPath, fsConstants.O_CREAT | fsConstants.O_RDWR, 384);
19249
- await handle.close();
19250
- await chmod3(dbPath, 384);
19251
- }
19252
- const ino = (await stat2(dbPath)).ino;
19253
- const db = opener(dbPath);
19254
- db.exec("PRAGMA busy_timeout = 5000");
19255
- db.exec("PRAGMA journal_mode = WAL");
19256
- db.exec("PRAGMA synchronous = FULL");
19257
- db.exec("PRAGMA secure_delete = ON");
19258
- db.exec("PRAGMA foreign_keys = ON");
19259
- const version = db.get("PRAGMA user_version")?.user_version ?? 0;
19260
- if (version > SCHEMA_VERSION) {
19261
- db.close();
19262
- throw new ReplicaError2(
19263
- ReplicaErrorCode2.STORAGE_ERROR,
19264
- `The replica at ${dir} was written by a newer tc (schema ${version}); upgrade tc to read it.`
19265
- );
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
+ }
19266
19358
  }
19267
- if (version < SCHEMA_VERSION) {
19268
- db.exec("BEGIN IMMEDIATE");
19269
- db.exec(SCHEMA);
19270
- db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
19271
- 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
+ }
19272
19430
  }
19273
- store = new _SqliteReplicaStore(dir, db, ino, options);
19274
19431
  } catch (error) {
19432
+ if (store) await store.close().catch(() => void 0);
19433
+ else if (directoryFd !== void 0) closeSync(directoryFd);
19275
19434
  throw storageError(error, `Opening the replica at ${dir}`);
19435
+ } finally {
19436
+ releaseOpen();
19276
19437
  }
19277
- const row = store.#row();
19278
- if (row?.revoked_detail != null && (row.purge_pending === 1 || await store.#holdsContentFiles().catch(() => true))) {
19279
- 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;
19280
19447
  }
19281
- return store;
19282
19448
  }
19283
19449
  #row() {
19284
19450
  return this.#db.get("SELECT * FROM replica WHERE id = 1");
19285
19451
  }
19286
19452
  /** Refuse to touch the filesystem for a replica that was deleted (or replaced) under us. */
19287
19453
  async #assertPresent() {
19288
- const ino = await stat2(join7(this.dir, "replica.db")).then(
19289
- (stats) => stats.ino,
19290
- () => void 0
19291
- );
19292
- 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) {
19293
19456
  throw new ReplicaError2(
19294
19457
  ReplicaErrorCode2.NOT_FOUND,
19295
19458
  `The replica at ${this.dir} was deleted while this command ran (its profile was deleted or the replica purged); nothing more was written.`
@@ -19519,7 +19682,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
19519
19682
  return held;
19520
19683
  }
19521
19684
  #blobPath(hash) {
19522
- return join7(this.dir, "blobs", hash.slice(0, 2), hash);
19685
+ return join7(this.#pinnedDir, "blobs", hash.slice(0, 2), hash);
19523
19686
  }
19524
19687
  /**
19525
19688
  * Make every blob durable before any row references it: write to a fresh
@@ -19536,11 +19699,11 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
19536
19699
  () => false
19537
19700
  );
19538
19701
  if (exists) continue;
19539
- const shard = join7(this.dir, "blobs", hash.slice(0, 2));
19702
+ const shard = join7(this.#pinnedDir, "blobs", hash.slice(0, 2));
19540
19703
  await mkdir2(shard, { mode: 448 }).catch((error) => {
19541
19704
  if (errnoOf(error) !== "EEXIST") throw error;
19542
19705
  });
19543
- 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")}`);
19544
19707
  const handle = await open2(temp, fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_WRONLY, 384);
19545
19708
  try {
19546
19709
  await handle.writeFile(bytes);
@@ -19556,7 +19719,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
19556
19719
  await unlink(temp);
19557
19720
  }
19558
19721
  dirs.add(shard);
19559
- dirs.add(join7(this.dir, "blobs"));
19722
+ dirs.add(join7(this.#pinnedDir, "blobs"));
19560
19723
  }
19561
19724
  for (const dir of dirs) {
19562
19725
  const handle = await open2(dir, fsConstants.O_RDONLY);
@@ -19664,7 +19827,7 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
19664
19827
  /** Unlink every blob not in `keep`, and every temp file. Only a missing directory counts as empty. */
19665
19828
  async #unlinkBlobs(keep) {
19666
19829
  let removed = 0;
19667
- const root = join7(this.dir, "blobs");
19830
+ const root = join7(this.#pinnedDir, "blobs");
19668
19831
  for (const shard of await entriesOf(root)) {
19669
19832
  const shardPath = join7(root, shard);
19670
19833
  if (shard === ".tmp") {
@@ -19683,13 +19846,13 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
19683
19846
  await rm3(join7(shardPath, name), { force: true });
19684
19847
  removed += 1;
19685
19848
  }
19686
- if (kept === 0) await rmdir2(shardPath).catch(() => void 0);
19849
+ if (kept === 0) await rmdir(shardPath).catch(() => void 0);
19687
19850
  }
19688
19851
  return removed;
19689
19852
  }
19690
19853
  /** Whether any content file (a blob or a temp file) is on disk. */
19691
19854
  async #holdsContentFiles() {
19692
- const root = join7(this.dir, "blobs");
19855
+ const root = join7(this.#pinnedDir, "blobs");
19693
19856
  for (const shard of await entriesOf(root)) {
19694
19857
  if ((await entriesOf(join7(root, shard))).length > 0) return true;
19695
19858
  }
@@ -19755,8 +19918,93 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
19755
19918
  }
19756
19919
  try {
19757
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();
19758
19933
  await this.#assertPresent();
19759
- 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
+ }
19760
20008
  } catch (error) {
19761
20009
  throw storageError(error, "Removing the replica");
19762
20010
  } finally {
@@ -19922,7 +20170,14 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
19922
20170
  };
19923
20171
  }
19924
20172
  async close() {
19925
- 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
+ }
19926
20181
  }
19927
20182
  };
19928
20183
 
@@ -19930,229 +20185,388 @@ var SqliteReplicaStore = class _SqliteReplicaStore {
19930
20185
  init_errors();
19931
20186
  init_constants();
19932
20187
  var SYNC_LEASE_MS = 6e4;
19933
- function refuseUnsafeReplicaPath(profile, detail) {
20188
+ function refuseUnsafeReplicaPath(profile, detail, operation = "logout") {
19934
20189
  return new CLIError(
19935
20190
  "REPLICA_PURGE_FAILED",
19936
- `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.`,
19937
20192
  ExitCode.ERROR
19938
20193
  );
19939
20194
  }
19940
- async function safeReplicaRoot(profile, root) {
20195
+ async function safeReplicaRoot(profile, root, operation = "logout") {
19941
20196
  let rootStats;
19942
20197
  try {
19943
- rootStats = await lstat2(root);
20198
+ rootStats = root.startsWith("/proc/self/fd/") ? await stat3(root) : await lstat3(root);
19944
20199
  } catch (error) {
19945
20200
  if (error?.code === "ENOENT") return null;
19946
20201
  throw error;
19947
20202
  }
19948
20203
  if (rootStats.isSymbolicLink()) {
19949
- throw refuseUnsafeReplicaPath(profile, `the replicas directory "${root}" is a symlink`);
20204
+ throw refuseUnsafeReplicaPath(profile, `the replicas directory "${root}" is a symlink`, operation);
19950
20205
  }
19951
20206
  if (!rootStats.isDirectory()) {
19952
- 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);
19953
20208
  }
19954
- const [profileRealPath, rootRealPath] = await Promise.all([realpath(profilePath3(profile)), realpath(root)]);
20209
+ const [profileRealPath, rootRealPath] = await Promise.all([realpath(profilePath4(profile)), realpath(root)]);
19955
20210
  if (!rootRealPath.startsWith(profileRealPath + sep)) {
19956
- 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);
19957
20212
  }
19958
20213
  return rootRealPath;
19959
20214
  }
19960
- async function safeReplicaEntry(profile, root, entry) {
20215
+ async function safeReplicaEntry(profile, root, entry, operation = "logout") {
19961
20216
  const entryPath = join8(root, entry.name);
19962
- const rootRealPath = await safeReplicaRoot(profile, root);
20217
+ const rootRealPath = await safeReplicaRoot(profile, root, operation);
19963
20218
  if (rootRealPath === null) {
19964
- throw refuseUnsafeReplicaPath(profile, `the replicas directory "${root}" disappeared`);
20219
+ throw refuseUnsafeReplicaPath(profile, `the replicas directory "${root}" disappeared`, operation);
19965
20220
  }
19966
- const entryStats = await lstat2(entryPath);
20221
+ const entryStats = await lstat3(entryPath);
19967
20222
  if (entry.isSymbolicLink() || entryStats.isSymbolicLink()) {
19968
- throw refuseUnsafeReplicaPath(profile, `replica entry "${entry.name}" is a symlink`);
20223
+ throw refuseUnsafeReplicaPath(profile, `replica entry "${entry.name}" is a symlink`, operation);
19969
20224
  }
19970
20225
  if (!entry.isDirectory() || !entryStats.isDirectory()) {
19971
- 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);
19972
20227
  }
19973
20228
  const entryRealPath = await realpath(entryPath);
19974
20229
  if (!entryRealPath.startsWith(rootRealPath + sep)) {
19975
- 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);
19976
20231
  }
19977
- return entryPath;
20232
+ return { path: entryPath, identity: { dev: entryStats.dev, ino: entryStats.ino } };
19978
20233
  }
19979
- async function removeProfileReplicas(profile, heldLeases) {
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;
20274
+ }
20275
+ }
20276
+ async function withPinnedReplicaForReset(profile, name, action) {
19980
20277
  return withProfileLock4(profile, async () => {
19981
- const root = join8(profilePath3(profile), "replicas");
19982
- if (await safeReplicaRoot(profile, root) === null) return [];
19983
- const replicas = (await readdir4(root, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
19984
- if (replicas.length === 0) return [];
19985
- for (const entry of replicas) await safeReplicaEntry(profile, root, entry);
19986
- await loadSqlite();
19987
- 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.`);
19988
20281
  try {
19989
- for (const entry of replicas) {
19990
- const entryPath = await safeReplicaEntry(profile, root, entry);
19991
- const store = await SqliteReplicaStore.open(entryPath, {
19992
- create: false,
19993
- guard: async (action) => {
19994
- try {
19995
- return await withProfileLock4(profile, action, { requireProfile: true });
19996
- } catch (error) {
19997
- if (error instanceof ProfileDeletedError2) {
19998
- 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;
19999
20359
  }
20000
- throw error;
20001
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);
20002
20365
  }
20003
- });
20004
- opened.push({ name: entry.name, store, lease: null });
20005
- if (await store.open() === null) {
20006
- throw new CLIError("REPLICA_PURGE_FAILED", `Replica "${entry.name}" has no initialized store; logout cleared the session but removed no replicas.`, ExitCode.ERROR);
20007
- }
20008
- const lease = heldLeases?.get(entryPath) ?? await store.acquireSyncLease(SYNC_LEASE_MS);
20009
- if (lease === null) {
20010
- 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;
20011
20371
  }
20012
- opened[opened.length - 1].lease = lease;
20013
- }
20014
- const removed = [];
20015
- for (const replica of opened) {
20016
- try {
20017
- await replica.store.destroy(replica.lease);
20018
- removed.push(replica.name);
20019
- } catch (error) {
20020
- const replicaStillExists = await lstat2(join8(root, replica.name)).then(
20021
- () => true,
20022
- (statError) => statError.code !== "ENOENT"
20023
- );
20024
- if (!replicaStillExists) removed.push(replica.name);
20025
- const remaining = opened.filter(({ name }) => !removed.includes(name)).map(({ name }) => name);
20026
- throw new CLIError(
20027
- "REPLICA_PURGE_FAILED",
20028
- `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)}`,
20029
- ExitCode.ERROR,
20030
- { replicasRemoved: removed, replicasRemaining: remaining }
20031
- );
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
+ }
20032
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
+ }));
20033
20404
  }
20034
- return removed;
20035
- } catch (error) {
20036
- if (error instanceof CLIError) throw error;
20037
- throw new CLIError(
20038
- "REPLICA_PURGE_FAILED",
20039
- `Logout cleared the session, but replica cleanup could not start; no replicas were removed. ${error instanceof Error ? error.message : String(error)}`,
20040
- ExitCode.ERROR
20041
- );
20042
20405
  } finally {
20043
- await Promise.all(opened.map(async ({ name, store, lease }) => {
20044
- if (lease !== null && heldLeases?.has(join8(root, name)) !== true) await store.releaseLease(lease).catch(() => void 0);
20045
- await store.close().catch(() => void 0);
20046
- }));
20406
+ if (!preflightRoot) await pinned.close();
20047
20407
  }
20048
20408
  });
20049
20409
  }
20050
- 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) {
20051
20443
  return withProfileLock4(profile, async () => {
20052
- const root = join8(profilePath3(profile), "replication");
20053
- const rootReal = await safeReplicaRoot(profile, root);
20054
- if (rootReal === null) return [];
20055
- const profileReal = await realpath(profilePath3(profile));
20056
- const partitions = await readdir4(root, { withFileTypes: true });
20057
- const opened = [];
20058
- const removed = [];
20059
- 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();
20060
20448
  try {
20061
- for (const partition of partitions) {
20062
- const partitionPath = join8(root, partition.name);
20063
- const partitionStats = await lstat2(partitionPath);
20064
- if (partition.isSymbolicLink() || partitionStats.isSymbolicLink()) {
20065
- throw refuseUnsafeReplicaPath(profile, `replication entry "${partition.name}" is a symlink`);
20066
- }
20067
- if (partitionStats.isDirectory()) {
20068
- const partitionReal = await realpath(partitionPath);
20069
- if (!partitionReal.startsWith(rootReal + sep) || !partitionReal.startsWith(profileReal + sep)) {
20070
- throw refuseUnsafeReplicaPath(profile, `replication entry "${partition.name}" resolves outside the selected profile`);
20071
- }
20072
- const replicasRoot2 = join8(partitionPath, "replicas");
20073
- const replicasStats = await lstat2(replicasRoot2).catch((error) => {
20074
- if (error.code === "ENOENT") return null;
20075
- throw error;
20076
- });
20077
- if (!replicasStats) continue;
20078
- if (replicasStats.isSymbolicLink() || !replicasStats.isDirectory()) {
20079
- 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`);
20080
20462
  }
20081
- const replicas = await readdir4(replicasRoot2, { withFileTypes: true });
20082
- for (const replica of replicas) {
20083
- const replicaPath = join8(replicasRoot2, replica.name);
20084
- const replicaStats = await lstat2(replicaPath);
20085
- if (replica.isSymbolicLink() || replicaStats.isSymbolicLink() || !replicaStats.isDirectory()) {
20086
- 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`);
20087
20467
  }
20088
- const replicaReal = await realpath(replicaPath);
20089
- if (!replicaReal.startsWith(rootReal + sep)) {
20090
- 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`);
20091
20476
  }
20092
- 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) => {
20093
20510
  if (error.code === "ENOENT") return null;
20094
20511
  throw error;
20095
20512
  });
20096
20513
  if (!database) continue;
20097
- if (database.isSymbolicLink() || !database.isFile()) {
20098
- 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;
20099
20537
  }
20100
20538
  }
20101
- } else if (!partitionStats.isFile()) {
20102
- throw refuseUnsafeReplicaPath(profile, `replication path "${partition.name}" is not a regular file or directory`);
20103
20539
  }
20104
- }
20105
- const hasReplicaDatabase = await Promise.all(partitions.filter((entry) => entry.isDirectory()).map(async (partition) => {
20106
- 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)));
20107
- })).then((values) => values.some((partition) => partition.some(Boolean)));
20108
- if (hasReplicaDatabase) await loadSqlite();
20109
- for (const partition of partitions.filter((entry) => entry.isDirectory())) {
20110
- const replicasRoot2 = join8(root, partition.name, "replicas");
20111
- for (const entry of await readdir4(replicasRoot2, { withFileTypes: true }).catch(() => [])) {
20112
- if (!entry.isDirectory()) continue;
20113
- const database = await lstat2(join8(replicasRoot2, entry.name, "replica.db")).catch((error) => {
20114
- if (error.code === "ENOENT") return null;
20115
- throw error;
20116
- });
20117
- if (!database) continue;
20118
- const name = `replication/${partition.name}/${entry.name}`;
20119
- const store = await SqliteReplicaStore.open(join8(replicasRoot2, entry.name), { create: false, guard: async (action) => withProfileLock4(profile, action, { requireProfile: true }) });
20120
- if (await store.open() === null) {
20121
- await store.close();
20122
- throw new CLIError("REPLICA_PURGE_FAILED", `Logout cleared the session, but flag replica "${name}" has no initialized store.`, ExitCode.ERROR);
20123
- }
20124
- const lease = heldLeases?.get(join8(replicasRoot2, entry.name)) ?? await store.acquireSyncLease(SYNC_LEASE_MS);
20125
- if (lease === null) {
20126
- await store.close();
20127
- throw new CLIError("REPLICA_BUSY", `Cannot log out: flag replica "${name}" is syncing. The session was cleared and no replicas were removed.`, ExitCode.ERROR);
20128
- }
20129
- 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);
20130
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
+ }));
20131
20567
  }
20132
- deleting = true;
20133
- for (const item of opened) {
20134
- await item.store.destroy(item.lease);
20135
- removed.push(item.name);
20136
- }
20137
- await rm4(root, { recursive: true });
20138
- return removed;
20139
- } catch (error) {
20140
- if (deleting) {
20141
- const remaining = opened.filter(({ name }) => !removed.includes(name)).map(({ name }) => name);
20142
- throw new CLIError(
20143
- "REPLICA_PURGE_FAILED",
20144
- `Logout cleared the session, but replication cleanup failed. Removed: ${removed.join(", ") || "none"}. Not removed: ${remaining.join(", ") || "none"}. ${error instanceof Error ? error.message : String(error)}`,
20145
- ExitCode.ERROR,
20146
- { replicasRemoved: removed, replicasRemaining: remaining }
20147
- );
20148
- }
20149
- if (error instanceof CLIError) throw error;
20150
- 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);
20151
20568
  } finally {
20152
- await Promise.all(opened.map(async ({ path, store, lease }) => {
20153
- if (heldLeases?.has(path) !== true) await store.releaseLease(lease).catch(() => void 0);
20154
- await store.close().catch(() => void 0);
20155
- }));
20569
+ if (!preflightRoot) await pinned.close();
20156
20570
  }
20157
20571
  });
20158
20572
  }
@@ -20160,36 +20574,54 @@ async function acquireLogoutLeases(profile) {
20160
20574
  return withProfileLock4(profile, async () => {
20161
20575
  let sqliteLoaded = false;
20162
20576
  const opened = [];
20577
+ const roots = /* @__PURE__ */ new Map();
20163
20578
  try {
20164
- const legacyRoot = join8(profilePath3(profile), "replicas");
20165
- 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;
20166
20584
  const entries = (await readdir4(legacyRoot, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
20167
20585
  for (const entry of entries) {
20168
- const path = await safeReplicaEntry(profile, legacyRoot, entry);
20586
+ const { path, identity: expectedDirectory, empty: empty4 } = await inspectLegacyReplica(profile, legacyRoot, entry);
20587
+ if (empty4) continue;
20169
20588
  if (!sqliteLoaded) {
20170
20589
  await loadSqlite();
20171
20590
  sqliteLoaded = true;
20172
20591
  }
20173
- const store = await SqliteReplicaStore.open(path, { create: false, guard: async (action) => withProfileLock4(profile, action, { requireProfile: true }) });
20174
- if (await store.open() === null) {
20175
- await store.close();
20176
- throw new CLIError("REPLICA_PURGE_FAILED", `Replica "${entry.name}" has no initialized store; logout cleared the session but removed no replicas.`, ExitCode.ERROR);
20177
- }
20178
- const lease = await store.acquireSyncLease(SYNC_LEASE_MS);
20179
- if (lease === null) {
20180
- await store.close();
20181
- 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;
20182
20612
  }
20183
- opened.push({ path, store, lease });
20184
20613
  }
20185
20614
  }
20186
- const replicationRoot = join8(profilePath3(profile), "replication");
20187
- const replicationReal = await safeReplicaRoot(profile, replicationRoot);
20188
- 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);
20189
20621
  const partitions = await readdir4(replicationRoot, { withFileTypes: true });
20190
20622
  for (const partition of partitions) {
20191
20623
  const partitionPath = join8(replicationRoot, partition.name);
20192
- const partitionStats = await lstat2(partitionPath);
20624
+ const partitionStats = await lstat3(partitionPath);
20193
20625
  if (partition.isSymbolicLink() || partitionStats.isSymbolicLink()) {
20194
20626
  throw refuseUnsafeReplicaPath(profile, `replication entry "${partition.name}" is a symlink`);
20195
20627
  }
@@ -20201,22 +20633,22 @@ async function acquireLogoutLeases(profile) {
20201
20633
  if (!partitionReal.startsWith(replicationReal + sep)) {
20202
20634
  throw refuseUnsafeReplicaPath(profile, `replication entry "${partition.name}" resolves outside the replication root`);
20203
20635
  }
20204
- const replicasRoot2 = join8(partitionPath, "replicas");
20205
- const replicasStats = await lstat2(replicasRoot2).catch((error) => {
20636
+ const replicasRoot = join8(partitionPath, "replicas");
20637
+ const replicasStats = await lstat3(replicasRoot).catch((error) => {
20206
20638
  if (error.code === "ENOENT") return null;
20207
20639
  throw error;
20208
20640
  });
20209
20641
  if (!replicasStats) continue;
20210
20642
  if (replicasStats.isSymbolicLink() || !replicasStats.isDirectory()) {
20211
- 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`);
20212
20644
  }
20213
- for (const replica of await readdir4(replicasRoot2, { withFileTypes: true })) {
20214
- const path = join8(replicasRoot2, replica.name);
20215
- 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);
20216
20648
  if (!replica.isDirectory() || replica.isSymbolicLink() || stats.isSymbolicLink() || !stats.isDirectory()) {
20217
20649
  throw refuseUnsafeReplicaPath(profile, `replication replica "${replica.name}" is not a real directory`);
20218
20650
  }
20219
- const database = await lstat2(join8(path, "replica.db")).catch((error) => {
20651
+ const database = await lstat3(join8(path, "replica.db")).catch((error) => {
20220
20652
  if (error.code === "ENOENT") return null;
20221
20653
  throw error;
20222
20654
  });
@@ -20232,26 +20664,38 @@ async function acquireLogoutLeases(profile) {
20232
20664
  await loadSqlite();
20233
20665
  sqliteLoaded = true;
20234
20666
  }
20235
- const store = await SqliteReplicaStore.open(path, { create: false, guard: async (action) => withProfileLock4(profile, action, { requireProfile: true }) });
20236
- if (await store.open() === null) {
20237
- await store.close();
20238
- throw new CLIError("REPLICA_PURGE_FAILED", `Logout cleared the session, but flag replica "replication/${partition.name}/${replica.name}" has no initialized store.`, ExitCode.ERROR);
20239
- }
20240
- const lease = await store.acquireSyncLease(SYNC_LEASE_MS);
20241
- if (lease === null) {
20242
- await store.close();
20243
- 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;
20244
20688
  }
20245
- opened.push({ path, store, lease });
20246
20689
  }
20247
20690
  }
20248
20691
  }
20249
- return opened;
20692
+ return { stores: opened, roots };
20250
20693
  } catch (error) {
20251
20694
  await Promise.all(opened.map(async ({ store, lease }) => {
20252
20695
  await store.releaseLease(lease).catch(() => void 0);
20253
20696
  await store.close().catch(() => void 0);
20254
20697
  }));
20698
+ await Promise.all([...roots.values()].map((root) => root.close()));
20255
20699
  throw error;
20256
20700
  }
20257
20701
  });
@@ -20259,31 +20703,32 @@ async function acquireLogoutLeases(profile) {
20259
20703
  async function removeProfileReplicasAndReplication(profile) {
20260
20704
  return withProfileLock4(profile, async () => {
20261
20705
  const acquired = await acquireLogoutLeases(profile);
20262
- const heldLeases = new Map(acquired.map(({ path, lease }) => [path, lease]));
20706
+ const heldLeases = new Map(acquired.stores.map(({ path, lease }) => [path, lease]));
20263
20707
  let legacyRemoved = [];
20264
20708
  try {
20265
- legacyRemoved = await removeProfileReplicas(profile, heldLeases);
20266
- 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"));
20267
20711
  return [...legacyRemoved, ...replication];
20268
20712
  } catch (error) {
20269
20713
  const partialRemoved = error instanceof CLIError && Array.isArray(error.metadata?.replicasRemoved) ? error.metadata.replicasRemoved.filter((name) => typeof name === "string") : [];
20270
20714
  const removed = [.../* @__PURE__ */ new Set([...legacyRemoved, ...partialRemoved])];
20271
20715
  const removedSet = new Set(removed);
20272
- const remaining = acquired.map(({ path }) => {
20273
- 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("/");
20274
20718
  return name.startsWith("replication/") ? name.replace("/replicas/", "/") : name.split("/").at(-1);
20275
20719
  }).filter((name) => !removedSet.has(name));
20276
20720
  throw new CLIError(
20277
20721
  error instanceof CLIError ? error.code : "REPLICA_PURGE_FAILED",
20278
- `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)}`,
20279
20723
  error instanceof CLIError ? error.exitCode : ExitCode.ERROR,
20280
20724
  { replicasRemoved: removed, replicasRemaining: remaining }
20281
20725
  );
20282
20726
  } finally {
20283
- await Promise.all(acquired.map(async ({ store, lease }) => {
20727
+ await Promise.all(acquired.stores.map(async ({ store, lease }) => {
20284
20728
  await store.releaseLease(lease).catch(() => void 0);
20285
20729
  await store.close().catch(() => void 0);
20286
20730
  }));
20731
+ await Promise.all([...acquired.roots.values()].map((root) => root.close()));
20287
20732
  }
20288
20733
  });
20289
20734
  }
@@ -20454,7 +20899,7 @@ function shouldOpenBrowser(options) {
20454
20899
  return env !== "1" && env !== "true";
20455
20900
  }
20456
20901
  async function callbackFlow(did, options = {}) {
20457
- return new Promise((resolve5, reject) => {
20902
+ return new Promise((resolve6, reject) => {
20458
20903
  let timeout;
20459
20904
  let settled = false;
20460
20905
  let rl;
@@ -20468,7 +20913,7 @@ async function callbackFlow(did, options = {}) {
20468
20913
  rl.close();
20469
20914
  }
20470
20915
  if (result.data) {
20471
- resolve5(result.data);
20916
+ resolve6(result.data);
20472
20917
  } else {
20473
20918
  reject(result.error);
20474
20919
  }
@@ -20547,8 +20992,8 @@ async function callbackFlow(did, options = {}) {
20547
20992
  }
20548
20993
  if (openBrowser) {
20549
20994
  try {
20550
- const open8 = (await import("open")).default;
20551
- await open8(authUrl);
20995
+ const open9 = (await import("open")).default;
20996
+ await open9(authUrl);
20552
20997
  } catch {
20553
20998
  settle({ error: new Error("Failed to open browser") });
20554
20999
  return;
@@ -20592,13 +21037,13 @@ Open this URL in a browser to authenticate:
20592
21037
  input: process.stdin,
20593
21038
  output: process.stderr
20594
21039
  });
20595
- return new Promise((resolve5, reject) => {
21040
+ return new Promise((resolve6, reject) => {
20596
21041
  let answered = false;
20597
21042
  rl.on("line", (input) => {
20598
21043
  if (answered || input.trim() === "") return;
20599
21044
  answered = true;
20600
21045
  rl.close();
20601
- void delegationFromInput(input, did, options).then(resolve5, reject);
21046
+ void delegationFromInput(input, did, options).then(resolve6, reject);
20602
21047
  });
20603
21048
  rl.on("close", () => {
20604
21049
  if (answered) return;
@@ -21499,7 +21944,7 @@ async function acquireDeviceDelegation(input) {
21499
21944
  expiresAt: new Date(deadline).toISOString().replace(/\.\d{3}Z$/, "Z")
21500
21945
  });
21501
21946
  let interval = started.interval;
21502
- const wait = input.wait ?? ((milliseconds) => new Promise((resolve5) => setTimeout(resolve5, milliseconds)));
21947
+ const wait = input.wait ?? ((milliseconds) => new Promise((resolve6) => setTimeout(resolve6, milliseconds)));
21503
21948
  while (Date.now() < deadline) {
21504
21949
  await wait(interval * 1e3);
21505
21950
  let response;
@@ -22135,7 +22580,7 @@ async function promptAuthMethod() {
22135
22580
  input: process.stdin,
22136
22581
  output: process.stderr
22137
22582
  });
22138
- return new Promise((resolve5) => {
22583
+ return new Promise((resolve6) => {
22139
22584
  process.stderr.write("\n" + theme.heading("Choose authentication method:") + "\n");
22140
22585
  process.stderr.write(` ${theme.accent("1)")} OpenKey ${theme.muted("(browser-based, for interactive use)")}
22141
22586
  `);
@@ -22146,9 +22591,9 @@ async function promptAuthMethod() {
22146
22591
  rl.close();
22147
22592
  const trimmed = answer.trim();
22148
22593
  if (trimmed === "2" || trimmed.toLowerCase() === "local") {
22149
- resolve5("local");
22594
+ resolve6("local");
22150
22595
  } else {
22151
- resolve5("openkey");
22596
+ resolve6("openkey");
22152
22597
  }
22153
22598
  });
22154
22599
  });
@@ -22850,7 +23295,7 @@ function describeSession(state2, expiresAt) {
22850
23295
  }
22851
23296
  async function emitPermissionRequestArtifact(artifact, emitOption) {
22852
23297
  if (typeof emitOption === "string" && emitOption.length > 0) {
22853
- await mkdir3(dirname3(emitOption), { recursive: true });
23298
+ await mkdir3(dirname4(emitOption), { recursive: true });
22854
23299
  await writeFile2(emitOption, JSON.stringify(artifact, null, 2) + "\n", "utf8");
22855
23300
  outputJson({
22856
23301
  emitted: true,
@@ -22886,13 +23331,13 @@ async function readStdin() {
22886
23331
  return Buffer.concat(chunks).toString("utf8");
22887
23332
  }
22888
23333
  function readUrl(source) {
22889
- return new Promise((resolve5, reject) => {
23334
+ return new Promise((resolve6, reject) => {
22890
23335
  const getter = source.startsWith("https://") ? httpsGet : httpGet;
22891
23336
  const request = getter(source, (response) => {
22892
23337
  const status = response.statusCode ?? 0;
22893
23338
  if (status >= 300 && status < 400 && response.headers.location) {
22894
23339
  response.resume();
22895
- readUrl(new URL(response.headers.location, source).toString()).then(resolve5, reject);
23340
+ readUrl(new URL(response.headers.location, source).toString()).then(resolve6, reject);
22896
23341
  return;
22897
23342
  }
22898
23343
  if (status < 200 || status >= 300) {
@@ -22908,7 +23353,7 @@ function readUrl(source) {
22908
23353
  response.on("data", (chunk) => {
22909
23354
  chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
22910
23355
  });
22911
- response.on("end", () => resolve5(Buffer.concat(chunks).toString("utf8")));
23356
+ response.on("end", () => resolve6(Buffer.concat(chunks).toString("utf8")));
22912
23357
  });
22913
23358
  request.on("error", reject);
22914
23359
  });
@@ -23148,7 +23593,7 @@ function permissionGrantReason(context, permissions) {
23148
23593
  return `${context} Requested: ${summary}${more}.`;
23149
23594
  }
23150
23595
  function execCapturedCommand(command) {
23151
- return new Promise((resolve5, reject) => {
23596
+ return new Promise((resolve6, reject) => {
23152
23597
  const child = spawn(process.execPath, [process.argv[1], ...command.argv], {
23153
23598
  cwd: command.cwd,
23154
23599
  env: process.env,
@@ -23167,7 +23612,7 @@ function execCapturedCommand(command) {
23167
23612
  if (code3 && code3 !== 0) {
23168
23613
  process.exitCode = code3;
23169
23614
  }
23170
- resolve5();
23615
+ resolve6();
23171
23616
  });
23172
23617
  });
23173
23618
  }
@@ -23196,8 +23641,8 @@ async function confirmPermissionRequest(permissions) {
23196
23641
  input: process.stdin,
23197
23642
  output: process.stderr
23198
23643
  });
23199
- const answer = await new Promise((resolve5) => {
23200
- 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);
23201
23646
  });
23202
23647
  rl.close();
23203
23648
  if (!/^y(es)?$/i.test(answer.trim())) {
@@ -24042,7 +24487,7 @@ init_errors();
24042
24487
  init_sdk();
24043
24488
  init_theme();
24044
24489
  import { readFile as readFile6, writeFile as writeFile3 } from "fs/promises";
24045
- import { resolve } from "path";
24490
+ import { resolve as resolve2 } from "path";
24046
24491
  function registerDuckdbCommand(program) {
24047
24492
  const duckdb = program.command("duckdb").description("DuckDB database operations");
24048
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) => {
@@ -24154,7 +24599,7 @@ ${rowCount} row${rowCount === 1 ? "" : "s"} returned`) + "\n");
24154
24599
  }
24155
24600
  const blob = result.data;
24156
24601
  const buffer = Buffer.from(await blob.arrayBuffer());
24157
- const outputPath = resolve(options.output);
24602
+ const outputPath = resolve2(options.output);
24158
24603
  await writeFile3(outputPath, buffer);
24159
24604
  outputJson({
24160
24605
  file: outputPath,
@@ -24170,7 +24615,7 @@ ${rowCount} row${rowCount === 1 ? "" : "s"} returned`) + "\n");
24170
24615
  const globalOpts = cmd.optsWithGlobals();
24171
24616
  const ctx = await ProfileManager.resolveContext(globalOpts);
24172
24617
  const node = await ensureAuthenticated(ctx);
24173
- const filePath = resolve(file);
24618
+ const filePath = resolve2(file);
24174
24619
  const bytes = new Uint8Array(await readFile6(filePath));
24175
24620
  const result = await withSpinner(
24176
24621
  "Importing database...",
@@ -24312,11 +24757,11 @@ init_storage();
24312
24757
  init_errors();
24313
24758
  init_constants();
24314
24759
  import { randomUUID as randomUUID2 } from "crypto";
24315
- import { lstat as lstat3, open as open3, rename as rename2, rm as rm5 } from "fs/promises";
24316
- 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";
24317
24762
  async function validatePrivateOutput(path, label = "output") {
24318
24763
  try {
24319
- const destination = await lstat3(path);
24764
+ const destination = await lstat4(path);
24320
24765
  if (!destination.isFile()) {
24321
24766
  throw new CLIError("INVALID_ARGUMENT", `${label} "${path}" must be a regular file, not a symlink, directory, or device.`, ExitCode.USAGE_ERROR);
24322
24767
  }
@@ -24324,7 +24769,7 @@ async function validatePrivateOutput(path, label = "output") {
24324
24769
  if (error?.code !== "ENOENT") throw error;
24325
24770
  }
24326
24771
  try {
24327
- const parent = await lstat3(dirname4(path));
24772
+ const parent = await lstat4(dirname5(path));
24328
24773
  if (!parent.isDirectory()) {
24329
24774
  throw new CLIError("INVALID_ARGUMENT", `${label} "${path}" requires an existing directory parent.`, ExitCode.USAGE_ERROR);
24330
24775
  }
@@ -24335,11 +24780,11 @@ async function validatePrivateOutput(path, label = "output") {
24335
24780
  }
24336
24781
  async function writePrivateOutput(path, value, label = "output") {
24337
24782
  await validatePrivateOutput(path, label);
24338
- const parentPath = dirname4(path);
24783
+ const parentPath = dirname5(path);
24339
24784
  const temp = join9(parentPath, `.${basename2(path)}.${randomUUID2()}.tmp`);
24340
24785
  let created = false;
24341
24786
  try {
24342
- const handle = await open3(temp, "wx", PRIVATE_FILE_MODE);
24787
+ const handle = await open4(temp, "wx", PRIVATE_FILE_MODE);
24343
24788
  created = true;
24344
24789
  try {
24345
24790
  await handle.writeFile(value);
@@ -24354,7 +24799,7 @@ async function writePrivateOutput(path, value, label = "output") {
24354
24799
  throw new CLIError("ERROR", `Could not write ${label.toLowerCase()} "${path}"${code3}.`, ExitCode.ERROR);
24355
24800
  }
24356
24801
  try {
24357
- const parent = await open3(parentPath, "r");
24802
+ const parent = await open4(parentPath, "r");
24358
24803
  try {
24359
24804
  await parent.sync();
24360
24805
  } finally {
@@ -24994,8 +25439,8 @@ Examples:
24994
25439
  try {
24995
25440
  if (isInteractive()) {
24996
25441
  const rl = createInterface3({ input: process.stdin, output: process.stderr });
24997
- const answer = await new Promise((resolve5) => {
24998
- 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);
24999
25444
  });
25000
25445
  rl.close();
25001
25446
  if (answer.toLowerCase() !== "y") {
@@ -25030,22 +25475,19 @@ function parseOperatorType(raw) {
25030
25475
  }
25031
25476
 
25032
25477
  // src/commands/replica.ts
25033
- init_sdk();
25034
- init_replication_registry();
25035
25478
  import { CLI_NAME as CLI_NAME22 } from "@tinycloud/operations/cli-name";
25036
25479
  import { randomBytes as randomBytes6 } from "crypto";
25037
- import { constants as fsConstants2 } from "fs";
25038
- 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";
25039
- 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";
25040
25483
 
25041
25484
  // src/lib/replication-report.ts
25042
- 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.";
25043
25485
  function percentile(values, fraction) {
25044
25486
  if (values.length === 0) return null;
25045
25487
  values.sort((left, right) => left - right);
25046
25488
  return values[Math.ceil(fraction * values.length) - 1] ?? values[values.length - 1];
25047
25489
  }
25048
- function createReplicationReport(since, events, replicas, partitions, warning) {
25490
+ function createReplicationReport(since, events, partitions) {
25049
25491
  const reads = events.filter((event) => event.type === "replication.read");
25050
25492
  const replicaReads = reads.filter((event) => event.source === "replica");
25051
25493
  const sourceLatency = /* @__PURE__ */ new Map();
@@ -25088,9 +25530,9 @@ function createReplicationReport(since, events, replicas, partitions, warning) {
25088
25530
  syncs,
25089
25531
  writes,
25090
25532
  divergences,
25091
- replicas,
25533
+ replicas: [],
25092
25534
  partitions,
25093
- ...warning === void 0 ? {} : { warning }
25535
+ replicasUnavailable: true
25094
25536
  };
25095
25537
  }
25096
25538
  function renderReplicationReport(report) {
@@ -25103,34 +25545,30 @@ function renderReplicationReport(report) {
25103
25545
  `Writes: ${Object.entries(report.writes).map(([key, count]) => `${key}=${count}`).join(", ") || "none"}`,
25104
25546
  `Read source/reason: ${Object.entries(report.readsBySourceReason).map(([key, count]) => `${key}=${count}`).join(", ") || "none"}`,
25105
25547
  `Divergences: ${report.totals.divergences}; showing ${report.divergences.length} of ${report.totals.divergences}`,
25106
- `Replicas: ${report.replicas.length}`
25548
+ "Replicas: unavailable (read-only report)"
25107
25549
  ];
25108
25550
  for (const divergence of report.divergences) {
25109
25551
  if (divergence.type === "replication.divergence") {
25110
25552
  lines.push(` ${divergence.space} ${divergence.key}: ${divergence.kind} stalenessMs=${divergence.stalenessMs}`);
25111
25553
  }
25112
25554
  }
25113
- for (const replica of report.replicas) {
25114
- const grant = replica.grant ? ` grant=${replica.grant.cid} parent=${replica.grant.parentCid ?? "\u2014"} expires=${replica.grant.expiresAt ?? "\u2014"}` : " grant=missing";
25115
- lines.push(` ${replica.prefix}: ${replica.state}; pending in_flight=${replica.pending.inFlight} committed=${replica.pending.committed} ambiguous=${replica.pending.ambiguous}${grant}`);
25116
- 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}`);
25117
- }
25118
25555
  for (const partition of report.partitions) {
25119
25556
  lines.push(`Partition ${partition.idHash}: host=${partition.host ?? "unknown"} space=${partition.space ?? "unknown"} pinned=${partition.pinned}`);
25120
25557
  }
25121
25558
  if (report.partitions.some((partition) => partition.pinned > 100)) lines.push("Warning: a partition holds more than 100 pinned records.");
25122
- if (report.warning) lines.push(`Warning: ${report.warning}`);
25123
25559
  return lines.join("\n");
25124
25560
  }
25125
25561
 
25126
25562
  // src/commands/replica.ts
25127
25563
  init_dist3();
25128
25564
  import {
25565
+ ProfileSymlinkError as ProfileSymlinkError3,
25129
25566
  ProfileDeletedError as ProfileDeletedError3,
25130
- profilePath as profilePath4,
25567
+ profilePath as profilePath5,
25131
25568
  readAdditionalDelegations as readAdditionalDelegations2,
25132
25569
  readSession as readSession2,
25133
- withProfileLock as withProfileLock5
25570
+ withProfileLock as withProfileLock5,
25571
+ withPinnedProfileDirectory as withPinnedProfileDirectory3
25134
25572
  } from "@tinycloud/operations/state";
25135
25573
  import { ownerSessionLacksProof as ownerSessionLacksProof4 } from "@tinycloud/operations/profile";
25136
25574
  init_profiles();
@@ -25186,9 +25624,9 @@ function toCliError(error) {
25186
25624
  const hint = error.code === ReplicaErrorCode.GRANT_MISSING && typeof error.detail?.hint === "string" ? error.detail.hint : void 0;
25187
25625
  return new CLIError(error.code, error.message, EXIT_BY_CODE[error.code], hint === void 0 ? void 0 : { hint });
25188
25626
  }
25189
- async function run(action) {
25627
+ async function run(action, needsSqlite = true) {
25190
25628
  try {
25191
- await loadSqlite();
25629
+ if (needsSqlite) await loadSqlite();
25192
25630
  await action();
25193
25631
  } catch (error) {
25194
25632
  return handleError(toCliError(error));
@@ -25201,20 +25639,25 @@ async function profileName(cmd) {
25201
25639
  const config = await ProfileManager.getConfig().catch(() => void 0);
25202
25640
  return config?.defaultProfile ?? DEFAULT_PROFILE;
25203
25641
  }
25204
- function replicasRoot(profile) {
25205
- return join10(profilePath4(profile), "replicas");
25206
- }
25207
25642
  async function replicaNames(profile) {
25208
- const names = [];
25209
- for (const name of await readdir5(replicasRoot(profile)).catch(() => [])) {
25210
- if (!NAME.test(name)) continue;
25211
- const exists = await stat3(join10(replicasRoot(profile), name, "replica.db")).then(
25212
- () => true,
25213
- () => false
25214
- );
25215
- if (exists) names.push(name);
25216
- }
25217
- 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
+ }) ?? [];
25218
25661
  }
25219
25662
  function assertName(name) {
25220
25663
  if (!NAME.test(name)) {
@@ -25222,6 +25665,67 @@ function assertName(name) {
25222
25665
  }
25223
25666
  return name;
25224
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
+ }
25225
25729
  async function existingReplicaName(profile, option) {
25226
25730
  if (option !== void 0) return assertName(option);
25227
25731
  const names = await replicaNames(profile);
@@ -25243,17 +25747,37 @@ function profileGuard(profile) {
25243
25747
  }
25244
25748
  };
25245
25749
  }
25246
- async function openReplica(profile, name) {
25247
- return SqliteReplicaStore.open(join10(replicasRoot(profile), name), { create: false, guard: profileGuard(profile) });
25248
- }
25249
- async function createReplica(profile, config) {
25250
- const guard = profileGuard(profile);
25251
- return guard(async () => {
25252
- const store = await SqliteReplicaStore.open(join10(replicasRoot(profile), config.name), { create: true, guard });
25253
- if (await store.open() === null) await store.init(config);
25254
- 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
+ }
25255
25758
  });
25256
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
+ }
25257
25781
  async function storedGrants(profile) {
25258
25782
  const authorizations = [];
25259
25783
  const session = await readSession2(profile).catch(() => null);
@@ -25383,9 +25907,9 @@ function describeStatus(status) {
25383
25907
  ].join("\n");
25384
25908
  }
25385
25909
  async function writeFileAtomic(path, bytes) {
25386
- const target = resolve2(path);
25387
- const temp = join10(dirname5(target), `.${basename3(target)}.${randomBytes6(6).toString("hex")}.tmp`);
25388
- 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);
25389
25913
  try {
25390
25914
  await handle.writeFile(bytes);
25391
25915
  await handle.sync();
@@ -25410,14 +25934,86 @@ function replicaSecretsWarning(space, prefix, secretsAllowed) {
25410
25934
  if (!secretsAllowed || !requiresSecretsOptIn(space, prefix)) return void 0;
25411
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.`;
25412
25936
  }
25413
- async function readReplicationEvents(profile, sinceMs) {
25414
- 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) {
25415
26014
  const events = [];
25416
26015
  for (const name of ["events.jsonl.1", "events.jsonl"]) {
25417
- const data = await readFile9(join10(root, name), "utf8").catch((error) => {
25418
- if (error.code === "ENOENT") return "";
25419
- throw error;
25420
- });
26016
+ const data = await readReportFile(profile, root, name) ?? "";
25421
26017
  for (const line of data.split("\n")) {
25422
26018
  if (!line) continue;
25423
26019
  try {
@@ -25429,27 +26025,26 @@ async function readReplicationEvents(profile, sinceMs) {
25429
26025
  }
25430
26026
  return events;
25431
26027
  }
25432
- async function replicationPartitions(profile) {
25433
- const root = join10(profilePath4(profile), "replication");
25434
- const entries = await readdir5(root, { withFileTypes: true }).catch((error) => {
25435
- if (error.code === "ENOENT") return [];
25436
- throw error;
25437
- });
26028
+ async function replicationPartitions(profile, root) {
26029
+ const entries = await readdir5(root, { withFileTypes: true });
25438
26030
  const partitions = [];
25439
26031
  for (const entry of entries) {
25440
- if (!entry.isDirectory() || !/^[a-z2-7]{26}$/.test(entry.name)) continue;
25441
- const directory = join10(root, entry.name);
25442
- const info = await lstat4(directory);
25443
- if (info.isSymbolicLink() || !info.isDirectory()) continue;
25444
- const identity = await readFile9(join10(directory, "identity.json"), "utf8").then((value) => JSON.parse(value)).catch(() => null);
25445
- const pending = await readFile9(join10(directory, "pending.json"), "utf8").then((value) => JSON.parse(value)).catch(() => null);
25446
- const pinned = pending?.records?.filter((record2) => record2.state === "in_flight" || record2.state === "ambiguous").length ?? 0;
25447
- partitions.push({
25448
- idHash: entry.name,
25449
- host: typeof identity?.host === "string" ? identity.host : null,
25450
- space: typeof identity?.space === "string" ? identity.space : null,
25451
- 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
+ };
25452
26045
  });
26046
+ if (!partition) throw localPathChanged(profile);
26047
+ partitions.push(partition);
25453
26048
  }
25454
26049
  return partitions.sort((left, right) => left.idHash.localeCompare(right.idHash));
25455
26050
  }
@@ -25468,52 +26063,41 @@ SECRETS_OPT_IN_REQUIRED; 3 grant missing; 4 key absent, deleted, content missing
25468
26063
  or coverage incomplete; 5 grant expired, revoked or not yet valid; 6 network;
25469
26064
  7 node, protocol or integrity error; 10 storage full.`
25470
26065
  );
25471
- 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(
25472
26067
  (options, cmd) => run(async () => {
25473
- const globals = cmd.optsWithGlobals();
25474
- const context = await ProfileManager.resolveContext(globals);
25475
- const node = context.replication ? await ensureAuthenticated(context) : void 0;
25476
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
+ }
25477
26075
  let durationMs;
25478
26076
  try {
25479
26077
  durationMs = parseDuration(options.since);
25480
26078
  } catch (error) {
25481
26079
  throw new CLIError("USAGE_ERROR", error instanceof Error ? error.message : String(error), ExitCode.USAGE_ERROR);
25482
26080
  }
25483
- const control = node?.replication ?? replicationForProfile(profile);
25484
- let cleared;
25485
- let purged;
25486
- let warning;
25487
- if (options.clearPending || options.purge) {
25488
- if (!control) throw new CLIError("REPLICATION_UNAVAILABLE", "No replication runtime is registered for this profile.", ExitCode.ERROR);
25489
- warning = CLEAR_PENDING_WARNING;
25490
- process.stderr.write(`Warning: ${warning}
25491
- `);
25492
- if (options.clearPending) cleared = await control.clearPending();
25493
- if (options.purge) purged = await control.purge();
25494
- }
25495
- const sinceMs = Date.now() - durationMs;
25496
- const [events, partitions, replicas] = await Promise.all([
25497
- readReplicationEvents(profile, sinceMs),
25498
- replicationPartitions(profile),
25499
- node?.replication ? node.replication.status() : control ? control.status() : Promise.resolve([])
25500
- ]);
25501
- const report = createReplicationReport(options.since, events, replicas, partitions, warning);
25502
- if (shouldOutputJson()) {
25503
- outputJson({
25504
- ...report,
25505
- ...cleared === void 0 ? {} : { cleared },
25506
- ...purged === void 0 ? {} : { purged }
25507
- });
25508
- } else {
25509
- process.stdout.write(`${renderReplicationReport(report)}
25510
- `);
25511
- if (cleared !== void 0) process.stdout.write(`Cleared ${cleared} pending write record(s).
25512
- `);
25513
- 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)}
25514
26099
  `);
25515
- }
25516
- })
26100
+ }, false)
25517
26101
  );
25518
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(
25519
26103
  (options, cmd) => run(async () => {
@@ -25525,87 +26109,98 @@ or coverage incomplete; 5 grant expired, revoked or not yet valid; 6 network;
25525
26109
  throw new CLIError("INVALID_REPLICATION_PREFIX", error instanceof Error ? error.message : String(error), ExitCode.USAGE_ERROR);
25526
26110
  }
25527
26111
  }
25528
- const globals = cmd.optsWithGlobals();
25529
- const config = await ProfileManager.getProfile(profile);
25530
- const deviceDid = config.sessionDid ?? config.did;
25531
- const jwk = await ProfileManager.getKey(profile);
25532
- if (jwk === null) {
25533
- throw new CLIError("AUTH_REQUIRED", `Profile "${profile}" has no device key.`, ExitCode.AUTH_REQUIRED);
25534
- }
25535
- if (options.limit !== void 0 && (!Number.isInteger(options.limit) || options.limit < 1 || options.limit > 1e3)) {
25536
- throw new CLIError("USAGE_ERROR", "--limit must be an integer from 1 through 1000.", ExitCode.USAGE_ERROR);
25537
- }
25538
- const existing = await replicaNames(profile);
25539
- 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);
25540
- const grants = await storedGrants(profile);
25541
- let store;
25542
- if (existing.includes(name)) {
25543
- store = await openReplica(profile, name);
25544
- const state2 = await store.open();
25545
- const differs = syncOptionsDiffer(state2.config, {
25546
- ...options.space === void 0 ? {} : { space: options.space },
25547
- ...options.prefix === void 0 ? {} : { prefix: options.prefix },
25548
- ...globals.host === void 0 ? {} : { host: globals.host }
25549
- });
25550
- if (differs !== void 0) {
25551
- await store.close();
25552
- throw new ReplicaError(
25553
- ReplicaErrorCode.CONFIG_MISMATCH,
25554
- `Replica ${name} already follows ${differs}. Use another --replica name, or ${CLI_NAME22} replica reset --replica ${name} --purge first.`
25555
- );
25556
- }
25557
- } else {
25558
- const prefix = options.prefix;
25559
- if (prefix === void 0) {
25560
- throw new CLIError("USAGE_ERROR", "The first sync of a replica needs --prefix (and a device grant covering it).", ExitCode.USAGE_ERROR);
25561
- }
25562
- const space = resolveSpace(options.space, prefix, grants);
25563
- if (requiresSecretsOptIn(space, prefix) && options.allowSecrets !== true) {
25564
- throw new ReplicaError(
25565
- ReplicaErrorCode.SECRETS_OPT_IN_REQUIRED,
25566
- `Replicating ${space}/kv/${prefix} copies encrypted secret material to this disk; pass --allow-secrets to opt in.`
25567
- );
25568
- }
25569
- const host = (globals.host ?? process.env.TC_HOST ?? config.host)?.replace(/\/+$/, "");
25570
- if (!host) throw new CLIError("USAGE_ERROR", "Pass --host: the profile has no host to pin.", ExitCode.USAGE_ERROR);
25571
- chooseGrant(grants, deviceDid, space, prefix);
25572
- store = await createReplica(profile, {
25573
- name,
25574
- replicaId: randomBytes6(16).toString("hex"),
25575
- host,
25576
- space,
25577
- prefix,
25578
- deviceDid,
25579
- allowSecrets: options.allowSecrets === true,
25580
- localReadPolicy: "whileGrantValid",
25581
- retentionGrantCid: null
25582
- });
25583
- }
25584
- try {
25585
- const state2 = await store.open();
25586
- const { space, prefix, host } = state2.config;
25587
- if (requiresSecretsOptIn(space, prefix) && !state2.config.allowSecrets && options.allowSecrets !== true) {
25588
- throw new ReplicaError(ReplicaErrorCode.SECRETS_OPT_IN_REQUIRED, `Replicating ${space}/kv/${prefix} needs --allow-secrets.`);
25589
- }
25590
- let grant;
25591
- try {
25592
- grant = chooseGrant(grants, state2.config.deviceDid, space, prefix);
25593
- } catch (error) {
25594
- if (!isReplicaError(error, ReplicaErrorCode.GRANT_MISSING) || (state2.grant ?? state2.pendingGrant) === null) throw error;
25595
- }
25596
- if (grant !== void 0) await store.installGrant(grant);
25597
- if (options.retentionGrant !== void 0 && options.retentionGrant !== state2.config.retentionGrantCid) {
25598
- await store.setRetentionGrant(options.retentionGrant === "" ? null : options.retentionGrant);
25599
- }
25600
- const current = await store.open();
25601
- const transportFor = await grantTransports({ host, space, deviceDid: current.config.deviceDid, jwk });
25602
- 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);
25603
26200
  if (warning !== void 0) process.stderr.write(`Warning: ${warning}
25604
26201
  `);
25605
- const report = await new Replica({ store, transportFor }).sync(options.limit === void 0 ? {} : { limit: options.limit });
25606
- const status = await store.status();
25607
26202
  if (shouldOutputJson()) {
25608
- outputJson({ replica: current.config.name, sync: report, status, ...warning === void 0 ? {} : { warning } });
26203
+ outputJson({ replica: replica2, sync: report, status, ...warning === void 0 ? {} : { warning } });
25609
26204
  } else {
25610
26205
  process.stdout.write(
25611
26206
  `${theme.success("\u2713")} Synced ${report.changes} change(s) in ${report.pages} page(s), fetched ${report.fetched} value(s).
@@ -25613,40 +26208,39 @@ ${describeStatus(status)}
25613
26208
  `
25614
26209
  );
25615
26210
  }
25616
- } finally {
25617
- await store.close();
25618
- }
26211
+ });
25619
26212
  })
25620
26213
  );
25621
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(
25622
26215
  (key, options, cmd) => run(async () => {
25623
26216
  const profile = await profileName(cmd);
25624
- const store = await openReplica(profile, await existingReplicaName(profile, options.replica));
25625
- let result;
25626
- try {
25627
- result = await new Replica({ store }).get(key, { verify: options.verify !== false });
25628
- } finally {
25629
- await store.close();
25630
- }
25631
- if (result.status !== "present") {
25632
- const failure = READ_FAILURE[result.status];
25633
- throw new CLIError(failure.code, `Key ${JSON.stringify(key)} ${failure.what}.`, failure.exit);
25634
- }
25635
- if (options.output) {
25636
- await writeFileAtomic(options.output, result.value);
25637
- outputJson({ key, written: options.output, etag: result.etag, meta: result.meta });
25638
- return;
25639
- }
25640
- 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
+ }
25641
26241
  process.stdout.write(result.value);
25642
- return;
25643
- }
25644
- if (shouldOutputJson()) {
25645
- outputJson({ key, status: result.status, ...jsonValue(result.value), etag: result.etag, metadata: result.metadata, meta: result.meta });
25646
- return;
25647
- }
25648
- process.stdout.write(result.value);
25649
- process.stdout.write("\n");
26242
+ process.stdout.write("\n");
26243
+ });
25650
26244
  })
25651
26245
  );
25652
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(
@@ -25655,13 +26249,17 @@ ${describeStatus(status)}
25655
26249
  throw new CLIError("USAGE_ERROR", "--limit must be a positive integer.", ExitCode.USAGE_ERROR);
25656
26250
  }
25657
26251
  const profile = await profileName(cmd);
25658
- const store = await openReplica(profile, await existingReplicaName(profile, options.replica));
25659
- try {
25660
- const { entries, meta } = await new Replica({ store }).list({
25661
- ...prefix === void 0 ? {} : { prefix },
25662
- ...options.after === void 0 ? {} : { after: options.after },
25663
- ...options.limit === void 0 ? {} : { limit: options.limit }
25664
- });
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();
25665
26263
  if (shouldOutputJson()) {
25666
26264
  outputJson({ keys: entries, count: entries.length, prefix: prefix ?? null, meta });
25667
26265
  } else if (entries.length === 0) {
@@ -25674,50 +26272,57 @@ ${describeStatus(status)}
25674
26272
  ) + "\n"
25675
26273
  );
25676
26274
  }
25677
- } finally {
25678
- await store.close();
25679
- }
26275
+ });
25680
26276
  })
25681
26277
  );
25682
26278
  replica.command("status").description("Show one replica's sync state, authority and counts, or all of them").option("--replica <name>", "Replica name").action(
25683
26279
  (options, cmd) => run(async () => {
25684
26280
  const profile = await profileName(cmd);
25685
- const names = options.replica === void 0 ? await replicaNames(profile) : [assertName(options.replica)];
25686
- const statuses = [];
25687
- for (const name of names) {
25688
- const store = await openReplica(profile, name);
25689
- try {
25690
- statuses.push(await new Replica({ store }).status());
25691
- } finally {
25692
- 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()));
25693
26286
  }
25694
- }
25695
- if (shouldOutputJson()) {
25696
- outputJson(options.replica === void 0 ? { replicas: statuses } : statuses[0]);
25697
- } else if (statuses.length === 0) {
25698
- process.stdout.write(theme.muted(`Profile "${profile}" has no replicas.`) + "\n");
25699
- } else {
25700
- process.stdout.write(statuses.map(describeStatus).join("\n\n") + "\n");
25701
- }
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
+ });
25702
26296
  })
25703
26297
  );
25704
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(
25705
26299
  (options, cmd) => run(async () => {
25706
26300
  const profile = await profileName(cmd);
25707
- const name = await existingReplicaName(profile, options.replica);
25708
- const store = await openReplica(profile, name);
25709
- try {
26301
+ await withProfileLock5(profile, async () => {
26302
+ const name = await existingReplicaName(profile, options.replica);
25710
26303
  if (options.purge) {
25711
- const lease = await store.acquireSyncLease(6e4);
25712
- if (lease === null) throw new ReplicaError(ReplicaErrorCode.BUSY, "Another process is syncing this replica.");
25713
- 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
+ });
25714
26321
  } else {
25715
- await new Replica({ store }).reset("manual");
26322
+ await withOpenReplica(profile, name, (store) => new Replica({ store }).reset("manual"));
25716
26323
  }
25717
- } finally {
25718
- await store.close();
25719
- }
25720
- outputJson({ replica: name, reset: true, purged: options.purge === true });
26324
+ outputJson({ replica: name, reset: true, purged: options.purge === true });
26325
+ }, { requireProfile: true });
25721
26326
  })
25722
26327
  );
25723
26328
  }
@@ -25730,7 +26335,7 @@ init_errors();
25730
26335
  init_warnings();
25731
26336
  init_constants();
25732
26337
  import { CLI_NAME as CLI_NAME24 } from "@tinycloud/operations/cli-name";
25733
- import { readFile as readFile10 } from "fs/promises";
26338
+ import { readFile as readFile9 } from "fs/promises";
25734
26339
  import { join as join11 } from "path";
25735
26340
  import { homedir, hostname, userInfo } from "os";
25736
26341
  import { appSecretScope } from "@tinycloud/sdk-core";
@@ -25759,24 +26364,24 @@ init_space();
25759
26364
  // src/lib/browser.ts
25760
26365
  init_formatter();
25761
26366
  init_theme();
25762
- import open5 from "open";
26367
+ import open6 from "open";
25763
26368
  var LAUNCH_CHECK_MS = 1e3;
25764
26369
  async function tryOpenBrowser(url) {
25765
26370
  let launcher;
25766
26371
  try {
25767
- launcher = await open5(url);
26372
+ launcher = await open6(url);
25768
26373
  } catch {
25769
26374
  return false;
25770
26375
  }
25771
- return new Promise((resolve5) => {
25772
- const timer = setTimeout(() => resolve5(true), LAUNCH_CHECK_MS);
26376
+ return new Promise((resolve6) => {
26377
+ const timer = setTimeout(() => resolve6(true), LAUNCH_CHECK_MS);
25773
26378
  launcher.once("error", () => {
25774
26379
  clearTimeout(timer);
25775
- resolve5(false);
26380
+ resolve6(false);
25776
26381
  });
25777
26382
  launcher.once("exit", (code3) => {
25778
26383
  clearTimeout(timer);
25779
- resolve5(code3 === 0);
26384
+ resolve6(code3 === 0);
25780
26385
  });
25781
26386
  });
25782
26387
  }
@@ -25820,9 +26425,9 @@ function runProbeChild(request, options = {}) {
25820
26425
  const killGraceMs = options.killGraceMs ?? KILL_GRACE_MS;
25821
26426
  const spawn2 = options.spawn ?? nodeSpawn;
25822
26427
  const signal = options.signal;
25823
- return new Promise((resolve5) => {
26428
+ return new Promise((resolve6) => {
25824
26429
  if (signal?.aborted) {
25825
- resolve5({ kind: "cancelled" });
26430
+ resolve6({ kind: "cancelled" });
25826
26431
  return;
25827
26432
  }
25828
26433
  const child = spawn2(command, args, { env: process.env, stdio: ["ignore", "pipe", "pipe"] });
@@ -25858,7 +26463,7 @@ function runProbeChild(request, options = {}) {
25858
26463
  const envelope = stdout.length > 0 && stdout.length <= MAX_PROBE_LINE_BYTES ? parseStatusEnvelope(stdout.toString("utf8")) : void 0;
25859
26464
  outcome = envelope !== void 0 ? { kind: "ok", envelope } : probeFailed(stderr.toString("utf8"));
25860
26465
  }
25861
- resolve5(outcome);
26466
+ resolve6(outcome);
25862
26467
  };
25863
26468
  if (child.pid !== void 0) options.onSpawn?.(child.pid);
25864
26469
  child.stdout?.on("data", (chunk) => {
@@ -25950,15 +26555,15 @@ var TIMEOUT_RE = /^(0|\d+s|\d+m)$/;
25950
26555
  var RETRYABLE_STATUS_ERRORS = { NODE_UNREACHABLE: true, NODE_ERROR: true };
25951
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.`;
25952
26557
  function abortableSleep(ms2, signal) {
25953
- return new Promise((resolve5) => {
26558
+ return new Promise((resolve6) => {
25954
26559
  if (signal.aborted) {
25955
- resolve5();
26560
+ resolve6();
25956
26561
  return;
25957
26562
  }
25958
26563
  const done = () => {
25959
26564
  clearTimeout(timer);
25960
26565
  signal.removeEventListener("abort", done);
25961
- resolve5();
26566
+ resolve6();
25962
26567
  };
25963
26568
  const timer = setTimeout(done, ms2);
25964
26569
  signal.addEventListener("abort", done, { once: true });
@@ -26624,7 +27229,7 @@ function permissionsFromDelegation2(delegation) {
26624
27229
  }
26625
27230
  async function loadDelegationCandidates(source) {
26626
27231
  try {
26627
- const raw = JSON.parse(await readFile10(source, "utf8"));
27232
+ const raw = JSON.parse(await readFile9(source, "utf8"));
26628
27233
  return normalizeDelegationCandidates(raw, source);
26629
27234
  } catch (error) {
26630
27235
  if (error?.code !== "ENOENT") {
@@ -26644,7 +27249,7 @@ async function loadDelegationCandidates(source) {
26644
27249
  }
26645
27250
  try {
26646
27251
  const importedPath = join11(resolveProfilesDir(), source, "additional-delegations.json");
26647
- const raw = JSON.parse(await readFile10(importedPath, "utf8"));
27252
+ const raw = JSON.parse(await readFile9(importedPath, "utf8"));
26648
27253
  return normalizeDelegationCandidates(raw, source);
26649
27254
  } catch (error) {
26650
27255
  if (error?.code === "ENOENT") {
@@ -27151,7 +27756,7 @@ function registerSecretsCommand(program, openKeyAcquisition) {
27151
27756
  throw new CLIError("USAGE_ERROR", "Provide only one of: value argument, --file, or --stdin", ExitCode.USAGE_ERROR);
27152
27757
  }
27153
27758
  if (options.file) {
27154
- secretValue = await readFile10(options.file, "utf-8");
27759
+ secretValue = await readFile9(options.file, "utf-8");
27155
27760
  } else if (options.stdin) {
27156
27761
  secretValue = (await readStdin3()).toString("utf-8");
27157
27762
  } else {
@@ -32738,7 +33343,7 @@ async function importAesKey(key32, usage) {
32738
33343
  [usage]
32739
33344
  );
32740
33345
  }
32741
- async function open6(blob, key32) {
33346
+ async function open7(blob, key32) {
32742
33347
  if (blob.length < HEADER_LENGTH + NONCE_LENGTH + TAG_LENGTH) {
32743
33348
  throw new TypeError(`sealed blob too short: ${blob.length} bytes`);
32744
33349
  }
@@ -33076,7 +33681,7 @@ async function resolvePolicyShare(link3, options) {
33076
33681
  options.signal?.throwIfAborted();
33077
33682
  let envelope;
33078
33683
  try {
33079
- 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));
33080
33685
  const value = JSON.parse(encoded);
33081
33686
  if (canonicalize2(value) !== encoded) throw new Error("non-canonical envelope");
33082
33687
  envelope = shareEnvelopeV3Schema.parse(value);
@@ -35668,9 +36273,9 @@ function receiveJson(result, path) {
35668
36273
 
35669
36274
  // src/share/io.ts
35670
36275
  import { constants } from "fs";
35671
- 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";
35672
36277
  import { randomBytes as randomBytes7 } from "crypto";
35673
- 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";
35674
36279
  init_errors();
35675
36280
  var MAX_SHARE_STDIN_BYTES = 100 * 1024 * 1024;
35676
36281
  var MAX_SHARE_URL_BYTES = 64 * 1024;
@@ -35707,26 +36312,26 @@ function shareFilename(value) {
35707
36312
  }
35708
36313
  }
35709
36314
  function shareInputFilename(input, name) {
35710
- return shareFilename(name ?? (input === "-" ? "stdin.md" : basename4(resolve3(input))));
36315
+ return shareFilename(name ?? (input === "-" ? "stdin.md" : basename4(resolve4(input))));
35711
36316
  }
35712
36317
  async function readShareInput(input, name, limit = MAX_SHARE_STDIN_BYTES) {
35713
36318
  const filename = shareInputFilename(input, name);
35714
36319
  if (input === "-") return { bytes: await readBoundedStdin(limit), filename };
35715
- const path = resolve3(input);
35716
- const info = await stat4(path);
36320
+ const path = resolve4(input);
36321
+ const info = await stat5(path);
35717
36322
  if (!info.isFile() || info.size > limit) throw new Error("MAX_BYTES_EXCEEDED");
35718
- const bytes = new Uint8Array(await readFile11(path));
36323
+ const bytes = new Uint8Array(await readFile10(path));
35719
36324
  if (bytes.byteLength > limit) throw new Error("MAX_BYTES_EXCEEDED");
35720
36325
  return { bytes, filename };
35721
36326
  }
35722
36327
  async function assertDirectory(path) {
35723
- const absolute = resolve3(path);
36328
+ const absolute = resolve4(path);
35724
36329
  const segments = absolute.split(sep2).filter(Boolean);
35725
36330
  let current = absolute.startsWith(sep2) ? sep2 : "";
35726
36331
  for (const segment of segments) {
35727
36332
  current = current === sep2 ? join12(current, segment) : join12(current, segment);
35728
36333
  try {
35729
- const info = await lstat5(current);
36334
+ const info = await lstat6(current);
35730
36335
  if (info.isSymbolicLink()) {
35731
36336
  const canonical = await realpath2(current);
35732
36337
  if (current !== "/tmp" && current !== "/var") throw new Error("OUTPUT_EXISTS");
@@ -35734,27 +36339,27 @@ async function assertDirectory(path) {
35734
36339
  } else if (!info.isDirectory()) throw new Error("OUTPUT_EXISTS");
35735
36340
  } catch (error) {
35736
36341
  if (error.code !== "ENOENT") throw error;
35737
- await mkdir4(current, { mode: 448 });
35738
- const created = await lstat5(current);
36342
+ await mkdir5(current, { mode: 448 });
36343
+ const created = await lstat6(current);
35739
36344
  if (created.isSymbolicLink() || !created.isDirectory()) throw new Error("OUTPUT_EXISTS");
35740
36345
  }
35741
36346
  }
35742
36347
  }
35743
36348
  async function writeShareOutput(directory, filename, bytes, force) {
35744
- const outputDirectory = resolve3(directory);
36349
+ const outputDirectory = resolve4(directory);
35745
36350
  await assertDirectory(outputDirectory);
35746
36351
  const safeName = safeFilename(filename);
35747
- 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));
35748
36353
  const stableDirectory = await realpath2(outputDirectory);
35749
36354
  const outputPath = join12(stableDirectory, safeName);
35750
36355
  const directoryIdentity = await directoryHandle.stat();
35751
36356
  const assertStableDirectory = async () => {
35752
- const current = await stat4(stableDirectory);
36357
+ const current = await stat5(stableDirectory);
35753
36358
  if (current.dev !== directoryIdentity.dev || current.ino !== directoryIdentity.ino) throw new Error("OUTPUT_EXISTS");
35754
36359
  };
35755
36360
  await assertStableDirectory();
35756
36361
  const stagingDirectory = await mkdtemp(join12(stableDirectory, ".tinycloud-share-stage-"));
35757
- const stagingInfo = await lstat5(stagingDirectory);
36362
+ const stagingInfo = await lstat6(stagingDirectory);
35758
36363
  if (!stagingInfo.isDirectory() || (stagingInfo.mode & 511) !== 448) throw new Error("OUTPUT_EXISTS");
35759
36364
  const stagingPath = join12(stagingDirectory, `.tinycloud-share-${randomBytes7(16).toString("hex")}.tmp`);
35760
36365
  let temporaryPath;
@@ -35762,13 +36367,13 @@ async function writeShareOutput(directory, filename, bytes, force) {
35762
36367
  try {
35763
36368
  await assertStableDirectory();
35764
36369
  try {
35765
- const existing = await lstat5(outputPath);
36370
+ const existing = await lstat6(outputPath);
35766
36371
  if (existing.isSymbolicLink()) throw new Error("UNSAFE_FILENAME");
35767
36372
  } catch (error) {
35768
36373
  if (error.code !== "ENOENT") throw error;
35769
36374
  }
35770
36375
  temporaryPath = stagingPath;
35771
- 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);
35772
36377
  await handle.writeFile(bytes);
35773
36378
  await handle.close();
35774
36379
  handle = void 0;
@@ -36218,8 +36823,8 @@ init_theme();
36218
36823
  import { CLI_NAME as CLI_NAME26 } from "@tinycloud/operations/cli-name";
36219
36824
  import { ACCOUNT_REGISTRY_SPACE as ACCOUNT_REGISTRY_SPACE2 } from "@tinycloud/node-sdk";
36220
36825
  import { randomBytes as randomBytes8 } from "crypto";
36221
- import { mkdir as mkdir5, writeFile as writeFile4 } from "fs/promises";
36222
- import { dirname as dirname6 } from "path";
36826
+ import { mkdir as mkdir6, writeFile as writeFile4 } from "fs/promises";
36827
+ import { dirname as dirname7 } from "path";
36223
36828
  function didWithoutFragment2(did) {
36224
36829
  const fragment = did.indexOf("#");
36225
36830
  return fragment === -1 ? did : did.slice(0, fragment);
@@ -36375,7 +36980,7 @@ it directly with \`${CLI_NAME26} space host <name>\` (no request needed).
36375
36980
  }
36376
36981
  async function emitHostRequestArtifact(artifact, emitOption) {
36377
36982
  if (typeof emitOption === "string" && emitOption.length > 0) {
36378
- await mkdir5(dirname6(emitOption), { recursive: true });
36983
+ await mkdir6(dirname7(emitOption), { recursive: true });
36379
36984
  await writeFile4(emitOption, JSON.stringify(artifact, null, 2) + "\n", "utf8");
36380
36985
  outputJson({
36381
36986
  emitted: true,
@@ -36401,7 +37006,7 @@ init_host();
36401
37006
  init_theme();
36402
37007
  import { CLI_NAME as CLI_NAME27 } from "@tinycloud/operations/cli-name";
36403
37008
  import { writeFile as writeFile5 } from "fs/promises";
36404
- import { resolve as resolve4 } from "path";
37009
+ import { resolve as resolve5 } from "path";
36405
37010
  async function dbHandle(node, dbName, spaceInput, profileName2) {
36406
37011
  const spaceUri = await resolveSpaceUri(spaceInput, profileName2);
36407
37012
  const sql = spaceUri ? node.sqlForSpace(spaceUri) : node.sql;
@@ -36543,7 +37148,7 @@ Output:
36543
37148
  }
36544
37149
  const blob = result.data;
36545
37150
  const buffer = Buffer.from(await blob.arrayBuffer());
36546
- const outputPath = resolve4(options.output);
37151
+ const outputPath = resolve5(options.output);
36547
37152
  await writeFile5(outputPath, buffer);
36548
37153
  outputJson({
36549
37154
  file: outputPath,
@@ -37099,7 +37704,7 @@ function registerUpgradeCommand(program) {
37099
37704
  }
37100
37705
 
37101
37706
  // src/commands/vault.ts
37102
- import { readFile as readFile12 } from "fs/promises";
37707
+ import { readFile as readFile11 } from "fs/promises";
37103
37708
  init_profiles();
37104
37709
  init_formatter();
37105
37710
  init_errors();
@@ -37161,7 +37766,7 @@ function registerVaultCommand(program) {
37161
37766
  throw new CLIError("USAGE_ERROR", "Provide only one of: value argument, --file, or --stdin", ExitCode.USAGE_ERROR);
37162
37767
  }
37163
37768
  if (options.file) {
37164
- putValue = new Uint8Array(await readFile12(options.file));
37769
+ putValue = new Uint8Array(await readFile11(options.file));
37165
37770
  } else if (options.stdin) {
37166
37771
  putValue = new Uint8Array(await readStdin4());
37167
37772
  } else {
@@ -37276,7 +37881,7 @@ function registerVaultCommand(program) {
37276
37881
  }
37277
37882
 
37278
37883
  // src/commands/vars.ts
37279
- import { readFile as readFile13 } from "fs/promises";
37884
+ import { readFile as readFile12 } from "fs/promises";
37280
37885
  init_profiles();
37281
37886
  init_formatter();
37282
37887
  init_errors();
@@ -37381,7 +37986,7 @@ function registerVarsCommand(program) {
37381
37986
  throw new CLIError("USAGE_ERROR", "Provide only one of: value argument, --file, or --stdin", ExitCode.USAGE_ERROR);
37382
37987
  }
37383
37988
  if (options.file) {
37384
- varValue = await readFile13(options.file, "utf-8");
37989
+ varValue = await readFile12(options.file, "utf-8");
37385
37990
  } else if (options.stdin) {
37386
37991
  varValue = (await readStdin5()).toString("utf-8");
37387
37992
  } else {