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

@shamar/cherubim

v0.1.4

Published

Authentication and access control for Shamar admin (abilities, policies, RBAC).

Downloads

688

Readme

@shamar/cherubim

Authentication and access control for Shamar admin panels.

Cherubim follows the Loom RBAC model: users ↔ roles ↔ permissions with policy classes for record-level rules (Laravel / Adonis style).

Install

pnpm add @shamar/cherubim

Depends on @shamar/core. In Adonis apps, prefer installing @shamar/adonis — it wires Cherubim into panel routes, navigation, and the JSON API.

Related packages

| Package | Role | |---------|------| | @shamar/core | Resource DSL + ShamarUser / permission catalog helpers | | @shamar/adonis | Host: auth.* config, ApiKeyResource, middleware | | @shamar/lucid / @shamar/mongoose | Persistence for roles, permissions, API keys |

Model

  1. Permissions — string abilities: products:viewAny, products:edit, products:*, *
  2. Roles — bundles of permissions, assigned via user.roleIds
  3. PoliciesPolicy classes on Resource.policy (or auth.policies) for record rules + scopeList

Resource.can* hooks delegate to Resource.policy when set, otherwise userHasPermission().

Policy (Loom / Laravel)

import { Policy, ownedBy, can } from '@shamar/cherubim'
import type { ShamarUser } from '@shamar/core'

export class OrderPolicy extends Policy {
  static ownerField = 'createdById'

  static view(user: ShamarUser, record: Record<string, unknown>, slug = 'orders') {
    return can(user, 'orders:view') && ownedBy(user, record, 'createdById')
  }

  static edit(user: ShamarUser, record: Record<string, unknown>, slug = 'orders') {
    return can(user, 'orders:edit') && ownedBy(user, record, 'createdById')
  }

  static scopeList(user: ShamarUser) {
    if (can(user, '*') || can(user, 'orders:*')) return undefined
    return { equals: { createdById: user.id } }
  }
}

// app/resources/admin/order_resource.ts
export default class OrderResource extends Resource {
  static override slug = 'orders'
  static override policy = OrderPolicy
}

Adonis instance policies

import { BasePolicy, instancePolicy } from '@shamar/cherubim'

export default class PostPolicy extends BasePolicy {
  update(user, post) {
    return this.allows(user, 'posts:edit') && post.userId === user.id
  }
}

// config/shamar.ts
auth: {
  policies: {
    posts: instancePolicy(PostPolicy, 'posts'),
  },
}

Checks

import { Authorizer, can, userHasPermission, assertPolicy } from '@shamar/cherubim'

can(user, 'deals:export')                    // wildcard-aware
userHasPermission(user, 'deals', 'viewAny') // Loom helper
authorizer.canResource(ctx, ProductResource, 'update', record)
authorizer.assertResource(ctx, ProductResource, 'delete', record)

Permissions assignment (ORM-agnostic)

Drop Loom-style permission checkboxes into a Role form once a permissions catalog exists:

import { PermissionsAssignment } from '@shamar/core'

// in RoleResource.form()
PermissionsAssignment.make('permissionIds')

Contract (any ORM):

  1. Register a read-only permissions resource
  2. Permission records expose name, resource, ability, and label
  3. Role stores permission ids (permissionIds / permission_ids)
  4. On boot, upsert via buildPermissionCatalog(registry) from @shamar/core

Both Lucid and Mongoose adapters enrich search() with name / group / ability so grouping and wildcard cascade work. Override .relationship(...) if your slug or title attribute differs.

API credentials (PATs + machine keys)

| Kind | Principal | Abilities | |------|-----------|-----------| | pat | Owning user (roles/permissions) | Optional narrow; empty = full user access | | machine | The key itself (apikey:…) | The key’s permissions (e.g. products:*, *) |

Admin UI

@shamar/adonis ships an ORM-agnostic ApiKeyResource. Bind your model and register it:

import { ApiKeyResource } from '@shamar/adonis'
import ApiKey from '#models/api_key'

export default class AppApiKeyResource extends ApiKeyResource {
  static override model = ApiKey
}

Create via /admin/api-keys/create (normal Shamar form). Abilities are picked from the permissions catalog (AbilitiesAssignment — permission names, not free text). The plaintext secret is flashed once after create.

Header recommendation

Prefer two headers when the API is gated by a machine key and endpoints still need a user:

| Header | Credential | Role | |--------|------------|------| | X-Api-Key | Machine key | Gateway — overall API access | | Authorization: Bearer | PAT | User identity for endpoint RBAC |

Avoid X-Authorization (non-standard, easy to confuse with Authorization). Do not put both secrets in Authorization — HTTP effectively has one auth scheme per request.

X-Api-Key: shm_machine_…
Authorization: Bearer shm_pat_…

Single-credential mode still works: Bearer or X-Api-Key alone (machine or PAT).

import {
  generateApiKey,
  resolveFromApiKey,
  type ApiKeyStore,
} from '@shamar/cherubim'

const { plainText, tokenHash, prefix } = generateApiKey()

// Machine key — mobile / devices / API gateway
await db.insert({ kind: 'machine', name: 'iOS', tokenHash, prefix, abilities: ['products:*'] })

// PAT — acts as a user
await db.insert({ kind: 'pat', name: 'CLI', userId, tokenHash, prefix, abilities: [] })

await resolveFromApiKey(plainText, store, { loadUser }) // dispatches by kind
auth: {
  apiKeys: {
    resolve: (plainText) => resolveFromApiKey(plainText, store, { loadUser }),
    // Opt-in: apply RequireApiKeyMiddleware to /api/shamar
    protectApi: true,
    // When both headers present, cap user permissions by the machine key (default true)
    intersectGatewayAbilities: true,
  },
  roleResolver: { resolveRolePermissions }, // applied for PATs + session
}

Or apply the middleware only on selected routes:

import router from '@adonisjs/core/services/router'

const middleware = router.named({
  shamarApiKey: () => import('@shamar/adonis/require_api_key_middleware'),
})

router
  .group(() => {
    router.get('/mobile/products', …)
  })
  .use([middleware.shamarApiKey()])

Integration

@shamar/adonis wires Cherubim into panel routes and AdminController:

  • auth.guard, auth.resolveUser, auth.roleResolver, auth.apiKeys, auth.policies
  • Resource actions enforced on every admin/API handler
  • Navigation filtered by viewAny
  • scopeList applied to list queries and IDOR checks on show/edit/delete

See the Adonis package README for full defineConfig auth options and middleware exports.

LDAP identity helpers

Cherubim does not speak LDAP on the wire. It normalizes directory identities and group→role mapping after the host authenticates:

import {
  parseLdapUsername,
  mapGroupsToRoleIds,
  mergeExternalRoles,
  ldapExternalId,
  type ExternalIdentity,
} from '@shamar/cherubim'

AuthorizationContext.authMethod may be 'ldap' when the session user was provisioned from a directory (user.authProvider === 'ldap'). Login orchestration (loginMode, multi-domain bind) lives in @shamar/adonis (resolvePasswordLogin).