This document defines the formal behavior specification for all Phase 3 Security Guard drivers.
It is intended for:
- Contributors
- Maintainers
- Advanced integrators
- Developers writing extensions or custom drivers
- Anyone needing deterministic, cross-datastore guarantees
This is NOT a usage guide.
(Usage examples are in docs/examples/phase3_driver_usage.md.)
All Security Guard drivers must implement identical semantics, regardless of datastore.
SecurityGuardService
→ SecurityGuardDriver (Phase 3)
→ AdapterInterface (maatify/data-adapters)
Phase 3 drivers never interact with:
- PDO directly
- Redis clients directly
- MongoDB clients directly
All storage actions MUST pass through:
MySQLAdapterRedisAdapterMongoAdapter
This ensures:
- Deterministic behavior
- Testability
- Real/Fake symmetry
- Storage abstraction
Every driver accepts:
new RedisSecurityGuard($adapter, 'login.guard');The identifier MUST be used as:
- The namespace for attempts
- The namespace for blocks
- The namespace for stats keys
Drivers MUST NOT change identifier format.
Each driver MUST implement the following behaviors identically.
ipsubjectresetAfter(seconds)userAgent(string|null)
Returns the new attempt count after increment.
- Combine IP + subject into a unique key.
- Increment failure counter atomically.
- Apply TTL equal to
resetAfter. - If TTL already active, refresh it.
- Return current count.
- TTL expiration MUST delete the attempts record.
| Behavior | MySQL | Redis | Mongo |
|---|---|---|---|
| Counter increment | UPDATE/INSERT | atomic INCR | atomic increment |
| TTL | manual (timestamp comparison) | EXPIRE | TTL index |
| Atomicity | transaction-safe | built-in | built-in |
| ResetAfter refresh | YES | YES | YES |
- Completely removes the attempts record.
- Operation MUST be idempotent.
| Driver | Behavior |
|---|---|
| MySQL | DELETE row |
| Redis | DEL key |
| Mongo | deleteOne document |
-
Store full block payload:
- ip
- subject
- type
- createdAt
- expiresAt (nullable)
-
Drivers MUST store block payload deterministically.
-
Field names MUST NOT change.
-
Types MUST be preserved:
inttimestampsstringtypestring|nullexpiresAt
| Scenario | Expected Behavior |
|---|---|
expiresAt = null |
permanent block |
expiresAt > now |
TTL applied |
| expired block | MUST be auto-removed on read |
- If block exists → remove it
- If block does not exist → no error
| Driver | Removal Behavior |
|---|---|
| MySQL | DELETE row |
| Redis | DEL key |
| Mongo | deleteOne |
[
'attempts' => int,
'blocked' => bool,
'block_expires_at' => int|null
]- Remove expired blocks before returning stats.
- Expired counters (TTL) MUST appear as 0 attempts.
- Value types MUST be:
| Field | Type |
|---|---|
| attempts | int |
| blocked | bool |
| block_expires_at | int or null |
| Driver | Implementation |
|---|---|
| MySQL | stored timestamp; expiration checked manually |
| Redis | EXPIRE key with exact TTL |
| Mongo | TTL index deletes expired documents |
- All drivers MUST treat TTL expiration as complete removal.
- TTL MUST refresh on every failed attempt.
- MUST persist indefinitely.
- MUST NOT apply TTL.
- MUST NOT auto-expire.
-
MUST apply TTL if supported natively (Redis, Mongo).
-
For MySQL:
- Expired blocks MUST be deleted during
getStats().
- Expired blocks MUST be deleted during
Even though this document only discusses real drivers:
-
The fake drivers MUST replicate all behaviors described here.
-
Differences allowed only in:
- microsecond timing
- internal storage engine
Resolver MUST map adapters to drivers:
| AdapterInstance | DriverClass |
|---|---|
| MySQLAdapter | MySQLSecurityGuard |
| RedisAdapter | RedisSecurityGuard |
| MongoAdapter | MongoSecurityGuard |
Behavior:
if ($adapter instanceof RedisAdapter) {
return new RedisSecurityGuard($adapter, $identifier);
}Unsupported adapter MUST throw UnsupportedDriverException.
- No driver may silently swallow storage errors.
- Every operation MUST be idempotent where documented.
- Expired blocks MUST be cleaned before reporting stats.
- Encoded block payload MUST be identical across drivers.
- Attempt counters MUST always be integers.
- No driver may store additional fields beyond the specification.
- Identifier MUST NOT be altered internally.
These illustrate expected behavior, not code usage.
CreatedAt: 1700000000
ExpiresAt: 1700003600
Now: 1700007200
Expected:
- Block is treated as non-existent
- Driver MUST remove it
getStats()returns:
blocked = false
block_expires_at = null
resetAfter = 900
LastAttemptAt = t0
Now = t0 + 901
Expected:
- Attempts MUST be treated as 0
- Driver MUST auto-remove attempts record
This specification defines:
- Required behavior for all Phase 3 drivers
- Cross-datastore normalization rules
- TTL and expiration logic
- Block persistence semantics
- Deterministic storage encoding
- Invariants required for Phase 4+
All future driver implementations MUST conform to this document.