xaniashield/laravel

Privacy-first, EU-hosted spam protection for Laravel forms. Official Laravel client for the Xania Shield API.

Maintainers

Package info

github.com/xaniacode/xaniashield-laravel

Homepage

pkg:composer/xaniashield/laravel

Transparency log

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-07-15 05:20 UTC

This package is auto-updated.

Last update: 2026-07-15 05:26:42 UTC


README

Privacy-first, EU-hosted spam protection for Laravel forms. This is the official Laravel client for the Xania Shield API — block bots and spam across your forms without CAPTCHAs, without Google, and without sending visitor data to third-party AI services.

  • No CAPTCHAs — works invisibly in the background
  • EU-hosted & GDPR-friendly — local statistical analysis, no Big Tech
  • Fail-open by design — if the API is unreachable, your forms keep working
  • Drop-in — one line per form, with optional honeypot and timing signals

Requirements

  • PHP 8.1+
  • Laravel 10, 11, 12, or 13
  • A Xania Shield API key (create one free at app.xaniashield.com)

Installation

composer require xaniashield/laravel

The service provider and Shield facade are auto-discovered — no manual registration needed.

Publish the config file (optional):

php artisan vendor:publish --tag=shield-config

Add your credentials to .env:

SHIELD_API_KEY=xshd_live_your_key_here
SHIELD_FAIL_OPEN=true
# Optional, only if you use the timing signal:
SHIELD_TIMING_SECRET=a-long-random-string

Usage

Basic — analyze a submission

Inject the client (or use the Shield facade) in any controller:

use XaniaShield\Laravel\ShieldClient;

public function submit(Request $request, ShieldClient $shield)
{
    $verdict = $shield->checkRequest($request, [
        'email'           => $request->input('email'),
        'content'         => $request->input('message'),
        'form_identifier' => 'contact',
    ]);

    if ($verdict->isBlocked()) {
        // Silently drop, show an error, whatever fits your form.
        return back()->with('status', 'Message sent.');
    }

    // allow or challenge — proceed normally
}

With the facade

use XaniaShield\Laravel\Facades\Shield;

$verdict = Shield::checkRequest($request, [
    'email'           => $request->input('email'),
    'form_identifier' => 'newsletter',
]);

The verdict object

$verdict->action();        // 'allow' | 'challenge' | 'block'
$verdict->score();         // 0-100
$verdict->isAllowed();     // bool
$verdict->isBlocked();     // bool
$verdict->shouldChallenge(); // bool
$verdict->reasons();       // string[]
$verdict->requestId();     // string|null
$verdict->failedOpen();    // bool — true if the API was unreachable

Honeypot

A honeypot is a hidden field that bots fill and humans never touch. Add one to your form:

<input type="text" name="website_url" tabindex="-1" autocomplete="off"
       style="position:absolute;left:-9999px" aria-hidden="true">

The client reads the configured honeypot field automatically (default website_url). To use a different field name, pass it explicitly:

$verdict = $shield->checkRequest($request, [
    'email'          => $request->input('email'),
    'honeypot_field' => 'my_hp_field',
    'honeypot_value' => $request->input('my_hp_field', ''),
]);

Timing signal (optional)

The timing signal measures how long a form took to fill — bots submit near-instantly. It requires SHIELD_TIMING_SECRET to be set.

Render a signed timestamp field in your form:

{!! app(\XaniaShield\Laravel\ShieldClient::class)->timingField() !!}

The client verifies and includes the elapsed time automatically on the next checkRequest().

Fail-open vs fail-closed

By default (SHIELD_FAIL_OPEN=true), if the API is unreachable, times out, or errors, submissions are allowed — protection never breaks your forms. Set SHIELD_FAIL_OPEN=false to block on uncertainty instead (stricter, but a Shield outage would block submissions).

Health check

Shield::configured(); // has an API key + https base URL
Shield::health();     // API reachable and key valid

Configuration reference

All values are read from config/shield.php (env-driven):

