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
onProgresscallback) - You want to reduce upload latency by cutting the server hop
How it works
- You write a TypeScript file (e.g.
src/dah.ts) that default-exports aDirectAccessHelperobject withcreateAssetand/orupdateAsset. - 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. - Pass the result to
capabilities.directAccessHelperin your integration definition. - 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
importin your DAH file gets bundled (e.g. a provider SDK, a hashing library). - Use TypeScript — full type checking with the exported
DahFunction,DahHelpers,DahData,DirectAccessHelpertypes. Pass aDahTypeConfiggeneric for typedadapterDataanddataparameters. See DAH typing. - Inject config values at build time — pass replacements via the second argument; references like
process.env.GRAPH_BASE_ENDPOINTin 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 DirectAccessHelperDo 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)
| Field | Type | When present |
|---|---|---|
parentId | string | createAsset — target folder ID |
assetId | string | updateAsset — asset to update |
name | string | filename |
comment | string | optional version comment |
file | File | Blob | the raw file bytes (not base64) |
createAssetOptions | Record<string, string> | provider-specific create options |
updateAssetOptions | Record<string, string> | provider-specific update options |
statusInfoPrefix | string | optional progress label prefix |
helpers (DahHelpers)
| Helper | Returns | Notes |
|---|---|---|
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() | unknown | the raw provider access token |
getAdapterData() | Record<string, unknown> | adapter data from login |
getAccessToken() | string | CI HUB platform access token |
getProviderAccessToken() | string | JWT wrapping payload + adapterData |
getSessionId() | string | CI HUB session ID |
getRequestHeaders() | Record<string, string> | extra headers from provider info |
getBase64FromString(str) | string | base64-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 }
}| Field | Required | Description |
|---|---|---|
id | yes | the created/updated asset ID |
name | yes | the asset filename |
sessionId | no | updated session ID (if the provider rotates sessions) |
userMsg | no | message to display to the user |
data | no | alternative 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 DirectAccessHelpersrc/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, Nodecrypto,Buffer, orprocess(except keys you inject viadefine). 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. KeepawaitinsidecreateUpdateAsset. (async functionitself is fine.) - No dynamic
import()orrequire(). 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'orimport 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 aneval'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:8080Fixed 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
createAssetandupdateAssetare callable. Anything else on the default export is ignored by current consumers. - Use the passed-in
axios. It has the right interceptors / headers /requestHeadersinjection. Importing your own bloats the bundle and defeats the integration. axiosversion is 0.19.2 — not the latest. Don't rely on features added after 0.19.x (signal,AbortControllerintegration, 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
createAssetinvocation. - 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 aprocess.env.Xthat wasn't injected throwsReferenceErrorwhen the DAH runs. defineis a textual replacement. AlwaysJSON.stringifystring 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/getBase64FromStringalready cover common cases). - Don't accidentally drag your main
index.tsinto 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:
| Consumer | MD5 | getAccessToken | getSessionId |
|---|---|---|---|
| Production CI HUB Plugins | real (CryptoJS) | CI HUB platform token | real session ID |
| SDK test UI | returns '' (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
-
Split shared logic into a small dedicated file. If your
createAsset/updateAssetshare helpers (e.g. an upload helper, a folder-ID normalizer), put them in a small file likesrc/dah-utils.tsthat both the DAH and your mainindex.tsimport. Don't import fromindex.tsitself — 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 -
One function for both create and update. If your create and update logic is similar, write a single
createUpdateAssetand export it as both. Branch ondata.assetId(truthy = update). -
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
-
Inject server-side config via
define. Pass values tobuildDAH's second argument and read them asprocess.env.Xinside 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')) }) -
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. -
Use
helpers.getAdapterData()for per-connection data. Values returned by yourloginhandler inadapterData(per-tenant endpoints, region selection, etc.) are runtime — read them viahelpers.getAdapterData()instead ofdefine.
Runtime
-
Use
payloadas the bearer token. The 2nd argument is the raw provider access token from yourloginhandler. Most providers just needAuthorization: Bearer ${payload}. -
Set
maxContentLength: Infinityon axios calls that upload file data — axios 0.19 rejects large payloads otherwise. -
Report progress for large uploads. Call
onProgress(loaded / total)inside axios'sonUploadProgresscallback. The UI shows a progress bar only when you report it. -
Test with real files. The SDK test UI passes a real
File/Blobto your DAH, not a base64 string. Usedata.file.size,data.file.name, anddata.file.typeas needed. -
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 {...} })(). Useexport default { ... }instead. - Passing un-stringified strings to
define— produces "Invalid define value" build error. - Importing
axiosdirectly insidedah.ts— causes bundle bloat and wrong interceptors. Use the injectedaxiosparameter. - Importing from
index.tsor 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