@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 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
- if (isInteractive()) {
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
- try {
758
- const open = (await import("open")).default;
759
- await open(authUrl);
760
- } catch {
761
- server.close();
762
- throw new Error("Failed to open browser");
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, options.paste);
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
- if (expectedSpaces.size > 0 && (expectedSpaces.size !== 1 || !expectedSpaces.has(returnedSpace))) {
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 handleLocalAuth(profileName, host) {
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" && profile.privateKey && profile.address) {
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
- await ProfileManager.setProfile(profileName, {
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
- outputJson({
2201
- authenticated: true,
2202
- profile: profileName,
2203
- did,
2204
- sessionDid,
2205
- address,
2206
- spaceId: sessionResult.spaceId,
2207
- authMethod: "local"
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, paste) {
2211
- const { profile, delegationData } = await refreshOpenKeySession(profileName, host, { paste });
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 result = await withSpinner(`Getting ${key}...`, () => node.kv.get(key));
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...", () => node.kv.list(listOptions));
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, _options, cmd) => {
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 result = await withSpinner(`Checking ${key}...`, () => node.kv.head(key));
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 withSpinner(params.label, params.operation);
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 withSpinner(params.label, params.operation);
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
  }