@tinycloud/cli 1.0.0-beta.16 → 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/README.md CHANGED
@@ -8,7 +8,16 @@ Self-sovereign storage from the terminal. `tc` is the command-line interface for
8
8
  npm install -g @tinycloud/cli
9
9
  ```
10
10
 
11
- Requires Node.js >= 20.
11
+ Requires Node.js >= 20. On Linux, `/usr/sbin/tc` (iproute2) can shadow this
12
+ CLI; call it as `"$(npm prefix --global)/bin/tc"` when in doubt.
13
+
14
+ ## Agent skills
15
+
16
+ The package ships the general `tc-cli` skill and its references in
17
+ `skills/tc-cli`. See [skill installation](skills/tc-cli/INSTALL.md) for a
18
+ pinned, cross-agent installation with OpenCode, Codex and Claude Code, and
19
+ [authentication](skills/tc-cli/AUTH.md) for device login and relaying the
20
+ approval code to the owner.
12
21
 
13
22
  ## Operations coverage
14
23
 
@@ -34,11 +43,17 @@ tc init
34
43
  # Authenticate via browser
35
44
  tc auth login
36
45
 
37
- # Authenticate on a remote/headless machine
38
- tc auth login --device
46
+ # Authenticate on a remote/headless machine: the owner approves an explicit
47
+ # manifest on their phone (URL + code printed to stderr). Use a new profile
48
+ # name if it already exists; keep the user's existing profiles.
49
+ tc init --name publisher --key-only
50
+ tc --profile publisher auth login --device --manifest builtin:share-publishing
51
+
52
+ # Same Share publishing scope, as a shortcut (on the dedicated profile)
53
+ tc --profile publisher enable share
39
54
 
40
- # Enable only Share publishing authority
41
- tc enable share
55
+ # Selected profile, owner, host and space (JSON; does not test access)
56
+ tc context
42
57
 
43
58
  # Store and retrieve data
44
59
  tc kv put greeting "Hello, world"
@@ -64,8 +79,10 @@ tc delegation create --to did:pkh:eip155:1:0x...
64
79
  |---------|-------------|
65
80
  | `tc init` | Set up a profile and generate keys |
66
81
  | `tc auth login` | Authenticate via browser |
67
- | `tc auth login --device` | Authenticate with an OpenKey URL and user code |
68
- | `tc enable share` | Enable narrowly scoped Share publishing authority |
82
+ | `tc auth login --device --manifest <file>` | Approve exactly a manifest's permissions on another device (phone) |
83
+ | `tc auth login --manifest <file>` | Browser OpenKey login scoped to a manifest |
84
+ | `tc enable share` | Device login with the built-in Share publishing manifest (use a dedicated profile) |
85
+ | `tc context` | Show the selected profile, owner, host, space and session state |
69
86
  | `tc auth status` | Show authentication status |
70
87
  | `tc auth whoami` | Show current identity |
71
88
  | `tc kv get <key>` | Retrieve a value |
package/dist/index.js CHANGED
@@ -21,7 +21,7 @@ var init_constants = __esm({
21
21
  CONFIG_DIR = tinycloudHomePath();
22
22
  PROFILES_DIR = profilesPath();
23
23
  CONFIG_FILE = tinycloudConfigPath();
24
- DEFAULT_HOST = "https://node.tinycloud.xyz";
24
+ DEFAULT_HOST = "https://tee.node.tinycloud.xyz";
25
25
  DEFAULT_PROFILE = "default";
26
26
  ExitCode = {
27
27
  SUCCESS: 0,
@@ -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";
@@ -104,6 +109,7 @@ var init_formatter = __esm({
104
109
  // src/output/errors.ts
105
110
  import { readFileSync, readdirSync } from "fs";
106
111
  import { join } from "path";
112
+ import { ProfileLockTimeoutError } from "@tinycloud/operations/state";
107
113
  function setActiveProfileName(name) {
108
114
  activeProfileName = name;
109
115
  }
@@ -121,6 +127,14 @@ function wrapError(error) {
121
127
  );
122
128
  }
123
129
  if (error instanceof CLIError) return error;
130
+ if (error instanceof ProfileLockTimeoutError) {
131
+ return new CLIError(
132
+ "PROFILE_LOCK_TIMEOUT",
133
+ `${message} Another tc or MCP process held this profile's lock, so the change that needed it was not written.`,
134
+ ExitCode.ERROR,
135
+ { hint: "Wait for the other command to finish and retry. A crashed process's lock is reclaimed automatically after 30 s." }
136
+ );
137
+ }
124
138
  if (message.includes("Not signed in") || message.includes("AUTH_EXPIRED") || message.includes("Session expired")) {
125
139
  return new CLIError("AUTH_REQUIRED", message, ExitCode.AUTH_REQUIRED);
126
140
  }
