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 integration at http://localhost:8080. You can point a CI HUB development environment at this URL to test your integration 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. For stricter compile-time types on adapterData, per-handler requestData, and OAuth state, pass a type config to defineIntegration. See Typed Integrations.
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 integration-specific section keyed by your integration name:
{
"serverBaseUrl": "http://localhost:8080",
"myIntegration": {
"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('myIntegration.clientId') // returns the value or throws if missing
config.get('myIntegration.maxQuerySize') // nested keys work the same way
config.has('myIntegration.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('myIntegration.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 = ['myIntegration.clientId', 'myIntegration.contact', 'myIntegration.authEndpoint', 'myIntegration.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 integration 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 npm scripts that wrap the CLI:
| Script | Command | Description |
|---|---|---|
dev | npm run dev | Start the local development server |
The CLI (integration-cli) ships inside @ci-hub/integration-sdk and is not installed as a global binary. The dev script runs it for you; for any other command, use your package manager:
npx integration-cli dev
npx integration-cli auth login
npx integration-cli config --init
npx integration-cli submitpnpm exec integration-cli dev
pnpm exec integration-cli auth login
pnpm exec integration-cli config --init
pnpm exec integration-cli submityarn integration-cli dev
yarn integration-cli auth login
yarn integration-cli config --init
yarn integration-cli submitbunx integration-cli dev
bunx integration-cli auth login
bunx integration-cli config --init
bunx integration-cli submitFor the full command reference (dev, auth, config, submit), see the CLI reference.