@tinycloud/cli 1.1.0-beta.13 → 1.1.0-beta.15

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
@@ -74,8 +74,14 @@ var init_theme = __esm({
74
74
 
75
75
  // src/output/formatter.ts
76
76
  import ora from "ora";
77
- function outputError(code3, message, hint, meta) {
78
- if (isInteractive()) {
77
+ function warningLine(warnings) {
78
+ const which = warnings.map((warning) => `${warning.grantCid ?? "unidentified"}: ${warning.reason}`).join(", ");
79
+ const subject = warnings.length === 1 ? "A stored grant was" : `${warnings.length} stored grants were`;
80
+ return `${subject} not used (${which}). Approve access again when a command needs it; the old grant expires on its own.`;
81
+ }
82
+ function outputError(code3, message, hint, details = {}) {
83
+ const { meta, warnings = [] } = details;
84
+ if (!shouldOutputJson()) {
79
85
  process.stderr.write(
80
86
  `${theme.error("\u2717")} ${theme.label(code3)}: ${message}
81
87
  `
@@ -86,22 +92,47 @@ function outputError(code3, message, hint, meta) {
86
92
  `);
87
93
  }
88
94
  }
95
+ if (warnings.length > 0) process.stderr.write(` ${theme.warn(warningLine(warnings))}
96
+ `);
89
97
  } else {
90
98
  const payload = {
91
99
  error: { code: code3, message }
92
100
  };
93
101
  if (hint) payload.error.hint = hint;
94
102
  if (meta) payload.error.meta = meta;
103
+ if (warnings.length > 0) payload.error.warnings = warnings;
95
104
  process.stderr.write(JSON.stringify(payload, null, 2) + "\n");
96
105
  }
97
106
  }
107
+ function operationWarnings(value) {
108
+ if (!Array.isArray(value)) return [];
109
+ return value.flatMap((warning) => {
110
+ if (warning === null || typeof warning !== "object") return [];
111
+ const { code: code3, reason, grantCid } = warning;
112
+ if (typeof code3 !== "string" || !WARNING_CODE.test(code3) || typeof reason !== "string" || !WARNING_REASON.test(reason)) {
113
+ return [];
114
+ }
115
+ return [{ code: code3, reason, ...typeof grantCid === "string" && DELEGATION_CID.test(grantCid) ? { grantCid } : {} }];
116
+ });
117
+ }
98
118
  function isInteractive() {
99
119
  return Boolean(process.stdout.isTTY);
100
120
  }
121
+ function setJsonOutputRequested(requested) {
122
+ globalThis[JSON_REQUESTED] = requested;
123
+ }
124
+ function shouldOutputJson() {
125
+ return !isInteractive() || (globalThis[JSON_REQUESTED] ?? process.argv.includes("--json"));
126
+ }
127
+ var WARNING_CODE, WARNING_REASON, DELEGATION_CID, JSON_REQUESTED;
101
128
  var init_formatter = __esm({
102
129
  "src/output/formatter.ts"() {
103
130
  "use strict";
104
131
  init_theme();
132
+ WARNING_CODE = /^[A-Z][A-Z_]{0,63}$/;
133
+ WARNING_REASON = /^[a-z][a-z_]{0,63}$/;
134
+ DELEGATION_CID = /^bafkr4i[a-z2-7]{52}$/;
135
+ JSON_REQUESTED = /* @__PURE__ */ Symbol.for("tinycloud.cli.jsonOutputRequested");
105
136
  }
106
137
  });
107
138
 
@@ -261,7 +292,10 @@ function handleError(error) {
261
292
  meta.resource = capability.resource;
262
293
  meta.requiredAction = capability.requiredAction;
263
294
  }
264
- outputError(cliError.code, cliError.message, hint, Object.keys(meta).length ? meta : void 0);
295
+ outputError(cliError.code, cliError.message, hint, {
296
+ ...Object.keys(meta).length ? { meta } : {},
297
+ warnings: operationWarnings(cliError.metadata?.warnings)
298
+ });
265
299
  process.exit(cliError.exitCode);
266
300
  }
267
301
  function buildAuthHint(error) {
@@ -5738,6 +5772,37 @@ var init_zod = __esm({
5738
5772
  }
5739
5773
  });
5740
5774
 
5775
+ // src/auth/session-expired.ts
5776
+ function signInAgainHint(profileName, posture) {
5777
+ if (posture === "local-owner-key") {
5778
+ return `Sign in again with: tc --profile ${profileName} auth login --method local`;
5779
+ }
5780
+ if (posture === "delegate-session") {
5781
+ return `Have the owner approve a new scoped login: tc --profile ${profileName} auth login --method openkey --paste --manifest <manifest.json>. Pass the owner's code on stdin, newline-terminated.`;
5782
+ }
5783
+ return `Sign in again with: tc --profile ${profileName} auth login --method openkey`;
5784
+ }
5785
+ function sessionExpiredError(profileName, posture) {
5786
+ return new CLIError(
5787
+ "AUTH_REQUIRED",
5788
+ `The session for profile "${profileName}" has expired or is no longer valid.`,
5789
+ ExitCode.AUTH_REQUIRED,
5790
+ { hint: signInAgainHint(profileName, posture), reason: SESSION_EXPIRED_REASON }
5791
+ );
5792
+ }
5793
+ function isSessionExpiredError(error) {
5794
+ return error instanceof CLIError && error.metadata?.reason === SESSION_EXPIRED_REASON;
5795
+ }
5796
+ var SESSION_EXPIRED_REASON;
5797
+ var init_session_expired = __esm({
5798
+ "src/auth/session-expired.ts"() {
5799
+ "use strict";
5800
+ init_constants();
5801
+ init_errors();
5802
+ SESSION_EXPIRED_REASON = "session_expired";
5803
+ }
5804
+ });
5805
+
5741
5806
  // src/lib/raw-encryption.ts
5742
5807
  var init_raw_encryption = __esm({
5743
5808
  "src/lib/raw-encryption.ts"() {
@@ -5800,13 +5865,14 @@ async function replayAdditionalDelegations(node, profile, options) {
5800
5865
  for (const stored of entries) {
5801
5866
  const kind = storedDelegationKind(stored);
5802
5867
  if (kind === "compact" || kind === "signed-login") {
5803
- const installed = await replayStoredDelegation(activator, stored, {
5868
+ const replay = await replayStoredDelegation(activator, stored, {
5804
5869
  host: options.host,
5805
5870
  migrated,
5806
5871
  resolveSpace
5807
5872
  });
5808
- if (installed === void 0 && process.env.TC_DEBUG_REPLAY === "1") {
5809
- process.stderr.write("[replay] skipping a stored delegation refused by validation or its request binding\n");
5873
+ if (replay.status === "skipped" && process.env.TC_DEBUG_REPLAY === "1") {
5874
+ process.stderr.write(`[replay] skipping a stored delegation: ${replay.reason}
5875
+ `);
5810
5876
  }
5811
5877
  continue;
5812
5878
  }
@@ -5884,6 +5950,16 @@ function signerJwkForProfile(profileName, sessionJwk, key) {
5884
5950
  }
5885
5951
  );
5886
5952
  }
5953
+ async function restoreProfileSession(node, profileName, profile, sessionData) {
5954
+ try {
5955
+ await node.restoreSession(sessionData);
5956
+ } catch (error) {
5957
+ if (typeof error === "object" && error !== null && "code" in error && error.code === "AUTH_EXPIRED") {
5958
+ throw sessionExpiredError(profileName, profile === null ? void 0 : resolveProfilePosture(profile));
5959
+ }
5960
+ throw error;
5961
+ }
5962
+ }
5887
5963
  async function createSDKInstance(ctx, options) {
5888
5964
  const profile = options?.privateKey ? await ProfileManager.getProfile(ctx.profile).catch(() => null) : await ProfileManager.getProfile(ctx.profile);
5889
5965
  const session = await ProfileManager.getSession(ctx.profile);
@@ -5903,7 +5979,7 @@ async function createSDKInstance(ctx, options) {
5903
5979
  });
5904
5980
  let restoredOwnSession2 = false;
5905
5981
  if (session && session.delegationHeader && session.delegationCid && session.spaceId) {
5906
- await node2.restoreSession({
5982
+ await restoreProfileSession(node2, ctx.profile, profile, {
5907
5983
  delegationHeader: session.delegationHeader,
5908
5984
  delegationCid: session.delegationCid,
5909
5985
  spaceId: session.spaceId,
@@ -5933,7 +6009,7 @@ async function createSDKInstance(ctx, options) {
5933
6009
  if (options?.privateKey) {
5934
6010
  await node.signIn();
5935
6011
  } else if (session && session.delegationHeader && session.delegationCid && session.spaceId) {
5936
- await node.restoreSession({
6012
+ await restoreProfileSession(node, ctx.profile, profile, {
5937
6013
  delegationHeader: session.delegationHeader,
5938
6014
  delegationCid: session.delegationCid,
5939
6015
  spaceId: session.spaceId,
@@ -6077,6 +6153,7 @@ var init_sdk = __esm({
6077
6153
  init_errors();
6078
6154
  init_constants();
6079
6155
  init_permissions();
6156
+ init_session_expired();
6080
6157
  BOOTSTRAP_PROFILE_FIELDS = ["sessionDid", "spaceId"];
6081
6158
  }
6082
6159
  });
@@ -6175,7 +6252,7 @@ function formatBannerLine(version2) {
6175
6252
  }
6176
6253
  function emitBanner(version2) {
6177
6254
  if (bannerEmitted) return;
6178
- if (!isInteractive()) return;
6255
+ if (shouldOutputJson()) return;
6179
6256
  if (process.env.TC_HIDE_BANNER === "1") return;
6180
6257
  bannerEmitted = true;
6181
6258
  process.stderr.write(formatBannerLine(version2) + "\n\n");
@@ -17370,6 +17447,7 @@ function registerShareCommand(program2) {
17370
17447
  init_storage();
17371
17448
  init_profiles();
17372
17449
  init_constants();
17450
+ init_session_expired();
17373
17451
  import { readFile as readFile4 } from "fs/promises";
17374
17452
  import { join as join6 } from "path";
17375
17453
  import { createHash } from "crypto";
@@ -17682,7 +17760,7 @@ function createShareAuthorityAdapters(input = {}) {
17682
17760
  } catch (error) {
17683
17761
  const profileConfig = await ProfileManager.getProfile(profile).catch(() => void 0);
17684
17762
  const code3 = typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
17685
- if (error instanceof InvalidRestoredSessionError || code3 === "AUTH_EXPIRED") {
17763
+ if (error instanceof InvalidRestoredSessionError || code3 === "AUTH_EXPIRED" || isSessionExpiredError(error)) {
17686
17764
  throw new SharePublishAuthorityError({
17687
17765
  kind: "owner-space-unresolved",
17688
17766
  localKey: profileConfig?.authMethod === "local",
@@ -18044,6 +18122,7 @@ function selectedShareProfile() {
18044
18122
  program.name("tc").description("TinyCloud CLI \u2014 self-sovereign storage from the terminal").version(version).option("-p, --profile <name>", "Profile to use").option("-H, --host <url>", "TinyCloud node URL").option("-v, --verbose", "Enable verbose output").option("--no-cache", "Disable caching").option("-q, --quiet", "Suppress non-essential output").option("--json", "Force JSON output");
18045
18123
  program.hook("preAction", async (thisCommand) => {
18046
18124
  const opts = thisCommand.optsWithGlobals();
18125
+ setJsonOutputRequested(opts.json === true);
18047
18126
  const parentName = thisCommand.parent?.name();
18048
18127
  const isShareCommand = parentName === "share" || thisCommand.name() === "share";
18049
18128
  if (!opts.quiet && !isShareCommand) {
@@ -18052,7 +18131,7 @@ program.hook("preAction", async (thisCommand) => {
18052
18131
  const commandName = thisCommand.name();
18053
18132
  const fullCommand = parentName && parentName !== "tc" ? `${parentName} ${commandName}` : commandName;
18054
18133
  const skipGuard = ["tc", "init", "doctor", "completion", "help", "upgrade", "status"].includes(commandName) || fullCommand === "profile create";
18055
- if (!skipGuard && !opts.quiet && isInteractive()) {
18134
+ if (!skipGuard && !opts.quiet && !shouldOutputJson()) {
18056
18135
  try {
18057
18136
  const config = await ProfileManager.getConfig();
18058
18137
  const profileName = opts.profile || config.defaultProfile;