@tinycloud/cli 1.0.1-beta.0 → 1.0.1-beta.2

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.
@@ -37,7 +37,7 @@ import {
37
37
  tinycloudConfigPath,
38
38
  tinycloudHomePath
39
39
  } from "@tinycloud/operations/state";
40
- var CONFIG_DIR, PROFILES_DIR, CONFIG_FILE, DEFAULT_HOST, DEFAULT_OPENKEY_HOST, DEFAULT_OPENKEY_DEVICE_API_HOST, DEFAULT_SHARE_ORIGIN, DEFAULT_PROFILE, DEFAULT_CHAIN_ID, ExitCode;
40
+ var CONFIG_DIR, PROFILES_DIR, CONFIG_FILE, DEFAULT_HOST, DEFAULT_OPENKEY_HOST, DEFAULT_OPENKEY_DEVICE_API_HOST, DEFAULT_SHARE_ORIGIN, DEFAULT_PROFILE, DEFAULT_CHAIN_ID, PROFILE_COMMIT_LOCK_TIMEOUT_MS, ExitCode;
41
41
  var init_constants = __esm({
42
42
  "src/config/constants.ts"() {
43
43
  "use strict";
@@ -50,6 +50,7 @@ var init_constants = __esm({
50
50
  DEFAULT_SHARE_ORIGIN = "https://share.tinycloud.xyz";
51
51
  DEFAULT_PROFILE = "default";
52
52
  DEFAULT_CHAIN_ID = 1;
53
+ PROFILE_COMMIT_LOCK_TIMEOUT_MS = 45e3;
53
54
  ExitCode = {
54
55
  SUCCESS: 0,
55
56
  ERROR: 1,
@@ -104,9 +105,6 @@ async function fileExists(filePath) {
104
105
  async function ensureDir(dirPath) {
105
106
  await mkdir(dirPath, { recursive: true, mode: PRIVATE_DIR_MODE });
106
107
  }
107
- async function removeDir(dirPath) {
108
- await rm(dirPath, { recursive: true, force: true });
109
- }
110
108
  async function listDirs(dirPath) {
111
109
  try {
112
110
  const entries = await readdir(dirPath, { withFileTypes: true });
@@ -641,9 +639,10 @@ var init_host = __esm({
641
639
  });
642
640
 
643
641
  // src/config/profiles.ts
644
- import { chmod, rm as rm2 } from "fs/promises";
642
+ import { chmod, lstat, readdir as readdir2, rm as rm2, rmdir } from "fs/promises";
645
643
  import { join as join3 } from "path";
646
644
  import {
645
+ profilePath,
647
646
  readSession,
648
647
  removeSession,
649
648
  withProfileLock,
@@ -719,8 +718,8 @@ var init_profiles = __esm({
719
718
  * Throws CLIError if the profile doesn't exist.
720
719
  */
721
720
  static async getProfile(name) {
722
- const profilePath = join3(PROFILES_DIR, name, "profile.json");
723
- const profile = await readJson(profilePath);
721
+ const profilePath2 = join3(PROFILES_DIR, name, "profile.json");
722
+ const profile = await readJson(profilePath2);
724
723
  if (!profile) {
725
724
  throw new CLIError(
726
725
  "PROFILE_NOT_FOUND",
@@ -755,10 +754,27 @@ var init_profiles = __esm({
755
754
  return listDirs(PROFILES_DIR);
756
755
  }
757
756
  /**
758
- * Deletes a profile directory.
759
- * Throws if trying to delete the current default profile.
757
+ * Deletes a profile. Its key, session, settings, stores and cache are
758
+ * removed while holding the profile lock, so another writer's critical
759
+ * section never sees them vanish midway: session and key first, settings
760
+ * last, so a crash midway never leaves a session or key without its
761
+ * profile. `.lock` itself is left to the lock's release; the then-empty
762
+ * directory is removed afterwards unless another writer took the lock (or
763
+ * wrote) meanwhile. A profile directory that is a symlink is unlinked, its
764
+ * target left alone.
765
+ * Throws if the name is not one path segment or names the default profile.
760
766
  */
761
767
  static async deleteProfile(name) {
768
+ let profileDir;
769
+ try {
770
+ profileDir = profilePath(name);
771
+ } catch {
772
+ throw new CLIError(
773
+ "INVALID_PROFILE_NAME",
774
+ `Invalid profile name "${name}": a profile name is one path segment (no "/", "\\", "." or "..").`,
775
+ ExitCode.USAGE_ERROR
776
+ );
777
+ }
762
778
  const config = await _ProfileManager.getConfig();
763
779
  if (config.defaultProfile === name) {
764
780
  throw new CLIError(
@@ -766,8 +782,24 @@ var init_profiles = __esm({
766
782
  `Cannot delete the default profile "${name}". Change the default first with \`tc profile default <other>\`.`
767
783
  );
768
784
  }
769
- const profileDir = join3(PROFILES_DIR, name);
770
- await removeDir(profileDir);
785
+ const isLink = await lstat(profileDir).then((stats) => stats.isSymbolicLink(), (error) => {
786
+ if (error.code === "ENOENT") return false;
787
+ throw error;
788
+ });
789
+ if (isLink) {
790
+ await rm2(profileDir, { force: true });
791
+ return;
792
+ }
793
+ await _ProfileManager.withLock(name, async () => {
794
+ for (const file of ["session.json", "key.json"]) await rm2(join3(profileDir, file), { force: true });
795
+ for (const entry of await readdir2(profileDir)) {
796
+ if (entry !== ".lock" && entry !== "profile.json") await rm2(join3(profileDir, entry), { recursive: true, force: true });
797
+ }
798
+ await rm2(join3(profileDir, "profile.json"), { force: true });
799
+ });
800
+ await rmdir(profileDir).catch((error) => {
801
+ if (error.code !== "ENOENT" && error.code !== "ENOTEMPTY" && error.code !== "EEXIST") throw error;
802
+ });
771
803
  }
772
804
  // ── Key management ──────────────────────────────────────────────────
773
805
  /**
@@ -7250,7 +7282,7 @@ var KVService = class extends BaseService {
7250
7282
  return err(
7251
7283
  storageQuotaExceededError(
7252
7284
  "kv",
7253
- `Storage quota exceeded for key "${key}": ${errorText}`,
7285
+ `Storage quota exceeded for key ${JSON.stringify(key)}: ${errorText}`,
7254
7286
  {
7255
7287
  status: response.status,
7256
7288
  ...quotaInfo ? { usedBytes: quotaInfo.usedBytes, limitBytes: quotaInfo.limitBytes } : {}
@@ -7263,7 +7295,7 @@ var KVService = class extends BaseService {
7263
7295
  return err(
7264
7296
  storageLimitReachedError(
7265
7297
  "kv",
7266
- `Storage limit reached for key "${key}": ${errorText}`,
7298
+ `Storage limit reached for key ${JSON.stringify(key)}: ${errorText}`,
7267
7299
  {
7268
7300
  status: response.status,
7269
7301
  ...quotaInfo ? { usedBytes: quotaInfo.usedBytes, limitBytes: quotaInfo.limitBytes } : {}
@@ -7549,12 +7581,11 @@ var KVService = class extends BaseService {
7549
7581
  if (!response.ok) {
7550
7582
  const errorText = await response.text();
7551
7583
  if (response.status === 401 || response.status === 403) {
7552
- const { resource, action: requiredAction } = parseAuthError(errorText);
7553
- return err(authUnauthorizedError("kv", errorText, {
7554
- status: response.status,
7555
- ...requiredAction && { requiredAction },
7556
- ...resource && { resource }
7557
- }));
7584
+ return this.authorizationFailure(
7585
+ `Failed to batch read ${keys.length} key(s)`,
7586
+ response,
7587
+ errorText
7588
+ );
7558
7589
  }
7559
7590
  if (response.status === 413) {
7560
7591
  return err(serviceError(
@@ -7653,23 +7684,39 @@ var KVService = class extends BaseService {
7653
7684
  } catch {
7654
7685
  }
7655
7686
  if (response.status === 401 || response.status === 403) {
7656
- const { resource, action } = parseAuthError(errorText);
7657
- return err(authUnauthorizedError("kv", errorText, {
7658
- status: response.status,
7659
- ...action && { requiredAction: action },
7660
- ...resource && { resource }
7661
- }));
7687
+ return this.authorizationFailure(
7688
+ `Failed to create signed read URL for key ${JSON.stringify(key)}`,
7689
+ response,
7690
+ errorText
7691
+ );
7662
7692
  }
7663
7693
  const code2 = response.status === 400 ? ErrorCodes.INVALID_INPUT : ErrorCodes.NETWORK_ERROR;
7664
7694
  return err(
7665
7695
  serviceError(
7666
7696
  code2,
7667
- `Failed to create signed read URL for key "${key}": ${response.status} - ${errorText}`,
7697
+ `Failed to create signed read URL for key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
7668
7698
  "kv",
7669
7699
  { meta: { status: response.status, statusText: response.statusText } }
7670
7700
  )
7671
7701
  );
7672
7702
  }
7703
+ /**
7704
+ * AUTH_UNAUTHORIZED for a 401/403 response. The message keeps the HTTP
7705
+ * status and the server text (`<context>: <status> - <text>`, the same
7706
+ * shape as every other KV failure) so callers that rethrow only the message
7707
+ * still see the status; `meta.status` carries it for typed callers.
7708
+ * Resource/ability hints are parsed from the raw body, never the message.
7709
+ */
7710
+ authorizationFailure(context, response, errorText, extraMeta = {}) {
7711
+ const { resource, action } = parseAuthError(errorText);
7712
+ const detail = errorText.trim().length > 0 ? errorText : response.statusText || "authorization failed";
7713
+ return err(authUnauthorizedError("kv", `${context}: ${response.status} - ${detail}`, {
7714
+ status: response.status,
7715
+ ...action && { requiredAction: action },
7716
+ ...resource && { resource },
7717
+ ...extraMeta
7718
+ }));
7719
+ }
7673
7720
  normalizeSignedReadUrlResponse(data) {
7674
7721
  if (!data || typeof data !== "object") {
7675
7722
  return void 0;
@@ -7712,14 +7759,13 @@ var KVService = class extends BaseService {
7712
7759
  if (!response.ok) {
7713
7760
  if (response.status === 401 || response.status === 403) {
7714
7761
  const errorText2 = await response.text();
7715
- const { resource, action } = parseAuthError(errorText2);
7716
7762
  const permissionHint = parsePermissionHintFromErrorText(errorText2);
7717
- return err(authUnauthorizedError("kv", errorText2, {
7718
- status: response.status,
7719
- ...action && { requiredAction: action },
7720
- ...resource && { resource },
7721
- ...permissionHint === void 0 ? {} : { permissionHint }
7722
- }));
7763
+ return this.authorizationFailure(
7764
+ `Failed to get key ${JSON.stringify(key)}`,
7765
+ response,
7766
+ errorText2,
7767
+ permissionHint === void 0 ? {} : { permissionHint }
7768
+ );
7723
7769
  }
7724
7770
  if (response.status === 404) {
7725
7771
  return this.classifyNotFound(response, key);
@@ -7728,7 +7774,7 @@ var KVService = class extends BaseService {
7728
7774
  if (response.status === 413) {
7729
7775
  return err(serviceError(
7730
7776
  ErrorCodes.KV_RESPONSE_TOO_LARGE,
7731
- `KV value at key "${key}" exceeds the requested response limit`,
7777
+ `KV value at key ${JSON.stringify(key)} exceeds the requested response limit`,
7732
7778
  "kv",
7733
7779
  { meta: { status: response.status, statusText: response.statusText } }
7734
7780
  ));
@@ -7736,7 +7782,7 @@ var KVService = class extends BaseService {
7736
7782
  return err(
7737
7783
  serviceError(
7738
7784
  ErrorCodes.NETWORK_ERROR,
7739
- `Failed to get key "${key}": ${response.status} - ${errorText}`,
7785
+ `Failed to get key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
7740
7786
  "kv",
7741
7787
  { meta: { status: response.status, statusText: response.statusText } }
7742
7788
  )
@@ -7792,21 +7838,18 @@ var KVService = class extends BaseService {
7792
7838
  }
7793
7839
  );
7794
7840
  if (response.status === 401 || response.status === 403) {
7795
- const errorText = await response.text();
7796
- const message = errorText.trim().length > 0 ? errorText : `Failed to put key "${key}": ${response.status} - ${response.statusText || "authorization failed"}`;
7797
- const { resource, action: requiredAction } = parseAuthError(errorText);
7798
- return err(authUnauthorizedError("kv", message, {
7799
- status: response.status,
7800
- ...requiredAction && { requiredAction },
7801
- ...resource && { resource }
7802
- }));
7841
+ return this.authorizationFailure(
7842
+ `Failed to put key ${JSON.stringify(key)}`,
7843
+ response,
7844
+ await response.text()
7845
+ );
7803
7846
  }
7804
7847
  if (!response.ok) {
7805
7848
  const errorText = await response.text();
7806
7849
  if (response.status === 412) {
7807
7850
  return err(serviceError(
7808
7851
  ErrorCodes.KV_PRECONDITION_FAILED,
7809
- `KV precondition failed for key "${key}"`,
7852
+ `KV precondition failed for key ${JSON.stringify(key)}`,
7810
7853
  "kv",
7811
7854
  { meta: { status: response.status, statusText: response.statusText } }
7812
7855
  ));
@@ -7814,7 +7857,7 @@ var KVService = class extends BaseService {
7814
7857
  if (response.status === 503 && (options?.ifMatch !== void 0 || options?.ifNoneMatch !== void 0)) {
7815
7858
  return err(serviceError(
7816
7859
  ErrorCodes.KV_CONFLICT,
7817
- `Concurrent KV update conflicted for key "${key}"`,
7860
+ `Concurrent KV update conflicted for key ${JSON.stringify(key)}`,
7818
7861
  "kv",
7819
7862
  { meta: { status: response.status, statusText: response.statusText } }
7820
7863
  ));
@@ -7830,7 +7873,7 @@ var KVService = class extends BaseService {
7830
7873
  return err(
7831
7874
  serviceError(
7832
7875
  ErrorCodes.KV_WRITE_FAILED,
7833
- `Failed to put key "${key}": ${response.status} - ${errorText}`,
7876
+ `Failed to put key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
7834
7877
  "kv",
7835
7878
  { meta: { status: response.status, statusText: response.statusText } }
7836
7879
  )
@@ -7924,12 +7967,11 @@ var KVService = class extends BaseService {
7924
7967
  );
7925
7968
  }
7926
7969
  if (response.status === 401 || response.status === 403) {
7927
- const { resource, action } = parseAuthError(errorText);
7928
- return err(authUnauthorizedError("kv", errorText, {
7929
- status: response.status,
7930
- ...action && { requiredAction: action },
7931
- ...resource && { resource }
7932
- }));
7970
+ return this.authorizationFailure(
7971
+ `Failed to batch put ${items.length} key(s)`,
7972
+ response,
7973
+ errorText
7974
+ );
7933
7975
  }
7934
7976
  const quotaError = this.handleQuotaErrorResponse(
7935
7977
  response,
@@ -8050,13 +8092,11 @@ var KVService = class extends BaseService {
8050
8092
  );
8051
8093
  if (!response.ok) {
8052
8094
  if (response.status === 401) {
8053
- const errorText2 = await response.text();
8054
- const { resource, action } = parseAuthError(errorText2);
8055
- return err(authUnauthorizedError("kv", errorText2, {
8056
- status: response.status,
8057
- ...action && { requiredAction: action },
8058
- ...resource && { resource }
8059
- }));
8095
+ return this.authorizationFailure(
8096
+ "Failed to list keys",
8097
+ response,
8098
+ await response.text()
8099
+ );
8060
8100
  }
8061
8101
  const errorText = await response.text();
8062
8102
  return err(
@@ -8105,13 +8145,11 @@ var KVService = class extends BaseService {
8105
8145
  );
8106
8146
  if (!response.ok) {
8107
8147
  if (response.status === 401) {
8108
- const errorText2 = await response.text();
8109
- const { resource, action } = parseAuthError(errorText2);
8110
- return err(authUnauthorizedError("kv", errorText2, {
8111
- status: response.status,
8112
- ...action && { requiredAction: action },
8113
- ...resource && { resource }
8114
- }));
8148
+ return this.authorizationFailure(
8149
+ `Failed to delete key ${JSON.stringify(key)}`,
8150
+ response,
8151
+ await response.text()
8152
+ );
8115
8153
  }
8116
8154
  if (response.status === 404) {
8117
8155
  return this.classifyNotFound(response, key);
@@ -8120,7 +8158,7 @@ var KVService = class extends BaseService {
8120
8158
  if (response.status === 412) {
8121
8159
  return err(serviceError(
8122
8160
  ErrorCodes.KV_PRECONDITION_FAILED,
8123
- `KV precondition failed for key "${key}"`,
8161
+ `KV precondition failed for key ${JSON.stringify(key)}`,
8124
8162
  "kv",
8125
8163
  { meta: { status: response.status, statusText: response.statusText } }
8126
8164
  ));
@@ -8128,7 +8166,7 @@ var KVService = class extends BaseService {
8128
8166
  if (response.status === 503 && options?.ifMatch !== void 0) {
8129
8167
  return err(serviceError(
8130
8168
  ErrorCodes.KV_CONFLICT,
8131
- `Concurrent KV delete conflicted for key "${key}"`,
8169
+ `Concurrent KV delete conflicted for key ${JSON.stringify(key)}`,
8132
8170
  "kv",
8133
8171
  { meta: { status: response.status, statusText: response.statusText } }
8134
8172
  ));
@@ -8136,7 +8174,7 @@ var KVService = class extends BaseService {
8136
8174
  return err(
8137
8175
  serviceError(
8138
8176
  ErrorCodes.NETWORK_ERROR,
8139
- `Failed to delete key "${key}": ${response.status} - ${errorText}`,
8177
+ `Failed to delete key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
8140
8178
  "kv",
8141
8179
  { meta: { status: response.status, statusText: response.statusText } }
8142
8180
  )
@@ -8169,13 +8207,11 @@ var KVService = class extends BaseService {
8169
8207
  );
8170
8208
  if (!response.ok) {
8171
8209
  if (response.status === 401) {
8172
- const errorText2 = await response.text();
8173
- const { resource, action } = parseAuthError(errorText2);
8174
- return err(authUnauthorizedError("kv", errorText2, {
8175
- status: response.status,
8176
- ...action && { requiredAction: action },
8177
- ...resource && { resource }
8178
- }));
8210
+ return this.authorizationFailure(
8211
+ `Failed to get metadata for key ${JSON.stringify(key)}`,
8212
+ response,
8213
+ await response.text()
8214
+ );
8179
8215
  }
8180
8216
  if (response.status === 404) {
8181
8217
  return this.classifyNotFound(response, key);
@@ -8184,7 +8220,7 @@ var KVService = class extends BaseService {
8184
8220
  return err(
8185
8221
  serviceError(
8186
8222
  ErrorCodes.NETWORK_ERROR,
8187
- `Failed to get metadata for key "${key}": ${response.status} - ${errorText}`,
8223
+ `Failed to get metadata for key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
8188
8224
  "kv",
8189
8225
  { meta: { status: response.status, statusText: response.statusText } }
8190
8226
  )
@@ -9643,10 +9679,14 @@ async function responseError(service, message, response) {
9643
9679
  }
9644
9680
  } catch {
9645
9681
  }
9646
- return wrapError2(
9682
+ const error = wrapError2(
9647
9683
  service,
9648
9684
  new Error(`${message}: ${response.status} ${detail}`)
9649
9685
  );
9686
+ return {
9687
+ ...error,
9688
+ meta: { ...error.meta, status: response.status, statusText: response.statusText }
9689
+ };
9650
9690
  }
9651
9691
  function isAbortError(error) {
9652
9692
  return error instanceof DOMException && error.name === "AbortError";
@@ -13493,39 +13533,89 @@ async function createSDKInstance(ctx, options) {
13493
13533
  return node;
13494
13534
  }
13495
13535
  async function bootstrapDelegatedSession(ctx, delegation) {
13496
- const profile = await ProfileManager.getProfile(ctx.profile);
13497
- if (resolveProfilePosture(profile) !== "delegate-session") {
13498
- throw new CLIError(
13499
- "AUTH_REQUIRED",
13500
- `Profile "${ctx.profile}" is not a delegate-session profile.`,
13501
- ExitCode.AUTH_REQUIRED
13502
- );
13503
- }
13504
- const sessionDid = profile.sessionDid ?? profile.did;
13505
- if (delegation.delegateDID.split("#", 1)[0] !== sessionDid.split("#", 1)[0]) {
13506
- throw new CLIError(
13507
- "DELEGATION_AUDIENCE_MISMATCH",
13508
- `Delegation targets ${delegation.delegateDID}, but profile "${ctx.profile}" uses ${sessionDid}.`,
13509
- ExitCode.PERMISSION_DENIED
13510
- );
13511
- }
13512
- const key = await ProfileManager.getKey(ctx.profile);
13513
- const jwk = signerJwkForProfile(ctx.profile, void 0, key);
13514
- await ProfileManager.withLock(ctx.profile, async () => {
13515
- await ProfileManager.setSession(ctx.profile, {
13536
+ const written = await ProfileManager.withLock(ctx.profile, async () => {
13537
+ const previousProfile = await ProfileManager.getProfile(ctx.profile);
13538
+ if (resolveProfilePosture(previousProfile) !== "delegate-session") {
13539
+ throw new CLIError(
13540
+ "AUTH_REQUIRED",
13541
+ `Profile "${ctx.profile}" is not a delegate-session profile.`,
13542
+ ExitCode.AUTH_REQUIRED
13543
+ );
13544
+ }
13545
+ const sessionDid = previousProfile.sessionDid ?? previousProfile.did;
13546
+ if (delegation.delegateDID.split("#", 1)[0] !== sessionDid.split("#", 1)[0]) {
13547
+ throw new CLIError(
13548
+ "DELEGATION_AUDIENCE_MISMATCH",
13549
+ `Delegation targets ${delegation.delegateDID}, but profile "${ctx.profile}" uses ${sessionDid}.`,
13550
+ ExitCode.PERMISSION_DENIED
13551
+ );
13552
+ }
13553
+ if (await ProfileManager.getSession(ctx.profile) !== null) {
13554
+ throw new CLIError(
13555
+ "PROFILE_CHANGED_DURING_IMPORT",
13556
+ `Profile "${ctx.profile}" gained a session (another login or import) after this import checked it. Nothing was saved; run the import again.`,
13557
+ ExitCode.ERROR
13558
+ );
13559
+ }
13560
+ const jwk = signerJwkForProfile(ctx.profile, void 0, await ProfileManager.getKey(ctx.profile));
13561
+ const session = {
13516
13562
  delegationHeader: delegation.delegationHeader,
13517
13563
  delegationCid: delegation.cid,
13518
13564
  spaceId: delegation.spaceId,
13519
13565
  jwk,
13520
13566
  verificationMethod: sessionDid
13521
- });
13522
- await ProfileManager.updateProfile(ctx.profile, (current) => ({
13523
- ...current,
13524
- sessionDid,
13525
- spaceId: delegation.spaceId
13526
- }));
13567
+ };
13568
+ const profile = { ...previousProfile, sessionDid, spaceId: delegation.spaceId };
13569
+ try {
13570
+ await ProfileManager.setSession(ctx.profile, session);
13571
+ await ProfileManager.setProfile(ctx.profile, profile);
13572
+ } catch (error) {
13573
+ throw annotate(error, await restoreBeforeBootstrap(ctx.profile, previousProfile));
13574
+ }
13575
+ return { previousProfile, profile, session };
13527
13576
  });
13528
- return createSDKInstance(ctx);
13577
+ const abandon = async (cause) => {
13578
+ const note = await ProfileManager.withLock(ctx.profile, async () => {
13579
+ const profile = await ProfileManager.getProfile(ctx.profile).catch((error) => {
13580
+ if (error instanceof CLIError && error.code === "PROFILE_NOT_FOUND") return null;
13581
+ throw error;
13582
+ });
13583
+ const session = await ProfileManager.getSession(ctx.profile);
13584
+ if (JSON.stringify(profile) !== JSON.stringify(written.profile) || JSON.stringify(session) !== JSON.stringify(written.session)) {
13585
+ 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.`;
13586
+ }
13587
+ return restoreBeforeBootstrap(ctx.profile, written.previousProfile);
13588
+ }, { timeoutMs: PROFILE_COMMIT_LOCK_TIMEOUT_MS }).catch((error) => (
13589
+ // The lock or a read failed: the import's error stays the one reported.
13590
+ `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\`.`
13591
+ ));
13592
+ throw annotate(cause, note);
13593
+ };
13594
+ let node;
13595
+ try {
13596
+ node = await createSDKInstance(ctx);
13597
+ } catch (error) {
13598
+ return abandon(error);
13599
+ }
13600
+ return { node, abandon };
13601
+ }
13602
+ async function restoreBeforeBootstrap(profileName, previousProfile) {
13603
+ const failures = [];
13604
+ for (const write of [
13605
+ () => ProfileManager.clearSession(profileName),
13606
+ () => ProfileManager.setProfile(profileName, previousProfile)
13607
+ ]) {
13608
+ await write().catch((error) => {
13609
+ failures.push(error instanceof Error ? error.message : String(error));
13610
+ });
13611
+ }
13612
+ if (failures.length === 0) return void 0;
13613
+ return `Rolling back the provisional session of profile "${profileName}" failed too (${failures.join("; ")}); check \`tc --profile ${profileName} context\`.`;
13614
+ }
13615
+ function annotate(error, note) {
13616
+ if (note === void 0) return error;
13617
+ const cause = wrapError(error);
13618
+ return new CLIError(cause.code, `${cause.message} ${note}`, cause.exitCode, cause.metadata);
13529
13619
  }
13530
13620
  async function ensureAuthenticated(ctx, options) {
13531
13621
  if (options?.privateKey) {
@@ -14590,7 +14680,6 @@ function assertSessionReplaceable(profileName, snapshot, ownerDid, scope, newExp
14590
14680
  ExitCode.USAGE_ERROR
14591
14681
  );
14592
14682
  }
14593
- var COMMIT_LOCK_TIMEOUT_MS = 45e3;
14594
14683
  async function restore(profileName, state) {
14595
14684
  const writes = [
14596
14685
  () => state.key === null ? ProfileManager.removeKey(profileName) : ProfileManager.setKey(profileName, state.key),
@@ -14616,7 +14705,7 @@ async function commitLogin(profileName, snapshot, commit) {
14616
14705
  if (canonicalJson(current.profile) !== canonicalJson(snapshot.profile) || canonicalJson(current.key) !== canonicalJson(snapshot.key) || canonicalJson(current.session) !== canonicalJson(snapshot.session)) {
14617
14706
  throw new CLIError(
14618
14707
  "PROFILE_CHANGED_DURING_LOGIN",
14619
- `Profile "${profileName}" changed while waiting for approval (another login, key rotation or logout). Nothing was saved; check \`tc --profile ${profileName} context\` and run the login again if it is still needed.`,
14708
+ `Profile "${profileName}" changed while this login was in progress (another login, key rotation or logout). Nothing was saved; check \`tc --profile ${profileName} context\` and run the login again if it is still needed.`,
14620
14709
  ExitCode.ERROR
14621
14710
  );
14622
14711
  }
@@ -14637,11 +14726,11 @@ async function commitLogin(profileName, snapshot, commit) {
14637
14726
  ExitCode.ERROR
14638
14727
  );
14639
14728
  }
14640
- }, { timeoutMs: COMMIT_LOCK_TIMEOUT_MS }).catch((error) => {
14729
+ }, { timeoutMs: PROFILE_COMMIT_LOCK_TIMEOUT_MS }).catch((error) => {
14641
14730
  if (!(error instanceof ProfileLockTimeoutError2)) throw error;
14642
14731
  throw new CLIError(
14643
14732
  "PROFILE_LOCK_TIMEOUT",
14644
- `Another tc process kept profile "${profileName}" locked for ${COMMIT_LOCK_TIMEOUT_MS / 1e3} s, so the approved login was not saved. Wait for it to finish (a crashed process's lock is reclaimed after 30 s) and run the login again.`,
14733
+ `Another tc process kept profile "${profileName}" locked for ${PROFILE_COMMIT_LOCK_TIMEOUT_MS / 1e3} s, so the approved login was not saved. Wait for it to finish (a crashed process's lock is reclaimed after 30 s) and run the login again.`,
14645
14734
  ExitCode.ERROR
14646
14735
  );
14647
14736
  });
@@ -15432,7 +15521,7 @@ function registerAuthCommand(program) {
15432
15521
  const profile = await ProfileManager.getProfile(ctx.profile);
15433
15522
  const session = await ProfileManager.getSession(ctx.profile);
15434
15523
  if (session || resolveProfilePosture(profile) !== "delegate-session") throw error;
15435
- node = await bootstrapDelegatedSession(ctx, imported.delegation);
15524
+ node = (await bootstrapDelegatedSession(ctx, imported.delegation)).node;
15436
15525
  }
15437
15526
  await appendAdditionalDelegation(ctx.profile, storedAdditionalDelegation(
15438
15527
  imported.delegation,
@@ -15757,15 +15846,11 @@ async function importRequestBoundDelegationWithBootstrap(ctx, artifact) {
15757
15846
  }
15758
15847
  const candidate = artifact;
15759
15848
  const delegation = normalizePortableDelegation(candidate.delegation);
15849
+ const bootstrap = await bootstrapDelegatedSession(ctx, delegation);
15760
15850
  try {
15761
- await bootstrapDelegatedSession(ctx, delegation);
15762
15851
  await importRequestBoundDelegation(ctx, artifact);
15763
15852
  } catch (error) {
15764
- await ProfileManager.withLock(ctx.profile, async () => {
15765
- await ProfileManager.clearSession(ctx.profile);
15766
- await ProfileManager.setProfile(ctx.profile, profile);
15767
- });
15768
- throw error;
15853
+ await bootstrap.abandon(error);
15769
15854
  }
15770
15855
  }
15771
15856
  async function importRequestBoundDelegation(ctx, artifact) {
@@ -16079,7 +16164,6 @@ async function rotateAuthKey(profileName, host, options = {}) {
16079
16164
  ExitCode.AUTH_REQUIRED
16080
16165
  );
16081
16166
  }
16082
- await ProfileManager.clearSession(profileName);
16083
16167
  const result2 = await handleLocalAuth(profileName, host, {
16084
16168
  emitOutput: false,
16085
16169
  forceSessionKey: true
@@ -17643,7 +17727,7 @@ init_errors();
17643
17727
  init_constants();
17644
17728
  init_storage();
17645
17729
  import { randomUUID as randomUUID2 } from "crypto";
17646
- import { lstat, open as open2, readFile as readFile8, rename as rename2, rm as rm3 } from "fs/promises";
17730
+ import { lstat as lstat2, open as open2, readFile as readFile8, rename as rename2, rm as rm3 } from "fs/promises";
17647
17731
  import { basename as basename2, dirname as dirname3, join as join5 } from "path";
17648
17732
  import { homedir } from "os";
17649
17733
  import { invokeOperation as invokeOperation2 } from "@tinycloud/operations";
@@ -17817,7 +17901,7 @@ function scopedSecretLoginHint(profileName) {
17817
17901
  }
17818
17902
  async function validateSecretOutput(path) {
17819
17903
  try {
17820
- const destination = await lstat(path);
17904
+ const destination = await lstat2(path);
17821
17905
  if (!destination.isFile()) {
17822
17906
  throw new CLIError("INVALID_ARGUMENT", `Secret output "${path}" must be a regular file, not a symlink, directory, or device.`, ExitCode.USAGE_ERROR);
17823
17907
  }
@@ -17825,7 +17909,7 @@ async function validateSecretOutput(path) {
17825
17909
  if (!isMissingFileError(error)) throw error;
17826
17910
  }
17827
17911
  try {
17828
- const parent = await lstat(dirname3(path));
17912
+ const parent = await lstat2(dirname3(path));
17829
17913
  if (!parent.isDirectory()) {
17830
17914
  throw new CLIError("INVALID_ARGUMENT", `Secret output "${path}" requires an existing directory parent.`, ExitCode.USAGE_ERROR);
17831
17915
  }
@@ -27477,7 +27561,7 @@ function receiveJson(result, path) {
27477
27561
 
27478
27562
  // src/share/io.ts
27479
27563
  import { constants } from "fs";
27480
- import { lstat as lstat2, mkdir as mkdir3, mkdtemp, open as open4, readFile as readFile9, realpath, stat as stat2, link, rename as rename3, rm as rm4, unlink } from "fs/promises";
27564
+ import { lstat as lstat3, mkdir as mkdir3, mkdtemp, open as open4, readFile as readFile9, realpath, stat as stat2, link, rename as rename3, rm as rm4, unlink } from "fs/promises";
27481
27565
  import { randomBytes as randomBytes4 } from "crypto";
27482
27566
  import { basename as basename3, join as join6, resolve as resolve2, sep } from "path";
27483
27567
  init_errors();
@@ -27535,7 +27619,7 @@ async function assertDirectory(path) {
27535
27619
  for (const segment of segments) {
27536
27620
  current = current === sep ? join6(current, segment) : join6(current, segment);
27537
27621
  try {
27538
- const info = await lstat2(current);
27622
+ const info = await lstat3(current);
27539
27623
  if (info.isSymbolicLink()) {
27540
27624
  const canonical = await realpath(current);
27541
27625
  if (current !== "/tmp" && current !== "/var") throw new Error("OUTPUT_EXISTS");
@@ -27544,7 +27628,7 @@ async function assertDirectory(path) {
27544
27628
  } catch (error) {
27545
27629
  if (error.code !== "ENOENT") throw error;
27546
27630
  await mkdir3(current, { mode: 448 });
27547
- const created = await lstat2(current);
27631
+ const created = await lstat3(current);
27548
27632
  if (created.isSymbolicLink() || !created.isDirectory()) throw new Error("OUTPUT_EXISTS");
27549
27633
  }
27550
27634
  }
@@ -27563,7 +27647,7 @@ async function writeShareOutput(directory, filename, bytes, force) {
27563
27647
  };
27564
27648
  await assertStableDirectory();
27565
27649
  const stagingDirectory = await mkdtemp(join6(stableDirectory, ".tinycloud-share-stage-"));
27566
- const stagingInfo = await lstat2(stagingDirectory);
27650
+ const stagingInfo = await lstat3(stagingDirectory);
27567
27651
  if (!stagingInfo.isDirectory() || (stagingInfo.mode & 511) !== 448) throw new Error("OUTPUT_EXISTS");
27568
27652
  const stagingPath = join6(stagingDirectory, `.tinycloud-share-${randomBytes4(16).toString("hex")}.tmp`);
27569
27653
  let temporaryPath;
@@ -27571,7 +27655,7 @@ async function writeShareOutput(directory, filename, bytes, force) {
27571
27655
  try {
27572
27656
  await assertStableDirectory();
27573
27657
  try {
27574
- const existing = await lstat2(outputPath);
27658
+ const existing = await lstat3(outputPath);
27575
27659
  if (existing.isSymbolicLink()) throw new Error("UNSAFE_FILENAME");
27576
27660
  } catch (error) {
27577
27661
  if (error.code !== "ENOENT") throw error;