@tinycloud/cli 1.1.0-beta.7 → 1.1.0-beta.9
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 +101 -49
- package/dist/index.js.map +1 -1
- package/dist/legacy-entry.js +395 -178
- package/dist/legacy-entry.js.map +1 -1
- package/package.json +5 -5
- package/skills/tc-cli/AUTH.md +1 -1
- package/skills/tc-cli/REFERENCE.md +18 -1
package/dist/index.js
CHANGED
|
@@ -72,7 +72,7 @@ var init_theme = __esm({
|
|
|
72
72
|
|
|
73
73
|
// src/output/formatter.ts
|
|
74
74
|
import ora from "ora";
|
|
75
|
-
function outputError(code3, message, hint) {
|
|
75
|
+
function outputError(code3, message, hint, meta) {
|
|
76
76
|
if (isInteractive()) {
|
|
77
77
|
process.stderr.write(
|
|
78
78
|
`${theme.error("\u2717")} ${theme.label(code3)}: ${message}
|
|
@@ -89,6 +89,7 @@ function outputError(code3, message, hint) {
|
|
|
89
89
|
error: { code: code3, message }
|
|
90
90
|
};
|
|
91
91
|
if (hint) payload.error.hint = hint;
|
|
92
|
+
if (meta) payload.error.meta = meta;
|
|
92
93
|
process.stderr.write(JSON.stringify(payload, null, 2) + "\n");
|
|
93
94
|
}
|
|
94
95
|
}
|
|
@@ -110,24 +111,24 @@ var init_formatter = __esm({
|
|
|
110
111
|
// src/output/errors.ts
|
|
111
112
|
import { readFileSync, readdirSync } from "fs";
|
|
112
113
|
import { join } from "path";
|
|
113
|
-
import { ProfileLockTimeoutError } from "@tinycloud/operations/state";
|
|
114
|
+
import { ProfileDeletedError, ProfileLockTimeoutError } from "@tinycloud/operations/state";
|
|
115
|
+
import { authorizationVerdictOf, parseCapabilityResource, SERVICE_LONG_TO_SHORT, validatedCapabilityOf } from "@tinycloud/sdk-core";
|
|
114
116
|
function setActiveProfileName(name) {
|
|
115
117
|
activeProfileName = name;
|
|
116
118
|
}
|
|
117
119
|
function wrapError(error) {
|
|
120
|
+
if (error instanceof CLIError) return error;
|
|
118
121
|
const message = error instanceof Error ? error.message : String(error);
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
return new CLIError(
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
);
|
|
122
|
+
const verdict = authorizationVerdictOf(error);
|
|
123
|
+
if (verdict === "unauthenticated") {
|
|
124
|
+
return new CLIError("AUTH_REQUIRED", message, ExitCode.AUTH_REQUIRED);
|
|
125
|
+
}
|
|
126
|
+
if (verdict === "forbidden") {
|
|
127
|
+
return new CLIError("PERMISSION_DENIED", message, ExitCode.PERMISSION_DENIED);
|
|
128
|
+
}
|
|
129
|
+
if (verdict === void 0 && message.includes("Missing private key parameter in JWK")) {
|
|
130
|
+
return missingPrivateKeyError();
|
|
129
131
|
}
|
|
130
|
-
if (error instanceof CLIError) return error;
|
|
131
132
|
if (error instanceof ProfileLockTimeoutError) {
|
|
132
133
|
return new CLIError(
|
|
133
134
|
"PROFILE_LOCK_TIMEOUT",
|
|
@@ -136,13 +137,16 @@ function wrapError(error) {
|
|
|
136
137
|
{ hint: "Wait for the other command to finish and retry. A crashed process's lock is reclaimed automatically after 30 s." }
|
|
137
138
|
);
|
|
138
139
|
}
|
|
139
|
-
if (
|
|
140
|
+
if (error instanceof ProfileDeletedError) {
|
|
141
|
+
return new CLIError("PROFILE_NOT_FOUND", message);
|
|
142
|
+
}
|
|
143
|
+
if (verdict === void 0 && (message.includes("Not signed in") || message.includes("AUTH_EXPIRED") || message.includes("Session expired"))) {
|
|
140
144
|
return new CLIError("AUTH_REQUIRED", message, ExitCode.AUTH_REQUIRED);
|
|
141
145
|
}
|
|
142
146
|
if (message.includes("NOT_FOUND") || message.includes("KV_NOT_FOUND")) {
|
|
143
147
|
return new CLIError("NOT_FOUND", message, ExitCode.NOT_FOUND);
|
|
144
148
|
}
|
|
145
|
-
if (message.includes("PERMISSION_DENIED")) {
|
|
149
|
+
if (verdict === void 0 && message.includes("PERMISSION_DENIED")) {
|
|
146
150
|
return new CLIError("PERMISSION_DENIED", message, ExitCode.PERMISSION_DENIED);
|
|
147
151
|
}
|
|
148
152
|
if (message.includes("ECONNREFUSED") || message.includes("ETIMEDOUT") || message.includes("fetch failed")) {
|
|
@@ -150,39 +154,51 @@ function wrapError(error) {
|
|
|
150
154
|
}
|
|
151
155
|
return new CLIError("ERROR", message, ExitCode.ERROR);
|
|
152
156
|
}
|
|
157
|
+
function missingPrivateKeyError() {
|
|
158
|
+
const profileName = activeProfileName ?? process.env.TC_PROFILE ?? DEFAULT_PROFILE;
|
|
159
|
+
return new CLIError(
|
|
160
|
+
"AUTH_REQUIRED",
|
|
161
|
+
`Profile "${profileName}" cannot restore its session because its private key material is missing.`,
|
|
162
|
+
ExitCode.AUTH_REQUIRED,
|
|
163
|
+
{ hint: `Sign in again with: tc --profile ${profileName} auth login --method openkey` }
|
|
164
|
+
);
|
|
165
|
+
}
|
|
153
166
|
function handleError(error) {
|
|
154
167
|
const cliError = wrapError(error);
|
|
155
168
|
const prebuilt = typeof cliError.metadata?.hint === "string" ? cliError.metadata.hint : void 0;
|
|
156
169
|
const hint = prebuilt ?? buildAuthHint(cliError) ?? (cliError.code === "NETWORK_ERROR" ? buildNetworkHint() : void 0);
|
|
157
|
-
|
|
170
|
+
const authMeta = cliError.code === "AUTH_REQUIRED" || cliError.code === "PERMISSION_DENIED" ? cliError.metadata : void 0;
|
|
171
|
+
const meta = {};
|
|
172
|
+
if (cliError.status !== void 0 && authMeta !== void 0) meta.status = cliError.status;
|
|
173
|
+
const capability = validatedCapabilityOf({ meta: authMeta });
|
|
174
|
+
if (capability) {
|
|
175
|
+
meta.resource = capability.resource;
|
|
176
|
+
meta.requiredAction = capability.requiredAction;
|
|
177
|
+
}
|
|
178
|
+
outputError(cliError.code, cliError.message, hint, Object.keys(meta).length ? meta : void 0);
|
|
158
179
|
process.exit(cliError.exitCode);
|
|
159
180
|
}
|
|
160
181
|
function buildAuthHint(error) {
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
return void 0;
|
|
165
|
-
}
|
|
166
|
-
const spec = capSpecFromAuthMeta(resource, requiredAction);
|
|
182
|
+
const capability = validatedCapabilityOf({ meta: error.metadata });
|
|
183
|
+
if (!capability) return void 0;
|
|
184
|
+
const spec = capSpecFromAuthMeta(capability.resource, capability.requiredAction);
|
|
167
185
|
if (!spec) return void 0;
|
|
168
186
|
return [
|
|
169
187
|
"The active session is missing a TinyCloud capability.",
|
|
170
|
-
`Request it with: tc auth request --cap
|
|
188
|
+
`Request it with: tc auth request --cap '${spec.replaceAll("'", "'\\''")}'`,
|
|
171
189
|
"Then retry the original command."
|
|
172
190
|
].join("\n");
|
|
173
191
|
}
|
|
174
192
|
function capSpecFromAuthMeta(resource, action) {
|
|
175
|
-
const slash =
|
|
176
|
-
if (slash
|
|
177
|
-
const
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
const spaceName = spaceUri.startsWith("tinycloud:") ? spaceUri.slice(spaceUri.lastIndexOf(":") + 1) : spaceUri;
|
|
185
|
-
return `tinycloud.${serviceShort}:${spaceName}:${path}:${actionName}`;
|
|
193
|
+
const slash = action.indexOf("/");
|
|
194
|
+
if (slash < 0) return void 0;
|
|
195
|
+
const longService = action.slice(0, slash);
|
|
196
|
+
const serviceShort = SERVICE_LONG_TO_SHORT[longService];
|
|
197
|
+
if (!serviceShort) return void 0;
|
|
198
|
+
const parsed = parseCapabilityResource(resource, serviceShort);
|
|
199
|
+
if (!parsed) return void 0;
|
|
200
|
+
const spaceName = parsed.space.slice(parsed.space.lastIndexOf(":") + 1);
|
|
201
|
+
return `${longService}:${spaceName}:${parsed.path}:${action.slice(slash + 1)}`;
|
|
186
202
|
}
|
|
187
203
|
function buildNetworkHint() {
|
|
188
204
|
const readHost = (name) => {
|
|
@@ -236,7 +252,9 @@ var init_errors = __esm({
|
|
|
236
252
|
this.exitCode = exitCode;
|
|
237
253
|
this.metadata = metadata;
|
|
238
254
|
this.name = "CLIError";
|
|
255
|
+
if (typeof metadata?.status === "number" && Number.isInteger(metadata.status) && metadata.status >= 400 && metadata.status <= 599) this.status = metadata.status;
|
|
239
256
|
}
|
|
257
|
+
status;
|
|
240
258
|
};
|
|
241
259
|
}
|
|
242
260
|
});
|
|
@@ -541,6 +559,8 @@ import { join as join3 } from "path";
|
|
|
541
559
|
import {
|
|
542
560
|
profilePath,
|
|
543
561
|
readSession,
|
|
562
|
+
recordProfileDeletion,
|
|
563
|
+
refuseWriteToDeletedProfile,
|
|
544
564
|
removeSession,
|
|
545
565
|
withProfileLock,
|
|
546
566
|
writeSession
|
|
@@ -655,10 +675,12 @@ var init_profiles = __esm({
|
|
|
655
675
|
* removed while holding the profile lock, so another writer's critical
|
|
656
676
|
* section never sees them vanish midway: session and key first, settings
|
|
657
677
|
* last, so a crash midway never leaves a session or key without its
|
|
658
|
-
* profile.
|
|
659
|
-
*
|
|
660
|
-
*
|
|
661
|
-
*
|
|
678
|
+
* profile. The deletion is then recorded, so a store write that waited for
|
|
679
|
+
* the lock meanwhile refuses rather than recreating a profile with only a
|
|
680
|
+
* session in it. `.lock` itself is left to the lock's release; the
|
|
681
|
+
* then-empty directory is removed afterwards unless another writer took
|
|
682
|
+
* the lock (or wrote) meanwhile. A profile directory that is a symlink is
|
|
683
|
+
* unlinked, its target left alone.
|
|
662
684
|
* Throws if the name is not one path segment or names the default profile.
|
|
663
685
|
*/
|
|
664
686
|
static async deleteProfile(name) {
|
|
@@ -679,11 +701,12 @@ var init_profiles = __esm({
|
|
|
679
701
|
`Cannot delete the default profile "${name}". Change the default first with \`tc profile default <other>\`.`
|
|
680
702
|
);
|
|
681
703
|
}
|
|
682
|
-
const
|
|
683
|
-
if (error.code === "ENOENT") return
|
|
704
|
+
const kind = await lstat(profileDir).then((stats) => stats.isSymbolicLink() ? "link" : "present", (error) => {
|
|
705
|
+
if (error.code === "ENOENT") return "missing";
|
|
684
706
|
throw error;
|
|
685
707
|
});
|
|
686
|
-
if (
|
|
708
|
+
if (kind === "missing") return;
|
|
709
|
+
if (kind === "link") {
|
|
687
710
|
await rm2(profileDir, { force: true });
|
|
688
711
|
return;
|
|
689
712
|
}
|
|
@@ -693,6 +716,7 @@ var init_profiles = __esm({
|
|
|
693
716
|
if (entry !== ".lock" && entry !== "profile.json") await rm2(join3(profileDir, entry), { recursive: true, force: true });
|
|
694
717
|
}
|
|
695
718
|
await rm2(join3(profileDir, "profile.json"), { force: true });
|
|
719
|
+
await recordProfileDeletion(name);
|
|
696
720
|
});
|
|
697
721
|
await rmdir(profileDir).catch((error) => {
|
|
698
722
|
if (error.code !== "ENOENT" && error.code !== "ENOTEMPTY" && error.code !== "EEXIST") throw error;
|
|
@@ -705,9 +729,14 @@ var init_profiles = __esm({
|
|
|
705
729
|
static async getKey(name) {
|
|
706
730
|
return readJson(join3(PROFILES_DIR, name, "key.json"));
|
|
707
731
|
}
|
|
708
|
-
/**
|
|
732
|
+
/**
|
|
733
|
+
* Saves a JWK key under the profile lock (0600, in an owner-only profile
|
|
734
|
+
* directory). Refused (PROFILE_NOT_FOUND) if the profile was deleted while
|
|
735
|
+
* this waited for the lock, rather than leaving a key-only profile.
|
|
736
|
+
*/
|
|
709
737
|
static async setKey(name, jwk) {
|
|
710
738
|
await _ProfileManager.withLock(name, async () => {
|
|
739
|
+
await refuseWriteToDeletedProfile(name);
|
|
711
740
|
await writeJson(join3(await _ProfileManager.ensureProfileDir(name), "key.json"), jwk);
|
|
712
741
|
});
|
|
713
742
|
}
|
|
@@ -5659,6 +5688,7 @@ import {
|
|
|
5659
5688
|
profileStoreMetadataPath,
|
|
5660
5689
|
readAdditionalDelegations,
|
|
5661
5690
|
readAuthRequests,
|
|
5691
|
+
refuseWriteToDeletedProfile as refuseWriteToDeletedProfile2,
|
|
5662
5692
|
updateProfileStore,
|
|
5663
5693
|
withProfileLock as withProfileLock2,
|
|
5664
5694
|
writeJsonAtomic
|
|
@@ -5885,13 +5915,13 @@ async function bootstrapDelegatedSession(ctx, delegation) {
|
|
|
5885
5915
|
throw error;
|
|
5886
5916
|
});
|
|
5887
5917
|
const session = await ProfileManager.getSession(ctx.profile);
|
|
5888
|
-
if (
|
|
5918
|
+
if (profile === null || BOOTSTRAP_PROFILE_FIELDS.some((field) => profile[field] !== written.profile[field]) || JSON.stringify(session) !== JSON.stringify(written.session)) {
|
|
5889
5919
|
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.`;
|
|
5890
5920
|
}
|
|
5891
5921
|
return restoreBeforeBootstrap(ctx.profile, written.previousProfile);
|
|
5892
5922
|
}, { timeoutMs: PROFILE_COMMIT_LOCK_TIMEOUT_MS }).catch((error) => (
|
|
5893
5923
|
// The lock or a read failed: the import's error stays the one reported.
|
|
5894
|
-
`Rolling back the provisional session of profile "${ctx.profile}" could not run (${
|
|
5924
|
+
`Rolling back the provisional session of profile "${ctx.profile}" could not run (${failureName(error)}); check \`tc --profile ${ctx.profile} context\`.`
|
|
5895
5925
|
));
|
|
5896
5926
|
throw annotate(cause, note);
|
|
5897
5927
|
};
|
|
@@ -5907,15 +5937,27 @@ async function restoreBeforeBootstrap(profileName, previousProfile) {
|
|
|
5907
5937
|
const failures = [];
|
|
5908
5938
|
for (const write of [
|
|
5909
5939
|
() => ProfileManager.clearSession(profileName),
|
|
5910
|
-
() => ProfileManager.
|
|
5940
|
+
() => ProfileManager.updateProfile(profileName, (current) => {
|
|
5941
|
+
const restored = { ...current };
|
|
5942
|
+
for (const field of BOOTSTRAP_PROFILE_FIELDS) {
|
|
5943
|
+
if (previousProfile[field] === void 0) delete restored[field];
|
|
5944
|
+
else restored[field] = previousProfile[field];
|
|
5945
|
+
}
|
|
5946
|
+
return restored;
|
|
5947
|
+
})
|
|
5911
5948
|
]) {
|
|
5912
5949
|
await write().catch((error) => {
|
|
5913
|
-
failures.push(
|
|
5950
|
+
failures.push(failureName(error));
|
|
5914
5951
|
});
|
|
5915
5952
|
}
|
|
5916
5953
|
if (failures.length === 0) return void 0;
|
|
5917
5954
|
return `Rolling back the provisional session of profile "${profileName}" failed too (${failures.join("; ")}); check \`tc --profile ${profileName} context\`.`;
|
|
5918
5955
|
}
|
|
5956
|
+
function failureName(error) {
|
|
5957
|
+
if (error instanceof SyntaxError) return "a profile file is not valid JSON";
|
|
5958
|
+
if (typeof error === "object" && error !== null && "code" in error && typeof error.code === "string") return error.code;
|
|
5959
|
+
return error instanceof Error ? error.name : "unknown error";
|
|
5960
|
+
}
|
|
5919
5961
|
function annotate(error, note) {
|
|
5920
5962
|
if (note === void 0) return error;
|
|
5921
5963
|
const cause = wrapError(error);
|
|
@@ -5939,6 +5981,7 @@ async function ensureAuthenticated(ctx, options) {
|
|
|
5939
5981
|
}
|
|
5940
5982
|
return createSDKInstance(ctx, options);
|
|
5941
5983
|
}
|
|
5984
|
+
var BOOTSTRAP_PROFILE_FIELDS;
|
|
5942
5985
|
var init_sdk = __esm({
|
|
5943
5986
|
"src/lib/sdk.ts"() {
|
|
5944
5987
|
"use strict";
|
|
@@ -5947,6 +5990,7 @@ var init_sdk = __esm({
|
|
|
5947
5990
|
init_errors();
|
|
5948
5991
|
init_constants();
|
|
5949
5992
|
init_permissions();
|
|
5993
|
+
BOOTSTRAP_PROFILE_FIELDS = ["sessionDid", "spaceId"];
|
|
5950
5994
|
}
|
|
5951
5995
|
});
|
|
5952
5996
|
|
|
@@ -14990,7 +15034,6 @@ async function notifyShare(input) {
|
|
|
14990
15034
|
};
|
|
14991
15035
|
}
|
|
14992
15036
|
let attempts = 0;
|
|
14993
|
-
let lastError;
|
|
14994
15037
|
while (attempts < attemptsLimit) {
|
|
14995
15038
|
if (input.signal?.aborted) throw new ShareNotifyError("share delivery was cancelled");
|
|
14996
15039
|
attempts += 1;
|
|
@@ -15017,10 +15060,19 @@ async function notifyShare(input) {
|
|
|
15017
15060
|
reason: "delivery-window-expired"
|
|
15018
15061
|
};
|
|
15019
15062
|
}
|
|
15020
|
-
|
|
15063
|
+
if (error !== null && typeof error === "object" && "status" in error && (error.status === 401 || error.status === 403)) {
|
|
15064
|
+
return {
|
|
15065
|
+
protocol: "tinycloud-share",
|
|
15066
|
+
version: 1,
|
|
15067
|
+
shareId: input.shareId,
|
|
15068
|
+
state: "partial-failure",
|
|
15069
|
+
idempotencyKey,
|
|
15070
|
+
attempts,
|
|
15071
|
+
retryable: false
|
|
15072
|
+
};
|
|
15073
|
+
}
|
|
15021
15074
|
}
|
|
15022
15075
|
}
|
|
15023
|
-
void lastError;
|
|
15024
15076
|
return { protocol: "tinycloud-share", version: 1, shareId: input.shareId, state: "partial-failure", idempotencyKey, attempts, retryable: true };
|
|
15025
15077
|
}
|
|
15026
15078
|
async function defaultIdempotencyKey(shareId, recipient) {
|