Entitlement
Quick summary
An entitlement is a short-lived signed statement of which features may be used. It is not permanent proof that a license remains active.
Server guards must re-check the current license state and the device proof; expired or revoked rights must fail closed.
The entitlement is a short-lived ES256 JWS minted at activation and presented on every gated call as Authorization: Entitlement <jws>. TTL 120 s (ENTITLEMENT_TTL_SEC); on 401 the client re-activates and retries once with a fresh proof.
Claims matrix
Quoted from the contract matrix §1 — Worker actual = be-cloudflare emits/verifies; SDK default = @yetdapay/license-server defaults; preset = createWorkerConfig().
| claim | Worker actual | SDK default | SDK worker-preset |
|---|---|---|---|
iss | "devpay-lic" (required on verify) | "yp" emitted + required | "devpay-lic" |
aud | absent — never emitted, never checked | absent unless audience set; then emitted + required | absent (aud emission is DEFERRED) |
lic_id | required | required | same |
product_id | required; re-checked vs live license row (401 on drift) | required; re-checked vs store | same |
tenant_id | absent — DEFERRED | emitted (nullable); enforced only via expectedTenantId | absent tolerated |
features | from product namespace; missing → [], non-strings dropped | required string array | same |
jkt_bound | emitted + required; bound to proof jwk | required (falls back to cnf.jkt) | same |
cnf.jkt | emitted (same as jkt_bound) | emitted + cross-checked vs jkt_bound | same |
iat | emitted; not verified on Worker (only exp checked) | required; future iat beyond 60 s skew rejected | same |
exp | iat + 120; expired rejected | required; exp <= now rejected | same |
jkt=sha256(sha256(RFC 7638 thumbprint))— DevPay-specific double-hash, not RFC 9449.products.features_jsonis the sole entitlement source of truth — features on the license row are not trusted.- Worker feature-gates all PoP endpoints behind
LICENSE_SECURITY_ENABLED; absent →404. - The wire is a DevPay-specific versioned contract, not RFC 9449 DPoP — there is no version field or negotiation (wire v1; versioning).
aud / tenant_id — deferred
The Worker neither emits nor verifies aud or tenant_id; adding them changes the token format and is deferred to a wire-v2 decision (known-limitations §3.8). The SDK already emits/normalizes tenant_id (nullable) and optional aud.
Guard order on the Worker
guard() evaluates, in order: entitlement signature/iss/exp → live license row re-query (revoke/expire/product-mismatch) → DPoP sig/htm/htu/iat → jkt match → jti atomic consume (license_nonces) → body hash h. Denials write security_events (no keys/tokens/PII in detail_json).
What the entitlement is not
- Not a session token — 120-second PoP grant bound to a device key; revocable mid-flight via the license-row re-query.
- Not self-contained authority — the server always re-checks the live license row; a valid signature alone never suffices.