@tinycloud/cli 0.6.0-beta.1 → 0.6.0-beta.11
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/README.md +1 -1
- package/dist/index.js +734 -79
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -549,7 +549,7 @@ var ProfileManager = class _ProfileManager {
|
|
|
549
549
|
|
|
550
550
|
// src/auth/local-key.ts
|
|
551
551
|
import { TCWSessionManager, importKey, initPanicHook } from "@tinycloud/node-sdk-wasm";
|
|
552
|
-
import { PrivateKeySigner } from "@tinycloud/node-sdk";
|
|
552
|
+
import { PrivateKeySigner, pkhDid } from "@tinycloud/node-sdk";
|
|
553
553
|
import { randomBytes } from "crypto";
|
|
554
554
|
var wasmInitialized = false;
|
|
555
555
|
function ensureWasm() {
|
|
@@ -583,7 +583,7 @@ async function deriveAddress(privateKey) {
|
|
|
583
583
|
return signer.getAddress();
|
|
584
584
|
}
|
|
585
585
|
function addressToDID(address, chainId = 1) {
|
|
586
|
-
return
|
|
586
|
+
return pkhDid(address, chainId);
|
|
587
587
|
}
|
|
588
588
|
async function generateLocalIdentity(chainId = 1) {
|
|
589
589
|
const privateKey = generateEthereumPrivateKey();
|
|
@@ -620,6 +620,25 @@ async function localKeySignIn(options) {
|
|
|
620
620
|
// src/auth/browser-auth.ts
|
|
621
621
|
import { createServer } from "http";
|
|
622
622
|
import { createInterface } from "readline";
|
|
623
|
+
var PRIVATE_JWK_FIELDS = /* @__PURE__ */ new Set([
|
|
624
|
+
"d",
|
|
625
|
+
"p",
|
|
626
|
+
"q",
|
|
627
|
+
"dp",
|
|
628
|
+
"dq",
|
|
629
|
+
"qi",
|
|
630
|
+
"oth",
|
|
631
|
+
"k"
|
|
632
|
+
]);
|
|
633
|
+
function publicJwkForDelegation(jwk) {
|
|
634
|
+
const publicJwk = {};
|
|
635
|
+
for (const [key, value] of Object.entries(jwk)) {
|
|
636
|
+
if (!PRIVATE_JWK_FIELDS.has(key)) {
|
|
637
|
+
publicJwk[key] = value;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return publicJwk;
|
|
641
|
+
}
|
|
623
642
|
async function startAuthFlow(did, options = {}) {
|
|
624
643
|
if (options.paste) {
|
|
625
644
|
return pasteFlow(did, options);
|
|
@@ -641,7 +660,9 @@ function buildAuthUrl(did, options = {}) {
|
|
|
641
660
|
params.set("callback", options.callback);
|
|
642
661
|
}
|
|
643
662
|
if (options.jwk) {
|
|
644
|
-
const jwkB64 = Buffer.from(
|
|
663
|
+
const jwkB64 = Buffer.from(
|
|
664
|
+
JSON.stringify(publicJwkForDelegation(options.jwk))
|
|
665
|
+
).toString("base64url");
|
|
645
666
|
params.set("jwk", jwkB64);
|
|
646
667
|
}
|
|
647
668
|
if (options.host) {
|
|
@@ -843,7 +864,7 @@ function registerInitCommand(program2) {
|
|
|
843
864
|
await ProfileManager.setProfile(profileName, {
|
|
844
865
|
...profileConfig,
|
|
845
866
|
spaceId: delegationData.spaceId,
|
|
846
|
-
|
|
867
|
+
ownerDid: delegationData.ownerDid
|
|
847
868
|
});
|
|
848
869
|
outputJson({
|
|
849
870
|
profile: profileName,
|
|
@@ -903,13 +924,22 @@ import {
|
|
|
903
924
|
} from "@tinycloud/node-sdk";
|
|
904
925
|
|
|
905
926
|
// src/lib/space.ts
|
|
927
|
+
import {
|
|
928
|
+
buildSpaceUri,
|
|
929
|
+
canonicalizeAddress,
|
|
930
|
+
makePkhSpaceId,
|
|
931
|
+
parsePkhDid,
|
|
932
|
+
parseSpaceUri
|
|
933
|
+
} from "@tinycloud/node-sdk";
|
|
906
934
|
function resolveAddress(profile, session) {
|
|
907
935
|
const sessAddr = session?.address;
|
|
908
|
-
if (typeof sessAddr === "string" && sessAddr.length > 0)
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
936
|
+
if (typeof sessAddr === "string" && sessAddr.length > 0) {
|
|
937
|
+
return canonicalizeAddress(sessAddr);
|
|
938
|
+
}
|
|
939
|
+
if (profile.address) return canonicalizeAddress(profile.address);
|
|
940
|
+
if (profile.ownerDid) {
|
|
941
|
+
const pkh = parsePkhDid(profile.ownerDid);
|
|
942
|
+
if (pkh) return pkh.address;
|
|
913
943
|
}
|
|
914
944
|
throw new CLIError(
|
|
915
945
|
"ADDRESS_UNKNOWN",
|
|
@@ -924,7 +954,17 @@ function resolveChainId(profile, session) {
|
|
|
924
954
|
}
|
|
925
955
|
async function resolveSpaceUri(input, profileName) {
|
|
926
956
|
if (!input) return void 0;
|
|
927
|
-
if (input.startsWith("tinycloud:"))
|
|
957
|
+
if (input.startsWith("tinycloud:")) {
|
|
958
|
+
const parsed = parseSpaceUri(input);
|
|
959
|
+
if (!parsed) {
|
|
960
|
+
throw new CLIError(
|
|
961
|
+
"INVALID_SPACE",
|
|
962
|
+
`Invalid --space "${input}". Use a short name ([A-Za-z0-9_-]) or a full tinycloud:... URI.`,
|
|
963
|
+
ExitCode.USAGE_ERROR
|
|
964
|
+
);
|
|
965
|
+
}
|
|
966
|
+
return buildSpaceUri(parsed.owner, parsed.name);
|
|
967
|
+
}
|
|
928
968
|
if (!/^[A-Za-z0-9_-]+$/.test(input)) {
|
|
929
969
|
throw new CLIError(
|
|
930
970
|
"INVALID_SPACE",
|
|
@@ -936,7 +976,7 @@ async function resolveSpaceUri(input, profileName) {
|
|
|
936
976
|
const session = await ProfileManager.getSession(profileName);
|
|
937
977
|
const address = resolveAddress(profile, session);
|
|
938
978
|
const chainId = resolveChainId(profile, session);
|
|
939
|
-
return
|
|
979
|
+
return makePkhSpaceId(address, chainId, input);
|
|
940
980
|
}
|
|
941
981
|
|
|
942
982
|
// src/lib/permissions.ts
|
|
@@ -959,8 +999,8 @@ function createPermissionRequestArtifact(params) {
|
|
|
959
999
|
posture: resolveProfilePosture(params.profile),
|
|
960
1000
|
operatorType: resolveProfileOperatorType(params.profile),
|
|
961
1001
|
host: params.host,
|
|
962
|
-
|
|
963
|
-
|
|
1002
|
+
sessionDid: didWithoutFragment(params.profile.sessionDid ?? params.profile.did),
|
|
1003
|
+
ownerDid: params.profile.ownerDid,
|
|
964
1004
|
spaceId: params.profile.spaceId,
|
|
965
1005
|
requestedExpiry: params.requestedExpiry,
|
|
966
1006
|
requested: params.requested,
|
|
@@ -1362,6 +1402,15 @@ function registerAuthCommand(program2) {
|
|
|
1362
1402
|
handleError(error);
|
|
1363
1403
|
}
|
|
1364
1404
|
});
|
|
1405
|
+
auth.command("rotate").description("Rotate the active profile session key").option("--paste", "Use manual paste mode instead of browser callback").action(async (options, cmd) => {
|
|
1406
|
+
try {
|
|
1407
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1408
|
+
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
1409
|
+
await rotateAuthKey(ctx.profile, ctx.host, { paste: options.paste });
|
|
1410
|
+
} catch (error) {
|
|
1411
|
+
handleError(error);
|
|
1412
|
+
}
|
|
1413
|
+
});
|
|
1365
1414
|
auth.command("status").description("Show current authentication state").action(async (_options, cmd) => {
|
|
1366
1415
|
try {
|
|
1367
1416
|
const globalOpts = cmd.optsWithGlobals();
|
|
@@ -1382,7 +1431,7 @@ function registerAuthCommand(program2) {
|
|
|
1382
1431
|
authenticated,
|
|
1383
1432
|
did: profile?.did ?? null,
|
|
1384
1433
|
sessionDid: profile?.sessionDid ?? null,
|
|
1385
|
-
|
|
1434
|
+
ownerDid: profile?.ownerDid ?? null,
|
|
1386
1435
|
spaceId: profile?.spaceId ?? null,
|
|
1387
1436
|
host: ctx.host,
|
|
1388
1437
|
profile: ctx.profile,
|
|
@@ -1402,7 +1451,7 @@ function registerAuthCommand(program2) {
|
|
|
1402
1451
|
process.stdout.write(formatField("Host", ctx.host) + "\n");
|
|
1403
1452
|
process.stdout.write(formatField("DID", profile?.did ?? null) + "\n");
|
|
1404
1453
|
process.stdout.write(formatField("Session DID", profile?.sessionDid ?? null) + "\n");
|
|
1405
|
-
process.stdout.write(formatField("
|
|
1454
|
+
process.stdout.write(formatField("Owner DID", profile?.ownerDid ?? null) + "\n");
|
|
1406
1455
|
process.stdout.write(formatField("Address", profile?.address ?? null) + "\n");
|
|
1407
1456
|
process.stdout.write(formatField("Space ID", profile?.spaceId ?? null) + "\n");
|
|
1408
1457
|
process.stdout.write(formatField("Has Key", hasKey !== null) + "\n");
|
|
@@ -1603,7 +1652,7 @@ function registerAuthCommand(program2) {
|
|
|
1603
1652
|
yes: options.yes === true
|
|
1604
1653
|
});
|
|
1605
1654
|
const result = await node.delegateTo(
|
|
1606
|
-
parsed.
|
|
1655
|
+
parsed.sessionDid,
|
|
1607
1656
|
parsed.requested,
|
|
1608
1657
|
parsed.requestedExpiry !== void 0 ? { expiry: parsed.requestedExpiry } : void 0
|
|
1609
1658
|
);
|
|
@@ -1735,7 +1784,7 @@ function registerAuthCommand(program2) {
|
|
|
1735
1784
|
profile: ctx.profile,
|
|
1736
1785
|
did: profile.did,
|
|
1737
1786
|
sessionDid: profile.sessionDid ?? null,
|
|
1738
|
-
|
|
1787
|
+
ownerDid: profile.ownerDid ?? null,
|
|
1739
1788
|
spaceId: profile.spaceId ?? null,
|
|
1740
1789
|
host: profile.host,
|
|
1741
1790
|
authenticated,
|
|
@@ -1749,7 +1798,7 @@ function registerAuthCommand(program2) {
|
|
|
1749
1798
|
process.stdout.write(formatField("Profile", ctx.profile) + "\n");
|
|
1750
1799
|
process.stdout.write(formatField("DID", profile.did) + "\n");
|
|
1751
1800
|
process.stdout.write(formatField("Session DID", profile.sessionDid ?? null) + "\n");
|
|
1752
|
-
process.stdout.write(formatField("
|
|
1801
|
+
process.stdout.write(formatField("Owner DID", profile.ownerDid ?? null) + "\n");
|
|
1753
1802
|
process.stdout.write(formatField("Auth Method", profile.authMethod ?? null) + "\n");
|
|
1754
1803
|
process.stdout.write(formatField("Posture", posture) + "\n");
|
|
1755
1804
|
process.stdout.write(formatField("Operator", operatorType) + "\n");
|
|
@@ -1880,7 +1929,7 @@ function normalizePortableDelegation(delegation) {
|
|
|
1880
1929
|
return { ...delegation, expiry };
|
|
1881
1930
|
}
|
|
1882
1931
|
async function ensureDelegationAuthority(params) {
|
|
1883
|
-
if (params.node.hasRuntimePermissions(params.requested)) return;
|
|
1932
|
+
if (!params.force && params.node.hasRuntimePermissions(params.requested)) return;
|
|
1884
1933
|
if (params.profile.authMethod === "openkey") {
|
|
1885
1934
|
const key = await ProfileManager.getKey(params.ctx.profile);
|
|
1886
1935
|
if (!key) {
|
|
@@ -2026,18 +2075,40 @@ function parseExpiryOption(raw) {
|
|
|
2026
2075
|
}
|
|
2027
2076
|
function groupPermissionsBySpace(permissions) {
|
|
2028
2077
|
const groups = /* @__PURE__ */ new Map();
|
|
2078
|
+
const rawEntries = [];
|
|
2029
2079
|
for (const permission of permissions) {
|
|
2080
|
+
if (isRawPermission(permission)) {
|
|
2081
|
+
rawEntries.push(permission);
|
|
2082
|
+
continue;
|
|
2083
|
+
}
|
|
2030
2084
|
const group = groups.get(permission.space) ?? [];
|
|
2031
2085
|
group.push(permission);
|
|
2032
2086
|
groups.set(permission.space, group);
|
|
2033
2087
|
}
|
|
2034
|
-
|
|
2088
|
+
const grouped = Array.from(groups.values());
|
|
2089
|
+
if (grouped.length === 0) {
|
|
2090
|
+
return rawEntries.length > 0 ? [rawEntries] : [];
|
|
2091
|
+
}
|
|
2092
|
+
grouped[0].push(...rawEntries);
|
|
2093
|
+
return grouped;
|
|
2094
|
+
}
|
|
2095
|
+
function isRawPermission(permission) {
|
|
2096
|
+
return permission.service === "tinycloud.encryption" && permission.path.startsWith("urn:tinycloud:encryption:");
|
|
2097
|
+
}
|
|
2098
|
+
function returnedSpaceMatchesExpected(returnedSpace, expectedSpace) {
|
|
2099
|
+
if (returnedSpace === expectedSpace) return true;
|
|
2100
|
+
if (!returnedSpace.startsWith("tinycloud:")) return false;
|
|
2101
|
+
const returnedName = returnedSpace.slice(returnedSpace.lastIndexOf(":") + 1);
|
|
2102
|
+
return returnedName === expectedSpace;
|
|
2035
2103
|
}
|
|
2036
2104
|
function portableFromOpenKeyDelegation(data, permissions, host) {
|
|
2037
|
-
const primary = permissions[0];
|
|
2038
|
-
const returnedSpace = String(data.spaceId ?? primary.space);
|
|
2039
|
-
const expectedSpaces = new Set(
|
|
2040
|
-
|
|
2105
|
+
const primary = permissions.find((permission) => !isRawPermission(permission)) ?? permissions[0];
|
|
2106
|
+
const returnedSpace = String(data.spaceId ?? primary.space ?? "encryption");
|
|
2107
|
+
const expectedSpaces = new Set(
|
|
2108
|
+
permissions.filter((permission) => !isRawPermission(permission)).map((permission) => permission.space)
|
|
2109
|
+
);
|
|
2110
|
+
const matchesExpectedSpace = expectedSpaces.size === 1 && returnedSpaceMatchesExpected(returnedSpace, Array.from(expectedSpaces)[0]);
|
|
2111
|
+
if (expectedSpaces.size > 0 && !matchesExpectedSpace) {
|
|
2041
2112
|
throw new CLIError(
|
|
2042
2113
|
"OPENKEY_SCOPE_MISMATCH",
|
|
2043
2114
|
`OpenKey returned delegation for ${returnedSpace}, expected ${Array.from(expectedSpaces).join(", ")}.`,
|
|
@@ -2072,16 +2143,74 @@ function inferDelegationExpiry(data) {
|
|
|
2072
2143
|
}
|
|
2073
2144
|
return new Date(Date.now() + 60 * 60 * 1e3);
|
|
2074
2145
|
}
|
|
2075
|
-
async function
|
|
2146
|
+
async function rotateAuthKey(profileName, host, options = {}) {
|
|
2147
|
+
const profile = await ProfileManager.getProfile(profileName);
|
|
2148
|
+
const posture = resolveProfilePosture(profile);
|
|
2149
|
+
const oldDid = profile.sessionDid ?? profile.did;
|
|
2150
|
+
if (posture === "delegate-session") {
|
|
2151
|
+
throw new CLIError(
|
|
2152
|
+
"ROTATE_DELEGATE_SESSION_UNSUPPORTED",
|
|
2153
|
+
`Profile "${profileName}" is a delegated session. Request or import a new owner delegation instead of rotating it locally.`,
|
|
2154
|
+
ExitCode.PERMISSION_DENIED
|
|
2155
|
+
);
|
|
2156
|
+
}
|
|
2157
|
+
if (profile.authMethod === "local" || posture === "local-owner-key") {
|
|
2158
|
+
if (!profile.privateKey) {
|
|
2159
|
+
throw new CLIError(
|
|
2160
|
+
"LOCAL_OWNER_KEY_REQUIRED",
|
|
2161
|
+
`Profile "${profileName}" does not have a local owner private key. Run \`tc auth login --method local\` first.`,
|
|
2162
|
+
ExitCode.AUTH_REQUIRED
|
|
2163
|
+
);
|
|
2164
|
+
}
|
|
2165
|
+
await ProfileManager.clearSession(profileName);
|
|
2166
|
+
const result2 = await handleLocalAuth(profileName, host, {
|
|
2167
|
+
emitOutput: false,
|
|
2168
|
+
forceSessionKey: true
|
|
2169
|
+
});
|
|
2170
|
+
outputRotationResult(result2.profile, profileName, oldDid, "local");
|
|
2171
|
+
return;
|
|
2172
|
+
}
|
|
2173
|
+
const { jwk, did } = await withSpinner("Generating session key...", async () => {
|
|
2174
|
+
return generateKey();
|
|
2175
|
+
});
|
|
2176
|
+
await ProfileManager.setKey(profileName, jwk);
|
|
2177
|
+
await ProfileManager.clearSession(profileName);
|
|
2178
|
+
await ProfileManager.setProfile(profileName, {
|
|
2179
|
+
...profile,
|
|
2180
|
+
host,
|
|
2181
|
+
did,
|
|
2182
|
+
sessionDid: did,
|
|
2183
|
+
posture: profile.posture ?? "owner-openkey",
|
|
2184
|
+
operatorType: profile.operatorType ?? "human",
|
|
2185
|
+
authMethod: "openkey"
|
|
2186
|
+
});
|
|
2187
|
+
const result = await refreshOpenKeySession(profileName, host, {
|
|
2188
|
+
paste: options.paste
|
|
2189
|
+
});
|
|
2190
|
+
outputRotationResult(result.profile, profileName, oldDid, "openkey");
|
|
2191
|
+
}
|
|
2192
|
+
function outputRotationResult(profile, profileName, oldDid, authMethod) {
|
|
2193
|
+
outputJson({
|
|
2194
|
+
rotated: true,
|
|
2195
|
+
profile: profileName,
|
|
2196
|
+
oldDid,
|
|
2197
|
+
did: profile.did,
|
|
2198
|
+
sessionDid: profile.sessionDid ?? null,
|
|
2199
|
+
authMethod,
|
|
2200
|
+
spaceId: profile.spaceId ?? null
|
|
2201
|
+
});
|
|
2202
|
+
}
|
|
2203
|
+
async function handleLocalAuth(profileName, host, options = {}) {
|
|
2076
2204
|
const profile = await ProfileManager.getProfile(profileName).catch(() => null);
|
|
2205
|
+
const posture = profile ? resolveProfilePosture(profile) : null;
|
|
2077
2206
|
let privateKey;
|
|
2078
2207
|
let address;
|
|
2079
2208
|
let did;
|
|
2080
2209
|
let sessionDid = profile?.sessionDid;
|
|
2081
|
-
if (profile?.authMethod === "local"
|
|
2210
|
+
if ((profile?.authMethod === "local" || posture === "local-owner-key") && profile.privateKey) {
|
|
2082
2211
|
privateKey = profile.privateKey;
|
|
2083
|
-
address = profile.address;
|
|
2084
|
-
did = profile.did;
|
|
2212
|
+
address = profile.address ?? await deriveAddress(privateKey);
|
|
2213
|
+
did = profile.did.startsWith("did:pkh:") ? profile.did : addressToDID(address, profile.chainId ?? DEFAULT_CHAIN_ID);
|
|
2085
2214
|
if (isInteractive()) {
|
|
2086
2215
|
process.stderr.write(theme.muted("Using existing local key") + "\n");
|
|
2087
2216
|
process.stderr.write(formatField("Address", address) + "\n");
|
|
@@ -2100,7 +2229,7 @@ async function handleLocalAuth(profileName, host) {
|
|
|
2100
2229
|
}
|
|
2101
2230
|
}
|
|
2102
2231
|
const hasKey = await ProfileManager.getKey(profileName);
|
|
2103
|
-
if (!hasKey) {
|
|
2232
|
+
if (options.forceSessionKey || !hasKey) {
|
|
2104
2233
|
const { jwk, did: generatedSessionDid } = await withSpinner("Generating session key...", async () => {
|
|
2105
2234
|
return generateKey();
|
|
2106
2235
|
});
|
|
@@ -2125,7 +2254,7 @@ async function handleLocalAuth(profileName, host) {
|
|
|
2125
2254
|
signature: sessionResult.signature
|
|
2126
2255
|
});
|
|
2127
2256
|
sessionDid = sessionResult.verificationMethod;
|
|
2128
|
-
|
|
2257
|
+
const updatedProfile = {
|
|
2129
2258
|
...profile,
|
|
2130
2259
|
name: profileName,
|
|
2131
2260
|
host,
|
|
@@ -2133,7 +2262,7 @@ async function handleLocalAuth(profileName, host) {
|
|
|
2133
2262
|
spaceName: "default",
|
|
2134
2263
|
did,
|
|
2135
2264
|
sessionDid,
|
|
2136
|
-
|
|
2265
|
+
ownerDid: did,
|
|
2137
2266
|
spaceId: sessionResult.spaceId,
|
|
2138
2267
|
createdAt: profile?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
2139
2268
|
posture: profile?.posture ?? "local-owner-key",
|
|
@@ -2141,18 +2270,32 @@ async function handleLocalAuth(profileName, host) {
|
|
|
2141
2270
|
authMethod: "local",
|
|
2142
2271
|
privateKey,
|
|
2143
2272
|
address
|
|
2144
|
-
}
|
|
2273
|
+
};
|
|
2274
|
+
await ProfileManager.setProfile(profileName, updatedProfile);
|
|
2275
|
+
if (options.emitOutput ?? true) {
|
|
2276
|
+
outputJson({
|
|
2277
|
+
authenticated: true,
|
|
2278
|
+
profile: profileName,
|
|
2279
|
+
did,
|
|
2280
|
+
sessionDid,
|
|
2281
|
+
address,
|
|
2282
|
+
spaceId: sessionResult.spaceId,
|
|
2283
|
+
authMethod: "local"
|
|
2284
|
+
});
|
|
2285
|
+
}
|
|
2286
|
+
return { profile: updatedProfile, sessionResult };
|
|
2287
|
+
}
|
|
2288
|
+
async function handleOpenKeyAuth(profileName, host, paste) {
|
|
2289
|
+
const { profile, delegationData } = await refreshOpenKeySession(profileName, host, { paste });
|
|
2145
2290
|
outputJson({
|
|
2146
2291
|
authenticated: true,
|
|
2147
2292
|
profile: profileName,
|
|
2148
|
-
did,
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
spaceId: sessionResult.spaceId,
|
|
2152
|
-
authMethod: "local"
|
|
2293
|
+
did: profile.did,
|
|
2294
|
+
spaceId: delegationData.spaceId,
|
|
2295
|
+
authMethod: "openkey"
|
|
2153
2296
|
});
|
|
2154
2297
|
}
|
|
2155
|
-
async function
|
|
2298
|
+
async function refreshOpenKeySession(profileName, host, options = {}) {
|
|
2156
2299
|
const key = await ProfileManager.getKey(profileName);
|
|
2157
2300
|
if (!key) {
|
|
2158
2301
|
throw new CLIError(
|
|
@@ -2163,7 +2306,7 @@ async function handleOpenKeyAuth(profileName, host, paste) {
|
|
|
2163
2306
|
}
|
|
2164
2307
|
const profile = await ProfileManager.getProfile(profileName);
|
|
2165
2308
|
const delegationData = await startAuthFlow(profile.did, {
|
|
2166
|
-
paste,
|
|
2309
|
+
paste: options.paste,
|
|
2167
2310
|
jwk: key,
|
|
2168
2311
|
host,
|
|
2169
2312
|
openkeyHost: resolveOpenKeyHost(profile)
|
|
@@ -2178,16 +2321,10 @@ async function handleOpenKeyAuth(profileName, host, paste) {
|
|
|
2178
2321
|
};
|
|
2179
2322
|
if (delegationData.spaceId) {
|
|
2180
2323
|
updatedProfile.spaceId = delegationData.spaceId;
|
|
2181
|
-
updatedProfile.
|
|
2324
|
+
updatedProfile.ownerDid = delegationData.ownerDid;
|
|
2182
2325
|
}
|
|
2183
2326
|
await ProfileManager.setProfile(profileName, updatedProfile);
|
|
2184
|
-
|
|
2185
|
-
authenticated: true,
|
|
2186
|
-
profile: profileName,
|
|
2187
|
-
did: profile.did,
|
|
2188
|
-
spaceId: delegationData.spaceId,
|
|
2189
|
-
authMethod: "openkey"
|
|
2190
|
-
});
|
|
2327
|
+
return { profile: updatedProfile, delegationData };
|
|
2191
2328
|
}
|
|
2192
2329
|
|
|
2193
2330
|
// src/commands/kv.ts
|
|
@@ -2200,14 +2337,19 @@ async function readStdin2() {
|
|
|
2200
2337
|
}
|
|
2201
2338
|
return Buffer.concat(chunks);
|
|
2202
2339
|
}
|
|
2340
|
+
async function kvHandle(node, spaceInput, profileName) {
|
|
2341
|
+
const spaceUri = await resolveSpaceUri(spaceInput, profileName);
|
|
2342
|
+
return spaceUri ? node.kvForSpace(spaceUri) : node.kv;
|
|
2343
|
+
}
|
|
2203
2344
|
function registerKvCommand(program2) {
|
|
2204
2345
|
const kv = program2.command("kv").description("Key-value store operations");
|
|
2205
|
-
kv.command("get <key>").description("Get a value by key").option("--raw", "Output raw value (no JSON wrapping)").option("-o, --output <file>", "Write value to file").action(async (key, options, cmd) => {
|
|
2346
|
+
kv.command("get <key>").description("Get a value by key").option("--raw", "Output raw value (no JSON wrapping)").option("-o, --output <file>", "Write value to file").option("--space <name|uri>", "Target a non-primary space (short name or full URI)").action(async (key, options, cmd) => {
|
|
2206
2347
|
try {
|
|
2207
2348
|
const globalOpts = cmd.optsWithGlobals();
|
|
2208
2349
|
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
2209
2350
|
const node = await ensureAuthenticated(ctx);
|
|
2210
|
-
const
|
|
2351
|
+
const kv2 = await kvHandle(node, options.space, ctx.profile);
|
|
2352
|
+
const result = await withSpinner(`Getting ${key}...`, () => kv2.get(key));
|
|
2211
2353
|
if (!result.ok) {
|
|
2212
2354
|
if (result.error.code === "KV_NOT_FOUND" || result.error.code === "NOT_FOUND") {
|
|
2213
2355
|
throw new CLIError("NOT_FOUND", `Key "${key}" not found`, ExitCode.NOT_FOUND);
|
|
@@ -2288,13 +2430,14 @@ function registerKvCommand(program2) {
|
|
|
2288
2430
|
handleError(error);
|
|
2289
2431
|
}
|
|
2290
2432
|
});
|
|
2291
|
-
kv.command("list").description("List keys").option("--prefix <prefix>", "Filter by key prefix").action(async (options, cmd) => {
|
|
2433
|
+
kv.command("list").description("List keys").option("--prefix <prefix>", "Filter by key prefix").option("--space <name|uri>", "Target a non-primary space (short name or full URI)").action(async (options, cmd) => {
|
|
2292
2434
|
try {
|
|
2293
2435
|
const globalOpts = cmd.optsWithGlobals();
|
|
2294
2436
|
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
2295
2437
|
const node = await ensureAuthenticated(ctx);
|
|
2438
|
+
const kv2 = await kvHandle(node, options.space, ctx.profile);
|
|
2296
2439
|
const listOptions = options.prefix ? { prefix: options.prefix } : void 0;
|
|
2297
|
-
const result = await withSpinner("Listing keys...", () =>
|
|
2440
|
+
const result = await withSpinner("Listing keys...", () => kv2.list(listOptions));
|
|
2298
2441
|
if (!result.ok) {
|
|
2299
2442
|
throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
|
|
2300
2443
|
}
|
|
@@ -2322,12 +2465,13 @@ function registerKvCommand(program2) {
|
|
|
2322
2465
|
handleError(error);
|
|
2323
2466
|
}
|
|
2324
2467
|
});
|
|
2325
|
-
kv.command("head <key>").description("Get metadata for a key (no body)").action(async (key,
|
|
2468
|
+
kv.command("head <key>").description("Get metadata for a key (no body)").option("--space <name|uri>", "Target a non-primary space (short name or full URI)").action(async (key, options, cmd) => {
|
|
2326
2469
|
try {
|
|
2327
2470
|
const globalOpts = cmd.optsWithGlobals();
|
|
2328
2471
|
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
2329
2472
|
const node = await ensureAuthenticated(ctx);
|
|
2330
|
-
const
|
|
2473
|
+
const kv2 = await kvHandle(node, options.space, ctx.profile);
|
|
2474
|
+
const result = await withSpinner(`Checking ${key}...`, () => kv2.head(key));
|
|
2331
2475
|
if (!result.ok) {
|
|
2332
2476
|
if (result.error.code === "KV_NOT_FOUND" || result.error.code === "NOT_FOUND") {
|
|
2333
2477
|
outputJson({ key, exists: false, metadata: {} });
|
|
@@ -2452,6 +2596,15 @@ function parseExpiry(input) {
|
|
|
2452
2596
|
}
|
|
2453
2597
|
|
|
2454
2598
|
// src/commands/delegation.ts
|
|
2599
|
+
import { principalDidEquals } from "@tinycloud/node-sdk";
|
|
2600
|
+
function didMatches(actual, expected) {
|
|
2601
|
+
if (!actual) return false;
|
|
2602
|
+
try {
|
|
2603
|
+
return principalDidEquals(actual, expected);
|
|
2604
|
+
} catch {
|
|
2605
|
+
return actual === expected;
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2455
2608
|
function registerDelegationCommand(program2) {
|
|
2456
2609
|
const delegation = program2.command("delegation").description("Manage delegations");
|
|
2457
2610
|
delegation.command("create").description("Create a delegation").requiredOption("--to <did>", "Recipient DID").requiredOption("--path <path>", "KV path scope").requiredOption("--actions <actions>", "Comma-separated actions (e.g., kv/get,kv/list)").option("--expiry <duration>", "Expiry duration (e.g., 1h, 7d, ISO date)", "1h").action(async (options, cmd) => {
|
|
@@ -2496,10 +2649,10 @@ function registerDelegationCommand(program2) {
|
|
|
2496
2649
|
let delegations = result.data;
|
|
2497
2650
|
if (options.granted) {
|
|
2498
2651
|
const myDid = node.did;
|
|
2499
|
-
delegations = delegations.filter((d) => d.delegatorDID
|
|
2652
|
+
delegations = delegations.filter((d) => didMatches(d.delegatorDID, myDid));
|
|
2500
2653
|
} else if (options.received) {
|
|
2501
2654
|
const myDid = node.did;
|
|
2502
|
-
delegations = delegations.filter((d) => d.delegateDID
|
|
2655
|
+
delegations = delegations.filter((d) => didMatches(d.delegateDID, myDid));
|
|
2503
2656
|
}
|
|
2504
2657
|
outputJson({
|
|
2505
2658
|
delegations: delegations.map((d) => ({
|
|
@@ -2910,7 +3063,7 @@ _tc_completions() {
|
|
|
2910
3063
|
commands="init auth kv space delegation share node profile completion"
|
|
2911
3064
|
|
|
2912
3065
|
case "\${COMP_WORDS[1]}" in
|
|
2913
|
-
auth) subcommands="login logout status whoami" ;;
|
|
3066
|
+
auth) subcommands="login logout rotate status whoami" ;;
|
|
2914
3067
|
kv) subcommands="get put delete list head" ;;
|
|
2915
3068
|
space) subcommands="list create info switch" ;;
|
|
2916
3069
|
delegation) subcommands="create list info revoke" ;;
|
|
@@ -2960,7 +3113,7 @@ _tc() {
|
|
|
2960
3113
|
;;
|
|
2961
3114
|
args)
|
|
2962
3115
|
case $words[1] in
|
|
2963
|
-
auth) _values 'subcommand' login logout status whoami ;;
|
|
3116
|
+
auth) _values 'subcommand' login logout rotate status whoami ;;
|
|
2964
3117
|
kv) _values 'subcommand' get put delete list head ;;
|
|
2965
3118
|
space) _values 'subcommand' list create info switch ;;
|
|
2966
3119
|
delegation) _values 'subcommand' create list info revoke ;;
|
|
@@ -2995,7 +3148,7 @@ complete -c tc -n "not __fish_seen_subcommand_from $commands" -a profile -d "Pro
|
|
|
2995
3148
|
complete -c tc -n "not __fish_seen_subcommand_from $commands" -a completion -d "Generate shell completions"
|
|
2996
3149
|
|
|
2997
3150
|
# Subcommands
|
|
2998
|
-
complete -c tc -n "__fish_seen_subcommand_from auth" -a "login logout status whoami"
|
|
3151
|
+
complete -c tc -n "__fish_seen_subcommand_from auth" -a "login logout rotate status whoami"
|
|
2999
3152
|
complete -c tc -n "__fish_seen_subcommand_from kv" -a "get put delete list head"
|
|
3000
3153
|
complete -c tc -n "__fish_seen_subcommand_from space" -a "list create info switch"
|
|
3001
3154
|
complete -c tc -n "__fish_seen_subcommand_from delegation" -a "create list info revoke"
|
|
@@ -3189,6 +3342,17 @@ function registerVaultCommand(program2) {
|
|
|
3189
3342
|
// src/commands/secrets.ts
|
|
3190
3343
|
import { readFile as readFile6 } from "fs/promises";
|
|
3191
3344
|
import { writeFile as writeFile5 } from "fs/promises";
|
|
3345
|
+
import {
|
|
3346
|
+
resolveSecretListPrefix,
|
|
3347
|
+
resolveSecretPath
|
|
3348
|
+
} from "@tinycloud/node-sdk";
|
|
3349
|
+
var SECRETS_SPACE = "secrets";
|
|
3350
|
+
var SECRET_KV_ABILITIES = {
|
|
3351
|
+
get: "tinycloud.kv/get",
|
|
3352
|
+
put: "tinycloud.kv/put",
|
|
3353
|
+
del: "tinycloud.kv/del",
|
|
3354
|
+
list: "tinycloud.kv/list"
|
|
3355
|
+
};
|
|
3192
3356
|
async function readStdin4() {
|
|
3193
3357
|
const chunks = [];
|
|
3194
3358
|
for await (const chunk of process.stdin) {
|
|
@@ -3204,6 +3368,123 @@ function resolveSecretScope(options) {
|
|
|
3204
3368
|
const scope = options.scope ?? options.space;
|
|
3205
3369
|
return scope ? { scope } : void 0;
|
|
3206
3370
|
}
|
|
3371
|
+
async function ensureSecretsNode(ctx, options) {
|
|
3372
|
+
const auth = authOptions(options);
|
|
3373
|
+
if (auth?.privateKey) {
|
|
3374
|
+
return ensureAuthenticated(ctx, auth);
|
|
3375
|
+
}
|
|
3376
|
+
const profile = await ProfileManager.getProfile(ctx.profile).catch(() => null);
|
|
3377
|
+
if (profile?.authMethod === "openkey" && canRequestOwnerPermissions(profile)) {
|
|
3378
|
+
const session = await ProfileManager.getSession(ctx.profile);
|
|
3379
|
+
if (!session || isStoredSessionExpired(session)) {
|
|
3380
|
+
await withSpinner(
|
|
3381
|
+
session ? "Refreshing TinyCloud session..." : "Creating TinyCloud session...",
|
|
3382
|
+
() => refreshOpenKeySession(ctx.profile, ctx.host)
|
|
3383
|
+
);
|
|
3384
|
+
}
|
|
3385
|
+
}
|
|
3386
|
+
return ensureAuthenticated(ctx, auth);
|
|
3387
|
+
}
|
|
3388
|
+
async function runSecretOperation(params) {
|
|
3389
|
+
const first = await runSecretOperationAttempt(params.label, params.operation);
|
|
3390
|
+
if (first.ok || !shouldRequestSecretPermissions(first.error)) {
|
|
3391
|
+
return first;
|
|
3392
|
+
}
|
|
3393
|
+
const profile = await ProfileManager.getProfile(params.ctx.profile);
|
|
3394
|
+
if (!canRequestOwnerPermissions(profile)) {
|
|
3395
|
+
return first;
|
|
3396
|
+
}
|
|
3397
|
+
const requested = secretPermissionEntries({
|
|
3398
|
+
action: params.action,
|
|
3399
|
+
name: params.name,
|
|
3400
|
+
options: params.scopeOptions,
|
|
3401
|
+
node: params.node
|
|
3402
|
+
});
|
|
3403
|
+
await withSpinner(
|
|
3404
|
+
"Requesting secret permissions...",
|
|
3405
|
+
() => ensureDelegationAuthority({
|
|
3406
|
+
ctx: params.ctx,
|
|
3407
|
+
profile,
|
|
3408
|
+
node: params.node,
|
|
3409
|
+
requested,
|
|
3410
|
+
expiryOption: void 0,
|
|
3411
|
+
yes: true,
|
|
3412
|
+
force: true
|
|
3413
|
+
})
|
|
3414
|
+
);
|
|
3415
|
+
return runSecretOperationAttempt(params.label, params.operation);
|
|
3416
|
+
}
|
|
3417
|
+
async function runSecretOperationAttempt(label, operation) {
|
|
3418
|
+
try {
|
|
3419
|
+
return await withSpinner(label, operation);
|
|
3420
|
+
} catch (error) {
|
|
3421
|
+
const permissionError = thrownPermissionError(error);
|
|
3422
|
+
if (permissionError) return permissionError;
|
|
3423
|
+
throw error;
|
|
3424
|
+
}
|
|
3425
|
+
}
|
|
3426
|
+
function canRequestOwnerPermissions(profile) {
|
|
3427
|
+
const posture = resolveProfilePosture(profile);
|
|
3428
|
+
return posture === "owner-openkey" || posture === "local-owner-key";
|
|
3429
|
+
}
|
|
3430
|
+
function shouldRequestSecretPermissions(error) {
|
|
3431
|
+
if (error.code !== "PERMISSION_DENIED") return false;
|
|
3432
|
+
return /permission|session expired|autosign|capabilit/i.test(error.message);
|
|
3433
|
+
}
|
|
3434
|
+
function thrownPermissionError(error) {
|
|
3435
|
+
const record = error;
|
|
3436
|
+
const message = typeof record?.message === "string" ? record.message : String(error);
|
|
3437
|
+
const code = typeof record?.code === "string" ? record.code : "PERMISSION_DENIED";
|
|
3438
|
+
if (code !== "PERMISSION_DENIED" && !/permission|session expired|autosign|capabilit/i.test(message)) {
|
|
3439
|
+
return null;
|
|
3440
|
+
}
|
|
3441
|
+
return {
|
|
3442
|
+
ok: false,
|
|
3443
|
+
error: {
|
|
3444
|
+
code: "PERMISSION_DENIED",
|
|
3445
|
+
message
|
|
3446
|
+
}
|
|
3447
|
+
};
|
|
3448
|
+
}
|
|
3449
|
+
function isStoredSessionExpired(session) {
|
|
3450
|
+
const record = session;
|
|
3451
|
+
const direct = parseDate(record.expiresAt ?? record.expiry ?? record.expirationTime);
|
|
3452
|
+
if (direct) return direct.getTime() <= Date.now();
|
|
3453
|
+
if (typeof record.siwe !== "string") return false;
|
|
3454
|
+
const match = record.siwe.match(/^Expiration Time:\s*(.+)$/im);
|
|
3455
|
+
const expiry = match ? parseDate(match[1].trim()) : null;
|
|
3456
|
+
return expiry !== null && expiry.getTime() <= Date.now();
|
|
3457
|
+
}
|
|
3458
|
+
function parseDate(value) {
|
|
3459
|
+
if (value instanceof Date) {
|
|
3460
|
+
return Number.isNaN(value.getTime()) ? null : value;
|
|
3461
|
+
}
|
|
3462
|
+
if (typeof value !== "string" || value.trim() === "") return null;
|
|
3463
|
+
const date = new Date(value);
|
|
3464
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
3465
|
+
}
|
|
3466
|
+
function secretKvAbility(action) {
|
|
3467
|
+
return SECRET_KV_ABILITIES[action];
|
|
3468
|
+
}
|
|
3469
|
+
function secretPermissionEntries(params) {
|
|
3470
|
+
const path = params.action === "list" ? resolveSecretListPrefix(params.options) : resolveSecretPath(params.name ?? "", params.options).permissionPaths.vault;
|
|
3471
|
+
const permissions = [{
|
|
3472
|
+
service: "tinycloud.kv",
|
|
3473
|
+
space: SECRETS_SPACE,
|
|
3474
|
+
path,
|
|
3475
|
+
actions: [secretKvAbility(params.action)],
|
|
3476
|
+
skipPrefix: true
|
|
3477
|
+
}];
|
|
3478
|
+
if (params.action === "get") {
|
|
3479
|
+
permissions.push({
|
|
3480
|
+
service: "tinycloud.encryption",
|
|
3481
|
+
path: params.node.getDefaultEncryptionNetworkId(),
|
|
3482
|
+
actions: ["tinycloud.encryption/decrypt"],
|
|
3483
|
+
skipPrefix: true
|
|
3484
|
+
});
|
|
3485
|
+
}
|
|
3486
|
+
return permissions;
|
|
3487
|
+
}
|
|
3207
3488
|
function registerSecretsCommand(program2) {
|
|
3208
3489
|
const secrets = program2.command("secrets").description("Encrypted secrets management");
|
|
3209
3490
|
const network = secrets.command("network").description("Manage the default secrets encryption network");
|
|
@@ -3249,12 +3530,16 @@ function registerSecretsCommand(program2) {
|
|
|
3249
3530
|
try {
|
|
3250
3531
|
const globalOpts = cmd.optsWithGlobals();
|
|
3251
3532
|
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
3252
|
-
const node = await
|
|
3533
|
+
const node = await ensureSecretsNode(ctx, options);
|
|
3253
3534
|
const scopeOptions = resolveSecretScope(options);
|
|
3254
|
-
const result = await
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3535
|
+
const result = await runSecretOperation({
|
|
3536
|
+
ctx,
|
|
3537
|
+
node,
|
|
3538
|
+
action: "list",
|
|
3539
|
+
scopeOptions,
|
|
3540
|
+
label: "Listing secrets...",
|
|
3541
|
+
operation: () => node.secrets.list(scopeOptions)
|
|
3542
|
+
});
|
|
3258
3543
|
if (!result.ok) {
|
|
3259
3544
|
throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
|
|
3260
3545
|
}
|
|
@@ -3273,12 +3558,17 @@ function registerSecretsCommand(program2) {
|
|
|
3273
3558
|
try {
|
|
3274
3559
|
const globalOpts = cmd.optsWithGlobals();
|
|
3275
3560
|
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
3276
|
-
const node = await
|
|
3561
|
+
const node = await ensureSecretsNode(ctx, options);
|
|
3277
3562
|
const scopeOptions = resolveSecretScope(options);
|
|
3278
|
-
const result = await
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3563
|
+
const result = await runSecretOperation({
|
|
3564
|
+
ctx,
|
|
3565
|
+
node,
|
|
3566
|
+
action: "get",
|
|
3567
|
+
name,
|
|
3568
|
+
scopeOptions,
|
|
3569
|
+
label: `Getting secret ${name}...`,
|
|
3570
|
+
operation: () => node.secrets.get(name, scopeOptions)
|
|
3571
|
+
});
|
|
3282
3572
|
if (!result.ok) {
|
|
3283
3573
|
if (result.error.code === "NOT_FOUND" || result.error.code === "KEY_NOT_FOUND") {
|
|
3284
3574
|
throw new CLIError("NOT_FOUND", `Secret "${name}" not found`, ExitCode.NOT_FOUND);
|
|
@@ -3304,7 +3594,7 @@ function registerSecretsCommand(program2) {
|
|
|
3304
3594
|
try {
|
|
3305
3595
|
const globalOpts = cmd.optsWithGlobals();
|
|
3306
3596
|
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
3307
|
-
const node = await
|
|
3597
|
+
const node = await ensureSecretsNode(ctx, options);
|
|
3308
3598
|
let secretValue;
|
|
3309
3599
|
const sources = [value !== void 0, !!options.file, !!options.stdin].filter(Boolean);
|
|
3310
3600
|
if (sources.length === 0) {
|
|
@@ -3321,10 +3611,15 @@ function registerSecretsCommand(program2) {
|
|
|
3321
3611
|
secretValue = value;
|
|
3322
3612
|
}
|
|
3323
3613
|
const scopeOptions = resolveSecretScope(options);
|
|
3324
|
-
const result = await
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3614
|
+
const result = await runSecretOperation({
|
|
3615
|
+
ctx,
|
|
3616
|
+
node,
|
|
3617
|
+
action: "put",
|
|
3618
|
+
name,
|
|
3619
|
+
scopeOptions,
|
|
3620
|
+
label: `Storing secret ${name}...`,
|
|
3621
|
+
operation: () => node.secrets.put(name, secretValue, scopeOptions)
|
|
3622
|
+
});
|
|
3328
3623
|
if (!result.ok) {
|
|
3329
3624
|
throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
|
|
3330
3625
|
}
|
|
@@ -3337,12 +3632,17 @@ function registerSecretsCommand(program2) {
|
|
|
3337
3632
|
try {
|
|
3338
3633
|
const globalOpts = cmd.optsWithGlobals();
|
|
3339
3634
|
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
3340
|
-
const node = await
|
|
3635
|
+
const node = await ensureSecretsNode(ctx, options);
|
|
3341
3636
|
const scopeOptions = resolveSecretScope(options);
|
|
3342
|
-
const result = await
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3637
|
+
const result = await runSecretOperation({
|
|
3638
|
+
ctx,
|
|
3639
|
+
node,
|
|
3640
|
+
action: "del",
|
|
3641
|
+
name,
|
|
3642
|
+
scopeOptions,
|
|
3643
|
+
label: `Deleting secret ${name}...`,
|
|
3644
|
+
operation: () => node.secrets.delete(name, scopeOptions)
|
|
3645
|
+
});
|
|
3346
3646
|
if (!result.ok) {
|
|
3347
3647
|
throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
|
|
3348
3648
|
}
|
|
@@ -4253,6 +4553,358 @@ function registerUpgradeCommand(program2) {
|
|
|
4253
4553
|
});
|
|
4254
4554
|
}
|
|
4255
4555
|
|
|
4556
|
+
// src/commands/status.ts
|
|
4557
|
+
import {
|
|
4558
|
+
NodeWasmBindings
|
|
4559
|
+
} from "@tinycloud/node-sdk";
|
|
4560
|
+
var wasmBindings = null;
|
|
4561
|
+
function registerStatusCommand(program2) {
|
|
4562
|
+
program2.command("status").description("Show local TinyCloud profile, session, delegation, and permission state").action(async (_options, cmd) => {
|
|
4563
|
+
try {
|
|
4564
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
4565
|
+
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
4566
|
+
const config = await ProfileManager.getConfig();
|
|
4567
|
+
const names = (await ProfileManager.listProfiles()).sort(
|
|
4568
|
+
(a, b) => a.localeCompare(b)
|
|
4569
|
+
);
|
|
4570
|
+
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4571
|
+
const profiles = await Promise.all(
|
|
4572
|
+
names.map(
|
|
4573
|
+
(name) => inspectProfile({
|
|
4574
|
+
name,
|
|
4575
|
+
activeProfile: ctx.profile,
|
|
4576
|
+
defaultProfile: config.defaultProfile
|
|
4577
|
+
})
|
|
4578
|
+
)
|
|
4579
|
+
);
|
|
4580
|
+
const summary = {
|
|
4581
|
+
generatedAt,
|
|
4582
|
+
activeProfile: ctx.profile,
|
|
4583
|
+
defaultProfile: config.defaultProfile,
|
|
4584
|
+
profileCount: profiles.length,
|
|
4585
|
+
authenticatedProfileCount: profiles.filter((p) => p.authenticated).length,
|
|
4586
|
+
activeDelegationCount: profiles.reduce(
|
|
4587
|
+
(sum, profile) => sum + profile.activeDelegationCount,
|
|
4588
|
+
0
|
|
4589
|
+
),
|
|
4590
|
+
profiles
|
|
4591
|
+
};
|
|
4592
|
+
if (shouldOutputJson()) {
|
|
4593
|
+
outputJson(summary);
|
|
4594
|
+
return;
|
|
4595
|
+
}
|
|
4596
|
+
process.stdout.write(formatStatus(summary));
|
|
4597
|
+
} catch (error) {
|
|
4598
|
+
handleError(error);
|
|
4599
|
+
}
|
|
4600
|
+
});
|
|
4601
|
+
}
|
|
4602
|
+
async function inspectProfile(params) {
|
|
4603
|
+
const issues = [];
|
|
4604
|
+
const profile = await readProfile(params.name, issues);
|
|
4605
|
+
const session = await readSession(params.name, issues);
|
|
4606
|
+
const hasKey = await readHasKey(params.name, issues);
|
|
4607
|
+
const storedDelegations = await readDelegations(params.name, issues);
|
|
4608
|
+
const sessionPermissions = session ? sessionPermissionsFromRecap(session) : [];
|
|
4609
|
+
const sessionExpiry = session ? extractSessionExpiry(session) : null;
|
|
4610
|
+
const sessionExpired = sessionExpiry === null ? null : sessionExpiry.getTime() <= Date.now();
|
|
4611
|
+
const statusSession = {
|
|
4612
|
+
present: session !== null,
|
|
4613
|
+
expired: session === null ? null : sessionExpired,
|
|
4614
|
+
expiresAt: sessionExpiry?.toISOString() ?? null,
|
|
4615
|
+
permissions: sessionPermissions,
|
|
4616
|
+
permissionsCompact: compactPermissions(sessionPermissions)
|
|
4617
|
+
};
|
|
4618
|
+
const delegations = storedDelegations.map(inspectDelegation);
|
|
4619
|
+
const activeDelegationPermissions = delegations.filter((delegation) => delegation.active).flatMap((delegation) => delegation.permissions);
|
|
4620
|
+
const permissions = uniquePermissions([
|
|
4621
|
+
...sessionPermissions,
|
|
4622
|
+
...activeDelegationPermissions
|
|
4623
|
+
]);
|
|
4624
|
+
const hasPrivateKey = typeof profile?.privateKey === "string" && profile.privateKey.length > 0;
|
|
4625
|
+
const localKeyAuthenticated = profile?.authMethod === "local" && hasPrivateKey;
|
|
4626
|
+
const sessionAuthenticated = session !== null && sessionExpired !== true;
|
|
4627
|
+
const authenticated = localKeyAuthenticated || sessionAuthenticated;
|
|
4628
|
+
const status = resolveStatus({
|
|
4629
|
+
exists: profile !== null,
|
|
4630
|
+
authenticated,
|
|
4631
|
+
localKeyAuthenticated,
|
|
4632
|
+
sessionExpired
|
|
4633
|
+
});
|
|
4634
|
+
return {
|
|
4635
|
+
name: params.name,
|
|
4636
|
+
active: params.name === params.activeProfile,
|
|
4637
|
+
default: params.name === params.defaultProfile,
|
|
4638
|
+
exists: profile !== null,
|
|
4639
|
+
status,
|
|
4640
|
+
host: profile?.host ?? null,
|
|
4641
|
+
did: profile?.did ?? null,
|
|
4642
|
+
sessionDid: profile?.sessionDid ?? null,
|
|
4643
|
+
ownerDid: profile?.ownerDid ?? null,
|
|
4644
|
+
address: profile?.address ?? null,
|
|
4645
|
+
spaceId: profile?.spaceId ?? null,
|
|
4646
|
+
authMethod: profile?.authMethod ?? null,
|
|
4647
|
+
posture: profile ? resolveProfilePosture(profile) : null,
|
|
4648
|
+
operatorType: profile ? resolveProfileOperatorType(profile) : null,
|
|
4649
|
+
hasKey,
|
|
4650
|
+
hasPrivateKey,
|
|
4651
|
+
authenticated,
|
|
4652
|
+
session: statusSession,
|
|
4653
|
+
delegations,
|
|
4654
|
+
permissions,
|
|
4655
|
+
permissionsCompact: compactPermissions(permissions),
|
|
4656
|
+
permissionCount: permissions.length,
|
|
4657
|
+
activeDelegationCount: delegations.filter((delegation) => delegation.active).length,
|
|
4658
|
+
delegationCount: delegations.length,
|
|
4659
|
+
issues
|
|
4660
|
+
};
|
|
4661
|
+
}
|
|
4662
|
+
async function readProfile(name, issues) {
|
|
4663
|
+
try {
|
|
4664
|
+
return await ProfileManager.getProfile(name);
|
|
4665
|
+
} catch (error) {
|
|
4666
|
+
issues.push(`profile: ${messageFromError(error)}`);
|
|
4667
|
+
return null;
|
|
4668
|
+
}
|
|
4669
|
+
}
|
|
4670
|
+
async function readSession(name, issues) {
|
|
4671
|
+
try {
|
|
4672
|
+
return asRecord(await ProfileManager.getSession(name));
|
|
4673
|
+
} catch (error) {
|
|
4674
|
+
issues.push(`session: ${messageFromError(error)}`);
|
|
4675
|
+
return null;
|
|
4676
|
+
}
|
|
4677
|
+
}
|
|
4678
|
+
async function readHasKey(name, issues) {
|
|
4679
|
+
try {
|
|
4680
|
+
return await ProfileManager.getKey(name) !== null;
|
|
4681
|
+
} catch (error) {
|
|
4682
|
+
issues.push(`key: ${messageFromError(error)}`);
|
|
4683
|
+
return false;
|
|
4684
|
+
}
|
|
4685
|
+
}
|
|
4686
|
+
async function readDelegations(name, issues) {
|
|
4687
|
+
try {
|
|
4688
|
+
return await loadAdditionalDelegations(name);
|
|
4689
|
+
} catch (error) {
|
|
4690
|
+
issues.push(`delegations: ${messageFromError(error)}`);
|
|
4691
|
+
return [];
|
|
4692
|
+
}
|
|
4693
|
+
}
|
|
4694
|
+
function inspectDelegation(entry) {
|
|
4695
|
+
const expiry = parseDate2(entry.delegation.expiry);
|
|
4696
|
+
const expired = expiry === null ? null : expiry.getTime() <= Date.now();
|
|
4697
|
+
const permissions = normalizePermissions(
|
|
4698
|
+
Array.isArray(entry.permissions) && entry.permissions.length > 0 ? entry.permissions : permissionsFromDelegation(entry.delegation)
|
|
4699
|
+
);
|
|
4700
|
+
return {
|
|
4701
|
+
cid: entry.delegation.cid,
|
|
4702
|
+
active: expired !== true,
|
|
4703
|
+
expired,
|
|
4704
|
+
expiresAt: expiry?.toISOString() ?? null,
|
|
4705
|
+
permissions,
|
|
4706
|
+
permissionsCompact: compactPermissions(permissions)
|
|
4707
|
+
};
|
|
4708
|
+
}
|
|
4709
|
+
function resolveStatus(params) {
|
|
4710
|
+
if (!params.exists) return "missing";
|
|
4711
|
+
if (params.localKeyAuthenticated) return "local-key";
|
|
4712
|
+
if (params.authenticated) return "logged-in";
|
|
4713
|
+
if (params.sessionExpired === true) return "expired";
|
|
4714
|
+
return "signed-out";
|
|
4715
|
+
}
|
|
4716
|
+
function sessionPermissionsFromRecap(session) {
|
|
4717
|
+
if (typeof session.siwe !== "string" || session.siwe.length === 0) return [];
|
|
4718
|
+
try {
|
|
4719
|
+
const rawEntries = getWasmBindings().parseRecapFromSiwe(session.siwe);
|
|
4720
|
+
if (!Array.isArray(rawEntries)) return [];
|
|
4721
|
+
return normalizePermissions(rawEntries.map(permissionFromRawRecap));
|
|
4722
|
+
} catch {
|
|
4723
|
+
return [];
|
|
4724
|
+
}
|
|
4725
|
+
}
|
|
4726
|
+
function permissionFromRawRecap(value) {
|
|
4727
|
+
const record = asRecord(value);
|
|
4728
|
+
if (!record) return null;
|
|
4729
|
+
const service = stringValue(record.service);
|
|
4730
|
+
const space = stringValue(record.space);
|
|
4731
|
+
const path = stringValue(record.path);
|
|
4732
|
+
const actions = Array.isArray(record.actions) ? record.actions.map(String).filter(Boolean) : [];
|
|
4733
|
+
if (!service || !space || path === null || actions.length === 0) return null;
|
|
4734
|
+
return {
|
|
4735
|
+
service: normalizeService2(service),
|
|
4736
|
+
space,
|
|
4737
|
+
path,
|
|
4738
|
+
actions
|
|
4739
|
+
};
|
|
4740
|
+
}
|
|
4741
|
+
function normalizePermissions(entries) {
|
|
4742
|
+
const permissions = [];
|
|
4743
|
+
for (const entry of entries) {
|
|
4744
|
+
const permission = permissionFromUnknown(entry);
|
|
4745
|
+
if (permission) permissions.push(permission);
|
|
4746
|
+
}
|
|
4747
|
+
return uniquePermissions(permissions);
|
|
4748
|
+
}
|
|
4749
|
+
function permissionFromUnknown(value) {
|
|
4750
|
+
const record = asRecord(value);
|
|
4751
|
+
if (!record) return null;
|
|
4752
|
+
const service = stringValue(record.service);
|
|
4753
|
+
const space = stringValue(record.space);
|
|
4754
|
+
const path = stringValue(record.path);
|
|
4755
|
+
const actions = Array.isArray(record.actions) ? record.actions.map(String).filter(Boolean) : [];
|
|
4756
|
+
if (!service || !space || path === null || actions.length === 0) return null;
|
|
4757
|
+
return {
|
|
4758
|
+
service: normalizeService2(service),
|
|
4759
|
+
space,
|
|
4760
|
+
path,
|
|
4761
|
+
actions
|
|
4762
|
+
};
|
|
4763
|
+
}
|
|
4764
|
+
function uniquePermissions(entries) {
|
|
4765
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4766
|
+
const unique2 = [];
|
|
4767
|
+
for (const entry of entries) {
|
|
4768
|
+
const key = compactPermission(entry);
|
|
4769
|
+
if (seen.has(key)) continue;
|
|
4770
|
+
seen.add(key);
|
|
4771
|
+
unique2.push(entry);
|
|
4772
|
+
}
|
|
4773
|
+
return unique2;
|
|
4774
|
+
}
|
|
4775
|
+
function compactPermissions(entries) {
|
|
4776
|
+
return entries.map(compactPermission);
|
|
4777
|
+
}
|
|
4778
|
+
function extractSessionExpiry(session) {
|
|
4779
|
+
for (const key of ["expiresAt", "expiry", "expirationTime"]) {
|
|
4780
|
+
const parsed = parseDate2(session[key]);
|
|
4781
|
+
if (parsed) return parsed;
|
|
4782
|
+
}
|
|
4783
|
+
if (typeof session.siwe !== "string") return null;
|
|
4784
|
+
const match = session.siwe.match(/^Expiration Time:\s*(.+)$/im);
|
|
4785
|
+
return match ? parseDate2(match[1].trim()) : null;
|
|
4786
|
+
}
|
|
4787
|
+
function parseDate2(value) {
|
|
4788
|
+
if (value instanceof Date) {
|
|
4789
|
+
return Number.isNaN(value.getTime()) ? null : value;
|
|
4790
|
+
}
|
|
4791
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
4792
|
+
const millis = value > 0 && value < 1e12 ? value * 1e3 : value;
|
|
4793
|
+
const date2 = new Date(millis);
|
|
4794
|
+
return Number.isNaN(date2.getTime()) ? null : date2;
|
|
4795
|
+
}
|
|
4796
|
+
if (typeof value !== "string" || value.trim() === "") return null;
|
|
4797
|
+
const date = new Date(value);
|
|
4798
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
4799
|
+
}
|
|
4800
|
+
function getWasmBindings() {
|
|
4801
|
+
wasmBindings ??= new NodeWasmBindings();
|
|
4802
|
+
return wasmBindings;
|
|
4803
|
+
}
|
|
4804
|
+
function normalizeService2(service) {
|
|
4805
|
+
return service.startsWith("tinycloud.") ? service : `tinycloud.${service}`;
|
|
4806
|
+
}
|
|
4807
|
+
function asRecord(value) {
|
|
4808
|
+
return value !== null && typeof value === "object" ? value : null;
|
|
4809
|
+
}
|
|
4810
|
+
function stringValue(value) {
|
|
4811
|
+
return typeof value === "string" ? value : null;
|
|
4812
|
+
}
|
|
4813
|
+
function messageFromError(error) {
|
|
4814
|
+
return error instanceof Error ? error.message : String(error);
|
|
4815
|
+
}
|
|
4816
|
+
function formatStatus(summary) {
|
|
4817
|
+
const lines = [];
|
|
4818
|
+
lines.push(theme.heading("TinyCloud Status"));
|
|
4819
|
+
lines.push(`Active profile: ${theme.value(summary.activeProfile)}`);
|
|
4820
|
+
lines.push(`Default profile: ${theme.value(summary.defaultProfile)}`);
|
|
4821
|
+
lines.push("");
|
|
4822
|
+
if (summary.profiles.length === 0) {
|
|
4823
|
+
lines.push(theme.muted("No profiles configured. Run: tc init"));
|
|
4824
|
+
return `${lines.join("\n")}
|
|
4825
|
+
`;
|
|
4826
|
+
}
|
|
4827
|
+
lines.push(theme.label("Profiles"));
|
|
4828
|
+
for (const profile of summary.profiles) {
|
|
4829
|
+
lines.push(formatProfile(profile));
|
|
4830
|
+
}
|
|
4831
|
+
return `${lines.join("\n")}
|
|
4832
|
+
`;
|
|
4833
|
+
}
|
|
4834
|
+
function formatProfile(profile) {
|
|
4835
|
+
const marker = profile.active ? theme.success("*") : " ";
|
|
4836
|
+
const name = profile.default ? `${profile.name} (default)` : profile.name;
|
|
4837
|
+
const host = profile.host ? theme.muted(profile.host) : theme.muted("no host");
|
|
4838
|
+
const summary = [
|
|
4839
|
+
`${marker} ${profile.active ? theme.brand(name) : name}`,
|
|
4840
|
+
formatProfileStatus(profile.status),
|
|
4841
|
+
profile.posture ?? "no posture",
|
|
4842
|
+
plural(profile.permissionCount, "permission"),
|
|
4843
|
+
`${profile.activeDelegationCount}/${profile.delegationCount} delegations`,
|
|
4844
|
+
host
|
|
4845
|
+
].join(" ");
|
|
4846
|
+
const lines = [summary];
|
|
4847
|
+
lines.push(` session: ${formatSession(profile.session)}`);
|
|
4848
|
+
if (profile.permissionsCompact.length > 0) {
|
|
4849
|
+
lines.push(" permissions:");
|
|
4850
|
+
for (const permission of profile.permissionsCompact) {
|
|
4851
|
+
lines.push(` ${permission}`);
|
|
4852
|
+
}
|
|
4853
|
+
}
|
|
4854
|
+
if (profile.delegations.length > 0) {
|
|
4855
|
+
lines.push(" delegations:");
|
|
4856
|
+
for (const delegation of profile.delegations) {
|
|
4857
|
+
lines.push(` ${formatDelegation(delegation)}`);
|
|
4858
|
+
}
|
|
4859
|
+
}
|
|
4860
|
+
if (profile.issues.length > 0) {
|
|
4861
|
+
lines.push(" issues:");
|
|
4862
|
+
for (const issue of profile.issues) {
|
|
4863
|
+
lines.push(` ${theme.warn(issue)}`);
|
|
4864
|
+
}
|
|
4865
|
+
}
|
|
4866
|
+
return lines.join("\n");
|
|
4867
|
+
}
|
|
4868
|
+
function formatProfileStatus(status) {
|
|
4869
|
+
switch (status) {
|
|
4870
|
+
case "logged-in":
|
|
4871
|
+
return theme.success("logged in");
|
|
4872
|
+
case "local-key":
|
|
4873
|
+
return theme.success("local key");
|
|
4874
|
+
case "expired":
|
|
4875
|
+
return theme.warn("expired");
|
|
4876
|
+
case "missing":
|
|
4877
|
+
return theme.warn("missing");
|
|
4878
|
+
case "signed-out":
|
|
4879
|
+
return theme.muted("signed out");
|
|
4880
|
+
}
|
|
4881
|
+
}
|
|
4882
|
+
function formatSession(session) {
|
|
4883
|
+
if (!session.present) return theme.muted("none");
|
|
4884
|
+
if (session.expired === true) {
|
|
4885
|
+
return `${theme.warn("expired")}${formatExpiresAt(session.expiresAt)}`;
|
|
4886
|
+
}
|
|
4887
|
+
if (session.expired === false) {
|
|
4888
|
+
return `${theme.success("active")}${formatExpiresAt(session.expiresAt)}`;
|
|
4889
|
+
}
|
|
4890
|
+
return `${theme.success("present")}${formatExpiresAt(session.expiresAt)}`;
|
|
4891
|
+
}
|
|
4892
|
+
function formatDelegation(delegation) {
|
|
4893
|
+
const state = delegation.expired === true ? theme.warn("expired") : theme.success("active");
|
|
4894
|
+
return [
|
|
4895
|
+
delegation.cid,
|
|
4896
|
+
state,
|
|
4897
|
+
formatExpiresAt(delegation.expiresAt).trim(),
|
|
4898
|
+
plural(delegation.permissions.length, "permission")
|
|
4899
|
+
].filter(Boolean).join(" ");
|
|
4900
|
+
}
|
|
4901
|
+
function formatExpiresAt(expiresAt) {
|
|
4902
|
+
return expiresAt ? ` until ${expiresAt}` : "";
|
|
4903
|
+
}
|
|
4904
|
+
function plural(count, label) {
|
|
4905
|
+
return `${count} ${label}${count === 1 ? "" : "s"}`;
|
|
4906
|
+
}
|
|
4907
|
+
|
|
4256
4908
|
// src/index.ts
|
|
4257
4909
|
var { version } = JSON.parse(
|
|
4258
4910
|
readFileSync3(new URL("../package.json", import.meta.url), "utf-8")
|
|
@@ -4267,7 +4919,7 @@ program.hook("preAction", async (thisCommand) => {
|
|
|
4267
4919
|
const commandName = thisCommand.name();
|
|
4268
4920
|
const parentName = thisCommand.parent?.name();
|
|
4269
4921
|
const fullCommand = parentName && parentName !== "tc" ? `${parentName} ${commandName}` : commandName;
|
|
4270
|
-
const skipGuard = ["tc", "init", "doctor", "completion", "help", "upgrade"].includes(commandName) || fullCommand === "profile create";
|
|
4922
|
+
const skipGuard = ["tc", "init", "doctor", "completion", "help", "upgrade", "status"].includes(commandName) || fullCommand === "profile create";
|
|
4271
4923
|
if (!skipGuard && !opts.quiet && isInteractive()) {
|
|
4272
4924
|
try {
|
|
4273
4925
|
const config = await ProfileManager.getConfig();
|
|
@@ -4302,6 +4954,9 @@ registerSqlCommand(program);
|
|
|
4302
4954
|
registerDuckdbCommand(program);
|
|
4303
4955
|
registerManifestCommand(program);
|
|
4304
4956
|
registerUpgradeCommand(program);
|
|
4957
|
+
registerStatusCommand(program);
|
|
4958
|
+
program.addHelpText("before", () => `${theme.label("Version:")} ${theme.value(version)}
|
|
4959
|
+
`);
|
|
4305
4960
|
program.addHelpText("afterAll", () => {
|
|
4306
4961
|
if (!process.stdout.isTTY) return "";
|
|
4307
4962
|
return `
|