CI HUBCI HUB SDK
Building an Adapter

Info Handler

Called after successful login. Returns adapter metadata and optional pre-configuration that shapes the plugin UI for this connection.

info?: (locals: Locals) => HandlerResult<ProviderInfoResponse>

CI HUB calls info once after a successful login (and after checkToken on reconnect). The response configures filters, locale pickers, upload options, and other UI elements for the duration of the session.

Response

Return via sendResult:

return sendResult(null, {
  remoteSystemPrefix: 'dam.example.com',
  // all other fields optional
})

remoteSystemPrefix (required)

Base URL hostname of the remote system. CI HUB uses this to build "open in source system" links on assets. Typically derived from the user's connection endpoint:

const remoteSystemPrefix = (locals: Locals) =>
  new URL(locals.adapterData.resourceEndpoint).hostname

filters

Pre-search filters — dropdown menus shown in the plugin UI before the user triggers a search. Set once at login, not updatable during search. Use only for always-applicable filters like "Library" or "Search Operation".

Each filter has an id, name, and options array. Option IDs use the filterId:value convention:

const filters = [
  {
    id: 'library',
    name: 'Library',
    options: [
      { id: 'library:all', name: 'All Libraries' },
      { id: 'library:123', name: 'Brand Assets' },
      { id: 'library:456', name: 'Marketing' },
    ]
  }
]

Options support default: true to pre-select, isDisabled to gray out, and isActive for toggling. Supports i18nName for localized display names.

These filters are static for the session. For filters that change based on search results (facets), use dynamic filters in the search handler.

dataLocales

Locale picker options shown in the plugin UI. Each locale has an id, optional name/displayName, and an optional default flag:

const dataLocales = [
  { id: 'en', name: 'en', default: true },
  { id: 'de', name: 'de' },
  { id: 'fr', name: 'fr' },
]

The selected locale is passed to subsequent handler calls (e.g. search) so the adapter can return localized metadata.

createAssetOptions / updateAssetOptions

Extra form fields shown in the plugin UI during upload (create) or re-upload (update). Each option defines an input with type select, string, or date:

const createAssetOptions = [
  {
    id: 'metadataTemplateId',
    name: 'Metadata Template',
    options: [
      { id: 'metadataTemplateId:tmpl_1', name: 'Product Photography' },
      { id: 'metadataTemplateId:tmpl_2', name: 'Marketing Campaign' },
    ]
  },
  {
    id: 'securityTemplateId',
    name: 'Security Template',
    options: [
      { id: 'securityTemplateId:sec_1', name: 'Internal Only' },
      { id: 'securityTemplateId:sec_2', name: 'Public' },
    ]
  }
]

The user's selection is passed to the createAsset/updateAsset handler in requestData.body.

nativeFolderOrder

If true, CI HUB displays folders in the order returned by the provider API instead of sorting alphabetically. Use when the source system has meaningful ordering (custom sort, priority-based).

customMetadata

Metadata field definitions used for detail panels and filter configurations. Contains groups (grouping headers) and fields (individual metadata fields, optionally assigned to a group via groupId).

See Custom Metadata for full details.

searchConfigs

Multiple search mode configurations. Adds configuration dropdowns to the search UI. searchQueryRequired: true prevents searching without a query string.

const searchConfigs = {
  searchQueryRequired: false,
  configs: [
    {
      id: 'single:searchMode',
      name: 'Search Mode',
      i18nName: { en: 'Search Mode' },
      type: 'select',
      options: [
        { id: 'all', name: 'All', isActive: false },
        { id: 'any', name: 'Any', isActive: false },
        { id: 'exact', name: 'Exact', isActive: false },
        { id: 'default', name: 'Default', isActive: true },
      ]
    }
  ]
}

requestHeaders

Custom HTTP headers the CI HUB client sends when making direct requests to asset URLs (thumbnails, downloads). Use when the provider requires authentication or cache-control headers on asset URLs:

requestHeaders: {
  'X-AdmiralCloud-ClientId': 'your-client-key',
  'Cache-Control': 'no-cache'
}

transformation

Transformation config for adapters that support on-the-fly asset transformations (e.g. Cloudinary). Contains URLs for fetching available transformations and executing them:

const transformation = {
  availableTransformationsUrl: `${adapterBaseUrl}/getAvailableTransformations`,
  actionUrl: `${adapterBaseUrl}/doTransformation`
}

showPdfPreviewOptions

If true, shows PDF preview options in the plugin UI.

assetSearchHelpUrl

URL linking to external documentation about the provider's search syntax. Shown as a help link in the search UI.

Patterns

Example - Filters from API data

Fetches brand libraries from the API and builds a pre-search filter:

