CI HUBCI HUB SDK
Getting Started

Installation

Prerequisites

  • Node.js 20.19+ or 22.12+
  • TypeScript 5 or later
  • An existing DAM or content platform with an API you can call

The fastest way to get started is the create command. It scaffolds a ready-to-run TypeScript project with the SDK pre-configured:

npm create @ci-hub/integration my-integration
pnpm create @ci-hub/integration my-integration
yarn create @ci-hub/integration my-integration
bun create @ci-hub/integration my-integration

CLI options

FlagDescription
--overwriteRemove existing files in the target directory before scaffolding
--installAuto-install dependencies (default: interactive prompt)
--no-installSkip dependency installation

After scaffolding, start the development server:

cd my-integration
npm run dev

The SDK starts a local server and exposes your adapter at http://localhost:8080. You can point a CI HUB development environment at this URL to test your adapter end to end.

Manual install

If you prefer to set up a project from scratch:

npm install @ci-hub/integration-sdk
pnpm add @ci-hub/integration-sdk
yarn add @ci-hub/integration-sdk
bun add @ci-hub/integration-sdk

TypeScript configuration

The scaffolded project includes a tsconfig.json. If you're setting up manually, your config should have at minimum:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "esModuleInterop": true
  },
  "include": ["src/**/*"]
}

The SDK ships with full type definitions — you get autocomplete and type checking for all handler signatures, locals, requestData, and response helpers.

The full defineIntegration skeleton

Below is the complete list of handlers you can implement. Start with the ones your platform supports and add more over time. Handlers you don't implement are simply omitted.

Every handler inside defineIntegration is fully typed — the SDK infers the correct locals and requestData types for each handler, so you don't need to annotate them manually.

import {
  defineIntegration,
  send, sendResult, sendError, sendStatus, sendAccessToken, sendRedirectUri,
  defaultFolderCapabilities, defaultAssetCapabilities,
  config, fs, path, CategoryEnum,
} from '@ci-hub/integration-sdk'

const imageBase64 = (name: string) =>
  `data:image/png;base64,${Buffer.from(fs.readFileSync(path.join(import.meta.dirname, name))).toString('base64')}`

const logo  = { data: imageBase64('logo.png'),  width: 600, height: 134, backgroundColor: '#FFFFFF' }
const glyph = { data: imageBase64('glyph.png'), width: 80,  height: 80,  backgroundColor: '#FFFFFF' }

export default defineIntegration({
  name: 'My DAM',
  version: '1.0.0',
  logo,
  glyph,
  contact: config.get('myIntegration.contact'),

  capabilities: {
    category: CategoryEnum.DAM,
    assetUploadLimitInMB: 500,
    directAssetDownload: true,
    assetSearch: {
      supportsParentId: true,
    },
    description: {
      en: 'Connect to My DAM from CI HUB.',
    },
  },

  // ─── Auth (required) ─────────────────────────────────────────────────────
  login:        async (locals, requestData) => { /* ... */ },
  logout:       async (locals, requestData) => { /* ... */ },
  checkToken:   async (locals)              => { /* ... */ },
  refreshToken: async (locals)              => { /* ... */ }, // optional — OAuth only

  // ─── Content (implement what your platform supports) ──────────────────────
  search:      async (locals, requestData) => { /* ... */ },
  getFolder:   async (locals, requestData) => { /* ... */ },
  download:    async (locals, requestData) => { /* ... */ },
  createAsset: async (locals, requestData) => { /* ... */ },
  updateAsset: async (locals, requestData) => { /* ... */ },
  deleteAsset: async (locals, requestData) => { /* ... */ },

  // ─── Folders ─────────────────────────────────────────────────────────────
  createFolder: async (locals, requestData) => { /* ... */ },
  renameFolder: async (locals, requestData) => { /* ... */ },
  deleteFolder: async (locals, requestData) => { /* ... */ },
  moveFolder:   async (locals, requestData) => { /* ... */ },

  // ─── Asset actions ───────────────────────────────────────────────────────
  lockAsset:        async (locals, requestData) => { /* ... */ },
  renameAsset:      async (locals, requestData) => { /* ... */ },
  moveAsset:        async (locals, requestData) => { /* ... */ },
  getAssetVersions: async (locals, requestData) => { /* ... */ },

  // ─── Tasks ───────────────────────────────────────────────────────────────
  searchTasks:           async (locals, requestData) => { /* ... */ },
  getTask:               async (locals, requestData) => { /* ... */ },
  getTaskAssets:         async (locals, requestData) => { /* ... */ },
  addTaskComment:        async (locals, requestData) => { /* ... */ },
  addTaskAsset:          async (locals, requestData) => { /* ... */ },
  deleteTaskAsset:       async (locals, requestData) => { /* ... */ },
  updateCustomTaskState: async (locals, requestData) => { /* ... */ },

  // ─── Brand Hub ───────────────────────────────────────────────────────────
  getBrandConfig: async (locals, requestData) => { /* ... */ },
  getBrandAssets: async (locals, requestData) => { /* ... */ },

  // ─── Advanced ────────────────────────────────────────────────────────────
  doTransformation:    async (locals, requestData) => { /* ... */ },
  additionalRendition: async (locals, requestData) => { /* ... */ },
  saveAssetRelations:  async (locals, requestData) => { /* ... */ },
})

config.json

The SDK reads configuration from config.json in your project root (or src/config.json). Structure it with a top-level serverBaseUrl and an adapter-specific section keyed by your adapter name:

{
  "serverBaseUrl": "http://localhost:8080",
  "myAdapter": {
    "contact": {
      "text": "If you experience any problems, please contact our support team.",
      "url": { "link": "https://your-platform.example.com/help", "email": "support@your-platform.example.com" }
    },
    "authEndpoint": "https://your-platform.example.com/oauth/authorize",
    "tokenEndpoint": "https://your-platform.example.com/oauth/token",
    "clientId": "your-client-id",
    "clientSecret": "your-client-secret",
    "maxQuerySize": 100,
    "maxFileSizeInMB": 5242880
  }
}

Access values with config.get() and config.has() using dot-separated paths:

config.get('myAdapter.clientId')       // returns the value or throws if missing
config.get('myAdapter.maxQuerySize')   // nested keys work the same way
config.has('myAdapter.clientSecret')   // returns boolean, never throws

config.get() throws if the key doesn't exist. Pass a default value as the second argument to avoid the throw: config.get('myAdapter.timeout', 30000).

Config validation

Validate required config keys at the top of your index.ts, before defineIntegration. This fails fast on startup instead of at runtime when a handler is called:

{
  const requiredKeys = ['myAdapter.clientId', 'myAdapter.contact', 'myAdapter.authEndpoint', 'myAdapter.tokenEndpoint']
  const missing = requiredKeys.filter(k => !config.has(k))
  if (missing.length) throw new Error(`Missing configuration: ${missing.join(', ')}`)
}

The block scope {} keeps requiredKeys and missing out of module scope.

Project structure

Keep all adapter logic in index.ts. Split only for long static data: GraphQL query strings (queries.ts), static filter/rendition lists (renditions.ts), or .ect login templates.

Available scripts

The scaffolded project includes these npm scripts:

ScriptCommandDescription
devnpm run devStart the local development server

On this page