Admin Pro

FoundationStable
@wabbit/tome-admin-prov0.2.3

Tier-gated pro extensions for @wabbit/tome-admin. Self-gates on TOME_ADMIN_LICENSE_KEY at registration time.

Install
  1. Get a free registry token from your credentials page. Every install from our registry needs one, free packages included.

  2. Add the registry and your token to the .npmrc at the root of your project, with your token in place of YOUR_TOKEN:

    @wabbit:registry=https://npm.wabbit.com/
    //npm.wabbit.com/:_authToken=YOUR_TOKEN
  3. Then install:

    npm install @wabbit/tome-admin-pro

Overview

@wabbit/tome-admin-pro

Tier-gated pro extensions for @wabbit/tome-admin. Ships pro widgets that self-gate on TOME_ADMIN_LICENSE_KEY at registration time.

Install

pnpm add @wabbit/tome-admin-pro

| Peer | Range | |---|---| | react / react-dom | >=19.0.0 | | payload | >=3.70 | | @payloadcms/ui | >=3.70 | | @wabbit/tome-admin | >=0.2.0 <1.0.0 | | @wabbit/tome-core | >=1.0.0 <2.0.0 |

Public API

| Export | Subpath | Description | |---|---|---| | Package version, license-gated widget wiring | . | TOME_ADMIN_PRO_PACKAGE_VERSION, the roleAnalytics / devDrawer widget definitions, and re-exports of hasLicense / getLicenseInfo / defineLicense; importing it runs the license-gated widget registration as a side effect | | hasLicense, getLicenseInfo, defineLicense, LicenseInfo/LicenseValidator types | ./license | hasLicense/getLicenseInfo read the env-var license check; defineLicense swaps in a custom validator (e.g. a real billing API) — see "License gate" below | | RoleAnalytics (logic) + RoleAnalyticsView (presentational) | ./widgets/role-analytics | Logins-by-role widget — 7d/30d/90d window, role-distribution bars | | DevDrawer (logic) + DevDrawerView (presentational) | ./widgets/dev-drawer | Audit-log viewer, capability tester, seed-state inspector, tome-* version table |

Pro widgets (v0.2)

  • Role Analytics (slug tome-pro-role-analytics) — logins by role over 7d/30d/90d (default 30d, set per placement); role-distribution view.
  • Dev Drawer (slug tome-pro-dev-drawer) — audit-log viewer, capability tester, seed-state inspector, tome-* version table.

Both widgets require the admin:view-system capability: the dashboard resolver filters them out for viewers without it, and each widget also returns nothing if rendered for such a viewer. Use the slugs above when authoring a tome-admin-layouts row that places them.

License gate

# production: real key required
TOME_ADMIN_LICENSE_KEY=tome-pro.<32 hex>.<8 hex hmac>

# development: the literal key `dev` unlocks the pro tier whenever NODE_ENV is not `production`
# (an unset NODE_ENV counts as development); it is rejected in production
NODE_ENV=development TOME_ADMIN_LICENSE_KEY=dev

The license is checked synchronously when the widget modules load — that is, when @wabbit/tome-admin-pro is first imported — by reading TOME_ADMIN_LICENSE_KEY through the installed validator. A missing, malformed, or HMAC-mismatched key = pro widgets are absent from admin.dashboard.widgets rather than rendered as disabled, and a one-time console.warn names the reason (an unset key is silent). Real billing is v0.3.

Custom validators must be installed before the root import. defineLicense() replaces the validator for later checks only; registration already ran if @wabbit/tome-admin-pro was imported first. Because ES imports are hoisted, put the defineLicense call in its own module and import that module above the package root:

// license-setup.ts
import { defineLicense } from '@wabbit/tome-admin-pro/license'

defineLicense((rawKey) => (rawKey === process.env.MY_ISSUED_KEY ? { tier: 'pro' } : null))
// payload.config.ts
import './license-setup' // must come first
import '@wabbit/tome-admin-pro'

The validator must be synchronous ((rawKey) => LicenseInfo | null); resolve anything remote before the pro widgets load. defineLicense(null) restores the built-in validator, which is useful in tests.

