command: sed -n '1,360p' routes/api.php && sed -n '1,300p' app/Http/Middleware/ResolveStore.php && sed -n '1,220p' app/Http/Middleware/EnsureApiAbility.php && sed -n '1,220p' config/auth.php

api.php

type: read

cmd: sed -n '1,360p' routes/api.php

auth.php

cmd: sed -n '1,220p' config/auth.php

stdout:
<?php

use App\Http\Controllers\Api\AdminController;
use App\Http\Controllers\Api\StorefrontAnalyticsController;
use App\Http\Controllers\Api\StorefrontCartController;
use App\Http\Controllers\Api\StorefrontCheckoutController;
use App\Http\Controllers\Api\StorefrontOrderController;
use App\Http\Controllers\Api\StorefrontSearchController;
use Illuminate\Session\Middleware\StartSession;
use Illuminate\Support\Facades\Route;

Route::prefix('storefront/v1')->middleware([StartSession::class, 'store.resolve', 'throttle:api.storefront'])->group(function (): void {
    Route::post('carts', [StorefrontCartController::class, 'store']);
    Route::get('carts/{cartId}', [StorefrontCartController::class, 'show']);
    Route::post('carts/{cartId}/lines', [StorefrontCartController::class, 'addLine']);
    Route::put('carts/{cartId}/lines/{lineId}', [StorefrontCartController::class, 'updateLine']);
    Route::delete('carts/{cartId}/lines/{lineId}', [StorefrontCartController::class, 'removeLine']);
    Route::post('checkouts', [StorefrontCheckoutController::class, 'store'])->middleware('throttle:checkout');
    Route::get('checkouts/{checkoutId}', [StorefrontCheckoutController::class, 'show'])->middleware('throttle:checkout');
    Route::put('checkouts/{checkoutId}/address', [StorefrontCheckoutController::class, 'address'])->middleware('throttle:checkout');
    Route::put('checkouts/{checkoutId}/shipping-method', [StorefrontCheckoutController::class, 'shippingMethod'])->middleware('throttle:checkout');
    Route::put('checkouts/{checkoutId}/payment-method', [StorefrontCheckoutController::class, 'paymentMethod'])->middleware('throttle:checkout');
    Route::post('checkouts/{checkoutId}/apply-discount', [StorefrontCheckoutController::class, 'applyDiscount'])->middleware('throttle:checkout');
    Route::delete('checkouts/{checkoutId}/discount', [StorefrontCheckoutController::class, 'removeDiscount'])->middleware('throttle:checkout');
    Route::post('checkouts/{checkoutId}/pay', [StorefrontCheckoutController::class, 'pay'])->middleware('throttle:checkout');
    Route::get('orders/{orderNumber}', [StorefrontOrderController::class, 'show']);
    Route::get('search', [StorefrontSearchController::class, 'index'])->middleware('throttle:search');
    Route::get('search/suggest', [StorefrontSearchController::class, 'suggest'])->middleware('throttle:search');
    Route::post('analytics/events', [StorefrontAnalyticsController::class, 'store'])->middleware('throttle:analytics');
});

Route::prefix('admin/v1/stores/{storeId}')->middleware(['auth:sanctum', 'store.resolve', 'role.check:owner,admin,staff,support', 'api.ability', 'throttle:api.admin'])->group(function (): void {
    Route::get('products', [AdminController::class, 'products']);
    Route::post('products', [AdminController::class, 'storeProduct'])->middleware('role.check:owner,admin,staff');
    Route::get('products/{productId}', [AdminController::class, 'showProduct']);
    Route::put('products/{productId}', [AdminController::class, 'updateProduct'])->middleware('role.check:owner,admin,staff');
    Route::delete('products/{productId}', [AdminController::class, 'deleteProduct'])->middleware('role.check:owner,admin,staff');
    Route::get('collections', [AdminController::class, 'collections']);
    Route::post('collections', [AdminController::class, 'storeCollection'])->middleware('role.check:owner,admin,staff');
    Route::put('collections/{collectionId}', [AdminController::class, 'updateCollection'])->middleware('role.check:owner,admin,staff');
    Route::delete('collections/{collectionId}', [AdminController::class, 'deleteCollection'])->middleware('role.check:owner,admin,staff');
    Route::get('orders', [AdminController::class, 'orders']);
    Route::get('orders/{orderId}', [AdminController::class, 'showOrder']);
    Route::get('customers', [AdminController::class, 'customers']);
    Route::get('discounts', [AdminController::class, 'discounts']);
    Route::post('discounts', [AdminController::class, 'storeDiscount'])->middleware('role.check:owner,admin,staff');
    Route::put('discounts/{discountId}', [AdminController::class, 'updateDiscount'])->middleware('role.check:owner,admin,staff');
    Route::delete('discounts/{discountId}', [AdminController::class, 'deleteDiscount'])->middleware('role.check:owner,admin');
    Route::get('shipping/zones', [AdminController::class, 'shippingZones']);
    Route::post('shipping/zones', [AdminController::class, 'storeShippingZone'])->middleware('role.check:owner,admin');
    Route::put('shipping/zones/{zoneId}', [AdminController::class, 'updateShippingZone'])->middleware('role.check:owner,admin');
    Route::post('shipping/zones/{zoneId}/rates', [AdminController::class, 'storeShippingRate'])->middleware('role.check:owner,admin');
    Route::get('tax/settings', [AdminController::class, 'taxSettings']);
    Route::put('tax/settings', [AdminController::class, 'updateTaxSettings'])->middleware('role.check:owner,admin');
    Route::get('pages', [AdminController::class, 'pages']);
    Route::post('pages', [AdminController::class, 'storePage'])->middleware('role.check:owner,admin,staff');
    Route::put('pages/{pageId}', [AdminController::class, 'updatePage'])->middleware('role.check:owner,admin,staff');
    Route::delete('pages/{pageId}', [AdminController::class, 'deletePage'])->middleware('role.check:owner,admin');
    Route::post('themes', [AdminController::class, 'storeTheme'])->middleware('role.check:owner,admin');
    Route::post('themes/{themeId}/publish', [AdminController::class, 'publishTheme'])->middleware('role.check:owner,admin');
    Route::put('themes/{themeId}/settings', [AdminController::class, 'updateThemeSettings'])->middleware('role.check:owner,admin');
    Route::post('search/reindex', [AdminController::class, 'reindex'])->middleware('role.check:owner,admin,staff');
    Route::get('search/status', [AdminController::class, 'searchStatus']);
    Route::get('analytics/summary', [AdminController::class, 'analyticsSummary']);
    Route::post('orders/{orderId}/fulfillments', [AdminController::class, 'fulfillOrder'])->middleware('role.check:owner,admin,staff');
    Route::post('orders/{orderId}/refunds', [AdminController::class, 'refundOrder'])->middleware('role.check:owner,admin');
});
<?php

namespace App\Http\Middleware;

use App\Enums\StoreDomainType;
use App\Enums\StoreStatus;
use App\Models\Store;
use App\Models\StoreDomain;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\View;
use Illuminate\Support\Str;
use Symfony\Component\HttpFoundation\Response;

class ResolveStore
{
    public function handle(Request $request, Closure $next, string $context = 'storefront'): Response
    {
        if (! Schema::hasTable('stores')) {
            return $next($request);
        }

        if ($this->isPublicAdminAuthRequest($request)) {
            return $next($request);
        }

        if ($this->isAdminApiRequest($request) && $request->user('sanctum') === null) {
            return $next($request);
        }

        $context = $context === 'storefront' && $this->isAdminRequest($request)
            ? 'admin'
            : $context;

        $store = $context === 'admin'
            ? $this->resolveAdminStore($request)
            : $this->resolveStorefrontStore($request);

        if ($store === null && $context === 'storefront' && $this->isPublicCustomerAuthRequest($request) && Store::query()->doesntExist()) {
            return $next($request);
        }

        abort_unless($store instanceof Store, $context === 'admin' ? 403 : 404);

        if ($store->status === StoreStatus::Suspended) {
            if ($context === 'storefront') {
                abort(503, 'This store is currently unavailable.');
            }

            if (! in_array($request->method(), ['GET', 'HEAD', 'OPTIONS'], true)) {
                abort(403);
            }
        }

        $binding = (string) config('tenancy.binding', 'current_store');
        app()->instance($binding, $store);
        View::share((string) config('tenancy.view_share', 'currentStore'), $store);

        return $next($request);
    }

    private function resolveStorefrontStore(Request $request): ?Store
    {
        $hostname = Str::lower(rtrim(trim($request->getHost()), '.'));
        $cacheKey = config('tenancy.cache_prefix', 'store-domains').':'.$hostname;

        $storeId = Cache::remember(
            $cacheKey,
            (int) config('tenancy.store_cache_ttl', 300),
            fn (): ?int => StoreDomain::query()
                ->where('hostname', $hostname)
                ->where('type', config('tenancy.storefront_domain_type', StoreDomainType::Storefront->value))
                ->value('store_id'),
        );

        return $storeId === null ? null : Store::query()->find($storeId);
    }

    private function resolveAdminStore(Request $request): ?Store
    {
        $isApiRequest = $this->isAdminApiRequest($request);
        $storeId = $isApiRequest
            ? $request->route('storeId')
            : $request->session()->get(config('tenancy.admin_session_key', 'current_store_id'));
        $user = $isApiRequest
            ? $request->user('sanctum')
            : ($request->user('web') ?? $request->user());

        if ($storeId === null || $user === null) {
            return null;
        }

        return $user->stores()->whereKey($storeId)->first();
    }

    private function isAdminRequest(Request $request): bool
    {
        if ($this->isAdminApiRequest($request)) {
            return true;
        }

        $prefix = trim((string) config('tenancy.admin_path_prefix', 'admin'), '/');

        if ($request->is($prefix, $prefix.'/*') || $request->routeIs($prefix.'.*')) {
            return true;
        }

        return $request->is('livewire/update') && str_contains((string) $request->headers->get('referer'), '/admin');
    }

    private function isAdminApiRequest(Request $request): bool
    {
        $prefix = trim((string) config('tenancy.admin_path_prefix', 'admin'), '/');

        return $request->is('api/'.$prefix, 'api/'.$prefix.'/*');
    }

    private function isPublicCustomerAuthRequest(Request $request): bool
    {
        return $request->is('forgot-password', 'reset-password/*');
    }

    private function isPublicAdminAuthRequest(Request $request): bool
    {
        return $request->is('admin/login', 'admin/forgot-password', 'admin/reset-password/*');
    }
}
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Symfony\Component\HttpFoundation\Response;

class EnsureApiAbility
{
    public function handle(Request $request, Closure $next): Response
    {
        $user = $request->user('sanctum');

        abort_unless($request->bearerToken() !== null && $user !== null, 401, 'A Sanctum bearer token is required.');

        $ability = $this->abilityFor($request);

        abort_unless($ability !== null && $user->tokenCan($ability), 403, 'This token does not have the required ability.');

        return $next($request);
    }

    private function abilityFor(Request $request): ?string
    {
        $path = $request->path();
        $method = $request->method();

        foreach (['products' => 'products', 'collections' => 'collections', 'orders' => 'orders', 'customers' => 'customers', 'discounts' => 'discounts'] as $segment => $resource) {
            if (Str::contains($path, '/'.$segment)) {
                return in_array($method, ['GET', 'HEAD'], true) ? 'read-'.$resource : 'write-'.$resource;
            }
        }

        if (Str::contains($path, '/themes')) {
            return in_array($method, ['GET', 'HEAD'], true) ? 'read-themes' : 'write-themes';
        }

        if (Str::contains($path, '/pages')) {
            return in_array($method, ['GET', 'HEAD'], true) ? 'read-content' : 'write-content';
        }

        if (Str::contains($path, '/shipping') || Str::contains($path, '/tax/')) {
            return in_array($method, ['GET', 'HEAD'], true) ? 'read-settings' : 'write-settings';
        }

        if (Str::contains($path, '/analytics')) {
            return 'read-analytics';
        }

        if (Str::contains($path, '/search')) {
            return in_array($method, ['GET', 'HEAD'], true) ? 'read-products' : 'write-products';
        }

        return null;
    }
}
<?php

