puyu-pe / sipro-internal-api-core
Framework-agnostic core contracts, HMAC security utilities, and API response/error standards for SIPRO internal API.
Package info
github.com/puyu-pe/sipro-internal-api-core
pkg:composer/puyu-pe/sipro-internal-api-core
Requires
- php: >=8.1
Requires (Dev)
- phpunit/phpunit: ^10.5
This package is auto-updated.
Last update: 2026-03-25 07:26:35 UTC
README
Paquete Composer framework-agnostic para integraciones entre SIPRO Control Plane y los SaaS en /internal/v1.
¿Qué resuelve este paquete?
- Contracts (DTOs) para provisioning, ciclo de vida y clonado de tenants.
- HMAC para firmado y verificación de requests entre servicios.
- Errores estándar para respuestas JSON consistentes (
ErrorResponse).
Objetivo: reducir código repetido y evitar diferencias de implementación entre servicios.
Payload de ejemplo: ProvisionPayloadDTO (createTenant)
{
"project": {
"name": "Acme Suite",
"code": "ACME",
"description": "Suite principal",
"billingCycle": "monthly",
"priceAgreed": 199.9,
"startDate": "2026-03-25",
"renewalDate": "2027-03-25",
"execStatus": "active",
"isActive": true,
"accessUrlCustom": "https://acme.sipro.app",
"accessUrls": {
"app": "https://acme.sipro.app",
"api": "https://api.acme.sipro.app"
},
"appKey": "acme-app-001",
"logo": null,
"address": "Av. Demo 123",
"phone": "+51 1 5555555",
"email": "admin@acme.pe",
"ubigeo": "150101",
"latitud": -12.0464,
"longitud": -77.0428,
"color": "#004c97",
"notes": "Cliente migrado desde legacy"
},
"client": {
"ruc": "20123456789",
"businessName": "Acme SAC",
"tradeName": "Acme"
},
"services": [
{
"key": "billing",
"externalId": "srv-01",
"code": "BILL",
"name": "Facturacion",
"description": "Modulo de facturacion",
"priceList": 99.0,
"defaultBillingCycle": "monthly",
"type": "core",
"accessUrl": "https://acme.sipro.app/billing",
"logo": null,
"credentials": [
{
"name": "Admin Acme",
"username": "admin",
"email": "admin@acme.pe",
"role": "owner",
"initialPassword": "Temporal123!",
"mustChangePassword": true
}
],
"modules": [
{
"id": 10,
"externalId": "mod-01",
"name": "Ventas",
"description": "Ventas y cotizaciones",
"price": 20.0,
"isUnlimited": false,
"customPrice": null,
"quantity": 5
}
]
}
],
"metadata": {
"source": "control-plane",
"priority": "high"
}
}
Ciclo de vida del tenant (warn / suspend / activate)
TenantLifecycleRequestDTO (payload estandar para warn/suspend/activate):
{
"appKey": "acme-app-001",
"projectCode": "ACME",
"reason": "PAYMENT_OVERDUE",
"requestedAt": "2026-03-25T10:00:00Z"
}
TenantLifecycleResponseDTO:
{
"appKey": "acme-app-001",
"projectCode": "ACME",
"status": "ok",
"systemStatus": "suspended"
}
Clonado de tenant (export / import)
TenantExportRequestDTO:
{
"appKey": "acme-app-001",
"projectCode": "ACME",
"reason": "MIGRATION"
}
TenantExportResponseDTO:
{
"appKey": "acme-app-001",
"projectCode": "ACME",
"dumpPath": "/mnt/backups/acme-20260325.sql.gz",
"checksum": "sha256:3f4b8c...",
"createdAt": "2026-03-25T10:35:00Z"
}
TenantImportRequestDTO:
{
"appKey": "acme-app-001",
"projectCode": "ACME",
"dumpPath": "/mnt/backups/acme-20260325.sql.gz",
"checksum": "sha256:3f4b8c..."
}
TenantImportResponseDTO:
{
"appKey": "acme-app-001",
"projectCode": "ACME",
"database": "acme_20260325",
"restored": true
}
Interfaces de adapters
Provisioning:
TenantProvisioningAdapterInterface::createTenant(ProvisionPayloadDTO $dto): ProvisionResponseDTO
Ciclo de vida:
TenantLifecycleAdapterInterface::warnTenant(string $appKey, TenantLifecycleRequestDTO $dto): TenantLifecycleResponseDTOTenantLifecycleAdapterInterface::suspendTenant(string $appKey, TenantLifecycleRequestDTO $dto): TenantLifecycleResponseDTOTenantLifecycleAdapterInterface::activateTenant(string $appKey, TenantLifecycleRequestDTO $dto): TenantLifecycleResponseDTO
Clonado:
TenantCloneAdapterInterface::exportTenant(string $appKey, TenantExportRequestDTO $dto): TenantExportResponseDTOTenantCloneAdapterInterface::importTenant(string $appKey, TenantImportRequestDTO $dto): TenantImportResponseDTO
Nota: TenantAdapterInterface agrupa provisioning + ciclo de vida. El clonado se mantiene separado en TenantCloneAdapterInterface.
Firma HMAC (Control Plane) — pasos
Headers requeridos:
X-Internal-KeyIdX-Internal-TimestampX-Internal-NonceX-Internal-Signature
Canonical string v1 exacto:
{METHOD}\n{PATH}\n{TIMESTAMP}\n{NONCE}\n{BODY_SHA256_HEX}
<?php use PuyuPe\SiproInternalApiCore\Security\Hmac\CanonicalRequest; use PuyuPe\SiproInternalApiCore\Security\Hmac\HmacSigner; $method = 'POST'; $path = '/internal/v1/tenants'; $rawBody = json_encode(['tenant_uuid' => '6fd22e43-c8a7-4f02-9f8f-31157a4f1b74'], JSON_THROW_ON_ERROR); $timestamp = (string) time(); $nonce = bin2hex(random_bytes(16)); $keyId = 'cp-key-01'; $secret = 'super-shared-secret'; // 1) BODY_SHA256_HEX $bodyHash = CanonicalRequest::bodySha256Hex($rawBody); // 2) Canonical string $canonical = CanonicalRequest::build($method, $path, $timestamp, $nonce, $rawBody); // 3) Firma con secret (hex por defecto) $signer = new HmacSigner(); $signature = $signer->sign($canonical, $secret, 'sha256', 'hex'); // 4) Armar headers $headers = [ 'X-Internal-KeyId' => $keyId, 'X-Internal-Timestamp' => $timestamp, 'X-Internal-Nonce' => $nonce, 'X-Internal-Signature' => $signature, ];
Verificación HMAC en SaaS con HmacVerifier
<?php use PuyuPe\SiproInternalApiCore\Security\Hmac\HmacVerifier; use PuyuPe\SiproInternalApiCore\Security\Hmac\NonceStoreInterface; final class RedisNonceStore implements NonceStoreInterface { public function has(string $nonce): bool { // GET nonce return false; } public function put(string $nonce, int $ttlSeconds): void { // SETEX nonce ttlSeconds 1 } } $verifier = new HmacVerifier(allowedClockSkewSeconds: 300); $result = $verifier->verify( method: 'POST', path: '/internal/v1/tenants', rawBody: $rawBody, headers: $headers, // resolveSecretByKeyId: lookup seguro por KeyId resolveSecretByKeyId: function (string $keyId): ?string { return $keyId === 'cp-key-01' ? 'super-shared-secret' : null; }, nonceStore: new RedisNonceStore(), // opcional, recomendado en producción ); if (! $result->ok) { // errorCode: VALIDATION_ERROR | REQUEST_EXPIRED | NONCE_REPLAY | INVALID_SIGNATURE }
Notas prácticas:
HmacVerifiervalida timestamp dentro de ±300s (configurable).NonceStoreInterfaceevita replay. Implementación típica:- Redis (
SETEXpor nonce), o - tabla en DB master con TTL/fecha de expiración.
- Redis (
- Si no envías
nonceStore, se verifica firma/timestamp pero sin protección anti-replay.
Ejemplos de ErrorResponse
1) VALIDATION_ERROR con errores por campo
{
"ok": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Validation failed.",
"details": {
"errors": [
{"field": "tenant_uuid", "code": "invalid_uuid_v4", "message": "tenant_uuid must be a valid UUID v4."},
{"field": "admin_user.email", "code": "invalid_email", "message": "admin_user.email must be a valid email address."}
]
}
}
}
2) INVALID_SIGNATURE
{
"ok": false,
"error": {
"code": "INVALID_SIGNATURE",
"message": "Invalid request signature."
}
}
Notas de seguridad
- No loguear secrets (ni secretos HMAC ni credenciales de DB).
- No exponer connection strings ni tokens sensibles en
error.details. - Usa
keyIdpara resolver secretos de forma rotativa y segura.
Versionado y compatibilidad
- Este paquete está orientado al contrato
/internal/v1. - Cambios incompatibles deben versionarse como nueva versión mayor del paquete y/o nueva ruta (
/internal/v2). - Mantén signer/verifier con el mismo formato canonical v1 para asegurar interoperabilidad.