Build Credential Providers

Define reusable lifecycle code for imported, minted, rotating, or interactively acquired Credentials.

Before you begin

Separate provider installation settings, bootstrap Credential slots, and per-Credential configuration. Determine whether the external service supports deterministic minting, single-use refresh, verification, source-side destruction, or guided acquisition. Declare only the operations the integration can implement safely.

Steps

  1. Export credentialProvider() with human metadata, a non-secret installation schema, per-Credential schema, rotation defaults, and bounded egress. This complete example exchanges a provider bootstrap secret for short-lived consumer material:
ts
import { credentialProvider } from "@constal/sdk";

export default credentialProvider({
  id: "example-token", version: "1.0.0",
  displayName: "Example service token",
  description: "Creates a short-lived token for one Example account.",
  credentialSlots: ["bootstrap-token"],
  configSchema: { type: "object", required: ["clientId"], additionalProperties: false,
    properties: { clientId: { type: "string", title: "Client ID" } } },
  credentialConfigSchema: { type: "object", required: ["accountId"], additionalProperties: false,
    properties: { accountId: { type: "string", title: "Account ID" } } },
  rotation: { mode: "auto", intervalMs: null, overlapMs: 60_000,
    maxAgeMs: null, refreshBeforeMs: 300_000 },
  egress: { rules: [{ operations: ["mint"], origin: "https://api.example.com",
    pathPrefix: "/oauth/tokens", methods: ["POST"] }] },
  mintRecovery: { kind: "outcome-unknown" },
  async mint(request, context) {
    const config = context.config as { clientId: string };
    const credential = request.configuration as { accountId: string };
    const bootstrap = await context.secret("bootstrap-token");
    const response = await context.egress({
      url: "https://api.example.com/oauth/tokens", method: "POST",
      headers: { authorization: `Bearer ${bootstrap.value}`, "content-type": "application/json" },
      bodyBase64: btoa(JSON.stringify({ client_id: config.clientId, account_id: credential.accountId })),
      maximumResponseBytes: 65_536, maximumRedirects: 0,
    });
    if (response.status !== 201) throw new Error("Example token exchange failed");
    const token = JSON.parse(atob(response.bodyBase64)) as { access_token?: string; expires_in?: number };
    if (!token.access_token || !token.expires_in) throw new Error("Example token response is invalid");
    return { material: token.access_token, expiresAt: Date.now() + token.expires_in * 1_000 };
  },
});
  1. Use context.secret(slot) only for declared bootstrap Credentials and context.egress() only for allowlisted requests. Consumer material and provider-private lifecycle state have separate encrypted lifecycles.
  2. Add the provider manifest, exact SDK dependency, and root schema.json; deploy the package privately.
  3. Install the catalog package into a namespace, configure bootstrap slots, then create Credentials from the installed provider. Add interaction.start and interaction.advance when the provider needs redirects, forms, device instructions, callbacks, approval, or polling.

Verify

Test schemas, every action/event transition, callback state, proof verification, replay rejection, expiry, poll limits, refresh ambiguity, verification, destruction, and egress denial. Confirm APIs return fingerprints and lifecycle metadata but never material or private state.

Next steps

Use the complete Build a CredentialProvider tutorial, Interaction lifecycle, and CredentialProvider SDK reference.