Build Credential Providers

Define reusable lifecycle code for imported, minted, rotating, or browser-authorized 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 browser authorization. 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 or authorize Credentials from the installed provider. Add authorization.begin and authorization.complete only when the provider needs an interactive browser flow.

Verify

Test schemas, begin/complete state, PKCE, callback replay rejection, expiry, 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, Authorization lifecycle, and CredentialProvider SDK reference.