# spam-shield

Canonical, proprietary spam-protection library for CompEdge/BST PHP projects. Distributed as a private Composer package so every consuming site can pull the same source of truth instead of carrying its own copy-pasted fork.

## Install

Add the private repository and require the package:

```json
{
    "repositories": [
        {"type": "vcs", "url": "git@github.com:SDMikel/spam-shield.git"}
    ],
    "require": {
        "compedgesolutions/spam-shield": "^1.0"
    }
}
```

```bash
composer require compedgesolutions/spam-shield
```

Every host in the fleet (desktop, DigitalOcean production, Mo, Larry, Curly, Shemp) already has an account-level SSH key registered on the `SDMikel` GitHub account, so the `git@github.com:...` URL resolves with no extra key setup.

## Updating a site that already depends on this package

Propagation is **manual and per-site by design** — a bad rule in this library should never break every consuming site's contact form at once.

```bash
composer update compedgesolutions/spam-shield
```

Then verify locally and redeploy that site on its own schedule.

## API

All functions are global (not namespaced) and prefixed `sp_` to avoid collisions. Call them in this order inside a form handler:

1. `sp_honeypot_tripped(string $field = 'website'): bool` — hidden field; bots fill it, humans never do.
2. `sp_timing_ok(int $minSeconds = 4, int $maxSeconds = 14400, string &$reason = ''): bool` — HMAC-signed timestamp; rejects too-fast POSTs. Pair with `sp_render_fields()` in the form template.
3. `sp_rate_ok(string $bucket = 'form', int $max = 5, int $window = 3600): bool` — per-IP sliding-window rate limit, file-based, fail-open.
4. `sp_spam_score(array $fields, array $extraKeywords = [], string $siteType = ''): int` — weighted keyword, scam-phrase, suspicious-link-host, and structural-signal scoring. `$siteType` is one of `service` | `ministry` | `saas` | `rental` for a preset keyword overlay; pass site-specific terms via `$extraKeywords` as `['phrase' => weight]`. Callers typically block at score >= 5.
5. `sp_recaptcha_verify(?string $token, string $expectedAction = '', float $threshold = 0.5): array` — reCAPTCHA v3; gracefully skips (returns `ok: true, skipped: true`) if `RECAPTCHA_SECRET_KEY` isn't set, so you can deploy without keys and add them later with no code change.
6. `sp_random_fields_ok(array $fields, int $maxHits = 2): bool` — rejects gibberish CamelCase bot filler.

Optional: `sp_email_domain_ok(string $email): bool` — MX/A record check, adds DNS latency, use judiciously.

## Silent-fail rule

Honeypot, timing, spam-score, and random-field hits MUST return a fake success to the visitor — never reveal which check failed. Rate-limit blocks may show a real user-facing error since they can legitimately happen to real users. reCAPTCHA failures are a spam-score input, not a hard block.

## Fail-open guarantee

Every layer that writes to disk or calls an external service (rate limiter, reCAPTCHA) allows the submission through if it cannot complete its check. A server-side write failure or a Google outage will never block a legitimate submission.

## Configuration

Override before the package is autoloaded, via `define()` or environment variables:

- `SP_RATE_DIR` — directory for per-IP rate-limit JSON files. Defaults to `sys_get_temp_dir() . '/sp_rate_limits'`.
- `SP_HMAC_KEY` — HMAC key for the timing token. Falls back to `APP_SECRET` env var, then `$_SERVER['SERVER_NAME']`.
- `RECAPTCHA_SECRET_KEY` (env var) — enables `sp_recaptcha_verify()`. Unset means the check is skipped, not failed.

See `MIGRATION.md` for the step-by-step recipe to onboard an existing site.
