Build, sign and publish
Build a connector package with cpc, sign it into an Ed25519 .cpkg file, and distribute it so Console can verify and install it.
Enterprise module
Connectors are the enterprise connectors module. A workspace without the module gets 402 from the connectors API.
The flow has four steps: cpc build turns your folder into a build output, cpc pack signs it into one .cpkg file, cpc verify checks the file against a trust file, and you hand the file to Console. Console checks the signature again every time it loads a package, so the way the file travels is not part of the trust.
Build
cpc build .cpc build [dir] [--reproducible] writes the build output to dist/package in the package folder. It:
- bundles
src/(the entry named bycognipeer.entryinpackage.json), the SDK and your dependencies into one ES module for Node 22,server/index.mjs. The bundle is at most 8 MB; - bundles each
uientry intoui/<id>.js, at most 1 MB each, with React and@cognipeer/connector-uileft external; - imports the bundle to read your definition and writes
connector.json, failing on any error in it (see the definition reference); - copies
i18n/,guides/andassets/, and writessbom.cdx.json, a CycloneDX 1.6 list of the npm packages in the bundle.
i18n/en.json and i18n/tr.json must exist. The package version comes from package.json and must be a semantic version.
Building runs your package's code, and so do tests and the install scripts of dependencies. Never run any of them in a process that holds a signing key.
--reproducible records the time and commit of the last commit that touched the package folder instead of the clock and HEAD, so the same content always builds to the same bytes. It needs the full git history (no shallow clone) and a clean folder.
The .cpkg format
A .cpkg file is a zip. Its entries:
| File | Required | What it is |
|---|---|---|
connector.json | yes | The manifest, generated from your definition |
server/index.mjs | yes | The package code: one ES module for Node 22, SDK included |
server/index.mjs.map | no | Source map, without sources |
ui/<id>.js | no | One browser bundle per screen |
i18n/en.json, i18n/tr.json | yes | Messages |
guides/*.md | no | Setup guides, one file per locale |
assets/* | no | The icon and other SVG or PNG files |
sbom.cdx.json | yes | CycloneDX 1.6 list of the bundled npm packages |
CHECKSUMS.sha256 | yes | <sha256 hex> <path> per file, sorted, excluding itself and SIGNATURE |
SIGNATURE | yes | The signature, below |
Console checks these entry rules before it reads anything:
| Rule | Value |
|---|---|
| Entries | at most 2 000 |
| One entry | at most 25 MB |
| All entries unpacked | at most 100 MB |
| The zip | at most 50 MB |
| Names | letters, digits, ., _, -, with / between folders; no . or .. segment; no duplicates |
| Extensions | json, mjs, js, map, md, svg, png, txt, sha256, and the file SIGNATURE |
SIGNATURE is JSON:
{
"format": "cognipeer.connector-signature/v1",
"alg": "ed25519",
"payload": {
"package": "acme.notes", "version": "1.0.0", "publisher": "acme",
"keyId": "acme-2026-1",
"manifestSha256": "<hex>", "checksumsSha256": "<hex>",
"signedAt": "2026-10-25T00:00:00.000Z"
},
"signature": "<base64 Ed25519 signature of the canonical payload>"
}The canonical payload is JSON with object keys sorted at every level and no whitespace. A package is accepted when all of these hold:
- Every file except
CHECKSUMS.sha256andSIGNATUREis listed and matches its hash, and every listed file exists. manifestSha256andchecksumsSha256match the files.package,versionandpublishermatchconnector.json, and the package id starts with<publisher>..keyIdis a trusted key of that publisher and the current time is inside itsnotBeforeandnotAfter.- The Ed25519 signature verifies.
The digest of a package is sha256:<hex> of the .cpkg bytes. Zip entries carry a fixed time, so a rebuild of the same input gives the same bytes.
Signing keys
The publisher in the package id (acme in acme.notes) is the namespace of the key that signs it. Make a key pair once:
cpc keygen --namespace acme --key-id acme-2026-1 --valid-days 730 --out ~/cpc-keysThis writes two files into the folder you give with --out:
| File | Holds |
|---|---|
<key-id>.pem | The private key (Ed25519). Keep it secret; the file is created readable by you only |
<key-id>.trust.json | The public part, as a trust entry |
The key id is 1 to 64 letters, digits, ., _ or -. The namespace is lower case letters, digits and -, up to 40 characters, starting with a letter.
Keep the private key in a password manager and in your CI system's secret store. Do not commit it, and delete the local copy once it is stored.
To sign, give cpc the key in one of two ways:
| Way | How |
|---|---|
| Environment | CPC_SIGNING_KEY (the PEM text), CPC_SIGNING_KEY_ID, and CPC_SIGNING_NAMESPACE. The namespace defaults to cognipeer, so set it to your own |
| Flags | --key-file <file> --key-id <id> --namespace <ns> |
cpc refuses to sign when the key's namespace is not the package's publisher.
Pack
cpc pack . --key-file ~/cpc-keys/acme-2026-1.pem --key-id acme-2026-1 --namespace acmecpc pack [dir] [--prebuilt | --reproducible] [--dev | --key-file f --key-id id [--namespace ns]] [--out file] builds the package, writes CHECKSUMS.sha256 and SIGNATURE, zips the files and writes dist/<id>-<version>.cpkg (or the file you give with --out). It prints the path, the digest, the size and the key id.
pack builds first, and building runs package code. With CPC_SIGNING_KEY set in the environment it refuses to build, because the package's code could read the key. For a release, build without the key and sign afterwards:
cpc build . --reproducible # no key in this environment
cpc pack . --prebuilt # signing key set; bundles, imports and runs nothing--prebuilt signs the existing dist/package. It treats the files as untrusted data: symbolic links, file types and sizes the format does not allow, a manifest that is not valid, or a build whose version differs from package.json are refused.
cpc pack --dev signs with a developer key that cpc makes and keeps under .keys/ at the root of the npm workspace that holds your package. Console accepts such a package only while developer packages are allowed (see Trust in Console). Use it for local trials, not for distribution.
Verify
cpc verify dist/acme.notes-1.0.0.cpkg --trust ~/cpc-keys/acme-2026-1.trust.jsoncpc verify <file.cpkg> [--trust trust.json]... [--allow-dev] applies the rules above and prints ok <id>@<version> <digest> key <key-id>. Always pass --trust; give it more than once to trust several files. Add --allow-dev to accept a developer key.
Trust in Console
Console accepts a package only if its signing key is in a trust store. A trust store is a JSON file:
{ "version": 1, "namespaces": { "acme": { "keys": [
{ "keyId": "acme-2026-1", "alg": "ed25519", "publicKey": "<raw 32 bytes, base64>",
"notBefore": "2026-10-25T00:00:00.000Z", "notAfter": "2028-10-24T00:00:00.000Z" }
] } } }cpc keygen writes exactly this shape to <key-id>.trust.json. The file holds public keys only. Console's own trust store has the keys that sign official packages (namespace cognipeer). For your own namespace:
| Setting | Meaning |
|---|---|
CONNECTOR_PACKAGES_TRUST_FILE | Path to your trust file. Console reads its namespaces and keys. It cannot add keys to the cognipeer namespace, and keys marked dev in it are ignored |
CONNECTOR_PACKAGES_ALLOW_DEV | 1 or true accepts packages signed with a developer key; 0 or false refuses them. When unset, developer packages are accepted outside production only |
Rotate a key before its notAfter: add the new key to the trust file, sign new releases with it, and keep the old key until no installed package needs it.
Install into Console
The offline channel needs no registry. Copy the .cpkg into the directory that CONNECTOR_PACKAGES_EXTRA_DIR names on the Console host (a mounted volume in a container) and restart. Console verifies each file at boot, unpacks it into a private directory and registers it. A package that fails verification or the engines.console check is logged and skipped; it never stops the boot. For the steps an administrator follows, see Install connector packages.
Publish as a GitHub Release
A release asset is a simple, durable way to distribute a package. Publish one release per version, with the signed .cpkg and a checksum file as assets:
v1.0.0
acme.notes-1.0.0.cpkg
SHA256SUMSUsers download the file, check it against SHA256SUMS and copy it to Console as above. Console verifies the signature regardless of where the file came from.
Build and signing run in two separate jobs, so the job that holds the key never runs package code:
name: release
on:
push:
tags: ['v*']
jobs:
build:
runs-on: ubuntu-latest
permissions: { contents: read }
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- uses: actions/setup-node@v4
with: { node-version: 22 }
- run: npm ci
- run: npm test
- run: npx cpc build . --reproducible
- uses: actions/upload-artifact@v4
with: { name: package, path: dist/package }
publish:
needs: build
runs-on: ubuntu-latest
environment: release # holds the secret; require a reviewer
permissions: { contents: write }
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22 }
- run: npm ci --ignore-scripts
- uses: actions/download-artifact@v4
with: { name: package, path: dist/package }
- run: npx cpc pack . --prebuilt
env:
CPC_SIGNING_KEY: ${{ secrets.CPC_SIGNING_KEY }}
CPC_SIGNING_KEY_ID: acme-2026-1
CPC_SIGNING_NAMESPACE: acme
- run: npx cpc verify dist/*.cpkg --trust trust/acme.trust.json
- run: cd dist && sha256sum *.cpkg > SHA256SUMS
- run: gh release create "$GITHUB_REF_NAME" dist/*.cpkg dist/SHA256SUMS
env: { GH_TOKEN: '${{ github.token }}' }Keep trust/acme.trust.json in the repository: it holds the public key, so it is safe to commit. The publish job builds and tests nothing.
Versioning
versioninpackage.jsonis the package version, a semantic version. Bump it for every change that ships.- A released version is never replaced. With
--reproducible, a package's bytes depend only on its content, so a release can run again without changing anything already released. If the content changed under the same version, the new bytes differ from the asset that is already there, and the release must stop: change the version and tag again. - From an index, Console installs a version only when it is higher than the one already installed.
- Bump
settingsVersionand add a migration when a setting changes meaning, so existing connectors keep working. See Settings versions.
engines.console
engines.console is the range of Console versions the package works with, for example >=1.20.0 <2.0.0. Console refuses to install a package outside it. Rules of thumb:
- Set the lower bound to the first Console version that has every host operation and manifest feature the package uses.
- Do not publish a package that uses
sql,mail,http.profileor a signing inject kind with a lower bound than the Console that introduced it. A Console that predates a feature cannot refuse what it does not know, so the range is the only protection. - Keep the upper bound at the next major version.
The package index
A signed index is a static JSON file that lists installable packages with their digest, size and URL. Console can poll one and install newer versions from it.
| Setting | Meaning |
|---|---|
CONNECTOR_PACKAGES_INDEX_URL | URL of the index. Unset: no index |
CONNECTOR_PACKAGES_INDEX_TOKEN | Optional bearer token sent to the index URL and to package URLs on the same origin |
CONNECTOR_PACKAGES_INDEX_REFRESH_HOURS | How often Console checks. Default 6 |
An index has the format cognipeer.connector-index/v1: a signed object (serial, generatedAt, expiresAt, packages, revoked) and an Ed25519 signature of its canonical JSON. Each package entry has id, version, publisher, digest, size and url (relative to the index or absolute). Console refuses an index whose signature fails, that has expired, or whose serial is lower than one it has already seen, and a downloaded package must match its digest and then pass the package checks. An index lives 30 days by default. revoked lists key ids and { id, version } pairs that Console must refuse.
Console checks an index against the official signing keys. For packages of your own namespace, distribute the .cpkg file through CONNECTOR_PACKAGES_EXTRA_DIR.

