---
name: bondry-publish
description: Take a finished Bondry module or theme zip from registration to approval, through the MCP tools or the signed HTTP routes, including the review, the webhooks your server receives and what to do on each refusal. Use when the user is ready to register, announce, upload or publish a Bondry artifact, or is reacting to a review decision.
---

# Publishing a Bondry module or theme

## What you have to know first

1. **We do not host your module.** You sell and distribute it wherever you
   want. What we keep is the truth about the file, and that truth is what the
   buyer's installer checks before it extracts a single byte.
2. **The zip you distribute is the signed file we gave back, byte for byte.**
   Do not recompress it, do not rename a file inside it, do not add a README
   afterwards. Any byte changed changes the sha256, and every installation will
   tell your buyer, in red, that the file is not the one you published. Changed
   something? That is a new version: announce, upload, collect the signed file
   and distribute that one.
3. **We delete the binary as soon as you download it**, and at most seven days
   after approval. Keep the signed file.
4. **Announcing does not publish.** The review publishes. Those are two
   different events and confusing them is the most common mistake here.
5. **The slug never changes.** It is reserved when you register, it goes into
   your manifest, and every installation knows you by it. Names in our
   namespace (`bondry-`, `core`, `admin`, `designer`, `members`, `lms`) are
   refused.

## Two ways in, one destination

The MCP tools and the HTTP routes call exactly the same code, with the same
rules, the same caps and the same audit trail. Use whichever fits: the agent in
your editor, or your pipeline.

| Step | MCP tool | HTTP route |
| --- | --- | --- |
| Register or update | `bondry_register_artifact` | account area, plus `POST /v1/publisher/artifacts/{slug}` for metadata |
| Check before sending | `bondry_review_preflight` | no equivalent: this one is MCP only |
| Announce a version | `bondry_announce_release` | `POST /v1/publisher/artifacts/{slug}/releases` |
| Send the package | `bondry_upload_package` (up to 8 MB) | `PUT /v1/publisher/releases/{version}/package` (up to 40 MB) |
| Watch the review | `bondry_artifact_status` | `GET /v1/publisher/artifacts/{slug}` |
| Collect the signed file | account area | `GET /v1/publisher/releases/{version}/package` |
| Webhook deliveries | `bondry_webhook_deliveries` | account area |

Neither path ever fetches your zip from a URL. Only bytes you send are read.

## Once, per artifact

1. **Register the artifact.** The slug is reserved here, not on your first
   release.
2. **Generate the publishing token** in your account. It is a `bkp_` id plus a
   secret shown once, one token per artifact, revocable on the spot. Revoking
   does not take down what is already published: it stops the next
   announcement.
3. **Set your webhook URL** if you want your server to hear from us. Https
   only, port 443, and the name has to resolve to public addresses.

The MCP token (`bkm_`) is a different credential: it belongs to the account,
not to one artifact, and it carries scopes. `publish` is unticked by default,
so a read-only agent cannot announce or upload by accident.

## The envelope, for the HTTP routes

Every `/v1/publisher` call uses the same signed envelope as the license API, so
there is nothing new to learn:

```
X-Bondry-Key:        bkp_3f7a91c25e08
X-Bondry-Timestamp:  1789459200
X-Bondry-Nonce:      3f7a91c25e0844b1
X-Bondry-Signature:  hex(hmac_sha256(secret, METHOD\nPATH\nTIMESTAMP\nNONCE\nsha256(body)))
```

`PATH` is the request path (`/v1/publisher/artifacts/directory/releases`),
`body` is the raw JSON (empty string on GET). The timestamp window is 300
seconds, the nonce is single use, and the body enters the signature through its
sha256, so not a byte changes on the way.

Every refusal tells you the field, what arrived and what was expected.
"Invalid payload" is not a message.

## Every release

### 1. Preflight

```
bondry_review_preflight { slug, version, package_base64 }
```

