# Authentication (/docs/capabilities/auth)

The `auth` capability lets an addon offer account connections within a workspace. It can accompany any other capability. Redmine uses `auth` and `dataSource`; Discord uses `auth` and `watcher`. A theme that only changes colors uses `theme` alone.

```yaml
capabilities:
  - auth
  - watcher
requiredApiVersion: '>=0.11.0'
```

## How it works [#how-it-works]

The manifest declares that the addon connects accounts. During activation, its code registers connection profiles through `contributions.connections.registerProfiles` and authentication methods through `registerAuthenticator`. Both operations return `Either`: check the result and stop activation on failure. Without `auth`, the application refuses these registrations.

A profile defines how many accounts can connect and which authentication methods it accepts. An authenticator implements communication with the external service, using credentials or OAuth. If authentication is unnecessary, omit `auth` and the authenticator; use addon or workspace settings.

Mr. Tick manages the selected account, authorization attempts, renewal, and credential storage. Each addon implements the specifics of its external service. Declaring `auth` does not make an addon a datasource or provide security isolation for its code.

## Registration [#registration]

```ts
import { type AddonContext, type AppError, type Either } from '@mr-tick/sdk'

export function registerProfiles(context: AddonContext): Either<AppError, void> {
  return context.contributions.connections.registerProfiles([
    {
      connectionProfileId: 'example-account',
      cardinality: 'multi',
      default: true,
      allowedAuthProfileIds: ['example-token'],
      defaultAuthProfileId: 'example-token',
    },
  ])
}
```

`connectionProfileId` identifies a connection model; `authProfileId` identifies an authentication implementation. Profiles can declare allowed authenticators, defaults and authorization roles. `single` limits a profile within a workspace; `multi` permits separate instances.

Every addon that connects accounts declares `auth` in its manifest and registers profiles with `contributions.connections.registerProfiles`, including datasources. Profile and authenticator details belong in code; registering or removing a datasource does not change those contributions. Both registration operations return `Either`; handle failure before continuing activation.

## Credentials authenticator [#credentials-authenticator]

The example creates an authenticator for a service whose `/me` endpoint returns non-empty strings for `id` and `name`. The validation functions check the input fields and HTTP response; authentication then uses the validated types. Pass an HTTP client that returns failures as `Either`.

```ts
import {
  AppError,
  Either,
  type AddonConnectionAuthenticator,
  type IHttpClient,
  type JsonValue,
} from '@mr-tick/sdk'

type Account = { id: string; name: string }

function isNonEmptyString(value: JsonValue | undefined): value is string {
  return typeof value === 'string' && value.trim().length > 0
}

function isAccount(value: JsonValue): value is Account {
  if (value === null || typeof value !== 'object' || Array.isArray(value))
    return false

  return isNonEmptyString(value.id) && isNonEmptyString(value.name)
}

export function createAuthenticator(
  http: IHttpClient,
): AddonConnectionAuthenticator {
  return {
    method: 'credentials',
    authProfileId: 'example-token',
    supportsRefresh: false,
    supportsRevocation: false,
    async authenticate(credentials, configuration) {
      const apiUrl = configuration.apiUrl
      const apiToken = credentials.apiToken

      if (!isNonEmptyString(apiUrl))
        return Either.failure(AppError.ValidationError('API_URL_REQUIRED'))

      if (!isNonEmptyString(apiToken))
        return Either.failure(AppError.ValidationError('API_TOKEN_REQUIRED'))

      const client = http.withConfig({
        baseURL: apiUrl,
        headers: { Authorization: 'Bearer ' + apiToken },
      })

      const account = await client.get<Account>('/me', {
        responseValidator: isAccount,
      })

      if (account.isFailure()) return account.forwardFailure()

      return Either.success({
        kind: 'authorized',
        identity: {
          kind: 'human',
          providerId: 'example',
          tenantId: apiUrl,
          providerSubjectId: account.success.id,
          displayName: account.success.name,
        },
        credentials: { apiToken },
      })
    },
  }
}
```

Register the returned authenticator with `registerAuthenticator`. Authentication returns `kind: authorized`, identity and credentials; it does not need a `MemberDTO`. Keep provider subject IDs in the provider domain.

## Core lifecycle [#core-lifecycle]

`core.connections` exposes `create`, `submitCredentials`, `createAuthorizationRequest`, `completeAuthentication`, `refresh`, `disconnect`, `archive`, `restore` and `remove`. Use explicit `workspaceId`, `connectionInstanceId` and, where needed, `authorizationRole`. Handle `Either<AppError, T>` results.

`list` and `get` expose projections without secrets. `withCredentials(scope, operation)` gives credentials to the addon callback only. The core persists connection credentials and coordinates renewal. Do not copy tokens into workspace-wide vault keys, expose them to the renderer or log them. Keychain unavailability is not credential absence.

Refresh-capable OAuth authenticators implement `refreshToken` returning `Either<TokenRefreshError, OAuthRenewalResult>`. Advertise remote revocation only with `supportsRevocation: true` and a working `revokeToken`.

See [OAuth PKCE](/docs/apis/oauth-pkce) and [vault and events](/docs/apis/vault-and-events).
