@tinycloud/cli 1.0.0-beta.17 → 1.0.0-beta.18

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
@@ -94,6 +94,11 @@ function outputError(code3, message, hint) {
94
94
  function isInteractive() {
95
95
  return Boolean(process.stdout.isTTY);
96
96
  }
97
+ function formatBytes(bytes) {
98
+ if (bytes < 1024) return `${bytes} B`;
99
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
100
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
101
+ }
97
102
  var init_formatter = __esm({
98
103
  "src/output/formatter.ts"() {
99
104
  "use strict";
@@ -15051,6 +15056,7 @@ function parseDuration(input) {
15051
15056
  }
15052
15057
 
15053
15058
  // src/commands/share.ts
15059
+ init_formatter();
15054
15060
  init_errors();
15055
15061
 
15056
15062
  // src/share/output.ts
@@ -15253,7 +15259,7 @@ function shareCliError(error) {
15253
15259
  }
15254
15260
  if (failure.kind === "scope-denied") {
15255
15261
  const requiredAction = failure.requiredAction === void 0 ? "" : ` (${failure.requiredAction})`;
15256
- const renew = localKey ? `renew it with ${loginHint} using the required capability` : `request the builtin:share-publishing scope with ${loginHint}`;
15262
+ const renew = localKey ? `renew it with ${loginHint} using the required capability` : `verify the session includes the builtin:share-publishing scope; if it does not, request it with ${loginHint}`;
15257
15263
  return new CLIError("PERMISSION_DENIED", `the session lacks ${failure.capability} authority${requiredAction}; ${renew}`, 5);
15258
15264
  }
15259
15265
  if (failure.kind === "lifetime-exceeds-session") {
@@ -15278,6 +15284,13 @@ function shareCliError(error) {
15278
15284
  if (failure.kind === "registry-rejected") {
15279
15285
  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);
15280
15286
  }
15287
+ if (failure.kind === "storage-quota-exceeded") {
15288
+ const sizes = failure.usedBytes === void 0 || failure.limitBytes === void 0 ? "" : ` (${formatBytes(failure.usedBytes)} used of ${formatBytes(failure.limitBytes)} limit)`;
15289
+ return new CLIError("STORAGE_QUOTA_EXCEEDED", `storage quota exceeded${sizes}; nothing was shared`, 4);
15290
+ }
15291
+ if (failure.kind === "upload-failed") {
15292
+ return new CLIError("UPLOAD_FAILED", "share source upload failed; nothing was shared", 4);
15293
+ }
15281
15294
  }
15282
15295
  if (error instanceof ShareNotifyError) {
15283
15296
  return new CLIError("INVALID_ARGUMENT", error.message, 2);
@@ -16897,7 +16910,26 @@ function requiredKvAction(meta) {
16897
16910
  const action = meta.requiredAction;
16898
16911
  return action === "tinycloud.kv/put" || action === "tinycloud.kv/get" ? action : void 0;
16899
16912
  }
16913
+ function isByteCount(value) {
16914
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
16915
+ }
16916
+ function throwKvUploadFailure(error) {
16917
+ if (typeof error === "object" && error !== null && "code" in error && error.code === "STORAGE_QUOTA_EXCEEDED") {
16918
+ const meta = "meta" in error && typeof error.meta === "object" && error.meta !== null ? error.meta : {};
16919
+ const { usedBytes, limitBytes } = meta;
16920
+ throw new SharePublishAuthorityError(isByteCount(usedBytes) && isByteCount(limitBytes) ? { kind: "storage-quota-exceeded", usedBytes, limitBytes } : { kind: "storage-quota-exceeded" });
16921
+ }
16922
+ throw new SharePublishAuthorityError({ kind: "upload-failed" });
16923
+ }
16900
16924
  var DEFAULT_SHARE_ORIGIN = "https://share.tinycloud.xyz";
16925
+ var URI_SAFE_FILENAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
16926
+ function safeStorageFilename(filename) {
16927
+ if (URI_SAFE_FILENAME.test(filename) && !filename.includes("..")) return filename;
16928
+ const dot = filename.lastIndexOf(".");
16929
+ const extension = dot >= 0 && /^[A-Za-z0-9]{1,16}$/.test(filename.slice(dot + 1)) ? filename.slice(dot + 1) : "";
16930
+ const stem = (extension === "" ? filename : filename.slice(0, dot)).normalize("NFKD").replace(/[\u0300-\u036f]/g, "").replace(extension === "" ? /[^A-Za-z0-9_-]+/g : /[^A-Za-z0-9._-]+/g, "-").replace(/\.{2,}/g, ".").replace(/-{2,}/g, "-").slice(0, 100).replace(/^[^A-Za-z0-9]+|[-.]+$/g, "");
16931
+ return `${stem === "" ? "share" : stem}${extension === "" ? "" : `.${extension}`}`;
16932
+ }
16901
16933
  function createEncryptedSessionHistory() {
16902
16934
  const records = /* @__PURE__ */ new Map();
16903
16935
  let keyPromise;
@@ -17129,7 +17161,7 @@ function createShareAuthorityAdapters(input = {}) {
17129
17161
  if (targetInput.target.kind === "bearer") {
17130
17162
  if (resourceKind !== "exact" || files.length !== 1) throw new Error("native bearer publication requires one exact source file");
17131
17163
  const file2 = files[0];
17132
- const resourcePath2 = `xyz.tinycloud.share/shares/${shareId}/${targetInput.filename}`;
17164
+ const resourcePath2 = `xyz.tinycloud.share/shares/${shareId}/${safeStorageFilename(targetInput.filename)}`;
17133
17165
  const written = await node.kvForSpace(ownerSpaceId).put(resourcePath2, file2.bytes.slice(), {
17134
17166
  contentType: targetInput.mediaType ?? file2.mediaType ?? "application/octet-stream"
17135
17167
  });
@@ -17145,7 +17177,7 @@ function createShareAuthorityAdapters(input = {}) {
17145
17177
  profileName: activeProfileName2
17146
17178
  });
17147
17179
  }
17148
- throw new Error("native bearer source upload failed");
17180
+ throwKvUploadFailure(written.error);
17149
17181
  }
17150
17182
  let native;
17151
17183
  try {
@@ -17193,7 +17225,7 @@ function createShareAuthorityAdapters(input = {}) {
17193
17225
  }
17194
17226
  if (resourceKind !== "exact" || files.length !== 1) throw new Error("addressed publication requires a single exact source file");
17195
17227
  const file = files[0];
17196
- const resourcePath = `shares/${shareId}/${targetInput.filename}`;
17228
+ const resourcePath = `shares/${shareId}/${safeStorageFilename(targetInput.filename)}`;
17197
17229
  const byteLength = file.bytes.byteLength;
17198
17230
  if (!Number.isSafeInteger(byteLength) || byteLength > 100 * 1024 * 1024) throw new Error("addressed publication exceeds the combined byte limit");
17199
17231
  const mediaType = targetInput.mediaType ?? file.mediaType ?? "application/octet-stream";
@@ -17228,7 +17260,7 @@ function createShareAuthorityAdapters(input = {}) {
17228
17260
  profileName: activeProfileName2
17229
17261
  });
17230
17262
  }
17231
- throw new Error("addressed source upload failed");
17263
+ throwKvUploadFailure(stored.error);
17232
17264
  }
17233
17265
  const contentSource = {
17234
17266
  shareId,