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.

FAQ

After the download, you have to make one include require_once('vendor/autoload.php');. After that you have to import the classes with use statements.

Example:
If you use only one package a project is not needed. But if you use more then one package, without a project it is not possible to import the classes with use statements.

In general, it is recommended to use always a project to download your libraries. In an application normally there is more than one library needed.
Some PHP packages are not free to download and because of that hosted in private repositories. In this case some credentials are needed to access such packages. Please use the auth.json textarea to insert credentials, if a package is coming from a private repository. You can look here for more information.

  • Some hosting areas are not accessible by a terminal or SSH. Then it is not possible to use Composer.
  • To use Composer is sometimes complicated. Especially for beginners.
  • Composer needs much resources. Sometimes they are not available on a simple webspace.
  • If you are using private repositories you don't need to share your credentials. You can set up everything on our site and then you provide a simple download link to your team member.
  • Simplify your Composer build process. Use our own command line tool to download the vendor folder as binary. This makes your build process faster and you don't need to expose your credentials for private repositories.
Please rate this library. Is it a good library?

Informations about the package token

zeroad.network/token (PHP)

Recognise Zero Ad Network subscribers in your PHP backend so you can let them browse without ads and access the paid content you include with their membership. Verification runs offline, with no dependencies beyond ext-sodium and no calls back to us.

In short

Step-by-step guide: PHP guide. Using WordPress? Use the WordPress plugin instead.

This is the PHP port of @zeroad.network/token. It speaks the same wire format, so a token verifies identically on either.


Two headers

This package handles both ends:

Direction Header Carries
You → visitor Better-Web-Publisher Your Publisher ID, so the extension finds you and credits the visit
Visitor → you Better-Web-Token Their signed membership token, bound to your hostname

Already clean? If your website has no ads, trackers, cookie banners, popups or paywall, you only need to send Better-Web-Publisher. You don't need to verify anything. Check whether your website is already clean →


Integrate

1. Copy your Publisher ID

Sign in, then copy your Publisher ID from Websites & creators. It starts with zapub_.

2. Create a publisher

Create it in your bootstrap. With PHP-FPM, that runs once per request. In a long-running application, reuse the instance across requests.

hostnames lists every host you serve. It's required, because a token only verifies on a listed host. See why hostnames are an allowlist.

3. Check every request

This does two things on every request:

  1. Sends Better-Web-Publisher, before any output, even when no token arrived. This is how the extension discovers your website.
  2. Checks the visitor's token. PHP exposes it as $_SERVER["HTTP_BETTER_WEB_TOKEN"], which $publisher->tokenHeaderServerKey gives you.

4. Serve subscribers the clean page

Unlock paid content on the server. Hiding a paywall overlay doesn't help if the content was never sent. Higher tiers can stay restricted.

5. Keep subscriber pages out of shared caches

If a CDN, reverse proxy or page cache sits in front of PHP, set it to bypass the cache for requests carrying Better-Web-Token. Those requests must reach PHP. Set up page caching and CDNs →

6. Check it works

  1. Confirm your responses include Better-Web-Publisher: curl -s -D - -o /dev/null https://example.com/ | grep -i better-web-publisher
  2. In your dashboard, open your website's page and select Test in your browser. No paid membership needed.
  3. Reload your website. You should see the clean page.
  4. Open the same URL without the extension, with caches warm. You should see the normal page.

Laravel and Symfony middleware to copy are in Framework examples. The rest of the guide is at zeroad.network/docs.


Framework examples

In middleware, check the request, set the publisher header on the response, and pass $visitor to your templates.

Laravel

Symfony

WordPress

Use the Zero Ad Network plugin. It bundles this SDK, and also handles page caches and popular ad, consent, popup and membership plugins. See the WordPress guide.


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 Allowed clock difference when checking expiry.
cache bool\|array on See caching. false turns it off.

It returns a Publisher. Reuse it within the request, or across requests in a long-running application:

$publisher->header ["Better-Web-Publisher", "zapub_..."]
$publisher->headerName / ->headerValue the same, separately
$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)

Pass the raw header value: a string, an array (some stacks return an array for a repeated header; the first wins), or null. It never throws on bad input. An invalid token returns a rejection result instead of throwing an exception.

$visitor->subscriber tells you which branch you're in:

$visitor->toArray() gives a JSON-friendly copy, with expiresAt as a Unix timestamp.

Rejection

A reason names the check that failed. It isn't proof of an attack.

Reason (Rejection::) Means What to do
MISSING No token header. Most of your traffic. Nothing. This is normal.
MALFORMED Not a well-formed token. Nothing.
UNSUPPORTED_VERSION Unsupported token format. Check for an SDK update.
EXPIRED Expiry is past the allowed time. Nothing, unless it comes in bursts.
UNKNOWN_HOSTNAME The host isn't in your allowlist. Check your hostnames config.
WRONG_HOSTNAME Authority signature passed; hostname signature failed. Check the hostname and your proxy.
FORGED Authority signature failed. Check for a publicKey override.

When a token arrives with a version newer than this package understands, it's rejected as UNSUPPORTED_VERSION, and a line in the PHP error log suggests an SDK update. The warning alone doesn't prove the token is genuine, or that a protocol upgrade has shipped. To silence it during a staged rollout, or in tests that send such tokens on purpose, call ZeroAd\Token\VersionWarning::suppress() once at startup.

Also available