return [

    /*
    |--------------------------------------------------------------------------
    | Authentication Defaults
    |--------------------------------------------------------------------------
    |
    | This option defines the default authentication "guard" and password
    | reset "broker" for your application. You may change these values
    | as required, but they're a perfect start for most applications.
    |
    */

    'defaults' => [
        'guard' => env('AUTH_GUARD', 'web'),
        'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
    ],

    /*
    |--------------------------------------------------------------------------
    | Authentication Guards
    |--------------------------------------------------------------------------
    |
    | Next, you may define every authentication guard for your application.
    | Of course, a great default configuration has been defined for you
    | which utilizes session storage plus the Eloquent user provider.
    |
    | All authentication guards have a user provider, which defines how the
    | users are actually retrieved out of your database or other storage
    | system used by the application. Typically, Eloquent is utilized.
    |
    | Supported: "session"
    |
    */

    'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],
        'customer' => [
            'driver' => 'session',
            'provider' => 'customers',
        ],
        'sanctum' => [
            'driver' => 'sanctum',
            'provider' => 'users',
        ],
    ],

    /*
    |--------------------------------------------------------------------------
    | User Providers
    |--------------------------------------------------------------------------
    |
    | All authentication guards have a user provider, which defines how the
    | users are actually retrieved out of your database or other storage
    | system used by the application. Typically, Eloquent is utilized.
    |
    | If you have multiple user tables or models you may configure multiple
    | providers to represent the model / table. These providers may then
    | be assigned to any extra authentication guards you have defined.
    |
    | Supported: "database", "eloquent"
    |
    */

    'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => env('AUTH_MODEL', App\Models\User::class),
        ],
        'customers' => [
            'driver' => 'customer',
            'model' => App\Models\Customer::class,
        ],

        // 'users' => [
        //     'driver' => 'database',
        //     'table' => 'users',
        // ],
    ],

    /*
    |--------------------------------------------------------------------------
    | Resetting Passwords
    |--------------------------------------------------------------------------
    |
    | These configuration options specify the behavior of Laravel's password
    | reset functionality, including the table utilized for token storage
    | and the user provider that is invoked to actually retrieve users.
    |
    | The expiry time is the number of minutes that each reset token will be
    | considered valid. This security feature keeps tokens short-lived so
    | they have less time to be guessed. You may change this as needed.
    |
    | The throttle setting is the number of seconds a user must wait before
    | generating more password reset tokens. This prevents the user from
    | quickly generating a very large amount of password reset tokens.
    |
    */

    'passwords' => [
        'users' => [
            'provider' => 'users',
            'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
            'expire' => 60,
            'throttle' => 60,
        ],
        'customers' => [
            'provider' => 'customers',
            'table' => 'customer_password_reset_tokens',
            'expire' => 60,
            'throttle' => 60,
        ],
    ],

    /*
    |--------------------------------------------------------------------------
    | Password Confirmation Timeout
    |--------------------------------------------------------------------------
    |
    | Here you may define the number of seconds before a password confirmation
    | window expires and users are asked to re-enter their password via the
    | confirmation screen. By default, the timeout lasts for three hours.
    |
    */

    'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),

];


text:
Script completed
Wall time 0.1 seconds
Output:

text:
<?php

use App\Http\Controllers\Api\AdminController;
use App\Http\Controllers\Api\StorefrontAnalyticsController;
use App\Http\Controllers\Api\StorefrontCartController;
use App\Http\Controllers\Api\StorefrontCheckoutController;
use App\Http\Controllers\Api\StorefrontOrderController;
use App\Http\Controllers\Api\StorefrontSearchController;
use Illuminate\Session\Middleware\StartSession;
use Illuminate\Support\Facades\Route;

Route::prefix('storefront/v1')->middleware([StartSession::class, 'store.resolve', 'throttle:api.storefront'])->group(function (): void {
    Route::post('carts', [StorefrontCartController::class, 'store']);
    Route::get('carts/{cartId}', [StorefrontCartController::class, 'show']);
    Route::post('carts/{cartId}/lines', [StorefrontCartController::class, 'addLine']);
    Route::put('carts/{cartId}/lines/{lineId}', [StorefrontCartController::class, 'updateLine']);
    Route::delete('carts/{cartId}/lines/{lineId}', [StorefrontCartController::class, 'removeLine']);
    Route::post('checkouts', [StorefrontCheckoutController::class, 'store'])->middleware('throttle:checkout');
    Route::get('checkouts/{checkoutId}', [StorefrontCheckoutController::class, 'show'])->middleware('throttle:checkout');
    Route::put('checkouts/{checkoutId}/address', [StorefrontCheckoutController::class, 'address'])->middleware('throttle:checkout');
    Route::put('checkouts/{checkoutId}/shipping-method', [StorefrontCheckoutController::class, 'shippingMethod'])->middleware('throttle:checkout');
    Route::put('checkouts/{checkoutId}/payment-method', [StorefrontCheckoutController::class, 'paymentMethod'])->middleware('throttle:checkout');
    Route::post('checkouts/{checkoutId}/apply-discount', [StorefrontCheckoutController::class, 'applyDiscount'])->middleware('throttle:checkout');
    Route::delete('checkouts/{checkoutId}/discount', [StorefrontCheckoutController::class, 'removeDiscount'])->middleware('throttle:checkout');
    Route::post('checkouts/{checkoutId}/pay', [StorefrontCheckoutController::class, 'pay'])->middleware('throttle:checkout');
    Route::get('orders/{orderNumber}', [StorefrontOrderController::class, 'show']);
    Route::get('search', [StorefrontSearchController::class, 'index'])->middleware('throttle:search');
    Route::get('search/suggest', [StorefrontSearchController::class, 'suggest'])->middleware('throttle:search');
    Route::post('analytics/events', [StorefrontAnalyticsController::class, 'store'])->middleware('throttle:analytics');
});

Route::prefix('admin/v1/stores/{storeId}')->middleware(['auth:sanctum', 'store.resolve', 'role.check:owner,admin,staff,support', 'api.ability', 'throttle:api.admin'])->group(function (): void {
    Route::get('products', [AdminController::class, 'products']);
    Route::post('products', [AdminController::class, 'storeProduct'])->middleware('role.check:owner,admin,staff');
    Route::get('products/{productId}', [AdminController::class, 'showProduct']);
    Route::put('products/{productId}', [AdminController::class, 'updateProduct'])->middleware('role.check:owner,admin,staff');
    Route::delete('products/{productId}', [AdminController::class, 'deleteProduct'])->middleware('role.check:owner,admin,staff');
    Route::get('collections', [AdminController::class, 'collections']);
    Route::post('collections', [AdminController::class, 'storeCollection'])->middleware('role.check:owner,admin,staff');
    Route::put('collections/{collectionId}', [AdminController::class, 'updateCollection'])->middleware('role.check:owner,admin,staff');
    Route::delete('collections/{collectionId}', [AdminController::class, 'deleteCollection'])->middleware('role.check:owner,admin,staff');
    Route::get('orders', [AdminController::class, 'orders']);
    Route::get('orders/{orderId}', [AdminController::class, 'showOrder']);
    Route::get('customers', [AdminController::class, 'customers']);
    Route::get('discounts', [AdminController::class, 'discounts']);
    Route::post('discounts', [AdminController::class, 'storeDiscount'])->middleware('role.check:owner,admin,staff');
    Route::put('discounts/{discountId}', [AdminController::class, 'updateDiscount'])->middleware('role.check:owner,admin,staff');
    Route::delete('discounts/{discountId}', [AdminController::class, 'deleteDiscount'])->middleware('role.check:owner,admin');
    Route::get('shipping/zones', [AdminController::class, 'shippingZones']);
    Route::post('shipping/zones', [AdminController::class, 'storeShippingZone'])->middleware('role.check:owner,admin');
    Route::put('shipping/zones/{zoneId}', [AdminController::class, 'updateShippingZone'])->middleware('role.check:owner,admin');
    Route::post('shipping/zones/{zoneId}/rates', [AdminController::class, 'storeShippingRate'])->middleware('role.check:owner,admin');
    Route::get('tax/settings', [AdminController::class, 'taxSettings']);
    Route::put('tax/settings', [AdminController::class, 'updateTaxSettings'])->middleware('role.check:owner,admin');
    Route::get('pages', [AdminController::class, 'pages']);
    Route::post('pages', [AdminController::class, 'storePage'])->middleware('role.check:owner,admin,staff');
    Route::put('pages/{pageId}', [AdminController::class, 'updatePage'])->middleware('role.check:owner,admin,staff');
    Route::delete('pages/{pageId}', [AdminController::class, 'deletePage'])->middleware('role.check:owner,admin');
    Route::post('themes', [AdminController::class, 'storeTheme'])->middleware('role.check:owner,admin');
    Route::post('themes/{themeId}/publish', [AdminController::class, 'publishTheme'])->middleware('role.check:owner,admin');
    Route::put('themes/{themeId}/settings', [AdminController::class, 'updateThemeSettings'])->middleware('role.check:owner,admin');
    Route::post('search/reindex', [AdminController::class, 'reindex'])->middleware('role.check:owner,admin,staff');
    Route::get('search/status', [AdminController::class, 'searchStatus']);
    Route::get('analytics/summary', [AdminController::class, 'analyticsSummary']);
    Route::post('orders/{orderId}/fulfillments', [AdminController::class, 'fulfillOrder'])->middleware('role.check:owner,admin,staff');
    Route::post('orders/{orderId}/refunds', [AdminController::class, 'refundOrder'])->middleware('role.check:owner,admin');
});
<?php

namespace App\Http\Middleware;

use App\Enums\StoreDomainType;
use App\Enums\StoreStatus;
use App\Models\Store;
use App\Models\StoreDomain;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\View;
use Illuminate\Support\Str;
use Symfony\Component\HttpFoundation\Response;

class ResolveStore
{
    public function handle(Request $request, Closure $next, string $context = 'storefront'): Response
    {
        if (! Schema::hasTable('stores')) {
            return $next($request);
        }

        if ($this->isPublicAdminAuthRequest($request)) {
            return $next($request);
        }

        if ($this->isAdminApiRequest($request) && $request->user('sanctum') === null) {
            return $next($request);
        }

        $context = $context === 'storefront' && $this->isAdminRequest($request)
            ? 'admin'
            : $context;

        $store = $context === 'admin'
            ? $this->resolveAdminStore($request)
            : $this->resolveStorefrontStore($request);

        if ($store === null && $context === 'storefront' && $this->isPublicCustomerAuthRequest($request) && Store::query()->doesntExist()) {
            return $next($request);
        }

        abort_unless($store instanceof Store, $context === 'admin' ? 403 : 404);

        if ($store->status === StoreStatus::Suspended) {
            if ($context === 'storefront') {
                abort(503, 'This store is currently unavailable.');
            }

            if (! in_array($request->method(), ['GET', 'HEAD', 'OPTIONS'], true)) {
                abort(403);
            }
        }

        $binding = (string) config('tenancy.binding', 'current_store');
        app()->instance($binding, $store);
        View::share((string) config('tenancy.view_share', 'currentStore'), $store);

        return $next($request);
    }

    private function resolveStorefrontStore(Request $request): ?Store
    {
        $hostname = Str::lower(rtrim(trim($request->getHost()), '.'));
        $cacheKey = config('tenancy.cache_prefix', 'store-domains').':'.$hostname;

        $storeId = Cache::remember(
            $cacheKey,
            (int) config('tenancy.store_cache_ttl', 300),
            fn (): ?int => StoreDomain::query()
                ->where('hostname', $hostname)
                ->where('type', config('tenancy.storefront_domain_type', StoreDomainType::Storefront->value))
                ->value('store_id'),
        );

        return $storeId === null ? null : Store::query()->find($storeId);
    }

