@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.
@@ -260,7 +260,7 @@ var init_formatter = __esm({
260
260
  // src/output/errors.ts
261
261
  import { readFileSync, readdirSync } from "fs";
262
262
  import { join as join2 } from "path";
263
- import { ProfileLockTimeoutError } from "@tinycloud/operations/state";
263
+ import { ProfileDeletedError, ProfileLockTimeoutError } from "@tinycloud/operations/state";
264
264
  function setActiveProfileName(name) {
265
265
  activeProfileName = name;
266
266
  }
@@ -286,6 +286,9 @@ function wrapError(error) {
286
286
  { hint: "Wait for the other command to finish and retry. A crashed process's lock is reclaimed automatically after 30 s." }
287
287
  );
288
288
  }
289
+ if (error instanceof ProfileDeletedError) {
290
+ return new CLIError("PROFILE_NOT_FOUND", message);
291
+ }
289
292
  if (message.includes("Not signed in") || message.includes("AUTH_EXPIRED") || message.includes("Session expired")) {
290
293
  return new CLIError("AUTH_REQUIRED", message, ExitCode.AUTH_REQUIRED);
291
294
  }
@@ -644,6 +647,8 @@ import { join as join3 } from "path";
644
647
  import {
645
648
  profilePath,
646
649
  readSession,
650
+ recordProfileDeletion,
651
+ refuseWriteToDeletedProfile,
647
652
  removeSession,
648
653
  withProfileLock,
649
654
  writeSession
@@ -758,10 +763,12 @@ var init_profiles = __esm({
758
763
  * removed while holding the profile lock, so another writer's critical
759
764
  * section never sees them vanish midway: session and key first, settings
760
765
  * 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.
766
+ * profile. The deletion is then recorded, so a store write that waited for
767
+ * the lock meanwhile refuses rather than recreating a profile with only a
768
+ * session in it. `.lock` itself is left to the lock's release; the
769
+ * then-empty directory is removed afterwards unless another writer took
770
+ * the lock (or wrote) meanwhile. A profile directory that is a symlink is
771
+ * unlinked, its target left alone.
765
772
  * Throws if the name is not one path segment or names the default profile.
766
773
  */
767
774
  static async deleteProfile(name) {
@@ -782,11 +789,12 @@ var init_profiles = __esm({
782
789
  `Cannot delete the default profile "${name}". Change the default first with \`tc profile default <other>\`.`
783
790
  );
784
791
  }
785
- const isLink = await lstat(profileDir).then((stats) => stats.isSymbolicLink(), (error) => {
786
- if (error.code === "ENOENT") return false;
792
+ const kind = await lstat(profileDir).then((stats) => stats.isSymbolicLink() ? "link" : "present", (error) => {
793
+ if (error.code === "ENOENT") return "missing";
787
794
  throw error;
788
795
  });
789
- if (isLink) {
796
+ if (kind === "missing") return;
797
+ if (kind === "link") {
790
798
  await rm2(profileDir, { force: true });
791
799
  return;
792
800
  }
@@ -796,6 +804,7 @@ var init_profiles = __esm({
796
804
  if (entry !== ".lock" && entry !== "profile.json") await rm2(join3(profileDir, entry), { recursive: true, force: true });
797
805
  }
798
806
  await rm2(join3(profileDir, "profile.json"), { force: true });
807
+ await recordProfileDeletion(name);
799
808
  });
800
809
  await rmdir(profileDir).catch((error) => {
801
810
  if (error.code !== "ENOENT" && error.code !== "ENOTEMPTY" && error.code !== "EEXIST") throw error;
@@ -808,9 +817,14 @@ var init_profiles = __esm({
808
817
  static async getKey(name) {
809
818
  return readJson(join3(PROFILES_DIR, name, "key.json"));
810
819
  }
811
- /** Saves a JWK key under the profile lock (0600, in an owner-only profile directory). */
820
+ /**
821
+ * Saves a JWK key under the profile lock (0600, in an owner-only profile
822
+ * directory). Refused (PROFILE_NOT_FOUND) if the profile was deleted while
823
+ * this waited for the lock, rather than leaving a key-only profile.
824
+ */
812
825
  static async setKey(name, jwk) {
813
826
  await _ProfileManager.withLock(name, async () => {
827
+ await refuseWriteToDeletedProfile(name);
814
828
  await writeJson(join3(await _ProfileManager.ensureProfileDir(name), "key.json"), jwk);
815
829
  });
816
830
  }
@@ -1779,7 +1793,8 @@ import {
1779
1793
  profileStoreMetadataPath,
1780
1794
  readAdditionalDelegations,
1781
1795
  readAuthRequests,
1782
- upsertProfileRecord,
1796
+ refuseWriteToDeletedProfile as refuseWriteToDeletedProfile2,
1797
+ updateProfileStore,
1783
1798
  withProfileLock as withProfileLock2,
1784
1799
  writeJsonAtomic
1785
1800
  } from "@tinycloud/operations/state";
@@ -13123,17 +13138,15 @@ function didWithoutFragment(did) {
13123
13138
  async function loadAdditionalDelegations(profile) {
13124
13139
  return readAdditionalDelegations(profile);
13125
13140
  }
13126
- async function saveAdditionalDelegations(profile, entries) {
13127
- await replaceSharedRecords(profile, "additional-delegations", entries);
13141
+ async function appendAdditionalDelegations(profile, entries) {
13142
+ const { mergeDelegationsWithoutRequest } = await import("@tinycloud/operations/delegation-binding");
13143
+ await updateProfileStore(profile, "additional-delegations", (stored) => ({
13144
+ records: mergeDelegationsWithoutRequest(stored, entries.map((entry) => ({ ...entry }))),
13145
+ result: void 0
13146
+ }));
13128
13147
  }
13129
13148
  async function appendAdditionalDelegation(profile, entry) {
13130
- await upsertProfileRecord(
13131
- profile,
13132
- "additional-delegations",
13133
- entry.delegation.cid,
13134
- entry,
13135
- (candidate) => candidate.delegation.cid
13136
- );
13149
+ await appendAdditionalDelegations(profile, [entry]);
13137
13150
  }
13138
13151
  async function loadPermissionRequestArtifacts(profile) {
13139
13152
  const raw = await readAuthRequests(profile);
@@ -13147,10 +13160,8 @@ async function appendPermissionRequestArtifact(profile, artifact) {
13147
13160
  await writeSharedRecords(profile, "auth-requests", next);
13148
13161
  });
13149
13162
  }
13150
- async function replaceSharedRecords(profile, store, entries) {
13151
- await withProfileLock2(profile, () => writeSharedRecords(profile, store, entries));
13152
- }
13153
13163
  async function writeSharedRecords(profile, store, entries) {
13164
+ await refuseWriteToDeletedProfile2(profile);
13154
13165
  const path = store === "additional-delegations" ? additionalDelegationsPath(profile) : permissionRequestsPath(profile);
13155
13166
  await writeJsonAtomic(path, entries);
13156
13167
  await writeJsonAtomic(profileStoreMetadataPath(profile, store), { formatVersion: 1 });
@@ -13163,13 +13174,48 @@ async function getLastPermissionRequestArtifact(profile) {
13163
13174
  const existing = await loadPermissionRequestArtifacts(profile);
13164
13175
  return existing.at(-1) ?? null;
13165
13176
  }
13166
- async function replayAdditionalDelegations(node, profile) {
13177
+ async function replayAdditionalDelegations(node, profile, options) {
13178
+ const {
13179
+ operationSpaceResolver,
13180
+ prepareStoredDelegationReplay,
13181
+ replayStoredDelegation,
13182
+ storedDelegationKind
13183
+ } = await import("@tinycloud/operations/delegation-binding");
13184
+ const activator = node;
13185
+ const migrated = await prepareStoredDelegationReplay(profile, activator, {
13186
+ host: options.host,
13187
+ migrate: options.migrate
13188
+ });
13189
+ const resolveSpace = operationSpaceResolver(node, options.ownerSpace);
13167
13190
  const entries = await loadAdditionalDelegations(profile);
13168
- for (const entry of entries) {
13191
+ for (const stored of entries) {
13192
+ const kind = storedDelegationKind(stored);
13193
+ if (kind === "compact" || kind === "signed-login") {
13194
+ const installed = await replayStoredDelegation(activator, stored, {
13195
+ host: options.host,
13196
+ migrated,
13197
+ resolveSpace
13198
+ });
13199
+ if (installed === void 0 && process.env.TC_DEBUG_REPLAY === "1") {
13200
+ process.stderr.write("[replay] skipping a stored delegation refused by validation or its request binding\n");
13201
+ }
13202
+ continue;
13203
+ }
13204
+ if (kind === "refused") {
13205
+ if (process.env.TC_DEBUG_REPLAY === "1") {
13206
+ process.stderr.write("[replay] skipping a malformed stored record or one bound to a request it cannot be held to\n");
13207
+ }
13208
+ continue;
13209
+ }
13210
+ const entry = stored;
13169
13211
  const expiry = entry.delegation.expiry instanceof Date ? entry.delegation.expiry : new Date(entry.delegation.expiry);
13170
13212
  if (expiry.getTime() <= Date.now()) continue;
13171
13213
  try {
13172
- await node.useRuntimeDelegation({ ...entry.delegation, expiry });
13214
+ await node.useRuntimeDelegation({
13215
+ ...entry.delegation,
13216
+ delegationHeader: { Authorization: entry.delegation.delegationHeader.Authorization },
13217
+ expiry
13218
+ });
13173
13219
  } catch (err2) {
13174
13220
  if (process.env.TC_DEBUG_REPLAY === "1") {
13175
13221
  process.stderr.write(`[replay] skipping ${entry.delegation.cid}: ${err2.message}
@@ -13474,6 +13520,7 @@ async function createSDKInstance(ctx, options) {
13474
13520
  host: ctx.host,
13475
13521
  privateKey: effectivePrivateKey
13476
13522
  });
13523
+ let restoredOwnSession2 = false;
13477
13524
  if (session && session.delegationHeader && session.delegationCid && session.spaceId) {
13478
13525
  await node2.restoreSession({
13479
13526
  delegationHeader: session.delegationHeader,
@@ -13486,16 +13533,22 @@ async function createSDKInstance(ctx, options) {
13486
13533
  siwe: session.siwe,
13487
13534
  signature: session.signature
13488
13535
  });
13536
+ restoredOwnSession2 = true;
13489
13537
  } else {
13490
13538
  await node2.signIn();
13491
13539
  }
13492
- await replayAdditionalDelegations(node2, ctx.profile);
13540
+ await replayAdditionalDelegations(node2, ctx.profile, {
13541
+ host: ctx.host,
13542
+ ownerSpace: profile.spaceId,
13543
+ migrate: restoredOwnSession2 && options?.privateKey === void 0
13544
+ });
13493
13545
  return node2;
13494
13546
  }
13495
13547
  const node = new TinyCloudNode({
13496
13548
  host: ctx.host,
13497
13549
  privateKey: options?.privateKey
13498
13550
  });
13551
+ let restoredOwnSession = false;
13499
13552
  if (options?.privateKey) {
13500
13553
  await node.signIn();
13501
13554
  } else if (session && session.delegationHeader && session.delegationCid && session.spaceId) {
@@ -13510,10 +13563,16 @@ async function createSDKInstance(ctx, options) {
13510
13563
  siwe: session.siwe,
13511
13564
  signature: session.signature
13512
13565
  });
13566
+ restoredOwnSession = true;
13513
13567
  }
13514
- await replayAdditionalDelegations(node, ctx.profile);
13568
+ await replayAdditionalDelegations(node, ctx.profile, {
13569
+ host: ctx.host,
13570
+ ownerSpace: profile?.spaceId,
13571
+ migrate: restoredOwnSession
13572
+ });
13515
13573
  return node;
13516
13574
  }
13575
+ var BOOTSTRAP_PROFILE_FIELDS = ["sessionDid", "spaceId"];
13517
13576
  async function bootstrapDelegatedSession(ctx, delegation) {
13518
13577
  const written = await ProfileManager.withLock(ctx.profile, async () => {
13519
13578
  const previousProfile = await ProfileManager.getProfile(ctx.profile);
@@ -13563,13 +13622,13 @@ async function bootstrapDelegatedSession(ctx, delegation) {
13563
13622
  throw error;
13564
13623
  });
13565
13624
  const session = await ProfileManager.getSession(ctx.profile);
13566
- if (JSON.stringify(profile) !== JSON.stringify(written.profile) || JSON.stringify(session) !== JSON.stringify(written.session)) {
13625
+ if (profile === null || BOOTSTRAP_PROFILE_FIELDS.some((field) => profile[field] !== written.profile[field]) || JSON.stringify(session) !== JSON.stringify(written.session)) {
13567
13626
  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.`;
13568
13627
  }
13569
13628
  return restoreBeforeBootstrap(ctx.profile, written.previousProfile);
13570
13629
  }, { timeoutMs: PROFILE_COMMIT_LOCK_TIMEOUT_MS }).catch((error) => (
13571
13630
  // The lock or a read failed: the import's error stays the one reported.
13572
- `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\`.`
13631
+ `Rolling back the provisional session of profile "${ctx.profile}" could not run (${failureName(error)}); check \`tc --profile ${ctx.profile} context\`.`
13573
13632
  ));
13574
13633
  throw annotate(cause, note);
13575
13634
  };
@@ -13585,15 +13644,27 @@ async function restoreBeforeBootstrap(profileName, previousProfile) {
13585
13644
  const failures = [];
13586
13645
  for (const write of [
13587
13646
  () => ProfileManager.clearSession(profileName),
13588
- () => ProfileManager.setProfile(profileName, previousProfile)
13647
+ () => ProfileManager.updateProfile(profileName, (current) => {
13648
+ const restored = { ...current };
13649
+ for (const field of BOOTSTRAP_PROFILE_FIELDS) {
13650
+ if (previousProfile[field] === void 0) delete restored[field];
13651
+ else restored[field] = previousProfile[field];
13652
+ }
13653
+ return restored;
13654
+ })
13589
13655
  ]) {
13590
13656
  await write().catch((error) => {
13591
- failures.push(error instanceof Error ? error.message : String(error));
13657
+ failures.push(failureName(error));
13592
13658
  });
13593
13659
  }
13594
13660
  if (failures.length === 0) return void 0;
13595
13661
  return `Rolling back the provisional session of profile "${profileName}" failed too (${failures.join("; ")}); check \`tc --profile ${profileName} context\`.`;
13596
13662
  }
13663
+ function failureName(error) {
13664
+ if (error instanceof SyntaxError) return "a profile file is not valid JSON";
13665
+ if (typeof error === "object" && error !== null && "code" in error && typeof error.code === "string") return error.code;
13666
+ return error instanceof Error ? error.name : "unknown error";
13667
+ }
13597
13668
  function annotate(error, note) {
13598
13669
  if (note === void 0) return error;
13599
13670
  const cause = wrapError(error);
@@ -13605,7 +13676,7 @@ async function ensureAuthenticated(ctx, options) {
13605
13676
  }
13606
13677
  const profile = await ProfileManager.getProfile(ctx.profile).catch(() => null);
13607
13678
  if (profile?.authMethod === "local" && profile.privateKey) {
13608
- return createSDKInstance(ctx, { privateKey: profile.privateKey });
13679
+ return createSDKInstance(ctx);
13609
13680
  }
13610
13681
  const session = await ProfileManager.getSession(ctx.profile);
13611
13682
  if (!session) {
@@ -15496,6 +15567,15 @@ function registerAuthCommand(program) {
15496
15567
  return;
15497
15568
  }
15498
15569
  const imported = normalizeDelegationImport(parsed);
15570
+ const { activateUnboundCompactImport, storedDelegationKind } = await import("@tinycloud/operations/delegation-binding");
15571
+ const kind = storedDelegationKind({ delegation: imported.delegation });
15572
+ if (kind === "refused") {
15573
+ throw new CLIError(
15574
+ "INVALID_AUTH_IMPORT",
15575
+ "Imported delegation must carry exactly one string Authorization header.",
15576
+ ExitCode.USAGE_ERROR
15577
+ );
15578
+ }
15499
15579
  let node;
15500
15580
  try {
15501
15581
  node = await ensureAuthenticated(ctx);
@@ -15505,18 +15585,33 @@ function registerAuthCommand(program) {
15505
15585
  if (session || resolveProfilePosture(profile) !== "delegate-session") throw error;
15506
15586
  node = (await bootstrapDelegatedSession(ctx, imported.delegation)).node;
15507
15587
  }
15508
- await appendAdditionalDelegation(ctx.profile, storedAdditionalDelegation(
15509
- imported.delegation,
15510
- imported.permissions
15511
- ));
15512
15588
  const targetsSessionKey = typeof imported.delegation.delegateDID === "string" && principalDidEquals(imported.delegation.delegateDID, node.sessionDid);
15513
15589
  let activated = false;
15514
- if (targetsSessionKey) {
15515
- await node.useRuntimeDelegation(imported.delegation);
15590
+ let permissions = imported.permissions;
15591
+ if (targetsSessionKey && kind === "compact") {
15592
+ const record = await activateUnboundCompactImport(
15593
+ node,
15594
+ imported.delegation,
15595
+ ctx.host
15596
+ );
15597
+ await appendAdditionalDelegation(ctx.profile, record);
15598
+ permissions = record.permissions;
15516
15599
  activated = true;
15600
+ } else {
15601
+ await appendAdditionalDelegation(ctx.profile, storedAdditionalDelegation(
15602
+ imported.delegation,
15603
+ imported.permissions
15604
+ ));
15605
+ if (targetsSessionKey) {
15606
+ await node.useRuntimeDelegation({
15607
+ ...imported.delegation,
15608
+ delegationHeader: { Authorization: imported.delegation.delegationHeader.Authorization }
15609
+ });
15610
+ activated = true;
15611
+ }
15517
15612
  }
15518
15613
  await appendGrantHistory(ctx.profile, {
15519
- addedCaps: imported.permissions,
15614
+ addedCaps: permissions,
15520
15615
  source: "cli",
15521
15616
  delegationCid: imported.delegation.cid,
15522
15617
  expiry: imported.delegation.expiry.toISOString()
@@ -15527,7 +15622,7 @@ function registerAuthCommand(program) {
15527
15622
  kind: "tinycloud.auth.delegation",
15528
15623
  requestId: imported.requestId ?? null,
15529
15624
  delegationCid: imported.delegation.cid,
15530
- permissions: imported.permissions,
15625
+ permissions,
15531
15626
  expiry: imported.delegation.expiry.toISOString()
15532
15627
  });
15533
15628
  } catch (error) {
@@ -15901,8 +15996,6 @@ async function activateAndStoreOpenKeyGrants(profileName, node, grants, source)
15901
15996
  for (const { delegation } of grants) await node.useRuntimeDelegation(delegation);
15902
15997
  if (grants.length === 0) return;
15903
15998
  await ProfileManager.withLock(profileName, async () => {
15904
- const existing = await loadAdditionalDelegations(profileName);
15905
- const replacing = new Set(grants.map(({ delegation }) => delegation.cid));
15906
15999
  for (const { delegation, effective } of grants) {
15907
16000
  await appendGrantHistory(profileName, {
15908
16001
  addedCaps: effective,
@@ -15911,10 +16004,10 @@ async function activateAndStoreOpenKeyGrants(profileName, node, grants, source)
15911
16004
  expiry: delegation.expiry.toISOString()
15912
16005
  });
15913
16006
  }
15914
- await saveAdditionalDelegations(profileName, [
15915
- ...existing.filter(({ delegation }) => !replacing.has(delegation.cid)),
15916
- ...grants.map(({ delegation, effective }) => storedAdditionalDelegation(delegation, effective))
15917
- ]);
16007
+ await appendAdditionalDelegations(
16008
+ profileName,
16009
+ grants.map(({ delegation, effective }) => storedAdditionalDelegation(delegation, effective))
16010
+ );
15918
16011
  });
15919
16012
  }
15920
16013
  async function ensureDelegationAuthority(params) {
@@ -29981,7 +30074,13 @@ async function readHasKey(name, issues) {
29981
30074
  }
29982
30075
  async function readDelegations(name, issues) {
29983
30076
  try {
29984
- return await loadAdditionalDelegations(name);
30077
+ const entries = await loadAdditionalDelegations(name);
30078
+ const { storedDelegationKind } = await import("@tinycloud/operations/delegation-binding");
30079
+ const readable = entries.filter((entry) => storedDelegationKind(entry) !== "refused");
30080
+ if (readable.length < entries.length) {
30081
+ issues.push(`delegations: ${entries.length - readable.length} unreadable stored record(s) skipped`);
30082
+ }
30083
+ return readable;
29985
30084
  } catch (error) {
29986
30085
  issues.push(`delegations: ${messageFromError(error)}`);
29987
30086
  return [];