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

hazo_admin

v0.11.1

Published

Standard site-admin package — auth-gated admin shell + panel kit + drop-in /admin preset

Readme

hazo_admin

Standard site-admin package for hazo apps — auth-gated admin shell + panel kit + drop-in /admin preset.

Install

npm install hazo_admin

Peer deps (required): hazo_core, hazo_ui, react, react-dom, next, lucide-react Peer deps (optional, each feature-gated): hazo_auth, hazo_connect, hazo_logs, and other hazo_* packages

Setup

See SETUP_CHECKLIST.md for step-by-step setup.

Tailwind v4 Setup (Required for consuming apps using Tailwind v4)

If your consuming app uses Tailwind CSS v4, you must configure @source to scan this package's Tailwind classes. Add the following to your app's globals.css (or main CSS file) AFTER the tailwindcss import:

@import "tailwindcss";

/* Required: Enable Tailwind to scan hazo_admin's Tailwind classes */
@source "../node_modules/hazo_admin/dist";

Without this directive, Tailwind v4's JIT compiler will not generate CSS for utility classes used in hazo_admin (colors, spacing, hover states, etc.), resulting in unstyled or broken layouts.

Note: If you are using Tailwind v3, you do not need to add @source.

AdminGate

The AdminGate is the auth/permission/scope gate that protects all admin routes.

adminGate(request, opts)

Non-throwing decision function. Returns an AdminGateResult:

  • { allowed: true, user, permissions, scope_ok? } — access granted
  • { allowed: false, status: 403, reason: 'unauthenticated' | 'forbidden' | 'scope_denied', missing_permissions? } — access denied
import { adminGate } from 'hazo_admin';

const result = await adminGate(request, {
  required_permissions: ['admin_system'],
  scope_id: params.orgId,  // optional — for HRBAC scope isolation
});

withAdminGate(handler, opts)

Route HOF. On denial returns a 403 JSON response; on allow calls the handler.

import { withAdminGate } from 'hazo_admin';

export const GET = withAdminGate(
  async (request, gate) => {
    // gate.user and gate.permissions are available here
    return Response.json({ ok: true });
  },
  { required_permissions: ['admin_system'] }
);

Permission Constants

import { HAZO_ADMIN_PERMISSIONS } from 'hazo_admin/client';

// Core
// HAZO_ADMIN_PERMISSIONS.ADMIN_SYSTEM    = 'admin_system'      — full admin access (default gate)
// HAZO_ADMIN_PERMISSIONS.ENV_MIGRATE     = 'env.migrate'       — run env migrations
// HAZO_ADMIN_PERMISSIONS.ENV_MASK_MANAGE = 'env.mask.manage'   — edit masking ruleset

// Extended (v0.2.0)
// HAZO_ADMIN_PERMISSIONS.AUDIT_READ      = 'audit.read'                 — view audit trail
// HAZO_ADMIN_PERMISSIONS.SETTINGS_MANAGE = 'admin.settings.manage'      — edit app config settings
// HAZO_ADMIN_PERMISSIONS.APIKEYS_MANAGE  = 'admin.apikeys.manage'       — manage API keys
// HAZO_ADMIN_PERMISSIONS.BLOG_MANAGE     = 'admin.blog.manage'          — manage blog posts
// HAZO_ADMIN_PERMISSIONS.FILES_MANAGE    = 'admin.files.manage'         — browse / manage data files
// HAZO_ADMIN_PERMISSIONS.FEEDBACK_REVIEW = 'admin.feedback.review'      — review user feedback
// HAZO_ADMIN_PERMISSIONS.METRICS_VIEW    = 'metrics.view'               — view metrics / analytics panel

// Issues (v0.5.0)
// HAZO_ADMIN_PERMISSIONS.ADMIN_ISSUE_TRIAGE         = 'admin_issue_triage'          — triage / resolve issues queue
// HAZO_ADMIN_PERMISSIONS.ADMIN_USER_SCOPE_ASSIGNMENT = 'admin_user_scope_assignment' — grant scope role assignments

These must exist as rows in the consuming app's hazo_permissions table. The consuming app is responsible for seeding them.

Nav sections / manifest kinds

All nav kind metadata is centralized in src/lib/admin_kinds.ts. Each kind maps to a panel, a composed package, and a default permission. Contributors adding a new kind should add it to the registry there first.

| Kind | Default permission | Composed package | Status | |---|---|---|---| | users | admin_system | hazo_auth | done | | jobs | admin_system | hazo_jobs | done | | logs | admin_system | hazo_logs | done | | env | env.migrate | hazo_env | done | | masking | env.mask.manage | hazo_env | done | | health | admin_system | hazo_env | done | | audit | audit.read | hazo_audit | stub (Phase 4) | | settings | settings.manage | hazo_config | stub (Phase 4) | | api_keys | apikeys.manage | hazo_api | stub (Phase 4) | | blog | blog.manage | hazo_blog | stub (Phase 4) | | files | files.manage | hazo_files | stub (Phase 4) | | feedback | feedback.review | hazo_feedback | stub (Phase 4) | | issues | admin_issue_triage | hazo_admin (built-in) | done (v0.5.0) | | metrics | metrics.view | consumer-supplied component (e.g. hazo_umetrics) | done (consumer-component) |

