Programa de publicadores
Registra tu módulo, publica una versión, recibe el paquete firmado y escucha los eventos que tu servidor necesita.
No alojamos tu módulo. Tú vendes y distribuyes donde quieras: tu propio sitio, tu propia tienda, un marketplace, un revendedor. Lo que queda aquí es la verdad sobre ese archivo, y es esa verdad la que el instalador del comprador verifica antes de extraer un solo byte.
Lo que haces, una vez#
- Registra el artefacto en tu cuenta, en Publicación. El slug se reserva
aquí, y no en tu primera versión: es el nombre que tu módulo lleva a cada
instalación, va a tu
module.jsony nunca cambia. Los nombres de nuestro namespace (bondry-,core,admin,designer,members,lms) se rechazan. - Genera el token de publicación. Es un id
bkp_más un secreto que se muestra una vez. Un token por artefacto, revocable al instante. Revocar no tumba lo ya publicado: impide el siguiente anuncio. - Indica tu URL de webhook si quieres que tu servidor sepa de nosotros. Solo https, puerto 443, y el nombre tiene que resolver a direcciones públicas.
Lo que hace tu servidor, en cada versión#
Todo se firma con el mismo sobre que ya conoces de la API de licencia, así que no hay nada nuevo que aprender:
X-Bondry-Key: bkp_3f7a91c25e08
X-Bondry-Timestamp: 1789459200
X-Bondry-Nonce: 3f7a91c25e0844b1
X-Bondry-Signature: hex(hmac_sha256(secreto, METHOD\nPATH\nTIMESTAMP\nNONCE\nsha256(cuerpo)))
La ventana del timestamp es de 300 segundos, el nonce es de un solo uso y el cuerpo entra en la firma por su sha256, así que ni un byte cambia en el camino.
| Llamada | Qué hace |
|---|---|
POST /v1/publisher/artifacts/{slug} |
Actualiza nombre, resumen, descripción por idioma, sitio y canal de soporte. Idempotente: el campo que no envías es el campo que no tocamos. El inglés es obligatorio en todo texto por idioma |
POST /v1/publisher/artifacts/{slug}/releases |
Anuncia una versión: version, min_core, max_core, php, notes por idioma, size, sha256 |
PUT /v1/publisher/releases/{version}/package |
Envía el zip, crudo en el cuerpo, hasta 40 MB. El sha256 tiene que coincidir con lo anunciado |
GET /v1/publisher/artifacts/{slug} |
Todo lo que tu pipeline necesita para decidir si sigue: estado de la revisión, versión publicada, hash de validación, último anuncio |
GET /v1/publisher/releases/{version}/package |
Tras la aprobación, una URL firmada y corta para recoger el zip firmado |
GET /v1/publisher/releases/{version}/hash |
El hash de validación de esa versión |
Anunciar es barato y repetible; subir 40 MB, no, y por eso son dos llamadas. Un anuncio sin paquete caduca solo a los siete días, y ese número de versión vuelve a quedar libre.
Cada rechazo dice el campo, lo que llegó y lo que se esperaba. "Invalid payload" no es un mensaje.
La revisión#
La mitad automática corre en cuanto llega tu paquete: ninguna ruta fuera del
archivo, ningún enlace simbólico, ninguna bomba de descompresión, tamaño dentro
del tope, manifiesto válido con el slug registrado y la versión anunciada,
versión que avanza, min_core que existe, traducción al inglés presente, y el
escaneo estático que levanta bandera en eval, un literal grande dentro de
base64_decode, shell_exec, una URL con IP fija y código ofuscado.
Una bandera no rechaza nada por sí sola: el código legítimo las usa todas de vez en cuando. Va a un revisor humano, con el archivo y la línea exacta.
La mitad humana mira tu manifiesto, los permisos y ganchos que declaras, las migraciones y los archivos de rutas del paquete, las banderas y la diferencia respecto a tu versión anterior. La decisión es aprobar, rechazar, pedir cambios o suspender, y todas menos la aprobación vienen con un motivo escrito que te llega palabra por palabra.
"Pedir cambios" devuelve la versión a borrador, que es el único estado en el que tu servidor puede enviar el paquete de la misma versión otra vez.
El zip firmado es tuyo para distribuir#
Aprobada, firmamos tu zip con la clave de paquete de Bondry y guardamos el sha256 del archivo firmado. Ese hash es la identidad de esa versión para siempre.
El zip firmado es tu zip más dos entradas en la raíz:
bondry-artifact.json {"slug":…,"version":…,"publisher":…,"files":{"<ruta>":"<sha256>"}}
bondry-artifact.sig base64 de RSA-SHA256 sobre el manifiesto canónico
La lista de archivos cubre cada entrada de tu zip original, y el instalador
verifica en los dos sentidos: nada del zip fuera de la lista, nada de la lista
faltando en el zip. Sin eso, bastaría añadir un .php al paquete después de
firmarlo y la firma seguiría cuadrando.
Recoges el archivo firmado desde tu cuenta o por la API, y borramos nuestra copia en cuanto lo descargas, y en todo caso siete días después de la aprobación, con un correo al tercer día si aún no lo has recogido. Lo que queda aquí es el manifiesto, la firma, el hash y las notas. El archivo es tuyo, y su custodia también.
Lo que hace el instalador del comprador#
Calcula el sha256 del archivo que tiene delante, verifica la firma incrustada con la clave pública que ya lleva para los updates del core, y le pregunta al registro:
GET /v1/artifacts/{slug}/verify?version=1.4.2&sha256=<64 hex>
Sin credencial, una de tres respuestas, siempre HTTP 200:
| Respuesta | Lo que ve el comprador |
|---|---|
verified |
"Archivo verificado con el registro de Bondry, versión 1.4.2 del publicador <tú>", y la instalación sigue |
altered |
La pantalla roja: este archivo no es el que publicó el autor. La instalación se rechaza por defecto |
unknown |
No consta en el registro: tratado como cualquier zip bajado de internet |
Una versión suspendida responde unknown. Sin red, la firma incrustada sigue
valiendo y el instalador dice que no pudo confirmar: la falta de red nunca es una
acusación, y tampoco una aprobación silenciosa.
Los eventos que recibes#
Registras una URL https por artefacto y la llamamos:
| Evento | Cuándo |
|---|---|
artifact.approved / artifact.rejected |
la decisión sobre tu registro |
release.approved |
la versión pasó la revisión y está publicada |
release.rejected |
rechazada, o devuelta para cambios, con el motivo |
release.suspended |
retirada tras publicarse, con el motivo |
package.altered |
una instalación con licencia recibió un archivo que no coincide con tu hash aprobado |
El cuerpo:
{
"id": "01J8ZC5E7Q2R8VQ1F0M4V8N0PA",
"type": "release.approved",
"created_at": "2026-09-17T11:31:55+00:00",
"data": { "slug": "directory", "version": "1.1.0", "validation_hash": "…" }
}
El id es estable: el reenvío lleva el mismo id, así que trátalo como tu clave
de idempotencia. Cuando un tipo cubre más de una decisión, data.decision lleva
la palabra exacta y data.reason, el motivo escrito.
Cómo verificar la firma#
Firmamos la llamada con el secreto de webhook de ese artefacto, en el mismo sobre
de arriba, con una diferencia: X-Bondry-Key lleva whk_<12 hex>, el id público
del secreto. Así tu lado sabe qué secreto usar tras una rotación, y por eso vale
la pena guardar tus secretos indexados por ese id.
$canonical = implode("\n", [
'POST',
'/tu/ruta/de/webhook',
$request->header('X-Bondry-Timestamp'),
$request->header('X-Bondry-Nonce'),
hash('sha256', $request->getContent()),
]);
$esperada = hash_hmac('sha256', $canonical, $tuSecretoDe($request->header('X-Bondry-Key')));
if (! hash_equals($esperada, (string) $request->header('X-Bondry-Signature'))) {
abort(401);
}
Rotar el secreto desde tu cuenta mantiene el anterior válido 24 horas, para que cambies tu configuración sin perder ningún evento.
Responde 2xx en 10 segundos. Fuera de eso, reintentamos en 1 min, 5 min, 30
min, 2 h, 12 h y 24 h, y luego el endpoint entra en cuarentena y recibes un
correo. Cada entrega y cada intento quedan visibles en tu cuenta, con la
respuesta que dio tu servidor y un botón para enviarlo de nuevo.
Cuando alguien distribuye una copia alterada#
La llamada de veredicto es pública, y por eso nunca genera alerta: cualquiera podría llamarla mil veces con un hash inventado, y el registro se volvería un amplificador de spam apuntado hacia ti.
Quien genera alerta es el evento: una instalación con licencia activa diciendo
que el archivo que recibió no es el que firmamos. Eso se convierte en el webhook
package.altered, un correo para ti con el hash recibido y el hash que
aprobamos, y una línea en nuestra cola, agrupada por artefacto y por hash. El
mismo hash alterado apareciendo en muchas licencias es piratería a escala, y eso
es justo lo que el agrupamiento existe para mostrar.
Nada de esto revoca nada por sí solo. Quien suspende es una persona, con un motivo escrito.