npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@laikacms/decap

v3.1.0

Published

Decap CMS server-side integrations for Laika CMS: OAuth2, the decap-api server adapter, and the Laika CMS backend for the Decap admin UI.

Readme

@laikacms/decap

Decap CMS server-side integrations for Laika CMS: the OAuth2 server, the Decap-compatible decap-api adapter, and the createLaikaBackend() Decap CMS backend (@laikacms/decap/decap-cms-backend-laika).

Moved (July 2026, DCMS-492): the client-side pieces that used to live here now ship with the @laikacms/decap-cms fork: icon widgets (@laikacms/decap-cms/widgets/lucide-icon, …/widgets/radix-icon), the AI chat widget (…/widgets/aichat) and server adapter (…/ai), the embedded-entry editor component (…/editor-component-embedded-entry), config type utilities (…/config-types), and the Dutch locale (…/locales/nl, bundled with all other locales). The @laikacms/decap-ai package is discontinued.

pnpm add @laikacms/decap

Exports

Backend & API

| Export | Purpose | | ----------------------------------------- | --------------------------------------------------- | | @laikacms/decap/decap-api | Decap-compatible HTTP API on top of a Laika storage | | @laikacms/decap/decap-oauth2 | OAuth2 server (GitHub-style) for Decap login | | @laikacms/decap/decap-cms-backend-laika | Decap CMS backend (createLaikaBackend()) |

decap-api options

Key options accepted by decapApi(options):

