@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.
package/dist/index.js CHANGED
@@ -7735,6 +7735,17 @@ var __export2 = (target, all) => {
7735
7735
  for (var name in all)
7736
7736
  __defProp2(target, name, { get: all[name], enumerable: true });
7737
7737
  };
7738
+ var UNSAFE_FILENAME_CODE_POINT = /[\p{Cc}\p{Cf}\p{Cs}\u2028\u2029]/u;
7739
+ function hasUnsafeFilenameCodePoint(value) {
7740
+ return UNSAFE_FILENAME_CODE_POINT.test(value);
7741
+ }
7742
+ function canonicalShareFilename(value) {
7743
+ const canonical = value.normalize("NFC");
7744
+ if (canonical.length === 0 || canonical === "." || canonical === ".." || canonical.includes("/") || canonical.includes("\\") || hasUnsafeFilenameCodePoint(canonical)) {
7745
+ throw new TypeError("share filename is unsafe");
7746
+ }
7747
+ return canonical;
7748
+ }
7738
7749
  var external_exports = {};
7739
7750
  __export2(external_exports, {
7740
7751
  BRAND: () => BRAND,
@@ -13908,8 +13919,17 @@ function normalizeShareTarget(target) {
13908
13919
  function targetAuthorizationMethod(target) {
13909
13920
  return authorizationMethodForTarget(target);
13910
13921
  }
13922
+ function publishFilename(value) {
13923
+ try {
13924
+ return canonicalShareFilename(value);
13925
+ } catch {
13926
+ throw new SharePublishError("invalid-argument", hasUnsafeFilenameCodePoint(value) ? "filename contains control or invisible characters" : "filename must be one safe path segment");
13927
+ }
13928
+ }
13911
13929
  async function publishTargetShare(input) {
13912
13930
  const target = normalizeShareTarget(input.target);
13931
+ const filename = publishFilename(input.filename);
13932
+ const files = input.files?.map((file) => ({ ...file, filename: publishFilename(file.filename) }));
13913
13933
  if (input.targetAdapter === void 0) {
13914
13934
  if (target.kind === "bearer") throw new SharePublishError("authority-required", "native bearer publication requires an authenticated TinyCloud node");
13915
13935
  return {
@@ -13935,9 +13955,6 @@ async function publishTargetShare(input) {
13935
13955
  return bytes3;
13936
13956
  })();
13937
13957
  if (source.byteLength === 0) throw new SharePublishError("invalid-argument", "share content is empty");
13938
- if (!input.filename || input.filename === "." || input.filename === ".." || /[/\\\u0000-\u001f\u007f]/.test(input.filename)) {
13939
- throw new SharePublishError("invalid-argument", "filename must be one safe path segment");
13940
- }
13941
13958
  if (target.kind === "bearer" && input.allowBinary !== true) {
13942
13959
  try {
13943
13960
  new TextDecoder("utf-8", { fatal: true }).decode(source);
@@ -13945,10 +13962,10 @@ async function publishTargetShare(input) {
13945
13962
  throw new SharePublishError("invalid-argument", "Markdown input must be valid UTF-8");
13946
13963
  }
13947
13964
  }
13948
- const files = input.files === void 0 || input.files.length === 0 ? [{ bytes: source }] : input.files;
13965
+ const publishFiles = files === void 0 || files.length === 0 ? [{ bytes: source }] : files;
13949
13966
  const limit = input.maxBytes ?? SHARE_CONTENT_LIMIT;
13950
13967
  let totalBytes = 0;
13951
- for (const file of files) {
13968
+ for (const file of publishFiles) {
13952
13969
  totalBytes += file.bytes.byteLength;
13953
13970
  if (!Number.isSafeInteger(totalBytes) || totalBytes > limit) {
13954
13971
  throw new SharePublishError("max-bytes-exceeded", "share publication exceeds the combined byte limit");
@@ -13959,8 +13976,8 @@ async function publishTargetShare(input) {
13959
13976
  if (!Number.isFinite(expiresAt.getTime()) || expiresAt.getTime() <= nowMs) throw new SharePublishError("invalid-argument", "expiresAt must be a valid future time");
13960
13977
  return input.targetAdapter.publish({
13961
13978
  source,
13962
- filename: input.filename,
13963
- ...input.files === void 0 ? {} : { files: input.files },
13979
+ filename,
13980
+ ...files === void 0 ? {} : { files },
13964
13981
  ...input.mediaType === void 0 ? {} : { mediaType: input.mediaType },
13965
13982
  ...input.resourceKind === void 0 ? {} : { resourceKind: input.resourceKind },
13966
13983
  ...input.actions === void 0 ? {} : { actions: input.actions },
@@ -14103,7 +14120,11 @@ function mailboxCredentialCommitment(target) {
14103
14120
  };
14104
14121
  }
14105
14122
  function prepareAddressedShare(request) {
14106
- if (request.filename.length === 0 || request.filename === "." || request.filename === ".." || /[/\\\u0000-\u001f\u007f]/.test(request.filename)) throw new TypeError("addressed filename is invalid");
14123
+ try {
14124
+ canonicalShareFilename(request.filename);
14125
+ } catch {
14126
+ throw new TypeError("addressed filename is invalid");
14127
+ }
14107
14128
  if (request.actions.length === 0 || request.policyActions.length === 0) throw new TypeError("addressed share actions are empty");
14108
14129
  const target = normalizeShareTarget(request.target);
14109
14130
  if (target.kind === "bearer") throw new TypeError("addressed target is required");
@@ -14121,9 +14142,10 @@ function assertMailboxCommitment(commitment, target) {
14121
14142
  if (canonicalize(commitment.profile) !== canonicalize(expected.profile) || canonicalize(commitment.credentialType) !== canonicalize(expected.credentialType) || commitment.descriptorDigest !== expected.descriptorDigest || commitment.issuerDid !== expected.issuerDid || commitment.issuerKid !== expected.issuerKid) throw new TypeError(`${label} shares require the ${label} credential profile`);
14122
14143
  if (commitment.requirementDigest !== expected.requirementDigest) throw new TypeError(`credential requirement is not bound to the email ${target.kind === "email" ? "address" : "domain"}`);
14123
14144
  }
14124
- async function publishAddressedShare(options) {
14125
- assertSafeInput(options);
14126
- const { target } = prepareAddressedShare(options);
14145
+ async function publishAddressedShare(input) {
14146
+ assertSafeInput(input);
14147
+ const { target } = prepareAddressedShare(input);
14148
+ const options = { ...input, filename: canonicalShareFilename(input.filename) };
14127
14149
  if (target.kind !== "recipientDid") assertMailboxCommitment(options.credentialRequirement, target);
14128
14150
  const expiry = rfc3339Seconds(options.expiresAt);
14129
14151
  const matcher = targetMatcher(target);
@@ -15099,6 +15121,7 @@ import { constants } from "fs";
15099
15121
  import { lstat, mkdir as mkdir2, mkdtemp, open as open2, readFile as readFile2, realpath, stat as stat2, link, rename as rename2, rm as rm3, unlink } from "fs/promises";
15100
15122
  import { randomBytes as randomBytes2 } from "crypto";
15101
15123
  import { basename as basename2, join as join4, resolve, sep } from "path";
15124
+ init_errors();
15102
15125
  var MAX_SHARE_STDIN_BYTES = 100 * 1024 * 1024;
15103
15126
  var MAX_SHARE_URL_BYTES = 64 * 1024;
15104
15127
  async function readBoundedStdin(limit = MAX_SHARE_STDIN_BYTES) {
@@ -15120,21 +15143,28 @@ async function readBoundedUrlStdin() {
15120
15143
  return value;
15121
15144
  }
15122
15145
  function safeFilename(value) {
15123
- if (value.length === 0 || value === "." || value === ".." || /[/\\\u0000-\u001f\u007f]/.test(value)) throw new Error("UNSAFE_FILENAME");
15124
- return value;
15146
+ return shareFilename(value);
15125
15147
  }
15126
15148
  function shareFilename(value) {
15127
- return safeFilename(value);
15149
+ try {
15150
+ return canonicalShareFilename(value);
15151
+ } catch {
15152
+ throw new CLIError(
15153
+ "UNSAFE_FILENAME",
15154
+ hasUnsafeFilenameCodePoint(value) ? "filename contains control or invisible characters" : "filename must be one safe path segment",
15155
+ 8
15156
+ );
15157
+ }
15158
+ }
15159
+ function shareInputFilename(input, name) {
15160
+ return shareFilename(name ?? (input === "-" ? "stdin.md" : basename2(resolve(input))));
15128
15161
  }
15129
15162
  async function readShareInput(input, name, limit = MAX_SHARE_STDIN_BYTES) {
15130
- if (input === "-") {
15131
- const bytes2 = await readBoundedStdin(limit);
15132
- return { bytes: bytes2, filename: shareFilename(name ?? "stdin.md") };
15133
- }
15163
+ const filename = shareInputFilename(input, name);
15164
+ if (input === "-") return { bytes: await readBoundedStdin(limit), filename };
15134
15165
  const path = resolve(input);
15135
15166
  const info = await stat2(path);
15136
15167
  if (!info.isFile() || info.size > limit) throw new Error("MAX_BYTES_EXCEEDED");
15137
- const filename = shareFilename(name ?? basename2(path));
15138
15168
  const bytes = new Uint8Array(await readFile2(path));
15139
15169
  if (bytes.byteLength > limit) throw new Error("MAX_BYTES_EXCEEDED");
15140
15170
  return { bytes, filename };
@@ -15254,6 +15284,14 @@ function shareCliError(error) {
15254
15284
  const localKey = "localKey" in failure && failure.localKey === true;
15255
15285
  const profileHint = profileName === void 0 ? "" : `--profile ${profileName} `;
15256
15286
  const loginHint = localKey ? `\`tc ${profileHint}auth login --method local\`` : `\`tc ${profileHint}auth login --device --manifest builtin:share-publishing\` (or \`tc ${profileHint}enable share\`)`;
15287
+ if (failure.kind === "caveated-session") {
15288
+ const holder = profileName === void 0 ? "this session" : `profile ${profileName}'s session`;
15289
+ return new CLIError(
15290
+ "PERMISSION_DENIED",
15291
+ `${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\``,
15292
+ 5
15293
+ );
15294
+ }
15257
15295
  if (failure.kind === "owner-space-unresolved") {
15258
15296
  return new CLIError("AUTH_REQUIRED", `a valid signed TinyCloud session is required; run ${loginHint}`, 3);
15259
15297
  }
@@ -15382,7 +15420,9 @@ function registerShareCommand(program2) {
15382
15420
  const json = jsonOutput(options, command);
15383
15421
  const maxBytes = byteLimit(options.maxBytes);
15384
15422
  if (files.length === 0 || files.includes("-") && files.length > 1) throw new CLIError("INVALID_ARGUMENT", "stdin must be the only publish input", 2);
15385
- const inputs = await Promise.all(files.map((file) => readShareInput(file, files.length === 1 ? options.name : void 0, maxBytes)));
15423
+ const name = files.length === 1 ? options.name : void 0;
15424
+ for (const file of files) shareInputFilename(file, name);
15425
+ const inputs = await Promise.all(files.map((file) => readShareInput(file, name, maxBytes)));
15386
15426
  assertAggregateInputLimit(inputs, maxBytes);
15387
15427
  const target = parseShareTarget(options.to);
15388
15428
  const actions = requestedActions(options.action);
@@ -17162,6 +17202,15 @@ function createShareAuthorityAdapters(input = {}) {
17162
17202
  if (resourceKind !== "exact" || files.length !== 1) throw new Error("native bearer publication requires one exact source file");
17163
17203
  const file2 = files[0];
17164
17204
  const resourcePath2 = `xyz.tinycloud.share/shares/${shareId}/${safeStorageFilename(targetInput.filename)}`;
17205
+ if (node.isSessionOnly) {
17206
+ const authority = node.sharing.preflightGenerate({ path: resourcePath2, actions: ["tinycloud.kv/get"], expiry: expiresAt });
17207
+ if (authority === "caveated") {
17208
+ throw new SharePublishAuthorityError({ kind: "caveated-session", profileName: activeProfileName2 });
17209
+ }
17210
+ if (authority === "not-covered") {
17211
+ throw new SharePublishAuthorityError({ kind: "scope-denied", capability: "sharing delegation", localKey, profileName: activeProfileName2 });
17212
+ }
17213
+ }
17165
17214
  const written = await node.kvForSpace(ownerSpaceId).put(resourcePath2, file2.bytes.slice(), {
17166
17215
  contentType: targetInput.mediaType ?? file2.mediaType ?? "application/octet-stream"
17167
17216
  });