{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://docs.unoverse.ai/schemas/nodes/credential.schema.json",
  "title": "Unoverse credential type",
  "description": "credentials/<name>.yaml \u2014 the SHAPE of a credential, never its value. Declared once per package and referenced by name from each node.yaml.\n\nSecrets stay where they already live: encrypted in Postgres via credentialManager on a deployed universe, and in the developer's own .env locally (LOCAL_STUDIO.md). Only the schema is data in a manifest, which is why a manifest is safe to commit and to deploy.",
  "type": "object",
  "required": [
    "name",
    "displayName",
    "properties"
  ],
  "properties": {
    "$schema": {
      "type": "string"
    },
    "name": {
      "type": "string",
      "pattern": "^[a-z][A-Za-z0-9]*$",
      "description": "camelCase id, e.g. openAICredential. Nodes reference this exact string, and lint checks every reference resolves."
    },
    "displayName": {
      "type": "string"
    },
    "description": {
      "type": "string",
      "description": "Where the developer gets this credential. A URL to the vendor's key page saves more time than any other field here."
    },
    "documentationUrl": {
      "type": "string",
      "format": "uri"
    },
    "properties": {
      "type": "array",
      "minItems": 1,
      "description": "The fields making up this credential. A node reads the whole credential as an object, e.g. {{ credentials.openAICredential.apiKey }}.",
      "items": {
        "type": "object",
        "required": [
          "name",
          "displayName",
          "secret"
        ],
        "properties": {
          "name": {
            "type": "string",
            "pattern": "^[a-z][A-Za-z0-9_]*$",
            "description": "The FIELD name, as a manifest reads it: credentials.<credentialName>.<fieldName>.\n\ncamelCase by convention. snake_case is ALLOWED, for exactly the reason a PORT name may be snake_case (_defs.schema.json): a migrated node must keep the names its code version used. A credential is read field by field out of stored, encrypted values \u2014 Cloudinary's are cloud_name, api_key and api_secret \u2014 so tidying them to camelCase would leave every existing workflow authenticating with undefined, and it would fail as a vendor 401 rather than as anything naming the rename. Fidelity beats house style here.\n\nPrefer camelCase for a NEW credential, where nothing is stored yet."
          },
          "displayName": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "type": {
            "enum": [
              "string",
              "number",
              "boolean",
              "select",
              "connect"
            ],
            "default": "string",
            "description": "`select` is a CHOICE: the person picks one of `options`, which renders as a dropdown. `connect` is a BUTTON: the person is sent to the provider's own screen to approve, and the fields this credential needs are filled by what comes back rather than typed. Anything else is a box."
          },
          "placeholder": {
            "type": "string",
            "description": "Shape hint shown in the empty field, e.g. \"sk-...\". Never a real value."
          },
          "required": {
            "type": "boolean",
            "default": true
          },
          "secret": {
            "type": "boolean",
            "default": true,
            "description": "REQUIRED. Encrypted at rest, masked in the UI, never returned to a client once saved. Optional was how a Google API key and a Cloudinary API key ended up in the database in plaintext (docs/auth-security/SECURITY.md, 2026-08-30): an omission read as false and nothing warned. Set false ONLY for a genuinely non-sensitive field such as a region or an account id, and say why."
          },
          "default": {
            "type": "string"
          },
          "env": {
            "type": "string",
            "description": "Override the .env variable name local Studio looks for. Defaults to the derived <NAME>_<FIELD>, e.g. OPENAI_API_KEY. Set this when the vendor's conventional variable name differs."
          },
          "options": {
            "type": "array",
            "minItems": 2,
            "description": "The choices of a `select` field, in the order they are offered. `name` is what the person reads, `value` is what is stored and what a manifest reads (`credentials.<credential>.<field>`).",
            "items": {
              "type": "object",
              "required": [
                "name",
                "value"
              ],
              "properties": {
                "name": {
                  "type": "string"
                },
                "value": {
                  "type": [
                    "string",
                    "number",
                    "boolean"
                  ]
                }
              },
              "additionalProperties": false
            }
          },
          "ui:dependencies": {
            "type": "object",
            "minProperties": 1,
            "description": "Show this field only when another field of the same credential holds a value: `{ provider: gmail }` shows it for one choice, `{ provider: [gmail, outlook] }` for any of several, and several keys are ANDed. The same word, with the same rule, as a node's config schema. A hidden field is not asked for, and `required` is not enforced on it.",
            "additionalProperties": {
              "type": [
                "string",
                "number",
                "boolean",
                "array"
              ]
            }
          },
          "connect": {
            "type": "string",
            "description": "A `connect` field's door: the path the form posts to for the URL to send the browser to, e.g. `/mail/connect`. When `providers` is given, the chosen one's id is appended. The service's own consent screen is the UI; the platform never asks for a password."
          },
          "connected": {
            "type": "string",
            "description": "A `connect` field's evidence: the name of the field that holds a value once the connection succeeded, so the form can show what is connected instead of the button. Usually the account it connected to."
          },
          "providers": {
            "type": "array",
            "minItems": 1,
            "description": "The services this can connect to, each drawn as its own button \u2014 the pick-your-service screen a phone shows when you add an account. Omit it for a field with one destination.",
            "items": {
              "type": "object",
              "required": [
                "id",
                "label"
              ],
              "additionalProperties": false,
              "properties": {
                "id": {
                  "type": "string",
                  "description": "Appended to `connect` to name the door, e.g. `google`."
                },
                "label": {
                  "type": "string",
                  "description": "What the button says, in the service's own name: Gmail, Outlook."
                }
              }
            }
          }
        },
        "additionalProperties": false
      }
    },
    "category": {
      "type": "string",
      "description": "Where this sits on the credentials screen, e.g. \"Communication\". Without it the screen guesses from the name, which is how a one-time platform setup ends up filed beside the thing people make many of. Use \"Platform setup\" for a credential an operator saves once for everyone."
    },
    "visibility": {
      "enum": [
        "public",
        "internal"
      ],
      "default": "public",
      "description": "`internal` keeps it out of the credentials picker. For a credential nobody CHOOSES: a platform's own registration with a service, collected once in the place that needs it. It still exists, is still encrypted, and is still read by name."
    }
  },
  "additionalProperties": false
}
