@tinycloud/cli 0.6.0-beta.9 → 0.6.1-beta.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,11 +1416,14 @@ function registerAuthCommand(program2) {
1402
1416
  handleError(error);
1403
1417
  }
1404
1418
  });
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) => {
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) => {
1406
1420
  try {
1407
1421
  const globalOpts = cmd.optsWithGlobals();
1408
1422
  const ctx = await ProfileManager.resolveContext(globalOpts);
1409
- await rotateAuthKey(ctx.profile, ctx.host, { paste: options.paste });
1423
+ await rotateAuthKey(ctx.profile, ctx.host, {
1424
+ paste: options.paste,
1425
+ noPopup: options.popup === false
1426
+ });
1410
1427
  } catch (error) {
1411
1428
  handleError(error);
1412
1429
  }
@@ -1468,7 +1485,7 @@ function registerAuthCommand(program2) {
1468
1485
  ).option("--permission <file>", 'JSON permission request: { "permissions": PermissionEntry[] }').option("--manifest <fileOrBase64>", "Manifest file, base64:<json>, or raw base64 JSON").option(
1469
1486
  "--expiry <duration>",
1470
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.`
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) => {
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) => {
1472
1489
  try {
1473
1490
  const globalOpts = cmd.optsWithGlobals();
1474
1491
  const ctx = await ProfileManager.resolveContext(globalOpts);
@@ -1513,7 +1530,8 @@ function registerAuthCommand(program2) {
1513
1530
  host: ctx.host,
1514
1531
  permissions: group,
1515
1532
  openkeyHost,
1516
- expiry: expiryOption
1533
+ expiry: expiryOption,
1534
+ noPopup: options.popup === false
1517
1535
  });
1518
1536
  const delegation = portableFromOpenKeyDelegation(delegationData, group, ctx.host);
1519
1537
  const stored = storedAdditionalDelegation(delegation, group);
@@ -1929,7 +1947,7 @@ function normalizePortableDelegation(delegation) {
1929
1947
  return { ...delegation, expiry };
1930
1948
  }
1931
1949
  async function ensureDelegationAuthority(params) {
1932
- if (params.node.hasRuntimePermissions(params.requested)) return;
1950
+ if (!params.force && params.node.hasRuntimePermissions(params.requested)) return;
1933
1951
  if (params.profile.authMethod === "openkey") {
1934
1952
  const key = await ProfileManager.getKey(params.ctx.profile);
1935
1953
  if (!key) {
@@ -2095,13 +2113,20 @@ function groupPermissionsBySpace(permissions) {
2095
2113
  function isRawPermission(permission) {
2096
2114
  return permission.service === "tinycloud.encryption" && permission.path.startsWith("urn:tinycloud:encryption:");
2097
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
+ }
2098
2122
  function portableFromOpenKeyDelegation(data, permissions, host) {
2099
2123
  const primary = permissions.find((permission) => !isRawPermission(permission)) ?? permissions[0];
2100
2124
  const returnedSpace = String(data.spaceId ?? primary.space ?? "encryption");
2101
2125
  const expectedSpaces = new Set(
2102
2126
  permissions.filter((permission) => !isRawPermission(permission)).map((permission) => permission.space)
2103
2127
  );
2104
- 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) {
2105
2130
  throw new CLIError(
2106
2131
  "OPENKEY_SCOPE_MISMATCH",
2107
2132
  `OpenKey returned delegation for ${returnedSpace}, expected ${Array.from(expectedSpaces).join(", ")}.`,
@@ -2117,7 +2142,7 @@ function portableFromOpenKeyDelegation(data, permissions, host) {
2117
2142
  actions: primary.actions,
2118
2143
  resources: permissions.map((permission) => ({
2119
2144
  service: permission.service.startsWith("tinycloud.") ? permission.service.slice("tinycloud.".length) : permission.service,
2120
- space: permission.space,
2145
+ space: isRawPermission(permission) ? permission.space : returnedSpace,
2121
2146
  path: permission.path,
2122
2147
  actions: [...permission.actions]
2123
2148
  })),
@@ -2178,7 +2203,8 @@ async function rotateAuthKey(profileName, host, options = {}) {
2178
2203
  authMethod: "openkey"
2179
2204
  });
2180
2205
  const result = await refreshOpenKeySession(profileName, host, {
2181
- paste: options.paste
2206
+ paste: options.paste,
2207
+ noPopup: options.noPopup
2182
2208
  });
2183
2209
  outputRotationResult(result.profile, profileName, oldDid, "openkey");
2184
2210
  }
@@ -2278,8 +2304,8 @@ async function handleLocalAuth(profileName, host, options = {}) {
2278
2304
  }
2279
2305
  return { profile: updatedProfile, sessionResult };
2280
2306
  }
2281
- async function handleOpenKeyAuth(profileName, host, paste) {
2282
- const { profile, delegationData } = await refreshOpenKeySession(profileName, host, { paste });
2307
+ async function handleOpenKeyAuth(profileName, host, options = {}) {
2308
+ const { profile, delegationData } = await refreshOpenKeySession(profileName, host, options);
2283
2309
  outputJson({
2284
2310
  authenticated: true,
2285
2311
  profile: profileName,
@@ -2300,6 +2326,7 @@ async function refreshOpenKeySession(profileName, host, options = {}) {
2300
2326
  const profile = await ProfileManager.getProfile(profileName);
2301
2327
  const delegationData = await startAuthFlow(profile.did, {
2302
2328
  paste: options.paste,
2329
+ noPopup: options.noPopup,
2303
2330
  jwk: key,
2304
2331
  host,
2305
2332
  openkeyHost: resolveOpenKeyHost(profile)
@@ -2330,14 +2357,19 @@ async function readStdin2() {
2330
2357
  }
2331
2358
  return Buffer.concat(chunks);
2332
2359
  }
2360
+ async function kvHandle(node, spaceInput, profileName) {
2361
+ const spaceUri = await resolveSpaceUri(spaceInput, profileName);
2362
+ return spaceUri ? node.kvForSpace(spaceUri) : node.kv;
2363
+ }
2333
2364
  function registerKvCommand(program2) {
2334
2365
  const kv = program2.command("kv").description("Key-value store operations");
2335
- 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) => {
2336
2367
  try {
2337
2368
  const globalOpts = cmd.optsWithGlobals();
2338
2369
  const ctx = await ProfileManager.resolveContext(globalOpts);
2339
2370
  const node = await ensureAuthenticated(ctx);
2340
- 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));
2341
2373
  if (!result.ok) {
2342
2374
  if (result.error.code === "KV_NOT_FOUND" || result.error.code === "NOT_FOUND") {
2343
2375
  throw new CLIError("NOT_FOUND", `Key "${key}" not found`, ExitCode.NOT_FOUND);
@@ -2418,13 +2450,14 @@ function registerKvCommand(program2) {
2418
2450
  handleError(error);
2419
2451
  }
2420
2452
  });
2421
- 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) => {
2422
2454
  try {
2423
2455
  const globalOpts = cmd.optsWithGlobals();
2424
2456
  const ctx = await ProfileManager.resolveContext(globalOpts);
2425
2457
  const node = await ensureAuthenticated(ctx);
2458
+ const kv2 = await kvHandle(node, options.space, ctx.profile);
2426
2459
  const listOptions = options.prefix ? { prefix: options.prefix } : void 0;
2427
- const result = await withSpinner("Listing keys...", () => node.kv.list(listOptions));
2460
+ const result = await withSpinner("Listing keys...", () => kv2.list(listOptions));
2428
2461
  if (!result.ok) {
2429
2462
  throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
2430
2463
  }
@@ -2452,12 +2485,13 @@ function registerKvCommand(program2) {
2452
2485
  handleError(error);
2453
2486
  }
2454
2487
  });
2455
- 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) => {
2456
2489
  try {
2457
2490
  const globalOpts = cmd.optsWithGlobals();
2458
2491
  const ctx = await ProfileManager.resolveContext(globalOpts);
2459
2492
  const node = await ensureAuthenticated(ctx);
2460
- 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));
2461
2495
  if (!result.ok) {
2462
2496
  if (result.error.code === "KV_NOT_FOUND" || result.error.code === "NOT_FOUND") {
2463
2497
  outputJson({ key, exists: false, metadata: {} });
@@ -3372,7 +3406,7 @@ async function ensureSecretsNode(ctx, options) {
3372
3406
  return ensureAuthenticated(ctx, auth);
3373
3407
  }
3374
3408
  async function runSecretOperation(params) {
3375
- const first = await withSpinner(params.label, params.operation);
3409
+ const first = await runSecretOperationAttempt(params.label, params.operation);
3376
3410
  if (first.ok || !shouldRequestSecretPermissions(first.error)) {
3377
3411
  return first;
3378
3412
  }
@@ -3394,10 +3428,20 @@ async function runSecretOperation(params) {
3394
3428
  node: params.node,
3395
3429
  requested,
3396
3430
  expiryOption: void 0,
3397
- yes: true
3431
+ yes: true,
3432
+ force: true
3398
3433
  })
3399
3434
  );
3400
- 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
+ }
3401
3445
  }
3402
3446
  function canRequestOwnerPermissions(profile) {
3403
3447
  const posture = resolveProfilePosture(profile);
@@ -3407,6 +3451,21 @@ function shouldRequestSecretPermissions(error) {
3407
3451
  if (error.code !== "PERMISSION_DENIED") return false;
3408
3452
  return /permission|session expired|autosign|capabilit/i.test(error.message);
3409
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
+ }
3410
3469
  function isStoredSessionExpired(session) {
3411
3470
  const record = session;
3412
3471
  const direct = parseDate(record.expiresAt ?? record.expiry ?? record.expirationTime);
@@ -3515,7 +3574,7 @@ function registerSecretsCommand(program2) {
3515
3574
  handleError(error);
3516
3575
  }
3517
3576
  });
3518
- 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) => {
3519
3578
  try {
3520
3579
  const globalOpts = cmd.optsWithGlobals();
3521
3580
  const ctx = await ProfileManager.resolveContext(globalOpts);
@@ -3542,7 +3601,7 @@ function registerSecretsCommand(program2) {
3542
3601
  outputJson({ name, written: options.output });
3543
3602
  return;
3544
3603
  }
3545
- if (options.raw) {
3604
+ if (options.raw || options.valueOnly) {
3546
3605
  process.stdout.write(value);
3547
3606
  return;
3548
3607
  }