Consumer wire-up

// payload.config.ts
import '@wabbit/tome-admin/widgets/starter-kit'
import '@wabbit/tome-admin-pro' // side-effect: registers pro widgets if license
import { mergeAdminComponents } from '@wabbit/tome-admin/payload'

export default buildConfig(
  mergeAdminComponents(config, { shell: true, dashboard: true }),
)

Server / client posture

Every widget follows the same logic/view split, and none of the four `.tsx` files in `src/` carry a `'use client'` directive (verified by grep 2026-07-12):

  • Logic (RoleAnalytics.tsx, DevDrawer.tsx) — each wraps its view in withWidgetContext(async ({ payload, req, can }) => {...}) from @wabbit/tome-admin/widgets, a genuine async Server Component pattern: it gates on can('admin:view-system'), fetches/computes data server-side (computeRoleAnalytics, gatherAuditPanel/gatherCapabilityPanel/gatherVersionPanel/gatherSeedPanel), and returns the rendered view.
  • View (RoleAnalyticsView.tsx, DevDrawerView.tsx) — pure presentational components; no hooks, no browser-only APIs. DevDrawerView uses native <details>/<summary> for its expand/collapse sections, which needs no client JS at all.

Net: this package's entire widget surface is RSC-eligible today. If a future widget needs real client interactivity (live polling, client-side filtering), that widget's view file is the seam to add a 'use client' boundary — the logic/view split already isolates data-fetching from render, so the split itself would not need to change.

Testing

pnpm --filter @wabbit/tome-admin-pro test runs the Vitest suite. License-dependent tests set process.env.TOME_ADMIN_LICENSE_KEY (and NODE_ENV) before importing the widget modules, and call defineLicense(null) in afterEach to reset any custom validator.

Status

v0.2: license primitive + 2 pro widgets, published to the private registry (see CHANGELOG.md). The license tier model knows only 'pro' today; the admin sidebar's enterprise tier is reserved for a future validator.

Exports

  • @wabbit/tome-admin-pro
  • @wabbit/tome-admin-pro/license
  • @wabbit/tome-admin-pro/widgets/role-analytics
  • @wabbit/tome-admin-pro/widgets/dev-drawer

Changelog

v0.2.3patch

775f90a: Published packages now contain compiled JavaScript and type declarations under a one-line licence banner, and no longer include source maps. What you install: one compiled `.js` (ESM) and `.cjs` (CommonJS) file per source module, its `.d.ts` / `.d.cts` declarations, and the stylesheets, fonts and other assets a package already shipped. Every JavaScript module opens with a comment naming the package and its licence: `/*! @wabbit/<package> — © Wabbit, LLC. Wabbit Tome Commercial License (see LICENSE.md). Not for redistribution. */`. The `.map` files and the `sourceMappingURL` comments that pointed at them are gone, which roughly halves the size of each tarball. Debugging: the code is still unbundled and unminified, one readable file per module, so a stack trace points at real code with real names. Line numbers in a stack trace are one higher than before, because of the banner line. A `'use client'` directive stays the first statement of its module (the banner is a comment above it), so React Server Component boundaries are unchanged. No API change, no runtime behaviour change, and nothing to do on upgrade. In `@wabbit/tome-blocks-gallery`, the source snapshots `extractGallerySource` writes from an installed pack leave out the licence banner line, so a component or config snapshot starts at the code and a paid block's preview shows its first 15 lines of real code.

  • 775f90a: Published packages now contain compiled JavaScript and type declarations under a one-line licence banner, and no longer include source maps. What you install: one compiled `.js` (ESM) and `.cjs` (CommonJS) file per source module, its `.d.ts` / `.d.cts` declarations, and the stylesheets, fonts and other assets a package already shipped. Every JavaScript module opens with a comment naming the package and its licence: `/*! @wabbit/<package> — © Wabbit, LLC. Wabbit Tome Commercial License (see LICENSE.md). Not for redistribution. */`. The `.map` files and the `sourceMappingURL` comments that pointed at them are gone, which roughly halves the size of each tarball. Debugging: the code is still unbundled and unminified, one readable file per module, so a stack trace points at real code with real names. Line numbers in a stack trace are one higher than before, because of the banner line. A `'use client'` directive stays the first statement of its module (the banner is a comment above it), so React Server Component boundaries are unchanged. No API change, no runtime behaviour change, and nothing to do on upgrade. In `@wabbit/tome-blocks-gallery`, the source snapshots `extractGallerySource` writes from an installed pack leave out the licence banner line, so a component or config snapshot starts at the code and a paid block's preview shows its first 15 lines of real code.
