CI HUBCI HUB SDK
Building an Integration

Direct Access Helper

A Direct Access Helper (DAH) is a browser-side upload module that lets CI HUB clients upload files directly to the provider API, bypassing the CI HUB server proxy. Without a DAH, every file upload flows through the CI HUB server (Browser → CI HUB Server → Provider API). With a DAH, the browser talks to the provider directly (Browser → Provider API).

This eliminates the server as a bottleneck for large files — no double transfer, no server memory pressure, no request timeouts on multi-GB uploads. A DAH can implement createAsset (new file upload), updateAsset (new version of existing file), or both. It runs inside the end-user's browser, receives an axios instance and auth credentials, and makes HTTP calls to the provider on its own.

When you need one

A DAH is required for all new integrations. Submissions without one will be rejected unless you can demonstrate that your provider's API cannot support browser-to-API uploads.

Beyond the submission requirement, a DAH is especially valuable when:

  • Your provider supports direct browser-to-API uploads (pre-signed URLs, chunked uploads, multipart, etc.)
  • File sizes may exceed what the CI HUB server proxy can comfortably handle (typically >100 MB)
  • You want to show real upload progress to the user (the DAH receives an onProgress callback)
  • You want to reduce upload latency by cutting the server hop

How it works

  1. You write a TypeScript file (e.g. src/dah.ts) that default-exports a DirectAccessHelper object with createAsset and/or updateAsset.
  2. In your integration entry file, call buildDAH(path.join(import.meta.dirname, 'dah.ts')) — it uses Vite internally to bundle your file into a self-contained IIFE string. Any packages or helper functions you import in your DAH file get bundled automatically.
  3. Pass the result to capabilities.directAccessHelper in your integration definition.
  4. The SDK test UI evaluates the string in the browser and shows a "Direct Access" button next to the normal "Create Asset" / "Update Asset" buttons.

Building your DAH

Call buildDAH in your integration entry file. It takes the path to your DAH source file and an optional define map for build-time config injection:

import path from 'node:path'
import { defineIntegration, buildDAH } from '@ci-hub/integration-sdk'
import * as queries from './queries.js'

const dahString = await buildDAH(path.join(import.meta.dirname, 'dah.ts'), {
  'process.env.GRAPH_BASE_ENDPOINT': JSON.stringify(config.get('myProvider.graphBaseEndpoint')),
  'process.env.UPLOAD_FILE_MUTATION': JSON.stringify(queries.uploadFileMutation),
  'process.env.REGION_API_URLS': {
    'qa-us': config.get('myProvider.regions.qa-us.apiUrl'),
    'prod-us': config.get('myProvider.regions.prod-us.apiUrl'),
    'prod-eu': config.get('myProvider.regions.prod-eu.apiUrl')
  }
})

defineIntegration({
  name: 'my-integration',
  version: '1.0.0',
  capabilities: {
    category: 'DAM',
    directAccessHelper: dahString,
    // ...
  },
  // ... handlers
})

buildDAH takes your source file, bundles it with Vite into a browser-ready IIFE expression, and returns the string. You can:

  • Import packages — any import in your DAH file gets bundled (e.g. a provider SDK, a hashing library).
  • Use TypeScript — full type checking with the exported DahFunction, DahHelpers, DahData, DirectAccessHelper types. Pass a DahTypeConfig generic for typed adapterData and data parameters. See DAH typing.
  • Inject config values at build time — pass replacements via the second argument; references like process.env.GRAPH_BASE_ENDPOINT in your DAH source get inlined.

Values are passed to Vite's define option, which does textual substitution. Always wrap string values in JSON.stringify(...) — otherwise multi-line strings (e.g. GraphQL queries) and special characters will produce Invalid define value (must be an entity name or JS literal) build errors.

Entry file requirements

The DAH entry file (dah.ts) must be a plain ES module that default-exports the helper object. No top-level IIFE wrapping, no top-level side effects.

import type { DirectAccessHelper, DahFunction } from '@ci-hub/integration-sdk'

const createUpdateAsset: DahFunction = async (axios, payload, _sessionId, data, onProgress, _adapterData, _accessToken, helpers) => {
  // ...
}

export default {
  createAsset: createUpdateAsset,
  updateAsset: createUpdateAsset
} satisfies DirectAccessHelper

