@tinycloud/cli 1.0.0-beta.19 → 1.0.0-beta.21

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.
@@ -20384,6 +20384,17 @@ var __export2 = (target, all) => {
20384
20384
  for (var name in all)
20385
20385
  __defProp2(target, name, { get: all[name], enumerable: true });
20386
20386
  };
20387
+ var UNSAFE_FILENAME_CODE_POINT = /[\p{Cc}\p{Cf}\p{Cs}\u2028\u2029]/u;
20388
+ function hasUnsafeFilenameCodePoint(value) {
20389
+ return UNSAFE_FILENAME_CODE_POINT.test(value);
20390
+ }
20391
+ function canonicalShareFilename(value) {
20392
+ const canonical = value.normalize("NFC");
20393
+ if (canonical.length === 0 || canonical === "." || canonical === ".." || canonical.includes("/") || canonical.includes("\\") || hasUnsafeFilenameCodePoint(canonical)) {
20394
+ throw new TypeError("share filename is unsafe");
20395
+ }
20396
+ return canonical;
20397
+ }
20387
20398
  var external_exports2 = {};
20388
20399
  __export2(external_exports2, {
20389
20400
  BRAND: () => BRAND2,
@@ -26519,8 +26530,17 @@ function normalizeShareTarget(target) {
26519
26530
  function targetAuthorizationMethod(target) {
26520
26531
  return authorizationMethodForTarget(target);
26521
26532
  }
26533
+ function publishFilename(value) {
26534
+ try {
26535
+ return canonicalShareFilename(value);
26536
+ } catch {
26537
+ throw new SharePublishError("invalid-argument", hasUnsafeFilenameCodePoint(value) ? "filename contains control or invisible characters" : "filename must be one safe path segment");
26538
+ }
26539
+ }
26522
26540
  async function publishTargetShare(input) {
26523
26541
  const target = normalizeShareTarget(input.target);
26542
+ const filename = publishFilename(input.filename);
26543
+ const files = input.files?.map((file) => ({ ...file, filename: publishFilename(file.filename) }));
26524
26544
  if (input.targetAdapter === void 0) {
26525
26545
  if (target.kind === "bearer") throw new SharePublishError("authority-required", "native bearer publication requires an authenticated TinyCloud node");
26526
26546
  return {
@@ -26546,9 +26566,6 @@ async function publishTargetShare(input) {
26546
26566
  return bytes3;
26547
26567
  })();
26548
26568
  if (source.byteLength === 0) throw new SharePublishError("invalid-argument", "share content is empty");
26549
- if (!input.filename || input.filename === "." || input.filename === ".." || /[/\\\u0000-\u001f\u007f]/.test(input.filename)) {
26550
- throw new SharePublishError("invalid-argument", "filename must be one safe path segment");
26551
- }
26552
26569
  if (target.kind === "bearer" && input.allowBinary !== true) {
26553
26570
  try {
26554
26571
  new TextDecoder("utf-8", { fatal: true }).decode(source);
@@ -26556,10 +26573,10 @@ async function publishTargetShare(input) {
26556
26573
  throw new SharePublishError("invalid-argument", "Markdown input must be valid UTF-8");
26557
26574
  }
26558
26575
  }
26559
- const files = input.files === void 0 || input.files.length === 0 ? [{ bytes: source }] : input.files;
26576
+ const publishFiles = files === void 0 || files.length === 0 ? [{ bytes: source }] : files;
26560
26577
  const limit = input.maxBytes ?? SHARE_CONTENT_LIMIT;
26561
26578
  let totalBytes = 0;
26562
- for (const file of files) {
26579
+ for (const file of publishFiles) {
26563
26580
  totalBytes += file.bytes.byteLength;
26564
26581
  if (!Number.isSafeInteger(totalBytes) || totalBytes > limit) {
26565
26582
  throw new SharePublishError("max-bytes-exceeded", "share publication exceeds the combined byte limit");
@@ -26570,8 +26587,8 @@ async function publishTargetShare(input) {
26570
26587
  if (!Number.isFinite(expiresAt.getTime()) || expiresAt.getTime() <= nowMs2) throw new SharePublishError("invalid-argument", "expiresAt must be a valid future time");
26571
26588
  return input.targetAdapter.publish({
26572
26589
  source,
26573
- filename: input.filename,
26574
- ...input.files === void 0 ? {} : { files: input.files },
26590
+ filename,
26591
+ ...files === void 0 ? {} : { files },
26575
26592
  ...input.mediaType === void 0 ? {} : { mediaType: input.mediaType },
26576
26593
  ...input.resourceKind === void 0 ? {} : { resourceKind: input.resourceKind },
26577
26594
  ...input.actions === void 0 ? {} : { actions: input.actions },
@@ -27302,6 +27319,7 @@ import { constants } from "fs";
27302
27319
  import { lstat, mkdir as mkdir3, mkdtemp, open as open3, readFile as readFile9, realpath, stat as stat2, link, rename as rename2, rm as rm3, unlink } from "fs/promises";
27303
27320
  import { randomBytes as randomBytes4 } from "crypto";
27304
27321
  import { basename as basename2, join as join6, resolve as resolve2, sep } from "path";
27322
+ init_errors();
27305
27323
  var MAX_SHARE_STDIN_BYTES = 100 * 1024 * 1024;
27306
27324
  var MAX_SHARE_URL_BYTES = 64 * 1024;
27307
27325
  async function readBoundedStdin(limit = MAX_SHARE_STDIN_BYTES) {
@@ -27323,21 +27341,28 @@ async function readBoundedUrlStdin() {
27323
27341
  return value;
27324
27342
  }
27325
27343
  function safeFilename(value) {
27326
- if (value.length === 0 || value === "." || value === ".." || /[/\\\u0000-\u001f\u007f]/.test(value)) throw new Error("UNSAFE_FILENAME");
27327
- return value;
27344
+ return shareFilename(value);
27328
27345
  }
27329
27346
  function shareFilename(value) {
27330
- return safeFilename(value);
27347
+ try {
27348
+ return canonicalShareFilename(value);
27349
+ } catch {
27350
+ throw new CLIError(
27351
+ "UNSAFE_FILENAME",
27352
+ hasUnsafeFilenameCodePoint(value) ? "filename contains control or invisible characters" : "filename must be one safe path segment",
27353
+ 8
27354
+ );
27355
+ }
27356
+ }
27357
+ function shareInputFilename(input, name) {
27358
+ return shareFilename(name ?? (input === "-" ? "stdin.md" : basename2(resolve2(input))));
27331
27359
  }
27332
27360
  async function readShareInput(input, name, limit = MAX_SHARE_STDIN_BYTES) {
27333
- if (input === "-") {
27334
- const bytes2 = await readBoundedStdin(limit);
27335
- return { bytes: bytes2, filename: shareFilename(name ?? "stdin.md") };
27336
- }
27361
+ const filename = shareInputFilename(input, name);
27362
+ if (input === "-") return { bytes: await readBoundedStdin(limit), filename };
27337
27363
  const path = resolve2(input);
27338
27364
  const info = await stat2(path);
27339
27365
  if (!info.isFile() || info.size > limit) throw new Error("MAX_BYTES_EXCEEDED");
27340
- const filename = shareFilename(name ?? basename2(path));
27341
27366
  const bytes = new Uint8Array(await readFile9(path));
27342
27367
  if (bytes.byteLength > limit) throw new Error("MAX_BYTES_EXCEEDED");
27343
27368
  return { bytes, filename };
@@ -27454,6 +27479,14 @@ function shareCliError(error) {
27454
27479
  const localKey = "localKey" in failure && failure.localKey === true;
27455
27480
  const profileHint = profileName === void 0 ? "" : `--profile ${profileName} `;
27456
27481
  const loginHint = localKey ? `\`tc ${profileHint}auth login --method local\`` : `\`tc ${profileHint}auth login --device --manifest builtin:share-publishing\` (or \`tc ${profileHint}enable share\`)`;
27482
+ if (failure.kind === "caveated-session") {
27483
+ const holder = profileName === void 0 ? "this session" : `profile ${profileName}'s session`;
27484
+ return new CLIError(
27485
+ "PERMISSION_DENIED",
27486
+ `${holder} carries signed restrictions (caveats) on the authority an anyone-with-link share needs, so it cannot create the share link; nothing was shared. Approve Share publishing without restrictions on a new, dedicated profile (any unused name): \`tc init --name publisher --key-only && tc --profile publisher enable share\``,
27487
+ 5
27488
+ );
27489
+ }
27457
27490
  if (failure.kind === "owner-space-unresolved") {
27458
27491
  return new CLIError("AUTH_REQUIRED", `a valid signed TinyCloud session is required; run ${loginHint}`, 3);
27459
27492
  }
@@ -27582,7 +27615,9 @@ function registerShareCommand(program) {
27582
27615
  const json = jsonOutput(options, command);
27583
27616
  const maxBytes = byteLimit(options.maxBytes);
27584
27617
  if (files.length === 0 || files.includes("-") && files.length > 1) throw new CLIError("INVALID_ARGUMENT", "stdin must be the only publish input", 2);
27585
- const inputs = await Promise.all(files.map((file) => readShareInput(file, files.length === 1 ? options.name : void 0, maxBytes)));
27618
+ const name = files.length === 1 ? options.name : void 0;
27619
+ for (const file of files) shareInputFilename(file, name);
27620
+ const inputs = await Promise.all(files.map((file) => readShareInput(file, name, maxBytes)));
27586
27621
  assertAggregateInputLimit(inputs, maxBytes);
27587
27622
  const target = parseShareTarget(options.to);
27588
27623
  const actions = requestedActions(options.action);