v0.2.2patch

6530765: CSS files are now copied to `dist/` by a post-build script instead of a tsup `onSuccess` hook; no behaviour change, and the published `dist/` is identical.

  • 6530765: CSS files are now copied to `dist/` by a post-build script instead of a tsup `onSuccess` hook; no behaviour change, and the published `dist/` is identical.
v0.2.1patch

f900b58: The developer drawer's layouts row count, when `payload.count()` is unavailable, now uses a one-row paginated read instead of loading every layout row. The count is unchanged.

  • f900b58: The developer drawer's layouts row count, when `payload.count()` is unavailable, now uses a one-row paginated read instead of loading every layout row. The count is unchanged.
v0.2.0minor

b01ca1f: Raise the `react` / `react-dom` peer floor to `>=19.0.0` (ruled 2026-09-01). The platform declared React peers in five different shapes — `>=18.0.0`, `>=18`, `^18 || ^19`, `^18.3.0 || ^19.0.0`, `^19.0.0` — while its kernel (`@wabbit/tome-core`) and five app-layer packages already required `>=19`. Any package advertising React 18 was advertising a configuration that could not be installed alongside the kernel, so the split was never a supported matrix; it was drift. One shape now, and it is the honest one. These nine version independently of the `linked` blocks family (which gets its own coordinated bump), so they are listed here: - `@wabbit/tome-admin`, `@wabbit/tome-admin-pro` — from `^18.3.0 || ^19.0.0` - `@wabbit/tome-blocks-gallery` — from `^18 || ^19`; devDeps `react`/`@types/react` `^18.0.0` → `^19.0.0` - `@wabbit/tome-blocks-org-pack` — from `>=18.0.0`; same devDep correction - `@wabbit/tome-engine`, `@wabbit/tome-motion`, `@wabbit/tome-rpg`, `@wabbit/tome-webgl` — from `>=18` - `@wabbit/tome-ui` — from `>=18.0.0` The `^18` devDependency pins on the two block-shaped packages were already fiction: the root `pnpm.overrides` pins `@types/react` to `19.2.14`, so both have been building against React 19 types regardless. Correcting them changes the manifest, not the resolved tree. Consumer impact: a React 18 consumer can no longer install these. That install was already impossible with the kernel in the graph.

  • b01ca1f: Raise the `react` / `react-dom` peer floor to `>=19.0.0` (ruled 2026-09-01). The platform declared React peers in five different shapes — `>=18.0.0`, `>=18`, `^18 || ^19`, `^18.3.0 || ^19.0.0`, `^19.0.0` — while its kernel (`@wabbit/tome-core`) and five app-layer packages already required `>=19`. Any package advertising React 18 was advertising a configuration that could not be installed alongside the kernel, so the split was never a supported matrix; it was drift. One shape now, and it is the honest one. These nine version independently of the `linked` blocks family (which gets its own coordinated bump), so they are listed here: - `@wabbit/tome-admin`, `@wabbit/tome-admin-pro` — from `^18.3.0 || ^19.0.0` - `@wabbit/tome-blocks-gallery` — from `^18 || ^19`; devDeps `react`/`@types/react` `^18.0.0` → `^19.0.0` - `@wabbit/tome-blocks-org-pack` — from `>=18.0.0`; same devDep correction - `@wabbit/tome-engine`, `@wabbit/tome-motion`, `@wabbit/tome-rpg`, `@wabbit/tome-webgl` — from `>=18` - `@wabbit/tome-ui` — from `>=18.0.0` The `^18` devDependency pins on the two block-shaped packages were already fiction: the root `pnpm.overrides` pins `@types/react` to `19.2.14`, so both have been building against React 19 types regardless. Correcting them changes the manifest, not the resolved tree. Consumer impact: a React 18 consumer can no longer install these. That install was already impossible with the kernel in the graph.
  • ce3d12d: Pin the package version this build reports about itself (2026-09-01 sale-readiness audit §3.2, T3(g)). `src/index.ts` declared `TOME_ADMIN_PRO_PACKAGE_VERSION = '0.1.0-rc.2'` while `package.json` said `0.1.0` and the README said `0.1.0-alpha.0` — a three-way drift on a single package's version, in the one package whose job is telling an operator what is installed. Nothing compared any pair of them. The const moves to `src/version.ts` (a leaf module with no imports) as `ADMIN_PRO_PACKAGE_VERSION`, corrected to `0.1.0`, and `src/index.ts` re-exports it under the existing public name — so no consumer import changes. `tests/version.test.ts` compares it to `package.json` and asserts the const has exactly one home: the barrel re-exports rather than re-declaring, and `src/version.ts` imports nothing. Where it surfaces, and why the wrong value is worse than none: the Dev Drawer widget renders it as "which build of admin-pro is this environment running?", read by whoever is debugging a licence or widget-registration failure. A stale literal there does not break a build or a test — it silently sends that person in the wrong direction. The Dev Drawer now imports the leaf module rather than the barrel. It was reaching back through `../../index`, which side-effect-imports both widget registries — a cycle that happened to work. This is the same fix nine layer packages already carry for their `registerLayer` literal (`src/version.ts` + `tests/layer-version.test.ts`), with the repo-wide twin in `scripts/assert-layer-version.mjs`. `admin-pro` calls `registerLayer` nowhere — it is an admin-shell app package, not a layer — so that assert has never looked at it, which is exactly how the drift survived. The test is the comparison that was missing.
  • 0836ef5: dist now raw-Node loadable: relative specifiers get explicit extensions post-build. `build` gains `&& node ../../scripts/fix-dist-extensions.mjs --strict` as its last step, joining the 13 packages that already ran it. tsup builds `bundle: false` and emits relative specifiers exactly as the TypeScript source wrote them — extensionless — which bundlers resolve and raw Node does not (ESM `ERR_MODULE_NOT_FOUND`; CJS worse, `require('./x')` finds the ESM `.js` twin and Node 22+ `require(esm)` then dies on that file's own extensionless import). Every consumer outside a bundler hit this: the payload CLI under plain node, `generate:types`, `generate:importmap`, ops scripts, codegen tools. No source changes, no API changes, and bundler consumers are unaffected — extensioned relative specifiers are universally resolvable. Two supporting changes made the wiring possible, both in repo scripts rather than package source. `fix-dist-extensions.mjs` now skips bundler-asset specifiers (`.css`, `.module.css`, `.scss`, fonts, images, shaders) by explicit extension allowlist instead of reporting them as unresolvable — that single gap is why the 13 prior adopters were exactly the 13 packages that ship no CSS, since `--strict` exited 1 on any package with a relative stylesheet import. Dotted MODULE names (`./config.meta`, `./x.variants`, `./y.demo`) are deliberately NOT treated as assets and still get `.js`/`.cjs` appended. `assert-node-loadable.mjs` gained the matching carve-outs so the new repo-wide CI gate reports real defects only: a resolution failure whose path lands under `node_modules` is a peer SKIP (next@15 has no exports map, so `next/image` fails as an absolute path), and a bundler-asset load failure is an environmental SKIP (CJS surfaces it as `SyntaxError: Unexpected token '.'` raised from inside the stylesheet). Verified before/after on four packages built one at a time: print 8 FAIL → 0, readout 22 FAIL → 0, ai 3 FAIL → 0, gamification 2 FAIL → 0 (its failure was the other signature — a `directory import` missing `/index`). cop was already clean on a fresh build, so the audit's "27 of 46 fail" figure includes at least one package whose local dist was merely stale.
  • 73081e6: Manifest metadata: `homepage`, `bugs`, `engines`. All 46 publishable manifests were missing the three fields a consumer sees before any code (2026-09-01 sale-readiness audit §6). Metadata only — no source, no build, no runtime change. - `homepage` deep-links to that package README on GitHub (`.../tree/main/packages/<dir>#readme`). Without it a registry page links to the monorepo root and the reader has to guess which of 46 folders they want. - `bugs.url` points at the repo issue tracker, so a paying customer has a place to report a defect that is not email. - `engines.node` is `>=22`, matching the root `engines` and `.nvmrc` set the same day. This is a real floor, not decoration: CI on Node 20 could not expand the glob the block packs use for `node --test`, and a package installed on Node 20 fails at a runtime the installer cannot connect back to the version. The forcing function ships with the change: `scripts/assert-manifest-metadata.mjs` (root `pnpm assert:manifest-metadata`, wired into `platform-discipline.yml` beside `assert:license-metadata`) fails when any publishable manifest lacks `description`, `repository.directory` matching its own folder, `homepage`, `bugs`, `engines.node` equal to the repo floor, `license`, `files` or `sideEffects`. It reported 138 violations before this change and 0 after.
v0.1.0patch

36e537a: Every package now declares an explicit `sideEffects` field (38 added; motion/engine/forms already correct). Registration-bearing modules (render files' `registerRenderer`, `blocks/*/index.ts` `defineBlock` self-registration, widget `register.ts` files, productHooks, permission self-registrations, print templates, chrome built-in variants) are listed so bundlers can tree-shake everything else WITHOUT dropping import-time registrations — previously the field was unset, which blocked cross-module tree-shaking through the barrels entirely. Never blanket `false` on a package with registration or CSS.

  • 36e537a: Every package now declares an explicit `sideEffects` field (38 added; motion/engine/forms already correct). Registration-bearing modules (render files' `registerRenderer`, `blocks/*/index.ts` `defineBlock` self-registration, widget `register.ts` files, productHooks, permission self-registrations, print templates, chrome built-in variants) are listed so bundlers can tree-shake everything else WITHOUT dropping import-time registrations — previously the field was unset, which blocked cross-module tree-shaking through the barrels entirely. Never blanket `false` on a package with registration or CSS.
  • a93f478: Dashboard performance: nine widget compute waterfalls parallelized (activity-feed, approval-queue, pending-actions, seo-health, scheduled-publishing's per-collection scans; user-sessions, kpi-card, form-submissions independent-fetch folds; admin-pro DevDrawer panel gathers) — per-collection error isolation and result ordering preserved exactly, all 717+87 tests unmodified and green. Also: `useMeCapabilities` extracted from the Nav entrypoint to `nav/useMeCapabilities.ts` with in-flight dedup + TTL cache; `usePinnedItems` gains optional `initialItems`/`initialRowId` seeding (non-breaking) + dedup; CommandPalette query reset moved to mount-by-construction (state lives in the dialog body now); SidebarProvider's mobile-close moved from an effect to the matchMedia event source.
v0.1.0-rc.3patch

Peer ranges corrected to evidence-based floors: `@wabbit/tome-admin` `>=0.2.0 <1.0.0` and `@wabbit/tome-core` `>=1.0.0 <2.0.0` (rc.2 shipped stale carets `^0.2.1` / `^0.3.0`; the core range excluded core 1.x entirely). Consumers no longer need `--legacy-peer-deps` to install this package alongside current core/admin.

  • Peer ranges corrected to evidence-based floors: `@wabbit/tome-admin` `>=0.2.0 <1.0.0` and `@wabbit/tome-core` `>=1.0.0 <2.0.0` (rc.2 shipped stale carets `^0.2.1` / `^0.3.0`; the core range excluded core 1.x entirely). Consumers no longer need `--legacy-peer-deps` to install this package alongside current core/admin.