# Publishing plugins to market.engine9.ai

For teams whose registration flow publishes plugin catalog entries: engine9 (`Engine9Workers.registerPluginCatalog`),
Frakture (`BotFactory.register`), and any other publisher.

market.engine9.ai keeps one **catalog entry** per plugin version and serves it publicly. Secrets tools, task
callers and settings screens read the entry instead of your code. Most importantly, market publishes each plugin's
**credentials definition**, the URL that secrets.engine9.ai uses to know which fields a connection needs and which are
secret.

The entry format is core's standard, not market's: [docs/plugin-catalog.md](https://github.com/engine9-ai/core/blob/main/docs/plugin-catalog.md)
in `@engine9/core` 1.20.1 or later.

## What you get

| URL | |
| --- | --- |
| `https://market.engine9.ai/plugins/<path>.json` | Your newest entry |
| `https://market.engine9.ai/plugins/<path>@<version>.json` | The entry for one version. Never changes |
| `https://market.engine9.ai/definitions/<path>.json` | **The credentials definition.** Use it as `definition_source` in secrets |
| `https://market.engine9.ai/definitions/<path>@<version>.json` | The definition as of one version |
| `https://market.engine9.ai/plugins/<path>` | A page for people |

`<path>` is the plugin path, the same path install uses, written as is:

```
https://market.engine9.ai/definitions/@frakture-com/channelbots/ActBlueBot.json
https://market.engine9.ai/definitions/@engine9/plugins/e9email.json
```

The definition is made from the entry's `credentials` with core's `pluginCredentialsDefinition`. You never write it
yourself. A plugin with no `credentials` has no definition (404).

## 1. Get a publisher key

engine9 sends you an invite link (`https://market.engine9.ai/invite/…`). Open it in a browser and press
**Create my key**. Your key (`e9mkt_…`) is shown once, on that page; engine9 never sees it and keeps no copy. The
link works once and expires after 7 days.

Each key may publish only its publisher's packages, for example:

| Publisher | Key may publish |
| --- | --- |
| engine9 | `@engine9/*` |
| Frakture | `@frakture-com/*` |

Keep the key where your deploys keep secrets (secrets.engine9.ai or your CI's secret store) and give it to the
registration flow as `E9MARKET_KEY`. Never commit it or log it. If it is lost or leaks, ask engine9 for a new
invite; your old key keeps working until engine9 revokes it, so you can switch without a gap.

Check it:

```bash
curl -s -H "Authorization: Bearer $E9MARKET_KEY" https://market.engine9.ai/v1/whoami
# { "name": "frakture", "packages": ["@frakture-com/*"] }
```

## 2. Build entries

Your registration flow already builds entries in the standard shape (core's `buildPluginCatalogEntry`, or your
builder that writes the same thing). Before publishing, each entry needs:

- `path`, starting with `package.name`.
- `package.version`: the npm version of the package the plugin ships in. This is the only version a plugin has.
- `name` and `publisher.name`.
- `credentials.fields` for anything a person enters to connect, each with `secret: true` or `false`. Keys, tokens
  and passwords are secret; ids and regions a person may read back are not. No credential goes in `settings`.
- Only the methods callers should run, each with described options.

Check before you publish, locally or against market (no key needed for this):

```bash
npx e9core catalog validate                      # in a package
curl -s -X POST -H 'Content-Type: application/json' --data @entry.json https://market.engine9.ai/v1/validate
```

## 3. Publish

Send all of a release's entries in one request: an array of entries, or a bundle `{ "<path>": entry }`.

```js
// At the end of registration, after the entries are built.
const res = await fetch('https://market.engine9.ai/v1/plugins', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${process.env.E9MARKET_KEY}` },
  body: JSON.stringify(entries) // [entry, …], up to 500 per request
});
const { published, unchanged, failed, results } = await res.json();
for (const r of results) if (r.http_status >= 400) console.error(`market: ${r.path}: ${r.error}`, r.errors || r.message);
```

Or one entry at a time: `PUT https://market.engine9.ai/v1/plugins/<path>` with the entry as the body.

From a shell, with entry files or bundles (`e9core catalog build` output, `engine9-plugins.catalog.json`,
`frakture-bots.catalog.json`):

```bash
E9MARKET_KEY=e9mkt_… node scripts/publish.mjs catalog/       # in the engine9-ai/market repository
```

### Results

Each entry in `results` has `http_status`, and `status`, `url`, `version_url`, `definition_url` and `warnings` when
it worked. A batch answers 200 when every entry worked and 207 when any failed.

| `http_status` | Meaning | What to do |
| --- | --- | --- |
| 201 `published` | New version stored. `newest: true` when it is now the newest | Nothing |
| 200 `unchanged` | This version is already published with the same content | Nothing. Registering again is safe |
| 409 `version_exists` | This version is already published with **different** content | Bump the package version. A published version never changes |
| 422 `invalid_entry` | The entry does not meet the standard. `errors` lists why | Fix the builder or the plugin's metadata |
| 403 `forbidden` | The key may not publish this package | Check `package.name`, or ask for the key's packages to change |
| 401 `unauthorized` | Missing, wrong or revoked key | Check `E9MARKET_KEY` |

## Rules that matter for a registration flow

- **Publishing is idempotent.** Registering the same entries on every deploy or start is fine; unchanged entries
  cost nothing. Two instances registering at once are fine too: exactly one stores the version.
- **A change needs a new version.** If anything in an entry changes (a new credentials field, a reworded label, a
  new method), the package version must change too, or market answers 409. If your deploys do not bump versions,
  make the version come from something that changes with the code, such as the release version of the deploy.
- **Keep entries deterministic.** Two builds of the same code must produce the same entry: no timestamps, hostnames,
  environment names, or build ids, and arrays in a stable order. Key order does not matter.
- **Do not let market fail your service.** If registration runs when a service starts, log market errors and carry
  on. Treat 409 and 422 as bugs to fix in the release, ideally caught in CI with `/v1/validate` before deploy.
- **Credentials follow the definition URL.** Secrets saved with `definition_source`
  `https://market.engine9.ai/definitions/<path>.json` pick up new fields the next time a person edits them. A field
  that was ever secret stays secret, even if a later entry says otherwise. Fields your plugin writes for itself (an
  OAuth refresh token) need not be declared; secrets treats them as secret.

## Using the definition in secrets

```js
const { SecretsClient } = require('@engine9/secrets-client');
const secrets = new SecretsClient();
const definition_source = 'https://market.engine9.ai/definitions/@frakture-com/channelbots/ActBlueBot.json';

await secrets.request('acme.actblue', { definition_source, message: 'Connect ActBlue' }); // a link for a person
await secrets.set('acme.actblue', value, { definition_source });                          // or store directly
```

## Notes for engine9

- Paths: `@engine9/plugins/<name>`, `@engine9/schemas/<name>`.
- `Engine9Workers.registerPluginCatalog` builds the entries; add the publish call after it, with the key from the
  worker's secrets. Plugins that run on engine9's own servers list their methods the same way as installed ones.

## Notes for Frakture

- Paths: `@frakture-com/channelbots/<BotName>`, with `bot_path` set to the Frakture bot id
  (`channelbots.ActBlueBot`).
- `BotFactory.register` converts each bot's `auth_fields` to `credentials` and its `mcp_methods` (or
  `catalog_methods`) to `methods`. Check that every converted field has `secret` set the way you mean it: an
  untyped legacy auth field becomes a secret password.
- One request can carry every bot: send the bundle `register` produces.

## Questions

Ask in the engine9 team channel. The entry standard is in `@engine9/core`; changes to it go there, not to market.
