CI HUBCI HUB SDK
Building an Adapter

Auth Handlers

Three handlers that complete the auth lifecycle after login: checkToken, refreshToken, and logout.

checkToken

Called before every session resume — CI HUB verifies the stored access token is still valid.

Make an API call to the platform's "current user" endpoint. On success, return user info via send(). On failure, return sendError() to force re-login.

Inputs

FieldDescription
locals.authPayloadStored access token (string or object)
locals.adapterDataAdapter-specific data persisted from login

Response

// Success — session resumes, plugin UI shows user display name
return send({ adapter: 'AdapterName', source: 'hostname-or-label', user: 'displayName' })

// Failure — forces re-login
return sendError(err, 400)

The source field appears in the plugin UI connection list. Typically derived from the server URL hostname.

Patterns

Example - OAuth adapter, validates token via REST call:

checkToken: async (locals) => {
  try {
    const data = await callApi(locals, 'GET', '/api/v4/currentUser/', {})

    return send({ adapter: 'ExampleAdapter', source: remoteSystemPrefix(locals), user: data.username })
  } catch (err) {
    return sendError(err, 400)
  }
},

Example - OAuth adapter, validates via SDK client:

checkToken: async (locals) => {
  const accessToken = locals.authPayload

  sstk.setAccessToken(accessToken)

  try {
    const usersApi = new sstk.UsersApi()
    const user = await usersApi.getUser()

    return send({ adapter: 'ExampleAdapter', source: remoteSystemPrefix, user: user.username })
  } catch (err) {
    return sendError(err, 400)
  }
},

Example - Credential-based adapter (API key/secret), no API call needed:

checkToken: async (locals) => {
  try {
    const cloudName = locals.authPayload.cloudName

    return send({ adapter: 'ExampleAdapter', source: remoteSystemPrefix, user: cloudName })
  } catch (err) {
    return sendError(err, 400)
  }
},

For credential-based adapters where the token is an object (not an OAuth access token), extract the display name directly — no API validation needed.

UI Behavior

On success, the plugin UI restores the previous session and shows the user value in the connection header. On failure, CI HUB drops the session and shows the login screen.


refreshToken

Called when checkToken fails and a stored refresh token exists. Exchange the refresh token for a new access token.

Return the new token via sendAccessToken(). Return sendError() to abandon refresh and force re-login.

Inputs

FieldDescription
locals.authPayloadStored refresh token
locals.adapterDataAdapter-specific data persisted from login

Response

// Success — new access token stored, session resumes
return sendAccessToken(null, accessToken, expiresIn, refreshToken, refreshExpiresIn, adapterData)

// Failure — forces re-login
return sendError(err, 400)

Parameter order for sendAccessToken: error, token, expiresIn, refreshToken, refreshExpiresIn, adapterData.

Patterns

Example - Standard OAuth refresh with adapterData preservation:

refreshToken: async (locals) => {
  const adapterData = locals.adapterData

  if (!adapterData.resourceEndpoint) {
    return sendError('Could not refresh token, please re-add the connection.', 400)
  }

  try {
    const oauth = getAuthClient(adapterData.resourceEndpoint)

    const accessToken = oauth.createToken({
      refresh_token: locals.authPayload
    })
    const { token } = await accessToken.refresh()

    if (!token || !token.access_token) {
      throw new Error('No access_token found')
    }

    return sendAccessToken(null, token.access_token, token.expires_in, undefined, undefined, adapterData)
  } catch (err) {
    return sendError(err, 400)
  }
},

Validate adapterData before attempting refresh — stale sessions missing required fields should error immediately with a clear message.

Example - OAuth refresh with client credentials:

refreshToken: async (locals) => {
  const clientKey = config.get('exampleAdapter2.clientKey')
  const clientSecret = config.get('exampleAdapter2.clientSecret')
  const refreshToken = locals.authPayload
  const oauth = getAuthClient()

  const token = oauth.createToken({
    refresh_token: refreshToken
  })

  try {
    const newToken = (await token.refresh({
      client_id: clientKey,
      client_secret: clientSecret
    })).token

    const accessToken = newToken.access_token
    const tokenExpiresIn = newToken.expires_in

    return sendAccessToken(null, accessToken, tokenExpiresIn)
  } catch (err) {
    return sendAccessToken(err)
  }
},

Example - Credential-based adapter, no actual refresh:

refreshToken: async (locals) => {
  try {
    const token = locals.authPayload
    const adapterData = locals.adapterData

    return sendAccessToken(null, token, null, null, null, adapterData)
  } catch (err) {
    return sendError(err, 400)
  }
},

Credential-based adapters (API key + secret) have no expiring OAuth token. Re-send the same token and adapterData to keep the session alive.

UI Behavior

On success, the session resumes transparently — the user never sees a login screen. On failure, CI HUB falls back to the login flow.


logout

Called when the user disconnects from the adapter in the plugin UI. Revoke tokens if the platform supports it, then return sendStatus(200).

CI HUB deletes all stored tokens regardless of the handler's response.

Inputs

FieldDescription
locals.authPayloadStored access token
requestData.body.refresh_tokenStored refresh token (if available)

Response

// Always return 200
return sendStatus(200)

Patterns

Example - Revokes refresh token via OAuth client:

logout: async (locals, requestData) => {
  const access_token = locals.authPayload
  const { refresh_token } = requestData.body

  try {
    const oauth = getAuthClient()

    const token = oauth.createToken({ access_token, refresh_token })

    await token.revoke('refresh_token')

    return sendStatus(200)
  } catch (err) {
    return sendError(err, 400)
  }
},

Example - No revocation endpoint, return immediately:

logout: async (locals, requestData) => {
  return sendStatus(200)
},

Example - No revocation, wrapped in try/catch:

logout: async (locals, requestData) => {
  try {
    return sendStatus(200)
  } catch (err) {
    return sendError(err, 400)
  }
},

If the platform has no token revocation endpoint, sendStatus(200) is sufficient. CI HUB cleans up stored tokens on its side.

UI Behavior

After logout completes, the plugin UI removes the connection and returns to the adapter selection or login screen.


Auth Flow Sequence


Check Token Schema | Refresh Token Schema | Logout Schema

On this page