metrics kind — consumer component pattern

The metrics kind has no hazo_admin dependency on hazo_umetrics. Instead, pass your metrics panel as a component prop on the section:

import { MetricsPanel } from 'hazo_umetrics';

const MANIFEST = {
  sections: [
    { kind: 'metrics' as const, component: <MetricsPanel /> },
  ],
};

If no component is supplied, the panel renders a placeholder message. This keeps hazo_admin dependency-free from any metrics package.

Issues system (v0.5.0)

The issues nav kind provides a built-in queue for actionable admin events — e.g. a user lacking a required permission triggers an issue that the nearest-scope admin can grant or deny.

Registering the issues section

const MANIFEST = {
  sections: [
    { kind: 'issues' as const },
    // custom permission if needed:
    // { kind: 'issues' as const, permission: 'my_custom_triage_perm' },
  ],
};

The default gate permission is admin_issue_triage. Add it to your hazo_permissions table and assign it to admin roles.

Issue types — consumer-defined

hazo_admin ships with zero built-in issue types. Consumers register types at boot:

import { registerIssueType } from 'hazo_admin';

registerIssueType({
  key: 'auth_permission',
  label: 'Permission Request',
  actions: [
    {
      key: 'grant',
      label: 'Grant',
      async run(issue, params, ctx) {
        // call hazo_auth to assign role or permission
        const connect = await ctx.getHazoConnect();
        // ... grant logic ...
        return { data: { roleId: params.roleId } };
      },
    },
    {
      key: 'deny',
      label: 'Deny',
      async run(issue, params, ctx) { /* optional deny side-effects */ },
    },
  ],
  buildResolutionNotice(issue, actionKey, result) {
    return {
      subject: actionKey === 'grant' ? 'Access granted' : 'Access denied',
      body: 'Your permission request has been reviewed.',
      deep_link: '/dashboard',
    };
  },
});

Custom card renderers (client)

Override how a type's issue card looks in the IssuesPanel:

import { registerIssueCardRenderer } from 'hazo_admin/client';

registerIssueCardRenderer('auth_permission', ({ issue, onTransition }) => (
  <div>
    <h3>{issue.title}</h3>
    <button onClick={() => onTransition('wip')}>Take it</button>
  </div>
));

If no renderer is registered for a type, DefaultIssueCard is used.

Issue routing config

Add to hazo_admin_config.ini:

[issue_routing]
; Map issue types to recipient strategies.
; Supported strategies: nearest_scope_admin
auth_permission = nearest_scope_admin

API routes (via createAdminPresetRoutes)

