@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/legacy-entry.js
CHANGED
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
tinycloudConfigPath,
|
|
38
38
|
tinycloudHomePath
|
|
39
39
|
} from "@tinycloud/operations/state";
|
|
40
|
-
var CONFIG_DIR, PROFILES_DIR, CONFIG_FILE, DEFAULT_HOST, DEFAULT_OPENKEY_HOST, DEFAULT_OPENKEY_DEVICE_API_HOST, DEFAULT_SHARE_ORIGIN, DEFAULT_PROFILE, DEFAULT_CHAIN_ID, ExitCode;
|
|
40
|
+
var CONFIG_DIR, PROFILES_DIR, CONFIG_FILE, DEFAULT_HOST, DEFAULT_OPENKEY_HOST, DEFAULT_OPENKEY_DEVICE_API_HOST, DEFAULT_SHARE_ORIGIN, DEFAULT_PROFILE, DEFAULT_CHAIN_ID, PROFILE_COMMIT_LOCK_TIMEOUT_MS, ExitCode;
|
|
41
41
|
var init_constants = __esm({
|
|
42
42
|
"src/config/constants.ts"() {
|
|
43
43
|
"use strict";
|
|
@@ -50,6 +50,7 @@ var init_constants = __esm({
|
|
|
50
50
|
DEFAULT_SHARE_ORIGIN = "https://share.tinycloud.xyz";
|
|
51
51
|
DEFAULT_PROFILE = "default";
|
|
52
52
|
DEFAULT_CHAIN_ID = 1;
|
|
53
|
+
PROFILE_COMMIT_LOCK_TIMEOUT_MS = 45e3;
|
|
53
54
|
ExitCode = {
|
|
54
55
|
SUCCESS: 0,
|
|
55
56
|
ERROR: 1,
|
|
@@ -104,9 +105,6 @@ async function fileExists(filePath) {
|
|
|
104
105
|
async function ensureDir(dirPath) {
|
|
105
106
|
await mkdir(dirPath, { recursive: true, mode: PRIVATE_DIR_MODE });
|
|
106
107
|
}
|
|
107
|
-
async function removeDir(dirPath) {
|
|
108
|
-
await rm(dirPath, { recursive: true, force: true });
|
|
109
|
-
}
|
|
110
108
|
async function listDirs(dirPath) {
|
|
111
109
|
try {
|
|
112
110
|
const entries = await readdir(dirPath, { withFileTypes: true });
|
|
@@ -641,9 +639,10 @@ var init_host = __esm({
|
|
|
641
639
|
});
|
|
642
640
|
|
|
643
641
|
// src/config/profiles.ts
|
|
644
|
-
import { chmod, rm as rm2 } from "fs/promises";
|
|
642
|
+
import { chmod, lstat, readdir as readdir2, rm as rm2, rmdir } from "fs/promises";
|
|
645
643
|
import { join as join3 } from "path";
|
|
646
644
|
import {
|
|
645
|
+
profilePath,
|
|
647
646
|
readSession,
|
|
648
647
|
removeSession,
|
|
649
648
|
withProfileLock,
|
|
@@ -719,8 +718,8 @@ var init_profiles = __esm({
|
|
|
719
718
|
* Throws CLIError if the profile doesn't exist.
|
|
720
719
|
*/
|
|
721
720
|
static async getProfile(name) {
|
|
722
|
-
const
|
|
723
|
-
const profile = await readJson(
|
|
721
|
+
const profilePath2 = join3(PROFILES_DIR, name, "profile.json");
|
|
722
|
+
const profile = await readJson(profilePath2);
|
|
724
723
|
if (!profile) {
|
|
725
724
|
throw new CLIError(
|
|
726
725
|
"PROFILE_NOT_FOUND",
|
|
@@ -755,10 +754,27 @@ var init_profiles = __esm({
|
|
|
755
754
|
return listDirs(PROFILES_DIR);
|
|
756
755
|
}
|
|
757
756
|
/**
|
|
758
|
-
* Deletes a profile
|
|
759
|
-
*
|
|
757
|
+
* Deletes a profile. Its key, session, settings, stores and cache are
|
|
758
|
+
* removed while holding the profile lock, so another writer's critical
|
|
759
|
+
* section never sees them vanish midway: session and key first, settings
|
|
760
|
+
* last, so a crash midway never leaves a session or key without its
|
|
761
|
+
* profile. `.lock` itself is left to the lock's release; the then-empty
|
|
762
|
+
* directory is removed afterwards unless another writer took the lock (or
|
|
763
|
+
* wrote) meanwhile. A profile directory that is a symlink is unlinked, its
|
|
764
|
+
* target left alone.
|
|
765
|
+
* Throws if the name is not one path segment or names the default profile.
|
|
760
766
|
*/
|
|
761
767
|
static async deleteProfile(name) {
|
|
768
|
+
let profileDir;
|
|
769
|
+
try {
|
|
770
|
+
profileDir = profilePath(name);
|
|
771
|
+
} catch {
|
|
772
|
+
throw new CLIError(
|
|
773
|
+
"INVALID_PROFILE_NAME",
|
|
774
|
+
`Invalid profile name "${name}": a profile name is one path segment (no "/", "\\", "." or "..").`,
|
|
775
|
+
ExitCode.USAGE_ERROR
|
|
776
|
+
);
|
|
777
|
+
}
|
|
762
778
|
const config = await _ProfileManager.getConfig();
|
|
763
779
|
if (config.defaultProfile === name) {
|
|
764
780
|
throw new CLIError(
|
|
@@ -766,8 +782,24 @@ var init_profiles = __esm({
|
|
|
766
782
|
`Cannot delete the default profile "${name}". Change the default first with \`tc profile default <other>\`.`
|
|
767
783
|
);
|
|
768
784
|
}
|
|
769
|
-
const
|
|
770
|
-
|
|
785
|
+
const isLink = await lstat(profileDir).then((stats) => stats.isSymbolicLink(), (error) => {
|
|
786
|
+
if (error.code === "ENOENT") return false;
|
|
787
|
+
throw error;
|
|
788
|
+
});
|
|
789
|
+
if (isLink) {
|
|
790
|
+
await rm2(profileDir, { force: true });
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
await _ProfileManager.withLock(name, async () => {
|
|
794
|
+
for (const file of ["session.json", "key.json"]) await rm2(join3(profileDir, file), { force: true });
|
|
795
|
+
for (const entry of await readdir2(profileDir)) {
|
|
796
|
+
if (entry !== ".lock" && entry !== "profile.json") await rm2(join3(profileDir, entry), { recursive: true, force: true });
|
|
797
|
+
}
|
|
798
|
+
await rm2(join3(profileDir, "profile.json"), { force: true });
|
|
799
|
+
});
|
|
800
|
+
await rmdir(profileDir).catch((error) => {
|
|
801
|
+
if (error.code !== "ENOENT" && error.code !== "ENOTEMPTY" && error.code !== "EEXIST") throw error;
|
|
802
|
+
});
|
|
771
803
|
}
|
|
772
804
|
// ── Key management ──────────────────────────────────────────────────
|
|
773
805
|
/**
|
|
@@ -7250,7 +7282,7 @@ var KVService = class extends BaseService {
|
|
|
7250
7282
|
return err(
|
|
7251
7283
|
storageQuotaExceededError(
|
|
7252
7284
|
"kv",
|
|
7253
|
-
`Storage quota exceeded for key
|
|
7285
|
+
`Storage quota exceeded for key ${JSON.stringify(key)}: ${errorText}`,
|
|
7254
7286
|
{
|
|
7255
7287
|
status: response.status,
|
|
7256
7288
|
...quotaInfo ? { usedBytes: quotaInfo.usedBytes, limitBytes: quotaInfo.limitBytes } : {}
|
|
@@ -7263,7 +7295,7 @@ var KVService = class extends BaseService {
|
|
|
7263
7295
|
return err(
|
|
7264
7296
|
storageLimitReachedError(
|
|
7265
7297
|
"kv",
|
|
7266
|
-
`Storage limit reached for key
|
|
7298
|
+
`Storage limit reached for key ${JSON.stringify(key)}: ${errorText}`,
|
|
7267
7299
|
{
|
|
7268
7300
|
status: response.status,
|
|
7269
7301
|
...quotaInfo ? { usedBytes: quotaInfo.usedBytes, limitBytes: quotaInfo.limitBytes } : {}
|
|
@@ -7549,12 +7581,11 @@ var KVService = class extends BaseService {
|
|
|
7549
7581
|
if (!response.ok) {
|
|
7550
7582
|
const errorText = await response.text();
|
|
7551
7583
|
if (response.status === 401 || response.status === 403) {
|
|
7552
|
-
|
|
7553
|
-
|
|
7554
|
-
|
|
7555
|
-
|
|
7556
|
-
|
|
7557
|
-
}));
|
|
7584
|
+
return this.authorizationFailure(
|
|
7585
|
+
`Failed to batch read ${keys.length} key(s)`,
|
|
7586
|
+
response,
|
|
7587
|
+
errorText
|
|
7588
|
+
);
|
|
7558
7589
|
}
|
|
7559
7590
|
if (response.status === 413) {
|
|
7560
7591
|
return err(serviceError(
|
|
@@ -7653,23 +7684,39 @@ var KVService = class extends BaseService {
|
|
|
7653
7684
|
} catch {
|
|
7654
7685
|
}
|
|
7655
7686
|
if (response.status === 401 || response.status === 403) {
|
|
7656
|
-
|
|
7657
|
-
|
|
7658
|
-
|
|
7659
|
-
|
|
7660
|
-
|
|
7661
|
-
}));
|
|
7687
|
+
return this.authorizationFailure(
|
|
7688
|
+
`Failed to create signed read URL for key ${JSON.stringify(key)}`,
|
|
7689
|
+
response,
|
|
7690
|
+
errorText
|
|
7691
|
+
);
|
|
7662
7692
|
}
|
|
7663
7693
|
const code2 = response.status === 400 ? ErrorCodes.INVALID_INPUT : ErrorCodes.NETWORK_ERROR;
|
|
7664
7694
|
return err(
|
|
7665
7695
|
serviceError(
|
|
7666
7696
|
code2,
|
|
7667
|
-
`Failed to create signed read URL for key
|
|
7697
|
+
`Failed to create signed read URL for key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
|
|
7668
7698
|
"kv",
|
|
7669
7699
|
{ meta: { status: response.status, statusText: response.statusText } }
|
|
7670
7700
|
)
|
|
7671
7701
|
);
|
|
7672
7702
|
}
|
|
7703
|
+
/**
|
|
7704
|
+
* AUTH_UNAUTHORIZED for a 401/403 response. The message keeps the HTTP
|
|
7705
|
+
* status and the server text (`<context>: <status> - <text>`, the same
|
|
7706
|
+
* shape as every other KV failure) so callers that rethrow only the message
|
|
7707
|
+
* still see the status; `meta.status` carries it for typed callers.
|
|
7708
|
+
* Resource/ability hints are parsed from the raw body, never the message.
|
|
7709
|
+
*/
|
|
7710
|
+
authorizationFailure(context, response, errorText, extraMeta = {}) {
|
|
7711
|
+
const { resource, action } = parseAuthError(errorText);
|
|
7712
|
+
const detail = errorText.trim().length > 0 ? errorText : response.statusText || "authorization failed";
|
|
7713
|
+
return err(authUnauthorizedError("kv", `${context}: ${response.status} - ${detail}`, {
|
|
7714
|
+
status: response.status,
|
|
7715
|
+
...action && { requiredAction: action },
|
|
7716
|
+
...resource && { resource },
|
|
7717
|
+
...extraMeta
|
|
7718
|
+
}));
|
|
7719
|
+
}
|
|
7673
7720
|
normalizeSignedReadUrlResponse(data) {
|
|
7674
7721
|
if (!data || typeof data !== "object") {
|
|
7675
7722
|
return void 0;
|
|
@@ -7712,14 +7759,13 @@ var KVService = class extends BaseService {
|
|
|
7712
7759
|
if (!response.ok) {
|
|
7713
7760
|
if (response.status === 401 || response.status === 403) {
|
|
7714
7761
|
const errorText2 = await response.text();
|
|
7715
|
-
const { resource, action } = parseAuthError(errorText2);
|
|
7716
7762
|
const permissionHint = parsePermissionHintFromErrorText(errorText2);
|
|
7717
|
-
return
|
|
7718
|
-
|
|
7719
|
-
|
|
7720
|
-
|
|
7721
|
-
|
|
7722
|
-
|
|
7763
|
+
return this.authorizationFailure(
|
|
7764
|
+
`Failed to get key ${JSON.stringify(key)}`,
|
|
7765
|
+
response,
|
|
7766
|
+
errorText2,
|
|
7767
|
+
permissionHint === void 0 ? {} : { permissionHint }
|
|
7768
|
+
);
|
|
7723
7769
|
}
|
|
7724
7770
|
if (response.status === 404) {
|
|
7725
7771
|
return this.classifyNotFound(response, key);
|
|
@@ -7728,7 +7774,7 @@ var KVService = class extends BaseService {
|
|
|
7728
7774
|
if (response.status === 413) {
|
|
7729
7775
|
return err(serviceError(
|
|
7730
7776
|
ErrorCodes.KV_RESPONSE_TOO_LARGE,
|
|
7731
|
-
`KV value at key
|
|
7777
|
+
`KV value at key ${JSON.stringify(key)} exceeds the requested response limit`,
|
|
7732
7778
|
"kv",
|
|
7733
7779
|
{ meta: { status: response.status, statusText: response.statusText } }
|
|
7734
7780
|
));
|
|
@@ -7736,7 +7782,7 @@ var KVService = class extends BaseService {
|
|
|
7736
7782
|
return err(
|
|
7737
7783
|
serviceError(
|
|
7738
7784
|
ErrorCodes.NETWORK_ERROR,
|
|
7739
|
-
`Failed to get key
|
|
7785
|
+
`Failed to get key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
|
|
7740
7786
|
"kv",
|
|
7741
7787
|
{ meta: { status: response.status, statusText: response.statusText } }
|
|
7742
7788
|
)
|
|
@@ -7792,21 +7838,18 @@ var KVService = class extends BaseService {
|
|
|
7792
7838
|
}
|
|
7793
7839
|
);
|
|
7794
7840
|
if (response.status === 401 || response.status === 403) {
|
|
7795
|
-
|
|
7796
|
-
|
|
7797
|
-
|
|
7798
|
-
|
|
7799
|
-
|
|
7800
|
-
...requiredAction && { requiredAction },
|
|
7801
|
-
...resource && { resource }
|
|
7802
|
-
}));
|
|
7841
|
+
return this.authorizationFailure(
|
|
7842
|
+
`Failed to put key ${JSON.stringify(key)}`,
|
|
7843
|
+
response,
|
|
7844
|
+
await response.text()
|
|
7845
|
+
);
|
|
7803
7846
|
}
|
|
7804
7847
|
if (!response.ok) {
|
|
7805
7848
|
const errorText = await response.text();
|
|
7806
7849
|
if (response.status === 412) {
|
|
7807
7850
|
return err(serviceError(
|
|
7808
7851
|
ErrorCodes.KV_PRECONDITION_FAILED,
|
|
7809
|
-
`KV precondition failed for key
|
|
7852
|
+
`KV precondition failed for key ${JSON.stringify(key)}`,
|
|
7810
7853
|
"kv",
|
|
7811
7854
|
{ meta: { status: response.status, statusText: response.statusText } }
|
|
7812
7855
|
));
|
|
@@ -7814,7 +7857,7 @@ var KVService = class extends BaseService {
|
|
|
7814
7857
|
if (response.status === 503 && (options?.ifMatch !== void 0 || options?.ifNoneMatch !== void 0)) {
|
|
7815
7858
|
return err(serviceError(
|
|
7816
7859
|
ErrorCodes.KV_CONFLICT,
|
|
7817
|
-
`Concurrent KV update conflicted for key
|
|
7860
|
+
`Concurrent KV update conflicted for key ${JSON.stringify(key)}`,
|
|
7818
7861
|
"kv",
|
|
7819
7862
|
{ meta: { status: response.status, statusText: response.statusText } }
|
|
7820
7863
|
));
|
|
@@ -7830,7 +7873,7 @@ var KVService = class extends BaseService {
|
|
|
7830
7873
|
return err(
|
|
7831
7874
|
serviceError(
|
|
7832
7875
|
ErrorCodes.KV_WRITE_FAILED,
|
|
7833
|
-
`Failed to put key
|
|
7876
|
+
`Failed to put key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
|
|
7834
7877
|
"kv",
|
|
7835
7878
|
{ meta: { status: response.status, statusText: response.statusText } }
|
|
7836
7879
|
)
|
|
@@ -7924,12 +7967,11 @@ var KVService = class extends BaseService {
|
|
|
7924
7967
|
);
|
|
7925
7968
|
}
|
|
7926
7969
|
if (response.status === 401 || response.status === 403) {
|
|
7927
|
-
|
|
7928
|
-
|
|
7929
|
-
|
|
7930
|
-
|
|
7931
|
-
|
|
7932
|
-
}));
|
|
7970
|
+
return this.authorizationFailure(
|
|
7971
|
+
`Failed to batch put ${items.length} key(s)`,
|
|
7972
|
+
response,
|
|
7973
|
+
errorText
|
|
7974
|
+
);
|
|
7933
7975
|
}
|
|
7934
7976
|
const quotaError = this.handleQuotaErrorResponse(
|
|
7935
7977
|
response,
|
|
@@ -8050,13 +8092,11 @@ var KVService = class extends BaseService {
|
|
|
8050
8092
|
);
|
|
8051
8093
|
if (!response.ok) {
|
|
8052
8094
|
if (response.status === 401) {
|
|
8053
|
-
|
|
8054
|
-
|
|
8055
|
-
|
|
8056
|
-
|
|
8057
|
-
|
|
8058
|
-
...resource && { resource }
|
|
8059
|
-
}));
|
|
8095
|
+
return this.authorizationFailure(
|
|
8096
|
+
"Failed to list keys",
|
|
8097
|
+
response,
|
|
8098
|
+
await response.text()
|
|
8099
|
+
);
|
|
8060
8100
|
}
|
|
8061
8101
|
const errorText = await response.text();
|
|
8062
8102
|
return err(
|
|
@@ -8105,13 +8145,11 @@ var KVService = class extends BaseService {
|
|
|
8105
8145
|
);
|
|
8106
8146
|
if (!response.ok) {
|
|
8107
8147
|
if (response.status === 401) {
|
|
8108
|
-
|
|
8109
|
-
|
|
8110
|
-
|
|
8111
|
-
|
|
8112
|
-
|
|
8113
|
-
...resource && { resource }
|
|
8114
|
-
}));
|
|
8148
|
+
return this.authorizationFailure(
|
|
8149
|
+
`Failed to delete key ${JSON.stringify(key)}`,
|
|
8150
|
+
response,
|
|
8151
|
+
await response.text()
|
|
8152
|
+
);
|
|
8115
8153
|
}
|
|
8116
8154
|
if (response.status === 404) {
|
|
8117
8155
|
return this.classifyNotFound(response, key);
|
|
@@ -8120,7 +8158,7 @@ var KVService = class extends BaseService {
|
|
|
8120
8158
|
if (response.status === 412) {
|
|
8121
8159
|
return err(serviceError(
|
|
8122
8160
|
ErrorCodes.KV_PRECONDITION_FAILED,
|
|
8123
|
-
`KV precondition failed for key
|
|
8161
|
+
`KV precondition failed for key ${JSON.stringify(key)}`,
|
|
8124
8162
|
"kv",
|
|
8125
8163
|
{ meta: { status: response.status, statusText: response.statusText } }
|
|
8126
8164
|
));
|
|
@@ -8128,7 +8166,7 @@ var KVService = class extends BaseService {
|
|
|
8128
8166
|
if (response.status === 503 && options?.ifMatch !== void 0) {
|
|
8129
8167
|
return err(serviceError(
|
|
8130
8168
|
ErrorCodes.KV_CONFLICT,
|
|
8131
|
-
`Concurrent KV delete conflicted for key
|
|
8169
|
+
`Concurrent KV delete conflicted for key ${JSON.stringify(key)}`,
|
|
8132
8170
|
"kv",
|
|
8133
8171
|
{ meta: { status: response.status, statusText: response.statusText } }
|
|
8134
8172
|
));
|
|
@@ -8136,7 +8174,7 @@ var KVService = class extends BaseService {
|
|
|
8136
8174
|
return err(
|
|
8137
8175
|
serviceError(
|
|
8138
8176
|
ErrorCodes.NETWORK_ERROR,
|
|
8139
|
-
`Failed to delete key
|
|
8177
|
+
`Failed to delete key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
|
|
8140
8178
|
"kv",
|
|
8141
8179
|
{ meta: { status: response.status, statusText: response.statusText } }
|
|
8142
8180
|
)
|
|
@@ -8169,13 +8207,11 @@ var KVService = class extends BaseService {
|
|
|
8169
8207
|
);
|
|
8170
8208
|
if (!response.ok) {
|
|
8171
8209
|
if (response.status === 401) {
|
|
8172
|
-
|
|
8173
|
-
|
|
8174
|
-
|
|
8175
|
-
|
|
8176
|
-
|
|
8177
|
-
...resource && { resource }
|
|
8178
|
-
}));
|
|
8210
|
+
return this.authorizationFailure(
|
|
8211
|
+
`Failed to get metadata for key ${JSON.stringify(key)}`,
|
|
8212
|
+
response,
|
|
8213
|
+
await response.text()
|
|
8214
|
+
);
|
|
8179
8215
|
}
|
|
8180
8216
|
if (response.status === 404) {
|
|
8181
8217
|
return this.classifyNotFound(response, key);
|
|
@@ -8184,7 +8220,7 @@ var KVService = class extends BaseService {
|
|
|
8184
8220
|
return err(
|
|
8185
8221
|
serviceError(
|
|
8186
8222
|
ErrorCodes.NETWORK_ERROR,
|
|
8187
|
-
`Failed to get metadata for key
|
|
8223
|
+
`Failed to get metadata for key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
|
|
8188
8224
|
"kv",
|
|
8189
8225
|
{ meta: { status: response.status, statusText: response.statusText } }
|
|
8190
8226
|
)
|
|
@@ -9643,10 +9679,14 @@ async function responseError(service, message, response) {
|
|
|
9643
9679
|
}
|
|
9644
9680
|
} catch {
|
|
9645
9681
|
}
|
|
9646
|
-
|
|
9682
|
+
const error = wrapError2(
|
|
9647
9683
|
service,
|
|
9648
9684
|
new Error(`${message}: ${response.status} ${detail}`)
|
|
9649
9685
|
);
|
|
9686
|
+
return {
|
|
9687
|
+
...error,
|
|
9688
|
+
meta: { ...error.meta, status: response.status, statusText: response.statusText }
|
|
9689
|
+
};
|
|
9650
9690
|
}
|
|
9651
9691
|
function isAbortError(error) {
|
|
9652
9692
|
return error instanceof DOMException && error.name === "AbortError";
|
|
@@ -13493,39 +13533,89 @@ async function createSDKInstance(ctx, options) {
|
|
|
13493
13533
|
return node;
|
|
13494
13534
|
}
|
|
13495
13535
|
async function bootstrapDelegatedSession(ctx, delegation) {
|
|
13496
|
-
const
|
|
13497
|
-
|
|
13498
|
-
|
|
13499
|
-
|
|
13500
|
-
|
|
13501
|
-
|
|
13502
|
-
|
|
13503
|
-
|
|
13504
|
-
|
|
13505
|
-
|
|
13506
|
-
|
|
13507
|
-
|
|
13508
|
-
|
|
13509
|
-
|
|
13510
|
-
|
|
13511
|
-
|
|
13512
|
-
|
|
13513
|
-
|
|
13514
|
-
|
|
13515
|
-
|
|
13536
|
+
const written = await ProfileManager.withLock(ctx.profile, async () => {
|
|
13537
|
+
const previousProfile = await ProfileManager.getProfile(ctx.profile);
|
|
13538
|
+
if (resolveProfilePosture(previousProfile) !== "delegate-session") {
|
|
13539
|
+
throw new CLIError(
|
|
13540
|
+
"AUTH_REQUIRED",
|
|
13541
|
+
`Profile "${ctx.profile}" is not a delegate-session profile.`,
|
|
13542
|
+
ExitCode.AUTH_REQUIRED
|
|
13543
|
+
);
|
|
13544
|
+
}
|
|
13545
|
+
const sessionDid = previousProfile.sessionDid ?? previousProfile.did;
|
|
13546
|
+
if (delegation.delegateDID.split("#", 1)[0] !== sessionDid.split("#", 1)[0]) {
|
|
13547
|
+
throw new CLIError(
|
|
13548
|
+
"DELEGATION_AUDIENCE_MISMATCH",
|
|
13549
|
+
`Delegation targets ${delegation.delegateDID}, but profile "${ctx.profile}" uses ${sessionDid}.`,
|
|
13550
|
+
ExitCode.PERMISSION_DENIED
|
|
13551
|
+
);
|
|
13552
|
+
}
|
|
13553
|
+
if (await ProfileManager.getSession(ctx.profile) !== null) {
|
|
13554
|
+
throw new CLIError(
|
|
13555
|
+
"PROFILE_CHANGED_DURING_IMPORT",
|
|
13556
|
+
`Profile "${ctx.profile}" gained a session (another login or import) after this import checked it. Nothing was saved; run the import again.`,
|
|
13557
|
+
ExitCode.ERROR
|
|
13558
|
+
);
|
|
13559
|
+
}
|
|
13560
|
+
const jwk = signerJwkForProfile(ctx.profile, void 0, await ProfileManager.getKey(ctx.profile));
|
|
13561
|
+
const session = {
|
|
13516
13562
|
delegationHeader: delegation.delegationHeader,
|
|
13517
13563
|
delegationCid: delegation.cid,
|
|
13518
13564
|
spaceId: delegation.spaceId,
|
|
13519
13565
|
jwk,
|
|
13520
13566
|
verificationMethod: sessionDid
|
|
13521
|
-
}
|
|
13522
|
-
|
|
13523
|
-
|
|
13524
|
-
|
|
13525
|
-
|
|
13526
|
-
})
|
|
13567
|
+
};
|
|
13568
|
+
const profile = { ...previousProfile, sessionDid, spaceId: delegation.spaceId };
|
|
13569
|
+
try {
|
|
13570
|
+
await ProfileManager.setSession(ctx.profile, session);
|
|
13571
|
+
await ProfileManager.setProfile(ctx.profile, profile);
|
|
13572
|
+
} catch (error) {
|
|
13573
|
+
throw annotate(error, await restoreBeforeBootstrap(ctx.profile, previousProfile));
|
|
13574
|
+
}
|
|
13575
|
+
return { previousProfile, profile, session };
|
|
13527
13576
|
});
|
|
13528
|
-
|
|
13577
|
+
const abandon = async (cause) => {
|
|
13578
|
+
const note = await ProfileManager.withLock(ctx.profile, async () => {
|
|
13579
|
+
const profile = await ProfileManager.getProfile(ctx.profile).catch((error) => {
|
|
13580
|
+
if (error instanceof CLIError && error.code === "PROFILE_NOT_FOUND") return null;
|
|
13581
|
+
throw error;
|
|
13582
|
+
});
|
|
13583
|
+
const session = await ProfileManager.getSession(ctx.profile);
|
|
13584
|
+
if (JSON.stringify(profile) !== JSON.stringify(written.profile) || JSON.stringify(session) !== JSON.stringify(written.session)) {
|
|
13585
|
+
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.`;
|
|
13586
|
+
}
|
|
13587
|
+
return restoreBeforeBootstrap(ctx.profile, written.previousProfile);
|
|
13588
|
+
}, { timeoutMs: PROFILE_COMMIT_LOCK_TIMEOUT_MS }).catch((error) => (
|
|
13589
|
+
// The lock or a read failed: the import's error stays the one reported.
|
|
13590
|
+
`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\`.`
|
|
13591
|
+
));
|
|
13592
|
+
throw annotate(cause, note);
|
|
13593
|
+
};
|
|
13594
|
+
let node;
|
|
13595
|
+
try {
|
|
13596
|
+
node = await createSDKInstance(ctx);
|
|
13597
|
+
} catch (error) {
|
|
13598
|
+
return abandon(error);
|
|
13599
|
+
}
|
|
13600
|
+
return { node, abandon };
|
|
13601
|
+
}
|
|
13602
|
+
async function restoreBeforeBootstrap(profileName, previousProfile) {
|
|
13603
|
+
const failures = [];
|
|
13604
|
+
for (const write of [
|
|
13605
|
+
() => ProfileManager.clearSession(profileName),
|
|
13606
|
+
() => ProfileManager.setProfile(profileName, previousProfile)
|
|
13607
|
+
]) {
|
|
13608
|
+
await write().catch((error) => {
|
|
13609
|
+
failures.push(error instanceof Error ? error.message : String(error));
|
|
13610
|
+
});
|
|
13611
|
+
}
|
|
13612
|
+
if (failures.length === 0) return void 0;
|
|
13613
|
+
return `Rolling back the provisional session of profile "${profileName}" failed too (${failures.join("; ")}); check \`tc --profile ${profileName} context\`.`;
|
|
13614
|
+
}
|
|
13615
|
+
function annotate(error, note) {
|
|
13616
|
+
if (note === void 0) return error;
|
|
13617
|
+
const cause = wrapError(error);
|
|
13618
|
+
return new CLIError(cause.code, `${cause.message} ${note}`, cause.exitCode, cause.metadata);
|
|
13529
13619
|
}
|
|
13530
13620
|
async function ensureAuthenticated(ctx, options) {
|
|
13531
13621
|
if (options?.privateKey) {
|
|
@@ -14590,7 +14680,6 @@ function assertSessionReplaceable(profileName, snapshot, ownerDid, scope, newExp
|
|
|
14590
14680
|
ExitCode.USAGE_ERROR
|
|
14591
14681
|
);
|
|
14592
14682
|
}
|
|
14593
|
-
var COMMIT_LOCK_TIMEOUT_MS = 45e3;
|
|
14594
14683
|
async function restore(profileName, state) {
|
|
14595
14684
|
const writes = [
|
|
14596
14685
|
() => state.key === null ? ProfileManager.removeKey(profileName) : ProfileManager.setKey(profileName, state.key),
|
|
@@ -14616,7 +14705,7 @@ async function commitLogin(profileName, snapshot, commit) {
|
|
|
14616
14705
|
if (canonicalJson(current.profile) !== canonicalJson(snapshot.profile) || canonicalJson(current.key) !== canonicalJson(snapshot.key) || canonicalJson(current.session) !== canonicalJson(snapshot.session)) {
|
|
14617
14706
|
throw new CLIError(
|
|
14618
14707
|
"PROFILE_CHANGED_DURING_LOGIN",
|
|
14619
|
-
`Profile "${profileName}" changed while
|
|
14708
|
+
`Profile "${profileName}" changed while this login was in progress (another login, key rotation or logout). Nothing was saved; check \`tc --profile ${profileName} context\` and run the login again if it is still needed.`,
|
|
14620
14709
|
ExitCode.ERROR
|
|
14621
14710
|
);
|
|
14622
14711
|
}
|
|
@@ -14637,11 +14726,11 @@ async function commitLogin(profileName, snapshot, commit) {
|
|
|
14637
14726
|
ExitCode.ERROR
|
|
14638
14727
|
);
|
|
14639
14728
|
}
|
|
14640
|
-
}, { timeoutMs:
|
|
14729
|
+
}, { timeoutMs: PROFILE_COMMIT_LOCK_TIMEOUT_MS }).catch((error) => {
|
|
14641
14730
|
if (!(error instanceof ProfileLockTimeoutError2)) throw error;
|
|
14642
14731
|
throw new CLIError(
|
|
14643
14732
|
"PROFILE_LOCK_TIMEOUT",
|
|
14644
|
-
`Another tc process kept profile "${profileName}" locked for ${
|
|
14733
|
+
`Another tc process kept profile "${profileName}" locked for ${PROFILE_COMMIT_LOCK_TIMEOUT_MS / 1e3} s, so the approved login was not saved. Wait for it to finish (a crashed process's lock is reclaimed after 30 s) and run the login again.`,
|
|
14645
14734
|
ExitCode.ERROR
|
|
14646
14735
|
);
|
|
14647
14736
|
});
|
|
@@ -15432,7 +15521,7 @@ function registerAuthCommand(program) {
|
|
|
15432
15521
|
const profile = await ProfileManager.getProfile(ctx.profile);
|
|
15433
15522
|
const session = await ProfileManager.getSession(ctx.profile);
|
|
15434
15523
|
if (session || resolveProfilePosture(profile) !== "delegate-session") throw error;
|
|
15435
|
-
node = await bootstrapDelegatedSession(ctx, imported.delegation);
|
|
15524
|
+
node = (await bootstrapDelegatedSession(ctx, imported.delegation)).node;
|
|
15436
15525
|
}
|
|
15437
15526
|
await appendAdditionalDelegation(ctx.profile, storedAdditionalDelegation(
|
|
15438
15527
|
imported.delegation,
|
|
@@ -15757,15 +15846,11 @@ async function importRequestBoundDelegationWithBootstrap(ctx, artifact) {
|
|
|
15757
15846
|
}
|
|
15758
15847
|
const candidate = artifact;
|
|
15759
15848
|
const delegation = normalizePortableDelegation(candidate.delegation);
|
|
15849
|
+
const bootstrap = await bootstrapDelegatedSession(ctx, delegation);
|
|
15760
15850
|
try {
|
|
15761
|
-
await bootstrapDelegatedSession(ctx, delegation);
|
|
15762
15851
|
await importRequestBoundDelegation(ctx, artifact);
|
|
15763
15852
|
} catch (error) {
|
|
15764
|
-
await
|
|
15765
|
-
await ProfileManager.clearSession(ctx.profile);
|
|
15766
|
-
await ProfileManager.setProfile(ctx.profile, profile);
|
|
15767
|
-
});
|
|
15768
|
-
throw error;
|
|
15853
|
+
await bootstrap.abandon(error);
|
|
15769
15854
|
}
|
|
15770
15855
|
}
|
|
15771
15856
|
async function importRequestBoundDelegation(ctx, artifact) {
|
|
@@ -16079,7 +16164,6 @@ async function rotateAuthKey(profileName, host, options = {}) {
|
|
|
16079
16164
|
ExitCode.AUTH_REQUIRED
|
|
16080
16165
|
);
|
|
16081
16166
|
}
|
|
16082
|
-
await ProfileManager.clearSession(profileName);
|
|
16083
16167
|
const result2 = await handleLocalAuth(profileName, host, {
|
|
16084
16168
|
emitOutput: false,
|
|
16085
16169
|
forceSessionKey: true
|
|
@@ -17643,7 +17727,7 @@ init_errors();
|
|
|
17643
17727
|
init_constants();
|
|
17644
17728
|
init_storage();
|
|
17645
17729
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
17646
|
-
import { lstat, open as open2, readFile as readFile8, rename as rename2, rm as rm3 } from "fs/promises";
|
|
17730
|
+
import { lstat as lstat2, open as open2, readFile as readFile8, rename as rename2, rm as rm3 } from "fs/promises";
|
|
17647
17731
|
import { basename as basename2, dirname as dirname3, join as join5 } from "path";
|
|
17648
17732
|
import { homedir } from "os";
|
|
17649
17733
|
import { invokeOperation as invokeOperation2 } from "@tinycloud/operations";
|
|
@@ -17817,7 +17901,7 @@ function scopedSecretLoginHint(profileName) {
|
|
|
17817
17901
|
}
|
|
17818
17902
|
async function validateSecretOutput(path) {
|
|
17819
17903
|
try {
|
|
17820
|
-
const destination = await
|
|
17904
|
+
const destination = await lstat2(path);
|
|
17821
17905
|
if (!destination.isFile()) {
|
|
17822
17906
|
throw new CLIError("INVALID_ARGUMENT", `Secret output "${path}" must be a regular file, not a symlink, directory, or device.`, ExitCode.USAGE_ERROR);
|
|
17823
17907
|
}
|
|
@@ -17825,7 +17909,7 @@ async function validateSecretOutput(path) {
|
|
|
17825
17909
|
if (!isMissingFileError(error)) throw error;
|
|
17826
17910
|
}
|
|
17827
17911
|
try {
|
|
17828
|
-
const parent = await
|
|
17912
|
+
const parent = await lstat2(dirname3(path));
|
|
17829
17913
|
if (!parent.isDirectory()) {
|
|
17830
17914
|
throw new CLIError("INVALID_ARGUMENT", `Secret output "${path}" requires an existing directory parent.`, ExitCode.USAGE_ERROR);
|
|
17831
17915
|
}
|
|
@@ -27477,7 +27561,7 @@ function receiveJson(result, path) {
|
|
|
27477
27561
|
|
|
27478
27562
|
// src/share/io.ts
|
|
27479
27563
|
import { constants } from "fs";
|
|
27480
|
-
import { lstat as
|
|
27564
|
+
import { lstat as lstat3, mkdir as mkdir3, mkdtemp, open as open4, readFile as readFile9, realpath, stat as stat2, link, rename as rename3, rm as rm4, unlink } from "fs/promises";
|
|
27481
27565
|
import { randomBytes as randomBytes4 } from "crypto";
|
|
27482
27566
|
import { basename as basename3, join as join6, resolve as resolve2, sep } from "path";
|
|
27483
27567
|
init_errors();
|
|
@@ -27535,7 +27619,7 @@ async function assertDirectory(path) {
|
|
|
27535
27619
|
for (const segment of segments) {
|
|
27536
27620
|
current = current === sep ? join6(current, segment) : join6(current, segment);
|
|
27537
27621
|
try {
|
|
27538
|
-
const info = await
|
|
27622
|
+
const info = await lstat3(current);
|
|
27539
27623
|
if (info.isSymbolicLink()) {
|
|
27540
27624
|
const canonical = await realpath(current);
|
|
27541
27625
|
if (current !== "/tmp" && current !== "/var") throw new Error("OUTPUT_EXISTS");
|
|
@@ -27544,7 +27628,7 @@ async function assertDirectory(path) {
|
|
|
27544
27628
|
} catch (error) {
|
|
27545
27629
|
if (error.code !== "ENOENT") throw error;
|
|
27546
27630
|
await mkdir3(current, { mode: 448 });
|
|
27547
|
-
const created = await
|
|
27631
|
+
const created = await lstat3(current);
|
|
27548
27632
|
if (created.isSymbolicLink() || !created.isDirectory()) throw new Error("OUTPUT_EXISTS");
|
|
27549
27633
|
}
|
|
27550
27634
|
}
|
|
@@ -27563,7 +27647,7 @@ async function writeShareOutput(directory, filename, bytes, force) {
|
|
|
27563
27647
|
};
|
|
27564
27648
|
await assertStableDirectory();
|
|
27565
27649
|
const stagingDirectory = await mkdtemp(join6(stableDirectory, ".tinycloud-share-stage-"));
|
|
27566
|
-
const stagingInfo = await
|
|
27650
|
+
const stagingInfo = await lstat3(stagingDirectory);
|
|
27567
27651
|
if (!stagingInfo.isDirectory() || (stagingInfo.mode & 511) !== 448) throw new Error("OUTPUT_EXISTS");
|
|
27568
27652
|
const stagingPath = join6(stagingDirectory, `.tinycloud-share-${randomBytes4(16).toString("hex")}.tmp`);
|
|
27569
27653
|
let temporaryPath;
|
|
@@ -27571,7 +27655,7 @@ async function writeShareOutput(directory, filename, bytes, force) {
|
|
|
27571
27655
|
try {
|
|
27572
27656
|
await assertStableDirectory();
|
|
27573
27657
|
try {
|
|
27574
|
-
const existing = await
|
|
27658
|
+
const existing = await lstat3(outputPath);
|
|
27575
27659
|
if (existing.isSymbolicLink()) throw new Error("UNSAFE_FILENAME");
|
|
27576
27660
|
} catch (error) {
|
|
27577
27661
|
if (error.code !== "ENOENT") throw error;
|