@@ -244,9 +258,9 @@ async function readJson(filePath) {
244
258
  async function writeJson(filePath, data) {
245
259
  const directory = dirname(filePath);
246
260
  const tempPath = join2(directory, `.${basename(filePath)}.${process.pid}.${randomUUID()}.tmp`);
247
- await mkdir(directory, { recursive: true });
261
+ await mkdir(directory, { recursive: true, mode: PRIVATE_DIR_MODE });
248
262
  try {
249
- await writeFile(tempPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
263
+ await writeFile(tempPath, JSON.stringify(data, null, 2) + "\n", { encoding: "utf-8", mode: PRIVATE_FILE_MODE });
250
264
  await rename(tempPath, filePath);
251
265
  } catch (err) {
252
266
  await rm(tempPath, { force: true }).catch(() => void 0);
@@ -265,7 +279,7 @@ async function fileExists(filePath) {
265
279
  }
266
280
  }
267
281
  async function ensureDir(dirPath) {
268
- await mkdir(dirPath, { recursive: true });
282
+ await mkdir(dirPath, { recursive: true, mode: PRIVATE_DIR_MODE });
269
283
  }
270
284
  async function removeDir(dirPath) {
271
285
  await rm(dirPath, { recursive: true, force: true });
@@ -281,9 +295,12 @@ async function listDirs(dirPath) {
281
295
  throw err;
282
296
  }
283
297
  }
298
+ var PRIVATE_FILE_MODE, PRIVATE_DIR_MODE;
284
299
  var init_storage = __esm({
285
300
  "src/config/storage.ts"() {
286
301
  "use strict";
302
+ PRIVATE_FILE_MODE = 384;
303
+ PRIVATE_DIR_MODE = 448;
287
304
  }
288
305
  });
289
306
 
@@ -419,17 +436,14 @@ function profileLocalNodeIdentityStore(profileName) {
419
436
  return profile?.pinnedLocalNodeDids?.[url];
420
437
  },
421
438
  set: async (url, nodeDid) => {
422
- const profile = await ProfileManager.getProfile(profileName).catch(
423
- () => null
424
- );
425
- if (!profile) return;
426
- await ProfileManager.setProfile(profileName, {
439
+ if (!await ProfileManager.profileExists(profileName)) return;
440
+ await ProfileManager.updateProfile(profileName, (profile) => ({
427
441
  ...profile,
428
442
  pinnedLocalNodeDids: {
429
443
  ...profile.pinnedLocalNodeDids,
430
444
  [url]: nodeDid
431
445
  }
432
- });
446
+ }));
433
447
  }
434
448
  };
435
449
  }
@@ -524,10 +538,12 @@ var init_host = __esm({
524
538
  });
525
539
 
526
540
  // src/config/profiles.ts
541
+ import { chmod, rm as rm2 } from "fs/promises";
527
542
  import { join as join3 } from "path";
528
543
  import {
529
544
  readSession,
530
545
  removeSession,
546
+ withProfileLock,
531
547
  writeSession
532
548
  } from "@tinycloud/operations/state";
533
549
  var ProfileManager;
@@ -540,11 +556,41 @@ var init_profiles = __esm({
540
556
  ProfileManager = class _ProfileManager {
541
557
  // ── Initialization ──────────────────────────────────────────────────
542
558
  /**
543
- * Creates ~/.tinycloud/ and ~/.tinycloud/profiles/ if they don't exist.
559
+ * Runs `action` holding the profile's store lock (shared with operations
560
+ * and MCP). Reentrant, so the writers below can be called inside it; hold
561
+ * it around a whole read-modify-write, not just each write.
562
+ */
563
+ static async withLock(name, action, options) {
564
+ return withProfileLock(name, action, options);
565
+ }
566
+ /**
567
+ * Read-modify-write of profile.json under the profile lock, so concurrent
568
+ * updates (another command, a login commit) are never erased.
569
+ */
570
+ static async updateProfile(name, update) {
571
+ return _ProfileManager.withLock(name, async () => {
572
+ const next = update(await _ProfileManager.getProfile(name));
573
+ await _ProfileManager.setProfile(name, next);
574
+ return next;
575
+ });
576
+ }
577
+ /**
578
+ * Creates ~/.tinycloud/ and ~/.tinycloud/profiles/ if they don't exist and
579
+ * (re)sets both to 0700: older releases created them 0775.
544
580
  */
545
581
  static async ensureConfigDir() {
546
- await ensureDir(CONFIG_DIR);
547
- await ensureDir(PROFILES_DIR);
582
+ for (const directory of [CONFIG_DIR, PROFILES_DIR]) {
583
+ await ensureDir(directory);
584
+ await chmod(directory, PRIVATE_DIR_MODE);
585
+ }
586
+ }
587
+ /** Owner-only profile directory (0700), created or tightened before any write into it. */
588
+ static async ensureProfileDir(name) {
589
+ await _ProfileManager.ensureConfigDir();
590
+ const profileDir = join3(PROFILES_DIR, name);
591
+ await ensureDir(profileDir);
592
+ await chmod(profileDir, PRIVATE_DIR_MODE);
593
+ return profileDir;
548
594
  }
549
595
  // ── Global config ───────────────────────────────────────────────────
550
596
  /**
@@ -581,12 +627,17 @@ var init_profiles = __esm({
581
627
  return profile;
582
628
  }
583
629
  /**
584
- * Saves a profile config, creating the profile directory if needed.
630
+ * Saves a profile config under the profile lock, creating the profile
631
+ * directory if needed. Use `updateProfile` to change an existing profile.
585
632
  */
586
633
  static async setProfile(name, data) {
587
- const profileDir = join3(PROFILES_DIR, name);
588
- await ensureDir(profileDir);
589
- await writeJson(join3(profileDir, "profile.json"), data);
634
+ await _ProfileManager.withLock(name, async () => {
635
+ await writeJson(join3(await _ProfileManager.ensureProfileDir(name), "profile.json"), data);
636
+ });
637
+ }
638
+ /** Removes profile.json under the profile lock (rollback of a profile a failed login created). */
639
+ static async removeProfileConfig(name) {
640
+ await _ProfileManager.withLock(name, () => rm2(join3(PROFILES_DIR, name, "profile.json"), { force: true }));
590
641
  }
591
642
  /**
592
643
  * Returns true if a profile directory exists.
@@ -622,13 +673,15 @@ var init_profiles = __esm({
622
673
  static async getKey(name) {
623
674
  return readJson(join3(PROFILES_DIR, name, "key.json"));
624
675
  }
625
- /**
626
- * Saves a JWK key for a profile.
627
- */
676
+ /** Saves a JWK key under the profile lock (0600, in an owner-only profile directory). */
628
677
  static async setKey(name, jwk) {
629
- const profileDir = join3(PROFILES_DIR, name);
630
- await ensureDir(profileDir);
631
- await writeJson(join3(profileDir, "key.json"), jwk);
678
+ await _ProfileManager.withLock(name, async () => {
679
+ await writeJson(join3(await _ProfileManager.ensureProfileDir(name), "key.json"), jwk);
680
+ });
681
+ }
682
+ /** Removes key.json under the profile lock (rollback of a key a failed login created). */
683
+ static async removeKey(name) {
684
+ await _ProfileManager.withLock(name, () => rm2(join3(PROFILES_DIR, name, "key.json"), { force: true }));
632
685
  }
633
686
  // ── Session management ──────────────────────────────────────────────
634
687
  /**
@@ -651,11 +704,13 @@ var init_profiles = __esm({
651
704
  }
652
705
  // ── Cache management ────────────────────────────────────────────────
653
706
  /**
654
- * Returns the path to the profile's cache directory, creating it if needed.
707
+ * Returns the profile's cache directory (share history lives here),
708
+ * created or tightened to 0700.
655
709
  */
656
710
  static async getCacheDir(name) {
657
- const cacheDir = join3(PROFILES_DIR, name, "cache");
711
+ const cacheDir = join3(await _ProfileManager.ensureProfileDir(name), "cache");
658
712
  await ensureDir(cacheDir);
713
+ await chmod(cacheDir, PRIVATE_DIR_MODE);
659
714
  return cacheDir;
660
715
  }
661
716
  // ── Resolution helpers ──────────────────────────────────────────────
@@ -5535,8 +5590,15 @@ var init_zod = __esm({
5535
5590
  }
5536
5591
  });
5537
5592
 
5593
+ // src/share/publishing-manifest.ts
5594
+ var init_publishing_manifest = __esm({
5595
+ "src/share/publishing-manifest.ts"() {
5596
+ "use strict";
5597
+ }
5598
+ });
5599
+
5538
5600
  // src/lib/permissions.ts
5539
- import { appendFile, readFile as readFile3 } from "fs/promises";
5601
+ import { appendFile, chmod as chmod2, readFile as readFile3 } from "fs/promises";
5540
5602
  import { join as join5 } from "path";
5541
5603
  import {
5542
5604
  buildPermissionRequestArtifact,
@@ -5549,7 +5611,7 @@ import {
5549
5611
  readAdditionalDelegations,
5550
5612
  readAuthRequests,
5551
5613
  upsertProfileRecord,
5552
- withProfileLock,
5614
+ withProfileLock as withProfileLock2,
5553
5615
  writeJsonAtomic
5554
5616
  } from "@tinycloud/operations/state";
5555
5617
  async function loadAdditionalDelegations(profile) {
@@ -5579,6 +5641,7 @@ var init_permissions = __esm({
5579
5641
  init_errors();
5580
5642
  init_constants();
5581
5643
  init_space();
5644
+ init_publishing_manifest();
5582
5645
  init_types();
5583
5646
  }
5584
5647
  });
@@ -5694,17 +5757,19 @@ async function bootstrapDelegatedSession(ctx, delegation) {
5694
5757
  }
5695
5758
  const key = await ProfileManager.getKey(ctx.profile);
5696
5759
  const jwk = signerJwkForProfile(ctx.profile, void 0, key);
5697
- await ProfileManager.setSession(ctx.profile, {
5698
- delegationHeader: delegation.delegationHeader,
5699
- delegationCid: delegation.cid,
5700
- spaceId: delegation.spaceId,
5701
- jwk,
5702
- verificationMethod: sessionDid
5703
- });
5704
- await ProfileManager.setProfile(ctx.profile, {
5705
- ...profile,
5706
- sessionDid,
5707
- spaceId: delegation.spaceId
5760
+ await ProfileManager.withLock(ctx.profile, async () => {
5761
+ await ProfileManager.setSession(ctx.profile, {
5762
+ delegationHeader: delegation.delegationHeader,
5763
+ delegationCid: delegation.cid,
5764
+ spaceId: delegation.spaceId,
5765
+ jwk,
5766
+ verificationMethod: sessionDid
5767
+ });
5768
+ await ProfileManager.updateProfile(ctx.profile, (current) => ({
5769
+ ...current,
5770
+ sessionDid,
5771
+ spaceId: delegation.spaceId
5772
+ }));
5708
5773
  });
5709
5774
  return createSDKInstance(ctx);
5710
5775
  }
@@ -14991,6 +15056,7 @@ function parseDuration(input) {
14991
15056
  }
14992
15057
 
14993
15058
  // src/commands/share.ts
15059
+ init_formatter();
14994
15060
  init_errors();
14995
15061
 
14996
15062
  // src/share/output.ts
@@ -15030,7 +15096,7 @@ function receiveJson(result, path) {
15030
15096
 
15031
15097
  // src/share/io.ts
15032
15098
  import { constants } from "fs";
15033
- import { lstat, mkdir as mkdir2, mkdtemp, open as open2, readFile as readFile2, realpath, stat as stat2, link, rename as rename2, rm as rm2, unlink } from "fs/promises";
15099
+ 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";
15034
15100
  import { randomBytes as randomBytes2 } from "crypto";
15035
15101
  import { basename as basename2, join as join4, resolve, sep } from "path";
15036
15102
  var MAX_SHARE_STDIN_BYTES = 100 * 1024 * 1024;
@@ -15144,7 +15210,7 @@ async function writeShareOutput(directory, filename, bytes, force) {
15144
15210
  if (temporaryPath !== void 0) await unlink(temporaryPath);
15145
15211
  } catch {
15146
15212
  }
15147
- await rm2(stagingDirectory, { recursive: true, force: true });
15213
+ await rm3(stagingDirectory, { recursive: true, force: true });
15148
15214
  await directoryHandle.close();
15149
15215
  }
15150
15216
  return join4(outputDirectory, safeName);
@@ -15187,23 +15253,24 @@ function shareCliError(error) {
15187
15253
  const profileName = "profileName" in failure ? failure.profileName : void 0;
15188
15254
  const localKey = "localKey" in failure && failure.localKey === true;
15189
15255
  const profileHint = profileName === void 0 ? "" : `--profile ${profileName} `;
15190
- const loginHint = `tc ${profileHint}auth login${localKey ? " --method local" : ""}`;
15256
+ const loginHint = localKey ? `\`tc ${profileHint}auth login --method local\`` : `\`tc ${profileHint}auth login --device --manifest builtin:share-publishing\` (or \`tc ${profileHint}enable share\`)`;
15191
15257
  if (failure.kind === "owner-space-unresolved") {
15192
- return new CLIError("AUTH_REQUIRED", `a valid signed TinyCloud session is required; run \`${loginHint}\``, 3);
15258
+ return new CLIError("AUTH_REQUIRED", `a valid signed TinyCloud session is required; run ${loginHint}`, 3);
15193
15259
  }
15194
15260
  if (failure.kind === "scope-denied") {
15195
15261
  const requiredAction = failure.requiredAction === void 0 ? "" : ` (${failure.requiredAction})`;
15196
- return new CLIError("PERMISSION_DENIED", `the session lacks ${failure.capability} authority${requiredAction}; renew it with \`${loginHint}\` using the required capability`, 5);
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}`;
15263
+ return new CLIError("PERMISSION_DENIED", `the session lacks ${failure.capability} authority${requiredAction}; ${renew}`, 5);
15197
15264
  }
15198
15265
  if (failure.kind === "lifetime-exceeds-session") {
15199
15266
  const expiresAt = failure.sessionExpiresAt.toISOString();
15200
15267
  if (failure.reason === "session-too-close") {
15201
- return new CLIError("AUTH_REQUIRED", `the signed session expires too soon (${expiresAt}); log in again with \`${loginHint}\``, 3);
15268
+ return new CLIError("AUTH_REQUIRED", `the signed session expires too soon (${expiresAt}); log in again with ${loginHint}`, 3);
15202
15269
  }
15203
15270
  if (failure.reason === "below-minimum") {
15204
15271
  return new CLIError("SESSION_LIFETIME_EXCEEDED", "share expiry must be at least 60 seconds from now; use a longer --expires value", 2);
15205
15272
  }
15206
- return new CLIError("SESSION_LIFETIME_EXCEEDED", `requested share lifetime exceeds session expiry ${expiresAt}; use a shorter --expires value or renew the session with \`${loginHint}\``, 2);
15273
+ return new CLIError("SESSION_LIFETIME_EXCEEDED", `requested share lifetime exceeds session expiry ${expiresAt}; use a shorter --expires value or renew the session with ${loginHint}`, 2);
15207
15274
  }
15208
15275
  if (failure.kind === "origin-mismatch") {
15209
15276
  return new CLIError("ORIGIN_MISMATCH", "share origin does not match the configured service", 2);
@@ -15217,6 +15284,13 @@ function shareCliError(error) {
15217
15284
  if (failure.kind === "registry-rejected") {
15218
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);
15219
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
+ }
15220
15294
  }
15221
15295
  if (error instanceof ShareNotifyError) {
15222
15296
  return new CLIError("INVALID_ARGUMENT", error.message, 2);
@@ -16836,7 +16910,26 @@ function requiredKvAction(meta) {
16836
16910
  const action = meta.requiredAction;
16837
16911
  return action === "tinycloud.kv/put" || action === "tinycloud.kv/get" ? action : void 0;
16838
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
+ }
16839
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
+ }
16840
16933
  function createEncryptedSessionHistory() {
16841
16934
  const records = /* @__PURE__ */ new Map();
16842
16935
  let keyPromise;
@@ -17068,7 +17161,7 @@ function createShareAuthorityAdapters(input = {}) {
17068
17161
  if (targetInput.target.kind === "bearer") {
17069
17162
  if (resourceKind !== "exact" || files.length !== 1) throw new Error("native bearer publication requires one exact source file");
17070
17163
  const file2 = files[0];
17071
- const resourcePath2 = `xyz.tinycloud.share/shares/${shareId}/${targetInput.filename}`;
17164
+ const resourcePath2 = `xyz.tinycloud.share/shares/${shareId}/${safeStorageFilename(targetInput.filename)}`;
17072
17165
  const written = await node.kvForSpace(ownerSpaceId).put(resourcePath2, file2.bytes.slice(), {
17073
17166
  contentType: targetInput.mediaType ?? file2.mediaType ?? "application/octet-stream"
17074
17167
  });
@@ -17084,7 +17177,7 @@ function createShareAuthorityAdapters(input = {}) {
17084
17177
  profileName: activeProfileName2
17085
17178
  });
17086
17179
  }
17087
- throw new Error("native bearer source upload failed");
17180
+ throwKvUploadFailure(written.error);
17088
17181
  }
17089
17182
  let native;
17090
17183
  try {
@@ -17132,7 +17225,7 @@ function createShareAuthorityAdapters(input = {}) {
17132
17225
  }
17133
17226
  if (resourceKind !== "exact" || files.length !== 1) throw new Error("addressed publication requires a single exact source file");
17134
17227
  const file = files[0];
17135
- const resourcePath = `shares/${shareId}/${targetInput.filename}`;
17228
+ const resourcePath = `shares/${shareId}/${safeStorageFilename(targetInput.filename)}`;
17136
17229
  const byteLength = file.bytes.byteLength;
17137
17230
  if (!Number.isSafeInteger(byteLength) || byteLength > 100 * 1024 * 1024) throw new Error("addressed publication exceeds the combined byte limit");
17138
17231
  const mediaType = targetInput.mediaType ?? file.mediaType ?? "application/octet-stream";
@@ -17167,7 +17260,7 @@ function createShareAuthorityAdapters(input = {}) {
17167
17260
  profileName: activeProfileName2
17168
17261
  });
17169
17262
  }
17170
- throw new Error("addressed source upload failed");
17263
+ throwKvUploadFailure(stored.error);
17171
17264
  }
17172
17265
  const contentSource = {
17173
17266
  shareId,