@tinycloud/cli 1.1.0-beta.12 → 1.1.0-beta.14

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
@@ -32,7 +32,9 @@ var init_constants = __esm({
32
32
  NOT_FOUND: 4,
33
33
  PERMISSION_DENIED: 5,
34
34
  NETWORK_ERROR: 6,
35
- NODE_ERROR: 7
35
+ NODE_ERROR: 7,
36
+ /** A write was refused because the owner's TinyCloud storage is full. */
37
+ STORAGE_FULL: 10
36
38
  };
37
39
  }
38
40
  });
@@ -72,8 +74,14 @@ var init_theme = __esm({
72
74
 
73
75
  // src/output/formatter.ts
74
76
  import ora from "ora";
75
- function outputError(code3, message, hint, meta) {
76
- 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()) {
77
85
  process.stderr.write(
78
86
  `${theme.error("\u2717")} ${theme.label(code3)}: ${message}
79
87
  `
@@ -84,27 +92,127 @@ function outputError(code3, message, hint, meta) {
84
92
  `);
85
93
  }
86
94
  }
95
+ if (warnings.length > 0) process.stderr.write(` ${theme.warn(warningLine(warnings))}
96
+ `);
87
97
  } else {
88
98
  const payload = {
89
99
  error: { code: code3, message }
90
100
  };
91
101
  if (hint) payload.error.hint = hint;
92
102
  if (meta) payload.error.meta = meta;
103
+ if (warnings.length > 0) payload.error.warnings = warnings;
93
104
  process.stderr.write(JSON.stringify(payload, null, 2) + "\n");
94
105
  }
95
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
+ }
96
118
  function isInteractive() {
97
119
  return Boolean(process.stdout.isTTY);
98
120
  }
99
- function formatBytes(bytes) {
100
- if (bytes < 1024) return `${bytes} B`;
101
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
102
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
121
+ function setJsonOutputRequested(requested) {
122
+ globalThis[JSON_REQUESTED] = requested;
103
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;
104
128
  var init_formatter = __esm({
105
129
  "src/output/formatter.ts"() {
106
130
  "use strict";
107
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");
136
+ }
137
+ });
138
+
139
+ // src/output/storage.ts
140
+ function storageRejection(error) {
141
+ let node = error;
142
+ for (let depth = 0; depth < MAX_CAUSE_DEPTH && typeof node === "object" && node !== null; depth += 1) {
143
+ const record = node;
144
+ let code3;
145
+ if (record.code === "STORAGE_QUOTA_EXCEEDED" || record.code === "STORAGE_LIMIT_REACHED") {
146
+ code3 = record.code;
147
+ } else if (record.code !== void 0) {
148
+ return void 0;
149
+ } else if (isStorageStatus(record)) {
150
+ code3 = storageCodeFromText(record.message);
151
+ }
152
+ if (code3) {
153
+ const account = accountTotals(record) ?? accountTotals(record.meta) ?? accountTotals(record.metadata);
154
+ return account ? { code: code3, account } : { code: code3 };
155
+ }
156
+ node = record.cause;
157
+ }
158
+ return void 0;
159
+ }
160
+ function describeStorageRejection(rejection, progress) {
161
+ const reason = rejection.code === "STORAGE_LIMIT_REACHED" ? "This write is larger than the TinyCloud storage you have left" : "TinyCloud storage is full";
162
+ const message = progress ? `${progress}${reason}.` : `${reason}; nothing was written.`;
163
+ const { account } = rejection;
164
+ const totals = account ? `${formatStorageBytes(account.usedBytes)} used of ${formatStorageBytes(account.limitBytes)}${account.plan ? ` (${account.plan} plan)` : ""}. ` : "";
165
+ return {
166
+ message,
167
+ hint: `${totals}Reading still works.
168
+ Free up space or upgrade: ${MANAGE_STORAGE_URL}`
169
+ };
170
+ }
171
+ function formatStorageBytes(bytes) {
172
+ const units = ["KiB", "MiB", "GiB", "TiB"];
173
+ if (bytes < 1024) return `${bytes} B`;
174
+ let value = bytes / 1024;
175
+ let unit = 0;
176
+ while (value >= 1024 && unit < units.length - 1) {
177
+ value /= 1024;
178
+ unit += 1;
179
+ }
180
+ return `${value.toFixed(1).replace(/\.0$/, "")} ${units[unit]}`;
181
+ }
182
+ function storageCodeFromText(message) {
183
+ if (typeof message !== "string") return void 0;
184
+ if (message.includes("larger than the TinyCloud storage you have left")) return "STORAGE_LIMIT_REACHED";
185
+ if (message.includes("TinyCloud storage is full") || /storage quota exceeded/i.test(message)) {
186
+ return "STORAGE_QUOTA_EXCEEDED";
187
+ }
188
+ return void 0;
189
+ }
190
+ function isStorageStatus(record) {
191
+ const meta = "meta" in record && typeof record.meta === "object" && record.meta !== null ? record.meta : {};
192
+ const statuses = [
193
+ "status" in record ? record.status : void 0,
194
+ "statusCode" in record ? record.statusCode : void 0,
195
+ "status" in meta ? meta.status : void 0
196
+ ];
197
+ return statuses.some((value) => value === 402 || value === 413);
198
+ }
199
+ function accountTotals(meta) {
200
+ if (typeof meta !== "object" || meta === null) return void 0;
201
+ const account = meta.account;
202
+ if (typeof account !== "object" || account === null) return void 0;
203
+ const { usedBytes, limitBytes, plan } = account;
204
+ if (!isByteCount(usedBytes) || !isByteCount(limitBytes) || limitBytes === 0) return void 0;
205
+ return typeof plan === "string" && /^[A-Za-z0-9][A-Za-z0-9 _.-]{0,31}$/.test(plan) ? { usedBytes, limitBytes, plan } : { usedBytes, limitBytes };
206
+ }
207
+ function isByteCount(value) {
208
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
209
+ }
210
+ var MANAGE_STORAGE_URL, MAX_CAUSE_DEPTH;
211
+ var init_storage = __esm({
212
+ "src/output/storage.ts"() {
213
+ "use strict";
214
+ MANAGE_STORAGE_URL = "https://account.tinycloud.xyz/billing";
215
+ MAX_CAUSE_DEPTH = 4;
108
216
  }
109
217
  });
110
218
 
@@ -117,6 +225,8 @@ function setActiveProfileName(name) {
117
225
  activeProfileName = name;
118
226
  }
119
227
  function wrapError(error) {
228
+ const storageFull = storageFullError(error);
229
+ if (storageFull) return storageFull;
120
230
  if (error instanceof CLIError) return error;
121
231
  const message = error instanceof Error ? error.message : String(error);
122
232
  const verdict = authorizationVerdictOf(error);
@@ -163,6 +273,13 @@ function missingPrivateKeyError() {
163
273
  { hint: `Sign in again with: tc --profile ${profileName} auth login --method openkey` }
164
274
  );
165
275
  }
276
+ function storageFullError(error, progress) {
277
+ if (error instanceof CLIError && error.exitCode === ExitCode.STORAGE_FULL) return error;
278
+ const rejection = storageRejection(error);
279
+ if (!rejection) return void 0;
280
+ const { message, hint } = describeStorageRejection(rejection, progress);
281
+ return new CLIError(rejection.code, message, ExitCode.STORAGE_FULL, { hint });
282
+ }
166
283
  function handleError(error) {
167
284
  const cliError = wrapError(error);
168
285
  const prebuilt = typeof cliError.metadata?.hint === "string" ? cliError.metadata.hint : void 0;
@@ -175,7 +292,10 @@ function handleError(error) {
175
292
  meta.resource = capability.resource;
176
293
  meta.requiredAction = capability.requiredAction;
177
294
  }
178
- 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
+ });
179
299
  process.exit(cliError.exitCode);
180
300
  }
181
301
  function buildAuthHint(error) {
@@ -245,6 +365,7 @@ var init_errors = __esm({
245
365
  "use strict";
246
366
  init_constants();
247
367
  init_formatter();
368
+ init_storage();
248
369
  CLIError = class extends Error {
249
370
  constructor(code3, message, exitCode = ExitCode.ERROR, metadata) {
250
371
  super(message);
@@ -312,7 +433,7 @@ async function listDirs(dirPath) {
312
433
  }
313
434
  }
314
435
  var PRIVATE_FILE_MODE, PRIVATE_DIR_MODE;
315
- var init_storage = __esm({
436
+ var init_storage2 = __esm({
316
437
  "src/config/storage.ts"() {
317
438
  "use strict";
318
439
  PRIVATE_FILE_MODE = 384;
@@ -570,7 +691,7 @@ var init_profiles = __esm({
570
691
  "src/config/profiles.ts"() {
571
692
  "use strict";
572
693
  init_constants();
573
- init_storage();
694
+ init_storage2();
574
695
  init_errors();
575
696
  ProfileManager = class _ProfileManager {
576
697
  // ── Initialization ──────────────────────────────────────────────────
@@ -5651,6 +5772,37 @@ var init_zod = __esm({
5651
5772
  }
5652
5773
  });
5653
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
+
5654
5806
  // src/lib/raw-encryption.ts
5655
5807
  var init_raw_encryption = __esm({
5656
5808
  "src/lib/raw-encryption.ts"() {
@@ -5713,13 +5865,14 @@ async function replayAdditionalDelegations(node, profile, options) {
5713
5865
  for (const stored of entries) {
5714
5866
  const kind = storedDelegationKind(stored);
5715
5867
  if (kind === "compact" || kind === "signed-login") {
5716
- const installed = await replayStoredDelegation(activator, stored, {
5868
+ const replay = await replayStoredDelegation(activator, stored, {
5717
5869
  host: options.host,
5718
5870
  migrated,
5719
5871
  resolveSpace
5720
5872
  });
5721
- if (installed === void 0 && process.env.TC_DEBUG_REPLAY === "1") {
5722
- 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
+ `);
5723
5876
  }
5724
5877
  continue;
5725
5878
  }
@@ -5750,7 +5903,7 @@ var init_permissions = __esm({
5750
5903
  "src/lib/permissions.ts"() {
5751
5904
  "use strict";
5752
5905
  init_constants();
5753
- init_storage();
5906
+ init_storage2();
5754
5907
  init_profiles();
5755
5908
  init_errors();
5756
5909
  init_constants();
@@ -5797,6 +5950,16 @@ function signerJwkForProfile(profileName, sessionJwk, key) {
5797
5950
  }
5798
5951
  );
5799
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
+ }
5800
5963
  async function createSDKInstance(ctx, options) {
5801
5964
  const profile = options?.privateKey ? await ProfileManager.getProfile(ctx.profile).catch(() => null) : await ProfileManager.getProfile(ctx.profile);
5802
5965
  const session = await ProfileManager.getSession(ctx.profile);
@@ -5816,7 +5979,7 @@ async function createSDKInstance(ctx, options) {
5816
5979
  });
5817
5980
  let restoredOwnSession2 = false;
5818
5981
  if (session && session.delegationHeader && session.delegationCid && session.spaceId) {
5819
- await node2.restoreSession({
5982
+ await restoreProfileSession(node2, ctx.profile, profile, {
5820
5983
  delegationHeader: session.delegationHeader,
5821
5984
  delegationCid: session.delegationCid,
5822
5985
  spaceId: session.spaceId,
@@ -5846,7 +6009,7 @@ async function createSDKInstance(ctx, options) {
5846
6009
  if (options?.privateKey) {
5847
6010
  await node.signIn();
5848
6011
  } else if (session && session.delegationHeader && session.delegationCid && session.spaceId) {
5849
- await node.restoreSession({
6012
+ await restoreProfileSession(node, ctx.profile, profile, {
5850
6013
  delegationHeader: session.delegationHeader,
5851
6014
  delegationCid: session.delegationCid,
5852
6015
  spaceId: session.spaceId,
@@ -5990,6 +6153,7 @@ var init_sdk = __esm({
5990
6153
  init_errors();
5991
6154
  init_constants();
5992
6155
  init_permissions();
6156
+ init_session_expired();
5993
6157
  BOOTSTRAP_PROFILE_FIELDS = ["sessionDid", "spaceId"];
5994
6158
  }
5995
6159
  });
@@ -6088,7 +6252,7 @@ function formatBannerLine(version2) {
6088
6252
  }
6089
6253
  function emitBanner(version2) {
6090
6254
  if (bannerEmitted) return;
6091
- if (!isInteractive()) return;
6255
+ if (shouldOutputJson()) return;
6092
6256
  if (process.env.TC_HIDE_BANNER === "1") return;
6093
6257
  bannerEmitted = true;
6094
6258
  process.stderr.write(formatBannerLine(version2) + "\n\n");
@@ -16699,7 +16863,6 @@ function parseDuration(input) {
16699
16863
  }
16700
16864
 
16701
16865
  // src/commands/share.ts
16702
- init_formatter();
16703
16866
  init_errors();
16704
16867
 
16705
16868
  // src/share/output.ts
@@ -16965,9 +17128,8 @@ function shareCliError(error, operation = "publish") {
16965
17128
  if (failure.kind === "registry-rejected") {
16966
17129
  return new CLIError("REGISTRY_REJECTED", "the TinyCloud location registry rejected this session's location record, so nothing was shared; retrying will not help. Log in again, and report the problem if it persists", 6);
16967
17130
  }
16968
- if (failure.kind === "storage-quota-exceeded") {
16969
- const sizes = failure.usedBytes === void 0 || failure.limitBytes === void 0 ? "" : ` (${formatBytes(failure.usedBytes)} used of ${formatBytes(failure.limitBytes)} limit)`;
16970
- return new CLIError("STORAGE_QUOTA_EXCEEDED", `storage quota exceeded${sizes}; nothing was shared`, 4);
17131
+ if (failure.kind === "storage-full") {
17132
+ return storageFullError(failure);
16971
17133
  }
16972
17134
  if (failure.kind === "upload-failed") {
16973
17135
  return new CLIError("UPLOAD_FAILED", "share source upload failed; nothing was shared", 4);
@@ -16987,6 +17149,8 @@ function shareCliError(error, operation = "publish") {
16987
17149
  const code3 = error.code === "fetch-failed" ? "NOT_FOUND" : error.code.replaceAll("-", "_").toUpperCase();
16988
17150
  return new CLIError(code3, error.message, exit);
16989
17151
  }
17152
+ const storageFull = storageFullError(error);
17153
+ if (storageFull) return storageFull;
16990
17154
  const message = error instanceof Error ? error.message : String(error);
16991
17155
  const nodeCode = typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
16992
17156
  const known = {
@@ -17280,8 +17444,10 @@ function registerShareCommand(program2) {
17280
17444
  }
17281
17445
 
17282
17446
  // src/share/adapters.ts
17447
+ init_storage();
17283
17448
  init_profiles();
17284
17449
  init_constants();
17450
+ init_session_expired();
17285
17451
  import { readFile as readFile4 } from "fs/promises";
17286
17452
  import { join as join6 } from "path";
17287
17453
  import { createHash } from "crypto";
@@ -17293,15 +17459,9 @@ function requiredKvAction(meta) {
17293
17459
  const action = meta.requiredAction;
17294
17460
  return action === "tinycloud.kv/put" || action === "tinycloud.kv/get" ? action : void 0;
17295
17461
  }
17296
- function isByteCount(value) {
17297
- return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
17298
- }
17299
17462
  function throwKvUploadFailure(error) {
17300
- if (typeof error === "object" && error !== null && "code" in error && error.code === "STORAGE_QUOTA_EXCEEDED") {
17301
- const meta = "meta" in error && typeof error.meta === "object" && error.meta !== null ? error.meta : {};
17302
- const { usedBytes, limitBytes } = meta;
17303
- throw new SharePublishAuthorityError(isByteCount(usedBytes) && isByteCount(limitBytes) ? { kind: "storage-quota-exceeded", usedBytes, limitBytes } : { kind: "storage-quota-exceeded" });
17304
- }
17463
+ const rejection = storageRejection(error);
17464
+ if (rejection) throw new SharePublishAuthorityError({ kind: "storage-full", ...rejection });
17305
17465
  throw new SharePublishAuthorityError({ kind: "upload-failed" });
17306
17466
  }
17307
17467
  var DEFAULT_SHARE_ORIGIN = "https://share.tinycloud.xyz";
@@ -17600,7 +17760,7 @@ function createShareAuthorityAdapters(input = {}) {
17600
17760
  } catch (error) {
17601
17761
  const profileConfig = await ProfileManager.getProfile(profile).catch(() => void 0);
17602
17762
  const code3 = typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
17603
- if (error instanceof InvalidRestoredSessionError || code3 === "AUTH_EXPIRED") {
17763
+ if (error instanceof InvalidRestoredSessionError || code3 === "AUTH_EXPIRED" || isSessionExpiredError(error)) {
17604
17764
  throw new SharePublishAuthorityError({
17605
17765
  kind: "owner-space-unresolved",
17606
17766
  localKey: profileConfig?.authMethod === "local",
@@ -17962,6 +18122,7 @@ function selectedShareProfile() {
17962
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");
17963
18123
  program.hook("preAction", async (thisCommand) => {
17964
18124
  const opts = thisCommand.optsWithGlobals();
18125
+ setJsonOutputRequested(opts.json === true);
17965
18126
  const parentName = thisCommand.parent?.name();
17966
18127
  const isShareCommand = parentName === "share" || thisCommand.name() === "share";
17967
18128
  if (!opts.quiet && !isShareCommand) {
@@ -17970,7 +18131,7 @@ program.hook("preAction", async (thisCommand) => {
17970
18131
  const commandName = thisCommand.name();
17971
18132
  const fullCommand = parentName && parentName !== "tc" ? `${parentName} ${commandName}` : commandName;
17972
18133
  const skipGuard = ["tc", "init", "doctor", "completion", "help", "upgrade", "status"].includes(commandName) || fullCommand === "profile create";
17973
- if (!skipGuard && !opts.quiet && isInteractive()) {
18134
+ if (!skipGuard && !opts.quiet && !shouldOutputJson()) {
17974
18135
  try {
17975
18136
  const config = await ProfileManager.getConfig();
17976
18137
  const profileName = opts.profile || config.defaultProfile;