npm package: @maniidev/failsafe-license
Description: Framework-agnostic license validation for Node.js, Express, and Next.js App Router
Author: Your Name
Latest version: 2.0.2

# @maniidev/failsafe-license

Framework-agnostic license validation for Node.js apps with first-class adapters for Express and Next.js App Router.

The package is designed to fail safe by default: transient validation failures preserve the last usable state when possible and do not automatically take down customer apps.

## Installation

```bash
npm install @maniidev/failsafe-license
```

Node 18+ is required.

## Importing

CommonJS:

```js
const license = require('@maniidev/failsafe-license');
const { createLicenseManager } = license;
```

ESM:

```js
import license from '@maniidev/failsafe-license';

const { createLicenseManager } = license;
```

`createLicenseManager` is available in `@maniidev/failsafe-license@2.0.0+`.
If `createLicenseManager` is `undefined`, upgrade your install:

```bash
npm install @maniidev/failsafe-license@latest
```

Older installs only expose the legacy singleton API on the root export.

## Usage

### Basic Node / server usage

```js
const license = require('@maniidev/failsafe-license');
const { createLicenseManager } = license;

const licenseManager = createLicenseManager({
  projectId: process.env.FAILSAFE_PROJECT_ID,
  secretKey: process.env.FAILSAFE_SECRET_KEY,
  disableAutoRevalidate: true,
  staleIfErrorMs: 60 * 60 * 1000,
});

async function main() {
  await licenseManager.init();

  const state = await licenseManager.ensureValid({ maxAgeMs: 5 * 60 * 1000 });

  if (!state.allow) {
    throw new Error('License blocked');
  }
}

main().catch((error) => {
  console.error(error);
  process.exit(1);
});
```

### Express

```js
const express = require('express');
const license = require('@maniidev/failsafe-license');
const {
  createExpressMiddleware,
  createGraceBannerMiddleware,
} = require('@maniidev/failsafe-license/express');

const app = express();
const { createLicenseManager } = license;
const licenseManager = createLicenseManager({
  projectId: process.env.FAILSAFE_PROJECT_ID,
  secretKey: process.env.FAILSAFE_SECRET_KEY,
});

async function main() {
  await licenseManager.init();

  app.use(
    createExpressMiddleware(licenseManager, {
      maxAgeMs: 5 * 60 * 1000,
      redirectTo: '/license-blocked',
    }),
  );

  app.use(
    createGraceBannerMiddleware(licenseManager, {
      maxAgeMs: 5 * 60 * 1000,
    }),
  );

  app.get('/', (req, res) => {
    res.send('<html><body><h1>Dashboard</h1></body></html>');
  });

  app.listen(3000);
}

main().catch(console.error);
```

### Next.js App Router route handlers

```js
// lib/license.js
const license = require('@maniidev/failsafe-license');
const { createLicenseManager } = license;

const licenseManager = createLicenseManager({
  projectId: process.env.FAILSAFE_PROJECT_ID,
  secretKey: process.env.FAILSAFE_SECRET_KEY,
  disableAutoRevalidate: true,
});

module.exports = {
  licenseManager,
};
```

```js
// app/api/protected/route.js
const { withRouteHandlerLicense } = require('@maniidev/failsafe-license/next');
const { licenseManager } = require('../../../lib/license');

exports.GET = withRouteHandlerLicense(
  async function GET(request, context, licenseState) {
    return Response.json({
      ok: true,
      status: licenseState.status,
      graceDaysLeft: licenseState.graceDaysLeft,
    });
  },
  licenseManager,
  {
    maxAgeMs: 5 * 60 * 1000,
    jsonBody: (state) => ({
      error: 'License blocked',
      reason: state.reason,
    }),
  },
);
```

### Next.js Server Components / layouts redirect guard

```js
// app/(protected)/layout.js
const { assertValidNext } = require('@maniidev/failsafe-license/next');
const { licenseManager } = require('../../lib/license');

module.exports = async function ProtectedLayout({ children }) {
  await assertValidNext(licenseManager, {
    maxAgeMs: 5 * 60 * 1000,
    redirectTo: '/license-blocked',
  });

  return children;
};
```

If you only need the current state without asserting, use `getLicenseState(licenseManager, options)`.

### React grace banner helper

```js
// app/layout.js
const { LicenseGraceBanner } = require('@maniidev/failsafe-license/react');
const { getLicenseState } = require('@maniidev/failsafe-license/next');
const { licenseManager } = require('../lib/license');

module.exports = async function RootLayout({ children }) {
  const licenseState = await getLicenseState(licenseManager, {
    maxAgeMs: 5 * 60 * 1000,
  });

  return (
    <html>
      <body>
        <LicenseGraceBanner state={licenseState} />
        {children}
      </body>
    </html>
  );
};
```

### Legacy singleton API

The root module still exposes the v1-style singleton methods:

```js
const license = require('@maniidev/failsafe-license');

await license.init({
  projectName: process.env.FAILSAFE_PROJECT_ID,
  secret: process.env.FAILSAFE_SECRET_KEY,
});

app.use(license.middleware());
app.use(license.graceBannerMiddleware());
```

## API notes

- `createLicenseManager(options)` creates an isolated manager instance with its own config, cached state, timer, and in-flight validation promise.
- `ensureValid({ maxAgeMs, revalidateIfStale })` is the main request-time guard.
- `revalidate()` forces a remote validation attempt.
- `destroy({ resetState })` stops background revalidation; pass `resetState: true` to clear cached state and config.
- Normalized state shape:

```js
{
  status: 'uninitialized' | 'valid' | 'grace' | 'blocked' | 'error',
  allow: boolean,
  grace: boolean,
  graceDaysLeft: number,
  lastChecked: string | null,
  error: string | null,
  reason: string | null
}
```

## Migration notes

- `axios` has been removed. v2 uses native `fetch` and requires Node 18+.
- The recommended API is now `createLicenseManager(options)` instead of relying on one global singleton.
- If your installed package does not expose `createLicenseManager`, you are on a pre-v2 release. Upgrade to `@latest` or keep using the legacy singleton API.
- Existing aliases still work: `projectName` maps to `projectId`, and `secret` maps to `secretKey`.
- `middleware()` and `graceBannerMiddleware()` still exist on the root export for backward compatibility, but the preferred Express entrypoint is `@maniidev/failsafe-license/express`.
- For Next.js App Router, use `@maniidev/failsafe-license/next` helpers instead of Express-style response mutation.

## Environment variables

```bash
FAILSAFE_PROJECT_ID=proj_xxxxxxxxxxxxxxxx
FAILSAFE_SECRET_KEY=sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
FAILSAFE_LICENSE_API_URL=https://license-manager-olive.vercel.app/api/validate
```

`FAILSAFE_LICENSE_API_URL` is optional.

## License

MIT