@tinycloud/cli 0.6.0-beta.8 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +188 -49
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -680,6 +680,11 @@ function buildAuthUrl(did, options = {}) {
|
|
|
680
680
|
const base = options.openkeyHost ?? DEFAULT_OPENKEY_HOST;
|
|
681
681
|
return `${base}/delegate?${params.toString()}`;
|
|
682
682
|
}
|
|
683
|
+
function shouldOpenBrowser(options) {
|
|
684
|
+
if (options.noPopup) return false;
|
|
685
|
+
const env = process.env.TC_AUTH_NO_POPUP ?? process.env.TC_NO_POPUP;
|
|
686
|
+
return env !== "1" && env !== "true";
|
|
687
|
+
}
|
|
683
688
|
async function callbackFlow(did, options = {}) {
|
|
684
689
|
return new Promise((resolve3, reject) => {
|
|
685
690
|
let timeout;
|
|
@@ -750,16 +755,21 @@ async function callbackFlow(did, options = {}) {
|
|
|
750
755
|
const port = addr.port;
|
|
751
756
|
const callbackUrl = `http://127.0.0.1:${port}/callback`;
|
|
752
757
|
const authUrl = buildAuthUrl(did, { ...options, callback: callbackUrl });
|
|
753
|
-
|
|
758
|
+
const openBrowser = shouldOpenBrowser(options);
|
|
759
|
+
if (openBrowser && isInteractive()) {
|
|
754
760
|
console.error(`Opening browser for authentication...`);
|
|
755
761
|
console.error(`If the browser doesn't open, visit: ${authUrl}`);
|
|
762
|
+
} else if (!openBrowser || isInteractive()) {
|
|
763
|
+
console.error(`Open this URL in a browser to authenticate: ${authUrl}`);
|
|
756
764
|
}
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
765
|
+
if (openBrowser) {
|
|
766
|
+
try {
|
|
767
|
+
const open = (await import("open")).default;
|
|
768
|
+
await open(authUrl);
|
|
769
|
+
} catch {
|
|
770
|
+
server.close();
|
|
771
|
+
throw new Error("Failed to open browser");
|
|
772
|
+
}
|
|
763
773
|
}
|
|
764
774
|
if (isInteractive()) {
|
|
765
775
|
console.error(`
|
|
@@ -816,7 +826,7 @@ Open this URL in a browser to authenticate:
|
|
|
816
826
|
|
|
817
827
|
// src/commands/init.ts
|
|
818
828
|
function registerInitCommand(program2) {
|
|
819
|
-
program2.command("init").description("Initialize a new TinyCloud profile").option("--name <profile>", "Profile name", "default").option("--key-only", "Only generate key, skip authentication").option("--host <url>", "TinyCloud node URL").option("--paste", "Use manual paste mode for authentication").action(async (options, cmd) => {
|
|
829
|
+
program2.command("init").description("Initialize a new TinyCloud profile").option("--name <profile>", "Profile name", "default").option("--key-only", "Only generate key, skip authentication").option("--host <url>", "TinyCloud node URL").option("--paste", "Use manual paste mode for authentication").option("--no-popup", "Print the OpenKey URL without opening a browser").action(async (options, cmd) => {
|
|
820
830
|
try {
|
|
821
831
|
const globalOpts = cmd.optsWithGlobals();
|
|
822
832
|
const profileName = options.name;
|
|
@@ -857,6 +867,7 @@ function registerInitCommand(program2) {
|
|
|
857
867
|
}
|
|
858
868
|
const delegationData = await startAuthFlow(did, {
|
|
859
869
|
paste: options.paste,
|
|
870
|
+
noPopup: options.popup === false,
|
|
860
871
|
jwk,
|
|
861
872
|
host
|
|
862
873
|
});
|
|
@@ -1366,7 +1377,7 @@ async function promptAuthMethod() {
|
|
|
1366
1377
|
}
|
|
1367
1378
|
function registerAuthCommand(program2) {
|
|
1368
1379
|
const auth = program2.command("auth").description("Authentication management");
|
|
1369
|
-
auth.command("login").description("Authenticate with TinyCloud").option("--paste", "Use manual paste mode instead of browser callback").option("--method <method>", "Authentication method: local or openkey").action(async (options, cmd) => {
|
|
1380
|
+
auth.command("login").description("Authenticate with TinyCloud").option("--paste", "Use manual paste mode instead of browser callback").option("--no-popup", "Print the OpenKey URL without opening a browser").option("--method <method>", "Authentication method: local or openkey").action(async (options, cmd) => {
|
|
1370
1381
|
try {
|
|
1371
1382
|
const globalOpts = cmd.optsWithGlobals();
|
|
1372
1383
|
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
@@ -1386,7 +1397,10 @@ function registerAuthCommand(program2) {
|
|
|
1386
1397
|
if (method === "local") {
|
|
1387
1398
|
await handleLocalAuth(ctx.profile, ctx.host);
|
|
1388
1399
|
} else {
|
|
1389
|
-
await handleOpenKeyAuth(ctx.profile, ctx.host,
|
|
1400
|
+
await handleOpenKeyAuth(ctx.profile, ctx.host, {
|
|
1401
|
+
paste: options.paste,
|
|
1402
|
+
noPopup: options.popup === false
|
|
1403
|
+
});
|
|
1390
1404
|
}
|
|
1391
1405
|
} catch (error) {
|
|
1392
1406
|
handleError(error);
|
|
@@ -1402,6 +1416,18 @@ function registerAuthCommand(program2) {
|
|
|
1402
1416
|
handleError(error);
|
|
1403
1417
|
}
|
|
1404
1418
|
});
|
|
1419
|
+
auth.command("rotate").description("Rotate the active profile session key").option("--paste", "Use manual paste mode instead of browser callback").option("--no-popup", "Print the OpenKey URL without opening a browser").action(async (options, cmd) => {
|
|
1420
|
+
try {
|
|
1421
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1422
|
+
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
1423
|
+
await rotateAuthKey(ctx.profile, ctx.host, {
|
|
1424
|
+
paste: options.paste,
|
|
1425
|
+
noPopup: options.popup === false
|
|
1426
|
+
});
|
|
1427
|
+
} catch (error) {
|
|
1428
|
+
handleError(error);
|
|
1429
|
+
}
|
|
1430
|
+
});
|
|
1405
1431
|
auth.command("status").description("Show current authentication state").action(async (_options, cmd) => {
|
|
1406
1432
|
try {
|
|
1407
1433
|
const globalOpts = cmd.optsWithGlobals();
|
|
@@ -1459,7 +1485,7 @@ function registerAuthCommand(program2) {
|
|
|
1459
1485
|
).option("--permission <file>", 'JSON permission request: { "permissions": PermissionEntry[] }').option("--manifest <fileOrBase64>", "Manifest file, base64:<json>, or raw base64 JSON").option(
|
|
1460
1486
|
"--expiry <duration>",
|
|
1461
1487
|
`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.`
|
|
1462
|
-
).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) => {
|
|
1488
|
+
).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).option("--no-popup", "Print the OpenKey URL without opening a browser when granting with OpenKey").action(async (options, cmd) => {
|
|
1463
1489
|
try {
|
|
1464
1490
|
const globalOpts = cmd.optsWithGlobals();
|
|
1465
1491
|
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
@@ -1504,7 +1530,8 @@ function registerAuthCommand(program2) {
|
|
|
1504
1530
|
host: ctx.host,
|
|
1505
1531
|
permissions: group,
|
|
1506
1532
|
openkeyHost,
|
|
1507
|
-
expiry: expiryOption
|
|
1533
|
+
expiry: expiryOption,
|
|
1534
|
+
noPopup: options.popup === false
|
|
1508
1535
|
});
|
|
1509
1536
|
const delegation = portableFromOpenKeyDelegation(delegationData, group, ctx.host);
|
|
1510
1537
|
const stored = storedAdditionalDelegation(delegation, group);
|
|
@@ -1920,7 +1947,7 @@ function normalizePortableDelegation(delegation) {
|
|
|
1920
1947
|
return { ...delegation, expiry };
|
|
1921
1948
|
}
|
|
1922
1949
|
async function ensureDelegationAuthority(params) {
|
|
1923
|
-
if (params.node.hasRuntimePermissions(params.requested)) return;
|
|
1950
|
+
if (!params.force && params.node.hasRuntimePermissions(params.requested)) return;
|
|
1924
1951
|
if (params.profile.authMethod === "openkey") {
|
|
1925
1952
|
const key = await ProfileManager.getKey(params.ctx.profile);
|
|
1926
1953
|
if (!key) {
|
|
@@ -2086,13 +2113,20 @@ function groupPermissionsBySpace(permissions) {
|
|
|
2086
2113
|
function isRawPermission(permission) {
|
|
2087
2114
|
return permission.service === "tinycloud.encryption" && permission.path.startsWith("urn:tinycloud:encryption:");
|
|
2088
2115
|
}
|
|
2116
|
+
function returnedSpaceMatchesExpected(returnedSpace, expectedSpace) {
|
|
2117
|
+
if (returnedSpace === expectedSpace) return true;
|
|
2118
|
+
if (!returnedSpace.startsWith("tinycloud:")) return false;
|
|
2119
|
+
const returnedName = returnedSpace.slice(returnedSpace.lastIndexOf(":") + 1);
|
|
2120
|
+
return returnedName === expectedSpace;
|
|
2121
|
+
}
|
|
2089
2122
|
function portableFromOpenKeyDelegation(data, permissions, host) {
|
|
2090
2123
|
const primary = permissions.find((permission) => !isRawPermission(permission)) ?? permissions[0];
|
|
2091
2124
|
const returnedSpace = String(data.spaceId ?? primary.space ?? "encryption");
|
|
2092
2125
|
const expectedSpaces = new Set(
|
|
2093
2126
|
permissions.filter((permission) => !isRawPermission(permission)).map((permission) => permission.space)
|
|
2094
2127
|
);
|
|
2095
|
-
|
|
2128
|
+
const matchesExpectedSpace = expectedSpaces.size === 1 && returnedSpaceMatchesExpected(returnedSpace, Array.from(expectedSpaces)[0]);
|
|
2129
|
+
if (expectedSpaces.size > 0 && !matchesExpectedSpace) {
|
|
2096
2130
|
throw new CLIError(
|
|
2097
2131
|
"OPENKEY_SCOPE_MISMATCH",
|
|
2098
2132
|
`OpenKey returned delegation for ${returnedSpace}, expected ${Array.from(expectedSpaces).join(", ")}.`,
|
|
@@ -2108,7 +2142,7 @@ function portableFromOpenKeyDelegation(data, permissions, host) {
|
|
|
2108
2142
|
actions: primary.actions,
|
|
2109
2143
|
resources: permissions.map((permission) => ({
|
|
2110
2144
|
service: permission.service.startsWith("tinycloud.") ? permission.service.slice("tinycloud.".length) : permission.service,
|
|
2111
|
-
space: permission.space,
|
|
2145
|
+
space: isRawPermission(permission) ? permission.space : returnedSpace,
|
|
2112
2146
|
path: permission.path,
|
|
2113
2147
|
actions: [...permission.actions]
|
|
2114
2148
|
})),
|
|
@@ -2127,16 +2161,75 @@ function inferDelegationExpiry(data) {
|
|
|
2127
2161
|
}
|
|
2128
2162
|
return new Date(Date.now() + 60 * 60 * 1e3);
|
|
2129
2163
|
}
|
|
2130
|
-
async function
|
|
2164
|
+
async function rotateAuthKey(profileName, host, options = {}) {
|
|
2165
|
+
const profile = await ProfileManager.getProfile(profileName);
|
|
2166
|
+
const posture = resolveProfilePosture(profile);
|
|
2167
|
+
const oldDid = profile.sessionDid ?? profile.did;
|
|
2168
|
+
if (posture === "delegate-session") {
|
|
2169
|
+
throw new CLIError(
|
|
2170
|
+
"ROTATE_DELEGATE_SESSION_UNSUPPORTED",
|
|
2171
|
+
`Profile "${profileName}" is a delegated session. Request or import a new owner delegation instead of rotating it locally.`,
|
|
2172
|
+
ExitCode.PERMISSION_DENIED
|
|
2173
|
+
);
|
|
2174
|
+
}
|
|
2175
|
+
if (profile.authMethod === "local" || posture === "local-owner-key") {
|
|
2176
|
+
if (!profile.privateKey) {
|
|
2177
|
+
throw new CLIError(
|
|
2178
|
+
"LOCAL_OWNER_KEY_REQUIRED",
|
|
2179
|
+
`Profile "${profileName}" does not have a local owner private key. Run \`tc auth login --method local\` first.`,
|
|
2180
|
+
ExitCode.AUTH_REQUIRED
|
|
2181
|
+
);
|
|
2182
|
+
}
|
|
2183
|
+
await ProfileManager.clearSession(profileName);
|
|
2184
|
+
const result2 = await handleLocalAuth(profileName, host, {
|
|
2185
|
+
emitOutput: false,
|
|
2186
|
+
forceSessionKey: true
|
|
2187
|
+
});
|
|
2188
|
+
outputRotationResult(result2.profile, profileName, oldDid, "local");
|
|
2189
|
+
return;
|
|
2190
|
+
}
|
|
2191
|
+
const { jwk, did } = await withSpinner("Generating session key...", async () => {
|
|
2192
|
+
return generateKey();
|
|
2193
|
+
});
|
|
2194
|
+
await ProfileManager.setKey(profileName, jwk);
|
|
2195
|
+
await ProfileManager.clearSession(profileName);
|
|
2196
|
+
await ProfileManager.setProfile(profileName, {
|
|
2197
|
+
...profile,
|
|
2198
|
+
host,
|
|
2199
|
+
did,
|
|
2200
|
+
sessionDid: did,
|
|
2201
|
+
posture: profile.posture ?? "owner-openkey",
|
|
2202
|
+
operatorType: profile.operatorType ?? "human",
|
|
2203
|
+
authMethod: "openkey"
|
|
2204
|
+
});
|
|
2205
|
+
const result = await refreshOpenKeySession(profileName, host, {
|
|
2206
|
+
paste: options.paste,
|
|
2207
|
+
noPopup: options.noPopup
|
|
2208
|
+
});
|
|
2209
|
+
outputRotationResult(result.profile, profileName, oldDid, "openkey");
|
|
2210
|
+
}
|
|
2211
|
+
function outputRotationResult(profile, profileName, oldDid, authMethod) {
|
|
2212
|
+
outputJson({
|
|
2213
|
+
rotated: true,
|
|
2214
|
+
profile: profileName,
|
|
2215
|
+
oldDid,
|
|
2216
|
+
did: profile.did,
|
|
2217
|
+
sessionDid: profile.sessionDid ?? null,
|
|
2218
|
+
authMethod,
|
|
2219
|
+
spaceId: profile.spaceId ?? null
|
|
2220
|
+
});
|
|
2221
|
+
}
|
|
2222
|
+
async function handleLocalAuth(profileName, host, options = {}) {
|
|
2131
2223
|
const profile = await ProfileManager.getProfile(profileName).catch(() => null);
|
|
2224
|
+
const posture = profile ? resolveProfilePosture(profile) : null;
|
|
2132
2225
|
let privateKey;
|
|
2133
2226
|
let address;
|
|
2134
2227
|
let did;
|
|
2135
2228
|
let sessionDid = profile?.sessionDid;
|
|
2136
|
-
if (profile?.authMethod === "local"
|
|
2229
|
+
if ((profile?.authMethod === "local" || posture === "local-owner-key") && profile.privateKey) {
|
|
2137
2230
|
privateKey = profile.privateKey;
|
|
2138
|
-
address = profile.address;
|
|
2139
|
-
did = profile.did;
|
|
2231
|
+
address = profile.address ?? await deriveAddress(privateKey);
|
|
2232
|
+
did = profile.did.startsWith("did:pkh:") ? profile.did : addressToDID(address, profile.chainId ?? DEFAULT_CHAIN_ID);
|
|
2140
2233
|
if (isInteractive()) {
|
|
2141
2234
|
process.stderr.write(theme.muted("Using existing local key") + "\n");
|
|
2142
2235
|
process.stderr.write(formatField("Address", address) + "\n");
|
|
@@ -2155,7 +2248,7 @@ async function handleLocalAuth(profileName, host) {
|
|
|
2155
2248
|
}
|
|
2156
2249
|
}
|
|
2157
2250
|
const hasKey = await ProfileManager.getKey(profileName);
|
|
2158
|
-
if (!hasKey) {
|
|
2251
|
+
if (options.forceSessionKey || !hasKey) {
|
|
2159
2252
|
const { jwk, did: generatedSessionDid } = await withSpinner("Generating session key...", async () => {
|
|
2160
2253
|
return generateKey();
|
|
2161
2254
|
});
|
|
@@ -2180,7 +2273,7 @@ async function handleLocalAuth(profileName, host) {
|
|
|
2180
2273
|
signature: sessionResult.signature
|
|
2181
2274
|
});
|
|
2182
2275
|
sessionDid = sessionResult.verificationMethod;
|
|
2183
|
-
|
|
2276
|
+
const updatedProfile = {
|
|
2184
2277
|
...profile,
|
|
2185
2278
|
name: profileName,
|
|
2186
2279
|
host,
|
|
@@ -2196,19 +2289,23 @@ async function handleLocalAuth(profileName, host) {
|
|
|
2196
2289
|
authMethod: "local",
|
|
2197
2290
|
privateKey,
|
|
2198
2291
|
address
|
|
2199
|
-
}
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2292
|
+
};
|
|
2293
|
+
await ProfileManager.setProfile(profileName, updatedProfile);
|
|
2294
|
+
if (options.emitOutput ?? true) {
|
|
2295
|
+
outputJson({
|
|
2296
|
+
authenticated: true,
|
|
2297
|
+
profile: profileName,
|
|
2298
|
+
did,
|
|
2299
|
+
sessionDid,
|
|
2300
|
+
address,
|
|
2301
|
+
spaceId: sessionResult.spaceId,
|
|
2302
|
+
authMethod: "local"
|
|
2303
|
+
});
|
|
2304
|
+
}
|
|
2305
|
+
return { profile: updatedProfile, sessionResult };
|
|
2209
2306
|
}
|
|
2210
|
-
async function handleOpenKeyAuth(profileName, host,
|
|
2211
|
-
const { profile, delegationData } = await refreshOpenKeySession(profileName, host,
|
|
2307
|
+
async function handleOpenKeyAuth(profileName, host, options = {}) {
|
|
2308
|
+
const { profile, delegationData } = await refreshOpenKeySession(profileName, host, options);
|
|
2212
2309
|
outputJson({
|
|
2213
2310
|
authenticated: true,
|
|
2214
2311
|
profile: profileName,
|
|
@@ -2229,6 +2326,7 @@ async function refreshOpenKeySession(profileName, host, options = {}) {
|
|
|
2229
2326
|
const profile = await ProfileManager.getProfile(profileName);
|
|
2230
2327
|
const delegationData = await startAuthFlow(profile.did, {
|
|
2231
2328
|
paste: options.paste,
|
|
2329
|
+
noPopup: options.noPopup,
|
|
2232
2330
|
jwk: key,
|
|
2233
2331
|
host,
|
|
2234
2332
|
openkeyHost: resolveOpenKeyHost(profile)
|
|
@@ -2259,14 +2357,19 @@ async function readStdin2() {
|
|
|
2259
2357
|
}
|
|
2260
2358
|
return Buffer.concat(chunks);
|
|
2261
2359
|
}
|
|
2360
|
+
async function kvHandle(node, spaceInput, profileName) {
|
|
2361
|
+
const spaceUri = await resolveSpaceUri(spaceInput, profileName);
|
|
2362
|
+
return spaceUri ? node.kvForSpace(spaceUri) : node.kv;
|
|
2363
|
+
}
|
|
2262
2364
|
function registerKvCommand(program2) {
|
|
2263
2365
|
const kv = program2.command("kv").description("Key-value store operations");
|
|
2264
|
-
kv.command("get <key>").description("Get a value by key").option("--raw", "Output raw value (no JSON wrapping)").option("-o, --output <file>", "Write value to file").action(async (key, options, cmd) => {
|
|
2366
|
+
kv.command("get <key>").description("Get a value by key").option("--raw", "Output raw value (no JSON wrapping)").option("-o, --output <file>", "Write value to file").option("--space <name|uri>", "Target a non-primary space (short name or full URI)").action(async (key, options, cmd) => {
|
|
2265
2367
|
try {
|
|
2266
2368
|
const globalOpts = cmd.optsWithGlobals();
|
|
2267
2369
|
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
2268
2370
|
const node = await ensureAuthenticated(ctx);
|
|
2269
|
-
const
|
|
2371
|
+
const kv2 = await kvHandle(node, options.space, ctx.profile);
|
|
2372
|
+
const result = await withSpinner(`Getting ${key}...`, () => kv2.get(key));
|
|
2270
2373
|
if (!result.ok) {
|
|
2271
2374
|
if (result.error.code === "KV_NOT_FOUND" || result.error.code === "NOT_FOUND") {
|
|
2272
2375
|
throw new CLIError("NOT_FOUND", `Key "${key}" not found`, ExitCode.NOT_FOUND);
|
|
@@ -2347,13 +2450,14 @@ function registerKvCommand(program2) {
|
|
|
2347
2450
|
handleError(error);
|
|
2348
2451
|
}
|
|
2349
2452
|
});
|
|
2350
|
-
kv.command("list").description("List keys").option("--prefix <prefix>", "Filter by key prefix").action(async (options, cmd) => {
|
|
2453
|
+
kv.command("list").description("List keys").option("--prefix <prefix>", "Filter by key prefix").option("--space <name|uri>", "Target a non-primary space (short name or full URI)").action(async (options, cmd) => {
|
|
2351
2454
|
try {
|
|
2352
2455
|
const globalOpts = cmd.optsWithGlobals();
|
|
2353
2456
|
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
2354
2457
|
const node = await ensureAuthenticated(ctx);
|
|
2458
|
+
const kv2 = await kvHandle(node, options.space, ctx.profile);
|
|
2355
2459
|
const listOptions = options.prefix ? { prefix: options.prefix } : void 0;
|
|
2356
|
-
const result = await withSpinner("Listing keys...", () =>
|
|
2460
|
+
const result = await withSpinner("Listing keys...", () => kv2.list(listOptions));
|
|
2357
2461
|
if (!result.ok) {
|
|
2358
2462
|
throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
|
|
2359
2463
|
}
|
|
@@ -2381,12 +2485,13 @@ function registerKvCommand(program2) {
|
|
|
2381
2485
|
handleError(error);
|
|
2382
2486
|
}
|
|
2383
2487
|
});
|
|
2384
|
-
kv.command("head <key>").description("Get metadata for a key (no body)").action(async (key,
|
|
2488
|
+
kv.command("head <key>").description("Get metadata for a key (no body)").option("--space <name|uri>", "Target a non-primary space (short name or full URI)").action(async (key, options, cmd) => {
|
|
2385
2489
|
try {
|
|
2386
2490
|
const globalOpts = cmd.optsWithGlobals();
|
|
2387
2491
|
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
2388
2492
|
const node = await ensureAuthenticated(ctx);
|
|
2389
|
-
const
|
|
2493
|
+
const kv2 = await kvHandle(node, options.space, ctx.profile);
|
|
2494
|
+
const result = await withSpinner(`Checking ${key}...`, () => kv2.head(key));
|
|
2390
2495
|
if (!result.ok) {
|
|
2391
2496
|
if (result.error.code === "KV_NOT_FOUND" || result.error.code === "NOT_FOUND") {
|
|
2392
2497
|
outputJson({ key, exists: false, metadata: {} });
|
|
@@ -2978,7 +3083,7 @@ _tc_completions() {
|
|
|
2978
3083
|
commands="init auth kv space delegation share node profile completion"
|
|
2979
3084
|
|
|
2980
3085
|
case "\${COMP_WORDS[1]}" in
|
|
2981
|
-
auth) subcommands="login logout status whoami" ;;
|
|
3086
|
+
auth) subcommands="login logout rotate status whoami" ;;
|
|
2982
3087
|
kv) subcommands="get put delete list head" ;;
|
|
2983
3088
|
space) subcommands="list create info switch" ;;
|
|
2984
3089
|
delegation) subcommands="create list info revoke" ;;
|
|
@@ -3028,7 +3133,7 @@ _tc() {
|
|
|
3028
3133
|
;;
|
|
3029
3134
|
args)
|
|
3030
3135
|
case $words[1] in
|
|
3031
|
-
auth) _values 'subcommand' login logout status whoami ;;
|
|
3136
|
+
auth) _values 'subcommand' login logout rotate status whoami ;;
|
|
3032
3137
|
kv) _values 'subcommand' get put delete list head ;;
|
|
3033
3138
|
space) _values 'subcommand' list create info switch ;;
|
|
3034
3139
|
delegation) _values 'subcommand' create list info revoke ;;
|
|
@@ -3063,7 +3168,7 @@ complete -c tc -n "not __fish_seen_subcommand_from $commands" -a profile -d "Pro
|
|
|
3063
3168
|
complete -c tc -n "not __fish_seen_subcommand_from $commands" -a completion -d "Generate shell completions"
|
|
3064
3169
|
|
|
3065
3170
|
# Subcommands
|
|
3066
|
-
complete -c tc -n "__fish_seen_subcommand_from auth" -a "login logout status whoami"
|
|
3171
|
+
complete -c tc -n "__fish_seen_subcommand_from auth" -a "login logout rotate status whoami"
|
|
3067
3172
|
complete -c tc -n "__fish_seen_subcommand_from kv" -a "get put delete list head"
|
|
3068
3173
|
complete -c tc -n "__fish_seen_subcommand_from space" -a "list create info switch"
|
|
3069
3174
|
complete -c tc -n "__fish_seen_subcommand_from delegation" -a "create list info revoke"
|
|
@@ -3262,6 +3367,12 @@ import {
|
|
|
3262
3367
|
resolveSecretPath
|
|
3263
3368
|
} from "@tinycloud/node-sdk";
|
|
3264
3369
|
var SECRETS_SPACE = "secrets";
|
|
3370
|
+
var SECRET_KV_ABILITIES = {
|
|
3371
|
+
get: "tinycloud.kv/get",
|
|
3372
|
+
put: "tinycloud.kv/put",
|
|
3373
|
+
del: "tinycloud.kv/del",
|
|
3374
|
+
list: "tinycloud.kv/list"
|
|
3375
|
+
};
|
|
3265
3376
|
async function readStdin4() {
|
|
3266
3377
|
const chunks = [];
|
|
3267
3378
|
for await (const chunk of process.stdin) {
|
|
@@ -3295,7 +3406,7 @@ async function ensureSecretsNode(ctx, options) {
|
|
|
3295
3406
|
return ensureAuthenticated(ctx, auth);
|
|
3296
3407
|
}
|
|
3297
3408
|
async function runSecretOperation(params) {
|
|
3298
|
-
const first = await
|
|
3409
|
+
const first = await runSecretOperationAttempt(params.label, params.operation);
|
|
3299
3410
|
if (first.ok || !shouldRequestSecretPermissions(first.error)) {
|
|
3300
3411
|
return first;
|
|
3301
3412
|
}
|
|
@@ -3317,10 +3428,20 @@ async function runSecretOperation(params) {
|
|
|
3317
3428
|
node: params.node,
|
|
3318
3429
|
requested,
|
|
3319
3430
|
expiryOption: void 0,
|
|
3320
|
-
yes: true
|
|
3431
|
+
yes: true,
|
|
3432
|
+
force: true
|
|
3321
3433
|
})
|
|
3322
3434
|
);
|
|
3323
|
-
return
|
|
3435
|
+
return runSecretOperationAttempt(params.label, params.operation);
|
|
3436
|
+
}
|
|
3437
|
+
async function runSecretOperationAttempt(label, operation) {
|
|
3438
|
+
try {
|
|
3439
|
+
return await withSpinner(label, operation);
|
|
3440
|
+
} catch (error) {
|
|
3441
|
+
const permissionError = thrownPermissionError(error);
|
|
3442
|
+
if (permissionError) return permissionError;
|
|
3443
|
+
throw error;
|
|
3444
|
+
}
|
|
3324
3445
|
}
|
|
3325
3446
|
function canRequestOwnerPermissions(profile) {
|
|
3326
3447
|
const posture = resolveProfilePosture(profile);
|
|
@@ -3330,6 +3451,21 @@ function shouldRequestSecretPermissions(error) {
|
|
|
3330
3451
|
if (error.code !== "PERMISSION_DENIED") return false;
|
|
3331
3452
|
return /permission|session expired|autosign|capabilit/i.test(error.message);
|
|
3332
3453
|
}
|
|
3454
|
+
function thrownPermissionError(error) {
|
|
3455
|
+
const record = error;
|
|
3456
|
+
const message = typeof record?.message === "string" ? record.message : String(error);
|
|
3457
|
+
const code = typeof record?.code === "string" ? record.code : "PERMISSION_DENIED";
|
|
3458
|
+
if (code !== "PERMISSION_DENIED" && !/permission|session expired|autosign|capabilit/i.test(message)) {
|
|
3459
|
+
return null;
|
|
3460
|
+
}
|
|
3461
|
+
return {
|
|
3462
|
+
ok: false,
|
|
3463
|
+
error: {
|
|
3464
|
+
code: "PERMISSION_DENIED",
|
|
3465
|
+
message
|
|
3466
|
+
}
|
|
3467
|
+
};
|
|
3468
|
+
}
|
|
3333
3469
|
function isStoredSessionExpired(session) {
|
|
3334
3470
|
const record = session;
|
|
3335
3471
|
const direct = parseDate(record.expiresAt ?? record.expiry ?? record.expirationTime);
|
|
@@ -3347,20 +3483,23 @@ function parseDate(value) {
|
|
|
3347
3483
|
const date = new Date(value);
|
|
3348
3484
|
return Number.isNaN(date.getTime()) ? null : date;
|
|
3349
3485
|
}
|
|
3486
|
+
function secretKvAbility(action) {
|
|
3487
|
+
return SECRET_KV_ABILITIES[action];
|
|
3488
|
+
}
|
|
3350
3489
|
function secretPermissionEntries(params) {
|
|
3351
3490
|
const path = params.action === "list" ? resolveSecretListPrefix(params.options) : resolveSecretPath(params.name ?? "", params.options).permissionPaths.vault;
|
|
3352
3491
|
const permissions = [{
|
|
3353
3492
|
service: "tinycloud.kv",
|
|
3354
3493
|
space: SECRETS_SPACE,
|
|
3355
3494
|
path,
|
|
3356
|
-
actions: [params.action],
|
|
3495
|
+
actions: [secretKvAbility(params.action)],
|
|
3357
3496
|
skipPrefix: true
|
|
3358
3497
|
}];
|
|
3359
3498
|
if (params.action === "get") {
|
|
3360
3499
|
permissions.push({
|
|
3361
3500
|
service: "tinycloud.encryption",
|
|
3362
3501
|
path: params.node.getDefaultEncryptionNetworkId(),
|
|
3363
|
-
actions: ["decrypt"],
|
|
3502
|
+
actions: ["tinycloud.encryption/decrypt"],
|
|
3364
3503
|
skipPrefix: true
|
|
3365
3504
|
});
|
|
3366
3505
|
}
|
|
@@ -3435,7 +3574,7 @@ function registerSecretsCommand(program2) {
|
|
|
3435
3574
|
handleError(error);
|
|
3436
3575
|
}
|
|
3437
3576
|
});
|
|
3438
|
-
secrets.command("get <name>").description("Get a secret value").option("--scope <scope>", "Logical secret scope").option("--space <scope>", "Deprecated alias for --scope").option("--raw", "Output raw value (no JSON wrapping)").option("-o, --output <file>", "Write value to file").option("--private-key <hex>", "Ethereum private key (or set TC_PRIVATE_KEY)").action(async (name, options, cmd) => {
|
|
3577
|
+
secrets.command("get <name>").description("Get a secret value").option("--scope <scope>", "Logical secret scope").option("--space <scope>", "Deprecated alias for --scope").option("--raw", "Output raw value (no JSON wrapping)").option("--value-only", "Output only the secret value (alias for --raw)").option("-o, --output <file>", "Write value to file").option("--private-key <hex>", "Ethereum private key (or set TC_PRIVATE_KEY)").action(async (name, options, cmd) => {
|
|
3439
3578
|
try {
|
|
3440
3579
|
const globalOpts = cmd.optsWithGlobals();
|
|
3441
3580
|
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
@@ -3462,7 +3601,7 @@ function registerSecretsCommand(program2) {
|
|
|
3462
3601
|
outputJson({ name, written: options.output });
|
|
3463
3602
|
return;
|
|
3464
3603
|
}
|
|
3465
|
-
if (options.raw) {
|
|
3604
|
+
if (options.raw || options.valueOnly) {
|
|
3466
3605
|
process.stdout.write(value);
|
|
3467
3606
|
return;
|
|
3468
3607
|
}
|