Authentication
Declare account connections independently of task integrations.
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.
capabilities:
- auth
- watcher
requiredApiVersion: '>=0.11.0'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
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
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.
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.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 and vault and events.