Verify the WayID of inbound AI agents at the NGINX edge, before traffic reaches your application. Part of the WayID verification SDK (#373).
An agent declares its identity with a request header:
WayID: wayid:agent:{24-char-base58} # full DID or the bare 24-char tail
An njs handler (njs/wayid.js), wired with js_content, reads the header,
resolves it against {issuer}/api/v1/agent/{did}, and:
- forwards
X-WayID-Decision/X-WayID-Verified/X-WayID-DID/X-WayID-Identity-Level/X-WayID-Statusto your upstream (viajs_vars), theninternalRedirects to it; - returns
403instead when$wayid_enforceisonand the decision isdeny.
Passive v1 needs no crypto, so the lightweight njs handler is sufficient — no
sidecar process. It fails open: a missing/unknown/malformed header or an issuer
outage never blocks traffic unless you explicitly enforce. Resolutions are cached in
a shared dict (js_shared_dict_zone) for the zone TTL, so the edge doesn't hit the
issuer on every request.
Identification, not authentication. The
WayIDheader is self-asserted. Cryptographic proof-of-possession is a planned v2 layer (Web Bot Auth / RFC 9421).
Why
js_content, notauth_request? njs'sngx.fetchdoes not run inside anauth_requestsubrequest (the subrequest never fires), so the natural-lookingauth_requestdesign does not work for a handler that calls the issuer. The working pattern isjs_content→ resolve →internalRedirectto the upstream.
- Build/run NGINX with the njs module (
ngx_http_js_module). On Debian/Ubuntu:apt-get install nginx-module-njs, thenload_module modules/ngx_http_js_module.so;. The officialnginxDocker image already ships the module under/etc/nginx/modules/. - Copy
njs/wayid.jsto/etc/nginx/njs/. - In
http {}:js_path "/etc/nginx/njs/"; js_import wayid from wayid.js; js_var $wayid_decision; js_var $wayid_verified; js_var $wayid_did; js_var $wayid_level; js_var $wayid_status; resolver 1.1.1.1 ipv6=off; # ngx.fetch needs a resolver js_shared_dict_zone zone=wayid:1m timeout=60s; # optional resolution cache
- Use
conf/wayid.confas a template for theserver {}block (alocation /withjs_content wayid.gate;and aninternallocation @wayid_upstreamthatproxy_passes to your app).
| nginx variable | default | meaning |
|---|---|---|
$wayid_issuer |
https://way.je |
issuer origin that minted the DIDs |
$wayid_enforce |
off |
on ⇒ return 403 on a deny decision |
$wayid_require_verified |
off |
on ⇒ require owner identityLevel == "verified" |
test/run.sh runs the handler in real NGINX + njs (Docker) against a mock issuer
and asserts allow / deny / flag behaviour:
bash test/run.sh # requires dockerIf your NGINX build lacks njs, run the JS package (@lineagelabs/wayid-verify,
withWayId worker adapter) as a small HTTP service and point auth_request at it
via proxy_pass; read the X-WayID-* response headers with
auth_request_set $v $upstream_http_*. (This works because the sidecar — not njs —
makes the issuer call.)
This is a single njs handler plus a sample config — there's no package registry artifact. Get it one of two ways:
- Tagged GitHub release (recommended). Each
v*tag publishes a release withnjs/wayid.jsandconf/wayid.confattached (see.github/workflows/release.yml). Downloadwayid.js, drop it in/etc/nginx/njs/, and wire up your config. - Clone / vendor. Copy
njs/wayid.jsinto your repo or config-management (Ansible, Docker build, etc.). Pin to a tag for reproducibility.
To cut a release: bump CHANGELOG.md, then git tag v0.1.0 && git push origin v0.1.0.
The release workflow runs the integration test before publishing.
MIT © Lineage Labs — see LICENSE.