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

@commonpub/persona

v0.2.1

Published

Framework-agnostic persona brain: the persona field registry, storage-sink partition, link platforms, completeness, processing-purpose registry and every persona Zod schema. Pure TypeScript, zod only.

Downloads

668

Readme

@commonpub/persona

The persona brain for CommonPub: the typed source of truth for what a persona field is, where its answer is stored, which answers can ever be counted, and what a member is actually agreeing to when they turn on a sharing toggle.

Pure TypeScript. The only runtime dependency is zod. No framework, no ORM, no database, no HTTP, no DOM.

Why it is its own package

The dependency direction is one way and shallow:

@commonpub/persona          pure TS, zod only
      |
      | imported by
      v
packages/schema/src/persona.ts     Drizzle tables only
packages/server/src/persona/       resolution, values, consent, metrics
layers/base/**                     routes, pages, components

Nothing here imports a sibling package, and an isolation test enforces that. The result is a package that can be reasoned about, tested and replaced on its own, and a feature that can change later without reaching into eight live subsystems.

Three deliberate copies live here rather than being lifted out of the code they mirror, each with a test pinning it to its original:

  • httpUrl / optionalUrl mirror packages/schema/src/validators/_shared.ts. Importing them would create a package edge for one validator.
  • fnv1a32 mirrors the private digest in layers/base/composables/useCookieConsent.ts. Editing that file risks changing the cookie scope digest, which would silently invalidate every stored consent on every instance.
  • The field-type taxonomy is persona's own, not the contest form engine's. Persona has no required and no pii, partitions by sink rather than by a PII toggle, and has its own renderer. The two share a handful of type names and nothing else.

What is in it

| Module | Contents | |---|---| | fields.ts | PERSONA_FIELD_TYPES, PERSONA_FIELD_SPECS (closed with satisfies), and a fail-closed registry lookup | | persona.ts | PersonaField, PersonaSection, personaFieldSink, isPersonaFieldAggregatable, personaCompleteness, the link platforms, BUILTIN_PERSONA_SECTIONS | | purposes.ts | PROCESSING_PURPOSES, the purpose specs and their plain-language copy, purposeIsOfferable, purposeScopeDigest | | statistics.ts | PERSONA_STATISTICS: the legitimate-interest disclosure and the objection, with its copy. Not a purpose, and deliberately not in the registry | | thresholds.ts | METRICS_MIN_BUCKET and MIN_AUDIENCE_POPULATION, the k-anonymity floors, in one place | | schemas.ts | Every Zod schema, plus definePersonaSections for commonpub.config.ts | | digest.ts | fnv1a32 | | url.ts | httpUrl, optionalUrl |

The model, in four lines

| Layer | Who sees it | Basis | The member's control | |---|---|---|---| | Profile | everyone | published by intent | none needed, they wrote it to be read | | Answers to the operator's questions | the member and the operator | the operator asked, the member answered | answer or do not; a field appears on the public profile only where the operator set showOnProfile: true | | Community statistics | the operator | legitimate interest, disclosed | an objection (statistics.ts) | | Named third parties | the recipients the operator declared | consent, default off | the one real toggle (purposes.ts) |

Two consequences worth being explicit about.

Answers are private by default. showOnProfile is an opt IN: absent or false means the answer never reaches /u/:username. No built-in section sets it. An operator running persona for operational questions (which machines are you checked out on) publishes nothing by accident, and an operator building a maker directory opts the fields in deliberately, one key at a time.

Being counted is not a consent question. The instance holds anonymous totals over its own members whether or not anyone agrees, so asking permission whose refusal would change nothing was a dark pattern with good intentions. Statistics run on legitimate interest with an objection, and the objection is honoured in the query. The one thing consent is asked for is the one thing that is genuinely the member's to decide: whether named third parties may find them by these answers.

Two invariants worth stating out loud

Free text is never counted. PERSONA_FIELD_SPECS marks every free-text type non-aggregatable and routes it to the text sink, so there is no operator setting, anywhere, that turns a bio into statistics. personaFieldSink is the single source of truth for storage routing, and both the write path and the analytics field list read it rather than re-deriving it.

Completeness knows nothing about consent. personaCompleteness takes sections and answers, and that is all it will ever take. Progress measures what you chose to write about yourself; sharing is a separate decision. A score that moved when you flipped a sharing toggle would make consent into a game.

Usage

import { definePersonaSections } from '@commonpub/persona';

export default defineCommonPubConfig({
  persona: {
    sections: definePersonaSections([
      {
        key: 'workshop',
        label: 'Workshop',
        fields: [
          {
            key: 'machines',
            label: 'What is in your workshop?',
            type: 'multiselect',
            options: [
              { value: 'lathe', label: 'Lathe' },
              { value: 'cnc', label: 'CNC router' },
            ],
          },
        ],
      },
    ]),
  },
});

License

AGPL-3.0-or-later