-
Privacy
-
Terms
-
+ Privacy
+
Terms
+
Powered by {{ $appName }}
diff --git a/packages/core-php/src/Core/Front/Components/View/Blade/forms/button.blade.php b/packages/core-php/src/Core/Front/Components/View/Blade/forms/button.blade.php
index 7d0d154..27de20d 100644
--- a/packages/core-php/src/Core/Front/Components/View/Blade/forms/button.blade.php
+++ b/packages/core-php/src/Core/Front/Components/View/Blade/forms/button.blade.php
@@ -6,7 +6,7 @@ Wraps flux:button with built-in authorization checking.
Usage:
Save
Save
-
Save
+
Save
--}}
@props([
diff --git a/packages/core-php/src/Core/Front/Components/View/Blade/forms/checkbox.blade.php b/packages/core-php/src/Core/Front/Components/View/Blade/forms/checkbox.blade.php
index 01f155f..a441fe3 100644
--- a/packages/core-php/src/Core/Front/Components/View/Blade/forms/checkbox.blade.php
+++ b/packages/core-php/src/Core/Front/Components/View/Blade/forms/checkbox.blade.php
@@ -5,7 +5,7 @@ Wraps flux:checkbox with built-in authorization checking.
Usage:
-
+
--}}
@props([
diff --git a/packages/core-php/src/Core/Front/Components/View/Blade/forms/input.blade.php b/packages/core-php/src/Core/Front/Components/View/Blade/forms/input.blade.php
index 4b23a42..bcf15dc 100644
--- a/packages/core-php/src/Core/Front/Components/View/Blade/forms/input.blade.php
+++ b/packages/core-php/src/Core/Front/Components/View/Blade/forms/input.blade.php
@@ -5,7 +5,7 @@ Wraps flux:input with built-in authorization checking.
Usage:
-
+
--}}
@props([
diff --git a/packages/core-php/src/Core/Front/Components/View/Blade/forms/select.blade.php b/packages/core-php/src/Core/Front/Components/View/Blade/forms/select.blade.php
index cf3f2ff..bbba146 100644
--- a/packages/core-php/src/Core/Front/Components/View/Blade/forms/select.blade.php
+++ b/packages/core-php/src/Core/Front/Components/View/Blade/forms/select.blade.php
@@ -9,7 +9,7 @@ Usage:
Dark
-
+
Light
Dark
diff --git a/packages/core-php/src/Core/Front/Components/View/Blade/forms/textarea.blade.php b/packages/core-php/src/Core/Front/Components/View/Blade/forms/textarea.blade.php
index 69c4341..540aa82 100644
--- a/packages/core-php/src/Core/Front/Components/View/Blade/forms/textarea.blade.php
+++ b/packages/core-php/src/Core/Front/Components/View/Blade/forms/textarea.blade.php
@@ -5,7 +5,7 @@ Wraps flux:textarea with built-in authorization checking.
Usage:
-
+
--}}
@props([
diff --git a/packages/core-php/src/Core/Front/Components/View/Blade/forms/toggle.blade.php b/packages/core-php/src/Core/Front/Components/View/Blade/forms/toggle.blade.php
index 7e65616..689fae7 100644
--- a/packages/core-php/src/Core/Front/Components/View/Blade/forms/toggle.blade.php
+++ b/packages/core-php/src/Core/Front/Components/View/Blade/forms/toggle.blade.php
@@ -6,7 +6,7 @@ Wraps flux:switch with built-in authorization checking.
Usage:
-
+
--}}
@props([
diff --git a/packages/core-php/src/Core/Front/HLCRF.md b/packages/core-php/src/Core/Front/HLCRF.md
new file mode 100644
index 0000000..3fd9c76
--- /dev/null
+++ b/packages/core-php/src/Core/Front/HLCRF.md
@@ -0,0 +1,246 @@
+# HLCRF Compositor
+
+**H**ierarchical **L**ayer **C**ompositing **R**ender **F**rame
+
+A data-driven layout system where each composite contains up to five regions - Header, Left, Content, Right, Footer. Composites nest infinitely: any region can contain another composite.
+
+## Quick Start
+
+```php
+use Core\Front\Components\Layout;
+
+// Simple page layout
+$page = Layout::make('HCF')
+ ->h('Navigation ')
+ ->c('Main content ')
+ ->f('');
+
+echo $page;
+```
+
+## The Five Regions
+
+| Letter | Region | HTML Element | Purpose |
+|--------|---------|--------------|---------|
+| **H** | Header | `` | Top navigation, branding |
+| **L** | Left | `` | Left sidebar |
+| **C** | Content | `` | Primary content |
+| **R** | Right | `` | Right sidebar |
+| **F** | Footer | `` | Site footer |
+
+## Variant Strings
+
+The variant string defines which regions are active. What's missing defines the layout type.
+
+| Variant | Description | Use Case |
+|---------|-------------|----------|
+| `C` | Content only | Widgets, embeds |
+| `HCF` | Header, Content, Footer | Standard page |
+| `HLCF` | + Left sidebar | Admin panel |
+| `HLCRF` | All regions | Full dashboard |
+
+```
+HCF layout:
+┌─────────────────────────┐
+│ H │
+├─────────────────────────┤
+│ C │
+├─────────────────────────┤
+│ F │
+└─────────────────────────┘
+
+HLCRF layout:
+┌─────────────────────────┐
+│ H │
+├───────┬─────────┬───────┤
+│ L │ C │ R │
+├───────┴─────────┴───────┤
+│ F │
+└─────────────────────────┘
+```
+
+## Nested Layouts
+
+Any region can contain another layout. The path system tracks hierarchy:
+
+```php
+$sidebar = Layout::make('HCF')
+ ->h('Widget ')
+ ->c('')
+ ->f('More ');
+
+$page = Layout::make('HLCF')
+ ->h(view('header'))
+ ->l($sidebar) // Nested layout
+ ->c(view('content'))
+ ->f(view('footer'));
+```
+
+The sidebar's regions receive paths: `L-H`, `L-C`, `L-F`.
+
+## Inline Nesting Syntax
+
+Declare nested structures in a single string using brackets:
+
+```
+H[LC]CF = Header contains a Left-Content layout, plus root Content and Footer
+
+┌─────────────────────────────────┐
+│ H ┌───────────┬───────────────┐ │
+│ │ H-L │ H-C │ │
+│ └───────────┴───────────────┘ │
+├─────────────────────────────────┤
+│ C │
+├─────────────────────────────────┤
+│ F │
+└─────────────────────────────────┘
+```
+
+## Path-Based IDs
+
+Every element has a unique, deterministic address:
+
+```
+L-H-0
+│ │ └─ Block index (first block)
+│ └─── Region in nested layout (Header)
+└───── Region in root layout (Left)
+```
+
+Examples:
+- `H-0` - First block in root Header
+- `L-C-2` - Third block in Content of layout nested in Left
+- `C-F-C-0` - First block in Content of layout nested in Footer of layout nested in Content
+
+## API
+
+### Factory
+
+```php
+Layout::make(string $variant = 'HCF', string $path = ''): static
+```
+
+### Slot Methods
+
+```php
+->h(mixed ...$items) // Header
+->l(mixed ...$items) // Left
+->c(mixed ...$items) // Content
+->r(mixed ...$items) // Right
+->f(mixed ...$items) // Footer
+```
+
+Accepts: strings, `Htmlable`, `Renderable`, `View`, nested `Layout`, callables.
+
+### Attributes
+
+```php
+->attributes(['id' => 'main'])
+->class('my-layout')
+```
+
+### Rendering
+
+```php
+->render(): string
+->toHtml(): string
+(string) $layout
+```
+
+## Generated HTML
+
+```html
+
+```
+
+## CSS
+
+Base styles for the grid structure:
+
+```css
+.hlcrf-layout {
+ display: flex;
+ flex-direction: column;
+ min-height: 100%;
+}
+
+.hlcrf-body {
+ display: flex;
+ flex: 1;
+}
+
+.hlcrf-content {
+ flex: 1;
+}
+
+.hlcrf-left,
+.hlcrf-right {
+ flex-shrink: 0;
+}
+
+/* Responsive: collapse sidebars on tablet */
+@media (max-width: 1023px) {
+ .hlcrf-left,
+ .hlcrf-right {
+ display: none;
+ }
+}
+```
+
+## Integration
+
+### Livewire
+
+```php
+$layout = Layout::make('HLCF')
+ ->h(livewire('nav'))
+ ->l(livewire('sidebar'))
+ ->c(livewire('content'))
+ ->f(livewire('footer'));
+```
+
+### Blade
+
+```blade
+{!! Core\Front\Components\Layout::make('HCF')
+ ->h(view('partials.header'))
+ ->c($slot)
+ ->f(view('partials.footer'))
+!!}
+```
+
+### JSON Configuration
+
+Store layout config in a JSON column:
+
+```json
+{
+ "layout_type": {
+ "desktop": "HLCRF",
+ "tablet": "HCF",
+ "phone": "CF"
+ }
+}
+```
+
+## Why HLCRF?
+
+1. **Data-driven** - Layout is data, not templates
+2. **Composable** - Infinite nesting with automatic path tracking
+3. **Portable** - A string describes the entire structure
+4. **Semantic** - Maps to HTML5 landmark elements
+5. **Simple** - Five regions, predictable behaviour
+
+---
+
+*Location: `Core\Front\Components\Layout`*
diff --git a/packages/core-php/src/Core/Headers/Boot.php b/packages/core-php/src/Core/Headers/Boot.php
index 88f09ee..82c2045 100644
--- a/packages/core-php/src/Core/Headers/Boot.php
+++ b/packages/core-php/src/Core/Headers/Boot.php
@@ -10,7 +10,10 @@ declare(strict_types=1);
namespace Core\Headers;
+use Core\Headers\Livewire\HeaderConfigurationManager;
+use Illuminate\Support\Facades\Blade;
use Illuminate\Support\ServiceProvider;
+use Livewire\Livewire;
/**
* Headers Module Service Provider.
@@ -19,6 +22,116 @@ use Illuminate\Support\ServiceProvider;
* - Device detection (User-Agent parsing)
* - GeoIP lookups (from headers or database)
* - Configurable security headers (CSP, Permissions-Policy, etc.)
+ * - CSP nonce generation for inline scripts/styles
+ * - Header configuration UI via Livewire component
+ *
+ * ## Content Security Policy (CSP) Configuration
+ *
+ * Configure CSP via `config/headers.php` (published) or environment variables.
+ *
+ * ### Quick Reference
+ *
+ * | Option | Environment Variable | Default | Description |
+ * |--------|---------------------|---------|-------------|
+ * | `csp.enabled` | `SECURITY_CSP_ENABLED` | `true` | Enable/disable CSP entirely |
+ * | `csp.report_only` | `SECURITY_CSP_REPORT_ONLY` | `false` | Log violations without blocking |
+ * | `csp.report_uri` | `SECURITY_CSP_REPORT_URI` | `null` | URL for violation reports |
+ * | `csp.nonce_enabled` | `SECURITY_CSP_NONCE_ENABLED` | `true` | Enable nonce-based CSP |
+ * | `csp.nonce_length` | `SECURITY_CSP_NONCE_LENGTH` | `16` | Nonce length in bytes (128 bits) |
+ *
+ * ### CSP Directives
+ *
+ * Default directives are configured in `config/headers.php` under `csp.directives`:
+ *
+ * ```php
+ * 'directives' => [
+ * 'default-src' => ["'self'"],
+ * 'script-src' => ["'self'"],
+ * 'style-src' => ["'self'", 'https://fonts.bunny.net'],
+ * 'img-src' => ["'self'", 'data:', 'https:', 'blob:'],
+ * 'font-src' => ["'self'", 'https://fonts.bunny.net'],
+ * 'connect-src' => ["'self'"],
+ * 'frame-src' => ["'self'", 'https://www.youtube.com'],
+ * 'frame-ancestors' => ["'self'"],
+ * 'base-uri' => ["'self'"],
+ * 'form-action' => ["'self'"],
+ * 'object-src' => ["'none'"],
+ * ],
+ * ```
+ *
+ * ### Environment-Specific Overrides
+ *
+ * Different environments can have different CSP rules:
+ *
+ * ```php
+ * 'environment' => [
+ * 'local' => [
+ * 'script-src' => ["'unsafe-inline'", "'unsafe-eval'"],
+ * 'style-src' => ["'unsafe-inline'"],
+ * ],
+ * 'production' => [
+ * // Production should be strict - nonces replace unsafe-inline
+ * ],
+ * ],
+ * ```
+ *
+ * ### Nonce-Based CSP
+ *
+ * Nonces provide secure inline script/style support without `'unsafe-inline'`.
+ *
+ * #### In Blade Templates
+ *
+ * ```blade
+ * {{-- Using the helper function --}}
+ *
+ *
+ * {{-- Using the Blade directive --}}
+ *
+ *
+ * {{-- Just the nonce value --}}
+ *
+ * ```
+ *
+ * #### Nonce Skip Environments
+ *
+ * In local/development environments, nonces are skipped by default to allow
+ * hot reload and dev tools. Configure via `csp.nonce_skip_environments`:
+ *
+ * ```php
+ * 'nonce_skip_environments' => ['local', 'development'],
+ * ```
+ *
+ * ### External Service Sources
+ *
+ * Enable third-party services via environment variables:
+ *
+ * | Service | Environment Variable | Sources Added |
+ * |---------|---------------------|---------------|
+ * | jsDelivr | `SECURITY_CSP_JSDELIVR` | cdn.jsdelivr.net |
+ * | unpkg | `SECURITY_CSP_UNPKG` | unpkg.com |
+ * | Google Analytics | `SECURITY_CSP_GOOGLE_ANALYTICS` | googletagmanager.com, google-analytics.com |
+ * | Facebook | `SECURITY_CSP_FACEBOOK` | connect.facebook.net, facebook.com |
+ *
+ * ### Other Security Headers
+ *
+ * | Header | Option | Default |
+ * |--------|--------|---------|
+ * | Strict-Transport-Security | `hsts.enabled` | `true` (production only) |
+ * | X-Frame-Options | `x_frame_options` | `SAMEORIGIN` |
+ * | X-Content-Type-Options | `x_content_type_options` | `nosniff` |
+ * | X-XSS-Protection | `x_xss_protection` | `1; mode=block` |
+ * | Referrer-Policy | `referrer_policy` | `strict-origin-when-cross-origin` |
+ * | Permissions-Policy | `permissions.enabled` | `true` |
+ *
+ * @see SecurityHeaders For the middleware implementation
+ * @see CspNonceService For nonce generation
+ * @see config/headers.php For full configuration reference
*/
class Boot extends ServiceProvider
{
@@ -31,6 +144,9 @@ class Boot extends ServiceProvider
$this->app->singleton(DetectDevice::class);
$this->app->singleton(DetectLocation::class);
+
+ // Register CSP nonce service as singleton (one nonce per request)
+ $this->app->singleton(CspNonceService::class);
}
/**
@@ -38,6 +154,50 @@ class Boot extends ServiceProvider
*/
public function boot(): void
{
- //
+ $this->loadViewsFrom(__DIR__.'/Views', 'core');
+
+ $this->registerBladeDirectives();
+ $this->registerHelperFunctions();
+ $this->registerLivewireComponents();
+ }
+
+ /**
+ * Register Blade directives for CSP nonces.
+ */
+ protected function registerBladeDirectives(): void
+ {
+ // @cspnonce - Outputs the nonce attribute
+ // Usage:
+ Blade::directive('cspnonce', function () {
+ return 'getNonceAttribute(); ?>';
+ });
+
+ // @cspnoncevalue - Outputs just the nonce value (for use in nonce="...")
+ // Usage:
+ Blade::directive('cspnoncevalue', function () {
+ return 'getNonce(); ?>';
+ });
+ }
+
+ /**
+ * Register global helper functions.
+ */
+ protected function registerHelperFunctions(): void
+ {
+ // Register the csp_nonce() helper function
+ if (! function_exists('csp_nonce')) {
+ require __DIR__.'/helpers.php';
+ }
+ }
+
+ /**
+ * Register Livewire components.
+ */
+ protected function registerLivewireComponents(): void
+ {
+ // Only register if Livewire is available
+ if (class_exists(Livewire::class)) {
+ Livewire::component('header-configuration-manager', HeaderConfigurationManager::class);
+ }
}
}
diff --git a/packages/core-php/src/Core/Headers/CspNonceService.php b/packages/core-php/src/Core/Headers/CspNonceService.php
new file mode 100644
index 0000000..411465d
--- /dev/null
+++ b/packages/core-php/src/Core/Headers/CspNonceService.php
@@ -0,0 +1,161 @@
+
+ * // Your inline JavaScript
+ *
+ *
+ *
+ * ```
+ *
+ * Or using the directive:
+ * ```blade
+ *
+ * ```
+ *
+ * ## Security
+ *
+ * - Nonces are generated once per request and cached
+ * - Uses cryptographically secure random bytes
+ * - Base64-encoded for safe use in HTML attributes
+ * - Nonces are 128 bits (16 bytes) by default
+ */
+class CspNonceService
+{
+ /**
+ * The generated nonce for this request.
+ */
+ protected ?string $nonce = null;
+
+ /**
+ * Whether nonce-based CSP is enabled.
+ */
+ protected bool $enabled = true;
+
+ /**
+ * Nonce length in bytes (before base64 encoding).
+ */
+ protected int $nonceLength = 16;
+
+ public function __construct()
+ {
+ $this->enabled = (bool) config('headers.csp.nonce_enabled', true);
+ $this->nonceLength = (int) config('headers.csp.nonce_length', 16);
+ }
+
+ /**
+ * Get the CSP nonce for the current request.
+ *
+ * Generates a new nonce if one hasn't been created yet.
+ */
+ public function getNonce(): string
+ {
+ if ($this->nonce === null) {
+ $this->nonce = $this->generateNonce();
+ }
+
+ return $this->nonce;
+ }
+
+ /**
+ * Generate a cryptographically secure nonce.
+ */
+ protected function generateNonce(): string
+ {
+ return base64_encode(random_bytes($this->nonceLength));
+ }
+
+ /**
+ * Get the nonce formatted for a CSP directive.
+ *
+ * Returns the nonce in the format: 'nonce-{base64-value}'
+ */
+ public function getCspNonceDirective(): string
+ {
+ return "'nonce-{$this->getNonce()}'";
+ }
+
+ /**
+ * Get the nonce as an HTML attribute.
+ *
+ * Returns: nonce="{base64-value}"
+ */
+ public function getNonceAttribute(): string
+ {
+ return 'nonce="' . $this->getNonce() . '"';
+ }
+
+ /**
+ * Check if nonce-based CSP is enabled.
+ */
+ public function isEnabled(): bool
+ {
+ return $this->enabled;
+ }
+
+ /**
+ * Enable nonce-based CSP.
+ */
+ public function enable(): self
+ {
+ $this->enabled = true;
+
+ return $this;
+ }
+
+ /**
+ * Disable nonce-based CSP.
+ */
+ public function disable(): self
+ {
+ $this->enabled = false;
+
+ return $this;
+ }
+
+ /**
+ * Reset the nonce (for testing or special cases).
+ *
+ * This should rarely be needed in production.
+ */
+ public function reset(): self
+ {
+ $this->nonce = null;
+
+ return $this;
+ }
+
+ /**
+ * Set a specific nonce (for testing purposes only).
+ */
+ public function setNonce(string $nonce): self
+ {
+ $this->nonce = $nonce;
+
+ return $this;
+ }
+}
diff --git a/packages/core-php/src/Core/Headers/Livewire/HeaderConfigurationManager.php b/packages/core-php/src/Core/Headers/Livewire/HeaderConfigurationManager.php
new file mode 100644
index 0000000..d5379cb
--- /dev/null
+++ b/packages/core-php/src/Core/Headers/Livewire/HeaderConfigurationManager.php
@@ -0,0 +1,444 @@
+
+ * ```
+ *
+ * Or with initial settings:
+ * ```blade
+ *
+ * ```
+ */
+class HeaderConfigurationManager extends Component
+{
+ /**
+ * Whether security headers are globally enabled.
+ */
+ public bool $headersEnabled = true;
+
+ /**
+ * HSTS configuration.
+ */
+ public bool $hstsEnabled = true;
+ public int $hstsMaxAge = 31536000;
+ public bool $hstsIncludeSubdomains = true;
+ public bool $hstsPreload = true;
+
+ /**
+ * CSP configuration.
+ */
+ public bool $cspEnabled = true;
+ public bool $cspReportOnly = false;
+ public ?string $cspReportUri = null;
+ public bool $cspNonceEnabled = true;
+
+ /**
+ * CSP Directives.
+ */
+ public array $cspDirectives = [];
+
+ /**
+ * External service toggles.
+ */
+ public bool $jsdelivrEnabled = false;
+ public bool $unpkgEnabled = false;
+ public bool $googleAnalyticsEnabled = false;
+ public bool $facebookEnabled = false;
+
+ /**
+ * Permissions Policy features.
+ */
+ public array $permissionsFeatures = [];
+
+ /**
+ * Other security headers.
+ */
+ public string $xFrameOptions = 'SAMEORIGIN';
+ public string $referrerPolicy = 'strict-origin-when-cross-origin';
+
+ /**
+ * UI state.
+ */
+ public string $activeTab = 'csp';
+ public bool $showAdvanced = false;
+ public ?string $saveMessage = null;
+ public ?string $errorMessage = null;
+
+ /**
+ * Available CSP directive options.
+ */
+ protected array $availableDirectives = [
+ 'default-src',
+ 'script-src',
+ 'style-src',
+ 'img-src',
+ 'font-src',
+ 'connect-src',
+ 'frame-src',
+ 'frame-ancestors',
+ 'base-uri',
+ 'form-action',
+ 'object-src',
+ 'media-src',
+ 'worker-src',
+ 'manifest-src',
+ ];
+
+ /**
+ * Mount the component with current configuration.
+ */
+ public function mount(): void
+ {
+ $this->loadConfiguration();
+ }
+
+ /**
+ * Load configuration from current settings.
+ */
+ public function loadConfiguration(): void
+ {
+ // Global
+ $this->headersEnabled = (bool) config('headers.enabled', true);
+
+ // HSTS
+ $this->hstsEnabled = (bool) config('headers.hsts.enabled', true);
+ $this->hstsMaxAge = (int) config('headers.hsts.max_age', 31536000);
+ $this->hstsIncludeSubdomains = (bool) config('headers.hsts.include_subdomains', true);
+ $this->hstsPreload = (bool) config('headers.hsts.preload', true);
+
+ // CSP
+ $this->cspEnabled = (bool) config('headers.csp.enabled', true);
+ $this->cspReportOnly = (bool) config('headers.csp.report_only', false);
+ $this->cspReportUri = config('headers.csp.report_uri');
+ $this->cspNonceEnabled = (bool) config('headers.csp.nonce_enabled', true);
+
+ // CSP Directives
+ $this->cspDirectives = $this->formatDirectivesForUI(
+ config('headers.csp.directives', [])
+ );
+
+ // External services
+ $this->jsdelivrEnabled = (bool) config('headers.csp.external.jsdelivr.enabled', false);
+ $this->unpkgEnabled = (bool) config('headers.csp.external.unpkg.enabled', false);
+ $this->googleAnalyticsEnabled = (bool) config('headers.csp.external.google_analytics.enabled', false);
+ $this->facebookEnabled = (bool) config('headers.csp.external.facebook.enabled', false);
+
+ // Permissions Policy
+ $this->permissionsFeatures = $this->formatPermissionsForUI(
+ config('headers.permissions.features', [])
+ );
+
+ // Other headers
+ $this->xFrameOptions = config('headers.x_frame_options', 'SAMEORIGIN');
+ $this->referrerPolicy = config('headers.referrer_policy', 'strict-origin-when-cross-origin');
+ }
+
+ /**
+ * Format CSP directives for UI display.
+ *
+ * @param array> $directives
+ * @return array
+ */
+ protected function formatDirectivesForUI(array $directives): array
+ {
+ $formatted = [];
+ foreach ($directives as $directive => $sources) {
+ $formatted[$directive] = implode(' ', $sources);
+ }
+
+ return $formatted;
+ }
+
+ /**
+ * Format permissions policy for UI display.
+ *
+ * @param array> $features
+ * @return array
+ */
+ protected function formatPermissionsForUI(array $features): array
+ {
+ $formatted = [];
+ foreach ($features as $feature => $allowlist) {
+ $formatted[$feature] = [
+ 'enabled' => ! empty($allowlist),
+ 'allowlist' => implode(' ', $allowlist),
+ ];
+ }
+
+ return $formatted;
+ }
+
+ /**
+ * Update a CSP directive value.
+ */
+ public function updateDirective(string $directive, string $value): void
+ {
+ $this->cspDirectives[$directive] = $value;
+ }
+
+ /**
+ * Add a new CSP directive.
+ */
+ public function addDirective(string $directive): void
+ {
+ if (! isset($this->cspDirectives[$directive])) {
+ $this->cspDirectives[$directive] = "'self'";
+ }
+ }
+
+ /**
+ * Remove a CSP directive.
+ */
+ public function removeDirective(string $directive): void
+ {
+ unset($this->cspDirectives[$directive]);
+ }
+
+ /**
+ * Toggle a permissions policy feature.
+ */
+ public function togglePermission(string $feature): void
+ {
+ if (isset($this->permissionsFeatures[$feature])) {
+ $current = $this->permissionsFeatures[$feature]['enabled'] ?? false;
+ $this->permissionsFeatures[$feature]['enabled'] = ! $current;
+
+ if (! $current) {
+ // Enabling - default to 'self'
+ $this->permissionsFeatures[$feature]['allowlist'] = 'self';
+ } else {
+ // Disabling - clear allowlist
+ $this->permissionsFeatures[$feature]['allowlist'] = '';
+ }
+ }
+ }
+
+ /**
+ * Set the active configuration tab.
+ */
+ public function setTab(string $tab): void
+ {
+ $this->activeTab = $tab;
+ }
+
+ /**
+ * Toggle advanced options visibility.
+ */
+ public function toggleAdvanced(): void
+ {
+ $this->showAdvanced = ! $this->showAdvanced;
+ }
+
+ /**
+ * Generate environment file content for current configuration.
+ */
+ public function generateEnvConfig(): string
+ {
+ $lines = [
+ '# Security Headers Configuration',
+ '# Generated by HeaderConfigurationManager',
+ '',
+ 'SECURITY_HEADERS_ENABLED=' . ($this->headersEnabled ? 'true' : 'false'),
+ '',
+ '# HSTS',
+ 'SECURITY_HSTS_ENABLED=' . ($this->hstsEnabled ? 'true' : 'false'),
+ 'SECURITY_HSTS_MAX_AGE=' . $this->hstsMaxAge,
+ 'SECURITY_HSTS_INCLUDE_SUBDOMAINS=' . ($this->hstsIncludeSubdomains ? 'true' : 'false'),
+ 'SECURITY_HSTS_PRELOAD=' . ($this->hstsPreload ? 'true' : 'false'),
+ '',
+ '# CSP',
+ 'SECURITY_CSP_ENABLED=' . ($this->cspEnabled ? 'true' : 'false'),
+ 'SECURITY_CSP_REPORT_ONLY=' . ($this->cspReportOnly ? 'true' : 'false'),
+ 'SECURITY_CSP_NONCE_ENABLED=' . ($this->cspNonceEnabled ? 'true' : 'false'),
+ ];
+
+ if ($this->cspReportUri) {
+ $lines[] = 'SECURITY_CSP_REPORT_URI=' . $this->cspReportUri;
+ }
+
+ $lines = array_merge($lines, [
+ '',
+ '# External Services',
+ 'SECURITY_CSP_JSDELIVR=' . ($this->jsdelivrEnabled ? 'true' : 'false'),
+ 'SECURITY_CSP_UNPKG=' . ($this->unpkgEnabled ? 'true' : 'false'),
+ 'SECURITY_CSP_GOOGLE_ANALYTICS=' . ($this->googleAnalyticsEnabled ? 'true' : 'false'),
+ 'SECURITY_CSP_FACEBOOK=' . ($this->facebookEnabled ? 'true' : 'false'),
+ '',
+ '# Other Headers',
+ 'SECURITY_X_FRAME_OPTIONS=' . $this->xFrameOptions,
+ 'SECURITY_REFERRER_POLICY=' . $this->referrerPolicy,
+ ]);
+
+ return implode("\n", $lines);
+ }
+
+ /**
+ * Get CSP directives as array for config.
+ *
+ * @return array>
+ */
+ protected function getDirectivesAsArray(): array
+ {
+ $directives = [];
+ foreach ($this->cspDirectives as $directive => $value) {
+ $sources = array_filter(array_map('trim', explode(' ', $value)));
+ if (! empty($sources)) {
+ $directives[$directive] = $sources;
+ }
+ }
+
+ return $directives;
+ }
+
+ /**
+ * Get permissions policy as array for config.
+ *
+ * @return array>
+ */
+ protected function getPermissionsAsArray(): array
+ {
+ $features = [];
+ foreach ($this->permissionsFeatures as $feature => $config) {
+ if ($config['enabled'] ?? false) {
+ $allowlist = array_filter(array_map('trim', explode(' ', $config['allowlist'] ?? '')));
+ $features[$feature] = $allowlist;
+ } else {
+ $features[$feature] = [];
+ }
+ }
+
+ return $features;
+ }
+
+ /**
+ * Get available CSP directives that can be added.
+ *
+ * @return array
+ */
+ public function getAvailableDirectives(): array
+ {
+ return array_diff($this->availableDirectives, array_keys($this->cspDirectives));
+ }
+
+ /**
+ * Get all permission feature names.
+ *
+ * @return array
+ */
+ public function getPermissionFeatures(): array
+ {
+ return array_keys($this->permissionsFeatures);
+ }
+
+ /**
+ * Preview the CSP header that would be generated.
+ */
+ public function previewCspHeader(): string
+ {
+ $directives = $this->getDirectivesAsArray();
+ $parts = [];
+
+ foreach ($directives as $directive => $sources) {
+ $parts[] = $directive . ' ' . implode(' ', $sources);
+ }
+
+ return implode('; ', $parts);
+ }
+
+ /**
+ * Reset to default configuration.
+ */
+ public function resetToDefaults(): void
+ {
+ // Clear runtime config and reload defaults
+ Config::set('headers', null);
+ $this->loadConfiguration();
+
+ $this->saveMessage = 'Configuration reset to defaults.';
+ $this->dispatch('configuration-reset');
+ }
+
+ /**
+ * Save configuration (dispatches event for parent to handle).
+ */
+ public function saveConfiguration(): void
+ {
+ $config = [
+ 'enabled' => $this->headersEnabled,
+ 'hsts' => [
+ 'enabled' => $this->hstsEnabled,
+ 'max_age' => $this->hstsMaxAge,
+ 'include_subdomains' => $this->hstsIncludeSubdomains,
+ 'preload' => $this->hstsPreload,
+ ],
+ 'csp' => [
+ 'enabled' => $this->cspEnabled,
+ 'report_only' => $this->cspReportOnly,
+ 'report_uri' => $this->cspReportUri,
+ 'nonce_enabled' => $this->cspNonceEnabled,
+ 'directives' => $this->getDirectivesAsArray(),
+ 'external' => [
+ 'jsdelivr' => ['enabled' => $this->jsdelivrEnabled],
+ 'unpkg' => ['enabled' => $this->unpkgEnabled],
+ 'google_analytics' => ['enabled' => $this->googleAnalyticsEnabled],
+ 'facebook' => ['enabled' => $this->facebookEnabled],
+ ],
+ ],
+ 'permissions' => [
+ 'features' => $this->getPermissionsAsArray(),
+ ],
+ 'x_frame_options' => $this->xFrameOptions,
+ 'referrer_policy' => $this->referrerPolicy,
+ ];
+
+ // Dispatch event for parent component or controller to handle persistence
+ $this->dispatch('header-configuration-saved', config: $config);
+
+ $this->saveMessage = 'Configuration saved successfully.';
+ }
+
+ /**
+ * Clear notification messages.
+ */
+ public function clearMessages(): void
+ {
+ $this->saveMessage = null;
+ $this->errorMessage = null;
+ }
+
+ /**
+ * Render the component.
+ */
+ public function render()
+ {
+ return view('core::headers.livewire.header-configuration-manager');
+ }
+}
diff --git a/packages/core-php/src/Core/Headers/SecurityHeaders.php b/packages/core-php/src/Core/Headers/SecurityHeaders.php
index 964fbcb..7cb8851 100644
--- a/packages/core-php/src/Core/Headers/SecurityHeaders.php
+++ b/packages/core-php/src/Core/Headers/SecurityHeaders.php
@@ -12,6 +12,7 @@ namespace Core\Headers;
use Closure;
use Illuminate\Http\Request;
+use Illuminate\Support\Facades\App;
use Symfony\Component\HttpFoundation\Response;
/**
@@ -25,9 +26,61 @@ use Symfony\Component\HttpFoundation\Response;
* - X-XSS-Protection - enable browser XSS filtering
* - Referrer-Policy - control referrer information
* - Permissions-Policy - control browser features
+ *
+ * Supports nonce-based CSP for inline scripts and styles via CspNonceService.
+ *
+ * ## Usage
+ *
+ * Register in your HTTP kernel or route middleware:
+ *
+ * ```php
+ * // app/Http/Kernel.php
+ * protected $middleware = [
+ * // ...
+ * \Core\Headers\SecurityHeaders::class,
+ * ];
+ * ```
+ *
+ * ## CSP Directive Resolution
+ *
+ * CSP directives are built in this order:
+ * 1. Base directives from `config('headers.csp.directives')`
+ * 2. Environment-specific overrides from `config('headers.csp.environment')`
+ * 3. Nonces added to script-src/style-src (if enabled)
+ * 4. CDN sources from `config('core.cdn.subdomain')`
+ * 5. External service sources (jsDelivr, Google Analytics, etc.)
+ * 6. Development WebSocket sources (localhost:8080)
+ * 7. Report URI (if configured)
+ *
+ * ## Report-Only Mode
+ *
+ * Enable `SECURITY_CSP_REPORT_ONLY=true` to log violations without blocking.
+ * This uses the `Content-Security-Policy-Report-Only` header instead.
+ *
+ * ## HSTS Behaviour
+ *
+ * HSTS is only added in production environments to avoid issues with
+ * local development over HTTP. Configure via:
+ *
+ * - `SECURITY_HSTS_ENABLED` - Enable/disable HSTS
+ * - `SECURITY_HSTS_MAX_AGE` - Max age in seconds (default: 1 year)
+ * - `SECURITY_HSTS_INCLUDE_SUBDOMAINS` - Include subdomains
+ * - `SECURITY_HSTS_PRELOAD` - Enable preload flag for browser preload lists
+ *
+ * @see CspNonceService For nonce generation
+ * @see Boot For configuration documentation
*/
class SecurityHeaders
{
+ /**
+ * The CSP nonce service.
+ */
+ protected ?CspNonceService $nonceService = null;
+ public function __construct(?CspNonceService $nonceService = null)
+ {
+ $this->nonceService = $nonceService ?? App::make(CspNonceService::class);
+ }
+
/**
* Handle an incoming request.
*/
@@ -47,6 +100,14 @@ class SecurityHeaders
return $response;
}
+ /**
+ * Get the CSP nonce service.
+ */
+ public function getNonceService(): CspNonceService
+ {
+ return $this->nonceService;
+ }
+
/**
* Add Strict-Transport-Security header.
*/
@@ -110,6 +171,9 @@ class SecurityHeaders
// Apply environment-specific overrides
$directives = $this->applyEnvironmentOverrides($directives, $config);
+ // Add nonces for script-src and style-src if enabled
+ $directives = $this->addNonceDirectives($directives, $config);
+
// Add CDN subdomain sources
$directives = $this->addCdnSources($directives, $config);
@@ -127,6 +191,51 @@ class SecurityHeaders
return $directives;
}
+ /**
+ * Add nonce directives for script-src and style-src.
+ *
+ * When nonce-based CSP is enabled, nonces are added to script-src and
+ * style-src directives, allowing inline scripts/styles that include
+ * the matching nonce attribute.
+ *
+ * @return array>
+ */
+ protected function addNonceDirectives(array $directives, array $config): array
+ {
+ $nonceEnabled = $config['nonce_enabled'] ?? true;
+
+ // Skip if nonces are disabled
+ if (! $nonceEnabled || ! $this->nonceService?->isEnabled()) {
+ return $directives;
+ }
+
+ // Don't add nonces in local/development environments with unsafe-inline
+ // as it would be redundant and could cause issues
+ $environment = app()->environment();
+ $skipNonceEnvs = $config['nonce_skip_environments'] ?? ['local', 'development'];
+
+ if (in_array($environment, $skipNonceEnvs, true)) {
+ return $directives;
+ }
+
+ $nonce = $this->nonceService->getCspNonceDirective();
+ $nonceDirectives = $config['nonce_directives'] ?? ['script-src', 'style-src'];
+
+ foreach ($nonceDirectives as $directive) {
+ if (isset($directives[$directive])) {
+ // Remove unsafe-inline if present and add nonce
+ // Nonces are more secure than unsafe-inline
+ $directives[$directive] = array_filter(
+ $directives[$directive],
+ fn ($value) => $value !== "'unsafe-inline'"
+ );
+ $directives[$directive][] = $nonce;
+ }
+ }
+
+ return $directives;
+ }
+
/**
* Get default CSP directives.
*
diff --git a/packages/core-php/src/Core/Headers/Testing/HeaderAssertions.php b/packages/core-php/src/Core/Headers/Testing/HeaderAssertions.php
new file mode 100644
index 0000000..31adce5
--- /dev/null
+++ b/packages/core-php/src/Core/Headers/Testing/HeaderAssertions.php
@@ -0,0 +1,476 @@
+get('/');
+ *
+ * $this->assertHasSecurityHeaders($response);
+ * $this->assertHasHstsHeader($response);
+ * $this->assertHasCspHeader($response);
+ * }
+ * }
+ * ```
+ *
+ * ## Available Assertions
+ *
+ * | Method | Description |
+ * |--------|-------------|
+ * | `assertHasSecurityHeaders()` | Assert all standard security headers present |
+ * | `assertHasHstsHeader()` | Assert HSTS header present with valid config |
+ * | `assertHasCspHeader()` | Assert CSP header present |
+ * | `assertCspContainsDirective()` | Assert CSP contains a specific directive |
+ * | `assertCspContainsSource()` | Assert CSP directive contains a source |
+ * | `assertCspDoesNotContainSource()` | Assert CSP directive does not contain source |
+ * | `assertHasPermissionsPolicy()` | Assert Permissions-Policy header present |
+ * | `assertPermissionsPolicyFeature()` | Assert specific feature in Permissions-Policy |
+ * | `assertHasXFrameOptions()` | Assert X-Frame-Options header present |
+ * | `assertHasXContentTypeOptions()` | Assert X-Content-Type-Options header present |
+ * | `assertHasReferrerPolicy()` | Assert Referrer-Policy header present |
+ * | `assertHasCspNonce()` | Assert CSP contains a nonce directive |
+ * | `assertNoCspUnsafeInline()` | Assert CSP does not use unsafe-inline |
+ */
+trait HeaderAssertions
+{
+ /**
+ * Assert that all standard security headers are present.
+ *
+ * Checks for:
+ * - X-Content-Type-Options: nosniff
+ * - X-Frame-Options
+ * - X-XSS-Protection
+ * - Referrer-Policy
+ *
+ * @param TestResponse $response The HTTP response to check
+ * @return $this
+ */
+ public function assertHasSecurityHeaders(TestResponse $response): self
+ {
+ $response->assertHeader('X-Content-Type-Options');
+ $response->assertHeader('X-Frame-Options');
+ $response->assertHeader('X-XSS-Protection');
+ $response->assertHeader('Referrer-Policy');
+
+ return $this;
+ }
+
+ /**
+ * Assert that HSTS header is present and properly configured.
+ *
+ * @param TestResponse $response The HTTP response to check
+ * @param int|null $minMaxAge Minimum max-age value (optional)
+ * @param bool|null $includeSubdomains Whether includeSubDomains should be present (optional)
+ * @param bool|null $preload Whether preload should be present (optional)
+ * @return $this
+ */
+ public function assertHasHstsHeader(
+ TestResponse $response,
+ ?int $minMaxAge = null,
+ ?bool $includeSubdomains = null,
+ ?bool $preload = null
+ ): self {
+ $response->assertHeader('Strict-Transport-Security');
+
+ $hsts = $response->headers->get('Strict-Transport-Security');
+ Assert::assertNotNull($hsts, 'HSTS header should not be null');
+
+ // Check max-age
+ if ($minMaxAge !== null) {
+ preg_match('/max-age=(\d+)/', $hsts, $matches);
+ Assert::assertNotEmpty($matches, 'HSTS should contain max-age directive');
+ Assert::assertGreaterThanOrEqual($minMaxAge, (int) $matches[1], "HSTS max-age should be at least {$minMaxAge}");
+ }
+
+ // Check includeSubDomains
+ if ($includeSubdomains === true) {
+ Assert::assertStringContainsString('includeSubDomains', $hsts, 'HSTS should include subdomains');
+ } elseif ($includeSubdomains === false) {
+ Assert::assertStringNotContainsString('includeSubDomains', $hsts, 'HSTS should not include subdomains');
+ }
+
+ // Check preload
+ if ($preload === true) {
+ Assert::assertStringContainsString('preload', $hsts, 'HSTS should have preload flag');
+ } elseif ($preload === false) {
+ Assert::assertStringNotContainsString('preload', $hsts, 'HSTS should not have preload flag');
+ }
+
+ return $this;
+ }
+
+ /**
+ * Assert that CSP header is present.
+ *
+ * @param TestResponse $response The HTTP response to check
+ * @param bool $reportOnly Whether to check for report-only header
+ * @return $this
+ */
+ public function assertHasCspHeader(TestResponse $response, bool $reportOnly = false): self
+ {
+ $headerName = $reportOnly
+ ? 'Content-Security-Policy-Report-Only'
+ : 'Content-Security-Policy';
+
+ $response->assertHeader($headerName);
+
+ return $this;
+ }
+
+ /**
+ * Assert that CSP contains a specific directive.
+ *
+ * @param TestResponse $response The HTTP response to check
+ * @param string $directive The CSP directive to check (e.g., 'default-src', 'script-src')
+ * @param bool $reportOnly Whether to check report-only header
+ * @return $this
+ */
+ public function assertCspContainsDirective(
+ TestResponse $response,
+ string $directive,
+ bool $reportOnly = false
+ ): self {
+ $csp = $this->getCspHeader($response, $reportOnly);
+ Assert::assertStringContainsString($directive, $csp, "CSP should contain '{$directive}' directive");
+
+ return $this;
+ }
+
+ /**
+ * Assert that a CSP directive contains a specific source.
+ *
+ * @param TestResponse $response The HTTP response to check
+ * @param string $directive The CSP directive (e.g., 'script-src')
+ * @param string $source The source to check for (e.g., "'self'", 'https://example.com')
+ * @param bool $reportOnly Whether to check report-only header
+ * @return $this
+ */
+ public function assertCspContainsSource(
+ TestResponse $response,
+ string $directive,
+ string $source,
+ bool $reportOnly = false
+ ): self {
+ $directives = $this->parseCspDirectives($response, $reportOnly);
+
+ Assert::assertArrayHasKey($directive, $directives, "CSP should contain '{$directive}' directive");
+ Assert::assertContains(
+ $source,
+ $directives[$directive],
+ "CSP directive '{$directive}' should contain source '{$source}'"
+ );
+
+ return $this;
+ }
+
+ /**
+ * Assert that a CSP directive does not contain a specific source.
+ *
+ * @param TestResponse $response The HTTP response to check
+ * @param string $directive The CSP directive (e.g., 'script-src')
+ * @param string $source The source that should not be present
+ * @param bool $reportOnly Whether to check report-only header
+ * @return $this
+ */
+ public function assertCspDoesNotContainSource(
+ TestResponse $response,
+ string $directive,
+ string $source,
+ bool $reportOnly = false
+ ): self {
+ $directives = $this->parseCspDirectives($response, $reportOnly);
+
+ if (isset($directives[$directive])) {
+ Assert::assertNotContains(
+ $source,
+ $directives[$directive],
+ "CSP directive '{$directive}' should not contain source '{$source}'"
+ );
+ }
+
+ return $this;
+ }
+
+ /**
+ * Assert that CSP contains a nonce directive.
+ *
+ * @param TestResponse $response The HTTP response to check
+ * @param string $directive The directive to check for nonce (default: 'script-src')
+ * @param bool $reportOnly Whether to check report-only header
+ * @return $this
+ */
+ public function assertHasCspNonce(
+ TestResponse $response,
+ string $directive = 'script-src',
+ bool $reportOnly = false
+ ): self {
+ $csp = $this->getCspHeader($response, $reportOnly);
+ $pattern = "/{$directive}[^;]*'nonce-[A-Za-z0-9+\/=]+'/";
+
+ Assert::assertMatchesRegularExpression(
+ $pattern,
+ $csp,
+ "CSP '{$directive}' should contain a nonce directive"
+ );
+
+ return $this;
+ }
+
+ /**
+ * Assert that CSP does not use 'unsafe-inline' in specified directive.
+ *
+ * @param TestResponse $response The HTTP response to check
+ * @param string $directive The directive to check (default: 'script-src')
+ * @param bool $reportOnly Whether to check report-only header
+ * @return $this
+ */
+ public function assertNoCspUnsafeInline(
+ TestResponse $response,
+ string $directive = 'script-src',
+ bool $reportOnly = false
+ ): self {
+ return $this->assertCspDoesNotContainSource($response, $directive, "'unsafe-inline'", $reportOnly);
+ }
+
+ /**
+ * Assert that CSP does not use 'unsafe-eval' in specified directive.
+ *
+ * @param TestResponse $response The HTTP response to check
+ * @param string $directive The directive to check (default: 'script-src')
+ * @param bool $reportOnly Whether to check report-only header
+ * @return $this
+ */
+ public function assertNoCspUnsafeEval(
+ TestResponse $response,
+ string $directive = 'script-src',
+ bool $reportOnly = false
+ ): self {
+ return $this->assertCspDoesNotContainSource($response, $directive, "'unsafe-eval'", $reportOnly);
+ }
+
+ /**
+ * Assert that Permissions-Policy header is present.
+ *
+ * @param TestResponse $response The HTTP response to check
+ * @return $this
+ */
+ public function assertHasPermissionsPolicy(TestResponse $response): self
+ {
+ $response->assertHeader('Permissions-Policy');
+
+ return $this;
+ }
+
+ /**
+ * Assert that Permissions-Policy contains a specific feature setting.
+ *
+ * @param TestResponse $response The HTTP response to check
+ * @param string $feature The feature name (e.g., 'geolocation', 'camera')
+ * @param array $allowList Expected allow list (empty array for '()')
+ * @return $this
+ */
+ public function assertPermissionsPolicyFeature(
+ TestResponse $response,
+ string $feature,
+ array $allowList = []
+ ): self {
+ $policy = $response->headers->get('Permissions-Policy');
+ Assert::assertNotNull($policy, 'Permissions-Policy header should be present');
+
+ if (empty($allowList)) {
+ // Feature should be disabled: feature=()
+ Assert::assertMatchesRegularExpression(
+ "/{$feature}=\(\)/",
+ $policy,
+ "Permissions-Policy should disable '{$feature}'"
+ );
+ } else {
+ // Feature should have specific origins
+ Assert::assertStringContainsString(
+ "{$feature}=",
+ $policy,
+ "Permissions-Policy should contain '{$feature}' feature"
+ );
+ }
+
+ return $this;
+ }
+
+ /**
+ * Assert that X-Frame-Options header is present with expected value.
+ *
+ * @param TestResponse $response The HTTP response to check
+ * @param string|null $expected Expected value ('DENY', 'SAMEORIGIN', etc.)
+ * @return $this
+ */
+ public function assertHasXFrameOptions(TestResponse $response, ?string $expected = null): self
+ {
+ $response->assertHeader('X-Frame-Options');
+
+ if ($expected !== null) {
+ $actual = $response->headers->get('X-Frame-Options');
+ Assert::assertSame($expected, $actual, "X-Frame-Options should be '{$expected}'");
+ }
+
+ return $this;
+ }
+
+ /**
+ * Assert that X-Content-Type-Options header is present with 'nosniff'.
+ *
+ * @param TestResponse $response The HTTP response to check
+ * @return $this
+ */
+ public function assertHasXContentTypeOptions(TestResponse $response): self
+ {
+ $response->assertHeader('X-Content-Type-Options', 'nosniff');
+
+ return $this;
+ }
+
+ /**
+ * Assert that Referrer-Policy header is present with expected value.
+ *
+ * @param TestResponse $response The HTTP response to check
+ * @param string|null $expected Expected value (e.g., 'strict-origin-when-cross-origin')
+ * @return $this
+ */
+ public function assertHasReferrerPolicy(TestResponse $response, ?string $expected = null): self
+ {
+ $response->assertHeader('Referrer-Policy');
+
+ if ($expected !== null) {
+ $actual = $response->headers->get('Referrer-Policy');
+ Assert::assertSame($expected, $actual, "Referrer-Policy should be '{$expected}'");
+ }
+
+ return $this;
+ }
+
+ /**
+ * Assert that X-XSS-Protection header is present.
+ *
+ * @param TestResponse $response The HTTP response to check
+ * @param string|null $expected Expected value (e.g., '1; mode=block')
+ * @return $this
+ */
+ public function assertHasXssProtection(TestResponse $response, ?string $expected = null): self
+ {
+ $response->assertHeader('X-XSS-Protection');
+
+ if ($expected !== null) {
+ $actual = $response->headers->get('X-XSS-Protection');
+ Assert::assertSame($expected, $actual, "X-XSS-Protection should be '{$expected}'");
+ }
+
+ return $this;
+ }
+
+ /**
+ * Assert that a header is NOT present.
+ *
+ * @param TestResponse $response The HTTP response to check
+ * @param string $headerName The header name to check
+ * @return $this
+ */
+ public function assertHeaderMissing(TestResponse $response, string $headerName): self
+ {
+ $response->assertHeaderMissing($headerName);
+
+ return $this;
+ }
+
+ /**
+ * Get the CSP header value from response.
+ *
+ * @param TestResponse $response The HTTP response
+ * @param bool $reportOnly Whether to get report-only header
+ * @return string The CSP header value
+ */
+ protected function getCspHeader(TestResponse $response, bool $reportOnly = false): string
+ {
+ $headerName = $reportOnly
+ ? 'Content-Security-Policy-Report-Only'
+ : 'Content-Security-Policy';
+
+ $csp = $response->headers->get($headerName);
+ Assert::assertNotNull($csp, "{$headerName} header should be present");
+
+ return $csp;
+ }
+
+ /**
+ * Parse CSP header into directives array.
+ *
+ * @param TestResponse $response The HTTP response
+ * @param bool $reportOnly Whether to parse report-only header
+ * @return array> Map of directive to sources
+ */
+ protected function parseCspDirectives(TestResponse $response, bool $reportOnly = false): array
+ {
+ $csp = $this->getCspHeader($response, $reportOnly);
+ $directives = [];
+
+ foreach (explode(';', $csp) as $part) {
+ $part = trim($part);
+ if (empty($part)) {
+ continue;
+ }
+
+ $tokens = preg_split('/\s+/', $part);
+ $directiveName = array_shift($tokens);
+ $directives[$directiveName] = $tokens;
+ }
+
+ return $directives;
+ }
+
+ /**
+ * Extract the nonce value from a CSP header.
+ *
+ * @param TestResponse $response The HTTP response
+ * @param string $directive The directive to extract nonce from
+ * @param bool $reportOnly Whether to check report-only header
+ * @return string|null The nonce value or null if not found
+ */
+ public function extractCspNonce(
+ TestResponse $response,
+ string $directive = 'script-src',
+ bool $reportOnly = false
+ ): ?string {
+ $csp = $this->getCspHeader($response, $reportOnly);
+
+ // Match nonce in the specified directive
+ if (preg_match("/{$directive}[^;]*'nonce-([A-Za-z0-9+\/=]+)'/", $csp, $matches)) {
+ return $matches[1];
+ }
+
+ return null;
+ }
+}
diff --git a/packages/core-php/src/Core/Headers/Testing/SecurityHeaderTester.php b/packages/core-php/src/Core/Headers/Testing/SecurityHeaderTester.php
new file mode 100644
index 0000000..de10f42
--- /dev/null
+++ b/packages/core-php/src/Core/Headers/Testing/SecurityHeaderTester.php
@@ -0,0 +1,583 @@
+assertEmpty($issues, 'Security headers should be valid');
+ *
+ * // Generate a security report
+ * $report = SecurityHeaderTester::report($response);
+ *
+ * // Check specific headers
+ * $this->assertTrue(SecurityHeaderTester::hasValidHsts($response));
+ * $this->assertTrue(SecurityHeaderTester::hasValidCsp($response));
+ * ```
+ *
+ * ## Validation Rules
+ *
+ * The validator checks against security best practices:
+ * - HSTS: max-age >= 31536000 (1 year), includeSubDomains, preload
+ * - CSP: No 'unsafe-inline' or 'unsafe-eval' in script-src/style-src
+ * - X-Frame-Options: Should be DENY or SAMEORIGIN
+ * - X-Content-Type-Options: Should be nosniff
+ * - Referrer-Policy: Should be strict-origin-when-cross-origin or stricter
+ */
+class SecurityHeaderTester
+{
+ /**
+ * Recommended minimum HSTS max-age (1 year).
+ */
+ public const RECOMMENDED_HSTS_MAX_AGE = 31536000;
+
+ /**
+ * Valid X-Frame-Options values.
+ *
+ * @var array
+ */
+ public const VALID_X_FRAME_OPTIONS = ['DENY', 'SAMEORIGIN'];
+
+ /**
+ * Strict referrer policies (recommended).
+ *
+ * @var array
+ */
+ public const STRICT_REFERRER_POLICIES = [
+ 'no-referrer',
+ 'no-referrer-when-downgrade',
+ 'same-origin',
+ 'strict-origin',
+ 'strict-origin-when-cross-origin',
+ ];
+
+ /**
+ * Validate all security headers and return any issues found.
+ *
+ * @param TestResponse|Response $response The HTTP response to validate
+ * @param array $options Validation options
+ * @return array Map of header name to issue description
+ */
+ public static function validate(TestResponse|Response $response, array $options = []): array
+ {
+ $issues = [];
+ $headers = self::getHeaders($response);
+
+ // Check required headers
+ $requiredHeaders = $options['required'] ?? [
+ 'X-Content-Type-Options',
+ 'X-Frame-Options',
+ 'Referrer-Policy',
+ ];
+
+ foreach ($requiredHeaders as $header) {
+ if (! isset($headers[strtolower($header)])) {
+ $issues[$header] = 'Header is missing';
+ }
+ }
+
+ // Validate specific headers
+ if ($issue = self::validateXContentTypeOptions($headers)) {
+ $issues['X-Content-Type-Options'] = $issue;
+ }
+
+ if ($issue = self::validateXFrameOptions($headers)) {
+ $issues['X-Frame-Options'] = $issue;
+ }
+
+ if ($issue = self::validateReferrerPolicy($headers)) {
+ $issues['Referrer-Policy'] = $issue;
+ }
+
+ if (($options['check_hsts'] ?? true) && isset($headers['strict-transport-security'])) {
+ if ($issue = self::validateHsts($headers)) {
+ $issues['Strict-Transport-Security'] = $issue;
+ }
+ }
+
+ if (($options['check_csp'] ?? true) && (isset($headers['content-security-policy']) || isset($headers['content-security-policy-report-only']))) {
+ $cspIssues = self::validateCsp($headers, $options);
+ foreach ($cspIssues as $directive => $issue) {
+ $issues["CSP:{$directive}"] = $issue;
+ }
+ }
+
+ if (($options['check_permissions'] ?? true) && isset($headers['permissions-policy'])) {
+ if ($issue = self::validatePermissionsPolicy($headers)) {
+ $issues['Permissions-Policy'] = $issue;
+ }
+ }
+
+ return $issues;
+ }
+
+ /**
+ * Generate a comprehensive security header report.
+ *
+ * @param TestResponse|Response $response The HTTP response to analyze
+ * @return array Detailed report of security header status
+ */
+ public static function report(TestResponse|Response $response): array
+ {
+ $headers = self::getHeaders($response);
+
+ return [
+ 'hsts' => self::analyzeHsts($headers),
+ 'csp' => self::analyzeCsp($headers),
+ 'permissions_policy' => self::analyzePermissionsPolicy($headers),
+ 'x_frame_options' => self::analyzeXFrameOptions($headers),
+ 'x_content_type_options' => self::analyzeXContentTypeOptions($headers),
+ 'referrer_policy' => self::analyzeReferrerPolicy($headers),
+ 'x_xss_protection' => self::analyzeXssProtection($headers),
+ 'issues' => self::validate($response),
+ 'score' => self::calculateScore($response),
+ ];
+ }
+
+ /**
+ * Calculate a security score (0-100) based on headers.
+ *
+ * @param TestResponse|Response $response The HTTP response to score
+ * @return int Security score from 0 (no security) to 100 (excellent)
+ */
+ public static function calculateScore(TestResponse|Response $response): int
+ {
+ $headers = self::getHeaders($response);
+ $score = 0;
+
+ // HSTS (20 points)
+ if (isset($headers['strict-transport-security'])) {
+ $score += 10;
+ $hsts = $headers['strict-transport-security'];
+ if (str_contains($hsts, 'includeSubDomains')) {
+ $score += 5;
+ }
+ if (str_contains($hsts, 'preload')) {
+ $score += 5;
+ }
+ }
+
+ // CSP (30 points)
+ $cspHeader = $headers['content-security-policy'] ?? $headers['content-security-policy-report-only'] ?? null;
+ if ($cspHeader) {
+ $score += 15;
+ if (! str_contains($cspHeader, "'unsafe-inline'")) {
+ $score += 10;
+ }
+ if (! str_contains($cspHeader, "'unsafe-eval'")) {
+ $score += 5;
+ }
+ }
+
+ // Permissions-Policy (10 points)
+ if (isset($headers['permissions-policy'])) {
+ $score += 10;
+ }
+
+ // X-Frame-Options (15 points)
+ if (isset($headers['x-frame-options'])) {
+ $score += 10;
+ if (in_array(strtoupper($headers['x-frame-options']), self::VALID_X_FRAME_OPTIONS, true)) {
+ $score += 5;
+ }
+ }
+
+ // X-Content-Type-Options (10 points)
+ if (isset($headers['x-content-type-options']) && strtolower($headers['x-content-type-options']) === 'nosniff') {
+ $score += 10;
+ }
+
+ // Referrer-Policy (10 points)
+ if (isset($headers['referrer-policy'])) {
+ $score += 5;
+ if (in_array(strtolower($headers['referrer-policy']), self::STRICT_REFERRER_POLICIES, true)) {
+ $score += 5;
+ }
+ }
+
+ // X-XSS-Protection (5 points - legacy but still good to have)
+ if (isset($headers['x-xss-protection'])) {
+ $score += 5;
+ }
+
+ return min(100, $score);
+ }
+
+ /**
+ * Check if HSTS header is valid.
+ *
+ * @param TestResponse|Response $response The HTTP response to check
+ * @return bool True if HSTS is properly configured
+ */
+ public static function hasValidHsts(TestResponse|Response $response): bool
+ {
+ $headers = self::getHeaders($response);
+
+ return self::validateHsts($headers) === null;
+ }
+
+ /**
+ * Check if CSP header is valid (no unsafe directives).
+ *
+ * @param TestResponse|Response $response The HTTP response to check
+ * @param array $options Validation options
+ * @return bool True if CSP is properly configured
+ */
+ public static function hasValidCsp(TestResponse|Response $response, array $options = []): bool
+ {
+ $headers = self::getHeaders($response);
+ $issues = self::validateCsp($headers, $options);
+
+ return empty($issues);
+ }
+
+ /**
+ * Parse CSP header into directives.
+ *
+ * @param TestResponse|Response $response The HTTP response
+ * @return array> Map of directive to sources
+ */
+ public static function parseCsp(TestResponse|Response $response): array
+ {
+ $headers = self::getHeaders($response);
+ $csp = $headers['content-security-policy'] ?? $headers['content-security-policy-report-only'] ?? '';
+
+ return self::parseCspString($csp);
+ }
+
+ // ─────────────────────────────────────────────────────────────────────────────
+ // Internal validation methods
+ // ─────────────────────────────────────────────────────────────────────────────
+
+ /**
+ * Get headers from response as lowercase key array.
+ *
+ * @return array
+ */
+ protected static function getHeaders(TestResponse|Response $response): array
+ {
+ $headers = [];
+
+ if ($response instanceof TestResponse) {
+ $headerBag = $response->headers;
+ } else {
+ $headerBag = $response->headers;
+ }
+
+ foreach ($headerBag->all() as $name => $values) {
+ $headers[strtolower($name)] = is_array($values) ? ($values[0] ?? '') : $values;
+ }
+
+ return $headers;
+ }
+
+ /**
+ * Validate X-Content-Type-Options header.
+ */
+ protected static function validateXContentTypeOptions(array $headers): ?string
+ {
+ $value = $headers['x-content-type-options'] ?? null;
+
+ if ($value === null) {
+ return null; // Handled by required check
+ }
+
+ if (strtolower($value) !== 'nosniff') {
+ return "Should be 'nosniff', got '{$value}'";
+ }
+
+ return null;
+ }
+
+ /**
+ * Validate X-Frame-Options header.
+ */
+ protected static function validateXFrameOptions(array $headers): ?string
+ {
+ $value = $headers['x-frame-options'] ?? null;
+
+ if ($value === null) {
+ return null; // Handled by required check
+ }
+
+ if (! in_array(strtoupper($value), self::VALID_X_FRAME_OPTIONS, true)) {
+ return "Should be DENY or SAMEORIGIN, got '{$value}'";
+ }
+
+ return null;
+ }
+
+ /**
+ * Validate Referrer-Policy header.
+ */
+ protected static function validateReferrerPolicy(array $headers): ?string
+ {
+ $value = $headers['referrer-policy'] ?? null;
+
+ if ($value === null) {
+ return null; // Handled by required check
+ }
+
+ if (strtolower($value) === 'unsafe-url') {
+ return "'unsafe-url' exposes full URL to third parties";
+ }
+
+ return null;
+ }
+
+ /**
+ * Validate Strict-Transport-Security header.
+ */
+ protected static function validateHsts(array $headers): ?string
+ {
+ $value = $headers['strict-transport-security'] ?? null;
+
+ if ($value === null) {
+ return 'HSTS header is missing';
+ }
+
+ if (! preg_match('/max-age=(\d+)/', $value, $matches)) {
+ return 'HSTS should contain max-age directive';
+ }
+
+ $maxAge = (int) $matches[1];
+ if ($maxAge < self::RECOMMENDED_HSTS_MAX_AGE) {
+ return "max-age should be at least " . self::RECOMMENDED_HSTS_MAX_AGE . " (1 year), got {$maxAge}";
+ }
+
+ return null;
+ }
+
+ /**
+ * Validate Content-Security-Policy header.
+ *
+ * @return array Map of directive to issue
+ */
+ protected static function validateCsp(array $headers, array $options = []): array
+ {
+ $csp = $headers['content-security-policy'] ?? $headers['content-security-policy-report-only'] ?? null;
+
+ if ($csp === null) {
+ return [];
+ }
+
+ $issues = [];
+ $allowUnsafeInline = $options['allow_unsafe_inline'] ?? false;
+ $allowUnsafeEval = $options['allow_unsafe_eval'] ?? false;
+
+ $directives = self::parseCspString($csp);
+
+ // Check for unsafe-inline in script-src
+ if (! $allowUnsafeInline && isset($directives['script-src'])) {
+ if (in_array("'unsafe-inline'", $directives['script-src'], true)) {
+ $issues['script-src'] = "'unsafe-inline' allows XSS attacks";
+ }
+ }
+
+ // Check for unsafe-eval in script-src
+ if (! $allowUnsafeEval && isset($directives['script-src'])) {
+ if (in_array("'unsafe-eval'", $directives['script-src'], true)) {
+ $issues['script-src'] = ($issues['script-src'] ?? '') . " 'unsafe-eval' allows code injection";
+ }
+ }
+
+ return $issues;
+ }
+
+ /**
+ * Validate Permissions-Policy header.
+ */
+ protected static function validatePermissionsPolicy(array $headers): ?string
+ {
+ $value = $headers['permissions-policy'] ?? null;
+
+ if ($value === null) {
+ return 'Permissions-Policy header is missing';
+ }
+
+ // Basic syntax check
+ if (empty(trim($value))) {
+ return 'Permissions-Policy header is empty';
+ }
+
+ return null;
+ }
+
+ /**
+ * Parse CSP string into directives array.
+ *
+ * @return array>
+ */
+ protected static function parseCspString(string $csp): array
+ {
+ $directives = [];
+
+ foreach (explode(';', $csp) as $part) {
+ $part = trim($part);
+ if (empty($part)) {
+ continue;
+ }
+
+ $tokens = preg_split('/\s+/', $part);
+ $directiveName = array_shift($tokens);
+ $directives[$directiveName] = $tokens;
+ }
+
+ return $directives;
+ }
+
+ // ─────────────────────────────────────────────────────────────────────────────
+ // Analysis methods for report generation
+ // ─────────────────────────────────────────────────────────────────────────────
+
+ /**
+ * Analyze HSTS header for report.
+ */
+ protected static function analyzeHsts(array $headers): array
+ {
+ $value = $headers['strict-transport-security'] ?? null;
+
+ if ($value === null) {
+ return ['present' => false, 'value' => null];
+ }
+
+ preg_match('/max-age=(\d+)/', $value, $matches);
+
+ return [
+ 'present' => true,
+ 'value' => $value,
+ 'max_age' => isset($matches[1]) ? (int) $matches[1] : null,
+ 'include_subdomains' => str_contains($value, 'includeSubDomains'),
+ 'preload' => str_contains($value, 'preload'),
+ ];
+ }
+
+ /**
+ * Analyze CSP header for report.
+ */
+ protected static function analyzeCsp(array $headers): array
+ {
+ $csp = $headers['content-security-policy'] ?? null;
+ $reportOnly = $headers['content-security-policy-report-only'] ?? null;
+
+ if ($csp === null && $reportOnly === null) {
+ return ['present' => false, 'value' => null, 'report_only' => null];
+ }
+
+ $value = $csp ?? $reportOnly;
+ $directives = self::parseCspString($value);
+
+ return [
+ 'present' => true,
+ 'report_only' => $csp === null,
+ 'value' => $value,
+ 'directives' => $directives,
+ 'has_nonce' => (bool) preg_match("/'nonce-/", $value),
+ 'has_unsafe_inline' => str_contains($value, "'unsafe-inline'"),
+ 'has_unsafe_eval' => str_contains($value, "'unsafe-eval'"),
+ ];
+ }
+
+ /**
+ * Analyze Permissions-Policy header for report.
+ */
+ protected static function analyzePermissionsPolicy(array $headers): array
+ {
+ $value = $headers['permissions-policy'] ?? null;
+
+ if ($value === null) {
+ return ['present' => false, 'value' => null];
+ }
+
+ // Parse features
+ $features = [];
+ preg_match_all('/(\w+(?:-\w+)*)=\(([^)]*)\)/', $value, $matches, PREG_SET_ORDER);
+ foreach ($matches as $match) {
+ $features[$match[1]] = trim($match[2]) === '' ? [] : preg_split('/\s+/', trim($match[2]));
+ }
+
+ return [
+ 'present' => true,
+ 'value' => $value,
+ 'features' => $features,
+ ];
+ }
+
+ /**
+ * Analyze X-Frame-Options header for report.
+ */
+ protected static function analyzeXFrameOptions(array $headers): array
+ {
+ $value = $headers['x-frame-options'] ?? null;
+
+ return [
+ 'present' => $value !== null,
+ 'value' => $value,
+ 'valid' => $value !== null && in_array(strtoupper($value), self::VALID_X_FRAME_OPTIONS, true),
+ ];
+ }
+
+ /**
+ * Analyze X-Content-Type-Options header for report.
+ */
+ protected static function analyzeXContentTypeOptions(array $headers): array
+ {
+ $value = $headers['x-content-type-options'] ?? null;
+
+ return [
+ 'present' => $value !== null,
+ 'value' => $value,
+ 'valid' => $value !== null && strtolower($value) === 'nosniff',
+ ];
+ }
+
+ /**
+ * Analyze Referrer-Policy header for report.
+ */
+ protected static function analyzeReferrerPolicy(array $headers): array
+ {
+ $value = $headers['referrer-policy'] ?? null;
+
+ return [
+ 'present' => $value !== null,
+ 'value' => $value,
+ 'strict' => $value !== null && in_array(strtolower($value), self::STRICT_REFERRER_POLICIES, true),
+ ];
+ }
+
+ /**
+ * Analyze X-XSS-Protection header for report.
+ */
+ protected static function analyzeXssProtection(array $headers): array
+ {
+ $value = $headers['x-xss-protection'] ?? null;
+
+ return [
+ 'present' => $value !== null,
+ 'value' => $value,
+ 'enabled' => $value !== null && str_starts_with($value, '1'),
+ 'mode_block' => $value !== null && str_contains($value, 'mode=block'),
+ ];
+ }
+}
diff --git a/packages/core-php/src/Core/Headers/Views/livewire/header-configuration-manager.blade.php b/packages/core-php/src/Core/Headers/Views/livewire/header-configuration-manager.blade.php
new file mode 100644
index 0000000..b320053
--- /dev/null
+++ b/packages/core-php/src/Core/Headers/Views/livewire/header-configuration-manager.blade.php
@@ -0,0 +1,324 @@
+
+ {{-- Notification Messages --}}
+ @if ($saveMessage)
+
+ @endif
+
+ @if ($errorMessage)
+
+ @endif
+
+ {{-- Header Section --}}
+
+
+
Security Headers Configuration
+
Configure HTTP security headers for your application.
+
+
+ Enable Headers
+
+
+
+
+ {{-- Tab Navigation --}}
+
+
+ @foreach (['csp' => 'Content Security Policy', 'hsts' => 'HSTS', 'permissions' => 'Permissions Policy', 'other' => 'Other Headers'] as $tab => $label)
+
+ {{ $label }}
+
+ @endforeach
+
+
+
+ {{-- CSP Tab --}}
+ @if ($activeTab === 'csp')
+
+ {{-- CSP Enable/Disable --}}
+
+
+
Content Security Policy
+
Control which resources can be loaded.
+
+
+
+
+
+
+ @if ($cspEnabled)
+ {{-- CSP Options --}}
+
+
+
+ Report URI
+
+
+
+ {{-- CSP Directives --}}
+
+
+
CSP Directives
+ @if (count($this->getAvailableDirectives()) > 0)
+
+
+ Add directive...
+ @foreach ($this->getAvailableDirectives() as $directive)
+ {{ $directive }}
+ @endforeach
+
+
+ @endif
+
+
+
+ @foreach ($cspDirectives as $directive => $value)
+
+
+ {{ $directive }}
+
+
+
+
+
+
+
+
+ @endforeach
+
+
+
+ {{-- External Services --}}
+
+
+ {{-- CSP Preview --}}
+
+
+ CSP Header Preview
+
+
{{ $this->previewCspHeader() }}
+
+ @endif
+
+ @endif
+
+ {{-- HSTS Tab --}}
+ @if ($activeTab === 'hsts')
+
+
+
+
HTTP Strict Transport Security
+
Enforce HTTPS connections.
+
+
+
+
+
+
+ @if ($hstsEnabled)
+
+
Max Age (seconds)
+
+
Recommended: 31536000 (1 year)
+
+
+
+
+
+
+ Note: HSTS headers are only sent in production environments to prevent development issues.
+
+
+ @endif
+
+ @endif
+
+ {{-- Permissions Policy Tab --}}
+ @if ($activeTab === 'permissions')
+
+
+
Permissions Policy
+
Control browser features and APIs.
+
+
+
+ @foreach ($permissionsFeatures as $feature => $config)
+
+
+ {{ $feature }}
+
+
+
+
+ @if ($config['enabled'])
+
+ @endif
+
+ @endforeach
+
+
+ @endif
+
+ {{-- Other Headers Tab --}}
+ @if ($activeTab === 'other')
+
+
+
X-Frame-Options
+
+ DENY
+ SAMEORIGIN
+
+
Prevents clickjacking attacks by controlling iframe embedding.
+
+
+
+
Referrer-Policy
+
+ no-referrer
+ no-referrer-when-downgrade
+ origin
+ origin-when-cross-origin
+ same-origin
+ strict-origin
+ strict-origin-when-cross-origin
+ unsafe-url
+
+
Controls how much referrer information is sent with requests.
+
+
+
+
Fixed Headers
+
These headers are always included for security:
+
+ X-Content-Type-Options: nosniff
+ X-XSS-Protection: 1; mode=block
+
+
+
+ @endif
+
+ {{-- Action Buttons --}}
+
+
+ Reset to Defaults
+
+
+
+ Export .env
+
+
+ Save Configuration
+
+
+
+
diff --git a/packages/core-php/src/Core/Headers/config.php b/packages/core-php/src/Core/Headers/config.php
index 893d2d3..59f8edc 100644
--- a/packages/core-php/src/Core/Headers/config.php
+++ b/packages/core-php/src/Core/Headers/config.php
@@ -63,6 +63,34 @@ return [
// Report URI for CSP violation reports
'report_uri' => env('SECURITY_CSP_REPORT_URI'),
+ /*
+ |----------------------------------------------------------------------
+ | Nonce-based CSP
+ |----------------------------------------------------------------------
+ |
+ | When enabled, a unique nonce is generated per request and added to
+ | script-src and style-src directives. Inline scripts/styles must
+ | include the nonce attribute to be allowed.
+ |
+ | Usage in Blade:
+ |
+ |
+ |
+ */
+
+ // Enable nonce-based CSP (recommended for production)
+ 'nonce_enabled' => env('SECURITY_CSP_NONCE_ENABLED', true),
+
+ // Nonce length in bytes (16 = 128 bits, recommended minimum)
+ 'nonce_length' => env('SECURITY_CSP_NONCE_LENGTH', 16),
+
+ // Directives to add nonces to
+ 'nonce_directives' => ['script-src', 'style-src'],
+
+ // Environments where nonces are skipped (unsafe-inline is used instead)
+ // This avoids issues with hot reload and dev tools
+ 'nonce_skip_environments' => ['local', 'development'],
+
// CSP Directives
'directives' => [
'default-src' => ["'self'"],
diff --git a/packages/core-php/src/Core/Headers/helpers.php b/packages/core-php/src/Core/Headers/helpers.php
new file mode 100644
index 0000000..6dff3e9
--- /dev/null
+++ b/packages/core-php/src/Core/Headers/helpers.php
@@ -0,0 +1,49 @@
+
+ * // Your inline JavaScript
+ *
+ * ```
+ *
+ * @return string The base64-encoded nonce value
+ */
+ function csp_nonce(): string
+ {
+ return app(CspNonceService::class)->getNonce();
+ }
+}
+
+if (! function_exists('csp_nonce_attribute')) {
+ /**
+ * Get the CSP nonce as an HTML attribute.
+ *
+ * Usage in Blade templates:
+ * ```blade
+ *
+ * ```
+ *
+ * @return string The nonce attribute (e.g., 'nonce="abc123..."')
+ */
+ function csp_nonce_attribute(): string
+ {
+ return app(CspNonceService::class)->getNonceAttribute();
+ }
+}
diff --git a/packages/core-php/src/Core/Helpers/HadesEncrypt.php b/packages/core-php/src/Core/Helpers/HadesEncrypt.php
index 99d5cf8..48f0cb9 100644
--- a/packages/core-php/src/Core/Helpers/HadesEncrypt.php
+++ b/packages/core-php/src/Core/Helpers/HadesEncrypt.php
@@ -165,7 +165,7 @@ TEXT;
// Check if it's already valid PEM format
if (str_starts_with($keyData, '-----BEGIN')) {
- // Handle escaped newlines from Docker/Coolify
+ // Handle escaped newlines from Docker environments
$keyData = str_replace(['\\n', '\n', '\\\\n'], "\n", $keyData);
return $keyData;
diff --git a/packages/core-php/src/Core/Input/Sanitiser.php b/packages/core-php/src/Core/Input/Sanitiser.php
index f79de77..8fe50cc 100644
--- a/packages/core-php/src/Core/Input/Sanitiser.php
+++ b/packages/core-php/src/Core/Input/Sanitiser.php
@@ -25,15 +25,72 @@ use Psr\Log\LoggerInterface;
* - Configurable filter rules per field via schema
* - Unicode NFC normalization for consistent string handling
* - Optional audit logging when content is modified
+ * - Rich text support with safe HTML tags whitelist
+ * - Configurable maximum input length enforcement
+ * - Transformation hooks for custom processing at different stages
+ *
+ * ## Transformation Hooks
+ *
+ * Register callbacks to transform values at specific stages of the
+ * sanitization pipeline:
+ *
+ * ```php
+ * $sanitiser = (new Sanitiser())
+ * ->beforeFilter(function (string $value, string $field): string {
+ * // Transform before any filtering
+ * return trim($value);
+ * })
+ * ->afterFilter(function (string $value, string $field): string {
+ * // Transform after all filtering is complete
+ * return $value;
+ * })
+ * ->transformField('username', function (string $value): string {
+ * // Field-specific transformation
+ * return strtolower($value);
+ * });
+ * ```
+ *
+ * Hook execution order:
+ * 1. Before hooks (global, then field-specific)
+ * 2. Standard filtering pipeline (normalize, strip, HTML, preset, filters, length)
+ * 3. After hooks (global, then field-specific)
*/
class Sanitiser
{
+ /**
+ * Default safe HTML tags for rich text fields.
+ * These tags are considered safe for user-generated content.
+ */
+ public const SAFE_HTML_TAGS = '