npm package: @m6d/cerebro
Description: Reusable frontend licensing primitives for JavaScript/TypeScript clients.
Latest version: 1.0.0

# Cerebro JavaScript SDK

Reusable frontend licensing primitives for JavaScript/TypeScript clients.

## What This SDK Covers

The SDK focuses on generic client-side licensing patterns.

It provides framework-agnostic building blocks for:

- Loading licensed features and checking feature availability
- Evaluating route access constraints (features only)
- Filtering navigation trees by access requirements

## Install

```bash
bun add @m6d/cerebro
```

For local development in this repository:

```bash
bun install
```

## Development Scripts

```bash
bun lint
bun check
bun test
bun run build
bun run sample:feature-store
```

## CI/CD and Publishing

- PRs targeting `main` run quality checks via `.github/workflows/pr-checks.yml`
- Pushes to `main` run semantic-release via `.github/workflows/release.yml`
- Releases use npm trusted publishing (OIDC), not long-lived npm tokens
- Provenance is currently disabled because npm does not support provenance from private GitHub repositories

To enable trusted publishing for `@m6d/cerebro`:

1. Open the package settings on npmjs.com for `@m6d/cerebro`
2. Add a Trusted Publisher for GitHub Actions
3. Set Organization/User, Repository, and workflow filename to `release.yml`
4. Ensure publishes run on GitHub-hosted runners (this workflow uses `ubuntu-latest`)

## Documentation

- `docs/README.md`
- `docs/licensing/README.md`
- `docs/routing/README.md`

## Samples

- `samples/README.md`
- `samples/vanilla-feature-store-sample/README.md`

## Generic Example

```ts
import {
  createFeatureStore,
  createLicenseFeatureLoader,
  evaluateRouteAccess,
} from "@m6d/cerebro";

const featureStore = createFeatureStore({
  loadFeatures: createLicenseFeatureLoader({
    url: "https://example.com/licenses/features",
  }),
});

await featureStore.load();

const decision = evaluateRouteAccess(
  {
    featureIds: ["kpis", "plans"],
  },
  {
    features: featureStore.getState().features,
  },
);

if (!decision.allowed) {
  console.log("Blocked", decision.reason, decision);
}
```

## Use Cases Coverage

These are common licensing use cases and how they map to this SDK:

- Route feature guard (`featureIds`) -> `evaluateRouteAccess(...)` or `canAccessRoute(...)`
- Filtering navigation/app lists by licensed modules -> `filterAccessibleItems(...)`
- Inline checks inside components/pages -> `featureStore.hasFeature(...)`, `featureStore.hasAnyFeature(...)`, `hasFeature(...)`
- Bootstrapping and refreshing licensed features -> `createLicenseFeatureLoader(...)` + `featureStore.load()`

This SDK intentionally does not include permission logic, notifications, router bindings, or framework-specific state wrappers.

## Angular Example

```ts
// app/core/licensing.service.ts
import { Injectable } from "@angular/core";
import {
  createFeatureStore,
  createLicenseFeatureLoader,
  evaluateRouteAccess,
} from "@m6d/cerebro";

@Injectable({ providedIn: "root" })
export class LicensingService {
  private featureStore = createFeatureStore({
    loadFeatures: createLicenseFeatureLoader({
      url: "/licenses/features",
      credentials: "include",
    }),
  });

  async init() {
    await this.featureStore.load();
  }

  hasFeature(featureId: string) {
    return this.featureStore.hasFeature(featureId);
  }

  canAccess(featureIds: readonly string[]) {
    return evaluateRouteAccess(
      { featureIds },
      { features: this.featureStore.getState().features },
    ).allowed;
  }
}
```

```ts
// app/core/feature.guard.ts
import { inject } from "@angular/core";
import { CanActivateFn } from "@angular/router";
import { LicensingService } from "./licensing.service";

export const featureGuard: CanActivateFn = (route) => {
  const licensing = inject(LicensingService);
  const featureIds = (route.data?.["featureIds"] as string[] | undefined) ?? [];
  return licensing.canAccess(featureIds);
};
```

```ts
// app/app.config.ts (or APP_INITIALIZER module setup)
import { APP_INITIALIZER } from "@angular/core";
import { LicensingService } from "./core/licensing.service";

export const licensingInitializer = {
  provide: APP_INITIALIZER,
  multi: true,
  deps: [LicensingService],
  useFactory: (licensing: LicensingService) => () => licensing.init(),
};
```

## React Example

```tsx
import {
  createFeatureStore,
  createLicenseFeatureLoader,
  evaluateRouteAccess,
} from "@m6d/cerebro";
import {
  createContext,
  useContext,
  useEffect,
  useMemo,
  useSyncExternalStore,
} from "react";

const featureStore = createFeatureStore({
  loadFeatures: createLicenseFeatureLoader({
    url: "/licenses/features",
    credentials: "include",
  }),
});

type LicensingContextValue = {
  features: string[];
  isLoading: boolean;
  hasFeature: (featureId: string) => boolean;
  canAccess: (featureIds: readonly string[]) => boolean;
};

const LicensingContext = createContext<LicensingContextValue | null>(null);

export function LicensingProvider({ children }: { children: React.ReactNode }) {
  const state = useSyncExternalStore(
    featureStore.subscribe,
    featureStore.getState,
  );

  useEffect(() => {
    featureStore.load().catch(() => undefined);
  }, []);

  const value = useMemo<LicensingContextValue>(() => {
    return {
      features: state.features,
      isLoading: state.isLoading,
      hasFeature: (featureId) => featureStore.hasFeature(featureId),
      canAccess: (featureIds) => {
        return evaluateRouteAccess({ featureIds }, { features: state.features })
          .allowed;
      },
    };
  }, [state.features, state.isLoading]);

  return <LicensingContext value={value}>{children}</LicensingContext>;
}

export function useLicensing() {
  const value = useContext(LicensingContext);
  if (!value) {
    throw new Error("useLicensing must be used inside <LicensingProvider>");
  }
  return value;
}
```

```tsx
// any component
import { useLicensing } from "./licensing-context";

function KpiPage() {
  const licensing = useLicensing();

  if (!licensing.canAccess(["kpis"])) {
    return <div>Feature not available in your license.</div>;
  }

  return <div>KPI content</div>;
}
```

## Notes

- Response parsers support both direct payloads and `Result<T>`-style payloads (`{ extra: ... }`)
- Feature checks default to case-insensitive matching
- Route `featureIds` default to `any` mode