Skip to content

Credential Profiles

Credential profiles are reusable named sets of secrets (username/password pairs, SSH keys, API tokens, etc.) that can be assigned to multiple adapter configurations. Secrets are encrypted at rest using the system ENCRYPTION_KEY (AES-256-GCM).

Location: src/services/auth/credential-service.ts
Types: src/lib/core/credentials.ts

Why Use Credential Profiles

Without credential profiles, each adapter configuration stores its own copy of credentials directly. Profiles allow:

  • Reuse: one profile referenced by many adapters
  • Rotation: update credentials in one place, all adapters pick up the change
  • Separation of concerns: admins who manage secrets do not need access to adapter configuration

Credential Types

TypePayload fieldsTypical use
USERNAME_PASSWORDusername, passwordMySQL, PostgreSQL, MSSQL, WebDAV, FTP
SSH_KEYusername, authType (password/privateKey/agent), password?, privateKey?, passphrase?SFTP SSH tunneling
ACCESS_KEYaccessKeyId, secretAccessKeyS3-compatible storage
TOKENtokenAPI tokens, bearer auth
SMTPuser, passwordEmail notification adapter
WEBHOOKurl, authHeader?Webhook notification endpoints
OAUTHclientId, clientSecret, refreshToken?Google Drive, OneDrive OAuth flows

Each type is validated against its Zod schema (CREDENTIAL_SCHEMAS in src/lib/core/credentials.ts) before the payload is encrypted and stored.

Prisma Model

prisma
model CredentialProfile {
  id          String   @id @default(cuid())
  name        String   @unique
  type        String   // CredentialType value
  description String?
  data        String   // AES-256-GCM encrypted JSON payload
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt

  primaryAdapters AdapterConfig[] @relation("PrimaryCredential")
  sshAdapters     AdapterConfig[] @relation("SshCredential")
}

The data field is never returned to API callers in plaintext. List and get responses return a sanitized CredentialProfileShape (no data). The only way to read plaintext secrets is through the explicit reveal endpoint, gated behind CREDENTIALS.REVEAL.

Service API

All functions are named exports from src/services/auth/credential-service.ts.

createCredentialProfile(name, type, data, description?)

Creates a new profile. Validates the payload against the type schema, enforces name uniqueness, encrypts the payload, and persists.

typescript
const profile = await createCredentialProfile(
  "My SFTP credentials",
  "SSH_KEY",
  { username: "backup", authType: "privateKey", privateKey: "-----BEGIN..." },
  "Used for SFTP destination on backup-server"
);
// Returns CredentialProfileShape (no plaintext data)

Throws ValidationError on invalid payload, ConflictError on duplicate name.

listCredentialProfiles(type?)

Returns all profiles, optionally filtered by type. Includes a secretStatus map indicating which payload fields are non-empty — without exposing their values.

typescript
const profiles = await listCredentialProfiles("OAUTH");
// profiles[0].secretStatus => { clientId: true, clientSecret: true, refreshToken: false }

listCredentialProfilesWithCounts(type?)

Same as listCredentialProfiles but includes a usageCount field — the total number of AdapterConfig rows referencing this profile (across both primary and SSH slots).

getCredentialProfile(id)

Returns a single sanitized profile or throws NotFoundError.

getDecryptedCredentialData(id, expectedType?)

Returns the decrypted and parsed payload. Pass expectedType to guard against type mismatches.

Security

Only call this from the backup/restore pipeline (resolveAdapterConfig) or the reveal API endpoint. Never expose the return value to API responses.

updateCredentialProfile(id, updates)

Updates name, data, and/or description. The credential type cannot be changed (doing so would silently invalidate adapters that reference it). Re-validates and re-encrypts the payload if data is updated.

deleteCredentialProfile(id)

Deletes the profile. Throws ConflictError if any adapter still references it in either the primary or SSH slot. Detach adapters first.

getCredentialUsage(id)

Returns which adapters reference this profile and in which slot:

typescript
const usage = await getCredentialUsage(profileId);
// [{ adapterId, name, type, slot: "primary" | "ssh" }]

getReferenceCount(id)

Returns the total number of adapters referencing this profile (primary + SSH slots combined).

Adapter Integration

Adapters declare which credential types they accept via AdapterCredentialRequirements on BaseAdapter.credentials:

typescript
// src/lib/adapters/storage/sftp/index.ts
export const SFTPAdapter: StorageAdapter & BaseAdapter = {
  credentials: {
    primary: "SSH_KEY",       // primary slot accepts SSH_KEY profiles
    primaryOptional: true,    // can operate without a profile (falls back to config values)
    ssh: "SSH_KEY",           // optional SSH tunnel slot
  },
  // ...
};

At runtime, resolveAdapterConfig() reads the assigned credential profile ID(s) from AdapterConfig.primaryCredentialId / AdapterConfig.sshCredentialId, decrypts the payload, and merges it into the resolved config before passing it to the adapter.

Permissions

Access to credential profiles is controlled by the CREDENTIALS permission category:

PermissionAction
credentials:readList profiles and view metadata (no secret values)
credentials:writeCreate and update profiles
credentials:deleteDelete profiles
credentials:revealReveal plaintext payload via the reveal endpoint
typescript
// Server Action guard example
await checkPermission(PERMISSIONS.CREDENTIALS.READ);

secretStatus Map

Rather than exposing secret field values, the service computes a Record<string, boolean> that indicates which fields hold a non-empty value. This lets the UI communicate state (e.g., whether an OAuth profile has been authorized and has a refreshToken) without transmitting secrets:

typescript
// OAUTH profile that has been through the consent flow:
{ clientId: true, clientSecret: true, refreshToken: true }

// OAUTH profile freshly created, not yet authorized:
{ clientId: true, clientSecret: true, refreshToken: false }

Released under the GNU General Public License. | Privacy · Legal Notice