Key Env Default Purpose
api_key SHIELD_API_KEY '' Your site API key
base_url SHIELD_BASE_URL https://xaniashield.com/v1 API base URL
timeout SHIELD_TIMEOUT 5 Request timeout (seconds)
fail_open SHIELD_FAIL_OPEN true Allow on API failure
timing_secret SHIELD_TIMING_SECRET '' HMAC secret for timing
challenge_threshold SHIELD_CHALLENGE_THRESHOLD 40 Score for challenge
block_threshold SHIELD_BLOCK_THRESHOLD 70 Score for block
honeypot_field SHIELD_HONEYPOT_FIELD website_url Honeypot field name
timing_field SHIELD_TIMING_FIELD xsh_tf Timing field name

Integration recipes

Forms differ across projects — field names, honeypots, Livewire vs controllers. Pick the entry point that fits. All of them ultimately call the same engine; choose by how much control you want.

1. Middleware (simplest — protect a whole route)

For standard forms with email / message / name / subject fields:

Route::post('/contact', [ContactController::class, 'submit'])
    ->middleware('shield:contact');

On a block verdict it aborts with HTTP 422 before reaching your controller. No controller changes needed. For non-standard field names, use one of the options below instead.

2. Validation rule (idiomatic — fits existing validation)

use XaniaShield\Laravel\Rules\ShieldRule;

$request->validate([
    'email'   => ['required', 'email', new ShieldRule('contact')],
    'message' => ['required', 'string', 'max:5000'],
]);

Attach the rule to one field only (usually email) — it analyses the whole request, not just that field. A block fails validation with a generic message you can customise: new ShieldRule('contact', 'Your message looks like spam.').

3. Controller call (most control — custom field mapping)

When your fields are non-standard, map them explicitly:

use XaniaShield\Laravel\Facades\Shield;

public function submit(Request $request)
{
    $verdict = Shield::checkRequest($request, [
        'email'           => $request->input('contact_email'),   // custom name
        'content'         => $request->input('enquiry_body'),
        'form_identifier' => 'enquiry',
        'honeypot_field'  => 'company_website',                  // custom honeypot
        'honeypot_value'  => $request->input('company_website', ''),
    ]);

    if ($verdict->isBlocked()) {
        return back()->withErrors(['enquiry_body' => 'Could not send. Please try again.']);
    }

    // proceed
}

4. Livewire component

use XaniaShield\Laravel\ShieldClient;

public function submit(ShieldClient $shield)
{
    $verdict = $shield->analyze([
        'email'           => $this->email,
        'content'         => $this->message,
        'form_identifier' => 'contact',
        'visitor_ip'      => request()->ip(),
    ]);

    if ($verdict->isBlocked()) {
        $this->addError('message', 'Could not send. Please try again.');
        return;
    }

    // proceed
}

Livewire has no per-submit HTTP request for the form fields, so pass values from the component state and add visitor_ip explicitly.

5. Form Request class

use Illuminate\Foundation\Http\FormRequest;
use XaniaShield\Laravel\Rules\ShieldRule;

class ContactRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'email'   => ['required', 'email', new ShieldRule('contact')],
            'message' => ['required', 'string'],
        ];
    }
}

6. API endpoint (JSON)

$verdict = Shield::checkRequest($request, [
    'email'           => $request->input('email'),
    'content'         => $request->input('body'),
    'form_identifier' => 'api-contact',
]);

if ($verdict->isBlocked()) {
    return response()->json(['message' => 'Rejected as spam.'], 422);
}

Honeypot & timing in Blade

Add a honeypot (and optionally a timing field) to any form with directives:

<form method="POST" action="/contact">
    @csrf
    @shieldHoneypot
    @shieldTiming   {{-- only renders if SHIELD_TIMING_SECRET is set --}}

    <input type="email" name="email">
    <textarea name="message"></textarea>
    <button>Send</button>
</form>

@shieldHoneypot renders a hidden field named after config('shield.honeypot_field'). The client reads it back automatically.

License

MIT — see LICENSE.