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

@apextelemed/partner-core

v0.14.0

Published

Shared frontend library for telehealth patient portals (api client, stores, headless flows).

Readme

@apextelemed/partner-core

Shared library for the telehealth patient-portal sites (perihelion, axiom-health, sculpted-sciences). Owns the API client, persistent stores, and — over time — headless flow hooks that let each site render its own UI on top of a shared state machine.

The local checkout for this repo lives at the-frontend/ for historical reasons; the published package name is @apextelemed/partner-core.

Documentation

Consumer documentation lives on the Apex Telemed developer docs site, next to the Partner API and survey helper docs:

https://thefortifiedgrp.github.io/apex-devdocs/partner-sdk/

Those pages are generated from this package's sources and are the reference for consumers. The rest of this README is maintainer material: workspace layout, publishing, the export map, and local development. Keep the site in sync when the public surface changes; its source is the public apex-devdocs repo.

Agent guidance

The Claude skill for this package now lives in the shared plugin repo (claude-kobayashi, skill apex-partner-core) rather than in .claude/skills/ here. It moved because the consumer sites are the audience that needs it most, and a repo-local skill only loads for someone sitting in this checkout.

/plugin marketplace add thefortifiedgrp/claude-kobayashi
/plugin install systems@kobayashi

Edit it there — porting a site, changing the package, the landmine list, and the public-surface reference all live in that skill.

A PR that changes this package's public surface updates that plugin in the same PR. The exports, the Primitives interface, the PortalConfig shape and the flow hook signatures are consumed by every site, and the plugin's public-surface.md is what the consumer sites read instead of this repo. A stale surface reference is worse than none — it gets believed.

What's in here

src/
  config.ts           configure({ apiBase, tenantKey, storagePrefix })
  api/
    client.ts         apiFetch + namespaced API modules (auth, payments, subs, ...)
  stores/
    auth.ts           authStore singleton (login, signup, logout, profile)
    cart.ts           cartStore singleton (items, promo, persistence)
    medication.ts     medicationStore singleton (selected drug)
  flows/              headless hooks for survey/checkout/signup/dashboard/etc.
  pages/              Tier 3 themed page components
  primitives.tsx      the Primitives contract sites implement
  theme/              CSS-variable stylesheets + UnoCSS preset
  testing/            the seam suite consumers run against their own site

Consumer setup

Sites must call configure() once at boot, before any store or API import is touched:

// src/index.tsx
import { configure } from '@apextelemed/partner-core/config';

configure({
  apiBase: import.meta.env.VITE_API_BASE_URL ?? '/api',
  tenantKey: import.meta.env.VITE_TENANT_KEY ?? 'perihelion-dev-key',
  storagePrefix: 'perihelion', // unique per site
});

// then mount the app

Imports use subpath exports:

import { authStore, cartStore } from '@apextelemed/partner-core/stores';
import { paymentsApi, subscriptionsApi, type User } from '@apextelemed/partner-core/api';

Distribution

Published to npm as a public package:

npm install @apextelemed/partner-core

Cut a release by bumping version in package.json, merging, then pushing a matching tag: git tag v0.1.0 && git push --tags. .github/workflows/release.yml builds, runs the guards, and publishes. A tag that is not on main is refused before any build runs.

Auth is npm trusted publishing (OIDC) — no token. The publisher is configured for this package on npmjs.com and matched on the workflow's filename, so renaming release.yml breaks publishing until that config is updated. There is deliberately no token fallback: npm prefers a token whenever one is set and never attempts the OIDC exchange, so a fallback would mean trusted publishing was never actually exercised.

The dev channel

Every push to dev also publishes, from the same workflow, a pre-release of the next version under the npm dist-tag dev:

npm install @apextelemed/partner-core@dev   # e.g. 0.10.0-dev.57