| Method | Path | Description | |---|---|---| | GET | /issues | List issues (filtered by admin's scope) | | GET | /issues/:id | Get single issue | | POST | /issues/:id/transition | Transition status — free-form: any known status → any other known status | | POST | /issues/:id/assign | Assign to a user | | POST | /issues/:id/assign-to-me | Assign to the authenticated actor (derived from the admin gate, not client input) | | POST | /issues/:id/resolve | Run an action + close the issue | | POST | /issues/archive-sweep | Move closed issues older than the configured cutoff to archivedglobal admins only (403 otherwise) | | GET | /issues/assignees | List all users assignable to issues ({ user_id, label, avatarUrl }, for the reassign picker) |

Scoped admins only see issues within their assigned scopes. Global admins (admin_system) see all.

Statuses: new / wip / on_hold / closed / archived. on_hold renders as a parked lane after Closed on the board. Transitions are unrestricted (only an unknown target status is rejected); → wip still auto-assigns the actor.

Drop-in IssuesPanel (v0.10.0, filter bar + owner picker updated in v0.11.0)

Mount the Issues triage board on its own, without the full AdminApp shell:

import { IssuesPanel } from 'hazo_admin/ui';

export default function AdminIssuesPage() {
  return <IssuesPanel basePath="/api/admin" currentUserId={currentUserId} />;
}

basePath defaults to /api/admin. The board has four columns (New → WIP → Closed → On Hold). Filter controls live in a horizontal top bar above the board — search inline, plus Type, Severity, and Age (Under 1 day / 1–7 days / 7–30 days / Over 30 days) as dropdown chips with active-count badges, a Clear · N button, and Manage types. Status isn't a facet here since it's already the board's columns.

Each card shows per-card type and owner controls. The owner control uses UserPickerSelect from hazo_auth/client (≥ 10.5.0) — avatar + name/email dropdown — populated from GET {basePath}/issues/assignees (now every user, not just resolvable admins); selecting one POSTs {basePath}/issues/:id/assign. A compact Me button next to it POSTs {basePath}/issues/:id/assign-to-me to self-claim.

The panel header also has an Archive old button that POSTs {basePath}/issues/archive-sweep (global admins only) and refreshes both boards.

Archive job

Register the built-in archive job to sweep old closed issues (default cutoff is now 1 month, was 3):

import { registerIssueArchiveJob, ADMIN_ISSUE_ARCHIVE_JOB_TYPE } from 'hazo_admin';
import { createJobQueue } from 'hazo_jobs';

registerIssueArchiveJob(jobQueue, { getHazoConnect, archiveAfterMonths: 1 });

The Archive old button in IssuesPanel covers the same sweep on demand, without needing the job scheduled.

createIssueStoreFromConnect(adapter) — PostgREST-native store (v0.6.0)

Use this factory when the consuming app connects via a PostgREST adapter (no direct SQL access). It builds the same IssueStore interface using createCrudService + adapter.claimRows so no raw SQL is issued.

import { createIssueStoreFromConnect } from 'hazo_admin';

// In your route handler or service bootstrap:
const store = await createIssueStoreFromConnect(adapter);
// store implements the full IssueStore interface
const { issue, isNew } = await store.createOrBumpIssue({ ... });

Capability sniff: if the adapter exposes a .raw() method (i.e. it is a direct-DB adapter), createIssueStoreFromConnect transparently delegates to createIssueStore instead. This lets callers use a single factory regardless of adapter type.

Async: returns a Promise<IssueStore> (lazy hazo_connect/server import on the CRUD path).

Raising issues (v0.8.0)

raiseIssue(connect, input, opts?) is the single documented ingest protocol for creating an issue from server-side code — a trusted-by-construction seam (no HTTP ingest endpoint). See design/issue_raising_protocol.md for the full contract.

import { raiseIssue } from 'hazo_admin';

const { issue, isNew } = await raiseIssue(connect, {
  type: 'auth_permission',   // must be a registered IssueTypeDef.typeKey
  scope_id: orgId,           // omit/null for a global (super-admin) issue
  severity: 'high',
  source: 'hazo_files',
  payload: { requestedPermission: 'files.manage' },
});

scope_id presence infers routing: present ⇒ scoped issue (scope admins), omitted/null ⇒ global issue (super admins only). title/summary are auto-derived from the type's buildDescriptor(payload) when not supplied.

Notify-on-raise is optional and async — only newly-created issues enqueue a hazo_admin.issue.notify job, and only if hazo_jobs + hazo_notify are installed and wired. Register the worker handler once at boot:

import { registerIssueNotifyJob } from 'hazo_admin';

registerIssueNotifyJob(worker, { getHazoConnect });

Without this wiring, issues still raise and show up in the Kanban — only the notification is skipped.


v0.2.0 Breaking changes

GET /env/list returns {name, role}[] instead of string[]

Prior to v0.2.0, GET /env/list returned a plain string array of env names. As of v0.2.0 it returns an array of objects:

// Before (v0.1.x)
// GET /env/list → string[]
// e.g. ['dev', 'prod']

// After (v0.2.0+)
// GET /env/list → { name: string; role: HazoEnvRole }[]
// e.g. [{ name: 'dev', role: 'development' }, { name: 'prod', role: 'production' }]

Consumers of this endpoint (e.g. EnvMigrationPanel) must read the role field from the response rather than re-deriving it from the env name string. This makes the panel robust to arbitrarily-named production environments.

Entries

| Import path | Contents | Server-safe | |---|---|---| | hazo_admin | adminGate, withAdminGate, HAZO_ADMIN_PERMISSIONS, AdminGateResult, createIssueStore, createIssueStoreFromConnect, registerIssueType, getIssueType, listIssueTypes, loadIssueRoutingConfig, registerIssueArchiveJob, ADMIN_ISSUE_ARCHIVE_JOB_TYPE, raiseIssue, registerIssueNotifyJob, issueNotifyJobHandler, ADMIN_ISSUE_NOTIFY_JOB_TYPE, resolveGlobalAdmins, resolveScopeAdmins | Server only | | hazo_admin/client | HAZO_ADMIN_PERMISSIONS, registerIssueCardRenderer, getIssueCardRenderer, listIssueCardRenderers, DefaultIssueCard, types | Yes | | hazo_admin/api | createAdminPresetRoutes | Server only | | hazo_admin/jobs | ENV_MIGRATE_JOB_TYPE, envMigrateJobHandler | Server only (no React) | | hazo_admin/ui | All panel components + resolveNav | Client | | hazo_admin/ui/<component> | Individual panel components (avoids barrel server-only leak) | Client | | hazo_admin/preset | AdminPresetPage (drop-in Next.js page) | Server/RSC |