@tinycloud/cli 1.0.0 → 1.0.1-beta.1
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 +144 -47
- package/dist/index.js.map +1 -1
- package/dist/legacy-entry.js +137 -60
- package/dist/legacy-entry.js.map +1 -1
- package/package.json +6 -6
- 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) {
|
|
@@ -12900,11 +12982,15 @@ var contentMetadataSchema = external_exports.object({
|
|
|
12900
12982
|
/** Encrypted presentation discriminator. The fixed entry point is index.html. */
|
|
12901
12983
|
artifact: external_exports.literal("html").optional()
|
|
12902
12984
|
}).strict();
|
|
12985
|
+
var deliveryEmailSchema = external_exports.string().email();
|
|
12986
|
+
function isEnvelopeDeliveryEmail(value) {
|
|
12987
|
+
return deliveryEmailSchema.safeParse(value).success;
|
|
12988
|
+
}
|
|
12903
12989
|
var unsignedShareEnvelopeV2BaseSchema = external_exports.object({
|
|
12904
12990
|
version: external_exports.literal(2),
|
|
12905
12991
|
shareId: external_exports.string().min(1),
|
|
12906
12992
|
recipientMatcher: recipientMatcherSchema,
|
|
12907
|
-
deliveryEmail:
|
|
12993
|
+
deliveryEmail: deliveryEmailSchema.optional(),
|
|
12908
12994
|
actions: external_exports.array(shareActionSchema).min(1).max(3),
|
|
12909
12995
|
resource: resourceSelectorSchema,
|
|
12910
12996
|
target: v2TargetSchema,
|
|
@@ -13044,7 +13130,7 @@ var unsignedShareEnvelopeV3BaseSchema = external_exports.object({
|
|
|
13044
13130
|
version: external_exports.literal(3),
|
|
13045
13131
|
shareId: external_exports.string().min(1),
|
|
13046
13132
|
recipientMatcher: recipientMatcherSchema,
|
|
13047
|
-
deliveryEmail:
|
|
13133
|
+
deliveryEmail: deliveryEmailSchema.optional(),
|
|
13048
13134
|
actions: external_exports.array(shareActionSchema).min(1).max(3),
|
|
13049
13135
|
resource: resourceSelectorSchema,
|
|
13050
13136
|
target: v3TargetSchema,
|
|
@@ -14147,6 +14233,7 @@ function prepareAddressedShare(request) {
|
|
|
14147
14233
|
}
|
|
14148
14234
|
if (request.actions.length === 0 || request.policyActions.length === 0) throw new TypeError("addressed share actions are empty");
|
|
14149
14235
|
if (request.policyActions.some((action) => !OWNER_SHARE_ACTIONS.has(action))) throw new TypeError("addressed share action is not supported");
|
|
14236
|
+
if (request.deliveryEmail !== void 0 && !isEnvelopeDeliveryEmail(request.deliveryEmail)) throw new TypeError("delivery email is not a valid envelope address");
|
|
14150
14237
|
const target = normalizeShareTarget(request.target);
|
|
14151
14238
|
if (target.kind === "bearer") throw new TypeError("addressed target is required");
|
|
14152
14239
|
if (target.kind === "recipientDid") return { target };
|
|
@@ -15138,7 +15225,7 @@ function receiveJson(result, path) {
|
|
|
15138
15225
|
|
|
15139
15226
|
// src/share/io.ts
|
|
15140
15227
|
import { constants } from "fs";
|
|
15141
|
-
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";
|
|
15142
15229
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
15143
15230
|
import { basename as basename2, join as join4, resolve, sep } from "path";
|
|
15144
15231
|
init_errors();
|
|
@@ -15196,7 +15283,7 @@ async function assertDirectory(path) {
|
|
|
15196
15283
|
for (const segment of segments) {
|
|
15197
15284
|
current = current === sep ? join4(current, segment) : join4(current, segment);
|
|
15198
15285
|
try {
|
|
15199
|
-
const info = await
|
|
15286
|
+
const info = await lstat2(current);
|
|
15200
15287
|
if (info.isSymbolicLink()) {
|
|
15201
15288
|
const canonical = await realpath(current);
|
|
15202
15289
|
if (current !== "/tmp" && current !== "/var") throw new Error("OUTPUT_EXISTS");
|
|
@@ -15205,7 +15292,7 @@ async function assertDirectory(path) {
|
|
|
15205
15292
|
} catch (error) {
|
|
15206
15293
|
if (error.code !== "ENOENT") throw error;
|
|
15207
15294
|
await mkdir2(current, { mode: 448 });
|
|
15208
|
-
const created = await
|
|
15295
|
+
const created = await lstat2(current);
|
|
15209
15296
|
if (created.isSymbolicLink() || !created.isDirectory()) throw new Error("OUTPUT_EXISTS");
|
|
15210
15297
|
}
|
|
15211
15298
|
}
|
|
@@ -15224,7 +15311,7 @@ async function writeShareOutput(directory, filename, bytes, force) {
|
|
|
15224
15311
|
};
|
|
15225
15312
|
await assertStableDirectory();
|
|
15226
15313
|
const stagingDirectory = await mkdtemp(join4(stableDirectory, ".tinycloud-share-stage-"));
|
|
15227
|
-
const stagingInfo = await
|
|
15314
|
+
const stagingInfo = await lstat2(stagingDirectory);
|
|
15228
15315
|
if (!stagingInfo.isDirectory() || (stagingInfo.mode & 511) !== 448) throw new Error("OUTPUT_EXISTS");
|
|
15229
15316
|
const stagingPath = join4(stagingDirectory, `.tinycloud-share-${randomBytes2(16).toString("hex")}.tmp`);
|
|
15230
15317
|
let temporaryPath;
|
|
@@ -15232,7 +15319,7 @@ async function writeShareOutput(directory, filename, bytes, force) {
|
|
|
15232
15319
|
try {
|
|
15233
15320
|
await assertStableDirectory();
|
|
15234
15321
|
try {
|
|
15235
|
-
const existing = await
|
|
15322
|
+
const existing = await lstat2(outputPath);
|
|
15236
15323
|
if (existing.isSymbolicLink()) throw new Error("UNSAFE_FILENAME");
|
|
15237
15324
|
} catch (error) {
|
|
15238
15325
|
if (error.code !== "ENOENT") throw error;
|
|
@@ -16704,11 +16791,15 @@ var contentMetadataSchema2 = external_exports2.object({
|
|
|
16704
16791
|
/** Encrypted presentation discriminator. The fixed entry point is index.html. */
|
|
16705
16792
|
artifact: external_exports2.literal("html").optional()
|
|
16706
16793
|
}).strict();
|
|
16794
|
+
var deliveryEmailSchema2 = external_exports2.string().email();
|
|
16795
|
+
function isEnvelopeDeliveryEmail2(value) {
|
|
16796
|
+
return deliveryEmailSchema2.safeParse(value).success;
|
|
16797
|
+
}
|
|
16707
16798
|
var unsignedShareEnvelopeV2BaseSchema2 = external_exports2.object({
|
|
16708
16799
|
version: external_exports2.literal(2),
|
|
16709
16800
|
shareId: external_exports2.string().min(1),
|
|
16710
16801
|
recipientMatcher: recipientMatcherSchema2,
|
|
16711
|
-
deliveryEmail:
|
|
16802
|
+
deliveryEmail: deliveryEmailSchema2.optional(),
|
|
16712
16803
|
actions: external_exports2.array(shareActionSchema2).min(1).max(3),
|
|
16713
16804
|
resource: resourceSelectorSchema2,
|
|
16714
16805
|
target: v2TargetSchema2,
|
|
@@ -16848,7 +16939,7 @@ var unsignedShareEnvelopeV3BaseSchema2 = external_exports2.object({
|
|
|
16848
16939
|
version: external_exports2.literal(3),
|
|
16849
16940
|
shareId: external_exports2.string().min(1),
|
|
16850
16941
|
recipientMatcher: recipientMatcherSchema2,
|
|
16851
|
-
deliveryEmail:
|
|
16942
|
+
deliveryEmail: deliveryEmailSchema2.optional(),
|
|
16852
16943
|
actions: external_exports2.array(shareActionSchema2).min(1).max(3),
|
|
16853
16944
|
resource: resourceSelectorSchema2,
|
|
16854
16945
|
target: v3TargetSchema2,
|
|
@@ -17354,6 +17445,12 @@ function createShareAuthorityAdapters(input = {}) {
|
|
|
17354
17445
|
policyActions,
|
|
17355
17446
|
contentSource,
|
|
17356
17447
|
...prepared.credentialRequirement === void 0 ? {} : { credentialRequirement: prepared.credentialRequirement },
|
|
17448
|
+
// tinycloud-node 1.17.2 signs a delivery receipt only for the envelope's
|
|
17449
|
+
// own signed delivery address (1.17.3 accepts it and no longer requires
|
|
17450
|
+
// it), so an exact-email share pins its canonical mailbox for `--notify`
|
|
17451
|
+
// and `tc share notify`. A mailbox the envelope's `deliveryEmail` rule
|
|
17452
|
+
// rejects (deployed viewers validate with it) is published unpinned.
|
|
17453
|
+
...prepared.target.kind === "email" && isEnvelopeDeliveryEmail2(prepared.target.address) ? { deliveryEmail: prepared.target.address } : {},
|
|
17357
17454
|
filename: targetInput.filename,
|
|
17358
17455
|
mediaType,
|
|
17359
17456
|
byteLength,
|