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
Scaffold a new project (recommended)
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-integrationpnpm create @ci-hub/integration my-integrationyarn create @ci-hub/integration my-integrationbun create @ci-hub/integration my-integrationCLI options
| Flag | Description |
|---|---|
--overwrite | Remove existing files in the target directory before scaffolding |
--install | Auto-install dependencies (default: interactive prompt) |
--no-install | Skip dependency installation |
After scaffolding, start the development server:
cd my-integration
npm run devThe 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-sdkpnpm add @ci-hub/integration-sdkyarn add @ci-hub/integration-sdkbun add @ci-hub/integration-sdkTypeScript 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 throwsconfig.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:
| Script | Command | Description |
|---|---|---|
dev | npm run dev | Start the local development server |