@tinycloud/cli 1.0.1-beta.0 → 1.0.1-beta.2
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 +125 -43
- package/dist/index.js.map +1 -1
- package/dist/legacy-entry.js +209 -125
- package/dist/legacy-entry.js.map +1 -1
- package/package.json +4 -4
- package/skills/tc-cli/AUTH.md +1 -0
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
tinycloudConfigPath,
|
|
15
15
|
tinycloudHomePath
|
|
16
16
|
} from "@tinycloud/operations/state";
|
|
17
|
-
var CONFIG_DIR, PROFILES_DIR, CONFIG_FILE, DEFAULT_HOST, DEFAULT_PROFILE, ExitCode;
|
|
17
|
+
var CONFIG_DIR, PROFILES_DIR, CONFIG_FILE, DEFAULT_HOST, DEFAULT_PROFILE, PROFILE_COMMIT_LOCK_TIMEOUT_MS, ExitCode;
|
|
18
18
|
var init_constants = __esm({
|
|
19
19
|
"src/config/constants.ts"() {
|
|
20
20
|
"use strict";
|
|
@@ -23,6 +23,7 @@ var init_constants = __esm({
|
|
|
23
23
|
CONFIG_FILE = tinycloudConfigPath();
|
|
24
24
|
DEFAULT_HOST = "https://tee.node.tinycloud.xyz";
|
|
25
25
|
DEFAULT_PROFILE = "default";
|
|
26
|
+
PROFILE_COMMIT_LOCK_TIMEOUT_MS = 45e3;
|
|
26
27
|
ExitCode = {
|
|
27
28
|
SUCCESS: 0,
|
|
28
29
|
ERROR: 1,
|
|
@@ -281,9 +282,6 @@ async function fileExists(filePath) {
|
|
|
281
282
|
async function ensureDir(dirPath) {
|
|
282
283
|
await mkdir(dirPath, { recursive: true, mode: PRIVATE_DIR_MODE });
|
|
283
284
|
}
|
|
284
|
-
async function removeDir(dirPath) {
|
|
285
|
-
await rm(dirPath, { recursive: true, force: true });
|
|
286
|
-
}
|
|
287
285
|
async function listDirs(dirPath) {
|
|
288
286
|
try {
|
|
289
287
|
const entries = await readdir(dirPath, { withFileTypes: true });
|
|
@@ -538,9 +536,10 @@ var init_host = __esm({
|
|
|
538
536
|
});
|
|
539
537
|
|
|
540
538
|
// src/config/profiles.ts
|
|
541
|
-
import { chmod, rm as rm2 } from "fs/promises";
|
|
539
|
+
import { chmod, lstat, readdir as readdir2, rm as rm2, rmdir } from "fs/promises";
|
|
542
540
|
import { join as join3 } from "path";
|
|
543
541
|
import {
|
|
542
|
+
profilePath,
|
|
544
543
|
readSession,
|
|
545
544
|
removeSession,
|
|
546
545
|
withProfileLock,
|
|
@@ -616,8 +615,8 @@ var init_profiles = __esm({
|
|
|
616
615
|
* Throws CLIError if the profile doesn't exist.
|
|
617
616
|
*/
|
|
618
617
|
static async getProfile(name) {
|
|
619
|
-
const
|
|
620
|
-
const profile = await readJson(
|
|
618
|
+
const profilePath2 = join3(PROFILES_DIR, name, "profile.json");
|
|
619
|
+
const profile = await readJson(profilePath2);
|
|
621
620
|
if (!profile) {
|
|
622
621
|
throw new CLIError(
|
|
623
622
|
"PROFILE_NOT_FOUND",
|
|
@@ -652,10 +651,27 @@ var init_profiles = __esm({
|
|
|
652
651
|
return listDirs(PROFILES_DIR);
|
|
653
652
|
}
|
|
654
653
|
/**
|
|
655
|
-
* Deletes a profile
|
|
656
|
-
*
|
|
654
|
+
* Deletes a profile. Its key, session, settings, stores and cache are
|
|
655
|
+
* removed while holding the profile lock, so another writer's critical
|
|
656
|
+
* section never sees them vanish midway: session and key first, settings
|
|
657
|
+
* last, so a crash midway never leaves a session or key without its
|
|
658
|
+
* profile. `.lock` itself is left to the lock's release; the then-empty
|
|
659
|
+
* directory is removed afterwards unless another writer took the lock (or
|
|
660
|
+
* wrote) meanwhile. A profile directory that is a symlink is unlinked, its
|
|
661
|
+
* target left alone.
|
|
662
|
+
* Throws if the name is not one path segment or names the default profile.
|
|
657
663
|
*/
|
|
658
664
|
static async deleteProfile(name) {
|
|
665
|
+
let profileDir;
|
|
666
|
+
try {
|
|
667
|
+
profileDir = profilePath(name);
|
|
668
|
+
} catch {
|
|
669
|
+
throw new CLIError(
|
|
670
|
+
"INVALID_PROFILE_NAME",
|
|
671
|
+
`Invalid profile name "${name}": a profile name is one path segment (no "/", "\\", "." or "..").`,
|
|
672
|
+
ExitCode.USAGE_ERROR
|
|
673
|
+
);
|
|
674
|
+
}
|
|
659
675
|
const config = await _ProfileManager.getConfig();
|
|
660
676
|
if (config.defaultProfile === name) {
|
|
661
677
|
throw new CLIError(
|
|
@@ -663,8 +679,24 @@ var init_profiles = __esm({
|
|
|
663
679
|
`Cannot delete the default profile "${name}". Change the default first with \`tc profile default <other>\`.`
|
|
664
680
|
);
|
|
665
681
|
}
|
|
666
|
-
const
|
|
667
|
-
|
|
682
|
+
const isLink = await lstat(profileDir).then((stats) => stats.isSymbolicLink(), (error) => {
|
|
683
|
+
if (error.code === "ENOENT") return false;
|
|
684
|
+
throw error;
|
|
685
|
+
});
|
|
686
|
+
if (isLink) {
|
|
687
|
+
await rm2(profileDir, { force: true });
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
await _ProfileManager.withLock(name, async () => {
|
|
691
|
+
for (const file of ["session.json", "key.json"]) await rm2(join3(profileDir, file), { force: true });
|
|
692
|
+
for (const entry of await readdir2(profileDir)) {
|
|
693
|
+
if (entry !== ".lock" && entry !== "profile.json") await rm2(join3(profileDir, entry), { recursive: true, force: true });
|
|
694
|
+
}
|
|
695
|
+
await rm2(join3(profileDir, "profile.json"), { force: true });
|
|
696
|
+
});
|
|
697
|
+
await rmdir(profileDir).catch((error) => {
|
|
698
|
+
if (error.code !== "ENOENT" && error.code !== "ENOTEMPTY" && error.code !== "EEXIST") throw error;
|
|
699
|
+
});
|
|
668
700
|
}
|
|
669
701
|
// ── Key management ──────────────────────────────────────────────────
|
|
670
702
|
/**
|
|
@@ -5758,39 +5790,89 @@ async function createSDKInstance(ctx, options) {
|
|
|
5758
5790
|
return node;
|
|
5759
5791
|
}
|
|
5760
5792
|
async function bootstrapDelegatedSession(ctx, delegation) {
|
|
5761
|
-
const
|
|
5762
|
-
|
|
5763
|
-
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
|
|
5767
|
-
|
|
5768
|
-
|
|
5769
|
-
|
|
5770
|
-
|
|
5771
|
-
|
|
5772
|
-
|
|
5773
|
-
|
|
5774
|
-
|
|
5775
|
-
|
|
5776
|
-
|
|
5777
|
-
|
|
5778
|
-
|
|
5779
|
-
|
|
5780
|
-
|
|
5793
|
+
const written = await ProfileManager.withLock(ctx.profile, async () => {
|
|
5794
|
+
const previousProfile = await ProfileManager.getProfile(ctx.profile);
|
|
5795
|
+
if (resolveProfilePosture(previousProfile) !== "delegate-session") {
|
|
5796
|
+
throw new CLIError(
|
|
5797
|
+
"AUTH_REQUIRED",
|
|
5798
|
+
`Profile "${ctx.profile}" is not a delegate-session profile.`,
|
|
5799
|
+
ExitCode.AUTH_REQUIRED
|
|
5800
|
+
);
|
|
5801
|
+
}
|
|
5802
|
+
const sessionDid = previousProfile.sessionDid ?? previousProfile.did;
|
|
5803
|
+
if (delegation.delegateDID.split("#", 1)[0] !== sessionDid.split("#", 1)[0]) {
|
|
5804
|
+
throw new CLIError(
|
|
5805
|
+
"DELEGATION_AUDIENCE_MISMATCH",
|
|
5806
|
+
`Delegation targets ${delegation.delegateDID}, but profile "${ctx.profile}" uses ${sessionDid}.`,
|
|
5807
|
+
ExitCode.PERMISSION_DENIED
|
|
5808
|
+
);
|
|
5809
|
+
}
|
|
5810
|
+
if (await ProfileManager.getSession(ctx.profile) !== null) {
|
|
5811
|
+
throw new CLIError(
|
|
5812
|
+
"PROFILE_CHANGED_DURING_IMPORT",
|
|
5813
|
+
`Profile "${ctx.profile}" gained a session (another login or import) after this import checked it. Nothing was saved; run the import again.`,
|
|
5814
|
+
ExitCode.ERROR
|
|
5815
|
+
);
|
|
5816
|
+
}
|
|
5817
|
+
const jwk = signerJwkForProfile(ctx.profile, void 0, await ProfileManager.getKey(ctx.profile));
|
|
5818
|
+
const session = {
|
|
5781
5819
|
delegationHeader: delegation.delegationHeader,
|
|
5782
5820
|
delegationCid: delegation.cid,
|
|
5783
5821
|
spaceId: delegation.spaceId,
|
|
5784
5822
|
jwk,
|
|
5785
5823
|
verificationMethod: sessionDid
|
|
5786
|
-
}
|
|
5787
|
-
|
|
5788
|
-
|
|
5789
|
-
|
|
5790
|
-
|
|
5791
|
-
})
|
|
5824
|
+
};
|
|
5825
|
+
const profile = { ...previousProfile, sessionDid, spaceId: delegation.spaceId };
|
|
5826
|
+
try {
|
|
5827
|
+
await ProfileManager.setSession(ctx.profile, session);
|
|
5828
|
+
await ProfileManager.setProfile(ctx.profile, profile);
|
|
5829
|
+
} catch (error) {
|
|
5830
|
+
throw annotate(error, await restoreBeforeBootstrap(ctx.profile, previousProfile));
|
|
5831
|
+
}
|
|
5832
|
+
return { previousProfile, profile, session };
|
|
5792
5833
|
});
|
|
5793
|
-
|
|
5834
|
+
const abandon = async (cause) => {
|
|
5835
|
+
const note = await ProfileManager.withLock(ctx.profile, async () => {
|
|
5836
|
+
const profile = await ProfileManager.getProfile(ctx.profile).catch((error) => {
|
|
5837
|
+
if (error instanceof CLIError && error.code === "PROFILE_NOT_FOUND") return null;
|
|
5838
|
+
throw error;
|
|
5839
|
+
});
|
|
5840
|
+
const session = await ProfileManager.getSession(ctx.profile);
|
|
5841
|
+
if (JSON.stringify(profile) !== JSON.stringify(written.profile) || JSON.stringify(session) !== JSON.stringify(written.session)) {
|
|
5842
|
+
return `Profile "${ctx.profile}" changed while the import was pending (another login, logout or profile update), so its newer state was kept and the provisional session was not rolled back.`;
|
|
5843
|
+
}
|
|
5844
|
+
return restoreBeforeBootstrap(ctx.profile, written.previousProfile);
|
|
5845
|
+
}, { timeoutMs: PROFILE_COMMIT_LOCK_TIMEOUT_MS }).catch((error) => (
|
|
5846
|
+
// The lock or a read failed: the import's error stays the one reported.
|
|
5847
|
+
`Rolling back the provisional session of profile "${ctx.profile}" could not run (${error instanceof Error ? error.message : String(error)}); check \`tc --profile ${ctx.profile} context\`.`
|
|
5848
|
+
));
|
|
5849
|
+
throw annotate(cause, note);
|
|
5850
|
+
};
|
|
5851
|
+
let node;
|
|
5852
|
+
try {
|
|
5853
|
+
node = await createSDKInstance(ctx);
|
|
5854
|
+
} catch (error) {
|
|
5855
|
+
return abandon(error);
|
|
5856
|
+
}
|
|
5857
|
+
return { node, abandon };
|
|
5858
|
+
}
|
|
5859
|
+
async function restoreBeforeBootstrap(profileName, previousProfile) {
|
|
5860
|
+
const failures = [];
|
|
5861
|
+
for (const write of [
|
|
5862
|
+
() => ProfileManager.clearSession(profileName),
|
|
5863
|
+
() => ProfileManager.setProfile(profileName, previousProfile)
|
|
5864
|
+
]) {
|
|
5865
|
+
await write().catch((error) => {
|
|
5866
|
+
failures.push(error instanceof Error ? error.message : String(error));
|
|
5867
|
+
});
|
|
5868
|
+
}
|
|
5869
|
+
if (failures.length === 0) return void 0;
|
|
5870
|
+
return `Rolling back the provisional session of profile "${profileName}" failed too (${failures.join("; ")}); check \`tc --profile ${profileName} context\`.`;
|
|
5871
|
+
}
|
|
5872
|
+
function annotate(error, note) {
|
|
5873
|
+
if (note === void 0) return error;
|
|
5874
|
+
const cause = wrapError(error);
|
|
5875
|
+
return new CLIError(cause.code, `${cause.message} ${note}`, cause.exitCode, cause.metadata);
|
|
5794
5876
|
}
|
|
5795
5877
|
async function ensureAuthenticated(ctx, options) {
|
|
5796
5878
|
if (options?.privateKey) {
|
|
@@ -15143,7 +15225,7 @@ function receiveJson(result, path) {
|
|
|
15143
15225
|
|
|
15144
15226
|
// src/share/io.ts
|
|
15145
15227
|
import { constants } from "fs";
|
|
15146
|
-
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";
|
|
15228
|
+
import { lstat as lstat2, 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";
|
|
15147
15229
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
15148
15230
|
import { basename as basename2, join as join4, resolve, sep } from "path";
|
|
15149
15231
|
init_errors();
|
|
@@ -15201,7 +15283,7 @@ async function assertDirectory(path) {
|
|
|
15201
15283
|
for (const segment of segments) {
|
|
15202
15284
|
current = current === sep ? join4(current, segment) : join4(current, segment);
|
|
15203
15285
|
try {
|
|
15204
|
-
const info = await
|
|
15286
|
+
const info = await lstat2(current);
|
|
15205
15287
|
if (info.isSymbolicLink()) {
|
|
15206
15288
|
const canonical = await realpath(current);
|
|
15207
15289
|
if (current !== "/tmp" && current !== "/var") throw new Error("OUTPUT_EXISTS");
|
|
@@ -15210,7 +15292,7 @@ async function assertDirectory(path) {
|
|
|
15210
15292
|
} catch (error) {
|
|
15211
15293
|
if (error.code !== "ENOENT") throw error;
|
|
15212
15294
|
await mkdir2(current, { mode: 448 });
|
|
15213
|
-
const created = await
|
|
15295
|
+
const created = await lstat2(current);
|
|
15214
15296
|
if (created.isSymbolicLink() || !created.isDirectory()) throw new Error("OUTPUT_EXISTS");
|
|
15215
15297
|
}
|
|
15216
15298
|
}
|
|
@@ -15229,7 +15311,7 @@ async function writeShareOutput(directory, filename, bytes, force) {
|
|
|
15229
15311
|
};
|
|
15230
15312
|
await assertStableDirectory();
|
|
15231
15313
|
const stagingDirectory = await mkdtemp(join4(stableDirectory, ".tinycloud-share-stage-"));
|
|
15232
|
-
const stagingInfo = await
|
|
15314
|
+
const stagingInfo = await lstat2(stagingDirectory);
|
|
15233
15315
|
if (!stagingInfo.isDirectory() || (stagingInfo.mode & 511) !== 448) throw new Error("OUTPUT_EXISTS");
|
|
15234
15316
|
const stagingPath = join4(stagingDirectory, `.tinycloud-share-${randomBytes2(16).toString("hex")}.tmp`);
|
|
15235
15317
|
let temporaryPath;
|
|
@@ -15237,7 +15319,7 @@ async function writeShareOutput(directory, filename, bytes, force) {
|
|
|
15237
15319
|
try {
|
|
15238
15320
|
await assertStableDirectory();
|
|
15239
15321
|
try {
|
|
15240
|
-
const existing = await
|
|
15322
|
+
const existing = await lstat2(outputPath);
|
|
15241
15323
|
if (existing.isSymbolicLink()) throw new Error("UNSAFE_FILENAME");
|
|
15242
15324
|
} catch (error) {
|
|
15243
15325
|
if (error.code !== "ENOENT") throw error;
|