    private function resolveAdminStore(Request $request): ?Store
    {
        $isApiRequest = $this->isAdminApiRequest($request);
        $storeId = $isApiRequest
            ? $request->route('storeId')
            : $request->session()->get(config('tenancy.admin_session_key', 'current_store_id'));
        $user = $isApiRequest
            ? $request->user('sanctum')
            : ($request->user('web') ?? $request->user());

        if ($storeId === null || $user === null) {
            return null;
        }

        return $user->stores()->whereKey($storeId)->first();
    }

    private function isAdminRequest(Request $request): bool
    {
        if ($this->isAdminApiRequest($request)) {
            return true;
        }

        $prefix = trim((string) config('tenancy.admin_path_prefix', 'admin'), '/');

        if ($request->is($prefix, $prefix.'/*') || $request->routeIs($prefix.'.*')) {
            return true;
        }

        return $request->is('livewire/update') && str_contains((string) $request->headers->get('referer'), '/admin');
    }

    private function isAdminApiRequest(Request $request): bool
    {
        $prefix = trim((string) config('tenancy.admin_path_prefix', 'admin'), '/');

        return $request->is('api/'.$prefix, 'api/'.$prefix.'/*');
    }

    private function isPublicCustomerAuthRequest(Request $request): bool
    {
        return $request->is('forgot-password', 'reset-password/*');
    }

    private function isPublicAdminAuthRequest(Request $request): bool
    {
        return $request->is('admin/login', 'admin/forgot-password', 'admin/reset-password/*');
    }
}
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Symfony\Component\HttpFoundation\Response;

class EnsureApiAbility
{
    public function handle(Request $request, Closure $next): Response
    {
        $user = $request->user('sanctum');

        abort_unless($request->bearerToken() !== null && $user !== null, 401, 'A Sanctum bearer token is required.');

        $ability = $this->abilityFor($request);

        abort_unless($ability !== null && $user->tokenCan($ability), 403, 'This token does not have the required ability.');

        return $next($request);
    }

    private function abilityFor(Request $request): ?string
    {
        $path = $request->path();
        $method = $request->method();

        foreach (['products' => 'products', 'collections' => 'collections', 'orders' => 'orders', 'customers' => 'customers', 'discounts' => 'discounts'] as $segment => $resource) {
            if (Str::contains($path, '/'.$segment)) {
                return in_array($method, ['GET', 'HEAD'], true) ? 'read-'.$resource : 'write-'.$resource;
            }
        }

        if (Str::contains($path, '/themes')) {
            return in_array($method, ['GET', 'HEAD'], true) ? 'read-themes' : 'write-themes';
        }

        if (Str::contains($path, '/pages')) {
            return in_array($method, ['GET', 'HEAD'], true) ? 'read-content' : 'write-content';
        }

        if (Str::contains($path, '/shipping') || Str::contains($path, '/tax/')) {
            return in_array($method, ['GET', 'HEAD'], true) ? 'read-settings' : 'write-settings';
        }

        if (Str::contains($path, '/analytics')) {
            return 'read-analytics';
        }

        if (Str::contains($path, '/search')) {
            return in_array($method, ['GET', 'HEAD'], true) ? 'read-products' : 'write-products';
        }

        return null;
    }
}
<?php

return [

    /*
    |--------------------------------------------------------------------------
    | Authentication Defaults
    |--------------------------------------------------------------------------
    |
    | This option defines the default authentication "guard" and password
    | reset "broker" for your application. You may change these values
    | as required, but they're a perfect start for most applications.
    |
    */

    'defaults' => [
        'guard' => env('AUTH_GUARD', 'web'),
        'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
    ],

    /*
    |--------------------------------------------------------------------------
    | Authentication Guards
    |--------------------------------------------------------------------------
    |
    | Next, you may define every authentication guard for your application.
    | Of course, a great default configuration has been defined for you
    | which utilizes session storage plus the Eloquent user provider.
    |
    | All authentication guards have a user provider, which defines how the
    | users are actually retrieved out of your database or other storage
    | system used by the application. Typically, Eloquent is utilized.
    |
    | Supported: "session"
    |
    */

    'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],
        'customer' => [
            'driver' => 'session',
            'provider' => 'customers',
        ],
        'sanctum' => [
            'driver' => 'sanctum',
            'provider' => 'users',
        ],
    ],

    /*
    |--------------------------------------------------------------------------
    | User Providers
    |--------------------------------------------------------------------------
    |
    | All authentication guards have a user provider, which defines how the
    | users are actually retrieved out of your database or other storage
    | system used by the application. Typically, Eloquent is utilized.
    |
    | If you have multiple user tables or models you may configure multiple
    | providers to represent the model / table. These providers may then
    | be assigned to any extra authentication guards you have defined.
    |
    | Supported: "database", "eloquent"
    |
    */

    'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => env('AUTH_MODEL', App\Models\User::class),
        ],
        'customers' => [
            'driver' => 'customer',
            'model' => App\Models\Customer::class,
        ],

        // 'users' => [
        //     'driver' => 'database',
        //     'table' => 'users',
        // ],
    ],

    /*
    |--------------------------------------------------------------------------
    | Resetting Passwords
    |--------------------------------------------------------------------------
    |
    | These configuration options specify the behavior of Laravel's password
    | reset functionality, including the table utilized for token storage
    | and the user provider that is invoked to actually retrieve users.
    |
    | The expiry time is the number of minutes that each reset token will be
    | considered valid. This security feature keeps tokens short-lived so
    | they have less time to be guessed. You may change this as needed.
    |
    | The throttle setting is the number of seconds a user must wait before
    | generating more password reset tokens. This prevents the user from
    | quickly generating a very large amount of password reset tokens.
    |
    */

    'passwords' => [
        'users' => [
            'provider' => 'users',
            'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
            'expire' => 60,
            'throttle' => 60,
        ],
        'customers' => [
            'provider' => 'customers',
            'table' => 'customer_password_reset_tokens',
            'expire' => 60,
            'throttle' => 60,
        ],
    ],

    /*
    |--------------------------------------------------------------------------
    | Password Confirmation Timeout
    |--------------------------------------------------------------------------
    |
    | Here you may define the number of seconds before a password confirmation
    | window expires and users are asked to re-enter their password via the
    | confirmation screen. By default, the timeout lasts for three hours.
    |
    */

    'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),

];


exec

status: completed

input:
const r = await tools.multi_agent_v1__wait_agent({targets:["01a0219c-6293-7b40-b5e1-7376c4c0fe0d"], timeout_ms:1000});
text(r);


text:
Script completed
Wall time 10.0 seconds
Output:


status: completed

input:
const matches = ALL_TOOLS.filter(x => /playwright/i.test(x.name + " " + x.description));
text(matches.map(x => x.name + " — " + x.description.slice(0,200)).join("\n"));


text:
Script completed
Wall time 0.0 seconds
Output:

text:
mcp__node_repl__js — Use `js` to run JavaScript in the persistent Node-backed kernel. When a skill or prompt says to use `node_repl`, call this server's `js` execution tool. Calls default to a 30000 ms (30 seconds) timeou
mcp__node_repl__js_add_node_module_dir — Use `js` to run JavaScript in the persistent Node-backed kernel. When a skill or prompt says to use `node_repl`, call this server's `js` execution tool. Calls default to a 30000 ms (30 seconds) timeou
mcp__node_repl__js_reset — Use `js` to run JavaScript in the persistent Node-backed kernel. When a skill or prompt says to use `node_repl`, call this server's `js` execution tool. Calls default to a 30000 ms (30 seconds) timeou
mcp__playwright__browser_click — Perform click on a web page

