@tinycloud/cli 0.5.1-beta.0 → 0.6.0-beta.10
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 +1311 -117
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -548,8 +548,8 @@ var ProfileManager = class _ProfileManager {
|
|
|
548
548
|
};
|
|
549
549
|
|
|
550
550
|
// src/auth/local-key.ts
|
|
551
|
-
import { TCWSessionManager, initPanicHook } from "@tinycloud/node-sdk-wasm";
|
|
552
|
-
import { PrivateKeySigner } from "@tinycloud/node-sdk";
|
|
551
|
+
import { TCWSessionManager, importKey, initPanicHook } from "@tinycloud/node-sdk-wasm";
|
|
552
|
+
import { PrivateKeySigner, pkhDid } from "@tinycloud/node-sdk";
|
|
553
553
|
import { randomBytes } from "crypto";
|
|
554
554
|
var wasmInitialized = false;
|
|
555
555
|
function ensureWasm() {
|
|
@@ -568,6 +568,12 @@ function generateKey() {
|
|
|
568
568
|
const did = mgr.getDID(keyId);
|
|
569
569
|
return { jwk, did };
|
|
570
570
|
}
|
|
571
|
+
function keyToDID(jwk) {
|
|
572
|
+
ensureWasm();
|
|
573
|
+
const mgr = new TCWSessionManager();
|
|
574
|
+
const keyId = importKey(mgr, JSON.stringify(jwk), "imported");
|
|
575
|
+
return mgr.getDID(keyId);
|
|
576
|
+
}
|
|
571
577
|
function generateEthereumPrivateKey() {
|
|
572
578
|
const keyBytes = randomBytes(32);
|
|
573
579
|
return "0x" + keyBytes.toString("hex");
|
|
@@ -577,7 +583,7 @@ async function deriveAddress(privateKey) {
|
|
|
577
583
|
return signer.getAddress();
|
|
578
584
|
}
|
|
579
585
|
function addressToDID(address, chainId = 1) {
|
|
580
|
-
return
|
|
586
|
+
return pkhDid(address, chainId);
|
|
581
587
|
}
|
|
582
588
|
async function generateLocalIdentity(chainId = 1) {
|
|
583
589
|
const privateKey = generateEthereumPrivateKey();
|
|
@@ -594,16 +600,45 @@ async function localKeySignIn(options) {
|
|
|
594
600
|
});
|
|
595
601
|
await node.signIn();
|
|
596
602
|
const address = await new PrivateKeySigner(options.privateKey).getAddress();
|
|
603
|
+
const session = node.session;
|
|
604
|
+
if (!session) {
|
|
605
|
+
throw new Error("Local key sign-in did not produce a TinyCloud session");
|
|
606
|
+
}
|
|
597
607
|
return {
|
|
598
|
-
spaceId:
|
|
608
|
+
spaceId: session.spaceId,
|
|
599
609
|
address,
|
|
600
|
-
chainId: 1
|
|
610
|
+
chainId: 1,
|
|
611
|
+
delegationHeader: session.delegationHeader,
|
|
612
|
+
delegationCid: session.delegationCid,
|
|
613
|
+
jwk: session.jwk,
|
|
614
|
+
verificationMethod: session.verificationMethod,
|
|
615
|
+
siwe: session.siwe,
|
|
616
|
+
signature: session.signature
|
|
601
617
|
};
|
|
602
618
|
}
|
|
603
619
|
|
|
604
620
|
// src/auth/browser-auth.ts
|
|
605
621
|
import { createServer } from "http";
|
|
606
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
|
+
}
|
|
607
642
|
async function startAuthFlow(did, options = {}) {
|
|
608
643
|
if (options.paste) {
|
|
609
644
|
return pasteFlow(did, options);
|
|
@@ -625,7 +660,9 @@ function buildAuthUrl(did, options = {}) {
|
|
|
625
660
|
params.set("callback", options.callback);
|
|
626
661
|
}
|
|
627
662
|
if (options.jwk) {
|
|
628
|
-
const jwkB64 = Buffer.from(
|
|
663
|
+
const jwkB64 = Buffer.from(
|
|
664
|
+
JSON.stringify(publicJwkForDelegation(options.jwk))
|
|
665
|
+
).toString("base64url");
|
|
629
666
|
params.set("jwk", jwkB64);
|
|
630
667
|
}
|
|
631
668
|
if (options.host) {
|
|
@@ -827,7 +864,7 @@ function registerInitCommand(program2) {
|
|
|
827
864
|
await ProfileManager.setProfile(profileName, {
|
|
828
865
|
...profileConfig,
|
|
829
866
|
spaceId: delegationData.spaceId,
|
|
830
|
-
|
|
867
|
+
ownerDid: delegationData.ownerDid
|
|
831
868
|
});
|
|
832
869
|
outputJson({
|
|
833
870
|
profile: profileName,
|
|
@@ -843,12 +880,41 @@ function registerInitCommand(program2) {
|
|
|
843
880
|
}
|
|
844
881
|
|
|
845
882
|
// src/commands/auth.ts
|
|
883
|
+
import { get as httpGet } from "http";
|
|
884
|
+
import { get as httpsGet } from "https";
|
|
885
|
+
import { spawn } from "child_process";
|
|
886
|
+
import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
|
|
887
|
+
import { dirname as dirname2 } from "path";
|
|
846
888
|
import { createInterface as createInterface2 } from "readline";
|
|
847
889
|
|
|
890
|
+
// src/config/types.ts
|
|
891
|
+
var CLI_PROFILE_POSTURES = [
|
|
892
|
+
"owner-openkey",
|
|
893
|
+
"delegate-session",
|
|
894
|
+
"local-owner-key"
|
|
895
|
+
];
|
|
896
|
+
var CLI_OPERATOR_TYPES = ["human", "agent"];
|
|
897
|
+
function isCLIProfilePosture(value) {
|
|
898
|
+
return typeof value === "string" && CLI_PROFILE_POSTURES.includes(value);
|
|
899
|
+
}
|
|
900
|
+
function isCLIOperatorType(value) {
|
|
901
|
+
return typeof value === "string" && CLI_OPERATOR_TYPES.includes(value);
|
|
902
|
+
}
|
|
903
|
+
function resolveProfilePosture(profile) {
|
|
904
|
+
if (isCLIProfilePosture(profile.posture)) return profile.posture;
|
|
905
|
+
if (profile.authMethod === "local") return "local-owner-key";
|
|
906
|
+
return "owner-openkey";
|
|
907
|
+
}
|
|
908
|
+
function resolveProfileOperatorType(profile) {
|
|
909
|
+
if (isCLIOperatorType(profile.operatorType)) return profile.operatorType;
|
|
910
|
+
return "human";
|
|
911
|
+
}
|
|
912
|
+
|
|
848
913
|
// src/lib/sdk.ts
|
|
849
914
|
import { TinyCloudNode } from "@tinycloud/node-sdk";
|
|
850
915
|
|
|
851
916
|
// src/lib/permissions.ts
|
|
917
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
852
918
|
import { appendFile, readFile as readFile2 } from "fs/promises";
|
|
853
919
|
import { join as join4 } from "path";
|
|
854
920
|
import {
|
|
@@ -858,13 +924,22 @@ import {
|
|
|
858
924
|
} from "@tinycloud/node-sdk";
|
|
859
925
|
|
|
860
926
|
// src/lib/space.ts
|
|
927
|
+
import {
|
|
928
|
+
buildSpaceUri,
|
|
929
|
+
canonicalizeAddress,
|
|
930
|
+
makePkhSpaceId,
|
|
931
|
+
parsePkhDid,
|
|
932
|
+
parseSpaceUri
|
|
933
|
+
} from "@tinycloud/node-sdk";
|
|
861
934
|
function resolveAddress(profile, session) {
|
|
862
935
|
const sessAddr = session?.address;
|
|
863
|
-
if (typeof sessAddr === "string" && sessAddr.length > 0)
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
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;
|
|
868
943
|
}
|
|
869
944
|
throw new CLIError(
|
|
870
945
|
"ADDRESS_UNKNOWN",
|
|
@@ -879,7 +954,17 @@ function resolveChainId(profile, session) {
|
|
|
879
954
|
}
|
|
880
955
|
async function resolveSpaceUri(input, profileName) {
|
|
881
956
|
if (!input) return void 0;
|
|
882
|
-
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
|
+
}
|
|
883
968
|
if (!/^[A-Za-z0-9_-]+$/.test(input)) {
|
|
884
969
|
throw new CLIError(
|
|
885
970
|
"INVALID_SPACE",
|
|
@@ -891,16 +976,44 @@ async function resolveSpaceUri(input, profileName) {
|
|
|
891
976
|
const session = await ProfileManager.getSession(profileName);
|
|
892
977
|
const address = resolveAddress(profile, session);
|
|
893
978
|
const chainId = resolveChainId(profile, session);
|
|
894
|
-
return
|
|
979
|
+
return makePkhSpaceId(address, chainId, input);
|
|
895
980
|
}
|
|
896
981
|
|
|
897
982
|
// src/lib/permissions.ts
|
|
898
983
|
function additionalDelegationsPath(profile) {
|
|
899
984
|
return join4(PROFILES_DIR, profile, "additional-delegations.json");
|
|
900
985
|
}
|
|
986
|
+
function permissionRequestsPath(profile) {
|
|
987
|
+
return join4(PROFILES_DIR, profile, "auth-requests.json");
|
|
988
|
+
}
|
|
901
989
|
function grantHistoryPath(profile) {
|
|
902
990
|
return join4(PROFILES_DIR, profile, "auth-grants.jsonl");
|
|
903
991
|
}
|
|
992
|
+
function createPermissionRequestArtifact(params) {
|
|
993
|
+
return {
|
|
994
|
+
kind: "tinycloud.auth.request",
|
|
995
|
+
version: 1,
|
|
996
|
+
requestId: `req_${Date.now().toString(36)}_${randomBytes2(4).toString("hex")}`,
|
|
997
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
998
|
+
profile: params.profileName,
|
|
999
|
+
posture: resolveProfilePosture(params.profile),
|
|
1000
|
+
operatorType: resolveProfileOperatorType(params.profile),
|
|
1001
|
+
host: params.host,
|
|
1002
|
+
sessionDid: didWithoutFragment(params.profile.sessionDid ?? params.profile.did),
|
|
1003
|
+
ownerDid: params.profile.ownerDid,
|
|
1004
|
+
spaceId: params.profile.spaceId,
|
|
1005
|
+
requestedExpiry: params.requestedExpiry,
|
|
1006
|
+
requested: params.requested,
|
|
1007
|
+
command: {
|
|
1008
|
+
argv: params.argv ?? process.argv.slice(2),
|
|
1009
|
+
cwd: params.cwd ?? process.cwd()
|
|
1010
|
+
}
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
function didWithoutFragment(did) {
|
|
1014
|
+
const fragment = did.indexOf("#");
|
|
1015
|
+
return fragment === -1 ? did : did.slice(0, fragment);
|
|
1016
|
+
}
|
|
904
1017
|
async function loadAdditionalDelegations(profile) {
|
|
905
1018
|
const raw = await readJson(
|
|
906
1019
|
additionalDelegationsPath(profile)
|
|
@@ -918,6 +1031,41 @@ async function appendAdditionalDelegation(profile, entry) {
|
|
|
918
1031
|
next.push(entry);
|
|
919
1032
|
await saveAdditionalDelegations(profile, next);
|
|
920
1033
|
}
|
|
1034
|
+
async function loadPermissionRequestArtifacts(profile) {
|
|
1035
|
+
const raw = await readJson(
|
|
1036
|
+
permissionRequestsPath(profile)
|
|
1037
|
+
);
|
|
1038
|
+
return Array.isArray(raw) ? raw.filter(isPermissionRequestArtifact) : [];
|
|
1039
|
+
}
|
|
1040
|
+
async function savePermissionRequestArtifacts(profile, entries) {
|
|
1041
|
+
const profileDir = join4(PROFILES_DIR, profile);
|
|
1042
|
+
await ensureDir(profileDir);
|
|
1043
|
+
await writeJson(permissionRequestsPath(profile), entries);
|
|
1044
|
+
}
|
|
1045
|
+
async function appendPermissionRequestArtifact(profile, artifact) {
|
|
1046
|
+
const existing = await loadPermissionRequestArtifacts(profile);
|
|
1047
|
+
const next = existing.filter((item) => item.requestId !== artifact.requestId);
|
|
1048
|
+
next.push(artifact);
|
|
1049
|
+
await savePermissionRequestArtifacts(profile, next);
|
|
1050
|
+
}
|
|
1051
|
+
async function getPermissionRequestArtifact(profile, requestId) {
|
|
1052
|
+
const existing = await loadPermissionRequestArtifacts(profile);
|
|
1053
|
+
return existing.find((item) => item.requestId === requestId) ?? null;
|
|
1054
|
+
}
|
|
1055
|
+
async function getLastPermissionRequestArtifact(profile) {
|
|
1056
|
+
const existing = await loadPermissionRequestArtifacts(profile);
|
|
1057
|
+
return existing.at(-1) ?? null;
|
|
1058
|
+
}
|
|
1059
|
+
function isPermissionRequestArtifact(value) {
|
|
1060
|
+
if (value === null || typeof value !== "object") return false;
|
|
1061
|
+
const candidate = value;
|
|
1062
|
+
return candidate.kind === "tinycloud.auth.request" && candidate.version === 1 && typeof candidate.requestId === "string" && Array.isArray(candidate.requested);
|
|
1063
|
+
}
|
|
1064
|
+
function isDelegationImportArtifact(value) {
|
|
1065
|
+
if (value === null || typeof value !== "object") return false;
|
|
1066
|
+
const candidate = value;
|
|
1067
|
+
return candidate.kind === "tinycloud.auth.delegation" && candidate.version === 1 && candidate.delegation !== void 0 && typeof candidate.delegation === "object";
|
|
1068
|
+
}
|
|
921
1069
|
async function replayAdditionalDelegations(node, profile) {
|
|
922
1070
|
const entries = await loadAdditionalDelegations(profile);
|
|
923
1071
|
for (const entry of entries) {
|
|
@@ -1130,7 +1278,21 @@ async function createSDKInstance(ctx, options) {
|
|
|
1130
1278
|
host: ctx.host,
|
|
1131
1279
|
privateKey: effectivePrivateKey
|
|
1132
1280
|
});
|
|
1133
|
-
|
|
1281
|
+
if (session && session.delegationHeader && session.delegationCid && session.spaceId) {
|
|
1282
|
+
await node2.restoreSession({
|
|
1283
|
+
delegationHeader: session.delegationHeader,
|
|
1284
|
+
delegationCid: session.delegationCid,
|
|
1285
|
+
spaceId: session.spaceId,
|
|
1286
|
+
jwk: session.jwk ?? key,
|
|
1287
|
+
verificationMethod: session.verificationMethod ?? profile.sessionDid ?? profile.did,
|
|
1288
|
+
address: session.address,
|
|
1289
|
+
chainId: session.chainId,
|
|
1290
|
+
siwe: session.siwe,
|
|
1291
|
+
signature: session.signature
|
|
1292
|
+
});
|
|
1293
|
+
} else {
|
|
1294
|
+
await node2.signIn();
|
|
1295
|
+
}
|
|
1134
1296
|
await replayAdditionalDelegations(node2, ctx.profile);
|
|
1135
1297
|
return node2;
|
|
1136
1298
|
}
|
|
@@ -1240,6 +1402,15 @@ function registerAuthCommand(program2) {
|
|
|
1240
1402
|
handleError(error);
|
|
1241
1403
|
}
|
|
1242
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
|
+
});
|
|
1243
1414
|
auth.command("status").description("Show current authentication state").action(async (_options, cmd) => {
|
|
1244
1415
|
try {
|
|
1245
1416
|
const globalOpts = cmd.optsWithGlobals();
|
|
@@ -1252,17 +1423,22 @@ function registerAuthCommand(program2) {
|
|
|
1252
1423
|
} catch {
|
|
1253
1424
|
profile = null;
|
|
1254
1425
|
}
|
|
1426
|
+
const posture = profile ? resolveProfilePosture(profile) : null;
|
|
1427
|
+
const operatorType = profile ? resolveProfileOperatorType(profile) : null;
|
|
1255
1428
|
const authenticated = session !== null;
|
|
1256
1429
|
if (shouldOutputJson()) {
|
|
1257
1430
|
outputJson({
|
|
1258
1431
|
authenticated,
|
|
1259
1432
|
did: profile?.did ?? null,
|
|
1260
|
-
|
|
1433
|
+
sessionDid: profile?.sessionDid ?? null,
|
|
1434
|
+
ownerDid: profile?.ownerDid ?? null,
|
|
1261
1435
|
spaceId: profile?.spaceId ?? null,
|
|
1262
1436
|
host: ctx.host,
|
|
1263
1437
|
profile: ctx.profile,
|
|
1264
1438
|
hasKey: hasKey !== null,
|
|
1265
1439
|
authMethod: profile?.authMethod ?? null,
|
|
1440
|
+
posture,
|
|
1441
|
+
operatorType,
|
|
1266
1442
|
address: profile?.address ?? null
|
|
1267
1443
|
});
|
|
1268
1444
|
} else {
|
|
@@ -1270,9 +1446,12 @@ function registerAuthCommand(program2) {
|
|
|
1270
1446
|
process.stdout.write(formatField("Profile", ctx.profile) + "\n");
|
|
1271
1447
|
process.stdout.write(formatField("Authenticated", authenticated) + "\n");
|
|
1272
1448
|
process.stdout.write(formatField("Auth Method", profile?.authMethod ?? null) + "\n");
|
|
1449
|
+
process.stdout.write(formatField("Posture", posture) + "\n");
|
|
1450
|
+
process.stdout.write(formatField("Operator", operatorType) + "\n");
|
|
1273
1451
|
process.stdout.write(formatField("Host", ctx.host) + "\n");
|
|
1274
1452
|
process.stdout.write(formatField("DID", profile?.did ?? null) + "\n");
|
|
1275
|
-
process.stdout.write(formatField("
|
|
1453
|
+
process.stdout.write(formatField("Session DID", profile?.sessionDid ?? null) + "\n");
|
|
1454
|
+
process.stdout.write(formatField("Owner DID", profile?.ownerDid ?? null) + "\n");
|
|
1276
1455
|
process.stdout.write(formatField("Address", profile?.address ?? null) + "\n");
|
|
1277
1456
|
process.stdout.write(formatField("Space ID", profile?.spaceId ?? null) + "\n");
|
|
1278
1457
|
process.stdout.write(formatField("Has Key", hasKey !== null) + "\n");
|
|
@@ -1281,7 +1460,7 @@ function registerAuthCommand(program2) {
|
|
|
1281
1460
|
handleError(error);
|
|
1282
1461
|
}
|
|
1283
1462
|
});
|
|
1284
|
-
auth.command("request").description("
|
|
1463
|
+
auth.command("request").description("Create a TinyCloud permission request artifact").option(
|
|
1285
1464
|
"--cap <spec>",
|
|
1286
1465
|
"Capability spec: tinycloud.<service>:<space>:<path>:<actions-csv> (repeatable)",
|
|
1287
1466
|
(value, previous) => [...previous, value],
|
|
@@ -1289,13 +1468,13 @@ function registerAuthCommand(program2) {
|
|
|
1289
1468
|
).option("--permission <file>", 'JSON permission request: { "permissions": PermissionEntry[] }').option("--manifest <fileOrBase64>", "Manifest file, base64:<json>, or raw base64 JSON").option(
|
|
1290
1469
|
"--expiry <duration>",
|
|
1291
1470
|
`Lifetime of the granted delegation. ms-format string (e.g. "7d", "30m") or raw milliseconds. Defaults to 7d, capped by the active session's expiry.`
|
|
1292
|
-
).option("--yes", "Skip local-key TTY confirmation", false).action(async (options, cmd) => {
|
|
1471
|
+
).option("--emit [file]", "Emit the request artifact to stdout, or write it to file when provided").option("--grant", "Grant the requested permissions immediately with this owner profile").option("--yes", "Skip local-key TTY confirmation", false).action(async (options, cmd) => {
|
|
1293
1472
|
try {
|
|
1294
1473
|
const globalOpts = cmd.optsWithGlobals();
|
|
1295
1474
|
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
1296
1475
|
const profile = await ProfileManager.getProfile(ctx.profile);
|
|
1297
|
-
const session = await ProfileManager.getSession(ctx.profile);
|
|
1298
1476
|
const requested = await collectRequestedPermissions(options, ctx.profile);
|
|
1477
|
+
const expiryOption = parseExpiryOption(options.expiry);
|
|
1299
1478
|
if (requested.length === 0) {
|
|
1300
1479
|
throw new CLIError(
|
|
1301
1480
|
"NO_CAPS_REQUESTED",
|
|
@@ -1303,6 +1482,18 @@ function registerAuthCommand(program2) {
|
|
|
1303
1482
|
ExitCode.USAGE_ERROR
|
|
1304
1483
|
);
|
|
1305
1484
|
}
|
|
1485
|
+
if (!options.grant) {
|
|
1486
|
+
const artifact = createPermissionRequestArtifact({
|
|
1487
|
+
profileName: ctx.profile,
|
|
1488
|
+
profile,
|
|
1489
|
+
host: ctx.host,
|
|
1490
|
+
requested,
|
|
1491
|
+
requestedExpiry: expiryOption
|
|
1492
|
+
});
|
|
1493
|
+
await appendPermissionRequestArtifact(ctx.profile, artifact);
|
|
1494
|
+
await emitPermissionRequestArtifact(artifact, options.emit);
|
|
1495
|
+
return;
|
|
1496
|
+
}
|
|
1306
1497
|
const node = await ensureAuthenticated(ctx);
|
|
1307
1498
|
if (node.hasRuntimePermissions(requested)) {
|
|
1308
1499
|
outputJson({ changed: false, missing: [], added: [] });
|
|
@@ -1316,14 +1507,13 @@ function registerAuthCommand(program2) {
|
|
|
1316
1507
|
const delegationCids2 = [];
|
|
1317
1508
|
let expiry2;
|
|
1318
1509
|
const openkeyHost = resolveOpenKeyHost(profile);
|
|
1319
|
-
const expiryOption2 = parseExpiryOption(options.expiry);
|
|
1320
1510
|
for (const group of groupPermissionsBySpace(requested)) {
|
|
1321
1511
|
const delegationData = await startAuthFlow(profile.did, {
|
|
1322
1512
|
jwk: key,
|
|
1323
1513
|
host: ctx.host,
|
|
1324
1514
|
permissions: group,
|
|
1325
1515
|
openkeyHost,
|
|
1326
|
-
expiry:
|
|
1516
|
+
expiry: expiryOption
|
|
1327
1517
|
});
|
|
1328
1518
|
const delegation = portableFromOpenKeyDelegation(delegationData, group, ctx.host);
|
|
1329
1519
|
const stored = storedAdditionalDelegation(delegation, group);
|
|
@@ -1358,8 +1548,6 @@ function registerAuthCommand(program2) {
|
|
|
1358
1548
|
ExitCode.USAGE_ERROR
|
|
1359
1549
|
);
|
|
1360
1550
|
}
|
|
1361
|
-
void session;
|
|
1362
|
-
const expiryOption = parseExpiryOption(options.expiry);
|
|
1363
1551
|
const delegations = await node.grantRuntimePermissions(
|
|
1364
1552
|
requested,
|
|
1365
1553
|
expiryOption !== void 0 ? { expiry: expiryOption } : void 0
|
|
@@ -1394,6 +1582,136 @@ function registerAuthCommand(program2) {
|
|
|
1394
1582
|
handleError(error);
|
|
1395
1583
|
}
|
|
1396
1584
|
});
|
|
1585
|
+
auth.command("import [source]").description("Import a TinyCloud delegation or permission request artifact").option("--stdin", "Read the JSON artifact from stdin").option("--paste", "Read the JSON artifact from stdin").action(async (source, options, cmd) => {
|
|
1586
|
+
try {
|
|
1587
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1588
|
+
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
1589
|
+
const raw = await readAuthArtifactSource(source, {
|
|
1590
|
+
stdin: options.stdin === true || options.paste === true
|
|
1591
|
+
});
|
|
1592
|
+
const parsed = JSON.parse(raw);
|
|
1593
|
+
if (isPermissionRequestArtifact(parsed)) {
|
|
1594
|
+
await appendPermissionRequestArtifact(ctx.profile, parsed);
|
|
1595
|
+
outputJson({
|
|
1596
|
+
imported: true,
|
|
1597
|
+
kind: parsed.kind,
|
|
1598
|
+
requestId: parsed.requestId,
|
|
1599
|
+
requested: parsed.requested,
|
|
1600
|
+
next: `tc auth retry ${parsed.requestId}`
|
|
1601
|
+
});
|
|
1602
|
+
return;
|
|
1603
|
+
}
|
|
1604
|
+
const imported = normalizeDelegationImport(parsed);
|
|
1605
|
+
const node = await ensureAuthenticated(ctx);
|
|
1606
|
+
await appendAdditionalDelegation(ctx.profile, storedAdditionalDelegation(
|
|
1607
|
+
imported.delegation,
|
|
1608
|
+
imported.permissions
|
|
1609
|
+
));
|
|
1610
|
+
await node.useRuntimeDelegation(imported.delegation);
|
|
1611
|
+
await appendGrantHistory(ctx.profile, {
|
|
1612
|
+
addedCaps: imported.permissions,
|
|
1613
|
+
source: "cli",
|
|
1614
|
+
delegationCid: imported.delegation.cid,
|
|
1615
|
+
expiry: imported.delegation.expiry.toISOString()
|
|
1616
|
+
});
|
|
1617
|
+
outputJson({
|
|
1618
|
+
imported: true,
|
|
1619
|
+
kind: "tinycloud.auth.delegation",
|
|
1620
|
+
requestId: imported.requestId ?? null,
|
|
1621
|
+
delegationCid: imported.delegation.cid,
|
|
1622
|
+
permissions: imported.permissions,
|
|
1623
|
+
expiry: imported.delegation.expiry.toISOString()
|
|
1624
|
+
});
|
|
1625
|
+
} catch (error) {
|
|
1626
|
+
handleError(error);
|
|
1627
|
+
}
|
|
1628
|
+
});
|
|
1629
|
+
auth.command("grant [request]").description("Grant a TinyCloud permission request artifact to its requester").option("--stdin", "Read the JSON request artifact from stdin").option("--paste", "Read the JSON request artifact from stdin").option("--yes", "Skip local-key TTY confirmation", false).action(async (source, options, cmd) => {
|
|
1630
|
+
try {
|
|
1631
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1632
|
+
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
1633
|
+
const profile = await ProfileManager.getProfile(ctx.profile);
|
|
1634
|
+
const raw = await readAuthArtifactSource(source, {
|
|
1635
|
+
stdin: options.stdin === true || options.paste === true
|
|
1636
|
+
});
|
|
1637
|
+
const parsed = JSON.parse(raw);
|
|
1638
|
+
if (!isPermissionRequestArtifact(parsed)) {
|
|
1639
|
+
throw new CLIError(
|
|
1640
|
+
"INVALID_AUTH_REQUEST",
|
|
1641
|
+
"Auth grant requires a tinycloud.auth.request artifact.",
|
|
1642
|
+
ExitCode.USAGE_ERROR
|
|
1643
|
+
);
|
|
1644
|
+
}
|
|
1645
|
+
const node = await ensureAuthenticated(ctx);
|
|
1646
|
+
await ensureDelegationAuthority({
|
|
1647
|
+
ctx,
|
|
1648
|
+
profile,
|
|
1649
|
+
node,
|
|
1650
|
+
requested: parsed.requested,
|
|
1651
|
+
expiryOption: parsed.requestedExpiry,
|
|
1652
|
+
yes: options.yes === true
|
|
1653
|
+
});
|
|
1654
|
+
const result = await node.delegateTo(
|
|
1655
|
+
parsed.sessionDid,
|
|
1656
|
+
parsed.requested,
|
|
1657
|
+
parsed.requestedExpiry !== void 0 ? { expiry: parsed.requestedExpiry } : void 0
|
|
1658
|
+
);
|
|
1659
|
+
outputJson({
|
|
1660
|
+
kind: "tinycloud.auth.delegation",
|
|
1661
|
+
version: 1,
|
|
1662
|
+
requestId: parsed.requestId,
|
|
1663
|
+
delegationCid: result.delegation.cid,
|
|
1664
|
+
delegation: result.delegation,
|
|
1665
|
+
permissions: parsed.requested,
|
|
1666
|
+
expiry: result.delegation.expiry.toISOString(),
|
|
1667
|
+
prompted: result.prompted
|
|
1668
|
+
});
|
|
1669
|
+
} catch (error) {
|
|
1670
|
+
handleError(error);
|
|
1671
|
+
}
|
|
1672
|
+
});
|
|
1673
|
+
auth.command("retry [requestId]").description("Check whether a stored permission request is now satisfied").option("--last", "Use the latest stored permission request for this profile").option("--exec", "Run the captured command when the request is covered").action(async (requestId, options, cmd) => {
|
|
1674
|
+
try {
|
|
1675
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1676
|
+
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
1677
|
+
const artifact = options.last ? await getLastPermissionRequestArtifact(ctx.profile) : requestId ? await getPermissionRequestArtifact(ctx.profile, requestId) : null;
|
|
1678
|
+
if (!artifact) {
|
|
1679
|
+
throw new CLIError(
|
|
1680
|
+
"REQUEST_NOT_FOUND",
|
|
1681
|
+
options.last ? `No stored permission requests exist for profile "${ctx.profile}".` : "Provide a requestId or use --last.",
|
|
1682
|
+
ExitCode.NOT_FOUND
|
|
1683
|
+
);
|
|
1684
|
+
}
|
|
1685
|
+
const node = await ensureAuthenticated(ctx);
|
|
1686
|
+
const covered = node.hasRuntimePermissions(artifact.requested);
|
|
1687
|
+
if (options.exec) {
|
|
1688
|
+
if (!covered) {
|
|
1689
|
+
throw new CLIError(
|
|
1690
|
+
"PERMISSIONS_MISSING",
|
|
1691
|
+
`Request ${artifact.requestId} is not covered yet. Import a delegation, then retry with --exec.`,
|
|
1692
|
+
ExitCode.PERMISSION_DENIED
|
|
1693
|
+
);
|
|
1694
|
+
}
|
|
1695
|
+
if (!artifact.command?.argv?.length) {
|
|
1696
|
+
throw new CLIError(
|
|
1697
|
+
"COMMAND_NOT_CAPTURED",
|
|
1698
|
+
`Request ${artifact.requestId} does not include a captured command.`,
|
|
1699
|
+
ExitCode.USAGE_ERROR
|
|
1700
|
+
);
|
|
1701
|
+
}
|
|
1702
|
+
await execCapturedCommand(artifact.command);
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
outputJson({
|
|
1706
|
+
requestId: artifact.requestId,
|
|
1707
|
+
covered,
|
|
1708
|
+
missing: covered ? [] : artifact.requested,
|
|
1709
|
+
command: artifact.command ?? null
|
|
1710
|
+
});
|
|
1711
|
+
} catch (error) {
|
|
1712
|
+
handleError(error);
|
|
1713
|
+
}
|
|
1714
|
+
});
|
|
1397
1715
|
auth.command("caps").description("Show granted capabilities for the active session").option("--diff <spec>", "Show missing capabilities for a spec").option("--history", "Show recent permission grants").action(async (options, cmd) => {
|
|
1398
1716
|
try {
|
|
1399
1717
|
const globalOpts = cmd.optsWithGlobals();
|
|
@@ -1459,23 +1777,31 @@ function registerAuthCommand(program2) {
|
|
|
1459
1777
|
const profile = await ProfileManager.getProfile(ctx.profile);
|
|
1460
1778
|
const session = await ProfileManager.getSession(ctx.profile);
|
|
1461
1779
|
const authenticated = session !== null;
|
|
1780
|
+
const posture = resolveProfilePosture(profile);
|
|
1781
|
+
const operatorType = resolveProfileOperatorType(profile);
|
|
1462
1782
|
if (shouldOutputJson()) {
|
|
1463
1783
|
outputJson({
|
|
1464
1784
|
profile: ctx.profile,
|
|
1465
1785
|
did: profile.did,
|
|
1466
|
-
|
|
1786
|
+
sessionDid: profile.sessionDid ?? null,
|
|
1787
|
+
ownerDid: profile.ownerDid ?? null,
|
|
1467
1788
|
spaceId: profile.spaceId ?? null,
|
|
1468
1789
|
host: profile.host,
|
|
1469
1790
|
authenticated,
|
|
1470
1791
|
authMethod: profile.authMethod ?? null,
|
|
1792
|
+
posture,
|
|
1793
|
+
operatorType,
|
|
1471
1794
|
address: profile.address ?? null
|
|
1472
1795
|
});
|
|
1473
1796
|
} else {
|
|
1474
1797
|
process.stdout.write(theme.heading("Identity") + "\n");
|
|
1475
1798
|
process.stdout.write(formatField("Profile", ctx.profile) + "\n");
|
|
1476
1799
|
process.stdout.write(formatField("DID", profile.did) + "\n");
|
|
1477
|
-
process.stdout.write(formatField("
|
|
1800
|
+
process.stdout.write(formatField("Session DID", profile.sessionDid ?? null) + "\n");
|
|
1801
|
+
process.stdout.write(formatField("Owner DID", profile.ownerDid ?? null) + "\n");
|
|
1478
1802
|
process.stdout.write(formatField("Auth Method", profile.authMethod ?? null) + "\n");
|
|
1803
|
+
process.stdout.write(formatField("Posture", posture) + "\n");
|
|
1804
|
+
process.stdout.write(formatField("Operator", operatorType) + "\n");
|
|
1479
1805
|
process.stdout.write(formatField("Address", profile.address ?? null) + "\n");
|
|
1480
1806
|
process.stdout.write(formatField("Space ID", profile.spaceId ?? null) + "\n");
|
|
1481
1807
|
process.stdout.write(formatField("Host", profile.host) + "\n");
|
|
@@ -1486,6 +1812,210 @@ function registerAuthCommand(program2) {
|
|
|
1486
1812
|
}
|
|
1487
1813
|
});
|
|
1488
1814
|
}
|
|
1815
|
+
async function emitPermissionRequestArtifact(artifact, emitOption) {
|
|
1816
|
+
if (typeof emitOption === "string" && emitOption.length > 0) {
|
|
1817
|
+
await mkdir2(dirname2(emitOption), { recursive: true });
|
|
1818
|
+
await writeFile2(emitOption, JSON.stringify(artifact, null, 2) + "\n", "utf8");
|
|
1819
|
+
outputJson({
|
|
1820
|
+
emitted: true,
|
|
1821
|
+
path: emitOption,
|
|
1822
|
+
requestId: artifact.requestId,
|
|
1823
|
+
requested: artifact.requested
|
|
1824
|
+
});
|
|
1825
|
+
return;
|
|
1826
|
+
}
|
|
1827
|
+
outputJson(artifact);
|
|
1828
|
+
}
|
|
1829
|
+
async function readAuthArtifactSource(source, options) {
|
|
1830
|
+
if (options.stdin || source === "-" || !source && !isInteractive()) {
|
|
1831
|
+
return readStdin();
|
|
1832
|
+
}
|
|
1833
|
+
if (!source) {
|
|
1834
|
+
throw new CLIError(
|
|
1835
|
+
"IMPORT_SOURCE_REQUIRED",
|
|
1836
|
+
"Provide an artifact file, URL, or use --stdin.",
|
|
1837
|
+
ExitCode.USAGE_ERROR
|
|
1838
|
+
);
|
|
1839
|
+
}
|
|
1840
|
+
if (source.startsWith("http://") || source.startsWith("https://")) {
|
|
1841
|
+
return readUrl(source);
|
|
1842
|
+
}
|
|
1843
|
+
return readFile3(source, "utf8");
|
|
1844
|
+
}
|
|
1845
|
+
async function readStdin() {
|
|
1846
|
+
const chunks = [];
|
|
1847
|
+
for await (const chunk of process.stdin) {
|
|
1848
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
|
|
1849
|
+
}
|
|
1850
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
1851
|
+
}
|
|
1852
|
+
function readUrl(source) {
|
|
1853
|
+
return new Promise((resolve3, reject) => {
|
|
1854
|
+
const getter = source.startsWith("https://") ? httpsGet : httpGet;
|
|
1855
|
+
const request = getter(source, (response) => {
|
|
1856
|
+
const status = response.statusCode ?? 0;
|
|
1857
|
+
if (status >= 300 && status < 400 && response.headers.location) {
|
|
1858
|
+
response.resume();
|
|
1859
|
+
readUrl(new URL(response.headers.location, source).toString()).then(resolve3, reject);
|
|
1860
|
+
return;
|
|
1861
|
+
}
|
|
1862
|
+
if (status < 200 || status >= 300) {
|
|
1863
|
+
response.resume();
|
|
1864
|
+
reject(new CLIError(
|
|
1865
|
+
"IMPORT_FETCH_FAILED",
|
|
1866
|
+
`Failed to fetch ${source}: HTTP ${status}.`,
|
|
1867
|
+
ExitCode.ERROR
|
|
1868
|
+
));
|
|
1869
|
+
return;
|
|
1870
|
+
}
|
|
1871
|
+
const chunks = [];
|
|
1872
|
+
response.on("data", (chunk) => {
|
|
1873
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
1874
|
+
});
|
|
1875
|
+
response.on("end", () => resolve3(Buffer.concat(chunks).toString("utf8")));
|
|
1876
|
+
});
|
|
1877
|
+
request.on("error", reject);
|
|
1878
|
+
});
|
|
1879
|
+
}
|
|
1880
|
+
function normalizeDelegationImport(value) {
|
|
1881
|
+
if (isDelegationImportArtifact(value)) {
|
|
1882
|
+
const delegation = normalizePortableDelegation(value.delegation);
|
|
1883
|
+
return {
|
|
1884
|
+
requestId: value.requestId,
|
|
1885
|
+
delegation,
|
|
1886
|
+
permissions: Array.isArray(value.permissions) && value.permissions.length > 0 ? value.permissions : permissionsFromDelegation(delegation)
|
|
1887
|
+
};
|
|
1888
|
+
}
|
|
1889
|
+
if (isStoredDelegationLike(value)) {
|
|
1890
|
+
const delegation = normalizePortableDelegation(value.delegation);
|
|
1891
|
+
return {
|
|
1892
|
+
delegation,
|
|
1893
|
+
permissions: Array.isArray(value.permissions) && value.permissions.length > 0 ? value.permissions : permissionsFromDelegation(delegation)
|
|
1894
|
+
};
|
|
1895
|
+
}
|
|
1896
|
+
if (isPortableDelegationLike(value)) {
|
|
1897
|
+
const delegation = normalizePortableDelegation(value);
|
|
1898
|
+
return {
|
|
1899
|
+
delegation,
|
|
1900
|
+
permissions: permissionsFromDelegation(delegation)
|
|
1901
|
+
};
|
|
1902
|
+
}
|
|
1903
|
+
throw new CLIError(
|
|
1904
|
+
"INVALID_AUTH_IMPORT",
|
|
1905
|
+
"Auth import must be a tinycloud.auth.delegation artifact, a portable delegation, or a tinycloud.auth.request artifact.",
|
|
1906
|
+
ExitCode.USAGE_ERROR
|
|
1907
|
+
);
|
|
1908
|
+
}
|
|
1909
|
+
function isStoredDelegationLike(value) {
|
|
1910
|
+
if (value === null || typeof value !== "object") return false;
|
|
1911
|
+
const candidate = value;
|
|
1912
|
+
return isPortableDelegationLike(candidate.delegation);
|
|
1913
|
+
}
|
|
1914
|
+
function isPortableDelegationLike(value) {
|
|
1915
|
+
if (value === null || typeof value !== "object") return false;
|
|
1916
|
+
const candidate = value;
|
|
1917
|
+
return typeof candidate.cid === "string" && typeof candidate.spaceId === "string" && typeof candidate.path === "string" && Array.isArray(candidate.actions) && candidate.delegationHeader !== void 0 && typeof candidate.delegationHeader === "object";
|
|
1918
|
+
}
|
|
1919
|
+
function normalizePortableDelegation(delegation) {
|
|
1920
|
+
const rawExpiry = delegation.expiry;
|
|
1921
|
+
const expiry = rawExpiry instanceof Date ? rawExpiry : new Date(String(rawExpiry));
|
|
1922
|
+
if (Number.isNaN(expiry.getTime())) {
|
|
1923
|
+
throw new CLIError(
|
|
1924
|
+
"INVALID_AUTH_IMPORT",
|
|
1925
|
+
"Imported delegation must include a valid expiry.",
|
|
1926
|
+
ExitCode.USAGE_ERROR
|
|
1927
|
+
);
|
|
1928
|
+
}
|
|
1929
|
+
return { ...delegation, expiry };
|
|
1930
|
+
}
|
|
1931
|
+
async function ensureDelegationAuthority(params) {
|
|
1932
|
+
if (!params.force && params.node.hasRuntimePermissions(params.requested)) return;
|
|
1933
|
+
if (params.profile.authMethod === "openkey") {
|
|
1934
|
+
const key = await ProfileManager.getKey(params.ctx.profile);
|
|
1935
|
+
if (!key) {
|
|
1936
|
+
throw new CLIError(
|
|
1937
|
+
"NO_KEY",
|
|
1938
|
+
`No key found for profile "${params.ctx.profile}". Run \`tc init\` first.`,
|
|
1939
|
+
ExitCode.AUTH_REQUIRED
|
|
1940
|
+
);
|
|
1941
|
+
}
|
|
1942
|
+
const openkeyHost = resolveOpenKeyHost(params.profile);
|
|
1943
|
+
for (const group of groupPermissionsBySpace(params.requested)) {
|
|
1944
|
+
const delegationData = await startAuthFlow(params.profile.did, {
|
|
1945
|
+
jwk: key,
|
|
1946
|
+
host: params.ctx.host,
|
|
1947
|
+
permissions: group,
|
|
1948
|
+
openkeyHost,
|
|
1949
|
+
expiry: params.expiryOption
|
|
1950
|
+
});
|
|
1951
|
+
const delegation = portableFromOpenKeyDelegation(delegationData, group, params.ctx.host);
|
|
1952
|
+
await appendAdditionalDelegation(
|
|
1953
|
+
params.ctx.profile,
|
|
1954
|
+
storedAdditionalDelegation(delegation, group)
|
|
1955
|
+
);
|
|
1956
|
+
await params.node.useRuntimeDelegation(delegation);
|
|
1957
|
+
await appendGrantHistory(params.ctx.profile, {
|
|
1958
|
+
addedCaps: group,
|
|
1959
|
+
source: "cli",
|
|
1960
|
+
delegationCid: delegation.cid,
|
|
1961
|
+
expiry: delegation.expiry.toISOString()
|
|
1962
|
+
});
|
|
1963
|
+
}
|
|
1964
|
+
return;
|
|
1965
|
+
}
|
|
1966
|
+
if (isInteractive()) {
|
|
1967
|
+
if (!params.yes) {
|
|
1968
|
+
await confirmPermissionRequest(params.requested);
|
|
1969
|
+
}
|
|
1970
|
+
} else if (!params.yes) {
|
|
1971
|
+
throw new CLIError(
|
|
1972
|
+
"CONFIRMATION_REQUIRED",
|
|
1973
|
+
"Local-key auth grants in non-interactive mode require --yes.",
|
|
1974
|
+
ExitCode.USAGE_ERROR
|
|
1975
|
+
);
|
|
1976
|
+
}
|
|
1977
|
+
const delegations = await params.node.grantRuntimePermissions(
|
|
1978
|
+
params.requested,
|
|
1979
|
+
params.expiryOption !== void 0 ? { expiry: params.expiryOption } : void 0
|
|
1980
|
+
);
|
|
1981
|
+
for (const delegation of delegations) {
|
|
1982
|
+
const covering = permissionsFromDelegation(delegation);
|
|
1983
|
+
await appendAdditionalDelegation(
|
|
1984
|
+
params.ctx.profile,
|
|
1985
|
+
storedAdditionalDelegation(delegation, covering)
|
|
1986
|
+
);
|
|
1987
|
+
await appendGrantHistory(params.ctx.profile, {
|
|
1988
|
+
addedCaps: covering,
|
|
1989
|
+
source: "cli",
|
|
1990
|
+
delegationCid: delegation.cid,
|
|
1991
|
+
expiry: delegation.expiry.toISOString()
|
|
1992
|
+
});
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
function execCapturedCommand(command) {
|
|
1996
|
+
return new Promise((resolve3, reject) => {
|
|
1997
|
+
const child = spawn(process.execPath, [process.argv[1], ...command.argv], {
|
|
1998
|
+
cwd: command.cwd,
|
|
1999
|
+
env: process.env,
|
|
2000
|
+
stdio: "inherit"
|
|
2001
|
+
});
|
|
2002
|
+
child.on("error", reject);
|
|
2003
|
+
child.on("exit", (code, signal) => {
|
|
2004
|
+
if (signal) {
|
|
2005
|
+
reject(new CLIError(
|
|
2006
|
+
"COMMAND_SIGNAL",
|
|
2007
|
+
`Captured command exited from signal ${signal}.`,
|
|
2008
|
+
ExitCode.ERROR
|
|
2009
|
+
));
|
|
2010
|
+
return;
|
|
2011
|
+
}
|
|
2012
|
+
if (code && code !== 0) {
|
|
2013
|
+
process.exitCode = code;
|
|
2014
|
+
}
|
|
2015
|
+
resolve3();
|
|
2016
|
+
});
|
|
2017
|
+
});
|
|
2018
|
+
}
|
|
1489
2019
|
async function collectRequestedPermissions(options, profile) {
|
|
1490
2020
|
const permissions = [];
|
|
1491
2021
|
for (const spec of options.cap ?? []) {
|
|
@@ -1545,18 +2075,40 @@ function parseExpiryOption(raw) {
|
|
|
1545
2075
|
}
|
|
1546
2076
|
function groupPermissionsBySpace(permissions) {
|
|
1547
2077
|
const groups = /* @__PURE__ */ new Map();
|
|
2078
|
+
const rawEntries = [];
|
|
1548
2079
|
for (const permission of permissions) {
|
|
2080
|
+
if (isRawPermission(permission)) {
|
|
2081
|
+
rawEntries.push(permission);
|
|
2082
|
+
continue;
|
|
2083
|
+
}
|
|
1549
2084
|
const group = groups.get(permission.space) ?? [];
|
|
1550
2085
|
group.push(permission);
|
|
1551
2086
|
groups.set(permission.space, group);
|
|
1552
2087
|
}
|
|
1553
|
-
|
|
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;
|
|
1554
2103
|
}
|
|
1555
2104
|
function portableFromOpenKeyDelegation(data, permissions, host) {
|
|
1556
|
-
const primary = permissions[0];
|
|
1557
|
-
const returnedSpace = String(data.spaceId ?? primary.space);
|
|
1558
|
-
const expectedSpaces = new Set(
|
|
1559
|
-
|
|
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) {
|
|
1560
2112
|
throw new CLIError(
|
|
1561
2113
|
"OPENKEY_SCOPE_MISMATCH",
|
|
1562
2114
|
`OpenKey returned delegation for ${returnedSpace}, expected ${Array.from(expectedSpaces).join(", ")}.`,
|
|
@@ -1591,15 +2143,74 @@ function inferDelegationExpiry(data) {
|
|
|
1591
2143
|
}
|
|
1592
2144
|
return new Date(Date.now() + 60 * 60 * 1e3);
|
|
1593
2145
|
}
|
|
1594
|
-
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 = {}) {
|
|
1595
2204
|
const profile = await ProfileManager.getProfile(profileName).catch(() => null);
|
|
2205
|
+
const posture = profile ? resolveProfilePosture(profile) : null;
|
|
1596
2206
|
let privateKey;
|
|
1597
2207
|
let address;
|
|
1598
2208
|
let did;
|
|
1599
|
-
|
|
2209
|
+
let sessionDid = profile?.sessionDid;
|
|
2210
|
+
if ((profile?.authMethod === "local" || posture === "local-owner-key") && profile.privateKey) {
|
|
1600
2211
|
privateKey = profile.privateKey;
|
|
1601
|
-
address = profile.address;
|
|
1602
|
-
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);
|
|
1603
2214
|
if (isInteractive()) {
|
|
1604
2215
|
process.stderr.write(theme.muted("Using existing local key") + "\n");
|
|
1605
2216
|
process.stderr.write(formatField("Address", address) + "\n");
|
|
@@ -1618,11 +2229,14 @@ async function handleLocalAuth(profileName, host) {
|
|
|
1618
2229
|
}
|
|
1619
2230
|
}
|
|
1620
2231
|
const hasKey = await ProfileManager.getKey(profileName);
|
|
1621
|
-
if (!hasKey) {
|
|
1622
|
-
const { jwk } = await withSpinner("Generating session key...", async () => {
|
|
2232
|
+
if (options.forceSessionKey || !hasKey) {
|
|
2233
|
+
const { jwk, did: generatedSessionDid } = await withSpinner("Generating session key...", async () => {
|
|
1623
2234
|
return generateKey();
|
|
1624
2235
|
});
|
|
1625
2236
|
await ProfileManager.setKey(profileName, jwk);
|
|
2237
|
+
sessionDid = generatedSessionDid;
|
|
2238
|
+
} else if (!sessionDid) {
|
|
2239
|
+
sessionDid = keyToDID(hasKey);
|
|
1626
2240
|
}
|
|
1627
2241
|
const sessionResult = await withSpinner("Signing in...", async () => {
|
|
1628
2242
|
return localKeySignIn({ privateKey, host });
|
|
@@ -1631,31 +2245,57 @@ async function handleLocalAuth(profileName, host) {
|
|
|
1631
2245
|
authMethod: "local",
|
|
1632
2246
|
address,
|
|
1633
2247
|
chainId: DEFAULT_CHAIN_ID,
|
|
1634
|
-
spaceId: sessionResult.spaceId
|
|
2248
|
+
spaceId: sessionResult.spaceId,
|
|
2249
|
+
delegationHeader: sessionResult.delegationHeader,
|
|
2250
|
+
delegationCid: sessionResult.delegationCid,
|
|
2251
|
+
jwk: sessionResult.jwk,
|
|
2252
|
+
verificationMethod: sessionResult.verificationMethod,
|
|
2253
|
+
siwe: sessionResult.siwe,
|
|
2254
|
+
signature: sessionResult.signature
|
|
1635
2255
|
});
|
|
1636
|
-
|
|
2256
|
+
sessionDid = sessionResult.verificationMethod;
|
|
2257
|
+
const updatedProfile = {
|
|
2258
|
+
...profile,
|
|
1637
2259
|
name: profileName,
|
|
1638
2260
|
host,
|
|
1639
2261
|
chainId: DEFAULT_CHAIN_ID,
|
|
1640
2262
|
spaceName: "default",
|
|
1641
2263
|
did,
|
|
1642
|
-
|
|
2264
|
+
sessionDid,
|
|
2265
|
+
ownerDid: did,
|
|
1643
2266
|
spaceId: sessionResult.spaceId,
|
|
1644
2267
|
createdAt: profile?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
2268
|
+
posture: profile?.posture ?? "local-owner-key",
|
|
2269
|
+
operatorType: profile?.operatorType ?? "human",
|
|
1645
2270
|
authMethod: "local",
|
|
1646
2271
|
privateKey,
|
|
1647
2272
|
address
|
|
1648
|
-
}
|
|
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 });
|
|
1649
2290
|
outputJson({
|
|
1650
2291
|
authenticated: true,
|
|
1651
2292
|
profile: profileName,
|
|
1652
|
-
did,
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
authMethod: "local"
|
|
2293
|
+
did: profile.did,
|
|
2294
|
+
spaceId: delegationData.spaceId,
|
|
2295
|
+
authMethod: "openkey"
|
|
1656
2296
|
});
|
|
1657
2297
|
}
|
|
1658
|
-
async function
|
|
2298
|
+
async function refreshOpenKeySession(profileName, host, options = {}) {
|
|
1659
2299
|
const key = await ProfileManager.getKey(profileName);
|
|
1660
2300
|
if (!key) {
|
|
1661
2301
|
throw new CLIError(
|
|
@@ -1666,7 +2306,7 @@ async function handleOpenKeyAuth(profileName, host, paste) {
|
|
|
1666
2306
|
}
|
|
1667
2307
|
const profile = await ProfileManager.getProfile(profileName);
|
|
1668
2308
|
const delegationData = await startAuthFlow(profile.did, {
|
|
1669
|
-
paste,
|
|
2309
|
+
paste: options.paste,
|
|
1670
2310
|
jwk: key,
|
|
1671
2311
|
host,
|
|
1672
2312
|
openkeyHost: resolveOpenKeyHost(profile)
|
|
@@ -1674,26 +2314,23 @@ async function handleOpenKeyAuth(profileName, host, paste) {
|
|
|
1674
2314
|
await ProfileManager.setSession(profileName, delegationData);
|
|
1675
2315
|
const updatedProfile = {
|
|
1676
2316
|
...profile,
|
|
2317
|
+
sessionDid: profile.sessionDid ?? profile.did,
|
|
2318
|
+
posture: profile.posture ?? "owner-openkey",
|
|
2319
|
+
operatorType: profile.operatorType ?? "human",
|
|
1677
2320
|
authMethod: "openkey"
|
|
1678
2321
|
};
|
|
1679
2322
|
if (delegationData.spaceId) {
|
|
1680
2323
|
updatedProfile.spaceId = delegationData.spaceId;
|
|
1681
|
-
updatedProfile.
|
|
2324
|
+
updatedProfile.ownerDid = delegationData.ownerDid;
|
|
1682
2325
|
}
|
|
1683
2326
|
await ProfileManager.setProfile(profileName, updatedProfile);
|
|
1684
|
-
|
|
1685
|
-
authenticated: true,
|
|
1686
|
-
profile: profileName,
|
|
1687
|
-
did: profile.did,
|
|
1688
|
-
spaceId: delegationData.spaceId,
|
|
1689
|
-
authMethod: "openkey"
|
|
1690
|
-
});
|
|
2327
|
+
return { profile: updatedProfile, delegationData };
|
|
1691
2328
|
}
|
|
1692
2329
|
|
|
1693
2330
|
// src/commands/kv.ts
|
|
1694
|
-
import { readFile as
|
|
1695
|
-
import { writeFile as
|
|
1696
|
-
async function
|
|
2331
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
2332
|
+
import { writeFile as writeFile3 } from "fs/promises";
|
|
2333
|
+
async function readStdin2() {
|
|
1697
2334
|
const chunks = [];
|
|
1698
2335
|
for await (const chunk of process.stdin) {
|
|
1699
2336
|
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
@@ -1718,7 +2355,7 @@ function registerKvCommand(program2) {
|
|
|
1718
2355
|
const metadata = result.data.headers ?? {};
|
|
1719
2356
|
if (options.output) {
|
|
1720
2357
|
const content = typeof data === "string" ? data : JSON.stringify(data);
|
|
1721
|
-
await
|
|
2358
|
+
await writeFile3(options.output, content);
|
|
1722
2359
|
outputJson({ key, written: options.output });
|
|
1723
2360
|
return;
|
|
1724
2361
|
}
|
|
@@ -1755,9 +2392,9 @@ function registerKvCommand(program2) {
|
|
|
1755
2392
|
throw new CLIError("USAGE_ERROR", "Provide only one of: value argument, --file, or --stdin", ExitCode.USAGE_ERROR);
|
|
1756
2393
|
}
|
|
1757
2394
|
if (options.file) {
|
|
1758
|
-
putValue = await
|
|
2395
|
+
putValue = await readFile4(options.file);
|
|
1759
2396
|
} else if (options.stdin) {
|
|
1760
|
-
putValue = await
|
|
2397
|
+
putValue = await readStdin2();
|
|
1761
2398
|
} else {
|
|
1762
2399
|
try {
|
|
1763
2400
|
putValue = JSON.parse(value);
|
|
@@ -1952,6 +2589,15 @@ function parseExpiry(input) {
|
|
|
1952
2589
|
}
|
|
1953
2590
|
|
|
1954
2591
|
// src/commands/delegation.ts
|
|
2592
|
+
import { principalDidEquals } from "@tinycloud/node-sdk";
|
|
2593
|
+
function didMatches(actual, expected) {
|
|
2594
|
+
if (!actual) return false;
|
|
2595
|
+
try {
|
|
2596
|
+
return principalDidEquals(actual, expected);
|
|
2597
|
+
} catch {
|
|
2598
|
+
return actual === expected;
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
1955
2601
|
function registerDelegationCommand(program2) {
|
|
1956
2602
|
const delegation = program2.command("delegation").description("Manage delegations");
|
|
1957
2603
|
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) => {
|
|
@@ -1996,10 +2642,10 @@ function registerDelegationCommand(program2) {
|
|
|
1996
2642
|
let delegations = result.data;
|
|
1997
2643
|
if (options.granted) {
|
|
1998
2644
|
const myDid = node.did;
|
|
1999
|
-
delegations = delegations.filter((d) => d.delegatorDID
|
|
2645
|
+
delegations = delegations.filter((d) => didMatches(d.delegatorDID, myDid));
|
|
2000
2646
|
} else if (options.received) {
|
|
2001
2647
|
const myDid = node.did;
|
|
2002
|
-
delegations = delegations.filter((d) => d.delegateDID
|
|
2648
|
+
delegations = delegations.filter((d) => didMatches(d.delegateDID, myDid));
|
|
2003
2649
|
}
|
|
2004
2650
|
outputJson({
|
|
2005
2651
|
delegations: delegations.map((d) => ({
|
|
@@ -2225,10 +2871,19 @@ function registerProfileCommand(program2) {
|
|
|
2225
2871
|
name: p.name,
|
|
2226
2872
|
host: p.host,
|
|
2227
2873
|
did: p.did,
|
|
2874
|
+
posture: resolveProfilePosture(p),
|
|
2875
|
+
operatorType: resolveProfileOperatorType(p),
|
|
2228
2876
|
active: name === config.defaultProfile
|
|
2229
2877
|
};
|
|
2230
2878
|
} catch {
|
|
2231
|
-
return {
|
|
2879
|
+
return {
|
|
2880
|
+
name,
|
|
2881
|
+
host: null,
|
|
2882
|
+
did: null,
|
|
2883
|
+
posture: null,
|
|
2884
|
+
operatorType: null,
|
|
2885
|
+
active: name === config.defaultProfile
|
|
2886
|
+
};
|
|
2232
2887
|
}
|
|
2233
2888
|
})
|
|
2234
2889
|
);
|
|
@@ -2242,7 +2897,8 @@ function registerProfileCommand(program2) {
|
|
|
2242
2897
|
const marker = p.active ? theme.success("\u25CF ") : " ";
|
|
2243
2898
|
const name = p.active ? theme.brand(p.name) : p.name;
|
|
2244
2899
|
const host = theme.muted(p.host || "no host");
|
|
2245
|
-
|
|
2900
|
+
const posture = p.posture ? theme.muted(String(p.posture)) : theme.muted("no posture");
|
|
2901
|
+
process.stdout.write(`${marker}${name} ${host} ${posture}
|
|
2246
2902
|
`);
|
|
2247
2903
|
}
|
|
2248
2904
|
}
|
|
@@ -2250,10 +2906,18 @@ function registerProfileCommand(program2) {
|
|
|
2250
2906
|
handleError(error);
|
|
2251
2907
|
}
|
|
2252
2908
|
});
|
|
2253
|
-
profile.command("create <name>").description("Create a new profile").option("--host <url>", "TinyCloud node URL").
|
|
2909
|
+
profile.command("create <name>").description("Create a new profile").option("--host <url>", "TinyCloud node URL").option(
|
|
2910
|
+
"--posture <posture>",
|
|
2911
|
+
`Profile posture: ${CLI_PROFILE_POSTURES.join(", ")}. Defaults to owner-openkey.`
|
|
2912
|
+
).option(
|
|
2913
|
+
"--operator <type>",
|
|
2914
|
+
`Operator type: ${CLI_OPERATOR_TYPES.join(", ")}. Defaults to human.`
|
|
2915
|
+
).action(async (name, options, cmd) => {
|
|
2254
2916
|
try {
|
|
2255
2917
|
const globalOpts = cmd.optsWithGlobals();
|
|
2256
2918
|
const host = options.host ?? globalOpts.host ?? "https://node.tinycloud.xyz";
|
|
2919
|
+
const posture = parseProfilePosture(options.posture);
|
|
2920
|
+
const operatorType = parseOperatorType(options.operator);
|
|
2257
2921
|
if (await ProfileManager.profileExists(name)) {
|
|
2258
2922
|
throw new CLIError("PROFILE_EXISTS", `Profile "${name}" already exists`, ExitCode.ERROR);
|
|
2259
2923
|
}
|
|
@@ -2266,9 +2930,12 @@ function registerProfileCommand(program2) {
|
|
|
2266
2930
|
chainId: 1,
|
|
2267
2931
|
spaceName: "default",
|
|
2268
2932
|
did,
|
|
2269
|
-
|
|
2933
|
+
sessionDid: did,
|
|
2934
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2935
|
+
posture,
|
|
2936
|
+
operatorType
|
|
2270
2937
|
});
|
|
2271
|
-
outputJson({ profile: name, did, host, created: true });
|
|
2938
|
+
outputJson({ profile: name, did, host, posture, operatorType, created: true });
|
|
2272
2939
|
} catch (error) {
|
|
2273
2940
|
handleError(error);
|
|
2274
2941
|
}
|
|
@@ -2283,9 +2950,13 @@ function registerProfileCommand(program2) {
|
|
|
2283
2950
|
const hasSession = await ProfileManager.getSession(profileName) !== null;
|
|
2284
2951
|
const config = await ProfileManager.getConfig();
|
|
2285
2952
|
const isDefault = profileName === config.defaultProfile;
|
|
2953
|
+
const posture = resolveProfilePosture(p);
|
|
2954
|
+
const operatorType = resolveProfileOperatorType(p);
|
|
2286
2955
|
if (shouldOutputJson()) {
|
|
2287
2956
|
outputJson({
|
|
2288
2957
|
...p,
|
|
2958
|
+
posture,
|
|
2959
|
+
operatorType,
|
|
2289
2960
|
hasKey,
|
|
2290
2961
|
hasSession,
|
|
2291
2962
|
isDefault
|
|
@@ -2295,6 +2966,9 @@ function registerProfileCommand(program2) {
|
|
|
2295
2966
|
`);
|
|
2296
2967
|
process.stdout.write(formatField("Host", p.host) + "\n");
|
|
2297
2968
|
process.stdout.write(formatField("DID", p.did) + "\n");
|
|
2969
|
+
process.stdout.write(formatField("Session DID", p.sessionDid ?? null) + "\n");
|
|
2970
|
+
process.stdout.write(formatField("Posture", posture) + "\n");
|
|
2971
|
+
process.stdout.write(formatField("Operator", operatorType) + "\n");
|
|
2298
2972
|
process.stdout.write(formatField("Space", p.spaceId || null) + "\n");
|
|
2299
2973
|
process.stdout.write(formatField("Key", hasKey) + "\n");
|
|
2300
2974
|
process.stdout.write(formatField("Session", hasSession) + "\n");
|
|
@@ -2336,6 +3010,24 @@ function registerProfileCommand(program2) {
|
|
|
2336
3010
|
}
|
|
2337
3011
|
});
|
|
2338
3012
|
}
|
|
3013
|
+
function parseProfilePosture(raw) {
|
|
3014
|
+
if (raw === void 0 || raw === null || raw === "") return "owner-openkey";
|
|
3015
|
+
if (isCLIProfilePosture(raw)) return raw;
|
|
3016
|
+
throw new CLIError(
|
|
3017
|
+
"INVALID_POSTURE",
|
|
3018
|
+
`Invalid posture "${String(raw)}". Use one of: ${CLI_PROFILE_POSTURES.join(", ")}.`,
|
|
3019
|
+
ExitCode.USAGE_ERROR
|
|
3020
|
+
);
|
|
3021
|
+
}
|
|
3022
|
+
function parseOperatorType(raw) {
|
|
3023
|
+
if (raw === void 0 || raw === null || raw === "") return "human";
|
|
3024
|
+
if (isCLIOperatorType(raw)) return raw;
|
|
3025
|
+
throw new CLIError(
|
|
3026
|
+
"INVALID_OPERATOR",
|
|
3027
|
+
`Invalid operator "${String(raw)}". Use one of: ${CLI_OPERATOR_TYPES.join(", ")}.`,
|
|
3028
|
+
ExitCode.USAGE_ERROR
|
|
3029
|
+
);
|
|
3030
|
+
}
|
|
2339
3031
|
|
|
2340
3032
|
// src/commands/completion.ts
|
|
2341
3033
|
function registerCompletionCommand(program2) {
|
|
@@ -2364,7 +3056,7 @@ _tc_completions() {
|
|
|
2364
3056
|
commands="init auth kv space delegation share node profile completion"
|
|
2365
3057
|
|
|
2366
3058
|
case "\${COMP_WORDS[1]}" in
|
|
2367
|
-
auth) subcommands="login logout status whoami" ;;
|
|
3059
|
+
auth) subcommands="login logout rotate status whoami" ;;
|
|
2368
3060
|
kv) subcommands="get put delete list head" ;;
|
|
2369
3061
|
space) subcommands="list create info switch" ;;
|
|
2370
3062
|
delegation) subcommands="create list info revoke" ;;
|
|
@@ -2414,7 +3106,7 @@ _tc() {
|
|
|
2414
3106
|
;;
|
|
2415
3107
|
args)
|
|
2416
3108
|
case $words[1] in
|
|
2417
|
-
auth) _values 'subcommand' login logout status whoami ;;
|
|
3109
|
+
auth) _values 'subcommand' login logout rotate status whoami ;;
|
|
2418
3110
|
kv) _values 'subcommand' get put delete list head ;;
|
|
2419
3111
|
space) _values 'subcommand' list create info switch ;;
|
|
2420
3112
|
delegation) _values 'subcommand' create list info revoke ;;
|
|
@@ -2449,7 +3141,7 @@ complete -c tc -n "not __fish_seen_subcommand_from $commands" -a profile -d "Pro
|
|
|
2449
3141
|
complete -c tc -n "not __fish_seen_subcommand_from $commands" -a completion -d "Generate shell completions"
|
|
2450
3142
|
|
|
2451
3143
|
# Subcommands
|
|
2452
|
-
complete -c tc -n "__fish_seen_subcommand_from auth" -a "login logout status whoami"
|
|
3144
|
+
complete -c tc -n "__fish_seen_subcommand_from auth" -a "login logout rotate status whoami"
|
|
2453
3145
|
complete -c tc -n "__fish_seen_subcommand_from kv" -a "get put delete list head"
|
|
2454
3146
|
complete -c tc -n "__fish_seen_subcommand_from space" -a "list create info switch"
|
|
2455
3147
|
complete -c tc -n "__fish_seen_subcommand_from delegation" -a "create list info revoke"
|
|
@@ -2468,10 +3160,10 @@ complete -c tc -l quiet -s q -d "Suppress non-essential output"
|
|
|
2468
3160
|
}
|
|
2469
3161
|
|
|
2470
3162
|
// src/commands/vault.ts
|
|
2471
|
-
import { readFile as
|
|
2472
|
-
import { writeFile as
|
|
3163
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
3164
|
+
import { writeFile as writeFile4 } from "fs/promises";
|
|
2473
3165
|
import { PrivateKeySigner as PrivateKeySigner2 } from "@tinycloud/node-sdk";
|
|
2474
|
-
async function
|
|
3166
|
+
async function readStdin3() {
|
|
2475
3167
|
const chunks = [];
|
|
2476
3168
|
for await (const chunk of process.stdin) {
|
|
2477
3169
|
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
@@ -2526,9 +3218,9 @@ function registerVaultCommand(program2) {
|
|
|
2526
3218
|
throw new CLIError("USAGE_ERROR", "Provide only one of: value argument, --file, or --stdin", ExitCode.USAGE_ERROR);
|
|
2527
3219
|
}
|
|
2528
3220
|
if (options.file) {
|
|
2529
|
-
putValue = new Uint8Array(await
|
|
3221
|
+
putValue = new Uint8Array(await readFile5(options.file));
|
|
2530
3222
|
} else if (options.stdin) {
|
|
2531
|
-
putValue = new Uint8Array(await
|
|
3223
|
+
putValue = new Uint8Array(await readStdin3());
|
|
2532
3224
|
} else {
|
|
2533
3225
|
putValue = value;
|
|
2534
3226
|
}
|
|
@@ -2558,7 +3250,7 @@ function registerVaultCommand(program2) {
|
|
|
2558
3250
|
const data = result.data.data ?? result.data;
|
|
2559
3251
|
if (options.output) {
|
|
2560
3252
|
const content = data instanceof Uint8Array ? Buffer.from(data) : typeof data === "string" ? data : JSON.stringify(data);
|
|
2561
|
-
await
|
|
3253
|
+
await writeFile4(options.output, content);
|
|
2562
3254
|
outputJson({ key, written: options.output });
|
|
2563
3255
|
return;
|
|
2564
3256
|
}
|
|
@@ -2641,9 +3333,20 @@ function registerVaultCommand(program2) {
|
|
|
2641
3333
|
}
|
|
2642
3334
|
|
|
2643
3335
|
// src/commands/secrets.ts
|
|
2644
|
-
import { readFile as
|
|
2645
|
-
import { writeFile as
|
|
2646
|
-
|
|
3336
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
3337
|
+
import { writeFile as writeFile5 } from "fs/promises";
|
|
3338
|
+
import {
|
|
3339
|
+
resolveSecretListPrefix,
|
|
3340
|
+
resolveSecretPath
|
|
3341
|
+
} from "@tinycloud/node-sdk";
|
|
3342
|
+
var SECRETS_SPACE = "secrets";
|
|
3343
|
+
var SECRET_KV_ABILITIES = {
|
|
3344
|
+
get: "tinycloud.kv/get",
|
|
3345
|
+
put: "tinycloud.kv/put",
|
|
3346
|
+
del: "tinycloud.kv/del",
|
|
3347
|
+
list: "tinycloud.kv/list"
|
|
3348
|
+
};
|
|
3349
|
+
async function readStdin4() {
|
|
2647
3350
|
const chunks = [];
|
|
2648
3351
|
for await (const chunk of process.stdin) {
|
|
2649
3352
|
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
@@ -2658,6 +3361,123 @@ function resolveSecretScope(options) {
|
|
|
2658
3361
|
const scope = options.scope ?? options.space;
|
|
2659
3362
|
return scope ? { scope } : void 0;
|
|
2660
3363
|
}
|
|
3364
|
+
async function ensureSecretsNode(ctx, options) {
|
|
3365
|
+
const auth = authOptions(options);
|
|
3366
|
+
if (auth?.privateKey) {
|
|
3367
|
+
return ensureAuthenticated(ctx, auth);
|
|
3368
|
+
}
|
|
3369
|
+
const profile = await ProfileManager.getProfile(ctx.profile).catch(() => null);
|
|
3370
|
+
if (profile?.authMethod === "openkey" && canRequestOwnerPermissions(profile)) {
|
|
3371
|
+
const session = await ProfileManager.getSession(ctx.profile);
|
|
3372
|
+
if (!session || isStoredSessionExpired(session)) {
|
|
3373
|
+
await withSpinner(
|
|
3374
|
+
session ? "Refreshing TinyCloud session..." : "Creating TinyCloud session...",
|
|
3375
|
+
() => refreshOpenKeySession(ctx.profile, ctx.host)
|
|
3376
|
+
);
|
|
3377
|
+
}
|
|
3378
|
+
}
|
|
3379
|
+
return ensureAuthenticated(ctx, auth);
|
|
3380
|
+
}
|
|
3381
|
+
async function runSecretOperation(params) {
|
|
3382
|
+
const first = await runSecretOperationAttempt(params.label, params.operation);
|
|
3383
|
+
if (first.ok || !shouldRequestSecretPermissions(first.error)) {
|
|
3384
|
+
return first;
|
|
3385
|
+
}
|
|
3386
|
+
const profile = await ProfileManager.getProfile(params.ctx.profile);
|
|
3387
|
+
if (!canRequestOwnerPermissions(profile)) {
|
|
3388
|
+
return first;
|
|
3389
|
+
}
|
|
3390
|
+
const requested = secretPermissionEntries({
|
|
3391
|
+
action: params.action,
|
|
3392
|
+
name: params.name,
|
|
3393
|
+
options: params.scopeOptions,
|
|
3394
|
+
node: params.node
|
|
3395
|
+
});
|
|
3396
|
+
await withSpinner(
|
|
3397
|
+
"Requesting secret permissions...",
|
|
3398
|
+
() => ensureDelegationAuthority({
|
|
3399
|
+
ctx: params.ctx,
|
|
3400
|
+
profile,
|
|
3401
|
+
node: params.node,
|
|
3402
|
+
requested,
|
|
3403
|
+
expiryOption: void 0,
|
|
3404
|
+
yes: true,
|
|
3405
|
+
force: true
|
|
3406
|
+
})
|
|
3407
|
+
);
|
|
3408
|
+
return runSecretOperationAttempt(params.label, params.operation);
|
|
3409
|
+
}
|
|
3410
|
+
async function runSecretOperationAttempt(label, operation) {
|
|
3411
|
+
try {
|
|
3412
|
+
return await withSpinner(label, operation);
|
|
3413
|
+
} catch (error) {
|
|
3414
|
+
const permissionError = thrownPermissionError(error);
|
|
3415
|
+
if (permissionError) return permissionError;
|
|
3416
|
+
throw error;
|
|
3417
|
+
}
|
|
3418
|
+
}
|
|
3419
|
+
function canRequestOwnerPermissions(profile) {
|
|
3420
|
+
const posture = resolveProfilePosture(profile);
|
|
3421
|
+
return posture === "owner-openkey" || posture === "local-owner-key";
|
|
3422
|
+
}
|
|
3423
|
+
function shouldRequestSecretPermissions(error) {
|
|
3424
|
+
if (error.code !== "PERMISSION_DENIED") return false;
|
|
3425
|
+
return /permission|session expired|autosign|capabilit/i.test(error.message);
|
|
3426
|
+
}
|
|
3427
|
+
function thrownPermissionError(error) {
|
|
3428
|
+
const record = error;
|
|
3429
|
+
const message = typeof record?.message === "string" ? record.message : String(error);
|
|
3430
|
+
const code = typeof record?.code === "string" ? record.code : "PERMISSION_DENIED";
|
|
3431
|
+
if (code !== "PERMISSION_DENIED" && !/permission|session expired|autosign|capabilit/i.test(message)) {
|
|
3432
|
+
return null;
|
|
3433
|
+
}
|
|
3434
|
+
return {
|
|
3435
|
+
ok: false,
|
|
3436
|
+
error: {
|
|
3437
|
+
code: "PERMISSION_DENIED",
|
|
3438
|
+
message
|
|
3439
|
+
}
|
|
3440
|
+
};
|
|
3441
|
+
}
|
|
3442
|
+
function isStoredSessionExpired(session) {
|
|
3443
|
+
const record = session;
|
|
3444
|
+
const direct = parseDate(record.expiresAt ?? record.expiry ?? record.expirationTime);
|
|
3445
|
+
if (direct) return direct.getTime() <= Date.now();
|
|
3446
|
+
if (typeof record.siwe !== "string") return false;
|
|
3447
|
+
const match = record.siwe.match(/^Expiration Time:\s*(.+)$/im);
|
|
3448
|
+
const expiry = match ? parseDate(match[1].trim()) : null;
|
|
3449
|
+
return expiry !== null && expiry.getTime() <= Date.now();
|
|
3450
|
+
}
|
|
3451
|
+
function parseDate(value) {
|
|
3452
|
+
if (value instanceof Date) {
|
|
3453
|
+
return Number.isNaN(value.getTime()) ? null : value;
|
|
3454
|
+
}
|
|
3455
|
+
if (typeof value !== "string" || value.trim() === "") return null;
|
|
3456
|
+
const date = new Date(value);
|
|
3457
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
3458
|
+
}
|
|
3459
|
+
function secretKvAbility(action) {
|
|
3460
|
+
return SECRET_KV_ABILITIES[action];
|
|
3461
|
+
}
|
|
3462
|
+
function secretPermissionEntries(params) {
|
|
3463
|
+
const path = params.action === "list" ? resolveSecretListPrefix(params.options) : resolveSecretPath(params.name ?? "", params.options).permissionPaths.vault;
|
|
3464
|
+
const permissions = [{
|
|
3465
|
+
service: "tinycloud.kv",
|
|
3466
|
+
space: SECRETS_SPACE,
|
|
3467
|
+
path,
|
|
3468
|
+
actions: [secretKvAbility(params.action)],
|
|
3469
|
+
skipPrefix: true
|
|
3470
|
+
}];
|
|
3471
|
+
if (params.action === "get") {
|
|
3472
|
+
permissions.push({
|
|
3473
|
+
service: "tinycloud.encryption",
|
|
3474
|
+
path: params.node.getDefaultEncryptionNetworkId(),
|
|
3475
|
+
actions: ["tinycloud.encryption/decrypt"],
|
|
3476
|
+
skipPrefix: true
|
|
3477
|
+
});
|
|
3478
|
+
}
|
|
3479
|
+
return permissions;
|
|
3480
|
+
}
|
|
2661
3481
|
function registerSecretsCommand(program2) {
|
|
2662
3482
|
const secrets = program2.command("secrets").description("Encrypted secrets management");
|
|
2663
3483
|
const network = secrets.command("network").description("Manage the default secrets encryption network");
|
|
@@ -2703,12 +3523,16 @@ function registerSecretsCommand(program2) {
|
|
|
2703
3523
|
try {
|
|
2704
3524
|
const globalOpts = cmd.optsWithGlobals();
|
|
2705
3525
|
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
2706
|
-
const node = await
|
|
3526
|
+
const node = await ensureSecretsNode(ctx, options);
|
|
2707
3527
|
const scopeOptions = resolveSecretScope(options);
|
|
2708
|
-
const result = await
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
3528
|
+
const result = await runSecretOperation({
|
|
3529
|
+
ctx,
|
|
3530
|
+
node,
|
|
3531
|
+
action: "list",
|
|
3532
|
+
scopeOptions,
|
|
3533
|
+
label: "Listing secrets...",
|
|
3534
|
+
operation: () => node.secrets.list(scopeOptions)
|
|
3535
|
+
});
|
|
2712
3536
|
if (!result.ok) {
|
|
2713
3537
|
throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
|
|
2714
3538
|
}
|
|
@@ -2727,12 +3551,17 @@ function registerSecretsCommand(program2) {
|
|
|
2727
3551
|
try {
|
|
2728
3552
|
const globalOpts = cmd.optsWithGlobals();
|
|
2729
3553
|
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
2730
|
-
const node = await
|
|
3554
|
+
const node = await ensureSecretsNode(ctx, options);
|
|
2731
3555
|
const scopeOptions = resolveSecretScope(options);
|
|
2732
|
-
const result = await
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
3556
|
+
const result = await runSecretOperation({
|
|
3557
|
+
ctx,
|
|
3558
|
+
node,
|
|
3559
|
+
action: "get",
|
|
3560
|
+
name,
|
|
3561
|
+
scopeOptions,
|
|
3562
|
+
label: `Getting secret ${name}...`,
|
|
3563
|
+
operation: () => node.secrets.get(name, scopeOptions)
|
|
3564
|
+
});
|
|
2736
3565
|
if (!result.ok) {
|
|
2737
3566
|
if (result.error.code === "NOT_FOUND" || result.error.code === "KEY_NOT_FOUND") {
|
|
2738
3567
|
throw new CLIError("NOT_FOUND", `Secret "${name}" not found`, ExitCode.NOT_FOUND);
|
|
@@ -2741,7 +3570,7 @@ function registerSecretsCommand(program2) {
|
|
|
2741
3570
|
}
|
|
2742
3571
|
const value = String(result.data);
|
|
2743
3572
|
if (options.output) {
|
|
2744
|
-
await
|
|
3573
|
+
await writeFile5(options.output, value);
|
|
2745
3574
|
outputJson({ name, written: options.output });
|
|
2746
3575
|
return;
|
|
2747
3576
|
}
|
|
@@ -2758,7 +3587,7 @@ function registerSecretsCommand(program2) {
|
|
|
2758
3587
|
try {
|
|
2759
3588
|
const globalOpts = cmd.optsWithGlobals();
|
|
2760
3589
|
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
2761
|
-
const node = await
|
|
3590
|
+
const node = await ensureSecretsNode(ctx, options);
|
|
2762
3591
|
let secretValue;
|
|
2763
3592
|
const sources = [value !== void 0, !!options.file, !!options.stdin].filter(Boolean);
|
|
2764
3593
|
if (sources.length === 0) {
|
|
@@ -2768,17 +3597,22 @@ function registerSecretsCommand(program2) {
|
|
|
2768
3597
|
throw new CLIError("USAGE_ERROR", "Provide only one of: value argument, --file, or --stdin", ExitCode.USAGE_ERROR);
|
|
2769
3598
|
}
|
|
2770
3599
|
if (options.file) {
|
|
2771
|
-
secretValue = await
|
|
3600
|
+
secretValue = await readFile6(options.file, "utf-8");
|
|
2772
3601
|
} else if (options.stdin) {
|
|
2773
|
-
secretValue = (await
|
|
3602
|
+
secretValue = (await readStdin4()).toString("utf-8");
|
|
2774
3603
|
} else {
|
|
2775
3604
|
secretValue = value;
|
|
2776
3605
|
}
|
|
2777
3606
|
const scopeOptions = resolveSecretScope(options);
|
|
2778
|
-
const result = await
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
3607
|
+
const result = await runSecretOperation({
|
|
3608
|
+
ctx,
|
|
3609
|
+
node,
|
|
3610
|
+
action: "put",
|
|
3611
|
+
name,
|
|
3612
|
+
scopeOptions,
|
|
3613
|
+
label: `Storing secret ${name}...`,
|
|
3614
|
+
operation: () => node.secrets.put(name, secretValue, scopeOptions)
|
|
3615
|
+
});
|
|
2782
3616
|
if (!result.ok) {
|
|
2783
3617
|
throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
|
|
2784
3618
|
}
|
|
@@ -2791,12 +3625,17 @@ function registerSecretsCommand(program2) {
|
|
|
2791
3625
|
try {
|
|
2792
3626
|
const globalOpts = cmd.optsWithGlobals();
|
|
2793
3627
|
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
2794
|
-
const node = await
|
|
3628
|
+
const node = await ensureSecretsNode(ctx, options);
|
|
2795
3629
|
const scopeOptions = resolveSecretScope(options);
|
|
2796
|
-
const result = await
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
3630
|
+
const result = await runSecretOperation({
|
|
3631
|
+
ctx,
|
|
3632
|
+
node,
|
|
3633
|
+
action: "del",
|
|
3634
|
+
name,
|
|
3635
|
+
scopeOptions,
|
|
3636
|
+
label: `Deleting secret ${name}...`,
|
|
3637
|
+
operation: () => node.secrets.delete(name, scopeOptions)
|
|
3638
|
+
});
|
|
2800
3639
|
if (!result.ok) {
|
|
2801
3640
|
throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
|
|
2802
3641
|
}
|
|
@@ -2848,10 +3687,10 @@ function registerSecretsCommand(program2) {
|
|
|
2848
3687
|
}
|
|
2849
3688
|
|
|
2850
3689
|
// src/commands/vars.ts
|
|
2851
|
-
import { readFile as
|
|
2852
|
-
import { writeFile as
|
|
3690
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
3691
|
+
import { writeFile as writeFile6 } from "fs/promises";
|
|
2853
3692
|
var VARIABLES_PREFIX = "variables/";
|
|
2854
|
-
async function
|
|
3693
|
+
async function readStdin5() {
|
|
2855
3694
|
const chunks = [];
|
|
2856
3695
|
for await (const chunk of process.stdin) {
|
|
2857
3696
|
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
@@ -2921,7 +3760,7 @@ function registerVarsCommand(program2) {
|
|
|
2921
3760
|
value = typeof data === "string" ? data : JSON.stringify(data);
|
|
2922
3761
|
}
|
|
2923
3762
|
if (options.output) {
|
|
2924
|
-
await
|
|
3763
|
+
await writeFile6(options.output, value);
|
|
2925
3764
|
outputJson({ name, written: options.output });
|
|
2926
3765
|
return;
|
|
2927
3766
|
}
|
|
@@ -2949,9 +3788,9 @@ function registerVarsCommand(program2) {
|
|
|
2949
3788
|
throw new CLIError("USAGE_ERROR", "Provide only one of: value argument, --file, or --stdin", ExitCode.USAGE_ERROR);
|
|
2950
3789
|
}
|
|
2951
3790
|
if (options.file) {
|
|
2952
|
-
varValue = await
|
|
3791
|
+
varValue = await readFile7(options.file, "utf-8");
|
|
2953
3792
|
} else if (options.stdin) {
|
|
2954
|
-
varValue = (await
|
|
3793
|
+
varValue = (await readStdin5()).toString("utf-8");
|
|
2955
3794
|
} else {
|
|
2956
3795
|
varValue = value;
|
|
2957
3796
|
}
|
|
@@ -3095,7 +3934,7 @@ function registerDoctorCommand(program2) {
|
|
|
3095
3934
|
}
|
|
3096
3935
|
|
|
3097
3936
|
// src/commands/sql.ts
|
|
3098
|
-
import { writeFile as
|
|
3937
|
+
import { writeFile as writeFile7 } from "fs/promises";
|
|
3099
3938
|
import { resolve } from "path";
|
|
3100
3939
|
async function dbHandle(node, dbName, spaceInput, profileName) {
|
|
3101
3940
|
const spaceUri = await resolveSpaceUri(spaceInput, profileName);
|
|
@@ -3231,7 +4070,7 @@ Output:
|
|
|
3231
4070
|
const blob = result.data;
|
|
3232
4071
|
const buffer = Buffer.from(await blob.arrayBuffer());
|
|
3233
4072
|
const outputPath = resolve(options.output);
|
|
3234
|
-
await
|
|
4073
|
+
await writeFile7(outputPath, buffer);
|
|
3235
4074
|
outputJson({
|
|
3236
4075
|
file: outputPath,
|
|
3237
4076
|
size: blob.size,
|
|
@@ -3360,7 +4199,7 @@ function quoteIdent(name) {
|
|
|
3360
4199
|
}
|
|
3361
4200
|
|
|
3362
4201
|
// src/commands/duckdb.ts
|
|
3363
|
-
import { readFile as
|
|
4202
|
+
import { readFile as readFile8, writeFile as writeFile8 } from "fs/promises";
|
|
3364
4203
|
import { resolve as resolve2 } from "path";
|
|
3365
4204
|
function registerDuckdbCommand(program2) {
|
|
3366
4205
|
const duckdb = program2.command("duckdb").description("DuckDB database operations");
|
|
@@ -3474,7 +4313,7 @@ ${rowCount} row${rowCount === 1 ? "" : "s"} returned`) + "\n");
|
|
|
3474
4313
|
const blob = result.data;
|
|
3475
4314
|
const buffer = Buffer.from(await blob.arrayBuffer());
|
|
3476
4315
|
const outputPath = resolve2(options.output);
|
|
3477
|
-
await
|
|
4316
|
+
await writeFile8(outputPath, buffer);
|
|
3478
4317
|
outputJson({
|
|
3479
4318
|
file: outputPath,
|
|
3480
4319
|
size: blob.size,
|
|
@@ -3490,7 +4329,7 @@ ${rowCount} row${rowCount === 1 ? "" : "s"} returned`) + "\n");
|
|
|
3490
4329
|
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
3491
4330
|
const node = await ensureAuthenticated(ctx);
|
|
3492
4331
|
const filePath = resolve2(file);
|
|
3493
|
-
const bytes = new Uint8Array(await
|
|
4332
|
+
const bytes = new Uint8Array(await readFile8(filePath));
|
|
3494
4333
|
const result = await withSpinner(
|
|
3495
4334
|
"Importing database...",
|
|
3496
4335
|
() => node.duckdb.db(options.db).import(bytes)
|
|
@@ -3511,7 +4350,7 @@ ${rowCount} row${rowCount === 1 ? "" : "s"} returned`) + "\n");
|
|
|
3511
4350
|
}
|
|
3512
4351
|
|
|
3513
4352
|
// src/commands/manifest.ts
|
|
3514
|
-
import { readFile as
|
|
4353
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
3515
4354
|
var DEFAULT_APP_SPACE = "applications";
|
|
3516
4355
|
function registerManifestCommand(program2) {
|
|
3517
4356
|
const manifest = program2.command("manifest").description("Inspect TinyCloud app manifests");
|
|
@@ -3619,7 +4458,7 @@ async function loadManifestSource(source) {
|
|
|
3619
4458
|
}
|
|
3620
4459
|
return response.text();
|
|
3621
4460
|
}
|
|
3622
|
-
return
|
|
4461
|
+
return readFile9(source, "utf8");
|
|
3623
4462
|
}
|
|
3624
4463
|
function prefixWithAppId(path, appId) {
|
|
3625
4464
|
const slash = path.indexOf("/");
|
|
@@ -3707,6 +4546,358 @@ function registerUpgradeCommand(program2) {
|
|
|
3707
4546
|
});
|
|
3708
4547
|
}
|
|
3709
4548
|
|
|
4549
|
+
// src/commands/status.ts
|
|
4550
|
+
import {
|
|
4551
|
+
NodeWasmBindings
|
|
4552
|
+
} from "@tinycloud/node-sdk";
|
|
4553
|
+
var wasmBindings = null;
|
|
4554
|
+
function registerStatusCommand(program2) {
|
|
4555
|
+
program2.command("status").description("Show local TinyCloud profile, session, delegation, and permission state").action(async (_options, cmd) => {
|
|
4556
|
+
try {
|
|
4557
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
4558
|
+
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
4559
|
+
const config = await ProfileManager.getConfig();
|
|
4560
|
+
const names = (await ProfileManager.listProfiles()).sort(
|
|
4561
|
+
(a, b) => a.localeCompare(b)
|
|
4562
|
+
);
|
|
4563
|
+
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4564
|
+
const profiles = await Promise.all(
|
|
4565
|
+
names.map(
|
|
4566
|
+
(name) => inspectProfile({
|
|
4567
|
+
name,
|
|
4568
|
+
activeProfile: ctx.profile,
|
|
4569
|
+
defaultProfile: config.defaultProfile
|
|
4570
|
+
})
|
|
4571
|
+
)
|
|
4572
|
+
);
|
|
4573
|
+
const summary = {
|
|
4574
|
+
generatedAt,
|
|
4575
|
+
activeProfile: ctx.profile,
|
|
4576
|
+
defaultProfile: config.defaultProfile,
|
|
4577
|
+
profileCount: profiles.length,
|
|
4578
|
+
authenticatedProfileCount: profiles.filter((p) => p.authenticated).length,
|
|
4579
|
+
activeDelegationCount: profiles.reduce(
|
|
4580
|
+
(sum, profile) => sum + profile.activeDelegationCount,
|
|
4581
|
+
0
|
|
4582
|
+
),
|
|
4583
|
+
profiles
|
|
4584
|
+
};
|
|
4585
|
+
if (shouldOutputJson()) {
|
|
4586
|
+
outputJson(summary);
|
|
4587
|
+
return;
|
|
4588
|
+
}
|
|
4589
|
+
process.stdout.write(formatStatus(summary));
|
|
4590
|
+
} catch (error) {
|
|
4591
|
+
handleError(error);
|
|
4592
|
+
}
|
|
4593
|
+
});
|
|
4594
|
+
}
|
|
4595
|
+
async function inspectProfile(params) {
|
|
4596
|
+
const issues = [];
|
|
4597
|
+
const profile = await readProfile(params.name, issues);
|
|
4598
|
+
const session = await readSession(params.name, issues);
|
|
4599
|
+
const hasKey = await readHasKey(params.name, issues);
|
|
4600
|
+
const storedDelegations = await readDelegations(params.name, issues);
|
|
4601
|
+
const sessionPermissions = session ? sessionPermissionsFromRecap(session) : [];
|
|
4602
|
+
const sessionExpiry = session ? extractSessionExpiry(session) : null;
|
|
4603
|
+
const sessionExpired = sessionExpiry === null ? null : sessionExpiry.getTime() <= Date.now();
|
|
4604
|
+
const statusSession = {
|
|
4605
|
+
present: session !== null,
|
|
4606
|
+
expired: session === null ? null : sessionExpired,
|
|
4607
|
+
expiresAt: sessionExpiry?.toISOString() ?? null,
|
|
4608
|
+
permissions: sessionPermissions,
|
|
4609
|
+
permissionsCompact: compactPermissions(sessionPermissions)
|
|
4610
|
+
};
|
|
4611
|
+
const delegations = storedDelegations.map(inspectDelegation);
|
|
4612
|
+
const activeDelegationPermissions = delegations.filter((delegation) => delegation.active).flatMap((delegation) => delegation.permissions);
|
|
4613
|
+
const permissions = uniquePermissions([
|
|
4614
|
+
...sessionPermissions,
|
|
4615
|
+
...activeDelegationPermissions
|
|
4616
|
+
]);
|
|
4617
|
+
const hasPrivateKey = typeof profile?.privateKey === "string" && profile.privateKey.length > 0;
|
|
4618
|
+
const localKeyAuthenticated = profile?.authMethod === "local" && hasPrivateKey;
|
|
4619
|
+
const sessionAuthenticated = session !== null && sessionExpired !== true;
|
|
4620
|
+
const authenticated = localKeyAuthenticated || sessionAuthenticated;
|
|
4621
|
+
const status = resolveStatus({
|
|
4622
|
+
exists: profile !== null,
|
|
4623
|
+
authenticated,
|
|
4624
|
+
localKeyAuthenticated,
|
|
4625
|
+
sessionExpired
|
|
4626
|
+
});
|
|
4627
|
+
return {
|
|
4628
|
+
name: params.name,
|
|
4629
|
+
active: params.name === params.activeProfile,
|
|
4630
|
+
default: params.name === params.defaultProfile,
|
|
4631
|
+
exists: profile !== null,
|
|
4632
|
+
status,
|
|
4633
|
+
host: profile?.host ?? null,
|
|
4634
|
+
did: profile?.did ?? null,
|
|
4635
|
+
sessionDid: profile?.sessionDid ?? null,
|
|
4636
|
+
ownerDid: profile?.ownerDid ?? null,
|
|
4637
|
+
address: profile?.address ?? null,
|
|
4638
|
+
spaceId: profile?.spaceId ?? null,
|
|
4639
|
+
authMethod: profile?.authMethod ?? null,
|
|
4640
|
+
posture: profile ? resolveProfilePosture(profile) : null,
|
|
4641
|
+
operatorType: profile ? resolveProfileOperatorType(profile) : null,
|
|
4642
|
+
hasKey,
|
|
4643
|
+
hasPrivateKey,
|
|
4644
|
+
authenticated,
|
|
4645
|
+
session: statusSession,
|
|
4646
|
+
delegations,
|
|
4647
|
+
permissions,
|
|
4648
|
+
permissionsCompact: compactPermissions(permissions),
|
|
4649
|
+
permissionCount: permissions.length,
|
|
4650
|
+
activeDelegationCount: delegations.filter((delegation) => delegation.active).length,
|
|
4651
|
+
delegationCount: delegations.length,
|
|
4652
|
+
issues
|
|
4653
|
+
};
|
|
4654
|
+
}
|
|
4655
|
+
async function readProfile(name, issues) {
|
|
4656
|
+
try {
|
|
4657
|
+
return await ProfileManager.getProfile(name);
|
|
4658
|
+
} catch (error) {
|
|
4659
|
+
issues.push(`profile: ${messageFromError(error)}`);
|
|
4660
|
+
return null;
|
|
4661
|
+
}
|
|
4662
|
+
}
|
|
4663
|
+
async function readSession(name, issues) {
|
|
4664
|
+
try {
|
|
4665
|
+
return asRecord(await ProfileManager.getSession(name));
|
|
4666
|
+
} catch (error) {
|
|
4667
|
+
issues.push(`session: ${messageFromError(error)}`);
|
|
4668
|
+
return null;
|
|
4669
|
+
}
|
|
4670
|
+
}
|
|
4671
|
+
async function readHasKey(name, issues) {
|
|
4672
|
+
try {
|
|
4673
|
+
return await ProfileManager.getKey(name) !== null;
|
|
4674
|
+
} catch (error) {
|
|
4675
|
+
issues.push(`key: ${messageFromError(error)}`);
|
|
4676
|
+
return false;
|
|
4677
|
+
}
|
|
4678
|
+
}
|
|
4679
|
+
async function readDelegations(name, issues) {
|
|
4680
|
+
try {
|
|
4681
|
+
return await loadAdditionalDelegations(name);
|
|
4682
|
+
} catch (error) {
|
|
4683
|
+
issues.push(`delegations: ${messageFromError(error)}`);
|
|
4684
|
+
return [];
|
|
4685
|
+
}
|
|
4686
|
+
}
|
|
4687
|
+
function inspectDelegation(entry) {
|
|
4688
|
+
const expiry = parseDate2(entry.delegation.expiry);
|
|
4689
|
+
const expired = expiry === null ? null : expiry.getTime() <= Date.now();
|
|
4690
|
+
const permissions = normalizePermissions(
|
|
4691
|
+
Array.isArray(entry.permissions) && entry.permissions.length > 0 ? entry.permissions : permissionsFromDelegation(entry.delegation)
|
|
4692
|
+
);
|
|
4693
|
+
return {
|
|
4694
|
+
cid: entry.delegation.cid,
|
|
4695
|
+
active: expired !== true,
|
|
4696
|
+
expired,
|
|
4697
|
+
expiresAt: expiry?.toISOString() ?? null,
|
|
4698
|
+
permissions,
|
|
4699
|
+
permissionsCompact: compactPermissions(permissions)
|
|
4700
|
+
};
|
|
4701
|
+
}
|
|
4702
|
+
function resolveStatus(params) {
|
|
4703
|
+
if (!params.exists) return "missing";
|
|
4704
|
+
if (params.localKeyAuthenticated) return "local-key";
|
|
4705
|
+
if (params.authenticated) return "logged-in";
|
|
4706
|
+
if (params.sessionExpired === true) return "expired";
|
|
4707
|
+
return "signed-out";
|
|
4708
|
+
}
|
|
4709
|
+
function sessionPermissionsFromRecap(session) {
|
|
4710
|
+
if (typeof session.siwe !== "string" || session.siwe.length === 0) return [];
|
|
4711
|
+
try {
|
|
4712
|
+
const rawEntries = getWasmBindings().parseRecapFromSiwe(session.siwe);
|
|
4713
|
+
if (!Array.isArray(rawEntries)) return [];
|
|
4714
|
+
return normalizePermissions(rawEntries.map(permissionFromRawRecap));
|
|
4715
|
+
} catch {
|
|
4716
|
+
return [];
|
|
4717
|
+
}
|
|
4718
|
+
}
|
|
4719
|
+
function permissionFromRawRecap(value) {
|
|
4720
|
+
const record = asRecord(value);
|
|
4721
|
+
if (!record) return null;
|
|
4722
|
+
const service = stringValue(record.service);
|
|
4723
|
+
const space = stringValue(record.space);
|
|
4724
|
+
const path = stringValue(record.path);
|
|
4725
|
+
const actions = Array.isArray(record.actions) ? record.actions.map(String).filter(Boolean) : [];
|
|
4726
|
+
if (!service || !space || path === null || actions.length === 0) return null;
|
|
4727
|
+
return {
|
|
4728
|
+
service: normalizeService2(service),
|
|
4729
|
+
space,
|
|
4730
|
+
path,
|
|
4731
|
+
actions
|
|
4732
|
+
};
|
|
4733
|
+
}
|
|
4734
|
+
function normalizePermissions(entries) {
|
|
4735
|
+
const permissions = [];
|
|
4736
|
+
for (const entry of entries) {
|
|
4737
|
+
const permission = permissionFromUnknown(entry);
|
|
4738
|
+
if (permission) permissions.push(permission);
|
|
4739
|
+
}
|
|
4740
|
+
return uniquePermissions(permissions);
|
|
4741
|
+
}
|
|
4742
|
+
function permissionFromUnknown(value) {
|
|
4743
|
+
const record = asRecord(value);
|
|
4744
|
+
if (!record) return null;
|
|
4745
|
+
const service = stringValue(record.service);
|
|
4746
|
+
const space = stringValue(record.space);
|
|
4747
|
+
const path = stringValue(record.path);
|
|
4748
|
+
const actions = Array.isArray(record.actions) ? record.actions.map(String).filter(Boolean) : [];
|
|
4749
|
+
if (!service || !space || path === null || actions.length === 0) return null;
|
|
4750
|
+
return {
|
|
4751
|
+
service: normalizeService2(service),
|
|
4752
|
+
space,
|
|
4753
|
+
path,
|
|
4754
|
+
actions
|
|
4755
|
+
};
|
|
4756
|
+
}
|
|
4757
|
+
function uniquePermissions(entries) {
|
|
4758
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4759
|
+
const unique2 = [];
|
|
4760
|
+
for (const entry of entries) {
|
|
4761
|
+
const key = compactPermission(entry);
|
|
4762
|
+
if (seen.has(key)) continue;
|
|
4763
|
+
seen.add(key);
|
|
4764
|
+
unique2.push(entry);
|
|
4765
|
+
}
|
|
4766
|
+
return unique2;
|
|
4767
|
+
}
|
|
4768
|
+
function compactPermissions(entries) {
|
|
4769
|
+
return entries.map(compactPermission);
|
|
4770
|
+
}
|
|
4771
|
+
function extractSessionExpiry(session) {
|
|
4772
|
+
for (const key of ["expiresAt", "expiry", "expirationTime"]) {
|
|
4773
|
+
const parsed = parseDate2(session[key]);
|
|
4774
|
+
if (parsed) return parsed;
|
|
4775
|
+
}
|
|
4776
|
+
if (typeof session.siwe !== "string") return null;
|
|
4777
|
+
const match = session.siwe.match(/^Expiration Time:\s*(.+)$/im);
|
|
4778
|
+
return match ? parseDate2(match[1].trim()) : null;
|
|
4779
|
+
}
|
|
4780
|
+
function parseDate2(value) {
|
|
4781
|
+
if (value instanceof Date) {
|
|
4782
|
+
return Number.isNaN(value.getTime()) ? null : value;
|
|
4783
|
+
}
|
|
4784
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
4785
|
+
const millis = value > 0 && value < 1e12 ? value * 1e3 : value;
|
|
4786
|
+
const date2 = new Date(millis);
|
|
4787
|
+
return Number.isNaN(date2.getTime()) ? null : date2;
|
|
4788
|
+
}
|
|
4789
|
+
if (typeof value !== "string" || value.trim() === "") return null;
|
|
4790
|
+
const date = new Date(value);
|
|
4791
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
4792
|
+
}
|
|
4793
|
+
function getWasmBindings() {
|
|
4794
|
+
wasmBindings ??= new NodeWasmBindings();
|
|
4795
|
+
return wasmBindings;
|
|
4796
|
+
}
|
|
4797
|
+
function normalizeService2(service) {
|
|
4798
|
+
return service.startsWith("tinycloud.") ? service : `tinycloud.${service}`;
|
|
4799
|
+
}
|
|
4800
|
+
function asRecord(value) {
|
|
4801
|
+
return value !== null && typeof value === "object" ? value : null;
|
|
4802
|
+
}
|
|
4803
|
+
function stringValue(value) {
|
|
4804
|
+
return typeof value === "string" ? value : null;
|
|
4805
|
+
}
|
|
4806
|
+
function messageFromError(error) {
|
|
4807
|
+
return error instanceof Error ? error.message : String(error);
|
|
4808
|
+
}
|
|
4809
|
+
function formatStatus(summary) {
|
|
4810
|
+
const lines = [];
|
|
4811
|
+
lines.push(theme.heading("TinyCloud Status"));
|
|
4812
|
+
lines.push(`Active profile: ${theme.value(summary.activeProfile)}`);
|
|
4813
|
+
lines.push(`Default profile: ${theme.value(summary.defaultProfile)}`);
|
|
4814
|
+
lines.push("");
|
|
4815
|
+
if (summary.profiles.length === 0) {
|
|
4816
|
+
lines.push(theme.muted("No profiles configured. Run: tc init"));
|
|
4817
|
+
return `${lines.join("\n")}
|
|
4818
|
+
`;
|
|
4819
|
+
}
|
|
4820
|
+
lines.push(theme.label("Profiles"));
|
|
4821
|
+
for (const profile of summary.profiles) {
|
|
4822
|
+
lines.push(formatProfile(profile));
|
|
4823
|
+
}
|
|
4824
|
+
return `${lines.join("\n")}
|
|
4825
|
+
`;
|
|
4826
|
+
}
|
|
4827
|
+
function formatProfile(profile) {
|
|
4828
|
+
const marker = profile.active ? theme.success("*") : " ";
|
|
4829
|
+
const name = profile.default ? `${profile.name} (default)` : profile.name;
|
|
4830
|
+
const host = profile.host ? theme.muted(profile.host) : theme.muted("no host");
|
|
4831
|
+
const summary = [
|
|
4832
|
+
`${marker} ${profile.active ? theme.brand(name) : name}`,
|
|
4833
|
+
formatProfileStatus(profile.status),
|
|
4834
|
+
profile.posture ?? "no posture",
|
|
4835
|
+
plural(profile.permissionCount, "permission"),
|
|
4836
|
+
`${profile.activeDelegationCount}/${profile.delegationCount} delegations`,
|
|
4837
|
+
host
|
|
4838
|
+
].join(" ");
|
|
4839
|
+
const lines = [summary];
|
|
4840
|
+
lines.push(` session: ${formatSession(profile.session)}`);
|
|
4841
|
+
if (profile.permissionsCompact.length > 0) {
|
|
4842
|
+
lines.push(" permissions:");
|
|
4843
|
+
for (const permission of profile.permissionsCompact) {
|
|
4844
|
+
lines.push(` ${permission}`);
|
|
4845
|
+
}
|
|
4846
|
+
}
|
|
4847
|
+
if (profile.delegations.length > 0) {
|
|
4848
|
+
lines.push(" delegations:");
|
|
4849
|
+
for (const delegation of profile.delegations) {
|
|
4850
|
+
lines.push(` ${formatDelegation(delegation)}`);
|
|
4851
|
+
}
|
|
4852
|
+
}
|
|
4853
|
+
if (profile.issues.length > 0) {
|
|
4854
|
+
lines.push(" issues:");
|
|
4855
|
+
for (const issue of profile.issues) {
|
|
4856
|
+
lines.push(` ${theme.warn(issue)}`);
|
|
4857
|
+
}
|
|
4858
|
+
}
|
|
4859
|
+
return lines.join("\n");
|
|
4860
|
+
}
|
|
4861
|
+
function formatProfileStatus(status) {
|
|
4862
|
+
switch (status) {
|
|
4863
|
+
case "logged-in":
|
|
4864
|
+
return theme.success("logged in");
|
|
4865
|
+
case "local-key":
|
|
4866
|
+
return theme.success("local key");
|
|
4867
|
+
case "expired":
|
|
4868
|
+
return theme.warn("expired");
|
|
4869
|
+
case "missing":
|
|
4870
|
+
return theme.warn("missing");
|
|
4871
|
+
case "signed-out":
|
|
4872
|
+
return theme.muted("signed out");
|
|
4873
|
+
}
|
|
4874
|
+
}
|
|
4875
|
+
function formatSession(session) {
|
|
4876
|
+
if (!session.present) return theme.muted("none");
|
|
4877
|
+
if (session.expired === true) {
|
|
4878
|
+
return `${theme.warn("expired")}${formatExpiresAt(session.expiresAt)}`;
|
|
4879
|
+
}
|
|
4880
|
+
if (session.expired === false) {
|
|
4881
|
+
return `${theme.success("active")}${formatExpiresAt(session.expiresAt)}`;
|
|
4882
|
+
}
|
|
4883
|
+
return `${theme.success("present")}${formatExpiresAt(session.expiresAt)}`;
|
|
4884
|
+
}
|
|
4885
|
+
function formatDelegation(delegation) {
|
|
4886
|
+
const state = delegation.expired === true ? theme.warn("expired") : theme.success("active");
|
|
4887
|
+
return [
|
|
4888
|
+
delegation.cid,
|
|
4889
|
+
state,
|
|
4890
|
+
formatExpiresAt(delegation.expiresAt).trim(),
|
|
4891
|
+
plural(delegation.permissions.length, "permission")
|
|
4892
|
+
].filter(Boolean).join(" ");
|
|
4893
|
+
}
|
|
4894
|
+
function formatExpiresAt(expiresAt) {
|
|
4895
|
+
return expiresAt ? ` until ${expiresAt}` : "";
|
|
4896
|
+
}
|
|
4897
|
+
function plural(count, label) {
|
|
4898
|
+
return `${count} ${label}${count === 1 ? "" : "s"}`;
|
|
4899
|
+
}
|
|
4900
|
+
|
|
3710
4901
|
// src/index.ts
|
|
3711
4902
|
var { version } = JSON.parse(
|
|
3712
4903
|
readFileSync3(new URL("../package.json", import.meta.url), "utf-8")
|
|
@@ -3721,7 +4912,7 @@ program.hook("preAction", async (thisCommand) => {
|
|
|
3721
4912
|
const commandName = thisCommand.name();
|
|
3722
4913
|
const parentName = thisCommand.parent?.name();
|
|
3723
4914
|
const fullCommand = parentName && parentName !== "tc" ? `${parentName} ${commandName}` : commandName;
|
|
3724
|
-
const skipGuard = ["tc", "init", "doctor", "completion", "help", "upgrade"].includes(commandName) || fullCommand === "profile create";
|
|
4915
|
+
const skipGuard = ["tc", "init", "doctor", "completion", "help", "upgrade", "status"].includes(commandName) || fullCommand === "profile create";
|
|
3725
4916
|
if (!skipGuard && !opts.quiet && isInteractive()) {
|
|
3726
4917
|
try {
|
|
3727
4918
|
const config = await ProfileManager.getConfig();
|
|
@@ -3756,6 +4947,9 @@ registerSqlCommand(program);
|
|
|
3756
4947
|
registerDuckdbCommand(program);
|
|
3757
4948
|
registerManifestCommand(program);
|
|
3758
4949
|
registerUpgradeCommand(program);
|
|
4950
|
+
registerStatusCommand(program);
|
|
4951
|
+
program.addHelpText("before", () => `${theme.label("Version:")} ${theme.value(version)}
|
|
4952
|
+
`);
|
|
3759
4953
|
program.addHelpText("afterAll", () => {
|
|
3760
4954
|
if (!process.stdout.isTTY) return "";
|
|
3761
4955
|
return `
|