It is the same packed tarball a release ships, with the same guards run on it, so a dev site exercises the real packaging before anything is released.

  • latest never moves on a dev publish. A pre-release sorts below its base (0.10.0-dev.57 < 0.10.0), so @latest and ordinary ranges never resolve to one; the workflow also checks latest afterwards and fails if it changed.
  • The tag release refuses any pre-release version, so a -dev.N build cannot be published as latest by tagging it.
  • The version is computed in CI (scripts/dev-version.mjs) and never committed: dev's package.json keeps the last released version until the release bump.
  • A site that wants a moving dev build installs @dev in its dev deploy only; a site that pins installs an exact X.Y.Z-dev.N. Production builds stay on latest or a released pin.
  • Dev builds are public on npm like releases are. Don't push anything to dev that must stay private until release.

provenance is off while this repo is private; turn it back on in publishConfig once it is public. 0.1.0 has no attestation and cannot be given one retroactively.

License

Source-available, not open source. The Apex Telemed Platform Integration License in LICENSE lets you use, modify, and redistribute the package however you like — as long as it is for building and operating a site that integrates with Apex Telemed's platform on behalf of an Apex Telemed customer. Any other use needs a separate license. There is no conversion to an open source license. package.json declares SEE LICENSE IN LICENSE, so dependency scanners will show it as a custom license; that is expected.

What ships, and why the export map looks like that

Only dist/ is published (files: ["dist", "README.md", "LICENSE"]), with sourcemaps. src/ is not in the tarball.

Each entry is exported under several conditions:

"./pages": {
  "apex-src": "./src/pages/index.tsx",
  "solid":    "./dist/pages/index.jsx",
  "import":   { "types": "./dist/pages/index.d.ts", "default": "./dist/pages/index.js" }
}
  • solid — JSX with types stripped but the JSX not compiled. vite-plugin-solid enables this condition and compiles it in the consumer's app, which is the best output: correct reactivity, real HMR, one Solid runtime.
  • import — the same code compiled with babel-preset-solid, for anyone resolving without that plugin (SSR, vitest, other bundlers).
  • apex-src — a private condition used only inside this workspace; see "Local development".

Shipping raw .tsx and letting consumers compile it — which is what the file: symlinks in this workspace effectively do — is not an option for a real install. Vite pre-bundles dependencies with esbuild, and esbuild cannot perform Solid's compile-time JSX transform; components render once and never update.

Every entry in build.entries.mjs must be .tsx, including the ones with no JSX in them. tsup-preset-solid emits the solid condition only for .tsx entries and builds those in a separate tsup invocation, i.e. a separate chunk graph. Mix the two and a consumer resolving through solid draws pages from one graph and config/stores from the other — two copies of modules whose whole design is that there is one: configure() writes a current the stores never read, and the host app's chrome watches a different authStore than the package's pages do. Nothing throws. tests/published/singleton.test.mjs guards it, and npm run smoke:pack proves the real tarball resolves correctly by installing it into a throwaway Vite app.

package.json's exports is generated — edit build.entries.mjs and run npm run gen:exports. CI fails if the two drift.

Local development

Sites in this workspace still link the package by path ("@apextelemed/partner-core": "file:../the-frontend"). Because the export map now points at dist/, a link alone would mean editing core requires a rebuild before the site sees it.

The apex-src condition restores instant HMR. In the consumer site's vite.config.ts:

resolve: {
  conditions: ['apex-src'],
  dedupe: ['solid-js'],
}