| Option | Type | Required | Description | | ------------------------- | -------------------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | documents | DocumentsRepository | yes | Document storage backend | | storage | StorageRepository | yes | Raw file storage backend | | assets | AssetsRepository | no | Binary asset storage; enables the /assets endpoint when provided | | locks | LockStore | no | Shared store enabling advisory entry locking; mounts the /locks endpoint (see below) | | locksTtlMs | number | no | Advisory-lock lifetime in ms. Defaults to 5 minutes (matches the Decap admin's refresh cadence) | | basePath | string | no | URL prefix for all endpoints (e.g. '/api/decap') | | authenticateAccessToken | (token: string) => Promise<User> | yes | Validates a Bearer access token and returns the principal's identity | | authenticateApiToken | (key: string) => Promise<User> | no | Validates an API key sent via X-API-Key or Authorization: ApiKey for M2M access | | authorize | (ctx: AuthorizeContext) => boolean \| Promise<boolean> | yes | The authorization gate — decides what the authenticated principal may do. Receives the pre-parsed ctx (below). Return false to reject with 403. Fails closed if it throws. | | logger | Pick<Console, 'error'\|'warn'\|'info'\|'debug'> | no | Receives structured diagnostic output; forwarded to storage, documents, and assets API sub-handlers | | cors | CorsOptions | no | CORS configuration; required when the admin UI is served from a different origin than the API. Set origins: '*' for local dev, explicit origins list for production. |

User type

The authenticate* callbacks return a User — the principal's identity, nothing more. Authorization is a separate concern handled by authorize (see below), so User carries no access/permission fields. The built-in identity fields are:

| Field | Type | Required | Description | | -------------- | -------- | -------- | ------------------------------------------------------------------------------- | | id | string | yes | Unique identifier for the principal | | email | string | yes | Email address | | name | string | no | Display name | | passwordHash | string | no | Stored by decap-oauth2; stripped before the user object is sent to the client |

Extend User with whatever identity/claim fields your authorize policy needs by augmenting the module:

declare module '@laikacms/decap/decap-api' {
  interface User {
    roles: string[];
    organizationId: string;
  }
}

authorize — the authorization gate (required)

Authentication answers who the principal is; authorize(ctx) answers what they may do. Return true to allow, false to reject with 403 Forbidden before the request reaches any repository. A thrown callback fails closed. There is no implicit default — you must state the policy.

ctx is an AuthorizeContext — the principal plus the request pre-parsed so a policy needn't re-parse the URL:

interface AuthorizeContext {
  user: User; // identity from your authenticate* callback
  request: Request; // raw request, for anything the parsed fields don't cover
  method: string; // upper-cased HTTP method
  domain: 'documents' | 'storage' | 'assets' | 'session';
  operation: 'read' | 'create' | 'update' | 'delete' | 'publish' | 'unpublish';
  collection?: string; // first path segment after the domain (the API resource)
  itemId?: string; // item key/slug, URL-decoded, when present
}
// Allow everything authenticated:
authorize: () => true,

// Read-only — allow reads, reject anything that mutates:
authorize: ctx => ctx.operation === 'read',

// Role-based (with `interface User { roles: string[] }` augmented in):
authorize: ctx =>
  ctx.operation === 'read' ? true : ctx.user.roles.includes('editor'),

"Scopes" and "roles" are just identity fields you attach to the User and check here — the API imposes no fixed permission vocabulary. Map ctx.domain/ctx.collection/ctx.operation to your own permissions; reach for ctx.request only for what the parsed fields don't cover.

decap-api return value

decapApi(options) returns a DecapApi object:

| Method | Description | | ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | fetch(request: Request): Promise<Response> | Main catch-all handler — route all Decap API traffic here | | authenticateRequest(request: Request): Promise<Response \| User> | Validates the request's auth (Bearer or API key) and returns a User on success, or a Response (401/403) on failure. Use in SSR route handlers to protect pages or inject the current user without routing through the API. |

Advisory entry locking

When two editors open the same entry, the Decap admin (@laikacms/decap-cms ≥ 4.1.0) can show a "being edited by X" banner. This is an advisory signal only — it never blocks a write; it just warns before someone clobbers a concurrent edit. Pass a locks store to arbitrate those locks server-side, so two different browsers/users see the same lock (the admin's bundled fallback only shares locks between tabs of one browser):

import { createInMemoryLockStore, decapApi } from '@laikacms/decap/decap-api';

const api = decapApi({
  // …documents, storage, authenticate*, authorize…
  locks: createInMemoryLockStore(),
});
  • Opt-in. Omit locks and the /locks endpoint isn't mounted; the admin silently degrades to "locking unsupported" (no banner, no errors).
  • Owner identity is the authenticated principal's (user.email), derived server-side — never trusted from the request body — so a caller can't release or override someone else's lock.
  • Bring a shared store for multi-node deploys. createInMemoryLockStore() is a single-instance reference; on more than one node its locks are invisible across nodes. Implement LockStore (a small TTL key/value: get/set/delete) over Redis, a KV namespace, or a DB table. Locks are advisory, so the store is best-effort — it need not be strictly atomic.

The client backend (createLaikaBackend()) implements the admin's four optional lock methods against this endpoint automatically — no extra wiring.

decap-cms-backend-laika options

createLaikaBackend(options?) accepts an optional options object:

| Option | Type | Default | Description | | ------------------------ | -------------------------------------------------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------- | | getDocumentsRepository | (opts: GetDocumentsRepositoryOptions) => DocumentsRepository | new DocumentsJsonApiProxyRepository(...) | Override the documents repository factory — use to add interceptors, logging, or custom routing | | getAssetsRepository | (opts: GetAssetsRepositoryOptions) => AssetsRepository | new AssetsJsonApiProxyRepository(...) | Override the assets repository factory | | documentsApiBaseUrl | string | Url.combine(base_url, api_root) + '/documents' | Explicit documents API base URL; use when documents and assets APIs are served from different hosts or base paths | | assetsApiBaseUrl | string | Url.combine(base_url, api_root) + '/assets' | Explicit assets API base URL | | onWarning | (error: LaikaError) => void | console.warn(...) | Hook called for every recoverable warning — use for structured logging, Sentry breadcrumbs, or observability toasts |

opts.baseUrl in custom factories — both GetDocumentsRepositoryOptions.baseUrl and GetAssetsRepositoryOptions.baseUrl receive the combined API URL (Url.combine(base_url, api_root) from the Decap backend config), not the raw base_url. With base_url: 'https://api.example.com' and api_root: '/api', opts.baseUrl is 'https://api.example.com/api'. The default factories append /documents and /assets to this value respectively, or use documentsApiBaseUrl / assetsApiBaseUrl when those are set.

The backend: block in your Decap config also accepts:

| Field | Type | Description | | ------------- | ---------- | -------------------------------------------------------------------------------------------------------------- | | acceptRoles | string[] | Restrict access to users whose role matches one of the listed values (e.g. acceptRoles: ['admin', 'editor']) |

Example with split API hosts and observability:

import { createLaikaBackend } from '@laikacms/decap/decap-cms-backend-laika';
import * as Sentry from '@sentry/browser';

const LaikaBackend = createLaikaBackend({
  documentsApiBaseUrl: 'https://api.example.com/documents',
  assetsApiBaseUrl: 'https://cdn.example.com/assets',
  onWarning: err => {
    Sentry.addBreadcrumb({ category: 'laika', level: 'warning', message: err.message });
  },
});

export default LaikaBackend;

resolveLaikaBackend — dev/remote backend selector (LCMS-449)

@laikacms/vite-plugin's localApi option (Slice 1) mounts LaikaCMS's own JSON:API on the Vite dev server at /__laika by default, so Decap can read and write local content while you develop instead of mutating a remote backend. resolveLaikaBackend({ local, remote }) picks the Decap backend: config to hand to CMS.init — the local one while vite dev is running, the remote OAuth one everywhere else — so a single admin config needs no manual switching:

import { createLaikaBackend, resolveLaikaBackend } from '@laikacms/decap/decap-cms-backend-laika';
import CMS from 'decap-cms-app';

CMS.registerBackend('laika', createLaikaBackend());

CMS.init({
  config: {
    backend: resolveLaikaBackend({
      remote: { name: 'laika', base_url: 'https://api.example.com', app_id: 'your-app-id' },
    }),
    collections: [
      /* ... */
    ],
  },
});

The selector reads import.meta.env.DEVthis requires the admin config to be bundled by Vite; a standalone/non-Vite admin is treated as remote. It fails safe to remote whenever the dev flag isn't truthy, so a production build (or any context where Vite hasn't substituted the flag) never targets the local endpoint.

| Option | Type | Required | Description | | -------- | -------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | local | LocalLaikaBackendOptions | no | { basePath?, devToken? } — overrides for the local backend. basePath defaults to /__laika (must match localApi.basePath); devToken defaults to a fixed dummy value | | remote | LaikaBackendModuleConfig | yes | The remote OAuth backend: config, used as-is whenever the dev flag isn't truthy | | dev | boolean | no | Overrides import.meta.env.DEV; only tests should pass this — real usage relies on the default so no wiring is needed |

The local backend uses dev_token under the hood — the same mechanism DevAuthenticationPage already renders for whenever config.backend.dev_token is set (see decap-cms-backend-laika/laika-backend.ts) — so no interactive login happens locally. Both createLaikaBackend and DevAuthenticationPage stay exported for hand-wiring a custom local/remote arrangement instead of using resolveLaikaBackend.

The local API's /session endpoint is a trivial stub. LaikaBackend.authenticate() always pings ${apiUrl}/session to resolve a display identity, even for the dummy-token local path. @laikacms/vite-plugin's localApi therefore also mounts ${basePath}/session, returning a fixed unauthenticated stub identity — not a fourth repository-backed sub-API.

i18n

i18n bundles are exposed per-module: …/decap-oauth2/i18n, …/decap-oauth2/i18n/en, …/decap-oauth2/i18n/nl.

Companion packages

Documentation

See the laikacms repository for setup and integration guides.

License

MIT