exec tool declaration:
```ts
declare const tools: { mcp__playwright__browser_click(args: {
  // Button to click, defaults to left
  button?: "left" | "right" | "middle";

mcp__playwright__browser_close — Close the page

exec tool declaration:
```ts
declare const tools: { mcp__playwright__browser_close(args: {}): Promise<CallToolResult>; };
```
mcp__playwright__browser_console_messages — Returns all console messages

exec tool declaration:
```ts
declare const tools: { mcp__playwright__browser_console_messages(args: {
  // Return all console messages since the beginning of the session,
mcp__playwright__browser_drag — Perform drag and drop between two elements

exec tool declaration:
```ts
declare const tools: { mcp__playwright__browser_drag(args: {
  // Human-readable target element description used to obtain the
mcp__playwright__browser_drop — Drop files or MIME-typed data onto an element, as if dragged from outside the page. At least one of "paths" or "data" must be provided.

exec tool declaration:
```ts
declare const tools: { mcp__playwr
mcp__playwright__browser_evaluate — Evaluate JavaScript expression on page or element

exec tool declaration:
```ts
declare const tools: { mcp__playwright__browser_evaluate(args: {
  // Human-readable element description used to obtain
mcp__playwright__browser_file_upload — Upload one or multiple files

exec tool declaration:
```ts
declare const tools: { mcp__playwright__browser_file_upload(args: {
  // The absolute paths to the files to upload. Can be single file or mul
mcp__playwright__browser_fill_form — Fill multiple form fields

exec tool declaration:
```ts
declare const tools: { mcp__playwright__browser_fill_form(args: {
  // Fields to fill in
  fields: Array<{
  // Human-readable element descripti
mcp__playwright__browser_find — Search the accessibility snapshot of the current page for text or a regular expression. Returns matching snapshot nodes with a few lines of surrounding context (like search snippets), each shown under
mcp__playwright__browser_handle_dialog — Handle a dialog

exec tool declaration:
```ts
declare const tools: { mcp__playwright__browser_handle_dialog(args: {
  // Whether to accept the dialog.
  accept: boolean;
  // The text of the prompt in
mcp__playwright__browser_hover — Hover over element on page

exec tool declaration:
```ts
declare const tools: { mcp__playwright__browser_hover(args: {
  // Human-readable element description used to obtain permission to interact wit
mcp__playwright__browser_navigate — Navigate to a URL

exec tool declaration:
```ts
declare const tools: { mcp__playwright__browser_navigate(args: {
  // The URL to navigate to
  url: string;
}): Promise<CallToolResult>; };
```
mcp__playwright__browser_navigate_back — Go back to the previous page in the history

exec tool declaration:
```ts
declare const tools: { mcp__playwright__browser_navigate_back(args: {}): Promise<CallToolResult>; };
```
mcp__playwright__browser_network_request — Returns full details (headers and body) of a single network request, or a single part if `part` is set. Use the number from browser_network_requests.

exec tool declaration:
```ts
declare const tools:
mcp__playwright__browser_network_requests — Returns a numbered list of network requests since loading the page. Use browser_network_request with the number to get full details.

exec tool declaration:
```ts
declare const tools: { mcp__playwrigh
mcp__playwright__browser_press_key — Press a key on the keyboard

exec tool declaration:
```ts
declare const tools: { mcp__playwright__browser_press_key(args: {
  // Name of the key to press or a character to generate, such as `ArrowLeft
mcp__playwright__browser_resize — Resize the browser window

exec tool declaration:
```ts
declare const tools: { mcp__playwright__browser_resize(args: {
  // Height of the browser window
  height: number;
  // Width of the browser win
mcp__playwright__browser_run_code_unsafe — Run a Playwright code snippet. Unsafe: executes arbitrary JavaScript in the Playwright server process and is RCE-equivalent.

exec tool declaration:
```ts
declare const tools: { mcp__playwright__brows
mcp__playwright__browser_select_option — Select an option in a dropdown

exec tool declaration:
```ts
declare const tools: { mcp__playwright__browser_select_option(args: {
  // Human-readable element description used to obtain permission to
mcp__playwright__browser_snapshot — Capture accessibility snapshot of the current page, this is better than screenshot

exec tool declaration:
```ts
declare const tools: { mcp__playwright__browser_snapshot(args: {
  // Include each elem
mcp__playwright__browser_tabs — List, create, close, or select a browser tab.

exec tool declaration:
```ts
declare const tools: { mcp__playwright__browser_tabs(args: {
  // Operation to perform
  action: "list" | "new" | "close" |
mcp__playwright__browser_take_screenshot — Take a screenshot of the current page. You can't perform actions based on the screenshot, use browser_snapshot for actions.

exec tool declaration:
```ts
declare const tools: { mcp__playwright__browse
mcp__playwright__browser_type — Type text into editable element

exec tool declaration:
```ts
declare const tools: { mcp__playwright__browser_type(args: {
  // Human-readable element description used to obtain permission to interact
mcp__playwright__browser_wait_for — Wait for text to appear or disappear or a specified time to pass

exec tool declaration:
```ts
declare const tools: { mcp__playwright__browser_wait_for(args: {
  // The text to wait for
  text?: strin


status: completed

input:
const r = await tools.mcp__playwright__browser_tabs({action:"list"});
text(r);


type: McpToolCall

server: playwright

action: list

status: completed

type: text

text:
### Result
- 0: (current) [Admin · Acme Fashion](http://shop.test:8000/admin)


text:
Script completed
Wall time 0.0 seconds
Output:

text: {"content":[{"type":"text","text":"### Result\n- 0: (current) [Admin · Acme Fashion](http://shop.test:8000/admin)"}]}


status: completed

input:
const r = await tools.mcp__playwright__browser_navigate({url:"http://shop.test:8000/"});
text(r);


text:
### Ran Playwright code
```js
await page.goto('http://shop.test:8000/');
```
### Page
- Page URL: http://shop.test:8000/
- Page Title: Acme Fashion
### Snapshot
- [Snapshot](.playwright-mcp/page-2026-08-20T23-59-11-859Z.yml)
### Events
- New console entries: .playwright-mcp/console-2026-08-20T23-59-11-659Z.log#L1


text:
Script completed
Wall time 0.2 seconds
Output:

text: {"content":[{"type":"text","text":"### Ran Playwright code\n```js\nawait page.goto('http://shop.test:8000/');\n```\n### Page\n- Page URL: http://shop.test:8000/\n- Page Title: Acme Fashion\n### Snapshot\n- [Snapshot](.playwright-mcp/page-2026-08-20T23-59-11-859Z.yml)\n### Events\n- New console entries: .playwright-mcp/console-2026-08-20T23-59-11-659Z.log#L1"}]}


status: completed

input:
const results = await Promise.all([
  tools.mcp__playwright__browser_snapshot({}),
  tools.mcp__playwright__browser_console_messages({onlyErrors:true})
]);
results.forEach((r, i) => { text(`---${i}---`); text(r); });


text:
### Result
Total messages: 1 (Errors: 0, Warnings: 0)

[LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test:8000/_boost/browser-logs @ http://shop.test:8000/:14


text:
### Page
- Page URL: http://shop.test:8000/
- Page Title: Acme Fashion
### Snapshot
```yaml
- generic [active] [ref=f62e1]:
  - link "Skip to main content" [ref=f62e2] [cursor=pointer]:
    - /url: "#main-content"
  - status [ref=f62e3]: Free shipping on orders over €50
  - banner [ref=f62e4]:
    - navigation "Main navigation" [ref=f62e5]:
      - link "Acme Fashion" [ref=f62e6] [cursor=pointer]:
        - /url: http://shop.test:8000
      - generic [ref=f62e7]:
        - link "Account" [ref=f62e8] [cursor=pointer]:
          - /url: http://shop.test:8000/account/login
        - button "Open cart" [ref=f62e9]: Cart
  - main [ref=f62e10]:
    - generic [ref=f62e11]:
      - generic [ref=f62e14]:
        - paragraph [ref=f62e15]: Acme Fashion
        - heading "Everyday pieces, thoughtfully made." [level=1] [ref=f62e16]
        - paragraph [ref=f62e17]: Timeless wardrobe essentials with an easy, modern fit.
        - link "Shop new arrivals" [ref=f62e18] [cursor=pointer]:
          - /url: http://shop.test:8000/collections/new-arrivals
      - generic [ref=f62e19]:
        - generic [ref=f62e20]:
          - generic [ref=f62e21]:
            - paragraph [ref=f62e22]: Explore
            - heading "Featured collections" [level=2] [ref=f62e23]
          - link "View all" [ref=f62e24] [cursor=pointer]:
            - /url: http://shop.test:8000/collections
        - generic [ref=f62e25]:
          - link [ref=f62e26] [cursor=pointer]:
            - /url: http://shop.test:8000/collections/t-shirts
            - paragraph [ref=f62e27]: T-Shirts
            - paragraph [ref=f62e28]: 2 products
          - link [ref=f62e29] [cursor=pointer]:
            - /url: http://shop.test:8000/collections/new-arrivals
            - paragraph [ref=f62e30]: New Arrivals
            - paragraph [ref=f62e31]: 3 products
      - generic [ref=f62e33]:
        - generic [ref=f62e35]:
          - paragraph [ref=f62e36]: Curated for you
          - heading "Featured products" [level=2] [ref=f62e37]
        - generic [ref=f62e38]:
          - article [ref=f62e39]:
            - link "Relaxed Backorder Hoodie" [ref=f62e40] [cursor=pointer]:
              - /url: http://shop.test:8000/products/relaxed-backorder-hoodie
              - generic [ref=f62e41]:
                - generic [ref=f62e42]: ⌂
                - generic [ref=f62e43]: Sale
              - generic [ref=f62e44]:
                - heading "Relaxed Backorder Hoodie" [level=3] [ref=f62e45]
                - paragraph [ref=f62e46]: €69.99 €74.99
          - article [ref=f62e47]:
            - link "Sold Out Limited Tee" [ref=f62e48] [cursor=pointer]:
              - /url: http://shop.test:8000/products/sold-out-limited-tee
              - generic [ref=f62e49]:
                - generic [ref=f62e50]: ⌂
                - generic [ref=f62e51]: Sale
                - generic [ref=f62e52]: Sold out
              - generic [ref=f62e53]:
                - heading "Sold Out Limited Tee" [level=3] [ref=f62e54]
                - paragraph [ref=f62e55]: €39.99 €44.99
          - article [ref=f62e56]:
            - link "Premium Slim Fit Jeans" [ref=f62e57] [cursor=pointer]:
              - /url: http://shop.test:8000/products/premium-slim-fit-jeans
              - generic [ref=f62e58]:
                - generic [ref=f62e59]: ⌂
                - generic [ref=f62e60]: Sale
              - generic [ref=f62e61]:
                - heading "Premium Slim Fit Jeans" [level=3] [ref=f62e62]
                - paragraph [ref=f62e63]: €79.99 €84.99
          - article [ref=f62e64]:
            - link "Classic Cotton T-Shirt" [ref=f62e65] [cursor=pointer]:
              - /url: http://shop.test:8000/products/classic-cotton-t-shirt
              - generic [ref=f62e66]:
                - generic [ref=f62e67]: ⌂
                - generic [ref=f62e68]: Sale
              - generic [ref=f62e69]:
                - heading "Classic Cotton T-Shirt" [level=3] [ref=f62e70]
                - paragraph [ref=f62e71]: €24.99 €29.99
      - generic [ref=f62e72]:
        - heading "Stay in the loop" [level=2] [ref=f62e73]
        - paragraph [ref=f62e74]: Subscribe for exclusive offers and updates.
        - generic [ref=f62e75]:
          - generic [ref=f62e76]: Email
          - textbox "Email" [ref=f62e77]:
            - /placeholder: Enter your email
          - button "Subscribe" [ref=f62e78]
  - contentinfo [ref=f62e79]:
    - generic [ref=f62e80]:
      - generic [ref=f62e81]:
        - paragraph [ref=f62e82]: Acme Fashion
        - paragraph [ref=f62e83]: Thoughtful everyday pieces, made to last.
      - generic [ref=f62e84]:
        - paragraph [ref=f62e85]: Shop
        - link "Collections" [ref=f62e86] [cursor=pointer]:
          - /url: http://shop.test:8000/collections
      - generic [ref=f62e87]:
        - paragraph [ref=f62e88]: Help
        - link "Your account" [ref=f62e89] [cursor=pointer]:
          - /url: http://shop.test:8000/account
      - generic [ref=f62e90]:
        - paragraph [ref=f62e91]: Stay in the loop
        - paragraph [ref=f62e92]: Subscribe for exclusive offers and updates.
    - generic [ref=f62e93]: © 2026 Acme Fashion. All rights reserved.
  - button "Open search" [ref=f62e95]: Search
```


text:
Script completed
Wall time 0.0 seconds
Output:

text: {"content":[{"type":"text","text":"### Page\n- Page URL: http://shop.test:8000/\n- Page Title: Acme Fashion\n### Snapshot\n```yaml\n- generic [active] [ref=f62e1]:\n  - link \"Skip to main content\" [ref=f62e2] [cursor=pointer]:\n    - /url: \"#main-content\"\n  - status [ref=f62e3]: Free shipping on orders over €50\n  - banner [ref=f62e4]:\n    - navigation \"Main navigation\" [ref=f62e5]:\n      - link \"Acme Fashion\" [ref=f62e6] [cursor=pointer]:\n        - /url: http://shop.test:8000\n      - generic [ref=f62e7]:\n        - link \"Account\" [ref=f62e8] [cursor=pointer]:\n          - /url: http://shop.test:8000/account/login\n        - button \"Open cart\" [ref=f62e9]: Cart\n  - main [ref=f62e10]:\n    - generic [ref=f62e11]:\n      - generic [ref=f62e14]:\n        - paragraph [ref=f62e15]: Acme Fashion\n        - heading \"Everyday pieces, thoughtfully made.\" [level=1] [ref=f62e16]\n        - paragraph [ref=f62e17]: Timeless wardrobe essentials with an easy, modern fit.\n        - link \"Shop new arrivals\" [ref=f62e18] [cursor=pointer]:\n          - /url: http://shop.test:8000/collections/new-arrivals\n      - generic [ref=f62e19]:\n        - generic [ref=f62e20]:\n          - generic [ref=f62e21]:\n            - paragraph [ref=f62e22]: Explore\n            - heading \"Featured collections\" [level=2] [ref=f62e23]\n          - link \"View all\" [ref=f62e24] [cursor=pointer]:\n            - /url: http://shop.test:8000/collections\n        - generic [ref=f62e25]:\n          - link [ref=f62e26] [cursor=pointer]:\n            - /url: http://shop.test:8000/collections/t-shirts\n            - paragraph [ref=f62e27]: T-Shirts\n            - paragraph [ref=f62e28]: 2 products\n          - link [ref=f62e29] [cursor=pointer]:\n            - /url: http://shop.test:8000/collections/new-arrivals\n            - paragraph [ref=f62e30]: New Arrivals\n            - paragraph [ref=f62e31]: 3 products\n      - generic [ref=f62e33]:\n        - generic [ref=f62e35]:\n          - paragraph [ref=f62e36]: Curated for you\n          - heading \"Featured products\" [level=2] [ref=f62e37]\n        - generic [ref=f62e38]:\n          - article [ref=f62e39]:\n            - link \"Relaxed Backorder Hoodie\" [ref=f62e40] [cursor=pointer]:\n              - /url: http://shop.test:8000/products/relaxed-backorder-hoodie\n              - generic [ref=f62e41]:\n                - generic [ref=f62e42]: ⌂\n                - generic [ref=f62e43]: Sale\n              - generic [ref=f62e44]:\n                - heading \"Relaxed Backorder Hoodie\" [level=3] [ref=f62e45]\n                - paragraph [ref=f62e46]: €69.99 €74.99\n          - article [ref=f62e47]:\n            - link \"Sold Out Limited Tee\" [ref=f62e48] [cursor=pointer]:\n              - /url: http://shop.test:8000/products/sold-out-limited-tee\n              - generic [ref=f62e49]:\n                - generic [ref=f62e50]: ⌂\n                - generic [ref=f62e51]: Sale\n                - generic [ref=f62e52]: Sold out\n              - generic [ref=f62e53]:\n                - heading \"Sold Out Limited Tee\" [level=3] [ref=f62e54]\n                - paragraph [ref=f62e55]: €39.99 €44.99\n          - article [ref=f62e56]:\n            - link \"Premium Slim Fit Jeans\" [ref=f62e57] [cursor=pointer]:\n              - /url: http://shop.test:8000/products/premium-slim-fit-jeans\n              - generic [ref=f62e58]:\n                - generic [ref=f62e59]: ⌂\n                - generic [ref=f62e60]: Sale\n              - generic [ref=f62e61]:\n                - heading \"Premium Slim Fit Jeans\" [level=3] [ref=f62e62]\n                - paragraph [ref=f62e63]: €79.99 €84.99\n          - article [ref=f62e64]:\n            - link \"Classic Cotton T-Shirt\" [ref=f62e65] [cursor=pointer]:\n              - /url: http://shop.test:8000/products/classic-cotton-t-shirt\n              - generic [ref=f62e66]:\n                - generic [ref=f62e67]: ⌂\n                - generic [ref=f62e68]: Sale\n              - generic [ref=f62e69]:\n                - heading \"Classic Cotton T-Shirt\" [level=3] [ref=f62e70]\n                - paragraph [ref=f62e71]: €24.99 €29.99\n      - generic [ref=f62e72]:\n        - heading \"Stay in the loop\" [level=2] [ref=f62e73]\n        - paragraph [ref=f62e74]: Subscribe for exclusive offers and updates.\n        - generic [ref=f62e75]:\n          - generic [ref=f62e76]: Email\n          - textbox \"Email\" [ref=f62e77]:\n            - /placeholder: Enter your email\n          - button \"Subscribe\" [ref=f62e78]\n  - contentinfo [ref=f62e79]:\n    - generic [ref=f62e80]:\n      - generic [ref=f62e81]:\n        - paragraph [ref=f62e82]: Acme Fashion\n        - paragraph [ref=f62e83]: Thoughtful everyday pieces, made to last.\n      - generic [ref=f62e84]:\n        - paragraph [ref=f62e85]: Shop\n        - link \"Collections\" [ref=f62e86] [cursor=pointer]:\n          - /url: http://shop.test:8000/collections\n      - generic [ref=f62e87]:\n        - paragraph [ref=f62e88]: Help\n        - link \"Your account\" [ref=f62e89] [cursor=pointer]:\n          - /url: http://shop.test:8000/account\n      - generic [ref=f62e90]:\n        - paragraph [ref=f62e91]: Stay in the loop\n        - paragraph [ref=f62e92]: Subscribe for exclusive offers and updates.\n    - generic [ref=f62e93]: © 2026 Acme Fashion. All rights reserved.\n  - button \"Open search\" [ref=f62e95]: Search\n```"}]}

text: {"content":[{"type":"text","text":"### Result\nTotal messages: 1 (Errors: 0, Warnings: 0)\n\n[LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://shop.test:8000/_boost/browser-logs @ http://shop.test:8000/:14"}]}


status: completed

input:
const r = await tools.multi_agent_v1__wait_agent({targets:["01a0219c-6293-7b40-b5e1-7376c4c0fe0d"], timeout_ms:10000});
text(r);


text:
Script completed
Wall time 10.0 seconds
Output:


status: completed

input:
const r = await tools.exec_command({
  cmd: "sed -n '1,360p' routes/web.php && printf '\\n--- livewire components ---\\n' && find app/Livewire -type f -maxdepth 6 | sort && printf '\\n--- spec route tables ---\\n' && sed -n '35,210p' specs/02-API-ROUTES.md",
  workdir: "/Users/wesner/Herd/shop",
  yield_time_ms: 10000,
  max_output_tokens: 40000
});
text(r.output);


type: CommandExecution

command: sed -n '1,360p' routes/web.php && printf '\n--- livewire components ---\n' && find app/Livewire -type f -maxdepth 6 | sort && printf '\n--- spec route tables ---\n' && sed -n '35,210p' specs/02-API-ROUTES.md

web.php

cmd: sed -n '1,360p' routes/web.php

cmd: find app/Livewire -type f -maxdepth 6

path: Livewire

cmd: sed -n '35,210p' specs/02-API-ROUTES.md

stdout:
<?php

use App\Livewire\Admin\Analytics\Index as AdminAnalyticsIndex;
use App\Livewire\Admin\Apps\Index as AdminAppsIndex;
use App\Livewire\Admin\Apps\Show as AdminAppsShow;
use App\Livewire\Admin\Auth\ForgotPassword as AdminForgotPassword;
use App\Livewire\Admin\Auth\Login as AdminLogin;
use App\Livewire\Admin\Auth\ResetPassword as AdminResetPassword;
use App\Livewire\Admin\Collections\Create as AdminCollectionsCreate;
use App\Livewire\Admin\Collections\Edit as AdminCollectionsEdit;
use App\Livewire\Admin\Collections\Index as AdminCollectionsIndex;
use App\Livewire\Admin\Customers\Index as AdminCustomersIndex;
use App\Livewire\Admin\Customers\Show as AdminCustomersShow;
use App\Livewire\Admin\Dashboard as AdminDashboard;
use App\Livewire\Admin\Developers\Index as AdminDevelopersIndex;
use App\Livewire\Admin\Discounts\Form as AdminDiscountForm;
use App\Livewire\Admin\Discounts\Index as AdminDiscountsIndex;
use App\Livewire\Admin\Inventory\Index as AdminInventoryIndex;
use App\Livewire\Admin\Navigation\Index as AdminNavigationIndex;
use App\Livewire\Admin\Orders\Index as AdminOrdersIndex;
use App\Livewire\Admin\Orders\Show as AdminOrdersShow;
use App\Livewire\Admin\Pages\Create as AdminPagesCreate;
use App\Livewire\Admin\Pages\Edit as AdminPagesEdit;
use App\Livewire\Admin\Pages\Index as AdminPagesIndex;
use App\Livewire\Admin\Products\Form as AdminProductForm;
use App\Livewire\Admin\Products\Index as AdminProductsIndex;
use App\Livewire\Admin\Search\Settings as AdminSearchSettings;
use App\Livewire\Admin\Settings\General as AdminSettingsGeneral;
use App\Livewire\Admin\Settings\Shipping as AdminSettingsShipping;
use App\Livewire\Admin\Settings\Taxes as AdminSettingsTaxes;
use App\Livewire\Admin\Themes\Editor as AdminThemesEditor;
use App\Livewire\Admin\Themes\Index as AdminThemesIndex;
use App\Livewire\Storefront\Account\Addresses\Index as AccountAddresses;
use App\Livewire\Storefront\Account\Auth\ForgotPassword as CustomerForgotPassword;
use App\Livewire\Storefront\Account\Auth\Login as CustomerLogin;
use App\Livewire\Storefront\Account\Auth\Register as CustomerRegister;
use App\Livewire\Storefront\Account\Auth\ResetPassword as CustomerResetPassword;
use App\Livewire\Storefront\Account\Dashboard as AccountDashboard;
use App\Livewire\Storefront\Account\Orders\Index as AccountOrders;
use App\Livewire\Storefront\Account\Orders\Show as AccountOrderShow;
use App\Livewire\Storefront\Cart\Show as CartShow;
use App\Livewire\Storefront\Checkout\Confirmation as CheckoutConfirmation;
use App\Livewire\Storefront\Checkout\Show as CheckoutShow;
use App\Livewire\Storefront\Collections\Index as CollectionsIndex;
use App\Livewire\Storefront\Collections\Show as CollectionShow;
use App\Livewire\Storefront\Home;
use App\Livewire\Storefront\Pages\Show as PageShow;
use App\Livewire\Storefront\Products\Show as ProductShow;
use App\Livewire\Storefront\Search\Index as SearchIndex;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Route;

Route::view('dashboard', 'dashboard')
    ->middleware(['auth', 'verified'])
    ->name('dashboard');

require __DIR__.'/settings.php';

Route::get('/favicon.ico', fn (): \Symfony\Component\HttpFoundation\Response => response('', 204))->name('favicon');

Route::middleware('store.resolve')->group(function (): void {
    Route::livewire('/', Home::class)->name('home');
    Route::livewire('/collections', CollectionsIndex::class)->name('collections.index');
    Route::livewire('/collections/{handle}', CollectionShow::class)->name('collection.show');
    Route::livewire('/products/{handle}', ProductShow::class)->name('product.show');
    Route::livewire('/cart', CartShow::class)->name('cart.show');
    Route::livewire('/search', SearchIndex::class)->name('search');
    Route::livewire('/pages/{handle}', PageShow::class)->name('page.show');
    Route::livewire('/checkout/{checkoutId}', CheckoutShow::class)->name('checkout.show');
    Route::livewire('/checkout/{checkoutId}/confirmation', CheckoutConfirmation::class)->name('checkout.confirmation');
    Route::livewire('/account/login', CustomerLogin::class)->middleware('throttle:login')->name('account.login');
    Route::livewire('/account/register', CustomerRegister::class)->name('account.register');
    Route::livewire('/forgot-password', CustomerForgotPassword::class)->name('password.request');
    Route::livewire('/reset-password/{token}', CustomerResetPassword::class)->name('password.reset');
    Route::post('/account/logout', function (): \Illuminate\Http\RedirectResponse {
        Auth::guard('customer')->logout();
        request()->session()->invalidate();
        request()->session()->regenerateToken();

        return redirect()->route('home');
    })->name('account.logout');

    Route::middleware('auth:customer')->group(function (): void {
        Route::livewire('/account', AccountDashboard::class)->name('account.dashboard');
        Route::livewire('/account/orders', AccountOrders::class)->name('account.orders');
        Route::livewire('/account/orders/{orderNumber}', AccountOrderShow::class)->name('account.order.show');
        Route::livewire('/account/addresses', AccountAddresses::class)->name('account.addresses');
    });
});

Route::livewire('/admin/login', AdminLogin::class)->middleware('throttle:login')->name('admin.login');
Route::livewire('/admin/forgot-password', AdminForgotPassword::class)->middleware('throttle:login')->name('admin.password.request');
Route::livewire('/admin/reset-password/{token}', AdminResetPassword::class)->name('admin.password.reset');
Route::post('/admin/logout', function (): \Illuminate\Http\RedirectResponse {
    Auth::guard('web')->logout();
    request()->session()->invalidate();
    request()->session()->regenerateToken();

    return redirect()->route('admin.login');
})->name('admin.logout');

Route::prefix('admin')->middleware(['auth', 'verified', 'store.resolve', 'role.check:owner,admin,staff,support'])->group(function (): void {
    Route::livewire('/', AdminDashboard::class)->name('admin.dashboard');
    Route::livewire('/products', AdminProductsIndex::class)->name('admin.products.index');
    Route::livewire('/products/create', AdminProductForm::class)->middleware('role.check:owner,admin,staff')->name('admin.products.create');
    Route::livewire('/products/{product}/edit', AdminProductForm::class)->middleware('role.check:owner,admin,staff')->name('admin.products.edit');
    Route::livewire('/orders', AdminOrdersIndex::class)->name('admin.orders.index');
    Route::livewire('/orders/{order}', AdminOrdersShow::class)->name('admin.orders.show');
    Route::livewire('/customers', AdminCustomersIndex::class)->name('admin.customers.index');
    Route::livewire('/customers/{customer}', AdminCustomersShow::class)->name('admin.customers.show');
    Route::livewire('/discounts', AdminDiscountsIndex::class)->name('admin.discounts.index');
    Route::livewire('/discounts/create', AdminDiscountForm::class)->middleware('role.check:owner,admin,staff')->name('admin.discounts.create');
    Route::livewire('/discounts/{discount}/edit', AdminDiscountForm::class)->middleware('role.check:owner,admin,staff')->name('admin.discounts.edit');
    Route::livewire('/settings', AdminSettingsGeneral::class)->middleware('role.check:owner,admin')->name('admin.settings');
    Route::livewire('/settings/shipping', AdminSettingsShipping::class)->middleware('role.check:owner,admin')->name('admin.settings.shipping');
    Route::livewire('/settings/taxes', AdminSettingsTaxes::class)->middleware('role.check:owner,admin')->name('admin.settings.taxes');
    Route::livewire('/inventory', AdminInventoryIndex::class)->name('admin.inventory.index');
    Route::livewire('/collections', AdminCollectionsIndex::class)->name('admin.collections.index');
    Route::livewire('/collections/create', AdminCollectionsCreate::class)->middleware('role.check:owner,admin,staff')->name('admin.collections.create');
    Route::livewire('/collections/{collection}/edit', AdminCollectionsEdit::class)->middleware('role.check:owner,admin,staff')->name('admin.collections.edit');
    Route::livewire('/themes', AdminThemesIndex::class)->name('admin.themes.index');
    Route::livewire('/themes/{theme}/editor', AdminThemesEditor::class)->name('admin.themes.editor');
    Route::livewire('/pages', AdminPagesIndex::class)->name('admin.pages.index');
    Route::livewire('/pages/create', AdminPagesCreate::class)->middleware('role.check:owner,admin')->name('admin.pages.create');
    Route::livewire('/pages/{page}/edit', AdminPagesEdit::class)->middleware('role.check:owner,admin')->name('admin.pages.edit');
    Route::livewire('/navigation', AdminNavigationIndex::class)->name('admin.navigation.index');
    Route::livewire('/apps', AdminAppsIndex::class)->name('admin.apps.index');
    Route::livewire('/apps/{installation}', AdminAppsShow::class)->name('admin.apps.show');
    Route::livewire('/developers', AdminDevelopersIndex::class)->middleware('role.check:owner,admin')->name('admin.developers.index');
    Route::livewire('/analytics', AdminAnalyticsIndex::class)->name('admin.analytics.index');
    Route::livewire('/search/settings', AdminSearchSettings::class)->name('admin.search.settings');
});

--- livewire components ---
app/Livewire/Actions/Logout.php
app/Livewire/Admin/Analytics/Index.php
app/Livewire/Admin/Apps/Index.php
app/Livewire/Admin/Apps/Show.php
app/Livewire/Admin/Auth/ForgotPassword.php
app/Livewire/Admin/Auth/Login.php
app/Livewire/Admin/Auth/ResetPassword.php
app/Livewire/Admin/Collections/Create.php
app/Livewire/Admin/Collections/Edit.php
app/Livewire/Admin/Collections/Form.php
app/Livewire/Admin/Collections/Index.php
app/Livewire/Admin/Customers/Index.php
app/Livewire/Admin/Customers/Show.php
app/Livewire/Admin/Dashboard.php
app/Livewire/Admin/Developers/Index.php
app/Livewire/Admin/Discounts/Form.php
app/Livewire/Admin/Discounts/Index.php
app/Livewire/Admin/Inventory/Index.php
app/Livewire/Admin/Navigation/Index.php
app/Livewire/Admin/Orders/Index.php
app/Livewire/Admin/Orders/Show.php
app/Livewire/Admin/Pages/Create.php
app/Livewire/Admin/Pages/Edit.php
app/Livewire/Admin/Pages/Form.php
app/Livewire/Admin/Pages/Index.php
app/Livewire/Admin/Products/Form.php
app/Livewire/Admin/Products/Index.php
app/Livewire/Admin/Search/Settings.php
app/Livewire/Admin/Section.php
app/Livewire/Admin/Settings/Domains.php
app/Livewire/Admin/Settings/General.php
app/Livewire/Admin/Settings/Shipping.php
app/Livewire/Admin/Settings/Taxes.php
app/Livewire/Admin/Themes/Editor.php
app/Livewire/Admin/Themes/Index.php
app/Livewire/Settings/Appearance.php
app/Livewire/Settings/DeleteUserForm.php
app/Livewire/Settings/Password.php
app/Livewire/Settings/Profile.php
app/Livewire/Settings/TwoFactor.php
app/Livewire/Settings/TwoFactor/RecoveryCodes.php
app/Livewire/Storefront/Account/Addresses/Index.php
app/Livewire/Storefront/Account/Auth/ForgotPassword.php
app/Livewire/Storefront/Account/Auth/Login.php
app/Livewire/Storefront/Account/Auth/Register.php
app/Livewire/Storefront/Account/Auth/ResetPassword.php
app/Livewire/Storefront/Account/Dashboard.php
app/Livewire/Storefront/Account/Orders/Index.php
app/Livewire/Storefront/Account/Orders/Show.php
app/Livewire/Storefront/Cart/Show.php
app/Livewire/Storefront/CartDrawer.php
app/Livewire/Storefront/Checkout/Confirmation.php
app/Livewire/Storefront/Checkout/Show.php
app/Livewire/Storefront/Collections/Index.php
app/Livewire/Storefront/Collections/Show.php
app/Livewire/Storefront/Home.php
app/Livewire/Storefront/Pages/Show.php
app/Livewire/Storefront/Products/Show.php
app/Livewire/Storefront/Search/Index.php
app/Livewire/Storefront/Search/Modal.php

--- spec route tables ---

> **Note:** The Create and Edit routes render the same Livewire form component (`ProductForm`, `OrderForm`, etc.) in create vs edit mode, following the shared-form pattern defined in Spec 03.

#### Dashboard

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin` | Dashboard with KPI tiles, orders chart, conversion funnel | `Admin\Dashboard` |

#### Products

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/products` | Product list - searchable, filterable by status, bulk actions | `Admin\Products\Index` |
| GET | `/admin/products/create` | Create product form | `Admin\Products\Create` |
| GET | `/admin/products/{product}/edit` | Edit product form with variants, media, options | `Admin\Products\Edit` |

#### Inventory

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/inventory` | Inventory management page | `Admin\Inventory\Index` |

#### Collections

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/collections` | Collection list | `Admin\Collections\Index` |
| GET | `/admin/collections/create` | Create collection form | `Admin\Collections\Create` |
| GET | `/admin/collections/{collection}/edit` | Edit collection form | `Admin\Collections\Edit` |

#### Orders

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/orders` | Order list with status filters | `Admin\Orders\Index` |
| GET | `/admin/orders/{order}` | Order detail with timeline, payments, fulfillments | `Admin\Orders\Show` |

#### Customers

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/customers` | Customer list | `Admin\Customers\Index` |
| GET | `/admin/customers/{customer}` | Customer detail with order history, addresses | `Admin\Customers\Show` |

#### Discounts

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/discounts` | Discount list | `Admin\Discounts\Index` |
| GET | `/admin/discounts/create` | Create discount form with rules builder | `Admin\Discounts\Create` |
| GET | `/admin/discounts/{discount}/edit` | Edit discount form | `Admin\Discounts\Edit` |

#### Settings

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/settings` | Store settings (tabs: General, Domains, Shipping, Taxes, Checkout, Notifications) | `Admin\Settings\Index` |
| GET | `/admin/settings/shipping` | Shipping zones and rates configuration | `Admin\Settings\Shipping` |
| GET | `/admin/settings/taxes` | Tax settings - mode, provider config, rates | `Admin\Settings\Taxes` |

#### Themes and Online Store

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/themes` | Theme management - cards with actions (Customize, Preview, Publish, Duplicate, Delete) | `Admin\Themes\Index` |
| GET | `/admin/themes/{theme}/editor` | Theme editor - left panel sections, center preview iframe, right panel settings | `Admin\Themes\Editor` |

#### Pages

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/pages` | Content pages list | `Admin\Pages\Index` |
| GET | `/admin/pages/create` | Create content page | `Admin\Pages\Create` |
| GET | `/admin/pages/{page}/edit` | Edit content page | `Admin\Pages\Edit` |

#### Navigation

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/navigation` | Navigation menus management | `Admin\Navigation\Index` |

#### Apps

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/apps` | Apps directory - list installed and available apps | `Admin\Apps\Index` |
| GET | `/admin/apps/{installation}` | Installed app detail - scopes, webhooks, usage | `Admin\Apps\Show` |

#### Developers

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/developers` | Developer settings - API keys/tokens, webhook management | `Admin\Developers\Index` |

> **Note:** Webhook subscription management (CRUD) is available exclusively through the admin UI at `/admin/developers`. There are no REST API endpoints for programmatic webhook subscription management in the initial implementation.

#### Analytics

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/analytics` | Analytics dashboard - sales, traffic, funnel, exports | `Admin\Analytics\Index` |

#### Search Settings

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/search/settings` | Search settings - synonyms, stopwords, reindex | `Admin\Search\Settings` |

---

### 1.3 Storefront Routes

**Middleware:** `web`, `store.resolve`

These routes serve the public-facing storefront using Blade templates rendered through the active theme engine.

| Method | Path | Description | Component / View |
|--------|------|-------------|------------------|
| GET | `/` | Home page | `Storefront\Home` |
| GET | `/collections` | All collections listing | `Storefront\Collections\Index` |
| GET | `/collections/{handle}` | Single collection page with product grid and filters | `Storefront\Collections\Show` |
| GET | `/products/{handle}` | Product page - gallery, variant selector, add-to-cart | `Storefront\Products\Show` |
| GET | `/cart` | Full cart page - line items, quantity controls, discount input, totals | `Storefront\Cart\Show` |
| GET | `/search` | Search results page with filter panel, sort dropdown, product grid | `Storefront\Search\Index` |
| GET | `/pages/{handle}` | Static content page (About, FAQ, etc.) | `Storefront\Pages\Show` |

#### Customer Account Routes

**Additional middleware:** `auth:customer`

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/account` | Customer account overview | `Storefront\Account\Dashboard` |
| GET | `/account/orders` | Order history list | `Storefront\Account\Orders\Index` |
| GET | `/account/orders/{orderNumber}` | Order detail | `Storefront\Account\Orders\Show` |
| GET | `/account/addresses` | Address book management | `Storefront\Account\Addresses\Index` |

#### Customer Auth Routes (no auth required)

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/account/login` | Customer login page | `Storefront\Account\Auth\Login` |
| GET | `/account/register` | Customer registration page | `Storefront\Account\Auth\Register` |
| GET | `/forgot-password` | Forgot password page | `Storefront\Account\Auth\ForgotPassword` |
| POST | `/forgot-password` | Send reset link | Handled by Livewire action |
| GET | `/reset-password/{token}` | Reset password form | `Storefront\Account\Auth\ResetPassword` |
| POST | `/reset-password` | Reset password | Handled by Livewire action |

---

### 1.4 Checkout Routes

**Middleware:** `web`, `store.resolve`

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/checkout/{checkoutId}` | Checkout page - stepper UI (Contact -> Shipping -> Payment) | `Storefront\Checkout\Show` |
| GET | `/checkout/{checkoutId}/confirmation` | Order confirmation page - order number, items, totals | `Storefront\Checkout\Confirmation` |

---

## 2. Storefront REST API

**Prefix:** `/api/storefront/v1`
**Middleware:** `store.resolve`, `throttle:api.storefront`
**Content-Type:** `application/json`

These endpoints power the interactive storefront experience (cart drawer, checkout flow, search autocomplete, analytics).

---

### 2.1 Cart Endpoints

#### POST /api/storefront/v1/carts


text:
Script completed
Wall time 0.1 seconds
Output:

text:
<?php

use App\Livewire\Admin\Analytics\Index as AdminAnalyticsIndex;
use App\Livewire\Admin\Apps\Index as AdminAppsIndex;
use App\Livewire\Admin\Apps\Show as AdminAppsShow;
use App\Livewire\Admin\Auth\ForgotPassword as AdminForgotPassword;
use App\Livewire\Admin\Auth\Login as AdminLogin;
use App\Livewire\Admin\Auth\ResetPassword as AdminResetPassword;
use App\Livewire\Admin\Collections\Create as AdminCollectionsCreate;
use App\Livewire\Admin\Collections\Edit as AdminCollectionsEdit;
use App\Livewire\Admin\Collections\Index as AdminCollectionsIndex;
use App\Livewire\Admin\Customers\Index as AdminCustomersIndex;
use App\Livewire\Admin\Customers\Show as AdminCustomersShow;
use App\Livewire\Admin\Dashboard as AdminDashboard;
use App\Livewire\Admin\Developers\Index as AdminDevelopersIndex;
use App\Livewire\Admin\Discounts\Form as AdminDiscountForm;
use App\Livewire\Admin\Discounts\Index as AdminDiscountsIndex;
use App\Livewire\Admin\Inventory\Index as AdminInventoryIndex;
use App\Livewire\Admin\Navigation\Index as AdminNavigationIndex;
use App\Livewire\Admin\Orders\Index as AdminOrdersIndex;
use App\Livewire\Admin\Orders\Show as AdminOrdersShow;
use App\Livewire\Admin\Pages\Create as AdminPagesCreate;
use App\Livewire\Admin\Pages\Edit as AdminPagesEdit;
use App\Livewire\Admin\Pages\Index as AdminPagesIndex;
use App\Livewire\Admin\Products\Form as AdminProductForm;
use App\Livewire\Admin\Products\Index as AdminProductsIndex;
use App\Livewire\Admin\Search\Settings as AdminSearchSettings;
use App\Livewire\Admin\Settings\General as AdminSettingsGeneral;
use App\Livewire\Admin\Settings\Shipping as AdminSettingsShipping;
use App\Livewire\Admin\Settings\Taxes as AdminSettingsTaxes;
use App\Livewire\Admin\Themes\Editor as AdminThemesEditor;
use App\Livewire\Admin\Themes\Index as AdminThemesIndex;
use App\Livewire\Storefront\Account\Addresses\Index as AccountAddresses;
use App\Livewire\Storefront\Account\Auth\ForgotPassword as CustomerForgotPassword;
use App\Livewire\Storefront\Account\Auth\Login as CustomerLogin;
use App\Livewire\Storefront\Account\Auth\Register as CustomerRegister;
use App\Livewire\Storefront\Account\Auth\ResetPassword as CustomerResetPassword;
use App\Livewire\Storefront\Account\Dashboard as AccountDashboard;
use App\Livewire\Storefront\Account\Orders\Index as AccountOrders;
use App\Livewire\Storefront\Account\Orders\Show as AccountOrderShow;
use App\Livewire\Storefront\Cart\Show as CartShow;
use App\Livewire\Storefront\Checkout\Confirmation as CheckoutConfirmation;
use App\Livewire\Storefront\Checkout\Show as CheckoutShow;
use App\Livewire\Storefront\Collections\Index as CollectionsIndex;
use App\Livewire\Storefront\Collections\Show as CollectionShow;
use App\Livewire\Storefront\Home;
use App\Livewire\Storefront\Pages\Show as PageShow;
use App\Livewire\Storefront\Products\Show as ProductShow;
use App\Livewire\Storefront\Search\Index as SearchIndex;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Route;

Route::view('dashboard', 'dashboard')
    ->middleware(['auth', 'verified'])
    ->name('dashboard');

require __DIR__.'/settings.php';

Route::get('/favicon.ico', fn (): \Symfony\Component\HttpFoundation\Response => response('', 204))->name('favicon');

Route::middleware('store.resolve')->group(function (): void {
    Route::livewire('/', Home::class)->name('home');
    Route::livewire('/collections', CollectionsIndex::class)->name('collections.index');
    Route::livewire('/collections/{handle}', CollectionShow::class)->name('collection.show');
    Route::livewire('/products/{handle}', ProductShow::class)->name('product.show');
    Route::livewire('/cart', CartShow::class)->name('cart.show');
    Route::livewire('/search', SearchIndex::class)->name('search');
    Route::livewire('/pages/{handle}', PageShow::class)->name('page.show');
    Route::livewire('/checkout/{checkoutId}', CheckoutShow::class)->name('checkout.show');
    Route::livewire('/checkout/{checkoutId}/confirmation', CheckoutConfirmation::class)->name('checkout.confirmation');
    Route::livewire('/account/login', CustomerLogin::class)->middleware('throttle:login')->name('account.login');
    Route::livewire('/account/register', CustomerRegister::class)->name('account.register');
    Route::livewire('/forgot-password', CustomerForgotPassword::class)->name('password.request');
    Route::livewire('/reset-password/{token}', CustomerResetPassword::class)->name('password.reset');
    Route::post('/account/logout', function (): \Illuminate\Http\RedirectResponse {
        Auth::guard('customer')->logout();
        request()->session()->invalidate();
        request()->session()->regenerateToken();

        return redirect()->route('home');
    })->name('account.logout');

    Route::middleware('auth:customer')->group(function (): void {
        Route::livewire('/account', AccountDashboard::class)->name('account.dashboard');
        Route::livewire('/account/orders', AccountOrders::class)->name('account.orders');
        Route::livewire('/account/orders/{orderNumber}', AccountOrderShow::class)->name('account.order.show');
        Route::livewire('/account/addresses', AccountAddresses::class)->name('account.addresses');
    });
});

Route::livewire('/admin/login', AdminLogin::class)->middleware('throttle:login')->name('admin.login');
Route::livewire('/admin/forgot-password', AdminForgotPassword::class)->middleware('throttle:login')->name('admin.password.request');
Route::livewire('/admin/reset-password/{token}', AdminResetPassword::class)->name('admin.password.reset');
Route::post('/admin/logout', function (): \Illuminate\Http\RedirectResponse {
    Auth::guard('web')->logout();
    request()->session()->invalidate();
    request()->session()->regenerateToken();

    return redirect()->route('admin.login');
})->name('admin.logout');

Route::prefix('admin')->middleware(['auth', 'verified', 'store.resolve', 'role.check:owner,admin,staff,support'])->group(function (): void {
    Route::livewire('/', AdminDashboard::class)->name('admin.dashboard');
    Route::livewire('/products', AdminProductsIndex::class)->name('admin.products.index');
    Route::livewire('/products/create', AdminProductForm::class)->middleware('role.check:owner,admin,staff')->name('admin.products.create');
    Route::livewire('/products/{product}/edit', AdminProductForm::class)->middleware('role.check:owner,admin,staff')->name('admin.products.edit');
    Route::livewire('/orders', AdminOrdersIndex::class)->name('admin.orders.index');
    Route::livewire('/orders/{order}', AdminOrdersShow::class)->name('admin.orders.show');
    Route::livewire('/customers', AdminCustomersIndex::class)->name('admin.customers.index');
    Route::livewire('/customers/{customer}', AdminCustomersShow::class)->name('admin.customers.show');
    Route::livewire('/discounts', AdminDiscountsIndex::class)->name('admin.discounts.index');
    Route::livewire('/discounts/create', AdminDiscountForm::class)->middleware('role.check:owner,admin,staff')->name('admin.discounts.create');
    Route::livewire('/discounts/{discount}/edit', AdminDiscountForm::class)->middleware('role.check:owner,admin,staff')->name('admin.discounts.edit');
    Route::livewire('/settings', AdminSettingsGeneral::class)->middleware('role.check:owner,admin')->name('admin.settings');
    Route::livewire('/settings/shipping', AdminSettingsShipping::class)->middleware('role.check:owner,admin')->name('admin.settings.shipping');
    Route::livewire('/settings/taxes', AdminSettingsTaxes::class)->middleware('role.check:owner,admin')->name('admin.settings.taxes');
    Route::livewire('/inventory', AdminInventoryIndex::class)->name('admin.inventory.index');
    Route::livewire('/collections', AdminCollectionsIndex::class)->name('admin.collections.index');
    Route::livewire('/collections/create', AdminCollectionsCreate::class)->middleware('role.check:owner,admin,staff')->name('admin.collections.create');
    Route::livewire('/collections/{collection}/edit', AdminCollectionsEdit::class)->middleware('role.check:owner,admin,staff')->name('admin.collections.edit');
    Route::livewire('/themes', AdminThemesIndex::class)->name('admin.themes.index');
    Route::livewire('/themes/{theme}/editor', AdminThemesEditor::class)->name('admin.themes.editor');
    Route::livewire('/pages', AdminPagesIndex::class)->name('admin.pages.index');
    Route::livewire('/pages/create', AdminPagesCreate::class)->middleware('role.check:owner,admin')->name('admin.pages.create');
    Route::livewire('/pages/{page}/edit', AdminPagesEdit::class)->middleware('role.check:owner,admin')->name('admin.pages.edit');
    Route::livewire('/navigation', AdminNavigationIndex::class)->name('admin.navigation.index');
    Route::livewire('/apps', AdminAppsIndex::class)->name('admin.apps.index');
    Route::livewire('/apps/{installation}', AdminAppsShow::class)->name('admin.apps.show');
    Route::livewire('/developers', AdminDevelopersIndex::class)->middleware('role.check:owner,admin')->name('admin.developers.index');
    Route::livewire('/analytics', AdminAnalyticsIndex::class)->name('admin.analytics.index');
    Route::livewire('/search/settings', AdminSearchSettings::class)->name('admin.search.settings');
});

--- livewire components ---
app/Livewire/Actions/Logout.php
app/Livewire/Admin/Analytics/Index.php
app/Livewire/Admin/Apps/Index.php
app/Livewire/Admin/Apps/Show.php
app/Livewire/Admin/Auth/ForgotPassword.php
app/Livewire/Admin/Auth/Login.php
app/Livewire/Admin/Auth/ResetPassword.php
app/Livewire/Admin/Collections/Create.php
app/Livewire/Admin/Collections/Edit.php
app/Livewire/Admin/Collections/Form.php
app/Livewire/Admin/Collections/Index.php
app/Livewire/Admin/Customers/Index.php
app/Livewire/Admin/Customers/Show.php
app/Livewire/Admin/Dashboard.php
app/Livewire/Admin/Developers/Index.php
app/Livewire/Admin/Discounts/Form.php
app/Livewire/Admin/Discounts/Index.php
app/Livewire/Admin/Inventory/Index.php
app/Livewire/Admin/Navigation/Index.php
app/Livewire/Admin/Orders/Index.php
app/Livewire/Admin/Orders/Show.php
app/Livewire/Admin/Pages/Create.php
app/Livewire/Admin/Pages/Edit.php
app/Livewire/Admin/Pages/Form.php
app/Livewire/Admin/Pages/Index.php
app/Livewire/Admin/Products/Form.php
app/Livewire/Admin/Products/Index.php
app/Livewire/Admin/Search/Settings.php
app/Livewire/Admin/Section.php
app/Livewire/Admin/Settings/Domains.php
app/Livewire/Admin/Settings/General.php
app/Livewire/Admin/Settings/Shipping.php
app/Livewire/Admin/Settings/Taxes.php
app/Livewire/Admin/Themes/Editor.php
app/Livewire/Admin/Themes/Index.php
app/Livewire/Settings/Appearance.php
app/Livewire/Settings/DeleteUserForm.php
app/Livewire/Settings/Password.php
app/Livewire/Settings/Profile.php
app/Livewire/Settings/TwoFactor.php
app/Livewire/Settings/TwoFactor/RecoveryCodes.php
app/Livewire/Storefront/Account/Addresses/Index.php
app/Livewire/Storefront/Account/Auth/ForgotPassword.php
app/Livewire/Storefront/Account/Auth/Login.php
app/Livewire/Storefront/Account/Auth/Register.php
app/Livewire/Storefront/Account/Auth/ResetPassword.php
app/Livewire/Storefront/Account/Dashboard.php
app/Livewire/Storefront/Account/Orders/Index.php
app/Livewire/Storefront/Account/Orders/Show.php
app/Livewire/Storefront/Cart/Show.php
app/Livewire/Storefront/CartDrawer.php
app/Livewire/Storefront/Checkout/Confirmation.php
app/Livewire/Storefront/Checkout/Show.php
app/Livewire/Storefront/Collections/Index.php
app/Livewire/Storefront/Collections/Show.php
app/Livewire/Storefront/Home.php
app/Livewire/Storefront/Pages/Show.php
app/Livewire/Storefront/Products/Show.php
app/Livewire/Storefront/Search/Index.php
app/Livewire/Storefront/Search/Modal.php

--- spec route tables ---

> **Note:** The Create and Edit routes render the same Livewire form component (`ProductForm`, `OrderForm`, etc.) in create vs edit mode, following the shared-form pattern defined in Spec 03.

#### Dashboard

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin` | Dashboard with KPI tiles, orders chart, conversion funnel | `Admin\Dashboard` |

#### Products

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/products` | Product list - searchable, filterable by status, bulk actions | `Admin\Products\Index` |
| GET | `/admin/products/create` | Create product form | `Admin\Products\Create` |
| GET | `/admin/products/{product}/edit` | Edit product form with variants, media, options | `Admin\Products\Edit` |

#### Inventory

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/inventory` | Inventory management page | `Admin\Inventory\Index` |

#### Collections

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/collections` | Collection list | `Admin\Collections\Index` |
| GET | `/admin/collections/create` | Create collection form | `Admin\Collections\Create` |
| GET | `/admin/collections/{collection}/edit` | Edit collection form | `Admin\Collections\Edit` |

#### Orders

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/orders` | Order list with status filters | `Admin\Orders\Index` |
| GET | `/admin/orders/{order}` | Order detail with timeline, payments, fulfillments | `Admin\Orders\Show` |

#### Customers

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/customers` | Customer list | `Admin\Customers\Index` |
| GET | `/admin/customers/{customer}` | Customer detail with order history, addresses | `Admin\Customers\Show` |

#### Discounts

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/discounts` | Discount list | `Admin\Discounts\Index` |
| GET | `/admin/discounts/create` | Create discount form with rules builder | `Admin\Discounts\Create` |
| GET | `/admin/discounts/{discount}/edit` | Edit discount form | `Admin\Discounts\Edit` |

#### Settings

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/settings` | Store settings (tabs: General, Domains, Shipping, Taxes, Checkout, Notifications) | `Admin\Settings\Index` |
| GET | `/admin/settings/shipping` | Shipping zones and rates configuration | `Admin\Settings\Shipping` |
| GET | `/admin/settings/taxes` | Tax settings - mode, provider config, rates | `Admin\Settings\Taxes` |

#### Themes and Online Store

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/themes` | Theme management - cards with actions (Customize, Preview, Publish, Duplicate, Delete) | `Admin\Themes\Index` |
| GET | `/admin/themes/{theme}/editor` | Theme editor - left panel sections, center preview iframe, right panel settings | `Admin\Themes\Editor` |

#### Pages

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/pages` | Content pages list | `Admin\Pages\Index` |
| GET | `/admin/pages/create` | Create content page | `Admin\Pages\Create` |
| GET | `/admin/pages/{page}/edit` | Edit content page | `Admin\Pages\Edit` |

#### Navigation

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/navigation` | Navigation menus management | `Admin\Navigation\Index` |

#### Apps

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/apps` | Apps directory - list installed and available apps | `Admin\Apps\Index` |
| GET | `/admin/apps/{installation}` | Installed app detail - scopes, webhooks, usage | `Admin\Apps\Show` |

#### Developers

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/developers` | Developer settings - API keys/tokens, webhook management | `Admin\Developers\Index` |

> **Note:** Webhook subscription management (CRUD) is available exclusively through the admin UI at `/admin/developers`. There are no REST API endpoints for programmatic webhook subscription management in the initial implementation.

#### Analytics

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/analytics` | Analytics dashboard - sales, traffic, funnel, exports | `Admin\Analytics\Index` |

#### Search Settings

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/search/settings` | Search settings - synonyms, stopwords, reindex | `Admin\Search\Settings` |

---

### 1.3 Storefront Routes

**Middleware:** `web`, `store.resolve`

These routes serve the public-facing storefront using Blade templates rendered through the active theme engine.

| Method | Path | Description | Component / View |
|--------|------|-------------|------------------|
| GET | `/` | Home page | `Storefront\Home` |
| GET | `/collections` | All collections listing | `Storefront\Collections\Index` |
| GET | `/collections/{handle}` | Single collection page with product grid and filters | `Storefront\Collections\Show` |
| GET | `/products/{handle}` | Product page - gallery, variant selector, add-to-cart | `Storefront\Products\Show` |
| GET | `/cart` | Full cart page - line items, quantity controls, discount input, totals | `Storefront\Cart\Show` |
| GET | `/search` | Search results page with filter panel, sort dropdown, product grid | `Storefront\Search\Index` |
| GET | `/pages/{handle}` | Static content page (About, FAQ, etc.) | `Storefront\Pages\Show` |

#### Customer Account Routes

**Additional middleware:** `auth:customer`

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/account` | Customer account overview | `Storefront\Account\Dashboard` |
| GET | `/account/orders` | Order history list | `Storefront\Account\Orders\Index` |
| GET | `/account/orders/{orderNumber}` | Order detail | `Storefront\Account\Orders\Show` |
| GET | `/account/addresses` | Address book management | `Storefront\Account\Addresses\Index` |

#### Customer Auth Routes (no auth required)

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/account/login` | Customer login page | `Storefront\Account\Auth\Login` |
| GET | `/account/register` | Customer registration page | `Storefront\Account\Auth\Register` |
| GET | `/forgot-password` | Forgot password page | `Storefront\Account\Auth\ForgotPassword` |
| POST | `/forgot-password` | Send reset link | Handled by Livewire action |
| GET | `/reset-password/{token}` | Reset password form | `Storefront\Account\Auth\ResetPassword` |
| POST | `/reset-password` | Reset password | Handled by Livewire action |

---

### 1.4 Checkout Routes

**Middleware:** `web`, `store.resolve`

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/checkout/{checkoutId}` | Checkout page - stepper UI (Contact -> Shipping -> Payment) | `Storefront\Checkout\Show` |
| GET | `/checkout/{checkoutId}/confirmation` | Order confirmation page - order number, items, totals | `Storefront\Checkout\Confirmation` |

---

## 2. Storefront REST API

**Prefix:** `/api/storefront/v1`
**Middleware:** `store.resolve`, `throttle:api.storefront`
**Content-Type:** `application/json`

These endpoints power the interactive storefront experience (cart drawer, checkout flow, search autocomplete, analytics).

---

### 2.1 Cart Endpoints

#### POST /api/storefront/v1/carts


exec

status: completed

input:
const r = await tools.multi_agent_v1__wait_agent({targets:["01a0219c-6293-7b40-b5e1-7376c4c0fe0d"], timeout_ms:30000});
text(r);


output:
Script running with cell ID 996
Wall time 11.0 seconds
Output:


output:
Script running with cell ID 996
Wall time 11.0 seconds
Output:


text:
Script completed
Wall time 4.3 seconds
Output:


status: completed

input:
const r = await tools.multi_agent_v1__send_input({
  target: "01a0219c-6293-7b40-b5e1-7376c4c0fe0d",
  message: "Please conclude the read-only audit now with a concise report of any critical/major issues found, or explicitly state none remain. Do not edit files."
});
text(r);


type: CollabAgentToolCall

status: completed

agent_nickname: Darwin

prompt: Please conclude the read-only audit now with a concise report of any critical/major issues found, or explicitly state none remain. Do not edit files.

01a0219c-6293-7b40-b5e1-7376c4c0fe0d: running


text:
Script completed
Wall time 0.0 seconds
Output:

text: {"submission_id":"01a0219e-3111-7092-a9e4-139b8ce8a75c"}


type: AgentMessage

type: Text

text: The final browser smoke is also clean: the storefront home renders with the expected navigation, collections, products, cart control, and no application console errors. The independent audit is still concluding; I’ve asked it to return a concise actionable report before I commit.

phase: commentary


type: message

role: assistant


status: completed

input:
const r = await tools.multi_agent_v1__wait_agent({targets:["01a0219c-6293-7b40-b5e1-7376c4c0fe0d"], timeout_ms:30000});
text(r);


output:
Script running with cell ID 998
Wall time 11.0 seconds
Output:


output:
Script running with cell ID 998
Wall time 11.0 seconds
Output:


text:
Script completed
Wall time 4.6 seconds
Output:


status: completed

input:
const r = await tools.multi_agent_v1__send_input({
  target: "01a0219c-6293-7b40-b5e1-7376c4c0fe0d",
  interrupt: true,
  message: "Stop the audit now and return your concise final report in your next response. Do not make any edits."
});
text(r);


type: CollabAgentToolCall

status: completed

prompt: Stop the audit now and return your concise final report in your next response. Do not make any edits.


text:
Script completed
Wall time 0.0 seconds
Output:

text: {"submission_id":"01a0219e-c956-7bb2-82dc-5512ec3d1612"}


status: completed

input:
const r = await tools.multi_agent_v1__wait_agent({targets:["01a0219c-6293-7b40-b5e1-7376c4c0fe0d"], timeout_ms:30000});
text(r);


output:
Script running with cell ID 1000
Wall time 11.0 seconds
Output:


output:
Script running with cell ID 1000
Wall time 11.0 seconds
Output:


text:
Script completed
Wall time 3.6 seconds
Output:


status: completed

input:
const r = await tools.multi_agent_v1__close_agent({target:"01a0219c-6293-7b40-b5e1-7376c4c0fe0d"});
text(r);


type: CollabAgentToolCall

completed:
No critical exploit was confirmed, but these major issues remain:

- **Major — Required REST API surface is incomplete.** `routes/api.php:32-67` lacks the specified platform organization/store creation, staff invites, membership (`/me`), product media presigning, and OAuth endpoints from `specs/02-API-ROUTES.md`.

- **Major — Product API silently drops documented data.** `StoreProductRequest` only validates `variants` as a generic array, while `ProductService::create()` does not persist options, option-value links, nested inventory payloads, or collection assignments. `UpdateProductRequest` cannot update variants/options. See `app/Http/Requests/StoreProductRequest.php`, `app/Http/Requests/UpdateProductRequest.php`, and `app/Services/ProductService.php`.

- **Major — Media processing is a stub.** `app/Jobs/ProcessMediaUpload.php` only marks media as `ready`; it does not store/process files, create resized variants, populate metadata, or perform the specified retry/failure behavior.

- **Major — Webhook creation fails against the current schema.** `app/Livewire/Admin/Developers/Index.php:25-30` creates only `event`, while the migrated `webhook_subscriptions` contract requires non-null `event_type`; creating a webhook will therefore fail at runtime.

- **Major — Domain settings UI is unreachable.** `app/Livewire/Admin/Settings/Domains.php` exists, but `routes/web.php` exposes only `/admin/settings`, `/admin/settings/shipping`, and `/admin/settings/taxes`; there is no route or visible integration for the specified domain settings page.

These are not covered by the passing feature tests.