@tinycloud/cli 1.1.0-beta.6 → 1.1.0-beta.8
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 +96 -20
- package/dist/index.js.map +1 -1
- package/dist/legacy-entry.js +146 -47
- package/dist/legacy-entry.js.map +1 -1
- package/package.json +3 -3
- package/skills/tc-cli/AUTH.md +14 -1
- package/skills/tc-cli/REFERENCE.md +20 -0
package/dist/index.js
CHANGED
|
@@ -110,7 +110,7 @@ var init_formatter = __esm({
|
|
|
110
110
|
// src/output/errors.ts
|
|
111
111
|
import { readFileSync, readdirSync } from "fs";
|
|
112
112
|
import { join } from "path";
|
|
113
|
-
import { ProfileLockTimeoutError } from "@tinycloud/operations/state";
|
|
113
|
+
import { ProfileDeletedError, ProfileLockTimeoutError } from "@tinycloud/operations/state";
|
|
114
114
|
function setActiveProfileName(name) {
|
|
115
115
|
activeProfileName = name;
|
|
116
116
|
}
|
|
@@ -136,6 +136,9 @@ function wrapError(error) {
|
|
|
136
136
|
{ hint: "Wait for the other command to finish and retry. A crashed process's lock is reclaimed automatically after 30 s." }
|
|
137
137
|
);
|
|
138
138
|
}
|
|
139
|
+
if (error instanceof ProfileDeletedError) {
|
|
140
|
+
return new CLIError("PROFILE_NOT_FOUND", message);
|
|
141
|
+
}
|
|
139
142
|
if (message.includes("Not signed in") || message.includes("AUTH_EXPIRED") || message.includes("Session expired")) {
|
|
140
143
|
return new CLIError("AUTH_REQUIRED", message, ExitCode.AUTH_REQUIRED);
|
|
141
144
|
}
|
|
@@ -541,6 +544,8 @@ import { join as join3 } from "path";
|
|
|
541
544
|
import {
|
|
542
545
|
profilePath,
|
|
543
546
|
readSession,
|
|
547
|
+
recordProfileDeletion,
|
|
548
|
+
refuseWriteToDeletedProfile,
|
|
544
549
|
removeSession,
|
|
545
550
|
withProfileLock,
|
|
546
551
|
writeSession
|
|
@@ -655,10 +660,12 @@ var init_profiles = __esm({
|
|
|
655
660
|
* removed while holding the profile lock, so another writer's critical
|
|
656
661
|
* section never sees them vanish midway: session and key first, settings
|
|
657
662
|
* last, so a crash midway never leaves a session or key without its
|
|
658
|
-
* profile.
|
|
659
|
-
*
|
|
660
|
-
*
|
|
661
|
-
*
|
|
663
|
+
* profile. The deletion is then recorded, so a store write that waited for
|
|
664
|
+
* the lock meanwhile refuses rather than recreating a profile with only a
|
|
665
|
+
* session in it. `.lock` itself is left to the lock's release; the
|
|
666
|
+
* then-empty directory is removed afterwards unless another writer took
|
|
667
|
+
* the lock (or wrote) meanwhile. A profile directory that is a symlink is
|
|
668
|
+
* unlinked, its target left alone.
|
|
662
669
|
* Throws if the name is not one path segment or names the default profile.
|
|
663
670
|
*/
|
|
664
671
|
static async deleteProfile(name) {
|
|
@@ -679,11 +686,12 @@ var init_profiles = __esm({
|
|
|
679
686
|
`Cannot delete the default profile "${name}". Change the default first with \`tc profile default <other>\`.`
|
|
680
687
|
);
|
|
681
688
|
}
|
|
682
|
-
const
|
|
683
|
-
if (error.code === "ENOENT") return
|
|
689
|
+
const kind = await lstat(profileDir).then((stats) => stats.isSymbolicLink() ? "link" : "present", (error) => {
|
|
690
|
+
if (error.code === "ENOENT") return "missing";
|
|
684
691
|
throw error;
|
|
685
692
|
});
|
|
686
|
-
if (
|
|
693
|
+
if (kind === "missing") return;
|
|
694
|
+
if (kind === "link") {
|
|
687
695
|
await rm2(profileDir, { force: true });
|
|
688
696
|
return;
|
|
689
697
|
}
|
|
@@ -693,6 +701,7 @@ var init_profiles = __esm({
|
|
|
693
701
|
if (entry !== ".lock" && entry !== "profile.json") await rm2(join3(profileDir, entry), { recursive: true, force: true });
|
|
694
702
|
}
|
|
695
703
|
await rm2(join3(profileDir, "profile.json"), { force: true });
|
|
704
|
+
await recordProfileDeletion(name);
|
|
696
705
|
});
|
|
697
706
|
await rmdir(profileDir).catch((error) => {
|
|
698
707
|
if (error.code !== "ENOENT" && error.code !== "ENOTEMPTY" && error.code !== "EEXIST") throw error;
|
|
@@ -705,9 +714,14 @@ var init_profiles = __esm({
|
|
|
705
714
|
static async getKey(name) {
|
|
706
715
|
return readJson(join3(PROFILES_DIR, name, "key.json"));
|
|
707
716
|
}
|
|
708
|
-
/**
|
|
717
|
+
/**
|
|
718
|
+
* Saves a JWK key under the profile lock (0600, in an owner-only profile
|
|
719
|
+
* directory). Refused (PROFILE_NOT_FOUND) if the profile was deleted while
|
|
720
|
+
* this waited for the lock, rather than leaving a key-only profile.
|
|
721
|
+
*/
|
|
709
722
|
static async setKey(name, jwk) {
|
|
710
723
|
await _ProfileManager.withLock(name, async () => {
|
|
724
|
+
await refuseWriteToDeletedProfile(name);
|
|
711
725
|
await writeJson(join3(await _ProfileManager.ensureProfileDir(name), "key.json"), jwk);
|
|
712
726
|
});
|
|
713
727
|
}
|
|
@@ -5659,20 +5673,56 @@ import {
|
|
|
5659
5673
|
profileStoreMetadataPath,
|
|
5660
5674
|
readAdditionalDelegations,
|
|
5661
5675
|
readAuthRequests,
|
|
5662
|
-
|
|
5676
|
+
refuseWriteToDeletedProfile as refuseWriteToDeletedProfile2,
|
|
5677
|
+
updateProfileStore,
|
|
5663
5678
|
withProfileLock as withProfileLock2,
|
|
5664
5679
|
writeJsonAtomic
|
|
5665
5680
|
} from "@tinycloud/operations/state";
|
|
5666
5681
|
async function loadAdditionalDelegations(profile) {
|
|
5667
5682
|
return readAdditionalDelegations(profile);
|
|
5668
5683
|
}
|
|
5669
|
-
async function replayAdditionalDelegations(node, profile) {
|
|
5684
|
+
async function replayAdditionalDelegations(node, profile, options) {
|
|
5685
|
+
const {
|
|
5686
|
+
operationSpaceResolver,
|
|
5687
|
+
prepareStoredDelegationReplay,
|
|
5688
|
+
replayStoredDelegation,
|
|
5689
|
+
storedDelegationKind
|
|
5690
|
+
} = await import("@tinycloud/operations/delegation-binding");
|
|
5691
|
+
const activator = node;
|
|
5692
|
+
const migrated = await prepareStoredDelegationReplay(profile, activator, {
|
|
5693
|
+
host: options.host,
|
|
5694
|
+
migrate: options.migrate
|
|
5695
|
+
});
|
|
5696
|
+
const resolveSpace = operationSpaceResolver(node, options.ownerSpace);
|
|
5670
5697
|
const entries = await loadAdditionalDelegations(profile);
|
|
5671
|
-
for (const
|
|
5698
|
+
for (const stored of entries) {
|
|
5699
|
+
const kind = storedDelegationKind(stored);
|
|
5700
|
+
if (kind === "compact" || kind === "signed-login") {
|
|
5701
|
+
const installed = await replayStoredDelegation(activator, stored, {
|
|
5702
|
+
host: options.host,
|
|
5703
|
+
migrated,
|
|
5704
|
+
resolveSpace
|
|
5705
|
+
});
|
|
5706
|
+
if (installed === void 0 && process.env.TC_DEBUG_REPLAY === "1") {
|
|
5707
|
+
process.stderr.write("[replay] skipping a stored delegation refused by validation or its request binding\n");
|
|
5708
|
+
}
|
|
5709
|
+
continue;
|
|
5710
|
+
}
|
|
5711
|
+
if (kind === "refused") {
|
|
5712
|
+
if (process.env.TC_DEBUG_REPLAY === "1") {
|
|
5713
|
+
process.stderr.write("[replay] skipping a malformed stored record or one bound to a request it cannot be held to\n");
|
|
5714
|
+
}
|
|
5715
|
+
continue;
|
|
5716
|
+
}
|
|
5717
|
+
const entry = stored;
|
|
5672
5718
|
const expiry = entry.delegation.expiry instanceof Date ? entry.delegation.expiry : new Date(entry.delegation.expiry);
|
|
5673
5719
|
if (expiry.getTime() <= Date.now()) continue;
|
|
5674
5720
|
try {
|
|
5675
|
-
await node.useRuntimeDelegation({
|
|
5721
|
+
await node.useRuntimeDelegation({
|
|
5722
|
+
...entry.delegation,
|
|
5723
|
+
delegationHeader: { Authorization: entry.delegation.delegationHeader.Authorization },
|
|
5724
|
+
expiry
|
|
5725
|
+
});
|
|
5676
5726
|
} catch (err) {
|
|
5677
5727
|
if (process.env.TC_DEBUG_REPLAY === "1") {
|
|
5678
5728
|
process.stderr.write(`[replay] skipping ${entry.delegation.cid}: ${err.message}
|
|
@@ -5749,6 +5799,7 @@ async function createSDKInstance(ctx, options) {
|
|
|
5749
5799
|
host: ctx.host,
|
|
5750
5800
|
privateKey: effectivePrivateKey
|
|
5751
5801
|
});
|
|
5802
|
+
let restoredOwnSession2 = false;
|
|
5752
5803
|
if (session && session.delegationHeader && session.delegationCid && session.spaceId) {
|
|
5753
5804
|
await node2.restoreSession({
|
|
5754
5805
|
delegationHeader: session.delegationHeader,
|
|
@@ -5761,16 +5812,22 @@ async function createSDKInstance(ctx, options) {
|
|
|
5761
5812
|
siwe: session.siwe,
|
|
5762
5813
|
signature: session.signature
|
|
5763
5814
|
});
|
|
5815
|
+
restoredOwnSession2 = true;
|
|
5764
5816
|
} else {
|
|
5765
5817
|
await node2.signIn();
|
|
5766
5818
|
}
|
|
5767
|
-
await replayAdditionalDelegations(node2, ctx.profile
|
|
5819
|
+
await replayAdditionalDelegations(node2, ctx.profile, {
|
|
5820
|
+
host: ctx.host,
|
|
5821
|
+
ownerSpace: profile.spaceId,
|
|
5822
|
+
migrate: restoredOwnSession2 && options?.privateKey === void 0
|
|
5823
|
+
});
|
|
5768
5824
|
return node2;
|
|
5769
5825
|
}
|
|
5770
5826
|
const node = new TinyCloudNode({
|
|
5771
5827
|
host: ctx.host,
|
|
5772
5828
|
privateKey: options?.privateKey
|
|
5773
5829
|
});
|
|
5830
|
+
let restoredOwnSession = false;
|
|
5774
5831
|
if (options?.privateKey) {
|
|
5775
5832
|
await node.signIn();
|
|
5776
5833
|
} else if (session && session.delegationHeader && session.delegationCid && session.spaceId) {
|
|
@@ -5785,8 +5842,13 @@ async function createSDKInstance(ctx, options) {
|
|
|
5785
5842
|
siwe: session.siwe,
|
|
5786
5843
|
signature: session.signature
|
|
5787
5844
|
});
|
|
5845
|
+
restoredOwnSession = true;
|
|
5788
5846
|
}
|
|
5789
|
-
await replayAdditionalDelegations(node, ctx.profile
|
|
5847
|
+
await replayAdditionalDelegations(node, ctx.profile, {
|
|
5848
|
+
host: ctx.host,
|
|
5849
|
+
ownerSpace: profile?.spaceId,
|
|
5850
|
+
migrate: restoredOwnSession
|
|
5851
|
+
});
|
|
5790
5852
|
return node;
|
|
5791
5853
|
}
|
|
5792
5854
|
async function bootstrapDelegatedSession(ctx, delegation) {
|
|
@@ -5838,13 +5900,13 @@ async function bootstrapDelegatedSession(ctx, delegation) {
|
|
|
5838
5900
|
throw error;
|
|
5839
5901
|
});
|
|
5840
5902
|
const session = await ProfileManager.getSession(ctx.profile);
|
|
5841
|
-
if (
|
|
5903
|
+
if (profile === null || BOOTSTRAP_PROFILE_FIELDS.some((field) => profile[field] !== written.profile[field]) || JSON.stringify(session) !== JSON.stringify(written.session)) {
|
|
5842
5904
|
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
5905
|
}
|
|
5844
5906
|
return restoreBeforeBootstrap(ctx.profile, written.previousProfile);
|
|
5845
5907
|
}, { timeoutMs: PROFILE_COMMIT_LOCK_TIMEOUT_MS }).catch((error) => (
|
|
5846
5908
|
// 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 (${
|
|
5909
|
+
`Rolling back the provisional session of profile "${ctx.profile}" could not run (${failureName(error)}); check \`tc --profile ${ctx.profile} context\`.`
|
|
5848
5910
|
));
|
|
5849
5911
|
throw annotate(cause, note);
|
|
5850
5912
|
};
|
|
@@ -5860,15 +5922,27 @@ async function restoreBeforeBootstrap(profileName, previousProfile) {
|
|
|
5860
5922
|
const failures = [];
|
|
5861
5923
|
for (const write of [
|
|
5862
5924
|
() => ProfileManager.clearSession(profileName),
|
|
5863
|
-
() => ProfileManager.
|
|
5925
|
+
() => ProfileManager.updateProfile(profileName, (current) => {
|
|
5926
|
+
const restored = { ...current };
|
|
5927
|
+
for (const field of BOOTSTRAP_PROFILE_FIELDS) {
|
|
5928
|
+
if (previousProfile[field] === void 0) delete restored[field];
|
|
5929
|
+
else restored[field] = previousProfile[field];
|
|
5930
|
+
}
|
|
5931
|
+
return restored;
|
|
5932
|
+
})
|
|
5864
5933
|
]) {
|
|
5865
5934
|
await write().catch((error) => {
|
|
5866
|
-
failures.push(
|
|
5935
|
+
failures.push(failureName(error));
|
|
5867
5936
|
});
|
|
5868
5937
|
}
|
|
5869
5938
|
if (failures.length === 0) return void 0;
|
|
5870
5939
|
return `Rolling back the provisional session of profile "${profileName}" failed too (${failures.join("; ")}); check \`tc --profile ${profileName} context\`.`;
|
|
5871
5940
|
}
|
|
5941
|
+
function failureName(error) {
|
|
5942
|
+
if (error instanceof SyntaxError) return "a profile file is not valid JSON";
|
|
5943
|
+
if (typeof error === "object" && error !== null && "code" in error && typeof error.code === "string") return error.code;
|
|
5944
|
+
return error instanceof Error ? error.name : "unknown error";
|
|
5945
|
+
}
|
|
5872
5946
|
function annotate(error, note) {
|
|
5873
5947
|
if (note === void 0) return error;
|
|
5874
5948
|
const cause = wrapError(error);
|
|
@@ -5880,7 +5954,7 @@ async function ensureAuthenticated(ctx, options) {
|
|
|
5880
5954
|
}
|
|
5881
5955
|
const profile = await ProfileManager.getProfile(ctx.profile).catch(() => null);
|
|
5882
5956
|
if (profile?.authMethod === "local" && profile.privateKey) {
|
|
5883
|
-
return createSDKInstance(ctx
|
|
5957
|
+
return createSDKInstance(ctx);
|
|
5884
5958
|
}
|
|
5885
5959
|
const session = await ProfileManager.getSession(ctx.profile);
|
|
5886
5960
|
if (!session) {
|
|
@@ -5892,6 +5966,7 @@ async function ensureAuthenticated(ctx, options) {
|
|
|
5892
5966
|
}
|
|
5893
5967
|
return createSDKInstance(ctx, options);
|
|
5894
5968
|
}
|
|
5969
|
+
var BOOTSTRAP_PROFILE_FIELDS;
|
|
5895
5970
|
var init_sdk = __esm({
|
|
5896
5971
|
"src/lib/sdk.ts"() {
|
|
5897
5972
|
"use strict";
|
|
@@ -5900,6 +5975,7 @@ var init_sdk = __esm({
|
|
|
5900
5975
|
init_errors();
|
|
5901
5976
|
init_constants();
|
|
5902
5977
|
init_permissions();
|
|
5978
|
+
BOOTSTRAP_PROFILE_FIELDS = ["sessionDid", "spaceId"];
|
|
5903
5979
|
}
|
|
5904
5980
|
});
|
|
5905
5981
|
|