A **package** is the community-node equivalent: one JSON file that adds ready-made pieces to a connector without a rebuild.
It can bundle any of these:

| Content | What the install does |
|---|---|
| **Sub-flows** | Reusable flow tabs. They appear in the editor palette under **Packages** and run through a `subflow` node (`calls: "pkg:<name>/<id>"`). |
| **Examples** | Flows that join the Examples gallery, with their inputs, mock services and sample record. |
| **Apps** | App definitions saved as **drafts** (templates), never published for you. |
| **OpenAPI specs** | Registered as `<package name>.<id>`, for an `http` node's `openapi: "<package name>.<id>#operationId"`. |
| **Event types** | Added to the [business-event catalogue](/docs/business-events). |

Packages are listed at [flow.opexcg.com/packages](https://flow.opexcg.com/packages). Open **Packages** in the editor's menu
(or Ctrl/Cmd+K, *Packages…*) to browse what is installed, fetch from the catalogue, or install a downloaded file.

## Trust

Every package carries an Ed25519 signature over its contents. A connector decides what to do with it:

| Trust | Meaning | Installs |
|---|---|---|
| `official` | Signed by the OpexCG publisher key built into the connector. | Always |
| `trusted` | Signed by a key in `packages.trusted_publishers`. | Always |
| `unsigned` | No signature. | Only with `packages.allow_unsigned: true`, shown with a warning |
| `unknown_signer` | Signed with a key this connector does not know, so the signature was **not** checked. | Only with `packages.allow_unsigned: true`, shown with a warning |

A signature that does not match the contents is **always refused**, with or without `allow_unsigned`: the file was changed
after it was signed. A verified package is only upgraded by a package signed with the same key.

A package can carry scripts (JavaScript transforms), HTTP and SQL calls and printer jobs, and they run inside your flows.
Read an unsigned or third-party package before you install it. Trust is checked again every time the connector starts: remove
a publisher key from the config and its packages become inactive (still listed, their sub-flows stop resolving, and a deploy
that calls them says so).

```yaml
packages:
  allow_unsigned: false                                  # default
  catalogue_url: https://flow.opexcg.com/api/packages    # "" = offline: install from a file only
  trusted_publishers:                                    # extra signers besides OpexCG
- id: acme
  public_key: <base64 raw 32-byte Ed25519 public key>
```

## Install, upgrade, uninstall

Installing needs the **admin** role and is audited (`package.install`, `package.upgrade`, `package.uninstall`).
Everything that can fail is checked **before** anything changes: signature and trust, `requires` (the lowest opex-flow that
runs the package; a newer requirement is refused with both versions), the contents, and conflicts. A package never
overwrites what exists: an event type name, an app id or slug, an example id or a loaded OpenAPI spec that is already taken
refuses the install and names it.

- **Install** adds the contents. The same package twice is refused: upgrade it instead.
- **Upgrade** replaces the contents with a newer version (same or older versions are refused). Sub-flows, specs and event
  types the new version drops are removed, so an upgrade is refused while a deployed flow uses one of them. A flow that calls
  the package runs the new sub-flow after its **next deploy** (the response says `redeploy: true`). An app you edited since it
  was installed is kept as it is.
- **Uninstall** removes everything the package added. It is refused (409 `in_use`) while a **deployed** flow calls one of its
  sub-flows, uses one of its OpenAPI specs or one of its event types; the answer lists the nodes. Apps you edited stay.

Installing from the catalogue downloads the file and checks its SHA-256 against the listing before it reads it; a download
must come from the catalogue's own host. For a machine with no internet access, download the `.opexpkg.json` file elsewhere,
copy it over and use **Packages > From file** (or `POST /api/packages/install {bundle}`).

State lives in the data directory: `packages.json` (the index) and `packages/<name>.json` (the verified bundle).

## Using what a package adds

A sub-flow shows up in the palette under **Packages**; dropping it creates a `subflow` node whose `calls` is
`pkg:<package name>/<sub-flow id>`. It behaves exactly like a sub-flow tab of your own flow (see
[Flows](/docs/flows)): each call site runs its own copy with its own counters, and an error inside follows the call node's
`error` wire. The package's own nodes are not on your canvas; a problem inside one is reported on the call node.

## REST and MCP

| Method | Path | |
|---|---|---|
| GET | `/api/packages` | Installed packages, the palette sub-flows and the policy (viewer) |
| GET | `/api/packages/catalogue` | The catalogue with `installed_version` and `update_available` (viewer) |
| GET | `/api/packages/{name}` | One installed package (viewer) |
| POST | `/api/packages/inspect` | `{name, version?}` or `{bundle}`: trust, warnings, install or upgrade, changes nothing (admin) |
| POST | `/api/packages/install` | Same body: installs (admin, audited) |
| POST | `/api/packages/{name}/upgrade` | `{version?}` or `{bundle}` (admin, audited) |
| DELETE | `/api/packages/{name}` | Uninstall (admin, audited) |

Errors carry `code`: `bad_signature` and `requires` and `invalid` (422), `unsigned_refused` (403), `already_installed`,
`not_newer`, `conflict` and `in_use` (409), `catalogue_unavailable` (502). MCP tools: `list_packages` (with `catalogue: true`
to fetch the catalogue), `install_package` (with `upgrade: true` to upgrade) and `uninstall_package`; `list_examples` and
`get_example` include the examples of installed packages.

## Build and sign your own

A package is a directory:

```text
manifest.json                 name, version, title, description, author, license, requires, homepage?
subflows/<id>.json            { title, description?, nodes, edges }: one subflow_in, one subflow_out, no sources
examples/<id>.json            an example file, as in apps/connector/examples/
apps/<id>.json                an app definition
openapi/<id>.json             an OpenAPI 3.x document
events/<name>.json            an event type: { description, owner, schema, ... }
```

```json
{
  "name": "com.acme.payments",
  "version": "1.0.0",
  "title": "ACME payments",
  "description": "What it does, in a sentence or two.",
  "author": "ACME",
  "license": "MIT",
  "requires": "2.1.0"
}
```

The `name` is reverse-DNS (lower-case words and dots, at least two parts), the `version` semver. Inside a package, a
`subflow` node calls a sibling by its local id (`"calls": "other"`). Node ids must not contain `::` or `#`.

```bash
opex-flow package keygen --out ~/.config/opex-flow/acme.key       # once; keep it out of git
opex-flow package pack ./my-package -o my-package.opexpkg.json   # validates, writes an UNSIGNED bundle
opex-flow package sign my-package.opexpkg.json --key ~/.config/opex-flow/acme.key --key-id acme
opex-flow package verify my-package.opexpkg.json --public-key <b64> --key-id acme   # exit 0 signed, 3 unsigned/unknown, 1 altered
opex-flow package info my-package.opexpkg.json                    # one line of JSON: manifest, counts, sha256
```

`pack` runs the install-time checks (shapes, ids, every sub-flow called once by the real expander, the examples rendered with
their defaults, specs indexed), so a package that packs installs. `scripts/sign-package.sh DIR` does pack, sign and verify in
one go; the key comes from `OPEX_PACKAGE_KEY_FILE` or `OPEX_PACKAGE_KEY` (base64 PKCS#8), never from the repository.
Give a customer the public key from `opex-flow package pubkey --key FILE` for their `trusted_publishers`.

## The OpexCG catalogue

`flow.opexcg.com` serves `GET /api/packages` (every package, newest version plus `versions`, each with `sha256`, `size`,
`signed`, `key_id`, `requires` and a `download` path) and `GET /api/packages/{name}/{version}` (the bundle, with its SHA-256
in `X-Checksum-Sha256`). Packages live in R2 with a row in D1; `scripts/publish-package.sh FILE` verifies the signature
against the OpexCG key, uploads the file and registers it, and `--withdraw NAME VERSION` hides a version. The signed
packages of this repository are in `packages/dist/` with their sources in `packages/src/`.