Docs menu
Signed license (Enterprise)
Verifies an Ed25519-signed license key; if valid, the premium EE layer unlocks — if invalid/expired, it falls back to free behavior, or boot halts with failClosed.
What it's for / when to use it#
@gnldev/auth-ee is a paid layer that implements the free @gnldev/auth's AuthProvider contract: it will not unlock premium features like RBAC, SSO, journal-backed user management, and audit without a valid license key. License verification is entirely offline — there's no telemetry/phone-home; the signature is checked locally with Ed25519. It's used when selling a deployment or shipping EE features locked to a self-hosted customer: the customer receives a license key, the host passes it to createEnterpriseAuth; if it's invalid (or expired), the system automatically falls back to the free fallback behavior — with failClosed: true, boot can be halted entirely instead of falling back (the recommended mode for paid deployments: it prevents silently running without premium on a wrong/expired key).
Setup / import#
import {
createEnterpriseAuth,
generateLicenseKeys,
signLicense,
validateLicense,
assertLicensed,
} from '@gnldev/auth-ee';There's no separate sub-package path — all exports come from the @gnldev/auth-ee root entry point (internally, the package re-exports the license, rbac, sso, sso-providers, users, user-store, postgres-user-store, audit and fga modules).
Step-by-step usage#
1. On the vendor side, generate an Ed25519 key pair once and sign a license for the customer:
const keys = generateLicenseKeys();
const license = signLicense(
{ sub: 'demo-musteri', plan: 'enterprise', exp: Date.now() + 365 * 86_400_000, seats: 5 },
keys.privateKey,
);
const check = validateLicense(license, { publicKey: keys.publicKey });
if (!check.valid) throw new Error(`unexpected: the generated license is invalid (${check.reason})`);
// license → goes to the customer as GNL_LICENSE_KEY
// keys.publicKey → goes to the customer as GNL_EE_PUBLIC_KEY
// keys.privateKey → the VENDOR SECRET: never distributed, stays in the vaultThe exp given to signLicense must be a finite epoch-ms number; if an invalid date (e.g. NaN) is given, the function throws — otherwise it would silently produce a license with no expiry. If features isn't specified, the license unlocks ALL EE features regardless of plan; for a restricted license, a subset like features: ['rbac', 'audit'] can be given.
2. On the customer/host side, pass the license to createEnterpriseAuth to set up the auth layer:
const auth = createEnterpriseAuth({
licenseKey: process.env.GNL_LICENSE_KEY,
publicKey: process.env.GNL_EE_PUBLIC_KEY,
failClosed: true,
userStore,
audit: createJournalAuditSink(toJournal(storage.runs)),
fallback: roleAuth({
admin: { token: ADMIN_TOKEN, user: 'ops' },
viewer: { token: ACME_TOKEN, orgId: 'acme' },
}),
});createEnterpriseAuth checks the license with validateLicense: if valid, it returns an AuthProvider composing RBAC + (if present) SSO + journal-backed userStore + audit; if invalid and failClosed: true, it throws (boot halts); if failClosed is unset/false, it logs a warning and returns fallback (premium off, free behavior continues). The GNL_EE_PUBLIC_KEY env var is read automatically if the publicKey option isn't given.
3. If you never want a premium path (e.g. a custom admin endpoint) to unlock without a license, you can enforce the validation result directly:
const info = validateLicense(process.env.GNL_LICENSE_KEY, { publicKey: process.env.GNL_EE_PUBLIC_KEY });
assertLicensed(info); // throws when invalid — the caller should fall backGNL_EE_DEV_LICENSE to a random value and make licenseKey match it EXACTLY — plan: 'dev', all features are considered unlocked. Legacy unsigned keys with the ee_ prefix are only accepted when GNL_EE_ALLOW_UNSIGNED=1; NEITHER should be used in production.Bounding how long a licence stays valid#
Two options exist for the case that actually happens: a key leaks, or one issued for a trial is still in use a year later. requireExp refuses a licence with no exp at all — without it, a payload minted without an expiry never expires. maxLifetimeMs refuses one whose expiry is further out than you allow, so a key issued for ten years cannot be accepted by a deployment that only ever wanted one. Neither is on by default: turning them on is a decision about your own issuing practice, and a default that rejected existing keys would be the wrong kind of surprise.
const auth = createEnterpriseAuth({
licenseKey: process.env.GNL_LICENSE_KEY,
failClosed: true, // invalid licence → throw at boot, do not drop to free silently
requireExp: true, // refuse a licence that never expires
maxLifetimeMs: 365 * 24 * 60 * 60 * 1000, // ...and one valid for longer than a year
});Fine-grained authorization (FGA)#
RBAC answers “may this role do this kind of thing”. FGA answers it per OBJECT: may this principal read this run, resume this workflow. createFga takes rules over a resource and an action; the host consults it after the coarse permission check, so a denial is a FgaDeniedError rather than a silent empty result, and recordFgaDecision puts the decision in the audit trail. It is a licence feature like the others — absent from the free tier, and absent from a deployment whose licence does not carry it.
API reference#
createEnterpriseAuthTakes EnterpriseAuthOptions; if the license is valid, returns an AuthProvider composing RBAC/SSO/userStore/audit; if invalid, falls back to fallback (throws if failClosed: true).
EnterpriseAuthOptions{ licenseKey?, fallback?, publicKey?, failClosed?, requireExp?, maxLifetimeMs?, rbac?, sso?, users?, userStore?, audit?, fga? } — the input to createEnterpriseAuth.
validateLicense(key?, { publicKey?, now? }) → LicenseInfo. Verifies the signed eeL1 format with Ed25519, checks exp; also supports the dev/unsigned escape hatches.
signLicense(payload: LicensePayload, privateKey) → a signed license key ("eeL1.<payload>.<imza>"). On the vendor side, with the private key.
generateLicenseKeys() → { publicKey, privateKey } (base64url DER). Called ONCE on the vendor side; the private key stays in the vault.
assertLicensedTakes a LicenseInfo; throws if not valid. Used as an extra enforcement point before unlocking a premium path.
LicenseInfo{ valid, plan?, features?, reason?, sub?, exp?, seats? } — the output of validateLicense; plan/exp flow into the license badge in Studio.
LicensePayload{ sub, plan, exp?, seats?, features? } — the input to signLicense; if exp is omitted the license never expires, if features is omitted all EE features unlock.
failClosed: false (the default), an invalid license silently falls back to fallback; if you don't want it running without premium unnoticed in production, use failClosed: true.