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

@fhirstarter/backend

v2.1.0

Published

Server-side SMART on FHIR Backend Services (client credentials) auth engine. Owns JWT assertion, JWKS, and a proactive refresh loop, then hands off a flat token surface any FHIR client can spread in.

Readme

@fhirstarter/backend

npm CI Publish

The server-side half of @fhirstarter: a SMART on FHIR Backend Services (client credentials) auth engine. It owns the JWT client assertion, JWKS derivation, and a proactive refresh loop that keeps a valid token ready at all times, then stays out of your way. Because it never touches FHIR itself, the result is a flat handoff you spread into whatever client you already use: FHIR.client(auth.fhirClient), auth.authHeaders for fetch, or auth.onRefresh(...) to feed fhir-kit-client.

Launching from an EHR instead? See the sister project @fhirstarter/ehr, a turnkey SMART EHR-launch wrapper with Vue/React components.

Contents

Install

npm install @fhirstarter/backend

Usage

This example uses the official fhirclient package as the FHIR client; fhirStarter only manages auth.

import FHIR from "fhirclient"
import fhirStarter from "@fhirstarter/backend"

const auth = await fhirStarter({
   serverUrl: "https://fhir.epic.com/interconnect-fhir-oauth/api/FHIR/R4",
   clientId: "your-client-id",
   privateKey: process.env.FHIR_PRIVATE_KEY!, // base64-encoded PKCS#8 PEM
   tokenEndpointUrl: "https://fhir.epic.com/interconnect-fhir-oauth/oauth2/token",
   scopes: ["system/Patient.rs", "system/Observation.rs"],
})

const client = FHIR.client(auth.fhirClient)

const bundle = await client.request("Patient?family=Smith")

await fhirStarter(config) fetches the first token, starts the proactive refresh loop, and resolves to a ready provider; it rejects if config is invalid or the first token request fails. auth.fhirClient is a ready-to-spread FHIR.client(...) argument built from your serverUrl and a live tokenResponse, so fhirclient always reads the latest token per request.

fhirStarter does not fetch FHIR resources and does not bundle a FHIR client. It manages authentication and refresh; the FHIR client does the rest.

privateKey can be PKCS#8 PEM text, a Buffer, or a base64-encoded PEM string (the preferred form for environment variables). File paths are not supported.

Other FHIR clients

For raw fetch or any HTTP client, spread the current auth headers:

const res = await fetch(`${auth.serverUrl}/Patient?family=Smith`, {
   headers: auth.authHeaders,
})

auth.authHeaders is { Authorization: "Bearer <token>" } when a valid token is cached, or {} otherwise. Read it per request so it always reflects the latest token. auth.accessToken and auth.expiresAt (epoch ms) expose the raw values.

For clients with a bearer token setter (e.g. fhir-kit-client):

const unsubscribe = auth.onRefresh((token) => {
   client.bearerToken = token
})

API

await fhirStarter(config) resolves to a ready provider (no new)

| Member | Returns | Description | |---|---|---| | serverUrl | string | FHIR base URL from config | | accessToken | string \| null | Current valid token, or null if expired | | expiresAt | number \| null | Epoch ms of actual expiry, or null | | token | string \| null | Alias of accessToken | | expiresIn | number \| null | Seconds until actual expiry, or null | | authorizationHeader | string \| null | Bearer <token> or null | | getAccessToken() | Promise<string> | Async valid token with lazy refresh | | applyScopes(scopes) | Promise<string> | Re-acquire with a new scope set now; adopted only on success (refresh is otherwise automatic) | | tokenResponse() | LiveTokenResponse | Getter-backed token response for fhirclient | | fhirClient | FhirClientState | Spread into FHIR.client(...); writable outer, live token | | authHeaders | AuthHeaders | { Authorization } for fetch, or {} when no token | | onRefresh(callback) | () => void | Subscribe to token re-acquisitions; returns unsubscribe | | onRefreshStart(callback) | () => void | Fires when a token request begins | | onRefreshEnd(callback) | () => void | Fires when a token request ends (success or failure) | | onError(callback) | () => void | Fires on failure with a redacted RefreshError | | fhirStarter.thumbprint(privateKey) | string | RFC 7638 JWK Thumbprint (base64url SHA-256) | | fhirStarter.memoryStore() | TokenStore | Single-process reference token store (handy for tests) | | fhirStarter.validate(config) | ValidationResult | Offline config check returning { ok, problems } (no network) | | fhirStarter.getJwks(config) | Promise<JwkSet> | Public JWKS from a private-key config (offline, no token request) |

fhirStarter.getJwks(config) strips private key material; host the output JSON at your registered JWKS URL and pass that URL as jwksUrl so the JWT jku header is set automatically. It runs offline but still expects a complete private-key config.

Thumbprint

Derive a deterministic kid from a private key without instantiating the class:

import fhirStarter from "@fhirstarter/backend"

const kid = fhirStarter.thumbprint(pemOrBuffer)
console.log(kid) // base64url SHA-256 of the canonical public JWK (RSA or EC)

This implements RFC 7638: the SHA-256 of the sorted canonical JWK members ({e, kty, n} for RSA, {crv, kty, x, y} for EC), base64url-encoded. Use it as the keyId when registering your JWKS.

JWKS

Some SMART Backend Services registrations require a public JWKS URL when using jku. Generate it from the same private key you use for auth:

import { writeFileSync } from "node:fs"
import fhirStarter from "@fhirstarter/backend"

// Offline static: no provider, no token request.
const jwks = await fhirStarter.getJwks({
   serverUrl: "https://fhir.example/r4",
   clientId: "your-client-id",
   privateKey: process.env.FHIR_PRIVATE_KEY!, // base64-encoded PKCS#8 PEM
   tokenEndpointUrl: "https://fhir.epic.com/interconnect-fhir-oauth/oauth2/token",
   scopes: ["system/Patient.rs"],
   keyId: "my-key-id",
   jwksUrl: "https://example.com/.well-known/jwks.json",
})
writeFileSync("./jwks.json", JSON.stringify(jwks, null, 3))

Host jwks.json at the exact URL you register with your authorization server, then pass that URL as jwksUrl. If you set keyId, the generated key includes kid, and signed JWTs use the same kid header. If you omit keyId, the kid defaults to the key's RFC 7638 thumbprint.

Key rotation

To rotate without downtime, publish the new key alongside the old one during an overlap window. Set the new key as privateKey (signing switches to it immediately) and list the previous key in retiredKeys so fhirStarter.getJwks(config) keeps publishing its public JWK for verifiers that cached the old JWKS:

const auth = await fhirStarter({
   serverUrl: "https://fhir.example/r4",
   clientId: "your-client-id",
   privateKey: process.env.FHIR_NEW_KEY!,      // active, signs all assertions
   retiredKeys: [process.env.FHIR_OLD_KEY!],   // publish-only, never signs
   tokenEndpointUrl: "https://auth.example/token",
   scopes: ["system/Patient.rs"],
})

Sequence: publish the successor in JWKS → wait through the JWKS cache lifetime → make it active → keep the retired key through the cache lifetime plus the max assertion lifetime (5 min), then drop it from retiredKeys. Each key is published under its own kid (its thumbprint unless overridden).

If a retired key was published under a custom kid while active, pass it as a { key, keyId } pair so fhirStarter.getJwks(config) republishes it under the same kid (otherwise it falls back to the thumbprint):

retiredKeys: [{ key: process.env.FHIR_OLD_KEY!, keyId: "old-kid" }],

Shared token store

By default each provider refreshes independently. When you run several processes under the same client identity, pass a tokenStore so they coordinate: only one process fetches a token at a time (via an owner-scoped lease) and the others adopt the shared result, avoiding refresh storms.

const auth = await fhirStarter({
   serverUrl: "https://fhir.example/r4",
   clientId: "your-client-id",
   privateKey: process.env.FHIR_PRIVATE_KEY!,
   tokenEndpointUrl: "https://auth.example/token",
   scopes: ["system/Patient.rs"],
   tokenStore: myRedisStore, // implements the TokenStore interface
})

fhirStarter.memoryStore() is a single-process reference implementation (handy for tests). For real multi-process coordination, supply a store backed by Redis, a database, or similar. The store contract requires atomic, owner-scoped leases and a setUnderLease that writes only while the caller still holds the lease.

Transport & retries

Token requests retry transient failures with exponential backoff and jitter. Only network errors and HTTP 408/429/5xx are retried; permanent 4xx such as invalid_client or invalid_scope fail immediately. A Retry-After header is honored when present. Each retry builds a fresh JWT assertion (new jti).

Tune via config: timeoutMs (per-attempt, default 30000), maxAttempts (default 3), and backoffMs (base delay, default 500).

Events

onRefresh(cb) fires on every successful token acquisition after the first (including a token adopted from a shared store), not on the initial acquisition or a late subscription. Use it to push tokens into clients that cache them:

auth.onRefresh((token) => (client.bearerToken = token))

onRefreshStart / onRefreshEnd bracket each token acquisition attempt; onError delivers a redacted RefreshError ({ message, status? }) that never contains tokens or secrets. Subscriptions observe attempts after the provider resolves; initial failures reject fhirStarter(config). All four return an unsubscribe function, and listener exceptions never break refresh handling.

Validation

fhirStarter.validate(config) runs a fast, offline check of the config: HTTPS FHIR and token endpoints, non-empty scopes, key parsing, supported algorithm (RS384/ES384), and unique kids across active and retired keys. It makes no network calls and returns { ok, problems }:

const { ok, problems } = fhirStarter.validate(config)
if (!ok) throw new Error(`Invalid config: ${problems.join("; ")}`)

Actual credential/scope acceptance can only be proven by a real token request, so treat a successful await fhirStarter(config) as the true integration check.

Compatibility

fhirClient and tokenResponse() are designed for fhirclient.request(), which reads the token per request. If a client copies the token at construction time instead, use onRefresh() to update or recreate that client. If fhirclient clears its internal state after a 401, recreate the client with auth.fhirClient.

Scripts

| Command | What | |---|---| | npm run check | tsc --noEmit | | npm run build | Compile to dist/ |

Notes

  • await fhirStarter(config) fetches the first token and begins the proactive refresh loop automatically; the resolved provider is ready to use
  • Tokens are cached with separate refresh and expiry timestamps; if a refresh fails but the token is not yet expired, the old token remains usable
  • Concurrent callers share a single in-flight token refresh
  • JWT assertions are signed RS384 (RSA) or ES384 (P-384 EC), expire after 5 minutes
  • Requires Node 20+, a PKCS#8 RSA or P-384 EC key, and SMART Backend Services scopes