That resolves every subpath to the-frontend/src/* instead of dist/*, so Vite compiles core as app source exactly as it did before. Set it in the site's vitest and Playwright configs too — miss one and that runner silently tests dist while the dev server tests src.

Two things about the name. It is not called development: Vite defaults resolve.conditions to ['module', 'browser', 'development|production'], so a development key would be enabled on every consumer's dev server — including external clients, who would resolve into a src/ this package does not publish, breaking vite dev while vite build passes. And ./testing deliberately has no apex-src condition: Playwright refuses to strip types from files under node_modules, so that entry stays prebuilt even in dev.

Alternatively, run npx tsup --watch in core and keep consuming dist/.

Testing a consumer site (the seam suite)

This package ships the test suite consumer sites run against themselves, from @apextelemed/partner-core/testing. A site supplies a ~40-line adapter describing itself; the checks come from here.

The division is deliberate. This repo already proves the funnel — npm run test:e2e drives these pages against a real backend across two tenant archetypes. What no test here can prove is whether a particular site wired the package up correctly, and that is a narrow, fast set of questions:

| check | catches | | --- | --- | | routes | a route that throws or renders nothing on boot | | boot | a stored session dropped during a cold page load | | storage | a key written without the site's prefix, so site and package read different storage | | singleton | site chrome that never sees the package's auth state |

Everything is mocked — no backend, no credentials, no fixtures. That is a requirement, not a convenience: a suite needing secrets cannot run on a fork PR, cannot be a required check, and so will not be the thing that stops a broken wiring from shipping.

Wiring a site

Three files. See perihelion for a worked example.

e2e/seam.spec.ts — the only site-specific part:

import { test, expect } from '@playwright/test';
import { defineSeamSuite } from '@apextelemed/partner-core/testing';

defineSeamSuite(
  {
    siteName: 'acme',
    storagePrefix: 'acme',        // must match the site's configure()
    apiBase: '/api',
    routes: { home: '/', login: '/login', signup: '/signup', dashboard: '/dashboard' },
    nav: { loggedOutText: 'Log In', loggedInText: 'Dashboard', logOutText: 'Log Out' },
    patient: { email: '[email protected]', password: 'test1234' },
    allowStorageKeys: [],         // any localStorage key the SITE writes
  },
  { test, expect },
);

test and expect are passed in rather than imported inside the suite. Sites link this package as file:../the-frontend, i.e. a symlink, and Node resolves a symlinked module's imports from its real path — so an import there would load a second copy of @playwright/test, which Playwright rejects outright.

playwright.seam.config.ts — call assertSeamEnv() at the top, then a normal config whose testMatch is seam.spec.ts.

package.json:

"test:seam": "NODE_OPTIONS=--preserve-symlinks playwright test --config playwright.seam.config.ts"

--preserve-symlinks is not optional, and cannot be applied from inside the config — it has to be set before Node starts. Without it the package resolves outside the site's project root and Playwright fails with "Test not found in the worker process", which says nothing about symlinks. assertSeamEnv() exists to turn that into a message that names the real problem. See src/testing/env.ts.

The CI gate

Sites call the reusable workflow in .github/workflows/site-seam-suite.yml rather than copying it:

jobs:
  seam:
    uses: thefortifiedgrp/apex-partner-core/.github/workflows/site-seam-suite.yml@dev
    secrets:
      core-token: ${{ secrets.APEX_CORE_TOKEN }}

It handles the sibling checkout of core (../the-frontend), builds the test kit, and runs the site's test:seam. It no longer clones or builds apex-survey-helper — core resolves the survey packages from the registry now.

Changing the suite

The checks live in src/testing/. Changing one changes every site at once — which is the point, and the reason to be careful:

  • src/testing/storage.ts declares every localStorage key this package writes. storage.contract.test.ts scans the source and fails if a key isn't declared, so the downstream audit can't quietly narrow. Add the suffix in the same commit that adds the key.
  • src/testing/fixtures.ts is deliberately free of imports from src/api/ — its declarations ship, and a relative type import would resolve against a dist/api/ that is never built. fixtures.contract.test.ts keeps the shapes honest instead.
  • npm run build:testing compiles the entry point. Core's CI runs it, because a broken emit here fails in every consumer site's PR rather than in this one.

Three tiers, picked per page

Different pages have different ratios of (logic complexity) × (visual divergence). Pick the highest tier each page can support:

| Tier | Ships | Use when | |---|---|---| | 1. Logic | api client, stores, types | always | | 2. Headless flow | hook returning { state, actions, derived } | site wants full UI control | | 3. Themed page | <SurveyPage/> etc. that consume usePrimitives() | site is happy to render through the package's structure with its own primitive components |

Sites that adopt Tier 3 must provide a primitive set via context (see below). Sites can mix per page: one site might use the themed <SurveyPage> but write its own dashboard JSX on top of useDashboardData().

Primitive set (Tier 3)

Sites consuming Tier 3 components must provide implementations of the Primitives interface and wrap the app in <PrimitivesProvider>:

import { PrimitivesProvider, type Primitives } from '@apextelemed/partner-core';
import { Button, Card, TextField, /* ... */ } from './primitives';