All in the ZeroAd\Token namespace: Constants (PLAN, PLAN_NAME, PUBLISHER_HEADER, PUBLISHER_ID_SCHEME, TOKEN_HEADER, TOKEN_HEADER_LOWERCASE, TOKEN_HEADER_SERVER_KEY, AUTHORITY_PUBLIC_KEY, PROTOCOL_VERSION), Rejection, VerificationResult, Token::TOKEN_BYTES, Token::TOKEN_CHARACTERS, Hostname::canonical(), PublisherHeader::encode(), PublisherHeader::parse() and VersionWarning::suppress().

This package only verifies. The platform's private authority key signs credentials, and the extension's private ephemeral keys bind them to hostnames. Neither key is in a visitor token.


Good to know


Caching

The extension reuses a token for its bound hostname while the credential is valid. So a returning visitor may send bytes you've already checked. Caching skips repeating the signature checks. It's on by default.

This caches verification results, not HTML. For page caches, see Page caching and CDNs.

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; the memory store ignores it

Three things worth knowing:

Failures are cached too. A forged token costs as much to reject as a real one costs to accept, and whoever sends it will likely send it again. This is safe: for a fixed public key, a rejection can never later become an acceptance. The only direction a verdict moves is from valid to expired, and each entry's own expiry handles that.

A success never outlives the token. The stored expiry is the earlier of your TTL and the token's own expiresAt. A generous TTL can't extend anybody's membership.

Cheap rejections are not cached. A length or byte check throws out a malformed, missing or expired token. Caching those would save nothing, and would let anyone fill memory with distinct keys.

Where results live

Store Lifetime under PHP-FPM Use it when
"memory" Ends with each request, even when the worker is reused Long-running apps that reuse the publisher
"apcu" Shared across requests in the same APCu segment You know APCu is installed
"auto" APCu when present, otherwise memory Code that ships to hosts you don't control

With "apcu", if the extension isn't available, the publisher logs a line and falls back to memory. The token is still verified, just not shared. "auto" falls back silently.


How the token works

You don't 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.

1. The platform signs batch credentials. The extension generates disposable key pairs locally, and sends only the public halves. The platform checks the account's membership, then signs the plan, expiry and each key. Standard credentials expire at midnight UTC two days after issue, about 24–48 hours later. The extension checks its pool hourly, and refreshes when credentials run low or near expiry. Demo and publisher-test access use separately issued tokens, restricted to one hostname.

2. The extension binds one to your hostname. On the first visit to example.com, it takes an unused key pair and signs your hostname with the private half. This happens offline. It reuses that bound token for your exact hostname while it stays valid.

3. You verify both signatures. The first proves the platform authorised the plan and expiry. The second proves the token was bound to your hostname.

The hostname is deliberately absent from the wire. Your server already knows what it serves, and rebuilds the signed message from that. A token bound elsewhere simply fails the signature.

What this stops

The token you receive contains a public key, and a signature over your own hostname. The secret that makes bindings never leaves the visitor's browser.

Privacy

Why hostnames are an allowlist

hostnames is required, and verify() won't fall back to whatever arrived in the Host header. Tokens are bound to a hostname, and the client sets Host. Without the allowlist, an attacker could bind a token to a domain they control, send it with Host: that-domain.example, and be admitted as a subscriber. Listing your hosts removes that possibility.

www.example.com and example.com are different hosts, but listing either admits both. They're the same domain under one owner, so a website serving both needs only one in the list. The signature is still checked against the exact host each request arrives on.


Performance

Measured on PHP 8.5, Apple Silicon, single core, libsodium 1.0.22, via php benchmarks/verify.php:

Cold verification, end to end 88µs, about 11,300/s
Cached verdict 0.95µs, about 1,050,000/s
Malformed token about 1.3µs, rejected on length before it's decoded

Verification is two Ed25519 checks through ext-sodium (sodium_crypto_sign_verify_detached). The cold cost tracks libsodium, and is close to the TypeScript SDK. At about 0.09ms, it's a rounding error next to a database query or a template render.

On classic PHP-FPM, the default memory cache lasts only for its request. So the cached figure applies within a request, and a returning visitor's token is verified cold on the next page load. Use "cache" => ["store" => "apcu"] or "auto" to share results across the whole worker pool.


Requirements


Troubleshooting

Every visitor comes back MISSING. That's expected: only subscribers send a token. Check that Better-Web-Publisher appears on your responses, then test with Test in your browser.

A test subscriber comes back MISSING. Reload after the first visit. Then check that your CDN or proxy forwards Better-Web-Token to PHP, and doesn't serve a cached page.

UNKNOWN_HOSTNAME. The host isn't in hostnames. The www/apex sibling of a listed host counts as listed. Log $visitor->hostname to see what arrived; a reverse proxy may pass something unexpected.

WRONG_HOSTNAME from real visitors. Apex and www share allowlist coverage, but their signatures are different. A token for example.com can't verify against www.example.com. Pass the actual public hostname, and check for proxy rewrites. A modified or replayed token can also cause this.

FORGED for everybody. A publicKey override left over from staging.

It got slower under load. Check $publisher->cacheStats(). A high evictions count, with size at maxSize, means the working set outgrew the cache: raise maxSize. Create the publisher once per request under PHP-FPM, or reuse it in a long-running app. Use APCu to cache across PHP-FPM requests.


License

Apache-2.0


All versions of token with dependencies

PHP Build Version
Package Version
Requires php Version ^7.2 || ^8.0
ext-sodium Version *
Composer command for our command line client (download client) This client runs in each environment. You don't need a specific PHP version etc. The first 20 API calls are free. Standard composer command

The package zeroad.network/token contains the following files

Loading the files please wait ...