Do not wrap the entry in an IIFE:

// WRONG — top-level IIFE produces a "double-wrapped" bundle whose value is undefined when eval'd
(function () {
  const createUpdateAsset = async (...) => {...}
  return { createAsset: createUpdateAsset, updateAsset: createUpdateAsset }
})()

Vite needs real ES exports to know what to surface from the bundled IIFE. Without export default, you'll either get an "empty chunk" warning, or a bundle that returns undefined from eval, and the host will silently fall back to the non-DAH upload path.

Function signature

import type { DahFunction } from '@ci-hub/integration-sdk'

const createAsset: DahFunction = async (
  axios,                // axios 0.19.2 instance
  payload,              // the raw access token (provider credentials)
  sessionId,            // CI HUB session ID (empty string in SDK test mode)
  data,                 // { parentId, name, file, comment, createAssetOptions, ... }
  onProgress,           // callback: pass a fraction 0..1 (e.g. 0.5 = 50%)
  adapterData,          // adapter-specific data from login (e.g. { resourceEndpoint })
  providerAccessToken,  // JWT string wrapping payload + adapterData
  helpers               // utility functions (hashing, token getters, etc.)
) => {
  // ... upload logic ...
  return { id: 'new-asset-id', name: 'filename.png' }
}

All eight parameters are always provided. You only use the ones your provider needs.

Input types

data (DahData)

FieldTypeWhen present
parentIdstringcreateAsset — target folder ID
assetIdstringupdateAsset — asset to update
namestringfilename
commentstringoptional version comment
fileFile | Blobthe raw file bytes (not base64)
createAssetOptionsRecord<string, string>provider-specific create options
updateAssetOptionsRecord<string, string>provider-specific update options
statusInfoPrefixstringoptional progress label prefix

helpers (DahHelpers)

HelperReturnsNotes
calcMd5Hash(blob)Promise<string>MD5 hex — not available in SDK test mode (Web Crypto limitation), returns ''
calcSha1Hash(blob)Promise<string>SHA-1 base64
calcSha256Hash(blob)Promise<string>SHA-256 hex
getPayload()unknownthe raw provider access token
getAdapterData()Record<string, unknown>adapter data from login
getAccessToken()stringCI HUB platform access token
getProviderAccessToken()stringJWT wrapping payload + adapterData
getSessionId()stringCI HUB session ID
getRequestHeaders()Record<string, string>extra headers from provider info
getBase64FromString(str)stringbase64-encode a UTF-8 string

axios

The DAH receives axios@0.19.2 (not the latest). This is intentional — the host depends on the 0.19.x API shape. See the axios 0.19.x documentation for the API of that version.

Output type

DahResult

type DahResult = {
  id: string
  name: string
  sessionId?: string
  userMsg?: string
  data?: { id: string; name: string }
}
FieldRequiredDescription
idyesthe created/updated asset ID
nameyesthe asset filename
sessionIdnoupdated session ID (if the provider rotates sessions)
userMsgnomessage to display to the user
datanoalternative nesting — { id, name } is unwrapped automatically

Both flat ({ id, name }) and nested ({ data: { id, name } }) return shapes are accepted. The SDK normalizes them.

Full example

src/dah.ts — the DAH entry file:

import type { DahFunction, DirectAccessHelper } from '@ci-hub/integration-sdk'

const createUpdateAsset: DahFunction = async (
  axios,
  payload,
  _sessionId,
  data,
  onProgress,
  _adapterData,
  _providerAccessToken,
  helpers
) => {
  const file = data.file!
  const fileSize = file.size
  const isUpdate = !!data.assetId

  const { data: uploadInfo } = await axios.post(
    'https://api.my-provider.com/uploads/init',
    {
      fileName: data.name,
      parentId: data.parentId,
      assetId: data.assetId,
      contentHash: await helpers.calcSha256Hash(file)
    },
    {
      headers: { Authorization: `Bearer ${payload}` }
    }
  )

  await axios.put(uploadInfo.uploadUrl, file, {
    headers: { 'Content-Type': 'application/octet-stream' },
    maxBodyLength: Infinity,
    onUploadProgress: (event) => {
      onProgress(event.loaded / fileSize)
    }
  })

  const { data: asset } = await axios.post(
    'https://api.my-provider.com/uploads/confirm',
    { uploadId: uploadInfo.id },
    { headers: { Authorization: `Bearer ${payload}` } }
  )

  return { id: asset.id, name: asset.name }
}

