GNL
Docs menu
Enterprise@gnldev/auth-ee

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
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:

vendor — generate key + sign license
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 vault

The 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:

host — fail-closed EE auth
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:

enforce the license on a custom path
const info = validateLicense(process.env.GNL_LICENSE_KEY, { publicKey: process.env.GNL_EE_PUBLIC_KEY });
assertLicensed(info); // throws when invalid the caller should fall back
Local development
To quickly try it out without generating a signed license, set GNL_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.

src/index.ts
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#

fncreateEnterpriseAuth

Takes EnterpriseAuthOptions; if the license is valid, returns an AuthProvider composing RBAC/SSO/userStore/audit; if invalid, falls back to fallback (throws if failClosed: true).

typeEnterpriseAuthOptions

{ licenseKey?, fallback?, publicKey?, failClosed?, requireExp?, maxLifetimeMs?, rbac?, sso?, users?, userStore?, audit?, fga? } — the input to createEnterpriseAuth.

fnvalidateLicense

(key?, { publicKey?, now? }) → LicenseInfo. Verifies the signed eeL1 format with Ed25519, checks exp; also supports the dev/unsigned escape hatches.

fnsignLicense

(payload: LicensePayload, privateKey) → a signed license key ("eeL1.<payload>.<imza>"). On the vendor side, with the private key.

fngenerateLicenseKeys

() → { publicKey, privateKey } (base64url DER). Called ONCE on the vendor side; the private key stays in the vault.

fnassertLicensed

Takes a LicenseInfo; throws if not valid. Used as an extra enforcement point before unlocking a premium path.

typeLicenseInfo

{ valid, plan?, features?, reason?, sub?, exp?, seats? } — the output of validateLicense; plan/exp flow into the license badge in Studio.

typeLicensePayload

{ 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.

License required
This package and the RBAC/SSO/user-store/audit features inside it do not work without a valid, unexpired license key — with 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.