Skip to content

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 ​

sh
cpc build .

cpc build [dir] [--reproducible] writes the build output to dist/package in the package folder. It:

  • bundles src/ (the entry named by cognipeer.entry in package.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 ui entry into ui/<id>.js, at most 1 MB each, with React and @cognipeer/connector-ui left 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/ and assets/, and writes sbom.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:

FileRequiredWhat it is
connector.jsonyesThe manifest, generated from your definition
server/index.mjsyesThe package code: one ES module for Node 22, SDK included
server/index.mjs.mapnoSource map, without sources
ui/<id>.jsnoOne browser bundle per screen
i18n/en.json, i18n/tr.jsonyesMessages
guides/*.mdnoSetup guides, one file per locale
assets/*noThe icon and other SVG or PNG files
sbom.cdx.jsonyesCycloneDX 1.6 list of the bundled npm packages
CHECKSUMS.sha256yes<sha256 hex> <path> per file, sorted, excluding itself and SIGNATURE
SIGNATUREyesThe signature, below

Console checks these entry rules before it reads anything:

RuleValue
Entriesat most 2 000
One entryat most 25 MB
All entries unpackedat most 100 MB
The zipat most 50 MB
Namesletters, digits, ., _, -, with / between folders; no . or .. segment; no duplicates
Extensionsjson, mjs, js, map, md, svg, png, txt, sha256, and the file SIGNATURE

SIGNATURE is JSON:

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:

  1. Every file except CHECKSUMS.sha256 and SIGNATURE is listed and matches its hash, and every listed file exists.
  2. manifestSha256 and checksumsSha256 match the files.
  3. package, version and publisher match connector.json, and the package id starts with <publisher>..
  4. keyId is a trusted key of that publisher and the current time is inside its notBefore and notAfter.
  5. 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:

sh
cpc keygen --namespace acme --key-id acme-2026-1 --valid-days 730 --out ~/cpc-keys

This writes two files into the folder you give with --out:

FileHolds
<key-id>.pemThe private key (Ed25519). Keep it secret; the file is created readable by you only
<key-id>.trust.jsonThe 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:

WayHow
EnvironmentCPC_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 ​

sh
cpc pack . --key-file ~/cpc-keys/acme-2026-1.pem --key-id acme-2026-1 --namespace acme

cpc 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:

sh
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 ​

sh
cpc verify dist/acme.notes-1.0.0.cpkg --trust ~/cpc-keys/acme-2026-1.trust.json

cpc 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:

json
{ "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:

SettingMeaning
CONNECTOR_PACKAGES_TRUST_FILEPath 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_DEV1 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
  SHA256SUMS

Users 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:

yaml
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 ​

  • version in package.json is 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 settingsVersion and 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.profile or 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.

SettingMeaning
CONNECTOR_PACKAGES_INDEX_URLURL of the index. Unset: no index
CONNECTOR_PACKAGES_INDEX_TOKENOptional bearer token sent to the index URL and to package URLs on the same origin
CONNECTOR_PACKAGES_INDEX_REFRESH_HOURSHow 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.

Studio · Pulse · Console · Agent SDK and more — the Cognipeer documentation hub