# Build a CredentialProvider

> Implement, test, package, and privately deploy custom credential lifecycle code.

This tutorial creates a provider package that mints short-lived material from one bootstrap Credential. Provider code executes as an ordinary private Driver with declared operations, egress, configuration, and recovery behavior.

## What you will build {#outcome}

The finished package will:

- accept a non-secret service URL during provider setup;
- bind one `bootstrap-token` Credential;
- accept a per-Credential account identifier;
- mint and verify short-lived tokens;
- expose setup metadata for an intuitive Console form.

## Project files {#project-files}

```text
constal.credential-provider.json
package.json
schema.json
index.ts
test/provider.test.ts
```

Use an exact `@constal/sdk` dependency and do not include `node_modules`, lifecycle scripts, or development dependencies in the archive.

## Define the provider {#define-provider}

```ts index.ts
import { credentialProvider } from "@constal/sdk";

export default credentialProvider({
  id: "example-token",
  version: "1.0.0",
  displayName: "Example service token",
  description: "Creates short-lived tokens for an Example account.",
  documentationUrl: "https://example.com/docs/constal",
  setup: {
    service: "Example",
    useCase: "Run unattended Example automation.",
    credentialSlots: {
      "bootstrap-token": {
        title: "Bootstrap token",
        description: "Create this in Example administration settings.",
        input: "secret"
      }
    }
  },
  credentialSlots: ["bootstrap-token"],
  configSchema: {
    type: "object",
    required: ["clientId"],
    additionalProperties: false,
    properties: {
      clientId: { type: "string", title: "Application client ID" }
    }
  },
  credentialConfigSchema: {
    type: "object",
    required: ["accountId"],
    additionalProperties: false,
    properties: {
      accountId: { type: "string", title: "Account ID" }
    }
  },
  rotation: {
    mode: "auto", intervalMs: null, overlapMs: 60000,
    maxAgeMs: null, refreshBeforeMs: 300000
  },
  egress: { rules: [{
    operations: ["mint"], origin: "https://api.example.com",
    pathPrefix: "/oauth/tokens", methods: ["POST"]
  }] },
  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: 65536,
      maximumRedirects: 0
    });
    if (response.status !== 201) throw new Error("Example token exchange failed");
    const token = JSON.parse(atob(response.bodyBase64));
    return {
      material: token.access_token,
      expiresAt: Date.now() + token.expires_in * 1000
    };
  }
});
```

Never declare secret-shaped fields in `configSchema`. Secrets belong in named bootstrap Credential slots.

## Test the contract {#test}

Test schema rejection, every operation result, retry classification, expiry handling, provider-private state, and egress boundaries. Use fixed clocks and protocol fixtures; do not call the real provider in unit tests.

## Package and upload {#package}

Create a ZIP or tar.gz whose root contains the manifest, exact package file, schema, entrypoint, and source. In **Add provider**, choose **Install a custom provider**, upload the archive or provide a public repository plus full commit SHA. The managed builder type-checks, bundles, probes, and publishes the package privately to the tenant catalog.

## Verify {#verify}

After the build completes, the Console should continue directly into provider setup. Confirm that schema titles become fields, slot help is visible, secret material is collected separately, and the installed provider can create one test Credential.

Continue with [Configuration schemas](/docs/credentials/providers/schemas.md), [Authorization lifecycle](/docs/credentials/providers/authorization.md), and [Package format](/docs/credentials/providers/package-format.md).