info: async (locals) => {
  try {
    const { brandId } = locals.adapterData
    const libraries = await callApi(locals, { query: queries.getGetBrandLibrariesQuery, variables: { brandId, page: 1, limit: 100 } })
    const libraryFilter = {
      id: 'library',
      name: 'Library',
      options: [{ id: 'library:all', name: 'All Libraries' }].concat(libraries.data.brand.libraries.items.map((library: any) => ({ id: `library:${library.id}`, name: library.name })))
    }

    return sendResult(null, { remoteSystemPrefix: remoteSystemPrefix(locals), filters: [ libraryFilter ] })
  } catch (error) {
    return sendError(`Error getting info: ${error}`, 400)
  }
}

Example - createAssetOptions from metadata templates

Fetches metadata and security templates from the API, builds upload form options dynamically:

info: async (locals) => {
  try {
    const createAssetOptions = []
    const filters = [
      {
        id: 'searchOperation',
        name: 'Operation',
        options: [
          { id: 'operation:AND', name: 'AND' },
          { id: 'operation:OR', name: 'OR' }
        ]
      },
      {
        id: 'searchField',
        name: 'Search Field',
        options: [
          { id: 'keywordSearchField:all', name: 'Everything', default: true },
          { id: 'keywordSearchField:filename', name: 'File Name' },
          { id: 'keywordSearchField:assetId', name: 'Asset ID' },
          { id: 'keywordSearchField:metadata', name: 'Metadata' },
          { id: 'keywordSearchField:content', name: 'Content' }
        ]
      }
    ]
    const metadata = await callApi(locals, 'GET', '/v1/metadata/template')
    const security = await callApi(locals, 'GET', '/v1/security/template')
    const optionalMetadata = metadata.filter(templateTypeFilter).filter(nonRequired)
    if (optionalMetadata.length > 0) {
      createAssetOptions.push({
        id: 'metadataTemplateId',
        name: 'Metadata Template',
        options: optionalMetadata.map(item => ({
          id: `metadataTemplateId:${item.templateId}`,
          name: item.templateName
        }))
      })
    }
    if (security?.length > 0) {
      createAssetOptions.push({
        id: 'securityTemplateId',
        name: 'Security Template',
        options: security?.map(item => ({
          id: `securityTemplateId:${item.templateId}`,
          name: item.templateName
        }))
      })
    }

    return sendResult(null, { remoteSystemPrefix: remoteSystemPrefix(locals), filters, createAssetOptions })
  } catch (error) {
    return sendError(error, 400)
  }
}

Example - dataLocales and customMetadata

Fetches available locales from the API and builds custom metadata field definitions:

info: async (locals) => {
  let customMetadata
  let dataLocales

  const availableLocales = await getAvailableLocales(locals, locals.authPayload)

  dataLocales = availableLocales.supportedLocals.
    split(',').
    map(locale => locale.trim()).
    map(locale => ({
      id: locale,
      name: locale,
      default: locale === availableLocales.defaultLocale
    }))

  try {
    customMetadata = { fields: [], groups: [] }
    const uiLocale = await getUiLocale(locals, locals.authPayload)
    const customFieldClient = await createSoapClient(locals && locals.authPayload, 'apiCustomFieldService')
    const customFieldsMapping = await getCustomFieldMapping(customFieldClient)

    customMetadata.fields = [
      { id: 'assetType', name: translate('File Category', uiLocale) },
      { id: 'mediaTypeName', name: translate('Asset Type', uiLocale) },
      { id: 'assetAvailability', name: translate('Asset Availability', uiLocale) }
    ]

    customFieldsMapping.forEach(customFieldMap => {
      const { id: groupId, name: groupName } = customFieldMap.attributes

      customMetadata.groups.push({ id: groupId, name: groupName })
      customFieldMap.CustomField.forEach(customField => {
        const fieldId = customField.attributes.id
        const fieldName = getCustomFieldName(customFieldsMapping, fieldId, uiLocale)

        customMetadata.fields.push({ id: `${fieldId}`, name: fieldName, groupId })
      })
    })
  } catch {
    // Suppress errors — custom metadata is optional
  }

  return sendResult(null, {
    remoteSystemPrefix: remoteSystemPrefix(locals.authPayload),
    showPdfPreviewOptions: true,
    dataLocales,
    customMetadata
  })
}

UI Behavior

FieldPlugin UI Effect
remoteSystemPrefix"Open in source" links on assets
filtersDropdown menus above search results (static for session)
dataLocalesLanguage picker in the plugin toolbar
createAssetOptionsExtra form fields in the upload dialog
updateAssetOptionsExtra form fields in the re-upload dialog
nativeFolderOrderFolders keep provider ordering instead of A–Z sort
customMetadataMetadata panels on asset details, filter options
searchConfigsConfiguration dropdowns in the search bar
requestHeadersSent with every direct asset URL request
transformationEnables transformation UI (Cloudinary)
showPdfPreviewOptionsShows PDF preview toggles

Info Schema | Custom Metadata

On this page