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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -171,9 +171,16 @@ whole space. Whole-space replication requires `kv-sync-root` support and
171
171
  `--replication-allow-secrets`, because it includes `vault/`. Replication needs
172
172
  unrestricted `get` and explicit `sync` on that folder or broader; an exact-key
173
173
  grant never qualifies. Bare replica prefixes are rejected; use `notes/`.
174
- Use `tc replica report` for event aggregates and pinned keys; `--clear-pending`
175
- prints a warning because later commits can be read as stale until a subsequent
176
- sync. `auth logout` removes both legacy replicas and the flag-owned
174
+ Use `tc replica report` for read-only aggregates of persisted replication events
175
+ and identity partitions. Live replica status is unavailable there. Report
176
+ `--purge` and `--clear-pending` refuse with exit 2 `REPORT_READ_ONLY`.
177
+ `tc replica reset --replica <name> --purge` is a partial alternative for a
178
+ named local replica only.
179
+ Flag-owned purge and pending clearing have no equivalent yet (TC-1309).
180
+ On macOS and Windows, `replica reset --purge` and `auth logout` with a legacy
181
+ `replicas/` tree refuse local removal rather than delete through an unpinned
182
+ path; leave that tree intact and retry on a supported platform.
183
+ `auth logout` removes both legacy replicas and the flag-owned
177
184
  `replication/` root; `auth logout --keep-replicas` keeps both. B-int release
178
185
  acceptance must verify the OpenKey `tc auth request --grant` fallback against
179
186
  a real OpenKey using a test account and no production writes: approve the