export default {
  createAsset: createUpdateAsset,
  updateAsset: createUpdateAsset
} satisfies DirectAccessHelper

src/index.ts — wiring the DAH into your integration:

import path from 'node:path'
import { defineIntegration, buildDAH } from '@ci-hub/integration-sdk'

const dahString = await buildDAH(path.join(import.meta.dirname, 'dah.ts'))

defineIntegration({
  name: 'my-provider',
  version: '1.0.0',
  capabilities: {
    category: 'DAM',
    directAccessHelper: dahString,
  },
  // ... handlers
})

Limitations

The DAH runs as a self-contained IIFE inside the end-user's browser. That shapes everything below.

Runtime environment

  • Browser-only, not Node. No fs, path, Node crypto, Buffer, or process (except keys you inject via define). Don't import Node-only packages (e.g. mime-types, fs-extra). Use Web platform APIs instead: fetch, crypto.subtle, URL, Blob, File, btoa, atob, TextEncoder, etc.
  • Strict mode is always on. Vite wraps the IIFE body with "use strict". Implicit globals, with, octal literals produce errors.
  • No top-level await. The IIFE is synchronous. Keep await inside createUpdateAsset. (async function itself is fine.)
  • No dynamic import() or require(). The bundle has to be fully self-contained. Anything you need must be imported statically and bundled in.
  • No CSS / asset imports. Don't import './style.css' or import logo from './logo.png' — these produce extra chunks that don't fit the single-string eval model.
  • No import.meta.url / import.meta.dirname. Module URLs don't exist inside an eval'd IIFE.

CORS

The provider's API must allow cross-origin requests from every host that runs the CI HUB panel. If the provider doesn't support CORS at all, a DAH won't work.

Allowed origins your provider needs to whitelist:

https://app-aagcgsdkbdg.canva-apps.com
https://app-aagppabnsou.canva-apps.com

https://live.ci-hub.com
https://stage.ci-hub.com
https://dev.ci-hub.com

https://ci-hub.azurewebsites.net
https://ci-hub-test.azurewebsites.net
https://ci-hub-beta.azurewebsites.net

http://localhost:8080
https://localhost:8080

Fixed function signature

You cannot change the parameter list — the host always calls:

directAccessHelper[action](
  axios,         // pre-configured axios@0.19.2 instance — use THIS, do not import your own
  payload,       // provider access token (string)
  sessionId,     // string
  data,          // { parentId?, assetId?, name, comment?, file, createAssetOptions?, updateAssetOptions? }
  onProgress,    // (fraction: number) => void
  adapterData,   // opaque provider state from login
  providerToken, // JWT wrapping payload + adapterData
  helpers        // see "helpers" table above
)
  • Only createAsset and updateAsset are callable. Anything else on the default export is ignored by current consumers.
  • Use the passed-in axios. It has the right interceptors / headers / requestHeaders injection. Importing your own bloats the bundle and defeats the integration.
  • axios version is 0.19.2 — not the latest. Don't rely on features added after 0.19.x (signal, AbortController integration, native ESM exports). See the axios 0.19.x README for the API of that version.

Bundle output contract

buildDAH produces a single IIFE expression that, when eval'd, returns the helper object. For this to work:

  • The entry file must export default { createAsset, updateAsset } (see Entry file requirements).
  • The default export must be a plain object literal — not the result of a function call, factory, or dynamic computation. Anything you put at the top level runs once at eval time, not on every createAsset invocation.
  • Unused imports and dead code are tree-shaken away. Don't reference helpers via dynamic string keys (obj['my' + 'Helper']()); the bundler may not see the reference and remove it.

define / process.env

  • Only the keys you pass to buildDAH's second argument exist at runtime. Referencing a process.env.X that wasn't injected throws ReferenceError when the DAH runs.
  • define is a textual replacement. Always JSON.stringify string values. Objects/arrays passed inline are auto-stringified by Vite, but anything you compute manually must be valid JSON.

