Browser — @yetdapay/license-web
Quick summary
The browser SDK activates a license, keeps the device key, and signs protected requests. The backend—not browser JavaScript—must make the final authorization decision.
Start with the Worker preset and follow the activation example. Device-key reset can consume another activation slot.
sdk/license/web/ — the buyer-side client. Holds a non-extractable WebCrypto P-256 device keypair, activates a license, stores the entitlement, and signs every gated request with a fresh DPoP-style proof.
Guarantees (and non-guarantees)
| Guaranteed | How |
|---|---|
| Device private key is not extractable to JS | generateKey({extractable:false}); the CryptoKey itself lives in IndexedDB |
| Request tampering/replay is blocked server-side | per-request proof {htu,htm,iat,jti,h,jwk}; jti consumed atomically server-side |
| Token copy alone is useless | every call re-proves jkt device binding |
| Revocation applies immediately | the server re-queries license status on every guarded call |
Not guaranteed: client-bundle obfuscation/integrity, DRM, offline protection, or anything about a fully hostile browser. See Limitations.
API surface (sdk/license/web/src/index.js)
import {
createLicenseClient, createWorkerConfig, WORKER_ACTIVATE_PATH,
configure, getDefaultClient,
activate, signedFetch, isEntitled, getEntitlement, restore,
clearEntitlement, resetDevice,
// stores & crypto primitives
createIndexedDbKeyStore, createMemoryKeyStore, loadOrCreateDeviceKey,
createLocalStorageEntitlementStore, createMemoryEntitlementStore,
createDpopProof, LicenseError, LicenseErrorCode,
} from '@yetdapay/license-web';2
3
4
5
6
7
8
9
10
Client vs convenience functions
createLicenseClient(options) returns a client object; the top-level configure()/activate()/signedFetch()/… functions operate on a module-level default client — pick one style, do not mix.
Pairing with the hosted Worker
const client = createLicenseClient(
createWorkerConfig({
baseUrl: 'https://api.example.com',
productId: 'prod_abc123',
})
);
await client.activate({ licenseKey: 'NG-…-…' });
const res = await client.signedFetch('/api/v1/licenses/status');2
3
4
5
6
7
8
createWorkerConfig() sets activatePath to the Worker route /api/v1/licenses/activate (exported as WORKER_ACTIVATE_PATH). The SDK default (/api/license/activate) targets a self-hosted @yetdapay/license-server — see the activation paths table.
Request shape produced by signedFetch
Authorization: Entitlement <entitlement-jws>DPoP: <proof-jws>with payload{htu, htm, iat, jti, h, jwk}htu= same-originpathname + query(not a full URI)h=base64url(sha256(raw request body))
- On
401, the client renews the entitlement once and retries with a new proof — bounded retry, no loop.
Storage adapters
createIndexedDbKeyStore— deviceCryptoKeypersistence (browser).createLocalStorageEntitlementStore/createMemoryEntitlementStore— entitlement persistence;restore()reloads a stored entitlement.
resetDevice() warning
resetDevice() clears the local device identity — it does not free the server-side license_bindings slot, so it permanently burns one max_devices slot (recorded gap; unbind is NOT_IMPLEMENTED). Only call it when you accept the slot loss.
Error handling
LicenseError carries .code verbatim from the backend {code,message} body plus .status (the real HTTP status). The Worker/SDK code deltas are documented in the contract matrix §2.