diff --git a/src/Core/Actions/CLAUDE.md b/src/Core/Actions/CLAUDE.md new file mode 100644 index 0000000..f20250d --- /dev/null +++ b/src/Core/Actions/CLAUDE.md @@ -0,0 +1,57 @@ +# Actions + +Single-purpose business logic pattern with scheduling support. + +## What It Does + +Provides the `Action` trait for extracting business logic from controllers/components into focused, testable classes. Each action does one thing via a `handle()` method and gets a static `run()` shortcut that resolves dependencies from the container. + +Also provides attribute-driven scheduling: annotate an Action with `#[Scheduled]` and it gets wired into Laravel's scheduler automatically. + +## Key Classes + +| Class | Purpose | +|-------|---------| +| `Action` (trait) | Adds `static run(...$args)` that resolves via container and calls `handle()` | +| `Actionable` (interface) | Optional contract for type-hinting actions | +| `Scheduled` (attribute) | Marks an Action for scheduled execution with frequency string | +| `ScheduledAction` (model) | Eloquent model persisted to `scheduled_actions` table | +| `ScheduledActionScanner` | Discovers `#[Scheduled]` attributes by scanning directories | +| `ScheduleServiceProvider` | Reads enabled scheduled actions from DB and registers with Laravel scheduler | + +## Public API + +```php +// Use the Action pattern +class CreateOrder { + use Action; + public function handle(User $user, array $data): Order { ... } +} +CreateOrder::run($user, $data); // resolves from container + +// Schedule an action +#[Scheduled(frequency: 'dailyAt:09:00', timezone: 'Europe/London')] +class PublishDigest { + use Action; + public function handle(): void { ... } +} +``` + +## Frequency String Format + +`method:arg1,arg2` maps directly to Laravel Schedule methods: +- `everyMinute` / `hourly` / `daily` / `weekly` / `monthly` +- `dailyAt:09:00` / `weeklyOn:1,09:00` / `cron:* * * * *` + +## Integration + +- Scanner skips `Tests/` directories and `*Test.php` files +- ScheduleServiceProvider validates namespace (`App\`, `Core\`, `Mod\`) and frequency method against allowlists before executing +- Actions are placed in `app/Mod/{Module}/Actions/` + +## Conventions + +- One action per file, named after what it does: `CreatePage`, `SendInvoice` +- Dependencies injected via constructor +- `handle()` is the single public method +- Scheduling state is DB-driven (enable/disable without code changes) diff --git a/src/Core/Activity/CLAUDE.md b/src/Core/Activity/CLAUDE.md new file mode 100644 index 0000000..49da5fe --- /dev/null +++ b/src/Core/Activity/CLAUDE.md @@ -0,0 +1,48 @@ +# Activity + +Workspace-aware activity logging built on `spatie/laravel-activitylog`. + +## What It Does + +Wraps Spatie's activity log with automatic `workspace_id` tagging, a fluent query service, a Livewire feed component for the admin panel, and a prune command for retention management. + +## Key Classes + +| Class | Purpose | +|-------|---------| +| `Boot` | Registers console commands, Livewire component, and service binding via lifecycle events | +| `Activity` (model) | Extends Spatie's model with `ActivityScopes` trait. Adds `workspace_id`, `old_values`, `new_values`, `changes`, `causer_name`, `subject_name` accessors | +| `ActivityLogService` | Fluent query builder: `logFor($model)`, `logBy($user)`, `forWorkspace($ws)`, `ofType('updated')`, `search('term')`, `paginate()`, `statistics()`, `timeline()`, `prune()` | +| `LogsActivity` (trait) | Drop-in trait for models. Auto-logs dirty attributes, auto-tags `workspace_id` from model or request context, generates human descriptions | +| `ActivityScopes` (trait) | 20+ Eloquent scopes: `forWorkspace`, `forSubject`, `byCauser`, `ofType`, `betweenDates`, `today`, `lastDays`, `search`, `withChanges`, `withExistingSubject` | +| `ActivityPruneCommand` | `php artisan activity:prune [--days=N] [--dry-run]` | +| `ActivityFeed` (Livewire) | `` with filters, search, pagination, detail modal | + +## Public API + +```php +// Make a model log activity +class Post extends Model { + use LogsActivity; + protected array $activityLogAttributes = ['title', 'status']; +} + +// Query activities +$service = app(ActivityLogService::class); +$service->logFor($post)->lastDays(7)->paginate(); +$service->forWorkspace($workspace)->ofType('deleted')->recent(10); +$service->statistics($workspace); // => [total, by_event, by_subject, by_user] +``` + +## Integration + +- Listens to `ConsoleBooting` and `AdminPanelBooting` lifecycle events +- `LogsActivity` trait auto-detects workspace from model's `workspace_id` attribute, request `workspace_model` attribute, or authenticated user's `defaultHostWorkspace()` +- Config: `core.activity.enabled`, `core.activity.retention_days` (default 90), `core.activity.log_name` +- Override activity model in `config/activitylog.php`: `'activity_model' => Activity::class` + +## Conventions + +- `LogsActivity::withoutActivityLogging(fn() => ...)` to suppress logging during bulk operations +- Models can implement `customizeActivity($activity, $event)` for custom property injection +- Config properties on model: `$activityLogAttributes`, `$activityLogName`, `$activityLogEvents`, `$activityLogWorkspace`, `$activityLogOnlyDirty` diff --git a/src/Core/Bouncer/CLAUDE.md b/src/Core/Bouncer/CLAUDE.md new file mode 100644 index 0000000..ac0d6ff --- /dev/null +++ b/src/Core/Bouncer/CLAUDE.md @@ -0,0 +1,61 @@ +# Bouncer + +Early-exit security middleware + whitelist-based action authorisation gate. + +## What It Does + +Two subsystems in one: + +1. **Bouncer** (top-level): IP blocklist + SEO redirects, runs before all other middleware +2. **Gate** (subdirectory): Whitelist-based controller action authorisation with training mode + +## Bouncer (IP Blocking + Redirects) + +### Key Classes + +| Class | Purpose | +|-------|---------| +| `Boot` | ServiceProvider registering `BlocklistService`, `RedirectService`, and migrations | +| `BouncerMiddleware` | Early-exit middleware: sets trusted proxies, checks blocklist (O(1) via cached set), handles SEO redirects, then passes through | +| `BlocklistService` | IP blocking with Redis-cached lookup. Statuses: `pending` (honeypot, needs review), `approved` (active block), `rejected` (reviewed, not blocked). Methods: `isBlocked()`, `block()`, `unblock()`, `syncFromHoneypot()`, `approve()`, `reject()`, `getPending()`, `getStats()` | +| `RedirectService` | Cached SEO redirects from `seo_redirects` table. Supports exact match and wildcard (`path/*`). Methods: `match()`, `add()`, `remove()` | + +### Hidden Ideas + +- Blocked IPs get `418 I'm a teapot` with `X-Powered-By: Earl Grey` +- Honeypot monitors paths from `robots.txt` disallow list; critical paths (`/admin`, `/.env`, `/wp-admin`) trigger auto-block +- Rate-limited honeypot logging prevents DoS via log flooding +- `TRUSTED_PROXIES` env var: comma-separated IPs or `*` (trust all) + +## Gate (Action Whitelist) + +Philosophy: **"If it wasn't trained, it doesn't exist."** + +### Key Classes + +| Class | Purpose | +|-------|---------| +| `Gate\Boot` | ServiceProvider registering middleware, migrations, route macros, and training routes | +| `ActionGateService` | Resolves action name from route (3-level priority), checks against `ActionPermission` table, logs to `ActionRequest`. Methods: `check()`, `allow()`, `deny()`, `resolveAction()` | +| `ActionGateMiddleware` | Enforces gate: allowed = pass, denied = 403, training = approval prompt (JSON for API, redirect for web) | +| `Action` (attribute) | `#[Action('product.create', scope: 'product')]` on controller methods | +| `ActionPermission` (model) | Whitelist record: action + guard + role + scope. Methods: `isAllowed()`, `train()`, `revoke()`, `allowedFor()` | +| `ActionRequest` (model) | Audit log of all permission checks. Methods: `log()`, `pending()`, `deniedActionsSummary()`, `prune()` | +| `RouteActionMacro` | Adds `->action('name')`, `->bypassGate()`, `->requiresTraining()` to Route | + +### Action Resolution Priority + +1. Route action: `Route::post(...)->action('product.create')` +2. Controller attribute: `#[Action('product.create')]` +3. Auto-resolved: `ProductController@store` becomes `product.store` + +### Training Mode + +When `core.bouncer.training_mode = true`, unknown actions prompt for approval instead of blocking. Training routes at `/_bouncer/approve` and `/_bouncer/pending`. + +## Integration + +- BouncerMiddleware runs FIRST in the stack (replaces Laravel TrustProxies) +- ActionGateMiddleware appends to `web`, `admin`, `api`, `client` groups +- Config: `core.bouncer.enabled`, `core.bouncer.training_mode`, `core.bouncer.guarded_middleware` +- DB tables: `blocked_ips`, `seo_redirects`, `honeypot_hits`, `core_action_permissions`, `core_action_requests` diff --git a/src/Core/Cdn/CLAUDE.md b/src/Core/Cdn/CLAUDE.md new file mode 100644 index 0000000..298cc4e --- /dev/null +++ b/src/Core/Cdn/CLAUDE.md @@ -0,0 +1,57 @@ +# Cdn + +BunnyCDN integration with vBucket workspace isolation and storage offloading. + +## What It Does + +Unified CDN and object storage layer providing: +- BunnyCDN pull zone operations (purge, stats) +- BunnyCDN storage zone operations (upload, download, list, delete) +- Context-aware URL building (CDN, origin, private, signed) +- vBucket-scoped paths using `LthnHash` for tenant isolation +- Asset pipeline for processing and offloading +- Flux Pro CDN delivery +- Storage offload migration from local to CDN + +## Key Classes + +| Class | Purpose | +|-------|---------| +| `Boot` | ServiceProvider registering all services as singletons + backward-compat aliases to `App\` namespaces | +| `BunnyCdnService` | Pull zone API: `purgeUrl()`, `purgeUrls()`, `purgeAll()`, `purgeByTag()`, `purgeWorkspace()`, `getStats()`, `getBandwidth()`, `listStorageFiles()`, `uploadFile()`, `deleteFile()`. Sanitises error messages to redact API keys | +| `BunnyStorageService` | Direct storage zone operations (separate from pull zone API) | +| `CdnUrlBuilder` | URL construction: `cdn()`, `origin()`, `private()`, `apex()`, `signed()`, `vBucket()`, `vBucketId()`, `vBucketPath()`, `asset()`, `withVersion()`, `urls()`, `allUrls()` | +| `StorageUrlResolver` | Context-aware URL resolution | +| `FluxCdnService` | Flux Pro component CDN delivery | +| `AssetPipeline` | Asset processing pipeline | +| `StorageOffload` (service) | Migrates files from local storage to CDN | +| `StorageOffload` (model) | Tracks offloaded files in DB | +| `Cdn` (facade) | `Cdn::purge(...)` etc. | +| `HasCdnUrls` (trait) | Adds CDN URL methods to Eloquent models | + +## Console Commands + +- `cdn:purge` -- Purge CDN cache +- `cdn:push-assets` -- Push assets to CDN storage +- `cdn:push-flux` -- Push Flux Pro assets to CDN +- `cdn:offload-migrate` -- Migrate local files to CDN storage + +## Middleware + +- `RewriteOffloadedUrls` -- Rewrites storage URLs in responses to CDN URLs +- `LocalCdnMiddleware` -- Serves CDN assets locally in development + +## vBucket Pattern + +Workspace-isolated CDN paths using `LthnHash::vBucketId()`: +``` +cdn.example.com/{vBucketId}/path/to/asset.js +``` +The vBucketId is a deterministic SHA-256 hash of the domain name, ensuring each workspace's assets are namespaced. + +## Integration + +- Reads credentials from `ConfigService` (DB-backed config), not just `.env` +- Signed URLs use HMAC-SHA256 with BunnyCDN token authentication +- Config files: `config.php` (CDN settings), `offload.php` (storage offload settings) +- Backward-compat aliases registered for all `App\Services\*` and `App\Models\*` namespaces diff --git a/src/Core/Config/CLAUDE.md b/src/Core/Config/CLAUDE.md new file mode 100644 index 0000000..60075a6 --- /dev/null +++ b/src/Core/Config/CLAUDE.md @@ -0,0 +1,70 @@ +# Config + +Database-backed configuration with scoping, versioning, profiles, and admin UI. + +## What It Does + +Replaces/supplements Laravel's file-based config with a DB-backed system supporting: +- Hierarchical scope resolution (global -> workspace -> user) +- Configuration profiles (sets of values that can be switched) +- Version history with diffs +- Sensitive value encryption +- Import/export (JSON/YAML) +- Livewire admin panels +- Event-driven invalidation + +## Key Classes + +| Class | Purpose | +|-------|---------| +| `Boot` | Listens to `AdminPanelBooting` and `ConsoleBooting` for registration | +| `ConfigService` | Primary API: `get()`, `set()`, `isConfigured()`, plus scope-aware resolution | +| `ConfigResolver` | Resolves values through scope hierarchy: user -> workspace -> global -> default | +| `ConfigResult` | DTO wrapping resolved value with metadata (source scope, profile, etc.) | +| `ConfigVersioning` | Tracks changes with diffs between versions | +| `VersionDiff` | Computes and formats diffs between config versions | +| `ConfigExporter` | Export/import config as JSON/YAML | +| `ImportResult` | DTO for import operation results | + +## Models + +| Model | Table | Purpose | +|-------|-------|---------| +| `ConfigKey` | `config_keys` | Key definitions with type, default, validation rules, `is_sensitive` flag | +| `ConfigValue` | `config_values` | Actual values scoped by type (global/workspace/user) | +| `ConfigProfile` | `config_profiles` | Named sets of config values (soft-deletable) | +| `ConfigVersion` | `config_versions` | Version history snapshots | +| `ConfigResolved` | -- | Value object for resolved config | +| `Channel` | -- | Notification channel config | + +## Enums + +- `ConfigType` -- Value types (string, int, bool, json, etc.) +- `ScopeType` -- Resolution scopes (global, workspace, user) + +## Events + +- `ConfigChanged` -- Fired when any config value changes +- `ConfigInvalidated` -- Fired when cache needs clearing +- `ConfigLocked` -- Fired when a config key is locked + +## Console Commands + +- `config:prime` -- Pre-populate config cache +- `config:list` -- List all config keys and values +- `config:version` -- Show version history +- `config:import` -- Import config from file +- `config:export` -- Export config to file + +## Admin UI + +- `ConfigPanel` (Livewire) -- General config editing panel +- `WorkspaceConfig` (Livewire) -- Workspace-specific config panel +- Routes registered under admin prefix + +## Integration + +- `ConfigService` is used by other subsystems (e.g., `BunnyCdnService` reads CDN credentials via `$this->config->get('cdn.bunny.api_key')`) +- Sensitive keys (`is_sensitive = true`) are encrypted at rest +- Seeder: `ConfigKeySeeder` populates default keys +- 4 migrations covering base tables, soft deletes, versions, and sensitive flag diff --git a/src/Core/Console/CLAUDE.md b/src/Core/Console/CLAUDE.md new file mode 100644 index 0000000..d26eff1 --- /dev/null +++ b/src/Core/Console/CLAUDE.md @@ -0,0 +1,32 @@ +# Core\Console + +Framework artisan commands registered via the `ConsoleBooting` lifecycle event. + +## Boot + +Uses the event-driven module loading pattern: + +```php +public static array $listens = [ + ConsoleBooting::class => 'onConsole', +]; +``` + +## Commands + +| Command | Signature | Purpose | +|---------|-----------|---------| +| `InstallCommand` | `core:install` | Framework setup wizard: env file, app config, migrations, app key, storage link. Supports `--dry-run` and `--force` | +| `NewProjectCommand` | `core:new` | Scaffold a new project | +| `MakeModCommand` | `make:mod {name}` | Generate a module in the `Mod` namespace with Boot.php. Flags: `--web`, `--admin`, `--api`, `--console`, `--all` | +| `MakePlugCommand` | `make:plug` | Generate a plugin scaffold | +| `MakeWebsiteCommand` | `make:website` | Generate a Website module scaffold | +| `PruneEmailShieldStatsCommand` | prunes `email_shield_stats` | Cleans old EmailShield validation stats | +| `ScheduleSyncCommand` | schedule sync | Schedule synchronisation | + +## Conventions + +- All commands use `declare(strict_types=1)` and the `Core\Console\Commands` namespace. +- `MakeModCommand` generates a complete module scaffold with optional handler stubs (web routes, admin panel, API, console). +- `InstallCommand` tracks progress via named installation steps and supports dry-run mode. +- Commands are registered via `$event->command()` on the `ConsoleBooting` event, not via a service provider's `$this->commands()`. diff --git a/src/Core/Crypt/CLAUDE.md b/src/Core/Crypt/CLAUDE.md new file mode 100644 index 0000000..d9abd41 --- /dev/null +++ b/src/Core/Crypt/CLAUDE.md @@ -0,0 +1,65 @@ +# Crypt + +Encryption utilities: encrypted Eloquent casts and LTHN QuasiHash identifier generator. + +## What It Does + +Two independent tools: + +1. **EncryptArrayObject** -- Eloquent cast that encrypts/decrypts array data transparently using Laravel's `Crypt` facade +2. **LthnHash** -- Deterministic identifier generator for workspace scoping, vBucket CDN paths, and consistent sharding + +## Key Classes + +| Class | Purpose | +|-------|---------| +| `EncryptArrayObject` | `CastsAttributes` implementation. Encrypts arrays as JSON+AES on write, decrypts on read. Fails gracefully (returns null + logs warning) | +| `LthnHash` | Static utility: `hash()`, `shortHash()`, `fastHash()`, `vBucketId()`, `toInt()`, `verify()`, `benchmark()`. Supports key rotation | + +## EncryptArrayObject Usage + +```php +class ApiCredential extends Model { + protected $casts = ['secrets' => EncryptArrayObject::class]; +} +$model->secrets['api_key'] = 'sk_live_xxx'; // encrypted in DB +``` + +## LthnHash API + +| Method | Output | Use Case | +|--------|--------|----------| +| `hash($input)` | 64 hex chars (SHA-256) | Default, high quality | +| `shortHash($input, $len)` | 16-32 hex chars | Space-constrained IDs | +| `fastHash($input)` | 8-16 hex chars (xxHash/CRC32) | High-throughput | +| `vBucketId($domain)` | 64 hex chars | CDN path isolation | +| `toInt($input, $max)` | int (60 bits) | Sharding/partitioning | +| `verify($input, $hash)` | bool | Constant-time comparison, tries all key maps | +| `benchmark($iterations)` | timing array | Performance measurement | + +## Algorithm + +1. Reverse input, apply character substitution map (key map) +2. Concatenate original + substituted string +3. Hash with SHA-256 (or xxHash/CRC32 for `fastHash`) + +## Key Rotation + +```php +LthnHash::addKeyMap('v2', $newMap, setActive: true); +// New hashes use v2, verify() tries v2 first then falls back to older maps +LthnHash::removeKeyMap('v1'); // after migration +``` + +## NOT For + +- Password hashing (use `password_hash()`) +- Security tokens (use `random_bytes()`) +- Cryptographic signatures + +## Integration + +- `CdnUrlBuilder::vBucketId()` delegates to `LthnHash::vBucketId()` +- `verify()` uses `hash_equals()` for timing-attack resistance +- `fastHash()` auto-selects xxh64 (PHP 8.1+) or CRC32b+CRC32c fallback +- `toInt()` uses GMP for safe large-integer modular arithmetic diff --git a/src/Core/Database/Seeders/CLAUDE.md b/src/Core/Database/Seeders/CLAUDE.md new file mode 100644 index 0000000..da5e233 --- /dev/null +++ b/src/Core/Database/Seeders/CLAUDE.md @@ -0,0 +1,68 @@ +# Database/Seeders + +Auto-discovering, dependency-aware seeder orchestration. + +## What It Does + +Replaces Laravel's manual seeder ordering with automatic discovery and topological sorting. Seeders declare their dependencies via attributes or properties, and the framework figures out the correct execution order using Kahn's algorithm. + +## Key Classes + +| Class | Purpose | +|-------|---------| +| `CoreDatabaseSeeder` | Base seeder class. Extends Laravel's `Seeder`. Auto-discovers seeders from configured paths, applies `--exclude` and `--only` filters, runs them in dependency order | +| `SeederDiscovery` | Scans directories for `*Seeder.php` files, reads priority/dependency metadata from attributes or properties, produces topologically sorted list | +| `SeederRegistry` | Manual registration alternative: `register(Class, priority: 10, after: [...])`. Fluent API with `registerMany()`, `merge()`, `getOrdered()` | + +## Attributes + +| Attribute | Target | Purpose | +|-----------|--------|---------| +| `#[SeederPriority(10)]` | Class | Lower values run first (default: 50) | +| `#[SeederAfter(FeatureSeeder::class)]` | Class | Must run after specified seeders (repeatable) | +| `#[SeederBefore(PackageSeeder::class)]` | Class | Must run before specified seeders (repeatable) | + +## Priority Guidelines + +- 0-20: Foundation (features, configuration) +- 20-40: Core data (packages, workspaces) +- 40-60: Default (general seeders) +- 60-80: Content (pages, posts) +- 80-100: Demo/test data + +## Ordering Rules + +Dependencies take precedence over priority. Within the same dependency level, lower priority numbers run first. Circular dependencies throw `CircularDependencyException` with the full cycle path. + +## Usage + +```php +// Auto-discovery (default) +class DatabaseSeeder extends CoreDatabaseSeeder { + protected function getSeederPaths(): array { + return [app_path('Core'), app_path('Mod')]; + } +} + +// Manual registration +class DatabaseSeeder extends CoreDatabaseSeeder { + protected bool $autoDiscover = false; + protected function registerSeeders(SeederRegistry $registry): void { + $registry->register(FeatureSeeder::class, priority: 10) + ->register(PackageSeeder::class, after: [FeatureSeeder::class]); + } +} + +// CLI filtering +php artisan db:seed --exclude=DemoSeeder --only=FeatureSeeder +``` + +## Discovery Paths + +Scans `{path}/*/Database/Seeders/*Seeder.php` (module subdirs) and `{path}/Database/Seeders/*Seeder.php` (direct). Configured via `core.seeders.paths` or defaults to `app/Core`, `app/Mod`, `app/Website`. + +## Integration + +- Properties alternative to attributes: `public int $priority = 10;`, `public array $after = [...]`, `public array $before = [...]` +- Pattern matching for `--exclude`/`--only`: full class name, short name, or partial match +- Config: `core.seeders.auto_discover`, `core.seeders.paths`, `core.seeders.exclude` diff --git a/src/Core/Events/CLAUDE.md b/src/Core/Events/CLAUDE.md new file mode 100644 index 0000000..37d7f29 --- /dev/null +++ b/src/Core/Events/CLAUDE.md @@ -0,0 +1,84 @@ +# Events + +Lifecycle events that drive the module loading system. + +## What It Does + +Defines the event classes that modules listen to via `static $listens` arrays in their Boot classes. Events use a request/collect pattern: modules call methods like `routes()`, `views()`, `livewire()` during event dispatch, and `LifecycleEventProvider` processes the collected requests afterwards. + +This is the **core of the module loading architecture**. Modules are never instantiated until their listened events fire. + +## Lifecycle Events (Mutually Exclusive by Context) + +| Event | Context | Middleware | Purpose | +|-------|---------|------------|---------| +| `WebRoutesRegistering` | Web requests | `web` | Public-facing routes, views | +| `AdminPanelBooting` | Admin requests | `admin` | Admin dashboard resources | +| `ApiRoutesRegistering` | API requests | `api` | REST API endpoints | +| `ClientRoutesRegistering` | Client dashboard | `client` | Authenticated SaaS user routes | +| `ConsoleBooting` | CLI | -- | Artisan commands | +| `QueueWorkerBooting` | Queue workers | -- | Job registration, queue init | +| `McpToolsRegistering` | MCP server | -- | MCP tool handlers | +| `McpRoutesRegistering` | MCP HTTP | `mcp` | MCP HTTP endpoints | +| `FrameworkBooted` | All contexts | -- | Late-stage cross-cutting init | + +## Capability Events (On-Demand) + +| Event | Purpose | +|-------|---------| +| `DomainResolving` | Multi-tenancy by domain. First provider to `register()` wins | +| `SearchRequested` | Lazy-load search: `searchable(Model::class)` | +| `MediaRequested` | Lazy-load media: `processor('image', ImageProcessor::class)` | +| `MailSending` | Lazy-load mail: `mailable(WelcomeEmail::class)` | + +## Base Class: LifecycleEvent + +All lifecycle events extend `LifecycleEvent`, which provides these request methods: + +| Method | Purpose | +|--------|---------| +| `routes(callable)` | Register route callback | +| `views(namespace, path)` | Register view namespace | +| `livewire(alias, class)` | Register Livewire component | +| `middleware(alias, class)` | Register middleware alias | +| `command(class)` | Register Artisan command | +| `translations(namespace, path)` | Register translation namespace | +| `bladeComponentPath(path, namespace)` | Register anonymous Blade components | +| `policy(model, policy)` | Register model policy | +| `navigation(item)` | Register nav item | + +Each has a corresponding `*Requests()` getter for `LifecycleEventProvider` to process. + +## Observability + +| Class | Purpose | +|-------|---------| +| `ListenerProfiler` | Measures execution time, memory, call count per listener. `enable()`, `getSlowListeners()`, `getSlowest(10)`, `getSummary()`, `export()` | +| `EventAuditLog` | Tracks success/failure of event handlers. `enable()`, `entries()`, `failures()`, `summary()` | + +## Event Versioning + +| Class | Purpose | +|-------|---------| +| `HasEventVersion` (trait) | Modules declare `$eventVersions` for compatibility checking | + +Events carry `VERSION` and `MIN_SUPPORTED_VERSION` constants. Handlers check `$event->version()` or `$event->supportsVersion(2)` for forward compatibility. + +## Integration + +```php +// Module Boot class +class Boot { + public static array $listens = [ + WebRoutesRegistering::class => 'onWebRoutes', + AdminPanelBooting::class => ['onAdmin', 10], // with priority + ]; + + public function onWebRoutes(WebRoutesRegistering $event): void { + $event->views('mymod', __DIR__.'/Views'); + $event->routes(fn() => require __DIR__.'/web.php'); + } +} +``` + +Flow: `ModuleScanner` reads `$listens` -> `ModuleRegistry` registers `LazyModuleListener` with Laravel Events -> Event fires -> Module instantiated via container -> Method called with event -> Requests collected -> `LifecycleEventProvider` processes. diff --git a/src/Core/Front/CLAUDE.md b/src/Core/Front/CLAUDE.md new file mode 100644 index 0000000..7fe36b6 --- /dev/null +++ b/src/Core/Front/CLAUDE.md @@ -0,0 +1,82 @@ +# Front + +UI layer: admin panel, web frontage, Blade components, layouts, and tag compilers. + +## What It Does + +Three distinct frontages sharing a component library: + +1. **Admin** (`Admin/`) -- Admin dashboard with its own middleware group, menu registry, 50+ Blade components, and `` tag compiler +2. **Web** (`Web/`) -- Public-facing pages with `web` middleware, `` tag compiler, domain resolution +3. **Components** (`Components/`) -- Programmatic component library (Card, Heading, NavList, Layout, etc.) implementing `Htmlable` for use by MCP tools and agents + +## Directory Structure + +``` +Front/ + Controller.php -- Abstract base controller + Admin/ + Boot.php -- Admin ServiceProvider (middleware, components, tag compiler) + AdminMenuRegistry.php -- Menu builder with entitlements, permissions, caching + AdminTagCompiler.php -- Blade precompiler + TabContext.php -- Tab state management + Contracts/ -- AdminMenuProvider, DynamicMenuProvider interfaces + Support/ -- MenuItemBuilder, MenuItemGroup + Concerns/ -- HasMenuPermissions trait + Validation/ -- IconValidator (Font Awesome Pro validation) + View/Components/ -- 18 class-backed components (DataTable, Stats, Metrics, etc.) + Blade/ + components/ -- 30+ anonymous Blade components + layouts/app.blade.php + Web/ + Boot.php -- Web ServiceProvider (middleware, tag compiler, lifecycle fire) + WebTagCompiler.php -- Blade precompiler + Middleware/ + FindDomainRecord.php -- Resolves domain to workspace + ResilientSession.php -- Handles session issues gracefully + RedirectIfAuthenticated.php + Blade/ + components/ -- nav-item, page + layouts/app.blade.php + Components/ + Component.php -- Abstract base: fluent attr/class API, Htmlable + Card.php, Heading.php, NavList.php, Layout.php + CoreTagCompiler.php -- tag compiler + View/Blade/ -- 20+ component templates (forms, table, autocomplete, avatar, etc.) + Tests/Unit/ -- DeviceDetectionServiceTest +``` + +## Admin Menu System + +`AdminMenuRegistry` is the central hub: +- Modules implement `AdminMenuProvider` interface and register via `$registry->register($provider)` +- Items grouped into: `dashboard`, `agents`, `workspaces`, `services`, `settings`, `admin` +- Entitlement checks via `EntitlementService::can()` +- Permission checks via Laravel's `$user->can()` +- `DynamicMenuProvider` for runtime items (never cached) +- Cached with configurable TTL, invalidatable per workspace/user +- Icon validation against Font Awesome Pro + +## Middleware Groups + +**Admin** (`admin`): EncryptCookies, Session, CSRF, Bindings, SecurityHeaders, `auth` +**Web** (`web`): EncryptCookies, Session, ResilientSession, CSRF, Bindings, SecurityHeaders, FindDomainRecord + +## Tag Compilers + +Custom Blade precompilers enable ``, ``, and `` syntax (same pattern as ``). + +## Programmatic Components + +`Component` base class provides fluent API for building HTML without Blade: +```php +Card::make()->class('p-4')->attr('data-id', 42)->render() +``` +Used by MCP tools and agents to compose UIs programmatically. + +## Integration + +- Admin Boot fires `AdminPanelBooting` lifecycle event +- Web Boot fires `WebRoutesRegistering` via `$app->booted()` callback +- `livewire` aliased to `admin` for Flux Pro compatibility +- All admin components prefixed `admin-` (e.g., ``) diff --git a/src/Core/Headers/CLAUDE.md b/src/Core/Headers/CLAUDE.md new file mode 100644 index 0000000..6f79cab --- /dev/null +++ b/src/Core/Headers/CLAUDE.md @@ -0,0 +1,72 @@ +# Headers + +HTTP security headers, CSP nonce generation, device detection, and GeoIP lookup. + +## What It Does + +Four concerns bundled as a single module: + +1. **SecurityHeaders** middleware -- Adds HSTS, CSP, Permissions-Policy, X-Frame-Options, X-Content-Type-Options, X-XSS-Protection, Referrer-Policy to all responses +2. **CspNonceService** -- Per-request cryptographic nonce for inline scripts/styles, integrated with Vite +3. **DetectDevice** -- User-Agent parsing for device type, OS, browser, in-app browser detection (14 platforms) +4. **DetectLocation** -- GeoIP from CloudFlare headers, custom headers, or MaxMind database + +## Key Classes + +| Class | Purpose | +|-------|---------| +| `Boot` | ServiceProvider: config, singletons, Blade directives (`@cspnonce`, `@cspnoncevalue`), `csp_nonce()` helper, Livewire `header-configuration-manager` component | +| `SecurityHeaders` | Middleware. CSP built from: base directives -> env overrides -> nonces -> CDN sources -> external services -> dev WebSocket -> report URI. Supports report-only mode | +| `CspNonceService` | Generates one nonce per request (128-bit, base64). Auto-registers with `Vite::useCspNonce()`. Methods: `getNonce()`, `getCspNonceDirective()`, `getNonceAttribute()` | +| `DetectDevice` | `parse($ua)` returns `{device_type, os_name, browser_name, in_app_browser, is_in_app}`. Helpers: `isBot()`, `isInstagram()`, `isFacebook()`, `isTikTok()`, `isMetaPlatform()`, `isStrictContentPlatform()` | +| `DetectLocation` | `lookup($ip, $request)` returns `{country_code, region, city}`. Checks CF headers first, then MaxMind DB. Cached 24h. Skips private IPs | +| `HeaderConfigurationManager` | Livewire component for admin-panel header config editing | + +## Testing Support + +| Class | Purpose | +|-------|---------| +| `HeaderAssertions` | Test assertion helpers for security headers | +| `SecurityHeaderTester` | Pre-built test scenarios | + +## CSP Configuration + +Config in `config/headers.php` (published via Boot): + +```php +'csp' => [ + 'enabled' => env('SECURITY_CSP_ENABLED', true), + 'report_only' => env('SECURITY_CSP_REPORT_ONLY', false), + 'nonce_enabled' => env('SECURITY_CSP_NONCE_ENABLED', true), + 'directives' => [...], + 'environment' => [ + 'local' => ['script-src' => ["'unsafe-inline'", "'unsafe-eval'"]], + ], + 'nonce_skip_environments' => ['local', 'development'], + 'external' => [ + 'jsdelivr' => ['enabled' => env('SECURITY_CSP_JSDELIVR', false)], + 'google_analytics' => [...], + ], +], +``` + +## Blade Usage + +```blade + + + +``` + +## In-App Browser Detection + +Detects 14 platforms: Instagram, Facebook, TikTok, Twitter/X, LinkedIn, Snapchat, Pinterest, Reddit, Threads, WeChat, LINE, Telegram, Discord, WhatsApp, plus generic WebView fallback. + +Key distinction: `isStrictContentPlatform()` returns true for platforms that enforce content policies (useful for adult content warnings). + +## Integration + +- `SecurityHeaders` middleware is included in both `web` and `admin` middleware groups (configured in `Front/Web/Boot` and `Front/Admin/Boot`) +- Nonces auto-removed in `nonce_skip_environments` to not break HMR/dev tools +- HSTS only added in production +- Dev environments get WebSocket sources for Vite HMR (`localhost:8080`) diff --git a/src/Core/Helpers/CLAUDE.md b/src/Core/Helpers/CLAUDE.md new file mode 100644 index 0000000..0242619 --- /dev/null +++ b/src/Core/Helpers/CLAUDE.md @@ -0,0 +1,39 @@ +# Core\Helpers + +Shared utility classes registered as singletons. The `Boot` service provider also registers backward-compat aliases from the old `App\Support\*` namespace. + +## Key Classes + +| Class | Purpose | +|-------|---------| +| `RecoveryCode` | Generates 2FA recovery codes (`XXXXX-XXXXX` format) | +| `LoginRateLimiter` | Brute-force protection: 5 attempts / 60s per email+IP | +| `RateLimit` | Generic sliding-window rate limiter (cache-backed) | +| `PrivacyHelper` | GDPR IP anonymisation (truncation + daily-rotating SHA256 hashes) | +| `HadesEncrypt` | Hybrid AES-256-GCM + RSA encryption of exceptions for error pages | +| `File` | Base64-to-UploadedFile conversion, remote URL fetching, Content-Disposition parsing | +| `Cdn` | CDN asset URL generation with optional `cdn.{domain}` subdomain and file-hash cache busting | +| `UtmHelper` | UTM parameter extraction from Request, array, or URL string | +| `TimezoneList` | Timezone list generator with GMT offset formatting, grouped by continent | +| `HorizonStatus` | Laravel Horizon supervisor status check (inactive/paused/active) | +| `SystemLogs` | Reads `storage/logs/*.log` files, caps at 3 MB per file | +| `CommandResult` | Value object for remote command output (exitCode, output, error) | +| `ServiceCollection` | Typed collection of service provider classes with group/filter/metadata methods | +| `Log` | Social module logging facade, routes to configurable `social.log_channel` | + +## Sub-directory + +- `Rules/HexRule` -- Validation rule for hex colour codes (#fff or #ffffff). `forceFull` option rejects 3-digit codes. + +## Patterns + +- All classes are stateless or cache-backed -- no database models. +- `PrivacyHelper` has two anonymisation levels: standard (last octet) and strong (last 2 octets). IPv6 is fully handled. +- `HadesEncrypt` uses a hardcoded RSA public key (safe to commit) with env-var override. The HTML comment includes a whimsical "Hades" explanation for curious visitors. +- `Boot::registerBackwardCompatAliases()` uses `class_alias` to map old `App\Support\*` names -- remove these once the migration is complete. + +## Integration + +- `PrivacyHelper` is used by Analytics Center and BioHost for consistent IP handling. +- `RateLimit` cache keys are prefixed `social:` -- consider making this configurable. +- `Cdn` reads from `config('cdn.enabled')` and `config('core.cdn.subdomain')`. diff --git a/src/Core/Input/CLAUDE.md b/src/Core/Input/CLAUDE.md new file mode 100644 index 0000000..46e09d2 --- /dev/null +++ b/src/Core/Input/CLAUDE.md @@ -0,0 +1,51 @@ +# Core\Input + +Pre-boot input sanitisation. Strips dangerous control characters from `$_GET` and `$_POST` before Laravel even creates the Request object. + +## Key Classes + +| Class | Purpose | +|-------|---------| +| `Input` | Static `capture()` method -- sanitises superglobals then delegates to `Request::capture()` | +| `Sanitiser` | Configurable filter pipeline: Unicode NFC normalisation, control char stripping, HTML filtering, presets, max length, transformation hooks | + +## Sanitiser Pipeline + +Execution order per string value: + +1. Before hooks (global, then field-specific) +2. Unicode NFC normalisation (via `intl` extension) +3. Control character stripping (`FILTER_UNSAFE_RAW` + `FILTER_FLAG_STRIP_LOW`) +4. HTML tag filtering (strip_tags with allowed tags) +5. Preset application (email, url, phone, alpha, alphanumeric, numeric, slug) +6. Additional schema-defined `filter_var` filters +7. Max length enforcement (`mb_substr`) +8. After hooks (global, then field-specific) +9. Audit logging (if enabled and value changed) + +## Public API + +```php +// Immutable builder pattern (returns cloned instance) +$s = (new Sanitiser) + ->richText() // allow safe HTML tags + ->maxLength(1000) // truncate to 1000 chars + ->email('email_field') // apply email preset to specific field + ->slug('url_slug') // apply slug preset + ->beforeFilter(fn($v, $f) => trim($v)) + ->transformField('username', fn($v) => strtolower($v)); + +$clean = $s->filter(['email_field' => $raw, 'url_slug' => $raw2]); +``` + +## Conventions + +- **Sanitiser sanitises, Laravel validates.** This is explicitly called out in the class docblock. +- Immutable: all `with*` / fluent methods return `clone $this`. +- Presets are static and extensible via `Sanitiser::registerPreset()`. +- The `*` wildcard key in schema applies to all fields as a default. +- Field-specific schema merges over global (`*`) schema. + +## Tests + +Pest tests at `Tests/Unit/InputFilteringTest.php` cover: clean passthrough, control char stripping, full Unicode preservation (CJK, Arabic, Russian, emojis), and edge cases. diff --git a/src/Core/Lang/CLAUDE.md b/src/Core/Lang/CLAUDE.md new file mode 100644 index 0000000..2997e7f --- /dev/null +++ b/src/Core/Lang/CLAUDE.md @@ -0,0 +1,55 @@ +# Core\Lang + +Internationalisation subsystem with ICU message formatting, translation memory with fuzzy matching, TMX import/export, and translation coverage analysis. + +## Key Classes + +| Class | Purpose | +|-------|---------| +| `LangServiceProvider` | Auto-discovered provider: registers ICU formatter, TM, coverage, fallback chain, missing key validation | +| `IcuMessageFormatter` | ICU MessageFormat support (plurals, select, number/date formatting) with intl fallback | +| `Boot` | Empty marker class -- all work done in LangServiceProvider | + +### TranslationMemory/ + +| Class | Purpose | +|-------|---------| +| `TranslationMemory` | Facade service: store, get, suggest (fuzzy), import/export TMX | +| `FuzzyMatcher` | Multi-algorithm similarity: Levenshtein + token (Jaccard) + n-gram (Dice coefficient), configurable weights | +| `TranslationMemoryEntry` | Immutable value object with quality scores (0.0-1.0), usage counts, metadata | +| `JsonTranslationMemoryRepository` | File-backed repo: JSON files per locale pair, in-memory cache + dirty tracking | +| `TranslationMemoryRepository` (contract) | Interface for storage backends (JSON, database) | +| `TmxImporter` / `TmxExporter` | TMX 1.4b format import/export with locale normalisation and metadata preservation | + +### Coverage/ + +| Class | Purpose | +|-------|---------| +| `TranslationCoverage` | Scans PHP/Blade/JS/Vue for translation keys, compares against lang files | +| `TranslationCoverageReport` | Report object with missing/unused keys, per-locale stats, text/JSON output | + +### Console Commands + +- `lang:coverage` -- Find missing and unused translation keys +- `lang:tm` -- Translation memory management (not fully read but registered) + +## Architecture + +- **Fallback chain**: `en_GB` -> `en` -> configured fallback. Built via `determineLocalesUsing()`. +- **Missing key validation**: Only in local/dev/testing. Logs at configurable level, triggers `trigger_deprecation` in local. +- **ICU formatter**: Caches compiled `MessageFormatter` instances (max 100, LRU eviction). Falls back to simple `{name}` placeholder replacement when intl is unavailable. +- **Fuzzy matching**: Combined algorithm weights: Levenshtein 0.25, token 0.50, n-gram 0.25. Confidence = similarity * 0.7 + quality * 0.3. +- **Translation Memory IDs**: Generated via `xxh128` hash of `sourceLocale:targetLocale:source`. + +## Configuration + +All under `config('core.lang.*')`: +- `fallback_chain`, `validate_keys`, `log_missing_keys`, `missing_key_log_level` +- `icu_enabled` +- `translation_memory.enabled`, `translation_memory.driver` (json/database), `translation_memory.fuzzy.*` + +## Integration + +- Translations loaded under the `core` namespace: `__('core::core.brand.name')` +- Override by publishing to `resources/lang/vendor/core/` +- Translation files live in `en_GB/core.php` within this directory diff --git a/src/Core/Mail/CLAUDE.md b/src/Core/Mail/CLAUDE.md new file mode 100644 index 0000000..50c4370 --- /dev/null +++ b/src/Core/Mail/CLAUDE.md @@ -0,0 +1,49 @@ +# Core\Mail + +Email validation and disposable domain blocking service with statistics tracking. + +## Key Classes + +| Class | Purpose | +|-------|---------| +| `EmailShield` | Main service: validate emails, block disposable domains, MX lookup, normalisation, async validation | +| `EmailShieldStat` | Eloquent model for daily validation stats (valid/invalid/disposable counts) | +| `EmailValidationResult` | Value object with named constructors: `valid()`, `invalid()`, `disposable()` | +| `Rules\ValidatedEmail` | Laravel validation rule wrapping EmailShield | +| `Boot` | Service provider: registers EmailShield singleton + backward-compat aliases | + +## EmailShield Features + +- **Disposable domain blocking**: 100k+ domains from community-maintained GitHub list, cached 24h, stored at `storage/app/email-shield/disposable-domains.txt` +- **MX record validation**: Cached 1h per domain, suppresses DNS warnings +- **Validation caching**: Results cached 5 min to avoid repeated checks +- **Email normalisation**: Gmail dot-stripping, plus-addressing removal, googlemail.com -> gmail.com +- **Async validation**: Immediate format+disposable check, queues MX lookup for background processing +- **Statistics**: Atomic daily counters via `insertOrIgnore` + `increment` + +## Public API + +```php +$shield = app(EmailShield::class); + +// Validate +$result = $shield->validate('user@example.com'); // EmailValidationResult +$result->passes(); // bool +$result->isDisposable; // bool + +// Normalise +$shield->normalize('J.O.H.N+spam@gmail.com'); // 'john@gmail.com' +$shield->isSameMailbox('john@gmail.com', 'j.o.h.n@googlemail.com'); // true + +// Update blocklist +$shield->updateDisposableDomainsList(); + +// In validation rules +'email' => ['required', new ValidatedEmail(blockDisposable: true)] +``` + +## Conventions + +- Disposable domain list update validates minimum 100 domains to prevent corrupted lists. +- `EmailShieldStat::pruneOldRecords(90)` for cleanup (called by Console PruneEmailShieldStatsCommand). +- Backward-compat aliases map `App\Services\Email\*` to `Core\Mail\*`. diff --git a/src/Core/Media/CLAUDE.md b/src/Core/Media/CLAUDE.md new file mode 100644 index 0000000..caecb3a --- /dev/null +++ b/src/Core/Media/CLAUDE.md @@ -0,0 +1,39 @@ +# Core\Media + +Image processing, media conversion pipeline, and on-demand thumbnail generation. + +## Directory Structure + +``` +Media/ + Abstracts/ MediaConversion base class, Image abstract + Conversions/ MediaImageResizerConversion, MediaVideoThumbConversion + Events/ ConversionProgress event + Image/ ImageOptimization, ImageOptimizer, ExifStripper, ModernFormatSupport, OptimizationResult + Jobs/ GenerateThumbnail, ProcessMediaConversion + Routes/ web.php (thumbnail routes) + Support/ ConversionProgressReporter, TemporaryFile, TemporaryDirectory, ImageResizer, MediaConversionData + Thumbnail/ ThumbnailController, LazyThumbnail, helpers.php + Boot.php Service provider + config.php Media configuration +``` + +## Key Concepts + +- **Media Conversions**: Abstract pipeline (`MediaConversion`) for processing uploaded media. Concrete implementations handle image resizing and video thumbnail extraction. +- **Thumbnail System**: On-demand thumbnail generation via `ThumbnailController` with `LazyThumbnail` for deferred processing. Routes registered in `Routes/web.php`. +- **Image Optimization**: `ImageOptimizer` handles format conversion, EXIF stripping, and compression. `ModernFormatSupport` detects WebP/AVIF browser support. +- **Progress Reporting**: `ConversionProgressReporter` + `ConversionProgress` event for tracking long-running media conversions. +- **Temporary Files**: `TemporaryFile` and `TemporaryDirectory` helpers for safe cleanup of processing artifacts. + +## Jobs + +- `GenerateThumbnail` -- Queued job for thumbnail generation +- `ProcessMediaConversion` -- Queued job for media conversion pipeline + +## Integration + +- Boot provider registers config from `config.php` +- Thumbnail routes are web routes (not API) +- Conversion data flows through `MediaConversionData` DTO +- `ImageResizer` in Support handles the actual resize operations diff --git a/src/Core/Rules/CLAUDE.md b/src/Core/Rules/CLAUDE.md new file mode 100644 index 0000000..5a5099b --- /dev/null +++ b/src/Core/Rules/CLAUDE.md @@ -0,0 +1,51 @@ +# Core\Rules + +Security-focused Laravel validation rules. No service provider -- use directly in validation arrays. + +## Rules + +### SafeWebhookUrl + +SSRF protection for webhook delivery URLs. + +**Blocks:** +- Localhost and loopback (127.0.0.0/8, ::1) +- Private networks (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) +- Link-local, reserved ranges, special-use addresses +- Local domain names (.local, .localhost, .internal) +- Decimal IP encoding (2130706433 = 127.0.0.1) +- IPv4-mapped IPv6 (::ffff:127.0.0.1) +- Non-HTTPS schemes + +**Service mode:** Optionally restrict to known webhook domains (Discord, Slack, Telegram). Known service domains skip SSRF checks. + +```php +'url' => [new SafeWebhookUrl] // any HTTPS, no SSRF +'url' => [new SafeWebhookUrl('discord')] // discord.com/discordapp.com only +``` + +### SafeJsonPayload + +Protects against malicious JSON payloads stored in the database. + +**Validates:** +- Maximum total size (default 10 KB) +- Maximum nesting depth (default 3) +- Maximum total keys across all levels (default 50) +- Maximum string value length (default 1000 chars) + +**Factory methods:** +- `SafeJsonPayload::default()` -- 10 KB, depth 3, 50 keys +- `SafeJsonPayload::small()` -- 2 KB, depth 2, 20 keys +- `SafeJsonPayload::large()` -- 100 KB, depth 5, 200 keys +- `SafeJsonPayload::metadata()` -- 5 KB, depth 2, 30 keys, 256 char strings + +```php +'payload' => ['array', SafeJsonPayload::metadata()] +``` + +## Conventions + +- Both rules implement `Illuminate\Contracts\Validation\ValidationRule`. +- `SafeWebhookUrl` resolves hostnames and checks ALL returned IPs against blocklists. +- These are standalone -- no Boot provider, no config. Import and use directly. diff --git a/src/Core/Search/CLAUDE.md b/src/Core/Search/CLAUDE.md new file mode 100644 index 0000000..9525e99 --- /dev/null +++ b/src/Core/Search/CLAUDE.md @@ -0,0 +1,62 @@ +# Core\Search + +Unified search across system components with analytics tracking, autocomplete suggestions, and result highlighting. + +## Key Classes + +| Class | Purpose | +|-------|---------| +| `Unified` | Main search service: searches MCP tools/resources, API endpoints, patterns, assets, todos, agent plans | +| `Analytics\SearchAnalytics` | Tracks queries, clicks, zero-result queries, trends. Privacy-aware (daily-rotating IP hashes, sensitive pattern exclusion) | +| `Suggestions\SearchSuggestions` | Autocomplete from popular queries, recent searches, and content. Logarithmic popularity scoring | +| `Support\SearchHighlighter` | Highlights matched terms in results with configurable HTML wrapper, snippet extraction with context | +| `Boot` | Service provider: merges config, registers singletons, loads migrations | + +## Search Sources (Unified) + +| Type | Source | Model/Data | +|------|--------|------------| +| `mcp_tool` | YAML files | `resource_path('mcp/servers/*.yaml')` | +| `mcp_resource` | YAML files | Same | +| `api_endpoint` | Config | `config('core.search.api_endpoints')` | +| `pattern` | Database | `Core\Mod\Uptelligence\Models\Pattern` | +| `asset` | Database | `Core\Mod\Uptelligence\Models\Asset` | +| `todo` | Database | `Core\Mod\Uptelligence\Models\UpstreamTodo` | +| `plan` | Database | `Core\Mod\Agentic\Models\AgentPlan` | + +## Scoring Algorithm + +Configurable weights via `config/search.php`: +- Exact match: 20 (30 if field equals query exactly) +- Starts-with: 15 +- Word match: 5 (7.5 for exact word) +- Position factor: earlier fields in the array score higher +- Fuzzy: Levenshtein distance, 0.5x score multiplier, min 4 char query + +## Analytics + +- Queries stored in `search_analytics` table with `query_hash` (xxh3) for grouping +- Click tracking in `search_analytics_clicks` +- Sensitive patterns excluded: password, secret, token, key, credit, ssn +- IP hashed with daily-rotating salt +- `prune()` respects `retention_days` config (default 90) + +## Suggestions + +Three sources (configurable priority): `popular`, `recent`, `content` +- Popular: From analytics, log-scale scoring, queries with results only +- Recent: Per-user cache (30 days), session fallback for guests +- Content: Prefix matching on model names +- Trending: Growth comparison between recent and earlier period + +## Configuration + +All in `config/search.php` (publishable): +- `scoring.*` -- match weights +- `fuzzy.*` -- Levenshtein settings +- `suggestions.*` -- autocomplete settings +- `analytics.*` -- tracking and retention + +## LIKE injection protection + +`escapeLikeQuery()` escapes `%` and `_` wildcards. If more than 3 wildcards, strips them entirely to prevent DoS. diff --git a/src/Core/Seo/CLAUDE.md b/src/Core/Seo/CLAUDE.md new file mode 100644 index 0000000..4df8c86 --- /dev/null +++ b/src/Core/Seo/CLAUDE.md @@ -0,0 +1,73 @@ +# Core\Seo + +SEO metadata management, JSON-LD schema generation, structured data validation, OG image handling, and score trend tracking. + +## Key Classes + +| Class | Purpose | +|-------|---------| +| `Schema` | High-level JSON-LD generator: auto-detects Article, HowTo, FAQ, Breadcrumb from content | +| `SeoMetadata` | Eloquent model (polymorphic): title, description, canonical, OG, Twitter, schema markup, score, issues | +| `HasSeoMetadata` | Trait for models: `seoMetadata()` morphOne, `updateSeo()`, `getSeoHeadTagsAttribute()` | +| `Boot` | Service provider: registers singletons + artisan commands | + +### Services/ + +| Class | Purpose | +|-------|---------| +| `SchemaBuilderService` | Lower-level schema building blocks | +| `ServiceOgImageService` | OG image generation for service pages | + +### Validation/ + +| Class | Purpose | +|-------|---------| +| `SchemaValidator` | Validates schema against schema.org specifications | +| `StructuredDataTester` | Tests structured data, checks rich results eligibility, generates reports | +| `CanonicalUrlValidator` | Validates URL format and detects conflicts between records | +| `OgImageValidator` | Validates OG image dimensions and requirements | + +### Analytics/ + +| Class | Purpose | +|-------|---------| +| `SeoScoreTrend` | Daily/weekly score trend tracking | +| `Models\SeoScoreHistory` | Historical score records | + +## Schema Generation + +`Schema::generateSchema($item)` builds a `@graph` containing: +1. Organisation schema (always, from config) +2. Article schema (TechArticle by default) +3. Breadcrumb schema +4. HowTo schema (if content has numbered steps) +5. FAQ schema (if content has FAQ section with Q&A pairs) + +Content detection uses regex on `display_content`. Steps extracted from JSON blocks or numbered lists. FAQs from `## FAQ` sections. + +## SeoMetadata Model + +- **Lazy schema_markup**: Custom accessor/mutator defers JSON parsing until accessed +- **Meta tag generation**: `meta_tags` attribute produces complete HTML ``, `<meta>`, `<link rel="canonical">`, OG, Twitter tags +- **JSON-LD output**: `json_ld` attribute wraps schema in `<script type="application/ld+json">` with XSS-safe `JSON_HEX_TAG` +- **Score tracking**: `recordScore()`, `getScoreHistory()`, `getDailyScoreTrend()`, `hasScoreImproved()` +- **Validation**: `validateOgImage()`, `validateCanonicalUrl()`, `checkCanonicalConflict()`, `validateStructuredData()`, `getRichResultsEligibility()` + +## Console Commands + +- `seo:record-scores` -- Record SEO scores for trend tracking +- `seo:test-structured-data` -- Test structured data against schema.org +- `seo:audit-canonical` -- Audit canonical URLs for conflicts +- `seo:generate-og-images` -- Generate OG images for services + +## Configuration + +Under `config/seo.php`: +- `trends.enabled`, `trends.retention_days`, `trends.record_on_save`, `trends.min_interval_hours` +- `structured_data.external_validation`, `structured_data.google_api_key`, `structured_data.cache_validation` + +## Integration + +- Use `HasSeoMetadata` trait on any Eloquent model to add polymorphic SEO data +- `Schema` reads organisation config from `core.organisation.*` and `core.social.*` +- Uses UK English in code: `colour`, `organisation` diff --git a/src/Core/Storage/CLAUDE.md b/src/Core/Storage/CLAUDE.md new file mode 100644 index 0000000..4d28042 --- /dev/null +++ b/src/Core/Storage/CLAUDE.md @@ -0,0 +1,53 @@ +# Core\Storage + +Cache resilience infrastructure: tiered caching, Redis circuit breaker, and cache warming. + +## Key Classes + +| Class | Purpose | +|-------|---------| +| `TieredCacheStore` | Multi-tier cascading cache: Memory -> Redis -> Database. Reads check fastest first, promotes hits upward. Writes go to all tiers with tier-specific TTLs | +| `CircuitBreaker` | Prevents cascading failures: Closed (normal) -> Open (skip Redis) -> Half-Open (test recovery). Configurable failure threshold, recovery timeout, success threshold | +| `ResilientRedisStore` | Redis wrapper that falls back gracefully on connection failure | +| `CacheResilienceProvider` | Wires up the resilience stack | +| `TierConfiguration` | Configuration DTO for cache tiers (name, driver, TTL) | +| `CacheWarmer` | Pre-populates cache with frequently accessed data | +| `StorageMetrics` | Tracks cache hits/misses per tier | + +### Commands/ + +- `WarmCacheCommand` -- Artisan command to warm the cache + +### Events/ + +- `RedisFallbackActivated` -- Dispatched when Redis fails and fallback is activated + +## Tiered Cache Architecture + +``` +get("key") + | + v +[Memory/Array] -- miss --> [Redis] -- miss --> [Database] + 60s TTL 1h TTL 24h TTL +``` + +- On hit at a lower tier, value is promoted to all faster tiers +- Writes propagate to all enabled tiers with per-tier TTLs +- Configuration via `config('core.storage.tiered_cache.tiers')` + +## Circuit Breaker States + +| State | Behaviour | +|-------|-----------| +| Closed | Normal: requests go to Redis | +| Open | Redis failing: requests go directly to fallback, skip Redis | +| Half-Open | Testing: allows limited requests to check if Redis recovered | + +Defaults: 5 failures to open, 30s recovery timeout, 2 successes to close. + +## Integration + +- `RedisFallbackActivated` event allows monitoring/alerting when Redis goes down +- `StorageMetrics` integrates with the tiered cache for hit/miss tracking +- No Boot.php in this directory -- the resilience stack is wired via `CacheResilienceProvider` diff --git a/src/Core/Webhook/CLAUDE.md b/src/Core/Webhook/CLAUDE.md new file mode 100644 index 0000000..e712775 --- /dev/null +++ b/src/Core/Webhook/CLAUDE.md @@ -0,0 +1,54 @@ +# Core\Webhook + +Inbound webhook receiving, signature verification, and event dispatch. + +## Key Classes + +| Class | Purpose | +|-------|---------| +| `Boot` | Service provider: merges config, registers `POST /webhooks/{source}` API route via `ApiRoutesRegistering` event | +| `WebhookController` | Receives webhooks: verifies signature (if verifier bound), stores call, dispatches `WebhookReceived` event | +| `WebhookCall` | Eloquent model (ULID primary key): source, event_type, headers, payload, signature_valid, processed_at | +| `WebhookReceived` | Event dispatched after a webhook is stored | +| `WebhookVerifier` | Interface: `verify(Request $request, string $secret): bool` | +| `CronTrigger` | Cron-based webhook triggering | + +## Request Flow + +``` +POST /api/webhooks/{source} + | + v +WebhookController::handle() + |-> Look up verifier: app("webhook.verifier.{$source}") + |-> Verify signature against config("webhook.secrets.{$source}") + |-> Extract event type from payload (type/event_type/event) + |-> Create WebhookCall record + |-> Dispatch WebhookReceived event + |-> Return {"ok": true} +``` + +## Adding a New Webhook Source + +1. Implement `WebhookVerifier` for your source +2. Bind it: `$this->app->bind('webhook.verifier.stripe', StripeWebhookVerifier::class)` +3. Set secret in config: `webhook.secrets.stripe` +4. Listen for `WebhookReceived` event and filter by source + +## WebhookCall Model + +- Uses ULIDs (not UUIDs) for sortable, unique identifiers +- `$timestamps = false` -- has `created_at` cast but no `updated_at` +- Scopes: `unprocessed()`, `forSource($source)` +- `markProcessed()` sets `processed_at` to now + +## Configuration + +`config/webhook.php` (merged from `config.php` in this directory): +- `secrets.*` -- per-source signing secrets + +## Integration + +- Route registered via `ApiRoutesRegistering` lifecycle event (event-driven module loading pattern) +- Source parameter constrained to `[a-z0-9\-]+` +- Pair with `Core\Rules\SafeWebhookUrl` for outbound webhook URL validation