@tinycloud/cli 1.1.0-beta.6 → 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,20 +5673,56 @@ import {
5659
5673
  profileStoreMetadataPath,
5660
5674
  readAdditionalDelegations,
5661
5675
  readAuthRequests,
5662
- upsertProfileRecord,
5676
+ refuseWriteToDeletedProfile as refuseWriteToDeletedProfile2,
5677
+ updateProfileStore,
5663
5678
  withProfileLock as withProfileLock2,
5664
5679
  writeJsonAtomic
5665
5680
  } from "@tinycloud/operations/state";
5666
5681
  async function loadAdditionalDelegations(profile) {
5667
5682
  return readAdditionalDelegations(profile);
5668
5683
  }
5669
- async function replayAdditionalDelegations(node, profile) {
5684
+ async function replayAdditionalDelegations(node, profile, options) {
5685
+ const {
5686
+ operationSpaceResolver,
5687
+ prepareStoredDelegationReplay,
5688
+ replayStoredDelegation,
5689
+ storedDelegationKind
5690
+ } = await import("@tinycloud/operations/delegation-binding");
5691
+ const activator = node;
5692
+ const migrated = await prepareStoredDelegationReplay(profile, activator, {
5693
+ host: options.host,
5694
+ migrate: options.migrate
5695
+ });
5696
+ const resolveSpace = operationSpaceResolver(node, options.ownerSpace);
5670
5697
  const entries = await loadAdditionalDelegations(profile);
5671
- for (const entry of entries) {
5698
+ for (const stored of entries) {
5699
+ const kind = storedDelegationKind(stored);
5700
+ if (kind === "compact" || kind === "signed-login") {
5701
+ const installed = await replayStoredDelegation(activator, stored, {
5702
+ host: options.host,
5703
+ migrated,
5704
+ resolveSpace
5705
+ });
5706
+ if (installed === void 0 && process.env.TC_DEBUG_REPLAY === "1") {
5707
+ process.stderr.write("[replay] skipping a stored delegation refused by validation or its request binding\n");
5708
+ }
5709
+ continue;
5710
+ }
5711
+ if (kind === "refused") {
5712
+ if (process.env.TC_DEBUG_REPLAY === "1") {
5713
+ process.stderr.write("[replay] skipping a malformed stored record or one bound to a request it cannot be held to\n");
5714
+ }
5715
+ continue;
5716
+ }
5717
+ const entry = stored;
5672
5718
  const expiry = entry.delegation.expiry instanceof Date ? entry.delegation.expiry : new Date(entry.delegation.expiry);
5673
5719
  if (expiry.getTime() <= Date.now()) continue;
5674
5720
  try {
5675
- await node.useRuntimeDelegation({ ...entry.delegation, expiry });
5721
+ await node.useRuntimeDelegation({
5722
+ ...entry.delegation,
5723
+ delegationHeader: { Authorization: entry.delegation.delegationHeader.Authorization },
5724
+ expiry
5725
+ });
5676
5726
  } catch (err) {
5677
5727
  if (process.env.TC_DEBUG_REPLAY === "1") {
5678
5728
  process.stderr.write(`[replay] skipping ${entry.delegation.cid}: ${err.message}
@@ -5749,6 +5799,7 @@ async function createSDKInstance(ctx, options) {
5749
5799
  host: ctx.host,
5750
5800
  privateKey: effectivePrivateKey
5751
5801
  });
5802
+ let restoredOwnSession2 = false;
5752
5803
  if (session && session.delegationHeader && session.delegationCid && session.spaceId) {
5753
5804
  await node2.restoreSession({
5754
5805
  delegationHeader: session.delegationHeader,
@@ -5761,16 +5812,22 @@ async function createSDKInstance(ctx, options) {
5761
5812
  siwe: session.siwe,
5762
5813
  signature: session.signature
5763
5814
  });
5815
+ restoredOwnSession2 = true;
5764
5816
  } else {
5765
5817
  await node2.signIn();
5766
5818
  }
5767
- await replayAdditionalDelegations(node2, ctx.profile);
5819
+ await replayAdditionalDelegations(node2, ctx.profile, {
5820
+ host: ctx.host,
5821
+ ownerSpace: profile.spaceId,
5822
+ migrate: restoredOwnSession2 && options?.privateKey === void 0
5823
+ });
5768
5824
  return node2;
5769
5825
  }
5770
5826
  const node = new TinyCloudNode({
5771
5827
  host: ctx.host,
5772
5828
  privateKey: options?.privateKey
5773
5829
  });
5830
+ let restoredOwnSession = false;
5774
5831
  if (options?.privateKey) {
5775
5832
  await node.signIn();
5776
5833
  } else if (session && session.delegationHeader && session.delegationCid && session.spaceId) {
@@ -5785,8 +5842,13 @@ async function createSDKInstance(ctx, options) {
5785
5842
  siwe: session.siwe,
5786
5843
  signature: session.signature
5787
5844
  });
5845
+ restoredOwnSession = true;
5788
5846
  }
5789
- await replayAdditionalDelegations(node, ctx.profile);
5847
+ await replayAdditionalDelegations(node, ctx.profile, {
5848
+ host: ctx.host,
5849
+ ownerSpace: profile?.spaceId,
5850
+ migrate: restoredOwnSession
5851
+ });
5790
5852
  return node;
5791
5853
  }
5792
5854
  async function bootstrapDelegatedSession(ctx, delegation) {
@@ -5838,13 +5900,13 @@ async function bootstrapDelegatedSession(ctx, delegation) {
5838
5900
  throw error;
5839
5901
  });
5840
5902
  const session = await ProfileManager.getSession(ctx.profile);
5841
- 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)) {
5842
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.`;
5843
5905
  }
5844
5906
  return restoreBeforeBootstrap(ctx.profile, written.previousProfile);
5845
5907
  }, { timeoutMs: PROFILE_COMMIT_LOCK_TIMEOUT_MS }).catch((error) => (
5846
5908
  // The lock or a read failed: the import's error stays the one reported.
5847
- `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\`.`
5848
5910
  ));
5849
5911
  throw annotate(cause, note);
5850
5912
  };
@@ -5860,15 +5922,27 @@ async function restoreBeforeBootstrap(profileName, previousProfile) {
5860
5922
  const failures = [];
5861
5923
  for (const write of [
5862
5924
  () => ProfileManager.clearSession(profileName),
5863
- () => 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
+ })
5864
5933
  ]) {
5865
5934
  await write().catch((error) => {
5866
- failures.push(error instanceof Error ? error.message : String(error));
5935
+ failures.push(failureName(error));
5867
5936
  });
5868
5937
  }
5869
5938
  if (failures.length === 0) return void 0;
5870
5939
  return `Rolling back the provisional session of profile "${profileName}" failed too (${failures.join("; ")}); check \`tc --profile ${profileName} context\`.`;
5871
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
+ }
5872
5946
  function annotate(error, note) {
5873
5947
  if (note === void 0) return error;
5874
5948
  const cause = wrapError(error);
@@ -5880,7 +5954,7 @@ async function ensureAuthenticated(ctx, options) {
5880
5954
  }
5881
5955
  const profile = await ProfileManager.getProfile(ctx.profile).catch(() => null);
5882
5956
  if (profile?.authMethod === "local" && profile.privateKey) {
5883
- return createSDKInstance(ctx, { privateKey: profile.privateKey });
5957
+ return createSDKInstance(ctx);
5884
5958
  }
5885
5959
  const session = await ProfileManager.getSession(ctx.profile);
5886
5960
  if (!session) {
@@ -5892,6 +5966,7 @@ async function ensureAuthenticated(ctx, options) {
5892
5966
  }
5893
5967
  return createSDKInstance(ctx, options);
5894
5968
  }
5969
+ var BOOTSTRAP_PROFILE_FIELDS;
5895
5970
  var init_sdk = __esm({
5896
5971
  "src/lib/sdk.ts"() {
5897
5972
  "use strict";
@@ -5900,6 +5975,7 @@ var init_sdk = __esm({
5900
5975
  init_errors();
5901
5976
  init_constants();
5902
5977
  init_permissions();
5978
+ BOOTSTRAP_PROFILE_FIELDS = ["sessionDid", "spaceId"];
5903
5979
  }
5904
5980
  });
5905
5981