const sitePrimitives: Primitives = {
  Card,
  TextField,
  Textarea,
  Select,
  RadioGroup,
  CheckboxGroup,
  Button,
  Banner,
  StatusPill,
  Spinner,
  Heading,
  StepIndicator,
};

render(() => (
  <PrimitivesProvider value={sitePrimitives}>
    <App />
  </PrimitivesProvider>
), root);

The interface is defined in src/primitives.tsx. It is intentionally small — every prop on a primitive is a contract every consumer site must implement. Resist the urge to add prop variants; extend the interface only when a Tier 3 component genuinely needs the variant.

What's shipped

| Surface | Subpath | Tier | |---|---|---| | Boot configuration | /config | 1 | | API client + types | /api | 1 | | Stores (auth, cart, medication) | /stores | 1 | | Headless flow hooks | /flows | 2 | | Tier 3 primitives contract | /primitives | 3 | | Tier 3 page components | /pages | 3 | | Themes (CSS variables + UnoCSS preset) | /theme, /theme/{admin,flow,affiliate,member-portal}.css | — | | Consumer seam suite | /testing | — |

Tier 3 pages

Nineteen components ship from /pages. Per-page props and behavior are documented on the docs site; this list is the inventory.

Customer funnel — <SurveyV2Page> (questionnaire composed from drugIds, qualifies guests), <SurveyPage> (deprecated v1 — needs a medication chosen first), <CheckoutPage>, <CartCheckoutPage>, <SignupPage>, <RegisterPage>, <SubscribePage>, <UpdatePaymentMethod>.

Auth — <ForgotPasswordPage>, <ResetPasswordPage>, <AffiliateForgotPassword>, <AffiliateResetPassword>.

Portals — <AdminDashboard>, <EmailTemplates>, <PatientDashboardPreview>, <MemberPortal>, <AffiliatePortal>, <AffiliateRegister>, <AffiliateLogin>.

Tier 2 flow hooks

Twenty hooks ship from /flows, alongside the pure helpers each flow module exports (formatters, buildTrackerView, the customer-360 derivations, the address and subscription-status vocabularies). Signatures are on the docs site.

  • Survey — useSurveyV2Flow; useSurveyFlow (deprecated v1)
  • Purchase — useCheckoutFlow, useCartCheckoutFlow, useSignupFlow, useRegisterFlow, useSubscribeFlow, useSubscriptionIntakeFlow, useUpdatePaymentMethodFlow
  • Portal data — useDashboardData, useMemberPortalData, useMessagesThread
  • Catalog — useCatalog, useProductDetail, useMedicationSelect
  • Affiliate — useAffiliateStats, useAffiliateConversions, useAffiliateDownline, useAffiliateLinks
  • Forms — useAddressPair

useSubscriptionIntakeFlow quotes the member's own offer as well as a typed promo code: with a session it reads offersApi.mine() and pricing() applies whichever of the two is worth more (never both; a tie goes to the code), the same rule the server charges by. pricing().offer is set only when the offer is the one applied — render it as its own line (offerSummaryText(offer) gives "$10 off every month", offer.deadlineText the "claim by" date) — and promoNotice() explains a valid code that lost to it. A site that totals a cart itself uses the same rule through bestOfferDiscount(offers, { productIds, baseCents }).

Hooks with no shared JSX (dashboard, messages, catalog, affiliate) are Tier 2 only: the site renders its own page on top.

Deep-linking into Messages

<MemberPortal>'s Messages tab reads its destination from the URL, so any link into the portal — an email, a support macro, another tab's CTA — can pre-select it instead of landing on the merged conversation list:

  • #messages — open the tab itself.
  • ?c=<id> — open this exact conversation/thread. Wins over to/cat when both are present; its channel is implied by the thread's own source.
  • ?to=care|support — pre-select which side of the merged inbox to land on.
  • ?cat=<category> — a support category (billing, orders, account, technical, other); support-only, and implies to=support when to is omitted. Unknown values on any of these params are ignored, not surfaced.

