feat(Production): Complete production deployment infrastructure

- Add comprehensive health check system with multiple endpoints
- Add Prometheus metrics endpoint
- Add production logging configurations (5 strategies)
- Add complete deployment documentation suite:
  * QUICKSTART.md - 30-minute deployment guide
  * DEPLOYMENT_CHECKLIST.md - Printable verification checklist
  * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle
  * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference
  * production-logging.md - Logging configuration guide
  * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation
  * README.md - Navigation hub
  * DEPLOYMENT_SUMMARY.md - Executive summary
- Add deployment scripts and automation
- Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment
- Update README with production-ready features

All production infrastructure is now complete and ready for deployment.
This commit is contained in:
2025-10-25 19:18:37 +02:00
parent caa85db796
commit fc3d7e6357
83016 changed files with 378904 additions and 20919 deletions

View File

@@ -0,0 +1,145 @@
<?php
declare(strict_types=1);
namespace App\Framework\LiveComponents\Serialization;
use App\Framework\LiveComponents\Exceptions\StateEncryptionException;
/**
* Encrypts serialized state data for LiveComponents
*
* Decorator pattern - wraps state serialization with encryption layer.
* Uses StateEncryptor for authenticated encryption (AES-256-GCM via libsodium).
*
* Framework Principles:
* - Readonly class with composition over inheritance
* - Decorator pattern for transparent encryption layer
* - Value Objects for all inputs/outputs
* - Explicit exceptions for all error conditions
*
* Usage:
* Components can opt-in to encryption via attribute:
* #[EncryptedState]
* final readonly class PaymentFormComponent implements LiveComponentContract
*
* @see StateEncryptor For encryption implementation
*/
final readonly class EncryptedStateSerializer
{
public function __construct(
private StateEncryptor $encryptor
) {
}
/**
* Serialize and encrypt state data
*
* @param object $state The state Value Object (must implement toArray())
* @return string Base64-encoded encrypted serialized state
* @throws StateEncryptionException if encryption fails
*/
public function serialize(object $state): string
{
try {
// 1. Convert state object to array
if (! method_exists($state, 'toArray')) {
throw new \InvalidArgumentException(
'State object must implement toArray() method. ' .
'Got: ' . get_class($state)
);
}
$stateArray = $state->toArray();
// 2. Serialize to JSON
$json = json_encode($stateArray, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
if ($json === false) {
throw new StateEncryptionException(
'Failed to serialize state to JSON: ' . json_last_error_msg()
);
}
// 3. Encrypt the serialized data
return $this->encryptor->encrypt($json);
} catch (StateEncryptionException $e) {
throw $e; // Re-throw encryption exceptions
} catch (\JsonException $e) {
throw StateEncryptionException::encryptionFailed(
'JSON serialization failed: ' . $e->getMessage(),
$e
);
} catch (\Throwable $e) {
throw StateEncryptionException::encryptionFailed(
'Serialization failed: ' . $e->getMessage(),
$e
);
}
}
/**
* Decrypt and deserialize state data
*
* @param string $encrypted Base64-encoded encrypted serialized state
* @param string $className The state class name to deserialize to
* @return object The deserialized state Value Object
* @throws StateEncryptionException if decryption or deserialization fails
*/
public function deserialize(string $encrypted, string $className): object
{
try {
// 1. Decrypt the data
$json = $this->encryptor->decrypt($encrypted);
// 2. Deserialize from JSON
$stateArray = json_decode($json, associative: true, flags: JSON_THROW_ON_ERROR);
if (! is_array($stateArray)) {
throw StateEncryptionException::dataCorrupted(
'Decrypted data is not a valid array'
);
}
// 3. Create state object from array
if (! method_exists($className, 'fromArray')) {
throw new \InvalidArgumentException(
"State class {$className} must implement fromArray() method"
);
}
$state = $className::fromArray($stateArray);
if (! is_object($state)) {
throw StateEncryptionException::dataCorrupted(
"fromArray() did not return an object"
);
}
return $state;
} catch (StateEncryptionException $e) {
throw $e; // Re-throw encryption exceptions
} catch (\JsonException $e) {
throw StateEncryptionException::decryptionFailed(
'JSON deserialization failed: ' . $e->getMessage(),
$e
);
} catch (\Throwable $e) {
throw StateEncryptionException::decryptionFailed(
'Deserialization failed: ' . $e->getMessage(),
$e
);
}
}
/**
* Check if data is encrypted
*
* @param string $data The data to check
* @return bool True if data appears to be encrypted
*/
public function isEncrypted(string $data): bool
{
return $this->encryptor->isEncrypted($data);
}
}

View File

@@ -0,0 +1,197 @@
<?php
declare(strict_types=1);
namespace App\Framework\LiveComponents\Serialization;
use App\Framework\Cryptography\CryptographicUtilities;
use App\Framework\LiveComponents\Exceptions\StateEncryptionException;
use App\Framework\Random\RandomGenerator;
/**
* Encrypts and decrypts LiveComponent state data
*
* Uses authenticated encryption (AES-256-GCM) via libsodium to protect sensitive state data.
* Each encryption operation generates a unique nonce for security.
*
* Framework Principles:
* - Readonly class with immutable encryption key
* - Uses framework's CryptographicUtilities and RandomGenerator
* - Value Objects for inputs/outputs
* - Explicit exceptions for all error conditions
* - No inheritance - composition only
*
* @see CryptographicUtilities For cryptographic primitives
* @see RandomGenerator For secure random number generation
*/
final readonly class StateEncryptor
{
private const ENCRYPTION_VERSION = 1;
private const NONCE_LENGTH = 24; // SODIUM_CRYPTO_SECRETBOX_NONCEBYTES
public function __construct(
private string $encryptionKey,
private CryptographicUtilities $crypto,
private RandomGenerator $random
) {
if (! extension_loaded('sodium')) {
throw new \RuntimeException(
'Sodium extension required for StateEncryptor. Install via: apt-get install php8.5-sodium'
);
}
if (strlen($this->encryptionKey) !== SODIUM_CRYPTO_SECRETBOX_KEYBYTES) {
throw new \InvalidArgumentException(
'Encryption key must be exactly ' . SODIUM_CRYPTO_SECRETBOX_KEYBYTES . ' bytes (32 bytes). ' .
'Generate via: bin/console generate:encryption-key'
);
}
// Validate key entropy (4.0 bits/byte is realistic minimum for cryptographically secure random data)
// Perfect random data has ~5 bits/byte Shannon entropy, not 8 (theoretical max for uniform distribution)
if (! $this->crypto->validateEntropy($this->encryptionKey, 4.0)) {
throw new \InvalidArgumentException(
'Encryption key has insufficient entropy. Use a cryptographically secure key.'
);
}
}
/**
* Encrypt state data
*
* @param string $plaintext The serialized state data to encrypt
* @return string Base64-encoded encrypted data with nonce prepended
* @throws StateEncryptionException if encryption fails
*/
public function encrypt(string $plaintext): string
{
try {
// Generate unique nonce using framework's CryptographicUtilities
$nonce = $this->crypto->generateNonce(self::NONCE_LENGTH);
// Authenticated encryption (encrypt + MAC) using libsodium
$ciphertext = sodium_crypto_secretbox($plaintext, $nonce, $this->encryptionKey);
if ($ciphertext === false) {
throw StateEncryptionException::encryptionFailed('Encryption operation failed');
}
// Prepend version byte + nonce to ciphertext
$versionByte = chr(self::ENCRYPTION_VERSION);
$encrypted = $versionByte . $nonce . $ciphertext;
// Clear sensitive data from memory using framework utility
$plaintextCopy = $plaintext;
$nonceCopy = $nonce;
$this->crypto->secureWipe($plaintextCopy);
$this->crypto->secureWipe($nonceCopy);
// Base64 encode for safe storage/transmission
return base64_encode($encrypted);
} catch (StateEncryptionException $e) {
throw $e; // Re-throw our exceptions
} catch (\Throwable $e) {
throw StateEncryptionException::encryptionFailed(
'Failed to encrypt state data: ' . $e->getMessage(),
$e
);
}
}
/**
* Decrypt state data
*
* @param string $encrypted Base64-encoded encrypted data with nonce prepended
* @return string The decrypted plaintext state data
* @throws StateEncryptionException if decryption fails or data is corrupted
*/
public function decrypt(string $encrypted): string
{
try {
// Decode from base64
$decoded = base64_decode($encrypted, strict: true);
if ($decoded === false) {
throw StateEncryptionException::decryptionFailed('Invalid base64 encoding');
}
// Validate minimum length: version (1) + nonce (24) + at least some ciphertext (1)
$minLength = 1 + self::NONCE_LENGTH + 1;
if (strlen($decoded) < $minLength) {
throw StateEncryptionException::decryptionFailed(
'Encrypted data too short - possible corruption'
);
}
// Extract version byte
$version = ord($decoded[0]);
if ($version !== self::ENCRYPTION_VERSION) {
throw StateEncryptionException::decryptionFailed(
"Unsupported encryption version: {$version}. Expected: " . self::ENCRYPTION_VERSION
);
}
// Extract nonce (24 bytes)
$nonce = substr($decoded, 1, self::NONCE_LENGTH);
// Extract ciphertext (remaining bytes)
$ciphertext = substr($decoded, 1 + self::NONCE_LENGTH);
// Authenticated decryption (verify MAC + decrypt) using libsodium
$plaintext = sodium_crypto_secretbox_open($ciphertext, $nonce, $this->encryptionKey);
if ($plaintext === false) {
throw StateEncryptionException::decryptionFailed(
'Decryption failed - data may be corrupted or tampered with (MAC verification failed)'
);
}
// Clear sensitive data from memory using framework utility
$nonceCopy = $nonce;
$ciphertextCopy = $ciphertext;
$this->crypto->secureWipe($nonceCopy);
$this->crypto->secureWipe($ciphertextCopy);
return $plaintext;
} catch (StateEncryptionException $e) {
throw $e; // Re-throw our exceptions
} catch (\Throwable $e) {
throw StateEncryptionException::decryptionFailed(
'Failed to decrypt state data: ' . $e->getMessage(),
$e
);
}
}
/**
* Verify if data is encrypted (has version byte + nonce + ciphertext)
*
* @param string $data The data to check
* @return bool True if data appears to be encrypted
*/
public function isEncrypted(string $data): bool
{
try {
$decoded = base64_decode($data, strict: true);
if ($decoded === false) {
return false;
}
// Must have at least: version byte (1) + nonce (24) + some ciphertext (>0)
$minLength = 1 + self::NONCE_LENGTH;
if (strlen($decoded) <= $minLength) {
return false;
}
// Check version byte is valid
$version = ord($decoded[0]);
return $version === self::ENCRYPTION_VERSION;
} catch (\Throwable) {
return false;
}
}
}