@tinycloud/cli 0.7.0-beta.5 → 0.7.0-beta.7

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
@@ -10990,6 +10990,7 @@ async function loadManifestPermissions(source, profile) {
10990
10990
  )
10991
10991
  };
10992
10992
  });
10993
+ permissions.push(...await secretPermissionsFromAppManifest(manifest, profile));
10993
10994
  return resolvePermissionSpaces(permissions, profile);
10994
10995
  }
10995
10996
  throw new CLIError(
@@ -10998,6 +10999,46 @@ async function loadManifestPermissions(source, profile) {
10998
10999
  ExitCode.USAGE_ERROR
10999
11000
  );
11000
11001
  }
11002
+ async function secretPermissionsFromAppManifest(manifest, profile) {
11003
+ if (manifest.secrets === void 0) {
11004
+ return [];
11005
+ }
11006
+ const resolved = resolveManifest({
11007
+ app_id: String(manifest.app_id),
11008
+ name: typeof manifest.name === "string" ? manifest.name : String(manifest.app_id),
11009
+ defaults: false,
11010
+ prefix: "",
11011
+ secrets: manifest.secrets
11012
+ });
11013
+ const permissions = resolved.resources.filter(
11014
+ (resource) => resource.service === "tinycloud.kv" && resource.space === "secrets" && resource.path.startsWith("vault/secrets/")
11015
+ );
11016
+ const needsDecrypt = permissions.some(
11017
+ (permission) => permission.actions.includes("tinycloud.kv/get")
11018
+ );
11019
+ if (needsDecrypt) {
11020
+ permissions.push({
11021
+ service: ENCRYPTION_PERMISSION_SERVICE,
11022
+ space: ENCRYPTION_MANIFEST_SPACE,
11023
+ path: await defaultSecretsNetworkId(profile),
11024
+ actions: ["tinycloud.encryption/decrypt"],
11025
+ skipPrefix: true
11026
+ });
11027
+ }
11028
+ return permissions;
11029
+ }
11030
+ async function defaultSecretsNetworkId(profileName) {
11031
+ const profile = await ProfileManager.getProfile(profileName);
11032
+ const ownerDid = (profile.ownerDid ?? profile.did)?.split("#")[0];
11033
+ if (!ownerDid) {
11034
+ throw new CLIError(
11035
+ "OWNER_DID_UNKNOWN",
11036
+ `Cannot determine owner DID for profile "${profileName}". Run \`tc auth login\` first.`,
11037
+ ExitCode.AUTH_REQUIRED
11038
+ );
11039
+ }
11040
+ return `urn:tinycloud:encryption:${ownerDid}:default`;
11041
+ }
11001
11042
  function permissionsFromDelegation(delegation) {
11002
11043
  if (delegation.resources?.length) {
11003
11044
  return delegation.resources.map((resource) => ({
@@ -13877,6 +13918,30 @@ function secretPermissionEntries(params) {
13877
13918
  }
13878
13919
  return permissions;
13879
13920
  }
13921
+ function formatSecretScopeFlag(options) {
13922
+ return options?.scope ? ` --scope ${JSON.stringify(options.scope)}` : "";
13923
+ }
13924
+ function outputSecretDoctor(result) {
13925
+ if (shouldOutputJson()) {
13926
+ outputJson(result);
13927
+ return;
13928
+ }
13929
+ process.stderr.write(formatSection("Secrets") + "\n");
13930
+ for (const check of result.checks) {
13931
+ process.stdout.write(formatCheck(check.ok, check.name, check.detail) + "\n");
13932
+ if (check.hint) {
13933
+ process.stdout.write(` ${theme.hint(check.hint)}
13934
+ `);
13935
+ }
13936
+ }
13937
+ process.stdout.write("\n");
13938
+ if (result.healthy) {
13939
+ process.stdout.write(theme.success("Secrets checks passed.") + "\n");
13940
+ } else {
13941
+ const failed = result.checks.filter((check) => check.ok === false).length;
13942
+ process.stdout.write(theme.warn(`${failed} secrets check${failed > 1 ? "s" : ""} need attention.`) + "\n");
13943
+ }
13944
+ }
13880
13945
  function registerSecretsCommand(program2) {
13881
13946
  const secrets = program2.command("secrets").description("Encrypted secrets management");
13882
13947
  const network = secrets.command("network").description("Manage the default secrets encryption network");
@@ -13918,6 +13983,93 @@ function registerSecretsCommand(program2) {
13918
13983
  handleError(error);
13919
13984
  }
13920
13985
  });
13986
+ secrets.command("doctor [name]").description("Check secrets setup and optional secret access").option("--scope <scope>", "Logical secret scope").option("--space <scope>", "Deprecated alias for --scope").option("--network <name>", "Encryption network name", "default").option("--private-key <hex>", "Ethereum private key (or set TC_PRIVATE_KEY)").action(async (name, options, cmd) => {
13987
+ try {
13988
+ const globalOpts = cmd.optsWithGlobals();
13989
+ const ctx = await ProfileManager.resolveContext(globalOpts);
13990
+ const node = await ensureSecretsNode(ctx, options);
13991
+ const networkName = options.network ?? "default";
13992
+ const networkId = networkName.startsWith("urn:tinycloud:encryption:") ? networkName : node.getDefaultEncryptionNetworkId(networkName);
13993
+ const descriptor = await withSpinner(
13994
+ "Checking secrets encryption network...",
13995
+ () => node.getEncryptionNetwork(networkName)
13996
+ );
13997
+ const checks = [
13998
+ descriptor ? {
13999
+ name: "Encryption network",
14000
+ ok: descriptor.state === "active" ? true : "warn",
14001
+ detail: `${networkName} (${descriptor.state})`
14002
+ } : {
14003
+ name: "Encryption network",
14004
+ ok: false,
14005
+ detail: `${networkName} not found`,
14006
+ hint: `tc secrets network init ${networkName}`
14007
+ }
14008
+ ];
14009
+ let secret;
14010
+ if (name) {
14011
+ const scopeOptions = resolveSecretScope(options);
14012
+ const resolved = resolveSecretPath2(name, scopeOptions);
14013
+ const result = await runSecretOperation({
14014
+ ctx,
14015
+ node,
14016
+ action: "get",
14017
+ name,
14018
+ scopeOptions,
14019
+ label: `Checking secret ${name}...`,
14020
+ operation: () => node.secrets.get(name, scopeOptions)
14021
+ });
14022
+ if (result.ok) {
14023
+ secret = {
14024
+ name: resolved.name,
14025
+ path: resolved.permissionPaths.vault,
14026
+ ...resolved.scope ? { scope: resolved.scope } : {},
14027
+ exists: true,
14028
+ readable: true
14029
+ };
14030
+ checks.push({
14031
+ name: "Secret access",
14032
+ ok: true,
14033
+ detail: `${resolved.permissionPaths.vault} readable`
14034
+ });
14035
+ } else {
14036
+ const notFound = result.error.code === "NOT_FOUND" || result.error.code === "KEY_NOT_FOUND";
14037
+ secret = {
14038
+ name: resolved.name,
14039
+ path: resolved.permissionPaths.vault,
14040
+ ...resolved.scope ? { scope: resolved.scope } : {},
14041
+ exists: !notFound,
14042
+ readable: false
14043
+ };
14044
+ checks.push({
14045
+ name: "Secret access",
14046
+ ok: false,
14047
+ detail: notFound ? `${resolved.permissionPaths.vault} not found` : result.error.message,
14048
+ hint: notFound ? `tc secrets put ${resolved.name}${formatSecretScopeFlag(scopeOptions)} <value>` : "Ask the owner profile to grant tinycloud.kv/get and tinycloud.encryption/decrypt."
14049
+ });
14050
+ }
14051
+ } else {
14052
+ checks.push({
14053
+ name: "Secret access",
14054
+ ok: "warn",
14055
+ detail: "skipped; pass a secret name to verify read access"
14056
+ });
14057
+ }
14058
+ outputSecretDoctor({
14059
+ healthy: checks.every((check) => check.ok !== false),
14060
+ network: {
14061
+ name: networkName,
14062
+ networkId,
14063
+ exists: descriptor !== null,
14064
+ ...descriptor?.state ? { state: descriptor.state } : {}
14065
+ },
14066
+ ...secret ? { secret } : {},
14067
+ checks
14068
+ });
14069
+ } catch (error) {
14070
+ handleError(error);
14071
+ }
14072
+ });
13921
14073
  secrets.command("list").description("List secrets").option("--scope <scope>", "Logical secret scope").option("--space <scope>", "Deprecated alias for --scope").option("--private-key <hex>", "Ethereum private key (or set TC_PRIVATE_KEY)").action(async (options, cmd) => {
13922
14074
  try {
13923
14075
  const globalOpts = cmd.optsWithGlobals();