The same shape is MessagesIntent (exported from /flows), which every in-portal CTA passes to onOpenMessages(intent?) — the URL query params are just that intent's serialization for links, not a separate mechanism.

Admin

<AdminDashboard> is themed via CSS variables, not the Primitives interface — back-office UIs don't differentiate the brand. Sites import the default stylesheet once and override individual variables to match their look:

// src/index.tsx
import '@apextelemed/partner-core/theme/admin.css';
/* site stylesheet — override any variables that need to match the brand */
:root {
  --app-admin-bg: #f7f9fb;
  --app-admin-fg: #0f172a;
  --app-admin-radius: 0;
  --app-admin-font-display: 'Orbitron', system-ui, sans-serif;
}

For sites that use UnoCSS, an optional preset adds shortcut classes (bg-admin-bg, text-admin-fg, etc.):

import { adminPreset } from '@apextelemed/partner-core/theme';

export default defineConfig({
  theme: { ...adminPreset.theme },
  shortcuts: { ...adminPreset.shortcuts },
});

The full variable list is exported as ADMIN_CSS_VARIABLES from /theme for sites that want to generate override stylesheets programmatically.

<AdminDashboard> has no single Promotions tab: Sales, Promo Codes and Offers are separate tabs, grouped under Marketing with Affiliates and Funnel. They are tabs of the dashboard, not separate pages.

The three are different things. A sale is for everyone and a promo code for whoever types it; an offer (offers tab, adminApi.getOffers() and friends) is a discount one person holds before they subscribe, with their own deadline to claim it. The tab lists offers with their grant counts and the sequences handing each out, and each offer has a grants page for giving it to a person by hand or taking an unclaimed grant back. A sequence hands an offer out from its Settings (Offer + valid-for days), next to the savings-members-only switch.

What a sequence achieved is the Results tab (sequence-results, under Email after Sequences): per sequence, how many people were emailed, followed a link and bought; the same per email; the offer's grants by state; what buyers have paid; and who they were. A site that passes enabledTabs has to add 'sequence-results' to see it. Links are counted by configure() itself: a sequence email's link arrives carrying ?fs=…, and the first configure() of the page load posts it to the backend and takes it off the address bar. Sites do nothing for this beyond being on a version that has it.

Customers and the detail-view standard

The Customers tab is a customer 360: the list carries a lifecycle bucket per account (active, past due, paused, payment unverified, canceled, card declined, awaiting prescriber, never subscribed) with facet counts, and opening a row replaces the list with a full-width detail — lifecycle reason, needs-attention list, journey stepper, subscriptions with billing attempts, prescriptions on the member-portal fulfillment beads, payments, support threads, pre-signup activity and a timeline. Subscriptions live inside it; the 'subscriptions' tab id still compiles but renders the customer list filtered to active subscribers.

Deep links: the tab reads /admin/customers/<uid> at mount and keeps it in sync with history.pushState when it is mounted under that path. Sites that want to own the URL pass customers={{ activeUid, onSelectCustomer }}, the same shape as affiliates. Sites whose admin router maps paths by exact match should prefix-match /admin/customers so a copied link lands on the customer.

The layout primitives the customer detail is built from (DetailHeader, DetailGrid, Section, AlertItem, Stepper, Timeline, Chip, Modal in pages/admin/shared.tsx, styled by the app-admin-detail*, app-admin-section*, app-admin-alert*, app-admin-stepper* and app-admin-timeline* classes) are the standard for admin detail views; the remaining side panels move onto them tab by tab. No new CSS variables were added — the detail view themes through the existing contract.

What stays out of this package

  • Page-level JSX for customer-facing pages where visual divergence is too high (Dashboard, Catalog, Login, Home, ProductDetail, Messages)
  • Branding components (OrbitField, MoleculeViz, hero illustrations)
  • Route configuration (App.tsx)
  • Per-site primitive implementations (each site builds its own Button/Card/TextField/etc. matching its design language)
  • Theme tokens (uno.config.ts)
  • Per-site product catalogs / seed data