It runs the package checks and the manifest checks together and answers with
what a human reviewer will see. Fix hard failures here, where it costs nothing.

### 2. Announce

```
bondry_announce_release { slug, version, min_core, max_core, php, notes, sha256, size }
```

The version has to be higher than every version already registered, and
`min_core` has to be a Bondry release that exists. Announcing is cheap and
repeatable; uploading is not, which is why they are two calls.

An announcement without a package expires on its own after seven days, and that
version number goes back to being free.

The `sha256` you announce is checked against the bytes that arrive. That is how
a truncated upload is caught before the review, not after.

### 3. Upload

```
bondry_upload_package { slug, version, package_base64 }
```

Or `PUT /v1/publisher/releases/{version}/package` with the zip raw in the body,
up to 40 MB. That route is the one to use for anything over 8 MB.

The automated review runs the moment the bytes land. A hard failure puts the
version back to draft and keeps no file.

### 4. Watch

```
bondry_artifact_status { slug }
```

State, published version, validation hash, last announcement, every version
with its own state and flag count, and what is still missing.

## The review

The automatic half runs on arrival:

- no path outside the archive, no symlink, no zip bomb;
- entry count and uncompressed size within the caps;
- a valid manifest, with the slug you registered and the version you announced;
- a version that moves forward;
- at least one file under a `lang/en/` folder, because English is the source
  language of the whole ecosystem;
- a static scan for `eval`, large base64 payloads, shell execution, hard-coded
  IP addresses and obfuscation.

**A flag is not a rejection.** Legitimate code uses all of those from time to
time, so a flag is recorded with the file and the line for a human to read, and
the package goes on to the human half.

The human half reads what the module does and whether the screens follow the
product. Only approval signs the package.

## What each refusal means

| Code | What happened | What to do |
| --- | --- | --- |
| `invalid_version` | Not semver | `MAJOR.MINOR.PATCH` |
| `version_not_newer` | Lower than or equal to a version already registered | Bump it; a published number is never reused |
| `unknown_min_core` | No Bondry release with that version | Use a core version that exists |
| `sha256_mismatch` | The bytes are not the ones you announced | Announce the real hash, or send the real file |
| `version_not_open` | The version already has a package | Announce a new version |
| `manifest_missing` | No `module.json` or `theme.json` at the root or one folder deep | Fix the zip layout |
| `slug_mismatch` | The manifest slug is not the registered slug | The slug never changes |
| `version_mismatch` | The manifest version is not the announced version | Make them agree |
| `missing_english` | No `lang/en/` in the package | English is required |
| `path_traversal`, `symlink`, `zip_bomb`, `too_many_entries`, `too_large` | The archive is unsafe | Rebuild it from a clean tree |
| `artifact_not_found` | That slug is not yours, or does not exist | Both answer the same way, on purpose |
| `scope_required` | The MCP token lacks the scope | Issue a token with the scope and revoke the old one; scopes are never widened |
| `rate_limited` | 500 announcements a day per token, or 120 MCP calls a minute | Back off |

## Webhooks on your server

Deliveries are signed with your `whsec_` secret, in the same canonical form as
the inbound envelope. Verify the signature, treat the event id as idempotent,
and answer 2xx quickly.

Repeated failures put the endpoint in quarantine. Resending (from the account
area, or `bondry_webhook_deliveries` with `resend_event_id` and the `publish`
scope) takes it out again: resending is you saying the server is back.

The event id does not change on a resend, so your side deduplicates it exactly
as it would a retry.

## After approval

1. Collect the signed zip. The link is short-lived, and the binary is deleted
   as soon as you take it.
2. Distribute that exact file. Byte for byte.
3. The buyer's installer verifies it against the registry: `verified` goes
   through, `altered` is refused and needs an explicit second step that becomes
   an audit line, `unknown` goes through with a warning.

If you ever need to change one line, that is a new version. There is no other
answer.
