Nuxt — @yetdapay/license-nuxt
Quick summary
Use the Nuxt 3 adapter to protect h3 server routes. Keep license secrets and authorization logic in server-only files; browser components only send signed requests.
Copy the server helper example first, then connect the client controller and verify denial paths.
sdk/license/nuxt/ — the Nuxt 3 adapter. Wraps @yetdapay/license-server into h3-friendly guards and ships two framework-agnostic cores (/core and /client) that work outside Nuxt too.
Setup
// server/utils/license.ts — SERVER ONLY
import { createLicenseServer, createSqliteStore } from '@yetdapay/license-server';
import { createNuxtLicense } from '@yetdapay/license-nuxt';
const server = createLicenseServer({
store: createSqliteStore({ file: './.data/license.db' }),
keys: { load: async () => JSON.parse(process.env.DEVPAY_LICENSE_KEYS) },
issuer: 'devpay',
audience: 'app:my-product',
entitlementTtlSec: 120,
});
export const license = createNuxtLicense({ server });2
3
4
5
6
7
8
9
10
11
12
// server/api/premium/convert.post.ts
export default license.defineLicenseEventHandler(
{ feature: 'premium_convert' },
async (event, context) => {
const body = await license.readJsonBody(event);
return { result: runPaidLogic(body.input, context) };
}
);2
3
4
5
6
7
8
Public API — createNuxtLicense({server, maxBodyBytes?, defaultFeature?})
| Member | Behavior |
|---|---|
requireLicense(event, {feature?, resource?}) | Verifies; returns LicenseContext; throws on denial. |
authorize(event) | Verifies without a feature gate. |
defineLicenseEventHandler(options, handler) | Verify + run handler; denials become JSON Responses. |
readJsonBody(event) | Parse the raw bytes the guard already read (cached on event._requestBody). |
descriptorOfEvent(event) | Extract {headers, method, requestTarget, rawBody}. |
errorDescriptor(error) / licenseErrorResponse(error) | LicenseError → {status, body} / web-standard Response. |
guard, server | The underlying instances. |
options.resource may be a function (event, descriptor) => resource — used to feed a resource id into the ownership hook.
Framework-agnostic cores
// @yetdapay/license-nuxt/core — any runtime (Express/Fastify/Workers/node:http)
import { createLicenseGuard } from '@yetdapay/license-nuxt/core';
const guard = createLicenseGuard({ server, maxBodyBytes: 64 * 1024 });
const ctx = await guard.requireLicense(
{ headers, method, requestTarget, rawBody },
{ feature: 'x' }
);2
3
4
5
6
7
// @yetdapay/license-nuxt/client — client-side state controller
import { createLicenseClient } from '@yetdapay/license-web';
import { createLicenseController } from '@yetdapay/license-nuxt/client';
const controller = createLicenseController({ client });
controller.subscribe((snapshot) => render(snapshot));
await controller.activate('NG-…-…');
await controller.call('/api/premium/convert', { method: 'POST', body: '…' });2
3
4
5
6
7
Snapshot: {status, entitled, features, expiresAt, productId, error}. The controller deliberately does not import vue — a file:-linked package resolving a second Vue instance silently breaks reactivity; wrap it once in your app (e.g. a useLicense() composable).
Body handling & error contract
- The guard reads raw body bytes first to compare against the proof's
h(body hash). Uselicense.readJsonBody(event)in handlers — plainreadBody()usually works butreadJsonBodyis the byte-preserving path. - Bodies over
maxBodyBytes(default 64 KiB) →413 BODY_TOO_LARGE. - Denial bodies follow the
@yetdapay/license-servererror contract:{"code":"FORBIDDEN","message":"…"}.errorDescriptorduck-types the error (code+status) rather thaninstanceof— a host bundling a second copy of the SDK would otherwise turn a 403 into a 500. Unknown errors always generalize to500 INTERNAL.
SSR notes
createNuxtLicenseis server-only — importing it outsideserver/leaks server code into the client bundle.- To guard a whole API subtree, call
requireLicensefromserver/middleware/license.tsand returnlicenseErrorResponse(error)on failure. - Static/SSR page rendering is unrelated to licensing — the protection target is server API routes, not pages.