@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 +24 -7
- package/dist/index.js +142 -49
- package/dist/index.js.map +1 -1
- package/dist/legacy-entry.js +1085 -361
- package/dist/legacy-entry.js.map +1 -1
- package/package.json +7 -5
- package/skills/tc-cli/AUTH.md +88 -0
- package/skills/tc-cli/INSTALL.md +28 -0
- package/skills/tc-cli/REFERENCE.md +217 -0
- package/skills/tc-cli/SDK.md +58 -0
- package/skills/tc-cli/SKILL.md +69 -0
- package/skills/tc-cli/release.json +15 -0
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
|
-
|
|
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
|
-
#
|
|
41
|
-
tc
|
|
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
|
|
68
|
-
| `tc
|
|
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
|
-
|
|
423
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
547
|
-
|
|
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
|
|
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
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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
|
-
|
|
630
|
-
|
|
631
|
-
|
|
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
|
|
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(
|
|
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.
|
|
5698
|
-
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
|
|
5702
|
-
|
|
5703
|
-
|
|
5704
|
-
|
|
5705
|
-
|
|
5706
|
-
|
|
5707
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
17263
|
+
throwKvUploadFailure(stored.error);
|
|
17171
17264
|
}
|
|
17172
17265
|
const contentSource = {
|
|
17173
17266
|
shareId,
|