Ctrl K
ComunidadGuías…

commands/secret.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import * as p from "@clack/prompts";
import arg from "arg";
import { z } from "zod";
import {
  configurationApi,
  confirmChange,
  readValueFile,
  resolveTarget,
  scopeArgs,
  targetName,
} from "./_configuration.ts";
import { SUBSCRIPTION_SECRET_NAMES, shadowRefusal } from "./_shared.ts";

export const secretNamesSchema = z.object({
  target: z.string(),
  secrets: z.array(z.string()),
  inherited: z.array(z.string()),
  // `.default([])` so a CLI newer than its server — a preview deployment, a self-hosted
  // install — keeps working: every command parses this shape, not just `auth`.
  overrides: z.array(z.object({ name: z.string(), repo: z.string() })).default([]),
  writable: z.boolean(),
});

export async function runCli(input: { args: string[]; prog: string; showHelp?: boolean }) {
  const args = arg({ ...scopeArgs, "--file": String, "--yes": Boolean }, { argv: input.args });
  if (input.showHelp || args["--help"] || !args._.length) {
    console.log(`usage: ${input.prog} secret <list|set|delete> [NAME...]

  list                 list Pullfrog-stored secret names, never values
  set NAME [NAME...]    prompt for masked values; save the group together
  delete NAME          remove a secret from the selected scope

  --repo, -R OWNER/REPO   select a repository (default: GitHub origin)
  --org OWNER            select shared organization or personal-account secrets
  --file PATH            value for one secret; - reads stdin
  --yes                  acknowledge deleting a secret

values are never accepted as command arguments or printed.
use auth codex, auth claude or auth grok for subscription credentials.`);
    return;
  }
  const command = args._[0];
  const names = args._.slice(1);
  if (!["list", "set", "delete"].includes(command))
    throw new Error("expected secret list, set or delete");
  if (names.some((name) => !/^[A-Z][A-Z0-9_]{0,63}$/.test(name)))
    throw new Error(
      "secret names must be uppercase alphanumeric with underscores, max 64 characters"
    );
  if (new Set(names).size !== names.length) throw new Error("duplicate secret name");
  const target = resolveTarget({ org: args["--org"], repo: args["--repo"] });
  const data = secretNamesSchema.parse(await configurationApi({ target, path: "credentials" }));
  if (command === "list") {
    if (names.length || args["--file"] !== undefined) throw new Error("usage: secret list");
    console.log(data.target);
    for (const name of data.secrets) console.log(`${name}  [${target.repo ? "repo" : "org"}]`);
    for (const name of data.inherited)
      console.log(`${name}  [org${data.secrets.includes(name) ? ", overridden" : ", inherited"}]`);
    if (!data.secrets.length && !data.inherited.length) console.log("no Pullfrog-stored secrets");
    return;
  }
  if (!data.writable)
    throw new Error(
      target.repo ? "repo admin required to change secrets" : "org owner required to change secrets"
    );
  if (command === "delete") {
    if (names.length !== 1 || args["--file"] !== undefined)
      throw new Error("usage: secret delete NAME");
    if (!data.secrets.includes(names[0])) {
      throw new Error(
        `secret is not stored on ${data.target}${target.repo && data.inherited.includes(names[0]) ? "; use --org to manage the inherited secret" : ""}`
      );
    }
    await confirmChange({
      message: `${targetName(target)}: delete ${names[0]}${target.repo && data.inherited.includes(names[0]) ? " (the org credential will apply instead)" : ""}`,
      yes: args["--yes"] === true,
    });
    await configurationApi({
      target,
      path: "credentials",
      method: "DELETE",
      body: { name: names[0], confirmed: true },
    });
  } else {
    if (!names.length || names.length > 64) throw new Error("usage: secret set NAME [NAME...]");
    const file = args["--file"];
    if (file !== undefined && names.length !== 1)
      throw new Error("--file requires exactly one secret name");
    if (file === undefined && !process.stdin.isTTY)
      throw new Error("use --file PATH or --file - without a terminal");
    // the other door to the same account-level write `auth` guards. checked before any value
    // is prompted for, so nobody types a credential that would land in a shadow.
    for (const name of names.filter((secret) => SUBSCRIPTION_SECRET_NAMES.includes(secret))) {
      const refusal = shadowRefusal({ overrides: data.overrides, owner: target.owner, name });
      if (refusal) throw new Error(refusal);
    }
    const secrets: { name: string; value: string }[] = [];
    for (const name of names) {
      const value =
        file === undefined
          ? await p.password({
              message: `${data.target}: ${name}${data.secrets.includes(name) ? " (replace)" : ""}`,
            })
          : await readValueFile(file);
      if (p.isCancel(value)) throw new Error("canceled; nothing changed");
      if (!value.trim() || Buffer.byteLength(value.trim(), "utf8") > 49152)
        throw new Error("secret must contain 1..49152 bytes");
      secrets.push({ name, value });
    }
    await configurationApi({ target, path: "credentials", method: "PUT", body: { secrets } });
  }
  console.log(`${data.target}: ${names.join(", ")} ${command === "delete" ? "deleted" : "saved"}`);
}