package/dist/index.js CHANGED
@@ -382,7 +382,7 @@ var init_storage = __esm({
382
382
  // src/output/errors.ts
383
383
  import { readFileSync, readdirSync } from "fs";
384
384
  import { join } from "path";
385
- import { ProfileDeletedError, ProfileLockTimeoutError } from "@tinycloud/operations/state";
385
+ import { ProfileDeletedError, ProfileLockTimeoutError, ProfileSymlinkError } from "@tinycloud/operations/state";
386
386
  import { isSafeRecovery } from "@tinycloud/operations/recovery";
387
387
  import { CLI_NAME as CLI_NAME3 } from "@tinycloud/operations/cli-name";
388
388
  import {
@@ -421,6 +421,9 @@ function wrapError(error) {
421
421
  if (error instanceof ProfileDeletedError) {
422
422
  return new CLIError("PROFILE_NOT_FOUND", message);
423
423
  }
424
+ if (error instanceof ProfileSymlinkError) {
425
+ return new CLIError("PROFILE_SYMLINK", message, ExitCode.ERROR);
426
+ }
424
427
  if (verdict === void 0 && (message.includes("Not signed in") || message.includes("AUTH_EXPIRED") || message.includes("Session expired"))) {
425
428
  return new CLIError("AUTH_REQUIRED", message, ExitCode.AUTH_REQUIRED);
426
429
  }
@@ -5649,12 +5652,14 @@ import { resolveSecretsOwner } from "@tinycloud/operations/secret-request";
5649
5652
  import {
5650
5653
  additionalDelegationsPath as sharedAdditionalDelegationsPath,
5651
5654
  authRequestsPath as sharedAuthRequestsPath,
5655
+ profilePath,
5652
5656
  profileStoreMetadataPath,
5653
5657
  readAdditionalDelegations,
5654
5658
  readAuthRequests,
5655
5659
  refuseWriteToDeletedProfile,
5656
5660
  updateProfileStore,
5657
5661
  withProfileLock,
5662
+ withPinnedProfileDirectory,
5658
5663
  writeJsonAtomic
5659
5664
  } from "@tinycloud/operations/state";
5660
5665
  async function replayAdditionalDelegations(node, profile, options) {
@@ -5669,7 +5674,6 @@ async function replayAdditionalDelegations(node, profile, options) {
5669
5674
  var init_permissions = __esm({
5670
5675
  "src/lib/permissions.ts"() {
5671
5676
  "use strict";
5672
- init_constants();
5673
5677
  init_storage2();
5674
5678
  init_profiles();
5675
5679
  init_errors();
@@ -7796,7 +7800,7 @@ var init_dist = __esm({
7796
7800
  // src/lib/replication-config.ts
7797
7801
  import { join as join5 } from "path";
7798
7802
  import { CLI_NAME as CLI_NAME8 } from "@tinycloud/operations/cli-name";
7799
- import { profilePath, withProfileLock as withProfileLock2 } from "@tinycloud/operations/state";
7803
+ import { profilePath as profilePath2, withProfileLock as withProfileLock2 } from "@tinycloud/operations/state";
7800
7804
  import { sqliteReplicaStorage } from "@tinycloud/node-sdk";
7801
7805
  function createCliReplicationConfig(profile, settings, output, enabled) {
7802
7806
  if (!enabled || !settings || settings.prefixes.length === 0) return void 0;
@@ -7814,7 +7818,7 @@ function createCliReplicationConfig(profile, settings, output, enabled) {
7814
7818
  );
7815
7819
  }
7816
7820
  }
7817
- const root = profilePath(profile);
7821
+ const root = profilePath2(profile);
7818
7822
  return {
7819
7823
  enabled: true,
7820
7824
  prefixes: settings.prefixes,
@@ -8300,17 +8304,21 @@ var init_host = __esm({
8300
8304
  });
8301
8305
 
8302
8306
  // src/config/profiles.ts
8303
- import { chmod as chmod2, lstat, readdir as readdir2, rm as rm2, rmdir } from "fs/promises";
8307
+ import { chmod as chmod2, lstat, readdir as readdir2, rm as rm2 } from "fs/promises";
8304
8308
  import { CLI_NAME as CLI_NAME11 } from "@tinycloud/operations/cli-name";
8305
8309
  import { join as join6 } from "path";
8306
8310
  import {
8307
- profilePath as profilePath2,
8311
+ assertRealProfileDirectory,
8312
+ ProfileSymlinkError as ProfileSymlinkError2,
8313
+ namedProfilePath,
8314
+ profilePath as profilePath3,
8308
8315
  readSession,
8309
8316
  recordProfileDeletion,
8310
8317
  refuseWriteToDeletedProfile as refuseWriteToDeletedProfile2,
8311
8318
  removeSession,
8312
8319
  withProfileLock as withProfileLock3,
8313
- writeSession
8320
+ writeSession,
8321
+ withPinnedProfileDirectory as withPinnedProfileDirectory2
8314
8322
  } from "@tinycloud/operations/state";
8315
8323
  var ProfileManager;
8316
8324
  var init_profiles = __esm({
@@ -8352,8 +8360,13 @@ var init_profiles = __esm({
8352
8360
  }
8353
8361
  /** Owner-only profile directory (0700), created or tightened before any write into it. */
8354
8362
  static async ensureProfileDir(name) {
8363
+ const profileDir = profilePath3(name);
8364
+ if (profileDir !== join6(PROFILES_DIR, name)) {
8365
+ await chmod2(profileDir, PRIVATE_DIR_MODE);
8366
+ return profileDir;
8367
+ }
8355
8368
  await _ProfileManager.ensureConfigDir();
8356
- const profileDir = join6(PROFILES_DIR, name);
8369
+ await assertRealProfileDirectory(name);
8357
8370
  await ensureDir(profileDir);
8358
8371
  await chmod2(profileDir, PRIVATE_DIR_MODE);
8359
8372
  return profileDir;
@@ -8382,8 +8395,7 @@ var init_profiles = __esm({
8382
8395
  * Throws CLIError if the profile doesn't exist.
8383
8396
  */
8384
8397
  static async getProfile(name) {
8385
- const profilePath3 = join6(PROFILES_DIR, name, "profile.json");
8386
- const profile = await readJson(profilePath3);
8398
+ const profile = await withPinnedProfileDirectory2(name, (directory) => directory ? readJson(join6(directory, "profile.json")) : Promise.resolve(null));
8387
8399
  if (!profile) {
8388
8400
  throw new CLIError(
8389
8401
  "PROFILE_NOT_FOUND",
@@ -8403,13 +8415,13 @@ var init_profiles = __esm({
8403
8415
  }
8404
8416
  /** Removes profile.json under the profile lock (rollback of a profile a failed login created). */
8405
8417
  static async removeProfileConfig(name) {
8406
- await _ProfileManager.withLock(name, () => rm2(join6(PROFILES_DIR, name, "profile.json"), { force: true }));
8418
+ await _ProfileManager.withLock(name, () => rm2(join6(profilePath3(name), "profile.json"), { force: true }));
8407
8419
  }
8408
8420
  /**
8409
8421
  * Returns true if a profile directory exists.
8410
8422
  */
8411
8423
  static async profileExists(name) {
8412
- return fileExists(join6(PROFILES_DIR, name, "profile.json"));
8424
+ return withPinnedProfileDirectory2(name, (directory) => directory ? fileExists(join6(directory, "profile.json")) : Promise.resolve(false));
8413
8425
  }
8414
8426
  /**
8415
8427
  * Returns an array of profile directory names.
@@ -8433,7 +8445,7 @@ var init_profiles = __esm({
8433
8445
  static async deleteProfile(name) {
8434
8446
  let profileDir;
8435
8447
  try {
8436
- profileDir = profilePath2(name);
8448
+ profileDir = namedProfilePath(name);
8437
8449
  } catch {
8438
8450
  throw new CLIError(
8439
8451
  "INVALID_PROFILE_NAME",
@@ -8458,23 +8470,21 @@ var init_profiles = __esm({
8458
8470
  return;
8459
8471
  }
8460
8472
  await _ProfileManager.withLock(name, async () => {
8461
- for (const file of ["session.json", "key.json"]) await rm2(join6(profileDir, file), { force: true });
8462
- for (const entry of await readdir2(profileDir)) {
8463
- if (entry !== ".lock" && entry !== "profile.json") await rm2(join6(profileDir, entry), { recursive: true, force: true });
8473
+ const directory = profilePath3(name);
8474
+ for (const file of ["session.json", "key.json"]) await rm2(join6(directory, file), { force: true });
8475
+ for (const entry of await readdir2(directory)) {
8476
+ if (entry !== ".lock" && entry !== "profile.json") await rm2(join6(directory, entry), { recursive: true, force: true });
8464
8477
  }
8465
- await rm2(join6(profileDir, "profile.json"), { force: true });
8478
+ await rm2(join6(directory, "profile.json"), { force: true });
8466
8479
  await recordProfileDeletion(name);
8467
8480
  });
8468
- await rmdir(profileDir).catch((error) => {
8469
- if (error.code !== "ENOENT" && error.code !== "ENOTEMPTY" && error.code !== "EEXIST") throw error;
8470
- });
8471
8481
  }
8472
8482
  // ── Key management ──────────────────────────────────────────────────
8473
8483
  /**
8474
8484
  * Returns the parsed JWK for a profile, or null if no key exists.
8475
8485
  */
8476
8486
  static async getKey(name) {
8477
- return readJson(join6(PROFILES_DIR, name, "key.json"));
8487
+ return withPinnedProfileDirectory2(name, (directory) => directory ? readJson(join6(directory, "key.json")) : Promise.resolve(null));
8478
8488
  }
8479
8489
  /**
8480
8490
  * Saves a JWK key under the profile lock (0600, in an owner-only profile
@@ -8489,7 +8499,7 @@ var init_profiles = __esm({
8489
8499
  }
8490
8500
  /** Removes key.json under the profile lock (rollback of a key a failed login created). */
8491
8501
  static async removeKey(name) {
8492
- await _ProfileManager.withLock(name, () => rm2(join6(PROFILES_DIR, name, "key.json"), { force: true }));
8502
+ await _ProfileManager.withLock(name, () => rm2(join6(profilePath3(name), "key.json"), { force: true }));
8493
8503
  }
8494
8504
  // ── Session management ──────────────────────────────────────────────
8495
8505
  /**
@@ -8516,10 +8526,13 @@ var init_profiles = __esm({
8516
8526
  * created or tightened to 0700.
8517
8527
  */
8518
8528
  static async getCacheDir(name) {
8519
- const cacheDir = join6(await _ProfileManager.ensureProfileDir(name), "cache");
8520
- await ensureDir(cacheDir);
8521
- await chmod2(cacheDir, PRIVATE_DIR_MODE);
8522
- return cacheDir;
8529
+ const alreadyPinned = profilePath3(name) !== namedProfilePath(name);
8530
+ return _ProfileManager.withLock(name, async () => {
8531
+ const cacheDir = join6(await _ProfileManager.ensureProfileDir(name), "cache");
8532
+ await ensureDir(cacheDir);
8533
+ await chmod2(cacheDir, PRIVATE_DIR_MODE);
8534
+ return alreadyPinned ? cacheDir : join6(namedProfilePath(name), "cache");
8535
+ });
8523
8536
  }
8524
8537
  // ── Resolution helpers ──────────────────────────────────────────────
8525
8538
  /**
@@ -8543,7 +8556,8 @@ var init_profiles = __esm({
8543
8556
  try {
8544
8557
  profileConfig = await _ProfileManager.getProfile(profile);
8545
8558
  profileHost = profileConfig.host;
8546
- } catch {
8559
+ } catch (error) {
8560
+ if (error instanceof ProfileSymlinkError2) throw error;
8547
8561
  }
8548
8562
  const envReplication = process.env.TC_REPLICATION?.toLowerCase();
8549
8563
  const envEnabled = envReplication === "1" || envReplication === "true" || envReplication === "on";