@tinycloud/cli 1.1.0-beta.7 → 1.1.0-beta.8

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/dist/index.js CHANGED
@@ -110,7 +110,7 @@ var init_formatter = __esm({
110
110
  // src/output/errors.ts
111
111
  import { readFileSync, readdirSync } from "fs";
112
112
  import { join } from "path";
113
- import { ProfileLockTimeoutError } from "@tinycloud/operations/state";
113
+ import { ProfileDeletedError, ProfileLockTimeoutError } from "@tinycloud/operations/state";
114
114
  function setActiveProfileName(name) {
115
115
  activeProfileName = name;
116
116
  }
@@ -136,6 +136,9 @@ function wrapError(error) {
136
136
  { hint: "Wait for the other command to finish and retry. A crashed process's lock is reclaimed automatically after 30 s." }
137
137
  );
138
138
  }
139
+ if (error instanceof ProfileDeletedError) {
140
+ return new CLIError("PROFILE_NOT_FOUND", message);
141
+ }
139
142
  if (message.includes("Not signed in") || message.includes("AUTH_EXPIRED") || message.includes("Session expired")) {
140
143
  return new CLIError("AUTH_REQUIRED", message, ExitCode.AUTH_REQUIRED);
141
144
  }
@@ -541,6 +544,8 @@ import { join as join3 } from "path";
541
544
  import {
542
545
  profilePath,
543
546
  readSession,
547
+ recordProfileDeletion,
548
+ refuseWriteToDeletedProfile,
544
549
  removeSession,
545
550
  withProfileLock,
546
551
  writeSession
@@ -655,10 +660,12 @@ var init_profiles = __esm({
655
660
  * removed while holding the profile lock, so another writer's critical
656
661
  * section never sees them vanish midway: session and key first, settings
657
662
  * last, so a crash midway never leaves a session or key without its
658
- * profile. `.lock` itself is left to the lock's release; the then-empty
659
- * directory is removed afterwards unless another writer took the lock (or
660
- * wrote) meanwhile. A profile directory that is a symlink is unlinked, its
661
- * target left alone.
663
+ * profile. The deletion is then recorded, so a store write that waited for
664
+ * the lock meanwhile refuses rather than recreating a profile with only a
665
+ * session in it. `.lock` itself is left to the lock's release; the
666
+ * then-empty directory is removed afterwards unless another writer took
667
+ * the lock (or wrote) meanwhile. A profile directory that is a symlink is
668
+ * unlinked, its target left alone.
662
669
  * Throws if the name is not one path segment or names the default profile.
663
670
  */
664
671
  static async deleteProfile(name) {
@@ -679,11 +686,12 @@ var init_profiles = __esm({
679
686
  `Cannot delete the default profile "${name}". Change the default first with \`tc profile default <other>\`.`
680
687
  );
681
688
  }
682
- const isLink = await lstat(profileDir).then((stats) => stats.isSymbolicLink(), (error) => {
683
- if (error.code === "ENOENT") return false;
689
+ const kind = await lstat(profileDir).then((stats) => stats.isSymbolicLink() ? "link" : "present", (error) => {
690
+ if (error.code === "ENOENT") return "missing";
684
691
  throw error;
685
692
  });
686
- if (isLink) {
693
+ if (kind === "missing") return;
694
+ if (kind === "link") {
687
695
  await rm2(profileDir, { force: true });
688
696
  return;
689
697
  }
@@ -693,6 +701,7 @@ var init_profiles = __esm({
693
701
  if (entry !== ".lock" && entry !== "profile.json") await rm2(join3(profileDir, entry), { recursive: true, force: true });
694
702
  }
695
703
  await rm2(join3(profileDir, "profile.json"), { force: true });
704
+ await recordProfileDeletion(name);
696
705
  });
697
706
  await rmdir(profileDir).catch((error) => {
698
707
  if (error.code !== "ENOENT" && error.code !== "ENOTEMPTY" && error.code !== "EEXIST") throw error;
@@ -705,9 +714,14 @@ var init_profiles = __esm({
705
714
  static async getKey(name) {
706
715
  return readJson(join3(PROFILES_DIR, name, "key.json"));
707
716
  }
708
- /** Saves a JWK key under the profile lock (0600, in an owner-only profile directory). */
717
+ /**
718
+ * Saves a JWK key under the profile lock (0600, in an owner-only profile
719
+ * directory). Refused (PROFILE_NOT_FOUND) if the profile was deleted while
720
+ * this waited for the lock, rather than leaving a key-only profile.
721
+ */
709
722
  static async setKey(name, jwk) {
710
723
  await _ProfileManager.withLock(name, async () => {
724
+ await refuseWriteToDeletedProfile(name);
711
725
  await writeJson(join3(await _ProfileManager.ensureProfileDir(name), "key.json"), jwk);
712
726
  });
713
727
  }
@@ -5659,6 +5673,7 @@ import {
5659
5673
  profileStoreMetadataPath,
5660
5674
  readAdditionalDelegations,
5661
5675
  readAuthRequests,
5676
+ refuseWriteToDeletedProfile as refuseWriteToDeletedProfile2,
5662
5677
  updateProfileStore,
5663
5678
  withProfileLock as withProfileLock2,
5664
5679
  writeJsonAtomic
@@ -5885,13 +5900,13 @@ async function bootstrapDelegatedSession(ctx, delegation) {
5885
5900
  throw error;
5886
5901
  });
5887
5902
  const session = await ProfileManager.getSession(ctx.profile);
5888
- if (JSON.stringify(profile) !== JSON.stringify(written.profile) || JSON.stringify(session) !== JSON.stringify(written.session)) {
5903
+ if (profile === null || BOOTSTRAP_PROFILE_FIELDS.some((field) => profile[field] !== written.profile[field]) || JSON.stringify(session) !== JSON.stringify(written.session)) {
5889
5904
  return `Profile "${ctx.profile}" changed while the import was pending (another login, logout or profile update), so its newer state was kept and the provisional session was not rolled back.`;
5890
5905
  }
5891
5906
  return restoreBeforeBootstrap(ctx.profile, written.previousProfile);
5892
5907
  }, { timeoutMs: PROFILE_COMMIT_LOCK_TIMEOUT_MS }).catch((error) => (
5893
5908
  // The lock or a read failed: the import's error stays the one reported.
5894
- `Rolling back the provisional session of profile "${ctx.profile}" could not run (${error instanceof Error ? error.message : String(error)}); check \`tc --profile ${ctx.profile} context\`.`
5909
+ `Rolling back the provisional session of profile "${ctx.profile}" could not run (${failureName(error)}); check \`tc --profile ${ctx.profile} context\`.`
5895
5910
  ));
5896
5911
  throw annotate(cause, note);
5897
5912
  };
@@ -5907,15 +5922,27 @@ async function restoreBeforeBootstrap(profileName, previousProfile) {
5907
5922
  const failures = [];
5908
5923
  for (const write of [
5909
5924
  () => ProfileManager.clearSession(profileName),
5910
- () => ProfileManager.setProfile(profileName, previousProfile)
5925
+ () => ProfileManager.updateProfile(profileName, (current) => {
5926
+ const restored = { ...current };
5927
+ for (const field of BOOTSTRAP_PROFILE_FIELDS) {
5928
+ if (previousProfile[field] === void 0) delete restored[field];
5929
+ else restored[field] = previousProfile[field];
5930
+ }
5931
+ return restored;
5932
+ })
5911
5933
  ]) {
5912
5934
  await write().catch((error) => {
5913
- failures.push(error instanceof Error ? error.message : String(error));
5935
+ failures.push(failureName(error));
5914
5936
  });
5915
5937
  }
5916
5938
  if (failures.length === 0) return void 0;
5917
5939
  return `Rolling back the provisional session of profile "${profileName}" failed too (${failures.join("; ")}); check \`tc --profile ${profileName} context\`.`;
5918
5940
  }
5941
+ function failureName(error) {
5942
+ if (error instanceof SyntaxError) return "a profile file is not valid JSON";
5943
+ if (typeof error === "object" && error !== null && "code" in error && typeof error.code === "string") return error.code;
5944
+ return error instanceof Error ? error.name : "unknown error";
5945
+ }
5919
5946
  function annotate(error, note) {
5920
5947
  if (note === void 0) return error;
5921
5948
  const cause = wrapError(error);
@@ -5939,6 +5966,7 @@ async function ensureAuthenticated(ctx, options) {
5939
5966
  }
5940
5967
  return createSDKInstance(ctx, options);
5941
5968
  }
5969
+ var BOOTSTRAP_PROFILE_FIELDS;
5942
5970
  var init_sdk = __esm({
5943
5971
  "src/lib/sdk.ts"() {
5944
5972
  "use strict";
@@ -5947,6 +5975,7 @@ var init_sdk = __esm({
5947
5975
  init_errors();
5948
5976
  init_constants();
5949
5977
  init_permissions();
5978
+ BOOTSTRAP_PROFILE_FIELDS = ["sessionDid", "spaceId"];
5950
5979
  }
5951
5980
  });
5952
5981