Bundle size

Everything you import is inlined into the IIFE string, then shipped over the wire as part of capabilities.directAccessHelper, parsed, and eval'd in the browser. Large provider SDKs can blow up the auth/providers payload and slow the panel cold-start.

  • Prefer tiny, focused utility libs (sub-1 KB).
  • Hand-roll helpers when feasible (the SDK's helpers.calcSha1Hash / calcSha256Hash / getBase64FromString already cover common cases).
  • Don't accidentally drag your main index.ts into the DAH — see Best practices below.

Host helpers vary between consumers

The helpers object is provided by whoever evaluates the DAH. There are two consumers today:

ConsumerMD5getAccessTokengetSessionId
Production CI HUB Pluginsreal (CryptoJS)CI HUB platform tokenreal session ID
SDK test UIreturns '' (Web Crypto has no MD5)same as getPayload()returns ''

If your DAH genuinely needs MD5 or a separate platform access token, the SDK test UI will not match production behavior exactly.

Best practices

Architecture

  1. Split shared logic into a small dedicated file. If your createAsset / updateAsset share helpers (e.g. an upload helper, a folder-ID normalizer), put them in a small file like src/dah-utils.ts that both the DAH and your main index.ts import. Don't import from index.ts itself — Vite will follow the import graph and pull half your integration into the DAH bundle.

    src/
      index.ts        ← integration entry (Node-side)
      dah.ts          ← imports only from dah-utils.ts
      dah-utils.ts    ← tiny, browser-safe, shared
      queries.ts      ← GraphQL strings, importable from both
  2. One function for both create and update. If your create and update logic is similar, write a single createUpdateAsset and export it as both. Branch on data.assetId (truthy = update).

  3. Export only one if that's all you support. If your provider only supports direct upload for create, just export createAsset. The SDK UI disables the unsupported direct-access button and surfaces a Problems-panel hint.

Configuration

  1. Inject server-side config via define. Pass values to buildDAH's second argument and read them as process.env.X inside the DAH:

    const dahString = await buildDAH(path.join(import.meta.dirname, 'dah.ts'), {
      'process.env.REGION_API_URLS': {
        'qa-us': REGIONS['qa-us'].apiUrl,
        'prod-us': REGIONS['prod-us'].apiUrl,
        'prod-eu': config.get('myIntegration.regions.prod-eu.apiUrl')
      },
      'process.env.GRAPH_BASE_ENDPOINT': JSON.stringify(config.get('myIntegration.graphBaseEndpoint'))
    })
  2. Wrap every string value in JSON.stringify. GraphQL queries, URLs, IDs — anything that's a plain string at build time. Objects passed inline don't need this.

  3. Use helpers.getAdapterData() for per-connection data. Values returned by your login handler in adapterData (per-tenant endpoints, region selection, etc.) are runtime — read them via helpers.getAdapterData() instead of define.

Runtime

  1. Use payload as the bearer token. The 2nd argument is the raw provider access token from your login handler. Most providers just need Authorization: Bearer ${payload}.

  2. Set maxContentLength: Infinity on axios calls that upload file data — axios 0.19 rejects large payloads otherwise.

  3. Report progress for large uploads. Call onProgress(loaded / total) inside axios's onUploadProgress callback. The UI shows a progress bar only when you report it.

  4. Test with real files. The SDK test UI passes a real File / Blob to your DAH, not a base64 string. Use data.file.size, data.file.name, and data.file.type as needed.

  5. Don't rely on MD5 in the SDK UI. If you need MD5 for testing, run against the production Vue client instead, or temporarily substitute SHA-1/SHA-256 in your DAH to verify the upload flow end-to-end.

Common pitfalls

  • Wrapping the entry file in (function() { ... return {...} })(). Use export default { ... } instead.
  • Passing un-stringified strings to define — produces "Invalid define value" build error.
  • Importing axios directly inside dah.ts — causes bundle bloat and wrong interceptors. Use the injected axios parameter.
  • Importing from index.ts or any Node-only file — either a Vite build error, or a giant bundle.
  • Top-level side effects like fetching config, creating clients, etc. They run once at eval time, not per call. Move them inside the handler.

See Upload | Capabilities Reference | CLI Config | Bynder Example

On this page