Download the PHP package zeroad.network/token without Composer
On this page you can find all versions of the php package zeroad.network/token. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download zeroad.network/token
More information about zeroad.network/token
Files in zeroad.network/token
Package token
Short Description An HTTP-header-based "access / entitlement token" library for Zero Ad Network partnering sites using PHP
License Apache-2.0
Homepage http://zeroad.network
Informations about the package token
zeroad.network/token (PHP)
Verify Zero Ad Network subscriber tokens in your PHP backend. Offline, with no
dependencies beyond ext-sodium and no calls back to us.
This is the PHP port of @zeroad.network/token.
It speaks the exact same wire format, so a token minted by the platform verifies identically on either.
The thirty second version
Zero Ad Network subscribers pay a monthly fee and install a browser extension. When one of them visits your site, the extension attaches a cryptographically signed token. You verify it locally, and if it checks out you owe that visitor a clean page - no ads, no trackers, no cookie dialog, no paywall. Your share of their subscription is paid out monthly based on the time they actually spent with you.
Two headers, and this package handles both ends:
| Direction | Header | Carries |
|---|---|---|
| You -> visitor | Better-Web-Publisher |
your publisher ID, so the visit can be credited |
| Visitor -> you | Better-Web-Token |
their signed, origin-bound subscription token |
Requirements
| Runtime | Version | Ready |
|---|---|---|
| PHP 7 | 7.2+ | ✅ |
| PHP 8 | 8.0+ | ✅ |
ext-sodium (bundled with PHP since 7.2) is the only dependency.
Integrate
1. Register
Sign up, add your site, and copy your publisher ID (zapub_...).
2. Create a publisher, once, at startup
hostnames is every host you serve. It is required, and it matters - see
why hostnames are a whitelist. Listing an apex covers its www (and
vice versa), so "example.com" already admits www.example.com.
3. Wire up one middleware
Two things happen on every request: announce participation on the response, and verify the token on the
request. PHP exposes the request header under a $_SERVER key, which $publisher->tokenHeaderServerKey
gives you.
4. Branch on it
That is the whole integration. A working example lives in examples/.
Set
Better-Web-Publishereven on pages where you never read a token. It is how the extension discovers that your site takes part at all, and how visits get attributed to you.
API
Publisher::create(array $options)
| Option | Type | Default | |
|---|---|---|---|
publisherId |
string |
- | From your dashboard. zapub_ followed by 24 alphanumerics. |
hostnames |
string\|array |
- | Every host you serve; an apex covers its www. Ports, schemes and paths are stripped. |
publicKey |
string |
platform key | Override for staging and tests. Leave alone in production. |
clockToleranceSeconds |
int |
60 |
Slack on expiry, for servers whose clocks drift. |
cache |
bool\|array |
on | See caching. false disables it. |
Returns a Publisher you keep for the life of the process:
$publisher->headerName / ->headerValue |
"Better-Web-Publisher" and "zapub_..." |
$publisher->header |
["Better-Web-Publisher", "zapub_..."] |
$publisher->tokenHeaderName |
"Better-Web-Token" |
$publisher->tokenHeaderNameLowercase |
"better-web-token" |
$publisher->tokenHeaderServerKey |
"HTTP_BETTER_WEB_TOKEN", the $_SERVER key |
$publisher->verify($token, $hostname?) |
VerificationResult |
$publisher->cacheStats() |
["size", "maxSize", "hits", "misses", "evictions"] |
$publisher->clearCache() |
drops every cached verdict |
$publisher->verify($token, $hostname = null)
Takes the raw header value - a string, an array (some stacks hand back an array for a repeated
header; the first wins), or null. Never throws on bad input; a junk token is a result, not an
exception.
The hostname may be omitted when exactly one was configured. Pass the request's host when you serve several - a host outside your whitelist is rejected, never trusted.
Returns a VerificationResult. subscriber says which branch you are in:
$visitor->toArray() gives a JSON-friendly copy (with expiresAt as a unix timestamp).
The only case verify() throws is when several hostnames are configured and none is passed.
Rejection
Worth logging. Most are ordinary; two are not.
Reason (Rejection::) |
Means | Ordinary? |
|---|---|---|
MISSING |
No token header. Most of your traffic. | yes |
MALFORMED |
Not a well-formed token. | yes |
UNSUPPORTED_VERSION |
A newer token format. Upgrade this package. | yes, but see below |
EXPIRED |
Genuine, but past its expiry. | yes |
UNKNOWN_HOSTNAME |
The host asked for is not in your whitelist. | check your config |
WRONG_HOSTNAME |
A genuine token minted for a different site. | somebody is replaying tokens |
FORGED |
Not signed by Zero Ad Network. | somebody is minting tokens |
When a token arrives whose version is newer than this package understands, it is rejected as
UNSUPPORTED_VERSION and a line is written to the PHP error log telling you an upgrade is due. To
silence it - during a staged rollout, or in tests that feed such tokens on purpose - call
ZeroAd\Token\VersionWarning::suppress() once at startup.
This package only verifies. Nothing here can mint a token - that requires a private key that never leaves the platform.
Caching
A subscriber's token stays the same all day, so a returning visitor sends bytes you have already checked. Verifying once and remembering the answer turns a pair of elliptic-curve operations into a map lookup. It is on by default and there is rarely a reason to touch it.
| Option | Default | |
|---|---|---|
enabled |
true |
|
ttl |
600000 |
milliseconds a verdict is trusted |
maxSize |
1000 |
entries (memory store only; APCu manages its own memory) |
store |
"memory" |
"memory", "apcu", or "auto" - where verdicts live (see below) |
prefix |
see below | namespaces the APCu keys; ignored by the memory store |
Three things it does that are worth knowing about:
Failures are cached too. A forged token costs exactly as much to reject as a real one costs to accept, and whoever sends it is likely to send it again. This is safe because, for a fixed public key, a rejection can never later become an acceptance.
A success never outlives the token. The stored expiry is the earlier of your TTL and the token's own
expiresAt, so a generous TTL cannot extend anybody's subscription.
Cheap rejections are not cached. A malformed, missing or expired token is thrown out by a length or byte check. Caching those would save nothing and would hand anyone who can send a request an easy way to fill memory with distinct keys.
The default memory store is per Publisher instance and lives for the life of the PHP process.
Under PHP-FPM that means per worker - it is not shared across workers, which is exactly why the
publisher is created once at startup and reused, never per request. Entries are evicted
least-used-first, oldest breaking ties.
Sharing verdicts across requests with APCu
On a classic PHP stack a fresh process handles each request, so the memory store starts empty every
time and a returning visitor's identical token is re-verified from scratch. Point store at APCu and the
verdict computed by one request is there for the next, across the whole FPM pool:
"apcu" requires the APCu extension; if it is not
available the publisher logs a line and falls back to the memory store, so a token is still verified,
just not shared. "auto" picks APCu when present and memory otherwise, silently - a good default for code
that ships to hosts you do not control. The prefix namespaces the keys so several sites sharing one
APCu segment do not collide, and clearCache() removes only keys under that prefix. With the APCu store,
cacheStats() reports evictions as 0 (APCu evicts under its own memory pressure) and maxSize is
advisory.
How the token works
You do not need this to integrate. You may want it before you trust it.
A token is 174 bytes, 232 base64url characters, and carries two Ed25519 signatures.
The platform signs a batch credential. Once a day, the extension generates a batch of throwaway keypairs locally and sends the public halves to us. We check the subscription is live, sign each one together with the plan and an expiry truncated to midnight UTC, and send them back. We never see the private halves, and the shared midnight expiry puts every subscriber in one anonymity set.
The extension binds one to your hostname. Offline, the first time it meets example.com it takes an
unused keypair and signs your hostname with the private half, then reuses that bound token until it
expires.
You verify both signatures. The first proves the platform issued the credential for a live subscription. The second proves it was minted for your host.
The hostname is deliberately absent from the wire. Your server already knows what it serves and rebuilds the signed message from that, so a token bound elsewhere simply fails the signature.
Why hostnames are a whitelist
hostnames is required, and verify() will not fall back to whatever arrived in the Host header,
because tokens are bound to a hostname and Host is set by the client. Without the whitelist an attacker
could bind a token to a domain they control, send it with Host: that-domain.example, and be admitted.
Listing your hosts removes the possibility.
www.example.com and example.com are technically different hosts, but listing either admits both, so a
site that serves both needs only one in the list. The signature is still checked against the exact host
each request arrives on.
Framework examples
Laravel
Symfony
WordPress
Performance
Measured on PHP 8.5, Apple Silicon, single core, libsodium 1.0.22, via php benchmarks/verify.php:
| Cold verification, end to end | 88us, about 11,300/s |
| Cached verdict | 0.95us, about 1,050,000/s |
| Malformed token | about 1.3us, rejected on length before it is decoded |
Verification is two Ed25519 checks through ext-sodium (sodium_crypto_sign_verify_detached), so the
cold cost tracks libsodium and is in the same ballpark as the TypeScript SDK. At ~0.09ms it is already a
rounding error next to a database query or a template render.
The one thing worth knowing is PHP's process model. The default memory cache lives only for the request
that filled it, so on a classic PHP-FPM stack the cached number above applies within a request, not
across them - a returning visitor's identical token is re-verified cold on the next page load. Point the
cache at APCu ("cache" => ["store" => "apcu"], see Caching) to turn that repeat into a
shared-memory lookup across the whole worker pool.
Troubleshooting
Every visitor comes back MISSING. Expected - only subscribers send a token. Confirm the pipe works
by checking Better-Web-Publisher appears on your responses (curl -sI https://your-site).
UNKNOWN_HOSTNAME. The host being verified is not in hostnames (the www/apex sibling of a listed
host counts as listed). Log $visitor->hostname to see what actually arrived; a reverse proxy may be
passing something you did not expect.
WRONG_HOSTNAME from real visitors. Should be rare - www and apex are folded together. A steady
stream means tokens are being replayed from another site; a trickle is usually a proxy rewriting Host.
FORGED for everybody. A publicKey override left over from staging.
Slower than expected. Check $publisher->cacheStats(). A high evictions count against size at
maxSize means the working set outgrew the cache - raise maxSize. Confirm you create the publisher
once at startup, never per request.
License
Apache-2.0
All versions of token with dependencies
ext-sodium Version *