Saltar al contenido principal

SDK para terceros

Guía de integración

Para que tu producto SaaS sepa qué ha comprado cada uno de tus clientes en Mitnix Commerce, lo verifique sin intermediarios y deje de concederlo cuando Commerce lo revoca.

Para el equipo de un tercero que vende su producto SaaS con la plataforma de Mitnix Commerce y quiere que ese producto sepa, sin intermediarios, qué ha comprado cada uno de sus clientes (capacidades, límites, cuotas y vigencia) y deje de concederlo cuando Commerce lo revoca. English version.

Todo lo que aquí se describe está implementado en los cinco paquetes @mitnix-ai-solutions/platform-* 1.0.2 y se ejecuta de principio a fin en el ejemplo tercero-saas, que Mitnix compila y prueba contra los paquetes tal como se publican.

1. Cómo encaja

 tu producto SaaS ──token OIDC de tu plataforma──▶ Commerce (/installation/v1/*)
      │                                                │
      │  1 trust/bundle        paquete de confianza ◀──┤  (autoridad de licencias de tu tercero)
      │  2 tenant-grants       Grants cifrados (JWE) ◀─┤  (uno por cliente, firmados por tu autoridad)
      │  3 trust/operations    clave de contenido ◀────┤  (open-grant: Trust la abre; la clave privada nunca sale del HSM)
      │  4 tenant-license-status  revocaciones ◀───────┘
      ▼
 verifica con el ancla ▶ concede capacidades, límites y cuotas por cliente (tenant)
  • No hay ningún secreto en tu producto. Te acreditas con el token OIDC que tu plataforma de ejecución emite para tu carga de trabajo; Commerce lo verifica contra lo que registraste.
  • Verificas tú, offline. Commerce te entrega el Grant, pero lo que lo hace de fiar es su firma (ES256, formato mitnix.signed-artifact/v3) comprobada contra un ancla que fijas tú. Una licencia de Mitnix o de otro tercero se rechaza.
  • Commerce deduce quién eres del token (tercero, producto y entorno). El SDK nunca envía tu vendorId.

2. Instalación

Los cinco paquetes están en npmjs.com y se instalan sin ningún token:

npm install @mitnix-ai-solutions/platform-common@1.0.2 @mitnix-ai-solutions/platform-artifacts@1.0.2 \
  @mitnix-ai-solutions/platform-trust@1.0.2 @mitnix-ai-solutions/platform-licensing@1.0.2 \
  @mitnix-ai-solutions/platform-commerce-client@1.0.2

Node 22 o superior (fetch y WebCrypto de la plataforma; el SDK no usa tipos de Node ni de DOM). La licencia del SDK es propietaria y gratuita, válida solo para usarlo con Commerce (LICENSE). La procedencia de lo publicado se verifica sin credenciales con el manifiesto firmado por Mitnix (verificación). Las versiones y su compatibilidad con /installation/v1 están en mitnix.net/sdk.

3. Registrar tu producto en la consola de Commerce

Lo hace el administrador de tu tercero en tu consola (https://<tu host>/consola/#/canal, «Identidad de tus productos»). Por cada producto y entorno registra:

Campo Qué es
Producto El de tu catálogo, con la forma <tercero>:<producto> (tras los dos puntos: minúsculas, dígitos y guiones internos, hasta 32).
Entorno development, staging o production. Debe ser el entorno en el que vende la instalación de Commerce (la consola lo indica y lo propone).
Emisor El iss exacto del token de tu plataforma.
Audiencia El aud exacto: la que pongas es la que tu producto pedirá al emitir el token. Usa la URL de Commerce.
Sujeto El sub exacto del token de tu carga de trabajo.

Qué hace Commerce al registrar:

  1. Descubre el emisor (<emisor>/.well-known/openid-configuration): el issuer del documento debe ser exactamente el registrado, y fija el jwks_uri. No vuelve a descubrir nada después.
  2. Exige que el emisor sea https:// con un nombre público (ni IP literal, ni puerto, ni credenciales, ni localhost) y que resuelva solo a direcciones públicas.
  3. Una identidad (emisor y sujeto) es de un solo producto y entorno en toda la plataforma.
  4. Pide a Trust la identidad de custodia tercero.<tercero>.<producto> del entorno. Un responsable de seguridad de Mitnix la aprueba. Con «Comprobar custodia», cuando está aprobada y activa, Commerce registra tu instalación SaaS tercero.<tercero>.<producto>.<entorno> (por ejemplo tercero.prueba.excerptia.staging): es el destinatario de tus Grants y el installationId que usa tu producto.

Cambiar la identidad de un producto y entorno ya registrado (otro emisor, audiencia o sujeto) la sustituye sin perder la custodia: no hay que volver a aprobarla.

4. El token de tu carga de trabajo

Tu plataforma de ejecución emite un token OIDC (JWT) para tu servicio. Commerce lo acepta solo si:

  • está firmado con RS256 o ES256 (P-256) con una clave del jwks_uri fijado, y lleva kid;
  • iss, sub y aud son exactamente los registrados;
  • lleva exp e iat, vive 24 horas o menos (exp − iat), no ha caducado y no se emitió ni empieza (nbf, si lo lleva) en el futuro, con 30 s de tolerancia de reloj.

Cómo se obtiene en cada plataforma lo explica la documentación de la plataforma; lo que Commerce exige es lo anterior. Verificado en Commerce hoy: Cloud Run (es como se identifican las aplicaciones de Mitnix): GET http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=<audiencia> con la cabecera Metadata-Flavor: Google; dura una hora. En Kubernetes (token de cuenta de servicio proyectado con la audiencia pedida), AWS, Azure o un CI con OIDC rige lo mismo: tu emisor tiene que ser público y descubrible por Commerce desde Internet. Un emisor privado (un clúster sin su documento de descubrimiento público) no se puede registrar.

El SDK no guarda el token ni su caché: llama a tu proveedor en cada petición (tokenProvider({ forceRefresh })) y lo pone solo en Authorization: Bearer. El token nunca aparece en una URL, un cuerpo, un error ni un registro. Tras un 401 pide uno nuevo (forceRefresh: true) y repite una sola vez.

const commerce = createCommerceChannelClient({
  commerceUrl: "https://<la URL de Commerce que te indique Mitnix>",
  tokenProvider: async ({ forceRefresh }) => pedirTokenALaPlataforma({ audience, ignorarCache: forceRefresh }),
});

Para desarrollo y pruebas, sin plataforma de ejecución, puedes usar un emisor OIDC tuyo con client_credentials (es lo que hace la prueba en staging del ejemplo); el ejemplo lo trae como una fuente de token más (MITNIX_TOKEN_URL).

5. El ancla de confianza

Para verificar licencias sin consultar a nadie, tu producto fija un ancla (solo claves públicas, nada secreto):

const ancla: VendorTrustAnchor = {
  vendorId: "acme",                                   // tu identificador de tercero
  environment: "production",                          // el `environment` del paquete que firma Trust (ver nota)
  trustUpdatePublicKey: { kty: "EC", crv: "P-256", x: "…", y: "…" },  // clave pública de la autoridad trust-update de Mitnix
};
  • El paquete de confianza (mitnix.trust.vendor-bundle/v1) lo firma la autoridad trust-update de Mitnix y contiene la raíz de terceros y las autoridades de licencias de tu tercero con sus claves públicas, más sus revocaciones. Si alguien te sirviera un paquete firmado con otra clave, de otro tercero o de otro entorno, applyVendorTrustBundle lo rechaza (VENDOR_BUNDLE_SIGNATURE_INVALID, …_SCOPE_MISMATCH, …_ENVIRONMENT_MISMATCH).
  • Nota sobre environment: es el entorno con el que Trust firma, no el entorno de tu aplicación. Trust solo admite hoy production y lo usa también en staging, de modo que el paquete de staging lleva environment: "production". Compruébalo en el paquete que recibes (payload.environment).
  • Dónde se obtiene: en tu consola de Commerce, «Identidad de tus productos» (https://<tu host>/consola/#/canal), sección Ancla de confianza. La ve cualquier miembro de tu equipo, también el lector. Muestra tu vendorId, el environment que debes poner (el que Trust escribe de verdad en tu paquete, con una nota si no coincide con el entorno de venta) y la clave pública de trust-update en JWK con su huella SHA-256 (RFC 7638). «Copiar ancla» y «Descargar JSON» te dan el objeto VendorTrustAnchor tal cual; «Copiar clave (JWK)» da solo la clave, que es lo que espera el ejemplo en MITNIX_TRUST_UPDATE_JWK o MITNIX_TRUST_UPDATE_JWK_FILE. Commerce la toma de la distribución de confianza de Trust y solo te la muestra si verifica la firma del paquete que te sirve. Si aún no tienes paquete (Trust no ha aprobado tu autoridad de licencias), la sección te lo dice.
  • Comprueba la huella antes de fijar el ancla: es el SHA-256 en base64url del JSON {"crv":"P-256","kty":"EC","x":"…","y":"…"} (claves en ese orden y sin espacios), y debe coincidir con la que ves en la consola.
  • Rotación: cuando Mitnix rota la clave de trust-update, la consola muestra más de una. «En uso» es la que firma el paquete que Commerce te sirve ahora; «Vigente» o «Siguiente» son las que vienen. El ancla del SDK admite una sola clave (trustUpdatePublicKey) y el verificador de Grants vuelve a comprobar el paquete guardado con ella. Por eso el cambio se hace así: prepara la clave nueva y, cuando la consola la marque «En uso», sustituye la del ancla y aplica enseguida el paquete nuevo (applyVendorTrustBundle). Mientras no cambies el ancla, el paquete nuevo se rechaza con VENDOR_BUNDLE_SIGNATURE_INVALID y tu producto sigue con el último que aplicó.

6. El flujo en código

El ejemplo lo implementa en src/licencias.ts (clase ServicioDeLicencias). Los pasos:

// 1. Paquete de confianza: se aplica tal cual lo sirve Commerce (base64url). Si falla, se conserva el último válido.
const entregado = await commerce.trustBundle();             // { vendorId, sequence, bundleId, bundle }
const aplicado = await applyVendorTrustBundle({ bundle: entregado.bundle, anchor: ancla, store: almacenDelPaquete });

// 2. Grants cifrados: uno por cliente (tenant).
const { grants } = await commerce.tenantGrants();           // [{ grantId, encryptedDocument }]

// 3. Abrir el JWE. open-grant pasa por Commerce hasta Trust: el SDK extrae la clave cifrada, la envía y descifra en local.
const abierto = await openTenantGrantJwe(grant.encryptedDocument, commerce.openGrant, { expectedInstallationId: installationId });

// 4. Verificar y aplicar: firma de TU autoridad, destinatario, vigencia, secuencia por tenant…
const runtime = new TenantGrantRuntime(almacenDeTenants, createAnchoredTenantGrantVerifier({ anchor: ancla, bundleStore: almacenDelPaquete }));
const r = await runtime.apply(abierto.value, { installationId, installationKind: "saas", now: new Date().toISOString(), expectedAuthority: { vendorId: ancla.vendorId } });

// 5. Revocaciones: lo que Commerce ha revocado después de firmar.
const estado = await commerce.tenantLicenseStatus();        // { tenants: [{ commercialAccountReference, grantId, revokedAddons, revokedAt?, confirmedEntitlements }] }

Qué garantiza cada paso:

  • openTenantGrantJwe valida el JWE (RSA-OAEP-256 + A256GCM, encrypted_key de 384 bytes), rechaza antes de gastar una operación de Key Vault si el destinatario no es tu instalación, y borra los bytes de la clave de contenido tras usarla. El resultado es el documento firmado sin verificar: descifrar no prueba quién lo firmó.
  • TenantGrantRuntime.apply valida en el orden normativo (TENANT_GRANT_VALIDATION_ORDER). Con expectedAuthority: { vendorId } solo vale tercero-<vendorId>-licencias y sus generaciones -g<n>: una licencia de Mitnix o de otro tercero da LG-CHAIN-INVALID. El verificador anclado solo acepta una autoridad que el último paquete declara activa, sin revocar, vigente cuando firmó y al verificar.
  • Una autoridad revocada en un paquete nuevo hace que sus Grants den LG-AUTHORITY-REVOKED.

Lo que aporta tu producto: los almacenes

El SDK no persiste nada (ni crea temporizadores). Tu producto implementa:

  • VendorTrustBundleStore: el último paquete aplicado y una marca de agua monótona (la mayor secuencia aplicada) en un sitio donde restaurar una copia antigua del paquete no la arrastre. Es el anti-rollback: un paquete viejo está bien firmado, así que solo la secuencia lo detiene.
  • TenantGrantStateStore: el estado protegido por tenant (Grant vigente, secuencia más alta aceptada, Grants sustituidos) con sustitución atómica condicionada.

El ejemplo usa los almacenes efímeros del SDK (createEphemeral…), que no son para producción: no persisten ni resisten un rollback.

7. Capacidades, límites y cuotas

Con el Grant aplicado (AppliedTenantGrant, de store.load(tenantRef)):

Necesitas Función Resultado
¿Puede usar la capacidad ahora? requireTenantCapability(grant, capabilityId, ahora) / tenantHasCapability CAPABILITY_NOT_GRANTED sin Grant vigente, sin la capacidad o con su entitlement vencido
Vigencia del contrato tenantGrantTermStatus(grant, ahora) not-yet-valid, active o expired (desde la entrada en vigor hasta term.validUntil, incluido)
Vigencia de un complemento tenantEntitlementStatus / tenantEntitlementValidUntil Cada entitlement puede tener su validUntil; nunca pasa del fin del contrato
Gracia tras vencer tenantGrantGrace(grant, ahora, politicas) Las políticas (TenantGracePolicy) las declaras tú; el SDK no trae ninguna
Límite de stock (p. ej. usuarios) admitStockOperation(grant, limitId, cuantos, ahora) STOCK_LIMIT_REACHED si ya alcanzaste el máximo
Cuotas por periodo y lotes prepago computeQuotaBalance, planReservation, closeReservation, replayConsumptionLedger Reserva al aceptar, cargo al completar, liberación al fallar; periodo calendar-month-utc y después los lotes por caducidad más próxima

El libro de consumo (ConsumptionLedgerEntry) es tuyo: el saldo se reconstruye desde el Grant y el libro; un libro inconsistente da LEDGER_INCONSISTENT y no produce saldo.

Combina el Grant con el estado de Commerce. El Grant firmado dice qué se compró y hasta cuándo; no dice que luego se devolvió. Commerce lo revoca (devolución, disputa perdida, impago) y lo anuncia en tenant-license-status: revokedAddons (todos los confirmados si el Grant está revocado) y revokedAt. Además, confirmedEntitlements es lo que Commerce confirma como adquirido: una capacidad firmada que no figura ahí no se concede. El ejemplo (describir en licencias.ts) hace esta combinación y falla cerrado: sin estado de Commerce, no concede nada.

8. Frecuencia de consulta recomendada

Es una recomendación del SDK, no un requisito del servidor (Commerce no envía Retry-After hoy).

Consulta Coste Recomendación
tenantLicenseStatus() Solo la base de Commerce; sin firma ni Trust Cada 5 minutos como mucho; es lo que detecta una devolución
trustBundle() Solo la base de Commerce; nunca consulta a Trust ni a Azure Al arrancar y cada hora; es lo que trae una revocación de tu autoridad
tenantGrants() Solo la base de Commerce Al arrancar y cuando tenantLicenseStatus muestre un grantId distinto del aplicado (renovación o ampliación)
openGrant (open-grant) Una operación de Key Vault de Trust por Grant Una vez por grantId: no abras uno ya aplicado ni repitas uno rechazado de forma definitiva mientras el paquete no cambie (el ejemplo lo hace así)
confirmPurge Idempotente Solo cuando Commerce lo pida (ver «Bajas»)

El SDK no reintenta nada salvo el 401, no espera y no sondea: la frecuencia y la espera las decide tu producto, y solo debe repetir lo idempotente (las consultas y confirmPurge).

9. Errores del canal

El cliente lanza CommerceChannelError con code, retryable, retryAfterSeconds?, status? y commerceCode?. El mensaje es un texto fijo del SDK: el del servidor nunca llega a tu producto.

Qué ves code del SDK Qué significa y qué hacer
401 COMMERCE_AUTHENTICATION_FAILED Commerce no te acredita, incluso tras pedir otro token. Revisa en #/canal emisor, audiencia y sujeto, la hora del reloj y que el token vive 24 h o menos. No se arregla repitiendo
503 INSTALLATION_IDENTITY_UNVERIFIABLE COMMERCE_UNAVAILABLE (retryable) El JWKS de tu emisor no respondió a Commerce. No es un fallo de tu producto: repite más tarde
404 NOT_FOUND en trust/bundle COMMERCE_REQUEST_REJECTED Commerce no reconoce el token como la identidad de ningún producto de tercero (el resto de rutas responden 401 en ese caso). Registra el emisor, la audiencia y el sujeto exactos en #/canal, para el producto y el entorno de la instalación
404 INSTALLATION_TRUST_BUNDLE_NOT_AVAILABLE COMMERCE_TRUST_BUNDLE_NOT_AVAILABLE Aún no hay paquete de confianza para tu tercero (la autoridad de licencias todavía no está aprobada). Espera y avisa a Mitnix; no es un fallo de tu producto
403 INSTALLATION_CUSTODY_SCOPE_FORBIDDEN COMMERCE_REQUEST_REJECTED La custodia solo opera con la aplicación y el entorno de tu identidad. Revisa el producto y el entorno registrados
429, otros 5xx, red o plazo COMMERCE_UNAVAILABLE (retryable) Repite más tarde; si hay Retry-After, retryAfterSeconds lo trae (acotado a 3600 s)
400, 422 y otros 4xx COMMERCE_REQUEST_REJECTED Fallo de integración; no se repite
3xx COMMERCE_REDIRECT_REFUSED Commerce no redirige el canal; el SDK rechaza cualquier redirección para que el token no salga del origen
2xx que no cumple el contrato COMMERCE_RESPONSE_INVALID Nunca recibes un dato a medias
Tu proveedor de token falla COMMERCE_TOKEN_UNAVAILABLE Reintentable si el fallo de tu proveedor lo es

Un fallo al abrir un Grant llega como TENANT_GRANT_KEY_OPEN_FAILED con details.causeCode, retryable y retryAfterSeconds (solo si tienen forma segura). Los rechazos de verificación (LG-CHAIN-INVALID, LG-SIGNATURE-INVALID, LG-AUTHORITY-REVOKED, LG-RECIPIENT-MISMATCH, LG-NOT-YET-VALID, LG-SUPERSEDED…) son definitivos para ese documento. El ejemplo los traduce a una acción en src/errores.ts. La lista completa de códigos está en los tipos que exportan los paquetes: CommerceChannelErrorCode en platform-commerce-client, y TenantGrantValidationCode y TenantGrantJweErrorCode en platform-licensing/tenant-grants.

10. Bajas y devoluciones

  • Devolución o disputa perdida: Commerce revoca la licencia; la verás en tenantLicenseStatus() (revokedAddons, revokedAt). El Grant firmado sigue siendo válido hasta su term.validUntil, así que tu producto debe consultar el estado y dejar de conceder.
  • Fin del contrato: tenantDispositions() indica, por tenant, si debes bloquear y conservar los datos (block-and-retain) o borrarlos (purge), con dataRetainedUntil. Tras una purga, confirmPurge({ commercialAccountReference, grantId, scope? }) confirma a Commerce que la hiciste.

11. Probar tu integración

  1. Unitarias, sin red: el ejemplo prueba todo el flujo contra un Commerce simulado a nivel HTTP (commerceSimulado en src/pruebas/mundo.ts) con claves reales (paquete firmado, Grant firmado por tu autoridad y por la de otro tercero, JWE cifrado con RSA-3072) y el SDK sin dobles. Puedes copiar ese doble para probar tu producto.
  2. De punta a punta, contra el Commerce de staging con un tercero de pruebas: registra la identidad de tu producto de pruebas, compra con tarjeta de prueba y ejecuta el ejemplo con tu token (ver su README).

12. Qué no hace el SDK

No guarda el token, no persiste el paquete ni los Grants, no abre conexiones propias fuera de fetch, no crea temporizadores ni sondea, no reintenta salvo un 401 y no envía nunca tu vendorId ni un secreto. Todo eso es de tu producto, a propósito: así el SDK no necesita ningún permiso en tu entorno.

Producto de ejemplo

Un producto SaaS mínimo (Node 22, TypeScript estricto) que hace todo el recorrido de esta guía con los paquetes publicados en npmjs. Sus pruebas se ejecutan sin red contra un Commerce simulado con claves reales.

tar -xzf mitnix-ejemplo-tercero-saas.tar.gz
cd mitnix-ejemplo-tercero-saas
npm install
npm test
Descargar el ejemplo

SHA-256: eccf2461e0eb9e0e36eb277c9eb6b9a39f46822cce076172f1d558ad0c0b1e54